Testland
Browse all skills & agents

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).

Modelsonnet

Tools

Read, Grep, Glob, Write, Bash(npx playwright *), Bash(npm init *)
View source

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:

  • stack - app technology (e.g. next.js, react+vite, django+htmx, rails).
  • entry - URL or start command (e.g. http://localhost:3000 or npm run dev).
  • runner - test runner choice: playwright (default), cypress, or selenium-webdriver.

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.

The design decisions this scaffold encodes (directory layout, fixture scoping, CI + reporting wiring) are owned by test-framework-blueprint; the page-object contract it emits is owned by object-model-patterns. This agent turns those decisions into files.

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:

RunnerConfig fileSpec directoryFixture directory
Playwrightplaywright.config.tstests/e2e/tests/fixtures/
Cypresscypress.config.tscypress/e2e/cypress/support/
Seleniumwdio.conf.tstests/e2e/tests/support/

Default to Playwright when the stack is Node/TypeScript-based and no runner preference is given.

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

// 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.
  protected constructor(protected readonly page: Page) {}

  async navigateTo(path: string): Promise<void> {
    await this.page.goto(path);
  }
}
// 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:

// 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
  await expect(homePage.heading).toBeVisible();
});

Step 5 - CI job

Per the Playwright configuration docs (playwright-config (opens in new window)), 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: true

Cypress variant

When the runner is cypress, emit the Cypress equivalents. Per the Cypress configuration docs (opens in new window), 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 (opens in new window) (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-patternWhy it failsFix
Running against an already-instrumented repoOverwrites existing fixtures and config.Check for playwright.config.ts / cypress.config.ts before writing.
Scaffolding with git add . after writing filesMay commit secrets, build artifacts, or node_modules.Stage named files only; prompt user to review.
Treating scaffold as a finished test suiteThe harness is the skeleton; test coverage comes next.Hand off to *-test-author agents after scaffolding.

Page-object anti-patterns (assertions inside the object, leaking the raw driver handle, mechanic-named methods) are catalogued in object-model-patterns.

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

  • Runner coverage: Playwright is the fully-worked example (folder tree, page-object base, fixtures, smoke test, config, CI). Cypress emits config + support/custom-commands + a smoke spec + the official CI action (the "Cypress variant" section). Selenium/WebDriverIO emits a partial skeleton (config + base class only, no CI snippet, as CI integration varies by grid provider).
  • Auth flows not scaffolded: login fixtures, session storage, and OAuth helpers are out of scope; add them after the base harness is in place.
  • Non-Node stacks: Python (pytest-playwright), Java (Playwright Java), and C# (Playwright .NET) produce a folder tree and base-class template but no npm init / CI snippet - those require stack-specific commands.

Hand-off targets

After the harness is in place:

References