UNPKG

@wdio/image-comparison-core

Version:

Image comparison core module for @wdio/visual-service - WebdriverIO visual testing framework

854 lines (853 loc) 37.9 kB
import { describe, it, expect, vi, beforeEach, afterEach } from 'vitest'; import { join } from 'node:path'; import { determineElementRectangles, determineScreenRectangles, determineStatusAddressToolBarRectangles, determineIgnoreRegions, splitIgnores, determineDeviceBlockOuts, prepareIgnoreRectangles } from './rectangles.js'; import { IMAGE_STRING } from '../mocks/image.js'; vi.mock('@wdio/globals', () => ({ browser: { execute: vi.fn(), } })); vi.mock('@wdio/logger', () => import(join(process.cwd(), '__mocks__', '@wdio/logger'))); describe('rectangles', () => { let mockBrowserInstance; let mockExecute; let mockGetElementRect; beforeEach(() => { mockExecute = vi.fn(); mockGetElementRect = vi.fn(); mockBrowserInstance = { execute: mockExecute, getElementRect: mockGetElementRect }; }); afterEach(() => { vi.clearAllMocks(); }); const baseDeviceRectangles = { bottomBar: { y: 0, x: 0, width: 0, height: 0 }, homeBar: { y: 0, x: 0, width: 0, height: 0 }, leftSidePadding: { y: 0, x: 0, width: 0, height: 0 }, rightSidePadding: { y: 0, x: 0, width: 0, height: 0 }, screenSize: { height: 0, width: 0 }, statusBar: { y: 0, x: 0, width: 0, height: 0 }, statusBarAndAddressBar: { y: 0, x: 0, width: 0, height: 0 }, viewport: { y: 0, x: 0, width: 0, height: 0 }, }; const createElementOptions = (overrides = {}) => ({ isAndroid: false, devicePixelRatio: 2, deviceRectangles: baseDeviceRectangles, isAndroidNativeWebScreenshot: false, innerHeight: 500, isIOS: false, initialDevicePixelRatio: 2, isEmulated: false, ...overrides, }); const createScreenOptions = (overrides = {}) => ({ innerHeight: 553, innerWidth: 375, isAndroidNativeWebScreenshot: false, isAndroidChromeDriverScreenshot: false, isIOS: false, devicePixelRatio: 2, isLandscape: false, initialDevicePixelRatio: 2, enableLegacyScreenshotMethod: false, isEmulated: false, ...overrides, }); const createStatusAddressToolBarOptions = (overrides = {}) => ({ blockOutSideBar: false, blockOutStatusBar: false, blockOutToolBar: false, isAndroid: false, isAndroidNativeWebScreenshot: false, isMobile: false, isViewPortScreenshot: false, ...overrides, }); const createDeviceRectanglesWithData = (overrides = {}) => ({ ...baseDeviceRectangles, statusBarAndAddressBar: { y: 0, x: 0, width: 1344, height: 320 }, viewport: { y: 320, x: 0, width: 1344, height: 2601 }, bottomBar: { y: 2921, x: 0, width: 1344, height: 71 }, leftSidePadding: { y: 320, x: 0, width: 0, height: 2601 }, rightSidePadding: { y: 320, x: 1344, width: 0, height: 2601 }, ...overrides, }); const createDeviceBlockOutsOptions = (overrides = {}) => ({ isAndroid: false, screenCompareOptions: { blockOutStatusBar: false, blockOutToolBar: false, }, instanceData: { appName: 'TestApp', browserName: 'Chrome', browserVersion: '118.0.0.0', deviceName: 'iPhone 14', devicePixelRatio: 2, deviceRectangles: { bottomBar: { y: 800, x: 0, width: 390, height: 0 }, homeBar: { x: 0, y: 780, width: 390, height: 34 }, leftSidePadding: { y: 0, x: 0, width: 0, height: 0 }, rightSidePadding: { y: 0, x: 0, width: 0, height: 0 }, screenSize: { height: 844, width: 390 }, statusBar: { x: 0, y: 0, width: 390, height: 47 }, statusBarAndAddressBar: { y: 0, x: 0, width: 390, height: 47 }, viewport: { y: 47, x: 0, width: 390, height: 733 } }, initialDevicePixelRatio: 2, isAndroid: false, isIOS: true, isMobile: true, logName: 'test-log', name: 'test-device', nativeWebScreenshot: false, platformName: 'iOS', platformVersion: '17.0' }, ...overrides, }); const createPrepareIgnoreRectanglesOptions = (overrides = {}) => ({ blockOut: [], ignoreRegions: [], deviceRectangles: createDeviceRectanglesWithData(), devicePixelRatio: 2, isMobile: false, isNativeContext: false, isAndroid: false, isAndroidNativeWebScreenshot: false, isViewPortScreenshot: true, imageCompareOptions: { blockOutSideBar: false, blockOutStatusBar: false, blockOutToolBar: false, }, ...overrides, }); describe('determineElementRectangles', () => { it('should determine them for iOS', async () => { const options = createElementOptions({ isIOS: true, innerHeight: 678, deviceRectangles: { ...baseDeviceRectangles, viewport: { y: 20, x: 30, width: 0, height: 0 }, }, }); mockExecute.mockResolvedValueOnce({ height: 120, width: 120, x: 100, y: 10, }); const result = await determineElementRectangles({ browserInstance: mockBrowserInstance, base64Image: IMAGE_STRING, options, element: 'element', }); expect(result).toMatchSnapshot(); expect(mockExecute).toHaveBeenCalled(); }); it('should determine them for Android Native webscreenshot', async () => { const options = createElementOptions({ isAndroid: true, devicePixelRatio: 3, initialDevicePixelRatio: 3, isAndroidNativeWebScreenshot: true, innerHeight: 678, deviceRectangles: { ...baseDeviceRectangles, viewport: { y: 200, x: 300, width: 0, height: 0 }, }, }); mockExecute.mockResolvedValueOnce({ height: 300, width: 200, x: 100, y: 10, }); const result = await determineElementRectangles({ browserInstance: mockBrowserInstance, base64Image: IMAGE_STRING, options, element: 'element', }); expect(result).toMatchSnapshot(); expect(mockExecute).toHaveBeenCalled(); }); it('should determine them for Android ChromeDriver', async () => { const options = createElementOptions({ isAndroid: true, devicePixelRatio: 1, initialDevicePixelRatio: 1, innerHeight: 678, deviceRectangles: { ...baseDeviceRectangles, viewport: { y: 200, x: 300, width: 0, height: 0 }, }, }); mockExecute.mockResolvedValueOnce({ height: 20, width: 375, x: 0, y: 0, }); const result = await determineElementRectangles({ browserInstance: mockBrowserInstance, base64Image: IMAGE_STRING, options, element: 'element', }); expect(result).toMatchSnapshot(); expect(mockExecute).toHaveBeenCalled(); }); it('should determine them for a desktop browser', async () => { const options = createElementOptions({ innerHeight: 500, }); mockExecute.mockResolvedValueOnce({ height: 20, width: 375, x: 12, y: 34, }); const result = await determineElementRectangles({ browserInstance: mockBrowserInstance, base64Image: IMAGE_STRING, options, element: 'element', }); expect(result).toMatchSnapshot(); expect(mockExecute).toHaveBeenCalled(); }); it('should determine them for emulated device', async () => { const options = createElementOptions({ isEmulated: true, innerHeight: 600, devicePixelRatio: 3, }); mockExecute.mockResolvedValueOnce({ height: 50, width: 200, x: 15, y: 25, }); const result = await determineElementRectangles({ browserInstance: mockBrowserInstance, base64Image: IMAGE_STRING, options, element: 'element', }); expect(result).toMatchSnapshot(); expect(mockExecute).toHaveBeenCalled(); }); it('should throw an error when the element height is 0', async () => { const options = createElementOptions(); mockExecute.mockResolvedValueOnce({ height: 0, width: 375, x: 12, y: 34, }); await expect(determineElementRectangles({ browserInstance: mockBrowserInstance, base64Image: IMAGE_STRING, options, element: { selector: '#elementID' }, })).rejects.toThrow('The element, with selector "$(#elementID)",is not visible. The dimensions are 375x0'); }); it('should throw an error when the element width is 0', async () => { const options = createElementOptions(); mockExecute.mockResolvedValueOnce({ height: 375, width: 0, x: 12, y: 34, }); await expect(determineElementRectangles({ browserInstance: mockBrowserInstance, base64Image: IMAGE_STRING, options, element: { selector: '#elementID' }, })).rejects.toThrow('The element, with selector "$(#elementID)",is not visible. The dimensions are 0x375'); }); it('should throw an error when the element width is 0 and no element selector is provided', async () => { const options = createElementOptions(); mockExecute.mockResolvedValueOnce({ height: 375, width: 0, x: 12, y: 34, }); await expect(determineElementRectangles({ browserInstance: mockBrowserInstance, base64Image: IMAGE_STRING, options, element: {}, })).rejects.toThrow('The element is not visible. The dimensions are 0x375'); }); it('should handle Android webview elements', async () => { const options = createElementOptions({ isAndroid: true, isAndroidNativeWebScreenshot: true, devicePixelRatio: 2, deviceRectangles: { ...baseDeviceRectangles, viewport: { y: 100, x: 50, width: 375, height: 667 }, }, }); mockExecute.mockResolvedValueOnce({ height: 100, width: 200, x: 50, y: 75, }); const result = await determineElementRectangles({ browserInstance: mockBrowserInstance, base64Image: IMAGE_STRING, options, element: 'webview-element', }); expect(result).toMatchSnapshot(); expect(mockExecute).toHaveBeenCalled(); }); }); describe('determineScreenRectangles', () => { it('should determine them for iOS', async () => { const options = createScreenOptions({ isIOS: true, }); expect(determineScreenRectangles(IMAGE_STRING, options)).toMatchSnapshot(); }); it('should determine them for Android ChromeDriver', async () => { const options = createScreenOptions({ isAndroidChromeDriverScreenshot: true, }); expect(determineScreenRectangles(IMAGE_STRING, options)).toMatchSnapshot(); }); it('should determine them for Android Native webscreenshot', async () => { const options = createScreenOptions({ isAndroidNativeWebScreenshot: true, }); expect(determineScreenRectangles(IMAGE_STRING, options)).toMatchSnapshot(); }); it('should determine them for desktop browser', async () => { const options = createScreenOptions({ innerHeight: 768, innerWidth: 1024, devicePixelRatio: 1, }); expect(determineScreenRectangles(IMAGE_STRING, options)).toMatchSnapshot(); }); it('should determine them for emulated device', async () => { const options = createScreenOptions({ isEmulated: true, devicePixelRatio: 3, isLandscape: true, }); expect(determineScreenRectangles(IMAGE_STRING, options)).toMatchSnapshot(); }); it('should determine them with legacy screenshot method', async () => { const options = createScreenOptions({ enableLegacyScreenshotMethod: true, isIOS: true, }); expect(determineScreenRectangles(IMAGE_STRING, options)).toMatchSnapshot(); }); it('should use initialDevicePixelRatio when isEmulated and enableLegacyScreenshotMethod are both true', async () => { const options = createScreenOptions({ isEmulated: true, enableLegacyScreenshotMethod: true, devicePixelRatio: 3, initialDevicePixelRatio: 2, innerHeight: 768, innerWidth: 1024, }); expect(determineScreenRectangles(IMAGE_STRING, options)).toMatchSnapshot(); }); it('should handle landscape rotation when height > width', async () => { const tallImage = 'data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAEAAAACCAYAAACZgbYnAAAAEklEQVR42mP8/5+hnoEIwDiqAAC4sAP9TiGZQgAAAABJRU5ErkJggg=='; const options = createScreenOptions({ isLandscape: true, innerHeight: 1024, innerWidth: 768, devicePixelRatio: 1, }); expect(determineScreenRectangles(tallImage, options)).toMatchSnapshot(); }); }); describe('determineStatusAddressToolBarRectangles', () => { it('should determine the rectangles with all blockouts enabled', async () => { const options = createStatusAddressToolBarOptions({ blockOutSideBar: true, blockOutStatusBar: true, blockOutToolBar: true, isAndroid: true, isAndroidNativeWebScreenshot: true, isMobile: true, isViewPortScreenshot: true, }); const deviceRectangles = createDeviceRectanglesWithData(); expect(determineStatusAddressToolBarRectangles({ deviceRectangles, options })).toMatchSnapshot(); }); it('should determine the rectangles with no blockouts', async () => { const options = createStatusAddressToolBarOptions(); const deviceRectangles = createDeviceRectanglesWithData(); expect(determineStatusAddressToolBarRectangles({ deviceRectangles, options })).toMatchSnapshot(); }); it('should determine the rectangles with only status bar blockout', async () => { const options = createStatusAddressToolBarOptions({ blockOutStatusBar: true, isAndroid: true, isMobile: true, isViewPortScreenshot: true, }); const deviceRectangles = createDeviceRectanglesWithData(); expect(determineStatusAddressToolBarRectangles({ deviceRectangles, options })).toMatchSnapshot(); }); it('should determine the rectangles with only toolbar blockout', async () => { const options = createStatusAddressToolBarOptions({ blockOutToolBar: true, isAndroid: true, isMobile: true, isViewPortScreenshot: true, }); const deviceRectangles = createDeviceRectanglesWithData(); expect(determineStatusAddressToolBarRectangles({ deviceRectangles, options })).toMatchSnapshot(); }); it('should determine the rectangles with only sidebar blockout', async () => { const options = createStatusAddressToolBarOptions({ blockOutSideBar: true, isAndroid: true, isMobile: true, isViewPortScreenshot: true, }); const deviceRectangles = createDeviceRectanglesWithData(); expect(determineStatusAddressToolBarRectangles({ deviceRectangles, options })).toMatchSnapshot(); }); it('should determine the rectangles for iOS with blockouts', async () => { const options = createStatusAddressToolBarOptions({ blockOutSideBar: true, blockOutStatusBar: true, blockOutToolBar: true, isAndroid: false, isAndroidNativeWebScreenshot: false, isMobile: true, isViewPortScreenshot: true, }); const deviceRectangles = createDeviceRectanglesWithData(); expect(determineStatusAddressToolBarRectangles({ deviceRectangles, options })).toMatchSnapshot(); }); it('should determine the rectangles for non-mobile device', async () => { const options = createStatusAddressToolBarOptions({ blockOutStatusBar: true, isMobile: false, isViewPortScreenshot: false, }); const deviceRectangles = createDeviceRectanglesWithData(); expect(determineStatusAddressToolBarRectangles({ deviceRectangles, options })).toMatchSnapshot(); }); it('should determine the rectangles for Android without native web screenshot', async () => { const options = createStatusAddressToolBarOptions({ blockOutStatusBar: true, blockOutToolBar: true, isAndroid: true, isAndroidNativeWebScreenshot: false, isMobile: true, isViewPortScreenshot: true, }); const deviceRectangles = createDeviceRectanglesWithData(); expect(determineStatusAddressToolBarRectangles({ deviceRectangles, options })).toMatchSnapshot(); }); it('should handle empty device rectangles', async () => { const options = createStatusAddressToolBarOptions({ blockOutStatusBar: true, blockOutToolBar: true, isAndroid: true, isMobile: true, isViewPortScreenshot: true, }); const deviceRectangles = baseDeviceRectangles; expect(determineStatusAddressToolBarRectangles({ deviceRectangles, options })).toMatchSnapshot(); }); it('should handle edge case with complex mobile configuration', async () => { const options = createStatusAddressToolBarOptions({ blockOutStatusBar: false, blockOutToolBar: false, blockOutSideBar: false, isAndroid: true, isAndroidNativeWebScreenshot: true, isMobile: true, isViewPortScreenshot: true, }); const deviceRectangles = createDeviceRectanglesWithData(); expect(determineStatusAddressToolBarRectangles({ deviceRectangles, options })).toEqual([]); }); }); describe('splitIgnores', () => { it('should split valid elements and regions correctly', () => { const mockElement1 = { elementId: 'element1', selector: '#test1' }; const mockElement2 = { elementId: 'element2', selector: '#test2' }; const mockRegion1 = { x: 10, y: 20, width: 100, height: 150 }; const mockRegion2 = { x: 30, y: 40, width: 200, height: 250 }; const items = [mockElement1, mockRegion1, mockElement2, mockRegion2]; const result = splitIgnores(items); expect(result).toEqual({ elements: [mockElement1, mockElement2], regions: [mockRegion1, mockRegion2] }); }); it('should handle nested element arrays', () => { const mockElement1 = { elementId: 'element1', selector: '#test1' }; const mockElement2 = { elementId: 'element2', selector: '#test2' }; const mockRegion = { x: 10, y: 20, width: 100, height: 150 }; const items = [[mockElement1, mockElement2], mockRegion]; const result = splitIgnores(items); expect(result).toEqual({ elements: [mockElement1, mockElement2], regions: [mockRegion] }); }); it('should handle only elements', () => { const mockElement1 = { elementId: 'element1', selector: '#test1' }; const mockElement2 = { elementId: 'element2', selector: '#test2' }; const items = [mockElement1, mockElement2]; const result = splitIgnores(items); expect(result).toEqual({ elements: [mockElement1, mockElement2], regions: [] }); }); it('should handle only regions', () => { const mockRegion1 = { x: 10, y: 20, width: 100, height: 150 }; const mockRegion2 = { x: 30, y: 40, width: 200, height: 250 }; const items = [mockRegion1, mockRegion2]; const result = splitIgnores(items); expect(result).toEqual({ elements: [], regions: [mockRegion1, mockRegion2] }); }); it('should handle empty array', () => { const result = splitIgnores([]); expect(result).toEqual({ elements: [], regions: [] }); }); it('should throw error for invalid element in top-level array', () => { const invalidItems = [ 'invalid-string', { invalid: 'object' }, 123 ]; expect(() => splitIgnores(invalidItems)).toThrowErrorMatchingSnapshot(); }); it('should throw error for invalid element in nested array', () => { const invalidNestedItems = [ [{ elementId: 'valid', selector: '#valid' }, 'invalid-nested'], { x: 10, y: 20, width: 100, height: 150 } ]; expect(() => splitIgnores(invalidNestedItems)).toThrowErrorMatchingSnapshot(); }); it('should throw error for mixed invalid items', () => { const mixedInvalidItems = [ [{ elementId: 'valid', selector: '#valid' }, { invalid: 'nested' }], 'invalid-string', { x: 10, y: 20, width: 100, height: 150 }, null ]; expect(() => splitIgnores(mixedInvalidItems)).toThrow('Invalid elements or regions'); }); it('should handle mixed valid and invalid items in nested array', () => { const validElement = { elementId: 'element1', selector: '#test1' }; const items = [ [validElement, 'invalid'], { x: 10, y: 20, width: 100, height: 150 } ]; expect(() => splitIgnores(items)).toThrowErrorMatchingSnapshot(); }); it('should handle object that looks like element but missing properties', () => { const almostElement = { elementId: 'element1' }; const items = [almostElement]; expect(() => splitIgnores(items)).toThrow('Invalid elements or regions'); }); it('should handle object that looks like region but missing properties', () => { const almostRegion = { x: 10, y: 20, width: 100 }; const items = [almostRegion]; expect(() => splitIgnores(items)).toThrow('Invalid elements or regions'); }); it('should handle region with non-numeric properties', () => { const invalidRegion = { x: '10', y: 20, width: 100, height: 150 }; const items = [invalidRegion]; expect(() => splitIgnores(items)).toThrow('Invalid elements or regions'); }); it('should handle element with non-string properties', () => { const invalidElement = { elementId: 123, selector: '#test' }; const items = [invalidElement]; expect(() => splitIgnores(items)).toThrow('Invalid elements or regions'); }); }); describe('determineIgnoreRegions', () => { it('should await promises, combine regions and elements, and round coordinates', async () => { const mockElement = { elementId: 'element1', selector: '#test1' }; const mockRegion = { x: 10.7, y: 20.3, width: 100.9, height: 150.1 }; mockGetElementRect.mockResolvedValueOnce({ x: 50.4, y: 60.8, width: 200.2, height: 250.6 }); const ignores = [mockElement, mockRegion]; const result = await determineIgnoreRegions(mockBrowserInstance, ignores); expect(result).toEqual([ { x: 11, y: 20, width: 101, height: 150 }, { x: 50, y: 61, width: 200, height: 251 } ]); expect(mockGetElementRect).toHaveBeenCalledWith('element1'); }); it('should handle Promise.all correctly for chainable elements', async () => { const chainableElement = Promise.resolve({ elementId: 'element1', selector: '#test1' }); const mockRegion = { x: 10, y: 20, width: 100, height: 150 }; mockGetElementRect.mockResolvedValueOnce({ x: 50, y: 60, width: 200, height: 250 }); const ignores = [chainableElement, mockRegion]; const result = await determineIgnoreRegions(mockBrowserInstance, ignores); expect(result).toEqual([ { x: 10, y: 20, width: 100, height: 150 }, { x: 50, y: 60, width: 200, height: 250 } ]); expect(mockGetElementRect).toHaveBeenCalledWith('element1'); }); it('should handle empty arrays', async () => { const result = await determineIgnoreRegions(mockBrowserInstance, []); expect(result).toEqual([]); expect(mockGetElementRect).not.toHaveBeenCalled(); }); it('should delegate validation to splitIgnores and propagate errors', async () => { const invalidIgnores = ['invalid-string']; // @ts-expect-error - invalid ignore regions for testing await expect(determineIgnoreRegions(mockBrowserInstance, invalidIgnores)) .rejects.toThrow('Invalid elements or regions'); }); }); describe('determineDeviceBlockOuts', () => { it('should return empty array when no blockouts are enabled', async () => { const options = createDeviceBlockOutsOptions(); const result = await determineDeviceBlockOuts(options); expect(result).toEqual([]); }); it('should return statusBar when blockOutStatusBar is enabled', async () => { const options = createDeviceBlockOutsOptions({ screenCompareOptions: { blockOutStatusBar: true, blockOutToolBar: false, } }); const result = await determineDeviceBlockOuts(options); expect(result).toMatchSnapshot(); }); it('should return homeBar when blockOutToolBar is enabled for non-Android device', async () => { const options = createDeviceBlockOutsOptions({ isAndroid: false, screenCompareOptions: { blockOutStatusBar: false, blockOutToolBar: true, } }); const result = await determineDeviceBlockOuts(options); expect(result).toMatchSnapshot(); }); it('should return both statusBar and homeBar when both blockouts are enabled for non-Android device', async () => { const options = createDeviceBlockOutsOptions({ isAndroid: false, screenCompareOptions: { blockOutStatusBar: true, blockOutToolBar: true, } }); const result = await determineDeviceBlockOuts(options); expect(result).toMatchSnapshot(); }); it('should not return homeBar when blockOutToolBar is enabled for Android device', async () => { const options = createDeviceBlockOutsOptions({ isAndroid: true, screenCompareOptions: { blockOutStatusBar: false, blockOutToolBar: true, } }); const result = await determineDeviceBlockOuts(options); expect(result).toEqual([]); }); it('should only return statusBar when both blockouts are enabled for Android device', async () => { const options = createDeviceBlockOutsOptions({ isAndroid: true, screenCompareOptions: { blockOutStatusBar: true, blockOutToolBar: true, } }); const result = await determineDeviceBlockOuts(options); expect(result).toMatchSnapshot(); }); it('should handle custom device rectangles', async () => { const customDeviceRectangles = createDeviceRectanglesWithData({ statusBar: { x: 10, y: 20, width: 500, height: 60 }, homeBar: { x: 10, y: 900, width: 500, height: 40 } }); const options = createDeviceBlockOutsOptions({ isAndroid: false, screenCompareOptions: { blockOutStatusBar: true, blockOutToolBar: true, }, instanceData: { ...createDeviceBlockOutsOptions().instanceData, deviceRectangles: customDeviceRectangles } }); const result = await determineDeviceBlockOuts(options); expect(result).toMatchSnapshot(); }); }); describe('prepareIgnoreRectangles', () => { it('should return empty ignored boxes and false hasIgnoreRectangles when no inputs provided', async () => { const options = createPrepareIgnoreRectanglesOptions(); const result = await prepareIgnoreRectangles(options); expect(result).toEqual({ ignoredBoxes: [], hasIgnoreRectangles: false }); }); it('should handle blockOut and ignoreRegions without mobile web rectangles', async () => { const options = createPrepareIgnoreRectanglesOptions({ blockOut: [{ x: 10, y: 20, width: 100, height: 50 }], ignoreRegions: [{ x: 200, y: 300, width: 150, height: 75 }], devicePixelRatio: 2, isAndroid: false }); const result = await prepareIgnoreRectangles(options); expect(result.hasIgnoreRectangles).toBe(true); expect(result.ignoredBoxes).toMatchSnapshot(); }); it('should handle Android device with different DPR calculation', async () => { const options = createPrepareIgnoreRectanglesOptions({ blockOut: [{ x: 10, y: 20, width: 100, height: 50 }], ignoreRegions: [{ x: 200, y: 300, width: 150, height: 75 }], devicePixelRatio: 3, isAndroid: true }); const result = await prepareIgnoreRectangles(options); expect(result.hasIgnoreRectangles).toBe(true); expect(result.ignoredBoxes).toMatchSnapshot(); }); it('should skip mobile web rectangles when not mobile', async () => { const options = createPrepareIgnoreRectanglesOptions({ isMobile: false, isNativeContext: false, imageCompareOptions: { blockOutSideBar: true, blockOutStatusBar: true, blockOutToolBar: true, } }); const result = await prepareIgnoreRectangles(options); expect(result).toEqual({ ignoredBoxes: [], hasIgnoreRectangles: false }); }); it('should skip mobile web rectangles when in native context', async () => { const options = createPrepareIgnoreRectanglesOptions({ isMobile: true, isNativeContext: true, imageCompareOptions: { blockOutSideBar: true, blockOutStatusBar: true, blockOutToolBar: true, } }); const result = await prepareIgnoreRectangles(options); expect(result).toEqual({ ignoredBoxes: [], hasIgnoreRectangles: false }); }); it('should include mobile web rectangles when mobile and not native context', async () => { const options = createPrepareIgnoreRectanglesOptions({ isMobile: true, isNativeContext: false, isAndroid: false, isAndroidNativeWebScreenshot: true, isViewPortScreenshot: true, devicePixelRatio: 2, imageCompareOptions: { blockOutSideBar: true, blockOutStatusBar: true, blockOutToolBar: true, } }); const result = await prepareIgnoreRectangles(options); expect(result.hasIgnoreRectangles).toBe(true); expect(result.ignoredBoxes).toMatchSnapshot(); }); it('should filter out zero-sized rectangles from mobile web context', async () => { const deviceRectanglesWithZeros = createDeviceRectanglesWithData({ statusBarAndAddressBar: { x: 0, y: 0, width: 0, height: 0 }, // Will be filtered bottomBar: { x: 0, y: 0, width: 390, height: 47 }, // Will be kept }); const options = createPrepareIgnoreRectanglesOptions({ deviceRectangles: deviceRectanglesWithZeros, isMobile: true, isNativeContext: false, isAndroid: false, isAndroidNativeWebScreenshot: true, isViewPortScreenshot: true, devicePixelRatio: 2, imageCompareOptions: { blockOutStatusBar: true, blockOutToolBar: true, } }); const result = await prepareIgnoreRectangles(options); expect(result.hasIgnoreRectangles).toBe(true); expect(result.ignoredBoxes).toMatchSnapshot(); }); it('should handle empty web rectangles without filtering', async () => { const options = createPrepareIgnoreRectanglesOptions({ isMobile: true, isNativeContext: false, isAndroid: true, isAndroidNativeWebScreenshot: false, isViewPortScreenshot: true, imageCompareOptions: { blockOutStatusBar: false, blockOutToolBar: false, blockOutSideBar: false, } }); const result = await prepareIgnoreRectangles(options); expect(result).toEqual({ ignoredBoxes: [], hasIgnoreRectangles: false }); }); it('should combine all rectangle sources correctly', async () => { const options = createPrepareIgnoreRectanglesOptions({ blockOut: [{ x: 10, y: 20, width: 100, height: 50 }], ignoreRegions: [{ x: 200, y: 300, width: 150, height: 75 }], isMobile: true, isNativeContext: false, isAndroid: false, isAndroidNativeWebScreenshot: true, isViewPortScreenshot: true, devicePixelRatio: 2, imageCompareOptions: { blockOutStatusBar: true, } }); const result = await prepareIgnoreRectangles(options); expect(result.hasIgnoreRectangles).toBe(true); expect(result.ignoredBoxes).toMatchSnapshot(); }); }); });