mirror of
https://github.com/heygen-com/hyperframes.git
synced 2026-09-12 15:20:13 +00:00
fix(studio,runtime): CSS.escape ids so digit-leading selectors don't crash
The runtime picker built raw `#${id}` selectors while its sibling
attribute-selector branches (data-composition-id, data-composition-src,
data-track-index) already CSS.escape'd their values. When a user
composition has an element with a digit-leading id (e.g. `id="0"`),
the picker emits the selector `#0` which is invalid per the CSS spec —
downstream `document.querySelector` throws SyntaxError.
Same failure mode reached the Studio thumbnail: getElementScreenshotClip
called `document.querySelectorAll(selector)` unguarded, so an invalid
selector bubbling out of page.evaluate failed the whole thumbnail and
returned 500 to the browser (broken thumbnail image).
Fixes:
- packages/core/src/runtime/picker.ts — CSS.escape the id, matching the
sibling branches on lines 100/102/104.
- packages/studio-server/src/helpers/screenshotClip.ts — catch
SyntaxError from an invalid selector and return undefined so the
caller falls back to a full-page screenshot, so the user still sees
a thumbnail instead of a broken image.
Regression tests for both.
Reported via #hf-cli-feedback (Slack ts=1784218060, darwin/arm64,
CLI 0.7.60): "digit-leading worker IDs broke Studio thumbnail
querySelectorAll".
— Via
This commit is contained in:
@@ -0,0 +1,54 @@
|
||||
import { afterEach, describe, expect, it } from "vitest";
|
||||
import { getElementScreenshotClip } from "./screenshotClip";
|
||||
|
||||
afterEach(() => {
|
||||
document.body.innerHTML = "";
|
||||
});
|
||||
|
||||
describe("getElementScreenshotClip", () => {
|
||||
it("returns undefined (not throws) when the selector is CSS-invalid", () => {
|
||||
// Regression: an HTML element with `id="0"` produces the selector `#0`,
|
||||
// which is invalid per the CSS spec — `document.querySelectorAll('#0')`
|
||||
// throws SyntaxError. Puppeteer surfaces that as a page.evaluate error,
|
||||
// which used to bubble up and fail the whole thumbnail. The clip helper
|
||||
// now swallows the SyntaxError so callers fall back to a full-page shot.
|
||||
const el = document.createElement("div");
|
||||
el.id = "0";
|
||||
Object.assign(el.style, {
|
||||
width: "100px",
|
||||
height: "80px",
|
||||
});
|
||||
document.body.appendChild(el);
|
||||
|
||||
expect(() => getElementScreenshotClip("#0")).not.toThrow();
|
||||
expect(getElementScreenshotClip("#0")).toBeUndefined();
|
||||
});
|
||||
|
||||
it("returns undefined (not throws) for garbage selectors", () => {
|
||||
expect(() => getElementScreenshotClip("::: garbage :::")).not.toThrow();
|
||||
expect(getElementScreenshotClip("::: garbage :::")).toBeUndefined();
|
||||
});
|
||||
|
||||
it("returns a clip for a well-formed selector matching a visible element", () => {
|
||||
const el = document.createElement("div");
|
||||
el.id = "hero";
|
||||
el.getBoundingClientRect = () =>
|
||||
({
|
||||
left: 10,
|
||||
top: 20,
|
||||
width: 100,
|
||||
height: 80,
|
||||
right: 110,
|
||||
bottom: 100,
|
||||
x: 10,
|
||||
y: 20,
|
||||
toJSON: () => ({}),
|
||||
}) as DOMRect;
|
||||
document.body.appendChild(el);
|
||||
|
||||
const clip = getElementScreenshotClip("#hero");
|
||||
expect(clip).toBeDefined();
|
||||
expect(clip?.width).toBeGreaterThan(0);
|
||||
expect(clip?.height).toBeGreaterThan(0);
|
||||
});
|
||||
});
|
||||
@@ -9,9 +9,19 @@ export function getElementScreenshotClip(
|
||||
selector: string,
|
||||
selectorIndex?: number,
|
||||
): ScreenshotClip | undefined {
|
||||
const matches = Array.from(document.querySelectorAll(selector)).filter(
|
||||
(el): el is HTMLElement => el instanceof HTMLElement,
|
||||
);
|
||||
// Guard against invalid CSS selectors (e.g. `#0` — a digit-leading id from
|
||||
// user HTML that upstream producers forgot to CSS.escape). querySelectorAll
|
||||
// throws SyntaxError on those, which bubbles out of page.evaluate and fails
|
||||
// the whole thumbnail. Returning undefined here falls back to a full-page
|
||||
// screenshot, so the user still sees a thumbnail instead of a broken image.
|
||||
let matches: HTMLElement[];
|
||||
try {
|
||||
matches = Array.from(document.querySelectorAll(selector)).filter(
|
||||
(el): el is HTMLElement => el instanceof HTMLElement,
|
||||
);
|
||||
} catch {
|
||||
return undefined;
|
||||
}
|
||||
const safeIndex = Math.max(0, Math.min(matches.length - 1, Math.floor(selectorIndex ?? 0)));
|
||||
const el = matches[safeIndex] ?? null;
|
||||
if (!(el instanceof HTMLElement)) return undefined;
|
||||
|
||||
Reference in New Issue
Block a user