🎭 Special DOM Elements in Playwright
The Hidden Rooms Story 🚪
Imagine you’re exploring a magical house. Most rooms are easy to enter—you just walk in. But some rooms have special doors:
- iFrames are like glass boxes inside a room—you can see what’s inside, but you need a special key to touch anything in there.
- Shadow DOM is like a secret room behind a painting—it exists, but it’s hidden from normal view.
Today, we’ll learn how Playwright helps us explore these special rooms!
🖼️ What is an iFrame?
An iFrame (inline frame) is a webpage inside another webpage. Think of it like a TV screen in your living room—the TV shows a different channel, but it’s still inside your house.
Real Life Examples:
- 📺 YouTube video embedded on a blog
- 💳 Payment form from Stripe
- 🗺️ Google Maps on a restaurant website
- 📝 A form from another website
The Problem:
Normal finder: "I'll click that button!"
Browser: "Which button? The one in YOUR page or the one INSIDE the TV?"
Playwright can’t just “see” inside iFrames automatically. It’s like trying to change the TV channel by touching the wall—you need to point the remote at the TV!
🔑 The frameLocator Method
frameLocator() is your special key to enter the glass box.
Basic Syntax:
// Step 1: Find the iFrame
const frame = page.frameLocator('#my-iframe');
// Step 2: Now interact with stuff INSIDE
await frame.locator('button').click();
Simple Analogy:
🏠 Your Page = Your House
🖼️ iFrame = TV Screen
🔑 frameLocator = TV Remote
Without remote: You can't change channels
With remote: You control what's on TV!
Finding iFrames by Different Ways:
// By ID
const frame = page.frameLocator('#payment-frame');
// By name attribute
const frame = page.frameLocator('[name="video-player"]');
// By CSS selector
const frame = page.frameLocator('iframe.embed-video');
// By source URL (partial match)
const frame = page.frameLocator('iframe[src*="youtube"]');
Complete Example:
// A YouTube video embedded on a blog
const videoFrame = page.frameLocator('#youtube-embed');
// Click the play button inside YouTube
await videoFrame.locator('.play-button').click();
// Check the video title inside the frame
const title = await videoFrame
.locator('.video-title')
.textContent();
🎁 Nested iFrames (Boxes Inside Boxes!)
Sometimes you have an iFrame inside another iFrame. It’s like those Russian nesting dolls—open one, find another inside!
graph TD A["🏠 Main Page"] --> B["📺 Outer iFrame"] B --> C["📱 Inner iFrame"] C --> D["🔘 The Button You Want"]
Real World Example:
A website shows a payment widget, and that widget loads a bank verification form.
How to Handle Nested iFrames:
// Chain the frameLocators!
const outerFrame = page.frameLocator('#widget-frame');
const innerFrame = outerFrame.frameLocator('#bank-form');
// Now click the button deep inside
await innerFrame.locator('#verify-btn').click();
Think of it Like This:
🏠 Main Page
└── 📺 Widget Frame (frameLocator #1)
└── 🏦 Bank Frame (frameLocator #2)
└── ✅ Verify Button (finally!)
Another Example - Three Levels Deep:
// Level 1: Social media embed
const socialFrame = page.frameLocator('#social-widget');
// Level 2: Comment section inside social widget
const commentFrame = socialFrame.frameLocator('#comments');
// Level 3: Reply form inside comments
const replyFrame = commentFrame.frameLocator('#reply-box');
// Now type in the deeply nested input!
await replyFrame.locator('textarea').fill('Great post!');
👻 Shadow DOM - The Hidden World
What is Shadow DOM?
Shadow DOM is like a secret compartment in furniture. The drawer looks normal from outside, but it has a hidden section that regular searches can’t find.
Web components use Shadow DOM to hide their internal structure. It keeps their code private and protected.
Everyday Examples:
- 🎵 Browser’s built-in
<audio>controls - 🎬 Browser’s built-in
<video>controls - 📅 Custom date picker components
- 🎨 Fancy custom dropdowns
The Magic Part - Playwright Handles It! ✨
Here’s the amazing news: Playwright automatically sees through Shadow DOM!
// This works even if #my-button is inside Shadow DOM!
await page.locator('#my-button').click();
Regular Tools vs Playwright:
Regular JavaScript:
document.querySelector('#shadow-button');
// Returns null! Can't see it! 😢
Playwright:
page.locator('#shadow-button');
// Found it! No problem! 😊
Why Does This Matter?
Many modern websites use Web Components with Shadow DOM:
- Salesforce Lightning
- GitHub (parts of it)
- YouTube
- Google products
With Playwright, you don’t need special tricks—it just works!
🔍 Shadow DOM - When You Need More Control
Sometimes you want to be specific about Shadow DOM. Here’s how:
Structure of Shadow DOM:
graph TD A["Regular DOM"] --> B["Shadow Host Element"] B --> C["Shadow Root"] C --> D["Hidden Elements Inside"] D --> E["Button, Input, etc."]
Example Component:
<!-- This is what you see in HTML -->
<my-custom-button id="fancy-btn">
#shadow-root
<button class="internal-btn">Click Me</button>
</my-custom-button>
How Playwright Sees It:
// Playwright pierces through automatically!
await page.locator('my-custom-button button').click();
// Or be more specific
await page.locator('#fancy-btn button').click();
// Both find the button inside Shadow DOM!
Working with Custom Web Components:
// A custom dropdown component
const dropdown = page.locator('fancy-dropdown');
// Click to open (even if internal structure is hidden)
await dropdown.locator('.dropdown-trigger').click();
// Select an option inside
await dropdown.locator('.option-item').first().click();
🎯 Quick Reference
| What You Need | How to Do It |
|---|---|
| Enter one iFrame | page.frameLocator('#id') |
| Enter nested iFrame | Chain .frameLocator() calls |
| Click in Shadow DOM | Just use .locator() normally! |
| Find by frame name | page.frameLocator('[name="x"]') |
🌟 Golden Rules
For iFrames:
- Always use frameLocator() to enter iFrames
- Chain for nested iFrames
- Selectors work normally once you’re “inside”
For Shadow DOM:
- Playwright auto-pierces Shadow DOM
- No special syntax needed for most cases
- Regular locators just work!
🎮 Putting It All Together
Here’s a real-world scenario:
const { test, expect } = require('@playwright/test');
test('complete payment in nested frame', async ({ page }) => {
await page.goto('https://shop.example.com/checkout');
// Enter the payment widget iFrame
const paymentFrame = page.frameLocator('#payment-widget');
// Enter the card form (nested inside payment widget)
const cardFrame = paymentFrame.frameLocator('#card-form');
// Fill the card number (Shadow DOM - auto handled!)
await cardFrame.locator('#card-number').fill('4242424242424242');
// Fill expiry
await cardFrame.locator('#expiry').fill('12/25');
// Fill CVC
await cardFrame.locator('#cvc').fill('123');
// Submit (back in the outer payment frame)
await paymentFrame.locator('#pay-now').click();
// Verify success (back on main page)
await expect(page.locator('.success-message')).toBeVisible();
});
🎉 You Did It!
Now you know how to:
- ✅ Use
frameLocator()to enter iFrames - ✅ Handle nested iFrames by chaining
- ✅ Work with Shadow DOM (Playwright does it automatically!)
- ✅ Navigate complex modern web pages
Think of it this way:
- iFrames = Glass boxes you need a key for (frameLocator)
- Shadow DOM = Hidden rooms Playwright can already see into!
Go explore those special DOM elements with confidence! 🚀
