Repository navigation
Description
Version
latest
Steps to reproduce
Here's a minimal bash repro Claude helped me set up
mkdir pw-repro && cd pw-repro
npm init -y
npm pkg set type=module
npm i -D @playwright/test
mkdir e2e && printf "import { test } from '@playwright/test'\ntest('noop', () => {})\n" > e2e/noop.spec.ts
printf "import { defineConfig } from '@playwright/test'\nexport default defineConfig({ testDir: './e2e' })\n" > playwright.config.ts
cat > race.sh <<'SH'
#!/usr/bin/env bash
export TMPDIR="$PWD/.tmp"; mkdir -p "$TMPDIR"
for round in $(seq 1 200); do
rm -rf "$TMPDIR"/playwright-transform-cache-* # force a cold, concurrent write each round
for i in $(seq 1 8); do npx playwright test --list >>out.log 2>&1 & done
wait
if grep -q "not extensible" out.log; then echo "REPRODUCED at round $round"; break; fi
done
SH
chmod +x race.sh && : > out.log && ./race.sh
grep "not extensible" out.log | head -1
cd .. && rm -rf pw-reproIt fails as expected (although it's not deterministic by nature):
output
frodi@Frodi-Karlsson ~/Development> sh repro.sh
Wrote to /Users/frodi/Development/pw-repro/package.json:
{
"name": "pw-repro",
"version": "1.0.0",
"description": "",
"main": "index.js",
"scripts": {
"test": "echo \"Error: no test specified\" && exit 1"
},
"keywords": [],
"author": "",
"license": "ISC",
"type": "commonjs"
}
added 3 packages, and audited 4 packages in 5s
found 0 vulnerabilities
REPRODUCED at round 8
TypeError: Cannot add property metadata, object is not extensibleExpected behavior
No crash
Actual behavior
TypeError: Cannot add property metadata, object is not extensible
Additional context
This occurred in our CI runners, so it's a real-life failure and not just hypothetical.
I think this is what's happening:
Playwright caches transformed TS at $TMPDIR/playwright-transform-cache-<uid>/…js, keyed by hash(filename+content)
packages/playwright/src/transform/compilationCache.ts has a fs.writeFileSync(codePath, code, 'utf8') that will truncate the file to zero length then fill it
packages/playwright/src/transform/transform.ts has if (cachedCode !== undefined) return { code: cachedCode, … } ('' is !== undefined)
So when two processes load the same config concurrently, one can read the cache file mid-write and get an empty string, meaning an empty module. With an ESM config, that empty module is an ES module namespace, and since it has no default export, loadUserConfig's object['default'] unwrap is skipped, so userConfig is the namespace itself.
config.ts then runs userConfig.metadata = userConfig.metadata || {} on that namespace, we get TypeError: Cannot add property metadata, object is not extensible
Suggested fix:
Make the cache write atomic, perhaps via writing to a tmp file and renaming. Happy to contribute with a PR.
Environment
frodi@Frodi-Karlsson ~/Development> npx envinfo --preset playwright
System:
OS: macOS 15.5
CPU: (11) arm64 Apple M3 Pro
Memory: 127.13 MB / 18.00 GB
Binaries:
Node: 24.18.0 - /Users/frodi/.local/state/fnm_multishells/41576_1783683870601/bin/node
Yarn: 1.22.22 - /Users/frodi/Library/pnpm/yarn
npm: 11.16.0 - /Users/frodi/.local/state/fnm_multishells/41576_1783683870601/bin/npm
bun: 1.3.3 - /Users/frodi/.bun/bin/bun
Deno: 2.7.7 - /opt/homebrew/bin/deno
IDEs:
VSCode: 1.127.0 - /usr/local/bin/code
Claude Code: 2.1.185 - /opt/homebrew/bin/claude
Languages:
Bash: 3.2.57 - /bin/bash
Activity
yury-s commentedon Jul 10, 2026
Running several
playwright testprocesses concurrently against the same config isn't a supported setup. The transform cache lives under$TMPDIRper user and is keyed by the config's absolute path, so two cold loads from the same checkout race on the same cache file — that's the crash. If you need to run them in parallel, use separate checkouts: different paths mean separate caches and no collision. For an immediate unblock without that, point each invocation at its own temp dir (TMPDIR=/tmp/pw-<unique>, unique per process), which avoids the cache race — but that only fixes this specific error; concurrent full runs in one directory will still clobber each other'stest-results/and report output, so separate checkouts are the real answer. Closing as won't-fix.