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:
BrowserContextlà 'phiên trình duyệt' (cookie, storage chung);Pagelà một tab trong context đó. Tab mới dotarget="_blank"haywindow.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ềupopupevent) — 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
waitForEventtrongPromise.allhoặ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ằngelement.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.openkhông do user gesture): trong Playwright thì không bị block vì automation được phép, nhưng nếu dùngpage.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:blanktrống — filter theo URL trước khi lấypages()[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.
