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
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";
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";
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: ";
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";
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.";
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";
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";
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";
this._addTest(test);
}
+
+ this.addDefaultTestsIfNeeded();
});
}
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 = {
.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;
+}
_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;
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();
}
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)
if (emptyContentPlaceholderElement)
return emptyContentPlaceholderElement;
- emptyContentPlaceholderElement = WI.createMessageTextView(message);
+ emptyContentPlaceholderElement = message instanceof Node ? message : WI.createMessageTextView(message);
this._emptyContentPlaceholderElements.set(treeOutline, emptyContentPlaceholderElement);
return emptyContentPlaceholderElement;