Skip to content

Commit 6cfe6a2

Browse files
committed
Merge branch 'dev' of github.com:LambdaTest/smartui-cli into DOT-4051
2 parents 4e9f37d + 80769a1 commit 6cfe6a2

File tree

12 files changed

+140
-1
lines changed

12 files changed

+140
-1
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.0.7",
3+
"version": "4.0.8",
44
"description": "A command line interface (CLI) to run SmartUI tests on LambdaTest",
55
"files": [
66
"dist/**/*"

src/commander/capture.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,7 @@ command
1818
.argument('<file>', 'Web static config file')
1919
.option('-P, --parallel [number]', 'Specify the number of instances per browser', parseInt)
2020
.option('-F, --force', 'forcefully apply the specified parallel instances per browser')
21+
.option('--fetch-results [filename]', 'Fetch results and optionally specify an output file, e.g., <filename>.json')
2122
.action(async function(file, _, command) {
2223
let ctx: Context = ctxInit(command.optsWithGlobals());
2324

src/commander/exec.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,7 @@ command
1919
.description('Run test commands around SmartUI')
2020
.argument('<command...>', 'Command supplied for running tests')
2121
.option('-P, --port <number>', 'Port number for the server')
22+
.option('--fetch-results [filename]', 'Fetch results and optionally specify an output file, e.g., <filename>.json')
2223
.action(async function(execCommand, _, command) {
2324
let ctx: Context = ctxInit(command.optsWithGlobals());
2425

src/commander/upload.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,7 @@ command
2525
.option('-i, --ignoreDir <patterns>', 'Comma-separated list of directories to ignore', val => {
2626
return val.split(',').map(pattern => pattern.trim());
2727
})
28+
.option('--fetch-results [filename]', 'Fetch results and optionally specify an output file, e.g., <filename>.json')
2829
.action(async function(directory, _, command) {
2930
let ctx: Context = ctxInit(command.optsWithGlobals());
3031

src/lib/constants.ts

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -122,6 +122,10 @@ export default {
122122
MOBILE_ORIENTATION_PORTRAIT: 'portrait',
123123
MOBILE_ORIENTATION_LANDSCAPE: 'landscape',
124124

125+
// build status
126+
BUILD_COMPLETE: 'completed',
127+
BUILD_ERROR: 'error',
128+
125129
// CI
126130
GITHUB_API_HOST: 'https://api.github.com',
127131

src/lib/ctx.ts

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,8 @@ export default (options: Record<string, string>): Context => {
1919
let ignoreStripExtension: Array<string>;
2020
let ignoreFilePattern: Array<string>;
2121
let parallelObj: number;
22+
let fetchResultObj: boolean;
23+
let fetchResultsFileObj: string;
2224
try {
2325
if (options.config) {
2426
config = JSON.parse(fs.readFileSync(options.config, 'utf-8'));
@@ -44,6 +46,17 @@ export default (options: Record<string, string>): Context => {
4446
ignoreFilePattern = options.ignoreDir || []
4547

4648
parallelObj = options.parallel ? options.parallel === true? 1 : options.parallel: 1;
49+
if (options.fetchResults) {
50+
if (options.fetchResults !== true && !options.fetchResults.endsWith('.json')) {
51+
console.error("Error: The file extension for --fetch-results must be .json");
52+
process.exit(1);
53+
}
54+
fetchResultObj = true
55+
fetchResultsFileObj = options.fetchResults === true ? 'results.json' : options.fetchResults;
56+
} else {
57+
fetchResultObj = false
58+
fetchResultsFileObj = ''
59+
}
4760
} catch (error: any) {
4861
console.log(`[smartui] Error: ${error.message}`);
4962
process.exit();
@@ -107,6 +120,8 @@ export default (options: Record<string, string>): Context => {
107120
fileExtension: extensionFiles,
108121
stripExtension: ignoreStripExtension,
109122
ignorePattern: ignoreFilePattern,
123+
fetchResults: fetchResultObj,
124+
fetchResultsFileName: fetchResultsFileObj,
110125
},
111126
cliVersion: version,
112127
totalSnapshots: -1

src/lib/httpClient.ts

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -64,6 +64,14 @@ export default class httpClient {
6464
}, log)
6565
}
6666

67+
getScreenshotData(buildId: string, baseline: boolean, log: Logger) {
68+
return this.request({
69+
url: '/screenshot',
70+
method: 'GET',
71+
params: { buildId, baseline }
72+
}, log);
73+
}
74+
6775
finalizeBuild(buildId: string, totalSnapshots: number, log: Logger) {
6876
let params: Record<string, string | number> = {buildId};
6977
if (totalSnapshots > -1) params.totalSnapshots = totalSnapshots;

src/lib/utils.ts

Lines changed: 93 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,9 @@ import fs from 'fs'
22
import { Context } from '../types.js'
33
import { chromium, firefox, webkit, Browser } from '@playwright/test'
44
import constants from './constants.js';
5+
import chalk from 'chalk';
6+
7+
let isPollingActive = false;
58

69
export function delDir(dir: string): void {
710
if (fs.existsSync(dir)) {
@@ -200,4 +203,94 @@ export function getRenderViewportsForOptions(options: any): Array<Record<string,
200203
...mobileRenderViewports[constants.MOBILE_OS_IOS],
201204
...mobileRenderViewports[constants.MOBILE_OS_ANDROID]
202205
];
206+
}
207+
208+
// Global SIGINT handler
209+
process.on('SIGINT', () => {
210+
if (isPollingActive) {
211+
console.log('Fetching results interrupted. Exiting...');
212+
isPollingActive = false;
213+
} else {
214+
console.log('\nExiting gracefully...');
215+
}
216+
process.exit(0);
217+
});
218+
219+
// Background polling function
220+
export async function startPolling(ctx: Context, task: any): Promise<void> {
221+
ctx.log.info('Fetching results in progress....');
222+
isPollingActive = true;
223+
224+
const intervalId = setInterval(async () => {
225+
if (!isPollingActive) {
226+
clearInterval(intervalId);
227+
return;
228+
}
229+
230+
try {
231+
const resp = await ctx.client.getScreenshotData(ctx.build.id, ctx.build.baseline, ctx.log);
232+
233+
if (!resp.build) {
234+
ctx.log.info("Error: Build data is null.");
235+
clearInterval(intervalId);
236+
isPollingActive = false;
237+
}
238+
239+
fs.writeFileSync(ctx.options.fetchResultsFileName, JSON.stringify(resp, null, 2));
240+
ctx.log.debug(`Updated results in ${ctx.options.fetchResultsFileName}`);
241+
242+
if (resp.build.build_status_ind === constants.BUILD_COMPLETE || resp.build.build_status_ind === constants.BUILD_ERROR) {
243+
clearInterval(intervalId);
244+
ctx.log.info(`Fetching results completed. Final results written to ${ctx.options.fetchResultsFileName}`);
245+
isPollingActive = false;
246+
247+
248+
// Evaluating Summary
249+
let totalScreenshotsWithMismatches = 0;
250+
let totalVariantsWithMismatches = 0;
251+
const totalScreenshots = Object.keys(resp.screenshots || {}).length;
252+
let totalVariants = 0;
253+
254+
for (const [screenshot, variants] of Object.entries(resp.screenshots || {})) {
255+
let screenshotHasMismatch = false;
256+
let variantMismatchCount = 0;
257+
258+
totalVariants += variants.length; // Add to total variants count
259+
260+
for (const variant of variants) {
261+
if (variant.mismatch_percentage > 0) {
262+
screenshotHasMismatch = true;
263+
variantMismatchCount++;
264+
}
265+
}
266+
267+
if (screenshotHasMismatch) {
268+
totalScreenshotsWithMismatches++;
269+
totalVariantsWithMismatches += variantMismatchCount;
270+
}
271+
}
272+
273+
// Display summary
274+
ctx.log.info(
275+
chalk.green.bold(
276+
`\nSummary of Mismatches:\n` +
277+
`${chalk.yellow('Total Variants with Mismatches:')} ${chalk.white(totalVariantsWithMismatches)} out of ${chalk.white(totalVariants)}\n` +
278+
`${chalk.yellow('Total Screenshots with Mismatches:')} ${chalk.white(totalScreenshotsWithMismatches)} out of ${chalk.white(totalScreenshots)}\n` +
279+
`${chalk.yellow('Branch Name:')} ${chalk.white(resp.build.branch)}\n` +
280+
`${chalk.yellow('Project Name:')} ${chalk.white(resp.project.name)}\n` +
281+
`${chalk.yellow('Build ID:')} ${chalk.white(resp.build.build_id)}\n`
282+
)
283+
);
284+
}
285+
} catch (error: any) {
286+
if (error.message.includes('ENOTFOUND')) {
287+
ctx.log.error('Error: Network error occurred while fetching build results. Please check your connection and try again.');
288+
clearInterval(intervalId);
289+
} else {
290+
ctx.log.error(`Error fetching screenshot data: ${error.message}`);
291+
}
292+
clearInterval(intervalId);
293+
isPollingActive = false;
294+
}
295+
}, 5000);
203296
}

src/tasks/captureScreenshots.ts

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,13 +3,17 @@ import { Context } from '../types.js'
33
import { captureScreenshots, captureScreenshotsConcurrent } from '../lib/screenshot.js'
44
import chalk from 'chalk';
55
import { updateLogContext } from '../lib/logger.js'
6+
import { startPolling } from '../lib/utils.js';
67

78
export default (ctx: Context): ListrTask<Context, ListrRendererFactory, ListrRendererFactory> => {
89
return {
910
title: 'Capturing screenshots',
1011
task: async (ctx, task): Promise<void> => {
1112
try {
1213
ctx.task = task;
14+
if (ctx.options.fetchResults) {
15+
startPolling(ctx, task);
16+
}
1317
updateLogContext({task: 'capture'});
1418

1519
if (ctx.options.parallel) {

src/tasks/exec.ts

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,11 +3,17 @@ 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'
67

78
export default (ctx: Context): ListrTask<Context, ListrRendererFactory, ListrRendererFactory> => {
89
return {
910
title: `Executing '${ctx.args.execCommand?.join(' ')}'`,
1011
task: async (ctx, task): Promise<void> => {
12+
13+
if (ctx.options.fetchResults) {
14+
startPolling(ctx, task);
15+
}
16+
1117
updateLogContext({task: 'exec'});
1218

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

0 commit comments

Comments
 (0)