From bbd6a16c742a5794dec0b5d5b7b8fe5fd0598235 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Miguel=20=C3=81ngel?= Date: Sat, 3 Oct 2026 15:19:17 -0400 Subject: [PATCH 1/3] test(studio): serve the edit bench GSAP from the repo, never the network --- .../studio/tests/e2e/edit-accuracy/case.mjs | 20 ++++++++++++- .../studio/tests/e2e/edit-accuracy/grid.mjs | 9 +++++- .../tests/e2e/edit-accuracy/grid.test.mjs | 29 +++++++++++++++++++ 3 files changed, 56 insertions(+), 2 deletions(-) create mode 100644 packages/studio/tests/e2e/edit-accuracy/grid.test.mjs diff --git a/packages/studio/tests/e2e/edit-accuracy/case.mjs b/packages/studio/tests/e2e/edit-accuracy/case.mjs index 5fb2dc36d3a..f94ab1f3b99 100644 --- a/packages/studio/tests/e2e/edit-accuracy/case.mjs +++ b/packages/studio/tests/e2e/edit-accuracy/case.mjs @@ -2,7 +2,7 @@ import { spawn } from "node:child_process"; import { readFileSync } from "node:fs"; import { join } from "node:path"; -import { COMPOSITION, PLAYHEAD } from "./grid.mjs"; +import { COMPOSITION, PLAYHEAD, localAsset } from "./grid.mjs"; import { angleOf, centre, @@ -772,6 +772,23 @@ async function nudgeGesture(ctx, pre) { }; } +/** Fulfils the fixtures' CDN requests from the repo, so a case never depends on the network. */ +async function serveFixtureAssetsLocally(page) { + const cdp = await page.createCDPSession(); + cdp.on("Fetch.requestPaused", ({ requestId, request }) => { + const file = localAsset(request.url); + if (!file) + return void cdp.send("Fetch.failRequest", { requestId, errorReason: "BlockedByClient" }); + void cdp.send("Fetch.fulfillRequest", { + requestId, + responseCode: 200, + responseHeaders: [{ name: "Content-Type", value: "text/javascript" }], + body: readFileSync(file).toString("base64"), + }); + }); + await cdp.send("Fetch.enable", { patterns: [{ urlPattern: "https://cdn.jsdelivr.net/*" }] }); +} + /** * Studio open on the case in a fresh browser context, snapping off, at the case's zoom, target selected; * `drive` measures the rest. A failure keeps a screenshot, and the context always closes. @@ -779,6 +796,7 @@ async function nudgeGesture(ctx, pre) { export async function inStudio({ browser, spec, dir, files, url, evidence }, drive) { const context = await browser.createBrowserContext(); const page = await context.newPage(); + await serveFixtureAssetsLocally(page); const ctx = { page, dir, diff --git a/packages/studio/tests/e2e/edit-accuracy/grid.mjs b/packages/studio/tests/e2e/edit-accuracy/grid.mjs index 6c067927081..6fdd0a8168a 100644 --- a/packages/studio/tests/e2e/edit-accuracy/grid.mjs +++ b/packages/studio/tests/e2e/edit-accuracy/grid.mjs @@ -1,6 +1,7 @@ // The edit accuracy grid: one flat-coloured element in a generated project, crossed with one gesture. // Projects are written to a tmp dir per case; nothing checked in is edited. import { mkdirSync, writeFileSync } from "node:fs"; +import { createRequire } from "node:module"; import { join } from "node:path"; import { dragCases } from "./drags.mjs"; @@ -10,7 +11,13 @@ export const PLAYHEAD = 1; export const TARGET = { width: 240, height: 160, color: "#f0c020" }; export const BACKGROUND = "#202020"; const NESTED_HOST = { left: 160, top: 90, width: 1600, height: 900 }; -const GSAP_CDN = "https://cdn.jsdelivr.net/npm/gsap@3.14.2/dist/gsap.min.js"; +// Fixtures keep the CDN URL users author; the bench serves it from the repo's gsap (`localAsset`). +const require = createRequire(import.meta.url); +const GSAP_CDN = `https://cdn.jsdelivr.net/npm/gsap@${require("gsap/package.json").version}/dist/gsap.min.js`; +const LOCAL_ASSETS = new Map([[GSAP_CDN, require.resolve("gsap/dist/gsap.min.js")]]); + +/** The repo file a fixture URL is served from, so no case waits on the network. */ +export const localAsset = (url) => LOCAL_ASSETS.get(url); // Studio has corner handles only (ResizeHandle is nw|ne|sw|se); its edge strips crop, so there is no edge resize. const GESTURES = ["move", "resize", "rotate", "crop", "nudge"]; diff --git a/packages/studio/tests/e2e/edit-accuracy/grid.test.mjs b/packages/studio/tests/e2e/edit-accuracy/grid.test.mjs new file mode 100644 index 00000000000..4d3142b5c33 --- /dev/null +++ b/packages/studio/tests/e2e/edit-accuracy/grid.test.mjs @@ -0,0 +1,29 @@ +import { existsSync, mkdtempSync, readdirSync, readFileSync, rmSync } from "node:fs"; +import { tmpdir } from "node:os"; +import { join } from "node:path"; +import { describe, expect, it } from "vitest"; +import { buildGrid, localAsset, writeFixture } from "./grid.mjs"; + +function fixtureUrls(spec) { + const dir = mkdtempSync(join(tmpdir(), "edit-bench-urls-")); + writeFixture(spec, dir); + const pages = readdirSync(dir, { recursive: true }).filter((f) => String(f).endsWith(".html")); + const urls = pages.flatMap((f) => + [...readFileSync(join(dir, String(f)), "utf8").matchAll(/https?:\/\/[^"'\s)]+/g)].map( + (m) => m[0], + ), + ); + rmSync(dir, { recursive: true }); + return urls; +} + +const servedLocally = (url) => existsSync(localAsset(url) ?? ""); + +describe("grid fixtures", () => { + it("reference only URLs the bench serves from the repo", () => { + const unserved = buildGrid("full") + .flatMap(fixtureUrls) + .filter((url) => !servedLocally(url)); + expect([...new Set(unserved)]).toEqual([]); + }); +}); From 4a46f7943aaacf1d1fbe62f54661f69ca45492c1 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Miguel=20=C3=81ngel?= Date: Sat, 3 Oct 2026 16:09:10 -0400 Subject: [PATCH 2/3] test(studio): serve the installed gsap dist and never crash the bench on a closed request --- .../studio/tests/e2e/edit-accuracy/case.mjs | 33 ++++++++++++------- .../studio/tests/e2e/edit-accuracy/grid.mjs | 20 +++++++---- .../tests/e2e/edit-accuracy/grid.test.mjs | 9 ++--- 3 files changed, 40 insertions(+), 22 deletions(-) diff --git a/packages/studio/tests/e2e/edit-accuracy/case.mjs b/packages/studio/tests/e2e/edit-accuracy/case.mjs index f94ab1f3b99..9cf4930d9f9 100644 --- a/packages/studio/tests/e2e/edit-accuracy/case.mjs +++ b/packages/studio/tests/e2e/edit-accuracy/case.mjs @@ -2,7 +2,7 @@ import { spawn } from "node:child_process"; import { readFileSync } from "node:fs"; import { join } from "node:path"; -import { COMPOSITION, PLAYHEAD, localAsset } from "./grid.mjs"; +import { COMPOSITION, FIXTURE_CDN, PLAYHEAD, localAsset } from "./grid.mjs"; import { angleOf, centre, @@ -772,21 +772,32 @@ async function nudgeGesture(ctx, pre) { }; } -/** Fulfils the fixtures' CDN requests from the repo, so a case never depends on the network. */ +const blockedCdnUrls = new Set(); + +/** Serves the fixtures' CDN requests from the repo; any other CDN URL is blocked and named once. */ async function serveFixtureAssetsLocally(page) { const cdp = await page.createCDPSession(); cdp.on("Fetch.requestPaused", ({ requestId, request }) => { const file = localAsset(request.url); - if (!file) - return void cdp.send("Fetch.failRequest", { requestId, errorReason: "BlockedByClient" }); - void cdp.send("Fetch.fulfillRequest", { - requestId, - responseCode: 200, - responseHeaders: [{ name: "Content-Type", value: "text/javascript" }], - body: readFileSync(file).toString("base64"), - }); + if (!file) { + if (!blockedCdnUrls.has(request.url)) console.warn(`edit bench: blocked ${request.url}`); + blockedCdnUrls.add(request.url); + cdp + .send("Fetch.failRequest", { requestId, errorReason: "BlockedByClient" }) + .catch(() => undefined); + return; + } + // A request whose frame went away rejects; that must not end the run. + cdp + .send("Fetch.fulfillRequest", { + requestId, + responseCode: 200, + responseHeaders: [{ name: "Content-Type", value: "text/javascript" }], + body: readFileSync(file).toString("base64"), + }) + .catch(() => undefined); }); - await cdp.send("Fetch.enable", { patterns: [{ urlPattern: "https://cdn.jsdelivr.net/*" }] }); + await cdp.send("Fetch.enable", { patterns: [{ urlPattern: `${FIXTURE_CDN}*` }] }); } /** diff --git a/packages/studio/tests/e2e/edit-accuracy/grid.mjs b/packages/studio/tests/e2e/edit-accuracy/grid.mjs index 6fdd0a8168a..2faf86ae23a 100644 --- a/packages/studio/tests/e2e/edit-accuracy/grid.mjs +++ b/packages/studio/tests/e2e/edit-accuracy/grid.mjs @@ -1,8 +1,8 @@ // The edit accuracy grid: one flat-coloured element in a generated project, crossed with one gesture. // Projects are written to a tmp dir per case; nothing checked in is edited. -import { mkdirSync, writeFileSync } from "node:fs"; +import { existsSync, mkdirSync, writeFileSync } from "node:fs"; import { createRequire } from "node:module"; -import { join } from "node:path"; +import { dirname, join } from "node:path"; import { dragCases } from "./drags.mjs"; export const COMPOSITION = { width: 1920, height: 1080 }; @@ -13,11 +13,17 @@ export const BACKGROUND = "#202020"; const NESTED_HOST = { left: 160, top: 90, width: 1600, height: 900 }; // Fixtures keep the CDN URL users author; the bench serves it from the repo's gsap (`localAsset`). const require = createRequire(import.meta.url); -const GSAP_CDN = `https://cdn.jsdelivr.net/npm/gsap@${require("gsap/package.json").version}/dist/gsap.min.js`; -const LOCAL_ASSETS = new Map([[GSAP_CDN, require.resolve("gsap/dist/gsap.min.js")]]); - -/** The repo file a fixture URL is served from, so no case waits on the network. */ -export const localAsset = (url) => LOCAL_ASSETS.get(url); +export const FIXTURE_CDN = "https://cdn.jsdelivr.net/"; +const GSAP_DIST = `${FIXTURE_CDN}npm/gsap@${require("gsap/package.json").version}/dist/`; +const GSAP_CDN = `${GSAP_DIST}gsap.min.js`; +const LOCAL_GSAP_DIST = dirname(require.resolve("gsap/dist/gsap.min.js")); + +/** The repo file a CDN URL is served from (the installed gsap's dist), so no case waits on the network. */ +export function localAsset(url) { + if (!url.startsWith(GSAP_DIST)) return undefined; + const file = join(LOCAL_GSAP_DIST, url.slice(GSAP_DIST.length)); + return existsSync(file) ? file : undefined; +} // Studio has corner handles only (ResizeHandle is nw|ne|sw|se); its edge strips crop, so there is no edge resize. const GESTURES = ["move", "resize", "rotate", "crop", "nudge"]; diff --git a/packages/studio/tests/e2e/edit-accuracy/grid.test.mjs b/packages/studio/tests/e2e/edit-accuracy/grid.test.mjs index 4d3142b5c33..e10153ae855 100644 --- a/packages/studio/tests/e2e/edit-accuracy/grid.test.mjs +++ b/packages/studio/tests/e2e/edit-accuracy/grid.test.mjs @@ -1,8 +1,8 @@ -import { existsSync, mkdtempSync, readdirSync, readFileSync, rmSync } from "node:fs"; +import { mkdtempSync, readdirSync, readFileSync, rmSync } from "node:fs"; import { tmpdir } from "node:os"; import { join } from "node:path"; import { describe, expect, it } from "vitest"; -import { buildGrid, localAsset, writeFixture } from "./grid.mjs"; +import { FIXTURE_CDN, buildGrid, localAsset, writeFixture } from "./grid.mjs"; function fixtureUrls(spec) { const dir = mkdtempSync(join(tmpdir(), "edit-bench-urls-")); @@ -17,7 +17,8 @@ function fixtureUrls(spec) { return urls; } -const servedLocally = (url) => existsSync(localAsset(url) ?? ""); +// Only the CDN host is intercepted, so a mapped URL anywhere else would still reach the network. +const servedLocally = (url) => url.startsWith(FIXTURE_CDN) && localAsset(url) !== undefined; describe("grid fixtures", () => { it("reference only URLs the bench serves from the repo", () => { @@ -25,5 +26,5 @@ describe("grid fixtures", () => { .flatMap(fixtureUrls) .filter((url) => !servedLocally(url)); expect([...new Set(unserved)]).toEqual([]); - }); + }, 30_000); }); From 56c4336681f5bc8bdfe6ba0922718524bab63fc8 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Miguel=20=C3=81ngel?= Date: Sat, 3 Oct 2026 17:28:00 -0400 Subject: [PATCH 3/3] test(studio): serve only regular files from the bench gsap dist --- packages/studio/tests/e2e/edit-accuracy/grid.mjs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/packages/studio/tests/e2e/edit-accuracy/grid.mjs b/packages/studio/tests/e2e/edit-accuracy/grid.mjs index 2faf86ae23a..df4c5a75223 100644 --- a/packages/studio/tests/e2e/edit-accuracy/grid.mjs +++ b/packages/studio/tests/e2e/edit-accuracy/grid.mjs @@ -1,6 +1,6 @@ // The edit accuracy grid: one flat-coloured element in a generated project, crossed with one gesture. // Projects are written to a tmp dir per case; nothing checked in is edited. -import { existsSync, mkdirSync, writeFileSync } from "node:fs"; +import { mkdirSync, statSync, writeFileSync } from "node:fs"; import { createRequire } from "node:module"; import { dirname, join } from "node:path"; import { dragCases } from "./drags.mjs"; @@ -22,7 +22,7 @@ const LOCAL_GSAP_DIST = dirname(require.resolve("gsap/dist/gsap.min.js")); export function localAsset(url) { if (!url.startsWith(GSAP_DIST)) return undefined; const file = join(LOCAL_GSAP_DIST, url.slice(GSAP_DIST.length)); - return existsSync(file) ? file : undefined; + return statSync(file, { throwIfNoEntry: false })?.isFile() ? file : undefined; } // Studio has corner handles only (ResizeHandle is nw|ne|sw|se); its edge strips crop, so there is no edge resize.