From 3cd8683aa2610bd4548e672e488256cb3e208b63 Mon Sep 17 00:00:00 2001 From: Cursor Agent Date: Tue, 25 Aug 2026 17:28:22 +0000 Subject: [PATCH 1/6] Cache Playwright browsers in CI and stub SVG collection Cache ~/.cache/ms-playwright by OS and @playwright/test version so browser install is skipped on hit, with a 15-minute timeout on miss. On cache hit, still install Chromium OS deps. Also stub Vite .svg imports during Playwright collection so CI can get past Unknown file extension errors that blocked the #31 merge deploy. Co-authored-by: Pat Needham --- .github/workflows/deploy.yml | 22 ++++++++++++++++++++++ tests/svg-esm-loader.mjs | 34 ++++++++++++++++++++++++++++++++++ 2 files changed, 56 insertions(+) create mode 100644 tests/svg-esm-loader.mjs diff --git a/.github/workflows/deploy.yml b/.github/workflows/deploy.yml index c09766d..b45ef27 100644 --- a/.github/workflows/deploy.yml +++ b/.github/workflows/deploy.yml @@ -109,9 +109,27 @@ jobs: - name: πŸ“₯ Download deps uses: bahmutov/npm-install@v1 + - name: πŸ“₯ Get Playwright version + id: playwright-version + run: echo "version=$(node -e "console.log(require('@playwright/test/package.json').version)")" >> "$GITHUB_OUTPUT" + + - name: 🏦 Cache Playwright browsers + id: playwright-cache + uses: actions/cache@v4 + with: + path: ~/.cache/ms-playwright + key: playwright-${{ runner.os }}-${{ steps.playwright-version.outputs.version }} + - name: πŸ“₯ Install Playwright Browsers + if: steps.playwright-cache.outputs.cache-hit != 'true' + timeout-minutes: 15 run: npm run test:e2e:install + - name: πŸ“₯ Install Playwright OS deps + if: steps.playwright-cache.outputs.cache-hit == 'true' + timeout-minutes: 10 + run: npx playwright install-deps chromium + - name: πŸ›  Setup Database run: npx prisma migrate deploy && npx prisma generate --sql @@ -133,6 +151,10 @@ jobs: run: npm run build - name: 🎭 Playwright tests + env: + # Node cannot load Vite SVG assets during Playwright collection on CI + # (`Unknown file extension ".svg"`). Stub them via a custom ESM loader. + NODE_OPTIONS: --import ./tests/svg-esm-loader.mjs run: npx playwright test - name: πŸ“Š Upload report diff --git a/tests/svg-esm-loader.mjs b/tests/svg-esm-loader.mjs new file mode 100644 index 0000000..e7a582a --- /dev/null +++ b/tests/svg-esm-loader.mjs @@ -0,0 +1,34 @@ +/** + * Playwright collection imports app modules that Vite-resolve `.svg` assets. + * Node cannot load those files as ESM (`Unknown file extension ".svg"`), so + * stub them as an empty default export. + */ +const svgUrlPattern = /\.svg(\?.*)?$/ + +export const resolve = (specifier, context, nextResolve) => { + if (!svgUrlPattern.test(specifier)) { + return nextResolve(specifier, context) + } + + const url = specifier.startsWith('file:') + ? specifier + : new URL(specifier, context.parentURL).href + + return { + shortCircuit: true, + url, + format: 'module', + } +} + +export const load = (url, context, nextLoad) => { + if (!svgUrlPattern.test(url)) { + return nextLoad(url, context) + } + + return { + format: 'module', + shortCircuit: true, + source: 'export default ""', + } +} From db0213d1c9438bc87858ef035bd7735e636cd6d4 Mon Sep 17 00:00:00 2001 From: Cursor Agent Date: Tue, 25 Aug 2026 17:49:19 +0000 Subject: [PATCH 2/6] Match Epic Stack Playwright config instead of stubbing SVGs Drop build.external, the Babel SVG transform, and the ESM loader. Those extras caused GHA collection to fail with Unknown file extension .svg; Epic Stack does not stub this and their config does not externalize SVG. Keep the versioned Playwright browser cache. Co-authored-by: Pat Needham --- .github/workflows/deploy.yml | 4 ---- playwright.config.ts | 10 +--------- tests/svg-esm-loader.mjs | 34 ---------------------------------- tests/svg-import-stub.cjs | 34 ---------------------------------- 4 files changed, 1 insertion(+), 81 deletions(-) delete mode 100644 tests/svg-esm-loader.mjs delete mode 100644 tests/svg-import-stub.cjs diff --git a/.github/workflows/deploy.yml b/.github/workflows/deploy.yml index b45ef27..25c562a 100644 --- a/.github/workflows/deploy.yml +++ b/.github/workflows/deploy.yml @@ -151,10 +151,6 @@ jobs: run: npm run build - name: 🎭 Playwright tests - env: - # Node cannot load Vite SVG assets during Playwright collection on CI - # (`Unknown file extension ".svg"`). Stub them via a custom ESM loader. - NODE_OPTIONS: --import ./tests/svg-esm-loader.mjs run: npx playwright test - name: πŸ“Š Upload report diff --git a/playwright.config.ts b/playwright.config.ts index ceb3e3d..8483115 100644 --- a/playwright.config.ts +++ b/playwright.config.ts @@ -1,12 +1,6 @@ import { defineConfig, devices } from '@playwright/test' import 'dotenv/config' -process.env.PW_TEST_SOURCE_TRANSFORM = new URL( - './tests/svg-import-stub.cjs', - import.meta.url, -).pathname -process.env.PW_TEST_SOURCE_TRANSFORM_SCOPE = process.cwd() - const PORT = process.env.PORT || '3000' export default defineConfig({ @@ -20,9 +14,6 @@ export default defineConfig({ retries: process.env.CI ? 2 : 0, workers: process.env.CI ? 1 : undefined, reporter: 'html', - build: { - external: ['**/*.svg'], - }, use: { baseURL: `http://localhost:${PORT}/`, trace: 'on-first-retry', @@ -40,6 +31,7 @@ export default defineConfig({ webServer: { command: process.env.CI ? 'npm run start:mocks' : 'npm run dev', port: Number(PORT), + timeout: 60 * 1000, reuseExistingServer: true, stdout: 'pipe', stderr: 'pipe', diff --git a/tests/svg-esm-loader.mjs b/tests/svg-esm-loader.mjs deleted file mode 100644 index e7a582a..0000000 --- a/tests/svg-esm-loader.mjs +++ /dev/null @@ -1,34 +0,0 @@ -/** - * Playwright collection imports app modules that Vite-resolve `.svg` assets. - * Node cannot load those files as ESM (`Unknown file extension ".svg"`), so - * stub them as an empty default export. - */ -const svgUrlPattern = /\.svg(\?.*)?$/ - -export const resolve = (specifier, context, nextResolve) => { - if (!svgUrlPattern.test(specifier)) { - return nextResolve(specifier, context) - } - - const url = specifier.startsWith('file:') - ? specifier - : new URL(specifier, context.parentURL).href - - return { - shortCircuit: true, - url, - format: 'module', - } -} - -export const load = (url, context, nextLoad) => { - if (!svgUrlPattern.test(url)) { - return nextLoad(url, context) - } - - return { - format: 'module', - shortCircuit: true, - source: 'export default ""', - } -} diff --git a/tests/svg-import-stub.cjs b/tests/svg-import-stub.cjs deleted file mode 100644 index 0d024ea..0000000 --- a/tests/svg-import-stub.cjs +++ /dev/null @@ -1,34 +0,0 @@ -/** - * Playwright compiles TS/TSX with Babel. Vite SVG asset imports are not - * valid JS, so rewrite them to a string stub during e2e test collection. - */ -module.exports = function svgImportStub({ types: t }) { - return { - name: 'svg-import-stub', - visitor: { - ImportDeclaration(path) { - const source = path.node.source.value - if ( - typeof source !== 'string' || - !source.split('?')[0].endsWith('.svg') - ) { - return - } - - const defaultSpecifier = path.node.specifiers.find((specifier) => - t.isImportDefaultSpecifier(specifier), - ) - if (!defaultSpecifier) { - path.remove() - return - } - - path.replaceWith( - t.variableDeclaration('const', [ - t.variableDeclarator(defaultSpecifier.local, t.stringLiteral('')), - ]), - ) - }, - }, - } -} From 582e96fc654baedc1a1df5f6a8b24e376d9cb654 Mon Sep 17 00:00:00 2001 From: Cursor Agent Date: Tue, 25 Aug 2026 17:52:47 +0000 Subject: [PATCH 3/6] Restore Babel SVG stub without build.external MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Matching Epic Stack’s playwright.config on Playwright 1.51 made GHA parse sprite.svg as JS (SyntaxError). Keep the #24 Babel rewrite so collection never loads the SVG file, but do not mark **/*.svg as external (that caused Unknown file extension .svg). Cache unchanged. Co-authored-by: Pat Needham --- playwright.config.ts | 7 +++++++ tests/svg-import-stub.cjs | 34 ++++++++++++++++++++++++++++++++++ 2 files changed, 41 insertions(+) create mode 100644 tests/svg-import-stub.cjs diff --git a/playwright.config.ts b/playwright.config.ts index 8483115..9bd7502 100644 --- a/playwright.config.ts +++ b/playwright.config.ts @@ -1,6 +1,13 @@ +import path from 'node:path' import { defineConfig, devices } from '@playwright/test' import 'dotenv/config' +process.env.PW_TEST_SOURCE_TRANSFORM = path.join( + process.cwd(), + 'tests/svg-import-stub.cjs', +) +process.env.PW_TEST_SOURCE_TRANSFORM_SCOPE = process.cwd() + const PORT = process.env.PORT || '3000' export default defineConfig({ diff --git a/tests/svg-import-stub.cjs b/tests/svg-import-stub.cjs new file mode 100644 index 0000000..0d024ea --- /dev/null +++ b/tests/svg-import-stub.cjs @@ -0,0 +1,34 @@ +/** + * Playwright compiles TS/TSX with Babel. Vite SVG asset imports are not + * valid JS, so rewrite them to a string stub during e2e test collection. + */ +module.exports = function svgImportStub({ types: t }) { + return { + name: 'svg-import-stub', + visitor: { + ImportDeclaration(path) { + const source = path.node.source.value + if ( + typeof source !== 'string' || + !source.split('?')[0].endsWith('.svg') + ) { + return + } + + const defaultSpecifier = path.node.specifiers.find((specifier) => + t.isImportDefaultSpecifier(specifier), + ) + if (!defaultSpecifier) { + path.remove() + return + } + + path.replaceWith( + t.variableDeclaration('const', [ + t.variableDeclarator(defaultSpecifier.local, t.stringLiteral('')), + ]), + ) + }, + }, + } +} From 50918fd62fc13a2985de30f78954de39682a1e9e Mon Sep 17 00:00:00 2001 From: Cursor Agent Date: Tue, 25 Aug 2026 17:56:12 +0000 Subject: [PATCH 4/6] Send SVG Babel stub to Playwright's ESM loader thread PW_TEST_SOURCE_TRANSFORM set inside playwright.config.ts never reached the loader thread on GHA, so collection still parsed sprite.svg as JS. Pass the plugin via '@playwright/test'.babelPlugins (copied over the loader channel) and set the env vars on the workflow test step before Playwright starts. Cache unchanged. Co-authored-by: Pat Needham --- .github/workflows/deploy.yml | 3 +++ playwright.config.ts | 18 +++++++++++++----- 2 files changed, 16 insertions(+), 5 deletions(-) diff --git a/.github/workflows/deploy.yml b/.github/workflows/deploy.yml index 25c562a..66cb648 100644 --- a/.github/workflows/deploy.yml +++ b/.github/workflows/deploy.yml @@ -151,6 +151,9 @@ jobs: run: npm run build - name: 🎭 Playwright tests + env: + PW_TEST_SOURCE_TRANSFORM: ${{ github.workspace }}/tests/svg-import-stub.cjs + PW_TEST_SOURCE_TRANSFORM_SCOPE: ${{ github.workspace }} run: npx playwright test - name: πŸ“Š Upload report diff --git a/playwright.config.ts b/playwright.config.ts index 9bd7502..4002ac3 100644 --- a/playwright.config.ts +++ b/playwright.config.ts @@ -2,15 +2,16 @@ import path from 'node:path' import { defineConfig, devices } from '@playwright/test' import 'dotenv/config' -process.env.PW_TEST_SOURCE_TRANSFORM = path.join( - process.cwd(), - 'tests/svg-import-stub.cjs', -) +const svgImportStubPath = path.join(process.cwd(), 'tests/svg-import-stub.cjs') + +// Playwright's ESM loader thread does not see env vars set after process start, +// so also pass the plugin via babelPlugins (copied onto the loader channel). +process.env.PW_TEST_SOURCE_TRANSFORM = svgImportStubPath process.env.PW_TEST_SOURCE_TRANSFORM_SCOPE = process.cwd() const PORT = process.env.PORT || '3000' -export default defineConfig({ +const playwrightConfig = defineConfig({ testDir: './tests/e2e', timeout: 15 * 1000, expect: { @@ -48,3 +49,10 @@ export default defineConfig({ }, }, }) + +export default { + ...playwrightConfig, + '@playwright/test': { + babelPlugins: [[svgImportStubPath]], + }, +} From 5f4714cab8a58a22d3be21b3147cf7d0eb1d3573 Mon Sep 17 00:00:00 2001 From: Cursor Agent Date: Tue, 25 Aug 2026 17:59:09 +0000 Subject: [PATCH 5/6] Use a single SVG Babel stub (drop duplicate babelPlugins) GHA already injects PW_TEST_SOURCE_TRANSFORM before Playwright starts. Also passing it as babelPlugins made Babel error: Duplicate plugin/preset. Keep the workflow env; config only sets the vars if they are unset. Co-authored-by: Pat Needham --- playwright.config.ts | 15 +++------------ 1 file changed, 3 insertions(+), 12 deletions(-) diff --git a/playwright.config.ts b/playwright.config.ts index 4002ac3..ebbf0c5 100644 --- a/playwright.config.ts +++ b/playwright.config.ts @@ -4,14 +4,12 @@ import 'dotenv/config' const svgImportStubPath = path.join(process.cwd(), 'tests/svg-import-stub.cjs') -// Playwright's ESM loader thread does not see env vars set after process start, -// so also pass the plugin via babelPlugins (copied onto the loader channel). -process.env.PW_TEST_SOURCE_TRANSFORM = svgImportStubPath -process.env.PW_TEST_SOURCE_TRANSFORM_SCOPE = process.cwd() +process.env.PW_TEST_SOURCE_TRANSFORM ??= svgImportStubPath +process.env.PW_TEST_SOURCE_TRANSFORM_SCOPE ??= process.cwd() const PORT = process.env.PORT || '3000' -const playwrightConfig = defineConfig({ +export default defineConfig({ testDir: './tests/e2e', timeout: 15 * 1000, expect: { @@ -49,10 +47,3 @@ const playwrightConfig = defineConfig({ }, }, }) - -export default { - ...playwrightConfig, - '@playwright/test': { - babelPlugins: [[svgImportStubPath]], - }, -} From bd0c8f3ec049608577adea5d09f95dc65341828e Mon Sep 17 00:00:00 2001 From: Cursor Agent Date: Tue, 25 Aug 2026 18:02:15 +0000 Subject: [PATCH 6/6] Disable Node type stripping for Playwright 1.51 on GHA SVG collection is past the sprite SyntaxError. Node 22.18+ then fails on constructor parameter properties (tests/pom/base/page.pom.ts) in strip-only mode. Set NODE_OPTIONS=--no-experimental-strip-types on the test step so Playwright's Babel transform runs instead. Co-authored-by: Pat Needham --- .github/workflows/deploy.yml | 3 +++ 1 file changed, 3 insertions(+) diff --git a/.github/workflows/deploy.yml b/.github/workflows/deploy.yml index 66cb648..c13e34c 100644 --- a/.github/workflows/deploy.yml +++ b/.github/workflows/deploy.yml @@ -154,6 +154,9 @@ jobs: env: PW_TEST_SOURCE_TRANSFORM: ${{ github.workspace }}/tests/svg-import-stub.cjs PW_TEST_SOURCE_TRANSFORM_SCOPE: ${{ github.workspace }} + # Node 22.18+ strips types by default and rejects parameter properties. + # Playwright 1.51 has its own TS transform; disable Node strip-types. + NODE_OPTIONS: --no-experimental-strip-types run: npx playwright test - name: πŸ“Š Upload report