100+ Playwright Interview Questions and Answers (Beginner to Exp
New Batch Starting Soon — Limited Seats! Book Demo Now
Stad Solution Logo
WhatsApp icon
STAD Solution — Interview Prep

Top 100 Playwright
Interview Questions & Answers

Real examples, step-by-step code, and scenario-based questions. prepared for both freshers and experienced professionals.

100 Questions
20+ Code Examples
15+ Scenarios
4 Levels
📘 Concepts 💻 Code Examples 🎯 Scenario Based ⚡ CI/CD & Advanced
🎭
Section 1 — Basics & Introduction
Core concepts every QA should know before the interview
Q1 – Q15
Q1 What is Playwright? Why is it used?
Beginner

Playwright is an open-source test automation tool built by Microsoft. It lets you write code that automatically opens a browser and tests your web application — exactly like a real user clicking, typing, and navigating.

Why teams use Playwright:

  • Supports all major browsers — Chrome, Firefox, Safari — with one test
  • Faster and more reliable than older tools like Selenium
  • Handles modern web features well — popups, file uploads, dynamic content
  • Supports JavaScript, TypeScript, Python, Java, and C#
💡 Real Example: Imagine someone at your company manually opens the website every day, logs in, adds a product to cart, and checks if the price is correct. Playwright does all of this automatically in seconds — without any human involvement.
Q2 What is the difference between Playwright and Selenium?
Beginner

Both Playwright and Selenium are used for browser automation, but Playwright is the more modern and faster option.

FeaturePlaywrightSelenium
Created byMicrosoft (2020)ThoughtWorks (2004)
SpeedFaster — built-in auto-waitSlower — manual waits needed
SetupSimple — one command installComplex — separate driver per browser
API TestingYes — built-inNo — needs extra library
Mobile TestingDevice emulation built-inNeeds Appium separately
Parallel ExecutionVery easy — built-inNeeds Selenium Grid setup
📌 Selenium is still widely used in large companies with older projects. Playwright is the preferred choice for new projects today. In interviews, always mention both — it shows awareness of industry tools.
Q3 Which programming languages does Playwright support?
Beginner

Playwright supports 4 programming languages. You only need to know one to get started:

  • JavaScript / TypeScript — most popular with Playwright, best job demand, most tutorials available
  • Python — good if your team already works in Python
  • Java — preferred in companies already using Java-based frameworks
  • C# (.NET) — used in Microsoft-stack companies
💡 Interview tip: If you are learning Playwright fresh, start with JavaScript. Most Playwright job requirements mention JavaScript or TypeScript. It also has the most community support and examples online.
Q4 How do you install Playwright?
Beginner

Installing Playwright takes less than 2 minutes. You need Node.js installed first. Then follow these steps:

Terminal
# Step 1: Create a new project folder mkdir my-playwright-project cd my-playwright-project # Step 2: Install Playwright (one command does everything) npm init playwright@latest # It will ask you a few questions: # TypeScript or JavaScript? → Choose JavaScript # Where to put tests? → Press Enter (default: tests/) # Install browsers? → Yes (downloads Chrome, Firefox, Safari)

After installation, run your first test with:

Terminal
# Run all tests npx playwright test # Open the HTML report in browser npx playwright show-report
Q5 What is a Browser, BrowserContext, and Page in Playwright?
Beginner

These are the 3 core building blocks of Playwright. Think of it like opening a browser manually:

Playwright Object Hierarchy
🌐 Browser
Chrome / Firefox / Safari
📦 BrowserContext
Like an Incognito window
📄 Page
One tab / one URL
  • Browser — The actual browser that Playwright opens. Can be Chrome, Firefox, or Safari.
  • BrowserContext — An isolated session inside that browser. Like opening a new incognito window. Each context has its own cookies, login state, and storage. You can run multiple contexts in parallel.
  • Page — One single tab inside a context. This is where you do all your actions — click, type, navigate, assert.
📌 Real example: You want to test login as two different users at the same time. Create 2 BrowserContexts — each has its own session. Both run in parallel without affecting each other.
Q6 What is Auto-Wait in Playwright? Why is it important?
Beginner

Auto-wait means Playwright automatically waits for an element to be ready before performing any action on it. You do not need to write manual wait commands.

Before clicking or typing, Playwright automatically checks that the element is:

  • Present in the HTML
  • Visible on screen (not hidden)
  • Enabled (not disabled or greyed out)
  • Stable (not moving or animating)
⚠️ Compare with Selenium: In Selenium, you had to write Thread.sleep(2000) or WebDriverWait manually for every action. If the wait was too short, the test failed. If too long, it was slow. Playwright removes this problem completely with auto-wait.
💡 Interview tip: This is one of the most asked questions. Always say — "In Playwright, I don't need to add manual waits. It automatically waits for the element to be visible and ready before clicking or filling."
Q7 What browsers does Playwright support?
Beginner

Playwright supports 3 browser engines that cover all major browsers:

  • Chromium — runs Google Chrome and Microsoft Edge tests
  • Firefox — runs Mozilla Firefox tests
  • WebKit — runs Apple Safari tests (works on all OS, not just Mac)

You can run the same test file across all 3 browsers with a simple config change. This is called cross-browser testing.

playwright.config.js
projects: [ { name: 'Chrome', use: { ...devices['Desktop Chrome'] } }, { name: 'Firefox', use: { ...devices['Desktop Firefox'] } }, { name: 'Safari', use: { ...devices['Desktop Safari'] } }, ]
Q8 What is headless mode in Playwright?
Beginner

Headless mode means the browser runs in the background — no browser window opens on screen. Tests run and work exactly the same, just without any visible UI.

ModeWhen to use
Headless (default)Running on CI/CD servers like Jenkins or GitHub Actions — no screen available. Also faster for large test suites.
Headed (visible)When writing new tests or debugging a failing test — you can see what is happening step by step.
Terminal
# Run with visible browser window npx playwright test --headed # Run in background (headless — this is default) npx playwright test
Q9 What is playwright.config.js and what can you configure in it?
Beginner

playwright.config.js is the main settings file for your entire test project. You set common options here once so you do not have to repeat them in every test file.

Common things you configure here:

  • Base URL of your application
  • Which browsers to run tests on
  • Timeout per test
  • Whether to run tests in parallel
  • Screenshot and video recording settings
playwright.config.js
const { defineConfig } = require('@playwright/test'); module.exports = defineConfig({ testDir: './tests', // where your test files are fullyParallel: true, // run tests in parallel retries: 1, // retry failed test once timeout: 30000, // max 30 seconds per test use: { baseURL: 'https://your-website.com', headless: true, screenshot: 'only-on-failure', video: 'retain-on-failure', }, });
Q10 What is the difference between page.click() and locator.click()?
Beginner

Both are used to click an element. The difference is that locator.click() is the newer and more reliable approach recommended by Playwright.

page.click()locator.click()
StyleOld wayNew recommended way
Auto retry on failureNoYes — retries automatically
Strict modeNoYes — fails if multiple elements match
JavaScript
// Old way — still works but not preferred await page.click('#submit-btn'); // New recommended way using locator await page.locator('#submit-btn').click(); // Best practice — use getByRole for accessibility await page.getByRole('button', { name: 'Submit' }).click();
💡 Interview tip: Always say you use locator-based approach. Mention getByRole, getByLabel, getByText as preferred locators — this shows you follow Playwright best practices.
Q11 How do you take a screenshot in Playwright?
Beginner

Playwright has built-in screenshot support. You can capture the full page, a specific element, or set it to capture automatically on test failure.

JavaScript
// Capture full page screenshot await page.screenshot({ path: 'screenshot.png', fullPage: true }); // Capture only a specific element await page.locator('.product-card').screenshot({ path: 'product.png' }); // In config — auto capture on every test failure use: { screenshot: 'only-on-failure' }
💡 Setting screenshot to only-on-failure in config is very useful in real projects. When a test fails in Jenkins at night, you can check the screenshot next morning to understand what went wrong — without re-running the test.
Q12 How do you record a test in Playwright without writing code?
Beginner

Playwright has a built-in tool called Codegen. It opens a browser and records every action you do — clicks, typing, navigation — and automatically generates the test code in real time.

Terminal
# Opens browser + code window side by side npx playwright codegen https://www.flipkart.com # Record and save the code directly to a file npx playwright codegen --output=tests/test.js https://www.flipkart.com

Just browse the website normally — Playwright writes the code for you automatically.

📌 Codegen is great for beginners to understand how locators look. Always review and clean up the generated code before using it in real projects — it may not follow best practices like getByRole.
Q13 What is the test() function in Playwright?
Beginner

The test() function is how you define a single test case. It takes a test name and a function that contains the actual steps. Every test you write is inside this function.

Structure of a basic Playwright test:

JavaScript
const { test, expect } = require('@playwright/test'); test('User can login successfully', async ({ page }) => { // Step 1: Go to the website await page.goto('https://example.com/login'); // Step 2: Fill in credentials await page.fill('#email', 'test@gmail.com'); await page.fill('#password', 'Password@123'); // Step 3: Click login button await page.click('#login-btn'); // Step 4: Verify login was successful await expect(page).toHaveURL('/dashboard'); });
Q14 What are test.describe() and test.beforeEach() used for?
Beginner

test.describe() is used to group related tests together — like a folder. It gives a name to a group of tests that test the same feature or page.

test.beforeEach() runs a block of code before every test inside that group. This avoids repeating the same setup steps in each test.

JavaScript
test.describe('Login Page Tests', () => { // This runs before EVERY test in this group test.beforeEach(async ({ page }) => { await page.goto('https://example.com/login'); }); test('Valid login works', async ({ page }) => { await page.fill('#email', 'valid@gmail.com'); await page.fill('#password', 'Valid@123'); await page.click('#login-btn'); await expect(page).toHaveURL('/dashboard'); }); test('Wrong password shows error message', async ({ page }) => { await page.fill('#email', 'valid@gmail.com'); await page.fill('#password', 'wrongpass'); await page.click('#login-btn'); await expect(page.locator('.error-msg')).toBeVisible(); }); });
💡 Without beforeEach, you would have to write page.goto('/login') at the start of every test. With beforeEach, you write it once and it runs automatically before each test.
Q15 How do you run only one specific test in Playwright?
Beginner

There are two ways to run a specific test — from terminal or from inside your test file.

Terminal
# Run all tests npx playwright test # Run only one specific test file npx playwright test tests/login.spec.js # Run tests with a specific name or keyword npx playwright test --grep "login" # Run only on Chrome browser npx playwright test --project=chromium # Run in debug mode — opens browser step by step npx playwright test --debug
JavaScript — Inside test file
// Use test.only() to run just this one test test.only('Login test', async ({ page }) => { // only this test will run in this file }); // Remember to remove .only before committing to Git!
⚠️ Always remove test.only() before pushing code to your repository. If you forget, only that one test will run in CI/CD and all other tests will be skipped — which is a common mistake in real teams.

Ready to Learn Playwright
with GenAI?

100% Practical Training 🎯 Real Projects 📜 Course Certificate 🤖 Gen AI Included 🏆 ISO Certified Institute
5000+ Students Placed
350+ Hiring Companies
10+ Years Experience
🎯
Section 2 — Locators & Selectors
How to find and identify elements on a webpage
Q16 – Q30
Q16 What are Locators in Playwright?
Beginner

A Locator is how you tell Playwright which element on the page to interact with. Before you can click a button or type in a field, Playwright first needs to find that element.

Playwright provides several built-in ways to find elements:

JavaScript
// By role — most recommended, works with accessibility page.getByRole('button', { name: 'Login' }) page.getByRole('textbox', { name: 'Email' }) // By visible text on screen page.getByText('Welcome to STAD Solution') // By placeholder text inside an input page.getByPlaceholder('Enter your email') // By label text (for form fields) page.getByLabel('Password') // By test ID — most stable for automation page.getByTestId('submit-button') // By CSS selector page.locator('#email-input') page.locator('.btn-primary')
📌Always prefer getByRole and getByLabel over CSS selectors. They are more readable and less likely to break when the page design changes.
Q17 What is the difference between CSS Selector and XPath in Playwright?
Beginner

Both CSS Selector and XPath are used to locate elements in the HTML. CSS Selector is faster and easier to read, while XPath is more powerful but complex.

PointCSS SelectorXPath
SpeedFasterSlightly slower
ReadabilityEasy to readHard to read
Find by textNot possible directlyYes — //button[text()='Login']
Navigate to parentNot possibleYes — /parent::div
RecommendedYes — for most casesOnly when CSS cannot work
JavaScript
// CSS Selector examples page.locator('#login-btn') // by ID page.locator('.submit-button') // by class page.locator('input[type="email"]') // by attribute // XPath examples page.locator('//button[@id="login-btn"]') page.locator('//button[text()="Login"]')
💡Interview tip: Say — "I prefer CSS selectors for speed and readability. I use XPath only when I need to find an element by its exact text content or navigate to a parent element."
Q18 What is getByRole() and why is it preferred?
Beginner

getByRole() finds elements based on their ARIA role — the purpose they serve on the page. For example, a submit button has the role "button", an input field has the role "textbox", a navigation link has the role "link".

Why it is preferred:

  • It finds elements the same way a screen reader or keyboard user would — by purpose, not by design
  • If a developer changes the button's CSS class or ID, your test still works because the role stays the same
  • Makes your tests more readable — anyone can understand what element you are referring to
JavaScript
// Find a button by its visible text await page.getByRole('button', { name: 'Submit' }).click(); // Find a text input field by its label await page.getByRole('textbox', { name: 'Email Address' }).fill('test@gmail.com'); // Find a heading on the page await page.getByRole('heading', { name: 'Dashboard' }); // Find a checkbox await page.getByRole('checkbox', { name: 'I agree to terms' }).check();
Q19 What is getByTestId() and how is it set up?
Beginner

getByTestId() finds elements using a special HTML attribute that developers add specifically for testing. It is the most stable way to locate elements because it does not change when the UI design is updated.

HTML — Developer adds this attribute
<button data-testid="submit-button">Submit</button> <input data-testid="email-input" type="email" />
JavaScript — QA uses this in test
await page.getByTestId('submit-button').click(); await page.getByTestId('email-input').fill('test@gmail.com');
💡Real team practice: Request your developers to add data-testid attributes on all important buttons, inputs, and links. This simple habit saves QA teams huge amounts of time and makes automation tests very stable.
Q20 How do you handle a list of elements in Playwright?
Intermediate

When multiple elements match your locator — like a list of products or table rows — you can use .all() to get all of them and loop through, or .count() to get the total number.

JavaScript
// Example: Get all product names from a listing page const products = page.locator('.product-name'); // Count how many products are there const count = await products.count(); console.log(`Total products: ${count}`); // Get text of each product one by one const allProducts = await products.all(); for (const product of allProducts) { const name = await product.textContent(); console.log(name); } // Get text of specific item — 3rd item (index starts at 0) const thirdItem = await products.nth(2).textContent();
Q21 What is the difference between locator.first(), .last(), and .nth()?
Intermediate

When your locator matches multiple elements, these methods let you pick a specific one by its position in the list.

JavaScript
// Say there are 5 products on the page // All have the class 'product-card' // Pick the FIRST product page.locator('.product-card').first() // Pick the LAST product page.locator('.product-card').last() // Pick the 3rd product (index starts from 0) page.locator('.product-card').nth(2) // Real example: Click 'Buy Now' button of the first product only await page.locator('.product-card').first() .locator('button.buy-now').click();
💡Interview tip: Remember — nth(0) is the first item, nth(1) is second. This is a common mistake beginners make. Always verify the index before using it in a real test.
Q22 How do you locate an element inside another element?
Intermediate

You can chain locators — first find the parent container, then find the child element inside it. This is useful when the same element (like a button) appears inside multiple containers and you need to target a specific one.

JavaScript
// Problem: Each product card has its own 'Add to Cart' button // You want to click 'Add to Cart' for the SECOND product only // Step 1: Get the second product card const secondCard = page.locator('.product-card').nth(1); // Step 2: Find the button INSIDE that specific card await secondCard.locator('button.add-to-cart').click(); // Another example: Find error message inside a specific form const loginForm = page.locator('#login-form'); await expect(loginForm.locator('.error-text')).toBeVisible();
Q23 What is filter() in Playwright locators?
Intermediate

.filter() narrows down a list of matched elements based on a condition — like finding a specific row in a table that contains certain text.

JavaScript
// Example: You have a table of orders // You want to click 'Cancel' only for orders with status "Pending" const pendingRow = page .locator('tr.order-row') .filter({ hasText: 'Pending' }); await pendingRow.locator('button.cancel-btn').click(); // Filter by containing a specific child element const activeUsers = page .locator('.user-card') .filter({ has: page.locator('.status-active') });
💡Real use case: In an order management page, you have 20 orders. You want to verify only orders with status "Failed". Use filter({ hasText: 'Failed' }) and then assert their count or click a retry button.
Q24 How do you handle dynamic elements that keep changing their ID or class?
Intermediate

Some websites generate IDs dynamically — like btn_1234 that changes to a different number on every page load. If you use that ID in your locator, the test will break the next time the page loads.

Solutions to handle dynamic elements:

  • Use getByRole with the button or link name — the visible text does not change
  • Use getByText with the element's visible text content
  • Use partial CSS — [id^="btn_"] matches any ID starting with "btn_"
  • Ask the developer to add a stable data-testid attribute
JavaScript
// BAD — this ID changes every time, test will break page.locator('#btn_1234') // GOOD — use visible button text page.getByRole('button', { name: 'Add to Cart' }) // GOOD — use data attribute page.locator('[data-action="add-cart"]') // GOOD — partial match on ID prefix page.locator('[id^="btn_"]')
Q25 What is the difference between getByText() and getByLabel()?
Intermediate
MethodFinds byBest used for
getByText()Visible text content of the elementParagraphs, headings, buttons, links, any element with visible text
getByLabel()Label associated with a form inputInput fields, dropdowns, checkboxes that have a label tag
JavaScript
// getByText — find element by its visible text await page.getByText('Welcome back, Rahul').isVisible(); await page.getByText('Logout').click(); // getByLabel — find input field associated with a label // HTML: <label for="email">Email Address</label><input id="email"> await page.getByLabel('Email Address').fill('test@gmail.com'); await page.getByLabel('Password').fill('Pass@123');
Q26 How do you check if an element is visible or exists on the page?
Intermediate

Playwright provides two methods — isVisible() returns true or false, while toBeVisible() is an assertion that fails the test if the element is not visible.

JavaScript
// Check visibility — returns true or false const isVisible = await page.locator('.success-msg').isVisible(); console.log(isVisible); // true or false // Assert element IS visible — test fails if not await expect(page.locator('.success-msg')).toBeVisible(); // Assert element is NOT visible await expect(page.locator('.error-msg')).not.toBeVisible(); // Check if element exists in DOM (even if hidden) const exists = await page.locator('#modal').count() > 0; console.log(exists); // true if present in HTML
📌Use isVisible() when you want to conditionally do something — like "if the cookie banner is visible, close it". Use toBeVisible() as an assertion when you want to verify something as part of your test.
Q27 How do you get the text content of an element?
Intermediate

There are two methods — textContent() gets all text including hidden text, and innerText() gets only the visible text the user can see on screen.

JavaScript
// Get ALL text — includes text in hidden elements const allText = await page.locator('.order-summary').textContent(); // Get only VISIBLE text — what user sees on screen const visibleText = await page.locator('.order-summary').innerText(); // Real example: Get order ID after placing order const orderId = await page.locator('.order-id-text').innerText(); console.log('Order placed with ID:', orderId);
💡Use innerText() in most cases — it gives you what the user actually sees. Use textContent() only when you need data that might be inside a hidden span or element.
Q28 How do you get an attribute value of an element in Playwright?
Intermediate

Use getAttribute() to read any HTML attribute of an element — like href, src, value, placeholder, disabled, etc.

JavaScript
// Get the href of a link const href = await page.locator('a.learn-more').getAttribute('href'); console.log(href); // e.g. '/courses/automation-testing' // Get the src of an image const imgSrc = await page.locator('img.logo').getAttribute('src'); // Check if a button has the disabled attribute const isDisabled = await page.locator('#submit-btn').getAttribute('disabled'); // Returns null if not disabled, returns "" or "disabled" if disabled // Get placeholder text of input const placeholder = await page.locator('#search-box').getAttribute('placeholder');
Q29 How do you handle a situation where your locator matches multiple elements?
Intermediate

Playwright's locator is in strict mode by default — if your locator matches more than one element and you try to click or type, it will throw an error saying "strict mode violation: locator resolved to X elements".

How to fix this:

  • Make your locator more specific — add parent container, add more attributes
  • Use .first(), .last(), or .nth() to pick a specific one
  • Use .filter() to narrow down based on text or child element
JavaScript
// Problem: '.delete-btn' matches 5 buttons on the page await page.locator('.delete-btn').click(); // ERROR — strict mode // Solution 1: Be more specific await page.locator('#order-123 .delete-btn').click(); // Solution 2: Use filter to find the right one await page.locator('.order-row') .filter({ hasText: 'Order #123' }) .locator('.delete-btn') .click(); // Solution 3: Use first() if you intentionally want the first one await page.locator('.delete-btn').first().click();
⚠️Never use first() just to fix the error without thinking. Always understand WHICH element you want to interact with and make your locator specific enough to target only that element.
Q30 What is the best locator strategy to follow in real projects?
Intermediate

Playwright recommends a priority order for choosing locators — from most stable to least stable. Always start from the top of this list:

PriorityLocatorWhy
1st ✅getByRole()Mirrors how users and screen readers find elements. Very stable.
2nd ✅getByLabel()Tied to form field labels. Stable when label text doesn't change.
3rd ✅getByPlaceholder()Good for inputs without visible labels.
4th ✅getByTestId()Most stable — specially added for automation.
5th ⚠️getByText()Can break if text is changed or translated.
6th ⚠️CSS SelectorCan break when page design is updated.
7th ❌XPathUse only as last resort — fragile and hard to read.
💡Interview tip: This is one of the best answers you can give. Say — "I follow Playwright's recommended locator priority. I start with getByRole and getByLabel. I use data-testid for elements that don't have a clear accessible role. I avoid XPath unless there is no other option."
Section 3 — Assertions & Waits
How to verify results and handle timing in tests
Q31 – Q45
Q31 What are Assertions in Playwright and why are they important?
Beginner

An assertion is a check in your test that verifies whether something is correct. Without assertions, your test just performs actions — it does not actually verify if the result is right or wrong. That means even if the website is broken, your test would still pass.

In Playwright, all assertions use the expect() function.

JavaScript
// Check current page URL after login await expect(page).toHaveURL('/dashboard'); // Check page title await expect(page).toHaveTitle('My App - Dashboard'); // Check element is visible on screen await expect(page.locator('.success-msg')).toBeVisible(); // Check element has specific text await expect(page.locator('h1')).toHaveText('Welcome, Rahul'); // Check element does NOT exist or is NOT visible await expect(page.locator('.error-msg')).not.toBeVisible();
📌Playwright assertions have auto-retry built in. If the element is not ready yet, Playwright keeps checking for up to 5 seconds before failing the test. This makes assertions very stable compared to Selenium.
Q32 What is the difference between toHaveText() and toContainText()?
Beginner

Both check text content of an element, but they work differently:

MethodWhat it checksExample
toHaveText()Exact full text matchElement must have EXACTLY "Order Placed"
toContainText()Partial text matchElement just needs to CONTAIN the word "Placed"
JavaScript
// Element text is: "Order #1234 Placed Successfully" // toHaveText — needs EXACT match — this will FAIL await expect(page.locator('.msg')).toHaveText('Placed'); // FAIL // toHaveText — exact full text — this will PASS await expect(page.locator('.msg')).toHaveText('Order #1234 Placed Successfully'); // PASS // toContainText — partial match — this will PASS await expect(page.locator('.msg')).toContainText('Placed'); // PASS await expect(page.locator('.msg')).toContainText('Successfully'); // PASS
💡Interview tip: Use toContainText() when the full text has dynamic parts like order numbers or dates. Use toHaveText() when you want to verify the complete static text.
Q33 What is the difference between toBeVisible() and toBeAttached()?
Beginner

Both check if an element exists, but at different levels:

  • toBeAttached() — checks that the element exists in the HTML code. It may be hidden with display:none or visibility:hidden, but it is in the DOM.
  • toBeVisible() — checks that the element is both in the DOM AND actually visible to the user on screen.
JavaScript
// A toast message exists in HTML but is hidden (display:none) await expect(page.locator('#toast')).toBeAttached(); // PASS — it is in HTML await expect(page.locator('#toast')).toBeVisible(); // FAIL — user cannot see it // After clicking Save button, toast becomes visible await page.click('#save-btn'); await expect(page.locator('#toast')).toBeVisible(); // PASS now
📌In most test cases you will use toBeVisible(). Use toBeAttached() only when you specifically want to verify that an element exists in the HTML code regardless of whether it is visible or not.
Q34 How do you assert a value inside an input field?
Intermediate

For input fields, you must use toHaveValue() — not toHaveText(). Input fields do not have text content — they have a value property. Using toHaveText() on an input will always fail.

JavaScript
// Fill a field and verify it was filled correctly await page.fill('#mobile-number', '9876543210'); await expect(page.locator('#mobile-number')).toHaveValue('9876543210'); // Check a checkbox is checked await expect(page.locator('#terms-checkbox')).toBeChecked(); // Check a checkbox is NOT checked await expect(page.locator('#newsletter')).not.toBeChecked(); // Check submit button is disabled await expect(page.locator('#submit-btn')).toBeDisabled(); // Check submit button is enabled await expect(page.locator('#submit-btn')).toBeEnabled();
⚠️Common mistake: Using toHaveText() on an input field. Input fields always have empty text content. Always use toHaveValue() for inputs, select dropdowns, and textareas.
Q35 What is soft assertion in Playwright?
Intermediate

Normally when an assertion fails, the test stops immediately. With soft assertion, the test continues running even after a failure — it collects all failures and reports them together at the end.

JavaScript
// Normal assertion — test STOPS if this fails await expect(page.locator('h1')).toHaveText('Dashboard'); // Soft assertion — test CONTINUES even if this fails await expect.soft(page.locator('h1')).toHaveText('Dashboard'); await expect.soft(page.locator('.user-name')).toHaveText('Rahul'); await expect.soft(page.locator('.plan-name')).toHaveText('Pro Plan'); await expect.soft(page.locator('.status')).toHaveText('Active'); // All soft failures are shown together at end of test
💡When to use: Use soft assertions when checking multiple fields on a profile or dashboard page. Instead of fixing one failure at a time, you get all failures in a single test run — saves time during bug reporting.
Q36 How do you assert that a list has a specific number of items?
Intermediate

Use toHaveCount() to assert the exact number of elements that match a locator.

JavaScript
// Verify exactly 5 products are shown on the page await expect(page.locator('.product-card')).toHaveCount(5); // Verify cart has 3 items await expect(page.locator('.cart-item')).toHaveCount(3); // Verify at least 1 result is shown after search const count = await page.locator('.result-item').count(); expect(count).toBeGreaterThan(0); // Verify no results when search finds nothing await expect(page.locator('.result-item')).toHaveCount(0);
Q37 How do you verify a URL contains a specific text or matches a pattern?
Intermediate

Use toHaveURL() with either a full string, a partial string, or a regex pattern.

JavaScript
// Exact URL match await expect(page).toHaveURL('https://example.com/dashboard'); // Partial match using regex — useful for dynamic URLs await expect(page).toHaveURL(/dashboard/); await expect(page).toHaveURL(/order\/\d+/); // matches /order/1234 // Using string includes — check URL contains this text const url = page.url(); expect(url).toContain('/order/'); expect(url).toContain('status=success');
💡Real use case: After placing an order, the URL might be /order/5892/confirmation where 5892 is dynamic. Use regex /order\/\d+\/confirmation/ to match any order ID number.
Q38 What is Auto-Wait in Playwright assertions?
Intermediate

Playwright assertions automatically retry until the condition is met or the timeout is reached. This means you do not need to add manual waits before your assertions — Playwright handles it for you.

JavaScript
// After clicking submit, the success message loads in 2 seconds // You do NOT need to add a manual wait here await page.click('#submit-btn'); // Playwright keeps retrying this for up to 5 seconds automatically await expect(page.locator('.success-msg')).toBeVisible(); // You can increase the timeout if needed await expect(page.locator('.report-table')).toBeVisible({ timeout: 15000 }); // Now Playwright retries for up to 15 seconds
⚠️The default assertion timeout is 5 seconds. If an element takes longer to appear — like a report that takes 10 seconds to generate — always increase the timeout explicitly. Do not rely on the default.
Q39 How do you wait for an element to appear or disappear?
Intermediate

Use waitFor() on a locator to explicitly wait for an element to reach a specific state.

JavaScript
// Wait for element to become visible await page.locator('.data-table').waitFor({ state: 'visible' }); // Wait for loading spinner to disappear await page.locator('.loading-spinner').waitFor({ state: 'hidden' }); // Wait with custom timeout await page.locator('.pdf-preview').waitFor({ state: 'visible', timeout: 20000 }); // Real example: After clicking Search, wait for spinner // to disappear before checking results await page.click('#search-btn'); await page.locator('.loading-spinner').waitFor({ state: 'hidden' }); await expect(page.locator('.search-results')).toBeVisible();
Q40 What is the difference between waitForTimeout() and waitFor()? Which should you use?
Intermediate
waitForTimeout()waitFor()
What it doesPauses test for a fixed time (like Thread.sleep)Waits until a specific condition is met
ReliabilityUnreliable — too short breaks tests, too long wastes timeReliable — stops as soon as condition is true
Use in real projectsNever — anti-patternYes — preferred approach
JavaScript
// BAD — fixed sleep, unreliable and slow await page.waitForTimeout(3000); // always waits 3 seconds even if ready in 0.5s // GOOD — waits only until condition is true await page.locator('.success-message').waitFor({ state: 'visible' }); // GOOD — wait for a page navigation await page.waitForURL('**/dashboard'); // GOOD — wait for network request to finish await page.waitForResponse('**/api/save');
⚠️Interview tip: If asked about waits, always say — "I avoid waitForTimeout() in my tests. It is an anti-pattern that makes tests slow and flaky. I use waitFor() with a specific state, or rely on Playwright's built-in auto-wait."
Q41 How do you wait for a page to fully load in Playwright?
Intermediate

When you navigate to a URL, you can tell Playwright how much to wait using the waitUntil option.

OptionWhat it waits forWhen to use
domcontentloadedHTML is parsed and DOM is readyFastest — for simple static pages
load (default)Page and all resources loadedMost common — good for most pages
networkidleNo network requests for 500msFor pages that load data via APIs after load
JavaScript
// Default — wait for 'load' event await page.goto('https://example.com'); // Wait until all API calls are finished (good for dashboards) await page.goto('https://example.com/dashboard', { waitUntil: 'networkidle' }); // Best practice: navigate + wait for a key element to appear await page.goto('https://example.com/dashboard'); await page.locator('.dashboard-content').waitFor();
💡The most reliable approach for modern web apps is to navigate to the URL and then wait for a specific element that confirms the page content has loaded — like the main heading or a data table appearing.
Q42 How do you assert CSS properties like color or font size?
Intermediate

Use toHaveCSS() to assert CSS property values of an element. This is useful for verifying UI styling — like checking an error message appears in red.

JavaScript
// Check error message text is red await expect(page.locator('.error-msg')) .toHaveCSS('color', 'rgb(220, 38, 38)'); // Check button has correct background color await expect(page.locator('#submit-btn')) .toHaveCSS('background-color', 'rgb(26, 86, 219)'); // Check element is hidden using CSS display await expect(page.locator('.tooltip')) .toHaveCSS('display', 'none');
📌CSS color values in Playwright are always in rgb() format, not hex. So #ff0000 becomes rgb(255, 0, 0). Check the actual computed style in browser DevTools to get the correct value.
Q43 How do you wait for a network API response in Playwright?
Intermediate

Use waitForResponse() to wait until a specific API call is completed. This is very useful when clicking a button triggers an API call and you want to wait for that call to finish before asserting the UI.

JavaScript
// Wait for API response when clicking Save button const [response] = await Promise.all([ page.waitForResponse('**/api/save-profile'), // wait for this API page.click('#save-btn') // trigger it by clicking ]); // Verify API returned success status expect(response.status()).toBe(200); // Also verify the response body content const body = await response.json(); expect(body.success).toBe(true);
💡Why use Promise.all here? If you wait for the response AFTER clicking, you might miss it — the API could already respond before your waitForResponse is set up. Promise.all sets up the wait and triggers the click at the same time.
Q44 What is expect.poll() and when do you use it?
Intermediate

expect.poll() keeps calling a function repeatedly until the return value matches the expected condition — or until timeout. Use it when Playwright's built-in assertions cannot handle your condition.

JavaScript
// Example: Wait until a background job is completed // The job status API is polled every few seconds await expect.poll(async () => { const response = await page.request.get('/api/job-status'); const body = await response.json(); return body.status; }, { timeout: 30000, // wait up to 30 seconds total intervals: [1000, 2000, 5000] // check after 1s, 2s, then every 5s }).toBe('completed');
📌Real use case: Testing a report generation feature that takes 10-20 seconds. You keep polling the status API until it returns "completed", then verify the report appeared on the UI.
Q45 How do you handle a loading spinner that appears between actions?
Intermediate

Many web applications show a loading spinner after clicking a button while data loads in the background. Your test must wait for the spinner to disappear before performing the next action — otherwise you may try to interact with elements that are not ready yet.

JavaScript
// Scenario: Search on an e-commerce site // After clicking Search, a spinner shows while results load // Step 1: Click the search button await page.click('#search-btn'); // Step 2: Wait for spinner to disappear await page.locator('.loading-spinner').waitFor({ state: 'hidden' }); // Step 3: Now results are ready — interact with them await expect(page.locator('.search-results')).toBeVisible(); const count = await page.locator('.result-item').count(); console.log(`Found ${count} results`);
💡Interview tip: This is a very common real-world scenario. Always explain this as — "I wait for the spinner to reach the hidden state, then proceed. This is better than a fixed wait because it adapts to how long the actual load takes."
🖱️
Section 4 — Page Interactions
Handling dropdowns, uploads, alerts, iframes, tabs and more
Q46 – Q60
Q46 How do you handle dropdowns in Playwright?
Beginner

Playwright handles two types of dropdowns differently — standard HTML <select> dropdowns and custom dropdowns built with divs or spans.

JavaScript
// Standard HTML select dropdown — use selectOption() await page.selectOption('#country-select', 'India'); // Select by value attribute await page.selectOption('#country-select', { value: 'IN' }); // Select by index (select the 3rd option) await page.selectOption('#country-select', { index: 2 }); // Select multiple options (for multi-select dropdowns) await page.selectOption('#skills', ['selenium', 'playwright', 'jmeter']); // Custom dropdown (div/span based — not a select tag) await page.click('.dropdown-toggle'); // open the dropdown await page.click('text=India'); // click the option
💡Interview tip: Always check in browser DevTools if the dropdown is a real <select> tag or a custom component. If it is a select tag, use selectOption(). If it is built with divs and custom CSS, use click() to open and click() to select.
Q47 How do you handle checkboxes and radio buttons?
Beginner

Playwright has dedicated methods for checkboxes — check() and uncheck(). For radio buttons, use check() as well.

JavaScript
// Check a checkbox await page.check('#terms-checkbox'); // Uncheck a checkbox await page.uncheck('#newsletter-checkbox'); // Verify checkbox is checked await expect(page.locator('#terms-checkbox')).toBeChecked(); // Verify checkbox is NOT checked await expect(page.locator('#newsletter')).not.toBeChecked(); // Select a radio button await page.check('input[value="online"]'); // Check using isChecked() method — returns true/false const isChecked = await page.locator('#terms-checkbox').isChecked(); console.log('Is checked:', isChecked);
Q48 How do you handle file uploads in Playwright?
Intermediate

Playwright has a simple setInputFiles() method for file uploads. It works directly with the file input element — no need to interact with the OS file dialog.

JavaScript
// Upload a single file await page.setInputFiles('input[type="file"]', './files/resume.pdf'); // Upload multiple files at once await page.setInputFiles('input[type="file"]', [ './files/photo.jpg', './files/certificate.pdf' ]); // Remove all uploaded files (clear the input) await page.setInputFiles('input[type="file"]', []); // When a button triggers the file dialog (not a direct input) const [fileChooser] = await Promise.all([ page.waitForEvent('filechooser'), page.click('#upload-btn') ]); await fileChooser.setFiles('./files/resume.pdf');
💡Interview tip: The file path in setInputFiles() is relative to where your test file is located. In a real project, keep test files in a test-data/ folder and reference them as './test-data/resume.pdf'.
Q49 How do you handle alerts, confirm, and prompt dialogs?
Intermediate

Browser dialogs (alert, confirm, prompt) are handled using the dialog event. You must register the handler BEFORE triggering the action that opens the dialog.

JavaScript
// Handle Alert — click OK page.on('dialog', dialog => dialog.accept()); await page.click('#show-alert-btn'); // Handle Confirm — click Cancel page.on('dialog', dialog => dialog.dismiss()); await page.click('#delete-btn'); // Handle Prompt — type a value and click OK page.on('dialog', async dialog => { console.log('Dialog message:', dialog.message()); await dialog.accept('Rahul Sharma'); // type this in the prompt }); await page.click('#enter-name-btn');
⚠️Always register page.on('dialog', ...) BEFORE clicking the button that triggers the dialog. If you register it after, Playwright automatically dismisses the dialog before your handler runs.
Q50 How do you handle multiple browser tabs or windows?
Intermediate

When clicking a link that opens in a new tab, use context.waitForEvent('page') to capture the new tab and work on it.

JavaScript
// Click a link that opens in a new tab const [newTab] = await Promise.all([ context.waitForEvent('page'), // listen for new tab page.click('a[target="_blank"]') // click link ]); // Wait for new tab to fully load await newTab.waitForLoadState(); // Work on the new tab await expect(newTab).toHaveURL(/terms-and-conditions/); await expect(newTab.locator('h1')).toBeVisible(); // Close new tab and go back to original tab await newTab.close(); // 'page' variable still refers to the original tab
📌Real example: On a job portal, clicking "View Company Website" opens a new tab. You test that the new tab URL is correct and has a valid heading, then close it and continue testing the job portal.
Q51 How do you handle iframes in Playwright?
Intermediate

An iframe is a web page embedded inside another web page. Common examples are payment gateways (Razorpay, PayU), Google Maps, and YouTube embeds. Elements inside an iframe cannot be accessed directly — you must use frameLocator() first.

JavaScript
// Get the iframe using frameLocator() const iframe = page.frameLocator('iframe[title="Payment Form"]'); // Now interact with elements INSIDE the iframe await iframe.locator('#card-number').fill('4111 1111 1111 1111'); await iframe.locator('#expiry').fill('12/26'); await iframe.locator('#cvv').fill('123'); await iframe.locator('button.pay-now').click(); // You can also locate iframe by src URL const mapFrame = page.frameLocator('iframe[src*="google.com/maps"]');
💡Playwright's frameLocator() is much simpler than Selenium's switchTo().frame(). You do not need to switch back — just use the iframe variable for all elements inside it and the page variable for elements outside.
Q52 How do you handle hover actions in Playwright?
Intermediate

Use hover() to move the mouse over an element — this triggers CSS :hover effects and shows tooltips or dropdown menus that appear on hover.

JavaScript
// Hover over user avatar to show profile dropdown menu await page.hover('#user-avatar'); // After hover, dropdown appears — now click an option await page.click('text=My Profile'); // Hover to reveal tooltip and verify its text await page.locator('.info-icon').hover(); await expect(page.locator('.tooltip')).toBeVisible(); await expect(page.locator('.tooltip')).toContainText('Click to learn more');
Q53 How do you handle keyboard actions in Playwright?
Intermediate
JavaScript
// Press Enter after typing in search box await page.fill('#search', 'Playwright'); await page.press('#search', 'Enter'); // Press Tab to move focus to next field await page.press('#email', 'Tab'); // Press Escape to close a modal await page.keyboard.press('Escape'); // Ctrl+A to select all text in a field await page.press('#text-editor', 'Control+A'); // Shift+Click to select multiple items in a list await page.click('.item:nth-child(1)'); await page.click('.item:nth-child(5)', { modifiers: ['Shift'] });
Q54 What is the difference between fill() and type() in Playwright?
Intermediate
MethodHow it worksSpeedUse when
fill()Clears the field and sets value directlyFastMost form inputs — standard text, email, password
type()Simulates actual key press for each characterSlowSearch inputs with live autocomplete suggestions
pressSequentially()Like type() but with configurable delay between keysSlowestWhen the app needs a typing delay to trigger events
JavaScript
// fill() — use for most inputs await page.fill('#email', 'test@gmail.com'); // type() — use for autocomplete search inputs // Each keypress triggers a suggestion list update await page.locator('#city-search').type('Mumbai'); await page.locator('.suggestion-item').first().click();
💡Interview tip: Always use fill() as the default. Switch to type() only when the input has a live search or autocomplete feature that listens to each individual keypress event.
Q55 How do you scroll to an element or scroll the page?
Intermediate

Playwright automatically scrolls to elements before clicking them. But sometimes you need to scroll manually — like to load lazy-loaded content or to scroll inside a specific container.

JavaScript
// Scroll a specific element into view await page.locator('#footer-section').scrollIntoViewIfNeeded(); // Scroll to bottom of page (for lazy loading) await page.evaluate(() => window.scrollTo(0, document.body.scrollHeight)); // Scroll to top of page await page.evaluate(() => window.scrollTo(0, 0)); // Scroll inside a specific container (like a modal) await page.locator('.modal-body').evaluate( el => el.scrollTop = el.scrollHeight );
Q56 How do you handle drag and drop in Playwright?
Intermediate
JavaScript
// Method 1: dragTo() — simplest approach await page.locator('#draggable-item').dragTo( page.locator('#drop-zone') ); // Verify item landed in drop zone await expect(page.locator('#drop-zone .item')).toBeVisible(); // Method 2: Manual mouse events (when dragTo doesn't work) await page.locator('#drag-item').hover(); await page.mouse.down(); await page.mouse.move(500, 300); // move to x,y coordinates await page.mouse.up();
📌Try dragTo() first — it works for most standard drag and drop implementations. Use the manual mouse approach only when dragTo() does not work, which happens with some complex drag libraries like SortableJS or React DnD.
Q57 How do you test a file download in Playwright?
Intermediate
JavaScript
test('Invoice PDF downloads successfully', async ({ page }) => { // Listen for download event and click button simultaneously const [download] = await Promise.all([ page.waitForEvent('download'), page.click('#download-invoice-btn') ]); // Verify the filename is correct expect(download.suggestedFilename()).toContain('invoice'); // Save the file and verify it is not empty const path = await download.path(); const fs = require('fs'); const fileSize = fs.statSync(path).size; expect(fileSize).toBeGreaterThan(0); console.log('Downloaded file size:', fileSize, 'bytes'); });
Q58 How do you handle a popup that appears randomly on the page?
Intermediate

Cookie banners, newsletter popups, and chat widgets can appear randomly during test execution. These need to be handled so they do not block your test actions.

JavaScript
// Method 1: Check if popup is visible, close it if present const popup = page.locator('.newsletter-popup'); if (await popup.isVisible()) { await page.locator('.popup-close-btn').click(); } // Method 2: addLocatorHandler — auto-handles popup whenever it appears // Add this in beforeEach so it runs before every test await page.addLocatorHandler( page.locator('.cookie-banner'), async () => { await page.locator('#accept-cookies').click(); } );
💡Use addLocatorHandler for popups that appear unpredictably during any point in your test. Once registered, Playwright automatically closes it whenever it appears — you do not need to handle it in every test.
Q59 How do you handle date picker inputs?
Intermediate

Date pickers come in two types — standard HTML date inputs and custom calendar components. Each needs a different approach.

JavaScript
// Type 1: Standard HTML date input — use fill() with YYYY-MM-DD format await page.fill('input[type="date"]', '2024-12-25'); // Type 2: If fill() doesn't work — set value via JavaScript await page.evaluate(() => { document.querySelector('input[type="date"]').value = '2024-12-25'; }); // Type 3: Custom calendar picker (click through month/day) await page.click('.datepicker-input'); // open calendar await page.click('text=December 2024'); // navigate to month await page.click('[aria-label="December 25, 2024"]'); // pick the day
⚠️Standard HTML date input format is always YYYY-MM-DD — not DD/MM/YYYY. Using the wrong format will silently fail to set the date without throwing an error.
Q60 How do you execute JavaScript directly on the page from a Playwright test?
Intermediate

Use page.evaluate() to run JavaScript code directly inside the browser — as if you typed it in the browser Console. This is useful when Playwright's built-in methods cannot do what you need.

JavaScript
// Get the page title using JS const title = await page.evaluate(() => document.title); // Set a value in localStorage (for login token injection) await page.evaluate(() => { localStorage.setItem('auth_token', 'test-token-123'); }); // Remove a blocking overlay or cookie banner using JS await page.evaluate(() => { document.querySelector('.cookie-overlay')?.remove(); }); // Pass values from test into the browser const discount = await page.evaluate((price) => { return price * 0.1; }, 500); // returns 50
⚠️Use evaluate() only when Playwright's built-in methods cannot solve the problem. Overusing it means your tests bypass the actual user experience — which defeats the purpose of UI testing.
"
⭐ Student Success Story

I was doing manual testing for 2 years with no growth. After completing the Playwright course at STAD Solution, I learned end-to-end automation, API testing, and CI/CD integration with real projects. Got placed within 45 days of completing the course.

R
Rahul Sharma ✓ Placed at Accenture
Manual Tester → QA Automation Engineer
🏗️
Section 5 — Page Object Model (POM)
Writing maintainable and scalable test code
Q61 – Q70
Q61 What is Page Object Model (POM)? Why should you use it?
Intermediate

Page Object Model is a design pattern where you create a separate class (file) for each page of the application. This class stores all the locators and actions for that page. Your test files then use these classes instead of writing locators and actions directly inside tests.

Why teams use POM:

  • If a locator changes, you update it in ONE place — not in 50 test files
  • Tests become shorter and easier to read
  • Same page class is reused across multiple test files
  • New team members can understand tests faster
POM Structure
📄 pages/
LoginPage.js
+
📄 pages/
DashboardPage.js
🧪 tests/
login.spec.js
💡Interview tip: Always say — "POM separates the test logic from the page structure. If the UI changes, I only update the Page Object class — not every test file. This makes the test suite easy to maintain."
Q62 How do you create a Page Object class in Playwright?
Intermediate

A Page Object class has two parts — the constructor where you define locators, and methods where you define actions that can be performed on that page.

pages/LoginPage.js
class LoginPage { constructor(page) { this.page = page; // Define all locators here — one place to update if UI changes this.emailInput = page.locator('#email'); this.passwordInput = page.locator('#password'); this.loginButton = page.getByRole('button', { name: 'Login' }); this.errorMessage = page.locator('.error-msg'); } // Action methods — reusable across multiple tests async goto() { await this.page.goto('/login'); } async login(email, password) { await this.emailInput.fill(email); await this.passwordInput.fill(password); await this.loginButton.click(); } async getErrorMessage() { return await this.errorMessage.textContent(); } } module.exports = { LoginPage };
tests/login.spec.js — Using the Page Object
const { test, expect } = require('@playwright/test'); const { LoginPage } = require('../pages/LoginPage'); test('Valid login works', async ({ page }) => { const loginPage = new LoginPage(page); await loginPage.goto(); await loginPage.login('user@test.com', 'Pass@123'); await expect(page).toHaveURL('/dashboard'); }); test('Wrong password shows error', async ({ page }) => { const loginPage = new LoginPage(page); await loginPage.goto(); await loginPage.login('user@test.com', 'wrongpass'); await expect(page.locator('.error-msg')).toBeVisible(); });
Q63 How do you structure a Playwright project with POM?
Intermediate

A well-structured Playwright project separates test files, page objects, test data, and utilities into different folders.

Project Structure
my-playwright-project/ │ ├── pages/ ← Page Object classes │ ├── LoginPage.js │ ├── DashboardPage.js │ └── CheckoutPage.js │ ├── tests/ ← Test files │ ├── login.spec.js │ ├── checkout.spec.js │ └── dashboard.spec.js │ ├── test-data/ ← Test input data │ ├── users.json │ └── products.json │ ├── utils/ ← Helper functions │ └── helpers.js │ ├── auth/ ← Saved login sessions │ └── admin-session.json │ └── playwright.config.js ← Main config file
💡Interview tip: Interviewers love this question. Show you know the standard folder structure. Explain each folder's purpose — it demonstrates that you have worked on a real project, not just written individual test scripts.
Q64 What is data-driven testing in Playwright?
Intermediate

Data-driven testing means running the same test multiple times with different input values. Instead of writing 5 separate tests for 5 user types, you write 1 test and supply 5 data sets.

JavaScript
const loginData = [ { email: 'admin@test.com', password: 'Admin@123', role: 'Admin' }, { email: 'user@test.com', password: 'User@123', role: 'User' }, { email: 'manager@test.com', password: 'Mgr@123', role: 'Manager' }, ]; // Loop creates 3 separate tests automatically for (const data of loginData) { test(`Login as ${data.role}`, async ({ page }) => { const loginPage = new LoginPage(page); await loginPage.goto(); await loginPage.login(data.email, data.password); await expect(page.locator('.role-label')).toHaveText(data.role); }); }
📌You can also store test data in an external JSON file and import it — this keeps your test code clean and makes it easy for non-developers to update test data without touching the test logic.
Q65 What are Playwright Fixtures and how are they different from beforeEach?
Intermediate

Fixtures are reusable setup blocks that are injected directly into test functions. They are more powerful than beforeEach because they are lazy — they only run if a test actually uses them.

beforeEachFixtures
Runs whenBefore every test in the groupOnly when a test requests it
ReusabilityOnly in the same fileShared across all test files
Best forSimple one-file setupLarge projects with many test files
fixtures/index.js — Define fixture
const { test: base } = require('@playwright/test'); const { LoginPage } = require('../pages/LoginPage'); const test = base.extend({ // loginPage fixture — auto creates LoginPage for every test loginPage: async ({ page }, use) => { const login = new LoginPage(page); await login.goto(); await use(login); // provide it to the test } }); module.exports = { test };
tests/login.spec.js — Use fixture
const { test } = require('../fixtures'); const { expect } = require('@playwright/test'); // loginPage is injected automatically — no new LoginPage() needed test('Valid login works', async ({ loginPage, page }) => { await loginPage.login('user@test.com', 'Pass@123'); await expect(page).toHaveURL('/dashboard'); });
Q66 What is storageState and how does it speed up tests?
Advanced

storageState saves your logged-in browser session — cookies, localStorage, sessionStorage — to a JSON file. You can load this file in your tests so they start already logged in, skipping the login step completely.

Why this matters: If you have 50 tests that all need login, without storageState each test logs in and logs out — that is 50 extra login flows. With storageState, login happens only once and all 50 tests reuse that session.

setup/auth.setup.js — Save session once
test('Save admin login session', async ({ page }) => { await page.goto('/login'); await page.fill('#email', 'admin@test.com'); await page.fill('#password', 'Admin@123'); await page.click('#login-btn'); await expect(page).toHaveURL('/dashboard'); // Save the session to a file await page.context().storageState({ path: './auth/admin-session.json' }); });
playwright.config.js — Load session in all tests
use: { // All tests start already logged in as admin storageState: './auth/admin-session.json' }
💡Interview tip: This shows senior-level knowledge. Say — "I use storageState to avoid repeated login across tests. I run a one-time setup test that logs in and saves the session. All other tests load that session and start already authenticated."
Q67 How do you run tests in parallel in Playwright?
Advanced

Playwright runs tests in parallel by default — multiple tests run at the same time using separate workers. Each worker gets its own browser and BrowserContext so tests are completely isolated.

playwright.config.js
// Run all tests in all files in parallel fullyParallel: true, // Control how many tests run at the same time workers: 4, // 4 tests run simultaneously // Run tests in a specific FILE one after another // Use when tests in a file depend on each other test.describe.configure({ mode: 'serial' });
📌Important: Parallel tests must be independent — they should not share test data or depend on each other's results. Each test should set up its own data and clean up after itself. Otherwise parallel tests will interfere with each other.
Q68 How do you use tags to run only specific tests?
Advanced

Tags help you organize and selectively run tests. Add tags using @tag in the test name, then filter by tag from the terminal.

JavaScript — Add tags to tests
// Add @tags to test names test('Login test @smoke', async ({ page }) => { }); test('Checkout test @smoke @regression', async ({ page }) => { }); test('Profile update @regression', async ({ page }) => { }); test('Payment @regression @payment', async ({ page }) => { });
Terminal — Run by tag
# Run only smoke tests (quick sanity check) npx playwright test --grep @smoke # Run full regression suite npx playwright test --grep @regression # Run only payment-related tests npx playwright test --grep @payment # Skip smoke tests, run everything else npx playwright test --grep-invert @smoke
💡Real team use: In companies, smoke tests run on every deployment to check basic functionality. Full regression runs overnight. Payment tests run before every release. Tags make this easy to manage from one test suite.
Q69 What is the Playwright HTML Report and Trace Viewer?
Advanced

Playwright has two powerful built-in tools for understanding test results:

HTML Report — A visual report showing all test results with pass/fail status, screenshots, error messages, and test duration. You can share this with your team or manager after a test run.

Trace Viewer — A step-by-step recording of everything that happened during a test — every click, every network call, every DOM snapshot. When a test fails, you open the trace to see exactly what went wrong.

playwright.config.js — Enable trace and report
reporter: ['html'], // generates HTML report use: { trace: 'retain-on-failure', // save trace only when test fails screenshot: 'only-on-failure', // save screenshot on failure video: 'retain-on-failure', // save video on failure }
Terminal
# Open HTML report in browser npx playwright show-report # Open trace file to debug a failure npx playwright show-trace trace.zip
📌Trace Viewer is one of Playwright's most powerful features. It shows you DOM snapshots at every step, network calls, console logs, and the exact line of code that failed — all in one timeline view. No other tool offers this level of debugging detail.
Q70 How do you integrate Playwright tests with CI/CD using GitHub Actions?
Advanced

CI/CD integration means your Playwright tests run automatically every time someone pushes code to GitHub. If tests fail, the code merge is blocked — preventing broken code from reaching production.

.github/workflows/playwright.yml
name: Playwright Tests on: push: branches: [main] pull_request: branches: [main] jobs: test: runs-on: ubuntu-latest steps: - uses: actions/checkout@v3 - uses: actions/setup-node@v3 with: node-version: 18 - name: Install dependencies run: npm ci - name: Install Playwright browsers run: npx playwright install --with-deps - name: Run Playwright tests run: npx playwright test - name: Upload HTML report if: always() uses: actions/upload-artifact@v3 with: name: playwright-report path: playwright-report/
💡Interview tip: This is a must-know for experienced candidates. Say — "After every code push, GitHub Actions automatically installs dependencies, installs browsers, runs all Playwright tests, and uploads the HTML report as an artifact. If any test fails, the pipeline fails and the merge is blocked."
🔌
Section 6 — API Testing in Playwright
Testing APIs directly and combining API + UI in tests
Q71 – Q80
Q71 Can Playwright do API testing? How is it different from Postman?
Intermediate

Yes. Playwright has a built-in request object that lets you send HTTP requests — GET, POST, PUT, DELETE — directly inside your test code without opening a browser.

Playwright API TestingPostman
Where it runsInside your test code — automatedManual tool — run by a person
CI/CDYes — runs automatically in pipelineNeeds extra setup (Newman CLI)
Combine with UIYes — API + UI in same testNo — separate tool
Best forAutomated regression testingQuick manual exploration
💡Interview tip: Say — "Playwright's API testing is not a replacement for Postman. Postman is great for exploring and manually testing APIs. Playwright is better when you want to automate API checks as part of your regression suite or combine API calls with UI tests."
Q72 How do you send a GET request in Playwright?
Intermediate

Use the request fixture provided by Playwright. It works like a built-in HTTP client — similar to axios or fetch but inside your test.

JavaScript
const { test, expect } = require('@playwright/test'); test('GET — Verify user data is returned correctly', async ({ request }) => { // Send GET request const response = await request.get('https://reqres.in/api/users/1'); // Verify status code is 200 expect(response.status()).toBe(200); // Parse the JSON response body const body = await response.json(); // Verify the response data expect(body.data.id).toBe(1); expect(body.data.email).toBeTruthy(); console.log('User email:', body.data.email); });
Q73 How do you send a POST request with a request body?
Intermediate
JavaScript
test('POST — Create a new user', async ({ request }) => { const response = await request.post('https://reqres.in/api/users', { data: { name: 'Rahul Sharma', job: 'QA Engineer' } }); // 201 = Created successfully expect(response.status()).toBe(201); const body = await response.json(); // Verify response has the data we sent expect(body.name).toBe('Rahul Sharma'); expect(body.job).toBe('QA Engineer'); // Server should have generated an ID expect(body.id).toBeTruthy(); console.log('New user created with ID:', body.id); });
Q74 How do you send PUT and DELETE requests in Playwright?
Intermediate
JavaScript
// PUT — update existing record test('PUT — Update user details', async ({ request }) => { const response = await request.put('https://reqres.in/api/users/2', { data: { name: 'Rahul Sharma', job: 'Senior QA' } }); expect(response.status()).toBe(200); const body = await response.json(); expect(body.job).toBe('Senior QA'); }); // DELETE — remove a record test('DELETE — Remove a user', async ({ request }) => { const response = await request.delete('https://reqres.in/api/users/2'); // 204 = No Content — deleted successfully expect(response.status()).toBe(204); });
📌HTTP Status codes to remember: 200 = OK, 201 = Created, 204 = No Content (delete success), 400 = Bad Request, 401 = Unauthorized, 404 = Not Found, 500 = Server Error.
Q75 How do you send API requests with headers and authentication token?
Intermediate

Most real APIs require authentication — you send a token in the request headers. Playwright makes this easy with the headers option.

JavaScript
// Send request with Authorization header (Bearer token) const response = await request.get('https://api.example.com/orders', { headers: { 'Authorization': `Bearer ${process.env.API_TOKEN}`, 'Content-Type': 'application/json', 'Accept': 'application/json' } }); expect(response.status()).toBe(200); // You can also set headers globally for all requests // in playwright.config.js use: { extraHTTPHeaders: { 'Authorization': `Bearer ${process.env.API_TOKEN}` } }
⚠️Never hardcode the actual token value in your test file. Always use process.env.API_TOKEN to read it from an environment variable. This keeps secrets out of your source code.
Q76 How do you use API calls to set up test data before a UI test?
Advanced

Instead of clicking through the UI to create test data, call the API directly — it is 5-10x faster. This technique is called API + UI hybrid testing and is highly valued in professional QA teams.

JavaScript
test('Verify new order appears in order list', async ({ page, request }) => { // Step 1: Create order via API — fast, no UI clicks needed const apiResponse = await request.post('/api/orders', { headers: { 'Authorization': `Bearer ${process.env.API_TOKEN}` }, data: { product: 'Selenium Course', amount: 4999 } }); const order = await apiResponse.json(); const orderId = order.id; // Step 2: Open UI and verify the order appears on screen await page.goto('/orders'); await expect(page.locator(`#order-row-${orderId}`)).toBeVisible(); await expect(page.locator(`#order-row-${orderId} .status`)).toHaveText('Pending'); });
💡Why this is powerful: In a pure UI test, creating an order requires: login → navigate to products → search → add to cart → checkout → fill address → pay. That is 7+ steps. With API setup, it is 1 API call. Your test becomes faster and focused only on what actually matters — verifying the UI display.
Q77 What is network interception? How do you mock an API response?
Advanced

Network interception lets you intercept any network request your page makes and return fake data instead of hitting the real server. This is useful for testing specific UI states without depending on the backend.

JavaScript
// Mock an API — return fake response instead of real server await page.route('**/api/courses', route => { route.fulfill({ status: 200, contentType: 'application/json', body: JSON.stringify([ { id: 1, name: 'Selenium Course', price: 4999 }, { id: 2, name: 'Playwright Course', price: 5999 } ]) }); }); // Now when page loads, it shows your fake data await page.goto('/courses'); await expect(page.locator('.course-card')).toHaveCount(2); // Block all image requests to speed up tests await page.route('**/*.{png,jpg,jpeg,gif,webp}', route => route.abort());
📌When to use mocking: Use it when the real API is slow, unreliable, or not yet built. Also useful for testing error states — like "what happens when the API returns a 500 error" — which is hard to reproduce with a real server.
Q78 How do you test that your app handles API errors correctly?
Advanced

Use route interception to simulate server errors and then verify the UI shows a proper error message to the user.

JavaScript
test('Shows error message when API fails', async ({ page }) => { // Make the products API return a 500 server error await page.route('**/api/products', route => { route.fulfill({ status: 500, body: 'Internal Server Error' }); }); await page.goto('/products'); // UI should show a user-friendly error message await expect(page.locator('.error-banner')).toBeVisible(); await expect(page.locator('.error-banner')).toContainText('Something went wrong'); // A retry button should appear await expect(page.locator('button.retry')).toBeVisible(); }); test('Shows 404 message when product not found', async ({ page }) => { await page.route('**/api/products/999', route => { route.fulfill({ status: 404, body: 'Not Found' }); }); await page.goto('/products/999'); await expect(page.locator('.not-found-msg')).toBeVisible(); });
Q79 What is the difference between page.request and the request fixture?
Advanced
request fixturepage.request
ContextStandalone — no browser contextShares browser context — has cookies and session
CookiesNo browser cookiesUses same cookies as the page
Best forPure API tests, setting up test data before UI testsCalling APIs that need the user's login session
JavaScript
// request fixture — standalone, no browser cookies test('API test', async ({ request }) => { const res = await request.get('/api/public-data'); }); // page.request — shares session with logged-in page test('Logged-in API call', async ({ page }) => { await page.goto('/login'); await page.fill('#email', 'user@test.com'); await page.click('#login-btn'); // This API call uses the same session as the logged-in browser const res = await page.request.get('/api/my-orders'); expect(res.status()).toBe(200); });
Q80 How do you validate the full structure of an API response?
Advanced

After parsing the response body, you can check individual fields, data types, array lengths, and nested objects. For complex schemas, use a library like zod or ajv.

JavaScript
test('Verify full API response structure', async ({ request }) => { const response = await request.get('https://reqres.in/api/users?page=1'); expect(response.status()).toBe(200); const body = await response.json(); // Check top-level fields exist expect(body).toHaveProperty('data'); expect(body).toHaveProperty('total'); expect(body).toHaveProperty('per_page'); // Check data is an array with items expect(Array.isArray(body.data)).toBe(true); expect(body.data.length).toBeGreaterThan(0); // Check structure of first user in the array const firstUser = body.data[0]; expect(firstUser).toHaveProperty('id'); expect(firstUser).toHaveProperty('email'); expect(firstUser).toHaveProperty('first_name'); // Check data types expect(typeof firstUser.id).toBe('number'); expect(typeof firstUser.email).toBe('string'); // Check email format using regex expect(firstUser.email).toMatch(/^[^\s@]+@[^\s@]+\.[^\s@]+$/); });
💡Interview tip: When asked about API testing, always mention — "I verify the status code, the response body structure, the data types of fields, and for arrays I check that the length is as expected. For critical APIs I also verify the response time is within acceptable limits."
Section 7 — Advanced Topics & CI/CD
Reporting, debugging, CI/CD pipelines and advanced features
Q81 – Q90
Q81 What is Playwright Trace Viewer and how do you use it?
Advanced

Trace Viewer is like a complete recording of your test run. It records every action, every network request, every screenshot, and every DOM snapshot step by step. When a test fails — especially on a CI server where you cannot see the browser — Trace Viewer shows you exactly what happened.

What Trace Viewer shows you:

  • A screenshot of the page at every test step
  • The exact line of code that failed
  • All network requests made during the test
  • Console logs and errors from the browser
  • DOM snapshot — inspect any element at any point in time
playwright.config.js — Enable trace on failure
use: { // Record trace only when test fails trace: 'retain-on-failure' }
Terminal — Open trace file
# After a test fails, open the trace file npx playwright show-trace test-results/trace.zip # Or open online without installing anything # Go to: https://trace.playwright.dev and upload the zip file
💡Interview tip: Trace Viewer is one of Playwright's most powerful features — no other automation tool has this. Always mention it when asked about debugging or test failure analysis. It shows you are aware of professional debugging practices.
Q82 How do you generate and share an HTML test report in Playwright?
Advanced

Playwright comes with a built-in HTML reporter that generates a beautiful, interactive report showing all test results — pass, fail, skip — with screenshots and traces attached to failed tests.

playwright.config.js
reporter: [ ['html', { outputFolder: 'playwright-report', open: 'never' }], ['list'] // also shows results in terminal ],
Terminal
# Run tests and generate report npx playwright test # Open the HTML report in browser npx playwright show-report
📌The HTML report is a single folder you can zip and share with your manager or client. It shows which tests passed, which failed, how long each test took, and has screenshots and videos attached for every failed test.
Q83 How do you run Playwright tests in a CI/CD pipeline using GitHub Actions?
Advanced

GitHub Actions automatically runs your Playwright tests every time code is pushed to the repository. If tests fail, the merge is blocked — this prevents broken code from going to production.

.github/workflows/playwright.yml
name: Playwright Tests on: push: branches: [main] pull_request: branches: [main] jobs: test: runs-on: ubuntu-latest steps: - uses: actions/checkout@v3 - uses: actions/setup-node@v3 with: node-version: 18 - name: Install dependencies run: npm ci - name: Install Playwright browsers run: npx playwright install --with-deps - name: Run Playwright tests run: npx playwright test - name: Upload test report if: always() uses: actions/upload-artifact@v3 with: name: playwright-report path: playwright-report/
💡Interview tip: Always mention — "I have set up GitHub Actions so tests run automatically on every pull request. Failed tests block the merge. The HTML report is uploaded as an artifact so the team can review failures without re-running tests."
Q84 What are test tags in Playwright and how do you run specific tags?
Advanced

Tags let you group and selectively run tests. In a large test suite, you might have smoke tests (quick sanity checks), regression tests (full coverage), and payment tests. Tags let you run only what you need.

JavaScript — Add tags to test names
// Add @tag in the test name test('Login works @smoke', async ({ page }) => { ... }); test('Checkout flow @regression @payment', async ({ page }) => { ... }); test('Profile update @regression', async ({ page }) => { ... });
Terminal — Run by tag
# Run only smoke tests (quick pre-deployment check) npx playwright test --grep @smoke # Run all regression tests npx playwright test --grep @regression # Run everything EXCEPT smoke tests npx playwright test --grep-invert @smoke # Run tests matching multiple tags npx playwright test --grep "@smoke|@payment"
📌Common tagging strategy in teams: @smoke = 10-15 critical tests run on every deployment (takes 5 min). @regression = full suite run nightly (takes 1-2 hrs). @payment = payment-related tests run before any payment code change.
Q85 What is visual testing in Playwright?
Advanced

Visual testing compares a current screenshot of the page with a saved baseline screenshot. If anything on the UI has changed — even by 1 pixel — the test fails. This catches accidental visual regressions like broken layouts, wrong colors, or shifted elements.

JavaScript
// First run: saves screenshot as baseline // All future runs: compares with baseline await expect(page).toHaveScreenshot('homepage.png'); // Compare only a specific element await expect(page.locator('.hero-section')).toHaveScreenshot('hero.png'); // Allow small differences (0.1 = allow up to 10% pixel difference) await expect(page).toHaveScreenshot('homepage.png', { maxDiffPixelRatio: 0.05 // 5% difference allowed }); # Update baseline when you intentionally change the UI # npx playwright test --update-snapshots
⚠️Visual tests can be flaky across different OS and screen resolutions. Run them on the same OS as your CI server. Use maxDiffPixelRatio to allow small rendering differences across environments.
Q86 How do you perform mobile device testing in Playwright?
Advanced

Playwright has 100+ built-in device profiles — each with the correct viewport size, user agent, and touch settings. You can run your tests on any device without needing a real phone.

playwright.config.js
const { devices } = require('@playwright/test'); projects: [ // Desktop { name: 'Desktop Chrome', use: { ...devices['Desktop Chrome'] } }, // Mobile devices { name: 'iPhone 14', use: { ...devices['iPhone 14'] } }, { name: 'Galaxy S23', use: { ...devices['Galaxy S23'] } }, { name: 'iPad Pro', use: { ...devices['iPad Pro'] } }, ]
Terminal — See all available devices
npx playwright devices
💡This is device emulation — not a real device. It simulates the screen size, user agent, and touch events. For apps where mobile behaviour is critical (like responsive layouts), always include iPhone and Android in your test projects.
Q87 What are test.skip(), test.only(), and test.fixme() used for?
Advanced
MethodWhat it doesWhen to use
test.skip()Test is skipped — not run at allFeature not yet built, or test environment issue
test.only()Only this test runs — all others are skippedTemporarily run one test during development
test.fixme()Test runs but is expected to fail — shown separatelyKnown bug tracked in JIRA — test documents the bug
JavaScript
// Skip this test completely test.skip('Payment test — gateway is down in staging', async ({ page }) => { // will not run }); // Run ONLY this test (remove before committing!) test.only('Login test', async ({ page }) => { // only this runs }); // Known bug — test runs but failure is expected test.fixme('Cart count is wrong — BUG-1234', async ({ page }) => { // documents the known bug }); // Conditional skip — skip on Firefox only test('Chrome-only feature test', async ({ page, browserName }) => { test.skip(browserName !== 'chromium', 'This feature is Chrome-only'); // rest of test });
⚠️Always remove test.only() before pushing code. If you forget, only that one test runs in CI — all other tests are silently skipped. This is one of the most common mistakes in teams using Playwright.
Q88 How do you debug a failing Playwright test?
Advanced

Playwright has several built-in debugging tools — use them in order from quickest to most detailed.

Terminal — Debugging approaches
# 1. Run with visible browser — see what is happening npx playwright test --headed # 2. Debug mode — opens Inspector, run step by step npx playwright test --debug # 3. UI mode — visual test runner with time-travel npx playwright test --ui # 4. Run only the failing test file npx playwright test tests/login.spec.js --headed
JavaScript — Pause inside test
test('Debug this test', async ({ page }) => { await page.goto('/login'); await page.fill('#email', 'test@test.com'); // Test pauses here — Inspector opens — you can inspect live await page.pause(); await page.click('#login-btn'); });
💡Step-by-step approach for debugging: First check the Trace Viewer for the failure. If you need more detail, run with --debug to step through line by line. Use page.pause() to stop at a specific point and inspect elements live in the browser.
Q89 What are flaky tests and how do you fix them?
Advanced

A flaky test is a test that sometimes passes and sometimes fails — without any code changes. It is one of the most discussed problems in QA teams because it destroys trust in the test suite.

Common causes and how to fix them:

  • Hard-coded waits — Replace waitForTimeout(2000) with waitFor({ state: 'visible' })
  • Race condition — API call not finished before assertion — use waitForResponse() to wait for the API
  • Unstable locator — Element re-renders and locator loses reference — switch to getByTestId or getByRole
  • Test data collision — Multiple parallel tests using the same data — use unique data per test
  • Animation not finished — Element is still moving when you try to click — wait for animation to complete
playwright.config.js — Short-term fix while investigating
// Retry failed tests automatically (max 2 times in CI) retries: process.env.CI ? 2 : 0,
⚠️Retries are a short-term band-aid — not a real fix. If a test is flaky, always investigate and fix the root cause. A flaky test that passes on retry is hiding a real problem in either your test code or the application itself.
Q90 What is Playwright UI Mode and when do you use it?
Advanced

UI Mode is a visual test runner that opens in your browser. It shows all your test files, lets you run individual tests, watch them execute in real time, and see screenshots and traces for every step — all without writing any terminal commands.

Terminal
npx playwright test --ui

Key features of UI Mode:

  • All tests listed in a sidebar — click any test to run it
  • Watch mode — test automatically reruns when you save your code
  • Time-travel debugging — scrub through screenshots at each step
  • Filter tests by file, status (pass/fail), or name
  • See network requests, console logs, and DOM snapshots side by side
💡When to use UI Mode vs --debug: Use UI Mode when writing new tests and want a visual feedback loop. Use --debug when you have a specific failing test and need to step through it line by line to find the problem.
🎯
Section 8 — Scenario Based Questions
Real interview scenarios — how to think and answer step by step
Q91 – Q100
Q91 Your test passes locally but fails in the CI pipeline. What will you check?
Scenario
🎯This is one of the most common real interview questions. Answer it step by step — shows your debugging mindset.

Step-by-step what to check:

  1. Check the CI failure screenshot and trace — Playwright saves these on failure. Look at what the page looked like when it failed.
  2. OS difference — Local is Windows or Mac, CI is Linux. Check if file paths, fonts, or case sensitivity differ.
  3. Timing issues — CI servers are slower than local machines. Increase timeouts in config for CI.
  4. Missing test data — CI has a fresh database. Data you have locally may not exist in CI.
  5. Browser version mismatch — Run npx playwright install in CI to ensure the same browser version is used.
  6. Environment variables missing — Check if API keys, base URLs are set in CI secrets correctly.
  7. Hard-coded waits — Replace any waitForTimeout() with proper element waits.
💡Answer tip: Start with — "First I would check the Playwright Trace Viewer file from the failed CI run. It shows screenshots at every step so I can see exactly where the test broke without re-running anything."
Q92 How would you automate an end-to-end e-commerce checkout flow?
Scenario
🎯This tests your ability to design a complete test — not just write single actions. Structure your answer as a flow.
JavaScript — Full E2E Checkout Test
test('Complete checkout flow', async ({ page, request }) => { // Step 1: Login via API (faster than UI login) const loginRes = await request.post('/api/login', { data: { email: 'user@test.com', password: 'Test@123' } }); const { token } = await loginRes.json(); // Step 2: Set auth token in browser await page.goto('/products'); await page.evaluate(t => localStorage.setItem('token', t), token); await page.reload(); // Step 3: Search and add product await page.fill('#search-box', 'Selenium Course'); await page.press('#search-box', 'Enter'); await page.locator('.product-card').first().locator('.add-to-cart').click(); await expect(page.locator('.cart-count')).toHaveText('1'); // Step 4: Checkout await page.click('#cart-icon'); await page.click('#proceed-to-checkout'); await page.fill('#delivery-address', '123 Test Street, Ahmedabad'); // Step 5: Place order and verify const [response] = await Promise.all([ page.waitForResponse('**/api/orders'), page.click('#place-order-btn') ]); expect(response.status()).toBe(201); await expect(page.locator('.order-success-msg')).toBeVisible(); });
💡Notice how login is done via API — not through the UI. This saves time and makes the test focused only on the checkout flow itself. Always use API for setup when possible.
Q93 A test is flaky — sometimes it passes, sometimes it fails. How do you investigate and fix it?
Scenario
🎯Flaky tests are discussed in almost every senior QA interview. A structured answer impresses the interviewer.

Step 1 — Reproduce it: Run the test 10 times in a loop to confirm it is actually flaky and see how often it fails.

Terminal
# Run same test 10 times to confirm flakiness npx playwright test tests/checkout.spec.js --repeat-each=10

Step 2 — Identify the root cause:

  • Open Trace Viewer of a failed run — see exactly which step failed
  • Check if there is a waitForTimeout() — replace with element-based wait
  • Check if element locator is unstable — switch to getByTestId or getByRole
  • Check if two parallel tests are using the same test data — make data unique per test
  • Check if an animation is still running when you try to click
JavaScript — Common fix examples
// WRONG — fixed sleep causes flakiness await page.waitForTimeout(2000); await page.click('#submit'); // RIGHT — wait for actual condition await page.locator('.loading-spinner').waitFor({ state: 'hidden' }); await page.click('#submit'); // WRONG — unstable locator breaks on re-render await page.locator('.MuiButton-root-123').click(); // RIGHT — stable locator await page.getByRole('button', { name: 'Submit' }).click();
Q94 How would you test an OTP-based login flow?
Scenario
🎯Very common in Indian company interviews — most apps use OTP login. Shows practical knowledge.

3 approaches — explain all three in the interview:

Approach 1 — Test phone number with fixed OTP (Best)

Ask the developer to create a test mobile number like 9999999999 whose OTP is always a fixed value like 123456 in staging/test environment.

JavaScript
await page.fill('#mobile', '9999999999'); await page.click('#send-otp'); await page.fill('#otp-input', '123456'); // fixed test OTP await page.click('#verify-otp');

Approach 2 — Mock the OTP verification API

JavaScript
// Intercept OTP verify API and return success always await page.route('**/api/verify-otp', route => { route.fulfill({ status: 200, body: JSON.stringify({ success: true, token: 'test-auth-token' }) }); }); await page.fill('#otp-input', '000000'); // any value works now await page.click('#verify-otp');
⚠️Never try to read the actual OTP from SMS in automation — it is complex, unreliable, and unnecessary. Always work with developers to make the test environment testable with fixed or mockable OTPs.
Q95 How would you handle a CAPTCHA in your automation tests?
Scenario
🎯This is asked in almost every QA interview. The correct answer shows real-world QA thinking — not just tool knowledge.

You should never try to solve or bypass the actual CAPTCHA in automation. That defeats the purpose of CAPTCHA. The correct approach is to make the test environment testable.

4 correct approaches — explain these in order:

  • Option 1 (Best): Ask the developer to disable CAPTCHA in the staging or test environment. Most apps have a flag for this.
  • Option 2: Developer adds a special header or test token that bypasses CAPTCHA when sent with the request.
  • Option 3: Mock the CAPTCHA verification API to always return success in tests.
  • Option 4 (Last resort): Use a CAPTCHA solving service — expensive, slow, and not reliable.
💡Best interview answer: "I would work with the development team to disable CAPTCHA in the test environment or add a bypass token for automation. As a QA, my job is to test the business logic — not fight CAPTCHA. A good developer always provides a way to make features testable."
Q96 How would you test pagination on a product listing page?
Scenario
JavaScript
test('Pagination works correctly', async ({ page }) => { await page.goto('/products'); // Verify page 1 is active by default await expect(page.locator('.page-btn.active')).toHaveText('1'); // Get first product name on page 1 const firstOnPage1 = await page.locator('.product-name').first().textContent(); // Verify page 1 shows correct number of products (e.g. 12 per page) await expect(page.locator('.product-card')).toHaveCount(12); // Go to page 2 await page.click('[aria-label="Next page"]'); await expect(page.locator('.page-btn.active')).toHaveText('2'); // First product on page 2 should be different from page 1 const firstOnPage2 = await page.locator('.product-name').first().textContent(); expect(firstOnPage2).not.toBe(firstOnPage1); // Go back to page 1 await page.click('[aria-label="Previous page"]'); await expect(page.locator('.page-btn.active')).toHaveText('1'); });
Q97 How would you test a multi-step registration form?
Scenario
JavaScript
test('Complete 3-step registration', async ({ page }) => { await page.goto('/register'); // Step 1: Personal Details await expect(page.locator('.step-indicator')).toHaveText('Step 1 of 3'); await page.fill('#full-name', 'Rahul Sharma'); await page.fill('#mobile', '9876543210'); await page.fill('#email', 'rahul@test.com'); await page.click('#next-btn'); // Step 2: Course Selection await expect(page.locator('.step-indicator')).toHaveText('Step 2 of 3'); await page.click('label[for="course-automation"]'); await page.selectOption('#batch-time', 'Morning'); await page.click('#next-btn'); // Step 3: Payment await expect(page.locator('.step-indicator')).toHaveText('Step 3 of 3'); await page.click('#pay-now-btn'); // Verify success await expect(page.locator('.registration-success')).toBeVisible(); await expect(page.locator('.confirm-email')).toContainText('rahul@test.com'); });
💡Always verify the step indicator at each step — this confirms the form is on the correct step before filling data. Also verify important data like email appears in the final confirmation.
Q98 How do you test that a form shows proper validation errors for empty or invalid inputs?
Scenario
JavaScript
test('Empty form shows required field errors', async ({ page }) => { await page.goto('/register'); // Click Submit without filling anything await page.click('#submit-btn'); // All required field errors should appear await expect(page.locator('#name-error')).toBeVisible(); await expect(page.locator('#email-error')).toBeVisible(); await expect(page.locator('#mobile-error')).toBeVisible(); // Verify error message text is correct await expect(page.locator('#email-error')).toHaveText('Email is required'); // Form should NOT have submitted — still on same page await expect(page).toHaveURL(/\/register/); }); test('Invalid email format shows error', async ({ page }) => { await page.goto('/register'); await page.fill('#email', 'notanemail'); await page.click('#submit-btn'); await expect(page.locator('#email-error')).toContainText('valid email'); });
Q99 You joined a new company and there are no automation tests. How do you start building the framework from scratch?
Scenario
🎯This is asked for senior roles and shows your planning and leadership skills — not just coding ability.

Step-by-step approach:

  1. Understand the application — Spend 1-2 weeks doing manual testing. Understand the key user flows, critical features, and existing bugs.
  2. Set up the project — Initialize Playwright with TypeScript, configure playwright.config.js with base URL, timeouts, retries, reporters.
  3. Define folder structure — Create /pages for POM, /tests for spec files, /fixtures for reusable setup, /test-data for data files.
  4. Start with smoke tests — Write 10-15 tests covering the most critical flows: login, main navigation, core business action. Get these stable first.
  5. Set up CI/CD — Add GitHub Actions workflow. Smoke tests run on every pull request. Full regression runs nightly.
  6. Expand coverage gradually — Add more tests every sprint. Prioritize tests for features with most bugs or highest business impact.
  7. Report to team — Share weekly test coverage metrics and failure reports so the team trusts and uses the automation.
💡Key point to mention: "I would not try to automate everything on day one. I would start with the critical smoke suite, make sure it is stable and running in CI, and then expand coverage sprint by sprint. Quality over quantity."
Q100 What is your approach when a critical test fails 30 minutes before a production release?
Scenario
🎯This is a pressure/judgment question. The interviewer wants to see how you handle urgency, communicate with the team, and make decisions — not just technical skills.

Step-by-step approach:

  1. Do not panic — Open Trace Viewer immediately. Find out if it is a real application bug or a test issue (wrong locator, timing problem).
  2. Reproduce manually — Open the browser and manually test the same scenario in 2 minutes. If it fails manually, it is a real bug. If it passes manually, it is likely a test issue.
  3. Communicate immediately — Tell the release manager right away — "Critical test is failing, investigating now." Never stay silent hoping it will resolve itself.
  4. If it is a real bug — Raise a blocker ticket with screenshots. The release manager and developers decide whether to fix it, defer it, or release with a known issue.
  5. If it is a test issue — Quick fix the locator or wait condition, re-run the test, confirm it passes. Document what was wrong.
  6. Document everything — Write a brief incident note: what failed, what was the root cause, what was done. This prevents the same issue next time.
💡Best answer line: "My first job is to find out whether it is a real application bug or a test issue — these need completely different responses. I check manually first, communicate with the team immediately, and never make the release decision alone."
🎉
All 100 Questions Completed!
You have covered everything from Playwright basics to advanced scenarios.
Practice these answers, understand the concepts, and you are ready for your interview.
✅ 100 Questions 💻 20+ Code Examples 🎯 10 Scenarios ⚡ CI/CD Ready 🏗️ POM Covered 🔌 API Testing
Get a Job Click Here!!!
Get Me
JOB

Experience the Training Before You Enroll

Understand the course, meet your mentor and see the live learning environment before joining.





     

    Apply for Job-Focused Training Program







       

      Experience the Training Before You Enroll

      Understand the course, meet your mentor and see the live learning environment before joining.