Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
20 commits
Select commit Hold shift + click to select a range
a989744
initial commit for smartui caps in start server
parthlambdatest Sep 3, 2025
137599b
Merge pull request #345 from parthlambdatest/Dot-6176
sushobhit-lt Sep 8, 2025
479c010
Add PDF upload functionality and related commands
lt-zeeshan Sep 8, 2025
1258ca9
Refactor PDF upload logic to use FormData headers and clean up unused…
lt-zeeshan Sep 8, 2025
2eec99f
Enhance PDF upload function to handle single PDF files and improve fi…
lt-zeeshan Sep 8, 2025
2ff20b7
Refactor PDF upload methods to improve error handling and logging
lt-zeeshan Sep 8, 2025
3cf9ad0
Update error handling in PDF upload to exit process on directory not …
lt-zeeshan Sep 8, 2025
0c78566
Changes
lt-zeeshan Sep 8, 2025
3cfd4a6
Fix http client
lt-zeeshan Sep 8, 2025
9f434eb
Merge pull request #349 from lt-zeeshan/DOT-5874
sushobhit-lt Sep 8, 2025
8828eea
fix non git info
sushobhit-lt Sep 8, 2025
e40e1af
Merge pull request #350 from sushobhit-lt/DOT-6200-prod
parthlambdatest Sep 8, 2025
75904fa
remove console log
sushobhit-lt Sep 9, 2025
e60ad86
Merge pull request #351 from sushobhit-lt/DOT-6200-prod
parthlambdatest Sep 9, 2025
93f2431
fix isStartExec
sushobhit-lt Sep 9, 2025
d9d306d
Merge pull request #352 from sushobhit-lt/stage
parthlambdatest Sep 9, 2025
3037ea6
fix poling issue
sushobhit-lt Sep 9, 2025
52ed5f8
Merge pull request #353 from sushobhit-lt/stage
parthlambdatest Sep 9, 2025
a516bd2
Refactor PDF result fetching and enhance error handling for project ID
lt-zeeshan Sep 9, 2025
1a66531
Merge pull request #354 from lt-zeeshan/DOT-5874
parthlambdatest Sep 9, 2025
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
2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
@@ -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/**/*"
Expand Down
2 changes: 2 additions & 0 deletions src/commander/commander.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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();

Expand Down Expand Up @@ -38,6 +39,7 @@ program
.addCommand(uploadWebFigmaCommand)
.addCommand(uploadAppFigmaCommand)
.addCommand(pingTest)
.addCommand(uploadPdf)



Expand Down
15 changes: 8 additions & 7 deletions src/commander/server.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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';

Expand All @@ -30,10 +30,10 @@ command

let tasks = new Listr<Context>(
[
auth(ctx),
authExec(ctx),
startServer(ctx),
getGitInfo(ctx),
createBuild(ctx),
createBuildExec(ctx),

],
{
Expand All @@ -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);
Expand Down
61 changes: 61 additions & 0 deletions src/commander/uploadPdf.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,61 @@
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('<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.`);
process.exit(1);
}

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);

if (ctx.options.fetchResults) {
startPdfPolling(ctx);
}
} catch (error) {
console.log('\nRefer docs: https://www.lambdatest.com/support/docs/smart-visual-regression-testing/');
process.exit(1);
}
});

export default command;
2 changes: 2 additions & 0 deletions src/lib/env.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand All @@ -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,
Expand Down
25 changes: 24 additions & 1 deletion src/lib/git.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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
}
}
Expand Down Expand Up @@ -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 || ''
}
}
76 changes: 76 additions & 0 deletions src/lib/httpClient.ts
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,18 @@ export default class httpClient {
username: string;
accessKey: string;

private handleHttpError(error: any, log: Logger): never {
if (error && 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 || '';
Expand Down Expand Up @@ -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);
}
}
);
Expand Down Expand Up @@ -644,4 +658,66 @@ export default class httpClient {
}
}, ctx.log);
}

async uploadPdf(ctx: Context, form: FormData, buildName?: string): Promise<any> {
form.append('projectToken', this.projectToken);
if (ctx.build.name !== undefined && ctx.build.name !== '') {
form.append('buildName', buildName);
}

try {
const response = await this.axiosInstance.request({
url: ctx.env.SMARTUI_UPLOAD_URL + '/pdf/upload',
method: 'POST',
headers: form.getHeaders(),
data: form,
});

ctx.log.debug(`http response: ${JSON.stringify({
status: response.status,
headers: response.headers,
body: response.data
})}`);

return response.data;
} catch (error: any) {
this.handleHttpError(error, ctx.log);
}
}

async fetchPdfResults(ctx: Context): Promise<any> {
const params: Record<string, string> = {};

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 + '/smartui/2.0/build/screenshots',
method: 'GET',
params: params,
headers: {
'accept': 'application/json',
'Authorization': `Basic ${auth}`
}
});

ctx.log.debug(`http response: ${JSON.stringify({
status: response.status,
headers: response.headers,
body: response.data
})}`);

return response.data;
} catch (error: any) {
this.handleHttpError(error, ctx.log);
}
}
}

47 changes: 34 additions & 13 deletions src/lib/server.ts
Original file line number Diff line number Diff line change
Expand Up @@ -63,7 +63,7 @@ export default async (ctx: Context): Promise<FastifyInstance<Server, IncomingMes
}
} catch (error: any) {
ctx.log.debug(`Failed to fetch capabilities for sessionId ${sessionId}: ${error.message}`);
console.log(`Failed to fetch capabilities for sessionId ${sessionId}: ${error.message}`);
// console.log(`Failed to fetch capabilities for sessionId ${sessionId}: ${error.message}`);
}
}

Expand Down Expand Up @@ -118,23 +118,44 @@ export default async (ctx: Context): Promise<FastifyInstance<Server, IncomingMes
}
}, 1000);
})
await ctx.client.finalizeBuild(ctx.build.id, ctx.totalSnapshots, ctx.log);

for (const [sessionId, capabilities] of ctx.sessionCapabilitiesMap.entries()) {
try {
const buildId = capabilities?.buildId || '';
const projectToken = capabilities?.projectToken || '';
const totalSnapshots = capabilities?.snapshotCount || 0;
const sessionBuildUrl = capabilities?.buildURL || '';
const testId = capabilities?.id || '';

if (buildId && projectToken) {
await ctx.client.finalizeBuildForCapsWithToken(buildId, totalSnapshots, projectToken, ctx.log);
}

if (testId && buildId) {
buildUrls += `TestId ${testId}: ${sessionBuildUrl}\n`;
}
} catch (error: any) {
ctx.log.debug(`Error finalizing build for session ${sessionId}: ${error.message}`);
}
}

if (ctx.build && ctx.build.id) {
await ctx.client.finalizeBuild(ctx.build.id, ctx.totalSnapshots, ctx.log);
let uploadCLILogsToS3 = ctx?.config?.useLambdaInternal || uploadDomToS3ViaEnv;
if (!uploadCLILogsToS3) {
ctx.log.debug(`Log file to be uploaded`)
let resp = await ctx.client.getS3PreSignedURL(ctx);
await ctx.client.uploadLogs(ctx, resp.data.url);
} else {
ctx.log.debug(`Skipping upload of CLI logs as useLambdaInternal is set`)
}
}

await ctx.browser?.close();
if (ctx.server){
ctx.server.close();
}

let uploadCLILogsToS3 = ctx?.config?.useLambdaInternal || uploadDomToS3ViaEnv;
if (!uploadCLILogsToS3) {
ctx.log.debug(`Log file to be uploaded`)
let resp = await ctx.client.getS3PreSignedURL(ctx);
await ctx.client.uploadLogs(ctx, resp.data.url);
} else {
ctx.log.debug(`Skipping upload of CLI logs as useLambdaInternal is set`)
// ctx.log.debug(`Log file to be uploaded via LSRS`)
// let resp = ctx.client.sendCliLogsToLSRS(ctx);
}

if (pingIntervalId !== null) {
clearInterval(pingIntervalId);
ctx.log.debug('Ping polling stopped immediately.');
Expand Down
2 changes: 1 addition & 1 deletion src/lib/snapshotQueue.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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}`);
}

Expand Down
Loading