diff --git a/lighthouse-core/config/config.js b/lighthouse-core/config/config.js index fa30a9917915..f078396f9e54 100644 --- a/lighthouse-core/config/config.js +++ b/lighthouse-core/config/config.js @@ -150,7 +150,7 @@ function validatePasses(passes, audits, rootPath) { }); } -function validateCategories(categories, audits, auditResults) { +function validateCategories(categories, audits, auditResults, groups) { if (!categories) { return; } @@ -167,6 +167,14 @@ function validateCategories(categories, audits, auditResults) { if (!auditIds.includes(audit.id)) { throw new Error(`could not find ${audit.id} audit for category ${categoryId}`); } + + if (categoryId === 'accessibility' && !audit.group) { + throw new Error(`${audit.id} accessibility audit does not have a group`); + } + + if (audit.group && !groups[audit.group]) { + throw new Error(`${audit.id} references unknown group ${audit.group}`); + } }); }); } @@ -321,10 +329,11 @@ class Config { this._audits = Config.requireAudits(configJSON.audits, this._configDir); this._artifacts = expandArtifacts(configJSON.artifacts); this._categories = configJSON.categories; + this._groups = configJSON.groups; // validatePasses must follow after audits are required validatePasses(configJSON.passes, this._audits, this._configDir); - validateCategories(configJSON.categories, this._audits, this._auditResults); + validateCategories(configJSON.categories, this._audits, this._auditResults, this._groups); } /** @@ -573,6 +582,11 @@ class Config { get categories() { return this._categories; } + + /** @type {Object|undefined} */ + get groups() { + return this._groups; + } } module.exports = Config; diff --git a/lighthouse-core/config/default.js b/lighthouse-core/config/default.js index 17b54b2e253a..270b85798c36 100644 --- a/lighthouse-core/config/default.js +++ b/lighthouse-core/config/default.js @@ -587,6 +587,40 @@ module.exports = { } }] }], + "groups": { + "a11y-color-contrast": { + "title": "Color Contrast Is Satisfactory", + "description": "Screen readers and other assitive technologies require annotations to understand otherwise ambiguous content." + }, + "a11y-describe-contents": { + "title": "Elements Describe Contents Well", + "description": "Screen readers and other assitive technologies require annotations to understand otherwise ambiguous content." + }, + "a11y-well-structured": { + "title": "Elements Are Well Structured", + "description": "Screen readers and other assitive technologies require annotations to understand otherwise ambiguous content." + }, + "a11y-aria": { + "title": "ARIA Attributes Follow Best Practices", + "description": "Screen readers and other assitive technologies require annotations to understand otherwise ambiguous content." + }, + "a11y-correct-attributes": { + "title": "Elements Use Attributes Correctly", + "description": "Screen readers and other assitive technologies require annotations to understand otherwise ambiguous content." + }, + "a11y-element-names": { + "title": "Elements Have Discernable Names", + "description": "Screen readers and other assitive technologies require annotations to understand otherwise ambiguous content." + }, + "a11y-language": { + "title": "Page Specifies Valid Language", + "description": "Screen readers and other assitive technologies require annotations to understand otherwise ambiguous content." + }, + "a11y-meta": { + "title": "Meta Tags Used Properly", + "description": "Screen readers and other assitive technologies require annotations to understand otherwise ambiguous content." + }, + }, "categories": { "pwa": { "name": "Progressive Web App", @@ -631,41 +665,41 @@ module.exports = { "name": "Accessibility", "description": "These checks highlight opportunities to [improve the accessibility of your app](https://developers.google.com/web/fundamentals/accessibility).", "audits": [ - {"id": "accesskeys", "weight": 1}, - {"id": "aria-allowed-attr", "weight": 1}, - {"id": "aria-required-attr", "weight": 1}, - {"id": "aria-required-children", "weight": 1}, - {"id": "aria-required-parent", "weight": 1}, - {"id": "aria-roles", "weight": 1}, - {"id": "aria-valid-attr-value", "weight": 1}, - {"id": "aria-valid-attr", "weight": 1}, - {"id": "audio-caption", "weight": 1}, - {"id": "button-name", "weight": 1}, - {"id": "bypass", "weight": 1}, - {"id": "color-contrast", "weight": 1}, - {"id": "definition-list", "weight": 1}, - {"id": "dlitem", "weight": 1}, - {"id": "document-title", "weight": 1}, - {"id": "duplicate-id", "weight": 1}, - {"id": "frame-title", "weight": 1}, - {"id": "html-has-lang", "weight": 1}, - {"id": "html-lang-valid", "weight": 1}, - {"id": "image-alt", "weight": 1}, - {"id": "input-image-alt", "weight": 1}, - {"id": "label", "weight": 1}, - {"id": "layout-table", "weight": 1}, - {"id": "link-name", "weight": 1}, - {"id": "list", "weight": 1}, - {"id": "listitem", "weight": 1}, - {"id": "meta-refresh", "weight": 1}, - {"id": "meta-viewport", "weight": 1}, - {"id": "object-alt", "weight": 1}, - {"id": "tabindex", "weight": 1}, - {"id": "td-headers-attr", "weight": 1}, - {"id": "th-has-data-cells", "weight": 1}, - {"id": "valid-lang", "weight": 1}, - {"id": "video-caption", "weight": 1}, - {"id": "video-description", "weight": 1}, + {"id": "accesskeys", "weight": 1, "group": "a11y-correct-attributes"}, + {"id": "aria-allowed-attr", "weight": 1, "group": "a11y-aria"}, + {"id": "aria-required-attr", "weight": 1, "group": "a11y-aria"}, + {"id": "aria-required-children", "weight": 1, "group": "a11y-aria"}, + {"id": "aria-required-parent", "weight": 1, "group": "a11y-aria"}, + {"id": "aria-roles", "weight": 1, "group": "a11y-aria"}, + {"id": "aria-valid-attr-value", "weight": 1, "group": "a11y-aria"}, + {"id": "aria-valid-attr", "weight": 1, "group": "a11y-aria"}, + {"id": "audio-caption", "weight": 1, "group": "a11y-correct-attributes"}, + {"id": "button-name", "weight": 1, "group": "a11y-element-names"}, + {"id": "bypass", "weight": 1, "group": "a11y-describe-contents"}, + {"id": "color-contrast", "weight": 1, "group": "a11y-color-contrast"}, + {"id": "definition-list", "weight": 1, "group": "a11y-well-structured"}, + {"id": "dlitem", "weight": 1, "group": "a11y-well-structured"}, + {"id": "document-title", "weight": 1, "group": "a11y-describe-contents"}, + {"id": "duplicate-id", "weight": 1, "group": "a11y-well-structured"}, + {"id": "frame-title", "weight": 1, "group": "a11y-describe-contents"}, + {"id": "html-has-lang", "weight": 1, "group": "a11y-language"}, + {"id": "html-lang-valid", "weight": 1, "group": "a11y-language"}, + {"id": "image-alt", "weight": 1, "group": "a11y-correct-attributes"}, + {"id": "input-image-alt", "weight": 1, "group": "a11y-correct-attributes"}, + {"id": "label", "weight": 1, "group": "a11y-describe-contents"}, + {"id": "layout-table", "weight": 1, "group": "a11y-describe-contents"}, + {"id": "link-name", "weight": 1, "group": "a11y-element-names"}, + {"id": "list", "weight": 1, "group": "a11y-well-structured"}, + {"id": "listitem", "weight": 1, "group": "a11y-well-structured"}, + {"id": "meta-refresh", "weight": 1, "group": "a11y-meta"}, + {"id": "meta-viewport", "weight": 1, "group": "a11y-meta"}, + {"id": "object-alt", "weight": 1, "group": "a11y-describe-contents"}, + {"id": "tabindex", "weight": 1, "group": "a11y-correct-attributes"}, + {"id": "td-headers-attr", "weight": 1, "group": "a11y-correct-attributes"}, + {"id": "th-has-data-cells", "weight": 1, "group": "a11y-correct-attributes"}, + {"id": "valid-lang", "weight": 1, "group": "a11y-language"}, + {"id": "video-caption", "weight": 1, "group": "a11y-describe-contents"}, + {"id": "video-description", "weight": 1, "group": "a11y-describe-contents"}, ] }, "best-practices": { diff --git a/lighthouse-core/report/v2/renderer/category-renderer.js b/lighthouse-core/report/v2/renderer/category-renderer.js index fa662daf03ad..204365a43d1f 100644 --- a/lighthouse-core/report/v2/renderer/category-renderer.js +++ b/lighthouse-core/report/v2/renderer/category-renderer.js @@ -144,9 +144,23 @@ class CategoryRenderer { /** * @param {!ReportRenderer.CategoryJSON} category + * @param {!Object} groups * @return {!Element} */ - render(category) { + render(category, groups) { + switch (category.id) { + case 'accessibility': + return this._renderAccessibilityCategory(category, groups); + default: + return this._renderDefaultCategory(category); + } + } + + /** + * @param {!ReportRenderer.CategoryJSON} category + * @return {!Element} + */ + _renderDefaultCategory(category) { const element = this._dom.createElement('div', 'lh-category'); element.id = category.id; element.appendChild(this._renderCategoryScore(category)); @@ -174,6 +188,87 @@ class CategoryRenderer { element.appendChild(passedElem); return element; } + + /** + * @param {!Array} audits + * @param {!ReportRenderer.GroupJSON} group + * @return {!Element} + */ + _renderAuditGroup(audits, group) { + const auditGroupElem = this._dom.createElement('details', + 'lh-audit-group lh-expandable-details'); + const auditGroupHeader = this._dom.createElement('div', + 'lh-audit-group__header lh-expandable-details__header'); + auditGroupHeader.textContent = group.title; + + const auditGroupDescription = this._dom.createElement('div', 'lh-audit-group__description'); + auditGroupDescription.textContent = group.description; + + const auditGroupSummary = this._dom.createElement('summary', + 'lh-audit-group__summary lh-expandable-details__summary'); + const auditGroupArrow = this._dom.createElement('div', 'lh-toggle-arrow', { + title: 'See audits', + }); + auditGroupSummary.appendChild(auditGroupHeader); + auditGroupSummary.appendChild(auditGroupArrow); + + auditGroupElem.appendChild(auditGroupSummary); + auditGroupElem.appendChild(auditGroupDescription); + audits.forEach(audit => auditGroupElem.appendChild(this._renderAudit(audit))); + return auditGroupElem; + } + + /** + * @param {!ReportRenderer.CategoryJSON} category + * @param {!Object} groups + * @return {!Element} + */ + _renderAccessibilityCategory(category, groupDefinitions) { + const element = this._dom.createElement('div', 'lh-category'); + element.id = category.id; + element.appendChild(this._renderCategoryScore(category)); + + const auditsGroupedByGroup = category.audits.reduce((indexed, audit) => { + const groupId = audit.group; + const groups = indexed[groupId] || {passed: [], failed: []}; + + if (audit.score === 100) { + groups.passed.push(audit); + } else { + groups.failed.push(audit); + } + + indexed[groupId] = groups; + return indexed; + }, {}); + + const passedElements = []; + Object.keys(auditsGroupedByGroup).forEach(groupId => { + const group = groupDefinitions[groupId]; + const groups = auditsGroupedByGroup[groupId]; + if (groups.failed.length) { + const auditGroupElem = this._renderAuditGroup(groups.failed, group); + auditGroupElem.open = true; + element.appendChild(auditGroupElem); + } + + if (groups.passed.length) { + const auditGroupElem = this._renderAuditGroup(groups.passed, group); + passedElements.push(auditGroupElem); + } + }); + + // don't create a passed section if there are no passed + if (!passedElements.length) return element; + + const passedElem = this._dom.createElement('details', 'lh-passed-audits'); + const passedSummary = this._dom.createElement('summary', 'lh-passed-audits-summary'); + passedElem.appendChild(passedSummary); + passedSummary.textContent = `View ${passedElements.length} passed items`; + passedElements.forEach(elem => passedElem.appendChild(elem)); + element.appendChild(passedElem); + return element; + } } if (typeof module !== 'undefined' && module.exports) { diff --git a/lighthouse-core/report/v2/renderer/report-renderer.js b/lighthouse-core/report/v2/renderer/report-renderer.js index f5318cdaa404..bea3f9cad0e8 100644 --- a/lighthouse-core/report/v2/renderer/report-renderer.js +++ b/lighthouse-core/report/v2/renderer/report-renderer.js @@ -149,7 +149,7 @@ class ReportRenderer { const categories = reportSection.appendChild(this._dom.createElement('div', 'lh-categories')); for (const category of report.reportCategories) { scoreHeader.appendChild(this._categoryRenderer.renderScoreGauge(category)); - categories.appendChild(this._categoryRenderer.render(category)); + categories.appendChild(this._categoryRenderer.render(category, report.reportGroups)); } reportSection.appendChild(this._renderReportFooter(report)); @@ -169,6 +169,7 @@ if (typeof module !== 'undefined' && module.exports) { * id: string, * weight: number, * score: number, + * group: string, * result: { * description: string, * debugString: string, @@ -195,6 +196,14 @@ ReportRenderer.AuditJSON; // eslint-disable-line no-unused-expressions */ ReportRenderer.CategoryJSON; // eslint-disable-line no-unused-expressions +/** + * @typedef {{ + * title: string, + * description: string, + * }} + */ +ReportRenderer.GroupJSON; // eslint-disable-line no-unused-expressions + /** * @typedef {{ * lighthouseVersion: string, @@ -202,6 +211,7 @@ ReportRenderer.CategoryJSON; // eslint-disable-line no-unused-expressions * initialUrl: string, * url: string, * reportCategories: !Array, + * reportGroups: !Object, * runtimeConfig: { * blockedUrlPatterns: !Array, * environment: !Array<{description: string, enabled: boolean, name: string}> diff --git a/lighthouse-core/report/v2/report-styles.css b/lighthouse-core/report/v2/report-styles.css index 9fa00037f0b4..bf55dba1c9aa 100644 --- a/lighthouse-core/report/v2/report-styles.css +++ b/lighthouse-core/report/v2/report-styles.css @@ -241,13 +241,14 @@ summary { margin-top: calc(var(--default-padding) / 2); } -.lh-score__header { - flex: 1; - margin-top: 2px; +.lh-score__snippet { + align-items: center; + justify-content: space-between; + /*outline: none;*/ } -.lh-score__header[open] .lh-toggle-arrow { - transform: rotateZ(90deg); +.lh-score__snippet::-moz-list-bullet { + display: none; } .lh-toggle-arrow { @@ -263,18 +264,27 @@ summary { border: none; } -.lh-score__snippet { +/* Expandable Details (Audit Groups, Audits) */ + +.lh-expandable-details { + flex: 1; + margin-top: 2px; +} + +.lh-expandable-details__summary { display: flex; - align-items: center; - justify-content: space-between; - /*outline: none;*/ + cursor: pointer; } -.lh-score__snippet::-moz-list-bullet { - display: none; +.lh-expandable-details__header { + flex: 1; } -.lh-score__snippet::-webkit-details-marker { +.lh-expandable-details[open] > .lh-expandable-details__summary > .lh-toggle-arrow { + transform: rotateZ(90deg); +} + +.lh-expandable-details__summary::-webkit-details-marker { display: none; } @@ -293,6 +303,22 @@ summary { font-size: var(--header-font-size); } +/* Audit Group */ + +.lh-audit-group { + margin-top: var(--default-padding); +} + +.lh-audit-group__header { + font-size: 18px; +} + +.lh-audit-group__description { + font-size: 16px; + color: var(--secondary-text-color); + margin-top: calc(var(--default-padding) / 2); +} + .lh-debug { margin-top: calc(var(--default-padding) / 2); margin-left: calc(var(--lh-audit-score-width) + var(--lh-score-margin)); @@ -334,8 +360,8 @@ summary { border: none; } -.lh-category > .lh-audit, -.lh-category > .lh-passed-audits > .lh-audit { +.lh-category .lh-audit, +.lh-category .lh-audit-group { margin-left: calc(var(--lh-category-score-width) + var(--lh-score-margin)); } diff --git a/lighthouse-core/report/v2/templates.html b/lighthouse-core/report/v2/templates.html index f58a31242c2d..d0c27a8ae5cc 100644 --- a/lighthouse-core/report/v2/templates.html +++ b/lighthouse-core/report/v2/templates.html @@ -15,8 +15,8 @@