⚙️ Playwright · Free cheat sheet

Playwright Runner Gearbox

Nine gears that control how your suite runs — ingredients, sessions, speed, reports and CI

Playwright + TypeScript · Runner, Config & CI · Intermediate

Why the Runner Matters

GearWhat it controlsWhy AWU learners care
1 · Spec shapedescribe, hooks, annotationsReadable, skippable, focusable tests
2 · IngredientsBuilt-in and custom fixturesNo copy-pasted setup code
3 · SessionsProjects + saved sign-in stateLog in once for the whole run
4 · SpeedWorkers, parallel mode, retries, timeoutsFast but stable pipelines
5 · ReportsBuilt-in and plugin reportersRight report for devs, leads and CI
6 · SelectionTags, grep, data-driven loopsRun exactly what you need
7 · Scale-outSharding + GitHub ActionsBig suites on many machines
8 · TemplateOne reference configCopy into every new project
9 · CommandsCLI + debugging toolsFind failures quickly

Gear 1 — Shape of a Spec File

specs/enrolment.spec.ts
import { test, expect } from '@playwright/test';
 
test.describe('Enrolment', () => {
  test.beforeAll(async () => { /* once per worker for this group */ });
 
  test.beforeEach(async ({ page }) => {
    await page.goto('/batches');
  });
 
  test('learner can reserve a seat', { tag: '@sanity' }, async ({ page }) => {
    await page.getByRole('link', { name: 'Playwright with TypeScript' }).click();
    await page.getByRole('button', { name: 'Reserve seat' }).click();
    await expect(page.getByRole('status')).toHaveText('Seat reserved');
  });
 
  test.afterEach(async ({ page }, testInfo) => {
    if (testInfo.status !== testInfo.expectedStatus) {
      await testInfo.attach('failure-view', {
        body: await page.screenshot(), contentType: 'image/png',
      });
    }
  });
});
AnnotationWhat the runner doesWhen AWU uses it
test.describe.configure({ mode: 'parallel' })Runs tests of one file side by sideBig file of independent checks
test.describe.serial()Keeps order; stops the group at the first failureSteps that truly depend on each other
test.slow()Gives the test 3× its timeoutCertificate PDF generation
test.fail()Passes only if the test failsPinning a known defect until it is fixed
test.fixme()Skips and marks as needing repairTicket raised, fix pending
test.skip(condition, reason)Skips when the condition is trueFeature disabled on WebKit
test.only()Runs just this testLocal debugging — never commit
Watch Out

Add forbidOnly: !!process.env.CI in the config so a stray test.only fails the CI build instead of silently skipping everything else.

Gear 2 — Fixtures: Ready-Made Test Ingredients

2.1 Built-in fixtures

Fixture nameLives forHands you
requestOne testAn API client (APIRequestContext) — no browser needed
pageOne testA brand-new tab in a brand-new context
contextOne testA private browser profile with its own cookies and storage
browserNameOne projectWhich engine is running: chromium, firefox or webkit
browserOne workerThe browser process shared by that worker

2.2 Your own fixtures for screen objects

kit/awu-test.ts
// kit/awu-test.ts
import { test as base } from '@playwright/test';
import { SignInScreen } from '../screens/SignInScreen';
import { LearnerHomeScreen } from '../screens/LearnerHomeScreen';
 
type AwuKit = {
  signInScreen: SignInScreen;
  learnerHome: LearnerHomeScreen;     // arrives already signed in
};
 
export const test = base.extend<AwuKit>({
  signInScreen: async ({ page }, use) => {
    await use(new SignInScreen(page));
  },
  learnerHome: async ({ page }, use) => {
    const signIn = new SignInScreen(page);
    await signIn.visit();
    await signIn.enterPortal('priya.k@awu.test', process.env.AWU_SECRET!);
    await use(new LearnerHomeScreen(page));   // the test body runs here
    // code after use() = cleanup
  },
});
export { expect } from '@playwright/test';
specs/learner-home.spec.ts
import { test, expect } from '../kit/awu-test';
 
test('learner home lists enrolled batches', async ({ learnerHome }) => {
  await expect(learnerHome.batchTiles).toHaveCount(2);
});

Gear 3 — Projects & the Sign-In-Once Trick

A project is a named bundle of settings. Use projects for browsers, devices and stages — and for one session project that signs in once and saves the cookies for everyone else.

specs/session.setup.ts
// specs/session.setup.ts
import { test as setup, expect } from '@playwright/test';
const stateFile = '.awu-session/learner-state.json';
 
setup('sign in once and keep the cookies', async ({ page }) => {
  await page.goto('/sign-in');
  await page.getByLabel('Learner email').fill('priya.k@awu.test');
  await page.getByLabel('Account password').fill(process.env.AWU_SECRET!);
  await page.getByRole('button', { name: 'Continue to portal' }).click();
  await expect(page).toHaveURL(/learner-home/);
  await page.context().storageState({ path: stateFile }); // cookies + localStorage
});
playwright.config.ts (projects)
projects: [
  { name: 'awu-session', testMatch: /.*\.setup\.ts/ },
  { name: 'desktop-chrome', dependencies: ['awu-session'],
    use: { ...devices['Desktop Chrome'], storageState: '.awu-session/learner-state.json' } },
  { name: 'desktop-edge', dependencies: ['awu-session'],
    use: { ...devices['Desktop Edge'], channel: 'msedge', storageState: '.awu-session/learner-state.json' } },
  { name: 'android-pixel', dependencies: ['awu-session'],
    use: { ...devices['Pixel 7'], storageState: '.awu-session/learner-state.json' } },
],
Key Takeaway

The session project runs first → saves storageState → every other project starts already signed in. Add .awu-session/ to .gitignore — it holds live session cookies.

Gear 4 — Speed Controls

KnobWhat it controlsOut-of-box value
timeoutTime allowed for one test30 seconds
expect.timeoutHow long a web-first assertion keeps retrying5 seconds
globalTimeoutHard ceiling for the entire runUnlimited (0)
workersNumber of parallel OS processesHalf the CPU cores
fullyParallelAlso split tests inside a single filefalse — files parallel, tests in a file in order
retriesExtra attempts for a failed test0
maxFailuresAbort the run after this many failuresUnlimited (0)
CI-aware settings
export default defineConfig({
  fullyParallel: true,
  workers: process.env.CI ? 3 : undefined,
  retries: process.env.CI ? 1 : 0,
  maxFailures: process.env.CI ? 10 : 0,
});

Gear 5 — Reports for Every Audience

ReporterHow to switch it onWho reads it
html['html', { open: 'never', outputFolder: 'awu-reports/html' }]You and developers — traces, screenshots, steps
allure'allure-playwright' (npm package)Leads — trends and history dashboards
junit['junit', { outputFile: 'awu-reports/junit-results.xml' }]Jenkins / GitLab test tabs
json['json', { outputFile: 'awu-reports/run.json' }]Scripts — Slack/Teams bots, custom dashboards
blob'blob'Sharded runs that will be merged later
list'list'Terminal — default for local runs
line / dot'line' · 'dot'Compact CI console output
several reporters together
reporter: [
  ['line'],
  ['html', { open: 'never', outputFolder: 'awu-reports/html' }],
  ['junit', { outputFile: 'awu-reports/junit-results.xml' }],
  ['allure-playwright'],
],

Gear 6 — Picking Which Tests Run

tagging
test('pay batch fee via UPI', { tag: ['@payments', '@nightly'] }, async ({ page }) => { /* ... */ });
test.describe('Wishlist', { tag: '@sanity' }, () => { /* every test inside inherits @sanity */ });
CommandRuns
npx playwright test --grep @sanityOnly @sanity
npx playwright test --grep-invert @nightlyEverything except @nightly
npx playwright test --grep "@sanity|@payments"Either tag (regex OR)
npx playwright test specs/wishlist/One folder
npx playwright test wishlist.spec.ts:18The test starting at line 18
one spec, three data rows
const plans = [
  { plan: 'self-paced', fee: '₹2,499', liveDoubts: false },
  { plan: 'live-batch', fee: '₹4,999', liveDoubts: true },
  { plan: 'mentorship', fee: '₹9,999', liveDoubts: true },
];
 
for (const p of plans) {
  test(`pricing card for ${p.plan}`, async ({ page }) => {
    await page.goto('/pricing');
    const card = page.getByTestId(`plan-${p.plan}`);
    await expect(card).toContainText(p.fee);
    await expect(card.getByText('Live doubt sessions')).toBeVisible({ visible: p.liveDoubts });
  });
}
Watch Out

Each generated test needs a unique title — that is why the plan name goes into it.

Gear 7 — Splitting Runs Across Machines

Sharding gives each machine a slice of the suite. Every slice writes a blob report; one final step merges them into a single HTML report.

terminal
npx playwright test --shard=1/3     # runner 1
npx playwright test --shard=2/3     # runner 2
npx playwright test --shard=3/3     # runner 3
npx playwright merge-reports --reporter html ./collected-blobs
.github/workflows/awu-regression.yml
name: awu-regression
on:
  push: { branches: [main] }
  workflow_dispatch:
jobs:
  e2e:
    runs-on: ubuntu-latest
    strategy:
      fail-fast: false
      matrix:
        shardIndex: [1, 2, 3]
        shardTotal: [3]
    steps:
      - uses: actions/checkout@v4
      - uses: actions/setup-node@v4
        with: { node-version: 22 }
      - run: npm ci
      - run: npx playwright install --with-deps
      - run: npx playwright test --shard=${{ matrix.shardIndex }}/${{ matrix.shardTotal }} --reporter=blob
      - uses: actions/upload-artifact@v4
        if: always()
        with:
          name: awu-blob-${{ matrix.shardIndex }}
          path: blob-report/

Gear 8 — The AWU Config Template

playwright.config.ts
import { defineConfig, devices } from '@playwright/test';
 
export default defineConfig({
  testDir: './specs',
  outputDir: './test-output',
  timeout: 45_000,
  expect: { timeout: 7_000 },
  fullyParallel: true,
  forbidOnly: !!process.env.CI,
  retries: process.env.CI ? 1 : 0,
  workers: process.env.CI ? 3 : undefined,
  reporter: [['line'], ['html', { open: 'never', outputFolder: 'awu-reports/html' }]],
  use: {
    baseURL: process.env.AWU_BASE_URL ?? 'https://qa.awu-portal.test',
    trace: 'retain-on-failure',
    screenshot: 'only-on-failure',
    video: 'on-first-retry',
  },
  projects: [
    { name: 'awu-session', testMatch: /.*\.setup\.ts/ },
    { name: 'desktop-chrome', dependencies: ['awu-session'], use: { ...devices['Desktop Chrome'] } },
  ],
});

Gear 9 — Commands at Your Fingertips

PurposeCommand / flag
Run the whole suite headlessnpx playwright test
Run one project--project=desktop-chrome
Watch the browser--headed
Run one test at a time--workers=1
Time-travel UI with watch mode--ui
Step through in the Inspector--debug
Stop a test at a lineawait page.pause()
Open a saved tracenpx playwright show-trace test-output/…/trace.zip
Hunt flaky tests--repeat-each=5
Override retries for this run--retries=2
Re-run only last failures--last-failed
Accept new screenshot baselines--update-snapshots
Record clicks into codenpx playwright codegen <url>
Open the HTML reportnpx playwright show-report awu-reports/html
Running 31 tests using 3 workers
  31 passed (52.7s)

To open last HTML report run:
  npx playwright show-report awu-reports/html

Interview Questions & Answers

Q1. Library vs @playwright/test?

The library drives browsers; the runner adds fixtures, parallel workers, retries, reporters, projects and config.

Q2. What exactly is a worker?

A separate OS process with its own browser; tests in different workers never share memory.

Q3. What changes when fullyParallel is true?

Tests inside the same file are also distributed across workers.

Q4. How do you sign in only once for the whole run?

A session project saves storageState; other projects list it in dependencies.

Q5. beforeAll or a worker-scoped fixture?

Both run once per worker; fixtures are reusable across files and clean up after use().

Q6. A test fails 1 in 10 runs. What do you do?

--repeat-each, trace on failure, UI Mode, then look for shared data, missing awaits or fixed waits.

Q7. What is sharding?

Splitting one run across machines with --shard=x/n, then merging blob reports.

Q8. Which reporter does Jenkins understand?

JUnit XML.

Q9. What does test.slow() do?

Triples that test's timeout.

Q10. How do you run only the sanity pack?

Tag tests @sanity and run --grep @sanity.

Want to learn this live?

Small batches, real projects, mock interviews included. Sit in on a free demo class first.

See all coursesBook a mock interview