UNPKG

@wdio/visual-service

Version:

Image comparison / visual regression testing for WebdriverIO

275 lines (274 loc) 11.9 kB
import { IOS_OFFSETS } from 'webdriver-image-comparison'; import { NOT_KNOWN } from 'webdriver-image-comparison/dist/helpers/constants.js'; 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 screenshot in pixels without the device pixel ratio */ export function getScreenshotSize(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 = getScreenshotSize(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({ currentBrowser, isAndroid, isMobile }) { const deviceScreenSize = { height: 0, width: 0, }; const devicePlatformRect = { statusBar: { height: 0, x: 0, width: 0, y: 0 }, homeBar: { height: 0, x: 0, width: 0, y: 0 }, }; let devicePixelRatio = 1; if (isMobile) { const currentDriverCapabilities = currentBrowser.capabilities; const { height, width } = await currentBrowser.getWindowSize(); deviceScreenSize.height = height; deviceScreenSize.width = width; // @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 devicePlatformRect.statusBar.height = currentDriverCapabilities?.statBarHeight; devicePlatformRect.statusBar.width = width; } } else { // This is to already determine the device pixel ratio if it's not set in the capabilities const base64Image = await currentBrowser.takeScreenshot(); devicePixelRatio = getDevicePixelRatio(base64Image, deviceScreenSize); const isIphone = width < 1024 && height < 1024; const deviceType = isIphone ? 'IPHONE' : 'IPAD'; const defaultPortraitHeight = isIphone ? 667 : 1024; const portraitHeight = width > height ? width : height; const offsetPortraitHeight = Object.keys(IOS_OFFSETS[deviceType]).indexOf(portraitHeight.toString()) > -1 ? portraitHeight : defaultPortraitHeight; const currentOffsets = IOS_OFFSETS[deviceType][offsetPortraitHeight].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 devicePlatformRect.statusBar = { y: 0, x: 0, width, height: currentOffsets.STATUS_BAR, }; devicePlatformRect.homeBar = currentOffsets.HOME_BAR; } } return { devicePixelRatio, devicePlatformRect, deviceScreenSize, }; } /** * 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(currentBrowser) { 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 } = currentBrowser; 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(currentBrowser) { const NOT_KNOWN = 'not-known'; const { capabilities: currentCapabilities, requestedCapabilities } = currentBrowser; 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(); let 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 } = currentBrowser; 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(currentBrowser); 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, devicePlatformRect, deviceScreenSize, } = await getMobileInstanceData({ currentBrowser, isAndroid, isMobile }); devicePixelRatio = isMobile ? mobileDevicePixelRatio : devicePixelRatio; return { appName, browserName, browserVersion, deviceName, devicePixelRatio, devicePlatformRect, deviceScreenSize, 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; } /** * We can't say it's native context if the autoWebview is provided and set to true, for all other cases we can say it's native */ export function determineNativeContext(driver) { // First check if it's multi remote if (driver.isMultiremote) { return Object.keys(driver).reduce((acc, instanceName) => { const instance = driver[instanceName]; if (instance.sessionId) { acc[instance.sessionId] = determineNativeContext(instance); } return acc; }, {}); } // If not check if it's a mobile if (driver.isMobile) { const isAppiumAppCapPresent = (capabilities) => { const appiumKeys = [ 'appium:app', 'appium:bundleId', 'appium:appPackage', 'appium:appActivity', 'appium:appWaitActivity', 'appium:appWaitPackage', 'appium:autoWebview', ]; const optionsKeys = appiumKeys.map(key => key.replace('appium:', '')); const isInRoot = appiumKeys.some(key => capabilities[key] !== undefined); // @ts-expect-error const isInAppiumOptions = capabilities['appium:options'] && // @ts-expect-error optionsKeys.some(key => capabilities['appium:options']?.[key] !== undefined); // @ts-expect-error const isInLtOptions = capabilities['lt:options'] && // @ts-expect-error optionsKeys.some(key => capabilities['lt:options']?.[key] !== undefined); return !!(isInRoot || isInAppiumOptions || isInLtOptions); }; const capabilities = driver.requestedCapabilities; const isBrowserNameFalse = !!capabilities.browserName === false; const isAutoWebviewFalse = !(capabilities['appium:autoWebview'] === true || capabilities['appium:options']?.autoWebview === true || capabilities['lt:options']?.autoWebview === true); return isBrowserNameFalse && isAppiumAppCapPresent(capabilities) && isAutoWebviewFalse; } // If not, it's webcontext return false; } /** * Get the native context for the current browser */ export function getNativeContext(browser, currentBrowser, nativeContext) { if (browser.isMultiremote) { return nativeContext[currentBrowser.sessionId]; } else if (typeof nativeContext === 'boolean') { return nativeContext; } return false; } /** * 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, }; }