Sportsbook E2E Regression Framework
- Playwright
- TypeScript
- GitHub Actions
- Custom reporters
- Teams webhooks
A layered Playwright + TypeScript suite that runs 35+ real betting journeys across multiple brands and environments in parallel, and reports straight into Teams.
- Four-layer architecture: page objects → reusable flows → business test cases → spec suites. Each layer has a strict contract: pages hold no assertions, flows throw, test cases record.
- Continue-on-failure step runner: one broken step never hides the rest. Failed bets are cleaned up in-app and the page is reloaded only if still dirty. A dead tab aborts the run at once instead of timing out on every step.
- One Playwright project per brand, generated from config: brands run side by side locally with a single login each. CI runs on a self-hosted runner.
- Custom reporters: timestamped, auto-pruned, zipped HTML reports, plus a plain-English run summary (“timed out waiting for the betslip”) posted to Teams as an Adaptive Card.
- Flakiness guardrails written from real incidents: prove every action landed, read only settled state, locate by content not position, each test owns its start state.
- The problem
- A betting site with dozens of critical flows (bets, cash-out, live scores) across several brands and environments. Far too many to click through by hand before every release.
- What I built
- An automated regression suite that plays through 35+ real user journeys per brand, in parallel, and posts a pass/fail summary to the team chat.
- The result
- Every release gets a full check, and failures arrive as readable sentences instead of stack traces, so the right person can act right away.
- 35+ scenarios
- 4-layer architecture
- Parallel per brand
- 3 environments
- 170+ commits
▸ The pattern: record every step, keep going, fail once with the full picture (generic example)
export async function runStep(name: string, results: StepResult[], fn: () => Promise<void>) {
try {
await fn();
results.push({ name, ok: true });
} catch (err) {
if (isSkip(err)) throw err; // genuine skips stay skips
results.push({ name, ok: false, error: firstLine(err) });
}
}
export function assertAllPassed(results: StepResult[]) {
const failed = results.filter((r) => !r.ok);
if (!failed.length) return;
throw new Error(`${failed.length} step(s) failed:\n` +
failed.map((f) => ` ✗ ${f.name}\n ${f.error}`).join('\n'));
}How it flows
- 01Suite per brand
- 02Business test cases
- 03Reusable flows
- 04Page objects
- 05Step results
- 06HTML report + Teams summary