Skip to content

Commit

Permalink
core(user-flow): audit flow from artifacts json (#13715)
Browse files Browse the repository at this point in the history
  • Loading branch information
adamraine committed Mar 8, 2022
1 parent 5488cbe commit 13eb37f
Show file tree
Hide file tree
Showing 11 changed files with 923,624 additions and 2,674 deletions.
14 changes: 12 additions & 2 deletions lighthouse-core/fraggle-rock/api.js
Expand Up @@ -5,16 +5,16 @@
*/
'use strict';

const {UserFlow, auditGatherSteps} = require('./user-flow.js');
const {snapshotGather} = require('./gather/snapshot-runner.js');
const {startTimespanGather} = require('./gather/timespan-runner.js');
const {navigationGather} = require('./gather/navigation-runner.js');
const {generateFlowReportHtml} = require('../../report/generator/report-generator.js');
const Runner = require('../runner.js');
const UserFlow = require('./user-flow.js');

/**
* @param {import('puppeteer').Page} page
* @param {UserFlow.UserFlowOptions} [options]
* @param {ConstructorParameters<LH.UserFlow>[1]} [options]
*/
async function startFlow(page, options) {
return new UserFlow(page, options);
Expand Down Expand Up @@ -58,10 +58,20 @@ async function generateFlowReport(flowResult) {
return generateFlowReportHtml(flowResult);
}

/**
* @param {LH.UserFlow.FlowArtifacts} flowArtifacts
* @param {LH.Config.Json} [config]
*/
async function auditFlowArtifacts(flowArtifacts, config) {
const {gatherSteps, name} = flowArtifacts;
return await auditGatherSteps(gatherSteps, {name, config});
}

module.exports = {
snapshot,
startTimespan,
navigation,
startFlow,
generateFlowReport,
auditFlowArtifacts,
};
128 changes: 88 additions & 40 deletions lighthouse-core/fraggle-rock/user-flow.js
Expand Up @@ -10,11 +10,12 @@ const {snapshotGather} = require('./gather/snapshot-runner.js');
const {startTimespanGather} = require('./gather/timespan-runner.js');
const {navigationGather} = require('./gather/navigation-runner.js');
const Runner = require('../runner.js');
const {initializeConfig} = require('./config/config.js');

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

class UserFlow {
/**
Expand All @@ -26,8 +27,10 @@ class UserFlow {
this.options = {page, ...options};
/** @type {string|undefined} */
this.name = options?.name;
/** @type {StepArtifact[]} */
this.stepArtifacts = [];
/** @type {LH.UserFlow.GatherStep[]} */
this._gatherSteps = [];
/** @type {GatherStepRunnerOptions} */
this._gatherStepRunnerOptions = new WeakMap();
}

/**
Expand Down Expand Up @@ -68,8 +71,8 @@ class UserFlow {
}

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

/**
*
* @param {LH.Gatherer.FRGatherResult} gatherResult
* @param {StepOptions} options
*/
_addGatherStep(gatherResult, options) {
const providedName = options?.stepName;
const gatherStep = {
artifacts: gatherResult.artifacts,
name: providedName || this._getDefaultStepName(gatherResult.artifacts),
config: options.config,
configContext: options.configContext,
};
this._gatherSteps.push(gatherStep);
this._gatherStepRunnerOptions.set(gatherStep, gatherResult.runnerOptions);
}

/**
* @param {LH.NavigationRequestor} requestor
* @param {StepOptions=} stepOptions
*/
async navigate(requestor, stepOptions) {
if (this.currentTimespan) throw new Error('Timespan already in progress');

const gatherResult = await navigationGather(
requestor,
this._getNextNavigationOptions(stepOptions)
);
const options = this._getNextNavigationOptions(stepOptions);
const gatherResult = await navigationGather(requestor, options);

const providedName = stepOptions?.stepName;
this.stepArtifacts.push({
gatherResult,
name: providedName || this._getDefaultStepName(gatherResult.artifacts),
});
this._addGatherStep(gatherResult, options);

return gatherResult;
}
Expand All @@ -121,11 +135,7 @@ class UserFlow {
const gatherResult = await timespan.endTimespanGather();
this.currentTimespan = undefined;

const providedName = options?.stepName;
this.stepArtifacts.push({
gatherResult,
name: providedName || this._getDefaultStepName(gatherResult.artifacts),
});
this._addGatherStep(gatherResult, options);

return gatherResult;
}
Expand All @@ -139,11 +149,7 @@ class UserFlow {
const options = {...this.options, ...stepOptions};
const gatherResult = await snapshotGather(options);

const providedName = stepOptions?.stepName;
this.stepArtifacts.push({
gatherResult,
name: providedName || this._getDefaultStepName(gatherResult.artifacts),
});
this._addGatherStep(gatherResult, options);

return gatherResult;
}
Expand All @@ -152,21 +158,11 @@ class UserFlow {
* @returns {Promise<LH.FlowResult>}
*/
async createFlowResult() {
if (!this.stepArtifacts.length) {
throw new Error('Need at least one step before getting the result');
}
const url = new URL(this.stepArtifacts[0].gatherResult.artifacts.URL.finalUrl);
const flowName = this.name || `User flow (${url.hostname})`;

/** @type {LH.FlowResult['steps']} */
const steps = [];
for (const {gatherResult, name} of this.stepArtifacts) {
const result = await Runner.audit(gatherResult.artifacts, gatherResult.runnerOptions);
if (!result) throw new Error(`Step "${name}" did not return a result`);
steps.push({lhr: result.lhr, name});
}

return {steps, name: flowName};
return auditGatherSteps(this._gatherSteps, {
name: this.name,
config: this.options.config,
gatherStepRunnerOptions: this._gatherStepRunnerOptions,
});
}

/**
Expand All @@ -176,6 +172,58 @@ class UserFlow {
const flowResult = await this.createFlowResult();
return generateFlowReportHtml(flowResult);
}

/**
* @return {LH.UserFlow.FlowArtifacts}
*/
createArtifactsJson() {
return {
gatherSteps: this._gatherSteps,
name: this.name,
};
}
}

/**
* @param {Array<LH.UserFlow.GatherStep>} gatherSteps
* @param {{name?: string, config?: LH.Config.Json, gatherStepRunnerOptions?: GatherStepRunnerOptions}} options
*/
async function auditGatherSteps(gatherSteps, options) {
if (!gatherSteps.length) {
throw new Error('Need at least one step before getting the result');
}

/** @type {LH.FlowResult['steps']} */
const steps = [];
for (const gatherStep of gatherSteps) {
const {artifacts, name, configContext} = gatherStep;

let runnerOptions = options.gatherStepRunnerOptions?.get(gatherStep);

// If the gather step is not active, we must recreate the runner options.
if (!runnerOptions) {
// Step specific configs take precedence over a config for the entire flow.
const configJson = gatherStep.config || options.config;
const {gatherMode} = artifacts.GatherContext;
const {config} = initializeConfig(configJson, {...configContext, gatherMode});
runnerOptions = {
config,
computedCache: new Map(),
};
}

const result = await Runner.audit(artifacts, runnerOptions);
if (!result) throw new Error(`Step "${name}" did not return a result`);
steps.push({lhr: result.lhr, name});
}

const url = new URL(gatherSteps[0].artifacts.URL.finalUrl);
const flowName = options.name || `User flow (${url.hostname})`;
return {steps, name: flowName};
}

module.exports = UserFlow;

module.exports = {
UserFlow,
auditGatherSteps,
};
15 changes: 15 additions & 0 deletions lighthouse-core/lib/asset-saver.js
Expand Up @@ -313,6 +313,20 @@ async function saveLanternNetworkData(devtoolsLog, outputPath) {
fs.writeFileSync(outputPath, JSON.stringify(lanternData));
}

/**
* Normalize timing data so it doesn't change every update.
* @param {LH.Result.MeasureEntry[]} timings
*/
function normalizeTimingEntries(timings) {
let baseTime = 0;
for (const timing of timings) {
// @ts-expect-error: Value actually is writeable at this point.
timing.startTime = baseTime++;
// @ts-expect-error: Value actually is writeable at this point.
timing.duration = 1;
}
}

module.exports = {
saveArtifacts,
saveLhr,
Expand All @@ -323,4 +337,5 @@ module.exports = {
saveDevtoolsLog,
saveLanternNetworkData,
stringifyReplacer,
normalizeTimingEntries,
};
90 changes: 63 additions & 27 deletions lighthouse-core/scripts/update-flow-fixtures.js
Expand Up @@ -14,7 +14,13 @@ import puppeteer from 'puppeteer';

import {LH_ROOT} from '../../root.js';
import api from '../fraggle-rock/api.js';
import assetSaver from '../lib/asset-saver.js';

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

/** @param {puppeteer.Page} page */
async function waitForImagesToLoad(page) {
Expand Down Expand Up @@ -45,49 +51,79 @@ async function waitForImagesToLoad(page) {
}, TIMEOUT);
}

(async () => {
/** @type {LH.Config.Json} */
const config = {
extends: 'lighthouse:default',
settings: {
skipAudits: ['uses-http2'],
},
};

async function rebaselineArtifacts() {
const browser = await puppeteer.launch({
ignoreDefaultArgs: ['--enable-automation'],
executablePath: process.env.CHROME_PATH,
headless: false,
});

try {
const page = await browser.newPage();
const flow = await api.startFlow(page);
const page = await browser.newPage();
const flow = await api.startFlow(page, {config});

await flow.navigate('https://www.mikescerealshack.co');
await flow.navigate('https://www.mikescerealshack.co');

await flow.startTimespan({stepName: 'Search input'});
await page.type('input', 'call of duty');
const networkQuietPromise = page.waitForNavigation({waitUntil: ['networkidle0']});
await page.click('button[type=submit]');
await networkQuietPromise;
await waitForImagesToLoad(page);
await flow.endTimespan();
await flow.startTimespan({stepName: 'Search input'});
await page.type('input', 'call of duty');
const networkQuietPromise = page.waitForNavigation({waitUntil: ['networkidle0']});
await page.click('button[type=submit]');
await networkQuietPromise;
await waitForImagesToLoad(page);
await flow.endTimespan();

await flow.snapshot({stepName: 'Search results'});
await flow.snapshot({stepName: 'Search results'});

await flow.navigate('https://www.mikescerealshack.co/corrections');
await flow.navigate('https://www.mikescerealshack.co/corrections');

const flowResult = await flow.createFlowResult();
await browser.close();

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

if (process.argv.includes('--view')) {
const htmlReport = await api.generateFlowReport(flowResult);
const filepath = `${LH_ROOT}/dist/sample-reports/flow-report/index.html`;
fs.writeFileSync(filepath, htmlReport);
open(filepath);
}
// Normalize some data so it doesn't change on every update.
for (const {artifacts} of flowArtifacts.gatherSteps) {
assetSaver.normalizeTimingEntries(artifacts.Timing);
}

fs.writeFileSync(ARTIFACTS_PATH, JSON.stringify(flowArtifacts, null, 2));
}

process.exit(0);
async function generateFlowResult() {
/** @type {LH.UserFlow.FlowArtifacts} */
const flowArtifacts = JSON.parse(fs.readFileSync(ARTIFACTS_PATH, 'utf-8'));
const flowResult = await api.auditFlowArtifacts(flowArtifacts, config);

// Normalize some data so it doesn't change on every update.
for (const {lhr} of flowResult.steps) {
assetSaver.normalizeTimingEntries(lhr.timing.entries);
lhr.timing.total = lhr.timing.entries.length;
}

fs.writeFileSync(FLOW_RESULT_PATH, JSON.stringify(flowResult, null, 2));

if (process.argv.includes('--view')) {
const htmlReport = await api.generateFlowReport(flowResult);
fs.writeFileSync(FLOW_REPORT_PATH, htmlReport);
open(FLOW_REPORT_PATH);
}
}

(async () => {
try {
if (process.argv.includes('--rebaseline-artifacts')) {
await rebaselineArtifacts();
}
await generateFlowResult();
} catch (err) {
console.error(err);
await browser.close();
process.exit(1);
}
})();

9 changes: 1 addition & 8 deletions lighthouse-core/scripts/update-report-fixtures.js
Expand Up @@ -36,14 +36,7 @@ async function update(artifactName) {

let newArtifacts = assetSaver.loadArtifacts(artifactPath);

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

if (artifactName) {
// Revert everything except the one artifact
Expand Down

0 comments on commit 13eb37f

Please sign in to comment.