JavaScript If-Else Practice Programs with Solutions (31 Programs)

Practise JavaScript if-else conditions with 31 programs, each with a full solution, the exact output and a short explanation. The programs start with the basics, move to intermediate problems and finish with real-world scenarios from test automation, such as test data, prices, form inputs and test results. They are written for testers learning JavaScript for Playwright.

How to practise: read the task, write your own solution first, then open Show solution to compare. Run any program with node program.js.

New to this topic? Read the JavaScript If Conditions lesson first.

JavaScript Beginner Programs

1. Positive, negative or zeroBeginner

Print whether each of 5, -3 and 0 is positive, negative or zero.

Show solution
for (const n of [5, -3, 0]) {
  if (n > 0) {
    console.log(n, 'is positive');
  } else if (n < 0) {
    console.log(n, 'is negative');
  } else {
    console.log(n, 'is zero');
  }
}

Output

5 is positive
-3 is negative
0 is zero

Explanation: an if / else if / else chain checks conditions in order and runs only the first matching branch.

2. Voting eligibilityBeginner

Print 'Eligible' when age is 18 or more, otherwise 'Not eligible', for ages 17 and 21.

Show solution
for (const age of [17, 21]) {
  if (age >= 18) {
    console.log(age, 'Eligible');
  } else {
    console.log(age, 'Not eligible');
  }
}

Output

17 Not eligible
21 Eligible

Explanation: if/else chooses between two outcomes based on one condition.

3. Largest of two numbersBeginner

Print the larger of 42 and 57.

Show solution
const a = 42;
const b = 57;
if (a > b) {
  console.log('Larger:', a);
} else {
  console.log('Larger:', b);
}

Output

Larger: 57

Explanation: a single comparison decides which branch runs.

4. Largest of three numbersBeginner

Find the largest of 12, 45 and 30 using if statements.

Show solution
const a = 12, b = 45, c = 30;
let largest;
if (a >= b && a >= c) {
  largest = a;
} else if (b >= a && b >= c) {
  largest = b;
} else {
  largest = c;
}
console.log('Largest:', largest);

Output

Largest: 45

Explanation: combine comparisons with &amp;&amp; so each branch checks one number against both others.

5. Leap yearBeginner

Check whether 2024, 1900 and 2000 are leap years.

Show solution
for (const year of [2024, 1900, 2000]) {
  const leap = (year % 4 === 0 && year % 100 !== 0) || year % 400 === 0;
  if (leap) console.log(year, 'is a leap year');
  else console.log(year, 'is not a leap year');
}

Output

2024 is a leap year
1900 is not a leap year
2000 is a leap year

Explanation: a year divisible by 4 is a leap year unless it is divisible by 100, but years divisible by 400 are leap years again.

6. Grade from marksBeginner

Convert marks 92, 78, 65 and 40 into grades A (90+), B (75+), C (60+) or F.

Show solution
for (const marks of [92, 78, 65, 40]) {
  let grade;
  if (marks >= 90) grade = 'A';
  else if (marks >= 75) grade = 'B';
  else if (marks >= 60) grade = 'C';
  else grade = 'F';
  console.log(marks, grade);
}

Output

92 A
78 B
65 C
40 F

Explanation: check the highest threshold first, so each value lands in the correct band.

7. Vowel or consonantBeginner

Check whether the letters 'e', 'K' and 'u' are vowels.

Show solution
for (const letter of ['e', 'K', 'u']) {
  if ('aeiou'.includes(letter.toLowerCase())) {
    console.log(letter, 'is a vowel');
  } else {
    console.log(letter, 'is a consonant');
  }
}

Output

e is a vowel
K is a consonant
u is a vowel

Explanation: includes checks membership, and toLowerCase makes the check case-insensitive.

8. Day name with switchBeginner

Print the day name for day numbers 1, 6 and 9 using switch.

Show solution
for (const day of [1, 6, 9]) {
  switch (day) {
    case 1: console.log(day, 'Monday'); break;
    case 2: console.log(day, 'Tuesday'); break;
    case 3: console.log(day, 'Wednesday'); break;
    case 4: console.log(day, 'Thursday'); break;
    case 5: console.log(day, 'Friday'); break;
    case 6: console.log(day, 'Saturday'); break;
    case 7: console.log(day, 'Sunday'); break;
    default: console.log(day, 'Invalid day');
  }
}

Output

1 Monday
6 Saturday
9 Invalid day

Explanation: switch compares one value against many cases. break stops execution falling into the next case.

9. Weekday or weekend with grouped casesBeginner

Use grouped case labels to print whether a day name is a weekday or the weekend.

Show solution
for (const day of ['Saturday', 'Tuesday']) {
  switch (day) {
    case 'Saturday':
    case 'Sunday':
      console.log(day, 'is the weekend');
      break;
    default:
      console.log(day, 'is a weekday');
  }
}

Output

Saturday is the weekend
Tuesday is a weekday

Explanation: stacked case labels without break share one block of code.

10. Check divisibilityBeginner

Check whether 30 is divisible by 3 and 5, and print FizzBuzz-style output.

Show solution
const n = 30;
if (n % 3 === 0 && n % 5 === 0) console.log('FizzBuzz');
else if (n % 3 === 0) console.log('Fizz');
else if (n % 5 === 0) console.log('Buzz');
else console.log(n);

Output

FizzBuzz

Explanation: check the most specific condition (both divisors) first, otherwise an earlier branch would catch it.

JavaScript Intermediate Programs

11. Nested conditionsIntermediate

Grant access when the user is logged in, then check the role inside.

Show solution
const users = [
  { name: 'Asha', loggedIn: true, role: 'admin' },
  { name: 'Ravi', loggedIn: true, role: 'viewer' },
  { name: 'Guest', loggedIn: false, role: 'viewer' }
];
for (const u of users) {
  if (u.loggedIn) {
    if (u.role === 'admin') console.log(u.name, '-> full access');
    else console.log(u.name, '-> read-only access');
  } else {
    console.log(u.name, '-> please log in');
  }
}

Output

Asha -> full access
Ravi -> read-only access
Guest -> please log in

Explanation: a nested if makes a second decision only after the first condition is true.

12. Guard clausesIntermediate

Rewrite nested checks as early returns in a validateAge function.

Show solution
function validateAge(age) {
  if (typeof age !== 'number') return 'Age must be a number';
  if (age < 0) return 'Age cannot be negative';
  if (age > 120) return 'Age looks unrealistic';
  return 'Age is valid';
}
console.log(validateAge('20'));
console.log(validateAge(-1));
console.log(validateAge(35));

Output

Age must be a number
Age cannot be negative
Age is valid

Explanation: guard clauses exit early on invalid input, keeping the main logic flat and readable.

13. Truthy checks in conditionsIntermediate

Print whether each value would enter an if block.

Show solution
const values = [0, 'qa', '', [], null];
const labels = ['0', "'qa'", "''", '[]', 'null'];
values.forEach((value, i) => {
  if (value) console.log(labels[i], '-> runs');
  else console.log(labels[i], '-> skipped');
});

Output

0 -> skipped
'qa' -> runs
'' -> skipped
[] -> runs
null -> skipped

Explanation: if checks truthiness, not just true. Empty arrays are truthy, while 0, '' and null are falsy.

14. switch with true for rangesIntermediate

Use switch (true) to classify response times of 120, 600 and 2500 ms.

Show solution
for (const ms of [120, 600, 2500]) {
  switch (true) {
    case ms < 200: console.log(ms, 'fast'); break;
    case ms < 1000: console.log(ms, 'acceptable'); break;
    default: console.log(ms, 'slow');
  }
}

Output

120 fast
600 acceptable
2500 slow

Explanation: switch (true) runs the first case whose expression is true, which works for ranges.

15. Object lookup instead of switchIntermediate

Map HTTP status codes to messages with an object lookup and a default.

Show solution
const messages = { 200: 'OK', 201: 'Created', 404: 'Not Found', 500: 'Server Error' };
for (const code of [200, 404, 418]) {
  console.log(code, messages[code] ?? 'Unknown status');
}

Output

200 OK
404 Not Found
418 Unknown status

Explanation: a lookup object replaces long switch statements when each case only returns a value.

16. Ternary for short decisionsIntermediate

Print '1 test' or 'N tests' using a ternary for the plural.

Show solution
for (const count of [1, 5, 0]) {
  console.log(`${count} ${count === 1 ? 'test' : 'tests'}`);
}

Output

1 test
5 tests
0 tests

Explanation: a ternary is ideal for small inline decisions such as singular and plural words.

17. Check string patternsIntermediate

Classify inputs as an email, a phone number or unknown.

Show solution
const inputs = ['qa@example.com', '9876543210', 'hello'];
for (const input of inputs) {
  if (input.includes('@') && input.includes('.')) console.log(input, '-> email');
  else if (/^\d{10}$/.test(input)) console.log(input, '-> phone');
  else console.log(input, '-> unknown');
}

Output

qa@example.com -> email
9876543210 -> phone
hello -> unknown

Explanation: conditions can combine string methods and regular expressions to classify text.

18. Triangle typeIntermediate

Classify triangles with sides (3,3,3), (3,4,4) and (3,4,5).

Show solution
const triangles = [[3, 3, 3], [3, 4, 4], [3, 4, 5]];
for (const [a, b, c] of triangles) {
  let type;
  if (a === b && b === c) type = 'equilateral';
  else if (a === b || b === c || a === c) type = 'isosceles';
  else type = 'scalene';
  console.log(`${a},${b},${c} -> ${type}`);
}

Output

3,3,3 -> equilateral
3,4,4 -> isosceles
3,4,5 -> scalene

Explanation: check the strictest case (all equal) before the looser one (any two equal).

19. Electricity bill slabsIntermediate

Calculate a bill where the first 100 units cost 5, the next 100 cost 7 and the rest cost 10 each, for 250 units.

Show solution
const units = 250;
let bill;
if (units <= 100) {
  bill = units * 5;
} else if (units <= 200) {
  bill = 100 * 5 + (units - 100) * 7;
} else {
  bill = 100 * 5 + 100 * 7 + (units - 200) * 10;
}
console.log('Bill:', bill);

Output

Bill: 1700

Explanation: each branch adds up the full slabs below it plus the partial slab the value falls in.

20. Optional values with ?? in conditionsIntermediate

Print a configured or default browser name, treating an empty string as a valid configured value.

Show solution
for (const configured of ['firefox', '', undefined]) {
  const browser = configured ?? 'chromium';
  console.log(JSON.stringify(configured) ?? 'undefined', '->', JSON.stringify(browser));
}

Output

"firefox" -> "firefox"
"" -> ""
undefined -> "chromium"

Explanation: ?? only falls back for null and undefined. Use || when empty strings should also fall back.

JavaScript Real-World QA Scenario Programs

21. Map a test status to an exit codeReal-world

Return exit code 0 for passed or skipped, 1 for failed and 2 for anything else.

Show solution
function exitCode(status) {
  switch (status) {
    case 'passed':
    case 'skipped':
      return 0;
    case 'failed':
      return 1;
    default:
      return 2;
  }
}
for (const s of ['passed', 'failed', 'skipped', 'timedOut']) console.log(s, exitCode(s));

Output

passed 0
failed 1
skipped 0
timedOut 2

Explanation: CI systems read exit codes, and switch maps each status to its code clearly.

22. Validate a passwordReal-world

Check a password for length (8+), a digit and an uppercase letter, and print the first failing rule.

Show solution
function checkPassword(pw) {
  if (pw.length < 8) return 'Too short';
  if (!/\d/.test(pw)) return 'Needs a digit';
  if (!/[A-Z]/.test(pw)) return 'Needs an uppercase letter';
  return 'Strong';
}
for (const pw of ['abc', 'abcdefgh', 'abcdefg1', 'Abcdefg1']) console.log(pw, '->', checkPassword(pw));

Output

abc -> Too short
abcdefgh -> Needs a digit
abcdefg1 -> Needs an uppercase letter
Abcdefg1 -> Strong

Explanation: guard clauses report the first rule that fails, which mirrors how form validation messages appear.

23. Choose environment URLReal-world

Select a base URL from an ENV value, defaulting to QA for unknown values.

Show solution
function baseUrlFor(env) {
  if (env === 'prod') return 'https://shop.test';
  if (env === 'staging') return 'https://staging.shop.test';
  return 'https://qa.shop.test';
}
for (const env of ['prod', 'staging', 'dev']) console.log(env, '->', baseUrlFor(env));

Output

prod -> https://shop.test
staging -> https://staging.shop.test
dev -> https://qa.shop.test

Explanation: conditions centralise environment selection, with a safe default for anything unexpected.

24. Shipping cost rulesReal-world

Free shipping over 999, 49 for 500–999 and 99 below 500. Calculate it for three cart totals.

Show solution
for (const total of [1200, 750, 300]) {
  let shipping;
  if (total > 999) shipping = 0;
  else if (total >= 500) shipping = 49;
  else shipping = 99;
  console.log(`Cart ${total} -> shipping ${shipping}`);
}

Output

Cart 1200 -> shipping 0
Cart 750 -> shipping 49
Cart 300 -> shipping 99

Explanation: boundary values (999, 500) are where bugs hide, so test values on and around each threshold.

25. Boundary value checksReal-world

For the shipping rule above, generate the boundary inputs 499, 500, 999 and 1000 and print the result for each.

Show solution
const shippingFor = total => (total > 999 ? 0 : total >= 500 ? 49 : 99);
for (const total of [499, 500, 999, 1000]) {
  console.log(total, shippingFor(total));
}

Output

499 99
500 49
999 49
1000 0

Explanation: boundary value analysis checks each edge, because off-by-one mistakes like &gt; versus &gt;= are common.

26. Decide test priorityReal-world

Mark a test as P1 if it is on the checkout path or a blocker, P2 if it is a regression test, otherwise P3.

Show solution
const tests = [
  { id: 'TC1', area: 'checkout', blocker: false, regression: true },
  { id: 'TC2', area: 'profile', blocker: true, regression: false },
  { id: 'TC3', area: 'search', blocker: false, regression: true },
  { id: 'TC4', area: 'help', blocker: false, regression: false }
];
for (const t of tests) {
  let priority;
  if (t.area === 'checkout' || t.blocker) priority = 'P1';
  else if (t.regression) priority = 'P2';
  else priority = 'P3';
  console.log(t.id, priority);
}

Output

TC1 P1
TC2 P1
TC3 P2
TC4 P3

Explanation: ordered conditions turn a written triage rule into code that is easy to review.

27. Verify a sort orderReal-world

Check whether a list of prices from the UI is sorted ascending and print a message.

Show solution
const prices = [99, 149, 149, 299, 250];
let sorted = true;
for (let i = 1; i < prices.length; i++) {
  if (prices[i] < prices[i - 1]) {
    console.log(`Out of order at index ${i}: ${prices[i - 1]} > ${prices[i]}`);
    sorted = false;
    break;
  }
}
if (sorted) console.log('Prices are sorted');

Output

Out of order at index 4: 299 > 250

Explanation: compare each item with the previous one. The if reports exactly where the order breaks.

28. Check an API responseReal-world

Report a clear message for different status and body combinations.

Show solution
const responses = [
  { status: 200, body: { items: [1, 2] } },
  { status: 200, body: { items: [] } },
  { status: 401, body: null },
  { status: 503, body: null }
];
for (const res of responses) {
  if (res.status >= 500) console.log(res.status, 'Server error - retry later');
  else if (res.status === 401) console.log(res.status, 'Unauthorized - check token');
  else if (res.body.items.length === 0) console.log(res.status, 'OK but no items');
  else console.log(res.status, `OK with ${res.body.items.length} items`);
}

Output

200 OK with 2 items
200 OK but no items
401 Unauthorized - check token
503 Server error - retry later

Explanation: check error statuses before reading the body, so you never access items on a null body.

29. Feature availability by planReal-world

Show which features a user can use based on their plan.

Show solution
function canUse(plan, feature) {
  if (feature === 'reports') return plan === 'pro' || plan === 'enterprise';
  if (feature === 'sso') return plan === 'enterprise';
  return true;
}
for (const plan of ['free', 'pro', 'enterprise']) {
  console.log(plan, 'reports:', canUse(plan, 'reports'), 'sso:', canUse(plan, 'sso'));
}

Output

free reports: false sso: false
pro reports: true sso: false
enterprise reports: true sso: true

Explanation: one function with clear conditions is easy to cover with a test per plan and feature.

30. Classify a flaky testReal-world

Classify a test as stable, flaky or broken from its pass count over 10 runs.

Show solution
for (const passes of [10, 7, 0]) {
  const runs = 10;
  let label;
  if (passes === runs) label = 'stable';
  else if (passes === 0) label = 'broken';
  else label = 'flaky';
  console.log(`${passes}/${runs} passed -> ${label}`);
}

Output

10/10 passed -> stable
7/10 passed -> flaky
0/10 passed -> broken

Explanation: a test that sometimes passes and sometimes fails is flaky. Always passing or always failing is deterministic.

31. Business days onlyReal-world

Skip weekend dates when scheduling a test run, using getUTCDay().

Show solution
const dates = ['2026-11-06', '2026-11-07', '2026-11-08', '2026-11-09'];
for (const d of dates) {
  const day = new Date(d + 'T00:00:00Z').getUTCDay();
  if (day === 0 || day === 6) console.log(d, 'weekend - skip');
  else console.log(d, 'run tests');
}

Output

2026-11-06 run tests
2026-11-07 weekend - skip
2026-11-08 weekend - skip
2026-11-09 run tests

Explanation: getUTCDay() returns 0 for Sunday and 6 for Saturday. Using UTC avoids time-zone surprises.

Keep Practising

JavaScript Operators Practice Programs with Solutions (32 Programs)

Practise JavaScript operators with 32 programs, each with a full solution, the exact output and a short explanation. The programs start with the basics, move to intermediate problems and finish with real-world scenarios from test automation, such as test data, prices, form inputs and test results. They are written for testers learning JavaScript for Playwright.

How to practise: read the task, write your own solution first, then open Show solution to compare. Run any program with node program.js.

New to this topic? Read the JavaScript Operators lesson first.

JavaScript Beginner Programs

1. Basic arithmeticBeginner

Given a = 17 and b = 5, print their sum, difference, product, quotient and remainder.

Show solution
const a = 17;
const b = 5;
console.log('Sum:', a + b);
console.log('Difference:', a - b);
console.log('Product:', a * b);
console.log('Quotient:', a / b);
console.log('Remainder:', a % b);

Output

Sum: 22
Difference: 12
Product: 85
Quotient: 3.4
Remainder: 2

Explanation: % returns the remainder after division. / always gives a decimal result when the division is not exact.

2. Exponent operatorBeginner

Print 2 to the power of 10 using ** and Math.pow.

Show solution
console.log(2 ** 10);
console.log(Math.pow(2, 10));

Output

1024
1024

Explanation: ** is the exponent operator and gives the same result as Math.pow.

3. Even or odd with modulusBeginner

Use % to print whether 7 and 12 are even or odd.

Show solution
for (const n of [7, 12]) {
  console.log(n, n % 2 === 0 ? 'is even' : 'is odd');
}

Output

7 is odd
12 is even

Explanation: a number is even when dividing by 2 leaves no remainder.

4. Compound assignmentBeginner

Start with total = 100 and apply +=, -=, *=, /= and %= one after another, printing each result.

Show solution
let total = 100;
total += 20; console.log(total);
total -= 10; console.log(total);
total *= 2; console.log(total);
total /= 4; console.log(total);
total %= 50; console.log(total);

Output

120
110
220
55
5

Explanation: compound operators update a variable using its current value, so total += 20 means total = total + 20.

5. Comparison operatorsBeginner

Compare score = 75 with 70 using &gt;, &lt;, &gt;=, &lt;=, === and !==.

Show solution
const score = 75;
console.log(score > 70, score < 70);
console.log(score >= 75, score <= 74);
console.log(score === 75, score !== 75);

Output

true false
true false
true false

Explanation: comparison operators return booleans that you can use in conditions and assertions.

6. Strict vs loose equalityBeginner

Compare '5' and 5 with == and ===.

Show solution
console.log('5' == 5);
console.log('5' === 5);
console.log('5' != 5);
console.log('5' !== 5);

Output

true
false
false
true

Explanation: == converts types before comparing, while === also checks the type. Prefer === in test code.

7. Logical AND, OR, NOTBeginner

With isLoggedIn = true and isAdmin = false, print the results of &amp;&amp;, || and !.

Show solution
const isLoggedIn = true;
const isAdmin = false;
console.log(isLoggedIn && isAdmin);
console.log(isLoggedIn || isAdmin);
console.log(!isAdmin);

Output

false
true
true

Explanation: &amp;&amp; needs both sides true, || needs at least one, and ! flips a boolean.

8. Increment and decrementBeginner

Show the difference between x++ and ++x.

Show solution
let x = 5;
console.log(x++);
console.log(x);
let y = 5;
console.log(++y);
console.log(y);

Output

5
6
6
6

Explanation: postfix x++ returns the old value and then increments. Prefix ++y increments first and returns the new value.

9. String concatenation with +Beginner

Join a first name and last name with a space using +, then add a number to a string.

Show solution
const firstName = 'Neha';
const lastName = 'Verma';
console.log(firstName + ' ' + lastName);
console.log('Build #' + 42);

Output

Neha Verma
Build #42

Explanation: when either side of + is a string, the other side is converted and the values are joined.

10. Ternary operatorBeginner

Use the ternary operator to print 'PASS' when score &gt;= 60 and 'FAIL' otherwise, for 72 and 45.

Show solution
for (const score of [72, 45]) {
  const result = score >= 60 ? 'PASS' : 'FAIL';
  console.log(score, result);
}

Output

72 PASS
45 FAIL

Explanation: condition ? a : b returns a when the condition is truthy, otherwise b.

JavaScript Intermediate Programs

11. Short-circuit with &&Intermediate

Call a function only when a flag is true, using &amp;&amp;.

Show solution
function takeScreenshot() {
  console.log('Screenshot taken');
  return true;
}
const debug = true;
const ci = false;
debug && takeScreenshot();
ci && takeScreenshot();
console.log('Done');

Output

Screenshot taken
Done

Explanation: &amp;&amp; stops at the first falsy value, so the function on its right runs only when the left side is truthy.

12. || vs ?? for defaultsIntermediate

Use both || and ?? to default a timeout of 0, null and undefined to 5000.

Show solution
for (const value of [0, null, undefined]) {
  console.log(String(value), '| ||:', value || 5000, '| ??:', value ?? 5000);
}

Output

0 | ||: 5000 | ??: 0
null | ||: 5000 | ??: 5000
undefined | ||: 5000 | ??: 5000

Explanation: || replaces every falsy value, including a valid 0. ?? only replaces null and undefined.

13. Logical assignment operatorsIntermediate

Use ||=, &amp;&amp;= and ??= on a settings object.

Show solution
const settings = { name: '', retries: null, headless: true };
settings.name ||= 'default-suite';
settings.retries ??= 2;
settings.headless &&= false;
console.log(settings);

Output

{ name: 'default-suite', retries: 2, headless: false }

Explanation: a ||= b assigns only if a is falsy, a ??= b only if a is null or undefined, and a &amp;&amp;= b only if a is truthy.

14. Optional chainingIntermediate

Safely read user.address.city when address may be missing.

Show solution
const withAddress = { name: 'Asha', address: { city: 'Delhi' } };
const withoutAddress = { name: 'Ravi' };
console.log(withAddress.address?.city);
console.log(withoutAddress.address?.city);
console.log(withoutAddress.address?.city ?? 'N/A');

Output

Delhi
undefined
N/A

Explanation: ?. returns undefined instead of throwing when the left side is null or undefined.

15. Operator precedenceIntermediate

Predict and print the results of 2 + 3 * 4, (2 + 3) * 4 and 10 - 4 - 3.

Show solution
console.log(2 + 3 * 4);
console.log((2 + 3) * 4);
console.log(10 - 4 - 3);
console.log(2 ** 3 ** 2);

Output

14
20
3
512

Explanation: * binds tighter than +, subtraction runs left to right, and ** runs right to left (3 ** 2 first).

16. typeof and instanceofIntermediate

Use typeof on a primitive and instanceof on an object.

Show solution
const today = new Date(0);
const tags = ['smoke'];
console.log(typeof 'text');
console.log(today instanceof Date);
console.log(tags instanceof Array);
console.log(tags instanceof Object);

Output

string
true
true
true

Explanation: typeof names primitive types, while instanceof checks whether an object was created by a constructor.

17. The in operatorIntermediate

Check whether a config object has the keys browser and proxy.

Show solution
const config = { browser: 'chromium', retries: 0 };
console.log('browser' in config);
console.log('proxy' in config);
console.log('retries' in config && config.retries === 0);

Output

true
false
true

Explanation: in checks whether a property exists, even when its value is falsy like 0.

18. Spread operatorIntermediate

Merge default and custom settings with the spread operator.

Show solution
const defaults = { browser: 'chromium', retries: 0, headless: true };
const custom = { retries: 2 };
const finalConfig = { ...defaults, ...custom };
console.log(finalConfig);
console.log([...[1, 2], ...[3]]);

Output

{ browser: 'chromium', retries: 2, headless: true }
[ 1, 2, 3 ]

Explanation: spread copies properties or elements. Later properties overwrite earlier ones with the same name.

19. Bitwise check of permission flagsIntermediate

Use bitwise operators to check which permissions a user has.

Show solution
const READ = 1, WRITE = 2, DELETE = 4;
const userPermissions = READ | WRITE;
console.log('Can read:', (userPermissions & READ) !== 0);
console.log('Can write:', (userPermissions & WRITE) !== 0);
console.log('Can delete:', (userPermissions & DELETE) !== 0);

Output

Can read: true
Can write: true
Can delete: false

Explanation: | combines flags into one number and &amp; tests whether a flag is set.

20. Swap numbers with arithmeticIntermediate

Swap two numbers using only arithmetic operators.

Show solution
let a = 8;
let b = 3;
a = a + b;
b = a - b;
a = a - b;
console.log(a, b);

Output

3 8

Explanation: storing the sum lets you recover each original value with subtraction. Destructuring is clearer in real code.

JavaScript Real-World QA Scenario Programs

21. Calculate cart total with taxReal-world

Calculate the total for a cart of 1200 with 18% tax, and round to 2 decimals.

Show solution
const subtotal = 1200;
const taxRate = 0.18;
const tax = subtotal * taxRate;
const total = subtotal + tax;
console.log('Tax:', tax.toFixed(2));
console.log('Total:', total.toFixed(2));

Output

Tax: 216.00
Total: 1416.00

Explanation: multiplication gives the tax, addition gives the total, and toFixed formats money for display.

22. Apply a discountReal-world

Apply a 15% discount to 2499 and print the discount and final price.

Show solution
const price = 2499;
const discountPercent = 15;
const discount = (price * discountPercent) / 100;
const finalPrice = price - discount;
console.log(`Discount: ${discount.toFixed(2)}`);
console.log(`Final price: ${finalPrice.toFixed(2)}`);

Output

Discount: 374.85
Final price: 2124.15

Explanation: percentages are calculated with * and /. Format the results for display.

23. Pass percentage and gradeReal-world

With passed = 47 of total = 52 tests, calculate the pass rate and label it 'Green' (95%+), 'Amber' (80%+) or 'Red'.

Show solution
const passed = 47;
const total = 52;
const rate = (passed / total) * 100;
const label = rate >= 95 ? 'Green' : rate >= 80 ? 'Amber' : 'Red';
console.log(`${rate.toFixed(1)}% - ${label}`);

Output

90.4% - Amber

Explanation: chained ternaries pick one of several labels. For more than three cases, an if/else chain is clearer.

24. Check a value is within rangeReal-world

Check that a response time of 850 ms is between 0 and 1000 ms, and that 1200 ms is not.

Show solution
const MIN = 0, MAX = 1000;
for (const ms of [850, 1200]) {
  const ok = ms >= MIN && ms <= MAX;
  console.log(`${ms} ms within SLA: ${ok}`);
}

Output

850 ms within SLA: true
1200 ms within SLA: false

Explanation: combine two comparisons with &amp;&amp; to test a range.

25. Validate login form fieldsReal-world

A login is valid when the username is not empty and the password has at least 8 characters. Test three cases.

Show solution
const cases = [['qa_user', 'Secret123'], ['', 'Secret123'], ['qa_user', 'short']];
for (const [username, password] of cases) {
  const valid = username.length > 0 && password.length >= 8;
  console.log(`'${username}' / ${password.length} chars -> ${valid ? 'valid' : 'invalid'}`);
}

Output

'qa_user' / 9 chars -> valid
'' / 9 chars -> invalid
'qa_user' / 5 chars -> invalid

Explanation: &amp;&amp; requires every rule to pass. One false condition makes the whole expression false.

26. Decide whether to retry a testReal-world

Retry when the test failed AND attempts are below the limit, OR when the failure was a timeout.

Show solution
const MAX_RETRIES = 2;
const runs = [
  { failed: true, attempts: 1, timeout: false },
  { failed: true, attempts: 2, timeout: false },
  { failed: true, attempts: 3, timeout: true }
];
for (const run of runs) {
  const retry = (run.failed && run.attempts < MAX_RETRIES) || run.timeout;
  console.log(`attempts=${run.attempts} timeout=${run.timeout} -> retry: ${retry}`);
}

Output

attempts=1 timeout=false -> retry: true
attempts=2 timeout=false -> retry: false
attempts=3 timeout=true -> retry: true

Explanation: parentheses make the business rule explicit, because &amp;&amp; binds tighter than ||.

27. Round-robin browser selectionReal-world

Assign 7 test cases to 3 browsers in turn using the modulus operator.

Show solution
const browsers = ['chromium', 'firefox', 'webkit'];
for (let i = 0; i < 7; i++) {
  console.log(`TC-${i + 1} -> ${browsers[i % browsers.length]}`);
}

Output

TC-1 -> chromium
TC-2 -> firefox
TC-3 -> webkit
TC-4 -> chromium
TC-5 -> firefox
TC-6 -> webkit
TC-7 -> chromium

Explanation: i % length cycles through 0, 1, 2, 0, 1, … which is ideal for distributing work.

28. Convert milliseconds to h:m:sReal-world

Convert 3725000 ms into hours, minutes and seconds.

Show solution
const ms = 3725000;
const totalSeconds = Math.floor(ms / 1000);
const hours = Math.floor(totalSeconds / 3600);
const minutes = Math.floor((totalSeconds % 3600) / 60);
const seconds = totalSeconds % 60;
console.log(`${hours}h ${minutes}m ${seconds}s`);

Output

1h 2m 5s

Explanation: division gives the larger units and % gives what is left over.

29. Compare expected and actual countsReal-world

Compare the expected item count with the count shown in the UI, which is text, and print a clear message.

Show solution
const expectedCount = 3;
const uiText = '3';
const loose = expectedCount == uiText;
const strict = expectedCount === Number(uiText);
console.log('Loose match:', loose);
console.log('Strict match after conversion:', strict);

Output

Loose match: true
Strict match after conversion: true

Explanation: convert UI text explicitly and use ===, so a mismatch in types never hides a real bug.

30. Build a feature-flag decisionReal-world

Show a beta banner when the feature is enabled and the user is either a beta tester or an employee.

Show solution
const users = [
  { name: 'Asha', betaTester: true, employee: false },
  { name: 'Ravi', betaTester: false, employee: false },
  { name: 'Meera', betaTester: false, employee: true }
];
const featureEnabled = true;
for (const u of users) {
  const showBanner = featureEnabled && (u.betaTester || u.employee);
  console.log(u.name, showBanner);
}

Output

Asha true
Ravi false
Meera true

Explanation: grouping the || part with parentheses makes the rule read exactly like the requirement.

31. Average response timeReal-world

Calculate the average of the response times 120, 340, 95 and 410 ms.

Show solution
const times = [120, 340, 95, 410];
let sum = 0;
for (const t of times) sum += t;
const avg = sum / times.length;
console.log('Average:', avg, 'ms');
console.log('Above 200 ms:', avg > 200);

Output

Average: 241.25 ms
Above 200 ms: true

Explanation: += accumulates the total and / divides by the count to get the average.

32. Version comparisonReal-world

Compare two version strings '1.48.2' and '1.50.0' part by part.

Show solution
const current = '1.48.2'.split('.').map(Number);
const required = '1.50.0'.split('.').map(Number);
let result = 0;
for (let i = 0; i < 3 && result === 0; i++) {
  result = current[i] > required[i] ? 1 : current[i] < required[i] ? -1 : 0;
}
console.log(result < 0 ? 'Upgrade needed' : 'Version OK');

Output

Upgrade needed

Explanation: comparing numbers part by part avoids string comparison bugs such as '1.9' &gt; '1.10'.

Keep Practising

JavaScript Type Conversion Practice Programs with Solutions (31 Programs)

Practise JavaScript type conversion with 31 programs, each with a full solution, the exact output and a short explanation. The programs start with the basics, move to intermediate problems and finish with real-world scenarios from test automation, such as test data, prices, form inputs and test results. They are written for testers learning JavaScript for Playwright.

How to practise: read the task, write your own solution first, then open Show solution to compare. Run any program with node program.js.

New to this topic? Read the JavaScript Type Conversion lesson first.

JavaScript Beginner Programs

1. String to number with Number()Beginner

Convert the strings '42', '3.14' and '' to numbers and print the results.

Show solution
console.log(Number('42'));
console.log(Number('3.14'));
console.log(Number(''));

Output

42
3.14
0

Explanation: Number() converts a whole string to a number. An empty string becomes 0.

2. Invalid number stringsBeginner

Convert '42px' and 'abc' with Number() and print the results.

Show solution
console.log(Number('42px'));
console.log(Number('abc'));

Output

NaN
NaN

Explanation: Number() returns NaN if any part of the string is not numeric.

3. parseInt and parseFloatBeginner

Read the leading number from '42px' with parseInt and from '3.75 kg' with parseFloat.

Show solution
console.log(parseInt('42px', 10));
console.log(parseFloat('3.75 kg'));
console.log(parseInt('px42', 10));

Output

42
3.75
NaN

Explanation: the parse functions read digits from the start and stop at the first invalid character. If the string does not start with a number, they return NaN.

4. Number to stringBeginner

Convert the number 404 to a string in three different ways and print the types.

Show solution
const code = 404;
const a = String(code);
const b = code.toString();
const c = `${code}`;
console.log(a, b, c);
console.log(typeof a, typeof b, typeof c);

Output

404 404 404
string string string

Explanation: String(), .toString() and template literals all produce a string.

5. Convert to booleanBeginner

Use Boolean() on 0, 1, '', '0' and 'false'.

Show solution
console.log(Boolean(0));
console.log(Boolean(1));
console.log(Boolean(''));
console.log(Boolean('0'));
console.log(Boolean('false'));

Output

false
true
false
true
true

Explanation: any non-empty string is truthy, even '0' and 'false'.

6. Unary plusBeginner

Convert '25' to a number with the unary + operator and add 5.

Show solution
const quantity = '25';
console.log(quantity + 5);
console.log(+quantity + 5);

Output

255
30

Explanation: without conversion + joins strings, giving '255'. Unary + converts the string first.

7. String concatenation coercionBeginner

Print the results of '5' + 3, '5' - 3, '5' * '2' and '10' / '2'.

Show solution
console.log('5' + 3);
console.log('5' - 3);
console.log('5' * '2');
console.log('10' / '2');

Output

53
2
10
5

Explanation: + prefers string concatenation, while -, * and / convert both sides to numbers.

8. Round and format numbersBeginner

Round 19.987 to two decimals as a string and as a number.

Show solution
const amount = 19.987;
const asString = amount.toFixed(2);
const asNumber = Number(asString);
console.log(asString, typeof asString);
console.log(asNumber, typeof asNumber);

Output

19.99 string
19.99 number

Explanation: toFixed returns a string. Wrap it with Number() when you need a number again.

9. Array to stringBeginner

Convert ['smoke', 'regression'] to a string using String() and join(' | ').

Show solution
const tags = ['smoke', 'regression'];
console.log(String(tags));
console.log(tags.join(' | '));

Output

smoke,regression
smoke | regression

Explanation: arrays convert to comma-separated strings. join lets you choose the separator.

10. String to arrayBeginner

Split 'chromium,firefox,webkit' into an array and print its length.

Show solution
const csv = 'chromium,firefox,webkit';
const browsers = csv.split(',');
console.log(browsers);
console.log(browsers.length);

Output

[ 'chromium', 'firefox', 'webkit' ]
3

Explanation: split converts a delimited string into an array of strings.

JavaScript Intermediate Programs

11. Loose vs strict equalityIntermediate

Compare 0 == '', 0 === '', '1' == 1 and null == 0.

Show solution
console.log(0 == '');
console.log(0 === '');
console.log('1' == 1);
console.log(null == 0);

Output

true
false
true
false

Explanation: == converts types before comparing, with some surprising rules. Prefer === in tests.

12. parseInt with a radixIntermediate

Parse '1010' as binary and 'ff' as hexadecimal, then convert 255 back to hex.

Show solution
console.log(parseInt('1010', 2));
console.log(parseInt('ff', 16));
console.log((255).toString(16));

Output

10
255
ff

Explanation: the second argument of parseInt is the base. toString(base) converts the other way.

13. Number() vs parseInt on decimalsIntermediate

Compare Number('12.9'), parseInt('12.9', 10) and Math.trunc(Number('12.9')).

Show solution
console.log(Number('12.9'));
console.log(parseInt('12.9', 10));
console.log(Math.trunc(Number('12.9')));

Output

12.9
12
12

Explanation: parseInt drops the decimal part. Use Number to keep it, or Math.trunc for an explicit integer.

14. Object to JSON and backIntermediate

Convert a test-config object to JSON text and parse it back.

Show solution
const config = { browser: 'chromium', retries: 2, headless: true };
const json = JSON.stringify(config);
console.log(json, typeof json);
const parsed = JSON.parse(json);
console.log(parsed.retries + 1);

Output

{"browser":"chromium","retries":2,"headless":true} string
3

Explanation: JSON.stringify turns data into text, for files or APIs. JSON.parse turns it back into objects with real numbers and booleans.

15. Pretty-print JSONIntermediate

Print an object as indented JSON with two spaces.

Show solution
const result = { test: 'login', status: 'passed' };
console.log(JSON.stringify(result, null, 2));

Output

{
  "test": "login",
  "status": "passed"
}

Explanation: the third argument of JSON.stringify adds indentation, which makes logs easier to read.

16. Values JSON cannot keepIntermediate

Stringify an object containing undefined, a function and NaN, and print the result.

Show solution
const data = { a: undefined, b: () => 1, c: NaN, d: 'ok' };
console.log(JSON.stringify(data));

Output

{"c":null,"d":"ok"}

Explanation: JSON drops undefined and functions, and turns NaN into null.

17. Date to timestamp and ISO stringIntermediate

Create a date from a fixed ISO string and print its timestamp and ISO form.

Show solution
const date = new Date('2026-01-15T10:30:00Z');
console.log(date.getTime());
console.log(date.toISOString());
console.log(new Date(0).toISOString());

Output

1768473000000
2026-01-15T10:30:00.000Z
1970-01-01T00:00:00.000Z

Explanation: getTime() gives milliseconds since 1 January 1970 UTC, and toISOString() gives a standard text format.

18. Object conversion with valueOfIntermediate

Create an object with valueOf and toString, then use it in maths and in a template literal.

Show solution
const price = {
  amount: 499,
  valueOf() { return this.amount; },
  toString() { return `β‚Ή${this.amount}`; }
};
console.log(price + 1);
console.log(`${price}`);

Output

500
β‚Ή499

Explanation: maths calls valueOf, while template literals call toString.

19. Number checks: isNaN vs Number.isNaNIntermediate

Compare isNaN('abc') with Number.isNaN('abc').

Show solution
console.log(isNaN('abc'));
console.log(Number.isNaN('abc'));
console.log(Number.isNaN(Number('abc')));

Output

true
false
true

Explanation: global isNaN converts its argument first. Number.isNaN only returns true for the actual NaN value.

20. Map and object conversionsIntermediate

Convert an object into a Map and back again.

Show solution
const scores = { login: 95, checkout: 80 };
const map = new Map(Object.entries(scores));
console.log(map.get('checkout'));
const back = Object.fromEntries(map);
console.log(back);

Output

80
{ login: 95, checkout: 80 }

Explanation: Object.entries and Object.fromEntries convert between plain objects and key/value pairs.

JavaScript Real-World QA Scenario Programs

21. Parse a price from page textReal-world

Extract the number from the UI text 'β‚Ή1,299.50'.

Show solution
const priceText = 'β‚Ή1,299.50';
const price = Number(priceText.replace(/[^0-9.]/g, ''));
console.log(price);
console.log(price + 100);

Output

1299.5
1399.5

Explanation: remove currency symbols and thousands separators, then convert. UI text is always a string.

22. Sum prices read from the pageReal-world

A test reads cart prices as strings. Convert and sum them.

Show solution
const priceTexts = ['$12.50', '$7.25', '$30.00'];
const total = priceTexts
  .map(text => Number(text.replace('$', '')))
  .reduce((sum, value) => sum + value, 0);
console.log(total.toFixed(2));

Output

49.75

Explanation: summing the raw strings would concatenate them, so convert each to a number first.

23. Read numeric environment variablesReal-world

Convert the environment values '4' and undefined to a worker count with a default of 2.

Show solution
const env = { WORKERS: '4' };
const workers = Number(env.WORKERS ?? 2);
const retries = Number(env.RETRIES ?? 0);
console.log(workers, typeof workers);
console.log(retries);

Output

4 number
0

Explanation: environment variables are always strings or missing. Convert them and provide defaults.

24. Parse a boolean flag safelyReal-world

Turn HEADLESS values such as 'true', 'false', '1' and undefined into booleans.

Show solution
const toBool = value => ['true', '1', 'yes'].includes(String(value).toLowerCase());
console.log(toBool('true'));
console.log(toBool('false'));
console.log(toBool('1'));
console.log(toBool(undefined));

Output

true
false
true
false

Explanation: Boolean('false') is true, so compare against known true values instead.

25. Parse CSV rows into typed objectsReal-world

Convert a CSV line 'qa_user,25,true' into an object with a string, number and boolean.

Show solution
const line = 'qa_user,25,true';
const [username, age, active] = line.split(',');
const user = { username, age: Number(age), active: active === 'true' };
console.log(user);

Output

{ username: 'qa_user', age: 25, active: true }

Explanation: CSV fields are text, so convert each column to the type your test expects.

26. Compare an API number with UI textReal-world

The API returns 1299 and the UI shows '1,299'. Compare them correctly.

Show solution
const apiPrice = 1299;
const uiText = '1,299';
console.log(apiPrice === uiText);
console.log(apiPrice === Number(uiText.replace(/,/g, '')));

Output

false
true

Explanation: normalise the UI text into a number before comparing it with API data.

27. Convert durationsReal-world

Convert a test duration of 125430 ms into minutes and seconds.

Show solution
const durationMs = 125430;
const totalSeconds = Math.floor(durationMs / 1000);
const minutes = Math.floor(totalSeconds / 60);
const seconds = totalSeconds % 60;
console.log(`${minutes}m ${String(seconds).padStart(2, '0')}s`);

Output

2m 05s

Explanation: integer division with Math.floor and % converts a raw number into a readable duration.

28. Read numbers from a sentenceReal-world

Extract all numbers from 'Passed 45 of 50 tests in 12.5 seconds'.

Show solution
const summary = 'Passed 45 of 50 tests in 12.5 seconds';
const numbers = summary.match(/\d+(\.\d+)?/g).map(Number);
console.log(numbers);
console.log(numbers[0] / numbers[1]);

Output

[ 45, 50, 12.5 ]
0.9

Explanation: match with a global regular expression finds every number as a string, and map(Number) converts them.

29. Normalise mixed input typesReal-world

A form sends quantities as 5, '7', '' and null. Convert them to numbers, treating empty values as 0.

Show solution
const inputs = [5, '7', '', null, '3'];
const quantities = inputs.map(value => (value === '' || value == null ? 0 : Number(value)));
console.log(quantities);
console.log(quantities.reduce((a, b) => a + b, 0));

Output

[ 5, 7, 0, 0, 3 ]
15

Explanation: decide explicitly how empty values convert, rather than relying on implicit coercion.

30. Format a number as currency textReal-world

Format 1234567.891 as a US dollar amount and an Indian rupee amount.

Show solution
const amount = 1234567.891;
console.log(amount.toLocaleString('en-US', { style: 'currency', currency: 'USD' }));
console.log(amount.toLocaleString('en-IN', { style: 'currency', currency: 'INR' }));

Output

$1,234,567.89
β‚Ή12,34,567.89

Explanation: toLocaleString converts numbers into locale-aware text, which is useful when asserting formatted prices.

31. Query string to objectReal-world

Convert the query string '?page=2&amp;sort=price&amp;inStock=true' into an object with typed values.

Show solution
const params = new URLSearchParams('?page=2&sort=price&inStock=true');
const query = {
  page: Number(params.get('page')),
  sort: params.get('sort'),
  inStock: params.get('inStock') === 'true'
};
console.log(query);

Output

{ page: 2, sort: 'price', inStock: true }

Explanation: URLSearchParams returns every value as a string, so convert numbers and booleans yourself.

Keep Practising

JavaScript Data Types Practice Programs with Solutions (31 Programs)

Practise JavaScript data types with 31 programs, each with a full solution, the exact output and a short explanation. The programs start with the basics, move to intermediate problems and finish with real-world scenarios from test automation, such as test data, prices, form inputs and test results. They are written for testers learning JavaScript for Playwright.

How to practise: read the task, write your own solution first, then open Show solution to compare. Run any program with node program.js.

New to this topic? Read the JavaScript Data Types lesson first.

JavaScript Beginner Programs

1. Print the type of each primitiveBeginner

Use typeof to print the type of a string, number, boolean, undefined, bigint and symbol.

Show solution
console.log(typeof 'Playwright');
console.log(typeof 42);
console.log(typeof true);
console.log(typeof undefined);
console.log(typeof 10n);
console.log(typeof Symbol('id'));

Output

string
number
boolean
undefined
bigint
symbol

Explanation: typeof returns a string naming the type. These six, plus null, are JavaScript’s primitive types.

2. typeof nullBeginner

Print typeof null and explain the result in a comment.

Show solution
const value = null;
console.log(typeof value);
console.log(value === null);

Output

object
true

Explanation: typeof null returns 'object' because of an old bug kept for compatibility. Check for null with === null.

3. Arrays and functions with typeofBeginner

Print typeof for an array, an object and a function.

Show solution
console.log(typeof ['chromium', 'firefox']);
console.log(typeof { retries: 2 });
console.log(typeof function login() {});

Output

object
object
function

Explanation: arrays are objects, so typeof returns 'object'. Functions are a special case and return 'function'.

4. Check for an arrayBeginner

Use Array.isArray to tell an array apart from a plain object.

Show solution
const browsers = ['chromium', 'webkit'];
const config = { browser: 'chromium' };
console.log(Array.isArray(browsers));
console.log(Array.isArray(config));

Output

true
false

Explanation: Array.isArray is the reliable array check, because typeof returns 'object' for both.

5. Integer or decimalBeginner

Check whether 10, 10.5 and -3 are integers.

Show solution
console.log(Number.isInteger(10));
console.log(Number.isInteger(10.5));
console.log(Number.isInteger(-3));

Output

true
false
true

Explanation: JavaScript has one number type for integers and decimals. Number.isInteger checks for whole numbers.

6. NaN is a numberBeginner

Divide a string by a number, then print the result, its type and whether it is NaN.

Show solution
const result = 'abc' / 2;
console.log(result);
console.log(typeof result);
console.log(Number.isNaN(result));

Output

NaN
number
true

Explanation: invalid maths produces NaN (“Not a Number”), which is still of type number.

7. InfinityBeginner

Print the results of dividing 1 by 0 and -1 by 0, and their type.

Show solution
console.log(1 / 0);
console.log(-1 / 0);
console.log(typeof Infinity);

Output

Infinity
-Infinity
number

Explanation: dividing by zero gives Infinity or -Infinity instead of an error.

8. Boolean values from comparisonsBeginner

Store the results of three comparisons in variables and print them with their type.

Show solution
const isPassed = 85 >= 70;
const isEmpty = ''.length === 0;
const isAdmin = 'viewer' === 'admin';
console.log(isPassed, isEmpty, isAdmin);
console.log(typeof isPassed);

Output

true true false
boolean

Explanation: comparison operators always produce a boolean, true or false.

9. null vs undefinedBeginner

Compare null and undefined with == and ===.

Show solution
let notAssigned;
const emptyValue = null;
console.log(notAssigned == emptyValue);
console.log(notAssigned === emptyValue);

Output

true
false

Explanation: loose equality treats null and undefined as equal. Strict equality also compares types, so they differ.

10. String length and charactersBeginner

Print the length of 'Playwright', its first character and its last character.

Show solution
const tool = 'Playwright';
console.log(tool.length);
console.log(tool[0]);
console.log(tool[tool.length - 1]);

Output

10
P
t

Explanation: strings have a length property and characters can be read by index, starting at 0.

JavaScript Intermediate Programs

11. Floating-point precisionIntermediate

Print 0.1 + 0.2, compare it with 0.3, then compare safely using a tolerance.

Show solution
const sum = 0.1 + 0.2;
console.log(sum);
console.log(sum === 0.3);
console.log(Math.abs(sum - 0.3) < Number.EPSILON);

Output

0.30000000000000004
false
true

Explanation: decimals are stored in binary floating point, so some sums are slightly off. Compare with a tolerance instead.

12. Safe integer limitsIntermediate

Print the largest safe integer and show that adding 1 and 2 to it gives the same result.

Show solution
const max = Number.MAX_SAFE_INTEGER;
console.log(max);
console.log(max + 1 === max + 2);
console.log(Number.isSafeInteger(max + 1));

Output

9007199254740991
true
false

Explanation: beyond 2^53 βˆ’ 1, numbers lose precision, so different integers can become equal.

13. BigInt for large IDsIntermediate

Store a large order ID as a BigInt, add 1 and print it.

Show solution
const orderId = 9007199254740993n;
const nextId = orderId + 1n;
console.log(nextId);
console.log(typeof nextId);

Output

9007199254740994n
bigint

Explanation: BigInt (numbers ending in n) stores arbitrarily large integers exactly. You cannot mix BigInt and number in maths.

14. Mixing BigInt and numberIntermediate

Try adding a BigInt to a number and print the error message, then do it correctly.

Show solution
try {
  console.log(10n + 5);
} catch (error) {
  console.log(error.name);
}
console.log(10n + BigInt(5));

Output

TypeError
15n

Explanation: mixing types throws a TypeError. Convert explicitly with BigInt() or Number().

15. Symbols are uniqueIntermediate

Create two symbols with the same description and compare them.

Show solution
const id1 = Symbol('testId');
const id2 = Symbol('testId');
console.log(id1 === id2);
console.log(id1.description);

Output

false
testId

Explanation: every Symbol() call creates a unique value, even with the same description.

16. Primitives are immutableIntermediate

Try to change the first character of a string and print it.

Show solution
let browser = 'chromium';
browser[0] = 'C';
console.log(browser);
browser = browser[0].toUpperCase() + browser.slice(1);
console.log(browser);

Output

chromium
Chromium

Explanation: you cannot change a string in place. Methods return a new string, which you assign back to the variable.

17. Truthy and falsy valuesIntermediate

Print whether each value in a list is truthy or falsy.

Show solution
const samples = [
  [0, '0'], ['', "''"], ['qa', "'qa'"], [null, 'null'], [undefined, 'undefined'],
  [NaN, 'NaN'], [[], '[]'], [{}, '{}'], [-1, '-1']
];
for (const [value, label] of samples) {
  console.log(label, '->', value ? 'truthy' : 'falsy');
}

Output

0 -> falsy
'' -> falsy
'qa' -> truthy
null -> falsy
undefined -> falsy
NaN -> falsy
[] -> truthy
{} -> truthy
-1 -> truthy

Explanation: 0, '', null, undefined, NaN and false are falsy. Empty arrays and objects are truthy.

18. Detect the real typeIntermediate

Write a function that returns 'null', 'array' or the typeof result, and test it with several values.

Show solution
function getType(value) {
  if (value === null) return 'null';
  if (Array.isArray(value)) return 'array';
  return typeof value;
}
console.log(getType(null));
console.log(getType([1, 2]));
console.log(getType({}));
console.log(getType('text'));

Output

null
array
object
string

Explanation: combining checks fixes the two surprises of typeof: null and arrays.

19. Object.prototype.toString type checkIntermediate

Print the internal type tag of a date, an array, null and a regular expression.

Show solution
const tag = value => Object.prototype.toString.call(value);
console.log(tag(new Date(0)));
console.log(tag([]));
console.log(tag(null));
console.log(tag(/test/));

Output

[object Date]
[object Array]
[object Null]
[object RegExp]

Explanation: Object.prototype.toString returns a precise tag such as [object Date], useful for robust type checks.

20. Compare by value vs by referenceIntermediate

Compare two identical arrays with ===, then compare their JSON strings.

Show solution
const expected = ['login', 'logout'];
const actual = ['login', 'logout'];
console.log(expected === actual);
console.log(JSON.stringify(expected) === JSON.stringify(actual));

Output

false
true

Explanation: === compares object references, not contents. For simple data, comparing JSON strings checks the values.

JavaScript Real-World QA Scenario Programs

21. Validate API field typesReal-world

Check that each field in an API user object has the expected type and print any mismatch.

Show solution
const user = { id: 101, name: 'Asha', active: 'yes', tags: ['qa'] };
const schema = { id: 'number', name: 'string', active: 'boolean', tags: 'array' };
for (const [field, expected] of Object.entries(schema)) {
  const value = user[field];
  const actual = Array.isArray(value) ? 'array' : typeof value;
  if (actual !== expected) console.log(`${field}: expected ${expected}, got ${actual}`);
}
console.log('Type check finished');

Output

active: expected boolean, got string
Type check finished

Explanation: simple schema checks catch API contract bugs early, such as a boolean sent as the string 'yes'.

22. Count values by typeReal-world

Count how many values of each type a response array contains.

Show solution
const response = [1, 'two', 3, null, true, 'six', undefined, 8];
const counts = {};
for (const value of response) {
  const type = value === null ? 'null' : typeof value;
  counts[type] = (counts[type] || 0) + 1;
}
console.log(counts);

Output

{ number: 3, string: 2, null: 1, boolean: 1, undefined: 1 }

Explanation: grouping by type helps spot unexpected data, such as nulls in a numeric column.

23. Check for missing valuesReal-world

Report which form fields are empty, null or undefined.

Show solution
const form = { firstName: 'Ravi', lastName: '', email: null, phone: undefined, city: 'Pune' };
const missing = Object.keys(form).filter(key => form[key] === '' || form[key] == null);
console.log('Missing fields:', missing.join(', '));

Output

Missing fields: lastName, email, phone

Explanation: == null matches both null and undefined, and === '' catches empty strings.

24. Verify a price is a valid numberReal-world

Check a list of prices and print whether each one is a finite, non-negative number.

Show solution
const prices = [499, -10, NaN, Infinity, 0, '299'];
for (const price of prices) {
  const valid = typeof price === 'number' && Number.isFinite(price) && price >= 0;
  console.log(String(price), valid ? 'valid' : 'invalid');
}

Output

499 valid
-10 invalid
NaN invalid
Infinity invalid
0 valid
299 invalid

Explanation: Number.isFinite rejects NaN and Infinity, and the typeof check rejects numbers stored as strings.

25. Optional response fieldsReal-world

Read a nested optional field safely and fall back to a default.

Show solution
const responseA = { user: { address: { city: 'Mumbai' } } };
const responseB = { user: {} };
console.log(responseA.user.address?.city ?? 'Unknown');
console.log(responseB.user.address?.city ?? 'Unknown');

Output

Mumbai
Unknown

Explanation: optional chaining returns undefined instead of throwing when a property is missing, and ?? supplies a default.

26. Identify boolean-like stringsReal-world

Convert the CSV values 'true', 'false' and 'TRUE' to real booleans and print their type.

Show solution
const csvValues = ['true', 'false', 'TRUE'];
for (const text of csvValues) {
  const bool = text.toLowerCase() === 'true';
  console.log(text, '->', bool, typeof bool);
}

Output

true -> true boolean
false -> false boolean
TRUE -> true boolean

Explanation: Boolean('false') would be true because the string is non-empty, so compare the text instead.

27. Detect empty arrays and objectsReal-world

Write checks that report whether an API returned an empty list or an empty object.

Show solution
const isEmptyArray = value => Array.isArray(value) && value.length === 0;
const isEmptyObject = value => value !== null && typeof value === 'object' && !Array.isArray(value) && Object.keys(value).length === 0;
console.log(isEmptyArray([]), isEmptyArray([1]));
console.log(isEmptyObject({}), isEmptyObject({ id: 1 }));

Output

true false
true false

Explanation: empty arrays and objects are truthy, so you must check length or Object.keys explicitly.

28. Copy test data safelyReal-world

Make a deep copy of nested test data with structuredClone, change the copy and show the original is unchanged.

Show solution
const baseUser = { name: 'Asha', roles: ['viewer'] };
const adminUser = structuredClone(baseUser);
adminUser.roles.push('admin');
console.log(baseUser.roles);
console.log(adminUser.roles);

Output

[ 'viewer' ]
[ 'viewer', 'admin' ]

Explanation: structuredClone copies nested objects too, so changing the copy does not leak into shared test data.

29. Spread creates a shallow copyReal-world

Copy an object with spread, change a nested array and observe the side effect.

Show solution
const original = { name: 'cart', items: ['pen'] };
const copy = { ...original };
copy.name = 'wishlist';
copy.items.push('book');
console.log(original.name, original.items);

Output

cart [ 'pen', 'book' ]

Explanation: spread copies only the top level. Nested arrays and objects are still shared between the copies.

30. Summarise data types in test dataReal-world

Print a one-line summary of the type of each property in a test case object.

Show solution
const testCase = { id: 'TC-101', priority: 1, automated: true, tags: ['smoke'], owner: null };
const summary = Object.entries(testCase)
  .map(([key, value]) => `${key}:${value === null ? 'null' : Array.isArray(value) ? 'array' : typeof value}`)
  .join(' | ');
console.log(summary);

Output

id:string | priority:number | automated:boolean | tags:array | owner:null

Explanation: a quick type summary is handy when debugging data loaded from JSON or CSV files.

31. Validate a status code typeReal-world

An API returns its status as a string. Show why === comparison with a number fails, and fix it.

Show solution
const response = { status: '200' };
console.log(response.status === 200);
console.log(Number(response.status) === 200);
console.log(typeof response.status);

Output

false
true
string

Explanation: strict equality compares types, so '200' === 200 is false. Convert explicitly before comparing.

Keep Practising

JavaScript Variables Practice Programs with Solutions (32 Programs)

Practise JavaScript variables with 32 programs, each with a full solution, the exact output and a short explanation. The programs start with the basics, move to intermediate problems and finish with real-world scenarios from test automation, such as test data, prices, form inputs and test results. They are written for testers learning JavaScript for Playwright.

How to practise: read the task, write your own solution first, then open Show solution to compare. Run any program with node program.js.

New to this topic? Read the JavaScript Variables lesson first.

JavaScript Beginner Programs

1. Declare and print variablesBeginner

Declare a variable testerName with your name and a variable experienceYears with a number, then print both.

Show solution
let testerName = 'Asha';
let experienceYears = 3;
console.log('Tester:', testerName);
console.log('Experience (years):', experienceYears);

Output

Tester: Asha
Experience (years): 3

Explanation: let declares a variable that holds a value. console.log prints its arguments separated by a space.

2. Reassign a let variableBeginner

Create browser with the value 'chromium', print it, change it to 'firefox' and print it again.

Show solution
let browser = 'chromium';
console.log('Before:', browser);
browser = 'firefox';
console.log('After:', browser);

Output

Before: chromium
After: firefox

Explanation: variables declared with let can be reassigned. The second assignment replaces the old value.

3. Constants cannot be reassignedBeginner

Store a base URL in a const, then try to reassign it inside try/catch and print the error message.

Show solution
const BASE_URL = 'https://qa.example.com';
try {
  BASE_URL = 'https://prod.example.com';
} catch (error) {
  console.log('Error:', error.message);
}
console.log('BASE_URL is still', BASE_URL);

Output

Error: Assignment to constant variable.
BASE_URL is still https://qa.example.com

Explanation: a const binding cannot be reassigned, so the assignment throws a TypeError. The original value stays unchanged.

4. Swap two variables with a temporary variableBeginner

Swap the values of a = 10 and b = 20 using a third variable and print them before and after.

Show solution
let a = 10;
let b = 20;
console.log('Before swap: a =', a, 'b =', b);
let temp = a;
a = b;
b = temp;
console.log('After swap: a =', a, 'b =', b);

Output

Before swap: a = 10 b = 20
After swap: a = 20 b = 10

Explanation: the temporary variable keeps the first value safe while it is overwritten.

5. Swap two variables with destructuringBeginner

Swap first = 'login' and second = 'logout' without a temporary variable.

Show solution
let first = 'login';
let second = 'logout';
[first, second] = [second, first];
console.log(first, second);

Output

logout login

Explanation: array destructuring builds a temporary array [second, first] and unpacks it back into the two variables in one step.

6. Declare several variables in one statementBeginner

Declare passed, failed and skipped test counts in a single let statement and print the total.

Show solution
let passed = 42, failed = 3, skipped = 5;
let total = passed + failed + skipped;
console.log('Total tests:', total);

Output

Total tests: 50

Explanation: one let can declare several comma-separated variables. Readability usually favours one per line in real projects.

7. Uninitialised variableBeginner

Declare result without a value and print it and its type.

Show solution
let result;
console.log(result);
console.log(typeof result);

Output

undefined
undefined

Explanation: a declared but unassigned variable holds undefined, whose type is also 'undefined'.

8. Build a message with a template literalBeginner

Use variables user = 'qa_admin' and role = 'Admin' to print Logged in as qa_admin (Admin).

Show solution
const user = 'qa_admin';
const role = 'Admin';
const message = `Logged in as ${user} (${role})`;
console.log(message);

Output

Logged in as qa_admin (Admin)

Explanation: template literals (backticks) insert variable values with ${...}.

9. Increment a counterBeginner

Start retryCount at 0, increase it three times using different syntax and print it after each step.

Show solution
let retryCount = 0;
retryCount = retryCount + 1;
console.log(retryCount);
retryCount += 1;
console.log(retryCount);
retryCount++;
console.log(retryCount);

Output

1
2
3

Explanation: x = x + 1, x += 1 and x++ all add one to a let variable.

10. Valid variable namesBeginner

Create variables named with camelCase, a leading underscore and a dollar sign, then print them together.

Show solution
const firstName = 'Ravi';
const _internalId = 101;
const $price = 499;
console.log(firstName, _internalId, $price);

Output

Ravi 101 499

Explanation: names may start with a letter, _ or $, and must not start with a digit. camelCase is the usual convention.

JavaScript Intermediate Programs

11. Block scope with letIntermediate

Declare status outside an if block and a different status inside it. Print both to show they are separate.

Show solution
let status = 'outer';
if (true) {
  let status = 'inner';
  console.log('Inside block:', status);
}
console.log('Outside block:', status);

Output

Inside block: inner
Outside block: outer

Explanation: let is block-scoped, so the inner status exists only inside the braces and does not affect the outer one.

12. var ignores block scopeIntermediate

Repeat the previous program using var and observe the difference.

Show solution
var status = 'outer';
if (true) {
  var status = 'inner';
  console.log('Inside block:', status);
}
console.log('Outside block:', status);

Output

Inside block: inner
Outside block: inner

Explanation: var is function-scoped, so both declarations refer to the same variable and the block overwrites it.

13. Hoisting with varIntermediate

Print a var variable before the line that declares it.

Show solution
console.log('Before declaration:', environment);
var environment = 'staging';
console.log('After declaration:', environment);

Output

Before declaration: undefined
After declaration: staging

Explanation: var declarations are hoisted and initialised with undefined, so the first log prints undefined instead of throwing.

14. Temporal dead zone with letIntermediate

Access a let variable before its declaration inside try/catch and print the error name.

Show solution
try {
  console.log(timeout);
  let timeout = 30000;
} catch (error) {
  console.log(error.name + ': ' + error.message);
}

Output

ReferenceError: Cannot access 'timeout' before initialization

Explanation: let and const are hoisted but not initialised. Using them before the declaration throws a ReferenceError (the temporal dead zone).

15. const object can still changeIntermediate

Store test settings in a const object, change one property and print the object.

Show solution
const settings = { headless: true, retries: 0 };
settings.retries = 2;
console.log(settings);

Output

{ headless: true, retries: 2 }

Explanation: const stops reassignment of the variable, not changes to the object it points to.

16. Freeze a configuration objectIntermediate

Freeze a config object, try to change it and print it to show the change was ignored.

Show solution
const config = Object.freeze({ browser: 'chromium', workers: 4 });
config.workers = 8;
console.log(config.workers);
console.log(Object.isFrozen(config));

Output

4
true

Explanation: Object.freeze makes properties read-only. In non-strict code the assignment is silently ignored.

17. Loop variable scope: let vs varIntermediate

Collect functions in a loop using let and using var, then call them to see which values they remember.

Show solution
const withLet = [];
for (let i = 0; i < 3; i++) withLet.push(() => i);
const withVar = [];
for (var j = 0; j < 3; j++) withVar.push(() => j);
console.log('let:', withLet.map(f => f()));
console.log('var:', withVar.map(f => f()));

Output

let: [ 0, 1, 2 ]
var: [ 3, 3, 3 ]

Explanation: let creates a new binding for each loop iteration. var shares one variable, so every function sees its final value 3.

18. Shadowing in nested blocksIntermediate

Use the same name level in three nested scopes and print it from each one.

Show solution
const level = 'file';
{
  const level = 'block';
  {
    const level = 'nested block';
    console.log(level);
  }
  console.log(level);
}
console.log(level);

Output

nested block
block
file

Explanation: an inner declaration shadows outer ones with the same name. Each scope sees its nearest declaration.

19. Variables copy primitives by valueIntermediate

Copy a number into another variable, change the copy and print both.

Show solution
let originalTimeout = 5000;
let copiedTimeout = originalTimeout;
copiedTimeout = 10000;
console.log('original:', originalTimeout);
console.log('copy:', copiedTimeout);

Output

original: 5000
copy: 10000

Explanation: primitives such as numbers are copied by value, so changing the copy does not affect the original.

20. Variables share objects by referenceIntermediate

Assign an object to a second variable, change it through the second variable and print the first.

Show solution
const user = { name: 'Asha', active: true };
const sameUser = user;
sameUser.active = false;
console.log(user.active);
console.log(user === sameUser);

Output

false
true

Explanation: both variables point to the same object, so a change through either one is visible through both.

JavaScript Real-World QA Scenario Programs

21. Store environment URLsReal-world

Store base URLs for qa, staging and prod in constants, pick one with a selectedEnv variable and print the login URL.

Show solution
const QA_URL = 'https://qa.shop.test';
const STAGING_URL = 'https://staging.shop.test';
const PROD_URL = 'https://shop.test';
const selectedEnv = 'staging';
const baseUrl = selectedEnv === 'qa' ? QA_URL : selectedEnv === 'staging' ? STAGING_URL : PROD_URL;
const loginUrl = `${baseUrl}/login`;
console.log(loginUrl);

Output

https://staging.shop.test/login

Explanation: constants keep fixed values in one place, and a single variable decides which environment the test uses.

22. Build unique test dataReal-world

Create a unique email for a sign-up test using a fixed runId variable and a username prefix.

Show solution
const prefix = 'qa.user';
const runId = 1730275200;
const email = `${prefix}+${runId}@example.com`;
console.log(email);

Output

qa.user+1730275200@example.com

Explanation: combining a stable prefix with a run ID gives unique but readable test data. In real tests the ID could come from Date.now().

23. Count test resultsReal-world

Use counter variables to tally the statuses ['passed', 'failed', 'passed', 'skipped', 'passed'].

Show solution
const statuses = ['passed', 'failed', 'passed', 'skipped', 'passed'];
let passed = 0, failed = 0, skipped = 0;
for (const status of statuses) {
  if (status === 'passed') passed++;
  else if (status === 'failed') failed++;
  else skipped++;
}
console.log(`Passed: ${passed}, Failed: ${failed}, Skipped: ${skipped}`);

Output

Passed: 3, Failed: 1, Skipped: 1

Explanation: counters declared with let are updated inside the loop. The loop variable status is a fresh const each time.

24. Calculate a pass percentageReal-world

With total = 80 and failed = 6, calculate and print the pass percentage rounded to one decimal place.

Show solution
const total = 80;
const failed = 6;
const passed = total - failed;
const passRate = (passed / total) * 100;
console.log(`Pass rate: ${passRate.toFixed(1)}%`);

Output

Pass rate: 92.5%

Explanation: intermediate variables like passed make the calculation readable. toFixed(1) formats the result.

25. Default timeout with fallbackReal-world

A variable customTimeout may be undefined. Use ?? to fall back to a default of 30000 ms and print the timeout used in both cases.

Show solution
const DEFAULT_TIMEOUT = 30000;
let customTimeout;
console.log('Timeout used:', customTimeout ?? DEFAULT_TIMEOUT);
customTimeout = 60000;
console.log('Timeout used:', customTimeout ?? DEFAULT_TIMEOUT);

Output

Timeout used: 30000
Timeout used: 60000

Explanation: ?? returns the right side only when the left side is null or undefined, which makes it ideal for optional settings.

26. Track login attemptsReal-world

Simulate three failed login attempts with a let counter and lock the account when it reaches MAX_ATTEMPTS = 3.

Show solution
const MAX_ATTEMPTS = 3;
let attempts = 0;
let locked = false;
for (let i = 1; i <= 3; i++) {
  attempts++;
  console.log(`Attempt ${attempts} failed`);
  if (attempts === MAX_ATTEMPTS) locked = true;
}
console.log('Account locked:', locked);

Output

Attempt 1 failed
Attempt 2 failed
Attempt 3 failed
Account locked: true

Explanation: a constant holds the business rule and let variables track state that changes during the scenario.

27. Build a test report lineReal-world

Store the test name, duration in milliseconds and status in variables, then print one formatted report line with the duration in seconds.

Show solution
const testName = 'Checkout with saved card';
const durationMs = 2345;
const status = 'PASSED';
const durationSec = (durationMs / 1000).toFixed(2);
console.log(`[${status}] ${testName} (${durationSec}s)`);

Output

[PASSED] Checkout with saved card (2.35s)

Explanation: separate variables for each piece of data keep the formatting step simple and easy to change.

28. Running cart totalReal-world

Add item prices 499, 1299 and 250 to a running cartTotal, printing the total after each item.

Show solution
let cartTotal = 0;
cartTotal += 499;
console.log('After item 1:', cartTotal);
cartTotal += 1299;
console.log('After item 2:', cartTotal);
cartTotal += 250;
console.log('After item 3:', cartTotal);

Output

After item 1: 499
After item 2: 1798
After item 3: 2048

Explanation: += updates the running total in place, which mirrors how a test checks a cart after each step.

29. Mask a password before loggingReal-world

Store a password in a variable and log a masked version that shows only the last two characters.

Show solution
const password = 'Secr3t!Pass';
const masked = '*'.repeat(password.length - 2) + password.slice(-2);
console.log('Password used:', masked);

Output

Password used: *********ss

Explanation: never log real secrets. A derived variable holds a safe version for logs and reports.

30. Read settings with fallbacksReal-world

Given envVars = { BASE_URL: 'https://qa.app.test' }, read BASE_URL and WORKERS into variables, defaulting WORKERS to 2.

Show solution
const envVars = { BASE_URL: 'https://qa.app.test' };
const baseUrl = envVars.BASE_URL ?? 'http://localhost:3000';
const workers = Number(envVars.WORKERS ?? 2);
console.log('baseUrl:', baseUrl);
console.log('workers:', workers);

Output

baseUrl: https://qa.app.test
workers: 2

Explanation: tests often read process.env, where values may be missing. Defaults keep the run predictable.

31. Destructure test data into variablesReal-world

Unpack username, password and an optional role (default 'viewer') from a test-data object.

Show solution
const testUser = { username: 'qa_tester', password: 'P@ss123' };
const { username, password, role = 'viewer' } = testUser;
console.log(username, role);
console.log('Password length:', password.length);

Output

qa_tester viewer
Password length: 7

Explanation: object destructuring creates several variables at once and can supply defaults for missing properties.

32. Rename while destructuringReal-world

An API returns { id: 7, full_name: 'Neha Sharma' }. Store the values in camelCase variables userId and fullName.

Show solution
const apiUser = { id: 7, full_name: 'Neha Sharma' };
const { id: userId, full_name: fullName } = apiUser;
console.log(`User #${userId}: ${fullName}`);

Output

User #7: Neha Sharma

Explanation: property: newName in destructuring renames a value, which keeps your code in camelCase even when an API uses snake_case.

Keep Practising

Playwright Cheat Sheet (TypeScript): Commands, Locators, Assertions and Config

Everything you use daily in Playwright with TypeScript, on one page: commands, locators, actions, assertions, waits, config, fixtures, API testing, debugging and CI. Bookmark it and keep it open while you code.

πŸ“– What You’ll Learn: CLI commands, test structure and annotations, locators, actions, assertions, waits, playwright.config.ts essentials, fixtures and page objects, API testing and network mocking, debugging and reports, a minimal CI workflow, AI tools, and the top 10 mistakes to avoid.


πŸ“Œ How to Use This Cheat Sheet

A one-page reference for Playwright Test with TypeScript. Each section links to the full lesson in the Playwright Tutorial with TypeScript. To keep a copy, use your browser’s Print, Save as PDF.


βš™οΈ Setup and CLI

npm init playwright@latest                 # new project (choose TypeScript)
npx playwright install --with-deps         # browsers + Linux dependencies
npx playwright test                        # run all tests
npx playwright test login.spec.ts:12       # one test by line
npx playwright test -g "checkout"          # by title
npx playwright test --grep @smoke          # by tag
npx playwright test --project=firefox      # one browser project
npx playwright test --headed               # visible browser
npx playwright test --ui                   # UI mode
npx playwright test --debug                # Inspector
npx playwright test --workers=4 --retries=2
npx playwright test --repeat-each=10       # find flaky tests
npx playwright test --last-failed
npx playwright test --shard=1/3 --reporter=blob
npx playwright merge-reports --reporter html ./all-blob-reports
npx playwright show-report
npx playwright show-trace trace.zip
npx playwright codegen https://example.com
npx playwright test --update-snapshots

Lessons: install, CLI commands, codegen


πŸ§ͺ Test Structure

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

test.describe('Cart', { tag: '@regression' }, () => {
  test.beforeEach(async ({ page }) => { await page.goto('/'); });

  test('adds an item', { tag: '@smoke' }, async ({ page }, testInfo) => {
    await test.step('add backpack', async () => { /* ... */ });
  });
});

test.only(...);  test.skip(...);  test.fixme(...);  test.slow();
test.describe.configure({ mode: 'parallel' });   // or 'serial'
test.use({ viewport: { width: 390, height: 844 } });

Lessons: first test, hooks, tags, annotations


🎯 Locators

LocatorExample
Rolepage.getByRole('button', { name: 'Login' })
Labelpage.getByLabel('Email')
Placeholderpage.getByPlaceholder('Username')
Textpage.getByText('Products', { exact: true })
Alt text / titlepage.getByAltText('logo'), page.getByTitle('Close')
Test idpage.getByTestId('checkout')
CSSpage.locator('#user-name'), page.locator('[data-test="error"]')
XPathpage.locator("//td[text()='Jason']/following-sibling::td[2]")
Filterpage.locator('tr').filter({ hasText: 'Jason' })
Chainpage.locator('form').getByRole('button')
Pick one.first(), .last(), .nth(2)
iFramepage.frameLocator('#frame').getByRole('button')

Lessons: locators, CSS and XPath


πŸ–±οΈ Actions

await page.goto('/inventory.html');
await locator.click();                     // { button: 'right' }, { clickCount: 2 }, { modifiers: ['Shift'] }
await locator.dblclick();
await locator.fill('text');                // clear + set value
await locator.pressSequentially('abc', { delay: 100 });
await locator.press('Enter');              // 'Control+A', 'ArrowDown', 'Tab'
await locator.check();  await locator.uncheck();  await locator.setChecked(true);
await locator.selectOption({ label: 'Option 2' });
await locator.setInputFiles('data/file.pdf');
await locator.hover();
await locator.dragTo(target);
await locator.scrollIntoViewIfNeeded();
await page.keyboard.press('Escape');

Lessons: actions, alerts, popups, iframes, tabs


βœ… Assertions

AssertionUse
await expect(page).toHaveURL(/inventory/)Current URL
await expect(page).toHaveTitle('Swag Labs')Page title
await expect(locator).toBeVisible() / toBeHidden()Visibility
await expect(locator).toHaveText('Products')Exact text
await expect(locator).toContainText('locked')Partial text
await expect(locator).toHaveValue('42')Input value
await expect(locator).toHaveCount(6)Number of elements
await expect(locator).toBeChecked() / toBeEnabled() / toBeDisabled()State
await expect(locator).toHaveAttribute('href', '/cart')Attribute
await expect(locator).toHaveClass(/active/)CSS class
await expect(page).toHaveScreenshot('home.png')Visual comparison
await expect(response).toBeOK()API response 2xx
expect.soft(...)Continue after a failure
expect(value).toEqual(obj) / toMatchObject(obj)Plain values (no await)

Lessons: assertions, visual testing


⏳ Waits

await expect(locator).toBeVisible({ timeout: 10_000 });   // preferred
await locator.waitFor({ state: 'hidden' });
await page.waitForURL('**/inventory.html');
await page.waitForLoadState('domcontentloaded');
const resp = page.waitForResponse(r => r.url().includes('/api/') && r.ok());
await button.click(); await resp;                        // start waiting BEFORE the action
const popup = page.waitForEvent('popup');  await link.click();  const newTab = await popup;
await expect.poll(async () => getStatus()).toBe('DONE');
// Avoid: page.waitForTimeout(5000)

Lesson: Playwright waits


πŸ› οΈ playwright.config.ts Essentials

import { defineConfig, devices } from '@playwright/test';

export default defineConfig({
  testDir: './tests',
  timeout: 30_000,
  expect: { timeout: 5_000 },
  fullyParallel: true,
  forbidOnly: !!process.env.CI,
  retries: process.env.CI ? 2 : 0,
  workers: process.env.CI ? 2 : undefined,
  reporter: [['list'], ['html', { open: 'never' }], ['junit', { outputFile: 'results/junit.xml' }]],
  use: {
    baseURL: process.env.BASE_URL ?? 'https://www.saucedemo.com',
    testIdAttribute: 'data-test',
    trace: 'on-first-retry',
    screenshot: 'only-on-failure',
    video: 'retain-on-failure',
  },
  projects: [
    { name: 'setup', testMatch: /.*\.setup\.ts/ },
    { name: 'chromium', use: { ...devices['Desktop Chrome'], storageState: 'playwright/.auth/user.json' }, dependencies: ['setup'] },
    { name: 'mobile', use: { ...devices['iPhone 13'] } },
  ],
});

Lessons: config file, cross-browser, mobile, login sessions


🧩 Fixtures and Page Objects

// Page object
export class LoginPage {
  readonly username: Locator;
  constructor(private readonly page: Page) {
    this.username = page.getByPlaceholder('Username');
  }
  async login(u: string, p: string) { /* fill + click */ }
}

// Custom fixtures
export const test = base.extend<{ loginPage: LoginPage }>({
  loginPage: async ({ page }, use) => { await use(new LoginPage(page)); },
});

// Worker-scoped fixture
account: [async ({}, use, workerInfo) => { await use('user_' + workerInfo.workerIndex); }, { scope: 'worker' }],

Lessons: POM, fixtures, custom fixtures, framework structure


πŸ”Œ API Testing and Network

const res = await request.get('/posts', { params: { userId: 1 } });
await request.post('/posts', { data: { title: 'x' } });       // JSON body
await request.post('/login', { form: { email, password } });    // form body
await request.put('/posts/1', { data });  await request.patch('/posts/1', { data });
await request.delete('/posts/1');
expect(res.status()).toBe(200);  const body = await res.json();

await page.route('**/api/items', r => r.fulfill({ json: [] }));              // mock
await page.route('**/api/items', async r => { const resp = await r.fetch(); await r.fulfill({ response: resp, json: { ...(await resp.json()), extra: 1 } }); });
await page.route('**/*.{png,jpg}', r => r.abort());                            // block
page.on('console', m => console.log(m.text()));

Lessons: API testing, network mocking


πŸ“Έ Debugging, Screenshots and Reports

await page.pause();                                        // open Inspector (headed)
await page.screenshot({ path: 'shot.png', fullPage: true, mask: [locator] });
await locator.screenshot({ path: 'element.png' });
await testInfo.attach('data', { body: JSON.stringify(obj), contentType: 'application/json' });
console.log(testInfo.retry, testInfo.workerIndex, testInfo.tags);

Lessons: debugging, Trace Viewer, screenshots and videos, reporters


πŸš€ CI in One Minute (GitHub Actions)

- uses: actions/checkout@v4
- uses: actions/setup-node@v4
  with: { node-version: lts/* }
- run: npm ci
- run: npx playwright install --with-deps
- run: npx playwright test
- uses: actions/upload-artifact@v4
  if: always()
  with: { name: playwright-report, path: playwright-report/ }

Lessons: parallel and sharding, GitHub Actions, Jenkins, Docker


πŸ€– AI Tools

claude mcp add playwright npx @playwright/mcp@latest     # Playwright MCP in Claude Code
npx playwright init-agents --loop=vscode                 # planner, generator, healer agents

Lessons: Playwright MCP, Test Agents


🚫 Top 10 Mistakes to Avoid

  1. Missing await before actions and web-first assertions.
  2. page.waitForTimeout() instead of waiting for a condition.
  3. expect(await locator.textContent()) instead of await expect(locator).toHaveText().
  4. force: true to “fix” covered elements.
  5. Long CSS or absolute XPath selectors.
  6. Tests that depend on each other or share data.
  7. Logging in through the UI in every test instead of storageState.
  8. Selectors and URLs hard-coded inside tests.
  9. Committing test.only, page.pause() or secrets.
  10. Raising retries instead of fixing flaky tests.

❓ FAQ

πŸ€” Is there a Playwright cheat sheet PDF?

Use your browser’s Print, Save as PDF on this page to keep an offline copy. The page is updated as Playwright evolves.

πŸ€” What are the most important Playwright commands to remember?

npx playwright test with -g, --grep, --project, --headed, --ui and --debug, plus show-report, show-trace and codegen.

πŸ€” Which Playwright locators should I memorize first?

getByRole, getByLabel, getByPlaceholder, getByText, getByTestId, and locator() with filter for everything else.


🎯 Next Steps

🎬 Watch the full Playwright tutorial series on YouTube!

Subscribe to SqaToolsByDY • Practice on sqatools.in

Playwright Interview Questions for Experienced (2, 3 and 5+ Years) with Answers

Experienced Playwright interviews focus on decisions, trade-offs and real problems you have solved. This guide groups the most common questions by experience level, with clear model answers and scenario questions used in SDET and automation lead interviews.

πŸ“– What You’ll Learn: how experienced interviews differ, questions for 2 years (fundamentals in depth), 3 years (framework, data, flakiness, API and UI), 5+ years (strategy, scale, migration, patterns, CI/CD, AI), scenario-based questions, and interview tips.


🎯 How Experienced Interviews Differ

Fresher interviews ask what a feature is. Experienced interviews ask why you chose it, how you scaled it, and what went wrong. Interviewers listen for real decisions, trade-offs and numbers (“we cut the suite from 40 to 9 minutes by…”).

Use this structure for every answer: concept, how you used it, the result. Questions are grouped by the experience level at which they are most commonly asked; prepare one level above your own.

For fundamentals, start with 200 Playwright + TypeScript interview questions.


🟒 2 Years of Experience: Strong Fundamentals

πŸ€” How does Playwright’s auto-waiting work, and when do you still need explicit waits?

Before each action Playwright checks that the element is attached, visible, stable, enabled and receives events (and editable for fill). Web-first assertions retry until they pass. Explicit waits are still useful for non-element conditions, such as waitForResponse for an API call, waitForURL after navigation, or waitForEvent('popup'). I avoid waitForTimeout completely. See Playwright waits.

πŸ€” What is the difference between page.locator() and page.$()?

locator() is lazy and re-resolves the element every time it is used, with auto-waiting and strictness. page.$() returns an ElementHandle immediately, which can become stale and does not auto-wait. Locators are the recommended API; element handles are discouraged.

πŸ€” Which locator strategy do you prefer and why?

User-facing locators first: getByRole with a name, then getByLabel, getByPlaceholder, getByText, then getByTestId. CSS for ids and test attributes, XPath only when I need text plus structure or parent navigation. They are resilient to styling changes and reflect what users see. See CSS and XPath selectors.

πŸ€” What is strict mode in Playwright?

Actions on a locator fail if it matches more than one element. It prevents clicking the wrong element. I fix violations by making the locator more specific or using filter, not by adding first() everywhere.

πŸ€” What is the difference between toHaveText and textContent with toBe?

await expect(locator).toHaveText() is a web-first assertion that retries until the text matches. expect(await locator.textContent()).toBe() reads once and fails if the page has not updated yet, which causes flakiness.

πŸ€” How do you handle a new tab, an alert and an iframe?

New tab: start page.waitForEvent('popup') before the click. Alert: page.once('dialog', d => d.accept()) before the action; Playwright dismisses dialogs by default. Iframe: page.frameLocator() or locator.contentFrame(). See alerts, popups, iframes and tabs.

πŸ€” What is a browser context and why is it important?

An isolated, incognito-like session with its own cookies and storage. Each test gets a fresh context, which gives isolation without restarting the browser. Contexts also let one test simulate multiple users.

πŸ€” How do you run a single test, a tag or a browser from the command line?

npx playwright test file.spec.ts:12, --grep @smoke, --project=firefox, and --ui or --debug for investigation. See CLI commands.


πŸ”΅ 3 Years of Experience: Framework and Reliability

πŸ€” Describe the architecture of the Playwright framework you built.

Tests in tests/ grouped by feature; src/pages for page objects and components; src/api for API clients; src/fixtures extending test to inject pages, API clients and data; typed data and factories; config per environment through TEST_ENV; a setup project that saves storageState; tags for smoke and regression; HTML and JUnit reporters; GitHub Actions with sharding. Link this to framework structure and give your own numbers.

πŸ€” Fixtures or hooks: when do you use which?

Hooks for simple, file-specific setup. Fixtures for anything reusable: they combine setup and teardown, run only when requested, can be test- or worker-scoped and pass values directly into tests. In our framework all page objects, API clients and test users come from fixtures. See hooks and custom fixtures.

πŸ€” How do you avoid logging in through the UI in every test?

A setup project logs in once per role and saves storageState to a file; test projects depend on it and load that state. For multiple roles I use one storage file per role and a fixture or project per role. See save login session.

πŸ€” How do you handle test data in parallel runs?

Each test creates its own data through the API with unique values (timestamp plus workerIndex) and cleans it up in the same fixture. Shared static data is read-only. Worker-scoped fixtures provide one account per worker when accounts are expensive to create.

πŸ€” A test is flaky in CI but passes locally. What do you do?

Reproduce with --repeat-each and several workers; enable trace: 'on-first-retry' and inspect the failing attempt in the Trace Viewer: actionability log, DOM snapshot, network and console. Common causes are timing assumptions, shared data, environment differences and overlays. Fix the root cause; retries are only a safety net. See flaky tests and Trace Viewer.

πŸ€” How do you combine API and UI testing?

API for setup, cleanup and fast verification; UI for user-visible behaviour. For example, create an order through the API, then verify it on the orders page. page.request shares cookies with the page when the API needs the logged-in session. See the hybrid framework project.

πŸ€” When do you mock network calls?

For error, empty and edge states, unreliable third-party services and features whose backend is not ready. Critical end-to-end journeys run against real services. I use page.route with fulfill, route.fetch to modify real responses, and HAR replay for stable fixtures. See network mocking.

πŸ€” How do you run tests across environments?

Base URLs per environment in config, selected by an environment variable; secrets from CI secret stores; .env locally with .env.example committed. Projects or separate config files for API and UI.

πŸ€” How do you implement visual testing, and how do you keep it stable?

toHaveScreenshot on key pages and components, with masks for dynamic content, small tolerances and baselines generated in the same Docker image as CI. Visual tests run in one browser project. See visual testing.


🟠 5+ Years of Experience: Architecture, Scale and Strategy

πŸ€” How would you design a test automation strategy for a new product?

Start from risk and the test pyramid: most checks at unit and API level, a focused set of end-to-end journeys in Playwright, visual checks for key screens, and contract tests between services. Define quality gates (smoke on every PR, full regression nightly), environments and data strategy, ownership, and metrics such as pipeline time, flaky rate and escaped defects.

πŸ€” How do you scale a suite of 3,000 Playwright tests?

fullyParallel plus sharding across CI machines with merged blob reports; login and data setup through API and storage state; tag-based selection so PRs run only relevant suites; trace on retry only; resource blocking where safe; regular removal of low-value or duplicate tests; and monitoring of the slowest tests. See parallel execution and sharding.

πŸ€” How do you measure and reduce flakiness at team level?

Track flaky tests from CI reports over time, fail release pipelines on flaky tests (--fail-on-flaky-tests), quarantine with a tag and a ticket, and require a root-cause fix within a set time. Review new tests with --repeat-each before merging.

πŸ€” How do you migrate a Selenium suite to Playwright?

Assess value first; do not port everything. Build the new framework skeleton, start with smoke journeys, run both suites in parallel for a period, migrate feature by feature with a concept map (waits to web-first assertions, switchTo to frameLocator and popup events), and retire Selenium tests as coverage moves. Measure execution time and flakiness before and after. See Playwright vs Selenium vs Cypress.

πŸ€” Which design patterns do you use in a Playwright framework?

Page Object and component objects, fixtures as dependency injection, factory and builder for test data, strategy for environment-specific behaviour, and facades for multi-step business flows. SOLID principles keep classes small and replaceable. See design patterns and SOLID.

πŸ€” How do you make the framework maintainable for a large team?

Clear layering and naming conventions, ESLint with the Playwright plugin, type checking in CI, code review checklists, documentation and examples, shared fixtures owned by the platform team, and pairing for onboarding.

πŸ€” How do you integrate Playwright into CI/CD and release decisions?

PR gate with smoke tests and type checks, nightly cross-browser regression, post-deployment smoke against each environment, reports and traces as artifacts, JUnit for dashboards, and notifications through a custom reporter. A failed gate blocks the merge or deployment. See GitHub Actions and Jenkins.

πŸ€” How are you using AI in test automation?

Playwright MCP for exploration and drafting, and Playwright Test Agents to plan, generate and heal tests, always with human review of plans, generated code and healed diffs. AI speeds up authoring and maintenance; deterministic tests in CI remain the source of truth. See Playwright MCP and Test Agents.


🧩 Scenario-Based Questions

πŸ€” The login page takes 8 seconds in the test environment and 200 tests log in through the UI. What do you change?

Log in once per role in a setup project and reuse storageState, or create sessions through an API. That removes 200 slow UI logins and their flakiness.

πŸ€” A button is sometimes covered by a toast notification and the click fails. How do you fix it?

Wait for the toast to disappear (await expect(toast).toBeHidden()) or close it, then click. Do not use force: true, which hides real user-facing problems.

πŸ€” Your suite takes 45 minutes on one CI machine. The team wants feedback in under 10. What is your plan?

Measure first (slowest tests, setup time). Then: fullyParallel, 4 shards with merged reports, API-based setup, storage state, smoke subset on PRs and full suite nightly, and remove redundant UI tests already covered at API level.

πŸ€” A healer agent “fixed” a failing test by changing the expected total. What do you do?

Reject the change and investigate: a changed expected value may hide a real bug. Healed changes are reviewed like any code change; skipped or changed assertions require confirmation from the product or development team.

πŸ€” Tests pass alone but fail when run in parallel. What is the likely cause?

Shared state: the same user, cart or record used by several tests. Make data unique per test or per worker and remove dependencies between tests.


πŸ’‘ Tips for the Interview

  • Prepare two stories: one about improving speed, one about fixing flakiness, each with numbers.
  • Be ready to share your screen and walk through your framework; a public project from Project 2 or Project 3 is ideal.
  • For lead roles, also review QA automation lead interview questions and manager round questions.
  • Practise writing a test live, without an IDE’s autocomplete, for a simple login and an API call.

🎯 Next Steps

🎬 Watch the full Playwright tutorial series on YouTube!

Subscribe to SqaToolsByDY • Practice on sqatools.in

Playwright Practice Assignments: Module-wise Exercises for Beginners to Advanced

Reading is not enough: you learn Playwright by building. This page gives you one checkpoint challenge per module of the course, using free practice sites, so you can prove your skills step by step from your first test to a CI-ready framework.

πŸ“– What You’ll Learn: how to use the checkpoints, the practice applications, a challenge for each of the 15 modules (setup, JavaScript, TypeScript, locators, assertions, organization, data, network, API, framework design, debugging, CI/CD, AI testing, projects and interview readiness).


πŸ‹οΈ How to Use This Page

Every lesson in the Playwright Tutorial with TypeScript ends with its own assignment. This page adds one module checkpoint per module: a slightly bigger challenge that combines the lessons of that module. Complete it before moving on.

Rules for every checkpoint:

  • Write the solution in TypeScript with Playwright Test.
  • Every test must pass three times in a row: npx playwright test --repeat-each=3.
  • No waitForTimeout, no force: true, no selectors inside tests from Module 10 onward.
  • Commit each checkpoint to Git with a clear message, so your repository shows your progress.

Practice applications used: https://www.saucedemo.com, https://the-internet.herokuapp.com, https://demo.playwright.dev/api-mocking, https://jsonplaceholder.typicode.com, https://automationexercise.com and the SQA Tools practice pages.

LevelModulesBadge
🟒 Beginner1 to 5Playwright Practitioner
πŸ”΅ Intermediate6 to 9Playwright Engineer
🟠 Advanced10 to 13Playwright Architect
🟣 Career14 to 15Interview Ready

🟒 Module 1 Checkpoint: Getting Started

Lessons: What Is Playwright, Install, First Test, Config, CLI

  1. Create a new TypeScript project with npm init playwright@latest and remove the example test.
  2. Configure baseURL for Sauce Demo, list plus html reporters, screenshots on failure and Chromium plus Firefox projects.
  3. Write three login tests (valid, locked out, empty form) in one describe block.
  4. Add npm scripts test, test:headed, test:ui and report.
  5. Write a short README explaining how to install and run the project.

🟒 Module 2 Checkpoint: JavaScript Essentials

Lessons: Variables to Promises and Async/Await

  1. Write a function calculateCartTotal(items) that takes an array of { name, price, qty } objects and returns the total rounded to two decimals.
  2. Write a function that returns the most expensive item using reduce.
  3. Write an async function fetchUser(id) that calls https://jsonplaceholder.typicode.com/users/<id> with fetch and handles errors with try/catch.
  4. Fetch users 1 to 5 in parallel with Promise.all and print their names.

🟒 Module 3 Checkpoint: TypeScript for Playwright

Lessons: TypeScript Variables to TypeScript for Playwright

  1. Create interfaces Product and CartItem, and a type SortOption = 'az' | 'za' | 'lohi' | 'hilo'.
  2. Create a generic class Repository<T extends { id: number }> with add, findById and all methods.
  3. Rewrite calculateCartTotal from Module 2 with full types.
  4. Add tsconfig.json with strict: true and make npx tsc --noEmit pass.

🟒 Module 4 Checkpoint: Locators and Interactions

Lessons: Locators, CSS and XPath, Actions, Codegen, Alerts, Frames, Tabs, Web Tables, Downloads

On the-internet.herokuapp.com:

  1. Accept, dismiss and answer the three JavaScript dialogs on /javascript_alerts.
  2. Open the new window on /windows and assert its heading, then return to the first tab.
  3. Read the text of the middle frame on /nested_frames.
  4. On /tables, find the person with the highest amount due in #table1 by reading all rows.
  5. Select both options on /dropdown, tick both boxes on /checkboxes, and upload a file on /upload.

🟒 Module 5 Checkpoint: Assertions, Waits and Screenshots

Lessons: Assertions, Waits, Screenshots and Videos, Visual Testing

  1. On /dynamic_loading/1 of the-internet, click Start and assert “Hello World!” appears, using only auto-waiting and web-first assertions.
  2. Use expect.soft to check five product prices on Sauce Demo in one test.
  3. Configure screenshots and videos on failure and attach a custom screenshot with testInfo.attach.
  4. Create visual baselines for the Sauce Demo login page (full page and .login-box element), masking the credentials panel.

πŸ”΅ Module 6 Checkpoint: Organizing Tests

Lessons: Hooks, Tags and grep, Annotations, Fixtures, Custom Fixtures, Retries and Flaky Tests

  1. Replace every beforeEach login in your Sauce Demo suite with a custom loggedInPage fixture.
  2. Tag tests @smoke and @regression and add npm scripts for each.
  3. Mark one test test.fixme with a reason and one test.slow.
  4. Configure retries for CI only with trace: 'on-first-retry', write a deliberately flaky test, and find it under Flaky in the report. Then delete it.

πŸ”΅ Module 7 Checkpoint: Test Data, Configuration and Cross-Browser

Lessons: Parameterized Tests, JSON, CSV, .env, Multiple Configs, Cross-Browser, Mobile

  1. Move all login test data into test-data/logins.json and generate one test per row.
  2. Read the Sauce Demo password from a .env file and add .env.example.
  3. Run the suite in Chromium, Firefox, WebKit, branded Chrome and a Pixel 5 project.
  4. Add responsive tests for 375, 768 and 1440 px widths.

πŸ”΅ Module 8 Checkpoint: Authentication and Network

Lessons: Login Sessions, Network Mocking, Passkeys

  1. Add a setup project that logs in to Sauce Demo once and saves storageState; make all inventory tests reuse it.
  2. On demo.playwright.dev/api-mocking, mock the fruits API with your own list, then simulate an empty list and a 500 error.
  3. Block all images on Sauce Demo and measure the time difference of your suite.

πŸ”΅ Module 9 Checkpoint: API Testing

Lessons: API Testing, API with POM

  1. Create an api project for JSONPlaceholder with baseURL.
  2. Write a PostsApi class with typed list, get, create, update and delete methods.
  3. Write a full CRUD test suite (at least 8 tests) using the class.
  4. Add one negative test (/posts/0 returns 404).

🟠 Module 10 Checkpoint: Framework Design

Lessons: POM, Folder Structure, Design Patterns, SOLID, BDD, BDD Framework, Enterprise Framework

  1. Restructure your Sauce Demo suite into src/ and tests/ with page objects, a HeaderComponent and fixtures.
  2. Add path aliases and an environments.ts file selected by TEST_ENV.
  3. Add ESLint with eslint-plugin-playwright and fix all warnings.
  4. Write two features in Gherkin (login, checkout) using the BDD approach from Module 10.

🟠 Module 11 Checkpoint: Debugging and Reporting

Lessons: Debugging, Trace Viewer, Reporters, Allure

  1. Break a locator, then diagnose it three ways: error call log, --debug Inspector, and Trace Viewer. Write a short note on which was fastest.
  2. Wrap your checkout test in test.step blocks and view them in the trace and HTML report.
  3. Configure list, html, junit and Allure reporters together.
  4. Write a custom reporter that prints the five slowest tests.

🟠 Module 12 Checkpoint: Parallel Execution and CI/CD

Lessons: Parallel and Sharding, GitHub Actions, Jenkins, Docker and Kubernetes

  1. Enable fullyParallel and make the whole suite pass with --workers=4 --repeat-each=3.
  2. Push the project to GitHub with a workflow that runs smoke tests on pull requests and the full suite nightly.
  3. Shard the nightly run into 3 jobs with a merged HTML report.
  4. Run the suite locally inside the official Playwright Docker image.

🟠 Module 13 Checkpoint: AI-Powered Testing

Lessons: Playwright MCP, Test Agents, AI Tools for QA

  1. Install Playwright MCP in your AI client and use it to explore the Sauce Demo checkout flow.
  2. Run npx playwright init-agents, write a seed test, and generate a test plan for sorting and the cart.
  3. Generate tests from the plan, review them, and refactor them into your page objects.
  4. Write a one-page reflection: what the agents did well, what you had to fix, and where human review was essential.

🟣 Module 14 Checkpoint: Projects

Complete Project 1, Project 2: E-commerce Framework and Project 3: API + UI Hybrid Framework, and publish at least Projects 2 and 3 on GitHub with a README, a passing CI badge and a short “design decisions” section.


🟣 Module 15 Checkpoint: Interview Readiness

  1. Answer the 200 Playwright + TypeScript questions out loud, timing yourself; mark the ones you could not explain.
  2. Prepare a 3-minute walkthrough of your Project 2 or 3 framework.
  3. Practise the scenario questions in interview questions for experienced.

❓ FAQ

πŸ€” Where can I practise Playwright automation for free?

Use the free demo sites listed on this page (Sauce Demo, the-internet, Playwright’s API-mocking demo, JSONPlaceholder and Automation Exercise) and the SQA Tools practice pages, together with the module checkpoints.

πŸ€” Are solutions provided for these assignments?

Each linked lesson contains the code and techniques you need. Compare your work with the examples in the lessons and the two framework projects.

πŸ€” How long does it take to complete all assignments?

With 1 to 2 hours a day, most learners finish the module checkpoints and projects in about 2 to 3 months.

πŸ€” Do I need to finish every checkpoint before the projects?

Finish at least Modules 1 to 10 before Project 2, and Modules 1 to 12 before Project 3.


🎯 Next Steps

🎬 Watch the full Playwright tutorial series on YouTube!

Subscribe to SqaToolsByDY • Practice on sqatools.in

Playwright Project: API + UI Hybrid Framework with CI (TypeScript)

In this capstone project you will combine API and UI automation like a senior SDET: an API client layer, fixtures that create and clean up test users through the API, hybrid tests that verify in the browser, and a CI pipeline with separate API and UI jobs.

πŸ“– What You’ll Learn: the practice API and its quirks, project structure and config with API and UI projects, a test data factory, a typed API client, setup and teardown fixtures, API tests, hybrid API-to-UI tests, a CI pipeline with dependent jobs, an acceptance checklist and stretch goals.


🎯 Project Brief

Real-world frameworks rarely test only the UI. Senior automation engineers use the API for fast, reliable setup and cleanup, the UI for what users actually see, and run everything in CI. In this capstone you will build exactly that.

Application under test: https://automationexercise.com, a free practice e-commerce site that offers both a web UI and a documented REST API (see its “API Testing” page).

Time needed: 10 to 15 hours. Complete Project 2 first.


πŸ”Œ The API You Will Use

EndpointMethodParameters (form data)Expected
/api/productsListGETnoneresponseCode 200 and a products array
/api/createAccountPOSTname, email, password, title, birth_date, birth_month, birth_year, firstname, lastname, company, address1, address2, country, zipcode, state, city, mobile_numberresponseCode 201, “User created!”
/api/verifyLoginPOSTemail, password200 “User exists!” or 404 “User not found!”
/api/getUserDetailByEmailGETemail (query)responseCode 200 with user details
/api/deleteAccountDELETEemail, passwordresponseCode 200, “Account deleted!”

⚠️ Common mistake: This API returns HTTP 200 for almost every call and puts the real result in a responseCode field in the JSON body. Always assert both the HTTP status and responseCode. This is a common pattern in real legacy APIs, and a classic interview question.


πŸ“‹ Requirements

  • An API client layer with typed methods for each endpoint (API testing, API with POM).
  • A newUser fixture that creates a unique account through the API before a test and deletes it afterwards.
  • Hybrid tests: create a user via API, then log in and verify through the UI.
  • Cross-checks: data shown in the UI matches the API.
  • Pure API tests tagged @api and UI tests tagged @ui, in separate projects.
  • CI pipeline with separate API and UI jobs and the HTML report as an artifact (GitHub Actions).

πŸ“ Step 1: Structure

hybrid-framework/
β”œβ”€β”€ src/
β”‚   β”œβ”€β”€ api/
β”‚   β”‚   └── AccountApi.ts
β”‚   β”œβ”€β”€ pages/
β”‚   β”‚   β”œβ”€β”€ LoginPage.ts
β”‚   β”‚   └── components/Header.ts
β”‚   β”œβ”€β”€ fixtures/index.ts
β”‚   └── data/userFactory.ts
β”œβ”€β”€ tests/
β”‚   β”œβ”€β”€ api/products.api.spec.ts
β”‚   β”œβ”€β”€ api/account.api.spec.ts
β”‚   └── ui/login.hybrid.spec.ts
β”œβ”€β”€ .github/workflows/ci.yml
└── playwright.config.ts

βš™οΈ Step 2: Config with API and UI Projects

import { defineConfig, devices } from '@playwright/test';

export default defineConfig({
  testDir: './tests',
  fullyParallel: true,
  retries: process.env.CI ? 2 : 0,
  reporter: [['list'], ['html', { open: 'never' }]],
  use: {
    baseURL: 'https://automationexercise.com',
    trace: 'on-first-retry',
    screenshot: 'only-on-failure',
  },
  projects: [
    { name: 'api', testMatch: /.*\.api\.spec\.ts/ },
    { name: 'ui',  testMatch: /.*\.hybrid\.spec\.ts/, use: { ...devices['Desktop Chrome'] } },
  ],
});

🏭 Step 3: Unique Test Data with a Factory

Every test needs its own user so tests can run in parallel without clashing (parallel execution):

// src/data/userFactory.ts
export interface NewUser {
  name: string; email: string; password: string;
  title: string; birth_date: string; birth_month: string; birth_year: string;
  firstname: string; lastname: string; company: string;
  address1: string; address2: string; country: string;
  zipcode: string; state: string; city: string; mobile_number: string;
}

export function buildUser(overrides: Partial<NewUser> = {}): NewUser {
  const id = Date.now() + '_' + Math.floor(Math.random() * 10000);
  return {
    name: 'QA User ' + id,
    email: 'qa_' + id + '@example.com',
    password: 'Test@' + id,
    title: 'Mr', birth_date: '10', birth_month: '5', birth_year: '1995',
    firstname: 'QA', lastname: 'Tester', company: 'SQA Tools',
    address1: '1 Test Street', address2: 'Floor 2', country: 'India',
    zipcode: '411001', state: 'Maharashtra', city: 'Pune', mobile_number: '9999999999',
    ...overrides,
  };
}

This is the Builder/Factory idea from design patterns, in its simplest form.


πŸ”Œ Step 4: The API Client

// src/api/AccountApi.ts
import { APIRequestContext, APIResponse, expect } from '@playwright/test';
import { NewUser } from '../data/userFactory';

interface ApiResult { responseCode: number; message?: string; [key: string]: unknown }

export class AccountApi {
  constructor(private readonly request: APIRequestContext) {}

  private async parse(response: APIResponse): Promise<ApiResult> {
    expect(response.status(), 'HTTP status').toBe(200);
    return response.json();
  }

  async productsList(): Promise<ApiResult> {
    return this.parse(await this.request.get('/api/productsList'));
  }

  async createAccount(user: NewUser): Promise<ApiResult> {
    return this.parse(await this.request.post('/api/createAccount', { form: { ...user } }));
  }

  async verifyLogin(email: string, password: string): Promise<ApiResult> {
    return this.parse(await this.request.post('/api/verifyLogin', { form: { email, password } }));
  }

  async getUserByEmail(email: string): Promise<ApiResult> {
    return this.parse(await this.request.get('/api/getUserDetailByEmail', { params: { email } }));
  }

  async deleteAccount(email: string, password: string): Promise<ApiResult> {
    return this.parse(await this.request.delete('/api/deleteAccount', { form: { email, password } }));
  }
}

🧩 Step 5: Fixtures: Setup and Cleanup in One Place

The newUser fixture creates the account before the test and deletes it after, even if the test fails (custom fixtures):

// src/fixtures/index.ts
import { test as base, expect } from '@playwright/test';
import { AccountApi } from '../api/AccountApi';
import { buildUser, NewUser } from '../data/userFactory';
import { LoginPage } from '../pages/LoginPage';

type Fixtures = { accountApi: AccountApi; newUser: NewUser; loginPage: LoginPage };

export const test = base.extend<Fixtures>({
  accountApi: async ({ request }, use) => {
    await use(new AccountApi(request));
  },

  newUser: async ({ accountApi }, use) => {
    const user = buildUser();
    const created = await accountApi.createAccount(user);
    expect(created.responseCode, 'create account').toBe(201);

    await use(user);                                  // the test runs here

    const deleted = await accountApi.deleteAccount(user.email, user.password);
    expect(deleted.responseCode, 'delete account').toBe(200);
  },

  loginPage: async ({ page }, use) => {
    await use(new LoginPage(page));
  },
});
export { expect };
// src/pages/LoginPage.ts
import { Page, Locator } from '@playwright/test';

export class LoginPage {
  readonly email: Locator;
  readonly password: Locator;
  readonly submit: Locator;

  constructor(private readonly page: Page) {
    this.email = page.locator('[data-qa="login-email"]');
    this.password = page.locator('[data-qa="login-password"]');
    this.submit = page.locator('[data-qa="login-button"]');
  }

  async open() { await this.page.goto('/login'); }

  async login(email: string, password: string) {
    await this.email.fill(email);
    await this.password.fill(password);
    await this.submit.click();
  }
}

πŸ§ͺ Step 6: API Tests

// tests/api/products.api.spec.ts
import { test, expect } from '../../src/fixtures';

test('products list returns products with required fields', { tag: '@api' }, async ({ accountApi }) => {
  const result = await accountApi.productsList();
  expect(result.responseCode).toBe(200);

  const products = result.products as { id: number; name: string; price: string; brand: string }[];
  expect(products.length).toBeGreaterThan(0);
  for (const p of products) {
    expect(p).toMatchObject({ id: expect.any(Number), name: expect.any(String), price: expect.any(String) });
  }
});
// tests/api/account.api.spec.ts
import { test, expect } from '../../src/fixtures';

test('a new user can be verified by login API', { tag: '@api' }, async ({ accountApi, newUser }) => {
  const result = await accountApi.verifyLogin(newUser.email, newUser.password);
  expect(result.responseCode).toBe(200);
  expect(result.message).toBe('User exists!');
});

test('unknown user is not found', { tag: '@api' }, async ({ accountApi }) => {
  const result = await accountApi.verifyLogin('nobody_' + Date.now() + '@example.com', 'wrong');
  expect(result.responseCode).toBe(404);
});

πŸ”— Step 7: Hybrid Tests (API Setup, UI Verification)

// tests/ui/login.hybrid.spec.ts
import { test, expect } from '../../src/fixtures';

test('user created via API can log in through the UI', { tag: '@ui' }, async ({ page, newUser, loginPage }) => {
  await loginPage.open();
  await loginPage.login(newUser.email, newUser.password);
  await expect(page.getByText('Logged in as ' + newUser.name)).toBeVisible();
});

test('wrong password shows an error', { tag: '@ui' }, async ({ page, newUser, loginPage }) => {
  await loginPage.open();
  await loginPage.login(newUser.email, 'wrong-password');
  await expect(page.getByText('Your email or password is incorrect!')).toBeVisible();
});

Creating the user through the UI would take a long sign-up form and several pages per test. Through the API it takes one request, which makes UI tests faster and far less flaky.

πŸ“ Note: Public practice sites sometimes show cookie-consent or advertising overlays. If one covers the form in your region, close it in a beforeEach or block third-party ad requests with page.route (network mocking).


πŸš€ Step 8: CI with Separate API and UI Jobs

name: Hybrid Framework CI
on: [push, pull_request]

jobs:
  api-tests:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4
      - uses: actions/setup-node@v4
        with: { node-version: lts/* }
      - run: npm ci
      - run: npx playwright test --project=api

  ui-tests:
    needs: api-tests                 # run UI only if the API is healthy
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4
      - uses: actions/setup-node@v4
        with: { node-version: lts/* }
      - run: npm ci
      - run: npx playwright install --with-deps chromium
      - run: npx playwright test --project=ui
      - uses: actions/upload-artifact@v4
        if: ${{ !cancelled() }}
        with:
          name: playwright-report
          path: playwright-report/

API tests need no browser, so that job is very fast. If the API is down, the UI job does not even start.


βœ… Acceptance Checklist

  • [ ] AccountApi covers all five endpoints with typed results
  • [ ] Every test that needs a user gets a unique one from the newUser fixture
  • [ ] Accounts are deleted after each test, even on failure
  • [ ] At least 6 API tests and 4 hybrid UI tests
  • [ ] Both status() and responseCode are asserted
  • [ ] Suite passes with --repeat-each=3 --workers=4
  • [ ] CI runs API and UI jobs; the HTML report is uploaded

πŸ… Evaluation Rubric

CriterionWeight
API layer design and assertions25%
Fixture design (setup and teardown, isolation)25%
Hybrid test value (faster, more reliable than UI-only)20%
Reliability in parallel and CI15%
Code quality and documentation (README)15%

πŸš€ Stretch Goals

  • Cross-check: search a product in the UI and assert its price matches productsList.
  • Add getUserByEmail assertions after an account update with PUT /api/updateAccount.
  • Validate the products response against a JSON schema.
  • Add a nightly scheduled run and a Slack or Teams notification using a custom reporter.
  • Run the UI job inside the official Playwright Docker image (Docker).

❓ FAQ

πŸ€” What is a hybrid API and UI test?

A test that uses API calls for setup or verification and the browser for the user-facing part, for example creating a user through the API and then logging in through the UI.

πŸ€” Why use the API for test setup in Playwright?

API calls are much faster and more stable than clicking through forms, so tests spend their time verifying behaviour instead of preparing data.

πŸ€” How do I clean up test data in Playwright?

Put creation and deletion in one fixture: create before use() and delete after it. The cleanup runs even when the test fails.

πŸ€” Why assert both HTTP status and responseCode?

Some APIs always return HTTP 200 and report errors inside the body. Checking only the HTTP status would let real failures pass.


🎯 Next Steps

🎬 Watch the full Playwright tutorial series on YouTube!

Subscribe to SqaToolsByDY • Practice on sqatools.in

Playwright Project: Build an E-commerce E2E Framework with TypeScript

Time to build something real. In this project you will create a complete Playwright + TypeScript framework for an online store, from requirements and folder structure to page objects, fixtures, data-driven tests, cross-browser runs and a CI pipeline.

πŸ“– What You’ll Learn: how to turn requirements into a test scope, project configuration with setup and browser projects, logging in once with storageState, page objects for inventory, cart and checkout, fixtures and typed test data, smoke and regression tests, CI, an acceptance checklist and an evaluation rubric.


🎯 Project Brief

You are the automation engineer for Swag Labs (https://www.saucedemo.com), an online store. Build a production-style Playwright + TypeScript framework that covers the store’s critical journeys and runs automatically in CI.

This project brings together everything from Modules 1 to 12. Work through it step by step; each step links back to the lesson that teaches the skill.

Time needed: 8 to 12 hours, spread over a week.


πŸ“‹ Requirements

Functional coverage

AreaScenarios
LoginValid login, locked-out user, wrong password, empty fields (data-driven)
InventoryProduct count, sorting A-Z, Z-A, price low-high, price high-low
CartAdd one, add several, remove, badge count, cart page contents
CheckoutRequired-field validation, price summary (item total, tax, total), order completion
SessionLogout, protected pages redirect to login when logged out

Technical requirements


πŸ“ Step 1: Create the Project and Folders

mkdir swaglabs-playwright && cd swaglabs-playwright
npm init playwright@latest
swaglabs-playwright/
β”œβ”€β”€ .github/workflows/playwright.yml
β”œβ”€β”€ src/
β”‚   β”œβ”€β”€ pages/
β”‚   β”‚   β”œβ”€β”€ BasePage.ts
β”‚   β”‚   β”œβ”€β”€ LoginPage.ts
β”‚   β”‚   β”œβ”€β”€ InventoryPage.ts
β”‚   β”‚   β”œβ”€β”€ CartPage.ts
β”‚   β”‚   └── CheckoutPage.ts
β”‚   β”œβ”€β”€ fixtures/index.ts
β”‚   └── data/
β”‚       β”œβ”€β”€ users.ts
β”‚       └── checkout.ts
β”œβ”€β”€ tests/
β”‚   β”œβ”€β”€ auth.setup.ts
β”‚   β”œβ”€β”€ login.spec.ts
β”‚   β”œβ”€β”€ inventory.spec.ts
β”‚   β”œβ”€β”€ cart.spec.ts
β”‚   └── checkout.spec.ts
β”œβ”€β”€ playwright/.auth/          # add to .gitignore
└── playwright.config.ts

The layout follows framework folder structure and best practices.


βš™οΈ Step 2: Configuration

// playwright.config.ts
import { defineConfig, devices } from '@playwright/test';

const authFile = 'playwright/.auth/standard.json';

export default defineConfig({
  testDir: './tests',
  fullyParallel: true,
  forbidOnly: !!process.env.CI,
  retries: process.env.CI ? 2 : 0,
  workers: process.env.CI ? 2 : undefined,
  reporter: process.env.CI ? [['dot'], ['github'], ['html', { open: 'never' }]] : [['list'], ['html', { open: 'on-failure' }]],
  use: {
    baseURL: 'https://www.saucedemo.com',
    testIdAttribute: 'data-test',
    trace: 'on-first-retry',
    screenshot: 'only-on-failure',
  },
  projects: [
    { name: 'setup', testMatch: /.*\.setup\.ts/ },
    { name: 'chromium', use: { ...devices['Desktop Chrome'], storageState: authFile }, dependencies: ['setup'] },
    { name: 'firefox',  use: { ...devices['Desktop Firefox'], storageState: authFile }, dependencies: ['setup'] },
    { name: 'mobile',   use: { ...devices['Pixel 5'], storageState: authFile }, dependencies: ['setup'], grep: /@smoke/ },
  ],
});

πŸ“ Note: Login tests must start logged out. In login.spec.ts add test.use({ storageState: { cookies: [], origins: [] } }) to ignore the saved session.


πŸ” Step 3: Log In Once (Setup Project)

// tests/auth.setup.ts
import { test as setup, expect } from '@playwright/test';

const authFile = 'playwright/.auth/standard.json';

setup('authenticate as standard user', async ({ page }) => {
  await page.goto('/');
  await page.getByPlaceholder('Username').fill('standard_user');
  await page.getByPlaceholder('Password').fill(process.env.SAUCE_PASSWORD ?? 'secret_sauce');
  await page.getByRole('button', { name: 'Login' }).click();
  await expect(page).toHaveURL(/inventory/);
  await page.context().storageState({ path: authFile });
});

🧱 Step 4: Page Objects

// src/pages/BasePage.ts
import { Page, Locator } from '@playwright/test';

export abstract class BasePage {
  readonly cartBadge: Locator;
  readonly cartLink: Locator;

  constructor(protected readonly page: Page) {
    this.cartBadge = page.locator('.shopping_cart_badge');
    this.cartLink = page.locator('.shopping_cart_link');
  }

  protected abstract readonly path: string;

  async open(): Promise<void> {
    await this.page.goto(this.path);
  }
}
// src/pages/InventoryPage.ts
import { Page, Locator } from '@playwright/test';
import { BasePage } from './BasePage';

export type SortOption = 'az' | 'za' | 'lohi' | 'hilo';

export class InventoryPage extends BasePage {
  protected readonly path = '/inventory.html';
  readonly title: Locator;
  readonly items: Locator;
  readonly sortSelect: Locator;

  constructor(page: Page) {
    super(page);
    this.title = page.locator('.title');
    this.items = page.locator('.inventory_item');
    this.sortSelect = page.getByTestId('product-sort-container');
  }

  item(name: string): Locator {
    return this.items.filter({ hasText: name });
  }

  async addToCart(...names: string[]): Promise<void> {
    for (const name of names) {
      await this.item(name).getByRole('button', { name: 'Add to cart' }).click();
    }
  }

  async sortBy(option: SortOption): Promise<void> {
    await this.sortSelect.selectOption(option);
  }

  async names(): Promise<string[]> {
    return this.page.locator('.inventory_item_name').allTextContents();
  }

  async prices(): Promise<number[]> {
    const texts = await this.page.locator('.inventory_item_price').allTextContents();
    return texts.map(t => Number(t.replace('$', '')));
  }
}
// src/pages/CartPage.ts
import { Page, Locator } from '@playwright/test';
import { BasePage } from './BasePage';

export class CartPage extends BasePage {
  protected readonly path = '/cart.html';
  readonly items: Locator;
  readonly checkoutButton: Locator;

  constructor(page: Page) {
    super(page);
    this.items = page.locator('.cart_item');
    this.checkoutButton = page.getByTestId('checkout');
  }

  async remove(name: string): Promise<void> {
    await this.items.filter({ hasText: name }).getByRole('button', { name: 'Remove' }).click();
  }
}
// src/pages/CheckoutPage.ts
import { Page, Locator } from '@playwright/test';
import { BasePage } from './BasePage';

export interface CustomerInfo { firstName: string; lastName: string; postalCode: string }

export class CheckoutPage extends BasePage {
  protected readonly path = '/checkout-step-one.html';
  readonly error: Locator;
  readonly itemTotal: Locator;
  readonly tax: Locator;
  readonly total: Locator;
  readonly completeHeader: Locator;

  constructor(page: Page) {
    super(page);
    this.error = page.getByTestId('error');
    this.itemTotal = page.locator('.summary_subtotal_label');
    this.tax = page.locator('.summary_tax_label');
    this.total = page.locator('.summary_total_label');
    this.completeHeader = page.locator('.complete-header');
  }

  async fillInformation(info: CustomerInfo): Promise<void> {
    await this.page.getByTestId('firstName').fill(info.firstName);
    await this.page.getByTestId('lastName').fill(info.lastName);
    await this.page.getByTestId('postalCode').fill(info.postalCode);
    await this.page.getByTestId('continue').click();
  }

  async finish(): Promise<void> {
    await this.page.getByTestId('finish').click();
  }
}

Write LoginPage yourself, following the example in the framework structure lesson.


🧩 Step 5: Fixtures and Test Data

// src/fixtures/index.ts
import { test as base } from '@playwright/test';
import { LoginPage } from '../pages/LoginPage';
import { InventoryPage } from '../pages/InventoryPage';
import { CartPage } from '../pages/CartPage';
import { CheckoutPage } from '../pages/CheckoutPage';

type Pages = { loginPage: LoginPage; inventoryPage: InventoryPage; cartPage: CartPage; checkoutPage: CheckoutPage };

export const test = base.extend<Pages>({
  loginPage:     async ({ page }, use) => { await use(new LoginPage(page)); },
  inventoryPage: async ({ page }, use) => { await use(new InventoryPage(page)); },
  cartPage:      async ({ page }, use) => { await use(new CartPage(page)); },
  checkoutPage:  async ({ page }, use) => { await use(new CheckoutPage(page)); },
});
export { expect } from '@playwright/test';
// src/data/checkout.ts
import { CustomerInfo } from '../pages/CheckoutPage';

export const validCustomer: CustomerInfo = { firstName: 'Asha', lastName: 'Verma', postalCode: '411001' };

export const missingFieldCases: { name: string; info: CustomerInfo; error: string }[] = [
  { name: 'first name', info: { firstName: '', lastName: 'Verma', postalCode: '411001' }, error: 'First Name is required' },
  { name: 'last name',  info: { firstName: 'Asha', lastName: '', postalCode: '411001' }, error: 'Last Name is required' },
  { name: 'postal code', info: { firstName: 'Asha', lastName: 'Verma', postalCode: '' }, error: 'Postal Code is required' },
];

πŸ§ͺ Step 6: Tests

// tests/inventory.spec.ts
import { test, expect } from '../src/fixtures';

test.beforeEach(async ({ inventoryPage }) => {
  await inventoryPage.open();
});

test('shows six products', { tag: '@smoke' }, async ({ inventoryPage }) => {
  await expect(inventoryPage.items).toHaveCount(6);
});

test('sorts by price low to high', { tag: '@regression' }, async ({ inventoryPage }) => {
  await inventoryPage.sortBy('lohi');
  const prices = await inventoryPage.prices();
  expect(prices).toEqual([...prices].sort((a, b) => a - b));
});

test('sorts by name Z to A', { tag: '@regression' }, async ({ inventoryPage }) => {
  await inventoryPage.sortBy('za');
  const names = await inventoryPage.names();
  expect(names).toEqual([...names].sort().reverse());
});
// tests/checkout.spec.ts
import { test, expect } from '../src/fixtures';
import { validCustomer, missingFieldCases } from '../src/data/checkout';

test.beforeEach(async ({ inventoryPage, cartPage }) => {
  await inventoryPage.open();
  await inventoryPage.addToCart('Sauce Labs Backpack');
  await inventoryPage.cartLink.click();
  await cartPage.checkoutButton.click();
});

test('complete an order with correct totals', { tag: ['@smoke', '@regression'] }, async ({ checkoutPage }) => {
  await checkoutPage.fillInformation(validCustomer);
  await expect(checkoutPage.itemTotal).toHaveText('Item total: $29.99');
  await expect(checkoutPage.tax).toHaveText('Tax: $2.40');
  await expect(checkoutPage.total).toHaveText('Total: $32.39');
  await checkoutPage.finish();
  await expect(checkoutPage.completeHeader).toHaveText('Thank you for your order!');
});

for (const c of missingFieldCases) {
  test('checkout requires ' + c.name, { tag: '@regression' }, async ({ checkoutPage }) => {
    await checkoutPage.fillInformation(c.info);
    await expect(checkoutPage.error).toContainText(c.error);
  });
}

Write login.spec.ts (data-driven, logged out) and cart.spec.ts (add several, remove, badge) yourself.

πŸ’‘ Tip: Each test adds its own products after the saved login, so tests never depend on each other and can run fully in parallel (parallel execution).


πŸš€ Step 7: Continuous Integration

Use the workflow from Run Playwright tests in GitHub Actions. Add playwright/.auth/ to .gitignore, push to GitHub and confirm that the pipeline runs, uploads the HTML report, and fails if you break a test.


βœ… Acceptance Checklist

  • [ ] At least 20 tests across login, inventory, cart, checkout and session
  • [ ] No selectors or URLs inside test files
  • [ ] Login runs once per run (setup project)
  • [ ] Data-driven login and checkout validation tests
  • [ ] @smoke suite runs in under 2 minutes
  • [ ] Passes in Chromium, Firefox and the mobile project
  • [ ] Stable with --repeat-each=5
  • [ ] CI pipeline green, HTML report and traces available

πŸ… Evaluation Rubric

CriterionWeight
Coverage of the required scenarios25%
Framework design (POM, fixtures, data, structure)25%
Reliability (no flaky tests, no hard waits)20%
Readability (naming, test titles, tags)15%
CI and reporting15%

πŸš€ Stretch Goals

  • Visual tests for the inventory and cart pages (visual testing).
  • Tests for problem_user that document the known UI bugs (use test.fail()).
  • Shard the pipeline into two jobs with a merged report.
  • Ask the Playwright Test Agents to propose missing scenarios and compare with your plan.

❓ FAQ

πŸ€” What is a good Playwright project for a portfolio?

An e-commerce framework like this one: page objects, fixtures, data-driven tests, cross-browser and mobile projects, a setup project for login, and a green CI pipeline on GitHub.

πŸ€” Should I put this project on GitHub?

Yes. A public repository with a clear README, a passing Actions badge and a short description of the design decisions is strong evidence in interviews.

πŸ€” Why use storageState instead of logging in before each test?

Logging in once and reusing the session makes the suite much faster and removes repeated UI steps that add flakiness.

πŸ€” How many tests should the project have?

Quality matters more than quantity. Around 20 to 30 well-designed, independent tests covering the scenarios above is a solid result.


🎯 Next Steps

🎬 Watch the full Playwright tutorial series on YouTube!

Subscribe to SqaToolsByDY • Practice on sqatools.in