@pygmalionjs/pygmalion 0.2.10 → 0.2.12
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.ko.md +36 -10
- package/README.md +36 -10
- package/dist-lib/{App-BDUGQN1Y.js → App-B0w9z2Br.js} +8004 -6025
- package/dist-lib/pygmalion.js +2043 -361
- package/dist-lib/style.css +1 -1
- package/dist-lib/testing.js +1 -1
- package/node/dev-mirror.mjs +68 -1
- package/node/dev-view.vite.mjs +7 -0
- package/node/inspect-plugin.mjs +8 -1
- package/node/preview-artifact-plugin.mjs +296 -0
- package/node/qa-capture-plugin.mjs +1226 -0
- package/node/route-preview-artifact-v3.mjs +584 -0
- package/node/source-revision.mjs +83 -0
- package/node/storyboard-capture-runtime.mjs +1165 -0
- package/node/storyboard-capture-scheduler.mjs +226 -0
- package/node/storyboard.mjs +44 -0
- package/node/vite.mjs +116 -14
- package/package.json +16 -1
- package/qa.d.ts +201 -0
- package/storyboard.d.ts +298 -0
- package/types.d.ts +724 -40
- package/vite.d.ts +102 -20
|
@@ -0,0 +1,226 @@
|
|
|
1
|
+
const CAPTURE_STATUSES = new Set([
|
|
2
|
+
'ready',
|
|
3
|
+
'rendered-with-qa-failure',
|
|
4
|
+
'capture-error',
|
|
5
|
+
]);
|
|
6
|
+
const NON_READY_STATUSES = new Set([
|
|
7
|
+
'rendered-with-qa-failure',
|
|
8
|
+
'capture-error',
|
|
9
|
+
]);
|
|
10
|
+
const EVIDENCE_KEYS = Object.freeze(['snapshot', 'screenshot', 'domTree']);
|
|
11
|
+
|
|
12
|
+
export const STORYBOARD_CAPTURE_SCHEDULER_LIMITS = Object.freeze({
|
|
13
|
+
defaultConcurrency: 8,
|
|
14
|
+
maximumConcurrency: 16,
|
|
15
|
+
});
|
|
16
|
+
|
|
17
|
+
function assertCaptureResult(result, index, phase) {
|
|
18
|
+
if (
|
|
19
|
+
!result ||
|
|
20
|
+
typeof result !== 'object' ||
|
|
21
|
+
!CAPTURE_STATUSES.has(result.status)
|
|
22
|
+
) {
|
|
23
|
+
throw new TypeError(
|
|
24
|
+
`Storyboard capture ${index} returned an invalid status during ${phase}.`,
|
|
25
|
+
);
|
|
26
|
+
}
|
|
27
|
+
return result;
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
function captureContext({
|
|
31
|
+
phase,
|
|
32
|
+
attempt,
|
|
33
|
+
index,
|
|
34
|
+
total,
|
|
35
|
+
previousResult,
|
|
36
|
+
signal,
|
|
37
|
+
}) {
|
|
38
|
+
return {
|
|
39
|
+
phase,
|
|
40
|
+
attempt,
|
|
41
|
+
index,
|
|
42
|
+
total,
|
|
43
|
+
isolated: phase !== 'primary',
|
|
44
|
+
requiresFreshContext: true,
|
|
45
|
+
...(previousResult == null ? {} : { previousResult }),
|
|
46
|
+
...(signal == null ? {} : { signal }),
|
|
47
|
+
};
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
function throwIfAborted(signal) {
|
|
51
|
+
signal?.throwIfAborted?.();
|
|
52
|
+
if (signal?.aborted) {
|
|
53
|
+
throw signal.reason instanceof Error
|
|
54
|
+
? signal.reason
|
|
55
|
+
: new Error('Storyboard capture scheduling was aborted.');
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
function mergeRetriedEvidence(previousResult, retryResult) {
|
|
60
|
+
if (!NON_READY_STATUSES.has(retryResult.status)) return retryResult;
|
|
61
|
+
let merged = retryResult;
|
|
62
|
+
for (const key of EVIDENCE_KEYS) {
|
|
63
|
+
if (
|
|
64
|
+
retryResult[key] == null &&
|
|
65
|
+
previousResult[key] != null
|
|
66
|
+
) {
|
|
67
|
+
if (merged === retryResult) merged = { ...retryResult };
|
|
68
|
+
merged[key] = previousResult[key];
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
if (
|
|
72
|
+
(!Array.isArray(retryResult.diagnostics) ||
|
|
73
|
+
retryResult.diagnostics.length === 0) &&
|
|
74
|
+
Array.isArray(previousResult.diagnostics) &&
|
|
75
|
+
previousResult.diagnostics.length > 0
|
|
76
|
+
) {
|
|
77
|
+
if (merged === retryResult) merged = { ...retryResult };
|
|
78
|
+
merged.diagnostics = previousResult.diagnostics;
|
|
79
|
+
}
|
|
80
|
+
return merged;
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
function resolveConcurrency(value) {
|
|
84
|
+
const concurrency =
|
|
85
|
+
value ?? STORYBOARD_CAPTURE_SCHEDULER_LIMITS.defaultConcurrency;
|
|
86
|
+
if (
|
|
87
|
+
!Number.isInteger(concurrency) ||
|
|
88
|
+
concurrency < 1 ||
|
|
89
|
+
concurrency > STORYBOARD_CAPTURE_SCHEDULER_LIMITS.maximumConcurrency
|
|
90
|
+
) {
|
|
91
|
+
throw new RangeError(
|
|
92
|
+
`Storyboard capture concurrency must be an integer from 1 to ${STORYBOARD_CAPTURE_SCHEDULER_LIMITS.maximumConcurrency}.`,
|
|
93
|
+
);
|
|
94
|
+
}
|
|
95
|
+
return concurrency;
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
function resolveRetryAttempts(value) {
|
|
99
|
+
const retryAttempts = value ?? 1;
|
|
100
|
+
if (
|
|
101
|
+
!Number.isInteger(retryAttempts) ||
|
|
102
|
+
retryAttempts < 0 ||
|
|
103
|
+
retryAttempts > 3
|
|
104
|
+
) {
|
|
105
|
+
throw new RangeError(
|
|
106
|
+
'Storyboard isolated retry attempts must be an integer from 0 to 3.',
|
|
107
|
+
);
|
|
108
|
+
}
|
|
109
|
+
return retryAttempts;
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
/**
|
|
113
|
+
* Schedules capture work without depending on a browser implementation.
|
|
114
|
+
*
|
|
115
|
+
* The first case completes globally before the bounded parallel pass begins.
|
|
116
|
+
* Capture adapters must create and close one fresh execution context for every
|
|
117
|
+
* invocation. Non-ready results receive bounded input-ordered, serial retry
|
|
118
|
+
* passes after the primary pass. A retry that remains non-ready inherits any
|
|
119
|
+
* stable evidence that an earlier attempt captured but the retry could not
|
|
120
|
+
* reproduce.
|
|
121
|
+
*/
|
|
122
|
+
export async function scheduleStoryboardCaptures(
|
|
123
|
+
items,
|
|
124
|
+
capture,
|
|
125
|
+
options = {},
|
|
126
|
+
) {
|
|
127
|
+
if (!Array.isArray(items)) {
|
|
128
|
+
throw new TypeError('Storyboard capture scheduling requires an item array.');
|
|
129
|
+
}
|
|
130
|
+
if (typeof capture !== 'function') {
|
|
131
|
+
throw new TypeError(
|
|
132
|
+
'Storyboard capture scheduling requires a capture function.',
|
|
133
|
+
);
|
|
134
|
+
}
|
|
135
|
+
const concurrency = resolveConcurrency(options.concurrency);
|
|
136
|
+
const isolatedRetryAttempts = resolveRetryAttempts(
|
|
137
|
+
options.isolatedRetryAttempts,
|
|
138
|
+
);
|
|
139
|
+
const signal = options.signal;
|
|
140
|
+
if (items.length === 0) return [];
|
|
141
|
+
throwIfAborted(signal);
|
|
142
|
+
|
|
143
|
+
const results = new Array(items.length);
|
|
144
|
+
results[0] = assertCaptureResult(
|
|
145
|
+
await capture(
|
|
146
|
+
items[0],
|
|
147
|
+
captureContext({
|
|
148
|
+
phase: 'warm-up',
|
|
149
|
+
attempt: 1,
|
|
150
|
+
index: 0,
|
|
151
|
+
total: items.length,
|
|
152
|
+
signal,
|
|
153
|
+
}),
|
|
154
|
+
),
|
|
155
|
+
0,
|
|
156
|
+
'warm-up',
|
|
157
|
+
);
|
|
158
|
+
|
|
159
|
+
let nextIndex = 1;
|
|
160
|
+
async function primaryWorker() {
|
|
161
|
+
while (nextIndex < items.length) {
|
|
162
|
+
throwIfAborted(signal);
|
|
163
|
+
const index = nextIndex;
|
|
164
|
+
nextIndex += 1;
|
|
165
|
+
results[index] = assertCaptureResult(
|
|
166
|
+
await capture(
|
|
167
|
+
items[index],
|
|
168
|
+
captureContext({
|
|
169
|
+
phase: 'primary',
|
|
170
|
+
attempt: 1,
|
|
171
|
+
index,
|
|
172
|
+
total: items.length,
|
|
173
|
+
signal,
|
|
174
|
+
}),
|
|
175
|
+
),
|
|
176
|
+
index,
|
|
177
|
+
'primary',
|
|
178
|
+
);
|
|
179
|
+
}
|
|
180
|
+
}
|
|
181
|
+
await Promise.all(
|
|
182
|
+
Array.from(
|
|
183
|
+
{
|
|
184
|
+
length: Math.min(concurrency, Math.max(0, items.length - 1)),
|
|
185
|
+
},
|
|
186
|
+
() => primaryWorker(),
|
|
187
|
+
),
|
|
188
|
+
);
|
|
189
|
+
|
|
190
|
+
for (
|
|
191
|
+
let retryAttempt = 1;
|
|
192
|
+
retryAttempt <= isolatedRetryAttempts;
|
|
193
|
+
retryAttempt += 1
|
|
194
|
+
) {
|
|
195
|
+
const retryIndexes = results.flatMap((result, index) =>
|
|
196
|
+
NON_READY_STATUSES.has(result.status) ? [index] : [],
|
|
197
|
+
);
|
|
198
|
+
if (retryIndexes.length === 0) break;
|
|
199
|
+
options.onRetryPass?.({
|
|
200
|
+
attempt: retryAttempt,
|
|
201
|
+
totalAttempts: isolatedRetryAttempts,
|
|
202
|
+
indexes: [...retryIndexes],
|
|
203
|
+
});
|
|
204
|
+
for (const index of retryIndexes) {
|
|
205
|
+
throwIfAborted(signal);
|
|
206
|
+
const previousResult = results[index];
|
|
207
|
+
const retryResult = assertCaptureResult(
|
|
208
|
+
await capture(
|
|
209
|
+
items[index],
|
|
210
|
+
captureContext({
|
|
211
|
+
phase: 'isolated-retry',
|
|
212
|
+
attempt: retryAttempt + 1,
|
|
213
|
+
index,
|
|
214
|
+
total: items.length,
|
|
215
|
+
previousResult,
|
|
216
|
+
signal,
|
|
217
|
+
}),
|
|
218
|
+
),
|
|
219
|
+
index,
|
|
220
|
+
'isolated-retry',
|
|
221
|
+
);
|
|
222
|
+
results[index] = mergeRetriedEvidence(previousResult, retryResult);
|
|
223
|
+
}
|
|
224
|
+
}
|
|
225
|
+
return results;
|
|
226
|
+
}
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
export {
|
|
2
|
+
PYGMALION_STORYBOARD_CANONICAL_CONTROL,
|
|
3
|
+
canonicalizeStoryboardExecutions,
|
|
4
|
+
executeStoryboardCandidates,
|
|
5
|
+
fingerprintStoryboardSnapshot,
|
|
6
|
+
normalizeStoryboardSnapshot,
|
|
7
|
+
recommendedStoryboardExecutionConcurrency,
|
|
8
|
+
} from './storyboard-canonical.mjs';
|
|
9
|
+
|
|
10
|
+
export {
|
|
11
|
+
STORYBOARD_CAPTURE_STATUSES,
|
|
12
|
+
StoryboardCaptureStageError,
|
|
13
|
+
assertFinalStoryboardState,
|
|
14
|
+
captureStoryboardCase,
|
|
15
|
+
collectStoryboardDomTree,
|
|
16
|
+
createStoryboardArtifactRecord,
|
|
17
|
+
findUnassertedStoryboardModal,
|
|
18
|
+
hashStoryboardCapture,
|
|
19
|
+
normalizeStoryboardDomStructure,
|
|
20
|
+
resolveStoryboardCaptureRoute,
|
|
21
|
+
resolveStoryboardCaptureUrl,
|
|
22
|
+
resolveStoryboardFixedTime,
|
|
23
|
+
runStoryboardAssertion,
|
|
24
|
+
runStoryboardInteraction,
|
|
25
|
+
serializeStoryboardPreviewDocument,
|
|
26
|
+
storyboardDocumentStabilitySignature,
|
|
27
|
+
storyboardCaptureCandidateTexts,
|
|
28
|
+
waitForStableStoryboardDocument,
|
|
29
|
+
} from './storyboard-capture-runtime.mjs';
|
|
30
|
+
|
|
31
|
+
export {
|
|
32
|
+
STORYBOARD_CAPTURE_SCHEDULER_LIMITS,
|
|
33
|
+
scheduleStoryboardCaptures,
|
|
34
|
+
} from './storyboard-capture-scheduler.mjs';
|
|
35
|
+
|
|
36
|
+
export {
|
|
37
|
+
ROUTE_PREVIEW_ARTIFACT_V3_LIMITS,
|
|
38
|
+
createRoutePreviewArtifactV3,
|
|
39
|
+
reconstructRoutePreviewArtifactScreenshotV3,
|
|
40
|
+
reconstructRoutePreviewArtifactSnapshotV3,
|
|
41
|
+
reconstructRoutePreviewArtifactSnapshotsV3,
|
|
42
|
+
validateRoutePreviewArtifactBundle,
|
|
43
|
+
validateRoutePreviewArtifactV3,
|
|
44
|
+
} from './route-preview-artifact-v3.mjs';
|
package/node/vite.mjs
CHANGED
|
@@ -51,6 +51,33 @@ import {
|
|
|
51
51
|
ROUTE_PREVIEW_ARTIFACT_V2_LIMITS,
|
|
52
52
|
validateRoutePreviewArtifactV2,
|
|
53
53
|
} from './route-preview-artifact-v2.mjs';
|
|
54
|
+
import {
|
|
55
|
+
ROUTE_PREVIEW_ARTIFACT_V3_LIMITS,
|
|
56
|
+
createRoutePreviewArtifactV3,
|
|
57
|
+
reconstructRoutePreviewArtifactScreenshotV3,
|
|
58
|
+
reconstructRoutePreviewArtifactSnapshotV3,
|
|
59
|
+
reconstructRoutePreviewArtifactSnapshotsV3,
|
|
60
|
+
validateRoutePreviewArtifactBundle,
|
|
61
|
+
validateRoutePreviewArtifactV3,
|
|
62
|
+
} from './route-preview-artifact-v3.mjs';
|
|
63
|
+
import {
|
|
64
|
+
DEFAULT_PYGMALION_PREVIEW_ARTIFACT_FILE,
|
|
65
|
+
PYGMALION_PREVIEW_ARTIFACT_ENDPOINT,
|
|
66
|
+
pygmalionPreviewArtifactPlugin,
|
|
67
|
+
} from './preview-artifact-plugin.mjs';
|
|
68
|
+
import {
|
|
69
|
+
DEFAULT_QA_CAPTURE_MAX_FRAMES,
|
|
70
|
+
PYGMALION_QA_ARTIFACT_PREFIX,
|
|
71
|
+
PYGMALION_QA_CAPTURE_ENDPOINT,
|
|
72
|
+
QaCaptureError,
|
|
73
|
+
comparePngScreenshots,
|
|
74
|
+
createQaCaptureService,
|
|
75
|
+
normalizeLocalCaptureBasePath,
|
|
76
|
+
pygmalionQaCapturePlugin,
|
|
77
|
+
runCaptureScript,
|
|
78
|
+
validateQaCaptureRequest,
|
|
79
|
+
} from './qa-capture-plugin.mjs';
|
|
80
|
+
import { resolveGitSourceState } from './source-revision.mjs';
|
|
54
81
|
|
|
55
82
|
function requiredPath(value, name, base) {
|
|
56
83
|
if (typeof value !== 'string' || value.trim() === '') {
|
|
@@ -73,8 +100,14 @@ function optionalNonEmptyString(value, name) {
|
|
|
73
100
|
|
|
74
101
|
function normalizeEndpoint(value, fallback, name) {
|
|
75
102
|
const endpoint = value ?? fallback;
|
|
76
|
-
if (
|
|
77
|
-
|
|
103
|
+
if (
|
|
104
|
+
typeof endpoint !== 'string' ||
|
|
105
|
+
!endpoint.startsWith('/') ||
|
|
106
|
+
endpoint.endsWith('/')
|
|
107
|
+
) {
|
|
108
|
+
throw new Error(
|
|
109
|
+
`Pygmalion ${name} must start with "/" and must not end with "/"`,
|
|
110
|
+
);
|
|
78
111
|
}
|
|
79
112
|
return endpoint;
|
|
80
113
|
}
|
|
@@ -90,27 +123,45 @@ export function resolvePygmalionProject(config) {
|
|
|
90
123
|
definePygmalionProject(config);
|
|
91
124
|
|
|
92
125
|
const configRoot = path.resolve(config.configRoot ?? process.cwd());
|
|
93
|
-
const projectRoot = requiredPath(
|
|
94
|
-
|
|
126
|
+
const projectRoot = requiredPath(
|
|
127
|
+
config.projectRoot,
|
|
128
|
+
'projectRoot',
|
|
129
|
+
configRoot,
|
|
130
|
+
);
|
|
131
|
+
const appRoot = requiredPath(
|
|
132
|
+
config.appRoot ?? projectRoot,
|
|
133
|
+
'appRoot',
|
|
134
|
+
configRoot,
|
|
135
|
+
);
|
|
95
136
|
const editorRoot = path.resolve(configRoot, config.editorRoot ?? appRoot);
|
|
96
|
-
const appDirectory =
|
|
97
|
-
.
|
|
98
|
-
|
|
137
|
+
const appDirectory =
|
|
138
|
+
(config.appDirectory ?? path.relative(projectRoot, appRoot) ?? '.')
|
|
139
|
+
.split(path.sep)
|
|
140
|
+
.join('/') || '.';
|
|
99
141
|
const source = optionalObject(config.source);
|
|
100
142
|
const branch = source.branch ?? 'dev';
|
|
101
143
|
const remote = source.remote ?? 'origin';
|
|
102
144
|
const ref = optionalNonEmptyString(source.ref, 'source.ref');
|
|
103
|
-
const mirror =
|
|
104
|
-
|
|
105
|
-
const
|
|
145
|
+
const mirror =
|
|
146
|
+
config.mirror === false ? false : optionalObject(config.mirror);
|
|
147
|
+
const sessions =
|
|
148
|
+
config.sessions === false ? false : optionalObject(config.sessions);
|
|
149
|
+
const inspect =
|
|
150
|
+
config.inspect === false ? false : optionalObject(config.inspect);
|
|
106
151
|
const preview = optionalObject(config.preview);
|
|
152
|
+
const qa = config.qa === false ? false : optionalObject(config.qa);
|
|
107
153
|
const dependencies = optionalObject(config.dependencies);
|
|
108
|
-
const inventory = config.inventory
|
|
154
|
+
const inventory = config.inventory
|
|
155
|
+
? optionalObject(config.inventory)
|
|
156
|
+
: undefined;
|
|
109
157
|
const mirrorRoot = path.resolve(
|
|
110
158
|
configRoot,
|
|
111
159
|
config.mirrorRoot ??
|
|
112
160
|
mirror?.root ??
|
|
113
|
-
path.join(
|
|
161
|
+
path.join(
|
|
162
|
+
path.dirname(projectRoot),
|
|
163
|
+
`${path.basename(projectRoot)}-${branch}-view`,
|
|
164
|
+
),
|
|
114
165
|
);
|
|
115
166
|
const mirrorAppRoot = path.resolve(
|
|
116
167
|
configRoot,
|
|
@@ -119,7 +170,9 @@ export function resolvePygmalionProject(config) {
|
|
|
119
170
|
(appDirectory === '.' ? mirrorRoot : path.join(mirrorRoot, appDirectory)),
|
|
120
171
|
);
|
|
121
172
|
const sourceDirectory =
|
|
122
|
-
(config.sourceDirectory ?? 'src')
|
|
173
|
+
(config.sourceDirectory ?? 'src')
|
|
174
|
+
.replace(/^\.?\//, '')
|
|
175
|
+
.replace(/\/$/, '') || '.';
|
|
123
176
|
|
|
124
177
|
return {
|
|
125
178
|
configRoot,
|
|
@@ -137,6 +190,7 @@ export function resolvePygmalionProject(config) {
|
|
|
137
190
|
sessions,
|
|
138
191
|
inspect,
|
|
139
192
|
preview,
|
|
193
|
+
qa,
|
|
140
194
|
dependencies,
|
|
141
195
|
inventory,
|
|
142
196
|
};
|
|
@@ -155,6 +209,32 @@ export function createPygmalionVitePlugins(config) {
|
|
|
155
209
|
}),
|
|
156
210
|
];
|
|
157
211
|
|
|
212
|
+
if (project.preview.artifactFile || project.preview.generateArtifact) {
|
|
213
|
+
plugins.push(
|
|
214
|
+
pygmalionPreviewArtifactPlugin({
|
|
215
|
+
root: project.appRoot,
|
|
216
|
+
artifactFile: project.preview.artifactFile,
|
|
217
|
+
endpoint: project.preview.artifactEndpoint,
|
|
218
|
+
generateArtifact: project.preview.generateArtifact,
|
|
219
|
+
}),
|
|
220
|
+
);
|
|
221
|
+
}
|
|
222
|
+
|
|
223
|
+
if (
|
|
224
|
+
project.qa !== false &&
|
|
225
|
+
(project.qa.captureScript || project.qa.captureRunner)
|
|
226
|
+
) {
|
|
227
|
+
plugins.push(
|
|
228
|
+
pygmalionQaCapturePlugin({
|
|
229
|
+
...project.qa,
|
|
230
|
+
root: project.appRoot,
|
|
231
|
+
scriptPath: project.qa.captureScript
|
|
232
|
+
? path.resolve(project.appRoot, project.qa.captureScript)
|
|
233
|
+
: undefined,
|
|
234
|
+
}),
|
|
235
|
+
);
|
|
236
|
+
}
|
|
237
|
+
|
|
158
238
|
if (project.inspect !== false) {
|
|
159
239
|
plugins.push(
|
|
160
240
|
pygmalionInspectPlugin({
|
|
@@ -218,7 +298,8 @@ export function createPygmalionVitePlugins(config) {
|
|
|
218
298
|
previewConfig: project.preview.configFile,
|
|
219
299
|
viteConfig: project.preview.viteConfig,
|
|
220
300
|
viteBin: project.preview.viteBin,
|
|
221
|
-
previewPort:
|
|
301
|
+
previewPort:
|
|
302
|
+
project.sessions.previewPort ?? project.preview.sessionPort,
|
|
222
303
|
worktreesRoot: project.sessions.worktreesRoot,
|
|
223
304
|
prefix: normalizeEndpoint(
|
|
224
305
|
project.sessions.prefix,
|
|
@@ -274,4 +355,25 @@ export {
|
|
|
274
355
|
reconstructRoutePreviewArtifactSnapshots,
|
|
275
356
|
ROUTE_PREVIEW_ARTIFACT_V2_LIMITS,
|
|
276
357
|
validateRoutePreviewArtifactV2,
|
|
358
|
+
ROUTE_PREVIEW_ARTIFACT_V3_LIMITS,
|
|
359
|
+
createRoutePreviewArtifactV3,
|
|
360
|
+
reconstructRoutePreviewArtifactScreenshotV3,
|
|
361
|
+
reconstructRoutePreviewArtifactSnapshotV3,
|
|
362
|
+
reconstructRoutePreviewArtifactSnapshotsV3,
|
|
363
|
+
validateRoutePreviewArtifactBundle,
|
|
364
|
+
validateRoutePreviewArtifactV3,
|
|
365
|
+
DEFAULT_PYGMALION_PREVIEW_ARTIFACT_FILE,
|
|
366
|
+
PYGMALION_PREVIEW_ARTIFACT_ENDPOINT,
|
|
367
|
+
pygmalionPreviewArtifactPlugin,
|
|
368
|
+
DEFAULT_QA_CAPTURE_MAX_FRAMES,
|
|
369
|
+
PYGMALION_QA_ARTIFACT_PREFIX,
|
|
370
|
+
PYGMALION_QA_CAPTURE_ENDPOINT,
|
|
371
|
+
QaCaptureError,
|
|
372
|
+
comparePngScreenshots,
|
|
373
|
+
createQaCaptureService,
|
|
374
|
+
normalizeLocalCaptureBasePath,
|
|
375
|
+
pygmalionQaCapturePlugin,
|
|
376
|
+
runCaptureScript,
|
|
377
|
+
validateQaCaptureRequest,
|
|
378
|
+
resolveGitSourceState,
|
|
277
379
|
};
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@pygmalionjs/pygmalion",
|
|
3
|
-
"version": "0.2.
|
|
3
|
+
"version": "0.2.12",
|
|
4
4
|
"description": "Code-backed DOM design sandbox and visual QA editor",
|
|
5
5
|
"license": "UNLICENSED",
|
|
6
6
|
"publishConfig": {
|
|
@@ -19,6 +19,10 @@
|
|
|
19
19
|
"types": "./vite.d.ts",
|
|
20
20
|
"default": "./node/vite.mjs"
|
|
21
21
|
},
|
|
22
|
+
"./storyboard": {
|
|
23
|
+
"types": "./storyboard.d.ts",
|
|
24
|
+
"default": "./node/storyboard.mjs"
|
|
25
|
+
},
|
|
22
26
|
"./dev-view": "./node/dev-view.vite.mjs",
|
|
23
27
|
"./testing": {
|
|
24
28
|
"types": "./testing.d.ts",
|
|
@@ -32,13 +36,22 @@
|
|
|
32
36
|
"node/dev-mirror.mjs",
|
|
33
37
|
"node/dev-view.vite.mjs",
|
|
34
38
|
"node/inspect-plugin.mjs",
|
|
39
|
+
"node/preview-artifact-plugin.mjs",
|
|
40
|
+
"node/qa-capture-plugin.mjs",
|
|
35
41
|
"node/route-preview-artifact-v2.mjs",
|
|
42
|
+
"node/route-preview-artifact-v3.mjs",
|
|
43
|
+
"node/source-revision.mjs",
|
|
36
44
|
"node/source-diff.mjs",
|
|
37
45
|
"node/source-graph.mjs",
|
|
38
46
|
"node/source-operations.mjs",
|
|
39
47
|
"node/storyboard-canonical.mjs",
|
|
48
|
+
"node/storyboard-capture-scheduler.mjs",
|
|
49
|
+
"node/storyboard-capture-runtime.mjs",
|
|
40
50
|
"node/storyboard-environment.mjs",
|
|
51
|
+
"node/storyboard.mjs",
|
|
41
52
|
"node/vite.mjs",
|
|
53
|
+
"qa.d.ts",
|
|
54
|
+
"storyboard.d.ts",
|
|
42
55
|
"types.d.ts",
|
|
43
56
|
"testing.d.ts",
|
|
44
57
|
"vite.d.ts"
|
|
@@ -67,6 +80,8 @@
|
|
|
67
80
|
"magic-string": "^0.30.17",
|
|
68
81
|
"mobx": "^6.13.0",
|
|
69
82
|
"mobx-react-lite": "^4.0.7",
|
|
83
|
+
"pixelmatch": "^7.2.0",
|
|
84
|
+
"pngjs": "^7.0.0",
|
|
70
85
|
"typescript": "^5.8.0"
|
|
71
86
|
},
|
|
72
87
|
"devDependencies": {
|
package/qa.d.ts
ADDED
|
@@ -0,0 +1,201 @@
|
|
|
1
|
+
import type { Plugin } from 'vite';
|
|
2
|
+
|
|
3
|
+
export const PYGMALION_QA_CAPTURE_ENDPOINT: '/__pygmalion-qa/capture';
|
|
4
|
+
export const PYGMALION_QA_ARTIFACT_PREFIX: '/__pygmalion-qa/artifacts/';
|
|
5
|
+
export const DEFAULT_QA_CAPTURE_MAX_FRAMES: number;
|
|
6
|
+
|
|
7
|
+
export interface QaCaptureHashes {
|
|
8
|
+
domStructureHash: string;
|
|
9
|
+
screenshotHash: string;
|
|
10
|
+
screenshotUrl?: string;
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
export interface QaCaptureFrame extends QaCaptureHashes {
|
|
14
|
+
id: string;
|
|
15
|
+
/** Runner-only PNG bytes; never serialized in a capture response. */
|
|
16
|
+
screenshotBytes?: Uint8Array;
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
export interface QaCaptureFailure {
|
|
20
|
+
id: string;
|
|
21
|
+
error: string;
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
export interface QaCaptureRunnerInput {
|
|
25
|
+
root: string;
|
|
26
|
+
scriptPath: string;
|
|
27
|
+
basePath: string;
|
|
28
|
+
frameIds: string[];
|
|
29
|
+
timeoutMs: number;
|
|
30
|
+
maxPngBytes?: number;
|
|
31
|
+
maxTotalPngBytes?: number;
|
|
32
|
+
signal?: AbortSignal;
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
export interface QaCaptureRunnerResult {
|
|
36
|
+
screens: QaCaptureFrame[];
|
|
37
|
+
failures: QaCaptureFailure[];
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
export type QaCaptureRunner = (
|
|
41
|
+
input: QaCaptureRunnerInput,
|
|
42
|
+
) => Promise<QaCaptureRunnerResult>;
|
|
43
|
+
|
|
44
|
+
export interface QaCaptureBaselineRequest {
|
|
45
|
+
phase: 'baseline';
|
|
46
|
+
basePath: string;
|
|
47
|
+
frameIds: string[];
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
export interface QaCaptureChangedRequest {
|
|
51
|
+
phase: 'changed';
|
|
52
|
+
basePath: string;
|
|
53
|
+
frameIds: string[];
|
|
54
|
+
baselineId: string;
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
export type QaCaptureRequest = QaCaptureBaselineRequest | QaCaptureChangedRequest;
|
|
58
|
+
|
|
59
|
+
export interface QaCaptureBaselineResponse {
|
|
60
|
+
ok: true;
|
|
61
|
+
baselineId: string;
|
|
62
|
+
frames: QaCaptureFrame[];
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
export interface QaCaptureChangedFrame {
|
|
66
|
+
id: string;
|
|
67
|
+
before: QaCaptureHashes;
|
|
68
|
+
after: QaCaptureHashes | null;
|
|
69
|
+
domChanged: boolean | null;
|
|
70
|
+
screenshotChanged: boolean | null;
|
|
71
|
+
pixelDiff?: QaPixelDiff;
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
export interface QaPixelDimensions {
|
|
75
|
+
width: number;
|
|
76
|
+
height: number;
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
export interface QaPixelDiff {
|
|
80
|
+
before: QaPixelDimensions;
|
|
81
|
+
after: QaPixelDimensions;
|
|
82
|
+
dimensionsMatch: boolean;
|
|
83
|
+
changedPixels: number;
|
|
84
|
+
totalPixels: number;
|
|
85
|
+
diffRatio: number;
|
|
86
|
+
threshold: number;
|
|
87
|
+
heatmapUrl: string | null;
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
export interface QaCaptureChangedResponse {
|
|
91
|
+
ok: true;
|
|
92
|
+
baselineId: string;
|
|
93
|
+
beforeBasePath: string;
|
|
94
|
+
afterBasePath: string;
|
|
95
|
+
frames: QaCaptureChangedFrame[];
|
|
96
|
+
complete: boolean;
|
|
97
|
+
failures: QaCaptureFailure[];
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
export interface QaCaptureErrorResponse {
|
|
101
|
+
ok: false;
|
|
102
|
+
error: string;
|
|
103
|
+
message: string;
|
|
104
|
+
failures?: QaCaptureFailure[];
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
export type QaCaptureResponse =
|
|
108
|
+
| QaCaptureBaselineResponse
|
|
109
|
+
| QaCaptureChangedResponse
|
|
110
|
+
| QaCaptureErrorResponse;
|
|
111
|
+
|
|
112
|
+
export class QaCaptureError extends Error {
|
|
113
|
+
statusCode: number;
|
|
114
|
+
code: string;
|
|
115
|
+
failures?: QaCaptureFailure[];
|
|
116
|
+
constructor(
|
|
117
|
+
statusCode: number,
|
|
118
|
+
code: string,
|
|
119
|
+
message: string,
|
|
120
|
+
failures?: QaCaptureFailure[],
|
|
121
|
+
);
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
export interface QaCaptureServiceOptions {
|
|
125
|
+
root?: string;
|
|
126
|
+
scriptPath?: string;
|
|
127
|
+
captureRunner?: QaCaptureRunner;
|
|
128
|
+
timeoutMs?: number;
|
|
129
|
+
maxFrameIds?: number;
|
|
130
|
+
maxBaselines?: number;
|
|
131
|
+
baselineTtlMs?: number;
|
|
132
|
+
maxBaselineBytes?: number;
|
|
133
|
+
maxArtifacts?: number;
|
|
134
|
+
maxArtifactBytes?: number;
|
|
135
|
+
artifactTtlMs?: number;
|
|
136
|
+
maxPngBytes?: number;
|
|
137
|
+
maxPngPixels?: number;
|
|
138
|
+
pixelThreshold?: number;
|
|
139
|
+
now?: () => number;
|
|
140
|
+
createBaselineId?: () => string;
|
|
141
|
+
createArtifactId?: () => string;
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
export interface QaCaptureService {
|
|
145
|
+
capture(
|
|
146
|
+
request: QaCaptureRequest | unknown,
|
|
147
|
+
context?: { allowedHost?: string | string[] },
|
|
148
|
+
): Promise<QaCaptureBaselineResponse | QaCaptureChangedResponse>;
|
|
149
|
+
getArtifact(
|
|
150
|
+
id: string,
|
|
151
|
+
): { bytes: Uint8Array; contentType: 'image/png' } | null;
|
|
152
|
+
readonly baselineCount: number;
|
|
153
|
+
readonly artifactCount: number;
|
|
154
|
+
readonly busy: boolean;
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
export interface QaPngComparison {
|
|
158
|
+
before: QaPixelDimensions;
|
|
159
|
+
after: QaPixelDimensions;
|
|
160
|
+
dimensionsMatch: boolean;
|
|
161
|
+
changedPixels: number;
|
|
162
|
+
totalPixels: number;
|
|
163
|
+
diffRatio: number;
|
|
164
|
+
threshold: number;
|
|
165
|
+
heatmapBytes: Uint8Array;
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
export function normalizeLocalCaptureBasePath(
|
|
169
|
+
value: unknown,
|
|
170
|
+
allowedHost?: string | string[],
|
|
171
|
+
): string;
|
|
172
|
+
|
|
173
|
+
export function validateQaCaptureRequest(
|
|
174
|
+
value: unknown,
|
|
175
|
+
options?: {
|
|
176
|
+
allowedHost?: string | string[];
|
|
177
|
+
maxFrameIds?: number;
|
|
178
|
+
},
|
|
179
|
+
): QaCaptureRequest;
|
|
180
|
+
|
|
181
|
+
export function runCaptureScript(
|
|
182
|
+
input: QaCaptureRunnerInput,
|
|
183
|
+
): Promise<QaCaptureRunnerResult>;
|
|
184
|
+
|
|
185
|
+
export function comparePngScreenshots(
|
|
186
|
+
beforeBytes: Uint8Array,
|
|
187
|
+
afterBytes: Uint8Array,
|
|
188
|
+
options?: {
|
|
189
|
+
maxPngBytes?: number;
|
|
190
|
+
maxPngPixels?: number;
|
|
191
|
+
threshold?: number;
|
|
192
|
+
},
|
|
193
|
+
): QaPngComparison;
|
|
194
|
+
|
|
195
|
+
export function createQaCaptureService(
|
|
196
|
+
options?: QaCaptureServiceOptions,
|
|
197
|
+
): QaCaptureService;
|
|
198
|
+
|
|
199
|
+
export function pygmalionQaCapturePlugin(
|
|
200
|
+
options?: QaCaptureServiceOptions,
|
|
201
|
+
): Plugin;
|