@alwaysmeticulous/cli 2.19.2 → 2.20.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 (33) hide show
  1. package/dist/api/replay.api.d.ts +1 -1
  2. package/dist/api/session.api.d.ts +2 -1
  3. package/dist/commands/replay/replay.command.d.ts +10 -3
  4. package/dist/commands/replay/replay.command.js +48 -22
  5. package/dist/commands/replay/utils/{compute-and-save-diff.d.ts → compute-diff.d.ts} +3 -2
  6. package/dist/commands/replay/utils/{compute-and-save-diff.js → compute-diff.js} +5 -21
  7. package/dist/commands/run-all-tests/run-all-tests.command.d.ts +5 -0
  8. package/dist/commands/run-all-tests/run-all-tests.command.js +7 -1
  9. package/dist/commands/screenshot-diff/screenshot-diff.command.d.ts +5 -2
  10. package/dist/commands/screenshot-diff/screenshot-diff.command.js +23 -18
  11. package/dist/config/config.types.d.ts +6 -1
  12. package/dist/deflake-tests/deflake-tests.handler.d.ts +1 -0
  13. package/dist/deflake-tests/deflake-tests.handler.js +2 -5
  14. package/dist/local-data/local-data.utils.d.ts +19 -0
  15. package/dist/local-data/local-data.utils.js +95 -1
  16. package/dist/local-data/replays.d.ts +2 -1
  17. package/dist/local-data/replays.js +30 -32
  18. package/dist/local-data/sessions.d.ts +2 -1
  19. package/dist/local-data/sessions.js +12 -27
  20. package/dist/main.js +1 -7
  21. package/dist/parallel-tests/__tests__/merge-test-results.spec.d.ts +1 -0
  22. package/dist/parallel-tests/__tests__/merge-test-results.spec.js +179 -0
  23. package/dist/parallel-tests/merge-test-results.d.ts +10 -0
  24. package/dist/parallel-tests/merge-test-results.js +98 -0
  25. package/dist/parallel-tests/parallel-tests.handler.d.ts +1 -0
  26. package/dist/parallel-tests/parallel-tests.handler.js +113 -24
  27. package/dist/parallel-tests/run-all-tests.d.ts +7 -1
  28. package/dist/parallel-tests/run-all-tests.js +35 -52
  29. package/dist/parallel-tests/run-all-tests.types.d.ts +1 -0
  30. package/dist/tsconfig.tsbuildinfo +1 -1
  31. package/dist/utils/sentry.utils.d.ts +0 -1
  32. package/dist/utils/sentry.utils.js +19 -14
  33. package/package.json +26 -6
@@ -1,6 +1,6 @@
1
1
  import { Replay } from "@alwaysmeticulous/common";
2
2
  import { AxiosInstance } from "axios";
3
- export declare const getReplay: (client: AxiosInstance, replayId: string) => Promise<any>;
3
+ export declare const getReplay: (client: AxiosInstance, replayId: string) => Promise<Replay>;
4
4
  export declare const createReplay: (options: {
5
5
  client: AxiosInstance;
6
6
  commitSha: string;
@@ -1,5 +1,6 @@
1
+ import { SessionData } from "@alwaysmeticulous/api";
1
2
  import { AxiosInstance } from "axios";
2
3
  export declare const getRecordedSession: (client: AxiosInstance, sessionId: string) => Promise<any>;
3
- export declare const getRecordedSessionData: (client: AxiosInstance, sessionId: string) => Promise<any>;
4
+ export declare const getRecordedSessionData: (client: AxiosInstance, sessionId: string) => Promise<SessionData>;
4
5
  export declare const getRecordingCommandId: (client: AxiosInstance) => Promise<string>;
5
6
  export declare const postSessionIdNotification: (client: AxiosInstance, sessionId: string, recordingCommandId: string) => Promise<void>;
@@ -10,13 +10,20 @@ export interface ReplayOptions extends AdditionalReplayOptions {
10
10
  generatedBy: GeneratedBy;
11
11
  testRunId: string | null;
12
12
  replayEventsDependencies: ReplayEventsDependencies;
13
+ suppressScreenshotDiffLogging: boolean;
13
14
  }
14
15
  export interface ReplayResult {
15
16
  replay: Replay;
16
- screenshotDiffResults: ScreenshotDiffResult[] | null;
17
+ /**
18
+ * Empty if screenshottingOptions.enabled was false.
19
+ */
20
+ screenshotDiffResults: ScreenshotDiffResult[];
21
+ /**
22
+ * Returned as { hasDiffs: false } if screenshottingOptions.enabled was false.
23
+ */
17
24
  screenshotDiffsSummary: ScreenshotDiffsSummary;
18
25
  }
19
- export declare const replayCommandHandler: ({ replayTarget, executionOptions, screenshottingOptions, apiToken, sessionId, commitSha: commitSha_, save, baseSimulationId: baseReplayId_, cookiesFile, generatedBy, testRunId, replayEventsDependencies, }: ReplayOptions) => Promise<ReplayResult>;
26
+ export declare const replayCommandHandler: ({ replayTarget, executionOptions, screenshottingOptions, apiToken, sessionId, commitSha: commitSha_, baseSimulationId: baseReplayId, cookiesFile, generatedBy, testRunId, replayEventsDependencies, suppressScreenshotDiffLogging, }: ReplayOptions) => Promise<ReplayResult>;
20
27
  export interface RawReplayCommandHandlerOptions extends ScreenshotDiffOptions, Omit<ReplayExecutionOptions, "maxDurationMs" | "maxEventCount">, AdditionalReplayOptions {
21
28
  screenshot: boolean;
22
29
  appUrl: string | null | undefined;
@@ -25,12 +32,12 @@ export interface RawReplayCommandHandlerOptions extends ScreenshotDiffOptions, O
25
32
  maxDurationMs: number | null | undefined;
26
33
  maxEventCount: number | null | undefined;
27
34
  storyboard: boolean;
35
+ save: boolean | null | undefined;
28
36
  }
29
37
  interface AdditionalReplayOptions {
30
38
  apiToken: string | null | undefined;
31
39
  commitSha: string | null | undefined;
32
40
  sessionId: string;
33
- save: boolean | null | undefined;
34
41
  baseSimulationId: string | null | undefined;
35
42
  cookiesFile: string | null | undefined;
36
43
  }
@@ -34,6 +34,7 @@ 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");
37
38
  const replay_api_1 = require("../../api/replay.api");
38
39
  const upload_1 = require("../../api/upload");
39
40
  const archive_1 = require("../../archive/archive");
@@ -46,8 +47,8 @@ const sessions_1 = require("../../local-data/sessions");
46
47
  const commit_sha_utils_1 = require("../../utils/commit-sha.utils");
47
48
  const config_utils_1 = require("../../utils/config.utils");
48
49
  const version_utils_1 = require("../../utils/version.utils");
49
- const compute_and_save_diff_1 = require("./utils/compute-and-save-diff");
50
- const replayCommandHandler = async ({ replayTarget, executionOptions, screenshottingOptions, apiToken, sessionId, commitSha: commitSha_, save, baseSimulationId: baseReplayId_, cookiesFile, generatedBy, testRunId, replayEventsDependencies, }) => {
50
+ const compute_diff_1 = require("./utils/compute-diff");
51
+ const replayCommandHandler = async ({ replayTarget, executionOptions, screenshottingOptions, apiToken, sessionId, commitSha: commitSha_, baseSimulationId: baseReplayId, cookiesFile, generatedBy, testRunId, replayEventsDependencies, suppressScreenshotDiffLogging, }) => {
51
52
  const transaction = Sentry.startTransaction({
52
53
  name: "replay.command_handler",
53
54
  description: "Handle the replay command",
@@ -163,32 +164,30 @@ const replayCommandHandler = async ({ replayTarget, executionOptions, screenshot
163
164
  logger.info(`View simulation at: ${replayUrl}`);
164
165
  logger.info("=======");
165
166
  // 12. Diff against base replay screenshot if one is provided
166
- const computeAndSaveDiffSpan = transaction.startChild({ op: "computeAndSaveDiff" });
167
- const { screenshotDiffResults, screenshotDiffsSummary } = screenshottingOptions.enabled && baseReplayId_
168
- ? await (0, compute_and_save_diff_1.computeAndSaveDiff)({
167
+ const computeDiffSpan = transaction.startChild({
168
+ op: "computeDiff",
169
+ });
170
+ const computeDiffsLogger = loglevel_1.default.getLogger(`METICULOUS_LOGGER_NAME/compute-diffs`);
171
+ if (suppressScreenshotDiffLogging) {
172
+ computeDiffsLogger.setLevel("ERROR", false);
173
+ }
174
+ else {
175
+ computeDiffsLogger.setLevel(logger.getLevel(), false);
176
+ }
177
+ const { screenshotDiffResults, screenshotDiffsSummary } = screenshottingOptions.enabled && baseReplayId
178
+ ? await (0, compute_diff_1.computeDiff)({
169
179
  client,
170
- baseReplayId: baseReplayId_ !== null && baseReplayId_ !== void 0 ? baseReplayId_ : "",
180
+ baseReplayId,
171
181
  headReplayId: replay.id,
172
182
  tempDir,
173
183
  screenshottingOptions,
174
- testRunId,
184
+ logger: computeDiffsLogger,
175
185
  })
176
186
  : {
177
- screenshotDiffResults: null,
187
+ screenshotDiffResults: [],
178
188
  screenshotDiffsSummary: { hasDiffs: false },
179
189
  };
180
- computeAndSaveDiffSpan.finish();
181
- // 13. Add test case to meticulous.json if --save option is passed
182
- if (save) {
183
- if (!screenshottingOptions.enabled) {
184
- logger.error("Warning: saving a new test case without screenshot enabled.");
185
- }
186
- await (0, config_utils_1.addTestCase)({
187
- title: `${sessionId} | ${replay.id}`,
188
- sessionId,
189
- baseReplayId: replay.id,
190
- });
191
- }
190
+ computeDiffSpan.finish();
192
191
  return { replay, screenshotDiffResults, screenshotDiffsSummary };
193
192
  }
194
193
  finally {
@@ -244,7 +243,7 @@ const rawReplayCommandHandler = async ({ apiToken, commitSha, sessionId, appUrl,
244
243
  }
245
244
  : { enabled: false };
246
245
  const replayEventsDependencies = await (0, replay_assets_1.loadReplayEventsDependencies)();
247
- const { replay, screenshotDiffsSummary } = await (0, exports.replayCommandHandler)({
246
+ const { replay, screenshotDiffsSummary, screenshotDiffResults } = await (0, exports.replayCommandHandler)({
248
247
  replayTarget: (0, exports.getReplayTarget)({
249
248
  appUrl: appUrl !== null && appUrl !== void 0 ? appUrl : null,
250
249
  simulationIdForAssets: simulationIdForAssets !== null && simulationIdForAssets !== void 0 ? simulationIdForAssets : null,
@@ -256,12 +255,39 @@ const rawReplayCommandHandler = async ({ apiToken, commitSha, sessionId, appUrl,
256
255
  cookiesFile,
257
256
  sessionId,
258
257
  baseSimulationId,
259
- save,
260
258
  generatedBy: generatedByOption,
261
259
  testRunId: null,
262
260
  replayEventsDependencies,
261
+ suppressScreenshotDiffLogging: false,
263
262
  });
263
+ // Add test case to meticulous.json if --save option is passed
264
+ if (save) {
265
+ if (!screenshottingOptions.enabled) {
266
+ const logger = loglevel_1.default.getLogger(common_1.METICULOUS_LOGGER_NAME);
267
+ logger.error("Warning: saving a new test case without screenshot enabled.");
268
+ }
269
+ await (0, config_utils_1.addTestCase)({
270
+ title: `${sessionId} | ${replay.id}`,
271
+ sessionId,
272
+ baseReplayId: replay.id,
273
+ });
274
+ }
264
275
  if (screenshotDiffsSummary.hasDiffs) {
276
+ const client = (0, client_1.createClient)({ apiToken });
277
+ if (baseSimulationId == null) {
278
+ throw new Error("baseSimulationId must have been defined if there are diffs, but was null-ish");
279
+ }
280
+ // Store the diff
281
+ await (0, replay_diff_api_1.createReplayDiff)({
282
+ client,
283
+ headReplayId: replay.id,
284
+ baseReplayId: baseSimulationId,
285
+ testRunId: null,
286
+ data: {
287
+ screenshotAssertionsOptions: screenshottingOptions,
288
+ screenshotDiffResults,
289
+ },
290
+ });
265
291
  process.exit(1);
266
292
  }
267
293
  return replay;
@@ -1,15 +1,16 @@
1
1
  import { ScreenshotAssertionsEnabledOptions, ScreenshotDiffResult } from "@alwaysmeticulous/api";
2
2
  import { AxiosInstance } from "axios";
3
+ import log from "loglevel";
3
4
  import { ScreenshotDiffsSummary } from "../../screenshot-diff/screenshot-diff.command";
4
5
  export interface ComputeAndSaveDiffOptions {
5
6
  client: AxiosInstance;
6
- testRunId: string | null;
7
7
  baseReplayId: string;
8
8
  headReplayId: string;
9
9
  tempDir: string;
10
10
  screenshottingOptions: ScreenshotAssertionsEnabledOptions;
11
+ logger: log.Logger;
11
12
  }
12
- export declare const computeAndSaveDiff: ({ client, baseReplayId, tempDir, headReplayId, screenshottingOptions, testRunId, }: ComputeAndSaveDiffOptions) => Promise<{
13
+ export declare const computeDiff: ({ client, baseReplayId, tempDir, headReplayId, screenshottingOptions, logger, }: ComputeAndSaveDiffOptions) => Promise<{
13
14
  screenshotDiffResults: ScreenshotDiffResult[];
14
15
  screenshotDiffsSummary: ScreenshotDiffsSummary;
15
16
  }>;
@@ -1,16 +1,9 @@
1
1
  "use strict";
2
- var __importDefault = (this && this.__importDefault) || function (mod) {
3
- return (mod && mod.__esModule) ? mod : { "default": mod };
4
- };
5
2
  Object.defineProperty(exports, "__esModule", { value: true });
6
- exports.computeAndSaveDiff = void 0;
7
- const common_1 = require("@alwaysmeticulous/common");
8
- const loglevel_1 = __importDefault(require("loglevel"));
9
- const replay_diff_api_1 = require("../../../api/replay-diff.api");
3
+ exports.computeDiff = void 0;
10
4
  const replays_1 = require("../../../local-data/replays");
11
5
  const screenshot_diff_command_1 = require("../../screenshot-diff/screenshot-diff.command");
12
- const computeAndSaveDiff = async ({ client, baseReplayId, tempDir, headReplayId, screenshottingOptions, testRunId, }) => {
13
- const logger = loglevel_1.default.getLogger(common_1.METICULOUS_LOGGER_NAME);
6
+ const computeDiff = async ({ client, baseReplayId, tempDir, headReplayId, screenshottingOptions, logger, }) => {
14
7
  logger.info(`Diffing screenshots against replay ${baseReplayId}`);
15
8
  await (0, replays_1.getOrFetchReplay)(client, baseReplayId);
16
9
  await (0, replays_1.getOrFetchReplayArchive)(client, baseReplayId);
@@ -23,24 +16,15 @@ const computeAndSaveDiff = async ({ client, baseReplayId, tempDir, headReplayId,
23
16
  baseScreenshotsDir: baseReplayScreenshotsDir,
24
17
  headScreenshotsDir: headReplayScreenshotsDir,
25
18
  diffOptions: screenshottingOptions.diffOptions,
19
+ logger,
26
20
  });
27
- const replayDiff = await (0, replay_diff_api_1.createReplayDiff)({
28
- client,
29
- headReplayId,
30
- baseReplayId,
31
- testRunId,
32
- data: {
33
- screenshotAssertionsOptions: screenshottingOptions,
34
- screenshotDiffResults,
35
- },
36
- });
37
- logger.debug(replayDiff);
38
21
  const screenshotDiffsSummary = (0, screenshot_diff_command_1.summarizeDifferences)({
39
22
  baseReplayId,
40
23
  headReplayId,
41
24
  results: screenshotDiffResults,
42
25
  diffOptions: screenshottingOptions.diffOptions,
26
+ logger,
43
27
  });
44
28
  return { screenshotDiffResults, screenshotDiffsSummary };
45
29
  };
46
- exports.computeAndSaveDiff = computeAndSaveDiff;
30
+ exports.computeDiff = computeDiff;
@@ -107,6 +107,11 @@ export declare const runAllTestsCommand: import("yargs").CommandModule<unknown,
107
107
  readonly description: "Attempt to deflake failing tests";
108
108
  readonly default: false;
109
109
  };
110
+ readonly maxRetriesOnFailure: {
111
+ readonly number: true;
112
+ readonly description: "If set to a value greater than 0 then will re-run any replays that give a screenshot diff and mark them as a flake if the screenshot generated on one of the retryed replays differs from that in the first replay.";
113
+ readonly default: 0;
114
+ };
110
115
  readonly useCache: {
111
116
  readonly boolean: true;
112
117
  readonly description: "Use result cache";
@@ -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, useCache, testsFile, disableRemoteFonts, noSandbox, skipPauses, moveBeforeClick, maxDurationMs, maxEventCount, storyboard, }) => {
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, }) => {
11
11
  const executionOptions = {
12
12
  headless,
13
13
  devTools,
@@ -48,6 +48,7 @@ const handler = async ({ apiToken, commitSha: commitSha_, baseCommitSha, appUrl,
48
48
  useAssetsSnapshottedInBaseSimulation,
49
49
  parallelTasks: parrelelTasks !== null && parrelelTasks !== void 0 ? parrelelTasks : null,
50
50
  deflake,
51
+ maxRetriesOnFailure,
51
52
  cachedTestRunResults,
52
53
  githubSummary,
53
54
  });
@@ -100,6 +101,11 @@ exports.runAllTestsCommand = (0, command_builder_1.buildCommand)("run-all-tests"
100
101
  description: "Attempt to deflake failing tests",
101
102
  default: false,
102
103
  },
104
+ maxRetriesOnFailure: {
105
+ number: true,
106
+ description: "If set to a value greater than 0 then will re-run any replays that give a screenshot diff and mark them as a flake if the screenshot generated on one of the retryed replays differs from that in the first replay.",
107
+ default: 0,
108
+ },
103
109
  useCache: {
104
110
  boolean: true,
105
111
  description: "Use result cache",
@@ -1,19 +1,22 @@
1
1
  /// <reference types="yargs" />
2
2
  import { ScreenshotDiffOptions, ScreenshotDiffResult } from "@alwaysmeticulous/api";
3
3
  import { AxiosInstance } from "axios";
4
- export declare const diffScreenshots: ({ client, headReplayId, baseReplayId, headScreenshotsDir, baseScreenshotsDir, diffOptions, }: {
4
+ import log from "loglevel";
5
+ export declare const diffScreenshots: ({ client, headReplayId, baseReplayId, headScreenshotsDir, baseScreenshotsDir, diffOptions, logger, }: {
5
6
  client: AxiosInstance;
6
7
  baseReplayId: string;
7
8
  headReplayId: string;
8
9
  baseScreenshotsDir: string;
9
10
  headScreenshotsDir: string;
10
11
  diffOptions: ScreenshotDiffOptions;
12
+ logger: log.Logger;
11
13
  }) => Promise<ScreenshotDiffResult[]>;
12
- export declare const summarizeDifferences: ({ baseReplayId, headReplayId, results, diffOptions, }: {
14
+ export declare const summarizeDifferences: ({ baseReplayId, headReplayId, results, diffOptions, logger, }: {
13
15
  baseReplayId: string;
14
16
  headReplayId: string;
15
17
  results: ScreenshotDiffResult[];
16
18
  diffOptions: ScreenshotDiffOptions;
19
+ logger: log.Logger;
17
20
  }) => ScreenshotDiffsSummary;
18
21
  export declare type ScreenshotDiffsSummary = HasDiffsScreenshotDiffsResult | NoDiffsScreenshotDiffsResult;
19
22
  export interface HasDiffsScreenshotDiffsResult {
@@ -15,8 +15,7 @@ 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, }) => {
19
- const logger = loglevel_1.default.getLogger(common_1.METICULOUS_LOGGER_NAME);
18
+ const diffScreenshots = async ({ client, headReplayId, baseReplayId, headScreenshotsDir, baseScreenshotsDir, diffOptions, logger, }) => {
20
19
  const { diffThreshold, diffPixelThreshold } = diffOptions;
21
20
  const baseReplayScreenshots = await (0, replays_1.getScreenshotFiles)(baseScreenshotsDir);
22
21
  const headReplayScreenshots = await (0, replays_1.getScreenshotFiles)(headScreenshotsDir);
@@ -99,8 +98,7 @@ const diffScreenshots = async ({ client, headReplayId, baseReplayId, headScreens
99
98
  return [...missingHeadImagesResults, ...headDiffResults];
100
99
  };
101
100
  exports.diffScreenshots = diffScreenshots;
102
- const summarizeDifferences = ({ baseReplayId, headReplayId, results, diffOptions, }) => {
103
- const logger = loglevel_1.default.getLogger(common_1.METICULOUS_LOGGER_NAME);
101
+ const summarizeDifferences = ({ baseReplayId, headReplayId, results, diffOptions, logger, }) => {
104
102
  const missingHeadImagesResults = results.flatMap((result) => result.outcome === "missing-head" ? [result] : []);
105
103
  if (missingHeadImagesResults.length) {
106
104
  const message = `Head replay is missing screenshots: ${missingHeadImagesResults
@@ -121,17 +119,19 @@ const summarizeDifferences = ({ baseReplayId, headReplayId, results, diffOptions
121
119
  .sort()}`;
122
120
  logger.info(message);
123
121
  }
124
- results.forEach((result) => {
122
+ const diffs = results.flatMap((result) => {
125
123
  const { outcome } = result;
126
124
  if (outcome === "different-size") {
127
125
  const message = `Screenshots ${(0, path_1.basename)(result.headScreenshotFile)} have different sizes => FAIL!`;
128
126
  logger.info(message);
129
- return {
130
- hasDiffs: true,
131
- summaryMessage: message,
132
- baseReplayId,
133
- headReplayId,
134
- };
127
+ return [
128
+ {
129
+ hasDiffs: true,
130
+ summaryMessage: message,
131
+ baseReplayId,
132
+ headReplayId,
133
+ },
134
+ ];
135
135
  }
136
136
  if (outcome === "diff" || outcome === "no-diff") {
137
137
  const mismatch = (result.mismatchFraction * 100).toFixed(3);
@@ -139,16 +139,19 @@ const summarizeDifferences = ({ baseReplayId, headReplayId, results, diffOptions
139
139
  const message = `${mismatch}% pixel mismatch for screenshot ${(0, path_1.basename)(result.headScreenshotFile)} (threshold is ${threshold}%) => ${outcome === "no-diff" ? "PASS" : "FAIL!"}`;
140
140
  logger.info(message);
141
141
  if (outcome === "diff") {
142
- return {
143
- hasDiffs: true,
144
- summaryMessage: message,
145
- baseReplayId,
146
- headReplayId,
147
- };
142
+ return [
143
+ {
144
+ hasDiffs: true,
145
+ summaryMessage: message,
146
+ baseReplayId,
147
+ headReplayId,
148
+ },
149
+ ];
148
150
  }
149
151
  }
152
+ return [];
150
153
  });
151
- return { hasDiffs: false };
154
+ return diffs.length > 0 ? diffs[0] : { hasDiffs: false };
152
155
  };
153
156
  exports.summarizeDifferences = summarizeDifferences;
154
157
  const getScreenshotIdentifier = (filename) => {
@@ -190,6 +193,7 @@ const handler = async ({ apiToken, baseSimulationId: baseReplayId, headSimulatio
190
193
  baseScreenshotsDir,
191
194
  headScreenshotsDir,
192
195
  diffOptions,
196
+ logger,
193
197
  });
194
198
  logger.debug(results);
195
199
  const diffSummary = (0, exports.summarizeDifferences)({
@@ -197,6 +201,7 @@ const handler = async ({ apiToken, baseSimulationId: baseReplayId, headSimulatio
197
201
  headReplayId,
198
202
  results,
199
203
  diffOptions,
204
+ logger,
200
205
  });
201
206
  if (diffSummary.hasDiffs) {
202
207
  process.exit(1);
@@ -4,7 +4,12 @@ export interface MeticulousCliConfig {
4
4
  }
5
5
  export interface TestCaseResult extends TestCase {
6
6
  headReplayId: string;
7
- result: "pass" | "fail";
7
+ /**
8
+ * A test case is marked as a flake if there were screenshot comparison failures,
9
+ * but for every one of those failures regenerating the screenshot on head sometimes gave
10
+ * a different screenshot to the original screenshot taken on head.
11
+ */
12
+ result: "pass" | "fail" | "flake";
8
13
  }
9
14
  export interface DetailedTestCaseResult extends TestCaseResult {
10
15
  screenshotDiffResults: ScreenshotDiffResult[];
@@ -15,6 +15,7 @@ interface HandleReplayOptions {
15
15
  generatedBy: GeneratedBy;
16
16
  testRunId: string | null;
17
17
  replayEventsDependencies: ReplayEventsDependencies;
18
+ suppressScreenshotDiffLogging: boolean;
18
19
  }
19
20
  export declare const deflakeReplayCommandHandler: ({ deflake, ...otherOptions }: DeflakeReplayCommandHandlerOptions) => Promise<DetailedTestCaseResult>;
20
21
  export {};
@@ -7,7 +7,7 @@ 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, }) => {
10
+ const handleReplay = async ({ testCase, replayTarget, executionOptions, screenshottingOptions, apiToken, commitSha, generatedBy, testRunId, replayEventsDependencies, suppressScreenshotDiffLogging, }) => {
11
11
  var _a, _b;
12
12
  const { replay, screenshotDiffResults, screenshotDiffsSummary } = await (0, replay_command_1.replayCommandHandler)({
13
13
  replayTarget,
@@ -17,15 +17,12 @@ const handleReplay = async ({ testCase, replayTarget, executionOptions, screensh
17
17
  commitSha,
18
18
  sessionId: testCase.sessionId,
19
19
  baseSimulationId: testCase.baseReplayId,
20
- save: false,
21
20
  cookiesFile: null,
22
21
  generatedBy,
23
22
  testRunId,
24
23
  replayEventsDependencies,
24
+ suppressScreenshotDiffLogging,
25
25
  });
26
- if (screenshotDiffResults == null) {
27
- throw new Error(`replayCommandHandler returned a null screenshotDiffResults, but was called with screenshottingOptions.enabled = true`);
28
- }
29
26
  return {
30
27
  ...testCase,
31
28
  headReplayId: replay.id,
@@ -1 +1,20 @@
1
1
  export declare const sanitizeFilename: (filename: string) => string;
2
+ declare type ReleaseLock = () => Promise<void>;
3
+ export interface LoadOrDownloadJsonFileOptions<T> {
4
+ filePath: string;
5
+ downloadJson: () => Promise<T | null>;
6
+ /**
7
+ * For debug messages e.g. 'session' or 'session data'
8
+ */
9
+ dataDescription: string;
10
+ }
11
+ /**
12
+ * Returns the JSON.parse'd contents of the file at the given path. If the file
13
+ * doesn't exist yet then it downloads the object, writes it to the file, and returns it.
14
+ *
15
+ * Handles concurrent processes trying to download to the same file at the same time.
16
+ */
17
+ export declare const getOrDownloadJsonFile: <T>({ filePath, downloadJson, dataDescription, }: LoadOrDownloadJsonFileOptions<T>) => Promise<T | null>;
18
+ export declare const fileExists: (filePath: string) => Promise<boolean>;
19
+ export declare const waitToAcquireLockOnDirectory: (directoryPath: string) => Promise<ReleaseLock>;
20
+ export {};
@@ -1,7 +1,101 @@
1
1
  "use strict";
2
+ var __importDefault = (this && this.__importDefault) || function (mod) {
3
+ return (mod && mod.__esModule) ? mod : { "default": mod };
4
+ };
2
5
  Object.defineProperty(exports, "__esModule", { value: true });
3
- exports.sanitizeFilename = void 0;
6
+ exports.waitToAcquireLockOnDirectory = exports.fileExists = exports.getOrDownloadJsonFile = exports.sanitizeFilename = void 0;
7
+ const promises_1 = require("fs/promises");
8
+ const path_1 = require("path");
9
+ const common_1 = require("@alwaysmeticulous/common");
10
+ const loglevel_1 = __importDefault(require("loglevel"));
11
+ const luxon_1 = require("luxon");
12
+ const proper_lockfile_1 = require("proper-lockfile");
4
13
  const sanitizeFilename = (filename) => {
5
14
  return filename.replace(/[^a-zA-Z0-9]/g, "_");
6
15
  };
7
16
  exports.sanitizeFilename = sanitizeFilename;
17
+ /**
18
+ * Returns the JSON.parse'd contents of the file at the given path. If the file
19
+ * doesn't exist yet then it downloads the object, writes it to the file, and returns it.
20
+ *
21
+ * Handles concurrent processes trying to download to the same file at the same time.
22
+ */
23
+ const getOrDownloadJsonFile = async ({ filePath, downloadJson, dataDescription, }) => {
24
+ const logger = loglevel_1.default.getLogger(common_1.METICULOUS_LOGGER_NAME);
25
+ await (0, promises_1.mkdir)((0, path_1.dirname)(filePath), { recursive: true });
26
+ // We create a lock file so that if multiple processes try downloading at the same
27
+ // time they don't interfere with each other. The second process to run will
28
+ // wait for the first process to complete, and then return straight away because
29
+ // it'll notice the file already exists.
30
+ const releaseLock = await waitToAcquireLockOnFile(filePath);
31
+ try {
32
+ const existingData = await (0, promises_1.readFile)(filePath)
33
+ .then((data) => JSON.parse(data.toString("utf-8")))
34
+ .catch(() => null);
35
+ if (existingData) {
36
+ logger.debug(`Reading ${dataDescription} from local copy in ${filePath}`);
37
+ return existingData;
38
+ }
39
+ const downloadedData = await downloadJson();
40
+ if (downloadedData) {
41
+ await (0, promises_1.writeFile)(filePath, JSON.stringify(downloadedData, null, 2));
42
+ logger.debug(`Wrote ${dataDescription} to ${filePath}`);
43
+ }
44
+ return downloadedData;
45
+ }
46
+ finally {
47
+ await releaseLock();
48
+ }
49
+ };
50
+ exports.getOrDownloadJsonFile = getOrDownloadJsonFile;
51
+ const waitToAcquireLockOnFile = async (filePath) => {
52
+ // In many cases the file doesn't exist yet, and can't exist yet (need to download the data, and creating an
53
+ // empty file beforehand is risky if the process crashes, and a second process tries reading the empty file).
54
+ // However proper-lockfile requires us to pass a file or directory as the first arg. This path is just used
55
+ // to detect if the same process tries taking out multiple locks on the same file. It just needs to be calculated
56
+ // as something that's unique to the file, and gives the same path for a given file everytime. So we create our
57
+ // own lock-target directory for this purpose (directory not file since mkdir is guaranteed to be synchronous).
58
+ // The path needs to actually exist, since proper-lockfile resolves symlinks on it.
59
+ const lockDirectory = `${filePath}.lock-target`;
60
+ await (0, promises_1.mkdir)(lockDirectory, { recursive: true });
61
+ try {
62
+ const releaseLock = await (0, proper_lockfile_1.lock)(lockDirectory, {
63
+ retries: LOCK_RETRY_OPTIONS,
64
+ lockfilePath: `${filePath}.lock`,
65
+ });
66
+ return async () => {
67
+ // Clean up our directory _before_ releasing the lock
68
+ // Note: it may have already been deleted: if we're just coming out of
69
+ // a lock released by someone else then they will have already deleted
70
+ // the directory
71
+ await (0, promises_1.rm)(lockDirectory, { recursive: true, force: true });
72
+ await releaseLock();
73
+ };
74
+ }
75
+ catch (err) {
76
+ await (0, promises_1.rm)(lockDirectory, { recursive: true, force: true });
77
+ throw err;
78
+ }
79
+ };
80
+ const fileExists = (filePath) => (0, promises_1.access)(filePath)
81
+ .then(() => true)
82
+ .catch(() => false);
83
+ exports.fileExists = fileExists;
84
+ const waitToAcquireLockOnDirectory = (directoryPath) => {
85
+ return (0, proper_lockfile_1.lock)(directoryPath, {
86
+ retries: LOCK_RETRY_OPTIONS,
87
+ lockfilePath: (0, path_1.join)(directoryPath, "dir.lock"),
88
+ });
89
+ };
90
+ exports.waitToAcquireLockOnDirectory = waitToAcquireLockOnDirectory;
91
+ const LOCK_RETRY_OPTIONS = {
92
+ // We want to keep on retrying till we get the maxRetryTime, so we set retries, which is a maximum, to a high value
93
+ retries: 1000,
94
+ factor: 1.05,
95
+ minTimeout: 500,
96
+ maxTimeout: 2000,
97
+ // Wait a maximum of 120s for the other process to finish downloading and/or extracting
98
+ maxRetryTime: luxon_1.Duration.fromObject({ minutes: 2 }).as("milliseconds"),
99
+ // Randomize so processes are less likely to clash on their retries
100
+ randomize: true,
101
+ };
@@ -1,5 +1,6 @@
1
+ import { Replay } from "@alwaysmeticulous/common";
1
2
  import { AxiosInstance } from "axios";
2
- export declare const getOrFetchReplay: (client: AxiosInstance, replayId: string) => Promise<any>;
3
+ export declare const getOrFetchReplay: (client: AxiosInstance, replayId: string) => Promise<Replay>;
3
4
  export declare const getOrFetchReplayArchive: (client: AxiosInstance, replayId: string) => Promise<void>;
4
5
  export declare const getScreenshotFiles: (screenshotsDirPath: string) => Promise<string[]>;
5
6
  export declare const getSnapshottedAssetsDir: (replayId: string) => string;