From a9897442a37c831da3d08bce02a03a8bcfe8c061 Mon Sep 17 00:00:00 2001 From: parthkirsan Date: Wed, 3 Sep 2025 20:02:36 +0530 Subject: [PATCH 01/13] initial commit for smartui caps in start server --- src/commander/server.ts | 8 ++++---- src/lib/server.ts | 45 ++++++++++++++++++++++++++++++----------- 2 files changed, 37 insertions(+), 16 deletions(-) diff --git a/src/commander/server.ts b/src/commander/server.ts index 1fbea907..11ac2675 100644 --- a/src/commander/server.ts +++ b/src/commander/server.ts @@ -2,10 +2,10 @@ import { Command } from 'commander'; import { Context } from '../types.js'; import { color, Listr, ListrDefaultRendererLogLevels } from 'listr2'; import startServer from '../tasks/startServer.js'; -import auth from '../tasks/auth.js'; +import authExec from '../tasks/authExec.js'; import ctxInit from '../lib/ctx.js'; import getGitInfo from '../tasks/getGitInfo.js'; -import createBuild from '../tasks/createBuild.js'; +import createBuildExec from '../tasks/createBuildExec.js'; import snapshotQueue from '../lib/snapshotQueue.js'; import { startPolling, startPingPolling } from '../lib/utils.js'; @@ -30,10 +30,10 @@ command let tasks = new Listr( [ - auth(ctx), + authExec(ctx), startServer(ctx), getGitInfo(ctx), - createBuild(ctx), + createBuildExec(ctx), ], { diff --git a/src/lib/server.ts b/src/lib/server.ts index e2a1955a..fd363b4a 100644 --- a/src/lib/server.ts +++ b/src/lib/server.ts @@ -118,23 +118,44 @@ export default async (ctx: Context): Promise Date: Mon, 8 Sep 2025 17:36:56 +0530 Subject: [PATCH 02/13] Add PDF upload functionality and related commands --- src/commander/commander.ts | 2 + src/commander/uploadPdf.ts | 60 ++++++++++++++ src/lib/env.ts | 2 + src/lib/httpClient.ts | 70 ++++++++++++++++ src/lib/utils.ts | 165 ++++++++++++++++++++++++++++++++++++- src/tasks/uploadPdfs.ts | 72 ++++++++++++++++ src/types.ts | 1 + 7 files changed, 371 insertions(+), 1 deletion(-) create mode 100644 src/commander/uploadPdf.ts create mode 100644 src/tasks/uploadPdfs.ts diff --git a/src/commander/commander.ts b/src/commander/commander.ts index 25f59e5b..5daf9357 100644 --- a/src/commander/commander.ts +++ b/src/commander/commander.ts @@ -10,6 +10,7 @@ import stopServer from './stopServer.js' import ping from './ping.js' import merge from './merge.js' import pingTest from './pingTest.js' +import uploadPdf from "./uploadPdf.js"; const program = new Command(); @@ -38,6 +39,7 @@ program .addCommand(uploadWebFigmaCommand) .addCommand(uploadAppFigmaCommand) .addCommand(pingTest) + .addCommand(uploadPdf) diff --git a/src/commander/uploadPdf.ts b/src/commander/uploadPdf.ts new file mode 100644 index 00000000..29ceeea0 --- /dev/null +++ b/src/commander/uploadPdf.ts @@ -0,0 +1,60 @@ +import {Command} from "commander"; +import { Context } from '../types.js'; +import ctxInit from '../lib/ctx.js'; +import { color, Listr, ListrDefaultRendererLogLevels, LoggerFormat } from 'listr2'; +import fs from 'fs'; +import auth from '../tasks/auth.js'; +import uploadPdfs from '../tasks/uploadPdfs.js'; +import {startPdfPolling} from "../lib/utils.js"; +const command = new Command(); + +command + .name('upload-pdf') + .description('Upload PDFs for visual comparison') + .argument('', 'Path of the directory containing PDFs') + .option('--fetch-results [filename]', 'Fetch results and optionally specify an output file, e.g., .json') + .option('--buildName ', 'Specify the build name') + .action(async function(directory, _, command) { + const options = command.optsWithGlobals(); + if (options.buildName === '') { + console.log(`Error: The '--buildName' option cannot be an empty string.`); + process.exit(1); + } + let ctx: Context = ctxInit(command.optsWithGlobals()); + + if (!fs.existsSync(directory)) { + console.log(`Error: The provided directory ${directory} not found.`); + return; + } + + ctx.uploadFilePath = directory; + + let tasks = new Listr( + [ + auth(ctx), + uploadPdfs(ctx) + ], + { + rendererOptions: { + icon: { + [ListrDefaultRendererLogLevels.OUTPUT]: `→` + }, + color: { + [ListrDefaultRendererLogLevels.OUTPUT]: color.gray as LoggerFormat + } + } + } + ); + + try { + await tasks.run(ctx); + + if (ctx.options.fetchResults) { + startPdfPolling(ctx); + } + } catch (error) { + console.log('\nRefer docs: https://www.lambdatest.com/support/docs/smart-visual-regression-testing/'); + } + }); + +export default command; \ No newline at end of file diff --git a/src/lib/env.ts b/src/lib/env.ts index 5dd1bab1..8e8cf1d5 100644 --- a/src/lib/env.ts +++ b/src/lib/env.ts @@ -4,6 +4,7 @@ export default (): Env => { const { PROJECT_TOKEN = '', SMARTUI_CLIENT_API_URL = 'https://api.lambdatest.com/visualui/1.0', + SMARTUI_UPLOAD_URL = 'https://api.lambdatest.com', SMARTUI_GIT_INFO_FILEPATH, SMARTUI_DO_NOT_USE_CAPTURED_COOKIES, HTTP_PROXY, @@ -27,6 +28,7 @@ export default (): Env => { return { PROJECT_TOKEN, SMARTUI_CLIENT_API_URL, + SMARTUI_UPLOAD_URL: SMARTUI_UPLOAD_URL, SMARTUI_GIT_INFO_FILEPATH, HTTP_PROXY, HTTPS_PROXY, diff --git a/src/lib/httpClient.ts b/src/lib/httpClient.ts index acf55626..7b1cf41c 100644 --- a/src/lib/httpClient.ts +++ b/src/lib/httpClient.ts @@ -644,4 +644,74 @@ export default class httpClient { } }, ctx.log); } + + async uploadPdf(ctx: Context, form: FormData, log: Logger, buildName?: string): Promise { + form.append('projectToken', this.projectToken); + if (ctx.build.name !== undefined && ctx.build.name !== '') { + form.append('buildName', buildName); + } + ctx.log.debug('Uploading PDF to SmartUI...'); + + const response = await this.axiosInstance.request({ + url: ctx.env.SMARTUI_UPLOAD_URL + '/pdf/upload', + method: 'POST', + headers: { + 'Content-Type': 'application/json', + }, + data: form, + }); + + log.debug(`http response: ${JSON.stringify({ + status: response.status, + headers: response.headers, + body: response.data + })}`); + + return response.data; + } + + async fetchPdfResults(ctx: Context, log: Logger): Promise { + const params: Record = { + projectToken: this.projectToken + }; + + // Use buildId if available, otherwise use buildName + if (ctx.build.id) { + params.buildId = ctx.build.id; + } else if (ctx.build.name) { + params.buildName = ctx.build.name; + } + + const auth = Buffer.from(`${this.username}:${this.accessKey}`).toString('base64'); + + try { + const response = await axios.request({ + url: ctx.env.SMARTUI_UPLOAD_URL + '/automation/smart-ui/screenshot/build/status', + method: 'GET', + params: params, + headers: { + 'accept': 'application/json', + 'Authorization': `Basic ${auth}` + } + }); + + log.debug(`http response: ${JSON.stringify({ + status: response.status, + headers: response.headers, + body: response.data + })}`); + + return response.data; + } catch (error: any) { + log.error(`Error fetching PDF results: ${error.message}`); + if (error.response) { + log.debug(`Response error: ${JSON.stringify({ + status: error.response.status, + data: error.response.data + })}`); + } + throw error; + } + } } + diff --git a/src/lib/utils.ts b/src/lib/utils.ts index 21384e15..659f37c0 100644 --- a/src/lib/utils.ts +++ b/src/lib/utils.ts @@ -509,4 +509,167 @@ export function calculateVariantCountFromSnapshot(snapshot: any, globalConfig?: } return variantCount; -} \ No newline at end of file +} + +export function startPdfPolling(ctx: Context) { + console.log(chalk.yellow('\nFetching PDF test results...')); + + if (!ctx.build.id && !ctx.build.name) { + ctx.log.error(chalk.red('Error: Build information not found for fetching results')); + 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 = 30; // 5 minutes (10 seconds * 30) + + console.log(chalk.yellow('Waiting for results...')); + + const interval = setInterval(async () => { + attempts++; + + try { + const response = await ctx.client.fetchPdfResults(ctx, ctx.log); + + if (response.status === 'success' && response.data && response.data.Screenshots) { + clearInterval(interval); + + const pdfGroups = groupScreenshotsByPdf(response.data.Screenshots); + + const pdfsWithMismatches = countPdfsWithMismatches(pdfGroups); + const pagesWithMismatches = countPagesWithMismatches(response.data.Screenshots); + + console.log(chalk.green('\nāœ“ PDF Test Results:')); + console.log(chalk.green(`Build Name: ${response.data.buildName}`)); + console.log(chalk.green(`Project Name: ${response.data.projectName}`)); + console.log(chalk.green(`Total PDFs: ${Object.keys(pdfGroups).length}`)); + console.log(chalk.green(`Total Pages: ${response.data.Screenshots.length}`)); + + if (pdfsWithMismatches > 0 || pagesWithMismatches > 0) { + console.log(chalk.yellow(`${pdfsWithMismatches} PDFs and ${pagesWithMismatches} Pages in build ${response.data.buildName} 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.mismatchPercentage > 0); + const statusColor = hasMismatch ? chalk.yellow : chalk.green; + + console.log(statusColor(`\nšŸ“„ ${pdfName} (${pages.length} pages)`)); + + pages.forEach(page => { + const pageStatusColor = page.mismatchPercentage > 0 ? chalk.yellow : chalk.green; + console.log(pageStatusColor(` - Page ${getPageNumber(page.screenshotName)}: ${page.status} (Mismatch: ${page.mismatchPercentage}%)`)); + }); + }); + + const formattedResults = { + status: response.status, + data: { + buildId: response.data.buildId, + buildName: response.data.buildName, + projectName: response.data.projectName, + buildStatus: response.data.buildStatus, + pdfs: formatPdfsForOutput(pdfGroups) + } + }; + + // 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'; + + fs.writeFileSync(filename, JSON.stringify(formattedResults, null, 2)); + console.log(chalk.green(`\nResults saved to ${filename}`)); + } + + return; + } else if (response.status === 'error') { + clearInterval(interval); + console.log(chalk.red(`\nError fetching results: ${response.message || 'Unknown error'}`)); + return; + } else { + process.stdout.write(chalk.yellow('.')); + } + + if (attempts >= maxAttempts) { + clearInterval(interval); + console.log(chalk.red('\nTimeout: Could not fetch PDF results after 5 minutes')); + return; + } + + } catch (error: any) { + ctx.log.debug(`Error during polling: ${error.message}`); + + if (attempts >= maxAttempts) { + clearInterval(interval); + console.log(chalk.red('\nTimeout: Could not fetch PDF results after 5 minutes')); + if (error.response && error.response.data) { + console.log(chalk.red(`Error details: ${JSON.stringify(error.response.data)}`)); + } else { + console.log(chalk.red(`Error details: ${error.message}`)); + } + return; + } + process.stdout.write(chalk.yellow('.')); + } + }, 10000); +} + +function groupScreenshotsByPdf(screenshots: any[]): Record { + const pdfGroups: Record = {}; + + screenshots.forEach(screenshot => { + // screenshot name format: "pdf-name.pdf#page-number" + const pdfName = screenshot.screenshotName.split('#')[0]; + + if (!pdfGroups[pdfName]) { + pdfGroups[pdfName] = []; + } + + pdfGroups[pdfName].push(screenshot); + }); + + return pdfGroups; +} + +function countPdfsWithMismatches(pdfGroups: Record): number { + let count = 0; + + Object.values(pdfGroups).forEach(pages => { + if (pages.some(page => page.mismatchPercentage > 0)) { + count++; + } + }); + + return count; +} + +function countPagesWithMismatches(screenshots: any[]): number { + return screenshots.filter(screenshot => screenshot.mismatchPercentage > 0).length; +} + +function formatPdfsForOutput(pdfGroups: Record): any[] { + return Object.entries(pdfGroups).map(([pdfName, pages]) => { + return { + pdfName, + pageCount: pages.length, + pages: pages.map(page => ({ + pageNumber: getPageNumber(page.screenshotName), + screenshotId: page.screenshotId, + mismatchPercentage: page.mismatchPercentage, + threshold: page.threshold, + status: page.status, + screenshotUrl: page.screenshotUrl + })) + }; + }); +} + +function getPageNumber(screenshotName: string): string { + const parts = screenshotName.split('#'); + return parts.length > 1 ? parts[1] : '1'; +} \ No newline at end of file diff --git a/src/tasks/uploadPdfs.ts b/src/tasks/uploadPdfs.ts new file mode 100644 index 00000000..8e406129 --- /dev/null +++ b/src/tasks/uploadPdfs.ts @@ -0,0 +1,72 @@ +import { ListrTask, ListrRendererFactory } from 'listr2'; +import { Context } from '../types.js'; +import chalk from 'chalk'; +import { updateLogContext } from '../lib/logger.js'; +import path from 'path'; +import fs from 'fs'; +import FormData from 'form-data'; + +export default (ctx: Context): ListrTask => { + return { + title: 'Uploading PDFs', + task: async (ctx, task): Promise => { + try { + ctx.task = task; + updateLogContext({ task: 'upload-pdf' }); + + // const pdfs = await getPdfsFromDirectory(ctx.uploadFilePath); + // if (pdfs.length === 0) { + // throw new Error('No PDF files found in the specified directory'); + // } + // + // for (const pdf of pdfs) { + // task.output = `Uploading ${path.basename(pdf)}...`; + // await uploadPdfs(ctx, pdf); + // } + await uploadPdfs(ctx, ctx.uploadFilePath); + + task.title = 'PDFs uploaded successfully'; + } catch (error: any) { + ctx.log.debug(error); + task.output = chalk.gray(`${error.message}`); + throw new Error('Uploading PDFs failed'); + } + }, + rendererOptions: { persistentOutput: true }, + exitOnError: false + }; +}; + +async function getPdfsFromDirectory(directory: string): Promise { + const files = await fs.promises.readdir(directory); + return files + .filter(file => path.extname(file).toLowerCase() === '.pdf') + .map(file => path.join(directory, file)); +} + +async function uploadPdfs(ctx: Context, pdfPath: string): Promise { + const formData = new FormData(); + const files = fs.readdirSync(pdfPath); + const pdfFiles = files.filter(file => file.endsWith('.pdf')); + + pdfFiles.forEach(pdf => { + const filePath = path.join(pdfPath, pdf); + formData.append('pathToFiles', fs.createReadStream(filePath)); + }) + + // formData.append('pathToFiles', fs.createReadStream(pdfPath)); + // formData.append('name', path.basename(pdfPath, '.pdf')); + + const buildName = ctx.options.buildName; + + if (buildName) { + ctx.build.name = buildName; + } + + const response = await ctx.client.uploadPdf(ctx, formData, ctx.log, buildName); + + if (response && response.data && response.data.buildId) { + ctx.build.id = response.data.buildId; + ctx.log.debug(`PDF upload successful. Build ID: ${ctx.build.id}`); + } +} \ No newline at end of file diff --git a/src/types.ts b/src/types.ts index 96371158..5c6eefe4 100644 --- a/src/types.ts +++ b/src/types.ts @@ -93,6 +93,7 @@ export interface Context { export interface Env { PROJECT_TOKEN: string; SMARTUI_CLIENT_API_URL: string; + SMARTUI_UPLOAD_URL: string; SMARTUI_DO_NOT_USE_CAPTURED_COOKIES: boolean; SMARTUI_GIT_INFO_FILEPATH: string | undefined; HTTP_PROXY: string | undefined; From 1258ca9d9881e511041ccfb08da40e3f30240ce2 Mon Sep 17 00:00:00 2001 From: Zeeshan Date: Mon, 8 Sep 2025 18:11:48 +0530 Subject: [PATCH 03/13] Refactor PDF upload logic to use FormData headers and clean up unused code --- src/lib/httpClient.ts | 4 +--- src/lib/utils.ts | 1 + src/tasks/uploadPdfs.ts | 23 ++--------------------- 3 files changed, 4 insertions(+), 24 deletions(-) diff --git a/src/lib/httpClient.ts b/src/lib/httpClient.ts index 7b1cf41c..de6e554a 100644 --- a/src/lib/httpClient.ts +++ b/src/lib/httpClient.ts @@ -655,9 +655,7 @@ export default class httpClient { const response = await this.axiosInstance.request({ url: ctx.env.SMARTUI_UPLOAD_URL + '/pdf/upload', method: 'POST', - headers: { - 'Content-Type': 'application/json', - }, + headers: form.getHeaders(), data: form, }); diff --git a/src/lib/utils.ts b/src/lib/utils.ts index 659f37c0..b9df5919 100644 --- a/src/lib/utils.ts +++ b/src/lib/utils.ts @@ -514,6 +514,7 @@ export function calculateVariantCountFromSnapshot(snapshot: any, globalConfig?: export function startPdfPolling(ctx: Context) { console.log(chalk.yellow('\nFetching PDF test results...')); + ctx.log.debug(`Starting fetching results for build: ${ctx.build.id || ctx.build.name}`); if (!ctx.build.id && !ctx.build.name) { ctx.log.error(chalk.red('Error: Build information not found for fetching results')); return diff --git a/src/tasks/uploadPdfs.ts b/src/tasks/uploadPdfs.ts index 8e406129..3f107536 100644 --- a/src/tasks/uploadPdfs.ts +++ b/src/tasks/uploadPdfs.ts @@ -14,15 +14,6 @@ export default (ctx: Context): ListrTask { - const files = await fs.promises.readdir(directory); - return files - .filter(file => path.extname(file).toLowerCase() === '.pdf') - .map(file => path.join(directory, file)); -} - async function uploadPdfs(ctx: Context, pdfPath: string): Promise { const formData = new FormData(); const files = fs.readdirSync(pdfPath); @@ -54,9 +38,6 @@ async function uploadPdfs(ctx: Context, pdfPath: string): Promise { formData.append('pathToFiles', fs.createReadStream(filePath)); }) - // formData.append('pathToFiles', fs.createReadStream(pdfPath)); - // formData.append('name', path.basename(pdfPath, '.pdf')); - const buildName = ctx.options.buildName; if (buildName) { @@ -65,8 +46,8 @@ async function uploadPdfs(ctx: Context, pdfPath: string): Promise { const response = await ctx.client.uploadPdf(ctx, formData, ctx.log, buildName); - if (response && response.data && response.data.buildId) { - ctx.build.id = response.data.buildId; + if (response && response.buildId) { + ctx.build.id = response.buildId; ctx.log.debug(`PDF upload successful. Build ID: ${ctx.build.id}`); } } \ No newline at end of file From 2eec99f541df614d5002e4af03ad98fda4bb615f Mon Sep 17 00:00:00 2001 From: Zeeshan Date: Mon, 8 Sep 2025 19:07:54 +0530 Subject: [PATCH 04/13] Enhance PDF upload function to handle single PDF files and improve file reading logic --- src/tasks/uploadPdfs.ts | 17 +++++++++++------ 1 file changed, 11 insertions(+), 6 deletions(-) diff --git a/src/tasks/uploadPdfs.ts b/src/tasks/uploadPdfs.ts index 3f107536..39b552ba 100644 --- a/src/tasks/uploadPdfs.ts +++ b/src/tasks/uploadPdfs.ts @@ -30,13 +30,18 @@ export default (ctx: Context): ListrTask { const formData = new FormData(); - const files = fs.readdirSync(pdfPath); - const pdfFiles = files.filter(file => file.endsWith('.pdf')); - pdfFiles.forEach(pdf => { - const filePath = path.join(pdfPath, pdf); - formData.append('pathToFiles', fs.createReadStream(filePath)); - }) + if (pdfPath.endsWith('.pdf')) { + formData.append('pathToFiles', fs.createReadStream(pdfPath)); + } else { + const files = fs.readdirSync(pdfPath); + const pdfFiles = files.filter(file => file.endsWith('.pdf')); + + pdfFiles.forEach(pdf => { + const filePath = path.join(pdfPath, pdf); + formData.append('pathToFiles', fs.createReadStream(filePath)); + }) + } const buildName = ctx.options.buildName; From 2ff20b75fce53159f621c79c4f407524052a6471 Mon Sep 17 00:00:00 2001 From: Zeeshan Date: Mon, 8 Sep 2025 20:08:09 +0530 Subject: [PATCH 05/13] Refactor PDF upload methods to improve error handling and logging --- src/lib/httpClient.ts | 58 ++++++++++++++++++++++++----------------- src/tasks/uploadPdfs.ts | 17 +++++++----- 2 files changed, 44 insertions(+), 31 deletions(-) diff --git a/src/lib/httpClient.ts b/src/lib/httpClient.ts index de6e554a..030fa7b3 100644 --- a/src/lib/httpClient.ts +++ b/src/lib/httpClient.ts @@ -14,6 +14,18 @@ export default class httpClient { username: string; accessKey: string; + private handleHttpError(error: any, log: Logger): never { + if (error.response) { + log.debug(`http response error: ${JSON.stringify({ + status: error.response.status, + body: error.response.data + })}`); + throw new Error(error.response.data?.message || error.response.data || `HTTP ${error.response.status} error`); + } + log.debug(`http request failed: ${error.message}`); + throw new Error(error.message); + } + constructor({ SMARTUI_CLIENT_API_URL, PROJECT_TOKEN, PROJECT_NAME, LT_USERNAME, LT_ACCESS_KEY, SMARTUI_API_PROXY, SMARTUI_API_SKIP_CERTIFICATES }: Env) { this.projectToken = PROJECT_TOKEN || ''; this.projectName = PROJECT_NAME || ''; @@ -83,6 +95,8 @@ export default class httpClient { // If we've reached max retries, reject with the error return Promise.reject(error); + } else { + return Promise.reject(error); } } ); @@ -645,30 +659,33 @@ export default class httpClient { }, ctx.log); } - async uploadPdf(ctx: Context, form: FormData, log: Logger, buildName?: string): Promise { + async uploadPdf(ctx: Context, form: FormData, buildName?: string): Promise { form.append('projectToken', this.projectToken); if (ctx.build.name !== undefined && ctx.build.name !== '') { form.append('buildName', buildName); } - ctx.log.debug('Uploading PDF to SmartUI...'); - const response = await this.axiosInstance.request({ - url: ctx.env.SMARTUI_UPLOAD_URL + '/pdf/upload', - method: 'POST', - headers: form.getHeaders(), - data: form, - }); + try { + const response = await this.axiosInstance.request({ + url: ctx.env.SMARTUI_UPLOAD_URL + '/pdf/upload', + method: 'POST', + headers: form.getHeaders(), + data: form, + }); - log.debug(`http response: ${JSON.stringify({ - status: response.status, - headers: response.headers, - body: response.data - })}`); + ctx.log.debug(`http response: ${JSON.stringify({ + status: response.status, + headers: response.headers, + body: response.data + })}`); - return response.data; + return response.data; + } catch (error: any) { + this.handleHttpError(error, ctx.log); + } } - async fetchPdfResults(ctx: Context, log: Logger): Promise { + async fetchPdfResults(ctx: Context): Promise { const params: Record = { projectToken: this.projectToken }; @@ -693,7 +710,7 @@ export default class httpClient { } }); - log.debug(`http response: ${JSON.stringify({ + ctx.log.debug(`http response: ${JSON.stringify({ status: response.status, headers: response.headers, body: response.data @@ -701,14 +718,7 @@ export default class httpClient { return response.data; } catch (error: any) { - log.error(`Error fetching PDF results: ${error.message}`); - if (error.response) { - log.debug(`Response error: ${JSON.stringify({ - status: error.response.status, - data: error.response.data - })}`); - } - throw error; + this.handleHttpError(error, ctx.log); } } } diff --git a/src/tasks/uploadPdfs.ts b/src/tasks/uploadPdfs.ts index 39b552ba..ddc651d3 100644 --- a/src/tasks/uploadPdfs.ts +++ b/src/tasks/uploadPdfs.ts @@ -19,8 +19,8 @@ export default (ctx: Context): ListrTask { ctx.build.name = buildName; } - const response = await ctx.client.uploadPdf(ctx, formData, ctx.log, buildName); - - if (response && response.buildId) { - ctx.build.id = response.buildId; - ctx.log.debug(`PDF upload successful. Build ID: ${ctx.build.id}`); + try { + const response = await ctx.client.uploadPdf(ctx, formData, buildName); + if (response && response.buildId) { + ctx.build.id = response.buildId; + ctx.log.debug(`PDF upload successful. Build ID: ${ctx.build.id}`); + } + } catch (error : any) { + throw new Error(error.message); } } \ No newline at end of file From 3cf9ad0054abb9017770c44e89d156e71c590a41 Mon Sep 17 00:00:00 2001 From: Zeeshan Date: Mon, 8 Sep 2025 20:13:40 +0530 Subject: [PATCH 06/13] Update error handling in PDF upload to exit process on directory not found --- src/commander/uploadPdf.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/commander/uploadPdf.ts b/src/commander/uploadPdf.ts index 29ceeea0..a82246cc 100644 --- a/src/commander/uploadPdf.ts +++ b/src/commander/uploadPdf.ts @@ -24,7 +24,7 @@ command if (!fs.existsSync(directory)) { console.log(`Error: The provided directory ${directory} not found.`); - return; + process.exit(1); } ctx.uploadFilePath = directory; From 0c7856602363ab1ee7b5444688915dd44e36b028 Mon Sep 17 00:00:00 2001 From: Zeeshan Date: Mon, 8 Sep 2025 20:14:40 +0530 Subject: [PATCH 07/13] Changes --- src/commander/uploadPdf.ts | 1 + 1 file changed, 1 insertion(+) diff --git a/src/commander/uploadPdf.ts b/src/commander/uploadPdf.ts index a82246cc..07c319f3 100644 --- a/src/commander/uploadPdf.ts +++ b/src/commander/uploadPdf.ts @@ -54,6 +54,7 @@ command } } catch (error) { console.log('\nRefer docs: https://www.lambdatest.com/support/docs/smart-visual-regression-testing/'); + process.exit(1); } }); From 3cfd4a6574f1939573ba8db4bc218ed2fec9a6df Mon Sep 17 00:00:00 2001 From: Zeeshan Date: Mon, 8 Sep 2025 20:15:47 +0530 Subject: [PATCH 08/13] Fix http client --- src/lib/httpClient.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/lib/httpClient.ts b/src/lib/httpClient.ts index 030fa7b3..3cbd31fd 100644 --- a/src/lib/httpClient.ts +++ b/src/lib/httpClient.ts @@ -15,7 +15,7 @@ export default class httpClient { accessKey: string; private handleHttpError(error: any, log: Logger): never { - if (error.response) { + if (error && error.response) { log.debug(`http response error: ${JSON.stringify({ status: error.response.status, body: error.response.data From 8828eeabe6234d4b60f526d2ad9fd9778c13161e Mon Sep 17 00:00:00 2001 From: Sushobhit Dua <38429226+sushobhit-lt@users.noreply.github.com> Date: Mon, 8 Sep 2025 23:12:26 +0530 Subject: [PATCH 09/13] fix non git info --- src/lib/git.ts | 25 ++++++++++++++++++++++++- src/tasks/getGitInfo.ts | 2 +- 2 files changed, 25 insertions(+), 2 deletions(-) diff --git a/src/lib/git.ts b/src/lib/git.ts index 324e10b0..fc01ec6e 100644 --- a/src/lib/git.ts +++ b/src/lib/git.ts @@ -17,11 +17,12 @@ function executeCommand(command: string): string { } } -export function isGitRepo(): boolean { +export function isGitRepo(ctx: Context): boolean { try { executeCommand('git status') return true } catch (error) { + setNonGitInfo(ctx) return false } } @@ -82,3 +83,25 @@ export default (ctx: Context): Git => { }; } } + + +function setNonGitInfo(ctx: Context) { + let branch = ctx.env.CURRENT_BRANCH || 'unknown-branch' + if (ctx.options.markBaseline) { + ctx.env.BASELINE_BRANCH = branch + ctx.env.SMART_GIT = false + } + let githubURL; + if (ctx.options.githubURL && ctx.options.githubURL.startsWith('https://')) { + githubURL = ctx.options.githubURL; + } + + ctx.git = { + branch: branch, + commitId: '-', + commitAuthor: '-', + commitMessage: '-', + githubURL: githubURL? githubURL : '', + baselineBranch: ctx.options.baselineBranch || ctx.env.BASELINE_BRANCH || '' + } +} \ No newline at end of file diff --git a/src/tasks/getGitInfo.ts b/src/tasks/getGitInfo.ts index b382fc22..734fc890 100644 --- a/src/tasks/getGitInfo.ts +++ b/src/tasks/getGitInfo.ts @@ -7,7 +7,7 @@ export default (ctx: Context): ListrTask { - return (!isGitRepo() && !ctx.env.SMARTUI_GIT_INFO_FILEPATH) ? '[SKIPPED] Fetching git repo details; not a git repo' : ''; + return (!isGitRepo(ctx) && !ctx.env.SMARTUI_GIT_INFO_FILEPATH) ? '[SKIPPED] Fetching git repo details; not a git repo' : ''; }, task: async (ctx, task): Promise => { if (ctx.env.CURRENT_BRANCH && ctx.env.CURRENT_BRANCH.trim() === '') { From 75904fac7ab613e4473e0464a72de353e2073a01 Mon Sep 17 00:00:00 2001 From: Sushobhit Dua <38429226+sushobhit-lt@users.noreply.github.com> Date: Tue, 9 Sep 2025 13:10:56 +0530 Subject: [PATCH 10/13] remove console log --- src/lib/server.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/lib/server.ts b/src/lib/server.ts index e2a1955a..b39673a8 100644 --- a/src/lib/server.ts +++ b/src/lib/server.ts @@ -63,7 +63,7 @@ export default async (ctx: Context): Promise Date: Tue, 9 Sep 2025 13:39:41 +0530 Subject: [PATCH 11/13] fix isStartExec --- src/lib/snapshotQueue.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/lib/snapshotQueue.ts b/src/lib/snapshotQueue.ts index 7cc9bcb2..0ca45a7d 100644 --- a/src/lib/snapshotQueue.ts +++ b/src/lib/snapshotQueue.ts @@ -286,7 +286,7 @@ export default class Queue { let drop = false; - if (this.ctx.isStartExec && !this.ctx.config.tunnel) { + if (this.ctx.isStartExec) { this.ctx.log.info(`Processing Snapshot: ${snapshot?.name}`); } From 3037ea6db338db36f3ce8880fc9704b3ad2847e2 Mon Sep 17 00:00:00 2001 From: Sushobhit Dua <38429226+sushobhit-lt@users.noreply.github.com> Date: Tue, 9 Sep 2025 14:19:01 +0530 Subject: [PATCH 12/13] fix poling issue --- package.json | 2 +- src/commander/server.ts | 7 ++++--- 2 files changed, 5 insertions(+), 4 deletions(-) diff --git a/package.json b/package.json index 9947f5bf..c6dc19d0 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "@lambdatest/smartui-cli", - "version": "4.1.28", + "version": "4.1.29", "description": "A command line interface (CLI) to run SmartUI tests on LambdaTest", "files": [ "dist/**/*" diff --git a/src/commander/server.ts b/src/commander/server.ts index 11ac2675..81781622 100644 --- a/src/commander/server.ts +++ b/src/commander/server.ts @@ -50,11 +50,12 @@ command try { await tasks.run(ctx); - startPingPolling(ctx); - if (ctx.options.fetchResults) { + if (ctx.build && ctx.build.id) { + startPingPolling(ctx); + } + if (ctx.options.fetchResults && ctx.build && ctx.build.id) { startPolling(ctx, '', false, '') } - } catch (error) { console.error('Error during server execution:', error); From a516bd256c4a4a2b4eeef5ad8f80f14d372ce737 Mon Sep 17 00:00:00 2001 From: Zeeshan Date: Tue, 9 Sep 2025 14:29:19 +0530 Subject: [PATCH 13/13] Refactor PDF result fetching and enhance error handling for project ID --- src/lib/httpClient.ts | 16 ++++++------ src/lib/utils.ts | 56 ++++++++++++++++++----------------------- src/tasks/uploadPdfs.ts | 3 +++ src/types.ts | 1 + 4 files changed, 35 insertions(+), 41 deletions(-) diff --git a/src/lib/httpClient.ts b/src/lib/httpClient.ts index 3cbd31fd..9a1ae937 100644 --- a/src/lib/httpClient.ts +++ b/src/lib/httpClient.ts @@ -686,22 +686,20 @@ export default class httpClient { } async fetchPdfResults(ctx: Context): Promise { - const params: Record = { - projectToken: this.projectToken - }; + const params: Record = {}; - // Use buildId if available, otherwise use buildName - if (ctx.build.id) { - params.buildId = ctx.build.id; - } else if (ctx.build.name) { - params.buildName = ctx.build.name; + if (ctx.build.projectId) { + params.project_id = ctx.build.projectId; + } else { + throw new Error('Project ID not found to fetch PDF results'); } + params.build_id = ctx.build.id; const auth = Buffer.from(`${this.username}:${this.accessKey}`).toString('base64'); try { const response = await axios.request({ - url: ctx.env.SMARTUI_UPLOAD_URL + '/automation/smart-ui/screenshot/build/status', + url: ctx.env.SMARTUI_UPLOAD_URL + '/smartui/2.0/build/screenshots', method: 'GET', params: params, headers: { diff --git a/src/lib/utils.ts b/src/lib/utils.ts index b9df5919..5ef2e313 100644 --- a/src/lib/utils.ts +++ b/src/lib/utils.ts @@ -526,7 +526,7 @@ export function startPdfPolling(ctx: Context) { } let attempts = 0; - const maxAttempts = 30; // 5 minutes (10 seconds * 30) + const maxAttempts = 60; // 5 minutes (10 seconds * 30) console.log(chalk.yellow('Waiting for results...')); @@ -534,47 +534,46 @@ export function startPdfPolling(ctx: Context) { attempts++; try { - const response = await ctx.client.fetchPdfResults(ctx, ctx.log); + const response = await ctx.client.fetchPdfResults(ctx); - if (response.status === 'success' && response.data && response.data.Screenshots) { + if (response.screenshots) { clearInterval(interval); - const pdfGroups = groupScreenshotsByPdf(response.data.Screenshots); - + const pdfGroups = groupScreenshotsByPdf(response.screenshots); const pdfsWithMismatches = countPdfsWithMismatches(pdfGroups); - const pagesWithMismatches = countPagesWithMismatches(response.data.Screenshots); + const pagesWithMismatches = countPagesWithMismatches(response.screenshots); console.log(chalk.green('\nāœ“ PDF Test Results:')); - console.log(chalk.green(`Build Name: ${response.data.buildName}`)); - console.log(chalk.green(`Project Name: ${response.data.projectName}`)); + console.log(chalk.green(`Build Name: ${response.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.data.Screenshots.length}`)); + console.log(chalk.green(`Total Pages: ${response.screenshots.length}`)); if (pdfsWithMismatches > 0 || pagesWithMismatches > 0) { - console.log(chalk.yellow(`${pdfsWithMismatches} PDFs and ${pagesWithMismatches} Pages in build ${response.data.buildName} have changes present.`)); + console.log(chalk.yellow(`${pdfsWithMismatches} PDFs and ${pagesWithMismatches} Pages in build ${response.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.mismatchPercentage > 0); + const hasMismatch = pages.some(page => page.mismatch_percentage > 0); const statusColor = hasMismatch ? chalk.yellow : chalk.green; console.log(statusColor(`\nšŸ“„ ${pdfName} (${pages.length} pages)`)); pages.forEach(page => { - const pageStatusColor = page.mismatchPercentage > 0 ? chalk.yellow : chalk.green; - console.log(pageStatusColor(` - Page ${getPageNumber(page.screenshotName)}: ${page.status} (Mismatch: ${page.mismatchPercentage}%)`)); + 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 formattedResults = { - status: response.status, + status: 'success', data: { - buildId: response.data.buildId, - buildName: response.data.buildName, - projectName: response.data.projectName, - buildStatus: response.data.buildStatus, + buildId: response.build.id, + buildName: response.build.name, + projectName: response.project.name, + buildStatus: response.build.build_satus, pdfs: formatPdfsForOutput(pdfGroups) } }; @@ -588,12 +587,6 @@ export function startPdfPolling(ctx: Context) { } return; - } else if (response.status === 'error') { - clearInterval(interval); - console.log(chalk.red(`\nError fetching results: ${response.message || 'Unknown error'}`)); - return; - } else { - process.stdout.write(chalk.yellow('.')); } if (attempts >= maxAttempts) { @@ -625,7 +618,7 @@ function groupScreenshotsByPdf(screenshots: any[]): Record { screenshots.forEach(screenshot => { // screenshot name format: "pdf-name.pdf#page-number" - const pdfName = screenshot.screenshotName.split('#')[0]; + const pdfName = screenshot.screenshot_name.split('#')[0]; if (!pdfGroups[pdfName]) { pdfGroups[pdfName] = []; @@ -641,7 +634,7 @@ function countPdfsWithMismatches(pdfGroups: Record): number { let count = 0; Object.values(pdfGroups).forEach(pages => { - if (pages.some(page => page.mismatchPercentage > 0)) { + if (pages.some(page => page.mismatch_percentage > 0)) { count++; } }); @@ -650,7 +643,7 @@ function countPdfsWithMismatches(pdfGroups: Record): number { } function countPagesWithMismatches(screenshots: any[]): number { - return screenshots.filter(screenshot => screenshot.mismatchPercentage > 0).length; + return screenshots.filter(screenshot => screenshot.mismatch_percentage > 0).length; } function formatPdfsForOutput(pdfGroups: Record): any[] { @@ -659,12 +652,11 @@ function formatPdfsForOutput(pdfGroups: Record): any[] { pdfName, pageCount: pages.length, pages: pages.map(page => ({ - pageNumber: getPageNumber(page.screenshotName), - screenshotId: page.screenshotId, - mismatchPercentage: page.mismatchPercentage, - threshold: page.threshold, + pageNumber: getPageNumber(page.screenshot_name), + screenshotId: page.captured_image_id, + mismatchPercentage: page.mismatch_percentage, status: page.status, - screenshotUrl: page.screenshotUrl + screenshotUrl: page.shareable_link })) }; }); diff --git a/src/tasks/uploadPdfs.ts b/src/tasks/uploadPdfs.ts index ddc651d3..c790b985 100644 --- a/src/tasks/uploadPdfs.ts +++ b/src/tasks/uploadPdfs.ts @@ -55,6 +55,9 @@ async function uploadPdfs(ctx: Context, pdfPath: string): Promise { ctx.build.id = response.buildId; ctx.log.debug(`PDF upload successful. Build ID: ${ctx.build.id}`); } + if (response && response.projectId) { + ctx.build.projectId = response.projectId; + } } catch (error : any) { throw new Error(error.message); } diff --git a/src/types.ts b/src/types.ts index 5c6eefe4..0e34e6c0 100644 --- a/src/types.ts +++ b/src/types.ts @@ -181,6 +181,7 @@ export interface Build { baseline: boolean; useKafkaFlow: boolean; hasDiscoveryError: boolean; + projectId?: string; } export interface WebConfig {