Playwright 1.64 Breaking Changes: Gate Every Upgrade

Table of Contents
Four breaking changes in a minor release. Ten more in the next one. The Playwright 1.64 breaking changes alter what window.screen reports, how JSX in test files compiles, whether a missing snapshot fails the run, and what counts as visible, and a ^1.63.0 range pulls all four into CI the next time someone regenerates the lockfile.
My position: a Playwright minor version is a major version with a smaller number. Pin it exactly, run every new release in a non-blocking canary lane for a week, and promote it only when every difference from the pinned run has an explanation.
Why do Playwright minor versions break tests?
Because Playwright doesn’t promise semver stability between minors, and its release notes say so in plain sight. Eight minor versions shipped between 1.57 (November 2025) and 1.65 (November 2026), and five of them carry a “Breaking Changes” heading. Each minor also bundles a new browser build: 1.64 moved from Chromium 153 to 156 (release notes), so even a release with no breaking-changes section changes the engine your tests run against.
What are the Playwright 1.64 breaking changes?
Playwright 1.64 changed four defaults: device descriptors now forward screen, JSX follows your tsconfig.json, --update-snapshots=missing now passes, and elements inside hidden iframes count as hidden. Two of them fail loudly. The other two can make a run greener than it should be.
| Change | What you’ll see | Direction |
|---|---|---|
screen forwarded from devices['Desktop Chrome'] | window.screen reports 1920×1080 instead of the 1280×720 viewport | Red, or silently different app behaviour |
JSX follows tsconfig.json | Component tests compile with the automatic react/jsx-runtime | Red |
--update-snapshots=missing passes | Tests that only wrote a new baseline now pass | Red → green |
| Hidden iframes hide their contents | toBeVisible() fails, toBeHidden() starts passing | Red, and red → green |
The screen change is the sneaky one
In the 1.65 bundle, the Desktop Chrome descriptor declares a 1280×720 viewport and a 1920×1080 screen. Before 1.64, Playwright ignored the descriptor’s screen and fell back to the viewport. Now any app code that reads window.screen.width, or any CSS using device-width media queries, sees a bigger monitor. Analytics payloads and “desktop vs tablet” branching shift without a single locator breaking.
The opt-out is one line, screen: undefined after the spread. I’d add it in the upgrade PR and remove it deliberately later, so the behaviour change becomes its own reviewed commit instead of riding along with a version bump.
Isn’t Chrome for Testing proof that upgrades are safe?
No. It’s the best counterexample I have. Playwright 1.57 switched from Chromium to Chrome for Testing builds, and the notes said existing tests “should continue to pass.” For almost everyone, they did.
At a Canadian insurer I worked with, the suite was about 1,400 tests across 8 shards, and 3 of those shards ran on Arm64 runners because they were cheaper. The 1.57 notes end with one line: on Arm64 Linux, Playwright keeps using Chromium. After the bump, the x64 shards ran Chrome for Testing and the Arm64 shards ran Chromium, both reporting the same Playwright version. Seventeen screenshot tests failed only on the Arm64 shards, and only when the scheduler happened to place them there. It took a day and a half to connect the failures to the runner architecture, and nothing in the version number pointed there.
No release note was wrong. A change everyone called safe went straight into the blocking pipeline, where the only options were “fix it now” or “revert.”
Why isn’t a caret range or lockfile enough?
A lockfile pins Playwright until someone runs npm update, deletes the lockfile to fix a merge conflict, or builds a fresh image from package.json. Then ^1.63.0 resolves to whatever is latest. The browser binaries follow the npm package, and the Playwright Docker image tag has to match it too, so an accidental minor bump is really three upgrades at once.
Pin the exact version and make the Docker tag move in the same commit:
"devDependencies": { "@playwright/test": "^1.63.0" "@playwright/test": "1.63.1"}Even an exact tag doesn’t freeze the image contents. The v1.64.1-noble tag was rebuilt with a different Chrome and Node.js major, so pin by digest if your CI pulls the image.
How does a canary lane gate a Playwright upgrade?
A canary lane runs your full suite nightly against the candidate Playwright version, next to the pinned run, with continue-on-error so it can never block a merge. You promote the candidate only after several nights where the canary’s results match the pinned run, or every difference is explained.
The Renovate PR’s CI run is one sample. A week of nightlies shows whether the flaky count moved.
Step 1: Hold the upgrade PR
Group every Playwright package so they move together, and stop Renovate from opening the PR until the first patch has had time to land. 1.64.1 came out a week after 1.64.0 and fixed a Chromium 155+ net::ERR_ABORTED regression with routed cross-origin redirects.
{ "packageRules": [ { "matchPackageNames": ["@playwright/test", "playwright", "mcr.microsoft.com/playwright"], "groupName": "playwright", "minimumReleaseAge": "7 days", "automerge": false } ]}minimumReleaseAge only works when the datasource exposes a release timestamp, so check that the Docker image actually appears in your Dependency Dashboard.
Step 2: Run the candidate nightly, sharded and non-blocking
This is the job, not the whole workflow. Add your schedule: trigger, Node setup, and an artifact upload for the canary/ directory.
canary: runs-on: ubuntu-latest continue-on-error: true strategy: matrix: shard: [1, 2, 3, 4] steps: - uses: actions/checkout@v4 - run: npm ci - run: npm install --no-save @playwright/test@${{ vars.PLAYWRIGHT_CANARY }} - run: npx playwright install --with-deps - run: npx playwright test --shard=${{ matrix.shard }}/4 --reporter=json env: PLAYWRIGHT_JSON_OUTPUT_NAME: canary/shard-${{ matrix.shard }}.jsonPLAYWRIGHT_CANARY is a repository variable you change by hand, which makes it the promotion lever. Set it to 1.65.0 and the lane tests 1.65 tonight, with nobody’s PR waiting.
Step 3: Fail the canary on silent greens
Pass/fail isn’t enough when two of the four changes make runs greener. Have the pinned nightly write its JSON the same way into pinned/, download both sets of artifacts, and compare test counts and flaky counts:
import { readdirSync, readFileSync } from 'node:fs';
const totals = (dir) => readdirSync(dir) .map((file) => JSON.parse(readFileSync(`${dir}/${file}`, 'utf8')).stats) .reduce((sum, s) => ({ ran: sum.ran + s.expected + s.unexpected + s.flaky, flaky: sum.flaky + s.flaky, }), { ran: 0, flaky: 0 });
const [pinned, canary] = process.argv.slice(2).map(totals);// A canary that runs fewer tests isn't greener. It's blind.if (canary.ran !== pinned.ran || canary.flaky > pinned.flaky) { console.error('Canary drifted', { pinned, canary }); process.exit(1);}If your CI uses --update-snapshots=missing, also fail when git status --porcelain shows a new file under any -snapshots/ directory. Since 1.64, those new files arrive in a passing run.
Step 4: Classify, then promote
Every canary failure goes into one of three buckets: our bug that the new version exposed, their deliberate change (adapt or opt out), or a Playwright regression (file an issue and hold). Promote when no failure is left unclassified. In practice that has taken me between two nights and two weeks, and the slow cases were always the ones that turned out to be real app bugs.
Is a canary lane overkill for a small suite?
For a single repo with fewer than about 200 tests, yes. Read the release notes, let the grouped Renovate PR run, and add the count check from Step 3.
The canary earns its keep once the suite spans several shards, runner architectures, or teams, which is exactly where the Arm64 split hid. At that size, a bad upgrade in the blocking pipeline costs every team a morning, and the canary costs four nightly jobs nobody waits on.
1.65 is already here
Playwright 1.65 shipped on November 5 with ten items under Breaking Changes, and it stopped testing on Node.js 20. Actions now wait for elements to stop being inert, cookie matching follows RFC 6265, and accessible names drop unrendered text. I covered the parts that change locators and agent snapshots in what Playwright 1.65 changed for agents. If you’re still on 1.63 and wanted 1.64’s --shuffle for finding shared-state bugs, you’re now taking fourteen breaking changes at once. That’s the strongest case for the canary.
Do one thing today: remove the caret from @playwright/test, add the canary job with PLAYWRIGHT_CANARY set to 1.65.0, and read the canary’s results on Monday before anyone opens the upgrade PR. More on Playwright framework design lives in the test automation archive.
FAQ
What are the Playwright 1.64 breaking changes?
Four: device descriptors now forward the screen size, JSX in test files follows tsconfig.json, --update-snapshots=missing now passes the run, and elements inside hidden iframes are considered hidden. See the official 1.64 release notes.
Should I pin @playwright/test to an exact version?
Yes. Playwright minor releases regularly contain breaking changes and new browser builds. Upgrade through a reviewed, grouped PR.
How do I keep the old screen behaviour after upgrading to 1.64?
Set screen: undefined in the use block after spreading the device descriptor. The context then falls back to the viewport size, as before 1.64.
Why does my Playwright upgrade PR pass but production tests miss bugs?
Some 1.64 changes turn failing runs into passing ones, such as missing snapshots in 'missing' mode and toBeHidden() inside hidden iframes. Compare test and flaky counts between versions, not just pass or fail.
