Skip to content

Commit 199625d

Browse files
committed
Merge branch 'prod' into DOT-4523
2 parents 59538f4 + 6d3f180 commit 199625d

14 files changed

+377
-67
lines changed

package.json

+2-1
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
{
22
"name": "@lambdatest/smartui-cli",
3-
"version": "4.1.7-beta.5",
3+
"version": "4.1.11-beta.0",
44
"description": "A command line interface (CLI) to run SmartUI tests on LambdaTest",
55
"files": [
66
"dist/**/*"
@@ -30,6 +30,7 @@
3030
"@types/cross-spawn": "^6.0.4",
3131
"@types/node": "^20.8.9",
3232
"@types/which": "^3.0.2",
33+
"@lambdatest/node-tunnel": "^4.0.9",
3334
"ajv": "^8.12.0",
3435
"ajv-errors": "^3.0.0",
3536
"axios": "^1.6.0",

src/commander/exec.ts

+3
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@ import exec from '../tasks/exec.js'
1111
import processSnapshots from '../tasks/processSnapshot.js'
1212
import finalizeBuild from '../tasks/finalizeBuild.js'
1313
import snapshotQueue from '../lib/snapshotQueue.js'
14+
import startTunnel from '../tasks/startTunnel.js'
1415

1516
const command = new Command();
1617

@@ -42,6 +43,7 @@ command
4243
authExec(ctx),
4344
startServer(ctx),
4445
getGitInfo(ctx),
46+
...(ctx.config.tunnel && ctx.config.tunnel?.type === 'auto' ? [startTunnel(ctx)] : []),
4547
createBuildExec(ctx),
4648
exec(ctx),
4749
processSnapshots(ctx),
@@ -63,6 +65,7 @@ command
6365
await tasks.run(ctx);
6466
} catch (error) {
6567
ctx.log.info('\nRefer docs: https://www.lambdatest.com/support/docs/smart-visual-regression-testing/');
68+
throw new Error();
6669
}
6770
})
6871

src/lib/ctx.ts

+11-5
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
import { Context, Env, WebConfig, MobileConfig, basicAuth } from '../types.js'
1+
import { Context, Env, WebConfig, MobileConfig, basicAuth, tunnelConfig } from '../types.js'
22
import constants from './constants.js'
33
import { version } from '../../package.json'
44
import { validateConfig } from './schemaValidation.js'
@@ -12,6 +12,7 @@ export default (options: Record<string, string>): Context => {
1212
let webConfig: WebConfig;
1313
let mobileConfig: MobileConfig;
1414
let basicAuthObj: basicAuth
15+
let tunnelObj: tunnelConfig
1516
let config = constants.DEFAULT_CONFIG;
1617
let port: number;
1718
let resolutionOff: boolean;
@@ -77,7 +78,10 @@ export default (options: Record<string, string>): Context => {
7778
}
7879
if (config.basicAuthorization) {
7980
basicAuthObj = config.basicAuthorization
80-
}
81+
}
82+
if (config.tunnel) {
83+
tunnelObj = config.tunnel
84+
}
8185

8286
return {
8387
env: env,
@@ -100,8 +104,8 @@ export default (options: Record<string, string>): Context => {
100104
useGlobalCache: config.useGlobalCache ?? false,
101105
ignoreHTTPSErrors: config.ignoreHTTPSErrors ?? false,
102106
skipBuildCreation: config.skipBuildCreation ?? false,
103-
tunnel: config.tunnel ?? false,
104-
tunnelName: config.tunnelName || ''
107+
tunnel: tunnelObj,
108+
userAgent: config.userAgent || ''
105109
},
106110
uploadFilePath: '',
107111
webStaticConfig: [],
@@ -143,6 +147,8 @@ export default (options: Record<string, string>): Context => {
143147
isSnapshotCaptured: false,
144148
sessionCapabilitiesMap: new Map<string, any[]>(),
145149
buildToSnapshotCountMap: new Map<string, number>(),
146-
fetchResultsForBuild: new Array<string>
150+
fetchResultsForBuild: new Array<string>,
151+
orgId: 0,
152+
userId: 0
147153
}
148154
}

src/lib/httpClient.ts

+70-11
Original file line numberDiff line numberDiff line change
@@ -45,14 +45,18 @@ export default class httpClient {
4545

4646
this.axiosInstance = axios.create(axiosConfig);
4747

48-
4948
this.axiosInstance.interceptors.request.use((config) => {
50-
if (!config.headers['projectToken']) {
49+
if (!config.headers['projectToken'] && this.projectToken !== '') {
5150
config.headers['projectToken'] = this.projectToken;
51+
} else if ((!config.headers['projectName'] && this.projectName !== '')) {
52+
config.headers['projectName'] = this.projectName;
53+
if (!config.headers['username'] || config.headers['username'] === '') {
54+
config.headers['username'] = this.username;
55+
}
56+
if (!config.headers['accessKey'] || config.headers['accessKey'] === '') {
57+
config.headers['accessKey'] = this.accessKey;
58+
}
5259
}
53-
config.headers['projectName'] = this.projectName;
54-
config.headers['username'] = this.username;
55-
config.headers['accessKey'] = this.accessKey;
5660
return config;
5761
});
5862

@@ -151,6 +155,53 @@ export default class httpClient {
151155
}
152156
}
153157

158+
async authExec(ctx: Context, log: Logger, env: Env): Promise<{ authResult: number, orgId: number, userId: number }> {
159+
let authResult = 1;
160+
let userName = '';
161+
let passWord = '';
162+
if (ctx.config.tunnel) {
163+
if (ctx.config.tunnel?.user && ctx.config.tunnel?.key) {
164+
userName = ctx.config.tunnel.user
165+
passWord = ctx.config.tunnel.key
166+
} else {
167+
userName = this.username
168+
passWord = this.accessKey
169+
}
170+
}
171+
if (this.projectToken) {
172+
authResult = 0;
173+
}
174+
const response = await this.request({
175+
url: '/token/verify',
176+
method: 'GET',
177+
headers: {
178+
username: userName,
179+
accessKey: passWord
180+
}
181+
}, log);
182+
if (response && response.projectToken) {
183+
let orgId = 0;
184+
let userId = 0;
185+
this.projectToken = response.projectToken;
186+
env.PROJECT_TOKEN = response.projectToken;
187+
if (response.message && response.message.includes('Project created successfully')) {
188+
authResult = 2;
189+
}
190+
if (response.orgId) {
191+
orgId = response.orgId
192+
}
193+
if (response.userId) {
194+
userId = response.userId
195+
}
196+
return { authResult, orgId, userId };
197+
} else {
198+
if(response && response.message) {
199+
throw new Error(response.message);
200+
}
201+
throw new Error('Authentication failed, project token not received. Refer to the smartui.log file for more information');
202+
}
203+
}
204+
154205
createBuild(git: Git, config: any, log: Logger, buildName: string, isStartExec: boolean) {
155206
return this.request({
156207
url: '/build',
@@ -174,16 +225,24 @@ export default class httpClient {
174225
}, log);
175226
}
176227

177-
getTunnelDetails(tunnelName: string, log: Logger) {
228+
getTunnelDetails(ctx: Context, log: Logger) {
229+
const data: any = {
230+
orgId: ctx.orgId,
231+
userId: ctx.userId,
232+
userName: ctx.config.tunnel?.user,
233+
password: ctx.config.tunnel?.key
234+
};
235+
236+
if (ctx.config.tunnel?.tunnelName) {
237+
data.tunnelName = ctx.config.tunnel.tunnelName;
238+
}
239+
178240
return this.request({
179241
url: '/tunnel',
180242
method: 'POST',
181-
data: {
182-
tunnelName: tunnelName
183-
}
184-
}, log)
243+
data: data
244+
}, log);
185245
}
186-
187246

188247
ping(buildId: string, log: Logger) {
189248
return this.request({

src/lib/processSnapshot.ts

+1-3
Original file line numberDiff line numberDiff line change
@@ -338,7 +338,7 @@ export default async function processSnapshot(snapshot: Snapshot, ctx: Context):
338338
ctx.log.debug(`Resource had a disallowed status for retry as well ${requestUrl} disallowed status [${responseOfRetry.status()}]`);
339339
if (responseOfRetry && responseOfRetry.headers()) {
340340
const responseHeadersRetry = responseOfRetry.headers();
341-
ctx.log.debug(`Response headers for ${requestUrl}: ${JSON.stringify(responseHeadersRetry, null, 2)}`);
341+
ctx.log.debug(`Response headers for retry ${requestUrl}: ${JSON.stringify(responseHeadersRetry, null, 2)}`);
342342
}
343343

344344
let data = {
@@ -361,8 +361,6 @@ export default async function processSnapshot(snapshot: Snapshot, ctx: Context):
361361
discoveryErrors.browsers[globalBrowser][globalViewport]?.push(data);
362362
}
363363
}
364-
365-
366364
} else {
367365
ctx.log.debug(`Handling request ${requestUrl}\n - content-type ${response.headers()['content-type']}`);
368366

src/lib/schemaValidation.ts

+60-4
Original file line numberDiff line numberDiff line change
@@ -189,12 +189,64 @@ const ConfigSchema = {
189189
errorMessage: "Invalid config; skipBuildCreation must be true/false"
190190
},
191191
tunnel: {
192-
type: "boolean",
193-
errorMessage: "Invalid config; tunnel must be true/false"
192+
type: "object",
193+
properties: {
194+
type: {
195+
type: "string",
196+
enum: ["auto", "manual"],
197+
errorMessage: "Invalid config; tunnel type is mandatory parameter of type string having value auto or manual",
198+
},
199+
tunnelName: {
200+
type: "string",
201+
errorMessage: "Invalid config; tunnelName should be a string value"
202+
},
203+
user: {
204+
type: "string",
205+
errorMessage: "Invalid config; user should be a string value"
206+
},
207+
key: {
208+
type: "string",
209+
errorMessage: "Invalid config; key should be a string value"
210+
},
211+
port: {
212+
type: "string",
213+
errorMessage: "Invalid config; port should be a string value"
214+
},
215+
proxyHost: {
216+
type: "string",
217+
errorMessage: "Invalid config; proxyHost should be a string value"
218+
},
219+
proxyPort: {
220+
type: "number",
221+
errorMessage: "Invalid config; proxyPort should be an int value"
222+
},
223+
proxyUser: {
224+
type: "string",
225+
errorMessage: "Invalid config; proxyUser should be a string value"
226+
},
227+
proxyPass: {
228+
type: "string",
229+
errorMessage: "Invalid config; proxyPass should be a string value"
230+
},
231+
dir: {
232+
type: "string",
233+
errorMessage: "Invalid config; dir should be a string value"
234+
},
235+
v: {
236+
type: "boolean",
237+
errorMessage: "Invalid config; v should be a boolean value"
238+
},
239+
logFile: {
240+
type: "string",
241+
errorMessage: "Invalid config; logFile should be a string value"
242+
},
243+
},
244+
required: ["type"],
245+
additionalProperties: false
194246
},
195-
tunnelName: {
247+
userAgent: {
196248
type: "string",
197-
errorMessage: "Invalid config; tunnelName must be string"
249+
errorMessage: "User Agent value must be a valid string"
198250
},
199251
},
200252
anyOf: [
@@ -226,6 +278,10 @@ const WebStaticConfigSchema: JSONSchemaType<WebStaticConfig> = {
226278
maximum: 30000,
227279
errorMessage: "waitForTimeout must be > 0 and <= 30000"
228280
},
281+
userAgent: {
282+
type: "string",
283+
errorMessage: "User Agent value must be a valid string"
284+
},
229285
execute: {
230286
type: "object",
231287
properties: {

src/lib/screenshot.ts

+10-2
Original file line numberDiff line numberDiff line change
@@ -16,12 +16,12 @@ async function captureScreenshotsForConfig(
1616
): Promise<void> {
1717
ctx.log.debug(`*** urlConfig ${JSON.stringify(urlConfig)}`);
1818

19-
let {name, url, waitForTimeout, execute, pageEvent} = urlConfig;
19+
let {name, url, waitForTimeout, execute, pageEvent, userAgent} = urlConfig;
2020
let afterNavigationScript = execute?.afterNavigation;
2121
let beforeSnapshotScript = execute?.beforeSnapshot;
2222
let waitUntilEvent = pageEvent || process.env.SMARTUI_PAGE_WAIT_UNTIL_EVENT || 'load';
2323

24-
let pageOptions = { waitUntil: waitUntilEvent, timeout: ctx.config.waitForPageRender || constants.DEFAULT_PAGE_LOAD_TIMEOUT };
24+
let pageOptions = { waitUntil: waitUntilEvent, timeout: ctx.config.waitForPageRender || constants.DEFAULT_PAGE_LOAD_TIMEOUT};
2525
ctx.log.debug(`url: ${url} pageOptions: ${JSON.stringify(pageOptions)}`);
2626
let ssId = name.toLowerCase().replace(/\s/g, '_');
2727
let context: BrowserContext;
@@ -31,6 +31,14 @@ async function captureScreenshotsForConfig(
3131
else if (browserName == constants.FIREFOX) contextOptions.userAgent = constants.FIREFOX_USER_AGENT;
3232
else if (browserName == constants.SAFARI) contextOptions.userAgent = constants.SAFARI_USER_AGENT;
3333
else if (browserName == constants.EDGE) contextOptions.userAgent = constants.EDGE_USER_AGENT;
34+
if (ctx.config.userAgent || userAgent) {
35+
if(ctx.config.userAgent !== ""){
36+
contextOptions.userAgent = ctx.config.userAgent;
37+
}
38+
if (userAgent && userAgent !== "") {
39+
contextOptions.userAgent = userAgent;
40+
}
41+
}
3442

3543
try {
3644
const browser = browsers[browserName];

src/lib/snapshotQueue.ts

+5-2
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@ import { Snapshot, Context } from "../types.js";
33
import constants from "./constants.js";
44
import processSnapshot, {prepareSnapshot} from "./processSnapshot.js"
55
import { v4 as uuidv4 } from 'uuid';
6-
import { startPolling } from "./utils.js";
6+
import { startPolling, stopTunnelHelper } from "./utils.js";
77

88
export default class Queue {
99
private snapshots: Array<Snapshot> = [];
@@ -272,7 +272,7 @@ export default class Queue {
272272
this.processingSnapshot = snapshot?.name;
273273
let drop = false;
274274

275-
if (this.ctx.isStartExec) {
275+
if (this.ctx.isStartExec && !this.ctx.config.tunnel) {
276276
this.ctx.log.info(`Processing Snapshot: ${snapshot?.name}`);
277277
}
278278

@@ -359,6 +359,9 @@ export default class Queue {
359359
useKafkaFlow: resp.data.useKafkaFlow || false,
360360
}
361361
} else {
362+
if (this.ctx.config.tunnel && this.ctx.config.tunnel?.type === 'auto') {
363+
await stopTunnelHelper(this.ctx)
364+
}
362365
throw new Error('SmartUI capabilities are missing in env variables or in driver capabilities');
363366
}
364367
if (this.ctx.options.fetchResults) {

src/lib/uploadWebFigma.ts

+3
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,9 @@ export default async (ctx: Context): Promise<string> => {
2222

2323
const responseData = await ctx.client.processWebFigma(requestBody, ctx.log);
2424
ctx.log.debug("responseData : "+ JSON.stringify(responseData));
25+
if (responseData && responseData.error && responseData.error.message) {
26+
throw new Error(responseData.error.message);
27+
}
2528

2629
if (responseData.data.message == "success") {
2730
results = responseData.data.message;

0 commit comments

Comments
 (0)