Back to Sanity

Multi-Tab, Window & Popup Testing

.agents/skills/playwright-best-practices/advanced/multi-context.md

5.20.08.2 KB
Original Source

Multi-Tab, Window & Popup Testing

This file covers single-user scenarios with multiple browser tabs, windows, and popups. For multi-user collaboration testing (multiple users interacting simultaneously), see multi-user.md.

Table of Contents

  1. Popup Handling
  2. New Tab Navigation
  3. OAuth Flows
  4. Multiple Windows
  5. Tab Coordination

Basic Popup

typescript
test('handle popup window', async ({page}) => {
  await page.goto('/')

  // Start waiting for popup before triggering it
  const popupPromise = page.waitForEvent('popup')
  await page.getByRole('button', {name: 'Open Support Chat'}).click()
  const popup = await popupPromise

  // Wait for popup to load
  await popup.waitForLoadState()

  // Interact with popup
  await popup.getByLabel('Message').fill('Need help')
  await popup.getByRole('button', {name: 'Send'}).click()

  await expect(popup.getByText('Message sent')).toBeVisible()

  // Close popup
  await popup.close()
})
typescript
test('popup login flow', async ({page}) => {
  await page.goto('/dashboard')

  const popupPromise = page.waitForEvent('popup')
  await page.getByRole('button', {name: 'Connect Account'}).click()
  const popup = await popupPromise

  await popup.waitForLoadState()

  // Complete login in popup
  await popup.getByLabel('Email').fill('[email protected]')
  await popup.getByLabel('Password').fill('password123')
  await popup.getByRole('button', {name: 'Log In'}).click()

  // Popup should close automatically after auth
  await popup.waitForEvent('close')

  // Main page should reflect connected state
  await expect(page.getByText('Account connected')).toBeVisible()
})

Handle Blocked Popups

typescript
test('handle popup blocker', async ({page}) => {
  await page.goto('/share')

  // Listen for console messages about blocked popup
  page.on('console', (msg) => {
    if (msg.text().includes('popup blocked')) {
      console.log('Popup was blocked')
    }
  })

  const popupPromise = page.waitForEvent('popup').catch(() => null)
  await page.getByRole('button', {name: 'Share to Twitter'}).click()
  const popup = await popupPromise

  if (!popup) {
    // Popup blocked - app should show fallback
    await expect(page.getByText('Copy share link instead')).toBeVisible()
  }
})

New Tab Navigation

typescript
test('external link opens in new tab', async ({page, context}) => {
  await page.goto('/resources')

  // Wait for new page in context
  const pagePromise = context.waitForEvent('page')
  await page.getByRole('link', {name: 'Documentation'}).click()
  const newPage = await pagePromise

  await newPage.waitForLoadState()

  expect(newPage.url()).toContain('docs.example.com')
  await expect(newPage.getByRole('heading', {level: 1})).toBeVisible()

  // Original page still there
  expect(page.url()).toContain('/resources')

  await newPage.close()
})

Intercept New Tab

typescript
test('prevent new tab for testing', async ({page}) => {
  await page.goto('/links')

  // Remove target="_blank" to keep navigation in same tab
  await page.evaluate(() => {
    document.querySelectorAll('a[target="_blank"]').forEach((a) => {
      a.removeAttribute('target')
    })
  })

  // Now link opens in same tab
  await page.getByRole('link', {name: 'External Site'}).click()

  // Can test the destination page
  await expect(page).toHaveURL(/external-site\.com/)
})

OAuth Flows

Google OAuth Popup

typescript
test('Google OAuth login', async ({page}) => {
  await page.goto('/login')

  const popupPromise = page.waitForEvent('popup')
  await page.getByRole('button', {name: 'Sign in with Google'}).click()
  const popup = await popupPromise

  await popup.waitForLoadState()

  // Handle Google's OAuth flow
  await popup.getByLabel('Email or phone').fill('[email protected]')
  await popup.getByRole('button', {name: 'Next'}).click()

  await popup.getByLabel('Enter your password').fill('password')
  await popup.getByRole('button', {name: 'Next'}).click()

  // Wait for redirect back and popup close
  await popup.waitForEvent('close')

  // Verify logged in on main page
  await expect(page.getByText('Welcome, Test User')).toBeVisible()
})
typescript
test('mock OAuth flow', async ({page, context}) => {
  // Mock the OAuth callback instead of real flow
  await page.route('**/auth/callback**', async (route) => {
    // Simulate successful OAuth
    const url = new URL(route.request().url())
    url.searchParams.set('code', 'mock-auth-code')
    await route.fulfill({
      status: 302,
      headers: {Location: '/dashboard'},
    })
  })

  // Mock token exchange
  await page.route('**/api/auth/token', (route) =>
    route.fulfill({
      json: {
        access_token: 'mock-token',
        user: {name: 'Test User', email: '[email protected]'},
      },
    }),
  )

  await page.goto('/login')
  await page.getByRole('button', {name: 'Sign in with Google'}).click()

  // Should redirect to dashboard without actual OAuth
  await expect(page).toHaveURL('/dashboard')
  await expect(page.getByText('Welcome, Test User')).toBeVisible()
})

OAuth Fixture

For comprehensive OAuth mocking patterns (fixtures, multiple providers, SAML SSO), see third-party.md. This section focuses on popup window handling mechanics for OAuth flows.

Multiple Windows

Test Across Multiple Windows

typescript
test('sync between windows', async ({context}) => {
  // Open two pages
  const page1 = await context.newPage()
  const page2 = await context.newPage()

  await page1.goto('/dashboard')
  await page2.goto('/dashboard')

  // Make change in first window
  await page1.getByRole('button', {name: 'Add Item'}).click()
  await page1.getByLabel('Name').fill('New Item')
  await page1.getByRole('button', {name: 'Save'}).click()

  // Should sync to second window (if app supports real-time sync)
  await expect(page2.getByText('New Item')).toBeVisible({timeout: 10000})
})

Different Users in Different Windows

For multi-user collaboration patterns (admin/user interactions, real-time collaboration, role-based testing, concurrent actions), see multi-user.md. This file focuses on single-user scenarios with multiple tabs/windows/popups.

Tab Coordination

Switch Between Tabs

typescript
test('manage multiple tabs', async ({context}) => {
  const page1 = await context.newPage()
  await page1.goto('/editor')

  const page2 = await context.newPage()
  await page2.goto('/preview')

  // Edit in first tab
  await page1.bringToFront()
  await page1.getByLabel('Content').fill('Hello World')

  // Check preview in second tab
  await page2.bringToFront()
  await page2.reload() // If preview needs refresh
  await expect(page2.getByText('Hello World')).toBeVisible()
})

Close All Tabs Except One

typescript
test('cleanup tabs after test', async ({context}) => {
  const mainPage = await context.newPage()
  await mainPage.goto('/')

  // Open several popups during test
  for (let i = 0; i < 3; i++) {
    const popup = await context.newPage()
    await popup.goto(`/popup/${i}`)
  }

  // Close all except main page
  for (const page of context.pages()) {
    if (page !== mainPage) {
      await page.close()
    }
  }

  expect(context.pages()).toHaveLength(1)
})

Anti-Patterns to Avoid

Anti-PatternProblemSolution
Not waiting for popupRace conditionUse waitForEvent("popup") before trigger
Testing real OAuthSlow, flaky, needs credentialsMock OAuth endpoints
Assuming popup opensMay be blockedHandle both open and blocked cases
Not closing extra pagesResource leakClose pages in cleanup