Day 17: Advanced Playwright Framework Layers — Config, Pages, Fixtures, and Reporters
This is Day 17 of the 21-day JS to Playwright Framework series. One lesson a day. JavaScript first. TypeScript next. Playwright Test after that. Framework now.
Days 1-7 were the language. Days 8-9 were the object. Days 10-15 opened the browser, found fields, saved a session, crossed frames, pierced shadow, asserted, hooked, and looped CSV. Day 16 was ownership: a spec that owns every locator versus a LoginPage that owns goto() and login(). Folder 21_Fixture in the fundamentals repo was a skipped placeholder. Folder 23_Advance_Framework was another skip. I said yesterday I would not invent a BasePage in that tree.
Today I open the tree where those layers actually live.
I am Pramod Dutta. I teach SDETs in India for a living. The week I open a framework, someone always pastes a folder tree from a README and calls the suite production-grade. A tree is not a layer. A layer is a file that owns one job and is imported by the next file. Today we read those files. We also read the gaps. The README is not the source of truth. The files are.
All labs come from my public framework repo: AdvancePlaywrightFramework1x on branch feat-cucumber. I fetched README.md, package.json, playwright.config.ts, tsconfig.json, the src/pages/ tree, src/fixtures/, src/config/, src/utils/, rules/, src/tests/e2e/e2e-checkout.spec.ts, src/tests/seed.spec.ts, and the repo root listing. I quote those files. I will not invent a file that is not there.
The app under test is TTACart. Login lives at /playwright/ttacart/index.html. Inventory, cart, item detail, and three checkout screens sit next to it. That is the storefront these Page Objects wrap. Restful Booker is the API target in a different project. I will name both. I will not merge them.
Two honest gaps, because they are real on this branch:
- The root Dockerfile exists and is empty (size 0, empty-blob SHA). There is no image recipe. A filename is not a container pipeline.
- The README lists test:lor for tag @lor, and rules/test-quality-checks.md includes @lor, but package.json scripts has test:e2e, test:p0, test:p1 only. test:lor is missing.
Classroom leftovers stay. ApiHelper.ts comments say Request Modifiction and exmaple3. UtilElementLocator.ts says direclty. I do not rename files or tidy comments to make this post prettier.
If you want the video plus project path after you finish these 21 posts, the course is here: Playwright Automation Mastery. The series hub for every day lives here: JavaScript to TypeScript to Playwright Advanced Framework 21-Day Guide.
*Want to master this with real projects? Join the Playwright Automation Mastery course at The Testing Academy.*

Contents
What you will be able to do after Day 17
By the end of this post you can:
- Draw the five-box layer: config, then fixtures, then pages, then specs, then reporter.
- Read
playwright.config.tsand say howresolveBaseURL()picks qa / stg / prod / dev / api, and whatCIchanges (retries, workers,forbidOnly). - Read
src/config/credentials.tsand name the two env keys:STANDARD_USERandTTA_SECRET. Admit the fallbacks are empty strings, not demo passwords. - Read
tsconfig.jsonpath aliases (@pages,@fixtures,@config,@utils,@ai,@api,@testdata,@tests) and import a page without a relative climb. - Read
BasePageand say what the constructor wires:page,el(UtilElementLocator),log(createLogger(scope)), and protectedgoto(). - Walk the TTACart pages as GitHub serves them:
LoginPage,InventoryPage,ItemDetailPage,CartPage,CheckoutStepOnePage,CheckoutStepTwoPage,CheckoutCompletePage. There is no singleCheckoutPage.ts. - Read
src/fixtures/test-base.tsand explain why fixtures construct pages and do not open them. - Read
src/fixtures/booker.fixture.tsas the API twin:bookingApiplusbookerToken. - Admit
src/fixtures/index.tsis an empty file (size 0). Specs import@fixtures/test-base, not a barrel. - Walk
e2e-checkout.spec.ts: login inbeforeEach, addtest-allthethings-tshirt-red, check out withDataGenerator.checkoutCustomer(), assert the thank-you header. - Read
visualStepandCustomTTAReportertogether: attachment namestep-N-slugmaps to the reporter step index. - Read
logger.tsas it is: console pluslogs/combined.log. Not the README rotated error log. - Read
ApiHelperas a thin HTTP wrapper overPage.requestorAPIRequestContext. - Say out loud: the Dockerfile is empty, and
test:loris not inpackage.json. - Tease Day 18 correctly: Cucumber is already on this branch under
src/cucumber/. Tomorrow we open it. Today we stay on the Playwright runner layers.
That is the skill. Not a new locator. The skill is which layer owns which secret. Env belongs to config. Locators belong to pages. Construction belongs to fixtures. Story belongs to the spec. Evidence belongs to the reporter.
The files we are actually using
I fetched the trees GitHub lists. Those files, as GitHub serves them on feat-cucumber:
Root wiring: README.md (marketing map, not gospel), package.json (scripts, Playwright 1.60, Winston, Faker 8, Cucumber, Allure, and no test:lor), playwright.config.ts (dotenv, resolveBaseURL, reporters, live projects api and chromium; firefox, webkit, mobile-chrome commented), tsconfig.json (module Node16, strict, path aliases), Dockerfile (empty, size 0). A committed .env is not on the tree. .env.example is on disk; I timed out fetching its body, so I will not quote keys I did not read.
src/config/ is one file: credentials.ts maps STANDARD_USER and TTA_SECRET into an as-const object.
src/pages/ is nine files: BasePage.ts, LoginPage.ts, InventoryPage.ts, ItemDetailPage.ts, CartPage.ts, CheckoutStepOnePage.ts, CheckoutStepTwoPage.ts, CheckoutCompletePage.ts, and index.ts (barrel re-exports). There is no CheckoutPage.ts.
src/fixtures/ is three files: test-base.ts (custom test, one fixture per TTACart page), booker.fixture.ts (Restful Booker client plus token), and index.ts which is empty (size 0, same empty-blob SHA as the Dockerfile). src/utils/ helpers this post needs: logger.ts (Winston root plus createLogger), visualStep.ts (test.step plus a named PNG attachment), CustomReporter.ts (class CustomTTAReporter, default export), ApiHelper.ts (GET POST PUT PATCH DELETE plus retry), UtilElementLocator.ts (Flex equals string or Locator), DataGenerator.ts (Faker v8 factory used by checkout).
src/tests/: e2e/e2e-checkout.spec.ts is the flagship TTACart flow. seed.spec.ts is a codegen stub with the comment generate code here. apiTests/ and tests/ exist as directories. I am not inventing spec names I did not list.
rules/: README.md has one row in the table. test-quality-checks.md requires typecheck plus lint after every test change. Tags listed there include @lor.
I am not opening src/cucumber/ as today lab. The folder is on this branch. Day 18 is that layer. I will name the files the README lists (support/world.ts, support/hooks.ts, level-00-Installation/) as a teaser only, because I fetched the README tree, not those file bodies, and I will not quote Gherkin I did not open today.
Why Day 17 is a layer day, not a locator day
Day 16 asked who owns the page. Three answers. Only two existed in the fundamentals repo: the spec, and a class you construct. The third answer — Playwright injects the class — was a skipped folder.
A framework is the third answer plus two more:
- Config owns the environment. baseURL, credentials, log level, CI knobs. Specs do not hard-code the host.
- A reporter owns the evidence. Steps, screenshots, video, logs, a human HTML file. The terminal is not the report.
If you came here hoping Day 17 is add Cucumber, stop. Cucumber is tomorrow. If you came here hoping Day 17 is Docker and ship it, stop. The Dockerfile is empty. If you came here hoping I will paste the README folder tree and call it a lesson, stop. We read constructors.
The TTACart path is the same storefront you met in Day 16 TTACartProject, grown up. Yesterday the login class was spelled Loginpage and checkout was one file. Today the login class is LoginPage, checkout is three pages, and a fixture constructs all seven. I do not pretend they are the same repo. They are the same app.
Layer 1 — config: playwright.config.ts and credentials.ts
Open playwright.config.ts first. Everything else hangs off it. The file 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 it is set — the escape hatch for a one-off host. If it is not set, TTA_ENV picks a lane. Default is qa. qa and prod fall through to https://app.thetestingacademy.com, the same origin TTACart lives on under /playwright/ttacart/. api is a different product: Restful Booker, defaulting to https://restful-booker.herokuapp.com. dev and local point at http://localhost:3000. stg / stage / staging use STG_BASE_URL or https://stage.thetestingacademy.com. I will not invent a local TTACart server file. There is no src/server in the trees I listed.
Then the runner itself. testDir is ./src/tests. timeout 60_000. expect timeout 10_000. fullyParallel true. isCI is !!process.env.CI. Locally: forbidOnly false, retries 0, workers left undefined for Playwright. On CI: forbidOnly true, retries 2, workers 4. That is the first honest CI switch in this file. A leftover test.only fails the build only when CI is set.
The reporter list is the second switch. Five entries, in this order: ./src/utils/CustomReporter.ts, html into playwright-report, json into test-results/results.json, allure-playwright (resultsDir allure-results, reportName TTACart Automation Report, environmentInfo with TTA_ENV, resolveBaseURL, Node, OS, CI, plus categories for assertion failures, broken tests, and Timeout), and list on stdout. I will not drop Allure from this list because someone prefers one HTML file. The file registers all five.
use sets shared defaults: baseURL from resolveBaseURL, screenshot only-on-failure, video on, trace on-first-retry, actionTimeout 15_000, navigationTimeout 30_000, extraHTTPHeaders Accept and Content-Type application/json. Screenshot only on failure. Video always. Trace on first retry. Those three matter when we reach visualStep: the helper adds extra per-step PNGs on top of the failure screenshot. Do not confuse them. extraHTTPHeaders is aimed at the API project. It also rides on the chromium project. I leave that as the file wrote it.
Projects: two live ones. api uses testMatch on src/tests/apiTests/*.spec.ts. chromium uses testIgnore on that same path and Desktop Chrome. 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. I will not silently uncomment them in this draft. The comment is the truth.
src/config/credentials.ts — two keys, empty fallbacks
The module exports credentials as standardUser: process.env.STANDARD_USER ?? empty string, and password: process.env.TTA_SECRET ?? empty string, as const. The file comment says dotenv.config() runs in playwright.config.ts before any spec loads, so process.env is already populated. That part is true. The same comment says the fallbacks are the public demo creds so the suite still runs if a local .env is missing. The code disagrees. The fallbacks are two empty strings.
export const credentials = {
standardUser: process.env.STANDARD_USER ?? '',
password: process.env.TTA_SECRET ?? '',
} as const;
If you clone this branch and do not set STANDARD_USER and TTA_SECRET, loginAs will send empty strings. The e2e file comment still writes standard_user / tta_secret as the intended pair. Those strings are not in credentials.ts. Put them in .env. I will not invent a committed .env.
The README env block lists QA_BASE_URL, STG_BASE_URL, PROD_BASE_URL, DEV_BASE_URL, API_BASE_URL, LOG_LEVEL, TEST_ENV, TEST_AUTHOR, and LLM keys. I am repeating the README block as documentation, not as a file I opened. The keys I can prove from code today are the ones playwright.config.ts, credentials.ts, logger.ts, and CustomReporter.ts actually read: BASE_URL, TTA_ENV, API_BASE_URL, DEV_BASE_URL, STG_BASE_URL, PROD_BASE_URL, QA_BASE_URL, CI, STANDARD_USER, TTA_SECRET, LOG_LEVEL, TEST_ENV, TEST_AUTHOR.
tsconfig.json — aliases are the import layer
paths: @ai, @api, @config, @fixtures, @pages, @testdata, @tests, @utils, each pointing at ./src/<name>/*. module and moduleResolution are Node16. strict is true. include is src/**/* plus playwright.config.ts. There is no type module in package.json. Imports are extensionless: import { LoginPage } from @pages/LoginPage. The README CommonJS / Faker v8 story matches the pin @faker-js/faker ^8.4.1. I will not walk ESM migration today.
Layer 2 — pages: BasePage and the TTACart POM
Day 16 LoginPage was a standalone class. No parent. No logger. No locator wrapper. Today BasePage is the parent every TTACart page extends.
The abstract class holds four things: a protected page handle, a protected el (UtilElementLocator), a protected log (Winston Logger), and a protected constructor that takes page plus scope. The constructor builds UtilElementLocator(page, scope) and createLogger(scope). protected goto(relativePath) calls page.goto then waitForLoadState domcontentloaded. baseURL from config prefixes the path.
Four jobs. That is all. The file comment is the design rule I want you to steal: subclasses still 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. Login is not a base concern. goto is protected. Pages expose open().
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');
}
}
The spec should not call goto with a raw cart path. The cart page owns that path.
UtilElementLocator — the action wrapper pages actually call
I fetched this because BasePage imports it. Without it, this.el.fill is magic. Flex equals string or Locator. toLocator() turns a CSS string into page.locator(target) or passes a Locator through. Default action timeout is 15_000 — the same number as actionTimeout in config. Methods the TTACart pages use, from the file: click, fill, getText, getAllTexts, getValue, plus a longer menu (doubleClick, rightClick, hover, type mapped to pressSequentially, clear, count, isVisible, waitForVisible, selectByText / Value / Index). waitForPageLoad waits domcontentloaded then networkidle and swallows a networkidle timeout, with a comment that TTACart is static plus localStorage and demo-origin analytics should not fail the test. Classroom comments stay: direclty, and Checking if it is a normal locator or a Playwright locator. I do not clean them.
LoginPage.ts — path, locators, one verb
static PATH is /playwright/ttacart/index.html. Constructor calls super(page, LoginPage) and binds username, password, login-button, error, and login-credentials via data-test locators. open() calls this.goto(LoginPage.PATH). loginAs(username, password) logs loginAs plus the username, then el.fill on username and password, then el.click on the login button. data-test everywhere. That is the TTACart contract. The page does not own the credential strings. Config does. errorBox and loginCredentialsHint are declared and never used in this file. There is no expectError on LoginPage. I will not invent a negative-login method. Checkout step one is the page that has expectErrorContains.
Inventory, item, cart — the storefront
InventoryPage.PATH is /playwright/ttacart/inventory.html. open() calls goto then assertLoaded(): title text Products, and expect.poll that inventory-item count is greater than 3. addToCart(id) targets data-test add-to-cart-${id}. removeFromCart(id) targets remove-${id}. openCart() uses the shopping-cart link. openItem(id) uses item-${id}-title-link. productNames() uses el.getAllTexts.
ItemDetailPage.PATH is /playwright/ttacart/inventory-item.html. openById(id) goes to PATH?id= and asserts the URL regex plus a visible name. addToCart / removeFromCart here use the generic data-test add-to-cart and remove — no id suffix, because you are already on one item. back() returns to products.
CartPage.PATH is /playwright/ttacart/cart.html. assertLoaded() expects title to contain Your Cart. rowCount() is itemRows.count(). checkout() uses data-test checkout. remove(id) uses remove-${id}. continueShopping() goes back. The e2e spec does not use ItemDetailPage. It adds from the inventory grid. The page still exists. I list it because GitHub lists it. I will not write an item-detail spec that is not in src/tests/e2e/.
Checkout is three pages, not one
Day 16 TTACart project had one checkout class. This repo split the flow the way the app splits the URLs.
CheckoutStepOnePage — /playwright/ttacart/checkout-step-one.html. Guest form: firstName, lastName, postalCode. fillGuest takes a CheckoutCustomer from DataGenerator (imported as type GuestUser). continue() does not assert navigation. The comment is the lesson: for valid input the page navigates to step 2; for invalid it stays. Do not blindly assert here — let the spec verify post-state. expectErrorContains and firstNameValue() exist because of problem_user. The file comment says it out loud: the first valid submit clears firstName and shows an inline error; specs that exercise problem_user submit twice; the POM does not hide the quirk. That is how you teach a flaky UI without lying in the page object.
CheckoutStepTwoPage — /playwright/ttacart/checkout-step-two.html. Overview. Subtotal / tax / total are full sentences inside one node (Item total: $29.99). parseMoney pulls the trailing dollar amount with a small regex. finish() uses data-test finish.
CheckoutCompletePage — /playwright/ttacart/checkout-complete.html. assertOrderComplete() checks URL checkout-complete(.html)?, title Checkout: Complete!, and header Thank you for your order! backHome() uses data-test back-to-products. src/pages/index.ts re-exports all eight classes. Specs in this repo prefer @pages/LoginPage or the fixture. The barrel is there if you want it.
Layer 3 — fixtures: construct, do not open
This is the file Day 16 could not show you. src/fixtures/test-base.ts imports test as base from @playwright/test and the seven TTACart page classes. It exports type TestFixture with loginPage, inventoryPage, itemDetailPage, cartPage, checkoutStepOnePage, checkoutStepTwoPage, checkoutCompletePage. Then export const test = base.extend of that type. Each fixture is the same shape: async ({ page }, use) => { await use(new LoginPage(page)); }. expect is re-exported from @playwright/test.
export const test = base.extend<TestFixture>({
loginPage: async ({ page }, use) => {
await use(new LoginPage(page));
},
// one block per TTACart page, same shape
});
Import rule: import { test, expect } from @fixtures/test-base. If you keep import { test } from @playwright/test, you get { page } and you do not get inventoryPage. That is the most common classroom bug on this layer. The spec compiles until you destructure a name the base test does not know.
Playwright builds a fresh page per test. The fixture news up the class against that page and hands it over. When the test function returns, the fixture tears down. There is no storageState here. There is no worker-scoped login. There is no authenticatedPage. Day 16 planned folder 21 listed those ideas. This file did not implement them. I will not write a worker-scoped auth fixture and pretend it lives in test-base.ts.
The file comment is the second design rule: fixtures hand over constructed page objects, not opened ones — different flows reach pages in different orders (you might land on the cart via a UI action, not goto). So each spec calls open() or navigates itself. That is why e2e-checkout.spec.ts does loginPage.open() in beforeEach and later cartPage.open() even though a UI action could have taken you there. The fixture does not guess your path.
booker.fixture.ts — the API twin
Same pattern, different product. bookingApi wraps Restful Booker via new BookingApi(request). bookerToken calls bookingApi.auth() so token generation lives in the fixture, not in every CRUD spec. I fetched this file. I did not fetch src/api/BookingApi.ts today, so I will not quote that class body. The import path is ../api/BookingApi — alias @api exists, this file did not use it. I leave the relative import.
src/fixtures/index.ts is empty
Size 0. Same empty blob as Dockerfile. There is no export star from test-base. Do not write import { test } from @fixtures. Write @fixtures/test-base. I will not invent a barrel.
Layer 4 — the spec that consumes every layer
One e2e file in src/tests/e2e/: e2e-checkout.spec.ts. It imports test and expect from @fixtures/test-base, DataGenerator from @utils/DataGenerator, credentials from @config/credentials, createLogger from @utils/logger, visualStep from @utils/visualStep. Scoped logger name is e2e-checkout. FIRST_ITEM_ID is test-allthethings-tshirt-red.
The describe title is @P0 @Regression E2E @Checkout Checkout Feature. beforeEach takes loginPage, logs Step 1, calls open(), then loginAs(credentials.standardUser, credentials.password). The single test should complete checkout successfully takes page, inventoryPage, cartPage, checkoutStepOnePage, checkoutStepTwoPage, checkoutCompletePage. It builds customer via DataGenerator.checkoutCustomer(), then six visualStep blocks: Go to the inventory page (inventoryPage.open), Add one item to the cart (addToCart FIRST_ITEM_ID), Open the cart (cartPage.open plus expect rowCount to be 1), Fill guest details (cartPage.checkout, assertLoaded, fillGuest, continue), Finish the order (assertLoaded, finish), Order is complete (assertOrderComplete). Read that spec as layers, not as a demo. Config owns credentials and baseURL. Fixtures inject the seven pages plus page. Pages expose open, loginAs, addToCart, rowCount, checkout, fillGuest, finish, assertOrderComplete. Utils supply DataGenerator, the scoped logger, and visualStep. The reporter turns six titles into step rows and six named PNGs. FIRST_ITEM_ID is test-allthethings-tshirt-red. The README fixture example used tta-bike-light. Different string. The spec wins. Tags live in the describe title as @P0 @Regression E2E @Checkout. package.json greps lowercase @e2e and @p0. I quote @P0 as the file writes it. src/tests/seed.spec.ts is a codegen stub: describe Test group, test seed, comment generate code here. It imports stock @playwright/test, not the fixture test. It will still run under chromium. A seed test that does nothing will pass. That is not coverage.
Layer 5 — utils the spec leans on
logger.ts — what the file does, not what the README sold
LOG_LEVEL comes from process.env.LOG_LEVEL, default info. The root Winston logger uses errors({ stack: true }), a timestamp YYYY-MM-DD HH:mm:ss, and a printf lineFormat: timestamp [level] [scope] message. Transports: colorized Console, and File to logs/combined.log. createLogger(scope) returns logger.child({ scope }). BasePage passes the class name as scope. The e2e spec passes e2e-checkout. When CustomTTAReporter collects result.stdout, those lines become Test Logs and, when steps exist, get associated into step console blocks.
The README logging section promised more: logs/error.log (errors only, JSON, 5MB rotation times 5) and logs/combined.log (everything, JSON, 5MB rotation times 5). That rotation pair is not in logger.ts. There is no daily-rotate transport. There is no second File transport. I will not document a rotation policy the transport does not implement.
visualStep.ts — the bridge to the reporter
A WeakMap from TestInfo to number keeps a per-test counter so it cannot leak across tests. visualStep(page, title, body) wraps test.step. After the body runs, it reads test.info(), increments the counter, and attaches a PNG named step-${index}-${slugify(title)}. The screenshot is taken after the body, so you see the resulting state. CustomTTAReporter.onTestEnd looks for step-${step.stepIndex}- on the attachment name and assigns that PNG to the step. If you attach a random screenshot.png, it still lands in the gallery. It will not sit inside the step row. The e2e file uses visualStep six times. That is the intended call site. One PNG per step is not free; use it on showcase and e2e specs.
DataGenerator.ts — Faker v8, checkout-shaped
checkoutCustomer() returns firstName, lastName, postalCode from faker.person and faker.location.zipCode(). That is exactly the TTACart step-one form. DataGenerator.credentials() here is random username/password — not the env credentials object. Two different words. src/config/credentials.ts is the login user. DataGenerator.credentials() is fake data. The e2e spec uses the config object for login and the generator for the guest form. Do not swap them. Faker is pinned to v8 because it ships a CommonJS build; v9/v10 are ESM-only. The file comments the v8 API: userName(), password({ length }), location.zipCode().
ApiHelper.ts — HTTP without a page object
Classroom file. Comments include Request Modifiction and exmaple3. I keep them. The class accepts Page or APIRequestContext. getRequest() uses page.request if request is on the context, otherwise the context itself. callApi switches GET POST PUT DELETE PATCH. callApiWithRetry polls with pollingInterval default 5000 and retryCount default 3. Convenience methods: get, post, put, delete, patch. parseJsonResponse, isSuccess (2xx), isFailureClient (4xx). There is no isFailureServer in the file. I will not add a 5xx helper. buildUrl appends URLSearchParams. The comment example2 writes a URL without a question mark in the prose; the code uses a query string. The code wins. This helper is the API-tests layer 2 in the README (src/tests/apiTests/02_restfulbooker_apiHelper/). I did not fetch those specs today. I will not invent a booking POST body I did not open.
Layer 6 — CustomTTAReporter
File path: src/utils/CustomReporter.ts. Class name: CustomTTAReporter. Default export. Config points at the path. I fetched the whole file. It is large (about 91 KB) because the HTML, CSS, and browser JS live in template strings. You do not need the CSS to understand the layer. You need the Playwright reporter hooks.
onBegin builds runId as YYYYMMDD_HHMMSS, sets outputFile to tta-report/report_${runId}.html, prints the TTA banner, writes the first live HTML. onTestBegin / onStepBegin / onStepEnd: only step.category === test.step is recorded. Playwright internal pw:api steps are ignored. That is why a spec without test.step or visualStep has logs but no step breakdown. onTestEnd copies PNG / webm / zip attachments into tta-report/screenshots, videos, traces. Maps step-N names onto steps. Collects stdout/stderr into logs. Associates logs to steps by title match, then sequential leftover distribution. onEnd runs RCA on failures (skipped if no LLM API key), flaky analyzer vs reports/runs/run-*.json, writes the final HTML, writes tta-report/index.html as a redirect to the latest report_*.html, writes history.html. Public extra door for Day 18: renderExternalRun({ runId, startTime, endTime, tests, stats, meta }). Cucumber does not speak Playwright Reporter. The README says src/cucumber/support/ttaFormatter.ts rebuilds TestData[] and calls this method. I did not fetch ttaFormatter.ts today. I am telling you the Playwright side of the bridge exists, because I read it. I am not quoting the Cucumber formatter body. Tabs in the HTML: Test Results, AI Data, AI Verdict, Flaky. Those tabs read attachments named ai-data and the RCA / flaky agents under src/ai/. I did not fetch src/ai/ file bodies today. I will not walk the LLM gateway. I will say the reporter has the tabs and the hooks. TEST_ENV defaults to UAT in the banner. TEST_AUTHOR defaults to TTA-QA in the table. Neither is TTA_ENV. Three different env names. Config uses TTA_ENV for the host. The reporter uses TEST_ENV for the badge. I will not merge them.
NPM scripts on this branch
I fetched the scripts object. Real keys include test, test:headed, test:ui, test:chromium, test:firefox, test:webkit, test:debug, test:e2e, test:p0, test:p1, test:report, test:report:ci, test:allure, the bdd family, cucumber level profiles, lint, typecheck, format, build, and clean. Not in the object: test:lor. README and rules mention @lor. The script key is missing. test:firefox and test:webkit exist as scripts. Those projects are comments in the config. Opposite bug from the missing lor script. BDD scripts are real on this branch. We name them so Day 18 has a door. We do not run them today.
The empty Dockerfile, and the other empty file
GitHub root listing: Dockerfile, size 0, empty-blob SHA. There is no image recipe and no Playwright base pin. If a slide says the framework is Dockerised, the slide is ahead of the branch. src/fixtures/index.ts is the same empty SHA. Two empty files. One is a container story that was never written. One is a barrel that was never written. Call both out in a PR review. Do not fill them with sample code I invented.
rules/ — the quality gate
rules/README.md has one row: test quality checks, trigger = adding or modifying any test under src/tests/**. rules/test-quality-checks.md asks for typecheck, lint, optional format:check, and a smoke run of the new spec on project chromium. Laws: CI fails on typecheck or lint. Do not commit .only, xit, or test.skip without a ticket reference. Every test must have at least one tag from @p0, @p1, @e2e, @smoke, @lor. Combined command: typecheck && lint && test. seed.spec.ts has no tag. The e2e describe uses @P0 not @p0. The rule and the suite are already arguing. I leave both on the table.
How the five boxes connect on one TTACart run
Say this out loud while you run chromium with TTA_ENV=qa. dotenv loads .env. resolveBaseURL returns QA_BASE_URL or app.thetestingacademy.com. Chromium starts. API specs are ignored. e2e-checkout.spec.ts imports test from @fixtures/test-base. Playwright constructs seven page objects against one page. beforeEach opens LoginPage.PATH and calls loginAs with credentials. Six visualStep blocks drive inventory, add test-allthethings-tshirt-red, cart rowCount 1, guest form from Faker, overview finish, thank-you header. Winston writes scoped lines to the console and logs/combined.log. CustomTTAReporter writes tta-report/report_RUNID.html live, copies six step PNGs plus the always-on webm, and leaves tta-report/index.html pointing at the latest run. Playwright HTML lands in playwright-report/. JSON in test-results/results.json. Allure results in allure-results/. That is a framework. Not because the README says batteries-included. Because each arrow in the diagram is a real import. STANDARD_USER and TTA_SECRET must be set or loginAs sends empty strings.
Honest mismatches I will not smooth over
- Dockerfile is empty. Filename is not a pipeline.
- test:lor is documented and not scripted.
- README logger is not logger.ts. No error.log rotation in the file I fetched.
- README credentials fallback is not credentials.ts. Empty strings, not demo users.
- Firefox, WebKit, mobile-chrome are comments. Scripts for firefox and webkit still exist.
- README fixture example uses tta-bike-light. The e2e spec uses test-allthethings-tshirt-red.
- @P0 in the e2e describe versus @p0 in scripts and rules. Case is data.
- src/fixtures/index.ts is empty. Import @fixtures/test-base.
- seed.spec.ts is a stub and will sit in the chromium run.
- LoginPage declares errorBox and loginCredentialsHint and never uses them.
- Checkout is three classes. Anyone asking for CheckoutPage.ts is looking at the Day 16 repo.
- I did not fetch cucumber bodies, ai bodies, BookingApi.ts, .env.example body, or a workflow file.
An SDET who can list those twelve points from the tree will survive a code review.
What Day 17 is not
It is not Cucumber. The BDD layer is on this branch. Tomorrow we open it. It is not Docker. The file is empty. It is not worker-scoped auth or storageState. test-base.ts constructs pages. That is all. It is not a local TTACart server. Paths are relative to baseURL. The live host in config default is app.thetestingacademy.com. It is not the fundamentals repo. LearningPlaywrightFundamentals folder 23 is still a skip. Do not go back there looking for BasePage. It is not a promise that test:firefox works. Read the commented projects.
FAQ
Is this the same TTACart as Day 16 TTACartProject?
Same app family. Different repo, different class names, different depth. Day 16 lived in LearningPlaywrightFundamentals on main with Loginpage, TtacartinventorypageTs, TtacartcheckoutpageTs. Day 17 lives in AdvancePlaywrightFramework1x on feat-cucumber with LoginPage plus three checkout pages and test-base fixtures. I do not merge the trees.
Where do TTACart credentials come from?
src/config/credentials.ts. process.env.STANDARD_USER and process.env.TTA_SECRET, fallback empty string. playwright.config.ts calls dotenv.config() so a root .env is loaded. The e2e comment mentions standard_user / tta_secret. Those values are not hard-coded in the credentials module.
Why do fixtures not call open()?
Because flows arrive on pages in different orders. You might goto the cart, or use the cart badge from inventory. test-base.ts says this in the header comment. The fixture job is new XPage(page). The spec job is navigation.
Why is there no CheckoutPage.ts?
GitHub src/pages listing is BasePage, CartPage, CheckoutCompletePage, CheckoutStepOnePage, CheckoutStepTwoPage, InventoryPage, ItemDetailPage, LoginPage, index.ts. Three checkout URLs, three classes. I will not invent a facade.
What does visualStep do that test.step does not?
It calls test.step, then attaches a PNG named step-N-slug so CustomTTAReporter can hang that image on the matching step. test.step alone gives you a title. visualStep gives you the title plus the picture of the state at the end of the step.
Why is the reporter class named CustomTTAReporter if the file is CustomReporter.ts?
Because that is how the file is written. Config imports the path. The default export is the class. Cucumber future renderExternalRun talks to the same class.
Is test:lor a real script on feat-cucumber?
No. I fetched package.json. The scripts object has test:e2e, test:p0, test:p1. It does not have test:lor. The README table and rules mention @lor. Documentation without a script.
Is the Dockerfile usable?
No. It is an empty file. Size 0. Do not run a docker build expecting a Playwright image.
Does test:firefox work on this branch?
The script exists. The firefox project is commented out in playwright.config.ts. The project is not registered until someone uncomments that block.
Which logger files are actually created?
logs/combined.log, plus colorized console. That is what logger.ts configures. I will not promise logs/error.log or size-based rotation.
What is src/tests/seed.spec.ts?
A codegen stub. Describe Test group, test seed, comment generate code here. It uses stock @playwright/test, not @fixtures/test-base.
Is Cucumber part of Day 17?
No. The branch is named feat-cucumber and package.json already has test:bdd and cucumber:level0. Day 18 opens src/cucumber/. Today we stay on config, pages, fixtures, and reporters.
What should I run after I change a spec?
rules/test-quality-checks.md: typecheck and lint, then the new spec on project chromium. Combined: typecheck && lint && test.
<script type=”application/ld+json”> { “@context”: “https://schema.org”, “@type”: “FAQPage”, “mainEntity”: [ {“@type”:”Question”,”name”:”Is AdvancePlaywrightFramework1x TTACart the same as Day 16 TTACartProject?”,”acceptedAnswer”:{“@type”:”Answer”,”text”:”Same TTACart app family, different repo. Day 16 used LearningPlaywrightFundamentals TTACartProject. Day 17 uses AdvancePlaywrightFramework1x on feat-cucumber with LoginPage, three checkout pages, and test-base fixtures.”}}, {“@type”:”Question”,”name”:”Where do Playwright TTACart credentials come from in this framework?”,”acceptedAnswer”:{“@type”:”Answer”,”text”:”src/config/credentials.ts reads STANDARD_USER and TTA_SECRET from process.env. Fallbacks are empty strings. playwright.config.ts calls dotenv.config() so a root .env is loaded.”}}, {“@type”:”Question”,”name”:”Why do Playwright page-object fixtures not call open()?”,”acceptedAnswer”:{“@type”:”Answer”,”text”:”src/fixtures/test-base.ts constructs each Page Object against the test page and does not navigate. Each spec calls open() or navigates itself.”}} ,{“@type”:”Question”,”name”:”Is there a CheckoutPage.ts in AdvancePlaywrightFramework1x?”,”acceptedAnswer”:{“@type”:”Answer”,”text”:”No. src/pages/ has CheckoutStepOnePage.ts, CheckoutStepTwoPage.ts, and CheckoutCompletePage.ts. There is no CheckoutPage.ts on feat-cucumber.”}}, {“@type”:”Question”,”name”:”What is the difference between Playwright test.step and visualStep?”,”acceptedAnswer”:{“@type”:”Answer”,”text”:”visualStep wraps test.step, then attaches a PNG named step-N-slug. CustomTTAReporter maps that attachment onto the step row.”}}, {“@type”:”Question”,”name”:”Does package.json define test:lor?”,”acceptedAnswer”:{“@type”:”Answer”,”text”:”No. The README lists test:lor and rules include @lor, but package.json on feat-cucumber has test:e2e, test:p0, and test:p1 only.”}}, {“@type”:”Question”,”name”:”Is the AdvancePlaywrightFramework1x Dockerfile ready to build?”,”acceptedAnswer”:{“@type”:”Answer”,”text”:”No. Dockerfile at the repo root on feat-cucumber is an empty file (size 0).”}}, {“@type”:”Question”,”name”:”Does test:firefox work on feat-cucumber?”,”acceptedAnswer”:{“@type”:”Answer”,”text”:”package.json defines test:firefox, but playwright.config.ts comments out firefox, webkit, and mobile-chrome. Only api and chromium are live.”}}, {“@type”:”Question”,”name”:”Which Winston log files does logger.ts create?”,”acceptedAnswer”:{“@type”:”Answer”,”text”:”Console output plus logs/combined.log. README rotation of error.log is not in logger.ts.”}}, {“@type”:”Question”,”name”:”What is Day 18 of the JS to Playwright Framework series?”,”acceptedAnswer”:{“@type”:”Answer”,”text”:”Day 18 is Cucumber / Gherkin on feat-cucumber: src/cucumber/, cucumber.js, and test:bdd. Day 17 stays on config, pages, fixtures, and reporters.”}} ] } </script>
Tomorrow — Day 18: Cucumber on the same pages
A framework that stops at Playwright Test is one audience: SDETs. A lot of teams also owe a reading to people who will never open test-base.ts. That reading is Gherkin. This branch is already named feat-cucumber. I did not tease a folder I had not seen on the tree. README names world.ts, hooks.ts, level-00-Installation, cucumber.js, and the bdd scripts. package.json confirms those scripts. CustomTTAReporter.renderExternalRun is the Playwright-side socket so a Cucumber run can write the same tta-report HTML. I did not fetch those Cucumber file bodies today. I will not paste a feature file I did not open. Tomorrow I will. Same TTACart. Same LoginPage.loginAs. Different runner. If you only remember one sentence from Day 17: config owns env, fixtures construct pages, pages own locators, specs name the story, the reporter writes the evidence, and the Dockerfile is still empty. Series hub (bookmark this): JavaScript to TypeScript to Playwright Advanced Framework 21-Day Guide.
Master Playwright end to end
If you want these layers as a live classroom, join Playwright Automation Mastery at The Testing Academy. Lifetime access. Real TTACart. A job-ready suite, not a README table that lists test:lor when package.json does not. *This is Day 17 of 21. Draft only. Not published.*
