Running the E2E command documented in the README:
I diagnosed the issue and verified the fix against a faithful minimal reproduction. The solution is saved at /workspace/solution.md and reproduced below.
playwright-default-testdir-collects-vitest-suites)Running the E2E command documented in the README:
cd frontend && npx playwright test
exits 1 immediately, and the errors come from Vitest unit tests, not from any E2E spec:
TypeError: Cannot read properties of undefined (reading 'VITE_API_BASE_URL') at src/lib/api.ts:9
TypeError: Cannot read properties of undefined (reading 'config') at src/__tests__/*.test.tsx
No E2E test ever runs.
frontend/playwright.config.ts declared no testDir and no testMatch. Playwright then falls back to its defaults:
testDir defaults to the directory containing the config file, i.e. frontend/ (the whole project root).testMatch defaults to **/*.@(spec|test).?(c|m)[jt]s?(x).So Playwright walks the entire frontend/ tree and greedily collects:
src/**/*.test.tsx (Vitest unit tests)tests/**/*.test.ts (Vitest unit tests)Those files import vitest and read import.meta.env. Under bare Playwright there is no Vitest runtime and no Vite import.meta.env replacement, so import.meta.env is undefined and the first module-scope read (import.meta.env.VITE_API_BASE_URL, or import.meta.env.config) throws during collection, before any test runs. The failure is a discovery bug, not an E2E test failure.
A second latent bug in the same class: naming Playwright specs *.spec.ts / *.test.ts makes them collide with Vitest's default include globs, so the two runners keep re-collecting each other's files. The two runners need disjoint file contracts.
frontend/playwright.config.ts:
import { defineConfig, devices } from '@playwright/test';
export default defineConfig({
// Only look at the E2E directory, never the project root.
testDir: './e2e',
// Only treat *.pw.ts as Playwright specs. This extension is NOT in
// Vitest's include globs, so the runners can never steal each other's files.
testMatch: '**/*.pw.ts',
// Optional belt-and-suspenders; not required once testDir is scoped.
// testIgnore: ['**/src/**', '**/tests/**'],
fullyParallel: true,
reporter: 'html',
use: {
baseURL: process.env.E2E_BASE_URL ?? 'http://localhost:5173',
trace: 'on-first-retry',
},
// ...projects/browsers as before...
});
Notes:
testDir: './e2e' is resolved relative to the config file (frontend/), so the E2E specs must live under frontend/e2e/.testMatch: '**/*.pw.ts' is matched against each file under testDir. If the suite also uses TSX or .mts, widen it explicitly: '**/*.pw.?(c|m)[jt]s?(x)'. Do not fall back to **/*.spec.ts.e2e/ and only with the .pw.ts suffix.The Vitest-based integration / browser suite must stay pointed at its own config and script. Do not fold it into the Playwright config.
frontend/package.json (scripts — names must match what the README tells users to run):
{
"scripts": {
"test": "vitest run",
"test:unit": "vitest run",
"test:integration": "vitest run --config vitest.integration.config.ts",
"test:e2e": "playwright test"
}
}
frontend/vitest.integration.config.ts stays unchanged; its test.include must remain scoped to the integration files and must not match e2e/**/*.pw.ts.
The README must map each suite to the runner that truly executes it. A stale/broken entry (pointing at a config or runner that doesn't exist) is part of the bug.
| Suite | Runner | Command | Config |
| --------------------- | ---------- | ------------------------------------------ | ------------------------------- |
| Unit | Vitest | `npm run test:unit` | `vitest.config.ts` |
| Integration / browser | Vitest | `npm run test:integration` | `vitest.integration.config.ts` |
| E2E (real browser) | Playwright | `npm run test:e2e` (or `npx playwright test`) | `playwright.config.ts` |
Remove or rewrite any README line that advertises npx playwright test as the integration/browser runner, or that references a non-existent config. E2E docs must reference playwright.config.ts; integration docs must reference vitest.integration.config.ts.
frontend/playwright.config.ts — add testDir: './e2e' and testMatch: '**/*.pw.ts'.frontend/e2e/*.pw.ts — E2E specs renamed to the .pw.ts contract (or kept there if already present).frontend/package.json — ensure test:e2e / test:integration scripts exist as above.README.md — correct the E2E vs integration runner table.These were executed against a faithful minimal reproduction (vitest + @playwright/test, a src/__tests__/*.test.tsx, a tests/*.test.ts, an e2e/smoke.pw.ts, and an e2e/legacy.spec.ts).
With defineConfig({}) (no testDir / testMatch):
$ npx playwright test --list
Error: Vitest cannot be imported in a CommonJS module using require() ...
at src/__tests__/api.test.tsx:1
SyntaxError: Cannot use 'import.meta' outside a module
Total: 0 tests in 0 files
exit=1
Playwright collected the Vitest suites from src/__tests__ and tests and died during collection — the same class of failure as the reported import.meta.env TypeError. (The exact error text depends on the module/transform setup; the invariant is that Playwright is importing Vitest files.)
$ npx playwright test --list
Listing tests:
smoke.pw.ts:2:5 › e2e smoke runs without a browser
Total: 1 test in 1 file
exit=0
src/__tests__ or tests/.e2e/legacy.spec.ts is also excluded (proves the extension contract matters).$ npx playwright test
Running 1 test using 1 worker
✓ 1 e2e/smoke.pw.ts:2:5 › e2e smoke runs without a browser (4ms)
1 passed (316ms)
exit=0
The Playwright config is not read by Vitest, so the unit suite is untouched. Verified by listing the unit suite both before and after changing playwright.config.ts:
$ npx vitest list
tests/unit.test.ts > unit > reads env
src/__tests__/api.test.tsx > api > has base
# identical output with the broken and the fixed playwright.config.ts
Against the real project, confirm the exact counts stated in the issue:
cd frontend
npx playwright test --list # only e2e/**.pw.ts entries; grep -c outside e2e must be 0
npx playwright test # exits 0
npm run test:unit # expect: 42 files / 744 tests, unchanged
npm run test:integration # integration/browser suite still runs on vitest
testDir confines the walk to e2e/, and testMatch confines the filename to *.pw.ts. Either one alone fixes the immediate crash; both together prevent regressions..pw.ts extension is load-bearing. .spec.ts / .test.ts are matched by Vitest's default include, so a Playwright naming change back to .spec.ts silently re-introduces cross-collection. Keep the contract documented in the README and in the config comment.testIgnore alone. Ignoring src/** and tests/** leaves the root as the discovery base and will re-break when a new test directory is added. Scope testDir.playwright.config.ts for E2E and vitest.integration.config.ts for the integration/browser suite.# Evidence - Problem class: playwright-default-testdir-collects-vitest-suites - Model: openrouter/deepseek/deepseek-v4.1-flash - Solved: 2026-09-12T19:23:35.671Z - Verification: solution produced by pi in sandbox; see signatures.json
{"description": "Symptom: the command the README documents for E2E (`cd frontend && npx playwright test`) exits 1 instantly with errors coming from VITEST test files, not from any E2E spec.\nRoot cause: playwright.config.ts declared no testDir and no testMatch, so Playwright fell back to default discovery over the config directory and collected the vitest suites (src/**/*.test.tsx and tests/**/*.test.ts). Those files import vitest and read import.meta.env, which is undefined under bare Playwright, so they throw during collection.\nFix: pin discovery in the Playwright config - testDir './e2e' plus testMatch '**/*.pw.ts' - and keep the real browser suite on its own runner (vitest.integration.config.ts, `npm run test:integration`). Docs must name the runner that actually exists.\nVerification that closes it: `npx playwright test --list` must list ONLY specs under the scoped testDir (zero src/__tests__ and tests/ entries), `npx playwright test` exits 0, and the other runner's counts are unchanged (unit suite 42 files / 744 tests).\nTrap: a non-.pw.ts extension is what keeps the two runners from fighting - .spec.ts / .test.ts would be re-collected by vitest's default include.", "environment": "frontend with BOTH vitest (unit + integration configs) and @playwright/test, playwright.config.ts at the frontend root", "language": "typescript", "model": "openrouter/deepseek/deepseek-v4.1-flash", "problem_class": "playwright-default-testdir-collects-vitest-suites", "provider": "openrouter", "solved_at": "2026-09-12T19:23:35.671Z", "version": ""}I diagnosed the issue and verified the fix against a faithful minimal reproduction. The solution is saved at /workspace/solution.md and reproduced below.
playwright-default-testdir-collects-vitest-suites)Running the E2E command documented in the README:
cd frontend && npx playwright test
exits 1 immediately, and the errors come from Vitest unit tests, not from any E2E spec:
TypeError: Cannot read properties of undefined (reading 'VITE_API_BASE_URL') at src/lib/api.ts:9
TypeError: Cannot read properties of undefined (reading 'config') at src/__tests__/*.test.tsx
No E2E test ever runs.
frontend/playwright.config.ts declared no testDir and no testMatch. Playwright then falls back to its defaults:
testDir defaults to the directory containing the config file, i.e. frontend/ (the whole project root).testMatch defaults to **/*.@(spec|test).?(c|m)[jt]s?(x).So Playwright walks the entire frontend/ tree and greedily collects:
src/**/*.test.tsx (Vitest unit tests)tests/**/*.test.ts (Vitest unit tests)Those files import vitest and read import.meta.env. Under bare Playwright there is no Vitest runtime and no Vite import.meta.env replacement, so import.meta.env is undefined and the first module-scope read (import.meta.env.VITE_API_BASE_URL, or import.meta.env.config) throws during collection, before any test runs. The failure is a discovery bug, not an E2E test failure.
A second latent bug in the same class: naming Playwright specs *.spec.ts / *.test.ts makes them collide with Vitest's default include globs, so the two runners keep re-collecting each other's files. The two runners need disjoint file contracts.
frontend/playwright.config.ts:
import { defineConfig, devices } from '@playwright/test';
export default defineConfig({
// Only look at the E2E directory, never the project root.
testDir: './e2e',
// Only treat *.pw.ts as Playwright specs. This extension is NOT in
// Vitest's include globs, so the runners can never steal each other's files.
testMatch: '**/*.pw.ts',
// Optional belt-and-suspenders; not required once testDir is scoped.
// testIgnore: ['**/src/**', '**/tests/**'],
fullyParallel: true,
reporter: 'html',
use: {
baseURL: process.env.E2E_BASE_URL ?? 'http://localhost:5173',
trace: 'on-first-retry',
},
// ...projects/browsers as before...
});
Notes:
testDir: './e2e' is resolved relative to the config file (frontend/), so the E2E specs must live under frontend/e2e/.testMatch: '**/*.pw.ts' is matched against each file under testDir. If the suite also uses TSX or .mts, widen it explicitly: '**/*.pw.?(c|m)[jt]s?(x)'. Do not fall back to **/*.spec.ts.e2e/ and only with the .pw.ts suffix.The Vitest-based integration / browser suite must stay pointed at its own config and script. Do not fold it into the Playwright config.
frontend/package.json (scripts — names must match what the README tells users to run):
{
"scripts": {
"test": "vitest run",
"test:unit": "vitest run",
"test:integration": "vitest run --config vitest.integration.config.ts",
"test:e2e": "playwright test"
}
}
frontend/vitest.integration.config.ts stays unchanged; its test.include must remain scoped to the integration files and must not match e2e/**/*.pw.ts.
The README must map each suite to the runner that truly executes it. A stale/broken entry (pointing at a config or runner that doesn't exist) is part of the bug.
| Suite | Runner | Command | Config |
| --------------------- | ---------- | ------------------------------------------ | ------------------------------- |
| Unit | Vitest | `npm run test:unit` | `vitest.config.ts` |
| Integration / browser | Vitest | `npm run test:integration` | `vitest.integration.config.ts` |
| E2E (real browser) | Playwright | `npm run test:e2e` (or `npx playwright test`) | `playwright.config.ts` |
Remove or rewrite any README line that advertises npx playwright test as the integration/browser runner, or that references a non-existent config. E2E docs must reference playwright.config.ts; integration docs must reference vitest.integration.config.ts.
frontend/playwright.config.ts — add testDir: './e2e' and testMatch: '**/*.pw.ts'.frontend/e2e/*.pw.ts — E2E specs renamed to the .pw.ts contract (or kept there if already present).frontend/package.json — ensure test:e2e / test:integration scripts exist as above.README.md — correct the E2E vs integration runner table.These were executed against a faithful minimal reproduction (vitest + @playwright/test, a src/__tests__/*.test.tsx, a tests/*.test.ts, an e2e/smoke.pw.ts, and an e2e/legacy.spec.ts).
With defineConfig({}) (no testDir / testMatch):
$ npx playwright test --list
Error: Vitest cannot be imported in a CommonJS module using require() ...
at src/__tests__/api.test.tsx:1
SyntaxError: Cannot use 'import.meta' outside a module
Total: 0 tests in 0 files
exit=1
Playwright collected the Vitest suites from src/__tests__ and tests and died during collection — the same class of failure as the reported import.meta.env TypeError. (The exact error text depends on the module/transform setup; the invariant is that Playwright is importing Vitest files.)
$ npx playwright test --list
Listing tests:
smoke.pw.ts:2:5 › e2e smoke runs without a browser
Total: 1 test in 1 file
exit=0
src/__tests__ or tests/.e2e/legacy.spec.ts is also excluded (proves the extension contract matters).$ npx playwright test
Running 1 test using 1 worker
✓ 1 e2e/smoke.pw.ts:2:5 › e2e smoke runs without a browser (4ms)
1 passed (316ms)
exit=0
The Playwright config is not read by Vitest, so the unit suite is untouched. Verified by listing the unit suite both before and after changing playwright.config.ts:
$ npx vitest list
tests/unit.test.ts > unit > reads env
src/__tests__/api.test.tsx > api > has base
# identical output with the broken and the fixed playwright.config.ts
Against the real project, confirm the exact counts stated in the issue:
cd frontend
npx playwright test --list # only e2e/**.pw.ts entries; grep -c outside e2e must be 0
npx playwright test # exits 0
npm run test:unit # expect: 42 files / 744 tests, unchanged
npm run test:integration # integration/browser suite still runs on vitest
testDir confines the walk to e2e/, and testMatch confines the filename to *.pw.ts. Either one alone fixes the immediate crash; both together prevent regressions..pw.ts extension is load-bearing. .spec.ts / .test.ts are matched by Vitest's default include, so a Playwright naming change back to .spec.ts silently re-introduces cross-collection. Keep the contract documented in the README and in the config comment.testIgnore alone. Ignoring src/** and tests/** leaves the root as the discovery base and will re-break when a new test directory is added. Scope testDir.playwright.config.ts for E2E and vitest.integration.config.ts for the integration/browser suite.# Evidence - Problem class: playwright-default-testdir-collects-vitest-suites - Model: openrouter/deepseek/deepseek-v4.1-flash - Solved: 2026-09-12T19:23:35.671Z - Verification: solution produced by pi in sandbox; see signatures.json
{"description": "Symptom: the command the README documents for E2E (`cd frontend && npx playwright test`) exits 1 instantly with errors coming from VITEST test files, not from any E2E spec.\nRoot cause: playwright.config.ts declared no testDir and no testMatch, so Playwright fell back to default discovery over the config directory and collected the vitest suites (src/**/*.test.tsx and tests/**/*.test.ts). Those files import vitest and read import.meta.env, which is undefined under bare Playwright, so they throw during collection.\nFix: pin discovery in the Playwright config - testDir './e2e' plus testMatch '**/*.pw.ts' - and keep the real browser suite on its own runner (vitest.integration.config.ts, `npm run test:integration`). Docs must name the runner that actually exists.\nVerification that closes it: `npx playwright test --list` must list ONLY specs under the scoped testDir (zero src/__tests__ and tests/ entries), `npx playwright test` exits 0, and the other runner's counts are unchanged (unit suite 42 files / 744 tests).\nTrap: a non-.pw.ts extension is what keeps the two runners from fighting - .spec.ts / .test.ts would be re-collected by vitest's default include.", "environment": "frontend with BOTH vitest (unit + integration configs) and @playwright/test, playwright.config.ts at the frontend root", "language": "typescript", "model": "openrouter/deepseek/deepseek-v4.1-flash", "problem_class": "playwright-default-testdir-collects-vitest-suites", "provider": "openrouter", "solved_at": "2026-09-12T19:23:35.671Z", "version": ""}