Iframe Handling & Config Options

Master iframe interactions and advanced Playwright configuration for professional test automation

🕒 Teaching Flow

1. Story Intro (5 min)

👉 "Imagine iframes are like picture frames hanging on a wall - each frame shows a different website inside your main website. Just like you need to step closer to read what's in a specific frame, Playwright needs special instructions to interact with iframe content."
Real-World Analogy:
Iframe = Picture frame showing another website
Config Options = Settings panel for your test environment
Parallel Running = Multiple workers doing different tasks simultaneously

Part 1: Iframe Handling (40 min)

What is an Iframe? (Simple Explanation)

Iframe (Inline Frame) is like a window inside a window. It's a way to embed another website or content inside your current webpage.

Think of it this way:
  • 🏠 Main Website = Your house
  • 🖼️ Iframe = A TV screen showing another channel
  • 📺 Iframe Content = The show playing on that TV

Example: YouTube videos embedded in websites, Google Maps, payment forms, chat widgets

Why Iframes are Tricky for Testing

Regular Elements Iframe Elements
✅ Direct access ❌ Need special navigation
🎯 Simple locators 🔍 Complex locator paths
⚡ Fast interaction ⏳ Slower (context switching)
🔄 Easy debugging 🐛 Harder to debug

Step 1: Basic Iframe Detection

📂 tests/iframe-basics.spec.js

import { test, expect } from '@playwright/test';

test.describe('Iframe Basics', () => {
  test('Detect iframe on page', async ({ page }) => {
    await page.goto('https://the-internet.herokuapp.com/iframe');
    
    // Method 1: Count total iframes
    const iframeCount = await page.locator('iframe').count();
    console.log(`📊 Total iframes found: ${iframeCount}`);
    
    // Method 2: Check if specific iframe exists
    const iframeExists = await page.locator('iframe#mce_0_ifr').isVisible();
    expect(iframeExists).toBeTruthy();
    
    // Method 3: Get iframe attributes
    const iframeSrc = await page.locator('iframe').getAttribute('src');
    console.log('🔗 Iframe source:', iframeSrc);
  });
});

Step 2: Accessing Iframe Content

Method 1: Using frameLocator() (Recommended)

test('Access iframe content with frameLocator', async ({ page }) => {
  await page.goto('https://the-internet.herokuapp.com/iframe');
  
  // Step 1: Get the iframe using frameLocator
  const iframe = page.frameLocator('iframe#mce_0_ifr');
  
  // Step 2: Interact with elements inside iframe
  await iframe.locator('body').click();
  await iframe.locator('body').fill('Hello from Playwright!');
  
  // Step 3: Verify content
  const content = await iframe.locator('body').textContent();
  expect(content).toContain('Hello from Playwright!');
  
  console.log('✅ Successfully interacted with iframe content');
});

Step 3: Multiple Iframe Handling

Handling Multiple Iframes

test('Handle multiple iframes', async ({ page }) => {
  await page.goto('https://the-internet.herokuapp.com/nested_frames');
  
  // Method 1: Access nested iframes
  const topFrame = page.frameLocator('frame[name="frame-top"]');
  const middleFrame = topFrame.frameLocator('frame[name="frame-middle"]');
  
  // Get text from nested iframe
  const middleText = await middleFrame.locator('body').textContent();
  expect(middleText).toContain('MIDDLE');
  console.log('📝 Middle frame text:', middleText);
  
  // Method 2: Access by index
  const firstFrame = page.frameLocator('frame').first();
  const firstFrameText = await firstFrame.locator('body').textContent();
  console.log('📝 First frame text:', firstFrameText);
  
  // Method 3: Access by name attribute
  const leftFrame = page.frameLocator('frame[name="frame-left"]');
  const leftFrameText = await leftFrame.locator('body').textContent();
  expect(leftFrameText).toContain('LEFT');
  console.log('📝 Left frame text:', leftFrameText);
});

Step 4: Real-World Iframe Examples

YouTube Video Iframe

test('Interact with YouTube iframe', async ({ page }) => {
  // Create a simple HTML page with YouTube iframe
  await page.setContent(`
    
      
        

My Website

`); // Wait for iframe to load await page.waitForSelector('iframe#youtube-player'); // Access iframe content const youtubeFrame = page.frameLocator('iframe#youtube-player'); // Check if play button exists (YouTube iframe) const playButton = youtubeFrame.locator('.ytp-play-button'); await expect(playButton).toBeVisible(); console.log('✅ YouTube iframe loaded successfully'); });

Step 5: Google Maps Iframe

Google Maps Integration

test('Interact with Google Maps iframe', async ({ page }) => {
  await page.setContent(`
    
      
        

Location Finder

`); // Wait for maps to load await page.waitForSelector('iframe#google-maps'); const mapsFrame = page.frameLocator('iframe#google-maps'); // Wait for map content to be ready await mapsFrame.locator('[role="main"]').waitFor({ state: 'visible' }); console.log('✅ Google Maps iframe loaded successfully'); });

Part 2: Configuration Options (50 min)

What are Configuration Options? (Simple Explanation)

Configuration Options are like the settings panel for your test environment. They tell Playwright how to behave, what devices to use, and how to run your tests.

Think of it this way:
  • 🎮 Config File = Game settings menu
  • 📱 Devices = Different gaming consoles
  • 🖥️ Viewport = Screen size and resolution
  • 👥 Workers = Number of players playing simultaneously

Step 1: Basic playwright.config.js Setup

📂 playwright.config.js - Basic Configuration

import { defineConfig, devices } from '@playwright/test';

export default defineConfig({
  // Test directory
  testDir: './tests',
  
  // Global test timeout
  timeout: 30 * 1000, // 30 seconds
  
  // Expect timeout
  expect: {
    timeout: 5000, // 5 seconds
  },
  
  // Retry failed tests
  retries: 1, // Retry once on failure
  
  // Number of workers (parallel execution)
  workers: 3, // Run 3 tests in parallel
  
  // Reporter configuration
  reporter: [
    ['html'],
    ['json', { outputFile: 'test-results.json' }]
  ],
  
  // Global test options
  use: {
    // Base URL for all tests
    baseURL: 'https://learn-playwright.great-site.net',
    
    // Browser context options
    trace: 'on-first-retry',
    screenshot: 'only-on-failure',
    video: 'retain-on-failure',
    
    // Viewport size
    viewport: { width: 1280, height: 720 },
    
    // Ignore HTTPS errors
    ignoreHTTPSErrors: true,
  },
  
  // Projects for different browsers/devices
  projects: [
    {
      name: 'chromium',
      use: { ...devices['Desktop Chrome'] },
    },
    {
      name: 'firefox',
      use: { ...devices['Desktop Firefox'] },
    },
    {
      name: 'webkit',
      use: { ...devices['Desktop Safari'] },
    },
  ],
});

Step 2: Command Line Configuration Options

Using --config Flag

# Use a different config file
npx playwright test --config=playwright-mobile.config.js

# Use specific project
npx playwright test --project=chromium

# Use specific device
npx playwright test --project="iPhone 12"

# Run with specific workers
npx playwright test --workers=5

# Run with specific retries
npx playwright test --retries=2

# Run in headed mode (see browser)
npx playwright test --headed

# Run in debug mode
npx playwright test --debug

Step 3: Viewport Configuration

Different Viewport Settings

// Method 1: Global viewport in config
use: {
  viewport: { width: 1920, height: 1080 }, // Full HD
}

// Method 2: Per-test viewport
test('Mobile viewport test', async ({ page }) => {
  await page.setViewportSize({ width: 375, height: 667 }); // iPhone SE
  await page.goto('https://learn-playwright.great-site.net');
  
  // Test mobile-specific elements
  await expect(page.locator('.mobile-menu')).toBeVisible();
});

// Method 3: Dynamic viewport
test('Responsive design test', async ({ page }) => {
  const viewports = [
    { width: 320, height: 568 },   // iPhone 5
    { width: 768, height: 1024 },  // iPad
    { width: 1920, height: 1080 }  // Desktop
  ];
  
  for (const viewport of viewports) {
    await page.setViewportSize(viewport);
    await page.goto('https://learn-playwright.great-site.net');
    
    console.log(`Testing viewport: ${viewport.width}x${viewport.height}`);
    // Add viewport-specific assertions
  }
});

Step 4: Device Configuration

📂 playwright.config.js - Device Projects

import { defineConfig, devices } from '@playwright/test';

export default defineConfig({
  projects: [
    // Desktop browsers
    {
      name: 'Desktop Chrome',
      use: { ...devices['Desktop Chrome'] },
    },
    {
      name: 'Desktop Firefox',
      use: { ...devices['Desktop Firefox'] },
    },
    {
      name: 'Desktop Safari',
      use: { ...devices['Desktop Safari'] },
    },
    
    // Mobile devices
    {
      name: 'iPhone 12',
      use: { ...devices['iPhone 12'] },
    },
    {
      name: 'iPhone 12 Pro',
      use: { ...devices['iPhone 12 Pro'] },
    },
    {
      name: 'Pixel 5',
      use: { ...devices['Pixel 5'] },
    },
    
    // Tablet devices
    {
      name: 'iPad',
      use: { ...devices['iPad'] },
    },
    {
      name: 'iPad Pro',
      use: { ...devices['iPad Pro'] },
    },
    
    // Custom device
    {
      name: 'Custom Mobile',
      use: {
        ...devices['iPhone 12'],
        viewport: { width: 414, height: 896 },
        userAgent: 'Custom Mobile Browser',
      },
    },
  ],
});

Step 5: Permissions Configuration

Setting Browser Permissions

// Method 1: Global permissions in config
use: {
  permissions: ['geolocation', 'camera', 'microphone'],
  geolocation: { latitude: 40.7128, longitude: -74.0060 }, // New York
}

// Method 2: Per-test permissions
test('Location-based test', async ({ page, context }) => {
  // Grant geolocation permission
  await context.grantPermissions(['geolocation']);
  
  // Set location
  await context.setGeolocation({ latitude: 37.7749, longitude: -122.4194 }); // San Francisco
  
  await page.goto('https://maps.google.com');
  
  // Test location-based functionality
  await page.click('[aria-label="Your location"]');
  await expect(page.locator('text=San Francisco')).toBeVisible();
});

// Method 3: Camera and microphone permissions
test('Video call test', async ({ page, context }) => {
  await context.grantPermissions(['camera', 'microphone']);
  
  await page.goto('https://meet.google.com');
  
  // Test camera/microphone access
  await page.click('[aria-label="Turn on camera"]');
  await expect(page.locator('.camera-preview')).toBeVisible();
});

Step 6: Retries Configuration

Retry Strategies

// Method 1: Global retries in config
export default defineConfig({
  retries: 1, // Retry once on failure
});

// Method 2: Per-project retries
projects: [
  {
    name: 'chromium',
    retries: 2, // Retry twice for Chrome
    use: { ...devices['Desktop Chrome'] },
  },
  {
    name: 'firefox',
    retries: 0, // No retries for Firefox
    use: { ...devices['Desktop Firefox'] },
  },
]

// Method 3: Per-test retries
test('Flaky test with retries', async ({ page }) => {
  // This test will retry if it fails
  await page.goto('https://learn-playwright.great-site.net');
  
  // Sometimes this might fail due to network issues
  await page.click('.unreliable-button');
  await expect(page.locator('.success-message')).toBeVisible();
});

// Method 4: Conditional retries
test('Network-dependent test', async ({ page }) => {
  // Retry logic within test
  let retries = 3;
  while (retries > 0) {
    try {
      await page.goto('https://unreliable-api.com');
      await expect(page.locator('.api-response')).toBeVisible();
      break; // Success, exit retry loop
    } catch (error) {
      retries--;
      if (retries === 0) throw error;
      console.log(`Retrying... ${retries} attempts left`);
      await page.waitForTimeout(1000);
    }
  }
});

Part 3: Parallel Running & Workers (30 min)

What is Parallel Running? (Simple Explanation)

Parallel Running is like having multiple workers doing different tasks at the same time, instead of one worker doing all tasks one by one.

Think of it this way:
  • 🐌 Sequential (1 worker) = One person washing dishes one by one
  • 🚀 Parallel (3 workers) = Three people washing different dishes simultaneously
  • Time Saved = 3x faster execution

Step 1: Workers Configuration

Setting Up Workers

// Method 1: Global workers in config
export default defineConfig({
  workers: 3, // Run 3 tests in parallel
});

// Method 2: Command line workers
npx playwright test --workers=5

// Method 3: Percentage of CPU cores
export default defineConfig({
  workers: '50%', // Use 50% of available CPU cores
});

// Method 4: Auto-detect optimal workers
export default defineConfig({
  workers: process.env.CI ? 2 : undefined, // 2 workers in CI, auto-detect locally
});

Step 2: Serial vs Parallel Test Execution

📂 tests/parallel-vs-serial.spec.js

import { test, expect } from '@playwright/test';

// PARALLEL EXECUTION (Default)
test.describe('Parallel Tests', () => {
  test('Test 1 - Login', async ({ page }) => {
    console.log('🚀 Starting Test 1 at:', new Date().toISOString());
    await page.goto('https://learn-playwright.great-site.net');
    await page.click('text=Sign in');
    await page.fill('#email', 'test@example.com');
    await page.fill('#password', 'password123');
    await page.click('button[type="submit"]');
    await expect(page.locator('.welcome-message')).toBeVisible();
    console.log('✅ Test 1 completed at:', new Date().toISOString());
  });

  test('Test 2 - Search', async ({ page }) => {
    console.log('🚀 Starting Test 2 at:', new Date().toISOString());
    await page.goto('https://learn-playwright.great-site.net');
    await page.fill('[placeholder*="Search"]', 'dress');
    await page.press('[placeholder*="Search"]', 'Enter');
    await expect(page.locator('.search-results')).toBeVisible();
    console.log('✅ Test 2 completed at:', new Date().toISOString());
  });

  test('Test 3 - Cart', async ({ page }) => {
    console.log('🚀 Starting Test 3 at:', new Date().toISOString());
    await page.goto('https://learn-playwright.great-site.net');
    await page.click('.add-to-cart');
    await expect(page.locator('.cart-count')).toContainText('1');
    console.log('✅ Test 3 completed at:', new Date().toISOString());
  });
});

// SERIAL EXECUTION (One after another)
test.describe.serial('Serial Tests', () => {
  test('Step 1 - Setup', async ({ page }) => {
    console.log('🚀 Starting Step 1 at:', new Date().toISOString());
    await page.goto('https://learn-playwright.great-site.net');
    await page.click('text=Sign in');
    await page.fill('#email', 'test@example.com');
    await page.fill('#password', 'password123');
    await page.click('button[type="submit"]');
    console.log('✅ Step 1 completed at:', new Date().toISOString());
  });

  test('Step 2 - Add Product', async ({ page }) => {
    console.log('🚀 Starting Step 2 at:', new Date().toISOString());
    // This test depends on Step 1 being completed
    await expect(page.locator('.welcome-message')).toBeVisible();
    await page.click('.product-card');
    await page.click('.add-to-cart');
    console.log('✅ Step 2 completed at:', new Date().toISOString());
  });

  test('Step 3 - Checkout', async ({ page }) => {
    console.log('🚀 Starting Step 3 at:', new Date().toISOString());
    // This test depends on Step 2 being completed
    await expect(page.locator('.cart-count')).toContainText('1');
    await page.click('.checkout-button');
    await expect(page.locator('.order-summary')).toBeVisible();
    console.log('✅ Step 3 completed at:', new Date().toISOString());
  });
});

Step 3: Test Tags and Filtering

Using --grep for Test Filtering

import { test, expect } from '@playwright/test';

// Tag tests with different categories
test.describe('Login Tests @smoke @critical', () => {
  test('Valid login @positive', async ({ page }) => {
    await page.goto('https://learn-playwright.great-site.net');
    await page.click('text=Sign in');
    await page.fill('#email', 'valid@example.com');
    await page.fill('#password', 'validpassword');
    await page.click('button[type="submit"]');
    await expect(page.locator('.welcome-message')).toBeVisible();
  });

  test('Invalid login @negative', async ({ page }) => {
    await page.goto('https://learn-playwright.great-site.net');
    await page.click('text=Sign in');
    await page.fill('#email', 'invalid@example.com');
    await page.fill('#password', 'wrongpassword');
    await page.click('button[type="submit"]');
    await expect(page.locator('.error-message')).toBeVisible();
  });
});

test.describe('Search Tests @regression', () => {
  test('Search by product name @search', async ({ page }) => {
    await page.goto('https://learn-playwright.great-site.net');
    await page.fill('[placeholder*="Search"]', 'dress');
    await page.press('[placeholder*="Search"]', 'Enter');
    await expect(page.locator('.search-results')).toBeVisible();
  });

  test('Search by category @search', async ({ page }) => {
    await page.goto('https://learn-playwright.great-site.net');
    await page.click('text=Clothes');
    await expect(page.locator('.category-products')).toBeVisible();
  });
});

test.describe('Cart Tests @e2e', () => {
  test('Add to cart @cart', async ({ page }) => {
    await page.goto('https://learn-playwright.great-site.net');
    await page.click('.product-card');
    await page.click('.add-to-cart');
    await expect(page.locator('.cart-count')).toContainText('1');
  });

  test('Remove from cart @cart', async ({ page }) => {
    await page.goto('https://learn-playwright.great-site.net');
    await page.click('.product-card');
    await page.click('.add-to-cart');
    await page.click('.remove-from-cart');
    await expect(page.locator('.cart-count')).toContainText('0');
  });
});

Step 4: Command Line Test Filtering

Using --grep with Different Patterns

# Run only smoke tests
npx playwright test --grep @smoke

# Run only critical tests
npx playwright test --grep @critical

# Run tests with specific tag
npx playwright test --grep @login

# Run tests matching multiple tags
npx playwright test --grep "@smoke|@critical"

# Run tests excluding certain tags
npx playwright test --grep "@smoke --grep-invert @slow"

# Run tests with specific test name pattern
npx playwright test --grep "login"

# Run tests with regex pattern
npx playwright test --grep ".*cart.*"

# Run tests in specific file
npx playwright test tests/login.spec.js --grep @positive

# Run tests with specific project and tag
npx playwright test --project=chromium --grep @smoke

# Run tests with workers and tags
npx playwright test --workers=5 --grep @regression

Step 5: Advanced Configuration Examples

📂 playwright.config.js - Complete Configuration

import { defineConfig, devices } from '@playwright/test';

export default defineConfig({
  // Test settings
  testDir: './tests',
  timeout: 30 * 1000,
  expect: { timeout: 5000 },
  retries: 1,
  workers: 3,
  
  // Reporter configuration
  reporter: [
    ['html', { outputFolder: 'playwright-report' }],
    ['json', { outputFile: 'test-results.json' }],
    ['junit', { outputFile: 'test-results.xml' }],
    ['line']
  ],
  
  // Global test options
  use: {
    baseURL: 'https://learn-playwright.great-site.net',
    trace: 'on-first-retry',
    screenshot: 'only-on-failure',
    video: 'retain-on-failure',
    viewport: { width: 1280, height: 720 },
    ignoreHTTPSErrors: true,
    permissions: ['geolocation'],
    geolocation: { latitude: 40.7128, longitude: -74.0060 },
  },
  
  // Projects for different environments
  projects: [
    // Desktop browsers
    {
      name: 'Desktop Chrome',
      use: { ...devices['Desktop Chrome'] },
      retries: 1,
    },
    {
      name: 'Desktop Firefox',
      use: { ...devices['Desktop Firefox'] },
      retries: 0,
    },
    {
      name: 'Desktop Safari',
      use: { ...devices['Desktop Safari'] },
      retries: 1,
    },
    
    // Mobile devices
    {
      name: 'iPhone 12',
      use: { ...devices['iPhone 12'] },
      retries: 2,
    },
    {
      name: 'Pixel 5',
      use: { ...devices['Pixel 5'] },
      retries: 2,
    },
    
    // Tablet
    {
      name: 'iPad',
      use: { ...devices['iPad'] },
      retries: 1,
    },
    
    // Custom configurations
    {
      name: 'High Resolution',
      use: {
        ...devices['Desktop Chrome'],
        viewport: { width: 2560, height: 1440 },
      },
    },
    {
      name: 'Low Resolution',
      use: {
        ...devices['Desktop Chrome'],
        viewport: { width: 1024, height: 768 },
      },
    },
  ],
  
  // Web server for local testing
  webServer: {
    command: 'npm run start',
    port: 3000,
    reuseExistingServer: !process.env.CI,
  },
});

🧑‍💻 Interactive Questions & Answers

1. Why do iframes require special handling in Playwright?

Answer:
  • Separate Context: Iframes create isolated browsing contexts within the main page
  • Security Boundaries: Cross-origin iframes have restricted access for security reasons
  • DOM Isolation: Elements inside iframes are not directly accessible from the main page
  • Loading Timing: Iframes load independently and may not be ready when the main page loads
  • Special Navigation: Playwright needs to switch context to interact with iframe content

2. What's the difference between workers and retries?

Answer:
  • Workers: Number of tests running simultaneously (parallel execution)
  • Retries: Number of times a failed test is re-executed
  • Workers Example: 3 workers = 3 tests running at the same time
  • Retries Example: retries: 1 = if test fails, run it once more
  • Combined: You can have 3 workers running tests, and each test can retry once if it fails

3. When should you use serial vs parallel test execution?

Answer:
  • Use Serial When:
    • Tests depend on each other (setup → action → verification)
    • Testing stateful applications (user sessions, database changes)
    • Limited resources (API rate limits, database connections)
  • Use Parallel When:
    • Tests are independent of each other
    • Testing different features or pages
    • Want faster execution time

4. How do you choose the right number of workers?

Answer:
  • CPU Cores: Start with number of CPU cores (e.g., 4 cores = 4 workers)
  • Memory: Each worker uses memory, don't exceed available RAM
  • Network: Too many workers can overwhelm the server
  • Test Complexity: Heavy tests need fewer workers, light tests can use more
  • CI Environment: Use fewer workers in CI (e.g., 2-3) to avoid resource conflicts

5. What are the benefits of using test tags with --grep?

Answer:
  • Selective Testing: Run only specific test categories (smoke, regression, critical)
  • Faster Feedback: Run quick smoke tests during development
  • CI/CD Optimization: Different test suites for different stages
  • Debugging: Run only failing test categories
  • Resource Management: Run heavy tests only when needed

🎯 Day 13 Homework Tasks

🟢 Beginner

Task 1

Create a test that interacts with an iframe element using frameLocator().

Task 2

Set up basic playwright.config.js with 2 workers and 1 retry.

🟡 Intermediate

Task 3

Create tests with different viewport sizes and verify responsive behavior.

Task 4

Set up projects for desktop and mobile devices with different retry settings.

🔴 Advanced

Task 5

Create a test suite with tags (@smoke, @regression, @e2e) and run specific subsets using --grep.

Task 6

Implement serial and parallel test execution patterns and measure performance differences.

Recommended Test Websites

Best Websites for Iframe and Config Testing:
  • Iframe Testing: https://the-internet.herokuapp.com/iframe
  • Nested Frames: https://the-internet.herokuapp.com/nested_frames
  • Frames: https://the-internet.herokuapp.com/frames
  • Responsive Design: https://learn-playwright.great-site.net
  • Geolocation: https://the-internet.herokuapp.com/geolocation

✅ Outcomes

  • Master iframe handling with frameLocator() and context switching
  • Configure Playwright with advanced options (viewport, devices, permissions)
  • Set up parallel and serial test execution strategies
  • Use test tags and --grep for selective test execution
  • Optimize test performance with proper worker configuration
  • Handle different devices and viewport sizes effectively
  • Implement retry strategies for flaky tests
  • Create comprehensive test configurations for different environments