@alwaysmeticulous/cli 1.4.1 → 1.5.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/commands/debug-replay/debug-replay.command.js +8 -2
- package/dist/commands/record/record.command.js +27 -21
- package/dist/commands/replay/replay.command.js +21 -14
- package/dist/commands/run-all-tests/run-all-tests.command.d.ts +2 -0
- package/dist/commands/run-all-tests/run-all-tests.command.js +101 -56
- package/dist/commands/screenshot-diff/screenshot-diff.command.js +13 -5
- package/dist/commands/show-project/show-project.command.js +8 -2
- package/dist/commands/upload-build/upload-build.command.js +18 -12
- package/dist/local-data/replay-assets.js +5 -3
- package/dist/local-data/replays.js +9 -6
- package/dist/local-data/screenshot-diffs.js +6 -1
- package/dist/local-data/sessions.js +12 -6
- package/dist/main.js +7 -0
- package/dist/parallel-tests/messages.types.d.ts +28 -0
- package/dist/parallel-tests/messages.types.js +2 -0
- package/dist/parallel-tests/parallel-tests.handler.d.ts +21 -0
- package/dist/parallel-tests/parallel-tests.handler.js +120 -0
- package/dist/parallel-tests/task.handler.d.ts +1 -0
- package/dist/parallel-tests/task.handler.js +94 -0
- package/dist/tsconfig.tsbuildinfo +1 -1
- package/dist/utils/commit-sha.utils.js +7 -1
- package/dist/utils/github-summary.utils.js +7 -1
- package/dist/utils/logger.utils.d.ts +2 -0
- package/dist/utils/logger.utils.js +39 -0
- package/package.json +3 -3
|
@@ -1,11 +1,17 @@
|
|
|
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
6
|
exports.debugReplay = void 0;
|
|
7
|
+
const common_1 = require("@alwaysmeticulous/common");
|
|
8
|
+
const loglevel_1 = __importDefault(require("loglevel"));
|
|
4
9
|
const client_1 = require("../../api/client");
|
|
5
10
|
const replay_assets_1 = require("../../local-data/replay-assets");
|
|
6
11
|
const sessions_1 = require("../../local-data/sessions");
|
|
7
12
|
const sentry_utils_1 = require("../../utils/sentry.utils");
|
|
8
13
|
const handler = async ({ apiToken, sessionId, appUrl, devTools, networkStubbing, moveBeforeClick, cookiesFile, }) => {
|
|
14
|
+
const logger = loglevel_1.default.getLogger(common_1.METICULOUS_LOGGER_NAME);
|
|
9
15
|
const client = (0, client_1.createClient)({ apiToken });
|
|
10
16
|
// 1. Check session files
|
|
11
17
|
await (0, sessions_1.getOrFetchRecordedSession)(client, sessionId);
|
|
@@ -21,8 +27,8 @@ const handler = async ({ apiToken, sessionId, appUrl, devTools, networkStubbing,
|
|
|
21
27
|
createReplayer = replayDebugger.createReplayer;
|
|
22
28
|
}
|
|
23
29
|
catch (error) {
|
|
24
|
-
|
|
25
|
-
|
|
30
|
+
logger.error("Error: could not import @alwaysmeticulous/replay-debugger");
|
|
31
|
+
logger.error(error);
|
|
26
32
|
process.exit(1);
|
|
27
33
|
}
|
|
28
34
|
// 5. Start replay debugger
|
|
@@ -1,7 +1,11 @@
|
|
|
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
6
|
exports.record = void 0;
|
|
4
7
|
const common_1 = require("@alwaysmeticulous/common");
|
|
8
|
+
const loglevel_1 = __importDefault(require("loglevel"));
|
|
5
9
|
const path_1 = require("path");
|
|
6
10
|
const client_1 = require("../../api/client");
|
|
7
11
|
const project_api_1 = require("../../api/project.api");
|
|
@@ -10,9 +14,10 @@ const replay_assets_1 = require("../../local-data/replay-assets");
|
|
|
10
14
|
const commit_sha_utils_1 = require("../../utils/commit-sha.utils");
|
|
11
15
|
const sentry_utils_1 = require("../../utils/sentry.utils");
|
|
12
16
|
const handler = async ({ apiToken, commitSha: commitSha_, devTools, width, height, uploadIntervalMs, incognito, trace, }) => {
|
|
13
|
-
const logger =
|
|
14
|
-
|
|
15
|
-
|
|
17
|
+
const logger = loglevel_1.default.getLogger(common_1.METICULOUS_LOGGER_NAME);
|
|
18
|
+
const debugLogger = trace ? await common_1.DebugLogger.create() : null;
|
|
19
|
+
debugLogger === null || debugLogger === void 0 ? void 0 : debugLogger.log("Record options:");
|
|
20
|
+
debugLogger === null || debugLogger === void 0 ? void 0 : debugLogger.logObject({
|
|
16
21
|
apiToken,
|
|
17
22
|
commitSha: commitSha_,
|
|
18
23
|
devTools,
|
|
@@ -22,27 +27,28 @@ const handler = async ({ apiToken, commitSha: commitSha_, devTools, width, heigh
|
|
|
22
27
|
incognito,
|
|
23
28
|
trace,
|
|
24
29
|
});
|
|
30
|
+
logger.info("Preparing recording...");
|
|
25
31
|
// 1. Fetch the recording token
|
|
26
32
|
const client = (0, client_1.createClient)({ apiToken });
|
|
27
33
|
const project = await (0, project_api_1.getProject)(client);
|
|
28
34
|
if (!project) {
|
|
29
|
-
|
|
30
|
-
|
|
35
|
+
logger.error("Could not retrieve project data. Is the API token correct?");
|
|
36
|
+
debugLogger === null || debugLogger === void 0 ? void 0 : debugLogger.log("Could not retrieve project data. Is the API token correct?");
|
|
31
37
|
process.exit(1);
|
|
32
38
|
}
|
|
33
39
|
const recordingToken = project.recordingToken;
|
|
34
40
|
if (!recordingToken) {
|
|
35
|
-
|
|
36
|
-
|
|
41
|
+
logger.error("Could not retrieve recording token.");
|
|
42
|
+
debugLogger === null || debugLogger === void 0 ? void 0 : debugLogger.log("Could not retrieve recording token.");
|
|
37
43
|
process.exit(1);
|
|
38
44
|
}
|
|
39
|
-
|
|
45
|
+
logger.debug(`Recording token: ${recordingToken}`);
|
|
40
46
|
// 2. Guess commit SHA1
|
|
41
47
|
const commitSha = (await (0, commit_sha_utils_1.getCommitSha)(commitSha_)) || "unknown";
|
|
42
|
-
|
|
48
|
+
logger.debug(`Commit: ${commitSha}`);
|
|
43
49
|
// 3. Load recording snippets
|
|
44
50
|
const recordingSnippet = await (0, replay_assets_1.fetchAsset)("https://snippet.meticulous.ai/v1/stagingMeticulousSnippet.js");
|
|
45
|
-
const
|
|
51
|
+
const earlyNetworkRecorderSnippet = await (0, replay_assets_1.fetchAsset)("https://snippet.meticulous.ai/record/v1/network-recorder.bundle.js");
|
|
46
52
|
// 4. Load recording package
|
|
47
53
|
let recordSession;
|
|
48
54
|
try {
|
|
@@ -50,10 +56,10 @@ const handler = async ({ apiToken, commitSha: commitSha_, devTools, width, heigh
|
|
|
50
56
|
recordSession = record.recordSession;
|
|
51
57
|
}
|
|
52
58
|
catch (error) {
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
59
|
+
logger.error("Error: could not import @alwaysmeticulous/record");
|
|
60
|
+
logger.error(error);
|
|
61
|
+
debugLogger === null || debugLogger === void 0 ? void 0 : debugLogger.log("Error: could not import @alwaysmeticulous/record");
|
|
62
|
+
debugLogger === null || debugLogger === void 0 ? void 0 : debugLogger.log(`${error}`);
|
|
57
63
|
process.exit(1);
|
|
58
64
|
}
|
|
59
65
|
const cookieDir = (0, path_1.join)((0, common_1.getMeticulousLocalDataDir)(), "cookies");
|
|
@@ -67,23 +73,23 @@ const handler = async ({ apiToken, commitSha: commitSha_, devTools, width, heigh
|
|
|
67
73
|
appCommitHash: commitSha,
|
|
68
74
|
devTools,
|
|
69
75
|
recordingSnippet,
|
|
70
|
-
|
|
76
|
+
earlyNetworkRecorderSnippet,
|
|
71
77
|
width,
|
|
72
78
|
height,
|
|
73
79
|
uploadIntervalMs,
|
|
74
80
|
incognito,
|
|
75
81
|
cookieDir,
|
|
76
|
-
|
|
82
|
+
debugLogger,
|
|
77
83
|
onDetectedSession: (sessionId) => {
|
|
78
84
|
(0, session_api_1.postSessionIdNotification)(client, sessionId, recordingCommandId).catch((error) => {
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
85
|
+
logger.error(`Warning: error while notifying session recording ${sessionId}`);
|
|
86
|
+
logger.error(error);
|
|
87
|
+
debugLogger === null || debugLogger === void 0 ? void 0 : debugLogger.log(`Warning: error while notifying session recording ${sessionId}`);
|
|
88
|
+
debugLogger === null || debugLogger === void 0 ? void 0 : debugLogger.log(`${error}`);
|
|
83
89
|
});
|
|
84
90
|
},
|
|
85
91
|
}).catch((error) => {
|
|
86
|
-
|
|
92
|
+
debugLogger === null || debugLogger === void 0 ? void 0 : debugLogger.log(`${error}`);
|
|
87
93
|
throw error;
|
|
88
94
|
});
|
|
89
95
|
};
|
|
@@ -1,8 +1,12 @@
|
|
|
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
6
|
exports.replay = exports.replayCommandHandler = void 0;
|
|
4
7
|
const common_1 = require("@alwaysmeticulous/common");
|
|
5
8
|
const promises_1 = require("fs/promises");
|
|
9
|
+
const loglevel_1 = __importDefault(require("loglevel"));
|
|
6
10
|
const luxon_1 = require("luxon");
|
|
7
11
|
const path_1 = require("path");
|
|
8
12
|
const client_1 = require("../../api/client");
|
|
@@ -19,13 +23,14 @@ const sentry_utils_1 = require("../../utils/sentry.utils");
|
|
|
19
23
|
const version_utils_1 = require("../../utils/version.utils");
|
|
20
24
|
const screenshot_diff_command_1 = require("../screenshot-diff/screenshot-diff.command");
|
|
21
25
|
const replayCommandHandler = async ({ apiToken, commitSha: commitSha_, sessionId, appUrl, headless, devTools, bypassCSP, screenshot, screenshotSelector, baseReplayId: baseReplayId_, diffThreshold, diffPixelThreshold, save, exitOnMismatch, padTime, networkStubbing, moveBeforeClick, cookies, cookiesFile, }) => {
|
|
26
|
+
const logger = loglevel_1.default.getLogger(common_1.METICULOUS_LOGGER_NAME);
|
|
22
27
|
const client = (0, client_1.createClient)({ apiToken });
|
|
23
28
|
// 1. Check session files
|
|
24
29
|
const session = await (0, sessions_1.getOrFetchRecordedSession)(client, sessionId);
|
|
25
30
|
const sessionData = await (0, sessions_1.getOrFetchRecordedSessionData)(client, sessionId);
|
|
26
31
|
// 2. Guess commit SHA1
|
|
27
32
|
const commitSha = (await (0, commit_sha_utils_1.getCommitSha)(commitSha_)) || "unknown";
|
|
28
|
-
|
|
33
|
+
logger.debug(`Commit: ${commitSha}`);
|
|
29
34
|
const meticulousSha = await (0, version_utils_1.getMeticulousVersion)();
|
|
30
35
|
// 3. Load replay assets
|
|
31
36
|
const reanimator = await (0, replay_assets_1.fetchAsset)("https://snippet.meticulous.ai/replay/v1/reanimator.bundle.js");
|
|
@@ -39,8 +44,8 @@ const replayCommandHandler = async ({ apiToken, commitSha: commitSha_, sessionId
|
|
|
39
44
|
replayEvents = replayer.replayEvents;
|
|
40
45
|
}
|
|
41
46
|
catch (error) {
|
|
42
|
-
|
|
43
|
-
|
|
47
|
+
logger.error("Error: could not import @alwaysmeticulous/replayer");
|
|
48
|
+
logger.error(error);
|
|
44
49
|
process.exit(1);
|
|
45
50
|
}
|
|
46
51
|
// Report replay start
|
|
@@ -91,14 +96,14 @@ const replayCommandHandler = async ({ apiToken, commitSha: commitSha_, sessionId
|
|
|
91
96
|
};
|
|
92
97
|
await (0, promises_1.writeFile)((0, path_1.join)(tempDir, "replayEventsParams.json"), JSON.stringify(replayEventsParams));
|
|
93
98
|
// 7. Perform replay
|
|
94
|
-
|
|
99
|
+
logger.info("Starting replay...");
|
|
95
100
|
const startTime = luxon_1.DateTime.utc();
|
|
96
101
|
const { eventsFinishedPromise, writesFinishedPromise } = await replayEvents(replayEventsParams);
|
|
97
102
|
await eventsFinishedPromise;
|
|
98
103
|
await writesFinishedPromise;
|
|
99
104
|
const endTime = luxon_1.DateTime.utc();
|
|
100
|
-
|
|
101
|
-
|
|
105
|
+
logger.info(`Replay time: ${endTime.diff(startTime).as("seconds")} seconds`);
|
|
106
|
+
logger.info("Sending replay results to Meticulous");
|
|
102
107
|
// 8. Create a Zip archive containing the replay files
|
|
103
108
|
const archivePath = await (0, archive_1.createReplayArchive)(tempDir);
|
|
104
109
|
// 9. Get upload URL
|
|
@@ -111,7 +116,7 @@ const replayCommandHandler = async ({ apiToken, commitSha: commitSha_, sessionId
|
|
|
111
116
|
});
|
|
112
117
|
const uploadUrlData = await (0, replay_api_1.getReplayPushUrl)(client, replay.id);
|
|
113
118
|
if (!uploadUrlData) {
|
|
114
|
-
|
|
119
|
+
logger.error("Error: Could not get a push URL from the Meticulous API");
|
|
115
120
|
process.exit(1);
|
|
116
121
|
}
|
|
117
122
|
const uploadUrl = uploadUrlData.pushUrl;
|
|
@@ -120,20 +125,22 @@ const replayCommandHandler = async ({ apiToken, commitSha: commitSha_, sessionId
|
|
|
120
125
|
await (0, upload_1.uploadArchive)(uploadUrl, archivePath);
|
|
121
126
|
}
|
|
122
127
|
catch (error) {
|
|
123
|
-
await (0, replay_api_1.putReplayPushedStatus)(client, replay.id, "failure", replayCommandId).catch(
|
|
124
|
-
|
|
128
|
+
await (0, replay_api_1.putReplayPushedStatus)(client, replay.id, "failure", replayCommandId).catch(logger.error);
|
|
129
|
+
logger.error(error);
|
|
125
130
|
process.exit(1);
|
|
126
131
|
}
|
|
127
132
|
// 11. Report successful upload to Meticulous
|
|
128
133
|
const updatedProjectBuild = await (0, replay_api_1.putReplayPushedStatus)(client, replay.id, "success", replayCommandId);
|
|
129
|
-
|
|
130
|
-
|
|
134
|
+
logger.info("Replay artifacts successfully sent to Meticulous");
|
|
135
|
+
logger.debug(updatedProjectBuild);
|
|
131
136
|
const replayUrl = (0, replay_api_1.getReplayUrl)(replay);
|
|
132
|
-
|
|
137
|
+
logger.info("=======");
|
|
138
|
+
logger.info(`View replay at: ${replayUrl}`);
|
|
139
|
+
logger.info("=======");
|
|
133
140
|
// 12. Diff against base replay screenshot if one is provided
|
|
134
141
|
const baseReplayId = baseReplayId_ || "";
|
|
135
142
|
if (screenshot && baseReplayId) {
|
|
136
|
-
|
|
143
|
+
logger.info(`Diffing final state screenshot against replay ${baseReplayId}`);
|
|
137
144
|
await (0, replays_1.getOrFetchReplay)(client, baseReplayId);
|
|
138
145
|
await (0, replays_1.getOrFetchReplayArchive)(client, baseReplayId);
|
|
139
146
|
const baseScreenshot = await (0, replays_1.readReplayScreenshot)(baseReplayId);
|
|
@@ -152,7 +159,7 @@ const replayCommandHandler = async ({ apiToken, commitSha: commitSha_, sessionId
|
|
|
152
159
|
// 13. Add test case to meticulous.json if --save option is passed
|
|
153
160
|
if (save) {
|
|
154
161
|
if (!screenshot) {
|
|
155
|
-
|
|
162
|
+
logger.error("Warning: saving a new test case without screenshot enabled.");
|
|
156
163
|
}
|
|
157
164
|
const meticulousConfig = await (0, config_1.readConfig)();
|
|
158
165
|
const newConfig = {
|
|
@@ -11,6 +11,8 @@ interface Options {
|
|
|
11
11
|
padTime: boolean;
|
|
12
12
|
networkStubbing: boolean;
|
|
13
13
|
githubSummary?: boolean | null | undefined;
|
|
14
|
+
parallelize?: boolean | null | undefined;
|
|
15
|
+
parallelTasks?: number | null | undefined;
|
|
14
16
|
}
|
|
15
17
|
export declare const runAllTests: CommandModule<unknown, Options>;
|
|
16
18
|
export {};
|
|
@@ -1,21 +1,28 @@
|
|
|
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
6
|
exports.runAllTests = void 0;
|
|
7
|
+
const common_1 = require("@alwaysmeticulous/common");
|
|
8
|
+
const loglevel_1 = __importDefault(require("loglevel"));
|
|
4
9
|
const client_1 = require("../../api/client");
|
|
5
10
|
const test_run_api_1 = require("../../api/test-run.api");
|
|
6
11
|
const config_1 = require("../../config/config");
|
|
12
|
+
const parallel_tests_handler_1 = require("../../parallel-tests/parallel-tests.handler");
|
|
7
13
|
const commit_sha_utils_1 = require("../../utils/commit-sha.utils");
|
|
8
14
|
const github_summary_utils_1 = require("../../utils/github-summary.utils");
|
|
9
15
|
const sentry_utils_1 = require("../../utils/sentry.utils");
|
|
10
16
|
const version_utils_1 = require("../../utils/version.utils");
|
|
11
17
|
const replay_command_1 = require("../replay/replay.command");
|
|
12
18
|
const screenshot_diff_command_1 = require("../screenshot-diff/screenshot-diff.command");
|
|
13
|
-
const handler = async ({ apiToken, commitSha: commitSha_, appUrl, headless, devTools, bypassCSP, diffThreshold, diffPixelThreshold, padTime, networkStubbing, githubSummary, }) => {
|
|
19
|
+
const handler = async ({ apiToken, commitSha: commitSha_, appUrl, headless, devTools, bypassCSP, diffThreshold, diffPixelThreshold, padTime, networkStubbing, githubSummary, parallelize, parallelTasks, }) => {
|
|
20
|
+
const logger = loglevel_1.default.getLogger(common_1.METICULOUS_LOGGER_NAME);
|
|
14
21
|
const client = (0, client_1.createClient)({ apiToken });
|
|
15
22
|
const config = await (0, config_1.readConfig)();
|
|
16
23
|
const testCases = config.testCases || [];
|
|
17
24
|
if (!testCases.length) {
|
|
18
|
-
|
|
25
|
+
logger.error("Error! No test case defined");
|
|
19
26
|
process.exit(1);
|
|
20
27
|
}
|
|
21
28
|
const commitSha = (await (0, commit_sha_utils_1.getCommitSha)(commitSha_)) || "unknown";
|
|
@@ -27,54 +34,78 @@ const handler = async ({ apiToken, commitSha: commitSha_, appUrl, headless, devT
|
|
|
27
34
|
configData: config,
|
|
28
35
|
});
|
|
29
36
|
const testRunUrl = (0, test_run_api_1.getTestRunUrl)(testRun);
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
const
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
const
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
37
|
+
logger.info("");
|
|
38
|
+
logger.info(`Test run URL: ${testRunUrl}`);
|
|
39
|
+
logger.info("");
|
|
40
|
+
const getResults = async () => {
|
|
41
|
+
if (parallelize) {
|
|
42
|
+
const results = await (0, parallel_tests_handler_1.runAllTestsInParallel)({
|
|
43
|
+
config,
|
|
44
|
+
client,
|
|
45
|
+
testRun,
|
|
46
|
+
apiToken,
|
|
47
|
+
commitSha,
|
|
48
|
+
appUrl,
|
|
49
|
+
headless,
|
|
50
|
+
devTools,
|
|
51
|
+
bypassCSP,
|
|
52
|
+
diffThreshold,
|
|
53
|
+
diffPixelThreshold,
|
|
54
|
+
padTime,
|
|
55
|
+
networkStubbing,
|
|
56
|
+
parallelTasks,
|
|
57
|
+
});
|
|
58
|
+
return results;
|
|
59
|
+
}
|
|
60
|
+
const results = [];
|
|
61
|
+
for (const testCase of testCases) {
|
|
62
|
+
const { sessionId, baseReplayId, options } = testCase;
|
|
63
|
+
const replayPromise = (0, replay_command_1.replayCommandHandler)({
|
|
64
|
+
apiToken,
|
|
65
|
+
commitSha,
|
|
66
|
+
sessionId,
|
|
67
|
+
appUrl,
|
|
68
|
+
headless,
|
|
69
|
+
devTools,
|
|
70
|
+
bypassCSP,
|
|
71
|
+
screenshot: true,
|
|
72
|
+
baseReplayId,
|
|
73
|
+
diffThreshold,
|
|
74
|
+
diffPixelThreshold,
|
|
75
|
+
save: false,
|
|
76
|
+
exitOnMismatch: false,
|
|
77
|
+
padTime,
|
|
78
|
+
networkStubbing,
|
|
79
|
+
...options,
|
|
80
|
+
});
|
|
81
|
+
const result = await replayPromise
|
|
82
|
+
.then((replay) => ({
|
|
83
|
+
...testCase,
|
|
84
|
+
headReplayId: replay.id,
|
|
85
|
+
result: "pass",
|
|
86
|
+
}))
|
|
87
|
+
.catch((error) => {
|
|
88
|
+
if (error instanceof screenshot_diff_command_1.DiffError && error.extras) {
|
|
89
|
+
return {
|
|
90
|
+
...testCase,
|
|
91
|
+
headReplayId: error.extras.headReplayId,
|
|
92
|
+
result: "fail",
|
|
93
|
+
};
|
|
94
|
+
}
|
|
95
|
+
logger.error(error);
|
|
96
|
+
return { ...testCase, headReplayId: "", result: "fail" };
|
|
97
|
+
});
|
|
98
|
+
results.push(result);
|
|
99
|
+
await (0, test_run_api_1.putTestRunResults)({
|
|
100
|
+
client,
|
|
101
|
+
testRunId: testRun.id,
|
|
102
|
+
status: "Running",
|
|
103
|
+
resultData: { results },
|
|
104
|
+
});
|
|
105
|
+
}
|
|
106
|
+
return results;
|
|
107
|
+
};
|
|
108
|
+
const results = await getResults();
|
|
78
109
|
const runAllFailure = results.find(({ result }) => result === "fail");
|
|
79
110
|
await (0, test_run_api_1.putTestRunResults)({
|
|
80
111
|
client,
|
|
@@ -82,13 +113,13 @@ const handler = async ({ apiToken, commitSha: commitSha_, appUrl, headless, devT
|
|
|
82
113
|
status: runAllFailure ? "Failure" : "Success",
|
|
83
114
|
resultData: { results },
|
|
84
115
|
});
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
116
|
+
logger.info("");
|
|
117
|
+
logger.info("Results");
|
|
118
|
+
logger.info("=======");
|
|
119
|
+
logger.info(`URL: ${testRunUrl}`);
|
|
120
|
+
logger.info("=======");
|
|
90
121
|
results.forEach(({ title, result }) => {
|
|
91
|
-
|
|
122
|
+
logger.info(`${title} => ${result}`);
|
|
92
123
|
});
|
|
93
124
|
if (githubSummary) {
|
|
94
125
|
await (0, github_summary_utils_1.writeGitHubSummary)({ testRun, results });
|
|
@@ -142,6 +173,20 @@ exports.runAllTests = {
|
|
|
142
173
|
description: "Stub network requests during replay",
|
|
143
174
|
default: true,
|
|
144
175
|
},
|
|
176
|
+
parallelize: {
|
|
177
|
+
boolean: true,
|
|
178
|
+
description: "Run tests in parallel",
|
|
179
|
+
},
|
|
180
|
+
parallelTasks: {
|
|
181
|
+
number: true,
|
|
182
|
+
description: "Number of tasks to run in parallel",
|
|
183
|
+
coerce: (value) => {
|
|
184
|
+
if (typeof value === "number" && value <= 0) {
|
|
185
|
+
return null;
|
|
186
|
+
}
|
|
187
|
+
return value;
|
|
188
|
+
},
|
|
189
|
+
},
|
|
145
190
|
},
|
|
146
191
|
handler: (0, sentry_utils_1.wrapHandler)(handler),
|
|
147
192
|
};
|
|
@@ -1,6 +1,11 @@
|
|
|
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
6
|
exports.screenshotDiff = exports.diffScreenshots = exports.DiffError = void 0;
|
|
7
|
+
const common_1 = require("@alwaysmeticulous/common");
|
|
8
|
+
const loglevel_1 = __importDefault(require("loglevel"));
|
|
4
9
|
const client_1 = require("../../api/client");
|
|
5
10
|
const replay_api_1 = require("../../api/replay.api");
|
|
6
11
|
const diff_utils_1 = require("../../image/diff.utils");
|
|
@@ -16,6 +21,7 @@ class DiffError extends Error {
|
|
|
16
21
|
}
|
|
17
22
|
exports.DiffError = DiffError;
|
|
18
23
|
const diffScreenshots = async ({ client, baseReplayId, headReplayId, baseScreenshot, headScreenshot, threshold: threshold_, pixelThreshold, exitOnMismatch, }) => {
|
|
24
|
+
const logger = loglevel_1.default.getLogger(common_1.METICULOUS_LOGGER_NAME);
|
|
19
25
|
const threshold = threshold_ || DEFAULT_MISMATCH_THRESHOLD;
|
|
20
26
|
const pixelmatchOptions = pixelThreshold ? { threshold: pixelThreshold } : null;
|
|
21
27
|
try {
|
|
@@ -24,11 +30,11 @@ const diffScreenshots = async ({ client, baseReplayId, headReplayId, baseScreens
|
|
|
24
30
|
head: headScreenshot,
|
|
25
31
|
...(pixelmatchOptions ? pixelmatchOptions : {}),
|
|
26
32
|
});
|
|
27
|
-
|
|
28
|
-
|
|
33
|
+
logger.debug({ mismatchPixels, mismatchFraction });
|
|
34
|
+
logger.info(`${Math.round(mismatchFraction * 100)}% pixel mismatch (threshold is ${Math.round(threshold * 100)}%) => ${mismatchFraction > threshold ? "FAIL!" : "PASS"}`);
|
|
29
35
|
await (0, screenshot_diffs_1.writeScreenshotDiff)({ baseReplayId, headReplayId, diff });
|
|
30
36
|
const diffUrl = await (0, replay_api_1.getDiffUrl)(client, baseReplayId, headReplayId);
|
|
31
|
-
|
|
37
|
+
logger.info(`View screenshot diff at ${diffUrl}`);
|
|
32
38
|
await (0, replay_api_1.postScreenshotDiffStats)(client, {
|
|
33
39
|
baseReplayId,
|
|
34
40
|
headReplayId,
|
|
@@ -39,7 +45,7 @@ const diffScreenshots = async ({ client, baseReplayId, headReplayId, baseScreens
|
|
|
39
45
|
},
|
|
40
46
|
});
|
|
41
47
|
if (mismatchFraction > threshold) {
|
|
42
|
-
|
|
48
|
+
logger.info("Screenshots do not match!");
|
|
43
49
|
if (exitOnMismatch) {
|
|
44
50
|
process.exit(1);
|
|
45
51
|
}
|
|
@@ -52,7 +58,9 @@ const diffScreenshots = async ({ client, baseReplayId, headReplayId, baseScreens
|
|
|
52
58
|
}
|
|
53
59
|
}
|
|
54
60
|
catch (error) {
|
|
55
|
-
|
|
61
|
+
if (!(error instanceof DiffError)) {
|
|
62
|
+
logger.error(error);
|
|
63
|
+
}
|
|
56
64
|
if (exitOnMismatch) {
|
|
57
65
|
process.exit(1);
|
|
58
66
|
}
|
|
@@ -1,17 +1,23 @@
|
|
|
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
6
|
exports.showProject = void 0;
|
|
7
|
+
const common_1 = require("@alwaysmeticulous/common");
|
|
8
|
+
const loglevel_1 = __importDefault(require("loglevel"));
|
|
4
9
|
const client_1 = require("../../api/client");
|
|
5
10
|
const project_api_1 = require("../../api/project.api");
|
|
6
11
|
const sentry_utils_1 = require("../../utils/sentry.utils");
|
|
7
12
|
const handler = async ({ apiToken }) => {
|
|
13
|
+
const logger = loglevel_1.default.getLogger(common_1.METICULOUS_LOGGER_NAME);
|
|
8
14
|
const client = (0, client_1.createClient)({ apiToken });
|
|
9
15
|
const project = await (0, project_api_1.getProject)(client);
|
|
10
16
|
if (!project) {
|
|
11
|
-
|
|
17
|
+
logger.error("Could not retrieve project data. Is the API token correct?");
|
|
12
18
|
process.exit(1);
|
|
13
19
|
}
|
|
14
|
-
|
|
20
|
+
logger.info(project);
|
|
15
21
|
};
|
|
16
22
|
exports.showProject = {
|
|
17
23
|
command: "show-project",
|
|
@@ -1,4 +1,7 @@
|
|
|
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
6
|
exports.uploadBuild = void 0;
|
|
4
7
|
const client_1 = require("../../api/client");
|
|
@@ -6,36 +9,39 @@ const project_build_api_1 = require("../../api/project-build.api");
|
|
|
6
9
|
const project_api_1 = require("../../api/project.api");
|
|
7
10
|
const upload_1 = require("../../api/upload");
|
|
8
11
|
const archive_1 = require("../../archive/archive");
|
|
12
|
+
const loglevel_1 = __importDefault(require("loglevel"));
|
|
9
13
|
const commit_sha_utils_1 = require("../../utils/commit-sha.utils");
|
|
10
14
|
const sentry_utils_1 = require("../../utils/sentry.utils");
|
|
15
|
+
const common_1 = require("@alwaysmeticulous/common");
|
|
11
16
|
const handler = async ({ apiToken, commitSha: commitSha_, dist, }) => {
|
|
17
|
+
const logger = loglevel_1.default.getLogger(common_1.METICULOUS_LOGGER_NAME);
|
|
12
18
|
// 1. Print project name
|
|
13
19
|
const client = (0, client_1.createClient)({ apiToken });
|
|
14
20
|
const project = await (0, project_api_1.getProject)(client);
|
|
15
21
|
if (!project) {
|
|
16
|
-
|
|
22
|
+
logger.error("Error: Could not retrieve project data. Is the API token correct?");
|
|
17
23
|
process.exit(1);
|
|
18
24
|
}
|
|
19
25
|
const projectName = `${project.organization.name}/${project.name}`;
|
|
20
|
-
|
|
26
|
+
logger.info(`Project: ${projectName}`);
|
|
21
27
|
// 2. Guess commit SHA1
|
|
22
28
|
const commitSha = await (0, commit_sha_utils_1.getCommitSha)(commitSha_);
|
|
23
29
|
if (!commitSha) {
|
|
24
|
-
|
|
30
|
+
logger.error("Error: Could not guess commit SHA1, aborting");
|
|
25
31
|
process.exit(1);
|
|
26
32
|
}
|
|
27
|
-
|
|
33
|
+
logger.info(`Commit: ${commitSha}`);
|
|
28
34
|
// 3. Create zip archive of build artifacts
|
|
29
|
-
|
|
35
|
+
logger.info(`Uploading build artifacts from: ${dist}`);
|
|
30
36
|
try {
|
|
31
37
|
await (0, archive_1.checkDistFolder)(dist);
|
|
32
38
|
}
|
|
33
39
|
catch (error) {
|
|
34
40
|
if (error instanceof Error) {
|
|
35
|
-
|
|
41
|
+
logger.error(`Error: ${error.message}`);
|
|
36
42
|
}
|
|
37
43
|
else {
|
|
38
|
-
|
|
44
|
+
logger.error(`Error: ${error}`);
|
|
39
45
|
}
|
|
40
46
|
process.exit(1);
|
|
41
47
|
}
|
|
@@ -44,7 +50,7 @@ const handler = async ({ apiToken, commitSha: commitSha_, dist, }) => {
|
|
|
44
50
|
const projectBuild = await (0, project_build_api_1.createProjectBuild)(client, commitSha);
|
|
45
51
|
const uploadUrlData = await (0, project_build_api_1.getProjectBuildPushUrl)(client, projectBuild.id);
|
|
46
52
|
if (!uploadUrlData) {
|
|
47
|
-
|
|
53
|
+
logger.error("Error: Could not get a push URL from the Meticulous API");
|
|
48
54
|
process.exit(1);
|
|
49
55
|
}
|
|
50
56
|
const uploadUrl = uploadUrlData.pushUrl;
|
|
@@ -53,14 +59,14 @@ const handler = async ({ apiToken, commitSha: commitSha_, dist, }) => {
|
|
|
53
59
|
await (0, upload_1.uploadArchive)(uploadUrl, archivePath);
|
|
54
60
|
}
|
|
55
61
|
catch (error) {
|
|
56
|
-
await (0, project_build_api_1.putProjectBuildPushedStatus)(client, projectBuild.id, "failure").catch((updateError) =>
|
|
57
|
-
|
|
62
|
+
await (0, project_build_api_1.putProjectBuildPushedStatus)(client, projectBuild.id, "failure").catch((updateError) => logger.error(updateError));
|
|
63
|
+
logger.error(error);
|
|
58
64
|
process.exit(1);
|
|
59
65
|
}
|
|
60
66
|
// 6. Report successful upload to Meticulous
|
|
61
67
|
const updatedProjectBuild = await (0, project_build_api_1.putProjectBuildPushedStatus)(client, projectBuild.id, "success");
|
|
62
|
-
|
|
63
|
-
|
|
68
|
+
logger.info("Build artifacts successfully sent to Meticulous");
|
|
69
|
+
logger.debug(updatedProjectBuild);
|
|
64
70
|
await (0, archive_1.deleteArchive)(archivePath);
|
|
65
71
|
};
|
|
66
72
|
exports.uploadBuild = {
|
|
@@ -7,6 +7,7 @@ exports.fetchAsset = exports.saveAssetMetadata = exports.loadAssetMetadata = voi
|
|
|
7
7
|
const common_1 = require("@alwaysmeticulous/common");
|
|
8
8
|
const axios_1 = __importDefault(require("axios"));
|
|
9
9
|
const promises_1 = require("fs/promises");
|
|
10
|
+
const loglevel_1 = __importDefault(require("loglevel"));
|
|
10
11
|
const path_1 = require("path");
|
|
11
12
|
const loadAssetMetadata = async () => {
|
|
12
13
|
const assetsDir = (0, path_1.join)((0, common_1.getMeticulousLocalDataDir)(), "assets");
|
|
@@ -29,6 +30,7 @@ const saveAssetMetadata = async (assetMetadata) => {
|
|
|
29
30
|
};
|
|
30
31
|
exports.saveAssetMetadata = saveAssetMetadata;
|
|
31
32
|
const fetchAsset = async (fetchUrl) => {
|
|
33
|
+
const logger = loglevel_1.default.getLogger(common_1.METICULOUS_LOGGER_NAME);
|
|
32
34
|
const assetsDir = (0, path_1.join)((0, common_1.getMeticulousLocalDataDir)(), "assets");
|
|
33
35
|
const assetMetadata = await (0, exports.loadAssetMetadata)();
|
|
34
36
|
const etag = (await axios_1.default.head(fetchUrl)).headers["etag"] || "";
|
|
@@ -38,17 +40,17 @@ const fetchAsset = async (fetchUrl) => {
|
|
|
38
40
|
: (0, path_1.basename)(new URL(fetchUrl).pathname);
|
|
39
41
|
const filePath = (0, path_1.join)(assetsDir, fileName);
|
|
40
42
|
if (entry && etag === entry.etag) {
|
|
41
|
-
|
|
43
|
+
logger.debug(`${fetchUrl} already present`);
|
|
42
44
|
return filePath;
|
|
43
45
|
}
|
|
44
46
|
const contents = (await axios_1.default.get(fetchUrl)).data;
|
|
45
47
|
await (0, promises_1.writeFile)(filePath, contents);
|
|
46
48
|
if (entry) {
|
|
47
|
-
|
|
49
|
+
logger.debug(`${fetchUrl} updated`);
|
|
48
50
|
entry.etag = etag;
|
|
49
51
|
}
|
|
50
52
|
else {
|
|
51
|
-
|
|
53
|
+
logger.debug(`${fetchUrl} downloaded`);
|
|
52
54
|
assetMetadata.assets.push({ fileName, etag, fetchUrl });
|
|
53
55
|
}
|
|
54
56
|
await (0, exports.saveAssetMetadata)(assetMetadata);
|