@lambdatest/smartui-cli 2.0.6 → 2.0.8

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 +174 -115
  2. package/package.json +1 -1
package/dist/index.cjs CHANGED
@@ -7,7 +7,7 @@ var listr2 = require('listr2');
7
7
  var chalk = require('chalk');
8
8
  var path2 = require('path');
9
9
  var fastify = require('fastify');
10
- var fs = require('fs');
10
+ var fs2 = require('fs');
11
11
  var test = require('@playwright/test');
12
12
  var Ajv = require('ajv');
13
13
  var addErrors = require('ajv-errors');
@@ -23,7 +23,7 @@ var which__default = /*#__PURE__*/_interopDefault(which);
23
23
  var chalk__default = /*#__PURE__*/_interopDefault(chalk);
24
24
  var path2__default = /*#__PURE__*/_interopDefault(path2);
25
25
  var fastify__default = /*#__PURE__*/_interopDefault(fastify);
26
- var fs__default = /*#__PURE__*/_interopDefault(fs);
26
+ var fs2__default = /*#__PURE__*/_interopDefault(fs2);
27
27
  var Ajv__default = /*#__PURE__*/_interopDefault(Ajv);
28
28
  var addErrors__default = /*#__PURE__*/_interopDefault(addErrors);
29
29
  var FormData__default = /*#__PURE__*/_interopDefault(FormData);
@@ -66,19 +66,109 @@ var __async = (__this, __arguments, generator) => {
66
66
  step((generator = generator.apply(__this, __arguments)).next());
67
67
  });
68
68
  };
69
+ function delDir(dir) {
70
+ if (fs2__default.default.existsSync(dir)) {
71
+ fs2__default.default.rmSync(dir, { recursive: true });
72
+ }
73
+ }
74
+ function scrollToBottomAndBackToTop({
75
+ frequency = 100,
76
+ timing = 8,
77
+ remoteWindow = window
78
+ } = {}) {
79
+ return new Promise((resolve) => {
80
+ let scrolls = 1;
81
+ let scrollLength = remoteWindow.document.body.scrollHeight / frequency;
82
+ (function scroll() {
83
+ let scrollBy = scrollLength * scrolls;
84
+ remoteWindow.setTimeout(() => {
85
+ remoteWindow.scrollTo(0, scrollBy);
86
+ if (scrolls < frequency) {
87
+ scrolls += 1;
88
+ scroll();
89
+ }
90
+ if (scrolls === frequency) {
91
+ remoteWindow.setTimeout(() => {
92
+ remoteWindow.scrollTo(0, 0);
93
+ resolve();
94
+ }, timing);
95
+ }
96
+ }, timing);
97
+ })();
98
+ });
99
+ }
100
+ var MAX_RESOURCE_SIZE = 5 * 1024 ** 2;
101
+ var ALLOWED_RESOURCES = ["document", "stylesheet", "image", "media", "font", "other"];
102
+ var ALLOWED_STATUSES = [200, 201];
69
103
  var MIN_VIEWPORT_HEIGHT = 1080;
70
104
  var processSnapshot_default = (snapshot, ctx) => __async(void 0, null, function* () {
105
+ ctx.log.debug(`Processing snapshot ${snapshot.name}`);
106
+ if (!ctx.browser)
107
+ ctx.browser = yield test.chromium.launch({ headless: true });
108
+ const context = yield ctx.browser.newContext();
109
+ const page = yield context.newPage();
110
+ let cache = {};
111
+ yield page.route("**/*", (route, request) => __async(void 0, null, function* () {
112
+ const requestUrl = request.url();
113
+ const snapshotHostname = new URL(snapshot.url).hostname;
114
+ const requestHostname = new URL(requestUrl).hostname;
115
+ try {
116
+ const response = yield page.request.fetch(request);
117
+ const body = yield response.body();
118
+ if (ctx.webConfig.enableJavaScript)
119
+ ALLOWED_RESOURCES.push("script");
120
+ if (!body) {
121
+ ctx.log.debug(`Handling request ${requestUrl}
122
+ - skipping no response`);
123
+ } else if (!body.length) {
124
+ ctx.log.debug(`Handling request ${requestUrl}
125
+ - skipping empty response`);
126
+ } else if (requestUrl === snapshot.url) {
127
+ ctx.log.debug(`Handling request ${requestUrl}
128
+ - skipping root resource`);
129
+ } else if (requestHostname !== snapshotHostname) {
130
+ ctx.log.debug(`Handling request ${requestUrl}
131
+ - skipping remote resource`);
132
+ } else if (cache[requestUrl]) {
133
+ ctx.log.debug(`Handling request ${requestUrl}
134
+ - skipping already cached resource`);
135
+ } else if (body.length > MAX_RESOURCE_SIZE) {
136
+ ctx.log.debug(`Handling request ${requestUrl}
137
+ - skipping resource larger than 5MB`);
138
+ } else if (!ALLOWED_STATUSES.includes(response.status())) {
139
+ ctx.log.debug(`Handling request ${requestUrl}
140
+ - skipping disallowed status [${response.status()}]`);
141
+ } else if (!ctx.webConfig.enableJavaScript && !ALLOWED_RESOURCES.includes(request.resourceType())) {
142
+ ctx.log.debug(`Handling request ${requestUrl}
143
+ - skipping disallowed resource type [${request.resourceType()}]`);
144
+ } else {
145
+ ctx.log.debug(`Handling request ${requestUrl}
146
+ - content-type ${response.headers()["content-type"]}`);
147
+ cache[requestUrl] = {
148
+ body: body.toString("base64"),
149
+ type: response.headers()["content-type"]
150
+ };
151
+ }
152
+ route.fulfill({
153
+ status: response.status(),
154
+ headers: response.headers(),
155
+ body
156
+ });
157
+ } catch (error) {
158
+ ctx.log.debug(`Handling request ${requestUrl} - aborted`);
159
+ route.abort();
160
+ }
161
+ }));
71
162
  let options = snapshot.options;
72
163
  let optionWarnings = /* @__PURE__ */ new Set();
73
164
  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) {
165
+ let selectors2 = [];
166
+ let ignoreOrSelectDOM;
167
+ let ignoreOrSelectBoxes;
168
+ if (options && Object.keys(options).length) {
169
+ ctx.log.debug(`Snapshot options: ${JSON.stringify(options)}`);
170
+ if (options.ignoreDOM && Object.keys(options.ignoreDOM).length || options.selectDOM && Object.keys(options.selectDOM).length) {
171
+ if (options.ignoreDOM && Object.keys(options.ignoreDOM).length) {
82
172
  processedOptions.ignoreBoxes = {};
83
173
  ignoreOrSelectDOM = "ignoreDOM";
84
174
  ignoreOrSelectBoxes = "ignoreBoxes";
@@ -87,59 +177,72 @@ var processSnapshot_default = (snapshot, ctx) => __async(void 0, null, function*
87
177
  ignoreOrSelectDOM = "selectDOM";
88
178
  ignoreOrSelectBoxes = "selectBoxes";
89
179
  }
90
- let selectors = [];
91
180
  for (const [key, value] of Object.entries(options[ignoreOrSelectDOM])) {
92
181
  switch (key) {
93
182
  case "id":
94
- selectors.push(...value.map((e) => "#" + e));
183
+ selectors2.push(...value.map((e) => "#" + e));
95
184
  break;
96
185
  case "class":
97
- selectors.push(...value.map((e) => "." + e));
186
+ selectors2.push(...value.map((e) => "." + e));
98
187
  break;
99
188
  case "xpath":
100
- selectors.push(...value.map((e) => "xpath=" + e));
189
+ selectors2.push(...value.map((e) => "xpath=" + e));
101
190
  break;
102
191
  case "cssSelector":
103
- selectors.push(...value);
192
+ selectors2.push(...value);
104
193
  break;
105
194
  }
106
195
  }
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
- optionWarnings.add(`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
- });
196
+ }
197
+ }
198
+ let navigated = false;
199
+ for (const viewport of ctx.webConfig.viewports) {
200
+ yield page.setViewportSize({ width: viewport.width, height: viewport.height || MIN_VIEWPORT_HEIGHT });
201
+ ctx.log.debug(`Page resized to ${viewport.width}x${viewport.height || MIN_VIEWPORT_HEIGHT}`);
202
+ if (!navigated) {
203
+ yield page.goto(snapshot.url);
204
+ navigated = true;
205
+ ctx.log.debug(`Navigated to ${snapshot.url}`);
206
+ }
207
+ if (!viewport.height)
208
+ yield page.evaluate(scrollToBottomAndBackToTop);
209
+ yield page.waitForLoadState("networkidle");
210
+ ctx.log.debug("Network idle 500ms");
211
+ if (selectors2.length) {
212
+ let viewportString = `${viewport.width}${viewport.height ? "x" + viewport.height : ""}`;
213
+ if (!Array.isArray(processedOptions[ignoreOrSelectBoxes][viewportString]))
214
+ processedOptions[ignoreOrSelectBoxes][viewportString] = [];
215
+ let locators = [];
216
+ let boxes = [];
217
+ for (const selector of selectors2) {
218
+ let l = yield page.locator(selector).all();
219
+ if (l.length === 0) {
220
+ optionWarnings.add(`For snapshot ${snapshot.name}, no element found for selector ${selector}`);
221
+ continue;
132
222
  }
133
- processedOptions[ignoreOrSelectBoxes][viewport].push(...boxes);
134
- yield page.close();
223
+ locators.push(...l);
135
224
  }
225
+ for (const locator of locators) {
226
+ let bb = yield locator.boundingBox();
227
+ if (bb)
228
+ boxes.push({
229
+ left: bb.x,
230
+ top: bb.y,
231
+ right: bb.x + bb.width,
232
+ bottom: bb.y + bb.height
233
+ });
234
+ }
235
+ processedOptions[ignoreOrSelectBoxes][viewportString].push(...boxes);
136
236
  }
137
237
  }
238
+ yield page.close();
239
+ yield context.close();
138
240
  return {
139
241
  processedSnapshot: {
140
242
  name: snapshot.name,
141
243
  url: snapshot.url,
142
244
  dom: Buffer.from(snapshot.dom.html).toString("base64"),
245
+ resources: cache,
143
246
  options: processedOptions
144
247
  },
145
248
  warnings: [...optionWarnings, ...snapshot.dom.warnings]
@@ -149,8 +252,12 @@ var ajv = new Ajv__default.default({ allErrors: true });
149
252
  ajv.addFormat("web-url", {
150
253
  type: "string",
151
254
  validate: (url) => {
152
- 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");
153
- return urlPattern.test(url.trim());
255
+ try {
256
+ new URL(url.trim());
257
+ return true;
258
+ } catch (error) {
259
+ return false;
260
+ }
154
261
  }
155
262
  });
156
263
  addErrors__default.default(ajv);
@@ -203,6 +310,10 @@ var ConfigSchema = {
203
310
  minimum: 0,
204
311
  maximum: 3e4,
205
312
  errorMessage: "Invalid config; waitForTimeout must be > 0 and <= 30000"
313
+ },
314
+ enableJavaScript: {
315
+ type: "boolean",
316
+ errorMessage: "Invalid config; enableJavaScript must be true/false"
206
317
  }
207
318
  },
208
319
  required: ["browsers", "viewports"],
@@ -333,7 +444,7 @@ var validateSnapshot = ajv.compile(SnapshotSchema);
333
444
  var server_default = (ctx) => __async(void 0, null, function* () {
334
445
  const server = fastify__default.default({ logger: false, bodyLimit: 1e7 });
335
446
  const opts = {};
336
- const SMARTUI_DOM = fs.readFileSync(path2__default.default.resolve(__dirname, "dom-serializer.js"), "utf-8");
447
+ const SMARTUI_DOM = fs2.readFileSync(path2__default.default.resolve(__dirname, "dom-serializer.js"), "utf-8");
337
448
  server.get("/healthcheck", opts, (_, reply) => {
338
449
  reply.code(200).send({ cliVersion: ctx.cliVersion });
339
450
  });
@@ -470,7 +581,8 @@ var DEFAULT_CONFIG = {
470
581
  [1366],
471
582
  [360]
472
583
  ],
473
- waitForTimeout: 1e3
584
+ waitForTimeout: 1e3,
585
+ enableJavaScript: false
474
586
  }
475
587
  };
476
588
  function createConfig(filepath) {
@@ -480,13 +592,13 @@ function createConfig(filepath) {
480
592
  console.log("Error: Config file must have .json extension");
481
593
  return;
482
594
  }
483
- if (fs__default.default.existsSync(filepath)) {
595
+ if (fs2__default.default.existsSync(filepath)) {
484
596
  console.log(`Error: SmartUI Config already exists: ${filepath}`);
485
597
  console.log(`To create a new file, please specify the file name like: 'smartui config:create .smartui-config.json'`);
486
598
  return;
487
599
  }
488
- fs__default.default.mkdirSync(path2__default.default.dirname(filepath), { recursive: true });
489
- fs__default.default.writeFileSync(filepath, JSON.stringify(DEFAULT_CONFIG, null, 2) + "\n");
600
+ fs2__default.default.mkdirSync(path2__default.default.dirname(filepath), { recursive: true });
601
+ fs2__default.default.writeFileSync(filepath, JSON.stringify(DEFAULT_CONFIG, null, 2) + "\n");
490
602
  console.log(`Created SmartUI Config: ${filepath}`);
491
603
  }
492
604
  function createWebStaticConfig(filepath) {
@@ -496,18 +608,18 @@ function createWebStaticConfig(filepath) {
496
608
  console.log("Error: Config file must have .json extension");
497
609
  return;
498
610
  }
499
- if (fs__default.default.existsSync(filepath)) {
611
+ if (fs2__default.default.existsSync(filepath)) {
500
612
  console.log(`Error: web-static config already exists: ${filepath}`);
501
613
  console.log(`To create a new file, please specify the file name like: 'smartui config:create-web-static links.json'`);
502
614
  return;
503
615
  }
504
- fs__default.default.mkdirSync(path2__default.default.dirname(filepath), { recursive: true });
505
- fs__default.default.writeFileSync(filepath, JSON.stringify(DEFAULT_WEB_STATIC_CONFIG, null, 2) + "\n");
616
+ fs2__default.default.mkdirSync(path2__default.default.dirname(filepath), { recursive: true });
617
+ fs2__default.default.writeFileSync(filepath, JSON.stringify(DEFAULT_WEB_STATIC_CONFIG, null, 2) + "\n");
506
618
  console.log(`Created web-static config: ${filepath}`);
507
619
  }
508
620
 
509
621
  // package.json
510
- var version = "2.0.6";
622
+ var version = "2.0.8";
511
623
  var package_default = {
512
624
  name: "@lambdatest/smartui-cli",
513
625
  version,
@@ -555,57 +667,6 @@ var package_default = {
555
667
  typescript: "^5.3.2"
556
668
  }
557
669
  };
558
- var HTTP_SCHEME = "https:";
559
- var HTTP_SCHEME_PREFIX = "https://";
560
- var WWW = "www.";
561
- function delDir(dir) {
562
- if (fs__default.default.existsSync(dir)) {
563
- fs__default.default.rmSync(dir, { recursive: true });
564
- }
565
- }
566
- function ensureHttps(urlString) {
567
- try {
568
- if (urlString && urlString.startsWith(WWW)) {
569
- urlString = HTTP_SCHEME_PREFIX + urlString;
570
- }
571
- let url = new URL(urlString);
572
- if (url.protocol !== HTTP_SCHEME) {
573
- url.protocol = HTTP_SCHEME;
574
- }
575
- return url.toString();
576
- } catch (error) {
577
- console.error("Invalid URL: " + urlString, error);
578
- return null;
579
- }
580
- }
581
- function scrollToBottomAndBackToTop({
582
- frequency = 100,
583
- timing = 8,
584
- remoteWindow = window
585
- } = {}) {
586
- return new Promise((resolve) => {
587
- let scrolls = 1;
588
- let scrollLength = remoteWindow.document.body.scrollHeight / frequency;
589
- (function scroll() {
590
- let scrollBy = scrollLength * scrolls;
591
- remoteWindow.setTimeout(() => {
592
- remoteWindow.scrollTo(0, scrollBy);
593
- if (scrolls < frequency) {
594
- scrolls += 1;
595
- scroll();
596
- }
597
- if (scrolls === frequency) {
598
- remoteWindow.setTimeout(() => {
599
- remoteWindow.scrollTo(0, 0);
600
- resolve();
601
- }, timing);
602
- }
603
- }, timing);
604
- })();
605
- });
606
- }
607
-
608
- // src/lib/httpClient.ts
609
670
  var httpClient = class {
610
671
  constructor({ SMARTUI_CLIENT_API_URL, PROJECT_TOKEN }) {
611
672
  this.axiosInstance = axios__default.default.create({
@@ -615,7 +676,7 @@ var httpClient = class {
615
676
  }
616
677
  request(config, log) {
617
678
  return __async(this, null, function* () {
618
- log.debug(`http request: ${JSON.stringify(config)}`);
679
+ log.debug(`http request: ${config.method} ${config.url}`);
619
680
  return this.axiosInstance.request(config).then((resp) => {
620
681
  log.debug(`http response: ${JSON.stringify({
621
682
  status: resp.status,
@@ -693,7 +754,7 @@ var httpClient = class {
693
754
  }, log);
694
755
  }
695
756
  uploadScreenshot({ id: buildId, name: buildName, baseline }, ssPath, ssName, browserName, viewport, completed) {
696
- const file = fs__default.default.readFileSync(ssPath);
757
+ const file = fs2__default.default.readFileSync(ssPath);
697
758
  const form = new FormData__default.default();
698
759
  form.append("screenshots", file, { filename: `${ssName}.png`, contentType: "image/png" });
699
760
  form.append("browser", browserName);
@@ -739,7 +800,7 @@ var ctx_default = (options) => {
739
800
  let config = DEFAULT_CONFIG;
740
801
  try {
741
802
  if (options.config) {
742
- config = JSON.parse(fs__default.default.readFileSync(options.config, "utf-8"));
803
+ config = JSON.parse(fs2__default.default.readFileSync(options.config, "utf-8"));
743
804
  if (config.web.resolutions) {
744
805
  config.web.viewports = config.web.resolutions;
745
806
  delete config.web.resolutions;
@@ -761,7 +822,8 @@ var ctx_default = (options) => {
761
822
  browsers: config.web.browsers,
762
823
  viewports,
763
824
  waitForPageRender: config.web.waitForPageRender || 0,
764
- waitForTimeout: config.web.waitForTimeout || 0
825
+ waitForTimeout: config.web.waitForTimeout || 0,
826
+ enableJavaScript: config.web.enableJavaScript || false
765
827
  },
766
828
  webStaticConfig: [],
767
829
  git: {
@@ -903,6 +965,7 @@ var finalizeBuild_default = (ctx) => {
903
965
  return {
904
966
  title: `Finalizing build`,
905
967
  task: (ctx2, task) => __async(void 0, null, function* () {
968
+ updateLogContext({ task: "finalizeBuild" });
906
969
  try {
907
970
  yield new Promise((resolve) => setTimeout(resolve, 2e3));
908
971
  yield ctx2.client.finalizeBuild(ctx2.build.id, ctx2.totalSnapshots, ctx2.log);
@@ -1014,11 +1077,7 @@ function captureScreenshots(ctx, screenshots) {
1014
1077
  let screenshot = screenshots[j];
1015
1078
  let screenshotId = screenshot.name.toLowerCase().replace(/\s/g, "-");
1016
1079
  const page = yield context.newPage();
1017
- if (screenshot.url) {
1018
- screenshot.url = screenshot.url.trim();
1019
- screenshot.url = ensureHttps(screenshot.url);
1020
- }
1021
- yield page.goto(screenshot.url, pageOptions);
1080
+ yield page.goto(screenshot.url.trim(), pageOptions);
1022
1081
  for (let k = 0; k < totalViewports; k++) {
1023
1082
  let { width, height } = ctx.webConfig.viewports[k];
1024
1083
  let ssName = `${browserName}-${width}x${height}-${screenshotId}.png`;
@@ -1070,12 +1129,12 @@ var command2 = new commander.Command();
1070
1129
  command2.name("capture").description("Capture screenshots of static sites").argument("<file>", "Web static config file").action(function(file, _, command3) {
1071
1130
  return __async(this, null, function* () {
1072
1131
  let ctx = ctx_default(command3.optsWithGlobals());
1073
- if (!fs__default.default.existsSync(file)) {
1132
+ if (!fs2__default.default.existsSync(file)) {
1074
1133
  console.log(`Error: Web Static Config file ${file} not found.`);
1075
1134
  return;
1076
1135
  }
1077
1136
  try {
1078
- ctx.webStaticConfig = JSON.parse(fs__default.default.readFileSync(file, "utf8"));
1137
+ ctx.webStaticConfig = JSON.parse(fs2__default.default.readFileSync(file, "utf8"));
1079
1138
  if (!validateWebStaticConfig(ctx.webStaticConfig))
1080
1139
  throw new Error(validateWebStaticConfig.errors[0].message);
1081
1140
  } catch (error) {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@lambdatest/smartui-cli",
3
- "version": "2.0.6",
3
+ "version": "2.0.8",
4
4
  "description": "A command line interface (CLI) to run SmartUI tests on LambdaTest",
5
5
  "files": [
6
6
  "dist/**/*"