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/capture.ts
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@ command
.description('Capture screenshots of static sites')
.argument('<file>', 'Web static config file')
.option('--parallel', 'Capture parallely on all browsers')
.option('--fetch-results [filename]', 'Fetch results and optionally specify an output file, e.g., <filename>.json')
.action(async function(file, _, command) {
let ctx: Context = ctxInit(command.optsWithGlobals());

Expand Down
1 change: 1 addition & 0 deletions src/commander/exec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@ command
.description('Run test commands around SmartUI')
.argument('<command...>', 'Command supplied for running tests')
.option('-P, --port <number>', 'Port number for the server')
.option('--fetch-results [filename]', 'Fetch results and optionally specify an output file, e.g., <filename>.json')
.action(async function(execCommand, _, command) {
let ctx: Context = ctxInit(command.optsWithGlobals());

Expand Down
1 change: 1 addition & 0 deletions src/commander/upload.ts
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,7 @@ command
.option('-i, --ignoreDir <patterns>', 'Comma-separated list of directories to ignore', val => {
return val.split(',').map(pattern => pattern.trim());
})
.option('--fetch-results [filename]', 'Fetch results and optionally specify an output file, e.g., <filename>.json')
.action(async function(directory, _, command) {
let ctx: Context = ctxInit(command.optsWithGlobals());

Expand Down
4 changes: 4 additions & 0 deletions src/lib/constants.ts
Original file line number Diff line number Diff line change
Expand Up @@ -122,6 +122,10 @@ export default {
MOBILE_ORIENTATION_PORTRAIT: 'portrait',
MOBILE_ORIENTATION_LANDSCAPE: 'landscape',

// build status
BUILD_COMPLETE: 'completed',
BUILD_ERROR: 'error',

// CI
GITHUB_API_HOST: 'https://api.github.com',

Expand Down
16 changes: 16 additions & 0 deletions src/lib/ctx.ts
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,8 @@ export default (options: Record<string, string>): Context => {
let extensionFiles: string;
let ignoreStripExtension: Array<string>;
let ignoreFilePattern: Array<string>;
let fetchResultObj: boolean;
let fetchResultsFileObj: string;
try {
if (options.config) {
config = JSON.parse(fs.readFileSync(options.config, 'utf-8'));
Expand All @@ -41,6 +43,18 @@ export default (options: Record<string, string>): Context => {
extensionFiles = options.files || ['png', 'jpeg', 'jpg'];
ignoreStripExtension = options.removeExtensions || false
ignoreFilePattern = options.ignoreDir || []

if (options.fetchResults) {
if (options.fetchResults !== true && !options.fetchResults.endsWith('.json')) {
console.error("Error: The file extension for --fetch-results must be .json");
process.exit(1);
}
fetchResultObj = true
fetchResultsFileObj = options.fetchResults === true ? 'results.json' : options.fetchResults;
} else {
fetchResultObj = false
fetchResultsFileObj = ''
}
} catch (error: any) {
console.log(`[smartui] Error: ${error.message}`);
process.exit();
Expand Down Expand Up @@ -103,6 +117,8 @@ export default (options: Record<string, string>): Context => {
fileExtension: extensionFiles,
stripExtension: ignoreStripExtension,
ignorePattern: ignoreFilePattern,
fetchResults: fetchResultObj,
fetchResultsFileName: fetchResultsFileObj,
},
cliVersion: version,
totalSnapshots: -1
Expand Down
8 changes: 8 additions & 0 deletions src/lib/httpClient.ts
Original file line number Diff line number Diff line change
Expand Up @@ -64,6 +64,14 @@ export default class httpClient {
}, log)
}

getScreenshotData(buildId: string, baseline: boolean, log: Logger) {
return this.request({
url: '/screenshot',
method: 'GET',
params: { buildId, baseline }
}, log);
}

finalizeBuild(buildId: string, totalSnapshots: number, log: Logger) {
let params: Record<string, string | number> = {buildId};
if (totalSnapshots > -1) params.totalSnapshots = totalSnapshots;
Expand Down
93 changes: 93 additions & 0 deletions src/lib/utils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,9 @@ import fs from 'fs'
import { Context } from '../types.js'
import { chromium, firefox, webkit, Browser } from '@playwright/test'
import constants from './constants.js';
import chalk from 'chalk';

let isPollingActive = false;

export function delDir(dir: string): void {
if (fs.existsSync(dir)) {
Expand Down Expand Up @@ -199,4 +202,94 @@ export function getRenderViewportsForOptions(options: any): Array<Record<string,
...mobileRenderViewports[constants.MOBILE_OS_IOS],
...mobileRenderViewports[constants.MOBILE_OS_ANDROID]
];
}

// Global SIGINT handler
process.on('SIGINT', () => {
if (isPollingActive) {
console.log('Fetching results interrupted. Exiting...');
isPollingActive = false;
} else {
console.log('\nExiting gracefully...');
}
process.exit(0);
});

// Background polling function
export async function startPolling(ctx: Context, task: any): Promise<void> {
ctx.log.info('Fetching results in progress....');
isPollingActive = true;

const intervalId = setInterval(async () => {
if (!isPollingActive) {
clearInterval(intervalId);
return;
}

try {
const resp = await ctx.client.getScreenshotData(ctx.build.id, ctx.build.baseline, ctx.log);

if (!resp.build) {
ctx.log.info("Error: Build data is null.");
clearInterval(intervalId);
isPollingActive = false;
}

fs.writeFileSync(ctx.options.fetchResultsFileName, JSON.stringify(resp, null, 2));
ctx.log.debug(`Updated results in ${ctx.options.fetchResultsFileName}`);

if (resp.build.build_status_ind === constants.BUILD_COMPLETE || resp.build.build_status_ind === constants.BUILD_ERROR) {
clearInterval(intervalId);
ctx.log.info(`Fetching results completed. Final results written to ${ctx.options.fetchResultsFileName}`);
isPollingActive = false;


// Evaluating Summary
let totalScreenshotsWithMismatches = 0;
let totalVariantsWithMismatches = 0;
const totalScreenshots = Object.keys(resp.screenshots || {}).length;
let totalVariants = 0;

for (const [screenshot, variants] of Object.entries(resp.screenshots || {})) {
let screenshotHasMismatch = false;
let variantMismatchCount = 0;

totalVariants += variants.length; // Add to total variants count

for (const variant of variants) {
if (variant.mismatch_percentage > 0) {
screenshotHasMismatch = true;
variantMismatchCount++;
}
}

if (screenshotHasMismatch) {
totalScreenshotsWithMismatches++;
totalVariantsWithMismatches += variantMismatchCount;
}
}

// Display summary
ctx.log.info(
chalk.green.bold(
`\nSummary of Mismatches:\n` +
`${chalk.yellow('Total Variants with Mismatches:')} ${chalk.white(totalVariantsWithMismatches)} out of ${chalk.white(totalVariants)}\n` +
`${chalk.yellow('Total Screenshots with Mismatches:')} ${chalk.white(totalScreenshotsWithMismatches)} out of ${chalk.white(totalScreenshots)}\n` +
`${chalk.yellow('Branch Name:')} ${chalk.white(resp.build.branch)}\n` +
`${chalk.yellow('Project Name:')} ${chalk.white(resp.project.name)}\n` +
`${chalk.yellow('Build ID:')} ${chalk.white(resp.build.build_id)}\n`
)
);
}
} catch (error: any) {
if (error.message.includes('ENOTFOUND')) {
ctx.log.error('Error: Network error occurred while fetching build results. Please check your connection and try again.');
clearInterval(intervalId);
} else {
ctx.log.error(`Error fetching screenshot data: ${error.message}`);
}
clearInterval(intervalId);
isPollingActive = false;
}
}, 5000);
}
4 changes: 4 additions & 0 deletions src/tasks/captureScreenshots.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,13 +3,17 @@ import { Context } from '../types.js'
import { captureScreenshots } from '../lib/screenshot.js'
import chalk from 'chalk';
import { updateLogContext } from '../lib/logger.js'
import { startPolling } from '../lib/utils.js';

export default (ctx: Context): ListrTask<Context, ListrRendererFactory, ListrRendererFactory> => {
return {
title: 'Capturing screenshots',
task: async (ctx, task): Promise<void> => {
try {
ctx.task = task;
if (ctx.options.fetchResults) {
startPolling(ctx, task);
}
updateLogContext({task: 'capture'});

let { capturedScreenshots, output } = await captureScreenshots(ctx);
Expand Down
6 changes: 6 additions & 0 deletions src/tasks/exec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,11 +3,17 @@ 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'

export default (ctx: Context): ListrTask<Context, ListrRendererFactory, ListrRendererFactory> => {
return {
title: `Executing '${ctx.args.execCommand?.join(' ')}'`,
task: async (ctx, task): Promise<void> => {

if (ctx.options.fetchResults) {
startPolling(ctx, task);
}

updateLogContext({task: 'exec'});

return new Promise((resolve, reject) => {
Expand Down
4 changes: 4 additions & 0 deletions src/tasks/uploadScreenshots.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,13 +3,17 @@ import { Context } from '../types.js';
import { uploadScreenshots } from '../lib/screenshot.js';
import chalk from 'chalk';
import { updateLogContext } from '../lib/logger.js';
import { startPolling } from '../lib/utils.js';

export default (ctx: Context): ListrTask<Context, ListrRendererFactory, ListrRendererFactory> => {
return {
title: 'Uploading screenshots',
task: async (ctx, task): Promise<void> => {
try {
ctx.task = task;
if (ctx.options.fetchResults) {
startPolling(ctx, task);
}
updateLogContext({ task: 'upload' });

await uploadScreenshots(ctx);
Expand Down
2 changes: 2 additions & 0 deletions src/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -43,6 +43,8 @@ export interface Context {
fileExtension?: Array<string>,
stripExtension?: boolean,
ignorePattern?: Array<string>,
fetchResults?: boolean,
fetchResultsFileName?: string
}
cliVersion: string;
totalSnapshots: number;
Expand Down