@juxtapose/core 0.2.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.
- package/LICENSE +21 -0
- package/README.md +17 -0
- package/dist/agent/handoff.d.ts +30 -0
- package/dist/agent/handoff.js +121 -0
- package/dist/agent/handoff.js.map +1 -0
- package/dist/analysis/affected.d.ts +19 -0
- package/dist/analysis/affected.js +132 -0
- package/dist/analysis/affected.js.map +1 -0
- package/dist/baseline/git-store.d.ts +26 -0
- package/dist/baseline/git-store.js +95 -0
- package/dist/baseline/git-store.js.map +1 -0
- package/dist/baseline/status.d.ts +22 -0
- package/dist/baseline/status.js +49 -0
- package/dist/baseline/status.js.map +1 -0
- package/dist/capture/capture.d.ts +32 -0
- package/dist/capture/capture.js +153 -0
- package/dist/capture/capture.js.map +1 -0
- package/dist/capture/launch.d.ts +35 -0
- package/dist/capture/launch.js +56 -0
- package/dist/capture/launch.js.map +1 -0
- package/dist/capture/pool.d.ts +42 -0
- package/dist/capture/pool.js +109 -0
- package/dist/capture/pool.js.map +1 -0
- package/dist/capture/settle.d.ts +50 -0
- package/dist/capture/settle.js +162 -0
- package/dist/capture/settle.js.map +1 -0
- package/dist/diff/diff.d.ts +25 -0
- package/dist/diff/diff.js +86 -0
- package/dist/diff/diff.js.map +1 -0
- package/dist/diff/regions.d.ts +8 -0
- package/dist/diff/regions.js +90 -0
- package/dist/diff/regions.js.map +1 -0
- package/dist/git/ancestry.d.ts +5 -0
- package/dist/git/ancestry.js +43 -0
- package/dist/git/ancestry.js.map +1 -0
- package/dist/index.d.ts +17 -0
- package/dist/index.js +16 -0
- package/dist/index.js.map +1 -0
- package/dist/platform.d.ts +83 -0
- package/dist/platform.js +5 -0
- package/dist/platform.js.map +1 -0
- package/dist/report/comment.d.ts +16 -0
- package/dist/report/comment.js +99 -0
- package/dist/report/comment.js.map +1 -0
- package/dist/report/github.d.ts +28 -0
- package/dist/report/github.js +118 -0
- package/dist/report/github.js.map +1 -0
- package/dist/run/run.d.ts +30 -0
- package/dist/run/run.js +120 -0
- package/dist/run/run.js.map +1 -0
- package/dist/types.d.ts +137 -0
- package/dist/types.js +4 -0
- package/dist/types.js.map +1 -0
- package/dist/url-platform.d.ts +10 -0
- package/dist/url-platform.js +22 -0
- package/dist/url-platform.js.map +1 -0
- package/package.json +42 -0
|
@@ -0,0 +1,118 @@
|
|
|
1
|
+
// Post the run summary as a PR comment, updating rather than accumulating.
|
|
2
|
+
//
|
|
3
|
+
// Hand-rolled against the REST API instead of pulling in Octokit: this needs
|
|
4
|
+
// three endpoints, and the CLI's install size is a real cost for a tool people
|
|
5
|
+
// add to every project.
|
|
6
|
+
import { readFile } from 'node:fs/promises';
|
|
7
|
+
import { COMMENT_MARKER } from './comment.js';
|
|
8
|
+
/**
|
|
9
|
+
* Assemble the context from the workflow environment.
|
|
10
|
+
*
|
|
11
|
+
* Returns null when anything is missing — running locally, on a push rather
|
|
12
|
+
* than a pull request, or without a token. That is an ordinary state, not an
|
|
13
|
+
* error: reporting is a side effect, and a missing token should never fail a
|
|
14
|
+
* build that otherwise passed.
|
|
15
|
+
*/
|
|
16
|
+
export async function resolveGitHubContext(env = process.env) {
|
|
17
|
+
const token = env.GITHUB_TOKEN ?? env.GH_TOKEN;
|
|
18
|
+
const repository = env.GITHUB_REPOSITORY;
|
|
19
|
+
if (!token || !repository)
|
|
20
|
+
return null;
|
|
21
|
+
const prNumber = await readPrNumber(env);
|
|
22
|
+
if (prNumber === null)
|
|
23
|
+
return null;
|
|
24
|
+
return {
|
|
25
|
+
token,
|
|
26
|
+
repository,
|
|
27
|
+
prNumber,
|
|
28
|
+
apiUrl: env.GITHUB_API_URL ?? 'https://api.github.com',
|
|
29
|
+
};
|
|
30
|
+
}
|
|
31
|
+
/**
|
|
32
|
+
* Find the PR number.
|
|
33
|
+
*
|
|
34
|
+
* `GITHUB_EVENT_PATH` points at the full webhook payload, which carries it for
|
|
35
|
+
* `pull_request` events. On a `push` there is no PR and we correctly find
|
|
36
|
+
* nothing. `GITHUB_REF` is checked as a fallback for workflows that re-dispatch
|
|
37
|
+
* with a synthesized ref.
|
|
38
|
+
*/
|
|
39
|
+
async function readPrNumber(env) {
|
|
40
|
+
const eventPath = env.GITHUB_EVENT_PATH;
|
|
41
|
+
if (eventPath) {
|
|
42
|
+
try {
|
|
43
|
+
const event = JSON.parse(await readFile(eventPath, 'utf8'));
|
|
44
|
+
const n = event.pull_request?.number ?? event.number;
|
|
45
|
+
if (typeof n === 'number')
|
|
46
|
+
return n;
|
|
47
|
+
}
|
|
48
|
+
catch {
|
|
49
|
+
// Malformed or unreadable payload; fall through to the ref.
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
// refs/pull/123/merge
|
|
53
|
+
const match = /^refs\/pull\/(\d+)\//.exec(env.GITHUB_REF ?? '');
|
|
54
|
+
return match ? Number(match[1]) : null;
|
|
55
|
+
}
|
|
56
|
+
/**
|
|
57
|
+
* Create or update the juxtapose comment.
|
|
58
|
+
*
|
|
59
|
+
* Returns the comment's URL, or null if nothing was posted. Never throws: a
|
|
60
|
+
* reporting failure must not fail a build. The caller decides the exit code
|
|
61
|
+
* from the run itself, which is the thing that actually matters.
|
|
62
|
+
*/
|
|
63
|
+
export async function postComment(ctx, body) {
|
|
64
|
+
try {
|
|
65
|
+
const existing = await findOwnComment(ctx);
|
|
66
|
+
const res = existing
|
|
67
|
+
? await api(ctx, `/repos/${ctx.repository}/issues/comments/${existing.id}`, {
|
|
68
|
+
method: 'PATCH',
|
|
69
|
+
body: JSON.stringify({ body }),
|
|
70
|
+
})
|
|
71
|
+
: await api(ctx, `/repos/${ctx.repository}/issues/${ctx.prNumber}/comments`, {
|
|
72
|
+
method: 'POST',
|
|
73
|
+
body: JSON.stringify({ body }),
|
|
74
|
+
});
|
|
75
|
+
if (!res.ok)
|
|
76
|
+
return null;
|
|
77
|
+
const created = (await res.json());
|
|
78
|
+
return created.html_url ?? null;
|
|
79
|
+
}
|
|
80
|
+
catch {
|
|
81
|
+
return null;
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
/**
|
|
85
|
+
* Locate our previous comment by its marker.
|
|
86
|
+
*
|
|
87
|
+
* Paginated because a busy PR can exceed one page, and our comment is
|
|
88
|
+
* typically the oldest — posted on the first CI run — so stopping at page one
|
|
89
|
+
* would make us post a duplicate on every subsequent push.
|
|
90
|
+
*/
|
|
91
|
+
async function findOwnComment(ctx) {
|
|
92
|
+
for (let page = 1; page <= MAX_PAGES; page++) {
|
|
93
|
+
const res = await api(ctx, `/repos/${ctx.repository}/issues/${ctx.prNumber}/comments?per_page=100&page=${page}`);
|
|
94
|
+
if (!res.ok)
|
|
95
|
+
return null;
|
|
96
|
+
const comments = (await res.json());
|
|
97
|
+
const found = comments.find((c) => c.body?.includes(COMMENT_MARKER));
|
|
98
|
+
if (found)
|
|
99
|
+
return found;
|
|
100
|
+
if (comments.length < 100)
|
|
101
|
+
return null;
|
|
102
|
+
}
|
|
103
|
+
return null;
|
|
104
|
+
}
|
|
105
|
+
const MAX_PAGES = 10;
|
|
106
|
+
function api(ctx, path, init = {}) {
|
|
107
|
+
return fetch(`${ctx.apiUrl}${path}`, {
|
|
108
|
+
...init,
|
|
109
|
+
headers: {
|
|
110
|
+
accept: 'application/vnd.github+json',
|
|
111
|
+
authorization: `Bearer ${ctx.token}`,
|
|
112
|
+
'x-github-api-version': '2022-11-28',
|
|
113
|
+
'content-type': 'application/json',
|
|
114
|
+
...init.headers,
|
|
115
|
+
},
|
|
116
|
+
});
|
|
117
|
+
}
|
|
118
|
+
//# sourceMappingURL=github.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"github.js","sourceRoot":"","sources":["../../src/report/github.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
import type { Browser } from 'playwright';
|
|
2
|
+
import type { Platform } from '../platform.js';
|
|
3
|
+
import type { BaselineStore, DiffStatus, ProjectConfig, RunReport, StoryManifest, Viewport } from '../types.js';
|
|
4
|
+
import { type AncestryCheck } from '../baseline/status.js';
|
|
5
|
+
export declare const DEFAULT_VIEWPORT: Viewport;
|
|
6
|
+
export interface RunOptions {
|
|
7
|
+
browser: Browser;
|
|
8
|
+
platform: Platform;
|
|
9
|
+
store: BaselineStore;
|
|
10
|
+
baseUrl: string;
|
|
11
|
+
stories: StoryManifest[];
|
|
12
|
+
/** Total story count before affected-filtering, for the report. */
|
|
13
|
+
totalCount: number;
|
|
14
|
+
config?: ProjectConfig;
|
|
15
|
+
ancestry?: AncestryCheck;
|
|
16
|
+
runId: string;
|
|
17
|
+
branch: string;
|
|
18
|
+
commitSha: string;
|
|
19
|
+
/** Receives each story's images so the caller decides where they land. */
|
|
20
|
+
onArtifact?: (artifact: RunArtifact) => Promise<void>;
|
|
21
|
+
onProgress?: (done: number, total: number, storyId: string) => void;
|
|
22
|
+
signal?: AbortSignal;
|
|
23
|
+
}
|
|
24
|
+
export interface RunArtifact {
|
|
25
|
+
storyId: string;
|
|
26
|
+
status: DiffStatus;
|
|
27
|
+
current: Buffer;
|
|
28
|
+
diff?: Buffer;
|
|
29
|
+
}
|
|
30
|
+
export declare function runCapture(opts: RunOptions): Promise<RunReport>;
|
package/dist/run/run.js
ADDED
|
@@ -0,0 +1,120 @@
|
|
|
1
|
+
// Orchestrate a run: enumerate → select → capture → judge → report.
|
|
2
|
+
//
|
|
3
|
+
// The ordering reflects the cost model. Hashing is cheap and decides pass/fail,
|
|
4
|
+
// so it runs for every story. Decoding and diffing are expensive and explain
|
|
5
|
+
// nothing about a passing story, so they run only for stories that already
|
|
6
|
+
// failed. On a green run this function never decodes a single image.
|
|
7
|
+
import { capturePool } from '../capture/pool.js';
|
|
8
|
+
import { resolveStatus, toDiffStatus } from '../baseline/status.js';
|
|
9
|
+
import { diffImages, resolveThreshold, resolveMinDiffPercent } from '../diff/diff.js';
|
|
10
|
+
export const DEFAULT_VIEWPORT = { width: 1280, height: 800 };
|
|
11
|
+
export async function runCapture(opts) {
|
|
12
|
+
const startedAt = new Date();
|
|
13
|
+
const started = Date.now();
|
|
14
|
+
const viewport = opts.config?.viewport ?? DEFAULT_VIEWPORT;
|
|
15
|
+
const manifest = await opts.store.readManifest();
|
|
16
|
+
const results = [];
|
|
17
|
+
const pool = capturePool(opts.stories, {
|
|
18
|
+
browser: opts.browser,
|
|
19
|
+
platform: opts.platform,
|
|
20
|
+
baseUrl: opts.baseUrl,
|
|
21
|
+
concurrency: opts.config?.concurrency,
|
|
22
|
+
warmSwitch: opts.config?.warmSwitch,
|
|
23
|
+
viewport,
|
|
24
|
+
onProgress: opts.onProgress,
|
|
25
|
+
signal: opts.signal,
|
|
26
|
+
});
|
|
27
|
+
for await (const outcome of pool) {
|
|
28
|
+
if (outcome.skipped) {
|
|
29
|
+
results.push({
|
|
30
|
+
storyId: outcome.story.storyId,
|
|
31
|
+
status: 'skip',
|
|
32
|
+
mismatchPixels: 0,
|
|
33
|
+
mismatchPercent: 0,
|
|
34
|
+
changedRegions: [],
|
|
35
|
+
durationMs: 0,
|
|
36
|
+
});
|
|
37
|
+
continue;
|
|
38
|
+
}
|
|
39
|
+
if (outcome.error || !outcome.snapshot) {
|
|
40
|
+
results.push({
|
|
41
|
+
storyId: outcome.story.storyId,
|
|
42
|
+
status: 'fail',
|
|
43
|
+
mismatchPixels: 0,
|
|
44
|
+
mismatchPercent: 0,
|
|
45
|
+
changedRegions: [],
|
|
46
|
+
error: outcome.error?.message ?? 'capture produced no snapshot',
|
|
47
|
+
durationMs: 0,
|
|
48
|
+
});
|
|
49
|
+
continue;
|
|
50
|
+
}
|
|
51
|
+
results.push(await judge(outcome.snapshot, outcome.story, manifest, opts));
|
|
52
|
+
}
|
|
53
|
+
const summary = tally(results);
|
|
54
|
+
return {
|
|
55
|
+
runId: opts.runId,
|
|
56
|
+
branch: opts.branch,
|
|
57
|
+
commitSha: opts.commitSha,
|
|
58
|
+
capturedCount: opts.stories.length,
|
|
59
|
+
totalCount: opts.totalCount,
|
|
60
|
+
results: results.sort((a, b) => a.storyId.localeCompare(b.storyId)),
|
|
61
|
+
summary,
|
|
62
|
+
durationMs: Date.now() - started,
|
|
63
|
+
startedAt: startedAt.toISOString(),
|
|
64
|
+
};
|
|
65
|
+
}
|
|
66
|
+
/** Decide one story's fate. Cheap path first. */
|
|
67
|
+
async function judge(snapshot, story, manifest, opts) {
|
|
68
|
+
const baselineStatus = resolveStatus(story.storyId, snapshot.image, manifest, opts.ancestry);
|
|
69
|
+
const status = toDiffStatus(baselineStatus);
|
|
70
|
+
const base = {
|
|
71
|
+
storyId: story.storyId,
|
|
72
|
+
status,
|
|
73
|
+
mismatchPixels: 0,
|
|
74
|
+
mismatchPercent: 0,
|
|
75
|
+
changedRegions: [],
|
|
76
|
+
durationMs: snapshot.durationMs,
|
|
77
|
+
};
|
|
78
|
+
// pass / new / stale need no pixel work: there is either nothing to explain
|
|
79
|
+
// or nothing to compare against.
|
|
80
|
+
if (status !== 'fail') {
|
|
81
|
+
await opts.onArtifact?.({ storyId: story.storyId, status, current: snapshot.image });
|
|
82
|
+
return base;
|
|
83
|
+
}
|
|
84
|
+
const baseline = await opts.store.get(story.storyId);
|
|
85
|
+
if (!baseline) {
|
|
86
|
+
// The manifest claimed a baseline the store can't produce.
|
|
87
|
+
await opts.onArtifact?.({ storyId: story.storyId, status, current: snapshot.image });
|
|
88
|
+
return { ...base, error: 'baseline missing from store' };
|
|
89
|
+
}
|
|
90
|
+
const diff = await diffImages(baseline, snapshot.image, {
|
|
91
|
+
threshold: resolveThreshold(story.params, opts.config),
|
|
92
|
+
minDiffPercent: resolveMinDiffPercent(story.params, opts.config),
|
|
93
|
+
});
|
|
94
|
+
await opts.onArtifact?.({
|
|
95
|
+
storyId: story.storyId,
|
|
96
|
+
status,
|
|
97
|
+
current: snapshot.image,
|
|
98
|
+
diff: diff.diffImage,
|
|
99
|
+
});
|
|
100
|
+
return {
|
|
101
|
+
...base,
|
|
102
|
+
mismatchPixels: diff.mismatchPixels,
|
|
103
|
+
mismatchPercent: diff.mismatchPercent,
|
|
104
|
+
changedRegions: diff.changedRegions,
|
|
105
|
+
error: diff.dimensionMismatch ? 'viewport size changed since baseline' : undefined,
|
|
106
|
+
};
|
|
107
|
+
}
|
|
108
|
+
function tally(results) {
|
|
109
|
+
const summary = {
|
|
110
|
+
pass: 0,
|
|
111
|
+
fail: 0,
|
|
112
|
+
new: 0,
|
|
113
|
+
stale: 0,
|
|
114
|
+
skip: 0,
|
|
115
|
+
};
|
|
116
|
+
for (const r of results)
|
|
117
|
+
summary[r.status]++;
|
|
118
|
+
return summary;
|
|
119
|
+
}
|
|
120
|
+
//# sourceMappingURL=run.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"run.js","sourceRoot":"","sources":["../../src/run/run.ts"],"names":[],"mappings":"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"}
|
package/dist/types.d.ts
ADDED
|
@@ -0,0 +1,137 @@
|
|
|
1
|
+
/** Per-story tuning, read from the platform's story parameters. */
|
|
2
|
+
export interface StoryParams {
|
|
3
|
+
/** Exclude this story from capture entirely. */
|
|
4
|
+
skip?: boolean;
|
|
5
|
+
viewport?: Viewport;
|
|
6
|
+
/** Fixed extra wait after settle, ms. Escape hatch; prefer waitForSelector. */
|
|
7
|
+
delay?: number;
|
|
8
|
+
/** pixelmatch colour-distance threshold, 0..1. Default 0.1. */
|
|
9
|
+
threshold?: number;
|
|
10
|
+
/** Mismatch % below which a diff still passes. Suppresses AA noise. Default 0. */
|
|
11
|
+
minDiffPercent?: number;
|
|
12
|
+
/** CSS animations during capture. Default 'disabled' so spinners don't flake. */
|
|
13
|
+
animations?: 'play' | 'disabled';
|
|
14
|
+
/** Capture after the story's interaction/play phase rather than at first render. */
|
|
15
|
+
captureAfterPlay?: boolean;
|
|
16
|
+
/** Wait for this selector before screenshotting. For async content the
|
|
17
|
+
* network drain can't see (SDK init, lazy canvas). */
|
|
18
|
+
waitForSelector?: string;
|
|
19
|
+
/** Max ms to wait for render-ready before screenshotting anyway. Default 2000. */
|
|
20
|
+
captureTimeout?: number;
|
|
21
|
+
}
|
|
22
|
+
export interface Viewport {
|
|
23
|
+
width: number;
|
|
24
|
+
height: number;
|
|
25
|
+
}
|
|
26
|
+
/** One capturable story, normalised across platforms. */
|
|
27
|
+
export interface StoryManifest {
|
|
28
|
+
/** Stable, URL-safe. Platform-native id (Storybook's `id`, Ladle's key). */
|
|
29
|
+
storyId: string;
|
|
30
|
+
/** Group/kind, e.g. "Components/Button". */
|
|
31
|
+
title: string;
|
|
32
|
+
/** Story name within the group, e.g. "Primary". */
|
|
33
|
+
name: string;
|
|
34
|
+
/** Absolute path to the source file. Drives affected-story analysis;
|
|
35
|
+
* empty string when the platform can't report it (URL-list adapter). */
|
|
36
|
+
componentPath: string;
|
|
37
|
+
params: StoryParams;
|
|
38
|
+
}
|
|
39
|
+
export interface Snapshot {
|
|
40
|
+
storyId: string;
|
|
41
|
+
/** WebP-encoded. PNG is an intermediate, never stored. */
|
|
42
|
+
image: Buffer;
|
|
43
|
+
viewport: Viewport;
|
|
44
|
+
capturedAt: string;
|
|
45
|
+
/** Wall time for this story's capture, ms. */
|
|
46
|
+
durationMs: number;
|
|
47
|
+
}
|
|
48
|
+
export interface ChangedRegion {
|
|
49
|
+
x: number;
|
|
50
|
+
y: number;
|
|
51
|
+
width: number;
|
|
52
|
+
height: number;
|
|
53
|
+
}
|
|
54
|
+
export type DiffStatus = 'pass' | 'fail' | 'new' | 'stale' | 'skip';
|
|
55
|
+
export interface DiffResult {
|
|
56
|
+
storyId: string;
|
|
57
|
+
status: DiffStatus;
|
|
58
|
+
mismatchPixels: number;
|
|
59
|
+
mismatchPercent: number;
|
|
60
|
+
/** Bounding boxes of changed clusters. Empty unless status is 'fail'. */
|
|
61
|
+
changedRegions: ChangedRegion[];
|
|
62
|
+
/** Set when the capture or diff threw. status is 'fail' in that case. */
|
|
63
|
+
error?: string;
|
|
64
|
+
durationMs: number;
|
|
65
|
+
}
|
|
66
|
+
export interface RunReport {
|
|
67
|
+
runId: string;
|
|
68
|
+
branch: string;
|
|
69
|
+
commitSha: string;
|
|
70
|
+
/** How many stories we captured vs how many exist. */
|
|
71
|
+
capturedCount: number;
|
|
72
|
+
totalCount: number;
|
|
73
|
+
results: DiffResult[];
|
|
74
|
+
summary: Record<DiffStatus, number>;
|
|
75
|
+
durationMs: number;
|
|
76
|
+
startedAt: string;
|
|
77
|
+
}
|
|
78
|
+
export interface ApprovalMeta {
|
|
79
|
+
commit: string;
|
|
80
|
+
approvedBy: string;
|
|
81
|
+
approvedAt: string;
|
|
82
|
+
}
|
|
83
|
+
export interface BaselineEntry extends ApprovalMeta {
|
|
84
|
+
/** sha256 of the baseline image bytes. Detects drift between manifest
|
|
85
|
+
* and store without downloading the image. */
|
|
86
|
+
hash: string;
|
|
87
|
+
}
|
|
88
|
+
/** Committed to the repo. The source of truth for what "approved" means,
|
|
89
|
+
* regardless of which store holds the actual bytes. */
|
|
90
|
+
export interface BaselineManifest {
|
|
91
|
+
version: 1;
|
|
92
|
+
baselines: Record<string, BaselineEntry>;
|
|
93
|
+
}
|
|
94
|
+
/** Project config, from juxtapose.config.{ts,js,json} or package.json#juxtapose. */
|
|
95
|
+
export interface ProjectConfig {
|
|
96
|
+
/** Which workbench. Autodetected when omitted. */
|
|
97
|
+
platform?: 'storybook' | 'ladle';
|
|
98
|
+
/** Where the built static workbench lives. */
|
|
99
|
+
buildDir?: string;
|
|
100
|
+
/** Capture against an already-running dev server instead of building. */
|
|
101
|
+
origin?: string;
|
|
102
|
+
/** Parallel pages. Default 8. More is not better — CPU saturation
|
|
103
|
+
* causes render timeouts. Scale to core count. */
|
|
104
|
+
concurrency?: number;
|
|
105
|
+
/** Reuse warm pages and switch stories in place instead of navigating.
|
|
106
|
+
* Much faster, but warm and cold captures do not currently produce
|
|
107
|
+
* identical pixels, which makes results depend on queue order under a
|
|
108
|
+
* byte-exact hash. Opt in only if you have measured it on your suite. */
|
|
109
|
+
warmSwitch?: boolean;
|
|
110
|
+
viewport?: Viewport;
|
|
111
|
+
threshold?: number;
|
|
112
|
+
minDiffPercent?: number;
|
|
113
|
+
/** Glob patterns of story ids to never capture. */
|
|
114
|
+
ignore?: string[];
|
|
115
|
+
storage?: StorageConfig;
|
|
116
|
+
}
|
|
117
|
+
export type StorageConfig = {
|
|
118
|
+
kind: 'git';
|
|
119
|
+
dir?: string;
|
|
120
|
+
} | {
|
|
121
|
+
kind: 'fs';
|
|
122
|
+
dir: string;
|
|
123
|
+
} | {
|
|
124
|
+
kind: 's3';
|
|
125
|
+
bucket: string;
|
|
126
|
+
prefix?: string;
|
|
127
|
+
endpoint?: string;
|
|
128
|
+
};
|
|
129
|
+
/** Baseline persistence. The manifest always lives in git; only the image
|
|
130
|
+
* bytes vary by implementation. */
|
|
131
|
+
export interface BaselineStore {
|
|
132
|
+
get(storyId: string): Promise<Buffer | null>;
|
|
133
|
+
put(storyId: string, image: Buffer, meta: ApprovalMeta): Promise<void>;
|
|
134
|
+
remove(storyId: string): Promise<void>;
|
|
135
|
+
readManifest(): Promise<BaselineManifest>;
|
|
136
|
+
writeManifest(manifest: BaselineManifest): Promise<void>;
|
|
137
|
+
}
|
package/dist/types.js
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"types.js","sourceRoot":"","sources":["../src/types.ts"],"names":[],"mappings":"AAAA,+EAA+E;AAC/E,uCAAuC"}
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
import type { Page } from 'playwright';
|
|
2
|
+
/**
|
|
3
|
+
* Build a `showStory` implementation from a URL builder.
|
|
4
|
+
*
|
|
5
|
+
* Note `waitUntil: 'load'` rather than `networkidle`: the platform's own
|
|
6
|
+
* render signal and core's settle pipeline decide when the page is actually
|
|
7
|
+
* ready. Waiting for network idle here would be both slower and less reliable
|
|
8
|
+
* — a story that polls would never satisfy it.
|
|
9
|
+
*/
|
|
10
|
+
export declare function navigateToStory(storyUrl: (baseUrl: string, storyId: string) => string): (page: Page, storyId: string, baseUrl: string) => Promise<void>;
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
// Helper for platforms that address a story by URL.
|
|
2
|
+
//
|
|
3
|
+
// Most workbenches do: Storybook has iframe.html?id=, Ladle has ?story=. For
|
|
4
|
+
// those, "show me this story" is just a navigation, and there is no reason for
|
|
5
|
+
// each adapter to write the same three lines.
|
|
6
|
+
//
|
|
7
|
+
// Playwright's component-testing galleries are the exception that made
|
|
8
|
+
// `showStory` an action rather than a URL — see Platform.showStory.
|
|
9
|
+
/**
|
|
10
|
+
* Build a `showStory` implementation from a URL builder.
|
|
11
|
+
*
|
|
12
|
+
* Note `waitUntil: 'load'` rather than `networkidle`: the platform's own
|
|
13
|
+
* render signal and core's settle pipeline decide when the page is actually
|
|
14
|
+
* ready. Waiting for network idle here would be both slower and less reliable
|
|
15
|
+
* — a story that polls would never satisfy it.
|
|
16
|
+
*/
|
|
17
|
+
export function navigateToStory(storyUrl) {
|
|
18
|
+
return async (page, storyId, baseUrl) => {
|
|
19
|
+
await page.goto(storyUrl(baseUrl, storyId), { waitUntil: 'load' });
|
|
20
|
+
};
|
|
21
|
+
}
|
|
22
|
+
//# sourceMappingURL=url-platform.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"url-platform.js","sourceRoot":"","sources":["../src/url-platform.ts"],"names":[],"mappings":"AAAA,oDAAoD;AACpD,EAAE;AACF,6EAA6E;AAC7E,+EAA+E;AAC/E,8CAA8C;AAC9C,EAAE;AACF,uEAAuE;AACvE,oEAAoE;AAIpE;;;;;;;GAOG;AACH,MAAM,UAAU,eAAe,CAC7B,QAAsD;IAEtD,OAAO,KAAK,EAAE,IAAI,EAAE,OAAO,EAAE,OAAO,EAAE,EAAE;QACtC,MAAM,IAAI,CAAC,IAAI,CAAC,QAAQ,CAAC,OAAO,EAAE,OAAO,CAAC,EAAE,EAAE,SAAS,EAAE,MAAM,EAAE,CAAC,CAAC;IACrE,CAAC,CAAC;AACJ,CAAC"}
|
package/package.json
ADDED
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@juxtapose/core",
|
|
3
|
+
"version": "0.2.0",
|
|
4
|
+
"type": "module",
|
|
5
|
+
"description": "Platform-agnostic capture, diff, baseline and analysis core for juxtapose.",
|
|
6
|
+
"publishConfig": {
|
|
7
|
+
"access": "public"
|
|
8
|
+
},
|
|
9
|
+
"license": "MIT",
|
|
10
|
+
"main": "./dist/index.js",
|
|
11
|
+
"types": "./dist/index.d.ts",
|
|
12
|
+
"exports": {
|
|
13
|
+
".": "./dist/index.js",
|
|
14
|
+
"./package.json": "./package.json"
|
|
15
|
+
},
|
|
16
|
+
"files": [
|
|
17
|
+
"dist"
|
|
18
|
+
],
|
|
19
|
+
"dependencies": {
|
|
20
|
+
"pixelmatch": "^6.0.0",
|
|
21
|
+
"sharp": "^0.33.5"
|
|
22
|
+
},
|
|
23
|
+
"peerDependencies": {
|
|
24
|
+
"playwright": "^1.48.0"
|
|
25
|
+
},
|
|
26
|
+
"peerDependenciesMeta": {
|
|
27
|
+
"playwright": {
|
|
28
|
+
"optional": true
|
|
29
|
+
}
|
|
30
|
+
},
|
|
31
|
+
"devDependencies": {
|
|
32
|
+
"@types/pixelmatch": "^5.2.6"
|
|
33
|
+
},
|
|
34
|
+
"repository": {
|
|
35
|
+
"type": "git",
|
|
36
|
+
"url": "git+https://github.com/juxtapose-dev/juxtapose.git",
|
|
37
|
+
"directory": "packages/core"
|
|
38
|
+
},
|
|
39
|
+
"scripts": {
|
|
40
|
+
"build": "tsc -p tsconfig.json"
|
|
41
|
+
}
|
|
42
|
+
}
|