@qoretechnologies/qlip 0.1.3-beta.20260724071501 → 0.1.3-pr.28.g211f2a4
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 +25 -0
- package/dist/fs/output.d.ts +8 -0
- package/dist/fs/output.d.ts.map +1 -1
- package/dist/fs/output.js +8 -0
- package/dist/plugin/vitestPlugin.d.ts.map +1 -1
- package/dist/plugin/vitestPlugin.js +2 -0
- package/dist/runtime/context.d.ts +19 -7
- package/dist/runtime/context.d.ts.map +1 -1
- package/dist/runtime/context.js +18 -4
- package/dist/runtime/screenshot.d.ts.map +1 -1
- package/dist/runtime/screenshot.js +40 -8
- package/dist/test-runner/cli/upload.d.ts.map +1 -1
- package/dist/test-runner/cli/upload.js +14 -3
- package/dist/test-runner/index.js +3 -3
- package/dist/test-runner/manifest-store.d.ts +15 -13
- package/dist/test-runner/manifest-store.d.ts.map +1 -1
- package/dist/test-runner/manifest-store.js +19 -11
- package/dist/types.d.ts +47 -0
- package/dist/types.d.ts.map +1 -1
- package/dist/upload/capture-report.d.ts +141 -0
- package/dist/upload/capture-report.d.ts.map +1 -0
- package/dist/upload/capture-report.js +235 -0
- package/dist/upload/census.d.ts +45 -0
- package/dist/upload/census.d.ts.map +1 -0
- package/dist/upload/census.js +129 -0
- package/dist/upload/finalize.d.ts +8 -0
- package/dist/upload/finalize.d.ts.map +1 -1
- package/dist/upload/finalize.js +71 -5
- package/dist/upload/manifest.d.ts +48 -14
- package/dist/upload/manifest.d.ts.map +1 -1
- package/dist/upload/manifest.js +125 -48
- package/dist/upload/reporter.d.ts +23 -6
- package/dist/upload/reporter.d.ts.map +1 -1
- package/dist/upload/reporter.js +27 -6
- package/dist-cjs/fs/output.js +10 -1
- package/dist-cjs/test-runner/cli/upload.js +14 -3
- package/dist-cjs/test-runner/index.js +2 -2
- package/dist-cjs/test-runner/manifest-store.js +20 -12
- package/dist-cjs/upload/capture-report.js +247 -0
- package/dist-cjs/upload/census.js +138 -0
- package/dist-cjs/upload/finalize.js +73 -6
- package/dist-cjs/upload/manifest.js +125 -48
- package/dist-cjs/upload/reporter.js +27 -6
- package/package.json +14 -15
package/README.md
CHANGED
|
@@ -180,6 +180,10 @@ export const LoggedIn = {
|
|
|
180
180
|
};
|
|
181
181
|
```
|
|
182
182
|
|
|
183
|
+
Take as many as you like per story — each name is its own snapshot on the server. Give them distinct names: two captures of one story sharing a name (including `auto`, which the automatic capture uses) resolve to one snapshot, and the run warns which image it had to drop.
|
|
184
|
+
|
|
185
|
+
**Known limitation:** qlip-server keys *baselines* by story, kind and viewport, without the screenshot name. A story's auto capture and its manual captures are therefore independent, but **two manual captures of one story at one viewport share a baseline** — accepting one sets the baseline for the other, which then shows a meaningless diff. The run warns when a build contains such captures. Fixing it needs a server-side change; until then, give such captures distinct viewports if you need independent baselines.
|
|
186
|
+
|
|
183
187
|
## Parameters
|
|
184
188
|
|
|
185
189
|
Configure screenshots per story via `parameters.qlip`:
|
|
@@ -219,6 +223,7 @@ qlipVitestPlugin({
|
|
|
219
223
|
waitForIdleMs: 300,
|
|
220
224
|
maxWaitForIdleMs: 2000,
|
|
221
225
|
ignoreElements: [],
|
|
226
|
+
diagnostics: false,
|
|
222
227
|
});
|
|
223
228
|
```
|
|
224
229
|
|
|
@@ -226,6 +231,10 @@ qlipVitestPlugin({
|
|
|
226
231
|
|
|
227
232
|
`ignoreElements` lets you provide CSS selectors to mask before capture. Qlip draws solid overlays on matching elements so layout stays intact while visual diffs ignore those regions.
|
|
228
233
|
|
|
234
|
+
qlip also reports what it *failed* to capture: the run warns when Storybook stories executed without producing a capture, naming them and the files they came from. It is advisory — a story that sets `parameters.qlip.auto = false` shows up there too.
|
|
235
|
+
|
|
236
|
+
`diagnostics` (or `QLIP_DEBUG=1`) logs one line per capture — story, kind, path, and which browser context wrote it. Turn it on when a CI build captured fewer stories than it ran; the always-written `capture-report.json` covers the same ground after the fact.
|
|
237
|
+
|
|
229
238
|
## Memory-safe config for large Storybooks
|
|
230
239
|
|
|
231
240
|
If your Storybook has more than ~150 stories, or any stories that mount a heavy app shell (dashboards, multi-pane editors, etc.), the **default Vitest browser-mode** can OOM — it spawns one worker per CPU core, each with its own BrowserContext, and on a 12-core machine that's easily 20+ GB peak RAM.
|
|
@@ -265,11 +274,27 @@ Verified on a real consumer (qorus-ide, 90 story files / 643 tests, Vitest 2.1.9
|
|
|
265
274
|
auto/<storyTitle>--<storyName>.png
|
|
266
275
|
manual/<storyTitle>--<storyName>--<screenshotName>.png
|
|
267
276
|
error/<storyTitle>--<storyName>--qlip-auto-error-capture.png
|
|
277
|
+
manifest-fragments/<contextId>-<seq>.json
|
|
268
278
|
manifest.json
|
|
279
|
+
capture-report.json
|
|
269
280
|
```
|
|
270
281
|
|
|
271
282
|
- `buildId` defaults to `YYYYMMDD-HHmmss`
|
|
272
283
|
- `parameters.qlip.skip === true` disables all captures for that story
|
|
284
|
+
- `manifest-fragments/` holds one append-only record per capture; they
|
|
285
|
+
merge into `manifest.json` at end-of-run. Safe to delete after a
|
|
286
|
+
build uploads — see [`design/MANIFEST_FRAGMENTS.md`](./design/MANIFEST_FRAGMENTS.md)
|
|
287
|
+
- `capture-report.json` is the run's audit: captures per context and
|
|
288
|
+
per story file, plus any screenshot on disk that no manifest entry
|
|
289
|
+
references
|
|
290
|
+
|
|
291
|
+
## Sharded CI runs
|
|
292
|
+
|
|
293
|
+
Several `vitest run` invocations (or `test-storybook` shards) can capture into one build: pin `buildId` — e.g. `buildId: process.env.GITHUB_RUN_ID` — so every shard writes into the same `<outputDir>/<buildId>/`, then upload once at the end with `npx qlip-upload --build-dir ./qlip/screenshots/$GITHUB_RUN_ID`. Fragment names carry a per-process id, so shards cannot overwrite each other's captures.
|
|
294
|
+
|
|
295
|
+
`isolate: false` is supported. Several story files then share one browser context and capture concurrently; fragments are append-only precisely so that is safe.
|
|
296
|
+
|
|
297
|
+
Add `--fail-on-partial-build` to `qlip-upload` (or `upload.failOnPartialBuild` on the plugin) to turn "screenshots on disk that never reached the manifest" from a warning into a red build.
|
|
273
298
|
|
|
274
299
|
## Manifest
|
|
275
300
|
|
package/dist/fs/output.d.ts
CHANGED
|
@@ -14,6 +14,14 @@ export declare const DEFAULT_VIEWPORT: {
|
|
|
14
14
|
* reference it without pulling node:fs/path into the browser bundle.
|
|
15
15
|
*/
|
|
16
16
|
export declare const MANIFEST_FRAGMENT_DIR = "manifest-fragments";
|
|
17
|
+
/**
|
|
18
|
+
* Fragment file name for one capture. `(fragmentId, seq)` is unique
|
|
19
|
+
* across browser contexts AND across processes sharing a build dir, so
|
|
20
|
+
* no two writers ever target the same path — that is what makes the
|
|
21
|
+
* fragment set append-only. Shared by both capture paths so they agree
|
|
22
|
+
* by construction. See `design/MANIFEST_FRAGMENTS.md`.
|
|
23
|
+
*/
|
|
24
|
+
export declare const fragmentFileName: (fragmentId: string, seq: number) => string;
|
|
17
25
|
export declare const generateBuildId: (date?: Date) => string;
|
|
18
26
|
export declare const sanitizeSegment: (value: string) => string;
|
|
19
27
|
export declare const joinPath: (...parts: string[]) => string;
|
package/dist/fs/output.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"output.d.ts","sourceRoot":"","sources":["../../src/fs/output.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,YAAY,EAAE,oBAAoB,EAAE,MAAM,aAAa,CAAC;AAEjE,eAAO,MAAM,SAAS,SAAS,CAAC;AAEhC,eAAO,MAAM,kBAAkB,uBAAuB,CAAC;AACvD,eAAO,MAAM,0BAA0B,4BAA4B,CAAC;AACpE,eAAO,MAAM,gBAAgB;;;CAA+B,CAAC;AAC7D;;;;;;GAMG;AACH,eAAO,MAAM,qBAAqB,uBAAuB,CAAC;AAE1D,eAAO,MAAM,eAAe,GAAI,OAAM,IAAiB,KAAG,MASzD,CAAC;AAEF,eAAO,MAAM,eAAe,GAAI,OAAO,MAAM,KAAG,MACD,CAAC;AAEhD,eAAO,MAAM,QAAQ,GAAI,GAAG,OAAO,MAAM,EAAE,KAAG,MAgB7C,CAAC;AA+BF,eAAO,MAAM,uBAAuB,GAAI,+CAKrC;IACD,QAAQ,EAAE,MAAM,CAAC;IACjB,OAAO,EAAE,MAAM,CAAC;IAChB,UAAU,CAAC,EAAE,MAAM,CAAC;IACpB,SAAS,CAAC,EAAE,MAAM,CAAC;CACpB;;;CAOA,CAAC;AAEF,eAAO,MAAM,yBAAyB,GAAI,+DAMvC;IACD,QAAQ,EAAE,MAAM,CAAC;IACjB,OAAO,EAAE,MAAM,CAAC;IAChB,UAAU,CAAC,EAAE,MAAM,CAAC;IACpB,SAAS,CAAC,EAAE,MAAM,CAAC;IACnB,cAAc,EAAE,MAAM,CAAC;CACxB;;;;CAYA,CAAC;AAKF,eAAO,MAAM,gBAAgB,GAAI,+CAK9B;IACD,QAAQ,EAAE,MAAM,CAAC;IACjB,OAAO,EAAE,MAAM,CAAC;IAChB,UAAU,CAAC,EAAE,MAAM,CAAC;IACpB,SAAS,CAAC,EAAE,MAAM,CAAC;CACpB;;;CAOA,CAAC;AAEF,eAAO,MAAM,kBAAkB,GAAI,+DAMhC;IACD,QAAQ,EAAE,MAAM,CAAC;IACjB,OAAO,EAAE,MAAM,CAAC;IAChB,UAAU,CAAC,EAAE,MAAM,CAAC;IACpB,SAAS,CAAC,EAAE,MAAM,CAAC;IACnB,cAAc,EAAE,MAAM,CAAC;CACxB;;;CAWA,CAAC;AAEF,eAAO,MAAM,cAAc,GAAI,oDAM5B;IACD,OAAO,EAAE,MAAM,CAAC;IAChB,SAAS,EAAE,MAAM,CAAC;IAClB,QAAQ,EAAE,oBAAoB,CAAC;IAC/B,IAAI,EAAE;QAAE,IAAI,EAAE,MAAM,CAAC;QAAC,OAAO,EAAE,MAAM,CAAA;KAAE,CAAC;IACxC,SAAS,CAAC,EAAE,IAAI,CAAC;CAClB,KAAG,YAeF,CAAC"}
|
|
1
|
+
{"version":3,"file":"output.d.ts","sourceRoot":"","sources":["../../src/fs/output.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,YAAY,EAAE,oBAAoB,EAAE,MAAM,aAAa,CAAC;AAEjE,eAAO,MAAM,SAAS,SAAS,CAAC;AAEhC,eAAO,MAAM,kBAAkB,uBAAuB,CAAC;AACvD,eAAO,MAAM,0BAA0B,4BAA4B,CAAC;AACpE,eAAO,MAAM,gBAAgB;;;CAA+B,CAAC;AAC7D;;;;;;GAMG;AACH,eAAO,MAAM,qBAAqB,uBAAuB,CAAC;AAE1D;;;;;;GAMG;AACH,eAAO,MAAM,gBAAgB,GAAI,YAAY,MAAM,EAAE,KAAK,MAAM,KAAG,MACb,CAAC;AAEvD,eAAO,MAAM,eAAe,GAAI,OAAM,IAAiB,KAAG,MASzD,CAAC;AAEF,eAAO,MAAM,eAAe,GAAI,OAAO,MAAM,KAAG,MACD,CAAC;AAEhD,eAAO,MAAM,QAAQ,GAAI,GAAG,OAAO,MAAM,EAAE,KAAG,MAgB7C,CAAC;AA+BF,eAAO,MAAM,uBAAuB,GAAI,+CAKrC;IACD,QAAQ,EAAE,MAAM,CAAC;IACjB,OAAO,EAAE,MAAM,CAAC;IAChB,UAAU,CAAC,EAAE,MAAM,CAAC;IACpB,SAAS,CAAC,EAAE,MAAM,CAAC;CACpB;;;CAOA,CAAC;AAEF,eAAO,MAAM,yBAAyB,GAAI,+DAMvC;IACD,QAAQ,EAAE,MAAM,CAAC;IACjB,OAAO,EAAE,MAAM,CAAC;IAChB,UAAU,CAAC,EAAE,MAAM,CAAC;IACpB,SAAS,CAAC,EAAE,MAAM,CAAC;IACnB,cAAc,EAAE,MAAM,CAAC;CACxB;;;;CAYA,CAAC;AAKF,eAAO,MAAM,gBAAgB,GAAI,+CAK9B;IACD,QAAQ,EAAE,MAAM,CAAC;IACjB,OAAO,EAAE,MAAM,CAAC;IAChB,UAAU,CAAC,EAAE,MAAM,CAAC;IACpB,SAAS,CAAC,EAAE,MAAM,CAAC;CACpB;;;CAOA,CAAC;AAEF,eAAO,MAAM,kBAAkB,GAAI,+DAMhC;IACD,QAAQ,EAAE,MAAM,CAAC;IACjB,OAAO,EAAE,MAAM,CAAC;IAChB,UAAU,CAAC,EAAE,MAAM,CAAC;IACpB,SAAS,CAAC,EAAE,MAAM,CAAC;IACnB,cAAc,EAAE,MAAM,CAAC;CACxB;;;CAWA,CAAC;AAEF,eAAO,MAAM,cAAc,GAAI,oDAM5B;IACD,OAAO,EAAE,MAAM,CAAC;IAChB,SAAS,EAAE,MAAM,CAAC;IAClB,QAAQ,EAAE,oBAAoB,CAAC;IAC/B,IAAI,EAAE;QAAE,IAAI,EAAE,MAAM,CAAC;QAAC,OAAO,EAAE,MAAM,CAAA;KAAE,CAAC;IACxC,SAAS,CAAC,EAAE,IAAI,CAAC;CAClB,KAAG,YAeF,CAAC"}
|
package/dist/fs/output.js
CHANGED
|
@@ -10,6 +10,14 @@ export const DEFAULT_VIEWPORT = { width: 1280, height: 720 };
|
|
|
10
10
|
* reference it without pulling node:fs/path into the browser bundle.
|
|
11
11
|
*/
|
|
12
12
|
export const MANIFEST_FRAGMENT_DIR = 'manifest-fragments';
|
|
13
|
+
/**
|
|
14
|
+
* Fragment file name for one capture. `(fragmentId, seq)` is unique
|
|
15
|
+
* across browser contexts AND across processes sharing a build dir, so
|
|
16
|
+
* no two writers ever target the same path — that is what makes the
|
|
17
|
+
* fragment set append-only. Shared by both capture paths so they agree
|
|
18
|
+
* by construction. See `design/MANIFEST_FRAGMENTS.md`.
|
|
19
|
+
*/
|
|
20
|
+
export const fragmentFileName = (fragmentId, seq) => `${fragmentId}-${String(seq).padStart(5, '0')}.json`;
|
|
13
21
|
export const generateBuildId = (date = new Date()) => {
|
|
14
22
|
const pad = (num) => String(num).padStart(2, '0');
|
|
15
23
|
const year = date.getFullYear();
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"vitestPlugin.d.ts","sourceRoot":"","sources":["../../src/plugin/vitestPlugin.ts"],"names":[],"mappings":"AAIA,OAAO,KAAK,EAAE,MAAM,EAAE,MAAM,eAAe,CAAC;AAU5C,OAAO,EAAE,iBAAiB,EAAqB,MAAM,aAAa,CAAC;AAMnE,eAAO,MAAM,gBAAgB,GAAI,UAAS,iBAAsB,KAAG,
|
|
1
|
+
{"version":3,"file":"vitestPlugin.d.ts","sourceRoot":"","sources":["../../src/plugin/vitestPlugin.ts"],"names":[],"mappings":"AAIA,OAAO,KAAK,EAAE,MAAM,EAAE,MAAM,eAAe,CAAC;AAU5C,OAAO,EAAE,iBAAiB,EAAqB,MAAM,aAAa,CAAC;AAMnE,eAAO,MAAM,gBAAgB,GAAI,UAAS,iBAAsB,KAAG,MAmQlE,CAAC"}
|
|
@@ -49,6 +49,8 @@ export const qlipVitestPlugin = (options = {}) => {
|
|
|
49
49
|
consoleLogExcludePatterns: options.consoleLogExcludePatterns ?? [],
|
|
50
50
|
},
|
|
51
51
|
tool: { name: TOOL_NAME, version },
|
|
52
|
+
// Read here, in Node, because the browser runtime has no env.
|
|
53
|
+
diagnostics: options.diagnostics ?? process.env['QLIP_DEBUG'] === '1',
|
|
52
54
|
};
|
|
53
55
|
const setupTs = fileURLToPath(new URL('../runtime/setup.ts', import.meta.url));
|
|
54
56
|
const setupJs = fileURLToPath(new URL('../runtime/setup.js', import.meta.url));
|
|
@@ -7,20 +7,32 @@ export interface QlipRuntimeState {
|
|
|
7
7
|
warnedMissingStorybook: boolean;
|
|
8
8
|
consoleLogs: QlipConsoleMessage[];
|
|
9
9
|
/**
|
|
10
|
-
*
|
|
11
|
-
*
|
|
12
|
-
*
|
|
13
|
-
*
|
|
14
|
-
*
|
|
15
|
-
* each other's writes to a shared `manifest.json`. See
|
|
16
|
-
* `src/upload/manifest.ts` for the Node-side merge.
|
|
10
|
+
* Identifies this browser context in the fragment file names it
|
|
11
|
+
* writes under `<buildDir>/manifest-fragments/`. Paired with
|
|
12
|
+
* `fragmentSeq` it makes every capture's file name unique, which is
|
|
13
|
+
* what lets fragments be append-only. See `src/upload/manifest.ts`
|
|
14
|
+
* for the Node-side merge.
|
|
17
15
|
*/
|
|
18
16
|
fragmentId: string;
|
|
17
|
+
/**
|
|
18
|
+
* Captures written by this context so far. Incremented per fragment
|
|
19
|
+
* write; never reused, so no fragment file is ever written twice.
|
|
20
|
+
*/
|
|
21
|
+
fragmentSeq: number;
|
|
19
22
|
}
|
|
20
23
|
export declare const getRuntimeState: () => QlipRuntimeState | null;
|
|
21
24
|
export declare const initRuntimeState: () => QlipRuntimeState | null;
|
|
22
25
|
export declare const nextStepName: (state: QlipRuntimeState, storyId: string) => string;
|
|
23
26
|
export declare const pushEntry: (state: QlipRuntimeState, entry: QlipManifestEntry) => void;
|
|
27
|
+
/**
|
|
28
|
+
* Claim the next fragment sequence number for this context.
|
|
29
|
+
*
|
|
30
|
+
* Synchronous and unshared, so two overlapping captures — the
|
|
31
|
+
* `afterEach` hooks of two test files in one context under
|
|
32
|
+
* `isolate: false` — can never claim the same number, and therefore
|
|
33
|
+
* never write the same fragment file.
|
|
34
|
+
*/
|
|
35
|
+
export declare const nextFragmentSeq: (state: QlipRuntimeState) => number;
|
|
24
36
|
/**
|
|
25
37
|
* Remove every `error`-kind entry for `storyId` from the in-memory
|
|
26
38
|
* manifest. Returns the removed entries so the caller can delete their
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"context.d.ts","sourceRoot":"","sources":["../../src/runtime/context.ts"],"names":[],"mappings":"AAAA,OAAO,EACL,kBAAkB,EAClB,YAAY,EACZ,iBAAiB,EACjB,iBAAiB,EAClB,MAAM,aAAa,CAAC;AAIrB,MAAM,WAAW,gBAAgB;IAC/B,MAAM,EAAE,iBAAiB,CAAC;IAC1B,QAAQ,EAAE,YAAY,CAAC;IACvB,QAAQ,EAAE,GAAG,CAAC,MAAM,EAAE,MAAM,CAAC,CAAC;IAC9B,SAAS,EAAE,MAAM,CAAC;IAClB,sBAAsB,EAAE,OAAO,CAAC;IAChC,WAAW,EAAE,kBAAkB,EAAE,CAAC;IAClC
|
|
1
|
+
{"version":3,"file":"context.d.ts","sourceRoot":"","sources":["../../src/runtime/context.ts"],"names":[],"mappings":"AAAA,OAAO,EACL,kBAAkB,EAClB,YAAY,EACZ,iBAAiB,EACjB,iBAAiB,EAClB,MAAM,aAAa,CAAC;AAIrB,MAAM,WAAW,gBAAgB;IAC/B,MAAM,EAAE,iBAAiB,CAAC;IAC1B,QAAQ,EAAE,YAAY,CAAC;IACvB,QAAQ,EAAE,GAAG,CAAC,MAAM,EAAE,MAAM,CAAC,CAAC;IAC9B,SAAS,EAAE,MAAM,CAAC;IAClB,sBAAsB,EAAE,OAAO,CAAC;IAChC,WAAW,EAAE,kBAAkB,EAAE,CAAC;IAClC;;;;;;OAMG;IACH,UAAU,EAAE,MAAM,CAAC;IACnB;;;OAGG;IACH,WAAW,EAAE,MAAM,CAAC;CACrB;AAyBD,eAAO,MAAM,eAAe,QAAO,gBAAgB,GAAG,IAKrD,CAAC;AAEF,eAAO,MAAM,gBAAgB,QAAO,gBAAgB,GAAG,IA4CtD,CAAC;AAEF,eAAO,MAAM,YAAY,GACvB,OAAO,gBAAgB,EACvB,SAAS,MAAM,KACd,MAKF,CAAC;AAEF,eAAO,MAAM,SAAS,GACpB,OAAO,gBAAgB,EACvB,OAAO,iBAAiB,SAGzB,CAAC;AAEF;;;;;;;GAOG;AACH,eAAO,MAAM,eAAe,GAAI,OAAO,gBAAgB,KAAG,MAGzD,CAAC;AAEF;;;;;;;;;;;;;;;;;;;GAmBG;AACH,eAAO,MAAM,sBAAsB,GACjC,OAAO,gBAAgB,EACvB,SAAS,MAAM,KACd,iBAAiB,EAmBnB,CAAC;AAEF,eAAO,MAAM,WAAW,GAAI,OAAO,gBAAgB,EAAE,SAAS,OAAO,CAAC,YAAY,CAAC,OAAO,CAAC,CAAC,SAM3F,CAAC;AAEF,eAAO,MAAM,oBAAoB,GAAI,OAAO,gBAAgB,SAE3D,CAAC;AAEF,eAAO,MAAM,cAAc,GACzB,OAAO,gBAAgB,EACvB,KAAK,kBAAkB,EACvB,SAAS,MAAM,SAKhB,CAAC;AAEF,eAAO,MAAM,gBAAgB,GAC3B,OAAO,gBAAgB,KACtB,kBAAkB,EAIpB,CAAC"}
|
package/dist/runtime/context.js
CHANGED
|
@@ -40,10 +40,11 @@ export const initRuntimeState = () => {
|
|
|
40
40
|
tool: runtimeConfig.tool,
|
|
41
41
|
});
|
|
42
42
|
// Fragment ID needs to be unique per browser context, not unique per
|
|
43
|
-
// build
|
|
44
|
-
//
|
|
45
|
-
//
|
|
46
|
-
// prefix that risk is
|
|
43
|
+
// build: under `isolate: false` several test files share one context
|
|
44
|
+
// (and one of these states), and several vitest processes can share
|
|
45
|
+
// one build dir. Stamp time + random suffix covers both — Math.random()
|
|
46
|
+
// alone would collide ~1 in 16M; with the millis prefix that risk is
|
|
47
|
+
// gone.
|
|
47
48
|
const fragmentId = `${realDateNow().toString(36)}-${Math.random()
|
|
48
49
|
.toString(36)
|
|
49
50
|
.slice(2, 10)}`;
|
|
@@ -55,6 +56,7 @@ export const initRuntimeState = () => {
|
|
|
55
56
|
warnedMissingStorybook: false,
|
|
56
57
|
consoleLogs: [],
|
|
57
58
|
fragmentId,
|
|
59
|
+
fragmentSeq: 0,
|
|
58
60
|
};
|
|
59
61
|
const globalState = globalThis;
|
|
60
62
|
globalState[GLOBAL_KEY] = runtime;
|
|
@@ -69,6 +71,18 @@ export const nextStepName = (state, storyId) => {
|
|
|
69
71
|
export const pushEntry = (state, entry) => {
|
|
70
72
|
state.manifest.entries.push(entry);
|
|
71
73
|
};
|
|
74
|
+
/**
|
|
75
|
+
* Claim the next fragment sequence number for this context.
|
|
76
|
+
*
|
|
77
|
+
* Synchronous and unshared, so two overlapping captures — the
|
|
78
|
+
* `afterEach` hooks of two test files in one context under
|
|
79
|
+
* `isolate: false` — can never claim the same number, and therefore
|
|
80
|
+
* never write the same fragment file.
|
|
81
|
+
*/
|
|
82
|
+
export const nextFragmentSeq = (state) => {
|
|
83
|
+
state.fragmentSeq += 1;
|
|
84
|
+
return state.fragmentSeq;
|
|
85
|
+
};
|
|
72
86
|
/**
|
|
73
87
|
* Remove every `error`-kind entry for `storyId` from the in-memory
|
|
74
88
|
* manifest. Returns the removed entries so the caller can delete their
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"screenshot.d.ts","sourceRoot":"","sources":["../../src/runtime/screenshot.ts"],"names":[],"mappings":"AAAA,OAAO,EAGL,qBAAqB,EACrB,gBAAgB,EAEjB,MAAM,aAAa,CAAC;
|
|
1
|
+
{"version":3,"file":"screenshot.d.ts","sourceRoot":"","sources":["../../src/runtime/screenshot.ts"],"names":[],"mappings":"AAAA,OAAO,EAGL,qBAAqB,EACrB,gBAAgB,EAEjB,MAAM,aAAa,CAAC;AA8BrB,OAAO,KAAK,EAAE,WAAW,EAAE,MAAM,QAAQ,CAAC;AA8L1C;;;;;;;;;;;;;;GAcG;AACH,eAAO,MAAM,0BAA0B,GACrC,SAAS,MAAM;AACf;;;;;;;GAOG;AACH,gBAAgB,MAAM,KACrB;IAAE,KAAK,EAAE,MAAM,GAAG,SAAS,CAAC;IAAC,IAAI,EAAE,MAAM,GAAG,SAAS,CAAA;CAiCvD,CAAC;AAwDF;;;;;;;GAOG;AACH,eAAO,MAAM,0BAA0B,GAAI,qEAIxC;IACD,iBAAiB,EAAE,OAAO,CAAC;IAC3B,oBAAoB,EAAE,OAAO,CAAC;IAC9B,qBAAqB,EAAE,OAAO,CAAC;CAChC,SA+DA,CAAC;AA+ZF,eAAO,MAAM,UAAU,GACrB,KAAK,gBAAgB,EACrB,gBAAgB,MAAM,GAAG,qBAAqB,EAC9C,UAAU,qBAAqB,kBA0BhC,CAAC;AAEF,KAAK,eAAe,GAAG,WAAW,GAAG;IAAE,KAAK,CAAC,EAAE,gBAAgB,CAAA;CAAE,CAAC;AA8ClE,eAAO,MAAM,qBAAqB,GAAU,KAAK,eAAe,kBAiG/D,CAAC;AAEF,eAAO,MAAM,sBAAsB,GAAU,KAAK,eAAe,kBAkEhE,CAAC"}
|
|
@@ -1,9 +1,31 @@
|
|
|
1
1
|
import { resolveQlipOptions } from '../config/parameters.js';
|
|
2
2
|
import { monotonicNow, realClearInterval, realSetInterval, realSetTimeout, } from './clock.js';
|
|
3
|
-
import { AUTO_ERROR_SCREENSHOT_BASE, MANIFEST_FRAGMENT_DIR, buildAutoLogPath, buildAutoScreenshotPath, buildManualLogPath, buildManualScreenshotPath, joinPath, sanitizeSegment, } from '../fs/output.js';
|
|
4
|
-
import { flushConsoleLogs, initRuntimeState, markStorybookWarning, nextStepName, pruneStaleErrorEntries, pushEntry, updateStats, } from './context.js';
|
|
5
|
-
const writeManifestFragment = async (commands,
|
|
6
|
-
|
|
3
|
+
import { AUTO_ERROR_SCREENSHOT_BASE, MANIFEST_FRAGMENT_DIR, buildAutoLogPath, buildAutoScreenshotPath, buildManualLogPath, buildManualScreenshotPath, fragmentFileName, joinPath, sanitizeSegment, } from '../fs/output.js';
|
|
4
|
+
import { flushConsoleLogs, initRuntimeState, markStorybookWarning, nextFragmentSeq, nextStepName, pruneStaleErrorEntries, pushEntry, updateStats, } from './context.js';
|
|
5
|
+
const writeManifestFragment = async (commands, state, payload) => {
|
|
6
|
+
const seq = nextFragmentSeq(state);
|
|
7
|
+
const fragment = {
|
|
8
|
+
...state.manifest,
|
|
9
|
+
entries: payload.entries ?? [],
|
|
10
|
+
fragmentId: state.fragmentId,
|
|
11
|
+
fragmentSeq: seq,
|
|
12
|
+
...(payload.tombstones ? { tombstones: payload.tombstones } : {}),
|
|
13
|
+
};
|
|
14
|
+
// JSON.stringify runs to completion before the first await, so the
|
|
15
|
+
// payload is a consistent snapshot even while a concurrent capture
|
|
16
|
+
// mutates the shared manifest.
|
|
17
|
+
const data = JSON.stringify(fragment, null, 2);
|
|
18
|
+
await commands.writeFile(joinPath(state.config.buildDir, MANIFEST_FRAGMENT_DIR, fragmentFileName(state.fragmentId, seq)), data, 'utf-8');
|
|
19
|
+
if (state.config.diagnostics === true) {
|
|
20
|
+
for (const entry of payload.entries ?? []) {
|
|
21
|
+
// eslint-disable-next-line no-console
|
|
22
|
+
console.log(`[qlip] captured ${entry.storyId} ${entry.kind}/${entry.status} → ${entry.path} (ctx ${state.fragmentId} #${String(seq)})`);
|
|
23
|
+
}
|
|
24
|
+
for (const tombstone of payload.tombstones ?? []) {
|
|
25
|
+
// eslint-disable-next-line no-console
|
|
26
|
+
console.log(`[qlip] retracted ${tombstone.storyId} ${tombstone.kind} (ctx ${state.fragmentId} #${String(seq)})`);
|
|
27
|
+
}
|
|
28
|
+
}
|
|
7
29
|
};
|
|
8
30
|
/**
|
|
9
31
|
* Best-effort file deletion for retry-mask pruning. Silent no-op when
|
|
@@ -398,7 +420,7 @@ const captureScreenshot = async ({ kind, story, screenshotName, options, testErr
|
|
|
398
420
|
const usesNamedPath = kind === 'manual' || kind === 'error';
|
|
399
421
|
if (resolved.skip) {
|
|
400
422
|
flushConsoleLogs(runtime);
|
|
401
|
-
|
|
423
|
+
const skippedEntry = buildEntry({
|
|
402
424
|
kind,
|
|
403
425
|
storyId: story.id,
|
|
404
426
|
storyTitle: story.title,
|
|
@@ -425,14 +447,17 @@ const captureScreenshot = async ({ kind, story, screenshotName, options, testErr
|
|
|
425
447
|
status: 'skipped',
|
|
426
448
|
error: testError ?? null,
|
|
427
449
|
timingsMs: monotonicNow() - captureStart,
|
|
428
|
-
})
|
|
450
|
+
});
|
|
451
|
+
pushEntry(runtime, skippedEntry);
|
|
429
452
|
updateStats(runtime, {
|
|
430
453
|
storiesTotal: kind === 'auto'
|
|
431
454
|
? runtime.manifest.stats.storiesTotal + 1
|
|
432
455
|
: runtime.manifest.stats.storiesTotal,
|
|
433
456
|
skipped: runtime.manifest.stats.skipped + 1,
|
|
434
457
|
});
|
|
435
|
-
await writeManifestFragment(commands, runtime
|
|
458
|
+
await writeManifestFragment(commands, runtime, {
|
|
459
|
+
entries: [skippedEntry],
|
|
460
|
+
});
|
|
436
461
|
return;
|
|
437
462
|
}
|
|
438
463
|
let relativePath = '';
|
|
@@ -561,7 +586,7 @@ const captureScreenshot = async ({ kind, story, screenshotName, options, testErr
|
|
|
561
586
|
: runtime.manifest.stats.failed,
|
|
562
587
|
});
|
|
563
588
|
}
|
|
564
|
-
await writeManifestFragment(commands, runtime
|
|
589
|
+
await writeManifestFragment(commands, runtime, { entries: [entry] });
|
|
565
590
|
};
|
|
566
591
|
export const screenshot = async (ctx, nameOrOptions, options) => {
|
|
567
592
|
const runtime = initRuntimeState();
|
|
@@ -688,6 +713,13 @@ export const captureAutoScreenshot = async (ctx) => {
|
|
|
688
713
|
const pruned = pruneStaleErrorEntries(runtime, story.id);
|
|
689
714
|
if (pruned.length) {
|
|
690
715
|
const { commands } = await ensureBrowserContext();
|
|
716
|
+
// Fragments are append-only, so dropping the entries from the
|
|
717
|
+
// in-memory manifest no longer removes them from disk — the
|
|
718
|
+
// fragments that carried them were already written. Retract them
|
|
719
|
+
// with a tombstone the merger applies.
|
|
720
|
+
await writeManifestFragment(commands, runtime, {
|
|
721
|
+
tombstones: [{ storyId: story.id, kind: 'error' }],
|
|
722
|
+
});
|
|
691
723
|
for (const entry of pruned) {
|
|
692
724
|
if (entry.path) {
|
|
693
725
|
await bestEffortRemoveFile(commands, joinPath(runtime.config.buildDir, entry.path));
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"upload.d.ts","sourceRoot":"","sources":["../../../src/test-runner/cli/upload.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GA+BG;
|
|
1
|
+
{"version":3,"file":"upload.d.ts","sourceRoot":"","sources":["../../../src/test-runner/cli/upload.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GA+BG;AAoIH,MAAM,WAAW,gBAAgB;IAC/B,+CAA+C;IAC/C,QAAQ,EAAE,MAAM,CAAC;IACjB,uEAAuE;IACvE,QAAQ,CAAC,EAAE,MAAM,CAAC;CACnB;AAED;;;;GAIG;AACH,eAAO,MAAM,SAAS,GACpB,MAAM,MAAM,EAAE,EACd,MAAK,MAAM,CAAC,UAAwB,EACpC,SAAQ;IAAE,GAAG,EAAE,CAAC,GAAG,EAAE,MAAM,KAAK,IAAI,CAAC;IAAC,KAAK,EAAE,CAAC,GAAG,EAAE,MAAM,KAAK,IAAI,CAAA;CAAY,KAC7E,OAAO,CAAC,gBAAgB,CAsE1B,CAAC"}
|
|
@@ -54,12 +54,18 @@ Options:
|
|
|
54
54
|
--commit <sha> Override commit detection.
|
|
55
55
|
--fail-on-upload-error Exit non-zero on upload failure (default:
|
|
56
56
|
log error + exit 0).
|
|
57
|
+
--fail-on-partial-build Exit non-zero when screenshots on disk are
|
|
58
|
+
missing from the manifest (default: warn).
|
|
57
59
|
-h, --help Show this help.
|
|
58
60
|
|
|
59
61
|
Env vars override defaults; CLI flags override env vars.
|
|
60
62
|
`;
|
|
61
63
|
const parseArgs = (argv) => {
|
|
62
|
-
const args = {
|
|
64
|
+
const args = {
|
|
65
|
+
failOnUploadError: false,
|
|
66
|
+
failOnPartialBuild: false,
|
|
67
|
+
help: false,
|
|
68
|
+
};
|
|
63
69
|
for (let i = 0; i < argv.length; i += 1) {
|
|
64
70
|
const arg = argv[i];
|
|
65
71
|
const next = () => argv[++i];
|
|
@@ -92,6 +98,9 @@ const parseArgs = (argv) => {
|
|
|
92
98
|
case '--fail-on-upload-error':
|
|
93
99
|
args.failOnUploadError = true;
|
|
94
100
|
break;
|
|
101
|
+
case '--fail-on-partial-build':
|
|
102
|
+
args.failOnPartialBuild = true;
|
|
103
|
+
break;
|
|
95
104
|
default:
|
|
96
105
|
if (arg.startsWith('-')) {
|
|
97
106
|
throw new Error(`Unknown option: ${arg}`);
|
|
@@ -172,6 +181,7 @@ export const runUpload = async (argv, env = process.env, logger = console) => {
|
|
|
172
181
|
...(parsed.branch !== undefined ? { branch: parsed.branch } : {}),
|
|
173
182
|
...(parsed.commit !== undefined ? { commit: parsed.commit } : {}),
|
|
174
183
|
failOnUploadError: parsed.failOnUploadError,
|
|
184
|
+
failOnPartialBuild: parsed.failOnPartialBuild,
|
|
175
185
|
};
|
|
176
186
|
// finalizeBuild needs a runtime config but only reads `buildDir`
|
|
177
187
|
// off it. We can synthesize a minimal one — the manifest itself
|
|
@@ -189,8 +199,9 @@ export const runUpload = async (argv, env = process.env, logger = console) => {
|
|
|
189
199
|
await finalizeBuild({ runtime, upload });
|
|
190
200
|
}
|
|
191
201
|
catch (err) {
|
|
192
|
-
// finalizeBuild only throws
|
|
193
|
-
//
|
|
202
|
+
// finalizeBuild only throws when the caller opted in: upload
|
|
203
|
+
// failed with --fail-on-upload-error, or the build came out
|
|
204
|
+
// partial with --fail-on-partial-build. Either way, exit non-zero.
|
|
194
205
|
logger.error(`qlip-upload: ${err.message}`);
|
|
195
206
|
return { exitCode: 2, buildDir };
|
|
196
207
|
}
|
|
@@ -45,7 +45,7 @@ import { promises as fs } from 'node:fs';
|
|
|
45
45
|
import path from 'node:path';
|
|
46
46
|
import { resolveQlipOptions } from '../config/parameters.js';
|
|
47
47
|
import { DEFAULT_OUTPUT_DIR, DEFAULT_VIEWPORT, MANIFEST_FRAGMENT_DIR, TOOL_NAME, buildAutoScreenshotPath, generateBuildId, } from '../fs/output.js';
|
|
48
|
-
import {
|
|
48
|
+
import { writeEntryFragment, getOrInitStore, nextStepName, peekStore, pushEntry, } from './manifest-store.js';
|
|
49
49
|
import { applyAnimationControlInPage, applyIgnoreMasksInPage, removeIgnoreMasksInPage, waitForDomIdleInPage, } from './prep-scripts.js';
|
|
50
50
|
import { QLIP_TOOL_VERSION } from './version.js';
|
|
51
51
|
/**
|
|
@@ -217,7 +217,7 @@ const qlipCaptureInner = async (page, context, options) => {
|
|
|
217
217
|
timingsMs: Date.now() - captureStart,
|
|
218
218
|
});
|
|
219
219
|
pushEntry(store, entry);
|
|
220
|
-
await
|
|
220
|
+
await writeEntryFragment(store, entry);
|
|
221
221
|
return entry;
|
|
222
222
|
}
|
|
223
223
|
let status = 'captured';
|
|
@@ -276,7 +276,7 @@ const qlipCaptureInner = async (page, context, options) => {
|
|
|
276
276
|
pushEntry(store, entry);
|
|
277
277
|
// Persist after every capture so the separate qlip-upload process
|
|
278
278
|
// sees the latest state even if the test run is interrupted.
|
|
279
|
-
await
|
|
279
|
+
await writeEntryFragment(store, entry);
|
|
280
280
|
return entry;
|
|
281
281
|
};
|
|
282
282
|
export { nextStepName }; // exposed for the manual-screenshot path (v1.5)
|
|
@@ -39,16 +39,18 @@ export interface IQlipTestRunnerStore {
|
|
|
39
39
|
/** ms-since-epoch at first capture, for `durationMs`. */
|
|
40
40
|
startedAt: number;
|
|
41
41
|
/**
|
|
42
|
-
* Per-process fragment ID
|
|
43
|
-
*
|
|
44
|
-
*
|
|
45
|
-
*
|
|
46
|
-
*
|
|
42
|
+
* Per-process fragment ID, prefixing every fragment file this
|
|
43
|
+
* process writes under `manifest-fragments/`. Lines up with the
|
|
44
|
+
* Vitest plugin path's model so `mergeManifestFragments()` (in
|
|
45
|
+
* `src/upload/manifest.ts`) works identically across both capture
|
|
46
|
+
* paths.
|
|
47
47
|
*
|
|
48
48
|
* Random + timestamp keeps it unique across parallel workers
|
|
49
49
|
* without coordination.
|
|
50
50
|
*/
|
|
51
51
|
fragmentId: string;
|
|
52
|
+
/** Captures written so far; see `QlipRuntimeState.fragmentSeq`. */
|
|
53
|
+
fragmentSeq: number;
|
|
52
54
|
}
|
|
53
55
|
/**
|
|
54
56
|
* Get-or-initialise the per-process store. Idempotent; safe to
|
|
@@ -62,16 +64,16 @@ export declare const getOrInitStore: (init: {
|
|
|
62
64
|
toolVersion: string;
|
|
63
65
|
}) => IQlipTestRunnerStore;
|
|
64
66
|
/**
|
|
65
|
-
* Write
|
|
66
|
-
*
|
|
67
|
-
*
|
|
67
|
+
* Write one capture's fragment to disk. Called after every capture so
|
|
68
|
+
* the qlip-upload CLI (a separate process) sees it even if the run is
|
|
69
|
+
* interrupted.
|
|
68
70
|
*
|
|
69
|
-
*
|
|
70
|
-
*
|
|
71
|
-
*
|
|
72
|
-
*
|
|
71
|
+
* Append-only, matching the Vitest path: a fragment file is named for
|
|
72
|
+
* `(fragmentId, seq)` and therefore written exactly once, so no two
|
|
73
|
+
* captures — in this process or any other sharing the build dir — can
|
|
74
|
+
* clobber each other. See `design/MANIFEST_FRAGMENTS.md`.
|
|
73
75
|
*/
|
|
74
|
-
export declare const
|
|
76
|
+
export declare const writeEntryFragment: (store: IQlipTestRunnerStore, entry: QlipManifestEntry) => Promise<void>;
|
|
75
77
|
/**
|
|
76
78
|
* Read the current store. Returns `undefined` if `qlipCapture` was
|
|
77
79
|
* never invoked (e.g. globalSetup teardown firing on a run with no
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"manifest-store.d.ts","sourceRoot":"","sources":["../../src/test-runner/manifest-store.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;GAyBG;AAIH,OAAO,KAAK,EACV,YAAY,EACZ,iBAAiB,
|
|
1
|
+
{"version":3,"file":"manifest-store.d.ts","sourceRoot":"","sources":["../../src/test-runner/manifest-store.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;GAyBG;AAIH,OAAO,KAAK,EACV,YAAY,EACZ,iBAAiB,EAEjB,oBAAoB,EACrB,MAAM,aAAa,CAAC;AAUrB,MAAM,WAAW,oBAAoB;IACnC,yDAAyD;IACzD,QAAQ,EAAE,MAAM,CAAC;IACjB,gEAAgE;IAChE,OAAO,EAAE,MAAM,CAAC;IAChB,qEAAqE;IACrE,SAAS,EAAE,MAAM,CAAC;IAClB,0DAA0D;IAC1D,QAAQ,EAAE,YAAY,CAAC;IACvB,4DAA4D;IAC5D,QAAQ,EAAE,GAAG,CAAC,MAAM,EAAE,MAAM,CAAC,CAAC;IAC9B,yDAAyD;IACzD,SAAS,EAAE,MAAM,CAAC;IAClB;;;;;;;;;OASG;IACH,UAAU,EAAE,MAAM,CAAC;IACnB,mEAAmE;IACnE,WAAW,EAAE,MAAM,CAAC;CACrB;AAQD;;;GAGG;AACH,eAAO,MAAM,cAAc,GAAI,MAAM;IACnC,OAAO,EAAE,MAAM,CAAC;IAChB,SAAS,EAAE,MAAM,CAAC;IAClB,QAAQ,EAAE,MAAM,CAAC;IACjB,QAAQ,EAAE,oBAAoB,CAAC;IAC/B,WAAW,EAAE,MAAM,CAAC;CACrB,KAAG,oBA6BH,CAAC;AAEF;;;;;;;;;GASG;AACH,eAAO,MAAM,kBAAkB,GAC7B,OAAO,oBAAoB,EAC3B,OAAO,iBAAiB,KACvB,OAAO,CAAC,IAAI,CAkBd,CAAC;AAEF;;;;GAIG;AACH,eAAO,MAAM,SAAS,QAAO,oBAAoB,GAAG,SAEnD,CAAC;AAEF,sEAAsE;AACtE,eAAO,MAAM,UAAU,QAAO,IAE7B,CAAC;AAEF,yDAAyD;AACzD,eAAO,MAAM,SAAS,GACpB,OAAO,oBAAoB,EAC3B,OAAO,iBAAiB,KACvB,IAaF,CAAC;AAEF,gEAAgE;AAChE,eAAO,MAAM,YAAY,GACvB,OAAO,oBAAoB,EAC3B,SAAS,MAAM,KACd,MAKF,CAAC"}
|
|
@@ -26,7 +26,7 @@
|
|
|
26
26
|
*/
|
|
27
27
|
import { promises as fs } from 'node:fs';
|
|
28
28
|
import path from 'node:path';
|
|
29
|
-
import { MANIFEST_FRAGMENT_DIR, TOOL_NAME, createManifest, } from '../fs/output.js';
|
|
29
|
+
import { MANIFEST_FRAGMENT_DIR, TOOL_NAME, createManifest, fragmentFileName, } from '../fs/output.js';
|
|
30
30
|
const STORE_KEY = Symbol.for('@qoretechnologies/qlip/__test_runner_store__');
|
|
31
31
|
const holder = () => globalThis;
|
|
32
32
|
/**
|
|
@@ -57,23 +57,31 @@ export const getOrInitStore = (init) => {
|
|
|
57
57
|
counters: new Map(),
|
|
58
58
|
startedAt: Date.now(),
|
|
59
59
|
fragmentId,
|
|
60
|
+
fragmentSeq: 0,
|
|
60
61
|
};
|
|
61
62
|
holder()[STORE_KEY] = store;
|
|
62
63
|
return store;
|
|
63
64
|
};
|
|
64
65
|
/**
|
|
65
|
-
* Write
|
|
66
|
-
*
|
|
67
|
-
*
|
|
66
|
+
* Write one capture's fragment to disk. Called after every capture so
|
|
67
|
+
* the qlip-upload CLI (a separate process) sees it even if the run is
|
|
68
|
+
* interrupted.
|
|
68
69
|
*
|
|
69
|
-
*
|
|
70
|
-
*
|
|
71
|
-
*
|
|
72
|
-
*
|
|
70
|
+
* Append-only, matching the Vitest path: a fragment file is named for
|
|
71
|
+
* `(fragmentId, seq)` and therefore written exactly once, so no two
|
|
72
|
+
* captures — in this process or any other sharing the build dir — can
|
|
73
|
+
* clobber each other. See `design/MANIFEST_FRAGMENTS.md`.
|
|
73
74
|
*/
|
|
74
|
-
export const
|
|
75
|
-
|
|
76
|
-
|
|
75
|
+
export const writeEntryFragment = async (store, entry) => {
|
|
76
|
+
store.fragmentSeq += 1;
|
|
77
|
+
const seq = store.fragmentSeq;
|
|
78
|
+
const fragment = {
|
|
79
|
+
...store.manifest,
|
|
80
|
+
entries: [entry],
|
|
81
|
+
fragmentId: store.fragmentId,
|
|
82
|
+
fragmentSeq: seq,
|
|
83
|
+
};
|
|
84
|
+
await fs.writeFile(path.join(store.buildDir, MANIFEST_FRAGMENT_DIR, fragmentFileName(store.fragmentId, seq)), JSON.stringify(fragment, null, 2), 'utf-8');
|
|
77
85
|
};
|
|
78
86
|
/**
|
|
79
87
|
* Read the current store. Returns `undefined` if `qlipCapture` was
|
package/dist/types.d.ts
CHANGED
|
@@ -101,6 +101,13 @@ export interface QlipUploadOptions {
|
|
|
101
101
|
* Set to `true` in CI when you want red builds on upload errors.
|
|
102
102
|
*/
|
|
103
103
|
failOnUploadError?: boolean;
|
|
104
|
+
/**
|
|
105
|
+
* Whether a partial build should fail the run. A build is partial
|
|
106
|
+
* when PNGs exist on disk that no manifest entry references — i.e.
|
|
107
|
+
* captures whose manifest record was lost. Default: `false` (warn
|
|
108
|
+
* only). See `design/MANIFEST_FRAGMENTS.md`.
|
|
109
|
+
*/
|
|
110
|
+
failOnPartialBuild?: boolean;
|
|
104
111
|
}
|
|
105
112
|
export interface QlipPluginOptions extends QlipCaptureOptions {
|
|
106
113
|
outputDir?: string;
|
|
@@ -111,6 +118,13 @@ export interface QlipPluginOptions extends QlipCaptureOptions {
|
|
|
111
118
|
* happens — the screenshots stay on local disk.
|
|
112
119
|
*/
|
|
113
120
|
upload?: QlipUploadOptions;
|
|
121
|
+
/**
|
|
122
|
+
* Log one line per capture (story, kind, path, context + sequence)
|
|
123
|
+
* and keep it in the build's `capture-report.json`. Off by default;
|
|
124
|
+
* `$QLIP_DEBUG=1` turns it on without touching config. Use it when a
|
|
125
|
+
* CI build captured fewer stories than it ran.
|
|
126
|
+
*/
|
|
127
|
+
diagnostics?: boolean;
|
|
114
128
|
}
|
|
115
129
|
export type QlipResolvedDefaults = Required<QlipCaptureOptions> & {
|
|
116
130
|
outputDir: string;
|
|
@@ -125,6 +139,8 @@ export interface QlipRuntimeConfig {
|
|
|
125
139
|
name: string;
|
|
126
140
|
version: string;
|
|
127
141
|
};
|
|
142
|
+
/** See `QlipPluginOptions.diagnostics`. */
|
|
143
|
+
diagnostics?: boolean;
|
|
128
144
|
}
|
|
129
145
|
export interface QlipScreenshotOptions extends QlipCaptureOptions {
|
|
130
146
|
name?: string;
|
|
@@ -256,4 +272,35 @@ export interface QlipManifest {
|
|
|
256
272
|
};
|
|
257
273
|
entries: QlipManifestEntry[];
|
|
258
274
|
}
|
|
275
|
+
/**
|
|
276
|
+
* Marks every entry for `(storyId, kind)` as stale, whatever order the
|
|
277
|
+
* fragments merge in. Written by the retry-mask prune when a story
|
|
278
|
+
* passes on a later attempt: the error captures from the failed
|
|
279
|
+
* attempts are no longer real failures. Order-independent by design —
|
|
280
|
+
* "this story ultimately passed" is true regardless of when the
|
|
281
|
+
* tombstone lands relative to the entries it retracts.
|
|
282
|
+
*/
|
|
283
|
+
export interface QlipManifestTombstone {
|
|
284
|
+
storyId: string;
|
|
285
|
+
kind: QlipEntryKind;
|
|
286
|
+
}
|
|
287
|
+
/**
|
|
288
|
+
* One capture's on-disk record under `<buildDir>/manifest-fragments/`.
|
|
289
|
+
*
|
|
290
|
+
* Fragments are append-only: one file per capture, written exactly
|
|
291
|
+
* once and never rewritten, so concurrent captures — in one context,
|
|
292
|
+
* across contexts, or across processes sharing a `--build-dir` —
|
|
293
|
+
* cannot clobber each other. `entries` therefore holds a single entry
|
|
294
|
+
* (or none, for a tombstone-only fragment), but the merger accepts any
|
|
295
|
+
* count so fragments written by older qlip versions still merge.
|
|
296
|
+
*
|
|
297
|
+
* See `design/MANIFEST_FRAGMENTS.md`.
|
|
298
|
+
*/
|
|
299
|
+
export interface QlipManifestFragment extends QlipManifest {
|
|
300
|
+
/** Identifies the writing browser context / process. */
|
|
301
|
+
fragmentId?: string;
|
|
302
|
+
/** Per-context capture counter; orders entries within a context. */
|
|
303
|
+
fragmentSeq?: number;
|
|
304
|
+
tombstones?: QlipManifestTombstone[];
|
|
305
|
+
}
|
|
259
306
|
//# sourceMappingURL=types.d.ts.map
|
package/dist/types.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"types.d.ts","sourceRoot":"","sources":["../src/types.ts"],"names":[],"mappings":"AAAA,MAAM,WAAW,YAAY;IAC3B,KAAK,EAAE,MAAM,CAAC;IACd,MAAM,EAAE,MAAM,CAAC;CAChB;AAED,MAAM,WAAW,kBAAkB;IACjC,IAAI,CAAC,EAAE,OAAO,CAAC;IACf,QAAQ,CAAC,EAAE,YAAY,CAAC;IACxB,iBAAiB,CAAC,EAAE,OAAO,CAAC;IAC5B,oBAAoB,CAAC,EAAE,OAAO,CAAC;IAC/B;;;;;;;;;OASG;IACH,qBAAqB,CAAC,EAAE,OAAO,CAAC;IAChC,aAAa,CAAC,EAAE,MAAM,CAAC;IACvB,gBAAgB,CAAC,EAAE,MAAM,CAAC;IAC1B,cAAc,CAAC,EAAE,MAAM,EAAE,CAAC;IAC1B,IAAI,CAAC,EAAE,OAAO,CAAC;IACf,MAAM,CAAC,EAAE,OAAO,CAAC;IACjB,KAAK,CAAC,EAAE,OAAO,CAAC;IAChB,cAAc,CAAC,EAAE,OAAO,CAAC;IACzB,oBAAoB,CAAC,EAAE,gBAAgB,EAAE,CAAC;IAC1C,cAAc,CAAC,EAAE,MAAM,CAAC;IACxB,yBAAyB,CAAC,EAAE,MAAM,EAAE,CAAC;CACtC;AAED,MAAM,WAAW,cAAe,SAAQ,kBAAkB;IACxD,cAAc,CAAC,EAAE,OAAO,CAAC;CAC1B;AAED;;;GAGG;AACH,MAAM,WAAW,iBAAiB;IAChC;;;;;;OAMG;IACH,SAAS,CAAC,EAAE,MAAM,CAAC;IACnB;;;;;;;;;;OAUG;IACH,WAAW,CAAC,EAAE,MAAM,CAAC;IACrB,2DAA2D;IAC3D,OAAO,CAAC,EAAE,MAAM,CAAC;IACjB;;;OAGG;IACH,MAAM,CAAC,EAAE,MAAM,CAAC;IAChB;;;OAGG;IACH,MAAM,CAAC,EAAE,MAAM,CAAC;IAChB;;;;;OAKG;IACH,UAAU,CAAC,EAAE,MAAM,CAAC;IACpB;;;;;OAKG;IACH,eAAe,CAAC,EAAE,MAAM,EAAE,CAAC;IAC3B;;;;;;;;OAQG;IACH,cAAc,CAAC,EAAE,MAAM,CAAC;IACxB,wDAAwD;IACxD,QAAQ,CAAC,EAAE,OAAO,CAAC;IACnB;;;;OAIG;IACH,iBAAiB,CAAC,EAAE,OAAO,CAAC;
|
|
1
|
+
{"version":3,"file":"types.d.ts","sourceRoot":"","sources":["../src/types.ts"],"names":[],"mappings":"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"}
|