@uniflowed/test 0.0.0-alpha.34 → 0.0.0-alpha.35
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/internal/axe.js +84 -1
- package/internal/isolation.js +41 -6
- package/package.json +2 -2
package/internal/axe.js
CHANGED
|
@@ -207,6 +207,87 @@ function excerpt(html: string): string {
|
|
|
207
207
|
return line.length > MAX_NODE_CHARS ? `${line.slice(0, MAX_NODE_CHARS)}…` : line;
|
|
208
208
|
}
|
|
209
209
|
|
|
210
|
+
/**
|
|
211
|
+
* The audit this process started last, settled or not.
|
|
212
|
+
*
|
|
213
|
+
* axe-core runs one audit at a time per process and refuses a second outright
|
|
214
|
+
* ("Axe is already running") rather than queueing it. One file never starts two
|
|
215
|
+
* on purpose, but a worker serves many files: a case that timed out in the
|
|
216
|
+
* middle of an audit leaves that audit running, and the next file's first audit
|
|
217
|
+
* used to fail on it — reported under a file that had done nothing wrong.
|
|
218
|
+
*/
|
|
219
|
+
let running: Promise<mixed> = Promise.resolve();
|
|
220
|
+
|
|
221
|
+
/**
|
|
222
|
+
* Run `audit` once the audit already running has settled, however it settles.
|
|
223
|
+
*
|
|
224
|
+
* Waiting rather than failing is safe because the audit being waited on always
|
|
225
|
+
* ends — axe-core walks a finite tree — and a case that waits too long is still
|
|
226
|
+
* bounded by its own timeout.
|
|
227
|
+
*/
|
|
228
|
+
function afterTheAuditRunning<T>(audit: () => Promise<T>): Promise<T> {
|
|
229
|
+
const turn = running.then(audit, audit);
|
|
230
|
+
running = turn.then(
|
|
231
|
+
() => undefined,
|
|
232
|
+
() => undefined,
|
|
233
|
+
);
|
|
234
|
+
return turn;
|
|
235
|
+
}
|
|
236
|
+
|
|
237
|
+
/** How long to wait between looks at an audit this module did not start. */
|
|
238
|
+
const FOREIGN_AUDIT_POLL_MS = 10;
|
|
239
|
+
|
|
240
|
+
/** How long an audit this module did not start may hold axe-core. */
|
|
241
|
+
const FOREIGN_AUDIT_LIMIT_MS = 10000;
|
|
242
|
+
|
|
243
|
+
// The timer and the clock as they were when this module loaded, so a file that
|
|
244
|
+
// fakes timers cannot stall a wait for a real audit to end.
|
|
245
|
+
const pause: typeof setTimeout = setTimeout;
|
|
246
|
+
const now: () => number = Date.now.bind(Date);
|
|
247
|
+
|
|
248
|
+
/**
|
|
249
|
+
* Whether axe-core refused to start because an audit is already running.
|
|
250
|
+
*
|
|
251
|
+
* `running` queues every audit this module starts, so a refusal means an audit
|
|
252
|
+
* started somewhere else. `uf dev`'s overlay runs axe-core itself, and stopping
|
|
253
|
+
* the overlay cannot recall an audit already walking the page: its own test can
|
|
254
|
+
* leave one finishing into the next file in the worker.
|
|
255
|
+
*/
|
|
256
|
+
function isAlreadyRunning(error: mixed): boolean {
|
|
257
|
+
return error instanceof Error && error.message.startsWith("Axe is already running");
|
|
258
|
+
}
|
|
259
|
+
|
|
260
|
+
/**
|
|
261
|
+
* Run the audit once axe-core is free of an audit this module did not start.
|
|
262
|
+
*
|
|
263
|
+
* Looks again every `FOREIGN_AUDIT_POLL_MS` until axe-core takes it, and after
|
|
264
|
+
* `FOREIGN_AUDIT_LIMIT_MS` says what held the engine rather than waiting on an
|
|
265
|
+
* audit that is not going to end.
|
|
266
|
+
*/
|
|
267
|
+
function runOnceAxeIsFree(
|
|
268
|
+
axe: $FlowFixMe,
|
|
269
|
+
node: mixed,
|
|
270
|
+
options: mixed,
|
|
271
|
+
since: number = now(),
|
|
272
|
+
): Promise<mixed> {
|
|
273
|
+
return Promise.resolve()
|
|
274
|
+
.then(() => axe.run(node, options))
|
|
275
|
+
.catch((error: mixed) => {
|
|
276
|
+
if (!isAlreadyRunning(error)) {
|
|
277
|
+
throw error;
|
|
278
|
+
}
|
|
279
|
+
if (now() - since >= FOREIGN_AUDIT_LIMIT_MS) {
|
|
280
|
+
throw new Error(
|
|
281
|
+
"axe-core has been running an audit uf did not start for ten seconds, such as " +
|
|
282
|
+
"`uf dev`'s overlay or a direct `axe.run`; this assertion could not start its own.",
|
|
283
|
+
);
|
|
284
|
+
}
|
|
285
|
+
return new Promise((resolve) => {
|
|
286
|
+
pause(resolve, FOREIGN_AUDIT_POLL_MS);
|
|
287
|
+
}).then(() => runOnceAxeIsFree(axe, node, options, since));
|
|
288
|
+
});
|
|
289
|
+
}
|
|
290
|
+
|
|
210
291
|
/**
|
|
211
292
|
* Audit `node` and report what it found, weakest results already dropped.
|
|
212
293
|
*
|
|
@@ -229,7 +310,9 @@ export async function auditElement(
|
|
|
229
310
|
}
|
|
230
311
|
const axe = await axeEngine();
|
|
231
312
|
const options = resolveOptions(overrides);
|
|
232
|
-
const results = await
|
|
313
|
+
const results = await afterTheAuditRunning(() =>
|
|
314
|
+
runOnceAxeIsFree(axe, node, axeRunOptions(options)),
|
|
315
|
+
);
|
|
233
316
|
const found: Array<AxeViolation> = [];
|
|
234
317
|
for (const raw of arrayAt(results, "violations")) {
|
|
235
318
|
const violation: AxeViolation = {
|
package/internal/isolation.js
CHANGED
|
@@ -8,10 +8,10 @@
|
|
|
8
8
|
// the process, outlives the file, and is handed to whichever file the schedule
|
|
9
9
|
// puts next in that worker.
|
|
10
10
|
//
|
|
11
|
-
// That second list is what this module is. It has been discovered
|
|
12
|
-
//
|
|
13
|
-
//
|
|
14
|
-
//
|
|
11
|
+
// That second list is what this module is. It has been discovered four times,
|
|
12
|
+
// and each time the same way — a suite that passed alone and failed beside
|
|
13
|
+
// another, naming the file that read the value rather than the file that wrote
|
|
14
|
+
// it:
|
|
15
15
|
//
|
|
16
16
|
// * ubugeeei-prod/uf#417, `uft.stubEnv("NODE_ENV", …)` still set for the
|
|
17
17
|
// next file;
|
|
@@ -20,7 +20,11 @@
|
|
|
20
20
|
// fired and the file hung with nothing on screen;
|
|
21
21
|
// * ubugeeei-prod/uf#607, `document.body` still holding the markup a
|
|
22
22
|
// hydration test wrote into it, so the next file's "there is one image on
|
|
23
|
-
// the page" found six
|
|
23
|
+
// the page" found six;
|
|
24
|
+
// * ubugeeei-prod/uf#944, the window around that body: a `matchMedia` a file
|
|
25
|
+
// removed, and the document's `FormData` left in place of Node's. That one is not on the list below, because this package does not
|
|
26
|
+
// make the window — `@uniflowed/react-testing` does, and registers how to
|
|
27
|
+
// put it back (see `registeredElsewhere`).
|
|
24
28
|
//
|
|
25
29
|
// Which files share a worker is decided by `.uf/test-timings.json`, so a leak
|
|
26
30
|
// makes the *result* of a suite depend on how the machine was loaded the last
|
|
@@ -168,6 +172,37 @@ function restoreDocument(): void {
|
|
|
168
172
|
}
|
|
169
173
|
}
|
|
170
174
|
|
|
175
|
+
/**
|
|
176
|
+
* Where other packages register process-wide state of their own.
|
|
177
|
+
*
|
|
178
|
+
* A package this one does not depend on can still install something a file
|
|
179
|
+
* changes and the next file reads: `@uniflowed/react-testing` installs a window
|
|
180
|
+
* on the first render and keeps it for the process. Only that package knows
|
|
181
|
+
* what the window looked like when it made it, so it registers how to put it
|
|
182
|
+
* back — a name for the message below, and a function — in a `Map` under this
|
|
183
|
+
* symbol, and every entry runs after the list above.
|
|
184
|
+
*
|
|
185
|
+
* A symbol from the global registry rather than an export, so the package that
|
|
186
|
+
* registers needs no import of this one: under another runner the entry is
|
|
187
|
+
* never read and costs nothing.
|
|
188
|
+
*/
|
|
189
|
+
const SHARED_STATE: symbol = Symbol.for("@uniflowed/test/shared-state");
|
|
190
|
+
|
|
191
|
+
/** Every entry another package registered, as this module's own entries are shaped. */
|
|
192
|
+
function registeredElsewhere(): $ReadOnlyArray<Shared> {
|
|
193
|
+
const registry: mixed = Reflect.get(globalThis, SHARED_STATE);
|
|
194
|
+
if (!(registry instanceof Map)) {
|
|
195
|
+
return [];
|
|
196
|
+
}
|
|
197
|
+
const entries: Array<Shared> = [];
|
|
198
|
+
for (const [what, restore] of registry) {
|
|
199
|
+
if (typeof what === "string" && typeof restore === "function") {
|
|
200
|
+
entries.push({ what, restore: () => void restore() });
|
|
201
|
+
}
|
|
202
|
+
}
|
|
203
|
+
return entries;
|
|
204
|
+
}
|
|
205
|
+
|
|
171
206
|
/**
|
|
172
207
|
* Put back everything the file that just ran may have changed.
|
|
173
208
|
*
|
|
@@ -185,7 +220,7 @@ function restoreDocument(): void {
|
|
|
185
220
|
*/
|
|
186
221
|
export function restoreSharedState(): void {
|
|
187
222
|
let failure: { readonly what: string, readonly thrown: mixed } | null = null;
|
|
188
|
-
for (const shared of SHARED) {
|
|
223
|
+
for (const shared of [...SHARED, ...registeredElsewhere()]) {
|
|
189
224
|
try {
|
|
190
225
|
shared.restore();
|
|
191
226
|
} catch (thrown) {
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@uniflowed/test",
|
|
3
|
-
"version": "0.0.0-alpha.
|
|
3
|
+
"version": "0.0.0-alpha.35",
|
|
4
4
|
"description": "The test API and worker for `uf test`: describe/it, a full matcher set, and the process uf fans test files out to.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"license": "MIT",
|
|
@@ -26,7 +26,7 @@
|
|
|
26
26
|
"!*.test.js"
|
|
27
27
|
],
|
|
28
28
|
"dependencies": {
|
|
29
|
-
"@uniflowed/host": "0.0.0-alpha.
|
|
29
|
+
"@uniflowed/host": "0.0.0-alpha.35"
|
|
30
30
|
},
|
|
31
31
|
"peerDependencies": {
|
|
32
32
|
"axe-core": ">=4"
|