Why the Runner Matters
| Gear | What it controls | Why AWU learners care |
|---|---|---|
| 1 · Spec shape | describe, hooks, annotations | Readable, skippable, focusable tests |
| 2 · Ingredients | Built-in and custom fixtures | No copy-pasted setup code |
| 3 · Sessions | Projects + saved sign-in state | Log in once for the whole run |
| 4 · Speed | Workers, parallel mode, retries, timeouts | Fast but stable pipelines |
| 5 · Reports | Built-in and plugin reporters | Right report for devs, leads and CI |
| 6 · Selection | Tags, grep, data-driven loops | Run exactly what you need |
| 7 · Scale-out | Sharding + GitHub Actions | Big suites on many machines |
| 8 · Template | One reference config | Copy into every new project |
| 9 · Commands | CLI + debugging tools | Find failures quickly |
Gear 1 — Shape of a Spec File
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',
});
}
});
});| Annotation | What the runner does | When AWU uses it |
|---|---|---|
test.describe.configure({ mode: 'parallel' }) | Runs tests of one file side by side | Big file of independent checks |
test.describe.serial() | Keeps order; stops the group at the first failure | Steps that truly depend on each other |
test.slow() | Gives the test 3× its timeout | Certificate PDF generation |
test.fail() | Passes only if the test fails | Pinning a known defect until it is fixed |
test.fixme() | Skips and marks as needing repair | Ticket raised, fix pending |
test.skip(condition, reason) | Skips when the condition is true | Feature disabled on WebKit |
test.only() | Runs just this test | Local debugging — never commit |
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 name | Lives for | Hands you |
|---|---|---|
| request | One test | An API client (APIRequestContext) — no browser needed |
| page | One test | A brand-new tab in a brand-new context |
| context | One test | A private browser profile with its own cookies and storage |
| browserName | One project | Which engine is running: chromium, firefox or webkit |
| browser | One worker | The browser process shared by that worker |
2.2 Your own fixtures for screen objects
// 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';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
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
});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' } },
],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
| Knob | What it controls | Out-of-box value |
|---|---|---|
timeout | Time allowed for one test | 30 seconds |
expect.timeout | How long a web-first assertion keeps retrying | 5 seconds |
globalTimeout | Hard ceiling for the entire run | Unlimited (0) |
workers | Number of parallel OS processes | Half the CPU cores |
fullyParallel | Also split tests inside a single file | false — files parallel, tests in a file in order |
retries | Extra attempts for a failed test | 0 |
maxFailures | Abort the run after this many failures | Unlimited (0) |
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
| Reporter | How to switch it on | Who 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 |
reporter: [
['line'],
['html', { open: 'never', outputFolder: 'awu-reports/html' }],
['junit', { outputFile: 'awu-reports/junit-results.xml' }],
['allure-playwright'],
],Gear 6 — Picking Which Tests Run
test('pay batch fee via UPI', { tag: ['@payments', '@nightly'] }, async ({ page }) => { /* ... */ });
test.describe('Wishlist', { tag: '@sanity' }, () => { /* every test inside inherits @sanity */ });| Command | Runs |
|---|---|
npx playwright test --grep @sanity | Only @sanity |
npx playwright test --grep-invert @nightly | Everything 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:18 | The test starting at line 18 |
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 });
});
}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.
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-blobsname: 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
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
| Purpose | Command / flag |
|---|---|
| Run the whole suite headless | npx 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 line | await page.pause() |
| Open a saved trace | npx 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 code | npx playwright codegen <url> |
| Open the HTML report | npx 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/htmlInterview 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