@alwaysmeticulous/cli 2.19.3 → 2.20.1

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.
Files changed (33) hide show
  1. package/dist/api/replay.api.d.ts +1 -1
  2. package/dist/api/session.api.d.ts +2 -1
  3. package/dist/commands/replay/replay.command.d.ts +3 -2
  4. package/dist/commands/replay/replay.command.js +46 -22
  5. package/dist/commands/replay/utils/{compute-and-save-diff.d.ts → compute-diff.d.ts} +3 -2
  6. package/dist/commands/replay/utils/{compute-and-save-diff.js → compute-diff.js} +5 -21
  7. package/dist/commands/run-all-tests/run-all-tests.command.d.ts +5 -0
  8. package/dist/commands/run-all-tests/run-all-tests.command.js +7 -1
  9. package/dist/commands/screenshot-diff/screenshot-diff.command.d.ts +5 -2
  10. package/dist/commands/screenshot-diff/screenshot-diff.command.js +23 -18
  11. package/dist/config/config.types.d.ts +6 -1
  12. package/dist/deflake-tests/deflake-tests.handler.d.ts +1 -0
  13. package/dist/deflake-tests/deflake-tests.handler.js +2 -2
  14. package/dist/local-data/local-data.utils.d.ts +19 -0
  15. package/dist/local-data/local-data.utils.js +95 -1
  16. package/dist/local-data/replays.d.ts +2 -1
  17. package/dist/local-data/replays.js +30 -32
  18. package/dist/local-data/sessions.d.ts +2 -1
  19. package/dist/local-data/sessions.js +12 -27
  20. package/dist/main.js +1 -7
  21. package/dist/parallel-tests/__tests__/merge-test-results.spec.d.ts +1 -0
  22. package/dist/parallel-tests/__tests__/merge-test-results.spec.js +191 -0
  23. package/dist/parallel-tests/merge-test-results.d.ts +10 -0
  24. package/dist/parallel-tests/merge-test-results.js +101 -0
  25. package/dist/parallel-tests/parallel-tests.handler.d.ts +1 -0
  26. package/dist/parallel-tests/parallel-tests.handler.js +113 -24
  27. package/dist/parallel-tests/run-all-tests.d.ts +7 -1
  28. package/dist/parallel-tests/run-all-tests.js +35 -52
  29. package/dist/parallel-tests/run-all-tests.types.d.ts +1 -0
  30. package/dist/tsconfig.tsbuildinfo +1 -1
  31. package/dist/utils/sentry.utils.d.ts +0 -1
  32. package/dist/utils/sentry.utils.js +19 -14
  33. package/package.json +26 -6
@@ -11,25 +11,19 @@ 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 local_data_utils_1 = require("./local-data.utils");
14
15
  const getOrFetchReplay = async (client, replayId) => {
15
16
  const logger = loglevel_1.default.getLogger(common_1.METICULOUS_LOGGER_NAME);
16
- const replayDir = (0, exports.getReplayDir)(replayId);
17
- await (0, promises_1.mkdir)(replayDir, { recursive: true });
18
- const replayFile = (0, path_1.join)(replayDir, `${replayId}.json`);
19
- const existingReplay = await (0, promises_1.readFile)(replayFile)
20
- .then((data) => JSON.parse(data.toString("utf-8")))
21
- .catch(() => null);
22
- if (existingReplay) {
23
- logger.debug(`Reading replay from local copy in ${replayFile}`);
24
- return existingReplay;
25
- }
26
- const replay = await (0, replay_api_1.getReplay)(client, replayId);
17
+ const replayFile = (0, path_1.join)((0, exports.getReplayDir)(replayId), `${replayId}.json`);
18
+ const replay = await (0, local_data_utils_1.getOrDownloadJsonFile)({
19
+ filePath: replayFile,
20
+ dataDescription: "replay",
21
+ downloadJson: () => (0, replay_api_1.getReplay)(client, replayId),
22
+ });
27
23
  if (!replay) {
28
24
  logger.error(`Error: Could not retrieve replay with id "${replayId}". Is the API token correct?`);
29
25
  process.exit(1);
30
26
  }
31
- await (0, promises_1.writeFile)(replayFile, JSON.stringify(replay, null, 2));
32
- logger.debug(`Wrote replay to ${replayFile}`);
33
27
  return replay;
34
28
  };
35
29
  exports.getOrFetchReplay = getOrFetchReplay;
@@ -37,27 +31,31 @@ const getOrFetchReplayArchive = async (client, replayId) => {
37
31
  const logger = loglevel_1.default.getLogger(common_1.METICULOUS_LOGGER_NAME);
38
32
  const replayDir = (0, exports.getReplayDir)(replayId);
39
33
  await (0, promises_1.mkdir)(replayDir, { recursive: true });
40
- const replayArchiveFile = (0, path_1.join)(replayDir, `${replayId}.zip`);
41
- const paramsFile = (0, path_1.join)(replayDir, "replayEventsParams.json");
42
- // Check if "replayEventsParams.json" exists. If yes, we assume the replay
43
- // zip archive has been downloaded and extracted.
44
- const paramsFileExists = await (0, promises_1.access)(paramsFile)
45
- .then(() => true)
46
- .catch(() => false);
47
- if (paramsFileExists) {
48
- logger.debug(`Replay archive already downloaded at ${replayDir}`);
49
- return;
34
+ const releaseLock = await (0, local_data_utils_1.waitToAcquireLockOnDirectory)(replayDir);
35
+ try {
36
+ const replayArchiveFile = (0, path_1.join)(replayDir, `${replayId}.zip`);
37
+ const paramsFile = (0, path_1.join)(replayDir, "replayEventsParams.json");
38
+ // Check if "replayEventsParams.json" exists. If yes, we assume the replay
39
+ // zip archive has been downloaded and extracted.
40
+ if (await (0, local_data_utils_1.fileExists)(paramsFile)) {
41
+ logger.debug(`Replay archive already downloaded at ${replayDir}`);
42
+ return;
43
+ }
44
+ const downloadUrlData = await (0, replay_api_1.getReplayDownloadUrl)(client, replayId);
45
+ if (!downloadUrlData) {
46
+ logger.error("Error: Could not retrieve replay archive URL. This may be an invalid replay");
47
+ await releaseLock();
48
+ process.exit(1);
49
+ }
50
+ await (0, download_1.downloadFile)(downloadUrlData.dowloadUrl, replayArchiveFile);
51
+ const zipFile = new adm_zip_1.default(replayArchiveFile);
52
+ zipFile.extractAllTo(replayDir, /*overwrite=*/ true);
53
+ await (0, promises_1.rm)(replayArchiveFile);
54
+ logger.debug(`Extracted replay archive in ${replayDir}`);
50
55
  }
51
- const downloadUrlData = await (0, replay_api_1.getReplayDownloadUrl)(client, replayId);
52
- if (!downloadUrlData) {
53
- logger.error("Error: Could not retrieve replay archive URL. This may be an invalid replay");
54
- process.exit(1);
56
+ finally {
57
+ await releaseLock();
55
58
  }
56
- await (0, download_1.downloadFile)(downloadUrlData.dowloadUrl, replayArchiveFile);
57
- const zipFile = new adm_zip_1.default(replayArchiveFile);
58
- zipFile.extractAllTo(replayDir, /*overwrite=*/ true);
59
- await (0, promises_1.rm)(replayArchiveFile);
60
- logger.debug(`Exrtracted replay archive in ${replayDir}`);
61
59
  };
62
60
  exports.getOrFetchReplayArchive = getOrFetchReplayArchive;
63
61
  const getScreenshotFiles = async (screenshotsDirPath) => {
@@ -1,3 +1,4 @@
1
+ import { SessionData } from "@alwaysmeticulous/api";
1
2
  import { AxiosInstance } from "axios";
2
3
  export declare const getOrFetchRecordedSession: (client: AxiosInstance, sessionId: string) => Promise<any>;
3
- export declare const getOrFetchRecordedSessionData: (client: AxiosInstance, sessionId: string) => Promise<any>;
4
+ export declare const getOrFetchRecordedSessionData: (client: AxiosInstance, sessionId: string) => Promise<SessionData>;
@@ -4,7 +4,6 @@ var __importDefault = (this && this.__importDefault) || function (mod) {
4
4
  };
5
5
  Object.defineProperty(exports, "__esModule", { value: true });
6
6
  exports.getOrFetchRecordedSessionData = exports.getOrFetchRecordedSession = void 0;
7
- const promises_1 = require("fs/promises");
8
7
  const path_1 = require("path");
9
8
  const common_1 = require("@alwaysmeticulous/common");
10
9
  const loglevel_1 = __importDefault(require("loglevel"));
@@ -12,45 +11,31 @@ const session_api_1 = require("../api/session.api");
12
11
  const local_data_utils_1 = require("./local-data.utils");
13
12
  const getOrFetchRecordedSession = async (client, sessionId) => {
14
13
  const logger = loglevel_1.default.getLogger(common_1.METICULOUS_LOGGER_NAME);
15
- const sessionsDir = (0, path_1.join)((0, common_1.getMeticulousLocalDataDir)(), "sessions");
16
- await (0, promises_1.mkdir)(sessionsDir, { recursive: true });
17
- const sessionFile = (0, path_1.join)(sessionsDir, `${(0, local_data_utils_1.sanitizeFilename)(sessionId)}.json`);
18
- const existingSession = await (0, promises_1.readFile)(sessionFile)
19
- .then((data) => JSON.parse(data.toString("utf-8")))
20
- .catch(() => null);
21
- if (existingSession) {
22
- logger.debug(`Reading session from local copy in ${sessionFile}`);
23
- return existingSession;
24
- }
25
- const session = await (0, session_api_1.getRecordedSession)(client, sessionId);
14
+ const sessionFile = (0, path_1.join)((0, common_1.getMeticulousLocalDataDir)(), "sessions", `${(0, local_data_utils_1.sanitizeFilename)(sessionId)}.json`);
15
+ const session = await (0, local_data_utils_1.getOrDownloadJsonFile)({
16
+ filePath: sessionFile,
17
+ dataDescription: "session",
18
+ downloadJson: () => (0, session_api_1.getRecordedSession)(client, sessionId),
19
+ });
26
20
  if (!session) {
27
21
  logger.error("Error: Could not retrieve session. Is the API token correct?");
28
22
  process.exit(1);
29
23
  }
30
- await (0, promises_1.writeFile)(sessionFile, JSON.stringify(session, null, 2));
31
- logger.debug(`Wrote session to ${sessionFile}`);
32
24
  return session;
33
25
  };
34
26
  exports.getOrFetchRecordedSession = getOrFetchRecordedSession;
35
27
  const getOrFetchRecordedSessionData = async (client, sessionId) => {
36
28
  const logger = loglevel_1.default.getLogger(common_1.METICULOUS_LOGGER_NAME);
37
- const sessionsDir = (0, path_1.join)((0, common_1.getMeticulousLocalDataDir)(), "sessions");
38
- await (0, promises_1.mkdir)(sessionsDir, { recursive: true });
39
- const sessionDataFile = (0, path_1.join)(sessionsDir, `${(0, local_data_utils_1.sanitizeFilename)(sessionId)}_data.json`);
40
- const existingSessionData = await (0, promises_1.readFile)(sessionDataFile)
41
- .then((data) => JSON.parse(data.toString("utf-8")))
42
- .catch(() => null);
43
- if (existingSessionData) {
44
- logger.debug(`Reading session data from local copy in ${sessionDataFile}`);
45
- return existingSessionData;
46
- }
47
- const sessionData = await (0, session_api_1.getRecordedSessionData)(client, sessionId);
29
+ const sessionFile = (0, path_1.join)((0, common_1.getMeticulousLocalDataDir)(), "sessions", `${(0, local_data_utils_1.sanitizeFilename)(sessionId)}_data.json`);
30
+ const sessionData = await (0, local_data_utils_1.getOrDownloadJsonFile)({
31
+ filePath: sessionFile,
32
+ dataDescription: "session data",
33
+ downloadJson: () => (0, session_api_1.getRecordedSessionData)(client, sessionId),
34
+ });
48
35
  if (!sessionData) {
49
36
  logger.error("Error: Could not retrieve session data. This may be an invalid session");
50
37
  process.exit(1);
51
38
  }
52
- await (0, promises_1.writeFile)(sessionDataFile, JSON.stringify(sessionData, null, 2));
53
- logger.debug(`Wrote session data to ${sessionDataFile}`);
54
39
  return sessionData;
55
40
  };
56
41
  exports.getOrFetchRecordedSessionData = getOrFetchRecordedSessionData;
package/dist/main.js CHANGED
@@ -28,7 +28,7 @@ const main = async () => {
28
28
  (0, logger_utils_1.initLogger)();
29
29
  const meticulousVersion = await (0, version_utils_1.getMeticulousVersion)();
30
30
  (0, sentry_utils_1.initSentry)(meticulousVersion);
31
- const promise = yargs_1.default
31
+ yargs_1.default
32
32
  .scriptName("meticulous")
33
33
  .usage(`$0 <command>
34
34
 
@@ -63,12 +63,6 @@ const main = async () => {
63
63
  (argv) => handleDataDir(argv.dataDir),
64
64
  (argv) => (0, sentry_utils_1.setOptions)(argv),
65
65
  ]).argv;
66
- if (promise instanceof Promise) {
67
- promise.catch((error) => {
68
- console.error(error);
69
- process.exit(1);
70
- });
71
- }
72
66
  };
73
67
  exports.main = main;
74
68
  (0, exports.main)();
@@ -0,0 +1,191 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ const merge_test_results_1 = require("../merge-test-results");
4
+ describe("mergeResults", () => {
5
+ it("keeps the result as a failure when all retried screenshots are 'identical'", () => {
6
+ const currentResult = testResult("fail", [diff(0), noDiff(1)]);
7
+ const comparisonToHeadReplay = testResult("pass", [noDiff(0), noDiff(1)]);
8
+ const mergedResult = (0, merge_test_results_1.mergeResults)({
9
+ currentResult,
10
+ comparisonToHeadReplay,
11
+ });
12
+ expect(mergedResult).toEqual(testResult("fail", [diff(0), noDiff(1)]));
13
+ });
14
+ it("ignores diffs to screenshots which originally passed", () => {
15
+ const currentResult = testResult("pass", [noDiff(0), noDiff(1)]);
16
+ const comparisonToHeadReplay = testResult("fail", [noDiff(0), diff(1)]);
17
+ const mergedResult = (0, merge_test_results_1.mergeResults)({
18
+ currentResult,
19
+ comparisonToHeadReplay,
20
+ });
21
+ expect(mergedResult).toEqual(testResult("pass", [noDiff(0), noDiff(1)]));
22
+ });
23
+ it("marks screenshots as flakes if the screenshot comparison originally failed, but the second retry gives a different screenshot again", () => {
24
+ const currentResult = testResult("fail", [noDiff(0), diff(1), diff(2)]);
25
+ const comparisonToHeadReplay = testResult("fail", [
26
+ noDiff(0),
27
+ diff(1),
28
+ noDiff(2),
29
+ ]);
30
+ const mergedResult = (0, merge_test_results_1.mergeResults)({
31
+ currentResult,
32
+ comparisonToHeadReplay,
33
+ });
34
+ expect(mergedResult).toEqual(testResult("fail", [noDiff(0), flake(1, diff(), [diff()]), diff(2)]));
35
+ });
36
+ it("marks overall test as a flake if there are only flakey screenshots and no failed screenshots", () => {
37
+ const currentResult = testResult("fail", [noDiff(0), diff(1), diff(2)]);
38
+ const comparisonToHeadReplay = testResult("fail", [
39
+ noDiff(0),
40
+ diff(1),
41
+ diff(2),
42
+ ]);
43
+ const mergedResult = (0, merge_test_results_1.mergeResults)({
44
+ currentResult,
45
+ comparisonToHeadReplay,
46
+ });
47
+ expect(mergedResult).toEqual(testResult("flake", [
48
+ noDiff(0),
49
+ flake(1, diff(), [diff()]),
50
+ flake(2, diff(), [diff()]),
51
+ ]));
52
+ });
53
+ it("adds to diffsToHeadScreenshotOnRetries for existing flakes", () => {
54
+ const currentResult = testResult("fail", [
55
+ diff(0),
56
+ flake(1, diff(), [missingHead()]),
57
+ flake(2, differentSize(), [diff()]),
58
+ flake(3, missingBase(), [diff(), diff()]),
59
+ ]);
60
+ const comparisonToHeadReplay = testResult("fail", [
61
+ noDiff(0),
62
+ diff(1),
63
+ differentSize(2),
64
+ missingHead(3),
65
+ ]);
66
+ const mergedResult = (0, merge_test_results_1.mergeResults)({
67
+ currentResult,
68
+ comparisonToHeadReplay,
69
+ });
70
+ expect(mergedResult).toEqual(testResult("fail", [
71
+ diff(0),
72
+ flake(1, diff(), [missingHead(), diff()]),
73
+ flake(2, differentSize(), [diff(), differentSize()]),
74
+ flake(3, missingBase(), [diff(), diff(), missingHead()]),
75
+ ]));
76
+ });
77
+ it("keeps a missing-head as is, if there is no corresponding retry screenshot", () => {
78
+ const currentResult = testResult("fail", [missingHead(0)]);
79
+ const comparisonToHeadReplay = testResult("pass", []);
80
+ const mergedResult = (0, merge_test_results_1.mergeResults)({
81
+ currentResult,
82
+ comparisonToHeadReplay,
83
+ });
84
+ expect(mergedResult).toEqual(testResult("fail", [missingHead(0)]));
85
+ });
86
+ it("adds to diffsToHeadScreenshotOnRetries for a flakey missing-head, if there is no corresponding retry screenshot", () => {
87
+ const currentResult = testResult("fail", [
88
+ flake(0, missingHead(), [missingBase()]),
89
+ diff(1),
90
+ ]);
91
+ const comparisonToHeadReplay = testResult("pass", [noDiff(1)]);
92
+ const mergedResult = (0, merge_test_results_1.mergeResults)({
93
+ currentResult,
94
+ comparisonToHeadReplay,
95
+ });
96
+ expect(mergedResult).toEqual(testResult("fail", [
97
+ flake(0, missingHead(), [missingBase(), missingBaseAndHead()]),
98
+ diff(1),
99
+ ]));
100
+ });
101
+ });
102
+ const id = (eventNumber = 0) => ({
103
+ type: "after-event",
104
+ eventNumber,
105
+ });
106
+ const testResult = (result, screenshotDiffResults) => {
107
+ return {
108
+ sessionId: "mock-session-id",
109
+ headReplayId: "mock-head-replay-id",
110
+ result,
111
+ screenshotDiffResults,
112
+ };
113
+ };
114
+ const diff = (eventNumber) => {
115
+ return {
116
+ identifier: id(eventNumber),
117
+ outcome: "diff",
118
+ headScreenshotFile: "mock-head-file",
119
+ baseScreenshotFile: "mock-base-file",
120
+ width: 1920,
121
+ height: 1080,
122
+ mismatchFraction: 0.01,
123
+ mismatchPixels: 1000,
124
+ };
125
+ };
126
+ const noDiff = (eventNumber) => {
127
+ return {
128
+ identifier: id(eventNumber),
129
+ outcome: "no-diff",
130
+ headScreenshotFile: "mock-head-file",
131
+ baseScreenshotFile: "mock-base-file",
132
+ width: 1920,
133
+ height: 1080,
134
+ mismatchFraction: 0.01,
135
+ mismatchPixels: 1000,
136
+ };
137
+ };
138
+ const flake = (eventNumber, diffToBaseScreenshot, diffsToHeadScreenshotOnRetries) => {
139
+ return {
140
+ identifier: id(eventNumber),
141
+ outcome: "flake",
142
+ diffToBaseScreenshot: asSingleTryDiff(diffToBaseScreenshot),
143
+ diffsToHeadScreenshotOnRetries: diffsToHeadScreenshotOnRetries.map(asRetryDiff),
144
+ };
145
+ };
146
+ const missingBase = (eventNumber) => {
147
+ return {
148
+ identifier: id(eventNumber),
149
+ outcome: "missing-base",
150
+ headScreenshotFile: "mock-head-file",
151
+ };
152
+ };
153
+ const missingHead = (eventNumber) => {
154
+ return {
155
+ identifier: id(eventNumber),
156
+ outcome: "missing-head",
157
+ baseScreenshotFile: "mock-base-file",
158
+ };
159
+ };
160
+ const missingBaseAndHead = (eventNumber) => {
161
+ return {
162
+ identifier: id(eventNumber),
163
+ outcome: "missing-base-and-head",
164
+ };
165
+ };
166
+ const differentSize = (eventNumber) => {
167
+ return {
168
+ identifier: id(eventNumber),
169
+ outcome: "different-size",
170
+ baseScreenshotFile: "mock-base-file",
171
+ headScreenshotFile: "mock-head-file",
172
+ baseWidth: 1920,
173
+ baseHeight: 1080,
174
+ headWidth: 10,
175
+ headHeight: 5,
176
+ };
177
+ };
178
+ const asSingleTryDiff = ({ identifier, // eslint-disable-line @typescript-eslint/no-unused-vars
179
+ ...rest }) => {
180
+ if (rest.outcome === "flake") {
181
+ throw new Error("Must not be a diff with a flake outcome");
182
+ }
183
+ return rest;
184
+ };
185
+ const asRetryDiff = ({ identifier, // eslint-disable-line @typescript-eslint/no-unused-vars
186
+ ...rest }) => {
187
+ if (rest.outcome === "flake") {
188
+ throw new Error("Must not be a diff with a flake outcome");
189
+ }
190
+ return rest;
191
+ };
@@ -0,0 +1,10 @@
1
+ import { DetailedTestCaseResult } from "../config/config.types";
2
+ export interface ResultsToMerge {
3
+ currentResult: DetailedTestCaseResult;
4
+ /**
5
+ * The result of replaying the session once more against the head commit and
6
+ * comparing the screenshots to those taken on the first replay against the head commit.
7
+ */
8
+ comparisonToHeadReplay: DetailedTestCaseResult;
9
+ }
10
+ export declare const mergeResults: ({ currentResult, comparisonToHeadReplay, }: ResultsToMerge) => DetailedTestCaseResult;
@@ -0,0 +1,101 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.mergeResults = void 0;
4
+ const utils_1 = require("@sentry/utils");
5
+ const mergeResults = ({ currentResult, comparisonToHeadReplay, }) => {
6
+ // If any of the screenshots diffs in comparisonToHeadReplay show a diff against one
7
+ // of the screenshots that orignally failed in currentResult then we have a flake
8
+ // on our hands
9
+ const retryDiffById = new Map();
10
+ comparisonToHeadReplay.screenshotDiffResults.forEach((result) => {
11
+ const hash = hashScreenshotIdentifier(result.identifier);
12
+ if (retryDiffById.has(hash)) {
13
+ throw new Error(`Received two screenshots for the same identifier '${hash}'. Screenshots should be unique.`);
14
+ }
15
+ retryDiffById.set(hash, result);
16
+ });
17
+ const newScreenshotDiffResults = currentResult.screenshotDiffResults.map((currentDiff) => {
18
+ if (currentDiff.outcome === "no-diff") {
19
+ return currentDiff;
20
+ }
21
+ const hash = hashScreenshotIdentifier(currentDiff.identifier);
22
+ const diffWhenRetrying = retryDiffById.get(hash);
23
+ // diffWhenRetrying is null in the case that there is a base screenshot for the base replay,
24
+ // but the first replay on head did not generate a head screenshot (got 'missing-head'),
25
+ // and the replay of that did not generate a head screenshot either (if the first replay on head
26
+ // did generate a screenshot, then diffWhenRetrying.outcome would be 'missing-head' instead)
27
+ if (diffWhenRetrying == null) {
28
+ const diffToBaseScreenshotOutcome = currentDiff.outcome === "flake"
29
+ ? currentDiff.diffToBaseScreenshot.outcome
30
+ : currentDiff.outcome;
31
+ if (diffToBaseScreenshotOutcome !== "missing-head") {
32
+ throw new Error(`Expected to find a screenshot comparison for ${hash}, but none was found. The screenshot must
33
+ have existed in the orginal replay, since the orginal comparison outcome was not '${diffToBaseScreenshotOutcome}'.`);
34
+ }
35
+ if (currentDiff.outcome === "flake") {
36
+ // Original comparison had missing-head, so we re-ran to check if it was flakey. In this case
37
+ // we got the same result.
38
+ return {
39
+ ...currentDiff,
40
+ diffsToHeadScreenshotOnRetries: [
41
+ ...currentDiff.diffsToHeadScreenshotOnRetries,
42
+ { outcome: "missing-base-and-head" },
43
+ ],
44
+ };
45
+ }
46
+ return currentDiff; // no difference, both screenshots were missing
47
+ }
48
+ if (diffWhenRetrying.outcome === "flake") {
49
+ throw new Error("Expected diffs when retrying and comparing to the original head screenshot to be first-try diffs, but got a flake.");
50
+ }
51
+ if (currentDiff.outcome === "flake") {
52
+ return {
53
+ ...currentDiff,
54
+ diffsToHeadScreenshotOnRetries: [
55
+ ...currentDiff.diffsToHeadScreenshotOnRetries,
56
+ withoutIdentifier(diffWhenRetrying),
57
+ ],
58
+ };
59
+ }
60
+ else if ((diffWhenRetrying === null || diffWhenRetrying === void 0 ? void 0 : diffWhenRetrying.outcome) === "no-diff") {
61
+ return currentDiff;
62
+ }
63
+ else {
64
+ return {
65
+ identifier: currentDiff.identifier,
66
+ outcome: "flake",
67
+ diffToBaseScreenshot: withoutIdentifier(currentDiff),
68
+ diffsToHeadScreenshotOnRetries: [withoutIdentifier(diffWhenRetrying)],
69
+ };
70
+ }
71
+ });
72
+ const noLongerHasFailures = newScreenshotDiffResults.every(({ outcome }) => outcome === "flake" || outcome === "no-diff");
73
+ return {
74
+ ...currentResult,
75
+ result: currentResult.result === "fail" && noLongerHasFailures
76
+ ? "flake"
77
+ : currentResult.result,
78
+ screenshotDiffResults: newScreenshotDiffResults,
79
+ };
80
+ };
81
+ exports.mergeResults = mergeResults;
82
+ const withoutIdentifier = ({ identifier, // eslint-disable-line @typescript-eslint/no-unused-vars
83
+ ...rest }) => rest;
84
+ const hashScreenshotIdentifier = (identifier) => {
85
+ if (identifier.type === "end-state") {
86
+ return "end-state";
87
+ }
88
+ else if (identifier.type === "after-event") {
89
+ return `after-event-${identifier.eventNumber}`;
90
+ }
91
+ else {
92
+ unknownScreenshotIdentifierType(identifier);
93
+ // The identifier is probably from a newer version of the bundle script
94
+ // and we're on an old version of the CLI. Our best bet is to stringify it
95
+ // and use that as a hash.
96
+ return JSON.stringify(identifier);
97
+ }
98
+ };
99
+ const unknownScreenshotIdentifierType = (identifier) => {
100
+ utils_1.logger.error(`Unknown type of screenshot identifier: ${JSON.stringify(identifier)}`);
101
+ };
@@ -17,6 +17,7 @@ export interface RunAllTestsInParallelOptions {
17
17
  parallelTasks: number | null;
18
18
  deflake: boolean;
19
19
  replayEventsDependencies: ReplayEventsDependencies;
20
+ maxRetriesOnFailure: number;
20
21
  onTestFinished?: (progress: TestRunProgress, resultsSoFar: DetailedTestCaseResult[]) => Promise<void>;
21
22
  }
22
23
  /** Handler for running Meticulous tests in parallel using child processes */