Skip to content

Commit bd8e859

Browse files
Merge pull request #397 from LambdaTest/stage
Release PR `4.1.37`
2 parents 79c337e + 67bbc85 commit bd8e859

File tree

11 files changed

+240
-36
lines changed

11 files changed

+240
-36
lines changed

package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
{
22
"name": "@lambdatest/smartui-cli",
3-
"version": "4.1.37-beta.0",
3+
"version": "4.1.37",
44
"description": "A command line interface (CLI) to run SmartUI tests on LambdaTest",
55
"files": [
66
"dist/**/*"

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/lib/constants.ts

Lines changed: 18 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
{
@@ -358,6 +359,22 @@ export default {
358359
'Aquos Sense 5G': { os: 'android', viewport: { width: 393, height: 731 } },
359360
'Xperia 10 IV': { os: 'android', viewport: { width: 412, height: 832 } },
360361
'Honeywell CT40': { os: 'android', viewport: { width: 360, height: 512 } },
362+
'Galaxy S25': { os: 'android', viewport: { width: 370, height: 802 } },
363+
'Galaxy S25 Plus': { os: 'android', viewport: { width: 393, height: 888 } },
364+
'Galaxy S25 Ultra': { os: 'android', viewport: { width: 432, height: 941 } },
365+
'iPhone 17': { os: 'ios', viewport: { width: 393, height: 852 } },
366+
'iPhone 17 Pro': { os: 'ios', viewport: { width: 393, height: 852 } },
367+
'iPhone 17 Pro Max': { os: 'ios', viewport: { width: 430, height: 932 } },
368+
'Galaxy Z Fold7': { os: 'android', viewport: { width: 373, height: 873 } },
369+
'Galaxy Z Flip7': { os: 'android', viewport: { width: 299, height: 723 } },
370+
'Galaxy Z Fold6': { os: 'android', viewport: { width: 373, height: 873 } },
371+
'Galaxy Z Flip6': { os: 'android', viewport: { width: 298, height: 713 } },
372+
'Pixel 10 Pro': { os: 'android', viewport: { width: 393, height: 852 } },
373+
'Pixel 10 Pro XL': { os: 'android', viewport: { width: 412, height: 915 } },
374+
'Motorola Edge 50 Pro': { os: 'android', viewport: { width: 384, height: 864 } },
375+
'OnePlus 12': { os: 'android', viewport: { width: 384, height: 884 } },
376+
'Nothing Phone 1': { os: 'android', viewport: { width: 393, height: 853 } },
377+
'Nothing Phone 2': { os: 'android', viewport: { width: 393, height: 878 } },
361378
},
362379

363380
FIGMA_API: 'https://api.figma.com/v1/',

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: 41 additions & 26 deletions
Original file line numberDiff line numberDiff line change
@@ -375,51 +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, variantCount: number, sync: boolean = false) {
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-
variantCount: variantCount,
391-
test: {
392-
type: ctx.testType,
393-
source: 'cli'
394-
},
395-
doRemoteDiscovery: snapshot.options.doRemoteDiscovery,
396-
discoveryErrors: discoveryErrors,
397-
sync: sync
398-
}
405+
data: requestData
399406
}, ctx.log)
400407
}
401408

402-
uploadSnapshotForCaps(ctx: Context, snapshot: ProcessedSnapshot, capsBuildId: string, capsProjectToken: string, discoveryErrors: DiscoveryErrors, variantCount: number, sync: boolean = false) {
409+
uploadSnapshotForCaps(ctx: Context, snapshot: ProcessedSnapshot, capsBuildId: string, capsProjectToken: string, discoveryErrors: DiscoveryErrors, variantCount: number, sync: boolean = false, approvalThreshold: number| undefined, rejectionThreshold: number| undefined) {
403410
// Use capsBuildId if provided, otherwise fallback to ctx.build.id
404411
const buildId = capsBuildId !== '' ? capsBuildId : ctx.build.id;
405-
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+
406430
return this.request({
407431
url: `/builds/${buildId}/snapshot`,
408432
method: 'POST',
409433
headers: {
410434
'Content-Type': 'application/json',
411435
projectToken: capsProjectToken !== '' ? capsProjectToken : this.projectToken // Use capsProjectToken dynamically
412436
},
413-
data: {
414-
snapshot,
415-
test: {
416-
type: ctx.testType,
417-
source: 'cli'
418-
},
419-
discoveryErrors: discoveryErrors,
420-
variantCount: variantCount,
421-
sync: sync
422-
}
437+
data: requestData
423438
}, ctx.log);
424439
}
425440

src/lib/schemaValidation.ts

Lines changed: 4 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: [

src/lib/snapshotQueue.ts

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -360,9 +360,10 @@ export default class Queue {
360360
}
361361

362362

363-
364363
if (useCapsBuildId) {
365364
this.ctx.log.info(`Using cached buildId: ${capsBuildId}`);
365+
let approvalThreshold = snapshot?.options?.approvalThreshold || this.ctx.config.approvalThreshold;
366+
let rejectionThreshold = snapshot?.options?.rejectionThreshold || this.ctx.config.rejectionThreshold;
366367
if (useKafkaFlowCaps) {
367368
let snapshotUuid = uuidv4();
368369
if (snapshot?.options?.contextId && this.ctx.contextToSnapshotMap?.has(snapshot.options.contextId)) {
@@ -378,9 +379,9 @@ export default class Queue {
378379
this.ctx.log.debug(`Uploading dom to S3 for snapshot using LSRS`);
379380
await this.ctx.client.sendDomToLSRSForCaps(this.ctx, processedSnapshot, snapshotUuid, capsBuildId, capsProjectToken);
380381
}
381-
await this.ctx.client.processSnapshotCaps(this.ctx, processedSnapshot, snapshotUuid, capsBuildId, capsProjectToken, discoveryErrors, calculateVariantCountFromSnapshot(processedSnapshot, this.ctx.config), snapshot?.options?.sync);
382+
await this.ctx.client.processSnapshotCaps(this.ctx, processedSnapshot, snapshotUuid, capsBuildId, capsProjectToken, discoveryErrors, calculateVariantCountFromSnapshot(processedSnapshot, this.ctx.config), snapshot?.options?.sync, approvalThreshold, rejectionThreshold);
382383
} else {
383-
await this.ctx.client.uploadSnapshotForCaps(this.ctx, processedSnapshot, capsBuildId, capsProjectToken, discoveryErrors, calculateVariantCountFromSnapshot(processedSnapshot, this.ctx.config), snapshot?.options?.sync);
384+
await this.ctx.client.uploadSnapshotForCaps(this.ctx, processedSnapshot, capsBuildId, capsProjectToken, discoveryErrors, calculateVariantCountFromSnapshot(processedSnapshot, this.ctx.config), snapshot?.options?.sync, approvalThreshold, rejectionThreshold);
384385
}
385386

386387
// Increment snapshot count for the specific buildId

src/lib/utils.ts

Lines changed: 148 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -740,4 +740,152 @@ export function validateCoordinates(
740740
valid: true,
741741
coords: { top, bottom, left, right }
742742
};
743+
}
744+
745+
export function createBasicAuthToken(username: string, accessKey: string): string {
746+
const credentials = `${username}:${accessKey}`;
747+
return Buffer.from(credentials).toString('base64');
748+
}
749+
750+
export async function listenToSmartUISSE(
751+
baseURL: string,
752+
accessToken: string,
753+
ctx: Context,
754+
onEvent?: (eventType: string, data: any) => void
755+
): Promise<{ abort: () => void }> {
756+
const url = `${baseURL}/api/v1/sse/smartui`;
757+
758+
const abortController = new AbortController();
759+
760+
try {
761+
const response = await fetch(url, {
762+
method: 'GET',
763+
headers: {
764+
'Accept': 'text/event-stream',
765+
'Cache-Control': 'no-cache',
766+
'Cookie': `stageAccessToken=Basic ${accessToken}`
767+
},
768+
signal: abortController.signal
769+
});
770+
771+
if (!response.ok) {
772+
throw new Error(`HTTP error! status: ${response.status}`);
773+
}
774+
775+
onEvent?.('open', { status: 'connected' });
776+
777+
const reader = response.body?.getReader();
778+
if (!reader) {
779+
throw new Error('No response body reader available');
780+
}
781+
782+
const decoder = new TextDecoder();
783+
let buffer = '';
784+
let currentEvent = '';
785+
786+
try {
787+
while (true) {
788+
const { done, value } = await reader.read();
789+
if (done) break;
790+
791+
const chunk = decoder.decode(value, { stream: true });
792+
793+
buffer += chunk;
794+
const lines = buffer.split('\n');
795+
796+
buffer = lines.pop() || '';
797+
798+
for (const line of lines) {
799+
if (line.startsWith('event:')) {
800+
currentEvent = line.substring(6).trim();
801+
}
802+
else if (line.startsWith('data:')) {
803+
const data = line.substring(5).trim();
804+
805+
if (data) {
806+
try {
807+
const parsedData = JSON.parse(data);
808+
onEvent?.(currentEvent, parsedData);
809+
} catch (parseError) {
810+
if (currentEvent === 'connection' && data === 'connected') {
811+
onEvent?.(currentEvent, { status: 'connected', message: data });
812+
} else {
813+
onEvent?.(currentEvent, data);
814+
}
815+
}
816+
}
817+
}
818+
else if (line.trim() === '') {
819+
currentEvent = '';
820+
}
821+
}
822+
}
823+
} catch (streamError: any) {
824+
ctx.log.debug('SSE Streaming error:', streamError);
825+
onEvent?.('error', streamError);
826+
} finally {
827+
reader.releaseLock();
828+
}
829+
830+
} catch (error) {
831+
ctx.log.debug('SSE Connection error:', error);
832+
onEvent?.('error', error);
833+
}
834+
835+
return {
836+
abort: () => abortController.abort()
837+
};
838+
}
839+
840+
export async function startSSEListener(ctx: Context) {
841+
let currentConnection: { abort: () => void } | null = null;
842+
let errorCount = 0;
843+
844+
try {
845+
ctx.log.debug('Attempting SSE connection');
846+
const accessKey = ctx.env.LT_ACCESS_KEY;
847+
const username = ctx.env.LT_USERNAME;
848+
849+
const basicAuthToken = createBasicAuthToken(username, accessKey);
850+
ctx.log.debug(`Basic auth token: ${basicAuthToken}`);
851+
currentConnection = await listenToSmartUISSE(
852+
ctx.env.SMARTUI_SSE_URL,
853+
basicAuthToken,
854+
ctx,
855+
(eventType, data) => {
856+
switch (eventType) {
857+
case 'open':
858+
ctx.log.debug('Connected to SSE server');
859+
break;
860+
861+
case 'connection':
862+
ctx.log.debug('Connection confirmed:', data);
863+
break;
864+
865+
case 'Dot_buildCompleted':
866+
ctx.log.debug('Build completed');
867+
ctx.log.info(chalk.green.bold('Build completed'));
868+
process.exit(0);
869+
case 'DOTUIError':
870+
if (data.buildId== ctx.build.id) {
871+
errorCount++;
872+
ctx.log.info(chalk.red.bold(`Error: ${data.message}`));
873+
}
874+
break;
875+
case 'DOTUIWarning':
876+
if (data.buildId== ctx.build.id) {
877+
ctx.log.info(chalk.yellow.bold(`Warning: ${data.message}`));
878+
}
879+
break;
880+
case 'error':
881+
ctx.log.debug('SSE Error occurred:', data);
882+
currentConnection?.abort();
883+
return;
884+
}
885+
}
886+
);
887+
888+
} catch (error) {
889+
ctx.log.debug('Failed to start SSE listener:', error);
890+
}
743891
}

src/tasks/exec.ts

Lines changed: 9 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@ import { Context } from '../types.js'
33
import chalk from 'chalk'
44
import spawn from 'cross-spawn'
55
import { updateLogContext } from '../lib/logger.js'
6-
import { startPolling } from '../lib/utils.js'
6+
import { startPolling, startSSEListener } from '../lib/utils.js'
77

88
export default (ctx: Context): ListrTask<Context, ListrRendererFactory, ListrRendererFactory> => {
99
return {
@@ -16,6 +16,14 @@ export default (ctx: Context): ListrTask<Context, ListrRendererFactory, ListrRen
1616
}
1717
}
1818

19+
if((ctx.env.SHOW_RENDER_ERRORS||ctx.options.showRenderErrors||ctx.config.showRenderErrors) && ctx.build && ctx.build.id) {
20+
if(ctx.env.LT_USERNAME&&ctx.env.LT_ACCESS_KEY) {
21+
startSSEListener(ctx);
22+
} else {
23+
ctx.log.info('LT_USERNAME and LT_ACCESS_KEY are not set, set them to display render errors');
24+
}
25+
}
26+
1927
updateLogContext({task: 'exec'});
2028

2129
return new Promise((resolve, reject) => {

0 commit comments

Comments
 (0)