Day 3: JavaScript Control Flow — if/else, Switch, and Real Test Branches
<!– DIAGRAM_PROMPT Compact Excalidraw-style diagram, landscape 16:9, white background, thin black strokes, muted teal / amber / gray fills only. No 3D. No shadows. No stock photos. Small enough to sit above the fold as one blog figure.
Title (hand-lettered, 18px): “if / else vs switch — pick the branch your test needs”
LEFT CARD — title “if / else if / else”:
- Diamond: status === 200
- Yes arrow → teal box “assert OK body”
- No arrow → diamond status === 401
- Yes → amber box “re-auth / skip”
- No → gray box “fail loud with status”
Footer of left card (tiny): “Use for ranges, AND/OR, nested roles”
RIGHT CARD — title “switch (value)”:
- Rounded box “statusCode”
- Stacked cases: 200 / 201 / 204 grouped “success”
- 401 / 403 grouped “auth”
- 404 “not found”
- default “unknown — fail loud”
Footer of right card (tiny): “Use for discrete codes, browsers, env names”
BOTTOM RULE (one line): “Ranges + nested roles → if/else. Discrete values → switch + break + default.” –>
This is Day 3 of the 21-day JS to Playwright Framework series. One lesson a day. JavaScript first. TypeScript next. Playwright after that. A production framework by Day 21.
On Day 1 we put values in boxes — let, const, hoisting. On Day 2 we compared those values — operators, == versus ===. Today the script starts to decide.
A test that cannot decide is not a test. It is a recording. Real suites branch on HTTP status, on process.env.ENV, on browserName, on whether the user is a viewer or an admin. That is control flow. If you skip this day, every later Playwright helper you write will be a pile of happy-path console.log statements that collapse the first time staging returns 401.
The labs are not invented. They live in my LearningPlaywrightBatch repo:
chapter_05_Statements/—33_Statement.jsthrough41_IQ.jschapter_06_Switch_Statements/—42_Switch.jsthrough52_User_Input.js
Open those files. Run them with node. Then come back here and I will map every branch to a Playwright decision you will write for the rest of this series.
*Want to master this with real projects? Join the Playwright Automation Mastery course at The Testing Academy.*

Contents
What you will be able to do after Day 3
By the end of this post you can:
- Write a clean
if / else if / elsechain and explain why order matters. - Nest a role check inside a login check the way
app.vwo.comactually works. - Branch on an API status code without the
==trap from Day 2. - Use
switchwithbreak,default, and grouped cases. - Spot the interview bugs: missing
break, duplicatecase,"5"versus5,0versusfalse, empty array is truthy. - Pick
if/elseorswitchon purpose when the input is a status, an env name, or a browser.
That is the skill. Not the syntax. The skill is choosing the right branch and failing loud when no branch matches.
A statement is a decision, not a sentence
JavaScript is a sequence of statements. An expression produces a value. A statement *does* something with that value — declare, assign, return, or take a branch.
let age = 20; // statement: create a binding
age > 18 // expression: produces true or false
if (age > 18) { ... } // statement: choose a path
In a Playwright spec the same split shows up every five lines:
const status = response.status(); // statement
status === 200 // expression
if (status === 200) { // statement — now the test decides
await expect(response).toBeOK();
}
If you treat every line as “just code,” you will put side effects inside conditions and forget the else. Testers who write production suites treat the condition as a contract: this path is success, that path is auth, that path is “I do not know this status — fail and show me the body.”
Lab 33 — the smallest useful branch
File: chapter_05_Statements/33_Statement.js
let age = 20;
if (age > 18) {
console.log("Yes, Goa");
} else {
console.log("Go home!");
}
This is the whole idea. One condition. Two worlds. Age is greater than 18, or it is not. There is no third road in this lab, and that is the point: start with a binary gate before you invent a ladder.
How I use the same shape in a test. Feature flags, adult-only flows, “is this environment allowed to hit production data”:
const env = process.env.ENV ?? "local";
if (env === "prod") {
test.skip(true, "Do not run destructive cart tests on prod");
} else {
// staging / local — proceed
}
Same skeleton as Goa versus home. The condition is a boolean. The braces stay. The else is written even when it only logs, because six months later someone will add a third environment and they need a place to hang it.
Rules I enforce in code review:
- Always wrap the body in
{ }. We will see why in lab 39. - Prefer
===inside the condition. Day 2 is not optional. - Name the boolean so the
ifreads like English:if (isAdult),if (isProd),if (isLoggedIn).
Lab 34 — else if is a ladder, not a pile of ifs
File: chapter_05_Statements/34_If_else_If.js
let score = 78;
if (score >= 90) {
console.log("Grade: A — Excellent");
} else if (score >= 80) {
console.log("Grade: B — Good");
} else if (score >= 70) {
console.log("Grade: C — Average");
} else if (score >= 60) {
console.log("Grade: D — Below Average");
} else {
console.log("Grade: F — Fail");
}
Score 78 prints Grade: C — Average. Walk it:
78 >= 90? No.78 >= 80? No.78 >= 70? Yes. Stop. The rest of the ladder is dead.
Order is the test. If I put score >= 70 first, every 95 becomes a C. I see this exact bug when people grade HTTP statuses as ranges:
// WRONG — 201 is "created", but 200-range check must come in the right order
if (status >= 200) {
// this also swallows 404 if you are sloppy later
}
The Playwright version of a grade ladder is severity, pass rate, or retry budget:
function gradeRun(passRate) {
if (passRate >= 0.99) return "release";
else if (passRate >= 0.95) return "warn";
else if (passRate >= 0.80) return "block-hotfix";
else return "block-release";
}
Same lab. Different nouns. The else at the bottom is the fail-loud default. Never let an ungraded score fall through as undefined.
Lab 35 — nested if is how real products work
File: chapter_05_Statements/35_REAL_LIVE_Example.js
This is the first lab that looks like a product, not a textbook. I use app.vwo.com in class: viewer, editor, admin.
let isLoggedIn = true;
let userRole = "editor";
// app.vwo.com -> viewer, editor or admin ->
// viwer = limited view
// editor can edit and view
// admin can do all the things
if (isLoggedIn) {
if (userRole === "admin") {
console.log("admin can do all the things");
} else if (userRole === "editor") {
console.log("Welcome Editor — Edit access granted.");
} else if (userRole === "viewer") {
console.log("Welcome Viewer — Read-only access.");
} else {
console.log("No idea which role you are !");
}
} else {
console.log("You are not logged in!!");
}
Two questions, in this order:
- Are you in the building? (
isLoggedIn) - Which badge are you wearing? (
userRole)
You do not ask about the badge if the person is standing on the street. That is why the role ladder is nested. A flat if (userRole === "admin") without the login gate will “pass” an admin assertion on a logged-out session if the default role string happens to be hanging around from the previous test. I have failed that review comment more times than I can count.
Playwright mapping — same nest, real locators later in the series:
test("editor can open the campaign editor", async ({ page }) => {
const isLoggedIn = await page.getByRole("button", { name: "Logout" }).isVisible();
if (!isLoggedIn) {
throw new Error("Precondition failed: user is not logged in");
}
const role = process.env.USER_ROLE ?? "viewer";
if (role === "admin" || role === "editor") {
await expect(page.getByRole("button", { name: "Edit" })).toBeVisible();
} else if (role === "viewer") {
await expect(page.getByRole("button", { name: "Edit" })).toHaveCount(0);
} else {
throw new Error(`Unknown USER_ROLE: ${role}`);
}
});
Notice the last else. Lab 35 already has it: "No idea which role you are !". That sentence is more valuable than the happy path. Unknown role is a data bug. Fail. Do not pretend they are a viewer.
Lab 36 — API status branching, and the Day 2 landmine
File: chapter_05_Statements/36_API_IF_ELSE.js
let statusCode = 200; // APIs are working fine
if (statusCode == 200) {
console.log("Working fine!");
} else if (statusCode == "404") {
console.log("not found!");
} else {
console.log("Not mathcing status code!");
}
I leave this file in the batch on purpose. It is a trap.
statusCodeis the number200.- The 404 branch compares against the string
"404". - The operator is
==, not===.
If someone later writes statusCode = "200" from a header or a CSV, == 200 still passes. If they write statusCode = 404 as a number, == "404" also passes because == coerces. Your test is green. Your mental model is a lie.
Playwright’s APIResponse.status() returns a number. Keep it a number. Compare with ===.
import { test, expect } from "@playwright/test";
test("Restful Booker ping branches on status", async ({ request }) => {
const response = await request.get("https://restful-booker.herokuapp.com/ping");
const statusCode = response.status();
if (statusCode === 201) {
// ping is created
} else if (statusCode === 200) {
// some stacks return 200
} else if (statusCode === 404) {
throw new Error("Ping route missing — environment is wrong");
} else {
throw new Error(`Unexpected ping status: ${statusCode} body=${await response.text()}`);
}
});
Three habits from this lab:
===only.- Do not quote a status unless the API literally returned a string — and then convert it with
Number(status)once, up front. - The final
elsemust include the actual status (and ideally the body)."Not matching"without the number is how juniors waste an hour.
We will do a cleaner switch version of this same idea in lab 45.
Lab 37 — interview: what is truthy inside if ()
File: chapter_05_Statements/37_IQ_IF_ESLE.js — yes, the filename is IF_ESLE. The batch is a classroom, not a linter.
// true
if ("hello") console.log("String is truthy"); // "hello" = truthy
if (42) console.log("Number is truthy");
if ({}) console.log("Empty object is truthy!");
if ([]) console.log("Empty array is truthy!");
// false results
if ("") console.log("Won't print"); // "" -> falsy result
if (null) console.log("Won't print");
if (undefined) console.log("Won't print");
if (NaN) console.log("Won't print");
if (0) console.log("Won't print");
Memorize the six falsy values. Write them on a sticky note if you have to:
false, 0, "", null, undefined, NaN.
Everything else is truthy. Including []. Including {}. Including "false". Including "0".
This destroys automation in three places I keep seeing:
1. Empty locator lists. page.locator(".row").all() returning [] is truthy. if (rows) will not tell you the table is empty. Use rows.length === 0.
2. Status 0. A failed request in some clients gives status = 0. if (status) is false, so you skip both the success branch and the “we got a status” branch. Check status === 0 explicitly.
3. Empty error text. if (errorMessage) is a decent “do we have a string” check. if (errorMessage !== "") is clearer. I want the next reader to know I meant “non-empty string,” not “any truthy junk.”
Playwright assertion style — do not reinvent boolean checks the runner already has:
await expect(page.getByRole("row")).toHaveCount(0); // empty table
await expect(response).toBeOK(); // 200–299
expect(response.status(), "auth failed").toBe(401);
toBeOK() is an if (status >= 200 && status < 300) that the framework already wrote for you. Use it. Write your own if when the product has a *custom* contract — “create must be 201, not 200.”
Lab 38 — AND, OR, and the locked account
File: chapter_05_Statements/38_Logical_Op_IF_ELSE.js
let username = "Dev";
let password = "secure123";
let isAccountLocked = true;
if ((username === "Dev" && password === "secure123") && !isAccountLocked) {
console.log("Allowed to enter");
} else {
console.log("not allwed to enter");
}
Credentials match. Account is locked. Output: not allwed to enter.
This is the login precondition every banking app in this series will need. Username and password are not enough. You also need !isAccountLocked, isEmailVerified, isMfaComplete. One && chain. One else.
In Playwright I still keep the chain, but I name the pieces so the failure is readable:
const credentialsOk = username === "Dev" && password === "secure123";
const canEnter = credentialsOk && !isAccountLocked;
if (!canEnter) {
throw new Error(
`Login blocked. credentialsOk=${credentialsOk} locked=${isAccountLocked}`
);
}
Do not write if (a && b && c && d && e) with no names. When it fails on CI at 2 a.m., you want the log to say *which* boolean flipped.
Short-circuit reminder from Day 2: && stops at the first falsy. || stops at the first truthy. That is why process.env.BASE_URL || "http://localhost:3000" works — and why process.env.RETRIES || 2 is dangerous if retries can be 0. Use ?? when 0 is a real value. We will lean on that again on Day 17 when we write the config layer.
Lab 39 — the missing-brace interview
File: chapter_05_Statements/39_IQ.js
let x = 10;
if (x > 5)
console.log("x is big");
This runs. x is big prints. Interviewers then add a second line:
if (x > 5)
console.log("x is big");
console.log("this always runs"); // NOT inside the if
Only the first statement belongs to the if. The second is a sibling. In a test that looks like this:
if (status === 200)
console.log("ok");
await expect(body.id).toBeDefined(); // always runs, even on 500
you get a false failure or a false pass depending on what body is. Always use braces. I do not care that the language allows you to skip them. Classroom lab 39 exists so you fail this question once, in training, not in a release suite.
Lab 40 — the empty helper is the real lesson
File: chapter_05_Statements/40_REAL_IF_ELSE.js
This is the entire file:
// function validateForm(email, password) {
// return true;
// }
I am not going to pretend there are 40 more lines in GitHub. The file is a stub. That is the teaching moment.
Beginners extract validateForm, hard-code return true, and then write 15 Playwright tests on top of a helper that never rejects a bad email. The suite is green. Production accepts "not-an-email". The helper lied.
Finish the function. Do not leave it commented. A honest first version:
function validateForm(email, password) {
if (typeof email !== "string" || !email.includes("@")) {
return false;
}
if (typeof password !== "string" || password.length < 8) {
return false;
}
return true;
}
Then drive it from a Playwright form test later in the series:
if (!validateForm(email, password)) {
await expect(page.getByText("Invalid credentials")).toBeVisible();
} else {
await expect(page).toHaveURL(/dashboard/);
}
The branch belongs to the product rule, not to “the function exists.” If the helper cannot say no, delete it.
Lab 41 — three interview cases in one file
File: chapter_05_Statements/41_IQ.js
if ([]) {
console.log("True!");
}
// case 1
let response;
if (response) {
}
// case 2
if (response !== null) {
}
if (true) {
} else if (false) {
}
// else{
// }
Unpack it the way I do in class.
Empty array. [] is truthy. Printed: True!. We already covered this in lab 37. If you are checking “did the API return rows?”, check Array.isArray(response) && response.length > 0.
Case 1 — if (response). response is declared, never assigned. It is undefined. The body does not run. This is the “I forgot to await the API” bug. In Playwright: let response; if (response) after a failed goto looks the same. Always assign from await request.get(...) before you branch.
Case 2 — if (response !== null). undefined !== null is true. So this body does run, even though nobody set response. That is why I hate != null used as a sloppy “is it there?” check when you have not decided whether the empty state is null or undefined. Pick one. In this series, missing API body is undefined until parse, null only if the server sent JSON null. Compare with === against the one you mean.
Dead else if (false). If the first branch is if (true), the else if is unreachable. Interviewers ask “can I put code there?” You can. It will never run. Same as a Playwright if (true) { test.skip() } else { /* your actual test */ } — congratulations, you never test.
The commented else at the bottom of the file is another nudge: write the else, even if you think the if is exhaustive. Exhaustive ifs rot the week a new status appears.
When the value is discrete, stop stacking else if — use switch
if / else if is perfect for ranges (score >= 90) and compound booleans (isLoggedIn && role === "admin"). It gets ugly when the input is one of a known list: day number, HTTP status, browser name, env name.
That is chapter 06.
JavaScript switch compares with strict equality (===). Remember that. Labs 50 and 51 exist only to burn it in.
Lab 42 — switch with break, the happy path
File: chapter_06_Switch_Statements/42_Switch.js
// 0 - Sunday, 1 - Monday, 2 - Tue.....
let day = 2;
switch (day) {
case 0:
console.log("Sunday — Rest Day");
let a = 10;
let b = 30;
console.log(a + b);
break;
case 1:
console.log("Monday — Sprint Planning");
break;
case 2:
console.log("Tuesday — Development");
break;
case 3:
console.log("Wednesday — Code Review");
break;
case 4:
console.log("Thursday — Testing");
break;
case 5:
console.log("Friday — Deployment & Retro");
break;
case 6:
console.log("Saturday — Rest Day");
break;
default:
console.log("Invalid day value");
}
day = 2 prints Tuesday — Development and stops. break is the stop sign.
Playwright already thinks in days and slots when you schedule a nightly vs a smoke. More useful: map a numeric job index or a cron day to a suite tag.
const day = new Date().getDay(); // 0–6, same convention as the lab
switch (day) {
case 1:
process.env.GREP = "@smoke";
break;
case 5:
process.env.GREP = "@deploy-smoke";
break;
case 0:
case 6:
process.env.GREP = "@weekend-canary";
break;
default:
process.env.GREP = "@regression";
}
case 0 in the lab also shows you can run multiple statements inside a case — let a, let b, a log. Those let bindings are scoped to the whole switch block, which is why a second let a in another case can throw "Identifier has already been declared". If two cases need their own locals, wrap each case body in { } extra braces. Interviewers love that one. We will see a cousin in lab 49.
Lab 43 — the file is named “with Break” and it has no break
File: chapter_06_Switch_Statements/43_Switch_with_Break.js
let day = 2;
switch (day) {
case 0:
console.log("Sunday — Rest Day");
case 1:
console.log("Monday — Sprint Planning");
case 2:
console.log("Tuesday — Development");
case 3:
console.log("Wednesday — Code Review");
case 4:
console.log("Thursday — Testing");
case 5:
console.log("Friday — Deployment & Retro");
case 6:
console.log("Saturday — Rest Day");
default:
console.log("Invalid day value");
}
day is 2. Without break, JavaScript falls through. Output:
Tuesday — Development
Wednesday — Code Review
Thursday — Testing
Friday — Deployment & Retro
Saturday — Rest Day
Invalid day value
Read that last line again. Even default runs. Your “invalid day” log fires on a perfectly valid Tuesday.
This is the most common switch bug in SDET interviews and in production status mappers. You think you handled 200. You forgot break. Suddenly every 200 also “handles” 401, 404, and default, and the last assignment wins. Tests pass for the wrong reason.
Fall-through is only a feature when you group cases on purpose. That is lab 46. Until then, every case ends with break or return.
Lab 44 — default is for the value you did not plan
File: chapter_06_Switch_Statements/44_Switch_with_Default.js
Same week map, but let day = 10. There is no tenth day. default prints Invalid day value.
default is not optional in a test suite. An unknown env, an unknown browser, an unknown status — those are setup bugs. I want CI red, not a silent skip.
switch (process.env.ENV) {
case "local":
baseURL = "http://localhost:3000";
break;
case "staging":
baseURL = "https://stg.thetestingacademy.com";
break;
default:
throw new Error(`ENV must be local|staging, got ${process.env.ENV}`);
}
No case "prod" yet? Good. Default throws. That is how you keep a junior from exporting ENV=prod and wiping a catalog on Friday evening.
Lab 45 — switch on a real API status
File: chapter_06_Switch_Statements/45_Switch_REAL_EXAMPLE.js
// You are working API Validation
// response Code - 200, 404, 401, 403.....404
let responseCode = 404;
switch (responseCode) {
case 200:
console.log("200 Ok");
break;
case 404:
console.log("404 Not found!");
break;
default:
console.log("Not status code match");
}
This is lab 36, rewritten the way I want it in a framework. Discrete codes. Strict match. Default for everything else.
In Playwright I almost always group the success family and the auth family. That is the next lab’s idea, applied to HTTP:
async function assertApiBranch(response) {
const code = response.status();
switch (code) {
case 200:
case 201:
case 204:
return response;
case 401:
case 403:
throw new Error(`Auth branch: ${code}`);
case 404:
throw new Error("Resource not found");
default:
throw new Error(`Unmapped status ${code}: ${await response.text()}`);
}
}
We will grow this into ApiHelper on Day 19 against Restful Booker. Today, just get the branch right.
Do not switch (String(code)) “to be safe.” Pick a type. response.status() is a number. Keep the cases as numbers.
Lab 46 — grouped cases, the browser one you will copy
File: chapter_06_Switch_Statements/46_Switch_GroupCase.js
let browser = "Edge";
switch (browser) {
case "Chrome":
case "Edge":
case "Brave":
case "Opera":
console.log("Chromium Project!");
break;
case "Firefox":
console.log("Mozilla Project!");
break;
case "Safari":
console.log("Apple browser — uses JavaScriptCore engine");
break;
default:
console.log("Unknown browser — manual testing needed");
}
Edge matches the Chromium group. There is no code between case "Chrome": and case "Opera":. That empty fall-through is the intended kind. One body. One break.
This is the most useful switch in a Playwright repo. Playwright’s fixture gives you browserName: "chromium" | "firefox" | "webkit".
test("engine-specific screenshot baseline", async ({ page, browserName }) => {
switch (browserName) {
case "chromium":
// Chrome, Edge, Brave — same engine family in this suite
await expect(page).toHaveScreenshot("home-chromium.png");
break;
case "firefox":
await expect(page).toHaveScreenshot("home-firefox.png");
break;
case "webkit":
await expect(page).toHaveScreenshot("home-webkit.png");
break;
default:
throw new Error(`Unexpected browserName: ${browserName}`);
}
});
Or skip a known engine bug without hiding it:
test("file chooser on webkit", async ({ page, browserName }) => {
test.skip(browserName === "webkit", "webkit file chooser pending — see issue 184");
});
That last one is an if disguised as test.skip(condition, reason). Same control flow. Prefer the built-in when Playwright already offers it.
Grouped cases also map env aliases: case "stg": case "staging": case "qa":. One body. One URL.
Lab 47 — interview bug: fruit salad fall-through
File: chapter_06_Switch_Statements/47_IQ_BUG.js
let fruit = "banana";
switch (fruit) {
case "apple":
console.log("Apple selected");
case "banana":
console.log("Banana selected");
case "cherry":
console.log("Cherry selected");
case "date":
console.log("Date selected");
default:
console.log("Default reached");
}
fruit is "banana". No breaks. Output:
Banana selected
Cherry selected
Date selected
Default reached
The interviewer asks: “What prints?” If you say “Banana selected” only, you are not hired for that round.
Same bug in a test helper:
switch (status) {
case 200:
kind = "ok";
case 404:
kind = "missing";
default:
kind = "other";
}
// kind is always "other"
Every 200 becomes "other". Your allure report lies. Add break, or better, return kind from each case so you cannot fall through.
Lab 48 — switch (true) for ranges, used sparingly
File: chapter_06_Switch_Statements/48_IQ.js
let testScore = 85;
switch (true) {
case (testScore >= 95):
console.log("Outstanding — Top performer");
break;
case (testScore >= 85):
console.log("Excellent — Above expectations");
break;
case (testScore >= 70):
console.log("Good — Meets expectations");
break;
case (testScore >= 50):
console.log("Needs Improvement");
break;
default:
console.log("Unsatisfactory — Requires training");
}
switch (true) asks: which case expression is === true? First hit wins. 85 >= 95 is false. 85 >= 85 is true. Prints Excellent — Above expectations.
This is a party trick. I show it so you can read it in a codebase. I do not want it in a Playwright helper. Lab 34’s if / else if is clearer for ranges. switch (true) hides the fact that you are doing inequality, and juniors then write case testScore >= 85: *without* the outer switch (true) and wonder why nothing matches.
Allowed in this series: if / else if for ranges. switch for discrete values. If I catch switch (true) in a PR after Day 10, I will ask you to rewrite it.
Lab 49 — duplicate case, first one wins
File: chapter_06_Switch_Statements/49_Switch_IQ.js
let x = 10;
switch (x) {
case 10:
let b1 = 1;
console.log(b1);
break;
case 10:
let b2 = 2;
console.log(b2);
break;
default:
console.log("d");
}
// IT will allow you to have the duplicate case with first as the usage.
JavaScript allows two case 10: labels. It uses the first. Output is 1. The second case is dead code. The commented second default in the file is the same idea: you do not get two defaults.
Why this matters in tests: a merge conflict or a sloppy copy-paste gives you two case "staging": blocks. CI still goes green. The second URL never runs. I grep for duplicate cases in review. You should too.
Also note let b1 and let b2 — different names. If both cases used let b, the switch’s single scope would throw at parse time. Wrap case bodies in blocks if you reuse names:
case 10: {
let b = 1;
break;
}
case 11: {
let b = 2;
break;
}
Lab 50 — switch uses ===, so “5” is not 5
File: chapter_06_Switch_Statements/50_Switch_IQ.js
let value = "5";
switch (value) {
case 5:
console.log("Number 5 matched");
break;
case "5":
console.log("String '5' matched");
break;
}
// Output: "String '5' matched"
// switch uses ===, so "5" !== 5 (different types)
This is Day 2, inside a switch. == would have matched both. switch will not.
Where testers get bitten: CSV and .env values are strings. process.env.RETRIES is "2", not 2. browser.version() is a string. Query params are strings.
const retries = Number(process.env.RETRIES ?? "0");
switch (retries) {
case 0:
// local default
break;
case 2:
// CI
break;
default:
throw new Error(`RETRIES must be 0 or 2, got ${process.env.RETRIES}`);
}
Convert once. Switch on one type. Do not add both case 2: and case "2": “just in case.” That is how you hide a config bug.
Lab 51 — 0 is not false inside switch
File: chapter_06_Switch_Statements/51_Switch_IQ.js
let status = 0;
switch (status) {
case false:
console.log("false matched");
break;
case 0:
console.log("0 matched");
break;
}
// Output: "0 matched" (0 === 0, NOT 0 === false)
if (status) treats 0 as falsy. switch (status) does not send 0 to case false. Different tools, different rules.
Playwright example: some mock servers use 0 as “request never left the client.” That is a number, not a boolean.
switch (response.status()) {
case 0:
throw new Error("No HTTP status — network or mock is down");
case 200:
break;
default:
throw new Error(`Unexpected ${response.status()}`);
}
Do not write case false: here. You are not switching on a boolean.
Lab 52 — the user-input stub, finished as env input
File: chapter_06_Switch_Statements/52_User_Input.js
The whole file:
let a = 10;
Again, I will not invent a prompt() demo that is not in the repo. In Node test automation, “user input” is almost never prompt(). It is environment, CLI, or test info.
const a = Number(process.env.DAY_INDEX ?? "10");
That single binding is the start of a switch you already know how to write from labs 42–44. In Playwright:
import { test } from "@playwright/test";
test("branch on project name", async ({}, testInfo) => {
switch (testInfo.project.name) {
case "chromium":
case "firefox":
case "webkit":
break;
case "api":
test.skip(true, "UI spec — use tests/api");
break;
default:
throw new Error(`Unknown project ${testInfo.project.name}`);
}
});
Input in, discrete cases, default throws. That is the entire chapter.
if/else vs switch — the rule I want you to memorize
| Situation in a Playwright suite | Use | Why | ||
|---|---|---|---|---|
| Range: score, pass rate, timeout budget | if / else if | Inequalities. Lab 34. | ||
| Compound boolean: login + role + locked | if + && / `\ | \ | ` | Lab 35, 38. |
| Nested precondition then detail | Nested if | Lab 35. Login first. | ||
| Discrete status: 200, 401, 404 | switch + groups | Labs 45, 46. | ||
| Discrete browser / env / project name | switch + default throw | Lab 46, 52. | ||
| “Is this value present?” | Explicit === / .length | Labs 37, 41. Not if (arr). | ||
Range dressed up as switch (true) | Rewrite as if / else if | Lab 48. Clever ≠ clear. |
And the two operator rules, tattooed from Day 2:
if (x == y)is a bug until proven otherwise.switch (x)is always===. Convert types before you switch.
Three Playwright branches you will write all series
I want these three patterns in your muscle memory before Day 10 installs the runner.
1. Status code — API setup, not just UI
const response = await request.post("/auth/login", {
data: { username, password },
});
switch (response.status()) {
case 200:
token = (await response.json()).token;
break;
case 401:
throw new Error("Login fixture got 401 — check creds vault");
case 429:
test.skip(true, "Rate limited — retry job");
break;
default:
throw new Error(`Login ${response.status()}: ${await response.text()}`);
}
This is labs 36 and 45, production-shaped. Day 19 will drop it into ApiHelper.
2. Environment — config, not an afterthought
const env = process.env.ENV ?? "local";
if (env === "local") {
retries = 0;
} else if (env === "staging") {
retries = 1;
} else if (env === "ci") {
retries = 2;
} else {
throw new Error(`Unknown ENV ${env}`);
}
Ranges? No. Discrete names. I still use if / else if here when there are three values and each branch sets *several* fields. I use switch when I only pick a baseURL. Both are correct. Default throw is not optional.
3. Browser name — engine families
Copy lab 46. chromium / firefox / webkit. Group only when the assertion is truly engine-shared. Do not group webkit with chromium because “they look similar on my laptop.” Safari layout bugs are why we pay for a third project.
Common mistakes I still see in SDET interviews
I have run this chapter with thousands of students at The Testing Academy. The same seven failures show up.
==on status (lab 36). Number versus string. Green test, wrong contract.- No braces (lab 39). Second line always runs.
- No
break(labs 43, 47). Fall-through assigns the last value. - No
default(lab 44). Unknown env silently uses yesterday’s URL. if ([])/if (response)(labs 37, 41). Truthy empty array. Undefined that you treated as “present” with!== null.- Duplicate
case(lab 49). First wins. Second is a ghost. - Switching on a string env and casing
case 2:(lab 50). Types never meet.
If you can explain those seven with a Playwright example, you will clear the JavaScript round at most product companies hiring SDETs in India in 2026.
Day 3 homework — do this before you close the laptop
Do not just read. The batch files are runnable.
- Clone or pull LearningPlaywrightBatch.
- Run every file in chapter 05 and 06:
node chapter_05_Statements/33_Statement.js
node chapter_05_Statements/34_If_else_If.js
node chapter_05_Statements/35_REAL_LIVE_Example.js
node chapter_05_Statements/36_API_IF_ELSE.js
node chapter_05_Statements/37_IQ_IF_ESLE.js
node chapter_05_Statements/38_Logical_Op_IF_ELSE.js
node chapter_05_Statements/39_IQ.js
node chapter_05_Statements/40_REAL_IF_ELSE.js
node chapter_05_Statements/41_IQ.js
node chapter_06_Switch_Statements/42_Switch.js
node chapter_06_Switch_Statements/43_Switch_with_Break.js
node chapter_06_Switch_Statements/44_Switch_with_Default.js
node chapter_06_Switch_Statements/45_Switch_REAL_EXAMPLE.js
node chapter_06_Switch_Statements/46_Switch_GroupCase.js
node chapter_06_Switch_Statements/47_IQ_BUG.js
node chapter_06_Switch_Statements/48_IQ.js
node chapter_06_Switch_Statements/49_Switch_IQ.js
node chapter_06_Switch_Statements/50_Switch_IQ.js
node chapter_06_Switch_Statements/51_Switch_IQ.js
node chapter_06_Switch_Statements/52_User_Input.js
- In
36_API_IF_ELSE.js, changestatusCodeto404(number) and to"200"(string). Write down what prints and why. - Add
breakto47_IQ_BUG.jslocally. Confirm onlyBanana selectedprints. - Write a new file on your machine — not in my repo — called
status-branch.js. Accept a number.switchon200,201,401,403,404,default. Use grouped cases for 200/201 and 401/403. - Write
browser-branch.jsthat readsprocess.env.BROWSERand groups Chromium-family names the way lab 46 does. Default must throw. - Explain out loud, in one minute: when do you use
if/else, when do you useswitch?
If you cannot do step 7 without looking, do not go to Day 4 yet.
Key takeaways
- A statement decides. An expression only produces a value. Playwright tests are decision machines.
if / else if / elseis for ranges, nested preconditions, and AND/OR logic. Order the ladder from tightest to loosest.- Nest role inside login. Lab 35 is the VWO model: viewer, editor, admin.
- API status is a number. Compare with
===. Lab 36’s== "404"is the bug I want you to unlearn. - Six falsy values.
[]and{}are truthy. Empty table checks use.length, notif (rows). switchuses===. Alwaysbreakunless you are grouping cases on purpose. Always writedefaultand make unknown values throw.- Grouped cases are the right model for Chromium-family browsers and for 2xx / 4xx families.
- Interview bugs: missing break, duplicate case,
"5"versus5,0versusfalse,ifwithout braces,response !== nullwhenresponseisundefined. - Labs
40_REAL_IF_ELSE.jsand52_User_Input.jsare stubs in the batch. Finish helpers. Read env, notprompt().
FAQ
Should I use if/else or switch in Playwright tests?
Use if/else when the condition is a range or a mix of booleans — pass rate, “logged in AND editor,” “retries greater than zero.” Use switch when the value is one of a known list — HTTP status, browserName, ENV, project name. If you need switch (true), you wanted if/else.
Why did my switch run every case after the one I wanted?
You forgot break. JavaScript falls through on purpose. Lab 43 and lab 47 are the classroom proof. Grouped cases (lab 46) are the only time fall-through is a feature.
Does switch use == or ===?
===. "5" does not match case 5. 0 does not match case false. Convert process.env strings with Number(...) before you switch. Labs 50 and 51.
How do I branch on API status codes in Playwright?
const code = response.status() — that is a number. switch (code) with grouped 2xx, grouped 401/403, a 404 case, and a default that throws with await response.text(). Do not compare to "404" as a string.
How do I skip a test for one browser or one env?
Prefer test.skip(browserName === "webkit", "reason") or test.skip(process.env.ENV === "prod", "reason"). That is an if the runner understands. Use a full switch when each engine needs a *different* assertion, not just a skip.
Why is an empty array truthy?
Because the only falsy values are false, 0, "", null, undefined, and NaN. [] is an object. if ([]) runs. Check array.length. Labs 37 and 41.
What does switch (true) do?
Each case is an expression compared to true. The first expression that is strictly true wins. Lab 48 uses it for score bands. I still want if / else if for that job.
Can I have two case 10 labels?
The parser allows it. The first one wins. The second is dead. Lab 49. Treat it as a bug, not a pattern.
What is Day 4?
Loops and arrays — chapter_07 and chapter_08 in the same batch. You will iterate test data, walk locator lists, and stop using if (rows) when you meant for (const row of rows).
<script type=”application/ld+json”> { “@context”: “https://schema.org”, “@type”: “FAQPage”, “mainEntity”: [ { “@type”: “Question”, “name”: “Should I use if/else or switch in Playwright tests?”, “acceptedAnswer”: { “@type”: “Answer”, “text”: “Use if/else for ranges and compound booleans such as login plus role. Use switch for discrete values such as HTTP status, browserName, and ENV. Avoid switch (true); rewrite those as if/else if.” } }, { “@type”: “Question”, “name”: “Why did my JavaScript switch run every case after the match?”, “acceptedAnswer”: { “@type”: “Answer”, “text”: “JavaScript switch falls through until it hits break or return. Missing break is the most common interview bug. Only omit break when you intentionally group cases, for example Chrome, Edge, Brave, and Opera as one Chromium family.” } }, { “@type”: “Question”, “name”: “Does JavaScript switch use == or ===?”, “acceptedAnswer”: { “@type”: “Answer”, “text”: “Switch uses strict equality (===). The string \”5\” does not match case 5, and 0 does not match case false. Convert environment variables with Number() before switching.” } }, { “@type”: “Question”, “name”: “How do I branch on API status codes in Playwright?”, “acceptedAnswer”: { “@type”: “Answer”, “text”: “Read response.status() as a number. Switch on 200/201/204, 401/403, 404, and a default that throws with the body text. Never compare a numeric status to the string \”404\” with ==.” } }, { “@type”: “Question”, “name”: “How do I skip a Playwright test by browser or environment?”, “acceptedAnswer”: { “@type”: “Answer”, “text”: “Use test.skip(browserName === \”webkit\”, \”reason\”) or test.skip(process.env.ENV === \”prod\”, \”reason\”). Use a full switch when each browser needs a different assertion, and throw from default on unknown names.” } }, { “@type”: “Question”, “name”: “Why is an empty array truthy in a JavaScript if?”, “acceptedAnswer”: { “@type”: “Answer”, “text”: “Only false, 0, empty string, null, undefined, and NaN are falsy. An empty array is an object, so if ([]) runs. Check array.length or use Playwright toHaveCount(0).” } }, { “@type”: “Question”, “name”: “What is next after Day 3 of the JS to Playwright Framework series?”, “acceptedAnswer”: { “@type”: “Answer”, “text”: “Day 4 covers loops and arrays from LearningPlaywrightBatch chapters 07 and 08: walking test data, locator lists, and replacing sloppy if (rows) checks with real iteration.” } } ] } </script>
Tomorrow — Day 4: loops and arrays
Control flow without a loop is a single decision. Tomorrow we repeat the decision across a list.
Day 4 of this series takes chapter_07 (loops) and chapter_08 (arrays) from the same LearningPlaywrightBatch repo. You will for, while, and for...of over test data, walk rows in a table, and stop treating [] as a boolean. That is how a status branch becomes a data-driven suite.
Series hub (bookmark this): JavaScript → TypeScript → Playwright Advanced Framework — 21-Day Guide.
Master Playwright end to end
If you want these labs as a live classroom — with the VWO role matrix, Restful Booker status branches, and the framework we assemble on Day 21 — join Playwright Automation Mastery at The Testing Academy. Lifetime access. Real projects. A job-ready suite, not a folder of console.log.
*This is Day 3 of 21. Draft only. Not published.*
