Where the framework stands, and today's ask
No new code today. The session opened with a checkpoint instead: share your repository in the chat, and get it to the point where the login spec runs green before Wednesday.
The reason given was not administrative. The next four topics all build on the framework existing:
Fixtures, API testing, cucumber and the agent factory only make sense once the framework is there. The argument from the floor was that the next four classes cannot help anyone who has not reached this level, because each of them modifies a framework rather than creating one. Sitting them out and catching up later costs more than doing the build now.
There was a second instruction, repeated several times, that is worth taking literally:
Build the framework from scratch three or four times. Not read it, not clone it. The claim made in class was fifteen-plus rebuilds before it was genuinely memorised, and that the people hired out of the earlier Playwright batches were the ones who could explain the framework end to end without notes. One learner offered ten rebuilds and was told that would be better still.
That is what the rest of this page is for. It is the build plan, in the order it was taught, so the second and third rebuild do not need the videos.
The build plan: seven phases
The class gave the list twice, first as seven phases and then as a longer ten-step version that splits the same work more finely (page objects, fixtures and specs, utilities, test data, observability, configuration, execution, reporting and artifacts, CI, quality gates). They are the same framework. Seven is the one to memorise.
Phases 1 and 2: structure, config and environments
Phase 1 is a folder skeleton and nothing else. Install Node, npm init, add Playwright and TypeScript, then create the tree before writing any code:
npm init -y
npm i -D @playwright/test typescript @types/node
npx playwright install --with-deps
src/
api/ config/ fixtures/
pages/ testdata/ tests/ utils/
The tsconfig.json carries path aliases so specs never contain ../../..:
{
"compilerOptions": {
"baseUrl": ".",
"paths": {
"@api/*": ["src/api/*"],
"@config/*": ["src/config/*"],
"@fixtures/*": ["src/fixtures/*"],
"@pages/*": ["src/pages/*"],
"@testdata/*": ["src/testdata/*"],
"@utils/*": ["src/utils/*"]
}
}
}
Aliases came up again in the quality-gates discussion at the end: import { LoginPage } from '@pages/LoginPage' is the rule, and a relative ../../pages/LoginPage is the thing the rule exists to stop. Playwright resolves these aliases from tsconfig.json with no extra plugin, which was confirmed by running the suite.
Phase 2 is playwright.config.ts plus environments. The part worth copying is the base URL resolver, because hardcoding a URL is what makes a framework single-environment forever:
function resolveBaseURL(): string {
if (process.env.BASE_URL) return process.env.BASE_URL;
const env = (process.env.TTA_ENV || 'qa').toLowerCase();
switch (env) {
case 'api':
return process.env.API_BASE_URL || 'https://restful-booker.herokuapp.com';
case 'dev':
case 'local':
return process.env.DEV_BASE_URL || 'http://localhost:3000';
case 'stg':
case 'staging':
return process.env.STG_BASE_URL || 'https://stage.thetestingacademy.com';
case 'prod':
return process.env.PROD_BASE_URL || 'https://app.thetestingacademy.com';
case 'qa':
default:
return process.env.QA_BASE_URL || 'https://app.thetestingacademy.com';
}
}
Asked in class: how do you add another environment? Add a case to the switch and a variable to .env. A follow-up worth knowing the answer to: your base URL is one value at a time. One run tests one environment. Two environments means two jobs, not one cleverer config.
Artifacts belong in this phase too: screenshot: 'only-on-failure', video: 'on', a trace setting, and CI-aware retries.
Phase 3: the three utilities
Utilities come before page objects, because page objects use them.
The logger is Winston, and the class made the point that you do not write this from scratch, you take createLogger from the Winston docs. What matters is the scoped child logger, so every line says where it came from:
export function createLogger(scope: string): winston.Logger {
return logger.child({ scope });
}
2026-08-24 08:20:31 [info] [LoginPage] loginAs standard_user
Output goes to the console and to logs/combined.log, so a CI run leaves an artifact behind. That file is also what an AI agent will read later to explain a failure.
Interview question given in class: name the Winston log levels. In order of severity: error, warn, info, http, verbose, debug, silly. The default is info, so debug lines stay hidden until you raise the level.
The util element locator is the wrapper every action goes through, and the Flex type is the reason it exists:
export type Flex = string | Locator;
private toLocator(target: Flex): Locator {
return typeof target === 'string' ? this.page.locator(target) : target;
}
Interview question given in class: why build a util element locator at all? The answer offered from the floor was "reusability", and the sharper version is: it does not care whether a caller passes a CSS or XPath string or a ready-built Playwright locator, so a suite of ten thousand tests written by people with different habits still funnels through one place for timeouts, logging and retries. Centralising the action is the point; supporting both locator styles is what makes it adoptable.
The data generator wraps Faker in a static class so tests import one thing. It is covered in full on the Faker and page object class, and there is a live problem with it in the repository right now, in the audit section below.
Phase 4: page objects, and the parent they share
BasePage comes first, because every other page extends it. It wires three things once so no child has to:
export abstract class BasePage {
protected readonly page: Page;
protected readonly el: UtilElementLocator;
protected readonly log: Logger;
protected constructor(page: Page, scope: string) {
this.page = page;
this.el = new UtilElementLocator(page, scope);
this.log = createLogger(scope);
}
protected async goto(relativePath: string): Promise<void> {
await this.page.goto(relativePath);
await this.page.waitForLoadState('domcontentloaded');
}
}
It is abstract deliberately: there is no such thing as an instance of "a page in general", so the class should not be constructible on its own.
Asked twice in class, once as a real interview question a learner had just been given: what does a page object contain? "Locators and actions" was accepted but pushed for more. The full answer: the locators, a constructor that initialises them, and async methods for what that page can do. What it does not contain is assertions. In arrange-act-assert terms, the page object is arrange and act; assert lives in the spec.
Phases 5 to 7: specs, quality gates, CI and AI rules
Phase 5 is the spec, and the spec is where assertions live. Every meaningful action goes inside a test.step with a log line, and tests carry tags:
test('logs in with valid credentials @p0', async ({ page }) => {
await test.step('Login as standard_user', async () => {
log.info('Logging in as standard_user');
await loginPage.loginAs('standard_user', 'tta_secret');
});
await test.step('Verify login form is no longer shown', async () => {
await expect(page.locator('[data-test="login-button"]')).toBeHidden();
});
});
Interview question given in class: why wrap actions in test.step? Answers offered were debugging, reporting and knowing which part failed, all accepted. The framing preferred in class was separation: the login half and the verification half are different concerns, and a step boundary makes the report say which one broke instead of just naming the test.
Phase 6 is reporting and quality gates. The custom reporter hangs off Playwright's reporter lifecycle. The class deferred the walkthrough to Wednesday, but the shape is worth having now, and the repository implements six hooks rather than the five mentioned live:
The quality gates named in class were ESLint for correctness, Prettier for formatting, and a type check with no ignored warnings, all run before a merge rather than after. Alongside them sits a rules/ folder of written conventions: page objects go in pages/, utilities in utils/, no console.log, no business logic in page objects, no direct locators in specs, and use the @ aliases instead of relative imports.
Phase 7 is CI and AI rules. GitHub Actions runs the suite on push and pull request and uploads the HTML report as an artifact. Jenkins is running in parallel as its own track. The AI rule files (CLAUDE.md, .cursorrules, AGENTS.md) exist so that whichever assistant a person uses, it is held to the same conventions as a human contributor.
What the repository actually contains today
The recap describes the finished plan. The repository is partway along it. This table is from cloning AdvancePlaywrightFramework2x, installing it and running it, not from the recap, so it doubles as your checklist.
| Phase | In the repo now | Not there yet |
|---|---|---|
| 1 Structure | package.json, tsconfig.json, all six path aliases, full src/ tree |
typescript itself is not a dependency |
| 2 Config | resolveBaseURL switch, dotenv, screenshot, video, CI retries |
only a chromium project, no Firefox, WebKit or Mobile Chrome; no workers or forbidOnly; src/config/ is empty, so no credentials.ts |
| 3 Utilities | logger.ts, UtilElementLocator.ts, DataGenerator.ts |
no visualStep helper |
| 4 Page objects | BasePage plus Login, Inventory, ItemDetail, Cart, CheckoutStepOne, CheckoutStepTwo, CheckoutComplete |
no barrel index.ts; src/fixtures/ is empty, so no test-base.ts |
| 5 Specs | login.spec.ts, with test.step and a @p0 tag |
no e2e-checkout.spec.ts |
| 6 Reporting | CustomReporter.ts at 2,262 lines, wired with html and list |
allure-playwright is installed but not in the reporter array; no ESLint, no Prettier, and package.json has no scripts at all |
| 7 CI and rules | GitHub Actions workflow, CLAUDE.md |
no Dockerfile, no .cursorrules, no AGENTS.md; rules/ is empty |
The src/ai/ folder is further ahead than the recap suggested: rcaAgent.ts and flakyAnalyzer.ts are already there, with a providers.ts stub whose hasApiKey() returns false until a model is wired up.
Three things that bite on a fresh clone
All three were reproduced on a clean clone with npm install, so expect them.
One: npm test does not exist. package.json ships with "scripts": {}.
npm error Missing script: "test"
Use Playwright directly, which does work. The login spec passes in about 1.4 seconds:
npx playwright test
Worth adding to package.json on your own copy, since Phase 6 calls for it:
"scripts": {
"test": "playwright test",
"test:p0": "playwright test --grep @p0",
"typecheck": "tsc --noEmit"
}
Two: DataGenerator.username() throws. This is the one to fix before Wednesday, because fixtures will lean on the data generator.
TypeError: faker.internet.userName is not a function
The file's own comment says the project pins Faker v8, but package.json asks for ^10.5.0, and the two disagree. Checked across versions: userName() exists in 8.4.1, still exists in 9.9.0 alongside the new username(), and is gone in v10. The lockfile installs 10.5.0, so the call fails.
static username(): string {
return faker.internet.username(); // lowercase 'n' from v9 onward
}
The comment also claims v8 is pinned because it is the last CommonJS-compatible release. That is not the case: a plain require('@faker-js/faker') was tested and works on 8.4.1, 9.9.0 and 10.6.0 alike. There is no CommonJS reason to stay on v8, and staying there is what makes the v9 rename look like a breaking change instead of a one-character fix. password({ length }), person.firstName() and location.zipCode() all behave the same on v10.
Three: the type check fails, and Phase 1 already has the fix. TypeScript is not a dependency, so npx tsc fetches whatever is current, today 6.0.3, and exits 2:
tsconfig.json(5,25): error TS5107: Option 'moduleResolution=node10' is deprecated
tsconfig.json(13,5): error TS5101: Option 'baseUrl' is deprecated
Both come from the old-style module resolution. The module: Node16 in the build plan is exactly the modern setting that clears the first one, and aliases can be declared without baseUrl. Pin typescript in devDependencies at the same time, or your quality gate changes result depending on the day you run it.
Questions from the floor
How are tests isolated from each other? Each Playwright test gets its own browser context and page, so cookies and storage do not leak between them. Scaling that out is Docker sharding or more workers, which is a later class. The Selenium comparison raised from the floor was Chrome profiles, and the answer was that a saved profile with a logged-in user is the closest equivalent.
How do you handle a flaky locator? Not with a fixed wait. Two answers were given. The first is a locator with fallbacks, sometimes sold as "self-healing", which the class called a fancy name for something simple: try page.locator, and if that fails try getByRole, and so on down a list. The second is that waitForSelector-style waits are the vulnerable pattern in the first place, because Playwright already waits before acting.
What do you say when an interviewer asks about promises here? They are usually fishing for Promise.all and Promise.race, which were covered in the JavaScript sessions.
Why TypeScript rather than JavaScript? The argument to take to a reluctant team: scale, strictness and maintainability. Past a few thousand tests, plain JavaScript drifts into loose functions and callbacks that nobody wants to maintain, which is why the large engineering organisations moved.
Which MCPs are actually used? Jira and the Atlassian set, GitHub, Figma for diagrams, and TestRail or TestLink where a team uses them. Selenium's MCP was called not good. Many companies also run internal ones over their own QA knowledge.
Does the log file grow forever? It appends rather than overwrites, and old log files get cleaned out on a schedule, ten to fifteen days in the practice described.
Can this framework be converted? Yes to BDD, and yes to Python and Java, where the page object design carries over unchanged and only syntax and the runner change: pytest and conftest.py in place of the Playwright test runner and fixtures. A Python conversion was promised in class.
What comes next
Wednesday picks up with fixtures, and the motivation given is the one that makes them click: without a fixture, every test that needs a logged-in user logs in again. A login fixture does the browser context and login once and injects the ready-made page into every test that asks for it.
After that: API testing, cucumber, and the agent factory, with Docker sharding alongside it. The custom reporter walkthrough that was deferred today also lands Wednesday.
Jenkins continues on its own track, with part 3 tonight at 9:00 PM IST running this framework's login spec on a freestyle job, and a part 4 planned to cover GitHub Actions.
Tasks and announcements
- Get your repository to the login-spec baseline before Wednesday, and post the link in the batch chat. This is the prerequisite for the next four classes, not a suggestion.
- Rebuild the framework from scratch three or four times. The phases on this page are the running order.
- Fix
DataGenerator.username()in your own copy while you are in there. One character. - Jenkins part 3 tonight, 9:00 PM IST, running the advanced framework's login spec.
- Previous classes for the mechanics: scaffolding and config, BasePage, utilities and the logger, Faker and the page object model, data-driven testing.