Playwright Complete Revision

Course-based revision hub • controls → automation → framework → reporting

Playwright • Final revision

From “I know commands” to “I know how to test.”

This page consolidates the Playwright material you uploaded and the practical patterns you worked through: locators, assertions, controls, dynamic elements, frames, tabs, files, hooks, groups, execution, reports, flaky tests, retries, and your OrangeHRM project.

LOCATE → ACTION → VERIFYWeb-first assertionstest.describe() HooksFramesPages / tabsUpload / download ReportsFlaky tests

01. Core mental model

The foundation behind almost every UI test

LOCATE → ACTION → VERIFY

LOCATE→ACTION→VERIFY

Locator finds the element. Action tells the browser what to do. Assertion proves the expected result.

What does await mean?

Browser operations take time. await tells JavaScript to wait for the operation before continuing.

Think: “Do this browser operation, finish it, then move to the next line.”

Action vs retrieve vs assert

CategoryExamplesMeaning
ACTIONfill(), click(), check(), uncheck(), selectOption()DO IT
RETRIEVE / ASKinputValue(), isChecked(), count(), textContent(), getAttribute()TELL ME WHAT HAPPENED
ASSERTtoHaveValue(), toBeChecked(), toHaveCount(), toBeVisible(), toHaveAttribute()PROVE IT IS CORRECT

02. Locators

How Playwright identifies the element you mean

User-facing locators

  • getByRole() — by role/name
  • getByLabel() — form control by label
  • getByPlaceholder() — placeholder
  • getByAltText() — image alt text
  • getByTitle() — title
  • getByTestId() — test-specific attribute
  • getByText() — visible text

CSS / XPath / locator()

Use locator() when a stable semantic locator is unavailable or when CSS/XPath is appropriate for the actual DOM.

page.locator('#username')
page.locator('.product-image-wrapper')
page.locator('input[type="checkbox"]')
CSS classes need a dot for each class: .class1.class2, not .class1 class2.

Position / filtering

locator.first()
locator.last()
locator.nth(2)
locator.filter({ hasText: 'Blue Top' })
locator.filter({ visible: true })
await locator.all()

nth(2) means the third matching element because indexing starts at 0.

Locator discipline

  • Prefer meaningful, stable locators.
  • Do not use nth() blindly.
  • Inspect actual HTML before copying a locator.
  • A locator identifies an element; it does not perform the action.

03. Assertions

Prove the application behaved as expected
AssertionUse
toBeVisible()Element is visible
toBeEnabled()Element is enabled
toBeEditable()Textbox can be edited
toHaveValue()Input / single-select contains expected value
toBeChecked()Checkbox/radio is selected
toHaveCount(n)Exactly n DOM elements match
toHaveLength(n)JavaScript array has n items
toHaveAttribute()Attribute has expected value
toHaveText()Element contains expected text
toHaveURL()Page is at expected URL
toHaveTitle()Page has expected title

Classic confusion

// Retrieve
const value = await email.inputValue();

// Assert
await expect(email).toHaveValue('test@gmail.com');

Count confusion

// Retrieve a number
const count = await items.count();

// Assert exact DOM count
await expect(items).toHaveCount(5);

// Array length
const texts = await items.allTextContents();
await expect(texts).toHaveLength(5);

04. UI controls

Identify the control first, then choose the user action
ControlUser actionMain Playwright actionVerification
TextboxType / editfill() / type()toHaveValue()
CheckboxChoose zero / one / manycheck() / uncheck()toBeChecked()
RadioChoose onecheck()toBeChecked()
Native dropdownSelect optionselectOption()toHaveValue()
Auto-suggestionType → find → clickfill() + click matching suggestionResult / state / text

Textbox

const email = page.getByRole('textbox', { name: 'Email Address' });

await expect(email).toBeVisible();
await expect(email).toBeEnabled();
await expect(email).toBeEditable();

await email.fill('test@gmail.com');
await expect(email).toHaveValue('test@gmail.com');

const value = await email.inputValue();

Checkbox

const monday = page.getByLabel('Monday');

await monday.check();
await expect(monday).toBeChecked();

const selected = await monday.isChecked();

await monday.uncheck();

Radio

const male = page.getByLabel('Male');

await male.check();
await expect(male).toBeChecked();

Memory trick: Radio = ONE.

Native dropdown

const country = page.locator('#country');

await country.selectOption('india');
await expect(country).toHaveValue('india');

await country.selectOption({ label: 'Germany' });
await country.selectOption({ index: 2 });

Multi-select dropdown

const colors = page.locator('#colors');

await colors.selectOption(['yellow', 'white']);

const selectedOptions = page.locator('#colors option:checked');
const selectedTexts = await selectedOptions.allTextContents();
const selectedValues = await selectedOptions.evaluateAll(
  options => options.map(option => option.value)
);

await expect(selectedOptions).toHaveCount(2);
await expect(selectedValues).toEqual(['yellow', 'white']);
For multiple selected options, think in terms of an array. Retrieve selected values/text and compare the array rather than treating the control like a single value.

05. Dynamic UI

Elements that appear/change based on interaction

Auto-suggestion dropdown

TYPE→SUGGESTIONS→READ→MATCH→CLICK
await page.locator('#search').fill('java');

const suggestions = page.locator('#suggestions li');
const items = await suggestions.all();

for (let item of items) {
  const text = await item.textContent();

  if (text && text.trim() === 'Java Selenium') {
    await item.click();
    break;
  }
}
Do not use selectOption() unless the control is a native <select>.

Dynamic button / timer

Your practice page included a dynamic button/timer area. The key lesson is to interact with the state the application actually presents and verify the resulting state rather than relying on arbitrary delays.

Alerts & popups

The practice page included simple, confirmation, and prompt-style modal alerts, plus popup-related exercises. The core idea is to identify whether you are dealing with a page element/modal or a separate browser/page event and automate the correct object.

Tabs inside a page

For UI tabs, locate the tab buttons and verify the associated content/state. This is different from browser tabs/pages, which use the Page objects discussed later.

06. Mouse & keyboard

Simulate common user interactions

Mouse actions

await locator.hover();
await locator.dblclick();
await locator.click({ button: 'right' });
await locator.scrollIntoViewIfNeeded();

Keyboard actions

await locator.click();
await page.keyboard.type('Bruce Wayne');
await page.keyboard.press('Tab');
await page.keyboard.press('Escape');

Example workflow: click a field → type → Tab → type next field → Tab/Escape → assert values.

Scrolling

const heading = page.getByText('Range Slider').first();

await heading.scrollIntoViewIfNeeded();
await expect(heading).toBeVisible();

await page.locator('body').press('Home');

07. Date pickers

Two important patterns from your practice

Calendar navigation

Open the date picker, inspect the displayed month/year, navigate with the previous/next controls, then select the target date.

const datePicker = page.locator('#datepicker');
await datePicker.click();

console.log(await page.locator('.ui-datepicker-year').innerText());
console.log(await page.locator('.ui-datepicker-month').innerText());

// navigate until the required month/year
// then click the required date

Native select inside a date picker

const yearDropdown = page.locator('.ui-datepicker-year');
await yearDropdown.selectOption({ label: '2025' });

const monthDropdown = page.locator('.ui-datepicker-month');
// The actual option label on your practice page was 'Nov'
await monthDropdown.selectOption({ label: 'Nov' });
One of your failures came from asking for November when the actual option label was Nov.

Important debugging lesson

A selector can be correct while the requested option is wrong. Always inspect the actual DOM/options. Also be careful with loop conditions when waiting for a combined year + month state.

08. Drag, drop & range slider

Interaction with non-text controls

Drag and drop

const source = page.locator('#drag-src');
const target = page.locator('#drag-tgt');

await source.dragTo(target);

const afterDropText = await target.innerText();
console.log(afterDropText);
Do not compare a string to a Locator. toHaveText() expects a string or regular expression, not another Locator.

Range slider

const volumeSlider = page.locator('#volumeSlider');

await volumeSlider.evaluate((slider) => {
  slider.value = 80;
});

This is the simple DOM-property approach you practiced. Your class material did not add event dispatching to this exercise.

Reset-state verification

For drag/drop, capture the actual text value if you need to compare later:

const initialText = await source.innerText();

// after reset:
await expect(target).toHaveText('Drop Here\nRelease to drop');

Conceptually: retrieve the text first; then assert using the retrieved string.

09. Frames / iframes

A frame is a separate DOM boundary

Why normal locators fail

page.locator() searches the main page DOM. An element inside an iframe belongs to that frame's DOM.

Main page DOM≠iframe DOM

Frame locator

const frame = page.frameLocator('#loginFrame');

await frame
  .locator('#username')
  .fill('admin');

Access a frame by URL

const frame1 = await page.frame({
  url: 'https://ui.vision/demo/webtest/frames/frame_1'
});

await frame1
  .getByRole('textbox')
  .fill('STAD Solution');

Nested frames

const frame3 = page.frame({ url: /frame_3/ });

await frame3
  .locator('input[name="mytext3"]')
  .fill('welcome');

const childFrames = frame3.childFrames();

const radioButton = childFrames[0]
  .locator('//[@id="i5"]/div[3]/div');

await radioButton.check();
await expect(radioButton).toBeChecked();

Memory model: main page → frame → child frame → element.

10. Multiple tabs / browser pages

Separate pages in a browser context

Manual page creation

const browser = await chromium.launch();

const context = await browser.newContext();

const page1 = await context.newPage();
const page2 = await context.newPage();

const allPages = context.pages();
console.log(allPages.length);

Wait for a newly opened page

const newPagePromise = context.waitForEvent('page');

await page.getByText('New Tab').click();

const newPage = await newPagePromise;
await newPage.waitForLoadState();

Multiple pages at the same time

const [page1, page2, page3] = await Promise.all([
  context.waitForEvent('page'),
  page.getByText('OrangeHRM, Inc').click(),
  page.getByText('forget password').click()
]);

Key idea: set up the event listener before the action that opens the page.

11. File upload & download

A very common real-world automation task

Single file upload

await page
  .locator('#file-upload')
  .setInputFiles('testdata/sample.pdf');

The file path is relative to the project root in the exercise.

Multiple file upload

await page
  .locator('#file-upload')
  .setInputFiles([
    'testdata/file1.pdf',
    'testdata/file2.pdf'
  ]);

The same setInputFiles() concept handles multiple files when the input supports it.

Download — the order matters

Listen→Click→Await download→Save / inspect
const downloadPromise = page.waitForEvent('download');

await page.getByText('Download').click();

const download = await downloadPromise;

console.log(download.suggestedFilename());

await download.saveAs('downloads/sample.pdf');
If you click first and start listening afterward, you can miss the download event.

12. Hooks

Reusable setup and cleanup around tests
HookWhen it runsTypical use
beforeAllOnce before tests in its scopeSuite-level setup
beforeEachBefore every testNavigate / reset test state
afterEachAfter every testCleanup / logging
afterAllOnce after tests in its scopeSuite-level cleanup

Best practical pattern

test.describe('Login Tests', () => {

  test.beforeEach(async ({ page }) => {
    await page.goto(LOGIN_URL);
  });

  test('valid login', async ({ page }) => {
    // test
  });

});

Important distinction

Use beforeEach when each test needs a fresh page/setup. Do not try to use the test-scoped page fixture as though it were a suite-wide object in beforeAll.

13. Groups, describe & tags

Organize a growing suite instead of keeping everything flat

test.describe()

test.describe('Login Module', () => {

  test('valid login', async ({ page }) => {
    // ...
  });

  test('invalid login', async ({ page }) => {
    // ...
  });

});

Why groups matter

  • Cleaner structure
  • Selective execution
  • Shared hooks
  • Better debugging
  • Scalability
  • Team ownership
  • Clearer reports

Tags and grep

test('Valid login @smoke', async ({ page }) => {
  // ...
});

test('Full login validation @regression', async ({ page }) => {
  // ...
});
npx playwright test --grep "@smoke"
npx playwright test -g "@smoke"
npx playwright test --grep "@smoke|@regression"
npx playwright test --grep-invert "@slow"
npx playwright test --grep "@regression" --grep-invert "@slow"

AND logic for tags

npx playwright test --grep "(?=.*@smoke)(?=.*@regression)"

The class example used a regex lookahead to select tests carrying both tags.

14. Execution model

Sequential vs parallel and test isolation

Sequential

Tests run one after another in the same worker flow. Useful when you deliberately want controlled ordering, but independent tests are generally easier to maintain.

Parallel

Independent tests can execute concurrently across workers. This can reduce total execution time, especially in CI.

Test isolation

Each test should be able to run independently. Avoid making one test depend on another test's state. This is also one of the major defenses against flaky tests.

Professional mindset: independent test + predictable state + stable locator + state-based assertion.

15. Playwright reports

Turn raw execution into useful results

Why reports matter

  • Quickly identify failing tests
  • Share results with non-technical stakeholders
  • Track regressions and trends
  • Useful in CI/CD

HTML reporter

export default defineConfig({
  reporter: 'html'
});

Multiple reporters

export default defineConfig({
  reporter: [
    ['html'],
    ['line'],
    ['json', { outputFile: 'results.json' }]
  ]
});

CI-aware example from class

export default defineConfig({
  reporter: process.env.CI
    ? [['github'], ['html', { open: 'never' }]]
    : [['html'], ['line']]
});

open: 'never' prevents the HTML report from automatically opening a browser in CI.

16. Flaky tests, retries & repeatEach

The same test sometimes passes and sometimes fails without a relevant code change

What is flaky?

Same test+Same app+Same environment→Different result

Common causes

  1. Timing / synchronization
  2. Unstable or incorrect locators
  3. Slow API / network
  4. Dynamic test data
  5. Race conditions
  6. Tests depending on other tests

Retries

export default defineConfig({
  retries: 2
});

Retry runs a test again when it fails. If it passes on retry, the result can be reported as flaky.

repeatEach

Repeats every test, including tests that pass, mainly to investigate instability.

Do not use retries as a substitute for fixing a flaky test's root cause.

Retries vs repeatEach

FeatureretriesrepeatEach
PurposeRetry failed testsRepeat every test
Passing test rerun?NoYes
Failed test rerun?YesYes
Flaky investigationUsefulVery useful
Typical useCI/CDInvestigation

17. OrangeHRM project

Your first structured project using groups + hooks + quality-focused coverage

Basic login coverage you executed

  1. Login page
  2. Login UI elements
  3. Valid credentials
  4. Invalid username
  5. Invalid password
  6. Invalid username + password
  7. Empty username
  8. Empty password
  9. Empty username + password
  10. Forgot password

Professional grouping direction

  • Smoke: availability, essential UI, valid login, forgot-password navigation
  • Regression: validations, whitespace, masking, refresh/state, URL leakage, boundary behavior
  • Security / robustness: harmless injection/XSS checks and resilience observations

What you learned from your actual failures

  • getByText('Dashboard').nth(2) was wrong because there were not three matching elements. A heading role was more precise.
  • getByText('Reset Password') matched both heading and button. Use a more specific role/name when strict mode finds multiple matches.
  • URL assertions can pass even when a later UI assertion fails.

18. Your actual debugging lessons

These are worth revising because you encountered them yourself

1. Locator ≠ element state

Creating a locator does not mean the element is visible, selected, or ready. Use the appropriate assertion/state query.

2. Retrieve vs assert

inputValue() gets a value; toHaveValue() checks it. Same idea for count() vs toHaveCount().

3. Missing await

If a method returns a Promise and you forget await, you may compare a Promise instead of the actual result.

4. Wrong expected type

toHaveText() expects a string/regex, not a Locator object.

5. Strict mode

Multiple matches can make Playwright reject an ambiguous locator. Make the locator more specific instead of blindly adding positions.

6. CSS selector syntax

Classes require dots; attribute selectors need correct quoting/brackets. A malformed selector can fail before the browser even searches for the element.

7. Actual DOM beats assumptions

Your date-picker failure showed that the actual option label was Nov, not November. Inspect the page instead of guessing.

8. Event listener order

For downloads and new pages, register the event listener/promise before triggering the action.

19. Interview quick-fire

Answer these without opening the code

Core

What is a locator?
How does it differ from an action?
Why is await needed?
What happens if you omit it?
What is an assertion?
Why is an action alone not enough?

Controls

fill() vs inputValue()?
check() vs isChecked() vs toBeChecked()?
count() vs toHaveCount() vs array.length vs toHaveLength()?
Native dropdown vs auto-suggestion?

Framework

Why test.describe()?
beforeAll vs beforeEach?
Why should tests be independent?
When would you use tags + --grep?

Advanced practical

Why does page.locator() fail inside an iframe?
Why must waitForEvent('download') come before click?
retries vs repeatEach?
What makes a test flaky?

⚡ 60-second final memory sheet

TopicRemember this
LocatorFIND the element
ActionDO what the user does
AssertionPROVE the expected result
TextboxVALUE
RadioONE choice
CheckboxMANY choices
Native dropdownselectOption()
Auto-suggestionTYPE → FIND → CLICK
FrameSeparate DOM boundary
New tab/pageListen for page event before action
DownloadListen → click → await → save/inspect
HooksbeforeAll / beforeEach / afterEach / afterAll
Groupstest.describe()
Tags--grep / --grep-invert
FlakySame test, different result without relevant code change
retriesRetry failed tests
repeatEachRepeat every test to investigate instability
ReportsTurn execution results into readable evidence