Wiki

← Tất cả bài wiki

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 document của trang cha nên page.locator() thường không thấy được.
  • FrameLocator là '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ề object Frame (đạ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ồi frame.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.
  • frameLocator trả về scope, nên frame.getByRole, frame.locator, frame.getByText... dùng y như trên page, kể cả expect(frame.getByText('OK')).toBeVisible().
  • page.frame({ url: /stripe/ }) hoặc page.frame({ name: 'checkout' }) lấy object Frame; page.frames() liệt kê tất cả frame; frame.childFrames duyệ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.frame khi 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 frameLocator trước: chỉ rớt xuống page.frame() khi cần evaluate(), waitForFunction() hoặc duyệt cây frame — thứ FrameLocator không làm được.
  • Chọn selector iframe ổn định: #id > name > src pattern > class — tránh attribute do lib generate động (ví dụ iframe[title*="Stripe"] ổn hơn iframe:nth-of-type(2)).
  • Chờ iframe load xong mới tương tác — may là frameLocator tự 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), Frame là snapshot object — iframe bị re-render xong object Frame cũ vứt, thao tác tiếp sẽ lỗi Frame 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 src trỏ về domain khác vẫn OK với Playwright, nhưng page.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 object Frame, không có trên FrameLocator — đây là lý do chính phải rớt xuống API cũ.