Skip to content

Commit 1a4c6a1

Browse files
Merge branch 'stage' into Dot-6351
2 parents 5f2ed07 + 7e52cea commit 1a4c6a1

File tree

14 files changed

+322
-49
lines changed

14 files changed

+322
-49
lines changed

src/commander/exec.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,7 @@ command
2525
.option('--scheduled <string>', 'Specify the schedule ID')
2626
.option('--userName <string>', 'Specify the LT username')
2727
.option('--accessKey <string>', 'Specify the LT accesskey')
28+
.option('--show-render-errors', 'Show render errors from SmartUI build')
2829
.action(async function(execCommand, _, command) {
2930
const options = command.optsWithGlobals();
3031
if (options.buildName === '') {

src/commander/uploadPdf.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,7 @@ command
1414
.argument('<directory>', 'Path of the directory containing PDFs')
1515
.option('--fetch-results [filename]', 'Fetch results and optionally specify an output file, e.g., <filename>.json')
1616
.option('--buildName <string>', 'Specify the build name')
17+
.option('--markBaseline', 'Mark this build baseline')
1718
.action(async function(directory, _, command) {
1819
const options = command.optsWithGlobals();
1920
if (options.buildName === '') {

src/lib/constants.ts

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -25,7 +25,8 @@ export default {
2525
waitForTimeout: 1000,
2626
enableJavaScript: false,
2727
allowedHostnames: [],
28-
smartIgnore: false
28+
smartIgnore: false,
29+
showRenderErrors: false
2930
},
3031
DEFAULT_WEB_STATIC_CONFIG: [
3132
{

src/lib/ctx.ts

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -155,6 +155,7 @@ export default (options: Record<string, string>): Context => {
155155
loadDomContent: loadDomContent,
156156
approvalThreshold: config.approvalThreshold,
157157
rejectionThreshold: config.rejectionThreshold,
158+
showRenderErrors: config.showRenderErrors ?? false
158159
},
159160
uploadFilePath: '',
160161
webStaticConfig: [],
@@ -192,7 +193,8 @@ export default (options: Record<string, string>): Context => {
192193
fetchResultsFileName: fetchResultsFileObj,
193194
baselineBranch: options.baselineBranch || '',
194195
baselineBuild: options.baselineBuild || '',
195-
githubURL : options.githubURL || ''
196+
githubURL : options.githubURL || '',
197+
showRenderErrors: options.showRenderErrors ? true : false
196198
},
197199
cliVersion: version,
198200
totalSnapshots: -1,

src/lib/env.ts

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -22,7 +22,9 @@ export default (): Env => {
2222
SMARTUI_API_PROXY,
2323
SMARTUI_API_SKIP_CERTIFICATES,
2424
USE_REMOTE_DISCOVERY,
25-
SMART_GIT
25+
SMART_GIT,
26+
SHOW_RENDER_ERRORS,
27+
SMARTUI_SSE_URL='https://server-events.lambdatest.com'
2628
} = process.env
2729

2830
return {
@@ -46,6 +48,8 @@ export default (): Env => {
4648
SMARTUI_API_PROXY,
4749
SMARTUI_API_SKIP_CERTIFICATES: SMARTUI_API_SKIP_CERTIFICATES === 'true',
4850
USE_REMOTE_DISCOVERY: USE_REMOTE_DISCOVERY === 'true',
49-
SMART_GIT: SMART_GIT === 'true'
51+
SMART_GIT: SMART_GIT === 'true',
52+
SHOW_RENDER_ERRORS: SHOW_RENDER_ERRORS === 'true',
53+
SMARTUI_SSE_URL
5054
}
5155
}

src/lib/httpClient.ts

Lines changed: 46 additions & 24 deletions
Original file line numberDiff line numberDiff line change
@@ -375,47 +375,66 @@ export default class httpClient {
375375
}, ctx.log)
376376
}
377377

378-
processSnapshotCaps(ctx: Context, snapshot: ProcessedSnapshot, snapshotUuid: string, capsBuildId: string, capsProjectToken: string, discoveryErrors: DiscoveryErrors) {
378+
processSnapshotCaps(ctx: Context, snapshot: ProcessedSnapshot, snapshotUuid: string, capsBuildId: string, capsProjectToken: string, discoveryErrors: DiscoveryErrors, variantCount: number, sync: boolean = false, approvalThreshold: number| undefined, rejectionThreshold: number| undefined) {
379+
const requestData: any = {
380+
name: snapshot.name,
381+
url: snapshot.url,
382+
snapshotUuid: snapshotUuid,
383+
variantCount: variantCount,
384+
test: {
385+
type: ctx.testType,
386+
source: 'cli'
387+
},
388+
doRemoteDiscovery: snapshot.options.doRemoteDiscovery,
389+
discoveryErrors: discoveryErrors,
390+
sync: sync
391+
}
392+
if (approvalThreshold !== undefined) {
393+
requestData.approvalThreshold = approvalThreshold;
394+
}
395+
if (rejectionThreshold !== undefined) {
396+
requestData.rejectionThreshold = rejectionThreshold;
397+
}
379398
return this.request({
380399
url: `/build/${capsBuildId}/snapshot`,
381400
method: 'POST',
382401
headers: {
383402
'Content-Type': 'application/json',
384403
projectToken: capsProjectToken !== '' ? capsProjectToken : this.projectToken
385404
},
386-
data: {
387-
name: snapshot.name,
388-
url: snapshot.url,
389-
snapshotUuid: snapshotUuid,
390-
test: {
391-
type: ctx.testType,
392-
source: 'cli'
393-
},
394-
doRemoteDiscovery: snapshot.options.doRemoteDiscovery,
395-
discoveryErrors: discoveryErrors,
396-
}
405+
data: requestData
397406
}, ctx.log)
398407
}
399408

400-
uploadSnapshotForCaps(ctx: Context, snapshot: ProcessedSnapshot, capsBuildId: string, capsProjectToken: string, discoveryErrors: DiscoveryErrors) {
409+
uploadSnapshotForCaps(ctx: Context, snapshot: ProcessedSnapshot, capsBuildId: string, capsProjectToken: string, discoveryErrors: DiscoveryErrors, variantCount: number, sync: boolean = false, approvalThreshold: number| undefined, rejectionThreshold: number| undefined) {
401410
// Use capsBuildId if provided, otherwise fallback to ctx.build.id
402411
const buildId = capsBuildId !== '' ? capsBuildId : ctx.build.id;
403-
412+
413+
const requestData: any = {
414+
snapshot,
415+
test: {
416+
type: ctx.testType,
417+
source: 'cli'
418+
},
419+
discoveryErrors: discoveryErrors,
420+
variantCount: variantCount,
421+
sync: sync
422+
}
423+
if (approvalThreshold !== undefined) {
424+
requestData.approvalThreshold = approvalThreshold;
425+
}
426+
if (rejectionThreshold !== undefined) {
427+
requestData.rejectionThreshold = rejectionThreshold;
428+
}
429+
404430
return this.request({
405431
url: `/builds/${buildId}/snapshot`,
406432
method: 'POST',
407433
headers: {
408434
'Content-Type': 'application/json',
409435
projectToken: capsProjectToken !== '' ? capsProjectToken : this.projectToken // Use capsProjectToken dynamically
410436
},
411-
data: {
412-
snapshot,
413-
test: {
414-
type: ctx.testType,
415-
source: 'cli'
416-
},
417-
discoveryErrors: discoveryErrors,
418-
}
437+
data: requestData
419438
}, ctx.log);
420439
}
421440

@@ -660,9 +679,9 @@ export default class httpClient {
660679
}, ctx.log)
661680
}
662681

663-
getSnapshotStatus(snapshotName: string, snapshotUuid: string, ctx: Context): Promise<Record<string, any>> {
682+
getSnapshotStatus(buildId: string, snapshotName: string, snapshotUuid: string, ctx: Context): Promise<Record<string, any>> {
664683
return this.request({
665-
url: `/snapshot/status?buildId=${ctx.build.id}&snapshotName=${snapshotName}&snapshotUUID=${snapshotUuid}`,
684+
url: `/snapshot/status?buildId=${buildId}&snapshotName=${snapshotName}&snapshotUUID=${snapshotUuid}`,
666685
method: 'GET',
667686
headers: {
668687
'Content-Type': 'application/json',
@@ -675,6 +694,9 @@ export default class httpClient {
675694
if (ctx.build.name !== undefined && ctx.build.name !== '') {
676695
form.append('buildName', buildName);
677696
}
697+
if (ctx.options.markBaseline) {
698+
form.append('markBaseline', ctx.options.markBaseline.toString());
699+
}
678700

679701
try {
680702
const response = await this.axiosInstance.request({

src/lib/processSnapshot.ts

Lines changed: 43 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -243,6 +243,47 @@ export default async function processSnapshot(snapshot: Snapshot, ctx: Context):
243243
ctx.log.debug('No valid cookies to add');
244244
}
245245
}
246+
247+
let options = snapshot.options;
248+
249+
// Custom cookies include those which cannot be captured by javascript function `document.cookie` like httpOnly, secure, sameSite etc.
250+
// These custom cookies will be captured by the user in their automation browser and sent to CLI through the snapshot options using `customCookies` field.
251+
if (options?.customCookies && Array.isArray(options.customCookies) && options.customCookies.length > 0) {
252+
ctx.log.debug(`Setting ${options.customCookies.length} custom cookies`);
253+
254+
const validCustomCookies = options.customCookies.filter(cookie => {
255+
if (!cookie.name || !cookie.value || !cookie.domain) {
256+
ctx.log.debug(`Skipping invalid custom cookie: missing required fields (name, value, or domain)`);
257+
return false;
258+
}
259+
260+
if (cookie.sameSite && !['Strict', 'Lax', 'None'].includes(cookie.sameSite)) {
261+
ctx.log.debug(`Skipping invalid custom cookie: invalid sameSite value '${cookie.sameSite}'`);
262+
return false;
263+
}
264+
265+
return true;
266+
}).map(cookie => ({
267+
name: cookie.name,
268+
value: cookie.value,
269+
domain: cookie.domain,
270+
path: cookie.path || '/',
271+
httpOnly: cookie.httpOnly || false,
272+
secure: cookie.secure || false,
273+
sameSite: cookie.sameSite || 'Lax'
274+
}));
275+
276+
if (validCustomCookies.length > 0) {
277+
try {
278+
await context.addCookies(validCustomCookies);
279+
ctx.log.debug(`Successfully added ${validCustomCookies.length} custom cookies`);
280+
} catch (error) {
281+
ctx.log.debug(`Failed to add custom cookies: ${error}`);
282+
}
283+
} else {
284+
ctx.log.debug('No valid custom cookies to add');
285+
}
286+
}
246287
const page = await context.newPage();
247288

248289
// populate cache with already captured resources
@@ -415,8 +456,6 @@ export default async function processSnapshot(snapshot: Snapshot, ctx: Context):
415456
route.abort();
416457
}
417458
});
418-
419-
let options = snapshot.options;
420459
let optionWarnings: Set<string> = new Set();
421460
let selectors: Array<string> = [];
422461
let ignoreOrSelectDOM: string;
@@ -582,6 +621,7 @@ export default async function processSnapshot(snapshot: Snapshot, ctx: Context):
582621
// adding extra timeout since domcontentloaded event is fired pretty quickly
583622
await new Promise(r => setTimeout(r, 1250));
584623
if (ctx.config.waitForTimeout) await page.waitForTimeout(ctx.config.waitForTimeout);
624+
await page.waitForLoadState("networkidle", { timeout: 10000 }).catch(() => { ctx.log.debug('networkidle event failed to fire within 10s') });
585625
navigated = true;
586626
ctx.log.debug(`Navigated to ${snapshot.url}`);
587627
} catch (error: any) {
@@ -605,7 +645,7 @@ export default async function processSnapshot(snapshot: Snapshot, ctx: Context):
605645
if (ctx.config.cliEnableJavaScript && fullPage) await page.evaluate(scrollToBottomAndBackToTop, { frequency: 100, timing: ctx.config.scrollTime });
606646

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

816856
if (hasBrowserErrors) {
817857
discoveryErrors.timestamp = new Date().toISOString();
818-
// ctx.log.warn(discoveryErrors);
819858
}
820859

821860
if (ctx.config.useGlobalCache) {

src/lib/schemaValidation.ts

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -295,6 +295,10 @@ const ConfigSchema = {
295295
minimum: 0,
296296
maximum: 100,
297297
errorMessage: "Invalid config; rejectionThreshold must be a number"
298+
},
299+
showRenderErrors: {
300+
type: "boolean",
301+
errorMessage: "Invalid config; showRenderErrors must be true/false"
298302
}
299303
},
300304
anyOf: [
@@ -582,6 +586,14 @@ const SnapshotSchema: JSONSchemaType<Snapshot> = {
582586
minimum: 0,
583587
maximum: 100,
584588
errorMessage: "Invalid snapshot options; rejectionThreshold must be a number between 0 and 100"
589+
},
590+
customCookies: {
591+
type: "array",
592+
items: {
593+
type: "object",
594+
minProperties: 1,
595+
},
596+
errorMessage: "Invalid snapshot options; customCookies must be an array of objects with string properties"
585597
}
586598
},
587599
additionalProperties: false

src/lib/screenshot.ts

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -25,7 +25,9 @@ async function captureScreenshotsForConfig(
2525
ctx.log.debug(`url: ${url} pageOptions: ${JSON.stringify(pageOptions)}`);
2626
let ssId = name.toLowerCase().replace(/\s/g, '_');
2727
let context: BrowserContext;
28-
let contextOptions: Record<string, any> = {};
28+
let contextOptions: Record<string, any> = {
29+
ignoreHTTPSErrors: ctx.config.ignoreHTTPSErrors
30+
};
2931
let page: Page;
3032
if (browserName == constants.CHROME) contextOptions.userAgent = constants.CHROME_USER_AGENT;
3133
else if (browserName == constants.FIREFOX) contextOptions.userAgent = constants.FIREFOX_USER_AGENT;

src/lib/server.ts

Lines changed: 15 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -118,7 +118,7 @@ export default async (ctx: Context): Promise<FastifyInstance<Server, IncomingMes
118118
}
119119

120120
if (contextId && ctx.contextToSnapshotMap) {
121-
ctx.contextToSnapshotMap.set(contextId, 0);
121+
ctx.contextToSnapshotMap.set(contextId, '0');
122122
ctx.log.debug(`Marking contextId as captured and added to queue: ${contextId}`);
123123
}
124124

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

255-
while (contextStatus==0) {
255+
let counter= 60;
256+
while (contextStatus==='0') {
257+
if(counter<=0){
258+
throw new Error('Snapshot processing failed');
259+
}
260+
contextStatus = ctx.contextToSnapshotMap.get(contextId);
256261
// Wait 5 seconds before next check
257262
await new Promise(resolve => setTimeout(resolve, 5000));
258-
259-
contextStatus = ctx.contextToSnapshotMap.get(contextId);
263+
counter--;
260264
}
261265

262-
if(contextStatus==2){
266+
if(contextStatus==='2'){
263267
throw new Error("Snapshot Failed");
264268
}
265269

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

272+
const buildId = contextStatus;
273+
if (!buildId) {
274+
throw new Error(`No buildId found for contextId: ${contextId}`);
275+
}
276+
268277
// Poll external API until it returns 200 or timeout is reached
269278
let lastExternalResponse: any = null;
270279
const startTime = Date.now();
271280

272281
while (true) {
273282
try {
274283
const externalResponse = await ctx.client.getSnapshotStatus(
284+
buildId,
275285
snapshotName,
276286
contextId,
277287
ctx

0 commit comments

Comments
 (0)