Skip to content

Commit d7cb77b

Browse files
authored
Merge pull request #165 from LambdaTest/stage
Release 4.0.8 --Fetch-results
2 parents 067d677 + 14a94a8 commit d7cb77b

File tree

12 files changed

+141
-1
lines changed

12 files changed

+141
-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
@@ -17,6 +17,7 @@ command
1717
.description('Capture screenshots of static sites')
1818
.argument('<file>', 'Web static config file')
1919
.option('--parallel', 'Capture parallely on all browsers')
20+
.option('--fetch-results [filename]', 'Fetch results and optionally specify an output file, e.g., <filename>.json')
2021
.action(async function(file, _, command) {
2122
let ctx: Context = ctxInit(command.optsWithGlobals());
2223

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: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,8 @@ export default (options: Record<string, string>): Context => {
1818
let extensionFiles: string;
1919
let ignoreStripExtension: Array<string>;
2020
let ignoreFilePattern: Array<string>;
21+
let fetchResultObj: boolean;
22+
let fetchResultsFileObj: string;
2123
try {
2224
if (options.config) {
2325
config = JSON.parse(fs.readFileSync(options.config, 'utf-8'));
@@ -41,6 +43,18 @@ export default (options: Record<string, string>): Context => {
4143
extensionFiles = options.files || ['png', 'jpeg', 'jpg'];
4244
ignoreStripExtension = options.removeExtensions || false
4345
ignoreFilePattern = options.ignoreDir || []
46+
47+
if (options.fetchResults) {
48+
if (options.fetchResults !== true && !options.fetchResults.endsWith('.json')) {
49+
console.error("Error: The file extension for --fetch-results must be .json");
50+
process.exit(1);
51+
}
52+
fetchResultObj = true
53+
fetchResultsFileObj = options.fetchResults === true ? 'results.json' : options.fetchResults;
54+
} else {
55+
fetchResultObj = false
56+
fetchResultsFileObj = ''
57+
}
4458
} catch (error: any) {
4559
console.log(`[smartui] Error: ${error.message}`);
4660
process.exit();
@@ -103,6 +117,8 @@ export default (options: Record<string, string>): Context => {
103117
fileExtension: extensionFiles,
104118
stripExtension: ignoreStripExtension,
105119
ignorePattern: ignoreFilePattern,
120+
fetchResults: fetchResultObj,
121+
fetchResultsFileName: fetchResultsFileObj,
106122
},
107123
cliVersion: version,
108124
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)) {
@@ -199,4 +202,94 @@ export function getRenderViewportsForOptions(options: any): Array<Record<string,
199202
...mobileRenderViewports[constants.MOBILE_OS_IOS],
200203
...mobileRenderViewports[constants.MOBILE_OS_ANDROID]
201204
];
205+
}
206+
207+
// Global SIGINT handler
208+
process.on('SIGINT', () => {
209+
if (isPollingActive) {
210+
console.log('Fetching results interrupted. Exiting...');
211+
isPollingActive = false;
212+
} else {
213+
console.log('\nExiting gracefully...');
214+
}
215+
process.exit(0);
216+
});
217+
218+
// Background polling function
219+
export async function startPolling(ctx: Context, task: any): Promise<void> {
220+
ctx.log.info('Fetching results in progress....');
221+
isPollingActive = true;
222+
223+
const intervalId = setInterval(async () => {
224+
if (!isPollingActive) {
225+
clearInterval(intervalId);
226+
return;
227+
}
228+
229+
try {
230+
const resp = await ctx.client.getScreenshotData(ctx.build.id, ctx.build.baseline, ctx.log);
231+
232+
if (!resp.build) {
233+
ctx.log.info("Error: Build data is null.");
234+
clearInterval(intervalId);
235+
isPollingActive = false;
236+
}
237+
238+
fs.writeFileSync(ctx.options.fetchResultsFileName, JSON.stringify(resp, null, 2));
239+
ctx.log.debug(`Updated results in ${ctx.options.fetchResultsFileName}`);
240+
241+
if (resp.build.build_status_ind === constants.BUILD_COMPLETE || resp.build.build_status_ind === constants.BUILD_ERROR) {
242+
clearInterval(intervalId);
243+
ctx.log.info(`Fetching results completed. Final results written to ${ctx.options.fetchResultsFileName}`);
244+
isPollingActive = false;
245+
246+
247+
// Evaluating Summary
248+
let totalScreenshotsWithMismatches = 0;
249+
let totalVariantsWithMismatches = 0;
250+
const totalScreenshots = Object.keys(resp.screenshots || {}).length;
251+
let totalVariants = 0;
252+
253+
for (const [screenshot, variants] of Object.entries(resp.screenshots || {})) {
254+
let screenshotHasMismatch = false;
255+
let variantMismatchCount = 0;
256+
257+
totalVariants += variants.length; // Add to total variants count
258+
259+
for (const variant of variants) {
260+
if (variant.mismatch_percentage > 0) {
261+
screenshotHasMismatch = true;
262+
variantMismatchCount++;
263+
}
264+
}
265+
266+
if (screenshotHasMismatch) {
267+
totalScreenshotsWithMismatches++;
268+
totalVariantsWithMismatches += variantMismatchCount;
269+
}
270+
}
271+
272+
// Display summary
273+
ctx.log.info(
274+
chalk.green.bold(
275+
`\nSummary of Mismatches:\n` +
276+
`${chalk.yellow('Total Variants with Mismatches:')} ${chalk.white(totalVariantsWithMismatches)} out of ${chalk.white(totalVariants)}\n` +
277+
`${chalk.yellow('Total Screenshots with Mismatches:')} ${chalk.white(totalScreenshotsWithMismatches)} out of ${chalk.white(totalScreenshots)}\n` +
278+
`${chalk.yellow('Branch Name:')} ${chalk.white(resp.build.branch)}\n` +
279+
`${chalk.yellow('Project Name:')} ${chalk.white(resp.project.name)}\n` +
280+
`${chalk.yellow('Build ID:')} ${chalk.white(resp.build.build_id)}\n`
281+
)
282+
);
283+
}
284+
} catch (error: any) {
285+
if (error.message.includes('ENOTFOUND')) {
286+
ctx.log.error('Error: Network error occurred while fetching build results. Please check your connection and try again.');
287+
clearInterval(intervalId);
288+
} else {
289+
ctx.log.error(`Error fetching screenshot data: ${error.message}`);
290+
}
291+
clearInterval(intervalId);
292+
isPollingActive = false;
293+
}
294+
}, 5000);
202295
}

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 } 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
let { capturedScreenshots, output } = await captureScreenshots(ctx);

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)