diff --git a/autotests/actions/setPageCookiesAndNavigateToUrl.ts b/autotests/actions/setPageCookiesAndNavigateToUrl.ts index 151f56b4..b937ffa5 100644 --- a/autotests/actions/setPageCookiesAndNavigateToUrl.ts +++ b/autotests/actions/setPageCookiesAndNavigateToUrl.ts @@ -4,12 +4,17 @@ import {getHeaderValue, log, replaceSetCookie} from 'e2ed/utils'; import type {Cookie, NavigationReturn, SetCookieHeaderString, StringHeaders, Url} from 'e2ed/types'; +type Options = Readonly<{ + pageCookies: readonly Cookie[]; + timeout?: number; +}>; + /** * Navigates to the url and set custom page cookies. */ export const setPageCookiesAndNavigateToUrl = ( url: Url, - pageCookies: readonly Cookie[], + {pageCookies, timeout}: Options, ): Promise => { const mapResponseHeaders = (headers: StringHeaders): StringHeaders => { const setCookies = getHeaderValue(headers, 'set-cookie'); @@ -28,5 +33,9 @@ export const setPageCookiesAndNavigateToUrl = ( log(`Navigate to ${url} and set page cookie`, {pageCookies, url}, LogEventType.Action); - return setHeadersAndNavigateToUrl(url, {mapResponseHeaders}); + return setHeadersAndNavigateToUrl( + url, + {mapResponseHeaders}, + timeout !== undefined ? {timeout} : undefined, + ); }; diff --git a/autotests/actions/setPageRequestHeadersAndNavigateToUrl.ts b/autotests/actions/setPageRequestHeadersAndNavigateToUrl.ts index fb1b6df4..dce2ec77 100644 --- a/autotests/actions/setPageRequestHeadersAndNavigateToUrl.ts +++ b/autotests/actions/setPageRequestHeadersAndNavigateToUrl.ts @@ -4,12 +4,17 @@ import {log} from 'e2ed/utils'; import type {NavigationReturn, StringHeaders, Url} from 'e2ed/types'; +type Options = Readonly<{ + pageRequestHeaders: StringHeaders; + timeout?: number; +}>; + /** * Navigates to the url and set additional page request headers. */ export const setPageRequestHeadersAndNavigateToUrl = ( url: Url, - pageRequestHeaders: StringHeaders, + {pageRequestHeaders, timeout}: Options, ): Promise => { const mapRequestHeaders = (): StringHeaders => pageRequestHeaders; @@ -19,5 +24,9 @@ export const setPageRequestHeadersAndNavigateToUrl = ( LogEventType.Action, ); - return setHeadersAndNavigateToUrl(url, {mapRequestHeaders}); + return setHeadersAndNavigateToUrl( + url, + {mapRequestHeaders}, + timeout !== undefined ? {timeout} : undefined, + ); }; diff --git a/autotests/configurator/regroupSteps.ts b/autotests/configurator/regroupSteps.ts index 42f22c2b..f06f06b8 100644 --- a/autotests/configurator/regroupSteps.ts +++ b/autotests/configurator/regroupSteps.ts @@ -5,8 +5,6 @@ import type {LogEvent, Mutable} from 'e2ed/types'; /** * Regroup log events (for grouping of `TestRun` steps). - * This base client function should not use scope variables (except other base functions). - * @internal */ export const regroupSteps = (logEvents: readonly LogEvent[]): readonly LogEvent[] => { const topLevelTypes: readonly LogEventType[] = [ diff --git a/autotests/pageObjects/pages/E2edReportExample/E2edReportExample.ts b/autotests/pageObjects/pages/E2edReportExample/E2edReportExample.ts index 8b47fd87..49a0376c 100644 --- a/autotests/pageObjects/pages/E2edReportExample/E2edReportExample.ts +++ b/autotests/pageObjects/pages/E2edReportExample/E2edReportExample.ts @@ -122,10 +122,16 @@ export class E2edReportExample extends Page { override navigateToPage(url: Url): Promise { if (this.pageRequestHeaders) { - return setPageRequestHeadersAndNavigateToUrl(url, this.pageRequestHeaders); + return setPageRequestHeadersAndNavigateToUrl(url, { + pageRequestHeaders: this.pageRequestHeaders, + timeout: E2edReportExample.navigationTimeout, + }); } - return setPageCookiesAndNavigateToUrl(url, this.pageCookies); + return setPageCookiesAndNavigateToUrl(url, { + pageCookies: this.pageCookies, + timeout: E2edReportExample.navigationTimeout, + }); } override async waitForPageLoaded(): Promise { diff --git a/autotests/tests/main/exists.ts b/autotests/tests/main/exists.ts index 98769bc9..5ed18782 100644 --- a/autotests/tests/main/exists.ts +++ b/autotests/tests/main/exists.ts @@ -20,7 +20,7 @@ import {assertFunctionThrows, getDocumentUrl} from 'e2ed/utils'; import type {Url} from 'e2ed/types'; // eslint-disable-next-line max-statements -test('exists', {meta: {testId: '1'}, testIdleTimeout: 10_000, testTimeout: 20_000}, async () => { +test('exists', {meta: {testId: '1'}, testIdleTimeout: 12_000, testTimeout: 20_000}, async () => { const language = 'en'; const searchQuery = 'foo'; const testScrollValue = 200; diff --git a/src/Page.ts b/src/Page.ts index 45f57be6..106f8450 100644 --- a/src/Page.ts +++ b/src/Page.ts @@ -167,6 +167,7 @@ export abstract class Page { await waitForAllRequestsComplete(() => true, { maxIntervalBetweenRequestsInMs: this.maxIntervalBetweenRequestsInMs, + timeout: (this.constructor as typeof Page).navigationTimeout, }); } diff --git a/src/README.md b/src/README.md index d1713140..9a2826ff 100644 --- a/src/README.md +++ b/src/README.md @@ -15,43 +15,44 @@ Modules in the dependency graph should only import the modules above them: 8. `utils/getHash` 9. `generators` 10. `utils/headers` -11. `utils/viewport` -12. `utils/parse` -13. `utils/distanceBetweenSelectors` -14. `utils/getDurationWithUnits` -15. `utils/valueToString` -16. `utils/error` -17. `utils/asserts` -18. `utils/object` -19. `utils/uiMode` -20. `utils/runLabel` -21. `utils/clone` -22. `utils/notIncludedInPackTests` -23. `utils/userland` -24. `utils/fn` -25. `utils/environment` -26. `utils/packCompiler` -27. `config` -28. `utils/config` -29. `utils/generalLog` -30. `utils/testFilePaths` -31. `utils/exit` -32. `utils/promise` -33. `utils/resourceUsage` -34. `utils/fs` -35. `utils/getGlobalErrorHandler` -36. `utils/tests` -37. `utils/end` -38. `utils/pack` -39. `useContext` -40. `context` -41. `utils/step` -42. `utils/apiStatistics` -43. `utils/selectors` -44. `selectors` -45. `utils/log` -46. `step` -47. `utils/waitForEvents` -48. `utils/expect` -49. `expect` -50. ... +11. `utils/screenshot` +12. `utils/viewport` +13. `utils/parse` +14. `utils/distanceBetweenSelectors` +15. `utils/getDurationWithUnits` +16. `utils/valueToString` +17. `utils/error` +18. `utils/asserts` +19. `utils/object` +20. `utils/uiMode` +21. `utils/runLabel` +22. `utils/clone` +23. `utils/notIncludedInPackTests` +24. `utils/userland` +25. `utils/fn` +26. `utils/environment` +27. `utils/packCompiler` +28. `config` +29. `utils/config` +30. `utils/generalLog` +31. `utils/testFilePaths` +32. `utils/exit` +33. `utils/promise` +34. `utils/resourceUsage` +35. `utils/fs` +36. `utils/getGlobalErrorHandler` +37. `utils/tests` +38. `utils/end` +39. `utils/pack` +40. `useContext` +41. `context` +42. `utils/step` +43. `utils/apiStatistics` +44. `utils/selectors` +45. `selectors` +46. `utils/log` +47. `step` +48. `utils/waitForEvents` +49. `utils/expect` +50. `expect` +51. ... diff --git a/src/actions/pages/reloadPage.ts b/src/actions/pages/reloadPage.ts index 214bffb9..96ff5674 100644 --- a/src/actions/pages/reloadPage.ts +++ b/src/actions/pages/reloadPage.ts @@ -1,4 +1,4 @@ -import {LogEventType} from '../../constants/internal'; +import {ADDITIONAL_STEP_TIMEOUT, LogEventType} from '../../constants/internal'; import {step} from '../../step'; import type {AnyPageClassType} from '../../types/internal'; @@ -18,5 +18,8 @@ export const reloadPage = (page: InstanceType): Promise await page.afterReloadPage?.(); }, - {type: LogEventType.InternalAction}, + { + timeout: (page.constructor as AnyPageClassType).navigationTimeout + ADDITIONAL_STEP_TIMEOUT, + type: LogEventType.InternalAction, + }, ); diff --git a/src/actions/takeElementScreenshot.ts b/src/actions/takeElementScreenshot.ts index eeec969b..8659911d 100644 --- a/src/actions/takeElementScreenshot.ts +++ b/src/actions/takeElementScreenshot.ts @@ -1,7 +1,12 @@ import {join} from 'node:path'; -import {LogEventType, SCREENSHOTS_DIRECTORY_PATH} from '../constants/internal'; +import { + ADDITIONAL_STEP_TIMEOUT, + LogEventType, + SCREENSHOTS_DIRECTORY_PATH, +} from '../constants/internal'; import {step} from '../step'; +import {getDimensionsString, getPngDimensions} from '../utils/screenshot'; import type {Locator} from '@playwright/test'; @@ -17,6 +22,7 @@ export const takeElementScreenshot = async ( options: Options = {}, ): Promise => { const {path: pathToScreenshot, ...optionsWithoutPath} = options; + const {timeout} = options; await step( 'Take a screenshot of the element', @@ -26,10 +32,14 @@ export const takeElementScreenshot = async ( options.path = join(SCREENSHOTS_DIRECTORY_PATH, pathToScreenshot); } - await selector.getPlaywrightLocator().screenshot(options); + const screenshot = await selector.getPlaywrightLocator().screenshot(options); + const dimensions = getDimensionsString(getPngDimensions(screenshot)); + + return {dimensions}; }, { payload: {pathToScreenshot, ...optionsWithoutPath, selector}, + ...(timeout !== undefined ? {timeout: timeout + ADDITIONAL_STEP_TIMEOUT} : undefined), type: LogEventType.InternalAction, }, ); diff --git a/src/actions/takeScreenshot.ts b/src/actions/takeScreenshot.ts index f39dac35..a2f3a7e6 100644 --- a/src/actions/takeScreenshot.ts +++ b/src/actions/takeScreenshot.ts @@ -1,8 +1,13 @@ import {join} from 'node:path'; -import {LogEventType, SCREENSHOTS_DIRECTORY_PATH} from '../constants/internal'; +import { + ADDITIONAL_STEP_TIMEOUT, + LogEventType, + SCREENSHOTS_DIRECTORY_PATH, +} from '../constants/internal'; import {step} from '../step'; import {getPlaywrightPage} from '../useContext'; +import {getDimensionsString, getPngDimensions} from '../utils/screenshot'; import type {Page} from '@playwright/test'; @@ -13,6 +18,7 @@ type Options = Parameters[0]; */ export const takeScreenshot = async (options: Options = {}): Promise => { const {path: pathToScreenshot, ...optionsWithoutPath} = options; + const {timeout} = options; await step( 'Take a screenshot of the page', @@ -24,8 +30,15 @@ export const takeScreenshot = async (options: Options = {}): Promise => { const page = getPlaywrightPage(); - await page.screenshot(options); + const screenshot = await page.screenshot(options); + const dimensions = getDimensionsString(getPngDimensions(screenshot)); + + return {dimensions}; + }, + { + payload: {pathToScreenshot, ...optionsWithoutPath}, + ...(timeout !== undefined ? {timeout: timeout + ADDITIONAL_STEP_TIMEOUT} : undefined), + type: LogEventType.InternalAction, }, - {payload: {pathToScreenshot, ...optionsWithoutPath}, type: LogEventType.InternalAction}, ); }; diff --git a/src/types/index.ts b/src/types/index.ts index cb40bca4..6ad7bf2e 100644 --- a/src/types/index.ts +++ b/src/types/index.ts @@ -47,6 +47,8 @@ export type { export type {KeyboardPressKey} from './keyboard'; export type {Log, LogContext, LogParams, LogPayload, LogTag} from './log'; export type { + Dimensions, + DimensionsString, MatchScreenshotConfig, ScreenshotMeta, ToMatchScreenshotOptions, diff --git a/src/types/internal.ts b/src/types/internal.ts index aef8191c..f8a8e1fd 100644 --- a/src/types/internal.ts +++ b/src/types/internal.ts @@ -78,10 +78,14 @@ export type { Payload, } from './log'; export type { + Dimensions, + DimensionsString, MatchScreenshotConfig, ScreenshotMeta, ToMatchScreenshotOptions, } from './matchScreenshot'; +/** @internal */ +export type {ScreenshotLogFields} from './matchScreenshot'; export type {ApiMockFunction} from './mockApiRoute'; /** @internal */ export type {ApiMockState} from './mockApiRoute'; diff --git a/src/types/matchScreenshot.ts b/src/types/matchScreenshot.ts index f66a5fc6..28d9d3fe 100644 --- a/src/types/matchScreenshot.ts +++ b/src/types/matchScreenshot.ts @@ -1,9 +1,23 @@ +import type {Brand} from './brand'; import type {Url} from './http'; import type {RunLabel} from './runLabel'; import type {Selector} from './selectors'; import type {TestStaticOptions} from './testRun'; import type {TestMetaPlaceholder} from './userland'; +/** + * Dimensions of screenshot image. + */ +export type Dimensions = Readonly<{ + height: number; + width: number; +}>; + +/** + * String with dimensions of screenshot, like `320x108`. + */ +export type DimensionsString = Brand; + /** * Functions that describe the `toMatchScreenshot` assert (in `expect`). */ @@ -32,6 +46,16 @@ export type MatchScreenshotConfig = Readonly<{ ) => Promise; }>; +/** + * Log fields for single screenshot. + * @internal + */ +export type ScreenshotLogFields = { + dimensions: DimensionsString; + readonly screenshotId: string; + url: Url; +}; + /** * General screenshot metadata (like test name, assert description, etc.). */ diff --git a/src/utils/expect/getEmptyAdditionalLogFields.ts b/src/utils/expect/getEmptyAdditionalLogFields.ts new file mode 100644 index 00000000..f96e43fe --- /dev/null +++ b/src/utils/expect/getEmptyAdditionalLogFields.ts @@ -0,0 +1,17 @@ +import type {AdditionalLogFields} from './types'; + +type Options = Readonly<{ + expectedScreenshotId: string; +}>; + +/** + * Get empty additional log fields object for `toMatchScreenshot` assertion. + * @internal + */ +export const getEmptyAdditionalLogFields = ({ + expectedScreenshotId, +}: Options): AdditionalLogFields => ({ + actual: undefined, + diff: undefined, + expected: {dimensions: undefined, screenshotId: expectedScreenshotId, url: undefined}, +}); diff --git a/src/utils/expect/toMatchScreenshot.ts b/src/utils/expect/toMatchScreenshot.ts index 106e828a..126a3efa 100644 --- a/src/utils/expect/toMatchScreenshot.ts +++ b/src/utils/expect/toMatchScreenshot.ts @@ -1,5 +1,4 @@ import {randomUUID} from 'node:crypto'; -import {readFile} from 'node:fs/promises'; import {join} from 'node:path'; import {isLocalRun} from '../../configurator'; @@ -14,24 +13,18 @@ import {getFullPackConfig} from '../config'; import {E2edError} from '../error'; import {writeFile} from '../fs'; import {setReadonlyProperty} from '../object'; +import {getDimensionsString, getPngDimensions} from '../screenshot'; +import {getEmptyAdditionalLogFields} from './getEmptyAdditionalLogFields'; import {getScreenshotMeta} from './getScreenshotMeta'; +import {writeScreenshotFromPath} from './writeScreenshotFromPath'; -import type {FilePathFromRoot, Selector, ToMatchScreenshotOptions, Url} from '../../types/internal'; +import type {FilePathFromRoot, Selector, ToMatchScreenshotOptions} from '../../types/internal'; import type {Expect} from './Expect'; import {expect as playwrightExpect} from '@playwright/test'; -type AdditionalLogFields = { - actualScreenshotId: string | undefined; - actualScreenshotUrl: Url | undefined; - diffScreenshotId: string | undefined; - diffScreenshotUrl: Url | undefined; - expectedScreenshotId: string; - expectedScreenshotUrl: Url | undefined; -}; - /** * Checks that the selector screenshot matches the one specified by `expectedScreenshotId`. * @internal @@ -44,8 +37,7 @@ export const toMatchScreenshot = async ( ): Promise => { const actualValue = context.actualValue as Selector; const {description} = context; - const {getScreenshotUrlById, readScreenshot, writeScreenshot} = - getFullPackConfig().matchScreenshot; + const {getScreenshotUrlById, readScreenshot} = getFullPackConfig().matchScreenshot; const assertId = randomUUID(); const screenshotFileName = `${assertId}.png`; @@ -54,14 +46,7 @@ export const toMatchScreenshot = async ( screenshotFileName, ) as FilePathFromRoot; - const additionalLogFields: AdditionalLogFields = { - actualScreenshotId: undefined, - actualScreenshotUrl: undefined, - diffScreenshotId: undefined, - diffScreenshotUrl: undefined, - expectedScreenshotId, - expectedScreenshotUrl: undefined, - }; + const additionalLogFields = getEmptyAdditionalLogFields({expectedScreenshotId}); setReadonlyProperty(context, 'additionalLogFields', additionalLogFields); @@ -70,13 +55,17 @@ export const toMatchScreenshot = async ( let expectedScreenshotFound = false; if (expectedScreenshotId) { - additionalLogFields.expectedScreenshotUrl = getScreenshotUrlById(expectedScreenshotId); + additionalLogFields.expected.url = getScreenshotUrlById(expectedScreenshotId); const expectedScreenshot = await readScreenshot(expectedScreenshotId, meta); if (expectedScreenshot !== undefined) { expectedScreenshotFound = true; + additionalLogFields.expected.dimensions = getDimensionsString( + getPngDimensions(expectedScreenshot), + ); + if (!isLocalRun) { await writeFile(screenshotPath, expectedScreenshot); } @@ -112,20 +101,19 @@ export const toMatchScreenshot = async ( const actualScreenshotPath = join(output, `${assertId}-actual.png`) as FilePathFromRoot; const diffScreenshotPath = join(output, `${assertId}-diff.png`) as FilePathFromRoot; - const actualScreenshot = await readFile(actualScreenshotPath); - const actualScreenshotId = await writeScreenshot(actualScreenshot, meta); - - additionalLogFields.actualScreenshotId = actualScreenshotId; - additionalLogFields.actualScreenshotUrl = getScreenshotUrlById(actualScreenshotId); - - const diffScreenshot = await readFile(diffScreenshotPath); - const diffScreenshotId = await writeScreenshot(diffScreenshot, { - ...meta, - actual: actualScreenshotId, + const actualScreenshotId = await writeScreenshotFromPath({ + additionalLogFields, + meta, + path: actualScreenshotPath, + type: 'actual', }); - additionalLogFields.diffScreenshotId = diffScreenshotId; - additionalLogFields.diffScreenshotUrl = getScreenshotUrlById(diffScreenshotId); + await writeScreenshotFromPath({ + additionalLogFields, + meta: {...meta, actual: actualScreenshotId}, + path: diffScreenshotPath, + type: 'diff', + }); } catch (secondError) { throw new E2edError(errorMessage, {secondError}); } @@ -138,11 +126,12 @@ export const toMatchScreenshot = async ( } try { - const actualScreenshot = await readFile(screenshotPath); - const actualScreenshotId = await writeScreenshot(actualScreenshot, meta); - - additionalLogFields.actualScreenshotId = actualScreenshotId; - additionalLogFields.actualScreenshotUrl = getScreenshotUrlById(actualScreenshotId); + await writeScreenshotFromPath({ + additionalLogFields, + meta, + path: screenshotPath, + type: 'actual', + }); } catch (secondError) { throw new E2edError(message, {secondError}); } diff --git a/src/utils/expect/types.ts b/src/utils/expect/types.ts index abb13d43..7c9dda52 100644 --- a/src/utils/expect/types.ts +++ b/src/utils/expect/types.ts @@ -1,6 +1,13 @@ import type {Expect as PlaywrightExpect} from '@playwright/test'; -import type {Fn, ToBeInViewportOptions, ToMatchScreenshotOptions} from '../../types/internal'; +import type { + DimensionsString, + Fn, + ScreenshotLogFields, + ToBeInViewportOptions, + ToMatchScreenshotOptions, + Url, +} from '../../types/internal'; import type {Expect} from './Expect'; @@ -12,6 +19,22 @@ type Extend = Type extends Extended ? Extended : never; type PlaywrightMatchers = ReturnType; +/** + * Additional log fields for `toMatchScreenshot` assertion. + * @internal + */ +export type AdditionalLogFields = { + actual: ScreenshotLogFields | undefined; + diff: ScreenshotLogFields | undefined; + expected: + | ScreenshotLogFields + | { + dimensions: DimensionsString | undefined; + readonly screenshotId: string; + url: Url | undefined; + }; +}; + /** * All assertion functions keys (names of assertion functions, like `eql`, `match`, etc). * @internal diff --git a/src/utils/expect/writeScreenshotFromPath.ts b/src/utils/expect/writeScreenshotFromPath.ts new file mode 100644 index 00000000..2029a5bb --- /dev/null +++ b/src/utils/expect/writeScreenshotFromPath.ts @@ -0,0 +1,39 @@ +import {readFile} from 'node:fs/promises'; + +import {getFullPackConfig} from '../config'; +import {getDimensionsString, getPngDimensions} from '../screenshot'; + +import type {FilePathFromRoot, ScreenshotMeta} from '../../types/internal'; + +import type {AdditionalLogFields} from './types'; + +type Options = Readonly<{ + additionalLogFields: AdditionalLogFields; + meta: ScreenshotMeta; + path: FilePathFromRoot; + type: Exclude; +}>; + +/** + * Reads screenshot from path and writes to storage. + * @internal + */ +export const writeScreenshotFromPath = async ({ + additionalLogFields, + meta, + path, + type, +}: Options): Promise => { + const {getScreenshotUrlById, writeScreenshot} = getFullPackConfig().matchScreenshot; + + const screenshot = await readFile(path); + const screenshotId = await writeScreenshot(screenshot, meta); + + const dimensions = getDimensionsString(getPngDimensions(screenshot)); + const url = getScreenshotUrlById(screenshotId); + + // eslint-disable-next-line no-param-reassign + additionalLogFields[type] = {dimensions, screenshotId, url}; + + return screenshotId; +}; diff --git a/src/utils/index.ts b/src/utils/index.ts index d5abcf71..a1ef4d45 100644 --- a/src/utils/index.ts +++ b/src/utils/index.ts @@ -50,6 +50,7 @@ export { } from './promise'; export {request} from './request'; export {getRunLabelObject} from './runLabel'; +export {getDimensionsString, getPngDimensions} from './screenshot'; export {getPackageInfo} from './startInfo'; export {isArray, isThenable} from './typeGuards'; export {isUiMode} from './uiMode'; diff --git a/src/utils/report/client/index.ts b/src/utils/report/client/index.ts index 32f7b763..cd1a4bb6 100644 --- a/src/utils/report/client/index.ts +++ b/src/utils/report/client/index.ts @@ -19,6 +19,8 @@ export {createJsxRuntime} from './createJsxRuntime'; /** @internal */ export {initialScript} from './initialScript'; /** @internal */ +export {isScreenshotLog} from './isScreenshotLog'; +/** @internal */ export {onDomContentLoad} from './onDomContentLoad'; /** @internal */ export {onFirstJsonReportDataLoad} from './onFirstJsonReportDataLoad'; diff --git a/src/utils/report/client/isScreenshotLog.ts b/src/utils/report/client/isScreenshotLog.ts new file mode 100644 index 00000000..368542f6 --- /dev/null +++ b/src/utils/report/client/isScreenshotLog.ts @@ -0,0 +1,18 @@ +import type {ScreenshotLogFields} from '../../../types/internal'; + +type ScreenshotLog = Pick & + Partial>; + +/** + * Returns `true`, if value is screenshot log. + * @internal + */ +export function isScreenshotLog(value: unknown): value is ScreenshotLog { + return ( + value instanceof Object && + 'screenshotId' in value && + typeof value.screenshotId === 'string' && + 'url' in value && + typeof value.url === 'string' + ); +} diff --git a/src/utils/report/client/render/Screenshot.tsx b/src/utils/report/client/render/Screenshot.tsx index bd8eb286..c0fced7a 100644 --- a/src/utils/report/client/render/Screenshot.tsx +++ b/src/utils/report/client/render/Screenshot.tsx @@ -1,6 +1,9 @@ +import type {DimensionsString} from '../../../../types/internal'; + declare const jsx: JSX.Runtime; type Props = Readonly<{ + dimensions?: DimensionsString | undefined; name: string; open?: boolean; url: string; @@ -11,16 +14,21 @@ type Props = Readonly<{ * This base client function should not use scope variables (except other base functions). * @internal */ -export const Screenshot: JSX.Component = ({name, open = false, url}) => ( -
- {name} - -
-); +export const Screenshot: JSX.Component = ({dimensions, name, open = false, url}) => { + const withDimensions = dimensions ? ` (${dimensions})` : ''; + const nameWithDimensions = name + withDimensions; + + return ( +
+ {nameWithDimensions} + +
+ ); +}; diff --git a/src/utils/report/client/render/StepContent.tsx b/src/utils/report/client/render/StepContent.tsx index 74e0d6da..81c48a77 100644 --- a/src/utils/report/client/render/StepContent.tsx +++ b/src/utils/report/client/render/StepContent.tsx @@ -1,10 +1,13 @@ import {LogEventType} from '../../../../constants/internal'; +import {isScreenshotLog as clientIsScreenshotLog} from '../isScreenshotLog'; + import {List as clientList} from './List'; import {Screenshot as clientScreenshot} from './Screenshot'; -import type {LogPayload, SafeHtml} from '../../../../types/internal'; +import type {DimensionsString, LogPayload, SafeHtml} from '../../../../types/internal'; +const isScreenshotLog = clientIsScreenshotLog; const List = clientList; const Screenshot = clientScreenshot; @@ -30,22 +33,35 @@ export const StepContent: JSX.Component = ({pathToScreenshotOfPage, paylo const screenshots: SafeHtml[] = []; if (pathToScreenshotOfPage !== undefined) { - screenshots.push(); + screenshots.push( + , + ); } if (type === LogEventType.InternalAssert) { - const {actualScreenshotUrl, diffScreenshotUrl, expectedScreenshotUrl} = payload; + const {actual, diff, expected} = payload; - if (typeof actualScreenshotUrl === 'string') { - screenshots.push(); + if (isScreenshotLog(actual)) { + screenshots.push( + , + ); } - if (typeof diffScreenshotUrl === 'string') { - screenshots.push(); + if (isScreenshotLog(diff)) { + screenshots.push(); } - if (typeof expectedScreenshotUrl === 'string') { - screenshots.push(); + if (isScreenshotLog(expected)) { + screenshots.push( + , + ); } } diff --git a/src/utils/report/getImgCspHosts.ts b/src/utils/report/getImgCspHosts.ts index 1201efb9..33d70f64 100644 --- a/src/utils/report/getImgCspHosts.ts +++ b/src/utils/report/getImgCspHosts.ts @@ -1,9 +1,13 @@ +/* eslint-disable max-depth */ + import {URL} from 'node:url'; import {LogEventType} from '../../constants/internal'; import {flatLogEvents} from '../flatLogEvents'; +import {isScreenshotLog} from './client'; + import type {ReportData} from '../../types/internal'; /** @@ -27,16 +31,23 @@ export const getImgCspHosts = (reportData: ReportData): string => { for (const {fullTestRuns} of retries) { for (const {logEvents} of fullTestRuns) { for (const {payload, type} of flatLogEvents(logEvents)) { - // eslint-disable-next-line max-depth if (type !== LogEventType.InternalAssert || payload === undefined) { continue; } - const {actualScreenshotUrl, diffScreenshotUrl, expectedScreenshotUrl} = payload; + const {actual, diff, expected} = payload; + + if (isScreenshotLog(actual)) { + processMaybeUrl(actual.url); + } - processMaybeUrl(actualScreenshotUrl); - processMaybeUrl(diffScreenshotUrl); - processMaybeUrl(expectedScreenshotUrl); + if (isScreenshotLog(diff)) { + processMaybeUrl(diff.url); + } + + if (isScreenshotLog(expected)) { + processMaybeUrl(expected.url); + } } } } diff --git a/src/utils/screenshot/getDimensionsString.ts b/src/utils/screenshot/getDimensionsString.ts new file mode 100644 index 00000000..fba61539 --- /dev/null +++ b/src/utils/screenshot/getDimensionsString.ts @@ -0,0 +1,7 @@ +import type {Dimensions, DimensionsString} from '../../types/internal'; + +/** + * Get dimensions string (`200x100`) by dimensions object. + */ +export const getDimensionsString = (dimensions: Dimensions): DimensionsString => + `${dimensions.width}x${dimensions.height}` as DimensionsString; diff --git a/src/utils/screenshot/getPngDimensions.ts b/src/utils/screenshot/getPngDimensions.ts new file mode 100644 index 00000000..673f5772 --- /dev/null +++ b/src/utils/screenshot/getPngDimensions.ts @@ -0,0 +1,45 @@ +import type {Dimensions} from '../../types/internal'; + +const decoder = new TextDecoder(); + +const dimensionsStartPositionIfFried = 32; +const dimensionsStartRegularPosition = 16; + +const getView = (screenshot: Uint8Array, offset: number): DataView => + new DataView(screenshot.buffer, screenshot.byteOffset + offset); + +const getUtf8String = (screenshot: Uint8Array, start = 0, end = screenshot.length): string => + decoder.decode(screenshot.slice(start, end)); + +const heightPositionOffset = 4; + +const readUint32 = (screenshot: Uint8Array, offset = 0): number => + getView(screenshot, offset).getUint32(0, false); + +const pngFriedChunkName = 'CgBI'; + +const pngFriedStartPosition = 12; + +/** + * Get dimensions (height and width) of PNG image (by `Uint8Array` buffer with image). + */ +export const getPngDimensions = (screenshot: Uint8Array): Dimensions => { + const isFried = + getUtf8String( + screenshot, + pngFriedStartPosition, + pngFriedStartPosition + pngFriedChunkName.length, + ) === pngFriedChunkName; + + if (isFried) { + return { + height: readUint32(screenshot, dimensionsStartPositionIfFried + heightPositionOffset), + width: readUint32(screenshot, dimensionsStartPositionIfFried), + }; + } + + return { + height: readUint32(screenshot, dimensionsStartRegularPosition + heightPositionOffset), + width: readUint32(screenshot, dimensionsStartRegularPosition), + }; +}; diff --git a/src/utils/screenshot/index.ts b/src/utils/screenshot/index.ts new file mode 100644 index 00000000..edbcb5b1 --- /dev/null +++ b/src/utils/screenshot/index.ts @@ -0,0 +1,2 @@ +export {getDimensionsString} from './getDimensionsString'; +export {getPngDimensions} from './getPngDimensions'; diff --git a/src/utils/step/processStepError.ts b/src/utils/step/processStepError.ts index b4fd45ee..af7fa2ef 100644 --- a/src/utils/step/processStepError.ts +++ b/src/utils/step/processStepError.ts @@ -17,13 +17,14 @@ type Options = Readonly<{ */ // eslint-disable-next-line complexity export const processStepError = ({error, errorProperties, logEvent}: Options): unknown => { + const message = `Caught an error in step "${errorProperties.stepName}"`; let stepError: unknown = error; if ( !(stepError instanceof E2edError) && Object.getOwnPropertySymbols(stepError ?? {}).length > 0 ) { - stepError = new E2edError('Caught an error in step', { + stepError = new E2edError(message, { cause: String(stepError), ...errorProperties, }); @@ -38,7 +39,7 @@ export const processStepError = ({error, errorProperties, logEvent}: Options): u ); } } else { - stepError = new E2edError('Caught an error in step', {cause: stepError, ...errorProperties}); + stepError = new E2edError(message, {cause: stepError, ...errorProperties}); } if (logEvent !== undefined) {