diff --git a/package.json b/package.json index e9ac594..157ab94 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "@lambdatest/smartui-cli", - "version": "3.0.11", + "version": "3.0.12", "description": "A command line interface (CLI) to run SmartUI tests on LambdaTest", "files": [ "dist/**/*" diff --git a/src/commander/exec.ts b/src/commander/exec.ts index d494abc..feb0531 100644 --- a/src/commander/exec.ts +++ b/src/commander/exec.ts @@ -5,10 +5,12 @@ import { color, Listr, ListrDefaultRendererLogLevels } from 'listr2' import startServer from '../tasks/startServer.js' import auth from '../tasks/auth.js' import ctxInit from '../lib/ctx.js' -import getGitInfo from '../tasks/getGitInfo.js'; +import getGitInfo from '../tasks/getGitInfo.js' import createBuild from '../tasks/createBuild.js' import exec from '../tasks/exec.js' +import processSnapshots from '../tasks/processSnapshot.js' import finalizeBuild from '../tasks/finalizeBuild.js' +import snapshotQueue from '../lib/processSnapshot.js' const command = new Command(); @@ -25,6 +27,7 @@ command return } ctx.args.execCommand = execCommand + ctx.snapshotQueue = new snapshotQueue(ctx) ctx.totalSnapshots = 0 let tasks = new Listr( @@ -34,6 +37,7 @@ command getGitInfo(ctx), createBuild(ctx), exec(ctx), + processSnapshots(ctx), finalizeBuild(ctx) ], { @@ -53,8 +57,10 @@ command } catch (error) { ctx.log.info('\nRefer docs: https://www.lambdatest.com/support/docs/smart-visual-regression-testing/'); } finally { - await ctx.server?.close(); await ctx.browser?.close(); + ctx.log.debug(`Closed browser`); + await ctx.server?.close(); + ctx.log.debug(`Closed server`); } }) diff --git a/src/lib/httpClient.ts b/src/lib/httpClient.ts index 436291b..50812e5 100644 --- a/src/lib/httpClient.ts +++ b/src/lib/httpClient.ts @@ -75,19 +75,19 @@ export default class httpClient { }, log) } - uploadSnapshot(buildId: string, snapshot: ProcessedSnapshot, testType: string, log: Logger) { + uploadSnapshot(ctx: Context, snapshot: ProcessedSnapshot) { return this.request({ - url: `/builds/${buildId}/snapshot`, + url: `/builds/${ctx.build.id}/snapshot`, method: 'POST', headers: { 'Content-Type': 'application/json' }, data: { snapshot, test: { - type: testType, + type: ctx.testType, source: 'cli' } } - }, log) + }, ctx.log) } uploadScreenshot( diff --git a/src/lib/processSnapshot.ts b/src/lib/processSnapshot.ts index 589dda4..80e14cb 100644 --- a/src/lib/processSnapshot.ts +++ b/src/lib/processSnapshot.ts @@ -9,7 +9,73 @@ const ALLOWED_STATUSES = [200, 201]; const REQUEST_TIMEOUT = 10000; const MIN_VIEWPORT_HEIGHT = 1080; -export default async (snapshot: Snapshot, ctx: Context): Promise> => { +export default class Queue { + private snapshots: Array = []; + private processedSnapshots: Array> = []; + private processing: boolean = false; + private processingSnapshot: string = ''; + private ctx: Context; + + constructor(ctx: Context) { + this.ctx = ctx; + } + + enqueue(item: Snapshot): void { + this.snapshots.push(item); + if (!this.processing) { + this.processing = true; + this.processNext(); + } + } + + private async processNext(): Promise { + if (!this.isEmpty()) { + const snapshot = this.snapshots.shift(); + try { + this.processingSnapshot = snapshot?.name; + let { processedSnapshot, warnings } = await processSnapshot(snapshot, this.ctx); + await this.ctx.client.uploadSnapshot(this.ctx, processedSnapshot); + this.ctx.totalSnapshots++; + this.processedSnapshots.push({name: snapshot.name, warnings}); + } catch (error: any) { + this.ctx.log.debug(`snapshot failed; ${error}`); + this.processedSnapshots.push({name: snapshot.name, error: error.message}); + } + // Close open browser contexts and pages + if (this.ctx.browser) { + for (let context of this.ctx.browser.contexts()) { + for (let page of context.pages()) { + await page.close(); + this.ctx.log.debug(`Closed browser page for snapshot ${snapshot.name}`); + } + await context.close(); + this.ctx.log.debug(`Closed browser context for snapshot ${snapshot.name}`); + } + } + this.processNext(); + } else { + this.processing = false; + } + } + + isProcessing(): boolean { + return this.processing; + } + + getProcessingSnapshot(): string { + return this.processingSnapshot; + } + + getProcessedSnapshots(): Array> { + return this.processedSnapshots; + } + + isEmpty(): boolean { + return this.snapshots.length ? false : true; + } +} + +async function processSnapshot(snapshot: Snapshot, ctx: Context): Promise> { ctx.log.debug(`Processing snapshot ${snapshot.name}`); let launchOptions: Record = { headless: true } diff --git a/src/lib/server.ts b/src/lib/server.ts index 8e71c45..29429af 100644 --- a/src/lib/server.ts +++ b/src/lib/server.ts @@ -23,7 +23,7 @@ export default async (ctx: Context): Promise { let replyCode: number; let replyBody: Record; @@ -31,28 +31,15 @@ export default async (ctx: Context): Promise => { + return { + title: `Processing snapshots`, + task: async (ctx, task): Promise => { + + try { + // wait for snapshot queue to be empty + await new Promise((resolve) => { + let output: string = ''; + const intervalId = setInterval(() => { + if (ctx.snapshotQueue?.isEmpty() && !ctx.snapshotQueue?.isProcessing()) { + clearInterval(intervalId); + resolve(); + } else { + task.title = `Processing snapshot ${ctx.snapshotQueue?.getProcessingSnapshot()}` + } + }, 500); + }) + + let output = ''; + for (let snapshot of ctx.snapshotQueue?.getProcessedSnapshots()) { + if (snapshot.error) output += `${chalk.red('\u{2717}')} ${chalk.gray(`${snapshot.name}\n[error] ${snapshot.error}`)}\n`; + else output += `${chalk.green('\u{2713}')} ${chalk.gray(snapshot.name)}\n${snapshot.warnings.length ? chalk.gray(`[warning] ${snapshot.warnings.join('\n[warning] ')}\n`) : ''}`; + } + task.output = output; + task.title = 'Processed snapshots' + } catch (error: any) { + ctx.log.debug(error); + task.output = chalk.gray(error.message); + throw new Error('Processing of snapshots failed'); + } + }, + rendererOptions: { persistentOutput: true } + } +} diff --git a/src/types.ts b/src/types.ts index 3e95961..e9719ed 100644 --- a/src/types.ts +++ b/src/types.ts @@ -4,6 +4,7 @@ import httpClient from './lib/httpClient.js' import type { Logger } from 'winston' import { ListrTaskWrapper, ListrRenderer } from "listr2"; import { Browser } from '@playwright/test'; +import snapshotQueue from './lib/processSnapshot.js'; export interface Context { env: Env; @@ -12,6 +13,7 @@ export interface Context { server?: FastifyInstance; client: httpClient; browser?: Browser; + snapshotQueue?: snapshotQueue; config: { web?: WebConfig; mobile?: MobileConfig, @@ -34,7 +36,8 @@ export interface Context { } cliVersion: string; totalSnapshots: number; - figmaDesignConfig: FigmaDesignConfig; + figmaDesignConfig?: FigmaDesignConfig; + testType?: string; } export interface Env {