UNPKG

@wdio/visual-service

Version:

Image comparison / visual regression testing for WebdriverIO

261 lines (260 loc) 12.1 kB
import { getMobileScreenSize, getMobileViewPortPosition, IOS_OFFSETS, NOT_KNOWN } from '@wdio/image-comparison-core'; /** * Get the folders data * * If folder options are passed in use those values * Otherwise, use the values set during instantiation */ export function getFolders(methodOptions, folders, currentTestPath) { return { actualFolder: methodOptions.actualFolder ?? folders.actualFolder, baselineFolder: methodOptions.baselineFolder ?? currentTestPath, diffFolder: methodOptions.diffFolder ?? folders.diffFolder, }; } /** * Get the size of a base64 screenshot in pixels without the device pixel ratio */ export function getBase64ScreenshotSize(screenshot, devicePixelRation = 1) { return { height: Buffer.from(screenshot, 'base64').readUInt32BE(20) / devicePixelRation, width: Buffer.from(screenshot, 'base64').readUInt32BE(16) / devicePixelRation, }; } /** * Get the device pixel ratio */ export function getDevicePixelRatio(screenshot, deviceScreenSize) { const screenshotSize = getBase64ScreenshotSize(screenshot); const devicePixelRatio = Math.round(screenshotSize.width / deviceScreenSize.width) === Math.round(screenshotSize.height / deviceScreenSize.height) ? Math.round(screenshotSize.width / deviceScreenSize.width) : Math.round(screenshotSize.height / deviceScreenSize.width); return Math.round(devicePixelRatio); } /** * Get the mobile instance data */ async function getMobileInstanceData({ browserInstance, initialDeviceRectangles, isNativeContext, nativeWebScreenshot, }) { const { isAndroid, isIOS, isMobile } = browserInstance; let devicePixelRatio = 1; let deviceRectangles = initialDeviceRectangles; if (isMobile) { const currentDriverCapabilities = browserInstance.capabilities; const { height: screenHeight, width: screenWidth } = await getMobileScreenSize({ browserInstance, isIOS, isNativeContext, }); // Update the width for the device rectangles for bottomBar, screenSize, statusBar, statusBarAndAddressBar deviceRectangles.screenSize.height = screenHeight; deviceRectangles.screenSize.width = screenWidth; deviceRectangles.bottomBar.width = screenWidth; deviceRectangles.statusBarAndAddressBar.width = screenWidth; deviceRectangles.statusBar.width = screenWidth; deviceRectangles = await getMobileViewPortPosition({ browserInstance, initialDeviceRectangles, isAndroid, isIOS, isNativeContext, nativeWebScreenshot, screenHeight, screenWidth, }); // @TODO: 20250317: When we have all things tested with the above, we can simplify the below part to only use the iOS part // @TODO: This is al based on PORTRAIT mode if (isAndroid && currentDriverCapabilities) { // We use a few `@ts-ignore` here because `pixelRatio` and `statBarHeight` // are returned by the driver, and not recognized by the types because they are not requested // @ts-ignore if (currentDriverCapabilities?.pixelRatio !== undefined) { // @ts-ignore devicePixelRatio = currentDriverCapabilities?.pixelRatio; } // @ts-ignore if (currentDriverCapabilities?.statBarHeight !== undefined) { // @ts-ignore deviceRectangles.statusBar.height = currentDriverCapabilities?.statBarHeight; deviceRectangles.statusBar.width = deviceRectangles.screenSize.width; } } else { // This is to already determine the device pixel ratio if it's not set in the capabilities const base64Image = await browserInstance.takeScreenshot(); devicePixelRatio = getDevicePixelRatio(base64Image, deviceRectangles.screenSize); const isIphone = deviceRectangles.screenSize.width < 1024 && deviceRectangles.screenSize.height < 1024; const deviceType = isIphone ? 'IPHONE' : 'IPAD'; const defaultPortraitHeight = isIphone ? 667 : 1024; const portraitHeight = deviceRectangles.screenSize.width > deviceRectangles.screenSize.height ? deviceRectangles.screenSize.width : deviceRectangles.screenSize.height; const offsetPortraitHeight = Object.keys(IOS_OFFSETS[deviceType]).indexOf(portraitHeight.toString()) > -1 ? portraitHeight : defaultPortraitHeight; const currentOffsets = IOS_OFFSETS[deviceType][offsetPortraitHeight][screenWidth > screenHeight ? 'LANDSCAPE' : 'PORTRAIT']; // NOTE: The values for iOS are based on CSS pixels, so we need to multiply them with the devicePixelRatio, // This will NOT be done here but in a central place deviceRectangles.statusBar = { x: 0, y: 0, width: deviceRectangles.screenSize.width, height: currentOffsets.STATUS_BAR, }; deviceRectangles.homeBar = currentOffsets.HOME_BAR; } } return { devicePixelRatio, deviceRectangles, }; } /** * Get the LambdaTest options, these can be case insensitive */ export function getLtOptions(capabilities) { const key = Object.keys(capabilities).find((k) => k.toLowerCase() === 'lt:options'); return key ? capabilities[key] : undefined; } /** * Get the device name */ function getDeviceName(browserInstance) { const { capabilities: { // We use a few `@ts-ignore` here because this is returned by the driver // and not recognized by the types because they are not requested // @ts-ignore deviceName: returnedDeviceName = NOT_KNOWN, }, requestedCapabilities } = browserInstance; let deviceName = NOT_KNOWN; // First check if it's a BrowserStack session, they don't: // - return the "requested" deviceName in the session capabilities // - don't use the `appium:deviceName` capability const isBrowserStack = 'bstack:options' in requestedCapabilities; const bsOptions = requestedCapabilities['bstack:options']; const capName = 'deviceName'; if (isBrowserStack && bsOptions && capName in bsOptions) { deviceName = bsOptions[capName]; } // Same for LabdaTest const isLambdaTest = 'lt:options' in requestedCapabilities; const ltOptions = getLtOptions(requestedCapabilities); if (isLambdaTest && ltOptions && capName in ltOptions) { deviceName = ltOptions[capName]; } const { 'appium:deviceName': requestedDeviceName } = requestedCapabilities; return (deviceName !== NOT_KNOWN ? deviceName : requestedDeviceName || returnedDeviceName || NOT_KNOWN).toLowerCase(); } /** * Get the instance data */ export async function getInstanceData({ browserInstance, initialDeviceRectangles, isNativeContext }) { const { capabilities: currentCapabilities, requestedCapabilities } = browserInstance; const { browserName: rawBrowserName = NOT_KNOWN, browserVersion: rawBrowserVersion = NOT_KNOWN, platformName: rawPlatformName = NOT_KNOWN, } = currentCapabilities; // Generic data const browserName = rawBrowserName === '' ? NOT_KNOWN : rawBrowserName.toLowerCase(); const browserVersion = rawBrowserVersion === '' ? NOT_KNOWN : rawBrowserVersion.toLowerCase(); // For #967: When a screenshot of an emulated device is taken, but the browser was initially // started as a "desktop" session, so not with emulated caps, we need to store the initial // devicePixelRatio when we take a screenshot and enableLegacyScreenshotMethod is enabled let devicePixelRatio = !browserInstance.isMobile ? (await browserInstance.execute('return window.devicePixelRatio')) : 1; const platformName = rawPlatformName === '' ? NOT_KNOWN : rawPlatformName.toLowerCase(); const logName = 'wdio-ics:options' in requestedCapabilities ? requestedCapabilities['wdio-ics:options']?.logName ?? '' : ''; const name = 'wdio-ics:options' in requestedCapabilities ? requestedCapabilities['wdio-ics:options']?.name ?? '' : ''; // Mobile data const { isAndroid, isIOS, isMobile } = browserInstance; const { // We use a few `@ts-ignore` here because this is returned by the driver // and not recognized by the types because they are not requested // @ts-ignore app: rawApp = NOT_KNOWN, // @ts-ignore platformVersion: rawPlatformVersion = NOT_KNOWN, } = currentCapabilities; const appName = rawApp !== NOT_KNOWN ? rawApp.replace(/\\/g, '/').split('/').pop().replace(/[^a-zA-Z0-9.]/g, '_') : NOT_KNOWN; const deviceName = getDeviceName(browserInstance); const ltOptions = getLtOptions(requestedCapabilities); // @TODO: Figure this one out in the future when we know more about the Appium capabilities from LT // 20241216: LT doesn't have the option to take a ChromeDriver screenshot, so if it's Android it's always native const nativeWebScreenshot = isAndroid && ltOptions || !!(requestedCapabilities['appium:nativeWebScreenshot']); const platformVersion = (rawPlatformVersion === undefined || rawPlatformVersion === '') ? NOT_KNOWN : rawPlatformVersion.toLowerCase(); const { devicePixelRatio: mobileDevicePixelRatio, deviceRectangles, } = await getMobileInstanceData({ browserInstance, initialDeviceRectangles, isNativeContext, nativeWebScreenshot }); devicePixelRatio = isMobile ? mobileDevicePixelRatio : devicePixelRatio; return { appName, browserName, browserVersion, deviceName, devicePixelRatio, deviceRectangles, initialDevicePixelRatio: devicePixelRatio, isAndroid, isIOS, isMobile, logName, name, nativeWebScreenshot, platformName, platformVersion, }; } /** * Traverse up the scope chain until browser element was reached */ export function getBrowserObject(elem) { const elemObject = elem; return elemObject.parent ? getBrowserObject(elemObject.parent) : elem; } /** * Get the native context for the current browser */ const appiumKeys = ['app', 'bundleId', 'appPackage', 'appActivity', 'appWaitActivity', 'appWaitPackage']; export function getNativeContext({ capabilities, isMobile }) { if (!capabilities || typeof capabilities !== 'object' || !isMobile) { return false; } const isAppiumAppCapPresent = (capabilities) => { return appiumKeys.some((key) => (capabilities[key] !== undefined || capabilities[`appium:${key}`] !== undefined || capabilities['appium:options']?.[key] !== undefined || capabilities['lt:options']?.[key] !== undefined)); }; const isBrowserNameFalse = !!capabilities?.browserName === false; const isAutoWebviewFalse = !( // @ts-expect-error capabilities?.autoWebview === true || capabilities['appium:autoWebview'] === true || capabilities['appium:options']?.autoWebview === true || capabilities['lt:options']?.autoWebview === true); return isBrowserNameFalse && isAppiumAppCapPresent(capabilities) && isAutoWebviewFalse; } /** * Make sure we have all the data for the test context */ export function enrichTestContext({ commandName, currentTestContext: { framework, parent, title, }, instanceData: { appName, browserName, browserVersion, deviceName, isAndroid, isIOS, isMobile, platformName, platformVersion, }, tag, }) { return { commandName, instanceData: { app: appName, browser: { name: browserName, version: browserVersion, }, deviceName, isMobile, isAndroid, isIOS, platform: { name: platformName, version: platformVersion, }, }, framework, parent, tag, title, }; }