Test automation for APIMatic v4 portals — the static documentation portals
produced by apimatic portal generate.
The repo is structured around the order things happen: portals are generated from a build and checked against what the build declared, and only then is the portal served and tested in a browser.
test-builds/petstore/src/ the build: apimatic.json, content/, spec/, static/
│
├──▶ 1. ARTIFACTS per scenario: copy src/ to a temp folder, change it,
│ generate, compare produced vs declared
│ filesystem only — no browser, no server
│
│ apimatic portal generate --input test-builds/petstore
▼
test-builds/petstore/portal/ the generated portal (gitignored)
│
│ http-server -c-1 --cors
▼
http://127.0.0.1:8080
│
├──▶ 2. VISUAL how it looks, against committed baselines
└──▶ 3. FUNCTIONAL what it does — runs against the auth build, whose
playground calls the API Tester on :3000
Each stage catches something the next one can't. Artifacts catch a portal built wrong — an SDK missing, a declared version that never reached the manifest. Visual catches one that renders wrong. Functional catches one that renders fine and behaves wrong.
Needs the APIMatic CLI and http-server on your PATH, plus Node and pnpm.
npm install -g @apimatic/cli http-server
pnpm install
pnpm exec playwright install chromiumEverything else resolves inside this repo — there is nothing to clone, pin or configure.
.env — all optional:
| Variable | What it does |
|---|---|
PORTAL |
Which build to test — a key in config/portals.ts. Defaults to petstore. |
PORTAL_PORT |
Port for http-server. Defaults to 8080. |
PORTAL_BASE_URL |
Test a portal that's already hosted. Skips generate and serve entirely. |
pnpm portal:generate # apimatic portal generate, nothing else
pnpm portal:serve # generate, then serve on :8080
pnpm test # all three projects
pnpm test:artifacts # stage 1 — generates its own portals, no server
pnpm test:visual # stage 2
PORTAL=auth pnpm test:functional # stage 3 — also starts the API Tester
# Portal Artifacts API — needs APIMATIC_AUTH_KEY, skipped without it
pnpm test:sdk-docs # petstore SDK docs vs baselines
pnpm test:code-samples # codegen-v2 code samples, OpenAPI 3.0 + 3.1 builds
pnpm test:portal-artifacts-api # both, in one run (what CI runs)
pnpm test:visual:update # accept the current rendering as the new baselines
pnpm report playwright-report/visual # open a stage's HTML reportEach stage writes to its own test-results/<stage>/ and
playwright-report/<stage>/ (a run of several stages writes to all; functional
adds the build, e.g. functional-auth), so one
stage's run can't wipe another's failures.
webServer uses reuseExistingServer locally, so if a portal is already up on
PORTAL_PORT the tests attach to it. To drive the two commands yourself:
cd test-builds/petstore
apimatic portal generate --force
cd portal
http-server -c-1 --cors-c-1 disables caching, so a regenerated portal is served immediately rather
than from a stale cache — which otherwise shows up as a snapshot that
mysteriously refuses to change.
tests/artifacts/ compares a generated bundle to the apimatic.json it was
built from.
The config is a set of promises: languages promises a downloadable archive, a
portal page and a package manifest carrying the identity you asked for;
plugin promises a context-plugin page; brand and navigation promise assets
and links. The artifact suite is where those promises get checked.
Each scenario generates its own portal. generateScenario() in
utils/scenario.ts copies the build's src/ to a temp folder, applies the
scenario's changes, runs apimatic portal generate on the copy and hands back
the output. The committed build is never edited:
test.describe("the plugin block removed from apimatic.json", () => {
let portal: GeneratedPortal;
test.beforeAll(async () => {
portal = await generateScenario("without plugin", {
config: (config) => { delete config.plugin; }, // edit apimatic.json
// files: { "content/guides/new.md": "# New" }, // add or overwrite files
// remove: ["spec/APIMATIC-META.json"], // delete files or folders
});
});
test.afterAll(() => portal?.cleanup());
test("no context-plugin page is generated", () => {
expect(portal.exists("context-plugin", "index.html")).toBe(false);
});
});One describe is one scenario and one generation (~35s, ~13 MB), shared by
every test inside it — so group related checks under one scenario. Temp folders
are deleted afterwards; set KEEP_SCENARIOS=1 to keep them for a look. A
scenario whose generation fails reports the tail of the CLI's output.
Because the suite generates for itself, a run of only --project=artifacts
doesn't start the web server.
utils/artifacts.ts has the reading side: GeneratedPortal (the declared
config of the copy, and the generated tree), and reading a package manifest out
of an SDK archive without extracting it (package.json, pyproject.toml or
.csproj, normalised so a test doesn't have to know which language spells its
package id how).
tests/visual/ compares screenshots against baselines in portal-snapshots/.
14 tests, chosen to cover each distinct render path the build exercises rather than to cover many pages:
| Area | Cases |
|---|---|
| Shell | landing page, landing page dark, top bar, sidebar tree |
| Authored content | a guide page, accordions, tabs, GFM tables |
Generated from apimatic.json |
SDKs index, the TypeScript SDK page, the context plugin page |
| Generated from the spec | an API endpoint page |
| Interaction and layout | search overlay with results, mobile viewport |
Two are element-scoped — the top bar and the sidebar — so a change in page content doesn't fail them. The rest are full viewport.
Snapshots are platform-specific: these were captured on win32/Chromium at a
pinned 1920×1080, so CI runs windows-latest to match.
tests/functional/ checks what the portal does, which a screenshot can't
judge: a playground call sending the right credentials, navigation landing
where it should, search actually matching, downloads being served. The portal
is a client-routed SPA, so a broken route shows as a blank shell rather than a
server error — exactly the failure a visual test absorbs and a functional test
catches.
The suite runs against the auth build (test-builds/auth), which has one
operation per auth type. Each operation points at a route of APIMatic's API
Tester — the trimmed copy in api-tester/, which checks credentials against
hardcoded values. test-builds/auth/README.md maps every operation to its
route and credentials.
A functional run needs two servers, and Playwright starts both (webServer in
playwright.config.ts):
| Server | Port | Started by |
|---|---|---|
| the generated auth portal | 8080 |
scripts/serve-portal.mts |
| the API Tester | 3000 |
node api-tester/bin/www |
The API Tester's dependencies are in the root package.json, so pnpm install
covers it. Locally both servers are reused if already up, so you can also start
them yourself:
PORTAL=auth pnpm portal:serve # the portal, on 8080
node api-tester/bin/www # the API it calls, on 3000Tests that only make sense on the auth build skip themselves unless
PORTAL=auth. Where the response panel's look matters, a test pairs its
wire-level assertions (headers sent, status returned) with an element-scoped
screenshot of the playground.
When adding cases, use the page objects in pages/ and the named routes in
config/portals.ts; don't put raw locators in test files. A case for another
build goes in its own folder under tests/functional/.
config/portals.ts which builds we test + their named routes
test-builds/<name>/src/ the build input (apimatic.json, content, spec, static)
test-builds/<name>/portal/ generated output — gitignored, rebuilt every run
utils/env.ts resolves build dir, generated dir, port, baseURL
utils/scenario.ts copy src/ to a temp folder, change it, generate
utils/artifacts.ts declared-vs-produced helpers, zip manifest reading
utils/visual.ts snapshot stability helpers
pages/ page objects — locators live here, never in tests
tests/artifacts/<build>/ stage 1
tests/visual/<build>/ stage 2
tests/functional/ stage 3
api-tester/ trimmed API Tester the auth build's playground calls
scripts/serve-portal.mts generate + serve
portal-snapshots/ committed visual baselines
.github/workflows/portal-tests.yml runs the same pipeline on
windows-latest (the baselines were captured there), each stage its own step:
install + authenticate the CLI -> artifacts -> generate -> visual -> generate auth -> functional
Generation is subscription-gated, so the runner authenticates with an APIMatic account. Two are configured, and a manual run picks between them:
| Secret | Used when |
|---|---|
APIMATIC_AUTH_KEY_DEV |
default — every push and PR, and manual runs left on dev |
APIMATIC_AUTH_KEY_PROD |
manual runs with environment: prod |
Routine CI stays on dev so it never spends against the production account. The
choice matters beyond billing: the two accounts have their own subscriptions, so
which one runs decides which languages are allowed, and therefore which SDK
artifacts the generator is even able to produce. That's why the workflow prints
apimatic auth status — a subscription that doesn't cover a declared language
would explain a missing archive.
Run against prod from the Actions tab, or:
gh workflow run portal-tests.yml --repo apimatic/v4-test-automation -f environment=prodThe key is checked before login, so a missing one fails with a message naming the exact secret rather than an opaque CLI error several steps later. Reports and failure output are named per environment, so a dev and a prod run don't collide.
The CLI version is pinned in the workflow (APIMATIC_CLI_VERSION) so a new
release can't silently change the generated output and move every baseline. The
workflow_dispatch inputs let you try another CLI version, or another build,
without committing to either.
/components/mermaid/crashes at runtime. It serves HTTP 200, but the page throwsExpected component 'Mermaid' to be defined(React error #419) and the shell never mounts. Generation reports no error. The route is left out ofconfig/portals.tsrather than left failing — add it back once the component is provided.- Declared SDK versions don't reach the manifests.
apimatic.jsondeclares typescript2.4.0, python1.0.3and csharp3.1.2; the archives ship1.0.26,1.0.26and no<Version>element at all. Package names are correct in all three. The check is markedtest.fail()— it keeps reporting the mismatch without turning CI red for everything else, and Playwright flags it the moment it starts passing. Remove that line when it's fixed.