Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
71 commits
Select commit Hold shift + click to select a range
4cc44c1
Font Size audit - WIP
kdzwinel Sep 13, 2017
ad797fc
Calculate percentage of ilegible text on page. Show failing elements …
kdzwinel Sep 16, 2017
b2aa5c8
Getting font-size information togheter with associated CSS rules - WIP
kdzwinel Sep 17, 2017
64360dc
Matched rules - WIP
kdzwinel Sep 18, 2017
3f2effc
Extracting style info - WIP
kdzwinel Sep 20, 2017
fdf6473
Expose effective rule
kdzwinel Sep 26, 2017
35599d3
Show what percentage of text each rule affected. Show table with fail…
kdzwinel Sep 28, 2017
483a5c2
Work on inline styles, clean up jsdoc
kdzwinel Oct 2, 2017
9a10a10
Handle inline styels, attribute styles and user agent styles.
kdzwinel Oct 3, 2017
4b0863c
Expose stylesheet URL, line number and column.
kdzwinel Oct 4, 2017
005d308
Update output table according to the latst decisions.
kdzwinel Oct 10, 2017
a4ec14e
Changes to the result table. Clean up.
kdzwinel Oct 11, 2017
7298ae6
Gatherer test.
kdzwinel Oct 11, 2017
acb9aa8
Smoke tests. Fixing edgecase when size is inherited from attribute st…
kdzwinel Oct 11, 2017
a61106e
Add unit tests.
kdzwinel Oct 11, 2017
eaf454b
Remove info about color prop.
kdzwinel Oct 11, 2017
6d1b518
Remove debugMessage from passing test expectations as it doesn't exist.
kdzwinel Oct 11, 2017
e07a3f8
Replace URL with parseURL
kdzwinel Oct 12, 2017
242024d
Make linter happy.
kdzwinel Oct 12, 2017
3c66033
Check number of failing items in the smoke test.
kdzwinel Oct 15, 2017
99d89ba
getOrigin -> findStyleRuleSource
kdzwinel Oct 15, 2017
018e2bf
Collect stylesheet metadata in font-size gatherer. Get rid of separat…
kdzwinel Oct 15, 2017
4e1f171
Font Size audit - WIP
kdzwinel Sep 13, 2017
dc13c14
Calculate percentage of ilegible text on page. Show failing elements …
kdzwinel Sep 16, 2017
66d4b73
Getting font-size information togheter with associated CSS rules - WIP
kdzwinel Sep 17, 2017
507c78d
Matched rules - WIP
kdzwinel Sep 18, 2017
045fc6a
Extracting style info - WIP
kdzwinel Sep 20, 2017
696f926
Expose effective rule
kdzwinel Sep 26, 2017
1da6cfe
Show what percentage of text each rule affected. Show table with fail…
kdzwinel Sep 28, 2017
9051b92
Work on inline styles, clean up jsdoc
kdzwinel Oct 2, 2017
8596665
Handle inline styels, attribute styles and user agent styles.
kdzwinel Oct 3, 2017
64374a0
Expose stylesheet URL, line number and column.
kdzwinel Oct 4, 2017
312d030
Update output table according to the latst decisions.
kdzwinel Oct 10, 2017
1f7ddbb
Changes to the result table. Clean up.
kdzwinel Oct 11, 2017
1698dd7
Gatherer test.
kdzwinel Oct 11, 2017
97e1096
Smoke tests. Fixing edgecase when size is inherited from attribute st…
kdzwinel Oct 11, 2017
12fecc7
Add unit tests.
kdzwinel Oct 11, 2017
0929685
Remove info about color prop.
kdzwinel Oct 11, 2017
a0a08f7
Remove debugMessage from passing test expectations as it doesn't exist.
kdzwinel Oct 11, 2017
b3a99b9
Replace URL with parseURL
kdzwinel Oct 12, 2017
d45a313
Make linter happy.
kdzwinel Oct 12, 2017
e810e67
Check number of failing items in the smoke test.
kdzwinel Oct 15, 2017
1f4c6d2
getOrigin -> findStyleRuleSource
kdzwinel Oct 15, 2017
682b2c7
Collect stylesheet metadata in font-size gatherer. Get rid of separat…
kdzwinel Oct 15, 2017
e3007b2
Merge branch 'seo-font-size-rdp' of github.com:kdzwinel/lighthouse in…
kdzwinel Oct 29, 2017
26ea8c3
Merge remote-tracking branch 'upstream/master' into seo-font-size-rdp
kdzwinel Oct 29, 2017
a8b14f6
Address some of the review comments
kdzwinel Oct 29, 2017
a7fa017
Add comment, fix jsdoc
kdzwinel Oct 31, 2017
f93ad70
Merge remote-tracking branch 'upstream/master' into seo-font-size-rdp
kdzwinel Nov 19, 2017
9e2346e
Merge remote-tracking branch 'upstream/master' into seo-font-size-rdp
kdzwinel Nov 19, 2017
83f49c7
Limit number of CDP calls to make sure that gatherer doesn't take too…
kdzwinel Nov 25, 2017
94efb06
Reuse method from driver, introduce block list for text nodes
kdzwinel Nov 25, 2017
9e912de
Adjust tests and make linter happy.
kdzwinel Nov 27, 2017
cf25be4
A word.
kdzwinel Nov 27, 2017
ece62d2
Sort nodes by text length before limiting number of visited.
kdzwinel Nov 27, 2017
c017451
Merge branch 'master' into seo-font-size-rdp
kdzwinel Nov 29, 2017
93efc77
Typo
kdzwinel Nov 29, 2017
ae2c823
Typo 🔎
kdzwinel Dec 1, 2017
a6c6ecd
Move analyzedFailingTextLength calculation to the gatherer, rename fa…
kdzwinel Dec 3, 2017
68f896a
Add fileoverview to font-size gatherer
kdzwinel Dec 3, 2017
f2af51f
Merge remote-tracking branch 'upstream/master' into seo-font-size-rdp
kdzwinel Dec 5, 2017
007a458
Remove gatherers from seo config as they are already in default config.
kdzwinel Dec 5, 2017
0d5da12
Prevent gatherer from failing if one of the nodes can't be found
kdzwinel Dec 5, 2017
3b91408
Take into account that each property can be defined multiple times in…
kdzwinel Dec 6, 2017
ede0864
Update copy, remove audits from seo config, show 'dynamic' as source …
kdzwinel Dec 7, 2017
a588863
Fix test
kdzwinel Dec 7, 2017
06443e5
Fix tests
kdzwinel Dec 7, 2017
88091d7
Add simple selector to nodes in the details table.
kdzwinel Dec 8, 2017
5a71a58
Add'l 🚲🏠
kdzwinel Dec 12, 2017
3223cb5
Fix tests.
kdzwinel Dec 12, 2017
0b0cb9a
Additional info in the helpText
kdzwinel Dec 13, 2017
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions lighthouse-cli/test/fixtures/seo/seo-failure-cases.html
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@
<head>
<!-- no <title>SEO audit failures page</title> -->
<meta charset="utf-8">
<!-- FAIL(font-size): missing viewport -->
<meta name="viewport" content="invalid-content=should_have_looked_it_up">
<!-- no <meta name="description" content=""> -->
<meta name="robots" content="nofollow, NOINDEX, all">
Expand Down
16 changes: 16 additions & 0 deletions lighthouse-cli/test/fixtures/seo/seo-tester.html
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,12 @@
<LINK REL="ALTERNATE" HREFLANG="ru-RU" HREF="https://ru.example.com" />
<LINK REL="alternate" HREFLANG="zh-Hans-TW" HREF="https://zh.example.com" />
<link rel="alternate" href="http://example.com/" hreflang="x-default" />

<style>
.small {
font-size: 15px;
}
</style>
</head>
<body>
<h1>SEO</h1>
Expand All @@ -26,5 +32,15 @@ <h2>Anchor text</h2>
<a href='#non-descriptive-local'>click this</a>
<a href='https://example.com/non-descriptive-no-follow.html' rel="nofollow">click this</a>
<a href='javascript:void(0);'>click this</a>

<h2>Small text</h2>
<!-- PASS(font-size): amount of illegible text is below the 75% threshold -->
<p class='small'> 1 </p>
<small>2</small>
<font size="1">3<b>4</b></font>
<p style='font-size:10px'>5 </p>
<script class='small'>
// text from SCRIPT tags should be ignored by the font-size audit
</script>
</body>
</html>
12 changes: 12 additions & 0 deletions lighthouse-cli/test/smokehouse/seo/expectations.js
Original file line number Diff line number Diff line change
Expand Up @@ -40,6 +40,14 @@ module.exports = [
'http-status-code': {
score: true,
},
'font-size': {
rawValue: true,
details: {
items: {
length: 6,
},
},
},
'link-text': {
score: true,
},
Expand Down Expand Up @@ -73,6 +81,10 @@ module.exports = [
score: false,
displayValue: '403',
},
'font-size': {
rawValue: false,
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

perhaps we can assert how many elements fail too? details: { items: { length: x }}

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Here we fail because viewport is not configured (see debugString below), so we don't even go into evaluating text size and details will be empty. Viewport is empty on this page because of the viewport audit that we also want to fail here.

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

yeah I figured that out last time, but who knows what I was thinking 18 days ago :)

debugString: 'Text is illegible because of a missing viewport config',
},
'link-text': {
score: false,
displayValue: '3 links found',
Expand Down
286 changes: 286 additions & 0 deletions lighthouse-core/audits/seo/font-size.js
Original file line number Diff line number Diff line change
@@ -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);
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

perhaps something got thrown off between the iterations but it seems like analyzed text nodes aren't always getting de-duped anymore?

image

rule is in CSS
image

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Turned out not to be a regression but rather bug that was there from the beginning. Note that font-size is defined twice in this rule and I've been only examining first occurrence in getEffectiveRule:

font-size-oh-no

This was causing effective rule not to be found and every node listed as a separate entry in the details table.

Fixed 🔧

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

ah ok, great catch!

const failingRule = failingRules.get(artifactId);

if (!failingRule) {
failingRules.set(artifactId, {
node,
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

will all duplicate rules correspond to the same node? or is this the optional node that's only set for inline style?

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It's always set, but it only matters for inline (font-size: 10px) or attribute (<font size='1'>) styles (where we want to show a node in the audit details). For CSS rules we don't show affected nodes so we don't have to collect them.

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

ah ok and in the inline and attribute cases it's obviously the same node since that's the source, gotcha

cssRule,
fontSize,
textLength,
});
} else {
failingRule.textLength += textLength;
}
});

return failingRules.valuesArray();
}

/**
* @param {Array<string>} attributes
* @returns {Map<string, string>}
*/
function getAttributeMap(attributes) {
const map = new Map();

for (let i=0; i<attributes.length; i+=2) {
const name = attributes[i].toLowerCase();
const value = attributes[i + 1].trim();

if (value) {
map.set(name, value);
}
}

return map;
}

/**
* TODO: return unique selector, like axe-core does, instead of just id/class/name of a single node
* @param {Node} node
* @returns {string}
*/
function getSelector(node) {
const attributeMap = getAttributeMap(node.attributes);

if (attributeMap.has('id')) {
return '#' + attributeMap.get('id');
} else if (attributeMap.has('class')) {
return '.' + attributeMap.get('class').split(/\s+/).join('.');
}

return node.localName.toLowerCase();
}

/**
* @param {Node} node
* @return {{type:string, selector: string, snippet:string}}
*/
function nodeToTableNode(node) {
const attributesString = node.attributes.map((value, idx) =>
(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',
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

this seems like an unfortunate situation to show to a user, is the default on mobile illegible text?

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Default UA font size value is legible (1em, which by default is 16px), but there are multiple tags with font-size < 1em, e.g. <h5>, <small> or <sub>.

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

got it

};
}

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 <style> block (stylesheet.startLine will vary)
const absoluteStartLine = range.startLine + stylesheet.startLine + 1;
const absoluteStartColumn = range.startColumn + stylesheet.startColumn + 1;

source += `:${absoluteStartLine}:${absoluteStartColumn}`;
}
} else {
// dynamically injected to page
source = 'dynamic';
}

return {
selector,
source,
};
}
}

return {
source: 'Unknown',
};
}

/**
* @param {WebInspector.CSSStyleDeclaration} styleDeclaration
* @param {Node} node
* @return string
*/
function getFontArtifactId(styleDeclaration, node) {
if (styleDeclaration && styleDeclaration.type === CSSStyleDeclaration.Type.Regular) {
const startLine = styleDeclaration.range ? styleDeclaration.range.startLine : 0;
const startColumn = styleDeclaration.range ? styleDeclaration.range.startColumn : 0;
return `${styleDeclaration.styleSheetId}@${startLine}:${startColumn}`;
} else {
return `node_${node.nodeId}`;
}
}

class FontSize extends Audit {
/**
* @return {!AuditMeta}
*/
static get meta() {
return {
name: 'font-size',
description: 'Document uses legible font sizes.',
failureDescription: 'Document doesn\'t use legible font sizes.',
helpText: 'Font sizes less than 16px are too small to be legible and require mobile ' +
'visitors to “pinch to zoom” in order to read. Strive to have >75% of page text ≥16px. ' +
'[Learn more](https://developers.google.com/speed/docs/insights/UseLegibleFontSizes).',
requiredArtifacts: ['FontSize', 'URL', 'Viewport'],
};
}

/**
* @param {!Artifacts} artifacts
* @return {!AuditResult}
*/
static audit(artifacts) {
const hasViewportSet = ViewportAudit.audit(artifacts).rawValue;
if (!hasViewportSet) {
return {
rawValue: false,
debugString: 'Text is illegible because of a missing viewport config',
};
}

const {
analyzedFailingNodesData,
analyzedFailingTextLength,
failingTextLength,
visitedTextLength,
totalTextLength,
} = artifacts.FontSize;

if (totalTextLength === 0) {
return {
rawValue: true,
};
}

const failingRules = getUniqueFailingRules(analyzedFailingNodesData);
const percentageOfPassingText =
(visitedTextLength - failingTextLength) / visitedTextLength * 100;
const pageUrl = artifacts.URL.finalUrl;

const headings = [
{key: 'source', itemType: 'url', text: 'Source'},
{key: 'selector', itemType: 'code', text: 'Selector'},
{key: 'coverage', itemType: 'text', text: '% of Page Text'},
{key: 'fontSize', itemType: 'text', text: 'Font Size'},
];

const tableData = failingRules.sort((a, b) => b.textLength - a.textLength)
.map(({cssRule, textLength, fontSize, node}) => {
const percentageOfAffectedText = textLength / visitedTextLength * 100;
const origin = findStyleRuleSource(pageUrl, cssRule, node);

return {
source: origin.source,
selector: origin.selector,
coverage: `${percentageOfAffectedText.toFixed(2)}%`,
fontSize: `${fontSize}px`,
};
});

// all failing nodes that were not fully analyzed will be displayed in a single row
if (analyzedFailingTextLength < failingTextLength) {
const percentageOfUnanalyzedFailingText =
(failingTextLength - analyzedFailingTextLength) / visitedTextLength * 100;

tableData.push({
source: 'Add\'l illegible text',
selector: null,
coverage: `${percentageOfUnanalyzedFailingText.toFixed(2)}%`,
fontSize: '< 16px',
});
}

if (percentageOfPassingText > 0) {
tableData.push({
source: 'Legible text',
selector: null,
coverage: `${percentageOfPassingText.toFixed(2)}%`,
fontSize: '≥ 16px',
});
}

const details = Audit.makeTableDetails(headings, tableData);
const passed = percentageOfPassingText >= MINIMAL_PERCENTAGE_OF_LEGIBLE_TEXT;
let debugString = null;

if (!passed) {
const percentageOfFailingText = parseFloat((100 - percentageOfPassingText).toFixed(2));
let disclaimer = '';

// if we were unable to visit all text nodes we should disclose that information
if (visitedTextLength < totalTextLength) {
const percentageOfVisitedText = visitedTextLength / totalTextLength * 100;
disclaimer = ` (based on ${percentageOfVisitedText.toFixed()}% sample)`;
}

debugString = `${percentageOfFailingText}% of text is too small${disclaimer}.`;
}

return {
rawValue: passed,
details,
debugString,
};
}
}

module.exports = FontSize;
2 changes: 2 additions & 0 deletions lighthouse-core/config/default.js
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,7 @@ module.exports = {
'dobetterweb/tags-blocking-first-paint',
'dobetterweb/websql',
'seo/meta-description',
'seo/font-size',
'seo/crawlable-links',
'seo/meta-robots',
'seo/hreflang',
Expand Down Expand Up @@ -164,6 +165,7 @@ module.exports = {
'dobetterweb/uses-passive-event-listeners',
'seo/meta-description',
'seo/http-status-code',
'seo/font-size',
'seo/link-text',
'seo/is-crawlable',
'seo/hreflang',
Expand Down
16 changes: 3 additions & 13 deletions lighthouse-core/config/seo.js
Original file line number Diff line number Diff line change
Expand Up @@ -9,20 +9,9 @@ module.exports = {
extends: 'lighthouse:default',
passes: [{
passName: 'defaultPass',
gatherers: [
'seo/meta-description',
'seo/crawlable-links',
'seo/meta-robots',
'seo/hreflang',
],
gatherers: [],
}],
audits: [
'seo/meta-description',
'seo/http-status-code',
'seo/link-text',
'seo/is-crawlable',
'seo/hreflang',
],
audits: [],
groups: {
'seo-mobile': {
title: 'Mobile Friendly',
Expand All @@ -47,6 +36,7 @@ module.exports = {
{id: 'document-title', weight: 1, group: 'seo-content'},
{id: 'meta-description', weight: 1, group: 'seo-content'},
{id: 'http-status-code', weight: 1, group: 'seo-crawl'},
{id: 'font-size', weight: 1, group: 'seo-mobile'},
{id: 'link-text', weight: 1, group: 'seo-content'},
{id: 'is-crawlable', weight: 1, group: 'seo-crawl'},
{id: 'hreflang', weight: 1, group: 'seo-content'},
Expand Down
Loading