JavaScript & TypeScript Basics
Playwright tests are just TypeScript code. Before you write a single test(...) block, you should be comfortable with variables, data types, control flow, functions, and modules. Tick each section as you cover it — your progress is saved locally in your browser.
Hello world
Open VS Code at an empty folder (e.g. D:\JS Fundamentals) and initialize a Node project:
D:\JS Fundamentals> npm init
# keep pressing enter, say yes at the end -- package.json is created
# create a folder for lessons, then a file
# lessons/lesson_1.js
console.log("Hello world !");
# run it
PS D:\JS Fundamentals\Lessons> node lesson_1.js
# output: Hello world !
Variables
A variable holds one value (or an object — see later). var is the old-style declaration, let is the modern, block-scoped form. Prefer let.
var firstName = "John"; let lastName = "Smith"; console.log(firstName); // declare now, assign later var age, dateOfBirth, sex; age = 5; sex = "Male"; console.log(age); // 5 age = 6; console.log(age); // 6
Constants
Use const when the binding should not be reassigned. Reassigning throws TypeError: Assignment to constant variable.
const occupation = "engineer"; occupation = "driver"; // TypeError: Assignment to constant variable. console.log(occupation);
Data types
JavaScript has dynamic types. TypeScript adds static typing on top.
var middleName = "David"; // string var yearInService = 5; // number var isHeMarried = false; // boolean var yearInMarriage = null; // null (intentionally empty) var numOfCars = undefined; // undefined (not assigned)
TypeScript equivalent:
let middleName: string = "David"; let yearInService: number = 5; let isHeMarried: boolean = false; let yearInMarriage: null = null; let numOfCars: undefined = undefined;
Concatenation
Glue strings together with +. Readable for two or three pieces; gets noisy beyond that — use interpolation below for anything bigger.
var item_name = "coffee";
var item_price = 50;
console.log("the price of your " + item_name + " is " + item_price + " dollars");
// the price of your coffee is 50 dollars
Interpolation (template literals)
Use backticks (`) — the key under Esc, left of 1. Embed expressions with ${ ... }.
var item_name = "tea";
var item_price = 5;
var msg = `my ${item_name} price is ${item_price} dollars`;
console.log(msg);
// my tea price is 5 dollars
Objects
An object holds multiple named values. Read / write fields with dot or bracket notation.
var customer = { firstName: "John", lastName: "Smith" };
// dot notation
console.log(customer.firstName); // John
customer.firstName = "Mike";
// bracket notation
console.log(`${customer["firstName"]} ${customer["lastName"]}`);
customer["lastName"] = "Brown";
Arrays
Ordered, zero-indexed list.
var cars = ["Volvo", "Toyota", "Tesla"]; console.log(cars[1]); // Toyota console.log(cars.length); // 3
Objects with arrays
Objects can hold arrays as values — the most common shape for real data.
var customer = {
firstName: "John",
lastName: "Smith",
cars: ["Volvo", "Toyota", "Tesla"]
};
console.log(customer.cars[1]); // Toyota
Relational operators
Compare two values. Result is always true or false.
console.log(10 < 75); // true console.log(10 > 75); // false console.log(10 <= 10); // true console.log(10 >= 20); // false
<— less than>— greater than<=— less than or equal>=— greater than or equal
Equality operators
Loose (==) compares value only, coercing types. Strict (===) compares value and type. Always prefer ===.
let x: any = 1; console.log(x == "1"); // true -- value match (loose) console.log(x === "1"); // false -- type differs (strict) console.log(x === 1); // true
Logical operators
console.log(true && true); // true (AND) console.log(true || false); // true (OR) console.log(!true); // false (NOT)
Conditional statements
let hour = 6;
if (hour >= 6 && hour < 12) {
console.log("Good morning");
} else if (hour >= 12 && hour < 18) {
console.log("Good afternoon");
} else {
console.log("Good evening");
}
for loop
for (let i = 0; i < 5; i++) {
console.log(i); // 0 1 2 3 4
}
for...of
Walk through every item of an iterable (array, string, set, etc.).
let cars = ["Volvo", "Toyota", "Tesla"];
for (let car of cars) {
console.log(car);
}
forEach
Array method that runs a callback per element. car is the iterator.
cars.forEach(car => console.log(car));
Loop break
for (let car of cars) {
if (car === "Toyota") {
break;
}
console.log(car); // prints only "Volvo"
}
Functions
function hello() {
console.log("Hello");
}
hello();
Function with argument
function printName(name: string) {
console.log(name);
}
printName("Wasim");
Return function
function multiplyBy2(num: number): number {
return num * 2;
}
console.log(multiplyBy2(5)); // 10
Arrow function
Shorter syntax. No own this binding (matters in callbacks).
const hello = () => {
console.log("Hello");
};
// concise body returns automatically
const square = (n: number) => n * n;
console.log(square(4)); // 16
Import / Export
Split code across files. Export from one module, import into another.
// helper.ts
export function printAge(age: number) {
console.log(`Age is ${age}`);
}
// main.ts
import { printAge } from "./helper";
printAge(30);
TypeScript extras you'll meet in Playwright
- Type annotations —
let name: string,const count: number = 0. - Interfaces — describe object shapes:
interface User { id: number; name: string; } - Async / await — almost every Playwright call returns a Promise:
await page.goto(url). - Optional chaining —
user?.address?.cityreturnsundefinedinstead of throwing. - Nullish coalescing —
const port = process.env.PORT ?? 3000;
Playwright with TypeScript
Playwright docsPlaywright is a Node.js end-to-end testing library by Microsoft. It drives Chromium, Firefox, and WebKit through a single API, ships with auto-waiting, network interception, video / trace recording, and parallel execution out of the box.
Setup · Node.js
Node.js is a runtime that runs JavaScript / TypeScript outside the browser. Playwright is published as a Node package, so Node is the first thing you install.
- Open nodejs.org/en/download.
- Click Windows and pick the x64 installer (Intel / AMD chips).
- Run the installer with default options. Verify in a new terminal:
node -v # e.g. v20.11.1 npm -v # e.g. 10.2.4
Setup · VS Code (+ Playwright extension)
Download Visual Studio Code (not Visual Studio — they are different products) from code.visualstudio.com.
Open VS Code → top menu Terminal → New Terminal and verify Node:
node -v npm -v # If npm fails on Windows with a script-execution policy error, open PowerShell as user and run: # Set-ExecutionPolicy RemoteSigned -Scope CurrentUser
Install the official Playwright extension: Playwright Test for VSCode (v1.1.17+). After installation a test-runner icon appears in the activity bar — from there you can run individual tests, see results, manage projects, and record new tests.
Setup · Git
Search Google for “Git SCM” → Git for Windows → install with defaults. Verify:
git -v # git version 2.45.1.windows.1
Clone the test application
We'll use a public practice app to learn against.
- Go to github.com, search
pw-practice. - Open the repo, click Code, and copy the HTTPS clone URL.
- Create a folder on your desktop (e.g.
D:\pw-practice) and open a terminal in it.
D:\pw-practice> git clone https://github.com/bondar-artem/pw-practice-app.git Cloning into 'pw-practice-app'... Receiving objects: 100% (8166/8166), 14.70 MiB Resolving deltas: 100% (5558/5558), done. D:\pw-practice> cd pw-practice-app
Run the test app
Open the folder in VS Code (File → Open Folder → D:\pw-practice\pw-practice-app) and install dependencies.
cd D:\pw-practice\pw-practice-app # 1. Clean npm cache (only if first install fails) npm cache clean --force # 2. Install dependencies (force flag handles peer-dep conflicts in this demo app) npm install --force # 3. Start the app npm start # -> http://localhost:4200/ — "compiled successfully" # Stop the app: Ctrl + C in the terminal # Restart: npm start (keep the terminal open while testing)
Troubleshooting: if npm install stalls, delete node_modules + the project folder, re-clone, and retry. A computer restart sometimes clears stuck file handles on Windows.
Init a Playwright project
In a separate folder (so the test code is its own project) initialize npm and add Playwright:
D:\pw-tests> npm init -y D:\pw-tests> npm init playwright@latest # answer the prompts: # - TypeScript # - tests folder name: tests # - add GitHub Actions workflow? no (for now) # - install Playwright browsers? yes # Playwright also installs browser binaries (Chromium, Firefox, WebKit).
Resulting layout:
pw-tests/ ├── tests/ │ └── example.spec.ts ├── tests-examples/ ├── playwright.config.ts ├── package.json └── tsconfig.json
playwright.config.ts
Central config — base URL, timeouts, retries, browser projects, reporters, trace / video / screenshot settings.
import { defineConfig, devices } from '@playwright/test';
export default defineConfig({
testDir: './tests',
fullyParallel: true,
retries: process.env.CI ? 2 : 0,
workers: process.env.CI ? 1 : undefined,
reporter: [['html'], ['list']],
use: {
baseURL: 'http://localhost:4200',
trace: 'on-first-retry',
screenshot: 'only-on-failure',
video: 'retain-on-failure',
actionTimeout: 10_000,
},
projects: [
{ name: 'chromium', use: { ...devices['Desktop Chrome'] } },
{ name: 'firefox', use: { ...devices['Desktop Firefox'] } },
{ name: 'webkit', use: { ...devices['Desktop Safari'] } },
],
});
First test
// tests/first.spec.ts
import { test, expect } from '@playwright/test';
test('home page shows welcome', async ({ page }) => {
await page.goto('/');
await expect(page).toHaveTitle(/Pw Practice App/i);
await page.getByRole('textbox', { name: 'Email' }).fill('test@test.com');
await page.getByRole('button', { name: 'Submit' }).click();
await expect(page.getByText('Welcome')).toBeVisible();
});
Run it:
npx playwright test npx playwright test --headed # show the browser npx playwright test --ui # interactive UI mode npx playwright show-report # open last HTML report
Built-in locators
Prefer Playwright's user-facing locators — they auto-wait and survive small DOM changes.
page.getByRole('button', { name: 'Sign in' });
page.getByText('Welcome back');
page.getByLabel('Password');
page.getByPlaceholder('Email address');
page.getByTestId('submit-btn');
page.getByAltText('Profile picture');
page.getByTitle('Close');
Fall back to CSS or XPath only when the user-facing locator does not fit.
XPath & axes
Absolute XPath starts at /html; relative XPath starts with // and is much more stable.
page.locator("//button[text()='Submit']");
page.locator("//input[@name='email']");
page.locator("//ul/li[3]"); // index
page.locator("//label[normalize-space()='Email']/following-sibling::input");
page.locator("//input[@id='email']/parent::div");
Useful axes: parent, ancestor, following-sibling, preceding-sibling, child, descendant, following, preceding.
CSS locators
page.locator('#login'); // id
page.locator('.btn-primary'); // class
page.locator('input.form-control'); // tag + class
page.locator('input[name="email"]'); // attribute
page.locator('form > button'); // direct child
page.locator('label + input'); // adjacent sibling
page.locator('li:nth-child(3)'); // pseudo-class
page.locator('li:has(span.tag-new)'); // Playwright :has()
page.locator('.row').filter({ hasText: 'Toyota' }); // Playwright filter
Actions
await page.getByRole('button', { name: 'Save' }).click();
await page.getByRole('button').dblclick();
await page.getByRole('button').click({ button: 'right' });
await page.getByRole('button').click({ modifiers: ['Shift'] });
await page.locator('#canvas').click({ position: { x: 10, y: 20 } });
await page.getByRole('button').click({ force: true });
await page.getByLabel('Email').fill('wasim@test.com');
await page.getByLabel('Search').type('laptop', { delay: 50 });
await page.getByLabel('Email').clear();
await page.keyboard.press('Enter');
await page.getByLabel('Subscribe').check();
await page.getByLabel('Subscribe').uncheck();
await page.getByLabel('Gender').first().check(); // radio
await page.getByRole('link', { name: 'Home' }).hover();
await page.getByRole('textbox').focus();
Dropdowns
// Native <select>
await page.getByLabel('Country').selectOption('IN'); // by value
await page.getByLabel('Country').selectOption({ label: 'India' }); // by text
await page.getByLabel('Country').selectOption({ index: 2 }); // by index
// Custom JS dropdown
await page.locator('.dropdown-trigger').click();
await page.locator('.dropdown-item').filter({ hasText: 'India' }).click();
File upload
await page.getByLabel('Avatar').setInputFiles('fixtures/avatar.png');
// multiple
await page.getByLabel('Docs').setInputFiles(['a.pdf', 'b.pdf']);
// clear
await page.getByLabel('Avatar').setInputFiles([]);
Extract text & iterate tables
const title = await page.locator('h1').textContent();
const visible = await page.locator('h1').innerText();
const value = await page.getByLabel('Email').inputValue();
const all = await page.locator('li').allTextContents();
const href = await page.getByRole('link', { name: 'Docs' }).getAttribute('href');
// Iterate a table
const rows = page.locator('table tbody tr');
const count = await rows.count();
for (let i = 0; i < count; i++) {
const cells = rows.nth(i).locator('td');
console.log(await cells.nth(0).textContent(), await cells.nth(1).textContent());
}
Pagination
const next = page.getByRole('button', { name: 'Next' });
const collected: string[] = [];
while (await next.isEnabled()) {
collected.push(...(await page.locator('.product-name').allTextContents()));
await next.click();
await page.waitForLoadState('networkidle');
}
collected.push(...(await page.locator('.product-name').allTextContents()));
console.log('Total:', collected.length);
Date pickers
// Native HTML5 date input
await page.getByLabel('Date').fill('2026-06-24');
// jQuery UI / Bootstrap calendar widget
await page.locator('#date').click();
await page.locator('.ui-datepicker-month').selectOption('5'); // June
await page.locator('.ui-datepicker-year').selectOption('2026');
await page.getByRole('link', { name: '24', exact: true }).click();
Dialogs & frames
// Alert / Confirm / Prompt
page.on('dialog', async dialog => {
console.log(dialog.type(), dialog.message());
await dialog.accept('optional prompt answer');
// or: await dialog.dismiss();
});
await page.getByRole('button', { name: 'Show alert' }).click();
// Frames
const frame = page.frameLocator('iframe#payment');
await frame.getByLabel('Card number').fill('4242424242424242');
Browser context, tabs & popups
// New tab / popup opened by the app
const [popup] = await Promise.all([
page.waitForEvent('popup'),
page.getByRole('link', { name: 'Open profile' }).click(),
]);
await popup.waitForLoadState();
await expect(popup).toHaveTitle(/Profile/);
// Two isolated users in one test
const ctxA = await browser.newContext();
const ctxB = await browser.newContext();
const userA = await ctxA.newPage();
const userB = await ctxB.newPage();
// Save / reuse login state
await context.storageState({ path: 'auth.json' });
Auto-waiting, timeouts & assertions
Before interacting, Playwright waits for the element to be Attached → Visible → Stable → Enabled → Editable. You rarely need explicit sleeps.
await page.waitForURL('**/dashboard');
await page.waitForLoadState('networkidle');
await page.waitForResponse(r => r.url().includes('/api/me') && r.ok());
// Page-level
await expect(page).toHaveURL(/dashboard/);
await expect(page).toHaveTitle(/Home/);
// Element-level
await expect(page.getByRole('alert')).toBeVisible();
await expect(page.getByLabel('Email')).toHaveValue('test@test.com');
await expect(page.locator('li')).toHaveCount(5);
// Soft (test continues after failure)
await expect.soft(page.locator('.banner')).toBeHidden();
Codegen — record your first test
npx playwright codegen http://localhost:4200 # A browser opens. Click around -- Playwright writes the spec for you in the Inspector. # Copy the generated code into tests/, clean it up, add assertions.
Trace viewer, screenshots & flaky tests
await page.screenshot({ path: 'home.png', fullPage: true });
await page.locator('#summary').screenshot({ path: 'summary.png' });
// In config: trace: 'on-first-retry' -- produces a .zip per failure
npx playwright show-trace trace.zip
// Mark known-bad tests so they don't fail the suite
test.fixme('legacy date picker breaks in webkit', async () => { /* ... */ });
test.slow(); // triples the timeout for the current test
Hooks, groups, tags & parallelism
import { test, expect } from '@playwright/test';
test.describe('Checkout', () => {
test.beforeAll(async () => { /* seed once */ });
test.beforeEach(async ({ page }) => { await page.goto('/cart'); });
test.afterEach(async () => { /* cleanup per test */ });
test.afterAll(async () => { /* teardown */ });
test('happy path @smoke', async ({ page }) => {
await expect(page.getByRole('heading')).toHaveText('Cart');
});
test.skip('legacy coupon @regression', async () => { /* ... */ });
});
// Filter by tag: npx playwright test --grep @smoke
// Workers: npx playwright test --workers 4
// CI sharding: npx playwright test --shard=1/3
Data-driven testing
// Inline
for (const term of ['laptop', 'mobile', 'headphones']) {
test(`search for ${term}`, async ({ page }) => {
await page.goto('/');
await page.getByPlaceholder('Search').fill(term);
await page.keyboard.press('Enter');
await expect(page.locator('.results')).toBeVisible();
});
}
// JSON
import data from './fixtures/search.json';
for (const row of data) { /* ...same shape... */ }
// Excel (via xlsx)
import * as XLSX from 'xlsx';
const wb = XLSX.readFile('fixtures/search.xlsx');
const rows = XLSX.utils.sheet_to_json<{ keyword: string }>(wb.Sheets['Sheet1']);
for (const { keyword } of rows) { /* ... */ }
Cheat sheet
# Install
npm init playwright@latest
# Run
npx playwright test
npx playwright test --headed
npx playwright test --ui
npx playwright test --project=chromium
npx playwright test --grep @smoke
npx playwright test path/to/file.spec.ts:42
# Reports / traces
npx playwright show-report
npx playwright show-trace trace.zip
# Record
npx playwright codegen http://localhost:4200
# Navigation
await page.goto(url);
await page.goBack();
await page.reload();
# Locate
page.getByRole / getByText / getByLabel / getByPlaceholder / getByTestId
page.locator('css or //xpath').filter({ hasText })
# Act
.click() .dblclick() .fill() .type() .check() .selectOption()
.setInputFiles() .hover() .focus() .press('Enter')
# Assert
expect(page).toHaveURL(...) toHaveTitle(...)
expect(locator).toBeVisible() toBeHidden() toHaveText() toHaveValue() toHaveCount()
# Config knobs
testDir, retries, workers, projects, use.baseURL,
use.trace, use.screenshot, use.video, use.actionTimeout