@recursica/adapter-tester 3.0.0 → 5.0.0

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.
Files changed (40) hide show
  1. package/README.md +47 -27
  2. package/dist/adapter-tester.schema.json.d.ts +36 -15
  3. package/dist/cli.cjs +41 -20
  4. package/dist/cli.cjs.map +1 -1
  5. package/dist/cli.js +344 -218
  6. package/dist/cli.js.map +1 -1
  7. package/dist/config.d.ts +38 -14
  8. package/dist/config.d.ts.map +1 -1
  9. package/dist/devServer.d.ts +1 -1
  10. package/dist/devServer.d.ts.map +1 -1
  11. package/dist/fileConfig.d.ts +25 -10
  12. package/dist/fileConfig.d.ts.map +1 -1
  13. package/dist/golden/diffPng.d.ts +12 -7
  14. package/dist/golden/diffPng.d.ts.map +1 -1
  15. package/dist/golden/manifestStore.d.ts +19 -1
  16. package/dist/golden/manifestStore.d.ts.map +1 -1
  17. package/dist/harness/mantineSourceOfTruth.d.ts +10 -1
  18. package/dist/harness/mantineSourceOfTruth.d.ts.map +1 -1
  19. package/dist/index.cjs +1 -1
  20. package/dist/index.cjs.map +1 -1
  21. package/dist/index.js +1 -1
  22. package/dist/index.js.map +1 -1
  23. package/dist/{mantineSourceOfTruth-BUkqMNEo.js → mantineSourceOfTruth-DE7XOgea.js} +36 -34
  24. package/dist/mantineSourceOfTruth-DE7XOgea.js.map +1 -0
  25. package/dist/{mantineSourceOfTruth-BUb5MZy0.cjs → mantineSourceOfTruth-Dpe4mlmF.cjs} +6 -6
  26. package/dist/mantineSourceOfTruth-Dpe4mlmF.cjs.map +1 -0
  27. package/dist/portDiscovery.d.ts +39 -0
  28. package/dist/portDiscovery.d.ts.map +1 -0
  29. package/dist/testing/runVisualRegression.d.ts +69 -20
  30. package/dist/testing/runVisualRegression.d.ts.map +1 -1
  31. package/dist/testing.cjs +4 -3
  32. package/dist/testing.cjs.map +1 -1
  33. package/dist/testing.d.ts +1 -1
  34. package/dist/testing.d.ts.map +1 -1
  35. package/dist/testing.js +286 -174
  36. package/dist/testing.js.map +1 -1
  37. package/package.json +2 -6
  38. package/src/adapter-tester.schema.json +36 -15
  39. package/dist/mantineSourceOfTruth-BUb5MZy0.cjs.map +0 -1
  40. package/dist/mantineSourceOfTruth-BUkqMNEo.js.map +0 -1
package/README.md CHANGED
@@ -12,9 +12,9 @@ This package provides three primary utilities:
12
12
  2. **Interactive Dev Mode:** A side-by-side synchronized browser environment with built-in note taking for auditing components and feeding fixes directly to an AI agent.
13
13
  3. **An installable devDependency**, wired into `@recursica/mantine-adapter` — the source-of-truth adapter, run in `isSourceOfTruthAdapter` mode (own-drift check only, since it has nothing above it to diverge from). Any other adapter repo — including one that never checked out this monorepo — can install it the same way. See [Using this package in another adapter repo](#using-this-package-in-another-adapter-repo).
14
14
 
15
- **Default mode** (used by any adapter repo installing this package): checks this project's own Storybook — the one already defined by its `storybook` npm script — against its own committed golden images, and flags divergence from the source-of-truth adapter's (`@recursica/mantine-adapter`) own golden images. No monorepo checkout, no manual config, in most cases no config file at all.
15
+ **Default mode** (used by any adapter repo installing this package): checks this project's own Storybook — the one already defined by its `storybook` npm script — against its own committed golden images. No monorepo checkout, no manual config, in most cases no config file at all. Divergence from the source-of-truth adapter's (`@recursica/mantine-adapter`) own golden images is a separate, opt-in check see [`--divergence-only`](#automated-visual-tests).
16
16
 
17
- **Non-standard mode**: this monorepo also runs adapter-tester against itself, pointed at an adapter's and `@recursica/mantine-adapter`'s local sibling workspace packages so the divergence check sees local, uncommitted `mantine-adapter` golden changes before they're published, not whatever was last published to npm. See `adapter-tester.config.json` in this package for that config; it's the same CLI, just pointed at `sourceOfTruth.type: "url"` instead of the default Mantine harness.
17
+ `@recursica/adapter-tester` itself has no default mode and no `adapter-tester.config.json` of its ownit's purely a tool other adapters install and configure. `mantine-adapter` and `mui-adapter` are the ones that run it, each with their own config.
18
18
 
19
19
  ---
20
20
 
@@ -26,13 +26,15 @@ Installing `@recursica/adapter-tester` as a devDependency (see [below](#using-th
26
26
  {
27
27
  "scripts": {
28
28
  "adapter-tester": "adapter-tester --serve",
29
- "adapter-tester:automated": "adapter-tester"
29
+ "adapter-tester:automated": "adapter-tester",
30
+ "adapter-tester:source-of-truth": "adapter-tester --divergence-only"
30
31
  }
31
32
  }
32
33
  ```
33
34
 
34
35
  - `npm run adapter-tester` — Interactive Dev Mode: boots both Storybooks (reusing them if already running) and opens the synced side-by-side comparison browser.
35
- - `npm run adapter-tester:automated` — the headless Playwright pixel-diff suite; see [Automated Visual Tests](#automated-visual-tests) below.
36
+ - `npm run adapter-tester:automated` — the headless own-drift check against this project's own committed goldens; see [Automated Visual Tests](#automated-visual-tests) below. This is the one to run normally — fast, no network calls.
37
+ - `npm run adapter-tester:source-of-truth` — the separate, opt-in divergence check against the source-of-truth adapter's published goldens. Not wired up for `@recursica/mantine-adapter` itself — it _is_ the source of truth, so it has nothing to diverge from.
36
38
 
37
39
  Both boot Storybooks themselves — nothing needs to be running beforehand.
38
40
 
@@ -76,10 +78,11 @@ The Dev Mode server fetches this file dynamically, so any changes you make to `r
76
78
 
77
79
  ## Automated Visual Tests
78
80
 
79
- To execute a full headless visual regression scan across all components:
81
+ Two independent checks, gated by `--divergence-only` the normal, everyday one and a separate opt-in one:
80
82
 
81
83
  ```bash
82
- npm run adapter-tester:automated # runs the `adapter-tester` CLI
84
+ npm run adapter-tester:automated # own-drift check only — the normal one to run
85
+ npm run adapter-tester:source-of-truth # divergence check only — opt-in, against Mantine
83
86
  ```
84
87
 
85
88
  `adapter-tester` reads `adapter-tester.config.json` from the current directory (or falls back to defaults if the file doesn't exist), generates a throwaway Playwright config + spec under `.adapter-tester/run/` (git-ignore this directory), and runs it. There's no Playwright config or spec file to hand-author.
@@ -92,14 +95,23 @@ Any other args are forwarded to `playwright test`, so you can scope a run to spe
92
95
  npm run adapter-tester:automated -- --grep "Toast"
93
96
  ```
94
97
 
98
+ Pin the source-of-truth adapter's version for the divergence check (instead of resolving `@recursica/mantine-adapter`'s `latest` on npm) with `--source-of-truth-version`:
99
+
100
+ ```bash
101
+ npm run adapter-tester:source-of-truth -- --source-of-truth-version 0.53.0
102
+ ```
103
+
104
+ Only meaningful with the default `sourceOfTruth.type: "mantine-harness"` mode — throws if combined with `sourceOfTruth.type: "url"` (that mode reads a sibling package's local checkout directly, there's no version to pin) or with `isSourceOfTruthAdapter: true` (nothing to pin a version for).
105
+
95
106
  ### What happens during execution?
96
107
 
97
108
  1. **Automatic Storybook Bootup**: `adapter-tester` boots only this project's own Storybook — the divergence check reads the source-of-truth adapter's stored golden files, never a live Storybook, so nothing else needs to boot.
98
109
  2. **Story Discovery**: The test suite fetches this project's own Storybook index (`/index.json`) to dynamically discover and parameterize tests for every component story, excluding `@recursica/storybook-template`'s own default token/theme demo stories (`Theme/*`, `Tokens/*`) and each adapter's own onboarding stories (`Introduction/*`).
99
- 3. **Headless Snapshot**: It launches headless Chrome, navigates to the isolated iframe view, and takes a screenshot.
100
- 4. **Own-Drift Check**: Diffs the screenshot against this project's own `test/golden/<story-id>.png` using `pixelmatch`, against the configured mismatch threshold. No golden yet for a story is not a failure — one is captured from this run.
101
- 5. **Divergence Check**: Diffs this project's own golden against the source-of-truth adapter's golden (skipped for the source-of-truth adapter's own config). Different and not yet reviewed via `--approve-divergence`? Flagged as a report annotation never a failure on its own.
102
- 6. **Native Report Generation**: Screenshots, diff overlays, and divergence flags are embedded directly as test attachments/annotations.
110
+ 3. **Headless Snapshot**: It launches headless Chrome, navigates to the isolated iframe view, and takes a screenshot. A missing golden is captured from this screenshot regardless of which check is running.
111
+ 4. **Own-Drift Check** (`adapter-tester:automated`): Diffs the screenshot against this project's own `test/golden/<story-id>.png` using `pixelmatch`, against `goldenThresholdPixels`. No golden yet for a story is not a failure — one is captured from this run.
112
+ 5. **Divergence Check** (`adapter-tester:source-of-truth`): Diffs this project's own golden against the source-of-truth adapter's golden (skipped for the source-of-truth adapter's own config), against `sourceOfTruthThresholdPixels`. Past that threshold and not yet reviewed via `--approve-divergence`? Flagged as a report annotation and fails the run.
113
+ 6. **Story Parity Check** (`adapter-tester:source-of-truth` only): Compares this project's live Storybook story ids against every id the source-of-truth adapter has a golden for. A source-of-truth story this adapter hasn't built at all — and hasn't marked `exclude: true` under `stories` — fails the run as a single `story parity with source of truth` test. The reverse (a story only this adapter has) is never flagged.
114
+ 7. **Native Report Generation**: Screenshots, diff overlays, and divergence flags are embedded directly as test attachments/annotations.
103
115
 
104
116
  ### Output & Reports
105
117
 
@@ -119,7 +131,7 @@ All test outcomes and visual outputs are compiled into the standard, git-ignored
119
131
  Each adapter's `test/golden/` directory — committed to git, alongside a `manifest.json` tracking when each was captured — is its own baseline:
120
132
 
121
133
  - **Own-drift check (hard fail):** this run's live render vs this project's own golden. Catches unintended CSS regressions.
122
- - **Divergence check (soft flag, never fails a run):** this project's own golden vs the source-of-truth adapter's golden — different underlying UI libraries can legitimately render a component differently, so a divergence isn't automatically wrong, just unreviewed.
134
+ - **Divergence check (hard fail):** this project's own golden vs the source-of-truth adapter's golden — different underlying UI libraries can legitimately render a component differently, so review a flagged divergence and either fix the styling or `--approve-divergence` it as intentional.
123
135
 
124
136
  These are explicit, developer-run workflows — not run in CI, and not scoped down automatically; use `--grep` to target specific stories.
125
137
 
@@ -129,8 +141,9 @@ These are explicit, developer-run workflows — not run in CI, and not scoped do
129
141
  npm run adapter-tester:automated -- --grep "Toast" --update-golden
130
142
 
131
143
  # Review a flagged divergence from the source of truth, and accept it as
132
- # intentional. Also (re)captures the own golden from the current live render.
133
- npm run adapter-tester:automated -- --grep "Toast" --approve-divergence
144
+ # intentional. Only valid alongside --divergence-only there's nothing to
145
+ # approve without the divergence check running.
146
+ npm run adapter-tester:source-of-truth -- --grep "Toast" --approve-divergence
134
147
  ```
135
148
 
136
149
  `--approve-divergence` records the source-of-truth adapter's own `manifest.json` `createdAt` for that story at review time. If the source of truth's golden is recaptured later, the divergence flags again for re-review — approval isn't a permanent exemption.
@@ -145,30 +158,36 @@ Renamed/removed stories just leave an orphaned `test/golden/<story-id>.png`/mani
145
158
 
146
159
  ## Configuring `adapter-tester.config.json`
147
160
 
148
- All fields are optional — an adapter repo with a normal `storybook` npm script (e.g. `storybook dev -p 6012`) needs no config file at all.
161
+ All fields are optional — an adapter repo with a normal `storybook` npm script (e.g. `storybook dev`, no fixed port) needs no config file at all.
149
162
 
150
163
  Every `adapter-tester.config.json` is validated against [`src/adapter-tester.schema.json`](./src/adapter-tester.schema.json) before a run — unknown fields, wrong types, and invalid `sourceOfTruth` shapes fail fast with a specific error instead of silently doing the wrong thing. Point your editor at it for autocomplete/inline docs:
151
164
 
152
165
  ```json
153
166
  {
154
- "$schema": "./node_modules/@recursica/adapter-tester/src/adapter-tester.schema.json",
167
+ "$schema": "https://raw.githubusercontent.com/borderux/recursica/main/packages/adapter-tester/src/adapter-tester.schema.json",
155
168
  "name": "MyAdapter",
156
169
  "storybook": { "port": 6006, "command": "npm run storybook" },
157
170
  "sourceOfTruth": { "type": "mantine-harness" },
158
- "diffThresholdPixels": 3500,
159
- "storyThresholds": { "ui-kit-slider": 15000 },
160
- "excludeTitlePrefixes": ["Theme", "Tokens", "Introduction"],
161
- "excludeStoryIds": ["ui-kit-slider--range-mode-with-icons-and-inputs"]
171
+ "goldenThresholdPixels": 10,
172
+ "sourceOfTruthThresholdPixels": 3500,
173
+ "stories": {
174
+ "ui-kit-slider": { "sourceOfTruthThreshold": 15000 },
175
+ "ui-kit-slider--range-mode-with-icons-and-inputs": { "exclude": true }
176
+ },
177
+ "excludeTitlePrefixes": ["Theme", "Tokens", "Introduction"]
162
178
  }
163
179
  ```
164
180
 
165
181
  - `name` — label for this project's own target. Defaults to the unscoped `package.json` name.
166
- - `storybook.port` — defaults to whatever `-p`/`--port` is set on this project's own `storybook` script; falls back to Storybook's default of `6006`.
167
- - `storybook.command`/`storybook.cwd` — default to `npm run storybook` in the current directory.
168
- - `sourceOfTruth` — defaults to `{ "type": "mantine-harness" }`: a throwaway harness that installs the _published_ `@recursica/mantine-adapter` from npm, so you never need this monorepo checked out. This is the standard mode every external adapter repo uses.
169
- - `sourceOfTruth.type: "url"` — the **non-standard** mode: points at an already-addressable Storybook via `command`/`port`/`cwd` instead of the harness, and reads that sibling package's `test/golden/` directly from disk for the divergence check (including uncommitted local changes) instead of resolving a published npm version. Used inside this monorepo see `adapter-tester.config.json` in this package.
182
+ - `storybook.port` — a first-guess only, not authoritative. Storybook is never pinned to it — the real port is auto-detected from Storybook's own startup output, since Storybook silently falls back to an OS-assigned port whenever its default/configured one is taken. Defaults to whatever `-p`/`--port` is set on this project's own `storybook` script, falling back to `6006`.
183
+ - `storybook.command`/`storybook.cwd` — default to `npm run storybook` in the current directory. Leave `-p`/`--port` off the script entirely unless you specifically need a fixed port — the tool doesn't need one.
184
+ - `sourceOfTruth` — defaults to `{ "type": "mantine-harness" }`: a throwaway harness that installs the _published_ `@recursica/mantine-adapter` from npm, so you never need this monorepo checked out. This is the standard mode every external adapter repo uses. `sourceOfTruth.mantineAdapterVersion` pins the version installed/fetched (defaults to `latest`) — or override it per-run with `--source-of-truth-version` instead of hardcoding it in the config.
185
+ - `sourceOfTruth.type: "url"` — the **non-standard** mode: points at an already-addressable Storybook via `command`/`port`/`cwd` instead of the harness, and reads that sibling package's `test/golden/` directly from disk for the divergence check (including uncommitted local changes) instead of resolving a published npm version. For comparing two locally checked-out sibling adapters instead of a published one. `port` here is the same first-guess-only hint as `storybook.port` above.
170
186
  - `isSourceOfTruthAdapter` — set `true` only in the source-of-truth adapter's own config (`@recursica/mantine-adapter`). Skips `sourceOfTruth`/the divergence check entirely; runs the own-drift check standalone. Also disables Dev Mode (`--serve`) — there's nothing to sync against.
171
- - `diffThresholdPixels`/`storyThresholds`/`excludeTitlePrefixes`/`excludeStoryIds` — same meaning as before; see `src/adapter-tester.schema.json` for full docs.
187
+ - `goldenThresholdPixels` — global threshold for the own-drift check (this project's live render vs. its own committed golden). Same library on both sides, so keep this tight; defaults to `10`.
188
+ - `sourceOfTruthThresholdPixels` — global threshold for the source-of-truth divergence check (this project's golden vs. the source-of-truth adapter's golden). Comparing across two different component libraries has legitimate structural variation, so this is expected to sit much higher than `goldenThresholdPixels`; defaults to `3500`. Unused on the source-of-truth adapter's own config (`isSourceOfTruthAdapter: true`).
189
+ - `stories.<id>.goldenThreshold`/`stories.<id>.sourceOfTruthThreshold` — per-story overrides of the two thresholds above, keyed by story id prefix.
190
+ - `stories.<id>.exclude`/`excludeTitlePrefixes` — same meaning as before; see `src/adapter-tester.schema.json` for full docs.
172
191
 
173
192
  ---
174
193
 
@@ -181,11 +200,12 @@ npm install --save-dev @recursica/adapter-tester @playwright/test
181
200
  npx playwright install chromium
182
201
  ```
183
202
 
184
- If your repo already has a `storybook` npm script with a port set (`storybook dev -p 6006`), no config file is required. Add the two scripts from [npm scripts](#npm-scripts) above to your `package.json`, then:
203
+ If your repo already has a `storybook` npm script (a plain `storybook dev`, no port needed), no config file is required. Add the scripts from [npm scripts](#npm-scripts) above to your `package.json`, then:
185
204
 
186
205
  ```bash
187
- npm run adapter-tester # Interactive Dev Mode
188
- npm run adapter-tester:automated # headless Playwright suite
206
+ npm run adapter-tester # Interactive Dev Mode
207
+ npm run adapter-tester:automated # own-drift check — run this normally
208
+ npm run adapter-tester:source-of-truth # divergence check — opt-in, against Mantine
189
209
  ```
190
210
 
191
211
  Any extra args after `adapter-tester:automated` are passed straight through to `playwright test`, so you can scope a run instead of executing the full suite:
@@ -8,7 +8,7 @@ declare const _default: {
8
8
  "properties": {
9
9
  "$schema": {
10
10
  "type": "string",
11
- "description": "Optional pointer to this schema for editor autocomplete, e.g. \"./node_modules/@recursica/adapter-tester/src/adapter-tester.schema.json\"."
11
+ "description": "Optional pointer to this schema for editor autocomplete, e.g. \"https://raw.githubusercontent.com/borderux/recursica/main/packages/adapter-tester/src/adapter-tester.schema.json\"."
12
12
  },
13
13
  "name": {
14
14
  "type": "string",
@@ -23,7 +23,7 @@ declare const _default: {
23
23
  "type": "integer",
24
24
  "minimum": 1,
25
25
  "maximum": 65535,
26
- "description": "Port the target's Storybook is served on. Auto-detected from this project's own `scripts.storybook` (a `-p <port>`/`--port <port>` flag) when omitted, falling back to Storybook's own default of 6006."
26
+ "description": "First-guess port only, not authoritative — the real port is auto-detected from this Storybook's own startup output, since Storybook silently falls back to an OS-assigned port whenever this one is taken. Auto-detected from this project's own `scripts.storybook` (a `-p <port>`/`--port <port>` flag) when omitted, falling back to 6006."
27
27
  },
28
28
  "command": {
29
29
  "type": "string",
@@ -48,7 +48,7 @@ declare const _default: {
48
48
  "type": "integer",
49
49
  "minimum": 1,
50
50
  "maximum": 65535,
51
- "description": "Port the throwaway harness's Storybook is served on. Defaults to 6011."
51
+ "description": "First-guess port only, not authoritative — the real port is auto-detected from the throwaway harness's Storybook's own startup output."
52
52
  },
53
53
  "mantineAdapterVersion": {
54
54
  "type": "string",
@@ -64,7 +64,7 @@ declare const _default: {
64
64
  "type": "object",
65
65
  "additionalProperties": false,
66
66
  "description": "Non-standard mode: points at an already-addressable Storybook — e.g. a sibling workspace package's own Storybook inside this monorepo.",
67
- "required": ["type", "port"],
67
+ "required": ["type"],
68
68
  "properties": {
69
69
  "type": { "const": "url" },
70
70
  "name": {
@@ -75,7 +75,7 @@ declare const _default: {
75
75
  "type": "integer",
76
76
  "minimum": 1,
77
77
  "maximum": 65535,
78
- "description": "Port the source-of-truth Storybook is served on."
78
+ "description": "First-guess port only, not authoritative — the real port is auto-detected from the source-of-truth Storybook's own startup output. Defaults to 6011."
79
79
  },
80
80
  "command": {
81
81
  "type": "string",
@@ -93,25 +93,46 @@ declare const _default: {
93
93
  "type": "boolean",
94
94
  "description": "True only for the source-of-truth adapter's own config (mantine-adapter). Skips `sourceOfTruth` entirely — there's nothing above it to diverge from — and runs the own-drift golden check standalone, against just this project's own Storybook. Defaults to false."
95
95
  },
96
- "diffThresholdPixels": {
96
+ "goldenThresholdPixels": {
97
97
  "type": "number",
98
98
  "minimum": 0,
99
- "description": "Global visual diff threshold, in mismatched pixels, before a story is considered a failure. Defaults to 3500. AI agents must not modify this field — see AGENT.md."
99
+ "description": "Global visual diff threshold for the own-drift check (this project's live render vs. its own committed `test/golden/` image), in mismatched pixels, before a story is considered a failure. Same library/component on both sides, so this should stay tight. Defaults to 10. AI agents must not modify this field — see AGENT.md.",
100
+ "default": 10
100
101
  },
101
- "storyThresholds": {
102
+ "sourceOfTruthThresholdPixels": {
103
+ "type": "number",
104
+ "minimum": 0,
105
+ "description": "Global visual diff threshold for the source-of-truth divergence check (this project's golden vs. the source-of-truth adapter's golden), in mismatched pixels, before a difference is flagged. Comparing across two different component libraries has legitimate structural variation (native control widgets, font rendering, etc.), so this is expected to be set much higher than `goldenThresholdPixels`. Ignored on the source-of-truth adapter's own config (`isSourceOfTruthAdapter: true`), which has no divergence check. Defaults to 3500. AI agents must not modify this field — see AGENT.md.",
106
+ "default": 3500
107
+ },
108
+ "stories": {
102
109
  "type": "object",
103
- "additionalProperties": { "type": "number", "minimum": 0 },
104
- "description": "Per-story diff threshold overrides, keyed by story id prefix (a story matches if its id equals the key or starts with it). Overrides `diffThresholdPixels` for matching stories. When more than one key matches a story, the longest (most specific) key wins."
110
+ "additionalProperties": {
111
+ "type": "object",
112
+ "additionalProperties": false,
113
+ "properties": {
114
+ "goldenThreshold": {
115
+ "type": "number",
116
+ "minimum": 0,
117
+ "description": "Diff threshold override for matching stories' own-drift check, in mismatched pixels. Overrides `goldenThresholdPixels`."
118
+ },
119
+ "sourceOfTruthThreshold": {
120
+ "type": "number",
121
+ "minimum": 0,
122
+ "description": "Diff threshold override for matching stories' source-of-truth divergence check, in mismatched pixels. Overrides `sourceOfTruthThresholdPixels`."
123
+ },
124
+ "exclude": {
125
+ "type": "boolean",
126
+ "description": "Skip matching stories entirely — no own-drift check, no divergence check, no golden captured. For stories that don't have a cross-adapter counterpart; use `excludeTitlePrefixes` to drop a whole title instead."
127
+ }
128
+ }
129
+ },
130
+ "description": "Per-story overrides, keyed by story id prefix (a story matches if its id equals the key or starts with it). When more than one key matches a story, the longest (most specific) key wins."
105
131
  },
106
132
  "excludeTitlePrefixes": {
107
133
  "type": "array",
108
134
  "items": { "type": "string" },
109
135
  "description": "Storybook title prefixes to exclude from comparison, in addition to the built-in denylist (`Theme/*`, `Tokens/*`, `Introduction`)."
110
- },
111
- "excludeStoryIds": {
112
- "type": "array",
113
- "items": { "type": "string" },
114
- "description": "Story id prefixes to skip entirely — no own-drift check, no divergence check, no golden captured. For excluding individual stories that don't have a cross-adapter counterpart; use `excludeTitlePrefixes` to drop a whole title instead."
115
136
  }
116
137
  }
117
138
  }
package/dist/cli.cjs CHANGED
@@ -1,35 +1,56 @@
1
1
  #!/usr/bin/env node
2
- "use strict";const x=require("node:child_process"),l=require("node:fs"),s=require("node:path"),D=require("node:url"),U=require("node:net"),q=require("express"),M=require("http-proxy-middleware"),G=require("./mantineSourceOfTruth-BUb5MZy0.cjs"),L=require("./index-C6uYPRmx.cjs");var w=typeof document<"u"?document.currentScript:null;const A=s.dirname(D.fileURLToPath(typeof document>"u"?require("url").pathToFileURL(__filename).href:w&&w.tagName.toUpperCase()==="SCRIPT"&&w.src||new URL("cli.cjs",document.baseURI).href));function k(t){return new Promise(o=>{const e=new U.Socket;e.setTimeout(200),e.once("connect",()=>{e.destroy(),o(!0)}),e.once("timeout",()=>{e.destroy(),o(!1)}),e.once("error",()=>{e.destroy(),o(!1)}),e.connect(t,"127.0.0.1")})}async function _(t,o=6e4){const e=Date.now();for(;Date.now()-e<o;){if(await k(t))return!0;await new Promise(r=>setTimeout(r,500))}return!1}function R(t){return Number(new URL(t).port)}function E(t,o){var r,n;console.log(`[Dev Launcher] Port ${o.port} is inactive. Launching Storybook for ${t}...`);const e=x.spawn(o.command,{cwd:o.cwd,stdio:"pipe",shell:!0});return(r=e.stdout)==null||r.on("data",a=>{for(const c of a.toString().split(`
3
- `))c.trim()&&console.log(`[${t} SB] ${c.trim()}`)}),(n=e.stderr)==null||n.on("data",a=>{for(const c of a.toString().split(`
4
- `))c.trim()&&console.error(`[${t} SB ERROR] ${c.trim()}`)}),e.on("error",a=>{console.error(`[${t} SB ERROR] Failed to start process:`,a)}),e}function J(t){const o=process.platform==="darwin"?"open":process.platform==="win32"?"start":"xdg-open";console.log(`[Dev Launcher] Auto-launching browser: ${t}`),x.spawn(o,[t],{shell:process.platform==="win32"}).on("error",e=>{console.error("[Dev Launcher] Failed to auto-launch browser:",e)})}function V(t,o,e={}){const[r,n]=t.targets,[a,c]=o;if(!(r!=null&&r.sourceOfTruth)||!n||n.sourceOfTruth||!a||!c)throw new Error("adapter-tester dev mode requires exactly two targets: [sourceOfTruth, target] — check adapter-tester.config.json.");const m=e.port??6010,h=R(r.url),i=R(n.url),u=q(),g=s.join(A,"../public"),S=s.join(A,"../report-header.txt");u.get("/",(d,p,y)=>{if(d.query.path){y();return}const P=l.readFileSync(s.join(g,"index.html"),"utf8").replace("<head>",`<head>
5
- <script>window.__ADAPTER_TESTER__ = ${JSON.stringify({ownName:n.name,sourceOfTruthName:r.name,sourceOfTruthPort:h})};<\/script>`);p.send(P)}),u.get("/report-header.txt",(d,p)=>{try{p.type("text/plain").send(l.readFileSync(S,"utf8"))}catch{p.status(500).send("Error loading report-header.txt")}}),u.use("/",M.createProxyMiddleware({target:`http://localhost:${i}`,changeOrigin:!0,ws:!0}));const f=[];let b=!1;const T=()=>{if(!b){b=!0,console.log(`
6
- [Dev Launcher] Shutting down Dev Mode server and spawned Storybooks...`);for(const d of f)d.kill("SIGINT");process.exit(0)}};process.on("SIGINT",T),process.on("SIGTERM",T),u.listen(m,async()=>{console.log(`
2
+ "use strict";const E=require("node:child_process"),u=require("node:fs"),i=require("node:path"),D=require("node:url"),q=require("express"),G=require("http-proxy-middleware"),V=require("node:net"),J=require("./mantineSourceOfTruth-Dpe4mlmF.cjs"),N=require("./index-C6uYPRmx.cjs");var k=typeof document<"u"?document.currentScript:null;const H=/\x1b\[[0-9;]*m/g,W=/Local:\s*(https?:\/\/localhost:\d+)/;function B(e){return new Promise(t=>{const r=new V.Socket;r.setTimeout(200),r.once("connect",()=>{r.destroy(),t(!0)}),r.once("timeout",()=>{r.destroy(),t(!1)}),r.once("error",()=>{r.destroy(),t(!1)}),r.connect(e,"127.0.0.1")})}function K(e){try{const t=JSON.parse(u.readFileSync(e,"utf8")).port;return typeof t=="number"?t:void 0}catch{return}}function X(e,t){u.mkdirSync(i.dirname(e),{recursive:!0}),u.writeFileSync(e,JSON.stringify({port:t}))}function Y(e,t){return new Promise((r,o)=>{var w,v;const n=E.spawn(e.command,{cwd:e.cwd,stdio:"pipe",shell:!0});let s=!1;const l=setTimeout(()=>{s||(s=!0,n.kill(),o(new Error(`Timed out after ${t}ms waiting for ${e.name}'s Storybook to report its URL (no "Local: http://localhost:<port>" line seen in its output).`)))},t),f=(a,d)=>{const p=a.toString();for(const c of p.split(`
3
+ `))if(c.trim()){const h=d?`${e.name} SB ERROR`:`${e.name} SB`;(d?console.error:console.log)(`[${h}] ${c.trim()}`)}if(s)return;const T=p.replace(H,"").match(W);if(T){s=!0,clearTimeout(l);const c=T[1],h=Number(new URL(c).port);X(e.cacheFile,h),r({url:c,port:h,process:n})}};(w=n.stdout)==null||w.on("data",a=>f(a,!1)),(v=n.stderr)==null||v.on("data",a=>f(a,!0)),n.on("error",a=>{s||(s=!0,clearTimeout(l),o(a))}),n.on("exit",a=>{s||(s=!0,clearTimeout(l),o(new Error(`${e.name}'s Storybook exited (code ${a}) before reporting its URL.`)))})})}function x(e,t){return{name:e,command:t.command,cwd:t.cwd,reuseExistingServer:t.reuseExistingServer,cacheFile:t.cacheFile,timeoutMs:t.timeout}}async function j(e){if(e.reuseExistingServer){const t=K(e.cacheFile);if(t!==void 0&&await B(t))return console.log(`[Dev Launcher] ${e.name} is already running on port ${t} (reused).`),{url:`http://localhost:${t}`,port:t,process:null}}return console.log(`[Dev Launcher] Launching Storybook for ${e.name}...`),Y(e,e.timeoutMs??120*1e3)}const _=i.dirname(D.fileURLToPath(typeof document>"u"?require("url").pathToFileURL(__filename).href:k&&k.tagName.toUpperCase()==="SCRIPT"&&k.src||new URL("cli.cjs",document.baseURI).href));function z(e){const t=process.platform==="darwin"?"open":process.platform==="win32"?"start":"xdg-open";console.log(`[Dev Launcher] Auto-launching browser: ${e}`),E.spawn(t,[e],{shell:process.platform==="win32"}).on("error",r=>{console.error("[Dev Launcher] Failed to auto-launch browser:",r)})}async function Q(e,t,r={}){const[o,n]=e.targets,[s,l]=t;if(!(o!=null&&o.sourceOfTruth)||!n||n.sourceOfTruth||!s||!l)throw new Error("adapter-tester dev mode requires exactly two targets: [sourceOfTruth, target] — check adapter-tester.config.json.");const f=r.port??6010;console.log(`[Dev Launcher] Resolving ${o.name} and ${n.name} Storybooks...`);const[w,v]=await Promise.all([j(x(o.name,s)),j(x(n.name,l))]);console.log("[Dev Launcher] Both Storybooks are active and responsive!");const a=[w.process,v.process].filter(m=>m!==null),d=q(),p=i.join(_,"../public"),T=i.join(_,"../report-header.txt");d.get("/",(m,b,S)=>{if(m.query.path){S();return}const M=u.readFileSync(i.join(p,"index.html"),"utf8").replace("<head>",`<head>
4
+ <script>window.__ADAPTER_TESTER__ = ${JSON.stringify({ownName:n.name,sourceOfTruthName:o.name,sourceOfTruthPort:w.port})};<\/script>`);b.send(M)}),d.get("/report-header.txt",(m,b)=>{try{b.type("text/plain").send(u.readFileSync(T,"utf8"))}catch{b.status(500).send("Error loading report-header.txt")}}),d.use("/",G.createProxyMiddleware({target:v.url,changeOrigin:!0,ws:!0}));let c=!1;const h=()=>{if(!c){c=!0,console.log(`
5
+ [Dev Launcher] Shutting down Dev Mode server and spawned Storybooks...`);for(const m of a)m.kill("SIGINT");process.exit(0)}};process.on("SIGINT",h),process.on("SIGTERM",h),d.listen(f,()=>{console.log(`
7
6
  ====================================================
8
7
  🚀 Adapter Dev Mode proxy running at:
9
- http://localhost:${m}
8
+ http://localhost:${f}
10
9
  ====================================================
11
- `);const[d,p]=await Promise.all([k(h),k(i)]);d?console.log(`[Dev Launcher] ${r.name} is already running on port ${h}.`):f.push(E(r.name,a)),p?console.log(`[Dev Launcher] ${n.name} is already running on port ${i}.`):f.push(E(n.name,c));const y=[];d||y.push(_(h)),p||y.push(_(i)),y.length>0?(console.log("[Dev Launcher] Waiting for Storybooks to be responsive..."),(await Promise.all(y)).every(Boolean)?console.log("[Dev Launcher] All Storybooks are active and responsive!"):console.warn("[Dev Launcher] Warning: some Storybooks timed out during startup, but proceeding...")):console.log("[Dev Launcher] Both Storybooks already active."),J(`http://localhost:${m}`)})}const B="http://json-schema.org/draft-07/schema#",W="https://github.com/borderux/recursica/tree/main/packages/adapter-tester/src/adapter-tester.schema.json",H="adapter-tester.config.json",K="Configuration for @recursica/adapter-tester. All fields are optional — an adapter repo with a normal `storybook` npm script needs no config file at all.",Y="object",X=!1,z={$schema:{type:"string",description:'Optional pointer to this schema for editor autocomplete, e.g. "./node_modules/@recursica/adapter-tester/src/adapter-tester.schema.json".'},name:{type:"string",description:`Label for this project's own target. Defaults to the unscoped name in this project's package.json (e.g. "@recursica/some-adapter" -> "some-adapter").`},storybook:{type:"object",additionalProperties:!1,description:"This project's own Storybook target.",properties:{port:{type:"integer",minimum:1,maximum:65535,description:"Port the target's Storybook is served on. Auto-detected from this project's own `scripts.storybook` (a `-p <port>`/`--port <port>` flag) when omitted, falling back to Storybook's own default of 6006."},command:{type:"string",description:"Command that boots the target's Storybook. Defaults to `npm run storybook`."},cwd:{type:"string",description:'Directory the command runs in, relative to the config file. Defaults to ".".'}}},sourceOfTruth:{description:'The comparison target. Defaults to `{ "type": "mantine-harness" }`.',oneOf:[{type:"object",additionalProperties:!1,description:"Default mode: boots a throwaway harness that installs the published @recursica/mantine-adapter from npm — no monorepo checkout required.",properties:{type:{const:"mantine-harness"},port:{type:"integer",minimum:1,maximum:65535,description:"Port the throwaway harness's Storybook is served on. Defaults to 6011."},mantineAdapterVersion:{type:"string",description:"npm version/tag of @recursica/mantine-adapter to install in the harness. Defaults to `latest`."},storybookTemplateVersion:{type:"string",description:"npm version/tag of @recursica/storybook-template to install in the harness. Defaults to `latest`."}}},{type:"object",additionalProperties:!1,description:"Non-standard mode: points at an already-addressable Storybook — e.g. a sibling workspace package's own Storybook inside this monorepo.",required:["type","port"],properties:{type:{const:"url"},name:{type:"string",description:'Label for the source-of-truth target. Defaults to "Mantine".'},port:{type:"integer",minimum:1,maximum:65535,description:"Port the source-of-truth Storybook is served on."},command:{type:"string",description:"Command that boots the source-of-truth Storybook. Defaults to `npm run storybook`."},cwd:{type:"string",description:'Directory the command runs in, relative to the config file. Defaults to ".".'}}}]},isSourceOfTruthAdapter:{type:"boolean",description:"True only for the source-of-truth adapter's own config (mantine-adapter). Skips `sourceOfTruth` entirely — there's nothing above it to diverge from — and runs the own-drift golden check standalone, against just this project's own Storybook. Defaults to false."},diffThresholdPixels:{type:"number",minimum:0,description:"Global visual diff threshold, in mismatched pixels, before a story is considered a failure. Defaults to 3500. AI agents must not modify this field — see AGENT.md."},storyThresholds:{type:"object",additionalProperties:{type:"number",minimum:0},description:"Per-story diff threshold overrides, keyed by story id prefix (a story matches if its id equals the key or starts with it). Overrides `diffThresholdPixels` for matching stories. When more than one key matches a story, the longest (most specific) key wins."},excludeTitlePrefixes:{type:"array",items:{type:"string"},description:"Storybook title prefixes to exclude from comparison, in addition to the built-in denylist (`Theme/*`, `Tokens/*`, `Introduction`)."},excludeStoryIds:{type:"array",items:{type:"string"},description:"Story id prefixes to skip entirely — no own-drift check, no divergence check, no golden captured. For excluding individual stories that don't have a cross-adapter counterpart; use `excludeTitlePrefixes` to drop a whole title instead."}},Q={$schema:B,$id:W,title:H,description:K,type:Y,additionalProperties:X,properties:z},Z=L.index,F=new L.ajvExports.Ajv({allErrors:!0,strict:!0});Z(F);const $=F.compile(Q);function ee(t,o){if($(t))return;const e=($.errors??[]).map(r=>{var a;const n=(a=r.params)!=null&&a.additionalProperty?` '${r.params.additionalProperty}'`:"";return` - ${r.instancePath||"root"} ${r.message}${n}`}).join(`
12
- `);throw new Error(`Invalid ${o}:
13
- ${e}`)}const te="@recursica/mantine-adapter",oe="adapter-tester.config.json",C="npm run storybook",re=6006,ne=6011,N="Mantine",se=3500;function I(t){const o=s.join(t,"package.json");if(!l.existsSync(o))throw new Error(`No package.json found in ${t} — run adapter-tester from the root of the project being tested.`);return JSON.parse(l.readFileSync(o,"utf8"))}function ie(t){var n;const e=(n=I(t).scripts)==null?void 0:n.storybook,r=e==null?void 0:e.match(/(?:-p|--port)[ =](\d+)/);return r?Number(r[1]):re}function ae(t){const e=I(t).name;return e?e.split("/").pop():"Adapter"}function ce(t){const o=s.join(t,oe);if(!l.existsSync(o))return{};const e=JSON.parse(l.readFileSync(o,"utf8"));return ee(e,o),e}function de(t){var b,T,d;const o=ce(t),e=o.isSourceOfTruthAdapter??!1,r=o.name??ae(t),n=((b=o.storybook)==null?void 0:b.port)??ie(t),a=((T=o.storybook)==null?void 0:T.command)??C,c=s.resolve(t,((d=o.storybook)==null?void 0:d.cwd)??"."),m={command:a,port:n,cwd:c,reuseExistingServer:!process.env.CI,timeout:120*1e3},h={diffThresholdPixels:o.diffThresholdPixels??se,storyThresholds:o.storyThresholds,excludeTitlePrefixes:o.excludeTitlePrefixes,excludeStoryIds:o.excludeStoryIds,goldenDir:s.join(c,"test","golden"),isSourceOfTruthAdapter:e,goldenMode:"check"};if(e)return{engineConfig:{...h,targets:[{name:r,url:`http://localhost:${n}`}]},webServers:[m]};const i=o.sourceOfTruth??{type:"mantine-harness"};let u,g,S,f;if(i.type==="url"){u=i.name??N,g=i.port;const p=s.resolve(t,i.cwd??".");S={command:i.command??C,port:i.port,cwd:p,reuseExistingServer:!process.env.CI,timeout:120*1e3},f={type:"local",dir:s.join(p,"test","golden")}}else u=N,g=i.port??ne,S=G.mantineSourceOfTruthWebServer({dir:s.join(t,".adapter-tester/mantine-harness"),port:g,mantineAdapterVersion:i.mantineAdapterVersion,storybookTemplateVersion:i.storybookTemplateVersion}),f={type:"npm",packageName:te,versionSpec:i.mantineAdapterVersion??"latest",cacheDir:s.join(t,".adapter-tester/mantine-golden-cache")};return{engineConfig:{...h,targets:[{name:u,url:`http://localhost:${g}`,sourceOfTruth:!0},{name:r,url:`http://localhost:${n}`}],sourceOfTruthGolden:f},webServers:[S,m]}}const pe=s.dirname(D.fileURLToPath(typeof document>"u"?require("url").pathToFileURL(__filename).href:w&&w.tagName.toUpperCase()==="SCRIPT"&&w.src||new URL("cli.cjs",document.baseURI).href)),le=D.pathToFileURL(s.join(pe,"testing.js")).href,O=process.cwd(),{engineConfig:v,webServers:j}=de(O);process.argv.includes("--update-golden")?v.goldenMode="update-golden":process.argv.includes("--approve-divergence")&&(v.goldenMode="approve-divergence");process.argv.includes("--dry-run")&&(console.log(JSON.stringify({engineConfig:v,webServers:j},null,2)),process.exit(0));const ue=new Set(["--dry-run","--serve","--update-golden","--approve-divergence"]),he=process.argv.slice(2).filter(t=>!ue.has(t));if(process.argv.includes("--serve")){if(v.isSourceOfTruthAdapter)throw new Error("Dev Mode (--serve) has nothing to sync this project's Storybook against — isSourceOfTruthAdapter is true in this project's adapter-tester.config.json.");V(v,j)}else fe(j.slice(-1),v);function fe(t,o){const e=s.join(O,".adapter-tester/run");l.mkdirSync(e,{recursive:!0}),l.writeFileSync(s.join(e,"playwright.config.js"),`// Generated by \`adapter-tester\` — do not edit, regenerated on every run.
10
+ `),z(`http://localhost:${f}`)})}const Z="http://json-schema.org/draft-07/schema#",ee="https://github.com/borderux/recursica/tree/main/packages/adapter-tester/src/adapter-tester.schema.json",te="adapter-tester.config.json",re="Configuration for @recursica/adapter-tester. All fields are optional — an adapter repo with a normal `storybook` npm script needs no config file at all.",oe="object",ne=!1,se={$schema:{type:"string",description:'Optional pointer to this schema for editor autocomplete, e.g. "https://raw.githubusercontent.com/borderux/recursica/main/packages/adapter-tester/src/adapter-tester.schema.json".'},name:{type:"string",description:`Label for this project's own target. Defaults to the unscoped name in this project's package.json (e.g. "@recursica/some-adapter" -> "some-adapter").`},storybook:{type:"object",additionalProperties:!1,description:"This project's own Storybook target.",properties:{port:{type:"integer",minimum:1,maximum:65535,description:"First-guess port only, not authoritative — the real port is auto-detected from this Storybook's own startup output, since Storybook silently falls back to an OS-assigned port whenever this one is taken. Auto-detected from this project's own `scripts.storybook` (a `-p <port>`/`--port <port>` flag) when omitted, falling back to 6006."},command:{type:"string",description:"Command that boots the target's Storybook. Defaults to `npm run storybook`."},cwd:{type:"string",description:'Directory the command runs in, relative to the config file. Defaults to ".".'}}},sourceOfTruth:{description:'The comparison target. Defaults to `{ "type": "mantine-harness" }`.',oneOf:[{type:"object",additionalProperties:!1,description:"Default mode: boots a throwaway harness that installs the published @recursica/mantine-adapter from npm — no monorepo checkout required.",properties:{type:{const:"mantine-harness"},port:{type:"integer",minimum:1,maximum:65535,description:"First-guess port only, not authoritative — the real port is auto-detected from the throwaway harness's Storybook's own startup output."},mantineAdapterVersion:{type:"string",description:"npm version/tag of @recursica/mantine-adapter to install in the harness. Defaults to `latest`."},storybookTemplateVersion:{type:"string",description:"npm version/tag of @recursica/storybook-template to install in the harness. Defaults to `latest`."}}},{type:"object",additionalProperties:!1,description:"Non-standard mode: points at an already-addressable Storybook — e.g. a sibling workspace package's own Storybook inside this monorepo.",required:["type"],properties:{type:{const:"url"},name:{type:"string",description:'Label for the source-of-truth target. Defaults to "Mantine".'},port:{type:"integer",minimum:1,maximum:65535,description:"First-guess port only, not authoritative — the real port is auto-detected from the source-of-truth Storybook's own startup output. Defaults to 6011."},command:{type:"string",description:"Command that boots the source-of-truth Storybook. Defaults to `npm run storybook`."},cwd:{type:"string",description:'Directory the command runs in, relative to the config file. Defaults to ".".'}}}]},isSourceOfTruthAdapter:{type:"boolean",description:"True only for the source-of-truth adapter's own config (mantine-adapter). Skips `sourceOfTruth` entirely — there's nothing above it to diverge from — and runs the own-drift golden check standalone, against just this project's own Storybook. Defaults to false."},goldenThresholdPixels:{type:"number",minimum:0,description:"Global visual diff threshold for the own-drift check (this project's live render vs. its own committed `test/golden/` image), in mismatched pixels, before a story is considered a failure. Same library/component on both sides, so this should stay tight. Defaults to 10. AI agents must not modify this field — see AGENT.md.",default:10},sourceOfTruthThresholdPixels:{type:"number",minimum:0,description:"Global visual diff threshold for the source-of-truth divergence check (this project's golden vs. the source-of-truth adapter's golden), in mismatched pixels, before a difference is flagged. Comparing across two different component libraries has legitimate structural variation (native control widgets, font rendering, etc.), so this is expected to be set much higher than `goldenThresholdPixels`. Ignored on the source-of-truth adapter's own config (`isSourceOfTruthAdapter: true`), which has no divergence check. Defaults to 3500. AI agents must not modify this field — see AGENT.md.",default:3500},stories:{type:"object",additionalProperties:{type:"object",additionalProperties:!1,properties:{goldenThreshold:{type:"number",minimum:0,description:"Diff threshold override for matching stories' own-drift check, in mismatched pixels. Overrides `goldenThresholdPixels`."},sourceOfTruthThreshold:{type:"number",minimum:0,description:"Diff threshold override for matching stories' source-of-truth divergence check, in mismatched pixels. Overrides `sourceOfTruthThresholdPixels`."},exclude:{type:"boolean",description:"Skip matching stories entirely — no own-drift check, no divergence check, no golden captured. For stories that don't have a cross-adapter counterpart; use `excludeTitlePrefixes` to drop a whole title instead."}}},description:"Per-story overrides, keyed by story id prefix (a story matches if its id equals the key or starts with it). When more than one key matches a story, the longest (most specific) key wins."},excludeTitlePrefixes:{type:"array",items:{type:"string"},description:"Storybook title prefixes to exclude from comparison, in addition to the built-in denylist (`Theme/*`, `Tokens/*`, `Introduction`)."}},ie={$schema:Z,$id:ee,title:te,description:re,type:oe,additionalProperties:ne,properties:se},ae=N.index,$=new N.ajvExports.Ajv({allErrors:!0,strict:!0});ae($);const R=$.compile(ie);function ce(e,t){if(R(e))return;const r=(R.errors??[]).map(o=>{var s;const n=(s=o.params)!=null&&s.additionalProperty?` '${o.params.additionalProperty}'`:"";return` - ${o.instancePath||"root"} ${o.message}${n}`}).join(`
11
+ `);throw new Error(`Invalid ${t}:
12
+ ${r}`)}const ue="@recursica/mantine-adapter",de="adapter-tester.config.json",L="npm run storybook",le=6006,F=6011,C="Mantine",pe=10,he=3500;function I(e){const t=i.join(e,"package.json");if(!u.existsSync(t))throw new Error(`No package.json found in ${e} — run adapter-tester from the root of the project being tested.`);return JSON.parse(u.readFileSync(t,"utf8"))}function fe(e){var n;const r=(n=I(e).scripts)==null?void 0:n.storybook,o=r==null?void 0:r.match(/(?:-p|--port)[ =](\d+)/);return o?Number(o[1]):le}function me(e){const r=I(e).name;return r?r.split("/").pop():"Adapter"}function ge(e){const t=i.join(e,de);if(!u.existsSync(t))return{};const r=JSON.parse(u.readFileSync(t,"utf8"));return ce(r,t),r}function ye(e,t={}){var h,m,b;const r=ge(e),o=r.isSourceOfTruthAdapter??!1;if(t.mantineAdapterVersion&&o)throw new Error("--source-of-truth-version has no effect on the source-of-truth adapter's own config (isSourceOfTruthAdapter: true) — there's nothing to pin a version for.");const n=r.name??me(e),s=((h=r.storybook)==null?void 0:h.port)??fe(e),l=((m=r.storybook)==null?void 0:m.command)??L,f=i.resolve(e,((b=r.storybook)==null?void 0:b.cwd)??"."),w={command:l,port:s,cwd:f,reuseExistingServer:!process.env.CI,cacheFile:i.join(e,".adapter-tester","last-port-own.json"),timeout:120*1e3},v={goldenThresholdPixels:r.goldenThresholdPixels??pe,sourceOfTruthThresholdPixels:r.sourceOfTruthThresholdPixels??he,stories:r.stories,excludeTitlePrefixes:r.excludeTitlePrefixes,goldenDir:i.join(f,"test","golden"),isSourceOfTruthAdapter:o,goldenMode:"check",checkMode:"own"};if(o)return{engineConfig:{...v,targets:[{name:n,url:`http://localhost:${s}`}]},webServers:[w]};const a=r.sourceOfTruth??{type:"mantine-harness"};let d,p,T,c;if(a.type==="url"){if(t.mantineAdapterVersion)throw new Error(`--source-of-truth-version has no effect with sourceOfTruth.type: "url" — that mode reads a sibling package's local checkout directly, not a published version.`);d=a.name??C,p=a.port??F;const S=i.resolve(e,a.cwd??".");T={command:a.command??L,port:p,cwd:S,reuseExistingServer:!process.env.CI,cacheFile:i.join(e,".adapter-tester","last-port-source-of-truth.json"),timeout:120*1e3},c={type:"local",dir:i.join(S,"test","golden")}}else{d=C,p=a.port??F;const S=t.mantineAdapterVersion??a.mantineAdapterVersion;T=J.mantineSourceOfTruthWebServer({dir:i.join(e,".adapter-tester/mantine-harness"),port:p,mantineAdapterVersion:S,storybookTemplateVersion:a.storybookTemplateVersion}),c={type:"npm",packageName:ue,versionSpec:S??"latest",cacheDir:i.join(e,".adapter-tester/mantine-golden-cache")}}return{engineConfig:{...v,targets:[{name:d,url:`http://localhost:${p}`,sourceOfTruth:!0},{name:n,url:`http://localhost:${s}`}],sourceOfTruthGolden:c},webServers:[T,w]}}const we=i.dirname(D.fileURLToPath(typeof document>"u"?require("url").pathToFileURL(__filename).href:k&&k.tagName.toUpperCase()==="SCRIPT"&&k.src||new URL("cli.cjs",document.baseURI).href)),ve=D.pathToFileURL(i.join(we,"testing.js")).href,y=process.argv.slice(2);let U;const O=y.indexOf("--source-of-truth-version");if(O!==-1){const e=y[O+1];if(e===void 0||e.startsWith("--"))throw new Error("--source-of-truth-version requires a value, e.g. --source-of-truth-version 0.53.0");U=e,y.splice(O,2)}const P=process.cwd(),{engineConfig:g,webServers:A}=ye(P,{mantineAdapterVersion:U});y.includes("--update-golden")?g.goldenMode="update-golden":y.includes("--approve-divergence")&&(g.goldenMode="approve-divergence");if(y.includes("--divergence-only")){if(g.isSourceOfTruthAdapter)throw new Error("--divergence-only has nothing to diverge from — isSourceOfTruthAdapter is true in this project's adapter-tester.config.json.");g.checkMode="divergence"}else if(g.goldenMode==="approve-divergence")throw new Error("--approve-divergence only makes sense with --divergence-only — there's nothing to approve without the divergence check running.");y.includes("--dry-run")&&(console.log(JSON.stringify({engineConfig:g,webServers:A},null,2)),process.exit(0));const Te=new Set(["--dry-run","--serve","--update-golden","--approve-divergence","--divergence-only"]),be=y.filter(e=>!Te.has(e));Se().catch(e=>{console.error(e),process.exitCode=1});async function Se(){if(y.includes("--serve")){if(g.isSourceOfTruthAdapter)throw new Error("Dev Mode (--serve) has nothing to sync this project's Storybook against — isSourceOfTruthAdapter is true in this project's adapter-tester.config.json.");await Q(g,A)}else{const e=A.at(-1);if(!e)throw new Error("resolveConfig() returned no webServers to boot.");await ke(e,g)}}async function ke(e,t){const r=i.join(P,".adapter-tester/run");u.mkdirSync(r,{recursive:!0});const o=t.targets.at(-1);if(!o)throw new Error("resolveConfig() returned no targets to check.");const{url:n,process:s}=await j(x(o.name,e));o.url=n,u.writeFileSync(i.join(r,"playwright.config.js"),`// Generated by \`adapter-tester\` — do not edit, regenerated on every run.
14
13
  import { defineConfig, devices } from "@playwright/test";
15
14
 
16
15
  export default defineConfig({
17
- testDir: ${JSON.stringify(e)},
18
- // Golden checks read-modify-write the same manifest.json across every
19
- // story's test body forced sequential (no worker parallelism) so those
20
- // writes never race each other.
21
- fullyParallel: false,
16
+ testDir: ${JSON.stringify(r)},
17
+ // Stories run across Playwright's default worker pool — each golden check
18
+ // only reads/writes its own story's manifest.json entry, under a lock, so
19
+ // concurrent workers never race each other (see \`updateManifestEntry\`).
20
+ // Override with \`--workers <n>\` (forwarded straight through to Playwright).
21
+ fullyParallel: true,
22
22
  forbidOnly: !!process.env.CI,
23
23
  retries: process.env.CI ? 2 : 0,
24
- workers: 1,
25
24
  reporter: [["html", { open: "never" }], ["list"]],
26
25
  use: { trace: "on-first-retry" },
27
26
  projects: [{ name: "chromium", use: { ...devices["Desktop Chrome"] } }],
28
- webServer: ${JSON.stringify(t,null,2)},
27
+ // No webServer entry — this project's own Storybook is already running by
28
+ // the time this config is used (see runAutomated() in cli.ts).
29
29
  });
30
- `),l.writeFileSync(s.join(e,"visual-regression.spec.js"),`// Generated by \`adapter-tester\` — do not edit, regenerated on every run.
31
- import { runVisualRegression } from ${JSON.stringify(le)};
30
+ `),u.writeFileSync(i.join(r,"visual-regression.spec.js"),`// Generated by \`adapter-tester\` — do not edit, regenerated on every run.
31
+ import { test, expect } from "@playwright/test";
32
+ import { resolveVisualRegressionPlan } from ${JSON.stringify(ve)};
32
33
 
33
- await runVisualRegression(${JSON.stringify(o,null,2)});
34
- `);const r=x.spawnSync("npx",["playwright","test","--config",s.join(e,"playwright.config.js"),...he],{stdio:"inherit",cwd:O,shell:process.platform==="win32"});process.exit(r.status??1)}
34
+ // \`test.describe\`/\`test\` are called here, not inside resolveVisualRegressionPlan,
35
+ // so Playwright's HTML report groups the suite under this file instead of a
36
+ // sourcemapped path into adapter-tester's own library code.
37
+ const { ownTargetName, suiteLabel, stories, missingFromSourceOfTruth, checkStory } =
38
+ await resolveVisualRegressionPlan(${JSON.stringify(t,null,2)});
39
+
40
+ test.describe(\`\${ownTargetName} — \${suiteLabel}\`, () => {
41
+ if (missingFromSourceOfTruth.length > 0) {
42
+ test("story parity with source of truth", () => {
43
+ expect(
44
+ missingFromSourceOfTruth,
45
+ \`\${missingFromSourceOfTruth.length} stor(y/ies) exist in the source of truth but are missing here. Add the missing story, or mark it \\\`exclude: true\\\` under \\\`stories\\\` in adapter-tester.config.json if intentional.\`,
46
+ ).toEqual([]);
47
+ });
48
+ }
49
+ for (const story of stories) {
50
+ test(story.id, async ({ browser }, testInfo) => {
51
+ await checkStory(story, browser, testInfo);
52
+ });
53
+ }
54
+ });
55
+ `);try{const l=E.spawnSync("npx",["playwright","test","--config",i.join(r,"playwright.config.js"),...be],{stdio:"inherit",cwd:P,shell:process.platform==="win32"});process.exitCode=l.status??1}finally{s==null||s.kill("SIGTERM")}}
35
56
  //# sourceMappingURL=cli.cjs.map