diff --git a/packages/studio/tests/e2e/edit-accuracy/case.mjs b/packages/studio/tests/e2e/edit-accuracy/case.mjs index 5fb2dc36d3..9cf4930d9f 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, FIXTURE_CDN, PLAYHEAD, localAsset } from "./grid.mjs"; import { angleOf, centre, @@ -772,6 +772,34 @@ async function nudgeGesture(ctx, pre) { }; } +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) { + 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: `${FIXTURE_CDN}*` }] }); +} + /** * 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 +807,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 6c06792708..df4c5a7522 100644 --- a/packages/studio/tests/e2e/edit-accuracy/grid.mjs +++ b/packages/studio/tests/e2e/edit-accuracy/grid.mjs @@ -1,7 +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 { join } from "node:path"; +import { mkdirSync, statSync, writeFileSync } from "node:fs"; +import { createRequire } from "node:module"; +import { dirname, join } from "node:path"; import { dragCases } from "./drags.mjs"; export const COMPOSITION = { width: 1920, height: 1080 }; @@ -10,7 +11,19 @@ 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); +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 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. 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 0000000000..e10153ae85 --- /dev/null +++ b/packages/studio/tests/e2e/edit-accuracy/grid.test.mjs @@ -0,0 +1,30 @@ +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 { FIXTURE_CDN, 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; +} + +// 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", () => { + const unserved = buildGrid("full") + .flatMap(fixtureUrls) + .filter((url) => !servedLocally(url)); + expect([...new Set(unserved)]).toEqual([]); + }, 30_000); +});