UNPKG

@wdio/image-comparison-core

Version:

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

1,228 lines (1,227 loc) 61.9 kB
import { describe, it, expect, vi, beforeEach, afterEach } from 'vitest'; import { join } from 'node:path'; import { promises as fsPromises, readFileSync, writeFileSync } from 'node:fs'; import logger from '@wdio/logger'; import { DEFAULT_RESIZE_DIMENSIONS } from '../helpers/constants.js'; import { checkIfImageExists, removeDiffImageIfExists, checkBaselineImageExists, getRotatedImageIfNeeded, logDimensionWarning, getAdjustedAxis, handleIOSBezelCorners, cropAndConvertToDataURL, makeCroppedBase64Image, makeFullPageBase64Image, rotateBase64Image, takeResizedBase64Screenshot, } from './images.js'; const log = logger('test'); vi.mock('jimp', () => ({ Jimp: Object.assign(vi.fn().mockImplementation(() => ({ composite: vi.fn().mockReturnThis(), getBase64: vi.fn().mockResolvedValue('data:image/png;base64,mockImageData'), opacity: vi.fn().mockReturnThis(), rotate: vi.fn().mockReturnThis(), crop: vi.fn().mockReturnThis(), })), { read: vi.fn(), MIME_PNG: 'image/png', }), JimpMime: { png: 'image/png', }, })); vi.mock('@wdio/logger', () => import(join(process.cwd(), '__mocks__', '@wdio/logger'))); vi.mock('node:fs', async () => { const actual = await vi.importActual('node:fs'); return { ...actual, promises: { access: vi.fn(), unlink: vi.fn(), }, readFileSync: vi.fn(), writeFileSync: vi.fn(), constants: { R_OK: 4, }, }; }); vi.mock('../helpers/utils.js', () => ({ getBase64ScreenshotSize: vi.fn(), getAndCreatePath: vi.fn(), getIosBezelImageNames: vi.fn(), updateVisualBaseline: vi.fn(), calculateDprData: vi.fn(), })); vi.mock('../helpers/constants.js', () => ({ DEFAULT_RESIZE_DIMENSIONS: { top: 0, right: 0, bottom: 0, left: 0 }, supportedIosBezelDevices: [ 'iphonex', 'iphonexs', 'iphonexsmax', 'iphonexr', 'iphone11', 'iphone11pro', 'iphone11promax', 'iphone12', 'iphone12mini', 'iphone12pro', 'iphone12promax', 'iphone13', 'iphone13mini', 'iphone13pro', 'iphone13promax', 'iphone14', 'iphone14plus', 'iphone14pro', 'iphone14promax', 'iphone15', 'ipadmini', 'ipadair', 'ipadpro11', 'ipadpro129' ], })); vi.mock('./rectangles.js', () => ({ isWdioElement: vi.fn(), determineStatusAddressToolBarRectangles: vi.fn(), })); vi.mock('./screenshots.js', () => ({ takeBase64Screenshot: vi.fn(), })); describe('checkIfImageExists', () => { let accessSpy; beforeEach(() => { accessSpy = vi.spyOn(fsPromises, 'access'); }); afterEach(() => { vi.clearAllMocks(); accessSpy.mockRestore(); }); it('should return true when file exists', async () => { accessSpy.mockResolvedValue(undefined); const result = await checkIfImageExists('/path/to/image.png'); expect(result).toMatchSnapshot(); }); it('should return false when file does not exist', async () => { accessSpy.mockRejectedValue(new Error('File not found')); const result = await checkIfImageExists('/path/to/image.png'); expect(result).toMatchSnapshot(); }); }); describe('removeDiffImageIfExists', () => { let accessSpy; let unlinkSpy; let logInfoSpy; beforeEach(() => { accessSpy = vi.spyOn(fsPromises, 'access'); unlinkSpy = vi.spyOn(fsPromises, 'unlink'); logInfoSpy = vi.spyOn(log, 'info').mockImplementation(() => { }); }); afterEach(() => { vi.clearAllMocks(); accessSpy.mockRestore(); unlinkSpy.mockRestore(); logInfoSpy.mockRestore(); }); // Note: We mock fsPromises.access here because removeDiffImageIfExists calls checkIfImageExists internally, // which in turn calls fsPromises.access. This allows us to test the real integration between the functions // without artificial mocking of internal dependencies. it('should remove file when it exists', async () => { accessSpy.mockResolvedValue(undefined); unlinkSpy.mockResolvedValue(undefined); await removeDiffImageIfExists('/path/to/diff.png'); expect(accessSpy).toHaveBeenCalledWith('/path/to/diff.png', 4); expect(unlinkSpy).toHaveBeenCalledWith('/path/to/diff.png'); expect(logInfoSpy).toHaveBeenCalledWith('Successfully removed the diff image before comparing at /path/to/diff.png'); }); it('should do nothing when file does not exist', async () => { accessSpy.mockRejectedValue(new Error('File not found')); await removeDiffImageIfExists('/path/to/diff.png'); expect(accessSpy).toHaveBeenCalledWith('/path/to/diff.png', 4); expect(unlinkSpy).not.toHaveBeenCalled(); expect(logInfoSpy).not.toHaveBeenCalled(); }); it('should throw error when file exists but cannot be removed', async () => { accessSpy.mockResolvedValue(undefined); const unlinkError = new Error('Permission denied'); unlinkSpy.mockRejectedValue(unlinkError); await expect(removeDiffImageIfExists('/path/to/diff.png')).rejects.toThrow('Could not remove the diff image. The following error was thrown: Error: Permission denied'); expect(accessSpy).toHaveBeenCalledWith('/path/to/diff.png', 4); expect(unlinkSpy).toHaveBeenCalledWith('/path/to/diff.png'); expect(logInfoSpy).not.toHaveBeenCalled(); }); }); describe('checkBaselineImageExists', () => { let accessSpy; let logInfoSpy; beforeEach(() => { accessSpy = vi.spyOn(fsPromises, 'access'); logInfoSpy = vi.spyOn(log, 'info').mockImplementation(() => { }); }); afterEach(() => { vi.clearAllMocks(); accessSpy.mockRestore(); logInfoSpy.mockRestore(); }); it('should do nothing when baseline exists and no flags are set', async () => { accessSpy.mockResolvedValue(undefined); await checkBaselineImageExists({ actualFilePath: '/path/to/actual.png', baselineFilePath: '/path/to/baseline.png' }); expect(accessSpy).toHaveBeenCalledWith('/path/to/baseline.png', 4); expect(logInfoSpy).not.toHaveBeenCalled(); }); it('should auto-save baseline when file does not exist and autoSaveBaseline is true', async () => { accessSpy.mockRejectedValue(new Error('File not found')); vi.mocked(readFileSync).mockReturnValue(Buffer.from('image data')); vi.mocked(writeFileSync).mockImplementation(() => { }); await checkBaselineImageExists({ actualFilePath: '/path/to/actual.png', baselineFilePath: '/path/to/baseline.png', autoSaveBaseline: true }); expect(accessSpy).toHaveBeenCalledWith('/path/to/baseline.png', 4); expect(vi.mocked(readFileSync)).toHaveBeenCalledWith('/path/to/actual.png'); expect(vi.mocked(writeFileSync)).toHaveBeenCalledWith('/path/to/baseline.png', Buffer.from('image data')); expect(logInfoSpy.mock.calls).toMatchSnapshot(); }); it('should update baseline when updateBaseline is true', async () => { vi.mocked(readFileSync).mockReturnValue(Buffer.from('image data')); vi.mocked(writeFileSync).mockImplementation(() => { }); await checkBaselineImageExists({ actualFilePath: '/path/to/actual.png', baselineFilePath: '/path/to/baseline.png', updateBaseline: true }); expect(vi.mocked(readFileSync)).toHaveBeenCalledWith('/path/to/actual.png'); expect(vi.mocked(writeFileSync)).toHaveBeenCalledWith('/path/to/baseline.png', Buffer.from('image data')); expect(logInfoSpy.mock.calls).toMatchSnapshot(); }); it('should use provided base64 when auto-saving baseline', async () => { accessSpy.mockRejectedValue(new Error('File not found')); const base64Actual = Buffer.from('image data').toString('base64'); vi.mocked(writeFileSync).mockImplementation(() => { }); await checkBaselineImageExists({ actualFilePath: '/path/to/actual.png', baselineFilePath: '/path/to/baseline.png', autoSaveBaseline: true, actualBase64Image: base64Actual, }); expect(vi.mocked(readFileSync)).not.toHaveBeenCalled(); expect(vi.mocked(writeFileSync)).toHaveBeenCalledWith('/path/to/baseline.png', Buffer.from(base64Actual, 'base64')); expect(logInfoSpy.mock.calls).toMatchSnapshot(); }); it('should throw error when file does not exist and autoSaveBaseline is false', async () => { accessSpy.mockRejectedValue(new Error('File not found')); await expect(checkBaselineImageExists({ actualFilePath: '/path/to/actual.png', baselineFilePath: '/path/to/baseline.png', autoSaveBaseline: false })).rejects.toThrow(); expect(accessSpy).toHaveBeenCalledWith('/path/to/baseline.png', 4); expect(vi.mocked(readFileSync)).not.toHaveBeenCalled(); expect(vi.mocked(writeFileSync)).not.toHaveBeenCalled(); expect(logInfoSpy).not.toHaveBeenCalled(); }); it('should throw error when copying fails', async () => { accessSpy.mockRejectedValue(new Error('File not found')); const copyError = new Error('Permission denied'); vi.mocked(readFileSync).mockImplementation(() => { throw copyError; }); await expect(checkBaselineImageExists({ actualFilePath: '/path/to/actual.png', baselineFilePath: '/path/to/baseline.png', autoSaveBaseline: true })).rejects.toThrow(); expect(accessSpy).toHaveBeenCalledWith('/path/to/baseline.png', 4); expect(vi.mocked(readFileSync)).toHaveBeenCalledWith('/path/to/actual.png'); expect(vi.mocked(writeFileSync)).not.toHaveBeenCalled(); expect(logInfoSpy).not.toHaveBeenCalled(); }); it('should mention missing actual file in error when not saved to disk', async () => { accessSpy.mockRejectedValue(new Error('File not found')); vi.mocked(fsPromises.access).mockRejectedValue(new Error('File not found')); await expect(checkBaselineImageExists({ actualFilePath: '/path/to/actual.png', baselineFilePath: '/path/to/baseline.png', autoSaveBaseline: false })).rejects.toThrow(/actual image was not saved to disk/); expect(vi.mocked(readFileSync)).not.toHaveBeenCalled(); expect(vi.mocked(writeFileSync)).not.toHaveBeenCalled(); }); }); describe('rotateBase64Image', () => { let jimpReadMock; beforeEach(async () => { const jimp = await import('jimp'); jimpReadMock = vi.mocked(jimp.Jimp.read); }); afterEach(() => { vi.clearAllMocks(); }); it('should rotate image by specified degrees', async () => { const mockImage = { rotate: vi.fn().mockReturnThis(), getBase64: vi.fn().mockResolvedValue('data:image/png;base64,rotatedImageData') }; jimpReadMock.mockResolvedValue(mockImage); const result = await rotateBase64Image({ base64Image: 'originalImageData', degrees: 90 }); expect(result).toMatchSnapshot(); expect(jimpReadMock.mock.calls).toMatchSnapshot(); expect(mockImage.rotate.mock.calls).toMatchSnapshot(); expect(mockImage.getBase64.mock.calls).toMatchSnapshot(); }); it('should rotate image by 180 degrees', async () => { const mockImage = { rotate: vi.fn().mockReturnThis(), getBase64: vi.fn().mockResolvedValue('data:image/png;base64,rotatedImageData') }; jimpReadMock.mockResolvedValue(mockImage); const result = await rotateBase64Image({ base64Image: 'originalImageData', degrees: 180 }); expect(result).toMatchSnapshot(); expect(mockImage.rotate.mock.calls).toMatchSnapshot(); }); it('should handle different base64 input', async () => { const mockImage = { rotate: vi.fn().mockReturnThis(), getBase64: vi.fn().mockResolvedValue('data:image/png;base64,differentRotatedData') }; jimpReadMock.mockResolvedValue(mockImage); const result = await rotateBase64Image({ base64Image: 'differentImageData', degrees: 270 }); expect(result).toMatchSnapshot(); expect(jimpReadMock.mock.calls).toMatchSnapshot(); expect(mockImage.rotate.mock.calls).toMatchSnapshot(); }); }); describe('getRotatedImageIfNeeded', () => { let getBase64ScreenshotSizeMock; beforeEach(async () => { const utils = await import('../helpers/utils.js'); getBase64ScreenshotSizeMock = vi.mocked(utils.getBase64ScreenshotSize); vi.clearAllMocks(); }); afterEach(() => { vi.clearAllMocks(); }); it('should return original image when no rotation is needed', async () => { getBase64ScreenshotSizeMock.mockReturnValue({ width: 1920, height: 1080 }); const result = await getRotatedImageIfNeeded({ isWebDriverElementScreenshot: false, isLandscape: false, base64Image: 'originalImageData' }); expect(result).toMatchSnapshot(); expect(getBase64ScreenshotSizeMock.mock.calls).toMatchSnapshot(); }); it('should call rotateBase64Image when landscape and height > width', async () => { getBase64ScreenshotSizeMock.mockReturnValue({ width: 1080, height: 1920 }); // We'll test that the function calls rotateBase64Image by checking the result // Since we can't easily mock the internal function, we'll verify the logic works const result = await getRotatedImageIfNeeded({ isWebDriverElementScreenshot: false, isLandscape: true, base64Image: 'originalImageData' }); expect(result).toMatchSnapshot(); expect(getBase64ScreenshotSizeMock.mock.calls).toMatchSnapshot(); }); it('should not rotate when isWebDriverElementScreenshot is true', async () => { getBase64ScreenshotSizeMock.mockReturnValue({ width: 1080, height: 1920 }); const result = await getRotatedImageIfNeeded({ isWebDriverElementScreenshot: true, isLandscape: true, base64Image: 'originalImageData' }); expect(result).toMatchSnapshot(); expect(getBase64ScreenshotSizeMock.mock.calls).toMatchSnapshot(); }); it('should not rotate when width >= height', async () => { getBase64ScreenshotSizeMock.mockReturnValue({ width: 1920, height: 1080 }); const result = await getRotatedImageIfNeeded({ isWebDriverElementScreenshot: false, isLandscape: true, base64Image: 'originalImageData' }); expect(result).toMatchSnapshot(); expect(getBase64ScreenshotSizeMock.mock.calls).toMatchSnapshot(); }); it('should not rotate when not landscape', async () => { getBase64ScreenshotSizeMock.mockReturnValue({ width: 1080, height: 1920 }); const result = await getRotatedImageIfNeeded({ isWebDriverElementScreenshot: false, isLandscape: false, base64Image: 'originalImageData' }); expect(result).toMatchSnapshot(); expect(getBase64ScreenshotSizeMock.mock.calls).toMatchSnapshot(); }); }); describe('logDimensionWarning', () => { let logWarnSpy; beforeEach(() => { logWarnSpy = vi.spyOn(log, 'warn').mockImplementation(() => { }); }); afterEach(() => { vi.clearAllMocks(); logWarnSpy.mockRestore(); }); it('should log warning for LEFT type', () => { logDimensionWarning({ dimension: 60, maxDimension: 1000, position: -10, type: 'LEFT' }); expect(logWarnSpy.mock.calls).toMatchSnapshot(); }); it('should log warning for RIGHT type', () => { logDimensionWarning({ dimension: 50, maxDimension: 1000, position: 1100, type: 'RIGHT' }); expect(logWarnSpy.mock.calls).toMatchSnapshot(); }); it('should log warning for TOP type', () => { logDimensionWarning({ dimension: 30, maxDimension: 800, position: -5, type: 'TOP' }); expect(logWarnSpy.mock.calls).toMatchSnapshot(); }); it('should log warning for BOTTOM type', () => { logDimensionWarning({ dimension: 40, maxDimension: 800, position: 850, type: 'BOTTOM' }); expect(logWarnSpy.mock.calls).toMatchSnapshot(); }); }); describe('getAdjustedAxis', () => { it('should return adjusted coordinates within bounds', () => { const result = getAdjustedAxis({ length: 100, maxDimension: 1000, paddingEnd: 10, paddingStart: 5, start: 50, warningType: 'WIDTH' }); expect(result).toMatchSnapshot(); }); it('should handle zero padding', () => { const result = getAdjustedAxis({ length: 100, maxDimension: 1000, paddingEnd: 0, paddingStart: 0, start: 50, warningType: 'WIDTH' }); expect(result).toMatchSnapshot(); }); it('should clamp start position to 0 when it goes below 0', () => { const result = getAdjustedAxis({ length: 100, maxDimension: 1000, paddingEnd: 10, paddingStart: 60, // This will make adjustedStart = 50 - 60 = -10 start: 50, warningType: 'WIDTH' }); expect(result).toMatchSnapshot(); }); it('should clamp end position to maxDimension when it exceeds maxDimension', () => { const result = getAdjustedAxis({ length: 100, maxDimension: 1000, paddingEnd: 50, // This will make adjustedEnd = 950 + 100 + 50 = 1100 paddingStart: 10, start: 950, warningType: 'WIDTH' }); expect(result).toMatchSnapshot(); }); it('should handle both start and end clamping', () => { const result = getAdjustedAxis({ length: 100, maxDimension: 1000, paddingEnd: 50, paddingStart: 60, start: 50, warningType: 'WIDTH' }); expect(result).toMatchSnapshot(); }); it('should handle HEIGHT warning type correctly', () => { const result = getAdjustedAxis({ length: 100, maxDimension: 800, paddingEnd: 50, paddingStart: 60, start: 50, warningType: 'HEIGHT' }); expect(result).toMatchSnapshot(); }); it('should handle edge case where start is exactly at maxDimension', () => { const result = getAdjustedAxis({ length: 100, maxDimension: 1000, paddingEnd: 10, paddingStart: 0, start: 1000, warningType: 'WIDTH' }); expect(result).toMatchSnapshot(); }); it('should handle edge case where start is 0', () => { const result = getAdjustedAxis({ length: 100, maxDimension: 1000, paddingEnd: 10, paddingStart: 0, start: 0, warningType: 'WIDTH' }); expect(result).toMatchSnapshot(); }); it('should handle large padding values', () => { const result = getAdjustedAxis({ length: 50, maxDimension: 100, paddingEnd: 100, paddingStart: 100, start: 50, warningType: 'WIDTH' }); expect(result).toMatchSnapshot(); }); it('should handle zero length', () => { const result = getAdjustedAxis({ length: 0, maxDimension: 1000, paddingEnd: 10, paddingStart: 5, start: 50, warningType: 'WIDTH' }); expect(result).toMatchSnapshot(); }); it('should handle negative start position', () => { const result = getAdjustedAxis({ length: 100, maxDimension: 1000, paddingEnd: 10, paddingStart: 0, start: -10, warningType: 'WIDTH' }); expect(result).toMatchSnapshot(); }); }); describe('handleIOSBezelCorners', () => { let logWarnSpy; let getIosBezelImageNamesMock; let readFileSyncMock; let getBase64ScreenshotSizeMock; let mockImage; beforeEach(async () => { logWarnSpy = vi.spyOn(log, 'warn').mockImplementation(() => { }); const utilsModule = vi.mocked(await import('../helpers/utils.js')); getIosBezelImageNamesMock = vi.spyOn(utilsModule, 'getIosBezelImageNames'); getBase64ScreenshotSizeMock = vi.spyOn(utilsModule, 'getBase64ScreenshotSize'); const fsModule = vi.mocked(await import('node:fs')); readFileSyncMock = vi.spyOn(fsModule, 'readFileSync'); mockImage = { composite: vi.fn().mockReturnThis(), getBase64: vi.fn().mockResolvedValue('data:image/png;base64,mockImageData'), opacity: vi.fn().mockReturnThis(), rotate: vi.fn().mockReturnThis(), }; }); afterEach(() => { vi.clearAllMocks(); logWarnSpy.mockRestore(); }); it('should do nothing when addIOSBezelCorners is false', async () => { await handleIOSBezelCorners({ addIOSBezelCorners: false, image: mockImage, deviceName: 'iPhone 14 Pro', devicePixelRatio: 3, height: 844, isLandscape: false, width: 390, }); expect(getIosBezelImageNamesMock).not.toHaveBeenCalled(); expect(readFileSyncMock).not.toHaveBeenCalled(); expect(logWarnSpy).not.toHaveBeenCalled(); }); it('should handle supported iPhone device', async () => { getIosBezelImageNamesMock.mockReturnValue({ topImageName: 'iphone14pro-top', bottomImageName: 'iphone14pro-bottom' }); readFileSyncMock.mockReturnValue('mockImageData'); getBase64ScreenshotSizeMock.mockReturnValue({ width: 100, height: 50 }); await handleIOSBezelCorners({ addIOSBezelCorners: true, image: mockImage, deviceName: 'iPhone 14 Pro', devicePixelRatio: 3, height: 844, isLandscape: false, width: 390, }); expect(getIosBezelImageNamesMock.mock.calls).toMatchSnapshot(); expect(readFileSyncMock).toHaveBeenCalledTimes(2); expect(mockImage.composite).toHaveBeenCalledTimes(2); expect(logWarnSpy).not.toHaveBeenCalled(); }); it('should handle supported iPhone device in landscape mode', async () => { getIosBezelImageNamesMock.mockReturnValue({ topImageName: 'iphone14pro-top', bottomImageName: 'iphone14pro-bottom' }); readFileSyncMock.mockReturnValue('mockImageData'); getBase64ScreenshotSizeMock.mockReturnValue({ width: 100, height: 50 }); await handleIOSBezelCorners({ addIOSBezelCorners: true, image: mockImage, deviceName: 'iPhone 14 Pro', devicePixelRatio: 3, height: 390, isLandscape: true, width: 844, }); expect(getIosBezelImageNamesMock.mock.calls).toMatchSnapshot(); expect(readFileSyncMock).toHaveBeenCalledTimes(2); expect(mockImage.composite).toHaveBeenCalledTimes(2); expect(logWarnSpy).not.toHaveBeenCalled(); }); it('should handle supported iPad device with sufficient dimensions', async () => { getIosBezelImageNamesMock.mockReturnValue({ topImageName: 'ipadair-top', bottomImageName: 'ipadair-bottom' }); readFileSyncMock.mockReturnValue('mockImageData'); getBase64ScreenshotSizeMock.mockReturnValue({ width: 100, height: 50 }); await handleIOSBezelCorners({ addIOSBezelCorners: true, image: mockImage, deviceName: 'iPad Air', devicePixelRatio: 2, height: 2400, // 2400 / 2 = 1200 >= 1133 isLandscape: false, width: 1600, // 1600 / 2 = 800 < 1133, but height meets requirement }); expect(getIosBezelImageNamesMock.mock.calls).toMatchSnapshot(); expect(readFileSyncMock).toHaveBeenCalledTimes(2); expect(mockImage.composite).toHaveBeenCalledTimes(2); expect(logWarnSpy).not.toHaveBeenCalled(); }); it('should not handle iPad device with insufficient dimensions', async () => { await handleIOSBezelCorners({ addIOSBezelCorners: true, image: mockImage, deviceName: 'iPad Air', devicePixelRatio: 2, height: 800, // Below 1133 threshold isLandscape: false, width: 600, }); expect(getIosBezelImageNamesMock).not.toHaveBeenCalled(); expect(readFileSyncMock).not.toHaveBeenCalled(); expect(logWarnSpy.mock.calls).toMatchSnapshot(); }); it('should handle device name normalization', async () => { getIosBezelImageNamesMock.mockReturnValue({ topImageName: 'iphone14pro-top', bottomImageName: 'iphone14pro-bottom' }); readFileSyncMock.mockReturnValue('mockImageData'); getBase64ScreenshotSizeMock.mockReturnValue({ width: 100, height: 50 }); await handleIOSBezelCorners({ addIOSBezelCorners: true, image: mockImage, deviceName: 'iPhone 14 Pro Simulator (5th generation)', devicePixelRatio: 3, height: 844, isLandscape: false, width: 390, }); expect(getIosBezelImageNamesMock.mock.calls).toMatchSnapshot(); expect(readFileSyncMock).toHaveBeenCalledTimes(2); expect(mockImage.composite).toHaveBeenCalledTimes(2); expect(logWarnSpy).not.toHaveBeenCalled(); }); it('should handle unsupported device', async () => { await handleIOSBezelCorners({ addIOSBezelCorners: true, image: mockImage, deviceName: 'iPhone 6', devicePixelRatio: 2, height: 667, isLandscape: false, width: 375, }); expect(getIosBezelImageNamesMock).not.toHaveBeenCalled(); expect(readFileSyncMock).not.toHaveBeenCalled(); expect(logWarnSpy.mock.calls).toMatchSnapshot(); }); it('should handle missing bezel image names', async () => { getIosBezelImageNamesMock.mockReturnValue({ topImageName: null, bottomImageName: null }); await handleIOSBezelCorners({ addIOSBezelCorners: true, image: mockImage, deviceName: 'iPhone 14 Pro', devicePixelRatio: 3, height: 844, isLandscape: false, width: 390, }); expect(getIosBezelImageNamesMock.mock.calls).toMatchSnapshot(); expect(readFileSyncMock).not.toHaveBeenCalled(); expect(mockImage.composite).not.toHaveBeenCalled(); expect(logWarnSpy.mock.calls).toMatchSnapshot(); }); it('should handle partial bezel image names', async () => { getIosBezelImageNamesMock.mockReturnValue({ topImageName: 'iphone14pro-top', bottomImageName: null }); await handleIOSBezelCorners({ addIOSBezelCorners: true, image: mockImage, deviceName: 'iPhone 14 Pro', devicePixelRatio: 3, height: 844, isLandscape: false, width: 390, }); expect(getIosBezelImageNamesMock.mock.calls).toMatchSnapshot(); expect(readFileSyncMock).not.toHaveBeenCalled(); expect(mockImage.composite).not.toHaveBeenCalled(); expect(logWarnSpy.mock.calls).toMatchSnapshot(); }); it('should handle Android device (not iOS)', async () => { await handleIOSBezelCorners({ addIOSBezelCorners: true, image: mockImage, deviceName: 'Samsung Galaxy S21', devicePixelRatio: 3, height: 2400, isLandscape: false, width: 1080, }); expect(getIosBezelImageNamesMock).not.toHaveBeenCalled(); expect(readFileSyncMock).not.toHaveBeenCalled(); expect(logWarnSpy.mock.calls).toMatchSnapshot(); }); }); describe('cropAndConvertToDataURL', () => { let mockImage; let mockCroppedImage; const defaultCropOptions = { addIOSBezelCorners: false, base64Image: 'originalImageData', deviceName: 'iPhone 14 Pro', devicePixelRatio: 3, height: 100, isIOS: false, isLandscape: false, sourceX: 50, sourceY: 25, width: 200, }; beforeEach(async () => { mockCroppedImage = { getBase64: vi.fn().mockResolvedValue('data:image/png;base64,croppedImageData'), }; mockImage = { crop: vi.fn().mockReturnValue(mockCroppedImage), }; const jimpModule = vi.mocked(await import('jimp')); vi.spyOn(jimpModule.Jimp, 'read').mockResolvedValue(mockImage); }); afterEach(() => { vi.clearAllMocks(); }); it('should crop image and return base64 data without iOS bezel corners', async () => { const result = await cropAndConvertToDataURL(defaultCropOptions); expect(mockImage.crop.mock.calls).toMatchSnapshot(); expect(mockCroppedImage.getBase64.mock.calls).toMatchSnapshot(); expect(result).toMatchSnapshot(); }); it('should crop image and add iOS bezel corners when isIOS is true', async () => { const result = await cropAndConvertToDataURL({ ...defaultCropOptions, addIOSBezelCorners: true, isIOS: true, }); expect(mockImage.crop.mock.calls).toMatchSnapshot(); expect(mockCroppedImage.getBase64.mock.calls).toMatchSnapshot(); expect(result).toMatchSnapshot(); }); it('should handle landscape orientation with iOS bezel corners', async () => { const result = await cropAndConvertToDataURL({ ...defaultCropOptions, addIOSBezelCorners: true, isIOS: true, isLandscape: true, }); expect(mockImage.crop.mock.calls).toMatchSnapshot(); expect(mockCroppedImage.getBase64.mock.calls).toMatchSnapshot(); expect(result).toMatchSnapshot(); }); it('should handle Android device (isIOS false) without bezel corners', async () => { const result = await cropAndConvertToDataURL({ ...defaultCropOptions, addIOSBezelCorners: true, deviceName: 'Samsung Galaxy S21', isIOS: false, }); expect(mockImage.crop.mock.calls).toMatchSnapshot(); expect(mockCroppedImage.getBase64.mock.calls).toMatchSnapshot(); expect(result).toMatchSnapshot(); }); it('should handle zero dimensions', async () => { const result = await cropAndConvertToDataURL({ ...defaultCropOptions, height: 0, sourceX: 0, sourceY: 0, width: 0, }); expect(mockImage.crop.mock.calls).toMatchSnapshot(); expect(mockCroppedImage.getBase64.mock.calls).toMatchSnapshot(); expect(result).toMatchSnapshot(); }); it('should handle large crop dimensions', async () => { const result = await cropAndConvertToDataURL({ ...defaultCropOptions, height: 2000, sourceX: 1000, sourceY: 500, width: 3000, }); expect(mockImage.crop.mock.calls).toMatchSnapshot(); expect(mockCroppedImage.getBase64.mock.calls).toMatchSnapshot(); expect(result).toMatchSnapshot(); }); it('should handle different base64 input data', async () => { const result = await cropAndConvertToDataURL({ ...defaultCropOptions, base64Image: 'differentImageData123', }); expect(mockImage.crop.mock.calls).toMatchSnapshot(); expect(mockCroppedImage.getBase64.mock.calls).toMatchSnapshot(); expect(result).toMatchSnapshot(); }); it('should handle different device pixel ratios', async () => { const result = await cropAndConvertToDataURL({ ...defaultCropOptions, addIOSBezelCorners: true, devicePixelRatio: 2, isIOS: true, }); expect(mockImage.crop.mock.calls).toMatchSnapshot(); expect(mockCroppedImage.getBase64.mock.calls).toMatchSnapshot(); expect(result).toMatchSnapshot(); }); }); describe('makeCroppedBase64Image', () => { let getBase64ScreenshotSizeMock; let mockImage; let mockCroppedImage; const defaultCropOptions = { addIOSBezelCorners: false, base64Image: 'originalImageData', deviceName: 'iPhone 14 Pro', devicePixelRatio: 3, isWebDriverElementScreenshot: false, isIOS: false, isLandscape: false, rectangles: { height: 100, width: 200, x: 50, y: 25, }, resizeDimensions: { top: 0, right: 0, bottom: 0, left: 0 }, }; beforeEach(async () => { const utilsModule = vi.mocked(await import('../helpers/utils.js')); getBase64ScreenshotSizeMock = vi.spyOn(utilsModule, 'getBase64ScreenshotSize'); mockCroppedImage = { getBase64: vi.fn().mockResolvedValue('data:image/png;base64,finalCroppedImageData'), composite: vi.fn().mockReturnThis(), opacity: vi.fn().mockReturnThis(), }; mockImage = { crop: vi.fn().mockReturnValue(mockCroppedImage), composite: vi.fn().mockReturnThis(), getBase64: vi.fn().mockResolvedValue('data:image/png;base64,mockImageData'), opacity: vi.fn().mockReturnThis(), rotate: vi.fn().mockReturnThis(), }; const jimpModule = vi.mocked(await import('jimp')); vi.spyOn(jimpModule.Jimp, 'read').mockResolvedValue(mockImage); getBase64ScreenshotSizeMock.mockReturnValue({ width: 1000, height: 2000 }); }); afterEach(() => { vi.clearAllMocks(); }); it('should create cropped base64 image with default settings', async () => { const result = await makeCroppedBase64Image(defaultCropOptions); expect(getBase64ScreenshotSizeMock.mock.calls).toMatchSnapshot(); expect(mockImage.crop.mock.calls).toMatchSnapshot(); expect(mockCroppedImage.getBase64.mock.calls).toMatchSnapshot(); expect(result).toMatchSnapshot(); }); it('should handle landscape orientation with rotation', async () => { const result = await makeCroppedBase64Image({ ...defaultCropOptions, isLandscape: true, }); expect(getBase64ScreenshotSizeMock.mock.calls).toMatchSnapshot(); expect(mockImage.crop.mock.calls).toMatchSnapshot(); expect(result).toMatchSnapshot(); }); it('should handle web driver element screenshots', async () => { const result = await makeCroppedBase64Image({ ...defaultCropOptions, isWebDriverElementScreenshot: true, }); expect(getBase64ScreenshotSizeMock.mock.calls).toMatchSnapshot(); expect(mockImage.crop.mock.calls).toMatchSnapshot(); expect(result).toMatchSnapshot(); }); it('should handle iOS devices with bezel corners', async () => { const result = await makeCroppedBase64Image({ ...defaultCropOptions, addIOSBezelCorners: true, isIOS: true, }); expect(getBase64ScreenshotSizeMock.mock.calls).toMatchSnapshot(); expect(mockImage.crop.mock.calls).toMatchSnapshot(); expect(result).toMatchSnapshot(); }); it('should handle custom resize dimensions', async () => { const result = await makeCroppedBase64Image({ ...defaultCropOptions, resizeDimensions: { top: 10, right: 20, bottom: 15, left: 5 }, }); expect(getBase64ScreenshotSizeMock.mock.calls).toMatchSnapshot(); expect(mockImage.crop.mock.calls).toMatchSnapshot(); expect(result).toMatchSnapshot(); }); it('should handle different rectangle dimensions', async () => { const result = await makeCroppedBase64Image({ ...defaultCropOptions, rectangles: { height: 300, width: 400, x: 100, y: 75, }, }); expect(getBase64ScreenshotSizeMock.mock.calls).toMatchSnapshot(); expect(mockImage.crop.mock.calls).toMatchSnapshot(); expect(result).toMatchSnapshot(); }); it('should handle different screenshot sizes', async () => { getBase64ScreenshotSizeMock.mockReturnValue({ width: 800, height: 600 }); const result = await makeCroppedBase64Image(defaultCropOptions); expect(getBase64ScreenshotSizeMock.mock.calls).toMatchSnapshot(); expect(mockImage.crop.mock.calls).toMatchSnapshot(); expect(result).toMatchSnapshot(); }); it('should handle different device pixel ratios', async () => { const result = await makeCroppedBase64Image({ ...defaultCropOptions, devicePixelRatio: 2, }); expect(getBase64ScreenshotSizeMock.mock.calls).toMatchSnapshot(); expect(mockImage.crop.mock.calls).toMatchSnapshot(); expect(result).toMatchSnapshot(); }); it('should handle zero rectangle dimensions', async () => { const result = await makeCroppedBase64Image({ ...defaultCropOptions, rectangles: { height: 0, width: 0, x: 0, y: 0, }, }); expect(getBase64ScreenshotSizeMock.mock.calls).toMatchSnapshot(); expect(mockImage.crop.mock.calls).toMatchSnapshot(); expect(result).toMatchSnapshot(); }); it('should handle edge case with padding that exceeds image bounds', async () => { const result = await makeCroppedBase64Image({ ...defaultCropOptions, rectangles: { height: 100, width: 200, x: 950, // Very close to image width (1000) y: 1900, // Very close to image height (2000) }, resizeDimensions: { top: 50, right: 100, bottom: 50, left: 50 }, }); expect(getBase64ScreenshotSizeMock.mock.calls).toMatchSnapshot(); expect(mockImage.crop.mock.calls).toMatchSnapshot(); expect(result).toMatchSnapshot(); }); }); describe('makeFullPageBase64Image', () => { let getBase64ScreenshotSizeMock; let mockCanvas; let mockImage; const defaultScreenshotsData = { fullPageHeight: 2000, fullPageWidth: 1000, data: [ { canvasWidth: 1000, canvasYPosition: 0, imageHeight: 800, imageWidth: 1000, imageXPosition: 0, imageYPosition: 0, screenshot: 'screenshot1-data' }, { canvasWidth: 1000, canvasYPosition: 800, imageHeight: 800, imageWidth: 1000, imageXPosition: 0, imageYPosition: 0, screenshot: 'screenshot2-data' }, { canvasWidth: 1000, canvasYPosition: 1600, imageHeight: 400, imageWidth: 1000, imageXPosition: 0, imageYPosition: 0, screenshot: 'screenshot3-data' } ] }; const defaultOptions = { devicePixelRatio: 2, isLandscape: false }; beforeEach(async () => { const utilsModule = vi.mocked(await import('../helpers/utils.js')); getBase64ScreenshotSizeMock = vi.spyOn(utilsModule, 'getBase64ScreenshotSize'); mockCanvas = { composite: vi.fn().mockReturnThis(), getBase64: vi.fn().mockResolvedValue('data:image/png;base64,fullPageImageData'), }; mockImage = { bitmap: { width: 1000, height: 800, }, crop: vi.fn().mockReturnThis(), composite: vi.fn().mockReturnThis(), getBase64: vi.fn().mockResolvedValue('data:image/png;base64,mockImageData'), opacity: vi.fn().mockReturnThis(), rotate: vi.fn().mockReturnThis(), }; const jimpModule = vi.mocked(await import('jimp')); vi.spyOn(jimpModule.Jimp, 'read').mockResolvedValue(mockImage); vi.mocked(jimpModule.Jimp).mockImplementation((options) => { if (options && (options.width || options.height)) { return mockCanvas; } return mockImage; }); getBase64ScreenshotSizeMock.mockReturnValue({ width: 1000, height: 800 }); }); afterEach(() => { vi.clearAllMocks(); }); it('should create full page base64 image with multiple screenshots', async () => { const result = await makeFullPageBase64Image(defaultScreenshotsData, defaultOptions); expect(getBase64ScreenshotSizeMock.mock.calls).toMatchSnapshot(); expect(mockImage.crop.mock.calls).toMatchSnapshot(); expect(mockCanvas.composite.mock.calls).toMatchSnapshot(); expect(mockCanvas.getBase64.mock.calls).toMatchSnapshot(); expect(result).toMatchSnapshot(); }); it('should handle landscape mode with rotation', async () => { getBase64ScreenshotSizeMock.mockReturnValue({ width: 800, height: 1000 }); const result = await makeFullPageBase64Image(defaultScreenshotsData, { ...defaultOptions, isLandscape: true }); expect(getBase64ScreenshotSizeMock.mock.calls).toMatchSnapshot(); expect(mockImage.crop.mock.calls).toMatchSnapshot(); expect(mockCanvas.composite.mock.calls).toMatchSnapshot(); expect(result).toMatchSnapshot(); }); it('should handle single screenshot', async () => { const singleScreenshotData = { fullPageHeight: 800, fullPageWidth: 1000, data: [ { canvasWidth: 1000, canvasYPosition: 0, imageHeight: 800, imageWidth: 1000, imageXPosition: 0, imageYPosition: 0, screenshot: 'single-screenshot-data' } ] }; const result = await makeFullPageBase64Image(singleScreenshotData, defaultOptions); expect(getBase64ScreenshotSizeMock.mock.calls).toMatchSnapshot(); expect(mockImage.crop.mock.calls).toMatchSnapshot(); expect(mockCanvas.composite.mock.calls).toMatchSnapshot(); expect(result).toMatchSnapshot(); }); it('should handle different device pixel ratios', async () => { const result = await makeFullPageBase64Image(defaultScreenshotsData, { ...defaultOptions, devicePixelRatio: 3 }); expect(getBase64ScreenshotSizeMock.mock.calls).toMatchSnapshot(); expect(mockImage.crop.mock.calls).toMatchSnapshot(); expect(mockCanvas.composite.mock.calls).toMatchSnapshot(); expect(result).toMatchSnapshot(); }); it('should handle screenshots with different dimensions', async () => { const mixedScreenshotsData = { fullPageHeight: 1500, fullPageWidth: 1200, data: [ { canvasWidth: 1200, canvasYPosition: 0, imageHeight: 600, imageWidth: 1200, imageXPosition: 0, imageYPosition: 0, screenshot: 'wide-screenshot-data' }, { canvasWidth: 1200, canvasYPosition: 600, imageHeight: 900, imageWidth: 1200, imageXPosition: 0, imageYPosition: 0, screenshot: 'tall-screenshot-data' } ] }; getBase64ScreenshotSizeMock .mockReturnValueOnce({ width: 1200, height: 600 }) .mockReturnValueOnce({ width: 1200, height: 900 }); const result = await makeFullPageBase64Image(mixedScreenshotsData, defaultOptions); expect(getBase64ScreenshotSizeMock.mock.calls).toMatchSnapshot(); expect(mockImage.crop.mock.calls).toMatchSnapshot(); expect(mockCanvas.composite.mock.calls).toMatchSnapshot(); expect(result).toMatchSnapshot(); }); it('should handle screenshots with cropping positions', async () => { const croppedScreenshotsData = { fullPageHeight: 1000, fullPageWidth: 1000, data: [ { canvasWidth: 1000, canvasYPosition: 0, imageHeight: 500, imageWidth: 500, imageXPosition: 100, imageYPosition: 50, screenshot: 'cropped-screenshot-data' } ] }; const result = await makeFullPageBase64Image(croppedScreenshotsData, defaultOptions); expect(getBase64ScreenshotSizeMock.mock.calls).toMatchSnapshot(); expect(mockImage.crop.mock.calls).toMatchSnapshot(); expect(mockCanvas.composite.mock.calls).toMatchSnapshot(); expect(result).toMatchSnapshot(); }); it('should handle landscape mode without rotation when width >= height', async () => { getBase64ScreenshotSizeMock.mockReturnValue({ width: 1000, height: 800 }); const result = await makeFullPageBase64Image(defaultScreenshotsData, { ...defaultOptions, isLandscape: true }); expect(getBase64ScreenshotSizeMock.mock.calls).toMatchSnapshot(); expect(mockImage.crop.mock.calls).toMatchSnapshot(); expect(mockCanvas.composite.mock.calls).toMatchSnapshot(); expect(result).toMatchSnapshot(); }); it('should handle empty screenshots array', async () => { const emptyScreenshotsData = { fullPageHeight: 0, fullPageWidth: 1000, data: [] }; const result = await makeFullPageBase64Image(emptyScreenshotsData, defaultOptions); expect(getBase64ScreenshotSizeMock.mock.calls).toMatchSnapshot(); expect(mockImage.crop.mock.calls).toMatchSnapshot(); expect(mockCanvas.composite.mock.calls).toMatchSnapshot(); expect(mockCanvas.getBase64.mock.calls).toMatchSnapshot(); expect(result).toMatchSnapshot(); }); it('should handle large canvas dimensions', async () => { const largeScreenshotsData = { fullPageHeight: 5000, fullPageWidth: 3000, data: [ { canvasWidth: 3000, canvasYPosition: 0, imageHeight: 2000, imageWidth: 3000, imageXPosition: 0, imageYPosition: 0, screenshot: 'large-screenshot-data' } ] }; getBase64ScreenshotSizeMock.mockReturnValue({ width: 3000, height: 2000 }); const result = await makeFullPageBase64Image(largeScreenshotsData, defaultOptions); expect(getBase64ScreenshotSizeMock.mock.calls).toMatchSnapshot(); expect(mockImage.crop.mock.calls).toMatchSnapshot(); expect(mockCanvas.composite.mock.calls).toMatchSnapshot(); expect(result).toMatchSnapshot(); }); it('should handle different screenshot data for each iteration', async () => { const result = await makeFullPageBase64Image(defaultScreenshotsData, defaultOptions); expect(getBase64ScreenshotSizeMock.mock.calls).toMatchSnapshot(); expect(result).toMatchSnapshot(); }); it('should handle canvas Y positions correctly', async () => { const result = await makeFullPageBase64Image(defaultScreenshotsData, defaultOptions); expect(mockCanvas.composite.mock.calls).toMatchSnapshot(); expect(result).toMatchSnapshot(); }); }); describe('takeResizedBase64Screenshot', () => { let mockBrowserInstance; let mockElement; let mockElementRegion; let takeBase64ScreenshotMock; let calculateDprDataMock; let isWdioElementMock; const defaultOptions = { browserInstance: {}, element: {}, devicePixelRatio: 2, isIOS: false, resizeDimensions: { top: 0, right: 0, bottom: 0, left: 0 } }; beforeEach(async () => { mockElementRegion = { height: 100, width: 200, x: 50, y: 25 }; mockElement = { elementId: 'test-element-id',