Skip to content

Commit 1dc379a

Browse files
Merge pull request #55 from pinanks/DOT-2984
Release v2.0.8
2 parents e1f4628 + 7a3b197 commit 1dc379a

File tree

8 files changed

+125
-42
lines changed

8 files changed

+125
-42
lines changed

package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
{
22
"name": "@lambdatest/smartui-cli",
3-
"version": "2.0.7",
3+
"version": "2.0.8",
44
"description": "A command line interface (CLI) to run SmartUI tests on LambdaTest",
55
"files": [
66
"dist/**/*"

src/lib/config.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -28,6 +28,7 @@ export const DEFAULT_CONFIG: Config = {
2828
[360],
2929
],
3030
waitForTimeout: 1000,
31+
enableJavaScript: false,
3132
}
3233
};
3334

src/lib/ctx.ts

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -38,7 +38,8 @@ export default (options: Record<string, string>): Context => {
3838
browsers: config.web.browsers,
3939
viewports: viewports,
4040
waitForPageRender: config.web.waitForPageRender || 0,
41-
waitForTimeout: config.web.waitForTimeout || 0
41+
waitForTimeout: config.web.waitForTimeout || 0,
42+
enableJavaScript: config.web.enableJavaScript || false
4243
},
4344
webStaticConfig: [],
4445
git: {

src/lib/httpClient.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -17,7 +17,7 @@ export default class httpClient {
1717
}
1818

1919
async request(config: AxiosRequestConfig, log: Logger): Promise<Record<string, any>> {
20-
log.debug(`http request: ${JSON.stringify(config)}`);
20+
log.debug(`http request: ${config.method} ${config.url}`);
2121

2222
return this.axiosInstance.request(config)
2323
.then(resp => {

src/lib/processSnapshot.ts

Lines changed: 110 additions & 39 deletions
Original file line numberDiff line numberDiff line change
@@ -1,22 +1,78 @@
11
import { Snapshot, Context, ProcessedSnapshot } from "../types.js";
2-
import { chromium, Locator } from "@playwright/test"
2+
import { scrollToBottomAndBackToTop } from "./utils.js"
3+
import { chromium, Locator, selectors } from "@playwright/test"
34

5+
const MAX_RESOURCE_SIZE = 5 * (1024 ** 2); // 5MB
6+
var ALLOWED_RESOURCES = ['document', 'stylesheet', 'image', 'media', 'font', 'other'];
7+
const ALLOWED_STATUSES = [200, 201];
48
const MIN_VIEWPORT_HEIGHT = 1080;
59

610
export default async (snapshot: Snapshot, ctx: Context): Promise<Record<string, any>> => {
7-
// Process snapshot options
11+
ctx.log.debug(`Processing snapshot ${snapshot.name}`);
12+
13+
if (!ctx.browser) ctx.browser = await chromium.launch({ headless: true });
14+
const context = await ctx.browser.newContext()
15+
const page = await context.newPage();
16+
let cache: Record<string, any> = {};
17+
18+
// Use route to intercept network requests and discover resources
19+
await page.route('**/*', async (route, request) => {
20+
const requestUrl = request.url()
21+
const snapshotHostname = new URL(snapshot.url).hostname;
22+
const requestHostname = new URL(requestUrl).hostname;
23+
24+
try {
25+
const response = await page.request.fetch(request);
26+
const body = await response.body();
27+
28+
if (ctx.webConfig.enableJavaScript) ALLOWED_RESOURCES.push('script');
29+
if (!body) {
30+
ctx.log.debug(`Handling request ${requestUrl}\n - skipping no response`);
31+
} else if (!body.length) {
32+
ctx.log.debug(`Handling request ${requestUrl}\n - skipping empty response`);
33+
} else if (requestUrl === snapshot.url) {
34+
ctx.log.debug(`Handling request ${requestUrl}\n - skipping root resource`);
35+
} else if (requestHostname !== snapshotHostname) {
36+
ctx.log.debug(`Handling request ${requestUrl}\n - skipping remote resource`);
37+
} else if (cache[requestUrl]) {
38+
ctx.log.debug(`Handling request ${requestUrl}\n - skipping already cached resource`);
39+
} else if (body.length > MAX_RESOURCE_SIZE) {
40+
ctx.log.debug(`Handling request ${requestUrl}\n - skipping resource larger than 5MB`);
41+
} else if (!ALLOWED_STATUSES.includes(response.status())) {
42+
ctx.log.debug(`Handling request ${requestUrl}\n - skipping disallowed status [${response.status()}]`);
43+
} else if (!ctx.webConfig.enableJavaScript && !ALLOWED_RESOURCES.includes(request.resourceType())) {
44+
ctx.log.debug(`Handling request ${requestUrl}\n - skipping disallowed resource type [${request.resourceType()}]`);
45+
} else {
46+
ctx.log.debug(`Handling request ${requestUrl}\n - content-type ${response.headers()['content-type']}`);
47+
cache[requestUrl] = {
48+
body: body.toString('base64'),
49+
type: response.headers()['content-type']
50+
}
51+
}
52+
53+
// Continue the request with the fetched response
54+
route.fulfill({
55+
status: response.status(),
56+
headers: response.headers(),
57+
body: body,
58+
});
59+
} catch (error) {
60+
ctx.log.debug(`Handling request ${requestUrl} - aborted`);
61+
route.abort();
62+
}
63+
});
64+
865
let options = snapshot.options;
966
let optionWarnings: Set<string> = new Set();
1067
let processedOptions: Record<string, any> = {};
11-
if (options && Object.keys(options).length !== 0) {
12-
ctx.log.debug(`Processing options: ${JSON.stringify(options)}`);
68+
let selectors: Array<string> = [];
69+
let ignoreOrSelectDOM: string;
70+
let ignoreOrSelectBoxes: string;
71+
if (options && Object.keys(options).length) {
72+
ctx.log.debug(`Snapshot options: ${JSON.stringify(options)}`);
1373

14-
if ((options.ignoreDOM && Object.keys(options.ignoreDOM).length !== 0) || (options.selectDOM && Object.keys(options.selectDOM).length !== 0)) {
15-
if (!ctx.browser) ctx.browser = await chromium.launch({ headless: true });
16-
17-
let ignoreOrSelectDOM: string;
18-
let ignoreOrSelectBoxes: string;
19-
if (options.ignoreDOM && Object.keys(options.ignoreDOM).length !== 0) {
74+
if ((options.ignoreDOM && Object.keys(options.ignoreDOM).length) || (options.selectDOM && Object.keys(options.selectDOM).length)) {
75+
if (options.ignoreDOM && Object.keys(options.ignoreDOM).length) {
2076
processedOptions.ignoreBoxes = {};
2177
ignoreOrSelectDOM = 'ignoreDOM';
2278
ignoreOrSelectBoxes = 'ignoreBoxes';
@@ -26,7 +82,6 @@ export default async (snapshot: Snapshot, ctx: Context): Promise<Record<string,
2682
ignoreOrSelectBoxes = 'selectBoxes';
2783
}
2884

29-
let selectors: Array<string> = [];
3085
for (const [key, value] of Object.entries(options[ignoreOrSelectDOM])) {
3186
switch (key) {
3287
case 'id':
@@ -42,46 +97,62 @@ export default async (snapshot: Snapshot, ctx: Context): Promise<Record<string,
4297
selectors.push(...value);
4398
break;
4499
}
45-
}
46-
47-
for (const vp of ctx.webConfig.viewports) {
48-
const page = await ctx.browser.newPage({ viewport: { width: vp.width, height: vp.height || MIN_VIEWPORT_HEIGHT}});
49-
await page.setContent(snapshot.dom.html);
100+
}
101+
}
102+
}
50103

51-
let viewport: string = `${vp.width}${vp.height ? 'x'+vp.height : ''}`;
52-
if (!Array.isArray(processedOptions[ignoreOrSelectBoxes][viewport])) processedOptions[ignoreOrSelectBoxes][viewport] = []
104+
// process for every viewport
105+
let navigated: boolean = false;
106+
for (const viewport of ctx.webConfig.viewports) {
107+
await page.setViewportSize({ width: viewport.width, height: viewport.height || MIN_VIEWPORT_HEIGHT });
108+
ctx.log.debug(`Page resized to ${viewport.width}x${viewport.height || MIN_VIEWPORT_HEIGHT}`);
109+
if (!navigated) {
110+
await page.goto(snapshot.url);
111+
navigated = true;
112+
ctx.log.debug(`Navigated to ${snapshot.url}`);
113+
}
114+
if (!viewport.height) await page.evaluate(scrollToBottomAndBackToTop);
115+
await page.waitForLoadState('networkidle');
116+
ctx.log.debug('Network idle 500ms');
53117

54-
let locators: Array<Locator> = [];
55-
let boxes: Array<Record<string, number>> = [];
56-
for (const selector of selectors) {
57-
let l = await page.locator(selector).all()
58-
if (l.length === 0) {
59-
optionWarnings.add(`For snapshot ${snapshot.name}, no element found for selector ${selector}`);
60-
continue;
61-
}
62-
locators.push(...l);
63-
}
64-
for (const locator of locators) {
65-
let bb = await locator.boundingBox();
66-
if (bb) boxes.push({
67-
left: bb.x,
68-
top: bb.y,
69-
right: bb.x + bb.width,
70-
bottom: bb.y + bb.height
71-
});
118+
// find bounding boxes for elements
119+
if (selectors.length) {
120+
let viewportString: string = `${viewport.width}${viewport.height ? 'x'+viewport.height : ''}`;
121+
if (!Array.isArray(processedOptions[ignoreOrSelectBoxes][viewportString])) processedOptions[ignoreOrSelectBoxes][viewportString] = []
122+
123+
let locators: Array<Locator> = [];
124+
let boxes: Array<Record<string, number>> = [];
125+
for (const selector of selectors) {
126+
let l = await page.locator(selector).all()
127+
if (l.length === 0) {
128+
optionWarnings.add(`For snapshot ${snapshot.name}, no element found for selector ${selector}`);
129+
continue;
72130
}
73-
74-
processedOptions[ignoreOrSelectBoxes][viewport].push(...boxes);
75-
await page.close();
131+
locators.push(...l);
132+
}
133+
for (const locator of locators) {
134+
let bb = await locator.boundingBox();
135+
if (bb) boxes.push({
136+
left: bb.x,
137+
top: bb.y,
138+
right: bb.x + bb.width,
139+
bottom: bb.y + bb.height
140+
});
76141
}
142+
143+
processedOptions[ignoreOrSelectBoxes][viewportString].push(...boxes);
77144
}
78145
}
79146

147+
await page.close();
148+
await context.close();
149+
80150
return {
81151
processedSnapshot: {
82152
name: snapshot.name,
83153
url: snapshot.url,
84154
dom: Buffer.from(snapshot.dom.html).toString('base64'),
155+
resources: cache,
85156
options: processedOptions
86157
},
87158
warnings: [...optionWarnings, ...snapshot.dom.warnings]

src/lib/schemaValidation.ts

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -66,6 +66,10 @@ const ConfigSchema = {
6666
maximum: 30000,
6767
errorMessage: "Invalid config; waitForTimeout must be > 0 and <= 30000"
6868
},
69+
enableJavaScript: {
70+
type: "boolean",
71+
errorMessage: "Invalid config; enableJavaScript must be true/false"
72+
}
6973
},
7074
required: ["browsers", "viewports"],
7175
additionalProperties: false

src/tasks/finalizeBuild.ts

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,14 @@
11
import { ListrTask, ListrRendererFactory } from 'listr2';
22
import { Context } from '../types.js'
33
import chalk from 'chalk';
4+
import { updateLogContext } from '../lib/logger.js';
45

56
export default (ctx: Context): ListrTask<Context, ListrRendererFactory, ListrRendererFactory> => {
67
return {
78
title: `Finalizing build`,
89
task: async (ctx, task): Promise<void> => {
10+
updateLogContext({task: 'finalizeBuild'});
11+
912
try {
1013
await new Promise(resolve => (setTimeout(resolve, 2000)));
1114
await ctx.client.finalizeBuild(ctx.build.id, ctx.totalSnapshots, ctx.log);

src/types.ts

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,7 @@ export interface Context {
1717
viewports: Array<{width: number, height: number}>;
1818
waitForPageRender: number;
1919
waitForTimeout: number;
20+
enableJavaScript: boolean;
2021
};
2122
webStaticConfig: WebStaticConfig;
2223
build: Build;
@@ -59,6 +60,7 @@ export interface ProcessedSnapshot {
5960
url: string,
6061
name: string,
6162
dom: string,
63+
resources: Record<string, any>,
6264
options: {
6365
ignoreBoxes?: Record<string, Array<Record<string, number>>>,
6466
selectBoxes?: Record<string, Array<Record<string, number>>>
@@ -91,6 +93,7 @@ export interface WebConfig {
9193
resolutions?: Array<Array<number>>; // for backward compatibility
9294
waitForPageRender?: number;
9395
waitForTimeout?: number;
96+
enableJavaScript?: boolean;
9497
}
9598

9699
export type WebStaticConfig = Array<{

0 commit comments

Comments
 (0)