Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
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
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
3 changes: 2 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
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
}
}
4 changes: 4 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
148 changes: 148 additions & 0 deletions src/lib/utils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -740,4 +740,152 @@ export function validateCoordinates(
valid: true,
coords: { top, bottom, left, right }
};
}

export function createBasicAuthToken(username: string, accessKey: string): string {
const credentials = `${username}:${accessKey}`;
return Buffer.from(credentials).toString('base64');
}

export async function listenToSmartUISSE(
baseURL: string,
accessToken: string,
ctx: Context,
onEvent?: (eventType: string, data: any) => void
): Promise<{ abort: () => void }> {
const url = `${baseURL}/api/v1/sse/smartui`;

const abortController = new AbortController();

try {
const response = await fetch(url, {
method: 'GET',
headers: {
'Accept': 'text/event-stream',
'Cache-Control': 'no-cache',
'Cookie': `stageAccessToken=Basic ${accessToken}`
},
signal: abortController.signal
});

if (!response.ok) {
throw new Error(`HTTP error! status: ${response.status}`);
}

onEvent?.('open', { status: 'connected' });

const reader = response.body?.getReader();
if (!reader) {
throw new Error('No response body reader available');
}

const decoder = new TextDecoder();
let buffer = '';
let currentEvent = '';

try {
while (true) {
const { done, value } = await reader.read();
if (done) break;

const chunk = decoder.decode(value, { stream: true });

buffer += chunk;
const lines = buffer.split('\n');

buffer = lines.pop() || '';

for (const line of lines) {
if (line.startsWith('event:')) {
currentEvent = line.substring(6).trim();
}
else if (line.startsWith('data:')) {
const data = line.substring(5).trim();

if (data) {
try {
const parsedData = JSON.parse(data);
onEvent?.(currentEvent, parsedData);
} catch (parseError) {
if (currentEvent === 'connection' && data === 'connected') {
onEvent?.(currentEvent, { status: 'connected', message: data });
} else {
onEvent?.(currentEvent, data);
}
}
}
}
else if (line.trim() === '') {
currentEvent = '';
}
}
}
} catch (streamError: any) {
ctx.log.debug('SSE Streaming error:', streamError);
onEvent?.('error', streamError);
} finally {
reader.releaseLock();
}

} catch (error) {
ctx.log.debug('SSE Connection error:', error);
onEvent?.('error', error);
}

return {
abort: () => abortController.abort()
};
}

export async function startSSEListener(ctx: Context) {
let currentConnection: { abort: () => void } | null = null;
let errorCount = 0;

try {
ctx.log.debug('Attempting SSE connection');
const accessKey = ctx.env.LT_ACCESS_KEY;
const username = ctx.env.LT_USERNAME;

const basicAuthToken = createBasicAuthToken(username, accessKey);
ctx.log.debug(`Basic auth token: ${basicAuthToken}`);
currentConnection = await listenToSmartUISSE(
ctx.env.SMARTUI_SSE_URL,
basicAuthToken,
ctx,
(eventType, data) => {
switch (eventType) {
case 'open':
ctx.log.debug('Connected to SSE server');
break;

case 'connection':
ctx.log.debug('Connection confirmed:', data);
break;

case 'Dot_buildCompleted':
ctx.log.debug('Build completed');
ctx.log.info(chalk.green.bold('Build completed'));
process.exit(0);
case 'DOTUIError':
if (data.buildId== ctx.build.id) {
errorCount++;
ctx.log.info(chalk.red.bold(`Error: ${data.message}`));
}
break;
case 'DOTUIWarning':
if (data.buildId== ctx.build.id) {
ctx.log.info(chalk.yellow.bold(`Warning: ${data.message}`));
}
break;
case 'error':
ctx.log.debug('SSE Error occurred:', data);
currentConnection?.abort();
return;
}
}
);

} catch (error) {
ctx.log.debug('Failed to start SSE listener:', error);
}
}
10 changes: 9 additions & 1 deletion src/tasks/exec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@ import { Context } from '../types.js'
import chalk from 'chalk'
import spawn from 'cross-spawn'
import { updateLogContext } from '../lib/logger.js'
import { startPolling } from '../lib/utils.js'
import { startPolling, startSSEListener } from '../lib/utils.js'

export default (ctx: Context): ListrTask<Context, ListrRendererFactory, ListrRendererFactory> => {
return {
Expand All @@ -16,6 +16,14 @@ export default (ctx: Context): ListrTask<Context, ListrRendererFactory, ListrRen
}
}

if((ctx.env.SHOW_RENDER_ERRORS||ctx.options.showRenderErrors||ctx.config.showRenderErrors) && ctx.build && ctx.build.id) {
if(ctx.env.LT_USERNAME&&ctx.env.LT_ACCESS_KEY) {
startSSEListener(ctx);
} else {
ctx.log.info('LT_USERNAME and LT_ACCESS_KEY are not set, set them to display render errors');
}
}

updateLogContext({task: 'exec'});

return new Promise((resolve, reject) => {
Expand Down
6 changes: 5 additions & 1 deletion src/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -43,6 +43,7 @@ export interface Context {
loadDomContent?: boolean;
approvalThreshold?: number;
rejectionThreshold?: number;
showRenderErrors?: boolean
};
uploadFilePath: string;
webStaticConfig: WebStaticConfig;
Expand Down Expand Up @@ -71,7 +72,8 @@ export interface Context {
fetchResultsFileName?: string,
baselineBranch?: string,
baselineBuild?: string,
githubURL?: string
githubURL?: string,
showRenderErrors?: boolean
}
cliVersion: string;
totalSnapshots: number;
Expand Down Expand Up @@ -120,6 +122,8 @@ export interface Env {
SMARTUI_API_SKIP_CERTIFICATES: boolean;
USE_REMOTE_DISCOVERY: boolean;
SMART_GIT: boolean;
SHOW_RENDER_ERRORS: boolean;
SMARTUI_SSE_URL: string;
}

export interface Snapshot {
Expand Down