@lambdatest/smartui-cli 2.0.7 → 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 +167 -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
  });
@@ -474,7 +581,8 @@ var DEFAULT_CONFIG = {
474
581
  [1366],
475
582
  [360]
476
583
  ],
477
- waitForTimeout: 1e3
584
+ waitForTimeout: 1e3,
585
+ enableJavaScript: false
478
586
  }
479
587
  };
480
588
  function createConfig(filepath) {
@@ -484,13 +592,13 @@ function createConfig(filepath) {
484
592
  console.log("Error: Config file must have .json extension");
485
593
  return;
486
594
  }
487
- if (fs__default.default.existsSync(filepath)) {
595
+ if (fs2__default.default.existsSync(filepath)) {
488
596
  console.log(`Error: SmartUI Config already exists: ${filepath}`);
489
597
  console.log(`To create a new file, please specify the file name like: 'smartui config:create .smartui-config.json'`);
490
598
  return;
491
599
  }
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");
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");
494
602
  console.log(`Created SmartUI Config: ${filepath}`);
495
603
  }
496
604
  function createWebStaticConfig(filepath) {
@@ -500,18 +608,18 @@ function createWebStaticConfig(filepath) {
500
608
  console.log("Error: Config file must have .json extension");
501
609
  return;
502
610
  }
503
- if (fs__default.default.existsSync(filepath)) {
611
+ if (fs2__default.default.existsSync(filepath)) {
504
612
  console.log(`Error: web-static config already exists: ${filepath}`);
505
613
  console.log(`To create a new file, please specify the file name like: 'smartui config:create-web-static links.json'`);
506
614
  return;
507
615
  }
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");
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");
510
618
  console.log(`Created web-static config: ${filepath}`);
511
619
  }
512
620
 
513
621
  // package.json
514
- var version = "2.0.7";
622
+ var version = "2.0.8";
515
623
  var package_default = {
516
624
  name: "@lambdatest/smartui-cli",
517
625
  version,
@@ -559,39 +667,6 @@ var package_default = {
559
667
  typescript: "^5.3.2"
560
668
  }
561
669
  };
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
670
  var httpClient = class {
596
671
  constructor({ SMARTUI_CLIENT_API_URL, PROJECT_TOKEN }) {
597
672
  this.axiosInstance = axios__default.default.create({
@@ -601,7 +676,7 @@ var httpClient = class {
601
676
  }
602
677
  request(config, log) {
603
678
  return __async(this, null, function* () {
604
- log.debug(`http request: ${JSON.stringify(config)}`);
679
+ log.debug(`http request: ${config.method} ${config.url}`);
605
680
  return this.axiosInstance.request(config).then((resp) => {
606
681
  log.debug(`http response: ${JSON.stringify({
607
682
  status: resp.status,
@@ -679,7 +754,7 @@ var httpClient = class {
679
754
  }, log);
680
755
  }
681
756
  uploadScreenshot({ id: buildId, name: buildName, baseline }, ssPath, ssName, browserName, viewport, completed) {
682
- const file = fs__default.default.readFileSync(ssPath);
757
+ const file = fs2__default.default.readFileSync(ssPath);
683
758
  const form = new FormData__default.default();
684
759
  form.append("screenshots", file, { filename: `${ssName}.png`, contentType: "image/png" });
685
760
  form.append("browser", browserName);
@@ -725,7 +800,7 @@ var ctx_default = (options) => {
725
800
  let config = DEFAULT_CONFIG;
726
801
  try {
727
802
  if (options.config) {
728
- config = JSON.parse(fs__default.default.readFileSync(options.config, "utf-8"));
803
+ config = JSON.parse(fs2__default.default.readFileSync(options.config, "utf-8"));
729
804
  if (config.web.resolutions) {
730
805
  config.web.viewports = config.web.resolutions;
731
806
  delete config.web.resolutions;
@@ -747,7 +822,8 @@ var ctx_default = (options) => {
747
822
  browsers: config.web.browsers,
748
823
  viewports,
749
824
  waitForPageRender: config.web.waitForPageRender || 0,
750
- waitForTimeout: config.web.waitForTimeout || 0
825
+ waitForTimeout: config.web.waitForTimeout || 0,
826
+ enableJavaScript: config.web.enableJavaScript || false
751
827
  },
752
828
  webStaticConfig: [],
753
829
  git: {
@@ -889,6 +965,7 @@ var finalizeBuild_default = (ctx) => {
889
965
  return {
890
966
  title: `Finalizing build`,
891
967
  task: (ctx2, task) => __async(void 0, null, function* () {
968
+ updateLogContext({ task: "finalizeBuild" });
892
969
  try {
893
970
  yield new Promise((resolve) => setTimeout(resolve, 2e3));
894
971
  yield ctx2.client.finalizeBuild(ctx2.build.id, ctx2.totalSnapshots, ctx2.log);
@@ -1052,12 +1129,12 @@ var command2 = new commander.Command();
1052
1129
  command2.name("capture").description("Capture screenshots of static sites").argument("<file>", "Web static config file").action(function(file, _, command3) {
1053
1130
  return __async(this, null, function* () {
1054
1131
  let ctx = ctx_default(command3.optsWithGlobals());
1055
- if (!fs__default.default.existsSync(file)) {
1132
+ if (!fs2__default.default.existsSync(file)) {
1056
1133
  console.log(`Error: Web Static Config file ${file} not found.`);
1057
1134
  return;
1058
1135
  }
1059
1136
  try {
1060
- ctx.webStaticConfig = JSON.parse(fs__default.default.readFileSync(file, "utf8"));
1137
+ ctx.webStaticConfig = JSON.parse(fs2__default.default.readFileSync(file, "utf8"));
1061
1138
  if (!validateWebStaticConfig(ctx.webStaticConfig))
1062
1139
  throw new Error(validateWebStaticConfig.errors[0].message);
1063
1140
  } 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.8",
4
4
  "description": "A command line interface (CLI) to run SmartUI tests on LambdaTest",
5
5
  "files": [
6
6
  "dist/**/*"