@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.
- package/dist/index.cjs +168 -90
- 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
|
|
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
|
|
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
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
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
|
-
|
|
183
|
+
selectors2.push(...value.map((e) => "#" + e));
|
|
95
184
|
break;
|
|
96
185
|
case "class":
|
|
97
|
-
|
|
186
|
+
selectors2.push(...value.map((e) => "." + e));
|
|
98
187
|
break;
|
|
99
188
|
case "xpath":
|
|
100
|
-
|
|
189
|
+
selectors2.push(...value.map((e) => "xpath=" + e));
|
|
101
190
|
break;
|
|
102
191
|
case "cssSelector":
|
|
103
|
-
|
|
192
|
+
selectors2.push(...value);
|
|
104
193
|
break;
|
|
105
194
|
}
|
|
106
195
|
}
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
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
|
-
|
|
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 =
|
|
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 (
|
|
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
|
-
|
|
493
|
-
|
|
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 (
|
|
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
|
-
|
|
509
|
-
|
|
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.
|
|
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: ${
|
|
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 =
|
|
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(
|
|
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 (!
|
|
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(
|
|
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) {
|