Skip to content

Commit 7f6fca6

Browse files
committed
PRO-17102 feat: add screenshot dimensions to HTML report and logs
1 parent 6b63918 commit 7f6fca6

21 files changed

+347
-116
lines changed

autotests/configurator/regroupSteps.ts

Lines changed: 0 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -5,8 +5,6 @@ import type {LogEvent, Mutable} from 'e2ed/types';
55

66
/**
77
* Regroup log events (for grouping of `TestRun` steps).
8-
* This base client function should not use scope variables (except other base functions).
9-
* @internal
108
*/
119
export const regroupSteps = (logEvents: readonly LogEvent[]): readonly LogEvent[] => {
1210
const topLevelTypes: readonly LogEventType[] = [

src/README.md

Lines changed: 41 additions & 40 deletions
Original file line numberDiff line numberDiff line change
@@ -15,43 +15,44 @@ Modules in the dependency graph should only import the modules above them:
1515
8. `utils/getHash`
1616
9. `generators`
1717
10. `utils/headers`
18-
11. `utils/viewport`
19-
12. `utils/parse`
20-
13. `utils/distanceBetweenSelectors`
21-
14. `utils/getDurationWithUnits`
22-
15. `utils/valueToString`
23-
16. `utils/error`
24-
17. `utils/asserts`
25-
18. `utils/object`
26-
19. `utils/uiMode`
27-
20. `utils/runLabel`
28-
21. `utils/clone`
29-
22. `utils/notIncludedInPackTests`
30-
23. `utils/userland`
31-
24. `utils/fn`
32-
25. `utils/environment`
33-
26. `utils/packCompiler`
34-
27. `config`
35-
28. `utils/config`
36-
29. `utils/generalLog`
37-
30. `utils/testFilePaths`
38-
31. `utils/exit`
39-
32. `utils/promise`
40-
33. `utils/resourceUsage`
41-
34. `utils/fs`
42-
35. `utils/getGlobalErrorHandler`
43-
36. `utils/tests`
44-
37. `utils/end`
45-
38. `utils/pack`
46-
39. `useContext`
47-
40. `context`
48-
41. `utils/step`
49-
42. `utils/apiStatistics`
50-
43. `utils/selectors`
51-
44. `selectors`
52-
45. `utils/log`
53-
46. `step`
54-
47. `utils/waitForEvents`
55-
48. `utils/expect`
56-
49. `expect`
57-
50. ...
18+
11. `utils/screenshot`
19+
12. `utils/viewport`
20+
13. `utils/parse`
21+
14. `utils/distanceBetweenSelectors`
22+
15. `utils/getDurationWithUnits`
23+
16. `utils/valueToString`
24+
17. `utils/error`
25+
18. `utils/asserts`
26+
19. `utils/object`
27+
20. `utils/uiMode`
28+
21. `utils/runLabel`
29+
22. `utils/clone`
30+
23. `utils/notIncludedInPackTests`
31+
24. `utils/userland`
32+
25. `utils/fn`
33+
26. `utils/environment`
34+
27. `utils/packCompiler`
35+
28. `config`
36+
29. `utils/config`
37+
30. `utils/generalLog`
38+
31. `utils/testFilePaths`
39+
32. `utils/exit`
40+
33. `utils/promise`
41+
34. `utils/resourceUsage`
42+
35. `utils/fs`
43+
36. `utils/getGlobalErrorHandler`
44+
37. `utils/tests`
45+
38. `utils/end`
46+
39. `utils/pack`
47+
40. `useContext`
48+
41. `context`
49+
42. `utils/step`
50+
43. `utils/apiStatistics`
51+
44. `utils/selectors`
52+
45. `selectors`
53+
46. `utils/log`
54+
47. `step`
55+
48. `utils/waitForEvents`
56+
49. `utils/expect`
57+
50. `expect`
58+
51. ...

src/actions/takeElementScreenshot.ts

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

3-
import {LogEventType, SCREENSHOTS_DIRECTORY_PATH} from '../constants/internal';
3+
import {
4+
ADDITIONAL_STEP_TIMEOUT,
5+
LogEventType,
6+
SCREENSHOTS_DIRECTORY_PATH,
7+
} from '../constants/internal';
48
import {step} from '../step';
9+
import {getDimensionsString, getPngDimensions} from '../utils/screenshot';
510

611
import type {Locator} from '@playwright/test';
712

@@ -17,6 +22,7 @@ export const takeElementScreenshot = async (
1722
options: Options = {},
1823
): Promise<void> => {
1924
const {path: pathToScreenshot, ...optionsWithoutPath} = options;
25+
const {timeout} = options;
2026

2127
await step(
2228
'Take a screenshot of the element',
@@ -26,10 +32,14 @@ export const takeElementScreenshot = async (
2632
options.path = join(SCREENSHOTS_DIRECTORY_PATH, pathToScreenshot);
2733
}
2834

29-
await selector.getPlaywrightLocator().screenshot(options);
35+
const screenshot = await selector.getPlaywrightLocator().screenshot(options);
36+
const dimensions = getDimensionsString(getPngDimensions(screenshot));
37+
38+
return {dimensions};
3039
},
3140
{
3241
payload: {pathToScreenshot, ...optionsWithoutPath, selector},
42+
...(timeout !== undefined ? {timeout: timeout + ADDITIONAL_STEP_TIMEOUT} : undefined),
3343
type: LogEventType.InternalAction,
3444
},
3545
);

src/actions/takeScreenshot.ts

Lines changed: 16 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,13 @@
11
import {join} from 'node:path';
22

3-
import {LogEventType, SCREENSHOTS_DIRECTORY_PATH} from '../constants/internal';
3+
import {
4+
ADDITIONAL_STEP_TIMEOUT,
5+
LogEventType,
6+
SCREENSHOTS_DIRECTORY_PATH,
7+
} from '../constants/internal';
48
import {step} from '../step';
59
import {getPlaywrightPage} from '../useContext';
10+
import {getDimensionsString, getPngDimensions} from '../utils/screenshot';
611

712
import type {Page} from '@playwright/test';
813

@@ -13,6 +18,7 @@ type Options = Parameters<Page['screenshot']>[0];
1318
*/
1419
export const takeScreenshot = async (options: Options = {}): Promise<void> => {
1520
const {path: pathToScreenshot, ...optionsWithoutPath} = options;
21+
const {timeout} = options;
1622

1723
await step(
1824
'Take a screenshot of the page',
@@ -24,8 +30,15 @@ export const takeScreenshot = async (options: Options = {}): Promise<void> => {
2430

2531
const page = getPlaywrightPage();
2632

27-
await page.screenshot(options);
33+
const screenshot = await page.screenshot(options);
34+
const dimensions = getDimensionsString(getPngDimensions(screenshot));
35+
36+
return {dimensions};
37+
},
38+
{
39+
payload: {pathToScreenshot, ...optionsWithoutPath},
40+
...(timeout !== undefined ? {timeout: timeout + ADDITIONAL_STEP_TIMEOUT} : undefined),
41+
type: LogEventType.InternalAction,
2842
},
29-
{payload: {pathToScreenshot, ...optionsWithoutPath}, type: LogEventType.InternalAction},
3043
);
3144
};

src/types/index.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -47,6 +47,8 @@ export type {
4747
export type {KeyboardPressKey} from './keyboard';
4848
export type {Log, LogContext, LogParams, LogPayload, LogTag} from './log';
4949
export type {
50+
Dimensions,
51+
DimensionsString,
5052
MatchScreenshotConfig,
5153
ScreenshotMeta,
5254
ToMatchScreenshotOptions,

src/types/internal.ts

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -78,10 +78,14 @@ export type {
7878
Payload,
7979
} from './log';
8080
export type {
81+
Dimensions,
82+
DimensionsString,
8183
MatchScreenshotConfig,
8284
ScreenshotMeta,
8385
ToMatchScreenshotOptions,
8486
} from './matchScreenshot';
87+
/** @internal */
88+
export type {ScreenshotLogFields} from './matchScreenshot';
8589
export type {ApiMockFunction} from './mockApiRoute';
8690
/** @internal */
8791
export type {ApiMockState} from './mockApiRoute';

src/types/matchScreenshot.ts

Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,23 @@
1+
import type {Brand} from './brand';
12
import type {Url} from './http';
23
import type {RunLabel} from './runLabel';
34
import type {Selector} from './selectors';
45
import type {TestStaticOptions} from './testRun';
56
import type {TestMetaPlaceholder} from './userland';
67

8+
/**
9+
* Dimensions of screenshot image.
10+
*/
11+
export type Dimensions = Readonly<{
12+
height: number;
13+
width: number;
14+
}>;
15+
16+
/**
17+
* String with dimensions of screenshot, like `320x108`.
18+
*/
19+
export type DimensionsString = Brand<string, 'DimensionsString'>;
20+
721
/**
822
* Functions that describe the `toMatchScreenshot` assert (in `expect`).
923
*/
@@ -32,6 +46,16 @@ export type MatchScreenshotConfig<TestMeta = TestMetaPlaceholder> = Readonly<{
3246
) => Promise<string>;
3347
}>;
3448

49+
/**
50+
* Log fields for single screenshot.
51+
* @internal
52+
*/
53+
export type ScreenshotLogFields = {
54+
dimensions: DimensionsString;
55+
readonly screenshotId: string;
56+
url: Url;
57+
};
58+
3559
/**
3660
* General screenshot metadata (like test name, assert description, etc.).
3761
*/
Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,17 @@
1+
import type {AdditionalLogFields} from './types';
2+
3+
type Options = Readonly<{
4+
expectedScreenshotId: string;
5+
}>;
6+
7+
/**
8+
* Get empty additional log fields object for `toMatchScreenshot` assertion.
9+
* @internal
10+
*/
11+
export const getEmptyAdditionalLogFields = ({
12+
expectedScreenshotId,
13+
}: Options): AdditionalLogFields => ({
14+
actual: undefined,
15+
diff: undefined,
16+
expected: {dimensions: undefined, screenshotId: expectedScreenshotId, url: undefined},
17+
});

src/utils/expect/toMatchScreenshot.ts

Lines changed: 28 additions & 39 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,4 @@
11
import {randomUUID} from 'node:crypto';
2-
import {readFile} from 'node:fs/promises';
32
import {join} from 'node:path';
43

54
import {isLocalRun} from '../../configurator';
@@ -14,24 +13,18 @@ import {getFullPackConfig} from '../config';
1413
import {E2edError} from '../error';
1514
import {writeFile} from '../fs';
1615
import {setReadonlyProperty} from '../object';
16+
import {getDimensionsString, getPngDimensions} from '../screenshot';
1717

18+
import {getEmptyAdditionalLogFields} from './getEmptyAdditionalLogFields';
1819
import {getScreenshotMeta} from './getScreenshotMeta';
20+
import {writeScreenshotFromPath} from './writeScreenshotFromPath';
1921

20-
import type {FilePathFromRoot, Selector, ToMatchScreenshotOptions, Url} from '../../types/internal';
22+
import type {FilePathFromRoot, Selector, ToMatchScreenshotOptions} from '../../types/internal';
2123

2224
import type {Expect} from './Expect';
2325

2426
import {expect as playwrightExpect} from '@playwright/test';
2527

26-
type AdditionalLogFields = {
27-
actualScreenshotId: string | undefined;
28-
actualScreenshotUrl: Url | undefined;
29-
diffScreenshotId: string | undefined;
30-
diffScreenshotUrl: Url | undefined;
31-
expectedScreenshotId: string;
32-
expectedScreenshotUrl: Url | undefined;
33-
};
34-
3528
/**
3629
* Checks that the selector screenshot matches the one specified by `expectedScreenshotId`.
3730
* @internal
@@ -44,8 +37,7 @@ export const toMatchScreenshot = async (
4437
): Promise<void> => {
4538
const actualValue = context.actualValue as Selector;
4639
const {description} = context;
47-
const {getScreenshotUrlById, readScreenshot, writeScreenshot} =
48-
getFullPackConfig().matchScreenshot;
40+
const {getScreenshotUrlById, readScreenshot} = getFullPackConfig().matchScreenshot;
4941

5042
const assertId = randomUUID();
5143
const screenshotFileName = `${assertId}.png`;
@@ -54,14 +46,7 @@ export const toMatchScreenshot = async (
5446
screenshotFileName,
5547
) as FilePathFromRoot;
5648

57-
const additionalLogFields: AdditionalLogFields = {
58-
actualScreenshotId: undefined,
59-
actualScreenshotUrl: undefined,
60-
diffScreenshotId: undefined,
61-
diffScreenshotUrl: undefined,
62-
expectedScreenshotId,
63-
expectedScreenshotUrl: undefined,
64-
};
49+
const additionalLogFields = getEmptyAdditionalLogFields({expectedScreenshotId});
6550

6651
setReadonlyProperty(context, 'additionalLogFields', additionalLogFields);
6752

@@ -70,13 +55,17 @@ export const toMatchScreenshot = async (
7055
let expectedScreenshotFound = false;
7156

7257
if (expectedScreenshotId) {
73-
additionalLogFields.expectedScreenshotUrl = getScreenshotUrlById(expectedScreenshotId);
58+
additionalLogFields.expected.url = getScreenshotUrlById(expectedScreenshotId);
7459

7560
const expectedScreenshot = await readScreenshot(expectedScreenshotId, meta);
7661

7762
if (expectedScreenshot !== undefined) {
7863
expectedScreenshotFound = true;
7964

65+
additionalLogFields.expected.dimensions = getDimensionsString(
66+
getPngDimensions(expectedScreenshot),
67+
);
68+
8069
if (!isLocalRun) {
8170
await writeFile(screenshotPath, expectedScreenshot);
8271
}
@@ -112,20 +101,19 @@ export const toMatchScreenshot = async (
112101
const actualScreenshotPath = join(output, `${assertId}-actual.png`) as FilePathFromRoot;
113102
const diffScreenshotPath = join(output, `${assertId}-diff.png`) as FilePathFromRoot;
114103

115-
const actualScreenshot = await readFile(actualScreenshotPath);
116-
const actualScreenshotId = await writeScreenshot(actualScreenshot, meta);
117-
118-
additionalLogFields.actualScreenshotId = actualScreenshotId;
119-
additionalLogFields.actualScreenshotUrl = getScreenshotUrlById(actualScreenshotId);
120-
121-
const diffScreenshot = await readFile(diffScreenshotPath);
122-
const diffScreenshotId = await writeScreenshot(diffScreenshot, {
123-
...meta,
124-
actual: actualScreenshotId,
104+
const actualScreenshotId = await writeScreenshotFromPath({
105+
additionalLogFields,
106+
meta,
107+
path: actualScreenshotPath,
108+
type: 'actual',
125109
});
126110

127-
additionalLogFields.diffScreenshotId = diffScreenshotId;
128-
additionalLogFields.diffScreenshotUrl = getScreenshotUrlById(diffScreenshotId);
111+
await writeScreenshotFromPath({
112+
additionalLogFields,
113+
meta: {...meta, actual: actualScreenshotId},
114+
path: diffScreenshotPath,
115+
type: 'diff',
116+
});
129117
} catch (secondError) {
130118
throw new E2edError(errorMessage, {secondError});
131119
}
@@ -138,11 +126,12 @@ export const toMatchScreenshot = async (
138126
}
139127

140128
try {
141-
const actualScreenshot = await readFile(screenshotPath);
142-
const actualScreenshotId = await writeScreenshot(actualScreenshot, meta);
143-
144-
additionalLogFields.actualScreenshotId = actualScreenshotId;
145-
additionalLogFields.actualScreenshotUrl = getScreenshotUrlById(actualScreenshotId);
129+
await writeScreenshotFromPath({
130+
additionalLogFields,
131+
meta,
132+
path: screenshotPath,
133+
type: 'actual',
134+
});
146135
} catch (secondError) {
147136
throw new E2edError(message, {secondError});
148137
}

0 commit comments

Comments
 (0)