@alwaysmeticulous/cli 2.19.3 → 2.20.1
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 +3 -2
- package/dist/commands/replay/replay.command.js +46 -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 -2
- 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 +191 -0
- package/dist/parallel-tests/merge-test-results.d.ts +10 -0
- package/dist/parallel-tests/merge-test-results.js +101 -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,6 +10,7 @@ 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;
|
|
@@ -22,7 +23,7 @@ export interface ReplayResult {
|
|
|
22
23
|
*/
|
|
23
24
|
screenshotDiffsSummary: ScreenshotDiffsSummary;
|
|
24
25
|
}
|
|
25
|
-
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>;
|
|
26
27
|
export interface RawReplayCommandHandlerOptions extends ScreenshotDiffOptions, Omit<ReplayExecutionOptions, "maxDurationMs" | "maxEventCount">, AdditionalReplayOptions {
|
|
27
28
|
screenshot: boolean;
|
|
28
29
|
appUrl: string | null | undefined;
|
|
@@ -31,12 +32,12 @@ export interface RawReplayCommandHandlerOptions extends ScreenshotDiffOptions, O
|
|
|
31
32
|
maxDurationMs: number | null | undefined;
|
|
32
33
|
maxEventCount: number | null | undefined;
|
|
33
34
|
storyboard: boolean;
|
|
35
|
+
save: boolean | null | undefined;
|
|
34
36
|
}
|
|
35
37
|
interface AdditionalReplayOptions {
|
|
36
38
|
apiToken: string | null | undefined;
|
|
37
39
|
commitSha: string | null | undefined;
|
|
38
40
|
sessionId: string;
|
|
39
|
-
save: boolean | null | undefined;
|
|
40
41
|
baseSimulationId: string | null | undefined;
|
|
41
42
|
cookiesFile: string | null | undefined;
|
|
42
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,34 +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
|
-
op: "
|
|
167
|
+
const computeDiffSpan = transaction.startChild({
|
|
168
|
+
op: "computeDiff",
|
|
168
169
|
});
|
|
169
|
-
const
|
|
170
|
-
|
|
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)({
|
|
171
179
|
client,
|
|
172
|
-
baseReplayId
|
|
180
|
+
baseReplayId,
|
|
173
181
|
headReplayId: replay.id,
|
|
174
182
|
tempDir,
|
|
175
183
|
screenshottingOptions,
|
|
176
|
-
|
|
184
|
+
logger: computeDiffsLogger,
|
|
177
185
|
})
|
|
178
186
|
: {
|
|
179
187
|
screenshotDiffResults: [],
|
|
180
188
|
screenshotDiffsSummary: { hasDiffs: false },
|
|
181
189
|
};
|
|
182
|
-
|
|
183
|
-
// 13. Add test case to meticulous.json if --save option is passed
|
|
184
|
-
if (save) {
|
|
185
|
-
if (!screenshottingOptions.enabled) {
|
|
186
|
-
logger.error("Warning: saving a new test case without screenshot enabled.");
|
|
187
|
-
}
|
|
188
|
-
await (0, config_utils_1.addTestCase)({
|
|
189
|
-
title: `${sessionId} | ${replay.id}`,
|
|
190
|
-
sessionId,
|
|
191
|
-
baseReplayId: replay.id,
|
|
192
|
-
});
|
|
193
|
-
}
|
|
190
|
+
computeDiffSpan.finish();
|
|
194
191
|
return { replay, screenshotDiffResults, screenshotDiffsSummary };
|
|
195
192
|
}
|
|
196
193
|
finally {
|
|
@@ -246,7 +243,7 @@ const rawReplayCommandHandler = async ({ apiToken, commitSha, sessionId, appUrl,
|
|
|
246
243
|
}
|
|
247
244
|
: { enabled: false };
|
|
248
245
|
const replayEventsDependencies = await (0, replay_assets_1.loadReplayEventsDependencies)();
|
|
249
|
-
const { replay, screenshotDiffsSummary } = await (0, exports.replayCommandHandler)({
|
|
246
|
+
const { replay, screenshotDiffsSummary, screenshotDiffResults } = await (0, exports.replayCommandHandler)({
|
|
250
247
|
replayTarget: (0, exports.getReplayTarget)({
|
|
251
248
|
appUrl: appUrl !== null && appUrl !== void 0 ? appUrl : null,
|
|
252
249
|
simulationIdForAssets: simulationIdForAssets !== null && simulationIdForAssets !== void 0 ? simulationIdForAssets : null,
|
|
@@ -258,12 +255,39 @@ const rawReplayCommandHandler = async ({ apiToken, commitSha, sessionId, appUrl,
|
|
|
258
255
|
cookiesFile,
|
|
259
256
|
sessionId,
|
|
260
257
|
baseSimulationId,
|
|
261
|
-
save,
|
|
262
258
|
generatedBy: generatedByOption,
|
|
263
259
|
testRunId: null,
|
|
264
260
|
replayEventsDependencies,
|
|
261
|
+
suppressScreenshotDiffLogging: false,
|
|
265
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
|
+
}
|
|
266
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
|
+
});
|
|
267
291
|
process.exit(1);
|
|
268
292
|
}
|
|
269
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,11 +17,11 @@ 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
26
|
return {
|
|
27
27
|
...testCase,
|
|
@@ -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;
|