Skip to content

Commit 234ab8c

Browse files
Merge pull request #430 from LambdaTest/stage
Release 8th Nov
2 parents a3467a1 + 89a6699 commit 234ab8c

File tree

9 files changed

+144
-16
lines changed

9 files changed

+144
-16
lines changed

package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
{
22
"name": "@lambdatest/smartui-cli",
3-
"version": "4.1.42-beta.0",
3+
"version": "4.1.42",
44
"description": "A command line interface (CLI) to run SmartUI tests on LambdaTest",
55
"files": [
66
"dist/**/*"

src/lib/ctx.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -163,6 +163,8 @@ export default (options: Record<string, string>): Context => {
163163
ignoreHTTPSErrors: config.ignoreHTTPSErrors ?? false,
164164
skipBuildCreation: config.skipBuildCreation ?? false,
165165
tunnel: tunnelObj,
166+
dedicatedProxyURL: config.dedicatedProxyURL || '',
167+
geolocation: config.geolocation || '',
166168
userAgent: config.userAgent || '',
167169
requestHeaders: config.requestHeaders || {},
168170
allowDuplicateSnapshotNames: allowDuplicateSnapshotNames,

src/lib/httpClient.ts

Lines changed: 23 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -154,7 +154,7 @@ export default class httpClient {
154154
})
155155
}
156156

157-
async auth(log: Logger, env: Env): Promise<number> {
157+
async auth(log: Logger, env: Env): Promise<{ authResult: number, orgId: number, userId: number }> {
158158
let result = 1;
159159
if (this.projectToken) {
160160
result = 0;
@@ -168,12 +168,20 @@ export default class httpClient {
168168
}
169169
}, log);
170170
if (response && response.projectToken) {
171+
let orgId = 0;
172+
let userId = 0;
171173
this.projectToken = response.projectToken;
172174
env.PROJECT_TOKEN = response.projectToken;
173175
if (response.message && response.message.includes('Project created successfully')) {
174176
result = 2;
175177
}
176-
return result;
178+
if (response.orgId) {
179+
orgId = response.orgId
180+
}
181+
if (response.userId) {
182+
userId = response.userId
183+
}
184+
return { authResult : result, orgId, userId };
177185
} else {
178186
throw new Error('Authentication failed, project token not received');
179187
}
@@ -701,6 +709,19 @@ export default class httpClient {
701709
}, ctx.log);
702710
}
703711

712+
async getGeolocationProxy(geoLocation: string, log: Logger): Promise<{ data?: { proxy: string, username: string, password: string }, statusCode?: number }> {
713+
try {
714+
const resp = await this.request({
715+
url: '/geolocation',
716+
method: 'GET',
717+
params: { geoLocation }
718+
}, log);
719+
return resp;
720+
} catch (error: any) {
721+
this.handleHttpError(error, log);
722+
}
723+
}
724+
704725
async uploadPdf(ctx: Context, form: FormData, buildName?: string): Promise<any> {
705726
form.append('projectToken', this.projectToken);
706727
if (ctx.build.name !== undefined && ctx.build.name !== '') {

src/lib/schemaValidation.ts

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -264,6 +264,14 @@ const ConfigSchema = {
264264
uniqueItems: "Invalid config; duplicates in requestHeaders"
265265
}
266266
},
267+
dedicatedProxyURL: {
268+
type: "string",
269+
errorMessage: "Invalid config; dedicatedProxyURL must be a string"
270+
},
271+
geolocation: {
272+
type: "string",
273+
errorMessage: "Invalid config; geolocation must be a string like 'lat,lon'"
274+
},
267275
allowDuplicateSnapshotNames: {
268276
type: "boolean",
269277
errorMessage: "Invalid config; allowDuplicateSnapshotNames must be true/false"
@@ -341,6 +349,9 @@ const WebStaticConfigSchema: JSONSchemaType<WebStaticConfig> = {
341349
execute: {
342350
type: "object",
343351
properties: {
352+
beforeNavigation: {
353+
type: "string",
354+
},
344355
afterNavigation : {
345356
type: "string",
346357
},

src/lib/screenshot.ts

Lines changed: 88 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,7 @@ async function captureScreenshotsForConfig(
1717
ctx.log.debug(`*** urlConfig ${JSON.stringify(urlConfig)}`);
1818

1919
let {name, url, waitForTimeout, execute, pageEvent, userAgent} = urlConfig;
20+
let beforeNavigationScript = execute?.beforeNavigation;
2021
let afterNavigationScript = execute?.afterNavigation;
2122
let beforeSnapshotScript = execute?.beforeSnapshot;
2223
let waitUntilEvent = pageEvent || process.env.SMARTUI_PAGE_WAIT_UNTIL_EVENT || 'load';
@@ -28,6 +29,77 @@ async function captureScreenshotsForConfig(
2829
let contextOptions: Record<string, any> = {
2930
ignoreHTTPSErrors: ctx.config.ignoreHTTPSErrors
3031
};
32+
33+
34+
35+
// Resolve proxy/tunnel/geolocation-proxy from global config
36+
try {
37+
if (ctx.config.tunnel && ctx.config.tunnel.tunnelName) {
38+
if (ctx.tunnelDetails && ctx.tunnelDetails.tunnelPort != -1 && ctx.tunnelDetails.tunnelHost) {
39+
const tunnelServer = `http://${ctx.tunnelDetails.tunnelHost}:${ctx.tunnelDetails.tunnelPort}`;
40+
ctx.log.info(`URL Capture :: Using tunnel address: ${tunnelServer}`);
41+
contextOptions.proxy = { server: tunnelServer };
42+
} else {
43+
let tunnelResp = await ctx.client.getTunnelDetails(ctx, ctx.log);
44+
ctx.log.debug(`Tunnel Response: ${JSON.stringify(tunnelResp)}`)
45+
if (tunnelResp && tunnelResp.data && tunnelResp.data.host && tunnelResp.data.port) {
46+
ctx.tunnelDetails = {
47+
tunnelHost: tunnelResp.data.host,
48+
tunnelPort: tunnelResp.data.port,
49+
tunnelName: tunnelResp.data.tunnel_name
50+
} as any;
51+
const tunnelServer = `http://${ctx.tunnelDetails.tunnelHost}:${ctx.tunnelDetails.tunnelPort}`;
52+
ctx.log.info(`URL Capture :: Using tunnel address: ${tunnelServer}`);
53+
contextOptions.proxy = { server: tunnelServer };
54+
} else if (tunnelResp && tunnelResp.error) {
55+
if (tunnelResp.error.message) {
56+
ctx.log.warn(`Error while fetching tunnel details: ${tunnelResp.error.message}`)
57+
}
58+
}
59+
}
60+
} else if (ctx.config.geolocation && ctx.config.geolocation !== '') {
61+
// Use cached geolocation proxy if available for the same geolocation key
62+
if (ctx.geolocationData && ctx.geolocationData.proxy && ctx.geolocationData.username && ctx.geolocationData.password && ctx.geolocationData.geoCode === ctx.config.geolocation) {
63+
ctx.log.info(`URL Capture :: Using cached geolocation proxy for ${ctx.config.geolocation}`);
64+
contextOptions.proxy = {
65+
server: ctx.geolocationData.proxy,
66+
username: ctx.geolocationData.username,
67+
password: ctx.geolocationData.password
68+
};
69+
} else {
70+
const geoResp = await ctx.client.getGeolocationProxy(ctx.config.geolocation, ctx.log);
71+
ctx.log.debug(`Geolocation proxy response: ${JSON.stringify(geoResp)}`);
72+
if (geoResp && geoResp.data && geoResp.data.proxy && geoResp.data.username && geoResp.data.password) {
73+
ctx.log.info(`URL Capture :: Using geolocation proxy for ${ctx.config.geolocation}`);
74+
ctx.geolocationData = {
75+
proxy: geoResp.data.proxy,
76+
username: geoResp.data.username,
77+
password: geoResp.data.password,
78+
geoCode: ctx.config.geolocation
79+
} as any;
80+
contextOptions.proxy = {
81+
server: geoResp.data.proxy,
82+
username: geoResp.data.username,
83+
password: geoResp.data.password
84+
};
85+
} else {
86+
ctx.log.warn(`Geolocation proxy not available for '${ctx.config.geolocation}', falling back if dedicatedProxyURL present`);
87+
if (ctx.config.dedicatedProxyURL && ctx.config.dedicatedProxyURL !== '') {
88+
ctx.log.info(`URL Capture :: Using dedicated proxy: ${ctx.config.dedicatedProxyURL}`);
89+
contextOptions.proxy = { server: ctx.config.dedicatedProxyURL };
90+
}
91+
}
92+
}
93+
} else if (ctx.config.dedicatedProxyURL && ctx.config.dedicatedProxyURL !== '') {
94+
ctx.log.info(`URL Capture :: Using dedicated proxy: ${ctx.config.dedicatedProxyURL}`);
95+
contextOptions.proxy = { server: ctx.config.dedicatedProxyURL };
96+
}
97+
98+
// Note: when using IP-based geolocation via proxy, browser geolocation permission is not required
99+
100+
} catch (e) {
101+
ctx.log.debug(`Failed resolving tunnel/proxy details: ${e}`);
102+
}
31103
let page: Page;
32104
if (browserName == constants.CHROME) contextOptions.userAgent = constants.CHROME_USER_AGENT;
33105
else if (browserName == constants.FIREFOX) contextOptions.userAgent = constants.FIREFOX_USER_AGENT;
@@ -46,6 +118,16 @@ async function captureScreenshotsForConfig(
46118
const browser = browsers[browserName];
47119
context = await browser?.newContext(contextOptions);
48120
page = await context?.newPage();
121+
122+
if (beforeNavigationScript && beforeNavigationScript !== "") {
123+
const wrappedScript = new Function('page', `
124+
return (async () => {
125+
${beforeNavigationScript}
126+
})();
127+
`);
128+
ctx.log.debug(`Executing before navigation script: ${wrappedScript}`);
129+
await wrappedScript(page);
130+
}
49131
const headersObject: Record<string, string> = {};
50132
if (ctx.config.requestHeaders && Array.isArray(ctx.config.requestHeaders)) {
51133
ctx.config.requestHeaders.forEach((headerObj) => {
@@ -62,6 +144,12 @@ async function captureScreenshotsForConfig(
62144
});
63145
}
64146

147+
if (ctx.config.basicAuthorization) {
148+
ctx.log.debug(`Adding basic authorization to the headers for root url`);
149+
let token = Buffer.from(`${ctx.config.basicAuthorization.username}:${ctx.config.basicAuthorization.password}`).toString('base64');
150+
headersObject['Authorization'] = `Basic ${token}`;
151+
}
152+
65153
ctx.log.debug(`Combined headers: ${JSON.stringify(headersObject)}`);
66154
if (Object.keys(headersObject).length > 0) {
67155
await page.setExtraHTTPHeaders(headersObject);

src/lib/server.ts

Lines changed: 7 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -294,7 +294,10 @@ export default async (ctx: Context): Promise<FastifyInstance<Server, IncomingMes
294294

295295
if (externalResponse.statusCode === 200) {
296296
replyCode = 200;
297-
replyBody = externalResponse.data;
297+
replyBody = {
298+
data: externalResponse.data,
299+
error: externalResponse.error.message
300+
}
298301
return reply.code(replyCode).send(replyBody);
299302
} else if (externalResponse.statusCode === 202 ) {
300303
replyBody= externalResponse.data;
@@ -306,13 +309,7 @@ export default async (ctx: Context): Promise<FastifyInstance<Server, IncomingMes
306309
}else {
307310
ctx.log.debug(`Unexpected response from external API: ${JSON.stringify(externalResponse)}`);
308311
replyCode = 500;
309-
replyBody = {
310-
error: {
311-
message: `Unexpected response from external API: ${externalResponse.statusCode}`,
312-
externalApiStatus: externalResponse.statusCode
313-
}
314-
};
315-
return reply.code(replyCode).send(replyBody);
312+
return reply.code(replyCode).send(externalResponse);
316313
}
317314

318315
ctx.log.debug(`timeoutDuration: ${timeoutDuration}`);
@@ -321,9 +318,8 @@ export default async (ctx: Context): Promise<FastifyInstance<Server, IncomingMes
321318
if (Date.now() - startTime > timeoutDuration) {
322319
replyCode = 202;
323320
replyBody = {
324-
data: {
325-
message: 'Request timed out-> Snapshot still processing'
326-
}
321+
error: 'Request timed out, Snapshot still processing',
322+
data: lastExternalResponse.data
327323
};
328324
return reply.code(replyCode).send(replyBody);
329325
}

src/lib/utils.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -459,7 +459,7 @@ export async function startPollingForTunnel(ctx: Context, build_id: string, base
459459

460460
export async function stopTunnelHelper(ctx: Context) {
461461
ctx.log.debug('stop-tunnel:: Stopping the tunnel now');
462-
const tunnelRunningStatus = await tunnelInstance.isRunning();
462+
const tunnelRunningStatus = await tunnelInstance?.isRunning();
463463
ctx.log.debug('stop-tunnel:: Running status of tunnel before stopping ? ' + tunnelRunningStatus);
464464

465465
const status = await tunnelInstance.stop();

src/tasks/auth.ts

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -10,14 +10,16 @@ export default (ctx: Context): ListrTask<Context, ListrRendererFactory, ListrRen
1010
updateLogContext({task: 'auth'});
1111

1212
try {
13-
const authResult = await ctx.client.auth(ctx.log, ctx.env);
13+
const { authResult, orgId, userId } = await ctx.client.auth(ctx.log, ctx.env);
1414
if (authResult === 2) {
1515
task.output = chalk.gray(`New project '${ctx.env.PROJECT_NAME}' created successfully`);
1616
} else if (authResult === 0) {
1717
task.output = chalk.gray(`Using existing project token '******#${ctx.env.PROJECT_TOKEN.split('#').pop()}'`);
1818
} else if (authResult === 1) {
1919
task.output = chalk.gray(`Using existing project '${ctx.env.PROJECT_NAME}'`);
2020
}
21+
ctx.orgId = orgId
22+
ctx.userId = userId
2123
task.title = 'Authenticated with SmartUI';
2224
} catch (error: any) {
2325
ctx.log.debug(error);

src/types.ts

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -34,6 +34,8 @@ export interface Context {
3434
ignoreHTTPSErrors : boolean;
3535
skipBuildCreation?: boolean;
3636
tunnel: tunnelConfig | undefined;
37+
dedicatedProxyURL?: string;
38+
geolocation?: string;
3739
userAgent?: string;
3840
requestHeaders?: Array<Record<string, string>>;
3941
allowDuplicateSnapshotNames?: boolean;
@@ -58,6 +60,12 @@ export interface Context {
5860
tunnelHost: string;
5961
tunnelName: string;
6062
}
63+
geolocationData?: {
64+
proxy: string;
65+
username: string;
66+
password: string;
67+
geoCode: string;
68+
}
6169
options: {
6270
parallel?: number,
6371
force?: boolean,

0 commit comments

Comments
 (0)