Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
40 commits
Select commit Hold shift + click to select a range
83479fb
Add support for custom cookies in snapshot options and enhance schema…
lt-zeeshan Sep 26, 2025
ae6971f
Reduce timeout for networkidle event in snapshot processing
lt-zeeshan Sep 26, 2025
cece758
show rendering errors on terminal
Nick-1234531 Sep 29, 2025
9d50c7f
minor change
Nick-1234531 Sep 29, 2025
9fdc7d8
dont throw error if SSE fails
Nick-1234531 Sep 29, 2025
d8e72e9
remove process.exit() if sse fails
Nick-1234531 Sep 29, 2025
638ac9d
add support for latest devices
parthlambdatest Sep 29, 2025
13477a1
Add option to mark baseline in PDF upload and HTTP client
lt-zeeshan Sep 30, 2025
180ad5c
Changes
lt-zeeshan Sep 30, 2025
94e17bb
added exit on build complete
Nick-1234531 Sep 30, 2025
43c6f7a
changed log
Nick-1234531 Oct 1, 2025
e5d94b0
on error dont exit
Nick-1234531 Oct 1, 2025
71f3169
added show render errors in options and config
Nick-1234531 Oct 3, 2025
2278246
revert
Nick-1234531 Oct 3, 2025
ba3fcbc
Merge pull request #379 from Nick-1234531/DOT-6238
sushobhit-lt Oct 3, 2025
2633ee2
fix fetch-pdf-results for smart-ignore
shrinishLT Oct 3, 2025
11eba11
Merge pull request #381 from shrinishLT/DOT-6386
sushobhit-lt Oct 4, 2025
e42432a
Merge pull request #383 from lt-zeeshan/DOT-6362
parthlambdatest Oct 6, 2025
1bd8662
Changes
lt-zeeshan Oct 6, 2025
29b7932
Merge pull request #378 from lt-zeeshan/DOT-6319
parthlambdatest Oct 6, 2025
12e6cac
Revert "show rendering errors on terminal"
parthlambdatest Oct 7, 2025
d756b06
Merge pull request #384 from LambdaTest/revert-379-DOT-6238
parthlambdatest Oct 7, 2025
0106225
add support of ignoreHTTPSErrors for capture-cmd
shrinishLT Oct 7, 2025
05837d1
Merge pull request #387 from shrinishLT/DOT-6482
parthlambdatest Oct 8, 2025
e463674
fix timeout
shrinishLT Oct 8, 2025
cd5e936
Merge pull request #389 from shrinishLT/fix-timeout
parthlambdatest Oct 8, 2025
97c8b88
added a fix for veeva
shreyansh-chandel Oct 8, 2025
00aaa22
4.1.37-beta.0
shreyansh-chandel Oct 8, 2025
af63e38
resolved comments
shreyansh-chandel Oct 8, 2025
eab0af5
Merge pull request #390 from shreyansh-chandel/veeva-fix
sushobhit-lt Oct 8, 2025
64ab7f9
Merge pull request #392 from parthlambdatest/Dot-6351
sushobhit-lt Oct 10, 2025
f8ece8f
update device names
parthlambdatest Oct 12, 2025
0c9b202
added fix for mismatch threshold
shreyansh-chandel Oct 13, 2025
6312523
Merge pull request #393 from LambdaTest/Dot-6351
sushobhit-lt Oct 13, 2025
5332c58
Merge pull request #394 from shreyansh-chandel/mismatch-threshold-fix
sushobhit-lt Oct 13, 2025
f9d894d
Revert "Revert "show rendering errors on terminal""
Nick-1234531 Oct 13, 2025
7e52cea
Merge pull request #395 from Nick-1234531/revert-384-revert-379-DOT-6238
sushobhit-lt Oct 13, 2025
4a93721
update galaxy s25 name
parthlambdatest Oct 13, 2025
5f2ed07
version bump
parthlambdatest Oct 13, 2025
1a4c6a1
Merge branch 'stage' into Dot-6351
parthlambdatest Oct 13, 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
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.34",
"version": "4.1.37",
"description": "A command line interface (CLI) to run SmartUI tests on LambdaTest",
"files": [
"dist/**/*"
Expand Down
1 change: 1 addition & 0 deletions src/commander/exec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,7 @@ command
.option('--scheduled <string>', 'Specify the schedule ID')
.option('--userName <string>', 'Specify the LT username')
.option('--accessKey <string>', 'Specify the LT accesskey')
.option('--show-render-errors', 'Show render errors from SmartUI build')
.action(async function(execCommand, _, command) {
const options = command.optsWithGlobals();
if (options.buildName === '') {
Expand Down
1 change: 1 addition & 0 deletions src/commander/uploadPdf.ts
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@ command
.argument('<directory>', 'Path of the directory containing PDFs')
.option('--fetch-results [filename]', 'Fetch results and optionally specify an output file, e.g., <filename>.json')
.option('--buildName <string>', 'Specify the build name')
.option('--markBaseline', 'Mark this build baseline')
.action(async function(directory, _, command) {
const options = command.optsWithGlobals();
if (options.buildName === '') {
Expand Down
19 changes: 18 additions & 1 deletion src/lib/constants.ts
Original file line number Diff line number Diff line change
Expand Up @@ -25,7 +25,8 @@ export default {
waitForTimeout: 1000,
enableJavaScript: false,
allowedHostnames: [],
smartIgnore: false
smartIgnore: false,
showRenderErrors: false
},
DEFAULT_WEB_STATIC_CONFIG: [
{
Expand Down Expand Up @@ -358,6 +359,22 @@ export default {
'Aquos Sense 5G': { os: 'android', viewport: { width: 393, height: 731 } },
'Xperia 10 IV': { os: 'android', viewport: { width: 412, height: 832 } },
'Honeywell CT40': { os: 'android', viewport: { width: 360, height: 512 } },
'Galaxy S25': { os: 'android', viewport: { width: 370, height: 802 } },
'Galaxy S25 Plus': { os: 'android', viewport: { width: 393, height: 888 } },
'Galaxy S25 Ultra': { os: 'android', viewport: { width: 432, height: 941 } },
'iPhone 17': { os: 'ios', viewport: { width: 393, height: 852 } },
'iPhone 17 Pro': { os: 'ios', viewport: { width: 393, height: 852 } },
'iPhone 17 Pro Max': { os: 'ios', viewport: { width: 430, height: 932 } },
'Galaxy Z Fold7': { os: 'android', viewport: { width: 373, height: 873 } },
'Galaxy Z Flip7': { os: 'android', viewport: { width: 299, height: 723 } },
'Galaxy Z Fold6': { os: 'android', viewport: { width: 373, height: 873 } },
'Galaxy Z Flip6': { os: 'android', viewport: { width: 298, height: 713 } },
'Pixel 10 Pro': { os: 'android', viewport: { width: 393, height: 852 } },
'Pixel 10 Pro XL': { os: 'android', viewport: { width: 412, height: 915 } },
'Motorola Edge 50 Pro': { os: 'android', viewport: { width: 384, height: 864 } },
'OnePlus 12': { os: 'android', viewport: { width: 384, height: 884 } },
'Nothing Phone 1': { os: 'android', viewport: { width: 393, height: 853 } },
'Nothing Phone 2': { os: 'android', viewport: { width: 393, height: 878 } },
},

FIGMA_API: 'https://api.figma.com/v1/',
Expand Down
4 changes: 3 additions & 1 deletion src/lib/ctx.ts
Original file line number Diff line number Diff line change
Expand Up @@ -155,6 +155,7 @@ export default (options: Record<string, string>): Context => {
loadDomContent: loadDomContent,
approvalThreshold: config.approvalThreshold,
rejectionThreshold: config.rejectionThreshold,
showRenderErrors: config.showRenderErrors ?? false
},
uploadFilePath: '',
webStaticConfig: [],
Expand Down Expand Up @@ -192,7 +193,8 @@ export default (options: Record<string, string>): Context => {
fetchResultsFileName: fetchResultsFileObj,
baselineBranch: options.baselineBranch || '',
baselineBuild: options.baselineBuild || '',
githubURL : options.githubURL || ''
githubURL : options.githubURL || '',
showRenderErrors: options.showRenderErrors ? true : false
},
cliVersion: version,
totalSnapshots: -1,
Expand Down
8 changes: 6 additions & 2 deletions src/lib/env.ts
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,9 @@ export default (): Env => {
SMARTUI_API_PROXY,
SMARTUI_API_SKIP_CERTIFICATES,
USE_REMOTE_DISCOVERY,
SMART_GIT
SMART_GIT,
SHOW_RENDER_ERRORS,
SMARTUI_SSE_URL='https://server-events.lambdatest.com'
} = process.env

return {
Expand All @@ -46,6 +48,8 @@ export default (): Env => {
SMARTUI_API_PROXY,
SMARTUI_API_SKIP_CERTIFICATES: SMARTUI_API_SKIP_CERTIFICATES === 'true',
USE_REMOTE_DISCOVERY: USE_REMOTE_DISCOVERY === 'true',
SMART_GIT: SMART_GIT === 'true'
SMART_GIT: SMART_GIT === 'true',
SHOW_RENDER_ERRORS: SHOW_RENDER_ERRORS === 'true',
SMARTUI_SSE_URL
}
}
70 changes: 46 additions & 24 deletions src/lib/httpClient.ts
Original file line number Diff line number Diff line change
Expand Up @@ -375,47 +375,66 @@ export default class httpClient {
}, ctx.log)
}

processSnapshotCaps(ctx: Context, snapshot: ProcessedSnapshot, snapshotUuid: string, capsBuildId: string, capsProjectToken: string, discoveryErrors: DiscoveryErrors) {
processSnapshotCaps(ctx: Context, snapshot: ProcessedSnapshot, snapshotUuid: string, capsBuildId: string, capsProjectToken: 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'
},
doRemoteDiscovery: snapshot.options.doRemoteDiscovery,
discoveryErrors: discoveryErrors,
sync: sync
}
if (approvalThreshold !== undefined) {
requestData.approvalThreshold = approvalThreshold;
}
if (rejectionThreshold !== undefined) {
requestData.rejectionThreshold = rejectionThreshold;
}
return this.request({
url: `/build/${capsBuildId}/snapshot`,
method: 'POST',
headers: {
'Content-Type': 'application/json',
projectToken: capsProjectToken !== '' ? capsProjectToken : this.projectToken
},
data: {
name: snapshot.name,
url: snapshot.url,
snapshotUuid: snapshotUuid,
test: {
type: ctx.testType,
source: 'cli'
},
doRemoteDiscovery: snapshot.options.doRemoteDiscovery,
discoveryErrors: discoveryErrors,
}
data: requestData
}, ctx.log)
}

uploadSnapshotForCaps(ctx: Context, snapshot: ProcessedSnapshot, capsBuildId: string, capsProjectToken: string, discoveryErrors: DiscoveryErrors) {
uploadSnapshotForCaps(ctx: Context, snapshot: ProcessedSnapshot, capsBuildId: string, capsProjectToken: string, discoveryErrors: DiscoveryErrors, variantCount: number, sync: boolean = false, approvalThreshold: number| undefined, rejectionThreshold: number| undefined) {
// Use capsBuildId if provided, otherwise fallback to ctx.build.id
const buildId = capsBuildId !== '' ? capsBuildId : ctx.build.id;


const requestData: any = {
snapshot,
test: {
type: ctx.testType,
source: 'cli'
},
discoveryErrors: discoveryErrors,
variantCount: variantCount,
sync: sync
}
if (approvalThreshold !== undefined) {
requestData.approvalThreshold = approvalThreshold;
}
if (rejectionThreshold !== undefined) {
requestData.rejectionThreshold = rejectionThreshold;
}

return this.request({
url: `/builds/${buildId}/snapshot`,
method: 'POST',
headers: {
'Content-Type': 'application/json',
projectToken: capsProjectToken !== '' ? capsProjectToken : this.projectToken // Use capsProjectToken dynamically
},
data: {
snapshot,
test: {
type: ctx.testType,
source: 'cli'
},
discoveryErrors: discoveryErrors,
}
data: requestData
}, ctx.log);
}

Expand Down Expand Up @@ -660,9 +679,9 @@ export default class httpClient {
}, ctx.log)
}

getSnapshotStatus(snapshotName: string, snapshotUuid: string, ctx: Context): Promise<Record<string, any>> {
getSnapshotStatus(buildId: string, snapshotName: string, snapshotUuid: string, ctx: Context): Promise<Record<string, any>> {
return this.request({
url: `/snapshot/status?buildId=${ctx.build.id}&snapshotName=${snapshotName}&snapshotUUID=${snapshotUuid}`,
url: `/snapshot/status?buildId=${buildId}&snapshotName=${snapshotName}&snapshotUUID=${snapshotUuid}`,
method: 'GET',
headers: {
'Content-Type': 'application/json',
Expand All @@ -675,6 +694,9 @@ export default class httpClient {
if (ctx.build.name !== undefined && ctx.build.name !== '') {
form.append('buildName', buildName);
}
if (ctx.options.markBaseline) {
form.append('markBaseline', ctx.options.markBaseline.toString());
}

try {
const response = await this.axiosInstance.request({
Expand Down
47 changes: 43 additions & 4 deletions src/lib/processSnapshot.ts
Original file line number Diff line number Diff line change
Expand Up @@ -243,6 +243,47 @@ export default async function processSnapshot(snapshot: Snapshot, ctx: Context):
ctx.log.debug('No valid cookies to add');
}
}

let options = snapshot.options;

// Custom cookies include those which cannot be captured by javascript function `document.cookie` like httpOnly, secure, sameSite etc.
// These custom cookies will be captured by the user in their automation browser and sent to CLI through the snapshot options using `customCookies` field.
if (options?.customCookies && Array.isArray(options.customCookies) && options.customCookies.length > 0) {
ctx.log.debug(`Setting ${options.customCookies.length} custom cookies`);

const validCustomCookies = options.customCookies.filter(cookie => {
if (!cookie.name || !cookie.value || !cookie.domain) {
ctx.log.debug(`Skipping invalid custom cookie: missing required fields (name, value, or domain)`);
return false;
}

if (cookie.sameSite && !['Strict', 'Lax', 'None'].includes(cookie.sameSite)) {
ctx.log.debug(`Skipping invalid custom cookie: invalid sameSite value '${cookie.sameSite}'`);
return false;
}

return true;
}).map(cookie => ({
name: cookie.name,
value: cookie.value,
domain: cookie.domain,
path: cookie.path || '/',
httpOnly: cookie.httpOnly || false,
secure: cookie.secure || false,
sameSite: cookie.sameSite || 'Lax'
}));

if (validCustomCookies.length > 0) {
try {
await context.addCookies(validCustomCookies);
ctx.log.debug(`Successfully added ${validCustomCookies.length} custom cookies`);
} catch (error) {
ctx.log.debug(`Failed to add custom cookies: ${error}`);
}
} else {
ctx.log.debug('No valid custom cookies to add');
}
}
const page = await context.newPage();

// populate cache with already captured resources
Expand Down Expand Up @@ -415,8 +456,6 @@ export default async function processSnapshot(snapshot: Snapshot, ctx: Context):
route.abort();
}
});

let options = snapshot.options;
let optionWarnings: Set<string> = new Set();
let selectors: Array<string> = [];
let ignoreOrSelectDOM: string;
Expand Down Expand Up @@ -582,6 +621,7 @@ export default async function processSnapshot(snapshot: Snapshot, ctx: Context):
// adding extra timeout since domcontentloaded event is fired pretty quickly
await new Promise(r => setTimeout(r, 1250));
if (ctx.config.waitForTimeout) await page.waitForTimeout(ctx.config.waitForTimeout);
await page.waitForLoadState("networkidle", { timeout: 10000 }).catch(() => { ctx.log.debug('networkidle event failed to fire within 10s') });
navigated = true;
ctx.log.debug(`Navigated to ${snapshot.url}`);
} catch (error: any) {
Expand All @@ -605,7 +645,7 @@ export default async function processSnapshot(snapshot: Snapshot, ctx: Context):
if (ctx.config.cliEnableJavaScript && fullPage) await page.evaluate(scrollToBottomAndBackToTop, { frequency: 100, timing: ctx.config.scrollTime });

try {
await page.waitForLoadState('networkidle', { timeout: 5000 });
await page.waitForLoadState('networkidle', { timeout: 15000 });
ctx.log.debug('Network idle 500ms');
} catch (error) {
ctx.log.debug(`Network idle failed due to ${error}`);
Expand Down Expand Up @@ -815,7 +855,6 @@ export default async function processSnapshot(snapshot: Snapshot, ctx: Context):

if (hasBrowserErrors) {
discoveryErrors.timestamp = new Date().toISOString();
// ctx.log.warn(discoveryErrors);
}

if (ctx.config.useGlobalCache) {
Expand Down
12 changes: 12 additions & 0 deletions src/lib/schemaValidation.ts
Original file line number Diff line number Diff line change
Expand Up @@ -295,6 +295,10 @@ const ConfigSchema = {
minimum: 0,
maximum: 100,
errorMessage: "Invalid config; rejectionThreshold must be a number"
},
showRenderErrors: {
type: "boolean",
errorMessage: "Invalid config; showRenderErrors must be true/false"
}
},
anyOf: [
Expand Down Expand Up @@ -582,6 +586,14 @@ const SnapshotSchema: JSONSchemaType<Snapshot> = {
minimum: 0,
maximum: 100,
errorMessage: "Invalid snapshot options; rejectionThreshold must be a number between 0 and 100"
},
customCookies: {
type: "array",
items: {
type: "object",
minProperties: 1,
},
errorMessage: "Invalid snapshot options; customCookies must be an array of objects with string properties"
}
},
additionalProperties: false
Expand Down
4 changes: 3 additions & 1 deletion src/lib/screenshot.ts
Original file line number Diff line number Diff line change
Expand Up @@ -25,7 +25,9 @@ async function captureScreenshotsForConfig(
ctx.log.debug(`url: ${url} pageOptions: ${JSON.stringify(pageOptions)}`);
let ssId = name.toLowerCase().replace(/\s/g, '_');
let context: BrowserContext;
let contextOptions: Record<string, any> = {};
let contextOptions: Record<string, any> = {
ignoreHTTPSErrors: ctx.config.ignoreHTTPSErrors
};
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 Down
20 changes: 15 additions & 5 deletions src/lib/server.ts
Original file line number Diff line number Diff line change
Expand Up @@ -118,7 +118,7 @@ export default async (ctx: Context): Promise<FastifyInstance<Server, IncomingMes
}

if (contextId && ctx.contextToSnapshotMap) {
ctx.contextToSnapshotMap.set(contextId, 0);
ctx.contextToSnapshotMap.set(contextId, '0');
ctx.log.debug(`Marking contextId as captured and added to queue: ${contextId}`);
}

Expand Down Expand Up @@ -252,26 +252,36 @@ export default async (ctx: Context): Promise<FastifyInstance<Server, IncomingMes
if (ctx.contextToSnapshotMap?.has(contextId)) {
let contextStatus = ctx.contextToSnapshotMap.get(contextId);

while (contextStatus==0) {
let counter= 60;
while (contextStatus==='0') {
if(counter<=0){
throw new Error('Snapshot processing failed');
}
contextStatus = ctx.contextToSnapshotMap.get(contextId);
// Wait 5 seconds before next check
await new Promise(resolve => setTimeout(resolve, 5000));

contextStatus = ctx.contextToSnapshotMap.get(contextId);
counter--;
}

if(contextStatus==2){
if(contextStatus==='2'){
throw new Error("Snapshot Failed");
}

ctx.log.debug("Snapshot uploaded successfully");

const buildId = contextStatus;
if (!buildId) {
throw new Error(`No buildId found for contextId: ${contextId}`);
}

// Poll external API until it returns 200 or timeout is reached
let lastExternalResponse: any = null;
const startTime = Date.now();

while (true) {
try {
const externalResponse = await ctx.client.getSnapshotStatus(
buildId,
snapshotName,
contextId,
ctx
Expand Down
Loading