@lambdatest/smartui-cli 2.0.7 → 2.0.9

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 +168 -90
  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]
@@ -207,6 +310,10 @@ var ConfigSchema = {
207
310
  minimum: 0,
208
311
  maximum: 3e4,
209
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"
210
317
  }
211
318
  },
212
319
  required: ["browsers", "viewports"],
@@ -337,7 +444,7 @@ var validateSnapshot = ajv.compile(SnapshotSchema);
337
444
  var server_default = (ctx) => __async(void 0, null, function* () {
338
445
  const server = fastify__default.default({ logger: false, bodyLimit: 1e7 });
339
446
  const opts = {};
340
- 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");
341
448
  server.get("/healthcheck", opts, (_, reply) => {
342
449
  reply.code(200).send({ cliVersion: ctx.cliVersion });
343
450
  });
@@ -360,6 +467,7 @@ var server_default = (ctx) => __async(void 0, null, function* () {
360
467
  yield server.listen();
361
468
  let { port } = server.addresses()[0];
362
469
  process.env.SMARTUI_SERVER_ADDRESS = `http://localhost:${port}`;
470
+ process.env.CYPRESS_SMARTUI_SERVER_ADDRESS = `http://localhost:${port}`;
363
471
  return server;
364
472
  });
365
473
 
@@ -474,7 +582,8 @@ var DEFAULT_CONFIG = {
474
582
  [1366],
475
583
  [360]
476
584
  ],
477
- waitForTimeout: 1e3
585
+ waitForTimeout: 1e3,
586
+ enableJavaScript: false
478
587
  }
479
588
  };
480
589
  function createConfig(filepath) {
@@ -484,13 +593,13 @@ function createConfig(filepath) {
484
593
  console.log("Error: Config file must have .json extension");
485
594
  return;
486
595
  }
487
- if (fs__default.default.existsSync(filepath)) {
596
+ if (fs2__default.default.existsSync(filepath)) {
488
597
  console.log(`Error: SmartUI Config already exists: ${filepath}`);
489
598
  console.log(`To create a new file, please specify the file name like: 'smartui config:create .smartui-config.json'`);
490
599
  return;
491
600
  }
492
- fs__default.default.mkdirSync(path2__default.default.dirname(filepath), { recursive: true });
493
- fs__default.default.writeFileSync(filepath, JSON.stringify(DEFAULT_CONFIG, null, 2) + "\n");
601
+ fs2__default.default.mkdirSync(path2__default.default.dirname(filepath), { recursive: true });
602
+ fs2__default.default.writeFileSync(filepath, JSON.stringify(DEFAULT_CONFIG, null, 2) + "\n");
494
603
  console.log(`Created SmartUI Config: ${filepath}`);
495
604
  }
496
605
  function createWebStaticConfig(filepath) {
@@ -500,18 +609,18 @@ function createWebStaticConfig(filepath) {
500
609
  console.log("Error: Config file must have .json extension");
501
610
  return;
502
611
  }
503
- if (fs__default.default.existsSync(filepath)) {
612
+ if (fs2__default.default.existsSync(filepath)) {
504
613
  console.log(`Error: web-static config already exists: ${filepath}`);
505
614
  console.log(`To create a new file, please specify the file name like: 'smartui config:create-web-static links.json'`);
506
615
  return;
507
616
  }
508
- fs__default.default.mkdirSync(path2__default.default.dirname(filepath), { recursive: true });
509
- fs__default.default.writeFileSync(filepath, JSON.stringify(DEFAULT_WEB_STATIC_CONFIG, null, 2) + "\n");
617
+ fs2__default.default.mkdirSync(path2__default.default.dirname(filepath), { recursive: true });
618
+ fs2__default.default.writeFileSync(filepath, JSON.stringify(DEFAULT_WEB_STATIC_CONFIG, null, 2) + "\n");
510
619
  console.log(`Created web-static config: ${filepath}`);
511
620
  }
512
621
 
513
622
  // package.json
514
- var version = "2.0.7";
623
+ var version = "2.0.9";
515
624
  var package_default = {
516
625
  name: "@lambdatest/smartui-cli",
517
626
  version,
@@ -559,39 +668,6 @@ var package_default = {
559
668
  typescript: "^5.3.2"
560
669
  }
561
670
  };
562
- function delDir(dir) {
563
- if (fs__default.default.existsSync(dir)) {
564
- fs__default.default.rmSync(dir, { recursive: true });
565
- }
566
- }
567
- function scrollToBottomAndBackToTop({
568
- frequency = 100,
569
- timing = 8,
570
- remoteWindow = window
571
- } = {}) {
572
- return new Promise((resolve) => {
573
- let scrolls = 1;
574
- let scrollLength = remoteWindow.document.body.scrollHeight / frequency;
575
- (function scroll() {
576
- let scrollBy = scrollLength * scrolls;
577
- remoteWindow.setTimeout(() => {
578
- remoteWindow.scrollTo(0, scrollBy);
579
- if (scrolls < frequency) {
580
- scrolls += 1;
581
- scroll();
582
- }
583
- if (scrolls === frequency) {
584
- remoteWindow.setTimeout(() => {
585
- remoteWindow.scrollTo(0, 0);
586
- resolve();
587
- }, timing);
588
- }
589
- }, timing);
590
- })();
591
- });
592
- }
593
-
594
- // src/lib/httpClient.ts
595
671
  var httpClient = class {
596
672
  constructor({ SMARTUI_CLIENT_API_URL, PROJECT_TOKEN }) {
597
673
  this.axiosInstance = axios__default.default.create({
@@ -601,7 +677,7 @@ var httpClient = class {
601
677
  }
602
678
  request(config, log) {
603
679
  return __async(this, null, function* () {
604
- log.debug(`http request: ${JSON.stringify(config)}`);
680
+ log.debug(`http request: ${config.method} ${config.url}`);
605
681
  return this.axiosInstance.request(config).then((resp) => {
606
682
  log.debug(`http response: ${JSON.stringify({
607
683
  status: resp.status,
@@ -679,7 +755,7 @@ var httpClient = class {
679
755
  }, log);
680
756
  }
681
757
  uploadScreenshot({ id: buildId, name: buildName, baseline }, ssPath, ssName, browserName, viewport, completed) {
682
- const file = fs__default.default.readFileSync(ssPath);
758
+ const file = fs2__default.default.readFileSync(ssPath);
683
759
  const form = new FormData__default.default();
684
760
  form.append("screenshots", file, { filename: `${ssName}.png`, contentType: "image/png" });
685
761
  form.append("browser", browserName);
@@ -725,7 +801,7 @@ var ctx_default = (options) => {
725
801
  let config = DEFAULT_CONFIG;
726
802
  try {
727
803
  if (options.config) {
728
- config = JSON.parse(fs__default.default.readFileSync(options.config, "utf-8"));
804
+ config = JSON.parse(fs2__default.default.readFileSync(options.config, "utf-8"));
729
805
  if (config.web.resolutions) {
730
806
  config.web.viewports = config.web.resolutions;
731
807
  delete config.web.resolutions;
@@ -747,7 +823,8 @@ var ctx_default = (options) => {
747
823
  browsers: config.web.browsers,
748
824
  viewports,
749
825
  waitForPageRender: config.web.waitForPageRender || 0,
750
- waitForTimeout: config.web.waitForTimeout || 0
826
+ waitForTimeout: config.web.waitForTimeout || 0,
827
+ enableJavaScript: config.web.enableJavaScript || false
751
828
  },
752
829
  webStaticConfig: [],
753
830
  git: {
@@ -889,6 +966,7 @@ var finalizeBuild_default = (ctx) => {
889
966
  return {
890
967
  title: `Finalizing build`,
891
968
  task: (ctx2, task) => __async(void 0, null, function* () {
969
+ updateLogContext({ task: "finalizeBuild" });
892
970
  try {
893
971
  yield new Promise((resolve) => setTimeout(resolve, 2e3));
894
972
  yield ctx2.client.finalizeBuild(ctx2.build.id, ctx2.totalSnapshots, ctx2.log);
@@ -1052,12 +1130,12 @@ var command2 = new commander.Command();
1052
1130
  command2.name("capture").description("Capture screenshots of static sites").argument("<file>", "Web static config file").action(function(file, _, command3) {
1053
1131
  return __async(this, null, function* () {
1054
1132
  let ctx = ctx_default(command3.optsWithGlobals());
1055
- if (!fs__default.default.existsSync(file)) {
1133
+ if (!fs2__default.default.existsSync(file)) {
1056
1134
  console.log(`Error: Web Static Config file ${file} not found.`);
1057
1135
  return;
1058
1136
  }
1059
1137
  try {
1060
- ctx.webStaticConfig = JSON.parse(fs__default.default.readFileSync(file, "utf8"));
1138
+ ctx.webStaticConfig = JSON.parse(fs2__default.default.readFileSync(file, "utf8"));
1061
1139
  if (!validateWebStaticConfig(ctx.webStaticConfig))
1062
1140
  throw new Error(validateWebStaticConfig.errors[0].message);
1063
1141
  } catch (error) {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@lambdatest/smartui-cli",
3
- "version": "2.0.7",
3
+ "version": "2.0.9",
4
4
  "description": "A command line interface (CLI) to run SmartUI tests on LambdaTest",
5
5
  "files": [
6
6
  "dist/**/*"