From 1df213c524b5a869f5cd102e15f5264831eb6bcc Mon Sep 17 00:00:00 2001 From: shrinishLT Date: Tue, 28 Oct 2025 16:00:19 +0530 Subject: [PATCH 1/8] add logs --- src/lib/screenshot.ts | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/src/lib/screenshot.ts b/src/lib/screenshot.ts index 2c0646de..890d858b 100644 --- a/src/lib/screenshot.ts +++ b/src/lib/screenshot.ts @@ -43,6 +43,7 @@ async function captureScreenshotsForConfig( } try { + ctx.log.debug(`SHRINISH :: contextOptions: ${JSON.stringify(contextOptions)}`); const browser = browsers[browserName]; context = await browser?.newContext(contextOptions); page = await context?.newPage(); @@ -62,6 +63,12 @@ async function captureScreenshotsForConfig( }); } + if (ctx.config.basicAuthorization) { + ctx.log.debug(`Adding basic authorization to the headers for root url`); + let token = Buffer.from(`${ctx.config.basicAuthorization.username}:${ctx.config.basicAuthorization.password}`).toString('base64'); + headersObject['Authorization'] = `Basic ${token}`; + } + ctx.log.debug(`Combined headers: ${JSON.stringify(headersObject)}`); if (Object.keys(headersObject).length > 0) { await page.setExtraHTTPHeaders(headersObject); From 01ea8f28e1b43ccccf9c898296ef99d434e22127 Mon Sep 17 00:00:00 2001 From: shrinishLT Date: Tue, 28 Oct 2025 18:02:44 +0530 Subject: [PATCH 2/8] add beforeNavigation --- src/lib/schemaValidation.ts | 3 +++ src/lib/screenshot.ts | 12 +++++++++++- 2 files changed, 14 insertions(+), 1 deletion(-) diff --git a/src/lib/schemaValidation.ts b/src/lib/schemaValidation.ts index 6b0efe13..ceea5ec8 100644 --- a/src/lib/schemaValidation.ts +++ b/src/lib/schemaValidation.ts @@ -337,6 +337,9 @@ const WebStaticConfigSchema: JSONSchemaType = { execute: { type: "object", properties: { + beforeNavigation: { + type: "string", + }, afterNavigation : { type: "string", }, diff --git a/src/lib/screenshot.ts b/src/lib/screenshot.ts index 890d858b..c80b7b1e 100644 --- a/src/lib/screenshot.ts +++ b/src/lib/screenshot.ts @@ -17,6 +17,7 @@ async function captureScreenshotsForConfig( ctx.log.debug(`*** urlConfig ${JSON.stringify(urlConfig)}`); let {name, url, waitForTimeout, execute, pageEvent, userAgent} = urlConfig; + let beforeNavigationScript = execute?.beforeNavigation; let afterNavigationScript = execute?.afterNavigation; let beforeSnapshotScript = execute?.beforeSnapshot; let waitUntilEvent = pageEvent || process.env.SMARTUI_PAGE_WAIT_UNTIL_EVENT || 'load'; @@ -43,10 +44,19 @@ async function captureScreenshotsForConfig( } try { - ctx.log.debug(`SHRINISH :: contextOptions: ${JSON.stringify(contextOptions)}`); const browser = browsers[browserName]; context = await browser?.newContext(contextOptions); page = await context?.newPage(); + + if (beforeNavigationScript && beforeNavigationScript !== "") { + const wrappedScript = new Function('page', ` + return (async () => { + ${beforeNavigationScript} + })(); + `); + + await wrappedScript(page); + } const headersObject: Record = {}; if (ctx.config.requestHeaders && Array.isArray(ctx.config.requestHeaders)) { ctx.config.requestHeaders.forEach((headerObj) => { From 9741bd7632003c968b6a7e125d00826a19cf067d Mon Sep 17 00:00:00 2001 From: shrinishLT Date: Wed, 29 Oct 2025 13:26:25 +0530 Subject: [PATCH 3/8] DP + tunnel support --- src/lib/ctx.ts | 1 + src/lib/schemaValidation.ts | 4 ++++ src/lib/screenshot.ts | 33 +++++++++++++++++++++++++++++++++ src/types.ts | 1 + 4 files changed, 39 insertions(+) diff --git a/src/lib/ctx.ts b/src/lib/ctx.ts index ef6e2091..80c1d2a2 100644 --- a/src/lib/ctx.ts +++ b/src/lib/ctx.ts @@ -146,6 +146,7 @@ export default (options: Record): Context => { ignoreHTTPSErrors: config.ignoreHTTPSErrors ?? false, skipBuildCreation: config.skipBuildCreation ?? false, tunnel: tunnelObj, + dedicatedProxyURL: config.dedicatedProxyURL || '', userAgent: config.userAgent || '', requestHeaders: config.requestHeaders || {}, allowDuplicateSnapshotNames: allowDuplicateSnapshotNames, diff --git a/src/lib/schemaValidation.ts b/src/lib/schemaValidation.ts index ceea5ec8..56603b4f 100644 --- a/src/lib/schemaValidation.ts +++ b/src/lib/schemaValidation.ts @@ -264,6 +264,10 @@ const ConfigSchema = { uniqueItems: "Invalid config; duplicates in requestHeaders" } }, + dedicatedProxyURL: { + type: "string", + errorMessage: "Invalid config; dedicatedProxyURL must be a string" + }, allowDuplicateSnapshotNames: { type: "boolean", errorMessage: "Invalid config; allowDuplicateSnapshotNames must be true/false" diff --git a/src/lib/screenshot.ts b/src/lib/screenshot.ts index c80b7b1e..e2c51a40 100644 --- a/src/lib/screenshot.ts +++ b/src/lib/screenshot.ts @@ -29,6 +29,39 @@ async function captureScreenshotsForConfig( let contextOptions: Record = { ignoreHTTPSErrors: ctx.config.ignoreHTTPSErrors }; + + // Resolve proxy/tunnel from global config + try { + if (ctx.config.tunnel && ctx.config.tunnel.tunnelName) { + if (ctx.tunnelDetails && ctx.tunnelDetails.tunnelPort != -1 && ctx.tunnelDetails.tunnelHost) { + const tunnelServer = `http://${ctx.tunnelDetails.tunnelHost}:${ctx.tunnelDetails.tunnelPort}`; + ctx.log.info(`URL Capture :: Using tunnel address: ${tunnelServer}`); + contextOptions.proxy = { server: tunnelServer }; + } else { + let tunnelResp = await ctx.client.getTunnelDetails(ctx, ctx.log); + ctx.log.debug(`Tunnel Response: ${JSON.stringify(tunnelResp)}`) + if (tunnelResp && tunnelResp.data && tunnelResp.data.host && tunnelResp.data.port) { + ctx.tunnelDetails = { + tunnelHost: tunnelResp.data.host, + tunnelPort: tunnelResp.data.port, + tunnelName: tunnelResp.data.tunnel_name + } as any; + const tunnelServer = `http://${ctx.tunnelDetails.tunnelHost}:${ctx.tunnelDetails.tunnelPort}`; + ctx.log.info(`URL Capture :: Using tunnel address: ${tunnelServer}`); + contextOptions.proxy = { server: tunnelServer }; + } else if (tunnelResp && tunnelResp.error) { + if (tunnelResp.error.message) { + ctx.log.warn(`Error while fetching tunnel details: ${tunnelResp.error.message}`) + } + } + } + } else if (ctx.config.dedicatedProxyURL && ctx.config.dedicatedProxyURL !== '') { + ctx.log.info(`URL Capture :: Using dedicated proxy: ${ctx.config.dedicatedProxyURL}`); + contextOptions.proxy = { server: ctx.config.dedicatedProxyURL }; + } + } catch (e) { + ctx.log.debug(`Failed resolving tunnel/proxy details: ${e}`); + } let page: Page; if (browserName == constants.CHROME) contextOptions.userAgent = constants.CHROME_USER_AGENT; else if (browserName == constants.FIREFOX) contextOptions.userAgent = constants.FIREFOX_USER_AGENT; diff --git a/src/types.ts b/src/types.ts index feb53f86..daf99009 100644 --- a/src/types.ts +++ b/src/types.ts @@ -34,6 +34,7 @@ export interface Context { ignoreHTTPSErrors : boolean; skipBuildCreation?: boolean; tunnel: tunnelConfig | undefined; + dedicatedProxyURL?: string; userAgent?: string; requestHeaders?: Array>; allowDuplicateSnapshotNames?: boolean; From 65fa17b30894436d7c7c3855341e8b2bb55e095e Mon Sep 17 00:00:00 2001 From: shrinishLT Date: Wed, 29 Oct 2025 19:05:27 +0530 Subject: [PATCH 4/8] logs --- src/lib/screenshot.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/lib/screenshot.ts b/src/lib/screenshot.ts index e2c51a40..5536afa6 100644 --- a/src/lib/screenshot.ts +++ b/src/lib/screenshot.ts @@ -87,7 +87,7 @@ async function captureScreenshotsForConfig( ${beforeNavigationScript} })(); `); - + ctx.log.debug(`Executing before navigation script: ${wrappedScript}`); await wrappedScript(page); } const headersObject: Record = {}; From bb45b1d35341fa98d35de2046b1c0737c4b99478 Mon Sep 17 00:00:00 2001 From: shrinishLT Date: Wed, 29 Oct 2025 23:01:12 +0530 Subject: [PATCH 5/8] fix auth-capture --- src/lib/httpClient.ts | 12 ++++++++++-- src/lib/utils.ts | 2 +- src/tasks/auth.ts | 4 +++- 3 files changed, 14 insertions(+), 4 deletions(-) diff --git a/src/lib/httpClient.ts b/src/lib/httpClient.ts index 4cf8b788..8a7b4ee2 100644 --- a/src/lib/httpClient.ts +++ b/src/lib/httpClient.ts @@ -148,7 +148,7 @@ export default class httpClient { }) } - async auth(log: Logger, env: Env): Promise { + async auth(log: Logger, env: Env): Promise<{ authResult: number, orgId: number, userId: number }> { let result = 1; if (this.projectToken) { result = 0; @@ -162,12 +162,20 @@ export default class httpClient { } }, log); if (response && response.projectToken) { + let orgId = 0; + let userId = 0; this.projectToken = response.projectToken; env.PROJECT_TOKEN = response.projectToken; if (response.message && response.message.includes('Project created successfully')) { result = 2; } - return result; + if (response.orgId) { + orgId = response.orgId + } + if (response.userId) { + userId = response.userId + } + return { authResult : result, orgId, userId }; } else { throw new Error('Authentication failed, project token not received'); } diff --git a/src/lib/utils.ts b/src/lib/utils.ts index 82454997..c43b80b9 100644 --- a/src/lib/utils.ts +++ b/src/lib/utils.ts @@ -458,7 +458,7 @@ export async function startPollingForTunnel(ctx: Context, build_id: string, base export async function stopTunnelHelper(ctx: Context) { ctx.log.debug('stop-tunnel:: Stopping the tunnel now'); - const tunnelRunningStatus = await tunnelInstance.isRunning(); + const tunnelRunningStatus = await tunnelInstance?.isRunning(); ctx.log.debug('stop-tunnel:: Running status of tunnel before stopping ? ' + tunnelRunningStatus); const status = await tunnelInstance.stop(); diff --git a/src/tasks/auth.ts b/src/tasks/auth.ts index 3ca9c1bf..59576d80 100644 --- a/src/tasks/auth.ts +++ b/src/tasks/auth.ts @@ -10,7 +10,7 @@ export default (ctx: Context): ListrTask Date: Tue, 4 Nov 2025 13:52:20 +0530 Subject: [PATCH 6/8] add geolocation support --- src/lib/ctx.ts | 1 + src/lib/httpClient.ts | 13 ++++++++++++ src/lib/schemaValidation.ts | 4 ++++ src/lib/screenshot.ts | 40 ++++++++++++++++++++++++++++++++++++- src/types.ts | 7 +++++++ 5 files changed, 64 insertions(+), 1 deletion(-) diff --git a/src/lib/ctx.ts b/src/lib/ctx.ts index 80c1d2a2..943c4bc9 100644 --- a/src/lib/ctx.ts +++ b/src/lib/ctx.ts @@ -147,6 +147,7 @@ export default (options: Record): Context => { skipBuildCreation: config.skipBuildCreation ?? false, tunnel: tunnelObj, dedicatedProxyURL: config.dedicatedProxyURL || '', + geolocation: config.geolocation || '', userAgent: config.userAgent || '', requestHeaders: config.requestHeaders || {}, allowDuplicateSnapshotNames: allowDuplicateSnapshotNames, diff --git a/src/lib/httpClient.ts b/src/lib/httpClient.ts index 8a7b4ee2..185ddce1 100644 --- a/src/lib/httpClient.ts +++ b/src/lib/httpClient.ts @@ -703,6 +703,19 @@ export default class httpClient { }, ctx.log); } + async getGeolocationProxy(geoLocation: string, log: Logger): Promise<{ data?: { proxy: string, username: string, password: string }, statusCode?: number }> { + try { + const resp = await this.request({ + url: 'https://api-custom-css-dev.lambdatestinternal.com/visualui/1.0/geolocation', + method: 'GET', + params: { geoLocation } + }, log); + return resp; + } catch (error: any) { + this.handleHttpError(error, log); + } + } + async uploadPdf(ctx: Context, form: FormData, buildName?: string): Promise { form.append('projectToken', this.projectToken); if (ctx.build.name !== undefined && ctx.build.name !== '') { diff --git a/src/lib/schemaValidation.ts b/src/lib/schemaValidation.ts index 56603b4f..e29a8bcc 100644 --- a/src/lib/schemaValidation.ts +++ b/src/lib/schemaValidation.ts @@ -268,6 +268,10 @@ const ConfigSchema = { type: "string", errorMessage: "Invalid config; dedicatedProxyURL must be a string" }, + geolocation: { + type: "string", + errorMessage: "Invalid config; geolocation must be a string like 'lat,lon'" + }, allowDuplicateSnapshotNames: { type: "boolean", errorMessage: "Invalid config; allowDuplicateSnapshotNames must be true/false" diff --git a/src/lib/screenshot.ts b/src/lib/screenshot.ts index 5536afa6..7798314e 100644 --- a/src/lib/screenshot.ts +++ b/src/lib/screenshot.ts @@ -30,7 +30,9 @@ async function captureScreenshotsForConfig( ignoreHTTPSErrors: ctx.config.ignoreHTTPSErrors }; - // Resolve proxy/tunnel from global config + + + // Resolve proxy/tunnel/geolocation-proxy from global config try { if (ctx.config.tunnel && ctx.config.tunnel.tunnelName) { if (ctx.tunnelDetails && ctx.tunnelDetails.tunnelPort != -1 && ctx.tunnelDetails.tunnelHost) { @@ -55,10 +57,46 @@ async function captureScreenshotsForConfig( } } } + } else if (ctx.config.geolocation && ctx.config.geolocation !== '') { + // Use cached geolocation proxy if available for the same geolocation key + if (ctx.geolocationData && ctx.geolocationData.proxy && ctx.geolocationData.username && ctx.geolocationData.password && ctx.geolocationData.geoCode === ctx.config.geolocation) { + ctx.log.info(`URL Capture :: Using cached geolocation proxy for ${ctx.config.geolocation}`); + contextOptions.proxy = { + server: ctx.geolocationData.proxy, + username: ctx.geolocationData.username, + password: ctx.geolocationData.password + }; + } else { + const geoResp = await ctx.client.getGeolocationProxy(ctx.config.geolocation, ctx.log); + ctx.log.debug(`Geolocation proxy response: ${JSON.stringify(geoResp)}`); + if (geoResp && geoResp.data && geoResp.data.proxy && geoResp.data.username && geoResp.data.password) { + ctx.log.info(`URL Capture :: Using geolocation proxy for ${ctx.config.geolocation}`); + ctx.geolocationData = { + proxy: geoResp.data.proxy, + username: geoResp.data.username, + password: geoResp.data.password, + geoCode: ctx.config.geolocation + } as any; + contextOptions.proxy = { + server: geoResp.data.proxy, + username: geoResp.data.username, + password: geoResp.data.password + }; + } else { + ctx.log.warn(`Geolocation proxy not available for '${ctx.config.geolocation}', falling back if dedicatedProxyURL present`); + if (ctx.config.dedicatedProxyURL && ctx.config.dedicatedProxyURL !== '') { + ctx.log.info(`URL Capture :: Using dedicated proxy: ${ctx.config.dedicatedProxyURL}`); + contextOptions.proxy = { server: ctx.config.dedicatedProxyURL }; + } + } + } } else if (ctx.config.dedicatedProxyURL && ctx.config.dedicatedProxyURL !== '') { ctx.log.info(`URL Capture :: Using dedicated proxy: ${ctx.config.dedicatedProxyURL}`); contextOptions.proxy = { server: ctx.config.dedicatedProxyURL }; } + + // Note: when using IP-based geolocation via proxy, browser geolocation permission is not required + } catch (e) { ctx.log.debug(`Failed resolving tunnel/proxy details: ${e}`); } diff --git a/src/types.ts b/src/types.ts index daf99009..ad8bd738 100644 --- a/src/types.ts +++ b/src/types.ts @@ -35,6 +35,7 @@ export interface Context { skipBuildCreation?: boolean; tunnel: tunnelConfig | undefined; dedicatedProxyURL?: string; + geolocation?: string; userAgent?: string; requestHeaders?: Array>; allowDuplicateSnapshotNames?: boolean; @@ -58,6 +59,12 @@ export interface Context { tunnelHost: string; tunnelName: string; } + geolocationData?: { + proxy: string; + username: string; + password: string; + geoCode: string; + } options: { parallel?: number, force?: boolean, From 03811d0aeea9eacfa3ba17e9b35b3a946737c8f8 Mon Sep 17 00:00:00 2001 From: shrinishLT Date: Fri, 7 Nov 2025 13:07:18 +0530 Subject: [PATCH 7/8] remove devenv url --- src/lib/httpClient.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/lib/httpClient.ts b/src/lib/httpClient.ts index 185ddce1..afee3b09 100644 --- a/src/lib/httpClient.ts +++ b/src/lib/httpClient.ts @@ -706,7 +706,7 @@ export default class httpClient { async getGeolocationProxy(geoLocation: string, log: Logger): Promise<{ data?: { proxy: string, username: string, password: string }, statusCode?: number }> { try { const resp = await this.request({ - url: 'https://api-custom-css-dev.lambdatestinternal.com/visualui/1.0/geolocation', + url: '/geolocation', method: 'GET', params: { geoLocation } }, log); From cea6dc35c4289fe97966dd22a62e4682ae091550 Mon Sep 17 00:00:00 2001 From: shrinishLT Date: Fri, 7 Nov 2025 13:45:32 +0530 Subject: [PATCH 8/8] version bump --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index 5b30c14b..06332c75 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "@lambdatest/smartui-cli", - "version": "4.1.39", + "version": "4.1.42", "description": "A command line interface (CLI) to run SmartUI tests on LambdaTest", "files": [ "dist/**/*"