Skip to content

Commit dea7172

Browse files
authored
Merge pull request #110 from joomcode/fix/support-custom-csp-hosts-in-html-report
fix: support custom CSP hosts for images in HTML report
2 parents f80607e + dc618af commit dea7172

7 files changed

Lines changed: 81 additions & 18 deletions

File tree

autotests/configurator/mapLogPayloadInConsole.ts

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -19,7 +19,8 @@ export const mapLogPayloadInConsole: MapLogPayloadInConsole = (message, payload)
1919

2020
if (
2121
message.startsWith('Caught an error when running tests in retry') ||
22-
message.startsWith('Usage:')
22+
message.startsWith('Usage:') ||
23+
message.includes('report was written')
2324
) {
2425
return payload;
2526
}

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

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -32,24 +32,24 @@ export function renderStepContent({pathToScreenshotOfPage, payload, type}: Optio
3232

3333
if (pathToScreenshotOfPage !== undefined) {
3434
images.push(
35-
sanitizeHtml`<img src="${pathToScreenshotOfPage}" alt="Screenshot of page" title="Screenshot of page">`,
35+
sanitizeHtml`<img src="${pathToScreenshotOfPage}" alt="Screenshot of page" title="Screenshot of page" />`,
3636
);
3737
}
3838

3939
if (type === LogEventType.InternalAssert) {
4040
const {actualScreenshotUrl, diffScreenshotUrl, expectedScreenshotUrl} = payload;
4141

4242
if (typeof actualScreenshotUrl === 'string') {
43-
images.push(sanitizeHtml`<img src="${actualScreenshotUrl}" alt="Actual" title="Actual">`);
43+
images.push(sanitizeHtml`<img src="${actualScreenshotUrl}" alt="Actual" title="Actual" />`);
4444
}
4545

4646
if (typeof diffScreenshotUrl === 'string') {
47-
images.push(sanitizeHtml`<img src="${diffScreenshotUrl}" alt="Diff" title="Diff">`);
47+
images.push(sanitizeHtml`<img src="${diffScreenshotUrl}" alt="Diff" title="Diff" />`);
4848
}
4949

5050
if (typeof expectedScreenshotUrl === 'string') {
5151
images.push(
52-
sanitizeHtml`<img src="${expectedScreenshotUrl}" alt="Expected" title="Expected">`,
52+
sanitizeHtml`<img src="${expectedScreenshotUrl}" alt="Expected" title="Expected" />`,
5353
);
5454
}
5555
}

src/utils/report/getImgCspHosts.ts

Lines changed: 43 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,43 @@
1+
import {URL} from 'node:url';
2+
3+
import {LogEventType} from '../../constants/internal';
4+
5+
import type {ReportData} from '../../types/internal';
6+
7+
/**
8+
* Get string with images hosts using in HTML report, for CSP `img-src` rule.
9+
* @internal
10+
*/
11+
export const getImgCspHosts = (reportData: ReportData): string => {
12+
const hosts = Object.create(null) as Record<string, true>;
13+
const {retries} = reportData;
14+
15+
const processMaybeUrl = (maybeUrl: unknown): void => {
16+
if (typeof maybeUrl === 'string' && maybeUrl.startsWith('https://')) {
17+
try {
18+
const {origin} = new URL(maybeUrl);
19+
20+
hosts[origin] = true;
21+
} catch {}
22+
}
23+
};
24+
25+
for (const {fullTestRuns} of retries) {
26+
for (const {logEvents} of fullTestRuns) {
27+
for (const {payload, type} of logEvents) {
28+
// eslint-disable-next-line max-depth
29+
if (type !== LogEventType.InternalAssert || payload === undefined) {
30+
continue;
31+
}
32+
33+
const {actualScreenshotUrl, diffScreenshotUrl, expectedScreenshotUrl} = payload;
34+
35+
processMaybeUrl(actualScreenshotUrl);
36+
processMaybeUrl(diffScreenshotUrl);
37+
processMaybeUrl(expectedScreenshotUrl);
38+
}
39+
}
40+
}
41+
42+
return Object.keys(hosts).join(' ');
43+
};

src/utils/report/render/renderHead.ts

Lines changed: 11 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -12,28 +12,31 @@ import type {SafeHtml} from '../../../types/internal';
1212
* Renders tag `<head>`.
1313
* @internal
1414
*/
15-
export const renderHead = (reportFileName: string): SafeHtml => {
15+
export const renderHead = (reportFileName: string, imgCspHosts: string): SafeHtml => {
1616
const renderedScript = renderScript();
1717
const renderedStyle = renderStyle();
1818

1919
const scriptContent = getContentFromRenderedElement(renderedScript);
2020
const styleContent = getContentFromRenderedElement(renderedStyle);
2121

22-
const cspStyleHash = getCspHash(styleContent);
2322
const cspScriptHash = getCspHash(scriptContent);
23+
const cspStyleHash = getCspHash(styleContent);
24+
25+
const cspContent = [
26+
"default-src 'self';",
27+
`img-src 'self' data: ${imgCspHosts};`,
28+
`script-src '${cspScriptHash}';`,
29+
`style-src '${cspStyleHash}';`,
30+
];
2431

25-
const safeCspStyleHash = createSafeHtmlWithoutSanitize`${cspStyleHash}`;
26-
const safeCspScriptHash = createSafeHtmlWithoutSanitize`${cspScriptHash}`;
32+
const safeCspContent = createSafeHtmlWithoutSanitize`${cspContent.join(' ')}`;
2733

2834
return sanitizeHtml`
2935
<head>
3036
<meta charset="utf-8" />
3137
<meta name="viewport" content="width=device-width, initial-scale=1" />
3238
<meta name="description" content="${reportFileName}" />
33-
<meta
34-
http-equiv="Content-Security-Policy"
35-
content="default-src 'self'; img-src 'self' data:; script-src '${safeCspScriptHash}'; style-src '${safeCspStyleHash}';"
36-
/>
39+
<meta http-equiv="Content-Security-Policy" content="${safeCspContent}" />
3740
<title>${reportFileName}</title>
3841
${renderFavicon()}
3942
${renderedStyle}

src/utils/report/render/renderReportToHtml.ts

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@ import {generalLog} from '../../generalLog';
33
import {getDurationWithUnits} from '../../getDurationWithUnits';
44

55
import {sanitizeHtml} from '../client';
6+
import {getImgCspHosts} from '../getImgCspHosts';
67
import {getRetriesProps} from '../getRetriesProps';
78

89
import {locator} from './locator';
@@ -26,13 +27,14 @@ export const renderReportToHtml = (reportData: ReportData): SafeHtml => {
2627

2728
assertValueIsNotNull(reportFileName, 'reportFileName is not null');
2829

30+
const imgCspHosts = getImgCspHosts(reportData);
2931
const retries = getRetriesProps(reportData);
3032
const retryNumbers = retries.map(({retryIndex}) => retryIndex);
3133
const maxRetry = Math.max(...retryNumbers);
3234

3335
const safeHtml = sanitizeHtml`<!DOCTYPE html>
3436
<html lang="en">
35-
${renderHead(reportFileName)}
37+
${renderHead(reportFileName, imgCspHosts)}
3638
<body>
3739
${renderNavigation({retries})}
3840
<div class="main" role="tabpanel">

src/utils/report/writeHtmlReport.ts

Lines changed: 9 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,9 @@
11
import {join} from 'node:path';
22

3-
import {REPORTS_DIRECTORY_PATH} from '../../constants/internal';
3+
import {
4+
ABSOLUTE_PATH_TO_PROJECT_ROOT_DIRECTORY,
5+
REPORTS_DIRECTORY_PATH,
6+
} from '../../constants/internal';
47

58
import {assertValueIsNotNull} from '../asserts';
69
import {getFileSize, writeFile} from '../fs';
@@ -25,7 +28,11 @@ export const writeHtmlReport = async (reportData: ReportData): Promise<void> =>
2528

2629
assertValueIsNotNull(reportFileName, 'reportFileName is not null');
2730

28-
const reportFilePath = join(REPORTS_DIRECTORY_PATH, reportFileName) as FilePathFromRoot;
31+
const reportFilePath = join(
32+
ABSOLUTE_PATH_TO_PROJECT_ROOT_DIRECTORY,
33+
REPORTS_DIRECTORY_PATH,
34+
reportFileName,
35+
) as FilePathFromRoot;
2936

3037
await writeFile(reportFilePath, String(reportHtml));
3138

src/utils/report/writeLiteJsonReport.ts

Lines changed: 9 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,9 @@
11
import {join} from 'node:path';
22

3-
import {REPORTS_DIRECTORY_PATH} from '../../constants/internal';
3+
import {
4+
ABSOLUTE_PATH_TO_PROJECT_ROOT_DIRECTORY,
5+
REPORTS_DIRECTORY_PATH,
6+
} from '../../constants/internal';
47

58
import {getFileSize, writeFile} from '../fs';
69
import {generalLog} from '../generalLog';
@@ -21,7 +24,11 @@ export const writeLiteJsonReport = async (liteReport: LiteReport): Promise<void>
2124
const {liteReportFileName} = liteReport;
2225
const reportJson = JSON.stringify(liteReport);
2326

24-
const reportFilePath = join(REPORTS_DIRECTORY_PATH, liteReportFileName) as FilePathFromRoot;
27+
const reportFilePath = join(
28+
ABSOLUTE_PATH_TO_PROJECT_ROOT_DIRECTORY,
29+
REPORTS_DIRECTORY_PATH,
30+
liteReportFileName,
31+
) as FilePathFromRoot;
2532

2633
await writeFile(reportFilePath, reportJson);
2734

0 commit comments

Comments
 (0)