2 * Copyright (C) 2018 Apple Inc. All rights reserved.
4 * Redistribution and use in source and binary forms, with or without
5 * modification, are permitted provided that the following conditions
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.
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.
26 WI.AuditManager = class AuditManager extends WI.Object
35 this._runningState = WI.AuditManager.RunningState.Inactive;
36 this._runningTests = [];
38 this._disabledDefaultTestsSetting = new WI.Setting("audit-disabled-default-tests", []);
40 WI.Frame.addEventListener(WI.Frame.Event.MainResourceDidChange, this._handleFrameMainResourceDidChange, this);
45 static synthesizeWarning(message)
47 message = WI.UIString("Audit Warning: %s").format(message);
49 if (window.InspectorTest) {
50 console.warn(message);
54 let consoleMessage = new WI.ConsoleMessage(WI.mainTarget, WI.ConsoleMessage.MessageSource.Other, WI.ConsoleMessage.MessageLevel.Warning, message);
55 consoleMessage.shouldRevealConsole = true;
57 WI.consoleLogViewController.appendConsoleMessage(consoleMessage);
60 static synthesizeError(message)
62 message = WI.UIString("Audit Error: %s").format(message);
64 if (window.InspectorTest) {
65 console.error(message);
69 let consoleMessage = new WI.ConsoleMessage(WI.mainTarget, WI.ConsoleMessage.MessageSource.Other, WI.ConsoleMessage.MessageLevel.Error, message);
70 consoleMessage.shouldRevealConsole = true;
72 WI.consoleLogViewController.appendConsoleMessage(consoleMessage);
77 get tests() { return this._tests; }
78 get results() { return this._results; }
79 get runningState() { return this._runningState; }
83 return this._runningState === WI.AuditManager.RunningState.Disabled;
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)
92 let runningState = editing ? WI.AuditManager.RunningState.Disabled : WI.AuditManager.RunningState.Inactive;
93 console.assert(runningState !== this._runningState);
94 if (runningState === this._runningState)
97 this._runningState = runningState;
99 this.dispatchEventToListeners(WI.AuditManager.Event.EditingChanged);
102 WI.objectStores.audits.clear();
104 let disabledDefaultTests = [];
105 let saveDisabledDefaultTest = (test) => {
107 disabledDefaultTests.push(test.name);
109 if (test instanceof WI.AuditTestGroup) {
110 for (let child of test.tests)
111 saveDisabledDefaultTest(child);
115 for (let test of this._tests) {
117 saveDisabledDefaultTest(test);
119 WI.objectStores.audits.addObject(test);
122 this._disabledDefaultTestsSetting.value = disabledDefaultTests;
128 console.assert(this._runningState === WI.AuditManager.RunningState.Inactive);
129 if (this._runningState !== WI.AuditManager.RunningState.Inactive)
132 if (tests && tests.length)
133 tests = tests.filter((test) => typeof test === "object" && test instanceof WI.AuditTestBase);
137 console.assert(tests.length);
141 let mainResource = WI.networkManager.mainFrame.mainResource;
143 this._runningState = WI.AuditManager.RunningState.Active;
144 this._runningTests = tests;
145 for (let test of this._runningTests)
148 this.dispatchEventToListeners(WI.AuditManager.Event.TestScheduled);
150 await Promise.chain(this._runningTests.map((test) => async () => {
151 if (this._runningState !== WI.AuditManager.RunningState.Active)
154 if (InspectorBackend.domains.Audit)
155 await AuditAgent.setup();
159 if (InspectorBackend.domains.Audit)
160 await AuditAgent.teardown();
163 let result = this._runningTests.map((test) => test.result).filter((result) => !!result);
165 this._runningState = WI.AuditManager.RunningState.Inactive;
166 this._runningTests = [];
168 this._addResult(result);
170 if (mainResource !== WI.networkManager.mainFrame.mainResource) {
171 // Navigated while tests were running.
172 for (let test of this._tests)
179 console.assert(this._runningState === WI.AuditManager.RunningState.Active);
180 if (this._runningState !== WI.AuditManager.RunningState.Active)
183 this._runningState = WI.AuditManager.RunningState.Stopping;
185 for (let test of this._runningTests)
189 async processJSON({json, error})
192 WI.AuditManager.synthesizeError(error);
196 if (typeof json !== "object" || json === null) {
197 WI.AuditManager.synthesizeError(WI.UIString("invalid JSON"));
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));
207 let object = await WI.AuditTestGroup.fromPayload(json) || await WI.AuditTestCase.fromPayload(json) || await WI.AuditTestGroupResult.fromPayload(json) || await WI.AuditTestCaseResult.fromPayload(json);
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);
217 WI.showRepresentedObject(object);
222 console.assert(object instanceof WI.AuditTestCase || object instanceof WI.AuditTestGroup || object instanceof WI.AuditTestCaseResult || object instanceof WI.AuditTestGroupResult, object);
224 let filename = object.name;
225 if (object instanceof WI.AuditTestResultBase)
226 filename = WI.UIString("%s Result").format(filename);
228 let url = "web-inspector:///" + encodeURI(filename) + ".json";
230 WI.FileUtilities.save({
232 content: JSON.stringify(object),
239 if (this._tests.length)
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);
249 WI.objectStores.audits.associateObject(test, key, payload);
254 this.addDefaultTestsIfNeeded();
260 this._tests.remove(test);
262 this.dispatchEventToListeners(WI.AuditManager.Event.TestRemoved, {test});
265 WI.objectStores.audits.deleteObject(test);
272 this._tests.push(test);
274 this.dispatchEventToListeners(WI.AuditManager.Event.TestAdded, {test});
279 if (!result || (Array.isArray(result) && !result.length))
282 this._results.push(result);
284 this.dispatchEventToListeners(WI.AuditManager.Event.TestCompleted, {
286 index: this._results.length - 1,
290 _handleFrameMainResourceDidChange(event)
292 if (!event.target.isMainFrame())
295 if (this._runningState === WI.AuditManager.RunningState.Active)
298 for (let test of this._tests)
303 addDefaultTestsIfNeeded()
305 if (this._tests.length)
308 const defaultTests = [
309 new WI.AuditTestGroup(WI.UIString("Demo Audit"), [
310 new WI.AuditTestGroup(WI.UIString("Result Levels"), [
311 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.")}),
312 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.")}),
313 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.")}),
314 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.")}),
315 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.")}),
316 ], {description: WI.UIString("These are all of the different test result levels.")}),
317 new WI.AuditTestGroup(WI.UIString("Result Data"), [
318 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.")}),
319 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.")}),
320 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.")}),
321 ], {description: WI.UIString("These are all of the different types of data that can be returned with the test result.")}),
322 ], {description: WI.UIString("These tests serve as a demonstration of the functionality and structure of audits.")}),
323 new WI.AuditTestGroup(WI.UIString("Accessibility"), [
324 new WI.AuditTestGroup(WI.UIString("Attributes"), [
325 new WI.AuditTestCase(`img-alt`, `function() { let domNodes = Array.from(document.getElementsByTagName("img")).filter((img) => !img.alt || !img.alt.length); return { level: domNodes.length ? "fail" : "pass", domNodes, domAttributes: ["alt"] }; }`, {description: WI.UIString("Ensure <img> elements have alternate text.")}),
326 new WI.AuditTestCase(`area-alt`, `function() { let domNodes = Array.from(document.getElementsByTagName("area")).filter((area) => !area.alt || !area.alt.length); return { level: domNodes.length ? "fail" : "pass", domNodes, domAttributes: ["alt"] }; }`, {description: WI.UIString("Ensure <area> elements have alternate text.")}),
327 new WI.AuditTestCase(`valid-tabindex`, `function() { let domNodes = Array.from(document.querySelectorAll("*[tabindex]")) .filter((node) => { let tabindex = node.getAttribute("tabindex"); if (!tabindex) return false; tabindex = parseInt(tabindex); return isNaN(tabindex) || (tabindex !== 0 && tabindex !== -1); }); return { level: domNodes.length ? "fail" : "pass", domNodes, domAttributes: ["tabindex"] }; }`, {description: WI.UIString("Ensure tabindex is a number.")}),
328 new WI.AuditTestCase(`frame-title`, `function() { let domNodes = Array.from(document.querySelectorAll("iframe, frame")) .filter((node) => { let title = node.getAttribute("title"); return !title || !title.trim().length; }); return { level: domNodes.length ? "fail" : "pass", domNodes, domAttributes: ["title"] }; }`, {description: WI.UIString("Ensure <frame> elements have a title.")}),
329 new WI.AuditTestCase(`hidden-body`, `function() { let domNodes = Array.from(document.querySelectorAll("body[hidden]")).filter((body) => body.hidden); return { level: domNodes.length ? "fail" : "pass", domNodes, domAttributes: ["hidden"] }; }`, {description: WI.UIString("Ensure hidden=true is not present on the <body>.")}),
330 new WI.AuditTestCase(`meta-refresh`, `function() { let domNodes = Array.from(document.querySelectorAll("meta[http-equiv=refresh]")); return { level: domNodes.length ? "warn" : "pass", domNodes, domAttributes: ["http-equiv"] }; }`, {description: WI.UIString("Ensure <meta http-equiv=refresh> is not used.")}),
331 ], {description: WI.UIString("Tests for element attribute accessibility issues.")}),
332 new WI.AuditTestGroup(WI.UIString("Elements"), [
333 new WI.AuditTestCase(`blink`, `function() { let domNodes = Array.from(document.getElementsByTagName("blink")); return { level: domNodes.length ? "warn" : "pass", domNodes }; }`, {description: WI.UIString("Ensure <blink> is not used.")}),
334 new WI.AuditTestCase(`marquee`, `function() { let domNodes = Array.from(document.getElementsByTagName("marquee")); return { level: domNodes.length ? "warn" : "pass", domNodes }; }`, {description: WI.UIString("Ensure <marquee> is not used.")}),
335 new WI.AuditTestCase(`dlitem`, `function() { function check(node) { if (!node) { return false; } if (node.nodeName === "DD") { return true; } return check(node.parentNode); } let domNodes = Array.from(document.querySelectorAll("dt, dd")).filter(check); return { level: domNodes.length ? "warn" : "pass", domNodes }; }`, {description: WI.UIString("Ensure <dt> and <dd> elements are contained by a <dl>.")}),
336 ], {description: WI.UIString("Tests for element accessibility issues.")}),
337 new WI.AuditTestGroup(WI.UIString("Forms"), [
338 new WI.AuditTestCase(`one-legend`, `function() { let formLegendsMap = Array.from(document.querySelectorAll("form legend")).reduce((accumulator, node) => { let existing = accumulator.get(node.form); if (!existing) { existing = []; accumulator.set(node.form, existing); } existing.push(node); return accumulator; }, new Map); let domNodes = Array.from(formLegendsMap.values()).reduce((accumulator, legends) => accumulator.concat(legends), []); return { level: domNodes.length ? "warn" : "pass", domNodes }; }`, {description: WI.UIString("Ensure exactly one <legend> exists per <form>.")}),
339 new WI.AuditTestCase(`legend-first-child`, `function() { let domNodes = Array.from(document.querySelectorAll("form > legend:not(:first-child)")); return { level: domNodes.length ? "warn" : "pass", domNodes }; }`, {description: WI.UIString("Ensure that the <legend> is the first child in the <form>.")}),
340 new WI.AuditTestCase(`form-input`, `function() { let domNodes = Array.from(document.getElementsByTagName("form")) .filter(node => !node.elements.length); return { level: domNodes.length ? "warn" : "pass", domNodes }; }`, {description: WI.UIString("Ensure <form>s have at least one input.")}),
341 ], {description: WI.UIString("Tests the accessibility of form elements.")}),
342 ], {description: WI.UIString("Tests for ways to improve accessibility.")}),
345 let checkDisabledDefaultTest = (test) => {
346 if (this._disabledDefaultTestsSetting.value.includes(test.name))
347 test.disabled = true;
349 if (test instanceof WI.AuditTestGroup) {
350 for (let child of test.tests)
351 checkDisabledDefaultTest(child);
355 for (let test of defaultTests) {
356 checkDisabledDefaultTest(test);
358 test.__default = true;
364 WI.AuditManager.RunningState = {
365 Disabled: "disabled",
366 Inactive: "inactive",
368 Stopping: "stopping",
371 WI.AuditManager.Event = {
372 EditingChanged: "audit-manager-editing-changed",
373 TestAdded: "audit-manager-test-added",
374 TestCompleted: "audit-manager-test-completed",
375 TestRemoved: "audit-manager-test-removed",
376 TestScheduled: "audit-manager-test-scheduled",