Skip to content

Commit

Permalink
clients(viewer): add support for flow reports (#13260)
Browse files Browse the repository at this point in the history
  • Loading branch information
adamraine authored Nov 24, 2021
1 parent de947ab commit d1bd86a
Show file tree
Hide file tree
Showing 25 changed files with 301 additions and 85 deletions.
2 changes: 1 addition & 1 deletion build/build-report.js
Original file line number Diff line number Diff line change
Expand Up @@ -58,7 +58,7 @@ async function buildStandaloneReport() {

async function buildFlowReport() {
const bundle = await rollup.rollup({
input: 'flow-report/standalone-flow.tsx',
input: 'flow-report/clients/standalone.ts',
plugins: [
rollupPlugins.inlineFs({verbose: true}),
rollupPlugins.replace({
Expand Down
10 changes: 10 additions & 0 deletions build/build-viewer.js
Original file line number Diff line number Diff line change
Expand Up @@ -43,6 +43,7 @@ async function run() {
html: {path: 'index.html'},
stylesheets: [
{path: 'styles/*'},
{path: '../../flow-report/assets/styles.css'},
],
javascripts: [
reportGeneratorJs,
Expand All @@ -51,6 +52,15 @@ async function run() {
rollupPlugins.shim({
'./locales.js': 'export default {}',
}),
rollupPlugins.typescript({
tsconfig: 'flow-report/tsconfig.json',
// Plugin struggles with custom outDir, so revert it from tsconfig value
// as well as any options that require an outDir is set.
outDir: null,
composite: false,
emitDeclarationOnly: false,
declarationMap: false,
}),
rollupPlugins.inlineFs({verbose: Boolean(process.env.DEBUG)}),
rollupPlugins.replace({
values: {
Expand Down
22 changes: 19 additions & 3 deletions build/gh-pages-app.js
Original file line number Diff line number Diff line change
Expand Up @@ -79,20 +79,22 @@ class GhPagesApp {
constructor(opts) {
this.opts = opts;
this.distDir = `${ghPagesDistDir}/${opts.name}`;
/** @type {string[]} */
this.preloadScripts = [];
}

async build() {
fs.rmSync(this.distDir, {recursive: true, force: true});

const bundledJs = await this._compileJs();
safeWriteFile(`${this.distDir}/src/bundled.js`, bundledJs);

const html = await this._compileHtml();
safeWriteFile(`${this.distDir}/index.html`, html);

const css = await this._compileCss();
safeWriteFile(`${this.distDir}/styles/bundled.css`, css);

const bundledJs = await this._compileJs();
safeWriteFile(`${this.distDir}/src/bundled.js`, bundledJs);

for (const {path, destDir, rename} of this.opts.assets) {
const dir = destDir ? `${this.distDir}/${destDir}` : this.distDir;
await cpy(path, dir, {
Expand Down Expand Up @@ -153,6 +155,7 @@ class GhPagesApp {
];
if (!process.env.DEBUG) plugins.push(rollupPlugins.terser());
const bundle = await rollup.rollup({
preserveEntrySignatures: 'strict',
input,
plugins,
});
Expand All @@ -166,6 +169,8 @@ class GhPagesApp {
safeWriteFile(`${this.distDir}/src/${output[i].fileName}`, code);
}
}
const scripts = output[0].imports.map(fileName => `src/${fileName}`);
this.preloadScripts.push(...scripts);
return output[0].code;
}

Expand All @@ -179,6 +184,17 @@ class GhPagesApp {
}
}

if (this.preloadScripts.length) {
const preloads = this.preloadScripts.map(fileName =>
`<link rel="preload" href="${fileName}" as="script" crossorigin="anonymous" />`
).join('\n');
const endHeadIndex = htmlSrc.indexOf('</head>');
if (endHeadIndex === -1) {
throw new Error('HTML file needs a <head> element to inject preloads');
}
htmlSrc = htmlSrc.slice(0, endHeadIndex) + preloads + htmlSrc.slice(endHeadIndex);
}

return htmlSrc;
}

Expand Down
18 changes: 18 additions & 0 deletions flow-report/api.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
/**
* @license Copyright 2021 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 {render, h} from 'preact';

import {App} from './src/app';

export function renderFlowReport(
flowResult: LH.FlowResult,
root: HTMLElement,
options?: LH.FlowReportOptions
) {
root.classList.add('flow-vars', 'lh-vars', 'lh-root');
render(h(App, {flowResult, options}), root);
}
Original file line number Diff line number Diff line change
Expand Up @@ -9,15 +9,14 @@
* The renderer code is bundled and injected into the report HTML along with the JSON report.
*/

import {render} from 'preact';

import {App} from './src/app';
import {renderFlowReport} from '../api';

function __initLighthouseFlowReport__() {
const container = document.body.querySelector('main');
if (!container) throw Error('Container element not found');
container.classList.add('flow-vars', 'lh-root', 'lh-vars');
render(<App flowResult={window.__LIGHTHOUSE_FLOW_JSON__} />, container);
renderFlowReport(window.__LIGHTHOUSE_FLOW_JSON__, container, {
getReportHtml: () => document.documentElement.outerHTML,
});
}

window.__initLighthouseFlowReport__ = __initLighthouseFlowReport__;
Expand Down
32 changes: 20 additions & 12 deletions flow-report/src/app.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -5,15 +5,16 @@
*/

import {FunctionComponent} from 'preact';
import {useLayoutEffect, useRef, useState} from 'preact/hooks';
import {useLayoutEffect, useMemo, useRef, useState} from 'preact/hooks';

import {Sidebar} from './sidebar/sidebar';
import {Summary} from './summary/summary';
import {classNames, FlowResultContext, useHashState} from './util';
import {classNames, FlowResultContext, OptionsContext, useHashState} from './util';
import {Report} from './wrappers/report';
import {Topbar} from './topbar';
import {Header} from './header';
import {I18nProvider} from './i18n/i18n';
import {Styles} from './wrappers/styles';

function getAnchorElement(hashState: LH.FlowResult.HashState|null) {
if (!hashState || !hashState.anchor) return null;
Expand Down Expand Up @@ -47,17 +48,24 @@ const Content: FunctionComponent = () => {
);
};

export const App: FunctionComponent<{flowResult: LH.FlowResult}> = ({flowResult}) => {
export const App: FunctionComponent<{
flowResult: LH.FlowResult,
options?: LH.FlowReportOptions
}> = ({flowResult, options}) => {
const [collapsed, setCollapsed] = useState(false);
const optionsValue = useMemo(() => options || {}, [options]);
return (
<FlowResultContext.Provider value={flowResult}>
<I18nProvider>
<div className={classNames('App', {'App--collapsed': collapsed})} data-testid="App">
<Topbar onMenuClick={() => setCollapsed(c => !c)} />
<Sidebar/>
<Content/>
</div>
</I18nProvider>
</FlowResultContext.Provider>
<OptionsContext.Provider value={optionsValue}>
<FlowResultContext.Provider value={flowResult}>
<I18nProvider>
<Styles/>
<div className={classNames('App', {'App--collapsed': collapsed})} data-testid="App">
<Topbar onMenuClick={() => setCollapsed(c => !c)} />
<Sidebar/>
<Content/>
</div>
</I18nProvider>
</FlowResultContext.Provider>
</OptionsContext.Provider>
);
};
4 changes: 1 addition & 3 deletions flow-report/src/sidebar/flow.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -48,8 +48,6 @@ export const SidebarFlow: FunctionComponent = () => {
{
flowResult.steps.map((step, index) => {
const {lhr, name} = step;
const url = new URL(location.href);
url.hash = `#index=${index}`;
return <>
{
lhr.gatherMode === 'navigation' && index !== 0 ?
Expand All @@ -59,7 +57,7 @@ export const SidebarFlow: FunctionComponent = () => {
<SidebarFlowStep
key={lhr.fetchTime}
mode={lhr.gatherMode}
href={url.href}
href={`#index=${index}`}
label={name}
isCurrent={index === (hashState && hashState.index)}
/>
Expand Down
4 changes: 1 addition & 3 deletions flow-report/src/sidebar/sidebar.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -17,11 +17,9 @@ const SidebarSummary: FunctionComponent = () => {
const hashState = useHashState();
const strings = useLocalizedStrings();

const url = new URL(location.href);
url.hash = '#';
return (
<a
href={url.href}
href="#"
className={classNames('SidebarSummary', {'Sidebar--current': hashState === null})}
data-testid="SidebarSummary"
>
Expand Down
35 changes: 22 additions & 13 deletions flow-report/src/topbar.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -8,20 +8,15 @@ import {FunctionComponent, JSX} from 'preact';
import {useState} from 'preact/hooks';

import {HelpDialog} from './help-dialog';
import {getFilenamePrefix} from '../../report/generator/file-namer';
import {getFlowResultFilenamePrefix} from '../../report/generator/file-namer';
import {useLocalizedStrings} from './i18n/i18n';
import {HamburgerIcon, InfoIcon} from './icons';
import {useFlowResult} from './util';
import {useFlowResult, useOptions} from './util';
import {saveFile} from '../../report/renderer/api';

function saveHtml(flowResult: LH.FlowResult) {
const htmlStr = document.documentElement.outerHTML;
function saveHtml(flowResult: LH.FlowResult, htmlStr: string) {
const blob = new Blob([htmlStr], {type: 'text/html'});

const lhr = flowResult.steps[0].lhr;
const name = flowResult.name.replace(/\s/g, '-');
const filename = getFilenamePrefix(name, lhr.fetchTime);

const filename = getFlowResultFilenamePrefix(flowResult);
saveFile(blob, filename);
}

Expand Down Expand Up @@ -78,6 +73,7 @@ export const Topbar: FunctionComponent<{onMenuClick: JSX.MouseEventHandler<HTMLB
const flowResult = useFlowResult();
const strings = useLocalizedStrings();
const [showHelpDialog, setShowHelpDialog] = useState(false);
const {getReportHtml, saveAsGist} = useOptions();

return (
<div className="Topbar">
Expand All @@ -88,10 +84,23 @@ export const Topbar: FunctionComponent<{onMenuClick: JSX.MouseEventHandler<HTMLB
<Logo/>
</div>
<div className="Topbar__title">{strings.title}</div>
<TopbarButton
onClick={() => saveHtml(flowResult)}
label="Button that saves the report as HTML"
>{strings.save}</TopbarButton>
{
getReportHtml &&
<TopbarButton
onClick={() => {
const htmlStr = getReportHtml(flowResult);
saveHtml(flowResult, htmlStr);
}}
label="Button that saves the report as HTML"
>{strings.save}</TopbarButton>
}
{
saveAsGist &&
<TopbarButton
onClick={() => saveAsGist(flowResult)}
label="Button that saves the report to a gist"
>{strings.dropdownSaveGist}</TopbarButton>
}
<div style={{flexGrow: 1}} />
<TopbarButton
onClick={() => setShowHelpDialog(previous => !previous)}
Expand Down
7 changes: 7 additions & 0 deletions flow-report/src/util.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ import {useContext, useEffect, useLayoutEffect, useMemo, useRef, useState} from
import type {UIStringsType} from './i18n/ui-strings';

const FlowResultContext = createContext<LH.FlowResult|undefined>(undefined);
const OptionsContext = createContext<LH.FlowReportOptions>({});

function getHashParam(param: string): string|null {
const params = new URLSearchParams(location.hash.replace('#', '?'));
Expand Down Expand Up @@ -147,8 +148,13 @@ function useExternalRenderer<T extends Element>(
return ref;
}

function useOptions() {
return useContext(OptionsContext);
}

export {
FlowResultContext,
OptionsContext,
classNames,
getScreenDimensions,
getFullPageScreenshot,
Expand All @@ -158,4 +164,5 @@ export {
useHashParams,
useHashState,
useExternalRenderer,
useOptions,
};
1 change: 1 addition & 0 deletions flow-report/src/wrappers/report.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,7 @@ export const Report: FunctionComponent<{hashState: LH.FlowResult.HashState}> =
return renderReport(hashState.currentLhr, {
disableAutoDarkModeAndFireworks: true,
omitTopbar: true,
omitGlobalStyles: true,
onPageAnchorRendered: link => convertAnchor(link, hashState.index),
});
}, [hashState]);
Expand Down
16 changes: 16 additions & 0 deletions flow-report/src/wrappers/styles.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
/**
* @license Copyright 2021 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 {FunctionComponent} from 'preact';

import {createStylesElement} from '../../../report/renderer/api';
import {useExternalRenderer} from '../util';

export const Styles: FunctionComponent = () => {
const ref = useExternalRenderer<HTMLDivElement>(createStylesElement);

return <div ref={ref}/>;
};
28 changes: 22 additions & 6 deletions flow-report/test/topbar-test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@ import {jest} from '@jest/globals';
import {FunctionComponent} from 'preact';
import {act, render} from '@testing-library/preact';

import {FlowResultContext} from '../src/util';
import {FlowResultContext, OptionsContext} from '../src/util';
import {I18nProvider} from '../src/i18n/i18n';

const mockSaveFile = jest.fn();
Expand All @@ -31,19 +31,24 @@ const flowResult = {
} as any;

let wrapper: FunctionComponent;
let options: LH.FlowReportOptions;

beforeEach(() => {
mockSaveFile.mockReset();
options = {};
wrapper = ({children}) => (
<FlowResultContext.Provider value={flowResult}>
<I18nProvider>
{children}
</I18nProvider>
</FlowResultContext.Provider>
<OptionsContext.Provider value={options}>
<FlowResultContext.Provider value={flowResult}>
<I18nProvider>
{children}
</I18nProvider>
</FlowResultContext.Provider>
</OptionsContext.Provider>
);
});

it('save button opens save dialog for HTML file', async () => {
options = {getReportHtml: () => '<html></html>'};
const root = render(<Topbar onMenuClick={() => {}}/>, {wrapper});

const saveButton = root.getByText('Save');
Expand All @@ -55,6 +60,17 @@ it('save button opens save dialog for HTML file', async () => {
);
});

it('provides save as gist option if defined', async () => {
const saveAsGist = jest.fn();
options = {saveAsGist};
const root = render(<Topbar onMenuClick={() => {}}/>, {wrapper});

const saveButton = root.getByText('Save as Gist');
saveButton.click();

expect(saveAsGist).toHaveBeenCalledWith(flowResult);
});

it('toggles help dialog', async () => {
const root = render(<Topbar onMenuClick={() => {}}/>, {wrapper});

Expand Down
4 changes: 4 additions & 0 deletions flow-report/types/flow-report.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,10 @@ declare global {
// Expose global types in LH namespace.
module LH {
export type ConfigSettings = Settings.ConfigSettings;
export interface FlowReportOptions {
getReportHtml?: (flowResult: FlowResult_) => string;
saveAsGist?: (flowResult: FlowResult_) => void;
}

export import FlowResult = FlowResult_;
}
Expand Down
Loading

0 comments on commit d1bd86a

Please sign in to comment.