Web Inspector: Audit: add default tests
authordrousso@apple.com <drousso@apple.com@268f45cc-cd09-0410-ab3c-d52691b4dbfc>
Sat, 17 Nov 2018 02:14:44 +0000 (02:14 +0000)
committerdrousso@apple.com <drousso@apple.com@268f45cc-cd09-0410-ab3c-d52691b4dbfc>
Sat, 17 Nov 2018 02:14:44 +0000 (02:14 +0000)
https://bugs.webkit.org/show_bug.cgi?id=191758

Reviewed by Matt Baker.

* UserInterface/Controllers/AuditManager.js:
(WI.AuditManager.prototype.loadStoredTests):
(WI.AuditManager.prototype.addDefaultTestsIfNeeded): Added.
If there are no previously saved tests in the database, automatically add the default set.

* UserInterface/Views/AuditNavigationSidebarPanel.js:
(WI.AuditNavigationSidebarPanel.prototype._addTest):
(WI.AuditNavigationSidebarPanel.prototype._addResult):
(WI.AuditNavigationSidebarPanel.prototype._handleAuditTestRemoved):
* UserInterface/Views/AuditNavigationSidebarPanel.css:
(.sidebar > .panel.navigation.audit.has-results:not(.has-tests) > .content > .message-text-view): Added.
(.sidebar > .panel.navigation.audit.has-results:not(.has-tests) > .content > .message-text-view > .message): Added.
(.sidebar > .panel.navigation.audit.has-results:not(.has-tests) > .content > .message-text-view > button): Added.
When the last test is removed, show a placeholder message that allows the user to re-add the
default set of tests.

* UserInterface/Views/NavigationSidebarPanel.js:
(WI.NavigationSidebarPanel.prototype.showEmptyContentPlaceholder):
(WI.NavigationSidebarPanel.prototype._createEmptyContentPlaceholderIfNeeded):

* Localizations/en.lproj/localizedStrings.js:

git-svn-id: https://svn.webkit.org/repository/webkit/trunk@238334 268f45cc-cd09-0410-ab3c-d52691b4dbfc

Source/WebInspectorUI/ChangeLog
Source/WebInspectorUI/Localizations/en.lproj/localizedStrings.js
Source/WebInspectorUI/UserInterface/Controllers/AuditManager.js
Source/WebInspectorUI/UserInterface/Views/AuditNavigationSidebarPanel.css
Source/WebInspectorUI/UserInterface/Views/AuditNavigationSidebarPanel.js
Source/WebInspectorUI/UserInterface/Views/NavigationSidebarPanel.js

index c14399d..804b279 100644 (file)
@@ -1,5 +1,34 @@
 2018-11-16  Devin Rousso  <drousso@apple.com>
 
+        Web Inspector: Audit: add default tests
+        https://bugs.webkit.org/show_bug.cgi?id=191758
+
+        Reviewed by Matt Baker.
+
+        * UserInterface/Controllers/AuditManager.js:
+        (WI.AuditManager.prototype.loadStoredTests):
+        (WI.AuditManager.prototype.addDefaultTestsIfNeeded): Added.
+        If there are no previously saved tests in the database, automatically add the default set.
+
+        * UserInterface/Views/AuditNavigationSidebarPanel.js:
+        (WI.AuditNavigationSidebarPanel.prototype._addTest):
+        (WI.AuditNavigationSidebarPanel.prototype._addResult):
+        (WI.AuditNavigationSidebarPanel.prototype._handleAuditTestRemoved):
+        * UserInterface/Views/AuditNavigationSidebarPanel.css:
+        (.sidebar > .panel.navigation.audit.has-results:not(.has-tests) > .content > .message-text-view): Added.
+        (.sidebar > .panel.navigation.audit.has-results:not(.has-tests) > .content > .message-text-view > .message): Added.
+        (.sidebar > .panel.navigation.audit.has-results:not(.has-tests) > .content > .message-text-view > button): Added.
+        When the last test is removed, show a placeholder message that allows the user to re-add the
+        default set of tests.
+
+        * UserInterface/Views/NavigationSidebarPanel.js:
+        (WI.NavigationSidebarPanel.prototype.showEmptyContentPlaceholder):
+        (WI.NavigationSidebarPanel.prototype._createEmptyContentPlaceholderIfNeeded):
+
+        * Localizations/en.lproj/localizedStrings.js:
+
+2018-11-16  Devin Rousso  <drousso@apple.com>
+
         Web Inspector: Audit: minor style improvements
         https://bugs.webkit.org/show_bug.cgi?id=191727
 
index 022e975..2f9fbb5 100644 (file)
@@ -77,6 +77,7 @@ localizedStrings["Activity Viewer"] = "Activity Viewer";
 localizedStrings["Add"] = "Add";
 localizedStrings["Add Action"] = "Add Action";
 localizedStrings["Add Breakpoint"] = "Add Breakpoint";
+localizedStrings["Add Default Audits"] = "Add Default Audits";
 localizedStrings["Add New"] = "Add New";
 localizedStrings["Add New Class"] = "Add New Class";
 localizedStrings["Add New Probe Expression"] = "Add New Probe Expression";
@@ -279,6 +280,7 @@ localizedStrings["Decoded"] = "Decoded";
 localizedStrings["Delete"] = "Delete";
 localizedStrings["Delete Breakpoint"] = "Delete Breakpoint";
 localizedStrings["Delete Breakpoints"] = "Delete Breakpoints";
+localizedStrings["Demo Audit"] = "Demo Audit";
 localizedStrings["Detach into separate window"] = "Detach into separate window";
 localizedStrings["Detached"] = "Detached";
 localizedStrings["Details"] = "Details";
@@ -360,6 +362,15 @@ localizedStrings["Enable paint flashing"] = "Enable paint flashing";
 localizedStrings["Enabled"] = "Enabled";
 localizedStrings["Encoded"] = "Encoded";
 localizedStrings["Encoding"] = "Encoding";
+localizedStrings["Ensure <area> elements of image maps have alternate text."] = "Ensure <area> elements of image maps have alternate text.";
+localizedStrings["Ensure <dt> and <dd> elements are contained by a <dl>."] = "Ensure <dt> and <dd> elements are contained by a <dl>.";
+localizedStrings["Ensure <img> elements have alternate text."] = "Ensure <img> elements have alternate text.";
+localizedStrings["Ensure <meta http-equiv=refresh> is not used."] = "Ensure <meta http-equiv=refresh> is not used.";
+localizedStrings["Ensure exactly one <legend> exists per form."] = "Ensure exactly one <legend> exists per form.";
+localizedStrings["Ensure forms have at least one input."] = "Ensure forms have at least one input.";
+localizedStrings["Ensure hidden=true is not present on the document body."] = "Ensure hidden=true is not present on the document body.";
+localizedStrings["Ensure legend is first child in form."] = "Ensure legend is first child in form.";
+localizedStrings["Ensure tabindex is a number."] = "Ensure tabindex is a number.";
 localizedStrings["Entire Recording"] = "Entire Recording";
 localizedStrings["Error"] = "Error";
 localizedStrings["Error: "] = "Error: ";
@@ -415,6 +426,7 @@ localizedStrings["Format: RGB"] = "Format: RGB";
 localizedStrings["Format: RGBA"] = "Format: RGBA";
 localizedStrings["Format: Short Hex"] = "Format: Short Hex";
 localizedStrings["Format: Short Hex with Alpha"] = "Format: Short Hex with Alpha";
+localizedStrings["Forms"] = "Forms";
 localizedStrings["Forward (%s)"] = "Forward (%s)";
 localizedStrings["Fragment"] = "Fragment";
 localizedStrings["Fragment Shader"] = "Fragment Shader";
@@ -593,6 +605,7 @@ localizedStrings["No Results Found"] = "No Results Found";
 localizedStrings["No Search Results"] = "No Search Results";
 localizedStrings["No Watch Expressions"] = "No Watch Expressions";
 localizedStrings["No audit selected"] = "No audit selected";
+localizedStrings["No audits"] = "No audits";
 localizedStrings["No matching ARIA role"] = "No matching ARIA role";
 localizedStrings["No preview available"] = "No preview available";
 localizedStrings["No request cookies."] = "No request cookies.";
@@ -729,6 +742,8 @@ localizedStrings["Response Headers"] = "Response Headers";
 localizedStrings["Response:"] = "Response:";
 localizedStrings["Restart (%s)"] = "Restart (%s)";
 localizedStrings["Restart animation"] = "Restart animation";
+localizedStrings["Result Data"] = "Result Data";
+localizedStrings["Result Levels"] = "Result Levels";
 localizedStrings["Results"] = "Results";
 localizedStrings["Resume Processing"] = "Resume Processing";
 localizedStrings["Resume Thread"] = "Resume Thread";
@@ -887,6 +902,10 @@ localizedStrings["Tabs"] = "Tabs";
 localizedStrings["Tag"] = "Tag";
 localizedStrings["Take snapshot"] = "Take snapshot";
 localizedStrings["Template Content"] = "Template Content";
+localizedStrings["Tests for element accessibility issues."] = "Tests for element accessibility issues.";
+localizedStrings["Tests for element attribute accessibility issues."] = "Tests for element attribute accessibility issues.";
+localizedStrings["Tests for ways to improve accessibility."] = "Tests for ways to improve accessibility.";
+localizedStrings["Tests the accessibility of form elements."] = "Tests the accessibility of form elements.";
 localizedStrings["Text"] = "Text";
 localizedStrings["Text Frame"] = "Text Frame";
 localizedStrings["Text Node"] = "Text Node";
@@ -898,8 +917,19 @@ localizedStrings["The “%s“ audit passed"] = "The “%s“ audit passed";
 localizedStrings["The “%s“ audit threw an error"] = "The “%s“ audit threw an error";
 localizedStrings["The “%s“ audit warned"] = "The “%s“ audit warned";
 localizedStrings["The “%s”\ntable is empty."] = "The “%s”\ntable is empty.";
+localizedStrings["These are all of the different test result levels."] = "These are all of the different test result levels.";
+localizedStrings["These are all of the different types of data that can be returned with the test result."] = "These are all of the different types of data that can be returned with the test result.";
+localizedStrings["These tests serve as a demonstration of the functionality and structure of audits."] = "These tests serve as a demonstration of the functionality and structure of audits.";
 localizedStrings["This action causes no visual change"] = "This action causes no visual change";
 localizedStrings["This action moves the path outside the visible area"] = "This action moves the path outside the visible area";
+localizedStrings["This is an example of how errors are shown. The error was thrown manually, but execution errors will appear in the same way."] = "This is an example of how errors are shown. The error was thrown manually, but execution errors will appear in the same way.";
+localizedStrings["This is an example of how result DOM nodes are shown. It will pass with all elements with an id attribute."] = "This is an example of how result DOM nodes are shown. It will pass with all elements with an id attribute.";
+localizedStrings["This is an example of how result DOM nodes are shown. It will pass with the <body> element."] = "This is an example of how result DOM nodes are shown. It will pass with the <body> element.";
+localizedStrings["This is what the result of a failing test with no data looks like."] = "This is what the result of a failing test with no data looks like.";
+localizedStrings["This is what the result of a passing test with no data looks like."] = "This is what the result of a passing test with no data looks like.";
+localizedStrings["This is what the result of a test that threw an error with no data looks like."] = "This is what the result of a test that threw an error with no data looks like.";
+localizedStrings["This is what the result of a unsupported test with no data looks like."] = "This is what the result of a unsupported test with no data looks like.";
+localizedStrings["This is what the result of a warning test with no data looks like."] = "This is what the result of a warning test with no data looks like.";
 localizedStrings["This object is a root"] = "This object is a root";
 localizedStrings["This object is referenced by internal objects"] = "This object is referenced by internal objects";
 localizedStrings["This text resource could benefit from compression"] = "This text resource could benefit from compression";
index 561730e..2c7ae8d 100644 (file)
@@ -150,6 +150,8 @@ WI.AuditManager = class AuditManager extends WI.Object
 
                 this._addTest(test);
             }
+
+            this.addDefaultTestsIfNeeded();
         });
     }
 
@@ -192,6 +194,54 @@ WI.AuditManager = class AuditManager extends WI.Object
         for (let test of this._tests)
             test.clearResult();
     }
+
+    addDefaultTestsIfNeeded()
+    {
+        if (this._tests.length)
+            return;
+
+        const defaultTests = [
+            new WI.AuditTestGroup(WI.UIString("Demo Audit"), [
+                new WI.AuditTestGroup(WI.UIString("Result Levels"), [
+                    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.")}),
+                    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.")}),
+                    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.")}),
+                    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.")}),
+                    new WI.AuditTestCase(`level-unsupported`, `function() { return {level: "unsupported"}; }`, {description: WI.UIString("This is what the result of a unsupported test with no data looks like.")}),
+                ], {description: WI.UIString("These are all of the different test result levels.")}),
+                new WI.AuditTestGroup(WI.UIString("Result Data"), [
+                    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.")}),
+                    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.")}),
+                    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.")}),
+                ], {description: WI.UIString("These are all of the different types of data that can be returned with the test result.")}),
+            ], {description: WI.UIString("These tests serve as a demonstration of the functionality and structure of audits.")}),
+            new WI.AuditTestGroup(WI.UIString("Accessibility"), [
+                new WI.AuditTestGroup(WI.UIString("Attributes"), [
+                    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.")}),
+                    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 of image maps have alternate text.")}),
+                    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.")}),
+                    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 <area> elements of image maps have alternate text.")}),
+                    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 document body.")}),
+                    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.")}),
+                ], {description: WI.UIString("Tests for element attribute accessibility issues.")}),
+                new WI.AuditTestGroup(WI.UIString("Elements"), [
+                    new WI.AuditTestCase(`blink`, `function() { let domNodes = Array.from(document.getElementsByTagName("blink")); return { level: domNodes.length ? "warn" : "pass", domNodes }; }`, {description: WI.UIString("Ensure hidden=true is not present on the document body.")}),
+                    new WI.AuditTestCase(`marquee`, `function() { let domNodes = Array.from(document.getElementsByTagName("marquee")); return { level: domNodes.length ? "warn" : "pass", domNodes }; }`, {description: WI.UIString("Ensure hidden=true is not present on the document body.")}),
+                    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>.")}),
+                ], {description: WI.UIString("Tests for element accessibility issues.")}),
+                new WI.AuditTestGroup(WI.UIString("Forms"), [
+                    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.")}),
+                    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 legend is first child in form.")}),
+                    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 forms have at least one input.")}),
+                ], {description: WI.UIString("Tests the accessibility of form elements.")}),
+            ], {description: WI.UIString("Tests for ways to improve accessibility.")}),
+        ];
+
+        for (let test of defaultTests) {
+            this._addTest(test);
+            WI.objectStores.audits.addObject(test);
+        }
+    }
 };
 
 WI.AuditManager.RunningState = {
index 90d6eab..9fa88d8 100644 (file)
 .sidebar > .panel.navigation.audit > .content {
     top: var(--navigation-bar-height);
 }
+
+.sidebar > .panel.navigation.audit.has-results:not(.has-tests) > .content > .message-text-view {
+    position: initial;
+    border-bottom: 1px solid var(--border-color);
+}
+
+.sidebar > .panel.navigation.audit.has-results:not(.has-tests) > .content > .message-text-view > .message {
+    display: none;
+}
+
+.sidebar > .panel.navigation.audit.has-results:not(.has-tests) > .content > .message-text-view > button {
+    margin: 8px 0 7px;
+}
index 5658da7..2254d8f 100644 (file)
@@ -105,15 +105,21 @@ WI.AuditNavigationSidebarPanel = class AuditNavigationSidebarPanel extends WI.Na
 
     _addTest(test)
     {
+        this.element.classList.add("has-tests");
+
         this._updateStartStopButtonNavigationItemState();
 
         this.contentTreeOutline.insertChild(new WI.AuditTreeElement(test), this.contentTreeOutline.children.indexOf(this._resultsFolderTreeElement));
 
         this._resultsFolderTreeElement.hidden = !this._resultsFolderTreeElement.children.length;
+
+        this.hideEmptyContentPlaceholder();
     }
 
     _addResult(result, index)
     {
+        this.element.classList.add("has-results");
+
         this._updateStartStopButtonNavigationItemState();
 
         this._resultsFolderTreeElement.hidden = false;
@@ -152,6 +158,28 @@ WI.AuditNavigationSidebarPanel = class AuditNavigationSidebarPanel extends WI.Na
         let treeElement = this.treeElementForRepresentedObject(test);
         this.contentTreeOutline.removeChild(treeElement);
 
+        this.element.classList.toggle("has-tests", !!WI.auditManager.tests.length);
+
+        if (!WI.auditManager.tests.length) {
+            let contentPlaceholder = WI.createMessageTextView(WI.UIString("No audits"));
+
+            let defaultButtonElement = contentPlaceholder.appendChild(document.createElement("button"));
+            defaultButtonElement.textContent = WI.UIString("Add Default Audits");
+            defaultButtonElement.addEventListener("click", () => {
+                WI.auditManager.addDefaultTestsIfNeeded();
+            });
+
+            contentPlaceholder = this.showEmptyContentPlaceholder(contentPlaceholder);
+
+            if (WI.auditManager.results.length) {
+                console.assert(this.contentTreeOutline.children[0] === this._resultsFolderTreeElement);
+
+                // Move the placeholder to be the first element in the content area, where it will
+                // be styled such that only the button is visible.
+                this.contentView.element.insertBefore(contentPlaceholder, this.contentView.element.firstChild);
+            }
+        }
+
         this._updateStartStopButtonNavigationItemState();
     }
 
index 3ab1dba..5ebeae9 100644 (file)
@@ -267,12 +267,14 @@ WI.NavigationSidebarPanel = class NavigationSidebarPanel extends WI.SidebarPanel
 
         let emptyContentPlaceholderElement = this._createEmptyContentPlaceholderIfNeeded(treeOutline, message);
         if (emptyContentPlaceholderElement.parentNode)
-            return;
+            return emptyContentPlaceholderElement;
 
         let emptyContentPlaceholderParentElement = treeOutline.element.parentNode;
         emptyContentPlaceholderParentElement.appendChild(emptyContentPlaceholderElement);
 
         this._updateContentOverflowShadowVisibility();
+
+        return emptyContentPlaceholderElement;
     }
 
     hideEmptyContentPlaceholder(treeOutline)
@@ -725,7 +727,7 @@ WI.NavigationSidebarPanel = class NavigationSidebarPanel extends WI.SidebarPanel
         if (emptyContentPlaceholderElement)
             return emptyContentPlaceholderElement;
 
-        emptyContentPlaceholderElement = WI.createMessageTextView(message);
+        emptyContentPlaceholderElement = message instanceof Node ? message : WI.createMessageTextView(message);
         this._emptyContentPlaceholderElements.set(treeOutline, emptyContentPlaceholderElement);
 
         return emptyContentPlaceholderElement;