@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.
@@ -7,11 +7,13 @@ exports.readLocalReplayScreenshot = exports.readReplayScreenshot = exports.getOr
7
7
  const common_1 = require("@alwaysmeticulous/common");
8
8
  const adm_zip_1 = __importDefault(require("adm-zip"));
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 download_1 = require("../api/download");
12
13
  const replay_api_1 = require("../api/replay.api");
13
14
  const io_utils_1 = require("../image/io.utils");
14
15
  const getOrFetchReplay = async (client, replayId) => {
16
+ const logger = loglevel_1.default.getLogger(common_1.METICULOUS_LOGGER_NAME);
15
17
  const replayDir = (0, path_1.join)((0, common_1.getMeticulousLocalDataDir)(), "replays", replayId);
16
18
  await (0, promises_1.mkdir)(replayDir, { recursive: true });
17
19
  const replayFile = (0, path_1.join)(replayDir, `${replayId}.json`);
@@ -19,20 +21,21 @@ const getOrFetchReplay = async (client, replayId) => {
19
21
  .then((data) => JSON.parse(data.toString("utf-8")))
20
22
  .catch(() => null);
21
23
  if (existingReplay) {
22
- console.log(`Reading replay from local copy in ${replayFile}`);
24
+ logger.debug(`Reading replay from local copy in ${replayFile}`);
23
25
  return existingReplay;
24
26
  }
25
27
  const replay = await (0, replay_api_1.getReplay)(client, replayId);
26
28
  if (!replay) {
27
- console.error("Error: Could not retrieve replay. Is the API token correct?");
29
+ logger.error("Error: Could not retrieve replay. Is the API token correct?");
28
30
  process.exit(1);
29
31
  }
30
32
  await (0, promises_1.writeFile)(replayFile, JSON.stringify(replay, null, 2));
31
- console.log(`Wrote replay to ${replayFile}`);
33
+ logger.debug(`Wrote replay to ${replayFile}`);
32
34
  return replay;
33
35
  };
34
36
  exports.getOrFetchReplay = getOrFetchReplay;
35
37
  const getOrFetchReplayArchive = async (client, replayId) => {
38
+ const logger = loglevel_1.default.getLogger(common_1.METICULOUS_LOGGER_NAME);
36
39
  const replayDir = (0, path_1.join)((0, common_1.getMeticulousLocalDataDir)(), "replays", replayId);
37
40
  await (0, promises_1.mkdir)(replayDir, { recursive: true });
38
41
  const replayArchiveFile = (0, path_1.join)(replayDir, `${replayId}.zip`);
@@ -43,19 +46,19 @@ const getOrFetchReplayArchive = async (client, replayId) => {
43
46
  .then(() => true)
44
47
  .catch(() => false);
45
48
  if (paramsFileExists) {
46
- console.log(`Replay archive already downloaded at ${replayDir}`);
49
+ logger.debug(`Replay archive already downloaded at ${replayDir}`);
47
50
  return;
48
51
  }
49
52
  const dowloadUrlData = await (0, replay_api_1.getReplayDownloadUrl)(client, replayId);
50
53
  if (!dowloadUrlData) {
51
- console.error("Error: Could not retrieve replay archive URL. This may be an invalid replay");
54
+ logger.error("Error: Could not retrieve replay archive URL. This may be an invalid replay");
52
55
  process.exit(1);
53
56
  }
54
57
  await (0, download_1.downloadFile)(dowloadUrlData.dowloadUrl, replayArchiveFile);
55
58
  const zipFile = new adm_zip_1.default(replayArchiveFile);
56
59
  zipFile.extractAllTo(replayDir, /*overwrite=*/ true);
57
60
  await (0, promises_1.rm)(replayArchiveFile);
58
- console.log(`Exrtracted replay archive in ${replayDir}`);
61
+ logger.debug(`Exrtracted replay archive in ${replayDir}`);
59
62
  };
60
63
  exports.getOrFetchReplayArchive = getOrFetchReplayArchive;
61
64
  const readReplayScreenshot = async (replayId) => {
@@ -1,15 +1,20 @@
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.writeScreenshotDiff = void 0;
4
7
  const common_1 = require("@alwaysmeticulous/common");
5
8
  const promises_1 = require("fs/promises");
6
9
  const path_1 = require("path");
10
+ const loglevel_1 = __importDefault(require("loglevel"));
7
11
  const io_utils_1 = require("../image/io.utils");
8
12
  const writeScreenshotDiff = async ({ baseReplayId, headReplayId, diff }) => {
13
+ const logger = loglevel_1.default.getLogger(common_1.METICULOUS_LOGGER_NAME);
9
14
  const diffDir = (0, path_1.join)((0, common_1.getMeticulousLocalDataDir)(), "screenshot-diffs");
10
15
  await (0, promises_1.mkdir)(diffDir, { recursive: true });
11
16
  const diffFile = (0, path_1.join)(diffDir, `${baseReplayId}+${headReplayId}.png`);
12
17
  await (0, io_utils_1.writePng)(diff, diffFile);
13
- console.log(`Screenshot diff written to ${diffFile}`);
18
+ logger.debug(`Screenshot diff written to ${diffFile}`);
14
19
  };
15
20
  exports.writeScreenshotDiff = writeScreenshotDiff;
@@ -1,12 +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.getOrFetchRecordedSessionData = exports.getOrFetchRecordedSession = 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 path_1 = require("path");
7
11
  const session_api_1 = require("../api/session.api");
8
12
  const local_data_utils_1 = require("./local-data.utils");
9
13
  const getOrFetchRecordedSession = async (client, sessionId) => {
14
+ const logger = loglevel_1.default.getLogger(common_1.METICULOUS_LOGGER_NAME);
10
15
  const sessionsDir = (0, path_1.join)((0, common_1.getMeticulousLocalDataDir)(), "sessions");
11
16
  await (0, promises_1.mkdir)(sessionsDir, { recursive: true });
12
17
  const sessionFile = (0, path_1.join)(sessionsDir, `${(0, local_data_utils_1.sanitizeFilename)(sessionId)}.json`);
@@ -14,20 +19,21 @@ const getOrFetchRecordedSession = async (client, sessionId) => {
14
19
  .then((data) => JSON.parse(data.toString("utf-8")))
15
20
  .catch(() => null);
16
21
  if (existingSession) {
17
- console.log(`Reading session from local copy in ${sessionFile}`);
22
+ logger.debug(`Reading session from local copy in ${sessionFile}`);
18
23
  return existingSession;
19
24
  }
20
25
  const session = await (0, session_api_1.getRecordedSession)(client, sessionId);
21
26
  if (!session) {
22
- console.error("Error: Could not retrieve session. Is the API token correct?");
27
+ logger.error("Error: Could not retrieve session. Is the API token correct?");
23
28
  process.exit(1);
24
29
  }
25
30
  await (0, promises_1.writeFile)(sessionFile, JSON.stringify(session, null, 2));
26
- console.log(`Wrote session to ${sessionFile}`);
31
+ logger.debug(`Wrote session to ${sessionFile}`);
27
32
  return session;
28
33
  };
29
34
  exports.getOrFetchRecordedSession = getOrFetchRecordedSession;
30
35
  const getOrFetchRecordedSessionData = async (client, sessionId) => {
36
+ const logger = loglevel_1.default.getLogger(common_1.METICULOUS_LOGGER_NAME);
31
37
  const sessionsDir = (0, path_1.join)((0, common_1.getMeticulousLocalDataDir)(), "sessions");
32
38
  await (0, promises_1.mkdir)(sessionsDir, { recursive: true });
33
39
  const sessionDataFile = (0, path_1.join)(sessionsDir, `${(0, local_data_utils_1.sanitizeFilename)(sessionId)}_data.json`);
@@ -35,16 +41,16 @@ const getOrFetchRecordedSessionData = async (client, sessionId) => {
35
41
  .then((data) => JSON.parse(data.toString("utf-8")))
36
42
  .catch(() => null);
37
43
  if (existingSessionData) {
38
- console.log(`Reading session data from local copy in ${sessionDataFile}`);
44
+ logger.debug(`Reading session data from local copy in ${sessionDataFile}`);
39
45
  return existingSessionData;
40
46
  }
41
47
  const sessionData = await (0, session_api_1.getRecordedSessionData)(client, sessionId);
42
48
  if (!sessionData) {
43
- console.error("Error: Could not retrieve session data. This may be an invalid session");
49
+ logger.error("Error: Could not retrieve session data. This may be an invalid session");
44
50
  process.exit(1);
45
51
  }
46
52
  await (0, promises_1.writeFile)(sessionDataFile, JSON.stringify(sessionData, null, 2));
47
- console.log(`Wrote session data to ${sessionDataFile}`);
53
+ logger.debug(`Wrote session data to ${sessionDataFile}`);
48
54
  return sessionData;
49
55
  };
50
56
  exports.getOrFetchRecordedSessionData = getOrFetchRecordedSessionData;
package/dist/main.js CHANGED
@@ -15,11 +15,13 @@ const run_all_tests_command_1 = require("./commands/run-all-tests/run-all-tests.
15
15
  const screenshot_diff_command_1 = require("./commands/screenshot-diff/screenshot-diff.command");
16
16
  const show_project_command_1 = require("./commands/show-project/show-project.command");
17
17
  const upload_build_command_1 = require("./commands/upload-build/upload-build.command");
18
+ const logger_utils_1 = require("./utils/logger.utils");
18
19
  const sentry_utils_1 = require("./utils/sentry.utils");
19
20
  const handleDataDir = (dataDir) => {
20
21
  (0, common_1.getMeticulousLocalDataDir)(dataDir);
21
22
  };
22
23
  const main = () => {
24
+ (0, logger_utils_1.initLogger)();
23
25
  (0, sentry_utils_1.initSentry)();
24
26
  const promise = yargs_1.default
25
27
  .scriptName("meticulous")
@@ -39,12 +41,17 @@ const main = () => {
39
41
  .strict()
40
42
  .demandCommand()
41
43
  .option({
44
+ logLevel: {
45
+ choices: ["trace", "debug", "info", "warn", "error", "silent"],
46
+ description: "Log level",
47
+ },
42
48
  dataDir: {
43
49
  string: true,
44
50
  description: "Where Meticulous stores data (sessions, replays, etc.)",
45
51
  },
46
52
  })
47
53
  .middleware([
54
+ (argv) => (0, logger_utils_1.setLogLevel)(argv.logLevel),
48
55
  (argv) => handleDataDir(argv.dataDir),
49
56
  (argv) => (0, sentry_utils_1.setOptions)(argv),
50
57
  ]).argv;
@@ -0,0 +1,28 @@
1
+ import log from "loglevel";
2
+ import { TestCase, TestCaseResult } from "../config/config.types";
3
+ export interface InitMessage {
4
+ kind: "init";
5
+ data: {
6
+ logLevel: log.LogLevel[keyof log.LogLevel];
7
+ dataDir: string;
8
+ runAllOptions: {
9
+ apiToken: string | null | undefined;
10
+ commitSha: string | null | undefined;
11
+ appUrl: string | null | undefined;
12
+ headless: boolean | null | undefined;
13
+ devTools: boolean | null | undefined;
14
+ bypassCSP: boolean | null | undefined;
15
+ diffThreshold: number | null | undefined;
16
+ diffPixelThreshold: number | null | undefined;
17
+ padTime: boolean;
18
+ networkStubbing: boolean;
19
+ };
20
+ testCase: TestCase;
21
+ };
22
+ }
23
+ export interface ResultMessage {
24
+ kind: "result";
25
+ data: {
26
+ result: TestCaseResult;
27
+ };
28
+ }
@@ -0,0 +1,2 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
@@ -0,0 +1,21 @@
1
+ import { AxiosInstance } from "axios";
2
+ import { TestRun } from "../api/test-run.api";
3
+ import { MeticulousCliConfig, TestCaseResult } from "../config/config.types";
4
+ export interface RunAllTestsInParallelOptions {
5
+ config: MeticulousCliConfig;
6
+ client: AxiosInstance;
7
+ testRun: TestRun;
8
+ apiToken: string | null | undefined;
9
+ commitSha: string | null | undefined;
10
+ appUrl: string | null | undefined;
11
+ headless: boolean | null | undefined;
12
+ devTools: boolean | null | undefined;
13
+ bypassCSP: boolean | null | undefined;
14
+ diffThreshold: number | null | undefined;
15
+ diffPixelThreshold: number | null | undefined;
16
+ padTime: boolean;
17
+ networkStubbing: boolean;
18
+ parallelTasks: number | null | undefined;
19
+ }
20
+ /** Handler for running Meticulous tests in parallel using child processes */
21
+ export declare const runAllTestsInParallel: (options: RunAllTestsInParallelOptions) => Promise<TestCaseResult[]>;
@@ -0,0 +1,120 @@
1
+ "use strict";
2
+ var __importDefault = (this && this.__importDefault) || function (mod) {
3
+ return (mod && mod.__esModule) ? mod : { "default": mod };
4
+ };
5
+ Object.defineProperty(exports, "__esModule", { value: true });
6
+ exports.runAllTestsInParallel = void 0;
7
+ const common_1 = require("@alwaysmeticulous/common");
8
+ const child_process_1 = require("child_process");
9
+ const loglevel_1 = __importDefault(require("loglevel"));
10
+ const os_1 = require("os");
11
+ const path_1 = require("path");
12
+ const test_run_api_1 = require("../api/test-run.api");
13
+ /** Handler for running Meticulous tests in parallel using child processes */
14
+ const runAllTestsInParallel = async ({ config, client, testRun, apiToken, commitSha, appUrl, headless, devTools, bypassCSP, diffThreshold, diffPixelThreshold, padTime, networkStubbing, parallelTasks, }) => {
15
+ const logger = loglevel_1.default.getLogger(common_1.METICULOUS_LOGGER_NAME);
16
+ const results = [];
17
+ const queue = [...(config.testCases || [])];
18
+ const allTasksDone = (0, common_1.defer)();
19
+ let inProgress = 0;
20
+ const maxTasks = parallelTasks !== null && parallelTasks !== void 0 ? parallelTasks : Math.max((0, os_1.cpus)().length, 1);
21
+ logger.debug(`Running with ${maxTasks} maximum tasks in parallel`);
22
+ const taskHandler = (0, path_1.join)(__dirname, "task.handler.js");
23
+ // Starts running a test case in a child process
24
+ const startTask = (testCase) => {
25
+ const deferredResult = (0, common_1.defer)();
26
+ const child = (0, child_process_1.fork)(taskHandler, [], { stdio: "inherit" });
27
+ const messageHandler = (message) => {
28
+ if (message &&
29
+ typeof message === "object" &&
30
+ message["kind"] === "result") {
31
+ const resultMessage = message;
32
+ deferredResult.resolve(resultMessage.data.result);
33
+ child.off("message", messageHandler);
34
+ }
35
+ };
36
+ child.on("error", (error) => {
37
+ if (deferredResult.getState() === "pending") {
38
+ deferredResult.reject(error);
39
+ }
40
+ });
41
+ child.on("exit", (code) => {
42
+ if (code) {
43
+ logger.debug(`child exited with code: ${code}`);
44
+ }
45
+ if (deferredResult.getState() === "pending") {
46
+ deferredResult.reject(new Error("No result"));
47
+ }
48
+ });
49
+ child.on("message", messageHandler);
50
+ // Send test case and arguments to child process
51
+ const initMessage = {
52
+ kind: "init",
53
+ data: {
54
+ logLevel: logger.getLevel(),
55
+ dataDir: (0, common_1.getMeticulousLocalDataDir)(),
56
+ runAllOptions: {
57
+ apiToken,
58
+ commitSha,
59
+ appUrl,
60
+ headless,
61
+ devTools,
62
+ bypassCSP,
63
+ diffThreshold,
64
+ diffPixelThreshold,
65
+ padTime,
66
+ networkStubbing,
67
+ },
68
+ testCase,
69
+ },
70
+ };
71
+ child.send(initMessage);
72
+ // Handle task completion
73
+ deferredResult.promise
74
+ .catch(() => {
75
+ const result = {
76
+ ...testCase,
77
+ headReplayId: "",
78
+ result: "fail",
79
+ };
80
+ return result;
81
+ })
82
+ .then(async (result) => {
83
+ --inProgress;
84
+ results.push(result);
85
+ (0, test_run_api_1.putTestRunResults)({
86
+ client,
87
+ testRunId: testRun.id,
88
+ status: "Running",
89
+ resultData: { results },
90
+ })
91
+ .catch((error) => {
92
+ logger.error(`Error while pushing partial results: ${error}`);
93
+ })
94
+ .then(() => {
95
+ var _a;
96
+ if (results.length === (((_a = config.testCases) === null || _a === void 0 ? void 0 : _a.length) || 0)) {
97
+ allTasksDone.resolve();
98
+ }
99
+ });
100
+ process.nextTick(checkNextTask);
101
+ });
102
+ };
103
+ // Checks if we can start a new child process
104
+ const checkNextTask = () => {
105
+ if (inProgress >= maxTasks) {
106
+ return;
107
+ }
108
+ const testCase = queue.shift();
109
+ if (!testCase) {
110
+ return;
111
+ }
112
+ ++inProgress;
113
+ startTask(testCase);
114
+ process.nextTick(checkNextTask);
115
+ };
116
+ process.nextTick(checkNextTask);
117
+ await allTasksDone.promise;
118
+ return results;
119
+ };
120
+ exports.runAllTestsInParallel = runAllTestsInParallel;
@@ -0,0 +1 @@
1
+ export {};
@@ -0,0 +1,94 @@
1
+ "use strict";
2
+ var __importDefault = (this && this.__importDefault) || function (mod) {
3
+ return (mod && mod.__esModule) ? mod : { "default": mod };
4
+ };
5
+ Object.defineProperty(exports, "__esModule", { value: true });
6
+ const common_1 = require("@alwaysmeticulous/common");
7
+ const loglevel_1 = __importDefault(require("loglevel"));
8
+ const luxon_1 = require("luxon");
9
+ const replay_command_1 = require("../commands/replay/replay.command");
10
+ const screenshot_diff_command_1 = require("../commands/screenshot-diff/screenshot-diff.command");
11
+ const logger_utils_1 = require("../utils/logger.utils");
12
+ const INIT_TIMEOUT = luxon_1.Duration.fromObject({ second: 1 });
13
+ const waitForInitMessage = () => {
14
+ return Promise.race([
15
+ new Promise((resolve) => {
16
+ const messageHandler = (message) => {
17
+ if (message &&
18
+ typeof message === "object" &&
19
+ message["kind"] === "init") {
20
+ const initMessage = message;
21
+ resolve(initMessage);
22
+ process.off("message", messageHandler);
23
+ }
24
+ };
25
+ process.on("message", messageHandler);
26
+ }),
27
+ new Promise((_resolve, reject) => {
28
+ setTimeout(() => {
29
+ reject(new Error("Timed out waiting for init message"));
30
+ }, INIT_TIMEOUT.toMillis());
31
+ }),
32
+ ]);
33
+ };
34
+ const main = async () => {
35
+ (0, logger_utils_1.initLogger)();
36
+ const logger = loglevel_1.default.getLogger(common_1.METICULOUS_LOGGER_NAME);
37
+ if (!process.send) {
38
+ console.error("Error: not started as a child process");
39
+ process.exit(1);
40
+ }
41
+ const initMessage = await waitForInitMessage();
42
+ const { logLevel, dataDir, runAllOptions, testCase } = initMessage.data;
43
+ logger.setLevel(logLevel);
44
+ (0, common_1.getMeticulousLocalDataDir)(dataDir);
45
+ const { apiToken, commitSha, appUrl, headless, devTools, bypassCSP, diffThreshold, diffPixelThreshold, padTime, networkStubbing, } = runAllOptions;
46
+ const { sessionId, baseReplayId, options } = testCase;
47
+ const replayPromise = (0, replay_command_1.replayCommandHandler)({
48
+ apiToken,
49
+ commitSha,
50
+ sessionId,
51
+ appUrl,
52
+ headless,
53
+ devTools,
54
+ bypassCSP,
55
+ screenshot: true,
56
+ baseReplayId,
57
+ diffThreshold,
58
+ diffPixelThreshold,
59
+ save: false,
60
+ exitOnMismatch: false,
61
+ padTime,
62
+ networkStubbing,
63
+ ...options,
64
+ });
65
+ const result = await replayPromise
66
+ .then((replay) => ({
67
+ ...testCase,
68
+ headReplayId: replay.id,
69
+ result: "pass",
70
+ }))
71
+ .catch((error) => {
72
+ if (error instanceof screenshot_diff_command_1.DiffError && error.extras) {
73
+ return {
74
+ ...testCase,
75
+ headReplayId: error.extras.headReplayId,
76
+ result: "fail",
77
+ };
78
+ }
79
+ logger.error(error);
80
+ return { ...testCase, headReplayId: "", result: "fail" };
81
+ });
82
+ const resultMessage = {
83
+ kind: "result",
84
+ data: {
85
+ result,
86
+ },
87
+ };
88
+ process.send(resultMessage);
89
+ process.disconnect();
90
+ };
91
+ main().catch((error) => {
92
+ console.error(error);
93
+ process.exit(1);
94
+ });