Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

core(user-flow): dry run flag #13837

Open
wants to merge 15 commits into
base: main
Choose a base branch
from
1 change: 1 addition & 0 deletions docs/user-flows.md
Original file line number Diff line number Diff line change
Expand Up @@ -221,6 +221,7 @@ As this flow has multiple steps, the flow report summarizes everything and allow
- Keep timespan recordings _short_ and focused on a single interaction sequence or page transition.
- Use snapshot recordings when a substantial portion of the page content has changed.
- Always wait for transitions and interactions to finish before ending a timespan. The puppeteer APIs `page.waitForSelector`/`page.waitForFunction`/`page.waitForResponse`/`page.waitForTimeout` are your friends here.
- Quickly validate your user flow with: `api.startFlow(page, {dryRun: true})`.

## Related Reading

Expand Down
59 changes: 59 additions & 0 deletions lighthouse-core/fraggle-rock/gather/dry-run.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,59 @@
/**
* @license Copyright 2022 The Lighthouse Authors. 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 Driver = require('./driver.js');
const emulation = require('../../lib/emulation.js');
const {initializeConfig} = require('../config/config.js');
const {gotoURL} = require('../../gather/driver/navigation.js');

/**
* @param {LH.Gatherer.GatherMode} gatherMode
* @param {{page: LH.Puppeteer.Page, config?: LH.Config.Json, configContext?: LH.Config.FRContext}} options
*/
async function dryRunSetup(gatherMode, options) {
const {page, config: configJson, configContext} = options;
const {config} = await initializeConfig(configJson, {...configContext, gatherMode});
const driver = new Driver(page);
await driver.connect();
await emulation.emulate(driver.defaultSession, config.settings);
return {driver, config};
}

Check warning on line 24 in lighthouse-core/fraggle-rock/gather/dry-run.js

View check run for this annotation

Codecov / codecov/patch

lighthouse-core/fraggle-rock/gather/dry-run.js#L17-L24

Added lines #L17 - L24 were not covered by tests

/**
* @param {Exclude<LH.Gatherer.GatherMode, 'navigation'>} gatherMode
* @param {{page: LH.Puppeteer.Page, config?: LH.Config.Json, configContext?: LH.Config.FRContext}} options
*/
async function dryRun(gatherMode, options) {
const {driver} = await dryRunSetup(gatherMode, options);
await driver.disconnect();
}

Check warning on line 33 in lighthouse-core/fraggle-rock/gather/dry-run.js

View check run for this annotation

Codecov / codecov/patch

lighthouse-core/fraggle-rock/gather/dry-run.js#L30-L33

Added lines #L30 - L33 were not covered by tests

/**
* @param {LH.NavigationRequestor} requestor
* @param {{page: LH.Puppeteer.Page, config?: LH.Config.Json, configContext?: LH.Config.FRContext}} options
*/
async function dryRunNavigation(requestor, options) {
const {driver, config} = await dryRunSetup('navigation', options);
if (!config.navigations || !config.navigations.length) {
throw new Error('No navigations configured');
}

const navigation = config.navigations[0];
await gotoURL(driver, requestor, {
...navigation,
maxWaitForFcp: config.settings.maxWaitForFcp,
maxWaitForLoad: config.settings.maxWaitForLoad,
waitUntil: navigation.pauseAfterFcpMs ? ['fcp', 'load'] : ['load'],
});

await driver.disconnect();
}

Check warning on line 54 in lighthouse-core/fraggle-rock/gather/dry-run.js

View check run for this annotation

Codecov / codecov/patch

lighthouse-core/fraggle-rock/gather/dry-run.js#L39-L54

Added lines #L39 - L54 were not covered by tests

module.exports = {
dryRun,
dryRunNavigation,
};
47 changes: 36 additions & 11 deletions lighthouse-core/fraggle-rock/user-flow.js
Original file line number Diff line number Diff line change
Expand Up @@ -9,11 +9,12 @@
const {snapshotGather} = require('./gather/snapshot-runner.js');
const {startTimespanGather} = require('./gather/timespan-runner.js');
const {navigationGather} = require('./gather/navigation-runner.js');
const {dryRun, dryRunNavigation} = require('./gather/dry-run.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'> & {name?: string, dryRun?: boolean}} UserFlowOptions */
/** @typedef {Omit<FrOptions, 'page'> & {stepName?: string}} StepOptions */
/** @typedef {WeakMap<LH.UserFlow.GatherStep, LH.Gatherer.FRGatherResult['runnerOptions']>} GatherStepRunnerOptions */

Expand All @@ -24,9 +25,11 @@
*/
constructor(page, options) {
/** @type {FrOptions} */
this.options = {page, ...options};
this._options = {page, ...options};

Check warning on line 28 in lighthouse-core/fraggle-rock/user-flow.js

View check run for this annotation

Codecov / codecov/patch

lighthouse-core/fraggle-rock/user-flow.js#L28

Added line #L28 was not covered by tests
/** @type {string|undefined} */
this.name = options?.name;
this._name = options?.name;
/** @type {boolean} */
this._dryRun = options?.dryRun === true;

Check warning on line 32 in lighthouse-core/fraggle-rock/user-flow.js

View check run for this annotation

Codecov / codecov/patch

lighthouse-core/fraggle-rock/user-flow.js#L30-L32

Added lines #L30 - L32 were not covered by tests
/** @type {LH.UserFlow.GatherStep[]} */
this._gatherSteps = [];
/** @type {GatherStepRunnerOptions} */
Expand Down Expand Up @@ -62,7 +65,7 @@
* @param {StepOptions=} stepOptions
*/
_getNextNavigationOptions(stepOptions) {
const options = {...this.options, ...stepOptions};
const options = {...this._options, ...stepOptions};

Check warning on line 68 in lighthouse-core/fraggle-rock/user-flow.js

View check run for this annotation

Codecov / codecov/patch

lighthouse-core/fraggle-rock/user-flow.js#L68

Added line #L68 was not covered by tests
const configContext = {...options.configContext};
const settingsOverrides = {...configContext.settingsOverrides};

Expand Down Expand Up @@ -111,8 +114,13 @@
if (this.currentNavigation) throw new Error('Navigation already in progress');

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

if (this._dryRun) {
await dryRunNavigation(requestor, options);
return;
}

const gatherResult = await navigationGather(requestor, options);

Check warning on line 123 in lighthouse-core/fraggle-rock/user-flow.js

View check run for this annotation

Codecov / codecov/patch

lighthouse-core/fraggle-rock/user-flow.js#L118-L123

Added lines #L118 - L123 were not covered by tests
this._addGatherStep(gatherResult, options);
}

Expand Down Expand Up @@ -174,13 +182,19 @@
async startTimespan(stepOptions) {
if (this.currentTimespan) throw new Error('Timespan already in progress');
if (this.currentNavigation) throw new Error('Navigation already in progress');
const options = {...this._options, ...stepOptions};

if (this._dryRun) {
await dryRun('timespan', options);
Copy link
Member

Choose a reason for hiding this comment

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

What happens to emulation when the driver disconnects? Does it persist or go away?

Copy link
Member Author

@adamraine adamraine Jun 22, 2022

Choose a reason for hiding this comment

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

https://bugs.chromium.org/p/chromium/issues/detail?id=1337089

TLDR it goes back to default, even if another session exists that is overriding the device emulation.

return;
}

Check warning on line 190 in lighthouse-core/fraggle-rock/user-flow.js

View check run for this annotation

Codecov / codecov/patch

lighthouse-core/fraggle-rock/user-flow.js#L185-L190

Added lines #L185 - L190 were not covered by tests

const options = {...this.options, ...stepOptions};
const timespan = await startTimespanGather(options);
this.currentTimespan = {timespan, options};
}

async endTimespan() {
if (this._dryRun) return;

Check warning on line 197 in lighthouse-core/fraggle-rock/user-flow.js

View check run for this annotation

Codecov / codecov/patch

lighthouse-core/fraggle-rock/user-flow.js#L197

Added line #L197 was not covered by tests
if (!this.currentTimespan) throw new Error('No timespan in progress');
if (this.currentNavigation) throw new Error('Navigation already in progress');

Expand All @@ -197,20 +211,30 @@
async snapshot(stepOptions) {
if (this.currentTimespan) throw new Error('Timespan already in progress');
if (this.currentNavigation) throw new Error('Navigation already in progress');
const options = {...this._options, ...stepOptions};

Check warning on line 214 in lighthouse-core/fraggle-rock/user-flow.js

View check run for this annotation

Codecov / codecov/patch

lighthouse-core/fraggle-rock/user-flow.js#L214

Added line #L214 was not covered by tests

const options = {...this.options, ...stepOptions};
const gatherResult = await snapshotGather(options);
if (this._dryRun) {
await dryRun('snapshot', options);
return;
}

Check warning on line 219 in lighthouse-core/fraggle-rock/user-flow.js

View check run for this annotation

Codecov / codecov/patch

lighthouse-core/fraggle-rock/user-flow.js#L216-L219

Added lines #L216 - L219 were not covered by tests

const gatherResult = await snapshotGather(options);

Check warning on line 221 in lighthouse-core/fraggle-rock/user-flow.js

View check run for this annotation

Codecov / codecov/patch

lighthouse-core/fraggle-rock/user-flow.js#L221

Added line #L221 was not covered by tests
this._addGatherStep(gatherResult, options);
}

/**
* @returns {Promise<LH.FlowResult>}
*/
async createFlowResult() {
if (this._dryRun) {
return {
name: 'Dry run',
steps: [],
};
}

Check warning on line 234 in lighthouse-core/fraggle-rock/user-flow.js

View check run for this annotation

Codecov / codecov/patch

lighthouse-core/fraggle-rock/user-flow.js#L229-L234

Added lines #L229 - L234 were not covered by tests
return auditGatherSteps(this._gatherSteps, {
name: this.name,
config: this.options.config,
name: this._name,
config: this._options.config,

Check warning on line 237 in lighthouse-core/fraggle-rock/user-flow.js

View check run for this annotation

Codecov / codecov/patch

lighthouse-core/fraggle-rock/user-flow.js#L236-L237

Added lines #L236 - L237 were not covered by tests
gatherStepRunnerOptions: this._gatherStepRunnerOptions,
});
}
Expand All @@ -219,6 +243,7 @@
* @return {Promise<string>}
*/
async generateReport() {
if (this._dryRun) return '<h1>Cannot generate a flow report from a dry run</h1>';

Check warning on line 246 in lighthouse-core/fraggle-rock/user-flow.js

View check run for this annotation

Codecov / codecov/patch

lighthouse-core/fraggle-rock/user-flow.js#L246

Added line #L246 was not covered by tests
const flowResult = await this.createFlowResult();
return generateFlowReportHtml(flowResult);
}
Expand All @@ -229,7 +254,7 @@
createArtifactsJson() {
return {
gatherSteps: this._gatherSteps,
name: this.name,
name: this._name,

Check warning on line 257 in lighthouse-core/fraggle-rock/user-flow.js

View check run for this annotation

Codecov / codecov/patch

lighthouse-core/fraggle-rock/user-flow.js#L257

Added line #L257 was not covered by tests
};
}
}
Expand Down
2 changes: 1 addition & 1 deletion lighthouse-core/scripts/update-flow-fixtures.js
Original file line number Diff line number Diff line change
Expand Up @@ -89,7 +89,7 @@ async function rebaselineArtifacts(artifactKeys) {
});

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

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

Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,72 @@
/**
* @license Copyright 2022 The Lighthouse Authors. 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.
*/

import {jest} from '@jest/globals';

import * as api from '../../../fraggle-rock/api.js';
import {createTestState} from './pptr-test-utils.js';
import {LH_ROOT} from '../../../../root.js';

/* eslint-env jest */
/* eslint-env browser */

jest.setTimeout(90_000);

describe('Dry Run', () => {
const state = createTestState();

state.installSetupAndTeardownHooks();

beforeAll(() => {
state.server.baseDir = `${LH_ROOT}/lighthouse-core/test/fixtures/fraggle-rock/snapshot-basic`;
});

it('should setup environment and perform flow actions', async () => {
const pageUrl = `${state.serverBaseUrl}/onclick.html`;

const flow = await api.startFlow(state.page, {dryRun: true});
await flow.navigate(pageUrl);

await flow.startNavigation();
await state.page.goto(pageUrl);
await flow.endNavigation();

await flow.startTimespan();
await state.page.click('button');
await flow.endTimespan();

await flow.snapshot();

// Ensure page navigated occurred and button was clicked.
const finalUrl = await state.page.url();
expect(finalUrl).toEqual(`${pageUrl}#done`);

// Ensure Lighthouse emulated a mobile device.
// Device scale factor override is not shared between sessions
// https://bugs.chromium.org/p/chromium/issues/detail?id=1337089
const deviceMetrics = await state.page.evaluate(() => ({
width: document.documentElement.clientWidth,
height: document.documentElement.clientHeight,
adamraine marked this conversation as resolved.
Show resolved Hide resolved
// deviceScaleFactor: window.devicePixelRatio,
}));
expect(deviceMetrics).toEqual({
height: 640,
width: 360,
// deviceScaleFactor: 2,
});

expect(flow.createArtifactsJson()).toEqual({
gatherSteps: [],
});
expect(await flow.createFlowResult()).toEqual({
name: 'Dry run',
steps: [],
});
expect(await flow.generateReport()).toEqual(
'<h1>Cannot generate a flow report from a dry run</h1>'
);
});
});
47 changes: 47 additions & 0 deletions lighthouse-core/test/fraggle-rock/user-flow-test.js
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,8 @@ const navigationModule = {navigationGather: jest.fn()};
jest.mock('../../fraggle-rock/gather/navigation-runner.js', () => navigationModule);
const timespanModule = {startTimespanGather: jest.fn()};
jest.mock('../../fraggle-rock/gather/timespan-runner.js', () => timespanModule);
const dryRunModule = {dryRun: jest.fn(), dryRunNavigation: jest.fn()};
jest.mock('../../fraggle-rock/gather/dry-run.js', () => dryRunModule);

const mockRunner = mockRunnerModule();

Expand All @@ -38,6 +40,9 @@ describe('UserFlow', () => {

mockRunner.reset();

dryRunModule.dryRun.mockReset();
dryRunModule.dryRunNavigation.mockReset();

snapshotModule.snapshotGather.mockReset();
snapshotModule.snapshotGather.mockResolvedValue({
artifacts: {
Expand Down Expand Up @@ -162,6 +167,14 @@ describe('UserFlow', () => {
expect(configContext).toEqual({settingsOverrides: {maxWaitForLoad: 1000}});
expect(configContextExplicit).toEqual({skipAboutBlank: false});
});

it('should perform a minimal navigation in a dry run', async () => {
const flow = new UserFlow(mockPage.asPage(), {dryRun: true});
await flow.navigate('https://example.com/1');

expect(navigationModule.navigationGather).not.toHaveBeenCalled();
expect(dryRunModule.dryRunNavigation).toHaveBeenCalled();
});
});

describe('.startNavigation()', () => {
Expand Down Expand Up @@ -238,13 +251,29 @@ describe('UserFlow', () => {
{name: 'Timespan report (www.example.com/)'},
]);
});

it('should setup emulation in a dry run', async () => {
const flow = new UserFlow(mockPage.asPage(), {dryRun: true});
await flow.startTimespan();

expect(timespanModule.startTimespanGather).not.toHaveBeenCalled();
expect(dryRunModule.dryRun).toHaveBeenCalled();
});
});

describe('.endTimespan()', () => {
it('should throw if a timespan has not started', async () => {
const flow = new UserFlow(mockPage.asPage());
await expect(flow.endTimespan()).rejects.toBeTruthy();
});

it('should do nothing in a dry run', async () => {
const flow = new UserFlow(mockPage.asPage(), {dryRun: true});
await flow.endTimespan();

expect(timespanModule.startTimespanGather).not.toHaveBeenCalled();
expect(dryRunModule.dryRun).not.toHaveBeenCalled();
});
});

describe('.snapshot()', () => {
Expand All @@ -266,6 +295,14 @@ describe('UserFlow', () => {
{name: 'Snapshot report (www.example.com/)'},
]);
});

it('should setup emulation in a dry run', async () => {
const flow = new UserFlow(mockPage.asPage(), {dryRun: true});
await flow.snapshot();

expect(snapshotModule.snapshotGather).not.toHaveBeenCalled();
expect(dryRunModule.dryRun).toHaveBeenCalled();
});
});

describe('.getFlowResult', () => {
Expand All @@ -275,6 +312,16 @@ describe('UserFlow', () => {
await expect(flowResultPromise).rejects.toThrow(/Need at least one step/);
});

it('should return shell object after a dry run', async () => {
const flow = new UserFlow(mockPage.asPage(), {dryRun: true});
await flow.snapshot();
const flowResult = await flow.createFlowResult();
await expect(flowResult).toEqual({
name: 'Dry run',
steps: [],
});
});

it('should audit active gather steps', async () => {
mockRunner.audit.mockImplementation(artifacts => ({
lhr: {
Expand Down