Skip to content

Commit cb99d64

Browse files
authored
Merge pull request #99 from parthlambdatest/Dot-3170
[Dot-3170] Uploading Images through SmartUI CLI
2 parents 3e8e82f + 0d3bb1e commit cb99d64

File tree

6 files changed

+189
-3
lines changed

6 files changed

+189
-3
lines changed

src/commander/commander.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@ import { Command } from 'commander'
22
import exec from './exec.js'
33
import { configWeb, configStatic, configFigma} from './config.js'
44
import capture from './capture.js'
5+
import upload from './upload.js'
56
import { version } from '../../package.json'
67
import uploadFigma from './uploadFigma.js'
78

@@ -16,6 +17,7 @@ program
1617
.addCommand(capture)
1718
.addCommand(configWeb)
1819
.addCommand(configStatic)
20+
.addCommand(upload)
1921
.addCommand(configFigma)
2022
.addCommand(uploadFigma)
2123

src/commander/upload.ts

Lines changed: 64 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,64 @@
1+
import fs from 'fs';
2+
import { Command } from 'commander';
3+
import { Context } from '../types.js';
4+
import { color, Listr, ListrDefaultRendererLogLevels } from 'listr2';
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 uploadScreenshots from '../tasks/uploadScreenshots.js';
10+
import finalizeBuild from '../tasks/finalizeBuild.js';
11+
12+
const command = new Command();
13+
14+
command
15+
.name('upload')
16+
.description('Upload screenshots from given directory')
17+
.argument('<directory>', 'Path of the directory')
18+
.option('-R, --ignoreResolutions', 'Ignore resolution')
19+
.option('-F, --files <extensions>', 'Comma-separated list of allowed file extensions', val => {
20+
return val.split(',').map(ext => ext.trim().toLowerCase());
21+
})
22+
.option('-E, --removeExtensions', 'Strips file extensions from snapshot names')
23+
.option('-i, --ignoreDir <patterns>', 'Comma-separated list of directories to ignore', val => {
24+
return val.split(',').map(pattern => pattern.trim());
25+
})
26+
.action(async function(directory, _, command) {
27+
let ctx: Context = ctxInit(command.optsWithGlobals());
28+
29+
if (!fs.existsSync(directory)) {
30+
console.log(`Error: The provided directory ${directory} not found.`);
31+
return;
32+
}
33+
34+
ctx.uploadFilePath = directory;
35+
36+
let tasks = new Listr<Context>(
37+
[
38+
auth(ctx),
39+
getGitInfo(ctx),
40+
createBuild(ctx),
41+
uploadScreenshots(ctx),
42+
finalizeBuild(ctx)
43+
],
44+
{
45+
rendererOptions: {
46+
icon: {
47+
[ListrDefaultRendererLogLevels.OUTPUT]: `→`
48+
},
49+
color: {
50+
[ListrDefaultRendererLogLevels.OUTPUT]: color.gray
51+
}
52+
}
53+
}
54+
);
55+
56+
try {
57+
await tasks.run(ctx);
58+
} catch (error) {
59+
console.log('\nRefer docs: https://www.lambdatest.com/support/docs/smart-visual-regression-testing/');
60+
}
61+
62+
});
63+
64+
export default command;

src/lib/ctx.ts

Lines changed: 14 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -13,7 +13,10 @@ export default (options: Record<string, string>): Context => {
1313
let mobileConfig: MobileConfig;
1414
let config = constants.DEFAULT_CONFIG;
1515
let port: number;
16-
16+
let resolutionOff: boolean;
17+
let extensionFiles: string;
18+
let ignoreStripExtension: Array<string>;
19+
let ignoreFilePattern: Array<string>;
1720
try {
1821
if (options.config) {
1922
config = JSON.parse(fs.readFileSync(options.config, 'utf-8'));
@@ -33,6 +36,10 @@ export default (options: Record<string, string>): Context => {
3336
if (isNaN(port) || port < 1 || port > 65535) {
3437
throw new Error('Invalid port number. Port number must be an integer between 1 and 65535.');
3538
}
39+
resolutionOff = options.ignoreResolutions || false;
40+
extensionFiles = options.files || ['png', 'jpeg', 'jpg'];
41+
ignoreStripExtension = options.ignoreStripExtensions || false
42+
ignoreFilePattern = options.ignorePattern || []
3643
} catch (error: any) {
3744
console.log(`[smartui] Error: ${error.message}`);
3845
process.exit();
@@ -62,6 +69,7 @@ export default (options: Record<string, string>): Context => {
6269
enableJavaScript: config.enableJavaScript || false,
6370
allowedHostnames: config.allowedHostnames || []
6471
},
72+
uploadFilePath: '',
6573
webStaticConfig: [],
6674
git: {
6775
branch: '',
@@ -81,7 +89,11 @@ export default (options: Record<string, string>): Context => {
8189
parallel: options.parallel ? true : false,
8290
markBaseline: options.markBaseline ? true : false,
8391
buildName: options.buildName || '',
84-
port: port
92+
port: port,
93+
ignoreResolutions: resolutionOff,
94+
fileExtension: extensionFiles,
95+
stripExtension: ignoreStripExtension,
96+
ignorePattern: ignoreFilePattern,
8597
},
8698
cliVersion: version,
8799
totalSnapshots: -1

src/lib/screenshot.ts

Lines changed: 77 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,5 @@
1+
import fs from 'fs';
2+
import path from 'path';
13
import { Browser, BrowserContext, Page } from "@playwright/test"
24
import { Context } from "../types.js"
35
import * as utils from "./utils.js"
@@ -133,4 +135,78 @@ export async function captureScreenshots(ctx: Context): Promise<Record<string,a
133135
utils.delDir('screenshots');
134136

135137
return { capturedScreenshots, output };
136-
}
138+
}
139+
140+
function getImageDimensions(filePath: string): { width: number, height: number } | null {
141+
const buffer = fs.readFileSync(filePath);
142+
let width, height;
143+
144+
if (buffer.toString('hex', 0, 2) === 'ffd8') {
145+
// JPEG
146+
let offset = 2;
147+
while (offset < buffer.length) {
148+
const marker = buffer.toString('hex', offset, offset + 2);
149+
offset += 2;
150+
const length = buffer.readUInt16BE(offset);
151+
if (marker === 'ffc0' || marker === 'ffc2') {
152+
height = buffer.readUInt16BE(offset + 3);
153+
width = buffer.readUInt16BE(offset + 5);
154+
return { width, height };
155+
}
156+
offset += length;
157+
}
158+
} else if (buffer.toString('hex', 1, 4) === '504e47') {
159+
// PNG
160+
width = buffer.readUInt32BE(16);
161+
height = buffer.readUInt32BE(20);
162+
return { width, height };
163+
}
164+
165+
return null;
166+
}
167+
168+
export async function uploadScreenshots(ctx: Context): Promise<void> {
169+
const allowedExtensions = ctx.options.fileExtension.map(ext => `.${ext.trim().toLowerCase()}`);
170+
171+
async function processDirectory(directory: string, relativePath: string = ''): Promise<void> {
172+
const files = fs.readdirSync(directory);
173+
174+
for (let file of files) {
175+
const filePath = path.join(directory, file);
176+
const stat = fs.statSync(filePath);
177+
const relativeFilePath = path.join(relativePath, file);
178+
179+
if (stat.isDirectory() && ctx.options.ignorePattern.includes(relativeFilePath)) {
180+
continue; // Skip this path
181+
}
182+
183+
if (stat.isDirectory()) {
184+
await processDirectory(filePath, relativeFilePath); // Recursively process subdirectory
185+
} else {
186+
let fileExtension = path.extname(file).toLowerCase();
187+
if (allowedExtensions.includes(fileExtension)) {
188+
let ssId = relativeFilePath;
189+
if (ctx.options.stripExtension) {
190+
ssId = path.join(relativePath, path.basename(file, fileExtension));
191+
}
192+
193+
let viewport = 'default';
194+
195+
if (!ctx.options.ignoreResolutions) {
196+
const dimensions = getImageDimensions(filePath);
197+
if (!dimensions) {
198+
throw new Error(`Unable to determine dimensions for image: ${filePath}`);
199+
}
200+
const width = dimensions.width;
201+
const height = dimensions.height;
202+
viewport = `${width}x${height}`;
203+
}
204+
205+
await ctx.client.uploadScreenshot(ctx.build, filePath, ssId, 'default', viewport, ctx.log);
206+
}
207+
}
208+
}
209+
}
210+
211+
await processDirectory(ctx.uploadFilePath);
212+
}

src/tasks/uploadScreenshots.ts

Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,27 @@
1+
import { ListrTask, ListrRendererFactory } from 'listr2';
2+
import { Context } from '../types.js';
3+
import { uploadScreenshots } from '../lib/screenshot.js';
4+
import chalk from 'chalk';
5+
import { updateLogContext } from '../lib/logger.js';
6+
7+
export default (ctx: Context): ListrTask<Context, ListrRendererFactory, ListrRendererFactory> => {
8+
return {
9+
title: 'Uploading screenshots',
10+
task: async (ctx, task): Promise<void> => {
11+
try {
12+
ctx.task = task;
13+
updateLogContext({ task: 'upload' });
14+
15+
await uploadScreenshots(ctx);
16+
17+
task.title = 'Screenshots uploaded successfully';
18+
} catch (error: any) {
19+
ctx.log.debug(error);
20+
task.output = chalk.gray(`${error.message}`);
21+
throw new Error('Uploading screenshots failed');
22+
}
23+
},
24+
rendererOptions: { persistentOutput: true },
25+
exitOnError: false
26+
};
27+
};

src/types.ts

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,7 @@ export interface Context {
2222
enableJavaScript: boolean;
2323
allowedHostnames: Array<string>;
2424
};
25+
uploadFilePath: string;
2526
webStaticConfig: WebStaticConfig;
2627
build: Build;
2728
git: Git;
@@ -33,6 +34,10 @@ export interface Context {
3334
markBaseline?: boolean,
3435
buildName?: string,
3536
port?: number,
37+
ignoreResolutions?: boolean,
38+
fileExtension?: Array<string>,
39+
stripExtension?: boolean,
40+
ignorePattern?: Array<string>,
3641
}
3742
cliVersion: string;
3843
totalSnapshots: number;

0 commit comments

Comments
 (0)