testaro 78.4.0 → 78.6.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/tests/qualWeb.ts CHANGED
@@ -15,27 +15,13 @@ import type {Page} from 'playwright';
15
15
  import {getAttributeXPath, getXPathCatalogIndex} from '../procs/xPath';
16
16
  import {getStandardResult, addInstance} from '../procs/standard';
17
17
  import type {Act, Report, StandardInstance, StandardResult} from '../types';
18
- /*
19
- The @qualweb packages declare their types only in package-exports maps, which
20
- this project's node10 module resolution cannot read, so the imports stay
21
- requires and are untyped.
22
- */
23
- const {QualWeb} = require('@qualweb/core');
24
- const {ACTRules} = require('@qualweb/act-rules');
25
- const {WCAGTechniques} = require('@qualweb/wcag-techniques');
26
- const {BestPractices} = require('@qualweb/best-practices');
27
- const {PlaywrightDriver} = require('@qualweb/playwright-driver');
28
- import {qualWebDefaults} from '../procs/config';
18
+ import {readFileSync} from 'fs';
29
19
 
30
20
  // TYPES
31
21
 
32
22
  // The qualWeb-act properties this reporter consumes.
33
23
  interface QualWebAct extends Act {
34
24
  rules?: string[];
35
- // Whether the tool's browser runs stealth evasions and blocks ads and trackers (both default
36
- // to true, or to QUALWEB_STEALTH and QUALWEB_ADBLOCK when set).
37
- stealth?: boolean;
38
- adBlock?: boolean;
39
25
  }
40
26
  // The section names of a QualWeb report.
41
27
  type QwSection = 'act-rules' | 'wcag-techniques' | 'best-practices';
@@ -61,87 +47,39 @@ interface QwRuleAssertions {
61
47
  interface QwModuleReport {
62
48
  assertions?: Record<string, QwRuleAssertions>;
63
49
  }
64
- // The native result: the customHtml QualWeb report.
50
+ // The native result: the module reports, keyed by section, as in QualWeb's own report.
65
51
  interface QwNativeResult {
66
- system?: {
67
- page?: {
68
- dom?: unknown;
69
- };
70
- };
52
+ system?: {url?: string; evaluation?: string};
71
53
  modules?: Record<string, QwModuleReport>;
72
54
  }
73
- // The options of a QualWeb evaluation.
74
- interface QualWebOptions {
75
- log: {console: boolean; file: boolean};
76
- crawlOptions: {
77
- maxDepth: number;
78
- maxUrls: number;
79
- timeout: number;
80
- maxParallelCrawls: number;
81
- logging: boolean;
82
- };
83
- execute: {counter: boolean; act?: boolean; wcag?: boolean; bp?: boolean};
84
- modules: unknown[];
85
- html?: string;
86
- 'act-rules'?: {rules?: string[]; levels?: string[]; principles?: string[]};
87
- 'wcag-techniques'?: {techniques?: string[]; levels?: string[]; principles?: string[]};
88
- 'best-practices'?: {bestPractices?: string[]};
55
+ // The options of one section's runner (QualWeb's EvaluationModule options).
56
+ interface QwSectionOptions {
57
+ include?: string[];
58
+ exclude?: string[];
59
+ levels?: string[];
60
+ principles?: string[];
89
61
  }
90
62
 
91
63
  // CONSTANTS
92
64
 
93
65
  /*
94
- QualWeb core engine with Playwright as driver, one instance per combination of options,
95
- created on first use. The driver's constructor options are {launcher, launchOptions,
96
- browser}, so its former {adBlock, stealth} options were ignored; stealth is provided by
97
- launching through playwright-extra with the stealth plugin, and ad and tracker blocking by
98
- the Ghostery blocker enabled on each page before it loads (QualWeb's beforePageLoad hook).
99
- The blocker's filter lists are cached in the temporary directory.
66
+ QualWeb runs in the page under test, as @qualweb/core runs it in its own browser: the
67
+ page-side bundles first, then one bundle per section. It used to be given page.content() and
68
+ run by @qualweb/core, which loads that HTML with setContent() into a blank page — so relative
69
+ stylesheet links resolved nowhere, the page it tested was unstyled, and every CSS-dependent
70
+ rule (contrast, visibility, text spacing…) judged a page the user never sees. The bundles are
71
+ evaluated (not added as script tags), so the page's Content Security Policy does not apply.
100
72
  */
101
- interface QualWebEngine {
102
- start: (options: unknown) => Promise<void>;
103
- evaluate: (options: unknown) => Promise<{customHtml: QwNativeResult}>;
104
- stop: () => Promise<void>;
105
- use: (plugin: unknown) => void;
106
- }
107
- const qualWebInstances: Record<string, QualWebEngine> = {};
108
- const getQualWeb = (options: {stealth: boolean; adBlock: boolean}): QualWebEngine => {
109
- const key = `${options.stealth}:${options.adBlock}`;
110
- if (! qualWebInstances[key]) {
111
- let launcher;
112
- if (options.stealth) {
113
- const {chromium} = require('playwright-extra');
114
- chromium.use(require('puppeteer-extra-plugin-stealth')());
115
- launcher = chromium;
116
- }
117
- const qualWeb: QualWebEngine = new QualWeb(undefined, new PlaywrightDriver(launcher ? {launcher} : {}));
118
- if (options.adBlock) {
119
- const {PlaywrightBlocker} = require('@ghostery/adblocker-playwright');
120
- let blockerPromise: Promise<unknown> | null = null;
121
- qualWeb.use({
122
- async beforePageLoad(page: {nativePage: unknown}) {
123
- blockerPromise ??= PlaywrightBlocker.fromPrebuiltAdsAndTracking(fetch, {
124
- path: require('path').join(require('os').tmpdir(), 'qualweb-adblocker-engine.bin'),
125
- read: require('fs').promises.readFile,
126
- write: require('fs').promises.writeFile
127
- }).catch((error: Error) => {
128
- console.log(`qualWeb: ad blocker unavailable, continuing without it (${error.message})`);
129
- return null;
130
- });
131
- const blocker = await blockerPromise as {enableBlockingInPage: (page: unknown) => Promise<void>} | null;
132
- if (blocker) {
133
- await blocker.enableBlockingInPage(page.nativePage);
134
- }
135
- }
136
- });
137
- }
138
- qualWebInstances[key] = qualWeb;
139
- }
140
- return qualWebInstances[key];
73
+ const readBundle = (name: string): string => readFileSync(require.resolve(name), 'utf8');
74
+ let pageBundles: string[] | null = null;
75
+ const sectionBundles: Partial<Record<QwSection, string>> = {};
76
+ const sectionPackages: Record<QwSection, string> = {
77
+ 'act-rules': '@qualweb/act-rules',
78
+ 'wcag-techniques': '@qualweb/wcag-techniques',
79
+ 'best-practices': '@qualweb/best-practices'
141
80
  };
142
- const actRulesModule = new ACTRules({});
143
- const wcagModule = new WCAGTechniques({});
144
- const bpModule = new BestPractices({});
81
+ // Rules a section may exclude after they crash before the section is given up on.
82
+ const MAX_EXCLUDED = 5;
145
83
  // Mapping of QualWeb module violation types to ordinal severities.
146
84
  const ordinalSeverities: Record<QwSection, Record<string, StandardInstance['ordinalSeverity']>> = {
147
85
  'act-rules': {
@@ -168,20 +106,10 @@ const ordinalSeverities: Record<QwSection, Record<string, StandardInstance['ordi
168
106
 
169
107
  // Conducts and reports the QualWeb tests.
170
108
  export const reporter = async (page: Page, report: Report, actIndex: number, timeLimit: number) => {
171
- const qualWebAct = report.acts[actIndex] as QualWebAct;
172
- const qualWeb = getQualWeb({
173
- stealth: qualWebAct.stealth ?? qualWebDefaults.stealth,
174
- adBlock: qualWebAct.adBlock ?? qualWebDefaults.adBlock
175
- });
176
109
  const act = report.acts[actIndex] as QualWebAct;
177
110
  const {rules} = act;
178
- const clusterOptions = {
179
- maxConcurrency: 1,
180
- timeout: timeLimit * 1000,
181
- monitor: false
182
- };
183
111
  // Initialize the act report.
184
- const data: {prevented?: boolean; error?: string} = {};
112
+ const data: {prevented?: boolean; error?: string; rulePreventions?: Record<string, string>} = {};
185
113
  const result: {nativeResult: QwNativeResult; standardResult: StandardResult} = {
186
114
  nativeResult: {},
187
115
  standardResult: {}
@@ -192,209 +120,210 @@ export const reporter = async (page: Page, report: Report, actIndex: number, tim
192
120
  // Initialize the standard result.
193
121
  result.standardResult = getStandardResult();
194
122
  }
195
- try {
196
- // Start the QualWeb core engine, which launches a Playwright browser.
197
- await qualWeb.start(clusterOptions);
198
- }
199
- // If the start fails:
200
- catch(error) {
123
+ /*
124
+ QualWeb tests HTML documents only: on anything else (e.g. an SVG image) its rules report
125
+ false results such as a missing lang on the html element.
126
+ */
127
+ const rootNamespace = await page.evaluate(() => document.documentElement?.namespaceURI ?? null)
128
+ .catch(() => null);
129
+ // If the document is not HTML:
130
+ if (rootNamespace !== 'http://www.w3.org/1999/xhtml') {
131
+ // Report this.
201
132
  return {
202
133
  data: {
203
134
  prevented: true,
204
- error: `Core engine start failed (${(error as Error).message})`
135
+ error: `qualWeb tests HTML documents only (root namespace ${rootNamespace})`
205
136
  },
206
137
  result
207
138
  };
208
139
  }
209
- // Otherwise, i.e. if the start succeeds, specify the invariant test options.
210
- const qualWebOptions: QualWebOptions = {
211
- log: {
212
- console: false,
213
- file: false
214
- },
215
- crawlOptions: {
216
- maxDepth: 0,
217
- maxUrls: 1,
218
- timeout: timeLimit * 1000,
219
- maxParallelCrawls: 1,
220
- logging: true
221
- },
222
- execute: {
223
- counter: true
224
- },
225
- modules: []
140
+ // Specify, per section, whether and what to test (a rules spec of 'act:', 'wcag:' or 'best:'
141
+ // alone skips that section; with numbers, it tests only those rules).
142
+ const allAAA = {
143
+ levels: ['A', 'AA', 'AAA'],
144
+ principles: ['Perceivable', 'Operable', 'Understandable', 'Robust']
226
145
  };
227
- try {
228
- // Provide the page content, including the data-xpath attributes.
229
- qualWebOptions.html = await page.content();
230
- // Specify which rules to test for, adding a custom execute property for report processing.
231
- const actSpec = rules ? rules.find(typeRules => typeRules.startsWith('act:')) : null;
232
- const wcagSpec = rules ? rules.find(typeRules => typeRules.startsWith('wcag:')) : null;
233
- const bestSpec = rules ? rules.find(typeRules => typeRules.startsWith('best:')) : null;
234
- if (actSpec) {
235
- if (actSpec === 'act:') {
236
- qualWebOptions.execute.act = false;
237
- }
238
- else {
239
- const actRules = actSpec.slice(4).split(',').map(num => `QW-ACT-R${num}`);
240
- qualWebOptions['act-rules'] = {rules: actRules};
241
- qualWebOptions.modules.push(actRulesModule);
242
- qualWebOptions.execute.act = true;
243
- }
146
+ const sectionSpec = (prefix: string, codePrefix: string, all: QwSectionOptions): QwSectionOptions | null => {
147
+ const spec = rules?.find(typeRules => typeRules.startsWith(prefix));
148
+ if (spec === undefined) {
149
+ return all;
244
150
  }
245
- else {
246
- qualWebOptions['act-rules'] = {
247
- levels: ['A', 'AA', 'AAA'],
248
- principles: ['Perceivable', 'Operable', 'Understandable', 'Robust']
249
- };
250
- qualWebOptions.modules.push(actRulesModule);
251
- qualWebOptions.execute.act = true;
151
+ if (spec === prefix) {
152
+ return null;
252
153
  }
253
- if (wcagSpec) {
254
- if (wcagSpec === 'wcag:') {
255
- qualWebOptions.execute.wcag = false;
256
- }
257
- else {
258
- const wcagTechniques = wcagSpec.slice(5).split(',').map(num => `QW-WCAG-T${num}`);
259
- qualWebOptions['wcag-techniques'] = {techniques: wcagTechniques};
260
- qualWebOptions.modules.push(wcagModule);
261
- qualWebOptions.execute.wcag = true;
262
- }
154
+ return {include: spec.slice(prefix.length).split(',').map(num => `${codePrefix}${num}`)};
155
+ };
156
+ const sections: Partial<Record<QwSection, QwSectionOptions>> = {};
157
+ for (const [section, options] of [
158
+ ['act-rules', sectionSpec('act:', 'QW-ACT-R', allAAA)],
159
+ ['wcag-techniques', sectionSpec('wcag:', 'QW-WCAG-T', allAAA)],
160
+ ['best-practices', sectionSpec('best:', 'QW-BP', {})]
161
+ ] as [QwSection, QwSectionOptions | null][]) {
162
+ if (options) {
163
+ sections[section] = options;
263
164
  }
264
- else {
265
- qualWebOptions['wcag-techniques'] = {
266
- levels: ['A', 'AA', 'AAA'],
267
- principles: ['Perceivable', 'Operable', 'Understandable', 'Robust']
268
- };
269
- qualWebOptions.modules.push(wcagModule);
270
- qualWebOptions.execute.wcag = true;
165
+ }
166
+ const deadline = Date.now() + timeLimit * 1000;
167
+ try {
168
+ // Load QualWeb into the page.
169
+ pageBundles ??= ['@qualweb/qw-page', '@qualweb/util', '@qualweb/locale'].map(readBundle);
170
+ for (const source of pageBundles) {
171
+ await page.evaluate(source);
271
172
  }
272
- if (bestSpec) {
273
- if (bestSpec === 'best:') {
274
- qualWebOptions.execute.bp = false;
173
+ // The page's own source, which QualWeb's meta-element rules read.
174
+ const sourceHtml = await page.content();
175
+ const modules: Record<string, QwModuleReport> = {};
176
+ const rulePreventions: Record<string, string> = {};
177
+ // For each section to be tested:
178
+ for (const [section, options] of Object.entries(sections) as [QwSection, QwSectionOptions][]) {
179
+ if (Date.now() > deadline) {
180
+ throw new Error(`time limit of ${timeLimit}s reached before ${section}`);
275
181
  }
276
- else {
277
- const bestPractices = bestSpec.slice(5).split(',').map(num => `QW-BP${num}`);
278
- qualWebOptions['best-practices'] = {bestPractices};
279
- qualWebOptions.modules.push(bpModule);
280
- qualWebOptions.execute.bp = true;
182
+ sectionBundles[section] ??= readBundle(sectionPackages[section]);
183
+ await page.evaluate(sectionBundles[section]!);
184
+ /*
185
+ One rule that throws takes its whole section down with it (e.g. QW-ACT-R76 in 0.8.5 on
186
+ lab(), oklab(), color() or color-mix() colours, qualweb/qualweb#352), so a rule named in
187
+ the stack trace of a crash is excluded and the section rerun; a crash that names no rule
188
+ prevents only its section.
189
+ */
190
+ const outcome = await page.evaluate(({section, options, sourceHtml, maxExcluded}) => {
191
+ // Only this section's bundle has been loaded, so name only its runner.
192
+ const Runner = section === 'act-rules'
193
+ // @ts-expect-error: defined by the section bundle evaluated above.
194
+ ? ACTRulesRunner
195
+ : section === 'wcag-techniques'
196
+ // @ts-expect-error: defined by the section bundle evaluated above.
197
+ ? WCAGTechniquesRunner
198
+ // @ts-expect-error: defined by the section bundle evaluated above.
199
+ : BestPracticesRunner;
200
+ const preventions: Record<string, string> = {};
201
+ const exclude: string[] = [];
202
+ for (;;) {
203
+ try {
204
+ const opts = exclude.length ? {...options, exclude: [...(options.exclude ?? []), ...exclude]} : options;
205
+ const runner = new Runner(opts, 'en').configure(opts).test({sourceHtml, newTabWasOpen: false});
206
+ (window as any).__testaroQwRunners = {...(window as any).__testaroQwRunners, [section]: runner};
207
+ // QualWeb core keeps the act-rules runner here for its special-case pass.
208
+ if (section === 'act-rules') {
209
+ (window as any).act = runner;
210
+ }
211
+ return {ok: true, preventions};
212
+ }
213
+ catch(error) {
214
+ const message = String((error as Error)?.message ?? error).slice(0, 200);
215
+ // Rule classes are named QW_ACT_R76, QW_WCAG_T23, QW_BP1.
216
+ const rule = String((error as Error)?.stack ?? '').match(/QW_(?:ACT_R|WCAG_T|BP)\d+/)?.[0].replace(/_/g, '-');
217
+ if (! rule || exclude.includes(rule) || exclude.length >= maxExcluded) {
218
+ preventions[section] = `section failed: ${message}`;
219
+ return {ok: false, preventions};
220
+ }
221
+ preventions[rule] = message;
222
+ exclude.push(rule);
223
+ }
224
+ }
225
+ }, {section, options, sourceHtml, maxExcluded: MAX_EXCLUDED});
226
+ Object.assign(rulePreventions, outcome.preventions);
227
+ if (! outcome.ok) {
228
+ continue;
281
229
  }
230
+ /*
231
+ QualWeb core's special case for act-rules: QW-ACT-R40 (zoomed text not clipped) is
232
+ re-tested at a 640 × 512 viewport, then the viewport is restored.
233
+ */
234
+ const include = options.include;
235
+ if (section === 'act-rules' && (! include || include.includes('QW-ACT-R40'))) {
236
+ const viewport = page.viewportSize();
237
+ if (viewport) {
238
+ try {
239
+ await page.setViewportSize({width: 640, height: 512});
240
+ await page.evaluate(() => (window as any).act?.testSpecial?.());
241
+ }
242
+ catch(error) {
243
+ rulePreventions['QW-ACT-R40'] = `special-case pass failed: ${String((error as Error).message).slice(0, 200)}`;
244
+ }
245
+ finally {
246
+ await page.setViewportSize(viewport);
247
+ }
248
+ }
249
+ }
250
+ modules[section] = await page.evaluate(
251
+ section => JSON.parse(JSON.stringify((window as any).__testaroQwRunners[section].getReport())),
252
+ section
253
+ );
282
254
  }
283
- else {
284
- qualWebOptions['best-practices'] = {};
285
- qualWebOptions.modules.push(bpModule);
286
- qualWebOptions.execute.bp = true;
287
- }
288
- let qwReport;
289
- try {
290
- // Get the report.
291
- qwReport = await qualWeb.evaluate(qualWebOptions);
292
- }
293
- catch(error) {
294
- return {
295
- data: {
296
- prevented: true,
297
- error: `qualWeb evaluation failed (${(error as Error).message})`
298
- },
299
- result
300
- };
255
+ if (Object.keys(rulePreventions).length) {
256
+ data.rulePreventions = rulePreventions;
301
257
  }
302
- // Add the report to the result.
303
- result.nativeResult = qwReport.customHtml;
304
- const {nativeResult, standardResult} = result;
305
- // If the report contains, as it should, a copy of the DOM:
306
- if (nativeResult?.system?.page?.dom) {
307
- // Delete the copy for parsimony.
308
- delete nativeResult.system.page.dom;
309
- const {modules} = nativeResult;
310
- // If the report contains, as it should, a modules property:
311
- if (modules) {
312
- // For each test section in it:
313
- for (const section of ['act-rules', 'wcag-techniques', 'best-practices'] as QwSection[]) {
314
- // If testing in the section was specified:
315
- if (qualWebOptions[section]) {
316
- // If the section exists:
317
- if (modules[section]) {
318
- const {assertions} = modules[section];
319
- // If it contains assertions (test results):
320
- if (assertions) {
321
- const ruleIDs = Object.keys(assertions);
322
- // For each rule:
323
- for (const ruleID of ruleIDs) {
324
- const ruleAssertions = assertions[ruleID];
325
- const {metadata} = ruleAssertions;
326
- // If there were any warnings or failures:
327
- if (metadata?.warning || metadata?.failed) {
328
- // Delete nonviolations from the results.
329
- ruleAssertions.results = ruleAssertions.results.filter(
330
- raResult => raResult.verdict !== 'passed'
331
- );
332
- // For each test result:
333
- for (const raResult of ruleAssertions.results) {
334
- const {elements, verdict} = raResult;
335
- // If any violations are reported:
336
- if (elements?.length) {
337
- // For each violating element:
338
- for (const element of elements) {
339
- // Limit the size of its reported excerpt.
340
- if ((element.htmlCode?.length as number) > 2000) {
341
- element.htmlCode = `${element.htmlCode!.slice(0, 2000)} …`;
342
- }
343
- // If standard results are to be reported:
344
- if (standard) {
345
- const what = `[${verdict}] ${raResult.description}`;
346
- const xPath = getAttributeXPath(element.htmlCode);
347
- // Add an instance to the standard result.
348
- addInstance(standardResult, {
349
- ruleID,
350
- what,
351
- ordinalSeverity: ordinalSeverities[section][verdict],
352
- outcome: verdict === 'warning' ? 'cantTell' : 'failed',
353
- catalogIndex: getXPathCatalogIndex(report, xPath)
354
- });
355
- }
356
- };
357
- }
358
- };
258
+ // Add the module reports to the result.
259
+ result.nativeResult = {system: {url: page.url(), evaluation: 'in-page'}, modules};
260
+ const {standardResult} = result;
261
+ // For each section that was to be tested:
262
+ for (const section of Object.keys(sections) as QwSection[]) {
263
+ const moduleReport = modules[section];
264
+ /*
265
+ A section that failed as a whole is recorded in rulePreventions; the act is prevented
266
+ only if no section ran.
267
+ */
268
+ if (! moduleReport) {
269
+ continue;
270
+ }
271
+ const {assertions} = moduleReport;
272
+ // If it contains assertions (test results):
273
+ if (assertions) {
274
+ const ruleIDs = Object.keys(assertions);
275
+ // For each rule:
276
+ for (const ruleID of ruleIDs) {
277
+ const ruleAssertions = assertions[ruleID];
278
+ const {metadata} = ruleAssertions;
279
+ // If there were any warnings or failures:
280
+ if (metadata?.warning || metadata?.failed) {
281
+ // Delete nonviolations from the results.
282
+ ruleAssertions.results = ruleAssertions.results.filter(
283
+ raResult => raResult.verdict !== 'passed'
284
+ );
285
+ // For each test result:
286
+ for (const raResult of ruleAssertions.results) {
287
+ const {elements, verdict} = raResult;
288
+ // If any violations are reported:
289
+ if (elements?.length) {
290
+ // For each violating element:
291
+ for (const element of elements) {
292
+ // Limit the size of its reported excerpt.
293
+ if ((element.htmlCode?.length as number) > 2000) {
294
+ element.htmlCode = `${element.htmlCode!.slice(0, 2000)} …`;
359
295
  }
360
- // Otherwise, i.e. if there were no warnings or failures:
361
- else {
362
- // Delete the rule.
363
- delete assertions[ruleID];
296
+ // If standard results are to be reported:
297
+ if (standard) {
298
+ const what = `[${verdict}] ${raResult.description}`;
299
+ const xPath = getAttributeXPath(element.htmlCode);
300
+ // Add an instance to the standard result.
301
+ addInstance(standardResult, {
302
+ ruleID,
303
+ what,
304
+ ordinalSeverity: ordinalSeverities[section][verdict],
305
+ outcome: verdict === 'warning' ? 'cantTell' : 'failed',
306
+ catalogIndex: getXPathCatalogIndex(report, xPath)
307
+ });
364
308
  }
365
309
  };
366
310
  }
367
- // Otherwise, i.e. if it contains no assertions:
368
- else {
369
- // Report this.
370
- data.prevented = true;
371
- data.error = 'No assertions';
372
- }
373
- }
374
- // Otherwise, i.e. if the section is missing:
375
- else {
376
- // Report this.
377
- data.prevented = true;
378
- data.error = `No ${section} section`;
379
- }
311
+ };
380
312
  }
381
- }
382
- }
383
- // Otherwise, i.e. if the report does not contain a modules property:
384
- else {
385
- // Report this.
386
- data.prevented = true;
387
- data.error = 'No modules';
313
+ // Otherwise, i.e. if there were no warnings or failures:
314
+ else {
315
+ // Delete the rule.
316
+ delete assertions[ruleID];
317
+ }
318
+ };
388
319
  }
389
320
  }
390
- // Otherwise, i.e. if the report does not contain a copy of the DOM:
391
- else {
321
+ // If no section ran:
322
+ if (! Object.keys(modules).length) {
392
323
  // Report this.
393
324
  data.prevented = true;
394
- data.error = 'No DOM';
325
+ data.error = `No qualWeb section ran (${JSON.stringify(rulePreventions).slice(0, 300)})`;
395
326
  }
396
- // Stop the QualWeb core engine.
397
- await qualWeb.stop();
398
327
  // Test whether the result is an object.
399
328
  try {
400
329
  JSON.stringify(result);