Dialog & iframe
Dialog, iframe, tab baru, download, modal buatan aplikasi, dan jam browser dengan page.clock.
Modul ini membahas situasi yang butuh cara khusus: dialog browser, iframe, tab baru, download, tabel yang berubah, dan tanda loading. Semuanya ada di Arena, jurus 4, 5, 6, 8, 10, dan 11.
Ini kasus yang sering membuat tester manual juga berhenti sejenak: pop-up konfirmasi, form di dalam form, link yang membuka tab baru, atau file yang diunduh. Playwright punya cara khusus untuk masing-masing.
1. Dialog browser: alert, confirm, prompt
Dialog bawaan browser (alert(), confirm(), prompt()) bukan bagian dari halaman, jadi tidak bisa dicari dengan locator. Playwright memberi tahu kemunculannya lewat event dialog.
- Pasang handler sebelum aksi yang memunculkan dialog.
- Tanpa handler, Playwright otomatis menolak dialognya. Untuk
confirm(), itu sama dengan menekan "Batal".
# Terima alert / confirm
page.once("dialog", lambda dialog: dialog.accept())
page.get_by_role("button", name="Tampilkan confirm").click() # munculkan confirm
expect(page.get_by_test_id("dialog-result")).to_have_text("Kamu memilih: OK") # confirm diterima (OK)
# Isi prompt dan periksa pesannya
def jawab(dialog):
assert dialog.type == "prompt" # jenis dialognya prompt
assert dialog.message == "Siapa namamu?" # teks pertanyaannya
dialog.accept("Rina") # isi jawaban lalu tekan OK
page.once("dialog", jawab) # pakai fungsi jawab untuk dialog berikutnya
page.get_by_role("button", name="Tampilkan prompt").click() # munculkan prompt
expect(page.get_by_test_id("dialog-result")).to_have_text("Halo, Rina!") # halaman memakai jawaban tadi// Terima alert / confirm
page.once('dialog', (dialog) => dialog.accept());
await page.getByRole('button', { name: 'Tampilkan confirm' }).click(); // munculkan confirm
await expect(page.getByTestId('dialog-result')).toHaveText('Kamu memilih: OK'); // confirm diterima (OK)
// Isi prompt dan periksa pesannya
page.once('dialog', async (dialog) => {
expect(dialog.type()).toBe('prompt');
expect(dialog.message()).toBe('Siapa namamu?'); // teks pertanyaannya
await dialog.accept('Rina'); // isi jawaban lalu tekan OK
});
await page.getByRole('button', { name: 'Tampilkan prompt' }).click(); // munculkan prompt
await expect(page.getByTestId('dialog-result')).toHaveText('Halo, Rina!'); // halaman memakai jawaban tadipage.oncemenangani dialog berikutnya saja.page.onmenangani semua dialog sampai dimatikan denganpage.remove_listener/page.off. Lebih aman pakaionce.- Pop-up buatan aplikasi (misalnya
<div role="dialog">) adalah bagian dari halaman. Cari denganget_by_role("dialog"), tanpa event.
2. iframe: halaman di dalam halaman
iframe adalah halaman lain yang ditempel di dalam halaman, misalnya form pembayaran atau kotak chat. Locator biasa tidak masuk ke iframe, jadi masuki iframe-nya dulu.
frame = page.frame_locator('iframe[title="Formulir newsletter"]') # masuk ke dalam iframe lewat judulnya
frame.get_by_label("Email").fill("rina@contoh.com") # isi Email di dalam iframe
frame.get_by_role("button", name="Daftar").click() # klik Daftar di dalam iframe
expect(frame.get_by_role("status")).to_have_text("Terdaftar: rina@contoh.com") # konfirmasi muncul di dalam iframe
# Alternatif yang lebih baru: dari locator elemen iframe → content_frame
frame = page.get_by_title("Formulir newsletter").content_frameconst frame = page.frameLocator('iframe[title="Formulir newsletter"]'); // masuk ke dalam iframe lewat judulnya
await frame.getByLabel('Email').fill('rina@contoh.com'); // isi Email di dalam iframe
await frame.getByRole('button', { name: 'Daftar' }).click(); // klik Daftar di dalam iframe
await expect(frame.getByRole('status')).toHaveText('Terdaftar: rina@contoh.com'); // konfirmasi muncul di dalam iframe
// Alternatif yang lebih baru: dari locator elemen iframe → contentFrame()
const frame2 = page.getByTitle('Formulir newsletter').contentFrame();3. Tab baru dan jendela pop-up
Link dengan target="_blank" membuka tab baru, yaitu Page baru. Mulai menunggu tab baru, klik linknya, lalu ambil tab-nya.
with page.context.expect_page() as info: # tunggu tab baru...
page.get_by_role("link", name="Buka pusat bantuan").click() # ...setelah link ini diklik
tab_baru = info.value # tab barunya
expect(tab_baru).to_have_title("Pusat Bantuan — Arena") # judul tab baru
tab_baru.close() # kembali bekerja dengan `page` seperti biasaconst tabBaruPromise = page.context().waitForEvent('page'); // JANGAN di-await dulu
await page.getByRole('link', { name: 'Buka pusat bantuan' }).click(); // klik link yang membuka tab baru
const tabBaru = await tabBaruPromise; // tab barunya
await expect(tabBaru).toHaveTitle('Pusat Bantuan — Arena'); // judul tab baru
await tabBaru.close(); // kembali bekerja dengan `page` seperti biasaPola with page.expect_xxx() as info: dipakai untuk tab baru, pop-up, download, respons server, dan file chooser: mulai menunggu, jalankan aksi di dalam blok, lalu ambil hasilnya dari info.value.
Pola "siapkan promise, lakukan aksi, lalu await hasilnya" dipakai untuk tab baru, pop-up, download, respons server, dan file chooser. Kalau waitForEvent di-await sebelum klik, test menunggu selamanya karena kliknya tidak pernah terjadi.
4. Download file
with page.expect_download() as info: # tunggu unduhan...
page.get_by_role("link", name="Unduh laporan").click() # ...setelah link ini diklik
download = info.value # unduhannya
assert download.suggested_filename == "laporan.csv" # nama file yang disarankan server
download.save_as(tmp_path / download.suggested_filename) # tmp_path = fixture folder sementara pytestconst downloadPromise = page.waitForEvent('download'); // mulai menunggu unduhan
await page.getByRole('link', { name: 'Unduh laporan' }).click(); // klik link unduhan
const download = await downloadPromise; // unduhannya
expect(download.suggestedFilename()).toBe('laporan.csv'); // nama file yang disarankan server
await download.saveAs(testInfo.outputPath(download.suggestedFilename())); // folder output milik testSetelah disimpan, isi file bisa dibaca dan diperiksa.
5. Tabel dan daftar yang berubah-ubah
baris = page.locator("#students").get_by_role("row") # semua baris tabel siswa
expect(baris).to_have_count(5) # ada 5 baris
# Baris tertentu berdasarkan isi
expect(baris.filter(has_text="Remedial")).to_have_count(2)
# Setelah diurutkan, cek urutan kolom pertama
page.get_by_role("button", name="Urutkan nilai tertinggi").click()
expect(page.locator("#students tr td:first-child")).to_have_text(["Citra", "Dewi", "Andi", "Eko", "Budi"]) # isi kolom nama, sesuai urutan
# Ambil teks semua sel untuk diolah sendiri (tidak menunggu, jadi pastikan tabel sudah siap)
nama = page.locator("#students tr td:first-child").all_inner_texts()const baris = page.locator('#students').getByRole('row'); // semua baris tabel siswa
await expect(baris).toHaveCount(5); // ada 5 baris
// Baris tertentu berdasarkan isi
await expect(baris.filter({ hasText: 'Remedial' })).toHaveCount(2);
// Setelah diurutkan, cek urutan kolom pertama
await page.getByRole('button', { name: 'Urutkan nilai tertinggi' }).click();
await expect(page.locator('#students tr td:first-child')).toHaveText(['Citra', 'Dewi', 'Andi', 'Eko', 'Budi']); // isi kolom nama, sesuai urutan
// Ambil teks semua sel untuk diolah sendiri (tidak menunggu, jadi pastikan tabel sudah siap)
const nama = await page.locator('#students tr td:first-child').allInnerTexts();Daftar di to_have_text/toHaveText mengecek jumlah dan urutan sekaligus, sambil menunggu. Lebih aman daripada all_inner_texts() lalu dicek sendiri.
6. Tanda loading
page.get_by_role("button", name="Muat data").click() # data muncul 1,5 detik kemudian
expect(page.get_by_text("Memuat...")).to_be_hidden() # tunggu loading selesai
expect(page.get_by_test_id("loaded-list").get_by_role("listitem")).to_have_count(3) # expect menunggu sampai 3 butir data munculawait page.getByRole('button', { name: 'Muat data' }).click(); // data muncul 1,5 detik kemudian
await expect(page.getByText('Memuat...')).toBeHidden(); // tunggu loading selesai
await expect(page.getByTestId('loaded-list').getByRole('listitem')).toHaveCount(3); // expect menunggu sampai 3 butir data munculKomponen di dalam Shadow DOM tetap bisa dicari dengan locator role, text, label, dan CSS. Pengecualiannya XPath dan shadow root dengan mode closed.
File asli Modul 9
Memuat tests/academy/test_m09_tricky.py…
uv run pytest tests/academy/test_m09_tricky.pyMemuat tests/academy/m09-tricky.spec.ts…
npx playwright test m09-tricky --project=chromiumLebih jauh (opsional)Trik komunitas
page.clock(v1.45+) bisa mengatur waktu di halaman: melompati hitung mundur, menguji sesi yang habis, atau membekukan jam, tanpa menunggu sungguhan.page.route_web_socket()(v1.48+) meng-intercept koneksi realtime, misalnya untuk fitur chat dan notifikasi.page.console_messages()danpage.page_errors()(v1.56+) mengambil catatan pesan dan error di halaman, tanpa harus memasang listener dari awal.- Pop-up dari
window.open:page.expect_popup()/page.waitForEvent('popup')lebih tepat daripada menunggu sembarang tab baru.
page.clock.install() # ambil alih jam browser
page.goto("/arena.html") # buka halaman SETELAH jam dipasang
page.clock.fast_forward("00:03") # lompat 3 detik, tanpa menunggu
expect(page.get_by_role("button", name="Proses")).to_be_enabled() # tombol aktif setelah jam dimajukan
print(page.page_errors()) # daftar error JavaScript di halamanawait page.clock.install(); // ambil alih jam browser
await page.goto('/arena.html'); // buka halaman SETELAH jam dipasang
await page.clock.fastForward('00:03'); // lompat 3 detik, tanpa menunggu
await expect(page.getByRole('button', { name: 'Proses' })).toBeEnabled(); // tombol aktif setelah jam dimajukan
console.log(await page.pageErrors()); // daftar error JavaScript di halamanLatihan
Latihan KlinikQA dan PerpusQA dipasangkan untuk membedakan dialog bawaan browser dengan modal buatan aplikasi. Latihan terakhir memakai page.clock untuk menguji jatuh tempo tanpa menunggu berhari-hari.
Batalkan dialog prompt
Pasang handler yang membatalkan (dismiss), tekan "Tampilkan prompt" di Arena, lalu cek teks hasilnya.
HasilPrompt dibatalkan
Memuat tests/latihan/test_m09_latihan.py…
Memuat tests/latihan/m09-latihan.spec.ts…
Email salah di dalam iframe
Di iframe "Formulir newsletter", isi email tanpa @, tekan Daftar, lalu cek pesan statusnya.
HasilEmail tidak valid
Memuat tests/latihan/test_m09_latihan.py…
Memuat tests/latihan/m09-latihan.spec.ts…
Buka tab baru, lalu tutup lagi
Tangkap tab baru saat mengklik "Buka pusat bantuan", pastikan ada 2 tab, tutup tab baru, lalu pastikan tinggal 1 tab dan Arena masih terlihat.
Hasiljumlah tab 2, lalu 1
Memuat tests/latihan/test_m09_latihan.py…
Memuat tests/latihan/m09-latihan.spec.ts…
Periksa isi file unduhan
Unduh laporan.csv dari Arena, simpan, lalu hitung barisnya dan cek baris pertamanya.
Hasil3 baris, baris pertama id,produk,terjual
Memuat tests/latihan/test_m09_latihan.py…
Memuat tests/latihan/m09-latihan.spec.ts…
Terima dialog konfirmasi di KlinikQA
Brief: di KlinikQA, tombol Batalkan janji temu memunculkan dialog confirm() bawaan browser.
- Login sebagai
pasien/sehat123. - Siapkan satu janji temu (lewat UI atau lewat
page.request), lalu buka halaman detailnya:/klinik-detail.html?id=<nomor janji>. - Tekan Batalkan janji temu, lalu terima dialognya.
- Pastikan statusnya berubah.
Hasilstatus janji temu menjadi Dibatalkan
Memuat tests/latihan/test_m09_latihan.py…
Memuat tests/latihan/m09-latihan.spec.ts…
Modal buatan sendiri di PerpusQA
Brief: di meja pustakawan PerpusQA, menerima pengembalian buku memakai modal buatan aplikasi (role="dialog"), bukan dialog bawaan browser.
- Siapkan data lewat
page.request: buka perpustakaan baru (username acak), daftarkan satu anggota, lalu login sebagai anggota itu dan pinjam buku "Atomic Habits" (bookId13). - Login di halaman PerpusQA sebagai pustakawan perpustakaan tadi.
- Tekan Terima pada pinjaman tadi, temukan modalnya dengan
get_by_role("dialog"), lalu tekan Terima buku. - Pastikan pinjaman itu hilang dari daftar "Sedang dipinjam".
Hasilmodal menyebut Tepat waktu, tanpa denda, lalu tertutup, dan pinjaman itu hilang dari daftar
Memuat tests/latihan/test_m09_latihan.py…
Memuat tests/latihan/m09-latihan.spec.ts…
Perhatikan bedanya dengan latihan KlinikQA: di sini tidak ada page.once("dialog"). Modal buatan aplikasi adalah elemen biasa di halaman, jadi dicari dan diklik seperti tombol lainnya.
Lompati waktu dengan page.clock
Brief: masa pinjam di PerpusQA 7 hari, dan statusnya dihitung dari jam browser. Majukan jam browser dengan page.clock.
- Siapkan anggota baru di perpustakaan sendiri (lewat
page.requestatau lewat meja pustakawan), login sebagai anggota itu, lalu pinjam buku "Clean Code" dari katalog. Buka halaman Pinjaman saya (/perpus-pinjaman.html) dan pastikan statusnyaSisa 7 hari. - Bekukan jam browser tepat 7 hari lagi dengan
page.clock.set_fixed_time()/page.clock.setFixedTime(), lalu muat ulang halaman. - Ulangi untuk 9 hari lagi.
Hasilstatus menjadi Jatuh tempo hari ini, lalu Terlambat 2 hari, denda Rp 2.000
Memuat tests/latihan/test_m09_latihan.py…
Memuat tests/latihan/m09-latihan.spec.ts…
set_fixed_time hanya mengubah jam di browser. Server tetap memakai tanggal asli, jadi tanggal jatuh tempo dari server tidak ikut bergeser, dan selisihnya dihitung di halaman. Untuk menguji denda yang dicatat server, pakai jalan pintas borrowedAt di API (Modul 13).