Skip to content

Commit 57865f1

Browse files
committed
FI-1727 fix: render JSX to SafeHtml
1 parent 84f1c0c commit 57865f1

15 files changed

Lines changed: 146 additions & 58 deletions

src/types/report.ts

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -96,7 +96,6 @@ export type ReportClientState = {
9696
readonly e2edRightColumnContainer: HTMLElement;
9797
readonly fullTestRuns: readonly FullTestRun[];
9898
readonly internalDirectoryName: string;
99-
readonly jsxRuntime: JSX.Runtime;
10099
lengthOfReadedJsonReportDataParts: number;
101100
readonly locator: LocatorFunction<SafeHtml>;
102101
readonly pathToScreenshotsDirectoryForReport: string | null;

src/types/utils.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@ import type {IsIncludeUndefined} from './undefined';
44
/**
55
* Entry pair that `Object.entries<Type>` returns.
66
*/
7-
type EntryPair<Type> = [key: keyof Type, value: Values<Type> | undefined];
7+
type EntryPair<Type> = [key: keyof Type, value: Values<Type>];
88

99
/**
1010
* Alias for type any (to suppress the @typescript-eslint/no-explicit-any rule).

src/utils/index.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -40,7 +40,7 @@ export {getKeysCounter} from './getKeysCounter';
4040
export {getEquivalentHeadersNames, getHeadersFromHeaderEntries, getHeaderValue} from './headers';
4141
export {getContentJsonHeaders} from './http';
4242
export {log} from './log';
43-
export {deepMerge, getKeys, setReadonlyProperty} from './object';
43+
export {deepMerge, getEntries, getKeys, setReadonlyProperty} from './object';
4444
export {parseMaybeEmptyValueAsJson, parseValueAsJsonIfNeeded} from './parse';
4545
export {
4646
addTimeoutToPromise,

src/utils/object/getEntries.ts

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,8 @@
1+
import type {ObjectEntries} from '../../types/internal';
2+
3+
/**
4+
* Get typed array of key-value pairs (like `Object.entries`, but typed).
5+
*/
6+
export const getEntries = <Value extends {}>(
7+
value: Value,
8+
): readonly ObjectEntries<Value>[number][] => Object.entries(value) as ObjectEntries<Value>;

src/utils/object/index.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,4 @@
11
export {deepMerge} from './deepMerge';
2+
export {getEntries} from './getEntries';
23
export {getKeys} from './getKeys';
34
export {setReadonlyProperty} from './setReadonlyProperty';
Lines changed: 58 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,66 @@
1+
import {
2+
createSafeHtmlWithoutSanitize as clientCreateSafeHtmlWithoutSanitize,
3+
isSafeHtml as clientIsSafeHtml,
4+
sanitizeHtml as clientSanitizeHtml,
5+
} from './sanitizeHtml';
6+
7+
import type {SafeHtml} from '../../../types/internal';
8+
9+
const createSafeHtmlWithoutSanitize = clientCreateSafeHtmlWithoutSanitize;
10+
const isSafeHtml: typeof clientIsSafeHtml = clientIsSafeHtml;
11+
const sanitizeHtml = clientSanitizeHtml;
12+
113
/**
214
* Creates JSX runtime (functions `createElement` and `Fragment`).
315
* This client function should not use scope variables (except global functions).
416
* @internal
517
*/
618
export function createJsxRuntime(): JSX.Runtime {
7-
const createElement: JSX.CreateElement = (type, properties, ...children) => '';
8-
const Fragment: JSX.Fragment = ({children}) => '';
19+
const maxDepth = 8;
20+
21+
const createElement: JSX.CreateElement = (type, properties, ...children) => {
22+
const flatChildren = children.flat(maxDepth);
23+
24+
if (typeof type === 'function') {
25+
const propertiesWithChildren =
26+
flatChildren.length === 0 ? properties : {...properties, children: flatChildren};
27+
28+
return type(propertiesWithChildren ?? undefined);
29+
}
30+
31+
const childrenParts: readonly SafeHtml[] = flatChildren.map((child) =>
32+
isSafeHtml(child) ? child : sanitizeHtml`${child}`,
33+
);
34+
const childrenHtml = createSafeHtmlWithoutSanitize`${childrenParts.join('')}`;
35+
36+
if (properties == null) {
37+
return sanitizeHtml`<${type}>${childrenHtml}</${type}>`;
38+
}
39+
40+
const attributesParts: readonly SafeHtml[] = Object.entries(properties).map(
41+
([key, value]) => sanitizeHtml`${key}="${value}"`,
42+
);
43+
const attributesHtml = createSafeHtmlWithoutSanitize`${attributesParts.join('')}`;
44+
45+
return sanitizeHtml`<${type} ${attributesHtml}>${childrenHtml}</${type}>`;
46+
};
47+
48+
const Fragment: JSX.Fragment = (properties) => {
49+
if (properties?.children == null) {
50+
return createSafeHtmlWithoutSanitize``;
51+
}
52+
53+
if (!Array.isArray(properties.children)) {
54+
return sanitizeHtml`${properties.children}`;
55+
}
56+
57+
const flatChildren: unknown[] = properties.children.flat(maxDepth);
58+
const childrenParts: readonly SafeHtml[] = flatChildren.map((child) =>
59+
isSafeHtml(child) ? child : sanitizeHtml`${child}`,
60+
);
61+
62+
return createSafeHtmlWithoutSanitize`${childrenParts.join('')}`;
63+
};
964

10-
return {createElement, Fragment};
65+
return {Fragment, createElement};
1166
}

src/utils/report/client/index.ts

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -41,6 +41,12 @@ export {
4141
renderTestRunError,
4242
} from './render';
4343
/** @internal */
44-
export {createSafeHtmlWithoutSanitize, sanitizeHtml, sanitizeJson} from './sanitizeHtml';
44+
export {
45+
createSafeHtmlWithoutSanitize,
46+
isSafeHtml,
47+
sanitizeHtml,
48+
sanitizeJson,
49+
sanitizeValue,
50+
} from './sanitizeHtml';
4551
/** @internal */
4652
export {setReadJsonReportDataObservers} from './setReadJsonReportDataObservers';

src/utils/report/client/initialScript.ts

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,8 @@ import {setReadJsonReportDataObservers as clientSetReadJsonReportDataObservers}
1616

1717
import type {ReportClientState, SafeHtml} from '../../../types/internal';
1818

19+
// eslint-disable-next-line @typescript-eslint/no-unused-vars
20+
declare let jsx: JSX.Runtime;
1921
declare const reportClientState: ReportClientState;
2022

2123
const addDomContentLoadedHandler = clientAddDomContentLoadedHandler;
@@ -36,7 +38,8 @@ const setReadJsonReportDataObservers = clientSetReadJsonReportDataObservers;
3638
* @internal
3739
*/
3840
export function initialScript(): void {
39-
const jsxRuntime = createJsxRuntime();
41+
jsx = createJsxRuntime();
42+
4043
const e2edRightColumnContainer = document.getElementById('e2edRightColumnContainer') ?? undefined;
4144

4245
assertValueIsDefined(e2edRightColumnContainer);
@@ -47,7 +50,6 @@ export function initialScript(): void {
4750

4851
Object.assign<ReportClientState, Partial<ReportClientState>>(reportClientState, {
4952
e2edRightColumnContainer,
50-
jsxRuntime,
5153
locator,
5254
});
5355

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

Lines changed: 10 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,12 @@ import {createSafeHtmlWithoutSanitize as clientCreateSafeHtmlWithoutSanitize} fr
22

33
import {renderApiStatisticsItem as clientRenderApiStatisticsItem} from './renderApiStatisticsItem';
44

5-
import type {ApiStatistics, ApiStatisticsReportHash, SafeHtml} from '../../../../types/internal';
5+
import type {
6+
ApiStatistics,
7+
ApiStatisticsReportHash,
8+
ObjectEntries,
9+
SafeHtml,
10+
} from '../../../../types/internal';
611

712
const createSafeHtmlWithoutSanitize = clientCreateSafeHtmlWithoutSanitize;
813
const renderApiStatisticsItem = clientRenderApiStatisticsItem;
@@ -62,15 +67,17 @@ export function renderApiStatistics({apiStatistics, hash}: Options): SafeHtml {
6267
} else {
6368
header = 'Resources';
6469

65-
for (const [url, byStatusCode] of Object.entries(apiStatistics.resources)) {
70+
for (const [url, byStatusCode] of Object.entries(apiStatistics.resources) as ObjectEntries<
71+
typeof apiStatistics.resources
72+
>) {
6673
for (const [statusCode, {count, duration, size}] of Object.entries(byStatusCode)) {
6774
items.push(
6875
renderApiStatisticsItem({
6976
count,
7077
duration,
71-
isUrl: true,
7278
name: `${url} ${statusCode}`,
7379
size,
80+
url,
7481
}),
7582
);
7683
}

src/utils/report/client/render/renderApiStatisticsItem.tsx

Lines changed: 6 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -1,18 +1,18 @@
11
import {renderDuration as clientRenderDuration} from './renderDuration';
22

3-
import type {ReportClientState, SafeHtml} from '../../../../types/internal';
3+
import type {SafeHtml, Url} from '../../../../types/internal';
44

55
const renderDuration = clientRenderDuration;
66

7-
declare const reportClientState: ReportClientState;
7+
declare const jsx: JSX.Runtime;
88

99
type Options = Readonly<{
1010
count: number;
1111
duration: number;
1212
isHeader?: boolean;
13-
isUrl?: boolean;
1413
name: string;
1514
size?: number;
15+
url?: Url;
1616
}>;
1717

1818
/**
@@ -24,23 +24,22 @@ export function renderApiStatisticsItem({
2424
count,
2525
duration,
2626
isHeader,
27-
isUrl,
2827
name,
2928
size,
29+
url,
3030
}: Options): SafeHtml {
3131
const bytesInKiB = 1_024;
3232
const durationHtml = renderDuration(duration / count);
3333
const countHtml = `${count}x`;
3434
const sizeHtml = size === undefined ? '' : `${(size / count / bytesInKiB).toFixed(2)} KiB / `;
35-
const {createElement, Fragment} = reportClientState.jsxRuntime;
3635

3736
let nameHtml: SafeHtml;
3837

3938
if (isHeader) {
4039
nameHtml = <b>{name}</b>;
41-
} else if (isUrl) {
40+
} else if (url !== undefined) {
4241
nameHtml = (
43-
<a href={name} rel="noreferrer" target="_blank">
42+
<a href={url} rel="noreferrer" target="_blank">
4443
{name}
4544
</a>
4645
);

0 commit comments

Comments
 (0)