Skip to content

Commit 5ae8489

Browse files
authored
Merge pull request #217 from LambdaTest/stage
[Dot-4568] Release PR for version 4.0.21
2 parents 176583d + dd47a83 commit 5ae8489

14 files changed

+302
-15
lines changed

package.json

+1-1
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
{
22
"name": "@lambdatest/smartui-cli",
3-
"version": "4.0.20",
3+
"version": "4.0.21",
44
"description": "A command line interface (CLI) to run SmartUI tests on LambdaTest",
55
"files": [
66
"dist/**/*"

src/commander/commander.ts

+7-1
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,9 @@ import capture from './capture.js'
55
import upload from './upload.js'
66
import { version } from '../../package.json'
77
import { uploadFigma, uploadWebFigmaCommand } from './uploadFigma.js'
8+
import startServer from './server.js';
9+
import stopServer from './stopServer.js'
10+
import ping from './ping.js'
811

912
const program = new Command();
1013

@@ -18,11 +21,14 @@ program
1821
.addCommand(configWeb)
1922
.addCommand(configStatic)
2023
.addCommand(upload)
24+
.addCommand(startServer)
25+
.addCommand(stopServer)
26+
.addCommand(ping)
2127
.addCommand(configFigma)
2228
.addCommand(uploadFigma)
2329
.addCommand(configWebFigma)
2430
.addCommand(uploadWebFigmaCommand)
2531

26-
32+
2733

2834
export default program;

src/commander/ping.ts

+41
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,41 @@
1+
import { Command } from 'commander';
2+
import axios from 'axios';
3+
import chalk from 'chalk'
4+
5+
function getSmartUIServerAddress() {
6+
const serverAddress = process.env.SMARTUI_SERVER_ADDRESS || 'http://localhost:49152';
7+
return serverAddress;
8+
}
9+
10+
const command = new Command();
11+
12+
command
13+
.name('exec:ping')
14+
.description('Ping the SmartUI server to check if it is running')
15+
.action(async function(this: Command) {
16+
try {
17+
console.log(chalk.yellow("Pinging server..."));
18+
const serverAddress = getSmartUIServerAddress();
19+
console.log(chalk.yellow(`Pinging server at ${serverAddress} from terminal...`));
20+
21+
// Send GET request to the /ping endpoint
22+
const response = await axios.get(`${serverAddress}/ping`, { timeout: 15000 });
23+
24+
// Log the response from the server
25+
if (response.status === 200) {
26+
console.log(chalk.green('SmartUI Server is running'));
27+
console.log(chalk.green(`Response: ${JSON.stringify(response.data)}`)); // Log response data if needed
28+
} else {
29+
console.log(chalk.red('Failed to reach the server'));
30+
}
31+
} catch (error: any) {
32+
// Handle any errors during the HTTP request
33+
if (error.code === 'ECONNABORTED') {
34+
console.error(chalk.red('Error: SmartUI server did not respond in 15 seconds'));
35+
} else {
36+
console.error(chalk.red('SmartUI server is not running'));
37+
}
38+
}
39+
});
40+
41+
export default command;

src/commander/server.ts

+64
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,64 @@
1+
import { Command } from 'commander';
2+
import { Context } from '../types.js';
3+
import { color, Listr, ListrDefaultRendererLogLevels } from 'listr2';
4+
import startServer from '../tasks/startServer.js';
5+
import auth from '../tasks/auth.js';
6+
import ctxInit from '../lib/ctx.js';
7+
import getGitInfo from '../tasks/getGitInfo.js';
8+
import createBuild from '../tasks/createBuild.js';
9+
import snapshotQueue from '../lib/snapshotQueue.js';
10+
import { startPolling, startPingPolling } from '../lib/utils.js';
11+
12+
const command = new Command();
13+
14+
command
15+
.name('exec:start')
16+
.description('Start SmartUI server')
17+
.option('-P, --port <number>', 'Port number for the server')
18+
.option('--fetch-results [filename]', 'Fetch results and optionally specify an output file, e.g., <filename>.json')
19+
.option('--buildName <string>', 'Specify the build name')
20+
.action(async function(this: Command) {
21+
const options = command.optsWithGlobals();
22+
if (options.buildName === '') {
23+
console.log(`Error: The '--buildName' option cannot be an empty string.`);
24+
process.exit(1);
25+
}
26+
let ctx: Context = ctxInit(command.optsWithGlobals());
27+
ctx.snapshotQueue = new snapshotQueue(ctx);
28+
ctx.totalSnapshots = 0
29+
ctx.isStartExec = true
30+
31+
let tasks = new Listr<Context>(
32+
[
33+
auth(ctx),
34+
startServer(ctx),
35+
getGitInfo(ctx),
36+
createBuild(ctx),
37+
38+
],
39+
{
40+
rendererOptions: {
41+
icon: {
42+
[ListrDefaultRendererLogLevels.OUTPUT]: `→`
43+
},
44+
color: {
45+
[ListrDefaultRendererLogLevels.OUTPUT]: color.gray
46+
}
47+
}
48+
}
49+
);
50+
51+
try {
52+
await tasks.run(ctx);
53+
startPingPolling(ctx);
54+
if (ctx.options.fetchResults) {
55+
startPolling(ctx);
56+
}
57+
58+
59+
} catch (error) {
60+
console.error('Error during server execution:', error);
61+
}
62+
});
63+
64+
export default command;

src/commander/stopServer.ts

+44
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,44 @@
1+
import { Command } from 'commander';
2+
import axios from 'axios'; // Import axios for HTTP requests
3+
import chalk from 'chalk'
4+
5+
const command = new Command();
6+
7+
function getSmartUIServerAddress() {
8+
const serverAddress = process.env.SMARTUI_SERVER_ADDRESS || 'http://localhost:49152';
9+
return serverAddress;
10+
}
11+
12+
command
13+
.name('exec:stop')
14+
.description('Stop the SmartUI server')
15+
.action(async function(this: Command) {
16+
try {
17+
const serverAddress = getSmartUIServerAddress();
18+
console.log(chalk.yellow(`Stopping server at ${serverAddress} from terminal...`));
19+
20+
// Send POST request to the /stop endpoint with the correct headers
21+
const response = await axios.post(`${serverAddress}/stop`, { timeout: 15000 }, {
22+
headers: {
23+
'Content-Type': 'application/json' // Ensure the correct Content-Type header
24+
}
25+
});
26+
27+
// Log the response from the server
28+
if (response.status === 200) {
29+
console.log(chalk.green('Server stopped successfully'));
30+
console.log(chalk.green(`Response: ${JSON.stringify(response.data)}`)); // Log response data if needed
31+
} else {
32+
console.log(chalk.red('Failed to stop server'));
33+
}
34+
} catch (error: any) {
35+
// Handle any errors during the HTTP request
36+
if (error.code === 'ECONNABORTED') {
37+
console.error(chalk.red('Error: SmartUI server did not respond in 15 seconds'));
38+
} else {
39+
console.error(chalk.red('Error while stopping server'));
40+
}
41+
}
42+
});
43+
44+
export default command;

src/lib/ctx.ts

+2-1
Original file line numberDiff line numberDiff line change
@@ -126,6 +126,7 @@ export default (options: Record<string, string>): Context => {
126126
fetchResultsFileName: fetchResultsFileObj,
127127
},
128128
cliVersion: version,
129-
totalSnapshots: -1
129+
totalSnapshots: -1,
130+
isStartExec: false
130131
}
131132
}

src/lib/env.ts

+5-1
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,8 @@ export default (): Env => {
1818
BASELINE_BRANCH,
1919
CURRENT_BRANCH,
2020
PROJECT_NAME,
21+
SMARTUI_API_PROXY,
22+
SMARTUI_API_SKIP_CERTIFICATES
2123
} = process.env
2224

2325
return {
@@ -36,6 +38,8 @@ export default (): Env => {
3638
CURRENT_BRANCH,
3739
LT_SDK_DEBUG: LT_SDK_DEBUG === 'true',
3840
SMARTUI_DO_NOT_USE_CAPTURED_COOKIES: SMARTUI_DO_NOT_USE_CAPTURED_COOKIES === 'true',
39-
PROJECT_NAME
41+
PROJECT_NAME,
42+
SMARTUI_API_PROXY,
43+
SMARTUI_API_SKIP_CERTIFICATES: SMARTUI_API_SKIP_CERTIFICATES === 'true'
4044
}
4145
}

src/lib/httpClient.ts

+42-6
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@ import { Env, Snapshot, ProcessedSnapshot, Git, Build, Context } from '../types.
55
import constants from './constants.js';
66
import type { Logger } from 'winston'
77
import pkgJSON from './../../package.json'
8+
import https from 'https';
89

910
export default class httpClient {
1011
axiosInstance: AxiosInstance;
@@ -13,15 +14,38 @@ export default class httpClient {
1314
username: string;
1415
accessKey: string;
1516

16-
constructor({ SMARTUI_CLIENT_API_URL, PROJECT_TOKEN, PROJECT_NAME, LT_USERNAME, LT_ACCESS_KEY }: Env) {
17+
constructor({ SMARTUI_CLIENT_API_URL, PROJECT_TOKEN, PROJECT_NAME, LT_USERNAME, LT_ACCESS_KEY, SMARTUI_API_PROXY, SMARTUI_API_SKIP_CERTIFICATES }: Env) {
1718
this.projectToken = PROJECT_TOKEN || '';
1819
this.projectName = PROJECT_NAME || '';
1920
this.username = LT_USERNAME || '';
2021
this.accessKey = LT_ACCESS_KEY || '';
2122

22-
this.axiosInstance = axios.create({
23+
let proxyUrl = null;
24+
try {
25+
// Handle URL with or without protocol
26+
const urlStr = SMARTUI_API_PROXY?.startsWith('http') ?
27+
SMARTUI_API_PROXY : `http://${SMARTUI_API_PROXY}`;
28+
proxyUrl = SMARTUI_API_PROXY ? new URL(urlStr) : null;
29+
} catch (error) {
30+
console.error('Invalid proxy URL:', error);
31+
}
32+
const axiosConfig: any = {
2333
baseURL: SMARTUI_CLIENT_API_URL,
24-
});
34+
proxy: proxyUrl ? {
35+
host: proxyUrl.hostname,
36+
port: proxyUrl.port ? Number(proxyUrl.port) : 80
37+
} : false
38+
};
39+
40+
if (SMARTUI_API_SKIP_CERTIFICATES) {
41+
axiosConfig.httpsAgent = new https.Agent({
42+
rejectUnauthorized: false
43+
});
44+
}
45+
46+
this.axiosInstance = axios.create(axiosConfig);
47+
48+
2549
this.axiosInstance.interceptors.request.use((config) => {
2650
config.headers['projectToken'] = this.projectToken;
2751
config.headers['projectName'] = this.projectName;
@@ -84,14 +108,15 @@ export default class httpClient {
84108
}
85109
}
86110

87-
createBuild(git: Git, config: any, log: Logger, buildName: string) {
111+
createBuild(git: Git, config: any, log: Logger, buildName: string, isStartExec: boolean) {
88112
return this.request({
89113
url: '/build',
90114
method: 'POST',
91115
data: {
92116
git,
93117
config,
94-
buildName
118+
buildName,
119+
isStartExec
95120
}
96121
}, log)
97122
}
@@ -102,7 +127,18 @@ export default class httpClient {
102127
method: 'GET',
103128
params: { buildId, baseline }
104129
}, log);
105-
}
130+
}
131+
132+
ping(buildId: string, log: Logger) {
133+
return this.request({
134+
url: '/build/ping',
135+
method: 'POST',
136+
data: {
137+
buildId: buildId
138+
}
139+
}, log);
140+
}
141+
106142

107143
finalizeBuild(buildId: string, totalSnapshots: number, log: Logger) {
108144
let params: Record<string, string | number> = {buildId};

src/lib/server.ts

+49
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@ import fastify, { FastifyInstance, RouteShorthandOptions } from 'fastify';
44
import { readFileSync } from 'fs'
55
import { Context } from '../types.js'
66
import { validateSnapshot } from './schemaValidation.js'
7+
import { pingIntervalId } from './utils.js';
78

89
export default async (ctx: Context): Promise<FastifyInstance<Server, IncomingMessage, ServerResponse>> => {
910

@@ -48,6 +49,54 @@ export default async (ctx: Context): Promise<FastifyInstance<Server, IncomingMes
4849
return reply.code(replyCode).send(replyBody);
4950
});
5051

52+
server.post('/stop', opts, async (_, reply) => {
53+
let replyCode: number;
54+
let replyBody: Record<string, any>;
55+
try {
56+
if(ctx.config.delayedUpload){
57+
ctx.log.debug("started after processing because of delayedUpload")
58+
ctx.snapshotQueue?.startProcessingfunc()
59+
}
60+
await new Promise((resolve) => {
61+
const intervalId = setInterval(() => {
62+
if (ctx.snapshotQueue?.isEmpty() && !ctx.snapshotQueue?.isProcessing()) {
63+
clearInterval(intervalId);
64+
resolve();
65+
}
66+
}, 1000);
67+
})
68+
await ctx.client.finalizeBuild(ctx.build.id, ctx.totalSnapshots, ctx.log);
69+
await ctx.browser?.close();
70+
if (ctx.server){
71+
ctx.server.close();
72+
}
73+
let resp = await ctx.client.getS3PreSignedURL(ctx);
74+
await ctx.client.uploadLogs(ctx, resp.data.url);
75+
76+
if (pingIntervalId !== null) {
77+
clearInterval(pingIntervalId);
78+
ctx.log.debug('Ping polling stopped immediately.');
79+
}
80+
replyCode = 200;
81+
replyBody = { data: { message: "success", type: "DELETE" } };
82+
} catch (error: any) {
83+
ctx.log.debug(error);
84+
ctx.log.debug(`stop endpoint failed; ${error}`);
85+
replyCode = 500;
86+
replyBody = { error: { message: error.message } };
87+
}
88+
89+
// Step 5: Return the response
90+
return reply.code(replyCode).send(replyBody);
91+
});
92+
93+
// Add /ping route to check server status
94+
server.get('/ping', opts, (_, reply) => {
95+
reply.code(200).send({ status: 'Server is running', version: ctx.cliVersion });
96+
});
97+
98+
99+
51100
await server.listen({ port: ctx.options.port });
52101
// store server's address for SDK
53102
let { port } = server.addresses()[0];

src/lib/snapshotQueue.ts

+4
Original file line numberDiff line numberDiff line change
@@ -270,6 +270,10 @@ export default class Queue {
270270
this.processingSnapshot = snapshot?.name;
271271
let drop = false;
272272

273+
if (this.ctx.isStartExec) {
274+
this.ctx.log.info(`Processing Snapshot: ${snapshot?.name}`);
275+
}
276+
273277
if (!this.ctx.config.delayedUpload && snapshot && snapshot.name && this.snapshotNames.includes(snapshot.name)) {
274278
drop = true;
275279
this.ctx.log.info(`Skipping duplicate SmartUI snapshot '${snapshot.name}'. To capture duplicate screenshots, please set the 'delayedUpload' configuration as true in your config file.`);

0 commit comments

Comments
 (0)