|

JavaScript to Playwright Advanced Framework: The Complete 21-Day SDET Guide

21-day path from JavaScript to TypeScript to Playwright to framework

This is the hub for my new 21-day series: JavaScript to Playwright Framework.

I am Pramod Dutta. I teach SDETs in India for a living. Every live batch of mine starts the same way, and it is not npx playwright test. It is Node. A .js file. console.log. Then var versus let versus const. Then hoisting. Then operators that silently fail an assertion. Then if / switch. Then loops and arrays. Then functions and closures. Then objects. Then Promises. Then a class that becomes a BasePage. Then TypeScript so that class refuses a wrong selector. Then — and only then — we launch a browser.

If you skip that climb, your Playwright suite will still run. It will also leak state, throw Temporal Dead Zone errors you cannot name, await inside a forEach that does not wait, copy a login spec five times, and fail interviews that a 20-minute JavaScript lab would have saved.

This post is the SEO hub and the map. Every day of the series lives under this URL. I will recap what each day actually teaches, from the files that actually exist on GitHub, and I will link you into that day. I will not invent a repo file. I will not polish a classroom typo. I will not pretend an empty Dockerfile is a pipeline.

If you want the video plus project path after you finish these 21 posts, the course is here: Playwright Automation Mastery. This blog series is the public spine. The course is the reviews, the live projects, and the framework we actually ship.

21-day path from JavaScript to TypeScript to Playwright to framework

Contents

This is not the 21-Day Playwright TypeScript Challenge

Scrolltest already has a live series: the 21-Day Playwright with TypeScript Challenge. That calendar starts at the Test runner. Day 1 of that challenge is install, a first spec, and TypeScript from the first hour. People land on it from Google, from the course, from a WhatsApp forward. It is a good series. It is a different series.

This series has a different slug prefix: js-playwright-framework-day-NN. This hub has a different slug: javascript-typescript-playwright-advanced-framework-21-day-guide. This series starts earlier. We earn page.goto from console.log. We earn a typed Page Object from a JavaScript class. We earn fixtures from a placeholder folder that is still skipped on main. We earn Cucumber from the same LoginPage, not from a second stack. We earn CI from a YAML that still does not run cucumber-js. We earn MCP last, not first.

If you landed here from the TypeScript Challenge calendar, stay or go back — both paths are mine — but do not mix the day numbers. Day 10 here is first Playwright tests. Day 10 there is a different lab. Different files. Different promise.

I will say this again on days that share a topic with the live challenge (codegen, MCP, traces), because Google will mix the URLs. Different slug. Different labs. Different gate.

Who this 21-day SDET path is for

This path is for the tester who can already click through a product and write a bug, and who now has to own a Playwright suite in JavaScript or TypeScript.

It is for the Java SDET who has TestNG and Rest Assured in their fingers and keeps translating every Playwright idea back into Java. You do not need a second runner to test /booking. You need APIRequestContext. Day 19 is that translation, after you have already learned why request.get is a Promise.

It is for the person who recorded a login with codegen last Tuesday and now has 40 specs that all type the password. Day 12 is storageState. Day 16 is a LoginPage. Day 17 is a fixture that constructs that page. Day 18 is a Given that should call open() — and on this branch, that Given is missing await. I will not hide that.

It is for the interviewer-side SDET who wants a map they can hand a junior: here is the language, here is the type, here is the locator rule, here is who owns the page, here is who owns the request, here is the CLI the pull request actually runs.

It is not for someone who wants a 20-minute “Playwright crash course” that starts at getByRole and ends at a green HTML report. That person should take the TypeScript Challenge, or the first weekend of the course. This hub is 21 mornings. Day 1 is 09:00 IST on 30 August 2026. Day 21 is 19 September 2026. One lesson a day. Homework that is a file, not a vibe.

You need Node.js 18 or newer. A terminal. Git. The willingness to run a file that throws, read the error, and not skip it. You do not need TypeScript on Day 1. You do not need Playwright installed on Day 1. You do not need an LLM key until Day 21, and even then hasApiKey() exists so CI can skip.

The three repos this series actually uses

I will name them once, here, so every day recap can stay short.

1. LearningPlaywrightBatch on branch main.

This is the language repo. Chapters 01 through 22 are JavaScript and TypeScript labs — 01_basic.js through 208_23_logs_Decortors.ts. Classroom spellings stay: Parrallel, sequenttialTest, Coverted, RESPOSNE, Clsss, Ovveride, Decortors, FreeTrail, 195_REAL_BRowser_Selection.ts. Empty files stay empty: 12_hoisting_if_block.js is 0 bytes, 207_Decorator.ts is 0 bytes. I skip them and I say so. The same repo later holds Lecture_Playwright_CLI, Lecture_Playwright_MCP, and Lecture_Playwright_AI_Agents. Days 1–9 and Days 20–21 live here.

2. LearningPlaywrightFundamentals on branch main.

This is the browser repo. tests/01_Basics through tests/22_Misc_Concepts, plus tests/Projects and TTACartProject. Labs 209 through 271 are the numbered specs. Folder 21_Fixture is a skipped placeholder. Folder 22_Misc_Concepts is a skipped placeholder. Folder 23_Advance_Framework is also a skipped placeholder — the real layers are not in this tree. Project 5 QA Profile is a README scaffold. There is no Task1.spec.ts in that folder. I will not invent one. Days 10–16 live here.

3. AdvancePlaywrightFramework1x on branch feat-cucumber, not main.

This is the framework repo. Config, BasePage, TTACart pages, test-base fixtures, Winston, visualStep, ApiHelper, CustomTTAReporter, Cucumber levels 00–02, Restful Booker layers, GitHub Actions, and src/ai. The Dockerfile is empty (0 bytes). src/fixtures/index.ts is empty (0 bytes). test:lor is documented and missing from package.json. Credentials fallbacks are empty strings, not demo passwords. CI runs npx playwright test and does not run cucumber-js. Days 17–21 live here.

Three clones. Three jobs. Do not look for BasePage.ts in the fundamentals tree. Do not look for chapter_02 in the framework tree. Do not look for Cucumber on main of the framework — the BDD tip is feat-cucumber.

git clone https://github.com/PramodDutta/LearningPlaywrightBatch.git
cd LearningPlaywrightBatch
git checkout main

git clone https://github.com/PramodDutta/LearningPlaywrightFundamentals.git
cd LearningPlaywrightFundamentals
git checkout main

git clone https://github.com/PramodDutta/AdvancePlaywrightFramework1x.git
cd AdvancePlaywrightFramework1x
git checkout feat-cucumber

Public demo hosts appear in these files: VWO, the-internet, SpiceJet, TTA widgets, TTA Bank, TTACart at https://app.thetestingacademy.com/playwright/ttacart/, Restful Booker. If a host is down, the spec fails. That is a classroom fact, not a Playwright bug.

How to use this hub

Read this page once. Bookmark it. Then open one day each morning at 09:00 IST.

Each day post is a full lesson: the files, the quotes, the interview checks, the FAQ, the diagram brief. This hub is the map and the recap. If you only have 20 minutes, read the day recap here, then jump. If you have 90 minutes, open the day, clone the repo, run the files, paste the output into a notes doc. The homework is the file. The recap is not a substitute for the throw.

I will link every day below, twice: once in the calendar table, once in the long recap. The slugs are stable. The day posts are drafts until they publish on their morning. This hub is itself a draft, scheduled 30 August 2026 09:00 Asia/Calcutta, same morning as Day 1. It is not published from this file. status: draft. do_not_publish: true.

When a day says a file is empty, believe the day, not a slide. When a day says CI does not run Cucumber, do not tell an interviewer the BDD track is gated. When a day says folder 21 is a skip, do not write test.extend and claim it lives in LearningPlaywrightFundamentals.

Want to master this with real projects? Join the Playwright Automation Mastery course at The Testing Academy.

The 21-day calendar

One lesson a day. 09:00 Asia/Calcutta. Day 1 is 30 August 2026. Day 21 is 19 September 2026.

DayDate (IST)PhaseTitleSlug
130 AugJSJavaScript basics, var/let/const, hoistingDay 1
231 AugJSIdentifiers, literals, operatorsDay 2
31 SepJSif/else, switch, control flowDay 3
42 SepJSLoops, arrays, test dataDay 4
53 SepJSFunctions, closures, stringsDay 5
64 SepJSObjects, multi-dimensional arraysDay 6
75 SepJSCallbacks, Promises, async/awaitDay 7
86 SepJSOOP, inheritance, BasePageDay 8
97 SepTSTypes, interfaces, enums, typed POMDay 9
108 SepPWFirst tests, context, pagesDay 10
119 SepPWLocators: role, CSS, XPathDay 11
1210 SepPWstorageState, Allure, web tablesDay 12
1311 SepPWFrames, keyboard, drag-drop, alertsDay 13
1412 SepPWSVG, Shadow DOM, upload, downloadDay 14
1513 SepPWAssertions, hooks, data-driven testsDay 15
1614 SepPWPOM, fixtures, course projectsDay 16
1715 SepFWAdvanced framework layersDay 17
1816 SepFWCucumber BDDDay 18
1917 SepFWAPI: Restful Booker, JSONPath, AjvDay 19
2018 SepFWCLI, codegen, trace, CIDay 20
2119 SepFWMCP, AI agents, capstoneDay 21

Four phases. Remember them as one sentence: language, then types, then the browser, then layers.

  • Days 1–8 — JavaScript. The language an SDET actually types. No Playwright install.
  • Day 9 — TypeScript. The contract. Still no spec run.
  • Days 10–16 — Playwright Test. Browser, locators, tables, frames, expect, POM. Fundamentals repo.
  • Days 17–21 — Framework. Layers, Cucumber, API, CLI/CI, MCP and agents. feat-cucumber.

Phase 1 — JavaScript for SDETs (Days 1 to 8)

Playwright is a Node library. Your spec file is JavaScript that Node executes. test(), expect(), page.goto(), a custom fixture, a storageState path, a const config object — all of that sits on top of the same engine rules you meet in chapter_01_Basics through chapter_17.

I have interviewed enough SDETs to see the pattern. Someone can write a login test with page.goto(baseURL) and still cannot explain why console.log(a) prints undefined with var and throws with let. That gap shows up later as a var i inside a retry loop that leaks into the next test, a let token used above its declaration, a helper written as const login = async () => {} and then called above the line, or await inside forEach that does not wait.

Days 1 through 8 are the fix. We stay on LearningPlaywrightBatch. We do not install Playwright. If that sentence makes you restless, you are the person this phase is for.

Day 1 — JavaScript basics, setup, var/let/const, and hoisting

Read: Day 1: JavaScript Basics for SDETs — Setup, var/let/const, and Hoisting

Day 1 is Node, a terminal, and the two-phase engine.

You clone LearningPlaywrightBatch, stay on main, and run chapter 01: 01_basic.js, 02_JS_Step_By_Step.js, 03_verify_setup.js, 04_hot_code.js. You print process.platform, process.arch, and process.version. On an Apple Silicon Mac, process.arch should later print arm64. That is exactly what 03_verify_setup.js is for. You feel why a 10,000-call add is “hot” and still not your suite’s bottleneck.

Then chapter 02, the folder named chapter_02_Java_Concepts: comments (05_Core_Comments_JS.js, the Ctrl+/ habit in 08_Lab.js), identifier rules (06_Core_Identifier_JS.js), and the file I want every junior to fail on purpose — 07_var_let_const.js. const by default. let when the value must change. var never in new code. const does not freeze an object; it freezes the binding. That sentence returns on Day 6.

Hoisting is the rest of the morning. 09_Hoisting.js sketches two phases: memory, then execution. 10_hoisting_var.js is the greeting version. 13_hoisting.js puts noise between the logs. 11_hoisting_function.js is function-scope hoisting inside a helper. Then the Temporal Dead Zone: 14_let_hoisting.js, 15_let_block.js, 17_rogit.js. var inside if or for leaks (16_var_if_loop.js). let in the same place throws if you touch it early.

Skipped on purpose: 12_hoisting_if_block.js exists on main and is empty (0 bytes). I do not invent an if-block lab for it. The same idea is in 15, 16, and 17.

Why Playwright cares: fixture state, loop indexes, helper functions, and const config all obey hoist and scope. Locator skill does not debug a TDZ error. A const login = async () => {} called above its line is a TypeError. A function login() declaration would have been hoisted. That triad — works / TDZ / TypeError — is the entire function-hoisting interview.

Homework is not optional. Uncomment the dangerous lines. Read the error. Re-comment. Do not skip the error.

Day 2 — Identifiers, literals, and operators

Read: Day 2: JavaScript Identifiers, Literals, and Operators for Test Automation

Day 2 is the day your assertions stop lying to you.

Identifiers cannot be reserved words. Do not shadow undefined, Infinity, or Playwright fixtures like page. 19_Identifier.js asks whether you can let undefined = 5. Yes. Should you? Never.

Literals are the values you type. "", " ", and "0" are three different things. Only "" is falsy. The three string literals that break assertions are the empty string, the space, and the string that looks like a number. getAttribute returns a string or null. "false" == false is true in loose equality. Stay strict.

Null versus undefined is the SDET version of a contract. null is intentional emptiness — the API returned {"middleName": null}, localStorage.getItem missed, you set selectedRow = null. undefined is absence — declared only, missing JSON key, skipped argument. null == undefined is true. null === undefined is false. expect is strict. expect(null).toBeUndefined() fails. People write it when they used == in the console, saw true, and assumed Playwright agrees.

= assigns. == coerces. === checks type and value. There is no !===. 25_IQ.js is the interview file. "0" == false is true. NaN == NaN is false. Loose equality is not transitive: "" == 0 and "0" == 0 do not make "" == "0". expect().toBe() lives on Object.is. That is why Day 2 exists before Day 10.

Operators you actually use in a suite: % and ** in sharding and backoff, += in retry budgets, + that concatenates the moment a string appears. && / || / ! short-circuit and follow truthiness. CI=false is still a truthy string. Ternary is for values. if is for actions. typeof null is "object". typeof [] is "object". Use Array.isArray and null checks.

Nullish coalescing is the test-data operator. ?? preserves 0 and "". || does not. Number(process.env.RETRIES || 2) is a production trap: after you have parsed to a number, 0 || 2 is 2. Parse first, then default: Number(process.env.RETRIES ?? 2), and reject NaN.

Day 15 will hand you CSV cells that are "", "0", null, and missing keys. Day 17 will hand you STANDARD_USER ?? ''. You will be glad you did 25_IQ.js with a notebook, not a highlighter.

Day 3 — if/else, switch, and real test branches

Read: Day 3: JavaScript Control Flow — if/else, Switch, and Real Test Branches

A statement is a decision, not a sentence. Day 3 takes classroom labs 33 through 52 and turns them into the branches you actually write: API status, environment, role, and browser.

Lab 33 is the smallest useful branch. Lab 34 is else if as a ladder, not a pile of ifs. Lab 35 is nested if the way real products work — login, then role, then feature flag. Lab 36 is API status branching, and it steps on the Day 2 landmine: "200" == 200 is not how you want to live. Lab 37 is what is truthy inside if (). An empty array is truthy. 0 is falsy. "" is falsy. "0" is truthy. Lab 38 is AND, OR, and the locked account. Lab 39 is the missing-brace interview. Lab 40 is the empty helper — the real lesson is a function that looks like a branch and does nothing. Lab 41 packs three interview cases in one file.

Then switch. When the value is discrete, stop stacking else if. Lab 42 is switch with break, the happy path. Lab 43 is named “with Break” and has no break — fall-through is the bug. Lab 44 is default for the value you did not plan. Lab 45 is switch on a real API status. Lab 46 is grouped cases, the browser one you will copy: Chromium family versus Firefox versus WebKit. Lab 47 is fruit-salad fall-through. Lab 48 is switch (true) for ranges, used sparingly. Lab 49 is duplicate case, first one wins. Lab 50 is the Day 2 reminder: switch uses ===, so "5" is not 5. Lab 51 is 0 is not false inside switch. Lab 52 is the user-input stub, finished as env input.

The rule I want you to memorize: if / else if for ranges, predicates, and mixed conditions. switch for a discrete value you can list. switch (true) is a trick, not a house style. Playwright later: status code in API setup, environment in config, browserName for engine families, test.skip for one browser or one env. An empty array is still truthy. A missing break still runs every case after the one you wanted.

Day 4 — Loops, arrays, and test data

Read: Day 4: JavaScript Loops and Arrays for Test Data and Assertions

A test that cannot walk a list is a one-shot recording. Day 4 is labs 53 through 75.

Increment is the engine of every loop. a++ and ++a print different values because one returns the old number. Labs 54 and 55 make you feel that. for is Init, Condition, Update (56, 57, 58). while is the same three pieces written as a sentence (59, 60). do-while runs the body once, then asks (61, 62). Pick the loop on purpose. for when you know the count. while when you are waiting for a condition. do-while when the body must run once — a first poll, a first screenshot.

Then arrays. Create the list the way a tester means it (63). Access, at(-1), modify in place (64). Add and remove from the ends, then splice the middle (65). Lab 66 is a real browser list, a leftover require, and a loop that talks. Search returns an index, a boolean, or the element (67). Four ways to walk, and one I do not want on arrays: for...in (68). for...of is for values. for...in is for keys, and on an array it will surprise you with indexes as strings and inherited junk.

map, filter, reduce, flat are the assertion toolbox (69). sort will betray numbers unless you bring a comparator (70) — 11 before 2 if you sort as strings. slice does not mutate; concat and spread build; join reports (71). Array.isArray, every, some (72). Copy versus the same box with two names (73, 75) — changing a “copied” array that is actually a shared reference changes the original. That is the Day 6 object lesson arriving early. Destructure the first items, rest the tail (74).

Playwright payoff in the same morning: locator lists, table rows, data-driven assertions. locator.all() is an array of locators. Walk it with for...of and await. Do not forEach plus await. Day 7 will make that a crime. Day 12 will read Helen Bennett’s country from a table with the same shape. Day 15 will register one test per CSV row with the same loop-outside-the-test rule.

Day 5 — Functions, closures, and strings

Read: Day 5: JavaScript Functions, Closures, and Strings for SDETs

Functions stop copy-paste. Closures remember retries. Strings write the expect message CI actually reads. Day 5 is chapters 09 and 10, labs 76 through 107.

Four function types: no argument no return, argument no return, no argument with return, argument and return (77–80). Function expressions (82). Arrow functions (83) — the form you will type in test('name', async ({ page }) => {}). Three styles, one API check (84). IIFE (85) — useful as a classroom idea, rarely needed in a Playwright repo because a module is already a scope. Default parameters (87). Rest parameters (88). Lab 89 is the hoisting TypeError, and the capital .JS filename. Spread (90). Return values (91). Function hoisting (92). Scope (93).

Closures are the heart. Lab 94 returns the inner function. Lab 95 is a counter object. Lab 96 is a retry tracker. Lab 97 is a rate limiter. A module-level retry counter is a bug: the next test inherits the count. A closure per call is the fix. Higher-order functions (98) take or return a function — map, a wrapper around a test body, a message factory. Pure functions (99) do not touch the outside world. Helpers that format an error string should be pure. Helpers that click Login cannot be. Callbacks (100, 101) set up Day 7.

Strings: quotes, templates, String() (102). Length, index, at, charCode (103). Search and check (104) — indexOf returns -1, not false, when the substring is missing. Slice and substring (105). Transform, replace, split, join (106). Conversion and immutability (107). A string assertion fails when the UI “looks correct” because you compared "Dashboard " to "Dashboard", or you used toBe on a concatenated message that had a different space. expect takes a message. Use it. Closures make the message factory. That is the Playwright payoff of Day 5, before we have Playwright.

Day 6 — Objects and multi-dimensional arrays

Read: Day 6: JavaScript Objects and Multi-Dimensional Arrays in Test Automation

An object is a bag of keys (108). const does not freeze the bag (109) — you can still grow a config, overwrite a field, delete a key (110). Primitive versus reference is the file I tell people to read twice (111). Two variables can point at the same box. expect(body).toBe(expected) fails when the fields match because toBe is identity. toEqual walks the fields. Day 15 will say this again. Day 2 already hinted.

A property is more than a value (112) — descriptors, writable, enumerable. Methods, this, and a chain (113). Destructure the body, do not pick keys by hand (114). Spread is a shallow copy, and this still exists (115). Getters look like fields, setters look like assignment (116). Object.keys, values, entries, for...in (117). Three objects you will actually ship (118): a config, an API body, an expected body. let can rebind, const cannot, both can mutate (119).

Then the map to Playwright: JSON fixtures, API request bodies, expected bodies. A fixture object you mutate in test A is still mutated in test B if you shared the reference. Clone it. Or build it per test. Day 17’s DataGenerator.checkoutCustomer() is a factory for this reason.

A table is an array of arrays (120). Index, mutate, last cell, three ways to walk (121). map, reduce, and find the fails (122). Labs 123–125 are nested loops with a rule: right triangle, inverted stack, pyramid. Those look like school. They are the muscle for walking a web table by row and cell. Day 12 is Helen Bennett. Day 6 is why you can say rows[i][j] without blinking. fullName becoming "PramodDutta" with no space is a getter that concatenated without a separator — a classroom bug I keep because it teaches you to read the getter, not the field names.

Day 7 — Callbacks, Promises, and async/await

Read: Day 7: JavaScript Callbacks, Promises, and Async/Await for Playwright

Playwright is a Promise library wearing a browser. Day 7 is the most important language day. If this day is foggy, Days 10 through 21 will look like magic.

Callbacks. A callback is a function you hand over (126). Playwright’s test(), page.on("dialog", cb), and afterEach are callbacks. A sync callback finishes before the next line (127). forEach is a sync callback. It will not wait. Do not await inside forEach over locator.nth or request.get. Walk with for...of. An async callback runs after the rest of the file (128). setTimeout does not pause the file. A, C, then B. Callback hell is a VWO login written as nests (129). The Pyramid of Doom without the VWO comments is 132. Page Object methods must return Promises, not take cb.

Promises. A Promise is an object you can print (133). page.goto returns Promise<Response>. Logging it does not unwrap it. .then runs only on resolve (134). .catch runs only on reject (135). Playwright APIRequestContext does not reject on HTTP 500. It resolves to an APIResponse. status() can be 500. Throw when !response.ok() if you want lab 135. .finally always runs — like afterEach (136). The VWO login as a Promise chain (137) — return the next Promise inside .then or the chain is a lie.

Promise.all is fail-fast parallel (138) — click plus waitForResponse, or three health GETs that must all be up. Create the waiter first, then fire the action. Promise.allSettled is a test report (139) — smoke three services, print every row including the 500. Promise.race is first settler (140) — and the classroom file does not actually delay, because the 100 / 500 sit outside setTimeout. I keep that bug. Prefer Playwright’s own timeout over a hand-rolled race.

async/await. .then versus await, and the missing token (142) — pass the auth header into the next request.get. The login chain converted (143); filename stays Coverted. async always returns a Promise (144). try/catch/finally is then/catch/finally (145). Sequential when step 2 needs step 1 (146); filename stays sequenttialTest. Parallel when it does not (147); filename stays Parrallel. Interview file (148): forgot await page.goto — assertion is D, navigation is C. A flaky API and a retry loop that does not break (149) — bounded, throw if every attempt failed, return on first ok(). Do not retry a POST that creates a booking unless the API is idempotent. Three retries of POST /booking is three bookings.

Day 19 will climb from raw request.get to ApiHelper to a fixture that owns the token. Every step of that climb is a Promise you learned to await today.

Day 8 — OOP, inheritance, and BasePage

Read: Day 8: JavaScript OOP and Inheritance — From Class to BasePage

A Page Object is not a Playwright feature. It is a class that extends a base. Day 8 takes chapters 16 and 17 and turns them into the BasePage → LoginPage shape you will write for the rest of this series.

A class is attributes plus behaviour (153). Constructor, this, and new (154). Two objects, and method versus function (155) — a method is a function that belongs to an object and uses this. A Browser class with a method body that lies (156). Two API clients, one class (157).

Private, public, static. #apiKey is hidden; user is not (158). cred.apiKey is undefined because the field is #apiKey. Static belongs to the class (159, 160) — a default timeout, a PATH constant, a counter you should not put on the instance if it is shared. Encapsulation hides the field and guards the door (161–163). A setter that can say no is the first validation you will put on a config object.

Export and import is how a class leaves its file. Named import from testutil.js (150). Two modules, same name, as (151). Default import of the logger (152). This is the muscle for import { LoginPage } from './LoginPage' on Day 16 and import { test } from '@fixtures/test-base' on Day 17.

Single inheritance: LoginPage extends BasePage and defines nothing (164) — the child gets the parent’s methods. super() in the constructor (165). Method overriding: whoever’s object is present (166). super.setup() then the extra step (167). One list, three execute() implementations (168). Page Object verify() — the POM you came for (169). The same idea for reports (170).

Multi-level: BasePage → AuthPage → AdminPage (171). JavaScript will not extends A, B (172) — mixins are the honest answer, multiple inheritance is not. Hierarchical skeleton (174) — I will not fill a file the classroom left as a sketch. Then the three files that become the pattern: Basepage.js (named export of the parent; classroom capital P), LoginPage.js (import parent, export child), 173_Test_2.js (the spec).

Day 16 will construct new LoginPage(page) with no parent. Day 17 will give you an abstract BasePage with page, el, log, and protected goto(). The idea is today’s. The Playwright Page type is tomorrow’s — Day 9 types it, Day 10 hands it to you.

Phase 2 — TypeScript is the contract (Day 9)

Day 9 — Types, interfaces, enums, generics, and a typed POM

Read: Day 9: TypeScript for Playwright — Types, Interfaces, Enums, Generics, and Typed POM

JavaScript got you a Page Object. TypeScript makes that Page Object refuse a wrong selector, a missing timeout, and a mistyped status code — before Playwright even launches a browser. Day 9 is still LearningPlaywrightBatch, chapters 18 through 22, labs 175 through 208. We still do not run a spec.

JavaScript stores whatever you pour. TypeScript asks what you meant (175–176). Primitives are string, number, boolean, null, undefined. There is no float. Arrays are T[] or Array<T>. Prefer unknown over any (177–179). any turns the checker off. unknown is the type of a payload you have not trusted yet — request.get().json() starts here. Narrow with typeof, or claim with as (204). void is a log. never is a throw (180).

SDET-shaped types arrive immediately: an endpoint builder, a success-code predicate, a step logger, a typed filter that keeps failed codes (181–183). An interface is a test case that cannot forget a field (184). Missing duration is a compile error. Optional is ?. Frozen is readonly (185) — the API response you must not mutate. A method signature is a calculator for now, a page action later (186). A call signature is a hook (187). A bug report is an interface you will paste into Jira (188). TestConfig is playwright.config.ts in twelve lines (189).

Page object interfaces include the FreeTrail typo (190). FreeTrail is the spelling on main. I do not rename it. A class implements the contract (191). An index signature is a dictionary, not a page object (192) — bags of keys, not LoginPage.

Enums: a string enum is a status that cannot be "pas" (193). Numeric enums, reverse lookup, and environment URLs (194). Browser enum is how you stop launching "chorme" (195). Filename stays 195_REAL_BRowser_Selection.ts. HTTP methods as an enum, not a comment (196). String unions ("chromium" | "firefox" | "webkit") are also valid and closer to Playwright project names. This batch uses enums. When we open playwright.config.ts we map classroom chrome / safari to chromium / webkit. Do not invent a Browser.Chromium member and claim it is in lab 195.

Generics are one function, many payload types (197, 198). wrapResponse<T> is the generic you actually ship (199). Filename stays 199_GENERIC_API_RESPOSNE.ts. One envelope. T is the payload. You stop copying UserResponse, FlagResponse, CountResponse as three types that only differ in data.

Access: public is the URL, private is the key, protected is the timeout (200). A typed Page Object with protected navigation (201). readonly config is how a retry cannot rewrite staging (202). abstract means you cannot new the base test (203). Filename stays 203_Abstract_Clsss.ts. as is a claim, not a proof (204). override is a label on the method you replaced (205, 206). Filename stays 205_Ovveride.ts. TypeScript private is compile-time. It is not a runtime vault.

207_Decorator.ts is 0 bytes. I do not invent a body. 208_23_logs_Decortors.ts is the log wrapper. @Log wraps add and prints arguments. Root tsconfig.json is strict, nodenext, esnext, noUncheckedIndexedAccess, exactOptionalPropertyTypes. It does not enable decorator flags, so treat 208 as a shape, not a guaranteed compile. For Playwright, prefer fixtures over decorators. Root package.json already has @playwright/test ^1.58.2. We still do not run a spec today.

Day 17’s factory on feat-cucumber is TypeScript. ProviderId, ChatMessage, RcaVerdict are types. You met that habit this morning. Day 10 hands you page: Page. The constructor you did not invent today becomes real tomorrow.

Phase 3 — Playwright Test, the browser half (Days 10 to 16)

Days 1–9 were language. Today we launch a browser. The repo switches. LearningPlaywrightFundamentals on main. The batch repo still has chapter_23_Playwright_Fundamentals/tests/example.spec.ts as a scaffold. The numbered labs 209–271 live in the fundamentals tree.

One page fixture, a title assert on VWO, a live test.only you must not commit, and the Browser → Context → Page model that every later locator sits on. Then locators, session, tables, frames, SVG, expect, POM. Folder 21 and folder 22 will skip. I will say so on Day 16 and I will not unsay it here.

Day 10 — First Playwright tests, annotations, context, and pages

Read: Day 10: First Playwright Tests — Install, Annotations, Browser Context, and Pages

The page fixture comes before locators. That is the order on purpose. If you do not know what { page } is, getByRole is a string you hope is unique.

How this repo actually sets Playwright up: playwright.config.ts on fundamentals main is headed (headless: false), traces on, video on, screenshot on, viewport 1920×1080, HTML reporter plus a custom reporter, Allure line commented, root dotenv import commented, testDir: './', testMatch includes tests/**/*.spec.ts and later TTACartProject/tests/**/*.spec.ts. I describe the file as the day describes it. I do not invent a second config.

The batch scaffold is chapter_23_Playwright_Fundamentals/tests/example.spec.ts. Lab 209 is the first real title test on VWO. Lab 210 is annotations — and a live test.only I will not hide. test.skip, test.only, test.fail, test.slow, test.fixme each mean a different contract with the runner. A live test.only is dangerous because it turns a suite into one test. On Day 17, forbidOnly is true when CI is set. That gate exists because of this file.

Util.ts in tests/01_Basics is empty. I will not invent a helper.

Lab 211 is first running test, still a fixture. Then the model: Browser → Context → Page. Lab 212 is a library script: launch, context, page, reverse cleanup. Labs 212, 213, and 214 are not Playwright Test files in the test() sense — they are library scripts that launch Chromium themselves. Lab 213 is two contexts, two users. Lab 214 is two pages, one context, shared cookies. That distinction is the whole day. Two contexts when you need two cookie jars (admin and shopper). Two pages when you need two tabs that share the jar.

Lab 215 is the fixture as isolation — each test gets a fresh context. Lab 216 is the browser fixture, two contexts, now it is a test. Lab 217 is context options: desktop France and an iPhone-shaped session. Lab 218 is test.use — it reuses settings, not a cookie jar. test.use does not share cookies across tests.

Day 9 types become today’s objects. Page is no longer an interface in a lab. It is the fixture you destructure. Tomorrow we put locators on it.

Day 11 — Locators: role, CSS, XPath, and VWO login

Read: Day 11: Playwright Locators — Role, CSS, XPath, and VWO Login

A locator is not a CSS string you hope is unique. It is a lazy, strict, auto-waiting handle.

Locator priority — write this on the wall. Role first (getByRole, then getByLabel, getByPlaceholder, getByText, getByAltText, getByTitle, getByTestId). CSS next. XPath last resort. I will fail a PR that reaches for XPath when a role exists.

goto is a contract, not a URL (219). waitUntil is part of that contract. Default goto, then one Referer (220). Reffer is the filename. Referer is the header (221). Classroom spelling stays.

Locators are lazy — the sentence every VWO spec repeats. Creating the locator does not talk to the page. The action does. Strict mode slaps you when two nodes match. Auto-wait retries until timeout.

The local fixture index.html has no ids on purpose. Lab 222 is VWO login with CSS ids. Lab 223 is XPath on the same login, last resort. Lab 224 is getByRole — the default I want in your PR. Lab 225 is CSS collections, first / nth / last. Lab 226 is pressSequentially — type like a human, then walk history. fill dumps the string. pressSequentially fires keys. Use it when the app listens. Lab 227 is cookies. Cookies belong to the context, not the page. That sentence is why Day 10’s two-context lab exists, and why Day 12 can snapshot a login.

What I would do in a framework — without inventing files: a locator helper that prefers role, a login flow that is not 40 copied fills, cookies stored on the context. Day 12 starts that. Day 16 names the class. Day 17 injects it.

Day 12 — storageState, Allure, lists, and web tables

Read: Day 12: Playwright Storage State, Allure, Lists, and Web Tables

Login once. Snapshot the cookie jar. Open the dashboard without typing the password again. Then label the run for Allure, walk a list of links, and read Helen Bennett’s country from a table. Day 12 is the first day your suite stops wasting time on the login form.

Lab 228 is a library script, not a Playwright Test, that saves user-session.json. I say that out loud because people run it with npx playwright test and then ask why the snapshot is empty. Lab 229 is Playwright Test loading the snapshot and skipping login. storageState is cookies plus localStorage as Playwright serializes them. It is not a promise that every sessionStorage key survived. It is not a worker-scoped auth fixture. It is not SSO. It is a file you load into a context.

Lab 230 is Allure labels on a real VWO login. The Allure reporter is commented out in playwright.config.ts on this repo. If you run npx playwright test and look for an Allure report, you will not find one until you uncomment that line and generate. I will not pretend the comment is a reporter.

Lab 231 is lists, allInnerTexts, and .first() so strict mode does not slap you. Lab 232 is Helen Bennett, XPath indexes, then the native locator. Lab 233 is a second table: count, nth, allInnerTexts. Lab 234 in folder 07_WebTables is empty — and the number is used again in folder 08. I skip the empty blob. I do not invent an employee-management table.

Day 4 already taught you rows[i][j]. Today you get those rows from the page. Day 6 already taught you a table is an array of arrays. Today you assert one cell. Common homework failures: treating 228 as a test, committing a live session file with a real password, uncommenting Allure and forgetting allure generate, using XPath indexes when getByRole on the row exists.

Day 13 — Selects, frames, keyboard, hover, drag-and-drop, and alerts

Read: Day 13: Playwright Selects, Frames, Keyboard, Hover, Drag-and-Drop, and Alerts

A locator on the main page cannot see an iframe. A click cannot accept a JS alert that already opened. A fill cannot open a hover menu. Day 13 is the day your suite stops treating the page as one flat document.

Module 08 is named frames. The files say selects. Lab 234 (234_Web.spec.ts) is a table, not a select — leftover number from yesterday. Lab 235 is the native <select> you do not run yet. Lab 236 is a custom dropdown. Lab 237 is React Select, first pass. Lab 238 is the one I run in reviews. util.ts is a helper that is not imported yet. I do not invent the import. Native selectOption works on a real <select>. It does not work on a div that looks like a dropdown. Custom and React Select are click-the-control, then click-the-option. Write that on the wall.

Module 09 is the iframe day. Lab 239 is one iframe, a whole form — frameLocator. Lab 240 is named frames and an inventory. Lab 241 is the nested chain: iframe within iframe. Main frame versus iframe is the rule: if you can see it in the screenshot and the locator times out, you are probably in the wrong frame.

Module 10 is keyboard, SpiceJet hover, Kanban drag, right-click. Lab 242 is keycode.info. Lab 243 is not in this folder — it sits with alerts. Lab 244 is SpiceJet Add-ons hover. Lab 245 is the honest dragTo. Lab 246 is the manual mouse path on a Kanban board. Use dragTo first. Use the mouse path when the app listens to a sequence dragTo does not fire. Lab 247 is context menu, right-click.

Module 11 is JS alerts. Register the handler before the click. Shared beforeEach. Alert — accept, then assert #result. Confirm — accept, or the commented dismiss. Prompt — accept with text. waitForTimeout is not required for frames and drag-and-drop. It is a classroom leftover when it appears. Prefer the locator’s auto-wait and the event.

What I extract into a framework later — not today: a frame helper, a dialog fixture, a hover menu page. Day 17’s UtilElementLocator already has hover, rightClick, selectByText. We do not jump there. We run these files first.

Day 14 — SVG, Shadow DOM, upload, download, and scroll

Read: Day 14: Playwright SVG, Shadow DOM, File Upload, Download, and Scroll

A CSS selector that works on a div can miss an SVG path. A locator that works on light DOM can miss an open shadow tree if you fight Playwright instead of using it. File upload is not fill. Download is not click. Scroll is not waitForTimeout. Day 14 is five boundaries from the actual files.

Module 12 — SVG is a namespace, not a CSS crisis. Lab 248 is Flipkart search, click the first SVG, scrape titles. Lab 249 is the TTA widget, the SVG gym. Lab 250 is name() — the XPath tax for SVG. DevTools will show a path your XPath misses because you forgot the SVG namespace. Playwright’s locator engine is kinder than raw XPath. Prefer it.

Module 13 — open Shadow DOM is a tree, not a pierce plugin. Lab 251 is 251_Shadom_DOM.spec.ts — classroom spelling stays — three hosts on one TTA widget. Playwright pierces open shadow roots through its locators. You do not need a pierce plugin for this lab. The cart assertion expecting 5 after two Increment clicks is a classroom number I will not “fix” in the hub. Read the widget. Read the spec.

Module 14 — upload is setInputFiles, not fill. Lab 252 is one disk file on HerokuApp. Lab 253 is in-memory files, PatternFly, filenames without disk reads. testdata.txt and the jpg names are as the lab serves them. I do not invent binary fixtures that are not on the tree.

Module 15 — download is an event, not a click with hope. Lab 254 is 254_File_Downlaod.spec.ts — classroom spelling stays. waitForEvent('download') first, then the click, then saveAs or path(). Live download-static is the target. Commented tta-notes stays commented.

Module 16 — scroll until the list grows, then poll. Lab 255 is a live lazy list; four recipes commented. scrollIntoViewIfNeeded plus expect.poll is the shape I want. waitForTimeout is not.

Describe titles that say “File Upload Demo” in the download and scroll labs are leftovers. I do not rename them. Common review failures: CSS on SVG, closed-shadow assumptions on an open tree, fill on <input type="file">, click-and-hope on a download, scroll by sleep.

Day 15 — Assertions, hooks, and data-driven tests

Read: Day 15: Playwright Assertions, Hooks, and Data-Driven Tests (CSV, JSON, Faker)

A click without an expect is a demo. A hook without a fixture is just a function you forgot to name. A login test you copy five times is not a suite. Day 15 is expect, then hooks, then one loop that registers one test per row — from the actual files.

Module 17 — prove it. Lab 256 is three expect families in one describe. Some expects need await (locator assertions auto-wait). Some do not (value assertions on an already-resolved number). toBe is identity. toEqual walks fields. expect.soft collects. Use it when you want three field failures in one test, not a stop at the first. Lab 257 is title, URL, and widget state. Filenames stay Asserations. The cheatsheet is Expect_Assertions_Cheatsheet.md. The long reference is More_Expect_Examples.md. I quote those names. I do not invent a third markdown.

Module 18 — prepare it. Lab 258 is titled HOOK and is annotations, not lifecycle hooks — skip, slow, fixme, fail. Lab 259 is one test, three test.steps. Lab 260 is the lifecycle: beforeEach, afterEach, beforeAll, afterAll. A fixture is not a hook. { page } is a fixture. beforeEach is a hook. Day 16 will say this again when folder 21 skips. Lab 261 is describe.serial versus parallel. Serial when the second test needs the first test’s booking. Parallel is the default. Day 19’s crud.spec.ts is serial for this reason.

Module 19 — multiply it. The loop rule: register tests, do not loop inside one test. Lab 262 is the inline array. login-data.csv as GitHub serves it. csvReader.ts is hand-rolled, string-only. Lab 263 is the first file reader, and it asks for a column the CSV does not have. I keep that landmine. Lab 264 is CSV plus hooks plus a branch. Lab 265 imports JSON and still says CSV in the title. Unused readers: yamlReader.ts, xlsxReader.ts, login-data.yaml. They exist. No spec consumes them. If you tell an interviewer this repo has YAML and Excel data-driven tests, you are inventing a consumer. Labs 266–269 are Faker, from one user to a loop.

Fixture versus hook versus DDT loop is the diagram in words. A fixture gives you an object. A hook runs around a test. A DDT loop registers N tests from N rows. Three tools. Do not use a for inside one test to fake N tests — you get one report row and the first failure stops the rest.

Day 16 — Page Object Model, fixtures, and course projects

Read: Day 16: Playwright Page Object Model, Fixtures, and Course Projects

A spec that owns every locator is a demo. A LoginPage that owns goto and login is a page object. A custom fixture that injects that page is the next layer — and that lab is still a skipped placeholder on main. Day 16 is the honest split: POM and two course projects are real. Fixtures and misc are not.

Module 20 is the same login, two owners. Lab 270 (270_WithOut_POM.spec.ts) owns every locator. The describe is called DDT Simple — leftover title. It asserts a URL. Lab 271 (271_Login_With_POM.spec.ts) names the flow: new LoginPage(page), Faker email/password, title assert. LoginPage.ts is the class, not a fixture. Constructor stores locators. goto() opens. login(username, password) fills and clicks. No BasePage. No test.extend. Four files in tests/20_Page_Object_Model. That is the real POM lesson in this repo.

Module 21 — custom fixtures are a placeholder. Say so. 272_Fixture_Placeholder.spec.ts is test.describe.skip('Custom fixtures placeholder', ...). The README plans custom test data, POM fixtures, worker-scoped auth, teardown. None of that is coded. Built-in fixtures you already used — page, browser from Day 10 — are not folder 21. If you run this folder and see a green skip, you have not “done fixtures.” You have confirmed the placeholder is still a placeholder.

Module 22 — miscellaneous concepts are a placeholder. Say so. 273_Misc_Concepts_Placeholder.spec.ts is test.skip. Planned README topics: metadata, timeouts/retries, traces/video/UI mode, artifacts, network/API, env config. Traces are not a Day 16 lab. They are a Day 20 lecture.

tests/Projects — two folders, one spec. Project 4 TTA Bank is helpers, not POM. Signup → transfer $5,000 → confirm → dashboard $45,000.00. Task1.spec.ts is the only project spec in this tree. Project 5 QA Profile is a README scaffold. Suggested Task1.spec.ts / pages/ / fixtures/ are not on disk.

TTACartProject/ lives outside tests/. The config testMatch includes it. Three page objects, classroom class names: Loginpage, TtacartinventorypageTs, TtacartcheckoutpageTs. One E2E. dotenv in the spec, not in the commented config block. .env commits TTACART_USERNAME / TTACART_PASSWORD demo values. Do not put real passwords here. Checkout is one class in this project. Day 17 will split checkout into three pages. I do not pretend they are the same repo. They are the same app.

tests/23_Advance_Framework is also a skipped placeholder (274_Advanced_Framework_Placeholder.spec.ts). It is tomorrow’s teaser. The real layers live in AdvancePlaywrightFramework1x on feat-cucumber. I will not quote a src/pages/BasePage.ts that this fundamentals tree does not have.

Three implementations, one idea: spec owns the page (270), class owns the page (271, TTACart), fixture owns the page (folder 21, not written). Only the first two exist as working code today. Day 8 already taught you class LoginPage extends BasePage. Day 9 already typed it. Today you put a real Page in the constructor. Tomorrow the fixture news that constructor for you.

Phase 4 — The framework layers (Days 17 to 21)

A page object is a class. A fixture is a lifecycle. A framework is layers: config that owns baseURL, pages that share BasePage, fixtures that inject those pages, a reporter that writes a human HTML file, a second runner that reuses the same pages, an API client that owns the token, a CLI that CI actually runs, and — last — an MCP port and an agent factory.

The repo switches again. AdvancePlaywrightFramework1x, branch feat-cucumber. Playwright 1.60. Node 18+. CommonJS / module: Node16. App under test: TTACart at https://app.thetestingacademy.com/playwright/ttacart/. API under test: Restful Booker. I fetched those trees. I will not invent a file GitHub does not list.

Day 17 — Config, pages, fixtures, reporters

Read: Day 17: Advanced Playwright Framework Layers — Config, Pages, Fixtures, and Reporters

Day 16 asked who owns the page. Three answers. Only two existed in the fundamentals repo. A framework is the third answer plus two more: config owns the environment, a reporter owns the evidence.

Layer 1 — config. playwright.config.ts starts with dotenv.config() so a root .env is in process.env before any spec imports credentials. resolveBaseURL() is the host owner. BASE_URL wins if set. Otherwise TTA_ENV picks a lane. Default is qa. qa and prod fall through to https://app.thetestingacademy.com. api is Restful Booker, default https://restful-booker.herokuapp.com. dev / local point at http://localhost:3000. stg / stage / staging use STG_BASE_URL or https://stage.thetestingacademy.com. There is no src/server in the trees I listed. I will not invent a local TTACart server.

testDir is ./src/tests. Timeout 60_000. Expect timeout 10_000. fullyParallel true. isCI is !!process.env.CI. Locally: forbidOnly false, retries 0, workers undefined. On CI: forbidOnly true, retries 2, workers 4. A leftover test.only fails the build only when CI is set. That is Day 10’s live test.only becoming a gate.

Five reporters, in this order: ./src/utils/CustomReporter.ts, HTML into playwright-report, JSON into test-results/results.json, allure-playwright, and list on stdout. use sets baseURL from resolveBaseURL, screenshot only-on-failure, video on, trace on-first-retry. Projects: two live ones. api uses testMatch on src/tests/apiTests/*.spec.ts. chromium uses testIgnore on that same path. Firefox, WebKit, and mobile-chrome are commented. The README still advertises four browsers. package.json still has test:firefox and test:webkit. Those scripts will look for projects this config does not register.

src/config/credentials.ts maps STANDARD_USER and TTA_SECRET into an as const object. The comment says the fallbacks are public demo creds. The code disagrees. The fallbacks are two empty strings. If you clone this branch and do not set the env keys, loginAs sends empty strings. I will not invent a committed .env. tsconfig.json aliases: @pages, @fixtures, @config, @utils, @ai, @api, @testdata, @tests. Imports are extensionless.

Layer 2 — pages. Abstract BasePage holds page, el (UtilElementLocator), log (createLogger(scope)), and protected goto(). Subclasses declare their own private readonly Locator fields. The base class deliberately does not pre-build any locators. I will not add a username field on BasePage. TTACart pages as GitHub serves them: LoginPage, InventoryPage, ItemDetailPage, CartPage, CheckoutStepOnePage, CheckoutStepTwoPage, CheckoutCompletePage. There is no CheckoutPage.ts. Day 16 had one checkout class. This repo split the flow the way the app splits the URLs. LoginPage.PATH is /playwright/ttacart/index.html. data-test everywhere. errorBox and loginCredentialsHint are declared and never used. There is no expectError on LoginPage. I will not invent a negative-login method.

Layer 3 — fixtures. src/fixtures/test-base.ts is the file Day 16 could not show you. test.extend of seven page fixtures. Each is the same shape: construct, do not open. Different flows reach pages in different orders. The spec calls open(). Import { test, expect } from @fixtures/test-base. If you keep @playwright/test, you get { page } and you do not get inventoryPage. booker.fixture.ts is the API twin: bookingApi plus bookerToken. src/fixtures/index.ts is empty (size 0, same empty-blob SHA as the Dockerfile). Specs import @fixtures/test-base, not a barrel. There is no storageState here. There is no worker-scoped login. I will not write one and pretend it lives in test-base.ts.

Layer 4 — the spec. One e2e file: src/tests/e2e/e2e-checkout.spec.ts. Describe title @P0 @Regression E2E @Checkout Checkout Feature. beforeEach opens login and calls loginAs. One test, six visualStep blocks, item id test-allthethings-tshirt-red. The README fixture example used tta-bike-light. Different string. The spec wins. seed.spec.ts is a codegen stub with the comment generate code here. It will still run under chromium. A seed test that does nothing will pass. That is not coverage.

Layer 5 — utils. logger.ts is console plus logs/combined.log. The README promised logs/error.log with rotation. That pair is not in the file. visualStep wraps test.step and attaches step-N-slug PNGs. DataGenerator.checkoutCustomer() is Faker v8, checkout-shaped. DataGenerator.credentials() is random — not the env credentials object. Two different words. ApiHelper is GET/POST/PUT/PATCH/DELETE plus retry. Classroom comments stay: Request Modifiction, exmaple3.

Layer 6 — CustomTTAReporter. File path src/utils/CustomReporter.ts. Class name CustomTTAReporter. About 91 KB because HTML/CSS/JS live in template strings. onEnd writes tta-report/report_RUNID.html and tta-report/index.html as a redirect. Public extra door for Day 18: renderExternalRun(...). Cucumber does not speak Playwright Reporter. The formatter rebuilds TestData[] and calls this method.

Honest mismatches I will not smooth over: Dockerfile empty. test:lor documented and not scripted. README logger is not logger.ts. README credentials fallback is not credentials.ts. Firefox/WebKit are comments. @P0 versus @p0. Empty fixtures barrel. seed.spec.ts untagged. Checkout is three classes. Day 17 is not Cucumber, not Docker, not worker-scoped auth. Cucumber is tomorrow.

Day 18 — Cucumber BDD on the same pages

Read: Day 18: Cucumber BDD in the Advanced Playwright Framework

A Gherkin line is not a test. A step that forgets await is not a Given. A second runner that reuses the same LoginPage is the point.

Two runners is a cost. You pay it only if someone in the room needs to read the scenario without opening TypeScript. Product. BA. A junior who can review “locked-out user is refused” before they can review loginAs. If that person does not exist on your team, stay on Playwright Test. Day 17’s e2e-checkout.spec.ts already tells the story in test.step titles. This framework pays the cost thin: business-readable .feature files drive TypeScript step definitions that reuse the same Page Objects. No parallel automation stack. If your steps start calling page.locator for every field, you grew a second stack.

cucumber.js is the file that is not TypeScript. requireModule, require, paths, three formatters, profiles default / level0 / level1 / level2. src/cucumber/tsconfig.json forces CommonJS while the rest of the repo is Node16. That override exists so ts-node can load step defs.

CustomWorld is the fixture. It holds browser, context, page, six page objects, CREDS, scratch, initPages(). hooks.ts: one Chromium in BeforeAll, a fresh context and page in Before, a PNG on every AfterStep, an extra PNG on fail. Browser once, page every scenario.

Level 0 is installation wiring. smoke.feature plus steps/smoke.spec.ts — a .spec.ts filename that Cucumber loads, not Playwright. Three-line happy path that only talks to this.loginPage and this.inventoryPage. Prove the wiring, then stop.

Level 1 is parameters, negatives, and the missing await. login.feature plus step/login.steps.ts (singular step). The Given that should open the login page is missing await on this.loginPage.open(). I do not patch it to make this post prettier. There are two “I am on the TTACart login page” steps. Read both. Call out the missing await.

Level 2 is three data-driven shapes, one step file (checkout.steps.ts). Shape 1: Scenario Outline. Shape 2: DataTable of product ids. Shape 3: personas loaded from customers.json — alice, bob, carol. That JSON is not generated by Faker in the file I fetched. Level 2 does not have its own login steps; it reuses what the world already constructed.

The TTA formatter is the same HTML, different driver. ttaFormatter.ts rebuilds TestData[] and calls CustomTTAReporter.renderExternalRun. ttaFormatter.cjs exists because Cucumber loads formatters with native ESM import(). Open tta-report/ after a BDD run the same way you did after Day 17.

CI does not run Cucumber. .github/workflows/playwright.yml runs npx playwright test. It does not call cucumber-js. package.json has test:bdd, test:bdd:smoke, test:bdd:tta, cucumber:level0 / level1 / level2. Those scripts are real. The workflow does not use them. If you tell an interviewer the BDD track is gated in GitHub Actions, you are inventing a job that is not in the YAML. Day 20 will say this again. Day 21 will not unsay it.

There is no level-03. There is no src/cucumber/support/hooks.js. There is no Playwright config switch that starts Cucumber. Classroom names stay. The skill is who owns the locator. The feature never does. The step should not. The page already does — Day 17 built it. Today we glue English to that page.

Day 19 — API testing: Restful Booker, JSONPath, Ajv

Read: Day 19: Playwright API Testing — Restful Booker, JSONPath, and Schema Validation

A request.get in the spec is a demo. An ApiHelper that owns GET/POST/PUT/PATCH/DELETE is a layer. A BookingApi fixture that owns the token is a framework. JSONPath reads the body. Ajv refuses a broken contract. Day 19 is that climb.

Playwright Test ships APIRequestContext. The built-in request fixture is one of those contexts. It shares baseURL and extraHTTPHeaders from config. It does not open Chromium. You do not need Rest Assured with extra steps. You need a project that matches API specs and ignores them in the browser project so the same file is not executed four times.

Layer 01 — the spec owns the HTTP. 01_restfulbooker_raw/. basic_ping.spec.ts hits /ping and expects 201, not 200. Restful Booker is like that. post_operation.spec.ts has two tests; both POST /booking. The second title says PUT. I do not rename it. put_operation.spec.ts is token, create, then PUT with Cookie token. crud.spec.ts is test.describe.serial, a BookingFlowState object that carries token and bookingId. newcontext_api.spec.ts hits gorest.in with isolated headers — a different host.

Layer 02 — verbs live in ApiHelper. create-booking.spec.ts and update-booking.spec.ts. Folder 02 does not call callApiWithRetry in the specs I fetched. The helper has the method. The specs use post / put / isSuccess / parseJsonResponse. There is no PATCH spec in folder 02.

Layer 03 — BookingApi and the fixture that owns the token. booking-crud.e2e.spec.ts. Typed client: auth, createBooking, updateBooking, patchBooking, deleteBooking, getBookingResponse. getBooking versus getBookingResponse is body versus raw response — read the file. Token generation lives in bookerToken in src/fixtures/booker.fixture.ts. The e2e spec never calls /auth itself. DELETE returns 201 in this spec. Restful Booker again. Then GET 404.

Layer 04 — JSONPath reads the body. jsonpath-plus queries on live Restful Booker responses: root, child, recursive descent, wildcard, index, slice, filter. JSONPath almost always returns an array. The cheatsheet and store.json (The Testing Academy Bookstore) are teaching docs. The e2e spec does not read store.json. Do not mix them.

Layer 05 — Ajv refuses a broken contract. Ajv 8 plus ajv-formats 3, Draft-07. Schema at src/testdata/schemas/create-booking.schema.json. Three steps in one spec: static sample, live POST, deliberately broken object.

On the tree, not in the day: 06_ai_datagen/. Present. Closed. That is a later layer.

Pins on this branch: @playwright/test ^1.60.0, jsonpath-plus ^10.4.0, ajv ^8.20.0, ajv-formats ^3.0.1, @faker-js/faker ^8.4.1. Run only API tests with the api project. Day 7 taught you the Promise. Day 15 taught you serial versus parallel. Day 17 taught you ApiHelper exists. Today we climb it.

Day 20 — CLI, codegen, Trace Viewer, and CI

Read: Day 20: Playwright CLI, Codegen, Trace Viewer, and CI

A local run is a demo. A trace zip is the recording of that demo. A GitHub Actions job that fails the pull request is a gate.

This is not the already-published scrolltest Day 20. That live post is on codegen and MCP. This series Day 20 is CLI, codegen, Trace Viewer, and CI. Different slug. Different labs. Different gate.

CLI labs come from LearningPlaywrightBatch, Lecture_Playwright_CLI, learning 01 through 10 and exercises 01 through 05. I will not invent a learning 11 or an exercise 06. The lecture config is playwright.config.js, not TypeScript. Classroom leftovers stay. The recorder login button locator uses a role name with a leading space, because that is how the-internet labels the button. Intentional-fail describes are titled INTENTIONAL FAIL.

Learning 01: the CLI is the product. npx playwright <command> [options]. --help on any command. Flags always override the config for the current run. The config is for defaults. The CLI is for overrides.

Learning 02: running tests. All tests, one file, multiple files, a directory, grep by title, project, headed, debug, workers, retries. These are the flags you will type every week.

Learning 03: the recorder is not a framework. codegen against a URL, against a device (iPhone 13, iPad Mini, Pixel 5). You record. You then move locators into a page object. Leaving codegen output as the suite is how you get 40 specs that all type the password.

Learning 04: the HTML report is the shareable artifact. Learning 05: UI Mode is a local loop, not CI. Host and port matter in containers. Learning 06: Inspector is a stepper. PWDEBUG=1. Learning 07: the trace is the film of the run. On, on-first-retry, off. Open the zip in Trace Viewer. Learning 08: browsers are a cache, CI is a clean disk. install, install chromium, the reason a fresh runner downloads 300 MB. Learning 09: flags beat config, config beats defaults. A custom --config path. Learning 10: CI textbooks versus the YAML on disk.

The CI bits come from the advanced framework on feat-cucumber: .github/workflows/playwright.yml, copilot-setup-steps.yml, plus the CI-aware fields in playwright.config.ts. This pipeline runs the Test runner. It does not run cucumber-js. It does not run typecheck. It does not run lint. forbidOnly is the test.only gate. Sharding appears in the lecture’s playwright-sharded.yml as a textbook. Read the file the day quotes. Do not invent a shard job that is not on feat-cucumber if the day says it is lecture-only.

Exercises 01 through 05: basic CLI, recording tests, inspector and trace, config versus flags, toolkit mini project. cli_project tests cover homepage, login, committed recorder samples, intentional failures, and demo scripts. I name those because the day names them. I do not add an exercise 06.

Day 10’s test.only is why CI has forbidOnly. Day 17’s isCI switch is the same file this workflow relies on. Day 18’s BDD scripts are still not in the workflow. That is the honest gate.

Day 21 — MCP, AI agents, and the capstone

Read: Day 21: Playwright MCP, AI Agents, and Assembling the Framework

A locator is a skill. A Page Object is a layer. A GitHub Actions job is a gate. An MCP server is a USB-C port for the AI. An agent is an LLM in a loop with tools. Day 21 is the capstone. It is also a draft. It is not published.

The last week of a live batch is when someone always pastes a ChatGPT login test and calls it an AI framework. A prompt is not a protocol. A screenshot of Claude clicking Login is not a pipeline. A folder named ai/ is not a factory until a file in that folder owns one job and is imported by the next file.

MCP and agent labs come from LearningPlaywrightBatch: Lecture_Playwright_MCP learning 01–08 and exercises 01–05; Lecture_Playwright_AI_Agents learning 01–07 (08–13 exist on the tree; the day does not open them) and exercises 01–05. Factory code comes from AdvancePlaywrightFramework1x on feat-cucumber: docs/AIAgentFactory.md, src/ai/AIAgentFactory.md, src/ai/index.ts, types.ts, models.json, gateway/llmGateway.ts, config/providers.ts, agents/customDataGenerator.ts, agents/rcaAgent.ts, agents/flakyAnalyzer.ts, utils/jsonExtract.ts. I will not invent a sixth agent or a fourth factory method.

The official Playwright MCP package, as the lecture writes it, is @playwright/mcp. Not a community fork. Not a renamed wrapper. The host spawns npx @playwright/mcp@latest. Transport in this lecture is stdio.

01_What_Is_MCP.md opens with USB-C. Before USB-C, every device had its own charger. MCP is one protocol for an AI to talk to any tool. Host, Client, Server. Discovery, then tools/list, then tools/call. JSON-RPC 2.0. Playwright MCP, Jira MCP, a file-system MCP. MCP in test automation as the lecture lists it: plan tests, write tests, execute tests, report defects, generate reports. That is the STLC story. It is not a replacement for npx playwright test on CI. Day 20 already told you the gate is the Test runner.

Playwright’s built-in agents in the lecture are Plan, Generate, Heal. Planner writes a plan. Generator writes a spec. Healer rewrites a broken locator. The workflow is that loop. The factory on feat-cucumber is a different loop: an LLM gateway, a data generator, an RCA agent, a flaky analyzer. hasApiKey() exists so CI can skip when the key is missing. The reporter tabs on Day 17 (AI Data, AI Verdict, Flaky) read attachments those agents produce. I will not walk a gateway I did not open beyond the files the day lists.

Why MCP is the last mile, not the first: because Days 1–20 are the climb. An agent that generates a spec you cannot read is a liability. An agent that heals a locator you never put in a page object is a second stack. Language first. Types next. Browser after that. Layers last. MCP and agents sit on top. They do not replace the twenty days.

How Days 1 to 21 assemble into one framework

Put the twenty-one days in one sentence and you can teach the batch tomorrow.

Language (1–7). Node, scope, hoisting, literals, ===, branches, loops, arrays, functions, closures, objects, Promises. page.goto() is a Promise. request.get does not reject on HTTP 500. forEach will not wait. ?? preserves 0. const does not freeze a bag.

Object and type (8–9). A class, a method, #private, export / import, extends, super, override. Then TypeScript: unknown, interface, enum, generic, protected, abstract, override. 207_Decorator.ts is empty. FreeTrail stays misspelled.

Browser (10–16). { page }. Browser → Context → Page. Role first, XPath last. storageState is a snapshot, 228 is a script. Allure is commented out. Helen Bennett. Native select versus React Select. frameLocator. Handler before the alert. SVG name(). Open shadow. setInputFiles. waitForEvent('download'). expect families. Hooks versus fixtures versus a DDT loop that registers tests. LoginPage is a class. Folder 21 is a skip. TTA Bank is helpers. TTACart class names stay ugly. Project 5 is a README.

Layers (17–20). Config owns the host. Pages share BasePage. Fixtures construct, do not open. Reporter writes tta-report/. Cucumber reuses the pages. Level 1 Given misses await. CI does not run Cucumber. Raw request → ApiHelperBookingApi + token fixture → JSONPath → Ajv. CLI flags beat config. Trace is the film. The gate is npx playwright test.

Capstone (21). @playwright/mcp. Plan / Generate / Heal. A factory with three agents and a gateway. hasApiKey(). The same TTA report tabs.

Arrows you can draw on a whiteboard:

JS labs  -->  TS contract  -->  page fixture  -->  locator
    -->  storageState / table / frame / expect
    -->  LoginPage class  -->  test.extend fixtures
    -->  CustomWorld (same pages)
    -->  BookingApi + Ajv
    -->  CLI + GitHub Actions
    -->  MCP + agents

Every arrow is a real import or a real command in a file I fetched. That is what I mean by framework. Not a slide that says batteries-included.

What this series will not invent

I keep a list so you can audit me.

  • 12_hoisting_if_block.js — 0 bytes. No lab.
  • 207_Decorator.ts — 0 bytes. No decorator body.
  • Util.ts in fundamentals 01_Basics — empty. No helper.
  • 234_WebTABLE_Employe_Management.spec.ts — empty. Number reused in folder 08.
  • tests/21_Fixture and tests/22_Misc_Concepts and tests/23_Advance_Framework — skipped placeholders.
  • Project_5_QA_Profile — README scaffold. No Task1.spec.ts.
  • Dockerfile on feat-cucumber — 0 bytes.
  • src/fixtures/index.ts — 0 bytes.
  • test:lor — documented, missing from package.json.
  • Demo password fallbacks in credentials.ts — the code is empty strings.
  • Allure reporter on fundamentals — commented out.
  • YAML / XLSX data-driven consumers — readers exist, no spec imports them.
  • A CheckoutPage.ts on the framework — checkout is three pages.
  • A Cucumber job in GitHub Actions — the workflow runs Playwright Test only.
  • A sixth factory agent — three agents in src/ai/agents/.
  • Learning 11 of the CLI lecture, AI Agents learning 08–13 as today’s curriculum, 06_ai_datagen as a Day 19 lab.

If a slide, a README, or a WhatsApp note disagrees with a file, the file wins. Classroom spellings win. Empty blobs win.

How I want you to study for 21 days

One morning, one day post, one repo, one notes doc.

Before 09:00 IST. Clone if you have not. Stay on the branch the day names. Do not switch to main on the framework when the day says feat-cucumber.

During the post. Run the files in the order the day lists. Paste the output. Uncomment the dangerous line. Read the throw. Re-comment. When the day says a file is empty, open it and confirm. When the day says a Given is missing await, open it and confirm.

After the post. Do the homework the day lists. Do not invent a sixth exercise. Tomorrow’s day will assume today’s throw is in your notes.

On weekends. Re-read this hub. Recite the four phases. Recite who owns what: env belongs to config, locators belong to pages, construction belongs to fixtures, story belongs to the spec, evidence belongs to the reporter, English belongs to the feature, HTTP verbs belong to the helper, the token belongs to the fixture, the gate belongs to the workflow.

Do not. Mix this calendar with the TypeScript Challenge calendar. Copy codegen output into src/tests and call it a framework. Tell an interviewer Cucumber is gated. Fill the empty Dockerfile with a sample I did not write. Commit a live user-session.json with a real password. Use var. Use == to decide a click. await inside forEach. Loop inside one test and call it DDT.

If you want me in the room for the reviews, enroll in Playwright Automation Mastery. The series is the spine. The course is the projects.

An interview map across the 21 days

People ask me for a list they can revise the night before. This is that list, tied to a day, tied to a file.

  • Why console.log(a) is undefined with var and a throw with let — Day 1, 14_let_hoisting.js.
  • Why "0" == false is true and expect still fails — Day 2, 25_IQ.js.
  • Why switch ran every case after mine — Day 3, lab 43, missing break. Why switch ("5") missed case 5 — lab 50, ===.
  • Why Array.sort put 11 before 2 — Day 4, lab 70. Why a “copy” mutated the original — labs 73, 75.
  • Why a module-level retry counter failed the second test — Day 5, lab 96 versus a closure.
  • Why expect(body).toBe(expected) failed when fields matched — Day 6, lab 111, toBe versus toEqual.
  • Why await inside forEach did not wait — Day 7, lab 127. Why /booking 500 did not reject — APIRequestContext resolves.
  • What #apiKey does to cred.apiKey — Day 8, lab 158. Why super() must run first — lab 165.
  • unknown versus any on response.json() — Day 9, labs 177, 179. Why 207_Decorator.ts is empty.
  • Why test.only is a CI incident — Day 10, lab 210. Two contexts versus two pages — labs 213, 214.
  • Role first, XPath last — Day 11. Cookies belong to the context — lab 227.
  • Why 228 is not a Playwright Test — Day 12. Why Allure did not generate — reporter commented out.
  • Why selectOption failed on a React dropdown — Day 13, labs 237–238. Handler before the alert.
  • Why XPath missed an SVG path — Day 14, lab 250, name(). Upload is setInputFiles.
  • Why some expects need await — Day 15. Why YAML “DDT” is a reader without a spec.
  • Who owns the page — Day 16. Folder 21 is a skip. TTACart class names as GitHub serves them.
  • Why fixtures do not call open() — Day 17. Empty Dockerfile. Empty strings in credentials.
  • Missing await on Level 1 Given — Day 18. CI does not run Cucumber.
  • /ping is 201 — Day 19. Token lives in bookerToken. Ajv refuses the broken object.
  • Flags beat config — Day 20. The gate is the Test runner.
  • Official package is @playwright/mcp — Day 21. Agents sit on top of Days 1–20.

If you can walk that list and point at a file, you will survive the interview. If you can only walk the list as slogans, you will not.

FAQ — JavaScript to Playwright Framework (21 days)

Is this the same as the 21-Day Playwright TypeScript Challenge on scrolltest?

No. That live series starts at the Test runner and TypeScript. This series starts at JavaScript console.log and earns Playwright on Day 10. Different slug prefix (js-playwright-framework-day-NN), different hub, different labs, different gate. If you landed here from the TypeScript Challenge calendar, do not mix day numbers.

Do I need TypeScript on Day 1?

No. Days 1–8 are plain JavaScript on Node from LearningPlaywrightBatch. Day 9 is TypeScript. Day 10 is the first Playwright spec. You can read TypeScript earlier. You do not need it to run 01_basic.js.

Which GitHub repos does this series use?

Three. LearningPlaywrightBatch on main (JS/TS labs, CLI, MCP, agents). LearningPlaywrightFundamentals on main (Playwright labs 209–271, TTA Bank, TTACartProject). AdvancePlaywrightFramework1x on feat-cucumber (framework layers, Cucumber, API, CI, src/ai). I do not invent files that are not on those trees.

Why does the series wait until Day 10 to install Playwright?

Because a spec is JavaScript. Fixture state, loop indexes, helper functions, const config, await page.goto(), and request.get().json() all obey language rules. Locator skill does not debug a Temporal Dead Zone error or an await inside forEach. We earn the browser after the language.

What is the locator strategy this series teaches?

Role first (getByRole and the other getBy* APIs), CSS next, XPath last resort. Locators are lazy, strict, and auto-waiting. Day 11 is the wall. Days 12–14 add tables, frames, SVG, and shadow trees without abandoning that priority.

Is Page Object Model the same as a Playwright fixture?

No. A POM is a class that owns locators and verbs (LoginPage.goto(), login()). A fixture is a lifecycle that can inject that class (test.extend). Day 16 has the class. Folder 21_Fixture in the fundamentals repo is a skipped placeholder. Day 17’s src/fixtures/test-base.ts is the real test.extend. Day 18’s CustomWorld is the Cucumber twin.

Does the advanced framework run Cucumber in GitHub Actions?

No. .github/workflows/playwright.yml on feat-cucumber runs npx playwright test. It does not call cucumber-js. BDD scripts exist in package.json. The workflow does not use them. Do not tell an interviewer the BDD track is gated.

Why does Restful Booker /ping return 201?

Because that is what the host returns, and basic_ping.spec.ts expects 201. DELETE in the e2e spec is also 201. Playwright API testing is not Rest Assured with extra steps — it is APIRequestContext, then ApiHelper, then BookingApi plus a token fixture, then JSONPath, then Ajv. Day 19 is that climb.

Should I start with Playwright MCP and AI agents?

No. MCP is the last mile. The official package is @playwright/mcp. Agents in the lecture are Plan, Generate, Heal. The factory on feat-cucumber is a gateway plus data / RCA / flaky agents. They sit on top of Days 1–20. They do not replace a Page Object, a fixture, or a CI gate.

What empty or missing files should I expect?

Several, on purpose. 12_hoisting_if_block.js and 207_Decorator.ts are 0 bytes. Fundamentals Util.ts is empty. Web table lab 234 is empty. Folders 21, 22, and 23 are skipped placeholders. Project 5 is a README. The framework Dockerfile and src/fixtures/index.ts are 0 bytes. test:lor is missing from package.json. Credentials fallbacks are empty strings. Allure on fundamentals is commented out. I will not fill those blobs in a blog post.

Where do I go after Day 21?

Back through this hub. Then into the course if you want reviews and the live project path: Playwright Automation Mastery. The public spine is these 21 posts. The job is a suite you can defend in a pull request.

Start tomorrow morning at 09:00 IST

Day 1 publishes with this hub on 30 August 2026 at 09:00 Asia/Calcutta. Node. A .js file. console.log. Then twenty more mornings until MCP.

Here is every day, one more time, so you can leave this tab open:

I am Pramod Dutta. I teach SDETs in India for a living. Language first. Types next. Browser after that. Layers last. See you at 09:00 IST.

Want to master this with real projects? Join the Playwright Automation Mastery course at The Testing Academy.

Similar Posts

Leave a Reply

Your email address will not be published. Required fields are marked *

This site uses Akismet to reduce spam. Learn how your comment data is processed.