Skip to content

Commit 4b8bb99

Browse files
authored
Merge pull request #113 from joomcode/fix/merge-urls-in-api-statistics
fix: merge urls for API statistics
2 parents afefd51 + 6f6c8e0 commit 4b8bb99

12 files changed

Lines changed: 286 additions & 93 deletions

src/types/internal.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -112,11 +112,13 @@ export type {
112112
export type {LiteReport, LiteRetry} from './report';
113113
/** @internal */
114114
export type {
115+
ReportClientData,
115116
ReportClientState,
116117
ReportData,
117118
Retry,
118119
RetryButtonProps,
119120
RetryProps,
121+
ScriptJsonData,
120122
TestRunButtonProps,
121123
} from './report';
122124
/** @internal */

src/types/report.ts

Lines changed: 63 additions & 48 deletions
Original file line numberDiff line numberDiff line change
@@ -13,28 +13,6 @@ import type {
1313
TestMetaPlaceholder,
1414
} from './userland';
1515

16-
/**
17-
* The complete report data (for printing report).
18-
* @internal
19-
*/
20-
export type ReportData = Readonly<{
21-
apiStatistics: ApiStatistics;
22-
customReportProperties: CustomReportPropertiesPlaceholder | undefined;
23-
endE2edReason: EndE2edReason;
24-
endTimeInMs: UtcTimeInMs;
25-
errors: readonly string[];
26-
exitCode: ExitCode;
27-
failedTestsMainParams: readonly string[];
28-
fullTestRuns: readonly FullTestRun[];
29-
liteReportFileName: string | null;
30-
logFileName: string | null;
31-
notIncludedInPackTests: readonly TestFilePath[];
32-
reportFileName: string | null;
33-
retries: readonly Retry[];
34-
startInfo: StartInfo;
35-
summaryPackResults: string;
36-
}>;
37-
3816
/**
3917
* The lite report data (for printing lite JSON report) with userland meta.
4018
*/
@@ -61,28 +39,48 @@ export type LiteReport<
6139
}>;
6240

6341
/**
64-
* RetryButton component props.
65-
* @internal
42+
* Lite retry object with all his lite test runs.
6643
*/
67-
export type RetryButtonProps = Readonly<{
68-
disabled: boolean;
69-
retry: number;
70-
selected: boolean;
44+
export type LiteRetry<TestMeta = TestMetaPlaceholder> = Readonly<{
45+
brokenLiteTestRuns: readonly LiteTestRun<TestMeta>[];
46+
concurrency: number;
47+
endTimeInMs: UtcTimeInMs;
48+
/**
49+
* Test runs of all statuses except broken.
50+
*/
51+
liteTestRuns: readonly LiteTestRun<TestMeta>[];
52+
retryIndex: number;
53+
startTimeInMs: UtcTimeInMs;
7154
}>;
7255

7356
/**
74-
* TestRunButton component props.
57+
* The complete report data (for printing report).
7558
* @internal
7659
*/
77-
export type TestRunButtonProps = Readonly<{
60+
export type ReportData = Readonly<{
61+
apiStatistics: ApiStatistics;
62+
customReportProperties: CustomReportPropertiesPlaceholder | undefined;
63+
endE2edReason: EndE2edReason;
7864
endTimeInMs: UtcTimeInMs;
79-
filePath: TestFilePath;
80-
mainParams: string;
81-
name: string;
82-
runHash: RunHash;
83-
runId: RunId;
84-
startTimeInMs: UtcTimeInMs;
85-
status: TestRunStatus;
65+
errors: readonly string[];
66+
exitCode: ExitCode;
67+
failedTestsMainParams: readonly string[];
68+
fullTestRuns: readonly FullTestRun[];
69+
liteReportFileName: string | null;
70+
logFileName: string | null;
71+
notIncludedInPackTests: readonly TestFilePath[];
72+
reportFileName: string | null;
73+
retries: readonly Retry[];
74+
startInfo: StartInfo;
75+
summaryPackResults: string;
76+
}>;
77+
78+
/**
79+
* The general report data that needed on client for rendering parts of HTML report.
80+
* @internal
81+
*/
82+
export type ReportClientData = Readonly<{
83+
apiStatistics: ApiStatistics;
8684
}>;
8785

8886
/**
@@ -96,6 +94,7 @@ export type ReportClientState = {
9694
lengthOfReadedJsonReportDataParts: number;
9795
readonly pathToScreenshotsDirectoryForReport: string | null;
9896
readonly readJsonReportDataObservers: MutationObserver[];
97+
reportClientData?: ReportClientData;
9998
testRunDetailsElementsByHash?: Record<RunHash, HTMLElement>;
10099
};
101100

@@ -112,18 +111,13 @@ export type Retry = Readonly<{
112111
}>;
113112

114113
/**
115-
* Lite retry object with all his lite test runs.
114+
* RetryButton component props.
115+
* @internal
116116
*/
117-
export type LiteRetry<TestMeta = TestMetaPlaceholder> = Readonly<{
118-
brokenLiteTestRuns: readonly LiteTestRun<TestMeta>[];
119-
concurrency: number;
120-
endTimeInMs: UtcTimeInMs;
121-
/**
122-
* Test runs of all statuses except broken.
123-
*/
124-
liteTestRuns: readonly LiteTestRun<TestMeta>[];
125-
retryIndex: number;
126-
startTimeInMs: UtcTimeInMs;
117+
export type RetryButtonProps = Readonly<{
118+
disabled: boolean;
119+
retry: number;
120+
selected: boolean;
127121
}>;
128122

129123
/**
@@ -137,3 +131,24 @@ export type RetryProps = Readonly<{
137131
startTimeInMs: UtcTimeInMs;
138132
testRunButtons: readonly TestRunButtonProps[];
139133
}>;
134+
135+
/**
136+
* JSON data in `<script>` tags with JSON presentation of report data.
137+
* @internal
138+
*/
139+
export type ScriptJsonData = ReportClientData | readonly FullTestRun[];
140+
141+
/**
142+
* TestRunButton component props.
143+
* @internal
144+
*/
145+
export type TestRunButtonProps = Readonly<{
146+
endTimeInMs: UtcTimeInMs;
147+
filePath: TestFilePath;
148+
mainParams: string;
149+
name: string;
150+
runHash: RunHash;
151+
runId: RunId;
152+
startTimeInMs: UtcTimeInMs;
153+
status: TestRunStatus;
154+
}>;

src/utils/apiStatistics/addPageToApiStatistics.ts

Lines changed: 3 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,7 @@
1-
import {URL} from 'node:url';
2-
31
import {getApiStatistics} from '../../context/apiStatistics';
42

53
import {addApiStatistics} from './addApiStatistics';
4+
import {getUrlTemplate} from './getUrlTemplate';
65

76
import type {ApiStatistics, PageName, Url} from '../../types/internal';
87

@@ -18,11 +17,10 @@ type Options = Readonly<{
1817
*/
1918
export const addPageToApiStatistics = ({duration, pageName, url}: Options): void => {
2019
const apiStatistics = getApiStatistics();
21-
const {origin, pathname} = new URL(url);
22-
const urlWithoutQuery = `${origin}${pathname}` as Url;
20+
const urlTemplate = getUrlTemplate(url);
2321

2422
const additionalApiStatistics: ApiStatistics = {
25-
pages: {[pageName]: {[urlWithoutQuery]: {count: 1, duration}}},
23+
pages: {[pageName]: {[urlTemplate]: {count: 1, duration}}},
2624
requests: {},
2725
};
2826

src/utils/apiStatistics/addResponseToApiStatistics.ts

Lines changed: 4 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,11 @@
1-
import {URL} from 'node:url';
2-
31
import {getApiStatistics} from '../../context/apiStatistics';
42

53
import {getHeaderValue} from '../headers';
64

75
import {addApiStatistics} from './addApiStatistics';
6+
import {getUrlTemplate} from './getUrlTemplate';
87

9-
import type {ApiStatistics, ResponseWithRequest, Url} from '../../types/internal';
8+
import type {ApiStatistics, ResponseWithRequest} from '../../types/internal';
109

1110
/**
1211
* Add single `ResponseWithRequest` to API statistics.
@@ -21,13 +20,11 @@ export const addResponseToApiStatistics = (responseWithRequest: ResponseWithRequ
2120
} = responseWithRequest;
2221

2322
const size = Number(getHeaderValue(responseWithRequest.responseHeaders, 'content-length')) || 0;
24-
25-
const {origin, pathname} = new URL(url);
26-
const urlWithoutQuery = `${origin}${pathname}` as Url;
23+
const urlTemplate = getUrlTemplate(url);
2724

2825
const additionalApiStatistics: ApiStatistics = {
2926
pages: {},
30-
requests: {[urlWithoutQuery]: {[method]: {[statusCode]: {count: 1, duration, size}}}},
27+
requests: {[urlTemplate]: {[method]: {[statusCode]: {count: 1, duration, size}}}},
3128
};
3229

3330
addApiStatistics(apiStatistics, additionalApiStatistics);
Lines changed: 61 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,61 @@
1+
import {assertValueIsDefined} from '../asserts';
2+
3+
const aCodePoint = 97;
4+
// eslint-disable-next-line @typescript-eslint/naming-convention
5+
const ACodePoint = 65;
6+
const nineCodePoint = 57;
7+
const zCodePoint = 122;
8+
// eslint-disable-next-line @typescript-eslint/naming-convention
9+
const ZCodePoint = 90;
10+
const zeroCodePoint = 48;
11+
12+
/**
13+
* Returns count of border (letter or non-letter) inside string.
14+
* @internal
15+
*/
16+
// eslint-disable-next-line complexity
17+
export const getBorderCount = (value: string): number => {
18+
let borderCount = -1;
19+
let mode: '.' | '0' | 'A' | 'a' | undefined;
20+
21+
for (const char of value) {
22+
const codePoint = char.codePointAt(0);
23+
24+
assertValueIsDefined(codePoint, 'codePoint is defined', {value});
25+
26+
switch (true) {
27+
case codePoint >= zeroCodePoint && codePoint <= nineCodePoint:
28+
if (mode !== '0') {
29+
borderCount += 1;
30+
}
31+
32+
mode = '0';
33+
break;
34+
35+
case codePoint >= ACodePoint && codePoint <= ZCodePoint:
36+
if (mode !== 'A') {
37+
borderCount += 1;
38+
}
39+
40+
mode = 'A';
41+
break;
42+
43+
case codePoint >= aCodePoint && codePoint <= zCodePoint:
44+
if (mode !== 'a') {
45+
borderCount += 1;
46+
}
47+
48+
mode = 'a';
49+
break;
50+
51+
default:
52+
if (mode !== '.') {
53+
borderCount += 1;
54+
}
55+
56+
mode = '.';
57+
}
58+
}
59+
60+
return borderCount;
61+
};
Lines changed: 52 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,52 @@
1+
import {URL} from 'node:url';
2+
3+
import {assertValueIsDefined} from '../asserts';
4+
5+
import {isIdentifier} from './isIdentifier';
6+
7+
import type {Url} from '../../types/internal';
8+
9+
const maxExtensionLength = 4;
10+
const minExtensionLength = 2;
11+
12+
/**
13+
* Get url template from url (for API statistics).
14+
* `query` part is cut off, and all identifiers in the `pathname` are replaced with asterisks.
15+
* @internal
16+
*/
17+
export const getUrlTemplate = (url: Url): Url => {
18+
const {origin, pathname} = new URL(url);
19+
const parts = pathname.split('/');
20+
21+
for (let index = 0; index < parts.length; index += 1) {
22+
const part = parts[index];
23+
24+
assertValueIsDefined(part, 'part is defined', {url});
25+
26+
if (index === parts.length - 1) {
27+
const indexOfPoint = part.lastIndexOf('.');
28+
const extensionLength = part.length - indexOfPoint - 1;
29+
30+
if (
31+
indexOfPoint !== -1 &&
32+
extensionLength >= minExtensionLength &&
33+
extensionLength <= maxExtensionLength
34+
) {
35+
const extension = part.slice(indexOfPoint + 1);
36+
37+
// eslint-disable-next-line max-depth
38+
if (/^[a-z]+$/.test(extension)) {
39+
continue;
40+
}
41+
}
42+
}
43+
44+
if (isIdentifier(part)) {
45+
parts[index] = '*';
46+
}
47+
}
48+
49+
const newPathname = parts.join('/');
50+
51+
return `${origin}${newPathname}` as Url;
52+
};
Lines changed: 41 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,41 @@
1+
import {getBorderCount} from './getBorderCount';
2+
3+
const maxBorderCountOfNonIdentifier = 6;
4+
const minIdentifierLength = 6;
5+
const maxIdentifierLengthForMaxBorder = 12;
6+
7+
/**
8+
* Returns `true`, is `value` is identifier, like `67ca3bc9fa30bbcd46dd6f40`.
9+
* @internal
10+
*/
11+
export const isIdentifier = (value: string): boolean => {
12+
if (value.length < minIdentifierLength) {
13+
return false;
14+
}
15+
16+
const borderCount = getBorderCount(value);
17+
18+
if (borderCount > maxBorderCountOfNonIdentifier) {
19+
return true;
20+
}
21+
22+
if (
23+
borderCount === maxBorderCountOfNonIdentifier &&
24+
value.length <= maxIdentifierLengthForMaxBorder
25+
) {
26+
return true;
27+
}
28+
29+
if (
30+
borderCount === maxBorderCountOfNonIdentifier - 1 &&
31+
value.length <= maxIdentifierLengthForMaxBorder - 2
32+
) {
33+
return true;
34+
}
35+
36+
if (/^[0-9]+$/.test(value)) {
37+
return true;
38+
}
39+
40+
return false;
41+
};

src/utils/events/registerEndE2edRunEvent.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,7 @@ let isEndAlreadyCalled = false;
1717
* Registers end e2ed run event (for report) after closing of all tests.
1818
* @internal
1919
*/
20+
// eslint-disable-next-line max-statements
2021
export const registerEndE2edRunEvent = async (): Promise<void> => {
2122
if (isEndAlreadyCalled) {
2223
return;
@@ -47,6 +48,7 @@ export const registerEndE2edRunEvent = async (): Promise<void> => {
4748
} catch (error) {
4849
generalLog('Caught an error on run "after pack" functions', {error});
4950

51+
setReadonlyProperty(liteReport, 'exitCode', ExitCode.HasErrorsInDoAfterPackFunctions);
5052
setReadonlyProperty(reportData, 'exitCode', ExitCode.HasErrorsInDoAfterPackFunctions);
5153
}
5254

0 commit comments

Comments
 (0)