diff --git a/lighthouse-cli/test/fixtures/seo/seo-failure-cases.html b/lighthouse-cli/test/fixtures/seo/seo-failure-cases.html index 062c31298314..c27b8a7aa7cd 100644 --- a/lighthouse-cli/test/fixtures/seo/seo-failure-cases.html +++ b/lighthouse-cli/test/fixtures/seo/seo-failure-cases.html @@ -9,6 +9,7 @@ + diff --git a/lighthouse-cli/test/fixtures/seo/seo-tester.html b/lighthouse-cli/test/fixtures/seo/seo-tester.html index 442ac3f7c04c..3b5959e4ab10 100644 --- a/lighthouse-cli/test/fixtures/seo/seo-tester.html +++ b/lighthouse-cli/test/fixtures/seo/seo-tester.html @@ -17,6 +17,12 @@ + +

SEO

@@ -26,5 +32,15 @@

Anchor text

click this click this click this + +

Small text

+ +

1

+ 2 + 34 +

5

+ diff --git a/lighthouse-cli/test/smokehouse/seo/expectations.js b/lighthouse-cli/test/smokehouse/seo/expectations.js index ae55871da27f..7a631e4fffff 100644 --- a/lighthouse-cli/test/smokehouse/seo/expectations.js +++ b/lighthouse-cli/test/smokehouse/seo/expectations.js @@ -40,6 +40,14 @@ module.exports = [ 'http-status-code': { score: true, }, + 'font-size': { + rawValue: true, + details: { + items: { + length: 6, + }, + }, + }, 'link-text': { score: true, }, @@ -73,6 +81,10 @@ module.exports = [ score: false, displayValue: '403', }, + 'font-size': { + rawValue: false, + debugString: 'Text is illegible because of a missing viewport config', + }, 'link-text': { score: false, displayValue: '3 links found', diff --git a/lighthouse-core/audits/seo/font-size.js b/lighthouse-core/audits/seo/font-size.js new file mode 100644 index 000000000000..11ad70526b30 --- /dev/null +++ b/lighthouse-core/audits/seo/font-size.js @@ -0,0 +1,286 @@ +/** + * @license Copyright 2017 Google Inc. All Rights Reserved. + * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 + * Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. + */ +'use strict'; + +const parseURL = require('url').parse; +const Audit = require('../audit'); +const ViewportAudit = require('../viewport'); +const CSSStyleDeclaration = require('../../lib/web-inspector').CSSStyleDeclaration; +const MINIMAL_PERCENTAGE_OF_LEGIBLE_TEXT = 75; + +/** + * @param {Array<{cssRule: WebInspector.CSSStyleDeclaration, fontSize: number, textLength: number, node: Node}>} fontSizeArtifact + * @returns {Array<{cssRule: WebInspector.CSSStyleDeclaration, fontSize: number, textLength: number, node: Node}>} + */ +function getUniqueFailingRules(fontSizeArtifact) { + const failingRules = new Map(); + + fontSizeArtifact.forEach(({cssRule, fontSize, textLength, node}) => { + const artifactId = getFontArtifactId(cssRule, node); + const failingRule = failingRules.get(artifactId); + + if (!failingRule) { + failingRules.set(artifactId, { + node, + cssRule, + fontSize, + textLength, + }); + } else { + failingRule.textLength += textLength; + } + }); + + return failingRules.valuesArray(); +} + +/** + * @param {Array} attributes + * @returns {Map} + */ +function getAttributeMap(attributes) { + const map = new Map(); + + for (let i=0; i + (idx % 2 === 0) ? ` ${value}` : `="${value}"` + ).join(''); + + return { + type: 'node', + selector: node.parentNode ? getSelector(node.parentNode) : '', + snippet: `<${node.localName}${attributesString}>`, + }; +} + +/** + * @param {string} baseURL + * @param {WebInspector.CSSStyleDeclaration} styleDeclaration + * @param {Node} node + * @returns {{source:!string, selector:string|object}} + */ +function findStyleRuleSource(baseURL, styleDeclaration, node) { + if ( + !styleDeclaration || + styleDeclaration.type === CSSStyleDeclaration.Type.Attributes || + styleDeclaration.type === CSSStyleDeclaration.Type.Inline + ) { + return { + source: baseURL, + selector: nodeToTableNode(node), + }; + } + + if (styleDeclaration.parentRule && + styleDeclaration.parentRule.origin === global.CSSAgent.StyleSheetOrigin.USER_AGENT) { + return { + selector: styleDeclaration.parentRule.selectors.map(item => item.text).join(', '), + source: 'User Agent Stylesheet', + }; + } + + if (styleDeclaration.type === CSSStyleDeclaration.Type.Regular && styleDeclaration.parentRule) { + const rule = styleDeclaration.parentRule; + const stylesheet = styleDeclaration.stylesheet; + + if (stylesheet) { + let source; + const selector = rule.selectors.map(item => item.text).join(', '); + + if (stylesheet.sourceURL) { + const url = parseURL(stylesheet.sourceURL, baseURL); + const range = styleDeclaration.range; + source = `${url.href}`; + + if (range) { + // `stylesheet` can be either an external file (stylesheet.startLine will always be 0), + // or a