From 1ad02e2b30645a6d387a38942d5e36f0a39b9c5b Mon Sep 17 00:00:00 2001 From: parthkirsan Date: Fri, 7 Nov 2025 16:48:08 +0530 Subject: [PATCH 1/4] add support for rendering errors for static approach --- src/lib/env.ts | 1 + src/lib/httpClient.ts | 5 +- src/lib/screenshot.ts | 156 ++++++++++++++++++++++++++++++++++-------- src/types.ts | 1 + 4 files changed, 132 insertions(+), 31 deletions(-) diff --git a/src/lib/env.ts b/src/lib/env.ts index 4901f49..dc892c7 100644 --- a/src/lib/env.ts +++ b/src/lib/env.ts @@ -57,5 +57,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/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..81ff0b8 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,98 @@ 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); + } + } + + // 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.SAFARI; + } + } + 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 (globalViewport.toLowerCase().includes("iphone") || globalViewport.toLowerCase().includes("ipad")) { + globalBrowser = constants.SAFARI; + } 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 +265,7 @@ async function captureScreenshotsForConfig( await page?.close(); await context?.close(); } - + } async function captureScreenshotsAsync( @@ -185,8 +275,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 +301,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 +320,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 +453,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 +470,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 +494,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 +514,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 +528,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 +550,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 +584,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..c1c9af0 100644 --- a/src/types.ts +++ b/src/types.ts @@ -138,6 +138,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 { From 807e40bd8dc532eed963ac87f005189f370cd84e Mon Sep 17 00:00:00 2001 From: parthkirsan Date: Tue, 18 Nov 2025 13:43:10 +0530 Subject: [PATCH 2/4] create endpoint for buildinfo from local server --- src/lib/server.ts | 29 +++++++++++++++++++++++++++++ 1 file changed, 29 insertions(+) diff --git a/src/lib/server.ts b/src/lib/server.ts index e15bef1..95cb11f 100644 --- a/src/lib/server.ts +++ b/src/lib/server.ts @@ -345,6 +345,35 @@ export default async (ctx: Context): Promise { + let replyCode: number; + let replyBody: Record; + + try { + if (ctx.build && ctx.build.id) { + const buildInfo = ctx.build; + const data = { + buildId: buildInfo.id, + buildName: buildInfo.name, + baseline: buildInfo.baseline, + projectToken: ctx.env.PROJECT_TOKEN || '', + } + replyCode = 200; + replyBody = { data: data }; + } else { + throw new Error('Build information is not available'); + } + } catch (error: any) { + ctx.log.debug(`build info failed; ${error}`); + replyCode = 500; + replyBody = { error: { message: error.message } }; + } + + return reply.code(replyCode).send(replyBody); + + }); + // Use the helper function to find and start server on available port if (ctx.sourceCommand && ctx.sourceCommand === 'exec-start') { From af3cd79569cdaf49f9390f3496dd9b90e1726438 Mon Sep 17 00:00:00 2001 From: parthkirsan Date: Tue, 18 Nov 2025 13:44:48 +0530 Subject: [PATCH 3/4] Revert "create endpoint for buildinfo from local server" This reverts commit 73ac5c5113b4d2d2275bc46ea1c6d8651640de8c. --- src/lib/server.ts | 29 ----------------------------- 1 file changed, 29 deletions(-) diff --git a/src/lib/server.ts b/src/lib/server.ts index 95cb11f..e15bef1 100644 --- a/src/lib/server.ts +++ b/src/lib/server.ts @@ -345,35 +345,6 @@ export default async (ctx: Context): Promise { - let replyCode: number; - let replyBody: Record; - - try { - if (ctx.build && ctx.build.id) { - const buildInfo = ctx.build; - const data = { - buildId: buildInfo.id, - buildName: buildInfo.name, - baseline: buildInfo.baseline, - projectToken: ctx.env.PROJECT_TOKEN || '', - } - replyCode = 200; - replyBody = { data: data }; - } else { - throw new Error('Build information is not available'); - } - } catch (error: any) { - ctx.log.debug(`build info failed; ${error}`); - replyCode = 500; - replyBody = { error: { message: error.message } }; - } - - return reply.code(replyCode).send(replyBody); - - }); - // Use the helper function to find and start server on available port if (ctx.sourceCommand && ctx.sourceCommand === 'exec-start') { From be6bb9c2099bdad0a27d826c895cbf872897d197 Mon Sep 17 00:00:00 2001 From: parthkirsan Date: Tue, 18 Nov 2025 20:03:35 +0530 Subject: [PATCH 4/4] resolved created bugs --- src/lib/screenshot.ts | 12 +++++++++--- 1 file changed, 9 insertions(+), 3 deletions(-) diff --git a/src/lib/screenshot.ts b/src/lib/screenshot.ts index 81ff0b8..b69a0cc 100644 --- a/src/lib/screenshot.ts +++ b/src/lib/screenshot.ts @@ -205,6 +205,8 @@ async function captureScreenshotsForConfig( if (discoveryErrors.browsers[globalBrowser]) { discoveryErrors.browsers[globalBrowser][globalViewport]?.push(data as any); } + + ctx.build.hasDiscoveryError = true; } // Continue the request with the fetched response @@ -225,10 +227,14 @@ async function captureScreenshotsForConfig( globalViewport = first.viewportString; globalBrowser = browserName; if (globalViewport.toLowerCase().includes("iphone") || globalViewport.toLowerCase().includes("ipad")) { - globalBrowser = constants.SAFARI; + 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) @@ -236,8 +242,8 @@ async function captureScreenshotsForConfig( globalViewport = viewportString; globalBrowser = browserName ctx.log.debug(`globalViewport : ${globalViewport}`); - if (globalViewport.toLowerCase().includes("iphone") || globalViewport.toLowerCase().includes("ipad")) { - globalBrowser = constants.SAFARI; + 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 });