Playwright 1.64 (Oktober 2026) dan 1.65 (November 2026) mengubah cukup banyak default sampai sekadar naik versi bisa bikin suite yang tadinya hijau jadi merah. Gejala yang umum: toBeVisible() tiba-tiba bilang hidden, fill() timeout dengan element is inert, atau page object melempar TypeError: Cannot read properties of undefined. Semua kegagalan di bawah direproduksi dengan menjalankan satu test suite di 1.63.0, lalu 1.64.1, lalu 1.65.0, dan setiap fix sudah dicek di 1.65.
Jawaban cepat
Cari pesan error kamu di daftar ini:
Received: "1920x1080"untukscreen.widthatau cekmatchMedia: tambahkanscreen: undefineddi sebelah...devices['Desktop Chrome'](1.64).toBeVisible()gagal denganReceived: hiddendi dalam iframe, atau call log bilangelement is inside a hidden frame: tampilkan iframe-nya dulu (1.64).Cannot find module 'react/jsx-runtime'atauReact is not defineddi.spec.tsx: installreactatau setjsxImportSource(1.64).- Call log bilang
element is inert: tutup modal atau dialog yang memasanginertsebelum beraksi (1.65). TypeError: Cannot read properties of undefineddi field page object: pindahkan assignment field ke constructor (1.65).toMatchAriaSnapshot()ataugetByRole({ name })kehilangan teks: teksnya tidak di-render, jadi update snapshot atau pakaihasText(1.65).cookies()mengembalikan[]: beri cookie domain dengan titik di depan atau perbaiki path-nya (1.65).Error: No tests founddi CI dengan--sharddan--grep: tambahkan--pass-with-no-tests(1.65).
Detail masing-masing ada di bawah.
1.64: window.screen sekarang ikut ukuran device
Sebelum 1.64, spread devices['Desktop Chrome'] mengatur viewport ke 1280x720, dan window.screen ikut sama. Di 1.64 descriptor itu juga mengatur screen, dan untuk Desktop Chrome nilainya 1920x1080. Viewport (innerWidth) tetap 1280x720, tapi screen.width dan media query device-width berubah:
Error: expect(received).toBe(expected) // Object.is equality
Expected: "1280x720"
Received: "1920x1080"
Kalau app atau test kamu membaca screen.width atau memakai (max-device-width: ...), reset nilainya:
// playwright.config.ts
use: {
...devices['Desktop Chrome'],
screen: undefined,
},
Dengan screen: undefined, screen.width kembali ke 1280 dan hasil media query yang lama kembali.
1.64: elemen di dalam iframe tersembunyi dianggap hidden
Elemen di dalam iframe dengan visibility: hidden dulu dianggap visible. Sekarang tidak lagi, baik untuk assertion maupun action:
Error: expect(locator).toBeVisible() failed
Locator: locator('#chat').contentFrame().getByRole('button', { name: 'Send' })
Expected: visible
Received: hidden
fill() terus retry sampai timeout:
TimeoutError: locator.fill: Timeout 2000ms exceeded.
Call log:
- waiting for locator('#chat').contentFrame().getByLabel('Message')
19 × locator resolved to <input aria-label="Message"/>
- element is inside a hidden frame, retrying
click() di kasus ini sudah gagal sejak 1.63 (dengan pesan intercepts pointer events), jadi yang baru rusak adalah toBeVisible(), isVisible(), dan action non-pointer seperti fill().
Test yang dulu lolos itu sebenarnya salah. User tidak bisa mengetik di iframe yang tersembunyi. Buka iframe-nya seperti yang dilakukan user, lalu assert:
const chat = page.locator('#chat').contentFrame();
await expect(chat.getByRole('button', { name: 'Send' })).toBeHidden();
await page.getByRole('button', { name: 'Open chat' }).click();
await expect(chat.getByRole('button', { name: 'Send' })).toBeVisible();
await chat.getByLabel('Message').fill('hi');
1.64: JSX di file test mengikuti tsconfig
Sampai 1.63, Playwright mengabaikan setting jsx di tsconfig dan meng-compile JSX di file test jadi object versinya sendiri. Di 1.64 dia mengikuti tsconfig dan default ke automatic runtime. Project yang tidak meng-install react, misalnya package E2E di monorepo, sekarang berhenti sebelum menjalankan apa pun:
Error: Cannot find module 'react/jsx-runtime'
Require stack:
- .../tests/jsx.spec.tsx
...
Error: No tests found
Itu terjadi dengan "jsx": "preserve", default Next.js, dan dengan "react-jsx". Dengan "jsx": "react" error-nya jadi ReferenceError: React is not defined.
Install react di package yang menjalankan Playwright, atau arahkan jsxImportSource ke JSX runtime yang kamu punya. Dengan react ter-install test-nya lolos, tapi nilainya sekarang React element beneran. Test yang membandingkannya dengan object biasa perlu di-update.
1.64: --update-snapshots=missing sekarang lolos
Yang ini dari merah jadi hijau. Di 1.63, --update-snapshots=missing menulis snapshot baru tapi tetap exit 1. Di 1.64 dia menulis snapshot dan exit 0. Kalau ada step CI yang mengandalkan kegagalan itu untuk menangkap test tanpa snapshot, sekarang tidak tertangkap lagi. Jalankan tanpa flag itu di CI, karena tanpa flag snapshot yang hilang tetap bikin gagal.
1.65: action menunggu elemen inert
click() pada elemen di belakang modal yang memasang inert sebelum 1.65 pun sudah timeout, tapi call log-nya menyalahkan hal lain (<body>…</body> intercepts pointer events). 1.65 menyebut penyebabnya:
TimeoutError: locator.click: Timeout 2000ms exceeded.
Call log:
- waiting for getByRole('button', { name: 'Save draft' })
- locator resolved to <button>Save draft</button>
- attempting click action
2 × waiting for element to be visible, enabled and stable
- element is inert
Perubahan yang lebih besar ada di fill(), clear(), dan selectOption(). Di 1.63 mereka "lolos" di elemen inert. fill() tidak melakukan apa-apa dan value-nya tetap kosong, sementara selectOption() mengubah value padahal user tidak bisa. Test yang mengisi field saat modal masih terbuka lolos di action-nya dan baru gagal di toHaveValue() berikutnya. Di 1.65 action-nya sendiri yang timeout:
TimeoutError: locator.fill: Timeout 2000ms exceeded.
Call log:
- waiting for getByLabel('Title')
- locator resolved to <input aria-label="Title"/>
- fill("hello")
- attempting fill action
2 × waiting for element to be visible, enabled and editable
- element is inert
Kalau halaman menghapus inert tidak lama kemudian, 1.65 menunggu dan action-nya lolos. Kalau tetap inert, test kamu beraksi saat modal masih terbuka. Tutup modalnya dulu:
await page.getByRole('button', { name: 'Close' }).click();
await page.getByLabel('Title').fill('hello');
Kalau memang mau assert kondisi terblokir, cek atributnya, jangan beraksi:
await expect(page.locator('main')).toHaveAttribute('inert');
click({ force: true }) masih bisa mengklik elemen inert di 1.65, tapi itu menyembunyikan bug-nya, bukan memperbaikinya.
1.65: page object melempar TypeError
Banyak page object ditulis seperti ini:
class LoginPage {
readonly email = this.page.getByLabel('Email');
constructor(private readonly page: Page) {}
}
Di 1.65, dengan target tsconfig ES2022 ke atas (atau tanpa target sama sekali), kode itu melempar:
TypeError: Cannot read properties of undefined (reading 'getByLabel')
3 | class LoginPage {
> 4 | readonly email = this.page.getByLabel('Email');
| ^
Transform TypeScript Playwright sekarang mengikuti useDefineForClassFields, yang default-nya aktif untuk target ES2022. Field initializer jalan sebelum parameter constructor page di-assign. Project dengan target: "ES2017" masih lolos.
Assign field-nya di constructor:
class LoginPage {
readonly email: Locator;
constructor(private readonly page: Page) {
this.email = page.getByLabel('Email');
}
}
Cara itu jalan di semua versi. Jembatan cepatnya "useDefineForClassFields": false di tsconfig yang dipakai Playwright, tapi itu mengubah cara semua class field di-compile.
TypeScript sebenarnya sudah melaporkan ini sebelum upgrade: error TS2729: Property 'page' is used before its initialization. Kalau folder e2e kamu belum dicek tsc --noEmit, tambahkan.
1.65: aria snapshot melewati teks yang tidak di-render
Accessible name dan aria snapshot sekarang tidak menyertakan teks yang tidak di-render browser. Termasuk isi <details> yang tertutup dan teks light DOM yang tidak di-slot oleh web component.
Error: expect(locator).toMatchAriaSnapshot(expected) failed
Expected: "- group: Shipping Orders ship in 2 days."
Received: "- group: Shipping"
Custom element <fancy-button role="button">Buy now</fancy-button> yang shadow root-nya cuma me-render ikon sekarang punya accessible name 🛒, bukan Buy now🛒. getByRole('button', { name: 'Buy now' }) tidak menemukan apa-apa. Screen reader juga tidak akan membaca teks itu, jadi update snapshot-nya (atau buka <details> dulu) dan cocokkan berdasarkan teks kalau perlu:
await page.getByRole('button').filter({ hasText: 'Buy now' }).click();
Fix yang lebih baik ada di komponennya. Tambahkan <slot> dan name-nya jadi 🛒 Buy now.
1.65: cookies() mengikuti RFC 6265
Dua query cookie sekarang mengembalikan lebih sedikit dari sebelumnya:
await context.addCookies([{ name: 'session', value: '1', url: 'https://example.com/' }]);
await context.cookies('https://app.example.com/'); // 1.65: []
Cookie yang ditambahkan dengan url itu host-only, jadi bukan milik subdomain. Versi lama tetap mengembalikannya. Cookie Path=/api juga tidak lagi cocok dengan /apiv2.
Untuk berbagi cookie dengan subdomain, pakai titik di depan:
await context.addCookies([
{ name: 'session', value: '1', domain: '.example.com', path: '/' },
]);
domain: 'example.com' tanpa titik tetap disimpan sebagai host-only dan tetap mengembalikan [] untuk app.example.com.
1.65: Error: No tests found dengan --shard
Shard yang kosong tetap lolos selama shard lain punya test. Satu test dengan --shard=2/2 exit 0 di 1.65. Perubahannya kena di run ber-shard yang tidak cocok dengan test apa pun, biasanya --grep yang menyaring semuanya:
npx playwright test --grep @smoke --shard=1/2
# 1.64: exit 0, no output
# 1.65: Error: No tests found, exit 1
Kalau itu memang wajar di beberapa job CI, tambahkan flag-nya:
npx playwright test --grep @smoke --shard=1/2 --pass-with-no-tests
Perubahan kecil di 1.65 yang bisa menggigit
request.get('/search?page=2', { params: 'q=shoes' }) dulu membuang page=2 dan me-request /search?q=shoes. Sekarang di-append: /search?page=2&q=shoes. Ini hanya berlaku untuk params string dan URLSearchParams. Params object dari dulu sudah di-append. Taruh query di satu tempat saja.
import './styles.css' di file test sekarang di-load sebagai module kosong, jadi file-nya harus ada. Import CSS yang hilang dulu diabaikan. Di 1.65 itu menghentikan seluruh run dengan Cannot find module './does-not-exist.css'.
test.skip('Not implemented yet') sekarang mencatat string itu sebagai deskripsi skip. Sebelumnya string itu dianggap kondisi truthy. test.skip(condition, 'reason') tetap sama, jadi tidak perlu ubah kode.
Playwright tidak lagi dites di Node 20, walaupun engines masih >=20. Image Docker pindah ke Node 26.
Cek hasilnya
npx playwright --version
npx playwright test --trace off
Harusnya muncul Version 1.65.0. Untuk kegagalan yang tidak dibahas di atas, baca call log di bawah timeout. Di 1.65 call log itu memberi tahu apakah elemennya inert, hidden, atau ada di dalam frame tersembunyi.
Kesimpulan
Sebagian besar kegagalan setelah upgrade Playwright 1.64 dan 1.65 adalah test yang dulu lolos dengan alasan yang salah: mengisi input di belakang modal, assert di iframe tersembunyi, atau membaca teks yang tidak bisa dilihat user. Perbaiki di test-nya. Tambahkan screen: undefined kalau kamu bergantung pada ukuran screen yang lama, dan pindahkan field page object ke constructor.