lighthouse 9.3.1-dev.20220210 → 9.3.1-dev.20220214
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/report/bundle.esm.js +13 -0
- package/dist/report/flow.js +2 -2
- package/dist/report/standalone.js +7 -7
- package/lighthouse-cli/test/smokehouse/core-tests.js +2 -3
- package/lighthouse-core/audits/autocomplete.js +34 -37
- package/lighthouse-core/config/default-config.js +1 -1
- package/lighthouse-core/fraggle-rock/config/default-config.js +3 -3
- package/lighthouse-core/gather/gatherers/inputs.js +113 -0
- package/lighthouse-core/util-commonjs.js +4 -0
- package/package.json +1 -1
- package/report/renderer/report-ui-features.js +9 -0
- package/report/renderer/util.js +4 -0
- package/report/test/renderer/__snapshots__/report-renderer-axe-test.js.snap +107 -0
- package/report/test/renderer/report-renderer-axe-test.js +38 -17
- package/report/types/report-renderer.d.ts +7 -0
- package/shared/localization/locales/en-US.json +6 -0
- package/shared/localization/locales/en-XL.json +6 -0
- package/types/artifacts.d.ts +14 -14
- package/lighthouse-core/gather/gatherers/form-elements.js +0 -113
|
@@ -14,7 +14,7 @@ import dbw from './test-definitions/dobetterweb.js';
|
|
|
14
14
|
import errorsExpiredSsl from './test-definitions/errors-expired-ssl.js';
|
|
15
15
|
import errorsIframeExpiredSsl from './test-definitions/errors-iframe-expired-ssl.js';
|
|
16
16
|
import errorsInfiniteLoop from './test-definitions/errors-infinite-loop.js';
|
|
17
|
-
|
|
17
|
+
import formsAutoComplete from './test-definitions/forms-autocomplete.js';
|
|
18
18
|
import issuesMixedContent from './test-definitions/issues-mixed-content.js';
|
|
19
19
|
import lanternFetch from './test-definitions/lantern-fetch.js';
|
|
20
20
|
import lanternIdleCallbackLong from './test-definitions/lantern-idle-callback-long.js';
|
|
@@ -72,8 +72,7 @@ const smokeTests = [
|
|
|
72
72
|
errorsExpiredSsl,
|
|
73
73
|
errorsIframeExpiredSsl,
|
|
74
74
|
errorsInfiniteLoop,
|
|
75
|
-
|
|
76
|
-
// formsAutoComplete,
|
|
75
|
+
formsAutoComplete,
|
|
77
76
|
issuesMixedContent,
|
|
78
77
|
lanternOnline,
|
|
79
78
|
lanternSetTimeout,
|
|
@@ -195,12 +195,12 @@ class AutocompleteAudit extends Audit {
|
|
|
195
195
|
title: str_(UIStrings.title),
|
|
196
196
|
failureTitle: str_(UIStrings.failureTitle),
|
|
197
197
|
description: str_(UIStrings.description),
|
|
198
|
-
requiredArtifacts: ['
|
|
198
|
+
requiredArtifacts: ['Inputs'],
|
|
199
199
|
};
|
|
200
200
|
}
|
|
201
201
|
|
|
202
202
|
/**
|
|
203
|
-
* @param {LH.Artifacts.
|
|
203
|
+
* @param {LH.Artifacts.InputElement} input
|
|
204
204
|
* @return {{hasValidTokens: boolean, isValidOrder?: boolean}}
|
|
205
205
|
*/
|
|
206
206
|
static checkAttributeValidity(input) {
|
|
@@ -224,47 +224,44 @@ class AutocompleteAudit extends Audit {
|
|
|
224
224
|
* @return {LH.Audit.Product}
|
|
225
225
|
*/
|
|
226
226
|
static audit(artifacts) {
|
|
227
|
-
const forms = artifacts.FormElements;
|
|
228
227
|
const failingFormsData = [];
|
|
229
228
|
const warnings = [];
|
|
230
229
|
let foundPrediction = false;
|
|
231
|
-
for (const
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
!input.autocomplete.attribute) continue;
|
|
230
|
+
for (const input of artifacts.Inputs.inputs) {
|
|
231
|
+
const validity = this.checkAttributeValidity(input);
|
|
232
|
+
if (validity.hasValidTokens && validity.isValidOrder) continue;
|
|
233
|
+
if (!input.autocomplete.prediction) continue;
|
|
234
|
+
if (noPrediction.includes(input.autocomplete.prediction) &&
|
|
235
|
+
!input.autocomplete.attribute) continue;
|
|
238
236
|
|
|
239
|
-
|
|
237
|
+
foundPrediction = true;
|
|
240
238
|
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
}
|
|
250
|
-
if (validity.isValidOrder === false) {
|
|
251
|
-
warnings.push(str_(UIStrings.warningOrder, {tokens: input.autocomplete.attribute,
|
|
252
|
-
snippet: input.node.snippet}));
|
|
253
|
-
suggestion = UIStrings.reviewOrder;
|
|
254
|
-
}
|
|
255
|
-
// If the autofill prediction is not in our autofill suggestion mapping, then we warn
|
|
256
|
-
if (!(input.autocomplete.prediction in predictionTypesToTokens) &&
|
|
257
|
-
validity.isValidOrder) {
|
|
258
|
-
log.warn(`Autocomplete prediction (${input.autocomplete.prediction})
|
|
259
|
-
not found in our mapping`);
|
|
260
|
-
continue;
|
|
261
|
-
}
|
|
262
|
-
failingFormsData.push({
|
|
263
|
-
node: Audit.makeNodeItem(input.node),
|
|
264
|
-
suggestion: suggestion,
|
|
265
|
-
current: input.autocomplete.attribute,
|
|
266
|
-
});
|
|
239
|
+
// @ts-ignore
|
|
240
|
+
let suggestion = predictionTypesToTokens[input.autocomplete.prediction];
|
|
241
|
+
// This is here to satisfy typescript because the possible null value of autocomplete.attribute is not compatible with Audit details.
|
|
242
|
+
if (!input.autocomplete.attribute) input.autocomplete.attribute = '';
|
|
243
|
+
// Warning is created because while there is an autocomplete attribute, the autocomplete property does not exsist, thus the attribute's value is invalid.
|
|
244
|
+
if (input.autocomplete.attribute) {
|
|
245
|
+
warnings.push(str_(UIStrings.warningInvalid, {token: input.autocomplete.attribute,
|
|
246
|
+
snippet: input.node.snippet}));
|
|
267
247
|
}
|
|
248
|
+
if (validity.isValidOrder === false) {
|
|
249
|
+
warnings.push(str_(UIStrings.warningOrder, {tokens: input.autocomplete.attribute,
|
|
250
|
+
snippet: input.node.snippet}));
|
|
251
|
+
suggestion = UIStrings.reviewOrder;
|
|
252
|
+
}
|
|
253
|
+
// If the autofill prediction is not in our autofill suggestion mapping, then we warn
|
|
254
|
+
if (!(input.autocomplete.prediction in predictionTypesToTokens) &&
|
|
255
|
+
validity.isValidOrder) {
|
|
256
|
+
log.warn(`Autocomplete prediction (${input.autocomplete.prediction})
|
|
257
|
+
not found in our mapping`);
|
|
258
|
+
continue;
|
|
259
|
+
}
|
|
260
|
+
failingFormsData.push({
|
|
261
|
+
node: Audit.makeNodeItem(input.node),
|
|
262
|
+
suggestion: suggestion,
|
|
263
|
+
current: input.autocomplete.attribute,
|
|
264
|
+
});
|
|
268
265
|
}
|
|
269
266
|
|
|
270
267
|
/** @type {LH.Audit.Details.Table['headings']} */
|
|
@@ -46,7 +46,7 @@ const artifacts = {
|
|
|
46
46
|
DOMStats: '',
|
|
47
47
|
EmbeddedContent: '',
|
|
48
48
|
FontSize: '',
|
|
49
|
-
|
|
49
|
+
Inputs: '',
|
|
50
50
|
FullPageScreenshot: '',
|
|
51
51
|
GlobalListeners: '',
|
|
52
52
|
IFrameElements: '',
|
|
@@ -96,7 +96,7 @@ const defaultConfig = {
|
|
|
96
96
|
{id: artifacts.DOMStats, gatherer: 'dobetterweb/domstats'},
|
|
97
97
|
{id: artifacts.EmbeddedContent, gatherer: 'seo/embedded-content'},
|
|
98
98
|
{id: artifacts.FontSize, gatherer: 'seo/font-size'},
|
|
99
|
-
{id: artifacts.
|
|
99
|
+
{id: artifacts.Inputs, gatherer: 'inputs'},
|
|
100
100
|
{id: artifacts.FullPageScreenshot, gatherer: 'full-page-screenshot'},
|
|
101
101
|
{id: artifacts.GlobalListeners, gatherer: 'global-listeners'},
|
|
102
102
|
{id: artifacts.IFrameElements, gatherer: 'iframe-elements'},
|
|
@@ -148,7 +148,7 @@ const defaultConfig = {
|
|
|
148
148
|
artifacts.DOMStats,
|
|
149
149
|
artifacts.EmbeddedContent,
|
|
150
150
|
artifacts.FontSize,
|
|
151
|
-
artifacts.
|
|
151
|
+
artifacts.Inputs,
|
|
152
152
|
artifacts.GlobalListeners,
|
|
153
153
|
artifacts.IFrameElements,
|
|
154
154
|
artifacts.ImageElements,
|
|
@@ -0,0 +1,113 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @license Copyright 2020 The Lighthouse Authors. All Rights Reserved.
|
|
3
|
+
* 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
|
|
4
|
+
* 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.
|
|
5
|
+
*/
|
|
6
|
+
'use strict';
|
|
7
|
+
|
|
8
|
+
/* global getNodeDetails */
|
|
9
|
+
|
|
10
|
+
const FRGatherer = require('../../fraggle-rock/gather/base-gatherer.js');
|
|
11
|
+
const pageFunctions = require('../../lib/page-functions.js');
|
|
12
|
+
|
|
13
|
+
/* eslint-env browser, node */
|
|
14
|
+
|
|
15
|
+
/**
|
|
16
|
+
* @return {LH.Artifacts['Inputs']}
|
|
17
|
+
*/
|
|
18
|
+
/* c8 ignore start */
|
|
19
|
+
function collectElements() {
|
|
20
|
+
/** @type {LH.Artifacts.InputElement[]} */
|
|
21
|
+
const inputArtifacts = [];
|
|
22
|
+
/** @type {Map<HTMLFormElement, LH.Artifacts.FormElement>} */
|
|
23
|
+
const formElToArtifact = new Map();
|
|
24
|
+
/** @type {Map<HTMLLabelElement, LH.Artifacts.LabelElement>} */
|
|
25
|
+
const labelElToArtifact = new Map();
|
|
26
|
+
|
|
27
|
+
/** @type {HTMLFormElement[]} */
|
|
28
|
+
// @ts-expect-error - put into scope via stringification
|
|
29
|
+
const formEls = getElementsInDocument('form'); // eslint-disable-line no-undef
|
|
30
|
+
for (const formEl of formEls) {
|
|
31
|
+
formElToArtifact.set(formEl, {
|
|
32
|
+
id: formEl.id,
|
|
33
|
+
name: formEl.name,
|
|
34
|
+
autocomplete: formEl.autocomplete,
|
|
35
|
+
// @ts-expect-error - getNodeDetails put into scope via stringification
|
|
36
|
+
node: getNodeDetails(formEl),
|
|
37
|
+
});
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
/** @type {HTMLLabelElement[]} */
|
|
41
|
+
// @ts-expect-error - put into scope via stringification
|
|
42
|
+
const labelEls = getElementsInDocument('label'); // eslint-disable-line no-undef
|
|
43
|
+
for (const labelEl of labelEls) {
|
|
44
|
+
labelElToArtifact.set(labelEl, {
|
|
45
|
+
for: labelEl.htmlFor,
|
|
46
|
+
// @ts-expect-error - getNodeDetails put into scope via stringification
|
|
47
|
+
node: getNodeDetails(labelEl),
|
|
48
|
+
});
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
/** @type {HTMLInputElement[]} */
|
|
52
|
+
// @ts-expect-error - put into scope via stringification
|
|
53
|
+
const inputEls = getElementsInDocument('textarea, input, select'); // eslint-disable-line no-undef
|
|
54
|
+
for (const inputEl of inputEls) {
|
|
55
|
+
// If the input element is in a form (either because an ancestor element is <form> or the
|
|
56
|
+
// form= attribute is associated with a <form> element's id), this will be set.
|
|
57
|
+
const parentFormEl = inputEl.form;
|
|
58
|
+
const parentFormIndex = parentFormEl ?
|
|
59
|
+
[...formElToArtifact.keys()].indexOf(parentFormEl) :
|
|
60
|
+
undefined;
|
|
61
|
+
const labelIndicies = [...inputEl.labels || []].map((labelEl) => {
|
|
62
|
+
return [...labelElToArtifact.keys()].indexOf(labelEl);
|
|
63
|
+
});
|
|
64
|
+
|
|
65
|
+
inputArtifacts.push({
|
|
66
|
+
parentFormIndex,
|
|
67
|
+
labelIndicies,
|
|
68
|
+
id: inputEl.id,
|
|
69
|
+
name: inputEl.name,
|
|
70
|
+
type: inputEl.type,
|
|
71
|
+
placeholder: inputEl instanceof HTMLSelectElement ? undefined : inputEl.placeholder,
|
|
72
|
+
autocomplete: {
|
|
73
|
+
property: inputEl.autocomplete,
|
|
74
|
+
attribute: inputEl.getAttribute('autocomplete'),
|
|
75
|
+
// Requires `--enable-features=AutofillShowTypePredictions`.
|
|
76
|
+
prediction: inputEl.getAttribute('autofill-prediction'),
|
|
77
|
+
},
|
|
78
|
+
// @ts-expect-error - getNodeDetails put into scope via stringification
|
|
79
|
+
node: getNodeDetails(inputEl),
|
|
80
|
+
});
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
return {
|
|
84
|
+
inputs: inputArtifacts,
|
|
85
|
+
forms: [...formElToArtifact.values()],
|
|
86
|
+
labels: [...labelElToArtifact.values()],
|
|
87
|
+
};
|
|
88
|
+
}
|
|
89
|
+
/* c8 ignore stop */
|
|
90
|
+
|
|
91
|
+
class Inputs extends FRGatherer {
|
|
92
|
+
/** @type {LH.Gatherer.GathererMeta} */
|
|
93
|
+
meta = {
|
|
94
|
+
supportedModes: ['snapshot', 'navigation'],
|
|
95
|
+
};
|
|
96
|
+
|
|
97
|
+
/**
|
|
98
|
+
* @param {LH.Gatherer.FRTransitionalContext} passContext
|
|
99
|
+
* @return {Promise<LH.Artifacts['Inputs']>}
|
|
100
|
+
*/
|
|
101
|
+
async getArtifact(passContext) {
|
|
102
|
+
return passContext.driver.executionContext.evaluate(collectElements, {
|
|
103
|
+
args: [],
|
|
104
|
+
useIsolation: true,
|
|
105
|
+
deps: [
|
|
106
|
+
pageFunctions.getElementsInDocumentString,
|
|
107
|
+
pageFunctions.getNodeDetailsString,
|
|
108
|
+
],
|
|
109
|
+
});
|
|
110
|
+
}
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
module.exports = Inputs;
|
|
@@ -630,6 +630,10 @@ const UIStrings = {
|
|
|
630
630
|
thirdPartyResourcesLabel: 'Show 3rd-party resources',
|
|
631
631
|
/** This label is for a button that opens a new tab to a webapp called "Treemap", which is a nested visual representation of a heierarchy of data related to the reports (script bytes and coverage, resource breakdown, etc.) */
|
|
632
632
|
viewTreemapLabel: 'View Treemap',
|
|
633
|
+
/** This label is for a button that will show the user a trace of the page. */
|
|
634
|
+
viewTraceLabel: 'View Trace',
|
|
635
|
+
/** This label is for a button that will show the user a trace of the page. */
|
|
636
|
+
viewOriginalTraceLabel: 'View Original Trace',
|
|
633
637
|
|
|
634
638
|
/** Option in a dropdown menu that opens a small, summary report in a print dialog. */
|
|
635
639
|
dropdownPrintSummary: 'Print Summary',
|
package/package.json
CHANGED
|
@@ -112,6 +112,15 @@ export class ReportUIFeatures {
|
|
|
112
112
|
});
|
|
113
113
|
}
|
|
114
114
|
|
|
115
|
+
if (this._opts.onViewTrace) {
|
|
116
|
+
this.addButton({
|
|
117
|
+
text: lhr.configSettings.throttlingMethod === 'simulate' ?
|
|
118
|
+
Util.i18n.strings.viewOriginalTraceLabel :
|
|
119
|
+
Util.i18n.strings.viewTraceLabel,
|
|
120
|
+
onClick: () => this._opts.onViewTrace?.(),
|
|
121
|
+
});
|
|
122
|
+
}
|
|
123
|
+
|
|
115
124
|
if (this._opts.getStandaloneReportHTML) {
|
|
116
125
|
this._dom.find('a[data-action="save-html"]', this._dom.rootEl).classList.remove('lh-hidden');
|
|
117
126
|
}
|
package/report/renderer/util.js
CHANGED
|
@@ -627,6 +627,10 @@ const UIStrings = {
|
|
|
627
627
|
thirdPartyResourcesLabel: 'Show 3rd-party resources',
|
|
628
628
|
/** This label is for a button that opens a new tab to a webapp called "Treemap", which is a nested visual representation of a heierarchy of data related to the reports (script bytes and coverage, resource breakdown, etc.) */
|
|
629
629
|
viewTreemapLabel: 'View Treemap',
|
|
630
|
+
/** This label is for a button that will show the user a trace of the page. */
|
|
631
|
+
viewTraceLabel: 'View Trace',
|
|
632
|
+
/** This label is for a button that will show the user a trace of the page. */
|
|
633
|
+
viewOriginalTraceLabel: 'View Original Trace',
|
|
630
634
|
|
|
631
635
|
/** Option in a dropdown menu that opens a small, summary report in a print dialog. */
|
|
632
636
|
dropdownPrintSummary: 'Print Summary',
|
|
@@ -0,0 +1,107 @@
|
|
|
1
|
+
// Jest Snapshot v1, https://goo.gl/fbAQLP
|
|
2
|
+
|
|
3
|
+
exports[`ReportRendererAxe with aXe renders without axe violations 2`] = `
|
|
4
|
+
Array [
|
|
5
|
+
Object {
|
|
6
|
+
"description": "Ensures every id attribute value is unique",
|
|
7
|
+
"help": "id attribute value must be unique",
|
|
8
|
+
"helpUrl": "https://dequeuniversity.com/rules/axe/4.3/duplicate-id?application=axeAPI",
|
|
9
|
+
"id": "duplicate-id",
|
|
10
|
+
"impact": "minor",
|
|
11
|
+
"nodes": Array [
|
|
12
|
+
Object {
|
|
13
|
+
"all": Array [],
|
|
14
|
+
"any": Array [
|
|
15
|
+
Object {
|
|
16
|
+
"data": "viewport",
|
|
17
|
+
"id": "duplicate-id",
|
|
18
|
+
"impact": "minor",
|
|
19
|
+
"message": "Document has multiple static elements with the same id attribute: viewport",
|
|
20
|
+
"relatedNodes": Array [
|
|
21
|
+
Object {
|
|
22
|
+
"html": "<div class=\\"lh-audit lh-audit--binary lh-audit--pass\\" id=\\"viewport\\">",
|
|
23
|
+
"target": Array [
|
|
24
|
+
"#seo > .lh-audit-group:nth-child(4) > .lh-clump--passed.lh-clump > .lh-audit--binary.lh-audit--pass.lh-audit:nth-child(2)",
|
|
25
|
+
],
|
|
26
|
+
},
|
|
27
|
+
Object {
|
|
28
|
+
"html": "<div class=\\"lh-audit lh-audit--binary lh-audit--pass\\" id=\\"viewport\\">",
|
|
29
|
+
"target": Array [
|
|
30
|
+
".lh-audit-group--pwa-optimized > .lh-audit--binary.lh-audit--pass.lh-audit:nth-child(6)",
|
|
31
|
+
],
|
|
32
|
+
},
|
|
33
|
+
],
|
|
34
|
+
},
|
|
35
|
+
],
|
|
36
|
+
"failureSummary": "Fix any of the following:
|
|
37
|
+
Document has multiple static elements with the same id attribute: viewport",
|
|
38
|
+
"html": "<div class=\\"lh-audit lh-audit--binary lh-audit--pass\\" id=\\"viewport\\">",
|
|
39
|
+
"impact": "minor",
|
|
40
|
+
"none": Array [],
|
|
41
|
+
"target": Array [
|
|
42
|
+
".lh-audit--binary.lh-audit--pass.lh-audit:nth-child(20)",
|
|
43
|
+
],
|
|
44
|
+
},
|
|
45
|
+
Object {
|
|
46
|
+
"all": Array [],
|
|
47
|
+
"any": Array [
|
|
48
|
+
Object {
|
|
49
|
+
"data": "image-alt",
|
|
50
|
+
"id": "duplicate-id",
|
|
51
|
+
"impact": "minor",
|
|
52
|
+
"message": "Document has multiple static elements with the same id attribute: image-alt",
|
|
53
|
+
"relatedNodes": Array [
|
|
54
|
+
Object {
|
|
55
|
+
"html": "<div class=\\"lh-audit lh-audit--binary lh-audit--fail\\" id=\\"image-alt\\">",
|
|
56
|
+
"target": Array [
|
|
57
|
+
".lh-audit-group--seo-content > .lh-audit--fail.lh-audit--binary.lh-audit:nth-child(3)",
|
|
58
|
+
],
|
|
59
|
+
},
|
|
60
|
+
],
|
|
61
|
+
},
|
|
62
|
+
],
|
|
63
|
+
"failureSummary": "Fix any of the following:
|
|
64
|
+
Document has multiple static elements with the same id attribute: image-alt",
|
|
65
|
+
"html": "<div class=\\"lh-audit lh-audit--binary lh-audit--fail\\" id=\\"image-alt\\">",
|
|
66
|
+
"impact": "minor",
|
|
67
|
+
"none": Array [],
|
|
68
|
+
"target": Array [
|
|
69
|
+
".lh-audit-group--a11y-names-labels > .lh-audit--fail.lh-audit--binary.lh-audit:nth-child(2)",
|
|
70
|
+
],
|
|
71
|
+
},
|
|
72
|
+
Object {
|
|
73
|
+
"all": Array [],
|
|
74
|
+
"any": Array [
|
|
75
|
+
Object {
|
|
76
|
+
"data": "document-title",
|
|
77
|
+
"id": "duplicate-id",
|
|
78
|
+
"impact": "minor",
|
|
79
|
+
"message": "Document has multiple static elements with the same id attribute: document-title",
|
|
80
|
+
"relatedNodes": Array [
|
|
81
|
+
Object {
|
|
82
|
+
"html": "<div class=\\"lh-audit lh-audit--binary lh-audit--pass\\" id=\\"document-title\\">",
|
|
83
|
+
"target": Array [
|
|
84
|
+
"#seo > .lh-audit-group:nth-child(4) > .lh-clump--passed.lh-clump > .lh-audit--binary.lh-audit--pass.lh-audit:nth-child(3)",
|
|
85
|
+
],
|
|
86
|
+
},
|
|
87
|
+
],
|
|
88
|
+
},
|
|
89
|
+
],
|
|
90
|
+
"failureSummary": "Fix any of the following:
|
|
91
|
+
Document has multiple static elements with the same id attribute: document-title",
|
|
92
|
+
"html": "<div class=\\"lh-audit lh-audit--binary lh-audit--pass\\" id=\\"document-title\\">",
|
|
93
|
+
"impact": "minor",
|
|
94
|
+
"none": Array [],
|
|
95
|
+
"target": Array [
|
|
96
|
+
".lh-audit--binary.lh-audit--pass.lh-audit:nth-child(13)",
|
|
97
|
+
],
|
|
98
|
+
},
|
|
99
|
+
],
|
|
100
|
+
"tags": Array [
|
|
101
|
+
"cat.parsing",
|
|
102
|
+
"wcag2a",
|
|
103
|
+
"wcag411",
|
|
104
|
+
],
|
|
105
|
+
},
|
|
106
|
+
]
|
|
107
|
+
`;
|
|
@@ -53,23 +53,44 @@ describe('ReportRendererAxe', () => {
|
|
|
53
53
|
// eslint-disable-next-line no-undef
|
|
54
54
|
const axeResults = await page.evaluate(config => axe.run(config), config);
|
|
55
55
|
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
56
|
+
// Color contrast failure only pops up if this pptr is run headfully.
|
|
57
|
+
// There are currently 27 problematic nodes, primarily audit display text and explanations.
|
|
58
|
+
// TODO: fix these failures, regardless.
|
|
59
|
+
// {
|
|
60
|
+
// id: 'color-contrast',
|
|
61
|
+
// },
|
|
62
|
+
|
|
63
|
+
expect(axeResults.violations.find(v => v.id === 'duplicate-id')).toMatchObject({
|
|
64
|
+
id: 'duplicate-id',
|
|
65
|
+
nodes: [
|
|
66
|
+
// We use these audits in multiple categories. Makes sense.
|
|
67
|
+
{html: '<div class="lh-audit lh-audit--binary lh-audit--pass" id="viewport">'},
|
|
68
|
+
{html: '<div class="lh-audit lh-audit--binary lh-audit--fail" id="image-alt">'},
|
|
69
|
+
{html: '<div class="lh-audit lh-audit--binary lh-audit--pass" id="document-title">'},
|
|
70
|
+
],
|
|
71
|
+
});
|
|
72
|
+
|
|
73
|
+
const axeSummary = axeResults.violations.map((v) => {
|
|
74
|
+
return {
|
|
75
|
+
id: v.id,
|
|
76
|
+
message: v.nodes.map((n) => n.failureSummary).join('\n'),
|
|
77
|
+
};
|
|
78
|
+
});
|
|
79
|
+
expect(axeSummary).toMatchInlineSnapshot(`
|
|
80
|
+
Array [
|
|
81
|
+
Object {
|
|
82
|
+
"id": "duplicate-id",
|
|
83
|
+
"message": "Fix any of the following:
|
|
84
|
+
Document has multiple static elements with the same id attribute: viewport
|
|
85
|
+
Fix any of the following:
|
|
86
|
+
Document has multiple static elements with the same id attribute: image-alt
|
|
87
|
+
Fix any of the following:
|
|
88
|
+
Document has multiple static elements with the same id attribute: document-title",
|
|
89
|
+
},
|
|
90
|
+
]
|
|
91
|
+
`);
|
|
92
|
+
|
|
93
|
+
expect(axeResults.violations).toMatchSnapshot();
|
|
73
94
|
},
|
|
74
95
|
// This test takes 10s on fast hardware, but can take longer in CI.
|
|
75
96
|
// https://github.com/dequelabs/axe-core/tree/b573b1c1/doc/examples/jest_react#timeout-issues
|
|
@@ -5,6 +5,7 @@
|
|
|
5
5
|
*/
|
|
6
6
|
|
|
7
7
|
import { Result as AuditResult } from "../../types/lhr/audit-result";
|
|
8
|
+
import { ReportUIFeatures } from "../renderer/report-ui-features.js";
|
|
8
9
|
|
|
9
10
|
declare module Renderer {
|
|
10
11
|
function renderReport(lhr: AuditResult, options?: Options): HTMLElement;
|
|
@@ -26,6 +27,12 @@ declare module Renderer {
|
|
|
26
27
|
onPageAnchorRendered?: (link: HTMLAnchorElement) => void;
|
|
27
28
|
/** If defined, `Save as HTML` option is shown in dropdown menu. */
|
|
28
29
|
getStandaloneReportHTML?: () => string;
|
|
30
|
+
/**
|
|
31
|
+
* If defined, adds a `View Trace` button to the report, and calls this callback when clicked.
|
|
32
|
+
* The callback should do something to present the user with a visualization of the trace
|
|
33
|
+
* data, which can be gotten from the artifacts.
|
|
34
|
+
*/
|
|
35
|
+
onViewTrace?: () => void;
|
|
29
36
|
}
|
|
30
37
|
}
|
|
31
38
|
|
|
@@ -2543,6 +2543,12 @@
|
|
|
2543
2543
|
"report/renderer/util.js | varianceDisclaimer": {
|
|
2544
2544
|
"message": "Values are estimated and may vary. The [performance score is calculated](https://web.dev/performance-scoring/) directly from these metrics."
|
|
2545
2545
|
},
|
|
2546
|
+
"report/renderer/util.js | viewOriginalTraceLabel": {
|
|
2547
|
+
"message": "View Original Trace"
|
|
2548
|
+
},
|
|
2549
|
+
"report/renderer/util.js | viewTraceLabel": {
|
|
2550
|
+
"message": "View Trace"
|
|
2551
|
+
},
|
|
2546
2552
|
"report/renderer/util.js | viewTreemapLabel": {
|
|
2547
2553
|
"message": "View Treemap"
|
|
2548
2554
|
},
|
|
@@ -2543,6 +2543,12 @@
|
|
|
2543
2543
|
"report/renderer/util.js | varianceDisclaimer": {
|
|
2544
2544
|
"message": "V̂ál̂úêś âŕê éŝt́îḿât́êd́ âńd̂ ḿâý v̂ár̂ý. T̂h́ê [ṕêŕf̂ór̂ḿâńĉé ŝćôŕê íŝ ćâĺĉúl̂át̂éd̂](https://web.dev/performance-scoring/) d́îŕêćt̂ĺŷ f́r̂óm̂ t́ĥéŝé m̂ét̂ŕîćŝ."
|
|
2545
2545
|
},
|
|
2546
|
+
"report/renderer/util.js | viewOriginalTraceLabel": {
|
|
2547
|
+
"message": "V̂íêẃ Ôŕîǵîńâĺ T̂ŕâćê"
|
|
2548
|
+
},
|
|
2549
|
+
"report/renderer/util.js | viewTraceLabel": {
|
|
2550
|
+
"message": "V̂íêẃ T̂ŕâćê"
|
|
2551
|
+
},
|
|
2546
2552
|
"report/renderer/util.js | viewTreemapLabel": {
|
|
2547
2553
|
"message": "V̂íêẃ T̂ŕêém̂áp̂"
|
|
2548
2554
|
},
|
package/types/artifacts.d.ts
CHANGED
|
@@ -139,8 +139,8 @@ export interface GathererArtifacts extends PublicGathererArtifacts,LegacyBaseArt
|
|
|
139
139
|
Fonts: Artifacts.Font[];
|
|
140
140
|
/** Information on poorly sized font usage and the text affected by it. */
|
|
141
141
|
FontSize: Artifacts.FontSize;
|
|
142
|
-
/** All the
|
|
143
|
-
|
|
142
|
+
/** All the input elements, including associated form and label elements. */
|
|
143
|
+
Inputs: {inputs: Artifacts.InputElement[]; forms: Artifacts.FormElement[]; labels: Artifacts.LabelElement[]};
|
|
144
144
|
/** Screenshot of the entire page (rather than just the above the fold content). */
|
|
145
145
|
FullPageScreenshot: Artifacts.FullPageScreenshot | null;
|
|
146
146
|
/** Information about event listeners registered on the global object. */
|
|
@@ -787,22 +787,22 @@ declare module Artifacts {
|
|
|
787
787
|
observedSpeedIndexTs: number;
|
|
788
788
|
}
|
|
789
789
|
|
|
790
|
-
interface
|
|
791
|
-
|
|
792
|
-
|
|
793
|
-
|
|
794
|
-
|
|
795
|
-
autocomplete: string;
|
|
796
|
-
};
|
|
797
|
-
node: NodeDetails | null;
|
|
798
|
-
inputs: Array<FormInput>;
|
|
799
|
-
labels: Array<FormLabel>;
|
|
790
|
+
interface FormElement {
|
|
791
|
+
id: string;
|
|
792
|
+
name: string;
|
|
793
|
+
autocomplete: string;
|
|
794
|
+
node: NodeDetails;
|
|
800
795
|
}
|
|
801
796
|
|
|
802
797
|
/** Attributes collected for every input element in the inputs array from the forms interface. */
|
|
803
|
-
interface
|
|
798
|
+
interface InputElement {
|
|
799
|
+
/** If set, the parent form is the index into the associated FormElement array. Otherwise, the input element has no parent form. */
|
|
800
|
+
parentFormIndex?: number;
|
|
801
|
+
/** Array of indices into associated LabelElement array. */
|
|
802
|
+
labelIndicies: number[];
|
|
804
803
|
id: string;
|
|
805
804
|
name: string;
|
|
805
|
+
type: string;
|
|
806
806
|
placeholder?: string;
|
|
807
807
|
autocomplete: {
|
|
808
808
|
property: string;
|
|
@@ -813,7 +813,7 @@ declare module Artifacts {
|
|
|
813
813
|
}
|
|
814
814
|
|
|
815
815
|
/** Attributes collected for every label element in the labels array from the forms interface */
|
|
816
|
-
interface
|
|
816
|
+
interface LabelElement {
|
|
817
817
|
for: string;
|
|
818
818
|
node: NodeDetails;
|
|
819
819
|
}
|