Skip to content

Commit 2c49bb8

Browse files
committed
FI-1635 feat: add API statistics to HTML report
1 parent 40d72e7 commit 2c49bb8

20 files changed

Lines changed: 223 additions & 94 deletions

src/types/apiStatistics.ts

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,12 @@ export type ApiStatistics = Readonly<{
1010
resources: Readonly<Record<Url, RequestStatistics>>;
1111
}>;
1212

13+
/**
14+
* Hash string of menu buttons for showing parts of `ApiStatistics` in HTML report.
15+
* @internal
16+
*/
17+
export type ApiStatisticsReportHash = `api-statistics-${keyof ApiStatistics}`;
18+
1319
/**
1420
* Page name (as name of page class).
1521
*/

src/types/internal.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,8 @@ export type {
99
RequestStatistics,
1010
StatisticsUnit,
1111
} from './apiStatistics';
12+
/** @internal */
13+
export type {ApiStatisticsReportHash} from './apiStatistics';
1214
export type {Brand, IsBrand} from './brand';
1315
export type {Expect, IsEqual, IsReadonlyKey} from './checks';
1416
export type {Class} from './class';

src/types/report.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -116,6 +116,7 @@ export type Retry = Readonly<{
116116
*/
117117
export type RetryButtonProps = Readonly<{
118118
disabled: boolean;
119+
name: string;
119120
retry: number;
120121
selected: boolean;
121122
}>;

src/utils/getDurationWithUnits.ts

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -8,8 +8,9 @@ export function getDurationWithUnits(durationInMs: number): string {
88
const msInSecond = 1_000;
99
const timeMultiplicator = 60;
1010

11-
const remainderInMs = durationInMs % msInSecond;
12-
const durationInSeconds = Math.round((durationInMs - remainderInMs) / msInSecond);
11+
const roundedDuration = Math.round(durationInMs);
12+
const remainderInMs = roundedDuration % msInSecond;
13+
const durationInSeconds = Math.round((roundedDuration - remainderInMs) / msInSecond);
1314
const remainderInSeconds = durationInSeconds % timeMultiplicator;
1415
const durationInMinutes = Math.round(
1516
(durationInSeconds - remainderInSeconds) / timeMultiplicator,
Lines changed: 54 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -1,32 +1,41 @@
11
import {assertValueIsDefined as clientAssertValueIsDefined} from './assertValueIsDefined';
2-
import {renderTestRunDetails as clientRenderTestRunDetails} from './render';
3-
4-
import type {ReportClientState, RunHash} from '../../../types/internal';
2+
import {
3+
renderApiStatistics as clientRenderApiStatistics,
4+
renderTestRunDetails as clientRenderTestRunDetails,
5+
} from './render';
6+
7+
import type {
8+
ApiStatisticsReportHash,
9+
ReportClientState,
10+
RunHash,
11+
SafeHtml,
12+
} from '../../../types/internal';
513

614
const assertValueIsDefined: typeof clientAssertValueIsDefined = clientAssertValueIsDefined;
15+
const renderApiStatistics = clientRenderApiStatistics;
716
const renderTestRunDetails = clientRenderTestRunDetails;
817

9-
declare const e2edTestRunDetailsContainer: HTMLElement;
18+
declare const e2edRightColumnContainer: HTMLElement;
1019
declare const reportClientState: ReportClientState;
1120

1221
/**
13-
* Chooses TestRun (render chosen TestRun in right panel).
22+
* Chooses `TestRun` (render chosen `TestRun` in right panel).
1423
* This base client function should not use scope variables (except other base functions).
1524
* @internal
1625
*/
26+
// eslint-disable-next-line max-statements
1727
export function chooseTestRun(runHash: RunHash): void {
1828
const previousHash = window.location.hash as RunHash;
1929

2030
window.location.hash = runHash;
2131

2232
if (reportClientState.testRunDetailsElementsByHash === undefined) {
23-
reportClientState.testRunDetailsElementsByHash = {};
33+
reportClientState.testRunDetailsElementsByHash = Object.create(null) as {};
2434
}
2535

2636
const {testRunDetailsElementsByHash} = reportClientState;
2737

28-
const previousTestRunDetailsElement =
29-
e2edTestRunDetailsContainer.firstElementChild as HTMLElement;
38+
const previousTestRunDetailsElement = e2edRightColumnContainer.firstElementChild as HTMLElement;
3039

3140
if (!(previousHash in testRunDetailsElementsByHash)) {
3241
testRunDetailsElementsByHash[previousHash] = previousTestRunDetailsElement;
@@ -42,23 +51,48 @@ export function chooseTestRun(runHash: RunHash): void {
4251
return;
4352
}
4453

45-
const {fullTestRuns} = reportClientState;
46-
const fullTestRun = fullTestRuns.find((testRun) => testRun.runHash === runHash);
54+
const pagesHash: ApiStatisticsReportHash = 'api-statistics-pages';
55+
const requestsHash: ApiStatisticsReportHash = 'api-statistics-requests';
56+
const resourcesHash: ApiStatisticsReportHash = 'api-statistics-resources';
4757

48-
if (fullTestRun === undefined) {
49-
// eslint-disable-next-line no-console
50-
console.error(
51-
`Cannot find test run with hash ${runHash} in JSON report data. Probably JSON report data for this test run not yet loaded. Please try click again later`,
52-
);
58+
let rightColumnHtml: SafeHtml | undefined;
5359

54-
return;
55-
}
60+
const hash = String(runHash);
61+
62+
if (hash === pagesHash || hash === requestsHash || hash === resourcesHash) {
63+
const {reportClientData} = reportClientState;
64+
65+
if (reportClientData === undefined) {
66+
// eslint-disable-next-line no-console
67+
console.error(
68+
`Cannot find report client data in JSON report data (tried to click "${runHash}"). Probably JSON report data not yet completely loaded. Please try click again later`,
69+
);
70+
71+
return;
72+
}
5673

57-
const testRunDetailsHtml = renderTestRunDetails(fullTestRun);
74+
const {apiStatistics} = reportClientData;
75+
76+
rightColumnHtml = renderApiStatistics({apiStatistics, hash});
77+
} else {
78+
const {fullTestRuns} = reportClientState;
79+
const fullTestRun = fullTestRuns.find((testRun) => testRun.runHash === runHash);
80+
81+
if (fullTestRun === undefined) {
82+
// eslint-disable-next-line no-console
83+
console.error(
84+
`Cannot find test run with hash ${runHash} in JSON report data. Probably JSON report data for this test run not yet loaded. Please try click again later`,
85+
);
86+
87+
return;
88+
}
89+
90+
rightColumnHtml = renderTestRunDetails(fullTestRun);
91+
}
5892

59-
e2edTestRunDetailsContainer.innerHTML = String(testRunDetailsHtml);
93+
e2edRightColumnContainer.innerHTML = String(rightColumnHtml);
6094

61-
const nextTestRunDetailsElement = e2edTestRunDetailsContainer.firstElementChild as HTMLElement;
95+
const nextTestRunDetailsElement = e2edRightColumnContainer.firstElementChild as HTMLElement;
6296

6397
testRunDetailsElementsByHash[runHash] = nextTestRunDetailsElement;
6498
}

src/utils/report/client/index.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -26,6 +26,8 @@ export {readJsonReportData} from './readJsonReportData';
2626
export {readPartOfJsonReportData} from './readPartOfJsonReportData';
2727
/** @internal */
2828
export {
29+
renderApiStatistics,
30+
renderApiStatisticsItem,
2931
renderAttributes,
3032
renderDatesInterval,
3133
renderDuration,

src/utils/report/client/initialScript.ts

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -29,11 +29,11 @@ const renderAttributes = clientRenderAttributes;
2929
const setReadJsonReportDataObservers = clientSetReadJsonReportDataObservers;
3030

3131
/**
32-
* Initial report page script.
32+
* Initial HTML report page script.
3333
* This client function should not use scope variables (except global functions).
3434
* @internal
3535
*/
36-
export const initialScript = (): void => {
36+
export function initialScript(): void {
3737
const {locator: locatorAttributes} = createSimpleLocator(createLocatorOptions);
3838

3939
locator = (...args): SafeHtml => renderAttributes(locatorAttributes(...(args as [string])));
@@ -47,4 +47,4 @@ export const initialScript = (): void => {
4747
setReadJsonReportDataObservers();
4848

4949
addDomContentLoadedHandler(onDomContentLoad);
50-
};
50+
}

src/utils/report/client/onFirstJsonReportDataLoad.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@ import {clickOnTestRun as clientClickOnTestRun} from './clickOnTestRun';
22

33
const clickOnTestRun = clientClickOnTestRun;
44

5-
declare const e2edTestRunDetailsContainer: HTMLElement;
5+
declare const e2edRightColumnContainer: HTMLElement;
66

77
/**
88
* Handler of loading first part of JSON report data for report page.
@@ -31,7 +31,7 @@ export function onFirstJsonReportDataLoad(): void {
3131
const {top} = buttonOfOpenStep.getBoundingClientRect();
3232

3333
setTimeout(() => {
34-
e2edTestRunDetailsContainer.scrollTop = top;
34+
e2edRightColumnContainer.scrollTop = top;
3535
}, scrollDelayInMs);
3636
}
3737
}

src/utils/report/client/render/index.ts

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,8 @@
11
/** @internal */
2+
export {renderApiStatistics} from './renderApiStatistics';
3+
/** @internal */
4+
export {renderApiStatisticsItem} from './renderApiStatisticsItem';
5+
/** @internal */
26
export {renderAttributes} from './renderAttributes';
37
/** @internal */
48
export {renderDatesInterval} from './renderDatesInterval';
Lines changed: 64 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,64 @@
1+
import {assertValueIsDefined as clientAssertValueIsDefined} from '../assertValueIsDefined';
2+
import {createSafeHtmlWithoutSanitize as clientCreateSafeHtmlWithoutSanitize} from '../sanitizeHtml';
3+
4+
import {renderApiStatisticsItem as clientRenderApiStatisticsItem} from './renderApiStatisticsItem';
5+
6+
import type {ApiStatistics, ApiStatisticsReportHash, SafeHtml} from '../../../../types/internal';
7+
8+
const assertValueIsDefined: typeof clientAssertValueIsDefined = clientAssertValueIsDefined;
9+
const createSafeHtmlWithoutSanitize = clientCreateSafeHtmlWithoutSanitize;
10+
const renderApiStatisticsItem = clientRenderApiStatisticsItem;
11+
12+
type Options = Readonly<{
13+
apiStatistics: ApiStatistics;
14+
hash: ApiStatisticsReportHash;
15+
}>;
16+
17+
/**
18+
* Renders `ApiStatistics` of one kind (pages, requests or resources).
19+
* This base client function should not use scope variables (except other base functions).
20+
* @internal
21+
*/
22+
export function renderApiStatistics({apiStatistics, hash}: Options): SafeHtml {
23+
let kind: keyof ApiStatistics | undefined;
24+
const items: SafeHtml[] = [];
25+
26+
if (hash === 'api-statistics-pages') {
27+
kind = 'pages';
28+
29+
for (const [name, byUrl] of Object.entries(apiStatistics.pages)) {
30+
let pageCount = 0;
31+
let pageDuration = 0;
32+
const pageItems: SafeHtml[] = [];
33+
34+
for (const [url, {count, duration}] of Object.entries(byUrl)) {
35+
pageCount += count;
36+
pageDuration += duration;
37+
38+
pageItems.push(renderApiStatisticsItem({duration: duration / count, name: url}));
39+
}
40+
41+
items.push(
42+
renderApiStatisticsItem({duration: pageDuration / pageCount, isHeader: true, name}),
43+
);
44+
items.push(...pageItems);
45+
}
46+
} else if (hash === 'api-statistics-requests') {
47+
kind = 'requests';
48+
} else {
49+
kind = 'resources';
50+
}
51+
52+
const firstKindString = kind[0];
53+
54+
assertValueIsDefined(firstKindString);
55+
56+
const capitalizedKind = `${firstKindString.toUpperCase()}${kind.slice(1)}`;
57+
58+
return createSafeHtmlWithoutSanitize`<article class="test-details">
59+
<h2 class="test-details__title">${capitalizedKind}</h2>
60+
<div role="tabpanel">
61+
<article class="overview">${items.join('')}</article>
62+
</div>
63+
</article>`;
64+
}

0 commit comments

Comments
 (0)