staysfixed 0.2.2 → 0.3.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/package.json +1 -1
- package/src/core/events.js +674 -0
- package/src/guard/api.js +168 -12
- package/src/guard/run.js +104 -6
- package/src/picture/capture.js +198 -3
- package/src/picture/run.js +157 -18
- package/src/types.js +25 -1
- package/src/watch/panel.js +836 -168
package/src/guard/api.js
CHANGED
|
@@ -5,6 +5,14 @@
|
|
|
5
5
|
* who wrote it has forgotten the bug. That is why assertions are a sentence plus
|
|
6
6
|
* a check, and never a bare comparison: `expect('the sidebar is hidden', ...)`
|
|
7
7
|
* fails with "expected: the sidebar is hidden", which anyone can act on.
|
|
8
|
+
*
|
|
9
|
+
* Those sentences are also the answer to the fairest question anyone asks about
|
|
10
|
+
* this tool: "is it only about how things look?" It is not — a guard drives the
|
|
11
|
+
* app and asserts what it still does — but that was invisible, because a guard
|
|
12
|
+
* reported one line however many things it proved. So every claim, and every
|
|
13
|
+
* action between the claims, now says itself out loud the moment it happens:
|
|
14
|
+
* announced as it starts, settled as it finishes. The list a person watches tick
|
|
15
|
+
* off IS the guard's own words, in the guard's own order.
|
|
8
16
|
*/
|
|
9
17
|
|
|
10
18
|
import { exec } from 'node:child_process';
|
|
@@ -28,15 +36,79 @@ const DEFAULT_RUN_TIMEOUT = 60_000;
|
|
|
28
36
|
/** Commands can print a lot; 10MB before we cut them off. */
|
|
29
37
|
const MAX_OUTPUT = 10 * 1024 * 1024;
|
|
30
38
|
|
|
39
|
+
/** Longest a selector, path or command is shown before it is cut short. */
|
|
40
|
+
const MAX_LABEL = 80;
|
|
41
|
+
|
|
42
|
+
/**
|
|
43
|
+
* How a step's id says what kind of step it is.
|
|
44
|
+
*
|
|
45
|
+
* An assertion is the point of a guard; opening a page or clicking a button is
|
|
46
|
+
* the setup that gets there. `CheckStep` has no room for that difference — and
|
|
47
|
+
* should not grow one for the sake of a colour — so it rides on the id, which
|
|
48
|
+
* every step needs anyway. Anything watching can draw `claim-` lines loud and
|
|
49
|
+
* `did-` lines quiet; anything that does not care sees a perfectly ordinary list.
|
|
50
|
+
*/
|
|
51
|
+
const CLAIM = 'claim';
|
|
52
|
+
const ACTION = 'did';
|
|
53
|
+
|
|
54
|
+
/**
|
|
55
|
+
* What a guard is told to report to, when anyone is collecting.
|
|
56
|
+
* @typedef {object} GuardApiOptions
|
|
57
|
+
* @property {(step: import('../types.js').CheckStep) => void} [onStep]
|
|
58
|
+
* Called as each claim and each action starts, and again as it settles.
|
|
59
|
+
* The two calls carry the same `key`.
|
|
60
|
+
*/
|
|
61
|
+
|
|
31
62
|
/**
|
|
32
63
|
* Build the object passed to a guard's `run`.
|
|
33
64
|
*
|
|
34
65
|
* @param {import('../types.js').PageApi} page
|
|
35
66
|
* @param {import('../types.js').Project} project
|
|
67
|
+
* @param {GuardApiOptions} [opts]
|
|
36
68
|
* @returns {import('../types.js').GuardApi}
|
|
37
69
|
*/
|
|
38
|
-
export function makeGuardApi(page, project) {
|
|
70
|
+
export function makeGuardApi(page, project, opts = {}) {
|
|
39
71
|
const root = project.paths.root;
|
|
72
|
+
const onStep = typeof opts.onStep === 'function' ? opts.onStep : null;
|
|
73
|
+
let counted = 0;
|
|
74
|
+
|
|
75
|
+
/**
|
|
76
|
+
* Hand one step out, and never let that matter.
|
|
77
|
+
*
|
|
78
|
+
* Reporting is a convenience laid over a check; a listener that throws must not
|
|
79
|
+
* change whether a bug that was fixed is still fixed.
|
|
80
|
+
*
|
|
81
|
+
* @param {import('../types.js').CheckStep} step
|
|
82
|
+
* @returns {void}
|
|
83
|
+
*/
|
|
84
|
+
function tell(step) {
|
|
85
|
+
if (!onStep) return;
|
|
86
|
+
try {
|
|
87
|
+
onStep(step);
|
|
88
|
+
} catch {
|
|
89
|
+
// Watching is never worth a guard.
|
|
90
|
+
}
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
/**
|
|
94
|
+
* Say a thing has started, and hand back the way to say how it went.
|
|
95
|
+
*
|
|
96
|
+
* When nobody is collecting this allocates nothing and returns a function that
|
|
97
|
+
* does nothing, so a guard run with no watcher costs exactly what it did before.
|
|
98
|
+
*
|
|
99
|
+
* @param {string} kind CLAIM or ACTION.
|
|
100
|
+
* @param {string} label Plain language, already final — the same words settle it.
|
|
101
|
+
* @returns {(state: import('../types.js').CheckStep['state'], detail?: string) => void}
|
|
102
|
+
*/
|
|
103
|
+
function announce(kind, label) {
|
|
104
|
+
if (!onStep) return () => {};
|
|
105
|
+
counted += 1;
|
|
106
|
+
const key = `${kind}-${counted}`;
|
|
107
|
+
tell({ key, label, state: 'running' });
|
|
108
|
+
return (state, detail) => {
|
|
109
|
+
tell(detail ? { key, label, detail, state } : { key, label, state });
|
|
110
|
+
};
|
|
111
|
+
}
|
|
40
112
|
|
|
41
113
|
return {
|
|
42
114
|
page,
|
|
@@ -46,16 +118,30 @@ export function makeGuardApi(page, project) {
|
|
|
46
118
|
* @param {string} to
|
|
47
119
|
* @returns {Promise<void>}
|
|
48
120
|
*/
|
|
49
|
-
open(to) {
|
|
50
|
-
|
|
121
|
+
async open(to) {
|
|
122
|
+
const settle = announce(ACTION, `opened ${short(to)}`);
|
|
123
|
+
try {
|
|
124
|
+
await page.goto(to);
|
|
125
|
+
} catch (error) {
|
|
126
|
+
settle('bad', reasonOf(error));
|
|
127
|
+
throw error;
|
|
128
|
+
}
|
|
129
|
+
settle('ok');
|
|
51
130
|
},
|
|
52
131
|
|
|
53
132
|
/**
|
|
54
133
|
* @param {string} selector
|
|
55
134
|
* @returns {Promise<void>}
|
|
56
135
|
*/
|
|
57
|
-
click(selector) {
|
|
58
|
-
|
|
136
|
+
async click(selector) {
|
|
137
|
+
const settle = announce(ACTION, `clicked ${short(selector)}`);
|
|
138
|
+
try {
|
|
139
|
+
await page.click(selector);
|
|
140
|
+
} catch (error) {
|
|
141
|
+
settle('bad', reasonOf(error));
|
|
142
|
+
throw error;
|
|
143
|
+
}
|
|
144
|
+
settle('ok');
|
|
59
145
|
},
|
|
60
146
|
|
|
61
147
|
/**
|
|
@@ -75,18 +161,32 @@ export function makeGuardApi(page, project) {
|
|
|
75
161
|
});
|
|
76
162
|
}
|
|
77
163
|
|
|
164
|
+
// The claim goes out before it is checked, not after. A person watching
|
|
165
|
+
// sees what is being asked while it is being asked — which is the whole
|
|
166
|
+
// difference between a list that ticks and a table that appears.
|
|
167
|
+
const settle = announce(CLAIM, claim.trim());
|
|
168
|
+
|
|
78
169
|
let result;
|
|
79
170
|
try {
|
|
80
171
|
result = await check();
|
|
81
172
|
} catch (cause) {
|
|
82
173
|
// A nested expectation already reads well — do not bury it in another layer.
|
|
83
|
-
if (cause instanceof ExpectationFailed)
|
|
84
|
-
|
|
174
|
+
if (cause instanceof ExpectationFailed) {
|
|
175
|
+
settle('bad', cause.claim === claim.trim() ? undefined : `inside it: ${cause.claim}`);
|
|
176
|
+
throw cause;
|
|
177
|
+
}
|
|
178
|
+
const why = cause instanceof Error ? cause.message : String(cause);
|
|
179
|
+
settle('bad', firstLine(why));
|
|
180
|
+
throw new Error(`while checking '${claim}': ${why}`, {
|
|
85
181
|
cause,
|
|
86
182
|
});
|
|
87
183
|
}
|
|
88
184
|
|
|
89
|
-
if (isNegative(result))
|
|
185
|
+
if (isNegative(result)) {
|
|
186
|
+
settle('bad', 'this is not true any more');
|
|
187
|
+
throw new ExpectationFailed(claim);
|
|
188
|
+
}
|
|
189
|
+
settle('ok');
|
|
90
190
|
},
|
|
91
191
|
|
|
92
192
|
/**
|
|
@@ -94,15 +194,17 @@ export function makeGuardApi(page, project) {
|
|
|
94
194
|
* must still succeed, a file that must still be generated — live here.
|
|
95
195
|
*
|
|
96
196
|
* A non-zero exit is returned, never thrown: whether it means failure is the
|
|
97
|
-
* guard's decision, not ours.
|
|
197
|
+
* guard's decision, not ours. The step says so the same way — a command that
|
|
198
|
+
* came back unhappy is worth noticing, and is still not a verdict.
|
|
98
199
|
*
|
|
99
200
|
* @param {string} cmd
|
|
100
201
|
* @param {{cwd?: string, timeoutMs?: number}} [runOpts]
|
|
101
202
|
* @returns {Promise<{code: number, stdout: string, stderr: string}>}
|
|
102
203
|
*/
|
|
103
|
-
run(cmd, runOpts = {}) {
|
|
204
|
+
async run(cmd, runOpts = {}) {
|
|
104
205
|
const cwd = runOpts.cwd ? path.resolve(root, runOpts.cwd) : root;
|
|
105
206
|
const timeoutMs = runOpts.timeoutMs ?? DEFAULT_RUN_TIMEOUT;
|
|
207
|
+
const settle = announce(ACTION, `ran ${short(cmd)}`);
|
|
106
208
|
|
|
107
209
|
/** @type {Promise<{code: number, stdout: string, stderr: string}>} */
|
|
108
210
|
const finished = new Promise((resolve) => {
|
|
@@ -129,7 +231,13 @@ export function makeGuardApi(page, project) {
|
|
|
129
231
|
},
|
|
130
232
|
);
|
|
131
233
|
});
|
|
132
|
-
|
|
234
|
+
|
|
235
|
+
const outcome = await finished;
|
|
236
|
+
if (outcome.code === 0) settle('ok', 'finished cleanly, code 0');
|
|
237
|
+
else if (outcome.code === 124) {
|
|
238
|
+
settle('warn', `stopped after ${humanTime(timeoutMs)}, code 124`);
|
|
239
|
+
} else settle('warn', `came back with code ${outcome.code}`);
|
|
240
|
+
return outcome;
|
|
133
241
|
},
|
|
134
242
|
|
|
135
243
|
/**
|
|
@@ -149,20 +257,30 @@ export function makeGuardApi(page, project) {
|
|
|
149
257
|
});
|
|
150
258
|
}
|
|
151
259
|
|
|
260
|
+
const settle = announce(ACTION, `read ${short(relative)}`);
|
|
261
|
+
/** @type {string} */
|
|
262
|
+
let text;
|
|
152
263
|
try {
|
|
153
|
-
|
|
264
|
+
text = await fsp.readFile(full, 'utf8');
|
|
154
265
|
} catch (cause) {
|
|
155
266
|
const code = /** @type {any} */ (cause)?.code;
|
|
156
267
|
if (code === 'ENOENT') {
|
|
268
|
+
settle('bad', 'there is no such file');
|
|
157
269
|
throw new StaysFixedError(`There is no file called "${relative}" in the project.`, { cause });
|
|
158
270
|
}
|
|
159
271
|
if (code === 'EISDIR') {
|
|
272
|
+
settle('bad', 'that is a folder, not a file');
|
|
160
273
|
throw new StaysFixedError(`"${relative}" is a folder, not a file.`, { cause });
|
|
161
274
|
}
|
|
275
|
+
settle('bad', reasonOf(cause));
|
|
162
276
|
throw new StaysFixedError(`Could not read "${relative}": ${cause instanceof Error ? cause.message : String(cause)}`, {
|
|
163
277
|
cause,
|
|
164
278
|
});
|
|
165
279
|
}
|
|
280
|
+
|
|
281
|
+
const lines = text === '' ? 0 : text.split('\n').length;
|
|
282
|
+
settle('ok', `${count(lines)} ${lines === 1 ? 'line' : 'lines'}`);
|
|
283
|
+
return text;
|
|
166
284
|
},
|
|
167
285
|
};
|
|
168
286
|
}
|
|
@@ -186,6 +304,44 @@ function isNegative(value) {
|
|
|
186
304
|
return false;
|
|
187
305
|
}
|
|
188
306
|
|
|
307
|
+
/**
|
|
308
|
+
* A selector, path or command, short enough to read in a list.
|
|
309
|
+
*
|
|
310
|
+
* @param {string} text
|
|
311
|
+
* @returns {string}
|
|
312
|
+
*/
|
|
313
|
+
function short(text) {
|
|
314
|
+
const one = String(text ?? '').replace(/\s+/g, ' ').trim();
|
|
315
|
+
if (one === '') return 'nothing';
|
|
316
|
+
return one.length > MAX_LABEL ? `${one.slice(0, MAX_LABEL - 1)}…` : one;
|
|
317
|
+
}
|
|
318
|
+
|
|
319
|
+
/**
|
|
320
|
+
* @param {unknown} error
|
|
321
|
+
* @returns {string}
|
|
322
|
+
*/
|
|
323
|
+
function reasonOf(error) {
|
|
324
|
+
return firstLine(error instanceof Error ? error.message : String(error));
|
|
325
|
+
}
|
|
326
|
+
|
|
327
|
+
/**
|
|
328
|
+
* @param {string} text
|
|
329
|
+
* @returns {string}
|
|
330
|
+
*/
|
|
331
|
+
function firstLine(text) {
|
|
332
|
+
const line = String(text ?? '').split('\n')[0].trim();
|
|
333
|
+
if (line === '') return 'it did not say why';
|
|
334
|
+
return line.length > 120 ? `${line.slice(0, 119)}…` : line;
|
|
335
|
+
}
|
|
336
|
+
|
|
337
|
+
/**
|
|
338
|
+
* @param {number} n
|
|
339
|
+
* @returns {string}
|
|
340
|
+
*/
|
|
341
|
+
function count(n) {
|
|
342
|
+
return Number.isFinite(n) ? Math.round(n).toLocaleString('en-US') : String(n);
|
|
343
|
+
}
|
|
344
|
+
|
|
189
345
|
/**
|
|
190
346
|
* @param {number} ms
|
|
191
347
|
* @returns {string}
|
package/src/guard/run.js
CHANGED
|
@@ -6,6 +6,14 @@
|
|
|
6
6
|
* from the same clean state, a guard that needs a second go is recorded as
|
|
7
7
|
* wobbly rather than green, and the failure message carries the story of the
|
|
8
8
|
* original bug so nobody has to go looking for it.
|
|
9
|
+
*
|
|
10
|
+
* A guard also has to be watchable while it happens. It is the part of this tool
|
|
11
|
+
* that checks behaviour rather than looks — it drives the app and asserts what it
|
|
12
|
+
* still does — and reporting it as a single line hid every one of those
|
|
13
|
+
* assertions. So each claim and each action is passed straight out as it starts
|
|
14
|
+
* and again as it settles, and the whole list travels with the result. A guard
|
|
15
|
+
* that fails on its fifth claim still shows the four that held: "these are fine,
|
|
16
|
+
* this one is not" is most of the value of running it at all.
|
|
9
17
|
*/
|
|
10
18
|
|
|
11
19
|
import { makeGuardApi, ExpectationFailed } from './api.js';
|
|
@@ -14,8 +22,14 @@ import { emitEvent } from '../core/events.js';
|
|
|
14
22
|
|
|
15
23
|
const DEFAULT_TIMEOUT = 30_000;
|
|
16
24
|
|
|
25
|
+
/** How the runner names its own first step — the clean start every guard gets. */
|
|
26
|
+
const FRESH_KEY = 'fresh';
|
|
27
|
+
|
|
17
28
|
/**
|
|
18
|
-
* @typedef {import('../types.js').GuardResult & {
|
|
29
|
+
* @typedef {import('../types.js').GuardResult & {
|
|
30
|
+
* retriedToPass?: boolean,
|
|
31
|
+
* checks?: import('../types.js').CheckStep[],
|
|
32
|
+
* }} GuardRunResult
|
|
19
33
|
*/
|
|
20
34
|
|
|
21
35
|
/**
|
|
@@ -25,6 +39,8 @@ const DEFAULT_TIMEOUT = 30_000;
|
|
|
25
39
|
* @property {string} [failedAt]
|
|
26
40
|
*/
|
|
27
41
|
|
|
42
|
+
/** @typedef {(step: import('../types.js').CheckStep) => void} StepSink */
|
|
43
|
+
|
|
28
44
|
/**
|
|
29
45
|
* Run every guard against an app that is already open.
|
|
30
46
|
*
|
|
@@ -91,10 +107,31 @@ export async function runGuards(project, app, guards, opts = {}) {
|
|
|
91
107
|
/** @type {AttemptOutcome} */
|
|
92
108
|
let outcome = { ok: false, message: 'This guard did not run.' };
|
|
93
109
|
let attempts = 0;
|
|
110
|
+
/** @type {import('../types.js').CheckStep[]} */
|
|
111
|
+
let checks = [];
|
|
94
112
|
|
|
95
113
|
while (attempts < retries + 1) {
|
|
96
114
|
attempts += 1;
|
|
97
|
-
|
|
115
|
+
// A second go starts the list again. What a person needs to see is what the
|
|
116
|
+
// verdict was actually made on, and that is the last attempt — the earlier
|
|
117
|
+
// one is already recorded, more usefully, as "it only passed on try 2".
|
|
118
|
+
const attempt = attempts;
|
|
119
|
+
/** @type {import('../types.js').CheckStep[]} */
|
|
120
|
+
const collected = [];
|
|
121
|
+
checks = collected;
|
|
122
|
+
|
|
123
|
+
/** @type {StepSink} */
|
|
124
|
+
const onStep = (step) => {
|
|
125
|
+
// Keys are unique inside one attempt; a retry re-announces the same
|
|
126
|
+
// claims, and a watcher must not mistake the second run of a claim for
|
|
127
|
+
// the settling of the first.
|
|
128
|
+
const stamped =
|
|
129
|
+
attempt > 1 && step.key ? { ...step, key: `try${attempt}-${step.key}` } : step;
|
|
130
|
+
record(collected, stamped);
|
|
131
|
+
emitEvent(events, { type: 'guard:step', name: guard.name, step: stamped });
|
|
132
|
+
};
|
|
133
|
+
|
|
134
|
+
outcome = await attemptGuard(project, app, guard, baseUrl, timeoutMs, onStep);
|
|
98
135
|
if (outcome.ok) break;
|
|
99
136
|
if (opts.signal?.aborted) break;
|
|
100
137
|
}
|
|
@@ -108,6 +145,7 @@ export async function runGuards(project, app, guards, opts = {}) {
|
|
|
108
145
|
durationMs: Date.now() - startedAt,
|
|
109
146
|
attempts,
|
|
110
147
|
};
|
|
148
|
+
if (checks.length > 0) result.checks = checks;
|
|
111
149
|
|
|
112
150
|
if (outcome.ok) {
|
|
113
151
|
// Passing only on the second go is not passing. The flake register picks
|
|
@@ -127,6 +165,29 @@ export async function runGuards(project, app, guards, opts = {}) {
|
|
|
127
165
|
return results;
|
|
128
166
|
}
|
|
129
167
|
|
|
168
|
+
/**
|
|
169
|
+
* Put one step into the list it belongs to.
|
|
170
|
+
*
|
|
171
|
+
* A step is said twice — once as it starts, once as it finishes — and the list
|
|
172
|
+
* should hold one line per thing, not two. The settled version replaces the
|
|
173
|
+
* running one in place, so the order stays the order it happened in.
|
|
174
|
+
*
|
|
175
|
+
* @param {import('../types.js').CheckStep[]} into
|
|
176
|
+
* @param {import('../types.js').CheckStep} step
|
|
177
|
+
* @returns {void}
|
|
178
|
+
*/
|
|
179
|
+
function record(into, step) {
|
|
180
|
+
if (step.key) {
|
|
181
|
+
for (let i = 0; i < into.length; i++) {
|
|
182
|
+
if (into[i].key === step.key) {
|
|
183
|
+
into[i] = step;
|
|
184
|
+
return;
|
|
185
|
+
}
|
|
186
|
+
}
|
|
187
|
+
}
|
|
188
|
+
into.push(step);
|
|
189
|
+
}
|
|
190
|
+
|
|
130
191
|
/**
|
|
131
192
|
* @param {import('../types.js').RunEvents|undefined} events
|
|
132
193
|
* @param {GuardRunResult} result
|
|
@@ -141,6 +202,10 @@ function emitGuardDone(events, result) {
|
|
|
141
202
|
message: result.message,
|
|
142
203
|
failedAt: result.failedAt,
|
|
143
204
|
because: result.because,
|
|
205
|
+
// Everything this guard actually asserted, in its own words and its own
|
|
206
|
+
// order — so a listener that arrived late, or one that only keeps the
|
|
207
|
+
// verdicts, still has the working.
|
|
208
|
+
checks: result.checks,
|
|
144
209
|
});
|
|
145
210
|
}
|
|
146
211
|
|
|
@@ -152,12 +217,26 @@ function emitGuardDone(events, result) {
|
|
|
152
217
|
* @param {import('../types.js').Guard} guard
|
|
153
218
|
* @param {string|undefined} baseUrl
|
|
154
219
|
* @param {number} timeoutMs
|
|
220
|
+
* @param {StepSink} [onStep]
|
|
155
221
|
* @returns {Promise<AttemptOutcome>}
|
|
156
222
|
*/
|
|
157
|
-
async function attemptGuard(project, app, guard, baseUrl, timeoutMs) {
|
|
223
|
+
async function attemptGuard(project, app, guard, baseUrl, timeoutMs, onStep) {
|
|
158
224
|
/** @type {ReturnType<typeof setTimeout>|undefined} */
|
|
159
225
|
let timer;
|
|
160
226
|
|
|
227
|
+
/**
|
|
228
|
+
* @param {import('../types.js').CheckStep} step
|
|
229
|
+
* @returns {void}
|
|
230
|
+
*/
|
|
231
|
+
const tell = (step) => {
|
|
232
|
+
if (!onStep) return;
|
|
233
|
+
try {
|
|
234
|
+
onStep(step);
|
|
235
|
+
} catch {
|
|
236
|
+
// Watching a guard must never be able to fail one.
|
|
237
|
+
}
|
|
238
|
+
};
|
|
239
|
+
|
|
161
240
|
try {
|
|
162
241
|
await Promise.race([
|
|
163
242
|
(async () => {
|
|
@@ -171,10 +250,29 @@ async function attemptGuard(project, app, guard, baseUrl, timeoutMs) {
|
|
|
171
250
|
// own, which is the single most confusing shape a failure can take. So an
|
|
172
251
|
// Electron window is reloaded instead. Its main process keeps whatever it
|
|
173
252
|
// was holding; only the screen goes back to how it opened.
|
|
174
|
-
|
|
175
|
-
|
|
253
|
+
const fresh = 'started from a clean screen';
|
|
254
|
+
tell({ key: FRESH_KEY, label: fresh, state: 'running' });
|
|
255
|
+
try {
|
|
256
|
+
if (baseUrl) await app.page.goto(baseUrl);
|
|
257
|
+
else await resetWindow(app);
|
|
258
|
+
} catch (error) {
|
|
259
|
+
tell({
|
|
260
|
+
key: FRESH_KEY,
|
|
261
|
+
label: fresh,
|
|
262
|
+
detail: error instanceof Error ? error.message : String(error),
|
|
263
|
+
state: 'bad',
|
|
264
|
+
});
|
|
265
|
+
throw error;
|
|
266
|
+
}
|
|
267
|
+
tell({
|
|
268
|
+
key: FRESH_KEY,
|
|
269
|
+
label: fresh,
|
|
270
|
+
detail: baseUrl ? 'back to the front door' : 'the window was reloaded',
|
|
271
|
+
state: 'ok',
|
|
272
|
+
});
|
|
273
|
+
|
|
176
274
|
clearConsole(app);
|
|
177
|
-
await guard.run(makeGuardApi(app.page, project));
|
|
275
|
+
await guard.run(makeGuardApi(app.page, project, onStep ? { onStep } : {}));
|
|
178
276
|
})(),
|
|
179
277
|
new Promise((_resolve, reject) => {
|
|
180
278
|
timer = setTimeout(() => {
|