What a framework actually is
The batch has been writing individual spec files. This session starts the framework that those specs will live inside, and the first job was defining the word.
A framework is a structured way of writing and managing your code, built for three things:
- Reusability. One place for a locator, not twenty.
- Manageability. One place for a module, test data, configuration, utilities.
- Scalability. Thousands of tests that still run and still make sense.
The problem it solves is what happens when flat spec files grow. The same locator gets retyped in every file. Login gets re-implemented in every test. Values get hardcoded. Nothing is wrong on day one and everything is wrong by test three hundred.
The framework being built here is the one running more than 12,000 test cases at Tekion, and the same shape used across the 40-plus companies Pramod has consulted for. It carries a custom reporter and an agent factory, which is the part most teams have not implemented.
The build is deliberately from scratch. The previous batch's repository is not being reused.
The tech stack, which is an interview question
"What is the tech stack you have used?" gets asked in interviews, and a vague answer costs you. This is the answer for this framework:
| Layer | Choice |
|---|---|
| Core | Playwright with TypeScript |
| Design pattern | Page Object Model |
| Test data | JSON fixtures with a factory |
| Reporting | Allure now, custom TTA reporter later |
| Logging | Winston |
| Fake data | Faker |
| Config | dotenv |
| Linting | ESLint |
| Database | SQL connection utility |
| CI/CD | GitHub Actions, Jenkins, Docker |
| AI layer | LLM gateway: OpenRouter, Groq, OpenAI, or a local model |
Three agents are planned on top: a failure RCA agent, a test data generator, and visual regression. The framework is also intended to work with Cucumber BDD and to carry API testing alongside the UI suite.
Fixtures: the setup and teardown you are already doing
A fixture is Playwright's way of providing setup and teardown: the things that happen before a test runs and after it finishes.
The class worked it out from the test's own needs. Before a test can do anything, three things have to exist, and the shorthand used in class is BCP:
- Browser
- Context
- Page
Start them before, close them after. Both halves are the fixture. Playwright already does this for you.
Five built-in fixtures, destructured straight into the test:
| Fixture | What it gives you |
|---|---|
page |
a fresh page |
context |
the browser context, cookies and storage |
browser |
the browser instance |
browserName |
the name of the browser you are running on |
request |
the API request context, used for API tests |
test("uses the built-in fixtures", async ({ page, context, browser, browserName, request }) => {
console.log(browserName);
console.log(page.url());
console.log(await context.cookies());
});
If you come from a TestNG or JUnit background, this is the same idea as the before and after hooks. The concept is not new; only the injection syntax is.
Custom fixtures: log in once, inject everywhere
Built-in fixtures hand you a raw page, a raw browser, a raw request. Real suites do not want raw. They want a session that is already signed in.
Without a fixture, two tests look like this, and the duplication is the whole point:
test("test one", async ({ page }) => {
await page.goto("/");
// log in as admin
// assert something on the dashboard
});
test("test two", async ({ page }) => {
await page.goto("/");
// log in as admin, again
// assert something else
});
The tests are different. The login is identical. With a custom fixture, the login disappears from the test:
test("test one", async ({ adminPage }) => {
// already logged in as admin, start at step two
});
The mechanism is the one the batch already met: log in once, store the session in storage.json, and reuse that state. The login runs a single time, the tests that consume it run many times, and they run faster because they skip the login every time.
One fixture per role, and the test picks the one it needs:
| Fixture | Session it injects |
|---|---|
| admin | logged in as an administrator |
| user | logged in as a standard user |
| guest | logged in as a guest |
The analogy from class: a pre-made pizza base. Add vegetables and it is a veg pizza, add meat and it is non-veg, add only cheese and it is a margherita. The base is common and made once. Whatever is common in your tests gets separated out and injected when it is needed. Python developers will recognise the decorator.
Asked how this differs from a utility: a utility is any helper you call. A fixture is specifically the thing injected around your test to handle what happens before and after it.
The obvious risk was raised in class and confirmed: if the login fails, every test that depends on that fixture fails. That is the trade for the speed.
Scaffolding the repository
New repository, named advanced-playwright-2x. Two ways to start it, and both work.
The recommended one:
npm init playwright@latest
The prompts, answered as they were in class: TypeScript, the default test folder, and yes to GitHub Actions. Browsers were already installed, so that step only updated the existing WebKit and Firefox builds.
The older way still works:
npm init
npm install --save-dev @playwright/test typescript
It works, but it creates almost nothing: no test folder, no configuration file, no sample spec. Everything then has to be built by hand, which is why the init command is the one to use.
The scaffold generated a deep-eval folder that nobody asked for, most likely from a local configuration. Delete it. DeepEval is a framework for LLM testing and has no place in this project.
The folder structure
Everything lives under src, with configuration files at the root:
.github/ copilot instructions and workflows
.env environment values
package.json
playwright.config.ts
tsconfig.json
src/
api/ API test helpers
config/ configuration
fixtures/ custom fixtures
pages/ page objects
data/ test data
tests/ spec files
utils/ utilities
Each folder holds exactly one kind of thing. Pages contain pages, fixtures contain fixtures, utilities contain utilities, tests contain tests. That is the whole discipline.
Screenshot this structure. It is the shape the rest of the course builds on, and it is also the answer when an interviewer asks you to describe your framework.
The structure itself was created by handing the diagram to an AI coding tool rather than typing directories by hand. The point made in class: you are a coder, not a typewriter. Use the tool for the mechanical part.
The dependencies, and what each one is for
Installed as a batch after the scaffold:
| Package | Used for |
|---|---|
dotenv |
reading values out of the .env file |
csv-parse |
reading CSV test data |
xlsx |
reading Excel test data |
winston |
logging |
@faker-js/faker |
generated test data |
jsonpath |
reading fields out of API responses |
ajv and ajv-formats |
JSON schema validation in API tests |
allure-playwright |
Allure reporting |
The JSON path, ajv and Allure packages belong to the API testing sessions and are being installed now so the framework is complete. They are not used yet.
Allure is the reporter for now. The custom TTA reporter comes later in the course, not in this session.
playwright.config, the brain of the framework
The generated configuration is a good starting point. A handful of values were changed:
export default defineConfig({
testDir: "./src/tests",
timeout: 60 * 1000,
expect: { timeout: 10 * 1000 },
fullyParallel: true,
retries: process.env.CI ? 2 : 0,
reporter: [["list"], ["html"]],
use: {
baseURL: resolveBaseURL(),
screenshot: "only-on-failure",
video: "on",
trace: "on-first-retry",
},
projects: [
{ name: "chromium", use: { ...devices["Desktop Chrome"] } },
],
});
What each line decides:
| Setting | Effect |
|---|---|
testDir |
where the spec files live |
timeout |
60 seconds for a whole test |
expect.timeout |
10 seconds for a single assertion |
fullyParallel |
tests run in parallel |
retries |
2 on CI, 0 locally |
reporter |
list in the terminal, HTML on disk |
screenshot |
captured only when a test fails |
video |
recorded |
trace |
captured on the first retry |
projects |
Chromium only, the other browsers are dropped |
The retries line is a ternary operator, and it is the piece worth understanding. process.env.CI reads an environment variable that comes from outside the project. If a CI flag is set, use 2 retries. If it is not, use 0.
Nothing inside the repository sets that flag. Jenkins sets it, GitHub Actions sets it, or you set it yourself on the command line. The configuration only reads it.
The class initially read the timeout as 60 minutes and corrected it on screen. It is 60 seconds. 60 * 1000 is milliseconds.
Environment switching, without hardcoding
The .env file holds everything that changes between environments: base URL, dev URL, staging URL, production URL, logging settings, and credentials.
BASE_URL=
DEV_URL=
STAGE_URL=
PROD_URL=
QA_URL=
USERNAME=admin
PASSWORD=admin123
The problem this creates: baseURL in the configuration is a single hardcoded value, but the real base URL is dynamic. Sometimes staging, sometimes production, sometimes dev.
The fix is a function. baseURL accepts one, so instead of a string, call something that decides:
function resolveBaseURL(): string {
if (process.env.BASE_URL) return process.env.BASE_URL;
switch (process.env.TEST_ENV) {
case "api": return process.env.API_URL;
case "dev":
case "local": return process.env.DEV_URL;
case "stage": return process.env.STAGE_URL;
case "prod": return process.env.PROD_URL;
case "qa": return process.env.QA_URL;
default: return process.env.PROD_URL;
}
}
An explicit BASE_URL wins outright. Otherwise the switch reads TEST_ENV and returns the matching URL. Set nothing and you fall through to the default.
Set the environment on the command line before running:
TEST_ENV=prod npm test
TypeScript writes the return type as string with a lowercase s. This came up live: do not carry the Java String habit across.
Credentials belong in .env and .env never goes to GitHub, the same rule the Python batch covered with python-dotenv.
The pages to model
The project for this framework is TTA Cart, a full end-to-end shopping flow. Walking it in the browser produced the page objects to build:
| # | Page |
|---|---|
| 1 | Login |
| 2 | Inventory |
| 3 | Cart |
| 4 | Checkout, step one |
| 5 | Checkout, step two |
| 6 | Order confirmation |
Each becomes a class in src/pages. Building them is where the next session starts.
Walk the cart flow yourself and confirm the six pages before the next class. Identifying page boundaries is the skill; the code that follows is mechanical.
Side note: never waiting on a usage limit
This was not on the agenda. It surfaced when the instructor's own Claude Code reported it was running GPT-5 mini mid-session, which is not what anyone expected to see, and the explanation turned out to be worth keeping.
The problem is familiar to anyone on a paid coding-assistant plan. Heavy use exhausts the quota quickly, and then you wait hours for the window to reset. During a research-heavy build, that wait is dead time.
The fix shown in class is claude-code-router, a proxy that sits in front of the CLI and switches the model behind it. Configure a chain, and when one provider runs out the router moves to the next on its own. No manual switching:
| Order | Provider |
|---|---|
| 1 | Claude Code |
| 2 | Codex |
| 3 | OpenCode, backed by DeepSeek |
| 4 | Command Code |
When the first provider's window resets, the cycle comes back around to it. The effect is a setup that keeps working around the clock instead of stopping every few hours.
It runs entirely on your own machine, and you can see which model is serving each request. The same mechanism explains the surprise on screen: the router was pointing Claude Code at a different model, and the fix was mapping each model name back to itself, Opus to Opus, Sonnet to Sonnet, Haiku to Haiku.
This has nothing to do with the framework and was flagged as a distraction at the time. A dedicated session was offered if there is enough interest, so say so in the batch thread if you want it.
Tasks and announcements
- Today's task: create the folder structure exactly as shown, add a README with the project details, then commit and push to your own repository.
- Starter code is being pushed to the
advanced-playwright-2xrepository. - Next session: Wednesday evening. This was stated several times and corrected explicitly: Wednesday, not Thursday.
- Coming later: the custom reporter, screenshots on all tests rather than only failures, and splitting configuration across multiple environment files. All three were asked about in class and deferred on purpose.
- Follow along after class, not during. The recording is available about an hour after the session. Copy-pasting live means you follow the keystrokes and miss the reasoning.