mirror of
https://github.com/heygen-com/hyperframes.git
synced 2026-09-10 04:18:01 +00:00
fix(studio): per-child patch op builders and persist-seam harness (#1909)
* test(studio): add design-panel QA fixture and triage matrix Fixture project covering all panel-editable element archetypes, plus the QA findings matrix from the design-panel bug campaign. * fix(studio): make canvas selection hit intended elements - honor author pointer-events:none in hit-testing (was selecting invisible overlays) - pause playback before mousedown sampling; fall back to hover selection on null resolve - invalidate committed selection when the active composition changes - double-click keeps selection and defers to multi-candidate click cycling * fix(studio): close remaining selection-layer review findings - hoverSelection fallback now wired at all 3 mousedown call sites (box-click, blocked-drag, plain overlay click) instead of just the overlay path - pointer-events override detection reads computed style, not inline style, so a CSS-class opt-in (not just inline style=) on a descendant is honored - defensively remove the pointer-events override before the group-fallback check too, closing a theoretical gap in the no-elementsFromPoint branch - a click that resolves to nothing (dead-zone / deselect) no longer leaves playback paused if it was already playing * fix(studio-server): child-scoped patch operations with batch abort - PatchOperation gains optional childSelector/childIndex resolved under the matched parent - pre-pass resolves every op target; any miss aborts the batch with matched:false, no partial write - style-decl parsing extracted to sourceStyleMutation to stay under the file-size cap - new ./source-mutation subpath export (mirrors ./finite-mutation) * fix(studio): per-child patch op builders and persist-seam harness - buildTextFieldChildLocator indexes over the parent's full same-tag child list - buildTextFieldChildOperations emits per-field ops for same-shape multi-field edits - SDK cutover declines child-scoped batches (hfId mapping would hit the parent) - persist-seam integration harness drives real client ops through patchElementInHtml * fix(studio): fail closed on unresolved text-field child index buildTextFieldChildLocator guessed a synthetic field's position by counting same-tag "child" fields elsewhere in the array whenever sourceChildIndex was absent. That heuristic is unreachable today (the count-mismatch guard in buildTextFieldChildOperations already refuses add/remove edits before it's reached) but would silently locate the wrong element for a future caller that wires up synthetic-field support without also computing a real sourceChildIndex. Return null instead so the caller falls back to the unsupported-structure path.
This commit is contained in:
@@ -0,0 +1,111 @@
|
||||
import { describe, expect, it } from "vitest";
|
||||
import type { DomEditTextField } from "../components/editor/domEditing";
|
||||
import { buildTextFieldChildOperations } from "./domEditTextFieldCommitOps";
|
||||
|
||||
function textField(input: {
|
||||
key: string;
|
||||
value: string;
|
||||
tagName?: string;
|
||||
inlineStyles?: Record<string, string>;
|
||||
source?: DomEditTextField["source"];
|
||||
sourceChildIndex?: number;
|
||||
}): DomEditTextField {
|
||||
return {
|
||||
key: input.key,
|
||||
label: input.key,
|
||||
value: input.value,
|
||||
tagName: input.tagName ?? "span",
|
||||
attributes: [],
|
||||
inlineStyles: input.inlineStyles ?? {},
|
||||
computedStyles: {},
|
||||
source: input.source ?? "child",
|
||||
...(input.sourceChildIndex == null ? {} : { sourceChildIndex: input.sourceChildIndex }),
|
||||
};
|
||||
}
|
||||
|
||||
describe("buildTextFieldChildOperations", () => {
|
||||
it("builds child-scoped text and style operations for changed child fields", () => {
|
||||
const originalFields = [
|
||||
textField({
|
||||
key: "first",
|
||||
value: "First",
|
||||
inlineStyles: { color: "red" },
|
||||
sourceChildIndex: 0,
|
||||
}),
|
||||
textField({
|
||||
key: "second",
|
||||
value: "Second",
|
||||
inlineStyles: { "font-size": "24px" },
|
||||
sourceChildIndex: 1,
|
||||
}),
|
||||
];
|
||||
const nextFields = [
|
||||
originalFields[0],
|
||||
textField({
|
||||
key: "second",
|
||||
value: "Second < &",
|
||||
inlineStyles: { "font-size": "24px", color: "#0000ff" },
|
||||
sourceChildIndex: 1,
|
||||
}),
|
||||
];
|
||||
|
||||
expect(buildTextFieldChildOperations(originalFields, nextFields)).toEqual([
|
||||
{
|
||||
type: "text-content",
|
||||
property: "text",
|
||||
value: "Second < &",
|
||||
childSelector: ":scope > span",
|
||||
childIndex: 1,
|
||||
},
|
||||
{
|
||||
type: "inline-style",
|
||||
property: "color",
|
||||
value: "#0000ff",
|
||||
childSelector: ":scope > span",
|
||||
childIndex: 1,
|
||||
},
|
||||
]);
|
||||
});
|
||||
|
||||
it("emits null for a removed inline style", () => {
|
||||
const originalFields = [
|
||||
textField({
|
||||
key: "first",
|
||||
value: "First",
|
||||
inlineStyles: { color: "red" },
|
||||
sourceChildIndex: 0,
|
||||
}),
|
||||
];
|
||||
const nextFields = [
|
||||
textField({ key: "first", value: "First", inlineStyles: {}, sourceChildIndex: 0 }),
|
||||
];
|
||||
|
||||
expect(buildTextFieldChildOperations(originalFields, nextFields)).toEqual([
|
||||
{
|
||||
type: "inline-style",
|
||||
property: "color",
|
||||
value: null,
|
||||
childSelector: ":scope > span",
|
||||
childIndex: 0,
|
||||
},
|
||||
]);
|
||||
});
|
||||
|
||||
it("returns null for structural changes, reordered fields, and text nodes", () => {
|
||||
const originalFields = [
|
||||
textField({ key: "first", value: "First" }),
|
||||
textField({ key: "second", value: "Second" }),
|
||||
];
|
||||
|
||||
expect(buildTextFieldChildOperations(originalFields, [originalFields[0]])).toBeNull();
|
||||
expect(
|
||||
buildTextFieldChildOperations(originalFields, [originalFields[1], originalFields[0]]),
|
||||
).toBeNull();
|
||||
expect(
|
||||
buildTextFieldChildOperations(originalFields, [
|
||||
originalFields[0],
|
||||
textField({ key: "second", value: "Second", tagName: "#text", source: "text-node" }),
|
||||
]),
|
||||
).toBeNull();
|
||||
});
|
||||
});
|
||||
Reference in New Issue
Block a user