lighthouse 9.3.1-dev.20220213 → 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.
@@ -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
- // import formsAutoComplete from './test-definitions/forms-autocomplete.js';
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
- // TODO: restore when --enable-features=AutofillShowTypePredictions is not needed.
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: ['FormElements'],
198
+ requiredArtifacts: ['Inputs'],
199
199
  };
200
200
  }
201
201
 
202
202
  /**
203
- * @param {LH.Artifacts.FormInput} input
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 form of forms) {
232
- for (const input of form.inputs) {
233
- const validity = this.checkAttributeValidity(input);
234
- if (validity.hasValidTokens && validity.isValidOrder) continue;
235
- if (!input.autocomplete.prediction) continue;
236
- if (noPrediction.includes(input.autocomplete.prediction) &&
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
- foundPrediction = true;
237
+ foundPrediction = true;
240
238
 
241
- // @ts-ignore
242
- let suggestion = predictionTypesToTokens[input.autocomplete.prediction];
243
- // This is here to satisfy typescript because the possible null value of autocomplete.attribute is not compatible with Audit details.
244
- if (!input.autocomplete.attribute) input.autocomplete.attribute = '';
245
- // Warning is created because while there is an autocomplete attribute, the autocomplete property does not exsist, thus the attribute's value is invalid.
246
- if (input.autocomplete.attribute) {
247
- warnings.push(str_(UIStrings.warningInvalid, {token: input.autocomplete.attribute,
248
- snippet: input.node.snippet}));
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']} */
@@ -144,7 +144,7 @@ const defaultConfig = {
144
144
  'meta-elements',
145
145
  'script-elements',
146
146
  'iframe-elements',
147
- 'form-elements',
147
+ 'inputs',
148
148
  'main-document-content',
149
149
  'global-listeners',
150
150
  'dobetterweb/doctype',
@@ -46,7 +46,7 @@ const artifacts = {
46
46
  DOMStats: '',
47
47
  EmbeddedContent: '',
48
48
  FontSize: '',
49
- FormElements: '',
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.FormElements, gatherer: 'form-elements'},
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.FormElements,
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;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "lighthouse",
3
- "version": "9.3.1-dev.20220213",
3
+ "version": "9.3.1-dev.20220214",
4
4
  "description": "Automated auditing, performance metrics, and best practices for the web.",
5
5
  "main": "./lighthouse-core/index.js",
6
6
  "bin": {
@@ -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 form elements in the page and formless inputs. */
143
- FormElements: Artifacts.Form[];
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 Form {
791
- /** If attributes is missing that means this is a formless set of elements. */
792
- attributes?: {
793
- id: string;
794
- name: string;
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 FormInput {
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 FormLabel {
816
+ interface LabelElement {
817
817
  for: string;
818
818
  node: NodeDetails;
819
819
  }
@@ -1,113 +0,0 @@
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['FormElements']}
17
- */
18
- /* c8 ignore start */
19
- function collectFormElements() {
20
- // @ts-expect-error - put into scope via stringification
21
- const formChildren = getElementsInDocument('textarea, input, label, select'); // eslint-disable-line no-undef
22
- /** @type {Map<HTMLFormElement|string, LH.Artifacts.Form>} */
23
- const forms = new Map();
24
- /** @type {LH.Artifacts.Form} */
25
- const formlessObj = {
26
- node: null,
27
- inputs: [],
28
- labels: [],
29
- };
30
- for (const child of formChildren) {
31
- const isButton = child instanceof HTMLInputElement &&
32
- (child.type === 'submit' || child.type === 'button');
33
- if (isButton) continue;
34
-
35
- const parentFormElement = child.form;
36
- const hasForm = !!parentFormElement;
37
- if (hasForm && !forms.has(parentFormElement)) {
38
- const newFormObj = {
39
- attributes: {
40
- id: parentFormElement.id,
41
- name: parentFormElement.name,
42
- autocomplete: parentFormElement.autocomplete,
43
- },
44
- // @ts-expect-error - getNodeDetails put into scope via stringification
45
- node: getNodeDetails(parentFormElement),
46
- inputs: [],
47
- labels: [],
48
- };
49
- forms.set(parentFormElement, newFormObj);
50
- }
51
- const formObj = forms.get(parentFormElement) || formlessObj;
52
- if (child instanceof HTMLInputElement || child instanceof HTMLTextAreaElement ||
53
- child instanceof HTMLSelectElement) {
54
- formObj.inputs.push({
55
- id: child.id,
56
- name: child.name,
57
- placeholder: child instanceof HTMLSelectElement ? undefined : child.placeholder,
58
- autocomplete: {
59
- property: child.autocomplete,
60
- // Requires `--enable-features=AutofillShowTypePredictions`.
61
- attribute: child.getAttribute('autocomplete'),
62
- prediction: child.getAttribute('autofill-prediction'),
63
- },
64
- // @ts-expect-error - getNodeDetails put into scope via stringification
65
- node: getNodeDetails(child),
66
- });
67
- }
68
- if (child instanceof HTMLLabelElement) {
69
- formObj.labels.push({
70
- for: child.htmlFor,
71
- // @ts-expect-error - getNodeDetails put into scope via stringification
72
- node: getNodeDetails(child),
73
- });
74
- }
75
- }
76
-
77
- if (formlessObj.inputs.length > 0 || formlessObj.labels.length > 0) {
78
- forms.set('formless', {
79
- node: formlessObj.node,
80
- inputs: formlessObj.inputs,
81
- labels: formlessObj.labels,
82
- });
83
- }
84
- return [...forms.values()];
85
- }
86
- /* c8 ignore stop */
87
-
88
- class FormElements extends FRGatherer {
89
- /** @type {LH.Gatherer.GathererMeta} */
90
- meta = {
91
- supportedModes: ['snapshot', 'navigation'],
92
- };
93
-
94
- /**
95
- * @param {LH.Gatherer.FRTransitionalContext} passContext
96
- * @return {Promise<LH.Artifacts['FormElements']>}
97
- */
98
- async getArtifact(passContext) {
99
- const driver = passContext.driver;
100
-
101
- const formElements = await driver.executionContext.evaluate(collectFormElements, {
102
- args: [],
103
- useIsolation: true,
104
- deps: [
105
- pageFunctions.getElementsInDocumentString,
106
- pageFunctions.getNodeDetailsString,
107
- ],
108
- });
109
- return formElements;
110
- }
111
- }
112
-
113
- module.exports = FormElements;