@alwaysmeticulous/cli 2.3.2 → 2.4.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/command-utils/command-utils.d.ts +9 -0
- package/dist/command-utils/command-utils.js +18 -0
- package/dist/command-utils/common-options.d.ts +110 -0
- package/dist/command-utils/common-options.js +87 -0
- package/dist/command-utils/common-types.d.ts +20 -0
- package/dist/command-utils/common-types.js +2 -0
- package/dist/commands/create-test/create-test.command.d.ts +2 -2
- package/dist/commands/create-test/create-test.command.js +23 -40
- package/dist/commands/record/record.command.d.ts +9 -9
- package/dist/commands/record/record.command.js +2 -1
- package/dist/commands/replay/replay.command.d.ts +24 -24
- package/dist/commands/replay/replay.command.js +96 -78
- package/dist/commands/run-all-tests/run-all-tests.command.d.ts +11 -18
- package/dist/commands/run-all-tests/run-all-tests.command.js +38 -77
- package/dist/commands/screenshot-diff/screenshot-diff.command.d.ts +17 -10
- package/dist/commands/screenshot-diff/screenshot-diff.command.js +84 -35
- package/dist/config/config.js +2 -3
- package/dist/config/config.types.d.ts +7 -8
- package/dist/deflake-tests/deflake-tests.handler.d.ts +12 -3
- package/dist/deflake-tests/deflake-tests.handler.js +41 -6
- package/dist/image/diff.utils.d.ts +8 -3
- package/dist/image/diff.utils.js +2 -3
- package/dist/local-data/replays.d.ts +3 -3
- package/dist/local-data/replays.js +25 -23
- package/dist/local-data/screenshot-diffs.d.ts +1 -0
- package/dist/local-data/screenshot-diffs.js +2 -2
- package/dist/parallel-tests/messages.types.d.ts +3 -18
- package/dist/parallel-tests/parallel-tests.handler.d.ts +9 -14
- package/dist/parallel-tests/parallel-tests.handler.js +11 -15
- package/dist/parallel-tests/task.handler.js +2 -26
- package/dist/tsconfig.tsbuildinfo +1 -1
- package/dist/utils/config.utils.d.ts +6 -1
- package/dist/utils/config.utils.js +17 -8
- package/package.json +6 -6
|
@@ -6,13 +6,15 @@ Object.defineProperty(exports, "__esModule", { value: true });
|
|
|
6
6
|
exports.screenshotDiff = exports.diffScreenshots = exports.DiffError = void 0;
|
|
7
7
|
const common_1 = require("@alwaysmeticulous/common");
|
|
8
8
|
const loglevel_1 = __importDefault(require("loglevel"));
|
|
9
|
+
const path_1 = require("path");
|
|
9
10
|
const client_1 = require("../../api/client");
|
|
10
11
|
const replay_api_1 = require("../../api/replay.api");
|
|
12
|
+
const common_options_1 = require("../../command-utils/common-options");
|
|
11
13
|
const diff_utils_1 = require("../../image/diff.utils");
|
|
14
|
+
const io_utils_1 = require("../../image/io.utils");
|
|
12
15
|
const replays_1 = require("../../local-data/replays");
|
|
13
16
|
const screenshot_diffs_1 = require("../../local-data/screenshot-diffs");
|
|
14
17
|
const sentry_utils_1 = require("../../utils/sentry.utils");
|
|
15
|
-
const DEFAULT_MISMATCH_THRESHOLD = 0.01;
|
|
16
18
|
class DiffError extends Error {
|
|
17
19
|
constructor(message, extras) {
|
|
18
20
|
super(message);
|
|
@@ -20,40 +22,85 @@ class DiffError extends Error {
|
|
|
20
22
|
}
|
|
21
23
|
}
|
|
22
24
|
exports.DiffError = DiffError;
|
|
23
|
-
const diffScreenshots = async ({ client, baseReplayId, headReplayId,
|
|
25
|
+
const diffScreenshots = async ({ client, baseReplayId, headReplayId, baseScreenshotsDir, headScreenshotsDir, diffOptions, exitOnMismatch, }) => {
|
|
26
|
+
const { diffThreshold, diffPixelThreshold } = diffOptions;
|
|
24
27
|
const logger = loglevel_1.default.getLogger(common_1.METICULOUS_LOGGER_NAME);
|
|
25
|
-
const
|
|
26
|
-
const
|
|
28
|
+
const baseReplayScreenshots = await (0, replays_1.getScreenshotFiles)(baseScreenshotsDir);
|
|
29
|
+
const headReplayScreenshots = await (0, replays_1.getScreenshotFiles)(headScreenshotsDir);
|
|
30
|
+
// Assume base replay screenshots are always a subset of the head replay screenshots.
|
|
31
|
+
// We report any missing base replay screenshots for visibility but don't count it as a difference.
|
|
32
|
+
const missingHeadImages = new Set([...baseReplayScreenshots].filter((file) => !headReplayScreenshots.includes(file)));
|
|
33
|
+
let totalMismatchPixels = 0;
|
|
34
|
+
const comparisonResults = [];
|
|
27
35
|
try {
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
const
|
|
37
|
-
|
|
36
|
+
if (missingHeadImages.size > 0) {
|
|
37
|
+
logComparisonResultMessage(logger, `Head replay is missing screenshots: ${[...missingHeadImages].sort()}`, "fail");
|
|
38
|
+
throw new DiffError(`Head replay is missing screenshots: ${[...missingHeadImages].sort()}`, {
|
|
39
|
+
baseReplayId,
|
|
40
|
+
headReplayId,
|
|
41
|
+
threshold: diffThreshold,
|
|
42
|
+
});
|
|
43
|
+
}
|
|
44
|
+
for (const screenshotFileName of headReplayScreenshots) {
|
|
45
|
+
if (!baseReplayScreenshots.includes(screenshotFileName)) {
|
|
46
|
+
logger.info(`Screenshot ${screenshotFileName} not present in base replay`);
|
|
47
|
+
continue;
|
|
48
|
+
}
|
|
49
|
+
const baseScreenshotFile = (0, path_1.join)(baseScreenshotsDir, screenshotFileName);
|
|
50
|
+
const headScreenshotFile = (0, path_1.join)(headScreenshotsDir, screenshotFileName);
|
|
51
|
+
const baseScreenshot = await (0, io_utils_1.readPng)(baseScreenshotFile);
|
|
52
|
+
const headScreenshot = await (0, io_utils_1.readPng)(headScreenshotFile);
|
|
53
|
+
const comparisonResult = (0, diff_utils_1.compareImages)({
|
|
54
|
+
base: baseScreenshot,
|
|
55
|
+
head: headScreenshot,
|
|
56
|
+
pixelThreshold: diffPixelThreshold,
|
|
57
|
+
});
|
|
58
|
+
totalMismatchPixels += comparisonResult.mismatchPixels;
|
|
59
|
+
logger.debug({
|
|
60
|
+
screenshotFileName,
|
|
61
|
+
mismatchPixels: comparisonResult.mismatchPixels,
|
|
62
|
+
mismatchFraction: comparisonResult.mismatchFraction,
|
|
63
|
+
});
|
|
64
|
+
await (0, screenshot_diffs_1.writeScreenshotDiff)({
|
|
65
|
+
baseReplayId,
|
|
66
|
+
headReplayId,
|
|
67
|
+
screenshotFileName,
|
|
68
|
+
diff: comparisonResult.diff,
|
|
69
|
+
});
|
|
70
|
+
comparisonResults.push({
|
|
71
|
+
baseScreenshotFile,
|
|
72
|
+
headScreenshotFile,
|
|
73
|
+
comparisonResult,
|
|
74
|
+
outcome: comparisonResult.mismatchFraction > diffThreshold ? "fail" : "pass",
|
|
75
|
+
});
|
|
76
|
+
}
|
|
38
77
|
await (0, replay_api_1.postScreenshotDiffStats)(client, {
|
|
39
78
|
baseReplayId,
|
|
40
79
|
headReplayId,
|
|
41
80
|
stats: {
|
|
42
|
-
width:
|
|
43
|
-
height:
|
|
44
|
-
mismatchPixels,
|
|
81
|
+
width: 0,
|
|
82
|
+
height: 0,
|
|
83
|
+
mismatchPixels: totalMismatchPixels,
|
|
45
84
|
},
|
|
46
85
|
});
|
|
47
|
-
|
|
48
|
-
|
|
86
|
+
const diffUrl = await (0, replay_api_1.getDiffUrl)(client, baseReplayId, headReplayId);
|
|
87
|
+
logger.info(`View screenshot diff at ${diffUrl}`);
|
|
88
|
+
comparisonResults.forEach((result) => {
|
|
89
|
+
logComparisonResultMessage(logger, `${Math.round(result.comparisonResult.mismatchFraction * 100)}% pixel mismatch for screenshot ${(0, path_1.basename)(result.headScreenshotFile)} (threshold is ${Math.round(diffThreshold * 100)}%)`, result.outcome);
|
|
90
|
+
});
|
|
91
|
+
// Check if individual screenshot mismatch is higher than the threshold.
|
|
92
|
+
const mismatchingScreenshots = comparisonResults.filter((result) => result.outcome == "fail");
|
|
93
|
+
if (mismatchingScreenshots.length) {
|
|
94
|
+
logger.info(`Screenshots ${mismatchingScreenshots
|
|
95
|
+
.map((result) => (0, path_1.basename)(result.headScreenshotFile))
|
|
96
|
+
.sort()} do not match!`);
|
|
49
97
|
if (exitOnMismatch) {
|
|
50
98
|
process.exit(1);
|
|
51
99
|
}
|
|
52
|
-
throw new DiffError(
|
|
100
|
+
throw new DiffError(`Screenshots ${mismatchingScreenshots.map((result) => (0, path_1.basename)(result.headScreenshotFile))} do not match!`, {
|
|
53
101
|
baseReplayId,
|
|
54
102
|
headReplayId,
|
|
55
|
-
threshold,
|
|
56
|
-
value: mismatchFraction,
|
|
103
|
+
threshold: diffThreshold,
|
|
57
104
|
});
|
|
58
105
|
}
|
|
59
106
|
}
|
|
@@ -67,28 +114,34 @@ const diffScreenshots = async ({ client, baseReplayId, headReplayId, baseScreens
|
|
|
67
114
|
throw new DiffError(`Error while diffing: ${error}`, {
|
|
68
115
|
baseReplayId,
|
|
69
116
|
headReplayId,
|
|
70
|
-
threshold,
|
|
117
|
+
threshold: diffThreshold,
|
|
71
118
|
value: 1,
|
|
72
119
|
});
|
|
73
120
|
}
|
|
121
|
+
return comparisonResults;
|
|
74
122
|
};
|
|
75
123
|
exports.diffScreenshots = diffScreenshots;
|
|
124
|
+
const logComparisonResultMessage = (logger, message, outcome) => {
|
|
125
|
+
logger.info(`${message} => ${outcome === "pass" ? "PASS" : "FAIL!"}`);
|
|
126
|
+
};
|
|
76
127
|
const handler = async ({ apiToken, baseSimulationId: baseReplayId, headSimulationId: headReplayId, threshold, pixelThreshold, }) => {
|
|
77
128
|
const client = (0, client_1.createClient)({ apiToken });
|
|
78
129
|
await (0, replays_1.getOrFetchReplay)(client, baseReplayId);
|
|
79
130
|
await (0, replays_1.getOrFetchReplayArchive)(client, baseReplayId);
|
|
80
131
|
await (0, replays_1.getOrFetchReplay)(client, headReplayId);
|
|
81
132
|
await (0, replays_1.getOrFetchReplayArchive)(client, headReplayId);
|
|
82
|
-
const
|
|
83
|
-
const
|
|
133
|
+
const baseScreenshotsDir = (0, replays_1.getScreenshotsDir)((0, replays_1.getReplayDir)(baseReplayId));
|
|
134
|
+
const headScreenshotsDir = (0, replays_1.getScreenshotsDir)((0, replays_1.getReplayDir)(headReplayId));
|
|
84
135
|
await (0, exports.diffScreenshots)({
|
|
85
136
|
client,
|
|
86
137
|
baseReplayId,
|
|
87
138
|
headReplayId,
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
139
|
+
baseScreenshotsDir,
|
|
140
|
+
headScreenshotsDir,
|
|
141
|
+
diffOptions: {
|
|
142
|
+
diffThreshold: threshold,
|
|
143
|
+
diffPixelThreshold: pixelThreshold,
|
|
144
|
+
},
|
|
92
145
|
exitOnMismatch: true,
|
|
93
146
|
});
|
|
94
147
|
};
|
|
@@ -109,12 +162,8 @@ exports.screenshotDiff = {
|
|
|
109
162
|
demandOption: true,
|
|
110
163
|
alias: "headReplayId",
|
|
111
164
|
},
|
|
112
|
-
threshold:
|
|
113
|
-
|
|
114
|
-
},
|
|
115
|
-
pixelThreshold: {
|
|
116
|
-
number: true,
|
|
117
|
-
},
|
|
165
|
+
threshold: common_options_1.SCREENSHOT_DIFF_OPTIONS.diffThreshold,
|
|
166
|
+
pixelThreshold: common_options_1.SCREENSHOT_DIFF_OPTIONS.diffPixelThreshold,
|
|
118
167
|
},
|
|
119
168
|
handler: (0, sentry_utils_1.wrapHandler)(handler),
|
|
120
169
|
};
|
package/dist/config/config.js
CHANGED
|
@@ -22,14 +22,13 @@ const getConfigFilePath = async () => {
|
|
|
22
22
|
return configFilePath;
|
|
23
23
|
};
|
|
24
24
|
const validateReplayOptions = (prevOptions) => {
|
|
25
|
-
const { screenshotSelector, diffThreshold, diffPixelThreshold,
|
|
25
|
+
const { screenshotSelector, diffThreshold, diffPixelThreshold, moveBeforeClick, simulationIdForAssets, } = prevOptions;
|
|
26
26
|
return {
|
|
27
27
|
...(screenshotSelector ? { screenshotSelector } : {}),
|
|
28
28
|
...(diffThreshold ? { diffThreshold } : {}),
|
|
29
29
|
...(diffPixelThreshold ? { diffPixelThreshold } : {}),
|
|
30
|
-
...(cookies ? { cookies } : {}),
|
|
31
30
|
...(moveBeforeClick ? { moveBeforeClick } : {}),
|
|
32
|
-
...(
|
|
31
|
+
...(simulationIdForAssets ? { simulationIdForAssets } : {}),
|
|
33
32
|
};
|
|
34
33
|
};
|
|
35
34
|
const validateConfig = (prevConfig) => {
|
|
@@ -1,19 +1,18 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
diffPixelThreshold?: number;
|
|
5
|
-
cookies?: Record<string, any>[];
|
|
6
|
-
moveBeforeClick?: boolean;
|
|
1
|
+
import { ScreenshotDiffOptions } from "../command-utils/common-types";
|
|
2
|
+
export interface TestCaseReplayOptions extends Partial<ScreenshotDiffOptions> {
|
|
3
|
+
appUrl?: string | null | undefined;
|
|
7
4
|
/**
|
|
8
5
|
* If present will run the session against a local server serving up previously snapshotted assets (HTML, JS, CSS etc.) from the specified prior replay, instead of against a URL.
|
|
9
6
|
*/
|
|
10
|
-
|
|
7
|
+
simulationIdForAssets?: string | undefined;
|
|
8
|
+
screenshotSelector?: string;
|
|
9
|
+
moveBeforeClick?: boolean;
|
|
11
10
|
}
|
|
12
11
|
export interface TestCase {
|
|
13
12
|
title: string;
|
|
14
13
|
sessionId: string;
|
|
15
14
|
baseReplayId: string;
|
|
16
|
-
options?:
|
|
15
|
+
options?: TestCaseReplayOptions;
|
|
17
16
|
}
|
|
18
17
|
export interface MeticulousCliConfig {
|
|
19
18
|
testCases?: TestCase[];
|
|
@@ -1,7 +1,16 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { ReplayExecutionOptions, ReplayTarget } from "@alwaysmeticulous/common/dist/types/replay.types";
|
|
2
|
+
import { ScreenshotAssertionsEnabledOptions } from "../command-utils/common-types";
|
|
2
3
|
import { TestCase, TestCaseResult } from "../config/config.types";
|
|
3
|
-
export interface DeflakeReplayCommandHandlerOptions extends
|
|
4
|
-
testCase: TestCase;
|
|
4
|
+
export interface DeflakeReplayCommandHandlerOptions extends HandleReplayOptions {
|
|
5
5
|
deflake: boolean;
|
|
6
6
|
}
|
|
7
|
+
interface HandleReplayOptions {
|
|
8
|
+
replayTarget: ReplayTarget;
|
|
9
|
+
executionOptions: ReplayExecutionOptions;
|
|
10
|
+
screenshottingOptions: ScreenshotAssertionsEnabledOptions;
|
|
11
|
+
testCase: TestCase;
|
|
12
|
+
apiToken: string | undefined;
|
|
13
|
+
commitSha: string;
|
|
14
|
+
}
|
|
7
15
|
export declare const deflakeReplayCommandHandler: (options: DeflakeReplayCommandHandlerOptions) => Promise<TestCaseResult>;
|
|
16
|
+
export {};
|
|
@@ -8,9 +8,21 @@ const common_1 = require("@alwaysmeticulous/common");
|
|
|
8
8
|
const loglevel_1 = __importDefault(require("loglevel"));
|
|
9
9
|
const replay_command_1 = require("../commands/replay/replay.command");
|
|
10
10
|
const screenshot_diff_command_1 = require("../commands/screenshot-diff/screenshot-diff.command");
|
|
11
|
-
const handleReplay = async ({ testCase,
|
|
11
|
+
const handleReplay = async ({ testCase, replayTarget, executionOptions, screenshottingOptions, apiToken, commitSha, }) => {
|
|
12
|
+
var _a, _b;
|
|
12
13
|
const logger = loglevel_1.default.getLogger(common_1.METICULOUS_LOGGER_NAME);
|
|
13
|
-
const replayPromise = (0, replay_command_1.replayCommandHandler)(
|
|
14
|
+
const replayPromise = (0, replay_command_1.replayCommandHandler)({
|
|
15
|
+
replayTarget,
|
|
16
|
+
executionOptions: applyTestCaseExecutionOptionOverrides(executionOptions, (_a = testCase.options) !== null && _a !== void 0 ? _a : {}),
|
|
17
|
+
screenshottingOptions: applyTestCaseScreenshottingOptionsOverrides(screenshottingOptions, (_b = testCase.options) !== null && _b !== void 0 ? _b : {}),
|
|
18
|
+
apiToken,
|
|
19
|
+
commitSha,
|
|
20
|
+
sessionId: testCase.sessionId,
|
|
21
|
+
baseSimulationId: testCase.baseReplayId,
|
|
22
|
+
save: false,
|
|
23
|
+
exitOnMismatch: false,
|
|
24
|
+
cookiesFile: undefined,
|
|
25
|
+
});
|
|
14
26
|
const result = await replayPromise
|
|
15
27
|
.then((replay) => ({
|
|
16
28
|
...testCase,
|
|
@@ -30,18 +42,41 @@ const handleReplay = async ({ testCase, options }) => {
|
|
|
30
42
|
});
|
|
31
43
|
return result;
|
|
32
44
|
};
|
|
33
|
-
const deflakeReplayCommandHandler = async ({
|
|
45
|
+
const deflakeReplayCommandHandler = async ({ deflake, ...otherOptions }) => {
|
|
34
46
|
const logger = loglevel_1.default.getLogger(common_1.METICULOUS_LOGGER_NAME);
|
|
35
|
-
const firstResult = await handleReplay(
|
|
47
|
+
const firstResult = await handleReplay(otherOptions);
|
|
36
48
|
if (firstResult.result === "pass" || !deflake) {
|
|
37
49
|
return firstResult;
|
|
38
50
|
}
|
|
39
|
-
const secondResult = await handleReplay(
|
|
51
|
+
const secondResult = await handleReplay(otherOptions);
|
|
40
52
|
if (secondResult.result === "fail") {
|
|
41
53
|
return secondResult;
|
|
42
54
|
}
|
|
43
|
-
const thirdResult = await handleReplay(
|
|
55
|
+
const thirdResult = await handleReplay(otherOptions);
|
|
44
56
|
logger.info(`FLAKE: ${thirdResult.title} => ${thirdResult.result}`);
|
|
45
57
|
return thirdResult;
|
|
46
58
|
};
|
|
47
59
|
exports.deflakeReplayCommandHandler = deflakeReplayCommandHandler;
|
|
60
|
+
const applyTestCaseExecutionOptionOverrides = (executionOptionsFromCliFlags, overridesFromTestCase) => {
|
|
61
|
+
var _a;
|
|
62
|
+
// Options specified in the test case override those passed as CLI flags
|
|
63
|
+
// (CLI flags set the defaults)
|
|
64
|
+
return {
|
|
65
|
+
...executionOptionsFromCliFlags,
|
|
66
|
+
moveBeforeClick: (_a = overridesFromTestCase.moveBeforeClick) !== null && _a !== void 0 ? _a : executionOptionsFromCliFlags.moveBeforeClick,
|
|
67
|
+
};
|
|
68
|
+
};
|
|
69
|
+
const applyTestCaseScreenshottingOptionsOverrides = (screenshottingOptionsFromCliFlags, overridesFromTestCase) => {
|
|
70
|
+
var _a, _b, _c;
|
|
71
|
+
// Options specified in the test case override those passed as CLI flags
|
|
72
|
+
// (CLI flags set the defaults)
|
|
73
|
+
const diffOptions = {
|
|
74
|
+
diffThreshold: (_a = overridesFromTestCase.diffThreshold) !== null && _a !== void 0 ? _a : screenshottingOptionsFromCliFlags.diffOptions.diffThreshold,
|
|
75
|
+
diffPixelThreshold: (_b = overridesFromTestCase.diffPixelThreshold) !== null && _b !== void 0 ? _b : screenshottingOptionsFromCliFlags.diffOptions.diffPixelThreshold,
|
|
76
|
+
};
|
|
77
|
+
return {
|
|
78
|
+
enabled: true,
|
|
79
|
+
screenshotSelector: (_c = overridesFromTestCase.screenshotSelector) !== null && _c !== void 0 ? _c : screenshottingOptionsFromCliFlags.screenshotSelector,
|
|
80
|
+
diffOptions,
|
|
81
|
+
};
|
|
82
|
+
};
|
|
@@ -2,9 +2,14 @@ import { PNG } from "pngjs";
|
|
|
2
2
|
export interface CompareImageOptions {
|
|
3
3
|
base: PNG;
|
|
4
4
|
head: PNG;
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
5
|
+
/**
|
|
6
|
+
* Maximum colour distance a given pixel is allowed to differ before counting two
|
|
7
|
+
* pixels as different.
|
|
8
|
+
*
|
|
9
|
+
* Measure is based on "Measuring perceived color difference using YIQ NTSC transmission color space
|
|
10
|
+
* in mobile applications" by Y. Kotsarenko and F. Ramos
|
|
11
|
+
*/
|
|
12
|
+
pixelThreshold: number;
|
|
8
13
|
}
|
|
9
14
|
export interface CompareImageResult {
|
|
10
15
|
mismatchPixels: number;
|
package/dist/image/diff.utils.js
CHANGED
|
@@ -6,15 +6,14 @@ Object.defineProperty(exports, "__esModule", { value: true });
|
|
|
6
6
|
exports.compareImages = void 0;
|
|
7
7
|
const pixelmatch_1 = __importDefault(require("pixelmatch"));
|
|
8
8
|
const pngjs_1 = require("pngjs");
|
|
9
|
-
const compareImages = ({ base, head,
|
|
9
|
+
const compareImages = ({ base, head, pixelThreshold }) => {
|
|
10
10
|
if (base.width !== head.width || base.height !== head.height) {
|
|
11
11
|
throw new Error("Cannot handle different size yet");
|
|
12
12
|
}
|
|
13
13
|
const { width, height } = base;
|
|
14
14
|
const diff = new pngjs_1.PNG({ width, height });
|
|
15
|
-
const threshold = (pixelmatchOptions === null || pixelmatchOptions === void 0 ? void 0 : pixelmatchOptions.threshold) || 0.01;
|
|
16
15
|
const mismatchPixels = (0, pixelmatch_1.default)(base.data, head.data, diff.data, width, height, {
|
|
17
|
-
threshold,
|
|
16
|
+
threshold: pixelThreshold,
|
|
18
17
|
});
|
|
19
18
|
const mismatchFraction = mismatchPixels / (width * height);
|
|
20
19
|
return {
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { AxiosInstance } from "axios";
|
|
2
|
-
import { PNG } from "pngjs";
|
|
3
2
|
export declare const getOrFetchReplay: (client: AxiosInstance, replayId: string) => Promise<any>;
|
|
4
3
|
export declare const getOrFetchReplayArchive: (client: AxiosInstance, replayId: string) => Promise<void>;
|
|
5
|
-
export declare const
|
|
6
|
-
export declare const readLocalReplayScreenshot: (tempDir: string) => Promise<PNG>;
|
|
4
|
+
export declare const getScreenshotFiles: (screenshotsDirPath: string) => Promise<string[]>;
|
|
7
5
|
export declare const getSnapshottedAssetsDir: (replayId: string) => string;
|
|
6
|
+
export declare const getScreenshotsDir: (replayDir: string) => string;
|
|
7
|
+
export declare const getReplayDir: (replayId: string) => string;
|
|
@@ -3,21 +3,21 @@ var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
|
3
3
|
return (mod && mod.__esModule) ? mod : { "default": mod };
|
|
4
4
|
};
|
|
5
5
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
6
|
-
exports.
|
|
6
|
+
exports.getReplayDir = exports.getScreenshotsDir = exports.getSnapshottedAssetsDir = exports.getScreenshotFiles = exports.getOrFetchReplayArchive = exports.getOrFetchReplay = void 0;
|
|
7
7
|
const common_1 = require("@alwaysmeticulous/common");
|
|
8
8
|
const adm_zip_1 = __importDefault(require("adm-zip"));
|
|
9
9
|
const promises_1 = require("fs/promises");
|
|
10
|
+
const promises_2 = require("fs/promises");
|
|
10
11
|
const loglevel_1 = __importDefault(require("loglevel"));
|
|
11
12
|
const path_1 = require("path");
|
|
12
13
|
const download_1 = require("../api/download");
|
|
13
14
|
const replay_api_1 = require("../api/replay.api");
|
|
14
|
-
const io_utils_1 = require("../image/io.utils");
|
|
15
15
|
const getOrFetchReplay = async (client, replayId) => {
|
|
16
16
|
const logger = loglevel_1.default.getLogger(common_1.METICULOUS_LOGGER_NAME);
|
|
17
|
-
const replayDir = getReplayDir(replayId);
|
|
18
|
-
await (0,
|
|
17
|
+
const replayDir = (0, exports.getReplayDir)(replayId);
|
|
18
|
+
await (0, promises_2.mkdir)(replayDir, { recursive: true });
|
|
19
19
|
const replayFile = (0, path_1.join)(replayDir, `${replayId}.json`);
|
|
20
|
-
const existingReplay = await (0,
|
|
20
|
+
const existingReplay = await (0, promises_2.readFile)(replayFile)
|
|
21
21
|
.then((data) => JSON.parse(data.toString("utf-8")))
|
|
22
22
|
.catch(() => null);
|
|
23
23
|
if (existingReplay) {
|
|
@@ -29,20 +29,20 @@ const getOrFetchReplay = async (client, replayId) => {
|
|
|
29
29
|
logger.error(`Error: Could not retrieve replay with id "${replayId}". Is the API token correct?`);
|
|
30
30
|
process.exit(1);
|
|
31
31
|
}
|
|
32
|
-
await (0,
|
|
32
|
+
await (0, promises_2.writeFile)(replayFile, JSON.stringify(replay, null, 2));
|
|
33
33
|
logger.debug(`Wrote replay to ${replayFile}`);
|
|
34
34
|
return replay;
|
|
35
35
|
};
|
|
36
36
|
exports.getOrFetchReplay = getOrFetchReplay;
|
|
37
37
|
const getOrFetchReplayArchive = async (client, replayId) => {
|
|
38
38
|
const logger = loglevel_1.default.getLogger(common_1.METICULOUS_LOGGER_NAME);
|
|
39
|
-
const replayDir = getReplayDir(replayId);
|
|
40
|
-
await (0,
|
|
39
|
+
const replayDir = (0, exports.getReplayDir)(replayId);
|
|
40
|
+
await (0, promises_2.mkdir)(replayDir, { recursive: true });
|
|
41
41
|
const replayArchiveFile = (0, path_1.join)(replayDir, `${replayId}.zip`);
|
|
42
42
|
const paramsFile = (0, path_1.join)(replayDir, "replayEventsParams.json");
|
|
43
43
|
// Check if "replayEventsParams.json" exists. If yes, we assume the replay
|
|
44
44
|
// zip archive has been downloaded and extracted.
|
|
45
|
-
const paramsFileExists = await (0,
|
|
45
|
+
const paramsFileExists = await (0, promises_2.access)(paramsFile)
|
|
46
46
|
.then(() => true)
|
|
47
47
|
.catch(() => false);
|
|
48
48
|
if (paramsFileExists) {
|
|
@@ -57,23 +57,25 @@ const getOrFetchReplayArchive = async (client, replayId) => {
|
|
|
57
57
|
await (0, download_1.downloadFile)(downloadUrlData.dowloadUrl, replayArchiveFile);
|
|
58
58
|
const zipFile = new adm_zip_1.default(replayArchiveFile);
|
|
59
59
|
zipFile.extractAllTo(replayDir, /*overwrite=*/ true);
|
|
60
|
-
await (0,
|
|
60
|
+
await (0, promises_2.rm)(replayArchiveFile);
|
|
61
61
|
logger.debug(`Exrtracted replay archive in ${replayDir}`);
|
|
62
62
|
};
|
|
63
63
|
exports.getOrFetchReplayArchive = getOrFetchReplayArchive;
|
|
64
|
-
const
|
|
65
|
-
const
|
|
66
|
-
const
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
return png;
|
|
64
|
+
const getScreenshotFiles = async (screenshotsDirPath) => {
|
|
65
|
+
const screenshotFiles = [];
|
|
66
|
+
const screenshotsDir = await (0, promises_1.opendir)(screenshotsDirPath);
|
|
67
|
+
for await (const dirEntry of screenshotsDir) {
|
|
68
|
+
if (dirEntry.isFile() && dirEntry.name.endsWith(".png")) {
|
|
69
|
+
screenshotFiles.push(dirEntry.name);
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
// Sort files alphabetically to help when reading results.
|
|
73
|
+
return screenshotFiles.sort();
|
|
75
74
|
};
|
|
76
|
-
exports.
|
|
77
|
-
const getSnapshottedAssetsDir = (replayId) => (0, path_1.join)(getReplayDir(replayId), "snapshotted-assets");
|
|
75
|
+
exports.getScreenshotFiles = getScreenshotFiles;
|
|
76
|
+
const getSnapshottedAssetsDir = (replayId) => (0, path_1.join)((0, exports.getReplayDir)(replayId), "snapshotted-assets");
|
|
78
77
|
exports.getSnapshottedAssetsDir = getSnapshottedAssetsDir;
|
|
78
|
+
const getScreenshotsDir = (replayDir) => (0, path_1.join)(replayDir, "screenshots");
|
|
79
|
+
exports.getScreenshotsDir = getScreenshotsDir;
|
|
79
80
|
const getReplayDir = (replayId) => (0, path_1.join)((0, common_1.getMeticulousLocalDataDir)(), "replays", replayId);
|
|
81
|
+
exports.getReplayDir = getReplayDir;
|
|
@@ -9,11 +9,11 @@ const promises_1 = require("fs/promises");
|
|
|
9
9
|
const path_1 = require("path");
|
|
10
10
|
const loglevel_1 = __importDefault(require("loglevel"));
|
|
11
11
|
const io_utils_1 = require("../image/io.utils");
|
|
12
|
-
const writeScreenshotDiff = async ({ baseReplayId, headReplayId, diff }) => {
|
|
12
|
+
const writeScreenshotDiff = async ({ baseReplayId, headReplayId, screenshotFileName, diff, }) => {
|
|
13
13
|
const logger = loglevel_1.default.getLogger(common_1.METICULOUS_LOGGER_NAME);
|
|
14
14
|
const diffDir = (0, path_1.join)((0, common_1.getMeticulousLocalDataDir)(), "screenshot-diffs");
|
|
15
15
|
await (0, promises_1.mkdir)(diffDir, { recursive: true });
|
|
16
|
-
const diffFile = (0, path_1.join)(diffDir, `${baseReplayId}+${headReplayId}.png`);
|
|
16
|
+
const diffFile = (0, path_1.join)(diffDir, `${baseReplayId}+${headReplayId}+${screenshotFileName}.png`);
|
|
17
17
|
await (0, io_utils_1.writePng)(diff, diffFile);
|
|
18
18
|
logger.debug(`Screenshot diff written to ${diffFile}`);
|
|
19
19
|
};
|
|
@@ -1,27 +1,12 @@
|
|
|
1
1
|
import log from "loglevel";
|
|
2
|
-
import {
|
|
2
|
+
import { TestCaseResult } from "../config/config.types";
|
|
3
|
+
import { DeflakeReplayCommandHandlerOptions } from "../deflake-tests/deflake-tests.handler";
|
|
3
4
|
export interface InitMessage {
|
|
4
5
|
kind: "init";
|
|
5
6
|
data: {
|
|
6
7
|
logLevel: log.LogLevel[keyof log.LogLevel];
|
|
7
8
|
dataDir: string;
|
|
8
|
-
|
|
9
|
-
apiToken: string | null | undefined;
|
|
10
|
-
commitSha: string | null | undefined;
|
|
11
|
-
appUrl: string | null | undefined;
|
|
12
|
-
simulationIdForAssets?: string | null | undefined;
|
|
13
|
-
headless: boolean | null | undefined;
|
|
14
|
-
devTools: boolean | null | undefined;
|
|
15
|
-
bypassCSP: boolean | null | undefined;
|
|
16
|
-
diffThreshold: number | null | undefined;
|
|
17
|
-
diffPixelThreshold: number | null | undefined;
|
|
18
|
-
padTime: boolean;
|
|
19
|
-
shiftTime: boolean;
|
|
20
|
-
networkStubbing: boolean;
|
|
21
|
-
accelerate: boolean;
|
|
22
|
-
};
|
|
23
|
-
testCase: TestCase;
|
|
24
|
-
deflake: boolean;
|
|
9
|
+
replayOptions: DeflakeReplayCommandHandlerOptions;
|
|
25
10
|
};
|
|
26
11
|
}
|
|
27
12
|
export interface ResultMessage {
|
|
@@ -1,26 +1,21 @@
|
|
|
1
|
+
import { ReplayExecutionOptions } from "@alwaysmeticulous/common";
|
|
1
2
|
import { AxiosInstance } from "axios";
|
|
2
3
|
import { TestRun } from "../api/test-run.api";
|
|
4
|
+
import { ScreenshotAssertionsEnabledOptions } from "../command-utils/common-types";
|
|
3
5
|
import { MeticulousCliConfig, TestCaseResult } from "../config/config.types";
|
|
4
6
|
export interface RunAllTestsInParallelOptions {
|
|
5
7
|
config: MeticulousCliConfig;
|
|
6
8
|
client: AxiosInstance;
|
|
7
9
|
testRun: TestRun;
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
diffThreshold: number | null | undefined;
|
|
16
|
-
diffPixelThreshold: number | null | undefined;
|
|
17
|
-
padTime: boolean;
|
|
18
|
-
shiftTime: boolean;
|
|
19
|
-
networkStubbing: boolean;
|
|
20
|
-
parallelTasks: number | null | undefined;
|
|
10
|
+
executionOptions: ReplayExecutionOptions;
|
|
11
|
+
screenshottingOptions: ScreenshotAssertionsEnabledOptions;
|
|
12
|
+
apiToken: string | undefined;
|
|
13
|
+
commitSha: string;
|
|
14
|
+
appUrl: string | undefined;
|
|
15
|
+
useAssetsSnapshottedInBaseSimulation: boolean;
|
|
16
|
+
parallelTasks: number | undefined;
|
|
21
17
|
deflake: boolean;
|
|
22
18
|
cachedTestRunResults: TestCaseResult[];
|
|
23
|
-
accelerate: boolean;
|
|
24
19
|
}
|
|
25
20
|
/** Handler for running Meticulous tests in parallel using child processes */
|
|
26
21
|
export declare const runAllTestsInParallel: (options: RunAllTestsInParallelOptions) => Promise<TestCaseResult[]>;
|
|
@@ -13,7 +13,7 @@ const test_run_api_1 = require("../api/test-run.api");
|
|
|
13
13
|
const config_utils_1 = require("../utils/config.utils");
|
|
14
14
|
const run_all_tests_utils_1 = require("../utils/run-all-tests.utils");
|
|
15
15
|
/** Handler for running Meticulous tests in parallel using child processes */
|
|
16
|
-
const runAllTestsInParallel = async ({ config, client, testRun, apiToken, commitSha, appUrl, useAssetsSnapshottedInBaseSimulation,
|
|
16
|
+
const runAllTestsInParallel = async ({ config, client, testRun, apiToken, commitSha, appUrl, useAssetsSnapshottedInBaseSimulation, executionOptions, screenshottingOptions, parallelTasks, deflake, cachedTestRunResults, }) => {
|
|
17
17
|
const logger = loglevel_1.default.getLogger(common_1.METICULOUS_LOGGER_NAME);
|
|
18
18
|
const results = [...cachedTestRunResults];
|
|
19
19
|
const queue = (0, run_all_tests_utils_1.getTestsToRun)({
|
|
@@ -58,23 +58,19 @@ const runAllTestsInParallel = async ({ config, client, testRun, apiToken, commit
|
|
|
58
58
|
data: {
|
|
59
59
|
logLevel: logger.getLevel(),
|
|
60
60
|
dataDir: (0, common_1.getMeticulousLocalDataDir)(),
|
|
61
|
-
|
|
61
|
+
replayOptions: {
|
|
62
62
|
apiToken,
|
|
63
63
|
commitSha,
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
simulationIdForAssets: (0, config_utils_1.getSimulationIdForAssets)(testCase, useAssetsSnapshottedInBaseSimulation),
|
|
74
|
-
accelerate,
|
|
64
|
+
testCase,
|
|
65
|
+
deflake,
|
|
66
|
+
replayTarget: (0, config_utils_1.getReplayTargetForTestCase)({
|
|
67
|
+
useAssetsSnapshottedInBaseSimulation,
|
|
68
|
+
appUrl,
|
|
69
|
+
testCase,
|
|
70
|
+
}),
|
|
71
|
+
executionOptions,
|
|
72
|
+
screenshottingOptions,
|
|
75
73
|
},
|
|
76
|
-
testCase,
|
|
77
|
-
deflake,
|
|
78
74
|
},
|
|
79
75
|
};
|
|
80
76
|
child.send(initMessage);
|
|
@@ -38,34 +38,10 @@ const main = async () => {
|
|
|
38
38
|
process.exit(1);
|
|
39
39
|
}
|
|
40
40
|
const initMessage = await waitForInitMessage();
|
|
41
|
-
const { logLevel, dataDir,
|
|
41
|
+
const { logLevel, dataDir, replayOptions } = initMessage.data;
|
|
42
42
|
logger.setLevel(logLevel);
|
|
43
43
|
(0, common_1.setMeticulousLocalDataDir)(dataDir);
|
|
44
|
-
const
|
|
45
|
-
const { sessionId, baseReplayId, options } = testCase;
|
|
46
|
-
const result = await (0, deflake_tests_handler_1.deflakeReplayCommandHandler)({
|
|
47
|
-
testCase,
|
|
48
|
-
deflake,
|
|
49
|
-
apiToken,
|
|
50
|
-
commitSha,
|
|
51
|
-
sessionId,
|
|
52
|
-
appUrl,
|
|
53
|
-
simulationIdForAssets,
|
|
54
|
-
headless,
|
|
55
|
-
devTools,
|
|
56
|
-
bypassCSP,
|
|
57
|
-
screenshot: true,
|
|
58
|
-
baseSimulationId: baseReplayId,
|
|
59
|
-
diffThreshold,
|
|
60
|
-
diffPixelThreshold,
|
|
61
|
-
save: false,
|
|
62
|
-
exitOnMismatch: false,
|
|
63
|
-
padTime,
|
|
64
|
-
shiftTime,
|
|
65
|
-
networkStubbing,
|
|
66
|
-
accelerate,
|
|
67
|
-
...options,
|
|
68
|
-
});
|
|
44
|
+
const result = await (0, deflake_tests_handler_1.deflakeReplayCommandHandler)(replayOptions);
|
|
69
45
|
const resultMessage = {
|
|
70
46
|
kind: "result",
|
|
71
47
|
data: {
|