Skip to content

Commit

Permalink
Merge pull request #89 from LambdaTest/stage
Browse files Browse the repository at this point in the history
Release v3.0.7
  • Loading branch information
pinanks authored May 16, 2024
2 parents d46ff99 + 4a95045 commit 15a2679
Show file tree
Hide file tree
Showing 14 changed files with 281 additions and 12 deletions.
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": "3.0.6",
"version": "3.0.7",
"description": "A command line interface (CLI) to run SmartUI tests on LambdaTest",
"files": [
"dist/**/*"
Expand Down
7 changes: 5 additions & 2 deletions src/commander/commander.ts
Original file line number Diff line number Diff line change
@@ -1,8 +1,9 @@
import { Command } from 'commander'
import exec from './exec.js'
import { configWeb, configStatic } from './config.js'
import { configWeb, configStatic, configFigma} from './config.js'
import capture from './capture.js'
import { version } from '../../package.json'
import uploadFigma from './uploadFigma.js'

const program = new Command();

Expand All @@ -15,6 +16,8 @@ program
.addCommand(capture)
.addCommand(configWeb)
.addCommand(configStatic)

.addCommand(configFigma)
.addCommand(uploadFigma)


export default program;
13 changes: 12 additions & 1 deletion src/commander/config.ts
Original file line number Diff line number Diff line change
@@ -1,8 +1,9 @@
import { Command } from 'commander'
import { createConfig, createWebStaticConfig } from '../lib/config.js'
import { createConfig, createWebStaticConfig, createFigmaConfig } from '../lib/config.js'

export const configWeb = new Command();
export const configStatic = new Command();
export const configFigma = new Command();

configWeb
.name('config:create')
Expand All @@ -19,3 +20,13 @@ configStatic
.action(async function(filepath, options) {
createWebStaticConfig(filepath);
})

configFigma
.name('config:create-figma')
.description('Create figma designs config file')
.argument('[filepath]', 'Optional config filepath')
.action(async function(filepath, options) {
createFigmaConfig(filepath);
})


65 changes: 65 additions & 0 deletions src/commander/uploadFigma.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,65 @@
import fs from 'fs'
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 getGitInfo from '../tasks/getGitInfo.js'
import createBuild from '../tasks/createBuild.js'
import captureScreenshots from '../tasks/captureScreenshots.js'
import finalizeBuild from '../tasks/finalizeBuild.js'
import { validateFigmaDesignConfig } from '../lib/schemaValidation.js'
import uploadFigmaDesigns from '../tasks/uploadFigmaDesigns.js'

const command = new Command();

command
.name('upload-figma')
.description('Capture screenshots of static sites')
.argument('<file>', 'figma design config file')
.option('--markBaseline', 'Mark the uploaded images as baseline')
.option('--buildName <buildName>' , 'Name of the build')
.action(async function(file, _, command) {
let ctx: Context = ctxInit(command.optsWithGlobals());

if (!fs.existsSync(file)) {
console.log(`Error: Figma Config file ${file} not found.`);
return;
}
try {
ctx.figmaDesignConfig = JSON.parse(fs.readFileSync(file, 'utf8'));
if (!validateFigmaDesignConfig(ctx.figmaDesignConfig)) {
const validationError = validateFigmaDesignConfig.errors?.[0]?.message;
throw new Error(validationError || 'Invalid figma design Config');
}
} catch (error: any) {
console.log(`[smartui] Error: Invalid figma design Config; ${error.message}`);
return;
}

let tasks = new Listr<Context>(
[
auth(ctx),
uploadFigmaDesigns(ctx)
],
{
rendererOptions: {
icon: {
[ListrDefaultRendererLogLevels.OUTPUT]: `→`
},
color: {
[ListrDefaultRendererLogLevels.OUTPUT]: color.gray as LoggerFormat
}
}
}
)

try {
await tasks.run(ctx);
} catch (error) {
console.log('\nRefer docs: https://www.lambdatest.com/support/docs/smart-visual-regression-testing/');
}

})

export default command;
22 changes: 22 additions & 0 deletions src/lib/config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -45,3 +45,25 @@ export function createWebStaticConfig(filepath: string) {
fs.writeFileSync(filepath, JSON.stringify(constants.DEFAULT_WEB_STATIC_CONFIG, null, 2) + '\n');
console.log(`Created web-static config: ${filepath}`);
};

export function createFigmaConfig(filepath: string) {
// default filepath
filepath = filepath || 'designs.json';
let filetype = path.extname(filepath);
if (filetype != '.json') {
console.log('Error: designs config file must have .json extension');
return
}

// verify the file does not already exist
if (fs.existsSync(filepath)) {
console.log(`Error: designs config already exists: ${filepath}`);
console.log(`To create a new file, please specify the file name like: 'smartui config:figma-config designs.json'`);
return
}

// write stringified default config options to the filepath
fs.mkdirSync(path.dirname(filepath), { recursive: true });
fs.writeFileSync(filepath, JSON.stringify(constants.DEFAULT_FIGMA_CONFIG, null, 2) + '\n');
console.log(`Created designs config: ${filepath}`);
};
14 changes: 14 additions & 0 deletions src/lib/constants.ts
Original file line number Diff line number Diff line change
Expand Up @@ -235,5 +235,19 @@ export default {
'iPhone XR': { os: 'ios', viewport: {width: 414, height: 896}},
'iPhone XS': { os: 'ios', viewport: {width: 375, height: 812}},
'iPhone XS Max': { os: 'ios', viewport: {width: 414, height: 896}},
},

FIGMA_API : 'https://api.figma.com/v1/',
DEFAULT_FIGMA_CONFIG: {
"depth": 2,
"figma_config": [
{
"figma_file_token": "token_for_first_figma_file",
"figma_ids": [
"id1",
"id2"
]
}
]
}
}
4 changes: 3 additions & 1 deletion src/lib/ctx.ts
Original file line number Diff line number Diff line change
Expand Up @@ -73,7 +73,9 @@ export default (options: Record<string, string>): Context => {
},
args: {},
options: {
parallel: options.parallel ? true : false
parallel: options.parallel ? true : false,
markBaseline: options.markBaseline ? true : false,
buildName: options.buildName || ''
},
cliVersion: version,
totalSnapshots: -1
Expand Down
10 changes: 8 additions & 2 deletions src/lib/env.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,10 @@ export default (): Env => {
SMARTUI_GIT_INFO_FILEPATH,
HTTP_PROXY,
HTTPS_PROXY,
GITHUB_ACTIONS
GITHUB_ACTIONS,
FIGMA_TOKEN,
LT_USERNAME,
LT_ACCESS_KEY
} = process.env

return {
Expand All @@ -20,6 +23,9 @@ export default (): Env => {
SMARTUI_GIT_INFO_FILEPATH,
HTTP_PROXY,
HTTPS_PROXY,
GITHUB_ACTIONS
GITHUB_ACTIONS,
FIGMA_TOKEN,
LT_USERNAME,
LT_ACCESS_KEY
}
}
23 changes: 22 additions & 1 deletion src/lib/httpClient.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import fs from 'fs';
import FormData from 'form-data';
import axios, { AxiosInstance, AxiosRequestConfig } from 'axios';
import { Env, ProcessedSnapshot, Git, Build } from '../types.js';
import { Env, ProcessedSnapshot, Git, Build, Context } from '../types.js';
import constants from './constants.js';
import type { Logger } from 'winston'
import pkgJSON from './../../package.json'
Expand Down Expand Up @@ -136,4 +136,25 @@ export default class httpClient {
}
}, log)
}

getFigmaFilesAndImages(figmaFileToken: string, figmaToken: String | undefined, queryParams: string, authToken: string, depth: number, markBaseline: boolean, buildName: string, log: Logger) {
const requestBody = {
figma_file_token: figmaFileToken,
figma_token: figmaToken,
query_params: queryParams,
auth: authToken,
depth: depth,
mark_base_line: markBaseline,
build_name: buildName
};

return this.request({
url: "/uploadfigma",
method: "POST",
headers: {
"Content-Type": "application/json",
},
data: JSON.stringify(requestBody)
}, log);
}
}
4 changes: 2 additions & 2 deletions src/lib/processSnapshot.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@ import { scrollToBottomAndBackToTop, getRenderViewports } from "./utils.js"
import { chromium, Locator } from "@playwright/test"
import constants from "./constants.js";

const MAX_RESOURCE_SIZE = 10 * (1024 ** 2); // 10MB
const MAX_RESOURCE_SIZE = 15 * (1024 ** 2); // 15MB
var ALLOWED_RESOURCES = ['document', 'stylesheet', 'image', 'media', 'font', 'other'];
const ALLOWED_STATUSES = [200, 201];
const REQUEST_TIMEOUT = 10000;
Expand Down Expand Up @@ -50,7 +50,7 @@ export default async (snapshot: Snapshot, ctx: Context): Promise<Record<string,
} else if (cache[requestUrl]) {
ctx.log.debug(`Handling request ${requestUrl}\n - skipping already cached resource`);
} else if (body.length > MAX_RESOURCE_SIZE) {
ctx.log.debug(`Handling request ${requestUrl}\n - skipping resource larger than 5MB`);
ctx.log.debug(`Handling request ${requestUrl}\n - skipping resource larger than 15MB`);
} else if (!ALLOWED_STATUSES.includes(response.status())) {
ctx.log.debug(`Handling request ${requestUrl}\n - skipping disallowed status [${response.status()}]`);
} else if (!ALLOWED_RESOURCES.includes(request.resourceType())) {
Expand Down
50 changes: 49 additions & 1 deletion src/lib/schemaValidation.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { Snapshot, WebStaticConfig } from '../types.js'
import { Snapshot, WebStaticConfig, FigmaDesignConfig } from '../types.js'
import Ajv, { JSONSchemaType } from 'ajv'
import addErrors from 'ajv-errors'
import constants from './constants.js'
Expand Down Expand Up @@ -272,6 +272,54 @@ const SnapshotSchema: JSONSchemaType<Snapshot> = {
errorMessage: "Invalid snapshot"
}

const FigmaDesignConfigSchema: JSONSchemaType<FigmaDesignConfig> = {
type: "object",
properties: {
depth: {
type: "integer",
minimum: 2,
errorMessage: "Depth must be an integer and greater than 1"
},
figma_config: {
type: "array",
items: {
type: "object",
properties: {
figma_file_token: {
type: "string",
minLength: 1,
errorMessage: "figma_file_token is mandatory and cannot be empty"
},
figma_ids: {
type: "array",
items: {
type: "string",
minLength: 1,
errorMessage: "Each ID in figma_ids must be a non-empty string"
},
minItems: 1,
uniqueItems: true,
errorMessage: {
type: "figma_ids must be an array of strings",
minItems: "figma_ids cannot be empty",
uniqueItems: "figma_ids must contain unique values"
}
}
},
required: ["figma_file_token"],
additionalProperties: false
},
uniqueItems: true,
errorMessage: {
uniqueItems: "Each entry in the Figma design configuration must be unique"
}
}
},
required: ["figma_config"],
additionalProperties: false
};

export const validateConfig = ajv.compile(ConfigSchema);
export const validateWebStaticConfig = ajv.compile(WebStaticConfigSchema);
export const validateSnapshot = ajv.compile(SnapshotSchema);
export const validateFigmaDesignConfig = ajv.compile(FigmaDesignConfigSchema);
30 changes: 30 additions & 0 deletions src/lib/uploadFigmaDesigns.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
import { Context } from "../types.js";

export default async (ctx: Context): Promise<string> => {
const depth = ctx.figmaDesignConfig.depth;
const figmaConfigs = ctx.figmaDesignConfig.figma_config;
let results = "";
let figmaFileToken = '';
const markBaseline = ctx.options.markBaseline;
const buildName = ctx.options.buildName;

for (const config of figmaConfigs) {

figmaFileToken = config.figma_file_token;
let queryParams = "";
if (config.figma_ids && config.figma_ids.length > 0) {
const fileIds = config.figma_ids.join(",");
queryParams += `?ids=${fileIds}`;
}

const authToken = `Basic ${Buffer.from(`${ctx.env.LT_USERNAME}:${ctx.env.LT_ACCESS_KEY}`).toString("base64")}`

const responseData = await ctx.client.getFigmaFilesAndImages(figmaFileToken, ctx.env.FIGMA_TOKEN, queryParams, authToken, depth, markBaseline, buildName ,ctx.log);

if (responseData.data.message == "success") {
results = responseData.data.message;
}
}

return results;
};
31 changes: 31 additions & 0 deletions src/tasks/uploadFigmaDesigns.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
import { ListrTask, ListrRendererFactory } from 'listr2';
import { Context } from '../types.js'
import { captureScreenshots } from '../lib/screenshot.js'
import chalk from 'chalk';
import { updateLogContext } from '../lib/logger.js'
import uploadFigmaDesigns from '../lib/uploadFigmaDesigns.js';

export default (ctx: Context): ListrTask<Context, ListrRendererFactory, ListrRendererFactory> => {
return {
title: 'Uploading Figma Designs',
task: async (ctx, task): Promise<void> => {
try {
ctx.task = task;
updateLogContext({task: 'upload-figma'});

let results = await uploadFigmaDesigns(ctx);
if (results != 'success') {
throw new Error('Uploading Figma designs failed');
}
task.title = 'Figma designs images uploaded successfully to SmartUI';
ctx.log.debug(`Figma designs processed: ${results}`);
} catch (error: any) {
ctx.log.debug(error);
task.output = chalk.gray(`${error.message}`);
throw new Error('Uploading Figma designs failed');
}
},
rendererOptions: { persistentOutput: true },
exitOnError: false
}
}
Loading

0 comments on commit 15a2679

Please sign in to comment.