From 6a057e28287bac4901695871aa674523ca60fcc8 Mon Sep 17 00:00:00 2001 From: uid11 Date: Wed, 1 Oct 2025 18:58:12 +0300 Subject: [PATCH 1/3] PRO-12619 feat: full update of HTML report layout --- autotests/packs/allTests.ts | 2 +- src/actions/asserts/assertUrlMatchRoute.ts | 4 +- src/types/report.ts | 3 +- src/utils/parse/parseValueAsJsonIfNeeded.ts | 12 +-- .../{chooseTestRun.ts => chooseTestRun.tsx} | 19 +++-- src/utils/report/client/createJsxRuntime.ts | 2 +- src/utils/report/client/index.ts | 22 +++--- src/utils/report/client/initialScript.ts | 13 +--- ...nderApiStatistics.ts => ApiStatistics.tsx} | 60 +++++++------- .../client/render/ApiStatisticsItem.tsx | 7 +- ...nderDatesInterval.ts => DatesInterval.tsx} | 22 +++--- .../{renderDuration.ts => Duration.tsx} | 10 ++- .../MaybeApiStatistics.tsx} | 24 +++--- .../client/render/{renderStep.ts => Step.tsx} | 51 ++++++------ .../report/client/render/StepContent.tsx | 65 ++++++++++++++++ .../render/{renderSteps.ts => Steps.tsx} | 14 ++-- .../client/render/TestRunDescription.tsx | 78 +++++++++++++++++++ .../report/client/render/TestRunDetails.tsx | 53 +++++++++++++ ...renderTestRunError.ts => TestRunError.tsx} | 24 +++--- src/utils/report/client/render/index.ts | 22 +++--- .../report/client/render/renderStepContent.ts | 62 --------------- .../client/render/renderTestRunDescription.ts | 71 ----------------- .../client/render/renderTestRunDetails.ts | 47 ----------- src/utils/report/client/sanitizeHtml.ts | 2 +- src/utils/report/render/RetryHeader.tsx | 31 ++++++++ src/utils/report/render/Warnings.tsx | 4 +- src/utils/report/render/locator.ts | 2 +- .../{client => }/render/renderAttributes.ts | 4 +- src/utils/report/render/renderRetry.ts | 4 +- src/utils/report/render/renderRetryHeader.ts | 24 ------ .../report/render/renderTestRunButton.ts | 4 +- src/utils/test/preparePage.ts | 4 +- styles/report.css | 2 +- 33 files changed, 402 insertions(+), 366 deletions(-) rename src/utils/report/client/{chooseTestRun.ts => chooseTestRun.tsx} (81%) rename src/utils/report/client/render/{renderApiStatistics.ts => ApiStatistics.tsx} (63%) rename src/utils/report/client/render/{renderDatesInterval.ts => DatesInterval.tsx} (57%) rename src/utils/report/client/render/{renderDuration.ts => Duration.tsx} (81%) rename src/utils/report/client/{maybeRenderApiStatistics.ts => render/MaybeApiStatistics.tsx} (68%) rename src/utils/report/client/render/{renderStep.ts => Step.tsx} (53%) create mode 100644 src/utils/report/client/render/StepContent.tsx rename src/utils/report/client/render/{renderSteps.ts => Steps.tsx} (78%) create mode 100644 src/utils/report/client/render/TestRunDescription.tsx create mode 100644 src/utils/report/client/render/TestRunDetails.tsx rename src/utils/report/client/render/{renderTestRunError.ts => TestRunError.tsx} (55%) delete mode 100644 src/utils/report/client/render/renderStepContent.ts delete mode 100644 src/utils/report/client/render/renderTestRunDescription.ts delete mode 100644 src/utils/report/client/render/renderTestRunDetails.ts create mode 100644 src/utils/report/render/RetryHeader.tsx rename src/utils/report/{client => }/render/renderAttributes.ts (86%) delete mode 100644 src/utils/report/render/renderRetryHeader.ts diff --git a/autotests/packs/allTests.ts b/autotests/packs/allTests.ts index 2350c0e3..69d88c5f 100644 --- a/autotests/packs/allTests.ts +++ b/autotests/packs/allTests.ts @@ -80,7 +80,7 @@ export const pack: Pack = { skipTests, takeFullPageScreenshotOnError: false, takeViewportScreenshotOnError: true, - testFileGlobs: ['**/autotests/tests/**/*.ts'], + testFileGlobs: ['**/autotests/tests/**/exists.ts'], testIdleTimeout: 8_000, testTimeout: 15_000, userAgent, diff --git a/src/actions/asserts/assertUrlMatchRoute.ts b/src/actions/asserts/assertUrlMatchRoute.ts index fb51c630..b7065f18 100644 --- a/src/actions/asserts/assertUrlMatchRoute.ts +++ b/src/actions/asserts/assertUrlMatchRoute.ts @@ -4,7 +4,7 @@ import {assertValueIsDefined, assertValueIsNotNull} from '../../utils/asserts'; import {log} from '../../utils/log'; import type {Route} from '../../Route'; -import type {Url} from '../../types/internal'; +import type {MaybePromise, Url} from '../../types/internal'; type MaybeUrlOrPath = Url | string | null | undefined; @@ -12,7 +12,7 @@ type MaybeUrlOrPath = Url | string | null | undefined; * Asserts that url or url path (which can be wrapped in a promise) match route. */ export const assertUrlMatchRoute = async ( - maybeUrlOrPath: MaybeUrlOrPath | Promise, + maybeUrlOrPath: MaybePromise, route: Route, ): Promise => { const {routeParams} = route; diff --git a/src/types/report.ts b/src/types/report.ts index 40426edd..a87d4ab4 100644 --- a/src/types/report.ts +++ b/src/types/report.ts @@ -5,7 +5,6 @@ import type {EndE2edReason, ExitCode, TestRunStatus} from '../constants/internal import type {ApiStatistics} from './apiStatistics'; import type {FullPackConfig} from './config'; import type {UtcTimeInMs} from './date'; -import type {SafeHtml} from './html'; import type {TestFilePath} from './paths'; import type {StartInfo} from './startInfo'; import type {FullTestRun, LiteTestRun, RunHash, RunId} from './testRun'; @@ -99,7 +98,7 @@ export type ReportClientState = { readonly fullTestRuns: readonly FullTestRun[]; readonly internalDirectoryName: string; lengthOfReadedJsonReportDataParts: number; - readonly locator: LocatorFunction; + readonly locator: LocatorFunction; readonly pathToScreenshotsDirectoryForReport: string | null; readonly readJsonReportDataObservers: MutationObserver[]; reportClientData?: ReportClientData; diff --git a/src/utils/parse/parseValueAsJsonIfNeeded.ts b/src/utils/parse/parseValueAsJsonIfNeeded.ts index 2979f0b9..54cb8b75 100644 --- a/src/utils/parse/parseValueAsJsonIfNeeded.ts +++ b/src/utils/parse/parseValueAsJsonIfNeeded.ts @@ -4,24 +4,24 @@ type Return = Readonly<{hasParseError: boolean; value: unknown}>; /** * Parses `unknown` value as JSON, if needed. - * If `isoValueInJsonFormat` is `true`, then parses value as JSON and saves parse error. - * If `isoValueInJsonFormat` is `false`, then returns value as is. - * If `isoValueInJsonFormat` is `undefined`, then safely tries to parse value as JSON. + * If `isValueInJsonFormat` is `true`, then parses value as JSON and saves parse error. + * If `isValueInJsonFormat` is `false`, then returns value as is. + * If `isValueInJsonFormat` is `undefined`, then safely tries to parse value as JSON. */ export const parseValueAsJsonIfNeeded = ( originalValue: unknown, - isoValueInJsonFormat?: boolean, + isValueInJsonFormat?: boolean, ): Return => { let hasParseError = false; let value = originalValue; - if (isoValueInJsonFormat === true) { + if (isValueInJsonFormat === true) { try { value = parseMaybeEmptyValueAsJson(originalValue); } catch { hasParseError = true; } - } else if (isoValueInJsonFormat !== false) { + } else if (isValueInJsonFormat !== false) { try { value = parseMaybeEmptyValueAsJson(originalValue); } catch {} diff --git a/src/utils/report/client/chooseTestRun.ts b/src/utils/report/client/chooseTestRun.tsx similarity index 81% rename from src/utils/report/client/chooseTestRun.ts rename to src/utils/report/client/chooseTestRun.tsx index c1af5cfc..6c7a01b9 100644 --- a/src/utils/report/client/chooseTestRun.ts +++ b/src/utils/report/client/chooseTestRun.tsx @@ -1,13 +1,16 @@ import {assertValueIsDefined as clientAssertValueIsDefined} from './assertValueIsDefined'; -import {maybeRenderApiStatistics as clientMaybeRenderApiStatistics} from './maybeRenderApiStatistics'; -import {renderTestRunDetails as clientRenderTestRunDetails} from './render'; +import { + MaybeApiStatistics as clientMaybeApiStatistics, + TestRunDetails as clientTestRunDetails, +} from './render'; -import type {ReportClientState, RunHash, SafeHtml} from '../../../types/internal'; +import type {ReportClientState, RunHash} from '../../../types/internal'; const assertValueIsDefined: typeof clientAssertValueIsDefined = clientAssertValueIsDefined; -const maybeRenderApiStatistics = clientMaybeRenderApiStatistics; -const renderTestRunDetails = clientRenderTestRunDetails; +const MaybeApiStatistics = clientMaybeApiStatistics; +const TestRunDetails = clientTestRunDetails; +declare const jsx: JSX.Runtime; declare const reportClientState: ReportClientState; /** @@ -62,9 +65,9 @@ export function chooseTestRun(runHash: RunHash): void { return; } - let rightColumnHtml: SafeHtml | undefined = maybeRenderApiStatistics(runHash); + let rightColumnHtml = ; - if (rightColumnHtml === undefined) { + if (rightColumnHtml.length === 0) { const {fullTestRuns} = reportClientState; const fullTestRun = fullTestRuns.find((testRun) => testRun.runHash === runHash); @@ -77,7 +80,7 @@ export function chooseTestRun(runHash: RunHash): void { return; } - rightColumnHtml = renderTestRunDetails(fullTestRun); + rightColumnHtml = ; } e2edRightColumnContainer.innerHTML = String(rightColumnHtml); diff --git a/src/utils/report/client/createJsxRuntime.ts b/src/utils/report/client/createJsxRuntime.ts index a6fc045b..deb8519f 100644 --- a/src/utils/report/client/createJsxRuntime.ts +++ b/src/utils/report/client/createJsxRuntime.ts @@ -38,7 +38,7 @@ export function createJsxRuntime(): JSX.Runtime { } const attributesParts: readonly SafeHtml[] = Object.entries(properties).map( - ([key, value]) => sanitizeHtml`${key}="${value}"`, + ([key, value]) => sanitizeHtml`${key.toLowerCase()}="${value}"`, ); const attributesHtml = createSafeHtmlWithoutSanitize`${attributesParts.join('')}`; diff --git a/src/utils/report/client/index.ts b/src/utils/report/client/index.ts index 1d5fdaf0..4bf2dbcb 100644 --- a/src/utils/report/client/index.ts +++ b/src/utils/report/client/index.ts @@ -17,8 +17,6 @@ export {createJsxRuntime} from './createJsxRuntime'; /** @internal */ export {initialScript} from './initialScript'; /** @internal */ -export {maybeRenderApiStatistics} from './maybeRenderApiStatistics'; -/** @internal */ export {onDomContentLoad} from './onDomContentLoad'; /** @internal */ export {onFirstJsonReportDataLoad} from './onFirstJsonReportDataLoad'; @@ -30,17 +28,17 @@ export {readJsonReportData} from './readJsonReportData'; export {readPartOfJsonReportData} from './readPartOfJsonReportData'; /** @internal */ export { + ApiStatistics, ApiStatisticsItem, - renderApiStatistics, - renderAttributes, - renderDatesInterval, - renderDuration, - renderStep, - renderStepContent, - renderSteps, - renderTestRunDescription, - renderTestRunDetails, - renderTestRunError, + DatesInterval, + Duration, + MaybeApiStatistics, + Step, + StepContent, + Steps, + TestRunDescription, + TestRunDetails, + TestRunError, } from './render'; /** @internal */ export { diff --git a/src/utils/report/client/initialScript.ts b/src/utils/report/client/initialScript.ts index 63a47cab..cea5354f 100644 --- a/src/utils/report/client/initialScript.ts +++ b/src/utils/report/client/initialScript.ts @@ -1,7 +1,4 @@ -import { - createSimpleLocator as clientCreateSimpleLocator, - type LocatorFunction, -} from 'create-locator'; +import {createSimpleLocator as clientCreateSimpleLocator} from 'create-locator'; import {addDomContentLoadedHandler as clientAddDomContentLoadedHandler} from './addDomContentLoadedHandler'; import {addOnClickOnClass as clientAddOnClickOnClass} from './addOnClickOnClass'; @@ -10,10 +7,9 @@ import {clickOnStep as clientClickOnStep} from './clickOnStep'; import {clickOnTestRun as clientClickOnTestRun} from './clickOnTestRun'; import {createJsxRuntime as clientCreateJsxRuntime} from './createJsxRuntime'; import {onDomContentLoad as clientOnDomContentLoad} from './onDomContentLoad'; -import {renderAttributes as clientRenderAttributes} from './render'; import {setReadJsonReportDataObservers as clientSetReadJsonReportDataObservers} from './setReadJsonReportDataObservers'; -import type {ReportClientState, SafeHtml} from '../../../types/internal'; +import type {ReportClientState} from '../../../types/internal'; // eslint-disable-next-line @typescript-eslint/no-unused-vars declare let jsx: JSX.Runtime; @@ -27,7 +23,6 @@ const clickOnTestRun = clientClickOnTestRun; const createJsxRuntime = clientCreateJsxRuntime; const createSimpleLocator = clientCreateSimpleLocator; const onDomContentLoad = clientOnDomContentLoad; -const renderAttributes = clientRenderAttributes; const setReadJsonReportDataObservers = clientSetReadJsonReportDataObservers; /** @@ -38,9 +33,7 @@ const setReadJsonReportDataObservers = clientSetReadJsonReportDataObservers; export function initialScript(): void { jsx = createJsxRuntime(); - const {locator: locatorAttributes} = createSimpleLocator(reportClientState.createLocatorOptions); - const locator: LocatorFunction = (...args) => - renderAttributes(locatorAttributes(...(args as [string]))); + const {locator} = createSimpleLocator(reportClientState.createLocatorOptions); Object.assign>(reportClientState, {locator}); diff --git a/src/utils/report/client/render/renderApiStatistics.ts b/src/utils/report/client/render/ApiStatistics.tsx similarity index 63% rename from src/utils/report/client/render/renderApiStatistics.ts rename to src/utils/report/client/render/ApiStatistics.tsx index d354f81f..ea7c61ae 100644 --- a/src/utils/report/client/render/renderApiStatistics.ts +++ b/src/utils/report/client/render/ApiStatistics.tsx @@ -3,7 +3,7 @@ import {createSafeHtmlWithoutSanitize as clientCreateSafeHtmlWithoutSanitize} fr import {ApiStatisticsItem as clientApiStatisticsItem} from './ApiStatisticsItem'; import type { - ApiStatistics, + ApiStatistics as ApiStatisticsType, ApiStatisticsReportHash, ObjectEntries, SafeHtml, @@ -12,8 +12,10 @@ import type { const createSafeHtmlWithoutSanitize = clientCreateSafeHtmlWithoutSanitize; const ApiStatisticsItem = clientApiStatisticsItem; -type Options = Readonly<{ - apiStatistics: ApiStatistics; +declare const jsx: JSX.Runtime; + +type Props = Readonly<{ + apiStatistics: ApiStatisticsType; hash: ApiStatisticsReportHash; }>; @@ -22,7 +24,7 @@ type Options = Readonly<{ * This base client function should not use scope variables (except other base functions). * @internal */ -export function renderApiStatistics({apiStatistics, hash}: Options): SafeHtml { +export const ApiStatistics: JSX.Component = ({apiStatistics, hash}) => { let header: string | undefined; const items: SafeHtml[] = []; @@ -38,11 +40,13 @@ export function renderApiStatistics({apiStatistics, hash}: Options): SafeHtml { pageCount += count; pageDuration += duration; - pageItems.push(ApiStatisticsItem({count, duration, name: url, url})); + pageItems.push( + , + ); } items.push( - ApiStatisticsItem({count: pageCount, duration: pageDuration, isHeader: true, name}), + , ); items.push(...pageItems); } @@ -54,12 +58,12 @@ export function renderApiStatistics({apiStatistics, hash}: Options): SafeHtml { // eslint-disable-next-line max-depth for (const [statusCode, {count, duration, size}] of Object.entries(byStatusCode)) { items.push( - ApiStatisticsItem({ - count, - duration, - name: `${method} ${url} ${statusCode}`, - size, - }), + , ); } } @@ -72,23 +76,25 @@ export function renderApiStatistics({apiStatistics, hash}: Options): SafeHtml { >) { for (const [statusCode, {count, duration, size}] of Object.entries(byStatusCode)) { items.push( - ApiStatisticsItem({ - count, - duration, - name: `${url} ${statusCode}`, - size, - url, - }), + , ); } } } - return createSafeHtmlWithoutSanitize`
-

-

${header}

-
-
${items.join('')}
-
-
`; -} + return ( +
+

+

{header}

+
+
{createSafeHtmlWithoutSanitize`${items.join('')}`}
+
+
+ ); +}; diff --git a/src/utils/report/client/render/ApiStatisticsItem.tsx b/src/utils/report/client/render/ApiStatisticsItem.tsx index f451a93b..8cbbecf5 100644 --- a/src/utils/report/client/render/ApiStatisticsItem.tsx +++ b/src/utils/report/client/render/ApiStatisticsItem.tsx @@ -1,8 +1,8 @@ -import {renderDuration as clientRenderDuration} from './renderDuration'; +import {Duration as clientDuration} from './Duration'; import type {SafeHtml, Url} from '../../../../types/internal'; -const renderDuration = clientRenderDuration; +const Duration = clientDuration; declare const jsx: JSX.Runtime; @@ -29,7 +29,6 @@ export const ApiStatisticsItem: JSX.Component = ({ url, }) => { const bytesInKiB = 1_024; - const durationHtml = renderDuration(duration / count); const countHtml = `${count}x`; const sizeHtml = size === undefined ? '' : `${(size / count / bytesInKiB).toFixed(2)} KiB / `; @@ -52,7 +51,7 @@ export const ApiStatisticsItem: JSX.Component = ({ {nameHtml} {countHtml} / {sizeHtml} - {durationHtml} + ); diff --git a/src/utils/report/client/render/renderDatesInterval.ts b/src/utils/report/client/render/DatesInterval.tsx similarity index 57% rename from src/utils/report/client/render/renderDatesInterval.ts rename to src/utils/report/client/render/DatesInterval.tsx index 24cf22c3..b4a47950 100644 --- a/src/utils/report/client/render/renderDatesInterval.ts +++ b/src/utils/report/client/render/DatesInterval.tsx @@ -1,22 +1,20 @@ /* eslint-disable @typescript-eslint/no-magic-numbers */ -import {sanitizeHtml as clientSanitizeHtml} from '../sanitizeHtml'; +import type {UtcTimeInMs} from '../../../../types/internal'; -import type {SafeHtml, UtcTimeInMs} from '../../../../types/internal'; - -type Options = Readonly<{ +type Props = Readonly<{ endTimeInMs: UtcTimeInMs; startTimeInMs: UtcTimeInMs; }>; -const sanitizeHtml = clientSanitizeHtml; +declare const jsx: JSX.Runtime; /** * Renders the interval between two dates. * This base client function should not use scope variables (except other base functions). * @internal */ -export function renderDatesInterval({endTimeInMs, startTimeInMs}: Options): SafeHtml { +export const DatesInterval: JSX.Component = ({endTimeInMs, startTimeInMs}) => { const startDate = new Date(startTimeInMs); const endDate = new Date(endTimeInMs); @@ -28,6 +26,12 @@ export function renderDatesInterval({endTimeInMs, startTimeInMs}: Options): Safe const startTime = startDateTime.slice(11, 19); const endTime = endDateTime.slice(11, 19); - return sanitizeHtml` – - UTC`; -} + return ( + <> + {' '} + – UTC + + ); +}; diff --git a/src/utils/report/client/render/renderDuration.ts b/src/utils/report/client/render/Duration.tsx similarity index 81% rename from src/utils/report/client/render/renderDuration.ts rename to src/utils/report/client/render/Duration.tsx index e04b5964..836d3547 100644 --- a/src/utils/report/client/render/renderDuration.ts +++ b/src/utils/report/client/render/Duration.tsx @@ -2,18 +2,20 @@ import {getDurationWithUnits as clientGetDurationWithUnits} from '../../../getDu import {createSafeHtmlWithoutSanitize as clientCreateSafeHtmlWithoutSanitize} from '../sanitizeHtml'; -import type {SafeHtml} from '../../../../types/internal'; - const getDurationWithUnits = clientGetDurationWithUnits; const createSafeHtmlWithoutSanitize = clientCreateSafeHtmlWithoutSanitize; +declare const jsx: JSX.Runtime; + +type Props = Readonly<{durationInMs: number}>; + /** * Renders the duration of time interval in hours, minutes, seconds and milliseconds. * This base client function should not use scope variables (except other base functions). * @internal */ -export function renderDuration(durationInMs: number): SafeHtml { +export const Duration: JSX.Component = ({durationInMs}) => { const durationWithUnits = getDurationWithUnits(durationInMs); return createSafeHtmlWithoutSanitize`${durationWithUnits}`; -} +}; diff --git a/src/utils/report/client/maybeRenderApiStatistics.ts b/src/utils/report/client/render/MaybeApiStatistics.tsx similarity index 68% rename from src/utils/report/client/maybeRenderApiStatistics.ts rename to src/utils/report/client/render/MaybeApiStatistics.tsx index bad9d9f1..e297b706 100644 --- a/src/utils/report/client/maybeRenderApiStatistics.ts +++ b/src/utils/report/client/render/MaybeApiStatistics.tsx @@ -1,23 +1,21 @@ -import {renderApiStatistics as clientRenderApiStatistics} from './render'; +import {ApiStatistics as clientApiStatistics} from './ApiStatistics'; -import type { - ApiStatisticsReportHash, - ReportClientState, - RunHash, - SafeHtml, -} from '../../../types/internal'; +import type {ApiStatisticsReportHash, ReportClientState, RunHash} from '../../../../types/internal'; -const renderApiStatistics = clientRenderApiStatistics; +const ApiStatistics = clientApiStatistics; +declare const jsx: JSX.Runtime; declare const reportClientState: ReportClientState; +type Props = Readonly<{runHash: RunHash}>; + /** * Renders `ApiStatistics` by `runHash`, if this is a one of kind of `ApiStatistics` hash * (pages, requests or resources). * This base client function should not use scope variables (except other base functions). * @internal */ -export function maybeRenderApiStatistics(runHash: RunHash): SafeHtml | undefined { +export const MaybeApiStatistics: JSX.Component = ({runHash}) => { const hash = String(runHash); const pagesHash: ApiStatisticsReportHash = 'api-statistics-pages'; @@ -25,7 +23,7 @@ export function maybeRenderApiStatistics(runHash: RunHash): SafeHtml | undefined const resourcesHash: ApiStatisticsReportHash = 'api-statistics-resources'; if (hash !== pagesHash && hash !== requestsHash && hash !== resourcesHash) { - return; + return <>; } const {reportClientData} = reportClientState; @@ -36,10 +34,10 @@ export function maybeRenderApiStatistics(runHash: RunHash): SafeHtml | undefined `Cannot find report client data in JSON report data (tried to click "${hash}"). Probably JSON report data not yet completely loaded. Please try click again later`, ); - return; + return <>; } const {apiStatistics} = reportClientData; - return renderApiStatistics({apiStatistics, hash}); -} + return ; +}; diff --git a/src/utils/report/client/render/renderStep.ts b/src/utils/report/client/render/Step.tsx similarity index 53% rename from src/utils/report/client/render/renderStep.ts rename to src/utils/report/client/render/Step.tsx index 869b9975..d3e43606 100644 --- a/src/utils/report/client/render/renderStep.ts +++ b/src/utils/report/client/render/Step.tsx @@ -1,19 +1,17 @@ import {LogEventStatus, LogEventType} from '../../../../constants/internal'; -import {sanitizeHtml as clientSanitizeHtml} from '../sanitizeHtml'; +import {Duration as clientDuration} from './Duration'; +import {StepContent as clientStepContent} from './StepContent'; -import {renderDuration as clientRenderDuration} from './renderDuration'; -import {renderStepContent as clientRenderStepContent} from './renderStepContent'; +import type {LogEvent, ReportClientState, UtcTimeInMs} from '../../../../types/internal'; -import type {LogEvent, ReportClientState, SafeHtml, UtcTimeInMs} from '../../../../types/internal'; - -const renderDuration = clientRenderDuration; -const renderStepContent = clientRenderStepContent; -const sanitizeHtml = clientSanitizeHtml; +const Duration = clientDuration; +const StepContent = clientStepContent; +declare const jsx: JSX.Runtime; declare const reportClientState: ReportClientState; -type Options = Readonly<{ +type Props = Readonly<{ logEvent: LogEvent; nextLogEventTime: UtcTimeInMs; }>; @@ -23,9 +21,8 @@ type Options = Readonly<{ * This base client function should not use scope variables (except other base functions). * @internal */ -export function renderStep({logEvent, nextLogEventTime}: Options): SafeHtml { +export const Step: JSX.Component = ({logEvent, nextLogEventTime}) => { const {message, payload, time, type} = logEvent; - const durationInMs = nextLogEventTime - time; const isPayloadEmpty = !payload || Object.keys(payload).length === 0; const status = payload?.logEventStatus ?? LogEventStatus.Passed; @@ -42,19 +39,25 @@ export function renderStep({logEvent, nextLogEventTime}: Options): SafeHtml { } } - const content = renderStepContent({ - pathToScreenshotOfPage, - payload: isPayloadEmpty ? undefined : payload, - type, - }); const maybeEmptyClass = isPayloadEmpty ? 'step-expanded_is-empty' : ''; const isErrorScreenshot = pathToScreenshotOfPage !== undefined; - return sanitizeHtml` - -${content} -`; -} + return ( + <> + + + + ); +}; diff --git a/src/utils/report/client/render/StepContent.tsx b/src/utils/report/client/render/StepContent.tsx new file mode 100644 index 00000000..ac6bb2b7 --- /dev/null +++ b/src/utils/report/client/render/StepContent.tsx @@ -0,0 +1,65 @@ +import {LogEventType} from '../../../../constants/internal'; + +import {createSafeHtmlWithoutSanitize as clientCreateSafeHtmlWithoutSanitize} from '../sanitizeHtml'; + +import type {LogPayload, SafeHtml} from '../../../../types/internal'; + +const createSafeHtmlWithoutSanitize = clientCreateSafeHtmlWithoutSanitize; + +declare const jsx: JSX.Runtime; + +type Props = Readonly<{ + pathToScreenshotOfPage: string | undefined; + payload: LogPayload | undefined; + type: LogEventType; +}>; + +/** + * Renders content of single step of test run. + * This base client function should not use scope variables (except other base functions). + * @internal + */ +export const StepContent: JSX.Component = ({pathToScreenshotOfPage, payload, type}) => { + if (payload === undefined) { + return <>; + } + + const payloadString = JSON.stringify(payload, null, 2); + const code = {payloadString}; + const images: SafeHtml[] = []; + + if (pathToScreenshotOfPage !== undefined) { + images.push( + Screenshot of page, + ); + } + + if (type === LogEventType.InternalAssert) { + const {actualScreenshotUrl, diffScreenshotUrl, expectedScreenshotUrl} = payload; + + if (typeof actualScreenshotUrl === 'string') { + images.push(Actual); + } + + if (typeof diffScreenshotUrl === 'string') { + images.push(Diff); + } + + if (typeof expectedScreenshotUrl === 'string') { + images.push(Expected); + } + } + + const imagesHtml = createSafeHtmlWithoutSanitize`${images.join('')}`; + + if (images.length > 0) { + return ( +
+
{code}
+ {imagesHtml} +
+ ); + } + + return
{code}
; +}; diff --git a/src/utils/report/client/render/renderSteps.ts b/src/utils/report/client/render/Steps.tsx similarity index 78% rename from src/utils/report/client/render/renderSteps.ts rename to src/utils/report/client/render/Steps.tsx index 2efe2909..896f5164 100644 --- a/src/utils/report/client/render/renderSteps.ts +++ b/src/utils/report/client/render/Steps.tsx @@ -1,15 +1,17 @@ import {assertValueIsDefined as clientAssertValueIsDefined} from '../assertValueIsDefined'; import {createSafeHtmlWithoutSanitize as clientCreateSafeHtmlWithoutSanitize} from '../sanitizeHtml'; -import {renderStep as clientRenderStep} from './renderStep'; +import {Step as clientStep} from './Step'; import type {LogEvent, SafeHtml, UtcTimeInMs} from '../../../../types/internal'; const assertValueIsDefined: typeof clientAssertValueIsDefined = clientAssertValueIsDefined; const createSafeHtmlWithoutSanitize = clientCreateSafeHtmlWithoutSanitize; -const renderStep = clientRenderStep; +const Step = clientStep; -type Options = Readonly<{ +declare const jsx: JSX.Runtime; + +type Props = Readonly<{ endTimeInMs: UtcTimeInMs; logEvents: readonly LogEvent[]; }>; @@ -19,7 +21,7 @@ type Options = Readonly<{ * This base client function should not use scope variables (except other base functions). * @internal */ -export function renderSteps({endTimeInMs, logEvents}: Options): SafeHtml { +export const Steps: JSX.Component = ({endTimeInMs, logEvents}) => { const stepHtmls: SafeHtml[] = []; for (let index = 0; index < logEvents.length; index += 1) { @@ -29,10 +31,10 @@ export function renderSteps({endTimeInMs, logEvents}: Options): SafeHtml { const nextLogEvent = logEvents[index + 1]; const nextLogEventTime = nextLogEvent?.time ?? endTimeInMs; - const stepHtml = renderStep({logEvent, nextLogEventTime}); + const stepHtml = ; stepHtmls.push(stepHtml); } return createSafeHtmlWithoutSanitize`${stepHtmls.join('')}`; -} +}; diff --git a/src/utils/report/client/render/TestRunDescription.tsx b/src/utils/report/client/render/TestRunDescription.tsx new file mode 100644 index 00000000..af31ea38 --- /dev/null +++ b/src/utils/report/client/render/TestRunDescription.tsx @@ -0,0 +1,78 @@ +import {parseMarkdownLinks as clientParseMarkdownLinks} from '../parseMarkdownLinks'; +import {createSafeHtmlWithoutSanitize as clientCreateSafeHtmlWithoutSanitize} from '../sanitizeHtml'; + +import {DatesInterval as clientDatesInterval} from './DatesInterval'; +import {Duration as clientDuration} from './Duration'; + +import type {FullTestRun, ReportClientState, SafeHtml} from '../../../../types/internal'; + +const createSafeHtmlWithoutSanitize = clientCreateSafeHtmlWithoutSanitize; +const parseMarkdownLinks = clientParseMarkdownLinks; +const DatesInterval = clientDatesInterval; +const Duration = clientDuration; + +declare const jsx: JSX.Runtime; +declare const reportClientState: ReportClientState; + +type Props = Readonly<{fullTestRun: FullTestRun}>; + +/** + * Renders tag `
` with test run description. + * The value strings of meta can contain links in markdown format. + * This base client function should not use scope variables (except other base functions). + * @internal + */ +export const TestRunDescription: JSX.Component = ({fullTestRun}) => { + const {endTimeInMs, outputDirectoryName, runError, startTimeInMs} = fullTestRun; + const {meta} = fullTestRun.options; + const metaHtmls: SafeHtml[] = []; + + for (const [key, value] of Object.entries(meta)) { + const valueWithLinks = parseMarkdownLinks`${value}`; + const metaHtml = ( + <> +
{key}
+
{valueWithLinks}
+ + ); + + metaHtmls.push(metaHtml); + } + + let traceHtml = <>; + + if (runError !== undefined) { + const {internalDirectoryName} = reportClientState; + const traceLabel = 'Download trace'; + const traceName = 'trace.zip'; + const traceUrl = `./${internalDirectoryName}/${outputDirectoryName}/${traceName}`; + + traceHtml = ( + <> +
{traceLabel}
+
+ + {traceName} + +
+ + ); + } + + const metaProperties = createSafeHtmlWithoutSanitize`${metaHtmls.join('')}`; + + return ( +
+ {metaProperties} + {traceHtml} +
Date
+
+ +
+
Duration
+
+ +
+
+ ); +}; diff --git a/src/utils/report/client/render/TestRunDetails.tsx b/src/utils/report/client/render/TestRunDetails.tsx new file mode 100644 index 00000000..e584dc6d --- /dev/null +++ b/src/utils/report/client/render/TestRunDetails.tsx @@ -0,0 +1,53 @@ +import {assertValueIsDefined as clientAssertValueIsDefined} from '../assertValueIsDefined'; + +import {Steps as clientSteps} from './Steps'; +import {TestRunDescription as clientTestRunDescription} from './TestRunDescription'; +import {TestRunError as clientTestRunError} from './TestRunError'; + +import type {FullTestRun, ReportClientState} from '../../../../types/internal'; + +declare const jsx: JSX.Runtime; +declare const reportClientState: ReportClientState; + +const assertValueIsDefined: typeof clientAssertValueIsDefined = clientAssertValueIsDefined; +const Steps = clientSteps; +const TestRunDescription = clientTestRunDescription; +const TestRunError = clientTestRunError; + +type Props = Readonly<{fullTestRun: FullTestRun}>; + +/** + * Renders test run details for report. + * This base client function should not use scope variables (except other base functions). + * @internal + */ +export const TestRunDetails: JSX.Component = ({fullTestRun}) => { + const {endTimeInMs, filePath, logEvents, name, runError, status} = fullTestRun; + const {locator} = reportClientState; + + const firstStatusString = status[0]; + + assertValueIsDefined(firstStatusString); + + const capitalizedStatus = `${firstStatusString.toUpperCase()}${status.slice(1)}`; + + return ( +
+

{filePath}

+

+ + {capitalizedStatus} + + {name} +

+
+ +
+

Execution

+ + +
+
+
+ ); +}; diff --git a/src/utils/report/client/render/renderTestRunError.ts b/src/utils/report/client/render/TestRunError.tsx similarity index 55% rename from src/utils/report/client/render/renderTestRunError.ts rename to src/utils/report/client/render/TestRunError.tsx index 41364f8d..b4fe4fcd 100644 --- a/src/utils/report/client/render/renderTestRunError.ts +++ b/src/utils/report/client/render/TestRunError.tsx @@ -1,15 +1,19 @@ import {sanitizeHtml as clientSanitizeHtml} from '../sanitizeHtml'; -import type {RunError, SafeHtml} from '../../../../types/internal'; +import type {RunError} from '../../../../types/internal'; const sanitizeHtml = clientSanitizeHtml; +declare const jsx: JSX.Runtime; + +type Props = Readonly<{runError: RunError}>; + /** * Renders `TestRun` error as simple message. * This base client function should not use scope variables (except other base functions). * @internal */ -export function renderTestRunError(runError: RunError): SafeHtml { +export const TestRunError: JSX.Component = ({runError}) => { if (runError === undefined) { return sanitizeHtml``; } @@ -19,11 +23,11 @@ export function renderTestRunError(runError: RunError): SafeHtml { const runErrorWithoutStyle = String(runError).replace(stylesRegexp, ''); - return sanitizeHtml` -
-
- ${runErrorWithoutStyle} -
-
-`; -} + return ( +
+
+ {runErrorWithoutStyle} +
+
+ ); +}; diff --git a/src/utils/report/client/render/index.ts b/src/utils/report/client/render/index.ts index 5f7e1c3c..e4d53138 100644 --- a/src/utils/report/client/render/index.ts +++ b/src/utils/report/client/render/index.ts @@ -1,22 +1,22 @@ /** @internal */ -export {ApiStatisticsItem} from './ApiStatisticsItem'; +export {ApiStatistics} from './ApiStatistics'; /** @internal */ -export {renderApiStatistics} from './renderApiStatistics'; +export {ApiStatisticsItem} from './ApiStatisticsItem'; /** @internal */ -export {renderAttributes} from './renderAttributes'; +export {DatesInterval} from './DatesInterval'; /** @internal */ -export {renderDatesInterval} from './renderDatesInterval'; +export {Duration} from './Duration'; /** @internal */ -export {renderDuration} from './renderDuration'; +export {MaybeApiStatistics} from './MaybeApiStatistics'; /** @internal */ -export {renderStep} from './renderStep'; +export {Step} from './Step'; /** @internal */ -export {renderStepContent} from './renderStepContent'; +export {StepContent} from './StepContent'; /** @internal */ -export {renderSteps} from './renderSteps'; +export {Steps} from './Steps'; /** @internal */ -export {renderTestRunDescription} from './renderTestRunDescription'; +export {TestRunDescription} from './TestRunDescription'; /** @internal */ -export {renderTestRunDetails} from './renderTestRunDetails'; +export {TestRunDetails} from './TestRunDetails'; /** @internal */ -export {renderTestRunError} from './renderTestRunError'; +export {TestRunError} from './TestRunError'; diff --git a/src/utils/report/client/render/renderStepContent.ts b/src/utils/report/client/render/renderStepContent.ts deleted file mode 100644 index 83e2e797..00000000 --- a/src/utils/report/client/render/renderStepContent.ts +++ /dev/null @@ -1,62 +0,0 @@ -import {LogEventType} from '../../../../constants/internal'; - -import { - createSafeHtmlWithoutSanitize as clientCreateSafeHtmlWithoutSanitize, - sanitizeHtml as clientSanitizeHtml, -} from '../sanitizeHtml'; - -import type {LogPayload, SafeHtml} from '../../../../types/internal'; - -const createSafeHtmlWithoutSanitize = clientCreateSafeHtmlWithoutSanitize; -const sanitizeHtml = clientSanitizeHtml; - -type Options = Readonly<{ - pathToScreenshotOfPage: string | undefined; - payload: LogPayload | undefined; - type: LogEventType; -}>; - -/** - * Renders content of single step of test run. - * This base client function should not use scope variables (except other base functions). - * @internal - */ -export function renderStepContent({pathToScreenshotOfPage, payload, type}: Options): SafeHtml { - if (payload === undefined) { - return sanitizeHtml``; - } - - const payloadString = JSON.stringify(payload, null, 2); - const code = sanitizeHtml`${payloadString}`; - const images: SafeHtml[] = []; - - if (pathToScreenshotOfPage !== undefined) { - images.push( - sanitizeHtml`Screenshot of page`, - ); - } - - if (type === LogEventType.InternalAssert) { - const {actualScreenshotUrl, diffScreenshotUrl, expectedScreenshotUrl} = payload; - - if (typeof actualScreenshotUrl === 'string') { - images.push(sanitizeHtml`Actual`); - } - - if (typeof diffScreenshotUrl === 'string') { - images.push(sanitizeHtml`Diff`); - } - - if (typeof expectedScreenshotUrl === 'string') { - images.push( - sanitizeHtml`Expected`, - ); - } - } - - const imagesHtml = createSafeHtmlWithoutSanitize`${images.join('')}`; - const content = images.length > 0 ? sanitizeHtml`
${code}
${imagesHtml}` : code; - const contentTag = images.length > 0 ? 'div' : 'pre'; - - return sanitizeHtml`<${contentTag} class="step-expanded-panel step__panel">${content}`; -} diff --git a/src/utils/report/client/render/renderTestRunDescription.ts b/src/utils/report/client/render/renderTestRunDescription.ts deleted file mode 100644 index 30574c73..00000000 --- a/src/utils/report/client/render/renderTestRunDescription.ts +++ /dev/null @@ -1,71 +0,0 @@ -import {parseMarkdownLinks as clientParseMarkdownLinks} from '../parseMarkdownLinks'; -import { - createSafeHtmlWithoutSanitize as clientCreateSafeHtmlWithoutSanitize, - sanitizeHtml as clientSanitizeHtml, -} from '../sanitizeHtml'; - -import {renderDatesInterval as clientRenderDatesInterval} from './renderDatesInterval'; -import {renderDuration as clientRenderDuration} from './renderDuration'; - -import type {FullTestRun, ReportClientState, SafeHtml} from '../../../../types/internal'; - -const createSafeHtmlWithoutSanitize = clientCreateSafeHtmlWithoutSanitize; -const parseMarkdownLinks = clientParseMarkdownLinks; -const renderDatesInterval = clientRenderDatesInterval; -const renderDuration = clientRenderDuration; -const sanitizeHtml = clientSanitizeHtml; - -declare const reportClientState: ReportClientState; - -/** - * Renders tag `
` with test run description. - * The value strings of meta can contain links in markdown format. - * This base client function should not use scope variables (except other base functions). - * @internal - */ -export function renderTestRunDescription(fullTestRun: FullTestRun): SafeHtml { - const {endTimeInMs, outputDirectoryName, runError, startTimeInMs} = fullTestRun; - const durationInMs = endTimeInMs - startTimeInMs; - const {meta} = fullTestRun.options; - const metaHtmls: SafeHtml[] = []; - - for (const [key, value] of Object.entries(meta)) { - const valueWithLinks = parseMarkdownLinks`${value}`; - const metaHtml = sanitizeHtml` -
${key}
-
${valueWithLinks}
`; - - metaHtmls.push(metaHtml); - } - - let traceHtml: SafeHtml = createSafeHtmlWithoutSanitize``; - - if (runError !== undefined) { - const {internalDirectoryName} = reportClientState; - const traceLabel = 'Download trace'; - const traceName = 'trace.zip'; - const traceUrl = `./${internalDirectoryName}/${outputDirectoryName}/${traceName}`; - - traceHtml = sanitizeHtml` -
${traceLabel}
-
- ${traceName} -
`; - } - - const metaProperties = createSafeHtmlWithoutSanitize`${metaHtmls.join('')}`; - - return sanitizeHtml` -
- ${metaProperties} - ${traceHtml} -
Date
-
- ${renderDatesInterval({endTimeInMs, startTimeInMs})} -
-
Duration
-
- ${renderDuration(durationInMs)} -
-
`; -} diff --git a/src/utils/report/client/render/renderTestRunDetails.ts b/src/utils/report/client/render/renderTestRunDetails.ts deleted file mode 100644 index 37c15f0b..00000000 --- a/src/utils/report/client/render/renderTestRunDetails.ts +++ /dev/null @@ -1,47 +0,0 @@ -import {assertValueIsDefined as clientAssertValueIsDefined} from '../assertValueIsDefined'; -import {sanitizeHtml as clientSanitizeHtml} from '../sanitizeHtml'; - -import {renderSteps as clientRenderSteps} from './renderSteps'; -import {renderTestRunDescription as clientRenderTestRunDescription} from './renderTestRunDescription'; -import {renderTestRunError as clientRenderTestRunError} from './renderTestRunError'; - -import type {FullTestRun, ReportClientState, SafeHtml} from '../../../../types/internal'; - -declare const reportClientState: ReportClientState; - -const assertValueIsDefined: typeof clientAssertValueIsDefined = clientAssertValueIsDefined; -const renderSteps = clientRenderSteps; -const renderTestRunDescription = clientRenderTestRunDescription; -const renderTestRunError = clientRenderTestRunError; -const sanitizeHtml = clientSanitizeHtml; - -/** - * Renders tag `
` with test run details. - * This base client function should not use scope variables (except other base functions). - * @internal - */ -export function renderTestRunDetails(fullTestRun: FullTestRun): SafeHtml { - const {endTimeInMs, filePath, logEvents, name, runError, status} = fullTestRun; - const {locator} = reportClientState; - - const firstStatusString = status[0]; - - assertValueIsDefined(firstStatusString); - - const capitalizedStatus = `${firstStatusString.toUpperCase()}${status.slice(1)}`; - - return sanitizeHtml`
-

${filePath}

-

- ${capitalizedStatus}${name} -

-
- ${renderTestRunDescription(fullTestRun)} -
-

Execution

- ${renderSteps({endTimeInMs, logEvents})} - ${renderTestRunError(runError)} -
-
-
`; -} diff --git a/src/utils/report/client/sanitizeHtml.ts b/src/utils/report/client/sanitizeHtml.ts index 257c0326..349aefa0 100644 --- a/src/utils/report/client/sanitizeHtml.ts +++ b/src/utils/report/client/sanitizeHtml.ts @@ -104,5 +104,5 @@ export function sanitizeHtml( * @internal */ export function sanitizeJson(json: string): string { - return json.replace(/ = ({endTimeInMs, retryIndex, startTimeInMs}) => { + const durationInMs = endTimeInMs - startTimeInMs; + + return ( + <> +

+ Retry {retryIndex} +

+

+ ( + +

+ + ); +}; diff --git a/src/utils/report/render/Warnings.tsx b/src/utils/report/render/Warnings.tsx index 2918e008..c701d0fd 100644 --- a/src/utils/report/render/Warnings.tsx +++ b/src/utils/report/render/Warnings.tsx @@ -1,3 +1,5 @@ +import {createSafeHtmlWithoutSanitize} from '../client'; + declare const jsx: JSX.Runtime; type Props = Readonly<{warnings: readonly string[]}>; @@ -13,5 +15,5 @@ export const Warnings: JSX.Component = ({warnings}) => { const renderedWarnings = warnings.map((warning) =>
{warning}
); - return
{renderedWarnings.join('')}
; + return
{createSafeHtmlWithoutSanitize`${renderedWarnings.join('')}`}
; }; diff --git a/src/utils/report/render/locator.ts b/src/utils/report/render/locator.ts index bef9509e..11a270a1 100644 --- a/src/utils/report/render/locator.ts +++ b/src/utils/report/render/locator.ts @@ -2,7 +2,7 @@ import {type CreateLocatorOptions, createSimpleLocator, type LocatorFunction} fr import {attributesOptions, e2edEnvironment} from '../../../constants/internal'; -import {renderAttributes} from '../client'; +import {renderAttributes} from './renderAttributes'; import type {SafeHtml} from '../../../types/internal'; diff --git a/src/utils/report/client/render/renderAttributes.ts b/src/utils/report/render/renderAttributes.ts similarity index 86% rename from src/utils/report/client/render/renderAttributes.ts rename to src/utils/report/render/renderAttributes.ts index f847f793..e28327e0 100644 --- a/src/utils/report/client/render/renderAttributes.ts +++ b/src/utils/report/render/renderAttributes.ts @@ -1,8 +1,8 @@ -import {createSafeHtmlWithoutSanitize as clientCreateSafeHtmlWithoutSanitize} from '../sanitizeHtml'; +import {createSafeHtmlWithoutSanitize as clientCreateSafeHtmlWithoutSanitize} from '../client'; import type {Attributes} from 'create-locator'; -import type {SafeHtml} from '../../../../types/internal'; +import type {SafeHtml} from '../../../types/internal'; const createSafeHtmlWithoutSanitize = clientCreateSafeHtmlWithoutSanitize; diff --git a/src/utils/report/render/renderRetry.ts b/src/utils/report/render/renderRetry.ts index 5deaf688..7c876aa7 100644 --- a/src/utils/report/render/renderRetry.ts +++ b/src/utils/report/render/renderRetry.ts @@ -2,8 +2,8 @@ import {createSafeHtmlWithoutSanitize} from '../client'; import {compareByStatuses} from './compareByStatuses'; import {locatorAttributes} from './locator'; -import {renderRetryHeader} from './renderRetryHeader'; import {renderTestRunButton} from './renderTestRunButton'; +import {RetryHeader} from './RetryHeader'; import type {RetryProps, SafeHtml} from '../../../types/internal'; @@ -24,7 +24,7 @@ export const renderRetry = ({retry}: Props): SafeHtml => {
- ${renderRetryHeader({...retry})} + ${RetryHeader({...retry})} ${buttons.join('')}
`; }; diff --git a/src/utils/report/render/renderRetryHeader.ts b/src/utils/report/render/renderRetryHeader.ts deleted file mode 100644 index b7f37c0e..00000000 --- a/src/utils/report/render/renderRetryHeader.ts +++ /dev/null @@ -1,24 +0,0 @@ -import {renderDatesInterval, renderDuration, sanitizeHtml} from '../client'; - -import {locatorAttributes} from './locator'; - -import type {RetryProps, SafeHtml} from '../../../types/internal'; - -type Props = RetryProps; - -const testId = 'RetryHeader'; - -/** - * Renders retry header. - * @internal - */ -export const renderRetryHeader = ({endTimeInMs, retryIndex, startTimeInMs}: Props): SafeHtml => { - const durationInMs = endTimeInMs - startTimeInMs; - - return sanitizeHtml` -

Retry ${retryIndex}

-

- ${renderDatesInterval({endTimeInMs, startTimeInMs})} - (${renderDuration(durationInMs)}) -

`; -}; diff --git a/src/utils/report/render/renderTestRunButton.ts b/src/utils/report/render/renderTestRunButton.ts index 71105b70..b5ae6597 100644 --- a/src/utils/report/render/renderTestRunButton.ts +++ b/src/utils/report/render/renderTestRunButton.ts @@ -1,4 +1,4 @@ -import {renderDuration, sanitizeHtml} from '../client'; +import {Duration, sanitizeHtml} from '../client'; import {locatorAttributes} from './locator'; @@ -35,6 +35,6 @@ export const renderTestRunButton = ({ testId, 'parameters', )}>${mainParams} - ${renderDuration(durationInMs)} + ${Duration({durationInMs})} `; }; diff --git a/src/utils/test/preparePage.ts b/src/utils/test/preparePage.ts index d5acb03b..3f966eb6 100644 --- a/src/utils/test/preparePage.ts +++ b/src/utils/test/preparePage.ts @@ -59,11 +59,11 @@ export const preparePage = async (page: Page): Promise => { const text = message.text(); const type = message.type() as ConsoleMessageType; - consoleMessages.push({args, dateTimeInIso, location, text, type}); - for (const jsHandle of message.args()) { args.push(await jsHandle.jsonValue().catch(() => 'Error with getting value of argument')); } + + consoleMessages.push({args, dateTimeInIso, location, text, type}); }); const pageerrorListener = AsyncLocalStorage.bind((error: Error) => { diff --git a/styles/report.css b/styles/report.css index 92146e5a..4b9eee98 100644 --- a/styles/report.css +++ b/styles/report.css @@ -556,7 +556,7 @@ a:visited { color: var(--font-color); } .test-description__definition { - margin: 0; + margin: 0 0 0 4px; display: inline; overflow-wrap: break-word; } From 48a5777752253a37f8a694e8da919c775a7e097f Mon Sep 17 00:00:00 2001 From: uid11 Date: Sun, 5 Oct 2025 05:01:15 +0300 Subject: [PATCH 2/3] PRO-12619 feat: switch to new layout --- .../E2edReportExample/E2edReportExample.ts | 2 +- src/types/global.d.ts | 4 + src/utils/getDurationWithUnits.ts | 2 +- src/utils/report/client/clickOnRetry.ts | 10 +- src/utils/report/client/clickOnTestRun.ts | 6 +- src/utils/report/client/createJsxRuntime.ts | 53 +- src/utils/report/client/index.ts | 3 + src/utils/report/client/initialScript.ts | 4 +- .../client/onFirstJsonReportDataLoad.ts | 2 +- .../report/client/render/ApiStatistics.tsx | 9 +- src/utils/report/client/render/Duration.tsx | 6 +- src/utils/report/client/render/List.tsx | 28 + src/utils/report/client/render/SafeHtml.tsx | 15 + src/utils/report/client/render/Screenshot.tsx | 22 + src/utils/report/client/render/Step.tsx | 45 +- .../report/client/render/StepContent.tsx | 48 +- src/utils/report/client/render/Steps.tsx | 10 +- .../client/render/TestRunDescription.tsx | 12 +- .../report/client/render/TestRunDetails.tsx | 14 +- .../report/client/render/TestRunError.tsx | 12 +- src/utils/report/client/render/index.ts | 6 + src/utils/report/render/DragContainer.tsx | 18 + src/utils/report/render/Errors.tsx | 23 + src/utils/report/render/Favicon.tsx | 12 + src/utils/report/render/Head.tsx | 48 + ...{renderReportToHtml.tsx => HtmlReport.tsx} | 43 +- .../{renderJsonData.ts => JsonData.tsx} | 17 +- src/utils/report/render/Logo.tsx | 31 + src/utils/report/render/Metadata.tsx | 28 + src/utils/report/render/Navigation.tsx | 24 + src/utils/report/render/Retries.tsx | 22 + src/utils/report/render/RetriesButtons.tsx | 40 + src/utils/report/render/Retry.tsx | 33 + src/utils/report/render/RetryButton.tsx | 22 + src/utils/report/render/RetryHeader.tsx | 8 +- src/utils/report/render/Script.tsx | 18 + ...ScriptFunctions.ts => ScriptFunctions.tsx} | 6 +- ...nderScriptGlobals.ts => ScriptGlobals.tsx} | 14 +- .../render/{renderStyle.ts => Style.tsx} | 12 +- src/utils/report/render/TestRunButton.tsx | 49 + src/utils/report/render/Warnings.tsx | 8 +- src/utils/report/render/index.ts | 2 +- src/utils/report/render/locator.ts | 15 +- src/utils/report/render/renderAttributes.ts | 22 - src/utils/report/render/renderErrors.ts | 17 - src/utils/report/render/renderFavicon.ts | 10 - src/utils/report/render/renderHead.ts | 45 - src/utils/report/render/renderLogo.ts | 21 - src/utils/report/render/renderMetadata.ts | 23 - src/utils/report/render/renderNavigation.ts | 22 - src/utils/report/render/renderRetries.ts | 20 - .../report/render/renderRetriesButtons.ts | 33 - src/utils/report/render/renderRetry.ts | 30 - src/utils/report/render/renderRetryButton.ts | 25 - src/utils/report/render/renderScript.ts | 17 - .../report/render/renderTestRunButton.ts | 40 - ...writeHtmlReport.ts => writeHtmlReport.tsx} | 6 +- styles/report.css | 1477 ++++++++++------- 58 files changed, 1563 insertions(+), 1051 deletions(-) create mode 100644 src/utils/report/client/render/List.tsx create mode 100644 src/utils/report/client/render/SafeHtml.tsx create mode 100644 src/utils/report/client/render/Screenshot.tsx create mode 100644 src/utils/report/render/DragContainer.tsx create mode 100644 src/utils/report/render/Errors.tsx create mode 100644 src/utils/report/render/Favicon.tsx create mode 100644 src/utils/report/render/Head.tsx rename src/utils/report/render/{renderReportToHtml.tsx => HtmlReport.tsx} (64%) rename src/utils/report/render/{renderJsonData.ts => JsonData.tsx} (81%) create mode 100644 src/utils/report/render/Logo.tsx create mode 100644 src/utils/report/render/Metadata.tsx create mode 100644 src/utils/report/render/Navigation.tsx create mode 100644 src/utils/report/render/Retries.tsx create mode 100644 src/utils/report/render/RetriesButtons.tsx create mode 100644 src/utils/report/render/Retry.tsx create mode 100644 src/utils/report/render/RetryButton.tsx create mode 100644 src/utils/report/render/Script.tsx rename src/utils/report/render/{renderScriptFunctions.ts => ScriptFunctions.tsx} (70%) rename src/utils/report/render/{renderScriptGlobals.ts => ScriptGlobals.tsx} (69%) rename src/utils/report/render/{renderStyle.ts => Style.tsx} (63%) create mode 100644 src/utils/report/render/TestRunButton.tsx delete mode 100644 src/utils/report/render/renderAttributes.ts delete mode 100644 src/utils/report/render/renderErrors.ts delete mode 100644 src/utils/report/render/renderFavicon.ts delete mode 100644 src/utils/report/render/renderHead.ts delete mode 100644 src/utils/report/render/renderLogo.ts delete mode 100644 src/utils/report/render/renderMetadata.ts delete mode 100644 src/utils/report/render/renderNavigation.ts delete mode 100644 src/utils/report/render/renderRetries.ts delete mode 100644 src/utils/report/render/renderRetriesButtons.ts delete mode 100644 src/utils/report/render/renderRetry.ts delete mode 100644 src/utils/report/render/renderRetryButton.ts delete mode 100644 src/utils/report/render/renderScript.ts delete mode 100644 src/utils/report/render/renderTestRunButton.ts rename src/utils/report/{writeHtmlReport.ts => writeHtmlReport.tsx} (91%) diff --git a/autotests/pageObjects/pages/E2edReportExample/E2edReportExample.ts b/autotests/pageObjects/pages/E2edReportExample/E2edReportExample.ts index 7d1a4a0c..1a0929b7 100644 --- a/autotests/pageObjects/pages/E2edReportExample/E2edReportExample.ts +++ b/autotests/pageObjects/pages/E2edReportExample/E2edReportExample.ts @@ -65,7 +65,7 @@ export class E2edReportExample extends Page { * List of test runs of retry. */ get testRunsList(): Selector { - return locator('column1'); + return locator('column-2'); } /** diff --git a/src/types/global.d.ts b/src/types/global.d.ts index a0aec00e..0ce7a781 100644 --- a/src/types/global.d.ts +++ b/src/types/global.d.ts @@ -58,6 +58,10 @@ declare global { class?: string; } >; + } & { + button: {popovertarget?: string}; + input: {popovertarget?: string}; + meta: {charset?: string}; }; /** diff --git a/src/utils/getDurationWithUnits.ts b/src/utils/getDurationWithUnits.ts index 5ac010fb..c6a17d64 100644 --- a/src/utils/getDurationWithUnits.ts +++ b/src/utils/getDurationWithUnits.ts @@ -2,7 +2,7 @@ * Get the duration of time interval in hours, minutes, seconds and milliseconds. * `getDurationWithUnits(1213)` = `'1s 213ms'`. * Should be a pure function without dependencies in the form of a function declaration, - * because it is used in the JS code of HTML report. + * because it is used in the JS client code of HTML report. */ export function getDurationWithUnits(durationInMs: number): string { const msInSecond = 1_000; diff --git a/src/utils/report/client/clickOnRetry.ts b/src/utils/report/client/clickOnRetry.ts index db67b0e1..460284cd 100644 --- a/src/utils/report/client/clickOnRetry.ts +++ b/src/utils/report/client/clickOnRetry.ts @@ -12,18 +12,16 @@ export function clickOnRetry(element: HTMLElement): void { retryElement.hidden = retryElement.id !== chosenRetryId; } - const previousChosenRetryButton = document.querySelector( - '.nav-tabs__button[aria-selected="true"]', - ); + const previousChosenRetryButton = document.querySelector('.retry-link[aria-current="true"]'); if (previousChosenRetryButton) { - previousChosenRetryButton.ariaSelected = 'false'; + previousChosenRetryButton.ariaCurrent = null; } // eslint-disable-next-line no-param-reassign - element.ariaSelected = 'true'; + element.ariaCurrent = 'true'; - const leftSection = document.querySelector('.main__section._position_left'); + const leftSection = document.querySelector('.column-2'); if (leftSection) { leftSection.ariaLabel = `Retry ${retry}`; diff --git a/src/utils/report/client/clickOnTestRun.ts b/src/utils/report/client/clickOnTestRun.ts index 2c1aaa30..ad31883f 100644 --- a/src/utils/report/client/clickOnTestRun.ts +++ b/src/utils/report/client/clickOnTestRun.ts @@ -12,14 +12,14 @@ const chooseTestRun = clientChooseTestRun; export function clickOnTestRun(element: HTMLElement): void { const runHash = (element.dataset as {runhash: RunHash}).runhash; - const previousChosenTestRunButton = document.querySelector('.test-button[aria-selected="true"]'); + const previousChosenTestRunButton = document.querySelector('.test-link[aria-current="true"]'); if (previousChosenTestRunButton) { - previousChosenTestRunButton.ariaSelected = 'false'; + previousChosenTestRunButton.ariaCurrent = null; } // eslint-disable-next-line no-param-reassign - element.ariaSelected = 'true'; + element.ariaCurrent = 'true'; chooseTestRun(runHash); } diff --git a/src/utils/report/client/createJsxRuntime.ts b/src/utils/report/client/createJsxRuntime.ts index deb8519f..1fa0e241 100644 --- a/src/utils/report/client/createJsxRuntime.ts +++ b/src/utils/report/client/createJsxRuntime.ts @@ -33,16 +33,57 @@ export function createJsxRuntime(): JSX.Runtime { ); const childrenHtml = createSafeHtmlWithoutSanitize`${childrenParts.join('')}`; + const isVoidElement = [ + 'area', + 'base', + 'br', + 'col', + 'embed', + 'hr', + 'img', + 'input', + 'link', + 'meta', + 'source', + 'track', + 'wbr', + ].includes(type); + + let closePart = createSafeHtmlWithoutSanitize``; + + if (isVoidElement) { + if (childrenHtml.length > 0) { + // eslint-disable-next-line no-console + console.error(`Element <${type}> is void element, but has children`, childrenHtml); + + closePart = childrenHtml; + } + } else { + closePart = sanitizeHtml`${childrenHtml}`; + } + if (properties == null) { - return sanitizeHtml`<${type}>${childrenHtml}`; + return sanitizeHtml`<${type}>${closePart}`; } - const attributesParts: readonly SafeHtml[] = Object.entries(properties).map( - ([key, value]) => sanitizeHtml`${key.toLowerCase()}="${value}"`, - ); - const attributesHtml = createSafeHtmlWithoutSanitize`${attributesParts.join('')}`; + const attributesParts: readonly SafeHtml[] = Object.entries(properties) + .filter(([key, value]) => { + if (value == null) { + return false; + } + + if (value !== false) { + return true; + } + + const lowerCaseKey = key.toLocaleLowerCase(); + + return lowerCaseKey.startsWith('aria-') || lowerCaseKey.startsWith('data-'); + }) + .map(([key, value]) => sanitizeHtml`${key.toLowerCase()}="${value}"`); + const attributesHtml = createSafeHtmlWithoutSanitize`${attributesParts.join(' ')}`; - return sanitizeHtml`<${type} ${attributesHtml}>${childrenHtml}`; + return sanitizeHtml`<${type} ${attributesHtml}>${closePart}`; }; const Fragment: JSX.Fragment = (properties) => { diff --git a/src/utils/report/client/index.ts b/src/utils/report/client/index.ts index 4bf2dbcb..8164ae6d 100644 --- a/src/utils/report/client/index.ts +++ b/src/utils/report/client/index.ts @@ -32,7 +32,10 @@ export { ApiStatisticsItem, DatesInterval, Duration, + List, MaybeApiStatistics, + SafeHtml, + Screenshot, Step, StepContent, Steps, diff --git a/src/utils/report/client/initialScript.ts b/src/utils/report/client/initialScript.ts index cea5354f..681fc28a 100644 --- a/src/utils/report/client/initialScript.ts +++ b/src/utils/report/client/initialScript.ts @@ -37,9 +37,9 @@ export function initialScript(): void { Object.assign>(reportClientState, {locator}); - addOnClickOnClass('nav-tabs__button', clickOnRetry); + addOnClickOnClass('retry-link', clickOnRetry); addOnClickOnClass('step-expanded', clickOnStep); - addOnClickOnClass('test-button', clickOnTestRun); + addOnClickOnClass('test-link', clickOnTestRun); setReadJsonReportDataObservers(); diff --git a/src/utils/report/client/onFirstJsonReportDataLoad.ts b/src/utils/report/client/onFirstJsonReportDataLoad.ts index 9a11790b..aab98877 100644 --- a/src/utils/report/client/onFirstJsonReportDataLoad.ts +++ b/src/utils/report/client/onFirstJsonReportDataLoad.ts @@ -17,7 +17,7 @@ export function onFirstJsonReportDataLoad(): void { } const buttonForFailedTestRun = document.querySelector( - '.retry:not([hidden]) .test-button_status_failed', + '.retry:not([hidden]) .test-link[data-status="failed"]', ); if (!buttonForFailedTestRun) { diff --git a/src/utils/report/client/render/ApiStatistics.tsx b/src/utils/report/client/render/ApiStatistics.tsx index ea7c61ae..a4bff456 100644 --- a/src/utils/report/client/render/ApiStatistics.tsx +++ b/src/utils/report/client/render/ApiStatistics.tsx @@ -1,6 +1,5 @@ -import {createSafeHtmlWithoutSanitize as clientCreateSafeHtmlWithoutSanitize} from '../sanitizeHtml'; - import {ApiStatisticsItem as clientApiStatisticsItem} from './ApiStatisticsItem'; +import {List as clientList} from './List'; import type { ApiStatistics as ApiStatisticsType, @@ -9,8 +8,8 @@ import type { SafeHtml, } from '../../../../types/internal'; -const createSafeHtmlWithoutSanitize = clientCreateSafeHtmlWithoutSanitize; const ApiStatisticsItem = clientApiStatisticsItem; +const List = clientList; declare const jsx: JSX.Runtime; @@ -92,9 +91,7 @@ export const ApiStatistics: JSX.Component = ({apiStatistics, hash}) => {

{header}

-
-
{createSafeHtmlWithoutSanitize`${items.join('')}`}
-
+
); }; diff --git a/src/utils/report/client/render/Duration.tsx b/src/utils/report/client/render/Duration.tsx index 836d3547..07fba604 100644 --- a/src/utils/report/client/render/Duration.tsx +++ b/src/utils/report/client/render/Duration.tsx @@ -1,9 +1,9 @@ import {getDurationWithUnits as clientGetDurationWithUnits} from '../../../getDurationWithUnits'; -import {createSafeHtmlWithoutSanitize as clientCreateSafeHtmlWithoutSanitize} from '../sanitizeHtml'; +import {SafeHtml as clientSafeHtml} from './SafeHtml'; const getDurationWithUnits = clientGetDurationWithUnits; -const createSafeHtmlWithoutSanitize = clientCreateSafeHtmlWithoutSanitize; +const SafeHtml = clientSafeHtml; declare const jsx: JSX.Runtime; @@ -17,5 +17,5 @@ type Props = Readonly<{durationInMs: number}>; export const Duration: JSX.Component = ({durationInMs}) => { const durationWithUnits = getDurationWithUnits(durationInMs); - return createSafeHtmlWithoutSanitize`${durationWithUnits}`; + return ; }; diff --git a/src/utils/report/client/render/List.tsx b/src/utils/report/client/render/List.tsx new file mode 100644 index 00000000..3415a846 --- /dev/null +++ b/src/utils/report/client/render/List.tsx @@ -0,0 +1,28 @@ +import {assertValueIsDefined as clientAssertValueIsDefined} from '../assertValueIsDefined'; +import {createSafeHtmlWithoutSanitize as clientCreateSafeHtmlWithoutSanitize} from '../sanitizeHtml'; + +import type {SafeHtml} from '../../../../types/internal'; + +const assertValueIsDefined: typeof clientAssertValueIsDefined = clientAssertValueIsDefined; +const createSafeHtmlWithoutSanitize = clientCreateSafeHtmlWithoutSanitize; + +declare const jsx: JSX.Runtime; + +type Props = Readonly<{ + elements?: readonly SafeHtml[]; + separator?: string; + withoutSanitize?: readonly string[]; +}>; + +/** + * Renders (join) list of any rendered elements. + * This base client function should not use scope variables (except other base functions). + * @internal + */ +export const List: JSX.Component = ({elements, separator = '', withoutSanitize}) => { + const strings = elements || withoutSanitize; + + assertValueIsDefined(strings); + + return createSafeHtmlWithoutSanitize`${strings.join(separator)}`; +}; diff --git a/src/utils/report/client/render/SafeHtml.tsx b/src/utils/report/client/render/SafeHtml.tsx new file mode 100644 index 00000000..e6957209 --- /dev/null +++ b/src/utils/report/client/render/SafeHtml.tsx @@ -0,0 +1,15 @@ +import {createSafeHtmlWithoutSanitize as clientCreateSafeHtmlWithoutSanitize} from '../sanitizeHtml'; + +const createSafeHtmlWithoutSanitize = clientCreateSafeHtmlWithoutSanitize; + +declare const jsx: JSX.Runtime; + +type Props = Readonly<{withoutSanitize: string}>; + +/** + * Renders any `SafeHtml` string. + * This base client function should not use scope variables (except other base functions). + * @internal + */ +export const SafeHtml: JSX.Component = ({withoutSanitize}) => + createSafeHtmlWithoutSanitize`${withoutSanitize}`; diff --git a/src/utils/report/client/render/Screenshot.tsx b/src/utils/report/client/render/Screenshot.tsx new file mode 100644 index 00000000..bfb28cd9 --- /dev/null +++ b/src/utils/report/client/render/Screenshot.tsx @@ -0,0 +1,22 @@ +declare const jsx: JSX.Runtime; + +type Props = Readonly<{name: string; open?: boolean; src: string}>; + +/** + * Renders screenshot of test in HTML report. + * This base client function should not use scope variables (except other base functions). + * @internal + */ +export const Screenshot: JSX.Component = ({name, open = false, src}) => ( +
+ {name} + +
+); diff --git a/src/utils/report/client/render/Step.tsx b/src/utils/report/client/render/Step.tsx index d3e43606..98178026 100644 --- a/src/utils/report/client/render/Step.tsx +++ b/src/utils/report/client/render/Step.tsx @@ -23,7 +23,9 @@ type Props = Readonly<{ */ export const Step: JSX.Component = ({logEvent, nextLogEventTime}) => { const {message, payload, time, type} = logEvent; + const date = new Date(time).toISOString(); const isPayloadEmpty = !payload || Object.keys(payload).length === 0; + const popoverId = Math.random().toString(16).slice(2); const status = payload?.logEventStatus ?? LogEventStatus.Passed; let pathToScreenshotOfPage: string | undefined; @@ -39,25 +41,32 @@ export const Step: JSX.Component = ({logEvent, nextLogEventTime}) => { } } - const maybeEmptyClass = isPayloadEmpty ? 'step-expanded_is-empty' : ''; - const isErrorScreenshot = pathToScreenshotOfPage !== undefined; - - return ( - <> - - - + + + + ); + + return ( +
  • + +
    + {date} +
    + {content} +
  • ); }; diff --git a/src/utils/report/client/render/StepContent.tsx b/src/utils/report/client/render/StepContent.tsx index ac6bb2b7..43c4bc8d 100644 --- a/src/utils/report/client/render/StepContent.tsx +++ b/src/utils/report/client/render/StepContent.tsx @@ -1,16 +1,18 @@ import {LogEventType} from '../../../../constants/internal'; -import {createSafeHtmlWithoutSanitize as clientCreateSafeHtmlWithoutSanitize} from '../sanitizeHtml'; +import {List as clientList} from './List'; +import {Screenshot as clientScreenshot} from './Screenshot'; import type {LogPayload, SafeHtml} from '../../../../types/internal'; -const createSafeHtmlWithoutSanitize = clientCreateSafeHtmlWithoutSanitize; +const List = clientList; +const Screenshot = clientScreenshot; declare const jsx: JSX.Runtime; type Props = Readonly<{ pathToScreenshotOfPage: string | undefined; - payload: LogPayload | undefined; + payload: LogPayload; type: LogEventType; }>; @@ -20,46 +22,38 @@ type Props = Readonly<{ * @internal */ export const StepContent: JSX.Component = ({pathToScreenshotOfPage, payload, type}) => { - if (payload === undefined) { - return <>; - } - const payloadString = JSON.stringify(payload, null, 2); - const code = {payloadString}; - const images: SafeHtml[] = []; + const screenshots: SafeHtml[] = []; if (pathToScreenshotOfPage !== undefined) { - images.push( - Screenshot of page, - ); + screenshots.push(); } if (type === LogEventType.InternalAssert) { const {actualScreenshotUrl, diffScreenshotUrl, expectedScreenshotUrl} = payload; if (typeof actualScreenshotUrl === 'string') { - images.push(Actual); + screenshots.push(); } if (typeof diffScreenshotUrl === 'string') { - images.push(Diff); + screenshots.push(); } if (typeof expectedScreenshotUrl === 'string') { - images.push(Expected); + screenshots.push(); } } - const imagesHtml = createSafeHtmlWithoutSanitize`${images.join('')}`; - - if (images.length > 0) { - return ( -
    -
    {code}
    - {imagesHtml} -
    - ); - } - - return
    {code}
    ; + return ( + <> +
    + Details +
    +          {payloadString}
    +        
    +
    + + + ); }; diff --git a/src/utils/report/client/render/Steps.tsx b/src/utils/report/client/render/Steps.tsx index 896f5164..40ba568b 100644 --- a/src/utils/report/client/render/Steps.tsx +++ b/src/utils/report/client/render/Steps.tsx @@ -1,12 +1,12 @@ import {assertValueIsDefined as clientAssertValueIsDefined} from '../assertValueIsDefined'; -import {createSafeHtmlWithoutSanitize as clientCreateSafeHtmlWithoutSanitize} from '../sanitizeHtml'; +import {List as clientList} from './List'; import {Step as clientStep} from './Step'; import type {LogEvent, SafeHtml, UtcTimeInMs} from '../../../../types/internal'; const assertValueIsDefined: typeof clientAssertValueIsDefined = clientAssertValueIsDefined; -const createSafeHtmlWithoutSanitize = clientCreateSafeHtmlWithoutSanitize; +const List = clientList; const Step = clientStep; declare const jsx: JSX.Runtime; @@ -36,5 +36,9 @@ export const Steps: JSX.Component = ({endTimeInMs, logEvents}) => { stepHtmls.push(stepHtml); } - return createSafeHtmlWithoutSanitize`${stepHtmls.join('')}`; + return ( +
      + +
    + ); }; diff --git a/src/utils/report/client/render/TestRunDescription.tsx b/src/utils/report/client/render/TestRunDescription.tsx index af31ea38..3fc09f3c 100644 --- a/src/utils/report/client/render/TestRunDescription.tsx +++ b/src/utils/report/client/render/TestRunDescription.tsx @@ -1,15 +1,15 @@ import {parseMarkdownLinks as clientParseMarkdownLinks} from '../parseMarkdownLinks'; -import {createSafeHtmlWithoutSanitize as clientCreateSafeHtmlWithoutSanitize} from '../sanitizeHtml'; import {DatesInterval as clientDatesInterval} from './DatesInterval'; import {Duration as clientDuration} from './Duration'; +import {List as clientList} from './List'; import type {FullTestRun, ReportClientState, SafeHtml} from '../../../../types/internal'; -const createSafeHtmlWithoutSanitize = clientCreateSafeHtmlWithoutSanitize; const parseMarkdownLinks = clientParseMarkdownLinks; const DatesInterval = clientDatesInterval; const Duration = clientDuration; +const List = clientList; declare const jsx: JSX.Runtime; declare const reportClientState: ReportClientState; @@ -51,7 +51,7 @@ export const TestRunDescription: JSX.Component = ({fullTestRun}) => { <>
    {traceLabel}
    - + {traceName}
    @@ -59,11 +59,9 @@ export const TestRunDescription: JSX.Component = ({fullTestRun}) => { ); } - const metaProperties = createSafeHtmlWithoutSanitize`${metaHtmls.join('')}`; - return ( -
    - {metaProperties} +
    + {traceHtml}
    Date
    diff --git a/src/utils/report/client/render/TestRunDetails.tsx b/src/utils/report/client/render/TestRunDetails.tsx index e584dc6d..5c91b790 100644 --- a/src/utils/report/client/render/TestRunDetails.tsx +++ b/src/utils/report/client/render/TestRunDetails.tsx @@ -35,19 +35,15 @@ export const TestRunDetails: JSX.Component = ({fullTestRun}) => {

    {filePath}

    - + {capitalizedStatus} {name}

    -
    - -
    -

    Execution

    - - -
    -
    + + +

    Execution

    +
    ); }; diff --git a/src/utils/report/client/render/TestRunError.tsx b/src/utils/report/client/render/TestRunError.tsx index b4fe4fcd..4c633f48 100644 --- a/src/utils/report/client/render/TestRunError.tsx +++ b/src/utils/report/client/render/TestRunError.tsx @@ -9,7 +9,7 @@ declare const jsx: JSX.Runtime; type Props = Readonly<{runError: RunError}>; /** - * Renders `TestRun` error as simple message. + * Renders `TestRun` error as a simple message. * This base client function should not use scope variables (except other base functions). * @internal */ @@ -24,10 +24,12 @@ export const TestRunError: JSX.Component = ({runError}) => { const runErrorWithoutStyle = String(runError).replace(stylesRegexp, ''); return ( -
    -
    - {runErrorWithoutStyle} +
    +
    +
    +          {runErrorWithoutStyle}
    +        
    -
    + ); }; diff --git a/src/utils/report/client/render/index.ts b/src/utils/report/client/render/index.ts index e4d53138..b038eff5 100644 --- a/src/utils/report/client/render/index.ts +++ b/src/utils/report/client/render/index.ts @@ -7,8 +7,14 @@ export {DatesInterval} from './DatesInterval'; /** @internal */ export {Duration} from './Duration'; /** @internal */ +export {List} from './List'; +/** @internal */ export {MaybeApiStatistics} from './MaybeApiStatistics'; /** @internal */ +export {SafeHtml} from './SafeHtml'; +/** @internal */ +export {Screenshot} from './Screenshot'; +/** @internal */ export {Step} from './Step'; /** @internal */ export {StepContent} from './StepContent'; diff --git a/src/utils/report/render/DragContainer.tsx b/src/utils/report/render/DragContainer.tsx new file mode 100644 index 00000000..7eb638eb --- /dev/null +++ b/src/utils/report/render/DragContainer.tsx @@ -0,0 +1,18 @@ +declare const jsx: JSX.Runtime; + +/** + * Renders drag container to change columns widths. + * @internal + */ +export const DragContainer: JSX.Component = () => ( + +); diff --git a/src/utils/report/render/Errors.tsx b/src/utils/report/render/Errors.tsx new file mode 100644 index 00000000..3f23c061 --- /dev/null +++ b/src/utils/report/render/Errors.tsx @@ -0,0 +1,23 @@ +import {List} from '../client'; + +declare const jsx: JSX.Runtime; + +type Props = Readonly<{errors: readonly string[]}>; + +/** + * Renders report errors. + * @internal + */ +export const Errors: JSX.Component = ({errors}) => { + if (errors.length === 0) { + return <>; + } + + const renderedErrors = errors.map((error) =>
    {error}
    ); + + return ( +
    + +
    + ); +}; diff --git a/src/utils/report/render/Favicon.tsx b/src/utils/report/render/Favicon.tsx new file mode 100644 index 00000000..74b83a60 --- /dev/null +++ b/src/utils/report/render/Favicon.tsx @@ -0,0 +1,12 @@ +declare const jsx: JSX.Runtime; + +/** + * Renders tag `` with favicon in data-uri format. + * @internal + */ +export const Favicon: JSX.Component = () => { + const faviconInBase64 = + 'data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAACAAAAAgCAYAAABzenr0AAAACXBIWXMAAAsTAAALEwEAmpwYAAAAAXNSR0IArs4c6QAAAARnQU1BAACxjwv8YQUAAAOoSURBVHgBtVfNTxNBFH9v2tIFI9bEBPEjKV70YGI96MGL9WaMIpw8Cge94kHPtPFqAvwFQOLdLRL1YEL9C6gHD5zYqAnChYJatrY7zzezLLTL7naB8ksm3c7X+5zfm0GIiUx+NWOfNkZAiLtIlJOAWQTKqDECrAogixArIOUX47dtVstD1Tj7YqcJxv21LKZwQiKOeQJjAXEO6rJofxq0IqeFDSiLd/pPTSLIF3AsYMFeGCiGjgZ1KquhB5f4MwvdgQX/6F6QN4S/I/VgI9dl4aD34j1TD37m/ANtHjgBy/044Il2D0QJJyhL6YyCoCFD7JwFSfe4cx4OB22gyi+vI+l9GMPrk7xhNngdFe33gwXvn+3+lFVLjWxMC0lLcU8ISlmClpk6BLuuXw1egXN2aWA8bEOXH/qmAGkMomGx18btxcFya6cbgpSYDFvlYGMmbMx4uJa3+3uXOwlnq2c4bDf9wvUYjKxmDNm7GbRQMVx9YeBs0Fh6eGMqBkcEWt2KpOEwvYbQEcf1AJ1qZuzBdwQyBxFQVqeT9UJ1IZqSOQnFXWVrENgDbYnFVk8QUAGiE05bvbN4obwDnZEkpFwYH6vMVnGGJm+awlkAmY/aLIbV+d1mcZvTa9LD65tRR4iVq7RWviCoXEnI5nht8aLpH9MnTCmPkG+uvAHVdlHhdjPZ6fxycNhDFD6OaPZibTzUaiaexvJEFs9ch+TVl+CsfQTa/qZGVA7lBRwR+g7AzFgvDYxWzWDhOnzMftTYBqr9APWbuv4aMNnvTckKtREcEtpqURsKcnkb0KX1ntuzkLzyDKCxBeLcHUhce+XNKPMpIAtcd3QWrJQlKjI3TNdjzE84TqVJNYDad1AhqH++BYkrz7Uy9Gdl1LHeWoLj+xViQuULL3hqPFob08nVAX8/XKpQY6uohGuFLj8B7LusTwsL195DtRnz/SwcBaQKElfEBpSjrl5KBgnxWAvkYsSFbQ72jGIqTsu+1UPd946hjB+ag9LDv6b5YwK6BCIw2aBSnNuxW45HOJ4ypBwfF1zOhdMshZ2YPRbuthf8UCcIEUy/MvtlwC3Ly3By90EPlr1wfsj7s8+Eis2EuufpQnFiwndlwEEFGLY5aDlCjMLJKKH3VjJaO4MfJm5Sdvdhwpb7hSsEFiM10Y0ThT6p4mLvPmgGc0Pnx6n2BhR46lOICfe17MyTwOkwwbEV8JDhU2K798c8C7jBS9ue56qoofc8TzABmfGe5/8BRCSvtlyDCPcAAAAASUVORK5CYII='; + + return ; +}; diff --git a/src/utils/report/render/Head.tsx b/src/utils/report/render/Head.tsx new file mode 100644 index 00000000..d5485c14 --- /dev/null +++ b/src/utils/report/render/Head.tsx @@ -0,0 +1,48 @@ +import {List} from '../client'; +import {getContentFromRenderedElement} from '../getContentFromRenderedElement'; +import {getCspHash} from '../getCspHash'; + +import {Favicon} from './Favicon'; +import {Script} from './Script'; +import {Style} from './Style'; + +declare const jsx: JSX.Runtime; + +type Props = Readonly<{imgCspHosts: string; reportFileName: string}>; + +/** + * Renders tag ``. + * @internal + */ +export const Head: JSX.Component = ({imgCspHosts, reportFileName}) => { + const renderedScript = `; + return ( + + ); }); - return createSafeHtmlWithoutSanitize`${scripts.join('')}`; + return ; }; diff --git a/src/utils/report/render/Logo.tsx b/src/utils/report/render/Logo.tsx new file mode 100644 index 00000000..ab1ba3b8 --- /dev/null +++ b/src/utils/report/render/Logo.tsx @@ -0,0 +1,31 @@ +import {readFileSync} from 'node:fs'; +import {join} from 'node:path'; + +import {INSTALLED_E2ED_DIRECTORY_PATH, READ_FILE_OPTIONS} from '../../../constants/internal'; + +import {SafeHtml} from '../client'; + +declare const jsx: JSX.Runtime; + +/** + * Renders SVG logo for report page. + * @internal + */ +export const Logo: JSX.Component = () => { + const pathToLogo = join(INSTALLED_E2ED_DIRECTORY_PATH, 'logo.svg'); + + const logoString = readFileSync(pathToLogo, READ_FILE_OPTIONS); + + return ( + + ); +}; diff --git a/src/utils/report/render/Metadata.tsx b/src/utils/report/render/Metadata.tsx new file mode 100644 index 00000000..39d73201 --- /dev/null +++ b/src/utils/report/render/Metadata.tsx @@ -0,0 +1,28 @@ +import type {ApiStatisticsReportHash} from '../../../types/internal'; + +declare const jsx: JSX.Runtime; + +/** + * Renders metadata of whole `e2ed` run. + * @internal + */ +export const Metadata: JSX.Component = () => { + const pagesHash: ApiStatisticsReportHash = 'api-statistics-pages'; + const requestsHash: ApiStatisticsReportHash = 'api-statistics-requests'; + const resourcesHash: ApiStatisticsReportHash = 'api-statistics-resources'; + + return ( + + ); +}; diff --git a/src/utils/report/render/Navigation.tsx b/src/utils/report/render/Navigation.tsx new file mode 100644 index 00000000..515ee3c1 --- /dev/null +++ b/src/utils/report/render/Navigation.tsx @@ -0,0 +1,24 @@ +import {locator} from './locator'; +import {Logo} from './Logo'; +import {RetriesButtons} from './RetriesButtons'; + +import type {RetryProps} from '../../../types/internal'; + +declare const jsx: JSX.Runtime; + +type Props = Readonly<{retries: readonly RetryProps[]}>; + +/** + * Renders tag `