VALIDATION LABIFRAMES

Embed

Iframes

Reach into an iframe, fill a form inside it, and verify the parent receives the submission via postMessage.

Advanced

Scenario

Stripe, Auth0, embedded video players, support widgets — iframes appear everywhere. Tests that ignore the frame boundary find nothing because `page.locator(...)` cannot pierce frames. `frameLocator` is the answer.

Parent counter: 0

Live widget · interact freely

Manual test checklist

  • 1Submit the iframe form — parent counter increments
  • 2Inspect the message in DevTools — confirm `postMessage` shape is what the parent expects
  • 3Reload the parent — counter resets to 0 (no localStorage persistence)
  • 4Switch viewports — iframe width is responsive
  • 5Confirm the iframe has a meaningful `title` attribute

Expected result

Submitting inside the frame updates a counter on the parent page.

Automation challenge

Use `page.frameLocator('[data-testid=frame-host] iframe')` to target the inner form. Fill, submit, then assert the parent counter incremented. Forgetting `frameLocator` is the most common iframe testing mistake.

Stable selectors

  • Embedded form[data-testid="frame-host"]
  • Parent counter[data-testid="frame-counter"]

Locator strategy

Three levels from simple IDs to scoped Playwright locators. IDs and names are easy to learn but are not always the best long-term choice when labels change or components repeat.

Use simple IDs and names to understand how locating elements works.

1
2
3
4
await page.goto('https://lab.hakdogan.com/practice/iframes');

const frame = page.frameLocator('[data-testid="frame-host"] iframe');
await frame.getByTestId('frame-input').fill('x@y.com');

Avoid as primary strategies

XPath (unless there is no alternative), long CSS chains, Tailwind-style utility class selectors, generated or unstable IDs, and volatile framework internals break when layout, styling, or DOM structure shifts.

1
2
await page.locator('.w-full.rounded-xl.border.bg-blue-500').fill('demo@example.com');
await page.locator('//div[2]/form/div[1]/input').fill('demo@example.com');

Reference Playwright spec

iframes.spec.ts
ts
1
2
3
4
5
6
7
8
9
10
11
import { test, expect } from '@playwright/test';

test('interact across iframe boundary', async ({ page }) => {
  await page.goto('https://lab.hakdogan.com/practice/iframes');

  const frame = page.frameLocator('[data-testid="frame-host"] iframe');
  await frame.getByTestId('frame-input').fill('cross-frame value');
  await frame.getByTestId('frame-submit').click();

  await expect(page.getByTestId('frame-counter')).toContainText('1');
});

Headed Test Playback

Simulated headed-browser flow — no real browser is launched.

Automation-style playback (Playwright-shaped logs). No real browser; no commands run on your machine.

Idleiframes.spec.ts
Failure demos
https://lab.hakdogan.com/practice/iframes
playwright · headed · chromium0.00s
# awaiting Run Test · terminal scrolls automatically
Steps
  1. Reaching into iframe
  2. Submitting cross-frame form
  3. Asserting parent counter
StatusIdle
BrowserChromium
FrameworkPlaywright + TypeScript
Elapsed (live)--
Speciframes.spec.ts