Quick answer
You need ~20% of JavaScript: const/let, arrays, objects, arrow functions, destructuring,async/await, and ES modules. TypeScript adds tiny type annotations on top. That is enough to read, write and debug any Playwright test in production.
1. Variables and types
// Use const by default; let only when you reassign
const url = 'https://example.com';
let attempts = 0;
attempts = attempts + 1;
// Primitive types
const name: string = 'Ada';
const price: number = 19.99;
const inStock: boolean = true;
const missing: null = null;
const nothing: undefined = undefined;var has function scope and hoists in weird ways. const and let are block-scoped like every other modern language.2. Arrays and objects
// Array of test data
const users = ['ada@a.co', 'lin@b.co', 'ken@c.co'];
users.length; // 3
users[0]; // 'ada@a.co'
users.push('new@d.co'); // add
users.includes('lin@b.co'); // true
// The three you'll use constantly
users.map(u => u.toUpperCase());
users.filter(u => u.endsWith('.co'));
users.find(u => u.startsWith('ken'));
// Object = key/value bag (a JSON row)
const product = {
id: 42,
title: 'Blue Widget',
price: 9.99,
tags: ['new', 'sale'],
};
product.title; // 'Blue Widget'
product['price']; // 9.99
Object.keys(product); // ['id','title','price','tags']3. Functions and arrow functions
// Classic function
function add(a: number, b: number) {
return a + b;
}
// Arrow function — same thing, shorter
const add2 = (a: number, b: number) => a + b;
// Multi-line arrow
const login = async (page, email: string, password: string) => {
await page.goto('/login');
await page.getByLabel('Email').fill(email);
await page.getByLabel('Password').fill(password);
await page.getByRole('button', { name: 'Sign in' }).click();
};4. Destructuring and spread
// Every Playwright test file starts with this
import { test, expect } from '@playwright/test';
// Object destructuring in test callbacks
test('checkout works', async ({ page, request, context }) => {
// page, request, context pulled from fixtures object
});
// Array destructuring
const [first, second] = ['a', 'b', 'c'];
// Spread — clone and extend
const base = { headless: true };
const opts = { ...base, slowMo: 100 };5. async / await — the heart of every test
Browser and network calls return promises. await pauses execution until the promise settles.
test('waits properly', async ({ page }) => {
await page.goto('/'); // 1. wait for nav
await page.getByRole('link', { name: 'Docs' }).click(); // 2. wait for click
await expect(page).toHaveURL(/docs/); // 3. wait for URL
});
// Parallel awaits — start both, wait for both
const [resp1, resp2] = await Promise.all([
page.waitForResponse('**/api/user'),
page.getByRole('button', { name: 'Refresh' }).click(),
]);await is the #1 source of intermittent failures. The test moves on before the action finishes. Enable the ESLint rule @typescript-eslint/no-floating-promises.6. Modules and npm
// lib/api.ts — export
export async function createUser(email: string) { /* ... */ }
export const BASE_URL = 'https://api.example.com';
// tests/user.spec.ts — import
import { createUser, BASE_URL } from '../lib/api';
import { test } from '@playwright/test'; // 3rd-party from npm
test('signs up a user', async () => {
await createUser('demo@example.com');
});npm install foo— add a dependency topackage.json.npm run test— run a script defined inpackage.json.npx playwright test— run a CLI shipped by a package without installing it globally.
7. TypeScript — the 5 syntaxes you'll actually use
| Syntax | Meaning | Example |
|---|---|---|
: string | Variable is a string | const name: string = 'Ada' |
{ email: string } | Object shape | function login(u: { email: string; pwd: string }) |
string[] | Array of strings | const tags: string[] = [] |
? | Optional | { nickname?: string } |
type / interface | Reusable shape | type User = { id: number } |
8. Applied to a real Playwright test
import { test, expect } from '@playwright/test';
type Product = { name: string; qty: number };
const items: Product[] = [
{ name: 'Blue Widget', qty: 2 },
{ name: 'Red Gadget', qty: 1 },
];
test('adds multiple items to cart', async ({ page }) => {
await page.goto('/shop');
for (const { name, qty } of items) {
const card = page.getByRole('article', { name });
for (let i = 0; i < qty; i++) {
await card.getByRole('button', { name: 'Add to cart' }).click();
}
}
const totalItems = items.reduce((sum: number, i: Product) => sum + i.qty, 0);
await expect(page.getByTestId('cart-badge')).toHaveText(String(totalItems));
});9. Common mistakes to avoid
Use for...of with await, not .forEach() — forEach does not respect promises.
Always use strict equality ===. Loose == does type coercion (0 == '' is true).
Test isolation matters. Don't push to a module-level array from inside a test — create fresh data per test.
10. Hands-on task (20 minutes)
- 1
Open a Node REPL
Run
nodein your terminal. Try[1,2,3].map(n => n * 2)and{ a: 1, ...{ b: 2 } }. - 2
Write a helper
Create
lib/data.tsexportingrandomEmail()that returns`test+${Date.now()}@example.com`. Import it in a spec. - 3
Refactor a test with destructuring
Take a test that reads
user.emailanduser.password. Rewrite asconst { email, password } = user. - 4
Loop through datasets
Given an array of 3 test users, write a
for...ofthat logs each in and asserts a dashboard heading.
Ready for real locators? Continue to Module 3 / Lesson 1 — Playwright Locators.