@wdio/image-comparison-core
Version:
Image comparison core module for @wdio/visual-service - WebdriverIO visual testing framework
673 lines (672 loc) • 38.4 kB
JavaScript
import { afterEach, beforeEach, describe, it, expect, vi } from 'vitest';
import { join } from 'node:path';
import logger from '@wdio/logger';
import { getDesktopFullPageScreenshotsData, getAndroidChromeDriverFullPageScreenshotsData, logHiddenRemovedError, takeBase64BiDiScreenshot, takeWebElementScreenshot, getMobileFullPageNativeWebScreenshotsData } from './screenshots.js';
import { MEDIUM_IMAGE_STRING, SMALL_IMAGE_STRING } from '../mocks/image.js';
import { DEVICE_RECTANGLES } from '../helpers/constants.js';
import * as rectanglesModule from './rectangles.js';
import * as utilsModule from '../helpers/utils.js';
const log = logger('test');
vi.mock('@wdio/logger', () => import(join(process.cwd(), '__mocks__', '@wdio/logger')));
vi.mock('./rectangles.js', () => ({
determineElementRectangles: vi.fn()
}));
vi.mock('../helpers/utils.js', async () => {
const actual = await vi.importActual('../helpers/utils.js');
return {
...actual,
getBase64ScreenshotSize: vi.fn(),
waitFor: vi.fn(),
calculateDprData: vi.fn()
};
});
vi.mock('../clientSideScripts/scrollToPosition.js', () => ({
default: vi.fn()
}));
vi.mock('../clientSideScripts/getDocumentScrollHeight.js', () => ({
default: vi.fn()
}));
vi.mock('../clientSideScripts/hideRemoveElements.js', () => ({
default: vi.fn()
}));
describe('screenshots', () => {
const createMockBrowserInstance = ({ takeScreenshot = SMALL_IMAGE_STRING, takeElementScreenshot = SMALL_IMAGE_STRING } = {}) => {
return {
takeScreenshot: vi.fn().mockResolvedValue(takeScreenshot),
takeElementScreenshot: vi.fn().mockResolvedValue(takeElementScreenshot),
getWindowHandle: vi.fn().mockResolvedValue('window-handle-123'),
browsingContextCaptureScreenshot: vi.fn().mockResolvedValue({ data: takeScreenshot }),
execute: vi.fn().mockResolvedValue(1000)
};
};
const createMockElement = () => {
return {
elementId: 'element-123'
};
};
let logWarnSpy;
describe('getMobileFullPageNativeWebScreenshotsData', () => {
const createMobileOptions = (overrides = {}) => ({
addressBarShadowPadding: 10,
devicePixelRatio: 2,
deviceRectangles: {
viewport: { x: 0, y: 100, width: 750, height: 1334 },
bottomBar: { x: 0, y: 1434, width: 750, height: 100 },
homeBar: { x: 0, y: 1534, width: 750, height: 34 },
leftSidePadding: { x: 0, y: 0, width: 0, height: 0 },
rightSidePadding: { x: 0, y: 0, width: 0, height: 0 },
statusBarAndAddressBar: { x: 0, y: 0, width: 750, height: 100 },
statusBar: { x: 0, y: 0, width: 750, height: 50 },
screenSize: { width: 750, height: 1668 }
},
fullPageScrollTimeout: 1000,
hideAfterFirstScroll: [],
isAndroid: false,
isIOS: true,
isLandscape: false,
innerHeight: 667,
toolBarShadowPadding: 5,
screenWidth: 375,
...overrides
});
beforeEach(() => {
logWarnSpy = vi.spyOn(log, 'warn');
vi.mocked(utilsModule.waitFor).mockResolvedValue(undefined);
vi.mocked(utilsModule.calculateDprData).mockImplementation((data) => data);
vi.mocked(utilsModule.getBase64ScreenshotSize).mockImplementation((_screenshot, dpr = 1) => {
const baseWidth = 750;
const baseHeight = 1334;
return {
width: Math.round(baseWidth / dpr),
height: Math.round(baseHeight / dpr)
};
});
});
afterEach(() => {
vi.clearAllMocks();
logWarnSpy.mockRestore();
});
it('should take single screenshot when content fits in viewport (iOS)', async () => {
const mockBrowserInstance = createMockBrowserInstance({ takeScreenshot: SMALL_IMAGE_STRING });
mockBrowserInstance.execute = vi.fn()
.mockResolvedValueOnce(undefined) // scrollToPosition
.mockResolvedValueOnce(undefined) // hideScrollBars
.mockResolvedValueOnce(652) // getDocumentScrollHeight (effective viewport height)
.mockResolvedValueOnce({ scrollTop: 0 }) // actualScrollInfo
.mockResolvedValueOnce(undefined); // hideScrollBars
const options = createMobileOptions(); // iOS device by default
const result = await getMobileFullPageNativeWebScreenshotsData(mockBrowserInstance, options);
expect(result).toMatchSnapshot();
expect(mockBrowserInstance.takeScreenshot).toHaveBeenCalledTimes(1);
expect(result.data).toHaveLength(1);
});
it('should take multiple screenshots when content exceeds viewport (Android)', async () => {
const mockBrowserInstance = createMockBrowserInstance({ takeScreenshot: SMALL_IMAGE_STRING });
mockBrowserInstance.execute = vi.fn()
.mockResolvedValueOnce(undefined) // scrollToPosition 0 (i=0)
.mockResolvedValueOnce(undefined) // hideScrollBars true
.mockResolvedValueOnce(1304) // getDocumentScrollHeight (2x effectiveViewportHeight)
.mockResolvedValueOnce({ scrollTop: 0 }) // actualScrollInfo
.mockResolvedValueOnce(undefined) // hideScrollBars false
.mockResolvedValueOnce(undefined) // scrollToPosition 652 (i=1)
.mockResolvedValueOnce(undefined) // hideScrollBars true
.mockResolvedValueOnce(1304) // getDocumentScrollHeight
.mockResolvedValueOnce({ scrollTop: 652 }) // actualScrollInfo
.mockResolvedValueOnce(undefined); // hideScrollBars false
const options = createMobileOptions({ isAndroid: true });
const result = await getMobileFullPageNativeWebScreenshotsData(mockBrowserInstance, options);
expect(result).toMatchSnapshot();
expect(mockBrowserInstance.takeScreenshot).toHaveBeenCalledTimes(2);
expect(result.data).toHaveLength(2);
});
it('should handle landscape mode with rotation detection', async () => {
const mockBrowserInstance = createMockBrowserInstance({ takeScreenshot: SMALL_IMAGE_STRING });
mockBrowserInstance.execute = vi.fn()
.mockResolvedValueOnce(undefined) // scrollToPosition
.mockResolvedValueOnce(undefined) // hideScrollBars
.mockResolvedValueOnce(652) // getDocumentScrollHeight
.mockResolvedValueOnce({ scrollTop: 0 }) // actualScrollInfo
.mockResolvedValueOnce(undefined); // hideScrollBars
const options = createMobileOptions({
isLandscape: true,
deviceRectangles: {
viewport: { x: 0, y: 100, width: 1334, height: 750 },
bottomBar: { x: 0, y: 850, width: 1334, height: 100 },
homeBar: { x: 0, y: 950, width: 1334, height: 34 },
leftSidePadding: { x: 0, y: 0, width: 0, height: 0 },
rightSidePadding: { x: 0, y: 0, width: 0, height: 0 },
statusBarAndAddressBar: { x: 0, y: 0, width: 1334, height: 100 },
statusBar: { x: 0, y: 0, width: 1334, height: 50 },
screenSize: { width: 1334, height: 984 }
}
});
const result = await getMobileFullPageNativeWebScreenshotsData(mockBrowserInstance, options);
expect(result).toMatchSnapshot();
expect(result.data).toHaveLength(1);
});
it('should hide elements after first scroll when hideAfterFirstScroll is provided', async () => {
const mockBrowserInstance = createMockBrowserInstance({ takeScreenshot: SMALL_IMAGE_STRING });
mockBrowserInstance.execute = vi.fn()
.mockResolvedValueOnce(undefined) // scrollToPosition 0 (i=0)
.mockResolvedValueOnce(undefined) // hideScrollBars true
.mockResolvedValueOnce(2638) // getDocumentScrollHeight (2x effectiveViewportHeight to trigger scroll)
.mockResolvedValueOnce({ scrollTop: 0 }) // actualScrollInfo
.mockResolvedValueOnce(undefined) // hideScrollBars false
.mockResolvedValueOnce(undefined) // scrollToPosition 1319 (i=1)
.mockResolvedValueOnce(undefined) // hideScrollBars true
.mockResolvedValueOnce(undefined) // hideRemoveElements (i=1, hide elements)
.mockResolvedValueOnce(2638) // getDocumentScrollHeight
.mockResolvedValueOnce({ scrollTop: 1319 }) // actualScrollInfo
.mockResolvedValueOnce(undefined) // hideScrollBars false
.mockResolvedValueOnce(undefined); // hideRemoveElements (restore at end)
const mockElements = [{ tagName: 'div' }];
const options = createMobileOptions({ hideAfterFirstScroll: [mockElements] });
const result = await getMobileFullPageNativeWebScreenshotsData(mockBrowserInstance, options);
expect(result).toMatchSnapshot();
const executeCalls = vi.mocked(mockBrowserInstance.execute).mock.calls;
const hideElementsCalls = executeCalls.filter(call => call.length === 3 &&
typeof call[1] === 'object' &&
call[1] &&
typeof call[1] === 'object' &&
'hide' in call[1] &&
Array.isArray(call[1].hide) &&
'remove' in call[1] &&
Array.isArray(call[1].remove));
expect(hideElementsCalls).toHaveLength(2);
expect(hideElementsCalls[0][2]).toBe(true);
expect(hideElementsCalls[1][2]).toBe(false);
});
it('should handle error when hiding elements fails', async () => {
const mockBrowserInstance = createMockBrowserInstance({ takeScreenshot: SMALL_IMAGE_STRING });
const executeError = new Error('Element not found');
mockBrowserInstance.execute = vi.fn()
.mockResolvedValueOnce(undefined) // scrollToPosition 0 (i=0)
.mockResolvedValueOnce(undefined) // hideScrollBars true
.mockResolvedValueOnce(2638) // getDocumentScrollHeight (2x effectiveViewportHeight)
.mockResolvedValueOnce({ scrollTop: 0 }) // actualScrollInfo
.mockResolvedValueOnce(undefined) // hideScrollBars false
.mockResolvedValueOnce(undefined) // scrollToPosition 1319 (i=1)
.mockResolvedValueOnce(undefined) // hideScrollBars true
.mockRejectedValueOnce(executeError) // hideRemoveElements fails
.mockResolvedValueOnce(2638) // getDocumentScrollHeight
.mockResolvedValueOnce({ scrollTop: 1319 }) // actualScrollInfo
.mockResolvedValueOnce(undefined) // hideScrollBars false
.mockRejectedValueOnce(executeError); // hideRemoveElements restore fails
const mockElements = [{ tagName: 'div' }];
const options = createMobileOptions({ hideAfterFirstScroll: [mockElements] });
const result = await getMobileFullPageNativeWebScreenshotsData(mockBrowserInstance, options);
expect(result).toBeDefined();
expect(result.data).toHaveLength(2);
expect(logWarnSpy).toHaveBeenCalledTimes(2);
});
it('should throw error when negative scrollY is detected', async () => {
const mockBrowserInstance = createMockBrowserInstance({ takeScreenshot: SMALL_IMAGE_STRING });
const options = createMobileOptions({
deviceRectangles: {
viewport: { x: 0, y: 100, width: 750, height: 50 },
bottomBar: { x: 0, y: 200, width: 750, height: 0 },
homeBar: { x: 0, y: 300, width: 750, height: 100 },
leftSidePadding: { x: 0, y: 0, width: 0, height: 0 },
rightSidePadding: { x: 0, y: 0, width: 0, height: 0 },
statusBarAndAddressBar: { x: 0, y: 0, width: 750, height: 100 },
statusBar: { x: 0, y: 0, width: 750, height: 50 },
screenSize: { width: 750, height: 500 }
},
addressBarShadowPadding: 20,
toolBarShadowPadding: 20,
isAndroid: false
});
mockBrowserInstance.execute = vi.fn()
.mockResolvedValueOnce(undefined) // scrollToPosition 0 (i=0, scrollY=0)
.mockResolvedValueOnce(undefined) // hideScrollBars true
.mockResolvedValueOnce(1000) // getDocumentScrollHeight
.mockResolvedValueOnce(undefined) // hideScrollBars false
// When trying to do i=1, scrollY would be negative, so it should error before the next execute calls
.mockResolvedValueOnce(0); // pageYOffset for error logging
await expect(getMobileFullPageNativeWebScreenshotsData(mockBrowserInstance, options))
.rejects.toThrow(/Negative scroll position detected/);
});
it('should throw error when scroll height cannot be determined', async () => {
const mockBrowserInstance = createMockBrowserInstance({ takeScreenshot: SMALL_IMAGE_STRING });
mockBrowserInstance.execute = vi.fn()
.mockResolvedValueOnce(undefined) // scrollToPosition
.mockResolvedValueOnce(undefined) // hideScrollBars
.mockResolvedValueOnce(undefined) // getDocumentScrollHeight returns undefined
.mockResolvedValueOnce(undefined); // hideScrollBars
const options = createMobileOptions();
await expect(getMobileFullPageNativeWebScreenshotsData(mockBrowserInstance, options))
.rejects.toThrow('Couldn\'t determine scroll height or screenshot size');
});
});
describe('getAndroidChromeDriverFullPageScreenshotsData', () => {
const createBaseOptions = (overrides = {}) => ({
devicePixelRatio: 1,
fullPageScrollTimeout: 1000,
innerHeight: 768,
hideAfterFirstScroll: [],
...overrides
});
beforeEach(() => {
logWarnSpy = vi.spyOn(log, 'warn');
vi.mocked(utilsModule.waitFor).mockResolvedValue(undefined);
vi.mocked(utilsModule.calculateDprData).mockImplementation((data) => data);
});
afterEach(() => {
vi.clearAllMocks();
logWarnSpy.mockRestore();
});
it('should take single screenshot when content fits in viewport', async () => {
const mockBrowserInstance = createMockBrowserInstance({ takeScreenshot: SMALL_IMAGE_STRING });
vi.mocked(utilsModule.getBase64ScreenshotSize).mockReturnValue({
width: 1366,
height: 768
});
mockBrowserInstance.execute = vi.fn()
.mockResolvedValueOnce(undefined) // scrollToPosition
.mockResolvedValueOnce(undefined) // hideScrollBars
.mockResolvedValueOnce(768) // getDocumentScrollHeight
.mockResolvedValueOnce(undefined); // hideScrollBars
const options = createBaseOptions();
const result = await getAndroidChromeDriverFullPageScreenshotsData(mockBrowserInstance, options);
expect(result).toMatchSnapshot();
expect(mockBrowserInstance.takeScreenshot).toHaveBeenCalledTimes(1);
expect(result.data).toHaveLength(1);
});
it('should take multiple screenshots when content exceeds viewport', async () => {
const mockBrowserInstance = createMockBrowserInstance({ takeScreenshot: SMALL_IMAGE_STRING });
vi.mocked(utilsModule.getBase64ScreenshotSize).mockReturnValue({
width: 1366,
height: 768
});
mockBrowserInstance.execute = vi.fn()
.mockResolvedValueOnce(undefined) // scrollToPosition 0
.mockResolvedValueOnce(undefined) // hideScrollBars
.mockResolvedValueOnce(1536) // getDocumentScrollHeight (2x viewport)
.mockResolvedValueOnce(undefined) // hideScrollBars
.mockResolvedValueOnce(undefined) // scrollToPosition 768
.mockResolvedValueOnce(undefined) // hideScrollBars
.mockResolvedValueOnce(1536) // getDocumentScrollHeight
.mockResolvedValueOnce(undefined); // hideScrollBars
const options = createBaseOptions();
const result = await getAndroidChromeDriverFullPageScreenshotsData(mockBrowserInstance, options);
expect(result).toMatchSnapshot();
expect(mockBrowserInstance.takeScreenshot).toHaveBeenCalledTimes(2);
expect(result.data).toHaveLength(2);
});
it('should hide elements after first scroll when hideAfterFirstScroll is provided', async () => {
const mockBrowserInstance = createMockBrowserInstance({ takeScreenshot: SMALL_IMAGE_STRING });
vi.mocked(utilsModule.getBase64ScreenshotSize).mockReturnValue({
width: 1366,
height: 768
});
mockBrowserInstance.execute = vi.fn()
.mockResolvedValueOnce(undefined) // scrollToPosition 0
.mockResolvedValueOnce(undefined) // hideScrollBars
.mockResolvedValueOnce(1536) // getDocumentScrollHeight
.mockResolvedValueOnce(undefined) // hideScrollBars
.mockResolvedValueOnce(undefined) // scrollToPosition 768
.mockResolvedValueOnce(undefined) // hideScrollBars
.mockResolvedValueOnce(undefined) // hideRemoveElements
.mockResolvedValueOnce(1536) // getDocumentScrollHeight
.mockResolvedValueOnce(undefined) // hideScrollBars
.mockResolvedValueOnce(undefined); // hideRemoveElements (restore)
const mockElements = [{ tagName: 'div' }];
const options = createBaseOptions({ hideAfterFirstScroll: [mockElements] });
const result = await getAndroidChromeDriverFullPageScreenshotsData(mockBrowserInstance, options);
expect(result).toMatchSnapshot();
expect(mockBrowserInstance.execute).toHaveBeenCalledWith(expect.any(Function), { hide: [mockElements], remove: [] }, true);
expect(mockBrowserInstance.execute).toHaveBeenCalledWith(expect.any(Function), { hide: [mockElements], remove: [] }, false);
});
it('should handle error when hiding elements fails', async () => {
const mockBrowserInstance = createMockBrowserInstance({ takeScreenshot: SMALL_IMAGE_STRING });
vi.mocked(utilsModule.getBase64ScreenshotSize).mockReturnValue({
width: 1366,
height: 768
});
const executeError = new Error('Element not found');
mockBrowserInstance.execute = vi.fn()
.mockResolvedValueOnce(undefined) // scrollToPosition 0
.mockResolvedValueOnce(undefined) // hideScrollBars
.mockResolvedValueOnce(1536) // getDocumentScrollHeight
.mockResolvedValueOnce(undefined) // hideScrollBars
.mockResolvedValueOnce(undefined) // scrollToPosition 768
.mockResolvedValueOnce(undefined) // hideScrollBars
.mockRejectedValueOnce(executeError) // hideRemoveElements fails
.mockResolvedValueOnce(1536) // getDocumentScrollHeight
.mockResolvedValueOnce(undefined) // hideScrollBars
.mockRejectedValueOnce(executeError); // hideRemoveElements restore fails
const mockElements = [{ tagName: 'div' }];
const options = createBaseOptions({ hideAfterFirstScroll: [mockElements] });
const result = await getAndroidChromeDriverFullPageScreenshotsData(mockBrowserInstance, options);
expect(result).toBeDefined();
expect(result.data).toHaveLength(2);
expect(logWarnSpy).toHaveBeenCalledTimes(2);
});
it('should throw error when scroll height cannot be determined', async () => {
const mockBrowserInstance = createMockBrowserInstance({ takeScreenshot: SMALL_IMAGE_STRING });
vi.mocked(utilsModule.getBase64ScreenshotSize).mockReturnValue({
width: 1366,
height: 768
});
mockBrowserInstance.execute = vi.fn()
.mockResolvedValueOnce(undefined) // scrollToPosition
.mockResolvedValueOnce(undefined) // hideScrollBars
.mockResolvedValueOnce(undefined) // getDocumentScrollHeight returns undefined
.mockResolvedValueOnce(undefined); // hideScrollBars
const options = createBaseOptions();
await expect(getAndroidChromeDriverFullPageScreenshotsData(mockBrowserInstance, options))
.rejects.toThrow('Couldn\'t determine scroll height or screenshot size');
});
});
describe('getDesktopFullPageScreenshotsData', () => {
const createBaseOptions = (overrides = {}) => ({
devicePixelRatio: 1,
fullPageScrollTimeout: 1000,
innerHeight: 768,
hideAfterFirstScroll: [],
...overrides
});
beforeEach(() => {
logWarnSpy = vi.spyOn(log, 'warn');
vi.mocked(utilsModule.waitFor).mockResolvedValue(undefined);
vi.mocked(utilsModule.calculateDprData).mockImplementation((data) => data);
});
afterEach(() => {
vi.clearAllMocks();
logWarnSpy.mockRestore();
});
it('should take single screenshot when content fits in viewport', async () => {
const mockBrowserInstance = createMockBrowserInstance({ takeScreenshot: SMALL_IMAGE_STRING });
vi.mocked(utilsModule.getBase64ScreenshotSize).mockReturnValue({
width: 1366,
height: 768
});
mockBrowserInstance.execute = vi.fn()
.mockResolvedValueOnce(undefined) // scrollToPosition
.mockResolvedValueOnce(768); // getDocumentScrollHeight
const options = createBaseOptions();
const result = await getDesktopFullPageScreenshotsData(mockBrowserInstance, options);
expect(result).toMatchSnapshot();
expect(mockBrowserInstance.takeScreenshot).toHaveBeenCalledTimes(1);
expect(mockBrowserInstance.execute).toHaveBeenCalledTimes(2); // scroll + getScrollHeight
expect(result.data).toHaveLength(1);
});
it('should take multiple screenshots when content exceeds viewport', async () => {
const mockBrowserInstance = createMockBrowserInstance({ takeScreenshot: SMALL_IMAGE_STRING });
vi.mocked(utilsModule.getBase64ScreenshotSize).mockReturnValue({
width: 1366,
height: 768
});
mockBrowserInstance.execute = vi.fn()
.mockResolvedValueOnce(undefined) // scrollToPosition 0
.mockResolvedValueOnce(1536) // getDocumentScrollHeight (2x viewport)
.mockResolvedValueOnce(undefined) // scrollToPosition 768
.mockResolvedValueOnce(1536); // getDocumentScrollHeight
const options = createBaseOptions();
const result = await getDesktopFullPageScreenshotsData(mockBrowserInstance, options);
expect(result).toMatchSnapshot();
expect(mockBrowserInstance.takeScreenshot).toHaveBeenCalledTimes(2);
expect(result.data).toHaveLength(2);
});
it('should handle screenshot size adjustment when different from inner height', async () => {
const mockBrowserInstance = createMockBrowserInstance({ takeScreenshot: SMALL_IMAGE_STRING });
vi.mocked(utilsModule.getBase64ScreenshotSize).mockReturnValue({
width: 1366,
height: 768.4
});
mockBrowserInstance.execute = vi.fn()
.mockResolvedValueOnce(undefined) // scrollToPosition
.mockResolvedValueOnce(768); // getDocumentScrollHeight
const options = createBaseOptions({ innerHeight: 768 });
const result = await getDesktopFullPageScreenshotsData(mockBrowserInstance, options);
expect(result).toMatchSnapshot();
expect(result.data).toHaveLength(1);
});
it('should hide elements after first scroll when hideAfterFirstScroll is provided', async () => {
const mockBrowserInstance = createMockBrowserInstance({ takeScreenshot: SMALL_IMAGE_STRING });
vi.mocked(utilsModule.getBase64ScreenshotSize).mockReturnValue({
width: 1366,
height: 768
});
mockBrowserInstance.execute = vi.fn()
.mockResolvedValueOnce(undefined) // scrollToPosition 0
.mockResolvedValueOnce(1536) // getDocumentScrollHeight
.mockResolvedValueOnce(undefined) // hideRemoveElements
.mockResolvedValueOnce(undefined) // scrollToPosition 768
.mockResolvedValueOnce(1536) // getDocumentScrollHeight
.mockResolvedValueOnce(undefined); // hideRemoveElements (restore)
const mockElements = [{ tagName: 'div' }];
const options = createBaseOptions({ hideAfterFirstScroll: [mockElements] });
const result = await getDesktopFullPageScreenshotsData(mockBrowserInstance, options);
expect(result).toMatchSnapshot();
expect(mockBrowserInstance.execute).toHaveBeenCalledWith(expect.any(Function), { hide: [mockElements], remove: [] }, true);
expect(mockBrowserInstance.execute).toHaveBeenCalledWith(expect.any(Function), { hide: [mockElements], remove: [] }, false);
});
it('should handle error when hiding elements fails', async () => {
const mockBrowserInstance = createMockBrowserInstance({ takeScreenshot: SMALL_IMAGE_STRING });
vi.mocked(utilsModule.getBase64ScreenshotSize).mockReturnValue({
width: 1366,
height: 768
});
const executeError = new Error('Element not found');
mockBrowserInstance.execute = vi.fn()
.mockResolvedValueOnce(undefined) // scrollToPosition 0 (i=0)
.mockResolvedValueOnce(1536) // getDocumentScrollHeight (i=0) - triggers another iteration
.mockResolvedValueOnce(undefined) // scrollToPosition 768 (i=1)
.mockRejectedValueOnce(executeError) // hideRemoveElements fails (i=1)
.mockResolvedValueOnce(1536) // getDocumentScrollHeight (i=1)
.mockRejectedValueOnce(executeError); // hideRemoveElements restore fails
const mockElements = [{ tagName: 'div' }];
const options = createBaseOptions({ hideAfterFirstScroll: [mockElements] });
const result = await getDesktopFullPageScreenshotsData(mockBrowserInstance, options);
expect(result).toBeDefined();
expect(result.data).toHaveLength(2);
expect(logWarnSpy).toHaveBeenCalledTimes(2);
});
it('should throw error when scroll height cannot be determined', async () => {
const mockBrowserInstance = createMockBrowserInstance({ takeScreenshot: SMALL_IMAGE_STRING });
vi.mocked(utilsModule.getBase64ScreenshotSize).mockReturnValue({
width: 1366,
height: 768
});
mockBrowserInstance.execute = vi.fn()
.mockResolvedValueOnce(undefined) // scrollToPosition
.mockResolvedValueOnce(undefined); // getDocumentScrollHeight returns undefined
const options = createBaseOptions();
await expect(getDesktopFullPageScreenshotsData(mockBrowserInstance, options))
.rejects.toThrow('Couldn\'t determine scroll height or screenshot size');
});
});
describe('takeBase64BiDiScreenshot', () => {
it('should take a BiDi screenshot with no arguments (uses defaults)', async () => {
const mockBrowserInstance = createMockBrowserInstance();
const result = await takeBase64BiDiScreenshot({ browserInstance: mockBrowserInstance });
expect(result).toBe(SMALL_IMAGE_STRING);
});
it('should take a BiDi screenshot with default viewport origin', async () => {
const mockBrowserInstance = createMockBrowserInstance();
const result = await takeBase64BiDiScreenshot({ browserInstance: mockBrowserInstance });
expect(result).toBe(SMALL_IMAGE_STRING);
});
it('should take a BiDi screenshot with document origin', async () => {
const mockBrowserInstance = createMockBrowserInstance();
const result = await takeBase64BiDiScreenshot({
browserInstance: mockBrowserInstance,
origin: 'document'
});
expect(result).toBe(SMALL_IMAGE_STRING);
});
it('should take a BiDi screenshot with clip rectangle', async () => {
const mockBrowserInstance = createMockBrowserInstance();
const clipRectangle = {
x: 10,
y: 20,
width: 300,
height: 400,
};
const result = await takeBase64BiDiScreenshot({
browserInstance: mockBrowserInstance,
clip: clipRectangle
});
expect(result).toBe(SMALL_IMAGE_STRING);
});
});
describe('logHiddenRemovedError', () => {
beforeEach(() => {
logWarnSpy = vi.spyOn(log, 'warn');
});
afterEach(() => {
vi.clearAllMocks();
logWarnSpy.mockRestore();
});
it('should log a warning when the elements are not found', () => {
logHiddenRemovedError(new Error('Element not found'));
expect(logWarnSpy.mock.calls).toMatchSnapshot();
});
});
describe('takeWebElementScreenshot', () => {
const createBaseTakeWebElementScreenshotOptions = (overrides = {}) => ({
addressBarShadowPadding: 10,
browserInstance: createMockBrowserInstance(),
devicePixelRatio: 1,
deviceRectangles: DEVICE_RECTANGLES,
element: Promise.resolve(createMockElement()),
fallback: false,
initialDevicePixelRatio: 1,
isEmulated: false,
innerHeight: 768,
isAndroid: false,
isAndroidChromeDriverScreenshot: false,
isAndroidNativeWebScreenshot: false,
isIOS: false,
isLandscape: false,
toolBarShadowPadding: 5,
...overrides
});
beforeEach(() => {
logWarnSpy = vi.spyOn(log, 'warn');
});
afterEach(() => {
vi.clearAllMocks();
logWarnSpy.mockRestore();
});
describe('normal mode (fallback = false)', () => {
it('should successfully take element screenshot using webdriver element screenshot', async () => {
const mockBrowserInstance = createMockBrowserInstance({ takeScreenshot: MEDIUM_IMAGE_STRING, takeElementScreenshot: SMALL_IMAGE_STRING });
const mockElement = createMockElement();
vi.mocked(utilsModule.getBase64ScreenshotSize).mockReturnValue({
width: 300,
height: 200
});
const options = createBaseTakeWebElementScreenshotOptions({
browserInstance: mockBrowserInstance,
element: Promise.resolve(mockElement)
});
const result = await takeWebElementScreenshot(options);
expect(result).toMatchSnapshot();
expect(mockBrowserInstance.takeElementScreenshot).toHaveBeenCalled();
expect(mockBrowserInstance.takeScreenshot).not.toHaveBeenCalled();
});
it('should throw error when element has zero width', async () => {
const mockBrowserInstance = createMockBrowserInstance({ takeElementScreenshot: SMALL_IMAGE_STRING });
const mockElement = createMockElement();
vi.mocked(utilsModule.getBase64ScreenshotSize).mockReturnValue({
width: 0,
height: 200
});
const options = createBaseTakeWebElementScreenshotOptions({
browserInstance: mockBrowserInstance,
element: Promise.resolve(mockElement)
});
vi.mocked(rectanglesModule.determineElementRectangles).mockResolvedValue({
x: 10,
y: 20,
width: 300,
height: 200
});
const result = await takeWebElementScreenshot(options);
expect(logWarnSpy.mock.calls).toMatchSnapshot();
expect(result.isWebDriverElementScreenshot).toBe(false);
expect(mockBrowserInstance.takeElementScreenshot).toHaveBeenCalled();
expect(mockBrowserInstance.takeScreenshot).toHaveBeenCalled();
});
it('should throw error when element has zero height', async () => {
const mockBrowserInstance = createMockBrowserInstance({ takeElementScreenshot: SMALL_IMAGE_STRING });
const mockElement = createMockElement();
vi.mocked(utilsModule.getBase64ScreenshotSize).mockReturnValue({
width: 300,
height: 0
});
const options = createBaseTakeWebElementScreenshotOptions({
browserInstance: mockBrowserInstance,
element: Promise.resolve(mockElement)
});
vi.mocked(rectanglesModule.determineElementRectangles).mockResolvedValue({
x: 10,
y: 20,
width: 300,
height: 200
});
const result = await takeWebElementScreenshot(options);
expect(logWarnSpy.mock.calls).toMatchSnapshot();
expect(result.isWebDriverElementScreenshot).toBe(false);
expect(mockBrowserInstance.takeElementScreenshot).toHaveBeenCalled();
expect(mockBrowserInstance.takeScreenshot).toHaveBeenCalled();
});
it('should fallback when takeElementScreenshot throws an error', async () => {
const mockBrowserInstance = createMockBrowserInstance({ takeScreenshot: MEDIUM_IMAGE_STRING });
const mockElement = createMockElement();
mockBrowserInstance.takeElementScreenshot = vi.fn().mockRejectedValue(new Error('Element screenshot failed'));
vi.mocked(rectanglesModule.determineElementRectangles).mockResolvedValue({
x: 10,
y: 20,
width: 300,
height: 200
});
const options = createBaseTakeWebElementScreenshotOptions({
browserInstance: mockBrowserInstance,
element: Promise.resolve(mockElement)
});
const result = await takeWebElementScreenshot(options);
expect(logWarnSpy.mock.calls).toMatchSnapshot();
expect(result).toMatchSnapshot();
expect(mockBrowserInstance.takeElementScreenshot).toHaveBeenCalledWith('element-123');
expect(mockBrowserInstance.takeScreenshot).toHaveBeenCalled();
expect(result.isWebDriverElementScreenshot).toBe(false);
expect(rectanglesModule.determineElementRectangles).toHaveBeenCalled();
});
});
describe('fallback mode (fallback = true)', () => {
it('should take full screenshot and determine element rectangles', async () => {
const mockBrowserInstance = createMockBrowserInstance({ takeScreenshot: MEDIUM_IMAGE_STRING });
const mockElement = createMockElement();
const mockRectangles = { x: 50, y: 100, width: 250, height: 150 };
vi.mocked(rectanglesModule.determineElementRectangles).mockResolvedValue(mockRectangles);
const options = createBaseTakeWebElementScreenshotOptions({
browserInstance: mockBrowserInstance,
element: Promise.resolve(mockElement),
fallback: true
});
const result = await takeWebElementScreenshot(options);
expect(result).toMatchSnapshot();
expect(mockBrowserInstance.takeScreenshot).toHaveBeenCalled();
expect(mockBrowserInstance.takeElementScreenshot).not.toHaveBeenCalled();
expect(rectanglesModule.determineElementRectangles).toHaveBeenCalledWith({
browserInstance: mockBrowserInstance,
base64Image: MEDIUM_IMAGE_STRING,
element: Promise.resolve(mockElement),
options: {
devicePixelRatio: 1,
deviceRectangles: DEVICE_RECTANGLES,
initialDevicePixelRatio: 1,
innerHeight: 768,
isEmulated: false,
isAndroidNativeWebScreenshot: false,
isAndroid: false,
isIOS: false
}
});
});
});
});
});