Skip to content

Commit 892eca0

Browse files
Merge pull request #200 from sushobhit-lt/DOT-4308
[DOT-4308] figma web implementation
2 parents 08385c2 + 5a46e23 commit 892eca0

12 files changed

+430
-19
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.15",
3+
"version": "4.0.16",
44
"description": "A command line interface (CLI) to run SmartUI tests on LambdaTest",
55
"files": [
66
"dist/**/*"

src/commander/commander.ts

+5-2
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,10 @@
11
import { Command } from 'commander'
22
import exec from './exec.js'
3-
import { configWeb, configStatic, configFigma} from './config.js'
3+
import { configWeb, configStatic, configFigma, configWebFigma} from './config.js'
44
import capture from './capture.js'
55
import upload from './upload.js'
66
import { version } from '../../package.json'
7-
import uploadFigma from './uploadFigma.js'
7+
import { uploadFigma, uploadWebFigmaCommand } from './uploadFigma.js'
88

99
const program = new Command();
1010

@@ -20,6 +20,9 @@ program
2020
.addCommand(upload)
2121
.addCommand(configFigma)
2222
.addCommand(uploadFigma)
23+
.addCommand(configWebFigma)
24+
.addCommand(uploadWebFigmaCommand)
25+
2326

2427

2528
export default program;

src/commander/config.ts

+10-1
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,11 @@
11
import { Command } from 'commander'
2-
import { createConfig, createWebStaticConfig, createFigmaConfig } from '../lib/config.js'
2+
import { createConfig, createWebStaticConfig, createFigmaConfig, createWebFigmaConfig } from '../lib/config.js'
33

44
export const configWeb = new Command();
55
export const configStatic = new Command();
66
export const configFigma = new Command();
7+
export const configWebFigma = new Command();
8+
79

810
configWeb
911
.name('config:create')
@@ -29,4 +31,11 @@ configFigma
2931
createFigmaConfig(filepath);
3032
})
3133

34+
configWebFigma
35+
.name('config:create-figma-web')
36+
.description('Create figma config file with browsers')
37+
.argument('[filepath]', 'Optional config filepath')
38+
.action(async function(filepath, options) {
39+
createWebFigmaConfig(filepath);
40+
})
3241

src/commander/uploadFigma.ts

+75-9
Original file line numberDiff line numberDiff line change
@@ -1,25 +1,28 @@
11
import fs from 'fs'
22
import { Command } from 'commander'
33
import { Context } from '../types.js'
4-
import { color , Listr, ListrDefaultRendererLogLevels, LoggerFormat } from 'listr2'
4+
import { color, Listr, ListrDefaultRendererLogLevels, LoggerFormat } from 'listr2'
55
import auth from '../tasks/auth.js'
66
import ctxInit from '../lib/ctx.js'
77
import getGitInfo from '../tasks/getGitInfo.js'
8-
import createBuild from '../tasks/createBuild.js'
9-
import captureScreenshots from '../tasks/captureScreenshots.js'
108
import finalizeBuild from '../tasks/finalizeBuild.js'
11-
import { validateFigmaDesignConfig } from '../lib/schemaValidation.js'
9+
import { validateFigmaDesignConfig, validateWebFigmaConfig } from '../lib/schemaValidation.js'
1210
import uploadFigmaDesigns from '../tasks/uploadFigmaDesigns.js'
11+
import uploadWebFigma from '../tasks/uploadWebFigma.js'
12+
import { verifyFigmaWebConfig } from '../lib/config.js'
13+
import chalk from 'chalk';
1314

14-
const command = new Command();
1515

16-
command
16+
const uploadFigma = new Command();
17+
const uploadWebFigmaCommand = new Command();
18+
19+
uploadFigma
1720
.name('upload-figma')
1821
.description('Capture screenshots of static sites')
1922
.argument('<file>', 'figma design config file')
2023
.option('--markBaseline', 'Mark the uploaded images as baseline')
21-
.option('--buildName <buildName>' , 'Name of the build')
22-
.action(async function(file, _, command) {
24+
.option('--buildName <buildName>', 'Name of the build')
25+
.action(async function (file, _, command) {
2326
let ctx: Context = ctxInit(command.optsWithGlobals());
2427

2528
if (!fs.existsSync(file)) {
@@ -62,4 +65,67 @@ command
6265

6366
})
6467

65-
export default command;
68+
uploadWebFigmaCommand
69+
.name('upload-figma-web')
70+
.description('Capture screenshots of static sites')
71+
.argument('<file>', 'figma config config file')
72+
.option('--markBaseline', 'Mark the uploaded images as baseline')
73+
.option('--buildName <buildName>', 'Name of the build')
74+
.action(async function (file, _, command) {
75+
let ctx: Context = ctxInit(command.optsWithGlobals());
76+
77+
if (!fs.existsSync(file)) {
78+
console.log(`Error: figma-web config file ${file} not found.`);
79+
return;
80+
}
81+
try {
82+
ctx.config = JSON.parse(fs.readFileSync(file, 'utf8'));
83+
ctx.log.info(JSON.stringify(ctx.config));
84+
if (!validateWebFigmaConfig(ctx.config)) {
85+
ctx.log.debug(JSON.stringify(validateWebFigmaConfig.errors, null, 2));
86+
// Iterate and add warning for "additionalProperties"
87+
validateWebFigmaConfig.errors?.forEach(error => {
88+
if (error.keyword === "additionalProperties") {
89+
ctx.log.warn(`Additional property "${error.params.additionalProperty}" is not allowed.`)
90+
} else {
91+
const validationError = error.message;
92+
throw new Error(validationError || 'Invalid figma-web config found in file : ' + file);
93+
}
94+
});
95+
}
96+
97+
//Validate the figma config
98+
verifyFigmaWebConfig(ctx);
99+
} catch (error: any) {
100+
ctx.log.error(chalk.red(`Invalid figma-web config; ${error.message}`));
101+
return;
102+
}
103+
104+
let tasks = new Listr<Context>(
105+
[
106+
auth(ctx),
107+
getGitInfo(ctx),
108+
uploadWebFigma(ctx),
109+
finalizeBuild(ctx)
110+
],
111+
{
112+
rendererOptions: {
113+
icon: {
114+
[ListrDefaultRendererLogLevels.OUTPUT]: `→`
115+
},
116+
color: {
117+
[ListrDefaultRendererLogLevels.OUTPUT]: color.gray as LoggerFormat
118+
}
119+
}
120+
}
121+
)
122+
123+
try {
124+
await tasks.run(ctx);
125+
} catch (error) {
126+
console.log('\nRefer docs: https://www.lambdatest.com/support/docs/smart-visual-regression-testing/');
127+
}
128+
129+
})
130+
131+
export { uploadFigma, uploadWebFigmaCommand }

src/lib/config.ts

+57
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
import path from 'path'
22
import fs from 'fs'
33
import constants from './constants.js';
4+
import { Context } from "../types.js";
45

56
export function createConfig(filepath: string) {
67
// default filepath
@@ -67,3 +68,59 @@ export function createFigmaConfig(filepath: string) {
6768
fs.writeFileSync(filepath, JSON.stringify(constants.DEFAULT_FIGMA_CONFIG, null, 2) + '\n');
6869
console.log(`Created designs config: ${filepath}`);
6970
};
71+
72+
export function createWebFigmaConfig(filepath: string) {
73+
// default filepath
74+
filepath = filepath || '.smartui.json';
75+
let filetype = path.extname(filepath);
76+
if (filetype != '.json') {
77+
console.log('Error: figma config file must have .json extension');
78+
return
79+
}
80+
81+
// verify the file does not already exist
82+
if (fs.existsSync(filepath)) {
83+
console.log(`Error: figma config already exists: ${filepath}`);
84+
console.log(`To create a new file, please specify the file name like: 'smartui config:create-figma-web <fileName>.json'`);
85+
return
86+
}
87+
88+
// write stringified default config options to the filepath
89+
fs.mkdirSync(path.dirname(filepath), { recursive: true });
90+
fs.writeFileSync(filepath, JSON.stringify(constants.WEB_FIGMA_CONFIG, null, 2) + '\n');
91+
console.log(`Created figma web config: ${filepath}`);
92+
};
93+
94+
export function verifyFigmaWebConfig(ctx: Context) {
95+
if (ctx.env.FIGMA_TOKEN == "") {
96+
throw new Error("Missing FIGMA_TOKEN in Environment Variables");
97+
}
98+
if (ctx.env.LT_USERNAME == "") {
99+
throw new Error("Missing LT_USERNAME in Environment Variables");
100+
}
101+
if (ctx.env.LT_ACCESS_KEY == "") {
102+
throw new Error("Missing LT_ACCESS_KEY in Environment Variables");
103+
}
104+
let figma = ctx.config && ctx.config?.figma || {};
105+
const screenshots = [];
106+
for (let c of figma?.configs) {
107+
if (c.screenshot_names && c.screenshot_names.length > 0 && c.figma_ids && c.figma_ids.length != c.screenshot_names.length) {
108+
throw new Error("Mismatch in Figma Ids and Screenshot Names in figma config");
109+
}
110+
if (isValidArray(c.screenshot_names)) {
111+
for (const name of c.screenshot_names) {
112+
screenshots.push(name);
113+
}
114+
}
115+
}
116+
117+
if (new Set(screenshots).size !== screenshots.length) {
118+
throw new Error("Found duplicate screenshot names in figma config");
119+
}
120+
121+
return true;
122+
};
123+
124+
function isValidArray(input) {
125+
return Array.isArray(input) && input.length > 0;
126+
}

src/lib/constants.ts

+25
Original file line numberDiff line numberDiff line change
@@ -370,5 +370,30 @@ export default {
370370
]
371371
}
372372
]
373+
},
374+
WEB_FIGMA_CONFIG: {
375+
web: {
376+
browsers: [
377+
'chrome',
378+
'firefox',
379+
'safari',
380+
'edge'
381+
]
382+
},
383+
figma: {
384+
"depth": 2,
385+
"configs": [
386+
{
387+
"figma_file_token": "<token>",
388+
"figma_ids": ["id-1", "id-2"],
389+
"screenshot_names": ["homepage", "about"]
390+
},
391+
{
392+
"figma_file_token": "<token>",
393+
"figma_ids": ["id-3", "id-4"],
394+
"screenshot_names": ["xyz", "abc"]
395+
},
396+
]
397+
}
373398
}
374399
}

src/lib/fetchFigma.ts

+50
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,50 @@
1+
import { Context } from "../types.js";
2+
3+
export default async (ctx: Context): Promise<any> => {
4+
const buildId = ctx.build.id;
5+
ctx.log.debug(`Fetching figma results for buildId ${buildId}`);
6+
const startTime = Date.now(); // Record the start time
7+
try {
8+
const results = await callFetchWebFigmaRecursive(startTime, buildId, ctx);
9+
return results;
10+
} catch (error) {
11+
ctx.log.error(`Failed to fetch figma results: ${error}`);
12+
return { message: "Failed to fetch figma results" };
13+
}
14+
};
15+
16+
17+
// Recursive function with 5-second interval and 5-minute timeout
18+
async function callFetchWebFigmaRecursive(
19+
startTime: number,
20+
buildId: any,
21+
ctx: Context
22+
): Promise<any> {
23+
const currentTime = Date.now();
24+
const elapsedTime = (currentTime - startTime) / 1000; // Elapsed time in seconds
25+
26+
// Check if total elapsed time exceeds 3 minutes
27+
if (elapsedTime >= 180) {
28+
ctx.log.error("Stopping execution after 5 minutes.");
29+
throw new Error("Timeout: Fetching figma results took more than 5 minutes.");
30+
}
31+
32+
try {
33+
const response = await ctx.client.fetchWebFigma(buildId, ctx.log);
34+
ctx.log.debug("responseData : " + JSON.stringify(response));
35+
36+
const message = response?.data?.message || "";
37+
if (message === "") {
38+
ctx.log.debug("No results yet. Retrying after 5 seconds...");
39+
await new Promise((resolve) => setTimeout(resolve, 5000)); // Wait for 5 seconds
40+
return await callFetchWebFigmaRecursive(startTime, buildId, ctx);
41+
} else {
42+
return response?.data?.message;
43+
}
44+
} catch (error) {
45+
ctx.log.error("Error in fetchWebFigma:", error);
46+
ctx.log.debug("Retrying after 5 seconds...");
47+
await new Promise((resolve) => setTimeout(resolve, 5000)); // Wait for 5 seconds
48+
return await callFetchWebFigmaRecursive(startTime, buildId, ctx);
49+
}
50+
}

src/lib/httpClient.ts

+26-1
Original file line numberDiff line numberDiff line change
@@ -33,6 +33,9 @@ export default class httpClient {
3333

3434
async request(config: AxiosRequestConfig, log: Logger): Promise<Record<string, any>> {
3535
log.debug(`http request: ${config.method} ${config.url}`);
36+
if(config && config.data) {
37+
log.debug(config.data);
38+
}
3639

3740
return this.axiosInstance.request(config)
3841
.then(resp => {
@@ -50,7 +53,7 @@ export default class httpClient {
5053
headers: error.response.headers,
5154
body: error.response.data
5255
})}`);
53-
throw new Error(error.response.data.error?.message || error.response.data.message);
56+
throw new Error(error.response.data.error?.message || error.response.data.message || error.response.data);
5457
}
5558
if (error.request) {
5659
log.debug(`http request failed: ${error.toJSON()}`);
@@ -223,4 +226,26 @@ export default class httpClient {
223226
maxContentLength: Infinity, // prevent axios from limiting the content size
224227
}, ctx.log)
225228
}
229+
230+
processWebFigma(requestBody: any, log: Logger) {
231+
return this.request({
232+
url: "figma-web/upload",
233+
method: "POST",
234+
headers: {
235+
"Content-Type": "application/json",
236+
},
237+
data: JSON.stringify(requestBody)
238+
}, log);
239+
}
240+
241+
fetchWebFigma(buildId: any, log: Logger) {
242+
return this.request({
243+
url: "figma-web/fetch",
244+
method: "GET",
245+
headers: {
246+
"Content-Type": "application/json",
247+
},
248+
params: { buildId }
249+
}, log);
250+
}
226251
}

0 commit comments

Comments
 (0)