FrameLocator & iframe (Làm việc với nội dung nhúng)
automationplaywright
FrameLocator là API của Playwright để thao tác với nội dung bên trong <iframe> — thứ mà locator thường không chui vào được. page.frameLocator('selector') trả về một scope mới để getByRole/locator như bình thường, xử lý gọn các case kinh điển như form thanh toán nhúng (Stripe), captcha, chat widget.
Khái niệm & Bản chất
- iframe là một document độc lệt nhúng trong trang — DOM bên trong không thuộc
documentcủa trang cha nênpage.locator()thường không thấy được. FrameLocatorlà 'con trỏ' trỏ vào document của iframe: mọi locator tạo từ nó được resolve trong frame đó, vẫn hưởng đủ auto-wait và tính lazy của locator.page.frame()/page.frames()là API mức thấp trả về objectFrame(đại diện frame đang tồn tại) — khác với FrameLocator vốn là khái niệm locator (resolve lại mỗi lần dùng, chịu được iframe bị re-attach).- Playwright phân biệt rõ:
Frame= instance hiện tại của frame (có thể detach khi iframe re-render),FrameLocator= cách tìm vào frame ổn định hơn. - iframe same-origin và cross-origin đều thao tác được — Playwright làm ở tầng browser nên không bị same-origin policy chặn như JS chạy trong page.
Cách hoạt động & API chính
const frame = page.frameLocator('#payment-iframe')→ rồiframe.getByLabel('Card number').fill('...')— API giống hệt Page.- Chain lồng nhau: iframe trong iframe →
page.frameLocator('#outer').frameLocator('#inner').locator(...)— chain bao nhiêu tầng cũng được. frameLocatortrả về scope, nênframe.getByRole,frame.locator,frame.getByText... dùng y như trên page, kể cảexpect(frame.getByText('OK')).toBeVisible().page.frame({ url: /stripe/ })hoặcpage.frame({ name: 'checkout' })lấy object Frame;page.frames()liệt kê tất cả frame;frame.childFramesduyệt cây.frame.locator('body').owner()đi ngược từ nội dung frame ra chính element<iframe>trên trang cha.
Ví dụ & Cú pháp thực tế
- Điền form Stripe nhúng (mỗi field có thể ở iframe riêng):
``
ts const cardFrame = page.frameLocator('iframe[name^="__privateStripeFrame"]').first(); await cardFrame.getByPlaceholder('1234 1234 1234 1234').fill('4242424242424242'); await cardFrame.getByPlaceholder('MM / YY').fill('12/30'); await cardFrame.getByPlaceholder('CVC').fill('123');`` - Iframe lồng nhau:
``
ts const inner = page .frameLocator('.widget-outer') .frameLocator('iframe.content'); await inner.getByRole('button', { name: 'Gửi' }).click();`` - Assert nội dung trong iframe (web-first, tự retry):
``
ts const frame = page.frameLocator('#invoice-frame'); await expect(frame.getByText('Thanh toán thành công')).toBeVisible();`` - Dùng
page.framekhi cần evaluate trong frame: ``ts const f = page.frame({ url: /checkout\.provider\.com/ }); if (f) { const token = await f.evaluate(() => (window as any).sessionToken); }`` - Lấy element iframe để assert attribute:
``
ts const iframeEl = page.frameLocator('#chat-widget').locator(':scope'); // hoặc đơn giản: page.locator('#chat-widget') để check src/visible await expect(page.locator('#chat-widget')).toHaveAttribute('src', /chat/);``
Best practices & Khi nào dùng
- Luôn dùng
frameLocatortrước: chỉ rớt xuốngpage.frame()khi cầnevaluate(),waitForFunction()hoặc duyệt cây frame — thứ FrameLocator không làm được. - Chọn selector iframe ổn định:
#id>name>srcpattern > class — tránh attribute do lib generate động (ví dụiframe[title*="Stripe"]ổn hơniframe:nth-of-type(2)). - Chờ iframe load xong mới tương tác — may là
frameLocatortự chờ frame attach + element visible, nên cứ dùng web-first assertion là đủ. - Với captcha/reCAPTCHA: đừng test giải captcha thật — dùng test key của Google (luôn pass) hoặc mock siteverify ở backend.
- Khi assert cần đọc data từ frame, vẫn ưu tiên
expect(frame.locator(...))thay vì lôi text ra ngoài so sánh.
Gotchas, So sánh & Lưu ý
frameLocator≠page.frame: FrameLocator là locator (lazy, tự re-resolve),Framelà snapshot object — iframe bị re-render xong object Frame cũ vứt, thao tác tiếp sẽ lỗiFrame has been detached.- Một số payment provider (Stripe, PayPal) render mỗi field một iframe — phải frameLocator từng cái, đừng tưởng một frame chứa hết.
- iframe
srctrỏ về domain khác vẫn OK với Playwright, nhưngpage.route()chỉ chặn được request đi qua context của bạn — request từ iframe vẫn route được vì chung context. - Popup OAuth mở tab mới không phải iframe — dùng
context.waitForEvent('page'), đừng nhầm hai cơ chế. - Shadow DOM trong iframe: frameLocator xuyên được cả hai —
frameLocator('#f').locator('my-component >> css=button')hoặc cứ css/text bình thường vì Playwright pierce shadow mặc định. frame.waitForLoadState()/frame.url()chỉ có trên objectFrame, không có trên FrameLocator — đây là lý do chính phải rớt xuống API cũ.
