MS Schippers Test Automation Suite
Acceptance storefronts

Engineering handbook

Build and maintain Playwright tests in hycommerce-e2e

This is the working manual for the Schippers Test Automation Suite: a Playwright + TypeScript project that exercises the Hycommerce storefronts (NL, BE, FR, DE) across Chrome, Firefox, Safari (WebKit) and Edge. It covers the layered architecture, environment setup, how to read and extend an existing test, how to write a brand-new one, and how the suite runs in CI and reports back through this dashboard.

Playwright Test + TypeScript Page Object Model 13 smoke specs 7 e2e journeys 1 data-driven integration spec

01Overview & architecture

Every test in this repo is assembled from four layers that each have exactly one job. Keeping the boundaries clean is what lets the same flow logic run against four countries and four browsers without duplicating code.

13
smoke specs
7
e2e journeys
10
integration CSV fixtures
17
Playwright projects

The layered model

test-cases

tests/*/test-cases/ — the spec files Playwright discovers. Each test(...) owns the tag, the fixtures it needs, and the pass/fail assertion. No selectors live here.

step-definitions

tests/*/step-definitions/ — reusable flow logic (runCartBadgeCountSmoke, runPurchaseFlowForStore…) composed from page-object calls, wrapped in test.step() via reportStep.

page-objects

src/page-objects/{common,smoke,e2e}/ and src/page-objects/components/ — selectors and low-level actions only, one class per page or reusable UI component, all extending BasePage.

test-data

tests/test-data/{smoke,e2e}/ — per-country inputs, timeouts and copy patterns, usually resolved from env vars with a per-country fallback (getCountryEnv).

Smoke, e2e and integration

  • Smoke (tests/smoke/) — fast, focused checks of one storefront surface at a time (cart, PDP, navigation, footer…). These run in parallel and make up the bulk of the suite (13 spec files).
  • e2e (tests/e2e/) — broader, multi-page journeys: full purchase flows, real identity-provider login, HyCare sales-admin reordering, registration. Several run serial and/or --workers=1 because they share authenticated state.
  • Integration (tests/integration/) — a single data-driven spec that replays the Hycommerce storefront API contract from CSV fixtures, with no browser at all (it uses Playwright's request context, project integration).

Where everything lives

PathPurpose
tests/smoke/test-cases/Smoke specs only — assertions and tags
tests/smoke/step-definitions/Smoke flow logic and shared smoke helpers
tests/e2e/Broader end-to-end journeys (test-cases + step-definitions)
tests/integration/API contract spec plus testdata/*.csv fixtures
tests/test-data/smoke/Smoke input/config data and shared smoke types
tests/test-data/e2e/e2e input/config data
src/page-objects/common/Page objects shared by e2e and smoke
src/page-objects/smoke/Smoke-only page objects
src/page-objects/e2e/e2e-only page objects
src/page-objects/components/Reusable UI components (login modal, header, footer)
src/support/waits/Explicit wait helpers for smoke flows
src/support/reporting/Smoke reporting helpers (reportStep, attachments)
src/fixtures/Shared test fixtures
src/api/Reusable API client and store API wrapper
src/utils/Generic helpers (env, storefront URL, cookie consent)

The @/ import alias

tsconfig.json maps @/* to ./src/* (plus the narrower @page-objects/* and @test-data/* aliases). Always import internal modules through the alias instead of relative ../../ chains:

import { test, expect } from '@/fixtures/pages.fixture';
import { CartPage } from '@/page-objects/common/cart.page-object';
import { cartData, getCartData } from '@test-data/smoke/cart.data';

02Environment & setup

The suite talks to real acceptance storefronts behind Cloudflare Access, so setup is mostly about installing browsers and supplying the right credentials and service-token headers before the first run.

Install dependencies and browsers

npm install then npm run install:browsers (installs Firefox, WebKit — used as safari — Chrome and Edge channels for Playwright). Run npm run install:chrome / npm run install:edge separately if you want the locally installed branded builds instead of Playwright's own channel (installing Edge may prompt for your macOS password).

Create your .env

cp .env.example .env, then fill in the required values. playwright.config.ts loads .env.example first for safe defaults and then overrides with .env if it exists.

Set the required storefront and Cloudflare variables

BASE_URL (NL), BASE_URL_BE, BASE_URL_FR, BASE_URL_DE, plus CF_ACCESS_CLIENT_ID and CF_ACCESS_CLIENT_SECRET for the Cloudflare Access service token. Without the last two, playwright.config.ts prints a warning and requests will not carry the bypass headers.

Add optional flow configuration as needed

PLAYWRIGHT_COUNTRIES / PLAYWRIGHT_BROWSERS to narrow the project matrix, TEST_PRODUCT_PATH / TEST_PRODUCT_NAME_CONTAINS / TEST_SEARCH_QUERY for storefront content, ACCEPTANCE_LOGIN_EMAIL/PASSWORD, HYCARE_SALES_ADMIN_EMAIL/PASSWORD and the per-country HYCARE_* values for authenticated e2e journeys, and REPORT_DATABASE_URL if you want local runs to persist into the same Postgres history CI uses.

Run something to confirm it works

npm run test:smoke:cart is a good smoke check — it only needs BASE_URL/BASE_URL_BE and the Cloudflare pair, and finishes quickly.

Required environment values

VariableUsed for
BASE_URLNL storefront base URL (default project baseURL)
BASE_URL_BE / _FR / _DEPer-country storefront base URLs for the -be, -fr, -de projects
CF_ACCESS_CLIENT_ID / CF_ACCESS_CLIENT_SECRETCloudflare Access service-token headers sent on every request
ACCEPTANCE_LOGIN_EMAIL / _PASSWORDShared credentials for Login, MS-Schippers and HyCare authenticated flows
HYCARE_SALES_ADMIN_EMAIL / _PASSWORDIsolated sales-admin account for the HyCare impersonation + reorder journey
HYCARE_CLIENT_NUMBER_NL|BE|FR|DE, HYCARE_BUSINESS_UNIT_*, HYCARE_IMPERSONATION_USER_EMAIL_*Per-country HyCare client/impersonation lookup data
HYCARE_PRODUCT_SKU / HYCARE_PRODUCT_SEARCH_TERMProduct used in the HyCare reorder flow
REPORT_DATABASE_URLPostgres connection string for persistent run history (use a pooled Supabase connection in CI)
Cloudflare Access bypass. playwright.config.ts sets CF-Access-Client-Id and CF-Access-Client-Secret globally under use.extraHTTPHeaders, so every request — browser navigation and API calls alike — carries the service-token bypass automatically. You never need to add these headers in a page object or test.

03Anatomy of a test

Walking through the real Cart smoke suite shows how the four layers fit together end to end.

1. The test-case — tests/smoke/test-cases/cart.smoke.spec.ts

Imports the fixtures, the step-definition functions, and the test data. Each test is one line of orchestration:

import { test } from '@/fixtures/pages.fixture';
import {
  runCartAutoOpensSmoke,
  runCartBadgeCountSmoke,
  // …other step-definition functions
} from '../step-definitions/cart.steps';
import { cartData, getCartData } from '@test-data/smoke/cart.data';

test.describe('Cart', () => {
  test.describe.configure({ mode: 'parallel' });
  test.setTimeout(cartData.timeoutMs);

  // TcSmokeAcct001: cart auto opens when item is added.
  test('TcSmokeAcct001 - cart auto opens when item added', { tag: ['@quick-smoke'] }, async ({
    page,
    productPage,
    cartPage,
    baseURL,
  }) => {
    await runCartAutoOpensSmoke({ page, productPage, cartPage }, getCartData(baseURL));
  });
});

Naming convention: Tc<Suite><Area><Number> - lowercase descriptive title, e.g. TcSmokeAcct001, TcSmokeCart009, TcSmokeNavg003. The numeric ID stays stable so historical runs in the Reports dashboard can be tracked across renames of the descriptive part. The second options argument, { tag: ['@quick-smoke'] }, is how a test opts into a suite — see Tags, suites & selective runs.

2. The step-definition — tests/smoke/step-definitions/cart.steps.ts

Owns the reusable flow logic and reports readable steps into the trace/report via reportStep:

async function runCartBadgeCountSmoke(
  context: CartSmokeContext,
  data: CartSmokeData
): Promise<void> {
  const { page } = context;
  const preparedCart = await prepareCartWithSingleProduct(context, data);

  await reportStep('Validate the cart badge increments by exactly one added unit', async () => {
    if (preparedCart.mode === 'login-gate') {
      expect(preparedCart.finalCartCount).toBe(preparedCart.initialCartCount);
      return;
    }
    expect(preparedCart.finalCartCount).toBe(preparedCart.initialCartCount + 1);
  });

  await attachVisitedUrls(preparedCart.visitedUrls);
  await attachSmokeContext('cart-badge-count-smoke-context', {
    finalURL: page.url(),
    initialCartCount: preparedCart.initialCartCount,
    finalCartCount: preparedCart.finalCartCount,
    productPath: data.productPath,
  });
}

reportStep, attachVisitedUrls and attachSmokeContext come from src/support/reporting/smoke.report.ts — thin wrappers around Playwright's test.step() and test.info().attach() so every smoke run leaves a structured trail (visited URLs, cart counts, product path) that shows up in the Reports dashboard under each test's attachments.

3. The test data — tests/test-data/smoke/cart.data.ts

function getCartData(baseURL?: string) {
  const country = getStorefrontCountry(baseURL) ?? 'nl';
  const countryConfig = manualTestData.smoke.cart.byCountry[country];

  return {
    timeoutMs: 180_000,
    maxAddToCartAttempts: 3,
    cartOpenTimeoutMs: 12_000,
    quantitySyncTimeoutMs: 20_000,
    productPath: getCountryEnv('TEST_PRODUCT_PATH', country, countryConfig.productPath),
    productNameContains: getCountryEnv('TEST_PRODUCT_NAME_CONTAINS', country, countryConfig.productNameContains),
    cartPath: countryConfig.cartPath,
    supportsAnonymousCart: countryConfig.supportsAnonymousCart,
    quantityInputSelector: 'input[aria-label="Input Number"]:visible',
    increaseButtonSelector: 'button[title="Increase"]:visible',
    decreaseButtonSelector: 'button[title="Decrease"]:visible',
    checkoutButtonPattern:
      /verder naar bestellen|afrekenen|checkout|continuer la commande|zur kasse|.../i,
    // …timeouts and localized text patterns for nl/be/fr/de
  };
}

Test data resolves country from the fixture's baseURL, then layers getCountryEnv('KEY', country, fallback) — a country-suffixed env var, then the bare env var (NL only), then a hardcoded fallback from manualTestData (tests/test-data/manual-test-data.ts / .json). This is why the same cart spec runs unmodified against NL, BE, FR and DE.

4. The page object — src/page-objects/common/cart.page-object.ts

class CartPage extends BasePage {
  async open(path = '/winkelwagen'): Promise<void> {
    await this.goto(path);
    await this.closeBlockingOverlays();
    await this.waitForCartMergeDialogToClose().catch(() => {});
    await expect(this.page).toHaveURL(this.cartUrlPattern);
  }

  async expectCartHasItems(): Promise<void> {
    await expect
      .poll(() => this.hasItems(), {
        timeout: 20_000,
        message: 'Expected the cart to contain at least one line item.',
      })
      .toBeTruthy();
  }
}

Every page object extends BasePage (src/page-objects/common/base.page-object.ts), which supplies a resilient goto(), clickFirstVisible() with a DOM-click fallback, and closeBlockingOverlays() for cookie/privacy/merge-cart dialogs — see Selectors & stability.

Fixtures — how the layers get wired together

  • src/fixtures/test.fixture.ts extends the base Playwright test with an auto cookieConsent fixture that hides the country-selection modal, retries flaky page.goto() navigations, blocks the Trengo chat widget by default, and auto-dismisses the cookie banner on every domcontentloaded event.
  • src/fixtures/pages.fixture.ts extends test.fixture.ts further and injects one fixture per page object / component (homePage, cartPage, checkoutPage, loginModal, siteHeader…), each a plain new SomePage(page).
import { test, expect } from '@/fixtures/pages.fixture';
// or, for page-level-only tests with no page objects:
import { test, expect } from '@/fixtures/test.fixture';

Use pages.fixture whenever a test needs page objects or components; use the lighter test.fixture for page-level-only checks.

04Writing a new smoke test

Follow the same order every time: data, then selectors, then flow, then the test case itself. That order keeps you from hardcoding a locale string or a selector directly into the spec file.

Add or extend a test-data file

In tests/test-data/smoke/<area>.data.ts, add a get<Area>Data(baseURL?) function that resolves the storefront country and returns timeouts, paths and any localized text patterns — follow the cart.data.ts pattern (getStorefrontCountry + getCountryEnv + a manualTestData fallback).

Add selectors and actions to a page object

Put storefront selectors in the right src/page-objects/{common,smoke,e2e}/ folder — common if e2e might reuse it, smoke if it's smoke-only. Extend BasePage, prefer getByRole/getByText/[data-testid] locators, and keep any new component (modal, header widget) in src/page-objects/components/.

Write the step-definition

In tests/smoke/step-definitions/<area>.steps.ts, compose page-object calls into a run<Scenario>Smoke(context, data) function. Wrap the meaningful actions in reportStep('…', async () => { … }) and finish with attachSmokeContext('<scenario>-smoke-context', { … }) so the Reports dashboard has something useful to show for the run.

Wire fixtures if you added a page object

If the page object is brand new, register it as a fixture in src/fixtures/pages.fixture.ts (add to PageFixtures and the test.extend block) so specs can request it by name.

Add the test case with the right tag

In tests/smoke/test-cases/<area>.smoke.spec.ts, add test('TcSmoke<Area><NNN> - description', { tag: ['@quick-smoke'] }, async ({ … }) => { … }). Pick the tag deliberately — see Tags, suites & selective runs — and add locale-scoping tags (@nl-only, @be-only, @not-fr) if the flow doesn't apply to every storefront.

Run it locally, then confirm typecheck and the wider suite

npx playwright test tests/smoke/test-cases/<area>.smoke.spec.ts --project=chrome
npm run typecheck

Then run the suite script that matches your tag (e.g. npm run test:quick:smoke) to confirm the new test is actually picked up by the grep filter.

05Writing e2e & integration tests

e2e journeys and the integration spec both live outside the smoke tree because they carry different constraints: real authentication, multi-page state, or no browser at all.

e2e journeys

e2e specs (tests/e2e/test-cases/) chain several page objects together — listing → product → cart → checkout, or a full identity-provider login. Where a flow depends on shared authenticated state (impersonation, a single sales-admin session), the describe block runs serial and the npm script pins --workers=1:

test.describe('Order', () => {
  test.describe.configure({ mode: 'serial', timeout: orderData.timeoutMs });

  test(
    'OrderE2E001 - sales admin can impersonate a client and place a HyCare reorder',
    { tag: ['@hycare', '@full-regression', '@requires-auth', '@nl-only'] },
    async ({
      page,
      accountLoginPage,
      identityProviderPage,
      siteHeader,
      salesAdminPage,
      hyCareOrderPage,
      baseURL,
    }) => {
      /* … */
    },
  );
});

identityProviderPage (src/page-objects/common/identity-provider.page-object.ts) drives the real Schippers identity provider login page (not a mocked auth step), asserting the URL matches login.schippers.cloud / *.auth.hycommerce.schippers.cloud before filling the password and submitting. The matching npm script cross-browsers the journey while keeping it serial:

"test:hycare:nl:cross-browser": "playwright test tests/e2e/test-cases/order.e2e.spec.ts --project=firefox --project=chrome --project=safari --project=edge --workers=1"

HyCare currently ships NL-only test data, so playwright.config.ts excludes tests/e2e/test-cases/order.e2e.spec.ts from the BE/FR/DE projects (nlOnlyHyCareSpecs) until matching customer data exists for those storefronts.

Integration: the data-driven API contract spec

tests/integration/test-cases/api-endpoints.integration.spec.ts is a single spec file that generates one Playwright test() per row of every CSV file in tests/integration/testdata/ — no page objects, no browser, just request context calls against the Hycommerce API:

const csvFiles = fs
  .readdirSync(testDataDirectory)
  .filter((fileName) => fileName.endsWith('.csv'))
  .sort((left, right) => left.localeCompare(right));

test.describe('@integration API endpoint coverage', () => {
  test.setTimeout(30_000);

  for (const csvFileName of csvFiles) {
    const rows = readCsvRows(path.join(testDataDirectory, csvFileName));

    for (const row of rows) {
      test(
        `${row.case_id} - ${row.description} [${csvFileName}]`,
        { tag: ['@full-regression'] },
        async ({}, testInfo) => {
          const apiContext = await createStoreRequestContext(playwrightRequest);
          const response = await apiContext.fetch(row.url, createRequestOptions(row));

          expect(response.status()).toBe(Number(row.expected_status));
          // …asserts required_json_path / expected_json_values / min_array_lengths from the row
        },
      );
    }
  }
});

Each CSV row is a self-contained contract check — method, URL, expected status/content-type, and one or more JSON paths that must be populated:

case_id,description,method,url,request_headers,request_body,expected_status,expected_content_type,required_json_path,additional_required_json_paths,expected_json_values,min_array_lengths
IntegrationAPI001,fetch product details,GET,https://acc.api.hycommerce.schippers.cloud/product/web/product/0301313?locale=nl-NL,,,200,application/json,sku,"[""name"",""brand"",""fullSlug""]","{""sku"":""0301313"",""brand"":""MS Schippers""}","{""variants.items"":1}"

To add a new contract check, add a row to an existing CSV (or a new *.csv file — it's picked up automatically) rather than editing the spec. Run the whole set with npm run test:integration, which targets the dedicated integration Playwright project (no browser use.baseURL beyond the default storefront origin).

06Tags, suites & selective runs

Every test carries one suite tag and, where relevant, one or more scoping tags. The suite tags decide which npm script picks the test up; the scoping tags decide which country/browser projects it runs under.

Suite tags

Tagnpm scriptWhat it runs
@quick-smokenpm run test:quick:smoke / npm run test:smoke:chromeLean smoke subset for a fast storefront health check
@full-smokenpm run test:full-smoke / npm run test:full-smoke:chromeThe full smoke spec set (superset of @quick-smoke)
@quick-regressionnpm run test:quick:regression / :chromeCritical smoke checks plus core e2e coverage
@full-regressionnpm run test:full:regression / :chromeFull Chrome NL+BE regression: smoke, e2e (incl. HyCare, My Account) and the integration API contract spec
@integrationnpm run test:integrationData-driven API contract checks against tests/integration/testdata/*.csv (project integration)

Scoping tags

TagEffect
@nl-onlyExcluded from the BE, FR and DE projects via grepInvert in playwright.config.ts
@be-onlyExcluded from the NL project's grepInvert (and thus effectively BE-scoped)
@not-frExcluded from the fr project only
@requires-authMarks a flow that needs ACCEPTANCE_LOGIN_* / HYCARE_* credentials configured — informational, doesn't change project scoping
@my-accountMy Schippers account-area coverage, typically paired with @requires-auth
@hycareHyCare sales-admin impersonation and reorder flow (NL-only test data today)

Direct Playwright invocations

npx playwright test --grep @quick-smoke --project=chrome --project=chrome-be
npx playwright test --grep @quick-regression --project=chrome --project=chrome-be
npx playwright test --grep @full-regression --project=chrome --project=chrome-be
npm run test:smoke:cart
npm run test:hycare:nl:cross-browser
npm run test:storefront-journeys

Browser projects are named <browser> for NL and <browser>-<country> for BE/FR/DE (e.g. chrome, chrome-be, firefox-fr, edge-de) — 4 browsers × 4 countries, plus the standalone integration project. Narrow either axis with PLAYWRIGHT_BROWSERS / PLAYWRIGHT_COUNTRIES or with explicit --project flags.

07Selectors & stability best practices

These tests run against live acceptance storefronts in four languages, so resilience isn't optional — a hardcoded English label or a one-shot waitForTimeout will flake the first time content or network timing shifts.

Do prefer getByRole(), getByText() and [data-testid="…"] over positional CSS or XPath. Where copy is localized, match with a single regex covering all four locales — see checkoutButtonPattern and loginGatePattern in tests/test-data/smoke/cart.data.ts — rather than four separate hardcoded strings.
Do reuse the explicit wait helpers in src/support/waits/page.wait.ts (waitForPageStable, waitForNavigationPanel, waitForSearchResults) and Playwright's expect.poll() for state synchronization instead of a bare page.waitForTimeout(). BasePage.goto() and CartPage.open() already retry on known-flaky navigation errors (ERR_CONNECTION_CLOSED, ERR_ABORTED, frame-detached, etc.) — don't re-implement that retry loop per page object.
Do let the framework handle cookie consent and blocking overlays. The cookieConsent auto-fixture in src/fixtures/test.fixture.ts calls acceptCookieBannerWithRetry (from src/utils/cookie-consent.ts) on every navigation and DOM-ready event, and BasePage.closeBlockingOverlays() dismisses privacy/cookie/merge-cart dialogs across nl/be/fr/de copy. New page objects get this for free by extending BasePage — don't write a bespoke "click accept cookies" step in a new test.
Watch for content drift. Because there's no mocked backend, a failing assertion can mean the storefront copy or price genuinely changed, not that the test is wrong. Before "fixing" a selector, check whether the CSV/regex/test-data value is simply stale.
Don't put business-rule assertions inside a page object. Page objects may expose narrow, poll-based readiness helpers (expectCartHasItems, expectProceedToCheckoutVisible) that describe UI state, but the decision of whether a scenario passes or fails belongs in the step-definition / test-case, where the expected business behaviour is visible and reviewable. Also avoid raw XPath except as a last-resort escape hatch (see CartPage.clickQuantityStepper's ancestor-or-self:: fallback) — try role/text/test-id locators first.

08Maintaining tests & CI

Every local or CI run produces this same dashboard, so the Reports page is both your results view and your primary triage tool.

Reporter & dashboard

scripts/reporting/dashboard.reporter.js is registered as a Playwright reporter in playwright.config.ts. After every run it writes a static site to playwright-report/ (this Manual page, the Reports overview, and the build-copied Coverage page) and retains run history in .report-history/. If REPORT_DATABASE_URL is set, the same run is also persisted to Postgres, and npm run report:rebuild can rehydrate the latest stored runs and regenerate the site without re-running any tests.

GitHub Actions workflows

WorkflowTrigger
.github/workflows/smoke-test-and-deploy-report.ymlPush to main (defaults to chromium / NL) and manual dispatch
.github/workflows/full-smoke-test-and-deploy-report.ymlManual dispatch with browser/country/environment inputs
.github/workflows/quick-regression-test-and-deploy-report.ymlManual dispatch
.github/workflows/full-regression-test-and-deploy-report.ymlManual dispatch
.github/workflows/pr-fast-check.ymlPull requests into main — typecheck plus a quick-smoke and quick-regression gate on Chrome/NL

Each of the four deploy workflows follows the same pipeline:

  1. Validates the Postgres report-history connection (npm run report:test-db).
  2. Resolves the selected browsers, countries and environment override from the workflow inputs.
  3. Runs the corresponding Playwright command.
  4. Persists the latest run to Postgres.
  5. Rebuilds the static dashboard and uploads it as a workflow artifact.
  6. Deploys playwright-report/ to Azure Static Web Apps and writes the live dashboard URL to the job summary.

Manual workflow dispatch defaults to chromium on NL with the current environment (other choices: acc, tst, dev). Required secrets: AZURE_WEBBAPP_DEPLOYMENT_TOKEN, BASE_URL, BASE_URL_BE, CF_ACCESS_CLIENT_ID, CF_ACCESS_CLIENT_SECRET, REPORT_DATABASE_URL.

Triaging a flaky live-site failure

  1. Open the failed test in the Reports dashboard — check the failure screenshot, trace, and any attachSmokeContext payload (visited URLs, cart counts, etc.) first.
  2. Reproduce locally against the same browser/country project: npx playwright test <spec> --project=<browser>[-<country>] --headed.
  3. Decide what changed: storefront copy/price drift, a genuine regression, or infra reachability (Cloudflare Access creds, acceptance environment downtime).
  4. If it's a real timing flake, tighten the wait (a more specific expect.poll() condition or locator) rather than adding blind retries — the base config runs with retries: 0 outside CI-specific overrides.
  5. If it's content drift, update the test-data pattern/value (and tests/test-data/manual-test-data.ts/.json if it's a shared fallback) rather than the assertion logic.

Maintenance checklist

  • Run npm run typecheck before pushing — tsconfig.json covers playwright.config.ts, src/**/*.ts and tests/**/*.ts.
  • Run npm run validate:test-data after touching tests/test-data/manual-test-data.json to make sure every country entry is complete.
  • Keep .env.example in sync whenever a test introduces a new environment variable.
  • Double-check the suite tag and any scoping tags (@nl-only/@be-only/@not-fr) match where the flow is actually expected to run.
  • When adding a page object, confirm it's registered in src/fixtures/pages.fixture.ts if any spec needs to request it as a fixture.
  • Prefer updating an existing CSV row over hand-editing the integration spec when an API contract changes.
Where to go next. Check the Coverage page to see which storefront surfaces and My Schippers account areas already have automated coverage before adding a new test. If you're using an AI coding assistant to help write specs, point it at this Manual and the repository README so generated tests follow the same layered conventions described here.

09Building tests with GitHub Copilot

This repo is set up so GitHub Copilot writes tests that already follow the layered conventions above. There are three files to know, a recommended mode and model, and a one-command way to scaffold a new test.

The files Copilot uses

.github/copilot-instructions.md

Loaded automatically by Copilot for every request in this repo — the golden rules, layering, tag table and do-NOT list. You don't attach it; Copilot always sees it. Keep it accurate.

docs/AI-TEST-AUTHORING-GUIDE.md

The deep guide: copy-paste skeletons, e2e/integration recipes, selector rules and the full do-NOT list. Attach it in chat with #file:docs/AI-TEST-AUTHORING-GUIDE.md when authoring.

.github/prompts/new-smoke-test.prompt.md

A reusable prompt file. Type /new-smoke-test in Copilot Chat to scaffold a new smoke test (data → page object → step-definition → spec) that already obeys every rule.

Recommended model

Use Gemini 2.5 Pro as the first choice, or GPT-5 as an equally good alternative — pick it from the model dropdown in the Copilot Chat input. A large-context, strong-coding model matters here because authoring a correct test means holding the instructions file, the authoring guide and several page objects / step-defs in context at once; Gemini 2.5 Pro's large context window does that comfortably. Avoid the small "fast" models for authoring — they tend to inline selectors, skip a layer, or forget the tag.

Which Copilot features to use

  • Agent mode — the create/edit mode that can touch several files and run the test. Use it for building tests (Ask mode is only for questions).
  • Context references#file:<path> to pin a specific file, #codebase (or @workspace) to let Copilot search the repo, and #selection for highlighted code.
  • Prompt files/new-smoke-test runs the reusable prompt above. Requires the chat.promptFiles setting (on by default in current Copilot).

Workflow

Open the area's existing trio

Open the nearest *.smoke.spec.ts, its *.steps.ts and *.data.ts, plus the page object, so Copilot has concrete patterns to mirror.

Pick Agent mode + Gemini 2.5 Pro (or GPT-5)

Select the model in the chat input's model picker and switch the mode to Agent.

Run /new-smoke-test (or write your own prompt)

Answer the area / behaviour / tag inputs. Copilot scaffolds data → page object → step-definition → spec across the right folders.

Make Copilot verify

Have it run npm run typecheck and the single test on Chrome, and iterate until green. Review the diff against the golden rules before committing.

Example prompts

/new-smoke-test
area: product comparison
behaviour: adding a third product keeps the comparison bar stable and removable
tag: @full-smoke
#file:docs/AI-TEST-AUTHORING-GUIDE.md #codebase
Add a @quick-smoke test that asserts the PDP breadcrumb links back to its category
listing. Follow the layering: selectors only on the product page object, assertions in
the step-definition, wiring only in the spec. Then typecheck and run it on Chrome.
The same setup works for OpenAI Codex and other assistants. Point them at docs/AI-TEST-AUTHORING-GUIDE.md and .github/copilot-instructions.md; the conventions and definition-of-done are identical regardless of the tool.