report(flow): report api (#13374)
This commit is contained in:
Родитель
fe3daf0b8c
Коммит
d3f338d9a7
|
@ -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'] = {
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
|
||||
|
|
Загрузка…
Ссылка в новой задаче