@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
@@ -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 {};
@@ -20,11 +20,6 @@ export declare const OPTIONS: {
20
20
  readonly description: "Enables bypass CSP in the browser (danger: this could mean you tests hit your production backend)";
21
21
  readonly default: false;
22
22
  };
23
- readonly padTime: {
24
- readonly boolean: true;
25
- readonly description: "Pad replay time according to recording duration. Please note this option will be ignored if running with the '--skipPauses' option.";
26
- readonly default: true;
27
- };
28
23
  readonly shiftTime: {
29
24
  readonly boolean: true;
30
25
  readonly description: "Shift time during simulation to be set as the recording time";
@@ -120,11 +115,6 @@ export declare const COMMON_REPLAY_OPTIONS: {
120
115
  readonly description: "Enables bypass CSP in the browser (danger: this could mean you tests hit your production backend)";
121
116
  readonly default: false;
122
117
  };
123
- padTime: {
124
- readonly boolean: true;
125
- readonly description: "Pad replay time according to recording duration. Please note this option will be ignored if running with the '--skipPauses' option.";
126
- readonly default: true;
127
- };
128
118
  shiftTime: {
129
119
  readonly boolean: true;
130
120
  readonly description: "Shift time during simulation to be set as the recording time";
@@ -33,11 +33,6 @@ exports.OPTIONS = {
33
33
  description: "Enables bypass CSP in the browser (danger: this could mean you tests hit your production backend)",
34
34
  default: false,
35
35
  },
36
- padTime: {
37
- boolean: true,
38
- description: "Pad replay time according to recording duration. Please note this option will be ignored if running with the '--skipPauses' option.",
39
- default: true,
40
- },
41
36
  shiftTime: {
42
37
  boolean: true,
43
38
  description: "Shift time during simulation to be set as the recording time",
@@ -109,7 +104,6 @@ exports.COMMON_REPLAY_OPTIONS = {
109
104
  headless: exports.OPTIONS.headless,
110
105
  devTools: exports.OPTIONS.devTools,
111
106
  bypassCSP: exports.OPTIONS.bypassCSP,
112
- padTime: exports.OPTIONS.padTime,
113
107
  shiftTime: exports.OPTIONS.shiftTime,
114
108
  networkStubbing: exports.OPTIONS.networkStubbing,
115
109
  skipPauses: exports.OPTIONS.skipPauses,
@@ -20,11 +20,6 @@ export declare const createTestCommand: import("yargs").CommandModule<unknown, i
20
20
  readonly description: "Enables bypass CSP in the browser (danger: this could mean you tests hit your production backend)";
21
21
  readonly default: false;
22
22
  };
23
- padTime: {
24
- readonly boolean: true;
25
- readonly description: "Pad replay time according to recording duration. Please note this option will be ignored if running with the '--skipPauses' option.";
26
- readonly default: true;
27
- };
28
23
  shiftTime: {
29
24
  readonly boolean: true;
30
25
  readonly description: "Shift time during simulation to be set as the recording time";
@@ -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
  };
@@ -60,7 +59,7 @@ apiToken, commitSha, devTools, bypassCSP,
60
59
  // Record options
61
60
  width, height, uploadIntervalMs, incognito, trace,
62
61
  // Replay options
63
- headless, screenshotSelector, padTime, shiftTime, networkStubbing, moveBeforeClick, cookiesFile, disableRemoteFonts, noSandbox, skipPauses, }) => {
62
+ headless, screenshotSelector, shiftTime, networkStubbing, moveBeforeClick, cookiesFile, disableRemoteFonts, noSandbox, skipPauses, }) => {
64
63
  const logger = loglevel_1.default.getLogger(common_1.METICULOUS_LOGGER_NAME);
65
64
  logger.info("Creating a new Meticulous test");
66
65
  logger.info("Step 1: record a new test");
@@ -97,7 +96,6 @@ headless, screenshotSelector, padTime, shiftTime, networkStubbing, moveBeforeCli
97
96
  bypassCSP,
98
97
  screenshot: true,
99
98
  screenshotSelector,
100
- padTime,
101
99
  shiftTime,
102
100
  disableRemoteFonts,
103
101
  noSandbox,
@@ -105,12 +103,10 @@ headless, screenshotSelector, padTime, shiftTime, networkStubbing, moveBeforeCli
105
103
  moveBeforeClick,
106
104
  cookiesFile,
107
105
  skipPauses,
108
- save: false,
109
106
  // We replay against the original recorded URL
110
107
  appUrl: null,
111
108
  simulationIdForAssets: null,
112
109
  // We don't try comparing to the original screenshot, so just set these to their defaults
113
- baseSimulationId: null,
114
110
  diffThreshold: common_options_1.OPTIONS.diffThreshold.default,
115
111
  diffPixelThreshold: common_options_1.OPTIONS.diffPixelThreshold.default,
116
112
  // 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, 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;
@@ -78,11 +72,6 @@ export declare const replayCommand: import("yargs").CommandModule<unknown, impor
78
72
  readonly description: "Enables bypass CSP in the browser (danger: this could mean you tests hit your production backend)";
79
73
  readonly default: false;
80
74
  };
81
- padTime: {
82
- readonly boolean: true;
83
- readonly description: "Pad replay time according to recording duration. Please note this option will be ignored if running with the '--skipPauses' option.";
84
- readonly default: true;
85
- };
86
75
  shiftTime: {
87
76
  readonly boolean: true;
88
77
  readonly description: "Shift time during simulation to be set as the recording time";
@@ -149,15 +138,6 @@ export declare const replayCommand: import("yargs").CommandModule<unknown, impor
149
138
  string: true;
150
139
  description: string;
151
140
  };
152
- baseSimulationId: {
153
- string: true;
154
- description: string;
155
- alias: string;
156
- };
157
- save: {
158
- boolean: true;
159
- description: string;
160
- };
161
141
  debugger: {
162
142
  boolean: true;
163
143
  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,12 +232,11 @@ 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, shiftTime, networkStubbing, moveBeforeClick, cookiesFile, disableRemoteFonts, noSandbox, skipPauses, maxDurationMs, maxEventCount, storyboard, essentialFeaturesOnly, debugger: enableStepThroughDebugger, }) => {
240
236
  const executionOptions = {
241
237
  headless,
242
238
  devTools,
243
239
  bypassCSP,
244
- padTime,
245
240
  shiftTime,
246
241
  networkStubbing,
247
242
  skipPauses,
@@ -265,7 +260,7 @@ const rawReplayCommandHandler = async ({ apiToken, commitSha, sessionId, appUrl,
265
260
  }
266
261
  : { enabled: false };
267
262
  const replayEventsDependencies = await (0, replay_assets_1.loadReplayEventsDependencies)();
268
- const { replay, screenshotDiffsSummary, screenshotDiffResults } = await (0, exports.replayCommandHandler)({
263
+ const { replay } = await (0, exports.replayCommandHandler)({
269
264
  replayTarget: (0, exports.getReplayTarget)({
270
265
  appUrl: appUrl !== null && appUrl !== void 0 ? appUrl : null,
271
266
  simulationIdForAssets: simulationIdForAssets !== null && simulationIdForAssets !== void 0 ? simulationIdForAssets : null,
@@ -276,43 +271,13 @@ const rawReplayCommandHandler = async ({ apiToken, commitSha, sessionId, appUrl,
276
271
  commitSha,
277
272
  cookiesFile,
278
273
  sessionId,
279
- baseSimulationId,
280
274
  generatedBy: generatedByOption,
275
+ baseTestRunId: null,
281
276
  testRunId: null,
282
277
  replayEventsDependencies,
283
278
  debugger: enableStepThroughDebugger,
284
279
  suppressScreenshotDiffLogging: false,
285
280
  });
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
281
  return replay;
317
282
  };
318
283
  exports.rawReplayCommandHandler = rawReplayCommandHandler;
@@ -356,15 +321,6 @@ exports.replayCommand = (0, command_builder_1.buildCommand)("simulate")
356
321
  string: true,
357
322
  description: "Query selector to screenshot a specific DOM element instead of the whole page",
358
323
  },
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
324
  debugger: {
369
325
  boolean: true,
370
326
  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`);
@@ -30,11 +30,6 @@ export declare const runAllTestsCommand: import("yargs").CommandModule<unknown,
30
30
  readonly description: "Enables bypass CSP in the browser (danger: this could mean you tests hit your production backend)";
31
31
  readonly default: false;
32
32
  };
33
- readonly padTime: {
34
- readonly boolean: true;
35
- readonly description: "Pad replay time according to recording duration. Please note this option will be ignored if running with the '--skipPauses' option.";
36
- readonly default: true;
37
- };
38
33
  readonly shiftTime: {
39
34
  readonly boolean: true;
40
35
  readonly description: "Shift time during simulation to be set as the recording time";
@@ -87,11 +82,6 @@ export declare const runAllTestsCommand: import("yargs").CommandModule<unknown,
87
82
  readonly string: true;
88
83
  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
84
  };
90
- readonly useAssetsSnapshottedInBaseSimulation: {
91
- readonly boolean: true;
92
- readonly description: string;
93
- readonly default: false;
94
- };
95
85
  readonly githubSummary: {
96
86
  readonly boolean: true;
97
87
  readonly description: "Outputs a summary page for GitHub actions";
@@ -126,6 +116,10 @@ export declare const runAllTestsCommand: import("yargs").CommandModule<unknown,
126
116
  readonly string: true;
127
117
  readonly description: string;
128
118
  };
119
+ readonly baseTestRunId: {
120
+ readonly string: true;
121
+ readonly description: "The id of a test run to compare screenshots against.";
122
+ };
129
123
  readonly moveBeforeClick: {
130
124
  readonly boolean: true;
131
125
  readonly description: "Simulate mouse movement before clicking";