AX: Audit tab should have built-in accessibility tests.
[WebKit-https.git] / Source / WebInspectorUI / UserInterface / Controllers / AuditManager.js
1 /*
2  * Copyright (C) 2018 Apple Inc. All rights reserved.
3  *
4  * Redistribution and use in source and binary forms, with or without
5  * modification, are permitted provided that the following conditions
6  * are met:
7  * 1. Redistributions of source code must retain the above copyright
8  *    notice, this list of conditions and the following disclaimer.
9  * 2. Redistributions in binary form must reproduce the above copyright
10  *    notice, this list of conditions and the following disclaimer in the
11  *    documentation and/or other materials provided with the distribution.
12  *
13  * THIS SOFTWARE IS PROVIDED BY APPLE INC. AND ITS CONTRIBUTORS ``AS IS''
14  * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO,
15  * THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR
16  * PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL APPLE INC. OR ITS CONTRIBUTORS
17  * BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR
18  * CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF
19  * SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
20  * INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN
21  * CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
22  * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF
23  * THE POSSIBILITY OF SUCH DAMAGE.
24  */
25
26 WI.AuditManager = class AuditManager extends WI.Object
27 {
28     constructor()
29     {
30         super();
31
32         this._tests = [];
33         this._results = [];
34
35         this._runningState = WI.AuditManager.RunningState.Inactive;
36         this._runningTests = [];
37
38         this._disabledDefaultTestsSetting = new WI.Setting("audit-disabled-default-tests", []);
39
40         WI.Frame.addEventListener(WI.Frame.Event.MainResourceDidChange, this._handleFrameMainResourceDidChange, this);
41     }
42
43     // Static
44
45     static synthesizeWarning(message)
46     {
47         message = WI.UIString("Audit Warning: %s").format(message);
48
49         if (window.InspectorTest) {
50             console.warn(message);
51             return;
52         }
53
54         let consoleMessage = new WI.ConsoleMessage(WI.mainTarget, WI.ConsoleMessage.MessageSource.Other, WI.ConsoleMessage.MessageLevel.Warning, message);
55         consoleMessage.shouldRevealConsole = true;
56
57         WI.consoleLogViewController.appendConsoleMessage(consoleMessage);
58     }
59
60     static synthesizeError(message)
61     {
62         message = WI.UIString("Audit Error: %s").format(message);
63
64         if (window.InspectorTest) {
65             console.error(message);
66             return;
67         }
68
69         let consoleMessage = new WI.ConsoleMessage(WI.mainTarget, WI.ConsoleMessage.MessageSource.Other, WI.ConsoleMessage.MessageLevel.Error, message);
70         consoleMessage.shouldRevealConsole = true;
71
72         WI.consoleLogViewController.appendConsoleMessage(consoleMessage);
73     }
74
75     // Public
76
77     get tests() { return this._tests; }
78     get results() { return this._results; }
79     get runningState() { return this._runningState; }
80
81     get editing()
82     {
83         return this._runningState === WI.AuditManager.RunningState.Disabled;
84     }
85
86     set editing(editing)
87     {
88         console.assert(this._runningState === WI.AuditManager.RunningState.Disabled || this._runningState === WI.AuditManager.RunningState.Inactive);
89         if (this._runningState !== WI.AuditManager.RunningState.Disabled && this._runningState !== WI.AuditManager.RunningState.Inactive)
90             return;
91
92         let runningState = editing ? WI.AuditManager.RunningState.Disabled : WI.AuditManager.RunningState.Inactive;
93         console.assert(runningState !== this._runningState);
94         if (runningState === this._runningState)
95             return;
96
97         this._runningState = runningState;
98
99         this.dispatchEventToListeners(WI.AuditManager.Event.EditingChanged);
100
101         if (!this.editing) {
102             WI.objectStores.audits.clear();
103
104             let disabledDefaultTests = [];
105             let saveDisabledDefaultTest = (test) => {
106                 if (test.disabled)
107                     disabledDefaultTests.push(test.name);
108
109                 if (test instanceof WI.AuditTestGroup) {
110                     for (let child of test.tests)
111                         saveDisabledDefaultTest(child);
112                 }
113             };
114
115             for (let test of this._tests) {
116                 if (test.__default)
117                     saveDisabledDefaultTest(test);
118                 else
119                     WI.objectStores.audits.addObject(test);
120             }
121
122             this._disabledDefaultTestsSetting.value = disabledDefaultTests;
123         }
124     }
125
126     async start(tests)
127     {
128         console.assert(this._runningState === WI.AuditManager.RunningState.Inactive);
129         if (this._runningState !== WI.AuditManager.RunningState.Inactive)
130             return;
131
132         if (tests && tests.length)
133             tests = tests.filter((test) => typeof test === "object" && test instanceof WI.AuditTestBase);
134         else
135             tests = this._tests;
136
137         console.assert(tests.length);
138         if (!tests.length)
139             return;
140
141         let mainResource = WI.networkManager.mainFrame.mainResource;
142
143         this._runningState = WI.AuditManager.RunningState.Active;
144         this._runningTests = tests;
145         for (let test of this._runningTests)
146             test.clearResult();
147
148         this.dispatchEventToListeners(WI.AuditManager.Event.TestScheduled);
149
150         await Promise.chain(this._runningTests.map((test) => async () => {
151             if (this._runningState !== WI.AuditManager.RunningState.Active)
152                 return;
153
154             if (InspectorBackend.domains.Audit)
155                 await AuditAgent.setup();
156
157             await test.start();
158
159             if (InspectorBackend.domains.Audit)
160                 await AuditAgent.teardown();
161         }));
162
163         let result = this._runningTests.map((test) => test.result).filter((result) => !!result);
164
165         this._runningState = WI.AuditManager.RunningState.Inactive;
166         this._runningTests = [];
167
168         this._addResult(result);
169
170         if (mainResource !== WI.networkManager.mainFrame.mainResource) {
171             // Navigated while tests were running.
172             for (let test of this._tests)
173                 test.clearResult();
174         }
175     }
176
177     stop()
178     {
179         console.assert(this._runningState === WI.AuditManager.RunningState.Active);
180         if (this._runningState !== WI.AuditManager.RunningState.Active)
181             return;
182
183         this._runningState = WI.AuditManager.RunningState.Stopping;
184
185         for (let test of this._runningTests)
186             test.stop();
187     }
188
189     async processJSON({json, error})
190     {
191         if (error) {
192             WI.AuditManager.synthesizeError(error);
193             return;
194         }
195
196         if (typeof json !== "object" || json === null) {
197             WI.AuditManager.synthesizeError(WI.UIString("invalid JSON"));
198             return;
199         }
200
201         if (json.type !== WI.AuditTestCase.TypeIdentifier && json.type !== WI.AuditTestGroup.TypeIdentifier
202             && json.type !== WI.AuditTestCaseResult.TypeIdentifier && json.type !== WI.AuditTestGroupResult.TypeIdentifier) {
203             WI.AuditManager.synthesizeError(WI.UIString("unknown %s \u0022%s\u0022").format(WI.unlocalizedString("type"), json.type));
204             return;
205         }
206
207         let object = await WI.AuditTestGroup.fromPayload(json) || await WI.AuditTestCase.fromPayload(json) || await WI.AuditTestGroupResult.fromPayload(json) || await WI.AuditTestCaseResult.fromPayload(json);
208         if (!object)
209             return;
210
211         if (object instanceof WI.AuditTestBase) {
212             this._addTest(object);
213             WI.objectStores.audits.addObject(object);
214         } else if (object instanceof WI.AuditTestResultBase)
215             this._addResult(object);
216
217         WI.showRepresentedObject(object);
218     }
219
220     export(object)
221     {
222         console.assert(object instanceof WI.AuditTestCase || object instanceof WI.AuditTestGroup || object instanceof WI.AuditTestCaseResult || object instanceof WI.AuditTestGroupResult, object);
223
224         let filename = object.name;
225         if (object instanceof WI.AuditTestResultBase)
226             filename = WI.UIString("%s Result").format(filename);
227
228         let url = "web-inspector:///" + encodeURI(filename) + ".json";
229
230         WI.FileUtilities.save({
231             url,
232             content: JSON.stringify(object),
233             forceSaveAs: true,
234         });
235     }
236
237     loadStoredTests()
238     {
239         if (this._tests.length)
240             return;
241
242         WI.objectStores.audits.getAll().then(async (tests) => {
243             for (let payload of tests) {
244                 let test = await WI.AuditTestGroup.fromPayload(payload) || await WI.AuditTestCase.fromPayload(payload);
245                 if (!test)
246                     continue;
247
248                 const key = null;
249                 WI.objectStores.audits.associateObject(test, key, payload);
250
251                 this._addTest(test);
252             }
253
254             this.addDefaultTestsIfNeeded();
255         });
256     }
257
258     removeTest(test)
259     {
260         this._tests.remove(test);
261
262         this.dispatchEventToListeners(WI.AuditManager.Event.TestRemoved, {test});
263
264         if (!test.__default)
265             WI.objectStores.audits.deleteObject(test);
266     }
267
268     // Private
269
270     _addTest(test)
271     {
272         this._tests.push(test);
273
274         this.dispatchEventToListeners(WI.AuditManager.Event.TestAdded, {test});
275     }
276
277     _addResult(result)
278     {
279         if (!result || (Array.isArray(result) && !result.length))
280             return;
281
282         this._results.push(result);
283
284         this.dispatchEventToListeners(WI.AuditManager.Event.TestCompleted, {
285             result,
286             index: this._results.length - 1,
287         });
288     }
289
290     _handleFrameMainResourceDidChange(event)
291     {
292         if (!event.target.isMainFrame())
293             return;
294
295         if (this._runningState === WI.AuditManager.RunningState.Active)
296             this.stop();
297         else {
298             for (let test of this._tests)
299                 test.clearResult();
300         }
301     }
302
303     addDefaultTestsIfNeeded()
304     {
305         if (this._tests.length)
306             return;
307
308         const testMenuRoleForRequiredChidren = function() {
309             const relationships = {
310                 menu: ["menuitem", "menuitemcheckbox", "menuitemradio"],
311                 menubar: ["menuitem", "menuitemcheckbox", "menuitemradio"],
312             };
313             let domNodes = [];
314             let visitedParents = new Set;
315             function hasChildWithRole(node, expectedRoles) {
316                 let childNode = node;
317                 if (!childNode)
318                     return false;
319
320                 if (childNode.parentNode)
321                     visitedParents.add(childNode.parentNode);
322
323                 while (childNode) {
324                     let properties = WebInspectorAudit.Accessibility.getComputedProperties(childNode);
325                     if (childNode.nodeType === Node.ELEMENT_NODE && properties) {
326                         if (expectedRoles.includes(properties.role))
327                             return true;
328
329                         if (childNode.hasChildNodes() && hasChildWithRole(childNode.firstChild, expectedRoles))
330                             return true;
331                     }
332                     childNode = childNode.nextSibling;
333                 }
334                 return false;
335             }
336             for (let role in relationships) {
337                 for (let parentNode of WebInspectorAudit.Accessibility.getElementsByComputedRole(role)) {
338                     if (visitedParents.has(parentNode))
339                         continue;
340
341                     if (!hasChildWithRole(parentNode.firstChild, relationships[role]))
342                         domNodes.push(parentNode);
343                 }
344             }
345             return {level: domNodes.length ? "fail" : "pass", domNodes, domAttributes: ["role"]};
346         };
347
348         const testGridRoleForRequiredChidren = function() {
349             const relationships = {
350                 grid: ["row", "rowgroup"],
351             };
352             let domNodes = [];
353             let visitedParents = new Set;
354             function hasChildWithRole(node, expectedRoles) {
355                 let childNode = node;
356                 if (!childNode)
357                     return false;
358
359                 if (childNode.parentNode)
360                     visitedParents.add(childNode.parentNode);
361
362                 while (childNode) {
363                     let properties = WebInspectorAudit.Accessibility.getComputedProperties(childNode);
364                     if (childNode.nodeType === Node.ELEMENT_NODE && properties) {
365                         if (expectedRoles.includes(properties.role))
366                             return true;
367
368                         if (childNode.hasChildNodes() && hasChildWithRole(childNode.firstChild, expectedRoles))
369                             return true;
370                     }
371                     childNode = childNode.nextSibling;
372                 }
373                 return false;
374             }
375             for (let role in relationships) {
376                 for (let parentNode of WebInspectorAudit.Accessibility.getElementsByComputedRole(role)) {
377                     if (visitedParents.has(parentNode))
378                         continue;
379
380                     if (!hasChildWithRole(parentNode.firstChild, relationships[role]))
381                         domNodes.push(parentNode);
382                 }
383             }
384             return {level: domNodes.length ? "fail" : "pass", domNodes, domAttributes: ["role"]};
385         };
386
387         const testForAriaLabelledBySpelling = function() {
388             let domNodes = Array.from(document.querySelectorAll("[aria-labeledby]"));
389             return {level: domNodes.length ? "fail" : "pass", domNodes, domAttributes: ["aria-labeledby"]};
390         };
391
392         const testForMultipleBanners = function() {
393             let domNodes = [];
394             let banners = WebInspectorAudit.Accessibility.getElementsByComputedRole("banner");
395             if (banners.length > 1)
396                 domNodes = banners;
397             return {level: domNodes.length ? "fail" : "pass", domNodes, domAttributes: ["role"]};
398         };
399
400         const testForLinkLabels = function() {
401             let links = WebInspectorAudit.Accessibility.getElementsByComputedRole("link");
402             let domNodes = links.filter((link) => !WebInspectorAudit.Accessibility.getComputedProperties(link).label);
403             return {level: domNodes.length ? "fail" : "pass", domNodes, domAttributes: ["aria-label", "aria-labelledby", "title"]};
404         };
405
406         const testRowGroupRoleForRequiredChidren = function() {
407             const relationships = {
408                 rowgroup: ["row"],
409             };
410             let domNodes = [];
411             let visitedParents = new Set;
412             function hasChildWithRole(node, expectedRoles) {
413                 let childNode = node;
414                 if (!childNode)
415                     return false;
416
417                 if (childNode.parentNode)
418                     visitedParents.add(childNode.parentNode);
419
420                 while (childNode) {
421                     let properties = WebInspectorAudit.Accessibility.getComputedProperties(childNode);
422                     if (childNode.nodeType === Node.ELEMENT_NODE && properties) {
423                         if (expectedRoles.includes(properties.role))
424                             return true;
425
426                         if (childNode.hasChildNodes() && hasChildWithRole(childNode.firstChild, expectedRoles))
427                             return true;
428                     }
429                     childNode = childNode.nextSibling;
430                 }
431                 return false;
432             }
433             for (let role in relationships) {
434                 for (let parentNode of WebInspectorAudit.Accessibility.getElementsByComputedRole(role)) {
435                     if (visitedParents.has(parentNode))
436                         continue;
437
438                     if (!hasChildWithRole(parentNode.firstChild, relationships[role]))
439                         domNodes.push(parentNode);
440                 }
441             }
442             return {level: domNodes.length ? "fail" : "pass", domNodes, domAttributes: ["role"]};
443         };
444
445         const testTableRoleForRequiredChidren = function() {
446             const relationships = {
447                 table: ["row", "rowgroup"],
448             };
449             let domNodes = [];
450             let visitedParents = new Set;
451             function hasChildWithRole(node, expectedRoles) {
452                 let childNode = node;
453                 if (!childNode)
454                     return false;
455
456                 if (childNode.parentNode)
457                     visitedParents.add(childNode.parentNode);
458
459                 while (childNode) {
460                     let properties = WebInspectorAudit.Accessibility.getComputedProperties(childNode);
461                     if (childNode.nodeType === Node.ELEMENT_NODE && properties) {
462                         if (expectedRoles.includes(properties.role))
463                             return true;
464
465                         if (childNode.hasChildNodes() && hasChildWithRole(childNode.firstChild, expectedRoles))
466                             return true;
467                     }
468                     childNode = childNode.nextSibling;
469                 }
470                 return false;
471             }
472             for (let role in relationships) {
473                 for (let parentNode of WebInspectorAudit.Accessibility.getElementsByComputedRole(role)) {
474                     if (visitedParents.has(parentNode))
475                         continue;
476
477                     if (!hasChildWithRole(parentNode.firstChild, relationships[role]))
478                         domNodes.push(parentNode);
479                 }
480             }
481             return {level: domNodes.length ? "fail" : "pass", domNodes, domAttributes: ["role"]};
482         };
483
484         const testForMultipleLiveRegions = function() {
485             const liveRegionRoles = ["alert", "log", "status", "marquee", "timer"];
486             let domNodes = [];
487             let liveRegions = liveRegionRoles.reduce((a, b) => {
488                 return a.concat(WebInspectorAudit.Accessibility.getElementsByComputedRole(b));
489             }, []);
490             liveRegions = liveRegions.concat(Array.from(document.querySelectorAll(`[aria-live="polite"], [aria-live="assertive"]`)));
491             if (liveRegions.length > 1)
492                 domNodes = liveRegions;
493             return {level: domNodes.length ? "warn" : "pass", domNodes, domAttributes: ["aria-live"]};
494         };
495
496         const testListBoxRoleForRequiredChidren = function() {
497             const relationships = {
498                 listbox: ["option"],
499             };
500             let domNodes = [];
501             let visitedParents = new Set;
502             function hasChildWithRole(node, expectedRoles) {
503                 let childNode = node;
504                 if (!childNode)
505                     return false;
506
507                 if (childNode.parentNode)
508                     visitedParents.add(childNode.parentNode);
509
510                 while (childNode) {
511                     let properties = WebInspectorAudit.Accessibility.getComputedProperties(childNode);
512                     if (childNode.nodeType === Node.ELEMENT_NODE && properties) {
513                         if (expectedRoles.includes(properties.role))
514                             return true;
515
516                         if (childNode.hasChildNodes() && hasChildWithRole(childNode.firstChild, expectedRoles))
517                             return true;
518                     }
519                     childNode = childNode.nextSibling;
520                 }
521                 return false;
522             }
523             for (let role in relationships) {
524                 for (let parentNode of WebInspectorAudit.Accessibility.getElementsByComputedRole(role)) {
525                     if (visitedParents.has(parentNode))
526                         continue;
527
528                     if (!hasChildWithRole(parentNode.firstChild, relationships[role]))
529                         domNodes.push(parentNode);
530                 }
531             }
532             return {level: domNodes.length ? "fail" : "pass", domNodes, domAttributes: ["role"]};
533         };
534
535         const testImageLabels = function() {
536             let images = WebInspectorAudit.Accessibility.getElementsByComputedRole("img");
537             let domNodes = images.filter((image) => !WebInspectorAudit.Accessibility.getComputedProperties(image).label);
538             return {level: domNodes.length ? "fail" : "pass", domNodes, domAttributes: ["aria-label", "aria-labelledby", "title", "alt"]};
539         };
540
541         const testForAriaHiddenFalse = function() {
542             let domNodes = Array.from(document.querySelectorAll(`[aria-hidden="false"]`));
543             return {level: domNodes.length ? "warn" : "pass", domNodes, domAttributes: ["aria-hidden"]};
544         };
545
546         const testTreeRoleForRequiredChidren = function() {
547             const relationships = {
548                 tree: ["treeitem", "group"],
549             };
550             let domNodes = [];
551             let visitedParents = new Set;
552             function hasChildWithRole(node, expectedRoles) {
553                 let childNode = node;
554                 if (!childNode)
555                     return false;
556
557                 if (childNode.parentNode)
558                     visitedParents.add(childNode.parentNode);
559
560                 while (childNode) {
561                     let properties = WebInspectorAudit.Accessibility.getComputedProperties(childNode);
562                     if (childNode.nodeType === Node.ELEMENT_NODE && properties) {
563                         if (expectedRoles.includes(properties.role))
564                             return true;
565
566                         if (childNode.hasChildNodes() && hasChildWithRole(childNode.firstChild, expectedRoles))
567                             return true;
568                     }
569                     childNode = childNode.nextSibling;
570                 }
571                 return false;
572             }
573             for (let role in relationships) {
574                 for (let parentNode of WebInspectorAudit.Accessibility.getElementsByComputedRole(role)) {
575                     if (visitedParents.has(parentNode))
576                         continue;
577
578                     if (!hasChildWithRole(parentNode.firstChild, relationships[role]))
579                         domNodes.push(parentNode);
580                 }
581             }
582             return {level: domNodes.length ? "fail" : "pass", domNodes, domAttributes: ["role"]};
583         };
584
585         const testRadioGroupRoleForRequiredChidren = function() {
586             const relationships = {
587                 radiogroup: ["radio"],
588             };
589             let domNodes = [];
590             let visitedParents = new Set;
591             function hasChildWithRole(node, expectedRoles) {
592                 let childNode = node;
593                 if (!childNode)
594                     return false;
595
596                 if (childNode.parentNode)
597                     visitedParents.add(childNode.parentNode);
598
599                 while (childNode) {
600                     let properties = WebInspectorAudit.Accessibility.getComputedProperties(childNode);
601                     if (childNode.nodeType === Node.ELEMENT_NODE && properties) {
602                         if (expectedRoles.includes(properties.role))
603                             return true;
604
605                         if (childNode.hasChildNodes() && hasChildWithRole(childNode.firstChild, expectedRoles))
606                             return true;
607                     }
608                     childNode = childNode.nextSibling;
609                 }
610                 return false;
611             }
612             for (let role in relationships) {
613                 for (let parentNode of WebInspectorAudit.Accessibility.getElementsByComputedRole(role)) {
614                     if (visitedParents.has(parentNode))
615                         continue;
616
617                     if (!hasChildWithRole(parentNode.firstChild, relationships[role]))
618                         domNodes.push(parentNode);
619                 }
620             }
621             return {level: domNodes.length ? "fail" : "pass", domNodes, domAttributes: ["role"]};
622         };
623
624         const testFeedRoleForRequiredChidren = function() {
625             const relationships = {
626                 feed: ["article"],
627             };
628             let domNodes = [];
629             let visitedParents = new Set;
630             function hasChildWithRole(node, expectedRoles) {
631                 let childNode = node;
632                 if (!childNode)
633                     return false;
634
635                 if (childNode.parentNode)
636                     visitedParents.add(childNode.parentNode);
637
638                 while (childNode) {
639                     let properties = WebInspectorAudit.Accessibility.getComputedProperties(childNode);
640                     if (childNode.nodeType === Node.ELEMENT_NODE && properties) {
641                         if (expectedRoles.includes(properties.role))
642                             return true;
643
644                         if (childNode.hasChildNodes() && hasChildWithRole(childNode.firstChild, expectedRoles))
645                             return true;
646                     }
647                     childNode = childNode.nextSibling;
648                 }
649                 return false;
650             }
651             for (let role in relationships) {
652                 for (let parentNode of WebInspectorAudit.Accessibility.getElementsByComputedRole(role)) {
653                     if (visitedParents.has(parentNode))
654                         continue;
655
656                     if (!hasChildWithRole(parentNode.firstChild, relationships[role]))
657                         domNodes.push(parentNode);
658                 }
659             }
660             return {level: domNodes.length ? "fail" : "pass", domNodes, domAttributes: ["role"]};
661         };
662
663         const testTabListRoleForRequiredChidren = function() {
664             const relationships = {
665                 tablist: ["tab"],
666             };
667             let domNodes = [];
668             let visitedParents = new Set;
669             function hasChildWithRole(node, expectedRoles) {
670                 let childNode = node;
671                 if (!childNode)
672                     return false;
673
674                 if (childNode.parentNode)
675                     visitedParents.add(childNode.parentNode);
676
677                 while (childNode) {
678                     let properties = WebInspectorAudit.Accessibility.getComputedProperties(childNode);
679                     if (childNode.nodeType === Node.ELEMENT_NODE && properties) {
680                         if (expectedRoles.includes(properties.role))
681                             return true;
682
683                         if (childNode.hasChildNodes() && hasChildWithRole(childNode.firstChild, expectedRoles))
684                             return true;
685                     }
686                     childNode = childNode.nextSibling;
687                 }
688                 return false;
689             }
690             for (let role in relationships) {
691                 for (let parentNode of WebInspectorAudit.Accessibility.getElementsByComputedRole(role)) {
692                     if (visitedParents.has(parentNode))
693                         continue;
694
695                     if (!hasChildWithRole(parentNode.firstChild, relationships[role]))
696                         domNodes.push(parentNode);
697                 }
698             }
699             return {level: domNodes.length ? "fail" : "pass", domNodes, domAttributes: ["role"]};
700         };
701
702         const testButtonLabels = function() {
703             let buttons = WebInspectorAudit.Accessibility.getElementsByComputedRole("button");
704             let domNodes = buttons.filter((button) => !WebInspectorAudit.Accessibility.getComputedProperties(button).label);
705             return {level: domNodes.length ? "fail" : "pass", domNodes, domAttributes: ["aria-label", "aria-labelledby", "title"]};
706         };
707
708         const testCellRoleForRequiredChidren = function() {
709             const relationships = {
710                 cell: ["row"],
711             };
712             let domNodes = [];
713             let visitedParents = new Set;
714             function hasChildWithRole(node, expectedRoles) {
715                 let childNode = node;
716                 if (!childNode)
717                     return false;
718
719                 if (childNode.parentNode)
720                     visitedParents.add(childNode.parentNode);
721
722                 while (childNode) {
723                     let properties = WebInspectorAudit.Accessibility.getComputedProperties(childNode);
724                     if (childNode.nodeType === Node.ELEMENT_NODE && properties) {
725                         if (expectedRoles.includes(properties.role))
726                             return true;
727
728                         if (childNode.hasChildNodes() && hasChildWithRole(childNode.firstChild, expectedRoles))
729                             return true;
730                     }
731                     childNode = childNode.nextSibling;
732                 }
733                 return false;
734             }
735             for (let role in relationships) {
736                 for (let parentNode of WebInspectorAudit.Accessibility.getElementsByComputedRole(role)) {
737                     if (visitedParents.has(parentNode))
738                         continue;
739
740                     if (!hasChildWithRole(parentNode.firstChild, relationships[role]))
741                         domNodes.push(parentNode);
742                 }
743             }
744             return {level: domNodes.length ? "fail" : "pass", domNodes, domAttributes: ["role"]};
745         };
746
747         const testListRoleForRequiredChidren = function() {
748             const relationships = {
749                 list: ["listitem", "group"],
750             };
751             let domNodes = [];
752             let visitedParents = new Set;
753             function hasChildWithRole(node, expectedRoles) {
754                 let childNode = node;
755                 if (!childNode)
756                     return false;
757
758                 if (childNode.parentNode)
759                     visitedParents.add(childNode.parentNode);
760
761                 while (childNode) {
762                     let properties = WebInspectorAudit.Accessibility.getComputedProperties(childNode);
763                     if (childNode.nodeType === Node.ELEMENT_NODE && properties) {
764                         if (expectedRoles.includes(properties.role))
765                             return true;
766
767                         if (childNode.hasChildNodes() && hasChildWithRole(childNode.firstChild, expectedRoles))
768                             return true;
769                     }
770                     childNode = childNode.nextSibling;
771                 }
772                 return false;
773             }
774             for (let role in relationships) {
775                 for (let parentNode of WebInspectorAudit.Accessibility.getElementsByComputedRole(role)) {
776                     if (visitedParents.has(parentNode))
777                         continue;
778
779                     if (!hasChildWithRole(parentNode.firstChild, relationships[role]))
780                         domNodes.push(parentNode);
781                 }
782             }
783             return {level: domNodes.length ? "warn" : "pass", domNodes, domAttributes: ["role"]};
784         };
785
786         const testComboBoxRoleForRequiredChidren = function() {
787             const relationships = {
788                 combobox: ["textbox", "listbox", "tree", "grid", "dialog"],
789             };
790             let domNodes = [];
791             let visitedParents = new Set;
792             function hasChildWithRole(node, expectedRoles) {
793                 let childNode = node;
794                 if (!childNode)
795                     return false;
796
797                 if (childNode.parentNode)
798                     visitedParents.add(childNode.parentNode);
799
800                 while (childNode) {
801                     let properties = WebInspectorAudit.Accessibility.getComputedProperties(childNode);
802                     if (childNode.nodeType === Node.ELEMENT_NODE && properties) {
803                         if (expectedRoles.includes(properties.role))
804                             return true;
805
806                         if (childNode.hasChildNodes() && hasChildWithRole(childNode.firstChild, expectedRoles))
807                             return true;
808                     }
809                     childNode = childNode.nextSibling;
810                 }
811                 return false;
812             }
813             for (let role in relationships) {
814                 for (let parentNode of WebInspectorAudit.Accessibility.getElementsByComputedRole(role)) {
815                     if (visitedParents.has(parentNode))
816                         continue;
817
818                     if (!hasChildWithRole(parentNode.firstChild, relationships[role]))
819                         domNodes.push(parentNode);
820                 }
821             }
822             return {level: domNodes.length ? "fail" : "pass", domNodes, domAttributes: ["role"]};
823         };
824
825         const testForMultipleMainContentSections = function() {
826             let domNodes = [];
827             let mainContentElements = WebInspectorAudit.Accessibility.getElementsByComputedRole("main");
828             if (mainContentElements.length > 1) {
829                 domNodes = mainContentElements;
830             }
831             return {level: domNodes.length ? "fail" : "pass", domNodes, domAttributes: ["role"]};
832         };
833
834         const testDialogsForLabels = function() {
835           let dialogs = WebInspectorAudit.Accessibility.getElementsByComputedRole("dialog");
836           let domNodes = dialogs.filter((dialog) => !WebInspectorAudit.Accessibility.getComputedProperties(dialog).label);
837           return {level: domNodes.length ? "fail" : "pass", domNodes, domAttributes: ["aria-label", "aria-labelledby", "title"]};
838         };
839
840         const testForInvalidAriaHiddenValue = function() {
841             let domNodes = Array.from(document.querySelectorAll(`[aria-hidden]:not([aria-hidden="true"], [aria-hidden="false"])`));
842             return {level: domNodes.length ? "fail" : "pass", domNodes, domAttributes: ["aria-hidden"]};
843         };
844
845         const defaultTests = [
846             new WI.AuditTestGroup(WI.UIString("Demo Audit"), [
847                 new WI.AuditTestGroup(WI.UIString("Result Levels"), [
848                     new WI.AuditTestCase(`level-pass`, `function() { return {level: "pass"}; }`, {description: WI.UIString("This is what the result of a passing test with no data looks like.")}),
849                     new WI.AuditTestCase(`level-warn`, `function() { return {level: "warn"}; }`, {description: WI.UIString("This is what the result of a warning test with no data looks like.")}),
850                     new WI.AuditTestCase(`level-fail`, `function() { return {level: "fail"}; }`, {description: WI.UIString("This is what the result of a failing test with no data looks like.")}),
851                     new WI.AuditTestCase(`level-error`, `function() { return {level: "error"}; }`, {description: WI.UIString("This is what the result of a test that threw an error with no data looks like.")}),
852                     new WI.AuditTestCase(`level-unsupported`, `function() { return {level: "unsupported"}; }`, {description: WI.UIString("This is what the result of an unsupported test with no data looks like.")}),
853                 ], {description: WI.UIString("These are all of the different test result levels.")}),
854                 new WI.AuditTestGroup(WI.UIString("Result Data"), [
855                     new WI.AuditTestCase(`data-domNodes`, `function() { return {domNodes: [document.body], level: "pass"}; }`, {description: WI.UIString("This is an example of how result DOM nodes are shown. It will pass with the <body> element.")}),
856                     new WI.AuditTestCase(`data-domAttributes`, `function() { return {domNodes: Array.from(document.querySelectorAll("[id]")), domAttributes: ["id"], level: "pass"}; }`, {description: WI.UIString("This is an example of how result DOM nodes are shown. It will pass with all elements with an id attribute.")}),
857                     new WI.AuditTestCase(`data-errors`, `function() { throw Error("this error was thrown from inside the audit test code."); }`, {description: WI.UIString("This is an example of how errors are shown. The error was thrown manually, but execution errors will appear in the same way.")}),
858                 ], {description: WI.UIString("These are all of the different types of data that can be returned with the test result.")}),
859             ], {description: WI.UIString("These tests serve as a demonstration of the functionality and structure of audits.")}),
860             new WI.AuditTestGroup(WI.UIString("Accessibility"), [
861                 new WI.AuditTestCase(`testMenuRoleForRequiredChidren`, testMenuRoleForRequiredChidren.toString(), {description: WI.UIString("Ensure that element of role \u0022%s\u0022 and \u0022%s\u0022 have required owned elements in accordance with WAI-ARIA.").format(WI.unlocalizedString("menu"), WI.unlocalizedString("menubar")), supports: 1}),
862                 new WI.AuditTestCase(`testGridRoleForRequiredChidren`, testGridRoleForRequiredChidren.toString(), {description: WI.UIString("Ensure that elements of role \u0022%s\u0022 have required owned elements in accordance with WAI-ARIA.").format(WI.unlocalizedString("grid")), supports: 1}),
863                 new WI.AuditTestCase(`testForAriaLabelledBySpelling`, testForAriaLabelledBySpelling.toString(), {description: WI.UIString("Ensure that \u0022%s\u0022 is spelled correctly.").format(WI.unlocalizedString("aria-labelledby")), supports: 1}),
864                 new WI.AuditTestCase(`testForMultipleBanners`, testForMultipleBanners.toString(), {description: WI.UIString("Ensure that only one banner is used on the page."), supports: 1}),
865                 new WI.AuditTestCase(`testForLinkLabels`, testForLinkLabels.toString(), {description: WI.UIString("Ensure that links have accessible labels for assistive technology."), supports: 1}),
866                 new WI.AuditTestCase(`testRowGroupRoleForRequiredChidren`, testRowGroupRoleForRequiredChidren.toString(), {description: WI.UIString("Ensure that element of role \u0022%s\u0022 have required owned elements in accordance with WAI-ARIA.").format(WI.unlocalizedString("rowgroup")), supports: 1}),
867                 new WI.AuditTestCase(`testTableRoleForRequiredChidren`, testTableRoleForRequiredChidren.toString(), {description: WI.UIString("Ensure that elements of role \u0022%s\u0022 have required owned elements in accordance with WAI-ARIA.").format(WI.unlocalizedString("table")), supports: 1}),
868                 new WI.AuditTestCase(`testForMultipleLiveRegions`, testForMultipleLiveRegions.toString(), {description: WI.UIString("Ensure that only one live region is used on the page."), supports: 1}),
869                 new WI.AuditTestCase(`testListBoxRoleForRequiredChidren`, testListBoxRoleForRequiredChidren.toString(), {description: WI.UIString("Ensure that elements of role \u0022%s\u0022 have required owned elements in accordance with WAI-ARIA.").format(WI.unlocalizedString("listbox")), supports: 1}),
870                 new WI.AuditTestCase(`testImageLabels`, testImageLabels.toString(), {description: WI.UIString("Ensure that elements of role \u0022%s\u0022 have accessible labels for assistive technology.").format(WI.unlocalizedString("img")), supports: 1}),
871                 new WI.AuditTestCase(`testForAriaHiddenFalse`, testForAriaHiddenFalse.toString(), {description: WI.UIString("Ensure aria-hidden=\u0022%s\u0022 is not used.").format(WI.unlocalizedString("false")), supports: 1}),
872                 new WI.AuditTestCase(`testTreeRoleForRequiredChidren`, testTreeRoleForRequiredChidren.toString(), {description: WI.UIString("Ensure that element of role \u0022%s\u0022 have required owned elements in accordance with WAI-ARIA.").format(WI.unlocalizedString("tree")), supports: 1}),
873                 new WI.AuditTestCase(`testRadioGroupRoleForRequiredChidren`, testRadioGroupRoleForRequiredChidren.toString(), {description: WI.UIString("Ensure that element of role \u0022%s\u0022 have required owned elements in accordance with WAI-ARIA.").format(WI.unlocalizedString("radiogroup")), supports: 1}),
874                 new WI.AuditTestCase(`testFeedRoleForRequiredChidren`, testFeedRoleForRequiredChidren.toString(), {description: WI.UIString("Ensure that elements of role \u0022%s\u0022 have required owned elements in accordance with WAI-ARIA.").format(WI.unlocalizedString("feed")), supports: 1}),
875                 new WI.AuditTestCase(`testTabListRoleForRequiredChidren`, testTabListRoleForRequiredChidren.toString(), {description: WI.UIString("Ensure that element of role \u0022%s\u0022 have required owned elements in accordance with WAI-ARIA.").format(WI.unlocalizedString("tablist")), supports: 1}),
876                 new WI.AuditTestCase(`testButtonLabels`, testButtonLabels.toString(), {description: WI.UIString("Ensure that buttons have accessible labels for assistive technology."), supports: 1}),
877                 new WI.AuditTestCase(`testCellRoleForRequiredChidren`, testCellRoleForRequiredChidren.toString(), {description: WI.UIString("Ensure that elements of role \u0022%s\u0022 have required owned elements in accordance with WAI-ARIA.").format(WI.unlocalizedString("cell")), supports: 1}),
878                 new WI.AuditTestCase(`testListRoleForRequiredChidren`, testListRoleForRequiredChidren.toString(), {description: WI.UIString("Ensure that elements of role \u0022%s\u0022 have required owned elements in accordance with WAI-ARIA.").format(WI.unlocalizedString("list")), supports: 1}),
879                 new WI.AuditTestCase(`testComboBoxRoleForRequiredChidren`, testComboBoxRoleForRequiredChidren.toString(), {description: WI.UIString("Ensure that elements of role \u0022%s\u0022 have required owned elements in accordance with WAI-ARIA.").format(WI.unlocalizedString("combobox")), supports: 1}),
880                 new WI.AuditTestCase(`testForMultipleMainContentSections`, testForMultipleMainContentSections.toString(), {description: WI.UIString("Ensure that only one main content section is used on the page."), supports: 1}),
881                 new WI.AuditTestCase(`testDialogsForLabels`, testDialogsForLabels.toString(), {description: WI.UIString("Ensure that dialogs have accessible labels for assistive technology."), supports: 1}),
882                 new WI.AuditTestCase(`testForInvalidAriaHiddenValue`, testForInvalidAriaHiddenValue.toString(), {description: WI.UIString("Ensure that values for \u0022%s\u0022 are valid.").format(WI.unlocalizedString("aria-hidden")), supports: 1})
883             ], {description: WI.UIString("Diagnoses common accessibility problems affecting screen readers and other assistive technology.")}),
884         ];
885
886         let checkDisabledDefaultTest = (test) => {
887             if (this._disabledDefaultTestsSetting.value.includes(test.name))
888                 test.disabled = true;
889
890             if (test instanceof WI.AuditTestGroup) {
891                 for (let child of test.tests)
892                     checkDisabledDefaultTest(child);
893             }
894         };
895
896         for (let test of defaultTests) {
897             checkDisabledDefaultTest(test);
898
899             test.__default = true;
900             this._addTest(test);
901         }
902     }
903 };
904
905 WI.AuditManager.RunningState = {
906     Disabled: "disabled",
907     Inactive: "inactive",
908     Active: "active",
909     Stopping: "stopping",
910 };
911
912 WI.AuditManager.Event = {
913     EditingChanged: "audit-manager-editing-changed",
914     TestAdded: "audit-manager-test-added",
915     TestCompleted: "audit-manager-test-completed",
916     TestRemoved: "audit-manager-test-removed",
917     TestScheduled: "audit-manager-test-scheduled",
918 };