Skip to content

Commit

Permalink
core(user-flow): i18n default names (#14455)
Browse files Browse the repository at this point in the history
  • Loading branch information
adamraine authored Oct 20, 2022
1 parent 5410eae commit 70d5652
Show file tree
Hide file tree
Showing 4 changed files with 244 additions and 25 deletions.
173 changes: 156 additions & 17 deletions core/test/user-flow-test.js
Original file line number Diff line number Diff line change
Expand Up @@ -9,19 +9,6 @@ import * as td from 'testdouble';

import {Runner} from '../runner.js';
import {createMockPage, mockRunnerModule} from './gather/mock-driver.js';
// import UserFlow from '../../user-flow.js';

// Some imports needs to be done dynamically, so that their dependencies will be mocked.
// See: https://jestjs.io/docs/ecmascript-modules#differences-between-esm-and-commonjs
// https://github.com/facebook/jest/issues/10025
/** @type {typeof import('../user-flow.js').UserFlow} */
let UserFlow;
/** @type {typeof import('../user-flow.js')['auditGatherSteps']} */
let auditGatherSteps;

before(async () => {
({UserFlow, auditGatherSteps} = await import('../user-flow.js'));
});

const snapshotModule = {snapshotGather: jestMock.fn()};
await td.replaceEsm('../gather/snapshot-runner.js', snapshotModule);
Expand All @@ -32,6 +19,11 @@ await td.replaceEsm('../gather/timespan-runner.js', timespanModule);

const mockRunner = await mockRunnerModule();

// Some imports needs to be done dynamically, so that their dependencies will be mocked.
// See: https://jestjs.io/docs/ecmascript-modules#differences-between-esm-and-commonjs
// https://github.com/facebook/jest/issues/10025
const {getStepName, getFlowName, UserFlow, auditGatherSteps} = await import('../user-flow.js');

describe('UserFlow', () => {
let mockPage = createMockPage();

Expand All @@ -45,6 +37,7 @@ describe('UserFlow', () => {
artifacts: {
URL: {finalDisplayedUrl: 'https://www.example.com'},
GatherContext: {gatherMode: 'snapshot'},
settings: {locale: 'en-US'},
},
runnerOptions: {
config: {},
Expand All @@ -57,6 +50,7 @@ describe('UserFlow', () => {
artifacts: {
URL: {finalDisplayedUrl: 'https://www.example.com'},
GatherContext: {gatherMode: 'navigation'},
settings: {locale: 'en-US'},
},
runnerOptions: {
config: {},
Expand All @@ -68,6 +62,7 @@ describe('UserFlow', () => {
artifacts: {
URL: {finalDisplayedUrl: 'https://www.example.com'},
GatherContext: {gatherMode: 'timespan'},
settings: {locale: 'en-US'},
},
runnerOptions: {
config: {},
Expand Down Expand Up @@ -331,38 +326,38 @@ describe('UserFlow', () => {
},
};

/** @type {LH.UserFlow.GatherStep[]} */
/** @type {any} */
const gatherSteps = [
{
flags: {name: 'Navigation'},
// @ts-expect-error Only these artifacts are used by the test.
artifacts: {
URL: {
requestedUrl: 'https://www.example.com',
mainDocumentUrl: 'https://www.example.com',
finalDisplayedUrl: 'https://www.example.com',
},
GatherContext: {gatherMode: 'navigation'},
settings: {locale: 'en-US'},
},
},
{
flags: {name: 'Timespan', onlyCategories: ['performance']},
// @ts-expect-error Only these artifacts are used by the test.
artifacts: {
URL: {
finalDisplayedUrl: 'https://www.example.com',
},
GatherContext: {gatherMode: 'timespan'},
settings: {locale: 'en-US'},
},
},
{
flags: {name: 'Snapshot', onlyCategories: ['accessibility']},
// @ts-expect-error Only these artifacts are used by the test.
artifacts: {
URL: {
finalDisplayedUrl: 'https://www.example.com',
},
GatherContext: {gatherMode: 'snapshot'},
settings: {locale: 'en-US'},
},
},
];
Expand Down Expand Up @@ -425,5 +420,149 @@ describe('UserFlow', () => {
});
});
});

describe('getStepName', () => {
it('returns name from flags if provided', () => {
/** @type {any} */
const artifacts = {
URL: {
finalDisplayedUrl: 'https://example.com',
},
GatherContext: {gatherMode: 'navigation'},
settings: {
locale: 'en-US',
},
};
const name = getStepName({name: 'Example name'}, artifacts);
expect(name).toEqual('Example name');
});

it('returns default navigation name if no name provided', () => {
/** @type {any} */
const artifacts = {
URL: {
finalDisplayedUrl: 'https://example.com',
},
GatherContext: {gatherMode: 'navigation'},
settings: {
locale: 'en-US',
},
};
const name = getStepName({}, artifacts);
expect(name).toEqual('Navigation report (example.com/)');
});

it('returns default timespan name if no name provided', () => {
/** @type {any} */
const artifacts = {
URL: {
finalDisplayedUrl: 'https://example.com',
},
GatherContext: {gatherMode: 'timespan'},
settings: {
locale: 'en-US',
},
};
const name = getStepName({}, artifacts);
expect(name).toEqual('Timespan report (example.com/)');
});

it('returns default snapshot name if no name provided', () => {
/** @type {any} */
const artifacts = {
URL: {
finalDisplayedUrl: 'https://example.com',
},
GatherContext: {gatherMode: 'snapshot'},
settings: {
locale: 'en-US',
},
};
const name = getStepName({}, artifacts);
expect(name).toEqual('Snapshot report (example.com/)');
});

it('throws on invalid gather mode', () => {
/** @type {any} */
const artifacts = {
URL: {
finalDisplayedUrl: 'https://example.com',
},
GatherContext: {gatherMode: 'invalid'},
settings: {
locale: 'en-US',
},
};
expect(() => getStepName({}, artifacts)).toThrow('Unsupported gather mode');
});

it('returns translated name for non-default locale', () => {
/** @type {any} */
const artifacts = {
URL: {
finalDisplayedUrl: 'https://example.com',
},
GatherContext: {gatherMode: 'navigation'},
settings: {
locale: 'en-XL',
},
};
const name = getStepName({}, artifacts);
expect(name).toEqual('N̂áv̂íĝát̂íôń r̂ép̂ór̂t́ (example.com/)');
});
});

describe('getFlowName', () => {
it('returns name from options if provided', () => {
/** @type {any} */
const gatherSteps = [{
artifacts: {
URL: {
finalDisplayedUrl: 'https://example.com',
},
GatherContext: {gatherMode: 'navigation'},
settings: {
locale: 'en-US',
},
},
}];
const name = getFlowName('Example name', gatherSteps);
expect(name).toEqual('Example name');
});

it('returns default navigation name if no name provided', () => {
/** @type {any} */
const gatherSteps = [{
artifacts: {
URL: {
finalDisplayedUrl: 'https://example.com',
},
GatherContext: {gatherMode: 'navigation'},
settings: {
locale: 'en-US',
},
},
}];
const name = getFlowName(undefined, gatherSteps);
expect(name).toEqual('User flow (example.com)');
});

it('returns translated name for non-default locale', () => {
/** @type {any} */
const gatherSteps = [{
artifacts: {
URL: {
finalDisplayedUrl: 'https://example.com',
},
GatherContext: {gatherMode: 'navigation'},
settings: {
locale: 'en-XL',
},
},
}];
const name = getFlowName(undefined, gatherSteps);
expect(name).toEqual('Ûśêŕ f̂ĺôẃ (example.com)');
});
});
});

72 changes: 64 additions & 8 deletions core/user-flow.js
Original file line number Diff line number Diff line change
Expand Up @@ -10,9 +10,46 @@ import {startTimespanGather} from './gather/timespan-runner.js';
import {navigationGather} from './gather/navigation-runner.js';
import {Runner} from './runner.js';
import {initializeConfig} from './config/config.js';
import {getFormatted} from '../shared/localization/format.js';
import * as i18n from './lib/i18n/i18n.js';

/** @typedef {WeakMap<LH.UserFlow.GatherStep, LH.Gatherer.FRGatherResult['runnerOptions']>} GatherStepRunnerOptions */

const UIStrings = {
/**
* @description Default name for a user flow on the given url. "User flow" refers to the series of page navigations and user interactions being tested on the page.
* @example {https://example.com} url
*/
defaultFlowName: 'User flow ({url})',
/**
* @description Default name for a user flow step that analyzes a page navigation.
* @example {https://example.com} url
*/
defaultNavigationName: 'Navigation report ({url})',
/**
* @description Default name for a user flow step that analyzes user interactions over a period of time.
* @example {https://example.com} url
*/
defaultTimespanName: 'Timespan report ({url})',
/**
* @description Default name for a user flow step that analyzes the page state at a point in time.
* @example {https://example.com} url
*/
defaultSnapshotName: 'Snapshot report ({url})',
};

const str_ = i18n.createIcuMessageFn(import.meta.url, UIStrings);

/**
* @param {string} message
* @param {Record<string, string | number>} values
* @param {LH.Locale} locale
*/
function translate(message, values, locale) {
const icuMessage = str_(message, values);
return getFormatted(icuMessage, locale);
}

class UserFlow {
/**
* @param {LH.Puppeteer.Page} page
Expand Down Expand Up @@ -215,23 +252,41 @@ function shortenUrl(longUrl) {
}

/**
* @param {LH.UserFlow.StepFlags|undefined} flags
* @param {LH.Artifacts} artifacts
* @return {string}
*/
function getDefaultStepName(artifacts) {
function getStepName(flags, artifacts) {
if (flags?.name) return flags.name;

const {locale} = artifacts.settings;
const shortUrl = shortenUrl(artifacts.URL.finalDisplayedUrl);
switch (artifacts.GatherContext.gatherMode) {
case 'navigation':
return `Navigation report (${shortUrl})`;
return translate(UIStrings.defaultNavigationName, {url: shortUrl}, locale);
case 'timespan':
return `Timespan report (${shortUrl})`;
return translate(UIStrings.defaultTimespanName, {url: shortUrl}, locale);
case 'snapshot':
return `Snapshot report (${shortUrl})`;
return translate(UIStrings.defaultSnapshotName, {url: shortUrl}, locale);
default:
throw new Error('Unsupported gather mode');
}
}

/**
* @param {string|undefined} name
* @param {LH.UserFlow.GatherStep[]} gatherSteps
* @return {string}
*/
function getFlowName(name, gatherSteps) {
if (name) return name;

const firstArtifacts = gatherSteps[0].artifacts;
const {locale} = firstArtifacts.settings;
const url = new URL(firstArtifacts.URL.finalDisplayedUrl).hostname;
return translate(UIStrings.defaultFlowName, {url}, locale);
}

/**
* @param {Array<LH.UserFlow.GatherStep>} gatherSteps
* @param {{name?: string, config?: LH.Config.Json, gatherStepRunnerOptions?: GatherStepRunnerOptions}} options
Expand All @@ -245,7 +300,7 @@ async function auditGatherSteps(gatherSteps, options) {
const steps = [];
for (const gatherStep of gatherSteps) {
const {artifacts, flags} = gatherStep;
const name = flags?.name || getDefaultStepName(artifacts);
const name = getStepName(flags, artifacts);

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

Expand All @@ -266,13 +321,14 @@ async function auditGatherSteps(gatherSteps, options) {
steps.push({lhr: result.lhr, name});
}

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


export {
UserFlow,
auditGatherSteps,
getStepName,
getFlowName,
UIStrings,
};
12 changes: 12 additions & 0 deletions shared/localization/locales/en-US.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

Loading

0 comments on commit 70d5652

Please sign in to comment.