playwright-mutation-gate 0.1.0 → 0.2.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -166,7 +166,35 @@ Options:
166
166
  -h, --help display help for command
167
167
  ```
168
168
 
169
- The package is CLI-only for now. Programmatic API is planned for v0.2.
169
+ ## Programmatic API
170
+
171
+ Since v0.2 the package also exposes a library entry, so a tool can run the gate
172
+ in-process and inspect the structured report instead of shelling out to the CLI
173
+ and parsing `--json`.
174
+
175
+ ```ts
176
+ import { runSpec, summarize } from 'playwright-mutation-gate';
177
+
178
+ const spec = await runSpec('tests/checkout.spec.ts', {
179
+ testIds: ['authedTest'], // extra test builders, same as --test-id
180
+ playwrightArgs: ['--project=chromium', '--retries=0'],
181
+ });
182
+ const summary = summarize([spec], { requireSentinel: true });
183
+
184
+ if (!summary.gatePassed) {
185
+ const hollow = spec.results.filter((r) => r.verdict === 'survived');
186
+ console.error(`${hollow.length} assertion(s) do not gate the result`);
187
+ process.exit(1);
188
+ }
189
+ ```
190
+
191
+ `runSpec(specPath, opts?)` returns a `SpecResult` (per-assertion `verdict`:
192
+ `killed` / `survived` / `cannot-mutate` / `error`, plus unmarked/stray/malformed
193
+ markers). `summarize(specs, opts)` folds one or more results into a `Summary`
194
+ with the `gatePassed` verdict. `jsonReport` and `renderTable` produce the CLI's
195
+ two output formats. Advanced: `planSpecMutations` and `invertAssertLine` expose
196
+ the mutation planner and the single-line inverter directly. Full types ship with
197
+ the package.
170
198
 
171
199
  ## Limitations
172
200
 
package/dist/cli.d.ts ADDED
@@ -0,0 +1,2 @@
1
+ #!/usr/bin/env node
2
+ export {};
@@ -0,0 +1,4 @@
1
+ export { runSpec, sweepStaleMutationFiles, DEFAULT_TIMEOUT_MS, type SpecResult, type MutationResult, type Verdict, type RunOptions, } from './run.js';
2
+ export { summarize, jsonReport, renderTable, type Summary, type GateOptions, type RenderOptions, } from './report.js';
3
+ export { planSpecMutations, MARKER, type SpecPlan, type Mutation, type PlanOptions, } from './mutate.js';
4
+ export { invertAssertLine, type InvertResult } from './invert.js';
package/dist/index.js ADDED
@@ -0,0 +1,15 @@
1
+ // Programmatic entry point.
2
+ //
3
+ // The CLI (`dist/cli.js`) is one consumer of this library; another is any tool
4
+ // that wants to run the gate in-process and inspect the report itself — e.g. a
5
+ // custom test-pipeline that already knows which specs changed and wants the
6
+ // structured `SpecResult`/`Summary` without shelling out and parsing `--json`.
7
+ //
8
+ // Curated surface: run a spec, summarize/render the results, and (advanced) plan
9
+ // or invert individual mutations. The tokenizer (`lex.ts`) and the on-disk
10
+ // mutant-file mechanics stay internal — `runSpec` orchestrates them for you.
11
+ export { runSpec, sweepStaleMutationFiles, DEFAULT_TIMEOUT_MS, } from './run.js';
12
+ export { summarize, jsonReport, renderTable, } from './report.js';
13
+ // Advanced: build or inspect the mutation plan directly.
14
+ export { planSpecMutations, MARKER, } from './mutate.js';
15
+ export { invertAssertLine } from './invert.js';
@@ -0,0 +1,16 @@
1
+ export type InvertResult = {
2
+ ok: string;
3
+ } | {
4
+ cannotMutate: string;
5
+ };
6
+ /**
7
+ * Invert the assertion on a single line of a Playwright spec:
8
+ * `expect(x).toY()` <-> `expect(x).not.toY()`.
9
+ *
10
+ * Purely string-based. Supported chain shape:
11
+ * `[await] expect[.soft|.poll](...) [.resolves|.rejects] [.not] .toXxx(`
12
+ * The matcher call must start on this line; its arguments may continue on
13
+ * following lines. Everything else returns `cannotMutate` with a reason
14
+ * rather than guessing.
15
+ */
16
+ export declare function invertAssertLine(line: string): InvertResult;
package/dist/lex.d.ts ADDED
@@ -0,0 +1,23 @@
1
+ /**
2
+ * Shared single-pass lexer for the string-based spec analysis. Classifies
3
+ * every char of a source (a whole file or a single line) so the scanners in
4
+ * invert.ts and mutate.ts agree about what is code. One implementation on
5
+ * purpose: two hand-rolled lexers had already diverged once.
6
+ */
7
+ /** Char inside a string, template literal, block comment or regex literal. */
8
+ export declare const NON_CODE = 0;
9
+ /** Executable source. */
10
+ export declare const CODE = 1;
11
+ /** Char of a `//` comment that starts in code context. */
12
+ export declare const LINE_COMMENT = 2;
13
+ /**
14
+ * Classify every char of `src` as CODE, LINE_COMMENT or NON_CODE. Handles
15
+ * strings with escapes, template literals with nested `${}` expressions,
16
+ * `//` and block comments, and regex literals (heuristic: see startsRegex).
17
+ */
18
+ export declare function lex(src: string): Uint8Array;
19
+ /**
20
+ * Find the `)` matching the `(` at `open`, counting only CODE chars.
21
+ * Returns -1 when it does not close within `src`.
22
+ */
23
+ export declare function findCloseMasked(src: string, kind: Uint8Array, open: number): number;
@@ -0,0 +1,66 @@
1
+ export declare const MARKER = "// @primary-assert";
2
+ export type Mutation = {
3
+ testTitle: string;
4
+ /** 1-based line of the test() declaration, for file:line targeting. */
5
+ testLine: number;
6
+ /** 1-based line of the `// @primary-assert` marker. */
7
+ markerLine: number;
8
+ /** 1-based line of the assertion that was inverted. */
9
+ assertLine: number;
10
+ /** Full spec source with exactly this one assertion inverted. */
11
+ mutatedSource: string;
12
+ } | {
13
+ testTitle: string;
14
+ testLine: number;
15
+ markerLine: number;
16
+ /** 1-based line of the assertion candidate; null when none was located. */
17
+ assertLine: number | null;
18
+ cannotMutate: string;
19
+ };
20
+ export interface PlanOptions {
21
+ /**
22
+ * Extra identifiers to treat as test builders, on top of `test` and the
23
+ * auto-detected `.extend` chains and renamed imports. For custom fixtures
24
+ * imported by their own name from another module (`import { authedTest }
25
+ * from './fixtures'`), which cannot be detected without resolving the import.
26
+ */
27
+ testIds?: string[];
28
+ }
29
+ export interface SpecPlan {
30
+ /** One entry per marked test: either a mutated copy or a cannot-mutate reason. */
31
+ mutations: Mutation[];
32
+ /** Runnable tests with no marker (sentinel accounting). skip/fixme/fail are exempt. */
33
+ unmarkedTests: {
34
+ title: string;
35
+ line: number;
36
+ }[];
37
+ /** 1-based lines of valid markers that sit outside every test() block. */
38
+ strayMarkerLines: number[];
39
+ /**
40
+ * 1-based lines of test.only() declarations. Any .only filters out every
41
+ * other test in the file, so the runner refuses the whole spec.
42
+ */
43
+ onlyTestLines: number[];
44
+ /** 1-based lines mentioning @primary-assert that do not form a valid marker. */
45
+ malformedMarkerLines: number[];
46
+ }
47
+ /**
48
+ * Parse a spec source and plan its mutations: one inverted copy per marked
49
+ * test. Anything the gate cannot honestly mutate is reported with a reason,
50
+ * never skipped silently.
51
+ */
52
+ export declare function planSpecMutations(source: string, opts?: PlanOptions): SpecPlan;
53
+ /**
54
+ * Name for a mutated copy: the marker segment goes right after the first
55
+ * dot-segment of the basename, so the ENTIRE original tail is preserved and
56
+ * the copy matches whatever testMatch convention picked up the original
57
+ * (`checkout.e2e.ts` -> `checkout.pmg-mutation-0.e2e.ts`).
58
+ */
59
+ export declare function mutationFilePath(specPath: string, index: number, tag?: string): string;
60
+ /**
61
+ * Write the mutated copy next to the original spec (same dir keeps relative
62
+ * imports and the Playwright testDir working) and return its path. The name
63
+ * carries the process id so overlapping gate runs in one checkout cannot
64
+ * collide. The caller owns cleanup.
65
+ */
66
+ export declare function writeMutationFile(specPath: string, index: number, mutatedSource: string): string;
@@ -0,0 +1,31 @@
1
+ import type { SpecResult } from './run.js';
2
+ export interface GateOptions {
3
+ /** When true (default), tests without a @primary-assert marker fail the gate. */
4
+ requireSentinel: boolean;
5
+ }
6
+ export interface Summary {
7
+ killed: number;
8
+ survived: number;
9
+ cannotMutate: number;
10
+ errors: number;
11
+ unmarkedTests: number;
12
+ strayMarkers: number;
13
+ malformedMarkers: number;
14
+ /**
15
+ * Green means proven: every marked assertion was killed under inversion and
16
+ * nothing was left unverified. Any survived, cannot-mutate or error verdict
17
+ * fails the gate, as do marker mistakes (stray or malformed markers) and,
18
+ * under require-sentinel, tests with no marker at all.
19
+ */
20
+ gatePassed: boolean;
21
+ }
22
+ export declare function summarize(specs: SpecResult[], opts: GateOptions): Summary;
23
+ export declare function jsonReport(specs: SpecResult[], opts: GateOptions): string;
24
+ export interface RenderOptions extends GateOptions {
25
+ color: boolean;
26
+ }
27
+ /**
28
+ * Human-readable report: per spec, one row per verdict plus sentinel findings,
29
+ * then a one-line summary and the gate outcome.
30
+ */
31
+ export declare function renderTable(specs: SpecResult[], opts: RenderOptions): string;
package/dist/run.d.ts ADDED
@@ -0,0 +1,68 @@
1
+ import { type Mutation, type SpecPlan } from './mutate.js';
2
+ export type Verdict = 'killed' | 'survived' | 'cannot-mutate' | 'error';
3
+ export interface MutationResult {
4
+ testTitle: string;
5
+ testLine: number;
6
+ /** 1-based line of the mutated assertion; null when none was located. */
7
+ assertLine: number | null;
8
+ verdict: Verdict;
9
+ /** Present for cannot-mutate and error verdicts. */
10
+ reason?: string;
11
+ }
12
+ export interface SpecResult {
13
+ specPath: string;
14
+ results: MutationResult[];
15
+ unmarkedTests: SpecPlan['unmarkedTests'];
16
+ strayMarkerLines: number[];
17
+ malformedMarkerLines: number[];
18
+ }
19
+ export interface RunOptions {
20
+ /** Directory Playwright is spawned from; defaults to process.cwd(). */
21
+ cwd?: string;
22
+ /** Hard cap for one mutation run; the child is killed past it. Default 5 min. */
23
+ timeoutMs?: number;
24
+ /** Extra args appended to every `playwright test` invocation. */
25
+ playwrightArgs?: string[];
26
+ /** Command that launches Playwright; default ['npx', 'playwright', 'test']. */
27
+ playwrightCommand?: string[];
28
+ /** Extra identifiers to treat as test builders (custom fixtures). */
29
+ testIds?: string[];
30
+ }
31
+ export declare const DEFAULT_TIMEOUT_MS: number;
32
+ type RunVerdict = {
33
+ verdict: 'killed' | 'survived';
34
+ } | {
35
+ verdict: 'cannot-mutate';
36
+ reason: string;
37
+ } | {
38
+ verdict: 'error';
39
+ reason: string;
40
+ };
41
+ /**
42
+ * Decide a verdict from a Playwright JSON report. The exit code alone cannot
43
+ * be trusted: Playwright exits 1 for a failed test, for "no tests found" and
44
+ * for many crashes alike, and only the report tells those apart.
45
+ */
46
+ export declare function verdictFromReport(raw: string): RunVerdict;
47
+ /**
48
+ * Run one planned mutation: write the inverted copy next to the spec, run
49
+ * Playwright against exactly that copy and test line, and read the verdict
50
+ * from the JSON report. The copy and the report directory are always removed.
51
+ */
52
+ export declare function runMutation(specPath: string, index: number, mutation: Extract<Mutation, {
53
+ mutatedSource: string;
54
+ }>, opts?: RunOptions): Promise<RunVerdict>;
55
+ /**
56
+ * Delete leftover mutation copies in a directory: files whose pid token
57
+ * points at a process that no longer exists. Copies of live concurrent gate
58
+ * runs are left alone. Returns the paths that were removed.
59
+ */
60
+ export declare function sweepStaleMutationFiles(dir: string): string[];
61
+ /**
62
+ * Gate one spec file end to end: plan its mutations, sweep stale copies from
63
+ * earlier crashed runs, run every mutable mutation and collect verdicts.
64
+ * A spec containing test.only is refused outright: .only filters out every
65
+ * other test, so verdicts from such a run would be fiction.
66
+ */
67
+ export declare function runSpec(specPath: string, opts?: RunOptions): Promise<SpecResult>;
68
+ export {};
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "playwright-mutation-gate",
3
- "version": "0.1.0",
3
+ "version": "0.2.1",
4
4
  "description": "Mutation testing for Playwright assertions: invert each test's primary assertion and fail the run if the test stays green.",
5
5
  "keywords": [
6
6
  "playwright",
@@ -16,6 +16,15 @@
16
16
  "license": "MIT",
17
17
  "author": "Vladyslav Dmitriiev <vladyslav.dmitriiev@pm.me>",
18
18
  "type": "module",
19
+ "main": "dist/index.js",
20
+ "types": "dist/index.d.ts",
21
+ "exports": {
22
+ ".": {
23
+ "types": "./dist/index.d.ts",
24
+ "default": "./dist/index.js"
25
+ },
26
+ "./package.json": "./package.json"
27
+ },
19
28
  "bin": {
20
29
  "playwright-mutation-gate": "dist/cli.js"
21
30
  },