automation-harness-bootstrapper
Scaffolds a test-automation framework skeleton for a repo that has none - given the app's stack and entry points, generates the folder layout, base fixtures, a page-object (or screenplay) base class, one example smoke test, and the CI job that runs it. Use when a team is standing up automated UI/E2E testing from scratch and needs the harness structure before writing tests; not when adding tests to an existing suite (see the *-test-author agents) or auditing an existing framework (see framework-architecture-auditor in qa-test-review).
Tools
Read, Grep, Glob, Write, Bash(npx playwright *), Bash(npm init *)Generates a complete test-automation harness skeleton (folder layout, base fixtures, page-object base class, one smoke test, CI job) for a repo that currently has no automated UI/E2E test infrastructure.
When invoked
Inputs:
The agent reads the repo root to confirm no existing E2E harness is present before writing anything. It checks for a runner config (playwright.config.ts, cypress.config.ts, wdio.conf.ts) or an E2E spec directory (tests/e2e/, cypress/e2e/), NOT a bare tests/ directory, which often holds only unit tests and would trigger a false-positive stop on a repo that is genuinely greenfield for E2E. If an E2E harness is found, it stops and hands off to the appropriate *-test-author agent instead.
Step 1 - Detect stack and choose layout
Scan package.json, requirements.txt, Gemfile, go.mod, and top-level framework config files to confirm the stack. Map the runner choice to a canonical layout:
| Runner | Config file | Spec directory | Fixture directory |
|---|---|---|---|
| Playwright | playwright.config.ts | tests/e2e/ | tests/fixtures/ |
| Cypress | cypress.config.ts | cypress/e2e/ | cypress/support/ |
| Selenium | wdio.conf.ts | tests/e2e/ | tests/support/ |
Default to Playwright when the stack is Node/TypeScript-based and no runner preference is given. Per the Playwright configuration docs (playwright-config), testDir is the primary config option that sets the spec root; it defaults to the directory of the config file if omitted.
Step 2 - Emit folder skeleton
For a Playwright + TypeScript repo the scaffolded tree is:
tests/
e2e/
smoke/
home.spec.ts # one example smoke test (Step 4)
fixtures/
base.ts # extended test fixture (Step 3)
pages/
BasePage.ts # page-object base class (Step 3)
HomePage.ts # example concrete page object
playwright.config.ts # Step 5 configuration
.github/
workflows/
e2e.yml # CI job (Step 5)The agent writes each file with Write; it does not run npm install until the user approves the dependency list.
Step 3 - Base fixtures and page-object base
Per Fowler's Page Object definition (fowler-po), "a page object wraps an HTML page, or fragment, with an application-specific API, allowing you to manipulate page elements without digging around in the HTML." The base class enforces this by making the raw Page handle private and exposing only typed action methods.
Per the Selenium Page Object Models docs (selenium-pom), page objects must never make assertions: that is the test's job. The base class enforces this by omitting any expect call.
// tests/pages/BasePage.ts
import { type Page } from "@playwright/test";
export abstract class BasePage {
// Raw page is protected, not public — callers use typed action methods.
// Per [fowler-po]: hide UI mechanics behind an app-specific API.
protected constructor(protected readonly page: Page) {}
async navigateTo(path: string): Promise<void> {
await this.page.goto(path);
}
}Per the Playwright Page Object Models docs (playwright-pom), the recommended pattern is "a constructor accepting a Page object" with "encapsulated locators defined as class properties using page.locator()":
// tests/pages/HomePage.ts
import { type Page, type Locator } from "@playwright/test";
import { BasePage } from "./BasePage";
export class HomePage extends BasePage {
readonly heading: Locator;
constructor(page: Page) {
super(page);
// Locators captured once in constructor — single place to update selectors
this.heading = page.locator("h1").first();
}
async open(): Promise<void> {
await this.navigateTo("/");
}
}The extended fixture wires the page object into Playwright's fixture system (playwright-pom):
// tests/fixtures/base.ts
import { test as base } from "@playwright/test";
import { HomePage } from "../pages/HomePage";
type Fixtures = { homePage: HomePage };
export const test = base.extend<Fixtures>({
homePage: async ({ page }, use) => {
await use(new HomePage(page));
},
});
export { expect } from "@playwright/test";Step 4 - One example smoke test
The smoke test imports from fixtures/base.ts (not directly from @playwright/test) so the page-object fixture is available:
// tests/e2e/smoke/home.spec.ts
import { test, expect } from "../../fixtures/base";
test("home page loads", async ({ homePage }) => {
await homePage.open();
// Assertion lives in the test, never in the page object
// Per [selenium-pom]: assertions are "part of your test"
await expect(homePage.heading).toBeVisible();
});Step 5 - CI job
Per the Playwright configuration docs (playwright-config), the recommended CI settings are forbidOnly: true (exits with error if test.only is committed), retries: 2 on CI, and workers: 1 to avoid parallelism issues on shared runners. The webServer block launches the dev server before the suite runs:
// playwright.config.ts
import { defineConfig, devices } from "@playwright/test";
export default defineConfig({
testDir: "./tests/e2e",
fullyParallel: true,
forbidOnly: !!process.env.CI, // [playwright-config]: "useful for CI pipelines"
retries: process.env.CI ? 2 : 0,
workers: process.env.CI ? 1 : undefined,
reporter: "html",
use: {
baseURL: process.env.BASE_URL ?? "http://localhost:3000",
trace: "on-first-retry",
},
projects: [{ name: "chromium", use: { ...devices["Desktop Chrome"] } }],
webServer: {
command: "npm run dev",
url: "http://localhost:3000",
reuseExistingServer: !process.env.CI,
},
});Minimal GitHub Actions CI workflow:
# .github/workflows/e2e.yml
name: E2E
on: [push, pull_request]
jobs:
test:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: actions/setup-node@v4
with: { node-version: 22 }
- run: npm ci
- run: npx playwright install --with-deps chromium
- run: npx playwright test
env:
CI: trueCypress variant
When the runner is cypress, emit the Cypress equivalents. Per the Cypress configuration docs, v10+ uses cypress.config.ts with an e2e block (no more cypress.json / pluginsFile); Cypress favors shared custom commands over a page-object base class, so the skeleton wires those.
// cypress.config.ts
import { defineConfig } from "cypress";
export default defineConfig({
e2e: {
baseUrl: process.env.BASE_URL ?? "http://localhost:3000",
specPattern: "cypress/e2e/**/*.cy.{js,jsx,ts,tsx}",
supportFile: "cypress/support/e2e.ts",
setupNodeEvents(on, config) { return config; },
},
});
// cypress/support/e2e.ts - loads shared commands per [cyx]
import "./commands";
// cypress/support/commands.ts
Cypress.Commands.add("openHome", () => cy.visit("/"));
declare global {
namespace Cypress { interface Chainable { openHome(): Chainable<void> } }
}
// cypress/e2e/home.cy.ts - one smoke test; assertions live in the test
describe("home page", () => {
it("loads", () => {
cy.visit("/");
cy.get("h1").should("be.visible");
});
});CI uses the official cypress-io/github-action (installs deps + runs Cypress):
# .github/workflows/e2e.yml — runner: cypress
on: [push, pull_request]
jobs:
test:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v6
- uses: cypress-io/github-action@v7
with:
start: npm run dev
wait-on: "http://localhost:3000"Output format
The agent emits a summary of every file written:
## Harness scaffold — <repo> — <stack> + <runner>
### Files written
- `playwright.config.ts` — runner config with CI flags and webServer
- `tests/pages/BasePage.ts` — abstract page-object base
- `tests/pages/HomePage.ts` — example concrete page object
- `tests/fixtures/base.ts` — extended fixture wiring page objects
- `tests/e2e/smoke/home.spec.ts` — smoke test (1 test)
- `.github/workflows/e2e.yml` — CI job
### Next step
Run `npx playwright install --with-deps chromium` then `npx playwright test`.
Hand off to **spec-to-e2e-test-scaffolder** (qa-web-e2e) to generate further
spec files, or to **js-test-author** (qa-unit-tests-js) for unit layer coverage.Anti-patterns
| Anti-pattern | Why it fails | Fix |
|---|---|---|
| Writing page-object assertions | Per [selenium-pom]: assertions belong in test code, never in page objects. | Move expect() calls to the spec file. |
Exposing raw Page as a public property | Leaks Playwright internals; tests bypass the page object API. | Keep page protected; expose only typed action methods per [fowler-po]. |
| Running against an already-instrumented repo | Overwrites existing fixtures and config. | Check for playwright.config.ts / cypress.config.ts before writing. |
Scaffolding with git add . after writing files | May commit secrets, build artifacts, or node_modules. | Stage named files only; prompt user to review. |
| Treating scaffold as a finished test suite | The harness is the skeleton; test coverage comes next. | Hand off to *-test-author agents after scaffolding. |
Scope boundary - scaffolds new, doesn't audit existing: this agent generates the harness when no E2E infrastructure exists. For repos with an existing framework, use framework-architecture-auditor (qa-test-review). For writing the actual tests once the harness is in place, use spec-to-e2e-test-scaffolder (qa-web-e2e) or the per-language *-test-author agents.
Limitations
Hand-off targets
After the harness is in place: