Skip to content

Commit 25acae6

Browse files
authored
Merge pull request #242 from LambdaTest/stage
Release PR 4.1.3-beta
2 parents c038b96 + c86f92b commit 25acae6

19 files changed

+456
-100
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.1.2",
3+
"version": "4.1.3-beta.0",
44
"description": "A command line interface (CLI) to run SmartUI tests on LambdaTest",
55
"files": [
66
"dist/**/*"

src/commander/exec.ts

+4-4
Original file line numberDiff line numberDiff line change
@@ -3,10 +3,10 @@ import which from 'which'
33
import { Context } from '../types.js'
44
import { color, Listr, ListrDefaultRendererLogLevels } from 'listr2'
55
import startServer from '../tasks/startServer.js'
6-
import auth from '../tasks/auth.js'
6+
import authExec from '../tasks/authExec.js'
77
import ctxInit from '../lib/ctx.js'
88
import getGitInfo from '../tasks/getGitInfo.js'
9-
import createBuild from '../tasks/createBuild.js'
9+
import createBuildExec from '../tasks/createBuildExec.js'
1010
import exec from '../tasks/exec.js'
1111
import processSnapshots from '../tasks/processSnapshot.js'
1212
import finalizeBuild from '../tasks/finalizeBuild.js'
@@ -39,10 +39,10 @@ command
3939

4040
let tasks = new Listr<Context>(
4141
[
42-
auth(ctx),
42+
authExec(ctx),
4343
startServer(ctx),
4444
getGitInfo(ctx),
45-
createBuild(ctx),
45+
createBuildExec(ctx),
4646
exec(ctx),
4747
processSnapshots(ctx),
4848
finalizeBuild(ctx)

src/commander/server.ts

+1-1
Original file line numberDiff line numberDiff line change
@@ -52,7 +52,7 @@ command
5252
await tasks.run(ctx);
5353
startPingPolling(ctx);
5454
if (ctx.options.fetchResults) {
55-
startPolling(ctx);
55+
startPolling(ctx, '', false, '')
5656
}
5757

5858

src/lib/ctx.ts

+6-2
Original file line numberDiff line numberDiff line change
@@ -53,7 +53,7 @@ export default (options: Record<string, string>): Context => {
5353
process.exit(1);
5454
}
5555
fetchResultObj = true
56-
fetchResultsFileObj = options.fetchResults === true ? 'results.json' : options.fetchResults;
56+
fetchResultsFileObj = options.fetchResults === true ? '' : options.fetchResults;
5757
} else {
5858
fetchResultObj = false
5959
fetchResultsFileObj = ''
@@ -98,6 +98,7 @@ export default (options: Record<string, string>): Context => {
9898
delayedUpload: config.delayedUpload ?? false,
9999
useGlobalCache: config.useGlobalCache ?? false,
100100
ignoreHTTPSErrors: config.ignoreHTTPSErrors ?? false,
101+
skipBuildCreation: config.skipBuildCreation ?? false
101102
},
102103
uploadFilePath: '',
103104
webStaticConfig: [],
@@ -131,6 +132,9 @@ export default (options: Record<string, string>): Context => {
131132
cliVersion: version,
132133
totalSnapshots: -1,
133134
isStartExec: false,
134-
isSnapshotCaptured: false
135+
isSnapshotCaptured: false,
136+
sessionCapabilitiesMap: new Map<string, any[]>(),
137+
buildToSnapshotCountMap: new Map<string, number>(),
138+
fetchResultsForBuild: new Array<string>
135139
}
136140
}

src/lib/httpClient.ts

+115-3
Original file line numberDiff line numberDiff line change
@@ -47,7 +47,9 @@ export default class httpClient {
4747

4848

4949
this.axiosInstance.interceptors.request.use((config) => {
50-
config.headers['projectToken'] = this.projectToken;
50+
if (!config.headers['projectToken']) {
51+
config.headers['projectToken'] = this.projectToken;
52+
}
5153
config.headers['projectName'] = this.projectName;
5254
config.headers['username'] = this.username;
5355
config.headers['accessKey'] = this.accessKey;
@@ -155,11 +157,12 @@ export default class httpClient {
155157
}, log)
156158
}
157159

158-
getScreenshotData(buildId: string, baseline: boolean, log: Logger) {
160+
getScreenshotData(buildId: string, baseline: boolean, log: Logger, projectToken: string) {
159161
return this.request({
160162
url: '/screenshot',
161163
method: 'GET',
162-
params: { buildId, baseline }
164+
params: { buildId, baseline },
165+
headers: {projectToken: projectToken}
163166
}, log);
164167
}
165168

@@ -174,6 +177,26 @@ export default class httpClient {
174177
}
175178

176179

180+
getSmartUICapabilities(sessionId: string, config: any, git: any, log: Logger) {
181+
return this.request({
182+
url: '/sessions/capabilities',
183+
method: 'GET',
184+
params: {
185+
sessionId: sessionId,
186+
},
187+
data: {
188+
git,
189+
config
190+
},
191+
headers: {
192+
projectToken: '',
193+
projectName: '',
194+
username: '',
195+
accessKey: ''
196+
},
197+
}, log);
198+
}
199+
177200
finalizeBuild(buildId: string, totalSnapshots: number, log: Logger) {
178201
let params: Record<string, string | number> = { buildId };
179202
if (totalSnapshots > -1) params.totalSnapshots = totalSnapshots;
@@ -185,7 +208,22 @@ export default class httpClient {
185208
}, log)
186209
}
187210

211+
finalizeBuildForCapsWithToken(buildId: string, totalSnapshots: number, projectToken: string, log: Logger) {
212+
let params: Record<string, string | number> = { buildId };
213+
if (totalSnapshots > -1) params.totalSnapshots = totalSnapshots;
214+
return this.request({
215+
url: '/build',
216+
method: 'DELETE',
217+
params: params,
218+
headers: {
219+
projectToken: projectToken, // Use projectToken dynamically
220+
},
221+
}, log);
222+
}
223+
224+
188225
uploadSnapshot(ctx: Context, snapshot: ProcessedSnapshot) {
226+
// Use capsBuildId if provided, otherwise fallback to ctx.build.id
189227
return this.request({
190228
url: `/builds/${ctx.build.id}/snapshot`,
191229
method: 'POST',
@@ -218,6 +256,50 @@ export default class httpClient {
218256
}, ctx.log)
219257
}
220258

259+
processSnapshotCaps(ctx: Context, snapshot: ProcessedSnapshot, snapshotUuid: string, capsBuildId: string, capsProjectToken: string) {
260+
return this.request({
261+
url: `/build/${capsBuildId}/snapshot`,
262+
method: 'POST',
263+
headers: {
264+
'Content-Type': 'application/json',
265+
projectToken: capsProjectToken !== '' ? capsProjectToken : this.projectToken
266+
},
267+
data: {
268+
name: snapshot.name,
269+
url: snapshot.url,
270+
snapshotUuid: snapshotUuid,
271+
test: {
272+
type: ctx.testType,
273+
source: 'cli'
274+
},
275+
async: false,
276+
}
277+
}, ctx.log)
278+
}
279+
280+
uploadSnapshotForCaps(ctx: Context, snapshot: ProcessedSnapshot, capsBuildId: string, capsProjectToken: string) {
281+
// Use capsBuildId if provided, otherwise fallback to ctx.build.id
282+
const buildId = capsBuildId !== '' ? capsBuildId : ctx.build.id;
283+
284+
return this.request({
285+
url: `/builds/${buildId}/snapshot`,
286+
method: 'POST',
287+
headers: {
288+
'Content-Type': 'application/json',
289+
projectToken: capsProjectToken !== '' ? capsProjectToken : this.projectToken // Use capsProjectToken dynamically
290+
},
291+
data: {
292+
snapshot,
293+
test: {
294+
type: ctx.testType,
295+
source: 'cli'
296+
}
297+
}
298+
}, ctx.log);
299+
}
300+
301+
302+
221303
uploadScreenshot(
222304
{ id: buildId, name: buildName, baseline }: Build,
223305
ssPath: string, ssName: string, browserName: string, viewport: string, log: Logger
@@ -311,6 +393,22 @@ export default class httpClient {
311393
}, ctx.log)
312394
}
313395

396+
getS3PresignedURLForSnapshotUploadCaps(ctx: Context, snapshotName: string, snapshotUuid: string, capsBuildId: string, capsProjectToken: string) {
397+
return this.request({
398+
url: `/snapshotuploadurl`,
399+
method: 'POST',
400+
headers: {
401+
'Content-Type': 'application/json',
402+
projectToken: capsProjectToken !== '' ? capsProjectToken : this.projectToken
403+
},
404+
data: {
405+
buildId: capsBuildId,
406+
snapshotName: snapshotName,
407+
snapshotUuid: snapshotUuid
408+
}
409+
}, ctx.log)
410+
}
411+
314412
uploadLogs(ctx: Context, uploadURL: string) {
315413
const fileStream = fs.createReadStream(constants.LOG_FILE_PATH);
316414
const { size } = fs.statSync(constants.LOG_FILE_PATH);
@@ -341,6 +439,20 @@ export default class httpClient {
341439
}, ctx.log)
342440
}
343441

442+
uploadSnapshotToS3Caps(ctx: Context, uploadURL: string, snapshot: Snapshot, capsProjectToken: string) {
443+
return this.request({
444+
url: uploadURL,
445+
method: 'PUT',
446+
headers: {
447+
'Content-Type': 'application/json',
448+
projectToken: capsProjectToken !== '' ? capsProjectToken : this.projectToken
449+
},
450+
data: snapshot,
451+
maxBodyLength: Infinity, // prevent axios from limiting the body size
452+
maxContentLength: Infinity, // prevent axios from limiting the content size
453+
}, ctx.log)
454+
}
455+
344456
processWebFigma(requestBody: any, log: Logger) {
345457
return this.request({
346458
url: "figma-web/upload",

src/lib/processSnapshot.ts

+12-1
Original file line numberDiff line numberDiff line change
@@ -191,6 +191,17 @@ export default async function processSnapshot(snapshot: Snapshot, ctx: Context):
191191
processedOptions.loadDomContent = true;
192192
}
193193

194+
if (options.sessionId) {
195+
const sessionId = options.sessionId;
196+
processedOptions.sessionId = sessionId
197+
if (ctx.sessionCapabilitiesMap && ctx.sessionCapabilitiesMap.has(sessionId)) {
198+
const sessionCapabilities = ctx.sessionCapabilitiesMap.get(sessionId);
199+
if (sessionCapabilities && sessionCapabilities.id) {
200+
processedOptions.testId = sessionCapabilities.id;
201+
}
202+
}
203+
}
204+
194205
if (options.web && Object.keys(options.web).length) {
195206
processedOptions.web = {};
196207

@@ -267,7 +278,7 @@ export default async function processSnapshot(snapshot: Snapshot, ctx: Context):
267278
}
268279
}
269280

270-
// process for every viewport
281+
// process for every viewport
271282
let navigated: boolean = false;
272283
let previousDeviceType: string | null = null;
273284

src/lib/schemaValidation.ts

+8
Original file line numberDiff line numberDiff line change
@@ -169,6 +169,10 @@ const ConfigSchema = {
169169
type: "boolean",
170170
errorMessage: "Invalid config; useGlobalCache must be true/false"
171171
},
172+
skipBuildCreation: {
173+
type: "boolean",
174+
errorMessage: "Invalid config; skipBuildCreation must be true/false"
175+
},
172176
},
173177
anyOf: [
174178
{ required: ["web"] },
@@ -390,6 +394,10 @@ const SnapshotSchema: JSONSchemaType<Snapshot> = {
390394
loadDomContent: {
391395
type: "boolean",
392396
errorMessage: "Invalid snapshot options; loadDomContent must be a boolean"
397+
},
398+
sessionId: {
399+
type: "string",
400+
errorMessage: "Invalid snapshot options; sessionId must be a string"
393401
}
394402
},
395403
additionalProperties: false

src/lib/server.ts

+31-1
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,11 @@
11
import { Server, IncomingMessage, ServerResponse } from 'http';
22
import path from 'path';
33
import fastify, { FastifyInstance, RouteShorthandOptions } from 'fastify';
4-
import { readFileSync } from 'fs'
4+
import { readFileSync, truncate } from 'fs'
55
import { Context } from '../types.js'
66
import { validateSnapshot } from './schemaValidation.js'
77
import { pingIntervalId } from './utils.js';
8+
import { startPolling } from './utils.js';
89

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

@@ -36,6 +37,35 @@ export default async (ctx: Context): Promise<FastifyInstance<Server, IncomingMes
3637
try {
3738
let { snapshot, testType } = request.body;
3839
if (!validateSnapshot(snapshot)) throw new Error(validateSnapshot.errors[0].message);
40+
41+
// Fetch sessionId from snapshot options if present
42+
const sessionId = snapshot?.options?.sessionId;
43+
let capsBuildId = ''
44+
45+
if (sessionId) {
46+
// Check if sessionId exists in the map
47+
if (ctx.sessionCapabilitiesMap?.has(sessionId)) {
48+
// Use cached capabilities if available
49+
const cachedCapabilities = ctx.sessionCapabilitiesMap.get(sessionId);
50+
capsBuildId = cachedCapabilities?.buildId || ''
51+
} else {
52+
// If not cached, fetch from API and cache it
53+
try {
54+
let fetchedCapabilitiesResp = await ctx.client.getSmartUICapabilities(sessionId, ctx.config, ctx.git, ctx.log);
55+
capsBuildId = fetchedCapabilitiesResp?.buildId || ''
56+
ctx.log.debug(`fetch caps for sessionId: ${sessionId} are ${JSON.stringify(fetchedCapabilitiesResp)}`)
57+
if (capsBuildId) {
58+
ctx.sessionCapabilitiesMap.set(sessionId, fetchedCapabilitiesResp);
59+
} else if (fetchedCapabilitiesResp && fetchedCapabilitiesResp?.sessionId) {
60+
ctx.sessionCapabilitiesMap.set(sessionId, fetchedCapabilitiesResp);
61+
}
62+
} catch (error: any) {
63+
ctx.log.debug(`Failed to fetch capabilities for sessionId ${sessionId}: ${error.message}`);
64+
console.log(`Failed to fetch capabilities for sessionId ${sessionId}: ${error.message}`);
65+
}
66+
}
67+
}
68+
3969
ctx.testType = testType;
4070
ctx.snapshotQueue?.enqueue(snapshot);
4171
ctx.isSnapshotCaptured = true;

0 commit comments

Comments
 (0)