@lambdatest/smartui-cli 4.1.32 → 4.1.34

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 +242 -55
  2. package/package.json +2 -1
package/dist/index.cjs CHANGED
@@ -61,6 +61,7 @@ var sharp__default = /*#__PURE__*/_interopDefault(sharp);
61
61
  var http__namespace = /*#__PURE__*/_interopNamespace(http);
62
62
 
63
63
  var __defProp = Object.defineProperty;
64
+ var __getOwnPropNames = Object.getOwnPropertyNames;
64
65
  var __getOwnPropSymbols = Object.getOwnPropertySymbols;
65
66
  var __hasOwnProp = Object.prototype.hasOwnProperty;
66
67
  var __propIsEnum = Object.prototype.propertyIsEnumerable;
@@ -83,6 +84,9 @@ var __require = /* @__PURE__ */ ((x) => typeof require !== "undefined" ? require
83
84
  return require.apply(this, arguments);
84
85
  throw Error('Dynamic require of "' + x + '" is not supported');
85
86
  });
87
+ var __commonJS = (cb, mod) => function __require2() {
88
+ return mod || (0, cb[__getOwnPropNames(cb)[0]])((mod = { exports: {} }).exports, mod), mod.exports;
89
+ };
86
90
  var __async = (__this, __arguments, generator) => {
87
91
  return new Promise((resolve, reject) => {
88
92
  var fulfilled = (value) => {
@@ -104,6 +108,73 @@ var __async = (__this, __arguments, generator) => {
104
108
  });
105
109
  };
106
110
 
111
+ // node_modules/.pnpm/find-free-port@2.0.0/node_modules/find-free-port/index.js
112
+ var require_find_free_port = __commonJS({
113
+ "node_modules/.pnpm/find-free-port@2.0.0/node_modules/find-free-port/index.js"(exports, module) {
114
+ var net = __require("net");
115
+ function findFreePort(beg, ...rest) {
116
+ const p = rest.slice(0, rest.length - 1), cb = rest[rest.length - 1];
117
+ let [end, ip, cnt] = Array.from(p);
118
+ if (!ip && end && !/^\d+$/.test(end)) {
119
+ ip = end;
120
+ end = 65534;
121
+ } else {
122
+ if (end == null) {
123
+ end = 65534;
124
+ }
125
+ }
126
+ if (cnt == null) {
127
+ cnt = 1;
128
+ }
129
+ const retcb = cb;
130
+ const res = [];
131
+ const probe = function(ip2, port, cb2) {
132
+ const s = net.createConnection({ port, host: ip2 });
133
+ s.on("connect", function() {
134
+ s.end();
135
+ cb2(null, port + 1);
136
+ });
137
+ s.on("error", (err) => {
138
+ cb2(port);
139
+ });
140
+ };
141
+ var onprobe = function(port, nextPort) {
142
+ if (port) {
143
+ res.push(port);
144
+ if (res.length >= cnt) {
145
+ retcb(null, ...res);
146
+ } else {
147
+ setImmediate(() => probe(ip, port + 1, onprobe));
148
+ }
149
+ } else {
150
+ if (nextPort >= end) {
151
+ retcb(new Error("No available ports"));
152
+ } else {
153
+ setImmediate(() => probe(ip, nextPort, onprobe));
154
+ }
155
+ }
156
+ };
157
+ return probe(ip, beg, onprobe);
158
+ }
159
+ function findFreePortPmfy(beg, ...rest) {
160
+ const last = rest[rest.length - 1];
161
+ if (typeof last === "function") {
162
+ findFreePort(beg, ...rest);
163
+ } else {
164
+ return new Promise((resolve, reject) => {
165
+ findFreePort(beg, ...rest, (err, ...ports) => {
166
+ if (err)
167
+ reject(err);
168
+ else
169
+ resolve(ports);
170
+ });
171
+ });
172
+ }
173
+ }
174
+ module.exports = findFreePortPmfy;
175
+ }
176
+ });
177
+
107
178
  // src/lib/constants.ts
108
179
  var constants_default = {
109
180
  // default configs
@@ -152,6 +223,8 @@ var constants_default = {
152
223
  EDGE: "edge",
153
224
  EDGE_CHANNEL: "msedge",
154
225
  WEBKIT: "webkit",
226
+ MIN_PORT_RANGE: 49100,
227
+ MAX_PORT_RANGE: 6e4,
155
228
  // discovery browser launch arguments
156
229
  LAUNCH_ARGS: [
157
230
  // disable the translate popup and optimization downloads
@@ -785,6 +858,10 @@ var ConfigSchema = {
785
858
  type: "boolean",
786
859
  errorMessage: "Invalid config; useLambdaInternal must be true/false"
787
860
  },
861
+ useRemoteDiscovery: {
862
+ type: "boolean",
863
+ errorMessage: "Invalid config; useRemoteDiscovery must be true/false"
864
+ },
788
865
  useExtendedViewport: {
789
866
  type: "boolean",
790
867
  errorMessage: "Invalid config; useExtendedViewport must be true/false"
@@ -853,6 +930,18 @@ var WebStaticConfigSchema = {
853
930
  type: "string",
854
931
  enum: ["load", "domcontentloaded"],
855
932
  errorMessage: "pageEvent can be load, domcontentloaded"
933
+ },
934
+ requestHeaders: {
935
+ type: "array",
936
+ items: {
937
+ type: "object",
938
+ minProperties: 1,
939
+ additionalProperties: { type: "string" }
940
+ },
941
+ uniqueItems: true,
942
+ errorMessage: {
943
+ uniqueItems: "Invalid config; duplicates in requestHeaders"
944
+ }
856
945
  }
857
946
  },
858
947
  required: ["name", "url"],
@@ -1959,7 +2048,32 @@ function validateCoordinates(coordString, pageHeight, pageWidth, snapshotName) {
1959
2048
  }
1960
2049
 
1961
2050
  // src/lib/server.ts
2051
+ var fp = require_find_free_port();
1962
2052
  var uploadDomToS3ViaEnv = process.env.USE_LAMBDA_INTERNAL || false;
2053
+ function findAvailablePort(server, startPort, log2) {
2054
+ return __async(this, null, function* () {
2055
+ let currentPort = startPort;
2056
+ try {
2057
+ yield server.listen({ port: currentPort });
2058
+ return currentPort;
2059
+ } catch (error) {
2060
+ if (error.code === "EADDRINUSE") {
2061
+ log2.debug(`Port ${currentPort} is in use, finding available port in range 49100-60000`);
2062
+ const availablePorts = yield fp(constants_default.MIN_PORT_RANGE, constants_default.MAX_PORT_RANGE);
2063
+ if (availablePorts.length > 0) {
2064
+ const freePort = availablePorts[0];
2065
+ yield server.listen({ port: freePort });
2066
+ log2.debug(`Found and started server on port ${freePort}`);
2067
+ return freePort;
2068
+ } else {
2069
+ throw new Error("No available ports found in range 49100-60000");
2070
+ }
2071
+ } else {
2072
+ throw error;
2073
+ }
2074
+ }
2075
+ });
2076
+ }
1963
2077
  var server_default = (ctx) => __async(void 0, null, function* () {
1964
2078
  const server = fastify__default.default({
1965
2079
  logger: {
@@ -2209,10 +2323,18 @@ var server_default = (ctx) => __async(void 0, null, function* () {
2209
2323
  return reply.code(replyCode).send(replyBody);
2210
2324
  }
2211
2325
  }));
2212
- yield server.listen({ port: ctx.options.port });
2213
- let { port } = server.addresses()[0];
2214
- process.env.SMARTUI_SERVER_ADDRESS = `http://localhost:${port}`;
2215
- process.env.CYPRESS_SMARTUI_SERVER_ADDRESS = `http://localhost:${port}`;
2326
+ if (ctx.sourceCommand && ctx.sourceCommand === "exec-start") {
2327
+ yield server.listen({ port: ctx.options.port });
2328
+ let { port } = server.addresses()[0];
2329
+ process.env.SMARTUI_SERVER_ADDRESS = `http://localhost:${port}`;
2330
+ process.env.CYPRESS_SMARTUI_SERVER_ADDRESS = `http://localhost:${port}`;
2331
+ ctx.log.debug(`Server started successfully on port ${port}`);
2332
+ } else {
2333
+ const actualPort = yield findAvailablePort(server, ctx.options.port, ctx.log);
2334
+ process.env.SMARTUI_SERVER_ADDRESS = `http://localhost:${actualPort}`;
2335
+ process.env.CYPRESS_SMARTUI_SERVER_ADDRESS = `http://localhost:${actualPort}`;
2336
+ ctx.log.debug(`Server started successfully on port ${actualPort}`);
2337
+ }
2216
2338
  return server;
2217
2339
  });
2218
2340
 
@@ -2357,7 +2479,7 @@ var authExec_default = (ctx) => {
2357
2479
  };
2358
2480
 
2359
2481
  // package.json
2360
- var version = "4.1.32";
2482
+ var version = "4.1.34";
2361
2483
  var package_default = {
2362
2484
  name: "@lambdatest/smartui-cli",
2363
2485
  version,
@@ -2413,6 +2535,7 @@ var package_default = {
2413
2535
  "simple-swizzle": "0.2.2"
2414
2536
  },
2415
2537
  devDependencies: {
2538
+ "find-free-port": "^2.0.0",
2416
2539
  typescript: "^5.3.2"
2417
2540
  }
2418
2541
  };
@@ -3098,6 +3221,7 @@ var ctx_default = (options) => {
3098
3221
  let buildNameObj;
3099
3222
  let allowDuplicateSnapshotNames = false;
3100
3223
  let useLambdaInternal = false;
3224
+ let useRemoteDiscovery = false;
3101
3225
  let useExtendedViewport = false;
3102
3226
  let loadDomContent = false;
3103
3227
  try {
@@ -3172,6 +3296,9 @@ var ctx_default = (options) => {
3172
3296
  if (config.useLambdaInternal) {
3173
3297
  useLambdaInternal = true;
3174
3298
  }
3299
+ if (config.useRemoteDiscovery) {
3300
+ useRemoteDiscovery = true;
3301
+ }
3175
3302
  if (config.useExtendedViewport) {
3176
3303
  useExtendedViewport = true;
3177
3304
  }
@@ -3207,6 +3334,7 @@ var ctx_default = (options) => {
3207
3334
  requestHeaders: config.requestHeaders || {},
3208
3335
  allowDuplicateSnapshotNames,
3209
3336
  useLambdaInternal,
3337
+ useRemoteDiscovery,
3210
3338
  useExtendedViewport,
3211
3339
  loadDomContent,
3212
3340
  approvalThreshold: config.approvalThreshold,
@@ -3256,6 +3384,7 @@ var ctx_default = (options) => {
3256
3384
  isSnapshotCaptured: false,
3257
3385
  sessionCapabilitiesMap: /* @__PURE__ */ new Map(),
3258
3386
  buildToSnapshotCountMap: /* @__PURE__ */ new Map(),
3387
+ sessionIdToSnapshotNameMap: /* @__PURE__ */ new Map(),
3259
3388
  fetchResultsForBuild: new Array(),
3260
3389
  orgId: 0,
3261
3390
  userId: 0,
@@ -3413,7 +3542,7 @@ var createBuildExec_default = (ctx) => {
3413
3542
  }
3414
3543
  task.output = chalk__default.default.gray(`build id: ${resp.data.buildId}`);
3415
3544
  task.title = "SmartUI build created";
3416
- if (ctx2.env.USE_REMOTE_DISCOVERY) {
3545
+ if (ctx2.env.USE_REMOTE_DISCOVERY || ctx2.config.useRemoteDiscovery) {
3417
3546
  task.output += chalk__default.default.gray(`
3418
3547
  Using remote discovery for this build`);
3419
3548
  }
@@ -4106,13 +4235,13 @@ function processSnapshot(snapshot, ctx) {
4106
4235
  for (const [key, value] of Object.entries(options[ignoreOrSelectDOM])) {
4107
4236
  switch (key) {
4108
4237
  case "id":
4109
- selectors.push(...value.map((e) => "#" + e));
4238
+ selectors.push(...value.map((e) => e.startsWith("#") ? e : "#" + e));
4110
4239
  break;
4111
4240
  case "class":
4112
- selectors.push(...value.map((e) => "." + e));
4241
+ selectors.push(...value.map((e) => e.startsWith(".") ? e : "." + e));
4113
4242
  break;
4114
4243
  case "xpath":
4115
- selectors.push(...value.map((e) => "xpath=" + e));
4244
+ selectors.push(...value.map((e) => e.startsWith("xpath=") ? e : "xpath=" + e));
4116
4245
  break;
4117
4246
  case "cssSelector":
4118
4247
  selectors.push(...value);
@@ -4227,14 +4356,7 @@ function processSnapshot(snapshot, ctx) {
4227
4356
  }
4228
4357
  }
4229
4358
  }
4230
- if (processedOptions.element) {
4231
- let l = yield page.locator(processedOptions.element).all();
4232
- if (l.length === 0) {
4233
- throw new Error(`for snapshot ${snapshot.name} viewport ${viewportString}, no element found for selector ${processedOptions.element}`);
4234
- } else if (l.length > 1) {
4235
- throw new Error(`for snapshot ${snapshot.name} viewport ${viewportString}, multiple elements found for selector ${processedOptions.element}`);
4236
- }
4237
- } else if (selectors.length) {
4359
+ if (selectors.length) {
4238
4360
  let height = 0;
4239
4361
  height = yield page.evaluate(() => {
4240
4362
  const DEFAULT_HEIGHT = 16384;
@@ -4259,7 +4381,6 @@ function processSnapshot(snapshot, ctx) {
4259
4381
  return Math.max(...measurements);
4260
4382
  });
4261
4383
  ctx.log.debug(`Calculated content height: ${height}`);
4262
- let locators = [];
4263
4384
  if (!Array.isArray(processedOptions[ignoreOrSelectBoxes][viewportString]))
4264
4385
  processedOptions[ignoreOrSelectBoxes][viewportString] = [];
4265
4386
  for (const selector of selectors) {
@@ -4282,44 +4403,75 @@ function processSnapshot(snapshot, ctx) {
4282
4403
  if (renderViewports.length > 1) {
4283
4404
  optionWarnings.add(`for snapshot ${snapshot.name} viewport ${viewportString}, coordinates may not be accurate for multiple viewports`);
4284
4405
  }
4285
- const coordinateElement = __spreadValues({
4406
+ __spreadValues({
4286
4407
  type: "coordinates"
4287
4408
  }, validation.coords);
4288
- locators.push(coordinateElement);
4289
4409
  continue;
4290
- }
4291
- let l = yield page.locator(selector).all();
4292
- if (l.length === 0) {
4293
- optionWarnings.add(`for snapshot ${snapshot.name} viewport ${viewportString}, no element found for selector ${selector}`);
4294
- continue;
4295
- }
4296
- locators.push(...l);
4297
- }
4298
- for (const locator of locators) {
4299
- if (locator && typeof locator === "object" && locator.hasOwnProperty("type") && locator.type === "coordinates") {
4300
- const coordLocator = locator;
4301
- const { top, bottom, left, right } = coordLocator;
4302
- processedOptions[ignoreOrSelectBoxes][viewportString].push({
4303
- left,
4304
- top,
4305
- right,
4306
- bottom
4307
- });
4308
- continue;
4309
- }
4310
- let bb = yield locator.boundingBox();
4311
- if (bb) {
4312
- const top = bb.y;
4313
- const bottom = bb.y + bb.height;
4314
- if (top <= height && bottom <= height) {
4315
- processedOptions[ignoreOrSelectBoxes][viewportString].push({
4316
- left: bb.x,
4317
- top,
4318
- right: bb.x + bb.width,
4319
- bottom
4320
- });
4410
+ } else {
4411
+ const isXPath = selector.startsWith("xpath=");
4412
+ const selectorValue = isXPath ? selector.substring(6) : selector;
4413
+ const boxes = yield page.evaluate(({ selectorValue: selectorValue2, isXPath: isXPath2 }) => {
4414
+ try {
4415
+ const DEFAULT_HEIGHT = 16384;
4416
+ const DEFAULT_WIDTH = 7680;
4417
+ const body = document.body;
4418
+ const html = document.documentElement;
4419
+ let pageHeight;
4420
+ let pageWidth;
4421
+ if (!body || !html) {
4422
+ pageHeight = DEFAULT_HEIGHT;
4423
+ pageWidth = DEFAULT_WIDTH;
4424
+ } else {
4425
+ const measurements = [
4426
+ (body == null ? void 0 : body.scrollHeight) || 0,
4427
+ (body == null ? void 0 : body.offsetHeight) || 0,
4428
+ (html == null ? void 0 : html.clientHeight) || 0,
4429
+ (html == null ? void 0 : html.scrollHeight) || 0,
4430
+ (html == null ? void 0 : html.offsetHeight) || 0
4431
+ ];
4432
+ const allMeasurementsInvalid = measurements.every((measurement) => !measurement);
4433
+ if (allMeasurementsInvalid) {
4434
+ pageHeight = DEFAULT_HEIGHT;
4435
+ } else {
4436
+ pageHeight = Math.max(...measurements);
4437
+ }
4438
+ const measurementsWidth = [
4439
+ (body == null ? void 0 : body.scrollWidth) || 0,
4440
+ (body == null ? void 0 : body.offsetWidth) || 0,
4441
+ (html == null ? void 0 : html.clientWidth) || 0,
4442
+ (html == null ? void 0 : html.scrollWidth) || 0,
4443
+ (html == null ? void 0 : html.offsetWidth) || 0
4444
+ ];
4445
+ const allMeasurementsInvalidWidth = measurementsWidth.every((measurement) => !measurement);
4446
+ if (allMeasurementsInvalidWidth) {
4447
+ pageWidth = DEFAULT_WIDTH;
4448
+ } else {
4449
+ pageWidth = Math.max(...measurementsWidth);
4450
+ }
4451
+ }
4452
+ let elements = [];
4453
+ if (isXPath2) {
4454
+ const xpathResult = document.evaluate(
4455
+ selectorValue2,
4456
+ document,
4457
+ null,
4458
+ XPathResult.ORDERED_NODE_SNAPSHOT_TYPE,
4459
+ null
4460
+ );
4461
+ for (let i = 0; i < xpathResult.snapshotLength; i++) {
4462
+ elements.push(xpathResult.snapshotItem(i));
4463
+ }
4464
+ } else {
4465
+ elements = Array.from(document.querySelectorAll(selectorValue2));
4466
+ }
4467
+ return elements;
4468
+ } catch (error) {
4469
+ }
4470
+ }, { selectorValue, isXPath });
4471
+ if (boxes && boxes.length >= 1) {
4472
+ processedOptions[ignoreOrSelectBoxes][viewportString].push(...boxes);
4321
4473
  } else {
4322
- ctx.log.debug(`Bounding box for selector skipped due to exceeding height: ${JSON.stringify({ top, bottom, height })}`);
4474
+ optionWarnings.add(`for snapshot ${snapshot.name} viewport ${viewportString}, no element found for selector ${selector}`);
4323
4475
  }
4324
4476
  }
4325
4477
  }
@@ -4604,8 +4756,24 @@ var Queue = class {
4604
4756
  this.ctx.log.info(`Processing Snapshot: ${snapshot == null ? void 0 : snapshot.name}`);
4605
4757
  }
4606
4758
  if (!this.ctx.config.delayedUpload && snapshot && snapshot.name && this.snapshotNames.includes(snapshot.name) && !this.ctx.config.allowDuplicateSnapshotNames) {
4607
- drop = true;
4608
- this.ctx.log.info(`Skipping duplicate SmartUI snapshot '${snapshot.name}'. To capture duplicate screenshots, please set the 'allowDuplicateSnapshotNames' or 'delayedUpload' configuration as true in your config file.`);
4759
+ if (this.ctx.sessionIdToSnapshotNameMap && snapshot.options && snapshot.options.sessionId) {
4760
+ if (this.ctx.sessionIdToSnapshotNameMap.has(snapshot.options.sessionId)) {
4761
+ console.log(`snapshot.options.sessionId`, snapshot.options.sessionId, `this.ctx.sessionIdToSnapshotNameMap`, JSON.stringify([...this.ctx.sessionIdToSnapshotNameMap]));
4762
+ const existingNames = this.ctx.sessionIdToSnapshotNameMap.get(snapshot.options.sessionId) || [];
4763
+ if (existingNames.includes(snapshot.name)) {
4764
+ drop = true;
4765
+ this.ctx.log.info(`Skipping123 duplicate SmartUI snapshot '${snapshot.name}'. To capture duplicate screenshots, please set the 'allowDuplicateSnapshotNames' or 'delayedUpload' configuration as true in your config file.`);
4766
+ } else {
4767
+ existingNames.push(snapshot.name);
4768
+ this.ctx.sessionIdToSnapshotNameMap.set(snapshot.options.sessionId, existingNames);
4769
+ }
4770
+ } else {
4771
+ this.ctx.sessionIdToSnapshotNameMap.set(snapshot.options.sessionId, [snapshot.name]);
4772
+ }
4773
+ } else {
4774
+ drop = true;
4775
+ this.ctx.log.info(`Skipping duplicate SmartUI snapshot '${snapshot.name}'. To capture duplicate screenshots, please set the 'allowDuplicateSnapshotNames' or 'delayedUpload' configuration as true in your config file.`);
4776
+ }
4609
4777
  }
4610
4778
  if (this.ctx.config.delayedUpload && snapshot && snapshot.name && this.snapshotNames.includes(snapshot.name)) {
4611
4779
  drop = this.filterExistingVariants(snapshot, this.ctx.config);
@@ -4632,7 +4800,7 @@ var Queue = class {
4632
4800
  }
4633
4801
  }
4634
4802
  let processedSnapshot, warnings, discoveryErrors;
4635
- if (this.ctx.env.USE_REMOTE_DISCOVERY) {
4803
+ if (this.ctx.env.USE_REMOTE_DISCOVERY || this.ctx.config.useRemoteDiscovery) {
4636
4804
  this.ctx.log.debug(`Using remote discovery`);
4637
4805
  let result = yield prepareSnapshot(snapshot, this.ctx);
4638
4806
  processedSnapshot = result.processedSnapshot;
@@ -5091,6 +5259,25 @@ function captureScreenshotsForConfig(ctx, browsers, urlConfig, browserName, rend
5091
5259
  const browser = browsers[browserName];
5092
5260
  context = yield browser == null ? void 0 : browser.newContext(contextOptions);
5093
5261
  page = yield context == null ? void 0 : context.newPage();
5262
+ const headersObject = {};
5263
+ if (ctx.config.requestHeaders && Array.isArray(ctx.config.requestHeaders)) {
5264
+ ctx.config.requestHeaders.forEach((headerObj) => {
5265
+ Object.entries(headerObj).forEach(([key, value]) => {
5266
+ headersObject[key] = value;
5267
+ });
5268
+ });
5269
+ }
5270
+ if (urlConfig.requestHeaders && Array.isArray(urlConfig.requestHeaders)) {
5271
+ urlConfig.requestHeaders.forEach((headerObj) => {
5272
+ Object.entries(headerObj).forEach(([key, value]) => {
5273
+ headersObject[key] = value;
5274
+ });
5275
+ });
5276
+ }
5277
+ ctx.log.debug(`Combined headers: ${JSON.stringify(headersObject)}`);
5278
+ if (Object.keys(headersObject).length > 0) {
5279
+ yield page.setExtraHTTPHeaders(headersObject);
5280
+ }
5094
5281
  yield page == null ? void 0 : page.goto(url.trim(), pageOptions);
5095
5282
  yield executeDocumentScripts(ctx, page, "afterNavigation", afterNavigationScript);
5096
5283
  for (let { viewport, viewportString, fullPage } of renderViewports) {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@lambdatest/smartui-cli",
3
- "version": "4.1.32",
3
+ "version": "4.1.34",
4
4
  "description": "A command line interface (CLI) to run SmartUI tests on LambdaTest",
5
5
  "files": [
6
6
  "dist/**/*"
@@ -47,6 +47,7 @@
47
47
  "simple-swizzle": "0.2.2"
48
48
  },
49
49
  "devDependencies": {
50
+ "find-free-port": "^2.0.0",
50
51
  "typescript": "^5.3.2"
51
52
  },
52
53
  "scripts": {