@alwaysmeticulous/cli 2.36.0 → 2.38.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 (48) 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/command-utils/common-options.d.ts +0 -10
  7. package/dist/command-utils/common-options.js +0 -6
  8. package/dist/commands/create-test/create-test.command.d.ts +0 -5
  9. package/dist/commands/create-test/create-test.command.js +1 -5
  10. package/dist/commands/replay/replay.command.d.ts +5 -25
  11. package/dist/commands/replay/replay.command.js +10 -54
  12. package/dist/commands/replay/utils/compute-diff.d.ts +3 -6
  13. package/dist/commands/replay/utils/compute-diff.js +83 -18
  14. package/dist/commands/replay/utils/exit-early-if-skip-upload-env-var-set.d.ts +1 -1
  15. package/dist/commands/replay/utils/exit-early-if-skip-upload-env-var-set.js +3 -3
  16. package/dist/commands/run-all-tests/run-all-tests.command.d.ts +4 -10
  17. package/dist/commands/run-all-tests/run-all-tests.command.js +6 -10
  18. package/dist/commands/screenshot-diff/screenshot-diff.command.d.ts +6 -8
  19. package/dist/commands/screenshot-diff/screenshot-diff.command.js +50 -97
  20. package/dist/commands/screenshot-diff/utils/get-screenshot-filename.d.ts +2 -0
  21. package/dist/commands/screenshot-diff/utils/get-screenshot-filename.js +18 -0
  22. package/dist/commands/screenshot-diff/utils/get-screenshot-identifier.d.ts +2 -0
  23. package/dist/commands/screenshot-diff/utils/get-screenshot-identifier.js +24 -0
  24. package/dist/commands/screenshot-diff/utils/has-notable-differences.d.ts +2 -0
  25. package/dist/commands/screenshot-diff/utils/has-notable-differences.js +10 -0
  26. package/dist/config/config.js +2 -3
  27. package/dist/config/config.types.d.ts +1 -1
  28. package/dist/deflake-tests/deflake-tests.handler.d.ts +1 -0
  29. package/dist/deflake-tests/deflake-tests.handler.js +9 -5
  30. package/dist/index.d.ts +1 -1
  31. package/dist/index.js +1 -3
  32. package/dist/local-data/replays.d.ts +6 -1
  33. package/dist/local-data/replays.js +11 -3
  34. package/dist/local-data/screenshot-diffs.js +1 -1
  35. package/dist/local-data/serve-assets-from-simulation.js +1 -1
  36. package/dist/parallel-tests/__tests__/mock-test-results.js +3 -1
  37. package/dist/parallel-tests/__tests__/parrallel-tests.handler.spec.js +1 -1
  38. package/dist/parallel-tests/merge-test-results.js +18 -9
  39. package/dist/parallel-tests/parallel-tests.handler.js +2 -1
  40. package/dist/parallel-tests/run-all-tests.d.ts +2 -2
  41. package/dist/parallel-tests/run-all-tests.js +50 -23
  42. package/dist/parallel-tests/screenshot-diff-results.utils.d.ts +7 -0
  43. package/dist/parallel-tests/screenshot-diff-results.utils.js +20 -0
  44. package/dist/utils/config.utils.d.ts +1 -2
  45. package/dist/utils/config.utils.js +1 -10
  46. package/dist/utils/run-all-tests.utils.d.ts +0 -1
  47. package/dist/utils/run-all-tests.utils.js +3 -50
  48. package/package.json +6 -4
@@ -7,12 +7,11 @@ const command_builder_1 = require("../../command-utils/command-builder");
7
7
  const common_options_1 = require("../../command-utils/common-options");
8
8
  const run_all_tests_1 = require("../../parallel-tests/run-all-tests");
9
9
  const commit_sha_utils_1 = require("../../utils/commit-sha.utils");
10
- const handler = async ({ apiToken, commitSha: commitSha_, baseCommitSha, appUrl, useAssetsSnapshottedInBaseSimulation, headless, devTools, bypassCSP, diffThreshold, diffPixelThreshold, padTime, shiftTime, networkStubbing, githubSummary, parallelize, parallelTasks: parrelelTasks_, deflake, maxRetriesOnFailure, useCache, testsFile, disableRemoteFonts, noSandbox, skipPauses, moveBeforeClick, maxDurationMs, maxEventCount, storyboard, essentialFeaturesOnly, }) => {
10
+ const handler = async ({ apiToken, commitSha: commitSha_, baseCommitSha, appUrl, headless, devTools, bypassCSP, diffThreshold, diffPixelThreshold, shiftTime, networkStubbing, githubSummary, parallelize, parallelTasks: parrelelTasks_, deflake, maxRetriesOnFailure, useCache, testsFile, disableRemoteFonts, noSandbox, skipPauses, moveBeforeClick, maxDurationMs, maxEventCount, storyboard, essentialFeaturesOnly, baseTestRunId, }) => {
11
11
  const executionOptions = {
12
12
  headless,
13
13
  devTools,
14
14
  bypassCSP,
15
- padTime,
16
15
  shiftTime,
17
16
  networkStubbing,
18
17
  disableRemoteFonts,
@@ -45,8 +44,8 @@ const handler = async ({ apiToken, commitSha: commitSha_, baseCommitSha, appUrl,
45
44
  apiToken: apiToken !== null && apiToken !== void 0 ? apiToken : null,
46
45
  commitSha,
47
46
  baseCommitSha: baseCommitSha !== null && baseCommitSha !== void 0 ? baseCommitSha : null,
47
+ baseTestRunId: baseTestRunId !== null && baseTestRunId !== void 0 ? baseTestRunId : null,
48
48
  appUrl: appUrl !== null && appUrl !== void 0 ? appUrl : null,
49
- useAssetsSnapshottedInBaseSimulation,
50
49
  parallelTasks: parrelelTasks !== null && parrelelTasks !== void 0 ? parrelelTasks : null,
51
50
  deflake,
52
51
  maxRetriesOnFailure,
@@ -70,13 +69,6 @@ exports.runAllTestsCommand = (0, command_builder_1.buildCommand)("run-all-tests"
70
69
  string: true,
71
70
  description: "The URL to execute the tests against. If left absent here and in the test cases file, then will use the URL the test was originally recorded against.",
72
71
  },
73
- useAssetsSnapshottedInBaseSimulation: {
74
- boolean: true,
75
- description: "If present will run each session against a local server serving up previously snapshotted assets (HTML, JS, CSS etc.)" +
76
- " from the base simulation/replay the test is comparing against. The sessions will then be replayed against those local urls." +
77
- " This is an alternative to specifying an appUrl.",
78
- default: false,
79
- },
80
72
  githubSummary: {
81
73
  boolean: true,
82
74
  description: "Outputs a summary page for GitHub actions",
@@ -117,6 +109,10 @@ exports.runAllTestsCommand = (0, command_builder_1.buildCommand)("run-all-tests"
117
109
  description: "The path to the meticulous.json file containing the list of tests you want to run." +
118
110
  " If not set a search will be performed to find a meticulous.json file in the current directory or the nearest parent directory.",
119
111
  },
112
+ baseTestRunId: {
113
+ string: true,
114
+ description: "The id of a test run to compare screenshots against.",
115
+ },
120
116
  moveBeforeClick: common_options_1.OPTIONS.moveBeforeClick,
121
117
  ...common_options_1.COMMON_REPLAY_OPTIONS,
122
118
  ...common_options_1.SCREENSHOT_DIFF_OPTIONS,
@@ -1,23 +1,21 @@
1
1
  /// <reference types="yargs" />
2
2
  import { ScreenshotDiffOptions, ScreenshotDiffResult } from "@alwaysmeticulous/api";
3
- import { AxiosInstance } from "axios";
4
3
  import log from "loglevel";
5
- export declare const diffScreenshots: ({ client, headReplayId, baseReplayId, headScreenshotsDir, baseScreenshotsDir, diffOptions, logger, }: {
6
- client: AxiosInstance;
4
+ import { IdentifiedScreenshotFile } from "../../local-data/replays";
5
+ export declare const diffScreenshots: ({ headReplayId, baseReplayId, baseScreenshotsDir, headScreenshotsDir, baseReplayScreenshots, headReplayScreenshots, diffOptions, }: {
7
6
  baseReplayId: string;
8
7
  headReplayId: string;
9
8
  baseScreenshotsDir: string;
10
9
  headScreenshotsDir: string;
10
+ baseReplayScreenshots: IdentifiedScreenshotFile[];
11
+ headReplayScreenshots: IdentifiedScreenshotFile[];
11
12
  diffOptions: ScreenshotDiffOptions;
12
- logger: log.Logger;
13
13
  }) => Promise<ScreenshotDiffResult[]>;
14
- export declare const summarizeDifferences: ({ baseReplayId, headReplayId, results, diffOptions, logger, }: {
15
- baseReplayId: string;
16
- headReplayId: string;
14
+ export declare const logDifferences: ({ results, diffOptions, logger, }: {
17
15
  results: ScreenshotDiffResult[];
18
16
  diffOptions: ScreenshotDiffOptions;
19
17
  logger: log.Logger;
20
- }) => ScreenshotDiffsSummary;
18
+ }) => void;
21
19
  export type ScreenshotDiffsSummary = HasDiffsScreenshotDiffsResult | NoDiffsScreenshotDiffsResult;
22
20
  export interface HasDiffsScreenshotDiffsResult {
23
21
  hasDiffs: true;
@@ -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;