diff --git a/.fallowrc.jsonc b/.fallowrc.jsonc index 948c0d545..2e47f6d43 100644 --- a/.fallowrc.jsonc +++ b/.fallowrc.jsonc @@ -583,6 +583,16 @@ "packages/studio/src/components/editor/PropertyPanel.test.tsx", "packages/studio/src/components/editor/propertyPanelFlatStyleSections.test.tsx", "packages/studio/src/components/editor/propertyPanelFlatMotionSection.test.tsx", + // Thumbnail id-escape fix (via/thumbnail-id-escape): getElementScreenshotClip's + // matches/rect/pad clip-computation body has a pre-existing 19-line clone with + // the inline `page.evaluate` block in vite.browser.ts (the dev-server thumbnail + // path). Adding a try/catch guard around the querySelectorAll call shifts the + // clone's line numbers, re-flagging the inherited duplication. Extracting the + // shared body into a common helper would require the browser-side page.evaluate + // to import from a Node-side module, which puppeteer's serialization boundary + // makes non-trivial. + "packages/studio-server/src/helpers/screenshotClip.ts", + "packages/studio/vite.browser.ts", ], }, "health": { @@ -798,6 +808,16 @@ // `waitForCompletion` is untouched. Line-shift fingerprint re-flags // the inherited complexity. "packages/cli/src/commands/lambda/render.ts", + // Thumbnail id-escape fix (via/thumbnail-id-escape): picker.ts has five + // pre-existing inherited-complexity findings (isEffectivelyHidden, + // isPickableElement, buildElementLabel, getPickCandidatesFromPoint, + // pickManyAtPoint — all in this file at the parent SHA). This PR's only + // logic edit inside picker.ts is a single-line change to buildElementSelector + // (`#${htmlEl.id}` → `#${CSS.escape(htmlEl.id)}`) plus a three-line comment; + // the added lines shift every function below buildElementSelector, and Fallow's + // file-level fingerprint invalidation re-flags the inherited findings even at + // unchanged line numbers. + "packages/core/src/runtime/picker.ts", ], }, } diff --git a/packages/core/src/runtime/picker.test.ts b/packages/core/src/runtime/picker.test.ts index a6a41ac95..e454a101c 100644 --- a/packages/core/src/runtime/picker.test.ts +++ b/packages/core/src/runtime/picker.test.ts @@ -1,32 +1,25 @@ import { describe, it, expect, vi, afterEach, beforeAll } from "vitest"; import { createPickerModule } from "./picker"; -// jsdom does not implement CSS.escape — polyfill with a spec-adjacent version. -// (Parallel polyfills already live in compositionLoader.test.ts / -// startResolver.test.ts, but each test file runs in an isolated environment.) +// jsdom does not implement CSS.escape — polyfill a compact spec-adjacent +// version. Parallel (simpler) polyfills already live in compositionLoader.test.ts +// / startResolver.test.ts, but they don't handle the leading-digit case this +// test needs. Each test file runs in an isolated environment, so we duplicate +// rather than import. beforeAll(() => { const css = globalThis.CSS as { escape?: (input: string) => string } | undefined; if (!css || typeof css.escape !== "function") { (globalThis as { CSS?: { escape: (input: string) => string } }).CSS = { ...(css ?? {}), escape: (value: string) => { - let out = ""; - for (let i = 0; i < value.length; i += 1) { - const ch = value[i] ?? ""; - const code = ch.charCodeAt(0); - const isDigit = code >= 48 && code <= 57; - const isAlpha = (code >= 65 && code <= 90) || (code >= 97 && code <= 122); - const isWordSafe = isAlpha || code === 45 || code === 95 || code >= 128; // - _ non-ASCII - const leadingDigit = i === 0 && isDigit; - if (leadingDigit) { - out += `\\${code.toString(16)} `; - } else if (isDigit || isWordSafe) { - out += ch; - } else { - out += `\\${ch}`; - } + // Non-word chars get a leading backslash (spec-adjacent). + const escaped = value.replace(/([^\w-])/g, "\\$1"); + // A leading digit must be encoded as `\ ` (space terminator) per CSS spec. + const first = value.charCodeAt(0); + if (first >= 48 && first <= 57) { + return `\\${first.toString(16)} ${escaped.slice(1)}`; } - return out; + return escaped; }, }; }