-
Notifications
You must be signed in to change notification settings - Fork 21
add pdf support in cli #227
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: stage
Are you sure you want to change the base?
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,62 @@ | ||
import { Command } from 'commander'; | ||
import { Context } from '../types.js'; | ||
import { color, Listr, ListrDefaultRendererLogLevels, LoggerFormat } from 'listr2'; | ||
import auth from '../tasks/auth.js'; | ||
import ctxInit from '../lib/ctx.js'; | ||
import uploadPdfs from '../tasks/uploadPdfs.js'; | ||
import fs from 'fs'; | ||
import { startPdfPolling } from '../lib/utils.js'; | ||
|
||
const command = new Command(); | ||
|
||
command | ||
.name('upload-pdf') | ||
.description('Upload PDFs for visual comparison') | ||
.argument('<directory>', 'Path of the directory containing PDFs') | ||
.option('--fetch-results [filename]', 'Fetch results and optionally specify an output file, e.g., <filename>.json') | ||
.option('--buildName <string>', '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<Context>( | ||
[ | ||
auth(ctx), | ||
uploadPdfs(ctx) | ||
], | ||
{ | ||
rendererOptions: { | ||
icon: { | ||
[ListrDefaultRendererLogLevels.OUTPUT]: `→` | ||
}, | ||
color: { | ||
[ListrDefaultRendererLogLevels.OUTPUT]: color.gray as LoggerFormat | ||
} | ||
} | ||
} | ||
); | ||
|
||
try { | ||
await tasks.run(ctx); | ||
|
||
// Start polling for results if requested | ||
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; |
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -327,4 +327,78 @@ export default class httpClient { | |
params: { buildId } | ||
}, log); | ||
} | ||
|
||
async uploadPdf(formData: FormData, buildName: string, log: Logger): Promise<any> { | ||
// Add required parameters to form data | ||
formData.append('projectToken', this.projectToken); | ||
formData.append('buildName', buildName); | ||
formData.append('projectType', 'pdf'); // Add project type | ||
|
||
// Create a new axios instance for this specific request | ||
const response = await axios.request({ | ||
url: 'https://api.lambdatest.com/pdf/upload', | ||
method: 'POST', | ||
headers: { | ||
...formData.getHeaders(), | ||
'username': this.username, | ||
'accessKey': this.accessKey | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. username and accessKey not required here |
||
}, | ||
data: formData, | ||
}); | ||
|
||
log.debug(`http response: ${JSON.stringify({ | ||
status: response.status, | ||
headers: response.headers, | ||
body: response.data | ||
})}`); | ||
|
||
return response.data; | ||
} | ||
|
||
async fetchPdfResults(buildName: string, buildId: string | undefined, log: Logger): Promise<any> { | ||
// Create params object with required parameters | ||
const params: Record<string, string> = { | ||
projectToken: this.projectToken | ||
}; | ||
|
||
// Use buildId if available, otherwise use buildName | ||
if (buildId) { | ||
params.buildId = buildId; | ||
} else if (buildName) { | ||
params.buildName = buildName; | ||
} | ||
|
||
// Create basic auth token | ||
const auth = Buffer.from(`${this.username}:${this.accessKey}`).toString('base64'); | ||
|
||
try { | ||
// Create a new axios instance for this specific request | ||
const response = await axios.request({ | ||
url: 'https://api.lambdatest.com/automation/smart-ui/screenshot/build/status', | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Use Constants for Host and Routes |
||
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; | ||
} | ||
} | ||
} |
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -328,5 +328,186 @@ export async function startPingPolling(ctx: Context): Promise<void> { | |
}, 10 * 60 * 1000); // 10 minutes interval | ||
} | ||
|
||
export function startPdfPolling(ctx: Context) { | ||
console.log(chalk.yellow('\nFetching PDF test results...')); | ||
|
||
// Use buildId if available, otherwise use buildName | ||
const buildName = ctx.options.buildName || ctx.pdfBuildName; | ||
const buildId = ctx.pdfBuildId; | ||
|
||
if (!buildId && !buildName) { | ||
console.log(chalk.red('Error: Build information not found for fetching results')); | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. use ctx.log.error() here |
||
return; | ||
} | ||
|
||
// Verify authentication credentials | ||
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')); | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. use ctx.log.error() here |
||
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(buildName, buildId, ctx.log); | ||
|
||
if (response.status === 'success' && response.data && response.data.Screenshots) { | ||
clearInterval(interval); | ||
|
||
// Group screenshots by PDF name | ||
const pdfGroups = groupScreenshotsByPdf(response.data.Screenshots); | ||
|
||
// Count PDFs with mismatches | ||
const pdfsWithMismatches = countPdfsWithMismatches(pdfGroups); | ||
const pagesWithMismatches = countPagesWithMismatches(response.data.Screenshots); | ||
|
||
// Display summary in terminal | ||
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.')); | ||
} | ||
|
||
// Display each PDF and its pages | ||
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}%)`)); | ||
}); | ||
}); | ||
|
||
// Format the results for JSON output | ||
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 | ||
const filename = typeof ctx.options.fetchResults === 'string' | ||
? ctx.options.fetchResults | ||
: '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') { | ||
// Handle API error response | ||
clearInterval(interval); | ||
console.log(chalk.red(`\nError fetching results: ${response.message || 'Unknown error'}`)); | ||
return; | ||
} else { | ||
// If we get a response but it's not complete yet | ||
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) { | ||
// Log the error but continue polling unless max attempts reached | ||
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); // Poll every 10 seconds | ||
} | ||
|
||
// Helper function to group screenshots by PDF name | ||
function groupScreenshotsByPdf(screenshots: any[]): Record<string, any[]> { | ||
const pdfGroups: Record<string, any[]> = {}; | ||
|
||
screenshots.forEach(screenshot => { | ||
// Extract PDF name from screenshot name (format: "pdfname.pdf#pagenumber") | ||
const pdfName = screenshot.screenshotName.split('#')[0]; | ||
|
||
if (!pdfGroups[pdfName]) { | ||
pdfGroups[pdfName] = []; | ||
} | ||
|
||
pdfGroups[pdfName].push(screenshot); | ||
}); | ||
|
||
return pdfGroups; | ||
} | ||
|
||
// Helper function to count PDFs with mismatches | ||
function countPdfsWithMismatches(pdfGroups: Record<string, any[]>): number { | ||
let count = 0; | ||
|
||
Object.values(pdfGroups).forEach(pages => { | ||
if (pages.some(page => page.mismatchPercentage > 0)) { | ||
count++; | ||
} | ||
}); | ||
|
||
return count; | ||
} | ||
|
||
// Helper function to count pages with mismatches | ||
function countPagesWithMismatches(screenshots: any[]): number { | ||
return screenshots.filter(screenshot => screenshot.mismatchPercentage > 0).length; | ||
} | ||
|
||
// Helper function to extract page number from screenshot name | ||
function getPageNumber(screenshotName: string): string { | ||
const parts = screenshotName.split('#'); | ||
return parts.length > 1 ? parts[1] : '1'; | ||
} | ||
|
||
// Helper function to format PDFs for JSON output | ||
function formatPdfsForOutput(pdfGroups: Record<string, any[]>): 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 | ||
})) | ||
}; | ||
}); | ||
} | ||
|
||
|
||
|
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Use Constants for Host and Routes, will be useful for stage and prod testing
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
ok