Practice Frameworks Advance Playwright framework
End-to-end framework
Concept reference - End to end framework

Advance Playwright framework - build it folder by folder

A step-by-step walk through every folder of the TTA Advance Playwright Framework - TypeScript, Page Object Model, fixtures, data-driven specs, environment config, a custom HTML reporter, Docker, and a sharded GitHub Actions workflow. Every code snippet is copy-paste-ready and targets the TTA practice pages at https://app.thetestingacademy.com/playwright/.

Architecture - the big picture (TTACart suite)

Page Object Model + fixture-based architecture. TypeScript. Playwright Test runner. All 10 boxes use the actual TTACart pages (/playwright/ttacart/...) as the system under test. Left column = framework code. Right column = config + infra.

Framework code (TTACart)

1. Page Objects - src/pages/

LoginPage.ts - /ttacart/index.html
locators: usernameInput, passwordInput, loginBtn, errorBanner
- async open()
- async loginAs(username, password): Promise<InventoryPage>
- async expectErrorContains(text)
InventoryPage.ts - /ttacart/inventory.html
locators: cardList, sortSelect, cartBadge, addBtn(productId)
- async sortBy(value: 'az'|'za'|'lohi'|'hilo')
- async addToCart(productId: string)
- async cartCount(): Promise<number>
- async openItem(productId): Promise<ItemDetailPage>
ItemDetailPage.ts - /ttacart/inventory-item.html
locators: name, desc, price, addBtn, backBtn
- async addToCart()
- async readPrice(): Promise<string>
CartPage.ts - /ttacart/cart.html
- async itemCount(): Promise<number>
- async remove(productId)
- async continueShopping(): InventoryPage
- async checkout(): CheckoutStepOnePage
CheckoutStepOnePage.ts - /ttacart/checkout-step-one.html
- async fillGuest({ firstName, lastName, postalCode })
- async continue(): CheckoutStepTwoPage
- async expectError(text)
CheckoutStepTwoPage.ts - /ttacart/checkout-step-two.html
- async readSubtotal(): Promise<string>
- async readTax(): Promise<string>
- async readTotal(): Promise<string>
- async finish(): CheckoutCompletePage
CheckoutCompletePage.ts - /ttacart/checkout-complete.html
- async assertThankYou()
- async backHome(): InventoryPage

2. Fixtures + Specs - src/fixtures/ + src/tests/

fixtures/test-base.ts
import { test as base } from '@playwright/test';
type Fix = {
  loginPage: LoginPage;
  inventoryPage: InventoryPage;
  cartPage: CartPage;
  checkoutOne: CheckoutStepOnePage;
  checkoutTwo: CheckoutStepTwoPage;
};
export const test = base.extend<Fix>({
  loginPage: async ({ page }, use) => { await use(new LoginPage(page)); },
  inventoryPage: async ({ page }, use) => { await use(new InventoryPage(page)); },
  // ... one fixture per page
});
export { expect } from '@playwright/test';
tests/login.spec.ts
test('@smoke standard_user logs in', async ({ loginPage, inventoryPage }) => {
  await loginPage.open();
  await loginPage.loginAs('standard_user', 'tta_secret');
  await expect(inventoryPage.cardList).toHaveCount(6);
});

test('@regression locked_out_user sees error', async ({ loginPage }) => {
  await loginPage.open();
  await loginPage.loginAs('locked_out_user', 'tta_secret');
  await loginPage.expectErrorContains('locked out');
});
tests/checkout.spec.ts
test('@e2e add 2 items and finish checkout', async ({
  loginPage, inventoryPage, cartPage, checkoutOne, checkoutTwo,
}) => {
  await loginPage.open();
  await loginPage.loginAs('standard_user', 'tta_secret');
  await inventoryPage.addToCart('tta-practice-backpack');
  await inventoryPage.addToCart('tta-bike-light');
  await expect(inventoryPage.cartBadge).toHaveText('2');
  await cartPage.checkout();
  await checkoutOne.fillGuest({ firstName: 'Aarav', lastName: 'S', postalCode: '560001' });
  await checkoutOne.continue();
  await checkoutTwo.finish();
  await expect(page).toHaveURL(/checkout-complete\.html/);
});

3. Utils / Helpers - src/utils/

UtilElementLocator.ts
type Flex = string | Locator;
class UtilElementLocator {
  click / doubleClick / rightClick / hover
  fill / type / pressSequentially / clear
  getText / getInnerText / getAllTexts
  getAttr / getValue / count
  isVisible / isEnabled / isChecked / isEditable
  waitForVisible / waitForPageLoad
  selectByText / selectByValue / selectByIndex
}
FileReader.ts
readJSON<T>(path) - typed JSON fixture
readCSV<T>(path)  - csv-parse/sync
readXLSX<T>(path, sheet) - xlsx
writeJSON(path, data) - snapshot write
DataFactory.ts
generateUser()    - faker name + email
generateCard()    - card number, expiry, cvc
randomZip()
pickProduct(): { id, name, price }
Logger.ts (Winston)
logger.info(scope, msg, meta?)
levels: debug | info | warn | error
transports: console + logs/run-{ts}.log

4. Test data - src/testdata/

users.json
[
  { "user": "standard_user",           "pwd": "tta_secret", "kind": "ok" },
  { "user": "locked_out_user",         "pwd": "tta_secret", "kind": "blocked" },
  { "user": "problem_user",            "pwd": "tta_secret", "kind": "broken-ui" },
  { "user": "performance_glitch_user", "pwd": "tta_secret", "kind": "slow" },
  { "user": "error_user",              "pwd": "tta_secret", "kind": "flaky" },
  { "user": "visual_user",             "pwd": "tta_secret", "kind": "visual" }
]
products.csv
id,name,price
tta-practice-backpack,TTA Practice Backpack,29.99
tta-bike-light,TTA Bike Light,9.99
tta-bolt-tshirt,TTA Bolt T-Shirt,15.99
tta-fleece-jacket,TTA Fleece Jacket,49.99
tta-junior-tester-onesie,TTA Junior Tester Onesie,7.99
test-allthethings-tshirt-red,Test.allTheThings T-Shirt (Red),15.99
register.xlsx
sheet: checkout
  firstName | lastName | postalCode | expected
sheet: cards
  brand | number | expiry | cvc

5. Observability - logs/ + tta-report/

logs/
logs/run-{ISO}.log     - Winston output
logs/network/{spec}.har- HAR per spec
logs/console/{spec}.txt- browser console
tta-report/ (custom HTML)
src/utils/CustomTTAReporter.ts
- onBegin / onTestBegin / onTestEnd / onEnd
- groups by spec file + project + tag
- inlines screenshot thumbs + trace links
- writes to tta-report/index.html
What to attach on failure
- trace.zip
- screenshot.png (full-page)
- console.txt
- network.har
- the failing locator + selector chain
Configuration & infrastructure

6. playwright.config.ts + envs

import { defineConfig, devices } from '@playwright/test';

const ENV = process.env.TTA_ENV ?? 'qa';
const BASES = {
  qa  : 'https://app.thetestingacademy.com',
  stg : 'https://staging.thetestingacademy.com',
  dev : 'http://localhost:8082',
  prod: 'https://app.thetestingacademy.com',
};

export default defineConfig({
  testDir: './src/tests',
  timeout: 30_000,
  retries: process.env.CI ? 2 : 0,
  workers: process.env.CI ? 4 : 6,
  reporter: [
    ['html', { outputFolder: 'playwright-report' }],
    ['json', { outputFile : 'playwright-report.json' }],
    ['allure-playwright'],
    ['./src/utils/CustomTTAReporter'],
  ],
  use: {
    baseURL: BASES[ENV],
    trace: 'on-first-retry',
    screenshot: 'only-on-failure',
    video: 'retain-on-failure',
  },
  projects: [
    { name: 'chromium',      use: { ...devices['Desktop Chrome'] } },
    { name: 'firefox',       use: { ...devices['Desktop Firefox'] } },
    { name: 'webkit',        use: { ...devices['Desktop Safari'] } },
    { name: 'mobile-chrome', use: { ...devices['Pixel 5'] } },
  ],
});
QA STG DEV PROD

7. Test execution + tags

Run commands
TTA_ENV=qa  npx playwright test
            npx playwright test --project=chromium
            npx playwright test --grep @smoke
            npx playwright test --headed --workers=4
            npx playwright test --debug
TTA_ENV=stg npx playwright test --shard=1/4
Tag suites
@smoke      - happy-path login + 1 add-to-cart + finish
@regression - 6 test users + sort + filter + edge cases
@e2e        - full checkout 2-step + payment + complete
@visual     - screenshot diff for visual_user
@flaky      - quarantine for error_user / perf_glitch

8. Reports + artifacts

HTML reporter (default)
playwright-report/index.html
- Tests grouped by spec + project
- Screenshots / video / trace links
- Filterable by status + project
Allure
allure-playwright reporter -> allure-results/
allure generate ./allure-results --clean -o ./allure-report
allure open ./allure-report
- History across runs, categories, severity
Trace viewer
npx playwright show-trace trace.zip
- Step-by-step timeline + DOM snapshots + network
Artifacts on failure
test-results/
  {spec}-{title}/
    trace.zip
    screenshot-1.png
    video.webm
    error-context.md

9. CI/CD + Docker + cloud grid

.github/workflows/playwright.yml
on: [push, pull_request]
jobs:
  test:
    strategy: { matrix: { shard: [1/4, 2/4, 3/4, 4/4] } }
    steps:
      - uses: actions/checkout@v4
      - uses: actions/setup-node@v4 with: node-version: 20
      - run: npm ci
      - run: npx playwright install --with-deps
      - run: TTA_ENV=qa npx playwright test --shard=${{ matrix.shard }}
      - uses: actions/upload-artifact@v4
        if: always()
        with: { name: report-${{ matrix.shard }}, path: playwright-report/ }
  merge:
    needs: test
    runs: playwright merge-reports
    publish: github-pages -> tta-report/
Docker
FROM mcr.microsoft.com/playwright:v1.57.0-jammy
WORKDIR /app
COPY package*.json ./
RUN npm ci
COPY . .
ENV TTA_ENV=qa
CMD ["npx", "playwright", "test"]
Remote / cross-browser cloud
BrowserStack Automate - connectOptions.wsEndpoint with caps
LambdaTest HyperExecute - autosplit + concurrency
Microsoft Playwright Service - regional workers
Sauce Labs - capabilities object
Jenkins (optional)
Jenkinsfile
agent docker { image 'mcr.microsoft.com/playwright:v1.57.0-jammy' }
stages: install -> test -> report
post { always { junit, allure, archiveArtifacts } }

10. Quality + package management + VCS

package.json scripts
"test"          : "playwright test",
"test:smoke"    : "playwright test --grep @smoke",
"test:shard"    : "playwright test --shard=$SHARD",
"report"        : "playwright show-report",
"report:allure" : "allure generate ./allure-results -o ./allure-report",
"lint"          : "eslint . && prettier --check .",
"typecheck"     : "tsc --noEmit"
Quality gates
eslint            - ban absolute XPath, ban nth-child
prettier          - format on save
husky + lint-staged - run lint+typecheck on pre-commit
commitlint        - conventional commits
.editorconfig     - 2 spaces / LF / trim trailing
tsc --noEmit      - blocks merge on type errors
Dependencies snapshot
devDependencies:
  @playwright/test
  @faker-js/faker
  csv-parse
  xlsx
  dotenv
  winston
  allure-playwright
  typescript
  eslint / prettier / husky / commitlint
Version control
.gitignore: node_modules/, test-results/, playwright-report/, logs/
branching : main + feature/* + fix/*
PR rules  : 2 approvals + CI green + no draft merges
.nvmrc    : 20    .npmrc: engine-strict=true

Each card below the diagram zooms into one box - read top to bottom or jump via the TOC chips above. 10 boxes: 5 framework code on the left, 5 config + infra on the right. Every code reference targets a real TTACart page under /playwright/ttacart/.

Diagrams + page screenshots (from the ttacart branch)

Same artefacts that ship in README-TTACART.md on the branch - rendered here so students see them without leaving the tutorial. Mermaid renders client-side; screenshots are pulled live from the branch's docs/screenshots/.

PR #3 Open PR - Review comment - HEAD: 34e68bf - README on branch

1. 10-box framework architecture

Framework code on the left, config + infra on the right. Reporter writes across the line into the right column.

graph LR
  classDef code fill:#fde7f3,stroke:#db2777,color:#111
  classDef data fill:#d1fae5,stroke:#16a34a,color:#111
  classDef obs  fill:#cffafe,stroke:#0891b2,color:#111
  classDef cfg  fill:#ede9fe,stroke:#8b5cf6,color:#111
  classDef ci   fill:#ffedd5,stroke:#f97316,color:#111
  classDef qty  fill:#fef9c3,stroke:#d97706,color:#111

  P[1. Page Objects
src/pages/ttacart/]:::code --> F[2. Fixtures + Specs
test-base.ts + tests/]:::code P --> U[3. Utils
UtilElementLocator + Logger + DataFactory + FileReader + DateUtil]:::code F --> D[4. Test data
users.json + products.csv + types.ts]:::data U --> O[5. Observability
CustomTTAReporter -> tta-report/]:::obs D --> O C[6. playwright.config.ts
TTA_ENV qa/stg/dev/prod]:::cfg --> E[7. Test execution + tags
smoke + regression + e2e]:::cfg C --> R[8. Reports + artifacts
HTML + Allure + trace]:::cfg E --> CI[9. CI/CD + Docker + cloud
ttacart.yml sharded 4-way]:::ci R --> CI CI --> Q[10. Quality + package + VCS
scripts + eslint + husky + gitignore]:::qty O -. writes report .-> R

2. End-to-end checkout sequence

One @e2e test wired through five page objects and the test fixture.

sequenceDiagram
  autonumber
  participant T as Test (checkout.spec.ts)
  participant LP as LoginPage
  participant IP as InventoryPage
  participant CP as CartPage
  participant C1 as CheckoutStepOnePage
  participant C2 as CheckoutStepTwoPage
  participant OK as CheckoutCompletePage

  T->>LP: open + loginAs(standard_user, tta_secret)
  LP-->>T: redirect /inventory.html
  T->>IP: addToCart(tta-practice-backpack)
  T->>IP: addToCart(tta-bike-light)
  T->>IP: expect cartCount = 2
  T->>IP: openCart()
  IP->>CP: navigate /cart.html
  T->>CP: checkout()
  CP->>C1: navigate /checkout-step-one.html
  T->>C1: fillGuest({firstName, lastName, postalCode})
  T->>C1: continue()
  C1->>C2: navigate /checkout-step-two.html
  T->>C2: read subtotal + tax + total
  T->>C2: finish()
  C2->>OK: navigate /checkout-complete.html
  T->>OK: expect headerText contains "Thank you"
              

3. CI sharding flow

Four parallel jobs each take one quarter of the suite, merge-reports stitches them back, deploy-pages publishes a single HTML.

graph LR
  T[push to ttacart
pull_request] --> S1[Shard 1/4] T --> S2[Shard 2/4] T --> S3[Shard 3/4] T --> S4[Shard 4/4] S1 -. report .-> M[merge-reports
playwright merge-reports] S2 -. report .-> M S3 -. report .-> M S4 -. report .-> M M --> D[deploy-pages -> github.io/tta-report] classDef trig fill:#a5d8ff,stroke:#2563eb,color:#111 classDef shard fill:#fde7f3,stroke:#db2777,color:#111 classDef merge fill:#ffd8a8,stroke:#f59e0b,color:#111 classDef ship fill:#c3fae8,stroke:#22c55e,color:#111 class T trig class S1,S2,S3,S4 shard class M merge class D ship

Live screenshots of every TTACart page

Captured by scripts/capture-screenshots.ts against the live site. Re-run after any layout change to refresh.

TTACart login page
Login/ttacart/index.html
TTACart inventory page (standard_user)
Inventory/ttacart/inventory.html
TTACart item detail page
Item detail/ttacart/inventory-item.html
TTACart cart page with 2 items
Cart/ttacart/cart.html
TTACart checkout step one - guest info
Checkout - guest info/ttacart/checkout-step-one.html
TTACart checkout step two - overview totals
Checkout - overview/ttacart/checkout-step-two.html
TTACart thank-you page
Checkout complete/ttacart/checkout-complete.html

1. Why a framework?

A flat folder of .spec.ts files works for ten tests. It breaks at fifty.

When tests grow, the same locator gets re-typed in twelve places. The login flow gets re-implemented for every spec. Hard-coded URLs leak into the code. CSVs of users get pasted inline. The CI takes 40 minutes because every spec opens its own browser.

A framework is the discipline that stops this. It gives you one place for locators (pages/), one place for reusable flows (modules/), one place for data (testdata/), one place for environment config (.env + config/), one place for utilities (utils/), and one place for custom reporters and fixtures. Add Docker so it runs the same everywhere, then shard it across four GitHub Actions runners and you have a 40-minute suite in 10 minutes.

What you build by the end of this page: a working TypeScript Playwright repo with Page Object Model, reusable modules, fixtures, JSON test data, environment-aware config, a custom HTML reporter, a Dockerfile, and a sharded GitHub Actions pipeline - all pointing at the TTA practice pages so you never need a third-party app.

2. Tech stack

Read directly from package.json + playwright.config.ts of the upstream repo.

Playwright Test docs

Test runner + browser automation. Bundled test(), expect(), fixtures, parallel workers, sharding, traces, retries.

TypeScript docs

Strict types catch typos in locator names and bad data fixtures at compile-time. Path aliases like @pages/* keep imports clean.

Page Object Model docs

One file per page. Locators as arrow functions, actions as instance methods, assertions as expectations. Plus a higher-level modules/ layer for multi-page flows.

dotenv npm

Loads .env into process.env. Switch between dev / staging / prod by exporting NODE_ENV at the shell.

JSON fixtures (testdata/)

Test data as typed JSON. users.json + types.ts give you autocomplete on validUsers[0].username.

Custom TTA Reporter

A standalone Reporter implementation. Emits a branded HTML report into tta-report/ with screenshots, steps, and per-file stats.

Built-in HTML + JSON reporters

Playwright's own HTML report (npx playwright show-report) and a results.json file for CI dashboards.

ESLint + Prettier

npm run lint + npm run format. Husky + lint-staged run them on every commit.

Docker image

Base image mcr.microsoft.com/playwright:v1.40.0-jammy. Same browsers everywhere - local, CI, your colleague's laptop.

GitHub Actions docs

Matrix shard 1/4 - 4/4 across four ubuntu runners. merge-reports stitches the four HTML outputs back into one.

3. Folder structure (TTACart branch)

Run tree -L 3 -I node_modules on the ttacart branch and you get this. Every file lines up with one box in the diagram above and one step in section 4.

Advance-Playwright-Framework/    # branch: ttacart
.
|-- .env.example                      # TTA_ENV switch (qa / stg / dev / prod)
|-- .github/
|   `-- workflows/
|       `-- ttacart.yml               # sharded 4-way CI + merge-reports
|-- Dockerfile                        # mcr.microsoft.com/playwright + npm ci
|-- package.json                      # scripts: test / test:smoke / test:e2e / report
|-- playwright.config.ts              # env-switched baseURL + 4 projects + reporters
|-- tsconfig.json                     # strict + path aliases (@pages/*, @utils/*)
|-- README-TTACART.md                 # quick-start + test-user table
|-- src/
|   |-- config/
|   |   `-- envs.ts                   # BASES map: qa/stg/dev/prod -> URL
|   |-- fixtures/
|   |   `-- test-base.ts              # test = base.extend<Fix> with one fixture per POM
|   |-- pages/                        # one file per TTACart page
|   |   |-- BasePage.ts
|   |   |-- LoginPage.ts              # /ttacart/index.html
|   |   |-- InventoryPage.ts          # /ttacart/inventory.html
|   |   |-- ItemDetailPage.ts         # /ttacart/inventory-item.html?id=...
|   |   |-- CartPage.ts               # /ttacart/cart.html
|   |   |-- CheckoutStepOnePage.ts    # /ttacart/checkout-step-one.html
|   |   |-- CheckoutStepTwoPage.ts    # /ttacart/checkout-step-two.html
|   |   `-- CheckoutCompletePage.ts   # /ttacart/checkout-complete.html
|   |-- testdata/
|   |   |-- users.json                # 6 TTACart test users (standard / locked / problem / glitch / error / visual)
|   |   |-- products.csv              # 6 TTA-branded products + prices
|   |   `-- types.ts                  # RegData, LoginData, ProductRow type aliases
|   |-- tests/                        # spec files - tagged @smoke @regression @e2e @visual @flaky
|   |   |-- login.spec.ts             # 6 cases - one per test user
|   |   |-- inventory.spec.ts         # sort + add-to-cart + badge
|   |   |-- cart.spec.ts              # add / remove / continue / checkout button
|   |   |-- checkout.spec.ts          # full e2e - login to thank-you
|   |   |-- negative.spec.ts          # invalid inputs + problem_user quirk
|   |   `-- data-driven.spec.ts       # CSV-fed add-to-cart for all 6 products
|   `-- utils/
|       |-- UtilElementLocator.ts     # Flex = string | Locator wrapper
|       |-- Logger.ts                 # Winston - file + console
|       |-- DataFactory.ts            # Faker user / card / zip
|       |-- FileReader.ts             # readJSON / readCSV / readXLSX
|       |-- DateUtil.ts               # now / offset / formatYMD
|       `-- CustomTTAReporter.ts      # branded HTML reporter -> tta-report/
|-- logs/                             # auto: run-{ISO}.log (Winston)
|-- test-results/                     # auto: traces / videos / screenshots (gitignored)
|-- playwright-report/                # auto: built-in HTML report (gitignored)
`-- tta-report/                       # auto: custom branded HTML report

What changed vs the original main branch: the seven page objects + six spec files map 1-to-1 to the seven static HTML pages under /playwright/ttacart/. The modules/ folder is folded into focused multi-page flows inside checkout.spec.ts; api/ is omitted because TTACart has no real backend (state is localStorage only). The CI workflow is renamed ttacart.yml to make it obvious which branch it belongs to.

4. Build the framework, folder by folder

Eight steps. Each step explains the folder, gives the mkdir / touch commands, and shows one representative file in full.

Step 1 - package.json + tsconfig.json + playwright.config.ts

Spin up the repo. npm init -y, install Playwright + TypeScript + dotenv, then scaffold the three root config files. playwright.config.ts is the heart - it defines the test directory, projects (browsers), reporters, retries, workers, and the default baseURL.

commands
mkdir advance-playwright-framework && cd advance-playwright-framework
npm init -y
npm i -D @playwright/test @types/node typescript dotenv
npx playwright install --with-deps
touch playwright.config.ts tsconfig.json .env .gitignore
mkdir -p src/{api,config,fixtures,modules,pages,testdata,tests,utils}
package.json
{
  "name": "advance-playwright-framework",
  "version": "1.0.0",
  "description": "TTA Advance Playwright Framework - TypeScript + POM + fixtures",
  "scripts": {
    "test":           "playwright test",
    "test:headed":    "playwright test --headed",
    "test:ui":        "playwright test --ui",
    "test:debug":     "playwright test --debug",
    "test:chromium":  "playwright test --project=chromium",
    "test:firefox":   "playwright test --project=firefox",
    "test:webkit":    "playwright test --project=webkit",
    "test:smoke":     "playwright test --grep @Smoke",
    "test:regression": "playwright test --grep @Regression",
    "test:p0":        "playwright test --grep @P0",
    "test:report":    "playwright show-report",
    "test:ci":        "playwright test --reporter=list,json",
    "lint":           "eslint src/**/*.ts",
    "lint:fix":       "eslint src/**/*.ts --fix",
    "format":         "prettier --write \"src/**/*.ts\"",
    "build":          "tsc",
    "clean":          "rm -rf dist test-results playwright-report tta-report"
  },
  "keywords": ["playwright", "typescript", "testing", "automation", "e2e"],
  "author": "The Testing Academy",
  "license": "ISC",
  "type": "commonjs",
  "devDependencies": {
    "@playwright/test": "^1.57.0",
    "@types/node":     "^25.0.6",
    "dotenv":          "^17.2.3",
    "typescript":      "^5.9.3"
  }
}
tsconfig.json
{
  "compilerOptions": {
    "target": "ES2020",
    "lib": ["ES2020", "DOM"],
    "module": "commonjs",
    "moduleResolution": "node",
    "strict": true,
    "esModuleInterop": true,
    "skipLibCheck": true,
    "resolveJsonModule": true,
    "outDir": "./dist",
    "baseUrl": ".",
    "paths": {
      "@pages/*":    ["src/pages/*"],
      "@modules/*":  ["src/modules/*"],
      "@utils/*":    ["src/utils/*"],
      "@fixtures/*": ["src/fixtures/*"],
      "@api/*":      ["src/api/*"],
      "@config/*":   ["src/config/*"],
      "@testdata/*": ["src/testdata/*"]
    }
  },
  "include": ["**/*.ts"],
  "exclude": ["node_modules", "dist"]
}
playwright.config.ts
import { defineConfig, devices } from '@playwright/test';
import * as dotenv from 'dotenv';

dotenv.config();

export default defineConfig({
  testDir: './src/tests',
  timeout: 60000,
  expect: { timeout: 10000 },
  fullyParallel: true,
  forbidOnly: !!process.env.CI,
  retries: process.env.CI ? 2 : 0,
  workers:  process.env.CI ? 2 : 3,

  reporter: [
    ['./src/utils/CustomTTAReporter.ts'],
    ['html', { open: 'never' }],
    ['json', { outputFile: 'test-results/results.json' }],
    ['list'],
  ],

  use: {
    baseURL:    process.env.BASE_URL || 'https://app.thetestingacademy.com',
    screenshot: 'only-on-failure',
    video:      'retain-on-failure',
    trace:      'retain-on-failure',
  },

  projects: [
    { name: 'chromium',      use: { ...devices['Desktop Chrome'] } },
    { name: 'firefox',       use: { ...devices['Desktop Firefox'] } },
    { name: 'webkit',        use: { ...devices['Desktop Safari'] } },
    { name: 'mobile-chrome', use: { ...devices['Pixel 5'] } },
  ],
});
Why three reporters? html is the built-in one for local debugging, json feeds CI dashboards like Currents/Allure-TestOps, and CustomTTAReporter is the branded student-facing report. list just prints to stdout.

Step 2 - src/pages/ Page Object Model

One file per page. Locators are declared as arrow functions returning a fresh Locator on every call - this avoids the stale-locator problem when SPAs re-mount. Actions and assertions are instance methods. The Page object is injected via the constructor.

src/pages/LoginPage.ts
import { expect, Page } from '@playwright/test';

export class LoginPage {
  private page: Page;

  constructor(page: Page) {
    this.page = page;
  }

  // ============================================
  // LOCATORS (arrow functions return fresh Locator)
  // ============================================
  usernameInput        = () => this.page.locator('#username');
  passwordInput        = () => this.page.locator('#password');
  loginButton          = () => this.page.locator('button[type="submit"]');
  errorMessage         = () => this.page.locator('[data-testid="error-message"]');
  rememberMeCheckbox   = () => this.page.locator('#remember-me');
  forgotPasswordLink   = () => this.page.locator('a:has-text("Forgot Password")');
  signUpLink           = () => this.page.locator('a:has-text("Sign Up")');

  // ----- actions -----
  async navigate() {
    await this.page.goto('/playwright/multiple_element_filter.html');
  }
  async enterUsername(value: string) { await this.usernameInput().fill(value); }
  async enterPassword(value: string) { await this.passwordInput().fill(value); }
  async clickLogin()                  { await this.loginButton().click(); }
  async toggleRememberMe()            { await this.rememberMeCheckbox().click(); }
  async getErrorMessage()             { return (await this.errorMessage().textContent()) || ''; }

  // ----- assertions -----
  async expectOnLoginPage()         { await expect(this.page).toHaveURL(/multiple_element_filter/); }
  async expectUsernameVisible()     { await expect(this.usernameInput()).toBeVisible(); }
  async expectPasswordVisible()     { await expect(this.passwordInput()).toBeVisible(); }
  async expectErrorVisible()        { await expect(this.errorMessage()).toBeVisible(); }
  async expectLoginButtonEnabled()  { await expect(this.loginButton()).toBeEnabled(); }
}
Why arrow-function locators? A method like usernameInput = () => this.page.locator(...) returns a fresh Locator every time it's called. That means if the page re-renders (e.g. after a React state change), the locator re-queries the DOM. A field like readonly usernameInput = this.page.locator(...) would also work because Playwright locators are lazy, but the arrow style makes the intent explicit.

Step 3 - src/utils/UtilElementLocator.ts + helpers

A thin wrapper that accepts either a string selector or a Locator object, centralises timeouts, and provides familiar action / read / assertion verbs. Drop it next to Logger.ts, WaitHelper.ts, DataGenerator.ts, ApiHelper.ts, and CustomTTAReporter.ts.

src/utils/UtilElementLocator.ts
import { Page, Locator } from '@playwright/test';

type Flex = string | Locator;

export class UtilElementLocator {
  private page: Page;
  private defaultTimeOut: number;

  constructor(page: Page, timeOut: number = 30000) {
    this.page = page;
    this.defaultTimeOut = timeOut;
  }

  private getLocator(locator: Flex): Locator {
    return typeof locator === 'string' ? this.page.locator(locator) : locator;
  }

  // actions
  async click(loc: Flex) { await this.getLocator(loc).click({ timeout: this.defaultTimeOut }); }
  async fill(loc: Flex, text: string) { await this.getLocator(loc).fill(text, { timeout: this.defaultTimeOut }); }
  async type(loc: Flex, text: string, delay = 50) {
    await this.getLocator(loc).pressSequentially(text, { delay, timeout: this.defaultTimeOut });
  }
  async clear(loc: Flex) { await this.getLocator(loc).clear({ timeout: this.defaultTimeOut }); }
  async selectByValue(loc: Flex, value: string) {
    await this.getLocator(loc).selectOption({ value }, { timeout: this.defaultTimeOut });
  }

  // reads
  async getText(loc: Flex)  { return (await this.getLocator(loc).innerText()).trim(); }
  async getValue(loc: Flex) { return await this.getLocator(loc).inputValue(); }
  async getAttr(loc: Flex, name: string) { return await this.getLocator(loc).getAttribute(name); }

  // waits
  async waitForVisible(loc: Flex, timeout = 5000) {
    try { await this.getLocator(loc).waitFor({ state: 'visible', timeout }); return true; }
    catch { return false; }
  }
  async waitForPageLoad(state: 'load' | 'domcontentloaded' | 'networkidle' = 'load') {
    await this.page.waitForLoadState(state);
  }
}
src/utils/Logger.ts
export enum LogLevel { DEBUG = 'DEBUG', INFO = 'INFO', WARN = 'WARN', ERROR = 'ERROR' }

export class Logger {
  private static logLevel: LogLevel = LogLevel.INFO;

  constructor(private context: string) {}

  static create(context: string) { return new Logger(context); }
  static setLogLevel(level: LogLevel) { Logger.logLevel = level; }

  private fmt(level: LogLevel, message: string) {
    return `[${new Date().toISOString()}] [${level}] [${this.context}] ${message}`;
  }

  info(m: string)  { console.info(this.fmt(LogLevel.INFO, m)); }
  warn(m: string)  { console.warn(this.fmt(LogLevel.WARN, m)); }
  error(m: string, e?: unknown) { console.error(this.fmt(LogLevel.ERROR, m), e || ''); }
  step(n: number, desc: string) { this.info(`Step ${n}: ${desc}`); }
}

Step 4 - src/testdata/ JSON fixtures + types

Test data lives next to the tests, version-controlled, typed. users.json holds valid users, invalid users with expected error messages, a locked-out user, and a new-user template. types.ts turns it into autocomplete-friendly interfaces.

src/testdata/users.json
{
  "validUsers": [
    {
      "id": "user-001",
      "username": "[email protected]",
      "password": "SecurePass123",
      "firstName": "Test",
      "lastName": "User",
      "role": "customer"
    },
    {
      "id": "user-002",
      "username": "[email protected]",
      "password": "AdminPass456",
      "firstName": "Admin",
      "lastName": "User",
      "role": "admin"
    },
    {
      "id": "user-003",
      "username": "[email protected]",
      "password": "PremiumPass789",
      "firstName": "Premium",
      "lastName": "Member",
      "role": "premium"
    }
  ],
  "invalidUsers": [
    { "username": "[email protected]",     "password": "wrongpassword", "expectedError": "Invalid credentials" },
    { "username": "[email protected]", "password": "anypassword",   "expectedError": "User not found" },
    { "username": "",                       "password": "somepassword",  "expectedError": "Username is required" },
    { "username": "[email protected]",        "password": "",               "expectedError": "Password is required" }
  ],
  "newUserTemplate": {
    "username": "newuser_{timestamp}@tta.dev",
    "password": "NewUserPass123",
    "firstName": "New",
    "lastName": "User"
  },
  "lockedUser": {
    "username": "[email protected]",
    "password": "LockedPass123",
    "expectedError": "Account is locked"
  }
}
src/testdata/types.ts
export interface ValidUser {
  id: string;
  username: string;
  password: string;
  firstName: string;
  lastName: string;
  role: string;
}

export interface InvalidUser {
  username: string;
  password: string;
  expectedError: string;
}

export interface UsersData {
  validUsers: ValidUser[];
  invalidUsers: InvalidUser[];
  newUserTemplate: { username: string; password: string; firstName: string; lastName: string };
  lockedUser: { username: string; password: string; expectedError: string };
}

Step 5 - src/modules/ reusable multi-page flows

Page objects know about one page. Modules combine multiple page objects into a business flow - "login", "buy a product", "complete checkout". Specs call modules, not raw page objects, so the test reads like a story.

src/modules/LoginModule.ts
import { Page } from '@playwright/test';
import { LoginPage } from '../pages/LoginPage';
import { HomePage } from '../pages/HomePage';
import { Logger } from '../utils/Logger';

export class LoginModule {
  private page: Page;
  private loginPage: LoginPage;
  private homePage: HomePage;
  private log: Logger;

  constructor(page: Page) {
    this.page = page;
    this.loginPage = new LoginPage(page);
    this.homePage  = new HomePage(page);
    this.log = Logger.create('LoginModule');
  }

  async doLogin(username: string, password: string): Promise<boolean> {
    this.log.step(1, 'Navigate to login page');
    await this.loginPage.navigate();

    this.log.step(2, 'Enter username');
    await this.loginPage.enterUsername(username);

    this.log.step(3, 'Enter password');
    await this.loginPage.enterPassword(password);

    this.log.step(4, 'Click login button');
    await this.loginPage.clickLogin();

    this.log.info('Login successful');
    return true;
  }

  async attemptInvalidLogin(username: string, password: string): Promise<string> {
    await this.loginPage.navigate();
    await this.loginPage.enterUsername(username);
    await this.loginPage.enterPassword(password);
    await this.loginPage.clickLogin();
    await this.loginPage.expectErrorVisible();
    return await this.loginPage.getErrorMessage();
  }
}

Step 6 - src/fixtures/ custom Playwright fixtures

Extend the base test() with your page-objects and modules. After this, every spec gets { loginPage, loginModule, authenticatedPage } injected directly - no manual new LoginPage(page) in beforeEach.

src/fixtures/index.ts
import { test as base, Page } from '@playwright/test';
import { LoginPage } from '../pages/LoginPage';
import { HomePage } from '../pages/HomePage';
import { LoginModule } from '../modules/LoginModule';
import { config } from '../config';

export type TestFixtures = {
  loginPage: LoginPage;
  homePage: HomePage;
  loginModule: LoginModule;
  authenticatedPage: Page;
};

export const test = base.extend<TestFixtures>({
  loginPage:   async ({ page }, use) => { await use(new LoginPage(page)); },
  homePage:    async ({ page }, use) => { await use(new HomePage(page)); },
  loginModule: async ({ page }, use) => { await use(new LoginModule(page)); },

  // Pre-authenticated page - perform login once per test that requests it
  authenticatedPage: async ({ browser }, use) => {
    const context = await browser.newContext();
    const page = await context.newPage();

    const loginPage = new LoginPage(page);
    await loginPage.navigate();
    await loginPage.enterUsername(config.testUser.username);
    await loginPage.enterPassword(config.testUser.password);
    await loginPage.clickLogin();

    await use(page);
    await context.close();
  },
});

export { expect } from '@playwright/test';

Step 7 - .env + src/config/ environment layering

The .env file holds non-secret defaults. src/config/index.ts reads them via dotenv and exposes a typed config object. getEnvironmentConfig() layers staging / prod overrides on top.

.env
# TTA practice host
BASE_URL=https://app.thetestingacademy.com
API_BASE_URL=https://app.thetestingacademy.com/api

# Default test credentials
[email protected]
TEST_PASSWORD=SecurePass123

# Timeouts
API_TIMEOUT=30000
DEFAULT_TIMEOUT=30000

# Logging
LOG_LEVEL=INFO

# Retry
RETRY_COUNT=3
src/config/index.ts
import * as dotenv from 'dotenv';
dotenv.config();

export interface AppConfig {
  baseUrl: string;
  apiBaseUrl: string;
  apiTimeout: number;
  testUser: { username: string; password: string };
  logLevel: string;
  retryCount: number;
}

export const config: AppConfig = {
  baseUrl:     process.env.BASE_URL || 'https://app.thetestingacademy.com',
  apiBaseUrl:  process.env.API_BASE_URL || 'https://app.thetestingacademy.com/api',
  apiTimeout:  parseInt(process.env.API_TIMEOUT || '30000', 10),
  testUser: {
    username: process.env.TEST_USERNAME || '[email protected]',
    password: process.env.TEST_PASSWORD || 'SecurePass123',
  },
  logLevel:   process.env.LOG_LEVEL || 'INFO',
  retryCount: parseInt(process.env.RETRY_COUNT || '3', 10),
};

export function getEnvironmentConfig(env?: string): Partial<AppConfig> {
  const environment = env || process.env.NODE_ENV || 'development';

  switch (environment) {
    case 'production':
      return { baseUrl: 'https://app.thetestingacademy.com', retryCount: 5 };
    case 'staging':
      return { baseUrl: 'https://staging.thetestingacademy.com', retryCount: 3 };
    case 'development':
    default:
      return { baseUrl: 'https://app.thetestingacademy.com', retryCount: 2 };
  }
}

export default config;

Step 8 - src/utils/CustomTTAReporter.ts + tta-report/

Implements Playwright's Reporter interface. Captures per-test steps, screenshots, retries, and tags, then writes a branded HTML page into tta-report/index.html. The reporter is registered at the top of playwright.config.ts.

src/utils/CustomTTAReporter.ts (signature)
import {
  FullConfig, FullResult, Reporter,
  Suite, TestCase, TestResult, TestStep,
} from '@playwright/test/reporter';
import * as fs from 'fs';

interface StepData {
  title: string; duration: number;
  status: 'passed' | 'failed' | 'skipped';
  screenshot?: string; error?: string;
}

class CustomTTAReporter implements Reporter {
  private outputFile = 'tta-report/index.html';
  private stats = { total: 0, passed: 0, failed: 0, skipped: 0 };

  onBegin(config: FullConfig, suite: Suite) {
    this.stats.total = suite.allTests().length;
  }

  onTestEnd(test: TestCase, result: TestResult) {
    if (result.status === 'passed')  this.stats.passed++;
    if (result.status === 'failed')  this.stats.failed++;
    if (result.status === 'skipped') this.stats.skipped++;
  }

  async onEnd(result: FullResult) {
    fs.mkdirSync('tta-report', { recursive: true });
    const html = `<!doctype html><html><head><title>TTA Report</title></head>
<body><h1>TTA Playwright Report</h1>
<p>Total: ${this.stats.total} | Pass: ${this.stats.passed} | Fail: ${this.stats.failed}</p>
</body></html>`;
    fs.writeFileSync(this.outputFile, html);
  }
}

export default CustomTTAReporter;
Production version handles per-test screenshots, per-step duration timelines, video clip start/end, console logs, file-group stats, and dark-mode CSS. The signature above is enough to get a working report; expand onEnd to emit a full template literal.

Step 9 - src/tests/ spec files (tagged + sharded-ready)

Specs import test from ../fixtures, not @playwright/test directly. Title-prefix tags like @P0, @Smoke, @Regression let you grep-filter at run-time: npx playwright test --grep @P0.

src/tests/login.spec.ts
import { test, expect } from '../fixtures';
import usersData from '../testdata/users.json';
import { UsersData, InvalidUser } from '../testdata/types';

const typed = usersData as UsersData;
const validUser = typed.validUsers[0];
const invalidUsers: InvalidUser[] = typed.invalidUsers;

test.describe('@P1 @Regression @Login Login Feature', () => {

  test.describe('@P0 @Smoke Valid Login', () => {
    test('should login with valid credentials', async ({ loginModule, page }) => {
      await test.step('login + verify URL', async () => {
        await loginModule.doLogin(validUser.username, validUser.password);
        expect(page.url()).toContain('multiple_element_filter');
      });
    });
  });

  test.describe('@P1 @Regression Invalid Login', () => {
    test('shows error with bad credentials', async ({ loginModule }) => {
      const [u] = invalidUsers;
      const error = await loginModule.attemptInvalidLogin(u.username, u.password);
      expect(error).toContain(u.expectedError);
    });
  });

  test.describe('@P2 Login Page Elements', () => {
    test('login form is fully rendered', async ({ loginPage }) => {
      await loginPage.navigate();
      await loginPage.expectUsernameVisible();
      await loginPage.expectPasswordVisible();
      await loginPage.expectLoginButtonEnabled();
    });
  });
});

test.describe('@P0 @Smoke Login - via fixture', () => {
  test('authenticated page is ready to use', async ({ authenticatedPage }) => {
    await expect(authenticatedPage).toHaveURL(/multiple_element_filter/);
  });
});

5. Worked scenario - TTACart end-to-end checkout

One spec that ties every box of the diagram together: log in as standard_user, add 2 items, walk through both checkout steps, finish, then assert the thank-you screen. Targets the live TTACart pages.

  • Login page: https://app.thetestingacademy.com/playwright/ttacart/index.html
  • Inventory: /playwright/ttacart/inventory.html
  • Cart: /playwright/ttacart/cart.html
  • Checkout step 1: /playwright/ttacart/checkout-step-one.html (guest info)
  • Checkout step 2: /playwright/ttacart/checkout-step-two.html (totals)
  • Complete: /playwright/ttacart/checkout-complete.html

Test users live in a JSON fixture. The spec uses the fixture-based test runner from src/fixtures/test-base.ts.

src/testdata/users.json
[
  { "username": "standard_user",           "password": "tta_secret", "kind": "ok" },
  { "username": "locked_out_user",         "password": "tta_secret", "kind": "blocked" },
  { "username": "problem_user",            "password": "tta_secret", "kind": "broken-ui" },
  { "username": "performance_glitch_user", "password": "tta_secret", "kind": "slow" },
  { "username": "error_user",              "password": "tta_secret", "kind": "flaky" },
  { "username": "visual_user",             "password": "tta_secret", "kind": "visual" }
]
src/pages/LoginPage.ts
import { expect, Locator, Page } from '@playwright/test';
import { UtilElementLocator } from '../utils/UtilElementLocator';

export class LoginPage {
  private readonly u: UtilElementLocator;
  readonly username: Locator;
  readonly password: Locator;
  readonly loginBtn: Locator;
  readonly errorBanner: Locator;

  constructor(private page: Page) {
    this.u = new UtilElementLocator(page);
    this.username    = page.getByTestId('username');
    this.password    = page.getByTestId('password');
    this.loginBtn    = page.getByTestId('login-button');
    this.errorBanner = page.getByTestId('error');
  }

  async open() {
    await this.page.goto('/playwright/ttacart/index.html');
  }

  async loginAs(username: string, password: string): Promise<void> {
    await this.u.fill(this.username, username);
    await this.u.fill(this.password, password);
    await this.u.click(this.loginBtn);
  }

  async expectErrorContains(text: string): Promise<void> {
    await expect(this.errorBanner).toContainText(text);
  }
}
src/pages/InventoryPage.ts (excerpt)
async addToCart(productId: string): Promise<void> {
  await this.page.getByTestId(`add-to-cart-${productId}`).click();
}

async cartCount(): Promise<number> {
  const badge = this.page.getByTestId('shopping-cart-badge');
  if (!(await badge.isVisible())) return 0;
  return Number(await badge.innerText());
}

async openCart(): Promise<void> {
  await this.page.getByTestId('shopping-cart-link').click();
}
src/tests/checkout.spec.ts
import { test, expect } from '../fixtures/test-base';

test('@e2e standard_user buys two items', async ({
  page, loginPage, inventoryPage, cartPage, checkoutOne, checkoutTwo, completePage,
}) => {

  // 1) login
  await loginPage.open();
  await loginPage.loginAs('standard_user', 'tta_secret');
  await expect(page).toHaveURL(/inventory\.html/);

  // 2) add 2 items, assert cart badge
  await inventoryPage.addToCart('tta-practice-backpack');
  await inventoryPage.addToCart('tta-bike-light');
  expect(await inventoryPage.cartCount()).toBe(2);

  // 3) cart -> checkout step 1
  await inventoryPage.openCart();
  await cartPage.assertLoaded();
  await cartPage.checkout();

  // 4) guest details
  await checkoutOne.fillGuest({ firstName: 'Aarav', lastName: 'Sharma', postalCode: '560001' });
  await checkoutOne.continue();

  // 5) overview totals math
  const subtotal = await checkoutTwo.subtotal();   // 29.99 + 9.99
  const tax      = await checkoutTwo.tax();        // 8% of subtotal
  const total    = await checkoutTwo.total();      // subtotal + tax
  expect(total).toBeCloseTo(subtotal + tax, 2);

  // 6) finish + assert thank-you
  await checkoutTwo.finish();
  await expect(page).toHaveURL(/checkout-complete\.html/);
  await expect(completePage.headerText()).resolves.toContain('Thank you');
});
src/tests/negative.spec.ts (one case)
test('@regression locked_out_user sees the locked-out error', async ({ loginPage }) => {
  await loginPage.open();
  await loginPage.loginAs('locked_out_user', 'tta_secret');
  await loginPage.expectErrorContains('locked out');
});

Full source lives on the ttacart branch - look under src/tests/ for the rest (login.spec.ts, inventory.spec.ts, cart.spec.ts, data-driven.spec.ts).

6. Run it

Eight commands from clone to coloured HTML report.

$ git clone -b ttacart https://github.com/PramodDutta/Advance-Playwright-Framework.git
$ cd Advance-Playwright-Framework
$ npm ci
$ npx playwright install --with-deps
$ TTA_ENV=qa npx playwright test # full TTACart suite
$ npx playwright test --grep @smoke # only smoke tagged
$ npx playwright test --project=chromium --headed # watch it run
$ npx playwright show-report # built-in HTML report
$ open tta-report/index.html # branded TTA report

Expected report

After a clean run the HTML report looks roughly like:

TTA Playwright Report advance-playwright-framework - chromium Total 42 Passed 40 Failed 2 login.spec.ts 8 / 8 passed - 4.2s register-login.spec.ts 3 pass - 1 fail - 7.8s

7. CI - GitHub Actions (sharded)

The sharded workflow runs 4 parallel jobs, each doing one quarter of the suite, then a final job stitches the four HTML reports back together with npx playwright merge-reports.

.github/workflows/ttacart.yml
name: TTACart Playwright Tests

on:
  push:        { branches: [ttacart] }
  pull_request: { branches: [main, ttacart] }
  workflow_dispatch:
    inputs:
      test_tag:
        description: 'Tag to run (@smoke / @regression / @e2e)'
        required: false
        default: ''

env:
  CI: true
  NODE_VERSION: '20'

jobs:
  test:
    name: Run Tests
    runs-on: ubuntu-latest
    timeout-minutes: 30
    strategy:
      fail-fast: false
      matrix: { shard: [1, 2, 3, 4] }

    steps:
      - uses: actions/checkout@v4
      - uses: actions/setup-node@v4
        with: { node-version: '${{ env.NODE_VERSION }}', cache: 'npm' }
      - name: Install deps
        run: npm ci
      - name: Install Playwright browsers
        run: npx playwright install --with-deps chromium
      - name: Run shard ${{ matrix.shard }}/4
        run: |
          if [ -n "${{ github.event.inputs.test_tag }}" ]; then
            npx playwright test --shard=${{ matrix.shard }}/4 --grep "${{ github.event.inputs.test_tag }}"
          else
            npx playwright test --shard=${{ matrix.shard }}/4
          fi
      - name: Upload test results
        if: always()
        uses: actions/upload-artifact@v4
        with:
          name: playwright-report-shard-${{ matrix.shard }}
          path: |
            playwright-report/
            test-results/
            tta-report/
          retention-days: 30

  merge-reports:
    needs: test
    runs-on: ubuntu-latest
    if: always()
    steps:
      - uses: actions/checkout@v4
      - uses: actions/setup-node@v4
        with: { node-version: '${{ env.NODE_VERSION }}', cache: 'npm' }
      - run: npm ci
      - uses: actions/download-artifact@v4
        with: { path: all-reports, pattern: playwright-report-shard-* }
      - name: Merge HTML reports
        run: npx playwright merge-reports --reporter html ./all-reports
      - uses: actions/upload-artifact@v4
        with:
          name: playwright-report-merged
          path: playwright-report/
          retention-days: 30
Sharding mental model: --shard=1/4 means "run only the first quarter of the test files this worker would otherwise have run". Playwright distributes files (not individual tests) by hash, so two specs in the same file always end up on the same shard. Total wall-time drops by ~3.5x with 4 shards.

Dockerfile for local + CI parity

Dockerfile
# Official Playwright image with all browsers preinstalled
FROM mcr.microsoft.com/playwright:v1.57.0-jammy

WORKDIR /app
ENV CI=true
ENV NODE_ENV=test
ENV PLAYWRIGHT_BROWSERS_PATH=/ms-playwright

COPY package*.json ./
RUN npm ci

COPY . .
RUN npm run build
RUN mkdir -p /app/test-results /app/playwright-report /app/tta-report

CMD ["npx", "playwright", "test"]

# Build:  docker build -t tta-playwright .
# Run:    docker run --rm -v $(pwd)/test-results:/app/test-results tta-playwright
# Shard:  docker run --rm tta-playwright npx playwright test --shard=1/4

8. Where to go next

Each of these has a dedicated TTA practice page or concept reference - click through and keep building.

  • Sharding + parallel CI - already shown above; tune workers + matrix.shard for your runner count.
  • Visual regression - await expect(page).toHaveScreenshot('home.png'). Pair with --update-snapshots on a green main.
  • Hybrid API + UI - use APIRequestContext to seed data via /api/auth/register, then drive the UI. See Network interception.
  • Custom fixtures - explored above. Next steps: dependent fixtures, worker-scoped fixtures, and the { test, expect } re-export pattern.
  • Global setup + teardown - globalSetup in playwright.config.ts for one-time database seeding before all tests.
  • BasePage - extract common methods (navigate-with-retry, wait-for-toast) into pages/BasePage.ts, then have LoginPage extends BasePage.
  • Allure history + categories - swap or augment the HTML reporter with allure-playwright for trend graphs and category buckets.
  • Faker + data factories - read Data-driven + POM for Faker / CSV / JSON / XLSX patterns.
  • Test modifiers + hooks - read Test modifiers, hooks, data for test.skip / slow / fixme / fail, test.step, describe.serial.
  • Assertions cookbook - Assertions (expect) covers every locator-level expectation.