Skip to content

Commit

Permalink
Merge pull request #66 from LambdaTest/stage
Browse files Browse the repository at this point in the history
Release v3.0.1
  • Loading branch information
arushsaxena1998 authored Apr 2, 2024
2 parents 0df6c6c + 88e9650 commit 2d18748
Show file tree
Hide file tree
Showing 11 changed files with 168 additions and 99 deletions.
2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "@lambdatest/smartui-cli",
"version": "3.0.0",
"version": "3.0.1",
"description": "A command line interface (CLI) to run SmartUI tests on LambdaTest",
"files": [
"dist/**/*"
Expand Down
1 change: 1 addition & 0 deletions src/commander/capture.ts
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@ command
.name('capture')
.description('Capture screenshots of static sites')
.argument('<file>', 'Web static config file')
.option('--parallel', 'Capture parallely on all browsers')
.action(async function(file, _, command) {
let ctx: Context = ctxInit(command.optsWithGlobals());

Expand Down
2 changes: 1 addition & 1 deletion src/lib/constants.ts
Original file line number Diff line number Diff line change
Expand Up @@ -44,7 +44,7 @@ export default {
FIREFOX: 'firefox',
EDGE: 'edge',
EDGE_CHANNEL: 'msedge',
PW_WEBKIT: 'webkit',
WEBKIT: 'webkit',

// viewports
MIN_VIEWPORT_HEIGHT: 1080,
Expand Down
6 changes: 4 additions & 2 deletions src/lib/ctx.ts
Original file line number Diff line number Diff line change
Expand Up @@ -69,10 +69,12 @@ export default (options: Record<string, string>): Context => {
id: '',
name: '',
baseline: false,
url: '',
projectId: '',
url: ''
},
args: {},
options: {
parallel: options.parallel ? true : false
},
cliVersion: version,
totalSnapshots: -1
}
Expand Down
5 changes: 3 additions & 2 deletions src/lib/httpClient.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@ import fs from 'fs';
import FormData from 'form-data';
import axios, { AxiosInstance, AxiosRequestConfig } from 'axios';
import { Env, ProcessedSnapshot, Git, Build } from '../types.js';
import { delDir } from './utils.js';
import constants from './constants.js';
import type { Logger } from 'winston'
import pkgJSON from './../../package.json'

Expand Down Expand Up @@ -35,7 +35,7 @@ export default class httpClient {
headers: error.response.headers,
body: error.response.data
})}`);
throw new Error(error.response.data.error.message);
throw new Error(error.response.data.error?.message);
}
if (error.request) {
log.debug(`http request failed: ${error.toJSON()}`);
Expand Down Expand Up @@ -94,6 +94,7 @@ export default class httpClient {
{ id: buildId, name: buildName, baseline }: Build,
ssPath: string, ssName: string, browserName :string, viewport: string, log: Logger
) {
browserName = browserName === constants.SAFARI ? constants.WEBKIT : browserName;
const file = fs.readFileSync(ssPath);
const form = new FormData();
form.append('screenshot', file, { filename: `${ssName}.png`, contentType: 'image/png'});
Expand Down
3 changes: 2 additions & 1 deletion src/lib/processSnapshot.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ import { chromium, Locator } from "@playwright/test"
const MAX_RESOURCE_SIZE = 5 * (1024 ** 2); // 5MB
var ALLOWED_RESOURCES = ['document', 'stylesheet', 'image', 'media', 'font', 'other'];
const ALLOWED_STATUSES = [200, 201];
const REQUEST_TIMEOUT = 10000;
const MIN_VIEWPORT_HEIGHT = 1080;

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

const response = await page.request.fetch(request);
const response = await page.request.fetch(request, { timeout: REQUEST_TIMEOUT });
const body = await response.body();
if (!body) {
ctx.log.debug(`Handling request ${requestUrl}\n - skipping no response`);
Expand Down
197 changes: 114 additions & 83 deletions src/lib/screenshot.ts
Original file line number Diff line number Diff line change
@@ -1,99 +1,130 @@
import { Browser } from "@playwright/test"
import { Browser, BrowserContext, Page } from "@playwright/test"
import { Context } from "../types.js"
import { delDir, scrollToBottomAndBackToTop, launchBrowsers, closeBrowsers } from "./utils.js"
import * as utils from "./utils.js"
import constants from './constants.js'
import chalk from 'chalk';


export async function captureScreenshots(ctx: Context): Promise<number> {
// Clean up directory to store screenshots
delDir('screenshots');

let browsers: Record<string,Browser> = {};
let capturedScreenshots: number = 0;
async function captureScreenshotsForConfig(
ctx: Context,
browsers: Record<string, Browser>,
{name, url, waitForTimeout}: Record<string, any>,
browserName: string,
renderViewports: Array<Record<string,any>>
): Promise<void> {
let pageOptions = { waitUntil: process.env.SMARTUI_PAGE_WAIT_UNTIL_EVENT || 'load' };
let totalScreenshots: number = ctx.webStaticConfig.length *
(((ctx.config.web?.browsers?.length * ctx.config.web?.viewports?.length) || 0) + (ctx.config.mobile?.devices?.length || 0));
let ssId = name.toLowerCase().replace(/\s/g, '_');
let context: BrowserContext;
let page: Page;

try {
browsers = await launchBrowsers(ctx);

for (let staticConfig of ctx.webStaticConfig) {
let screenshotId = staticConfig.name.toLowerCase().replace(/\s/g, '_');

// capture screenshots for web config
if (ctx.config.web) {
for (let browserName of ctx.config.web.browsers) {
const browser: Browser = browsers[browserName];
const context = await browser.newContext();
const page = await context.newPage();

await page.goto(staticConfig.url.trim(), pageOptions);
for (let { width, height } of ctx.config.web.viewports) {
let ssPath = `screenshots/${screenshotId}/${browserName}-${width}x${height}-${screenshotId}.png`;
await page.setViewportSize({ width, height: height || constants.MIN_VIEWPORT_HEIGHT });
if (height === 0) await page.evaluate(scrollToBottomAndBackToTop);
await page.waitForTimeout(staticConfig.waitForTimeout || 0);
await page.screenshot({ path: ssPath, fullPage: height ? false: true });

browserName = browserName === constants.SAFARI ? constants.PW_WEBKIT : browserName;
await ctx.client.uploadScreenshot(ctx.build, ssPath, staticConfig.name, browserName, `${width}${height ? `x${height}` : ``}`, ctx.log);
capturedScreenshots++;

ctx.task.output = chalk.gray(`screenshots captured: ${capturedScreenshots}/${totalScreenshots}`);
}

await page.close();
await context.close();
}
}
const browser = browsers[browserName];
context = await browser?.newContext();
page = await context?.newPage();

await page?.goto(url.trim(), pageOptions);
for (let { viewport, viewportString, fullPage } of renderViewports) {
let ssPath = `screenshots/${ssId}/${`${browserName}-${viewport.width}x${viewport.height}`}-${ssId}.png`;
await page?.setViewportSize({ width: viewport.width, height: viewport.height || constants.MIN_VIEWPORT_HEIGHT });
if (fullPage) await page?.evaluate(utils.scrollToBottomAndBackToTop);
await page?.waitForTimeout(waitForTimeout || 0);
await page?.screenshot({ path: ssPath, fullPage });

await ctx.client.uploadScreenshot(ctx.build, ssPath, name, browserName, viewportString, ctx.log);
}
} catch (error) {
throw new Error(`captureScreenshotsForConfig failed for browser ${browserName}; error: ${error}`);
} finally {
await page?.close();
await context?.close();
}

}

async function captureScreenshotsAsync(
ctx: Context,
staticConfig: Record<string, any>,
browsers: Record<string, Browser>
): Promise<void[]> {
let capturePromises: Array<Promise<void>> = [];

// capture screenshots for web config
if (ctx.config.web) {
for (let browserName of ctx.config.web.browsers) {
let webRenderViewports = utils.getWebRenderViewports(ctx);
capturePromises.push(captureScreenshotsForConfig(ctx, browsers, staticConfig, browserName, webRenderViewports))
}
}
// capture screenshots for mobile config
if (ctx.config.mobile) {
let mobileRenderViewports = utils.getMobileRenderViewports(ctx);
if (mobileRenderViewports[constants.MOBILE_OS_IOS].length) {
capturePromises.push(captureScreenshotsForConfig(ctx, browsers, staticConfig, constants.SAFARI, mobileRenderViewports[constants.MOBILE_OS_IOS]))
}
if (mobileRenderViewports[constants.MOBILE_OS_ANDROID].length) {
capturePromises.push(captureScreenshotsForConfig(ctx, browsers, staticConfig, constants.CHROME, mobileRenderViewports[constants.MOBILE_OS_ANDROID]))
}
}

// capture screenshots for mobile config
if (ctx.config.mobile) {
let contextChrome = await browsers[constants.CHROME]?.newContext();
let contextSafari = await browsers[constants.SAFARI]?.newContext();
let pageChrome = await contextChrome?.newPage();
let pageSafari = await contextSafari?.newPage();
return Promise.all(capturePromises);
}

async function captureScreenshotsSync(
ctx: Context,
staticConfig: Record<string, any>,
browsers: Record<string, Browser>
): Promise<void> {
// capture screenshots for web config
if (ctx.config.web) {
for (let browserName of ctx.config.web.browsers) {
let webRenderViewports = utils.getWebRenderViewports(ctx);
await captureScreenshotsForConfig(ctx, browsers, staticConfig, browserName, webRenderViewports);
}
}
// capture screenshots for mobile config
if (ctx.config.mobile) {
let mobileRenderViewports = utils.getMobileRenderViewports(ctx);
if (mobileRenderViewports[constants.MOBILE_OS_IOS].length) {
await captureScreenshotsForConfig(ctx, browsers, staticConfig, constants.SAFARI, mobileRenderViewports[constants.MOBILE_OS_IOS]);
}
if (mobileRenderViewports[constants.MOBILE_OS_ANDROID].length) {
await captureScreenshotsForConfig(ctx, browsers, staticConfig, constants.CHROME, mobileRenderViewports[constants.MOBILE_OS_ANDROID]);
}
}
}

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

if (constants.SUPPORTED_MOBILE_DEVICES[device].os === constants.MOBILE_OS_ANDROID) {
await pageChrome?.setViewportSize({ width: portrait ? width : height, height: portrait ? height : width });
if (ctx.config.mobile.fullPage) await pageChrome?.evaluate(scrollToBottomAndBackToTop);
await pageChrome?.waitForTimeout(staticConfig.waitForTimeout || 0);
await pageChrome?.screenshot({ path: ssPath, fullPage: ctx.config.mobile.fullPage });
await ctx.client.uploadScreenshot(ctx.build, ssPath, staticConfig.name, constants.CHROME, `${device} (${ctx.config.mobile.orientation})`, ctx.log);
} else {
await pageSafari?.setViewportSize({ width: portrait ? width : height, height: portrait ? height : width });
if (ctx.config.mobile.fullPage) await pageChrome?.evaluate(scrollToBottomAndBackToTop);
await pageSafari?.waitForTimeout(staticConfig.waitForTimeout || 0);
await pageSafari?.screenshot({ path: ssPath, fullPage: ctx.config.mobile.fullPage });
await ctx.client.uploadScreenshot(ctx.build, ssPath, staticConfig.name, constants.PW_WEBKIT, `${device} (${ctx.config.mobile.orientation})`, ctx.log);
}
let browsers: Record<string,Browser> = {};
let capturedScreenshots: number = 0;
let output: string = '';

capturedScreenshots++;
ctx.task.output = chalk.gray(`screenshots captured: ${capturedScreenshots}/${totalScreenshots}`);
}
try {
browsers = await utils.launchBrowsers(ctx);
} catch (error) {
await utils.closeBrowsers(browsers);
ctx.log.debug(error)
throw new Error(`Failed launching browsers`);
}

await pageChrome?.close();
await pageSafari?.close();
await contextChrome?.close();
await contextSafari?.close();
}
for (let staticConfig of ctx.webStaticConfig) {
try {
if (ctx.options.parallel) await captureScreenshotsAsync(ctx, staticConfig, browsers);
else await captureScreenshotsSync(ctx, staticConfig, browsers);

output += (`${chalk.gray(staticConfig.name)} ${chalk.green('\u{2713}')}\n`);
ctx.task.output = output;
capturedScreenshots++;
} catch (error) {
ctx.log.debug(`screenshot capture failed for ${JSON.stringify(staticConfig)}; error: ${error}`);
output += `${chalk.gray(staticConfig.name)} ${chalk.red('\u{2717}')}\n`;
ctx.task.output = output;
}

await closeBrowsers(browsers);
delDir('screenshots');
} catch (error) {
await closeBrowsers(browsers);
delDir('screenshots');
throw error;
}

return capturedScreenshots;
await utils.closeBrowsers(browsers);
utils.delDir('screenshots');

return { capturedScreenshots, output };
}
37 changes: 31 additions & 6 deletions src/lib/utils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -76,27 +76,52 @@ export async function closeBrowsers(browsers: Record<string, Browser>): Promise<
for (const browserName of Object.keys(browsers)) await browsers[browserName]?.close();
}

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

if (ctx.config.web) {
for (const viewport of ctx.config.web.viewports) {
renderViewports.push({
webRenderViewports.push({
viewport,
viewportString: `${viewport.width}${viewport.height ? 'x'+viewport.height : ''}`,
fullPage: viewport.height ? false : true,
device: false
})
}
}

return webRenderViewports
}

export function getMobileRenderViewports(ctx: Context): Record<string,any> {
let mobileRenderViewports: Record<string, Array<Record<string, any>>> = {}
mobileRenderViewports[constants.MOBILE_OS_IOS] = [];
mobileRenderViewports[constants.MOBILE_OS_ANDROID] = [];

if (ctx.config.mobile) {
for (const device of ctx.config.mobile.devices) {
renderViewports.push({
viewport: constants.SUPPORTED_MOBILE_DEVICES[device].viewport,
let os = constants.SUPPORTED_MOBILE_DEVICES[device].os;
let { width, height } = constants.SUPPORTED_MOBILE_DEVICES[device].viewport;
let portrait = (ctx.config.mobile.orientation === constants.MOBILE_ORIENTATION_PORTRAIT) ? true : false;

mobileRenderViewports[os]?.push({
viewport: { width: portrait ? width : height, height: portrait ? height : width },
viewportString: `${device} (${ctx.config.mobile.orientation})`,
fullPage: ctx.config.mobile.fullPage,
device: true,
os: os
})
}
}

return renderViewports;
return mobileRenderViewports
}

export function getRenderViewports(ctx: Context): Array<Record<string,any>> {
let mobileRenderViewports = getMobileRenderViewports(ctx)
return [
...getWebRenderViewports(ctx),
...mobileRenderViewports[constants.MOBILE_OS_IOS],
...mobileRenderViewports[constants.MOBILE_OS_ANDROID]
];
}
9 changes: 7 additions & 2 deletions src/tasks/captureScreenshots.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,18 +2,23 @@ import { ListrTask, ListrRendererFactory } from 'listr2';
import { Context } from '../types.js'
import { captureScreenshots } from '../lib/screenshot.js'
import chalk from 'chalk';
import { updateLogContext } from '../lib/logger.js'

export default (ctx: Context): ListrTask<Context, ListrRendererFactory, ListrRendererFactory> => {
return {
title: 'Capturing screenshots',
task: async (ctx, task): Promise<void> => {
try {
ctx.task = task;
updateLogContext({task: 'capture'});

let totalScreenshots = await captureScreenshots(ctx);
let { capturedScreenshots, output } = await captureScreenshots(ctx);
if (capturedScreenshots != ctx.webStaticConfig.length) {
throw new Error(output)
}
task.title = 'Screenshots captured successfully'
task.output = chalk.gray(`total screenshots: ${totalScreenshots}`)
} catch (error: any) {
ctx.log.debug(error);
task.output = chalk.gray(`${error.message}`);
throw new Error('Capturing screenshots failed');
}
Expand Down
2 changes: 1 addition & 1 deletion src/tasks/createBuild.ts
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,7 @@ export default (ctx: Context): ListrTask<Context, ListrRendererFactory, ListrRen
task.title = 'SmartUI build created'
} catch (error: any) {
ctx.log.debug(error);
task.output = chalk.gray(JSON.parse(error.message).message);
task.output = chalk.gray(error.message);
throw new Error('SmartUI build creation failed');
}
},
Expand Down
Loading

0 comments on commit 2d18748

Please sign in to comment.