🎯 Advanced Playwright Locators & Multi-Page Handling

"Mastering Smart Locators and Multi-Tab Navigation"

Duration: 1.5 Hours | Theme: Advanced Locators & Multi-Page

πŸ•’ Session Flow

🎭 Story Warm-up (5 min)

πŸ‘‰ "As testers, sometimes clicking a link opens a new tab, sometimes we need to find fields without IDs, and sometimes we select buttons based on text. Playwright gives us smart locators (`getBy*`) to handle this."

1. πŸ”— Handling New Pages (20 min)

// Example: PrestaShop footer link opens new tab const [newPage] = await Promise.all([ context.waitForEvent('page'), // waits for new tab page.getByRole("link", { name: "Ecommerce software by PrestaShopβ„’" }).click(), ]); console.log(await newPage.title());

πŸ’‘ QA Scenario

Verify that clicking the footer PrestaShop link opens a new page.

πŸ’‘ Interview Q: What happens if you don't wrap `waitForEvent('page')` with `Promise.all`?

🎯 Answer:

Race condition occurs! The test might wait indefinitely for a new page event that never comes, or the click might happen before the listener is set up. `Promise.all` ensures both operations (waiting for event + clicking) happen simultaneously.

🎯 Practice: Multi-Tab Navigation

  1. Navigate to a page with external links
  2. Click a link that opens in new tab
  3. Switch to the new page
  4. Verify the page title
  5. Close the new page and return to original

2. 🏷️ Using `getByLabel` (15 min)

// Fill form fields using visible labels await page.getByLabel("Email address").fill("qa.presta@example.com"); await page.getByLabel("Password").fill("abc123");

πŸ’‘ QA Scenario

Fill Login form where fields have visible labels.

πŸ’‘ Interview Q: How is `getByLabel` better than using `#id`?

🎯 Answer:

Accessibility & Maintainability:

  • Works even if IDs change
  • Follows accessibility best practices
  • More readable and self-documenting
  • Resilient to UI changes
  • Matches how users actually interact with forms

3. πŸ” Using `getByPlaceholder` (10 min)

// Search using placeholder text await page.getByPlaceholder("Search our catalog").fill("Dress");

πŸ’‘ QA Scenario

On PrestaShop homepage β†’ search bar placeholder.

πŸ’‘ Interview Q: When would `getByPlaceholder` fail?

🎯 Answer:

Common failure scenarios:

  • Placeholder text changes or is removed
  • Multiple elements have same placeholder
  • Element is not visible or disabled
  • Placeholder contains special characters
  • Element is in shadow DOM or iframe

4. 🎭 Using `getByRole` (15 min)

// Use semantic roles for better accessibility await page.getByRole("button", { name: "Sign in" }).click(); await page.getByRole("link", { name: "Clothes" }).click(); await page.getByRole("checkbox", { name: "Terms & Conditions" }).check(); await page.getByRole("radio", { name: "Express Delivery" }).check();

πŸ’‘ QA Scenario

Use role β†’ button, link, checkbox, radio.

πŸ’‘ Interview Q: Why is `getByRole` recommended over CSS/XPath?

🎯 Answer:

Multiple advantages:

  • Accessibility: Matches screen reader behavior
  • Resilience: Survives CSS class changes
  • Semantic: Based on element purpose, not appearance
  • Maintainability: Less brittle than CSS selectors
  • Performance: Often faster than complex CSS/XPath
  • Best Practice: Aligns with web standards

5. πŸ“ Using `getByText` (10 min)

// Navigate by visible text await page.getByText("Create an account").click(); await page.getByText("Forgot your password?").click(); await page.getByText("Product successfully added").toBeVisible();

πŸ’‘ QA Scenario

Navigate by visible text, e.g., "Create an account".

πŸ’‘ Interview Q: What if two elements have the same text?

🎯 Answer:

Multiple strategies:

  • Use more specific selectors: `getByRole('button', { name: 'Login' })`
  • Filter by parent: `page.locator('.header').getByText('Login')`
  • Use nth: `page.getByText('Login').nth(1)`
  • Combine with other attributes: `page.getByText('Login').filter({ hasAttribute: 'data-testid' })`
  • Use exact matching: `page.getByText('Login', { exact: true })`

6. πŸ” Filtering Locators with `filter({ hasText })` (20 min)

// Filter product cards by text content await page.locator(".product-miniature") .filter({ hasText: "Printed Dress" }) .getByRole("button", { name: "Add to cart" }) .click(); // Filter by multiple criteria await page.locator(".product-item") .filter({ hasText: "Shirt" }) .filter({ hasAttribute: "data-in-stock", hasAttributeValue: "true" }) .first() .click();

πŸ’‘ QA Scenario

Add a specific product to cart (instead of using nth selectors).

πŸ’‘ Interview Q: What is the difference between `.nth()` and `.filter({ hasText })`?

🎯 Answer:

Key differences:

`.nth()` `.filter({ hasText })`
Position-based (1st, 2nd, 3rd) Content-based (specific text)
Brittle - breaks if order changes Resilient - works regardless of position
Hard to maintain Self-documenting and maintainable
Fast execution Slightly slower but more reliable

7. βœ… Assertion Example (10 min)

// Verify success message after adding to cart await expect(page.getByText("Product successfully added to your shopping cart")).toBeVisible(); // Verify checkbox is checked await expect(page.getByRole("checkbox", { name: "Terms" })).toBeChecked(); // Verify URL contains expected path await expect(page).toHaveURL(/checkout/); // Verify element count await expect(page.locator(".product-item")).toHaveCount(5);

πŸ’‘ QA Scenario

Validate success message after adding to cart.

πŸ’‘ Interview Q: Difference between `isVisible()` and `toBeVisible()`?

🎯 Answer:

Key differences:

  • `isVisible()`: Returns boolean immediately, doesn't wait
  • `toBeVisible()`: Assertion that waits and retries, throws error if not visible
  • Use `isVisible()`: For conditional logic, checking state
  • Use `toBeVisible()`: For test assertions, verifying expected behavior
  • Example: `if (await page.locator('.modal').isVisible()) { await page.locator('.close').click(); }`

🎯 Day 8 Learning Outcomes

πŸ“‘ Day 8 Homework (Practice Tasks)

🟒 Beginner

  1. Fill login form using `getByLabel` instead of IDs.
  2. Search for "Shirt" using `getByPlaceholder`.

🟑 Intermediate

  1. Click on "Clothes" category using `getByRole("link")`.
  2. Verify success message after login with `toBeVisible()`.
  3. Click PrestaShop footer link and print the new page's title.

πŸ”΄ Advanced

  1. Extract all product names with `.allTextContents()` β†’ print in console.
  2. From product grid, add "Printed Dress" to cart using `.filter({ hasText })`.
  3. On checkout page, tick Terms & Conditions checkbox using `getByRole("checkbox")` and validate `toBeChecked()`.

🎯 Additional Interview Questions & Answers

πŸ’‘ Q1: What's the difference between `page.locator()` and `page.getBy*()` methods?

🎯 Answer:

`page.locator()` is the base method that accepts any CSS selector, XPath, or text. `page.getBy*()` methods are semantic helpers that use best practices for accessibility and maintainability.

Example:

  • `page.locator('#email')` - CSS selector
  • `page.getByLabel('Email')` - semantic, accessibility-focused

πŸ’‘ Q2: How do you handle dynamic content that loads after page load?

🎯 Answer:

Multiple strategies:

  • Wait for element: `await page.waitForSelector('.dynamic-content')`
  • Auto-waiting: `await page.getByText('Dynamic Text').toBeVisible()`
  • Network idle: `await page.waitForLoadState('networkidle')`
  • Custom wait: `await page.waitForFunction(() => document.querySelector('.content').textContent.includes('Expected'))`

πŸ’‘ Q3: What's the best practice for handling multiple windows/tabs?

🎯 Answer:

Always use Promise.all pattern:

const [newPage] = await Promise.all([ context.waitForEvent('page'), page.getByRole('link', { name: 'Open New Tab' }).click() ]);

Why? Prevents race conditions and ensures the event listener is set up before the action that triggers it.

πŸ’‘ Q4: How do you handle iframes in Playwright?

🎯 Answer:

Frame handling:

// Get frame by name or URL const frame = page.frame({ name: 'payment-frame' }); await frame.getByRole('button', { name: 'Pay Now' }).click(); // Or use frameLocator await page.frameLocator('iframe[name="payment"]') .getByRole('button', { name: 'Pay Now' }) .click();

πŸ’‘ Q5: What's the difference between `click()` and `click({ force: true })`?

🎯 Answer:

Key differences:

  • `click()`: Waits for element to be visible and clickable, respects element state
  • `click({ force: true })`: Bypasses checks, clicks even if element is hidden/covered
  • Use force: Only when you're certain the element should be clicked despite visual state
  • Warning: Force clicking can lead to unreliable tests

πŸ’‘ Q6: How do you handle file uploads in Playwright?

🎯 Answer:

File upload methods:

// Method 1: Using setInputFiles await page.getByLabel('Upload file').setInputFiles('path/to/file.pdf'); // Method 2: Multiple files await page.getByLabel('Upload files').setInputFiles([ 'file1.pdf', 'file2.jpg' ]); // Method 3: Buffer upload await page.getByLabel('Upload file').setInputFiles({ name: 'test.pdf', mimeType: 'application/pdf', buffer: Buffer.from('test content') });

πŸ’‘ Q7: What's the best way to handle authentication in tests?

🎯 Answer:

Authentication strategies:

  • Storage state: Save cookies/localStorage after login, reuse in other tests
  • API login: Use API calls for faster authentication
  • Environment variables: Use test credentials from env vars
  • Test isolation: Each test should be independent
// Save authentication state await context.storageState({ path: 'auth.json' }); // Use in other tests const context = await browser.newContext({ storageState: 'auth.json' });

πŸ’‘ Q8: How do you handle test data management?

🎯 Answer:

Test data strategies:

  • Fixtures: Use Playwright fixtures for reusable data
  • Factories: Create test data dynamically
  • Cleanup: Always clean up test data after tests
  • Isolation: Each test should have unique data
  • External tools: Use tools like Faker.js for realistic data