diff --git a/package.json b/package.json index f481dcb..ec9d3c7 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "@lambdatest/smartui-cli", - "version": "4.1.45", + "version": "4.1.46", "description": "A command line interface (CLI) to run SmartUI tests on LambdaTest", "files": [ "dist/**/*" diff --git a/src/commander/commander.ts b/src/commander/commander.ts index fb85fb0..e62a162 100644 --- a/src/commander/commander.ts +++ b/src/commander/commander.ts @@ -23,6 +23,7 @@ program .option('--baselineBranch ', 'Mark this build baseline') .option('--baselineBuild ', 'Mark this build baseline') .option('--githubURL ', 'GitHub URL including commitId') + .option('--gitURL ', 'Git URL including commitId') .option('--userName ', 'Specify the LT username') .option('--accessKey ', 'Specify the LT accesskey') .addCommand(exec) diff --git a/src/lib/ctx.ts b/src/lib/ctx.ts index 2c971ce..ae1b194 100644 --- a/src/lib/ctx.ts +++ b/src/lib/ctx.ts @@ -114,6 +114,12 @@ export default (options: Record): Context => { orientation: config.mobile.orientation || constants.MOBILE_ORIENTATION_PORTRAIT, } } + if (env.BASIC_AUTH_USERNAME && env.BASIC_AUTH_PASSWORD) { + basicAuthObj = { + 'username': env.BASIC_AUTH_USERNAME, + 'password': env.BASIC_AUTH_PASSWORD + } + } if (config.basicAuthorization) { basicAuthObj = config.basicAuthorization; } @@ -213,7 +219,7 @@ export default (options: Record): Context => { fetchResultsFileName: fetchResultsFileObj, baselineBranch: options.baselineBranch || '', baselineBuild: options.baselineBuild || '', - githubURL : options.githubURL || '', + githubURL : options.gitURL || options.githubURL || '', showRenderErrors: options.showRenderErrors ? true : false, userName: options.userName || '', accessKey: options.accessKey || '' diff --git a/src/lib/env.ts b/src/lib/env.ts index 4901f49..5cf55d8 100644 --- a/src/lib/env.ts +++ b/src/lib/env.ts @@ -11,7 +11,9 @@ export default (): Env => { HTTPS_PROXY, SMARTUI_HTTP_PROXY, SMARTUI_HTTPS_PROXY, - GITHUB_ACTIONS, + GIT_URL, + BASIC_AUTH_USERNAME, + BASIC_AUTH_PASSWORD, FIGMA_TOKEN, LT_USERNAME, LT_ACCESS_KEY, @@ -39,7 +41,9 @@ export default (): Env => { HTTPS_PROXY, SMARTUI_HTTP_PROXY, SMARTUI_HTTPS_PROXY, - GITHUB_ACTIONS, + GIT_URL, + BASIC_AUTH_USERNAME, + BASIC_AUTH_PASSWORD, FIGMA_TOKEN, LT_USERNAME, LT_ACCESS_KEY, @@ -57,5 +61,6 @@ export default (): Env => { LT_SDK_SKIP_EXECUTION_LOGS: LT_SDK_SKIP_EXECUTION_LOGS === 'true', MAX_CONCURRENT_PROCESSING: MAX_CONCURRENT_PROCESSING ? parseInt(MAX_CONCURRENT_PROCESSING, 10) : 0, DO_NOT_USE_USER_AGENT: DO_NOT_USE_USER_AGENT === 'true', + CAPTURE_RENDERING_ERRORS: process.env.CAPTURE_RENDERING_ERRORS === 'true', } } diff --git a/src/lib/git.ts b/src/lib/git.ts index fc01ec6..3a5989a 100644 --- a/src/lib/git.ts +++ b/src/lib/git.ts @@ -38,6 +38,9 @@ export default (ctx: Context): Git => { if (ctx.options.githubURL && ctx.options.githubURL.startsWith('https://')) { githubURL = ctx.options.githubURL; } + if (ctx.options.gitURL && ctx.options.gitURL.startsWith('https://')) { + githubURL = ctx.options.gitURL; + } if (ctx.env.SMARTUI_GIT_INFO_FILEPATH) { let gitInfo = JSON.parse(fs.readFileSync(ctx.env.SMARTUI_GIT_INFO_FILEPATH, 'utf-8')); @@ -51,7 +54,7 @@ export default (ctx: Context): Git => { commitId: gitInfo.commit_id.slice(0,6) || '', commitMessage: gitInfo.commit_body || '', commitAuthor: gitInfo.commit_author || '', - githubURL: githubURL ? githubURL : (ctx.env.GITHUB_ACTIONS) ? `${constants.GITHUB_API_HOST}/repos/${process.env.GITHUB_REPOSITORY}/statuses/${gitInfo.commit_id}` : '', + githubURL: githubURL ? githubURL : (ctx.env.GIT_URL) ? ctx.env.GIT_URL : `${constants.GITHUB_API_HOST}/repos/${process.env.GITHUB_REPOSITORY}/statuses/${gitInfo.commit_id}`, baselineBranch: ctx.options.baselineBranch || ctx.env.BASELINE_BRANCH || '' } } else { @@ -78,7 +81,7 @@ export default (ctx: Context): Git => { commitId: res[0] || '', commitMessage: res[2] || '', commitAuthor: res[7] || '', - githubURL: githubURL ? githubURL : (ctx.env.GITHUB_ACTIONS) ? `${constants.GITHUB_API_HOST}/repos/${process.env.GITHUB_REPOSITORY}/statuses/${res[1]}` : '', + githubURL: githubURL ? githubURL : (ctx.env.GIT_URL) ? ctx.env.GIT_URL : `${constants.GITHUB_API_HOST}/repos/${process.env.GITHUB_REPOSITORY}/statuses/${res[1]}`, baselineBranch: ctx.options.baselineBranch || ctx.env.BASELINE_BRANCH || '' }; } @@ -95,6 +98,9 @@ function setNonGitInfo(ctx: Context) { if (ctx.options.githubURL && ctx.options.githubURL.startsWith('https://')) { githubURL = ctx.options.githubURL; } + if (ctx.options.gitURL && ctx.options.gitURL.startsWith('https://')) { + githubURL = ctx.options.gitURL; + } ctx.git = { branch: branch, diff --git a/src/lib/httpClient.ts b/src/lib/httpClient.ts index 00de738..89e70ee 100644 --- a/src/lib/httpClient.ts +++ b/src/lib/httpClient.ts @@ -457,7 +457,7 @@ export default class httpClient { uploadScreenshot( { id: buildId, name: buildName, baseline }: Build, - ssPath: string, ssName: string, browserName: string, viewport: string, url: string = '', log: Logger + ssPath: string, ssName: string, browserName: string, viewport: string, url: string = '', log: Logger, discoveryErrors?: DiscoveryErrors, ctx?: Context ) { browserName = browserName === constants.SAFARI ? constants.WEBKIT : browserName; const file = fs.readFileSync(ssPath); @@ -470,6 +470,9 @@ export default class httpClient { form.append('screenshotName', ssName); form.append('baseline', baseline.toString()); form.append('pageUrl',url) + if (ctx?.env.CAPTURE_RENDERING_ERRORS && discoveryErrors) { + form.append('discoveryErrors', JSON.stringify(discoveryErrors)); + } return this.axiosInstance.request({ url: `/screenshot`, diff --git a/src/lib/screenshot.ts b/src/lib/screenshot.ts index 7798314..b69a0cc 100644 --- a/src/lib/screenshot.ts +++ b/src/lib/screenshot.ts @@ -1,7 +1,7 @@ import fs from 'fs'; import path from 'path'; import { Browser, BrowserContext, Page } from "@playwright/test" -import { Context } from "../types.js" +import { Context, DiscoveryErrors } from "../types.js" import * as utils from "./utils.js" import constants from './constants.js' import chalk from 'chalk'; @@ -10,9 +10,9 @@ import sharp from 'sharp'; async function captureScreenshotsForConfig( ctx: Context, browsers: Record, - urlConfig : Record, + urlConfig: Record, browserName: string, - renderViewports: Array> + renderViewports: Array> ): Promise { ctx.log.debug(`*** urlConfig ${JSON.stringify(urlConfig)}`); @@ -22,7 +22,18 @@ async function captureScreenshotsForConfig( let beforeSnapshotScript = execute?.beforeSnapshot; let waitUntilEvent = pageEvent || process.env.SMARTUI_PAGE_WAIT_UNTIL_EVENT || 'load'; - let pageOptions = { waitUntil: waitUntilEvent, timeout: ctx.config.waitForPageRender || constants.DEFAULT_PAGE_LOAD_TIMEOUT}; + let discoveryErrors: DiscoveryErrors = { + name: "", + url: "", + timestamp: "", + snapshotUUID: "", + browsers: {} + }; + + let globalViewport = "" + let globalBrowser = constants.CHROME + + let pageOptions = { waitUntil: waitUntilEvent, timeout: ctx.config.waitForPageRender || constants.DEFAULT_PAGE_LOAD_TIMEOUT }; ctx.log.debug(`url: ${url} pageOptions: ${JSON.stringify(pageOptions)}`); let ssId = name.toLowerCase().replace(/\s/g, '_'); let context: BrowserContext; @@ -106,8 +117,8 @@ async function captureScreenshotsForConfig( else if (browserName == constants.SAFARI) contextOptions.userAgent = constants.SAFARI_USER_AGENT; else if (browserName == constants.EDGE) contextOptions.userAgent = constants.EDGE_USER_AGENT; if (ctx.config.userAgent || userAgent) { - if(ctx.config.userAgent !== ""){ - contextOptions.userAgent = ctx.config.userAgent; + if (ctx.config.userAgent !== "") { + contextOptions.userAgent = ctx.config.userAgent; } if (userAgent && userAgent !== "") { contextOptions.userAgent = userAgent; @@ -155,19 +166,104 @@ async function captureScreenshotsForConfig( await page.setExtraHTTPHeaders(headersObject); } + + if (ctx.env.CAPTURE_RENDERING_ERRORS) { + await page.route('**/*', async (route, request) => { + const requestUrl = request.url() + const requestHostname = new URL(requestUrl).hostname; + let requestOptions: Record = { + timeout: 30000, + headers: { + ...await request.allHeaders(), + ...constants.REQUEST_HEADERS + } + } + + try { + + // get response + let response, body; + response = await page.request.fetch(request, requestOptions); + body = await response.body(); + + let data = { + statusCode: `${response.status()}`, + url: requestUrl, + } + + if ((response.status() >= 400 && response.status() < 600) && response.status() !== 0) { + if (!discoveryErrors.browsers[globalBrowser]) { + discoveryErrors.browsers[globalBrowser] = {}; + } + + // Check if the discoveryErrors.browsers[globalBrowser] exists, and if not, initialize it + if (discoveryErrors.browsers[globalBrowser] && !discoveryErrors.browsers[globalBrowser][globalViewport]) { + discoveryErrors.browsers[globalBrowser][globalViewport] = []; + } + + // Dynamically push the data into the correct browser and viewport + if (discoveryErrors.browsers[globalBrowser]) { + discoveryErrors.browsers[globalBrowser][globalViewport]?.push(data as any); + } + + ctx.build.hasDiscoveryError = true; + } + + // Continue the request with the fetched response + route.fulfill({ + status: response.status(), + headers: response.headers(), + body: body, + }); + } catch (error: any) { + ctx.log.debug(`Handling request ${requestUrl}\n - aborted due to ${error.message}`); + route.abort(); + } + }); + } + + if (renderViewports && renderViewports.length > 0) { + const first = renderViewports[0]; + globalViewport = first.viewportString; + globalBrowser = browserName; + if (globalViewport.toLowerCase().includes("iphone") || globalViewport.toLowerCase().includes("ipad")) { + globalBrowser = constants.WEBKIT; + } + } + + if (browserName == constants.SAFARI || (globalViewport.toLowerCase().includes("iphone") || globalViewport.toLowerCase().includes("ipad"))) { + globalBrowser = constants.WEBKIT; + } + await page?.goto(url.trim(), pageOptions); await executeDocumentScripts(ctx, page, "afterNavigation", afterNavigationScript) for (let { viewport, viewportString, fullPage } of renderViewports) { + globalViewport = viewportString; + globalBrowser = browserName + ctx.log.debug(`globalViewport : ${globalViewport}`); + if (browserName == constants.SAFARI || (globalViewport.toLowerCase().includes("iphone") || globalViewport.toLowerCase().includes("ipad"))) { + globalBrowser = constants.WEBKIT; + } 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 executeDocumentScripts(ctx, page, "beforeSnapshot", beforeSnapshotScript) + discoveryErrors.name = name; + discoveryErrors.url = url; + discoveryErrors.timestamp = new Date().toISOString(); await page?.screenshot({ path: ssPath, fullPage }); - await ctx.client.uploadScreenshot(ctx.build, ssPath, name, browserName, viewportString, url, ctx.log); + await ctx.client.uploadScreenshot(ctx.build, ssPath, name, browserName, viewportString, url, ctx.log, discoveryErrors, ctx); + discoveryErrors = { + name: "", + url: "", + timestamp: "", + snapshotUUID: "", + browsers: {} + }; } } catch (error) { throw new Error(`captureScreenshotsForConfig failed for browser ${browserName}; error: ${error}`); @@ -175,7 +271,7 @@ async function captureScreenshotsForConfig( await page?.close(); await context?.close(); } - + } async function captureScreenshotsAsync( @@ -185,8 +281,8 @@ async function captureScreenshotsAsync( ): Promise { let capturePromises: Array> = []; - // capture screenshots for web config - if (ctx.config.web) { + // 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)) @@ -211,8 +307,8 @@ async function captureScreenshotsSync( staticConfig: Record, browsers: Record ): Promise { - // capture screenshots for web config - if (ctx.config.web) { + // 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); @@ -230,11 +326,11 @@ async function captureScreenshotsSync( } } -export async function captureScreenshots(ctx: Context): Promise> { +export async function captureScreenshots(ctx: Context): Promise> { // Clean up directory to store screenshots utils.delDir('screenshots'); - let browsers: Record = {}; + let browsers: Record = {}; let capturedScreenshots: number = 0; let output: string = ''; @@ -363,7 +459,13 @@ export async function uploadScreenshots(ctx: Context): Promise { } } - await ctx.client.uploadScreenshot(ctx.build, filePath, ssId, 'default', viewport,"", ctx.log); + await ctx.client.uploadScreenshot(ctx.build, filePath, ssId, 'default', viewport, "", ctx.log, { + name: "", + url: "", + timestamp: new Date().toISOString(), + snapshotUUID: "", + browsers: {} + }, ctx); ctx.log.info(`${filePath} : uploaded successfully`) noOfScreenshots++; } else { @@ -374,20 +476,20 @@ export async function uploadScreenshots(ctx: Context): Promise { } await processDirectory(ctx.uploadFilePath); - if(noOfScreenshots == 0){ + if (noOfScreenshots == 0) { ctx.log.info(`No screenshots uploaded.`); } else { ctx.log.info(`${noOfScreenshots} screenshots uploaded successfully.`); } } -export async function captureScreenshotsConcurrent(ctx: Context): Promise> { +export async function captureScreenshotsConcurrent(ctx: Context): Promise> { // Clean up directory to store screenshots utils.delDir('screenshots'); let totalSnapshots = ctx.webStaticConfig && ctx.webStaticConfig.length; let browserInstances = ctx.options.parallel || 1; - let optimizeBrowserInstances : number = 0 + let optimizeBrowserInstances: number = 0 optimizeBrowserInstances = Math.floor(Math.log2(totalSnapshots)); if (optimizeBrowserInstances < 1) { optimizeBrowserInstances = 1; @@ -398,11 +500,11 @@ export async function captureScreenshotsConcurrent(ctx: Context): Promise 1){ + if (ctx.options.force && browserInstances > 1) { optimizeBrowserInstances = browserInstances; } - let urlsPerInstance : number = 0; + let urlsPerInstance: number = 0; if (optimizeBrowserInstances == 1) { urlsPerInstance = totalSnapshots; } else { @@ -418,9 +520,9 @@ export async function captureScreenshotsConcurrent(ctx: Context): Promise { - let { capturedScreenshots, finalOutput} = await processChunk(ctx, urlConfig); + let { capturedScreenshots, finalOutput } = await processChunk(ctx, urlConfig); return { capturedScreenshots, finalOutput }; - })); + })); responses.forEach((response: Record) => { totalCapturedScreenshots += response.capturedScreenshots; @@ -432,17 +534,17 @@ export async function captureScreenshotsConcurrent(ctx: Context): Promise>): Promise> { - - let browsers: Record = {}; +async function processChunk(ctx: Context, urlConfig: Array>): Promise> { + + let browsers: Record = {}; let capturedScreenshots: number = 0; let finalOutput: string = ''; @@ -454,13 +556,13 @@ async function processChunk(ctx: Context, urlConfig: Array>) throw new Error(`Failed launching browsers ${error}`); } - for (let staticConfig of urlConfig) { + for (let staticConfig of urlConfig) { try { await captureScreenshotsAsync(ctx, staticConfig, browsers); utils.delDir(`screenshots/${staticConfig.name.toLowerCase().replace(/\s/g, '_')}`); let output = (`${chalk.gray(staticConfig.name)} ${chalk.green('\u{2713}')}\n`); - ctx.task.output = ctx.task.output? ctx.task.output +output : output; + ctx.task.output = ctx.task.output ? ctx.task.output + output : output; finalOutput += output; capturedScreenshots++; } catch (error) { @@ -488,6 +590,6 @@ async function executeDocumentScripts(ctx: Context, page: Page, actionType: stri } } catch (error) { ctx.log.error(`Error executing script for action ${actionType}: `, error); - throw error; + throw error; } } \ No newline at end of file diff --git a/src/types.ts b/src/types.ts index 2a735b3..c51a82e 100644 --- a/src/types.ts +++ b/src/types.ts @@ -82,6 +82,7 @@ export interface Context { baselineBranch?: string, baselineBuild?: string, githubURL?: string, + gitURL?: string, showRenderErrors?: boolean, userName?: string, accessKey?: string @@ -121,7 +122,9 @@ export interface Env { HTTPS_PROXY: string | undefined; SMARTUI_HTTP_PROXY: string | undefined; SMARTUI_HTTPS_PROXY: string | undefined; - GITHUB_ACTIONS: string | undefined; + GIT_URL: string | undefined; + BASIC_AUTH_USERNAME: string | undefined; + BASIC_AUTH_PASSWORD: string | undefined; FIGMA_TOKEN: string | undefined; LT_USERNAME : string | undefined; LT_ACCESS_KEY : string | undefined; @@ -138,6 +141,7 @@ export interface Env { LT_SDK_SKIP_EXECUTION_LOGS: boolean; MAX_CONCURRENT_PROCESSING: number; DO_NOT_USE_USER_AGENT: boolean; + CAPTURE_RENDERING_ERRORS: boolean; } export interface Snapshot {