diff --git a/flow-report/assets/styles.css b/flow-report/assets/styles.css
index 155b2ff75393..c20c664cf4f0 100644
--- a/flow-report/assets/styles.css
+++ b/flow-report/assets/styles.css
@@ -26,6 +26,10 @@
font-size: var(--app-font-size);
}
+.Content {
+ overflow-y: auto;
+}
+
.FlowStepIcon {
height: 100%;
display: flex;
diff --git a/flow-report/jest.config.js b/flow-report/jest.config.js
index 616f66148201..15dccfb2cd26 100644
--- a/flow-report/jest.config.js
+++ b/flow-report/jest.config.js
@@ -5,9 +5,12 @@
*/
module.exports = {
- testEnvironment: 'jsdom',
+ testEnvironment: 'node',
preset: 'ts-jest',
globalSetup: './test/setup/global-setup.ts',
+ setupFilesAfterEnv: [
+ './test/setup/env-setup.ts',
+ ],
testMatch: [
'**/test/**/*-test.ts',
'**/test/**/*-test.tsx',
diff --git a/flow-report/src/app.tsx b/flow-report/src/app.tsx
index 8f51c698f6d9..093f2c142425 100644
--- a/flow-report/src/app.tsx
+++ b/flow-report/src/app.tsx
@@ -9,26 +9,22 @@ import {ReportRendererProvider} from './wrappers/report-renderer';
import {Sidebar} from './sidebar/sidebar';
import {Summary} from './summary/summary';
import {FlowResultContext, useCurrentLhr} from './util';
+import {Report} from './wrappers/report';
+
+const Content: FunctionComponent = () => {
+ const currentLhr = useCurrentLhr();
-const Report: FunctionComponent<{lhr: LH.Result}> = ({lhr}) => {
- // TODO(FR-COMPAT): Render an actual report here.
return (
-
-
{lhr.finalUrl}
+
{
- Object.values(lhr.categories).map((category) =>
-
{category.id}: {category.score}
- )
+ currentLhr ?
+ :
+
}
);
};
-const Content: FunctionComponent = () => {
- const currentLhr = useCurrentLhr();
- return currentLhr ?
: ;
-};
-
export const App: FunctionComponent<{flowResult: LH.FlowResult}> = ({flowResult}) => {
return (
diff --git a/flow-report/src/util.ts b/flow-report/src/util.ts
index 1868e0bc1f87..6b9b2be987f4 100644
--- a/flow-report/src/util.ts
+++ b/flow-report/src/util.ts
@@ -5,7 +5,7 @@
*/
import {createContext} from 'preact';
-import {useContext, useEffect, useState} from 'preact/hooks';
+import {useContext, useEffect, useMemo, useState} from 'preact/hooks';
export const FlowResultContext = createContext(undefined);
@@ -59,37 +59,46 @@ export function useLocale(): LH.Locale {
return flowResult.lhrs[0].configSettings.locale;
}
-export function useCurrentLhr(): {value: LH.Result, index: number}|null {
- const flowResult = useFlowResult();
- const [indexString, setIndexString] = useState(getHashParam('index'));
+export function useHashParam(param: string) {
+ const [paramValue, setParamValue] = useState(getHashParam(param));
// Use two-way-binding on the URL hash.
- // Triggers a re-render if the LHR index changes.
+ // Triggers a re-render if the param changes.
useEffect(() => {
function hashListener() {
- const newIndexString = getHashParam('index');
- if (newIndexString === indexString) return;
- setIndexString(newIndexString);
+ const newIndexString = getHashParam(param);
+ if (newIndexString === paramValue) return;
+ setParamValue(newIndexString);
}
window.addEventListener('hashchange', hashListener);
return () => window.removeEventListener('hashchange', hashListener);
- }, [indexString]);
+ }, [paramValue]);
- if (!indexString) return null;
+ return paramValue;
+}
- const index = Number(indexString);
- if (!Number.isFinite(index)) {
- console.warn(`Invalid hash index: ${indexString}`);
- return null;
- }
+export function useCurrentLhr(): {value: LH.Result, index: number}|null {
+ const flowResult = useFlowResult();
+ const indexString = useHashParam('index');
- const value = flowResult.lhrs[index];
- if (!value) {
- console.warn(`No LHR at index ${index}`);
- return null;
- }
+ // Memoize result so a new object is not created on every call.
+ return useMemo(() => {
+ if (!indexString) return null;
+
+ const index = Number(indexString);
+ if (!Number.isFinite(index)) {
+ console.warn(`Invalid hash index: ${indexString}`);
+ return null;
+ }
+
+ const value = flowResult.lhrs[index];
+ if (!value) {
+ console.warn(`No LHR at index ${index}`);
+ return null;
+ }
- return {value, index};
+ return {value, index};
+ }, [indexString, flowResult]);
}
export function useDerivedStepNames() {
diff --git a/flow-report/src/wrappers/report-renderer.tsx b/flow-report/src/wrappers/report-renderer.tsx
index 5c6e94d91530..a044f5fdb599 100644
--- a/flow-report/src/wrappers/report-renderer.tsx
+++ b/flow-report/src/wrappers/report-renderer.tsx
@@ -9,11 +9,13 @@ 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(undefined);
@@ -29,10 +31,12 @@ export const ReportRendererProvider: FunctionComponent = ({children}) => {
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 (
diff --git a/flow-report/src/wrappers/report.tsx b/flow-report/src/wrappers/report.tsx
new file mode 100644
index 000000000000..a22fc0c5107b
--- /dev/null
+++ b/flow-report/src/wrappers/report.tsx
@@ -0,0 +1,70 @@
+/**
+ * @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 {useEffect, useLayoutEffect, useRef} from 'preact/hooks';
+import {useCurrentLhr, useHashParam} from '../util';
+import {useReportRenderer} from './report-renderer';
+
+/**
+ * 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.
+ * e.g. ->
+ */
+export function convertChildAnchors(element: HTMLElement, index: number) {
+ const links = element.querySelectorAll('a') as NodeListOf;
+ 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;
+
+ const nodeId = link.hash.substr(1);
+ link.hash = `#index=${index}&anchor=${nodeId}`;
+ }
+}
+
+export const Report: FunctionComponent = () => {
+ const {dom, reportRenderer} = useReportRenderer();
+ const ref = useRef(null);
+ const anchor = useHashParam('anchor');
+ const currentLhr = useCurrentLhr();
+
+ useLayoutEffect(() => {
+ if (!currentLhr) return;
+
+ if (ref.current) {
+ dom.clearComponentCache();
+ reportRenderer.renderReport(currentLhr.value, ref.current);
+ convertChildAnchors(ref.current, currentLhr.index);
+ }
+
+ return () => {
+ if (ref.current) ref.current.textContent = '';
+ };
+ }, [reportRenderer, currentLhr]);
+
+ useEffect(() => {
+ if (anchor) {
+ const el = document.getElementById(anchor);
+ if (el) {
+ el.scrollIntoView({behavior: 'smooth'});
+ return;
+ }
+ }
+
+ // Scroll to top no anchor is found.
+ if (ref.current) ref.current.scrollIntoView();
+ }, [anchor, currentLhr]);
+
+ return (
+
+ );
+};
diff --git a/flow-report/test/app-test.tsx b/flow-report/test/app-test.tsx
index 5ccd565db5e1..5bd0b2256d9c 100644
--- a/flow-report/test/app-test.tsx
+++ b/flow-report/test/app-test.tsx
@@ -18,73 +18,29 @@ const flowResult = JSON.parse(
)
);
-let mockLocation: URL;
-
-beforeEach(() => {
- mockLocation = new URL('file:///Users/example/report.html');
- Object.defineProperty(window, 'location', {
- get: () => mockLocation,
- });
-});
-
it('renders a standalone report with summary', async () => {
const root = render();
- await expect(root.findByTestId('Summary')).resolves.toBeTruthy();
+ expect(root.getByTestId('Summary')).toBeTruthy();
});
it('renders the navigation step', async () => {
- mockLocation.hash = '#index=0';
+ global.location.hash = '#index=0';
const root = render();
- await expect(root.findByTestId('Report')).resolves.toBeTruthy();
-
- const link = await root.findByText(/https:/);
- expect(link.textContent).toEqual('https://www.mikescerealshack.co/');
-
- const scores = await root.findAllByText(/^\S+: [0-9.]+/);
- expect(scores.map(s => s.textContent)).toEqual([
- 'performance: 0.98',
- 'accessibility: 1',
- 'best-practices: 1',
- 'seo: 1',
- 'pwa: 0.3',
- ]);
+ expect(root.getByTestId('Report')).toBeTruthy();
});
it('renders the timespan step', async () => {
- mockLocation.hash = '#index=1';
+ global.location.hash = '#index=1';
const root = render();
- await expect(root.findByTestId('Report')).resolves.toBeTruthy();
-
- const link = await root.findByText(/https:/);
- expect(link.textContent).toEqual('https://www.mikescerealshack.co/search?q=call+of+duty');
-
- const scores = await root.findAllByText(/^\S+: [0-9.]+/);
- expect(scores.map(s => s.textContent)).toEqual([
- 'performance: 1',
- 'best-practices: 0.71',
- 'seo: 0',
- 'pwa: 1',
- ]);
+ expect(root.getByTestId('Report')).toBeTruthy();
});
it('renders the snapshot step', async () => {
- mockLocation.hash = '#index=2';
+ global.location.hash = '#index=2';
const root = render();
- await expect(root.findByTestId('Report')).resolves.toBeTruthy();
-
- const link = await root.findByText(/https:/);
- expect(link.textContent).toEqual('https://www.mikescerealshack.co/search?q=call+of+duty');
-
- const scores = await root.findAllByText(/^\S+: [0-9.]+/);
- expect(scores.map(s => s.textContent)).toEqual([
- 'performance: 0',
- 'accessibility: 0.9',
- 'best-practices: 0.88',
- 'seo: 0.86',
- 'pwa: 1',
- ]);
+ expect(root.getByTestId('Report')).toBeTruthy();
});
diff --git a/flow-report/test/common-test.tsx b/flow-report/test/common-test.tsx
index d534f575f47b..a7963158af5e 100644
--- a/flow-report/test/common-test.tsx
+++ b/flow-report/test/common-test.tsx
@@ -27,7 +27,7 @@ describe('CategoryRatio', () => {
category={category}
href="index=0&achor=seo"
/>);
- const link = await root.findByRole('link');
+ const link = root.getByRole('link');
expect(link.className).toEqual('CategoryRatio CategoryRatio--average');
expect(link.textContent).toEqual('2/3');
@@ -52,7 +52,7 @@ describe('CategoryRatio', () => {
category={category}
href="index=0&achor=seo"
/>);
- const link = await root.findByRole('link');
+ const link = root.getByRole('link');
expect(link.className).toEqual('CategoryRatio CategoryRatio--fail');
expect(link.textContent).toEqual('0/3');
@@ -77,7 +77,7 @@ describe('CategoryRatio', () => {
category={category}
href="index=0&achor=seo"
/>);
- const link = await root.findByRole('link');
+ const link = root.getByRole('link');
expect(link.className).toEqual('CategoryRatio CategoryRatio--pass');
expect(link.textContent).toEqual('3/3');
@@ -100,7 +100,7 @@ describe('CategoryRatio', () => {
category={category}
href="index=0&achor=seo"
/>);
- const link = await root.findByRole('link');
+ const link = root.getByRole('link');
expect(link.className).toEqual('CategoryRatio CategoryRatio--null');
expect(link.textContent).toEqual('1/2');
@@ -122,7 +122,7 @@ describe('CategoryRatio', () => {
category={category}
href="index=0&achor=seo"
/>);
- const link = await root.findByRole('link');
+ const link = root.getByRole('link');
expect(link.className).toEqual('CategoryRatio CategoryRatio--average');
expect(link.textContent).toEqual('1/2');
diff --git a/flow-report/test/setup/env-setup.ts b/flow-report/test/setup/env-setup.ts
new file mode 100644
index 000000000000..923327ffb159
--- /dev/null
+++ b/flow-report/test/setup/env-setup.ts
@@ -0,0 +1,25 @@
+/**
+ * @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 {jest} from '@jest/globals';
+import {JSDOM} from 'jsdom';
+
+/**
+ * The jest environment "jsdom" does not work when preact is combined with the report renderer.
+ */
+export function setupJsDom() {
+ const {window} = new JSDOM(undefined, {
+ url: 'file:///Users/example/report.html/',
+ });
+ global.window = window as any;
+ global.document = window.document;
+ global.location = window.location;
+
+ // Function not implemented in JSDOM.
+ window.Element.prototype.scrollIntoView = jest.fn();
+}
+
+global.beforeEach(setupJsDom);
diff --git a/flow-report/test/sidebar/flow-test.tsx b/flow-report/test/sidebar/flow-test.tsx
index 2dc8757f9890..b4510c47b63a 100644
--- a/flow-report/test/sidebar/flow-test.tsx
+++ b/flow-report/test/sidebar/flow-test.tsx
@@ -14,14 +14,9 @@ const flowResult = JSON.parse(
)
);
-let mockLocation: URL;
let wrapper: FunctionComponent;
beforeEach(() => {
- mockLocation = new URL('file:///Users/example/report.html');
- Object.defineProperty(window, 'location', {
- get: () => mockLocation,
- });
wrapper = ({children}) => (
{children}
);
@@ -31,12 +26,12 @@ describe('SidebarFlow', () => {
it('renders flow steps', async () => {
const root = render(, {wrapper});
- const navigation = await root.findByText('Navigation (1)');
- const timespan = await root.findByText('Timespan (1)');
- const snapshot = await root.findByText('Snapshot (1)');
- const navigation2 = await root.findByText('Navigation (2)');
+ const navigation = root.getByText('Navigation (1)');
+ const timespan = root.getByText('Timespan (1)');
+ const snapshot = root.getByText('Snapshot (1)');
+ const navigation2 = root.getByText('Navigation (2)');
- const links = await root.findAllByRole('link') as HTMLAnchorElement[];
+ const links = root.getAllByRole('link') as HTMLAnchorElement[];
expect(links.map(a => a.textContent)).toEqual([
navigation.textContent,
timespan.textContent,
@@ -44,27 +39,27 @@ describe('SidebarFlow', () => {
navigation2.textContent,
]);
expect(links.map(a => a.href)).toEqual([
- 'file:///Users/example/report.html#index=0',
- 'file:///Users/example/report.html#index=1',
- 'file:///Users/example/report.html#index=2',
- 'file:///Users/example/report.html#index=3',
+ 'file:///Users/example/report.html/#index=0',
+ 'file:///Users/example/report.html/#index=1',
+ 'file:///Users/example/report.html/#index=2',
+ 'file:///Users/example/report.html/#index=3',
]);
});
it('no steps highlighted on summary page', async () => {
const root = render(, {wrapper});
- const links = await root.findAllByRole('link');
+ const links = root.getAllByRole('link');
const highlighted = links.filter(h => h.classList.contains('Sidebar--current'));
expect(highlighted).toHaveLength(0);
});
it('highlight current step', async () => {
- mockLocation.hash = '#index=1';
+ global.location.hash = '#index=1';
const root = render(, {wrapper});
- const links = await root.findAllByRole('link');
+ const links = root.getAllByRole('link');
const highlighted = links.filter(h => h.classList.contains('Sidebar--current'));
expect(highlighted).toHaveLength(1);
diff --git a/flow-report/test/sidebar/sidebar-test.tsx b/flow-report/test/sidebar/sidebar-test.tsx
index 19f697d802e8..2ce21518351e 100644
--- a/flow-report/test/sidebar/sidebar-test.tsx
+++ b/flow-report/test/sidebar/sidebar-test.tsx
@@ -20,14 +20,9 @@ const flowResult = JSON.parse(
)
);
-let mockLocation: URL;
let wrapper: FunctionComponent;
beforeEach(() => {
- mockLocation = new URL('file:///Users/example/report.html');
- Object.defineProperty(window, 'location', {
- get: () => mockLocation,
- });
wrapper = ({children}) => (
{children}
);
@@ -39,17 +34,17 @@ describe('SidebarHeader', () => {
const date = '2021-08-03T18:28:13.296Z';
const root = render(, {wrapper});
- await expect(root.findByText(title)).resolves.toBeTruthy();
- await expect(root.findByText('Aug 3, 2021, 6:28 PM UTC')).resolves.toBeTruthy();
+ expect(root.getByText(title)).toBeTruthy();
+ expect(root.getByText('Aug 3, 2021, 6:28 PM UTC')).toBeTruthy();
});
});
describe('SidebarSummary', () => {
it('highlighted by default', async () => {
const root = render(, {wrapper});
- const link = await root.findByRole('link') as HTMLAnchorElement;
+ const link = root.getByRole('link') as HTMLAnchorElement;
- expect(link.href).toEqual('file:///Users/example/report.html#');
+ expect(link.href).toEqual('file:///Users/example/report.html/#');
expect(link.classList).toContain('Sidebar--current');
});
});
diff --git a/flow-report/test/summary/summary-test.tsx b/flow-report/test/summary/summary-test.tsx
index 5c542a26a287..f20e10c7e32c 100644
--- a/flow-report/test/summary/summary-test.tsx
+++ b/flow-report/test/summary/summary-test.tsx
@@ -21,15 +21,9 @@ const flowResult:LH.FlowResult = JSON.parse(
)
);
-let mockLocation: URL;
let wrapper: FunctionComponent;
beforeEach(() => {
- mockLocation = new URL('file:///Users/example/report.html');
- Object.defineProperty(window, 'location', {
- get: () => mockLocation,
- });
-
wrapper = ({children}) => (
@@ -43,8 +37,8 @@ describe('SummaryHeader', () => {
it('renders header content', async () => {
const root = render(, {wrapper});
- const lhrCounts = await root.findByText(/·/);
- await expect(root.findByText('Summary')).resolves.toBeTruthy();
+ const lhrCounts = root.getByText(/·/);
+ expect(root.getByText('Summary')).toBeTruthy();
expect(lhrCounts.textContent).toEqual(
'2 navigation reports · 1 timespan reports · 1 snapshot reports'
);
@@ -59,23 +53,23 @@ describe('SummaryFlowStep', () => {
hashIndex={0}
/>, {wrapper});
- await expect(root.findByTestId('SummaryNavigationHeader')).resolves.toBeTruthy();
+ expect(root.getByTestId('SummaryNavigationHeader')).toBeTruthy();
- await expect(root.findByText('Navigation (1)')).resolves.toBeTruthy();
+ expect(root.getByText('Navigation (1)')).toBeTruthy();
- const screenshot = await root.findByTestId('SummaryFlowStep__screenshot') as HTMLImageElement;
+ const screenshot = root.getByTestId('SummaryFlowStep__screenshot') as HTMLImageElement;
expect(screenshot.src).toMatch(/data:image\/jpeg;base64/);
- const gauges = await root.findAllByTestId('Gauge');
+ const gauges = root.getAllByTestId('Gauge');
expect(gauges).toHaveLength(4);
- const links = await root.findAllByRole('link') as HTMLAnchorElement[];
+ const links = root.getAllByRole('link') as HTMLAnchorElement[];
expect(links.map(a => a.href)).toEqual([
- 'http://localhost/#index=0',
- 'http://localhost/#index=0&anchor=performance',
- 'http://localhost/#index=0&anchor=accessibility',
- 'http://localhost/#index=0&anchor=best-practices',
- 'http://localhost/#index=0&anchor=seo',
+ 'file:///Users/example/report.html/#index=0',
+ 'file:///Users/example/report.html/#index=0&anchor=performance',
+ 'file:///Users/example/report.html/#index=0&anchor=accessibility',
+ 'file:///Users/example/report.html/#index=0&anchor=best-practices',
+ 'file:///Users/example/report.html/#index=0&anchor=seo',
]);
});
@@ -86,24 +80,24 @@ describe('SummaryFlowStep', () => {
hashIndex={1}
/>, {wrapper});
- await expect(root.findByTestId('SummaryNavigationHeader')).rejects.toBeTruthy();
+ expect(() => root.getByTestId('SummaryNavigationHeader')).toThrow();
- await expect(root.findByText('Timespan (1)')).resolves.toBeTruthy();
+ expect(root.getByText('Timespan (1)')).toBeTruthy();
- const screenshot = await root.findByTestId('SummaryFlowStep__screenshot') as HTMLImageElement;
+ const screenshot = root.getByTestId('SummaryFlowStep__screenshot') as HTMLImageElement;
expect(screenshot.src).toBeFalsy();
- await expect(root.findByTestId('SummaryCategory__null'));
- const gauges = await root.findAllByTestId('CategoryRatio');
+ expect(root.getByTestId('SummaryCategory__null'));
+ const gauges = root.getAllByTestId('CategoryRatio');
expect(gauges).toHaveLength(3);
- const links = await root.findAllByRole('link') as HTMLAnchorElement[];
+ const links = root.getAllByRole('link') as HTMLAnchorElement[];
expect(links.map(a => a.href)).toEqual([
- 'http://localhost/#index=1',
- 'http://localhost/#index=1&anchor=performance',
+ 'file:///Users/example/report.html/#index=1',
+ 'file:///Users/example/report.html/#index=1&anchor=performance',
// Accessibility is missing in timespan.
- 'http://localhost/#index=1&anchor=best-practices',
- 'http://localhost/#index=1&anchor=seo',
+ 'file:///Users/example/report.html/#index=1&anchor=best-practices',
+ 'file:///Users/example/report.html/#index=1&anchor=seo',
]);
});
@@ -114,23 +108,23 @@ describe('SummaryFlowStep', () => {
hashIndex={2}
/>, {wrapper});
- await expect(root.findByTestId('SummaryNavigationHeader')).rejects.toBeTruthy();
+ expect(() => root.getByTestId('SummaryNavigationHeader')).toThrow();
- await expect(root.findByText('Snapshot (1)')).resolves.toBeTruthy();
+ expect(root.getByText('Snapshot (1)')).toBeTruthy();
- const screenshot = await root.findByTestId('SummaryFlowStep__screenshot') as HTMLImageElement;
+ const screenshot = root.getByTestId('SummaryFlowStep__screenshot') as HTMLImageElement;
expect(screenshot.src).toMatch(/data:image\/jpeg;base64/);
- const gauges = await root.findAllByTestId('CategoryRatio');
+ const gauges = root.getAllByTestId('CategoryRatio');
expect(gauges).toHaveLength(4);
- const links = await root.findAllByRole('link') as HTMLAnchorElement[];
+ const links = root.getAllByRole('link') as HTMLAnchorElement[];
expect(links.map(a => a.href)).toEqual([
- 'http://localhost/#index=2',
- 'http://localhost/#index=2&anchor=performance',
- 'http://localhost/#index=2&anchor=accessibility',
- 'http://localhost/#index=2&anchor=best-practices',
- 'http://localhost/#index=2&anchor=seo',
+ 'file:///Users/example/report.html/#index=2',
+ 'file:///Users/example/report.html/#index=2&anchor=performance',
+ 'file:///Users/example/report.html/#index=2&anchor=accessibility',
+ 'file:///Users/example/report.html/#index=2&anchor=best-practices',
+ 'file:///Users/example/report.html/#index=2&anchor=seo',
]);
});
});
diff --git a/flow-report/test/util-test.tsx b/flow-report/test/util-test.tsx
index f0ba45612ceb..8722429f1302 100644
--- a/flow-report/test/util-test.tsx
+++ b/flow-report/test/util-test.tsx
@@ -23,7 +23,6 @@ const flowResult: LH.FlowResult = JSON.parse(
let wrapper: FunctionComponent;
beforeEach(() => {
- window.location.hash = '';
wrapper = ({children}) => (
{children}
);
@@ -31,7 +30,7 @@ beforeEach(() => {
describe('useCurrentLhr', () => {
it('gets current lhr index from url hash', () => {
- window.location.hash = '#index=1';
+ global.location.hash = '#index=1';
const {result} = renderHook(() => useCurrentLhr(), {wrapper});
expect(result.current).toEqual({
index: 1,
@@ -40,7 +39,7 @@ describe('useCurrentLhr', () => {
});
it('changes on navigation', async () => {
- window.location.hash = '#index=1';
+ global.location.hash = '#index=1';
const render = renderHook(() => useCurrentLhr(), {wrapper});
expect(render.result.current).toEqual({
@@ -49,7 +48,7 @@ describe('useCurrentLhr', () => {
});
await act(() => {
- window.location.hash = '#index=2';
+ global.location.hash = '#index=2';
});
await render.waitForNextUpdate();
@@ -65,13 +64,13 @@ describe('useCurrentLhr', () => {
});
it('return null if lhr index is out of bounds', () => {
- window.location.hash = '#index=5';
+ global.location.hash = '#index=5';
const {result} = renderHook(() => useCurrentLhr(), {wrapper});
expect(result.current).toBeNull();
});
it('returns null for invalid value', () => {
- window.location.hash = '#index=OHNO';
+ global.location.hash = '#index=OHNO';
const {result} = renderHook(() => useCurrentLhr(), {wrapper});
expect(result.current).toBeNull();
});
diff --git a/flow-report/types/flow-report.d.ts b/flow-report/types/flow-report.d.ts
index 3d64984ad834..4b98121d0546 100644
--- a/flow-report/types/flow-report.d.ts
+++ b/flow-report/types/flow-report.d.ts
@@ -7,6 +7,8 @@
import FlowResult_ from '../../types/lhr/flow';
import * as Settings from '../../types/lhr/settings';
+// Import to augment querySelector/querySelectorAll with stricter type checking.
+import '../../types/query-selector';
import '../../report/types/html-renderer';
declare global {
diff --git a/report/renderer/dom.js b/report/renderer/dom.js
index dcc6e09eee9f..5797d9a1b4a6 100644
--- a/report/renderer/dom.js
+++ b/report/renderer/dom.js
@@ -107,6 +107,10 @@ export class DOM {
return cloned;
}
+ clearComponentCache() {
+ this._componentCache.clear();
+ }
+
/**
* @param {string} text
* @return {Element}