@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.
Files changed (45) hide show
  1. package/dist/api/replay.api.d.ts +0 -1
  2. package/dist/api/replay.api.js +1 -13
  3. package/dist/api/test-run.api.d.ts +8 -2
  4. package/dist/api/test-run.api.js +21 -3
  5. package/dist/api/types.d.ts +9 -0
  6. package/dist/commands/create-test/create-test.command.js +0 -3
  7. package/dist/commands/replay/replay.command.d.ts +5 -20
  8. package/dist/commands/replay/replay.command.js +10 -53
  9. package/dist/commands/replay/utils/compute-diff.d.ts +3 -6
  10. package/dist/commands/replay/utils/compute-diff.js +83 -18
  11. package/dist/commands/replay/utils/exit-early-if-skip-upload-env-var-set.d.ts +1 -1
  12. package/dist/commands/replay/utils/exit-early-if-skip-upload-env-var-set.js +3 -3
  13. package/dist/commands/run-all-tests/run-all-tests.command.d.ts +4 -5
  14. package/dist/commands/run-all-tests/run-all-tests.command.js +6 -9
  15. package/dist/commands/screenshot-diff/screenshot-diff.command.d.ts +6 -8
  16. package/dist/commands/screenshot-diff/screenshot-diff.command.js +50 -97
  17. package/dist/commands/screenshot-diff/utils/get-screenshot-filename.d.ts +2 -0
  18. package/dist/commands/screenshot-diff/utils/get-screenshot-filename.js +18 -0
  19. package/dist/commands/screenshot-diff/utils/get-screenshot-identifier.d.ts +2 -0
  20. package/dist/commands/screenshot-diff/utils/get-screenshot-identifier.js +24 -0
  21. package/dist/commands/screenshot-diff/utils/has-notable-differences.d.ts +2 -0
  22. package/dist/commands/screenshot-diff/utils/has-notable-differences.js +10 -0
  23. package/dist/config/config.js +2 -3
  24. package/dist/config/config.types.d.ts +1 -1
  25. package/dist/deflake-tests/deflake-tests.handler.d.ts +1 -0
  26. package/dist/deflake-tests/deflake-tests.handler.js +9 -5
  27. package/dist/index.d.ts +1 -1
  28. package/dist/index.js +1 -3
  29. package/dist/local-data/replays.d.ts +6 -1
  30. package/dist/local-data/replays.js +11 -3
  31. package/dist/local-data/screenshot-diffs.js +1 -1
  32. package/dist/local-data/serve-assets-from-simulation.js +1 -1
  33. package/dist/parallel-tests/__tests__/mock-test-results.js +3 -1
  34. package/dist/parallel-tests/__tests__/parrallel-tests.handler.spec.js +1 -1
  35. package/dist/parallel-tests/merge-test-results.js +18 -9
  36. package/dist/parallel-tests/parallel-tests.handler.js +2 -1
  37. package/dist/parallel-tests/run-all-tests.d.ts +2 -2
  38. package/dist/parallel-tests/run-all-tests.js +50 -23
  39. package/dist/parallel-tests/screenshot-diff-results.utils.d.ts +7 -0
  40. package/dist/parallel-tests/screenshot-diff-results.utils.js +20 -0
  41. package/dist/utils/config.utils.d.ts +1 -2
  42. package/dist/utils/config.utils.js +1 -10
  43. package/dist/utils/run-all-tests.utils.d.ts +0 -1
  44. package/dist/utils/run-all-tests.utils.js +3 -50
  45. 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.summarizeDifferences = exports.diffScreenshots = void 0;
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 diffScreenshots = async ({ client, headReplayId, baseReplayId, headScreenshotsDir, baseScreenshotsDir, diffOptions, logger, }) => {
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 baseReplayScreenshots = await (0, replays_1.getScreenshotFiles)(baseScreenshotsDir);
21
- const headReplayScreenshots = await (0, replays_1.getScreenshotFiles)(headScreenshotsDir);
22
- const missingHeadImages = new Set(baseReplayScreenshots.filter((file) => !headReplayScreenshots.includes(file)));
23
- const missingHeadImagesResults = Array.from(missingHeadImages).flatMap((screenshotFileName) => {
24
- const identifier = getScreenshotIdentifier(screenshotFileName);
25
- if (identifier == null) {
26
- return [];
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/${screenshotFileName}`,
35
+ baseScreenshotFile: `screenshots/${fileName}`,
33
36
  },
34
37
  ];
35
38
  });
36
- const diffAgainstBase = async (screenshotFileName) => {
37
- const identifier = getScreenshotIdentifier(screenshotFileName);
38
- if (identifier == null) {
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/${screenshotFileName}`,
46
+ headScreenshotFile: `screenshots/${headScreenshot.fileName}`,
47
47
  },
48
48
  ];
49
49
  }
50
- const baseScreenshotFile = (0, path_1.join)(baseScreenshotsDir, screenshotFileName);
51
- const headScreenshotFile = (0, path_1.join)(headScreenshotsDir, screenshotFileName);
52
- const baseScreenshot = await (0, io_utils_1.readPng)(baseScreenshotFile);
53
- const headScreenshot = await (0, io_utils_1.readPng)(headScreenshotFile);
54
- if (baseScreenshot.width !== headScreenshot.width ||
55
- baseScreenshot.height !== headScreenshot.height) {
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
- headScreenshotFile: `screenshots/${screenshotFileName}`,
61
- baseScreenshotFile: `screenshots/${screenshotFileName}`,
62
- baseWidth: baseScreenshot.width,
63
- baseHeight: baseScreenshot.height,
64
- headWidth: headScreenshot.width,
65
- headHeight: headScreenshot.height,
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: baseScreenshot,
71
- head: headScreenshot,
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
- headScreenshotFile: `screenshots/${screenshotFileName}`,
87
- baseScreenshotFile: `screenshots/${screenshotFileName}`,
88
- width: baseScreenshot.width,
89
- height: baseScreenshot.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 summarizeDifferences = ({ baseReplayId, headReplayId, results, diffOptions, logger, }) => {
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
- const diffs = results.flatMap((result) => {
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
- const diffSummary = (0, exports.summarizeDifferences)({
200
- baseReplayId,
201
- headReplayId,
154
+ (0, exports.logDifferences)({
202
155
  results,
203
156
  diffOptions,
204
157
  logger,
205
158
  });
206
- if (diffSummary.hasDiffs) {
159
+ if ((0, has_notable_differences_1.hasNotableDifferences)(results)) {
207
160
  process.exit(1);
208
161
  }
209
162
  };
@@ -0,0 +1,2 @@
1
+ import { ScreenshotIdentifier } from "@alwaysmeticulous/api";
2
+ export declare const getScreenshotFilename: (identifier: ScreenshotIdentifier) => string;
@@ -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,2 @@
1
+ import { ScreenshotIdentifier } from "@alwaysmeticulous/api";
2
+ export declare const getScreenshotIdentifier: (filename: string) => ScreenshotIdentifier | undefined;
@@ -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,2 @@
1
+ import { ScreenshotDiffResult } from "@alwaysmeticulous/api";
2
+ export declare const hasNotableDifferences: (screenshotDiffResults: ScreenshotDiffResult[]) => boolean;
@@ -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;
@@ -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, baseReplayId, options }) => ({
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} | ${rest.baseReplayId}`,
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
- screenshotDiffResults: ScreenshotDiffResult[];
15
+ screenshotDiffResultsByBaseReplayId: Record<string, ScreenshotDiffResult[]>;
16
16
  }
@@ -14,6 +14,7 @@ interface HandleReplayOptions {
14
14
  commitSha: string;
15
15
  generatedBy: GeneratedBy;
16
16
  testRunId: string | null;
17
+ baseTestRunId: string | null;
17
18
  replayEventsDependencies: ReplayEventsDependencies;
18
19
  suppressScreenshotDiffLogging: boolean;
19
20
  }
@@ -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 handleReplay = async ({ testCase, replayTarget, executionOptions, screenshottingOptions, apiToken, commitSha, generatedBy, testRunId, replayEventsDependencies, suppressScreenshotDiffLogging, }) => {
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, screenshotDiffResults, screenshotDiffsSummary } = await (0, replay_command_1.replayCommandHandler)({
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
- baseSimulationId: testCase.baseReplayId,
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: screenshotDiffsSummary.hasDiffs ? "fail" : "pass",
31
- screenshotDiffResults,
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 { getLatestTestRunResults, GetLatestTestRunResultsOptions, } from "./api/test-run.api";
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 = exports.getLatestTestRunResults = void 0;
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 declare const getScreenshotFiles: (screenshotsDirPath: string) => Promise<string[]>;
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() && dirEntry.name.endsWith(".png")) {
67
- screenshotFiles.push(dirEntry.name);
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}+${headReplayId}+${screenshotFileName}.png`);
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 or --useAssetsSnapshottedInBaseSimulation flag." +
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
- screenshotDiffResults,
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
- baseReplayId: "mock-base-replay-id",
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.screenshotDiffResults.forEach((result) => {
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.screenshotDiffResults.map((currentDiff) => {
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
- withoutIdentifier(diffWhenRetrying),
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: withoutIdentifier(currentDiff),
68
- diffsToHeadScreenshotOnRetries: [withoutIdentifier(diffWhenRetrying)],
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
- screenshotDiffResults: newScreenshotDiffResults,
84
+ screenshotDiffResultsByBaseReplayId: (0, screenshot_diff_results_utils_1.groupScreenshotDiffResults)(newScreenshotDiffResults),
79
85
  };
80
86
  };
81
87
  exports.mergeResults = mergeResults;
82
- const withoutIdentifier = ({ identifier, // eslint-disable-line @typescript-eslint/no-unused-vars
83
- ...rest }) => rest;
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 JSON.stringify(identifier);
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.screenshotDiffResults.filter((result) => result.outcome !== "no-diff").length;
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, useAssetsSnapshottedInBaseSimulation, executionOptions, screenshottingOptions, parallelTasks, deflake, maxRetriesOnFailure, cachedTestRunResults: cachedTestRunResults_, githubSummary, environment, onTestRunCreated, onTestFinished: onTestFinished_, }: Options) => Promise<RunAllTestsResult>;
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>;