Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "@lambdatest/smartui-cli",
"version": "4.1.42-beta.0",
"version": "4.1.42",
"description": "A command line interface (CLI) to run SmartUI tests on LambdaTest",
"files": [
"dist/**/*"
Expand Down
2 changes: 2 additions & 0 deletions src/lib/ctx.ts
Original file line number Diff line number Diff line change
Expand Up @@ -163,6 +163,8 @@ export default (options: Record<string, string>): Context => {
ignoreHTTPSErrors: config.ignoreHTTPSErrors ?? false,
skipBuildCreation: config.skipBuildCreation ?? false,
tunnel: tunnelObj,
dedicatedProxyURL: config.dedicatedProxyURL || '',
geolocation: config.geolocation || '',
userAgent: config.userAgent || '',
requestHeaders: config.requestHeaders || {},
allowDuplicateSnapshotNames: allowDuplicateSnapshotNames,
Expand Down
25 changes: 23 additions & 2 deletions src/lib/httpClient.ts
Original file line number Diff line number Diff line change
Expand Up @@ -154,7 +154,7 @@ export default class httpClient {
})
}

async auth(log: Logger, env: Env): Promise<number> {
async auth(log: Logger, env: Env): Promise<{ authResult: number, orgId: number, userId: number }> {
let result = 1;
if (this.projectToken) {
result = 0;
Expand All @@ -168,12 +168,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');
}
Expand Down Expand Up @@ -701,6 +709,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: '/geolocation',
method: 'GET',
params: { geoLocation }
}, log);
return resp;
} catch (error: any) {
this.handleHttpError(error, log);
}
}

async uploadPdf(ctx: Context, form: FormData, buildName?: string): Promise<any> {
form.append('projectToken', this.projectToken);
if (ctx.build.name !== undefined && ctx.build.name !== '') {
Expand Down
11 changes: 11 additions & 0 deletions src/lib/schemaValidation.ts
Original file line number Diff line number Diff line change
Expand Up @@ -264,6 +264,14 @@ const ConfigSchema = {
uniqueItems: "Invalid config; duplicates in requestHeaders"
}
},
dedicatedProxyURL: {
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"
Expand Down Expand Up @@ -341,6 +349,9 @@ const WebStaticConfigSchema: JSONSchemaType<WebStaticConfig> = {
execute: {
type: "object",
properties: {
beforeNavigation: {
type: "string",
},
afterNavigation : {
type: "string",
},
Expand Down
88 changes: 88 additions & 0 deletions src/lib/screenshot.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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';
Expand All @@ -28,6 +29,77 @@ async function captureScreenshotsForConfig(
let contextOptions: Record<string, any> = {
ignoreHTTPSErrors: ctx.config.ignoreHTTPSErrors
};



// 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) {
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.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}`);
}
let page: Page;
if (browserName == constants.CHROME) contextOptions.userAgent = constants.CHROME_USER_AGENT;
else if (browserName == constants.FIREFOX) contextOptions.userAgent = constants.FIREFOX_USER_AGENT;
Expand All @@ -46,6 +118,16 @@ async function captureScreenshotsForConfig(
const browser = browsers[browserName];
context = await browser?.newContext(contextOptions);
page = await context?.newPage();

if (beforeNavigationScript && beforeNavigationScript !== "") {
const wrappedScript = new Function('page', `
return (async () => {
${beforeNavigationScript}
})();
`);
ctx.log.debug(`Executing before navigation script: ${wrappedScript}`);
await wrappedScript(page);
}
const headersObject: Record<string, string> = {};
if (ctx.config.requestHeaders && Array.isArray(ctx.config.requestHeaders)) {
ctx.config.requestHeaders.forEach((headerObj) => {
Expand All @@ -62,6 +144,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);
Expand Down
2 changes: 1 addition & 1 deletion src/lib/utils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -459,7 +459,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();
Expand Down
4 changes: 3 additions & 1 deletion src/tasks/auth.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,14 +10,16 @@ export default (ctx: Context): ListrTask<Context, ListrRendererFactory, ListrRen
updateLogContext({task: 'auth'});

try {
const authResult = await ctx.client.auth(ctx.log, ctx.env);
const { authResult, orgId, userId } = await ctx.client.auth(ctx.log, ctx.env);
if (authResult === 2) {
task.output = chalk.gray(`New project '${ctx.env.PROJECT_NAME}' created successfully`);
} else if (authResult === 0) {
task.output = chalk.gray(`Using existing project token '******#${ctx.env.PROJECT_TOKEN.split('#').pop()}'`);
} else if (authResult === 1) {
task.output = chalk.gray(`Using existing project '${ctx.env.PROJECT_NAME}'`);
}
ctx.orgId = orgId
ctx.userId = userId
task.title = 'Authenticated with SmartUI';
} catch (error: any) {
ctx.log.debug(error);
Expand Down
8 changes: 8 additions & 0 deletions src/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,8 @@ export interface Context {
ignoreHTTPSErrors : boolean;
skipBuildCreation?: boolean;
tunnel: tunnelConfig | undefined;
dedicatedProxyURL?: string;
geolocation?: string;
userAgent?: string;
requestHeaders?: Array<Record<string, string>>;
allowDuplicateSnapshotNames?: boolean;
Expand All @@ -58,6 +60,12 @@ export interface Context {
tunnelHost: string;
tunnelName: string;
}
geolocationData?: {
proxy: string;
username: string;
password: string;
geoCode: string;
}
options: {
parallel?: number,
force?: boolean,
Expand Down