@lambdatest/smartui-cli 3.0.12 → 4.0.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.
Files changed (2) hide show
  1. package/dist/index.cjs +333 -75
  2. package/package.json +6 -5
package/dist/index.cjs CHANGED
@@ -16,6 +16,7 @@ var axios = require('axios');
16
16
  var child_process = require('child_process');
17
17
  var spawn = require('cross-spawn');
18
18
  var test = require('@playwright/test');
19
+ var sharp = require('sharp');
19
20
 
20
21
  function _interopDefault (e) { return e && e.__esModule ? e : { default: e }; }
21
22
 
@@ -29,6 +30,7 @@ var addErrors__default = /*#__PURE__*/_interopDefault(addErrors);
29
30
  var FormData__default = /*#__PURE__*/_interopDefault(FormData);
30
31
  var axios__default = /*#__PURE__*/_interopDefault(axios);
31
32
  var spawn__default = /*#__PURE__*/_interopDefault(spawn);
33
+ var sharp__default = /*#__PURE__*/_interopDefault(sharp);
32
34
 
33
35
  var __defProp = Object.defineProperty;
34
36
  var __getOwnPropSymbols = Object.getOwnPropertySymbols;
@@ -128,6 +130,20 @@ var constants_default = {
128
130
  MOBILE_ORIENTATION_LANDSCAPE: "landscape",
129
131
  // CI
130
132
  GITHUB_API_HOST: "https://api.github.com",
133
+ // log file path
134
+ LOG_FILE_PATH: ".smartui.log",
135
+ // Disallowed file extension
136
+ FILE_EXTENSION_ZIP: ".zip",
137
+ FILE_EXTENSION_GIFS: "gif",
138
+ // Default scrollTime
139
+ DEFAULT_SCROLL_TIME: 8,
140
+ // Magic Numbers
141
+ MAGIC_NUMBERS: [
142
+ { ext: "jpg", magic: Buffer.from([255, 216, 255]) },
143
+ { ext: "jpeg", magic: Buffer.from([255, 216, 255]) },
144
+ { ext: "png", magic: Buffer.from([137, 80, 78, 71, 13, 10, 26, 10]) },
145
+ { ext: "gif", magic: Buffer.from([71, 73, 70, 56]) }
146
+ ],
131
147
  SUPPORTED_MOBILE_DEVICES: {
132
148
  "Blackberry KEY2 LE": { os: "android", viewport: { width: 412, height: 618 } },
133
149
  "Galaxy A12": { os: "android", viewport: { width: 360, height: 800 } },
@@ -421,6 +437,16 @@ var ConfigSchema = {
421
437
  type: "boolean",
422
438
  errorMessage: "Invalid config; enableJavaScript must be true/false"
423
439
  },
440
+ cliEnableJavaScript: {
441
+ type: "boolean",
442
+ errorMessage: "Invalid config; cliEnableJavaScript must be true/false"
443
+ },
444
+ scrollTime: {
445
+ type: "number",
446
+ minimum: 1,
447
+ maximum: 1e3,
448
+ errorMessage: "Invalid config; scrollTime must be > 1 and <= 1000"
449
+ },
424
450
  allowedHostnames: {
425
451
  type: "array",
426
452
  items: {
@@ -632,7 +658,15 @@ var validateFigmaDesignConfig = ajv.compile(FigmaDesignConfigSchema);
632
658
 
633
659
  // src/lib/server.ts
634
660
  var server_default = (ctx) => __async(void 0, null, function* () {
635
- const server = fastify__default.default({ logger: ctx.env.LT_SDK_DEBUG ? true : false, bodyLimit: 3e7 });
661
+ const server = fastify__default.default({
662
+ logger: {
663
+ level: "debug",
664
+ stream: { write: (message) => {
665
+ ctx.log.debug(message);
666
+ } }
667
+ },
668
+ bodyLimit: 3e7
669
+ });
636
670
  const opts = {};
637
671
  const SMARTUI_DOM = fs5.readFileSync(path2__default.default.resolve(__dirname, "dom-serializer.js"), "utf-8");
638
672
  server.get("/healthcheck", opts, (_, reply) => {
@@ -672,7 +706,6 @@ var env_default = () => {
672
706
  const {
673
707
  PROJECT_TOKEN = "",
674
708
  SMARTUI_CLIENT_API_URL = "https://api.lambdatest.com/visualui/1.0",
675
- LT_SDK_LOG_LEVEL,
676
709
  LT_SDK_DEBUG,
677
710
  SMARTUI_GIT_INFO_FILEPATH,
678
711
  HTTP_PROXY,
@@ -687,7 +720,6 @@ var env_default = () => {
687
720
  return {
688
721
  PROJECT_TOKEN,
689
722
  SMARTUI_CLIENT_API_URL,
690
- LT_SDK_LOG_LEVEL,
691
723
  LT_SDK_DEBUG,
692
724
  SMARTUI_GIT_INFO_FILEPATH,
693
725
  HTTP_PROXY,
@@ -700,37 +732,37 @@ var env_default = () => {
700
732
  CURRENT_BRANCH
701
733
  };
702
734
  };
703
- var logContext = { task: "smartui-cli" };
735
+ var logContext = {};
704
736
  function updateLogContext(newContext) {
705
737
  logContext = __spreadValues(__spreadValues({}, logContext), newContext);
706
738
  }
707
739
  var logLevel = () => {
708
740
  let env = env_default();
709
- let debug = env.LT_SDK_DEBUG === "true" ? "debug" : void 0;
710
- return debug || env.LT_SDK_LOG_LEVEL || "info";
741
+ return env.LT_SDK_DEBUG === "true" ? "debug" : "info";
711
742
  };
712
743
  var logger = winston.createLogger({
713
- level: logLevel(),
714
744
  format: winston.format.combine(
715
745
  winston.format.timestamp(),
716
746
  winston.format.printf((info) => {
717
747
  let contextString = Object.values(logContext).join(" | ");
718
- let message = typeof info.message === "object" ? JSON.stringify(info.message) : info.message;
748
+ let message = typeof info.message === "object" ? JSON.stringify(info.message).trim() : info.message.trim();
719
749
  switch (info.level) {
720
- case "debug":
721
- message = chalk7__default.default.blue(message);
722
- break;
723
750
  case "warn":
724
751
  message = chalk7__default.default.yellow(message);
725
752
  break;
726
- case "error":
727
- message = chalk7__default.default.red(message);
728
- break;
729
753
  }
730
754
  return info.level === "info" ? message : `[${contextString}:${info.level}] ` + message;
731
755
  })
732
756
  ),
733
- transports: [new winston.transports.Console(), new winston.transports.File({ filename: ".smartui.log" })]
757
+ transports: [
758
+ new winston.transports.Console({
759
+ level: logLevel()
760
+ }),
761
+ new winston.transports.File({
762
+ level: "debug",
763
+ filename: constants_default.LOG_FILE_PATH
764
+ })
765
+ ]
734
766
  });
735
767
  var logger_default = logger;
736
768
 
@@ -774,7 +806,7 @@ var auth_default = (ctx) => {
774
806
  };
775
807
 
776
808
  // package.json
777
- var version = "3.0.12";
809
+ var version = "4.0.1";
778
810
  var package_default = {
779
811
  name: "@lambdatest/smartui-cli",
780
812
  version,
@@ -798,10 +830,10 @@ var package_default = {
798
830
  author: "LambdaTest <keys@lambdatest.com>",
799
831
  license: "MIT",
800
832
  dependencies: {
801
- "@playwright/browser-chromium": "^1.40.1",
802
- "@playwright/browser-firefox": "^1.40.1",
803
- "@playwright/browser-webkit": "^1.40.1",
804
- "@playwright/test": "^1.40.1",
833
+ "@playwright/browser-chromium": "^1.45.3",
834
+ "@playwright/browser-firefox": "^1.45.3",
835
+ "@playwright/browser-webkit": "^1.45.3",
836
+ "@playwright/test": "^1.45.3",
805
837
  "@types/cross-spawn": "^6.0.4",
806
838
  "@types/node": "^20.8.9",
807
839
  "@types/which": "^3.0.2",
@@ -814,6 +846,7 @@ var package_default = {
814
846
  fastify: "^4.24.3",
815
847
  "form-data": "^4.0.0",
816
848
  listr2: "^7.0.1",
849
+ sharp: "^0.33.4",
817
850
  tsup: "^7.2.0",
818
851
  which: "^4.0.0",
819
852
  winston: "^3.10.0"
@@ -829,11 +862,11 @@ var httpClient = class {
829
862
  headers: { "projectToken": PROJECT_TOKEN }
830
863
  });
831
864
  }
832
- request(config, log) {
865
+ request(config, log2) {
833
866
  return __async(this, null, function* () {
834
- log.debug(`http request: ${config.method} ${config.url}`);
867
+ log2.debug(`http request: ${config.method} ${config.url}`);
835
868
  return this.axiosInstance.request(config).then((resp) => {
836
- log.debug(`http response: ${JSON.stringify({
869
+ log2.debug(`http response: ${JSON.stringify({
837
870
  status: resp.status,
838
871
  headers: resp.headers,
839
872
  body: resp.data
@@ -842,7 +875,7 @@ var httpClient = class {
842
875
  }).catch((error) => {
843
876
  var _a;
844
877
  if (error.response) {
845
- log.debug(`http response: ${JSON.stringify({
878
+ log2.debug(`http response: ${JSON.stringify({
846
879
  status: error.response.status,
847
880
  headers: error.response.headers,
848
881
  body: error.response.data
@@ -850,21 +883,21 @@ var httpClient = class {
850
883
  throw new Error(((_a = error.response.data.error) == null ? void 0 : _a.message) || error.response.data.message);
851
884
  }
852
885
  if (error.request) {
853
- log.debug(`http request failed: ${error.toJSON()}`);
886
+ log2.debug(`http request failed: ${error.toJSON()}`);
854
887
  throw new Error(error.toJSON().message);
855
888
  }
856
- log.debug(`http request failed: ${error.message}`);
889
+ log2.debug(`http request failed: ${error.message}`);
857
890
  throw new Error(error.message);
858
891
  });
859
892
  });
860
893
  }
861
- auth(log) {
894
+ auth(log2) {
862
895
  return this.request({
863
896
  url: "/token/verify",
864
897
  method: "GET"
865
- }, log);
898
+ }, log2);
866
899
  }
867
- createBuild(git, config, log) {
900
+ createBuild(git, config, log2) {
868
901
  return this.request({
869
902
  url: "/build",
870
903
  method: "POST",
@@ -872,9 +905,9 @@ var httpClient = class {
872
905
  git,
873
906
  config
874
907
  }
875
- }, log);
908
+ }, log2);
876
909
  }
877
- finalizeBuild(buildId, totalSnapshots, log) {
910
+ finalizeBuild(buildId, totalSnapshots, log2) {
878
911
  let params = { buildId };
879
912
  if (totalSnapshots > -1)
880
913
  params.totalSnapshots = totalSnapshots;
@@ -882,7 +915,7 @@ var httpClient = class {
882
915
  url: "/build",
883
916
  method: "DELETE",
884
917
  params
885
- }, log);
918
+ }, log2);
886
919
  }
887
920
  uploadSnapshot(ctx, snapshot) {
888
921
  return this.request({
@@ -898,7 +931,7 @@ var httpClient = class {
898
931
  }
899
932
  }, ctx.log);
900
933
  }
901
- uploadScreenshot({ id: buildId, name: buildName, baseline }, ssPath, ssName, browserName, viewport, log) {
934
+ uploadScreenshot({ id: buildId, name: buildName, baseline }, ssPath, ssName, browserName, viewport, log2) {
902
935
  browserName = browserName === constants_default.SAFARI ? constants_default.WEBKIT : browserName;
903
936
  const file = fs5__default.default.readFileSync(ssPath);
904
937
  const form = new FormData__default.default();
@@ -915,7 +948,7 @@ var httpClient = class {
915
948
  headers: form.getHeaders(),
916
949
  data: form
917
950
  }).then(() => {
918
- log.debug(`${ssName} for ${browserName} ${viewport} uploaded successfully`);
951
+ log2.debug(`${ssName} for ${browserName} ${viewport} uploaded successfully`);
919
952
  }).catch((error) => {
920
953
  if (error.response) {
921
954
  throw new Error(error.response.data.error.message);
@@ -926,7 +959,7 @@ var httpClient = class {
926
959
  throw new Error(error.message);
927
960
  });
928
961
  }
929
- checkUpdate(log) {
962
+ checkUpdate(log2) {
930
963
  return this.request({
931
964
  url: `/packageinfo`,
932
965
  method: "GET",
@@ -935,9 +968,9 @@ var httpClient = class {
935
968
  packageName: package_default.name,
936
969
  packageVersion: package_default.version
937
970
  }
938
- }, log);
971
+ }, log2);
939
972
  }
940
- getFigmaFilesAndImages(figmaFileToken, figmaToken, queryParams, authToken, depth, markBaseline, buildName, log) {
973
+ getFigmaFilesAndImages(figmaFileToken, figmaToken, queryParams, authToken, depth, markBaseline, buildName, log2) {
941
974
  const requestBody = {
942
975
  figma_file_token: figmaFileToken,
943
976
  figma_token: figmaToken,
@@ -954,7 +987,34 @@ var httpClient = class {
954
987
  "Content-Type": "application/json"
955
988
  },
956
989
  data: JSON.stringify(requestBody)
957
- }, log);
990
+ }, log2);
991
+ }
992
+ getS3PreSignedURL(ctx) {
993
+ return this.request({
994
+ url: `/loguploadurl`,
995
+ method: "POST",
996
+ headers: { "Content-Type": "application/json" },
997
+ data: {
998
+ buildId: ctx.build.id
999
+ }
1000
+ }, ctx.log);
1001
+ }
1002
+ uploadLogs(ctx, uploadURL) {
1003
+ const fileStream = fs5__default.default.createReadStream(constants_default.LOG_FILE_PATH);
1004
+ const { size } = fs5__default.default.statSync(constants_default.LOG_FILE_PATH);
1005
+ return this.request({
1006
+ url: uploadURL,
1007
+ method: "PUT",
1008
+ headers: {
1009
+ "Content-Type": "text/plain",
1010
+ "Content-Length": size
1011
+ },
1012
+ data: fileStream,
1013
+ maxBodyLength: Infinity,
1014
+ // prevent axios from limiting the body size
1015
+ maxContentLength: Infinity
1016
+ // prevent axios from limiting the content size
1017
+ }, ctx.log);
958
1018
  }
959
1019
  };
960
1020
  var ctx_default = (options) => {
@@ -964,6 +1024,10 @@ var ctx_default = (options) => {
964
1024
  let mobileConfig;
965
1025
  let config = constants_default.DEFAULT_CONFIG;
966
1026
  let port;
1027
+ let resolutionOff;
1028
+ let extensionFiles;
1029
+ let ignoreStripExtension;
1030
+ let ignoreFilePattern;
967
1031
  try {
968
1032
  if (options.config) {
969
1033
  config = JSON.parse(fs5__default.default.readFileSync(options.config, "utf-8"));
@@ -979,6 +1043,10 @@ var ctx_default = (options) => {
979
1043
  if (isNaN(port) || port < 1 || port > 65535) {
980
1044
  throw new Error("Invalid port number. Port number must be an integer between 1 and 65535.");
981
1045
  }
1046
+ resolutionOff = options.ignoreResolutions || false;
1047
+ extensionFiles = options.files || ["png", "jpeg", "jpg"];
1048
+ ignoreStripExtension = options.removeExtensions || false;
1049
+ ignoreFilePattern = options.ignoreDir || [];
982
1050
  } catch (error) {
983
1051
  console.log(`[smartui] Error: ${error.message}`);
984
1052
  process.exit();
@@ -1005,8 +1073,11 @@ var ctx_default = (options) => {
1005
1073
  waitForPageRender: config.waitForPageRender || 0,
1006
1074
  waitForTimeout: config.waitForTimeout || 0,
1007
1075
  enableJavaScript: config.enableJavaScript || false,
1076
+ cliEnableJavaScript: config.cliEnableJavaScript || true,
1077
+ scrollTime: config.scrollTime || constants_default.DEFAULT_SCROLL_TIME,
1008
1078
  allowedHostnames: config.allowedHostnames || []
1009
1079
  },
1080
+ uploadFilePath: "",
1010
1081
  webStaticConfig: [],
1011
1082
  git: {
1012
1083
  branch: "",
@@ -1026,16 +1097,20 @@ var ctx_default = (options) => {
1026
1097
  parallel: options.parallel ? true : false,
1027
1098
  markBaseline: options.markBaseline ? true : false,
1028
1099
  buildName: options.buildName || "",
1029
- port
1100
+ port,
1101
+ ignoreResolutions: resolutionOff,
1102
+ fileExtension: extensionFiles,
1103
+ stripExtension: ignoreStripExtension,
1104
+ ignorePattern: ignoreFilePattern
1030
1105
  },
1031
1106
  cliVersion: version,
1032
1107
  totalSnapshots: -1
1033
1108
  };
1034
1109
  };
1035
- function executeCommand(command4) {
1110
+ function executeCommand(command5) {
1036
1111
  let dst = process.cwd();
1037
1112
  try {
1038
- return child_process.execSync(command4, {
1113
+ return child_process.execSync(command5, {
1039
1114
  cwd: dst,
1040
1115
  stdio: ["ignore"],
1041
1116
  encoding: "utf-8"
@@ -1066,8 +1141,8 @@ var git_default = (ctx) => {
1066
1141
  } else {
1067
1142
  const splitCharacter = "<##>";
1068
1143
  const prettyFormat = ["%h", "%H", "%s", "%f", "%b", "%at", "%ct", "%an", "%ae", "%cn", "%ce", "%N", ""];
1069
- const command4 = 'git log -1 --pretty=format:"' + prettyFormat.join(splitCharacter) + '" && git rev-parse --abbrev-ref HEAD && git tag --contains HEAD';
1070
- let res = executeCommand(command4).split(splitCharacter);
1144
+ const command5 = 'git log -1 --pretty=format:"' + prettyFormat.join(splitCharacter) + '" && git rev-parse --abbrev-ref HEAD && git tag --contains HEAD';
1145
+ let res = executeCommand(command5).split(splitCharacter);
1071
1146
  var branchAndTags = res[res.length - 1].split("\n").filter((n) => n);
1072
1147
  var branch = ctx.env.CURRENT_BRANCH || branchAndTags[0];
1073
1148
  branchAndTags.slice(1);
@@ -1208,9 +1283,9 @@ var finalizeBuild_default = (ctx) => {
1208
1283
  return {
1209
1284
  title: `Finalizing build`,
1210
1285
  task: (ctx2, task) => __async(void 0, null, function* () {
1286
+ var _a, _b;
1211
1287
  updateLogContext({ task: "finalizeBuild" });
1212
1288
  try {
1213
- yield new Promise((resolve) => setTimeout(resolve, 2e3));
1214
1289
  yield ctx2.client.finalizeBuild(ctx2.build.id, ctx2.totalSnapshots, ctx2.log);
1215
1290
  task.output = chalk7__default.default.gray(`build url: ${ctx2.build.url}`);
1216
1291
  task.title = "Finalized build";
@@ -1219,6 +1294,17 @@ var finalizeBuild_default = (ctx) => {
1219
1294
  task.output = chalk7__default.default.gray(error.message);
1220
1295
  throw new Error("Finalize build failed");
1221
1296
  }
1297
+ try {
1298
+ yield (_a = ctx2.browser) == null ? void 0 : _a.close();
1299
+ ctx2.log.debug(`Closed browser`);
1300
+ yield (_b = ctx2.server) == null ? void 0 : _b.close();
1301
+ ctx2.log.debug(`Closed server`);
1302
+ let resp = yield ctx2.client.getS3PreSignedURL(ctx2);
1303
+ yield ctx2.client.uploadLogs(ctx2, resp.data.url);
1304
+ fs5.unlinkSync(constants_default.LOG_FILE_PATH);
1305
+ } catch (error) {
1306
+ ctx2.log.debug(error);
1307
+ }
1222
1308
  }),
1223
1309
  rendererOptions: { persistentOutput: true }
1224
1310
  };
@@ -1331,8 +1417,9 @@ function getMobileRenderViewports(ctx) {
1331
1417
  }
1332
1418
  function getRenderViewports(ctx) {
1333
1419
  let mobileRenderViewports = getMobileRenderViewports(ctx);
1420
+ let webRenderViewports = getWebRenderViewports(ctx);
1334
1421
  return [
1335
- ...getWebRenderViewports(ctx),
1422
+ ...webRenderViewports,
1336
1423
  ...mobileRenderViewports[constants_default.MOBILE_OS_IOS],
1337
1424
  ...mobileRenderViewports[constants_default.MOBILE_OS_ANDROID]
1338
1425
  ];
@@ -1403,10 +1490,11 @@ var Queue = class {
1403
1490
  function processSnapshot(snapshot, ctx) {
1404
1491
  return __async(this, null, function* () {
1405
1492
  var _a;
1493
+ updateLogContext({ task: "discovery" });
1406
1494
  ctx.log.debug(`Processing snapshot ${snapshot.name}`);
1407
1495
  let launchOptions = { headless: true };
1408
1496
  let contextOptions = {
1409
- javaScriptEnabled: ctx.config.enableJavaScript,
1497
+ javaScriptEnabled: ctx.config.cliEnableJavaScript,
1410
1498
  userAgent: constants_default.CHROME_USER_AGENT
1411
1499
  };
1412
1500
  if (!((_a = ctx.browser) == null ? void 0 : _a.isConnected())) {
@@ -1527,8 +1615,13 @@ function processSnapshot(snapshot, ctx) {
1527
1615
  }
1528
1616
  }
1529
1617
  let navigated = false;
1618
+ let previousDeviceType = null;
1530
1619
  let renderViewports = getRenderViewports(ctx);
1531
- for (const { viewport, viewportString, fullPage } of renderViewports) {
1620
+ for (const { viewport, viewportString, fullPage, device } of renderViewports) {
1621
+ if (previousDeviceType !== null && previousDeviceType !== device) {
1622
+ navigated = false;
1623
+ }
1624
+ previousDeviceType = device;
1532
1625
  yield page.setViewportSize({ width: viewport.width, height: viewport.height || MIN_VIEWPORT_HEIGHT });
1533
1626
  ctx.log.debug(`Page resized to ${viewport.width}x${viewport.height || MIN_VIEWPORT_HEIGHT}`);
1534
1627
  if (!navigated) {
@@ -1544,14 +1637,15 @@ function processSnapshot(snapshot, ctx) {
1544
1637
  throw new Error(error.message);
1545
1638
  }
1546
1639
  }
1547
- if (ctx.config.enableJavaScript && fullPage)
1548
- yield page.evaluate(scrollToBottomAndBackToTop);
1640
+ if (ctx.config.cliEnableJavaScript && fullPage)
1641
+ yield page.evaluate(scrollToBottomAndBackToTop, { frequency: 100, timing: ctx.config.scrollTime });
1549
1642
  try {
1550
1643
  yield page.waitForLoadState("networkidle", { timeout: 5e3 });
1551
1644
  ctx.log.debug("Network idle 500ms");
1552
1645
  } catch (error) {
1553
1646
  ctx.log.debug(`Network idle failed due to ${error}`);
1554
1647
  }
1648
+ yield new Promise((r) => setTimeout(r, 1e3));
1555
1649
  if (processedOptions.element) {
1556
1650
  let l = yield page.locator(processedOptions.element).all();
1557
1651
  if (l.length === 0) {
@@ -1606,10 +1700,9 @@ function processSnapshot(snapshot, ctx) {
1606
1700
 
1607
1701
  // src/commander/exec.ts
1608
1702
  var command = new commander.Command();
1609
- 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, _, command4) {
1703
+ 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) {
1610
1704
  return __async(this, null, function* () {
1611
- var _a, _b;
1612
- let ctx = ctx_default(command4.optsWithGlobals());
1705
+ let ctx = ctx_default(command5.optsWithGlobals());
1613
1706
  if (!which__default.default.sync(execCommand[0], { nothrow: true })) {
1614
1707
  ctx.log.error(`Error: Command not found "${execCommand[0]}"`);
1615
1708
  return;
@@ -1642,11 +1735,6 @@ command.name("exec").description("Run test commands around SmartUI").argument("<
1642
1735
  yield tasks.run(ctx);
1643
1736
  } catch (error) {
1644
1737
  ctx.log.info("\nRefer docs: https://www.lambdatest.com/support/docs/smart-visual-regression-testing/");
1645
- } finally {
1646
- yield (_a = ctx.browser) == null ? void 0 : _a.close();
1647
- ctx.log.debug(`Closed browser`);
1648
- yield (_b = ctx.server) == null ? void 0 : _b.close();
1649
- ctx.log.debug(`Closed server`);
1650
1738
  }
1651
1739
  });
1652
1740
  });
@@ -1832,6 +1920,108 @@ function captureScreenshots(ctx) {
1832
1920
  return { capturedScreenshots, output };
1833
1921
  });
1834
1922
  }
1923
+ function getImageDimensions(filePath) {
1924
+ const buffer = fs5__default.default.readFileSync(filePath);
1925
+ let width, height;
1926
+ if (buffer.toString("hex", 0, 2) === "ffd8") {
1927
+ let offset = 2;
1928
+ while (offset < buffer.length) {
1929
+ const marker = buffer.toString("hex", offset, offset + 2);
1930
+ offset += 2;
1931
+ const length = buffer.readUInt16BE(offset);
1932
+ if (marker === "ffc0" || marker === "ffc2") {
1933
+ height = buffer.readUInt16BE(offset + 3);
1934
+ width = buffer.readUInt16BE(offset + 5);
1935
+ return { width, height };
1936
+ }
1937
+ offset += length;
1938
+ }
1939
+ } else if (buffer.toString("hex", 1, 4) === "504e47") {
1940
+ width = buffer.readUInt32BE(16);
1941
+ height = buffer.readUInt32BE(20);
1942
+ return { width, height };
1943
+ }
1944
+ return null;
1945
+ }
1946
+ function isAllowedImage(filePath) {
1947
+ return __async(this, null, function* () {
1948
+ try {
1949
+ const fileBuffer = fs5__default.default.readFileSync(filePath);
1950
+ const isMagicValid = constants_default.MAGIC_NUMBERS.some((magic) => fileBuffer.slice(0, magic.magic.length).equals(magic.magic));
1951
+ const metadata = yield sharp__default.default(filePath).metadata();
1952
+ if (metadata.format === constants_default.FILE_EXTENSION_GIFS) {
1953
+ return false;
1954
+ }
1955
+ if (metadata.width > 0 && metadata.height > 0) {
1956
+ return true;
1957
+ }
1958
+ if (isMagicValid && metadata.format !== constants_default.FILE_EXTENSION_GIFS) {
1959
+ return true;
1960
+ }
1961
+ return false;
1962
+ } catch (error) {
1963
+ return false;
1964
+ }
1965
+ });
1966
+ }
1967
+ function uploadScreenshots(ctx) {
1968
+ return __async(this, null, function* () {
1969
+ const allowedExtensions = ctx.options.fileExtension.map((ext) => `.${ext.trim().toLowerCase()}`);
1970
+ let noOfScreenshots = 0;
1971
+ function processDirectory(directory, relativePath = "") {
1972
+ return __async(this, null, function* () {
1973
+ const files = fs5__default.default.readdirSync(directory);
1974
+ for (let file of files) {
1975
+ const filePath = path2__default.default.join(directory, file);
1976
+ const stat = fs5__default.default.statSync(filePath);
1977
+ const relativeFilePath = path2__default.default.join(relativePath, file);
1978
+ if (stat.isDirectory() && ctx.options.ignorePattern.includes(relativeFilePath)) {
1979
+ ctx.log.info(`Ignoring Directory ${relativeFilePath}`);
1980
+ continue;
1981
+ }
1982
+ if (stat.isDirectory()) {
1983
+ yield processDirectory(filePath, relativeFilePath);
1984
+ } else {
1985
+ let fileExtension = path2__default.default.extname(file).toLowerCase();
1986
+ if (allowedExtensions.includes(fileExtension)) {
1987
+ const isValid = yield isAllowedImage(filePath);
1988
+ if (!isValid) {
1989
+ ctx.log.info(`File ${filePath} is not a valid ${fileExtension} image or is corrupted. Skipping.`);
1990
+ continue;
1991
+ }
1992
+ let ssId = relativeFilePath;
1993
+ if (ctx.options.stripExtension) {
1994
+ ssId = path2__default.default.join(relativePath, path2__default.default.basename(file, fileExtension));
1995
+ }
1996
+ let viewport = "default";
1997
+ if (!ctx.options.ignoreResolutions) {
1998
+ const dimensions = getImageDimensions(filePath);
1999
+ if (!dimensions) {
2000
+ ctx.log.info(`Unable to determine dimensions for image: ${filePath}`);
2001
+ } else {
2002
+ const width = dimensions.width;
2003
+ const height = dimensions.height;
2004
+ viewport = `${width}x${height}`;
2005
+ }
2006
+ }
2007
+ yield ctx.client.uploadScreenshot(ctx.build, filePath, ssId, "default", viewport, ctx.log);
2008
+ ctx.log.info(`${filePath} : uploaded successfully`);
2009
+ noOfScreenshots++;
2010
+ } else {
2011
+ ctx.log.info(`File ${filePath} has invalid file extension: ${fileExtension}. Skipping`);
2012
+ }
2013
+ }
2014
+ }
2015
+ });
2016
+ }
2017
+ yield processDirectory(ctx.uploadFilePath);
2018
+ if (noOfScreenshots == 0) {
2019
+ ctx.log.info(`No screenshots uploaded.`);
2020
+ } else {
2021
+ ctx.log.info(`${noOfScreenshots} screenshots uploaded successfully.`);
2022
+ }
2023
+ });
2024
+ }
1835
2025
  var captureScreenshots_default = (ctx) => {
1836
2026
  return {
1837
2027
  title: "Capturing screenshots",
@@ -1857,9 +2047,9 @@ var captureScreenshots_default = (ctx) => {
1857
2047
 
1858
2048
  // src/commander/capture.ts
1859
2049
  var command2 = new commander.Command();
1860
- 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, _, command4) {
2050
+ 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) {
1861
2051
  return __async(this, null, function* () {
1862
- let ctx = ctx_default(command4.optsWithGlobals());
2052
+ let ctx = ctx_default(command5.optsWithGlobals());
1863
2053
  if (!fs5__default.default.existsSync(file)) {
1864
2054
  console.log(`Error: Web Static Config file ${file} not found.`);
1865
2055
  return;
@@ -1899,6 +2089,71 @@ command2.name("capture").description("Capture screenshots of static sites").argu
1899
2089
  });
1900
2090
  });
1901
2091
  var capture_default = command2;
2092
+ var uploadScreenshots_default = (ctx) => {
2093
+ return {
2094
+ title: "Uploading screenshots",
2095
+ task: (ctx2, task) => __async(void 0, null, function* () {
2096
+ try {
2097
+ ctx2.task = task;
2098
+ updateLogContext({ task: "upload" });
2099
+ yield uploadScreenshots(ctx2);
2100
+ task.title = "Screenshots uploaded successfully";
2101
+ } catch (error) {
2102
+ ctx2.log.debug(error);
2103
+ task.output = chalk7__default.default.gray(`${error.message}`);
2104
+ throw new Error("Uploading screenshots failed");
2105
+ }
2106
+ }),
2107
+ rendererOptions: { persistentOutput: true },
2108
+ exitOnError: false
2109
+ };
2110
+ };
2111
+
2112
+ // src/commander/upload.ts
2113
+ var command3 = new commander.Command();
2114
+ command3.name("upload").description("Upload screenshots from given directory").argument("<directory>", "Path of the directory").option("-R, --ignoreResolutions", "Ignore resolution").option("-F, --files <extensions>", "Comma-separated list of allowed file extensions", (val) => {
2115
+ return val.split(",").map((ext) => ext.trim().toLowerCase());
2116
+ }).option("-E, --removeExtensions", "Strips file extensions from snapshot names").option("-i, --ignoreDir <patterns>", "Comma-separated list of directories to ignore", (val) => {
2117
+ return val.split(",").map((pattern) => pattern.trim());
2118
+ }).action(function(directory, _, command5) {
2119
+ return __async(this, null, function* () {
2120
+ let ctx = ctx_default(command5.optsWithGlobals());
2121
+ if (!fs5__default.default.existsSync(directory)) {
2122
+ console.log(`Error: The provided directory ${directory} not found.`);
2123
+ return;
2124
+ }
2125
+ if (path2__default.default.extname(directory).toLowerCase() === constants_default.FILE_EXTENSION_ZIP) {
2126
+ ctx.log.debug(`Error: The provided directory ${directory} is a zip file. Zips are not accepted.`);
2127
+ return;
2128
+ }
2129
+ ctx.uploadFilePath = directory;
2130
+ let tasks = new listr2.Listr(
2131
+ [
2132
+ auth_default(),
2133
+ getGitInfo_default(),
2134
+ createBuild_default(),
2135
+ uploadScreenshots_default(),
2136
+ finalizeBuild_default()
2137
+ ],
2138
+ {
2139
+ rendererOptions: {
2140
+ icon: {
2141
+ [listr2.ListrDefaultRendererLogLevels.OUTPUT]: `\u2192`
2142
+ },
2143
+ color: {
2144
+ [listr2.ListrDefaultRendererLogLevels.OUTPUT]: listr2.color.gray
2145
+ }
2146
+ }
2147
+ }
2148
+ );
2149
+ try {
2150
+ yield tasks.run(ctx);
2151
+ } catch (error) {
2152
+ console.log("\nRefer docs: https://www.lambdatest.com/support/docs/smart-visual-regression-testing/");
2153
+ }
2154
+ });
2155
+ });
2156
+ var upload_default = command3;
1902
2157
 
1903
2158
  // src/lib/uploadFigmaDesigns.ts
1904
2159
  var uploadFigmaDesigns_default = (ctx) => __async(void 0, null, function* () {
@@ -1950,11 +2205,11 @@ var uploadFigmaDesigns_default2 = (ctx) => {
1950
2205
  };
1951
2206
 
1952
2207
  // src/commander/uploadFigma.ts
1953
- var command3 = new commander.Command();
1954
- command3.name("upload-figma").description("Capture screenshots of static sites").argument("<file>", "figma design config file").option("--markBaseline", "Mark the uploaded images as baseline").option("--buildName <buildName>", "Name of the build").action(function(file, _, command4) {
2208
+ var command4 = new commander.Command();
2209
+ command4.name("upload-figma").description("Capture screenshots of static sites").argument("<file>", "figma design config file").option("--markBaseline", "Mark the uploaded images as baseline").option("--buildName <buildName>", "Name of the build").action(function(file, _, command5) {
1955
2210
  return __async(this, null, function* () {
1956
2211
  var _a, _b;
1957
- let ctx = ctx_default(command4.optsWithGlobals());
2212
+ let ctx = ctx_default(command5.optsWithGlobals());
1958
2213
  if (!fs5__default.default.existsSync(file)) {
1959
2214
  console.log(`Error: Figma Config file ${file} not found.`);
1960
2215
  return;
@@ -1992,31 +2247,34 @@ command3.name("upload-figma").description("Capture screenshots of static sites")
1992
2247
  }
1993
2248
  });
1994
2249
  });
1995
- var uploadFigma_default = command3;
2250
+ var uploadFigma_default = command4;
1996
2251
 
1997
2252
  // src/commander/commander.ts
1998
2253
  var program = new commander.Command();
1999
- program.name("smartui").description("CLI to help you run your SmartUI tests on LambdaTest platform").version(`v${version}`).option("-c --config <filepath>", "Config file path").addCommand(exec_default2).addCommand(capture_default).addCommand(configWeb).addCommand(configStatic).addCommand(configFigma).addCommand(uploadFigma_default);
2254
+ program.name("smartui").description("CLI to help you run your SmartUI tests on LambdaTest platform").version(`v${version}`).option("-c --config <filepath>", "Config file path").addCommand(exec_default2).addCommand(capture_default).addCommand(configWeb).addCommand(configStatic).addCommand(upload_default).addCommand(configFigma).addCommand(uploadFigma_default);
2000
2255
  var commander_default = program;
2001
2256
  (function() {
2002
2257
  return __async(this, null, function* () {
2003
2258
  let client = new httpClient(env_default());
2004
- let log = logger_default;
2259
+ let log2 = logger_default;
2005
2260
  try {
2006
- log.info(`
2261
+ let { data: { latestVersion, deprecated, additionalDescription, additionalDescriptionLatestVersion } } = yield client.checkUpdate(log2);
2262
+ log2.info(`
2007
2263
  LambdaTest SmartUI CLI v${package_default.version}`);
2008
- let { data: { latestVersion, deprecated } } = yield client.checkUpdate(log);
2009
- if (deprecated)
2010
- log.warn(`This version is deprecated. A new version ${latestVersion} is available!
2264
+ log2.info(chalk7__default.default.yellow(`${additionalDescription}`));
2265
+ if (deprecated) {
2266
+ log2.warn(`This version is deprecated. A new version ${latestVersion} is available!`);
2267
+ log2.warn(`${additionalDescriptionLatestVersion}
2011
2268
  `);
2012
- else if (package_default.version !== latestVersion)
2013
- log.info(chalk7__default.default.gray(`A new version ${latestVersion} is available!
2269
+ } else if (package_default.version !== latestVersion) {
2270
+ log2.info(chalk7__default.default.green(`A new version ${latestVersion} is available!`));
2271
+ log2.info(chalk7__default.default.red(`${additionalDescriptionLatestVersion}
2014
2272
  `));
2015
- else
2016
- log.info(chalk7__default.default.gray("https://www.npmjs.com/package/@lambdatest/smartui-cli\n"));
2273
+ } else
2274
+ log2.info(chalk7__default.default.gray("https://www.npmjs.com/package/@lambdatest/smartui-cli\n"));
2017
2275
  } catch (error) {
2018
- log.debug(error);
2019
- log.info(chalk7__default.default.gray("https://www.npmjs.com/package/@lambdatest/smartui-cli\n"));
2276
+ log2.debug(error);
2277
+ log2.info(chalk7__default.default.gray("https://www.npmjs.com/package/@lambdatest/smartui-cli\n"));
2020
2278
  }
2021
2279
  commander_default.parse();
2022
2280
  });
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@lambdatest/smartui-cli",
3
- "version": "3.0.12",
3
+ "version": "4.0.1",
4
4
  "description": "A command line interface (CLI) to run SmartUI tests on LambdaTest",
5
5
  "files": [
6
6
  "dist/**/*"
@@ -17,10 +17,10 @@
17
17
  "author": "LambdaTest <keys@lambdatest.com>",
18
18
  "license": "MIT",
19
19
  "dependencies": {
20
- "@playwright/browser-chromium": "^1.40.1",
21
- "@playwright/browser-firefox": "^1.40.1",
22
- "@playwright/browser-webkit": "^1.40.1",
23
- "@playwright/test": "^1.40.1",
20
+ "@playwright/browser-chromium": "^1.45.3",
21
+ "@playwright/browser-firefox": "^1.45.3",
22
+ "@playwright/browser-webkit": "^1.45.3",
23
+ "@playwright/test": "^1.45.3",
24
24
  "@types/cross-spawn": "^6.0.4",
25
25
  "@types/node": "^20.8.9",
26
26
  "@types/which": "^3.0.2",
@@ -33,6 +33,7 @@
33
33
  "fastify": "^4.24.3",
34
34
  "form-data": "^4.0.0",
35
35
  "listr2": "^7.0.1",
36
+ "sharp": "^0.33.4",
36
37
  "tsup": "^7.2.0",
37
38
  "which": "^4.0.0",
38
39
  "winston": "^3.10.0"