Skip to content

Commit 2d18748

Browse files
Merge pull request #66 from LambdaTest/stage
Release v3.0.1
2 parents 0df6c6c + 88e9650 commit 2d18748

File tree

11 files changed

+168
-99
lines changed

11 files changed

+168
-99
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": "3.0.0",
3+
"version": "3.0.1",
44
"description": "A command line interface (CLI) to run SmartUI tests on LambdaTest",
55
"files": [
66
"dist/**/*"

src/commander/capture.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,7 @@ command
1616
.name('capture')
1717
.description('Capture screenshots of static sites')
1818
.argument('<file>', 'Web static config file')
19+
.option('--parallel', 'Capture parallely on all browsers')
1920
.action(async function(file, _, command) {
2021
let ctx: Context = ctxInit(command.optsWithGlobals());
2122

src/lib/constants.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -44,7 +44,7 @@ export default {
4444
FIREFOX: 'firefox',
4545
EDGE: 'edge',
4646
EDGE_CHANNEL: 'msedge',
47-
PW_WEBKIT: 'webkit',
47+
WEBKIT: 'webkit',
4848

4949
// viewports
5050
MIN_VIEWPORT_HEIGHT: 1080,

src/lib/ctx.ts

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -69,10 +69,12 @@ export default (options: Record<string, string>): Context => {
6969
id: '',
7070
name: '',
7171
baseline: false,
72-
url: '',
73-
projectId: '',
72+
url: ''
7473
},
7574
args: {},
75+
options: {
76+
parallel: options.parallel ? true : false
77+
},
7678
cliVersion: version,
7779
totalSnapshots: -1
7880
}

src/lib/httpClient.ts

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@ import fs from 'fs';
22
import FormData from 'form-data';
33
import axios, { AxiosInstance, AxiosRequestConfig } from 'axios';
44
import { Env, ProcessedSnapshot, Git, Build } from '../types.js';
5-
import { delDir } from './utils.js';
5+
import constants from './constants.js';
66
import type { Logger } from 'winston'
77
import pkgJSON from './../../package.json'
88

@@ -35,7 +35,7 @@ export default class httpClient {
3535
headers: error.response.headers,
3636
body: error.response.data
3737
})}`);
38-
throw new Error(error.response.data.error.message);
38+
throw new Error(error.response.data.error?.message);
3939
}
4040
if (error.request) {
4141
log.debug(`http request failed: ${error.toJSON()}`);
@@ -94,6 +94,7 @@ export default class httpClient {
9494
{ id: buildId, name: buildName, baseline }: Build,
9595
ssPath: string, ssName: string, browserName :string, viewport: string, log: Logger
9696
) {
97+
browserName = browserName === constants.SAFARI ? constants.WEBKIT : browserName;
9798
const file = fs.readFileSync(ssPath);
9899
const form = new FormData();
99100
form.append('screenshot', file, { filename: `${ssName}.png`, contentType: 'image/png'});

src/lib/processSnapshot.ts

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@ import { chromium, Locator } from "@playwright/test"
55
const MAX_RESOURCE_SIZE = 5 * (1024 ** 2); // 5MB
66
var ALLOWED_RESOURCES = ['document', 'stylesheet', 'image', 'media', 'font', 'other'];
77
const ALLOWED_STATUSES = [200, 201];
8+
const REQUEST_TIMEOUT = 10000;
89
const MIN_VIEWPORT_HEIGHT = 1080;
910

1011
export default async (snapshot: Snapshot, ctx: Context): Promise<Record<string, any>> => {
@@ -24,7 +25,7 @@ export default async (snapshot: Snapshot, ctx: Context): Promise<Record<string,
2425
ctx.config.allowedHostnames.push(new URL(snapshot.url).hostname);
2526
if (ctx.config.enableJavaScript) ALLOWED_RESOURCES.push('script');
2627

27-
const response = await page.request.fetch(request);
28+
const response = await page.request.fetch(request, { timeout: REQUEST_TIMEOUT });
2829
const body = await response.body();
2930
if (!body) {
3031
ctx.log.debug(`Handling request ${requestUrl}\n - skipping no response`);

src/lib/screenshot.ts

Lines changed: 114 additions & 83 deletions
Original file line numberDiff line numberDiff line change
@@ -1,99 +1,130 @@
1-
import { Browser } from "@playwright/test"
1+
import { Browser, BrowserContext, Page } from "@playwright/test"
22
import { Context } from "../types.js"
3-
import { delDir, scrollToBottomAndBackToTop, launchBrowsers, closeBrowsers } from "./utils.js"
3+
import * as utils from "./utils.js"
44
import constants from './constants.js'
55
import chalk from 'chalk';
66

7-
8-
export async function captureScreenshots(ctx: Context): Promise<number> {
9-
// Clean up directory to store screenshots
10-
delDir('screenshots');
11-
12-
let browsers: Record<string,Browser> = {};
13-
let capturedScreenshots: number = 0;
7+
async function captureScreenshotsForConfig(
8+
ctx: Context,
9+
browsers: Record<string, Browser>,
10+
{name, url, waitForTimeout}: Record<string, any>,
11+
browserName: string,
12+
renderViewports: Array<Record<string,any>>
13+
): Promise<void> {
1414
let pageOptions = { waitUntil: process.env.SMARTUI_PAGE_WAIT_UNTIL_EVENT || 'load' };
15-
let totalScreenshots: number = ctx.webStaticConfig.length *
16-
(((ctx.config.web?.browsers?.length * ctx.config.web?.viewports?.length) || 0) + (ctx.config.mobile?.devices?.length || 0));
15+
let ssId = name.toLowerCase().replace(/\s/g, '_');
16+
let context: BrowserContext;
17+
let page: Page;
1718

1819
try {
19-
browsers = await launchBrowsers(ctx);
20-
21-
for (let staticConfig of ctx.webStaticConfig) {
22-
let screenshotId = staticConfig.name.toLowerCase().replace(/\s/g, '_');
23-
24-
// capture screenshots for web config
25-
if (ctx.config.web) {
26-
for (let browserName of ctx.config.web.browsers) {
27-
const browser: Browser = browsers[browserName];
28-
const context = await browser.newContext();
29-
const page = await context.newPage();
30-
31-
await page.goto(staticConfig.url.trim(), pageOptions);
32-
for (let { width, height } of ctx.config.web.viewports) {
33-
let ssPath = `screenshots/${screenshotId}/${browserName}-${width}x${height}-${screenshotId}.png`;
34-
await page.setViewportSize({ width, height: height || constants.MIN_VIEWPORT_HEIGHT });
35-
if (height === 0) await page.evaluate(scrollToBottomAndBackToTop);
36-
await page.waitForTimeout(staticConfig.waitForTimeout || 0);
37-
await page.screenshot({ path: ssPath, fullPage: height ? false: true });
38-
39-
browserName = browserName === constants.SAFARI ? constants.PW_WEBKIT : browserName;
40-
await ctx.client.uploadScreenshot(ctx.build, ssPath, staticConfig.name, browserName, `${width}${height ? `x${height}` : ``}`, ctx.log);
41-
capturedScreenshots++;
42-
43-
ctx.task.output = chalk.gray(`screenshots captured: ${capturedScreenshots}/${totalScreenshots}`);
44-
}
45-
46-
await page.close();
47-
await context.close();
48-
}
49-
}
20+
const browser = browsers[browserName];
21+
context = await browser?.newContext();
22+
page = await context?.newPage();
23+
24+
await page?.goto(url.trim(), pageOptions);
25+
for (let { viewport, viewportString, fullPage } of renderViewports) {
26+
let ssPath = `screenshots/${ssId}/${`${browserName}-${viewport.width}x${viewport.height}`}-${ssId}.png`;
27+
await page?.setViewportSize({ width: viewport.width, height: viewport.height || constants.MIN_VIEWPORT_HEIGHT });
28+
if (fullPage) await page?.evaluate(utils.scrollToBottomAndBackToTop);
29+
await page?.waitForTimeout(waitForTimeout || 0);
30+
await page?.screenshot({ path: ssPath, fullPage });
31+
32+
await ctx.client.uploadScreenshot(ctx.build, ssPath, name, browserName, viewportString, ctx.log);
33+
}
34+
} catch (error) {
35+
throw new Error(`captureScreenshotsForConfig failed for browser ${browserName}; error: ${error}`);
36+
} finally {
37+
await page?.close();
38+
await context?.close();
39+
}
40+
41+
}
42+
43+
async function captureScreenshotsAsync(
44+
ctx: Context,
45+
staticConfig: Record<string, any>,
46+
browsers: Record<string, Browser>
47+
): Promise<void[]> {
48+
let capturePromises: Array<Promise<void>> = [];
49+
50+
// capture screenshots for web config
51+
if (ctx.config.web) {
52+
for (let browserName of ctx.config.web.browsers) {
53+
let webRenderViewports = utils.getWebRenderViewports(ctx);
54+
capturePromises.push(captureScreenshotsForConfig(ctx, browsers, staticConfig, browserName, webRenderViewports))
55+
}
56+
}
57+
// capture screenshots for mobile config
58+
if (ctx.config.mobile) {
59+
let mobileRenderViewports = utils.getMobileRenderViewports(ctx);
60+
if (mobileRenderViewports[constants.MOBILE_OS_IOS].length) {
61+
capturePromises.push(captureScreenshotsForConfig(ctx, browsers, staticConfig, constants.SAFARI, mobileRenderViewports[constants.MOBILE_OS_IOS]))
62+
}
63+
if (mobileRenderViewports[constants.MOBILE_OS_ANDROID].length) {
64+
capturePromises.push(captureScreenshotsForConfig(ctx, browsers, staticConfig, constants.CHROME, mobileRenderViewports[constants.MOBILE_OS_ANDROID]))
65+
}
66+
}
5067

51-
// capture screenshots for mobile config
52-
if (ctx.config.mobile) {
53-
let contextChrome = await browsers[constants.CHROME]?.newContext();
54-
let contextSafari = await browsers[constants.SAFARI]?.newContext();
55-
let pageChrome = await contextChrome?.newPage();
56-
let pageSafari = await contextSafari?.newPage();
68+
return Promise.all(capturePromises);
69+
}
70+
71+
async function captureScreenshotsSync(
72+
ctx: Context,
73+
staticConfig: Record<string, any>,
74+
browsers: Record<string, Browser>
75+
): Promise<void> {
76+
// capture screenshots for web config
77+
if (ctx.config.web) {
78+
for (let browserName of ctx.config.web.browsers) {
79+
let webRenderViewports = utils.getWebRenderViewports(ctx);
80+
await captureScreenshotsForConfig(ctx, browsers, staticConfig, browserName, webRenderViewports);
81+
}
82+
}
83+
// capture screenshots for mobile config
84+
if (ctx.config.mobile) {
85+
let mobileRenderViewports = utils.getMobileRenderViewports(ctx);
86+
if (mobileRenderViewports[constants.MOBILE_OS_IOS].length) {
87+
await captureScreenshotsForConfig(ctx, browsers, staticConfig, constants.SAFARI, mobileRenderViewports[constants.MOBILE_OS_IOS]);
88+
}
89+
if (mobileRenderViewports[constants.MOBILE_OS_ANDROID].length) {
90+
await captureScreenshotsForConfig(ctx, browsers, staticConfig, constants.CHROME, mobileRenderViewports[constants.MOBILE_OS_ANDROID]);
91+
}
92+
}
93+
}
5794

58-
await pageChrome?.goto(staticConfig.url.trim(), pageOptions);
59-
await pageSafari?.goto(staticConfig.url.trim(), pageOptions);
60-
for (let device of ctx.config.mobile.devices) {
61-
let ssPath = `screenshots/${screenshotId}/${device.replace(/\s/g, '_')}_${screenshotId}.png`;
62-
let { width, height } = constants.SUPPORTED_MOBILE_DEVICES[device].viewport;
63-
let portrait = (ctx.config.mobile.orientation === constants.MOBILE_ORIENTATION_PORTRAIT) ? true : false;
95+
export async function captureScreenshots(ctx: Context): Promise<Record<string,any>> {
96+
// Clean up directory to store screenshots
97+
utils.delDir('screenshots');
6498

65-
if (constants.SUPPORTED_MOBILE_DEVICES[device].os === constants.MOBILE_OS_ANDROID) {
66-
await pageChrome?.setViewportSize({ width: portrait ? width : height, height: portrait ? height : width });
67-
if (ctx.config.mobile.fullPage) await pageChrome?.evaluate(scrollToBottomAndBackToTop);
68-
await pageChrome?.waitForTimeout(staticConfig.waitForTimeout || 0);
69-
await pageChrome?.screenshot({ path: ssPath, fullPage: ctx.config.mobile.fullPage });
70-
await ctx.client.uploadScreenshot(ctx.build, ssPath, staticConfig.name, constants.CHROME, `${device} (${ctx.config.mobile.orientation})`, ctx.log);
71-
} else {
72-
await pageSafari?.setViewportSize({ width: portrait ? width : height, height: portrait ? height : width });
73-
if (ctx.config.mobile.fullPage) await pageChrome?.evaluate(scrollToBottomAndBackToTop);
74-
await pageSafari?.waitForTimeout(staticConfig.waitForTimeout || 0);
75-
await pageSafari?.screenshot({ path: ssPath, fullPage: ctx.config.mobile.fullPage });
76-
await ctx.client.uploadScreenshot(ctx.build, ssPath, staticConfig.name, constants.PW_WEBKIT, `${device} (${ctx.config.mobile.orientation})`, ctx.log);
77-
}
99+
let browsers: Record<string,Browser> = {};
100+
let capturedScreenshots: number = 0;
101+
let output: string = '';
78102

79-
capturedScreenshots++;
80-
ctx.task.output = chalk.gray(`screenshots captured: ${capturedScreenshots}/${totalScreenshots}`);
81-
}
103+
try {
104+
browsers = await utils.launchBrowsers(ctx);
105+
} catch (error) {
106+
await utils.closeBrowsers(browsers);
107+
ctx.log.debug(error)
108+
throw new Error(`Failed launching browsers`);
109+
}
82110

83-
await pageChrome?.close();
84-
await pageSafari?.close();
85-
await contextChrome?.close();
86-
await contextSafari?.close();
87-
}
111+
for (let staticConfig of ctx.webStaticConfig) {
112+
try {
113+
if (ctx.options.parallel) await captureScreenshotsAsync(ctx, staticConfig, browsers);
114+
else await captureScreenshotsSync(ctx, staticConfig, browsers);
115+
116+
output += (`${chalk.gray(staticConfig.name)} ${chalk.green('\u{2713}')}\n`);
117+
ctx.task.output = output;
118+
capturedScreenshots++;
119+
} catch (error) {
120+
ctx.log.debug(`screenshot capture failed for ${JSON.stringify(staticConfig)}; error: ${error}`);
121+
output += `${chalk.gray(staticConfig.name)} ${chalk.red('\u{2717}')}\n`;
122+
ctx.task.output = output;
88123
}
89-
90-
await closeBrowsers(browsers);
91-
delDir('screenshots');
92-
} catch (error) {
93-
await closeBrowsers(browsers);
94-
delDir('screenshots');
95-
throw error;
96124
}
97125

98-
return capturedScreenshots;
126+
await utils.closeBrowsers(browsers);
127+
utils.delDir('screenshots');
128+
129+
return { capturedScreenshots, output };
99130
}

src/lib/utils.ts

Lines changed: 31 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -76,27 +76,52 @@ export async function closeBrowsers(browsers: Record<string, Browser>): Promise<
7676
for (const browserName of Object.keys(browsers)) await browsers[browserName]?.close();
7777
}
7878

79-
export function getRenderViewports(ctx: Context): Array<Record<string,any>> {
80-
let renderViewports: Array<Record<string,any>> = [];
79+
export function getWebRenderViewports(ctx: Context): Array<Record<string,any>> {
80+
let webRenderViewports: Array<Record<string,any>> = [];
8181

8282
if (ctx.config.web) {
8383
for (const viewport of ctx.config.web.viewports) {
84-
renderViewports.push({
84+
webRenderViewports.push({
8585
viewport,
8686
viewportString: `${viewport.width}${viewport.height ? 'x'+viewport.height : ''}`,
8787
fullPage: viewport.height ? false : true,
88+
device: false
8889
})
8990
}
9091
}
92+
93+
return webRenderViewports
94+
}
95+
96+
export function getMobileRenderViewports(ctx: Context): Record<string,any> {
97+
let mobileRenderViewports: Record<string, Array<Record<string, any>>> = {}
98+
mobileRenderViewports[constants.MOBILE_OS_IOS] = [];
99+
mobileRenderViewports[constants.MOBILE_OS_ANDROID] = [];
100+
91101
if (ctx.config.mobile) {
92102
for (const device of ctx.config.mobile.devices) {
93-
renderViewports.push({
94-
viewport: constants.SUPPORTED_MOBILE_DEVICES[device].viewport,
103+
let os = constants.SUPPORTED_MOBILE_DEVICES[device].os;
104+
let { width, height } = constants.SUPPORTED_MOBILE_DEVICES[device].viewport;
105+
let portrait = (ctx.config.mobile.orientation === constants.MOBILE_ORIENTATION_PORTRAIT) ? true : false;
106+
107+
mobileRenderViewports[os]?.push({
108+
viewport: { width: portrait ? width : height, height: portrait ? height : width },
95109
viewportString: `${device} (${ctx.config.mobile.orientation})`,
96110
fullPage: ctx.config.mobile.fullPage,
111+
device: true,
112+
os: os
97113
})
98114
}
99115
}
100116

101-
return renderViewports;
117+
return mobileRenderViewports
118+
}
119+
120+
export function getRenderViewports(ctx: Context): Array<Record<string,any>> {
121+
let mobileRenderViewports = getMobileRenderViewports(ctx)
122+
return [
123+
...getWebRenderViewports(ctx),
124+
...mobileRenderViewports[constants.MOBILE_OS_IOS],
125+
...mobileRenderViewports[constants.MOBILE_OS_ANDROID]
126+
];
102127
}

src/tasks/captureScreenshots.ts

Lines changed: 7 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -2,18 +2,23 @@ import { ListrTask, ListrRendererFactory } from 'listr2';
22
import { Context } from '../types.js'
33
import { captureScreenshots } from '../lib/screenshot.js'
44
import chalk from 'chalk';
5+
import { updateLogContext } from '../lib/logger.js'
56

67
export default (ctx: Context): ListrTask<Context, ListrRendererFactory, ListrRendererFactory> => {
78
return {
89
title: 'Capturing screenshots',
910
task: async (ctx, task): Promise<void> => {
1011
try {
1112
ctx.task = task;
13+
updateLogContext({task: 'capture'});
1214

13-
let totalScreenshots = await captureScreenshots(ctx);
15+
let { capturedScreenshots, output } = await captureScreenshots(ctx);
16+
if (capturedScreenshots != ctx.webStaticConfig.length) {
17+
throw new Error(output)
18+
}
1419
task.title = 'Screenshots captured successfully'
15-
task.output = chalk.gray(`total screenshots: ${totalScreenshots}`)
1620
} catch (error: any) {
21+
ctx.log.debug(error);
1722
task.output = chalk.gray(`${error.message}`);
1823
throw new Error('Capturing screenshots failed');
1924
}

src/tasks/createBuild.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -21,7 +21,7 @@ export default (ctx: Context): ListrTask<Context, ListrRendererFactory, ListrRen
2121
task.title = 'SmartUI build created'
2222
} catch (error: any) {
2323
ctx.log.debug(error);
24-
task.output = chalk.gray(JSON.parse(error.message).message);
24+
task.output = chalk.gray(error.message);
2525
throw new Error('SmartUI build creation failed');
2626
}
2727
},

0 commit comments

Comments
 (0)