staysfixed 0.2.3 → 0.3.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/package.json +1 -1
- package/src/core/events.js +174 -18
- package/src/guard/api.js +168 -12
- package/src/guard/run.js +104 -6
- package/src/picture/capture.js +134 -14
- package/src/picture/run.js +67 -1
- package/src/types.js +9 -2
- package/src/watch/panel.js +806 -255
package/src/picture/capture.js
CHANGED
|
@@ -13,6 +13,11 @@ import { applyFreeze, prepareForShutter } from '../freeze/index.js';
|
|
|
13
13
|
import { settle } from '../freeze/settle.js';
|
|
14
14
|
import { resolveMasks, paintMasks } from '../freeze/mask.js';
|
|
15
15
|
import { StaysFixedError, isExpected, messageOf } from '../core/errors.js';
|
|
16
|
+
// The words a step is described by, and the two builders that turn what is known at
|
|
17
|
+
// this instant into one line of the list. Imported rather than written again here so
|
|
18
|
+
// the line a person watches tick is the same line, in the same words, that the
|
|
19
|
+
// finished list hands back a moment later.
|
|
20
|
+
import { CHECK_KEYS, CHECK_LABELS, checkStep, runningStep } from '../core/events.js';
|
|
16
21
|
// store.js reads `pngSize` back out of this file. Two modules about the same pictures
|
|
17
22
|
// leaning on each other is fine here — both sides are plain functions, so neither is
|
|
18
23
|
// half-built when the other asks for it.
|
|
@@ -51,10 +56,23 @@ const KNOWN_KEYS = new Set([...ACTION_ORDER, 'text', 'note']);
|
|
|
51
56
|
* freeze: import('../types.js').FreezeConfig,
|
|
52
57
|
* masks: import('../types.js').Mask[],
|
|
53
58
|
* }} settings
|
|
54
|
-
* @param {{
|
|
59
|
+
* @param {{
|
|
60
|
+
* fixturesDir: string,
|
|
61
|
+
* record?: boolean,
|
|
62
|
+
* timeoutMs?: number,
|
|
63
|
+
* thumbnail?: boolean,
|
|
64
|
+
* onStep?: (step: import('../types.js').CheckStep) => void,
|
|
65
|
+
* }} ctx
|
|
55
66
|
* `thumbnail` is asked for only while somebody is watching the run happen; it costs a
|
|
56
67
|
* decode of the picture that was just taken (skipped when the masks already decoded it)
|
|
57
68
|
* plus about forty milliseconds of shrinking, so it is off unless it is wanted.
|
|
69
|
+
*
|
|
70
|
+
* `onStep` is the same idea for words instead of pixels: hand one in and this says
|
|
71
|
+
* what it is doing as it does it — each phase named the moment it starts and named
|
|
72
|
+
* again the moment it settles — so a watcher can tick the list off live instead of
|
|
73
|
+
* being shown a finished table. Gated exactly the way `thumbnail` is: the run hands
|
|
74
|
+
* one in only while a panel is open, so the ordinary case is one `typeof` check per
|
|
75
|
+
* phase and nothing allocated at all.
|
|
58
76
|
* @returns {Promise<import('../types.js').CaptureReport & {
|
|
59
77
|
* masks: import('../types.js').MaskRect[],
|
|
60
78
|
* timings: {steps: number, prepare: number, settle: number},
|
|
@@ -83,12 +101,88 @@ export async function captureScreen(page, screen, settings, ctx) {
|
|
|
83
101
|
await page.setViewport(settings.viewport);
|
|
84
102
|
page.clearConsole();
|
|
85
103
|
|
|
104
|
+
// What the freeze layer is being ASKED for. Worked out before it is asked, because a
|
|
105
|
+
// line announced as it starts can only be worded from what is already known — and a
|
|
106
|
+
// project that switched the clock freezing off has to be told that at the moment it
|
|
107
|
+
// does not happen, not have a freeze claimed and then taken back.
|
|
108
|
+
/** @type {import('../core/events.js').FrozenPlan} */
|
|
109
|
+
const plan = {
|
|
110
|
+
clock: settings.freeze.clock !== false,
|
|
111
|
+
motion: settings.freeze.motion !== false,
|
|
112
|
+
random: settings.freeze.random !== 'off',
|
|
113
|
+
fonts: settings.freeze.fonts !== false,
|
|
114
|
+
network: settings.freeze.network ?? 'block-external',
|
|
115
|
+
frames: settleConfig.frames ?? 2,
|
|
116
|
+
maxDriftPixels: settleConfig.maxDriftPixels ?? 0,
|
|
117
|
+
};
|
|
118
|
+
const masksAsked = (settings.masks ?? []).length;
|
|
119
|
+
|
|
120
|
+
// Gated exactly the way `thumbnail` is: the run hands an `onStep` in only while a
|
|
121
|
+
// panel is open. Nobody watching means this is null, every announcement below is one
|
|
122
|
+
// `if` that does nothing, and not a single object is made.
|
|
123
|
+
const onStep = typeof ctx.onStep === 'function' ? ctx.onStep : null;
|
|
124
|
+
/**
|
|
125
|
+
* The line announced as started and not yet settled — so a failure can close it
|
|
126
|
+
* instead of leaving a row spinning forever on somebody's screen.
|
|
127
|
+
* @type {import('../types.js').CheckStep|null}
|
|
128
|
+
*/
|
|
129
|
+
let pending = null;
|
|
130
|
+
|
|
131
|
+
/**
|
|
132
|
+
* Name a phase as it begins.
|
|
133
|
+
* @param {import('../core/events.js').CheckKey} key
|
|
134
|
+
* @param {import('../core/events.js').ChecksInput} input
|
|
135
|
+
* @returns {void}
|
|
136
|
+
*/
|
|
137
|
+
const begins = (key, input) => {
|
|
138
|
+
if (!onStep) return;
|
|
139
|
+
const step = runningStep(key, input);
|
|
140
|
+
pending = step ?? null;
|
|
141
|
+
if (step) onStep(step);
|
|
142
|
+
};
|
|
143
|
+
|
|
144
|
+
/**
|
|
145
|
+
* Name the same phase again, settled, under the same key.
|
|
146
|
+
* @param {import('../core/events.js').CheckKey} key
|
|
147
|
+
* @param {import('../core/events.js').ChecksInput} input
|
|
148
|
+
* @param {import('../types.js').CheckStep} [instead] For a line whose number is not
|
|
149
|
+
* knowable yet; the finished list fills that in.
|
|
150
|
+
* @returns {void}
|
|
151
|
+
*/
|
|
152
|
+
const settled = (key, input, instead) => {
|
|
153
|
+
if (!onStep) return;
|
|
154
|
+
pending = null;
|
|
155
|
+
const step = instead ?? checkStep(key, input);
|
|
156
|
+
if (step) onStep(step);
|
|
157
|
+
};
|
|
158
|
+
|
|
159
|
+
/**
|
|
160
|
+
* Close whatever was in flight when something went wrong.
|
|
161
|
+
* @param {unknown} error
|
|
162
|
+
* @returns {void}
|
|
163
|
+
*/
|
|
164
|
+
const closePending = (error) => {
|
|
165
|
+
if (!onStep || !pending) return;
|
|
166
|
+
const why = messageOf(error);
|
|
167
|
+
// Where a phase has its own wording for going wrong, use it: "could not reach the
|
|
168
|
+
// screen" is what happened, and leaving "reached the screen" up with a cross beside
|
|
169
|
+
// it says the opposite of the truth for as long as anybody is reading it.
|
|
170
|
+
const label = pending.key === CHECK_KEYS.steps ? CHECK_LABELS.stepsFailed : pending.label;
|
|
171
|
+
onStep({ ...pending, label, state: 'bad', detail: why });
|
|
172
|
+
pending = null;
|
|
173
|
+
};
|
|
174
|
+
|
|
175
|
+
begins(CHECK_KEYS.frozen, { screen, frozen: plan });
|
|
86
176
|
const frozen = await applyFreeze(page, settings.freeze, {
|
|
87
177
|
fixturesDir: ctx.fixturesDir,
|
|
88
178
|
screenName: screen.name,
|
|
89
179
|
record: ctx.record ?? false,
|
|
90
180
|
deviceScaleFactor,
|
|
181
|
+
}).catch((error) => {
|
|
182
|
+
closePending(error);
|
|
183
|
+
throw error;
|
|
91
184
|
});
|
|
185
|
+
settled(CHECK_KEYS.frozen, { screen, frozen: plan });
|
|
92
186
|
|
|
93
187
|
// A frozen clock cannot time anything, so the stopwatch is the host's own, and it is
|
|
94
188
|
// the monotonic one: a machine that adjusts its clock mid-run must not be able to
|
|
@@ -99,11 +193,13 @@ export async function captureScreen(page, screen, settings, ctx) {
|
|
|
99
193
|
const spent = { steps: 0, prepare: 0, settle: 0 };
|
|
100
194
|
|
|
101
195
|
try {
|
|
196
|
+
begins(CHECK_KEYS.steps, { screen });
|
|
102
197
|
if (typeof screen.do === 'function') {
|
|
103
198
|
await screen.do(page);
|
|
104
199
|
} else {
|
|
105
200
|
await runSteps(page, screen.steps ?? []);
|
|
106
201
|
}
|
|
202
|
+
settled(CHECK_KEYS.steps, { screen });
|
|
107
203
|
|
|
108
204
|
// Scrolling back to the top is the deterministic default, but a screen that
|
|
109
205
|
// deliberately scrolled somewhere must be photographed where it landed.
|
|
@@ -113,11 +209,21 @@ export async function captureScreen(page, screen, settings, ctx) {
|
|
|
113
209
|
const startedPrepare = clock();
|
|
114
210
|
spent.steps = since(startedSteps, startedPrepare);
|
|
115
211
|
|
|
212
|
+
// Waiting for the page to be ready. WHAT was still loading can only be read off the
|
|
213
|
+
// frame that ends up being kept, which does not exist yet, so this line settles on
|
|
214
|
+
// the one thing that is true here — the shutter waited — and the finished list fills
|
|
215
|
+
// in the count of faces and pictures a moment later, on the same line.
|
|
216
|
+
begins(CHECK_KEYS.loaded, { screen, frozen: plan });
|
|
116
217
|
await prepareForShutter(page, {
|
|
117
218
|
fonts: settings.freeze.fonts !== false,
|
|
118
219
|
timeoutMs,
|
|
119
220
|
keepScroll: scrolledOnPurpose,
|
|
120
221
|
});
|
|
222
|
+
settled(
|
|
223
|
+
CHECK_KEYS.loaded,
|
|
224
|
+
{ screen, frozen: plan },
|
|
225
|
+
plan.fonts ? { key: CHECK_KEYS.loaded, label: CHECK_LABELS.loaded, state: 'ok' } : undefined,
|
|
226
|
+
);
|
|
121
227
|
|
|
122
228
|
/** @type {import('../types.js').CaptureOptions} */
|
|
123
229
|
const shotOptions = {};
|
|
@@ -133,6 +239,10 @@ export async function captureScreen(page, screen, settings, ctx) {
|
|
|
133
239
|
const startedSettle = clock();
|
|
134
240
|
spent.prepare = since(startedPrepare, startedSettle);
|
|
135
241
|
|
|
242
|
+
// Holding still — and the shutter is inside it. The settle loop photographs the
|
|
243
|
+
// screen over and over until two frames in a row are identical and keeps the last of
|
|
244
|
+
// them, so this is ONE line rather than two: there is one thing happening.
|
|
245
|
+
begins(CHECK_KEYS.settle, { screen, frozen: plan });
|
|
136
246
|
const held = await settle(page, {
|
|
137
247
|
frames: settleConfig.frames ?? 2,
|
|
138
248
|
intervalMs: settleConfig.intervalMs ?? 250,
|
|
@@ -142,6 +252,7 @@ export async function captureScreen(page, screen, settings, ctx) {
|
|
|
142
252
|
probe: () => page.shoot(probeOptions),
|
|
143
253
|
});
|
|
144
254
|
spent.settle = since(startedSettle, clock());
|
|
255
|
+
settled(CHECK_KEYS.settle, { screen, settle: held.report, frozen: plan });
|
|
145
256
|
|
|
146
257
|
// Asked the moment the picture exists, and never before: this is a statement about
|
|
147
258
|
// the frame that was kept. Reading it is a single round trip that touches nothing —
|
|
@@ -151,6 +262,7 @@ export async function captureScreen(page, screen, settings, ctx) {
|
|
|
151
262
|
// question later; by then the app has moved on.
|
|
152
263
|
const loaded = await readLoaded(page);
|
|
153
264
|
|
|
265
|
+
begins(CHECK_KEYS.masks, { screen, masksAsked });
|
|
154
266
|
const rects = await resolveMasks(page, settings.masks ?? [], { deviceScaleFactor });
|
|
155
267
|
// Masks force us to decode the picture; hold on to those pixels. The preview a
|
|
156
268
|
// watcher is shown is made from the very same ones — the picture that gets
|
|
@@ -159,6 +271,9 @@ export async function captureScreen(page, screen, settings, ctx) {
|
|
|
159
271
|
const painted = rects.length > 0 ? paintInto(held.png, rects) : null;
|
|
160
272
|
const png = painted ? painted.png : held.png;
|
|
161
273
|
const size = pngSize(png);
|
|
274
|
+
// Said after the painting, not after the finding: the line claims the boxes are on
|
|
275
|
+
// the picture, so it waits until they are.
|
|
276
|
+
settled(CHECK_KEYS.masks, { screen, masks: rects, masksAsked });
|
|
162
277
|
|
|
163
278
|
// The picture is taken; now put the app back if this screen asked us to.
|
|
164
279
|
//
|
|
@@ -173,6 +288,13 @@ export async function captureScreen(page, screen, settings, ctx) {
|
|
|
173
288
|
await runSteps(page, screen.after);
|
|
174
289
|
}
|
|
175
290
|
|
|
291
|
+
// Requests are not a phase — the outside world is kept out for the whole capture —
|
|
292
|
+
// so this is one line, said once, at the end. Counted here rather than a few lines
|
|
293
|
+
// earlier so the number a watcher sees is the same number the finished list carries,
|
|
294
|
+
// including anything the putting-back steps asked for.
|
|
295
|
+
const stats = frozen.stats();
|
|
296
|
+
settled(CHECK_KEYS.network, { screen, freeze: stats, frozen: plan });
|
|
297
|
+
|
|
176
298
|
/** @type {import('../types.js').CaptureReport & {masks: import('../types.js').MaskRect[], timings: typeof spent, spent: typeof spent, frozen: import('../core/events.js').FrozenPlan, loaded?: import('../core/events.js').LoadedReport, thumbnail?: string}} */
|
|
177
299
|
const report = {
|
|
178
300
|
png,
|
|
@@ -180,20 +302,13 @@ export async function captureScreen(page, screen, settings, ctx) {
|
|
|
180
302
|
height: size.height,
|
|
181
303
|
settle: held.report,
|
|
182
304
|
consoleErrors: page.consoleErrors(),
|
|
183
|
-
freeze:
|
|
305
|
+
freeze: stats,
|
|
184
306
|
masks: rects,
|
|
185
|
-
// What was asked of the freeze layer, in the same words the config used.
|
|
186
|
-
//
|
|
187
|
-
//
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
motion: settings.freeze.motion !== false,
|
|
191
|
-
random: settings.freeze.random !== 'off',
|
|
192
|
-
fonts: settings.freeze.fonts !== false,
|
|
193
|
-
network: settings.freeze.network ?? 'block-external',
|
|
194
|
-
frames: settleConfig.frames ?? 2,
|
|
195
|
-
maxDriftPixels: settleConfig.maxDriftPixels ?? 0,
|
|
196
|
-
},
|
|
307
|
+
// What was asked of the freeze layer, in the same words the config used. Worked
|
|
308
|
+
// out at the top rather than here, because by the time anybody reports on this
|
|
309
|
+
// screen the settings have been merged away — and because the live list needs the
|
|
310
|
+
// same answer before any of this happens, and the two must never disagree.
|
|
311
|
+
frozen: plan,
|
|
197
312
|
// The same three numbers under both names the rest of the tool asks for them by.
|
|
198
313
|
timings: spent,
|
|
199
314
|
spent,
|
|
@@ -204,6 +319,11 @@ export async function captureScreen(page, screen, settings, ctx) {
|
|
|
204
319
|
if (small) report.thumbnail = small;
|
|
205
320
|
}
|
|
206
321
|
return report;
|
|
322
|
+
} catch (error) {
|
|
323
|
+
// A row that stays "running" for the rest of the run is a lie about what happened.
|
|
324
|
+
// The run reports the failure itself; this only closes the line it fell over on.
|
|
325
|
+
closePending(error);
|
|
326
|
+
throw error;
|
|
207
327
|
} finally {
|
|
208
328
|
// Releasing must never be the thing that hides a real failure.
|
|
209
329
|
try {
|
package/src/picture/run.js
CHANGED
|
@@ -21,7 +21,7 @@ import { resetWindow } from '../drive/launch.js';
|
|
|
21
21
|
import { gitInfo } from '../core/git.js';
|
|
22
22
|
import { messageOf } from '../core/errors.js';
|
|
23
23
|
import { detail } from '../core/log.js';
|
|
24
|
-
import { emitEvent, fileUrl, buildChecks } from '../core/events.js';
|
|
24
|
+
import { emitEvent, fileUrl, buildChecks, checkStep, runningStep, CHECK_KEYS } from '../core/events.js';
|
|
25
25
|
|
|
26
26
|
/**
|
|
27
27
|
* A picture result plus two things the plain result has no room for: whether it
|
|
@@ -231,6 +231,50 @@ async function runOneScreen(project, page, screen, ctx) {
|
|
|
231
231
|
/** @type {import('../types.js').CompareReport|null} */
|
|
232
232
|
let compare = null;
|
|
233
233
|
let attempts = 0;
|
|
234
|
+
|
|
235
|
+
/**
|
|
236
|
+
* Say one thing about this screen the moment it is true.
|
|
237
|
+
*
|
|
238
|
+
* The finished list at the end is still the authority — a watcher that missed every
|
|
239
|
+
* one of these ends up with exactly the same rows — but a person who is watching
|
|
240
|
+
* wants to see the working tick past as it happens rather than appear all at once
|
|
241
|
+
* when the screen is already over.
|
|
242
|
+
*
|
|
243
|
+
* Only while somebody is watching, and gated on the very same flag the small pictures
|
|
244
|
+
* are: the run sets `thumbnail` when a panel is open. Nobody watching means this is
|
|
245
|
+
* undefined, nothing is built and nothing is sent.
|
|
246
|
+
*
|
|
247
|
+
* @type {((step: import('../types.js').CheckStep) => void)|undefined}
|
|
248
|
+
*/
|
|
249
|
+
const onStep =
|
|
250
|
+
ctx.thumbnail === true && ctx.events
|
|
251
|
+
? (/** @type {import('../types.js').CheckStep} */ step) => {
|
|
252
|
+
emitEvent(ctx.events, { type: 'screen:step', name: screen.name, step });
|
|
253
|
+
}
|
|
254
|
+
: undefined;
|
|
255
|
+
|
|
256
|
+
/**
|
|
257
|
+
* One line of the working, built from what is known at this instant.
|
|
258
|
+
*
|
|
259
|
+
* @param {import('../core/events.js').CheckKey} key
|
|
260
|
+
* @param {boolean} [starting] True to name it as it begins, false once it has settled.
|
|
261
|
+
* @returns {void}
|
|
262
|
+
*/
|
|
263
|
+
function tell(key, starting) {
|
|
264
|
+
if (!onStep) return;
|
|
265
|
+
/** @type {import('../core/events.js').ChecksInput} */
|
|
266
|
+
const known = {
|
|
267
|
+
screen,
|
|
268
|
+
size,
|
|
269
|
+
approvedSize: compare?.approvedSize,
|
|
270
|
+
compare,
|
|
271
|
+
hasApproved: Boolean(approved),
|
|
272
|
+
tolerance: settings.tolerance,
|
|
273
|
+
consoleErrors,
|
|
274
|
+
};
|
|
275
|
+
const step = starting ? runningStep(key, known) : checkStep(key, known);
|
|
276
|
+
if (step) onStep(step);
|
|
277
|
+
}
|
|
234
278
|
/** @type {Awaited<ReturnType<typeof captureScreen>>|undefined} */
|
|
235
279
|
let last;
|
|
236
280
|
|
|
@@ -288,6 +332,10 @@ async function runOneScreen(project, page, screen, ctx) {
|
|
|
288
332
|
record: ctx.record ?? false,
|
|
289
333
|
timeoutMs: settings.freeze.settle?.timeoutMs,
|
|
290
334
|
thumbnail: ctx.thumbnail === true,
|
|
335
|
+
// The phases inside one photograph — freezing, the recipe, waiting, holding
|
|
336
|
+
// still, painting over the live areas — can only be announced by the thing
|
|
337
|
+
// doing them. Everything after the shutter is announced here instead.
|
|
338
|
+
onStep,
|
|
291
339
|
});
|
|
292
340
|
accountForCapture(ctx.timings, shot);
|
|
293
341
|
last = shot;
|
|
@@ -318,6 +366,12 @@ async function runOneScreen(project, page, screen, ctx) {
|
|
|
318
366
|
|
|
319
367
|
if (!approved) break;
|
|
320
368
|
|
|
369
|
+
// These two lines exist only once there is something to compare against, so they
|
|
370
|
+
// are said here rather than by the capture: the size match and the pixel count are
|
|
371
|
+
// both answered by the single call below.
|
|
372
|
+
tell(CHECK_KEYS.size, true);
|
|
373
|
+
tell(CHECK_KEYS.pixels, true);
|
|
374
|
+
|
|
321
375
|
const stopCompare = ctx.timings?.mark('compare');
|
|
322
376
|
// compareFast, not comparePng: identical bytes are answered from the PNG header
|
|
323
377
|
// instead of decoding two retina images pixel by pixel. Nothing changed on most
|
|
@@ -325,11 +379,23 @@ async function runOneScreen(project, page, screen, ctx) {
|
|
|
325
379
|
// comparing stage from about a quarter of a second a screen to nothing at all.
|
|
326
380
|
compare = compareFast(approved.png, shot.png, settings.tolerance, shot.masks);
|
|
327
381
|
stopCompare?.();
|
|
382
|
+
tell(CHECK_KEYS.size);
|
|
383
|
+
tell(CHECK_KEYS.pixels);
|
|
328
384
|
if (compare.equal) break;
|
|
329
385
|
if (attempt <= ctx.retries) {
|
|
330
386
|
detail(`${screen.name} looked different on attempt ${attempt} — taking it again.`);
|
|
331
387
|
}
|
|
332
388
|
}
|
|
389
|
+
|
|
390
|
+
if (!approved) {
|
|
391
|
+
// Nothing to measure this against, which is an answer rather than a gap. Said
|
|
392
|
+
// here because the loop above never reached a comparison to say it.
|
|
393
|
+
tell(CHECK_KEYS.size);
|
|
394
|
+
tell(CHECK_KEYS.pixels);
|
|
395
|
+
}
|
|
396
|
+
// Whatever the page shouted while nobody was looking. Known from the moment the
|
|
397
|
+
// picture was taken, and the last line of the working either way.
|
|
398
|
+
tell(CHECK_KEYS.console);
|
|
333
399
|
} catch (error) {
|
|
334
400
|
return done(
|
|
335
401
|
{
|
package/src/types.js
CHANGED
|
@@ -304,6 +304,9 @@
|
|
|
304
304
|
* @property {string} [failedAt] The plain-language expectation that failed.
|
|
305
305
|
* @property {string} [file]
|
|
306
306
|
* @property {string} [because]
|
|
307
|
+
* @property {CheckStep[]} [checks] Every claim the guard asserted and every action it took,
|
|
308
|
+
* in order. A guard that failed still shows the claims that
|
|
309
|
+
* held before it — that is most of the value.
|
|
307
310
|
* @property {number} durationMs
|
|
308
311
|
* @property {number} [attempts]
|
|
309
312
|
*/
|
|
@@ -482,7 +485,7 @@ export {};
|
|
|
482
485
|
* run began) so a watcher can draw a timeline without keeping its own clock.
|
|
483
486
|
*
|
|
484
487
|
* @typedef {object} RunEvent
|
|
485
|
-
* @property {'run:start'|'screen:start'|'screen:shot'|'screen:done'|'guard:start'|'guard:done'|'phase'|'note'|'run:done'} type
|
|
488
|
+
* @property {'run:start'|'screen:start'|'screen:step'|'screen:shot'|'screen:done'|'guard:start'|'guard:step'|'guard:done'|'phase'|'note'|'run:done'} type
|
|
486
489
|
* @property {number} at Milliseconds since the run started.
|
|
487
490
|
* @property {string} [name] Screen or guard name.
|
|
488
491
|
* @property {string} [describe] The plain-language description.
|
|
@@ -503,6 +506,8 @@ export {};
|
|
|
503
506
|
* thumbnail is unreadable, which is the whole point of looking.
|
|
504
507
|
* @property {string} [approvedFile] file:// URL of the approved picture.
|
|
505
508
|
* @property {string} [diffFile] file:// URL of the difference image.
|
|
509
|
+
* @property {CheckStep} [step] One step, reported the moment it happens, so a watcher can
|
|
510
|
+
* tick the list off live instead of waiting for the verdict.
|
|
506
511
|
* @property {CheckStep[]} [checks] What was actually done to this screen, in order.
|
|
507
512
|
* A verdict on its own ("matches") does not tell anyone what
|
|
508
513
|
* was verified, and a row showing only a name and a duration
|
|
@@ -559,5 +564,7 @@ export {};
|
|
|
559
564
|
* @typedef {object} CheckStep
|
|
560
565
|
* @property {string} label Plain language: "held still", "every pixel compared".
|
|
561
566
|
* @property {string} [detail] The number behind it: "5,184,000 pixels, none different".
|
|
562
|
-
* @property {'ok'|'warn'|'bad'|'skipped'} state
|
|
567
|
+
* @property {'running'|'ok'|'warn'|'bad'|'skipped'} state
|
|
568
|
+
* @property {string} [key] Stable id, so a step reported as 'running' can be found
|
|
569
|
+
* again and settled when it finishes.
|
|
563
570
|
*/
|