This commit is contained in:
Adam Raine 2021-11-18 14:56:40 -05:00 коммит произвёл GitHub
Родитель fe3daf0b8c
Коммит d3f338d9a7
Не найден ключ, соответствующий данной подписи
Идентификатор ключа GPG: 4AEE18F83AFDEB23
22 изменённых файлов: 268 добавлений и 225 удалений

Просмотреть файл

@ -28,12 +28,10 @@ limitations under the License.
<body>
<noscript>Lighthouse report requires JavaScript. Please enable.</noscript>
<main class="flow-vars lh-root lh-vars"><!-- report populated here --></main>
<main><!-- report populated here --></main>
<script>window.__LIGHTHOUSE_FLOW_JSON__ = %%LIGHTHOUSE_FLOW_JSON%%;</script>
<script>%%LIGHTHOUSE_FLOW_JAVASCRIPT%%
__initLighthouseFlowReport__();
</script>
<script>%%LIGHTHOUSE_FLOW_JAVASCRIPT%%</script>
<script>console.log('window.__LIGHTHOUSE_FLOW_JSON__', __LIGHTHOUSE_FLOW_JSON__);</script>
</body>
</html>

Просмотреть файл

@ -7,7 +7,6 @@
import {FunctionComponent} from 'preact';
import {useLayoutEffect, useRef, useState} from 'preact/hooks';
import {ReportRendererProvider} from './wrappers/report-renderer';
import {Sidebar} from './sidebar/sidebar';
import {Summary} from './summary/summary';
import {classNames, FlowResultContext, useHashState} from './util';
@ -52,15 +51,13 @@ export const App: FunctionComponent<{flowResult: LH.FlowResult}> = ({flowResult}
const [collapsed, setCollapsed] = useState(false);
return (
<FlowResultContext.Provider value={flowResult}>
<ReportRendererProvider>
<I18nProvider>
<div className={classNames('App', {'App--collapsed': collapsed})} data-testid="App">
<Topbar onMenuClick={() => setCollapsed(c => !c)} />
<Sidebar/>
<Content/>
</div>
</I18nProvider>
</ReportRendererProvider>
<I18nProvider>
<div className={classNames('App', {'App--collapsed': collapsed})} data-testid="App">
<Topbar onMenuClick={() => setCollapsed(c => !c)} />
<Sidebar/>
<Content/>
</div>
</I18nProvider>
</FlowResultContext.Provider>
);
};

Просмотреть файл

@ -12,11 +12,9 @@ import {getFilenamePrefix} from '../../report/generator/file-namer';
import {useLocalizedStrings} from './i18n/i18n';
import {HamburgerIcon, InfoIcon} from './icons';
import {useFlowResult} from './util';
import {useReportRenderer} from './wrappers/report-renderer';
import {saveFile} from '../../report/renderer/api';
import type {DOM} from '../../report/renderer/dom';
function saveHtml(flowResult: LH.FlowResult, dom: DOM) {
function saveHtml(flowResult: LH.FlowResult) {
const htmlStr = document.documentElement.outerHTML;
const blob = new Blob([htmlStr], {type: 'text/html'});
@ -24,7 +22,7 @@ function saveHtml(flowResult: LH.FlowResult, dom: DOM) {
const name = flowResult.name.replace(/\s/g, '-');
const filename = getFilenamePrefix(name, lhr.fetchTime);
dom.saveFile(blob, filename);
saveFile(blob, filename);
}
/* eslint-disable max-len */
@ -78,7 +76,6 @@ const TopbarButton: FunctionComponent<{
export const Topbar: FunctionComponent<{onMenuClick: JSX.MouseEventHandler<HTMLButtonElement>}> =
({onMenuClick}) => {
const flowResult = useFlowResult();
const {dom} = useReportRenderer();
const strings = useLocalizedStrings();
const [showHelpDialog, setShowHelpDialog] = useState(false);
@ -92,7 +89,7 @@ export const Topbar: FunctionComponent<{onMenuClick: JSX.MouseEventHandler<HTMLB
</div>
<div className="Topbar__title">{strings.title}</div>
<TopbarButton
onClick={() => saveHtml(flowResult, dom)}
onClick={() => saveHtml(flowResult)}
label="Button that saves the report as HTML"
>{strings.save}</TopbarButton>
<div style={{flexGrow: 1}} />

Просмотреть файл

@ -5,7 +5,7 @@
*/
import {createContext} from 'preact';
import {useContext, useEffect, useMemo, useState} from 'preact/hooks';
import {useContext, useEffect, useLayoutEffect, useMemo, useRef, useState} from 'preact/hooks';
import type {UIStringsType} from './i18n/ui-strings';
@ -121,6 +121,32 @@ function useHashState(): LH.FlowResult.HashState|null {
}, [indexString, flowResult, anchor]);
}
/**
* Creates a DOM subtree from non-preact code (e.g. LH report renderer).
* @param renderCallback Callback that renders a DOM subtree.
* @param inputs Changes to these values will trigger a re-render of the DOM subtree.
* @return Reference to the element that will contain the DOM subtree.
*/
function useExternalRenderer<T extends Element>(
renderCallback: () => Node,
inputs?: ReadonlyArray<unknown>
) {
const ref = useRef<T>(null);
useLayoutEffect(() => {
if (!ref.current) return;
const root = renderCallback();
ref.current.appendChild(root);
return () => {
if (ref.current?.contains(root)) ref.current.removeChild(root);
};
}, inputs);
return ref;
}
export {
FlowResultContext,
classNames,
@ -131,4 +157,5 @@ export {
useFlowResult,
useHashParams,
useHashState,
useExternalRenderer,
};

Просмотреть файл

@ -5,37 +5,22 @@
*/
import {FunctionComponent} from 'preact';
import {useEffect, useLayoutEffect, useRef} from 'preact/hooks';
import {useReportRenderer} from './report-renderer';
import {renderCategoryScore} from '../../../report/renderer/api';
import {useExternalRenderer} from '../util';
export const CategoryScore: FunctionComponent<{
category: LH.ReportResult.Category,
href: string,
gatherMode: LH.Result.GatherMode,
}> = ({category, href, gatherMode}) => {
const {categoryRenderer} = useReportRenderer();
const ref = useRef<HTMLDivElement>(null);
useLayoutEffect(() => {
const el = categoryRenderer.renderCategoryScore(category, {}, {gatherMode});
// Category label is displayed in the navigation header.
const label = el.querySelector('.lh-gauge__label,.lh-fraction__label');
if (label) label.remove();
if (ref.current) ref.current.append(el);
return () => {
if (ref.current && ref.current.contains(el)) {
ref.current.removeChild(el);
}
};
}, [categoryRenderer, category]);
useEffect(() => {
const anchor = ref.current && ref.current.querySelector('a') as HTMLAnchorElement;
if (anchor) anchor.href = href;
}, [href]);
const ref = useExternalRenderer<HTMLDivElement>(() => {
return renderCategoryScore(category, {
gatherMode,
omitLabel: true,
onPageAnchorRendered: link => link.href = href,
});
}, [category, href]);
return (
<div ref={ref} data-testid="CategoryScore"/>

Просмотреть файл

@ -5,22 +5,13 @@
*/
import {FunctionComponent} from 'preact';
import {useLayoutEffect, useRef} from 'preact/hooks';
import {useReportRenderer} from '../wrappers/report-renderer';
import {convertMarkdownCodeSnippets} from '../../../report/renderer/api';
import {useExternalRenderer} from '../util';
export const Markdown: FunctionComponent<{text: string}> = ({text}) => {
const {dom} = useReportRenderer();
const ref = useRef<HTMLSpanElement>(null);
useLayoutEffect(() => {
if (ref.current) {
const md = dom.convertMarkdownCodeSnippets(text);
ref.current.appendChild(md);
}
return () => {
if (ref.current) ref.current.innerHTML = '';
};
const ref = useExternalRenderer<HTMLSpanElement>(() => {
return convertMarkdownCodeSnippets(text);
}, [text]);
return <span ref={ref}/>;

Просмотреть файл

@ -1,53 +0,0 @@
/**
* @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 {createContext, FunctionComponent} from 'preact';
import {useContext, useMemo} from 'preact/hooks';
import {CategoryRenderer} from '../../../report/renderer/category-renderer';
import {DetailsRenderer} from '../../../report/renderer/details-renderer';
import {DOM} from '../../../report/renderer/dom';
import {ReportRenderer} from '../../../report/renderer/report-renderer';
interface ReportRendererGlobals {
dom: DOM,
detailsRenderer: DetailsRenderer,
categoryRenderer: CategoryRenderer,
reportRenderer: ReportRenderer,
}
const ReportRendererContext = createContext<ReportRendererGlobals|undefined>(undefined);
function useReportRenderer() {
const globals = useContext(ReportRendererContext);
if (!globals) throw Error('Globals not defined');
return globals;
}
const ReportRendererProvider: FunctionComponent = ({children}) => {
const globals = useMemo(() => {
// @ts-expect-error Still using legacy
const dom = new DOM(document);
const detailsRenderer = new DetailsRenderer(dom);
const categoryRenderer = new CategoryRenderer(dom, detailsRenderer);
const reportRenderer = new ReportRenderer(dom);
return {
dom,
detailsRenderer,
categoryRenderer,
reportRenderer,
};
}, []);
return (
<ReportRendererContext.Provider value={globals}>{children}</ReportRendererContext.Provider>
);
};
export {
ReportRendererContext,
ReportRendererProvider,
useReportRenderer,
};

Просмотреть файл

@ -5,73 +5,41 @@
*/
import {FunctionComponent} from 'preact';
import {useLayoutEffect, useRef} from 'preact/hooks';
import {ElementScreenshotRenderer} from '../../../report/renderer/element-screenshot-renderer';
import {getFullPageScreenshot} from '../util';
import {useReportRenderer} from './report-renderer';
import {renderReport} from '../../../report/renderer/api.js';
import {useExternalRenderer} from '../util';
/**
* The default behavior of anchor links is not compatible with the flow report's hash navigation.
* This function converts any anchor links under the provided element to a flow report link.
* This function converts a category score anchor link to a flow report link.
* e.g. <a href="#link"> -> <a href="#index=0&anchor=link">
*/
function convertChildAnchors(element: HTMLElement, index: number) {
const links = element.querySelectorAll('a[href]') as NodeListOf<HTMLAnchorElement>;
for (const link of links) {
// Check if the link destination is in the report.
const currentUrl = new URL(location.href);
currentUrl.hash = '';
currentUrl.search = '';
const linkUrl = new URL(link.href);
linkUrl.hash = '';
linkUrl.search = '';
if (currentUrl.href !== linkUrl.href || !link.hash) continue;
function convertAnchor(link: HTMLAnchorElement, index: number) {
// Clear existing event listeners by cloning node.
const newLink = link.cloneNode(true) as HTMLAnchorElement;
if (!newLink.hash) return newLink;
const nodeId = link.hash.substr(1);
link.hash = `#index=${index}&anchor=${nodeId}`;
link.onclick = e => {
e.preventDefault();
const el = document.getElementById(nodeId);
if (el) el.scrollIntoView();
};
}
const nodeId = link.hash.substr(1);
newLink.hash = `#index=${index}&anchor=${nodeId}`;
newLink.onclick = e => {
e.preventDefault();
const el = document.getElementById(nodeId);
if (el) el.scrollIntoView();
};
link.replaceWith(newLink);
}
const Report: FunctionComponent<{hashState: LH.FlowResult.HashState}> =
export const Report: FunctionComponent<{hashState: LH.FlowResult.HashState}> =
({hashState}) => {
const {dom, reportRenderer} = useReportRenderer();
const ref = useRef<HTMLDivElement>(null);
useLayoutEffect(() => {
if (ref.current) {
dom.clearComponentCache();
reportRenderer.renderReport(hashState.currentLhr, ref.current);
convertChildAnchors(ref.current, hashState.index);
const fullPageScreenshot = getFullPageScreenshot(hashState.currentLhr);
if (fullPageScreenshot) {
ElementScreenshotRenderer.installOverlayFeature({
dom,
rootEl: ref.current,
overlayContainerEl: ref.current,
fullPageScreenshot,
});
}
const topbar = ref.current.querySelector('.lh-topbar');
if (topbar) topbar.remove();
}
return () => {
if (ref.current) ref.current.textContent = '';
};
}, [reportRenderer, hashState]);
const ref = useExternalRenderer<HTMLDivElement>(() => {
return renderReport(hashState.currentLhr, {
disableAutoDarkModeAndFireworks: true,
omitTopbar: true,
onPageAnchorRendered: link => convertAnchor(link, hashState.index),
});
}, [hashState]);
return (
<div ref={ref} className="lh-root" data-testid="Report"/>
<div ref={ref} data-testid="Report"/>
);
};
export {
convertChildAnchors,
Report,
};

Просмотреть файл

@ -13,12 +13,12 @@ import {render} from 'preact';
import {App} from './src/app';
// Used by standalone-flow.html
function __initLighthouseFlowReport__() {
// TODO(adamraine): add lh-vars, etc classes programmatically instead of in the HTML template
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);
}
window.__initLighthouseFlowReport__ = __initLighthouseFlowReport__;
window.__initLighthouseFlowReport__();

Просмотреть файл

@ -9,18 +9,24 @@ import {JSDOM} from 'jsdom';
/**
* The jest environment "jsdom" does not work when preact is combined with the report renderer.
* This sets up our own environment with JSDOM globals.
*/
function setupJsDom() {
beforeEach(() => {
const {window} = new JSDOM(undefined, {
url: 'file:///Users/example/report.html/',
});
global.window = window as any;
global.document = window.document;
global.location = window.location;
global.self = global.window;
// Use JSDOM types as necessary.
global.Blob = window.Blob;
global.HTMLInputElement = window.HTMLInputElement;
// Function not implemented in JSDOM.
// Functions not implemented in JSDOM.
window.Element.prototype.scrollIntoView = jest.fn();
}
global.beforeEach(setupJsDom);
global.self.matchMedia = jest.fn<any, any>(() => ({
addListener: jest.fn(),
}));
});

Просмотреть файл

@ -11,7 +11,6 @@ import {SummaryTooltip} from '../../src/summary/category';
import {flowResult} from '../sample-flow';
import {I18nProvider} from '../../src/i18n/i18n';
import {FlowResultContext} from '../../src/util';
import {ReportRendererProvider} from '../../src/wrappers/report-renderer';
let wrapper: FunctionComponent;
@ -19,11 +18,9 @@ beforeEach(() => {
// Include sample flowResult for locale in I18nProvider.
wrapper = ({children}) => (
<FlowResultContext.Provider value={flowResult}>
<ReportRendererProvider>
<I18nProvider>
{children}
</I18nProvider>
</ReportRendererProvider>
<I18nProvider>
{children}
</I18nProvider>
</FlowResultContext.Provider>
);
});

Просмотреть файл

@ -10,7 +10,6 @@ import {FunctionComponent} from 'preact';
import {I18nProvider} from '../../src/i18n/i18n';
import {SummaryHeader, SummaryFlowStep} from '../../src/summary/summary';
import {FlowResultContext} from '../../src/util';
import {ReportRendererProvider} from '../../src/wrappers/report-renderer';
import {flowResult} from '../sample-flow';
let wrapper: FunctionComponent;
@ -18,11 +17,9 @@ let wrapper: FunctionComponent;
beforeEach(() => {
wrapper = ({children}) => (
<FlowResultContext.Provider value={flowResult}>
<ReportRendererProvider>
<I18nProvider>
{children}
</I18nProvider>
</ReportRendererProvider>
<I18nProvider>
{children}
</I18nProvider>
</FlowResultContext.Provider>
);
});

Просмотреть файл

@ -8,11 +8,19 @@ import {jest} from '@jest/globals';
import {FunctionComponent} from 'preact';
import {act, render} from '@testing-library/preact';
import {Topbar} from '../src/topbar';
import {FlowResultContext} from '../src/util';
import {ReportRendererContext} from '../src/wrappers/report-renderer';
import {I18nProvider} from '../src/i18n/i18n';
const mockSaveFile = jest.fn();
jest.unstable_mockModule('../../../report/renderer/api.js', () => ({
saveFile: mockSaveFile,
}));
let Topbar: typeof import('../src/topbar').Topbar;
beforeAll(async () => {
Topbar = (await import('../src/topbar')).Topbar;
});
const flowResult = {
name: 'User flow',
steps: [{lhr: {
@ -23,22 +31,14 @@ const flowResult = {
} as any;
let wrapper: FunctionComponent;
let mockSaveFile = jest.fn();
beforeEach(() => {
mockSaveFile = jest.fn();
const reportRendererValue: any = {
dom: {
saveFile: mockSaveFile,
},
};
mockSaveFile.mockReset();
wrapper = ({children}) => (
<FlowResultContext.Provider value={flowResult}>
<ReportRendererContext.Provider value={reportRendererValue}>
<I18nProvider>
{children}
</I18nProvider>
</ReportRendererContext.Provider>
<I18nProvider>
{children}
</I18nProvider>
</FlowResultContext.Provider>
);
});

Просмотреть файл

@ -5,11 +5,12 @@
*/
import {jest} from '@jest/globals';
import {render} from '@testing-library/preact';
import {renderHook} from '@testing-library/preact-hooks';
import {FunctionComponent} from 'preact';
import {act} from 'preact/test-utils';
import {FlowResultContext, useHashState} from '../src/util';
import {FlowResultContext, useExternalRenderer, useHashState} from '../src/util';
import {flowResult} from './sample-flow';
let wrapper: FunctionComponent;
@ -94,3 +95,39 @@ describe('useHashState', () => {
expect(result.current).toBeNull();
});
});
describe('useExternalRenderer', () => {
it('attaches DOM subtree of render callback', () => {
const Container: FunctionComponent = () => {
const ref = useExternalRenderer<HTMLDivElement>(() => {
const el = document.createElement('div');
el.textContent = 'Some text';
return el;
});
return <div ref={ref}/>;
};
const root = render(<Container/>);
expect(root.getByText('Some text')).toBeTruthy();
});
it('re-renders DOM subtree when input changes', () => {
const Container: FunctionComponent<{text: string}> = ({text}) => {
const ref = useExternalRenderer<HTMLDivElement>(() => {
const el = document.createElement('div');
el.textContent = text;
return el;
}, [text]);
return <div ref={ref}/>;
};
const root = render(<Container text="Some text"/>);
expect(root.getByText('Some text')).toBeTruthy();
root.rerender(<Container text="New text"/>);
expect(root.getByText('New text')).toBeTruthy();
});
});

Просмотреть файл

@ -9,7 +9,6 @@ import {render} from '@testing-library/preact';
import {CategoryScore} from '../../src/wrappers/category-score';
import {FlowResultContext} from '../../src/util';
import {ReportRendererProvider} from '../../src/wrappers/report-renderer';
import {I18nProvider} from '../../src/i18n/i18n';
import {flowResult} from '../sample-flow';
@ -18,11 +17,9 @@ let wrapper: FunctionComponent;
beforeEach(() => {
wrapper = ({children}) => (
<FlowResultContext.Provider value={flowResult}>
<ReportRendererProvider>
<I18nProvider>
{children}
</I18nProvider>
</ReportRendererProvider>
<I18nProvider>
{children}
</I18nProvider>
</FlowResultContext.Provider>
);
});

Просмотреть файл

@ -5,24 +5,12 @@
*/
import {render} from '@testing-library/preact';
import {FunctionComponent} from 'preact';
import {Markdown} from '../../src/wrappers/markdown';
import {ReportRendererProvider} from '../../src/wrappers/report-renderer';
let wrapper: FunctionComponent;
beforeEach(() => {
wrapper = ({children}) => (
<ReportRendererProvider>
{children}
</ReportRendererProvider>
);
});
describe('Markdown', () => {
it('renders markdown text', () => {
const root = render(<Markdown text="Some `fancy` text"/>, {wrapper});
const root = render(<Markdown text="Some `fancy` text"/>);
const text = root.getByText(/^Some.*text$/);
expect(text.innerHTML).toEqual('Some <code>fancy</code> text');
});

Просмотреть файл

@ -9,13 +9,15 @@
import {DOM} from '../renderer/dom.js';
import {ReportRenderer} from '../renderer/report-renderer.js';
import {ReportUIFeatures} from '../renderer/report-ui-features.js';
import {CategoryRenderer} from './category-renderer.js';
import {DetailsRenderer} from './details-renderer.js';
/**
* @param {LH.Result} lhr
* @param {LH.Renderer.Options} opts
* @return {HTMLElement}
*/
export function renderReport(lhr, opts = {}) {
function renderReport(lhr, opts = {}) {
const rootEl = document.createElement('article');
rootEl.classList.add('lh-root', 'lh-vars');
@ -30,3 +32,35 @@ export function renderReport(lhr, opts = {}) {
features.initFeatures(lhr);
return rootEl;
}
/**
* @param {LH.ReportResult.Category} category
* @param {Parameters<CategoryRenderer['renderCategoryScore']>[2]=} options
* @return {DocumentFragment}
*/
function renderCategoryScore(category, options) {
const dom = new DOM(document, document.documentElement);
const detailsRenderer = new DetailsRenderer(dom);
const categoryRenderer = new CategoryRenderer(dom, detailsRenderer);
return categoryRenderer.renderCategoryScore(category, {}, options);
}
/**
* @param {Blob} blob
* @param {string} filename
*/
function saveFile(blob, filename) {
const dom = new DOM(document, document.documentElement);
dom.saveFile(blob, filename);
}
/**
* @param {string} markdownText
* @return {Element}
*/
function convertMarkdownCodeSnippets(markdownText) {
const dom = new DOM(document, document.documentElement);
return dom.convertMarkdownCodeSnippets(markdownText);
}
export {renderReport, renderCategoryScore, saveFile, convertMarkdownCodeSnippets};

Просмотреть файл

@ -348,14 +348,28 @@ export class CategoryRenderer {
/**
* @param {LH.ReportResult.Category} category
* @param {Record<string, LH.Result.ReportGroup>} groupDefinitions
* @param {{gatherMode: LH.Result.GatherMode}=} options
* @param {{gatherMode: LH.Result.GatherMode, omitLabel?: boolean, onPageAnchorRendered?: (link: HTMLAnchorElement) => void}=} options
* @return {DocumentFragment}
*/
renderCategoryScore(category, groupDefinitions, options) {
let categoryScore;
if (options && Util.shouldDisplayAsFraction(options.gatherMode)) {
return this.renderCategoryFraction(category);
categoryScore = this.renderCategoryFraction(category);
} else {
categoryScore = this.renderScoreGauge(category, groupDefinitions);
}
return this.renderScoreGauge(category, groupDefinitions);
if (options?.omitLabel) {
const label = this.dom.find('.lh-gauge__label,.lh-fraction__label', categoryScore);
label.remove();
}
if (options?.onPageAnchorRendered) {
const anchor = this.dom.find('a', categoryScore);
options.onPageAnchorRendered(anchor);
}
return categoryScore;
}
/**

Просмотреть файл

@ -224,6 +224,7 @@ export class ReportRenderer {
e.preventDefault();
destEl.scrollIntoView();
});
this._opts.onPageAnchorRendered?.(gaugeWrapperEl);
}

Просмотреть файл

@ -500,6 +500,34 @@ describe('CategoryRenderer', () => {
});
});
describe('renderCategoryScore', () => {
it('removes label if omitLabel is true', () => {
const options = {omitLabel: true};
const categoryScore = renderer.renderCategoryScore(
sampleResults.categories.performance,
{},
options
);
const label = categoryScore.querySelector('.lh-gauge__label,.lh-fraction__label');
assert.ok(!label);
});
it('uses custom callback if present', () => {
const options = {
onPageAnchorRendered: link => {
link.href = '#index=0&anchor=performance';
},
};
const categoryScore = renderer.renderCategoryScore(
sampleResults.categories.performance,
{},
options
);
const link = categoryScore.querySelector('a');
assert.equal(link.hash, '#index=0&anchor=performance');
});
});
it('renders audits by weight', () => {
const defaultAuditRef = {
title: '',

Просмотреть файл

@ -122,6 +122,35 @@ describe('ReportRenderer', () => {
}
});
it('renders score gauges with custom callback', () => {
const sampleResultsCopy = JSON.parse(JSON.stringify(sampleResults));
const opts = {
onPageAnchorRendered: link => {
const id = link.hash.substring(1);
link.hash = `#index=0&anchor=${id}`;
},
};
const container = renderer._dom.document().body;
const output = renderer.renderReport(sampleResultsCopy, container, opts);
const anchors = output.querySelectorAll('a.lh-gauge__wrapper, a.lh-fraction__wrapper');
const hashes = Array.from(anchors).map(anchor => anchor.hash).filter(hash => hash);
// One set for the sticky header, on set for the gauges at the top.
assert.deepStrictEqual(hashes, [
'#index=0&anchor=performance',
'#index=0&anchor=accessibility',
'#index=0&anchor=best-practices',
'#index=0&anchor=seo',
'#index=0&anchor=pwa',
'#index=0&anchor=performance',
'#index=0&anchor=accessibility',
'#index=0&anchor=best-practices',
'#index=0&anchor=seo',
'#index=0&anchor=pwa',
]);
});
it('renders plugin score gauge', () => {
const sampleResultsCopy = JSON.parse(JSON.stringify(sampleResults));
sampleResultsCopy.categories['lighthouse-plugin-someplugin'] = {

5
report/types/report-renderer.d.ts поставляемый
Просмотреть файл

@ -17,6 +17,11 @@ declare module Renderer {
/** Disable the topbar UI component */
omitTopbar?: boolean;
/**
* Convert report anchor links to a different format.
* Flow report uses this to convert `#seo` to `#index=0&anchor=seo`.
*/
onPageAnchorRendered?: (link: HTMLAnchorElement) => void;
}
}