Wiki

← Tất cả bài wiki

Multiple Pages, Tabs & Popups (Nhiều trang và tab)

automationplaywright

Multiple pages/tabs là tình huống một hành động (click link target="_blank", OAuth redirect...) mở ra tab/popup mới trong cùng browser context. Playwright bắt tab mới qua context.waitForEvent('page'), và quản lý toàn bộ tab đang mở qua context.pages() — cùng context nghĩa là cùng cookie/storage, test được luồng xuyên tab.

Khái niệm & Bản chất

  • Page vs Context: BrowserContext là 'phiên trình duyệt' (cookie, storage chung); Page là một tab trong context đó. Tab mới do target="_blank" hay window.open() sinh ra thuộc cùng context → kế thừa session đăng nhập.
  • Sự kiện page: mỗi lần context có tab mới, nó phát event 'page'. context.waitForEvent('page') trả về Page object của tab mới — đây là cách chính thống để 'bắt' popup.
  • Popup trong Playwright gồm cả tab mới lẫn cửa sổ window.open — cùng một cơ chế bắt, khác bài iframe (document lồng trong cùng page).
  • context.pages() trả về mảng tất cả Page đang mở — dùng để đếm tab, tìm tab theo URL, hoặc kiểm tra tab có bị đóng không.
  • Mỗi Page là một object độc lập: có goto, locator, waitForEvent, close() riêng — thao tác tab nào thì dùng đúng object đó.

Cách hoạt động & API chính

  • Pattern chuẩn bắt tab mới: ``ts const popupPromise = context.waitForEvent('page'); await page.getByRole('link', { name: 'Điều khoản' }).click(); const popup = await popupPromise; await popup.waitForLoadState(); ``
  • popup.bringToFront() đưa tab lên trước; popup.close() đóng tab; page.waitForEvent('close') biết khi nào tab bị đóng.
  • page.waitForEvent('popup') — biến thể ở cấp page: chỉ bắt popup do chính page đó mở ra (window.open), trong khi event 'page' của context bắt mọi tab mới kể cả từ extension/iframe.
  • context.pages() + filter: context.pages().find(p => p.url().includes('auth')) để tìm tab theo URL.
  • page.opener() trả về Page đã mở ra popup này (ngược lại chiều popup event) — hữu ích đi ngược từ tab con về tab cha.

Ví dụ & Cú pháp thực tế

  • Link target="_blank" mở tab mới: ``ts test('link điều khoản mở tab mới', async ({ page, context }) => { await page.goto('/signup'); const [termsPage] = await Promise.all([ context.waitForEvent('page'), page.getByRole('link', { name: 'Điều khoản' }).click(), ]); await termsPage.waitForLoadState('domcontentloaded'); await expect(termsPage).toHaveURL(/\/terms/); await expect(termsPage.getByRole('heading')).toContainText('Điều khoản'); await termsPage.close(); }); ``
  • Luồng OAuth qua popup (Google login giả lập): ``ts const [authPopup] = await Promise.all([ context.waitForEvent('page'), page.getByRole('button', { name: 'Đăng nhập Google' }).click(), ]); await authPopup.getByLabel('Email').fill('test@gmail.com'); // ... hoàn tất flow trên popup, nó tự đóng, tab gốc được redirect await page.waitForURL('**/dashboard'); ``
  • Kiểm tra số tab đang mở: ``ts await page.getByText('Mở 3 báo cáo').click(); await expect.poll(() => context.pages().length).toBe(4); // gốc + 3 mới ``
  • Quay lại tab gốc sau khi đóng popup: ``ts await popup.close(); await page.bringToFront(); // optional, headless không cần await expect(page.getByText('Đã xác thực')).toBeVisible(); ``
  • Bắt popup do chính page mở (window.open): ``ts const [preview] = await Promise.all([ page.waitForEvent('popup'), page.getByRole('button', { name: 'Xem trước' }).click(), ]); await expect(preview.locator('.doc')).toBeVisible(); ``

Best practices & Khi nào dùng

  • Luôn bọc waitForEvent trong Promise.all hoặc tạo promise trước khi trigger — tab mở nhanh hơn bạn đăng ký listener là miss event, test treo đến timeout.
  • Chọn đúng event: context.waitForEvent('page') cho mọi tab mới; page.waitForEvent('popup') khi muốn chắc popup do page này sinh (tránh bắt nhầm tab từ hành động khác chạy song song).
  • Sau khi bắt popup, await popup.waitForLoadState() hoặc assert thẳng bằng web-first (expect(popup).toHaveURL(...)) — đừng thao tác ngay khi document chưa sẵn sàng.
  • Đóng popup sau khi xong (popup.close()) để test sau không bị context.pages() dư tab — đặc biệt khi reuse context giữa các test.
  • Khi app cho phép, ưu tiên đổi target="_blank" thành điều hướng cùng tab bằng element.evaluate(el => el.removeAttribute('target')) nếu chỉ cần verify nội dung — đơn giản hóa test.

Gotchas, So sánh & Lưu ý

  • Headless vẫn xử lý đa tab bình thường — 'tab' ở đây là Page object, không cần UI thật; đừng nghĩ headless chỉ có 1 tab.
  • Popup bị browser block (khi window.open không do user gesture): trong Playwright thì không bị block vì automation được phép, nhưng nếu dùng page.evaluate(() => window.open(...)) gọi trực tiếp vẫn ổn — chỉ lưu ý khác với hành vi người dùng thật trên trình duyệt thường.
  • Tab mới ở context khác (ví dụ link mở sang app không chia sẻ context) sẽ không xuất hiện trong context.pages() — Playwright mỗi test một context, link external vẫn mở trong cùng context nên hiếm gặp.
  • context.pages() trả về cả tab đã about:blank trống — filter theo URL trước khi lấy pages()[1] theo index, vì thứ tự mảng không luôn trực quan.
  • OAuth provider thật (Google, FB) có bot detection — test login thật qua popup dễ bị chặn; nên mock route OAuth hoặc dùng storageState đã đăng nhập sẵn.
  • Video/trace của mỗi Page ghi riêng — khi debug test đa tab, check trace cho đúng page bị fail vì UI của Trace Viewer gộp cả context.