diff --git a/packages/sw/tests/ai-template-consumer.browser.mjs b/packages/sw/tests/ai-template-consumer.browser.mjs index b4b1abf..f389a61 100644 --- a/packages/sw/tests/ai-template-consumer.browser.mjs +++ b/packages/sw/tests/ai-template-consumer.browser.mjs @@ -1,14 +1,14 @@ // Browser check of the independent consumer built by ai-template-consumer.mjs. // Usage: node packages/sw/tests/ai-template-consumer.browser.mjs // Serves /dist and checks examples A–D (rendering, assets, dropdowns, callbacks, focus, styles). -import { createRequire } from "node:module" import { createServer } from "node:http" import { mkdir, readFile, writeFile } from "node:fs/promises" import path from "node:path" +import { loadPlaywright } from "./playwright-runtime.mjs" const dist = path.resolve(process.argv[2], "dist") const out = path.resolve(process.argv[3]) -const { chromium } = createRequire(import.meta.url)(process.env.SW_PLAYWRIGHT_PATH ?? "C:/Users/user/.cache/codex-runtimes/codex-primary-runtime/dependencies/node/node_modules/playwright") +const { chromium } = loadPlaywright() await mkdir(out, { recursive: true }) const types = { ".html": "text/html", ".js": "text/javascript", ".css": "text/css" } const server = createServer(async (request, response) => { diff --git a/packages/sw/tests/detail-buttons.browser.mjs b/packages/sw/tests/detail-buttons.browser.mjs index 7979336..c1f70b3 100644 --- a/packages/sw/tests/detail-buttons.browser.mjs +++ b/packages/sw/tests/detail-buttons.browser.mjs @@ -1,10 +1,10 @@ // Historical since 2026-10-19: opens SW Story IDs retired by the unused demo removal (composition, colorize, layer-detail-input; preservation/verify modes). Kept as evidence // for its recorded run; current regression: storybook-unused-removal.browser.mjs. -import { createRequire } from 'node:module' import { mkdir, readFile, writeFile } from 'node:fs/promises' import path from 'node:path' import http from 'node:http' import { swStoryEntries, retainedInventory, retiredPropertyPanelIds } from './storybook-inventory.mjs' +import { loadPlaywright } from './playwright-runtime.mjs' // Usage: node packages/sw/tests/detail-buttons.browser.mjs baseline|verify [base-url] const mode = process.argv[2] ?? 'verify' @@ -16,7 +16,7 @@ if (base === 'static') { server=http.createServer(async(req,res)=>{try{const p=new URL(req.url,'http://localhost').pathname,file=path.resolve(root,'.'+decodeURIComponent(p==='/'?'/index.html':p));if(path.relative(root,file).startsWith('..'))return res.writeHead(403).end();res.setHeader('Content-Type',mime[path.extname(file)]??'application/octet-stream');res.end(await readFile(file))}catch{res.writeHead(404).end()}}) await new Promise(resolve=>server.listen(0,'127.0.0.1',resolve));base=`http://127.0.0.1:${server.address().port}` } -const { chromium } = createRequire(import.meta.url)(process.env.SW_PLAYWRIGHT_PATH ?? 'C:/Users/user/.cache/codex-runtimes/codex-primary-runtime/dependencies/node/node_modules/playwright') +const { chromium } = loadPlaywright() await mkdir(output, { recursive: true }) const browser = await chromium.launch({ headless: true, executablePath: process.env.SW_CHROME_PATH ?? 'C:/Program Files/Google/Chrome/Application/chrome.exe' }) const page = await browser.newPage({ viewport: { width: 1920, height: 1000 } }) diff --git a/packages/sw/tests/dropdown-consolidation.browser.mjs b/packages/sw/tests/dropdown-consolidation.browser.mjs index f814f7c..c45665b 100644 --- a/packages/sw/tests/dropdown-consolidation.browser.mjs +++ b/packages/sw/tests/dropdown-consolidation.browser.mjs @@ -1,12 +1,12 @@ // Build Storybook first. Historical QA is preserved; use a new evidence directory for every run. // Usage: node packages/sw/tests/dropdown-consolidation.browser.mjs [evidence-dir] -import { createRequire } from 'node:module' import { readFile, writeFile, mkdir } from 'node:fs/promises' import { execFileSync } from 'node:child_process' import path from 'node:path' import http from 'node:http' import { parse } from '@babel/parser' import { retiredDropdownIds, swStoryEntries, retainedInventory, inventorySignature } from './storybook-inventory.mjs' +import { loadPlaywright } from './playwright-runtime.mjs' const out = path.resolve(process.argv[2] ?? 'docs/sw/storybook-dropdown-page-qa') await mkdir(out, { recursive: true }) @@ -50,7 +50,7 @@ const server = http.createServer(async (req, res) => { }) await new Promise(resolve => server.listen(0, '127.0.0.1', resolve)) const base = `http://127.0.0.1:${server.address().port}` -const { chromium } = createRequire(import.meta.url)(process.env.SW_PLAYWRIGHT_PATH ?? 'C:/Users/user/.cache/codex-runtimes/codex-primary-runtime/dependencies/node/node_modules/playwright') +const { chromium } = loadPlaywright() let browser try { browser = await chromium.launch({ headless: true, executablePath: process.env.SW_CHROME_PATH ?? 'C:/Program Files/Google/Chrome/Application/chrome.exe' }) diff --git a/packages/sw/tests/followup-four.browser.mjs b/packages/sw/tests/followup-four.browser.mjs index e5ff94a..0fe6238 100644 --- a/packages/sw/tests/followup-four.browser.mjs +++ b/packages/sw/tests/followup-four.browser.mjs @@ -1,12 +1,12 @@ // Historical since 2026-10-19: opens SW Story IDs retired by the unused demo removal (property-bar, additional-hover). Kept as evidence // for its recorded run; current regression: storybook-unused-removal.browser.mjs. // Usage: node packages/sw/tests/followup-four.browser.mjs [storybook-url] (Storybook uses packages/sw/dist: build first) -import { createRequire } from 'node:module' import { mkdir, writeFile } from 'node:fs/promises' import path from 'node:path' +import { loadPlaywright } from './playwright-runtime.mjs' const out = path.resolve(process.argv[2]) const base = process.argv[3] ?? 'http://127.0.0.1:6024' -const { chromium } = createRequire(import.meta.url)(process.env.SW_PLAYWRIGHT_PATH ?? 'C:/Users/user/.cache/codex-runtimes/codex-primary-runtime/dependencies/node/node_modules/playwright') +const { chromium } = loadPlaywright() await mkdir(out, { recursive: true }) const browser = await chromium.launch({ executablePath: process.env.SW_CHROME_PATH ?? 'C:/Program Files/Google/Chrome/Application/chrome.exe' }) const page = await browser.newPage({ viewport: { width: 1920, height: 1000 } }) diff --git a/packages/sw/tests/footer-design-review.browser.mjs b/packages/sw/tests/footer-design-review.browser.mjs index f4caa68..0bc9967 100644 --- a/packages/sw/tests/footer-design-review.browser.mjs +++ b/packages/sw/tests/footer-design-review.browser.mjs @@ -3,12 +3,11 @@ // each state; design judgement (PASS / REVIEW) is written in the report, this script only records facts and checks // the approved, unambiguous rules. // Usage: node packages/sw/tests/footer-design-review.browser.mjs [storybook-url] [evidence-dir] -import { createRequire } from 'node:module' import { mkdir, writeFile } from 'node:fs/promises' import path from 'node:path' +import { runtimeRequire } from './playwright-runtime.mjs' -const runtime = process.env.SW_RUNTIME_MODULES ?? 'C:/Users/user/.cache/codex-runtimes/codex-primary-runtime/dependencies/node/node_modules' -const require = createRequire(path.join(runtime, 'index.js')) +const require = runtimeRequire() const { chromium } = require('playwright') const [storybook = 'http://127.0.0.1:6024', outArg = 'docs/sw/sw-noto-bold-qa/footer-design'] = process.argv.slice(2) const out = path.resolve(outArg); await mkdir(out, { recursive: true }) diff --git a/packages/sw/tests/footer-popup-viewer-refinement.browser.mjs b/packages/sw/tests/footer-popup-viewer-refinement.browser.mjs index 17c8ab4..b0182ad 100644 --- a/packages/sw/tests/footer-popup-viewer-refinement.browser.mjs +++ b/packages/sw/tests/footer-popup-viewer-refinement.browser.mjs @@ -1,12 +1,11 @@ // 2026-10-20 SW Footer · Popup · Viewer refinement: Footer Story cleanup, Header field dropdowns inside SWPopup, // SWSlider appearances (panel / property), Popup B, viewer title art at narrow widths. // Usage: build Storybook first, then node packages/sw/tests/footer-popup-viewer-refinement.browser.mjs [evidence-dir] -import { createRequire } from 'node:module' import { readFile, writeFile, mkdir } from 'node:fs/promises' import path from 'node:path' import http from 'node:http' -const runtime = process.env.SW_RUNTIME_MODULES ?? 'C:/Users/user/.cache/codex-runtimes/codex-primary-runtime/dependencies/node/node_modules' -const require = createRequire(path.join(runtime, 'index.js')) +import { runtimeRequire } from './playwright-runtime.mjs' +const require = runtimeRequire() const { chromium } = require('playwright') const out = path.resolve(process.argv[2] ?? 'docs/sw/footer-popup-viewer-refinement-qa'); await mkdir(out, { recursive: true }) const root = path.resolve('storybook-static'), mime = { '.html': 'text/html', '.js': 'text/javascript', '.css': 'text/css', '.json': 'application/json', '.svg': 'image/svg+xml', '.png': 'image/png', '.ttf': 'font/ttf', '.woff2': 'font/woff2' } diff --git a/packages/sw/tests/home-menu-integration.browser.mjs b/packages/sw/tests/home-menu-integration.browser.mjs index 09a5c97..94754c9 100644 --- a/packages/sw/tests/home-menu-integration.browser.mjs +++ b/packages/sw/tests/home-menu-integration.browser.mjs @@ -1,12 +1,11 @@ // Core checks of the Task A + B integration (SWHeader homeMenu, 2026-10-19) on the built Storybook. // Usage: node packages/sw/tests/home-menu-integration.browser.mjs (build Storybook first) -import { createRequire } from 'node:module' import { mkdir, readFile, writeFile } from 'node:fs/promises' import path from 'node:path' import http from 'node:http' +import { runtimeRequire } from './playwright-runtime.mjs' -const runtime = process.env.SW_RUNTIME_MODULES ?? 'C:/Users/user/.cache/codex-runtimes/codex-primary-runtime/dependencies/node/node_modules' -const require = createRequire(path.join(runtime, 'index.js')) +const require = runtimeRequire() const out = path.resolve(process.argv[2] ?? 'docs/sw/home-menu-integration-qa/home-menu') await mkdir(out, { recursive: true }) const root = path.resolve('storybook-static'), mime = { '.html': 'text/html', '.js': 'text/javascript', '.css': 'text/css', '.json': 'application/json', '.svg': 'image/svg+xml', '.png': 'image/png', '.ttf': 'font/ttf', '.woff2': 'font/woff2' } diff --git a/packages/sw/tests/hover-stability.browser.mjs b/packages/sw/tests/hover-stability.browser.mjs index 8b0671e..5e4f37e 100644 --- a/packages/sw/tests/hover-stability.browser.mjs +++ b/packages/sw/tests/hover-stability.browser.mjs @@ -4,12 +4,11 @@ // including the ▲ panel). Also: SWHeader field values are Noto Sans KR 400 and not cut; long footer commands end in an // ellipsis inside the unchanged 68 × 47 slot and show their full name on hover. // Usage: build Storybook first, then node packages/sw/tests/hover-stability.browser.mjs [evidence-dir] [chromium|firefox] -import { createRequire } from 'node:module' import { readFile, writeFile, mkdir } from 'node:fs/promises' import path from 'node:path' import http from 'node:http' -const runtime = process.env.SW_RUNTIME_MODULES ?? 'C:/Users/user/.cache/codex-runtimes/codex-primary-runtime/dependencies/node/node_modules' -const require = createRequire(path.join(runtime, 'index.js')) +import { runtimeRequire } from './playwright-runtime.mjs' +const require = runtimeRequire() const playwright = require('playwright') const out = path.resolve(process.argv[2] ?? 'docs/sw/hover-stability-qa'); await mkdir(out, { recursive: true }) const engine = process.argv[3] ?? 'chromium' diff --git a/packages/sw/tests/integrated-consumer.browser.mjs b/packages/sw/tests/integrated-consumer.browser.mjs index 73c2a32..8efb236 100644 --- a/packages/sw/tests/integrated-consumer.browser.mjs +++ b/packages/sw/tests/integrated-consumer.browser.mjs @@ -1,14 +1,14 @@ // Browser check of the consumer built by integrated-consumer.mjs (2026-10-19): both stylesheet orders, examples // E (custom popup) and F (Open + tool bar), HM UI inside an SW popup, Escape stack, computed-style identity. // Usage: node packages/sw/tests/integrated-consumer.browser.mjs -import { createRequire } from "node:module" import { createServer } from "node:http" import { mkdir, readFile, writeFile } from "node:fs/promises" import path from "node:path" +import { loadPlaywright } from "./playwright-runtime.mjs" const dist = path.resolve(process.argv[2], "dist") const out = path.resolve(process.argv[3]) -const { chromium } = createRequire(import.meta.url)(process.env.SW_PLAYWRIGHT_PATH ?? "C:/Users/user/.cache/codex-runtimes/codex-primary-runtime/dependencies/node/node_modules/playwright") +const { chromium } = loadPlaywright() await mkdir(out, { recursive: true }) const types = { ".html": "text/html", ".js": "text/javascript", ".css": "text/css", ".ttf": "font/ttf" } const server = createServer(async (request, response) => { diff --git a/packages/sw/tests/integrated-improvement.browser.mjs b/packages/sw/tests/integrated-improvement.browser.mjs index 2629b0d..ad9ce38 100644 --- a/packages/sw/tests/integrated-improvement.browser.mjs +++ b/packages/sw/tests/integrated-improvement.browser.mjs @@ -1,13 +1,12 @@ // Functional checks of the 2026-10-19 SW integrated improvement on the built Storybook (storybook-static). // Usage: node packages/sw/tests/integrated-improvement.browser.mjs [section ...] // Sections: options, tool, popup (default: all). Screenshots and result JSON go to . -import { createRequire } from 'node:module' import { mkdir, readFile, writeFile } from 'node:fs/promises' import path from 'node:path' import http from 'node:http' +import { runtimeRequire } from './playwright-runtime.mjs' -const runtime = process.env.SW_RUNTIME_MODULES ?? 'C:/Users/user/.cache/codex-runtimes/codex-primary-runtime/dependencies/node/node_modules' -const require = createRequire(path.join(runtime, 'index.js')) +const require = runtimeRequire() const out = path.resolve(process.argv[2] ?? 'docs/sw/integrated-improvement-qa/verify') // 'tool' opens sw-tool--tool-buttons, retired 2026-10-19 (storybook-inventory retiredToolIds): historical, run it only // by name against an old build. The default runs the sections whose Stories exist (2026-10-20). diff --git a/packages/sw/tests/integrated-visual.browser.mjs b/packages/sw/tests/integrated-visual.browser.mjs index e8cc3a8..f7a0c05 100644 --- a/packages/sw/tests/integrated-visual.browser.mjs +++ b/packages/sw/tests/integrated-visual.browser.mjs @@ -3,13 +3,12 @@ // compare: pixel-diffs two capture directories and writes compare-result.json. // Usage: node packages/sw/tests/integrated-visual.browser.mjs capture // node packages/sw/tests/integrated-visual.browser.mjs compare -import { createRequire } from 'node:module' import { mkdir, readFile, readdir, writeFile } from 'node:fs/promises' import path from 'node:path' import http from 'node:http' +import { runtimeRequire } from './playwright-runtime.mjs' -const runtime = process.env.SW_RUNTIME_MODULES ?? 'C:/Users/user/.cache/codex-runtimes/codex-primary-runtime/dependencies/node/node_modules' -const require = createRequire(path.join(runtime, 'index.js')) +const require = runtimeRequire() const [mode, a, b, c] = process.argv.slice(2) // Stories that existed before the improvement; IDs are fixed so new Stories never enter the comparison. diff --git a/packages/sw/tests/overview.browser.mjs b/packages/sw/tests/overview.browser.mjs index cff53de..5b60cc2 100644 --- a/packages/sw/tests/overview.browser.mjs +++ b/packages/sw/tests/overview.browser.mjs @@ -1,12 +1,12 @@ // Focused Overview integration checks. Build Storybook first; historical evidence stays untouched. // Usage: node packages/sw/tests/overview.browser.mjs [evidence-dir] -import { createRequire } from 'node:module' import { readFile, writeFile, mkdir } from 'node:fs/promises' import { execFileSync } from 'node:child_process' import path from 'node:path' import http from 'node:http' import { parse } from '@babel/parser' import { swStoryEntries, retainedInventory, inventorySignature, retiredDropdownIds, integratedChangedFiles } from './storybook-inventory.mjs' +import { loadPlaywright } from './playwright-runtime.mjs' const out = path.resolve(process.argv[2] ?? 'docs/sw/storybook-overview-qa') await mkdir(out, { recursive: true }) @@ -37,7 +37,7 @@ const server = http.createServer(async(req,res) => { }) await new Promise(resolve => server.listen(0,'127.0.0.1',resolve)) const base = `http://127.0.0.1:${server.address().port}` -const { chromium } = createRequire(import.meta.url)(process.env.SW_PLAYWRIGHT_PATH ?? 'C:/Users/user/.cache/codex-runtimes/codex-primary-runtime/dependencies/node/node_modules/playwright') +const { chromium } = loadPlaywright() let browser, page try { browser = await chromium.launch({ headless:true, executablePath:process.env.SW_CHROME_PATH ?? 'C:/Program Files/Google/Chrome/Application/chrome.exe' }) diff --git a/packages/sw/tests/playwright-runtime.mjs b/packages/sw/tests/playwright-runtime.mjs new file mode 100644 index 0000000..44db071 --- /dev/null +++ b/packages/sw/tests/playwright-runtime.mjs @@ -0,0 +1,76 @@ +// Playwright (and the image helpers pngjs / pixelmatch) for the SW browser tests. Playwright is not a repository +// dependency, so the first location that has it is used: +// 1. SW_PLAYWRIGHT_PATH the playwright package directory (or its entry file) +// 2. SW_RUNTIME_MODULES a node_modules directory that contains playwright +// 3. HM_PLAYWRIGHT_DIR a project directory whose node_modules contains playwright (scripts/verify-quality-runtime.mjs) +// 4. this repository node_modules/playwright (npm i -D playwright, not committed) +// 5. the Codex runtime /.cache/codex-runtimes/codex-primary-runtime/dependencies/node/node_modules +// 6. global npm `npm root -g` +// A location set by an environment variable must exist: a wrong value fails instead of falling back silently. +import { createRequire } from 'node:module' +import { execFileSync } from 'node:child_process' +import { existsSync } from 'node:fs' +import os from 'node:os' +import path from 'node:path' + +const nodeModulesOf = file => { + const parts = path.resolve(file).split(path.sep) + const index = parts.lastIndexOf('node_modules') + return index < 0 ? null : parts.slice(0, index + 1).join(path.sep) +} +const fromNodeModules = dir => createRequire(path.join(dir, 'index.js')) +const globalRoot = () => { + try { return execFileSync('npm', ['root', '-g'], { encoding: 'utf8', shell: process.platform === 'win32', stdio: ['ignore', 'pipe', 'ignore'] }).trim() } catch { return null } +} + +function candidates() { + const list = [] + const { SW_PLAYWRIGHT_PATH: entry, SW_RUNTIME_MODULES: modules, HM_PLAYWRIGHT_DIR: project } = process.env + if (entry) { + const modulesDir = nodeModulesOf(entry) + list.push({ source: `SW_PLAYWRIGHT_PATH=${entry}`, explicit: true, entry: path.resolve(entry), require: modulesDir ? fromNodeModules(modulesDir) : createRequire(import.meta.url) }) + } + if (modules) list.push({ source: `SW_RUNTIME_MODULES=${modules}`, explicit: true, require: fromNodeModules(path.resolve(modules)) }) + if (project) list.push({ source: `HM_PLAYWRIGHT_DIR=${project}`, explicit: true, require: createRequire(path.join(path.resolve(project), 'package.json')) }) + list.push({ source: 'repository node_modules', require: createRequire(import.meta.url) }) + const codex = path.join(os.homedir(), '.cache', 'codex-runtimes', 'codex-primary-runtime', 'dependencies', 'node', 'node_modules') + if (existsSync(codex)) list.push({ source: `Codex runtime ${codex}`, require: fromNodeModules(codex) }) + const global = globalRoot() + if (global && existsSync(global)) list.push({ source: `global npm ${global}`, require: fromNodeModules(global) }) + return list +} + +let resolved +function resolve() { + if (resolved) return resolved + const tried = [] + for (const candidate of candidates()) { + try { + const entry = candidate.entry ?? candidate.require.resolve('playwright') + if (candidate.entry && !existsSync(candidate.entry)) throw new Error('path does not exist') + resolved = { ...candidate, entry } + return resolved + } catch (error) { + tried.push(`${candidate.source}: ${error.message.split('\n')[0]}`) + if (candidate.explicit) break + } + } + throw new Error(`Playwright not found for the SW browser tests. Set SW_RUNTIME_MODULES to a node_modules directory that contains playwright (or SW_PLAYWRIGHT_PATH to the playwright package).\n tried:\n - ${tried.join('\n - ')}`) +} + +/** The playwright module (`{ chromium, firefox, ... }`). */ +export const loadPlaywright = () => resolve().require(resolve().entry) + +/** + * A `require` rooted where Playwright was found, for the other runtime modules the tests use (pngjs, pixelmatch). + * `require('playwright')` through it returns the same module as `loadPlaywright()`. + */ +export function runtimeRequire() { + const { require, entry } = resolve() + const wrapped = id => (id === 'playwright' ? require(entry) : require(id)) + wrapped.resolve = (id, options) => (id === 'playwright' ? entry : require.resolve(id, options)) + return wrapped +} + +/** Where Playwright came from (for reports). */ +export const playwrightSource = () => resolve().source diff --git a/packages/sw/tests/property-close.browser.mjs b/packages/sw/tests/property-close.browser.mjs index a6edfa7..05e5c81 100644 --- a/packages/sw/tests/property-close.browser.mjs +++ b/packages/sw/tests/property-close.browser.mjs @@ -3,13 +3,13 @@ // Property panel close button (Figma 26:12366): canvas bleed, Default / Hover, close by click and Escape. // Usage: node packages/sw/tests/property-close.browser.mjs