QAPage – Software Testing Jobs, QA Resources & Careers

QAPage Navigation & Directory
Type any keyword to search live pages, tools, or press Search to query site-wide.

Playwright Full Notes PDF For QA

Playwright Full Notes PDF

Playwright Notes For QA – friendly explanations • TypeScript examples • Real-time QA scenarios • Interview questions
These notes cover the 10 most important Playwright topics for software testers. Each topic includes what it means, why it is used, syntax, practical examples, common mistakes, and interview questions.
All examples use Playwright Test with TypeScript, a common setup for web automation.
Playwright Notes For QA

1. Locators

1.1 What is a locator?

A locator is a way to identify a web element on a webpage so that Playwright can interact with it.
Web elements include:
  • Text boxes
  • Buttons
  • Links
  • Checkboxes
  • Radio buttons
  • Dropdowns
  • Tables
  • Error messages
For example, on a login page, Playwright must identify the username field, password field, and Login button before it can enter data or click anything.
await page.getByLabel('Email').fill('tester@example.com');
await page.getByLabel('Password').fill('Test@123');
await page.getByRole('button', { name: 'Login' }).click();
Here, Playwright identifies the email field, password field, and Login button using their accessible labels and roles.

1.2 Types of locators

A. getByRole()
Identifies elements based on their accessible role and, optionally, their accessible name.
await page.getByRole('button', { name: 'Login' }).click();
await page.getByRole('link', { name: 'Forgot Password?' }).click();await page.getByRole('checkbox', { name: 'Remember me' }).check();
Common roles include button, link, textbox, checkbox, radio, and heading.
When to use: When an element has a meaningful accessible role and name. This is usually a good starting point because it reflects how users interact with the interface.
B. getByText()
Finds an element by its visible text.
await page.getByText('Login successful').waitFor();
await page.getByText('Add to Cart').click();
Use it when the text identifies the target element clearly. For buttons and links, getByRole() is often more descriptive.
C. getByLabel()
Identifies form controls using their associated accessible label.
await page.getByLabel('Username').fill('qa_tester');
await page.getByLabel('Password').fill('Test@123');
This works well for properly labelled form inputs.
D. getByPlaceholder()
Identifies inputs through their placeholder text.
await page.getByPlaceholder('Enter your email').fill('qa@example.com');
A placeholder is the hint displayed inside an empty field. It is not always a substitute for a proper accessible label.
E. getByTestId()
Identifies elements through a dedicated test attribute, commonly data-testid.
HTML:
<button data-testid="submit-login">Login</button>
Playwright:
await page.getByTestId('submit-login').click();
Use this when the application team provides stable testing attributes, particularly when visible text or layout changes frequently.
F. CSS locator
Uses CSS selectors to find elements by attributes, IDs, classes, and relationships.
await page.locator('#username').fill('tester');
await page.locator('.login-button').click();await page.locator('input[name="email"]').fill('qa@example.com');
  • #username selects an element with the ID username.
  • .login-button selects an element with the class login-button.
  • input[name="email"] selects an input whose name attribute is email.
G. XPath locator
X Path is a language for locating elements in an HTML or XML document.
await page.locator('//input[@name="username"]').fill('tester');
await page.locator('//button[normalize-space()="Login"]').click();
XPath can help with complex relationships, but long XPath expressions that depend on page structure are often fragile. Prefer role-based locators or stable attributes when possible.

1.3 Handling multiple matching elements

Suppose a page contains three buttons named “Edit.” A locator that matches all three cannot be clicked as though it identified one unique target.
const editButtons = page.getByRole('button', { name: 'Edit' });
await expect(editButtons).toHaveCount(3);await editButtons.nth(1).click();
nth(1) selects the second matching element because indexing starts at zero.
You can also use:
await editButtons.first().click();await editButtons.last().click();
Prefer narrowing the locator to the correct row or section instead of relying on position when possible.

1.4 Handling dynamic elements

A dynamic element may appear after an API response, loading animation, or user action.
const successMessage = page.getByText('Order placed successfully');
await expect(successMessage).toBeVisible();
Playwright retries the assertion until it passes or times out. This is more reliable than immediately checking an element that might not have appeared yet.

1.5 Best practices for locators

  1. Prefer getByRole() and getByLabel() when appropriate.
  2. Use stable test IDs when the application supports them.
  3. Avoid selectors that depend on deeply nested HTML structures.
  4. Ensure the locator identifies the intended element uniquely.
  5. Avoid using nth() to hide ambiguity when a more specific locator is available.
  6. Use assertions to verify that the element is in the expected state.
Interview questions
  • What is a locator in Playwright?
  • What is the difference between CSS and XPath?
  • What is the difference between getByRole() and getByText()?
  • How do you handle duplicate buttons?
  • How do you locate a dynamic web element?

2. Assertions — expect() and Web-First Assertions

2.1 What is an assertion?

An assertion checks whether the actual result matches the expected result.
For example, after a successful login, the tester expects the dashboard to appear. An assertion verifies that expectation.
Without assertions, an automation script might click buttons successfully but never confirm whether the application behaved correctly.

2.2 Basic syntax

import { test, expect } from '@playwright/test';test('Verify page title', async ({ page }) => {
await page.goto('https://example.com'); await expect(page).toHaveTitle(/Example/);});
Explanation:
  • test() defines a test case.
  • page.goto() opens the website.
  • expect() starts an assertion.
  • toHaveTitle() verifies the page title.
  • /Example/ is a regular expression that matches a title containing “Example.”

2.3 Common Playwright assertions

Assertion
Purpose
toHaveTitle()
Verify the page title
toHaveURL()
Verify the current URL
toBeVisible()
Verify an element is visible
toBeHidden()
Verify an element is hidden or absent
toBeEnabled()
Verify an element is enabled
toBeDisabled()
Verify an element is disabled
toHaveText()
Verify the expected text
toContainText()
Verify that text contains a particular value
toHaveValue()
Verify an input’s value
toBeChecked()
Verify a checkbox or radio button is checked
toHaveCount()
Verify the number of matching elements
toHaveAttribute()
Verify an attribute value
Example:
await expect(page).toHaveURL(/dashboard/);
await expect(page.getByRole('heading', { name: 'Dashboard' })).toBeVisible();await expect(page.getByRole('button', { name: 'Logout' })).toBeEnabled();

2.4 What are web-first assertions?

Web-first assertions automatically retry the check until the expected condition becomes true or the assertion times out.
await page.getByRole('button', { name: 'Login' }).click();
await expect( page.getByText('Login successful')).toBeVisible();
If the message takes a short time to appear, Playwright continues checking rather than failing immediately.
Compare this with a one-time snapshot:
const visible = await page.getByText('Login successful').isVisible();expect(visible).toBe(true);
The second example checks visibility at that moment. The boolean assertion does not automatically retry the visibility check.

2.5 Hard assertions vs. soft assertions

Hard assertion: Stops the test at the failed assertion.
await expect(page.getByRole('heading')).toHaveText('Dashboard');
Soft assertion: Records the failure but allows the test to continue.
await expect.soft(page.getByTestId('profile-name')).toHaveText('Tester');
await expect.soft(page.getByTestId('account-status')).toHaveText('Active');
Soft assertions are useful when you want to collect multiple independent validation failures in one test. They should not be used to continue blindly into steps that depend on a failed result.

2.6 Real-time scenario

Requirement: Verify that an invalid login displays an error message.
test('Invalid login shows error', async ({ page }) => {
await page.goto('https://example.com/login'); await page.getByLabel('Email').fill('wrong@example.com');
await page.getByLabel('Password').fill('WrongPassword');
await page.getByRole('button', { name: 'Login' }).click();
await expect( page.getByRole('alert') ).toContainText('Invalid credentials');});
This test checks application behavior, not merely whether the Login button can be clicked.
Interview questions
  • What is an assertion?
  • What is the difference between hard and soft assertions?
  • What is a web-first assertion?
  • What happens when an assertion fails?
  • What is the difference between toHaveText() and toContainText()?

3. Auto-Waiting — Synchronization and Flaky Tests

3.1 What is auto-waiting?

Auto-waiting is a Playwright feature that waits for an element to become ready for an action before performing that action.
Consider a login page:
  1. The tester clicks Login.
  2. The application sends a request to the server.
  3. The server validates the credentials.
  4. The dashboard appears after the response.
The dashboard does not appear instantly. If the script checks too early, it may fail even though the application is working correctly.
Playwright helps by automatically waiting for actionability conditions and retrying web-first assertions.

3.2 What are actionability checks?

Before clicking an element, Playwright checks relevant conditions, including whether the element is:
  • Visible: It is visible on the page.
  • Stable: Its position and dimensions have stopped changing.
  • Receives events: Another element is not intercepting the click.
  • Enabled: It is not disabled.
The precise checks depend on the action. For example, filling an input does not require exactly the same checks as clicking a button.
Example:
await page.getByRole('button', { name: 'Submit' }).click();
Playwright waits for the button to be ready for the click, subject to the configured timeout.

3.3 Types of waiting

A. Automatic waiting
await page.getByLabel('Email').fill('qa@example.com');
await page.getByRole('button', { name: 'Submit' }).click();
Playwright automatically waits for the actions’ required conditions.
B. Assertion-based waiting
await expect(page.getByText('Payment successful')).toBeVisible();
The assertion retries until the expected state is reached or the timeout expires.
C. Explicit waiting
Sometimes a test must wait for a specific event or condition.
await page.getByTestId('loading-spinner').waitFor({  state: 'hidden'});
For API activity, use a response wait when the response itself is relevant:
const responsePromise = page.waitForResponse(response =>  response.url().includes('/api/orders') &&  response.request().method() === 'POST');await page.getByRole('button', { name: 'Place Order' }).click();const response = await responsePromise;expect(response.ok()).toBeTruthy();
Register the response wait before the action that triggers it, so the response is not missed.

3.4 Why should we avoid fixed waits?

A fixed wait pauses for a predetermined amount of time regardless of whether the application is ready.
await page.waitForTimeout(5000);
Problems:
  • It wastes time if the page loads in one second.
  • It may still fail if the page needs more than five seconds.
  • It can make tests slower and less reliable.
Prefer waiting for the specific state or event you need.

3.5 What is a flaky test?

A flaky test sometimes passes and sometimes fails without a relevant change to the application or test.
Common causes:
  • Fixed waits and timing assumptions
  • Unstable locators
  • Shared test data
  • Tests that depend on execution order
  • Network delays or unstable environments
  • Animations and overlays
  • Incomplete cleanup between tests

3.6 How to fix flaky tests

  1. Replace fixed waits with condition-based waits.
  2. Use stable and unique locators.
  3. Use web-first assertions.
  4. Give each test independent data where possible.
  5. Avoid dependencies between tests.
  6. Investigate traces, screenshots, and error logs.
  7. Use retries as a diagnostic aid, not as a substitute for fixing the cause.
Interview questions
  • What is auto-waiting in Playwright?
  • What is the difference between auto-waiting and an explicit wait?
  • Why is waitForTimeout() discouraged?
  • What is a flaky test, and how do you fix it?
  • How does Playwright handle an element covered by a loader?

4. Browser Actions — Forms, Dropdowns, Tabs and Frames

Browser actions are operations performed on webpage elements or browser pages. A tester must know how to enter data, interact with controls, handle new windows, and work with embedded pages.

4.1 Text boxes and form fields

await page.getByLabel('Full Name').fill('Rahul Sharma');
await page.getByLabel('Email').fill('rahul@example.com');
await page.getByLabel('Password').fill('Test@123');
fill() clears the existing input value and fills in the new value.
To type character by character:
await page.getByLabel('Search').pressSequentially('Playwright');
This can be useful for testing applications that respond to individual keystrokes.

4.2 Buttons and keyboard actions

await page.getByRole('button', { name: 'Submit' }).click();
await page.getByLabel('Search').press('Enter');await page.getByLabel('Email').press('Control+A');
Use the appropriate keyboard modifier for the operating system when required.

4.3 Checkboxes and radio buttons

await page.getByRole('checkbox', { name: 'Accept Terms' }).check();
await page.getByRole('radio', { name: 'Credit Card' }).check();await expect( page.getByRole('checkbox', { name: 'Accept Terms' })).toBeChecked();
Use check() and uncheck() when you want to set a known state. They are generally safer than blindly clicking a checkbox, which might toggle it in the wrong direction.

4.4 Dropdowns

For a native HTML select element:
await page.getByLabel('Country').selectOption({ label: 'India' });
You can also select by value:
await page.getByLabel('Country').selectOption('IN');
The value must match an actual option value.
Important: Many modern websites use custom dropdowns built from div or button elements. For these, click the dropdown and then select the relevant option using a locator. selectOption() is intended for native select controls.

4.5 File uploads

await page.getByLabel('Upload Resume').setInputFiles('resume.pdf');
For file inputs that are hidden behind an upload button, use the appropriate file chooser event:
const fileChooserPromise = page.waitForEvent('filechooser');
await page.getByRole('button', { name: 'Upload Resume' }).click();const fileChooser = await fileChooserPromise;await fileChooser.setFiles('resume.pdf');

4.6 Multiple tabs and popups

Suppose clicking a link opens a new tab.
const newPagePromise = page.context().waitForEvent('page');
await page.getByRole('link', { name: 'Open Report' }).click();const newPage = await newPagePromise;await newPage.waitForLoadState();await expect(newPage).toHaveTitle(/Report/);
For a popup directly associated with a page, page.waitForEvent('popup') is another suitable approach.

4.7 Handling iframes

An iframe embeds another webpage or document inside the current page.
For example, a payment provider may display a card-number input inside an iframe.
const paymentFrame = page.frameLocator('#payment-frame');
await paymentFrame.getByLabel('Card Number').fill('4111111111111111');
The selector #payment-frame must identify the actual iframe element, and the field must be accessible within that frame.

4.8 Real-time scenario: Registration form

A typical automated registration test should:
  1. Open the registration page.
  2. Fill in name and email.
  3. Enter a password.
  4. Select the country.
  5. Accept terms and conditions.
  6. Submit the form.
  7. Verify the success message.
  8. Confirm the correct next page or account state.
The final validation is important: clicking Submit successfully does not prove that registration succeeded.
Interview questions
  • What is the difference between fill() and pressSequentially()?
  • How do you select an option from a dropdown?
  • How do you handle a new tab?
  • What is an iframe, and how do you interact with it?
  • How do you upload a file using Playwright?

5. Playwright Test Runner — Hooks, Grouping, Retries and Execution

5.1 What is a test runner?

A test runner is the component that discovers, organizes, executes, and reports automated tests.
Playwright Test provides:
  • Test discovery
  • Assertions
  • Fixtures
  • Hooks
  • Parallel execution
  • Retries
  • Timeouts
  • Reporting

5.2 Writing a test

import { test, expect } from '@playwright/test';test('Verify homepage title',
async ({ page }) => { await page.goto('https://example.com');
await expect(page).toHaveTitle(/Example/);});
Save the file with a name such as homepage.spec.ts.

5.3 Grouping tests

Use test.describe() to group related tests.
test.describe('Login functionality', () => {  test('Valid login', async ({ page }) => {   
// Test valid login }); test('Invalid login', async ({ page }) => { // Test invalid login });});
Grouping makes a test suite easier to read and organize.

5.4 What are hooks?

Hooks execute setup or cleanup logic at particular points in the test lifecycle.
Hook
When it runs
beforeEach()
Before each test in its scope
afterEach()
After each test in its scope
beforeAll()
Once before tests in its scope
afterAll()
Once after tests in its scope
Example:
test.describe('Login tests', () => { test.beforeEach(async ({ page }) => {
await page.goto('https://example.com/login'); }); test('Login page is visible', async ({ page }) => {
await expect( page.getByRole('heading', { name: 'Login' }) ).toBeVisible(); });});
The page is opened before each test. This avoids repeating the navigation step.
Important: beforeAll() and afterAll() are useful for shared setup and cleanup, but they should not be used carelessly to share mutable browser state across tests. Each test should remain as independent as practical.

5.5 Running tests

npx playwright test
1. Run a specific file:
npx playwright test tests/login.spec.ts
2.  Run tests with a matching title:
npx playwright test -g "Invalid login"
3. Run with a visible browser:
npx playwright test --headed
4. Run a particular browser project:
npx playwright test --project=chromium
The project name must exist in your configuration.

5.6 Retries and timeouts

Retries allow a failed test to run again.
// playwright.config.tsimport { defineConfig } 
from '@playwright/test';export default defineConfig({ retries: 1, timeout: 30000});
Here, a test may receive one retry after its initial attempt, and the configured test timeout is 30 seconds.
Retries can help identify intermittent failures, but a test that only passes after a retry still deserves investigation.

5.7 Skipping and focusing tests

test.skip('Feature not ready', async ({ page }) => {  // Test implementation});
You can temporarily focus on a test during local debugging with test.only(), but remove it before committing code so the full suite can run.
Interview questions
  • What is Playwright Test?
  • What is the difference between beforeEach() and beforeAll()?
  • How do you run one test file?
  • What are retries?
  • How do you skip a test?
  • Why should tests be independent?

6. Page Object Model (POM) — Page Classes and Reusable Methods

6.1 What is POM?

Page Object Model is a design pattern that separates page-specific locators and actions from test cases.
Without POM, the same login locators may be repeated in many tests. If the login page changes, every test might require an update.
With POM, the login-page logic is kept in one class and reused across tests.

6.2 Example folder structure

playwright-project/├── pages/│   └── LoginPage.ts├── tests/│  
└── login.spec.ts├── playwright.config.ts└── package.json

6.3 Create a page class

pages/LoginPage.ts
import { Page, Locator } from '@playwright/test';export class LoginPage {  
readonly page: Page; readonly email: Locator; readonly password: Locator; readonly loginButton: Locator; readonly errorMessage: Locator; constructor(page: Page) { this.page = page; this.email = page.getByLabel('Email'); this.password = page.getByLabel('Password'); this.loginButton = page.getByRole('button', { name: 'Login' }); this.errorMessage = page.getByRole('alert'); } async open() { await this.page.goto('/login'); } async login(email: string, password: string) { await this.email.fill(email); await this.password.fill(password); await this.loginButton.click(); }}
Explanation:
  • class LoginPage defines the page object.
  • constructor(page) receives the Playwright page.
  • The locators identify elements on the page.
  • open() navigates to the login page.
  • login() performs the login actions with supplied data.
The relative URL /login assumes that baseURL is configured.

6.4 Use the page object in a test

tests/login.spec.ts
import { test, expect } from '@playwright/test';import { LoginPage }
from '../pages/LoginPage';test('Invalid login displays an error',
async ({ page }) => { const loginPage = new LoginPage(page); await loginPage.open();
await loginPage.login('wrong@example.com', 'WrongPassword');
await expect(loginPage.errorMessage).toContainText('Invalid credentials');});
The test focuses on the scenario and expected result, while the page class handles the login interactions.

6.5 Advantages of POM

  • Reduces duplicated code.
  • Makes maintenance easier.
  • Improves readability.
  • Encourages reusable page methods.
  • Keeps test scenarios separate from UI implementation details.

6.6 Common POM mistakes

  • Putting all test assertions inside page classes without a clear reason.
  • Creating one enormous page class for the whole application.
  • Using unstable locators.
  • Building unnecessary layers for a small project.
  • Sharing mutable state between tests.
Interview questions
  • What is Page Object Model?
  • Why do we use POM?
  • What is the difference between a page object and a test file?
  • How does POM improve maintainability?
  • How would you design a reusable login page?
7. Fixtures — page, context, and Custom Fixtures

7.1 What is a fixture?

A fixture provides the resources and setup that a test needs to run.
For example, before testing a login page, the test needs a browser page. Playwright Test supplies this through its built-in page fixture.
test('Verify login page', async ({ page }) => { await page.goto('https://example.com/login');
await expect( page.getByRole('heading', { name: 'Login' }) ).toBeVisible();});
Here, { page } receives the fixture from Playwright Test. You do not need to create a browser manually for this ordinary test.

7.2 Important built-in fixtures

Fixture
Purpose
page
A browser tab or page for UI testing
context
An isolated browser session with its own cookies and storage
browser
The browser instance
request
An API request context for sending HTTP requests
Example: Two pages in the same browser context can share cookies and session storage rules associated with that context, whereas separate contexts provide isolation.
test('Open two pages', async ({ context }) => { const page1 = await context.newPage();
const page2 = await context.newPage(); await page1.goto('https://example.com');
await page2.goto('https://example.org'); await expect(page1).toHaveTitle(/Example/);});

7.3 What is a custom fixture?

A custom fixture is a fixture you define yourself to provide reusable setup or resources beyond the built-in fixtures.
For example, suppose many tests need a LoginPage page object. Instead of constructing it in every test, you can create a fixture.
fixtures/test-fixtures.ts
import { test as base, expect } from '@playwright/test';import { LoginPage } from '../pages/LoginPage';type MyFixtures = {  loginPage: LoginPage;};export const test = base.extend<MyFixtures>({  loginPage: async ({ page }, use) => {    const loginPage = new LoginPage(page);    await use(loginPage);  }});export { expect };
Now the test can use the custom fixture directly.
import { test, expect } from '../fixtures/test-fixtures';test('Verify login page', async ({ loginPage }) => {  await loginPage.open();  await expect(loginPage.loginButton).toBeVisible();});
Playwright initializes the fixture when it is needed, makes it available to the test, and completes its fixture lifecycle afterward.

7.4 Fixtures vs. hooks

Fixtures
Hooks
Provide resources and setup to tests
Run logic at defined lifecycle points
Can supply values such as page or loginPage
Commonly perform setup or cleanup
Can manage resource creation and teardown
Do not themselves provide a typed resource to the test
Can be composed and reused
Are organized within test scopes

7.5 Why are fixtures important?

Fixtures help teams build consistent test environments, avoid repeated setup code, manage cleanup, and isolate test resources.
Interview questions
  • What is a fixture in Playwright?
  • What is the difference between page and context?
  • What is a custom fixture?
  • How is a fixture different from a hook?
  • Why is test isolation important?

8. API Testing — Requests, Responses and Status Codes

8.1 What is API testing?

API testing verifies communication between applications or services without necessarily using the user interface.
For example, when a user logs in, the frontend may send a request to an authentication API. The API validates the credentials and returns a response.
API testing verifies whether that interaction behaves as expected.
A typical API test checks:
  1. Request method and endpoint.
  2. Headers and authentication.
  3. Request body or query parameters.
  4. HTTP status code.
  5. Response body and data.
  6. Error handling and validation.

8.2 HTTP methods

Method
Common purpose
GET
Retrieve data
POST
Create a resource or perform an operation
PUT
Replace a resource
PATCH
Partially update a resource
DELETE
Delete a resource
The actual behavior depends on the API contract.

8.3 Common status codes

Status
Meaning
200 OK
Request succeeded
201 Created
Resource was created
204 No Content
Request succeeded without a response body
400 Bad Request
Request is invalid
401 Unauthorized
Authentication is missing or invalid
403 Forbidden
Access is not permitted
404 Not Found
Resource was not found
409 Conflict
Request conflicts with the current resource state
500 Internal Server Error
Server encountered an unexpected error
A 401 typically indicates an authentication issue, whereas a 403 indicates that the request is not permitted. Exact API conventions can vary.

8.4 Writing an API test

The following example assumes an API that returns a user with an id and name field.
import { test, expect } from '@playwright/test';test('Verify user API response', async ({ request }) => {  const response = await request.get(    'https://api.example.com/users/1'  );  expect(response.status()).toBe(200);  const body = await response.json();  expect(body.id).toBe(1);  expect(body.name).toBeTruthy();});
This test checks both the HTTP status and the response data. The endpoint and expected fields must match the actual API contract.

8.5 Testing a POST request

test('Create a user through API', async ({ request }) => {  const response = await request.post(    'https://api.example.com/users',    {      data: {        name: 'QA Tester',        email: 'qa@example.com'      }    }  );  expect(response.status()).toBe(201);  const body = await response.json();  expect(body.name).toBe('QA Tester');});
This assumes the API accepts the supplied fields and returns 201. Use a test environment and valid test data; public example endpoints may behave differently or not persist data.

8.6 API testing vs. UI testing

API testing
UI testing
Tests endpoints and responses
Tests what the user sees and does
Usually faster for business rules and data validation
Validates user workflows and visual interactions
Does not require rendering the UI
Requires browser interaction
Can validate status, headers, and response data
Can validate elements, text, navigation, and controls

8.7 Combining API and UI testing

A practical workflow is to create test data using an API, then use the UI to verify that the data appears correctly.
For example:
  1. Create a test user through an API.
  2. Open the website.
  3. Log in as that user.
  4. Open the profile page.
  5. Verify the profile details.
  6. Clean up the test data if the environment allows it.
This reduces dependence on repetitive UI setup and helps isolate failures.
Interview questions
  • What is API testing?
  • What is the difference between GET and POST?
  • What is the difference between 401 and 403?
  • How do you validate a JSON response?
  • How do you combine API and UI testing in Playwright?
9. Debugging — Trace Viewer, Screenshots and Reports

9.1 What is debugging?

Debugging is the process of investigating why a test failed and identifying the underlying cause.
A test might fail because:
  • The application has a defect.
  • A locator does not match the correct element.
  • An API response is incorrect.
  • A test environment is unavailable.
  • A timeout is too short.
  • The test has incorrect assumptions.
A failed automation test does not automatically mean that the application has a bug. The cause must be investigated.

9.2 Screenshots

Screenshots capture the browser page at a particular moment.
await page.screenshot({  path: 'screenshots/login-page.png',  fullPage: true});
Use screenshots to inspect visible content, layout, error messages, and page state.
To capture a screenshot when a test fails, configure Playwright to capture one on failure or use an appropriate test hook.

9.3 HTML reports

The HTML report shows test outcomes, execution details, errors, and related artifacts when available.
Run the tests:
npx playwright test
Open the report:
npx playwright show-report
You can configure the HTML reporter in playwright.config.ts:
reporter: 'html'
Reports help testers identify failed tests and review the associated error details.

9.4 Trace Viewer

Trace Viewer is one of the most useful Playwright debugging tools.
A trace can contain:
  • Actions performed during the test
  • Snapshots of the page
  • Locator information
  • Console messages
  • Network activity
  • Errors and timing information
  • Screenshots, depending on the trace contents
Enable tracing on failures:
// playwright.config.tsimport { defineConfig }
from '@playwright/test';export default defineConfig({ use: { trace: 'retain-on-failure' }});
Run your tests, then open a trace ZIP file:
npx playwright show-trace path/to/trace.zip
The trace must have been generated by a test run. You cannot open an arbitrary ZIP file and expect it to contain a valid trace.

9.5 Playwright Inspector and UI Mode

Playwright Inspector helps you step through a test and inspect locators. You can pause a test using:
await page.pause();
Run the test in debug mode:
npx playwright test --debug
UI Mode provides an interactive way to explore test runs, inspect steps, and debug failures.
npx playwright test --ui
These tools are particularly helpful when a test fails at a particular step or a locator does not identify the intended element.

9.6 Example debugging workflow

Suppose a checkout test fails when clicking “Place Order.”
  1. Open the failed test report.
  2. Read the error message and identify the failed step.
  3. Inspect the screenshot and trace.
  4. Check whether the button was visible, enabled, and unobstructed.
  5. Verify whether a required field or validation message blocked submission.
  6. Check the relevant network response.
  7. Fix the application defect or test code based on the evidence.
  8. Rerun the test and verify that the fix works.
Interview questions
  • What is Trace Viewer?
  • How do you debug a failed Playwright test?
  • What is the difference between a screenshot and a trace?
  • How do you generate an HTML report?
  • What is the purpose of Playwright Inspector?
  • How do you investigate flaky tests?

10. Framework — Configuration, Git and CI/CD

10.1 What is an automation framework?

An automation framework is an organized structure of test scripts, reusable functions, configuration, test data, reporting, and execution processes.
A good framework helps a QA team maintain tests, execute them consistently, investigate failures, and run regression tests efficiently.
A typical Playwright framework might look like this:
playwright-project/├── tests/│   ├── login.spec.ts│   └── checkout.spec.ts├── pages/│   ├── LoginPage.ts│   └── CheckoutPage.ts├── fixtures/│   └── test-fixtures.ts├── test-data/│   └── users.json├── playwright.config.ts├── package.json└── .gitignore
The folders are an organizational choice, not a mandatory Playwright structure.

10.2 Playwright configuration

The playwright.config.ts file controls how tests run.
Example:
import { defineConfig, devices } from '@playwright/test';
export default defineConfig({ testDir: './tests',
timeout: 30000, retries: 1, workers: 4, reporter: 'html', use:
{
baseURL: 'https://example.com', headless: true, screenshot: 'only-on-failure', trace: 'retain-on-failure'
},
projects: [ { name: 'chromium', use: { ...devices['Desktop Chrome'] } } ]});
What these settings mean:
Setting
Explanation
testDir
Folder containing tests
timeout
Maximum duration allowed for an individual test
retries
Number of retries after a failed attempt
workers
Number of parallel worker processes, subject to available resources
reporter
How test results are reported
baseURL
Base address for relative navigation
headless
Whether the browser runs without a visible window
screenshot
Screenshot capture policy
trace
Trace capture policy
projects
Browser or configuration combinations in which tests run
For example, when baseURL is configured, you can write:
await page.goto('/login');
Instead of repeating the full website URL.

10.3 Git and GitHub

Git is a version-control system that tracks changes in your automation code.
GitHub is a platform for storing repositories and collaborating with other developers and testers.
Common Git commands:
git initgit statusgit add .git commit -m "Add Playwright login tests"git pushgit pull
Typical QA workflow:
  1. Create or clone the project repository.
  2. Create a feature branch.
  3. Write or update test scripts.
  4. Run tests locally.
  5. Commit the changes.
  6. Push the branch to GitHub.
  7. Create a pull request.
  8. Review and merge approved changes.

10.4 What is CI/CD?

CI/CD stands for Continuous Integration and Continuous Delivery or Deployment.
In test automation, a CI pipeline can automatically install dependencies, run Playwright tests, and publish reports whenever code is pushed or a pull request is created.
For example, a team may run:
  • Smoke tests after each deployment.
  • Regression tests on pull requests.
  • Full cross-browser tests nightly.
  • Tests against a staging environment before release.

10.5 Example: GitHub Actions workflow

Create the file .github/workflows/playwright.yml:
name: Playwright Testson:  push: 
pull_request:jobs: test: timeout-minutes: 30 runs-on: ubuntu-latest steps: - uses: actions/checkout@v4 - uses: actions/setup-node@v4 with: node-version:
22 - name: Install dependencies run: npm ci - name: Install Playwright browsers run: npx playwright install --with-deps - name: Run tests run: npx playwright test - name: Upload report if: ${{ !cancelled() }} uses: actions/upload-artifact@v4 with: name: playwright-report path: playwright-report/ retention-days: 7
This example assumes the repository has a valid package-lock.json and a package.json containing the required Playwright dependencies and test scripts. A production workflow should also consider secrets, environment configuration, and how failed test artifacts are retained.

10.6 How does automation run in CI/CD?

1. Code is pushed
Developer updates the repository.
2. Pipeline starts
Dependencies and browsers are installed.
3. Tests execute
Playwright runs the selected test suite.
4. Results are published
The team reviews test status, reports, and failure artifacts.

10.7 Framework best practices

  • Use meaningful test names.
  • Keep test cases independent.
  • Use reusable page objects where appropriate.
  • Store environment-specific configuration separately.
  • Keep passwords and API tokens out of source code.
  • Use stable locators and web-first assertions.
  • Capture traces and screenshots for useful failure diagnosis.
  • Run tests in parallel only when the environment and test data support it.
  • Review flaky tests rather than repeatedly ignoring failures.
  • Maintain a clear distinction between test failures and application defects.

Leave a Comment

Your email address will not be published. Required fields are marked *

Scroll to Top