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

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.

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. 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: true

Cypress 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-patternWhy it failsFix
Writing page-object assertionsPer [selenium-pom]: assertions belong in test code, never in page objects.Move expect() calls to the spec file.
Exposing raw Page as a public propertyLeaks Playwright internals; tests bypass the page object API.Keep page protected; expose only typed action methods per [fowler-po].
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.

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

  • fowler-po - Martin Fowler, "Page Object": wrap a page behind an application-specific API; page objects avoid assertions.
  • selenium-pom - SeleniumHQ, "Page object models": assertions belong in the test, never in a page object; selectors centralized in one place.
  • playwright-pom - Playwright, "Page object models": constructor accepts a Page; locators defined as class properties via page.locator().
  • playwright-config - Playwright, "Test configuration": testDir, forbidOnly, retries, workers, webServer (recommends forbidOnly: !!process.env.CI, retries: 2 and workers: 1 on CI).
  • cyc / cyx / cya - Cypress configuration, custom commands, and the official GitHub Action (v10+ cypress.config.ts e2e block; cypress/e2e spec glob; cypress-io/github-action@v7).