@lambdatest/smartui-cli 4.1.28 → 4.1.30

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 +476 -42
  2. package/package.json +1 -1
package/dist/index.cjs CHANGED
@@ -904,6 +904,12 @@ var SnapshotSchema = {
904
904
  items: { type: "string", minLength: 1 },
905
905
  uniqueItems: true,
906
906
  errorMessage: "Invalid snapshot options; ignoreDOM xpath array must have unique and non-empty items"
907
+ },
908
+ coordinates: {
909
+ type: "array",
910
+ items: { type: "string", minLength: 1 },
911
+ uniqueItems: true,
912
+ errorMessage: "Invalid snapshot options; ignoreDOM coordinates array must have unique and non-empty items"
907
913
  }
908
914
  }
909
915
  },
@@ -933,6 +939,12 @@ var SnapshotSchema = {
933
939
  items: { type: "string", minLength: 1 },
934
940
  uniqueItems: true,
935
941
  errorMessage: "Invalid snapshot options; selectDOM xpath array must have unique and non-empty items"
942
+ },
943
+ coordinates: {
944
+ type: "array",
945
+ items: { type: "string", minLength: 1 },
946
+ uniqueItems: true,
947
+ errorMessage: "Invalid snapshot options; selectDOM coordinates array must have unique and non-empty items"
936
948
  }
937
949
  }
938
950
  },
@@ -1716,6 +1728,180 @@ function calculateVariantCountFromSnapshot(snapshot, globalConfig) {
1716
1728
  }
1717
1729
  return variantCount;
1718
1730
  }
1731
+ function startPdfPolling(ctx) {
1732
+ console.log(chalk__default.default.yellow("\nFetching PDF test results..."));
1733
+ ctx.log.debug(`Starting fetching results for build: ${ctx.build.id || ctx.build.name}`);
1734
+ if (!ctx.build.id && !ctx.build.name) {
1735
+ ctx.log.error(chalk__default.default.red("Error: Build information not found for fetching results"));
1736
+ return;
1737
+ }
1738
+ if (!ctx.env.LT_USERNAME || !ctx.env.LT_ACCESS_KEY) {
1739
+ console.log(chalk__default.default.red("Error: LT_USERNAME and LT_ACCESS_KEY environment variables are required for fetching results"));
1740
+ return;
1741
+ }
1742
+ let attempts = 0;
1743
+ const maxAttempts = 60;
1744
+ console.log(chalk__default.default.yellow("Waiting for results..."));
1745
+ const interval = setInterval(() => __async(this, null, function* () {
1746
+ attempts++;
1747
+ try {
1748
+ const response = yield ctx.client.fetchPdfResults(ctx);
1749
+ if (response.screenshots) {
1750
+ clearInterval(interval);
1751
+ const pdfGroups = groupScreenshotsByPdf(response.screenshots);
1752
+ const pdfsWithMismatches = countPdfsWithMismatches(pdfGroups);
1753
+ const pagesWithMismatches = countPagesWithMismatches(response.screenshots);
1754
+ console.log(chalk__default.default.green("\n\u2713 PDF Test Results:"));
1755
+ console.log(chalk__default.default.green(`Build Name: ${response.build.name}`));
1756
+ console.log(chalk__default.default.green(`Project Name: ${response.project.name}`));
1757
+ console.log(chalk__default.default.green(`Total PDFs: ${Object.keys(pdfGroups).length}`));
1758
+ console.log(chalk__default.default.green(`Total Pages: ${response.screenshots.length}`));
1759
+ if (pdfsWithMismatches > 0 || pagesWithMismatches > 0) {
1760
+ console.log(chalk__default.default.yellow(`${pdfsWithMismatches} PDFs and ${pagesWithMismatches} Pages in build ${response.build.name} have changes present.`));
1761
+ } else {
1762
+ console.log(chalk__default.default.green("All PDFs match the baseline."));
1763
+ }
1764
+ Object.entries(pdfGroups).forEach(([pdfName, pages]) => {
1765
+ const hasMismatch = pages.some((page) => page.mismatch_percentage > 0);
1766
+ const statusColor = hasMismatch ? chalk__default.default.yellow : chalk__default.default.green;
1767
+ console.log(statusColor(`
1768
+ \u{1F4C4} ${pdfName} (${pages.length} pages)`));
1769
+ pages.forEach((page) => {
1770
+ const pageStatusColor = page.mismatch_percentage > 0 ? chalk__default.default.yellow : chalk__default.default.green;
1771
+ console.log(pageStatusColor(` - Page ${getPageNumber(page.screenshot_name)}: ${page.status} (Mismatch: ${page.mismatch_percentage}%)`));
1772
+ });
1773
+ });
1774
+ const formattedResults = {
1775
+ status: "success",
1776
+ data: {
1777
+ buildId: response.build.id,
1778
+ buildName: response.build.name,
1779
+ projectName: response.project.name,
1780
+ buildStatus: response.build.build_satus,
1781
+ pdfs: formatPdfsForOutput(pdfGroups)
1782
+ }
1783
+ };
1784
+ if (ctx.options.fetchResults && ctx.options.fetchResultsFileName) {
1785
+ const filename = ctx.options.fetchResultsFileName !== "" ? ctx.options.fetchResultsFileName : "pdf-results.json";
1786
+ fs5__default.default.writeFileSync(filename, JSON.stringify(formattedResults, null, 2));
1787
+ console.log(chalk__default.default.green(`
1788
+ Results saved to ${filename}`));
1789
+ }
1790
+ return;
1791
+ }
1792
+ if (attempts >= maxAttempts) {
1793
+ clearInterval(interval);
1794
+ console.log(chalk__default.default.red("\nTimeout: Could not fetch PDF results after 5 minutes"));
1795
+ return;
1796
+ }
1797
+ } catch (error) {
1798
+ ctx.log.debug(`Error during polling: ${error.message}`);
1799
+ if (attempts >= maxAttempts) {
1800
+ clearInterval(interval);
1801
+ console.log(chalk__default.default.red("\nTimeout: Could not fetch PDF results after 5 minutes"));
1802
+ if (error.response && error.response.data) {
1803
+ console.log(chalk__default.default.red(`Error details: ${JSON.stringify(error.response.data)}`));
1804
+ } else {
1805
+ console.log(chalk__default.default.red(`Error details: ${error.message}`));
1806
+ }
1807
+ return;
1808
+ }
1809
+ process.stdout.write(chalk__default.default.yellow("."));
1810
+ }
1811
+ }), 1e4);
1812
+ }
1813
+ function groupScreenshotsByPdf(screenshots) {
1814
+ const pdfGroups = {};
1815
+ screenshots.forEach((screenshot) => {
1816
+ const pdfName = screenshot.screenshot_name.split("#")[0];
1817
+ if (!pdfGroups[pdfName]) {
1818
+ pdfGroups[pdfName] = [];
1819
+ }
1820
+ pdfGroups[pdfName].push(screenshot);
1821
+ });
1822
+ return pdfGroups;
1823
+ }
1824
+ function countPdfsWithMismatches(pdfGroups) {
1825
+ let count = 0;
1826
+ Object.values(pdfGroups).forEach((pages) => {
1827
+ if (pages.some((page) => page.mismatch_percentage > 0)) {
1828
+ count++;
1829
+ }
1830
+ });
1831
+ return count;
1832
+ }
1833
+ function countPagesWithMismatches(screenshots) {
1834
+ return screenshots.filter((screenshot) => screenshot.mismatch_percentage > 0).length;
1835
+ }
1836
+ function formatPdfsForOutput(pdfGroups) {
1837
+ return Object.entries(pdfGroups).map(([pdfName, pages]) => {
1838
+ return {
1839
+ pdfName,
1840
+ pageCount: pages.length,
1841
+ pages: pages.map((page) => ({
1842
+ pageNumber: getPageNumber(page.screenshot_name),
1843
+ screenshotId: page.captured_image_id,
1844
+ mismatchPercentage: page.mismatch_percentage,
1845
+ status: page.status,
1846
+ screenshotUrl: page.shareable_link
1847
+ }))
1848
+ };
1849
+ });
1850
+ }
1851
+ function getPageNumber(screenshotName) {
1852
+ const parts = screenshotName.split("#");
1853
+ return parts.length > 1 ? parts[1] : "1";
1854
+ }
1855
+ function validateCoordinates(coordString, pageHeight, pageWidth, snapshotName) {
1856
+ const coords = coordString.split(",").map(Number);
1857
+ if (coords.length !== 4) {
1858
+ return {
1859
+ valid: false,
1860
+ error: `for snapshot ${snapshotName}, invalid coordinates format: ${coordString}. Expected: top,bottom,left,right`
1861
+ };
1862
+ }
1863
+ const [top, bottom, left, right] = coords;
1864
+ if (coords.some(isNaN)) {
1865
+ return {
1866
+ valid: false,
1867
+ error: `for snapshot ${snapshotName}, invalid coordinate values: ${coordString}. All values must be numbers`
1868
+ };
1869
+ }
1870
+ if (top < 0 || left < 0 || bottom < 0 || right < 0) {
1871
+ return {
1872
+ valid: false,
1873
+ error: `for snapshot ${snapshotName}, invalid coordinate bounds: ${coordString}. top,left,bottom,right must be >= 0`
1874
+ };
1875
+ }
1876
+ if (top >= bottom) {
1877
+ return {
1878
+ valid: false,
1879
+ error: `for snapshot ${snapshotName}, invalid coordinate bounds: ${coordString}. top must be < bottom`
1880
+ };
1881
+ }
1882
+ if (left >= right) {
1883
+ return {
1884
+ valid: false,
1885
+ error: `for snapshot ${snapshotName}, invalid coordinate bounds: ${coordString}. left must be < right`
1886
+ };
1887
+ }
1888
+ if (bottom > pageHeight) {
1889
+ return {
1890
+ valid: false,
1891
+ error: `for snapshot ${snapshotName}, coordinates exceed viewport bounds: ${coordString}. bottom (${bottom}) exceeds viewport height (${pageHeight})`
1892
+ };
1893
+ }
1894
+ if (right > pageWidth) {
1895
+ return {
1896
+ valid: false,
1897
+ error: `for snapshot ${snapshotName}, coordinates exceed viewport bounds: ${coordString}. right (${right}) exceeds viewport width (${pageWidth})`
1898
+ };
1899
+ }
1900
+ return {
1901
+ valid: true,
1902
+ coords: { top, bottom, left, right }
1903
+ };
1904
+ }
1719
1905
 
1720
1906
  // src/lib/server.ts
1721
1907
  var uploadDomToS3ViaEnv = process.env.USE_LAMBDA_INTERNAL || false;
@@ -1764,7 +1950,6 @@ var server_default = (ctx) => __async(void 0, null, function* () {
1764
1950
  }
1765
1951
  } catch (error) {
1766
1952
  ctx.log.debug(`Failed to fetch capabilities for sessionId ${sessionId}: ${error.message}`);
1767
- console.log(`Failed to fetch capabilities for sessionId ${sessionId}: ${error.message}`);
1768
1953
  }
1769
1954
  }
1770
1955
  if (capsBuildId && capsBuildId !== "") {
@@ -1813,19 +1998,39 @@ var server_default = (ctx) => __async(void 0, null, function* () {
1813
1998
  }
1814
1999
  }, 1e3);
1815
2000
  });
1816
- yield ctx.client.finalizeBuild(ctx.build.id, ctx.totalSnapshots, ctx.log);
1817
- yield (_b = ctx.browser) == null ? void 0 : _b.close();
2001
+ for (const [sessionId, capabilities] of ctx.sessionCapabilitiesMap.entries()) {
2002
+ try {
2003
+ const buildId = (capabilities == null ? void 0 : capabilities.buildId) || "";
2004
+ const projectToken = (capabilities == null ? void 0 : capabilities.projectToken) || "";
2005
+ const totalSnapshots = (capabilities == null ? void 0 : capabilities.snapshotCount) || 0;
2006
+ const sessionBuildUrl = (capabilities == null ? void 0 : capabilities.buildURL) || "";
2007
+ const testId = (capabilities == null ? void 0 : capabilities.id) || "";
2008
+ if (buildId && projectToken) {
2009
+ yield ctx.client.finalizeBuildForCapsWithToken(buildId, totalSnapshots, projectToken, ctx.log);
2010
+ }
2011
+ if (testId && buildId) {
2012
+ buildUrls += `TestId ${testId}: ${sessionBuildUrl}
2013
+ `;
2014
+ }
2015
+ } catch (error) {
2016
+ ctx.log.debug(`Error finalizing build for session ${sessionId}: ${error.message}`);
2017
+ }
2018
+ }
2019
+ if (ctx.build && ctx.build.id) {
2020
+ yield ctx.client.finalizeBuild(ctx.build.id, ctx.totalSnapshots, ctx.log);
2021
+ let uploadCLILogsToS3 = ((_b = ctx == null ? void 0 : ctx.config) == null ? void 0 : _b.useLambdaInternal) || uploadDomToS3ViaEnv;
2022
+ if (!uploadCLILogsToS3) {
2023
+ ctx.log.debug(`Log file to be uploaded`);
2024
+ let resp = yield ctx.client.getS3PreSignedURL(ctx);
2025
+ yield ctx.client.uploadLogs(ctx, resp.data.url);
2026
+ } else {
2027
+ ctx.log.debug(`Skipping upload of CLI logs as useLambdaInternal is set`);
2028
+ }
2029
+ }
2030
+ yield (_c = ctx.browser) == null ? void 0 : _c.close();
1818
2031
  if (ctx.server) {
1819
2032
  ctx.server.close();
1820
2033
  }
1821
- let uploadCLILogsToS3 = ((_c = ctx == null ? void 0 : ctx.config) == null ? void 0 : _c.useLambdaInternal) || uploadDomToS3ViaEnv;
1822
- if (!uploadCLILogsToS3) {
1823
- ctx.log.debug(`Log file to be uploaded`);
1824
- let resp = yield ctx.client.getS3PreSignedURL(ctx);
1825
- yield ctx.client.uploadLogs(ctx, resp.data.url);
1826
- } else {
1827
- ctx.log.debug(`Skipping upload of CLI logs as useLambdaInternal is set`);
1828
- }
1829
2034
  if (pingIntervalId !== null) {
1830
2035
  clearInterval(pingIntervalId);
1831
2036
  ctx.log.debug("Ping polling stopped immediately.");
@@ -1942,6 +2147,7 @@ var env_default = () => {
1942
2147
  const {
1943
2148
  PROJECT_TOKEN = "",
1944
2149
  SMARTUI_CLIENT_API_URL = "https://api.lambdatest.com/visualui/1.0",
2150
+ SMARTUI_UPLOAD_URL = "https://api.lambdatest.com",
1945
2151
  SMARTUI_GIT_INFO_FILEPATH,
1946
2152
  SMARTUI_DO_NOT_USE_CAPTURED_COOKIES,
1947
2153
  HTTP_PROXY,
@@ -1964,6 +2170,7 @@ var env_default = () => {
1964
2170
  return {
1965
2171
  PROJECT_TOKEN,
1966
2172
  SMARTUI_CLIENT_API_URL,
2173
+ SMARTUI_UPLOAD_URL,
1967
2174
  SMARTUI_GIT_INFO_FILEPATH,
1968
2175
  HTTP_PROXY,
1969
2176
  HTTPS_PROXY,
@@ -2076,7 +2283,7 @@ var authExec_default = (ctx) => {
2076
2283
  };
2077
2284
 
2078
2285
  // package.json
2079
- var version = "4.1.28";
2286
+ var version = "4.1.30";
2080
2287
  var package_default = {
2081
2288
  name: "@lambdatest/smartui-cli",
2082
2289
  version,
@@ -2133,6 +2340,18 @@ var package_default = {
2133
2340
  }
2134
2341
  };
2135
2342
  var httpClient = class {
2343
+ handleHttpError(error, log2) {
2344
+ var _a;
2345
+ if (error && error.response) {
2346
+ log2.debug(`http response error: ${JSON.stringify({
2347
+ status: error.response.status,
2348
+ body: error.response.data
2349
+ })}`);
2350
+ throw new Error(((_a = error.response.data) == null ? void 0 : _a.message) || error.response.data || `HTTP ${error.response.status} error`);
2351
+ }
2352
+ log2.debug(`http request failed: ${error.message}`);
2353
+ throw new Error(error.message);
2354
+ }
2136
2355
  constructor({ SMARTUI_CLIENT_API_URL, PROJECT_TOKEN, PROJECT_NAME, LT_USERNAME, LT_ACCESS_KEY, SMARTUI_API_PROXY, SMARTUI_API_SKIP_CERTIFICATES }) {
2137
2356
  this.projectToken = PROJECT_TOKEN || "";
2138
2357
  this.projectName = PROJECT_NAME || "";
@@ -2189,6 +2408,8 @@ var httpClient = class {
2189
2408
  return this.axiosInstance(config);
2190
2409
  }
2191
2410
  return Promise.reject(error);
2411
+ } else {
2412
+ return Promise.reject(error);
2192
2413
  }
2193
2414
  })
2194
2415
  );
@@ -2715,6 +2936,61 @@ var httpClient = class {
2715
2936
  }
2716
2937
  }, ctx.log);
2717
2938
  }
2939
+ uploadPdf(ctx, form, buildName) {
2940
+ return __async(this, null, function* () {
2941
+ form.append("projectToken", this.projectToken);
2942
+ if (ctx.build.name !== void 0 && ctx.build.name !== "") {
2943
+ form.append("buildName", buildName);
2944
+ }
2945
+ try {
2946
+ const response = yield this.axiosInstance.request({
2947
+ url: ctx.env.SMARTUI_UPLOAD_URL + "/pdf/upload",
2948
+ method: "POST",
2949
+ headers: form.getHeaders(),
2950
+ data: form
2951
+ });
2952
+ ctx.log.debug(`http response: ${JSON.stringify({
2953
+ status: response.status,
2954
+ headers: response.headers,
2955
+ body: response.data
2956
+ })}`);
2957
+ return response.data;
2958
+ } catch (error) {
2959
+ this.handleHttpError(error, ctx.log);
2960
+ }
2961
+ });
2962
+ }
2963
+ fetchPdfResults(ctx) {
2964
+ return __async(this, null, function* () {
2965
+ const params = {};
2966
+ if (ctx.build.projectId) {
2967
+ params.project_id = ctx.build.projectId;
2968
+ } else {
2969
+ throw new Error("Project ID not found to fetch PDF results");
2970
+ }
2971
+ params.build_id = ctx.build.id;
2972
+ const auth = Buffer.from(`${this.username}:${this.accessKey}`).toString("base64");
2973
+ try {
2974
+ const response = yield axios__default.default.request({
2975
+ url: ctx.env.SMARTUI_UPLOAD_URL + "/smartui/2.0/build/screenshots",
2976
+ method: "GET",
2977
+ params,
2978
+ headers: {
2979
+ "accept": "application/json",
2980
+ "Authorization": `Basic ${auth}`
2981
+ }
2982
+ });
2983
+ ctx.log.debug(`http response: ${JSON.stringify({
2984
+ status: response.status,
2985
+ headers: response.headers,
2986
+ body: response.data
2987
+ })}`);
2988
+ return response.data;
2989
+ } catch (error) {
2990
+ this.handleHttpError(error, ctx.log);
2991
+ }
2992
+ });
2993
+ }
2718
2994
  };
2719
2995
  var ctx_default = (options) => {
2720
2996
  var _a, _b, _c, _d, _e, _f, _g, _h, _i, _j;
@@ -2889,10 +3165,10 @@ var ctx_default = (options) => {
2889
3165
  mergeByBuild: false
2890
3166
  };
2891
3167
  };
2892
- function executeCommand(command10) {
3168
+ function executeCommand(command11) {
2893
3169
  let dst = process.cwd();
2894
3170
  try {
2895
- return child_process.execSync(command10, {
3171
+ return child_process.execSync(command11, {
2896
3172
  cwd: dst,
2897
3173
  stdio: ["ignore"],
2898
3174
  encoding: "utf-8"
@@ -2901,11 +3177,12 @@ function executeCommand(command10) {
2901
3177
  throw new Error(error.message);
2902
3178
  }
2903
3179
  }
2904
- function isGitRepo() {
3180
+ function isGitRepo(ctx) {
2905
3181
  try {
2906
3182
  executeCommand("git status");
2907
3183
  return true;
2908
3184
  } catch (error) {
3185
+ setNonGitInfo(ctx);
2909
3186
  return false;
2910
3187
  }
2911
3188
  }
@@ -2937,8 +3214,8 @@ var git_default = (ctx) => {
2937
3214
  } else {
2938
3215
  const splitCharacter = "<##>";
2939
3216
  const prettyFormat = ["%h", "%H", "%s", "%f", "%b", "%at", "%ct", "%an", "%ae", "%cn", "%ce", "%N", ""];
2940
- const command10 = 'git log -1 --pretty=format:"' + prettyFormat.join(splitCharacter) + '" && git rev-parse --abbrev-ref HEAD && git tag --contains HEAD';
2941
- let res = executeCommand(command10).split(splitCharacter);
3217
+ const command11 = 'git log -1 --pretty=format:"' + prettyFormat.join(splitCharacter) + '" && git rev-parse --abbrev-ref HEAD && git tag --contains HEAD';
3218
+ let res = executeCommand(command11).split(splitCharacter);
2942
3219
  var branchAndTags = res[res.length - 1].split("\n").filter((n) => n);
2943
3220
  var branch = ctx.env.CURRENT_BRANCH || branchAndTags[0];
2944
3221
  branchAndTags.slice(1);
@@ -2956,11 +3233,30 @@ var git_default = (ctx) => {
2956
3233
  };
2957
3234
  }
2958
3235
  };
3236
+ function setNonGitInfo(ctx) {
3237
+ let branch = ctx.env.CURRENT_BRANCH || "unknown-branch";
3238
+ if (ctx.options.markBaseline) {
3239
+ ctx.env.BASELINE_BRANCH = branch;
3240
+ ctx.env.SMART_GIT = false;
3241
+ }
3242
+ let githubURL;
3243
+ if (ctx.options.githubURL && ctx.options.githubURL.startsWith("https://")) {
3244
+ githubURL = ctx.options.githubURL;
3245
+ }
3246
+ ctx.git = {
3247
+ branch,
3248
+ commitId: "-",
3249
+ commitAuthor: "-",
3250
+ commitMessage: "-",
3251
+ githubURL: githubURL ? githubURL : "",
3252
+ baselineBranch: ctx.options.baselineBranch || ctx.env.BASELINE_BRANCH || ""
3253
+ };
3254
+ }
2959
3255
  var getGitInfo_default = (ctx) => {
2960
3256
  return {
2961
3257
  title: `Fetching git repo details`,
2962
3258
  skip: (ctx2) => {
2963
- return !isGitRepo() && !ctx2.env.SMARTUI_GIT_INFO_FILEPATH ? "[SKIPPED] Fetching git repo details; not a git repo" : "";
3259
+ return !isGitRepo(ctx2) && !ctx2.env.SMARTUI_GIT_INFO_FILEPATH ? "[SKIPPED] Fetching git repo details; not a git repo" : "";
2964
3260
  },
2965
3261
  task: (ctx2, task) => __async(void 0, null, function* () {
2966
3262
  if (ctx2.env.CURRENT_BRANCH && ctx2.env.CURRENT_BRANCH.trim() === "") {
@@ -3168,7 +3464,7 @@ var finalizeBuild_default = (ctx) => {
3168
3464
  task.output = chalk__default.default.gray(error.message);
3169
3465
  throw new Error("Finalize build failed");
3170
3466
  }
3171
- let buildUrls = `build url: ${ctx2.build.url}
3467
+ let buildUrls2 = `build url: ${ctx2.build.url}
3172
3468
  `;
3173
3469
  if (pingIntervalId !== null) {
3174
3470
  clearInterval(pingIntervalId);
@@ -3198,14 +3494,14 @@ var finalizeBuild_default = (ctx) => {
3198
3494
  yield ctx2.client.finalizeBuildForCapsWithToken(buildId, totalSnapshots, projectToken, ctx2.log);
3199
3495
  }
3200
3496
  if (testId && buildId) {
3201
- buildUrls += `TestId ${testId}: ${sessionBuildUrl}
3497
+ buildUrls2 += `TestId ${testId}: ${sessionBuildUrl}
3202
3498
  `;
3203
3499
  }
3204
3500
  } catch (error) {
3205
3501
  ctx2.log.debug(`Error finalizing build for session ${sessionId}: ${error.message}`);
3206
3502
  }
3207
3503
  }
3208
- task.output = chalk__default.default.gray(buildUrls);
3504
+ task.output = chalk__default.default.gray(buildUrls2);
3209
3505
  task.title = "Finalized build";
3210
3506
  try {
3211
3507
  yield (_a = ctx2.browser) == null ? void 0 : _a.close();
@@ -3344,6 +3640,9 @@ function prepareSnapshot(snapshot, ctx) {
3344
3640
  case "cssSelector":
3345
3641
  selectors.push(...value);
3346
3642
  break;
3643
+ case "coordinates":
3644
+ selectors.push(...value.map((e) => `coordinates=${e}`));
3645
+ break;
3347
3646
  }
3348
3647
  }
3349
3648
  }
@@ -3686,6 +3985,9 @@ function processSnapshot(snapshot, ctx) {
3686
3985
  case "cssSelector":
3687
3986
  selectors.push(...value);
3688
3987
  break;
3988
+ case "coordinates":
3989
+ selectors.push(...value.map((e) => `coordinates=${e}`));
3990
+ break;
3689
3991
  }
3690
3992
  }
3691
3993
  }
@@ -3823,6 +4125,31 @@ function processSnapshot(snapshot, ctx) {
3823
4125
  if (!Array.isArray(processedOptions[ignoreOrSelectBoxes][viewportString]))
3824
4126
  processedOptions[ignoreOrSelectBoxes][viewportString] = [];
3825
4127
  for (const selector of selectors) {
4128
+ if (selector.startsWith("coordinates=")) {
4129
+ const coordString = selector.replace("coordinates=", "");
4130
+ let pageHeight = height;
4131
+ if (viewport.height) {
4132
+ pageHeight = viewport.height;
4133
+ }
4134
+ const validation = validateCoordinates(
4135
+ coordString,
4136
+ pageHeight,
4137
+ viewport.width,
4138
+ snapshot.name
4139
+ );
4140
+ if (!validation.valid) {
4141
+ optionWarnings.add(validation.error);
4142
+ continue;
4143
+ }
4144
+ if (renderViewports.length > 1) {
4145
+ optionWarnings.add(`for snapshot ${snapshot.name} viewport ${viewportString}, coordinates may not be accurate for multiple viewports`);
4146
+ }
4147
+ const coordinateElement = __spreadValues({
4148
+ type: "coordinates"
4149
+ }, validation.coords);
4150
+ locators.push(coordinateElement);
4151
+ continue;
4152
+ }
3826
4153
  let l = yield page.locator(selector).all();
3827
4154
  if (l.length === 0) {
3828
4155
  optionWarnings.add(`for snapshot ${snapshot.name} viewport ${viewportString}, no element found for selector ${selector}`);
@@ -3831,6 +4158,17 @@ function processSnapshot(snapshot, ctx) {
3831
4158
  locators.push(...l);
3832
4159
  }
3833
4160
  for (const locator of locators) {
4161
+ if (locator && typeof locator === "object" && locator.hasOwnProperty("type") && locator.type === "coordinates") {
4162
+ const coordLocator = locator;
4163
+ const { top, bottom, left, right } = coordLocator;
4164
+ processedOptions[ignoreOrSelectBoxes][viewportString].push({
4165
+ left,
4166
+ top,
4167
+ right,
4168
+ bottom
4169
+ });
4170
+ continue;
4171
+ }
3834
4172
  let bb = yield locator.boundingBox();
3835
4173
  if (bb) {
3836
4174
  const top = bb.y;
@@ -4124,7 +4462,7 @@ var Queue = class {
4124
4462
  try {
4125
4463
  this.processingSnapshot = snapshot == null ? void 0 : snapshot.name;
4126
4464
  let drop = false;
4127
- if (this.ctx.isStartExec && !this.ctx.config.tunnel) {
4465
+ if (this.ctx.isStartExec) {
4128
4466
  this.ctx.log.info(`Processing Snapshot: ${snapshot == null ? void 0 : snapshot.name}`);
4129
4467
  }
4130
4468
  if (!this.ctx.config.delayedUpload && snapshot && snapshot.name && this.snapshotNames.includes(snapshot.name) && !this.ctx.config.allowDuplicateSnapshotNames) {
@@ -4319,15 +4657,15 @@ var startTunnel_default = (ctx) => {
4319
4657
 
4320
4658
  // src/commander/exec.ts
4321
4659
  var command = new commander.Command();
4322
- 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").option("--buildName <string>", "Specify the build name").option("--scheduled <string>", "Specify the schedule ID").option("--userName <string>", "Specify the LT username").option("--accessKey <string>", "Specify the LT accesskey").action(function(execCommand, _, command10) {
4660
+ 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").option("--buildName <string>", "Specify the build name").option("--scheduled <string>", "Specify the schedule ID").option("--userName <string>", "Specify the LT username").option("--accessKey <string>", "Specify the LT accesskey").action(function(execCommand, _, command11) {
4323
4661
  return __async(this, null, function* () {
4324
4662
  var _a;
4325
- const options = command10.optsWithGlobals();
4663
+ const options = command11.optsWithGlobals();
4326
4664
  if (options.buildName === "") {
4327
4665
  console.log(`Error: The '--buildName' option cannot be an empty string.`);
4328
4666
  process.exit(1);
4329
4667
  }
4330
- let ctx = ctx_default(command10.optsWithGlobals());
4668
+ let ctx = ctx_default(command11.optsWithGlobals());
4331
4669
  if (!which__default.default.sync(execCommand[0], { nothrow: true })) {
4332
4670
  ctx.log.error(`Error: Command not found "${execCommand[0]}"`);
4333
4671
  return;
@@ -4943,15 +5281,15 @@ var captureScreenshots_default = (ctx) => {
4943
5281
 
4944
5282
  // src/commander/capture.ts
4945
5283
  var command2 = new commander.Command();
4946
- 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").option("--buildName <string>", "Specify the build name").option("--scheduled <string>", "Specify the schedule ID").option("--userName <string>", "Specify the LT username").option("--accessKey <string>", "Specify the LT accesskey").action(function(file, _, command10) {
5284
+ 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").option("--buildName <string>", "Specify the build name").option("--scheduled <string>", "Specify the schedule ID").option("--userName <string>", "Specify the LT username").option("--accessKey <string>", "Specify the LT accesskey").action(function(file, _, command11) {
4947
5285
  return __async(this, null, function* () {
4948
5286
  var _a, _b;
4949
- const options = command10.optsWithGlobals();
5287
+ const options = command11.optsWithGlobals();
4950
5288
  if (options.buildName === "") {
4951
5289
  console.log(`Error: The '--buildName' option cannot be an empty string.`);
4952
5290
  process.exit(1);
4953
5291
  }
4954
- let ctx = ctx_default(command10.optsWithGlobals());
5292
+ let ctx = ctx_default(command11.optsWithGlobals());
4955
5293
  ctx.isSnapshotCaptured = true;
4956
5294
  if (!fs5__default.default.existsSync(file)) {
4957
5295
  ctx.log.error(`Web Static Config file ${file} not found.`);
@@ -5039,14 +5377,14 @@ command3.name("upload").description("Upload screenshots from given directory").a
5039
5377
  return val.split(",").map((ext) => ext.trim().toLowerCase());
5040
5378
  }).option("-E, --removeExtensions", "Strips file extensions from snapshot names").option("-i, --ignoreDir <patterns>", "Comma-separated list of directories to ignore", (val) => {
5041
5379
  return val.split(",").map((pattern) => pattern.trim());
5042
- }).option("--fetch-results [filename]", "Fetch results and optionally specify an output file, e.g., <filename>.json").option("--buildName <string>", "Specify the build name").option("--userName <string>", "Specify the LT username").option("--accessKey <string>", "Specify the LT accesskey").action(function(directory, _, command10) {
5380
+ }).option("--fetch-results [filename]", "Fetch results and optionally specify an output file, e.g., <filename>.json").option("--buildName <string>", "Specify the build name").option("--userName <string>", "Specify the LT username").option("--accessKey <string>", "Specify the LT accesskey").action(function(directory, _, command11) {
5043
5381
  return __async(this, null, function* () {
5044
- const options = command10.optsWithGlobals();
5382
+ const options = command11.optsWithGlobals();
5045
5383
  if (options.buildName === "") {
5046
5384
  console.log(`Error: The '--buildName' option cannot be an empty string.`);
5047
5385
  process.exit(1);
5048
5386
  }
5049
- let ctx = ctx_default(command10.optsWithGlobals());
5387
+ let ctx = ctx_default(command11.optsWithGlobals());
5050
5388
  ctx.isSnapshotCaptured = true;
5051
5389
  if (!fs5__default.default.existsSync(directory)) {
5052
5390
  console.log(`Error: The provided directory ${directory} not found.`);
@@ -5324,10 +5662,10 @@ var uploadAppFigma_default2 = (ctx) => {
5324
5662
  var uploadFigma = new commander.Command();
5325
5663
  var uploadWebFigmaCommand = new commander.Command();
5326
5664
  var uploadAppFigmaCommand = new commander.Command();
5327
- uploadFigma.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, _, command10) {
5665
+ uploadFigma.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, _, command11) {
5328
5666
  return __async(this, null, function* () {
5329
5667
  var _a, _b;
5330
- let ctx = ctx_default(command10.optsWithGlobals());
5668
+ let ctx = ctx_default(command11.optsWithGlobals());
5331
5669
  ctx.isSnapshotCaptured = true;
5332
5670
  if (!fs5__default.default.existsSync(file)) {
5333
5671
  console.log(`Error: Figma Config file ${file} not found.`);
@@ -5366,10 +5704,10 @@ uploadFigma.name("upload-figma").description("Capture screenshots of static site
5366
5704
  }
5367
5705
  });
5368
5706
  });
5369
- uploadWebFigmaCommand.name("upload-figma-web").description("Capture figma screenshots into CLI build").argument("<file>", "figma config config file").option("--markBaseline", "Mark the uploaded images as baseline").option("--buildName <buildName>", "Name of the build").option("--fetch-results [filename]", "Fetch results and optionally specify an output file, e.g., <filename>.json").action(function(file, _, command10) {
5707
+ uploadWebFigmaCommand.name("upload-figma-web").description("Capture figma screenshots into CLI build").argument("<file>", "figma config config file").option("--markBaseline", "Mark the uploaded images as baseline").option("--buildName <buildName>", "Name of the build").option("--fetch-results [filename]", "Fetch results and optionally specify an output file, e.g., <filename>.json").action(function(file, _, command11) {
5370
5708
  return __async(this, null, function* () {
5371
5709
  var _a;
5372
- let ctx = ctx_default(command10.optsWithGlobals());
5710
+ let ctx = ctx_default(command11.optsWithGlobals());
5373
5711
  if (!fs5__default.default.existsSync(file)) {
5374
5712
  console.log(`Error: figma-web config file ${file} not found.`);
5375
5713
  return;
@@ -5419,10 +5757,10 @@ uploadWebFigmaCommand.name("upload-figma-web").description("Capture figma screen
5419
5757
  }
5420
5758
  });
5421
5759
  });
5422
- uploadAppFigmaCommand.name("upload-figma-app").description("Capture figma screenshots into App Build").argument("<file>", "figma config config file").option("--markBaseline", "Mark the uploaded images as baseline").option("--buildName <buildName>", "Name of the build").option("--fetch-results [filename]", "Fetch results and optionally specify an output file, e.g., <filename>.json").action(function(file, _, command10) {
5760
+ uploadAppFigmaCommand.name("upload-figma-app").description("Capture figma screenshots into App Build").argument("<file>", "figma config config file").option("--markBaseline", "Mark the uploaded images as baseline").option("--buildName <buildName>", "Name of the build").option("--fetch-results [filename]", "Fetch results and optionally specify an output file, e.g., <filename>.json").action(function(file, _, command11) {
5423
5761
  return __async(this, null, function* () {
5424
5762
  var _a;
5425
- let ctx = ctx_default(command10.optsWithGlobals());
5763
+ let ctx = ctx_default(command11.optsWithGlobals());
5426
5764
  if (!fs5__default.default.existsSync(file)) {
5427
5765
  console.log(`Error: figma-app config file ${file} not found.`);
5428
5766
  return;
@@ -5486,10 +5824,10 @@ command4.name("exec:start").description("Start SmartUI server").option("-P, --po
5486
5824
  ctx.isStartExec = true;
5487
5825
  let tasks = new listr2.Listr(
5488
5826
  [
5489
- auth_default(),
5827
+ authExec_default(),
5490
5828
  startServer_default(),
5491
5829
  getGitInfo_default(),
5492
- createBuild_default()
5830
+ createBuildExec_default()
5493
5831
  ],
5494
5832
  {
5495
5833
  rendererOptions: {
@@ -5504,8 +5842,10 @@ command4.name("exec:start").description("Start SmartUI server").option("-P, --po
5504
5842
  );
5505
5843
  try {
5506
5844
  yield tasks.run(ctx);
5507
- startPingPolling(ctx);
5508
- if (ctx.options.fetchResults) {
5845
+ if (ctx.build && ctx.build.id) {
5846
+ startPingPolling(ctx);
5847
+ }
5848
+ if (ctx.options.fetchResults && ctx.build && ctx.build.id) {
5509
5849
  startPolling(ctx, "", false, "");
5510
5850
  }
5511
5851
  } catch (error) {
@@ -5866,10 +6206,104 @@ command9.name("exec:pingTest").description("Ping the SmartUI server to check if
5866
6206
  });
5867
6207
  });
5868
6208
  var pingTest_default = command9;
6209
+ var uploadPdfs_default = (ctx) => {
6210
+ return {
6211
+ title: "Uploading PDFs",
6212
+ task: (ctx2, task) => __async(void 0, null, function* () {
6213
+ try {
6214
+ ctx2.task = task;
6215
+ updateLogContext({ task: "upload-pdf" });
6216
+ yield uploadPdfs(ctx2, ctx2.uploadFilePath);
6217
+ task.title = "PDFs uploaded successfully";
6218
+ } catch (error) {
6219
+ ctx2.log.debug(error);
6220
+ task.output = chalk__default.default.red(`${error.message}`);
6221
+ throw new Error("PDF upload failed");
6222
+ }
6223
+ }),
6224
+ rendererOptions: { persistentOutput: true },
6225
+ exitOnError: false
6226
+ };
6227
+ };
6228
+ function uploadPdfs(ctx, pdfPath) {
6229
+ return __async(this, null, function* () {
6230
+ const formData = new FormData__default.default();
6231
+ if (pdfPath.endsWith(".pdf")) {
6232
+ formData.append("pathToFiles", fs5__default.default.createReadStream(pdfPath));
6233
+ } else {
6234
+ const files = fs5__default.default.readdirSync(pdfPath);
6235
+ const pdfFiles = files.filter((file) => file.endsWith(".pdf"));
6236
+ pdfFiles.forEach((pdf) => {
6237
+ const filePath = path2__default.default.join(pdfPath, pdf);
6238
+ formData.append("pathToFiles", fs5__default.default.createReadStream(filePath));
6239
+ });
6240
+ }
6241
+ const buildName = ctx.options.buildName;
6242
+ if (buildName) {
6243
+ ctx.build.name = buildName;
6244
+ }
6245
+ try {
6246
+ const response = yield ctx.client.uploadPdf(ctx, formData, buildName);
6247
+ if (response && response.buildId) {
6248
+ ctx.build.id = response.buildId;
6249
+ ctx.log.debug(`PDF upload successful. Build ID: ${ctx.build.id}`);
6250
+ }
6251
+ if (response && response.projectId) {
6252
+ ctx.build.projectId = response.projectId;
6253
+ }
6254
+ } catch (error) {
6255
+ throw new Error(error.message);
6256
+ }
6257
+ });
6258
+ }
6259
+
6260
+ // src/commander/uploadPdf.ts
6261
+ var command10 = new commander.Command();
6262
+ command10.name("upload-pdf").description("Upload PDFs for visual comparison").argument("<directory>", "Path of the directory containing PDFs").option("--fetch-results [filename]", "Fetch results and optionally specify an output file, e.g., <filename>.json").option("--buildName <string>", "Specify the build name").action(function(directory, _, command11) {
6263
+ return __async(this, null, function* () {
6264
+ const options = command11.optsWithGlobals();
6265
+ if (options.buildName === "") {
6266
+ console.log(`Error: The '--buildName' option cannot be an empty string.`);
6267
+ process.exit(1);
6268
+ }
6269
+ let ctx = ctx_default(command11.optsWithGlobals());
6270
+ if (!fs5__default.default.existsSync(directory)) {
6271
+ console.log(`Error: The provided directory ${directory} not found.`);
6272
+ process.exit(1);
6273
+ }
6274
+ ctx.uploadFilePath = directory;
6275
+ let tasks = new listr2.Listr(
6276
+ [
6277
+ auth_default(),
6278
+ uploadPdfs_default()
6279
+ ],
6280
+ {
6281
+ rendererOptions: {
6282
+ icon: {
6283
+ [listr2.ListrDefaultRendererLogLevels.OUTPUT]: `\u2192`
6284
+ },
6285
+ color: {
6286
+ [listr2.ListrDefaultRendererLogLevels.OUTPUT]: listr2.color.gray
6287
+ }
6288
+ }
6289
+ }
6290
+ );
6291
+ try {
6292
+ yield tasks.run(ctx);
6293
+ if (ctx.options.fetchResults) {
6294
+ startPdfPolling(ctx);
6295
+ }
6296
+ } catch (error) {
6297
+ console.log("\nRefer docs: https://www.lambdatest.com/support/docs/smart-visual-regression-testing/");
6298
+ process.exit(1);
6299
+ }
6300
+ });
6301
+ });
6302
+ var uploadPdf_default = command10;
5869
6303
 
5870
6304
  // src/commander/commander.ts
5871
6305
  var program2 = new commander.Command();
5872
- program2.name("smartui").description("CLI to help you run your SmartUI tests on LambdaTest platform").version(`v${version}`).option("-c --config <filepath>", "Config file path").option("--markBaseline", "Mark this build baseline").option("--baselineBranch <string>", "Mark this build baseline").option("--baselineBuild <string>", "Mark this build baseline").option("--githubURL <string>", "GitHub URL including commitId").addCommand(exec_default2).addCommand(capture_default).addCommand(configWeb).addCommand(configStatic).addCommand(upload_default).addCommand(server_default2).addCommand(stopServer_default).addCommand(merge_default).addCommand(ping_default).addCommand(configFigma).addCommand(uploadFigma).addCommand(configWebFigma).addCommand(configAppFigma).addCommand(uploadWebFigmaCommand).addCommand(uploadAppFigmaCommand).addCommand(pingTest_default);
6306
+ program2.name("smartui").description("CLI to help you run your SmartUI tests on LambdaTest platform").version(`v${version}`).option("-c --config <filepath>", "Config file path").option("--markBaseline", "Mark this build baseline").option("--baselineBranch <string>", "Mark this build baseline").option("--baselineBuild <string>", "Mark this build baseline").option("--githubURL <string>", "GitHub URL including commitId").addCommand(exec_default2).addCommand(capture_default).addCommand(configWeb).addCommand(configStatic).addCommand(upload_default).addCommand(server_default2).addCommand(stopServer_default).addCommand(merge_default).addCommand(ping_default).addCommand(configFigma).addCommand(uploadFigma).addCommand(configWebFigma).addCommand(configAppFigma).addCommand(uploadWebFigmaCommand).addCommand(uploadAppFigmaCommand).addCommand(pingTest_default).addCommand(uploadPdf_default);
5873
6307
  var commander_default = program2;
5874
6308
  (function() {
5875
6309
  return __async(this, null, function* () {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@lambdatest/smartui-cli",
3
- "version": "4.1.28",
3
+ "version": "4.1.30",
4
4
  "description": "A command line interface (CLI) to run SmartUI tests on LambdaTest",
5
5
  "files": [
6
6
  "dist/**/*"