From 6b3a73f3614d6ba179c4b9dd231fe8fedf444d38 Mon Sep 17 00:00:00 2001 From: Shrinish Vhanbatte Date: Sat, 18 Apr 2026 00:37:39 +0530 Subject: [PATCH 1/3] fix PDF fetch-results --- package.json | 2 +- src/lib/utils.ts | 66 +++++++++++++++++++++++++++++++----------------- 2 files changed, 44 insertions(+), 24 deletions(-) diff --git a/package.json b/package.json index deaee65..a63be3e 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "@lambdatest/smartui-cli", - "version": "4.1.63", + "version": "4.1.64-up-1", "description": "A command line interface (CLI) to run SmartUI tests on LambdaTest", "files": [ "dist/**/*" diff --git a/src/lib/utils.ts b/src/lib/utils.ts index 75ca63d..51962d7 100644 --- a/src/lib/utils.ts +++ b/src/lib/utils.ts @@ -628,60 +628,74 @@ export function startPdfPolling(ctx: Context) { return } - if (!ctx.env.LT_USERNAME || !ctx.env.LT_ACCESS_KEY) { - console.log(chalk.red('Error: LT_USERNAME and LT_ACCESS_KEY environment variables are required for fetching results')); - return; - } - let attempts = 0; - const maxAttempts = 60; // 5 minutes (10 seconds * 30) + const maxAttempts = 60; // 10 minutes (10 seconds * 60) console.log(chalk.yellow('Waiting for results...')); + const projectToken = ctx.env.PROJECT_TOKEN || ''; + const interval = setInterval(async () => { attempts++; try { - const response = await ctx.client.fetchPdfResults(ctx); + const response = await ctx.client.getScreenshotData(ctx.build.id, false, ctx.log, projectToken, ''); + + if (!response || !response.build) { + if (attempts >= maxAttempts) { + clearInterval(interval); + console.log(chalk.red('\nTimeout: Could not fetch PDF results after 10 minutes')); + } + return; + } - if (response.screenshots && response.build?.build_status !== constants.BUILD_RUNNING) { + if (response.screenshots && (response.build.build_status_ind === constants.BUILD_COMPLETE || response.build.build_status_ind === constants.BUILD_ERROR)) { clearInterval(interval); - const pdfGroups = groupScreenshotsByPdf(response.screenshots); + // Flatten screenshots object to array for PDF grouping + const screenshotsArray: any[] = []; + for (const [, variants] of Object.entries(response.screenshots || {})) { + for (const variant of (variants as any[])) { + screenshotsArray.push(variant); + } + } + + const pdfGroups = groupScreenshotsByPdf(screenshotsArray); const pdfsWithMismatches = countPdfsWithMismatches(pdfGroups); - const pagesWithMismatches = countPagesWithMismatches(response.screenshots); + const pagesWithMismatches = countPagesWithMismatches(screenshotsArray); console.log(chalk.green('\nāœ“ PDF Test Results:')); - console.log(chalk.green(`Build Name: ${response.build.name}`)); + console.log(chalk.green(`Build Name: ${response.build.build_name}`)); console.log(chalk.green(`Project Name: ${response.project.name}`)); console.log(chalk.green(`Total PDFs: ${Object.keys(pdfGroups).length}`)); - console.log(chalk.green(`Total Pages: ${response.screenshots.length}`)); + console.log(chalk.green(`Total Pages: ${screenshotsArray.length}`)); if (pdfsWithMismatches > 0 || pagesWithMismatches > 0) { - console.log(chalk.yellow(`${pdfsWithMismatches} PDFs and ${pagesWithMismatches} Pages in build ${response.build.name} have changes present.`)); + console.log(chalk.yellow(`${pdfsWithMismatches} PDFs and ${pagesWithMismatches} Pages in build ${response.build.build_name} have changes present.`)); } else { console.log(chalk.green('All PDFs match the baseline.')); } Object.entries(pdfGroups).forEach(([pdfName, pages]) => { - const hasMismatch = pages.some(page => page.mismatch_percentage > 0); + const hasMismatch = pages.some(page => isPageMismatch(page)); const statusColor = hasMismatch ? chalk.yellow : chalk.green; console.log(statusColor(`\nšŸ“„ ${pdfName} (${pages.length} pages)`)); pages.forEach(page => { - const pageStatusColor = page.mismatch_percentage > 0 ? chalk.yellow : chalk.green; - console.log(pageStatusColor(` - Page ${getPageNumber(page.screenshot_name)}: ${page.status} (Mismatch: ${page.mismatch_percentage}%)`)); + const pageStatusColor = isPageMismatch(page) ? chalk.yellow : chalk.green; + const mismatchInfo = page.mismatch_percentage !== undefined ? ` (Mismatch: ${page.mismatch_percentage}%)` : ''; + console.log(pageStatusColor(` - Page ${getPageNumber(page.screenshot_name)}: ${page.status}${mismatchInfo}`)); }); }); const formattedResults = { status: 'success', data: { - buildId: response.build.id, - buildName: response.build.name, + buildId: response.build.build_id, + buildName: response.build.build_name, projectName: response.project.name, - buildStatus: response.build.build_satus, + buildStatus: response.build.build_status, pdfs: formatPdfsForOutput(pdfGroups) } }; @@ -699,7 +713,7 @@ export function startPdfPolling(ctx: Context) { if (attempts >= maxAttempts) { clearInterval(interval); - console.log(chalk.red('\nTimeout: Could not fetch PDF results after 5 minutes')); + console.log(chalk.red('\nTimeout: Could not fetch PDF results after 10 minutes')); return; } @@ -708,7 +722,7 @@ export function startPdfPolling(ctx: Context) { if (attempts >= maxAttempts) { clearInterval(interval); - console.log(chalk.red('\nTimeout: Could not fetch PDF results after 5 minutes')); + console.log(chalk.red('\nTimeout: Could not fetch PDF results after 10 minutes')); if (error.response && error.response.data) { console.log(chalk.red(`Error details: ${JSON.stringify(error.response.data)}`)); } else { @@ -738,11 +752,17 @@ function groupScreenshotsByPdf(screenshots: any[]): Record { return pdfGroups; } +const NON_MISMATCH_STATUSES = ['Approved', 'moved', 'new-screenshot']; + +function isPageMismatch(page: any): boolean { + return !NON_MISMATCH_STATUSES.includes(page.status); +} + function countPdfsWithMismatches(pdfGroups: Record): number { let count = 0; Object.values(pdfGroups).forEach(pages => { - if (pages.some(page => page.mismatch_percentage > 0)) { + if (pages.some(page => isPageMismatch(page))) { count++; } }); @@ -751,7 +771,7 @@ function countPdfsWithMismatches(pdfGroups: Record): number { } function countPagesWithMismatches(screenshots: any[]): number { - return screenshots.filter(screenshot => screenshot.mismatch_percentage > 0).length; + return screenshots.filter(screenshot => isPageMismatch(screenshot)).length; } function formatPdfsForOutput(pdfGroups: Record): any[] { From 4d7d970305ad4736ee9f0d95604669759b44bbdb Mon Sep 17 00:00:00 2001 From: Shrinish Vhanbatte Date: Tue, 21 Apr 2026 13:52:33 +0530 Subject: [PATCH 2/3] log --- package.json | 2 +- src/lib/httpClient.ts | 5 ++++- 2 files changed, 5 insertions(+), 2 deletions(-) diff --git a/package.json b/package.json index a63be3e..fa6b3ca 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "@lambdatest/smartui-cli", - "version": "4.1.64-up-1", + "version": "4.1.69", "description": "A command line interface (CLI) to run SmartUI tests on LambdaTest", "files": [ "dist/**/*" diff --git a/src/lib/httpClient.ts b/src/lib/httpClient.ts index a90e378..abab5bc 100644 --- a/src/lib/httpClient.ts +++ b/src/lib/httpClient.ts @@ -852,9 +852,12 @@ export default class httpClient { const auth = Buffer.from(`${this.username}:${this.accessKey}`).toString('base64'); + const url = ctx.env.SMARTUI_UPLOAD_URL + '/smartui/2.0/build/screenshots'; + ctx.log.debug(`Fetching PDF results from URL: ${url} with params: ${JSON.stringify(params)}`); + try { const response = await axios.request({ - url: ctx.env.SMARTUI_UPLOAD_URL + '/smartui/2.0/build/screenshots', + url: url, method: 'GET', params: params, headers: { From ed93c4128ee6313c89b5c484857bc304e8bfd49e Mon Sep 17 00:00:00 2001 From: Shrinish Vhanbatte Date: Fri, 24 Apr 2026 11:57:49 +0530 Subject: [PATCH 3/3] fix fetch results for omni --- package.json | 2 +- src/commander/uploadPdf.ts | 2 +- src/lib/constants.ts | 1 + src/lib/httpClient.ts | 3 +- src/lib/utils.ts | 411 +++++++++++++++++++++++++++++-------- 5 files changed, 333 insertions(+), 86 deletions(-) diff --git a/package.json b/package.json index fa6b3ca..e6490bc 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "@lambdatest/smartui-cli", - "version": "4.1.69", + "version": "4.1.69-shri", "description": "A command line interface (CLI) to run SmartUI tests on LambdaTest", "files": [ "dist/**/*" diff --git a/src/commander/uploadPdf.ts b/src/commander/uploadPdf.ts index c34e280..4056549 100644 --- a/src/commander/uploadPdf.ts +++ b/src/commander/uploadPdf.ts @@ -56,7 +56,7 @@ command try { await tasks.run(ctx); - if (ctx.options.fetchResults) { + if (ctx.options.fetchResults && ctx.build && ctx.build.id) { startPdfPolling(ctx); } } catch (error) { diff --git a/src/lib/constants.ts b/src/lib/constants.ts index 3eadfd0..dada35a 100644 --- a/src/lib/constants.ts +++ b/src/lib/constants.ts @@ -129,6 +129,7 @@ export default { BUILD_RUNNING: 'running', BUILD_COMPLETE: 'completed', BUILD_ERROR: 'error', + BUILD_TYPE_OMNI: 'omni', // CI GITHUB_API_HOST: 'https://api.github.com', diff --git a/src/lib/httpClient.ts b/src/lib/httpClient.ts index abab5bc..cafef04 100644 --- a/src/lib/httpClient.ts +++ b/src/lib/httpClient.ts @@ -118,6 +118,7 @@ export default class httpClient { if (config && config.data && !config.data.skipLogging && config.data.snapshotUuid && config.method!=='PUT') { log.debug(config.data); } + return this.axiosInstance.request(config) .then(resp => { if (resp) { @@ -258,7 +259,7 @@ export default class httpClient { getScreenshotData(buildId: string, baseline: boolean, log: Logger, projectToken: string, buildName: string, sessionId?: string, type?: string) { log.debug(`Fetching screenshot data for buildId: ${buildId} having buildName: ${buildName} with baseline: ${baseline}`); - const params: Record = { buildId, baseline, buildName }; + const params: Record = { buildId, baseline: false, buildName }; if (sessionId) { params.sessionId = sessionId; } diff --git a/src/lib/utils.ts b/src/lib/utils.ts index 51962d7..762c5e3 100644 --- a/src/lib/utils.ts +++ b/src/lib/utils.ts @@ -367,43 +367,84 @@ export async function startPolling(ctx: Context, build_id: string, baseline: boo clearInterval(intervalId); ctx.log.info(`Fetching results completed. Final results written to ${fileName}`); + if (resp.build.build_type === constants.BUILD_TYPE_OMNI) { + // Omni build: separate PDFs from normal screenshots and display both + const { normalScreenshots, pdfScreenshots } = separateScreenshots(resp.screenshots || {}); - // Evaluating Summary - let totalScreenshotsWithMismatches = 0; - let totalVariantsWithMismatches = 0; - const totalScreenshots = Object.keys(resp.screenshots || {}).length; - let totalVariants = 0; + printOmniHeader(resp.build, resp.project); - for (const [screenshot, variants] of Object.entries(resp.screenshots || {})) { - let screenshotHasMismatch = false; - let variantMismatchCount = 0; + let pdfGroups: Record = {}; + if (pdfScreenshots.length > 0) { + const pdfResult = printPdfSection(pdfScreenshots); + pdfGroups = pdfResult.pdfGroups; + } - totalVariants += variants.length; // Add to total variants count + if (Object.keys(normalScreenshots).length > 0) { + printScreenshotSection(normalScreenshots); + } - for (const variant of variants) { - if (variant.mismatch_percentage > 0) { - screenshotHasMismatch = true; - variantMismatchCount++; + const buildResult = resp.build.build_status?.toLowerCase() === 'approved' ? 'Passed' : 'Failed'; + const resultColor = buildResult === 'Passed' ? chalk.green : chalk.red; + + // Write formatted omni results + const formattedResults = { + status: 'success', + data: { + buildId: resp.build.build_id, + buildName: resp.build.build_name, + projectName: resp.project.name, + buildStatus: resp.build.build_status, + buildResult, + branchName: resp.build.branch, + pdfs: formatPdfsForOutput(pdfGroups), + screenshots: formatScreenshotsForOutput(normalScreenshots) } + }; + + if (ctx.options.fetchResults && ctx.options.fetchResultsFileName) { + const omniFileName = ctx.options.fetchResultsFileName !== '' ? ctx.options.fetchResultsFileName : 'results.json'; + fs.writeFileSync(omniFileName, JSON.stringify(formattedResults, null, 2)); + console.log(chalk.green(`\nResults saved to ${omniFileName}`)); } + console.log(resultColor.bold(`\nResult of Build ${resp.build.build_name} : ${buildResult}`)); + } else { + // Non-omni build: existing behavior + 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 as any[]).length; + + for (const variant of (variants as any[])) { + if (variant.mismatch_percentage > 0) { + screenshotHasMismatch = true; + variantMismatchCount++; + } + } - if (screenshotHasMismatch) { - totalScreenshotsWithMismatches++; - totalVariantsWithMismatches += variantMismatchCount; + if (screenshotHasMismatch) { + totalScreenshotsWithMismatches++; + totalVariantsWithMismatches += variantMismatchCount; + } } - } - // Display summary - ctx.log.info( - chalk.green.bold( - `\nSummary of Mismatches for buildId: ${build_id}\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` - ) - ); + // Display summary + ctx.log.info( + chalk.green.bold( + `\nSummary of Mismatches for buildId: ${build_id}\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')) { @@ -652,60 +693,97 @@ export function startPdfPolling(ctx: Context) { if (response.screenshots && (response.build.build_status_ind === constants.BUILD_COMPLETE || response.build.build_status_ind === constants.BUILD_ERROR)) { clearInterval(interval); - // Flatten screenshots object to array for PDF grouping - const screenshotsArray: any[] = []; - for (const [, variants] of Object.entries(response.screenshots || {})) { - for (const variant of (variants as any[])) { - screenshotsArray.push(variant); + if (response.build.build_type === constants.BUILD_TYPE_OMNI) { + const { normalScreenshots, pdfScreenshots } = separateScreenshots(response.screenshots || {}); + printOmniHeader(response.build, response.project); + + let pdfGroups: Record = {}; + if (pdfScreenshots.length > 0) { + const pdfResult = printPdfSection(pdfScreenshots); + pdfGroups = pdfResult.pdfGroups; } - } - const pdfGroups = groupScreenshotsByPdf(screenshotsArray); - const pdfsWithMismatches = countPdfsWithMismatches(pdfGroups); - const pagesWithMismatches = countPagesWithMismatches(screenshotsArray); + if (Object.keys(normalScreenshots).length > 0) { + printScreenshotSection(normalScreenshots); + } - console.log(chalk.green('\nāœ“ PDF Test Results:')); - console.log(chalk.green(`Build Name: ${response.build.build_name}`)); - console.log(chalk.green(`Project Name: ${response.project.name}`)); - console.log(chalk.green(`Total PDFs: ${Object.keys(pdfGroups).length}`)); - console.log(chalk.green(`Total Pages: ${screenshotsArray.length}`)); + const buildResult = response.build.build_status?.toLowerCase() === 'approved' ? 'Passed' : 'Failed'; + const resultColor = buildResult === 'Passed' ? chalk.green : chalk.red; + + const formattedResults = { + status: 'success', + data: { + buildId: response.build.build_id, + buildName: response.build.build_name, + projectName: response.project.name, + buildStatus: response.build.build_status, + buildResult, + branchName: response.build.branch, + pdfs: formatPdfsForOutput(pdfGroups), + screenshots: formatScreenshotsForOutput(normalScreenshots) + } + }; - if (pdfsWithMismatches > 0 || pagesWithMismatches > 0) { - console.log(chalk.yellow(`${pdfsWithMismatches} PDFs and ${pagesWithMismatches} Pages in build ${response.build.build_name} have changes present.`)); + if (ctx.options.fetchResults && ctx.options.fetchResultsFileName) { + const filename = ctx.options.fetchResultsFileName !== '' ? ctx.options.fetchResultsFileName : 'results.json'; + fs.writeFileSync(filename, JSON.stringify(formattedResults, null, 2)); + console.log(chalk.green(`\nResults saved to ${filename}`)); + } + console.log(resultColor.bold(`\nResult of Build ${response.build.build_name} : ${buildResult}`)); } else { - console.log(chalk.green('All PDFs match the baseline.')); - } - - Object.entries(pdfGroups).forEach(([pdfName, pages]) => { - const hasMismatch = pages.some(page => isPageMismatch(page)); - const statusColor = hasMismatch ? chalk.yellow : chalk.green; + // Non-omni build: existing PDF-only behavior + const screenshotsArray: any[] = []; + for (const [, variants] of Object.entries(response.screenshots || {})) { + for (const variant of (variants as any[])) { + screenshotsArray.push(variant); + } + } - console.log(statusColor(`\nšŸ“„ ${pdfName} (${pages.length} pages)`)); + const pdfGroups = groupScreenshotsByPdf(screenshotsArray); + const pdfsWithMismatches = countPdfsWithMismatches(pdfGroups); + const pagesWithMismatches = countPagesWithMismatches(screenshotsArray); - pages.forEach(page => { - const pageStatusColor = isPageMismatch(page) ? chalk.yellow : chalk.green; - const mismatchInfo = page.mismatch_percentage !== undefined ? ` (Mismatch: ${page.mismatch_percentage}%)` : ''; - console.log(pageStatusColor(` - Page ${getPageNumber(page.screenshot_name)}: ${page.status}${mismatchInfo}`)); - }); - }); + console.log(chalk.green('\nāœ“ PDF Test Results:')); + console.log(chalk.green(`Build Name: ${response.build.build_name}`)); + console.log(chalk.green(`Project Name: ${response.project.name}`)); + console.log(chalk.green(`Total PDFs: ${Object.keys(pdfGroups).length}`)); + console.log(chalk.green(`Total Pages: ${screenshotsArray.length}`)); - const formattedResults = { - status: 'success', - data: { - buildId: response.build.build_id, - buildName: response.build.build_name, - projectName: response.project.name, - buildStatus: response.build.build_status, - pdfs: formatPdfsForOutput(pdfGroups) + if (pdfsWithMismatches > 0 || pagesWithMismatches > 0) { + console.log(chalk.yellow(`${pdfsWithMismatches} PDFs and ${pagesWithMismatches} Pages in build ${response.build.build_name} have changes present.`)); + } else { + console.log(chalk.green('All PDFs match the baseline.')); } - }; - // Save results to file if filename provided - if (ctx.options.fetchResults && ctx.options.fetchResultsFileName) { - const filename = ctx.options.fetchResultsFileName !== '' ? ctx.options.fetchResultsFileName : 'pdf-results.json'; + Object.entries(pdfGroups).forEach(([pdfName, pages]) => { + const hasMismatch = pages.some(page => isPageMismatch(page)); + const statusColor = hasMismatch ? chalk.yellow : chalk.green; - fs.writeFileSync(filename, JSON.stringify(formattedResults, null, 2)); - console.log(chalk.green(`\nResults saved to ${filename}`)); + console.log(statusColor(`\nšŸ“„ ${pdfName} (${pages.length} pages)`)); + + pages.forEach(page => { + const pageStatusColor = isPageMismatch(page) ? chalk.yellow : chalk.green; + const mismatchInfo = page.mismatch_percentage !== undefined ? ` (Mismatch: ${page.mismatch_percentage}%)` : ''; + console.log(pageStatusColor(` - Page ${getPageNumber(page.screenshot_name, page.browser_name)}: ${page.status}${mismatchInfo}`)); + }); + }); + + const formattedResults = { + status: 'success', + data: { + buildId: response.build.build_id, + buildName: response.build.build_name, + projectName: response.project.name, + buildStatus: response.build.build_status, + pdfs: formatPdfsForOutput(pdfGroups) + } + }; + + if (ctx.options.fetchResults && ctx.options.fetchResultsFileName) { + const filename = ctx.options.fetchResultsFileName !== '' ? ctx.options.fetchResultsFileName : 'pdf-results.json'; + fs.writeFileSync(filename, JSON.stringify(formattedResults, null, 2)); + console.log(chalk.green(`\nResults saved to ${filename}`)); + } } return; @@ -735,12 +813,104 @@ export function startPdfPolling(ctx: Context) { }, 10000); } +// --- PDF/Screenshot detection and extraction helpers --- + +function isPdfScreenshot(variant: any): boolean { + return variant.browser_name && variant.browser_name.endsWith('.pdf'); +} + +function extractPdfNameAndPage(screenshotName: string, browserName: string): { pdfName: string, pageNumber: string } { + // Find the last occurrence of ".pdf#" to handle hashes in user-provided names + // e.g., "my#report.pdf#3" → pdfName: "my#report.pdf", pageNumber: "3" + const marker = '.pdf#'; + const lastIdx = screenshotName.lastIndexOf(marker); + if (lastIdx !== -1) { + return { + pdfName: screenshotName.substring(0, lastIdx + 4), // include ".pdf" + pageNumber: screenshotName.substring(lastIdx + 5) // after "#" + }; + } + // Fallback: use browser_name as pdf name + return { pdfName: browserName, pageNumber: '1' }; +} + +function separateScreenshots(screenshots: Record): { normalScreenshots: Record, pdfScreenshots: any[] } { + const normalScreenshots: Record = {}; + const pdfScreenshots: any[] = []; + + for (const [name, variants] of Object.entries(screenshots || {})) { + if (variants.length > 0 && isPdfScreenshot(variants[0])) { + for (const variant of variants) { + pdfScreenshots.push(variant); + } + } else { + normalScreenshots[name] = variants; + } + } + + return { normalScreenshots, pdfScreenshots }; +} + +// --- Status helpers --- + +const NON_MISMATCH_STATUSES = ['Approved', 'moved', 'new-screenshot']; + +function isPageMismatch(page: any): boolean { + return !NON_MISMATCH_STATUSES.includes(page.status); +} + +interface StatusCounts { + total: number; + mismatches: number; + new: number; + changesFound: number; + approved: number; + rejected: number; + addedToBaseline: number; +} + +function getStatusCategory(status: string): keyof Omit | null { + switch (status) { + case 'new-screenshot': return 'new'; + case 'Changes Found': + case 'Under Screening': return 'changesFound'; + case 'Approved': return 'approved'; + case 'Rejected': return 'rejected'; + case 'moved': return 'addedToBaseline'; + default: return null; + } +} + +function countItemsByStatus(items: any[]): StatusCounts { + const counts: StatusCounts = { total: items.length, mismatches: 0, new: 0, changesFound: 0, approved: 0, rejected: 0, addedToBaseline: 0 }; + for (const item of items) { + if (isPageMismatch(item)) counts.mismatches++; + const cat = getStatusCategory(item.status); + if (cat) counts[cat]++; + } + return counts; +} + +function countGroupsByStatus(groups: Record): StatusCounts { + const counts: StatusCounts = { total: Object.keys(groups).length, mismatches: 0, new: 0, changesFound: 0, approved: 0, rejected: 0, addedToBaseline: 0 }; + for (const items of Object.values(groups)) { + if (items.some(i => isPageMismatch(i))) counts.mismatches++; + if (items.some(i => i.status === 'new-screenshot')) counts.new++; + if (items.some(i => i.status === 'Changes Found' || i.status === 'Under Screening')) counts.changesFound++; + if (items.every(i => i.status === 'Approved')) counts.approved++; + if (items.some(i => i.status === 'Rejected')) counts.rejected++; + if (items.some(i => i.status === 'moved')) counts.addedToBaseline++; + } + return counts; +} + +// --- PDF grouping and formatting --- + function groupScreenshotsByPdf(screenshots: any[]): Record { const pdfGroups: Record = {}; screenshots.forEach(screenshot => { - // screenshot name format: "pdf-name.pdf#page-number" - const pdfName = screenshot.screenshot_name.split('#')[0]; + const { pdfName } = extractPdfNameAndPage(screenshot.screenshot_name, screenshot.browser_name); if (!pdfGroups[pdfName]) { pdfGroups[pdfName] = []; @@ -752,12 +922,6 @@ function groupScreenshotsByPdf(screenshots: any[]): Record { return pdfGroups; } -const NON_MISMATCH_STATUSES = ['Approved', 'moved', 'new-screenshot']; - -function isPageMismatch(page: any): boolean { - return !NON_MISMATCH_STATUSES.includes(page.status); -} - function countPdfsWithMismatches(pdfGroups: Record): number { let count = 0; @@ -780,7 +944,7 @@ function formatPdfsForOutput(pdfGroups: Record): any[] { pdfName, pageCount: pages.length, pages: pages.map(page => ({ - pageNumber: getPageNumber(page.screenshot_name), + pageNumber: extractPdfNameAndPage(page.screenshot_name, page.browser_name).pageNumber, screenshotId: page.captured_image_id, mismatchPercentage: page.mismatch_percentage, status: page.status, @@ -790,9 +954,90 @@ function formatPdfsForOutput(pdfGroups: Record): any[] { }); } -function getPageNumber(screenshotName: string): string { - const parts = screenshotName.split('#'); - return parts.length > 1 ? parts[1] : '1'; +function formatScreenshotsForOutput(screenshots: Record): any[] { + return Object.entries(screenshots).map(([name, variants]) => { + return { + screenshotName: name, + variantCount: variants.length, + variants: variants.map(variant => ({ + variantId: variant.captured_image_id, + browser: variant.browser_name, + viewport: variant.viewport, + os: variant.os, + mismatchPercentage: variant.mismatch_percentage, + status: variant.status, + screenshotUrl: variant.shareable_link + })) + }; + }); +} + +// --- Omni display helpers --- + +function printOmniHeader(build: any, project: any) { + console.log(chalk.green.bold(`\nProject Name: ${project.name}`)); + console.log(chalk.green.bold(`Build Name: ${build.build_name}`)); + console.log(chalk.green.bold(`Build ID: ${build.build_id}`)); + console.log(chalk.green.bold(`Build Status: ${build.build_status}`)); + const buildResult = build.build_status?.toLowerCase() === 'approved' ? 'Passed' : 'Failed'; + const resultColor = buildResult === 'Passed' ? chalk.green : chalk.red; + console.log(resultColor.bold(`Build Result : ${buildResult}`)); + console.log(chalk.green.bold(`Branch Name: ${build.branch}`)); + console.log(chalk.white('-----')); +} + +function printPdfSection(pdfScreenshots: any[]) { + const pdfGroups = groupScreenshotsByPdf(pdfScreenshots); + const pageCounts = countItemsByStatus(pdfScreenshots); + const pdfCounts = countGroupsByStatus(pdfGroups); + + console.log(chalk.green.bold('\nPDF Test Results:')); + console.log(chalk.green(`Total PDFs: ${pdfCounts.total}`)); + console.log(chalk.green(`Total Pages: ${pageCounts.total}`)); + + if (pageCounts.mismatches > 0 || pdfCounts.mismatches > 0) { + console.log(chalk.yellow(`\n${pageCounts.mismatches} page(s) and ${pdfCounts.mismatches} PDF(s) have mismatches`)); + } + + if (pageCounts.new > 0 || pdfCounts.new > 0) console.log(chalk.cyan(`${pageCounts.new} page(s) and ${pdfCounts.new} PDF(s) are new`)); + if (pageCounts.changesFound > 0 || pdfCounts.changesFound > 0) console.log(chalk.yellow(`${pageCounts.changesFound} page(s) and ${pdfCounts.changesFound} PDF(s) have changes found`)); + if (pageCounts.approved > 0 || pdfCounts.approved > 0) console.log(chalk.green(`${pageCounts.approved} page(s) and ${pdfCounts.approved} PDF(s) are approved`)); + if (pageCounts.addedToBaseline > 0 || pdfCounts.addedToBaseline > 0) console.log(chalk.green(`${pageCounts.addedToBaseline} page(s) and ${pdfCounts.addedToBaseline} PDF(s) are added to baseline`)); + if (pageCounts.rejected > 0 || pdfCounts.rejected > 0) console.log(chalk.red(`${pageCounts.rejected} page(s) and ${pdfCounts.rejected} PDF(s) have been rejected`)); + + console.log(chalk.white('-----')); + + return { pdfGroups, pageCounts, pdfCounts }; +} + +function printScreenshotSection(normalScreenshots: Record) { + const allVariants: any[] = []; + for (const variants of Object.values(normalScreenshots)) { + for (const v of variants) allVariants.push(v); + } + + const variantCounts = countItemsByStatus(allVariants); + const screenshotCounts = countGroupsByStatus(normalScreenshots); + + console.log(chalk.green.bold('\nScreenshot Test Results:')); + console.log(chalk.green(`Total Screenshots: ${screenshotCounts.total}`)); + console.log(chalk.green(`Total Variants: ${variantCounts.total}`)); + + if (variantCounts.mismatches > 0 || screenshotCounts.mismatches > 0) { + console.log(chalk.yellow(`\n${variantCounts.mismatches} variant(s) and ${screenshotCounts.mismatches} screenshot(s) have mismatches`)); + } + + if (variantCounts.new > 0 || screenshotCounts.new > 0) console.log(chalk.cyan(`${variantCounts.new} variant(s) and ${screenshotCounts.new} screenshot(s) are new`)); + if (variantCounts.changesFound > 0 || screenshotCounts.changesFound > 0) console.log(chalk.yellow(`${variantCounts.changesFound} variant(s) and ${screenshotCounts.changesFound} screenshot(s) have changes found`)); + if (variantCounts.approved > 0 || screenshotCounts.approved > 0) console.log(chalk.green(`${variantCounts.approved} variant(s) and ${screenshotCounts.approved} screenshot(s) are approved`)); + if (variantCounts.addedToBaseline > 0 || screenshotCounts.addedToBaseline > 0) console.log(chalk.green(`${variantCounts.addedToBaseline} variant(s) and ${screenshotCounts.addedToBaseline} screenshot(s) are added to baseline`)); + if (variantCounts.rejected > 0 || screenshotCounts.rejected > 0) console.log(chalk.red(`${variantCounts.rejected} variant(s) and ${screenshotCounts.rejected} screenshot(s) have been rejected`)); + + return { variantCounts, screenshotCounts }; +} + +function getPageNumber(screenshotName: string, browserName?: string): string { + return extractPdfNameAndPage(screenshotName, browserName || '').pageNumber; } export function validateCoordinates(