JavaScript fundamentals

JS notes

Taught in teaching order — start at the top, work down. Engine architecture → variables → operators → equality → truthy/falsy → control flow → loops → arrays → objects → functions → this → arrow / IIFE / rest → callbacks → assignments. Hand-drawn diagrams for the tricky bits, key concepts on the left, code samples in the middle, interview-style quiz on the right.

01JavaScript & V8 Engine

JS is a high-level, interpreted, dynamically-typed language. In the browser it runs on a JS engine — Chrome and Node.js use V8. V8 takes your source, builds a syntax tree, runs it in an interpreter, and JIT-compiles the hot paths to native machine code.

V8 Engine (Chrome + Node.js) JS Source .js file Parser tokenize → build AST AST Ignition interpreter → bytecode Execute slow path hot? YES TurboFan JIT compile → machine code optimised re-entry
Hot code = looped or often-called functions → TurboFan compiles to native, 10-100× faster on next call.
  • JS engines: V8 (Chrome, Node.js, Edge), SpiderMonkey (Firefox), JavaScriptCore (Safari).
  • Parser turns source → tokens → AST; Ignition reads AST and emits bytecode.
  • Bytecode runs in an interpreter loop. Functions get instrumented for hotness counting.
  • When a function gets "hot" (called many times / runs in a loop), TurboFan JIT-compiles it to native machine code.
  • If runtime types change, V8 de-optimises back to bytecode (visible via --trace-deopt).
  • Garbage collection runs in V8 too — the heap is split into Young / Old generations.
Inspect V8 internals from Node.js
node --trace-opt --trace-deopt --trace-ic yourfile.js
node --print-bytecode yourfile.js
Hot-code example
function add(a, b) {
  return a + b;
}

for (let i = 0; i < 1_000_000; i++) {
  add(2, 3);  // called millions of times → marked hot → JIT compiled
}

02var · let · const · Hoisting · typeof

Three keywords to declare. Two scopes (function vs block). One quirk called hoisting that trips up most candidates. And a famous bug: typeof null === 'object'.

Hoisting timeline · what JS sees vs what you wrote var · function-scoped var a; // hoisted = undefined console.log(a) // undefined ✓ a = 10; let · block-scoped · TDZ let b; // hoisted, NOT inited console.log(b) // ReferenceError b = 20; // exits TDZ here const · block · must init const PI = 3.14; // OK PI = 3.15; // TypeError const X; // SyntaxError
var = function scope + auto undefined. let/const = block scope + Temporal Dead Zone until the line you wrote.
  • var: function-scoped, hoisted & auto-initialised to undefined, allows re-declaration.
  • let: block-scoped, hoisted but in Temporal Dead Zone until declaration — access before throws ReferenceError.
  • const: block-scoped, must be initialised at declaration, cannot be re-assigned (the binding, not the value — object props can still mutate).
  • Primitives: Number, String, Boolean, null, undefined, BigInt, Symbol.
  • Non-primitives: Object, Array, Function — passed by reference.
  • typeof null === 'object' — a 1995 bug never fixed for backward compat. NaN is 'number'.
var vs let scope
function scopeTest() {
  if (true) {
    var v = 'I am var';
    let l = 'I am let';
  }
  console.log(v);  // 'I am var'  — function-scoped, leaks out of the if
  console.log(l);  // ReferenceError — block-scoped
}
typeof grab-bag
typeof 42;            // 'number'
typeof 'hello';       // 'string'
typeof true;          // 'boolean'
typeof undefined;     // 'undefined'
typeof null;          // 'object'   ← famous bug
typeof [1,2,3];        // 'object'
typeof function(){};   // 'function'
typeof Symbol('id');    // 'symbol'

03Operators · Template literals · Increment / Decrement

Every math operator except + coerces strings to numbers. + is the trickster — if either side is a string, it concatenates. Template literals (backticks) replace messy string concatenation.

Prefix vs Postfix · the timing difference ++a · prefix · "increment, then return" let a = 5; console.log(++a); // 6 console.log(a); // 6 step 1: a becomes 6 · step 2: 6 returned a++ · postfix · "return, then increment" let b = 5; console.log(b++); // 5 console.log(b); // 6 step 1: 5 returned · step 2: b becomes 6
  • + is dual-purpose: math or concatenation. If either side is a string, it concatenates.
  • Every other math operator (- * / % **) coerces strings to numbers — '5' - 2 === 3.
  • Unary + converts: +'42' === 42; +'abc' === NaN.
  • Template literals (backticks) support multi-line strings + ${expr} interpolation.
  • Prefix ++a: increment first, return new value. Postfix a++: return current, then increment.
The + coercion gotcha
console.log(1 + "1");     // '11'   string wins
console.log("1" + 1);     // '11'
console.log(1 + 1);       // 2
console.log(5 - "2");     // 3      - coerces string
console.log(1 + '1' + 1); // '111'  left-to-right
Template literal vs concat
let name = 'Aarav';
let age  = 30;

// old way — fragile, lots of quotes
console.log('hello ' + name + ' you are ' + age);

// template literal — readable, multi-line OK
console.log(`hello ${name} you are ${age}`);
console.log(`sum = ${100 + 200}`);  // expressions work

04== vs === · Loose vs Strict equality

Use === always — it compares value and type. == coerces and creates the weird table that interviewers love to ask about.

  • == coerces types before comparing. === never coerces — different types means false.
  • Famous coercions: true == 1, false == 0, '' == 0, [] == 0 — all true.
  • null == undefinedtrue (special rule). null === undefinedfalse.
  • Use value == null as a shorthand to check for both null and undefined in one go.
  • NaN !== NaN — use Number.isNaN(x) or Object.is(x, NaN) to check.
== coercion grab-bag
console.log(10     == "10");     // true
console.log(true   == 1);        // true
console.log(false  == 0);        // true
console.log(""     == 0);        // true
console.log([]     == 0);        // true (!)
console.log(null   == undefined); // true
=== never lies
console.log(10     === "10");     // false — different types
console.log(true   === 1);        // false
console.log(null   === undefined); // false
console.log(Number.isNaN(NaN));    // true   correct NaN check

05Truthy & Falsy values

Only 8 values are falsy in JavaScript. Everything else (including "0", " ", [], {}) is truthy.

  • The 8 falsy: false, 0, -0, 0n (BigInt zero), "", null, undefined, NaN.
  • "0" (string) and " " (space) are truthy — they have characters.
  • [] and {} are objects → always truthy. To check emptiness use arr.length or Object.keys(obj).length.
  • Boolean coercion: Boolean(value) or shortcut !!value.
Quick truthiness check
Boolean("");          // false
Boolean("0");         // true   (non-empty string)
Boolean(" ");         // true   (space char)
Boolean([]);          // true   (object)
Boolean({});          // true   (object)
Boolean(0);           // false
Boolean(NaN);         // false
Common SDET pattern
const items = await page.$$('li.item');

// wrong — items is always an Array (truthy)
if (items) { console.log('has items'); }

// right — check length
if (items.length) { console.log('has items'); }

06Conditionals · if / else if / switch

Use if for one branch, if / else if / else for mutually exclusive paths, switch when you're matching one value against many constants.

  • if-else if stops at the first match — efficient. Multiple separate ifs evaluate them all (and else binds only to the last if).
  • switch is best for fixed equality checks (status codes, browser names) — clearer than a long if-else if chain.
  • Forget break in a switch case and execution falls through into the next case.
  • Ternary cond ? a : b — great for inline assignments, avoid for multi-statement logic.
  • Dead code (if (false) {...}) — keep it out of production; some linters flag it.
Wrong: multiple ifs (all checked + else binding bug)
let browser = 'chrome';
if (browser === 'chrome')  { console.log('launch chrome'); }
if (browser === 'firefox') { console.log('launch firefox'); }  // still evaluated!
if (browser === 'edge')    { console.log('launch edge'); }
else                       { console.log('wrong browser'); }  // binds only to edge
Right: if / else if chain — stops at match
if (browser === 'chrome')       console.log('launch chrome');
else if (browser === 'firefox')  console.log('launch firefox');
else if (browser === 'edge')     console.log('launch edge');
else                              console.log('wrong browser');
Switch — equality on one value
switch (browser) {
  case 'chrome':  console.log('launch chrome');  break;
  case 'firefox': console.log('launch firefox'); break;
  default:        console.log('wrong browser');
}

07Loops · while · for · do-while

Three loop forms. while for unknown counts (polling). for for known counts (clean control). do-while when the body must run at least once.

while · for · do-while at a glance while · check first cond? body no → exit for · init·cond·step init cond? body · step do-while · body first body (always once) cond?
do-while is the only loop where the body is guaranteed to run at least once.
while · unknown iteration count
let i = 1;
while (i <= 10) {
  console.log(i);
  i++;
}
for · known count, compact
for (let m = 1; m <= 10; m++) {
  console.log(m);
}

// even numbers
for (let n = 1; n <= 10; n++) {
  if (n % 2 === 0) console.log(n);
}
do-while · run body, then check
let p = 1;
do {
  console.log(p);
  p++;
} while (p <= 10);

// SDET wait-pattern
let attempts = 0;
while (!(await button.isEnabled()) && attempts < 10) {
  await page.waitForTimeout(500);
  attempts++;
}

082D arrays · reverse · object references

Arrays of arrays = matrices. reverse() mutates in place. Two variables holding the same object share one memory entry — change via one, both see it.

Stack vs Heap · where your data lives STACK · primitives + call frames let n = 42 // number let s = 'hi' // string let b = true // boolean let u = { ... } // ref → heap HEAP · objects + arrays + functions u → { name: 'Tom', age: 30, city: 'LA' } ref arr → [ 10, 20, 30, 40 ]
Primitives live on the stack (copied by value). Objects/arrays live on the heap; variables hold a reference. Re-assigning a primitive copies; re-assigning an object shares the heap entry.
  • 2D array = array of arrays. Access m[row][col]. Walk with nested for.
  • arr.reverse() mutates and returns the same array. Use [...arr].reverse() to keep the original.
  • [1,2] + [3,4]'1,23,4' — both coerced to strings, then concatenated. Use arr1.concat(arr2) or [...arr1, ...arr2].
  • Object assignment copies the reference: let b = a; b.x = 5 changes a.x too.
  • Shallow vs deep copy: {...a} copies top-level only — nested objects still shared. Use structuredClone(a) for deep copy.
Matrix walk
let matrix = [
  [1, 2, 3],
  [4, 5, 6],
  [7, 8, 9],
];

for (let i = 0; i < matrix.length; i++) {
  for (let j = 0; j < matrix[i].length; j++) {
    console.log(matrix[i][j]);
  }
}
Reference sharing
let u1 = { name: 'ravi', age: 30 };
let u2 = u1;                // same heap entry
u1.name = 'ravi-updated';
console.log(u2.name);         // 'ravi-updated'

// to make a true copy
let u3 = { ...u1 };          // shallow — nested still shared
let u4 = structuredClone(u1); // deep

09Object iteration · JSON · null handling

for...in over keys, for...of over array values, Object.entries(obj) when you want both. JSON.stringify / JSON.parse are the round-trip pair for API payloads. ?. protects you from crashing on null.

  • for (let k in obj) — iterates string keys (including inherited). Filter with obj.hasOwnProperty(k) if needed.
  • for (let v of arr) — iterates values. Works on arrays, strings, Maps, Sets.
  • Object.keys / .values / .entries return arrays — easy to combine with .forEach / .map / .filter.
  • JSON.stringify(obj, null, 2) serialises with 2-space indent. JSON.parse(s) creates a fresh copy (no shared refs).
  • Optional chaining obj?.a?.b returns undefined instead of crashing if any link is null/undefined.
  • Nullish coalescing a ?? b returns b only if a is null or undefined (unlike || which also fires on 0 / '').
Iterate an object three ways
const user = { name: 'Tom', age: 30, city: 'LA' };

// 1) for...in over keys
for (let k in user) {
  console.log(k, ':', user[k]);
}

// 2) Object.keys → array
Object.keys(user).forEach(k => console.log(k, user[k]));

// 3) entries → [key, value] pairs
for (let [k, v] of Object.entries(user)) {
  console.log(`${k} = ${v}`);
}
JSON round-trip · API pattern
// outbound: object → JSON string
const body = JSON.stringify({ email: '[email protected]', role: 'sdet' });
await page.request.post('/api/users', { data: body });

// inbound: JSON string → object
const res  = await page.request.get('/api/users/1');
const user = JSON.parse(await res.text());

// optional chaining + nullish coalescing
const city = user?.address?.city ?? 'unknown';

10Functions · Heap / Stack · Call stack

Functions are first-class. Call them and a frame goes on the stack. Return and the frame pops. Recurse without a base case → "Maximum call stack size exceeded".

  • Four flavours: (1) no input/no return, (2) no input/return, (3) input/return, (4) multi-branch return.
  • Parameter = name in signature. Argument = value at call site. JS does not enforce parameter types.
  • Missing arg → undefined. Missing return → returns undefined.
  • Function.name / Function.length (= parameter count) are introspection helpers.
  • Each call pushes a frame onto the call stack (LIFO). Recurse without a base case → stack overflow.
  • Function objects themselves live on the heap; the variable holding the function reference lives on the stack.
Four function flavours
function test() {                  // (1) no input, no return
  console.log('test');
}

function getTrainerName() {       // (2) no input, return
  return 'TTA Instructor';
}

function add(a, b) {                // (3) input + return
  return a + b;
}

function getStudentMarks(name) {    // (4) switch with multi-return
  switch (name.trim().toLowerCase()) {
    case 'ravi':  return 90;
    case 'pooja': return 80;
    default:       return -1;
  }
}
Type-coercion gotcha (no error)
function add(a, b) { return a + b; }

console.log(add(10, 'abc'));   // '10abc'  (string concat, not math)
console.log(add(10));            // NaN  (b is undefined → 10 + undefined)

// guard manually if math is required
function safeAdd(a, b) {
  if (typeof a !== 'number' || typeof b !== 'number') {
    throw new Error('numbers only');
  }
  return a + b;
}

11Duplicate functions · Default params · Function expressions · this

JS has no function overloading — last declaration wins. Default params fill in missing args. Function expressions are not hoisted. this inside an object method refers to the object itself.

  • Two functions same name → the second overwrites the first silently.
  • Default params: function f(a, b = 10) { … }. Passing undefined uses the default; passing null does NOT.
  • Function declarations are hoisted; function expressions (assigned to variables) are not.
  • Inside an object method, this = the object. From a free call, this = undefined (strict) or the global object (sloppy).
  • Avoid losing this when passing methods around — bind it with fn.bind(obj) or wrap in an arrow.
Duplicate functions — last wins
function search(p, price)        { console.log('v1', p, price); }
function search(p, price, status){ console.log('v2', p, price, status); }

search('imac', 1000);    // 'v2' imac 1000 undefined
Default params
function login(user, pass, role = 'admin', status = 'active') {
  console.log(user, pass, role, status);
}

login('[email protected]', 'pw');
// → [email protected] pw admin active

login('[email protected]', 'pw', null);
// → [email protected] pw null active   (null overrides, undefined uses default)
Function expression vs declaration
// declaration — hoisted
decl();                       // works
function decl() { console.log('decl'); }

// expression — not hoisted
expr();                       // ReferenceError
let expr = function() { console.log('expr'); };
this inside an object method
let loginPage = {
  username: '[email protected]',
  login() {
    console.log('login via ' + this.username);
    this.resetPwd();
  },
  resetPwd() { console.log('pwd reset'); }
};

loginPage.login();
// → login via [email protected]
// → pwd reset

12IIFE · Arrow functions · Rest params · Optional chaining

Modern function tools: IIFE for run-once init, arrow for concise callbacks (and lexical this), rest for variadic args, ?. for safe property reads.

  • Anonymous function: no name; usually assigned to a variable or passed as a callback.
  • Arrow function: (args) => expr. Single param can drop parens. Single expression → implicit return.
  • Arrow functions do NOT have their own this / arguments — they inherit from the surrounding scope.
  • Rest parameter ...args collects remaining args into an array. Must be last.
  • IIFE = "Immediately Invoked Function Expression". Pattern: (function () { … })(); — runs once, isolates scope.
  • Optional chaining obj?.a?.b returns undefined on a null/undefined link.
Arrow flavours
let hello   = () => console.log('hello');          // 0 args
let double  = n  => n * 2;                          // 1 arg, implicit return
let sum     = (a, b) => a + b;                       // multi-arg
let log     = (b) => {                                // block body, explicit return
  console.log(b);
  return b;
};
Rest parameter
function fillValues(name, ...details) {
  console.log('for ' + name, details);
  for (let e of details) console.log(e);
}
fillValues('aarav', 101, 'sector 7', 'india');
// for aarav [ 101, 'sector 7', 'india' ]
IIFE — run once, leak nothing
(function (browser) {
  console.log('init ' + browser);
})('chromium');

// arrow IIFE
(() => { console.log('bootstrapped'); })();
Optional chaining
let obj = { name: 'Tom', addr: { city: 'LA' } };
console.log(obj?.addr?.city);    // 'LA'

obj = null;
console.log(obj?.name);          // undefined  (no crash)

13Callback functions · Higher-order helpers

A callback is a function passed as an argument. Array methods (forEach, map, filter, reduce) all take callbacks. This is the gateway to promises and async/await.

Callback flow · "you call me when you're ready" caller passes fn as arg (cb) higher-order fn stores · later invokes cb(result) callback runs handles the result forEach · map · filter · reduce all follow this pattern.
Basic callback
function sayHello(callback) {
  callback();
}
sayHello(function() { console.log('hello'); });
sayHello(() => console.log('hello arrow'));
Calculator pattern · higher-order fn
const add = (a, b) => a + b;
const sub = (a, b) => a - b;
const mul = (a, b) => a * b;

function calculator(a, b, op) {
  return op(a, b);
}

console.log(calculator(10, 20, add)); // 30
console.log(calculator(10, 20, (a, b) => a / b)); // 0.5
Array methods · the bread & butter
const nums = [1, 2, 3, 4, 5];

nums.forEach(n => console.log(n + 5));    // side effect only

const big = nums.filter(n => n > 3);    // [ 4, 5 ]
const doubled = nums.map(n => n * 2);   // [ 2, 4, 6, 8, 10 ]
const total = nums.reduce((acc, n) => acc + n, 0); // 15
Callback + rest = variadic helper
function calc(cb, ...args) {
  console.log(cb(...args));
}

const addAll = (...a) => a.reduce((s, v) => s + v, 0);
calc(addAll, 1, 2, 3, 4);    // 10

14Practice assignments

Self-test questions taken from the course's Chapter-1-to-7 and Session-8-to-13 sets. Treat each like a flash-card: write the answer in your editor, then run it.

Chapters 1-7 · 70 questions

  1. What is JavaScript? Compiled or interpreted?
  2. Name three places JavaScript can run.
  3. What is Node.js and why use it?
  4. What is V8? Which browsers use it?
  5. JS engines in Chrome, Firefox, Safari?
  6. Browser engine vs JS engine — difference?
  7. Command to check Node.js installation.
  8. What is VS Code? Two useful extensions for JS.
  9. Print "Hello JavaScript!" via console.log + Node.js.
  10. Can JS run without a browser? How?
  11. Three ways to declare variables.
  12. Difference between let and const.
  13. Can you reassign a const? What error?
  14. What is hoisting? Show with var.
  15. Using let before declaration — what error?
  16. List all primitive types.
  17. null vs undefined difference.
  18. typeof for: 42, 'hello', true, null, undefined.
  19. Number type range? Beyond range behaviour?
  20. Declare var, let, const; print all three.
  21. String concatenation example with +.
  22. What are template literals?
  23. Rewrite with template literal: 'My name is ' + name + ' and I am ' + age.
  24. Single vs double vs backticks difference.
  25. List basic math operators.
  26. Result of 10 % 3? Modulus explanation.
  27. Result of 2 ** 4?
  28. What does += do? Example.
  29. 'Hello' + 5 = ? Result type?
  30. Program: add two numbers, print with template literal.
  31. == vs === difference.
  32. 5 == '5' vs 5 === '5' — why?
  33. What is NaN? Example.
  34. Check if value is NaN — use ===?
  35. What is Infinity? Operation that produces it.
  36. Infinity + 100 = ?
  37. i++ vs ++i difference. Example.
  38. let x = 5; let y = x++; — values of x and y?
  39. What is type coercion? Example.
  40. null == undefined vs null === undefined.
  41. if-else syntax.
  42. Purpose of else if. When use?
  43. Program: positive / negative / zero check with if / else if / else.
  44. What is the ternary operator? Example.
  45. switch-case syntax. Forget break — what happens?
  46. Day-name program (1=Monday, 2=Tuesday, …).
  47. default case purpose. Required?
  48. Set breakpoint in VS Code.
  49. if-else vs switch-case — preference?
  50. Ternary: check age >= 18, print 'Eligible' or 'Not Eligible'.
  51. for-loop syntax. Three parts in parentheses?
  52. for loop: print 1 to 10.
  53. while vs do-while difference.
  54. while loop: print 10 down to 1.
  55. break and continue keywords — what do they do?
  56. for loop: even numbers 1 to 20.
  57. Falsy values — list all.
  58. Truthy values — three examples.
  59. for loop: sum 1 to 100.
  60. while(true) without exit — danger? Avoid?
  61. Create a 5-element array example.
  62. Access third element. Index?
  63. push() method. Example.
  64. pop() method. Return value?
  65. shift() vs unshift() difference.
  66. splice() method. Remove 2 elements from index 1.
  67. Find array element index. Not-found return?
  68. length property usage.
  69. Create 5-fruit array; add one to end; remove one from start; print result.
  70. splice() vs slice() difference.

Sessions 8-13 · 72 questions

  1. What is a 2D array? 3×2 example.
  2. Access row 1, column 0 in a matrix.
  3. Print all 2D array elements (nested for loops).
  4. reverse() method — changes original?
  5. Reverse [10, 20, 30, 40, 50]; print.
  6. JS object definition — different from array?
  7. Create a 'user' object: name, age, city, isActive.
  8. Dot notation vs bracket notation — both examples.
  9. Add a new property. Delete a property.
  10. Nested object example. user.address with flat, zip, city.
  11. user.address.flat return value.
  12. Assign one object to another — same reference or copy?
  13. Get all object keys — which method?
  14. for...in loop: print all keys and values.
  15. for...in vs for...of difference.
  16. JSON.stringify() does what? Return type?
  17. JSON.parse() does what? Return type?
  18. Convert { name: 'Tom', age: 30 } to JSON string.
  19. JSON string back to object; access name property.
  20. Serialization vs deserialization.
  21. Set object to null; access property — what happens?
  22. Optional chaining operator ?. — how it helps null.
  23. Nested object: iterate address keys inside for...in.
  24. After stringify + parse — same reference as original?
  25. What is a function? Why use functions?
  26. Simple greet() function that prints 'Hello World'.
  27. Parameters vs arguments difference.
  28. Function: add two numbers, return result.
  29. No return statement — what returns?
  30. typeof on function name returns?
  31. Call by value definition. Simple example.
  32. Call stack definition. Function called — what happens?
  33. Heap memory definition. What data stored?
  34. getStudentMarks(name): if-else return marks or -1.
  35. functionName.length returns?
  36. functionName.name returns?
  37. Two functions with the same name — what happens?
  38. Does JS support function overloading? Explain.
  39. Default parameters in a function. Example.
  40. Pass null to a default param — what happens?
  41. Function expression definition. Different from declaration?
  42. Function expression: assign to a 'cart' variable.
  43. Call function expression before its definition — why fails?
  44. Add functions (methods) inside a JS object.
  45. loginPage object: username, password, login() method.
  46. this keyword inside an object method refers to?
  47. Object with two methods; one calls the other via this.
  48. functionExpression.name returns — variable or function name?
  49. Rest parameter (...) in JS — symbol used?
  50. Function accepting any number of args via rest param.
  51. Rest param with regular params. Rest placement?
  52. Anonymous function definition. One example.
  53. Anonymous functions commonly used where?
  54. Arrow function definition. Add two numbers example.
  55. Arrow function vs regular — syntax difference.
  56. Arrow: no params, print 'Hello JS'.
  57. Arrow: one param, return value × 10.
  58. IIFE stands for? What does it do?
  59. IIFE that prints 'Server started'.
  60. Pass arguments to an IIFE — browser name example.
  61. Callback function definition.
  62. One function passed as argument — simple example.
  63. calculator(a, b, callback) function.
  64. calculator with add, subtract, multiply, divide.
  65. Call by value vs call by reference vs call by function.
  66. perform(cb1, cb2) function that calls both.
  67. forEach use callback — array numbers example.
  68. filter use callback — filter > 3 from [1,2,3,4,5].
  69. Anonymous arrow as callback. Example.
  70. launchBrowser(name, callback): switch-case return true/false.
  71. Calculator with rest params — add any number of values.
  72. Callback not passed — what happens?

Reference page · TTA-rebranded · every code sample written against https://app.thetestingacademy.com/ patterns. Push these snippets into your IDE, run with node fileName.js, and tweak. The diagrams above also exist as Excalidraw scenes you can open and remix.