@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.
- package/dist/api/replay.api.d.ts +1 -1
- package/dist/api/session.api.d.ts +2 -1
- package/dist/commands/replay/replay.command.d.ts +10 -3
- package/dist/commands/replay/replay.command.js +48 -22
- package/dist/commands/replay/utils/{compute-and-save-diff.d.ts → compute-diff.d.ts} +3 -2
- package/dist/commands/replay/utils/{compute-and-save-diff.js → compute-diff.js} +5 -21
- package/dist/commands/run-all-tests/run-all-tests.command.d.ts +5 -0
- package/dist/commands/run-all-tests/run-all-tests.command.js +7 -1
- package/dist/commands/screenshot-diff/screenshot-diff.command.d.ts +5 -2
- package/dist/commands/screenshot-diff/screenshot-diff.command.js +23 -18
- package/dist/config/config.types.d.ts +6 -1
- package/dist/deflake-tests/deflake-tests.handler.d.ts +1 -0
- package/dist/deflake-tests/deflake-tests.handler.js +2 -5
- package/dist/local-data/local-data.utils.d.ts +19 -0
- package/dist/local-data/local-data.utils.js +95 -1
- package/dist/local-data/replays.d.ts +2 -1
- package/dist/local-data/replays.js +30 -32
- package/dist/local-data/sessions.d.ts +2 -1
- package/dist/local-data/sessions.js +12 -27
- package/dist/main.js +1 -7
- package/dist/parallel-tests/__tests__/merge-test-results.spec.d.ts +1 -0
- package/dist/parallel-tests/__tests__/merge-test-results.spec.js +179 -0
- package/dist/parallel-tests/merge-test-results.d.ts +10 -0
- package/dist/parallel-tests/merge-test-results.js +98 -0
- package/dist/parallel-tests/parallel-tests.handler.d.ts +1 -0
- package/dist/parallel-tests/parallel-tests.handler.js +113 -24
- package/dist/parallel-tests/run-all-tests.d.ts +7 -1
- package/dist/parallel-tests/run-all-tests.js +35 -52
- package/dist/parallel-tests/run-all-tests.types.d.ts +1 -0
- package/dist/tsconfig.tsbuildinfo +1 -1
- package/dist/utils/sentry.utils.d.ts +0 -1
- package/dist/utils/sentry.utils.js +19 -14
- package/package.json +26 -6
package/dist/api/replay.api.d.ts
CHANGED
|
@@ -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<
|
|
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<
|
|
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
|
-
|
|
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_,
|
|
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
|
|
50
|
-
const replayCommandHandler = async ({ replayTarget, executionOptions, screenshottingOptions, apiToken, sessionId, commitSha: commitSha_,
|
|
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
|
|
167
|
-
|
|
168
|
-
|
|
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
|
|
180
|
+
baseReplayId,
|
|
171
181
|
headReplayId: replay.id,
|
|
172
182
|
tempDir,
|
|
173
183
|
screenshottingOptions,
|
|
174
|
-
|
|
184
|
+
logger: computeDiffsLogger,
|
|
175
185
|
})
|
|
176
186
|
: {
|
|
177
|
-
screenshotDiffResults:
|
|
187
|
+
screenshotDiffResults: [],
|
|
178
188
|
screenshotDiffsSummary: { hasDiffs: false },
|
|
179
189
|
};
|
|
180
|
-
|
|
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
|
|
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.
|
|
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
|
|
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.
|
|
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
|
-
|
|
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.
|
|
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
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
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
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
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
|
-
|
|
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<
|
|
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;
|