Skip to content

Commit 13eb37f

Browse files
authoredMar 8, 2022
core(user-flow): audit flow from artifacts json (#13715)
1 parent 5488cbe commit 13eb37f

File tree

11 files changed

+923624
-2674
lines changed

11 files changed

+923624
-2674
lines changed
 

‎lighthouse-core/fraggle-rock/api.js

+12-2
Original file line numberDiff line numberDiff line change
@@ -5,16 +5,16 @@
55
*/
66
'use strict';
77

8+
const {UserFlow, auditGatherSteps} = require('./user-flow.js');
89
const {snapshotGather} = require('./gather/snapshot-runner.js');
910
const {startTimespanGather} = require('./gather/timespan-runner.js');
1011
const {navigationGather} = require('./gather/navigation-runner.js');
1112
const {generateFlowReportHtml} = require('../../report/generator/report-generator.js');
1213
const Runner = require('../runner.js');
13-
const UserFlow = require('./user-flow.js');
1414

1515
/**
1616
* @param {import('puppeteer').Page} page
17-
* @param {UserFlow.UserFlowOptions} [options]
17+
* @param {ConstructorParameters<LH.UserFlow>[1]} [options]
1818
*/
1919
async function startFlow(page, options) {
2020
return new UserFlow(page, options);
@@ -58,10 +58,20 @@ async function generateFlowReport(flowResult) {
5858
return generateFlowReportHtml(flowResult);
5959
}
6060

61+
/**
62+
* @param {LH.UserFlow.FlowArtifacts} flowArtifacts
63+
* @param {LH.Config.Json} [config]
64+
*/
65+
async function auditFlowArtifacts(flowArtifacts, config) {
66+
const {gatherSteps, name} = flowArtifacts;
67+
return await auditGatherSteps(gatherSteps, {name, config});
68+
}
69+
6170
module.exports = {
6271
snapshot,
6372
startTimespan,
6473
navigation,
6574
startFlow,
6675
generateFlowReport,
76+
auditFlowArtifacts,
6777
};

‎lighthouse-core/fraggle-rock/user-flow.js

+88-40
Original file line numberDiff line numberDiff line change
@@ -10,11 +10,12 @@ const {snapshotGather} = require('./gather/snapshot-runner.js');
1010
const {startTimespanGather} = require('./gather/timespan-runner.js');
1111
const {navigationGather} = require('./gather/navigation-runner.js');
1212
const Runner = require('../runner.js');
13+
const {initializeConfig} = require('./config/config.js');
1314

1415
/** @typedef {Parameters<snapshotGather>[0]} FrOptions */
1516
/** @typedef {Omit<FrOptions, 'page'> & {name?: string}} UserFlowOptions */
1617
/** @typedef {Omit<FrOptions, 'page'> & {stepName?: string}} StepOptions */
17-
/** @typedef {{gatherResult: LH.Gatherer.FRGatherResult, name: string}} StepArtifact */
18+
/** @typedef {WeakMap<LH.UserFlow.GatherStep, LH.Gatherer.FRGatherResult['runnerOptions']>} GatherStepRunnerOptions */
1819

1920
class UserFlow {
2021
/**
@@ -26,8 +27,10 @@ class UserFlow {
2627
this.options = {page, ...options};
2728
/** @type {string|undefined} */
2829
this.name = options?.name;
29-
/** @type {StepArtifact[]} */
30-
this.stepArtifacts = [];
30+
/** @type {LH.UserFlow.GatherStep[]} */
31+
this._gatherSteps = [];
32+
/** @type {GatherStepRunnerOptions} */
33+
this._gatherStepRunnerOptions = new WeakMap();
3134
}
3235

3336
/**
@@ -68,8 +71,8 @@ class UserFlow {
6871
}
6972

7073
// On repeat navigations, we want to disable storage reset by default (i.e. it's not a cold load).
71-
const isSubsequentNavigation = this.stepArtifacts
72-
.some(step => step.gatherResult.artifacts.GatherContext.gatherMode === 'navigation');
74+
const isSubsequentNavigation = this._gatherSteps
75+
.some(step => step.artifacts.GatherContext.gatherMode === 'navigation');
7376
if (isSubsequentNavigation) {
7477
if (settingsOverrides.disableStorageReset === undefined) {
7578
settingsOverrides.disableStorageReset = true;
@@ -82,23 +85,34 @@ class UserFlow {
8285
return options;
8386
}
8487

88+
/**
89+
*
90+
* @param {LH.Gatherer.FRGatherResult} gatherResult
91+
* @param {StepOptions} options
92+
*/
93+
_addGatherStep(gatherResult, options) {
94+
const providedName = options?.stepName;
95+
const gatherStep = {
96+
artifacts: gatherResult.artifacts,
97+
name: providedName || this._getDefaultStepName(gatherResult.artifacts),
98+
config: options.config,
99+
configContext: options.configContext,
100+
};
101+
this._gatherSteps.push(gatherStep);
102+
this._gatherStepRunnerOptions.set(gatherStep, gatherResult.runnerOptions);
103+
}
104+
85105
/**
86106
* @param {LH.NavigationRequestor} requestor
87107
* @param {StepOptions=} stepOptions
88108
*/
89109
async navigate(requestor, stepOptions) {
90110
if (this.currentTimespan) throw new Error('Timespan already in progress');
91111

92-
const gatherResult = await navigationGather(
93-
requestor,
94-
this._getNextNavigationOptions(stepOptions)
95-
);
112+
const options = this._getNextNavigationOptions(stepOptions);
113+
const gatherResult = await navigationGather(requestor, options);
96114

97-
const providedName = stepOptions?.stepName;
98-
this.stepArtifacts.push({
99-
gatherResult,
100-
name: providedName || this._getDefaultStepName(gatherResult.artifacts),
101-
});
115+
this._addGatherStep(gatherResult, options);
102116

103117
return gatherResult;
104118
}
@@ -121,11 +135,7 @@ class UserFlow {
121135
const gatherResult = await timespan.endTimespanGather();
122136
this.currentTimespan = undefined;
123137

124-
const providedName = options?.stepName;
125-
this.stepArtifacts.push({
126-
gatherResult,
127-
name: providedName || this._getDefaultStepName(gatherResult.artifacts),
128-
});
138+
this._addGatherStep(gatherResult, options);
129139

130140
return gatherResult;
131141
}
@@ -139,11 +149,7 @@ class UserFlow {
139149
const options = {...this.options, ...stepOptions};
140150
const gatherResult = await snapshotGather(options);
141151

142-
const providedName = stepOptions?.stepName;
143-
this.stepArtifacts.push({
144-
gatherResult,
145-
name: providedName || this._getDefaultStepName(gatherResult.artifacts),
146-
});
152+
this._addGatherStep(gatherResult, options);
147153

148154
return gatherResult;
149155
}
@@ -152,21 +158,11 @@ class UserFlow {
152158
* @returns {Promise<LH.FlowResult>}
153159
*/
154160
async createFlowResult() {
155-
if (!this.stepArtifacts.length) {
156-
throw new Error('Need at least one step before getting the result');
157-
}
158-
const url = new URL(this.stepArtifacts[0].gatherResult.artifacts.URL.finalUrl);
159-
const flowName = this.name || `User flow (${url.hostname})`;
160-
161-
/** @type {LH.FlowResult['steps']} */
162-
const steps = [];
163-
for (const {gatherResult, name} of this.stepArtifacts) {
164-
const result = await Runner.audit(gatherResult.artifacts, gatherResult.runnerOptions);
165-
if (!result) throw new Error(`Step "${name}" did not return a result`);
166-
steps.push({lhr: result.lhr, name});
167-
}
168-
169-
return {steps, name: flowName};
161+
return auditGatherSteps(this._gatherSteps, {
162+
name: this.name,
163+
config: this.options.config,
164+
gatherStepRunnerOptions: this._gatherStepRunnerOptions,
165+
});
170166
}
171167

172168
/**
@@ -176,6 +172,58 @@ class UserFlow {
176172
const flowResult = await this.createFlowResult();
177173
return generateFlowReportHtml(flowResult);
178174
}
175+
176+
/**
177+
* @return {LH.UserFlow.FlowArtifacts}
178+
*/
179+
createArtifactsJson() {
180+
return {
181+
gatherSteps: this._gatherSteps,
182+
name: this.name,
183+
};
184+
}
185+
}
186+
187+
/**
188+
* @param {Array<LH.UserFlow.GatherStep>} gatherSteps
189+
* @param {{name?: string, config?: LH.Config.Json, gatherStepRunnerOptions?: GatherStepRunnerOptions}} options
190+
*/
191+
async function auditGatherSteps(gatherSteps, options) {
192+
if (!gatherSteps.length) {
193+
throw new Error('Need at least one step before getting the result');
194+
}
195+
196+
/** @type {LH.FlowResult['steps']} */
197+
const steps = [];
198+
for (const gatherStep of gatherSteps) {
199+
const {artifacts, name, configContext} = gatherStep;
200+
201+
let runnerOptions = options.gatherStepRunnerOptions?.get(gatherStep);
202+
203+
// If the gather step is not active, we must recreate the runner options.
204+
if (!runnerOptions) {
205+
// Step specific configs take precedence over a config for the entire flow.
206+
const configJson = gatherStep.config || options.config;
207+
const {gatherMode} = artifacts.GatherContext;
208+
const {config} = initializeConfig(configJson, {...configContext, gatherMode});
209+
runnerOptions = {
210+
config,
211+
computedCache: new Map(),
212+
};
213+
}
214+
215+
const result = await Runner.audit(artifacts, runnerOptions);
216+
if (!result) throw new Error(`Step "${name}" did not return a result`);
217+
steps.push({lhr: result.lhr, name});
218+
}
219+
220+
const url = new URL(gatherSteps[0].artifacts.URL.finalUrl);
221+
const flowName = options.name || `User flow (${url.hostname})`;
222+
return {steps, name: flowName};
179223
}
180224

181-
module.exports = UserFlow;
225+
226+
module.exports = {
227+
UserFlow,
228+
auditGatherSteps,
229+
};

‎lighthouse-core/lib/asset-saver.js

+15
Original file line numberDiff line numberDiff line change
@@ -313,6 +313,20 @@ async function saveLanternNetworkData(devtoolsLog, outputPath) {
313313
fs.writeFileSync(outputPath, JSON.stringify(lanternData));
314314
}
315315

316+
/**
317+
* Normalize timing data so it doesn't change every update.
318+
* @param {LH.Result.MeasureEntry[]} timings
319+
*/
320+
function normalizeTimingEntries(timings) {
321+
let baseTime = 0;
322+
for (const timing of timings) {
323+
// @ts-expect-error: Value actually is writeable at this point.
324+
timing.startTime = baseTime++;
325+
// @ts-expect-error: Value actually is writeable at this point.
326+
timing.duration = 1;
327+
}
328+
}
329+
316330
module.exports = {
317331
saveArtifacts,
318332
saveLhr,
@@ -323,4 +337,5 @@ module.exports = {
323337
saveDevtoolsLog,
324338
saveLanternNetworkData,
325339
stringifyReplacer,
340+
normalizeTimingEntries,
326341
};

‎lighthouse-core/scripts/update-flow-fixtures.js

+63-27
Original file line numberDiff line numberDiff line change
@@ -14,7 +14,13 @@ import puppeteer from 'puppeteer';
1414

1515
import {LH_ROOT} from '../../root.js';
1616
import api from '../fraggle-rock/api.js';
17+
import assetSaver from '../lib/asset-saver.js';
1718

19+
const ARTIFACTS_PATH =
20+
`${LH_ROOT}/lighthouse-core/test/fixtures/fraggle-rock/artifacts/sample-flow-artifacts.json`;
21+
const FLOW_RESULT_PATH =
22+
`${LH_ROOT}/lighthouse-core/test/fixtures/fraggle-rock/reports/sample-flow-result.json`;
23+
const FLOW_REPORT_PATH = `${LH_ROOT}/dist/sample-reports/flow-report/index.html`;
1824

1925
/** @param {puppeteer.Page} page */
2026
async function waitForImagesToLoad(page) {
@@ -45,49 +51,79 @@ async function waitForImagesToLoad(page) {
4551
}, TIMEOUT);
4652
}
4753

48-
(async () => {
54+
/** @type {LH.Config.Json} */
55+
const config = {
56+
extends: 'lighthouse:default',
57+
settings: {
58+
skipAudits: ['uses-http2'],
59+
},
60+
};
61+
62+
async function rebaselineArtifacts() {
4963
const browser = await puppeteer.launch({
5064
ignoreDefaultArgs: ['--enable-automation'],
5165
executablePath: process.env.CHROME_PATH,
5266
headless: false,
5367
});
5468

55-
try {
56-
const page = await browser.newPage();
57-
const flow = await api.startFlow(page);
69+
const page = await browser.newPage();
70+
const flow = await api.startFlow(page, {config});
5871

59-
await flow.navigate('https://www.mikescerealshack.co');
72+
await flow.navigate('https://www.mikescerealshack.co');
6073

61-
await flow.startTimespan({stepName: 'Search input'});
62-
await page.type('input', 'call of duty');
63-
const networkQuietPromise = page.waitForNavigation({waitUntil: ['networkidle0']});
64-
await page.click('button[type=submit]');
65-
await networkQuietPromise;
66-
await waitForImagesToLoad(page);
67-
await flow.endTimespan();
74+
await flow.startTimespan({stepName: 'Search input'});
75+
await page.type('input', 'call of duty');
76+
const networkQuietPromise = page.waitForNavigation({waitUntil: ['networkidle0']});
77+
await page.click('button[type=submit]');
78+
await networkQuietPromise;
79+
await waitForImagesToLoad(page);
80+
await flow.endTimespan();
6881

69-
await flow.snapshot({stepName: 'Search results'});
82+
await flow.snapshot({stepName: 'Search results'});
7083

71-
await flow.navigate('https://www.mikescerealshack.co/corrections');
84+
await flow.navigate('https://www.mikescerealshack.co/corrections');
7285

73-
const flowResult = await flow.createFlowResult();
86+
await browser.close();
7487

75-
fs.writeFileSync(
76-
`${LH_ROOT}/lighthouse-core/test/fixtures/fraggle-rock/reports/sample-flow-result.json`,
77-
JSON.stringify(flowResult, null, 2)
78-
);
88+
const flowArtifacts = flow.createArtifactsJson();
7989

80-
if (process.argv.includes('--view')) {
81-
const htmlReport = await api.generateFlowReport(flowResult);
82-
const filepath = `${LH_ROOT}/dist/sample-reports/flow-report/index.html`;
83-
fs.writeFileSync(filepath, htmlReport);
84-
open(filepath);
85-
}
90+
// Normalize some data so it doesn't change on every update.
91+
for (const {artifacts} of flowArtifacts.gatherSteps) {
92+
assetSaver.normalizeTimingEntries(artifacts.Timing);
93+
}
94+
95+
fs.writeFileSync(ARTIFACTS_PATH, JSON.stringify(flowArtifacts, null, 2));
96+
}
8697

87-
process.exit(0);
98+
async function generateFlowResult() {
99+
/** @type {LH.UserFlow.FlowArtifacts} */
100+
const flowArtifacts = JSON.parse(fs.readFileSync(ARTIFACTS_PATH, 'utf-8'));
101+
const flowResult = await api.auditFlowArtifacts(flowArtifacts, config);
102+
103+
// Normalize some data so it doesn't change on every update.
104+
for (const {lhr} of flowResult.steps) {
105+
assetSaver.normalizeTimingEntries(lhr.timing.entries);
106+
lhr.timing.total = lhr.timing.entries.length;
107+
}
108+
109+
fs.writeFileSync(FLOW_RESULT_PATH, JSON.stringify(flowResult, null, 2));
110+
111+
if (process.argv.includes('--view')) {
112+
const htmlReport = await api.generateFlowReport(flowResult);
113+
fs.writeFileSync(FLOW_REPORT_PATH, htmlReport);
114+
open(FLOW_REPORT_PATH);
115+
}
116+
}
117+
118+
(async () => {
119+
try {
120+
if (process.argv.includes('--rebaseline-artifacts')) {
121+
await rebaselineArtifacts();
122+
}
123+
await generateFlowResult();
88124
} catch (err) {
89125
console.error(err);
90-
await browser.close();
91126
process.exit(1);
92127
}
93128
})();
129+

‎lighthouse-core/scripts/update-report-fixtures.js

+1-8
Original file line numberDiff line numberDiff line change
@@ -36,14 +36,7 @@ async function update(artifactName) {
3636

3737
let newArtifacts = assetSaver.loadArtifacts(artifactPath);
3838

39-
// Normalize some data so it doesn't change on every update.
40-
let baseTime = 0;
41-
for (const timing of newArtifacts.Timing) {
42-
// @ts-expect-error: Value actually is writeable at this point.
43-
timing.startTime = baseTime++;
44-
// @ts-expect-error: Value actually is writeable at this point.
45-
timing.duration = 1;
46-
}
39+
assetSaver.normalizeTimingEntries(newArtifacts.Timing);
4740

4841
if (artifactName) {
4942
// Revert everything except the one artifact

0 commit comments

Comments
 (0)