diff --git a/package.json b/package.json index c6dc19d..1c2d24a 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "@lambdatest/smartui-cli", - "version": "4.1.29", + "version": "4.1.30", "description": "A command line interface (CLI) to run SmartUI tests on LambdaTest", "files": [ "dist/**/*" diff --git a/src/lib/processSnapshot.ts b/src/lib/processSnapshot.ts index 82d6ea4..7128fcc 100644 --- a/src/lib/processSnapshot.ts +++ b/src/lib/processSnapshot.ts @@ -1,5 +1,5 @@ import { Snapshot, Context, DiscoveryErrors } from "../types.js"; -import { scrollToBottomAndBackToTop, getRenderViewports, getRenderViewportsForOptions } from "./utils.js" +import { scrollToBottomAndBackToTop, getRenderViewports, getRenderViewportsForOptions, validateCoordinates } from "./utils.js" import { chromium, Locator } from "@playwright/test" import constants from "./constants.js"; import { updateLogContext } from '../lib/logger.js' @@ -126,6 +126,9 @@ export async function prepareSnapshot(snapshot: Snapshot, ctx: Context): Promise case 'cssSelector': selectors.push(...value); break; + case 'coordinates': + selectors.push(...value.map(e => `coordinates=${e}`)); + break; } } } @@ -500,6 +503,9 @@ export default async function processSnapshot(snapshot: Snapshot, ctx: Context): case 'cssSelector': selectors.push(...value); break; + case 'coordinates': + selectors.push(...value.map(e => `coordinates=${e}`)); + break; } } } @@ -663,6 +669,37 @@ export default async function processSnapshot(snapshot: Snapshot, ctx: Context): if (!Array.isArray(processedOptions[ignoreOrSelectBoxes][viewportString])) processedOptions[ignoreOrSelectBoxes][viewportString] = [] for (const selector of selectors) { + if (selector.startsWith('coordinates=')) { + const coordString = selector.replace('coordinates=', ''); + let pageHeight = height; + if (viewport.height) { + pageHeight = viewport.height; + } + const validation = validateCoordinates( + coordString, + pageHeight, + viewport.width, + snapshot.name + ); + + if (!validation.valid) { + optionWarnings.add(validation.error!); + continue; + } + + if(renderViewports.length > 1){ + optionWarnings.add(`for snapshot ${snapshot.name} viewport ${viewportString}, coordinates may not be accurate for multiple viewports`); + } + + + const coordinateElement = { + type: 'coordinates', + ...validation.coords + }; + locators.push(coordinateElement as any); + continue; + } + let l = await page.locator(selector).all() if (l.length === 0) { optionWarnings.add(`for snapshot ${snapshot.name} viewport ${viewportString}, no element found for selector ${selector}`); @@ -670,7 +707,20 @@ export default async function processSnapshot(snapshot: Snapshot, ctx: Context): } locators.push(...l); } + for (const locator of locators) { + if (locator && typeof locator === 'object' && locator.hasOwnProperty('type') && (locator as any).type === 'coordinates') { + const coordLocator = locator as any; + const { top, bottom, left, right } = coordLocator; + processedOptions[ignoreOrSelectBoxes][viewportString].push({ + left: left, + top: top, + right: right, + bottom: bottom + }); + continue; + } + let bb = await locator.boundingBox(); if (bb) { // Calculate top and bottom from the bounding box properties @@ -738,3 +788,5 @@ export default async function processSnapshot(snapshot: Snapshot, ctx: Context): discoveryErrors: discoveryErrors } } + + diff --git a/src/lib/schemaValidation.ts b/src/lib/schemaValidation.ts index c6b1fdf..cdfc42d 100644 --- a/src/lib/schemaValidation.ts +++ b/src/lib/schemaValidation.ts @@ -395,6 +395,12 @@ const SnapshotSchema: JSONSchemaType = { uniqueItems: true, errorMessage: "Invalid snapshot options; ignoreDOM xpath array must have unique and non-empty items" }, + coordinates: { + type: "array", + items: { type: "string", minLength: 1 }, + uniqueItems: true, + errorMessage: "Invalid snapshot options; ignoreDOM coordinates array must have unique and non-empty items" + } } }, selectDOM: { @@ -424,6 +430,12 @@ const SnapshotSchema: JSONSchemaType = { uniqueItems: true, errorMessage: "Invalid snapshot options; selectDOM xpath array must have unique and non-empty items" }, + coordinates: { + type: "array", + items: { type: "string", minLength: 1 }, + uniqueItems: true, + errorMessage: "Invalid snapshot options; selectDOM coordinates array must have unique and non-empty items" + } } }, ignoreType: { diff --git a/src/lib/utils.ts b/src/lib/utils.ts index 5ef2e31..43f24cb 100644 --- a/src/lib/utils.ts +++ b/src/lib/utils.ts @@ -665,4 +665,70 @@ function formatPdfsForOutput(pdfGroups: Record): any[] { function getPageNumber(screenshotName: string): string { const parts = screenshotName.split('#'); return parts.length > 1 ? parts[1] : '1'; +} + +export function validateCoordinates( + coordString: string, + pageHeight: number, + pageWidth: number, + snapshotName: string +): { valid: boolean, error?: string, coords?: { top: number, bottom: number, left: number, right: number } } { + + const coords = coordString.split(',').map(Number); + + if (coords.length !== 4) { + return { + valid: false, + error: `for snapshot ${snapshotName}, invalid coordinates format: ${coordString}. Expected: top,bottom,left,right` + }; + } + + const [top, bottom, left, right] = coords; + + if (coords.some(isNaN)) { + return { + valid: false, + error: `for snapshot ${snapshotName}, invalid coordinate values: ${coordString}. All values must be numbers` + }; + } + + if (top < 0 || left < 0 || bottom < 0 || right < 0) { + return { + valid: false, + error: `for snapshot ${snapshotName}, invalid coordinate bounds: ${coordString}. top,left,bottom,right must be >= 0` + }; + } + + if (top >= bottom) { + return { + valid: false, + error: `for snapshot ${snapshotName}, invalid coordinate bounds: ${coordString}. top must be < bottom` + }; + } + + if (left >= right) { + return { + valid: false, + error: `for snapshot ${snapshotName}, invalid coordinate bounds: ${coordString}. left must be < right` + }; + } + + if (bottom > pageHeight) { + return { + valid: false, + error: `for snapshot ${snapshotName}, coordinates exceed viewport bounds: ${coordString}. bottom (${bottom}) exceeds viewport height (${pageHeight})` + }; + } + + if (right > pageWidth) { + return { + valid: false, + error: `for snapshot ${snapshotName}, coordinates exceed viewport bounds: ${coordString}. right (${right}) exceeds viewport width (${pageWidth})` + }; + } + + return { + valid: true, + coords: { top, bottom, left, right } + }; } \ No newline at end of file diff --git a/src/types.ts b/src/types.ts index 0e34e6c..28487f0 100644 --- a/src/types.ts +++ b/src/types.ts @@ -123,13 +123,15 @@ export interface Snapshot { id?: Array, class?: Array, cssSelector?: Array, - xpath?: Array + xpath?: Array, + coordinates?: Array }, selectDOM?: { id?: Array, class?: Array, cssSelector?: Array, - xpath?: Array + xpath?: Array, + coordinates?: Array }, element?: { id?: string,