Web Inspector: REGRESSION: Audit: default audits aren't added when an existing audit...
[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.putObject(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 null;
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 null;
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             let topLevelTest = this._topLevelTestForTest(test);
158             console.assert(topLevelTest || window.InspectorTest, "No matching top-level test found", test);
159             if (topLevelTest)
160                 await topLevelTest.setup();
161
162             await test.start();
163
164             if (InspectorBackend.domains.Audit)
165                 await AuditAgent.teardown();
166         }));
167
168         let result = this._runningTests.map((test) => test.result).filter((result) => !!result);
169
170         this._runningState = WI.AuditManager.RunningState.Inactive;
171         this._runningTests = [];
172
173         this._addResult(result);
174
175         if (mainResource !== WI.networkManager.mainFrame.mainResource) {
176             // Navigated while tests were running.
177             for (let test of this._tests)
178                 test.clearResult();
179         }
180
181         return this._results.lastValue === result ? result : null;
182     }
183
184     stop()
185     {
186         console.assert(this._runningState === WI.AuditManager.RunningState.Active);
187         if (this._runningState !== WI.AuditManager.RunningState.Active)
188             return;
189
190         this._runningState = WI.AuditManager.RunningState.Stopping;
191
192         for (let test of this._runningTests)
193             test.stop();
194     }
195
196     async processJSON({json, error})
197     {
198         if (error) {
199             WI.AuditManager.synthesizeError(error);
200             return;
201         }
202
203         if (typeof json !== "object" || json === null) {
204             WI.AuditManager.synthesizeError(WI.UIString("invalid JSON"));
205             return;
206         }
207
208         if (json.type !== WI.AuditTestCase.TypeIdentifier && json.type !== WI.AuditTestGroup.TypeIdentifier
209             && json.type !== WI.AuditTestCaseResult.TypeIdentifier && json.type !== WI.AuditTestGroupResult.TypeIdentifier) {
210             WI.AuditManager.synthesizeError(WI.UIString("unknown %s \u0022%s\u0022").format(WI.unlocalizedString("type"), json.type));
211             return;
212         }
213
214         let object = await WI.AuditTestGroup.fromPayload(json) || await WI.AuditTestCase.fromPayload(json) || await WI.AuditTestGroupResult.fromPayload(json) || await WI.AuditTestCaseResult.fromPayload(json);
215         if (!object)
216             return;
217
218         if (object instanceof WI.AuditTestBase) {
219             this._addTest(object);
220             WI.objectStores.audits.putObject(object);
221         } else if (object instanceof WI.AuditTestResultBase)
222             this._addResult(object);
223
224         WI.showRepresentedObject(object);
225     }
226
227     export(object)
228     {
229         console.assert(object instanceof WI.AuditTestCase || object instanceof WI.AuditTestGroup || object instanceof WI.AuditTestCaseResult || object instanceof WI.AuditTestGroupResult, object);
230
231         let filename = object.name;
232         if (object instanceof WI.AuditTestResultBase)
233             filename = WI.UIString("%s Result").format(filename);
234
235         WI.FileUtilities.save({
236             url: WI.FileUtilities.inspectorURLForFilename(filename + ".json"),
237             content: JSON.stringify(object),
238             forceSaveAs: true,
239         });
240     }
241
242     loadStoredTests()
243     {
244         if (this._tests.length)
245             return;
246
247         this._addDefaultTests();
248
249         WI.objectStores.audits.getAll().then(async (tests) => {
250             for (let payload of tests) {
251                 let test = await WI.AuditTestGroup.fromPayload(payload) || await WI.AuditTestCase.fromPayload(payload);
252                 if (!test)
253                     continue;
254
255                 const key = null;
256                 WI.objectStores.audits.associateObject(test, key, payload);
257
258                 this._addTest(test);
259             }
260         });
261     }
262
263     removeTest(test)
264     {
265         if (test.__default) {
266             if (test.disabled) {
267                 InspectorFrontendHost.beep();
268                 return;
269             }
270
271             test.disabled = true;
272
273             let disabledTests = this._disabledDefaultTestsSetting.value.slice();
274             disabledTests.push(test.name);
275             this._disabledDefaultTestsSetting.value = disabledTests;
276
277             return;
278         }
279
280         this._tests.remove(test);
281
282         this.dispatchEventToListeners(WI.AuditManager.Event.TestRemoved, {test});
283
284         WI.objectStores.audits.deleteObject(test);
285     }
286
287     // Private
288
289     _addTest(test)
290     {
291         this._tests.push(test);
292
293         this.dispatchEventToListeners(WI.AuditManager.Event.TestAdded, {test});
294     }
295
296     _addResult(result)
297     {
298         if (!result || (Array.isArray(result) && !result.length))
299             return;
300
301         this._results.push(result);
302
303         this.dispatchEventToListeners(WI.AuditManager.Event.TestCompleted, {
304             result,
305             index: this._results.length - 1,
306         });
307     }
308
309     _topLevelTestForTest(test)
310     {
311         function walk(group) {
312             if (group === test)
313                 return true;
314             if (group instanceof WI.AuditTestGroup) {
315                 for (let subtest of group.tests) {
316                     if (walk(subtest))
317                         return true;
318                 }
319             }
320             return false;
321         }
322
323         for (let topLevelTest of this._tests) {
324             if (walk(topLevelTest))
325                 return topLevelTest;
326         }
327
328         return null;
329     }
330
331     _handleFrameMainResourceDidChange(event)
332     {
333         if (!event.target.isMainFrame())
334             return;
335
336         if (this._runningState === WI.AuditManager.RunningState.Active)
337             this.stop();
338         else {
339             for (let test of this._tests)
340                 test.clearResult();
341         }
342     }
343
344     _addDefaultTests()
345     {
346         const testMenuRoleForRequiredChidren = function() {
347             const relationships = {
348                 menu: ["menuitem", "menuitemcheckbox", "menuitemradio"],
349                 menubar: ["menuitem", "menuitemcheckbox", "menuitemradio"],
350             };
351             let domNodes = [];
352             let visitedParents = new Set;
353             function hasChildWithRole(node, expectedRoles) {
354                 let childNode = node;
355                 if (!childNode)
356                     return false;
357
358                 if (childNode.parentNode)
359                     visitedParents.add(childNode.parentNode);
360
361                 while (childNode) {
362                     let properties = WebInspectorAudit.Accessibility.getComputedProperties(childNode);
363                     if (childNode.nodeType === Node.ELEMENT_NODE && properties) {
364                         if (expectedRoles.includes(properties.role))
365                             return true;
366
367                         if (childNode.hasChildNodes() && hasChildWithRole(childNode.firstChild, expectedRoles))
368                             return true;
369                     }
370                     childNode = childNode.nextSibling;
371                 }
372                 return false;
373             }
374             for (let role in relationships) {
375                 for (let parentNode of WebInspectorAudit.Accessibility.getElementsByComputedRole(role)) {
376                     if (visitedParents.has(parentNode))
377                         continue;
378
379                     if (!hasChildWithRole(parentNode.firstChild, relationships[role]))
380                         domNodes.push(parentNode);
381                 }
382             }
383             return {level: domNodes.length ? "fail" : "pass", domNodes, domAttributes: ["role"]};
384         };
385
386         const testGridRoleForRequiredChidren = function() {
387             const relationships = {
388                 grid: ["row", "rowgroup"],
389             };
390             let domNodes = [];
391             let visitedParents = new Set;
392             function hasChildWithRole(node, expectedRoles) {
393                 let childNode = node;
394                 if (!childNode)
395                     return false;
396
397                 if (childNode.parentNode)
398                     visitedParents.add(childNode.parentNode);
399
400                 while (childNode) {
401                     let properties = WebInspectorAudit.Accessibility.getComputedProperties(childNode);
402                     if (childNode.nodeType === Node.ELEMENT_NODE && properties) {
403                         if (expectedRoles.includes(properties.role))
404                             return true;
405
406                         if (childNode.hasChildNodes() && hasChildWithRole(childNode.firstChild, expectedRoles))
407                             return true;
408                     }
409                     childNode = childNode.nextSibling;
410                 }
411                 return false;
412             }
413             for (let role in relationships) {
414                 for (let parentNode of WebInspectorAudit.Accessibility.getElementsByComputedRole(role)) {
415                     if (visitedParents.has(parentNode))
416                         continue;
417
418                     if (!hasChildWithRole(parentNode.firstChild, relationships[role]))
419                         domNodes.push(parentNode);
420                 }
421             }
422             return {level: domNodes.length ? "fail" : "pass", domNodes, domAttributes: ["role"]};
423         };
424
425         const testForAriaLabelledBySpelling = function() {
426             let domNodes = Array.from(document.querySelectorAll("[aria-labeledby]"));
427             return {level: domNodes.length ? "fail" : "pass", domNodes, domAttributes: ["aria-labeledby"]};
428         };
429
430         const testForMultipleBanners = function() {
431             let domNodes = [];
432             let banners = WebInspectorAudit.Accessibility.getElementsByComputedRole("banner");
433             if (banners.length > 1)
434                 domNodes = banners;
435             return {level: domNodes.length ? "fail" : "pass", domNodes, domAttributes: ["role"]};
436         };
437
438         const testForLinkLabels = function() {
439             let links = WebInspectorAudit.Accessibility.getElementsByComputedRole("link");
440             let domNodes = links.filter((link) => !WebInspectorAudit.Accessibility.getComputedProperties(link).label);
441             return {level: domNodes.length ? "fail" : "pass", domNodes, domAttributes: ["aria-label", "aria-labelledby", "title"]};
442         };
443
444         const testRowGroupRoleForRequiredChidren = function() {
445             const relationships = {
446                 rowgroup: ["row"],
447             };
448             let domNodes = [];
449             let visitedParents = new Set;
450             function hasChildWithRole(node, expectedRoles) {
451                 let childNode = node;
452                 if (!childNode)
453                     return false;
454
455                 if (childNode.parentNode)
456                     visitedParents.add(childNode.parentNode);
457
458                 while (childNode) {
459                     let properties = WebInspectorAudit.Accessibility.getComputedProperties(childNode);
460                     if (childNode.nodeType === Node.ELEMENT_NODE && properties) {
461                         if (expectedRoles.includes(properties.role))
462                             return true;
463
464                         if (childNode.hasChildNodes() && hasChildWithRole(childNode.firstChild, expectedRoles))
465                             return true;
466                     }
467                     childNode = childNode.nextSibling;
468                 }
469                 return false;
470             }
471             for (let role in relationships) {
472                 for (let parentNode of WebInspectorAudit.Accessibility.getElementsByComputedRole(role)) {
473                     if (visitedParents.has(parentNode))
474                         continue;
475
476                     if (!hasChildWithRole(parentNode.firstChild, relationships[role]))
477                         domNodes.push(parentNode);
478                 }
479             }
480             return {level: domNodes.length ? "fail" : "pass", domNodes, domAttributes: ["role"]};
481         };
482
483         const testTableRoleForRequiredChidren = function() {
484             const relationships = {
485                 table: ["row", "rowgroup"],
486             };
487             let domNodes = [];
488             let visitedParents = new Set;
489             function hasChildWithRole(node, expectedRoles) {
490                 let childNode = node;
491                 if (!childNode)
492                     return false;
493
494                 if (childNode.parentNode)
495                     visitedParents.add(childNode.parentNode);
496
497                 while (childNode) {
498                     let properties = WebInspectorAudit.Accessibility.getComputedProperties(childNode);
499                     if (childNode.nodeType === Node.ELEMENT_NODE && properties) {
500                         if (expectedRoles.includes(properties.role))
501                             return true;
502
503                         if (childNode.hasChildNodes() && hasChildWithRole(childNode.firstChild, expectedRoles))
504                             return true;
505                     }
506                     childNode = childNode.nextSibling;
507                 }
508                 return false;
509             }
510             for (let role in relationships) {
511                 for (let parentNode of WebInspectorAudit.Accessibility.getElementsByComputedRole(role)) {
512                     if (visitedParents.has(parentNode))
513                         continue;
514
515                     if (!hasChildWithRole(parentNode.firstChild, relationships[role]))
516                         domNodes.push(parentNode);
517                 }
518             }
519             return {level: domNodes.length ? "fail" : "pass", domNodes, domAttributes: ["role"]};
520         };
521
522         const testForMultipleLiveRegions = function() {
523             const liveRegionRoles = ["alert", "log", "status", "marquee", "timer"];
524             let domNodes = [];
525             let liveRegions = liveRegionRoles.reduce((a, b) => {
526                 return a.concat(WebInspectorAudit.Accessibility.getElementsByComputedRole(b));
527             }, []);
528             liveRegions = liveRegions.concat(Array.from(document.querySelectorAll(`[aria-live="polite"], [aria-live="assertive"]`)));
529             if (liveRegions.length > 1)
530                 domNodes = liveRegions;
531             return {level: domNodes.length ? "warn" : "pass", domNodes, domAttributes: ["aria-live"]};
532         };
533
534         const testListBoxRoleForRequiredChidren = function() {
535             const relationships = {
536                 listbox: ["option"],
537             };
538             let domNodes = [];
539             let visitedParents = new Set;
540             function hasChildWithRole(node, expectedRoles) {
541                 let childNode = node;
542                 if (!childNode)
543                     return false;
544
545                 if (childNode.parentNode)
546                     visitedParents.add(childNode.parentNode);
547
548                 while (childNode) {
549                     let properties = WebInspectorAudit.Accessibility.getComputedProperties(childNode);
550                     if (childNode.nodeType === Node.ELEMENT_NODE && properties) {
551                         if (expectedRoles.includes(properties.role))
552                             return true;
553
554                         if (childNode.hasChildNodes() && hasChildWithRole(childNode.firstChild, expectedRoles))
555                             return true;
556                     }
557                     childNode = childNode.nextSibling;
558                 }
559                 return false;
560             }
561             for (let role in relationships) {
562                 for (let parentNode of WebInspectorAudit.Accessibility.getElementsByComputedRole(role)) {
563                     if (visitedParents.has(parentNode))
564                         continue;
565
566                     if (!hasChildWithRole(parentNode.firstChild, relationships[role]))
567                         domNodes.push(parentNode);
568                 }
569             }
570             return {level: domNodes.length ? "fail" : "pass", domNodes, domAttributes: ["role"]};
571         };
572
573         const testImageLabels = function() {
574             let images = WebInspectorAudit.Accessibility.getElementsByComputedRole("img");
575             let domNodes = images.filter((image) => !WebInspectorAudit.Accessibility.getComputedProperties(image).label);
576             return {level: domNodes.length ? "fail" : "pass", domNodes, domAttributes: ["aria-label", "aria-labelledby", "title", "alt"]};
577         };
578
579         const testForAriaHiddenFalse = function() {
580             let domNodes = Array.from(document.querySelectorAll(`[aria-hidden="false"]`));
581             return {level: domNodes.length ? "warn" : "pass", domNodes, domAttributes: ["aria-hidden"]};
582         };
583
584         const testTreeRoleForRequiredChidren = function() {
585             const relationships = {
586                 tree: ["treeitem", "group"],
587             };
588             let domNodes = [];
589             let visitedParents = new Set;
590             function hasChildWithRole(node, expectedRoles) {
591                 let childNode = node;
592                 if (!childNode)
593                     return false;
594
595                 if (childNode.parentNode)
596                     visitedParents.add(childNode.parentNode);
597
598                 while (childNode) {
599                     let properties = WebInspectorAudit.Accessibility.getComputedProperties(childNode);
600                     if (childNode.nodeType === Node.ELEMENT_NODE && properties) {
601                         if (expectedRoles.includes(properties.role))
602                             return true;
603
604                         if (childNode.hasChildNodes() && hasChildWithRole(childNode.firstChild, expectedRoles))
605                             return true;
606                     }
607                     childNode = childNode.nextSibling;
608                 }
609                 return false;
610             }
611             for (let role in relationships) {
612                 for (let parentNode of WebInspectorAudit.Accessibility.getElementsByComputedRole(role)) {
613                     if (visitedParents.has(parentNode))
614                         continue;
615
616                     if (!hasChildWithRole(parentNode.firstChild, relationships[role]))
617                         domNodes.push(parentNode);
618                 }
619             }
620             return {level: domNodes.length ? "fail" : "pass", domNodes, domAttributes: ["role"]};
621         };
622
623         const testRadioGroupRoleForRequiredChidren = function() {
624             const relationships = {
625                 radiogroup: ["radio"],
626             };
627             let domNodes = [];
628             let visitedParents = new Set;
629             function hasChildWithRole(node, expectedRoles) {
630                 let childNode = node;
631                 if (!childNode)
632                     return false;
633
634                 if (childNode.parentNode)
635                     visitedParents.add(childNode.parentNode);
636
637                 while (childNode) {
638                     let properties = WebInspectorAudit.Accessibility.getComputedProperties(childNode);
639                     if (childNode.nodeType === Node.ELEMENT_NODE && properties) {
640                         if (expectedRoles.includes(properties.role))
641                             return true;
642
643                         if (childNode.hasChildNodes() && hasChildWithRole(childNode.firstChild, expectedRoles))
644                             return true;
645                     }
646                     childNode = childNode.nextSibling;
647                 }
648                 return false;
649             }
650             for (let role in relationships) {
651                 for (let parentNode of WebInspectorAudit.Accessibility.getElementsByComputedRole(role)) {
652                     if (visitedParents.has(parentNode))
653                         continue;
654
655                     if (!hasChildWithRole(parentNode.firstChild, relationships[role]))
656                         domNodes.push(parentNode);
657                 }
658             }
659             return {level: domNodes.length ? "fail" : "pass", domNodes, domAttributes: ["role"]};
660         };
661
662         const testFeedRoleForRequiredChidren = function() {
663             const relationships = {
664                 feed: ["article"],
665             };
666             let domNodes = [];
667             let visitedParents = new Set;
668             function hasChildWithRole(node, expectedRoles) {
669                 let childNode = node;
670                 if (!childNode)
671                     return false;
672
673                 if (childNode.parentNode)
674                     visitedParents.add(childNode.parentNode);
675
676                 while (childNode) {
677                     let properties = WebInspectorAudit.Accessibility.getComputedProperties(childNode);
678                     if (childNode.nodeType === Node.ELEMENT_NODE && properties) {
679                         if (expectedRoles.includes(properties.role))
680                             return true;
681
682                         if (childNode.hasChildNodes() && hasChildWithRole(childNode.firstChild, expectedRoles))
683                             return true;
684                     }
685                     childNode = childNode.nextSibling;
686                 }
687                 return false;
688             }
689             for (let role in relationships) {
690                 for (let parentNode of WebInspectorAudit.Accessibility.getElementsByComputedRole(role)) {
691                     if (visitedParents.has(parentNode))
692                         continue;
693
694                     if (!hasChildWithRole(parentNode.firstChild, relationships[role]))
695                         domNodes.push(parentNode);
696                 }
697             }
698             return {level: domNodes.length ? "fail" : "pass", domNodes, domAttributes: ["role"]};
699         };
700
701         const testTabListRoleForRequiredChidren = function() {
702             const relationships = {
703                 tablist: ["tab"],
704             };
705             let domNodes = [];
706             let visitedParents = new Set;
707             function hasChildWithRole(node, expectedRoles) {
708                 let childNode = node;
709                 if (!childNode)
710                     return false;
711
712                 if (childNode.parentNode)
713                     visitedParents.add(childNode.parentNode);
714
715                 while (childNode) {
716                     let properties = WebInspectorAudit.Accessibility.getComputedProperties(childNode);
717                     if (childNode.nodeType === Node.ELEMENT_NODE && properties) {
718                         if (expectedRoles.includes(properties.role))
719                             return true;
720
721                         if (childNode.hasChildNodes() && hasChildWithRole(childNode.firstChild, expectedRoles))
722                             return true;
723                     }
724                     childNode = childNode.nextSibling;
725                 }
726                 return false;
727             }
728             for (let role in relationships) {
729                 for (let parentNode of WebInspectorAudit.Accessibility.getElementsByComputedRole(role)) {
730                     if (visitedParents.has(parentNode))
731                         continue;
732
733                     if (!hasChildWithRole(parentNode.firstChild, relationships[role]))
734                         domNodes.push(parentNode);
735                 }
736             }
737             return {level: domNodes.length ? "fail" : "pass", domNodes, domAttributes: ["role"]};
738         };
739
740         const testButtonLabels = function() {
741             let buttons = WebInspectorAudit.Accessibility.getElementsByComputedRole("button");
742             let domNodes = buttons.filter((button) => !WebInspectorAudit.Accessibility.getComputedProperties(button).label);
743             return {level: domNodes.length ? "fail" : "pass", domNodes, domAttributes: ["aria-label", "aria-labelledby", "title"]};
744         };
745
746         const testRowRoleForRequiredChidren = function() {
747             const relationships = {
748                 row: ["cell", "gridcell", "columnheader", "rowheader"],
749             };
750             let domNodes = [];
751             let visitedParents = new Set;
752             function hasChildWithRole(node, expectedRoles) {
753                 let childNode = node;
754                 if (!childNode)
755                     return false;
756
757                 if (childNode.parentNode)
758                     visitedParents.add(childNode.parentNode);
759
760                 while (childNode) {
761                     let properties = WebInspectorAudit.Accessibility.getComputedProperties(childNode);
762                     if (childNode.nodeType === Node.ELEMENT_NODE && properties) {
763                         if (expectedRoles.includes(properties.role))
764                             return true;
765
766                         if (childNode.hasChildNodes() && hasChildWithRole(childNode.firstChild, expectedRoles))
767                             return true;
768                     }
769                     childNode = childNode.nextSibling;
770                 }
771                 return false;
772             }
773             for (let role in relationships) {
774                 for (let parentNode of WebInspectorAudit.Accessibility.getElementsByComputedRole(role)) {
775                     if (visitedParents.has(parentNode))
776                         continue;
777
778                     if (!hasChildWithRole(parentNode.firstChild, relationships[role]))
779                         domNodes.push(parentNode);
780                 }
781             }
782             return {level: domNodes.length ? "fail" : "pass", domNodes, domAttributes: ["role"]};
783         };
784
785         const testListRoleForRequiredChidren = function() {
786             const relationships = {
787                 list: ["listitem", "group"],
788             };
789             let domNodes = [];
790             let visitedParents = new Set;
791             function hasChildWithRole(node, expectedRoles) {
792                 let childNode = node;
793                 if (!childNode)
794                     return false;
795
796                 if (childNode.parentNode)
797                     visitedParents.add(childNode.parentNode);
798
799                 while (childNode) {
800                     let properties = WebInspectorAudit.Accessibility.getComputedProperties(childNode);
801                     if (childNode.nodeType === Node.ELEMENT_NODE && properties) {
802                         if (expectedRoles.includes(properties.role))
803                             return true;
804
805                         if (childNode.hasChildNodes() && hasChildWithRole(childNode.firstChild, expectedRoles))
806                             return true;
807                     }
808                     childNode = childNode.nextSibling;
809                 }
810                 return false;
811             }
812             for (let role in relationships) {
813                 for (let parentNode of WebInspectorAudit.Accessibility.getElementsByComputedRole(role)) {
814                     if (visitedParents.has(parentNode))
815                         continue;
816
817                     if (!hasChildWithRole(parentNode.firstChild, relationships[role]))
818                         domNodes.push(parentNode);
819                 }
820             }
821             return {level: domNodes.length ? "warn" : "pass", domNodes, domAttributes: ["role"]};
822         };
823
824         const testComboBoxRoleForRequiredChidren = function() {
825             const relationships = {
826                 combobox: ["textbox", "listbox", "tree", "grid", "dialog"],
827             };
828             let domNodes = [];
829             let visitedParents = new Set;
830             function hasChildWithRole(node, expectedRoles) {
831                 let childNode = node;
832                 if (!childNode)
833                     return false;
834
835                 if (childNode.parentNode)
836                     visitedParents.add(childNode.parentNode);
837
838                 while (childNode) {
839                     let properties = WebInspectorAudit.Accessibility.getComputedProperties(childNode);
840                     if (childNode.nodeType === Node.ELEMENT_NODE && properties) {
841                         if (expectedRoles.includes(properties.role))
842                             return true;
843
844                         if (childNode.hasChildNodes() && hasChildWithRole(childNode.firstChild, expectedRoles))
845                             return true;
846                     }
847                     childNode = childNode.nextSibling;
848                 }
849                 return false;
850             }
851             for (let role in relationships) {
852                 for (let parentNode of WebInspectorAudit.Accessibility.getElementsByComputedRole(role)) {
853                     if (visitedParents.has(parentNode))
854                         continue;
855
856                     if (!hasChildWithRole(parentNode.firstChild, relationships[role]))
857                         domNodes.push(parentNode);
858                 }
859             }
860             return {level: domNodes.length ? "fail" : "pass", domNodes, domAttributes: ["role"]};
861         };
862
863         const testForMultipleMainContentSections = function() {
864             let domNodes = [];
865             let mainContentElements = WebInspectorAudit.Accessibility.getElementsByComputedRole("main");
866             if (mainContentElements.length > 1) {
867                 domNodes = mainContentElements;
868             }
869             return {level: domNodes.length ? "fail" : "pass", domNodes, domAttributes: ["role"]};
870         };
871
872         const testDialogsForLabels = function() {
873           let dialogs = WebInspectorAudit.Accessibility.getElementsByComputedRole("dialog");
874           let domNodes = dialogs.filter((dialog) => !WebInspectorAudit.Accessibility.getComputedProperties(dialog).label);
875           return {level: domNodes.length ? "fail" : "pass", domNodes, domAttributes: ["aria-label", "aria-labelledby", "title"]};
876         };
877
878         const testForInvalidAriaHiddenValue = function() {
879             let domNodes = Array.from(document.querySelectorAll(`[aria-hidden]:not([aria-hidden="true"], [aria-hidden="false"])`));
880             return {level: domNodes.length ? "fail" : "pass", domNodes, domAttributes: ["aria-hidden"]};
881         };
882
883         const defaultTests = [
884             new WI.AuditTestGroup(WI.UIString("Demo Audit"), [
885                 new WI.AuditTestGroup(WI.UIString("Result Levels"), [
886                     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.")}),
887                     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.")}),
888                     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.")}),
889                     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.")}),
890                     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.")}),
891                 ], {description: WI.UIString("These are all of the different test result levels.")}),
892                 new WI.AuditTestGroup(WI.UIString("Result Data"), [
893                     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.")}),
894                     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.")}),
895                     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.")}),
896                 ], {description: WI.UIString("These are all of the different types of data that can be returned with the test result.")}),
897             ], {description: WI.UIString("These tests serve as a demonstration of the functionality and structure of audits.")}),
898             new WI.AuditTestGroup(WI.UIString("Accessibility"), [
899                 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}),
900                 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}),
901                 new WI.AuditTestCase(`testForAriaLabelledBySpelling`, testForAriaLabelledBySpelling.toString(), {description: WI.UIString("Ensure that \u0022%s\u0022 is spelled correctly.").format(WI.unlocalizedString("aria-labelledby")), supports: 1}),
902                 new WI.AuditTestCase(`testForMultipleBanners`, testForMultipleBanners.toString(), {description: WI.UIString("Ensure that only one banner is used on the page."), supports: 1}),
903                 new WI.AuditTestCase(`testForLinkLabels`, testForLinkLabels.toString(), {description: WI.UIString("Ensure that links have accessible labels for assistive technology."), supports: 1}),
904                 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}),
905                 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}),
906                 new WI.AuditTestCase(`testForMultipleLiveRegions`, testForMultipleLiveRegions.toString(), {description: WI.UIString("Ensure that only one live region is used on the page."), supports: 1}),
907                 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}),
908                 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}),
909                 new WI.AuditTestCase(`testForAriaHiddenFalse`, testForAriaHiddenFalse.toString(), {description: WI.UIString("Ensure aria-hidden=\u0022%s\u0022 is not used.").format(WI.unlocalizedString("false")), supports: 1}),
910                 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}),
911                 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}),
912                 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}),
913                 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}),
914                 new WI.AuditTestCase(`testButtonLabels`, testButtonLabels.toString(), {description: WI.UIString("Ensure that buttons have accessible labels for assistive technology."), supports: 1}),
915                 new WI.AuditTestCase(`testRowRoleForRequiredChidren`, testRowRoleForRequiredChidren.toString(), {description: WI.UIString("Ensure that elements of role \u0022%s\u0022 have required owned elements in accordance with WAI-ARIA.").format(WI.unlocalizedString("row")), supports: 1}),
916                 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}),
917                 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}),
918                 new WI.AuditTestCase(`testForMultipleMainContentSections`, testForMultipleMainContentSections.toString(), {description: WI.UIString("Ensure that only one main content section is used on the page."), supports: 1}),
919                 new WI.AuditTestCase(`testDialogsForLabels`, testDialogsForLabels.toString(), {description: WI.UIString("Ensure that dialogs have accessible labels for assistive technology."), supports: 1}),
920                 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})
921             ], {description: WI.UIString("Diagnoses common accessibility problems affecting screen readers and other assistive technology.")}),
922         ];
923
924         let checkDisabledDefaultTest = (test) => {
925             if (this._disabledDefaultTestsSetting.value.includes(test.name))
926                 test.disabled = true;
927
928             if (test instanceof WI.AuditTestGroup) {
929                 for (let child of test.tests)
930                     checkDisabledDefaultTest(child);
931             }
932         };
933
934         for (let test of defaultTests) {
935             checkDisabledDefaultTest(test);
936
937             test.__default = true;
938             this._addTest(test);
939         }
940     }
941 };
942
943 WI.AuditManager.RunningState = {
944     Disabled: "disabled",
945     Inactive: "inactive",
946     Active: "active",
947     Stopping: "stopping",
948 };
949
950 WI.AuditManager.Event = {
951     EditingChanged: "audit-manager-editing-changed",
952     TestAdded: "audit-manager-test-added",
953     TestCompleted: "audit-manager-test-completed",
954     TestRemoved: "audit-manager-test-removed",
955     TestScheduled: "audit-manager-test-scheduled",
956 };