From 83479fbb4f8333de33f99109bde9efc5d8cd812b Mon Sep 17 00:00:00 2001 From: Zeeshan Date: Fri, 26 Sep 2025 12:31:38 +0530 Subject: [PATCH 01/27] Add support for custom cookies in snapshot options and enhance schema validation --- src/lib/processSnapshot.ts | 45 ++++++++++++++++++++++++++++++++++--- src/lib/schemaValidation.ts | 8 +++++++ src/types.ts | 11 +++++++++ 3 files changed, 61 insertions(+), 3 deletions(-) diff --git a/src/lib/processSnapshot.ts b/src/lib/processSnapshot.ts index 0fe827d..5aa63f7 100644 --- a/src/lib/processSnapshot.ts +++ b/src/lib/processSnapshot.ts @@ -243,6 +243,47 @@ export default async function processSnapshot(snapshot: Snapshot, ctx: Context): ctx.log.debug('No valid cookies to add'); } } + + let options = snapshot.options; + + // Custom cookies include those which cannot be captured by javascript function `document.cookie` like httpOnly, secure, sameSite etc. + // These custom cookies will be captured by the user in their automation browser and sent to CLI through the snapshot options using `customCookies` field. + if (options?.customCookies && Array.isArray(options.customCookies) && options.customCookies.length > 0) { + ctx.log.debug(`Setting ${options.customCookies.length} custom cookies`); + + const validCustomCookies = options.customCookies.filter(cookie => { + if (!cookie.name || !cookie.value || !cookie.domain) { + ctx.log.debug(`Skipping invalid custom cookie: missing required fields (name, value, or domain)`); + return false; + } + + if (cookie.sameSite && !['Strict', 'Lax', 'None'].includes(cookie.sameSite)) { + ctx.log.debug(`Skipping invalid custom cookie: invalid sameSite value '${cookie.sameSite}'`); + return false; + } + + return true; + }).map(cookie => ({ + name: cookie.name, + value: cookie.value, + domain: cookie.domain, + path: cookie.path || '/', + httpOnly: cookie.httpOnly || false, + secure: cookie.secure || false, + sameSite: cookie.sameSite || 'Lax' + })); + + if (validCustomCookies.length > 0) { + try { + await context.addCookies(validCustomCookies); + ctx.log.debug(`Successfully added ${validCustomCookies.length} custom cookies`); + } catch (error) { + ctx.log.debug(`Failed to add custom cookies: ${error}`); + } + } else { + ctx.log.debug('No valid custom cookies to add'); + } + } const page = await context.newPage(); // populate cache with already captured resources @@ -415,8 +456,6 @@ export default async function processSnapshot(snapshot: Snapshot, ctx: Context): route.abort(); } }); - - let options = snapshot.options; let optionWarnings: Set = new Set(); let selectors: Array = []; let ignoreOrSelectDOM: string; @@ -582,6 +621,7 @@ export default async function processSnapshot(snapshot: Snapshot, ctx: Context): // adding extra timeout since domcontentloaded event is fired pretty quickly await new Promise(r => setTimeout(r, 1250)); if (ctx.config.waitForTimeout) await page.waitForTimeout(ctx.config.waitForTimeout); + await page.waitForLoadState("networkidle", { timeout: 30000 }).catch(() => { ctx.log.debug('networkidle event failed to fire within 30s') }); navigated = true; ctx.log.debug(`Navigated to ${snapshot.url}`); } catch (error: any) { @@ -815,7 +855,6 @@ export default async function processSnapshot(snapshot: Snapshot, ctx: Context): if (hasBrowserErrors) { discoveryErrors.timestamp = new Date().toISOString(); - // ctx.log.warn(discoveryErrors); } if (ctx.config.useGlobalCache) { diff --git a/src/lib/schemaValidation.ts b/src/lib/schemaValidation.ts index 518a404..6b015c4 100644 --- a/src/lib/schemaValidation.ts +++ b/src/lib/schemaValidation.ts @@ -582,6 +582,14 @@ const SnapshotSchema: JSONSchemaType = { minimum: 0, maximum: 100, errorMessage: "Invalid snapshot options; rejectionThreshold must be a number between 0 and 100" + }, + customCookies: { + type: "array", + items: { + type: "object", + minProperties: 1, + }, + errorMessage: "Invalid snapshot options; customCookies must be an array of objects with string properties" } }, additionalProperties: false diff --git a/src/types.ts b/src/types.ts index e647b0c..b02baeb 100644 --- a/src/types.ts +++ b/src/types.ts @@ -164,6 +164,7 @@ export interface Snapshot { useExtendedViewport?: boolean; approvalThreshold?: number; rejectionThreshold?: number; + customCookies?: CustomCookie[]; } } @@ -251,6 +252,16 @@ export interface FigmaWebConfig { configs: Array<{ figma_file_token: string, figma_ids: Array, screenshot_names:Array }>; } +export interface CustomCookie { + name: string; + value: string; + domain: string; + path: string; + httpOnly: boolean; + secure: boolean; + sameSite: 'Strict' | 'Lax' | 'None'; +} + export interface ViewportErrors { statusCode: "aborted" | "404" | string; From ae6971faf75fa916f99138d673c7229c31b6cd41 Mon Sep 17 00:00:00 2001 From: Zeeshan Date: Fri, 26 Sep 2025 12:40:19 +0530 Subject: [PATCH 02/27] Reduce timeout for networkidle event in snapshot processing --- src/lib/processSnapshot.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/lib/processSnapshot.ts b/src/lib/processSnapshot.ts index 5aa63f7..5de3504 100644 --- a/src/lib/processSnapshot.ts +++ b/src/lib/processSnapshot.ts @@ -621,7 +621,7 @@ export default async function processSnapshot(snapshot: Snapshot, ctx: Context): // adding extra timeout since domcontentloaded event is fired pretty quickly await new Promise(r => setTimeout(r, 1250)); if (ctx.config.waitForTimeout) await page.waitForTimeout(ctx.config.waitForTimeout); - await page.waitForLoadState("networkidle", { timeout: 30000 }).catch(() => { ctx.log.debug('networkidle event failed to fire within 30s') }); + await page.waitForLoadState("networkidle", { timeout: 10000 }).catch(() => { ctx.log.debug('networkidle event failed to fire within 10s') }); navigated = true; ctx.log.debug(`Navigated to ${snapshot.url}`); } catch (error: any) { From cece758f8f6152d9f57740d75d2248749322cde1 Mon Sep 17 00:00:00 2001 From: Nick-1234531 Date: Mon, 29 Sep 2025 13:47:48 +0530 Subject: [PATCH 03/27] show rendering errors on terminal --- src/lib/ctx.ts | 6 +- src/lib/env.ts | 8 ++- src/lib/utils.ts | 164 ++++++++++++++++++++++++++++++++++++++++++++++ src/tasks/exec.ts | 10 ++- src/types.ts | 2 + 5 files changed, 184 insertions(+), 6 deletions(-) diff --git a/src/lib/ctx.ts b/src/lib/ctx.ts index f891142..aeb16d2 100644 --- a/src/lib/ctx.ts +++ b/src/lib/ctx.ts @@ -120,9 +120,9 @@ export default (options: Record): Context => { } //if config.waitForPageRender has value and if its less than 30000 then make it to 30000 default - if (config.waitForPageRender && config.waitForPageRender < 30000) { - config.waitForPageRender = 30000; - } + // if (config.waitForPageRender && config.waitForPageRender < 30000) { + // config.waitForPageRender = 30000; + // } return { env: env, diff --git a/src/lib/env.ts b/src/lib/env.ts index 8e8cf1d..2ce8d69 100644 --- a/src/lib/env.ts +++ b/src/lib/env.ts @@ -22,7 +22,9 @@ export default (): Env => { SMARTUI_API_PROXY, SMARTUI_API_SKIP_CERTIFICATES, USE_REMOTE_DISCOVERY, - SMART_GIT + SMART_GIT, + SHOW_RENDER_ERRORS, + SMARTUI_SSE_URL='https://server-events.lambdatest.com' } = process.env return { @@ -46,6 +48,8 @@ export default (): Env => { SMARTUI_API_PROXY, SMARTUI_API_SKIP_CERTIFICATES: SMARTUI_API_SKIP_CERTIFICATES === 'true', USE_REMOTE_DISCOVERY: USE_REMOTE_DISCOVERY === 'true', - SMART_GIT: SMART_GIT === 'true' + SMART_GIT: SMART_GIT === 'true', + SHOW_RENDER_ERRORS: SHOW_RENDER_ERRORS === 'true', + SMARTUI_SSE_URL } } diff --git a/src/lib/utils.ts b/src/lib/utils.ts index 9a88614..4dea965 100644 --- a/src/lib/utils.ts +++ b/src/lib/utils.ts @@ -740,4 +740,168 @@ export function validateCoordinates( valid: true, coords: { top, bottom, left, right } }; +} + +export function createBasicAuthToken(username: string, accessKey: string): string { + const credentials = `${username}:${accessKey}`; + return Buffer.from(credentials).toString('base64'); +} + +export async function listenToSmartUISSE( + baseURL: string, + accessToken: string, + onEvent?: (eventType: string, data: any) => void +): Promise<{ abort: () => void }> { + const url = `${baseURL}/api/v1/sse/smartui`; + + const abortController = new AbortController(); + + try { + const response = await fetch(url, { + method: 'GET', + headers: { + 'Accept': 'text/event-stream', + 'Cache-Control': 'no-cache', + 'Cookie': `stageAccessToken=Basic ${accessToken}` + }, + signal: abortController.signal + }); + + if (!response.ok) { + throw new Error(`HTTP error! status: ${response.status}`); + } + + onEvent?.('open', { status: 'connected' }); + + const reader = response.body?.getReader(); + if (!reader) { + throw new Error('No response body reader available'); + } + + const decoder = new TextDecoder(); + let buffer = ''; + let currentEvent = ''; + + try { + while (true) { + const { done, value } = await reader.read(); + if (done) break; + + const chunk = decoder.decode(value, { stream: true }); + + buffer += chunk; + const lines = buffer.split('\n'); + + buffer = lines.pop() || ''; + + for (const line of lines) { + if (line.startsWith('event:')) { + currentEvent = line.substring(6).trim(); + } + else if (line.startsWith('data:')) { + const data = line.substring(5).trim(); + + if (data) { + try { + const parsedData = JSON.parse(data); + onEvent?.(currentEvent, parsedData); + } catch (parseError) { + if (currentEvent === 'connection' && data === 'connected') { + onEvent?.(currentEvent, { status: 'connected', message: data }); + } else { + onEvent?.(currentEvent, data); + } + } + } + } + else if (line.trim() === '') { + currentEvent = ''; + } + } + } + } catch (streamError: any) { + console.error('SSE Streaming error:', streamError); + onEvent?.('error', streamError); + } finally { + reader.releaseLock(); + } + + } catch (error) { + console.error('SSE Connection error:', error); + onEvent?.('error', error); + } + + return { + abort: () => abortController.abort() + }; +} + +export async function startSSEListener(ctx: Context) { + let retryCount = 0; + const maxRetries = 3; + let currentConnection: { abort: () => void } | null = null; + let errorCount = 0; + + const connectWithRetry = async () => { + try { + ctx.log.debug(`Attempting SSE connection (attempt ${retryCount + 1}/${maxRetries})`); + const accessKey = ctx.env.LT_ACCESS_KEY; + const username = ctx.env.LT_USERNAME; + + const basicAuthToken = createBasicAuthToken(username, accessKey); + ctx.log.debug(`Basic auth token: ${basicAuthToken}`); + currentConnection = await listenToSmartUISSE( + ctx.env.SMARTUI_SSE_URL, + basicAuthToken, + (eventType, data) => { + switch (eventType) { + case 'open': + ctx.log.debug('Connected to SSE server'); + retryCount = 0; + break; + + case 'connection': + ctx.log.debug('Connection confirmed:', data); + retryCount = 0; + break; + + case 'Dot_buildCompleted': + ctx.log.debug('Build completed'); + console.log('Build completed'); + currentConnection?.abort(); + if(errorCount > 0) { + process.exit(1); + } + process.exit(0); + + case 'DOTUIError': + if (data.buildId== ctx.build.id) { + errorCount++; + console.error('Error in build:', data.message); + } + break; + + case 'error': + ctx.log.debug('SSE Error occurred:', data); + currentConnection?.abort(); + process.exit(0); + + } + } + ); + + } catch (error) { + ctx.log.debug(`Failed to start SSE listener (attempt ${retryCount + 1}):`, error); + retryCount++; + + if (retryCount < maxRetries) { + ctx.log.debug(`Retrying in 2 seconds...`); + setTimeout(connectWithRetry, 2000); + } else { + ctx.log.debug('Max retries reached. SSE listener failed.'); + } + } + }; + + connectWithRetry(); } \ No newline at end of file diff --git a/src/tasks/exec.ts b/src/tasks/exec.ts index d65abee..958fe9a 100644 --- a/src/tasks/exec.ts +++ b/src/tasks/exec.ts @@ -3,7 +3,7 @@ import { Context } from '../types.js' import chalk from 'chalk' import spawn from 'cross-spawn' import { updateLogContext } from '../lib/logger.js' -import { startPolling } from '../lib/utils.js' +import { startPolling, startSSEListener } from '../lib/utils.js' export default (ctx: Context): ListrTask => { return { @@ -16,6 +16,14 @@ export default (ctx: Context): ListrTask { diff --git a/src/types.ts b/src/types.ts index e647b0c..8e9c7bc 100644 --- a/src/types.ts +++ b/src/types.ts @@ -120,6 +120,8 @@ export interface Env { SMARTUI_API_SKIP_CERTIFICATES: boolean; USE_REMOTE_DISCOVERY: boolean; SMART_GIT: boolean; + SHOW_RENDER_ERRORS: boolean; + SMARTUI_SSE_URL: string; } export interface Snapshot { From 9d50c7f01dfb60dc65567a47fdf2047160f86bb9 Mon Sep 17 00:00:00 2001 From: Nick-1234531 Date: Mon, 29 Sep 2025 16:05:00 +0530 Subject: [PATCH 04/27] minor change --- src/lib/utils.ts | 107 ++++++++++++++++++++--------------------------- 1 file changed, 46 insertions(+), 61 deletions(-) diff --git a/src/lib/utils.ts b/src/lib/utils.ts index 4dea965..8da1395 100644 --- a/src/lib/utils.ts +++ b/src/lib/utils.ts @@ -837,71 +837,56 @@ export async function listenToSmartUISSE( } export async function startSSEListener(ctx: Context) { - let retryCount = 0; - const maxRetries = 3; let currentConnection: { abort: () => void } | null = null; let errorCount = 0; - const connectWithRetry = async () => { - try { - ctx.log.debug(`Attempting SSE connection (attempt ${retryCount + 1}/${maxRetries})`); - const accessKey = ctx.env.LT_ACCESS_KEY; - const username = ctx.env.LT_USERNAME; - - const basicAuthToken = createBasicAuthToken(username, accessKey); - ctx.log.debug(`Basic auth token: ${basicAuthToken}`); - currentConnection = await listenToSmartUISSE( - ctx.env.SMARTUI_SSE_URL, - basicAuthToken, - (eventType, data) => { - switch (eventType) { - case 'open': - ctx.log.debug('Connected to SSE server'); - retryCount = 0; - break; - - case 'connection': - ctx.log.debug('Connection confirmed:', data); - retryCount = 0; - break; - - case 'Dot_buildCompleted': - ctx.log.debug('Build completed'); - console.log('Build completed'); - currentConnection?.abort(); - if(errorCount > 0) { - process.exit(1); - } - process.exit(0); + try { + ctx.log.debug('Attempting SSE connection'); + const accessKey = ctx.env.LT_ACCESS_KEY; + const username = ctx.env.LT_USERNAME; + + const basicAuthToken = createBasicAuthToken(username, accessKey); + ctx.log.debug(`Basic auth token: ${basicAuthToken}`); + currentConnection = await listenToSmartUISSE( + ctx.env.SMARTUI_SSE_URL, + basicAuthToken, + (eventType, data) => { + switch (eventType) { + case 'open': + ctx.log.debug('Connected to SSE server'); + break; + + case 'connection': + ctx.log.debug('Connection confirmed:', data); + break; + + case 'Dot_buildCompleted': + ctx.log.debug('Build completed'); + console.log('Build completed'); + currentConnection?.abort(); + if(errorCount > 0) { + process.exit(1); + } + process.exit(0); + + case 'DOTUIError': + if (data.buildId== ctx.build.id) { + errorCount++; + console.error('Error in build:', data.message); + } + break; + + case 'error': + ctx.log.debug('SSE Error occurred:', data); + currentConnection?.abort(); + process.exit(0); - case 'DOTUIError': - if (data.buildId== ctx.build.id) { - errorCount++; - console.error('Error in build:', data.message); - } - break; - - case 'error': - ctx.log.debug('SSE Error occurred:', data); - currentConnection?.abort(); - process.exit(0); - - } } - ); - - } catch (error) { - ctx.log.debug(`Failed to start SSE listener (attempt ${retryCount + 1}):`, error); - retryCount++; - - if (retryCount < maxRetries) { - ctx.log.debug(`Retrying in 2 seconds...`); - setTimeout(connectWithRetry, 2000); - } else { - ctx.log.debug('Max retries reached. SSE listener failed.'); } - } - }; - - connectWithRetry(); + ); + + } catch (error) { + ctx.log.debug('Failed to start SSE listener:', error); + throw error; + } } \ No newline at end of file From 9fdc7d8c93304414973e898fa584844a74c0efe2 Mon Sep 17 00:00:00 2001 From: Nick-1234531 Date: Mon, 29 Sep 2025 19:24:45 +0530 Subject: [PATCH 05/27] dont throw error if SSE fails --- src/lib/utils.ts | 1 - 1 file changed, 1 deletion(-) diff --git a/src/lib/utils.ts b/src/lib/utils.ts index 8da1395..44c00f4 100644 --- a/src/lib/utils.ts +++ b/src/lib/utils.ts @@ -887,6 +887,5 @@ export async function startSSEListener(ctx: Context) { } catch (error) { ctx.log.debug('Failed to start SSE listener:', error); - throw error; } } \ No newline at end of file From d8e72e90952c0af28963fe697017e9db7e2444cd Mon Sep 17 00:00:00 2001 From: Nick-1234531 Date: Mon, 29 Sep 2025 20:27:22 +0530 Subject: [PATCH 06/27] remove process.exit() if sse fails --- src/lib/utils.ts | 14 ++++++-------- 1 file changed, 6 insertions(+), 8 deletions(-) diff --git a/src/lib/utils.ts b/src/lib/utils.ts index 44c00f4..1063e9f 100644 --- a/src/lib/utils.ts +++ b/src/lib/utils.ts @@ -750,6 +750,7 @@ export function createBasicAuthToken(username: string, accessKey: string): strin export async function listenToSmartUISSE( baseURL: string, accessToken: string, + ctx: Context, onEvent?: (eventType: string, data: any) => void ): Promise<{ abort: () => void }> { const url = `${baseURL}/api/v1/sse/smartui`; @@ -820,14 +821,14 @@ export async function listenToSmartUISSE( } } } catch (streamError: any) { - console.error('SSE Streaming error:', streamError); + ctx.log.debug('SSE Streaming error:', streamError); onEvent?.('error', streamError); } finally { reader.releaseLock(); } } catch (error) { - console.error('SSE Connection error:', error); + ctx.log.debug('SSE Connection error:', error); onEvent?.('error', error); } @@ -850,6 +851,7 @@ export async function startSSEListener(ctx: Context) { currentConnection = await listenToSmartUISSE( ctx.env.SMARTUI_SSE_URL, basicAuthToken, + ctx, (eventType, data) => { switch (eventType) { case 'open': @@ -864,11 +866,7 @@ export async function startSSEListener(ctx: Context) { ctx.log.debug('Build completed'); console.log('Build completed'); currentConnection?.abort(); - if(errorCount > 0) { - process.exit(1); - } - process.exit(0); - + break; case 'DOTUIError': if (data.buildId== ctx.build.id) { errorCount++; @@ -879,7 +877,7 @@ export async function startSSEListener(ctx: Context) { case 'error': ctx.log.debug('SSE Error occurred:', data); currentConnection?.abort(); - process.exit(0); + break; } } From 638ac9d8020e1f888e48b90df265188c2149acd4 Mon Sep 17 00:00:00 2001 From: parthkirsan Date: Mon, 29 Sep 2025 20:44:50 +0530 Subject: [PATCH 07/27] add support for latest devices --- src/lib/constants.ts | 19 +++++++++++++++++++ 1 file changed, 19 insertions(+) diff --git a/src/lib/constants.ts b/src/lib/constants.ts index ebcb81c..6c002dd 100644 --- a/src/lib/constants.ts +++ b/src/lib/constants.ts @@ -358,6 +358,25 @@ export default { 'Aquos Sense 5G': { os: 'android', viewport: { width: 393, height: 731 } }, 'Xperia 10 IV': { os: 'android', viewport: { width: 412, height: 832 } }, 'Honeywell CT40': { os: 'android', viewport: { width: 360, height: 512 } }, + 'Samsung Galaxy S25': { os: 'android', viewport: { width: 370, height: 802 } }, + 'Samsung Galaxy S25+': { os: 'android', viewport: { width: 393, height: 888 } }, + 'Samsung Galaxy S25 Ultra': { os: 'android', viewport: { width: 432, height: 941 } }, + 'iPhone 17': { os: 'ios', viewport: { width: 393, height: 852 } }, + 'iPhone 17 Pro': { os: 'ios', viewport: { width: 393, height: 852 } }, + 'iPhone 17 Pro Max': { os: 'ios', viewport: { width: 430, height: 932 } }, + 'Samsung Galaxy Z Fold7': { os: 'android', viewport: { width: 373, height: 873 } }, + 'Samsung Galaxy Z Flip7': { os: 'android', viewport: { width: 299, height: 723 } }, + 'Samsung Galaxy Z Fold6': { os: 'android', viewport: { width: 373, height: 873 } }, + 'Samsung Galaxy Z Flip6': { os: 'android', viewport: { width: 298, height: 713 } }, + 'Google Pixel 10 Pro': { os: 'android', viewport: { width: 393, height: 852 } }, + 'Google Pixel 10 Pro XL': { os: 'android', viewport: { width: 412, height: 915 } }, + 'Google Pixel 9': { os: 'android', viewport: { width: 393, height: 852 } }, + 'Google Pixel 9 Pro': { os: 'android', viewport: { width: 393, height: 852 } }, + 'Google Pixel 9 Pro XL': { os: 'android', viewport: { width: 412, height: 915 } }, + 'Motorola Edge 50 Pro': { os: 'android', viewport: { width: 384, height: 864 } }, + 'OnePlus 12': { os: 'android', viewport: { width: 384, height: 884 } }, + 'Nothing Phone 1': { os: 'android', viewport: { width: 393, height: 853 } }, + 'Nothing Phone 2': { os: 'android', viewport: { width: 393, height: 878 } }, }, FIGMA_API: 'https://api.figma.com/v1/', From 13477a1d614afe4ce27e8e928ec6d8f04760e6d5 Mon Sep 17 00:00:00 2001 From: Zeeshan Date: Tue, 30 Sep 2025 16:32:27 +0530 Subject: [PATCH 08/27] Add option to mark baseline in PDF upload and HTTP client --- src/commander/uploadPdf.ts | 1 + src/lib/httpClient.ts | 3 +++ 2 files changed, 4 insertions(+) diff --git a/src/commander/uploadPdf.ts b/src/commander/uploadPdf.ts index 07c319f..2b9eacb 100644 --- a/src/commander/uploadPdf.ts +++ b/src/commander/uploadPdf.ts @@ -14,6 +14,7 @@ command .argument('', 'Path of the directory containing PDFs') .option('--fetch-results [filename]', 'Fetch results and optionally specify an output file, e.g., .json') .option('--buildName ', 'Specify the build name') + .option('--markBaseline', 'Mark this build baseline') .action(async function(directory, _, command) { const options = command.optsWithGlobals(); if (options.buildName === '') { diff --git a/src/lib/httpClient.ts b/src/lib/httpClient.ts index 2df51b6..dea27be 100644 --- a/src/lib/httpClient.ts +++ b/src/lib/httpClient.ts @@ -675,6 +675,9 @@ export default class httpClient { if (ctx.build.name !== undefined && ctx.build.name !== '') { form.append('buildName', buildName); } + if (ctx.options.markBaseline) { + form.append('markBaseline', ctx.options.markBaseline); + } try { const response = await this.axiosInstance.request({ From 180ad5c0a359575061412d46aaa70c2a600e57ae Mon Sep 17 00:00:00 2001 From: Zeeshan Date: Tue, 30 Sep 2025 17:04:58 +0530 Subject: [PATCH 09/27] Changes --- src/lib/httpClient.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/lib/httpClient.ts b/src/lib/httpClient.ts index dea27be..3628328 100644 --- a/src/lib/httpClient.ts +++ b/src/lib/httpClient.ts @@ -676,8 +676,8 @@ export default class httpClient { form.append('buildName', buildName); } if (ctx.options.markBaseline) { - form.append('markBaseline', ctx.options.markBaseline); - } + form.append('markBaseline', ctx.options.markBaseline.toString()); + } try { const response = await this.axiosInstance.request({ From 94e17bb0b5fe1d450e4acf64a6a243be30baafc9 Mon Sep 17 00:00:00 2001 From: Nick-1234531 Date: Tue, 30 Sep 2025 19:24:03 +0530 Subject: [PATCH 10/27] added exit on build complete --- src/lib/utils.ts | 16 +++++++++------- src/tasks/exec.ts | 2 +- 2 files changed, 10 insertions(+), 8 deletions(-) diff --git a/src/lib/utils.ts b/src/lib/utils.ts index 1063e9f..3a53baa 100644 --- a/src/lib/utils.ts +++ b/src/lib/utils.ts @@ -864,20 +864,22 @@ export async function startSSEListener(ctx: Context) { case 'Dot_buildCompleted': ctx.log.debug('Build completed'); - console.log('Build completed'); - currentConnection?.abort(); - break; + ctx.log.info(chalk.green.bold('Build completed')); + process.exit(0); case 'DOTUIError': if (data.buildId== ctx.build.id) { errorCount++; - console.error('Error in build:', data.message); + ctx.log.info(chalk.red.bold(`Error in build: ${data.message}`)); + } + break; + case 'DOTUIWarning': + if (data.buildId== ctx.build.id) { + ctx.log.info(chalk.yellow.bold(`Warning in build: ${data.message}`)); } break; - case 'error': ctx.log.debug('SSE Error occurred:', data); - currentConnection?.abort(); - break; + process.exit(0); } } diff --git a/src/tasks/exec.ts b/src/tasks/exec.ts index 958fe9a..ba4ad0f 100644 --- a/src/tasks/exec.ts +++ b/src/tasks/exec.ts @@ -20,7 +20,7 @@ export default (ctx: Context): ListrTask Date: Thu, 2 Oct 2025 01:43:15 +0530 Subject: [PATCH 11/27] changed log --- src/lib/utils.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/lib/utils.ts b/src/lib/utils.ts index 3a53baa..e80a0b0 100644 --- a/src/lib/utils.ts +++ b/src/lib/utils.ts @@ -869,12 +869,12 @@ export async function startSSEListener(ctx: Context) { case 'DOTUIError': if (data.buildId== ctx.build.id) { errorCount++; - ctx.log.info(chalk.red.bold(`Error in build: ${data.message}`)); + ctx.log.info(chalk.red.bold(`Error: ${data.message}`)); } break; case 'DOTUIWarning': if (data.buildId== ctx.build.id) { - ctx.log.info(chalk.yellow.bold(`Warning in build: ${data.message}`)); + ctx.log.info(chalk.yellow.bold(`Warning: ${data.message}`)); } break; case 'error': From e5d94b0f4a43bf7607b1210e7b65269e2a24570b Mon Sep 17 00:00:00 2001 From: Nick-1234531 Date: Thu, 2 Oct 2025 01:53:23 +0530 Subject: [PATCH 12/27] on error dont exit --- src/lib/utils.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/lib/utils.ts b/src/lib/utils.ts index e80a0b0..6617f01 100644 --- a/src/lib/utils.ts +++ b/src/lib/utils.ts @@ -879,8 +879,8 @@ export async function startSSEListener(ctx: Context) { break; case 'error': ctx.log.debug('SSE Error occurred:', data); - process.exit(0); - + currentConnection?.abort(); + return; } } ); From 71f316997798a9497feb6bdb25ca783998515053 Mon Sep 17 00:00:00 2001 From: Nick-1234531 Date: Fri, 3 Oct 2025 11:34:52 +0530 Subject: [PATCH 13/27] added show render errors in options and config --- src/commander/exec.ts | 1 + src/lib/constants.ts | 3 ++- src/lib/ctx.ts | 4 +++- src/lib/schemaValidation.ts | 4 ++++ src/tasks/exec.ts | 2 +- src/types.ts | 4 +++- 6 files changed, 14 insertions(+), 4 deletions(-) diff --git a/src/commander/exec.ts b/src/commander/exec.ts index ddef0fd..f4911d0 100644 --- a/src/commander/exec.ts +++ b/src/commander/exec.ts @@ -25,6 +25,7 @@ command .option('--scheduled ', 'Specify the schedule ID') .option('--userName ', 'Specify the LT username') .option('--accessKey ', 'Specify the LT accesskey') + .option('--show-render-errors', 'Show render errors from SmartUI build') .action(async function(execCommand, _, command) { const options = command.optsWithGlobals(); if (options.buildName === '') { diff --git a/src/lib/constants.ts b/src/lib/constants.ts index ebcb81c..490879e 100644 --- a/src/lib/constants.ts +++ b/src/lib/constants.ts @@ -25,7 +25,8 @@ export default { waitForTimeout: 1000, enableJavaScript: false, allowedHostnames: [], - smartIgnore: false + smartIgnore: false, + showRenderErrors: false }, DEFAULT_WEB_STATIC_CONFIG: [ { diff --git a/src/lib/ctx.ts b/src/lib/ctx.ts index aeb16d2..c0325c8 100644 --- a/src/lib/ctx.ts +++ b/src/lib/ctx.ts @@ -155,6 +155,7 @@ export default (options: Record): Context => { loadDomContent: loadDomContent, approvalThreshold: config.approvalThreshold, rejectionThreshold: config.rejectionThreshold, + showRenderErrors: config.showRenderErrors ?? false }, uploadFilePath: '', webStaticConfig: [], @@ -192,7 +193,8 @@ export default (options: Record): Context => { fetchResultsFileName: fetchResultsFileObj, baselineBranch: options.baselineBranch || '', baselineBuild: options.baselineBuild || '', - githubURL : options.githubURL || '' + githubURL : options.githubURL || '', + showRenderErrors: options.showRenderErrors ? true : false }, cliVersion: version, totalSnapshots: -1, diff --git a/src/lib/schemaValidation.ts b/src/lib/schemaValidation.ts index 518a404..db33044 100644 --- a/src/lib/schemaValidation.ts +++ b/src/lib/schemaValidation.ts @@ -295,6 +295,10 @@ const ConfigSchema = { minimum: 0, maximum: 100, errorMessage: "Invalid config; rejectionThreshold must be a number" + }, + showRenderErrors: { + type: "boolean", + errorMessage: "Invalid config; showRenderErrors must be true/false" } }, anyOf: [ diff --git a/src/tasks/exec.ts b/src/tasks/exec.ts index ba4ad0f..ecfd9be 100644 --- a/src/tasks/exec.ts +++ b/src/tasks/exec.ts @@ -16,7 +16,7 @@ export default (ctx: Context): ListrTask Date: Fri, 3 Oct 2025 11:46:37 +0530 Subject: [PATCH 14/27] revert --- src/lib/ctx.ts | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/lib/ctx.ts b/src/lib/ctx.ts index c0325c8..ef6e209 100644 --- a/src/lib/ctx.ts +++ b/src/lib/ctx.ts @@ -120,9 +120,9 @@ export default (options: Record): Context => { } //if config.waitForPageRender has value and if its less than 30000 then make it to 30000 default - // if (config.waitForPageRender && config.waitForPageRender < 30000) { - // config.waitForPageRender = 30000; - // } + if (config.waitForPageRender && config.waitForPageRender < 30000) { + config.waitForPageRender = 30000; + } return { env: env, From 2633ee2b91b066a9c889b8687c202d93d120eb1a Mon Sep 17 00:00:00 2001 From: shrinishLT Date: Fri, 3 Oct 2025 23:01:13 +0530 Subject: [PATCH 15/27] fix fetch-pdf-results for smart-ignore --- src/lib/utils.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/lib/utils.ts b/src/lib/utils.ts index 6617f01..f10af98 100644 --- a/src/lib/utils.ts +++ b/src/lib/utils.ts @@ -545,7 +545,7 @@ export function startPdfPolling(ctx: Context) { try { const response = await ctx.client.fetchPdfResults(ctx); - if (response.screenshots) { + if (response.screenshots && response.build?.build_status === constants.BUILD_COMPLETE) { clearInterval(interval); const pdfGroups = groupScreenshotsByPdf(response.screenshots); From 1bd86627ec7df80fa32170b50bcd068aa3b96c33 Mon Sep 17 00:00:00 2001 From: Zeeshan Date: Mon, 6 Oct 2025 17:36:39 +0530 Subject: [PATCH 16/27] Changes --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index 0f31840..0b3a2c4 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "@lambdatest/smartui-cli", - "version": "4.1.34", + "version": "4.1.35", "description": "A command line interface (CLI) to run SmartUI tests on LambdaTest", "files": [ "dist/**/*" From 12e6cac6c82f8e3eaf1db20287bfac14aedbf86f Mon Sep 17 00:00:00 2001 From: parthlambdatest Date: Tue, 7 Oct 2025 17:54:05 +0530 Subject: [PATCH 17/27] Revert "show rendering errors on terminal" --- src/commander/exec.ts | 1 - src/lib/constants.ts | 3 +- src/lib/ctx.ts | 4 +- src/lib/env.ts | 8 +- src/lib/schemaValidation.ts | 4 - src/lib/utils.ts | 148 ------------------------------------ src/tasks/exec.ts | 10 +-- src/types.ts | 6 +- 8 files changed, 6 insertions(+), 178 deletions(-) diff --git a/src/commander/exec.ts b/src/commander/exec.ts index f4911d0..ddef0fd 100644 --- a/src/commander/exec.ts +++ b/src/commander/exec.ts @@ -25,7 +25,6 @@ command .option('--scheduled ', 'Specify the schedule ID') .option('--userName ', 'Specify the LT username') .option('--accessKey ', 'Specify the LT accesskey') - .option('--show-render-errors', 'Show render errors from SmartUI build') .action(async function(execCommand, _, command) { const options = command.optsWithGlobals(); if (options.buildName === '') { diff --git a/src/lib/constants.ts b/src/lib/constants.ts index 490879e..ebcb81c 100644 --- a/src/lib/constants.ts +++ b/src/lib/constants.ts @@ -25,8 +25,7 @@ export default { waitForTimeout: 1000, enableJavaScript: false, allowedHostnames: [], - smartIgnore: false, - showRenderErrors: false + smartIgnore: false }, DEFAULT_WEB_STATIC_CONFIG: [ { diff --git a/src/lib/ctx.ts b/src/lib/ctx.ts index ef6e209..f891142 100644 --- a/src/lib/ctx.ts +++ b/src/lib/ctx.ts @@ -155,7 +155,6 @@ export default (options: Record): Context => { loadDomContent: loadDomContent, approvalThreshold: config.approvalThreshold, rejectionThreshold: config.rejectionThreshold, - showRenderErrors: config.showRenderErrors ?? false }, uploadFilePath: '', webStaticConfig: [], @@ -193,8 +192,7 @@ export default (options: Record): Context => { fetchResultsFileName: fetchResultsFileObj, baselineBranch: options.baselineBranch || '', baselineBuild: options.baselineBuild || '', - githubURL : options.githubURL || '', - showRenderErrors: options.showRenderErrors ? true : false + githubURL : options.githubURL || '' }, cliVersion: version, totalSnapshots: -1, diff --git a/src/lib/env.ts b/src/lib/env.ts index 2ce8d69..8e8cf1d 100644 --- a/src/lib/env.ts +++ b/src/lib/env.ts @@ -22,9 +22,7 @@ export default (): Env => { SMARTUI_API_PROXY, SMARTUI_API_SKIP_CERTIFICATES, USE_REMOTE_DISCOVERY, - SMART_GIT, - SHOW_RENDER_ERRORS, - SMARTUI_SSE_URL='https://server-events.lambdatest.com' + SMART_GIT } = process.env return { @@ -48,8 +46,6 @@ export default (): Env => { SMARTUI_API_PROXY, SMARTUI_API_SKIP_CERTIFICATES: SMARTUI_API_SKIP_CERTIFICATES === 'true', USE_REMOTE_DISCOVERY: USE_REMOTE_DISCOVERY === 'true', - SMART_GIT: SMART_GIT === 'true', - SHOW_RENDER_ERRORS: SHOW_RENDER_ERRORS === 'true', - SMARTUI_SSE_URL + SMART_GIT: SMART_GIT === 'true' } } diff --git a/src/lib/schemaValidation.ts b/src/lib/schemaValidation.ts index 6b0efe1..6b015c4 100644 --- a/src/lib/schemaValidation.ts +++ b/src/lib/schemaValidation.ts @@ -295,10 +295,6 @@ const ConfigSchema = { minimum: 0, maximum: 100, errorMessage: "Invalid config; rejectionThreshold must be a number" - }, - showRenderErrors: { - type: "boolean", - errorMessage: "Invalid config; showRenderErrors must be true/false" } }, anyOf: [ diff --git a/src/lib/utils.ts b/src/lib/utils.ts index f10af98..d6a0c19 100644 --- a/src/lib/utils.ts +++ b/src/lib/utils.ts @@ -740,152 +740,4 @@ export function validateCoordinates( valid: true, coords: { top, bottom, left, right } }; -} - -export function createBasicAuthToken(username: string, accessKey: string): string { - const credentials = `${username}:${accessKey}`; - return Buffer.from(credentials).toString('base64'); -} - -export async function listenToSmartUISSE( - baseURL: string, - accessToken: string, - ctx: Context, - onEvent?: (eventType: string, data: any) => void -): Promise<{ abort: () => void }> { - const url = `${baseURL}/api/v1/sse/smartui`; - - const abortController = new AbortController(); - - try { - const response = await fetch(url, { - method: 'GET', - headers: { - 'Accept': 'text/event-stream', - 'Cache-Control': 'no-cache', - 'Cookie': `stageAccessToken=Basic ${accessToken}` - }, - signal: abortController.signal - }); - - if (!response.ok) { - throw new Error(`HTTP error! status: ${response.status}`); - } - - onEvent?.('open', { status: 'connected' }); - - const reader = response.body?.getReader(); - if (!reader) { - throw new Error('No response body reader available'); - } - - const decoder = new TextDecoder(); - let buffer = ''; - let currentEvent = ''; - - try { - while (true) { - const { done, value } = await reader.read(); - if (done) break; - - const chunk = decoder.decode(value, { stream: true }); - - buffer += chunk; - const lines = buffer.split('\n'); - - buffer = lines.pop() || ''; - - for (const line of lines) { - if (line.startsWith('event:')) { - currentEvent = line.substring(6).trim(); - } - else if (line.startsWith('data:')) { - const data = line.substring(5).trim(); - - if (data) { - try { - const parsedData = JSON.parse(data); - onEvent?.(currentEvent, parsedData); - } catch (parseError) { - if (currentEvent === 'connection' && data === 'connected') { - onEvent?.(currentEvent, { status: 'connected', message: data }); - } else { - onEvent?.(currentEvent, data); - } - } - } - } - else if (line.trim() === '') { - currentEvent = ''; - } - } - } - } catch (streamError: any) { - ctx.log.debug('SSE Streaming error:', streamError); - onEvent?.('error', streamError); - } finally { - reader.releaseLock(); - } - - } catch (error) { - ctx.log.debug('SSE Connection error:', error); - onEvent?.('error', error); - } - - return { - abort: () => abortController.abort() - }; -} - -export async function startSSEListener(ctx: Context) { - let currentConnection: { abort: () => void } | null = null; - let errorCount = 0; - - try { - ctx.log.debug('Attempting SSE connection'); - const accessKey = ctx.env.LT_ACCESS_KEY; - const username = ctx.env.LT_USERNAME; - - const basicAuthToken = createBasicAuthToken(username, accessKey); - ctx.log.debug(`Basic auth token: ${basicAuthToken}`); - currentConnection = await listenToSmartUISSE( - ctx.env.SMARTUI_SSE_URL, - basicAuthToken, - ctx, - (eventType, data) => { - switch (eventType) { - case 'open': - ctx.log.debug('Connected to SSE server'); - break; - - case 'connection': - ctx.log.debug('Connection confirmed:', data); - break; - - case 'Dot_buildCompleted': - ctx.log.debug('Build completed'); - ctx.log.info(chalk.green.bold('Build completed')); - process.exit(0); - case 'DOTUIError': - if (data.buildId== ctx.build.id) { - errorCount++; - ctx.log.info(chalk.red.bold(`Error: ${data.message}`)); - } - break; - case 'DOTUIWarning': - if (data.buildId== ctx.build.id) { - ctx.log.info(chalk.yellow.bold(`Warning: ${data.message}`)); - } - break; - case 'error': - ctx.log.debug('SSE Error occurred:', data); - currentConnection?.abort(); - return; - } - } - ); - - } catch (error) { - ctx.log.debug('Failed to start SSE listener:', error); - } } \ No newline at end of file diff --git a/src/tasks/exec.ts b/src/tasks/exec.ts index ecfd9be..d65abee 100644 --- a/src/tasks/exec.ts +++ b/src/tasks/exec.ts @@ -3,7 +3,7 @@ import { Context } from '../types.js' import chalk from 'chalk' import spawn from 'cross-spawn' import { updateLogContext } from '../lib/logger.js' -import { startPolling, startSSEListener } from '../lib/utils.js' +import { startPolling } from '../lib/utils.js' export default (ctx: Context): ListrTask => { return { @@ -16,14 +16,6 @@ export default (ctx: Context): ListrTask { diff --git a/src/types.ts b/src/types.ts index a0dde3f..b02baeb 100644 --- a/src/types.ts +++ b/src/types.ts @@ -43,7 +43,6 @@ export interface Context { loadDomContent?: boolean; approvalThreshold?: number; rejectionThreshold?: number; - showRenderErrors?: boolean }; uploadFilePath: string; webStaticConfig: WebStaticConfig; @@ -72,8 +71,7 @@ export interface Context { fetchResultsFileName?: string, baselineBranch?: string, baselineBuild?: string, - githubURL?: string, - showRenderErrors?: boolean + githubURL?: string } cliVersion: string; totalSnapshots: number; @@ -122,8 +120,6 @@ export interface Env { SMARTUI_API_SKIP_CERTIFICATES: boolean; USE_REMOTE_DISCOVERY: boolean; SMART_GIT: boolean; - SHOW_RENDER_ERRORS: boolean; - SMARTUI_SSE_URL: string; } export interface Snapshot { From 01062256d009f8a94f10f201904bbde7522104cd Mon Sep 17 00:00:00 2001 From: shrinishLT Date: Wed, 8 Oct 2025 01:44:29 +0530 Subject: [PATCH 18/27] add support of ignoreHTTPSErrors for capture-cmd --- package.json | 2 +- src/lib/screenshot.ts | 4 +++- 2 files changed, 4 insertions(+), 2 deletions(-) diff --git a/package.json b/package.json index 0b3a2c4..7ea7313 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "@lambdatest/smartui-cli", - "version": "4.1.35", + "version": "4.1.36", "description": "A command line interface (CLI) to run SmartUI tests on LambdaTest", "files": [ "dist/**/*" diff --git a/src/lib/screenshot.ts b/src/lib/screenshot.ts index 6c785ed..2c0646d 100644 --- a/src/lib/screenshot.ts +++ b/src/lib/screenshot.ts @@ -25,7 +25,9 @@ async function captureScreenshotsForConfig( ctx.log.debug(`url: ${url} pageOptions: ${JSON.stringify(pageOptions)}`); let ssId = name.toLowerCase().replace(/\s/g, '_'); let context: BrowserContext; - let contextOptions: Record = {}; + let contextOptions: Record = { + ignoreHTTPSErrors: ctx.config.ignoreHTTPSErrors + }; let page: Page; if (browserName == constants.CHROME) contextOptions.userAgent = constants.CHROME_USER_AGENT; else if (browserName == constants.FIREFOX) contextOptions.userAgent = constants.FIREFOX_USER_AGENT; From e463674f85a3e2e1027fbf24f2a56cf76a968698 Mon Sep 17 00:00:00 2001 From: shrinishLT Date: Wed, 8 Oct 2025 19:54:50 +0530 Subject: [PATCH 19/27] fix timeout --- src/lib/processSnapshot.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/lib/processSnapshot.ts b/src/lib/processSnapshot.ts index 5de3504..263141c 100644 --- a/src/lib/processSnapshot.ts +++ b/src/lib/processSnapshot.ts @@ -645,7 +645,7 @@ export default async function processSnapshot(snapshot: Snapshot, ctx: Context): if (ctx.config.cliEnableJavaScript && fullPage) await page.evaluate(scrollToBottomAndBackToTop, { frequency: 100, timing: ctx.config.scrollTime }); try { - await page.waitForLoadState('networkidle', { timeout: 5000 }); + await page.waitForLoadState('networkidle', { timeout: 15000 }); ctx.log.debug('Network idle 500ms'); } catch (error) { ctx.log.debug(`Network idle failed due to ${error}`); From 97c8b889ed27d024edc460a617d1568af3c5ff88 Mon Sep 17 00:00:00 2001 From: shreyanshc Date: Wed, 8 Oct 2025 21:26:37 +0530 Subject: [PATCH 20/27] added a fix for veeva --- src/lib/httpClient.ts | 12 ++++++++---- src/lib/server.ts | 15 +++++++++++++-- src/lib/snapshotQueue.ts | 19 +++++++++++++------ src/types.ts | 2 +- 4 files changed, 35 insertions(+), 13 deletions(-) diff --git a/src/lib/httpClient.ts b/src/lib/httpClient.ts index 3628328..5d43708 100644 --- a/src/lib/httpClient.ts +++ b/src/lib/httpClient.ts @@ -375,7 +375,7 @@ export default class httpClient { }, ctx.log) } - processSnapshotCaps(ctx: Context, snapshot: ProcessedSnapshot, snapshotUuid: string, capsBuildId: string, capsProjectToken: string, discoveryErrors: DiscoveryErrors) { + processSnapshotCaps(ctx: Context, snapshot: ProcessedSnapshot, snapshotUuid: string, capsBuildId: string, capsProjectToken: string, discoveryErrors: DiscoveryErrors, variantCount: number, sync: boolean = false) { return this.request({ url: `/build/${capsBuildId}/snapshot`, method: 'POST', @@ -387,17 +387,19 @@ export default class httpClient { name: snapshot.name, url: snapshot.url, snapshotUuid: snapshotUuid, + variantCount: variantCount, test: { type: ctx.testType, source: 'cli' }, doRemoteDiscovery: snapshot.options.doRemoteDiscovery, discoveryErrors: discoveryErrors, + sync: sync } }, ctx.log) } - uploadSnapshotForCaps(ctx: Context, snapshot: ProcessedSnapshot, capsBuildId: string, capsProjectToken: string, discoveryErrors: DiscoveryErrors) { + uploadSnapshotForCaps(ctx: Context, snapshot: ProcessedSnapshot, capsBuildId: string, capsProjectToken: string, discoveryErrors: DiscoveryErrors, variantCount: number, sync: boolean = false) { // Use capsBuildId if provided, otherwise fallback to ctx.build.id const buildId = capsBuildId !== '' ? capsBuildId : ctx.build.id; @@ -415,6 +417,8 @@ export default class httpClient { source: 'cli' }, discoveryErrors: discoveryErrors, + variantCount: variantCount, + sync: sync } }, ctx.log); } @@ -660,9 +664,9 @@ export default class httpClient { }, ctx.log) } - getSnapshotStatus(snapshotName: string, snapshotUuid: string, ctx: Context): Promise> { + getSnapshotStatus(buildId: string, snapshotName: string, snapshotUuid: string, ctx: Context): Promise> { return this.request({ - url: `/snapshot/status?buildId=${ctx.build.id}&snapshotName=${snapshotName}&snapshotUUID=${snapshotUuid}`, + url: `/snapshot/status?buildId=${buildId}&snapshotName=${snapshotName}&snapshotUUID=${snapshotUuid}`, method: 'GET', headers: { 'Content-Type': 'application/json', diff --git a/src/lib/server.ts b/src/lib/server.ts index cc03eb9..bb0df89 100644 --- a/src/lib/server.ts +++ b/src/lib/server.ts @@ -252,19 +252,29 @@ export default async (ctx: Context): Promise setTimeout(resolve, 5000)); contextStatus = ctx.contextToSnapshotMap.get(contextId); + counter--; } - if(contextStatus==2){ + if(contextStatus==='2'){ throw new Error("Snapshot Failed"); } ctx.log.debug("Snapshot uploaded successfully"); + const buildId = contextStatus; + if (!buildId) { + throw new Error(`No buildId found for contextId: ${contextId}`); + } + // Poll external API until it returns 200 or timeout is reached let lastExternalResponse: any = null; const startTime = Date.now(); @@ -272,6 +282,7 @@ export default async (ctx: Context): Promise; + contextToSnapshotMap?: Map; sourceCommand?: string; autoTunnelStarted?: boolean; } From 00aaa22b50a2c91ea6229df9494cdc290f1434c1 Mon Sep 17 00:00:00 2001 From: shreyanshc Date: Wed, 8 Oct 2025 21:27:26 +0530 Subject: [PATCH 21/27] 4.1.37-beta.0 --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index 7ea7313..b660c3b 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "@lambdatest/smartui-cli", - "version": "4.1.36", + "version": "4.1.37-beta.0", "description": "A command line interface (CLI) to run SmartUI tests on LambdaTest", "files": [ "dist/**/*" From af63e384bb27ec3782751b4c8762a848cb6cba57 Mon Sep 17 00:00:00 2001 From: shreyanshc Date: Wed, 8 Oct 2025 21:44:46 +0530 Subject: [PATCH 22/27] resolved comments --- src/lib/server.ts | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/src/lib/server.ts b/src/lib/server.ts index bb0df89..a286e2f 100644 --- a/src/lib/server.ts +++ b/src/lib/server.ts @@ -118,7 +118,7 @@ export default async (ctx: Context): Promise setTimeout(resolve, 5000)); - - contextStatus = ctx.contextToSnapshotMap.get(contextId); counter--; } From f8ece8f274161235df88dafac5a2aeb4d4d11dcc Mon Sep 17 00:00:00 2001 From: parthkirsan Date: Sun, 12 Oct 2025 19:06:23 +0530 Subject: [PATCH 23/27] update device names --- src/lib/constants.ts | 21 +++++++++------------ 1 file changed, 9 insertions(+), 12 deletions(-) diff --git a/src/lib/constants.ts b/src/lib/constants.ts index 6c002dd..b4f4c05 100644 --- a/src/lib/constants.ts +++ b/src/lib/constants.ts @@ -358,21 +358,18 @@ export default { 'Aquos Sense 5G': { os: 'android', viewport: { width: 393, height: 731 } }, 'Xperia 10 IV': { os: 'android', viewport: { width: 412, height: 832 } }, 'Honeywell CT40': { os: 'android', viewport: { width: 360, height: 512 } }, - 'Samsung Galaxy S25': { os: 'android', viewport: { width: 370, height: 802 } }, - 'Samsung Galaxy S25+': { os: 'android', viewport: { width: 393, height: 888 } }, - 'Samsung Galaxy S25 Ultra': { os: 'android', viewport: { width: 432, height: 941 } }, + 'Galaxy S25': { os: 'android', viewport: { width: 370, height: 802 } }, + 'Galaxy S25+': { os: 'android', viewport: { width: 393, height: 888 } }, + 'Galaxy S25 Ultra': { os: 'android', viewport: { width: 432, height: 941 } }, 'iPhone 17': { os: 'ios', viewport: { width: 393, height: 852 } }, 'iPhone 17 Pro': { os: 'ios', viewport: { width: 393, height: 852 } }, 'iPhone 17 Pro Max': { os: 'ios', viewport: { width: 430, height: 932 } }, - 'Samsung Galaxy Z Fold7': { os: 'android', viewport: { width: 373, height: 873 } }, - 'Samsung Galaxy Z Flip7': { os: 'android', viewport: { width: 299, height: 723 } }, - 'Samsung Galaxy Z Fold6': { os: 'android', viewport: { width: 373, height: 873 } }, - 'Samsung Galaxy Z Flip6': { os: 'android', viewport: { width: 298, height: 713 } }, - 'Google Pixel 10 Pro': { os: 'android', viewport: { width: 393, height: 852 } }, - 'Google Pixel 10 Pro XL': { os: 'android', viewport: { width: 412, height: 915 } }, - 'Google Pixel 9': { os: 'android', viewport: { width: 393, height: 852 } }, - 'Google Pixel 9 Pro': { os: 'android', viewport: { width: 393, height: 852 } }, - 'Google Pixel 9 Pro XL': { os: 'android', viewport: { width: 412, height: 915 } }, + 'Galaxy Z Fold7': { os: 'android', viewport: { width: 373, height: 873 } }, + 'Galaxy Z Flip7': { os: 'android', viewport: { width: 299, height: 723 } }, + 'Galaxy Z Fold6': { os: 'android', viewport: { width: 373, height: 873 } }, + 'Galaxy Z Flip6': { os: 'android', viewport: { width: 298, height: 713 } }, + 'Pixel 10 Pro': { os: 'android', viewport: { width: 393, height: 852 } }, + 'Pixel 10 Pro XL': { os: 'android', viewport: { width: 412, height: 915 } }, 'Motorola Edge 50 Pro': { os: 'android', viewport: { width: 384, height: 864 } }, 'OnePlus 12': { os: 'android', viewport: { width: 384, height: 884 } }, 'Nothing Phone 1': { os: 'android', viewport: { width: 393, height: 853 } }, From 0c9b20206ca4be54aaf406f1cc690bf908511da6 Mon Sep 17 00:00:00 2001 From: shreyanshc Date: Mon, 13 Oct 2025 13:20:03 +0530 Subject: [PATCH 24/27] added fix for mismatch threshold --- src/lib/httpClient.ts | 67 ++++++++++++++++++++++++---------------- src/lib/snapshotQueue.ts | 7 +++-- 2 files changed, 45 insertions(+), 29 deletions(-) diff --git a/src/lib/httpClient.ts b/src/lib/httpClient.ts index 5d43708..bb125b3 100644 --- a/src/lib/httpClient.ts +++ b/src/lib/httpClient.ts @@ -375,7 +375,26 @@ export default class httpClient { }, ctx.log) } - processSnapshotCaps(ctx: Context, snapshot: ProcessedSnapshot, snapshotUuid: string, capsBuildId: string, capsProjectToken: string, discoveryErrors: DiscoveryErrors, variantCount: number, sync: boolean = false) { + processSnapshotCaps(ctx: Context, snapshot: ProcessedSnapshot, snapshotUuid: string, capsBuildId: string, capsProjectToken: string, discoveryErrors: DiscoveryErrors, variantCount: number, sync: boolean = false, approvalThreshold: number| undefined, rejectionThreshold: number| undefined) { + const requestData: any = { + name: snapshot.name, + url: snapshot.url, + snapshotUuid: snapshotUuid, + variantCount: variantCount, + test: { + type: ctx.testType, + source: 'cli' + }, + doRemoteDiscovery: snapshot.options.doRemoteDiscovery, + discoveryErrors: discoveryErrors, + sync: sync + } + if (approvalThreshold !== undefined) { + requestData.approvalThreshold = approvalThreshold; + } + if (rejectionThreshold !== undefined) { + requestData.rejectionThreshold = rejectionThreshold; + } return this.request({ url: `/build/${capsBuildId}/snapshot`, method: 'POST', @@ -383,26 +402,31 @@ export default class httpClient { 'Content-Type': 'application/json', projectToken: capsProjectToken !== '' ? capsProjectToken : this.projectToken }, - data: { - name: snapshot.name, - url: snapshot.url, - snapshotUuid: snapshotUuid, - variantCount: variantCount, - test: { - type: ctx.testType, - source: 'cli' - }, - doRemoteDiscovery: snapshot.options.doRemoteDiscovery, - discoveryErrors: discoveryErrors, - sync: sync - } + data: requestData }, ctx.log) } - uploadSnapshotForCaps(ctx: Context, snapshot: ProcessedSnapshot, capsBuildId: string, capsProjectToken: string, discoveryErrors: DiscoveryErrors, variantCount: number, sync: boolean = false) { + uploadSnapshotForCaps(ctx: Context, snapshot: ProcessedSnapshot, capsBuildId: string, capsProjectToken: string, discoveryErrors: DiscoveryErrors, variantCount: number, sync: boolean = false, approvalThreshold: number| undefined, rejectionThreshold: number| undefined) { // Use capsBuildId if provided, otherwise fallback to ctx.build.id const buildId = capsBuildId !== '' ? capsBuildId : ctx.build.id; - + + const requestData: any = { + snapshot, + test: { + type: ctx.testType, + source: 'cli' + }, + discoveryErrors: discoveryErrors, + variantCount: variantCount, + sync: sync + } + if (approvalThreshold !== undefined) { + requestData.approvalThreshold = approvalThreshold; + } + if (rejectionThreshold !== undefined) { + requestData.rejectionThreshold = rejectionThreshold; + } + return this.request({ url: `/builds/${buildId}/snapshot`, method: 'POST', @@ -410,16 +434,7 @@ export default class httpClient { 'Content-Type': 'application/json', projectToken: capsProjectToken !== '' ? capsProjectToken : this.projectToken // Use capsProjectToken dynamically }, - data: { - snapshot, - test: { - type: ctx.testType, - source: 'cli' - }, - discoveryErrors: discoveryErrors, - variantCount: variantCount, - sync: sync - } + data: requestData }, ctx.log); } diff --git a/src/lib/snapshotQueue.ts b/src/lib/snapshotQueue.ts index 5548675..e36af8c 100644 --- a/src/lib/snapshotQueue.ts +++ b/src/lib/snapshotQueue.ts @@ -360,9 +360,10 @@ export default class Queue { } - if (useCapsBuildId) { this.ctx.log.info(`Using cached buildId: ${capsBuildId}`); + let approvalThreshold = snapshot?.options?.approvalThreshold || this.ctx.config.approvalThreshold; + let rejectionThreshold = snapshot?.options?.rejectionThreshold || this.ctx.config.rejectionThreshold; if (useKafkaFlowCaps) { let snapshotUuid = uuidv4(); if (snapshot?.options?.contextId && this.ctx.contextToSnapshotMap?.has(snapshot.options.contextId)) { @@ -378,9 +379,9 @@ export default class Queue { this.ctx.log.debug(`Uploading dom to S3 for snapshot using LSRS`); await this.ctx.client.sendDomToLSRSForCaps(this.ctx, processedSnapshot, snapshotUuid, capsBuildId, capsProjectToken); } - await this.ctx.client.processSnapshotCaps(this.ctx, processedSnapshot, snapshotUuid, capsBuildId, capsProjectToken, discoveryErrors, calculateVariantCountFromSnapshot(processedSnapshot, this.ctx.config), snapshot?.options?.sync); + await this.ctx.client.processSnapshotCaps(this.ctx, processedSnapshot, snapshotUuid, capsBuildId, capsProjectToken, discoveryErrors, calculateVariantCountFromSnapshot(processedSnapshot, this.ctx.config), snapshot?.options?.sync, approvalThreshold, rejectionThreshold); } else { - await this.ctx.client.uploadSnapshotForCaps(this.ctx, processedSnapshot, capsBuildId, capsProjectToken, discoveryErrors, calculateVariantCountFromSnapshot(processedSnapshot, this.ctx.config), snapshot?.options?.sync); + await this.ctx.client.uploadSnapshotForCaps(this.ctx, processedSnapshot, capsBuildId, capsProjectToken, discoveryErrors, calculateVariantCountFromSnapshot(processedSnapshot, this.ctx.config), snapshot?.options?.sync, approvalThreshold, rejectionThreshold); } // Increment snapshot count for the specific buildId From f9d894d7b047ca3780c1f78ddf3173a1e8435839 Mon Sep 17 00:00:00 2001 From: Nikhil Goyal Date: Mon, 13 Oct 2025 14:56:16 +0530 Subject: [PATCH 25/27] Revert "Revert "show rendering errors on terminal"" --- src/commander/exec.ts | 1 + src/lib/constants.ts | 3 +- src/lib/ctx.ts | 4 +- src/lib/env.ts | 8 +- src/lib/schemaValidation.ts | 4 + src/lib/utils.ts | 148 ++++++++++++++++++++++++++++++++++++ src/tasks/exec.ts | 10 ++- src/types.ts | 6 +- 8 files changed, 178 insertions(+), 6 deletions(-) diff --git a/src/commander/exec.ts b/src/commander/exec.ts index ddef0fd..f4911d0 100644 --- a/src/commander/exec.ts +++ b/src/commander/exec.ts @@ -25,6 +25,7 @@ command .option('--scheduled ', 'Specify the schedule ID') .option('--userName ', 'Specify the LT username') .option('--accessKey ', 'Specify the LT accesskey') + .option('--show-render-errors', 'Show render errors from SmartUI build') .action(async function(execCommand, _, command) { const options = command.optsWithGlobals(); if (options.buildName === '') { diff --git a/src/lib/constants.ts b/src/lib/constants.ts index b4f4c05..6b9e299 100644 --- a/src/lib/constants.ts +++ b/src/lib/constants.ts @@ -25,7 +25,8 @@ export default { waitForTimeout: 1000, enableJavaScript: false, allowedHostnames: [], - smartIgnore: false + smartIgnore: false, + showRenderErrors: false }, DEFAULT_WEB_STATIC_CONFIG: [ { diff --git a/src/lib/ctx.ts b/src/lib/ctx.ts index f891142..ef6e209 100644 --- a/src/lib/ctx.ts +++ b/src/lib/ctx.ts @@ -155,6 +155,7 @@ export default (options: Record): Context => { loadDomContent: loadDomContent, approvalThreshold: config.approvalThreshold, rejectionThreshold: config.rejectionThreshold, + showRenderErrors: config.showRenderErrors ?? false }, uploadFilePath: '', webStaticConfig: [], @@ -192,7 +193,8 @@ export default (options: Record): Context => { fetchResultsFileName: fetchResultsFileObj, baselineBranch: options.baselineBranch || '', baselineBuild: options.baselineBuild || '', - githubURL : options.githubURL || '' + githubURL : options.githubURL || '', + showRenderErrors: options.showRenderErrors ? true : false }, cliVersion: version, totalSnapshots: -1, diff --git a/src/lib/env.ts b/src/lib/env.ts index 8e8cf1d..2ce8d69 100644 --- a/src/lib/env.ts +++ b/src/lib/env.ts @@ -22,7 +22,9 @@ export default (): Env => { SMARTUI_API_PROXY, SMARTUI_API_SKIP_CERTIFICATES, USE_REMOTE_DISCOVERY, - SMART_GIT + SMART_GIT, + SHOW_RENDER_ERRORS, + SMARTUI_SSE_URL='https://server-events.lambdatest.com' } = process.env return { @@ -46,6 +48,8 @@ export default (): Env => { SMARTUI_API_PROXY, SMARTUI_API_SKIP_CERTIFICATES: SMARTUI_API_SKIP_CERTIFICATES === 'true', USE_REMOTE_DISCOVERY: USE_REMOTE_DISCOVERY === 'true', - SMART_GIT: SMART_GIT === 'true' + SMART_GIT: SMART_GIT === 'true', + SHOW_RENDER_ERRORS: SHOW_RENDER_ERRORS === 'true', + SMARTUI_SSE_URL } } diff --git a/src/lib/schemaValidation.ts b/src/lib/schemaValidation.ts index 6b015c4..6b0efe1 100644 --- a/src/lib/schemaValidation.ts +++ b/src/lib/schemaValidation.ts @@ -295,6 +295,10 @@ const ConfigSchema = { minimum: 0, maximum: 100, errorMessage: "Invalid config; rejectionThreshold must be a number" + }, + showRenderErrors: { + type: "boolean", + errorMessage: "Invalid config; showRenderErrors must be true/false" } }, anyOf: [ diff --git a/src/lib/utils.ts b/src/lib/utils.ts index d6a0c19..f10af98 100644 --- a/src/lib/utils.ts +++ b/src/lib/utils.ts @@ -740,4 +740,152 @@ export function validateCoordinates( valid: true, coords: { top, bottom, left, right } }; +} + +export function createBasicAuthToken(username: string, accessKey: string): string { + const credentials = `${username}:${accessKey}`; + return Buffer.from(credentials).toString('base64'); +} + +export async function listenToSmartUISSE( + baseURL: string, + accessToken: string, + ctx: Context, + onEvent?: (eventType: string, data: any) => void +): Promise<{ abort: () => void }> { + const url = `${baseURL}/api/v1/sse/smartui`; + + const abortController = new AbortController(); + + try { + const response = await fetch(url, { + method: 'GET', + headers: { + 'Accept': 'text/event-stream', + 'Cache-Control': 'no-cache', + 'Cookie': `stageAccessToken=Basic ${accessToken}` + }, + signal: abortController.signal + }); + + if (!response.ok) { + throw new Error(`HTTP error! status: ${response.status}`); + } + + onEvent?.('open', { status: 'connected' }); + + const reader = response.body?.getReader(); + if (!reader) { + throw new Error('No response body reader available'); + } + + const decoder = new TextDecoder(); + let buffer = ''; + let currentEvent = ''; + + try { + while (true) { + const { done, value } = await reader.read(); + if (done) break; + + const chunk = decoder.decode(value, { stream: true }); + + buffer += chunk; + const lines = buffer.split('\n'); + + buffer = lines.pop() || ''; + + for (const line of lines) { + if (line.startsWith('event:')) { + currentEvent = line.substring(6).trim(); + } + else if (line.startsWith('data:')) { + const data = line.substring(5).trim(); + + if (data) { + try { + const parsedData = JSON.parse(data); + onEvent?.(currentEvent, parsedData); + } catch (parseError) { + if (currentEvent === 'connection' && data === 'connected') { + onEvent?.(currentEvent, { status: 'connected', message: data }); + } else { + onEvent?.(currentEvent, data); + } + } + } + } + else if (line.trim() === '') { + currentEvent = ''; + } + } + } + } catch (streamError: any) { + ctx.log.debug('SSE Streaming error:', streamError); + onEvent?.('error', streamError); + } finally { + reader.releaseLock(); + } + + } catch (error) { + ctx.log.debug('SSE Connection error:', error); + onEvent?.('error', error); + } + + return { + abort: () => abortController.abort() + }; +} + +export async function startSSEListener(ctx: Context) { + let currentConnection: { abort: () => void } | null = null; + let errorCount = 0; + + try { + ctx.log.debug('Attempting SSE connection'); + const accessKey = ctx.env.LT_ACCESS_KEY; + const username = ctx.env.LT_USERNAME; + + const basicAuthToken = createBasicAuthToken(username, accessKey); + ctx.log.debug(`Basic auth token: ${basicAuthToken}`); + currentConnection = await listenToSmartUISSE( + ctx.env.SMARTUI_SSE_URL, + basicAuthToken, + ctx, + (eventType, data) => { + switch (eventType) { + case 'open': + ctx.log.debug('Connected to SSE server'); + break; + + case 'connection': + ctx.log.debug('Connection confirmed:', data); + break; + + case 'Dot_buildCompleted': + ctx.log.debug('Build completed'); + ctx.log.info(chalk.green.bold('Build completed')); + process.exit(0); + case 'DOTUIError': + if (data.buildId== ctx.build.id) { + errorCount++; + ctx.log.info(chalk.red.bold(`Error: ${data.message}`)); + } + break; + case 'DOTUIWarning': + if (data.buildId== ctx.build.id) { + ctx.log.info(chalk.yellow.bold(`Warning: ${data.message}`)); + } + break; + case 'error': + ctx.log.debug('SSE Error occurred:', data); + currentConnection?.abort(); + return; + } + } + ); + + } catch (error) { + ctx.log.debug('Failed to start SSE listener:', error); + } } \ No newline at end of file diff --git a/src/tasks/exec.ts b/src/tasks/exec.ts index d65abee..ecfd9be 100644 --- a/src/tasks/exec.ts +++ b/src/tasks/exec.ts @@ -3,7 +3,7 @@ import { Context } from '../types.js' import chalk from 'chalk' import spawn from 'cross-spawn' import { updateLogContext } from '../lib/logger.js' -import { startPolling } from '../lib/utils.js' +import { startPolling, startSSEListener } from '../lib/utils.js' export default (ctx: Context): ListrTask => { return { @@ -16,6 +16,14 @@ export default (ctx: Context): ListrTask { diff --git a/src/types.ts b/src/types.ts index c97d1fc..099b1c9 100644 --- a/src/types.ts +++ b/src/types.ts @@ -43,6 +43,7 @@ export interface Context { loadDomContent?: boolean; approvalThreshold?: number; rejectionThreshold?: number; + showRenderErrors?: boolean }; uploadFilePath: string; webStaticConfig: WebStaticConfig; @@ -71,7 +72,8 @@ export interface Context { fetchResultsFileName?: string, baselineBranch?: string, baselineBuild?: string, - githubURL?: string + githubURL?: string, + showRenderErrors?: boolean } cliVersion: string; totalSnapshots: number; @@ -120,6 +122,8 @@ export interface Env { SMARTUI_API_SKIP_CERTIFICATES: boolean; USE_REMOTE_DISCOVERY: boolean; SMART_GIT: boolean; + SHOW_RENDER_ERRORS: boolean; + SMARTUI_SSE_URL: string; } export interface Snapshot { From 4a937215099ab4f3ec68f47e57a2f8a3581624d1 Mon Sep 17 00:00:00 2001 From: parthkirsan Date: Mon, 13 Oct 2025 17:00:17 +0530 Subject: [PATCH 26/27] update galaxy s25 name --- src/lib/constants.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/lib/constants.ts b/src/lib/constants.ts index b4f4c05..83b6a5b 100644 --- a/src/lib/constants.ts +++ b/src/lib/constants.ts @@ -359,7 +359,7 @@ export default { 'Xperia 10 IV': { os: 'android', viewport: { width: 412, height: 832 } }, 'Honeywell CT40': { os: 'android', viewport: { width: 360, height: 512 } }, 'Galaxy S25': { os: 'android', viewport: { width: 370, height: 802 } }, - 'Galaxy S25+': { os: 'android', viewport: { width: 393, height: 888 } }, + 'Galaxy S25 Plus': { os: 'android', viewport: { width: 393, height: 888 } }, 'Galaxy S25 Ultra': { os: 'android', viewport: { width: 432, height: 941 } }, 'iPhone 17': { os: 'ios', viewport: { width: 393, height: 852 } }, 'iPhone 17 Pro': { os: 'ios', viewport: { width: 393, height: 852 } }, From 5f2ed07b9fd17f637747fbd4869af6792fca2011 Mon Sep 17 00:00:00 2001 From: parthkirsan Date: Mon, 13 Oct 2025 17:01:14 +0530 Subject: [PATCH 27/27] version bump --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index 0f31840..50c433e 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "@lambdatest/smartui-cli", - "version": "4.1.34", + "version": "4.1.37", "description": "A command line interface (CLI) to run SmartUI tests on LambdaTest", "files": [ "dist/**/*"