About Playwright is the end-to-end testing library It loads your app into headless browser and interacts with it like it would do a human Commands "playwright": "npx playwright install && npx playwright test", "playwright:ui": "npx playwright install && npx playwright test --ui", "playwright:debug": "npx playwright install && npx playwright test --debug", "playwright:codegen": "npx playwright install && npx playwright codegen https://localhost:3000 --ignore-https-errors", "playwright:update": "npm i -D @playwright/test@latest", Parallel vs serial test.describe.configure({ mode: 'parallel' }) // default // test.describe.configure({ mode: 'serial' }) test('runs in parallel 1', async ({ page }) => { /* ... */ }) test('runs in parallel 2', async ({ page }) => { /* ... */ }) Assertions https://playwright.dev/docs/test-assertions // Auto-retrying assertions await expect(locator).toBeAttached() // element is attached await expect(locator).toBeChecked() // checkbox is checked await expect(locator).toBeDisabled() // element is disabled await expect(locator).toBeEditable() // element is editable await expect(locator).toBeEmpty() // container is empty await expect(locator).toBeEnabled() // element is enabled await expect(locator).toBeFocused() // element is focused await expect(locator).toBeHidden() // element is not visible await expect(locator).toBeInViewport() // element intersects viewport await expect(locator).toBeVisible() // element is visible await expect(locator).toContainText() // element contains text await expect(locator).toHaveAccessibleDescription() // element has a matching accessible description await expect(locator).toHaveAccessibleName() // element has a matching accessible name await expect(locator).toHaveAttribute() // element has a DOM attribute await expect(locator).toHaveClass() // element has a class property await expect(locator).toHaveCount() // list has exact number of children await expect(locator).toHaveCSS() // element has CSS property await expect(locator).toHaveId() // element has an ID await expect(locator).toHaveJSProperty() // element has a JavaScript property await expect(locator).toHaveRole() // element has a specific ARIA role await expect(locator).toHaveScreenshot() // element has a screenshot await expect(locator).toHaveText() // element matches text await expect(locator).toHaveValue() // input has a value await expect(locator).toHaveValues() // select has options selected await expect(page).toHaveScreenshot() // page has a screenshot await expect(page).toHaveTitle() // page has a title await expect(page).toHaveURL() // page has a URL await expect(response).toBeOK() // response has an OK status // Non-retrying assertions expect(value).toBe() // value is the same expect(value).toBeCloseTo() // number is approximately equal expect(value).toBeDefined() // value is not undefined expect(value).toBeFalsy() // value is falsy, e.g. false, 0, null, etc. expect(value).toBeGreaterThan() // number is more than expect(value).toBeGreaterThanOrEqual() // number is more than or equal expect(value).toBeInstanceOf() // object is an instance of a class expect(value).toBeLessThan() // number is less than expect(value).toBeLessThanOrEqual() // number is less than or equal expect(value).toBeNaN() // value is NaN expect(value).toBeNull() // value is null expect(value).toBeTruthy() // value is truthy, i.e. not false, 0, null, etc. expect(value).toBeUndefined() // value is undefined expect(value).toContain() // string contains a substring expect(data.message).toContain('Expired Token') // example from work expect(value).toContain() // array or set contains an element expect(value).toContainEqual() // array or set contains a similar element expect(value).toEqual() // value is similar // deep equality and pattern matching expect(value).toHaveLength() // array or string has length expect(value).toHaveProperty() // object has a property expect(value).toMatch() // string matches a regular expression expect(value).toMatchObject() // object contains specified properties expect(value).toStrictEqual() // value is similar, including property types expect(value).toThrow() // function throws an error expect(value).any() // matches any instance of a class/primitive expect(value).anything() // matches anything expect(value).arrayContaining() // array contains specific elements expect(value).closeTo() // number is approximately equal expect(value).objectContaining() // object contains specific properties expect(value).stringContaining() // string contains a substring expect(value).stringMatching() // string matches a regular expression Negating matchers expect(value).not.toEqual(0); await expect(locator).not.toContainText('some text'); Custom expect message String "should be logged in" will be visible in test logs await expect(page.getByText('Name'), 'should be logged in').toBeVisible() Soft assertions // Make a few checks that will not stop the test when failed... await expect.soft(page.getByTestId('status')).toHaveText('Success') // ... and continue the test to check more things. await page.getByRole('link', { name: 'next page' }).click() Locators https://playwright.dev/docs/locators https://playwright.dev/docs/other-locators // Recommended page.getByRole() // to locate by explicit and implicit accessibility attributes. page.getByText() // to locate by text content. page.getByLabel() // to locate a form control by associated label's text. page.getByPlaceholder() // to locate an input by placeholder. page.getByAltText() // to locate an element, usually image, by its text alternative. page.getByTitle() // to locate an element by its title attribute. page.getByTestId() // to locate an element based on its data-testid attribute (other attributes can be configured). // Not recommended await page.locator('#tsf > div:nth-child(2) > div.A8SBwf > input').click(); // CSS locator (not recommended) // Playwright adds custom pseudo-classes like :visible, :has-text(), :has(), :is(), :nth-match() and more. await page.locator('css=button').click(); await page.locator('css=button:visible').click(); await page.locator('css=[data-test="login"]:enabled').click(); await page.locator('button').click(); await page.locator('button:visible').click(); await page.locator(':has-text("Playwright")').click(); await page.locator('article:has-text("Playwright")').click(); await page.locator('#nav-bar :text("Home")').click(); await page.locator('#nav-bar :text-is(("Home")').click() // matches exact text await page.locator('#nav-bar :text-matches("reg?ex", "i")').click() // matches reg exp await page.locator('article:has(div.promo)').textContent() // elements that contain other elements await page.locator('button:has-text("Log in"), button:has-text("Sign in")').click(); await page.locator('button:near(.promo-card)').click(); await page.locator('button:near(div > button)').click(); await page.locator('button:above(.promo-card)').click(); await page.locator('button:below(.promo-card)').click(); await page.locator('button:right-of(.promo-card)').click() // button to the right of card await page.locator('input:right-of(:text("Username"))').fill('value'); await page.locator('button:left-of(.promo-card)').click(); await page.locator('[type=radio]:left-of(:text("Label 3"))').first().click(); await page.locator(':nth-match(:text("Buy"), 3)').click() // click 3rd "Buy" button await page.locator('button').locator('nth=0').click() // click 1st button await page.locator('button').locator('nth=-1').click() // click last button await page.locator('button:has-text("Log in"), button:has-text("Sign in")').click() // matching one of the conditions await page.locator('id=username').fill('value') // Fill an input with the id "username" await page.locator('data-test-id=submit').click() // Click an element with data-test-id "submit" // Parent element locator const child = page.getByText('Hello'); const parent = page.getByRole('listitem').filter({ has: child }); // React locator, only work against unminified application builds await page.locator('_react=BookItem').click() // match by component await page.locator('_react=BookItem[author = "Steven King"]').click() // match by component and exact property value, case-sensitive await page.locator('_react=[author = "Steven King" i]').click() // match by property value only, case-insensitive await page.locator('_react=MyButton[enabled]').click() // match by component and truthy property value await page.locator('_react=MyButton[enabled = false]').click() // match by component and boolean value await page.locator('_react=[author *= "King"]').click() // match by property value substring await page.locator('_react=BookItem[author *= "king" i][year = 1990]').click() // match by component and multiple properties await page.locator('_react=[some.nested.value = 12]').click() // match by nested property value await page.locator('_react=BookItem[author ^= "Steven"]').click() // match by component and property value prefix await page.locator('_react=BookItem[author $= "Steven"]').click() // match by component and property value suffix await page.locator('_react=BookItem[key = '2']').click() // match by component and key await page.locator('_react=[author = /Steven(\\s+King)?/i]'2']').click() // match by property value regex: // Examples await page.getByLabel('User Name').fill('John'); await page.getByLabel('Password').fill('secret-password'); await page.getByRole('button', { name: 'Sign in' }).click(); await expect(page.getByRole('heading', { name: 'Sign up' })).toBeVisible(); await page.getByRole('checkbox', { name: 'Subscribe' }).check(); await page.getByRole('button', { name: /submit/i }).click(); const locator = page.getByRole('button', { name: 'Sign in' }); await locator.hover(); await locator.click(); await expect(page.getByText('Welcome, John!')).toBeVisible(); await expect(page.getByText('Welcome, John', { exact: true })).toBeVisible(); await expect(page.getByText(/welcome, [A-Za-z]+$/i)).toBeVisible(); await page.getByAltText('playwright logo').click(); await expect(page.getByTitle('Issues count')).toHaveText('25 issues'); await page.getByTestId('directions').click(); Filtering locators Locators can be filtered by text with the locator.filter() method Search for a particular string somewhere inside the element // has text await page .getByRole('listitem') .filter({ hasText: 'Product 2' }) .getByRole('button', { name: 'Add to cart' }) .click(); // Use a regular expression await page .getByRole('listitem') .filter({ hasText: /Product 2/ }) .getByRole('button', { name: 'Add to cart' }) .click(); // Not having text await expect(page.getByRole('listitem').filter({ hasNotText: 'Out of stock' })).toHaveCount(5); // Filter by another locator await page .getByRole('listitem') .filter({ has: page.getByRole('heading', { name: 'Product 2' }) }) .getByRole('button', { name: 'Add to cart' }) .click(); await expect(page .getByRole('listitem') .filter({ has: page.getByRole('heading', { name: 'Product 2' }) })) .toHaveCount(1); await expect(page .getByRole('listitem') .filter({ hasNot: page.getByText('Product 2') })) .toHaveCount(1); // Matching inside a locator const product = page.getByRole('listitem').filter({ hasText: 'Product 2' }); await product.getByRole('button', { name: 'Add to cart' }).click(); await expect(product).toHaveCount(1); // Matching inside a locator with locator() const saveButton = page.getByRole('button', { name: 'Save' }); const dialog = page.getByTestId('settings-dialog'); await dialog.locator(saveButton).click(); // Matching two locators simultaneously const button = page.getByRole('button').and(page.getByTitle('Subscribe')); // Matching one of the two alternative locators const newEmail = page.getByRole('button', { name: 'New' }); const dialog = page.getByText('Confirm security settings'); await expect(newEmail.or(dialog).first()).toBeVisible(); if (await dialog.isVisible()) await page.getByRole('button', { name: 'Dismiss' }).click(); await newEmail.click(); // Matching only visible elements await page.locator('button').locator('visible=true').click(); // Count items in a list await expect(page.getByRole('listitem')).toHaveCount(3); // Assert all text in a list await expect(page .getByRole('listitem')) .toHaveText(['apple', 'banana', 'orange']); // Get a specific item await page.getByText('orange').click(); await page.getByTestId('orange').click(); const banana = await page.getByRole('listitem').nth(1); await page .getByRole('listitem') .filter({ hasText: 'orange' }) .click(); // Chaining filters const rowLocator = page.getByRole('listitem'); await rowLocator .filter({ hasText: 'Mary' }) .filter({ has: page.getByRole('button', { name: 'Say goodbye' }) }) // Do something with each element in the list for (const row of await page.getByRole('listitem').all()) console.log(await row.textContent()) const rows = page.getByRole('listitem'); const count = await rows.count(); for (let i = 0; i < count; ++i) console.log(await rows.nth(i).textContent()); const rows = page.getByRole('listitem'); const texts = await rows.evaluateAll( list => list.map(element => element.textContent)); // If more than one element await page.getByRole('button').click() // throws an error await page.getByRole('button').count() // ok // Locate specific item when many locator.first() locator.last() locator.nth() Actions https://playwright.dev/docs/actionability await locator.check() await locator.click() await locator.dblclick() await locator.setChecked() await locator.tap() await locator.uncheck() await locator.hover() await locator.dragTo() await locator.screenshot() await locator.fill() await locator.clear() await locator.selectOption() await locator.selectText() await locator.scrollIntoViewIfNeeded() await locator.blur() await locator.dispatchEvent() await locator.focus() await locator.press() await locator.pressSequentially() await locator.setInputFiles() Actions example https://playwright.dev/docs/input // Text input example await page.getByRole('textbox').fill('Peter') // Text input await page.getByLabel('Birth date').fill('2020-02-02') // Date input await page.getByLabel('Appointment time').fill('13:15') // Time input await page.getByLabel('Local time').fill('2020-03-02T05:15') // Local datetime input // Checkboxes and radio buttons example await page.getByLabel('I agree to the terms above').check() // Check the checkbox expect(page.getByLabel('Subscribe to newsletter')).toBeChecked() // Assert the checked state await page.getByLabel('XL').check() // Select the radio button // Select options example await page.getByLabel('Choose a color').selectOption('blue') // Single selection matching the value or label await page.getByLabel('Choose a color').selectOption({ label: 'Blue' }) // Single selection matching the label await page.getByLabel('Choose multiple colors').selectOption(['red', 'green', 'blue']) // Multiple selected items // Mouse click example await page.getByRole('button').click() // Generic click await page.getByText('Item').dblclick() // Double click await page.getByText('Item').click({ button: 'right' }) // Right click await page.getByText('Item').click({ modifiers: ['Shift'] }) // Shift + click await page.getByText('Item').click({ modifiers: ['ControlOrMeta'] }) // Ctrl + click or Windows and Linux, Meta + click on macOS await page.getByText('Item').hover() // Hover over element await page.getByText('Item').click({ position: { x: 0, y: 0 } }) // Click the top left corner // Forcing the click example await page.getByRole('button').click({ force: true }); // Programmatic click example await page.getByRole('button').dispatchEvent('click'); // Type characters example await page.locator('#area').pressSequentially('Hello World!') // Press keys one by one // Keys and shortcuts example await page.getByText('Submit').press('Enter') // Hit Enter await page.getByRole('textbox').press('Control+ArrowRight') // Dispatch Control+Right await page.getByRole('textbox').press('$') // Press $ sign on keyboard /* Can use Shift, Control, Alt, Meta, Backquote, Minus, Equal, Backslash, Backspace, Tab, Delete, Escape, ArrowDown, End, Enter, Home, Insert, PageDown, PageUp, ArrowRight, ArrowUp, F1 - F12, Digit0 - Digit9, KeyA - KeyZ, etc. "a"..."Z" "Control+o", "Control+Shift+T" */ // Upload files example await page.getByLabel('Upload file').setInputFiles(path.join(__dirname, 'myfile.pdf')) // Select one file await page.getByLabel('Upload files').setInputFiles([ path.join(__dirname, 'file1.txt'), path.join(__dirname, 'file2.txt'), ]) // Select multiple files await page.getByLabel('Upload directory').setInputFiles(path.join(__dirname, 'mydir')) // Select a directory await page.getByLabel('Upload file').setInputFiles([]) // Remove all the selected files await page.getByLabel('Upload file').setInputFiles({ name: 'file.txt', mimeType: 'text/plain', buffer: Buffer.from('this is test') }) // Upload buffer from memory // If you don't have input element in hand (it is created dynamically) // Start waiting for file chooser before clicking. Note no await. const fileChooserPromise = page.waitForEvent('filechooser'); await page.getByLabel('Upload file').click(); const fileChooser = await fileChooserPromise; await fileChooser.setFiles(path.join(__dirname, 'myfile.pdf')); // Focus element example await page.getByLabel('Password').focus(); // Drag and Drop example await page.locator('#item-to-be-dragged').dragTo(page.locator('#item-to-drop-at')); // Dragging manually example await page.locator('#item-to-be-dragged').hover(); await page.mouse.down(); await page.locator('#item-to-drop-at').hover(); await page.mouse.up(); // Scrolling (usually Playwright does it automatically) await page.getByText('Footer text').scrollIntoViewIfNeeded() // Scroll the footer into view, forcing an "infinite list" to load more content // Position the mouse and scroll with the mouse wheel await page.getByTestId('scrolling-container').hover(); await page.mouse.wheel(0, 10); // Alternatively, programmatically scroll a specific element await page.getByTestId('scrolling-container').evaluate(e => e.scrollTop += 100); API testing APIRequestContext can send all kinds of HTTP(S) requests over network import { apiUrl } from '@back/consts/apiUrl' import { connectToDb } from '@back/db/connectToDb' import { UserModel } from '@back/db/models/userModel' import { baseUrlBack } from '@back/utils/env' import { test, expect } from '@playwright/test' import { userFilePath } from 'tests/setup/userFilePath' test.describe.configure({ mode: 'serial' }) test.describe('#activateRouter', () => { test.beforeAll(async () => { await connectToDb() }) test.afterAll(async ({ request }) => { // console.log('do after test, for ex clean db') }) test.use({ baseURL: baseUrlBack }) const email = 'anton.arbus@gmail.com' test('should not return successful status if key is missing', async ({ request, }) => { const res = await request.post(apiUrl.activate, { data: { activationKey: 'bad activation key', }, }) expect(res.ok()).toBeFalsy() expect(await res.json()).toMatchObject({ message: 'activation key not found', }) }) test('should return successful status if activation key is correct', async ({ request, }) => { const userDocument = await UserModel.findOneAndUpdate( { email }, { activationKey: 'good activation key', isActivated: false, }, { upsert: true, new: true }, ).lean() const res = await request.post(apiUrl.activate, { data: { activationKey: userDocument.activationKey, }, }) await request.storageState({ path: userFilePath.authenticated }) expect(res.ok()).toBeTruthy() expect(await res.json()).toMatchObject({ message: 'activated' }) }) test('should return successful status if account had been already activated', async ({ request, }) => { const userDocument = await UserModel.findOneAndUpdate( { email }, { isActivated: true }, { upsert: true, new: true }, ).lean() const res = await request.post(apiUrl.activate, { data: { activationKey: userDocument.activationKey, }, }) expect(res.ok()).toBeTruthy() expect(await res.json()).toMatchObject({ message: 'already activated' }) }) }) request object is also available in Authentication https://playwright.dev/docs/auth Mainly we want to make tests for authenticated user We may do authentication for all tests using a dependency Create a file where auth user tokens will be stored mkdir -p playwright/.auth echo $'\nplaywright/.auth' >> .gitignore Create a dependency which will be run ones before all tests // auth.setup.ts import { test as setup, request } from '@playwright/test' import fs from 'fs/promises' import path from 'path' setup('authenticate', async () => { const context = await request.newContext({ ignoreHTTPSErrors: true, // This line ignores certificate errors }) const response = await context.post('/api/login', { data: { email: 'email@gmail.com', password: 'pass', }, }) if (response.ok()) { const authDir = path.resolve('playwright', '.auth') const filePath = path.join(authDir, 'authenticated_user.json') await fs.mkdir(authDir, { recursive: true }) await context.storageState({ path: filePath }) } else { throw new Error(`Failed to authenticate: ${response.status()}`) } }) Run all *.setup.ts files as a dependency before all tests // playwright.config.ts import { baseUrlFrontDev } from '@back/utils/env' import { defineConfig, devices } from '@playwright/test' // https://playwright.dev/docs/test-configuration export default defineConfig({ testDir: './tests', fullyParallel: true, forbidOnly: Boolean(process.env.CI), retries: process.env.CI ? 2 : 0, workers: process.env.CI ? 1 : undefined, reporter: process.env.CI ? 'dot' : 'list', use: { baseURL: baseUrlFrontDev, trace: 'on-first-retry', }, projects: [ { name: 'setup', testMatch: /.*\.setup\.ts/u, use: { launchOptions: { args: ['--ignore-certificate-errors'], }, }, }, { name: 'chromium', use: { ...devices['Desktop Chrome'], launchOptions: { args: ['--ignore-certificate-errors'], }, storageState: 'playwright/.auth/authenticated_user.json', }, dependencies: ['setup'], }, ], /* Run your local dev server before starting the tests */ webServer: { command: 'npm run start', url: 'https://localhost:3000', reuseExistingServer: !process.env.CI, ignoreHTTPSErrors: true, }, }) After setup test is run it will populate following file with cookies which should include auth data // playwright/.auth/authenticated_user.json { "cookies": [ { "name": "refreshJwtToken", "value": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJlbWFpbCI6ImFudG9uLmFyYnVzQGdtYWlsLmNvbSIsInJvbGVzIjpbInVzZXIiXSwiaWF0IjoxNzI2NTE4NzM5LCJleHAiOjE3MjkxMTA3Mzl9.-ig09wjRB3Seo6oSP3LAfIn0qE6E7lhrHCgxGaar5g8", "domain": "localhost", "path": "/", "expires": 1730268519, "httpOnly": true, "secure": false, "sameSite": "Lax" } ], "origins": [] } To avoid authentication we may create a file with empty cookies // playwright/.auth/guest_user.json { "cookies": [], "origins": [] } Reference it in tests like where you don't need a user to be authenticated test.use({ storageState: 'playwright/.auth/guest_user.json' }) // or test.use({ storageState: { cookies: [], origins: [] } }); test.describe('nav icons for guest user', () => { test.use({ viewport: { width: 1600, height: 1200 } }) test.use({ storageState: 'playwright/.auth/guest_user.json' }) test('should show icons & text', async ({ page }) => { await expect(nav.locator('[data-testid="login icon"]')).toBeVisible() await expect(nav).toHaveText(/Log in/u, { timeout: 1000 }) await expect(nav.locator('[data-testid="profile icon"]')).not.toBeVisible() await expect(nav).not.toHaveText(/Profile/u, { timeout: 1000 }) }) }) Same way you may reference different auth files in different tests instead of setting it in the config For different auth roles you just do multiple login setup functions which create different files // basic-auth.setup.ts import { test as setup, request } from '@playwright/test' import fs from 'fs/promises' import path from 'path' setup('authenticate basic user', async () => { const context = await request.newContext({ ignoreHTTPSErrors: true, // This line ignores certificate errors }) const response = await context.post('/api/login', { data: { email: 'basic-user@gmail.com', password: 'some password', }, }) if (response.ok()) { const authDir = path.resolve('playwright', '.auth') const filePath = path.join(authDir, 'basic_user.json') await fs.mkdir(authDir, { recursive: true }) await context.storageState({ path: filePath }) } else { throw new Error(`Failed to authenticate: ${response.status()}`) } }) // admin-auth.setup.ts import { test as setup, request } from '@playwright/test' import fs from 'fs/promises' import path from 'path' setup('authenticate admin user', async () => { const context = await request.newContext({ ignoreHTTPSErrors: true, // This line ignores certificate errors }) const response = await context.post('/api/login', { data: { email: 'admin-user@gmail.com', password: 'some password', }, }) if (response.ok()) { const authDir = path.resolve('playwright', '.auth') const filePath = path.join(authDir, 'admin_user.json') await fs.mkdir(authDir, { recursive: true }) await context.storageState({ path: filePath }) } else { throw new Error(`Failed to authenticate: ${response.status()}`) } }) And you just point to those files in storageState in tests or test groups, instead of setting it globally in the config. import { test } from '@playwright/test'; test.use({ storageState: 'playwright/.auth/basic_user.json' }); test('basic user test', async ({ page }) => { // page is authenticated as basic user }); test.describe(() => { test.use({ storageState: 'playwright/.auth/admin_user.json' }); test('admin user test', async ({ page }) => { // page is authenticated as a admin_ user }); }) To test how users with different roles interact together in a single test have to create pages with different contexts import { test } from '@playwright/test'; test('admin and user', async ({ browser }) => { // adminContext and all pages inside, including adminPage, are signed in as "admin". const adminContext = await browser.newContext({ storageState: 'playwright/.auth/admin.json' }); const adminPage = await adminContext.newPage(); // userContext and all pages inside, including userPage, are signed in as "user". const userContext = await browser.newContext({ storageState: 'playwright/.auth/user.json' }); const userPage = await userContext.newPage(); // ... interact with both adminPage and userPage ... await adminContext.close(); await userContext.close(); }); If some api test invalidates auth tokens you need to update the files to prevent other tests to fail test('should return successful status if activation key is correct', async ({ request, }) => { const userDocument = await UserModel.findOneAndUpdate( { email }, { activationKey: 'good activation key', isActivated: false, }, { upsert: true, new: true }, ).lean() const res = await request.post(apiUrl.activate, { data: { activationKey: userDocument.activationKey, }, }) const filePath = path.resolve( 'playwright', '.auth', 'authenticated_user.json', ) await request.storageState({ path: filePath }) expect(res.ok()).toBeTruthy() expect(await res.json()).toMatchObject({ message: 'activated', }) }) Session storage Some data from session storage may be required for tests, for example Session storage is specific to a particular domain It is not persisted across page loads Playwright does not provide API to persist session storage Here is the hack to emulate the session storage // Get session storage and store as env variable const sessionStorage = await page.evaluate(() => JSON.stringify(sessionStorage)); fs.writeFileSync('playwright/.auth/session.json', sessionStorage, 'utf-8'); // Set session storage in a new context const sessionStorage = JSON.parse(fs.readFileSync('playwright/.auth/session.json', 'utf-8')); await context.addInitScript(storage => { if (window.location.hostname === 'example.com') { for (const [key, value] of Object.entries(storage)) window.sessionStorage.setItem(key, value); } }, sessionStorage); Clock Clock api provides the following methods to control time: setFixedTime() // Sets the fixed time for Date.now() and new Date() install() // initializes the clock and allows you to pauseAt() // Pauses the time at a specific time fastForward() // Fast forwards the time runFor() // Runs the time for a specific duration resume() // Resumes the time setSystemTime() // Sets the current system time <div id="current-time" data-testid="current-time"></div> <script> const renderTime = () => { document.getElementById('current-time').textContent = new Date().toLocaleString(); }; setInterval(renderTime, 1000); </script> // test 1 - setFixedTime await page.clock.setFixedTime(new Date('2024-02-02T10:00:00')); await page.goto('http://localhost:3333'); await expect(page.getByTestId('current-time')).toHaveText('2/2/2024, 10:00:00 AM'); await page.clock.setFixedTime(new Date('2024-02-02T10:30:00')); // We know that the page has a timer that updates the time every second. await expect(page.getByTestId('current-time')).toHaveText('2/2/2024, 10:30:00 AM'); // test 2 - install + pauseAt + fastForward // Initialize clock with some time before the test time and let the page load // naturally. `Date.now` will progress as the timers fire. await page.clock.install({ time: new Date('2024-02-02T08:00:00') }); await page.goto('http://localhost:3333'); // Pretend that the user closed the laptop lid and opened it again at 10am, // Pause the time once reached that point. await page.clock.pauseAt(new Date('2024-02-02T10:00:00')); // Assert the page state. await expect(page.getByTestId('current-time')).toHaveText('2/2/2024, 10:00:00 AM'); // Close the laptop lid again and open it at 10:30am. await page.clock.fastForward('30:00'); await expect(page.getByTestId('current-time')).toHaveText('2/2/2024, 10:30:00 AM'); // test 3 - runFor // Initialize clock with a specific time, let the page load naturally. await page.clock.install({ time: new Date('2024-02-02T08:00:00') }); await page.goto('http://localhost:3333'); // Pause the time flow, stop the timers, you now have manual control // over the page time. await page.clock.pauseAt(new Date('2024-02-02T10:00:00')); await expect(page.getByTestId('current-time')).toHaveText('2/2/2024, 10:00:00 AM'); // Tick through time manually, firing all timers in the process. // In this case, time will be updated in the screen 2 times. await page.clock.runFor(2000); await expect(page.getByTestId('current-time')).toHaveText('2/2/2024, 10:00:02 AM'); Dialogs alert(), confirm(), prompt() are dismissed by default in playwright to handle the dialog you need to register the listener // alert(), confirm(), prompt() page.on('dialog', dialog => dialog.accept()) // or dialog.dismiss() await page.getByRole('button').click() // beforeunload page.on('dialog', async dialog => { assert(dialog.type() === 'beforeunload') await dialog.dismiss() }) await page.close({ runBeforeUnload: true }) // printer dialog await page.goto('<url>') await page.evaluate('(() => {window.waitForPrintDialog = new Promise(f => window.print = f)})()') await page.getByText('Print it!').click() await page.waitForFunction('window.waitForPrintDialog') Downloads For every download by the page, page.on('download') event is emitted attachments are downloaded into a temporary folder You can obtain the download url, file name and payload stream Downloaded files are deleted when the browser context that produced them is closed // Start waiting for download before clicking. Note no await. const downloadPromise = page.waitForEvent('download') await page.getByText('Download file').click() const download = await downloadPromise // Wait for the download process to complete and save the downloaded file somewhere. await download.saveAs('/path/to/save/at/' + download.suggestedFilename()) If you have no idea what initiates the download, you can still handle the event page.on('download', download => download.path().then(console.log)) Run JS in page page.evaluate() allows you to execute JavaScript in the context of the web page test and page has different contexts but you can run JS in page and bring result back to test // not promise const href = await page.evaluate(() => document.location.href); console.log(await page.evaluate('1 + 2')); // prints "3" const x = 10; console.log(await page.evaluate(`1 + ${x}`)); // prints "11" // promise const status = await page.evaluate(async () => { const response = await fetch(location.href); return response.status; }); if you need to pass a variable to the page as a parameter const data = 'some data'; const result = await page.evaluate(data => { window.myApp.use(data); }, data); https://playwright.dev/docs/evaluating check it further, not very clear.... Events https://playwright.dev/docs/events check it further, not very clear.... iFrame a page can have iframe // Locate element inside frame const username = await page.frameLocator('.frame-class').getByLabel('User Name'); await username.fill('John') // Get frame using the frame's name attribute const frame = page.frame('frame-login'); // Get frame using frame's URL const frame = page.frame({ url: /.*domain.*/ }); // Interact with the frame await frame.fill('#username-input', 'John'); Mock request Any requests that a page does can be tracked, modified and mocked The following code will intercept all the calls to */**/api/v1/fruits and will return a custom response instead. No requests to the API will be made test("mocks a fruit and doesn't call api", async ({ page }) => { // Mock the api call before navigating await page.route('*/**/api/v1/fruits', async route => { const json = [{ name: 'Strawberry', id: 21 }] await route.fulfill({ json }) }) // Go to the page await page.goto('https://demo.playwright.dev/api-mocking') // Assert that the Strawberry fruit is visible await expect(page.getByText('Strawberry')).toBeVisible() }) Shorter version await page.route('**/api/fetch_data', route => route.fulfill({ status: 200, body: testData, })); await page.goto('https://example.com'); Modify response test('gets the json from api and adds a new fruit', async ({ page }) => { // Get the response and add to it await page.route('*/**/api/v1/fruits', async route => { const response = await route.fetch() const json = await response.json() json.push({ name: 'Loquat', id: 100 }) // Fulfill using the original response, while patching the response body // with the given JSON object. await route.fulfill({ response, json }) }) // Go to the page await page.goto('https://demo.playwright.dev/api-mocking') // Assert that the new fruit is visible await expect(page.getByText('Loquat', { exact: true })).toBeVisible() }) You can override individual fields on the response await page.route('**/title.html', async route => { // Fetch original response. const response = await route.fetch(); // Add a prefix to the title. let body = await response.text(); body = body.replace('<title>', '<title>My prefix:'); await route.fulfill({ // Pass all fields from the response. response, // Override response body. body, // Force content type to be html. headers: { ...response.headers(), 'content-type': 'text/html' } }); }); Modify request // Delete header await page.route('**/*', async route => { const headers = route.request().headers(); delete headers['X-Secret']; await route.continue({ headers }); }); // Continue requests as POST. await page.route('**/*', route => route.continue({ method: 'POST' })); Abort request test.beforeEach(async ({ context }) => { // Block any css requests for each test in this file. await context.route(/.css$/, route => route.abort()) }) test('loads page without css', async ({ page }) => { await page.goto('https://playwright.dev') // ... test goes here }) Or, you can use page.route() test('loads page without images', async ({ page }) => { // Block png and jpeg images await page.route(/(png|jpeg)$/, route => route.abort()) await page.goto('https://playwright.dev') // ... test goes here }) // Abort based on the request type await page.route('**/*', route => { return route.request().resourceType() === 'image' ? route.abort() : route.continue(); }); HTTP Authentication Via playwright.config.ts import { defineConfig } from '@playwright/test'; export default defineConfig({ use: { httpCredentials: { username: 'bill', password: 'pa55w0rd', } } }); In test const context = await browser.newContext({ httpCredentials: { username: 'bill', password: 'pa55w0rd', }, }); const page = await context.newPage(); await page.goto('https://example.com'); HTTP Proxy Proxy can be either set globally for the entire browser, or for each browser context individually // playwright.config.ts import { defineConfig } from '@playwright/test'; export default defineConfig({ use: { proxy: { server: 'http://myproxy.com:3128', username: 'usr', password: 'pwd' } } }); Or in test import { test, expect } from '@playwright/test'; test('should use custom proxy on a new context', async ({ browser }) => { const context = await browser.newContext({ proxy: { server: 'http://myproxy.com:3128', } }); const page = await context.newPage(); await context.close(); }); WebSockets Playwright supports WebSockets inspection out of the box page.on('websocket', ws => { console.log(`WebSocket opened: ${ws.url()}>`); ws.on('framesent', event => console.log(event.payload)); ws.on('framereceived', event => console.log(event.payload)); ws.on('close', () => console.log('WebSocket closed')); }); Network events You can monitor all the Requests and Responses // Subscribe to 'request' and 'response' events. page.on('request', request => console.log('>>', request.method(), request.url())); page.on('response', response => console.log('<<', response.status(), response.url())); await page.goto('https://example.com'); Or wait for a network response after the button click with page.waitForResponse() // Use a glob URL pattern. Note no await. const responsePromise = page.waitForResponse('**/api/fetch_data'); await page.getByText('Update').click(); const response = await responsePromise; // or use a RegExp. Note no await. const responsePromise = page.waitForResponse(/\.jpeg$/); await page.getByText('Update').click(); const response = await responsePromise; // Use a predicate taking a Response object. Note no await. const responsePromise = page.waitForResponse(response => response.url().includes(token)); await page.getByText('Update').click(); const response = await responsePromise; Page loading Playwright can load page and wait for the target elements to become actionable If clicking an element could trigger multiple navigations then use waitForURL() // goto() await page.goto('https://example.com') await page.getByText('Example Domain').click() // waitForURL() await page.getByText('Click me').click() await page.waitForURL('**/login') Page model Page objects may simplify authoring by creating a higher-level API for your page PlaywrightDevPage helper class to encapsulate common operations on the playwright.dev page. // playwright-dev-page.ts import { expect, type Locator, type Page } from '@playwright/test'; export class PlaywrightDevPage { readonly page: Page; readonly getStartedLink: Locator; readonly gettingStartedHeader: Locator; readonly pomLink: Locator; readonly tocList: Locator; constructor(page: Page) { this.page = page; this.getStartedLink = page.locator('a', { hasText: 'Get started' }); this.gettingStartedHeader = page.locator('h1', { hasText: 'Installation' }); this.pomLink = page.locator('li', { hasText: 'Guides', }).locator('a', { hasText: 'Page Object Model', }); this.tocList = page.locator('article div.markdown ul > li > a'); } async goto() { await this.page.goto('https://playwright.dev'); } async getStarted() { await this.getStartedLink.first().click(); await expect(this.gettingStartedHeader).toBeVisible(); } async pageObjectModel() { await this.getStarted(); await this.pomLink.click(); } } use the custom page in your tests import { test, expect } from '@playwright/test'; import { PlaywrightDevPage } from './playwright-dev-page'; test('getting started should contain table of contents', async ({ page }) => { const playwrightDev = new PlaywrightDevPage(page); await playwrightDev.goto(); await playwrightDev.getStarted(); await expect(playwrightDev.tocList).toHaveText([ `How to install Playwright`, `What's Installed`, `How to run the example test`, `How to open the HTML test report`, `Write tests using web first assertions, page fixtures and locators`, `Run single test, multiple tests, headed mode`, `Generate tests with Codegen`, `See a trace of your tests` ]); }); test('should show Page Object Model article', async ({ page }) => { const playwrightDev = new PlaywrightDevPage(page); await playwrightDev.goto(); await playwrightDev.pageObjectModel(); await expect(page.locator('article')).toContainText('Page Object Model is a common pattern'); }); Multiple browser contexts test('admin and user', async ({ browser }) => { // Create two isolated browser contexts const adminContext = await browser.newContext() const userContext = await browser.newContext() // Create pages and interact with contexts independently const adminPage = await adminContext.newPage() const userPage = await userContext.newPage() }) Multiple pages Each BrowserContext can have multiple pages A Page refers to a single tab or a popup window within a browser context It should be used to navigate to URLs and interact with the page content. // Create a page. const page = await context.newPage(); // Navigate explicitly, similar to entering a URL in the browser. await page.goto('http://example.com'); // Fill an input. await page.locator('#search').fill('query'); // Navigate implicitly by clicking a link. await page.locator('#submit').click(); // Expect a new url. console.log(page.url()); Each browser context can host multiple pages (tabs) // Create two pages const pageOne = await context.newPage(); const pageTwo = await context.newPage(); // Get pages of a browser context const allPages = context.pages(); Link opened in a new page target="_blank" // Start waiting for new page before clicking. Note no await. const pagePromise = context.waitForEvent('page'); await page.getByText('open new tab').click(); const newPage = await pagePromise; // Interact with the new page normally. await newPage.getByRole('button').click(); console.log(await newPage.title()); If the action that triggers the new page is unknown, the following pattern can be used. // Get all new pages (including popups) in the context context.on('page', async page => { await page.waitForLoadState(); console.log(await page.title()); }); If the page opens a pop-up // Start waiting for popup before clicking. Note no await. const popupPromise = page.waitForEvent('popup'); await page.getByText('open the popup').click(); const popup = await popupPromise; // Interact with the new popup normally. await popup.getByRole('button').click(); console.log(await popup.title()); If the action that triggers the popup is unknown, the following pattern can be used. // Get all popups when they open page.on('popup', async popup => { await popup.waitForLoadState(); console.log(await popup.title()); }); Cookie modification test.describe('user/me', () => { test.use({ baseURL: process.env.BACKEND_BASE_URL }) test('should throw 403 status if ltpa token is invalid', async ({ request, page }) => { await page.context().clearCookies() await page.context().addCookies([ { name: 'LtpaToken', value: 'invalid token', domain: '/webapp.com', path: '/', httpOnly: true, secure: true, expires: -1 } ]) const res = await page.request.get('user/') expect(res.status()).toBe(403) const data = await res.json() expect(data.message).toContain('Expired LTPA Token') })