Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
278 changes: 275 additions & 3 deletions README.md

Large diffs are not rendered by default.

16 changes: 16 additions & 0 deletions cspell.json
Original file line number Diff line number Diff line change
Expand Up @@ -12,25 +12,41 @@
],
"words": [
"autonumber",
"behaviour",
"bunx",
"callsign",
"callsigns",
"capturable",
"chromium",
"codegen",
"domcontentloaded",
"elysiumoss",
"ffmpeg",
"geofence",
"libvpx",
"libwebp",
"multidomain",
"networkidle",
"normalises",
"Odnis",
"playwright",
"RESQ",
"routerlink",
"screenshotted",
"screenshotting",
"slugified",
"slugifies",
"slugifying",
"subdirs",
"swiftshader",
"toolkits",
"tsdown",
"ultrawide",
"ungated",
"unparseable",
"unreviewable",
"unseed",
"untargeted",
"viewports",
"webm",
"webp"
Expand Down
7 changes: 5 additions & 2 deletions knip.json
Original file line number Diff line number Diff line change
@@ -1,8 +1,11 @@
{
"$schema": "https://unpkg.com/knip@5/schema.json",
"tsdown": false,
"entry": ["src/index.ts", "src/cli.ts", "src/**/*.test.ts"],
"project": ["src/**/*.ts"],
"includeEntryExports": false,
"ignoreExportsUsedInFile": {
"interface": true,
"type": true
},
"project": ["src/**/*.ts"]
}
}
82 changes: 82 additions & 0 deletions src/docs.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,82 @@
/**
*
* Copyright 2026 Mike Odnis
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
*/

import { readFileSync } from "node:fs";
import { describe, expect, it } from "vitest";
import { USAGE } from "./runner.js";
import { CaptureConfig, CaptureStep } from "./schemas.js";

/**
* Documentation drift is the defect class that survives every other test: the
* flag works, the schema is right, and only the README lies. These assertions
* make `--help`, the README and the schema fail together.
*/
const README = readFileSync(new URL("../README.md", import.meta.url), "utf8");

describe("README ↔ USAGE", () => {
it("reproduces the whole option and example list verbatim", () => {
// The README drops only USAGE's leading description paragraph, since the
// page already carries one; everything from `Arguments:` on must match.
const body = USAGE.slice(USAGE.indexOf("Arguments:"));
expect(README).toContain(body.trimEnd());
});

it("shows the scripted-state flags in both", () => {
const flags = [
"--states",
"--state-filter",
"--skip-routes",
"--state-timeout",
"--precondition-timeout",
"--allow-state-requests",
"--fail-on-state-error",
];
for (const flag of flags) {
expect(USAGE, `USAGE is missing ${flag}`).toContain(flag);
expect(README, `README is missing ${flag}`).toContain(flag);
}
});
});

describe("README ↔ schema", () => {
it("documents every step kind in the vocabulary table", () => {
const kinds = CaptureStep.members.map(
(member) => member.fields.kind.literals[0],
);
expect(kinds.length).toBeGreaterThan(0);
for (const kind of kinds) {
expect(README, `README does not document the "${kind}" step`).toContain(
`| \`${kind}\` |`,
);
}
});

it("documents every scripted-state config field with its default", () => {
const rows: ReadonlyArray<readonly [string, string]> = [
["states", "`[]`"],
["stateTimeout", `\`${CaptureConfig.Default.stateTimeout}\``],
[
"preconditionTimeout",
`\`${CaptureConfig.Default.preconditionTimeout}\``,
],
["captureRoutes", `\`${CaptureConfig.Default.captureRoutes}\``],
["allowStateRequests", `\`${CaptureConfig.Default.allowStateRequests}\``],
];
for (const [field, rendered] of rows) {
const row = README.split("\n").find((line) =>
line.startsWith(`| \`${field}\``),
);
expect(row, `README has no config row for ${field}`).toBeDefined();
expect(row, `${field} row does not show ${rendered}`).toContain(rendered);
}
});
});
42 changes: 42 additions & 0 deletions src/errors.ts
Original file line number Diff line number Diff line change
Expand Up @@ -42,3 +42,45 @@ export class FileSystemError extends S.TaggedError<FileSystemError>()(
cause: S.Unknown,
},
) {}

/**
* A scripted state that could never have worked: a duplicate or unknown name,
* an `extends` cycle, an off-host `request` path, a viewport filter naming a
* viewport that is not configured.
*
* Definition errors are raised before Chromium launches and abort the run,
* because no amount of retrying makes a typo'd `extends` resolve. Runtime
* problems are {@link StateCaptureError} instead, and are recorded per state
* so one broken script cannot abort a capture run.
*/
export class StateDefinitionError extends S.TaggedError<StateDefinitionError>()(
"StateDefinitionError",
{
state: S.String,
message: S.String,
cause: S.Unknown,
},
) {}

/**
* A scripted state that failed while running: a selector that never appeared,
* an action that threw, a seeding request that returned the wrong status, or
* the whole state exceeding its budget.
*
* `stepIndex: -1` denotes a failure that no step is answerable for, with
* `stepKind` naming which one: `"state"` for navigation or the whole-state
* budget, `"precondition"` for a probe that could not be evaluated at all —
* a malformed selector, a page that went away. A precondition that is simply
* *absent* is not a failure and produces no error; the state is `skipped`.
*/
export class StateCaptureError extends S.TaggedError<StateCaptureError>()(
"StateCaptureError",
{
state: S.String,
stepIndex: S.Number,
stepKind: S.String,
target: S.String,
message: S.String,
cause: S.Unknown,
},
) {}
25 changes: 25 additions & 0 deletions src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,8 @@ export {
BrowserError,
CaptureError,
FileSystemError,
StateCaptureError,
StateDefinitionError,
} from "./errors.js";
// CLI runner (also useful for embedding the CLI in another tool)
export { buildInvocation, parseCliArgs, runFromArgs, USAGE } from "./runner.js";
Expand All @@ -30,11 +32,24 @@ export {
type CaptureConfigOverrides,
CaptureReport,
CaptureResult,
CaptureState,
type CaptureStateInput,
CaptureStep,
ClickStep,
createCaptureConfig,
FillStep,
PressStep,
ReloadStep,
RequestStep,
ScreenshotPaths,
SelectStep,
StateStatus,
StatesFile,
VideoOptions,
VideoQualityPaths,
ViewportConfig,
WaitForStep,
WaitStep,
} from "./schemas.js";
// Service / DI
// Default export for convenience
Expand All @@ -44,3 +59,13 @@ export {
UICaptureService,
UICaptureService as default,
} from "./service.js";
// Scripted states: parsing, chain resolution and pre-launch validation
export {
filterStates,
MAX_STATE_CHAIN_DEPTH,
parseStatesFile,
type ResolvedState,
resolveStateSteps,
statesUseRequests,
validateStates,
} from "./states.js";
Loading
Loading