Playwright tests use a narrow slice of JavaScript: variables, functions, objects, arrays, destructuring, and async/await. Most beginners either skip the language entirely and can't debug when something breaks, or try to learn all of JavaScript first and never get to writing tests. This article covers only what appears in real Playwright test code, with async/await as the priority because forgetting await before a browser call is the most common cause of intermittent, hard-to-trace failures.

Why JavaScript for QA specifically

Playwright tests are JavaScript (or TypeScript) files. When you write:

``typescript

await page.click('.submit-button');

`

That await keyword, that arrow function, that string argument are all JavaScript. If you don't understand what await does, you can't debug a test that hangs. If you don't understand variables, you can't extract a value from the page and check it.

The good news: QA automation uses a small, predictable slice of JavaScript. You're not building web apps. You're writing scripts that interact with them. The code patterns repeat.

Variables: let and const

Two ways to store values:

`typescript

// const: value doesn't change after assignment

const baseUrl = 'https://lab.becomeqa.com';

const timeout = 5000;

// let: value can change

let loginAttempts = 0;

loginAttempts = loginAttempts + 1;

`

In test code, use const by default. Only reach for let when you need to reassign.

You'll also see var in older code. Ignore it. It has scoping behavior that causes bugs. const and let replaced it.

Data types

The types you'll actually use in test code:

`typescript

// String — text

const username = 'admin@becomeqa.com';

const password = 'testpass123';

// Number — for timeouts, counts, price checks

const timeout = 30000; // 30 seconds in ms

const itemCount = 5;

// Boolean — for conditions

const isLoggedIn = true;

const hasError = false;

// null / undefined — absence of value

// You'll see these in assertions

const itemTitle = null; // intentionally empty

`

In Playwright tests, strings and numbers show up everywhere. Booleans appear in conditions. null appears when something isn't found.

Functions

Functions package code so you can reuse it. Two styles you'll see:

`typescript

// Regular function (older style, still common)

function generateEmail() {

return user_${Date.now()}@test.com;

}

// Arrow function (modern, common in Playwright code)

const generateEmail = () => user_${Date.now()}@test.com;

// Arrow function with a body (multiple lines)

const login = async (page, email, password) => {

await page.getByRole('button', { name: 'Login' }).click();

await page.getByLabel('Email').fill(email);

await page.getByLabel('Password').fill(password);

await page.getByRole('button', { name: 'Submit' }).click();

};

`

Arrow functions are the default in modern Playwright code. async before the function means it uses await inside, covered in the Async/Await in Plain English (for Testers Who Get Tripped Up by Promises).

Objects

Objects group related data:

`typescript

// An object with properties

const user = {

email: 'admin@becomeqa.com',

password: 'testpass123',

role: 'admin',

};

// Access a property with dot notation

console.log(user.email); // 'admin@becomeqa.com'

// Or bracket notation

console.log(user['email']); // same result

`

In Playwright, objects appear constantly as options passed to functions:

`typescript

// { name: 'Login' } is an object

await page.getByRole('button', { name: 'Login' }).click();

// { data: { title: 'Tokyo' } } is a nested object

await request.post('/api/items', { data: { title: 'Tokyo' } });

`

Arrays

Arrays store lists of values:

`typescript

const statuses = ['Planned', 'In Progress', 'Completed'];

const prices = [9.99, 14.99, 29.99];

// Access by index (starts at 0)

console.log(statuses[0]); // 'Planned'

console.log(statuses[2]); // 'Completed'

// Length

console.log(statuses.length); // 3

`

In tests, arrays appear when you get multiple elements from the page:

`typescript

// Get all row texts from a table

const rows = await page.getByRole('row').allTextContents();

// rows is now an array: ['Tokyo', 'Paris', 'London']

expect(rows).toContain('Tokyo');

`

if / else: making decisions

`typescript

// Basic condition

if (user.role === 'admin') {

await page.goto('/admin');

} else {

await page.goto('/dashboard');

}

// Checking for something missing

const errorMessage = await page.getByText('Invalid credentials').isVisible();

if (errorMessage) {

console.log('Login failed as expected');

}
`

The === comparison (triple equals) checks both value AND type. Always use === instead of == in JavaScript.

Template literals: building strings

Old way (confusing with lots of variables):

`typescript

const url = 'https://' + environment + '.lab.becomeqa.com/item/' + itemId;

`

Modern way with backticks:

`typescript

const url = https://${environment}.lab.becomeqa.com/item/${itemId};

`

Template literals (backtick strings) let you embed variables with ${}. You'll see them constantly in test code for building URLs, messages, and test data.

Destructuring: extracting from objects and arrays

This pattern appears everywhere in Playwright fixtures and Page Object Model code:

`typescript

// Instead of this:

const email = user.email;

const password = user.password;

// You can do this (destructuring):

const { email, password } = user;

`

With arrays:

`typescript

const [first, second] = ['Tokyo', 'Paris', 'London'];

// first = 'Tokyo', second = 'Paris'

`

In Playwright, you'll see this constantly in test parameters:

`typescript

// The { page, request } here is destructuring from the fixture object

test('should work', async ({ page, request }) => {

// page and request are available directly

});

`

Modules: import and export

Playwright test files import from the framework and from your own code:

`typescript

// Import from the Playwright package

import { test, expect } from '@playwright/test';

// Import your page object

import { LoginPage } from '../pages/LoginPage';

// Import test data

import { testUsers } from '../data/users';

`

When you create a helper file, you export from it:

`typescript

// In utils/helpers.ts

export const baseUrl = 'https://lab.becomeqa.com';

export function generateTestEmail() {

return test_${Date.now()}@example.com;

} ` import brings things in. export makes things available to import elsewhere. That's the whole system.

What about loops?

You'll occasionally need loops in test code, but less than you'd expect. The main cases:

`typescript

// Loop through an array of test data

const destinations = ['Tokyo', 'Paris', 'London'];

for (const destination of destinations) {

await page.getByLabel('Destination').fill(destination);

await page.getByRole('button', { name: 'Save' }).click();

}
`

The for...of loop is the clearest for iterating arrays. forEach, map, filter are covered in a separate article on array methods.

The one thing that confuses everyone: async/await

Almost every line of Playwright code has await in front of it:

`typescript

await page.goto('https://lab.becomeqa.com');

await page.getByRole('button', { name: 'Login' }).click();

await expect(page.getByText('Dashboard')).toBeVisible();

`

The rule is simple: any Playwright method that interacts with the browser returns a Promise, and you need await to wait for it to finish.

If you forget await, the test usually runs the next line before the current action completes, causing confusing, intermittent failures.

This is important enough to have Async/Await in Plain English (for Testers Who Get Tripped Up by Promises). The short version: always put await before Playwright calls. TypeScript will warn you if you forget.

What you don't need (yet)

Things QA engineers frequently try to learn before they need them, which creates unnecessary confusion:

  • Classes: useful for Page Object Model, but you can understand POM without deeply knowing classes first
  • Promises/then/catch: async/await is the modern replacement; learn await first
  • Closures, prototypes, the event loop: JavaScript internals that rarely affect test code
  • DOM manipulation: you're using Playwright to interact with the DOM; you don't manipulate it directly

Learn these when a specific problem requires them. Not before.

A complete test using everything above

Here's a Playwright test that uses every concept from this article:

`typescript

import { test, expect } from '@playwright/test';

// Object: user data

const testUser = {

email: 'admin@becomeqa.com',

password: 'testpass123',

};

// Template literal: dynamic URL

const baseUrl = 'https://lab.becomeqa.com';

test('user can add a travel item', async ({ page }) => {

// Destructuring: extract email and password from object

const { email, password } = testUser;

// Functions (Playwright methods) with await

await page.goto(baseUrl);

await page.getByRole('button', { name: 'Login' }).click();

await page.getByLabel('Username').fill(email);

await page.getByLabel('Password').fill(password);

await page.getByRole('button', { name: 'Submit' }).click();

// String variable

const destination = 'Tokyo';

await page.getByRole('button', { name: 'Add Item' }).click();

await page.getByLabel('Destination').fill(destination);

// Boolean: checking if something is visible

const saveButton = page.getByRole('button', { name: 'Save' });

// Conditional

if (await saveButton.isVisible()) {

await saveButton.click();

}

// Array: allTextContents returns an array

const rows = await page.getByRole('row').allTextContents();

// expect on the array

expect(rows.some(row => row.includes(destination))).toBeTruthy();

});

``

This is a real, complete test. The JavaScript it uses is exactly what's covered in this article. Nothing more.

FAQ

Do I need to complete a full JavaScript course before starting Playwright?

No. Learn JavaScript concepts as you need them. Start writing Playwright tests immediately using the patterns above, and look things up when you're confused. "Learn JS first, then Playwright" leads to many people learning JS for months and never getting to Playwright.

JavaScript or TypeScript for Playwright?

TypeScript. Playwright's official documentation uses TypeScript. TypeScript adds types to JavaScript, which means your editor catches mistakes before you run the tests. The difference is small for beginners. See TypeScript for QA: Why Static Types Make Your Tests Better for the specifics.

What if I don't understand a piece of Playwright code?

Look it up. MDN Web Docs (developer.mozilla.org) is the best reference for JavaScript language features. The Playwright documentation (playwright.dev) covers Playwright-specific APIs. Between those two sources, every question about test code has an answer.

How long does it take to learn enough JavaScript to write Playwright tests?

2–4 weeks of daily practice is usually enough to be productive. You'll keep learning specific things as you encounter them, but 2–4 weeks of focused work on the concepts in this article gets you writing and understanding real tests.

The concept that trips up most beginners most often is async/await. It underlies every Playwright interaction. Understanding it unlocks debugging.

→ See also: Async/Await in Plain English (for Testers Who Get Tripped Up by Promises) | TypeScript for QA: Why Static Types Make Your Tests Better