@alwaysmeticulous/cli 2.35.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
@@ -34,4 +34,3 @@ export interface ScreenshotDiffStats {
34
34
  }
35
35
  export declare const postScreenshotDiffStats: (client: AxiosInstance, options: ScreenshotDiffStats) => Promise<void>;
36
36
  export declare const getReplayUrl: (replay: any) => string;
37
- export declare const getDiffUrl: (client: AxiosInstance, baseReplayId: string, headReplayId: string) => Promise<string>;
@@ -3,9 +3,8 @@ 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.getDiffUrl = exports.getReplayUrl = exports.postScreenshotDiffStats = exports.getReplayCommandId = exports.putReplayPushedStatus = exports.getReplayDownloadUrl = exports.getReplayPushUrl = exports.createReplay = exports.getReplay = void 0;
6
+ exports.getReplayUrl = exports.postScreenshotDiffStats = exports.getReplayCommandId = exports.putReplayPushedStatus = exports.getReplayDownloadUrl = exports.getReplayPushUrl = exports.createReplay = exports.getReplay = void 0;
7
7
  const axios_1 = __importDefault(require("axios"));
8
- const project_api_1 = require("./project.api");
9
8
  const getReplay = async (client, replayId) => {
10
9
  const { data } = await client.get(`replays/${replayId}`).catch((error) => {
11
10
  var _a;
@@ -88,14 +87,3 @@ const getReplayUrl = (replay) => {
88
87
  return replayUrl;
89
88
  };
90
89
  exports.getReplayUrl = getReplayUrl;
91
- const getDiffUrl = async (client, baseReplayId, headReplayId) => {
92
- const project = await (0, project_api_1.getProject)(client);
93
- if (!project) {
94
- throw new Error(`Unexpected error: could not retrieve project data`);
95
- }
96
- const organizationName = encodeURIComponent(project.organization.name);
97
- const projectName = encodeURIComponent(project.name);
98
- const diffUrl = `https://app.meticulous.ai/projects/${organizationName}/${projectName}/simulations/${headReplayId}/diff/${baseReplayId}`;
99
- return diffUrl;
100
- };
101
- exports.getDiffUrl = getDiffUrl;
@@ -1,7 +1,7 @@
1
1
  import { TestRunConfigData } from "@alwaysmeticulous/api";
2
2
  import { AxiosInstance } from "axios";
3
3
  import { TestCaseResult } from "../config/config.types";
4
- import { TestRun, TestRunStatus } from "./types";
4
+ import { ScreenshotLocator, TestRun, TestRunStatus } from "./types";
5
5
  export declare const getTestRun: (options: {
6
6
  client: AxiosInstance;
7
7
  testRunId: string;
@@ -26,4 +26,10 @@ export interface GetLatestTestRunResultsOptions {
26
26
  commitSha: string;
27
27
  }
28
28
  export declare const getCachedTestRunResults: ({ client, commitSha, }: GetLatestTestRunResultsOptions) => Promise<TestCaseResult[]>;
29
- export declare const getLatestTestRunResults: ({ client, commitSha, }: GetLatestTestRunResultsOptions) => Promise<TestRun | null>;
29
+ export declare const getLatestTestRunId: (opts: GetLatestTestRunResultsOptions) => Promise<string | null>;
30
+ export interface GetBaseScreenshotLocatorsOptions {
31
+ client: AxiosInstance;
32
+ testRunId: string;
33
+ sessionId: string;
34
+ }
35
+ export declare const getBaseScreenshots: ({ client, testRunId, sessionId, }: GetBaseScreenshotLocatorsOptions) => Promise<ScreenshotLocator[]>;
@@ -26,7 +26,7 @@ var __importDefault = (this && this.__importDefault) || function (mod) {
26
26
  return (mod && mod.__esModule) ? mod : { "default": mod };
27
27
  };
28
28
  Object.defineProperty(exports, "__esModule", { value: true });
29
- exports.getLatestTestRunResults = exports.getCachedTestRunResults = exports.getTestRunUrl = exports.putTestRunResults = exports.createTestRun = exports.getTestRun = void 0;
29
+ exports.getBaseScreenshots = exports.getLatestTestRunId = exports.getCachedTestRunResults = exports.getTestRunUrl = exports.putTestRunResults = exports.createTestRun = exports.getTestRun = void 0;
30
30
  const common_1 = require("@alwaysmeticulous/common");
31
31
  const axios_1 = __importStar(require("axios"));
32
32
  const loglevel_1 = __importDefault(require("loglevel"));
@@ -75,7 +75,7 @@ const getCachedTestRunResults = async ({ client, commitSha, }) => {
75
75
  logger.warn("Test run cache not supported: no commit hash");
76
76
  return [];
77
77
  }
78
- const results = (_c = (_b = (_a = (await (0, exports.getLatestTestRunResults)({ client, commitSha }))) === null || _a === void 0 ? void 0 : _a.resultData) === null || _b === void 0 ? void 0 : _b.results) !== null && _c !== void 0 ? _c : [];
78
+ const results = (_c = (_b = (_a = (await getLatestTestRunResults({ client, commitSha }))) === null || _a === void 0 ? void 0 : _a.resultData) === null || _b === void 0 ? void 0 : _b.results) !== null && _c !== void 0 ? _c : [];
79
79
  // Only return passing tests
80
80
  return results.filter(({ result }) => result === "pass");
81
81
  };
@@ -93,4 +93,22 @@ const getLatestTestRunResults = async ({ client, commitSha, }) => {
93
93
  });
94
94
  return (_a = data) !== null && _a !== void 0 ? _a : null;
95
95
  };
96
- exports.getLatestTestRunResults = getLatestTestRunResults;
96
+ const getLatestTestRunId = async (opts) => {
97
+ var _a, _b;
98
+ return (_b = (_a = (await getLatestTestRunResults(opts))) === null || _a === void 0 ? void 0 : _a.id) !== null && _b !== void 0 ? _b : null;
99
+ };
100
+ exports.getLatestTestRunId = getLatestTestRunId;
101
+ const getBaseScreenshots = async ({ client, testRunId, sessionId, }) => {
102
+ var _a;
103
+ const { data } = await client
104
+ .get(`test-runs/${encodeURIComponent(testRunId)}/base-screenshots?sessionId=${encodeURIComponent(sessionId)}`)
105
+ .catch((error) => {
106
+ var _a;
107
+ if (error instanceof axios_1.AxiosError && ((_a = error.response) === null || _a === void 0 ? void 0 : _a.status) === 404) {
108
+ return { data: null };
109
+ }
110
+ throw error;
111
+ });
112
+ return (_a = data) !== null && _a !== void 0 ? _a : [];
113
+ };
114
+ exports.getBaseScreenshots = getBaseScreenshots;
@@ -1,3 +1,4 @@
1
+ import { ScreenshotIdentifier } from "@alwaysmeticulous/api";
1
2
  import { TestCaseResult } from "../config/config.types";
2
3
  interface Organization {
3
4
  id: string;
@@ -27,4 +28,12 @@ export interface TestRun {
27
28
  };
28
29
  [key: string]: any;
29
30
  }
31
+ /**
32
+ * All the information required to locate & download a screenshot of a replay.
33
+ */
34
+ export interface ScreenshotLocator {
35
+ replayId: string;
36
+ screenshotIdentifier: ScreenshotIdentifier;
37
+ screenshotUrl: string;
38
+ }
30
39
  export {};
@@ -48,7 +48,6 @@ const handleTestCreation = async (replay, sessionId) => {
48
48
  await (0, config_utils_1.addTestCase)({
49
49
  title: testNameResponse.name,
50
50
  sessionId,
51
- baseReplayId: replay.id,
52
51
  });
53
52
  logger.info(chalk_1.default.bold.white(`Test saved to ${chalk_1.default.green("meticulous.json")}.`));
54
53
  };
@@ -105,12 +104,10 @@ headless, screenshotSelector, padTime, shiftTime, networkStubbing, moveBeforeCli
105
104
  moveBeforeClick,
106
105
  cookiesFile,
107
106
  skipPauses,
108
- save: false,
109
107
  // We replay against the original recorded URL
110
108
  appUrl: null,
111
109
  simulationIdForAssets: null,
112
110
  // We don't try comparing to the original screenshot, so just set these to their defaults
113
- baseSimulationId: null,
114
111
  diffThreshold: common_options_1.OPTIONS.diffThreshold.default,
115
112
  diffPixelThreshold: common_options_1.OPTIONS.diffPixelThreshold.default,
116
113
  // We don't expose these options
@@ -2,7 +2,6 @@
2
2
  import { ScreenshotDiffResult } from "@alwaysmeticulous/api";
3
3
  import { GeneratedBy, Replay, ReplayEventsDependencies, ReplayExecutionOptions, ReplayTarget } from "@alwaysmeticulous/common";
4
4
  import { ScreenshotAssertionsOptions, ScreenshotDiffOptions } from "../../command-utils/common-types";
5
- import { ScreenshotDiffsSummary } from "../screenshot-diff/screenshot-diff.command";
6
5
  export interface ReplayOptions extends AdditionalReplayOptions {
7
6
  replayTarget: ReplayTarget;
8
7
  executionOptions: ReplayExecutionOptions;
@@ -17,14 +16,10 @@ export interface ReplayResult {
17
16
  /**
18
17
  * Empty if screenshottingOptions.enabled was false.
19
18
  */
20
- screenshotDiffResults: ScreenshotDiffResult[];
21
- /**
22
- * Returned as { hasDiffs: false } if screenshottingOptions.enabled was false.
23
- */
24
- screenshotDiffsSummary: ScreenshotDiffsSummary;
19
+ screenshotDiffResultsByBaseReplayId: Record<string, ScreenshotDiffResult[]>;
25
20
  }
26
- export declare const replayCommandHandler: ({ replayTarget, executionOptions, screenshottingOptions, apiToken, sessionId, commitSha: commitSha_, baseSimulationId: baseReplayId, cookiesFile, generatedBy, testRunId, replayEventsDependencies, debugger: enableStepThroughDebugger, suppressScreenshotDiffLogging, }: ReplayOptions) => Promise<ReplayResult>;
27
- export interface RawReplayCommandHandlerOptions extends ScreenshotDiffOptions, Omit<ReplayExecutionOptions, "maxDurationMs" | "maxEventCount">, AdditionalReplayOptions {
21
+ export declare const replayCommandHandler: ({ replayTarget, executionOptions, screenshottingOptions, apiToken, sessionId, commitSha: commitSha_, baseTestRunId, cookiesFile, generatedBy, testRunId, replayEventsDependencies, debugger: enableStepThroughDebugger, suppressScreenshotDiffLogging, }: ReplayOptions) => Promise<ReplayResult>;
22
+ export interface RawReplayCommandHandlerOptions extends ScreenshotDiffOptions, Omit<ReplayExecutionOptions, "maxDurationMs" | "maxEventCount">, Omit<AdditionalReplayOptions, "baseTestRunId"> {
28
23
  screenshot: boolean;
29
24
  appUrl: string | null | undefined;
30
25
  simulationIdForAssets: string | null | undefined;
@@ -32,17 +27,16 @@ export interface RawReplayCommandHandlerOptions extends ScreenshotDiffOptions, O
32
27
  maxDurationMs: number | null | undefined;
33
28
  maxEventCount: number | null | undefined;
34
29
  storyboard: boolean;
35
- save: boolean | null | undefined;
36
30
  }
37
31
  interface AdditionalReplayOptions {
38
32
  apiToken: string | null | undefined;
39
33
  commitSha: string | null | undefined;
40
34
  sessionId: string;
41
- baseSimulationId: string | null | undefined;
35
+ baseTestRunId: string | null | undefined;
42
36
  cookiesFile: string | null | undefined;
43
37
  debugger: boolean;
44
38
  }
45
- export declare const rawReplayCommandHandler: ({ apiToken, commitSha, sessionId, appUrl, simulationIdForAssets, headless, devTools, bypassCSP, screenshot, screenshotSelector, baseSimulationId, diffThreshold, diffPixelThreshold, save, padTime, shiftTime, networkStubbing, moveBeforeClick, cookiesFile, disableRemoteFonts, noSandbox, skipPauses, maxDurationMs, maxEventCount, storyboard, essentialFeaturesOnly, debugger: enableStepThroughDebugger, }: RawReplayCommandHandlerOptions) => Promise<Replay>;
39
+ export declare const rawReplayCommandHandler: ({ apiToken, commitSha, sessionId, appUrl, simulationIdForAssets, headless, devTools, bypassCSP, screenshot, screenshotSelector, diffThreshold, diffPixelThreshold, padTime, shiftTime, networkStubbing, moveBeforeClick, cookiesFile, disableRemoteFonts, noSandbox, skipPauses, maxDurationMs, maxEventCount, storyboard, essentialFeaturesOnly, debugger: enableStepThroughDebugger, }: RawReplayCommandHandlerOptions) => Promise<Replay>;
46
40
  export declare const getReplayTarget: ({ appUrl, simulationIdForAssets, }: {
47
41
  appUrl: string | null;
48
42
  simulationIdForAssets: string | null;
@@ -149,15 +143,6 @@ export declare const replayCommand: import("yargs").CommandModule<unknown, impor
149
143
  string: true;
150
144
  description: string;
151
145
  };
152
- baseSimulationId: {
153
- string: true;
154
- description: string;
155
- alias: string;
156
- };
157
- save: {
158
- boolean: true;
159
- description: string;
160
- };
161
146
  debugger: {
162
147
  boolean: true;
163
148
  description: string;
@@ -34,7 +34,6 @@ const Sentry = __importStar(require("@sentry/node"));
34
34
  const loglevel_1 = __importDefault(require("loglevel"));
35
35
  const luxon_1 = require("luxon");
36
36
  const client_1 = require("../../api/client");
37
- const replay_diff_api_1 = require("../../api/replay-diff.api");
38
37
  const replay_api_1 = require("../../api/replay.api");
39
38
  const upload_1 = require("../../api/upload");
40
39
  const archive_1 = require("../../archive/archive");
@@ -45,11 +44,10 @@ const replay_assets_1 = require("../../local-data/replay-assets");
45
44
  const serve_assets_from_simulation_1 = require("../../local-data/serve-assets-from-simulation");
46
45
  const sessions_1 = require("../../local-data/sessions");
47
46
  const commit_sha_utils_1 = require("../../utils/commit-sha.utils");
48
- const config_utils_1 = require("../../utils/config.utils");
49
47
  const version_utils_1 = require("../../utils/version.utils");
50
48
  const compute_diff_1 = require("./utils/compute-diff");
51
49
  const exit_early_if_skip_upload_env_var_set_1 = require("./utils/exit-early-if-skip-upload-env-var-set");
52
- const replayCommandHandler = async ({ replayTarget, executionOptions, screenshottingOptions, apiToken, sessionId, commitSha: commitSha_, baseSimulationId: baseReplayId, cookiesFile, generatedBy, testRunId, replayEventsDependencies, debugger: enableStepThroughDebugger, suppressScreenshotDiffLogging, }) => {
50
+ const replayCommandHandler = async ({ replayTarget, executionOptions, screenshottingOptions, apiToken, sessionId, commitSha: commitSha_, baseTestRunId, cookiesFile, generatedBy, testRunId, replayEventsDependencies, debugger: enableStepThroughDebugger, suppressScreenshotDiffLogging, }) => {
53
51
  var _a;
54
52
  if (executionOptions.headless === true &&
55
53
  enableStepThroughDebugger === true) {
@@ -137,7 +135,7 @@ const replayCommandHandler = async ({ replayTarget, executionOptions, screenshot
137
135
  closeServer === null || closeServer === void 0 ? void 0 : closeServer();
138
136
  const endTime = luxon_1.DateTime.utc();
139
137
  logger.info(`Simulation time: ${endTime.diff(startTime).as("seconds")} seconds`);
140
- (0, exit_early_if_skip_upload_env_var_set_1.exitEarlyIfSkipUploadEnvVarSet)(baseReplayId);
138
+ (0, exit_early_if_skip_upload_env_var_set_1.exitEarlyIfSkipUploadEnvVarSet)(baseTestRunId);
141
139
  logger.info("Sending simulation results to Meticulous");
142
140
  // 8. Create a Zip archive containing the replay files
143
141
  const createReplayArchiveSpan = transaction.startChild({
@@ -195,21 +193,19 @@ const replayCommandHandler = async ({ replayTarget, executionOptions, screenshot
195
193
  else {
196
194
  computeDiffsLogger.setLevel(logger.getLevel(), false);
197
195
  }
198
- const { screenshotDiffResults, screenshotDiffsSummary } = screenshottingOptions.enabled && baseReplayId
196
+ const screenshotDiffResultsByBaseReplayId = screenshottingOptions.enabled && baseTestRunId
199
197
  ? await (0, compute_diff_1.computeDiff)({
200
198
  client,
201
- baseReplayId,
199
+ baseTestRunId,
200
+ sessionId,
202
201
  headReplayId: replay.id,
203
202
  tempDir,
204
203
  screenshottingOptions,
205
204
  logger: computeDiffsLogger,
206
205
  })
207
- : {
208
- screenshotDiffResults: [],
209
- screenshotDiffsSummary: { hasDiffs: false },
210
- };
206
+ : {};
211
207
  computeDiffSpan.finish();
212
- return { replay, screenshotDiffResults, screenshotDiffsSummary };
208
+ return { replay, screenshotDiffResultsByBaseReplayId };
213
209
  }
214
210
  finally {
215
211
  await (0, archive_1.deleteArchive)(archivePath);
@@ -236,7 +232,7 @@ const serveOrGetAppUrl = async (client, replayTarget) => {
236
232
  const unknownReplayTargetType = (replayTarget) => {
237
233
  throw new Error(`Unknown type of replay target: ${JSON.stringify(replayTarget)}`);
238
234
  };
239
- const rawReplayCommandHandler = async ({ apiToken, commitSha, sessionId, appUrl, simulationIdForAssets, headless, devTools, bypassCSP, screenshot, screenshotSelector, baseSimulationId, diffThreshold, diffPixelThreshold, save, padTime, shiftTime, networkStubbing, moveBeforeClick, cookiesFile, disableRemoteFonts, noSandbox, skipPauses, maxDurationMs, maxEventCount, storyboard, essentialFeaturesOnly, debugger: enableStepThroughDebugger, }) => {
235
+ const rawReplayCommandHandler = async ({ apiToken, commitSha, sessionId, appUrl, simulationIdForAssets, headless, devTools, bypassCSP, screenshot, screenshotSelector, diffThreshold, diffPixelThreshold, padTime, shiftTime, networkStubbing, moveBeforeClick, cookiesFile, disableRemoteFonts, noSandbox, skipPauses, maxDurationMs, maxEventCount, storyboard, essentialFeaturesOnly, debugger: enableStepThroughDebugger, }) => {
240
236
  const executionOptions = {
241
237
  headless,
242
238
  devTools,
@@ -265,7 +261,7 @@ const rawReplayCommandHandler = async ({ apiToken, commitSha, sessionId, appUrl,
265
261
  }
266
262
  : { enabled: false };
267
263
  const replayEventsDependencies = await (0, replay_assets_1.loadReplayEventsDependencies)();
268
- const { replay, screenshotDiffsSummary, screenshotDiffResults } = await (0, exports.replayCommandHandler)({
264
+ const { replay } = await (0, exports.replayCommandHandler)({
269
265
  replayTarget: (0, exports.getReplayTarget)({
270
266
  appUrl: appUrl !== null && appUrl !== void 0 ? appUrl : null,
271
267
  simulationIdForAssets: simulationIdForAssets !== null && simulationIdForAssets !== void 0 ? simulationIdForAssets : null,
@@ -276,43 +272,13 @@ const rawReplayCommandHandler = async ({ apiToken, commitSha, sessionId, appUrl,
276
272
  commitSha,
277
273
  cookiesFile,
278
274
  sessionId,
279
- baseSimulationId,
280
275
  generatedBy: generatedByOption,
276
+ baseTestRunId: null,
281
277
  testRunId: null,
282
278
  replayEventsDependencies,
283
279
  debugger: enableStepThroughDebugger,
284
280
  suppressScreenshotDiffLogging: false,
285
281
  });
286
- // Add test case to meticulous.json if --save option is passed
287
- if (save) {
288
- if (!screenshottingOptions.enabled) {
289
- const logger = loglevel_1.default.getLogger(common_1.METICULOUS_LOGGER_NAME);
290
- logger.error("Warning: saving a new test case without screenshot enabled.");
291
- }
292
- await (0, config_utils_1.addTestCase)({
293
- title: `${sessionId} | ${replay.id}`,
294
- sessionId,
295
- baseReplayId: replay.id,
296
- });
297
- }
298
- if (screenshotDiffsSummary.hasDiffs) {
299
- const client = (0, client_1.createClient)({ apiToken });
300
- if (baseSimulationId == null) {
301
- throw new Error("baseSimulationId must have been defined if there are diffs, but was null-ish");
302
- }
303
- // Store the diff
304
- await (0, replay_diff_api_1.createReplayDiff)({
305
- client,
306
- headReplayId: replay.id,
307
- baseReplayId: baseSimulationId,
308
- testRunId: null,
309
- data: {
310
- screenshotAssertionsOptions: screenshottingOptions,
311
- screenshotDiffResults,
312
- },
313
- });
314
- process.exit(1);
315
- }
316
282
  return replay;
317
283
  };
318
284
  exports.rawReplayCommandHandler = rawReplayCommandHandler;
@@ -356,15 +322,6 @@ exports.replayCommand = (0, command_builder_1.buildCommand)("simulate")
356
322
  string: true,
357
323
  description: "Query selector to screenshot a specific DOM element instead of the whole page",
358
324
  },
359
- baseSimulationId: {
360
- string: true,
361
- description: "Base simulation id to diff the final state screenshot against",
362
- alias: "baseReplayId",
363
- },
364
- save: {
365
- boolean: true,
366
- description: "Adds the simulation to the list of test cases in meticulous.json",
367
- },
368
325
  debugger: {
369
326
  boolean: true,
370
327
  description: "Opens a step through debugger to advance through the replay event by event",
@@ -1,16 +1,13 @@
1
1
  import { ScreenshotAssertionsEnabledOptions, ScreenshotDiffResult } from "@alwaysmeticulous/api";
2
2
  import { AxiosInstance } from "axios";
3
3
  import log from "loglevel";
4
- import { ScreenshotDiffsSummary } from "../../screenshot-diff/screenshot-diff.command";
5
4
  export interface ComputeAndSaveDiffOptions {
6
5
  client: AxiosInstance;
7
- baseReplayId: string;
6
+ baseTestRunId: string;
7
+ sessionId: string;
8
8
  headReplayId: string;
9
9
  tempDir: string;
10
10
  screenshottingOptions: ScreenshotAssertionsEnabledOptions;
11
11
  logger: log.Logger;
12
12
  }
13
- export declare const computeDiff: ({ client, baseReplayId, tempDir, headReplayId, screenshottingOptions, logger, }: ComputeAndSaveDiffOptions) => Promise<{
14
- screenshotDiffResults: ScreenshotDiffResult[];
15
- screenshotDiffsSummary: ScreenshotDiffsSummary;
16
- }>;
13
+ export declare const computeDiff: ({ client, baseTestRunId, sessionId, tempDir, headReplayId, screenshottingOptions, logger, }: ComputeAndSaveDiffOptions) => Promise<Record<string, ScreenshotDiffResult[]>>;
@@ -1,30 +1,95 @@
1
1
  "use strict";
2
2
  Object.defineProperty(exports, "__esModule", { value: true });
3
3
  exports.computeDiff = void 0;
4
+ const promises_1 = require("fs/promises");
5
+ const path_1 = require("path");
6
+ const download_1 = require("../../../api/download");
7
+ const test_run_api_1 = require("../../../api/test-run.api");
4
8
  const replays_1 = require("../../../local-data/replays");
5
9
  const screenshot_diff_command_1 = require("../../screenshot-diff/screenshot-diff.command");
6
- const computeDiff = async ({ client, baseReplayId, tempDir, headReplayId, screenshottingOptions, logger, }) => {
7
- logger.info(`Diffing screenshots against replay ${baseReplayId}`);
8
- await (0, replays_1.getOrFetchReplay)(client, baseReplayId);
9
- await (0, replays_1.getOrFetchReplayArchive)(client, baseReplayId);
10
- const baseReplayScreenshotsDir = (0, replays_1.getScreenshotsDir)((0, replays_1.getReplayDir)(baseReplayId));
11
- const headReplayScreenshotsDir = (0, replays_1.getScreenshotsDir)(tempDir);
12
- const screenshotDiffResults = await (0, screenshot_diff_command_1.diffScreenshots)({
10
+ const get_screenshot_filename_1 = require("../../screenshot-diff/utils/get-screenshot-filename");
11
+ const computeDiff = async ({ client, baseTestRunId, sessionId, tempDir, headReplayId, screenshottingOptions, logger, }) => {
12
+ var _a;
13
+ logger.info(`Diffing screenshots against replays of session ${sessionId} in test run ${baseTestRunId}`);
14
+ const baseScreenshotsDir = (0, path_1.join)((0, replays_1.getScreenshotsDir)(tempDir), "base-screenshots");
15
+ await (0, promises_1.mkdir)(baseScreenshotsDir, { recursive: true });
16
+ const baseScreenshots = await (0, test_run_api_1.getBaseScreenshots)({
13
17
  client,
14
- baseReplayId,
15
- headReplayId,
16
- baseScreenshotsDir: baseReplayScreenshotsDir,
17
- headScreenshotsDir: headReplayScreenshotsDir,
18
- diffOptions: screenshottingOptions.diffOptions,
19
- logger,
18
+ testRunId: baseTestRunId,
19
+ sessionId,
20
20
  });
21
- const screenshotDiffsSummary = (0, screenshot_diff_command_1.summarizeDifferences)({
22
- baseReplayId,
23
- headReplayId,
24
- results: screenshotDiffResults,
21
+ const screenshotIdentifiersByBaseReplayId = groupByBaseReplayId(baseScreenshots);
22
+ await downloadScreenshots(baseScreenshots, baseScreenshotsDir);
23
+ logger.debug(`Downloaded ${baseScreenshots.length} base screenshots to ${baseScreenshotsDir}}`);
24
+ const headReplayScreenshotsDir = (0, replays_1.getScreenshotsDir)(tempDir);
25
+ const headReplayScreenshots = await (0, replays_1.getScreenshotFiles)(headReplayScreenshotsDir);
26
+ const screenshotDiffResultsByBaseReplayId = {};
27
+ // Diff the screenshots for each base replay
28
+ for (const baseReplayId of screenshotIdentifiersByBaseReplayId.keys()) {
29
+ const baseScreenshotIdentifiers = (_a = screenshotIdentifiersByBaseReplayId.get(baseReplayId)) !== null && _a !== void 0 ? _a : [];
30
+ const baseReplayScreenshots = baseScreenshotIdentifiers.map((identifier) => ({
31
+ identifier,
32
+ fileName: (0, get_screenshot_filename_1.getScreenshotFilename)(identifier),
33
+ }));
34
+ const resultsForBaseReplay = await (0, screenshot_diff_command_1.diffScreenshots)({
35
+ baseReplayId,
36
+ headReplayId,
37
+ baseScreenshotsDir: baseScreenshotsDir,
38
+ headScreenshotsDir: headReplayScreenshotsDir,
39
+ headReplayScreenshots,
40
+ baseReplayScreenshots,
41
+ diffOptions: screenshottingOptions.diffOptions,
42
+ });
43
+ screenshotDiffResultsByBaseReplayId[baseReplayId] = resultsForBaseReplay;
44
+ }
45
+ (0, screenshot_diff_command_1.logDifferences)({
46
+ results: Object.values(screenshotDiffResultsByBaseReplayId).flat(),
25
47
  diffOptions: screenshottingOptions.diffOptions,
26
48
  logger,
27
49
  });
28
- return { screenshotDiffResults, screenshotDiffsSummary };
50
+ // Delete base screenshots directory and all files inside it
51
+ // (we don't want to upload it to the backend)
52
+ await deleteScreenshotsDirectory(baseScreenshotsDir);
53
+ return screenshotDiffResultsByBaseReplayId;
29
54
  };
30
55
  exports.computeDiff = computeDiff;
56
+ const deleteScreenshotsDirectory = async (directory) => {
57
+ const files = await (0, promises_1.opendir)(directory);
58
+ for await (const file of files) {
59
+ if (file.isFile()) {
60
+ await (0, promises_1.rm)((0, path_1.join)(directory, file.name));
61
+ }
62
+ else {
63
+ throw new Error(`Expected screenshots directory to only contain screenshot files, but it contained: ${file.name}`);
64
+ }
65
+ }
66
+ await (0, promises_1.rmdir)(directory);
67
+ };
68
+ const groupByBaseReplayId = (screenshots) => {
69
+ const screenshotIdentifiersByBaseReplayId = new Map();
70
+ screenshots.forEach(({ replayId, screenshotIdentifier }) => {
71
+ var _a;
72
+ const screenshotIdentifiers = (_a = screenshotIdentifiersByBaseReplayId.get(replayId)) !== null && _a !== void 0 ? _a : [];
73
+ screenshotIdentifiers.push(screenshotIdentifier);
74
+ screenshotIdentifiersByBaseReplayId.set(replayId, screenshotIdentifiers);
75
+ });
76
+ return screenshotIdentifiersByBaseReplayId;
77
+ };
78
+ const downloadScreenshots = async (screenshots, directory) => {
79
+ // Download in batches of 20
80
+ const screenshotBatches = chunk(screenshots, 20);
81
+ for (const screenshotBatch of screenshotBatches) {
82
+ const screenshotDownloads = screenshotBatch.map((screenshot) => {
83
+ const filePath = (0, path_1.join)(directory, (0, get_screenshot_filename_1.getScreenshotFilename)(screenshot.screenshotIdentifier));
84
+ return (0, download_1.downloadFile)(screenshot.screenshotUrl, filePath);
85
+ });
86
+ await Promise.all(screenshotDownloads);
87
+ }
88
+ };
89
+ const chunk = (array, size) => {
90
+ const chunkedArray = [];
91
+ for (let i = 0; i < array.length; i += size) {
92
+ chunkedArray.push(array.slice(i, i + size));
93
+ }
94
+ return chunkedArray;
95
+ };
@@ -1 +1 @@
1
- export declare const exitEarlyIfSkipUploadEnvVarSet: (baseReplayId: string | null | undefined) => void;
1
+ export declare const exitEarlyIfSkipUploadEnvVarSet: (baseTestRunId: string | null | undefined) => void;
@@ -9,12 +9,12 @@ const loglevel_1 = __importDefault(require("loglevel"));
9
9
  const METICULOUS_SKIP_UPLOAD_ENV_VAR = "METICULOUS_SKIP_UPLOAD";
10
10
  // Uploading the zip can take a long time, so we expose a env variable to support skipping it
11
11
  // We don't expose this as a first-class CLI option because we don't want to pollute the list of CLI options
12
- const exitEarlyIfSkipUploadEnvVarSet = (baseReplayId) => {
12
+ const exitEarlyIfSkipUploadEnvVarSet = (baseTestRunId) => {
13
13
  if (!shouldSkipUpload()) {
14
14
  return;
15
15
  }
16
- if (baseReplayId != null) {
17
- throw new Error(`Cannot specify baseReplayId and compare to base results when ${METICULOUS_SKIP_UPLOAD_ENV_VAR} is set to true`);
16
+ if (baseTestRunId != null) {
17
+ throw new Error(`Cannot specify baseTestRunId and compare to base results when ${METICULOUS_SKIP_UPLOAD_ENV_VAR} is set to true`);
18
18
  }
19
19
  const logger = loglevel_1.default.getLogger(common_1.METICULOUS_LOGGER_NAME);
20
20
  logger.info(`Skipping upload / exiting early since ${METICULOUS_SKIP_UPLOAD_ENV_VAR} is set to true`);
@@ -87,11 +87,6 @@ export declare const runAllTestsCommand: import("yargs").CommandModule<unknown,
87
87
  readonly string: true;
88
88
  readonly 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.";
89
89
  };
90
- readonly useAssetsSnapshottedInBaseSimulation: {
91
- readonly boolean: true;
92
- readonly description: string;
93
- readonly default: false;
94
- };
95
90
  readonly githubSummary: {
96
91
  readonly boolean: true;
97
92
  readonly description: "Outputs a summary page for GitHub actions";
@@ -126,6 +121,10 @@ export declare const runAllTestsCommand: import("yargs").CommandModule<unknown,
126
121
  readonly string: true;
127
122
  readonly description: string;
128
123
  };
124
+ readonly baseTestRunId: {
125
+ readonly string: true;
126
+ readonly description: "The id of a test run to compare screenshots against.";
127
+ };
129
128
  readonly moveBeforeClick: {
130
129
  readonly boolean: true;
131
130
  readonly description: "Simulate mouse movement before clicking";
@@ -7,7 +7,7 @@ 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, padTime, 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,
@@ -45,8 +45,8 @@ const handler = async ({ apiToken, commitSha: commitSha_, baseCommitSha, appUrl,
45
45
  apiToken: apiToken !== null && apiToken !== void 0 ? apiToken : null,
46
46
  commitSha,
47
47
  baseCommitSha: baseCommitSha !== null && baseCommitSha !== void 0 ? baseCommitSha : null,
48
+ baseTestRunId: baseTestRunId !== null && baseTestRunId !== void 0 ? baseTestRunId : null,
48
49
  appUrl: appUrl !== null && appUrl !== void 0 ? appUrl : null,
49
- useAssetsSnapshottedInBaseSimulation,
50
50
  parallelTasks: parrelelTasks !== null && parrelelTasks !== void 0 ? parrelelTasks : null,
51
51
  deflake,
52
52
  maxRetriesOnFailure,
@@ -70,13 +70,6 @@ exports.runAllTestsCommand = (0, command_builder_1.buildCommand)("run-all-tests"
70
70
  string: true,
71
71
  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
72
  },
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
73
  githubSummary: {
81
74
  boolean: true,
82
75
  description: "Outputs a summary page for GitHub actions",
@@ -117,6 +110,10 @@ exports.runAllTestsCommand = (0, command_builder_1.buildCommand)("run-all-tests"
117
110
  description: "The path to the meticulous.json file containing the list of tests you want to run." +
118
111
  " If not set a search will be performed to find a meticulous.json file in the current directory or the nearest parent directory.",
119
112
  },
113
+ baseTestRunId: {
114
+ string: true,
115
+ description: "The id of a test run to compare screenshots against.",
116
+ },
120
117
  moveBeforeClick: common_options_1.OPTIONS.moveBeforeClick,
121
118
  ...common_options_1.COMMON_REPLAY_OPTIONS,
122
119
  ...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;