Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
28 commits
Select commit Hold shift + click to select a range
43c4f35
changes for threshold
Nick-1234531 Sep 10, 2025
e7bc81d
validation check
Nick-1234531 Sep 10, 2025
db3c867
added schema validation for config
Nick-1234531 Sep 11, 2025
29dafad
validation for threshold values
Nick-1234531 Sep 11, 2025
3d66db0
Merge branch 'stage' of https://github.com/LambdaTest/smartui-cli int…
Nick-1234531 Sep 11, 2025
15cacbf
handling threshold constraints
Nick-1234531 Sep 12, 2025
98408be
handling console message
Nick-1234531 Sep 12, 2025
de7bf53
overrides simple-swizzle version to 0.2.2
sushobhit-lt Sep 15, 2025
f6902b7
Merge pull request #361 from sushobhit-lt/DOT-6243
parthlambdatest Sep 15, 2025
36ee857
auto tunnel support added in exec:start
sushobhit-lt Sep 15, 2025
76b4f20
validation at build level
Nick-1234531 Sep 15, 2025
6daa126
Merge pull request #362 from sushobhit-lt/DOT-6255
parthlambdatest Sep 16, 2025
24beb63
add is start exec for session caps
parthlambdatest Sep 16, 2025
0e8ed8f
Merge pull request #363 from parthlambdatest/tunnel_session_caps
parthlambdatest Sep 16, 2025
0993b97
Merge pull request #364 from lt-zeeshan/DOT-6242
sushobhit-lt Sep 16, 2025
ffa118a
resolved merge conflicts
Nick-1234531 Sep 16, 2025
bdf170e
handle tunnel cases with exec:start
sushobhit-lt Sep 17, 2025
0df35a2
Merge pull request #359 from Nick-1234531/DOT-6136
parthlambdatest Sep 17, 2025
0a94852
version bump
Nick-1234531 Sep 17, 2025
d13b7b7
Merge pull request #366 from Nick-1234531/DOT-6136
parthlambdatest Sep 17, 2025
fbd0e2b
remove ping-pong incase of auto tunnel
sushobhit-lt Sep 17, 2025
b76cf61
optimise code
sushobhit-lt Sep 17, 2025
dcd62dd
fix baseline
sushobhit-lt Sep 18, 2025
9c4e598
Merge branch 'prod' of github.com:LambdaTest/smartui-cli into DOT-6255
sushobhit-lt Sep 18, 2025
6d4c388
bump version
sushobhit-lt Sep 18, 2025
cc65292
Merge branch 'stage' of github.com:LambdaTest/smartui-cli into DOT-6255
sushobhit-lt Sep 18, 2025
84d3fcc
fix import statement
sushobhit-lt Sep 18, 2025
aa0f6c2
Merge pull request #369 from sushobhit-lt/DOT-6255
parthlambdatest Sep 18, 2025
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
5 changes: 4 additions & 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.31",
"version": "4.1.32",
"description": "A command line interface (CLI) to run SmartUI tests on LambdaTest",
"files": [
"dist/**/*"
Expand Down Expand Up @@ -49,6 +49,9 @@
"which": "^4.0.0",
"winston": "^3.10.0"
},
"overrides": {
"simple-swizzle": "0.2.2"
},
"devDependencies": {
"typescript": "^5.3.2"
}
Expand Down
1 change: 1 addition & 0 deletions src/commander/exec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -40,6 +40,7 @@ command
ctx.args.execCommand = execCommand
ctx.snapshotQueue = new snapshotQueue(ctx)
ctx.totalSnapshots = 0
ctx.sourceCommand = 'exec'

let tasks = new Listr<Context>(
[
Expand Down
10 changes: 7 additions & 3 deletions src/commander/server.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ import getGitInfo from '../tasks/getGitInfo.js';
import createBuildExec from '../tasks/createBuildExec.js';
import snapshotQueue from '../lib/snapshotQueue.js';
import { startPolling, startPingPolling } from '../lib/utils.js';
import startTunnel from '../tasks/startTunnel.js'

const command = new Command();

Expand All @@ -27,12 +28,14 @@ command
ctx.snapshotQueue = new snapshotQueue(ctx);
ctx.totalSnapshots = 0
ctx.isStartExec = true

ctx.sourceCommand = 'exec-start'

let tasks = new Listr<Context>(
[
authExec(ctx),
startServer(ctx),
getGitInfo(ctx),
...(ctx.config.tunnel && ctx.config.tunnel?.type === 'auto' ? [startTunnel(ctx)] : []),
createBuildExec(ctx),

],
Expand All @@ -50,15 +53,16 @@ command

try {
await tasks.run(ctx);
if (ctx.build && ctx.build.id) {
if (ctx.build && ctx.build.id && !ctx.autoTunnelStarted) {
startPingPolling(ctx);
}
if (ctx.options.fetchResults && ctx.build && ctx.build.id) {
startPolling(ctx, '', false, '')
}

} catch (error) {
console.error('Error during server execution:', error);
process.exit(1);
}
});

Expand Down
4 changes: 3 additions & 1 deletion src/commander/stopServer.ts
Original file line number Diff line number Diff line change
Expand Up @@ -33,8 +33,10 @@ command
}
} catch (error: any) {
// Handle any errors during the HTTP request
if (error.code === 'ECONNABORTED') {
if (error && error.code === 'ECONNABORTED') {
console.error(chalk.red('Error: SmartUI server did not respond in 15 seconds'));
} if (error && error.code === 'ECONNREFUSED') {
console.error(chalk.red('Error: Looks like smartui cli server is already stopped'));
} else {
console.error(chalk.red('Error while stopping server'));
}
Expand Down
8 changes: 8 additions & 0 deletions src/lib/ctx.ts
Original file line number Diff line number Diff line change
Expand Up @@ -39,6 +39,12 @@ export default (options: Record<string, string>): Context => {
delete config.web.resolutions;
}

if(config.approvalThreshold && config.rejectionThreshold) {
if(config.rejectionThreshold <= config.approvalThreshold) {
throw new Error('Invalid config; rejectionThreshold must be greater than approvalThreshold');
}
}

let validateConfigFn = options.scheduled ? validateConfigForScheduled : validateConfig;

// validate config
Expand Down Expand Up @@ -142,6 +148,8 @@ export default (options: Record<string, string>): Context => {
useLambdaInternal: useLambdaInternal,
useExtendedViewport: useExtendedViewport,
loadDomContent: loadDomContent,
approvalThreshold: config.approvalThreshold,
rejectionThreshold: config.rejectionThreshold,
},
uploadFilePath: '',
webStaticConfig: [],
Expand Down
49 changes: 30 additions & 19 deletions src/lib/httpClient.ts
Original file line number Diff line number Diff line change
Expand Up @@ -77,7 +77,7 @@ export default class httpClient {
(response) => response,
async (error) => {
const { config } = error;
if (config && config.url === '/screenshot' && config.method === 'post') {
if (config && config.url === '/screenshot' && config.method === 'post' && error?.response?.status !== 401) {
// Set default retry count and delay if not already defined
if (!config.retryCount) {
config.retryCount = 0;
Expand Down Expand Up @@ -242,11 +242,12 @@ export default class httpClient {
}, log)
}

getScreenshotData(buildId: string, baseline: boolean, log: Logger, projectToken: string) {
getScreenshotData(buildId: string, baseline: boolean, log: Logger, projectToken: string, buildName: string) {
log.debug(`Fetching screenshot data for buildId: ${buildId} having buildName: ${buildName} with baseline: ${baseline}`);
return this.request({
url: '/screenshot',
method: 'GET',
params: { buildId, baseline },
params: { buildId, baseline, buildName },
headers: {projectToken: projectToken}
}, log);
}
Expand Down Expand Up @@ -281,7 +282,7 @@ export default class httpClient {
}


getSmartUICapabilities(sessionId: string, config: any, git: any, log: Logger) {
getSmartUICapabilities(sessionId: string, config: any, git: any, log: Logger, isStartExec: boolean) {
return this.request({
url: '/sessions/capabilities',
method: 'GET',
Expand All @@ -290,7 +291,8 @@ export default class httpClient {
},
data: {
git,
config
config,
isStartExec
},
headers: {
projectToken: '',
Expand Down Expand Up @@ -343,24 +345,33 @@ export default class httpClient {
}, ctx.log)
}

processSnapshot(ctx: Context, snapshot: ProcessedSnapshot, snapshotUuid: string, discoveryErrors: DiscoveryErrors, variantCount: number, sync: boolean = false) {
processSnapshot(ctx: Context, snapshot: ProcessedSnapshot, snapshotUuid: 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'
},
discoveryErrors: discoveryErrors,
doRemoteDiscovery: snapshot.options.doRemoteDiscovery,
sync: sync
};

if (approvalThreshold !== undefined) {
requestData.approvalThreshold = approvalThreshold;
}
if (rejectionThreshold !== undefined) {
requestData.rejectionThreshold = rejectionThreshold;
}

return this.request({
url: `/build/${ctx.build.id}/snapshot`,
method: 'POST',
headers: { 'Content-Type': 'application/json' },
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)
}

Expand Down
29 changes: 29 additions & 0 deletions src/lib/schemaValidation.ts
Original file line number Diff line number Diff line change
Expand Up @@ -239,6 +239,11 @@ const ConfigSchema = {
type: "string",
errorMessage: "Invalid config; logFile should be a string value"
},
environment: {
type: "string",
enum: ["stage", "prod"],
errorMessage: "Invalid config; environment should be a string value either stage or prod"
}
},
required: ["type"],
additionalProperties: false
Expand Down Expand Up @@ -274,6 +279,18 @@ const ConfigSchema = {
loadDomContent: {
type: "boolean",
errorMessage: "Invalid config; loadDomContent must be true/false"
},
approvalThreshold: {
type: "number",
minimum: 0,
maximum: 100,
errorMessage: "Invalid config; approvalThreshold must be a number"
},
rejectionThreshold: {
type: "number",
minimum: 0,
maximum: 100,
errorMessage: "Invalid config; rejectionThreshold must be a number"
}
},
anyOf: [
Expand Down Expand Up @@ -537,6 +554,18 @@ const SnapshotSchema: JSONSchemaType<Snapshot> = {
useExtendedViewport: {
type: "boolean",
errorMessage: "Invalid snapshot options; useExtendedViewport must be a boolean"
},
approvalThreshold: {
type: "number",
minimum: 0,
maximum: 100,
errorMessage: "Invalid snapshot options; approvalThreshold must be a number between 0 and 100"
},
rejectionThreshold: {
type: "number",
minimum: 0,
maximum: 100,
errorMessage: "Invalid snapshot options; rejectionThreshold must be a number between 0 and 100"
}
},
additionalProperties: false
Expand Down
32 changes: 27 additions & 5 deletions src/lib/server.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,8 +4,7 @@ import fastify, { FastifyInstance, RouteShorthandOptions } from 'fastify';
import { readFileSync, truncate } from 'fs'
import { Context } from '../types.js'
import { validateSnapshot } from './schemaValidation.js'
import { pingIntervalId } from './utils.js';
import { startPolling } from './utils.js';
import { pingIntervalId, startPollingForTunnel, stopTunnelHelper, isTunnelPolling } from './utils.js';

const uploadDomToS3ViaEnv = process.env.USE_LAMBDA_INTERNAL || false;
export default async (ctx: Context): Promise<FastifyInstance<Server, IncomingMessage, ServerResponse>> => {
Expand Down Expand Up @@ -38,6 +37,12 @@ export default async (ctx: Context): Promise<FastifyInstance<Server, IncomingMes
try {
let { snapshot, testType } = request.body;
if (!validateSnapshot(snapshot)) throw new Error(validateSnapshot.errors[0].message);

if(snapshot?.options?.approvalThreshold !== undefined && snapshot?.options?.rejectionThreshold !== undefined) {
if(snapshot?.options?.rejectionThreshold <= snapshot?.options?.approvalThreshold) {
throw new Error(`Invalid snapshot options; rejectionThreshold (${snapshot.options.rejectionThreshold}) must be greater than approvalThreshold (${snapshot.options.approvalThreshold})`);
}
}

// Fetch sessionId from snapshot options if present
const sessionId = snapshot?.options?.sessionId;
Expand All @@ -53,7 +58,7 @@ export default async (ctx: Context): Promise<FastifyInstance<Server, IncomingMes
} else {
// If not cached, fetch from API and cache it
try {
let fetchedCapabilitiesResp = await ctx.client.getSmartUICapabilities(sessionId, ctx.config, ctx.git, ctx.log);
let fetchedCapabilitiesResp = await ctx.client.getSmartUICapabilities(sessionId, ctx.config, ctx.git, ctx.log, ctx.isStartExec);
capsBuildId = fetchedCapabilitiesResp?.buildId || ''
ctx.log.debug(`fetch caps for sessionId: ${sessionId} are ${JSON.stringify(fetchedCapabilitiesResp)}`)
if (capsBuildId) {
Expand Down Expand Up @@ -106,6 +111,7 @@ export default async (ctx: Context): Promise<FastifyInstance<Server, IncomingMes
let replyCode: number;
let replyBody: Record<string, any>;
try {
ctx.log.info('Received stop command. Finalizing build ...');
if(ctx.config.delayedUpload){
ctx.log.debug("started after processing because of delayedUpload")
ctx.snapshotQueue?.startProcessingfunc()
Expand All @@ -118,6 +124,7 @@ export default async (ctx: Context): Promise<FastifyInstance<Server, IncomingMes
}
}, 1000);
})
let buildUrls = `build url: ${ctx.build.url}\n`;

for (const [sessionId, capabilities] of ctx.sessionCapabilitiesMap.entries()) {
try {
Expand All @@ -126,9 +133,12 @@ export default async (ctx: Context): Promise<FastifyInstance<Server, IncomingMes
const totalSnapshots = capabilities?.snapshotCount || 0;
const sessionBuildUrl = capabilities?.buildURL || '';
const testId = capabilities?.id || '';

ctx.log.debug(`Capabilities for sessionId ${sessionId}: ${JSON.stringify(capabilities)}`)
if (buildId && projectToken) {
await ctx.client.finalizeBuildForCapsWithToken(buildId, totalSnapshots, projectToken, ctx.log);
if (ctx.autoTunnelStarted) {
await startPollingForTunnel(ctx, buildId, false, projectToken, capabilities?.buildName);
}
}

if (testId && buildId) {
Expand All @@ -151,6 +161,16 @@ export default async (ctx: Context): Promise<FastifyInstance<Server, IncomingMes
}
}


//If Tunnel Details are present, start polling for tunnel status
if (ctx.tunnelDetails && ctx.tunnelDetails.tunnelHost != "" && ctx.build?.id) {
await startPollingForTunnel(ctx, ctx.build.id, false, '', '');
}
//stop the tunnel if it was auto started and no tunnel polling is active
if (ctx.autoTunnelStarted && isTunnelPolling === null) {
await stopTunnelHelper(ctx);
}

await ctx.browser?.close();
if (ctx.server){
ctx.server.close();
Expand All @@ -168,7 +188,9 @@ export default async (ctx: Context): Promise<FastifyInstance<Server, IncomingMes
replyCode = 500;
replyBody = { error: { message: error.message } };
}


ctx.log.info('Stop command processed. Tearing down server.');

// Step 5: Return the response
return reply.code(replyCode).send(replyBody);
});
Expand Down
6 changes: 4 additions & 2 deletions src/lib/snapshotQueue.ts
Original file line number Diff line number Diff line change
Expand Up @@ -380,7 +380,7 @@ export default class Queue {
useKafkaFlow: resp.data.useKafkaFlow || false,
}
} else {
if (this.ctx.config.tunnel && this.ctx.config.tunnel?.type === 'auto') {
if (this.ctx.autoTunnelStarted) {
await stopTunnelHelper(this.ctx)
}
throw new Error('SmartUI capabilities are missing in env variables or in driver capabilities');
Expand Down Expand Up @@ -427,7 +427,9 @@ export default class Queue {
}
this.processNext();
} else {
await this.ctx.client.processSnapshot(this.ctx, processedSnapshot, snapshotUuid, discoveryErrors,calculateVariantCountFromSnapshot(processedSnapshot, this.ctx.config),snapshot?.options?.sync);
let approvalThreshold = snapshot?.options?.approvalThreshold || this.ctx.config.approvalThreshold;
let rejectionThreshold = snapshot?.options?.rejectionThreshold || this.ctx.config.rejectionThreshold;
await this.ctx.client.processSnapshot(this.ctx, processedSnapshot, snapshotUuid, discoveryErrors,calculateVariantCountFromSnapshot(processedSnapshot, this.ctx.config),snapshot?.options?.sync, approvalThreshold, rejectionThreshold);
if(snapshot?.options?.contextId && this.ctx.contextToSnapshotMap?.has(snapshot.options.contextId)){
this.ctx.contextToSnapshotMap.set(snapshot.options.contextId, 1);
}
Expand Down
1 change: 1 addition & 0 deletions src/lib/uploadAppFigma.ts
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@ export default async (ctx: Context): Promise<string> => {
smartIgnore: ctx.config.smartIgnore,
git: ctx.git,
platformType: 'app',
markBaseline: ctx.options.markBaseline,
};

const responseData = await ctx.client.processWebFigma(requestBody, ctx.log);
Expand Down
1 change: 1 addition & 0 deletions src/lib/uploadWebFigma.ts
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@ export default async (ctx: Context): Promise<string> => {
figma: figmaConfig,
smartIgnore: ctx.config.smartIgnore,
git: ctx.git,
markBaseline: ctx.options.markBaseline,
};

const responseData = await ctx.client.processWebFigma(requestBody, ctx.log);
Expand Down
Loading