Automation Prerequisites · Lesson 2

JavaScript Basics for Testers

You do not need to become a programmer to automate tests. You need to read a test script and know what each line does — and understand the one idea that trips up every beginner: why tests have to wait. This lesson gives you exactly that.

Prerequisites Foundations for Non-Coders — Lesson 2 of 3 ~25 min read · ~60 min with exercises

1 The Hook

Tama moved from manual testing into a small automation squad at a Christchurch fintech. His first real test logged into a fictional banking app, clicked “Pay”, and checked that a success message appeared. He wrote it, ran it, and it passed. He ran it again — it failed. Ran it a third time — passed. Same code, same app, different result each time. To a manual tester, that is maddening: nothing changed, so why did the answer change?

He showed it to a senior tester, who read the script in about ten seconds and said: “You’re not waiting for the message. The app takes a moment to show it, and your test checks before it appears. Sometimes the message is up in time, sometimes it isn’t — that’s your flaky test.”

The fix was one word: await. The script needed to wait for the success message to appear before checking for it. Tama had written the steps in the right order, but he had not told the test to pause for the app to catch up — and that gap between “asking the app to do something” and “the app actually finishing it” is the single most important idea in reading test code.

Here is the lesson hidden in that story: a test script is not a list of instant actions. Some steps take time, and the test has to wait for them. Once you understand variables, functions, and waiting, you can read almost any test script and see what it is doing — and why it sometimes goes flaky. That is what this lesson teaches.

2 The Rule

A test script is made of three things you can learn quickly: variables (names for values), functions (named groups of steps), and waiting (telling the test to pause until the app finishes). The hard part is not the syntax — it is remembering that many test actions take time, so you must await them, or the test races ahead and checks before the app is ready.

3 The Analogy

Analogy

Ordering a flat white at a busy Wellington café.

You walk up and order a flat white. The barista does not hand it to you instantly — they take the order, and you step aside to wait. If you marched straight to the pickup counter and grabbed the first cup you saw before yours was made, you would either get nothing or someone else’s drink. You have to wait for your order to be ready before you collect it.

A test script works the same way. When the test clicks “Pay”, that is placing an order — the app needs a moment to process it and show the result. await is you stepping aside and waiting for your name to be called. A test without await is the person snatching a cup before it is poured: sometimes the drink happens to be ready and it works, sometimes it is not and everything goes wrong. That randomness is exactly what a flaky test feels like.

4 Variables — Naming Things

A variable is just a name you give to a value so you can use it again. In JavaScript you create one with const or let, a name, an equals sign, and the value:

const username = 'aroha@example.nz';  // a name for a piece of text
const amount = 120;  // a name for a number
let isLoggedIn = false;  // a name for a true/false value

The difference between the two: const is for a value that will not be reassigned (most of the time, use this), and let is for a value you expect to change later. The values themselves come in a few everyday types: text (called a string, always in quotes), numbers, and true/false (called a boolean). The // marks a comment — a note for humans that the computer ignores.

Why testers care: test scripts use variables to hold things like the username they log in with, the expected total, or the element they just found, so the same value can be reused across several steps without retyping it.

5 Functions — Reusable Steps

A function is a named group of steps you can run whenever you want. Think of it as a saved procedure — like a reusable test step. You define it once and “call” it by name as many times as you like:

function greet(name) {
  return 'Kia ora, ' + name;
}

greet('Aroha');  // calling it — gives back "Kia ora, Aroha"

Read it piece by piece: function declares it, greet is the name, (name) is the parameter — a value you hand in when you call it — and the part in curly braces { } is the body, the steps it runs. return is the answer it hands back. When you write greet('Aroha') you are calling the function with 'Aroha' as the name.

You will also meet a shorter style called an arrow function, which is the same idea written more compactly — you will see it a lot in test files:

const greet = (name) => 'Kia ora, ' + name;

Why testers care: every test you write is a function. When you see test('user can log in', async () => { ... }), the part in braces is the body of a function — the steps of that one test.

6 Common Syntax You’ll See

You do not need to memorise a language. You need to recognise the handful of symbols and shapes that show up in every test file. Here is the cheat sheet:

{ } curly braces — wrap a block of steps that belong together (a function body, a test body).
( ) round brackets — hold the values you pass in when calling something.
; semicolon — marks the end of one statement (one step).
. dot — “do this thing to that thing”, e.g. page.click(...) means “on the page, run click”.
'text' or "text" quotes — mark a string (a piece of text). Single or double, both fine.
// note a comment — a note for humans, ignored by the computer.
=== three equals — “is exactly equal to”, used when checking a result.

The dot is worth dwelling on, because test scripts are full of it. page.goto('...'), page.click('#buy-now'), expect(total).toBe(120) — each is “take the thing on the left, run the action on the right.” Once that clicks, most test lines read almost like English: “on the page, click the buy-now button.”

7 Async, Await & Promises — Why Tests Wait

This is the section that matters most, because it is the idea behind nearly every flaky test. Some actions finish instantly — adding two numbers. Others take time — loading a page, clicking a button that talks to a server, waiting for a message to appear. JavaScript handles the slow ones with three connected words:

  • A promise is an IOU. When you ask the app to do something slow, it does not hand back the result immediately — it hands back a promise: “I’ll get back to you when this is done.” (Like the café taking your order.)
  • await means “pause here until that promise is fulfilled.” It is you waiting for your name to be called before collecting the coffee. After an await, you know the action has actually finished.
  • async is a label on a function that says “this function contains waiting.” You can only use await inside a function marked async. That is why almost every test starts with async () => { ... }.

See the difference in one example. Without await, the test races ahead:

page.click('#pay');  // asks the app to pay — but does NOT wait
expect(message).toBe('Payment complete');  // checks before the app is ready → FLAKY

With await, the test waits for each slow step to finish before moving on:

await page.click('#pay');  // waits for the click to be handled
await expect(page.locator('.message')).toHaveText('Payment complete');  // waits for the message → reliable

That one word was Tama’s whole bug. The actions were in the right order; the test just was not waiting for the slow ones to finish.

Pro tip: When a test passes sometimes and fails other times with nothing changing, a missing await is the first thing to suspect. “Flaky” almost always means “the test checked before the app had finished.” Look for an action that talks to the app — click, fill, navigate — that is missing its await.

8 Reading a Simple Test Script

Now put it together. Here is a complete, simple test for a fictional Kiwibank online-banking login. You will not write this yet — the goal is to read it and narrate what each line does.

test('customer can log in', async () => {
  const username = 'aroha@example.nz';  // 1. name the login email
  await page.goto('https://bank.example.nz/login');  // 2. open the login page, wait for it
  await page.fill('#username', username);  // 3. type the email into the username field
  await page.fill('#password', 'CorrectHorse1');  // 4. type the password
  await page.click('#login-btn');  // 5. click Log in, wait for it
  await expect(page.locator('.welcome')).toBeVisible();  // 6. wait for & check the welcome message
});

Read it as a story: this is a test named “customer can log in”; it is async because it waits. It names the login email in a variable, opens the page, fills the username and password fields, clicks the login button, then waits for and checks that a welcome message appears. Notice every action that talks to the app is preceded by await — that is what makes it reliable. The #username, #password, and #login-btn are CSS-selector locators — exactly what you learned in Lesson 1. The two lessons meet right here.

9 Common Mistakes

🚫 Forgetting await on an action that talks to the app

Why it happens: The steps are in the right order, so it looks correct — and it even passes sometimes.
The fix: Any action that loads, clicks, fills, or checks the live app takes time and returns a promise. Put await in front of it so the test waits for it to finish. A missing await is the number-one cause of flaky tests.

🚫 Confusing = with ===

Why it happens: They look almost the same.
The fix: A single = assigns a value to a variable (const x = 5). Three === checks whether two things are equal, which is what you want in a test assertion. Using one where you mean the other is a classic beginner bug.

🚫 Fixing flakiness with a fixed sleep

Why it happens: “Just wait 3 seconds” feels like it solves the timing problem.
The fix: A fixed wait is slow when the app is fast and still fails when the app is slow. Prefer an await that waits for the actual condition — the element appearing, the text showing — not a guess at how long it takes.

🚫 Thinking you must memorise the whole language

Why it happens: JavaScript is big, so it feels like you need all of it.
The fix: Reading tests needs a small slice: variables, functions, the dot, and await. Learn to read first — understanding what a script does is far more valuable early on than being able to write everything from scratch.

10 Now You Try

Three graded exercises — spot it, fix it, build it. Write your answer, run it for AI feedback, then compare to the model answer.

🔍 Exercise 1 of 3 — Narrate the Script

Read this test script for a fictional Trade Me “Watch this listing” flow. In plain English, say what each numbered line does, and explain why every action has await in front of it.

test('user can watch a listing', async () => {
  1  await page.goto('https://trademe.example.nz/item/12345');
  2  await page.click('[data-testid="watch-button"]');
  3  await expect(page.locator('.watch-status')).toHaveText('Watching');
});

Explain each line and why await is used:

Show model answer
Line 1: Opens (navigates to) the Trade Me listing page at that URL and waits for it to load.

Line 2: Clicks the "Watch" button, found by its data-testid locator "watch-button", and waits for the click to be handled.

Line 3: Waits for the element with class "watch-status" to show the text "Watching", and checks that it does. This is the assertion — the actual test of whether watching worked.

Why every action has await: Each of these actions talks to the live app and takes time — loading a page, handling a click, the status updating. They return promises (IOUs). await pauses the test until each one actually finishes, so the next line does not run before the app is ready. Without await on line 3, the test could check the status before it updated and fail randomly — a flaky test.

Bonus: the test is marked async (async () => {...}) precisely because it uses await inside.
🔧 Exercise 2 of 3 — Fix the Flaky Test

This test for a fictional IRD myIR payment passes sometimes and fails other times, with nothing about the app changing. Spot what is wrong and rewrite it so it is reliable. Explain your fix.

Flaky test:
test('payment shows confirmation', async () => {
  await page.goto('https://myir.example.nz/pay');
  page.click('#pay-now');  // <-- something is off here
  await expect(page.locator('.confirmation')).toBeVisible();
});

Rewrite the test and explain the fix:

Show model answer
My corrected test:
test('payment shows confirmation', async () => {
  await page.goto('https://myir.example.nz/pay');
  await page.click('#pay-now');            // added await
  await expect(page.locator('.confirmation')).toBeVisible();
});

What was wrong and why my fix works:
The click on #pay-now was missing its await. Clicking "Pay now" talks to the app and takes time, so it returns a promise. Without await, the test fires the click and immediately races on to check for the confirmation — sometimes the confirmation has appeared in time (test passes), sometimes it has not (test fails). That randomness is the flakiness. Adding await in front of page.click('#pay-now') makes the test wait for the click to be handled before moving on, so by the time it checks for the confirmation the app has had its chance to respond. Reliable every run.

Note: do NOT "fix" it by adding a fixed sleep — await the real action, not a guessed delay.
🏗️ Exercise 3 of 3 — Build a Login Test in Plain Pseudocode

You do not need perfect JavaScript yet. Write the steps of a login test for a fictional RealMe sign-in as a numbered list, marking which steps need to wait (would have await) and which do not. Then turn each step into a rough line of test code — close enough is fine.

The flow to test: open the RealMe login page, type a username and password, click “Log in”, then confirm a “Welcome back” heading appears.
Show model answer
Step 1 — Open the RealMe login page — needs await? YES (loading a page takes time) — rough code: await page.goto('https://realme.example.nz/login');

Step 2 — Type the username into the username field — needs await? YES (it talks to the app) — rough code: await page.fill('#username', 'aroha@example.nz');

Step 3 — Type the password into the password field — needs await? YES — rough code: await page.fill('#password', 'CorrectHorse1');

Step 4 — Click the "Log in" button — needs await? YES (the click is handled by the app) — rough code: await page.click('#login-btn');

Step 5 — Confirm the "Welcome back" heading appears — needs await? YES (you must wait for it to show before checking) — rough code: await expect(page.locator('h1')).toHaveText('Welcome back');

Marking guide: full marks for a sensible ordered flow where EVERY app-touching step (goto, fill, click, the final check) is marked as needing await, with locators that look like CSS selectors (#username, #login-btn). The key learning: in a UI test, nearly every step talks to the app and so needs await. The only things that would NOT need await are pure in-memory steps like naming a variable (const username = '...').

11 Self-Check

Click each question to reveal the answer.

Q1: What is the difference between const and let?

Both name a value. const is for a value you will not reassign — use it most of the time. let is for a value you expect to change later. Reaching for const by default and only using let when something genuinely changes is the normal habit.

Q2: In plain English, what does await do?

It pauses the test at that line until the slow action finishes — like waiting for your name to be called before collecting your coffee. After an await, you know the page has loaded, the click has been handled, or the element has appeared, so the next line can safely run.

Q3: A test passes sometimes and fails other times with no code or app changes. What is the most likely cause?

A missing await on an action that talks to the app. The test is racing ahead and checking before the app has finished. “Flaky” almost always means “checked too early.” Find the click, fill, or navigation that has no await in front of it.

Q4: Why is almost every test function marked async?

Because you can only use await inside a function marked async, and almost every UI test needs to wait for slow actions. So you see async () => { ... } at the top of nearly every test — it is the signal that the test contains waiting.

Q5: Why is adding a fixed 3-second sleep a poor way to fix a flaky test?

A fixed sleep is too slow when the app responds quickly — you waste time every run — and still fails when the app happens to be slower than your guess. Far better to await the actual condition (the element appearing, the text showing), which waits exactly as long as needed and no longer.

12 Interview Prep

Real questions asked in NZ QA interviews for junior automation roles. Read the model answers, then practise your own version.

“Explain what async and await do, like I’ve never seen code.”

Some actions in a test take time — loading a page, clicking a button that talks to a server. When you ask for one, you get back a promise, which is basically an IOU: “I’ll be done in a moment.” await means “pause here until that’s actually done” — like waiting for your coffee order before you collect it. async is the label on a function that says “this one does some waiting”, and you can only use await inside an async function. Together they make the test do its steps in the right order and not race ahead of the app.

“A teammate’s test is flaky. Where would you start?”

My first suspect is a missing await. Flaky usually means the test checked something before the app had finished — it passes when the app happens to be quick and fails when it’s slow. I’d read the script and look for any action that touches the app — a click, a fill, a navigation — that doesn’t have await in front of it. I’d add it, and I’d avoid the temptation to paper over it with a fixed sleep, because that just trades flakiness for slowness and can still fail.

“You’re coming from manual testing. How much JavaScript do you actually know?”

I’m honest that I’m early on, but I can read a test script and tell you what it does line by line — the variables, the functions, the locators, and crucially where it waits and why. I understand the async/await idea that causes most flaky tests, and I can spot a missing await. I’m focused on reading and fixing confidently first, and building up to writing more from scratch — which I think is the right order coming from a strong manual background.