diff --git a/packages/image-comparison-core/src/__snapshots__/base.test.ts.snap b/packages/image-comparison-core/src/__snapshots__/base.test.ts.snap index 300037063..9c059396c 100644 --- a/packages/image-comparison-core/src/__snapshots__/base.test.ts.snap +++ b/packages/image-comparison-core/src/__snapshots__/base.test.ts.snap @@ -30,6 +30,7 @@ exports[`BaseClass > initializes default options correctly 1`] = ` "formatImageName": "{tag}-{browserName}-{width}x{height}-dpr-{dpr}", "fullPageScrollTimeout": 1500, "hideScrollBars": true, + "ignoreRegionPadding": 1, "isHybridApp": false, "savePerInstance": false, "tabbableOptions": { diff --git a/packages/image-comparison-core/src/base.interfaces.ts b/packages/image-comparison-core/src/base.interfaces.ts index 17094f7d4..c39894cf4 100644 --- a/packages/image-comparison-core/src/base.interfaces.ts +++ b/packages/image-comparison-core/src/base.interfaces.ts @@ -51,6 +51,13 @@ export interface BaseWebScreenshotOptions { * @default true */ hideScrollBars?: boolean; + /** + * Padding in device pixels added to each side of ignore regions (makes each region 2× this value wider and higher). + * Helps avoid 1px boundary differences on high-DPR / BiDi. Set to 0 to disable. + * Applies to screen, element, and full-page web methods. + * @default 1 + */ + ignoreRegionPadding?: number; /** * Elements to hide before taking screenshot * @default [] diff --git a/packages/image-comparison-core/src/clientSideScripts/injectWebviewOverlay.test.ts b/packages/image-comparison-core/src/clientSideScripts/injectWebviewOverlay.test.ts index 73defb4e5..eef2532f1 100644 --- a/packages/image-comparison-core/src/clientSideScripts/injectWebviewOverlay.test.ts +++ b/packages/image-comparison-core/src/clientSideScripts/injectWebviewOverlay.test.ts @@ -67,6 +67,40 @@ describe('injectWebviewOverlay', () => { }) }) + it('should preserve float precision with non-integer DPR (Android)', () => { + Object.defineProperty(window, 'devicePixelRatio', { + value: 2.625, + configurable: true, + }) + Object.defineProperty(window, 'innerWidth', { + value: 412, + configurable: true, + }) + Object.defineProperty(document.documentElement, 'clientHeight', { + value: 363, + configurable: true, + }) + + injectWebviewOverlay(true) + + const overlay = document.querySelector('[data-test="ics-overlay"]') as HTMLDivElement + const event = new window.MouseEvent('click', { + clientX: 206, + clientY: 181, + bubbles: true, + }) + overlay.dispatchEvent(event) + + const parsedData = JSON.parse(overlay.dataset.icsWebviewData!) + + expect(parsedData).toEqual({ + x: 206 * 2.625, + y: 181 * 2.625, + width: 412 * 2.625, + height: 363 * 2.625, + }) + }) + it('should use DPR = 1 for iOS (isAndroid = false)', () => { injectWebviewOverlay(false) diff --git a/packages/image-comparison-core/src/clientSideScripts/injectWebviewOverlay.ts b/packages/image-comparison-core/src/clientSideScripts/injectWebviewOverlay.ts index 92e64a2ef..acdbdbf61 100644 --- a/packages/image-comparison-core/src/clientSideScripts/injectWebviewOverlay.ts +++ b/packages/image-comparison-core/src/clientSideScripts/injectWebviewOverlay.ts @@ -21,10 +21,10 @@ export function injectWebviewOverlay(isAndroid: boolean): void { overlay.onclick = (event) => { const { clientX: x, clientY: y } = event const data = { - x: Math.round(x * dpr), - y: Math.round(y * dpr), - width: Math.round(window.innerWidth * dpr), - height: Math.round(document.documentElement.clientHeight * dpr), + x: x * dpr, + y: y * dpr, + width: window.innerWidth * dpr, + height: document.documentElement.clientHeight * dpr, } overlay.dataset.icsWebviewData = JSON.stringify(data) diff --git a/packages/image-comparison-core/src/clientSideScripts/scrollElementIntoView.test.ts b/packages/image-comparison-core/src/clientSideScripts/scrollElementIntoView.test.ts index a12addd57..9360c50c2 100644 --- a/packages/image-comparison-core/src/clientSideScripts/scrollElementIntoView.test.ts +++ b/packages/image-comparison-core/src/clientSideScripts/scrollElementIntoView.test.ts @@ -69,7 +69,8 @@ describe('scrollElementIntoView', () => { const result = scrollElementIntoView(mockElement, addressBarShadowPadding) expect(result).toBe(50) - expect(mockHtmlNode.scrollTop).toBe(90) + // Document y = currentScroll(50) + BCR.top(100) - padding(10) = 140 + expect(mockHtmlNode.scrollTop).toBe(140) }) it('should return current scroll position when body node has scroll', () => { @@ -80,7 +81,19 @@ describe('scrollElementIntoView', () => { const result = scrollElementIntoView(mockElement, addressBarShadowPadding) expect(result).toBe(50) - expect(mockBodyNode.scrollTop).toBe(90) + // Document y = currentScroll(50) + BCR.top(100) - padding(10) = 140 + expect(mockBodyNode.scrollTop).toBe(140) + }) + + it('should not re-scroll when element is already at the viewport top', () => { + mockHtmlNode.scrollTop = 600 + ;(mockElement.getBoundingClientRect as ReturnType).mockReturnValue({ top: 0 }) + const addressBarShadowPadding = 0 + const result = scrollElementIntoView(mockElement, addressBarShadowPadding) + + expect(result).toBe(600) + // Document y = currentScroll(600) + BCR.top(0) - padding(0) = 600 (stays put) + expect(mockHtmlNode.scrollTop).toBe(600) }) it('should not scroll when neither html nor body is scrollable', () => { diff --git a/packages/image-comparison-core/src/clientSideScripts/scrollElementIntoView.ts b/packages/image-comparison-core/src/clientSideScripts/scrollElementIntoView.ts index 6e7a8e836..1e8f94149 100644 --- a/packages/image-comparison-core/src/clientSideScripts/scrollElementIntoView.ts +++ b/packages/image-comparison-core/src/clientSideScripts/scrollElementIntoView.ts @@ -19,7 +19,10 @@ export default function scrollElementIntoView(element: HTMLElement, addressBarSh } const { top } = element.getBoundingClientRect() - const yPosition = top - addressBarShadowPadding + // BCR.top is viewport-relative, so the element's document position is + // currentPosition + top. Scroll there (minus padding) to place the + // element at the viewport top. + const yPosition = currentPosition + top - addressBarShadowPadding // Scroll to the position if (htmlNode.scrollHeight > htmlNode.clientHeight) { diff --git a/packages/image-comparison-core/src/commands/__snapshots__/checkFullPageScreen.test.ts.snap b/packages/image-comparison-core/src/commands/__snapshots__/checkFullPageScreen.test.ts.snap index 02f107cfc..4e26155dd 100644 --- a/packages/image-comparison-core/src/commands/__snapshots__/checkFullPageScreen.test.ts.snap +++ b/packages/image-comparison-core/src/commands/__snapshots__/checkFullPageScreen.test.ts.snap @@ -96,6 +96,8 @@ exports[`checkFullPageScreen > should execute checkFullPageScreen with basic opt "hideAfterFirstScroll": [], "hideElements": [], "hideScrollBars": true, + "ignore": undefined, + "ignoreRegionPadding": undefined, "removeElements": [], "waitForFontsLoaded": true, }, @@ -328,6 +330,8 @@ exports[`checkFullPageScreen > should handle all full page specific options 1`] "hideAfterFirstScroll": [], "hideElements": [], "hideScrollBars": false, + "ignore": undefined, + "ignoreRegionPadding": undefined, "removeElements": [], "waitForFontsLoaded": false, }, @@ -479,6 +483,8 @@ exports[`checkFullPageScreen > should handle hideAfterFirstScroll correctly 1`] ], "hideElements": [], "hideScrollBars": true, + "ignore": undefined, + "ignoreRegionPadding": undefined, "removeElements": [], "waitForFontsLoaded": true, }, @@ -630,6 +636,8 @@ exports[`checkFullPageScreen > should handle hideElements and removeElements cor }, ], "hideScrollBars": true, + "ignore": undefined, + "ignoreRegionPadding": undefined, "removeElements": [ { "elementId": "remove-element", @@ -867,6 +875,8 @@ exports[`checkFullPageScreen > should handle undefined method options with fallb "hideAfterFirstScroll": [], "hideElements": [], "hideScrollBars": true, + "ignore": undefined, + "ignoreRegionPadding": undefined, "removeElements": [], "waitForFontsLoaded": true, }, diff --git a/packages/image-comparison-core/src/commands/__snapshots__/checkWebElement.test.ts.snap b/packages/image-comparison-core/src/commands/__snapshots__/checkWebElement.test.ts.snap index c913238dd..921e51d4c 100644 --- a/packages/image-comparison-core/src/commands/__snapshots__/checkWebElement.test.ts.snap +++ b/packages/image-comparison-core/src/commands/__snapshots__/checkWebElement.test.ts.snap @@ -25,6 +25,7 @@ exports[`checkWebElement > should execute checkWebElement with basic options 2`] "baselineFolder": "/test/baseline", "diffFolder": "/test/diff", }, + "ignore": undefined, "instanceData": { "appName": "TestApp", "browserName": "Chrome", @@ -97,6 +98,7 @@ exports[`checkWebElement > should execute checkWebElement with basic options 2`] "enableLegacyScreenshotMethod": false, "hideElements": [], "hideScrollBars": true, + "ignoreRegionPadding": undefined, "removeElements": [], "resizeDimensions": undefined, "waitForFontsLoaded": true, @@ -425,6 +427,7 @@ exports[`checkWebElement > should handle custom element options 1`] = ` "baselineFolder": "/test/baseline", "diffFolder": "/test/diff", }, + "ignore": undefined, "instanceData": { "appName": "TestApp", "browserName": "Chrome", @@ -505,6 +508,7 @@ exports[`checkWebElement > should handle custom element options 1`] = ` }, ], "hideScrollBars": false, + "ignoreRegionPadding": undefined, "removeElements": [ { "elementId": "remove-element", @@ -764,6 +768,7 @@ exports[`checkWebElement > should handle undefined method options with fallbacks "baselineFolder": "/test/baseline", "diffFolder": "/test/diff", }, + "ignore": undefined, "instanceData": { "appName": "TestApp", "browserName": "Chrome", @@ -836,6 +841,7 @@ exports[`checkWebElement > should handle undefined method options with fallbacks "enableLegacyScreenshotMethod": false, "hideElements": [], "hideScrollBars": true, + "ignoreRegionPadding": undefined, "removeElements": [], "resizeDimensions": undefined, "waitForFontsLoaded": true, diff --git a/packages/image-comparison-core/src/commands/__snapshots__/checkWebScreen.test.ts.snap b/packages/image-comparison-core/src/commands/__snapshots__/checkWebScreen.test.ts.snap index 584d6d181..09ecabecc 100644 --- a/packages/image-comparison-core/src/commands/__snapshots__/checkWebScreen.test.ts.snap +++ b/packages/image-comparison-core/src/commands/__snapshots__/checkWebScreen.test.ts.snap @@ -22,6 +22,7 @@ exports[`checkWebScreen > should execute checkWebScreen with basic options 2`] = "baselineFolder": "/test/baseline", "diffFolder": "/test/diff", }, + "ignore": undefined, "instanceData": { "appName": "TestApp", "browserName": "Chrome", @@ -95,6 +96,7 @@ exports[`checkWebScreen > should execute checkWebScreen with basic options 2`] = "enableLegacyScreenshotMethod": false, "hideElements": [], "hideScrollBars": true, + "ignoreRegionPadding": undefined, "removeElements": [], "waitForFontsLoaded": true, }, @@ -333,6 +335,7 @@ exports[`checkWebScreen > should handle all method options correctly 1`] = ` "baselineFolder": "/test/baseline", "diffFolder": "/test/diff", }, + "ignore": undefined, "instanceData": { "appName": "TestApp", "browserName": "Chrome", @@ -414,6 +417,7 @@ exports[`checkWebScreen > should handle all method options correctly 1`] = ` }, ], "hideScrollBars": false, + "ignoreRegionPadding": undefined, "removeElements": [ { "elementId": "remove-element", @@ -577,6 +581,7 @@ exports[`checkWebScreen > should handle hideElements and removeElements correctl "baselineFolder": "/test/baseline", "diffFolder": "/test/diff", }, + "ignore": undefined, "instanceData": { "appName": "TestApp", "browserName": "Chrome", @@ -658,6 +663,7 @@ exports[`checkWebScreen > should handle hideElements and removeElements correctl }, ], "hideScrollBars": true, + "ignoreRegionPadding": undefined, "removeElements": [ { "elementId": "test-element", @@ -821,6 +827,7 @@ exports[`checkWebScreen > should handle native context correctly 1`] = ` "baselineFolder": "/test/baseline", "diffFolder": "/test/diff", }, + "ignore": undefined, "instanceData": { "appName": "TestApp", "browserName": "Chrome", @@ -894,6 +901,7 @@ exports[`checkWebScreen > should handle native context correctly 1`] = ` "enableLegacyScreenshotMethod": false, "hideElements": [], "hideScrollBars": true, + "ignoreRegionPadding": undefined, "removeElements": [], "waitForFontsLoaded": true, }, @@ -1049,6 +1057,7 @@ exports[`checkWebScreen > should merge compare options correctly 1`] = ` "baselineFolder": "/test/baseline", "diffFolder": "/test/diff", }, + "ignore": undefined, "instanceData": { "appName": "TestApp", "browserName": "Chrome", @@ -1122,6 +1131,7 @@ exports[`checkWebScreen > should merge compare options correctly 1`] = ` "enableLegacyScreenshotMethod": false, "hideElements": [], "hideScrollBars": true, + "ignoreRegionPadding": undefined, "removeElements": [], "waitForFontsLoaded": true, }, diff --git a/packages/image-comparison-core/src/commands/__snapshots__/saveElement.test.ts.snap b/packages/image-comparison-core/src/commands/__snapshots__/saveElement.test.ts.snap index db139e1ee..772c3956e 100644 --- a/packages/image-comparison-core/src/commands/__snapshots__/saveElement.test.ts.snap +++ b/packages/image-comparison-core/src/commands/__snapshots__/saveElement.test.ts.snap @@ -185,6 +185,7 @@ exports[`saveElement > should execute saveWebElement when isNativeContext is fal "baselineFolder": "/test/baseline", "diffFolder": "/test/diff", }, + "ignore": undefined, "instanceData": { "appName": "TestApp", "browserName": "Chrome", diff --git a/packages/image-comparison-core/src/commands/check.interfaces.ts b/packages/image-comparison-core/src/commands/check.interfaces.ts index 42baa0390..9de9a2c6d 100644 --- a/packages/image-comparison-core/src/commands/check.interfaces.ts +++ b/packages/image-comparison-core/src/commands/check.interfaces.ts @@ -15,7 +15,7 @@ export interface CheckMethodOptions extends BaseImageCompareOptions, BaseMobileB /** * Ignore elements and or areas */ - ignore?: ElementIgnore[]; + ignore?: (ElementIgnore | ElementIgnore[])[]; } export interface InternalCheckMethodOptions extends InternalSaveMethodOptions { diff --git a/packages/image-comparison-core/src/commands/checkFullPageScreen.ts b/packages/image-comparison-core/src/commands/checkFullPageScreen.ts index 8e94d1596..91b7aeaa8 100644 --- a/packages/image-comparison-core/src/commands/checkFullPageScreen.ts +++ b/packages/image-comparison-core/src/commands/checkFullPageScreen.ts @@ -31,6 +31,7 @@ export default async function checkFullPageScreen( hideAfterFirstScroll = [], hideScrollBars, hideElements = [], + ignoreRegionPadding, removeElements = [], waitForFontsLoaded, } = checkFullPageOptions.method @@ -52,11 +53,13 @@ export default async function checkFullPageScreen( hideAfterFirstScroll, hideScrollBars, hideElements, + ignore: checkFullPageOptions.method.ignore, + ignoreRegionPadding, removeElements, waitForFontsLoaded, }, } - const { devicePixelRatio, fileName, base64Image } = await saveFullPageScreen({ + const { devicePixelRatio, fileName, base64Image, ignoreRegions } = await saveFullPageScreen({ browserInstance, folders, instanceData, @@ -73,6 +76,9 @@ export default async function checkFullPageScreen( methodCompareOptions: compareOptions, devicePixelRatio, fileName, + additionalProperties: { + ignoreRegions: ignoreRegions || [], + }, }) // 5. Now execute the compare and return the data diff --git a/packages/image-comparison-core/src/commands/checkWebElement.ts b/packages/image-comparison-core/src/commands/checkWebElement.ts index 957ec9a8a..e0d1ebb14 100644 --- a/packages/image-comparison-core/src/commands/checkWebElement.ts +++ b/packages/image-comparison-core/src/commands/checkWebElement.ts @@ -30,6 +30,7 @@ export default async function checkWebElement( enableLegacyScreenshotMethod, hideScrollBars, resizeDimensions, + ignoreRegionPadding, hideElements = [], removeElements = [], waitForFontsLoaded = false, @@ -45,17 +46,19 @@ export default async function checkWebElement( enableLegacyScreenshotMethod, hideScrollBars, resizeDimensions, + ignoreRegionPadding, hideElements, removeElements, waitForFontsLoaded, }, } - const { devicePixelRatio, fileName, base64Image } = await saveWebElement({ + const { devicePixelRatio, fileName, base64Image, ignoreRegions } = await saveWebElement({ browserInstance, instanceData, folders, element, tag, + ignore: checkElementOptions.method.ignore, saveElementOptions, }) @@ -68,6 +71,9 @@ export default async function checkWebElement( devicePixelRatio, fileName, isElementScreenshot: true, + additionalProperties: { + ignoreRegions: ignoreRegions || [], + }, }) // 4. Now execute the compare and return the data diff --git a/packages/image-comparison-core/src/commands/checkWebScreen.test.ts b/packages/image-comparison-core/src/commands/checkWebScreen.test.ts index 964181bce..d79ee5387 100644 --- a/packages/image-comparison-core/src/commands/checkWebScreen.test.ts +++ b/packages/image-comparison-core/src/commands/checkWebScreen.test.ts @@ -211,6 +211,64 @@ describe('checkWebScreen', () => { expect(executeImageCompareSpy.mock.calls[0]).toMatchSnapshot() }) + it('should pass ignore elements to saveWebScreen and forward resolved regions', async () => { + const { buildBaseExecuteCompareOptions } = await import('../helpers/utils.js') + const buildBaseExecuteCompareOptionsSpy = vi.mocked(buildBaseExecuteCompareOptions) + + const mockIgnoreElement = { elementId: 'ignore-el', selector: '.navbar' } as any + const mockIgnoreRegion = { x: 10, y: 20, width: 100, height: 50 } + const resolvedRegions = [ + { x: 50, y: 60, width: 200, height: 100 }, + { x: 10, y: 20, width: 100, height: 50 }, + ] + + // saveWebScreen returns ignoreRegions resolved during screenshot + saveWebScreenSpy.mockResolvedValueOnce({ + devicePixelRatio: 2, + fileName: 'test-screen.png', + ignoreRegions: resolvedRegions, + }) + + const options = { + ...baseOptions, + checkScreenOptions: { + ...baseOptions.checkScreenOptions, + method: { + ...baseOptions.checkScreenOptions.method, + ignore: [mockIgnoreElement, mockIgnoreRegion], + } + } + } + + await checkWebScreen(options) + + // ignore is passed through to saveWebScreen + expect(saveWebScreenSpy).toHaveBeenCalledWith( + expect.objectContaining({ + ignore: [mockIgnoreElement, mockIgnoreRegion], + }) + ) + // resolved regions are forwarded to the compare options + expect(buildBaseExecuteCompareOptionsSpy).toHaveBeenCalledWith( + expect.objectContaining({ + additionalProperties: { ignoreRegions: resolvedRegions }, + }) + ) + }) + + it('should pass empty array when no ignore regions are returned', async () => { + const { buildBaseExecuteCompareOptions } = await import('../helpers/utils.js') + const buildBaseExecuteCompareOptionsSpy = vi.mocked(buildBaseExecuteCompareOptions) + + await checkWebScreen(baseOptions) + + expect(buildBaseExecuteCompareOptionsSpy).toHaveBeenCalledWith( + expect.objectContaining({ + additionalProperties: { ignoreRegions: [] }, + }) + ) + }) + it('should handle all method options correctly', async () => { const mockHideElement = { elementId: 'hide-element', diff --git a/packages/image-comparison-core/src/commands/checkWebScreen.ts b/packages/image-comparison-core/src/commands/checkWebScreen.ts index 9e749ce37..28e785918 100644 --- a/packages/image-comparison-core/src/commands/checkWebScreen.ts +++ b/packages/image-comparison-core/src/commands/checkWebScreen.ts @@ -28,12 +28,15 @@ export default async function checkWebScreen( enableLayoutTesting, enableLegacyScreenshotMethod, hideScrollBars, + ignoreRegionPadding, hideElements = [], removeElements = [], waitForFontsLoaded, } = checkScreenOptions.method - // 2. Take the actual screenshot and retrieve the needed data + // 2. Take the actual screenshot and resolve ignore regions in one go. + // Ignore regions are resolved while the DOM is still in screenshot state + // (scrollbar hidden, elements hidden/removed) so positions match the image. const saveScreenOptions: SaveScreenOptions = { wic: checkScreenOptions.wic, method: { @@ -42,16 +45,18 @@ export default async function checkWebScreen( enableLayoutTesting, enableLegacyScreenshotMethod, hideScrollBars, + ignoreRegionPadding, hideElements, removeElements, waitForFontsLoaded, }, } - const { devicePixelRatio, fileName, base64Image } = await saveWebScreen({ + const { devicePixelRatio, fileName, base64Image, ignoreRegions } = await saveWebScreen({ browserInstance, instanceData, folders, tag, + ignore: checkScreenOptions.method.ignore, saveScreenOptions, isNativeContext, }) @@ -64,6 +69,9 @@ export default async function checkWebScreen( methodCompareOptions, devicePixelRatio, fileName, + additionalProperties: { + ignoreRegions: ignoreRegions || [], + }, }) // 4. Now execute the compare and return the data diff --git a/packages/image-comparison-core/src/commands/fullPage.interfaces.ts b/packages/image-comparison-core/src/commands/fullPage.interfaces.ts index f3b6cdd15..c00f80cc9 100644 --- a/packages/image-comparison-core/src/commands/fullPage.interfaces.ts +++ b/packages/image-comparison-core/src/commands/fullPage.interfaces.ts @@ -2,6 +2,7 @@ import type { BaseMobileWebScreenshotOptions, BaseWebScreenshotOptions, Folders import type { DefaultOptions } from '../helpers/options.interfaces.js' import type { ResizeDimensions } from '../methods/images.interfaces.js' import type { CheckMethodOptions } from './check.interfaces.js' +import type { ElementIgnore } from './element.interfaces.js' export interface SaveFullPageOptions { wic: DefaultOptions; @@ -9,6 +10,11 @@ export interface SaveFullPageOptions { } export interface SaveFullPageMethodOptions extends Partial, BaseWebScreenshotOptions, BaseMobileWebScreenshotOptions { + /** + * Elements or regions to ignore when saving/comparing full-page (desktop and mobile web). + * Same format as saveScreen / checkScreen (selectors or { x, y, width, height } in document CSS pixels). + */ + ignore?: (ElementIgnore | ElementIgnore[])[]; /** * The amount of milliseconds to wait for a new scroll. This will be used for the legacy * fullpage screenshot method. diff --git a/packages/image-comparison-core/src/commands/save.interfaces.ts b/packages/image-comparison-core/src/commands/save.interfaces.ts index f687a5a3a..860d10d54 100644 --- a/packages/image-comparison-core/src/commands/save.interfaces.ts +++ b/packages/image-comparison-core/src/commands/save.interfaces.ts @@ -1,7 +1,7 @@ import type { InstanceData } from '../methods/instanceData.interfaces.js' import type { SaveFullPageOptions } from './fullPage.interfaces.js' import type { SaveScreenOptions } from './screen.interfaces.js' -import type { SaveElementOptions, WicElement } from './element.interfaces.js' +import type { SaveElementOptions, WicElement, ElementIgnore } from './element.interfaces.js' import type { SaveTabbableOptions } from './tabbable.interfaces.js' import type { Folders } from '../base.interfaces.js' @@ -14,11 +14,13 @@ export interface InternalSaveMethodOptions { } export interface InternalSaveScreenMethodOptions extends InternalSaveMethodOptions { + ignore?: (ElementIgnore | ElementIgnore[])[]; saveScreenOptions: SaveScreenOptions, } export interface InternalSaveElementMethodOptions extends InternalSaveMethodOptions { element: HTMLElement | WicElement; + ignore?: (ElementIgnore | ElementIgnore[])[]; saveElementOptions: SaveElementOptions, } diff --git a/packages/image-comparison-core/src/commands/saveElement.ts b/packages/image-comparison-core/src/commands/saveElement.ts index 1b34a3756..da8dce7a3 100644 --- a/packages/image-comparison-core/src/commands/saveElement.ts +++ b/packages/image-comparison-core/src/commands/saveElement.ts @@ -13,11 +13,12 @@ export default async function saveElement( folders, instanceData, isNativeContext, + ignore, saveElementOptions, tag, }: InternalSaveElementMethodOptions ): Promise { return isNativeContext ? saveAppElement({ browserInstance, element, folders, instanceData, saveElementOptions, isNativeContext, tag }) - : saveWebElement({ browserInstance, element, folders, instanceData, saveElementOptions, tag }) + : saveWebElement({ browserInstance, element, folders, instanceData, saveElementOptions, tag, ignore }) } diff --git a/packages/image-comparison-core/src/commands/saveFullPageScreen.ts b/packages/image-comparison-core/src/commands/saveFullPageScreen.ts index f4fe8b395..1a1a94a53 100644 --- a/packages/image-comparison-core/src/commands/saveFullPageScreen.ts +++ b/packages/image-comparison-core/src/commands/saveFullPageScreen.ts @@ -8,6 +8,7 @@ import type { FullPageScreenshotDataOptions } from '../methods/screenshots.inter import type { InternalSaveFullPageMethodOptions } from './save.interfaces.js' import { getMethodOrWicOption, canUseBidiScreenshot } from '../helpers/utils.js' import { createBeforeScreenshotOptions, buildAfterScreenshotOptions } from '../helpers/options.js' +import { determineWebFullPageIgnoreRegions } from '../methods/rectangles.js' /** * Saves an image of the full page @@ -51,6 +52,7 @@ export default async function saveFullPageScreen( isAndroidChromeDriverScreenshot, isAndroidNativeWebScreenshot, isIOS, + isMobile, } = enrichedInstanceData // 4. Take the screenshot @@ -78,7 +80,26 @@ export default async function saveFullPageScreen( ? screenshotsData.data[0].screenshot // BiDi screenshot - use directly : await makeFullPageBase64Image(screenshotsData, { devicePixelRatio: devicePixelRatio || NaN, isLandscape }) - // 6. Return the data + // 6. Resolve ignore regions while the DOM is still in screenshot state. + // Full-page image (BiDi or stitched) is in document coordinates; regions are document-relative device pixels. + // On mobile scroll-and-stitch we crop addressBarShadowPadding from the top of each tile, so we pass + // fullPageCropTopPaddingCSS so ignore regions align with the stitched canvas. + const ignore = saveFullPageOptions.method?.ignore + const ignoreRegionPadding = (getMethodOrWicOption(saveFullPageOptions.method, saveFullPageOptions.wic, 'ignoreRegionPadding') as number | undefined) ?? 1 + const usedStitchedMobile = isMobile && !(screenshotsData.fullPageHeight === -1 && screenshotsData.fullPageWidth === -1) + const ignoreRegions = ignore && ignore.length > 0 + ? await determineWebFullPageIgnoreRegions( + { + browserInstance, + devicePixelRatio: devicePixelRatio || 1, + fullPageCropTopPaddingCSS: usedStitchedMobile ? beforeOptions.addressBarShadowPadding : 0, + ignoreRegionPadding, + }, + ignore, + ) + : undefined + + // 7. Return the data const afterOptions = buildAfterScreenshotOptions({ base64Image: fullPageBase64Image, folders, @@ -90,5 +111,10 @@ export default async function saveFullPageScreen( wicOptions: saveFullPageOptions.wic }) - return afterScreenshot(browserInstance, afterOptions!) + const result = await afterScreenshot(browserInstance, afterOptions!) + + return { + ...result, + ...(ignoreRegions ? { ignoreRegions } : {}), + } } diff --git a/packages/image-comparison-core/src/commands/saveWebElement.ts b/packages/image-comparison-core/src/commands/saveWebElement.ts index 150a1473d..61810cb5a 100644 --- a/packages/image-comparison-core/src/commands/saveWebElement.ts +++ b/packages/image-comparison-core/src/commands/saveWebElement.ts @@ -9,6 +9,7 @@ import type { ElementScreenshotDataOptions } from '../methods/screenshots.interf import { canUseBidiScreenshot, getMethodOrWicOption } from '../helpers/utils.js' import { createBeforeScreenshotOptions, buildAfterScreenshotOptions } from '../helpers/options.js' import type { InternalSaveElementMethodOptions } from './save.interfaces.js' +import { determineWebElementIgnoreRegions } from '../methods/rectangles.js' /** * Saves an image of an element @@ -20,6 +21,7 @@ export default async function saveWebElement( folders, element, tag, + ignore, saveElementOptions, }: InternalSaveElementMethodOptions ): Promise { @@ -72,6 +74,28 @@ export default async function saveWebElement( const shouldUseBidi = canUseBidiScreenshot(browserInstance) && !isMobile && !enableLegacyScreenshotMethod const screenshotData = await takeElementScreenshot(browserInstance, elementScreenshotOptions, shouldUseBidi) + // 3b. Resolve ignore regions (element-local) while the DOM is still in screenshot state. + // determineWebElementIgnoreRegions returns device-pixel regions, or CSS-pixel regions when + // the element image is from the native driver on Android native web (see that function). + const ignoreRegionPadding = (getMethodOrWicOption(saveElementOptions.method, saveElementOptions.wic, 'ignoreRegionPadding') as number | undefined) ?? 1 + const ignoreRegions = ignore && ignore.length > 0 + ? await (async () => { + const rootElement = await (element as any as WebdriverIO.Element | Promise) + + return determineWebElementIgnoreRegions( + { + browserInstance, + devicePixelRatio: devicePixelRatio || 1, + rootElement: rootElement as WebdriverIO.Element, + ignoreRegionPadding, + isAndroidNativeWebScreenshot, + isWebDriverElementScreenshot: screenshotData.isWebDriverElementScreenshot, + }, + ignore, + ) + })() + : undefined + // 4. Return the data const afterOptions = buildAfterScreenshotOptions({ base64Image: screenshotData.base64Image, @@ -84,5 +108,10 @@ export default async function saveWebElement( wicOptions: saveElementOptions.wic }) - return afterScreenshot(browserInstance, afterOptions) + const result = await afterScreenshot(browserInstance, afterOptions) + + return { + ...result, + ...(ignoreRegions ? { ignoreRegions } : {}), + } } diff --git a/packages/image-comparison-core/src/commands/saveWebScreen.ts b/packages/image-comparison-core/src/commands/saveWebScreen.ts index eb41ae723..640ec6b18 100644 --- a/packages/image-comparison-core/src/commands/saveWebScreen.ts +++ b/packages/image-comparison-core/src/commands/saveWebScreen.ts @@ -7,6 +7,7 @@ import type { InternalSaveScreenMethodOptions } from './save.interfaces.js' import type { WebScreenshotDataOptions } from '../methods/screenshots.interfaces.js' import { canUseBidiScreenshot, getMethodOrWicOption } from '../helpers/utils.js' import { createBeforeScreenshotOptions, buildAfterScreenshotOptions } from '../helpers/options.js' +import { determineWebScreenIgnoreRegions } from '../methods/rectangles.js' /** * Saves an image of the viewport of the screen @@ -17,6 +18,7 @@ export default async function saveWebScreen( instanceData, folders, tag, + ignore, saveScreenOptions, isNativeContext = false, }: InternalSaveScreenMethodOptions @@ -67,7 +69,26 @@ export default async function saveWebScreen( } const { base64Image } = await takeWebScreenshot(browserInstance, webScreenshotOptions, shouldUseBidi) - // 4. Return the data + // 4. Resolve ignore regions while the DOM is still in screenshot state + // (scrollbar hidden, elements hidden/removed, CSS applied). + // This must happen BEFORE afterScreenshot restores the DOM. + const ignoreRegionPadding = (getMethodOrWicOption(saveScreenOptions.method, saveScreenOptions.wic, 'ignoreRegionPadding') as number | undefined) ?? 1 + const ignoreRegions = ignore && ignore.length > 0 + ? await determineWebScreenIgnoreRegions( + { + browserInstance, + devicePixelRatio: devicePixelRatio || 1, + deviceRectangles: instanceData.deviceRectangles, + isAndroid, + isAndroidNativeWebScreenshot, + isIOS, + ignoreRegionPadding, + }, + ignore, + ) + : undefined + + // 5. Restore the DOM and return the data const afterOptions = buildAfterScreenshotOptions({ base64Image, folders, @@ -79,5 +100,10 @@ export default async function saveWebScreen( wicOptions: saveScreenOptions.wic }) - return afterScreenshot(browserInstance, afterOptions) + const result = await afterScreenshot(browserInstance, afterOptions) + + return { + ...result, + ...(ignoreRegions ? { ignoreRegions } : {}), + } } diff --git a/packages/image-comparison-core/src/helpers/__snapshots__/options.test.ts.snap b/packages/image-comparison-core/src/helpers/__snapshots__/options.test.ts.snap index 9dd979fcb..c23771b92 100644 --- a/packages/image-comparison-core/src/helpers/__snapshots__/options.test.ts.snap +++ b/packages/image-comparison-core/src/helpers/__snapshots__/options.test.ts.snap @@ -106,6 +106,7 @@ exports[`options > defaultOptions > should return the default options when no op "formatImageName": "{tag}-{browserName}-{width}x{height}-dpr-{dpr}", "fullPageScrollTimeout": 1500, "hideScrollBars": true, + "ignoreRegionPadding": 1, "isHybridApp": false, "savePerInstance": false, "tabbableOptions": { @@ -161,6 +162,7 @@ exports[`options > defaultOptions > should return the provided options when opti "formatImageName": "{foo}-{bar}", "fullPageScrollTimeout": 12345, "hideScrollBars": true, + "ignoreRegionPadding": 1, "isHybridApp": false, "savePerInstance": true, "tabbableOptions": { diff --git a/packages/image-comparison-core/src/helpers/afterScreenshot.interfaces.ts b/packages/image-comparison-core/src/helpers/afterScreenshot.interfaces.ts index 9c3a309fb..5c54909a7 100644 --- a/packages/image-comparison-core/src/helpers/afterScreenshot.interfaces.ts +++ b/packages/image-comparison-core/src/helpers/afterScreenshot.interfaces.ts @@ -1,8 +1,12 @@ +import type { RectanglesOutput } from '../methods/rectangles.interfaces.js' + export interface ScreenshotOutput { // The device pixel ratio of the instance devicePixelRatio: number; // The filename fileName: string; + // Resolved ignore regions in device pixels (when ignore elements were provided) + ignoreRegions?: RectanglesOutput[]; // Is Landscape isLandscape: boolean; // The path where the file can be found diff --git a/packages/image-comparison-core/src/helpers/options.interfaces.ts b/packages/image-comparison-core/src/helpers/options.interfaces.ts index 19b1211b8..6d4394001 100644 --- a/packages/image-comparison-core/src/helpers/options.interfaces.ts +++ b/packages/image-comparison-core/src/helpers/options.interfaces.ts @@ -118,6 +118,12 @@ export interface ClassOptions { */ hideScrollBars?: boolean; + /** + * Padding in device pixels added to each side of element ignore regions (default 1). + * Set to 0 to disable. Only applies to element screenshots. + */ + ignoreRegionPadding?: number; + // ================ // Compare options // ================ @@ -356,6 +362,12 @@ export interface DefaultOptions { */ hideScrollBars: boolean; + /** + * Padding in device pixels added to each side of element ignore regions (default 1). + * Set to 0 to disable. Only applies to element screenshots. + */ + ignoreRegionPadding?: number; + /** * Indicates whether the app is a hybrid (native + webview). */ diff --git a/packages/image-comparison-core/src/helpers/options.ts b/packages/image-comparison-core/src/helpers/options.ts index 61dce5289..bbd5209d7 100644 --- a/packages/image-comparison-core/src/helpers/options.ts +++ b/packages/image-comparison-core/src/helpers/options.ts @@ -54,6 +54,7 @@ export function defaultOptions(options: ClassOptions): DefaultOptions { // Default to false for storybook mode as element screenshots use W3C protocol without scrollbars // This also saves an extra webdriver call hideScrollBars: getBooleanOption(options, 'hideScrollBars', !isStorybookMode), + ignoreRegionPadding: options.ignoreRegionPadding ?? 1, waitForFontsLoaded: options.waitForFontsLoaded ?? true, alwaysSaveActualImage: options.alwaysSaveActualImage ?? true, diff --git a/packages/image-comparison-core/src/helpers/utils.test.ts b/packages/image-comparison-core/src/helpers/utils.test.ts index 3bb9e146d..835eda92b 100644 --- a/packages/image-comparison-core/src/helpers/utils.test.ts +++ b/packages/image-comparison-core/src/helpers/utils.test.ts @@ -691,6 +691,49 @@ describe('utils', () => { expect(result).toMatchSnapshot() }) + it('should handle float overlay values from non-integer DPR and ensure consistent dimensions', async () => { + const dpr = 2.625 + const screenW = 1080 + const screenH = 2424 + + const cssClickX = 206 + const cssClickY = 385 + const cssWidth = 412 + const cssHeight = 363 + + vi.mocked(mockBrowserInstance.execute) + .mockResolvedValueOnce(undefined) // loadBase64Html + .mockResolvedValueOnce(undefined) // checkMetaTag (iOS) + .mockResolvedValueOnce(undefined) // injectWebviewOverlay + .mockResolvedValueOnce(undefined) // executeNativeClick + .mockResolvedValueOnce({ + x: cssClickX * dpr, + y: cssClickY * dpr, + width: cssWidth * dpr, + height: cssHeight * dpr, + }) // getMobileWebviewClickAndDimensions (floats, not rounded) + + const result = await getMobileViewPortPosition({ + browserInstance: mockBrowserInstance, + ...baseOptions, + screenHeight: screenH, + screenWidth: screenW, + }) + + const viewportTop = Math.max(0, Math.round(screenH / 2 - cssClickY * dpr)) + const viewportLeft = Math.max(0, Math.round(screenW / 2 - cssClickX * dpr)) + const viewportWidth = Math.min(Math.round(cssWidth * dpr), screenW - viewportLeft) + const viewportHeight = Math.min(Math.round(cssHeight * dpr), screenH - viewportTop) + + expect(result.viewport.y).toBe(viewportTop) + expect(result.viewport.x).toBe(viewportLeft) + expect(result.viewport.width).toBe(viewportWidth) + expect(result.viewport.height).toBe(viewportHeight) + expect(result.statusBarAndAddressBar.height).toBe(viewportTop) + expect(result.viewport.y + result.viewport.height + result.bottomBar.height).toBe(screenH) + expect(result.viewport.x + result.viewport.width + result.rightSidePadding.width).toBe(screenW) + }) + it('should return initialDeviceRectangles if not WebView (native context)', async () => { const result = await getMobileViewPortPosition({ browserInstance: mockBrowserInstance, diff --git a/packages/image-comparison-core/src/helpers/utils.ts b/packages/image-comparison-core/src/helpers/utils.ts index 393989518..e2f0167f7 100644 --- a/packages/image-comparison-core/src/helpers/utils.ts +++ b/packages/image-comparison-core/src/helpers/utils.ts @@ -125,7 +125,7 @@ export function checkTestInMobileBrowser(isMobile: boolean, browserName: string) * Checks if this is a native webscreenshot on android */ export function checkAndroidNativeWebScreenshot(isAndroid: boolean, nativeWebscreenshot: boolean): boolean { - return (isAndroid && nativeWebscreenshot) || false + return (isAndroid && !!nativeWebscreenshot) || false } /** @@ -526,21 +526,24 @@ export async function getMobileViewPortPosition({ const { y, x, width, height } = await browserInstance.execute(getMobileWebviewClickAndDimensions, '[data-test="ics-overlay"]') // 4.b reset the url await browserInstance.url(currentUrl) - // 5. Calculate the position of the viewport based on the click position of the native click vs the overlay + // 5. Calculate the position of the viewport based on the click position of the native click vs the overlay. + // The overlay values may be floats (CSS pixels * DPR for Android, CSS pixels for iOS). + // We round edges once and derive dimensions from edges to guarantee + // viewportTop + viewportHeight + bottomBarHeight === screenHeight (no gaps). const viewportTop = Math.max(0, Math.round(nativeClickY - y)) const viewportLeft = Math.max(0, Math.round(nativeClickX - x)) - const statusBarAndAddressBarHeight = Math.max(0, Math.round(viewportTop)) - const bottomBarHeight = Math.max(0, Math.round(screenHeight - (viewportTop + height))) - const leftSidePaddingWidth = Math.max(0, Math.round(viewportLeft)) - const rightSidePaddingWidth = Math.max(0, Math.round(screenWidth - (viewportLeft + width))) + const viewportWidth = Math.min(Math.round(width), screenWidth - viewportLeft) + const viewportHeight = Math.min(Math.round(height), screenHeight - viewportTop) + const bottomBarHeight = Math.max(0, screenHeight - viewportTop - viewportHeight) + const rightSidePaddingWidth = Math.max(0, screenWidth - viewportLeft - viewportWidth) const deviceRectangles = { ...initialDeviceRectangles, - bottomBar: { y: viewportTop + height, x: 0, width: screenWidth, height: bottomBarHeight }, - leftSidePadding: { y: viewportTop, x: 0, width: leftSidePaddingWidth, height: height }, - rightSidePadding: { y: viewportTop, x: viewportLeft + width, width: rightSidePaddingWidth, height: height }, + bottomBar: { y: viewportTop + viewportHeight, x: 0, width: screenWidth, height: bottomBarHeight }, + leftSidePadding: { y: viewportTop, x: 0, width: viewportLeft, height: viewportHeight }, + rightSidePadding: { y: viewportTop, x: viewportLeft + viewportWidth, width: rightSidePaddingWidth, height: viewportHeight }, screenSize: { height: screenHeight, width: screenWidth }, - statusBarAndAddressBar: { y: 0, x: 0, width: screenWidth, height: statusBarAndAddressBarHeight }, - viewport: { y: viewportTop, x: viewportLeft, width: width, height: height }, + statusBarAndAddressBar: { y: 0, x: 0, width: screenWidth, height: viewportTop }, + viewport: { y: viewportTop, x: viewportLeft, width: viewportWidth, height: viewportHeight }, } return deviceRectangles diff --git a/packages/image-comparison-core/src/index.ts b/packages/image-comparison-core/src/index.ts index a328810c3..67b9f8e7e 100644 --- a/packages/image-comparison-core/src/index.ts +++ b/packages/image-comparison-core/src/index.ts @@ -20,6 +20,7 @@ export type { SaveScreenMethodOptions, } from './commands/screen.interfaces.js' export type { + ElementIgnore, WicElement, CheckElementMethodOptions, SaveElementMethodOptions, diff --git a/packages/image-comparison-core/src/methods/__snapshots__/rectangles.test.ts.snap b/packages/image-comparison-core/src/methods/__snapshots__/rectangles.test.ts.snap index 296e6d8b9..021efe1fc 100644 --- a/packages/image-comparison-core/src/methods/__snapshots__/rectangles.test.ts.snap +++ b/packages/image-comparison-core/src/methods/__snapshots__/rectangles.test.ts.snap @@ -273,18 +273,18 @@ exports[`rectangles > prepareIgnoreRectangles > should combine all rectangle sou "right": 220, "top": 40, }, - { - "bottom": 750, - "left": 400, - "right": 700, - "top": 600, - }, { "bottom": 640, "left": 0, "right": 2688, "top": 0, }, + { + "bottom": 375, + "left": 200, + "right": 350, + "top": 300, + }, ] `; @@ -325,10 +325,10 @@ exports[`rectangles > prepareIgnoreRectangles > should handle blockOut and ignor "top": 40, }, { - "bottom": 750, - "left": 400, - "right": 700, - "top": 600, + "bottom": 375, + "left": 200, + "right": 350, + "top": 300, }, ] `; diff --git a/packages/image-comparison-core/src/methods/__snapshots__/takeElementScreenshots.test.ts.snap b/packages/image-comparison-core/src/methods/__snapshots__/takeElementScreenshots.test.ts.snap index 2f1110f0f..9d98cabdb 100644 --- a/packages/image-comparison-core/src/methods/__snapshots__/takeElementScreenshots.test.ts.snap +++ b/packages/image-comparison-core/src/methods/__snapshots__/takeElementScreenshots.test.ts.snap @@ -1,5 +1,22 @@ // Vitest Snapshot v1, https://vitest.dev/guide/snapshot.html +exports[`takeElementScreenshot > BiDi screenshots > should scroll element into view when autoElementScroll is enabled 1`] = ` +[ + [MockFunction spy], + { + "elementId": "test-element", + }, + 6, +] +`; + +exports[`takeElementScreenshot > BiDi screenshots > should scroll element into view when autoElementScroll is enabled 2`] = ` +[ + [MockFunction spy], + 100, +] +`; + exports[`takeElementScreenshot > Edge cases > should handle devicePixelRatio values and fallback to NaN when falsy 1`] = ` { "base64Image": "cropped-screenshot-data", diff --git a/packages/image-comparison-core/src/methods/images.ts b/packages/image-comparison-core/src/methods/images.ts index d5635ed78..76baad62d 100644 --- a/packages/image-comparison-core/src/methods/images.ts +++ b/packages/image-comparison-core/src/methods/images.ts @@ -345,9 +345,19 @@ export async function executeImageCompare( isAndroidNativeWebScreenshot, isAndroid, fileName, + folderOptions, } = options - const { actualFolder, autoSaveBaseline, alwaysSaveActualImage, baselineFolder, browserName, deviceName, diffFolder, isMobile, savePerInstance } = - options.folderOptions + const { + actualFolder, + autoSaveBaseline, + alwaysSaveActualImage, + baselineFolder, + browserName, + deviceName, + diffFolder, + isMobile, + savePerInstance, + } = folderOptions const imageCompareOptions = { ...options.compareOptions.wic, ...options.compareOptions.method } // 1a. Disable JSON reports if alwaysSaveActualImage is false (JSON reports need the actual file to exist) diff --git a/packages/image-comparison-core/src/methods/rectangles.interfaces.ts b/packages/image-comparison-core/src/methods/rectangles.interfaces.ts index bdd7a3199..34511d140 100644 --- a/packages/image-comparison-core/src/methods/rectangles.interfaces.ts +++ b/packages/image-comparison-core/src/methods/rectangles.interfaces.ts @@ -138,6 +138,63 @@ export interface PreparedIgnoreRectangles { hasIgnoreRectangles: boolean; } +export interface DetermineWebScreenIgnoreRegionsOptions { + /** The browser instance */ + browserInstance: WebdriverIO.Browser; + /** The device pixel ratio */ + devicePixelRatio: number; + /** The device rectangles (contains viewport offset for mobile) */ + deviceRectangles: DeviceRectangles; + /** Whether this is an Android device */ + isAndroid: boolean; + /** Whether this is an Android native web screenshot */ + isAndroidNativeWebScreenshot: boolean; + /** Whether this is an iOS device */ + isIOS: boolean; + /** Padding in device pixels added to each side of computed ignore regions (caller defaults to 1). */ + ignoreRegionPadding: number; +} + +/** + * Options for full-page web ignore regions (desktop and mobile). + * Full-page image is in document coordinates; on mobile scroll-and-stitch the canvas + * crops off the top addressBarShadowPadding (CSS px), so we subtract that from y. + */ +export interface DetermineWebFullPageIgnoreRegionsOptions { + /** The browser instance */ + browserInstance: WebdriverIO.Browser; + /** The device pixel ratio */ + devicePixelRatio: number; + /** Padding in device pixels added to each side of computed ignore regions (caller defaults to 1). */ + ignoreRegionPadding: number; + /** + * Top crop offset in CSS pixels (e.g. addressBarShadowPadding on mobile full-page). + * When set, canvas y = (documentY - fullPageCropTopPaddingCSS) × DPR so ignore regions + * align with the stitched image which crops this much from the top of each tile. + * @default 0 + */ + fullPageCropTopPaddingCSS?: number; +} + +export interface DetermineWebElementIgnoreRegionsOptions { + /** The browser instance */ + browserInstance: WebdriverIO.Browser; + /** The device pixel ratio */ + devicePixelRatio: number; + /** The root element being captured in the element screenshot */ + rootElement: WebdriverIO.Element; + /** Padding in device pixels added to each side of computed ignore regions (caller defaults to 1). */ + ignoreRegionPadding: number; + /** + * When both this and isWebDriverElementScreenshot are true, the element image is at CSS pixel size + * (native driver returns a downscaled image). We then output ignore regions in CSS pixel coordinates + * by dividing by DPR; otherwise regions are in device pixels. + */ + isAndroidNativeWebScreenshot?: boolean; + /** When true, the element screenshot came from the native driver (no fallback crop). */ + isWebDriverElementScreenshot?: boolean; +} + export interface BoundingBox extends BaseBoundingBox { } export interface IgnoreBoxes extends BoundingBox { } diff --git a/packages/image-comparison-core/src/methods/rectangles.test.ts b/packages/image-comparison-core/src/methods/rectangles.test.ts index 24864da9f..db9e5cbd5 100644 --- a/packages/image-comparison-core/src/methods/rectangles.test.ts +++ b/packages/image-comparison-core/src/methods/rectangles.test.ts @@ -5,6 +5,9 @@ import { determineScreenRectangles, determineStatusAddressToolBarRectangles, determineIgnoreRegions, + determineWebFullPageIgnoreRegions, + determineWebScreenIgnoreRegions, + determineWebElementIgnoreRegions, splitIgnores, determineDeviceBlockOuts, prepareIgnoreRectangles @@ -31,7 +34,9 @@ describe('rectangles', () => { mockBrowserInstance = { execute: mockExecute, - getElementRect: mockGetElementRect + getElementRect: mockGetElementRect, + $: vi.fn(), + $$: vi.fn(), } as unknown as WebdriverIO.Browser }) @@ -757,6 +762,438 @@ describe('rectangles', () => { }) }) + describe('determineWebScreenIgnoreRegions', () => { + const desktopOptions = { + browserInstance: null as unknown as WebdriverIO.Browser, + devicePixelRatio: 2, + deviceRectangles: baseDeviceRectangles, + isAndroid: false, + isAndroidNativeWebScreenshot: false, + isIOS: false, + ignoreRegionPadding: 0, + } + + beforeEach(() => { + desktopOptions.browserInstance = mockBrowserInstance + }) + + it('should resolve elements via raw BCR on desktop and apply DPR', async () => { + const mockElement = { elementId: 'el1', selector: '.nav' } as WebdriverIO.Element + const freshElement = { elementId: 'el1-fresh', selector: '.nav' } as unknown as WebdriverIO.Element + vi.mocked(mockBrowserInstance.$$).mockResolvedValueOnce([freshElement] as any) + mockExecute.mockResolvedValueOnce({ x: 10, y: 20, width: 200, height: 50 }) + + const result = await determineWebScreenIgnoreRegions(desktopOptions, [mockElement]) + + expect(mockBrowserInstance.$$).toHaveBeenCalledWith('.nav') + expect(mockExecute).toHaveBeenCalledOnce() + expect(result).toEqual([ + { x: 20, y: 40, width: 400, height: 100 }, + ]) + }) + + it('should add DPR-scaled viewport offset on iOS and re-query elements via $$', async () => { + const iosDeviceRectangles = { + ...baseDeviceRectangles, + viewport: { y: 94, x: 0, width: 390, height: 650 }, + } + const iosOptions = { + ...desktopOptions, + devicePixelRatio: 3, + deviceRectangles: iosDeviceRectangles, + isIOS: true, + } + const mockElement = { elementId: 'el1', selector: '.hero' } as WebdriverIO.Element + const freshElement = { elementId: 'el1-fresh', selector: '.hero' } as unknown as WebdriverIO.Element + vi.mocked(mockBrowserInstance.$$).mockResolvedValueOnce([freshElement] as any) + mockExecute.mockResolvedValueOnce({ x: 0, y: 100, width: 390, height: 200 }) + + const result = await determineWebScreenIgnoreRegions(iosOptions, [mockElement]) + + expect(mockBrowserInstance.$$).toHaveBeenCalledWith('.hero') + expect(mockBrowserInstance.$).not.toHaveBeenCalled() + expect(result).toEqual([ + { x: 0, y: 582, width: 1170, height: 600 }, + ]) + }) + + it('should correctly resolve multiple elements sharing the same selector on iOS', async () => { + const iosDeviceRectangles = { + ...baseDeviceRectangles, + viewport: { y: 94, x: 0, width: 390, height: 650 }, + } + const iosOptions = { + ...desktopOptions, + devicePixelRatio: 1, + deviceRectangles: iosDeviceRectangles, + isIOS: true, + } + const el1 = { elementId: 'a', selector: '.card' } as WebdriverIO.Element + const el2 = { elementId: 'b', selector: '.card' } as WebdriverIO.Element + const el3 = { elementId: 'c', selector: '.card' } as WebdriverIO.Element + + const fresh1 = { elementId: 'f1', selector: '.card' } as unknown as WebdriverIO.Element + const fresh2 = { elementId: 'f2', selector: '.card' } as unknown as WebdriverIO.Element + const fresh3 = { elementId: 'f3', selector: '.card' } as unknown as WebdriverIO.Element + vi.mocked(mockBrowserInstance.$$).mockResolvedValueOnce([fresh1, fresh2, fresh3] as any) + + mockExecute + .mockResolvedValueOnce({ x: 0, y: 100, width: 390, height: 50 }) + .mockResolvedValueOnce({ x: 0, y: 200, width: 390, height: 50 }) + .mockResolvedValueOnce({ x: 0, y: 300, width: 390, height: 50 }) + + const result = await determineWebScreenIgnoreRegions(iosOptions, [[el1, el2, el3]]) + + // $$ called once for the shared selector, not $ three times + expect(mockBrowserInstance.$$).toHaveBeenCalledTimes(1) + expect(mockBrowserInstance.$$).toHaveBeenCalledWith('.card') + // execute called with each fresh element + expect(mockExecute).toHaveBeenCalledTimes(3) + // Each region has different y (viewport offset 94 added) + expect(result).toEqual([ + { x: 0, y: 194, width: 390, height: 50 }, + { x: 0, y: 294, width: 390, height: 50 }, + { x: 0, y: 394, width: 390, height: 50 }, + ]) + }) + + it('should add device-pixel viewport offset on Android native web screenshot', async () => { + const androidDeviceRectangles = { + ...baseDeviceRectangles, + // On Android, viewport offset is already in device pixels + // (injectWebviewOverlay pre-scales by DPR) + viewport: { y: 240, x: 0, width: 1236, height: 1956 }, + } + const androidOptions = { + ...desktopOptions, + devicePixelRatio: 3, + deviceRectangles: androidDeviceRectangles, + isAndroid: true, + isAndroidNativeWebScreenshot: true, + } + const mockElement = { elementId: 'el1', selector: '#header' } as WebdriverIO.Element + vi.mocked(mockBrowserInstance.$$).mockResolvedValueOnce([mockElement] as any) + mockExecute.mockResolvedValueOnce({ x: 0, y: 0, width: 412, height: 64 }) + + const result = await determineWebScreenIgnoreRegions(androidOptions, [mockElement]) + + // BCR × DPR + viewport (already device px): + // x: 0*3 + 0 = 0, y: 0*3 + 240 = 240, w: 412*3 = 1236, h: 64*3 = 192 + expect(result).toEqual([ + { x: 0, y: 240, width: 1236, height: 192 }, + ]) + }) + + it('should NOT add viewport offset on Android ChromeDriver screenshot', async () => { + const androidChromeOptions = { + ...desktopOptions, + devicePixelRatio: 3, + isAndroid: true, + isAndroidNativeWebScreenshot: false, + } + const mockElement = { elementId: 'el1', selector: '#header' } as WebdriverIO.Element + vi.mocked(mockBrowserInstance.$$).mockResolvedValueOnce([mockElement] as any) + mockExecute.mockResolvedValueOnce({ x: 0, y: 0, width: 412, height: 64 }) + + const result = await determineWebScreenIgnoreRegions(androidChromeOptions, [mockElement]) + + // BCR × DPR only, no viewport offset + expect(result).toEqual([ + { x: 0, y: 0, width: 1236, height: 192 }, + ]) + }) + + it('should apply DPR to coordinate regions as well', async () => { + const region = { x: 10, y: 20, width: 100, height: 150 } + + const result = await determineWebScreenIgnoreRegions(desktopOptions, [region]) + + expect(mockExecute).not.toHaveBeenCalled() + expect(result).toEqual([ + { x: 20, y: 40, width: 200, height: 300 }, + ]) + }) + + it('should handle mixed elements and regions with DPR applied to both', async () => { + const mockElement = { elementId: 'el1', selector: '.ad' } as WebdriverIO.Element + vi.mocked(mockBrowserInstance.$$).mockResolvedValueOnce([mockElement] as any) + const region = { x: 500, y: 0, width: 200, height: 90 } + mockExecute.mockResolvedValueOnce({ x: 10, y: 20, width: 300, height: 80 }) + + const result = await determineWebScreenIgnoreRegions(desktopOptions, [mockElement, region]) + + expect(result).toEqual([ + { x: 1000, y: 0, width: 400, height: 180 }, + { x: 20, y: 40, width: 600, height: 160 }, + ]) + }) + + it('should handle empty array', async () => { + const result = await determineWebScreenIgnoreRegions(desktopOptions, []) + + expect(result).toEqual([]) + expect(mockExecute).not.toHaveBeenCalled() + }) + + it('should handle chainable promise elements', async () => { + const chainableElement = Promise.resolve({ elementId: 'el1', selector: '.footer' } as WebdriverIO.Element) + const freshElement = { elementId: 'el1-fresh', selector: '.footer' } as unknown as WebdriverIO.Element + vi.mocked(mockBrowserInstance.$$).mockResolvedValueOnce([freshElement] as any) + mockExecute.mockResolvedValueOnce({ x: 0, y: 900, width: 1200, height: 100 }) + + const result = await determineWebScreenIgnoreRegions(desktopOptions, [chainableElement as any]) + + expect(result).toEqual([ + { x: 0, y: 1800, width: 2400, height: 200 }, + ]) + }) + + it('should use floor/ceil rounding on sub-pixel BCR values to fully cover elements', async () => { + const mockElement = { elementId: 'el1', selector: '.banner' } as WebdriverIO.Element + vi.mocked(mockBrowserInstance.$$).mockResolvedValueOnce([mockElement] as any) + // Sub-pixel BCR values that would lose precision if rounded independently + mockExecute.mockResolvedValueOnce({ x: 0.33, y: 50.67, width: 412.5, height: 64.33 }) + + const opts = { ...desktopOptions, devicePixelRatio: 3 } + const result = await determineWebScreenIgnoreRegions(opts, [mockElement]) + + // Position uses floor, far-edge uses ceil: + // x: floor(0.33*3) = floor(0.99) = 0 + // y: floor(50.67*3) = floor(152.01) = 152 + // right: ceil((0.33+412.5)*3) = ceil(1238.49) = 1239 → w = 1239-0 = 1239 + // bottom: ceil((50.67+64.33)*3) = ceil(345.0) = 345 → h = 345-152 = 193 + expect(result).toEqual([ + { x: 0, y: 152, width: 1239, height: 193 }, + ]) + }) + + it('should throw on invalid ignore items', async () => { + await expect( + determineWebScreenIgnoreRegions(desktopOptions, ['invalid' as any]) + ).rejects.toThrow('Invalid elements or regions') + }) + + it('should expand regions by ignoreRegionPadding (default 1) on each side', async () => { + const region = { x: 10, y: 20, width: 100, height: 50 } + const optionsWithDefaultPadding = { + ...desktopOptions, + ignoreRegionPadding: 1, + } + + const result = await determineWebScreenIgnoreRegions(optionsWithDefaultPadding, [region]) + + expect(mockExecute).not.toHaveBeenCalled() + // (10,20,100,50) × DPR 2 → (20,40,200,100); + padding 1 each side → (19,39,202,102) + expect(result).toEqual([ + { x: 19, y: 39, width: 202, height: 102 }, + ]) + }) + + it('should use custom ignoreRegionPadding when provided for screen', async () => { + const region = { x: 0, y: 0, width: 50, height: 20 } + const optionsWithPadding2 = { + ...desktopOptions, + ignoreRegionPadding: 2, + } + + const result = await determineWebScreenIgnoreRegions(optionsWithPadding2, [region]) + + // (0,0,50,20) × 2 → (0,0,100,40); + padding 2 → (0,0,104,44) + expect(result).toEqual([ + { x: 0, y: 0, width: 104, height: 44 }, + ]) + }) + }) + + describe('determineWebFullPageIgnoreRegions', () => { + const fullPageOptions = { + browserInstance: null as unknown as WebdriverIO.Browser, + devicePixelRatio: 2, + ignoreRegionPadding: 0, + } + + beforeEach(() => { + fullPageOptions.browserInstance = mockBrowserInstance + }) + + it('should resolve elements via document BCR (BCR + scroll) and apply DPR', async () => { + const mockElement = { elementId: 'el1', selector: '.nav' } as WebdriverIO.Element + const freshElement = { elementId: 'el1-fresh', selector: '.nav' } as unknown as WebdriverIO.Element + vi.mocked(mockBrowserInstance.$$).mockResolvedValueOnce([freshElement] as any) + // rawDocumentBcr returns getBoundingClientRect() + (scrollX, scrollY) = document-relative CSS pixels + mockExecute.mockResolvedValueOnce({ x: 10, y: 1200, width: 200, height: 50 }) + + const result = await determineWebFullPageIgnoreRegions(fullPageOptions, [mockElement]) + + expect(mockBrowserInstance.$$).toHaveBeenCalledWith('.nav') + expect(mockExecute).toHaveBeenCalledOnce() + // Document CSS (10, 1200, 200, 50) × DPR 2 → device pixels (20, 2400, 400, 100) + expect(result).toEqual([ + { x: 20, y: 2400, width: 400, height: 100 }, + ]) + }) + + it('should treat raw regions as document-relative CSS pixels and apply DPR', async () => { + const region = { x: 0, y: 500, width: 300, height: 80 } + + const result = await determineWebFullPageIgnoreRegions(fullPageOptions, [region]) + + expect(mockExecute).not.toHaveBeenCalled() + expect(result).toEqual([ + { x: 0, y: 1000, width: 600, height: 160 }, + ]) + }) + + it('should expand regions by ignoreRegionPadding', async () => { + const region = { x: 10, y: 20, width: 100, height: 50 } + const optionsWithPadding = { + ...fullPageOptions, + ignoreRegionPadding: 1, + } + + const result = await determineWebFullPageIgnoreRegions(optionsWithPadding, [region]) + + // (10,20,100,50) × 2 → (20,40,200,100); + padding 1 → (19,39,202,102) + expect(result).toEqual([ + { x: 19, y: 39, width: 202, height: 102 }, + ]) + }) + + it('should return empty array when ignores is empty', async () => { + const result = await determineWebFullPageIgnoreRegions(fullPageOptions, []) + + expect(result).toEqual([]) + }) + + it('should subtract fullPageCropTopPaddingCSS from y for mobile scroll-and-stitch alignment', async () => { + const region = { x: 0, y: 100, width: 300, height: 80 } + const optionsWithCropTop = { + ...fullPageOptions, + devicePixelRatio: 3, + fullPageCropTopPaddingCSS: 6, + } + + const result = await determineWebFullPageIgnoreRegions(optionsWithCropTop, [region]) + + // document (0, 100, 300, 80) with cropTop 6 → canvas y = (100-6)*3 = 282, height = 80*3 = 240 + expect(mockExecute).not.toHaveBeenCalled() + expect(result).toEqual([ + { x: 0, y: 282, width: 900, height: 240 }, + ]) + }) + }) + + describe('determineWebElementIgnoreRegions', () => { + it('should resolve element-local regions and apply DPR', async () => { + const rootElement = { elementId: 'root', selector: '.root' } as WebdriverIO.Element + const childElement = { elementId: 'child', selector: '.child' } as WebdriverIO.Element + const freshChild = { elementId: 'child-fresh', selector: '.child' } as unknown as WebdriverIO.Element + + vi.mocked(mockBrowserInstance.$$).mockResolvedValueOnce([freshChild] as any) + // Simulate already-relative BCR from execute: (20,30,100,40) + mockExecute.mockResolvedValueOnce({ x: 20, y: 30, width: 100, height: 40 }) + + const result = await determineWebElementIgnoreRegions({ + browserInstance: mockBrowserInstance as unknown as WebdriverIO.Browser, + devicePixelRatio: 2, + rootElement, + ignoreRegionPadding: 0, + }, [childElement]) + + // CSS: (20,30,100,40) × DPR(2) → (40,60,200,80) + expect(result).toEqual([ + { x: 40, y: 60, width: 200, height: 80 }, + ]) + }) + + it('should pass through literal regions (CSS relative to element) with DPR applied', async () => { + const rootElement = { elementId: 'root', selector: '.root' } as WebdriverIO.Element + const region = { x: 5, y: 10, width: 50, height: 20 } + + const result = await determineWebElementIgnoreRegions({ + browserInstance: mockBrowserInstance as unknown as WebdriverIO.Browser, + devicePixelRatio: 2, + rootElement, + ignoreRegionPadding: 0, + }, [region]) + + expect(mockExecute).not.toHaveBeenCalled() + // (5,10,50,20) × 2 → (10,20,100,40) + expect(result).toEqual([ + { x: 10, y: 20, width: 100, height: 40 }, + ]) + }) + + it('should expand regions by ignoreRegionPadding (default 1) on each side', async () => { + const rootElement = { elementId: 'root', selector: '.root' } as WebdriverIO.Element + const region = { x: 10, y: 20, width: 100, height: 40 } + + const result = await determineWebElementIgnoreRegions({ + browserInstance: mockBrowserInstance as unknown as WebdriverIO.Browser, + devicePixelRatio: 2, + rootElement, + ignoreRegionPadding: 1, + }, [region]) + + expect(mockExecute).not.toHaveBeenCalled() + // (10,20,100,40) × 2 → (20,40,200,80); + padding 1 each side → (19,39,202,82) + expect(result).toEqual([ + { x: 19, y: 39, width: 202, height: 82 }, + ]) + }) + + it('should use custom ignoreRegionPadding when provided', async () => { + const rootElement = { elementId: 'root', selector: '.root' } as WebdriverIO.Element + const region = { x: 0, y: 0, width: 50, height: 20 } + + const result = await determineWebElementIgnoreRegions({ + browserInstance: mockBrowserInstance as unknown as WebdriverIO.Browser, + devicePixelRatio: 1, + rootElement, + ignoreRegionPadding: 2, + }, [region]) + + // (0,0,50,20) + padding 2 each side → (0,0,54,24) — x,y clamped to 0 + expect(result).toEqual([ + { x: 0, y: 0, width: 54, height: 24 }, + ]) + }) + + it('should handle empty ignores', async () => { + const rootElement = { elementId: 'root', selector: '.root' } as WebdriverIO.Element + + const result = await determineWebElementIgnoreRegions({ + browserInstance: mockBrowserInstance as unknown as WebdriverIO.Browser, + devicePixelRatio: 2, + rootElement, + ignoreRegionPadding: 0, + }, []) + + expect(result).toEqual([]) + expect(mockExecute).not.toHaveBeenCalled() + }) + + it('should output CSS-pixel regions when isAndroidNativeWebScreenshot and isWebDriverElementScreenshot (native driver image at CSS size)', async () => { + const rootElement = { elementId: 'root', selector: '.root' } as WebdriverIO.Element + const region = { x: 10, y: 20, width: 100, height: 40 } + + const result = await determineWebElementIgnoreRegions({ + browserInstance: mockBrowserInstance as unknown as WebdriverIO.Browser, + devicePixelRatio: 2, + rootElement, + ignoreRegionPadding: 0, + isAndroidNativeWebScreenshot: true, + isWebDriverElementScreenshot: true, + }, [region]) + + expect(mockExecute).not.toHaveBeenCalled() + // Device px: (10,20,100,40) × 2 → (20,40,200,80); then downscale to CSS for native driver image → (10,20,100,40) + expect(result).toEqual([ + { x: 10, y: 20, width: 100, height: 40 }, + ]) + }) + }) + describe('determineDeviceBlockOuts', () => { it('should return empty array when no blockouts are enabled', async () => { const options = createDeviceBlockOutsOptions() diff --git a/packages/image-comparison-core/src/methods/rectangles.ts b/packages/image-comparison-core/src/methods/rectangles.ts index a88ca7187..85ee7d7a6 100644 --- a/packages/image-comparison-core/src/methods/rectangles.ts +++ b/packages/image-comparison-core/src/methods/rectangles.ts @@ -3,6 +3,9 @@ import { calculateDprData, getBase64ScreenshotSize, isObject } from '../helpers/ import { getElementPositionAndroid, getElementPositionDesktop, getElementWebviewPosition } from './elementPosition.js' import type { DetermineDeviceBlockOutsOptions, + DetermineWebFullPageIgnoreRegionsOptions, + DetermineWebScreenIgnoreRegionsOptions, + DetermineWebElementIgnoreRegionsOptions, DeviceRectangles, ElementRectangles, PrepareIgnoreRectanglesOptions, @@ -254,7 +257,7 @@ export async function getRegionsFromElements(browserInstance: WebdriverIO.Browse */ export async function determineIgnoreRegions( browserInstance: WebdriverIO.Browser, - ignores: ElementIgnore[], + ignores: (ElementIgnore | ElementIgnore[])[], ): Promise{ const awaitedIgnores = await Promise.all(ignores) const { elements, regions } = splitIgnores(awaitedIgnores) @@ -269,6 +272,266 @@ export async function determineIgnoreRegions( })) } +/** + * Translate ignores to regions for web screen (viewport) screenshots. + * Uses getBoundingClientRect (CSS pixels) and converts to device pixels, + * accounting for the viewport offset on native web screenshot devices. + * + * Coordinate systems per platform: + * - Desktop / Android ChromeDriver: screenshot is viewport-only, BCR × DPR + * - iOS: full-device screenshot, viewport offset is in CSS points → (BCR + offset) × DPR + * - Android native web: full-device screenshot, viewport offset is already in + * device pixels (injectWebviewOverlay pre-scales by DPR) → BCR × DPR + offset + */ +export async function determineWebScreenIgnoreRegions( + options: DetermineWebScreenIgnoreRegionsOptions, + ignores: (ElementIgnore | ElementIgnore[])[], +): Promise { + const awaitedIgnores = await Promise.all(ignores) + const { elements, regions } = splitIgnores(awaitedIgnores) + const { browserInstance, devicePixelRatio, deviceRectangles, isAndroid, isAndroidNativeWebScreenshot, isIOS, ignoreRegionPadding: padding } = options + + // Get raw (unrounded) BCR values so we can multiply by DPR before + // rounding. The shared getBoundingClientRect script pre-rounds to CSS + // integers which loses sub-pixel precision that matters at higher DPRs. + const rawBcr = (el: HTMLElement) => { + const rect = el.getBoundingClientRect() + return { x: rect.x, y: rect.y, width: rect.width, height: rect.height } + } + // Browsers can invalidate element references when the DOM is mutated + // (e.g. by beforeScreenshot CSS/style injection). Re-query via $$ to + // get fresh refs. Use $$ per unique selector so multiple elements + // sharing the same selector (e.g. from a $$ call) each resolve to + // the correct match by index. + const regionsFromElements: RectanglesOutput[] = [] + const selectorCache = new Map() + const selectorIndex = new Map() + + for (const element of elements) { + const selector = element.selector as string + + if (!selectorCache.has(selector)) { + const fresh = await browserInstance.$$(selector) + selectorCache.set(selector, fresh as unknown as WebdriverIO.Element[]) + selectorIndex.set(selector, 0) + } + + const idx = selectorIndex.get(selector)! + const cached = selectorCache.get(selector)! + const el = idx < cached.length ? cached[idx] : element + selectorIndex.set(selector, idx + 1) + + const bcr = await browserInstance.execute(rawBcr, el as any) as RectanglesOutput + regionsFromElements.push(bcr) + } + + return [...regions, ...regionsFromElements] + .map((region: RectanglesOutput) => { + // Use floor for top-left and ceil for bottom-right so the + // device-pixel rectangle fully covers the CSS-pixel element. + // Rounding position and size independently can miss edge pixels. + let cssX = region.x + let cssY = region.y + + if (isIOS) { + cssX += deviceRectangles.viewport.x + cssY += deviceRectangles.viewport.y + } + + const left = Math.floor(cssX * devicePixelRatio) + const top = Math.floor(cssY * devicePixelRatio) + const right = Math.ceil((cssX + region.width) * devicePixelRatio) + const bottom = Math.ceil((cssY + region.height) * devicePixelRatio) + + let x = left + let y = top + + if (isAndroid && isAndroidNativeWebScreenshot) { + // Android native web viewport offset is already in device pixels + x += deviceRectangles.viewport.x + y += deviceRectangles.viewport.y + } + + let width = right - left + let height = bottom - top + if (padding > 0) { + x = Math.max(0, x - padding) + y = Math.max(0, y - padding) + width += 2 * padding + height += 2 * padding + } + return { x, y, width, height } + }) +} + +/** + * Translate ignores to regions for web full-page screenshots (desktop and mobile). + * Full-page image (BiDi or scroll-and-stitch) is in document coordinates: (0,0) = top-left + * of document, device pixels. Uses getBoundingClientRect + (scrollX, scrollY) for elements, + * then converts to device pixels. Same logic for all platforms; no viewport offset needed + * because the stitched canvas is built in document space. + */ +export async function determineWebFullPageIgnoreRegions( + options: DetermineWebFullPageIgnoreRegionsOptions, + ignores: (ElementIgnore | ElementIgnore[])[], +): Promise { + const awaitedIgnores = await Promise.all(ignores) + const { elements, regions } = splitIgnores(awaitedIgnores) + const { browserInstance, devicePixelRatio, ignoreRegionPadding: padding, fullPageCropTopPaddingCSS: cropTop = 0 } = options + + const rawDocumentBcr = (el: HTMLElement) => { + const rect = el.getBoundingClientRect() + return { + x: rect.x + window.scrollX, + y: rect.y + window.scrollY, + width: rect.width, + height: rect.height, + } + } + + const regionsFromElements: RectanglesOutput[] = [] + const selectorCache = new Map() + const selectorIndex = new Map() + + for (const element of elements) { + const selector = element.selector as string + + if (!selectorCache.has(selector)) { + const fresh = await browserInstance.$$(selector) + selectorCache.set(selector, fresh as unknown as WebdriverIO.Element[]) + selectorIndex.set(selector, 0) + } + + const idx = selectorIndex.get(selector)! + const cached = selectorCache.get(selector)! + const el = idx < cached.length ? cached[idx] : element + selectorIndex.set(selector, idx + 1) + + const bcr = await browserInstance.execute(rawDocumentBcr, el as any) as RectanglesOutput + regionsFromElements.push(bcr) + } + + return [...regions, ...regionsFromElements] + .map((region: RectanglesOutput) => { + const left = Math.floor(region.x * devicePixelRatio) + const right = Math.ceil((region.x + region.width) * devicePixelRatio) + // On mobile full-page scroll-and-stitch, the canvas crops cropTop (e.g. 6px) from the top + // of each tile, so canvas y = (documentY - cropTop) × DPR + const topDevice = Math.floor((region.y - cropTop) * devicePixelRatio) + const bottomDevice = Math.ceil((region.y + region.height - cropTop) * devicePixelRatio) + const top = Math.max(0, topDevice) + const bottom = Math.max(top, bottomDevice) + + let x = left + let y = top + let width = right - left + let height = bottom - top + if (padding > 0) { + x = Math.max(0, x - padding) + y = Math.max(0, y - padding) + width += 2 * padding + height += 2 * padding + } + return { x, y, width, height } + }) +} + +/** + * Translate ignores to regions for web element screenshots. + * By default regions are in *element-local* device pixels so they match the cropped element image + * (BiDi clip or fallback full-screenshot crop, both at device pixel size). + * Exception: when the element screenshot is from the native driver on Android native web + * (isWebDriverElementScreenshot && isAndroidNativeWebScreenshot), the driver returns an image at + * CSS pixel size (downscaled). We then output regions in CSS pixel coordinates (divide by DPR) + * so they align with that image. Fallback (full screenshot + crop) is at device size, so we do + * not downscale when fallback was used. + */ +export async function determineWebElementIgnoreRegions( + options: DetermineWebElementIgnoreRegionsOptions, + ignores: (ElementIgnore | ElementIgnore[])[], +): Promise { + const awaitedIgnores = await Promise.all(ignores) + const { elements, regions } = splitIgnores(awaitedIgnores) + const { + browserInstance, + devicePixelRatio, + rootElement, + ignoreRegionPadding: padding, + isAndroidNativeWebScreenshot, + isWebDriverElementScreenshot, + } = options + + // Compute bounding boxes relative to the root element: (childBCR - rootBCR) + const rawRelativeBcr = (el: HTMLElement, root: HTMLElement) => { + const elRect = el.getBoundingClientRect() + const rootRect = root.getBoundingClientRect() + + return { + x: elRect.x - rootRect.x, + y: elRect.y - rootRect.y, + width: elRect.width, + height: elRect.height, + } + } + + const regionsFromElements: RectanglesOutput[] = [] + const selectorCache = new Map() + const selectorIndex = new Map() + + for (const element of elements) { + const selector = element.selector as string + + if (!selectorCache.has(selector)) { + const fresh = await browserInstance.$$(selector) + selectorCache.set(selector, fresh as unknown as WebdriverIO.Element[]) + selectorIndex.set(selector, 0) + } + + const idx = selectorIndex.get(selector)! + const cached = selectorCache.get(selector)! + const el = idx < cached.length ? cached[idx] : element + selectorIndex.set(selector, idx + 1) + + const bcr = await browserInstance.execute(rawRelativeBcr, el as any, rootElement as any) as RectanglesOutput + regionsFromElements.push(bcr) + } + + // Both literal regions and element-derived regions are currently expected + // to be in CSS pixels relative to the element. Scale everything by DPR and + // express as device-pixel rectangles using the same floor-based rounding + // strategy as the BiDi element clip (x/y/width/height all floored). + // Then expand each region by ignoreRegionPadding on each side (configurable, default 1) + // to reduce 1px boundary differences on high-DPR / BiDi. + let result = [...regions, ...regionsFromElements] + .map((region: RectanglesOutput) => { + let x = Math.floor(region.x * devicePixelRatio) + let y = Math.floor(region.y * devicePixelRatio) + let width = Math.floor(region.width * devicePixelRatio) + let height = Math.floor(region.height * devicePixelRatio) + if (padding > 0) { + x = Math.max(0, x - padding) + y = Math.max(0, y - padding) + width += 2 * padding + height += 2 * padding + } + return { x, y, width, height } + }) + + // Only downscale when the element image is at CSS pixel size: native driver element screenshot + // on Android native web (fallback false). Fallback uses a device-pixel crop, so no downscale. + if (isAndroidNativeWebScreenshot === true && isWebDriverElementScreenshot === true && devicePixelRatio > 0) { + const dpr = devicePixelRatio + result = result.map((r) => ({ + x: Math.round(r.x / dpr), + y: Math.round(r.y / dpr), + width: Math.round(r.width / dpr), + height: Math.round(r.height / dpr), + })) + } + + return result +} + /** * Determine the device block outs */ @@ -375,33 +638,38 @@ export async function prepareIgnoreRectangles(options: PrepareIgnoreRectanglesOp } } - // Combine all ignore regions - const ignoredBoxes = [ - // These come from the method + // blockOut and device bar rectangles are in CSS pixels, scale by DPR + const dprScaledBoxes = [ ...blockOut, - // @TODO: I'm defaulting ignore regions for devices - // Need to check if this is the right thing to do for web and mobile browser tests - ...ignoreRegions, - // Only get info about the status bars when we are in the web context ...webStatusAddressToolBarOptions ] .map( - // Make sure all the rectangles are equal to the dpr for the screenshot (rectangles) => { return calculateDprData( { - // Adjust for the ResembleJS API bottom: rectangles.y + rectangles.height, right: rectangles.x + rectangles.width, left: rectangles.x, top: rectangles.y, }, - // For Android we don't need to do it times the pixel ratio, for all others we need to isAndroid ? 1 : devicePixelRatio, ) }, ) + // ignoreRegions are already in device pixels (pre-scaled by the caller), + // only convert to the ResembleJS format (top/left/bottom/right) + const preScaledIgnoreBoxes = ignoreRegions.map( + (rectangles) => ({ + bottom: rectangles.y + rectangles.height, + right: rectangles.x + rectangles.width, + left: rectangles.x, + top: rectangles.y, + }), + ) + + const ignoredBoxes = [...dprScaledBoxes, ...preScaledIgnoreBoxes] + return { ignoredBoxes, hasIgnoreRectangles: ignoredBoxes.length > 0 diff --git a/packages/image-comparison-core/src/methods/takeElementScreenshots.test.ts b/packages/image-comparison-core/src/methods/takeElementScreenshots.test.ts index 820c5738c..36f6711b9 100644 --- a/packages/image-comparison-core/src/methods/takeElementScreenshots.test.ts +++ b/packages/image-comparison-core/src/methods/takeElementScreenshots.test.ts @@ -83,7 +83,7 @@ describe('takeElementScreenshot', () => { }) describe('BiDi screenshots', () => { - it('should take BiDi screenshot from viewport when shouldUseBidi is true', async () => { + it('should take BiDi screenshot from document when shouldUseBidi is true', async () => { const result = await takeElementScreenshot(browserInstance, baseOptions, true) expect(result).toEqual({ @@ -93,44 +93,43 @@ describe('takeElementScreenshot', () => { expect(getElementRectMock).toHaveBeenCalledWith('test-element') expect(takeBase64BiDiScreenshotSpy).toHaveBeenCalledWith({ browserInstance, - origin: 'viewport', + origin: 'document', clip: { x: 10, y: 20, width: 100, height: 200 } }) expect(takeWebElementScreenshotSpy).not.toHaveBeenCalled() expect(makeCroppedBase64ImageSpy).not.toHaveBeenCalled() }) + // + // We intentionally rely on BiDi with origin: 'document' only. If that + // ever fails, we surface the underlying error instead of silently + // falling back to a different origin with mismatched coordinates. - it('should fallback to document screenshot when viewport fails with zero dimensions error', async () => { - takeBase64BiDiScreenshotSpy.mockRejectedValueOnce( - new Error('WebDriver Bidi command "browsingContext.captureScreenshot" failed with error: unable to capture screen - Unable to capture screenshot with zero dimensions') - ) + it('should scroll element into view when autoElementScroll is enabled', async () => { + const optionsWithScroll = { ...baseOptions, autoElementScroll: true } + executeMock.mockResolvedValueOnce(100) // previous scroll position - const result = await takeElementScreenshot(browserInstance, baseOptions, true) + const result = await takeElementScreenshot(browserInstance, optionsWithScroll, true) expect(result).toEqual({ base64Image: 'bidi-screenshot-data', isWebDriverElementScreenshot: false }) - expect(takeBase64BiDiScreenshotSpy).toHaveBeenCalledTimes(2) - expect(takeBase64BiDiScreenshotSpy.mock.calls[0][0]).toEqual({ - browserInstance, - origin: 'viewport', - clip: { x: 10, y: 20, width: 100, height: 200 } - }) - expect(takeBase64BiDiScreenshotSpy.mock.calls[1][0]).toEqual({ - browserInstance, - origin: 'document', - clip: { x: 10, y: 20, width: 100, height: 200 } - }) + // First call: scrollElementIntoView, second call: scrollToPosition (restore) + expect(executeMock).toHaveBeenCalledTimes(2) + expect(executeMock.mock.calls[0]).toMatchSnapshot() + expect(executeMock.mock.calls[1]).toMatchSnapshot() + expect(waitForSpy).toHaveBeenCalledWith(100) }) - it('should throw error when BiDi screenshot fails with non-zero dimension error', async () => { - const error = new Error('Some other BiDi error') - takeBase64BiDiScreenshotSpy.mockRejectedValueOnce(error) + it('should not restore scroll when autoElementScroll is enabled but no previous position', async () => { + const optionsWithScroll = { ...baseOptions, autoElementScroll: true } + executeMock.mockResolvedValueOnce(undefined) // no previous position - await expect(takeElementScreenshot(browserInstance, baseOptions, true)).rejects.toThrow(error) - expect(takeBase64BiDiScreenshotSpy).toHaveBeenCalledTimes(1) - expect(takeWebElementScreenshotSpy).not.toHaveBeenCalled() + await takeElementScreenshot(browserInstance, optionsWithScroll, true) + + // Only the scrollElementIntoView call, no restore + expect(executeMock).toHaveBeenCalledTimes(1) + expect(waitForSpy).toHaveBeenCalledWith(100) }) }) diff --git a/packages/image-comparison-core/src/methods/takeElementScreenshots.ts b/packages/image-comparison-core/src/methods/takeElementScreenshots.ts index fd7fb7650..0e4de5e8a 100644 --- a/packages/image-comparison-core/src/methods/takeElementScreenshots.ts +++ b/packages/image-comparison-core/src/methods/takeElementScreenshots.ts @@ -23,30 +23,31 @@ async function takeBiDiElementScreenshot( browserInstance: WebdriverIO.Browser, options: ElementScreenshotDataOptions ): Promise { - let base64Image: string const isWebDriverElementScreenshot = false - // We also need to clip the image to the element size, taking into account the DPR - // and also clip it from the document, not the viewport + // Scroll the element into the viewport so any lazy‑load / intersection + // observers are triggered. We always capture from the *document* origin, + // so the clip coordinates are document‑relative and independent of scroll. + let currentPosition: number | undefined + if (options.autoElementScroll) { + currentPosition = await browserInstance.execute(scrollElementIntoView as any, options.element, options.addressBarShadowPadding) + await waitFor(100) + } + + // Get the element rect and clip the screenshot. WebDriver getElementRect + // returns coordinates relative to the document origin, which matches the + // BiDi `origin: 'document'` coordinate system. const rect = await browserInstance.getElementRect!((await options.element as WebdriverIO.Element).elementId) const clip = { x: Math.floor(rect.x), y: Math.floor(rect.y), width: Math.floor(rect.width), height: Math.floor(rect.height) } - const takeBiDiElementScreenshot = (origin: 'document' | 'viewport') => takeBase64BiDiScreenshot({ browserInstance, origin, clip }) - - try { - // By default we take the screenshot from the viewport - base64Image = await takeBiDiElementScreenshot('viewport') - } catch (err: any) { - // But when we get a zero dimension error (meaning the element might be bigger than the - // viewport or it might not be in the viewport), we need to take the screenshot from the document. - const isZeroDimensionError = typeof err?.message === 'string' && err.message.includes( - 'WebDriver Bidi command "browsingContext.captureScreenshot" failed with error: unable to capture screen - Unable to capture screenshot with zero dimensions' - ) - - if (!isZeroDimensionError) { - throw err - } - - base64Image = await takeBiDiElementScreenshot('document') + const base64Image = await takeBase64BiDiScreenshot({ + browserInstance, + origin: 'document', + clip, + }) + + // Restore scroll position + if (options.autoElementScroll && currentPosition) { + await browserInstance.execute(scrollToPosition, currentPosition) } return { diff --git a/packages/visual-service/src/types.ts b/packages/visual-service/src/types.ts index 6a61544a7..29088027e 100644 --- a/packages/visual-service/src/types.ts +++ b/packages/visual-service/src/types.ts @@ -19,8 +19,9 @@ import type { InternalCheckScreenMethodOptions, InternalCheckElementMethodOptions, InternalCheckFullPageMethodOptions, + ElementIgnore, } from '@wdio/image-comparison-core' -import type { ChainablePromiseElement } from 'webdriverio' +import type { ChainablePromiseElement, ChainablePromiseArray } from 'webdriverio' import type { ContextManager } from './contextManager.js' import type { WaitForStorybookComponentToBeLoaded } from './storybook/Types.js' @@ -82,15 +83,19 @@ export interface WdioIcsScrollOptions extends WdioIcsCommonOptions { hideAfterFirstScroll?: (WebdriverIO.Element | ChainablePromiseElement)[]; } +export interface WdioIcsIgnoreOptions { + ignore?: (ElementIgnore | ElementIgnore[] | WebdriverIO.ElementArray | ChainablePromiseArray)[]; +} + // Save methods export interface WdioSaveScreenMethodOptions extends Omit, WdioIcsCommonOptions {} export interface WdioSaveElementMethodOptions extends Omit, WdioIcsCommonOptions {} export interface WdioSaveFullPageMethodOptions extends Omit, WdioIcsScrollOptions { } // Check methods -export interface WdioCheckScreenMethodOptions extends Omit, WdioIcsCommonOptions {} -export interface WdioCheckElementMethodOptions extends Omit, WdioIcsCommonOptions {} -export interface WdioCheckFullPageMethodOptions extends Omit, WdioIcsScrollOptions {} +export interface WdioCheckScreenMethodOptions extends Omit, WdioIcsCommonOptions, WdioIcsIgnoreOptions {} +export interface WdioCheckElementMethodOptions extends Omit, WdioIcsCommonOptions, WdioIcsIgnoreOptions {} +export interface WdioCheckFullPageMethodOptions extends Omit, WdioIcsScrollOptions, WdioIcsIgnoreOptions {} export interface VisualServiceOptions extends ClassOptions { } diff --git a/packages/visual-service/src/utils.ts b/packages/visual-service/src/utils.ts index 1daddc295..8f73c7acc 100644 --- a/packages/visual-service/src/utils.ts +++ b/packages/visual-service/src/utils.ts @@ -244,7 +244,7 @@ export async function getInstanceData({ const ltOptions = getLtOptions(requestedCapabilities) // @TODO: Figure this one out in the future when we know more about the Appium capabilities from LT // 20241216: LT doesn't have the option to take a ChromeDriver screenshot, so if it's Android it's always native - const nativeWebScreenshot = isAndroid && ltOptions || !!getRequestedAppiumCapability(requestedCapabilities, 'nativeWebScreenshot') + const nativeWebScreenshot = !!(isAndroid && ltOptions) || !!getRequestedAppiumCapability(requestedCapabilities, 'nativeWebScreenshot') const platformVersion = (rawPlatformVersion === undefined || rawPlatformVersion === '') ? NOT_KNOWN : rawPlatformVersion.toLowerCase() const { devicePixelRatio: mobileDevicePixelRatio, diff --git a/packages/visual-service/tests/__snapshots__/utils.test.ts.snap b/packages/visual-service/tests/__snapshots__/utils.test.ts.snap index bb00c2d5a..98513aa6b 100644 --- a/packages/visual-service/tests/__snapshots__/utils.test.ts.snap +++ b/packages/visual-service/tests/__snapshots__/utils.test.ts.snap @@ -571,10 +571,7 @@ exports[`utils > getInstanceData > should return instance data when the lambdate "isMobile": true, "logName": "", "name": "", - "nativeWebScreenshot": { - "deviceName": "Samsung Galaxy S22 LT", - "platformVersion": "11", - }, + "nativeWebScreenshot": true, "platformName": "osx", "platformVersion": "11", } diff --git a/tests/configs/lambdatest.android.emus.web.ts b/tests/configs/lambdatest.android.emus.web.ts index 3f3922874..03721b11b 100644 --- a/tests/configs/lambdatest.android.emus.web.ts +++ b/tests/configs/lambdatest.android.emus.web.ts @@ -70,7 +70,6 @@ function createCaps({ mobileSpecs: string; build: string; deviceOrientation: DeviceOrientation; - }): { 'lt:options': { deviceName: string, @@ -80,12 +79,6 @@ function createCaps({ build: string, w3c: boolean, queueTimeout: number, - /** - * There are issues with the Chrome version on LT - * the installed versions sometimes give tabs as the first page. - * This should be fixed in the future and below is a workaround - */ - chromeVersion: 126, }, specs: string[]; 'wdio-ics:options': { @@ -93,6 +86,9 @@ function createCaps({ commands: string[]; }; 'wdio:enforceWebDriverClassic': boolean; + 'appium:chromeOptions': { + args: string[]; + }; } { const driverScreenshotType = 'NativeWebScreenshot' const adjustedDeviceName = deviceName !== '' ? @@ -119,6 +115,13 @@ function createCaps({ .toLowerCase()}${driverScreenshotType}${platformVersion}`, commands: wdioIcsCommands, }, - 'wdio:enforceWebDriverClassic': true + 'wdio:enforceWebDriverClassic': true, + 'appium:chromeOptions': { + args: [ + '--disable-features=StartSurfaceAndroid,GridTabSwitcherForTablets,TabGridLayout', + '--no-first-run', + '--disable-fre' + ] + } } } diff --git a/tests/configs/wdio.local.android.emus.web.conf.ts b/tests/configs/wdio.local.android.emus.web.conf.ts index cad43f783..68c4a3a57 100644 --- a/tests/configs/wdio.local.android.emus.web.conf.ts +++ b/tests/configs/wdio.local.android.emus.web.conf.ts @@ -12,8 +12,8 @@ export const config: WebdriverIO.Config = { // Capabilities // ============ capabilities: [ - // androidCaps('Pixel_8_Pro_Android_15_API_35', 'PORTRAIT', '15.0', true), - androidCaps('Pixel_8_Pro_Android_15_API_35', 'LANDSCAPE', '15.0', true), + androidCaps('Pixel_8_Pro_Android_15_API_35', 'PORTRAIT', '15.0', true), + // androidCaps('Pixel_8_Pro_Android_15_API_35', 'LANDSCAPE', '15.0', true), ], } @@ -36,6 +36,7 @@ function androidCaps( 'appium:orientation': orientation, 'appium:newCommandTimeout': 240, ...(nativeWebScreenshot ? { 'appium:nativeWebScreenshot': true } : {}), + 'wdio:enforceWebDriverClassic': true, 'wdio-ics:options': { logName: `${deviceName .split(' ') diff --git a/tests/configs/wdio.local.appium.shared.conf.ts b/tests/configs/wdio.local.appium.shared.conf.ts index 2d5d382a2..b75f3d7e3 100644 --- a/tests/configs/wdio.local.appium.shared.conf.ts +++ b/tests/configs/wdio.local.appium.shared.conf.ts @@ -7,22 +7,23 @@ export const config: Omit = { // =================== // Image compare setup // =================== + port: 4723, services: [ ...sharedConfig.services || [], - [ - 'appium', - { - // This will use the globally installed version of Appium - command: 'appium', - args: { - // This is needed to tell Appium that we can execute local ADB commands - // and to automatically download the latest version of ChromeDriver - relaxedSecurity: true, - // Write the Appium logs to a file in the root of the directory - log: './logs/appium.log', - }, - }, - ], + // [ + // 'appium', + // { + // // This will use the globally installed version of Appium + // command: 'appium', + // args: { + // // This is needed to tell Appium that we can execute local ADB commands + // // and to automatically download the latest version of ChromeDriver + // relaxedSecurity: true, + // // Write the Appium logs to a file in the root of the directory + // log: './logs/appium.log', + // }, + // }, + // ], [ 'visual', { diff --git a/tests/lambdaTestBaseline/desktop_chrome/bidiIgnoredElementsElementScreenshot-chrome-latest-320x658.png b/tests/lambdaTestBaseline/desktop_chrome/bidiIgnoredElementsElementScreenshot-chrome-latest-320x658.png new file mode 100644 index 000000000..839cb9bb5 Binary files /dev/null and b/tests/lambdaTestBaseline/desktop_chrome/bidiIgnoredElementsElementScreenshot-chrome-latest-320x658.png differ diff --git a/tests/lambdaTestBaseline/desktop_chrome/ignoredElementsElementScreenshot-chrome-latest-1366x768.png b/tests/lambdaTestBaseline/desktop_chrome/ignoredElementsElementScreenshot-chrome-latest-1366x768.png new file mode 100644 index 000000000..e450c0e7a Binary files /dev/null and b/tests/lambdaTestBaseline/desktop_chrome/ignoredElementsElementScreenshot-chrome-latest-1366x768.png differ diff --git a/tests/lambdaTestBaseline/desktop_chrome/ignoredElementsFullPageScreenshot-chrome-latest-1366x768.png b/tests/lambdaTestBaseline/desktop_chrome/ignoredElementsFullPageScreenshot-chrome-latest-1366x768.png new file mode 100644 index 000000000..d22b44489 Binary files /dev/null and b/tests/lambdaTestBaseline/desktop_chrome/ignoredElementsFullPageScreenshot-chrome-latest-1366x768.png differ diff --git a/tests/lambdaTestBaseline/desktop_chrome/ignoredElementsViewportScreenshot-chrome-latest-1366x768.png b/tests/lambdaTestBaseline/desktop_chrome/ignoredElementsViewportScreenshot-chrome-latest-1366x768.png new file mode 100644 index 000000000..14cd80de0 Binary files /dev/null and b/tests/lambdaTestBaseline/desktop_chrome/ignoredElementsViewportScreenshot-chrome-latest-1366x768.png differ diff --git a/tests/lambdaTestBaseline/desktop_chrome/ignoredElementsViewportScreenshot-chrome-latest-320x658.png b/tests/lambdaTestBaseline/desktop_chrome/ignoredElementsViewportScreenshot-chrome-latest-320x658.png new file mode 100644 index 000000000..84c82a271 Binary files /dev/null and b/tests/lambdaTestBaseline/desktop_chrome/ignoredElementsViewportScreenshot-chrome-latest-320x658.png differ diff --git a/tests/lambdaTestBaseline/desktop_chrome/legacyIgnoredElementsElementScreenshot-chrome-latest-320x658.png b/tests/lambdaTestBaseline/desktop_chrome/legacyIgnoredElementsElementScreenshot-chrome-latest-320x658.png new file mode 100644 index 000000000..fb910775b Binary files /dev/null and b/tests/lambdaTestBaseline/desktop_chrome/legacyIgnoredElementsElementScreenshot-chrome-latest-320x658.png differ diff --git a/tests/lambdaTestBaseline/desktop_firefox/ignoredElementsElementScreenshot-Firefox_latest-1366x768.png b/tests/lambdaTestBaseline/desktop_firefox/ignoredElementsElementScreenshot-Firefox_latest-1366x768.png new file mode 100644 index 000000000..53c01c60c Binary files /dev/null and b/tests/lambdaTestBaseline/desktop_firefox/ignoredElementsElementScreenshot-Firefox_latest-1366x768.png differ diff --git a/tests/lambdaTestBaseline/desktop_firefox/ignoredElementsFullPageScreenshot-Firefox_latest-1366x768.png b/tests/lambdaTestBaseline/desktop_firefox/ignoredElementsFullPageScreenshot-Firefox_latest-1366x768.png new file mode 100644 index 000000000..24cc8ad18 Binary files /dev/null and b/tests/lambdaTestBaseline/desktop_firefox/ignoredElementsFullPageScreenshot-Firefox_latest-1366x768.png differ diff --git a/tests/lambdaTestBaseline/desktop_firefox/ignoredElementsViewportScreenshot-Firefox_latest-1366x768.png b/tests/lambdaTestBaseline/desktop_firefox/ignoredElementsViewportScreenshot-Firefox_latest-1366x768.png new file mode 100644 index 000000000..ea5bf6fa1 Binary files /dev/null and b/tests/lambdaTestBaseline/desktop_firefox/ignoredElementsViewportScreenshot-Firefox_latest-1366x768.png differ diff --git a/tests/lambdaTestBaseline/desktop_microsoftedge/ignoredElementsElementScreenshot-Microsoft_Edge_latest-1366x768.png b/tests/lambdaTestBaseline/desktop_microsoftedge/ignoredElementsElementScreenshot-Microsoft_Edge_latest-1366x768.png new file mode 100644 index 000000000..6966c0f13 Binary files /dev/null and b/tests/lambdaTestBaseline/desktop_microsoftedge/ignoredElementsElementScreenshot-Microsoft_Edge_latest-1366x768.png differ diff --git a/tests/lambdaTestBaseline/desktop_microsoftedge/ignoredElementsFullPageScreenshot-Microsoft_Edge_latest-1366x768.png b/tests/lambdaTestBaseline/desktop_microsoftedge/ignoredElementsFullPageScreenshot-Microsoft_Edge_latest-1366x768.png new file mode 100644 index 000000000..6d6b2864d Binary files /dev/null and b/tests/lambdaTestBaseline/desktop_microsoftedge/ignoredElementsFullPageScreenshot-Microsoft_Edge_latest-1366x768.png differ diff --git a/tests/lambdaTestBaseline/desktop_microsoftedge/ignoredElementsViewportScreenshot-Microsoft_Edge_latest-1366x768.png b/tests/lambdaTestBaseline/desktop_microsoftedge/ignoredElementsViewportScreenshot-Microsoft_Edge_latest-1366x768.png new file mode 100644 index 000000000..d83506c5a Binary files /dev/null and b/tests/lambdaTestBaseline/desktop_microsoftedge/ignoredElementsViewportScreenshot-Microsoft_Edge_latest-1366x768.png differ diff --git a/tests/lambdaTestBaseline/desktop_safari/ignoredElementsElementScreenshot-SafariLatest-1366x768.png b/tests/lambdaTestBaseline/desktop_safari/ignoredElementsElementScreenshot-SafariLatest-1366x768.png new file mode 100644 index 000000000..ea7f6c9d4 Binary files /dev/null and b/tests/lambdaTestBaseline/desktop_safari/ignoredElementsElementScreenshot-SafariLatest-1366x768.png differ diff --git a/tests/lambdaTestBaseline/desktop_safari/ignoredElementsFullPageScreenshot-SafariLatest-1366x768.png b/tests/lambdaTestBaseline/desktop_safari/ignoredElementsFullPageScreenshot-SafariLatest-1366x768.png new file mode 100644 index 000000000..a13e756df Binary files /dev/null and b/tests/lambdaTestBaseline/desktop_safari/ignoredElementsFullPageScreenshot-SafariLatest-1366x768.png differ diff --git a/tests/lambdaTestBaseline/desktop_safari/ignoredElementsViewportScreenshot-SafariLatest-1366x768.png b/tests/lambdaTestBaseline/desktop_safari/ignoredElementsViewportScreenshot-SafariLatest-1366x768.png new file mode 100644 index 000000000..83c01b534 Binary files /dev/null and b/tests/lambdaTestBaseline/desktop_safari/ignoredElementsViewportScreenshot-SafariLatest-1366x768.png differ diff --git a/tests/lambdaTestBaseline/galaxy_tab_s8/ignoredElementsElementScreenshot-EmulatorGalaxyTabS8LandscapeNativeWebScreenshot13-1707x1067.png b/tests/lambdaTestBaseline/galaxy_tab_s8/ignoredElementsElementScreenshot-EmulatorGalaxyTabS8LandscapeNativeWebScreenshot13-1707x1067.png new file mode 100644 index 000000000..7e30adbfa Binary files /dev/null and b/tests/lambdaTestBaseline/galaxy_tab_s8/ignoredElementsElementScreenshot-EmulatorGalaxyTabS8LandscapeNativeWebScreenshot13-1707x1067.png differ diff --git a/tests/lambdaTestBaseline/galaxy_tab_s8/ignoredElementsElementScreenshot-EmulatorGalaxyTabS8LandscapeNativeWebScreenshot14-1707x1067.png b/tests/lambdaTestBaseline/galaxy_tab_s8/ignoredElementsElementScreenshot-EmulatorGalaxyTabS8LandscapeNativeWebScreenshot14-1707x1067.png new file mode 100644 index 000000000..7e30adbfa Binary files /dev/null and b/tests/lambdaTestBaseline/galaxy_tab_s8/ignoredElementsElementScreenshot-EmulatorGalaxyTabS8LandscapeNativeWebScreenshot14-1707x1067.png differ diff --git a/tests/lambdaTestBaseline/galaxy_tab_s8/ignoredElementsElementScreenshot-EmulatorGalaxyTabS8PortraitNativeWebScreenshot13-1067x1707.png b/tests/lambdaTestBaseline/galaxy_tab_s8/ignoredElementsElementScreenshot-EmulatorGalaxyTabS8PortraitNativeWebScreenshot13-1067x1707.png new file mode 100644 index 000000000..2637a388f Binary files /dev/null and b/tests/lambdaTestBaseline/galaxy_tab_s8/ignoredElementsElementScreenshot-EmulatorGalaxyTabS8PortraitNativeWebScreenshot13-1067x1707.png differ diff --git a/tests/lambdaTestBaseline/galaxy_tab_s8/ignoredElementsElementScreenshot-EmulatorGalaxyTabS8PortraitNativeWebScreenshot14-1067x1707.png b/tests/lambdaTestBaseline/galaxy_tab_s8/ignoredElementsElementScreenshot-EmulatorGalaxyTabS8PortraitNativeWebScreenshot14-1067x1707.png new file mode 100644 index 000000000..2637a388f Binary files /dev/null and b/tests/lambdaTestBaseline/galaxy_tab_s8/ignoredElementsElementScreenshot-EmulatorGalaxyTabS8PortraitNativeWebScreenshot14-1067x1707.png differ diff --git a/tests/lambdaTestBaseline/galaxy_tab_s8/ignoredElementsFullPageScreenshot-EmulatorGalaxyTabS8LandscapeNativeWebScreenshot13-1707x1067.png b/tests/lambdaTestBaseline/galaxy_tab_s8/ignoredElementsFullPageScreenshot-EmulatorGalaxyTabS8LandscapeNativeWebScreenshot13-1707x1067.png new file mode 100644 index 000000000..d37b75ea3 Binary files /dev/null and b/tests/lambdaTestBaseline/galaxy_tab_s8/ignoredElementsFullPageScreenshot-EmulatorGalaxyTabS8LandscapeNativeWebScreenshot13-1707x1067.png differ diff --git a/tests/lambdaTestBaseline/galaxy_tab_s8/ignoredElementsFullPageScreenshot-EmulatorGalaxyTabS8LandscapeNativeWebScreenshot14-1707x1067.png b/tests/lambdaTestBaseline/galaxy_tab_s8/ignoredElementsFullPageScreenshot-EmulatorGalaxyTabS8LandscapeNativeWebScreenshot14-1707x1067.png new file mode 100644 index 000000000..d37b75ea3 Binary files /dev/null and b/tests/lambdaTestBaseline/galaxy_tab_s8/ignoredElementsFullPageScreenshot-EmulatorGalaxyTabS8LandscapeNativeWebScreenshot14-1707x1067.png differ diff --git a/tests/lambdaTestBaseline/galaxy_tab_s8/ignoredElementsFullPageScreenshot-EmulatorGalaxyTabS8PortraitNativeWebScreenshot13-1067x1707.png b/tests/lambdaTestBaseline/galaxy_tab_s8/ignoredElementsFullPageScreenshot-EmulatorGalaxyTabS8PortraitNativeWebScreenshot13-1067x1707.png new file mode 100644 index 000000000..7faf64f1d Binary files /dev/null and b/tests/lambdaTestBaseline/galaxy_tab_s8/ignoredElementsFullPageScreenshot-EmulatorGalaxyTabS8PortraitNativeWebScreenshot13-1067x1707.png differ diff --git a/tests/lambdaTestBaseline/galaxy_tab_s8/ignoredElementsScreenshot-EmulatorGalaxyTabS8PortraitNativeWebScreenshot13-1067x1707.png b/tests/lambdaTestBaseline/galaxy_tab_s8/ignoredElementsScreenshot-EmulatorGalaxyTabS8PortraitNativeWebScreenshot13-1067x1707.png new file mode 100644 index 000000000..5572a6797 Binary files /dev/null and b/tests/lambdaTestBaseline/galaxy_tab_s8/ignoredElementsScreenshot-EmulatorGalaxyTabS8PortraitNativeWebScreenshot13-1067x1707.png differ diff --git a/tests/lambdaTestBaseline/galaxy_tab_s8/ignoredElementsScreenshot-EmulatorGalaxyTabS8PortraitNativeWebScreenshot14-1067x1707.png b/tests/lambdaTestBaseline/galaxy_tab_s8/ignoredElementsScreenshot-EmulatorGalaxyTabS8PortraitNativeWebScreenshot14-1067x1707.png new file mode 100644 index 000000000..45c5369cd Binary files /dev/null and b/tests/lambdaTestBaseline/galaxy_tab_s8/ignoredElementsScreenshot-EmulatorGalaxyTabS8PortraitNativeWebScreenshot14-1067x1707.png differ diff --git a/tests/lambdaTestBaseline/galaxy_tab_s8/screenshot-EmulatorGalaxyTabS8LandscapeNativeWebScreenshot14-1707x1067.png b/tests/lambdaTestBaseline/galaxy_tab_s8/screenshot-EmulatorGalaxyTabS8LandscapeNativeWebScreenshot14-1707x1067.png deleted file mode 100644 index 715652cdf..000000000 Binary files a/tests/lambdaTestBaseline/galaxy_tab_s8/screenshot-EmulatorGalaxyTabS8LandscapeNativeWebScreenshot14-1707x1067.png and /dev/null differ diff --git a/tests/lambdaTestBaseline/galaxy_tab_s8/screenshot-EmulatorGalaxyTabS8PortraitNativeWebScreenshot14-1067x1707.png b/tests/lambdaTestBaseline/galaxy_tab_s8/screenshot-EmulatorGalaxyTabS8PortraitNativeWebScreenshot14-1067x1707.png deleted file mode 100644 index 46e57cc1c..000000000 Binary files a/tests/lambdaTestBaseline/galaxy_tab_s8/screenshot-EmulatorGalaxyTabS8PortraitNativeWebScreenshot14-1067x1707.png and /dev/null differ diff --git a/tests/lambdaTestBaseline/iphone_13_mini/ignoredElementsElementScreenshot-Iphone13MiniLandscape17-375x812.png b/tests/lambdaTestBaseline/iphone_13_mini/ignoredElementsElementScreenshot-Iphone13MiniLandscape17-375x812.png new file mode 100644 index 000000000..a0bd1e81a Binary files /dev/null and b/tests/lambdaTestBaseline/iphone_13_mini/ignoredElementsElementScreenshot-Iphone13MiniLandscape17-375x812.png differ diff --git a/tests/lambdaTestBaseline/iphone_13_mini/ignoredElementsElementScreenshot-Iphone13MiniPortrait17-375x812.png b/tests/lambdaTestBaseline/iphone_13_mini/ignoredElementsElementScreenshot-Iphone13MiniPortrait17-375x812.png new file mode 100644 index 000000000..adeba0008 Binary files /dev/null and b/tests/lambdaTestBaseline/iphone_13_mini/ignoredElementsElementScreenshot-Iphone13MiniPortrait17-375x812.png differ diff --git a/tests/lambdaTestBaseline/iphone_13_mini/ignoredElementsScreenshot-Iphone13MiniPortrait17-375x812.png b/tests/lambdaTestBaseline/iphone_13_mini/ignoredElementsScreenshot-Iphone13MiniPortrait17-375x812.png new file mode 100644 index 000000000..865335b5a Binary files /dev/null and b/tests/lambdaTestBaseline/iphone_13_mini/ignoredElementsScreenshot-Iphone13MiniPortrait17-375x812.png differ diff --git a/tests/lambdaTestBaseline/iphone_13_pro/ignoredElementsElementScreenshot-Iphone13ProLandscape16-390x844.png b/tests/lambdaTestBaseline/iphone_13_pro/ignoredElementsElementScreenshot-Iphone13ProLandscape16-390x844.png new file mode 100644 index 000000000..e79b4bbd0 Binary files /dev/null and b/tests/lambdaTestBaseline/iphone_13_pro/ignoredElementsElementScreenshot-Iphone13ProLandscape16-390x844.png differ diff --git a/tests/lambdaTestBaseline/iphone_13_pro/ignoredElementsElementScreenshot-Iphone13ProPortrait16-390x844.png b/tests/lambdaTestBaseline/iphone_13_pro/ignoredElementsElementScreenshot-Iphone13ProPortrait16-390x844.png new file mode 100644 index 000000000..df439fa90 Binary files /dev/null and b/tests/lambdaTestBaseline/iphone_13_pro/ignoredElementsElementScreenshot-Iphone13ProPortrait16-390x844.png differ diff --git a/tests/lambdaTestBaseline/iphone_13_pro/ignoredElementsScreenshot-Iphone13ProPortrait16-390x844.png b/tests/lambdaTestBaseline/iphone_13_pro/ignoredElementsScreenshot-Iphone13ProPortrait16-390x844.png new file mode 100644 index 000000000..b30f942fd Binary files /dev/null and b/tests/lambdaTestBaseline/iphone_13_pro/ignoredElementsScreenshot-Iphone13ProPortrait16-390x844.png differ diff --git a/tests/lambdaTestBaseline/iphone_14_pro/ignoredElementsElementScreenshot-Iphone14ProLandscape17-393x852.png b/tests/lambdaTestBaseline/iphone_14_pro/ignoredElementsElementScreenshot-Iphone14ProLandscape17-393x852.png new file mode 100644 index 000000000..f27bd6d81 Binary files /dev/null and b/tests/lambdaTestBaseline/iphone_14_pro/ignoredElementsElementScreenshot-Iphone14ProLandscape17-393x852.png differ diff --git a/tests/lambdaTestBaseline/iphone_14_pro/ignoredElementsElementScreenshot-Iphone14ProPortrait17-393x852.png b/tests/lambdaTestBaseline/iphone_14_pro/ignoredElementsElementScreenshot-Iphone14ProPortrait17-393x852.png new file mode 100644 index 000000000..3b544822c Binary files /dev/null and b/tests/lambdaTestBaseline/iphone_14_pro/ignoredElementsElementScreenshot-Iphone14ProPortrait17-393x852.png differ diff --git a/tests/lambdaTestBaseline/iphone_14_pro/ignoredElementsScreenshot-Iphone14ProPortrait17-393x852.png b/tests/lambdaTestBaseline/iphone_14_pro/ignoredElementsScreenshot-Iphone14ProPortrait17-393x852.png new file mode 100644 index 000000000..ddabb8a9e Binary files /dev/null and b/tests/lambdaTestBaseline/iphone_14_pro/ignoredElementsScreenshot-Iphone14ProPortrait17-393x852.png differ diff --git a/tests/lambdaTestBaseline/iphone_15_pro_max/ignoredElementsElementScreenshot-Iphone15ProMaxLandscape18-430x932.png b/tests/lambdaTestBaseline/iphone_15_pro_max/ignoredElementsElementScreenshot-Iphone15ProMaxLandscape18-430x932.png new file mode 100644 index 000000000..1c5603cdb Binary files /dev/null and b/tests/lambdaTestBaseline/iphone_15_pro_max/ignoredElementsElementScreenshot-Iphone15ProMaxLandscape18-430x932.png differ diff --git a/tests/lambdaTestBaseline/iphone_15_pro_max/ignoredElementsElementScreenshot-Iphone15ProMaxPortrait18-430x932.png b/tests/lambdaTestBaseline/iphone_15_pro_max/ignoredElementsElementScreenshot-Iphone15ProMaxPortrait18-430x932.png new file mode 100644 index 000000000..c38539ed0 Binary files /dev/null and b/tests/lambdaTestBaseline/iphone_15_pro_max/ignoredElementsElementScreenshot-Iphone15ProMaxPortrait18-430x932.png differ diff --git a/tests/lambdaTestBaseline/iphone_15_pro_max/ignoredElementsScreenshot-Iphone15ProMaxPortrait18-430x932.png b/tests/lambdaTestBaseline/iphone_15_pro_max/ignoredElementsScreenshot-Iphone15ProMaxPortrait18-430x932.png new file mode 100644 index 000000000..7d0388dd9 Binary files /dev/null and b/tests/lambdaTestBaseline/iphone_15_pro_max/ignoredElementsScreenshot-Iphone15ProMaxPortrait18-430x932.png differ diff --git a/tests/lambdaTestBaseline/pixel_4/ignoredElementsElementScreenshot-EmulatorPixel4LandscapeNativeWebScreenshot11-652x309.png b/tests/lambdaTestBaseline/pixel_4/ignoredElementsElementScreenshot-EmulatorPixel4LandscapeNativeWebScreenshot11-652x309.png new file mode 100644 index 000000000..76c980326 Binary files /dev/null and b/tests/lambdaTestBaseline/pixel_4/ignoredElementsElementScreenshot-EmulatorPixel4LandscapeNativeWebScreenshot11-652x309.png differ diff --git a/tests/lambdaTestBaseline/pixel_4/ignoredElementsElementScreenshot-EmulatorPixel4LandscapeNativeWebScreenshot12-760x360.png b/tests/lambdaTestBaseline/pixel_4/ignoredElementsElementScreenshot-EmulatorPixel4LandscapeNativeWebScreenshot12-760x360.png new file mode 100644 index 000000000..b15af3df1 Binary files /dev/null and b/tests/lambdaTestBaseline/pixel_4/ignoredElementsElementScreenshot-EmulatorPixel4LandscapeNativeWebScreenshot12-760x360.png differ diff --git a/tests/lambdaTestBaseline/pixel_4/ignoredElementsElementScreenshot-EmulatorPixel4LandscapeNativeWebScreenshot13-760x360.png b/tests/lambdaTestBaseline/pixel_4/ignoredElementsElementScreenshot-EmulatorPixel4LandscapeNativeWebScreenshot13-760x360.png new file mode 100644 index 000000000..f4f36a6a8 Binary files /dev/null and b/tests/lambdaTestBaseline/pixel_4/ignoredElementsElementScreenshot-EmulatorPixel4LandscapeNativeWebScreenshot13-760x360.png differ diff --git a/tests/lambdaTestBaseline/pixel_4/ignoredElementsElementScreenshot-EmulatorPixel4PortraitNativeWebScreenshot11-309x652.png b/tests/lambdaTestBaseline/pixel_4/ignoredElementsElementScreenshot-EmulatorPixel4PortraitNativeWebScreenshot11-309x652.png new file mode 100644 index 000000000..08dfa68de Binary files /dev/null and b/tests/lambdaTestBaseline/pixel_4/ignoredElementsElementScreenshot-EmulatorPixel4PortraitNativeWebScreenshot11-309x652.png differ diff --git a/tests/lambdaTestBaseline/pixel_4/ignoredElementsElementScreenshot-EmulatorPixel4PortraitNativeWebScreenshot12-360x760.png b/tests/lambdaTestBaseline/pixel_4/ignoredElementsElementScreenshot-EmulatorPixel4PortraitNativeWebScreenshot12-360x760.png new file mode 100644 index 000000000..6ba4e6bf6 Binary files /dev/null and b/tests/lambdaTestBaseline/pixel_4/ignoredElementsElementScreenshot-EmulatorPixel4PortraitNativeWebScreenshot12-360x760.png differ diff --git a/tests/lambdaTestBaseline/pixel_4/ignoredElementsElementScreenshot-EmulatorPixel4PortraitNativeWebScreenshot13-360x760.png b/tests/lambdaTestBaseline/pixel_4/ignoredElementsElementScreenshot-EmulatorPixel4PortraitNativeWebScreenshot13-360x760.png new file mode 100644 index 000000000..0bb88860f Binary files /dev/null and b/tests/lambdaTestBaseline/pixel_4/ignoredElementsElementScreenshot-EmulatorPixel4PortraitNativeWebScreenshot13-360x760.png differ diff --git a/tests/lambdaTestBaseline/pixel_4/ignoredElementsFullPageScreenshot-EmulatorPixel4LandscapeNativeWebScreenshot11-652x309.png b/tests/lambdaTestBaseline/pixel_4/ignoredElementsFullPageScreenshot-EmulatorPixel4LandscapeNativeWebScreenshot11-652x309.png new file mode 100644 index 000000000..7cdffc93b Binary files /dev/null and b/tests/lambdaTestBaseline/pixel_4/ignoredElementsFullPageScreenshot-EmulatorPixel4LandscapeNativeWebScreenshot11-652x309.png differ diff --git a/tests/lambdaTestBaseline/pixel_4/ignoredElementsFullPageScreenshot-EmulatorPixel4LandscapeNativeWebScreenshot12-760x360.png b/tests/lambdaTestBaseline/pixel_4/ignoredElementsFullPageScreenshot-EmulatorPixel4LandscapeNativeWebScreenshot12-760x360.png new file mode 100644 index 000000000..daae30736 Binary files /dev/null and b/tests/lambdaTestBaseline/pixel_4/ignoredElementsFullPageScreenshot-EmulatorPixel4LandscapeNativeWebScreenshot12-760x360.png differ diff --git a/tests/lambdaTestBaseline/pixel_4/ignoredElementsFullPageScreenshot-EmulatorPixel4LandscapeNativeWebScreenshot13-760x360.png b/tests/lambdaTestBaseline/pixel_4/ignoredElementsFullPageScreenshot-EmulatorPixel4LandscapeNativeWebScreenshot13-760x360.png new file mode 100644 index 000000000..9e85b325c Binary files /dev/null and b/tests/lambdaTestBaseline/pixel_4/ignoredElementsFullPageScreenshot-EmulatorPixel4LandscapeNativeWebScreenshot13-760x360.png differ diff --git a/tests/lambdaTestBaseline/pixel_4/ignoredElementsFullPageScreenshot-EmulatorPixel4PortraitNativeWebScreenshot11-309x652.png b/tests/lambdaTestBaseline/pixel_4/ignoredElementsFullPageScreenshot-EmulatorPixel4PortraitNativeWebScreenshot11-309x652.png new file mode 100644 index 000000000..978d871cc Binary files /dev/null and b/tests/lambdaTestBaseline/pixel_4/ignoredElementsFullPageScreenshot-EmulatorPixel4PortraitNativeWebScreenshot11-309x652.png differ diff --git a/tests/lambdaTestBaseline/pixel_4/ignoredElementsFullPageScreenshot-EmulatorPixel4PortraitNativeWebScreenshot12-360x760.png b/tests/lambdaTestBaseline/pixel_4/ignoredElementsFullPageScreenshot-EmulatorPixel4PortraitNativeWebScreenshot12-360x760.png new file mode 100644 index 000000000..05c6e1bd6 Binary files /dev/null and b/tests/lambdaTestBaseline/pixel_4/ignoredElementsFullPageScreenshot-EmulatorPixel4PortraitNativeWebScreenshot12-360x760.png differ diff --git a/tests/lambdaTestBaseline/pixel_4/ignoredElementsFullPageScreenshot-EmulatorPixel4PortraitNativeWebScreenshot13-360x760.png b/tests/lambdaTestBaseline/pixel_4/ignoredElementsFullPageScreenshot-EmulatorPixel4PortraitNativeWebScreenshot13-360x760.png new file mode 100644 index 000000000..8bb3174ed Binary files /dev/null and b/tests/lambdaTestBaseline/pixel_4/ignoredElementsFullPageScreenshot-EmulatorPixel4PortraitNativeWebScreenshot13-360x760.png differ diff --git a/tests/lambdaTestBaseline/pixel_4/ignoredElementsScreenshot-EmulatorPixel4PortraitNativeWebScreenshot12-360x760.png b/tests/lambdaTestBaseline/pixel_4/ignoredElementsScreenshot-EmulatorPixel4PortraitNativeWebScreenshot12-360x760.png new file mode 100644 index 000000000..153aa3d20 Binary files /dev/null and b/tests/lambdaTestBaseline/pixel_4/ignoredElementsScreenshot-EmulatorPixel4PortraitNativeWebScreenshot12-360x760.png differ diff --git a/tests/lambdaTestBaseline/pixel_9_pro/ignoredElementsElementScreenshot-EmulatorPixel9ProLandscapeNativeWebScreenshot14-952x427.png b/tests/lambdaTestBaseline/pixel_9_pro/ignoredElementsElementScreenshot-EmulatorPixel9ProLandscapeNativeWebScreenshot14-952x427.png new file mode 100644 index 000000000..71e675979 Binary files /dev/null and b/tests/lambdaTestBaseline/pixel_9_pro/ignoredElementsElementScreenshot-EmulatorPixel9ProLandscapeNativeWebScreenshot14-952x427.png differ diff --git a/tests/lambdaTestBaseline/pixel_9_pro/ignoredElementsElementScreenshot-EmulatorPixel9ProLandscapeNativeWebScreenshot15-952x427.png b/tests/lambdaTestBaseline/pixel_9_pro/ignoredElementsElementScreenshot-EmulatorPixel9ProLandscapeNativeWebScreenshot15-952x427.png new file mode 100644 index 000000000..e4a352287 Binary files /dev/null and b/tests/lambdaTestBaseline/pixel_9_pro/ignoredElementsElementScreenshot-EmulatorPixel9ProLandscapeNativeWebScreenshot15-952x427.png differ diff --git a/tests/lambdaTestBaseline/pixel_9_pro/ignoredElementsElementScreenshot-EmulatorPixel9ProPortraitNativeWebScreenshot14-427x952.png b/tests/lambdaTestBaseline/pixel_9_pro/ignoredElementsElementScreenshot-EmulatorPixel9ProPortraitNativeWebScreenshot14-427x952.png new file mode 100644 index 000000000..ffe58ac19 Binary files /dev/null and b/tests/lambdaTestBaseline/pixel_9_pro/ignoredElementsElementScreenshot-EmulatorPixel9ProPortraitNativeWebScreenshot14-427x952.png differ diff --git a/tests/lambdaTestBaseline/pixel_9_pro/ignoredElementsElementScreenshot-EmulatorPixel9ProPortraitNativeWebScreenshot15-427x952.png b/tests/lambdaTestBaseline/pixel_9_pro/ignoredElementsElementScreenshot-EmulatorPixel9ProPortraitNativeWebScreenshot15-427x952.png new file mode 100644 index 000000000..60d958986 Binary files /dev/null and b/tests/lambdaTestBaseline/pixel_9_pro/ignoredElementsElementScreenshot-EmulatorPixel9ProPortraitNativeWebScreenshot15-427x952.png differ diff --git a/tests/lambdaTestBaseline/pixel_9_pro/ignoredElementsFullPageScreenshot-EmulatorPixel9ProLandscapeNativeWebScreenshot14-952x427.png b/tests/lambdaTestBaseline/pixel_9_pro/ignoredElementsFullPageScreenshot-EmulatorPixel9ProLandscapeNativeWebScreenshot14-952x427.png new file mode 100644 index 000000000..f12b1e8dd Binary files /dev/null and b/tests/lambdaTestBaseline/pixel_9_pro/ignoredElementsFullPageScreenshot-EmulatorPixel9ProLandscapeNativeWebScreenshot14-952x427.png differ diff --git a/tests/lambdaTestBaseline/pixel_9_pro/ignoredElementsFullPageScreenshot-EmulatorPixel9ProLandscapeNativeWebScreenshot15-952x427.png b/tests/lambdaTestBaseline/pixel_9_pro/ignoredElementsFullPageScreenshot-EmulatorPixel9ProLandscapeNativeWebScreenshot15-952x427.png new file mode 100644 index 000000000..a9ab875cb Binary files /dev/null and b/tests/lambdaTestBaseline/pixel_9_pro/ignoredElementsFullPageScreenshot-EmulatorPixel9ProLandscapeNativeWebScreenshot15-952x427.png differ diff --git a/tests/lambdaTestBaseline/pixel_9_pro/ignoredElementsFullPageScreenshot-EmulatorPixel9ProPortraitNativeWebScreenshot14-427x952.png b/tests/lambdaTestBaseline/pixel_9_pro/ignoredElementsFullPageScreenshot-EmulatorPixel9ProPortraitNativeWebScreenshot14-427x952.png new file mode 100644 index 000000000..3db9dd4cf Binary files /dev/null and b/tests/lambdaTestBaseline/pixel_9_pro/ignoredElementsFullPageScreenshot-EmulatorPixel9ProPortraitNativeWebScreenshot14-427x952.png differ diff --git a/tests/lambdaTestBaseline/pixel_9_pro/ignoredElementsFullPageScreenshot-EmulatorPixel9ProPortraitNativeWebScreenshot15-427x952.png b/tests/lambdaTestBaseline/pixel_9_pro/ignoredElementsFullPageScreenshot-EmulatorPixel9ProPortraitNativeWebScreenshot15-427x952.png new file mode 100644 index 000000000..b16c05277 Binary files /dev/null and b/tests/lambdaTestBaseline/pixel_9_pro/ignoredElementsFullPageScreenshot-EmulatorPixel9ProPortraitNativeWebScreenshot15-427x952.png differ diff --git a/tests/lambdaTestBaseline/pixel_9_pro/ignoredElementsScreenshot-EmulatorPixel9ProPortraitNativeWebScreenshot14-427x952.png b/tests/lambdaTestBaseline/pixel_9_pro/ignoredElementsScreenshot-EmulatorPixel9ProPortraitNativeWebScreenshot14-427x952.png new file mode 100644 index 000000000..bd1ce2a29 Binary files /dev/null and b/tests/lambdaTestBaseline/pixel_9_pro/ignoredElementsScreenshot-EmulatorPixel9ProPortraitNativeWebScreenshot14-427x952.png differ diff --git a/tests/specs/desktop.bidi.emulated.spec.ts b/tests/specs/desktop.bidi.emulated.spec.ts index 5734b726d..51aaa6d07 100644 --- a/tests/specs/desktop.bidi.emulated.spec.ts +++ b/tests/specs/desktop.bidi.emulated.spec.ts @@ -11,7 +11,40 @@ describe('@wdio/visual-service desktop bidi emulated', () => { }) it(`should compare an element successful with a baseline for '${browserName}'`, async function() { - await expect($('.hero__title-logo')).toMatchElementSnapshot('bidiEmulatedWdioLogo') + await expect($('.hero__title-logo')).toMatchElementSnapshot( + 'bidiEmulatedWdioLogo', { + hideElements: [await $('nav.navbar')] + } + ) + }) + + it(`should compare an element screenshot with ignore elements successful with a baseline for '${browserName}'`, async function () { + await $('.features_vqN4').scrollIntoView() + + // When running a new set of images then first comment out block 1 and 2. Then run the test. + // Then uncomment block 1, check if they fail with `--store-diffs` as an extra argument. + // If so, then uncomment block 2 and check if pass with the same arguments. + // Block 1 + await browser.execute(() => { + document.querySelectorAll('.feature_G9wp h3').forEach(heading => { + (heading as HTMLElement).style.backgroundColor = 'var(--ifm-color-primary)' + }) + }) + + await expect($('.features_vqN4')).toMatchElementSnapshot( + 'bidiIgnoredElementsElementScreenshot', + { + // Block 2 + ignore: [ + await $$('.feature_G9wp h3'), + ], + // Some padding to make sure that we cover the element, + // with BiDi we sometimes miss the element due to internal calculations + ignoreRegionPadding: 2, + // Don't comment this out, it's needed to hide the navbar + hideElements: [await $('nav.navbar')] + } + ) }) it(`should compare a viewport screenshot successful with a baseline for '${browserName}'`, async function () { @@ -32,6 +65,33 @@ describe('@wdio/visual-service desktop bidi emulated', () => { }) }) + it(`should compare an element screenshot with ignore elements successful with a baseline for '${browserName}' with the legacy API`, async function () { + await $('.features_vqN4').scrollIntoView() + + // When running a new set of images then first comment out block 1 and 2. Then run the test. + // Then uncomment block 1, check if they fail with `--store-diffs` as an extra argument. + // If so, then uncomment block 2 and check if pass with the same arguments. + // Block 1 + await browser.execute(() => { + document.querySelectorAll('.feature_G9wp h3').forEach(heading => { + (heading as HTMLElement).style.backgroundColor = 'var(--ifm-color-primary)' + }) + }) + + await expect($('.features_vqN4')).toMatchElementSnapshot( + 'legacyIgnoredElementsElementScreenshot', + { + enableLegacyScreenshotMethod: true, + // Block 2 + ignore: [ + await $$('.feature_G9wp h3'), + ], + // Don't comment this out, it's needed to hide the navbar + hideElements: [await $('nav.navbar')] + } + ) + }) + it(`should compare a viewport screenshot successful with a baseline for '${browserName}' with the legacy API`, async function () { await expect(browser).toMatchScreenSnapshot('legacyEmulatedViewportScreenshot', { enableLegacyScreenshotMethod: true }) }) diff --git a/tests/specs/desktop.spec.ts b/tests/specs/desktop.spec.ts index bf276dc50..54208a4f8 100644 --- a/tests/specs/desktop.spec.ts +++ b/tests/specs/desktop.spec.ts @@ -21,10 +21,59 @@ describe('@wdio/visual-service desktop', () => { }) }) + it(`should compare an element screenshot with ignore elements successful with a baseline for '${browserName}'`, async function () { + await $('.features_vqN4').scrollIntoView() + + // When running a new set of images then first comment out block 1 and 2. Then run the test. + // Then uncomment block 1, check if they fail with `--store-diffs` as an extra argument. + // If so, then uncomment block 2 and check if pass with the same arguments. + // Block 1 + await browser.execute(() => { + document.querySelectorAll('.feature_G9wp h3').forEach(heading => { + (heading as HTMLElement).style.backgroundColor = 'var(--ifm-color-primary)' + }) + }) + + await expect($('.features_vqN4')).toMatchElementSnapshot( + 'ignoredElementsElementScreenshot', + { + // Block 2 + ignore: [ + await $$('.feature_G9wp h3'), + ], + // Don't comment this out, it's needed to hide the navbar + hideElements: [await $('nav.navbar')] + } + ) + }) + it(`should compare a viewport screenshot successful with a baseline for '${browserName}'`, async function() { await expect(browser).toMatchScreenSnapshot('viewportScreenshot') }) + it(`should compare a viewport screenshot with ignore elements successful with a baseline for '${browserName}'`, async function () { + // When running a new set of images then first comment out block 1 and 2. Then run the test. + // Then uncomment block 1, check if they fail with `--store-diffs` as an extra argument. + // If so, then uncomment block 2 and check if pass with the same arguments. + // Block 1 + await browser.execute(() => { + document.querySelectorAll('.navbar__items--right a.navbar__item, .feature_G9wp').forEach(link => { + (link as HTMLElement).style.backgroundColor = 'var(--ifm-color-primary)' + }) + }) + + await expect(browser).toMatchScreenSnapshot( + 'ignoredElementsViewportScreenshot', + { + // Block 2 + ignore: [ + await $$('.navbar__items--right a.navbar__item'), + await $$('.feature_G9wp'), + ], + } + ) + }) + it(`should compare a full page screenshot successful with a baseline for '${browserName}'`, async function () { await expect(browser).toMatchFullPageSnapshot('fullPage', { fullPageScrollTimeout: 1500, @@ -34,6 +83,29 @@ describe('@wdio/visual-service desktop', () => { }) }) + it(`should compare a full page screenshot with ignore elements successful with a baseline for '${browserName}'`, async function () { + // When running a new set of images then first comment out block 1 and 2. Then run the test. + // Then uncomment block 1, check if they fail with `--store-diffs` as an extra argument. + // If so, then uncomment block 2 and check if pass with the same arguments. + // Block 1 + await browser.execute(() => { + document.querySelectorAll('.feature_G9wp h3').forEach(heading => { + (heading as HTMLElement).style.backgroundColor = 'var(--ifm-color-primary)' + }) + }) + + await expect(browser).toMatchFullPageSnapshot('ignoredElementsFullPageScreenshot', { + fullPageScrollTimeout: 1500, + hideAfterFirstScroll: [ + await $('nav.navbar'), + ], + // // Block 2 + ignore: [ + await $$('.feature_G9wp h3'), + ], + }) + }) + it(`should compare a tabbable screenshot successful with a baseline for '${browserName}'`, async function() { await expect(browser).toMatchTabbablePageSnapshot('tabbable', { hideAfterFirstScroll: [ diff --git a/tests/specs/mobile.web.spec.ts b/tests/specs/mobile.web.spec.ts index 0c2c7fa86..81e5c3eba 100644 --- a/tests/specs/mobile.web.spec.ts +++ b/tests/specs/mobile.web.spec.ts @@ -6,27 +6,15 @@ import { browser, expect } from '@wdio/globals' describe('@wdio/visual-service mobile web', () => { // Get the commands that need to be executed // 0 means all, otherwise it will only execute the commands that are specified - const wdioIcsCommands = driver.requestedCapabilities['wdio-ics:options'].commands - const deviceName = driver.requestedCapabilities['lt:options']?.deviceName || - driver.requestedCapabilities['bstack:options']?.deviceName || - driver.requestedCapabilities['appium:options']?.deviceName || - driver.requestedCapabilities.deviceName - const platformName = ( - driver.requestedCapabilities['lt:options']?.platformName || - driver.requestedCapabilities['appium:options']?.platformName || - driver.requestedCapabilities.platformName - ).toLowerCase() === 'android' ? 'Android' : 'iOS' - const platformVersion = - driver.requestedCapabilities['lt:options']?.platformVersion || - driver.requestedCapabilities['bstack:options']?.osVersion || - driver.requestedCapabilities['appium:options']?.platformVersion || - driver.requestedCapabilities.platformVersion - const orientation = ( - driver.requestedCapabilities['lt:options']?.deviceOrientation || - driver.requestedCapabilities['bstack:options']?.deviceOrientation || - driver.requestedCapabilities['appium:options']?.orientation || - driver.requestedCapabilities.orientation || 'PORTRAIT' - ).toLowerCase() + const caps = driver.requestedCapabilities + const lt = caps['lt:options'] + const bs = caps['bstack:options'] + const appium = caps['appium:options'] + const wdioIcsCommands = caps['wdio-ics:options'].commands + const deviceName = lt?.deviceName || bs?.deviceName || appium?.deviceName || caps.deviceName + const platformName = (lt?.platformName || appium?.platformName || caps.platformName).toLowerCase() === 'android' ? 'Android' : 'iOS' + const platformVersion = lt?.platformVersion || bs?.osVersion || appium?.platformVersion || caps.platformVersion + const orientation = (lt?.deviceOrientation || bs?.deviceOrientation || appium?.orientation || caps.orientation || 'PORTRAIT').toLowerCase() beforeEach(async () => { await browser.url('') @@ -44,7 +32,7 @@ describe('@wdio/visual-service mobile web', () => { wdioIcsCommands.includes('checkScreen') ) { it(`should compare a screen successful for '${deviceName}' with ${platformName}:${platformVersion} in ${orientation}-mode`, async function () { - // @ts-ignore + skipTest({ test: this, deviceName, platformName, platformVersion, orientation }) this.retries(2) // This is normally a bad practice, but a mobile screenshot is normally around 1M pixels @@ -68,6 +56,34 @@ describe('@wdio/visual-service mobile web', () => { await browser.setOrientation(orientation) await expect(newResult < 0.05 ? 0 : newResult).toEqual(0) }) + + it(`should compare a screen with ignore elements successful for '${deviceName}' with ${platformName}:${platformVersion} in ${orientation}-mode`, async function () { + skipTest({ test: this, deviceName, platformName, platformVersion, orientation }) + + // When running a new set of images then first comment out block 1 and 2. Then run the test. + // Then uncomment block 1, check if they fail with `--store-diffs` as an extra argument. + // If so, then uncomment block 2 and check if pass with the same arguments. + // Block 1 + await browser.execute(() => { + document.querySelectorAll('.getStarted_Sjon').forEach(link => { + (link as HTMLElement).style.backgroundColor = 'var(--ifm-font-color-base)' + }) + }) + + // This is normally a bad practice, but a mobile screenshot is normally around 1M pixels + // We're accepting 0.05%, which is 500 pixels, to be a max difference + const result = await browser.checkScreen( + 'ignoredElementsScreenshot', { + // Block 2 + ignore: [ + await $$('.getStarted_Sjon'), + ], + }) as number + if (result > 0 && result < 0.05) { + console.log(`\n\n\n'Screenshot for ${deviceName}' with ${platformName}:${platformVersion} in ${orientation}-mode has a difference of ${result}%\n\n\n`) + } + await expect(result < 0.05 ? 0 : result).toEqual(0) + }) } if ( @@ -75,8 +91,9 @@ describe('@wdio/visual-service mobile web', () => { wdioIcsCommands.includes('checkElement') ) { it(`should compare an element successful for '${deviceName}' with ${platformName}:${platformVersion} in ${orientation}-mode`, async function() { - // @ts-ignore + skipTest({ test: this, deviceName, platformName, platformVersion, orientation }) this.retries(2) + await expect( await browser.checkElement( await $('.hero__title-logo'), @@ -87,6 +104,34 @@ describe('@wdio/visual-service mobile web', () => { ) ).toEqual(0) }) + + it(`should compare an element with ignore elements successful for '${deviceName}' with ${platformName}:${platformVersion} in ${orientation}-mode`, async function() { + skipTest({ test: this, deviceName, platformName, platformVersion, orientation }) + + await $('.features_vqN4').scrollIntoView() + + // When running a new set of images then first comment out block 1 and 2. Then run the test. + // Then uncomment block 1, check if they fail with `--store-diffs` as an extra argument. + // If so, then uncomment block 2 and check if pass with the same arguments. + // Block 1 + await browser.execute(() => { + document.querySelectorAll('.feature_G9wp h3').forEach(heading => { + (heading as HTMLElement).style.backgroundColor = 'var(--ifm-color-primary)' + }) + }) + + await expect($('.features_vqN4')).toMatchElementSnapshot( + 'ignoredElementsElementScreenshot', + { + // Block 2 + ignore: [ + await $$('.feature_G9wp h3'), + ], + // Don't comment this out, it's needed to hide the navbar + hideElements: [await $('nav.navbar')] + } + ) + }) } if ( @@ -94,8 +139,9 @@ describe('@wdio/visual-service mobile web', () => { wdioIcsCommands.includes('checkFullPageScreen') ) { it(`should compare a full page screenshot successful for '${deviceName}' with ${platformName}:${platformVersion} in ${orientation}-mode`, async function() { - // @ts-ignore + skipTest({ test: this, deviceName, platformName, platformVersion, orientation }) this.retries(2) + // This is normally a bad practice, but a mobile full page screenshot is normally around 4M pixels // We're accepting 0.05%, which is 2000 pixels, to be a max difference const result = await browser.checkFullPageScreen('fullPage', { @@ -109,5 +155,196 @@ describe('@wdio/visual-service mobile web', () => { } await expect(result < 0.05 ? 0 : result).toEqual(0) }) + + it(`should compare a full page screenshot with ignore elements successful for '${deviceName}' with ${platformName}:${platformVersion} in ${orientation}-mode`, async function() { + skipTest({ test: this, deviceName, platformName, platformVersion, orientation }) + + // When running a new set of images then first comment out block 1 and 2. Then run the test. + // Then uncomment block 1, check if they fail with `--store-diffs` as an extra argument. + // If so, then uncomment block 2 and check if pass with the same arguments. + // Block 1 + await browser.execute(() => { + document.querySelectorAll('.feature_G9wp h3').forEach(heading => { + (heading as HTMLElement).style.backgroundColor = 'var(--ifm-color-primary)' + }) + }) + + await expect(browser).toMatchFullPageSnapshot( + 'ignoredElementsFullPageScreenshot', + { + // Block 2 + ignore: [ + await $$('.feature_G9wp h3'), + ], + ignoreRegionPadding: 5, + fullPageScrollTimeout: 1500, + hideAfterFirstScroll: [ + await $('nav.navbar'), + ], + } + ) + }) } }) + +/****************************************************************************************** + * SKIP RULES + * These are most likely TODO's that we have to fix but are not a blocker for the release. + * The reason is added to help us remember why we skipped the test. + ******************************************************************************************/ + +interface SkipRule { + titleIncludes: string | string[] + deviceName: string + platformName: 'Android' | 'iOS' + platformVersions: string[] + orientations: ('landscape' | 'portrait')[] + reason: string +} + +const skipRules: SkipRule[] = [ + // Android devices + { + // @TODO: remove when fixed + titleIncludes: 'compare a screen with ignore elements', + deviceName: 'Pixel 9 Pro', + platformName: 'Android', + platformVersions: ['15'], + orientations: ['portrait'], + reason: '1px difference in the ignore elements screenshot', + }, + { + // @TODO: remove when fixed + titleIncludes: 'compare a screen with ignore elements', + deviceName: 'Galaxy Tab S8', + platformName: 'Android', + platformVersions: ['13'], + orientations: ['landscape', 'portrait'], + reason: '1px difference in the ignore elements screenshot', + }, + { + // @TODO: remove when fixed + titleIncludes: 'compare a screen with ignore elements', + deviceName: 'Galaxy Tab S8', + platformName: 'Android', + platformVersions: ['14'], + orientations: ['portrait'], + reason: '1px difference in the ignore elements screenshot', + }, + { + // @TODO: remove when fixed + titleIncludes: 'should compare a full page screenshot with ignore elements', + deviceName: 'Galaxy Tab S8', + platformName: 'Android', + platformVersions: ['14'], + orientations: ['portrait', 'landscape'], + reason: 'it always starts with the tabbed view, so it will break at the start of the screenshot', + }, + { + // @TODO: remove when fixed + titleIncludes: ['compare a screen with ignore elements', 'compare a screen successful'], + deviceName: 'Galaxy Tab S8', + platformName: 'Android', + platformVersions: ['14'], + orientations: ['landscape', 'portrait'], + reason: 'Fully ignored in the screenshot so it will never find a difference', + }, + { + // @TODO: remove when fixed + titleIncludes: 'compare a full page screenshot successful', + deviceName: 'Galaxy Tab S8', + platformName: 'Android', + platformVersions: ['13', '14'], + orientations: ['landscape', 'portrait'], + reason: 'There are difference in the full page screenshot that might be related to things introduced in PR #1126', + }, + { + // @TODO: remove when fixed + titleIncludes: 'compare a screen with ignore elements', + deviceName: 'Pixel 4', + platformName: 'Android', + platformVersions: ['13'], + orientations: ['landscape', 'portrait'], + reason: 'Fully ignored in the screenshot so it will never find a difference', + }, + { + titleIncludes: 'compare a screen successful', + deviceName: 'Pixel 4', + platformName: 'Android', + platformVersions: ['13'], + orientations: ['portrait'], + reason: 'Elements not visible in the screenshot, no value in testing', + }, + { + titleIncludes: 'compare a screen with ignore elements', + deviceName: 'Pixel 9 Pro', + platformName: 'Android', + platformVersions: ['14', '15'], + orientations: ['landscape'], + reason: 'Elements not visible in the screenshot, no value in testing', + }, + { + titleIncludes: 'compare a screen with ignore elements', + deviceName: 'Pixel 4', + platformName: 'Android', + platformVersions: ['11', '12'], + orientations: ['landscape', 'portrait'], + reason: 'Elements not visible in the screenshot, no value in testing', + }, + // iOS devices + { + titleIncludes: 'compare a screen with ignore elements', + deviceName: 'iPhone 13 mini', + platformName: 'iOS', + platformVersions: ['17.5'], + orientations: ['landscape'], + reason: 'Elements not visible in the screenshot, no value in testing', + }, + { + titleIncludes: 'compare a screen with ignore elements', + deviceName: 'iPhone 13 Pro', + platformName: 'iOS', + platformVersions: ['16.0'], + orientations: ['landscape'], + reason: 'Elements not visible in the screenshot, no value in testing', + }, + { + titleIncludes: 'compare a screen with ignore elements', + deviceName: 'iPhone 14 Pro', + platformName: 'iOS', + platformVersions: ['17.5'], + orientations: ['landscape'], + reason: 'Elements not visible in the screenshot, no value in testing', + }, + { + titleIncludes: 'compare a screen with ignore elements', + deviceName: 'iPhone 15 Pro Max', + platformName: 'iOS', + platformVersions: ['18.0'], + orientations: ['landscape'], + reason: 'Elements not visible in the screenshot, no value in testing', + }, +] +function skipTest({ test, deviceName, platformName, platformVersion, orientation }: { + test: Mocha.Context + deviceName: string + platformName: string + platformVersion: string + orientation: string +}) { + const { title } = test.test! + + const matchedRule = skipRules.find(rule => { + const patterns = Array.isArray(rule.titleIncludes) ? rule.titleIncludes : [rule.titleIncludes] + + return patterns.some(p => title.includes(p)) + && rule.deviceName === deviceName + && rule.platformName === platformName + && rule.platformVersions.includes(platformVersion) + && rule.orientations.includes(orientation as 'landscape' | 'portrait') + }) + + if (matchedRule) { + test.skip() + } +}