fix(studio): a timeline edit on a clip the file lacks says how the clip was looked up (#6661)

* fix(studio): a timeline edit on a clip the file lacks says how the clip was looked up

* fix(studio): name only the lookups a missing clip was really found by

* refactor(studio): split the missing-clip lookup words into small helpers

* refactor(studio): fit the missing-clip lookup words under the no-visible-change size
This commit is contained in:
Miguel Ángel
2026-10-28 13:30:46 +00:00
committed by GitHub
parent 6486d58021
commit 40c28e10db
2 changed files with 42 additions and 1 deletions
@@ -414,6 +414,31 @@ describe("persistTimelineBatchEdit", () => {
).rejects.toThrow("Unable to patch timeline element ghost in index.html");
expect(writes).toHaveLength(0);
});
it("names which lookups a missing clip had, never their values", async () => {
stubReadFileContent(SOURCE);
// As the timeline builds them: a clip with an id also carries `#id` as its selector.
const missing = (element: Partial<TimelineElement>) => {
const change = moveMember("ghost", 3, 0, 2);
change.element = { ...change.element, ...element };
return persistTimelineBatchEdit(batchInput([change], []));
};
await expect(
missing({
domId: "intro__hf2",
hfId: "hf-zz9q",
selector: "#intro__hf2",
sourceFile: "compositions/intro.html",
}),
).rejects.toThrow("(by hf-id, runtime-renamed id; file from the clip)");
await expect(missing({ selector: "#ghost" })).rejects.toThrow("(by id; no file on the clip)");
await expect(
missing({ domId: undefined, selector: ".caption", selectorIndex: 4 }),
).rejects.toThrow("(by class; no file on the clip)");
await expect(
missing({ domId: undefined, selector: '[data-composition-id="intro__hf2"]' }),
).rejects.toThrow("(by runtime-renamed composition id; no file on the clip)");
});
});
describe("persistTimelineEdit", () => {
@@ -387,6 +387,20 @@ export function operationChanges(
}));
}
const RENAMED_MOUNT = /__hf\d+"?\]?$/;
const lookup = (value: string, kind: string) =>
RENAMED_MOUNT.test(value) ? `runtime-renamed ${kind}` : kind;
const selectorLookup = (selector: string) =>
selector.startsWith("[data-composition-id=")
? lookup(selector, "composition id")
: /^\.[\w-]+$/.test(selector) && "class";
function missedLookups(element: Pick<TimelineElement, "sourceFile">, target: PatchTarget): string {
const kinds = [target.hfId && "hf-id", target.id && lookup(target.id, "id")];
const by = [...kinds, target.selector && selectorLookup(target.selector)].filter(Boolean);
return `by ${by.join(", ") || "nothing"}; ${element.sourceFile ? "file from the clip" : "no file on the clip"}`;
}
/** Patches each change into `source`, failing loudly on a target the file does not hold. */
export function patchTimelineChangesInSource(
source: string,
@@ -399,7 +413,9 @@ export function patchTimelineChangesInSource(
if (!target) throw new Error(`Timeline element ${element.id} is missing a patchable target`);
// Resolve first: a member already at its target values patches to the same string, a missing one must throw.
if (!findTagByTarget(current, target)) {
throw new Error(`Unable to patch timeline element ${element.id} in ${targetPath}`);
throw new Error(
`Unable to patch timeline element ${element.id} in ${targetPath} (${missedLookups(element, target)})`,
);
}
current = buildPatches(current, target);
}