@lambdatest/smartui-cli 2.0.3 → 2.0.5

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 +341 -107
  2. package/package.json +1 -1
package/dist/index.cjs CHANGED
@@ -8,14 +8,14 @@ var chalk = require('chalk');
8
8
  var path2 = require('path');
9
9
  var fastify = require('fastify');
10
10
  var fs = require('fs');
11
- var winston = require('winston');
11
+ var test = require('@playwright/test');
12
12
  var Ajv = require('ajv');
13
13
  var addErrors = require('ajv-errors');
14
+ var winston = require('winston');
14
15
  var FormData = require('form-data');
15
16
  var axios = require('axios');
16
17
  var child_process = require('child_process');
17
18
  var spawn = require('cross-spawn');
18
- var test = require('@playwright/test');
19
19
 
20
20
  function _interopDefault (e) { return e && e.__esModule ? e : { default: e }; }
21
21
 
@@ -66,6 +66,271 @@ var __async = (__this, __arguments, generator) => {
66
66
  step((generator = generator.apply(__this, __arguments)).next());
67
67
  });
68
68
  };
69
+ var MIN_VIEWPORT_HEIGHT = 1080;
70
+ var processSnapshot_default = (snapshot, ctx) => __async(void 0, null, function* () {
71
+ let options = snapshot.options;
72
+ let warnings = [];
73
+ let processedOptions = {};
74
+ if (options && Object.keys(options).length !== 0) {
75
+ ctx.log.debug(`Processing options: ${JSON.stringify(options)}`);
76
+ if (options.ignoreDOM && Object.keys(options.ignoreDOM).length !== 0 || options.selectDOM && Object.keys(options.selectDOM).length !== 0) {
77
+ if (!ctx.browser)
78
+ ctx.browser = yield test.chromium.launch({ headless: true });
79
+ let ignoreOrSelectDOM;
80
+ let ignoreOrSelectBoxes;
81
+ if (options.ignoreDOM && Object.keys(options.ignoreDOM).length !== 0) {
82
+ processedOptions.ignoreBoxes = {};
83
+ ignoreOrSelectDOM = "ignoreDOM";
84
+ ignoreOrSelectBoxes = "ignoreBoxes";
85
+ } else {
86
+ processedOptions.selectBoxes = {};
87
+ ignoreOrSelectDOM = "selectDOM";
88
+ ignoreOrSelectBoxes = "selectBoxes";
89
+ }
90
+ let selectors = [];
91
+ for (const [key, value] of Object.entries(options[ignoreOrSelectDOM])) {
92
+ switch (key) {
93
+ case "id":
94
+ selectors.push(...value.map((e) => "#" + e));
95
+ break;
96
+ case "class":
97
+ selectors.push(...value.map((e) => "." + e));
98
+ break;
99
+ case "xpath":
100
+ selectors.push(...value.map((e) => "xpath=" + e));
101
+ break;
102
+ case "cssSelector":
103
+ selectors.push(...value);
104
+ break;
105
+ }
106
+ }
107
+ for (const vp of ctx.webConfig.viewports) {
108
+ const page = yield ctx.browser.newPage({ viewport: { width: vp.width, height: vp.height || MIN_VIEWPORT_HEIGHT } });
109
+ yield page.setContent(snapshot.dom.html);
110
+ let viewport = `${vp.width}${vp.height ? "x" + vp.height : ""}`;
111
+ if (!Array.isArray(processedOptions[ignoreOrSelectBoxes][viewport]))
112
+ processedOptions[ignoreOrSelectBoxes][viewport] = [];
113
+ let locators = [];
114
+ let boxes = [];
115
+ for (const selector of selectors) {
116
+ let l = yield page.locator(selector).all();
117
+ if (l.length === 0) {
118
+ warnings.push(`For snapshot ${snapshot.name}, no element found for selector ${selector}`);
119
+ continue;
120
+ }
121
+ locators.push(...l);
122
+ }
123
+ for (const locator of locators) {
124
+ let bb = yield locator.boundingBox();
125
+ if (bb)
126
+ boxes.push({
127
+ left: bb.x,
128
+ top: bb.y,
129
+ right: bb.x + bb.width,
130
+ bottom: bb.y + bb.height
131
+ });
132
+ }
133
+ processedOptions[ignoreOrSelectBoxes][viewport].push(...boxes);
134
+ yield page.close();
135
+ }
136
+ }
137
+ }
138
+ warnings.push(...snapshot.dom.warnings);
139
+ return {
140
+ processedSnapshot: {
141
+ name: snapshot.name,
142
+ url: snapshot.url,
143
+ dom: Buffer.from(snapshot.dom.html).toString("base64"),
144
+ options: processedOptions
145
+ },
146
+ warnings
147
+ };
148
+ });
149
+ var ajv = new Ajv__default.default({ allErrors: true });
150
+ ajv.addFormat("web-url", {
151
+ type: "string",
152
+ validate: (url) => {
153
+ const urlPattern = new RegExp("^(https?:\\/\\/)?((([a-z\\d]([a-z\\d-]*[a-z\\d])*)\\.)+[a-z]{2,}|((\\d{1,3}\\.){3}\\d{1,3}))(\\:\\d+)?(\\/[-a-z\\d%_.~+]*)*(\\?[;&a-z\\d%_.~+=-]*)?(\\#[-a-z\\d_]*)?$", "i");
154
+ return urlPattern.test(url.trim());
155
+ }
156
+ });
157
+ addErrors__default.default(ajv);
158
+ var ConfigSchema = {
159
+ type: "object",
160
+ properties: {
161
+ web: {
162
+ type: "object",
163
+ properties: {
164
+ browsers: {
165
+ type: "array",
166
+ items: { type: "string", enum: ["chrome", "firefox", "edge", "safari"] },
167
+ uniqueItems: true,
168
+ maxItems: 4,
169
+ errorMessage: "Invalid config; allowed browsers - chrome, firefox, edge, safari"
170
+ },
171
+ viewports: {
172
+ type: "array",
173
+ items: {
174
+ type: "array",
175
+ oneOf: [
176
+ {
177
+ items: [{ type: "number", minimum: 320, maximum: 7680 }],
178
+ minItems: 1,
179
+ maxItems: 1
180
+ },
181
+ {
182
+ items: [
183
+ { type: "number", minimum: 320, maximum: 7680 },
184
+ { type: "number", minimum: 320, maximum: 7680 }
185
+ ],
186
+ minItems: 2,
187
+ maxItems: 2
188
+ }
189
+ ],
190
+ errorMessage: "Invalid config; width/height must be >= 320 and <= 7680"
191
+ },
192
+ uniqueItems: true,
193
+ maxItems: 5,
194
+ errorMessage: "Invalid config; max unique viewports allowed - 5"
195
+ },
196
+ waitForPageRender: {
197
+ type: "number",
198
+ minimum: 0,
199
+ maximum: 3e5,
200
+ errorMessage: "Invalid config; waitForPageRender must be > 0 and <= 300000"
201
+ },
202
+ waitForTimeout: {
203
+ type: "number",
204
+ minimum: 0,
205
+ maximum: 3e4,
206
+ errorMessage: "Invalid config; waitForTimeout must be > 0 and <= 30000"
207
+ }
208
+ },
209
+ required: ["browsers", "viewports"],
210
+ additionalProperties: false
211
+ }
212
+ },
213
+ required: ["web"],
214
+ additionalProperties: false
215
+ };
216
+ var WebStaticConfigSchema = {
217
+ type: "array",
218
+ items: {
219
+ type: "object",
220
+ properties: {
221
+ name: {
222
+ type: "string",
223
+ minLength: 1,
224
+ errorMessage: "name is mandatory and cannot be empty"
225
+ },
226
+ url: {
227
+ type: "string",
228
+ format: "web-url",
229
+ errorMessage: "url is mandatory and must be a valid web URL"
230
+ },
231
+ waitForTimeout: {
232
+ type: "number",
233
+ nullable: true,
234
+ minimum: 0,
235
+ maximum: 3e4,
236
+ errorMessage: "waitForTimeout must be > 0 and <= 30000"
237
+ }
238
+ },
239
+ required: ["name", "url"],
240
+ additionalProperties: false
241
+ },
242
+ uniqueItems: true
243
+ };
244
+ var SnapshotSchema = {
245
+ type: "object",
246
+ properties: {
247
+ name: {
248
+ type: "string",
249
+ minLength: 1,
250
+ errorMessage: "Invalid snapshot; name is mandatory and cannot be empty"
251
+ },
252
+ url: {
253
+ type: "string",
254
+ format: "web-url",
255
+ errorMessage: "Invalid snapshot; url is mandatory and must be a valid web URL"
256
+ },
257
+ dom: {
258
+ type: "object"
259
+ },
260
+ options: {
261
+ type: "object",
262
+ properties: {
263
+ ignoreDOM: {
264
+ type: "object",
265
+ properties: {
266
+ id: {
267
+ type: "array",
268
+ items: { type: "string", minLength: 1, pattern: "^[^;]*$", errorMessage: "Invalid snapshot options; id cannot be empty or have semicolon" },
269
+ uniqueItems: true,
270
+ errorMessage: "Invalid snapshot options; id array must have unique items"
271
+ },
272
+ class: {
273
+ type: "array",
274
+ items: { type: "string", minLength: 1, pattern: "^[^;]*$", errorMessage: "Invalid snapshot options; class cannot be empty or have semicolon" },
275
+ uniqueItems: true,
276
+ errorMessage: "Invalid snapshot options; class array must have unique items"
277
+ },
278
+ cssSelector: {
279
+ type: "array",
280
+ items: { type: "string", minLength: 1, pattern: "^[^;]*$", errorMessage: "Invalid snapshot options; cssSelector cannot be empty or have semicolon" },
281
+ uniqueItems: true,
282
+ errorMessage: "Invalid snapshot options; cssSelector array must have unique items"
283
+ },
284
+ xpath: {
285
+ type: "array",
286
+ items: { type: "string", minLength: 1 },
287
+ uniqueItems: true,
288
+ errorMessage: "Invalid snapshot options; xpath array must have unique and non-empty items"
289
+ }
290
+ }
291
+ },
292
+ selectDOM: {
293
+ type: "object",
294
+ properties: {
295
+ id: {
296
+ type: "array",
297
+ items: { type: "string", minLength: 1, pattern: "^[^;]*$", errorMessage: "Invalid snapshot options; id cannot be empty or have semicolon" },
298
+ uniqueItems: true,
299
+ errorMessage: "Invalid snapshot options; id array must have unique items"
300
+ },
301
+ class: {
302
+ type: "array",
303
+ items: { type: "string", minLength: 1, pattern: "^[^;]*$", errorMessage: "Invalid snapshot options; class cannot be empty or have semicolon" },
304
+ uniqueItems: true,
305
+ errorMessage: "Invalid snapshot options; class array must have unique items"
306
+ },
307
+ cssSelector: {
308
+ type: "array",
309
+ items: { type: "string", minLength: 1, pattern: "^[^;]*$", errorMessage: "Invalid snapshot options; cssSelector cannot be empty or have semicolon" },
310
+ uniqueItems: true,
311
+ errorMessage: "Invalid snapshot options; cssSelector array must have unique items"
312
+ },
313
+ xpath: {
314
+ type: "array",
315
+ items: { type: "string", minLength: 1 },
316
+ uniqueItems: true,
317
+ errorMessage: "Invalid snapshot options; xpath array must have unique and non-empty items"
318
+ }
319
+ }
320
+ }
321
+ },
322
+ additionalProperties: false
323
+ }
324
+ },
325
+ required: ["name", "url", "dom", "options"],
326
+ additionalProperties: false,
327
+ errorMessage: "Invalid snapshot"
328
+ };
329
+ var validateConfig = ajv.compile(ConfigSchema);
330
+ var validateWebStaticConfig = ajv.compile(WebStaticConfigSchema);
331
+ var validateSnapshot = ajv.compile(SnapshotSchema);
332
+
333
+ // src/lib/server.ts
69
334
  var server_default = (ctx) => __async(void 0, null, function* () {
70
335
  const server = fastify__default.default({ logger: false, bodyLimit: 1e7 });
71
336
  const opts = {};
@@ -77,15 +342,17 @@ var server_default = (ctx) => __async(void 0, null, function* () {
77
342
  reply.code(200).send({ data: { dom: SMARTUI_DOM } });
78
343
  });
79
344
  server.post("/snapshot", opts, (request, reply) => __async(void 0, null, function* () {
80
- let { snapshot, testType } = request.body;
81
- snapshot.dom = Buffer.from(snapshot.dom).toString("base64");
82
345
  try {
83
- yield ctx.client.uploadSnapshot(ctx.build.id, snapshot, testType, ctx.log);
346
+ let { snapshot, testType } = request.body;
347
+ if (!validateSnapshot(snapshot))
348
+ throw new Error(validateSnapshot.errors[0].message);
349
+ let { processedSnapshot, warnings } = yield processSnapshot_default(snapshot, ctx);
350
+ yield ctx.client.uploadSnapshot(ctx.build.id, processedSnapshot, testType, ctx.log);
351
+ ctx.totalSnapshots++;
352
+ reply.code(200).send({ data: { message: "success", warnings } });
84
353
  } catch (error) {
85
- reply.code(500).send({ error: { message: error.message } });
354
+ return reply.code(500).send({ error: { message: error.message } });
86
355
  }
87
- ctx.totalSnapshots++;
88
- reply.code(200).send({ data: { message: "success" } });
89
356
  }));
90
357
  yield server.listen({ port: 8080 });
91
358
  let { port } = server.addresses()[0];
@@ -196,9 +463,9 @@ var DEFAULT_CONFIG = {
196
463
  "edge"
197
464
  ],
198
465
  viewports: [
199
- [1920, 1080],
200
- [1366, 768],
201
- [360, 640]
466
+ [1920],
467
+ [1366],
468
+ [360]
202
469
  ],
203
470
  waitForTimeout: 1e3
204
471
  }
@@ -237,100 +504,56 @@ function createWebStaticConfig(filepath) {
237
504
  }
238
505
 
239
506
  // package.json
240
- var version = "2.0.3";
241
- var ajv = new Ajv__default.default({ allErrors: true });
242
- ajv.addFormat("web-url", {
243
- type: "string",
244
- validate: (url) => {
245
- const urlPattern = new RegExp("^(https?:\\/\\/)?((([a-z\\d]([a-z\\d-]*[a-z\\d])*)\\.)+[a-z]{2,}|((\\d{1,3}\\.){3}\\d{1,3}))(\\:\\d+)?(\\/[-a-z\\d%_.~+]*)*(\\?[;&a-z\\d%_.~+=-]*)?(\\#[-a-z\\d_]*)?$", "i");
246
- return urlPattern.test(url);
247
- }
248
- });
249
- addErrors__default.default(ajv);
250
- var ConfigSchema = {
251
- type: "object",
252
- properties: {
253
- web: {
254
- type: "object",
255
- properties: {
256
- browsers: {
257
- type: "array",
258
- items: { type: "string", enum: ["chrome", "firefox", "edge", "safari"] },
259
- uniqueItems: true,
260
- maxItems: 4,
261
- errorMessage: "Invalid config; allowed browsers - chrome, firefox, edge, safari"
262
- },
263
- viewports: {
264
- type: "array",
265
- items: {
266
- type: "array",
267
- items: [
268
- { type: "number", minimum: 320, maximum: 7680 },
269
- { type: "number", minimum: 320, maximum: 7680 }
270
- ],
271
- additionalItems: false,
272
- minItems: 2,
273
- maxItems: 2
274
- },
275
- uniqueItems: true,
276
- maxItems: 5,
277
- errorMessage: "Invalid config; width/height must be >= 320 and <= 7680; max viewports allowed - 5"
278
- },
279
- waitForPageRender: {
280
- type: "number",
281
- minimum: 0,
282
- maximum: 3e5,
283
- errorMessage: "Invalid config; waitForPageRender must be > 0 and <= 300000"
284
- },
285
- waitForTimeout: {
286
- type: "number",
287
- minimum: 0,
288
- maximum: 3e4,
289
- errorMessage: "Invalid config; waitForTimeout must be > 0 and <= 30000"
290
- }
291
- },
292
- required: ["browsers", "viewports"],
293
- additionalProperties: false
294
- }
295
- },
296
- required: ["web"],
297
- additionalProperties: false
298
- };
299
- var WebStaticConfigSchema = {
300
- type: "array",
301
- items: {
302
- type: "object",
303
- properties: {
304
- name: {
305
- type: "string",
306
- minLength: 1,
307
- errorMessage: "name is mandatory and cannot be empty"
308
- },
309
- url: {
310
- type: "string",
311
- format: "web-url",
312
- errorMessage: "url is mandatory and must be a valid web URL"
313
- },
314
- waitForTimeout: {
315
- type: "number",
316
- nullable: true,
317
- minimum: 0,
318
- maximum: 3e4,
319
- errorMessage: "waitForTimeout must be > 0 and <= 30000"
320
- }
321
- },
322
- required: ["name", "url"],
323
- additionalProperties: false
324
- },
325
- uniqueItems: true
326
- };
327
- var validateConfig = ajv.compile(ConfigSchema);
328
- var validateWebStaticConfig = ajv.compile(WebStaticConfigSchema);
507
+ var version = "2.0.5";
508
+ var HTTP_SCHEME = "https:";
509
+ var HTTP_SCHEME_PREFIX = "https://";
510
+ var WWW = "www.";
329
511
  function delDir(dir) {
330
512
  if (fs__default.default.existsSync(dir)) {
331
513
  fs__default.default.rmSync(dir, { recursive: true });
332
514
  }
333
515
  }
516
+ function ensureHttps(urlString) {
517
+ try {
518
+ if (urlString && urlString.startsWith(WWW)) {
519
+ urlString = HTTP_SCHEME_PREFIX + urlString;
520
+ }
521
+ let url = new URL(urlString);
522
+ if (url.protocol !== HTTP_SCHEME) {
523
+ url.protocol = HTTP_SCHEME;
524
+ }
525
+ return url.toString();
526
+ } catch (error) {
527
+ console.error("Invalid URL: " + urlString, error);
528
+ return null;
529
+ }
530
+ }
531
+ function scrollToBottomAndBackToTop({
532
+ frequency = 100,
533
+ timing = 8,
534
+ remoteWindow = window
535
+ } = {}) {
536
+ return new Promise((resolve) => {
537
+ let scrolls = 1;
538
+ let scrollLength = remoteWindow.document.body.scrollHeight / frequency;
539
+ (function scroll() {
540
+ let scrollBy = scrollLength * scrolls;
541
+ remoteWindow.setTimeout(() => {
542
+ remoteWindow.scrollTo(0, scrollBy);
543
+ if (scrolls < frequency) {
544
+ scrolls += 1;
545
+ scroll();
546
+ }
547
+ if (scrolls === frequency) {
548
+ remoteWindow.setTimeout(() => {
549
+ remoteWindow.scrollTo(0, 0);
550
+ resolve();
551
+ }, timing);
552
+ }
553
+ }, timing);
554
+ })();
555
+ });
556
+ }
334
557
 
335
558
  // src/lib/httpClient.ts
336
559
  var httpClient = class {
@@ -461,7 +684,7 @@ var ctx_default = (options) => {
461
684
  process.exit();
462
685
  }
463
686
  for (let viewport of config.web.viewports)
464
- viewports.push({ width: viewport[0], height: viewport[1] });
687
+ viewports.push({ width: viewport[0], height: viewport[1] || 0 });
465
688
  return {
466
689
  env,
467
690
  log: logger_default,
@@ -629,7 +852,7 @@ var finalizeBuild_default = (ctx) => {
629
852
  var command = new commander.Command();
630
853
  command.name("exec").description("Run test commands around SmartUI").argument("<command...>", "Command supplied for running tests").action(function(execCommand, _, command3) {
631
854
  return __async(this, null, function* () {
632
- var _a;
855
+ var _a, _b;
633
856
  let ctx = ctx_default(command3.optsWithGlobals());
634
857
  if (!which__default.default.sync(execCommand[0], { nothrow: true })) {
635
858
  console.log(`Error: Command not found "${execCommand[0]}"`);
@@ -663,6 +886,7 @@ command.name("exec").description("Run test commands around SmartUI").argument("<
663
886
  console.log("\nRefer docs: https://www.lambdatest.com/support/docs/smart-visual-regression-testing/");
664
887
  } finally {
665
888
  yield (_a = ctx.server) == null ? void 0 : _a.close();
889
+ yield (_b = ctx.browser) == null ? void 0 : _b.close();
666
890
  }
667
891
  });
668
892
  });
@@ -685,6 +909,7 @@ var BROWSER_FIREFOX = "firefox";
685
909
  var BROWSER_EDGE = "edge";
686
910
  var EDGE_CHANNEL = "msedge";
687
911
  var PW_WEBKIT = "webkit";
912
+ var MIN_VIEWPORT_HEIGHT2 = 1080;
688
913
  function captureScreenshots(ctx, screenshots) {
689
914
  return __async(this, null, function* () {
690
915
  var _a;
@@ -692,6 +917,7 @@ function captureScreenshots(ctx, screenshots) {
692
917
  let totalBrowsers = ctx.webConfig.browsers.length;
693
918
  let totalViewports = ctx.webConfig.viewports.length;
694
919
  let totalScreenshots = screenshots.length;
920
+ let capturedScreenshots = 0;
695
921
  for (let i = 0; i < totalBrowsers; i++) {
696
922
  let browserName = (_a = ctx.webConfig.browsers[i]) == null ? void 0 : _a.toLowerCase();
697
923
  let browser;
@@ -718,17 +944,25 @@ function captureScreenshots(ctx, screenshots) {
718
944
  let screenshot = screenshots[j];
719
945
  let screenshotId = screenshot.name.toLowerCase().replace(/\s/g, "-");
720
946
  const page = yield context.newPage();
947
+ if (screenshot.url) {
948
+ screenshot.url = screenshot.url.trim();
949
+ screenshot.url = ensureHttps(screenshot.url);
950
+ }
721
951
  yield page.goto(screenshot.url, pageOptions);
722
- yield page.waitForTimeout(screenshot.waitForTimeout || 0);
723
952
  for (let k = 0; k < totalViewports; k++) {
724
953
  let { width, height } = ctx.webConfig.viewports[k];
725
954
  let ssName = `${browserName}-${width}x${height}-${screenshotId}.png`;
726
955
  let ssPath = `screenshots/${screenshotId}/${ssName}.png`;
727
- yield page.setViewportSize({ width, height });
728
- yield page.screenshot({ path: ssPath });
956
+ yield page.setViewportSize({ width, height: height || MIN_VIEWPORT_HEIGHT2 });
957
+ if (height === 0)
958
+ yield page.evaluate(scrollToBottomAndBackToTop);
959
+ yield page.waitForTimeout(screenshot.waitForTimeout || 0);
960
+ yield page.screenshot({ path: ssPath, fullPage: height ? false : true });
729
961
  let completed = i == totalBrowsers - 1 && j == totalScreenshots - 1 && k == totalViewports - 1 ? true : false;
730
962
  browserName = browserName === BROWSER_SAFARI ? PW_WEBKIT : browserName;
731
963
  ctx.client.uploadScreenshot(ctx.build, ssPath, screenshot.name, browserName, `${width}x${height}`, completed);
964
+ capturedScreenshots++;
965
+ ctx.task.output = chalk__default.default.gray(`screenshots captured: ${capturedScreenshots}/${totalBrowsers * totalViewports * totalScreenshots}`);
732
966
  }
733
967
  yield page.close();
734
968
  }
@@ -747,8 +981,8 @@ var captureScreenshots_default = (ctx) => {
747
981
  title: "Capturing screenshots",
748
982
  task: (ctx2, task) => __async(void 0, null, function* () {
749
983
  try {
750
- let { webStaticConfig: screenshots } = ctx2;
751
- let totalScreenshots = yield captureScreenshots(ctx2, screenshots);
984
+ ctx2.task = task;
985
+ let totalScreenshots = yield captureScreenshots(ctx2, ctx2.webStaticConfig);
752
986
  task.title = "Screenshots captured successfully";
753
987
  task.output = chalk__default.default.gray(`total screenshots: ${totalScreenshots}`);
754
988
  } catch (error) {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@lambdatest/smartui-cli",
3
- "version": "2.0.3",
3
+ "version": "2.0.5",
4
4
  "description": "A command line interface (CLI) to run SmartUI tests on LambdaTest",
5
5
  "files": [
6
6
  "dist/**/*"