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