D
P
0

Web Development

Kartu Nyangkut di State `:hover` di Viewport Mobile Playwright? Itu Hover Hantu dari Mouse Virtual, Bukan Bug Situs

4 Juli 2026·3 menit baca
Kartu Nyangkut di State `:hover` di Viewport Mobile Playwright? Itu Hover Hantu dari Mouse Virtual, Bukan Bug Situs

Saya sedang memverifikasi tampilan sebuah situs di viewport mobile lewat Playwright sebelum rilis. Screenshot demi screenshot aman, sampai satu kartu di grid bikin saya berhenti: overlay-nya tampil, judulnya bergaris bawah. Itu jelas-jelas state hover. Di viewport sentuh. Layar sentuh tidak punya kursor, jadi :hover seharusnya tidak pernah menyala di sana. Kelihatan seperti bug sticky-hover klasik di mobile, dan saya sudah setengah jalan menulis laporan bug lengkap dengan patch CSS-nya.

Untung saya berhenti sebentar. Sebelum "memperbaiki", saya coba reproduksi di HP sungguhan: buka halaman yang sama, scroll ke kartu yang sama, tap-tap di sekitarnya. Tidak ada overlay yang nyangkut. Tidak ada garis bawah. Kartu itu baik-baik saja di perangkat asli. Jadi bug-nya cuma ada di dalam Playwright. Itu petunjuk besar: kalau sebuah "bug" hanya muncul di dalam alat, kemungkinan besar pelakunya alat itu sendiri.

Kenapa ini terjadi

Saya scroll balik langkah-langkah yang dijalankan otomasi sebelum screenshot diambil. Di situ ketemu: beberapa langkah sebelumnya, ada click() di sebuah elemen. Di Playwright, click() bukan sekadar event sintetis. Dia menggerakkan mouse virtual ke koordinat elemen itu, menekan tombol, lalu selesai. Yang tidak pernah saya sadari: mouse virtual itu ditinggal di sana. Dia tidak pernah pindah sendiri. Tidak ada tangan manusia yang menggeser kursor keluar layar setelah klik.

test.use({ ...devices["Pixel 7"] });
 
test("grid looks clean on mobile", async ({ page }) => {
  await page.goto("/work");
 
  // this click parks the virtual mouse at the button's coordinates
  await page.getByRole("button", { name: "Show all" }).click();
 
  // the grid reflows, a card slides under the parked cursor,
  // and CSS :hover engages -- on a "touch" viewport
  await expect(page).toHaveScreenshot("grid-mobile.png");
});

Setelah klik itu, grid reflow dan sebuah kartu meluncur tepat ke bawah posisi kursor yang parkir. CSS :hover langsung nyala dan bertahan, karena kursornya memang tidak pernah pergi. Hover hantu.

Bagian yang paling menjebak: saya pikir dengan mengemulasi viewport mobile, mouse-nya ikut hilang. Ternyata tidak. Emulasi sentuh di Playwright mengubah ukuran layar, user-agent, dan touch events, tapi pointer mouse yang diemulasikan tetap ada dan tetap punya posisi. HP sungguhan tidak punya kursor yang diam menunggu di satu titik; browser otomasi punya. State yang saya lihat mustahil terjadi di perangkat asli.

Perbaikannya

Perbaikannya bukan di situs, tapi di harness pengetesan. Sebelum assert apa pun yang bersifat visual, pindahkan pointer ke pojok netral supaya tidak ada elemen yang sedang di-hover:

// clear any phantom hover before asserting visual state
await page.mouse.move(0, 0);
await expect(page).toHaveScreenshot("grid-mobile.png");

Saya jalankan ulang dengan satu baris itu. Overlay hilang, garis bawah hilang, kartu kembali normal. Tidak ada bug situs sama sekali. Alternatifnya, kalau alur pengetesan memungkinkan, reload halaman dulu sebelum screenshot; halaman yang baru dimuat belum pernah disentuh pointer, jadi bersih dari state sisa.

Sabuk plus suspender: guard di CSS

Meskipun situsnya terbukti tidak salah, ada satu pengerasan yang tetap layak dipasang: bungkus semua style yang khusus hover di dalam media query hover: hover, supaya browser yang input utamanya sentuhan tidak pernah menerapkannya:

/* hover styles cannot engage on touch-primary devices */
@media (hover: hover) {
  .card:hover .card-overlay {
    opacity: 1;
  }
  .card:hover .card-title {
    text-decoration: underline;
  }
}

Ini bukan cuma menutup celah hover hantu di alat otomasi. Ini juga memperbaiki sticky-hover yang sungguhan di layar sentuh, karena beberapa browser mobile memang mengaktifkan :hover setelah tap dan membiarkannya nyangkut sampai tap berikutnya. Satu guard, dua masalah beres.

Pelajaran

Yang hampir terjadi di sini lebih menakutkan dari bug-nya: saya hampir mengirim "perbaikan" untuk masalah yang tidak pernah ada, berdasarkan kesaksian sebuah alat yang mencemari TKP-nya sendiri. Sekarang, sebelum melaporkan bug yang ditemukan lewat otomasi, saya jalankan checklist pendek ini:

  • Tanya dulu: state apa yang ditambahkan alatnya? Posisi kursor, fokus keyboard, emulasi prefers-reduced-motion dan kawan-kawannya.
  • page.mouse.move(0, 0) sebelum setiap assert visual, atau reload halaman supaya mulai dari state bersih.
  • Bungkus style khusus hover dalam @media (hover: hover).
  • Reproduksi di perangkat sungguhan sebelum menulis satu baris pun perbaikan.

Browser otomasi itu saksi yang berguna, tapi dia bukan saksi netral. Dia membawa kursor, fokus, dan asumsi-asumsinya sendiri ke setiap halaman yang dia buka. Sejak kejadian ini, pertanyaan pertama saya saat melihat anomali lewat alat bukan lagi "kenapa situsnya rusak", melainkan "jejak apa yang barusan ditinggalkan alat saya?"