@alwaysmeticulous/cli 2.36.0 → 2.37.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/api/replay.api.d.ts +0 -1
- package/dist/api/replay.api.js +1 -13
- package/dist/api/test-run.api.d.ts +8 -2
- package/dist/api/test-run.api.js +21 -3
- package/dist/api/types.d.ts +9 -0
- package/dist/commands/create-test/create-test.command.js +0 -3
- package/dist/commands/replay/replay.command.d.ts +5 -20
- package/dist/commands/replay/replay.command.js +10 -53
- package/dist/commands/replay/utils/compute-diff.d.ts +3 -6
- package/dist/commands/replay/utils/compute-diff.js +83 -18
- package/dist/commands/replay/utils/exit-early-if-skip-upload-env-var-set.d.ts +1 -1
- package/dist/commands/replay/utils/exit-early-if-skip-upload-env-var-set.js +3 -3
- package/dist/commands/run-all-tests/run-all-tests.command.d.ts +4 -5
- package/dist/commands/run-all-tests/run-all-tests.command.js +6 -9
- package/dist/commands/screenshot-diff/screenshot-diff.command.d.ts +6 -8
- package/dist/commands/screenshot-diff/screenshot-diff.command.js +50 -97
- package/dist/commands/screenshot-diff/utils/get-screenshot-filename.d.ts +2 -0
- package/dist/commands/screenshot-diff/utils/get-screenshot-filename.js +18 -0
- package/dist/commands/screenshot-diff/utils/get-screenshot-identifier.d.ts +2 -0
- package/dist/commands/screenshot-diff/utils/get-screenshot-identifier.js +24 -0
- package/dist/commands/screenshot-diff/utils/has-notable-differences.d.ts +2 -0
- package/dist/commands/screenshot-diff/utils/has-notable-differences.js +10 -0
- package/dist/config/config.js +2 -3
- package/dist/config/config.types.d.ts +1 -1
- package/dist/deflake-tests/deflake-tests.handler.d.ts +1 -0
- package/dist/deflake-tests/deflake-tests.handler.js +9 -5
- package/dist/index.d.ts +1 -1
- package/dist/index.js +1 -3
- package/dist/local-data/replays.d.ts +6 -1
- package/dist/local-data/replays.js +11 -3
- package/dist/local-data/screenshot-diffs.js +1 -1
- package/dist/local-data/serve-assets-from-simulation.js +1 -1
- package/dist/parallel-tests/__tests__/mock-test-results.js +3 -1
- package/dist/parallel-tests/__tests__/parrallel-tests.handler.spec.js +1 -1
- package/dist/parallel-tests/merge-test-results.js +18 -9
- package/dist/parallel-tests/parallel-tests.handler.js +2 -1
- package/dist/parallel-tests/run-all-tests.d.ts +2 -2
- package/dist/parallel-tests/run-all-tests.js +50 -23
- package/dist/parallel-tests/screenshot-diff-results.utils.d.ts +7 -0
- package/dist/parallel-tests/screenshot-diff-results.utils.js +20 -0
- package/dist/utils/config.utils.d.ts +1 -2
- package/dist/utils/config.utils.js +1 -10
- package/dist/utils/run-all-tests.utils.d.ts +0 -1
- package/dist/utils/run-all-tests.utils.js +3 -50
- package/package.json +6 -4
|
@@ -3,114 +3,106 @@ 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.screenshotDiffCommand = exports.
|
|
6
|
+
exports.screenshotDiffCommand = exports.logDifferences = exports.diffScreenshots = void 0;
|
|
7
7
|
const path_1 = require("path");
|
|
8
8
|
const common_1 = require("@alwaysmeticulous/common");
|
|
9
|
+
const fast_json_stable_stringify_1 = __importDefault(require("fast-json-stable-stringify"));
|
|
9
10
|
const loglevel_1 = __importDefault(require("loglevel"));
|
|
10
11
|
const client_1 = require("../../api/client");
|
|
11
|
-
const replay_api_1 = require("../../api/replay.api");
|
|
12
12
|
const command_builder_1 = require("../../command-utils/command-builder");
|
|
13
13
|
const common_options_1 = require("../../command-utils/common-options");
|
|
14
14
|
const diff_utils_1 = require("../../image/diff.utils");
|
|
15
15
|
const io_utils_1 = require("../../image/io.utils");
|
|
16
16
|
const replays_1 = require("../../local-data/replays");
|
|
17
17
|
const screenshot_diffs_1 = require("../../local-data/screenshot-diffs");
|
|
18
|
-
const
|
|
18
|
+
const has_notable_differences_1 = require("./utils/has-notable-differences");
|
|
19
|
+
const diffScreenshots = async ({ headReplayId, baseReplayId, baseScreenshotsDir, headScreenshotsDir, baseReplayScreenshots, headReplayScreenshots, diffOptions, }) => {
|
|
19
20
|
const { diffThreshold, diffPixelThreshold } = diffOptions;
|
|
20
|
-
const
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
21
|
+
const headReplayScreenshotsByIdentifier = new Map(headReplayScreenshots.map((screenshot) => [
|
|
22
|
+
(0, fast_json_stable_stringify_1.default)(screenshot.identifier),
|
|
23
|
+
screenshot,
|
|
24
|
+
]));
|
|
25
|
+
const baseReplayScreenshotsByIdentifier = new Map(baseReplayScreenshots.map((screenshot) => [
|
|
26
|
+
(0, fast_json_stable_stringify_1.default)(screenshot.identifier),
|
|
27
|
+
screenshot,
|
|
28
|
+
]));
|
|
29
|
+
const missingHeadImages = new Set(baseReplayScreenshots.filter((file) => !headReplayScreenshotsByIdentifier.has((0, fast_json_stable_stringify_1.default)(file.identifier))));
|
|
30
|
+
const missingHeadImagesResults = Array.from(missingHeadImages).flatMap(({ identifier, fileName }) => {
|
|
28
31
|
return [
|
|
29
32
|
{
|
|
30
33
|
identifier,
|
|
31
34
|
outcome: "missing-head",
|
|
32
|
-
baseScreenshotFile: `screenshots/${
|
|
35
|
+
baseScreenshotFile: `screenshots/${fileName}`,
|
|
33
36
|
},
|
|
34
37
|
];
|
|
35
38
|
});
|
|
36
|
-
const diffAgainstBase = async (
|
|
37
|
-
const
|
|
38
|
-
if (
|
|
39
|
-
return [];
|
|
40
|
-
}
|
|
41
|
-
if (!baseReplayScreenshots.includes(screenshotFileName)) {
|
|
39
|
+
const diffAgainstBase = async (headScreenshot) => {
|
|
40
|
+
const baseScreenshot = baseReplayScreenshotsByIdentifier.get((0, fast_json_stable_stringify_1.default)(headScreenshot.identifier));
|
|
41
|
+
if (baseScreenshot == null) {
|
|
42
42
|
return [
|
|
43
43
|
{
|
|
44
|
-
identifier,
|
|
44
|
+
identifier: headScreenshot.identifier,
|
|
45
45
|
outcome: "missing-base",
|
|
46
|
-
headScreenshotFile: `screenshots/${
|
|
46
|
+
headScreenshotFile: `screenshots/${headScreenshot.fileName}`,
|
|
47
47
|
},
|
|
48
48
|
];
|
|
49
49
|
}
|
|
50
|
-
const baseScreenshotFile = (0, path_1.join)(baseScreenshotsDir,
|
|
51
|
-
const headScreenshotFile = (0, path_1.join)(headScreenshotsDir,
|
|
52
|
-
const
|
|
53
|
-
const
|
|
54
|
-
if (
|
|
55
|
-
|
|
50
|
+
const baseScreenshotFile = (0, path_1.join)(baseScreenshotsDir, baseScreenshot.fileName);
|
|
51
|
+
const headScreenshotFile = (0, path_1.join)(headScreenshotsDir, headScreenshot.fileName);
|
|
52
|
+
const baseScreenshotContents = await (0, io_utils_1.readPng)(baseScreenshotFile);
|
|
53
|
+
const headScreenshotContents = await (0, io_utils_1.readPng)(headScreenshotFile);
|
|
54
|
+
if (baseScreenshotContents.width !== headScreenshotContents.width ||
|
|
55
|
+
baseScreenshotContents.height !== headScreenshotContents.height) {
|
|
56
56
|
return [
|
|
57
57
|
{
|
|
58
|
-
identifier,
|
|
58
|
+
identifier: headScreenshot.identifier,
|
|
59
59
|
outcome: "different-size",
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
baseWidth:
|
|
63
|
-
baseHeight:
|
|
64
|
-
headWidth:
|
|
65
|
-
headHeight:
|
|
60
|
+
baseScreenshotFile: `screenshots/${baseScreenshot.fileName}`,
|
|
61
|
+
headScreenshotFile: `screenshots/${headScreenshot.fileName}`,
|
|
62
|
+
baseWidth: baseScreenshotContents.width,
|
|
63
|
+
baseHeight: baseScreenshotContents.height,
|
|
64
|
+
headWidth: headScreenshotContents.width,
|
|
65
|
+
headHeight: headScreenshotContents.height,
|
|
66
66
|
},
|
|
67
67
|
];
|
|
68
68
|
}
|
|
69
69
|
const comparisonResult = (0, diff_utils_1.compareImages)({
|
|
70
|
-
base:
|
|
71
|
-
head:
|
|
70
|
+
base: baseScreenshotContents,
|
|
71
|
+
head: headScreenshotContents,
|
|
72
72
|
pixelThreshold: diffPixelThreshold,
|
|
73
73
|
});
|
|
74
74
|
await (0, screenshot_diffs_1.writeScreenshotDiff)({
|
|
75
75
|
baseReplayId,
|
|
76
76
|
headReplayId,
|
|
77
|
-
screenshotFileName,
|
|
77
|
+
screenshotFileName: headScreenshot.fileName,
|
|
78
78
|
diff: comparisonResult.diff,
|
|
79
79
|
});
|
|
80
80
|
return [
|
|
81
81
|
{
|
|
82
|
-
identifier,
|
|
82
|
+
identifier: headScreenshot.identifier,
|
|
83
83
|
outcome: comparisonResult.mismatchFraction > diffThreshold
|
|
84
84
|
? "diff"
|
|
85
85
|
: "no-diff",
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
width:
|
|
89
|
-
height:
|
|
86
|
+
baseScreenshotFile: `screenshots/${baseScreenshot.fileName}`,
|
|
87
|
+
headScreenshotFile: `screenshots/${headScreenshot.fileName}`,
|
|
88
|
+
width: baseScreenshotContents.width,
|
|
89
|
+
height: baseScreenshotContents.height,
|
|
90
90
|
mismatchPixels: comparisonResult.mismatchPixels,
|
|
91
91
|
mismatchFraction: comparisonResult.mismatchFraction,
|
|
92
92
|
},
|
|
93
93
|
];
|
|
94
94
|
};
|
|
95
95
|
const headDiffResults = (await Promise.all(headReplayScreenshots.map(diffAgainstBase))).flat();
|
|
96
|
-
const diffUrl = await (0, replay_api_1.getDiffUrl)(client, baseReplayId, headReplayId);
|
|
97
|
-
logger.info(`View screenshot diff at ${diffUrl}`);
|
|
98
96
|
return [...missingHeadImagesResults, ...headDiffResults];
|
|
99
97
|
};
|
|
100
98
|
exports.diffScreenshots = diffScreenshots;
|
|
101
|
-
const
|
|
99
|
+
const logDifferences = ({ results, diffOptions, logger, }) => {
|
|
102
100
|
const missingHeadImagesResults = results.flatMap((result) => result.outcome === "missing-head" ? [result] : []);
|
|
103
101
|
if (missingHeadImagesResults.length) {
|
|
104
102
|
const message = `Head replay is missing screenshots: ${missingHeadImagesResults
|
|
105
103
|
.map(({ baseScreenshotFile }) => (0, path_1.basename)(baseScreenshotFile))
|
|
106
104
|
.sort()} => FAIL!`;
|
|
107
105
|
logger.info(message);
|
|
108
|
-
return {
|
|
109
|
-
hasDiffs: true,
|
|
110
|
-
summaryMessage: message,
|
|
111
|
-
baseReplayId,
|
|
112
|
-
headReplayId,
|
|
113
|
-
};
|
|
114
106
|
}
|
|
115
107
|
const missingBaseImagesResults = results.flatMap((result) => result.outcome === "missing-base" ? [result] : []);
|
|
116
108
|
if (missingHeadImagesResults.length) {
|
|
@@ -119,60 +111,21 @@ const summarizeDifferences = ({ baseReplayId, headReplayId, results, diffOptions
|
|
|
119
111
|
.sort()}`;
|
|
120
112
|
logger.info(message);
|
|
121
113
|
}
|
|
122
|
-
|
|
114
|
+
results.forEach((result) => {
|
|
123
115
|
const { outcome } = result;
|
|
124
116
|
if (outcome === "different-size") {
|
|
125
117
|
const message = `Screenshots ${(0, path_1.basename)(result.headScreenshotFile)} have different sizes => FAIL!`;
|
|
126
118
|
logger.info(message);
|
|
127
|
-
return [
|
|
128
|
-
{
|
|
129
|
-
hasDiffs: true,
|
|
130
|
-
summaryMessage: message,
|
|
131
|
-
baseReplayId,
|
|
132
|
-
headReplayId,
|
|
133
|
-
},
|
|
134
|
-
];
|
|
135
119
|
}
|
|
136
120
|
if (outcome === "diff" || outcome === "no-diff") {
|
|
137
121
|
const mismatch = (result.mismatchFraction * 100).toFixed(3);
|
|
138
122
|
const threshold = (diffOptions.diffThreshold * 100).toFixed(3);
|
|
139
123
|
const message = `${mismatch}% pixel mismatch for screenshot ${(0, path_1.basename)(result.headScreenshotFile)} (threshold is ${threshold}%) => ${outcome === "no-diff" ? "PASS" : "FAIL!"}`;
|
|
140
124
|
logger.info(message);
|
|
141
|
-
if (outcome === "diff") {
|
|
142
|
-
return [
|
|
143
|
-
{
|
|
144
|
-
hasDiffs: true,
|
|
145
|
-
summaryMessage: message,
|
|
146
|
-
baseReplayId,
|
|
147
|
-
headReplayId,
|
|
148
|
-
},
|
|
149
|
-
];
|
|
150
|
-
}
|
|
151
125
|
}
|
|
152
|
-
return [];
|
|
153
126
|
});
|
|
154
|
-
return diffs.length > 0 ? diffs[0] : { hasDiffs: false };
|
|
155
|
-
};
|
|
156
|
-
exports.summarizeDifferences = summarizeDifferences;
|
|
157
|
-
const getScreenshotIdentifier = (filename) => {
|
|
158
|
-
const name = (0, path_1.basename)(filename);
|
|
159
|
-
if (name === "final-state.png") {
|
|
160
|
-
return {
|
|
161
|
-
type: "end-state",
|
|
162
|
-
};
|
|
163
|
-
}
|
|
164
|
-
if (name.startsWith("screenshot-after-event")) {
|
|
165
|
-
const match = name.match(/^(?:.*)-(\d+)[.]png$/);
|
|
166
|
-
const eventNumber = match ? parseInt(match[1], 10) : undefined;
|
|
167
|
-
if (match && eventNumber != null && !isNaN(eventNumber)) {
|
|
168
|
-
return {
|
|
169
|
-
type: "after-event",
|
|
170
|
-
eventNumber,
|
|
171
|
-
};
|
|
172
|
-
}
|
|
173
|
-
}
|
|
174
|
-
return undefined;
|
|
175
127
|
};
|
|
128
|
+
exports.logDifferences = logDifferences;
|
|
176
129
|
const handler = async ({ apiToken, baseSimulationId: baseReplayId, headSimulationId: headReplayId, threshold, pixelThreshold, }) => {
|
|
177
130
|
const logger = loglevel_1.default.getLogger(common_1.METICULOUS_LOGGER_NAME);
|
|
178
131
|
const client = (0, client_1.createClient)({ apiToken });
|
|
@@ -186,24 +139,24 @@ const handler = async ({ apiToken, baseSimulationId: baseReplayId, headSimulatio
|
|
|
186
139
|
diffThreshold: threshold,
|
|
187
140
|
diffPixelThreshold: pixelThreshold,
|
|
188
141
|
};
|
|
142
|
+
const baseReplayScreenshots = await (0, replays_1.getScreenshotFiles)(baseScreenshotsDir);
|
|
143
|
+
const headReplayScreenshots = await (0, replays_1.getScreenshotFiles)(headScreenshotsDir);
|
|
189
144
|
const results = await (0, exports.diffScreenshots)({
|
|
190
|
-
client,
|
|
191
145
|
baseReplayId,
|
|
192
146
|
headReplayId,
|
|
193
147
|
baseScreenshotsDir,
|
|
194
148
|
headScreenshotsDir,
|
|
149
|
+
baseReplayScreenshots,
|
|
150
|
+
headReplayScreenshots,
|
|
195
151
|
diffOptions,
|
|
196
|
-
logger,
|
|
197
152
|
});
|
|
198
153
|
logger.debug(results);
|
|
199
|
-
|
|
200
|
-
baseReplayId,
|
|
201
|
-
headReplayId,
|
|
154
|
+
(0, exports.logDifferences)({
|
|
202
155
|
results,
|
|
203
156
|
diffOptions,
|
|
204
157
|
logger,
|
|
205
158
|
});
|
|
206
|
-
if (
|
|
159
|
+
if ((0, has_notable_differences_1.hasNotableDifferences)(results)) {
|
|
207
160
|
process.exit(1);
|
|
208
161
|
}
|
|
209
162
|
};
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.getScreenshotFilename = void 0;
|
|
4
|
+
// Note: ideally this should match the filenames produced by the screenshotting code
|
|
5
|
+
// in replay-node, and in https://github.com/alwaysmeticulous/meticulous-sdk/blob/395af4394dc51d9b51ba1136fc26b23fcbba5604/packages/replayer/src/screenshot.utils.ts#L42
|
|
6
|
+
const getScreenshotFilename = (identifier) => {
|
|
7
|
+
if (identifier.type === "end-state") {
|
|
8
|
+
return "final-state.png";
|
|
9
|
+
}
|
|
10
|
+
else if (identifier.type === "after-event") {
|
|
11
|
+
const eventIndexStr = identifier.eventNumber.toString().padStart(5, "0");
|
|
12
|
+
return `screenshot-after-event-${eventIndexStr}.png`;
|
|
13
|
+
}
|
|
14
|
+
else {
|
|
15
|
+
throw new Error("Unexpected screenshot identifier: " + JSON.stringify(identifier));
|
|
16
|
+
}
|
|
17
|
+
};
|
|
18
|
+
exports.getScreenshotFilename = getScreenshotFilename;
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.getScreenshotIdentifier = void 0;
|
|
4
|
+
const path_1 = require("path");
|
|
5
|
+
const getScreenshotIdentifier = (filename) => {
|
|
6
|
+
const name = (0, path_1.basename)(filename);
|
|
7
|
+
if (name === "final-state.png") {
|
|
8
|
+
return {
|
|
9
|
+
type: "end-state",
|
|
10
|
+
};
|
|
11
|
+
}
|
|
12
|
+
if (name.startsWith("screenshot-after-event")) {
|
|
13
|
+
const match = name.match(/^(?:.*)-(\d+)[.]png$/);
|
|
14
|
+
const eventNumber = match ? parseInt(match[1], 10) : undefined;
|
|
15
|
+
if (match && eventNumber != null && !isNaN(eventNumber)) {
|
|
16
|
+
return {
|
|
17
|
+
type: "after-event",
|
|
18
|
+
eventNumber,
|
|
19
|
+
};
|
|
20
|
+
}
|
|
21
|
+
}
|
|
22
|
+
return undefined;
|
|
23
|
+
};
|
|
24
|
+
exports.getScreenshotIdentifier = getScreenshotIdentifier;
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.hasNotableDifferences = void 0;
|
|
4
|
+
const hasNotableDifferences = (screenshotDiffResults) => {
|
|
5
|
+
// Note: we ignore flakes & missing-bases here
|
|
6
|
+
return screenshotDiffResults.some((diff) => diff.outcome === "diff" ||
|
|
7
|
+
diff.outcome === "missing-head" ||
|
|
8
|
+
diff.outcome === "different-size");
|
|
9
|
+
};
|
|
10
|
+
exports.hasNotableDifferences = hasNotableDifferences;
|
package/dist/config/config.js
CHANGED
|
@@ -35,14 +35,13 @@ const validateReplayOptions = (prevOptions) => {
|
|
|
35
35
|
const validateConfig = (prevConfig) => {
|
|
36
36
|
const { testCases, ...rest } = prevConfig;
|
|
37
37
|
const nextTestCases = (testCases || [])
|
|
38
|
-
.map(({ title, sessionId,
|
|
38
|
+
.map(({ title, sessionId, options }) => ({
|
|
39
39
|
title: typeof title === "string" ? title : "",
|
|
40
40
|
sessionId: typeof sessionId === "string" ? sessionId : "",
|
|
41
|
-
...(typeof baseReplayId === "string" ? { baseReplayId } : {}),
|
|
42
41
|
...(options ? { options: validateReplayOptions(options) } : {}),
|
|
43
42
|
}))
|
|
44
43
|
.map(({ title, ...rest }) => ({
|
|
45
|
-
title: title || `${rest.sessionId}
|
|
44
|
+
title: title || `${rest.sessionId}`,
|
|
46
45
|
...rest,
|
|
47
46
|
}))
|
|
48
47
|
.filter(({ sessionId }) => sessionId);
|
|
@@ -12,5 +12,5 @@ export interface TestCaseResult extends TestCase {
|
|
|
12
12
|
result: "pass" | "fail" | "flake";
|
|
13
13
|
}
|
|
14
14
|
export interface DetailedTestCaseResult extends TestCaseResult {
|
|
15
|
-
|
|
15
|
+
screenshotDiffResultsByBaseReplayId: Record<string, ScreenshotDiffResult[]>;
|
|
16
16
|
}
|
|
@@ -7,16 +7,17 @@ exports.deflakeReplayCommandHandler = void 0;
|
|
|
7
7
|
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
|
-
const
|
|
10
|
+
const has_notable_differences_1 = require("../commands/screenshot-diff/utils/has-notable-differences");
|
|
11
|
+
const handleReplay = async ({ testCase, replayTarget, executionOptions, screenshottingOptions, apiToken, commitSha, generatedBy, testRunId, replayEventsDependencies, suppressScreenshotDiffLogging, baseTestRunId, }) => {
|
|
11
12
|
var _a, _b;
|
|
12
|
-
const { replay,
|
|
13
|
+
const { replay, screenshotDiffResultsByBaseReplayId } = await (0, replay_command_1.replayCommandHandler)({
|
|
13
14
|
replayTarget,
|
|
14
15
|
executionOptions: applyTestCaseExecutionOptionOverrides(executionOptions, (_a = testCase.options) !== null && _a !== void 0 ? _a : {}),
|
|
15
16
|
screenshottingOptions: applyTestCaseScreenshottingOptionsOverrides(screenshottingOptions, (_b = testCase.options) !== null && _b !== void 0 ? _b : {}),
|
|
16
17
|
apiToken,
|
|
17
18
|
commitSha,
|
|
18
19
|
sessionId: testCase.sessionId,
|
|
19
|
-
|
|
20
|
+
baseTestRunId,
|
|
20
21
|
cookiesFile: null,
|
|
21
22
|
generatedBy,
|
|
22
23
|
testRunId,
|
|
@@ -24,11 +25,14 @@ const handleReplay = async ({ testCase, replayTarget, executionOptions, screensh
|
|
|
24
25
|
suppressScreenshotDiffLogging,
|
|
25
26
|
debugger: false,
|
|
26
27
|
});
|
|
28
|
+
const result = (0, has_notable_differences_1.hasNotableDifferences)(Object.values(screenshotDiffResultsByBaseReplayId).flat())
|
|
29
|
+
? "fail"
|
|
30
|
+
: "pass";
|
|
27
31
|
return {
|
|
28
32
|
...testCase,
|
|
29
33
|
headReplayId: replay.id,
|
|
30
|
-
result
|
|
31
|
-
|
|
34
|
+
result,
|
|
35
|
+
screenshotDiffResultsByBaseReplayId,
|
|
32
36
|
};
|
|
33
37
|
};
|
|
34
38
|
const deflakeReplayCommandHandler = async ({ deflake, ...otherOptions }) => {
|
package/dist/index.d.ts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
export {
|
|
1
|
+
export { GetLatestTestRunResultsOptions } from "./api/test-run.api";
|
|
2
2
|
export { bootstrapCommand } from "./commands/bootstrap/bootstrap.command";
|
|
3
3
|
export { createTestCommand } from "./commands/create-test/create-test.command";
|
|
4
4
|
export { downloadReplayCommand } from "./commands/download-replay/download-replay.command";
|
package/dist/index.js
CHANGED
|
@@ -1,8 +1,6 @@
|
|
|
1
1
|
"use strict";
|
|
2
2
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
-
exports.initSentry = exports.setLogLevel = exports.initLogger = exports.runAllTests = exports.updateTestsCommand = exports.showProjectCommand = exports.screenshotDiffCommand = exports.runAllTestsCommand = exports.replayCommand = exports.recordCommand = exports.downloadSessionCommand = exports.downloadReplayCommand = exports.createTestCommand = exports.bootstrapCommand =
|
|
4
|
-
var test_run_api_1 = require("./api/test-run.api");
|
|
5
|
-
Object.defineProperty(exports, "getLatestTestRunResults", { enumerable: true, get: function () { return test_run_api_1.getLatestTestRunResults; } });
|
|
3
|
+
exports.initSentry = exports.setLogLevel = exports.initLogger = exports.runAllTests = exports.updateTestsCommand = exports.showProjectCommand = exports.screenshotDiffCommand = exports.runAllTestsCommand = exports.replayCommand = exports.recordCommand = exports.downloadSessionCommand = exports.downloadReplayCommand = exports.createTestCommand = exports.bootstrapCommand = void 0;
|
|
6
4
|
var bootstrap_command_1 = require("./commands/bootstrap/bootstrap.command");
|
|
7
5
|
Object.defineProperty(exports, "bootstrapCommand", { enumerable: true, get: function () { return bootstrap_command_1.bootstrapCommand; } });
|
|
8
6
|
var create_test_command_1 = require("./commands/create-test/create-test.command");
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { ScreenshotIdentifier } from "@alwaysmeticulous/api";
|
|
1
2
|
import { Replay } from "@alwaysmeticulous/common";
|
|
2
3
|
import { AxiosInstance } from "axios";
|
|
3
4
|
export declare const getOrFetchReplay: (client: AxiosInstance, replayId: string) => Promise<{
|
|
@@ -7,7 +8,11 @@ export declare const getOrFetchReplay: (client: AxiosInstance, replayId: string)
|
|
|
7
8
|
export declare const getOrFetchReplayArchive: (client: AxiosInstance, replayId: string) => Promise<{
|
|
8
9
|
fileName: string;
|
|
9
10
|
}>;
|
|
10
|
-
export
|
|
11
|
+
export interface IdentifiedScreenshotFile {
|
|
12
|
+
identifier: ScreenshotIdentifier;
|
|
13
|
+
fileName: string;
|
|
14
|
+
}
|
|
15
|
+
export declare const getScreenshotFiles: (screenshotsDirPath: string) => Promise<IdentifiedScreenshotFile[]>;
|
|
11
16
|
export declare const getSnapshottedAssetsDir: (replayId: string) => string;
|
|
12
17
|
export declare const getScreenshotsDir: (replayDir: string) => string;
|
|
13
18
|
export declare const getReplayDir: (replayId: string) => string;
|
|
@@ -11,6 +11,7 @@ const adm_zip_1 = __importDefault(require("adm-zip"));
|
|
|
11
11
|
const loglevel_1 = __importDefault(require("loglevel"));
|
|
12
12
|
const download_1 = require("../api/download");
|
|
13
13
|
const replay_api_1 = require("../api/replay.api");
|
|
14
|
+
const get_screenshot_identifier_1 = require("../commands/screenshot-diff/utils/get-screenshot-identifier");
|
|
14
15
|
const local_data_utils_1 = require("./local-data.utils");
|
|
15
16
|
const getOrFetchReplay = async (client, replayId) => {
|
|
16
17
|
const logger = loglevel_1.default.getLogger(common_1.METICULOUS_LOGGER_NAME);
|
|
@@ -62,13 +63,20 @@ exports.getOrFetchReplayArchive = getOrFetchReplayArchive;
|
|
|
62
63
|
const getScreenshotFiles = async (screenshotsDirPath) => {
|
|
63
64
|
const screenshotFiles = [];
|
|
64
65
|
const screenshotsDir = await (0, promises_1.opendir)(screenshotsDirPath);
|
|
66
|
+
const logger = loglevel_1.default.getLogger(common_1.METICULOUS_LOGGER_NAME);
|
|
65
67
|
for await (const dirEntry of screenshotsDir) {
|
|
66
|
-
if (dirEntry.isFile()
|
|
67
|
-
|
|
68
|
+
if (!dirEntry.isFile() || !dirEntry.name.endsWith(".png")) {
|
|
69
|
+
continue;
|
|
70
|
+
}
|
|
71
|
+
const identifier = (0, get_screenshot_identifier_1.getScreenshotIdentifier)(dirEntry.name);
|
|
72
|
+
if (identifier == null) {
|
|
73
|
+
logger.error(`Ignoring screenshot file with unrecognized name pattern: ${dirEntry.name}`);
|
|
74
|
+
continue;
|
|
68
75
|
}
|
|
76
|
+
screenshotFiles.push({ identifier, fileName: dirEntry.name });
|
|
69
77
|
}
|
|
70
78
|
// Sort files alphabetically to help when reading results.
|
|
71
|
-
return screenshotFiles.sort();
|
|
79
|
+
return screenshotFiles.sort((a, b) => a.fileName.localeCompare(b.fileName));
|
|
72
80
|
};
|
|
73
81
|
exports.getScreenshotFiles = getScreenshotFiles;
|
|
74
82
|
const getSnapshottedAssetsDir = (replayId) => (0, path_1.join)((0, exports.getReplayDir)(replayId), "snapshotted-assets");
|
|
@@ -13,7 +13,7 @@ const writeScreenshotDiff = async ({ baseReplayId, headReplayId, screenshotFileN
|
|
|
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}
|
|
16
|
+
const diffFile = (0, path_1.join)(diffDir, `${baseReplayId}-vs-${headReplayId}-${screenshotFileName}.png`);
|
|
17
17
|
await (0, io_utils_1.writePng)(diff, diffFile);
|
|
18
18
|
logger.debug(`Screenshot diff written to ${diffFile}`);
|
|
19
19
|
};
|
|
@@ -20,7 +20,7 @@ async function serveAssetsFromSimulation(client, simulationId) {
|
|
|
20
20
|
const snapshottedAssetsDir = (0, replays_1.getSnapshottedAssetsDir)(simulationId);
|
|
21
21
|
if (!(0, fs_1.existsSync)(snapshottedAssetsDir)) {
|
|
22
22
|
logger.error(`No snapshotted assets found for simulation '${simulationId}'.` +
|
|
23
|
-
" Please re-run without the --simulationIdForAssets
|
|
23
|
+
" Please re-run without the --simulationIdForAssets flag." +
|
|
24
24
|
" You can optionally specify an --appUrl to run the simulation against.");
|
|
25
25
|
process.exit(1);
|
|
26
26
|
}
|
|
@@ -13,7 +13,9 @@ const testResult = (result, screenshotDiffResults, testCase) => {
|
|
|
13
13
|
sessionId: (_a = testCase === null || testCase === void 0 ? void 0 : testCase.sessionId) !== null && _a !== void 0 ? _a : "mock-session-id",
|
|
14
14
|
headReplayId: "mock-head-replay-id",
|
|
15
15
|
result,
|
|
16
|
-
|
|
16
|
+
screenshotDiffResultsByBaseReplayId: {
|
|
17
|
+
"mock-base-replay-id": screenshotDiffResults,
|
|
18
|
+
},
|
|
17
19
|
};
|
|
18
20
|
};
|
|
19
21
|
exports.testResult = testResult;
|
|
@@ -160,7 +160,7 @@ describe("runAllTestsInParallel", () => {
|
|
|
160
160
|
const testCase = (num) => ({
|
|
161
161
|
sessionId: "mock-session-id",
|
|
162
162
|
title: `${num}`,
|
|
163
|
-
|
|
163
|
+
baseTestRunId: "mock-base-test-run-id",
|
|
164
164
|
});
|
|
165
165
|
const expectPromiseToNotHaveResolved = async (promise) => {
|
|
166
166
|
let resolved = false;
|
|
@@ -1,20 +1,25 @@
|
|
|
1
1
|
"use strict";
|
|
2
|
+
var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
3
|
+
return (mod && mod.__esModule) ? mod : { "default": mod };
|
|
4
|
+
};
|
|
2
5
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
6
|
exports.mergeResults = void 0;
|
|
4
7
|
const utils_1 = require("@sentry/utils");
|
|
8
|
+
const fast_json_stable_stringify_1 = __importDefault(require("fast-json-stable-stringify"));
|
|
9
|
+
const screenshot_diff_results_utils_1 = require("./screenshot-diff-results.utils");
|
|
5
10
|
const mergeResults = ({ currentResult, comparisonToHeadReplay, }) => {
|
|
6
11
|
// If any of the screenshots diffs in comparisonToHeadReplay show a diff against one
|
|
7
12
|
// of the screenshots that orignally failed in currentResult then we have a flake
|
|
8
13
|
// on our hands
|
|
9
14
|
const retryDiffById = new Map();
|
|
10
|
-
comparisonToHeadReplay.
|
|
15
|
+
(0, screenshot_diff_results_utils_1.flattenScreenshotDiffResults)(comparisonToHeadReplay).forEach((result) => {
|
|
11
16
|
const hash = hashScreenshotIdentifier(result.identifier);
|
|
12
17
|
if (retryDiffById.has(hash)) {
|
|
13
18
|
throw new Error(`Received two screenshots for the same identifier '${hash}'. Screenshots should be unique.`);
|
|
14
19
|
}
|
|
15
20
|
retryDiffById.set(hash, result);
|
|
16
21
|
});
|
|
17
|
-
const newScreenshotDiffResults = currentResult.
|
|
22
|
+
const newScreenshotDiffResults = (0, screenshot_diff_results_utils_1.flattenScreenshotDiffResults)(currentResult).map((currentDiff) => {
|
|
18
23
|
if (currentDiff.outcome === "no-diff") {
|
|
19
24
|
return currentDiff;
|
|
20
25
|
}
|
|
@@ -53,7 +58,7 @@ const mergeResults = ({ currentResult, comparisonToHeadReplay, }) => {
|
|
|
53
58
|
...currentDiff,
|
|
54
59
|
diffsToHeadScreenshotOnRetries: [
|
|
55
60
|
...currentDiff.diffsToHeadScreenshotOnRetries,
|
|
56
|
-
|
|
61
|
+
withoutIdentifiers(diffWhenRetrying),
|
|
57
62
|
],
|
|
58
63
|
};
|
|
59
64
|
}
|
|
@@ -62,10 +67,11 @@ const mergeResults = ({ currentResult, comparisonToHeadReplay, }) => {
|
|
|
62
67
|
}
|
|
63
68
|
else {
|
|
64
69
|
return {
|
|
70
|
+
baseReplayId: currentDiff.baseReplayId,
|
|
65
71
|
identifier: currentDiff.identifier,
|
|
66
72
|
outcome: "flake",
|
|
67
|
-
diffToBaseScreenshot:
|
|
68
|
-
diffsToHeadScreenshotOnRetries: [
|
|
73
|
+
diffToBaseScreenshot: withoutIdentifiers(currentDiff),
|
|
74
|
+
diffsToHeadScreenshotOnRetries: [withoutIdentifiers(diffWhenRetrying)],
|
|
69
75
|
};
|
|
70
76
|
}
|
|
71
77
|
});
|
|
@@ -75,12 +81,15 @@ const mergeResults = ({ currentResult, comparisonToHeadReplay, }) => {
|
|
|
75
81
|
result: currentResult.result === "fail" && noLongerHasFailures
|
|
76
82
|
? "flake"
|
|
77
83
|
: currentResult.result,
|
|
78
|
-
|
|
84
|
+
screenshotDiffResultsByBaseReplayId: (0, screenshot_diff_results_utils_1.groupScreenshotDiffResults)(newScreenshotDiffResults),
|
|
79
85
|
};
|
|
80
86
|
};
|
|
81
87
|
exports.mergeResults = mergeResults;
|
|
82
|
-
const
|
|
83
|
-
|
|
88
|
+
const withoutIdentifiers = ({
|
|
89
|
+
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
|
90
|
+
identifier: _identifier,
|
|
91
|
+
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
|
92
|
+
baseReplayId: _baseReplayId, ...rest }) => rest;
|
|
84
93
|
const hashScreenshotIdentifier = (identifier) => {
|
|
85
94
|
if (identifier.type === "end-state") {
|
|
86
95
|
return "end-state";
|
|
@@ -93,7 +102,7 @@ const hashScreenshotIdentifier = (identifier) => {
|
|
|
93
102
|
// The identifier is probably from a newer version of the bundle script
|
|
94
103
|
// and we're on an old version of the CLI. Our best bet is to stringify it
|
|
95
104
|
// and use that as a hash.
|
|
96
|
-
return
|
|
105
|
+
return (0, fast_json_stable_stringify_1.default)(identifier);
|
|
97
106
|
}
|
|
98
107
|
};
|
|
99
108
|
const unknownScreenshotIdentifierType = (identifier) => {
|
|
@@ -8,6 +8,7 @@ const os_1 = require("os");
|
|
|
8
8
|
const common_1 = require("@alwaysmeticulous/common");
|
|
9
9
|
const loglevel_1 = __importDefault(require("loglevel"));
|
|
10
10
|
const merge_test_results_1 = require("./merge-test-results");
|
|
11
|
+
const screenshot_diff_results_utils_1 = require("./screenshot-diff-results.utils");
|
|
11
12
|
/** Handler for running Meticulous tests in parallel using child processes */
|
|
12
13
|
const runAllTestsInParallel = async ({ testsToRun, parallelTasks, maxRetriesOnFailure, executeTest, onTestFinished, onTestFailedToRun, }) => {
|
|
13
14
|
const logger = loglevel_1.default.getLogger(common_1.METICULOUS_LOGGER_NAME);
|
|
@@ -134,7 +135,7 @@ const logRetrySummary = (nameOfTest, retryResult) => {
|
|
|
134
135
|
logger.info(`Retried taking screenshots for failed test ${nameOfTest}, but got the same results`);
|
|
135
136
|
}
|
|
136
137
|
else {
|
|
137
|
-
const numDifferingScreenshots = retryResult.
|
|
138
|
+
const numDifferingScreenshots = (0, screenshot_diff_results_utils_1.flattenScreenshotDiffResults)(retryResult).filter((result) => result.outcome !== "no-diff").length;
|
|
138
139
|
logger.info(`Retried taking screenshots for failed test ${nameOfTest}, and ${numDifferingScreenshots} screenshots came out different. Results for these screenshots are assumed to be flakes, and so will be ignored.`);
|
|
139
140
|
}
|
|
140
141
|
};
|
|
@@ -19,7 +19,6 @@ export interface Options {
|
|
|
19
19
|
*/
|
|
20
20
|
baseCommitSha: string | null;
|
|
21
21
|
appUrl: string | null;
|
|
22
|
-
useAssetsSnapshottedInBaseSimulation: boolean;
|
|
23
22
|
/**
|
|
24
23
|
* If null runs in parralel with a sensible number of parrelel tasks for the given machine.
|
|
25
24
|
*
|
|
@@ -44,6 +43,7 @@ export interface Options {
|
|
|
44
43
|
* Captured environment for this run
|
|
45
44
|
*/
|
|
46
45
|
environment?: TestRunEnvironment;
|
|
46
|
+
baseTestRunId: string | null;
|
|
47
47
|
onTestRunCreated?: (testRun: RunAllTestsTestRun & {
|
|
48
48
|
status: "Running";
|
|
49
49
|
}) => void;
|
|
@@ -61,4 +61,4 @@ export interface RunAllTestsResult {
|
|
|
61
61
|
* Runs all the test cases in the provided file.
|
|
62
62
|
* @returns The results of the tests that were executed (note that this does not include results from any cachedTestRunResults passed in)
|
|
63
63
|
*/
|
|
64
|
-
export declare const runAllTests: ({ testsFile, apiToken, commitSha, baseCommitSha, appUrl,
|
|
64
|
+
export declare const runAllTests: ({ testsFile, apiToken, commitSha, baseCommitSha, appUrl, executionOptions, screenshottingOptions, parallelTasks, deflake, maxRetriesOnFailure, cachedTestRunResults: cachedTestRunResults_, githubSummary, environment, baseTestRunId, onTestRunCreated, onTestFinished: onTestFinished_, }: Options) => Promise<RunAllTestsResult>;
|