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(fr): add navigations to config #11957

Merged
merged 2 commits into from
Jan 19, 2021
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
16 changes: 16 additions & 0 deletions lighthouse-core/config/constants.js
Original file line number Diff line number Diff line change
Expand Up @@ -136,6 +136,21 @@ const defaultPassConfig = {
gatherers: [],
};

/** @type {Required<LH.Config.NavigationJson>} */
const defaultNavigationConfig = {
id: 'default',
loadFailureMode: 'fatal',
disableThrottling: false,
disableStorageReset: false,
pauseAfterFcpMs: 0,
pauseAfterLoadMs: 0,
networkQuietThresholdMs: 0,
cpuQuietThresholdMs: 0,
blockedUrlPatterns: [],
blankPage: 'about:blank',
artifacts: [],
};

const nonSimulatedPassConfigOverrides = {
pauseAfterFcpMs: 5250,
pauseAfterLoadMs: 5250,
Expand All @@ -149,5 +164,6 @@ module.exports = {
userAgents,
defaultSettings,
defaultPassConfig,
defaultNavigationConfig,
nonSimulatedPassConfigOverrides,
};
37 changes: 36 additions & 1 deletion lighthouse-core/fraggle-rock/config/config.js
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ const path = require('path');
const log = require('lighthouse-logger');
const Runner = require('../../runner.js');
const defaultConfig = require('./default-config.js');
const {defaultNavigationConfig} = require('../../config/constants.js');
const {isFRGathererDefn} = require('./validation.js');
const {filterConfigByGatherMode} = require('./filters.js');
const {
Expand Down Expand Up @@ -75,6 +76,39 @@ function resolveArtifactsToDefns(artifacts, configDir) {
return artifactDefns;
}

/**
*
* @param {LH.Config.NavigationJson[]|null|undefined} navigations
* @param {LH.Config.ArtifactDefn[]|null|undefined} artifactDefns
* @return {LH.Config.NavigationDefn[] | null}
*/
function resolveNavigationsToDefns(navigations, artifactDefns) {
if (!navigations) return null;
if (!artifactDefns) throw new Error('Cannot use navigations without defining artifacts');

const status = {msg: 'Resolve navigation definitions', id: 'lh:config:resolveNavigationsToDefns'};
log.time(status, 'verbose');

const artifactsById = new Map(artifactDefns.map(defn => [defn.id, defn]));

const navigationDefns = navigations.map(navigation => {
const navigationWithDefaults = {...defaultNavigationConfig, ...navigation};
const navId = navigationWithDefaults.id;
const artifacts = navigationWithDefaults.artifacts.map(id => {
const artifact = artifactsById.get(id);
if (!artifact) throw new Error(`Unrecognized artifact "${id}" in navigation "${navId}"`);
return artifact;
});

// TODO(FR-COMPAT): enforce navigation throttling invariants

return {...navigationWithDefaults, artifacts};
});

log.timeEnd(status);
return navigationDefns;
}

/**
* @param {LH.Config.Json|undefined} configJSON
* @param {{gatherMode: LH.Gatherer.GatherMode, configPath?: string, settingsOverrides?: LH.SharedFlagsSettings}} context
Expand All @@ -88,14 +122,15 @@ function initializeConfig(configJSON, context) {

// TODO(FR-COMPAT): handle config extension
// TODO(FR-COMPAT): handle config plugins
// TODO(FR-COMPAT): enforce navigation invariants

const settings = resolveSettings(configWorkingCopy.settings || {}, context.settingsOverrides);
const artifacts = resolveArtifactsToDefns(configWorkingCopy.artifacts, configDir);
const navigations = resolveNavigationsToDefns(configWorkingCopy.navigations, artifacts);

/** @type {LH.Config.FRConfig} */
let config = {
artifacts,
navigations,
audits: resolveAuditsToDefns(configWorkingCopy.audits, configDir),
categories: configWorkingCopy.categories || null,
groups: configWorkingCopy.groups || null,
Expand Down
6 changes: 6 additions & 0 deletions lighthouse-core/fraggle-rock/config/default-config.js
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,12 @@ const defaultConfig = {
{id: 'Accessibility', gatherer: 'accessibility'},
{id: 'ConsoleMessages', gatherer: 'console-messages'},
],
navigations: [
{
id: 'default',
artifacts: ['Accessibility', 'ConsoleMessages'],
},
],
settings: legacyDefaultConfig.settings,
audits: legacyDefaultConfig.audits,
categories: legacyDefaultConfig.categories,
Expand Down
65 changes: 61 additions & 4 deletions lighthouse-core/test/fraggle-rock/config/config-test.js
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,12 @@ const {initializeConfig} = require('../../../fraggle-rock/config/config.js');
/* eslint-env jest */

describe('Fraggle Rock Config', () => {
const gatherMode = 'snapshot';
/** @type {LH.Gatherer.GatherMode} */
let gatherMode = 'snapshot';

beforeEach(() => {
gatherMode = 'snapshot';
});

it('should throw if the config path is not absolute', () => {
const configFn = () =>
Expand Down Expand Up @@ -62,6 +67,61 @@ describe('Fraggle Rock Config', () => {
expect(() => initializeConfig(configJson, {gatherMode})).toThrow(/ImageElements gatherer/);
});

it('should resolve navigation definitions', () => {
gatherMode = 'navigation';
const configJson = {
artifacts: [{id: 'Accessibility', gatherer: 'accessibility'}],
navigations: [{id: 'default', artifacts: ['Accessibility']}],
};
const {config} = initializeConfig(configJson, {gatherMode});

expect(config).toMatchObject({
artifacts: [{id: 'Accessibility', gatherer: {path: 'accessibility'}}],
navigations: [
{id: 'default', artifacts: [{id: 'Accessibility', gatherer: {path: 'accessibility'}}]},
],
});
});

it('should throw when navigations are defined without artifacts', () => {
const configJson = {
navigations: [{id: 'default', artifacts: ['Accessibility']}],
};

expect(() => initializeConfig(configJson, {gatherMode})).toThrow(/Cannot use navigations/);
});

it('should throw when navigations use unrecognized artifacts', () => {
const configJson = {
artifacts: [],
navigations: [{id: 'default', artifacts: ['Accessibility']}],
};

expect(() => initializeConfig(configJson, {gatherMode})).toThrow(/Unrecognized artifact/);
});

it('should set default properties on navigations', () => {
gatherMode = 'navigation';
const configJson = {
artifacts: [],
navigations: [{id: 'default'}],
};
const {config} = initializeConfig(configJson, {gatherMode});

expect(config).toMatchObject({
navigations: [
{
id: 'default',
blankPage: 'about:blank',
artifacts: [],
disableThrottling: false,
networkQuietThresholdMs: 0,
cpuQuietThresholdMs: 0,
},
],
});
});

it('should filter configuration by gatherMode', () => {
const timespanGatherer = new BaseGatherer();
timespanGatherer.meta = {supportedModes: ['timespan']};
Expand All @@ -83,10 +143,7 @@ describe('Fraggle Rock Config', () => {

it.todo('should support extension');
it.todo('should support plugins');
it.todo('should set default properties on navigations');
it.todo('should adjust default pass options for throttling method');
it.todo('should normalize gatherer inputs');
it.todo('should require gatherers from their paths');
it.todo('should filter configuration by inclusive settings');
it.todo('should filter configuration by exclusive settings');
it.todo('should validate audit/gatherer interdependencies');
Expand Down
9 changes: 8 additions & 1 deletion lighthouse-core/test/fraggle-rock/config/filters-test.js
Original file line number Diff line number Diff line change
Expand Up @@ -129,7 +129,14 @@ describe('Fraggle Rock Config Filtering', () => {

describe('filterConfigByGatherMode', () => {
it('should filter the entire config', () => {
const config = {artifacts, audits, categories, groups: null, settings: defaultSettings};
const config = {
artifacts,
navigations: null,
audits,
categories,
groups: null,
settings: defaultSettings,
};
expect(filters.filterConfigByGatherMode(config, 'snapshot')).toMatchObject({
artifacts: [{id: 'Snapshot'}],
audits: [{implementation: SnapshotAudit}],
Expand Down
54 changes: 47 additions & 7 deletions types/config.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -20,12 +20,17 @@ declare global {
export interface Json {
extends?: 'lighthouse:default' | string;
settings?: SharedFlagsSettings;
artifacts?: ArtifactJson[] | null;
passes?: PassJson[] | null;
audits?: Config.AuditJson[] | null;
categories?: Record<string, CategoryJson> | null;
groups?: Record<string, Config.GroupJson> | null;
plugins?: Array<string>,
plugins?: Array<string>;

// Fraggle Rock Only
artifacts?: ArtifactJson[] | null;
navigations?: NavigationJson[] | null;

// Legacy Only
passes?: PassJson[] | null;
}

/**
Expand All @@ -45,25 +50,56 @@ declare global {
export interface FRConfig {
settings: Settings;
artifacts: ArtifactDefn[] | null;
navigations: NavigationDefn[] | null;
audits: AuditDefn[] | null;
categories: Record<string, Category> | null;
groups: Record<string, Group> | null;
}

export interface PassJson {
passName: string;
interface SharedPassNavigationJson {
/**
* Controls the behavior when the navigation fails to complete (due to server error, no FCP, etc).
* Fatal means Lighthouse will exit immediately and return a runtimeError / non-zero exit code.
* Warn means a toplevel warning will appear in the report, but the run will complete with success.
* Ignore means a failure is expected and no action should be taken.
*/
loadFailureMode?: 'fatal'|'warn'|'ignore';
recordTrace?: boolean;
useThrottling?: boolean;
/** The number of milliseconds to wait after FCP until the page should be considered loaded. */
pauseAfterFcpMs?: number;
/** The number of milliseconds to wait after the load event until the page should be considered loaded. */
pauseAfterLoadMs?: number;
/** The number of milliseconds to wait between high priority network requests or 3 simulataneous requests before the page should be considered loaded. */
networkQuietThresholdMs?: number;
/** The number of milliseconds to wait between long tasks until the page should be considered loaded. */
cpuQuietThresholdMs?: number;
/** Substring patterns of network resources to block during this navigation, '*' wildcards supported though unnecessary as prefix or suffix (due to substring matching). */
blockedUrlPatterns?: string[];
/** The URL to use for the "blank" neutral page in between navigations. Defaults to `about:blank`. */
blankPage?: string;
}

export interface PassJson extends SharedPassNavigationJson {
/** The identifier for the pass. Config extension will deduplicate passes with the same passName. */
passName: string;
/** Whether a trace and devtoolsLog should be recorded for the pass. */
recordTrace?: boolean;
/** Whether throttling settings should be used for the pass. */
useThrottling?: boolean;
/** The array of gatherers to run during the pass. */
gatherers?: GathererJson[];
}

export interface NavigationJson extends SharedPassNavigationJson {
/** The identifier for the navigation. Config extension will deduplicate navigations with the same id. */
id: string;
/** Whether throttling settings should be skipped for the pass. */
disableThrottling?: boolean;
/** Whether storage clearing (service workers, cache storage) should be skipped for the pass. A run-wide setting of `true` takes precedence over this value. */
disableStorageReset?: boolean;
/** The array of artifacts to collect during the navigation. */
artifacts?: Array<string>;
}

export interface ArtifactJson {
id: string;
gatherer: GathererJson;
Expand Down Expand Up @@ -120,6 +156,10 @@ declare global {
gatherers: GathererDefn[];
}

export interface NavigationDefn extends Omit<Required<NavigationJson>, 'artifacts'> {
artifacts: ArtifactDefn[];
}

export interface ArtifactDefn {
id: string;
gatherer: FRGathererDefn;
Expand Down