@lambdatest/smartui-cli 4.0.7 → 4.0.9

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (3) hide show
  1. package/README.md +68 -0
  2. package/dist/index.cjs +367 -151
  3. package/package.json +3 -2
package/README.md ADDED
@@ -0,0 +1,68 @@
1
+ # SmartUI-CLI
2
+
3
+ <img height="400" src="https://user-images.githubusercontent.com/126776938/232535511-8d51cf1b-1a33-48fc-825c-b13e7a9ec388.png">
4
+
5
+ <p align="center">
6
+ <a href="https://www.lambdatest.com/blog/?utm_source=github&utm_medium=repo&utm_campaign=playwright-sample" target="_bank">Blog</a>
7
+ &nbsp; &#8901; &nbsp;
8
+ <a href="https://www.lambdatest.com/support/docs/?utm_source=github&utm_medium=repo&utm_campaign=playwright-sample" target="_bank">Docs</a>
9
+ &nbsp; &#8901; &nbsp;
10
+ <a href="https://www.lambdatest.com/learning-hub/?utm_source=github&utm_medium=repo&utm_campaign=playwright-sample" target="_bank">Learning Hub</a>
11
+ &nbsp; &#8901; &nbsp;
12
+ <a href="https://www.lambdatest.com/newsletter/?utm_source=github&utm_medium=repo&utm_campaign=playwright-sample" target="_bank">Newsletter</a>
13
+ &nbsp; &#8901; &nbsp;
14
+ <a href="https://www.lambdatest.com/certifications/?utm_source=github&utm_medium=repo&utm_campaign=playwright-sample" target="_bank">Certifications</a>
15
+ &nbsp; &#8901; &nbsp;
16
+ <a href="https://www.youtube.com/c/LambdaTest" target="_bank">YouTube</a>
17
+ </p>
18
+ &emsp;
19
+ &emsp;
20
+ &emsp;
21
+
22
+
23
+
24
+ [<img height="58" width="200" src="https://user-images.githubusercontent.com/70570645/171866795-52c11b49-0728-4229-b073-4b704209ddde.png">](https://accounts.lambdatest.com/register?utm_source=github&utm_medium=repo&utm_campaign=playwright-sample)
25
+
26
+
27
+ The **SmartUI-CLI** allows you to capture visual snapshots of your web applications, upload images, and run visual regression tests using [LambdaTest's SmartUI](https://www.lambdatest.com/visual-regression-testing) platform directly from the command line.
28
+
29
+ - [Installation](#installation)
30
+ - [Commands](#commands)
31
+ - [Documentation](#documentation)
32
+ - [Issues](#issues)
33
+
34
+ ## Installation
35
+
36
+ ```sh-session
37
+ $ npm install smartui-cli
38
+ ```
39
+
40
+ **Note:**
41
+ If you face any problems executing tests with SmartUI-CLI `versions >= v4.x.x`, upgrade your Node.js version to `v20.3` or above.
42
+
43
+ ## Commands
44
+ - `npx smartui exec` - Capture DOM assets for visual testing across multiple browsers and resolutions.
45
+ - `npx smartui capture` - Bulk capture static URLs for visual testing.
46
+ - `npx smartui upload` - Upload custom images or screenshots for visual comparison.
47
+ - `npx smartui upload-figma` - Upload Figma design images for visual comparison.
48
+ - `npx smartui config` - Creates configuration file according to the usecase.
49
+
50
+ ### Documentation
51
+
52
+ In addition to its core functionalities, the SmartUI CLI leverages LambdaTest's cloud infrastructure for robust, scalable visual regression testing across various browsers and devices.
53
+
54
+ - [SmartUI Selenium SDK](https://www.lambdatest.com/support/docs/smartui-selenium-java-sdk) - A complete SDK to capture DOM assets for visual tests.
55
+ - [LambdaTest Documentation](https://www.lambdatest.com/support/docs/) - Official LambdaTest documentation for SmartUI and other integrations.
56
+ - [Bulk capturing static URLs with SmartUI](https://www.lambdatest.com/support/docs/smartui-cli/) - Documentation for capturing satatic urls in bulk with SmartUI
57
+ - [Bring your own screenshots](https://www.lambdatest.com/support/docs/smartui-cli-upload/) - Documentation for capturing satatic urls in bulk
58
+ - [Figma CLI](https://www.lambdatest.com/support/docs/smartui-cli-figma/) - Documentation for uploading figma components to SmartUI
59
+
60
+ ### Issues
61
+
62
+ If you encounter problems with SmartUI-CLI, [add an issue on GitHub](https://github.com/LambdaTest/smartui-cli/issues/new).
63
+
64
+ For other support issues, reach out via [LambdaTest Support](https://www.lambdatest.com/support).
65
+
66
+ ------
67
+
68
+ [Know more](https://www.lambdatest.com/visual-regression-testing) about SmartUI and it's AI enabled comparison engines.
package/dist/index.cjs CHANGED
@@ -4,7 +4,7 @@
4
4
  var commander = require('commander');
5
5
  var which = require('which');
6
6
  var listr2 = require('listr2');
7
- var chalk7 = require('chalk');
7
+ var chalk6 = require('chalk');
8
8
  var path2 = require('path');
9
9
  var fastify = require('fastify');
10
10
  var fs5 = require('fs');
@@ -21,7 +21,7 @@ var sharp = require('sharp');
21
21
  function _interopDefault (e) { return e && e.__esModule ? e : { default: e }; }
22
22
 
23
23
  var which__default = /*#__PURE__*/_interopDefault(which);
24
- var chalk7__default = /*#__PURE__*/_interopDefault(chalk7);
24
+ var chalk6__default = /*#__PURE__*/_interopDefault(chalk6);
25
25
  var path2__default = /*#__PURE__*/_interopDefault(path2);
26
26
  var fastify__default = /*#__PURE__*/_interopDefault(fastify);
27
27
  var fs5__default = /*#__PURE__*/_interopDefault(fs5);
@@ -187,6 +187,9 @@ var constants_default = {
187
187
  MOBILE_OS_IOS: "ios",
188
188
  MOBILE_ORIENTATION_PORTRAIT: "portrait",
189
189
  MOBILE_ORIENTATION_LANDSCAPE: "landscape",
190
+ // build status
191
+ BUILD_COMPLETE: "completed",
192
+ BUILD_ERROR: "error",
190
193
  // CI
191
194
  GITHUB_API_HOST: "https://api.github.com",
192
195
  // log file path
@@ -196,6 +199,8 @@ var constants_default = {
196
199
  FILE_EXTENSION_GIFS: "gif",
197
200
  // Default scrollTime
198
201
  DEFAULT_SCROLL_TIME: 8,
202
+ // Default page load time
203
+ DEFAULT_PAGE_LOAD_TIMEOUT: 18e4,
199
204
  // Magic Numbers
200
205
  MAGIC_NUMBERS: [
201
206
  { ext: "jpg", magic: Buffer.from([255, 216, 255]) },
@@ -888,7 +893,7 @@ var logger = winston.createLogger({
888
893
  let message = typeof info.message === "object" ? JSON.stringify(info.message).trim() : info.message.trim();
889
894
  switch (info.level) {
890
895
  case "warn":
891
- message = chalk7__default.default.yellow(message);
896
+ message = chalk6__default.default.yellow(message);
892
897
  break;
893
898
  }
894
899
  return info.level === "info" ? message : `[${contextString}:${info.level}] ` + message;
@@ -915,11 +920,11 @@ var startServer_default = (ctx) => {
915
920
  updateLogContext({ task: "startServer" });
916
921
  try {
917
922
  ctx2.server = yield server_default(ctx2);
918
- task.output = chalk7__default.default.gray(`listening on port ${(_a = ctx2.server.addresses()[0]) == null ? void 0 : _a.port}`);
923
+ task.output = chalk6__default.default.gray(`listening on port ${(_a = ctx2.server.addresses()[0]) == null ? void 0 : _a.port}`);
919
924
  task.title = "SmartUI started";
920
925
  } catch (error) {
921
926
  ctx2.log.debug(error);
922
- task.output = chalk7__default.default.gray(error.message);
927
+ task.output = chalk6__default.default.gray(error.message);
923
928
  throw new Error("SmartUI server setup failed");
924
929
  }
925
930
  }),
@@ -933,11 +938,11 @@ var auth_default = (ctx) => {
933
938
  updateLogContext({ task: "auth" });
934
939
  try {
935
940
  yield ctx2.client.auth(ctx2.log);
936
- task.output = chalk7__default.default.gray(`using project token '******#${ctx2.env.PROJECT_TOKEN.split("#").pop()}'`);
941
+ task.output = chalk6__default.default.gray(`using project token '******#${ctx2.env.PROJECT_TOKEN.split("#").pop()}'`);
937
942
  task.title = "Authenticated with SmartUI";
938
943
  } catch (error) {
939
944
  ctx2.log.debug(error);
940
- task.output = chalk7__default.default.gray(error.message);
945
+ task.output = chalk6__default.default.gray(error.message);
941
946
  throw new Error("Authentication failed");
942
947
  }
943
948
  }),
@@ -946,7 +951,7 @@ var auth_default = (ctx) => {
946
951
  };
947
952
 
948
953
  // package.json
949
- var version = "4.0.7";
954
+ var version = "4.0.9";
950
955
  var package_default = {
951
956
  name: "@lambdatest/smartui-cli",
952
957
  version,
@@ -956,7 +961,8 @@ var package_default = {
956
961
  ],
957
962
  scripts: {
958
963
  build: "tsup",
959
- release: "pnpm run build && pnpm publish --access public --no-git-checks"
964
+ release: "pnpm run build && pnpm publish --access public --no-git-checks",
965
+ "local-build": "pnpm run build && pnpm pack"
960
966
  },
961
967
  bin: {
962
968
  smartui: "./dist/index.cjs"
@@ -1047,6 +1053,13 @@ var httpClient = class {
1047
1053
  }
1048
1054
  }, log2);
1049
1055
  }
1056
+ getScreenshotData(buildId, baseline, log2) {
1057
+ return this.request({
1058
+ url: "/screenshot",
1059
+ method: "GET",
1060
+ params: { buildId, baseline }
1061
+ }, log2);
1062
+ }
1050
1063
  finalizeBuild(buildId, totalSnapshots, log2) {
1051
1064
  let params = { buildId };
1052
1065
  if (totalSnapshots > -1)
@@ -1090,13 +1103,13 @@ var httpClient = class {
1090
1103
  }).then(() => {
1091
1104
  log2.debug(`${ssName} for ${browserName} ${viewport} uploaded successfully`);
1092
1105
  }).catch((error) => {
1093
- if (error.response) {
1106
+ log2.error(`Unable to upload screenshot ${JSON.stringify(error)}`);
1107
+ if (error && error.response && error.response.data && error.response.data.error) {
1094
1108
  throw new Error(error.response.data.error.message);
1095
1109
  }
1096
- if (error.request) {
1097
- throw new Error(error.toJSON().message);
1110
+ if (error) {
1111
+ throw new Error(JSON.stringify(error));
1098
1112
  }
1099
- throw new Error(error.message);
1100
1113
  });
1101
1114
  }
1102
1115
  checkUpdate(log2) {
@@ -1169,6 +1182,9 @@ var ctx_default = (options) => {
1169
1182
  let extensionFiles;
1170
1183
  let ignoreStripExtension;
1171
1184
  let ignoreFilePattern;
1185
+ let parallelObj;
1186
+ let fetchResultObj;
1187
+ let fetchResultsFileObj;
1172
1188
  try {
1173
1189
  if (options.config) {
1174
1190
  config = JSON.parse(fs5__default.default.readFileSync(options.config, "utf-8"));
@@ -1188,6 +1204,18 @@ var ctx_default = (options) => {
1188
1204
  extensionFiles = options.files || ["png", "jpeg", "jpg"];
1189
1205
  ignoreStripExtension = options.removeExtensions || false;
1190
1206
  ignoreFilePattern = options.ignoreDir || [];
1207
+ parallelObj = options.parallel ? options.parallel === true ? 1 : options.parallel : 1;
1208
+ if (options.fetchResults) {
1209
+ if (options.fetchResults !== true && !options.fetchResults.endsWith(".json")) {
1210
+ console.error("Error: The file extension for --fetch-results must be .json");
1211
+ process.exit(1);
1212
+ }
1213
+ fetchResultObj = true;
1214
+ fetchResultsFileObj = options.fetchResults === true ? "results.json" : options.fetchResults;
1215
+ } else {
1216
+ fetchResultObj = false;
1217
+ fetchResultsFileObj = "";
1218
+ }
1191
1219
  } catch (error) {
1192
1220
  console.log(`[smartui] Error: ${error.message}`);
1193
1221
  process.exit();
@@ -1241,14 +1269,17 @@ var ctx_default = (options) => {
1241
1269
  },
1242
1270
  args: {},
1243
1271
  options: {
1244
- parallel: options.parallel ? true : false,
1272
+ parallel: parallelObj,
1273
+ force: options.force ? true : false,
1245
1274
  markBaseline: options.markBaseline ? true : false,
1246
1275
  buildName: options.buildName || "",
1247
1276
  port,
1248
1277
  ignoreResolutions: resolutionOff,
1249
1278
  fileExtension: extensionFiles,
1250
1279
  stripExtension: ignoreStripExtension,
1251
- ignorePattern: ignoreFilePattern
1280
+ ignorePattern: ignoreFilePattern,
1281
+ fetchResults: fetchResultObj,
1282
+ fetchResultsFileName: fetchResultsFileObj
1252
1283
  },
1253
1284
  cliVersion: version,
1254
1285
  totalSnapshots: -1
@@ -1315,11 +1346,11 @@ var getGitInfo_default = (ctx) => {
1315
1346
  }
1316
1347
  try {
1317
1348
  ctx2.git = git_default(ctx2);
1318
- task.output = chalk7__default.default.gray(`branch: ${ctx2.git.branch}, commit: ${ctx2.git.commitId}, author: ${ctx2.git.commitAuthor}`);
1349
+ task.output = chalk6__default.default.gray(`branch: ${ctx2.git.branch}, commit: ${ctx2.git.commitId}, author: ${ctx2.git.commitAuthor}`);
1319
1350
  task.title = "Fetched git information";
1320
1351
  } catch (error) {
1321
1352
  ctx2.log.debug(error);
1322
- task.output = chalk7__default.default.gray(`${error.message}`);
1353
+ task.output = chalk6__default.default.gray(`${error.message}`);
1323
1354
  throw new Error("Error fetching git repo details");
1324
1355
  }
1325
1356
  }),
@@ -1339,127 +1370,18 @@ var createBuild_default = (ctx) => {
1339
1370
  url: resp.data.buildURL,
1340
1371
  baseline: resp.data.baseline
1341
1372
  };
1342
- task.output = chalk7__default.default.gray(`build id: ${resp.data.buildId}`);
1373
+ task.output = chalk6__default.default.gray(`build id: ${resp.data.buildId}`);
1343
1374
  task.title = "SmartUI build created";
1344
1375
  } catch (error) {
1345
1376
  ctx2.log.debug(error);
1346
- task.output = chalk7__default.default.gray(error.message);
1377
+ task.output = chalk6__default.default.gray(error.message);
1347
1378
  throw new Error("SmartUI build creation failed");
1348
1379
  }
1349
1380
  }),
1350
1381
  rendererOptions: { persistentOutput: true }
1351
1382
  };
1352
1383
  };
1353
- var exec_default = (ctx) => {
1354
- var _a;
1355
- return {
1356
- title: `Executing '${(_a = ctx.args.execCommand) == null ? void 0 : _a.join(" ")}'`,
1357
- task: (ctx2, task) => __async(void 0, null, function* () {
1358
- updateLogContext({ task: "exec" });
1359
- return new Promise((resolve, reject) => {
1360
- var _a2, _b, _c;
1361
- const childProcess = spawn__default.default(ctx2.args.execCommand[0], (_a2 = ctx2.args.execCommand) == null ? void 0 : _a2.slice(1));
1362
- let totalOutput = "";
1363
- const output = listr2.createWritable((chunk) => {
1364
- totalOutput += chunk;
1365
- task.output = chalk7__default.default.gray(totalOutput);
1366
- });
1367
- (_b = childProcess.stdout) == null ? void 0 : _b.pipe(output);
1368
- (_c = childProcess.stderr) == null ? void 0 : _c.pipe(output);
1369
- childProcess.on("error", (error) => {
1370
- var _a3;
1371
- task.output = chalk7__default.default.gray(`error: ${error.message}`);
1372
- throw new Error(`Execution of '${(_a3 = ctx2.args.execCommand) == null ? void 0 : _a3.join(" ")}' failed`);
1373
- });
1374
- childProcess.on("close", (code, signal) => __async(void 0, null, function* () {
1375
- var _a3;
1376
- if (code !== null) {
1377
- task.title = `Execution of '${(_a3 = ctx2.args.execCommand) == null ? void 0 : _a3.join(" ")}' completed; exited with code ${code}`;
1378
- } else if (signal !== null) {
1379
- throw new Error(`Child process killed with signal ${signal}`);
1380
- }
1381
- resolve();
1382
- }));
1383
- });
1384
- }),
1385
- rendererOptions: { persistentOutput: true },
1386
- exitOnError: false
1387
- };
1388
- };
1389
- var processSnapshot_default = (ctx) => {
1390
- return {
1391
- title: `Processing snapshots`,
1392
- task: (ctx2, task) => __async(void 0, null, function* () {
1393
- var _a, _b;
1394
- try {
1395
- if (ctx2.config.delayedUpload) {
1396
- ctx2.log.debug("started after processing because of delayedUpload");
1397
- (_a = ctx2.snapshotQueue) == null ? void 0 : _a.startProcessingfunc();
1398
- }
1399
- yield new Promise((resolve) => {
1400
- let output2 = "";
1401
- const intervalId = setInterval(() => {
1402
- var _a2, _b2, _c;
1403
- if (((_a2 = ctx2.snapshotQueue) == null ? void 0 : _a2.isEmpty()) && !((_b2 = ctx2.snapshotQueue) == null ? void 0 : _b2.isProcessing())) {
1404
- clearInterval(intervalId);
1405
- resolve();
1406
- } else {
1407
- task.title = `Processing snapshot ${(_c = ctx2.snapshotQueue) == null ? void 0 : _c.getProcessingSnapshot()}`;
1408
- }
1409
- }, 500);
1410
- });
1411
- let output = "";
1412
- for (let snapshot of (_b = ctx2.snapshotQueue) == null ? void 0 : _b.getProcessedSnapshots()) {
1413
- if (snapshot.error)
1414
- output += `${chalk7__default.default.red("\u2717")} ${chalk7__default.default.gray(`${snapshot.name}
1415
- [error] ${snapshot.error}`)}
1416
- `;
1417
- else
1418
- output += `${chalk7__default.default.green("\u2713")} ${chalk7__default.default.gray(snapshot.name)}
1419
- ${snapshot.warnings.length ? chalk7__default.default.gray(`[warning] ${snapshot.warnings.join("\n[warning] ")}
1420
- `) : ""}`;
1421
- }
1422
- task.output = output;
1423
- task.title = "Processed snapshots";
1424
- } catch (error) {
1425
- ctx2.log.debug(error);
1426
- task.output = chalk7__default.default.gray(error.message);
1427
- throw new Error("Processing of snapshots failed");
1428
- }
1429
- }),
1430
- rendererOptions: { persistentOutput: true }
1431
- };
1432
- };
1433
- var finalizeBuild_default = (ctx) => {
1434
- return {
1435
- title: `Finalizing build`,
1436
- task: (ctx2, task) => __async(void 0, null, function* () {
1437
- var _a, _b;
1438
- updateLogContext({ task: "finalizeBuild" });
1439
- try {
1440
- yield ctx2.client.finalizeBuild(ctx2.build.id, ctx2.totalSnapshots, ctx2.log);
1441
- task.output = chalk7__default.default.gray(`build url: ${ctx2.build.url}`);
1442
- task.title = "Finalized build";
1443
- } catch (error) {
1444
- ctx2.log.debug(error);
1445
- task.output = chalk7__default.default.gray(error.message);
1446
- throw new Error("Finalize build failed");
1447
- }
1448
- try {
1449
- yield (_a = ctx2.browser) == null ? void 0 : _a.close();
1450
- ctx2.log.debug(`Closed browser`);
1451
- yield (_b = ctx2.server) == null ? void 0 : _b.close();
1452
- ctx2.log.debug(`Closed server`);
1453
- let resp = yield ctx2.client.getS3PreSignedURL(ctx2);
1454
- yield ctx2.client.uploadLogs(ctx2, resp.data.url);
1455
- fs5.unlinkSync(constants_default.LOG_FILE_PATH);
1456
- } catch (error) {
1457
- ctx2.log.debug(error);
1458
- }
1459
- }),
1460
- rendererOptions: { persistentOutput: true }
1461
- };
1462
- };
1384
+ var isPollingActive = false;
1463
1385
  function delDir(dir) {
1464
1386
  if (fs5__default.default.existsSync(dir)) {
1465
1387
  fs5__default.default.rmSync(dir, { recursive: true });
@@ -1493,8 +1415,10 @@ function scrollToBottomAndBackToTop({
1493
1415
  }
1494
1416
  function launchBrowsers(ctx) {
1495
1417
  return __async(this, null, function* () {
1418
+ var _a;
1496
1419
  let browsers = {};
1497
- let launchOptions = { headless: true };
1420
+ const isHeadless = ((_a = process.env.HEADLESS) == null ? void 0 : _a.toLowerCase()) === "false" ? false : true;
1421
+ let launchOptions = { headless: isHeadless };
1498
1422
  if (ctx.config.web) {
1499
1423
  for (const browser of ctx.config.web.browsers) {
1500
1424
  switch (browser) {
@@ -1634,6 +1558,197 @@ function getRenderViewportsForOptions(options) {
1634
1558
  ...mobileRenderViewports[constants_default.MOBILE_OS_ANDROID]
1635
1559
  ];
1636
1560
  }
1561
+ process.on("SIGINT", () => {
1562
+ if (isPollingActive) {
1563
+ console.log("Fetching results interrupted. Exiting...");
1564
+ isPollingActive = false;
1565
+ } else {
1566
+ console.log("\nExiting gracefully...");
1567
+ }
1568
+ process.exit(0);
1569
+ });
1570
+ function startPolling(ctx, task) {
1571
+ return __async(this, null, function* () {
1572
+ ctx.log.info("Fetching results in progress....");
1573
+ isPollingActive = true;
1574
+ const intervalId = setInterval(() => __async(this, null, function* () {
1575
+ if (!isPollingActive) {
1576
+ clearInterval(intervalId);
1577
+ return;
1578
+ }
1579
+ try {
1580
+ const resp = yield ctx.client.getScreenshotData(ctx.build.id, ctx.build.baseline, ctx.log);
1581
+ if (!resp.build) {
1582
+ ctx.log.info("Error: Build data is null.");
1583
+ clearInterval(intervalId);
1584
+ isPollingActive = false;
1585
+ }
1586
+ fs5__default.default.writeFileSync(ctx.options.fetchResultsFileName, JSON.stringify(resp, null, 2));
1587
+ ctx.log.debug(`Updated results in ${ctx.options.fetchResultsFileName}`);
1588
+ if (resp.build.build_status_ind === constants_default.BUILD_COMPLETE || resp.build.build_status_ind === constants_default.BUILD_ERROR) {
1589
+ clearInterval(intervalId);
1590
+ ctx.log.info(`Fetching results completed. Final results written to ${ctx.options.fetchResultsFileName}`);
1591
+ isPollingActive = false;
1592
+ let totalScreenshotsWithMismatches = 0;
1593
+ let totalVariantsWithMismatches = 0;
1594
+ const totalScreenshots = Object.keys(resp.screenshots || {}).length;
1595
+ let totalVariants = 0;
1596
+ for (const [screenshot, variants] of Object.entries(resp.screenshots || {})) {
1597
+ let screenshotHasMismatch = false;
1598
+ let variantMismatchCount = 0;
1599
+ totalVariants += variants.length;
1600
+ for (const variant of variants) {
1601
+ if (variant.mismatch_percentage > 0) {
1602
+ screenshotHasMismatch = true;
1603
+ variantMismatchCount++;
1604
+ }
1605
+ }
1606
+ if (screenshotHasMismatch) {
1607
+ totalScreenshotsWithMismatches++;
1608
+ totalVariantsWithMismatches += variantMismatchCount;
1609
+ }
1610
+ }
1611
+ ctx.log.info(
1612
+ chalk6__default.default.green.bold(
1613
+ `
1614
+ Summary of Mismatches:
1615
+ ${chalk6__default.default.yellow("Total Variants with Mismatches:")} ${chalk6__default.default.white(totalVariantsWithMismatches)} out of ${chalk6__default.default.white(totalVariants)}
1616
+ ${chalk6__default.default.yellow("Total Screenshots with Mismatches:")} ${chalk6__default.default.white(totalScreenshotsWithMismatches)} out of ${chalk6__default.default.white(totalScreenshots)}
1617
+ ${chalk6__default.default.yellow("Branch Name:")} ${chalk6__default.default.white(resp.build.branch)}
1618
+ ${chalk6__default.default.yellow("Project Name:")} ${chalk6__default.default.white(resp.project.name)}
1619
+ ${chalk6__default.default.yellow("Build ID:")} ${chalk6__default.default.white(resp.build.build_id)}
1620
+ `
1621
+ )
1622
+ );
1623
+ }
1624
+ } catch (error) {
1625
+ if (error.message.includes("ENOTFOUND")) {
1626
+ ctx.log.error("Error: Network error occurred while fetching build results. Please check your connection and try again.");
1627
+ clearInterval(intervalId);
1628
+ } else {
1629
+ ctx.log.error(`Error fetching screenshot data: ${error.message}`);
1630
+ }
1631
+ clearInterval(intervalId);
1632
+ isPollingActive = false;
1633
+ }
1634
+ }), 5e3);
1635
+ });
1636
+ }
1637
+
1638
+ // src/tasks/exec.ts
1639
+ var exec_default = (ctx) => {
1640
+ var _a;
1641
+ return {
1642
+ title: `Executing '${(_a = ctx.args.execCommand) == null ? void 0 : _a.join(" ")}'`,
1643
+ task: (ctx2, task) => __async(void 0, null, function* () {
1644
+ if (ctx2.options.fetchResults) {
1645
+ startPolling(ctx2);
1646
+ }
1647
+ updateLogContext({ task: "exec" });
1648
+ return new Promise((resolve, reject) => {
1649
+ var _a2, _b, _c;
1650
+ const childProcess = spawn__default.default(ctx2.args.execCommand[0], (_a2 = ctx2.args.execCommand) == null ? void 0 : _a2.slice(1));
1651
+ let totalOutput = "";
1652
+ const output = listr2.createWritable((chunk) => {
1653
+ totalOutput += chunk;
1654
+ task.output = chalk6__default.default.gray(totalOutput);
1655
+ });
1656
+ (_b = childProcess.stdout) == null ? void 0 : _b.pipe(output);
1657
+ (_c = childProcess.stderr) == null ? void 0 : _c.pipe(output);
1658
+ childProcess.on("error", (error) => {
1659
+ var _a3;
1660
+ task.output = chalk6__default.default.gray(`error: ${error.message}`);
1661
+ throw new Error(`Execution of '${(_a3 = ctx2.args.execCommand) == null ? void 0 : _a3.join(" ")}' failed`);
1662
+ });
1663
+ childProcess.on("close", (code, signal) => __async(void 0, null, function* () {
1664
+ var _a3;
1665
+ if (code !== null) {
1666
+ task.title = `Execution of '${(_a3 = ctx2.args.execCommand) == null ? void 0 : _a3.join(" ")}' completed; exited with code ${code}`;
1667
+ } else if (signal !== null) {
1668
+ throw new Error(`Child process killed with signal ${signal}`);
1669
+ }
1670
+ resolve();
1671
+ }));
1672
+ });
1673
+ }),
1674
+ rendererOptions: { persistentOutput: true },
1675
+ exitOnError: false
1676
+ };
1677
+ };
1678
+ var processSnapshot_default = (ctx) => {
1679
+ return {
1680
+ title: `Processing snapshots`,
1681
+ task: (ctx2, task) => __async(void 0, null, function* () {
1682
+ var _a, _b;
1683
+ try {
1684
+ if (ctx2.config.delayedUpload) {
1685
+ ctx2.log.debug("started after processing because of delayedUpload");
1686
+ (_a = ctx2.snapshotQueue) == null ? void 0 : _a.startProcessingfunc();
1687
+ }
1688
+ yield new Promise((resolve) => {
1689
+ let output2 = "";
1690
+ const intervalId = setInterval(() => {
1691
+ var _a2, _b2, _c;
1692
+ if (((_a2 = ctx2.snapshotQueue) == null ? void 0 : _a2.isEmpty()) && !((_b2 = ctx2.snapshotQueue) == null ? void 0 : _b2.isProcessing())) {
1693
+ clearInterval(intervalId);
1694
+ resolve();
1695
+ } else {
1696
+ task.title = `Processing snapshot ${(_c = ctx2.snapshotQueue) == null ? void 0 : _c.getProcessingSnapshot()}`;
1697
+ }
1698
+ }, 500);
1699
+ });
1700
+ let output = "";
1701
+ for (let snapshot of (_b = ctx2.snapshotQueue) == null ? void 0 : _b.getProcessedSnapshots()) {
1702
+ if (snapshot.error)
1703
+ output += `${chalk6__default.default.red("\u2717")} ${chalk6__default.default.gray(`${snapshot.name}
1704
+ [error] ${snapshot.error}`)}
1705
+ `;
1706
+ else
1707
+ output += `${chalk6__default.default.green("\u2713")} ${chalk6__default.default.gray(snapshot.name)}
1708
+ ${snapshot.warnings.length ? chalk6__default.default.gray(`[warning] ${snapshot.warnings.join("\n[warning] ")}
1709
+ `) : ""}`;
1710
+ }
1711
+ task.output = output;
1712
+ task.title = "Processed snapshots";
1713
+ } catch (error) {
1714
+ ctx2.log.debug(error);
1715
+ task.output = chalk6__default.default.gray(error.message);
1716
+ throw new Error("Processing of snapshots failed");
1717
+ }
1718
+ }),
1719
+ rendererOptions: { persistentOutput: true }
1720
+ };
1721
+ };
1722
+ var finalizeBuild_default = (ctx) => {
1723
+ return {
1724
+ title: `Finalizing build`,
1725
+ task: (ctx2, task) => __async(void 0, null, function* () {
1726
+ var _a, _b;
1727
+ updateLogContext({ task: "finalizeBuild" });
1728
+ try {
1729
+ yield ctx2.client.finalizeBuild(ctx2.build.id, ctx2.totalSnapshots, ctx2.log);
1730
+ task.output = chalk6__default.default.gray(`build url: ${ctx2.build.url}`);
1731
+ task.title = "Finalized build";
1732
+ } catch (error) {
1733
+ ctx2.log.debug(error);
1734
+ task.output = chalk6__default.default.gray(error.message);
1735
+ throw new Error("Finalize build failed");
1736
+ }
1737
+ try {
1738
+ yield (_a = ctx2.browser) == null ? void 0 : _a.close();
1739
+ ctx2.log.debug(`Closed browser`);
1740
+ yield (_b = ctx2.server) == null ? void 0 : _b.close();
1741
+ ctx2.log.debug(`Closed server`);
1742
+ let resp = yield ctx2.client.getS3PreSignedURL(ctx2);
1743
+ yield ctx2.client.uploadLogs(ctx2, resp.data.url);
1744
+ fs5.unlinkSync(constants_default.LOG_FILE_PATH);
1745
+ } catch (error) {
1746
+ ctx2.log.debug(error);
1747
+ }
1748
+ }),
1749
+ rendererOptions: { persistentOutput: true }
1750
+ };
1751
+ };
1637
1752
  var MAX_RESOURCE_SIZE = 15 * 1024 ** 2;
1638
1753
  var ALLOWED_RESOURCES = ["document", "stylesheet", "image", "media", "font", "other"];
1639
1754
  var ALLOWED_STATUSES = [200, 201];
@@ -1641,18 +1756,19 @@ var REQUEST_TIMEOUT = 1e4;
1641
1756
  var MIN_VIEWPORT_HEIGHT = 1080;
1642
1757
  function processSnapshot(snapshot, ctx) {
1643
1758
  return __async(this, null, function* () {
1644
- var _a;
1759
+ var _a, _b;
1645
1760
  updateLogContext({ task: "discovery" });
1646
1761
  ctx.log.debug(`Processing snapshot ${snapshot.name} ${snapshot.url}`);
1762
+ const isHeadless = ((_a = process.env.HEADLESS) == null ? void 0 : _a.toLowerCase()) === "false" ? false : true;
1647
1763
  let launchOptions = {
1648
- headless: true,
1764
+ headless: isHeadless,
1649
1765
  args: constants_default.LAUNCH_ARGS
1650
1766
  };
1651
1767
  let contextOptions = {
1652
1768
  javaScriptEnabled: ctx.config.cliEnableJavaScript,
1653
1769
  userAgent: constants_default.CHROME_USER_AGENT
1654
1770
  };
1655
- if (!((_a = ctx.browser) == null ? void 0 : _a.isConnected())) {
1771
+ if (!((_b = ctx.browser) == null ? void 0 : _b.isConnected())) {
1656
1772
  if (ctx.env.HTTP_PROXY || ctx.env.HTTPS_PROXY)
1657
1773
  launchOptions.proxy = { server: ctx.env.HTTP_PROXY || ctx.env.HTTPS_PROXY };
1658
1774
  ctx.browser = yield test.chromium.launch(launchOptions);
@@ -2210,7 +2326,7 @@ var Queue = class {
2210
2326
 
2211
2327
  // src/commander/exec.ts
2212
2328
  var command = new commander.Command();
2213
- command.name("exec").description("Run test commands around SmartUI").argument("<command...>", "Command supplied for running tests").option("-P, --port <number>", "Port number for the server").action(function(execCommand, _, command5) {
2329
+ command.name("exec").description("Run test commands around SmartUI").argument("<command...>", "Command supplied for running tests").option("-P, --port <number>", "Port number for the server").option("--fetch-results [filename]", "Fetch results and optionally specify an output file, e.g., <filename>.json").action(function(execCommand, _, command5) {
2214
2330
  return __async(this, null, function* () {
2215
2331
  let ctx = ctx_default(command5.optsWithGlobals());
2216
2332
  if (!which__default.default.sync(execCommand[0], { nothrow: true })) {
@@ -2319,7 +2435,7 @@ configFigma.name("config:create-figma").description("Create figma designs config
2319
2435
  });
2320
2436
  function captureScreenshotsForConfig(_0, _1, _2, _3, _4) {
2321
2437
  return __async(this, arguments, function* (ctx, browsers, { name, url, waitForTimeout }, browserName, renderViewports) {
2322
- let pageOptions = { waitUntil: process.env.SMARTUI_PAGE_WAIT_UNTIL_EVENT || "load" };
2438
+ let pageOptions = { waitUntil: process.env.SMARTUI_PAGE_WAIT_UNTIL_EVENT || "load", timeout: ctx.config.waitForPageRender || constants_default.DEFAULT_PAGE_LOAD_TIMEOUT };
2323
2439
  let ssId = name.toLowerCase().replace(/\s/g, "_");
2324
2440
  let context;
2325
2441
  let contextOptions = {};
@@ -2414,13 +2530,13 @@ function captureScreenshots(ctx) {
2414
2530
  else
2415
2531
  yield captureScreenshotsSync(ctx, staticConfig, browsers);
2416
2532
  delDir(`screenshots/${staticConfig.name.toLowerCase().replace(/\s/g, "_")}`);
2417
- output += `${chalk7__default.default.gray(staticConfig.name)} ${chalk7__default.default.green("\u2713")}
2533
+ output += `${chalk6__default.default.gray(staticConfig.name)} ${chalk6__default.default.green("\u2713")}
2418
2534
  `;
2419
2535
  ctx.task.output = output;
2420
2536
  capturedScreenshots++;
2421
2537
  } catch (error) {
2422
2538
  ctx.log.debug(`screenshot capture failed for ${JSON.stringify(staticConfig)}; error: ${error}`);
2423
- output += `${chalk7__default.default.gray(staticConfig.name)} ${chalk7__default.default.red("\u2717")}
2539
+ output += `${chalk6__default.default.gray(staticConfig.name)} ${chalk6__default.default.red("\u2717")}
2424
2540
  `;
2425
2541
  ctx.task.output = output;
2426
2542
  }
@@ -2532,21 +2648,113 @@ function uploadScreenshots(ctx) {
2532
2648
  }
2533
2649
  });
2534
2650
  }
2651
+ function captureScreenshotsConcurrent(ctx) {
2652
+ return __async(this, null, function* () {
2653
+ delDir("screenshots");
2654
+ let totalSnapshots = ctx.webStaticConfig && ctx.webStaticConfig.length;
2655
+ let browserInstances = ctx.options.parallel || 1;
2656
+ let optimizeBrowserInstances = 0;
2657
+ optimizeBrowserInstances = Math.floor(Math.log2(totalSnapshots));
2658
+ if (optimizeBrowserInstances < 1) {
2659
+ optimizeBrowserInstances = 1;
2660
+ }
2661
+ if (optimizeBrowserInstances > browserInstances) {
2662
+ optimizeBrowserInstances = browserInstances;
2663
+ }
2664
+ if (ctx.options.force && browserInstances > 1) {
2665
+ optimizeBrowserInstances = browserInstances;
2666
+ }
2667
+ let urlsPerInstance = 0;
2668
+ if (optimizeBrowserInstances == 1) {
2669
+ urlsPerInstance = totalSnapshots;
2670
+ } else {
2671
+ urlsPerInstance = Math.ceil(totalSnapshots / optimizeBrowserInstances);
2672
+ }
2673
+ ctx.log.debug(`*** browserInstances requested ${ctx.options.parallel} `);
2674
+ ctx.log.debug(`*** optimizeBrowserInstances ${optimizeBrowserInstances} `);
2675
+ ctx.log.debug(`*** urlsPerInstance ${urlsPerInstance}`);
2676
+ ctx.task.output = `URLs : ${totalSnapshots} || Parallel Browser Instances: ${optimizeBrowserInstances}
2677
+ `;
2678
+ let staticURLChunks = splitURLs(ctx.webStaticConfig, urlsPerInstance);
2679
+ let totalCapturedScreenshots = 0;
2680
+ let output = "";
2681
+ const responses = yield Promise.all(staticURLChunks.map((urlConfig) => __async(this, null, function* () {
2682
+ let { capturedScreenshots, finalOutput } = yield processChunk(ctx, urlConfig);
2683
+ return { capturedScreenshots, finalOutput };
2684
+ })));
2685
+ responses.forEach((response) => {
2686
+ totalCapturedScreenshots += response.capturedScreenshots;
2687
+ output += response.finalOutput;
2688
+ });
2689
+ delDir("screenshots");
2690
+ return { totalCapturedScreenshots, output };
2691
+ });
2692
+ }
2693
+ function splitURLs(arr, chunkSize) {
2694
+ const result = [];
2695
+ for (let i = 0; i < arr.length; i += chunkSize) {
2696
+ result.push(arr.slice(i, i + chunkSize));
2697
+ }
2698
+ return result;
2699
+ }
2700
+ function processChunk(ctx, urlConfig) {
2701
+ return __async(this, null, function* () {
2702
+ let browsers = {};
2703
+ let capturedScreenshots = 0;
2704
+ let finalOutput = "";
2705
+ try {
2706
+ browsers = yield launchBrowsers(ctx);
2707
+ } catch (error) {
2708
+ yield closeBrowsers(browsers);
2709
+ ctx.log.debug(error);
2710
+ throw new Error(`Failed launching browsers ${error}`);
2711
+ }
2712
+ for (let staticConfig of urlConfig) {
2713
+ try {
2714
+ yield captureScreenshotsAsync(ctx, staticConfig, browsers);
2715
+ delDir(`screenshots/${staticConfig.name.toLowerCase().replace(/\s/g, "_")}`);
2716
+ let output = `${chalk6__default.default.gray(staticConfig.name)} ${chalk6__default.default.green("\u2713")}
2717
+ `;
2718
+ ctx.task.output = ctx.task.output ? ctx.task.output + output : output;
2719
+ finalOutput += output;
2720
+ capturedScreenshots++;
2721
+ } catch (error) {
2722
+ ctx.log.debug(`screenshot capture failed for ${JSON.stringify(staticConfig)}; error: ${error}`);
2723
+ let output = `${chalk6__default.default.gray(staticConfig.name)} ${chalk6__default.default.red("\u2717")}
2724
+ `;
2725
+ ctx.task.output += output;
2726
+ finalOutput += output;
2727
+ }
2728
+ }
2729
+ yield closeBrowsers(browsers);
2730
+ return { capturedScreenshots, finalOutput };
2731
+ });
2732
+ }
2535
2733
  var captureScreenshots_default = (ctx) => {
2536
2734
  return {
2537
2735
  title: "Capturing screenshots",
2538
2736
  task: (ctx2, task) => __async(void 0, null, function* () {
2539
2737
  try {
2540
2738
  ctx2.task = task;
2739
+ if (ctx2.options.fetchResults) {
2740
+ startPolling(ctx2, task);
2741
+ }
2541
2742
  updateLogContext({ task: "capture" });
2542
- let { capturedScreenshots, output } = yield captureScreenshots(ctx2);
2543
- if (capturedScreenshots != ctx2.webStaticConfig.length) {
2544
- throw new Error(output);
2743
+ if (ctx2.options.parallel) {
2744
+ let { totalCapturedScreenshots, output } = yield captureScreenshotsConcurrent(ctx2);
2745
+ if (totalCapturedScreenshots != ctx2.webStaticConfig.length) {
2746
+ throw new Error(output);
2747
+ }
2748
+ } else {
2749
+ let { capturedScreenshots, output } = yield captureScreenshots(ctx2);
2750
+ if (capturedScreenshots != ctx2.webStaticConfig.length) {
2751
+ throw new Error(output);
2752
+ }
2545
2753
  }
2546
2754
  task.title = "Screenshots captured successfully";
2547
2755
  } catch (error) {
2548
2756
  ctx2.log.debug(error);
2549
- task.output = chalk7__default.default.gray(`${error.message}`);
2757
+ task.output = chalk6__default.default.gray(`${error.message}`);
2550
2758
  throw new Error("Capturing screenshots failed");
2551
2759
  }
2552
2760
  }),
@@ -2557,21 +2765,26 @@ var captureScreenshots_default = (ctx) => {
2557
2765
 
2558
2766
  // src/commander/capture.ts
2559
2767
  var command2 = new commander.Command();
2560
- command2.name("capture").description("Capture screenshots of static sites").argument("<file>", "Web static config file").option("--parallel", "Capture parallely on all browsers").action(function(file, _, command5) {
2768
+ command2.name("capture").description("Capture screenshots of static sites").argument("<file>", "Web static config file").option("-C, --parallel [number]", "Specify the number of instances per browser", parseInt).option("-F, --force", "forcefully apply the specified parallel instances per browser").option("--fetch-results [filename]", "Fetch results and optionally specify an output file, e.g., <filename>.json").action(function(file, _, command5) {
2561
2769
  return __async(this, null, function* () {
2562
2770
  let ctx = ctx_default(command5.optsWithGlobals());
2563
2771
  if (!fs5__default.default.existsSync(file)) {
2564
- console.log(`Error: Web Static Config file ${file} not found.`);
2772
+ ctx.log.error(`Web Static Config file ${file} not found.`);
2565
2773
  return;
2566
2774
  }
2567
2775
  try {
2568
2776
  ctx.webStaticConfig = JSON.parse(fs5__default.default.readFileSync(file, "utf8"));
2569
2777
  if (!validateWebStaticConfig(ctx.webStaticConfig))
2570
2778
  throw new Error(validateWebStaticConfig.errors[0].message);
2779
+ if (ctx.webStaticConfig && ctx.webStaticConfig.length === 0) {
2780
+ ctx.log.error(`No URLs found in the specified config file -> ${file}`);
2781
+ return;
2782
+ }
2571
2783
  } catch (error) {
2572
- console.log(`[smartui] Error: Invalid Web Static Config; ${error.message}`);
2784
+ ctx.log.error(`Invalid Web Static Config; ${error.message}`);
2573
2785
  return;
2574
2786
  }
2787
+ ctx.log.debug(ctx.config);
2575
2788
  let tasks = new listr2.Listr(
2576
2789
  [
2577
2790
  auth_default(),
@@ -2605,12 +2818,15 @@ var uploadScreenshots_default = (ctx) => {
2605
2818
  task: (ctx2, task) => __async(void 0, null, function* () {
2606
2819
  try {
2607
2820
  ctx2.task = task;
2821
+ if (ctx2.options.fetchResults) {
2822
+ startPolling(ctx2, task);
2823
+ }
2608
2824
  updateLogContext({ task: "upload" });
2609
2825
  yield uploadScreenshots(ctx2);
2610
2826
  task.title = "Screenshots uploaded successfully";
2611
2827
  } catch (error) {
2612
2828
  ctx2.log.debug(error);
2613
- task.output = chalk7__default.default.gray(`${error.message}`);
2829
+ task.output = chalk6__default.default.gray(`${error.message}`);
2614
2830
  throw new Error("Uploading screenshots failed");
2615
2831
  }
2616
2832
  }),
@@ -2625,7 +2841,7 @@ command3.name("upload").description("Upload screenshots from given directory").a
2625
2841
  return val.split(",").map((ext) => ext.trim().toLowerCase());
2626
2842
  }).option("-E, --removeExtensions", "Strips file extensions from snapshot names").option("-i, --ignoreDir <patterns>", "Comma-separated list of directories to ignore", (val) => {
2627
2843
  return val.split(",").map((pattern) => pattern.trim());
2628
- }).action(function(directory, _, command5) {
2844
+ }).option("--fetch-results [filename]", "Fetch results and optionally specify an output file, e.g., <filename>.json").action(function(directory, _, command5) {
2629
2845
  return __async(this, null, function* () {
2630
2846
  let ctx = ctx_default(command5.optsWithGlobals());
2631
2847
  if (!fs5__default.default.existsSync(directory)) {
@@ -2705,7 +2921,7 @@ var uploadFigmaDesigns_default2 = (ctx) => {
2705
2921
  ctx2.log.debug(`Figma designs processed: ${results}`);
2706
2922
  } catch (error) {
2707
2923
  ctx2.log.debug(error);
2708
- task.output = chalk7__default.default.gray(`${error.message}`);
2924
+ task.output = chalk6__default.default.gray(`${error.message}`);
2709
2925
  throw new Error("Uploading Figma designs failed");
2710
2926
  }
2711
2927
  }),
@@ -2771,16 +2987,16 @@ var commander_default = program;
2771
2987
  let { data: { latestVersion, deprecated, additionalDescription } } = yield client.checkUpdate(log2);
2772
2988
  log2.info(`
2773
2989
  LambdaTest SmartUI CLI v${package_default.version}`);
2774
- log2.info(chalk7__default.default.yellow(`${additionalDescription}`));
2990
+ log2.info(chalk6__default.default.yellow(`${additionalDescription}`));
2775
2991
  if (deprecated) {
2776
2992
  log2.warn(`This version is deprecated. A new version ${latestVersion} is available!`);
2777
2993
  } else if (package_default.version !== latestVersion) {
2778
- log2.info(chalk7__default.default.green(`A new version ${latestVersion} is available!`));
2994
+ log2.info(chalk6__default.default.green(`A new version ${latestVersion} is available!`));
2779
2995
  } else
2780
- log2.info(chalk7__default.default.gray("https://www.npmjs.com/package/@lambdatest/smartui-cli\n"));
2996
+ log2.info(chalk6__default.default.gray("https://www.npmjs.com/package/@lambdatest/smartui-cli\n"));
2781
2997
  } catch (error) {
2782
2998
  log2.debug(error);
2783
- log2.info(chalk7__default.default.gray("https://www.npmjs.com/package/@lambdatest/smartui-cli\n"));
2999
+ log2.info(chalk6__default.default.gray("https://www.npmjs.com/package/@lambdatest/smartui-cli\n"));
2784
3000
  }
2785
3001
  commander_default.parse();
2786
3002
  });
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@lambdatest/smartui-cli",
3
- "version": "4.0.7",
3
+ "version": "4.0.9",
4
4
  "description": "A command line interface (CLI) to run SmartUI tests on LambdaTest",
5
5
  "files": [
6
6
  "dist/**/*"
@@ -43,6 +43,7 @@
43
43
  },
44
44
  "scripts": {
45
45
  "build": "tsup",
46
- "release": "pnpm run build && pnpm publish --access public --no-git-checks"
46
+ "release": "pnpm run build && pnpm publish --access public --no-git-checks",
47
+ "local-build": "pnpm run build && pnpm pack"
47
48
  }
48
49
  }