UNPKG

@wdio/image-comparison-core

Version:

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

315 lines (314 loc) 16.2 kB
import { beforeEach, describe, it, expect, vi, afterEach } from 'vitest'; import { join } from 'node:path'; import logger from '@wdio/logger'; import { takeElementScreenshot } from './takeElementScreenshots.js'; import { takeBase64BiDiScreenshot, takeWebElementScreenshot } from './screenshots.js'; import { makeCroppedBase64Image } from './images.js'; import { getBase64ScreenshotSize, waitFor, hasResizeDimensions } from '../helpers/utils.js'; const log = logger('test'); vi.mock('./screenshots.js', () => ({ takeBase64BiDiScreenshot: vi.fn().mockResolvedValue('bidi-screenshot-data'), takeWebElementScreenshot: vi.fn().mockResolvedValue({ base64Image: 'web-element-screenshot-data', rectangles: { x: 0, y: 0, width: 100, height: 100 }, isWebDriverElementScreenshot: false }) })); vi.mock('./images.js', () => ({ makeCroppedBase64Image: vi.fn().mockResolvedValue('cropped-screenshot-data') })); vi.mock('../clientSideScripts/scrollElementIntoView.js', () => ({ default: vi.fn() })); vi.mock('../clientSideScripts/scrollToPosition.js', () => ({ default: vi.fn() })); vi.mock('../helpers/utils.js', () => ({ getBase64ScreenshotSize: vi.fn().mockReturnValue({ width: 100, height: 100 }), waitFor: vi.fn().mockResolvedValue(undefined), hasResizeDimensions: vi.fn().mockReturnValue(false) })); vi.mock('@wdio/logger', () => import(join(process.cwd(), '__mocks__', '@wdio/logger'))); describe('takeElementScreenshot', () => { const takeBase64BiDiScreenshotSpy = vi.mocked(takeBase64BiDiScreenshot); const takeWebElementScreenshotSpy = vi.mocked(takeWebElementScreenshot); const makeCroppedBase64ImageSpy = vi.mocked(makeCroppedBase64Image); const getBase64ScreenshotSizeSpy = vi.mocked(getBase64ScreenshotSize); const waitForSpy = vi.mocked(waitFor); const hasResizeDimensionsSpy = vi.mocked(hasResizeDimensions); const executeMock = vi.fn().mockResolvedValue(undefined); const getElementRectMock = vi.fn().mockResolvedValue({ x: 10, y: 20, width: 100, height: 200 }); const browserInstance = { execute: executeMock, getElementRect: getElementRectMock }; const baseOptions = { addressBarShadowPadding: 6, autoElementScroll: false, deviceName: 'desktop', devicePixelRatio: 2, deviceRectangles: { bottomBar: { height: 0, width: 390, x: 0, y: 800 }, homeBar: { height: 34, width: 390, x: 0, y: 780 }, leftSidePadding: { height: 0, width: 0, x: 0, y: 0 }, rightSidePadding: { height: 0, width: 0, x: 0, y: 0 }, screenSize: { height: 844, width: 390 }, statusBar: { height: 47, width: 390, x: 0, y: 0 }, statusBarAndAddressBar: { height: 47, width: 390, x: 0, y: 0 }, viewport: { height: 733, width: 390, x: 0, y: 47 } }, element: { elementId: 'test-element' }, isEmulated: false, initialDevicePixelRatio: 2, innerHeight: 900, isAndroid: false, isAndroidChromeDriverScreenshot: false, isAndroidNativeWebScreenshot: false, isIOS: false, isLandscape: false, isMobile: false, resizeDimensions: { top: 0, right: 0, bottom: 0, left: 0 }, toolBarShadowPadding: 5 }; afterEach(() => { vi.clearAllMocks(); }); describe('BiDi screenshots', () => { it('should take BiDi screenshot from viewport when shouldUseBidi is true', async () => { const result = await takeElementScreenshot(browserInstance, baseOptions, true); expect(result).toEqual({ base64Image: 'bidi-screenshot-data', isWebDriverElementScreenshot: false }); expect(getElementRectMock).toHaveBeenCalledWith('test-element'); expect(takeBase64BiDiScreenshotSpy).toHaveBeenCalledWith({ browserInstance, origin: 'viewport', clip: { x: 10, y: 20, width: 100, height: 200 } }); expect(takeWebElementScreenshotSpy).not.toHaveBeenCalled(); expect(makeCroppedBase64ImageSpy).not.toHaveBeenCalled(); }); 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')); const result = await takeElementScreenshot(browserInstance, baseOptions, 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 } }); }); 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); await expect(takeElementScreenshot(browserInstance, baseOptions, true)).rejects.toThrow(error); expect(takeBase64BiDiScreenshotSpy).toHaveBeenCalledTimes(1); expect(takeWebElementScreenshotSpy).not.toHaveBeenCalled(); }); }); describe('Legacy screenshots', () => { let logErrorSpy; beforeEach(() => { logErrorSpy = vi.spyOn(log, 'error').mockImplementation(() => { }); }); afterEach(() => { vi.clearAllMocks(); logErrorSpy.mockRestore(); }); it('should take legacy screenshot when shouldUseBidi is false', async () => { const result = await takeElementScreenshot(browserInstance, baseOptions, false); expect(result).toEqual({ base64Image: 'cropped-screenshot-data', isWebDriverElementScreenshot: false }); expect(takeWebElementScreenshotSpy).toHaveBeenCalledWith({ addressBarShadowPadding: 6, browserInstance, devicePixelRatio: 2, deviceRectangles: baseOptions.deviceRectangles, element: baseOptions.element, initialDevicePixelRatio: 2, isEmulated: false, innerHeight: 900, isAndroid: false, isAndroidChromeDriverScreenshot: false, isAndroidNativeWebScreenshot: false, isIOS: false, isLandscape: false, toolBarShadowPadding: 5, fallback: false }); expect(makeCroppedBase64ImageSpy).toHaveBeenCalledWith({ addIOSBezelCorners: false, base64Image: 'web-element-screenshot-data', deviceName: 'desktop', devicePixelRatio: 2, isWebDriverElementScreenshot: false, isIOS: false, isLandscape: false, rectangles: { x: 0, y: 0, width: 100, height: 100 }, resizeDimensions: { top: 0, right: 0, bottom: 0, left: 0 } }); expect(takeBase64BiDiScreenshotSpy).not.toHaveBeenCalled(); }); it('should handle auto scroll when enabled', async () => { const optionsWithScroll = { ...baseOptions, autoElementScroll: true }; executeMock.mockResolvedValueOnce(100); // scroll position const result = await takeElementScreenshot(browserInstance, optionsWithScroll, false); expect(result).toEqual({ base64Image: 'cropped-screenshot-data', isWebDriverElementScreenshot: false }); expect(executeMock).toHaveBeenCalledTimes(2); // First call for scrolling element into view expect(executeMock.mock.calls[0]).toMatchSnapshot(); // Second call for scrolling back to original position expect(executeMock.mock.calls[1]).toMatchSnapshot(); expect(waitForSpy).toHaveBeenCalledWith(100); }); it('should not scroll back when autoElementScroll is enabled but no current position', async () => { const optionsWithScroll = { ...baseOptions, autoElementScroll: true }; executeMock.mockResolvedValueOnce(undefined); // no scroll position returned const result = await takeElementScreenshot(browserInstance, optionsWithScroll, false); expect(result).toMatchSnapshot(); expect(executeMock).toHaveBeenCalledTimes(1); // Only the scroll into view call expect(waitForSpy).toHaveBeenCalledWith(100); }); it('should enable fallback when resizeDimensions is provided', async () => { const optionsWithResize = { ...baseOptions, resizeDimensions: { top: 10, right: 10, bottom: 10, left: 10 } }; hasResizeDimensionsSpy.mockReturnValueOnce(true); const result = await takeElementScreenshot(browserInstance, optionsWithResize, false); expect(result).toMatchSnapshot(); expect(hasResizeDimensionsSpy).toHaveBeenCalledWith(optionsWithResize.resizeDimensions); expect(takeWebElementScreenshotSpy).toHaveBeenCalledWith(expect.objectContaining({ fallback: true })); }); it('should enable fallback when device is emulated', async () => { const optionsEmulated = { ...baseOptions, isEmulated: true }; const result = await takeElementScreenshot(browserInstance, optionsEmulated, false); expect(result).toMatchSnapshot(); expect(takeWebElementScreenshotSpy).toHaveBeenCalledWith(expect.objectContaining({ fallback: true })); }); it('should disable fallback when no resizeDimensions and not emulated', async () => { const optionsNoResize = { ...baseOptions, resizeDimensions: undefined, isEmulated: false }; hasResizeDimensionsSpy.mockReturnValueOnce(false); const result = await takeElementScreenshot(browserInstance, optionsNoResize, false); expect(result).toMatchSnapshot(); expect(hasResizeDimensionsSpy).toHaveBeenCalledWith(undefined); expect(takeWebElementScreenshotSpy).toHaveBeenCalledWith(expect.objectContaining({ fallback: false })); }); it('should handle zero dimensions by falling back to viewport size', async () => { takeWebElementScreenshotSpy.mockResolvedValueOnce({ base64Image: 'web-element-screenshot-data', rectangles: { x: 0, y: 0, width: 0, height: 0 }, isWebDriverElementScreenshot: false }); const result = await takeElementScreenshot(browserInstance, baseOptions, false); expect(result).toMatchSnapshot(); expect(getBase64ScreenshotSizeSpy).toHaveBeenCalledWith('web-element-screenshot-data'); expect(logErrorSpy.mock.calls).toMatchSnapshot(); expect(makeCroppedBase64ImageSpy).toHaveBeenCalledWith(expect.objectContaining({ rectangles: { x: 0, y: 0, width: 100, height: 100 } })); }); it('should handle zero width only', async () => { takeWebElementScreenshotSpy.mockResolvedValueOnce({ base64Image: 'web-element-screenshot-data', rectangles: { x: 50, y: 100, width: 0, height: 150 }, isWebDriverElementScreenshot: true }); const result = await takeElementScreenshot(browserInstance, baseOptions, false); expect(result).toMatchSnapshot(); expect(getBase64ScreenshotSizeSpy).toHaveBeenCalledWith('web-element-screenshot-data'); expect(logErrorSpy.mock.calls).toMatchSnapshot(); }); it('should handle zero height only', async () => { takeWebElementScreenshotSpy.mockResolvedValueOnce({ base64Image: 'web-element-screenshot-data', rectangles: { x: 50, y: 100, width: 150, height: 0 }, isWebDriverElementScreenshot: true }); const result = await takeElementScreenshot(browserInstance, baseOptions, false); expect(result).toMatchSnapshot(); expect(getBase64ScreenshotSizeSpy).toHaveBeenCalledWith('web-element-screenshot-data'); expect(logErrorSpy.mock.calls).toMatchSnapshot(); }); it('should pass correct WebDriver element screenshot flag', async () => { takeWebElementScreenshotSpy.mockResolvedValueOnce({ base64Image: 'web-element-screenshot-data', rectangles: { x: 0, y: 0, width: 100, height: 100 }, isWebDriverElementScreenshot: true }); const result = await takeElementScreenshot(browserInstance, baseOptions, false); expect(result).toEqual({ base64Image: 'cropped-screenshot-data', isWebDriverElementScreenshot: true }); expect(makeCroppedBase64ImageSpy).toHaveBeenCalledWith(expect.objectContaining({ isWebDriverElementScreenshot: true })); }); }); describe('Edge cases', () => { it('should handle devicePixelRatio values and fallback to NaN when falsy', async () => { const optionsWithValidDPR = { ...baseOptions, devicePixelRatio: 1 }; const result1 = await takeElementScreenshot(browserInstance, optionsWithValidDPR, false); expect(result1).toMatchSnapshot(); expect(takeWebElementScreenshotSpy).toHaveBeenCalledWith(expect.objectContaining({ devicePixelRatio: 1 })); expect(makeCroppedBase64ImageSpy).toHaveBeenCalledWith(expect.objectContaining({ devicePixelRatio: 1 })); vi.clearAllMocks(); const optionsWithZeroDPR = { ...baseOptions, devicePixelRatio: 0 }; const result2 = await takeElementScreenshot(browserInstance, optionsWithZeroDPR, false); expect(result2).toMatchSnapshot(); expect(takeWebElementScreenshotSpy).toHaveBeenCalledWith(expect.objectContaining({ devicePixelRatio: 0 })); expect(makeCroppedBase64ImageSpy).toHaveBeenCalledWith(expect.objectContaining({ devicePixelRatio: NaN })); }); it('should handle undefined innerHeight', async () => { const optionsWithUndefinedHeight = { ...baseOptions, innerHeight: undefined }; const result = await takeElementScreenshot(browserInstance, optionsWithUndefinedHeight, false); expect(result).toMatchSnapshot(); expect(takeWebElementScreenshotSpy).toHaveBeenCalledWith(expect.objectContaining({ innerHeight: undefined })); }); it('should default shouldUseBidi to false when not provided', async () => { const result = await takeElementScreenshot(browserInstance, baseOptions); expect(result).toEqual({ base64Image: 'cropped-screenshot-data', isWebDriverElementScreenshot: false }); expect(takeBase64BiDiScreenshotSpy).not.toHaveBeenCalled(); expect(takeWebElementScreenshotSpy).toHaveBeenCalled(); }); }); });