diff --git a/packages/ocr-service/tests/utils/__snapshots__/ocrGetData.test.ts.snap b/packages/ocr-service/tests/utils/__snapshots__/ocrGetData.test.ts.snap index 7bb7b2b84..56fdfa8a1 100644 --- a/packages/ocr-service/tests/utils/__snapshots__/ocrGetData.test.ts.snap +++ b/packages/ocr-service/tests/utils/__snapshots__/ocrGetData.test.ts.snap @@ -37,7 +37,7 @@ exports[`getData > processes OCR data correctly using a haystack 1`] = ` "Using system installed version of Tesseract", ], [ - "It took '0.000s' to process the image.", + "It took 'X.XXXs' to process the image.", ], [ "The following text was found through OCR: diff --git a/packages/ocr-service/tests/utils/ocrGetData.test.ts b/packages/ocr-service/tests/utils/ocrGetData.test.ts index e732d57c3..54c51919c 100644 --- a/packages/ocr-service/tests/utils/ocrGetData.test.ts +++ b/packages/ocr-service/tests/utils/ocrGetData.test.ts @@ -117,6 +117,13 @@ describe('getData', () => { vi.mocked(getBase64ScreenshotSize).mockReturnValue({ height: 1200, width: 1200 }) const result = await getData(browser, options) + const sanitizedCalls = logInfoMock.mock.calls.map((args) => + args.map((msg) => + typeof msg === 'string' + ? msg.replace(/'[\d.]+s'/g, "'X.XXXs'") + : msg + ) + ) expect(browser.getWindowSize).toHaveBeenCalled() expect(browser.takeScreenshot).toHaveBeenCalled() @@ -128,7 +135,7 @@ describe('getData', () => { language: 'ENG' }) expect(adjustElementBbox).toHaveBeenCalledTimes(4) - expect(logInfoMock.mock.calls).toMatchSnapshot() + expect(sanitizedCalls).toMatchSnapshot() expect(result).toMatchSnapshot() }) diff --git a/packages/visual-service/src/contextManager.ts b/packages/visual-service/src/contextManager.ts new file mode 100644 index 000000000..fde871b1c --- /dev/null +++ b/packages/visual-service/src/contextManager.ts @@ -0,0 +1,82 @@ +import logger from '@wdio/logger' +import type { DeviceRectangles } from 'webdriver-image-comparison' +import { DEVICE_RECTANGLES } from 'webdriver-image-comparison' +import { getNativeContext } from './utils.js' + +const log = logger('@wdio/visual-service:ContextManager') + +export class ContextManager { + #browser: WebdriverIO.Browser + #needsUpdate = false + #currentContext?: string + #isNativeContext: boolean + private cachedViewport: DeviceRectangles = DEVICE_RECTANGLES + + constructor(browser: WebdriverIO.Browser) { + this.#browser = browser + const capabilities = this.#browser.requestedCapabilities + this.#isNativeContext = getNativeContext({ capabilities, isMobile: this.#browser.isMobile }) + this.#browser.on('result', this.#onCommandResult.bind(this)) + } + + getViewportContext(): DeviceRectangles { + return this.cachedViewport + } + + setViewPortContext(viewport: DeviceRectangles): void { + this.cachedViewport = viewport + this.#needsUpdate = false + } + + get browser(): WebdriverIO.Browser { + return this.#browser + } + + get needsUpdate(): boolean { + return this.#needsUpdate + } + + markForUpdate(): void { + this.#needsUpdate = true + } + + setCurrentContext(context: string) { + this.#currentContext = context + if (this.#browser.isMobile) { + this.#isNativeContext = context ? context === 'NATIVE_APP' : this.#isNativeContext + } + } + + async getCurrentContext () { + return this.#currentContext + } + + get isNativeContext() { + return this.#isNativeContext + } + + async #onCommandResult(event: { command: string, body: unknown, result: unknown }) { + const commands = ['switchAppiumContext', 'switchContext', 'setOrientation'] + if (commands.includes(event.command)) { + const { body: { name, orientation }, result } = event as { body: { name?: string; orientation?: string }, result: unknown } + // Check if result exists and is not an error object + // @ts-expect-error + const isSuccess = result && !result.error && typeof result === 'object' + if (isSuccess) { + if (event.command === 'setOrientation') { + log.info(`Device rotation to "${orientation}" detected, context will need recalculation.`) + this.markForUpdate() + } else { + if (name && name !== 'NATIVE_APP') { + log.info(`Context changed to "${name}", context will need recalculation.`) + this.markForUpdate() + } + // Set the context to the current context + this.setCurrentContext(name as string) + } + } else { + log.warn('Orientation set failed: \n', result, '\nWe could not recalibrate the context') + } + } + } +} diff --git a/packages/visual-service/src/service.ts b/packages/visual-service/src/service.ts index bcbd18f52..7471f0f67 100644 --- a/packages/visual-service/src/service.ts +++ b/packages/visual-service/src/service.ts @@ -15,9 +15,10 @@ import { FOLDERS, DEFAULT_TEST_CONTEXT, } from 'webdriver-image-comparison' -import type { TestContext } from 'webdriver-image-comparison' +import type { InstanceData, TestContext } from 'webdriver-image-comparison' import { SevereServiceError } from 'webdriverio' -import { determineNativeContext, enrichTestContext, getFolders, getInstanceData, getNativeContext } from './utils.js' +import { enrichTestContext, getFolders, getInstanceData, getNativeContext } from './utils.js' +import { wrapWithContext } from './wrapWithContext.js' import { toMatchScreenSnapshot, toMatchFullPageSnapshot, @@ -26,8 +27,9 @@ import { } from './matcher.js' import { waitForStorybookComponentToBeLoaded } from './storybook/utils.js' import type { WaitForStorybookComponentToBeLoaded } from './storybook/Types.js' -import type { MultiremoteCommandResult, NativeContextType, VisualServiceOptions } from './types.js' +import type { VisualServiceOptions } from './types.js' import { PAGE_OPTIONS_MAP } from './constants.js' +import { ContextManager } from './contextManager.js' const log = logger('@wdio/visual-service') const elementCommands = { saveElement, checkElement } @@ -47,12 +49,12 @@ export default class WdioImageComparisonService extends BaseClass { #currentFilePath?: string #testContext: TestContext #browser?: WebdriverIO.Browser | WebdriverIO.MultiRemoteBrowser - private _isNativeContext: NativeContextType | undefined + private _contextManager?: ContextManager + private _contextManagers?: Map = new Map() constructor(options: VisualServiceOptions, _: WebdriverIO.Capabilities, config: WebdriverIO.Config) { super(options) this.#config = config - this._isNativeContext = undefined this.#testContext = DEFAULT_TEST_CONTEXT } @@ -69,7 +71,6 @@ export default class WdioImageComparisonService extends BaseClass { browser: WebdriverIO.Browser | WebdriverIO.MultiRemoteBrowser ) { this.#browser = browser - this._isNativeContext = determineNativeContext(this.#browser) if (!this.#browser.isMultiremote) { log.info('Adding commands to global browser') @@ -78,10 +79,6 @@ export default class WdioImageComparisonService extends BaseClass { await this.#extendMultiremoteBrowser(capabilities as Capabilities.RequestedMultiremoteCapabilities) } - if (browser.isMultiremote) { - this.#setupMultiremoteContextListener() - } - /** * add custom matcher for visual comparison when expect has been added. * this is not the case in standalone mode @@ -109,14 +106,6 @@ export default class WdioImageComparisonService extends BaseClass { this.#testContext = this.#getTestContext(world) } - afterCommand(commandName: string, _args: string[], result: number | string, error: any) { - // This is for the cases where in the E2E tests we switch to a WEBVIEW or back to NATIVE_APP context - if (commandName === 'getContext' && error === undefined && typeof result === 'string') { - // Multiremote logic is handled in the `before` method during an event listener - this._isNativeContext = this.#browser?.isMultiremote ? this._isNativeContext : result.includes('NATIVE') - } - } - #getBaselineFolder() { const isDefaultBaselineFolder = normalize(FOLDERS.DEFAULT.BASE) === this.folders.baselineFolder const baselineFolder =(isDefaultBaselineFolder && this.#currentFilePath ? this.#currentFilePath : this.folders.baselineFolder) as string @@ -146,7 +135,12 @@ export default class WdioImageComparisonService extends BaseClass { */ for (const browserName of browserNames) { log.info(`Adding commands to Multi Browser: ${browserName}`) + const browserInstance = browser.getInstance(browserName) + const contextManager = new ContextManager(browserInstance) + + this._contextManagers?.set(browserName, contextManager) + await this.#addCommandsToBrowser(browserInstance) } @@ -172,123 +166,166 @@ export default class WdioImageComparisonService extends BaseClass { * Add commands to the "normal" browser object */ async #addCommandsToBrowser(currentBrowser: WebdriverIO.Browser) { - const isNativeContext = getNativeContext( - this.#browser as WebdriverIO.Browser | WebdriverIO.MultiRemoteBrowser, - currentBrowser, - this._isNativeContext as NativeContextType - ) + this._contextManager = new ContextManager(currentBrowser); + (currentBrowser as any).visualService = this const instanceData = await getInstanceData({ currentBrowser, - initialDeviceRectangles: this.deviceRectangles, - isNativeContext, + initialDeviceRectangles: this._contextManager.getViewportContext(), + isNativeContext: this._contextManager.isNativeContext, }) + // Update the context manager with the current viewport + this._contextManager.setViewPortContext(instanceData.deviceRectangles) + for (const [commandName, command] of Object.entries(elementCommands)) { - this.#addElementCommand(currentBrowser, commandName, command, instanceData, isNativeContext) + this.#addElementCommand(currentBrowser, commandName, command, instanceData) } for (const [commandName, command] of Object.entries(pageCommands)) { - this.#addPageCommand(currentBrowser, commandName, command, instanceData, isNativeContext) + this.#addPageCommand(currentBrowser, commandName, command, instanceData) } } /** * Add new element commands to the browser object */ - #addElementCommand(browser: WebdriverIO.Browser, commandName: string, command: any, instanceData: any, isNativeContext: boolean) { + #addElementCommand( + browser: WebdriverIO.Browser, + commandName: string, + command: any, + initialInstanceData: InstanceData, + ) { log.info(`Adding element command "${commandName}" to browser object`) + const elementOptionsKey = commandName === 'saveElement' ? 'saveElementOptions' : 'checkElementOptions' const self = this + browser.addCommand( commandName, function ( - this: typeof browser, - element, - tag, - elementOptions = {}, + this: WebdriverIO.Browser, + element: WebdriverIO.Element, + tag: string, + elementOptions = {} ) { - const elementOptionsKey = commandName === 'saveElement' ? 'saveElementOptions' : 'checkElementOptions' + const wrapped = wrapWithContext({ + browser, + command, + contextManager: self.contextManager, + getArgs: () => { + const updatedInstanceData = { + ...initialInstanceData, + deviceRectangles: self.contextManager.getViewportContext(), + } + const isCurrentContextNative = self.contextManager.isNativeContext - return command( - { - methods: { - executor: ( - fn: string | ((...args: InnerArguments) => ReturnValue), - ...args: InnerArguments): Promise => { - return this.execute.bind(browser)(fn, ...args) as Promise + return [{ + methods: { + executor: ( + fn: string | ((...args: InnerArguments) => ReturnValue), + ...args: InnerArguments + ): Promise => { + return this.execute(fn, ...args) as Promise + }, + getElementRect: this.getElementRect.bind(this), + screenShot: this.takeScreenshot.bind(this), + takeElementScreenshot: this.takeElementScreenshot.bind(this), }, - getElementRect: this.getElementRect.bind(browser), - screenShot: this.takeScreenshot.bind(browser), - takeElementScreenshot: this.takeElementScreenshot.bind(browser), - }, - instanceData, - folders: getFolders(elementOptions, self.folders, self.#getBaselineFolder()), - element, - tag, - [elementOptionsKey]: { - wic: self.defaultOptions, - method: elementOptions, - }, - isNativeContext, - testContext: enrichTestContext({ - commandName, - currentTestContext: self.#testContext, - instanceData, + instanceData: updatedInstanceData, + folders: getFolders(elementOptions, self.folders, self.#getBaselineFolder()), + element, tag, - }) + [elementOptionsKey]: { + wic: self.defaultOptions, + method: elementOptions, + }, + isNativeContext: isCurrentContextNative, + testContext: enrichTestContext({ + commandName, + currentTestContext: self.#testContext, + instanceData: updatedInstanceData, + tag, + }), + }] } - ) - }) + }) + + return wrapped.call(this) + } + ) } /** * Add new page commands to the browser object */ - #addPageCommand(browser: WebdriverIO.Browser, commandName: string, command: any, instanceData: any, isNativeContext: boolean) { + #addPageCommand( + browser: WebdriverIO.Browser, + commandName: string, + command: any, + initialInstanceData: InstanceData, + ) { log.info(`Adding browser command "${commandName}" to browser object`) const self = this const pageOptionsKey = PAGE_OPTIONS_MAP[commandName] if (commandName === 'waitForStorybookComponentToBeLoaded') { - browser.addCommand(commandName, (options: WaitForStorybookComponentToBeLoaded) => waitForStorybookComponentToBeLoaded(options)) - } else { - browser.addCommand( - commandName, - function ( - this: typeof browser, - tag, - pageOptions = {} - ) { - return command( - { + browser.addCommand(commandName, (options: WaitForStorybookComponentToBeLoaded) => + waitForStorybookComponentToBeLoaded(options) + ) + return + } + + browser.addCommand( + commandName, + function ( + this: WebdriverIO.Browser, + tag: string, + pageOptions = {} + ) { + const wrapped = wrapWithContext({ + browser, + command, + contextManager: self.contextManager, + getArgs: () => { + const updatedInstanceData = { + ...initialInstanceData, + deviceRectangles: self.contextManager.getViewportContext() + } + const isCurrentContextNative = self.contextManager.isNativeContext + + return [{ methods: { executor: ( fn: string | ((...args: InnerArguments) => ReturnValue), - ...args: InnerArguments): Promise => { - return this.execute.bind(browser)(fn, ...args) as Promise + ...args: InnerArguments + ): Promise => { + return this.execute(fn, ...args) as Promise }, - getElementRect: this.getElementRect.bind(browser), - screenShot: this.takeScreenshot.bind(browser), + getElementRect: this.getElementRect.bind(this), + screenShot: this.takeScreenshot.bind(this), }, - instanceData, + instanceData: updatedInstanceData, folders: getFolders(pageOptions, self.folders, self.#getBaselineFolder()), tag, - [pageOptionsKey]:{ + [pageOptionsKey]: { wic: self.defaultOptions, method: pageOptions, }, - isNativeContext, + isNativeContext: isCurrentContextNative, testContext: enrichTestContext({ commandName, currentTestContext: self.#testContext, - instanceData, + instanceData: updatedInstanceData, tag, - }) - } - ) + }), + }] + } }) - } + + return wrapped.call(this) + } + ) } #addMultiremoteElementCommand(browser: WebdriverIO.MultiRemoteBrowser, browserNames: string[], commandName: string, command: any) { @@ -301,137 +338,162 @@ export default class WdioImageComparisonService extends BaseClass { this: WebdriverIO.MultiRemoteBrowser, element, tag, - pageOptions = {} + elementOptions = {} ) { const returnData: Record = {} const elementOptionsKey = commandName === 'saveElement' ? 'saveElementOptions' : 'checkElementOptions' for (const browserName of browserNames) { const browserInstance = browser.getInstance(browserName) - const isNativeContext = getNativeContext( - self.#browser as WebdriverIO.Browser | WebdriverIO.MultiRemoteBrowser, - browserInstance, - self._isNativeContext as NativeContextType - ) - const instanceData = await getInstanceData({ + const contextManager = self._contextManagers?.get(browserName) + + if (!contextManager) { + throw new Error(`No ContextManager found for browser instance: ${browserName}`) + } + + const isNativeContext = contextManager.isNativeContext + const initialInstanceData = await getInstanceData({ currentBrowser: browserInstance, - initialDeviceRectangles: self.deviceRectangles, + initialDeviceRectangles: contextManager.getViewportContext(), isNativeContext }) - returnData[browserName] = await command( - { - methods: { - executor: ( - fn: string | ((...args: InnerArguments) => ReturnValue), - ...args: InnerArguments): Promise => { - return this.execute.bind(browser)(fn, ...args) as Promise + const wrapped = wrapWithContext({ + browser: browserInstance, + command, + contextManager, + getArgs: () => { + const updatedInstanceData = { + ...initialInstanceData, + deviceRectangles: contextManager.getViewportContext(), + } + + return [{ + methods: { + executor: ( + fn: string | ((...args: InnerArguments) => ReturnValue), + ...args: InnerArguments + ): Promise => { + return browserInstance.execute(fn, ...args) as Promise + }, + getElementRect: browserInstance.getElementRect.bind(browserInstance), + screenShot: browserInstance.takeScreenshot.bind(browserInstance), + takeElementScreenshot: browserInstance.takeElementScreenshot.bind(browserInstance), }, - getElementRect: browserInstance.getElementRect.bind(browserInstance), - screenShot: browserInstance.takeScreenshot.bind(browserInstance), - takeElementScreenshot: browserInstance.takeElementScreenshot.bind(browserInstance), - }, - instanceData, - folders:getFolders(pageOptions, self.folders, self.#getBaselineFolder()), - tag, - element, - [elementOptionsKey]:{ - wic: self.defaultOptions, - method: pageOptions, - }, - isNativeContext, - testContext: enrichTestContext({ - commandName, - currentTestContext: self.#testContext, - instanceData, + instanceData: updatedInstanceData, + folders: getFolders(elementOptions, self.folders, self.#getBaselineFolder()), tag, - }) + element, + [elementOptionsKey]: { + wic: self.defaultOptions, + method: elementOptions, + }, + isNativeContext, + testContext: enrichTestContext({ + commandName, + currentTestContext: self.#testContext, + instanceData: updatedInstanceData, + tag, + }), + }] } - ) + }) + + returnData[browserName] = await wrapped.call(browserInstance) } + return returnData }) } - #addMultiremoteCommand(browser: WebdriverIO.MultiRemoteBrowser, browserNames: string[], commandName: string, command: any) { + #addMultiremoteCommand( + browser: WebdriverIO.MultiRemoteBrowser, + browserNames: string[], + commandName: string, + command: any + ) { log.info(`Adding browser command "${commandName}" to Multi browser object`) const self = this if (commandName === 'waitForStorybookComponentToBeLoaded') { browser.addCommand(commandName, waitForStorybookComponentToBeLoaded) - } else { - browser.addCommand( - commandName, - async function ( - this: WebdriverIO.MultiRemoteBrowser, - tag, - pageOptions = {} - ) { - const returnData: Record = {} - const pageOptionsKey = PAGE_OPTIONS_MAP[commandName] - - for (const browserName of browserNames) { - const browserInstance = browser.getInstance(browserName) - const isNativeContext = getNativeContext( - self.#browser as WebdriverIO.Browser | WebdriverIO.MultiRemoteBrowser, - browserInstance, - self._isNativeContext as NativeContextType - ) - const instanceData = await getInstanceData({ - currentBrowser: browserInstance, - initialDeviceRectangles: self.deviceRectangles, - isNativeContext - }) - - returnData[browserName] = await command( - { + return + } + + browser.addCommand( + commandName, + async function ( + this: WebdriverIO.MultiRemoteBrowser, + tag, + pageOptions = {} + ) { + const returnData: Record = {} + const pageOptionsKey = PAGE_OPTIONS_MAP[commandName] + + for (const browserName of browserNames) { + const browserInstance = browser.getInstance(browserName) + const contextManager = self._contextManagers?.get(browserName) + + if (!contextManager) { + throw new Error(`No ContextManager found for browser instance: ${browserName}`) + } + + const isNativeContext = getNativeContext({ + capabilities: browserInstance.requestedCapabilities, + isMobile: browserInstance.isMobile, + }) + const initialInstanceData = await getInstanceData({ + currentBrowser: browserInstance, + initialDeviceRectangles: contextManager.getViewportContext(), + isNativeContext + }) + + const wrapped = wrapWithContext({ + browser: browserInstance, + command, + contextManager, + getArgs: () => { + const updatedInstanceData = { + ...initialInstanceData, + deviceRectangles: contextManager.getViewportContext() + } + const isCurrentContextNative = contextManager.isNativeContext + + return [{ methods: { executor: ( fn: string | ((...args: InnerArguments) => ReturnValue), - ...args: InnerArguments): Promise => { - return this.execute.bind(browser)(fn, ...args) as Promise + ...args: InnerArguments + ): Promise => { + return browserInstance.execute(fn, ...args) as Promise }, getElementRect: browserInstance.getElementRect.bind(browserInstance), screenShot: browserInstance.takeScreenshot.bind(browserInstance), }, - instanceData, - folders:getFolders(pageOptions, self.folders, self.#getBaselineFolder()), + instanceData: updatedInstanceData, + folders: getFolders(pageOptions, self.folders, self.#getBaselineFolder()), tag, - [pageOptionsKey]:{ + [pageOptionsKey]: { wic: self.defaultOptions, method: pageOptions, }, - isNativeContext, + isNativeContext: isCurrentContextNative, testContext: enrichTestContext({ commandName, currentTestContext: self.#testContext, - instanceData, + instanceData: updatedInstanceData, tag, - }) - }) - } - return returnData - }) - } - } + }), + }] + } + }) - #setupMultiremoteContextListener() { - const multiremoteBrowser = this.#browser as WebdriverIO.MultiRemoteBrowser - const browserInstances = multiremoteBrowser.instances - - for (const instanceName of browserInstances) { - const instance = (multiremoteBrowser as any)[instanceName] - instance.on('result', (result:MultiremoteCommandResult) => { - if (result.command === 'getContext') { - const value = result.result.value - const sessionId = instance.sessionId - if (typeof this._isNativeContext !== 'object' || this._isNativeContext === null) { - this._isNativeContext = {} - } - this._isNativeContext[sessionId] = value.includes('NATIVE') + returnData[browserName] = await wrapped.call(browserInstance) } - }) - } + + return returnData + } + ) } #getTestContext(test: Frameworks.Test | Frameworks.World): TestContext { @@ -484,4 +546,11 @@ export default class WdioImageComparisonService extends BaseClass { throw new SevereServiceError(`Framework ${framework} is not supported by the Visual Service and should be either "mocha", "jasmine" or "cucumber".`) } + + get contextManager(): ContextManager { + if (!this._contextManager) { + throw new Error('ContextManager has not been initialized') + } + return this._contextManager + } } diff --git a/packages/visual-service/src/types.ts b/packages/visual-service/src/types.ts index 451e12d1c..ea87fce11 100644 --- a/packages/visual-service/src/types.ts +++ b/packages/visual-service/src/types.ts @@ -13,6 +13,7 @@ import type { InstanceData, } from 'webdriver-image-comparison' import type { ChainablePromiseElement } from 'webdriverio' +import type { ContextManager } from './contextManager.js' type MultiOutput = { [browserName: string]: ScreenshotOutput; @@ -22,17 +23,6 @@ type MultiResult = { [browserName: string]: ImageCompareResult | number; }; export type Result = MultiResult | (ImageCompareResult | number); -export type NativeContextType = boolean | Record -export type MultiremoteCommandResult = { - command: string, - method: string, - endpoint: string, - body: Record, - result: { value: string }, - sessionId: string | undefined, - cid: string, - type: string, -} export type MobileInstanceData = { devicePixelRatio: number; deviceRectangles: DeviceRectangles; @@ -62,6 +52,13 @@ export type GetMobileInstanceDataOptions = { nativeWebScreenshot:boolean; } +export interface WrapWithContextOptions any> { + browser: WebdriverIO.Browser + command: T + contextManager: ContextManager + getArgs: () => Parameters +} + export interface WdioIcsOptions { logName?: string; name?: string; diff --git a/packages/visual-service/src/utils.ts b/packages/visual-service/src/utils.ts index 56309f84a..194ac6c7c 100644 --- a/packages/visual-service/src/utils.ts +++ b/packages/visual-service/src/utils.ts @@ -8,7 +8,6 @@ import type { GetInstanceDataOptions, GetMobileInstanceDataOptions, MobileInstanceData, - NativeContextType, WdioIcsOptions, } from './types.js' @@ -76,7 +75,12 @@ async function getMobileInstanceData({ const getUrl = () => currentBrowser.getUrl() const url = (arg:string) => currentBrowser.url(arg) const currentDriverCapabilities = currentBrowser.capabilities - const { height: screenHeight, width: screenWidth } = await getMobileScreenSize({ executor, isIOS }) + const { height: screenHeight, width: screenWidth } = await getMobileScreenSize({ + currentBrowser, + executor, + isIOS, + isNativeContext, + }) // Update the width for the device rectangles for bottomBar, screenSize, statusBar, statusBarAndAddressBar deviceRectangles.screenSize.height = screenHeight deviceRectangles.screenSize.width = screenWidth @@ -242,6 +246,7 @@ export async function getInstanceData({ devicePixelRatio: mobileDevicePixelRatio, deviceRectangles, } = await getMobileInstanceData({ currentBrowser, initialDeviceRectangles, isNativeContext, nativeWebScreenshot }) + devicePixelRatio = isMobile ? mobileDevicePixelRatio : devicePixelRatio return { @@ -270,77 +275,36 @@ export function getBrowserObject (elem: WebdriverIO.Element | WebdriverIO.Browse return (elemObject as WebdriverIO.Element).parent ? getBrowserObject(elemObject.parent) : elem as WebdriverIO.Browser } -/** - * 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: WebdriverIO.Browser | WebdriverIO.MultiRemoteBrowser): NativeContextType { - // First check if it's multi remote - if (driver.isMultiremote) { - return Object.keys(driver).reduce((acc, instanceName) => { - const instance = (driver as any)[instanceName] as WebdriverIO.Browser - - if (instance.sessionId) { - acc[instance.sessionId] = determineNativeContext(instance) as boolean - } - return acc - }, {} as Record) - } - - // If not check if it's a mobile - if (driver.isMobile) { - const isAppiumAppCapPresent = (capabilities: AppiumCapabilities) => { - 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 as keyof AppiumCapabilities] !== undefined) - // @ts-expect-error - const isInAppiumOptions = capabilities['appium:options'] && - // @ts-expect-error - optionsKeys.some(key => capabilities['appium:options']?.[key as keyof AppiumCapabilities['appium:options']] !== undefined) - // @ts-expect-error - const isInLtOptions = capabilities['lt:options'] && - // @ts-expect-error - optionsKeys.some(key => capabilities['lt:options']?.[key as keyof AppiumCapabilities['lt:options']] !== undefined) - - return !!(isInRoot || isInAppiumOptions || isInLtOptions) - } - const capabilities = driver.requestedCapabilities as WebdriverIO.Capabilities & AppiumCapabilities - 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: WebdriverIO.Browser | WebdriverIO.MultiRemoteBrowser, - currentBrowser: WebdriverIO.Browser, - nativeContext: NativeContextType +const appiumKeys = ['app', 'bundleId', 'appPackage', 'appActivity', 'appWaitActivity', 'appWaitPackage'] as const +type AppiumKeysType = typeof appiumKeys[number] +export function getNativeContext({ capabilities, isMobile }: + { capabilities: WebdriverIO.Capabilities, isMobile: boolean } ): boolean { - if (browser.isMultiremote) { - return (nativeContext as any)[currentBrowser.sessionId] - } else if (typeof nativeContext === 'boolean') { - return nativeContext + if (!capabilities || typeof capabilities !== 'object' || !isMobile) { + return false } - return false + const isAppiumAppCapPresent = (capabilities: Capabilities.RequestedStandaloneCapabilities) => { + return appiumKeys.some((key) => ( + (capabilities as Capabilities.AppiumCapabilities)[key as keyof Capabilities.AppiumCapabilities] !== undefined || + (capabilities as Capabilities.AppiumCapabilities)[`appium:${key}`as keyof Capabilities.AppiumCapabilities] !== undefined || + (capabilities as WebdriverIO.Capabilities)['appium:options']?.[key as AppiumKeysType] !== undefined || + (capabilities as WebdriverIO.Capabilities)['lt:options']?.[key as AppiumKeysType] !== 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 } /** diff --git a/packages/visual-service/src/wrapWithContext.ts b/packages/visual-service/src/wrapWithContext.ts new file mode 100644 index 000000000..726ef6414 --- /dev/null +++ b/packages/visual-service/src/wrapWithContext.ts @@ -0,0 +1,29 @@ +import type { InstanceData } from 'webdriver-image-comparison' +import type { WrapWithContextOptions } from './types.js' +import { getInstanceData } from './utils.js' + +/** + * Wrap the command with the context manager + * This will make sure that the context manager is updated when needed + * and that the command is executed in the correct context + */ + +export function wrapWithContext any>(opts: WrapWithContextOptions): () => Promise> { + const { browser, command, contextManager, getArgs } = opts + + return async function (this: WebdriverIO.Browser): Promise> { + if (contextManager.needsUpdate) { + const instanceData: InstanceData = await getInstanceData({ + currentBrowser: browser, + initialDeviceRectangles: contextManager.getViewportContext(), + isNativeContext: contextManager.isNativeContext, + }) + + contextManager.setViewPortContext(instanceData.deviceRectangles) + } + + const finalArgs = getArgs() + + return command.apply(this, finalArgs) + } +} diff --git a/packages/visual-service/tests/ContextManager.test.ts b/packages/visual-service/tests/ContextManager.test.ts new file mode 100644 index 000000000..00601f0c2 --- /dev/null +++ b/packages/visual-service/tests/ContextManager.test.ts @@ -0,0 +1,111 @@ +import { describe, it, expect, vi, beforeEach } from 'vitest' +import { DEVICE_RECTANGLES } from 'webdriver-image-comparison' +import { ContextManager } from '../src/contextManager.js' + +vi.mock('../src/utils', async () => { + return { + getNativeContext: vi.fn().mockReturnValue(true), + } +}) + +describe('ContextManager', () => { + let browserMock: WebdriverIO.Browser + let eventHandlers: Record + + beforeEach(() => { + eventHandlers = {} + browserMock = { + capabilities: {}, + isMobile: true, + on: vi.fn((event: string, handler: Function) => { + eventHandlers[event] = handler + }) + } as unknown as WebdriverIO.Browser + }) + + it('should initialize with default values and attach event listener', () => { + const cm = new ContextManager(browserMock) + expect(cm.getViewportContext()).toEqual(DEVICE_RECTANGLES) + expect(cm.needsUpdate).toBe(false) + expect(browserMock.on).toHaveBeenCalledWith('result', expect.any(Function)) + }) + + it('should update the viewport context and reset the needsUpdate flag', () => { + const cm = new ContextManager(browserMock) + const newRects = { + ...DEVICE_RECTANGLES, + screenSize: { height: 100, width: 200 }, + } + cm.setViewPortContext(newRects) + expect(cm.getViewportContext()).toEqual(newRects) + expect(cm.needsUpdate).toBe(false) + }) + + it('should mark for update', () => { + const cm = new ContextManager(browserMock) + cm.markForUpdate() + expect(cm.needsUpdate).toBe(true) + }) + + it('should handle orientation change command', async () => { + const cm = new ContextManager(browserMock) + const event = { + command: 'setOrientation', + body: { orientation: 'LANDSCAPE' }, + result: {}, + } + // @ts-ignore + await eventHandlers.result(event) + expect(cm.needsUpdate).toBe(true) + }) + + it('should not mark for update if orientation fails', async () => { + const cm = new ContextManager(browserMock) + const event = { + command: 'setOrientation', + body: { orientation: 'PORTRAIT' }, + result: { error: 'some error' } + } + // @ts-ignore + await eventHandlers.result(event) + expect(cm.needsUpdate).toBe(false) + }) + + it('should not mark for update on invalid command', async () => { + const cm = new ContextManager(browserMock) + const event = { + command: 'unknownCommand', + body: {}, + result: {}, + } + // @ts-ignore + await eventHandlers.result(event) + expect(cm.needsUpdate).toBe(false) + }) + + it('should handle context change command (switchAppiumContext)', async () => { + const cm = new ContextManager(browserMock) + const event = { + command: 'switchAppiumContext', + body: { name: 'WEBVIEW_1' }, + result: {}, + } + // @ts-ignore + await eventHandlers.result(event) + expect(await cm.getCurrentContext()).toBe('WEBVIEW_1') + expect(cm.needsUpdate).toBe(true) + }) + + it('should handle context change command (switchContext)', async () => { + const cm = new ContextManager(browserMock) + const event = { + command: 'switchContext', + body: { name: 'NATIVE_APP' }, + result: {}, + } + // @ts-ignore + await eventHandlers.result(event) + expect(await cm.getCurrentContext()).toBe('NATIVE_APP') + expect(cm.needsUpdate).toBe(false) + }) +}) diff --git a/packages/visual-service/tests/__snapshots__/utils.test.ts.snap b/packages/visual-service/tests/__snapshots__/utils.test.ts.snap index e5a044078..a0074cdb6 100644 --- a/packages/visual-service/tests/__snapshots__/utils.test.ts.snap +++ b/packages/visual-service/tests/__snapshots__/utils.test.ts.snap @@ -1,5 +1,30 @@ // Vitest Snapshot v1, https://vitest.dev/guide/snapshot.html +exports[`utils > enrichTestContext > should generate the expected TestContext structure with values 1`] = ` +{ + "commandName": "checkScreen", + "framework": "mocha", + "instanceData": { + "app": "myApp.apk", + "browser": { + "name": "chrome", + "version": "114", + }, + "deviceName": "Pixel_5", + "isAndroid": true, + "isIOS": false, + "isMobile": true, + "platform": { + "name": "android", + "version": "13.0", + }, + }, + "parent": "Login tests", + "tag": "login-screen", + "title": "should show login screen", +} +`; + exports[`utils > getFolders > should be able to return the correct folders when method options are provided 1`] = ` { "actualFolder": "methodActual", @@ -744,3 +769,17 @@ exports[`utils > getInstanceData > should return instance data when wdio-ics opt "platformVersion": "not_known", } `; + +exports[`utils > getLtOptions > should return the lt:options when it exists (correct casing) 1`] = ` +{ + "project": "testProject", + "user": "wim", +} +`; + +exports[`utils > getLtOptions > should return the lt:options when it exists (different casing) 1`] = ` +{ + "project": "testUpper", + "user": "upperCase", +} +`; diff --git a/packages/visual-service/tests/service.expect.test.ts b/packages/visual-service/tests/service.expect.test.ts index 6e2ba70b8..1d053fa08 100644 --- a/packages/visual-service/tests/service.expect.test.ts +++ b/packages/visual-service/tests/service.expect.test.ts @@ -15,6 +15,16 @@ vi.mock('webdriver-image-comparison', () => ({ checkTabbablePage: vi.fn(), DEFAULT_TEST_CONTEXT: {}, NOT_KNOWN: 'not_known', + DEVICE_RECTANGLES: { + bottomBar: { y: 0, x: 0, width: 0, height: 0 }, + homeBar: { y: 0, x: 0, width: 0, height: 0 }, + leftSidePadding: { y: 0, x: 0, width: 0, height: 0 }, + rightSidePadding: { y: 0, x: 0, width: 0, height: 0 }, + statusBar: { y: 0, x: 0, width: 0, height: 0 }, + statusBarAndAddressBar: { y: 0, x: 0, width: 0, height: 0 }, + screenSize: { width: 0, height: 0 }, + viewport: { y: 0, x: 0, width: 0, height: 0 }, + } })) const log = logger('test') @@ -29,7 +39,8 @@ describe('@wdio/visual-service', () => { isMultiremote: false, addCommand: vi.fn(), capabilities: {}, - requestedCapabilities: {} + requestedCapabilities: {}, + on: vi.fn(), } as any as WebdriverIO.Browser await service.before({}, [], browser) // expect(log.warn).toBeCalledTimes(1) diff --git a/packages/visual-service/tests/service.test.ts b/packages/visual-service/tests/service.test.ts index c1ac5e934..94a781d5d 100644 --- a/packages/visual-service/tests/service.test.ts +++ b/packages/visual-service/tests/service.test.ts @@ -18,6 +18,16 @@ vi.mock('webdriver-image-comparison', () => ({ checkTabbablePage: vi.fn(), DEFAULT_TEST_CONTEXT: {}, NOT_KNOWN: 'not_known', + DEVICE_RECTANGLES: { + bottomBar: { y: 0, x: 0, width: 0, height: 0 }, + homeBar: { y: 0, x: 0, width: 0, height: 0 }, + leftSidePadding: { y: 0, x: 0, width: 0, height: 0 }, + rightSidePadding: { y: 0, x: 0, width: 0, height: 0 }, + statusBar: { y: 0, x: 0, width: 0, height: 0 }, + statusBarAndAddressBar: { y: 0, x: 0, width: 0, height: 0 }, + screenSize: { width: 0, height: 0 }, + viewport: { y: 0, x: 0, width: 0, height: 0 }, + }, })) vi.mock('@wdio/globals', async () => ({ expect: { @@ -33,7 +43,12 @@ describe('@wdio/visual-service', () => { describe('remoteSetup', () => { it('should call the before hook when using the remoteSetup method', async () => { const service = new VisualService({}, {}, {} as unknown as WebdriverIO.Config) - const browser = { addCommand: vi.fn(), capabilities: {}, requestedCapabilities: {} } as any as WebdriverIO.Browser + const browser = { + addCommand: vi.fn(), + capabilities: {}, + requestedCapabilities: {}, + on: vi.fn(), + } as any as WebdriverIO.Browser const spy = vi.spyOn(service, 'before') await service.remoteSetup(browser as any) @@ -45,9 +60,9 @@ describe('@wdio/visual-service', () => { }) describe('before', () => { - let service - let browser - let browserInstance + let service: VisualService + let browser: WebdriverIO.Browser | WebdriverIO.MultiRemoteBrowser + let browserInstance: WebdriverIO.Browser let chromeInstance let firefoxInstance const commands = ['saveElement', 'checkElement', 'saveScreen', 'saveFullPageScreen', 'saveTabbablePage', 'checkScreen', 'checkFullPageScreen', 'checkTabbablePage', 'waitForStorybookComponentToBeLoaded'] @@ -69,6 +84,7 @@ describe('@wdio/visual-service', () => { browser = { isMultiremote: false, addCommand: vi.fn((name, fn) => { + // @ts-expect-error browser[name] = fn }), capabilities: {}, @@ -77,9 +93,12 @@ describe('@wdio/visual-service', () => { getInstance: vi.fn().mockReturnValue(browserInstance), chrome: chromeInstance, firefox: firefoxInstance, + on: vi.fn(), } as any as WebdriverIO.Browser + // @ts-expect-error browserInstance = { addCommand: vi.fn((name, fn) => { + // @ts-expect-error browserInstance[name] = fn }), capabilities: {}, @@ -98,7 +117,9 @@ describe('@wdio/visual-service', () => { it('adds command to multiremote browser in before hook', async () => { browser.isMultiremote = true + // @ts-expect-error browser.getInstances = vi.fn().mockReturnValue(['chrome', 'firefox']) + // @ts-expect-error browser.getInstance = vi.fn().mockReturnValue(browserInstance) await service.before({ @@ -120,7 +141,8 @@ describe('@wdio/visual-service', () => { isMultiremote: false, addCommand: vi.fn(), capabilities: {}, - requestedCapabilities: {} + requestedCapabilities: {}, + on: vi.fn(), } as any as WebdriverIO.Browser await service.before({}, [], browser) @@ -138,7 +160,8 @@ describe('@wdio/visual-service', () => { isMultiremote: false, addCommand: vi.fn(), capabilities: {}, - requestedCapabilities: {} + requestedCapabilities: {}, + on: vi.fn(), } as any as WebdriverIO.Browser await service.before({}, [], browser) @@ -146,22 +169,4 @@ describe('@wdio/visual-service', () => { expect(log.warn).toMatchSnapshot() }) }) - - describe('afterCommand', () => { - it('should set _isNativeContext to true when dealing with a native app', () => { - const service = new VisualService({}, {}, {} as unknown as WebdriverIO.Config) - service.afterCommand('getContext', [], 'NATIVE_APP', undefined) - - // @ts-ignore - expect(service._isNativeContext).toBe(true) - }) - - it('should set _isNativeContext to false when not dealing with a native app', () => { - const service = new VisualService({}, {}, {} as unknown as WebdriverIO.Config) - service.afterCommand('getContext', [], 'WEBVIEW', undefined) - - // @ts-ignore - expect(service._isNativeContext).toBe(false) - }) - }) }) diff --git a/packages/visual-service/tests/utils.test.ts b/packages/visual-service/tests/utils.test.ts index c0fd0db01..55095083f 100644 --- a/packages/visual-service/tests/utils.test.ts +++ b/packages/visual-service/tests/utils.test.ts @@ -1,7 +1,13 @@ -import { describe, it, expect, beforeEach, afterEach, vi } from 'vitest' -import { determineNativeContext, getBrowserObject, getDevicePixelRatio, getFolders, getInstanceData, getBase64ScreenshotSize } from '../src/utils.js' -// @TODO: Remove this import when the types are fixed -import type { AppiumCapabilities } from '@wdio/types/build/Capabilities.js' +import { describe, it, expect, afterEach, vi } from 'vitest' +import { + getBrowserObject, + getDevicePixelRatio, getFolders, + getInstanceData, + getBase64ScreenshotSize, + getNativeContext, + enrichTestContext, + getLtOptions, +} from '../src/utils.js' const DEVICE_RECTANGLES = { bottomBar: { y: 0, x: 0, width: 0, height: 0 }, @@ -84,6 +90,43 @@ describe('utils', () => { }) }) + describe('getLtOptions', () => { + it('should return the lt:options when it exists (correct casing)', () => { + const caps = { + 'lt:options': { user: 'wim', project: 'testProject' } + } + + expect(getLtOptions(caps)).toMatchSnapshot() + }) + + it('should return the lt:options when it exists (different casing)', () => { + const caps = { + 'LT:OPTIONS': { user: 'upperCase', project: 'testUpper' } + } + + // @ts-expect-error + expect(getLtOptions(caps)).toMatchSnapshot() + }) + + it('should return undefined when lt:options does not exist', () => { + const caps = { + platformName: 'iOS', + deviceName: 'iPhone 14' + } + + expect(getLtOptions(caps)).toBeUndefined() + }) + + it('should return undefined when capabilities is an empty object', () => { + expect(getLtOptions({})).toBeUndefined() + }) + + it('should handle unexpected types gracefully', () => { + const caps = Object.create(null) + expect(getLtOptions(caps)).toBeUndefined() + }) + }) + describe('getInstanceData', () => { const DEFAULT_DESKTOP_BROWSER = { capabilities:{ @@ -167,6 +210,7 @@ describe('utils', () => { isMobile: true, getWindowSize: vi.fn().mockResolvedValueOnce({ width: 100, height: 200 }), execute: vi.fn().mockResolvedValueOnce({ realDisplaySize:'100x200' }), + getOrientation: vi.fn().mockResolvedValue('PORTRAIT') }) expect(await getInstanceData({ currentBrowser: driver, initialDeviceRectangles: DEVICE_RECTANGLES, isNativeContext:true })).toMatchSnapshot() }) @@ -202,6 +246,8 @@ describe('utils', () => { isMobile: true, takeScreenshot: vi.fn().mockResolvedValueOnce(mockScreenshot), execute: vi.fn().mockResolvedValueOnce({ screenSize: { height: 852, width: 393 } }), + getWindowSize: vi.fn(), + getOrientation: vi.fn().mockResolvedValue('PORTRAIT') }) expect(await getInstanceData({ currentBrowser: driver, initialDeviceRectangles: DEVICE_RECTANGLES, isNativeContext:true })).toMatchSnapshot() }) @@ -236,6 +282,8 @@ describe('utils', () => { isMobile: true, takeScreenshot: vi.fn().mockResolvedValueOnce(mockScreenshot), execute: vi.fn().mockResolvedValueOnce({ screenSize: { height: 1194, width: 834 } }), + getWindowSize: vi.fn(), + getOrientation: vi.fn().mockResolvedValue('PORTRAIT') }) expect(await getInstanceData({ currentBrowser: driver, initialDeviceRectangles: DEVICE_RECTANGLES, isNativeContext:true })).toMatchSnapshot() }) @@ -270,7 +318,9 @@ describe('utils', () => { isAndroid: false, isMobile: true, takeScreenshot: vi.fn().mockResolvedValueOnce(mockScreenshot), - execute: vi.fn().mockResolvedValueOnce({ screenSize: { height: 834, width: 1194 } }), + execute: vi.fn().mockResolvedValueOnce({ screenSize: { height: 1194, width: 834 } }), + getWindowSize: vi.fn(), + getOrientation: vi.fn().mockResolvedValue('LANDSCAPE') }) expect(await getInstanceData({ currentBrowser: driver, initialDeviceRectangles: DEVICE_RECTANGLES, isNativeContext:true })).toMatchSnapshot() }) @@ -303,7 +353,9 @@ describe('utils', () => { isAndroid: false, isMobile: true, takeScreenshot: vi.fn().mockResolvedValueOnce(mockScreenshot), - execute: vi.fn().mockResolvedValueOnce({ screenSize: { height: 888, width: 1234 } }), + execute: vi.fn().mockResolvedValueOnce({ screenSize: { height: 1234, width: 888 } }), + getWindowSize: vi.fn(), + getOrientation: vi.fn().mockResolvedValue('LANDSCAPE') }) expect(await getInstanceData({ currentBrowser: driver, initialDeviceRectangles: DEVICE_RECTANGLES, isNativeContext:true })).toMatchSnapshot() }) @@ -335,6 +387,8 @@ describe('utils', () => { isAndroid: true, isMobile: true, execute: vi.fn().mockResolvedValueOnce({ realDisplaySize:'100x200' }), + getWindowSize: vi.fn(), + getOrientation: vi.fn().mockResolvedValue('PORTRAIT') }) expect(await getInstanceData({ currentBrowser: driver, initialDeviceRectangles: DEVICE_RECTANGLES, isNativeContext:true })).toMatchSnapshot() }) @@ -356,7 +410,9 @@ describe('utils', () => { }, isAndroid: true, isMobile: true, - execute: vi.fn().mockResolvedValueOnce({ realDisplaySize:'100x200' }), + execute: vi.fn().mockResolvedValueOnce({ realDisplaySize: '100x200' }), + getWindowSize: vi.fn(), + getOrientation: vi.fn().mockResolvedValue('PORTRAIT') }) expect(await getInstanceData({ currentBrowser: driver, initialDeviceRectangles: DEVICE_RECTANGLES, isNativeContext:true })).toMatchSnapshot() }) @@ -380,7 +436,9 @@ describe('utils', () => { }, isAndroid: true, isMobile: true, - execute: vi.fn().mockResolvedValueOnce({ realDisplaySize:'100x200' }), + execute: vi.fn().mockResolvedValueOnce({ realDisplaySize: '100x200' }), + getWindowSize: vi.fn(), + getOrientation: vi.fn().mockResolvedValue('PORTRAIT') }) expect(await getInstanceData({ currentBrowser: driver, initialDeviceRectangles: DEVICE_RECTANGLES, isNativeContext:true })).toMatchSnapshot() }) @@ -425,232 +483,128 @@ describe('utils', () => { }) }) - describe('determineNativeContext', ()=>{ - const DRIVER_DEFAULTS = { - sessionId: 'sessionId', - isMobile: true, - capabilities:{} as WebdriverIO.Capabilities, - requestedCapabilities:{} as WebdriverIO.Capabilities | AppiumCapabilities, - } - let driver = {} as WebdriverIO.Browser - beforeEach(() => { - driver = structuredClone(DRIVER_DEFAULTS) as WebdriverIO.Browser - }) - - it('should return false for desktop browsers', async() => { - driver.isMobile = false - expect(await determineNativeContext(driver)).toBeFalsy() - }) - - it('should return false for mobile browsers when no browser has been set', async() => { - expect(await determineNativeContext(driver)).toBeFalsy() + describe('getNativeContext', () => { + it('should return false if capabilities is not an object', () => { + expect(getNativeContext({ capabilities: null as any, isMobile: true })).toBe(false) + expect(getNativeContext({ capabilities: undefined as any, isMobile: true })).toBe(false) + expect(getNativeContext({ capabilities: 'not-object' as any, isMobile: true })).toBe(false) }) - it('should return false for mobile browsers', async() => { - (driver.capabilities as WebdriverIO.Capabilities).browserName = 'chrome' - expect(await determineNativeContext(driver)).toBeFalsy() + it('should return false if isMobile is false', () => { + expect(getNativeContext({ capabilities: {}, isMobile: false })).toBe(false) }) - it('should return true for when the app is provided and browser name is empty', async() => { - (driver.capabilities as WebdriverIO.Capabilities).browserName = ''; - (driver.requestedCapabilities as AppiumCapabilities)['appium:app'] = 'app' - expect(await determineNativeContext(driver)).toBeTruthy() + it('should return false if browserName is present', () => { + const capabilities = { browserName: 'chrome' } + expect(getNativeContext({ capabilities, isMobile: true })).toBe(false) }) - it('should return true for when the app is provided and autoWebview is false', async() => { - (driver.capabilities as WebdriverIO.Capabilities).browserName = ''; - (driver.requestedCapabilities as AppiumCapabilities)['appium:app'] = 'app'; - (driver.requestedCapabilities as AppiumCapabilities)['appium:autoWebview'] = false - - expect(await determineNativeContext(driver)).toBeTruthy() - }) - - it('should return false for when the app is provided and autoWebview is true', async() => { - (driver.capabilities as WebdriverIO.Capabilities).browserName = ''; - (driver.requestedCapabilities as AppiumCapabilities)['appium:app'] = 'app'; - (driver.requestedCapabilities as AppiumCapabilities)['appium:autoWebview'] = true - - expect(await determineNativeContext(driver)).toBeFalsy() - }) - - it('should return true for when appium:appPackage is provided and autoWebview is true', async() => { - (driver.capabilities as WebdriverIO.Capabilities).browserName = ''; - (driver.requestedCapabilities as AppiumCapabilities)['appium:appPackage'] = 'string'; - (driver.requestedCapabilities as AppiumCapabilities)['appium:autoWebview'] = true - - expect(await determineNativeContext(driver)).toBeFalsy() - }) - - // For iOS - it('should return true for when appium:bundleId is provided and autoWebview is true', async() => { - (driver.capabilities as WebdriverIO.Capabilities).browserName = ''; - (driver.requestedCapabilities as AppiumCapabilities)['appium:bundleId'] = 'string'; - (driver.requestedCapabilities as AppiumCapabilities)['appium:autoWebview'] = true - - expect(await determineNativeContext(driver)).toBeFalsy() - }) - - // For Android - it('should return true for when appium:appPackage is provided', async() => { - (driver.capabilities as WebdriverIO.Capabilities).browserName = ''; - (driver.requestedCapabilities as AppiumCapabilities)['appium:appPackage'] = 'string' - - expect(await determineNativeContext(driver)).toBeTruthy() - }) - - it('should return true for when appium:appActivity is provided', async() => { - (driver.capabilities as WebdriverIO.Capabilities).browserName = ''; - (driver.requestedCapabilities as AppiumCapabilities)['appium:appActivity'] = 'appActivity' - - expect(await determineNativeContext(driver)).toBeTruthy() - }) - - it('should return true for when appium:appWaitActivity is provided', async() => { - (driver.capabilities as WebdriverIO.Capabilities).browserName = ''; - (driver.requestedCapabilities as AppiumCapabilities)['appium:appWaitActivity'] = 'appWaitActivity' - - expect(await determineNativeContext(driver)).toBeTruthy() - }) - - it('should return true for when appium:appWaitPackage is provided', async() => { - (driver.capabilities as WebdriverIO.Capabilities).browserName = ''; - (driver.requestedCapabilities as AppiumCapabilities)['appium:appWaitPackage'] = 'appWaitPackage' - - expect(await determineNativeContext(driver)).toBeTruthy() - }) - - /** - * For the `appium:options` - */ - it('should return true for when the app is provided and browser name is empty for the `appium:options`', async() => { - (driver.capabilities as WebdriverIO.Capabilities).browserName = ''; - (driver.requestedCapabilities as AppiumCapabilities)['appium:options'] = { 'app': 'app' } - expect(await determineNativeContext(driver)).toBeTruthy() - }) + it('should return false if autoWebview is true in various places', () => { + const variants = [ + { autoWebview: true }, + { 'appium:autoWebview': true }, + { 'appium:options': { autoWebview: true } }, + { 'lt:options': { autoWebview: true } }, + ] - it('should return true for when the app is provided and autoWebview is false for the `appium:options`', async() => { - (driver.capabilities as WebdriverIO.Capabilities).browserName = ''; - (driver.requestedCapabilities as AppiumCapabilities)['appium:options'] = { app: 'app', autoWebview: false } - - expect(await determineNativeContext(driver)).toBeTruthy() - }) - - it('should return false for when the app is provided and autoWebview is true for the `appium:options`', async() => { - (driver.capabilities as WebdriverIO.Capabilities).browserName = ''; - (driver.requestedCapabilities as AppiumCapabilities)['appium:options'] = { app: 'app', autoWebview: true } - - expect(await determineNativeContext(driver)).toBeFalsy() - }) - - it('should return true for when appPackage is provided and autoWebview is true for the `appium:options`', async() => { - (driver.capabilities as WebdriverIO.Capabilities).browserName = ''; - (driver.requestedCapabilities as AppiumCapabilities)['appium:options'] = { appPackage: 'string', autoWebview: true } - - expect(await determineNativeContext(driver)).toBeFalsy() - }) - - // For iOS - it('should return true for when bundleId is provided and autoWebview is true for the `appium:options`', async() => { - (driver.capabilities as WebdriverIO.Capabilities).browserName = ''; - (driver.requestedCapabilities as AppiumCapabilities)['appium:options'] = { bundleId: 'string', autoWebview: true } - - expect(await determineNativeContext(driver)).toBeFalsy() - }) - - // For Android - it('should return true for when appPackage is provided for the `appium:options`', async() => { - (driver.capabilities as WebdriverIO.Capabilities).browserName = ''; - (driver.requestedCapabilities as AppiumCapabilities)['appium:options'] = { appPackage: 'string' } - - expect(await determineNativeContext(driver)).toBeTruthy() - }) - - it('should return true for when appActivity is provided for the `appium:options`', async() => { - (driver.capabilities as WebdriverIO.Capabilities).browserName = ''; - (driver.requestedCapabilities as AppiumCapabilities)['appium:options'] = { appActivity: 'appActivity' } - - expect(await determineNativeContext(driver)).toBeTruthy() - }) - - it('should return true for when appWaitActivity is provided for the `appium:options`', async() => { - (driver.capabilities as WebdriverIO.Capabilities).browserName = ''; - (driver.requestedCapabilities as AppiumCapabilities)['appium:options'] = { appWaitActivity: 'appWaitActivity' } - - expect(await determineNativeContext(driver)).toBeTruthy() - }) - - it('should return true for when appWaitPackage is provided for the `appium:options`', async() => { - (driver.capabilities as WebdriverIO.Capabilities).browserName = ''; - (driver.requestedCapabilities as AppiumCapabilities)['appium:options'] = { appWaitPackage: 'appWaitPackage' } - - expect(await determineNativeContext(driver)).toBeTruthy() - }) - - /** - * For the `lt:options` - */ - it('should return true for when the app is provided and browser name is empty for the `lt:options`', async() => { - (driver.capabilities as WebdriverIO.Capabilities).browserName = ''; - (driver.requestedCapabilities as AppiumCapabilities)['lt:options'] = { 'app': 'app' } - expect(await determineNativeContext(driver)).toBeTruthy() - }) - - it('should return true for when the app is provided and autoWebview is false for the `lt:options`', async() => { - (driver.capabilities as WebdriverIO.Capabilities).browserName = ''; - (driver.requestedCapabilities as AppiumCapabilities)['lt:options'] = { app: 'app', autoWebview: false } - - expect(await determineNativeContext(driver)).toBeTruthy() - }) - - it('should return false for when the app is provided and autoWebview is true for the `lt:options`', async() => { - (driver.capabilities as WebdriverIO.Capabilities).browserName = ''; - (driver.requestedCapabilities as AppiumCapabilities)['lt:options'] = { app: 'app', autoWebview: true } - - expect(await determineNativeContext(driver)).toBeFalsy() - }) - - it('should return true for when appPackage is provided and autoWebview is true for the `lt:options`', async() => { - (driver.capabilities as WebdriverIO.Capabilities).browserName = ''; - (driver.requestedCapabilities as AppiumCapabilities)['lt:options'] = { appPackage: 'string', autoWebview: true } - - expect(await determineNativeContext(driver)).toBeFalsy() + for (const caps of variants) { + const capabilities = { browserName: undefined, ...caps } + expect(getNativeContext({ capabilities, isMobile: true })).toBe(false) + } }) - // For iOS - it('should return true for when bundleId is provided and autoWebview is true for the `lt:options`', async() => { - (driver.capabilities as WebdriverIO.Capabilities).browserName = ''; - (driver.requestedCapabilities as AppiumCapabilities)['lt:options'] = { bundleId: 'string', autoWebview: true } - - expect(await determineNativeContext(driver)).toBeFalsy() + it('should return true if browserName is falsy, autoWebview is false, and appium app caps are present in root', () => { + const capabilities = { + browserName: undefined, + app: 'my.app', + } + expect(getNativeContext({ capabilities, isMobile: true })).toBe(true) }) - // For Android - it('should return true for when appPackage is provided for the `lt:options`', async() => { - (driver.capabilities as WebdriverIO.Capabilities).browserName = ''; - (driver.requestedCapabilities as AppiumCapabilities)['lt:options'] = { appPackage: 'string' } - - expect(await determineNativeContext(driver)).toBeTruthy() + it('should return true if appPackage is in appium:options and autoWebview is false', () => { + const capabilities = { + browserName: undefined, + 'appium:options': { + appPackage: 'com.example', + }, + } + expect(getNativeContext({ capabilities, isMobile: true })).toBe(true) }) - it('should return true for when appActivity is provided for the `lt:options`', async() => { - (driver.capabilities as WebdriverIO.Capabilities).browserName = ''; - (driver.requestedCapabilities as AppiumCapabilities)['lt:options'] = { appActivity: 'appActivity' } - - expect(await determineNativeContext(driver)).toBeTruthy() + it('should return true if bundleId is in lt:options and autoWebview is false', () => { + const capabilities = { + browserName: undefined, + 'lt:options': { + bundleId: 'com.example.app', + }, + } + expect(getNativeContext({ capabilities, isMobile: true })).toBe(true) }) - it('should return true for when appWaitActivity is provided for the `lt:options`', async() => { - (driver.capabilities as WebdriverIO.Capabilities).browserName = ''; - (driver.requestedCapabilities as AppiumCapabilities)['lt:options'] = { appWaitActivity: 'appWaitActivity' } - - expect(await determineNativeContext(driver)).toBeTruthy() + it('should return false if no appium-related caps are found', () => { + const capabilities = { + browserName: undefined, + someOtherCap: true + } + expect(getNativeContext({ capabilities, isMobile: true })).toBe(false) }) + }) - it('should return true for when appWaitPackage is provided for the `lt:options`', async() => { - (driver.capabilities as WebdriverIO.Capabilities).browserName = ''; - (driver.requestedCapabilities as AppiumCapabilities)['lt:options'] = { appWaitPackage: 'appWaitPackage' } + describe('enrichTestContext', () => { + it('should generate the expected TestContext structure with values', () => { + const result = enrichTestContext({ + commandName: 'checkScreen', + currentTestContext: { + commandName: 'checkScreen', + framework: 'mocha', + parent: 'Login tests', + tag: 'login-screen', + title: 'should show login screen', + instanceData: { + browser: { + name: 'chrome', + version: '114', + }, + deviceName: 'Pixel_5', + platform: { + name: 'android', + version: '13.0', + }, + app: 'myApp.apk', + isMobile: true, + isAndroid: true, + isIOS: false, + } + }, + instanceData: { + appName: 'myApp.apk', + browserName: 'chrome', + browserVersion: '114', + deviceName: 'Pixel_5', + isMobile: true, + isAndroid: true, + isIOS: false, + platformName: 'android', + platformVersion: '13.0', + devicePixelRatio: 3.5, + deviceRectangles: { + bottomBar: { y: 0, x: 0, width: 0, height: 0 }, + homeBar: { y: 0, x: 0, width: 0, height: 0 }, + leftSidePadding: { y: 0, x: 0, width: 0, height: 0 }, + rightSidePadding: { y: 0, x: 0, width: 0, height: 0 }, + statusBar: { y: 0, x: 0, width: 0, height: 0 }, + statusBarAndAddressBar: { y: 0, x: 0, width: 0, height: 0 }, + screenSize: { width: 0, height: 0 }, + viewport: { y: 0, x: 0, width: 0, height: 0 }, + }, + logName: 'Pixel_5_Chrome', + name: 'Pixel_5', + nativeWebScreenshot: false, + }, + tag: 'login-screen' + }) - expect(await determineNativeContext(driver)).toBeTruthy() + expect(result).toMatchSnapshot() }) }) }) diff --git a/packages/visual-service/tests/wrapWithContext.test.ts b/packages/visual-service/tests/wrapWithContext.test.ts new file mode 100644 index 000000000..4db7024aa --- /dev/null +++ b/packages/visual-service/tests/wrapWithContext.test.ts @@ -0,0 +1,62 @@ +import { DEVICE_RECTANGLES } from 'webdriver-image-comparison' +import { describe, it, expect, vi, beforeEach } from 'vitest' +import * as utilsModule from '../src/utils.js' +import { wrapWithContext } from '../src/wrapWithContext.js' + +describe('wrapWithContext', () => { + let mockCommand: any + let mockContextManager: any + let mockBrowser: WebdriverIO.Browser + + beforeEach(() => { + mockCommand = vi.fn() + mockContextManager = { + needsUpdate: false, + getViewportContext: vi.fn(() => DEVICE_RECTANGLES), + setViewPortContext: vi.fn(), + isNativeContext: false, + } + mockBrowser = {} as WebdriverIO.Browser + }) + + it('should call command directly when no update is needed', async () => { + const wrapper = wrapWithContext({ + browser: mockBrowser, + command: mockCommand, + contextManager: mockContextManager, + getArgs: () => ['arg1'] + }) + + await wrapper.call(mockBrowser) + expect(mockCommand).toHaveBeenCalledWith('arg1') + expect(mockContextManager.setViewPortContext).not.toHaveBeenCalled() + }) + + it('should fetch new instanceData and update context when needed', async () => { + mockContextManager.needsUpdate = true + const updatedRectangles = { + ...DEVICE_RECTANGLES, + screenSize: { height: 900, width: 450 } + } + + const getInstanceDataMock = vi.spyOn(utilsModule, 'getInstanceData').mockResolvedValue({ + deviceRectangles: updatedRectangles, + devicePixelRatio: 3, + } as any) + + const wrapper = wrapWithContext({ + browser: mockBrowser, + command: mockCommand, + contextManager: mockContextManager, + getArgs: () => ['arg2'] + }) + + await wrapper.call(mockBrowser) + + expect(getInstanceDataMock).toHaveBeenCalled() + expect(mockContextManager.setViewPortContext).toHaveBeenCalledWith(updatedRectangles) + expect(mockCommand).toHaveBeenCalledWith('arg2') + + getInstanceDataMock.mockRestore() + }) +}) diff --git a/packages/webdriver-image-comparison/src/base.ts b/packages/webdriver-image-comparison/src/base.ts index 7aae08ed8..d6668f630 100644 --- a/packages/webdriver-image-comparison/src/base.ts +++ b/packages/webdriver-image-comparison/src/base.ts @@ -2,16 +2,14 @@ import { join, normalize } from 'node:path' import { rmSync } from 'node:fs' import logger from '@wdio/logger' import { defaultOptions } from './helpers/options.js' -import { DEVICE_RECTANGLES, FOLDERS } from './helpers/constants.js' +import { FOLDERS } from './helpers/constants.js' import type { Folders } from './base.interfaces.js' import type { ClassOptions, DefaultOptions } from './helpers/options.interfaces.js' -import type { DeviceRectangles } from './methods/rectangles.interfaces.js' const log = logger('@wdio/visual-service:webdriver-image-comparison') export default class BaseClass { defaultOptions: DefaultOptions - deviceRectangles: DeviceRectangles folders: Folders constructor(options: ClassOptions) { @@ -31,8 +29,6 @@ export default class BaseClass { diffFolder: join(baseFolder, FOLDERS.DIFF), } - this.deviceRectangles = DEVICE_RECTANGLES - if (options.clearRuntimeFolder) { log.info('\x1b[33m\n##############################\n!!CLEARING!!\n##############################\x1b[0m') rmSync(this.folders.actualFolder, { recursive: true, force: true }) diff --git a/packages/webdriver-image-comparison/src/helpers/utils.interfaces.ts b/packages/webdriver-image-comparison/src/helpers/utils.interfaces.ts index 66d0a2757..63ebe0800 100644 --- a/packages/webdriver-image-comparison/src/helpers/utils.interfaces.ts +++ b/packages/webdriver-image-comparison/src/helpers/utils.interfaces.ts @@ -117,3 +117,10 @@ export interface GetMobileViewPortPositionOptions { screenHeight: number, screenWidth: number, } + +export interface GetMobileScreenSizeOptions { + currentBrowser: WebdriverIO.Browser, + executor: Executor, + isIOS: boolean, + isNativeContext: boolean, +} diff --git a/packages/webdriver-image-comparison/src/helpers/utils.test.ts b/packages/webdriver-image-comparison/src/helpers/utils.test.ts index a57cfbd15..72cea16d8 100644 --- a/packages/webdriver-image-comparison/src/helpers/utils.test.ts +++ b/packages/webdriver-image-comparison/src/helpers/utils.test.ts @@ -574,72 +574,97 @@ describe('utils', () => { }) describe('getMobileScreenSize', () => { - it('should return screen size for iOS using mobile: deviceScreenInfo', async () => { - const mockExecutor = vi.fn().mockResolvedValue({ - statusBarSize: { width: 0, height: 0 }, - scale: 2, + afterEach(() => { + vi.restoreAllMocks() + }) + + it('returns iOS screen size in portrait', async () => { + const executor = vi.fn().mockResolvedValue({ screenSize: { width: 390, height: 844 }, }) + const browser = { getOrientation: vi.fn().mockResolvedValue('PORTRAIT') } as any - const result = await getMobileScreenSize({ executor: mockExecutor, isIOS: true }) + const result = await getMobileScreenSize({ executor, currentBrowser: browser, isIOS: true, isNativeContext: true }) - expect(mockExecutor).toHaveBeenCalledWith('mobile: deviceScreenInfo') expect(result).toEqual({ width: 390, height: 844 }) }) - it('should return screen size for Android using mobile: deviceInfo', async () => { - const mockExecutor = vi.fn().mockResolvedValue({ - realDisplaySize: '1080x2400', + it('returns iOS screen size in landscape', async () => { + const executor = vi.fn().mockResolvedValue({ + screenSize: { width: 390, height: 844 }, }) + const browser = { getOrientation: vi.fn().mockResolvedValue('LANDSCAPE') } as any + + const result = await getMobileScreenSize({ executor, currentBrowser: browser, isIOS: true, isNativeContext: true }) + + expect(result).toEqual({ width: 844, height: 390 }) + }) - const result = await getMobileScreenSize({ executor: mockExecutor, isIOS: false }) + it('returns Android screen size in portrait', async () => { + const executor = vi.fn().mockResolvedValue({ realDisplaySize: '1080x2400' }) + const browser = { getOrientation: vi.fn().mockResolvedValue('PORTRAIT') } as any + + const result = await getMobileScreenSize({ executor, currentBrowser: browser, isIOS: false, isNativeContext: true }) - expect(mockExecutor).toHaveBeenCalledWith('mobile: deviceInfo') expect(result).toEqual({ width: 1080, height: 2400 }) }) - it('should log a warning return screen size if iOS height format is invalid', async () => { - const logWarnMock = vi.spyOn(log, 'warn') - const mockExecutor = vi.fn().mockResolvedValueOnce({ - statusBarSize: { width: 0, height: 0 }, - scale: 2, - }) - .mockResolvedValueOnce({ - width: 390, height: 844 - }) - const result = await getMobileScreenSize({ executor: mockExecutor, isIOS: true }) + it('falls back for iOS when screenSize is missing (web context)', async () => { + const executor = vi.fn() + .mockRejectedValueOnce(new Error('Missing screenSize')) + .mockResolvedValueOnce({ width: 800, height: 1200 }) + const warnSpy = vi.spyOn(log, 'warn') + const browser = { + getOrientation: vi.fn().mockResolvedValue('PORTRAIT'), + } as any - expect(result).toEqual({ width: 390, height: 844 }) - expect(logWarnMock).toHaveBeenCalledWith( - 'Error getting mobile screen size:\n', - new TypeError('Cannot read properties of undefined (reading \'height\')'), - '\nFalling back to window.screen.height and window.screen.width' - ) + const result = await getMobileScreenSize({ executor, currentBrowser: browser, isIOS: true, isNativeContext: false }) - logWarnMock.mockRestore() + expect(warnSpy).toHaveBeenCalled() + expect(result).toEqual({ width: 800, height: 1200 }) }) - it('should log a warning return screen size if Android realDisplaySize format is invalid', async () => { - const logWarnMock = vi.spyOn(log, 'warn') - const mockExecutor = vi.fn() - .mockResolvedValueOnce({ - realDisplaySize: 'invalid-format', - }) - .mockResolvedValueOnce({ - height: 300, - width: 100, - }) - - expect(await getMobileScreenSize({ executor: mockExecutor, isIOS: false })) - .toEqual({ width: 100, height: 300 }) - - expect(logWarnMock).toHaveBeenCalledWith( - 'Error getting mobile screen size:\n', - new Error('Invalid realDisplaySize format. Expected \'widthxheight\', got "invalid-format"'), - '\nFalling back to window.screen.height and window.screen.width' - ) + it('falls back for Android when realDisplaySize is invalid (web context)', async () => { + const executor = vi.fn() + .mockResolvedValueOnce({ realDisplaySize: 'invalid' }) + .mockResolvedValueOnce({ width: 800, height: 1200 }) + const warnSpy = vi.spyOn(log, 'warn') + const browser = { + getOrientation: vi.fn().mockResolvedValue('PORTRAIT'), + } as any - logWarnMock.mockRestore() + const result = await getMobileScreenSize({ executor, currentBrowser: browser, isIOS: false, isNativeContext: false }) + + expect(warnSpy).toHaveBeenCalled() + expect(result).toEqual({ width: 800, height: 1200 }) + }) + + it('falls back to getWindowSize in native context', async () => { + const executor = vi.fn().mockRejectedValueOnce(new Error('Boom')) + const warnSpy = vi.spyOn(log, 'warn') + const browser = { + getOrientation: vi.fn().mockResolvedValue('PORTRAIT'), + getWindowSize: vi.fn().mockResolvedValue({ width: 123, height: 456 }) + } as any + + const result = await getMobileScreenSize({ executor, currentBrowser: browser, isIOS: true, isNativeContext: true }) + + expect(result).toEqual({ width: 123, height: 456 }) + expect(warnSpy).toHaveBeenCalled() + }) + + it('returns dimensions in landscape fallback native context', async () => { + const executor = vi.fn().mockRejectedValueOnce(new Error('Boom')) + const warnSpy = vi.spyOn(log, 'warn') + const browser = { + getOrientation: vi.fn().mockResolvedValue('LANDSCAPE'), + getWindowSize: vi.fn().mockResolvedValue({ width: 123, height: 456 }) + } as any + + const result = await getMobileScreenSize({ executor, currentBrowser: browser, isIOS: true, isNativeContext: true }) + + expect(result).toEqual({ width: 456, height: 123 }) + expect(warnSpy).toHaveBeenCalled() }) }) @@ -749,10 +774,9 @@ describe('utils', () => { }) expect(mockGetUrl).toHaveBeenCalled() - expect(mockUrl).toHaveBeenCalledWith(expect.stringMatching(/^data:text\/html;base64,/)) + expect(mockUrl).toHaveBeenCalledTimes(2) expect(mockExecutor).toHaveBeenCalledWith(injectWebviewOverlay, false) expect(mockExecutor).toHaveBeenCalledWith(getMobileWebviewClickAndDimensions, '[data-test="ics-overlay"]') - expect(mockUrl).toHaveBeenCalledWith('http://example.com') expect(result).toMatchSnapshot() }) diff --git a/packages/webdriver-image-comparison/src/helpers/utils.ts b/packages/webdriver-image-comparison/src/helpers/utils.ts index 25f391df9..64176bca9 100644 --- a/packages/webdriver-image-comparison/src/helpers/utils.ts +++ b/packages/webdriver-image-comparison/src/helpers/utils.ts @@ -7,6 +7,7 @@ import type { FormatFileNameOptions, GetAddressBarShadowPaddingOptions, GetAndCreatePathOptions, + GetMobileScreenSizeOptions, GetMobileViewPortPositionOptions, GetToolBarShadowPaddingOptions, ScreenshotSize, @@ -339,13 +340,13 @@ export function updateVisualBaseline(): boolean { * Get the mobile screen size, this is different for native and webview */ export async function getMobileScreenSize({ + currentBrowser, executor, - isIOS -}: { - executor: Executor, - isIOS: boolean -}): Promise<{ height: number; width: number }> { + isIOS, + isNativeContext, +}: GetMobileScreenSizeOptions): Promise<{ height: number; width: number }> { let height = 0, width = 0 + const isLandscapeByOrientation = (await currentBrowser.getOrientation()).toUpperCase() === 'LANDSCAPE' try { if (isIOS) { @@ -354,7 +355,6 @@ export async function getMobileScreenSize({ scale: number, screenSize: { width: number, height: number }, }) - // It's Android } else { const { realDisplaySize } = (await executor('mobile: deviceInfo')) as { realDisplaySize: string } @@ -365,13 +365,27 @@ export async function getMobileScreenSize({ [width, height] = realDisplaySize.split('x').map(Number) } } catch (error: unknown) { - log.warn('Error getting mobile screen size:\n', error, '\nFalling back to window.screen.height and window.screen.width'); - // This is a fallback and not 100% accurate, but we need to have something =) - ({ height, width } = await executor(() => { - const { height, width } = window.screen - const isPortrait = window.matchMedia('(orientation: portrait)').matches - return { height: isPortrait? height: width, width: isPortrait? width: height } - })) + log.warn('Error getting mobile screen size:\n', error, `\nFalling back to ${isNativeContext ? + '`getWindowSize()` which might not be as accurate' : + 'window.screen.height and window.screen.width'}` + ) + + if (isNativeContext) { + ({ height, width } = await currentBrowser.getWindowSize()) + } else { + // This is a fallback and not 100% accurate, but we need to have something =) + ({ height, width } = await executor(() => { + const { height, width } = window.screen + return { height, width } + })) + } + } + + // There are issues where the landscape mode by orientation is not the same as the landscape mode by value + // So we need to check and fix this + const isLandscapeByValue = width > height + if (isLandscapeByOrientation !== isLandscapeByValue) { + [height, width] = [width, height] } return { height, width } diff --git a/tests/configs/lambdatest.ios.sims.web.ts b/tests/configs/lambdatest.ios.sims.web.ts index cd474a8bf..02cf109a0 100644 --- a/tests/configs/lambdatest.ios.sims.web.ts +++ b/tests/configs/lambdatest.ios.sims.web.ts @@ -6,31 +6,39 @@ export function lambdaTestIosSimWeb({ buildName }: { buildName: string }) { const iOSDevices = [ { // 812 + appiumVersion: '2.16.2', deviceName: 'iPhone 13 mini', platformVersion: '17.5', }, { // 844 + appiumVersion: '2.16.2', deviceName: 'iPhone 13 Pro', platformVersion: '16.0', }, { // 852 + appiumVersion: '2.16.2', deviceName: 'iPhone 14 Pro', platformVersion: '17.5', }, { // 932 + appiumVersion: '2.16.2', deviceName: 'iPhone 15 Pro Max', platformVersion: '18.0', } ] return [ - ...(['landscape', 'portrait'] as DeviceOrientation[]) + ...([ + 'landscape', + 'portrait' + ] as DeviceOrientation[]) .map((orientation) => iOSDevices - .map(({ deviceName, platformVersion }) => ({ + .map(({ appiumVersion, deviceName, platformVersion }) => ({ 'lt:options': { + ...(appiumVersion ? { appiumVersion } : {}), deviceName, platformName: 'ios', platformVersion, diff --git a/tests/configs/sauce.android.emus.app.ts b/tests/configs/sauce.android.emus.app.ts index 6ead5dfb6..2de8f3d06 100644 --- a/tests/configs/sauce.android.emus.app.ts +++ b/tests/configs/sauce.android.emus.app.ts @@ -70,6 +70,8 @@ function createCaps({ 'appium:platformVersion': platformVersion, 'appium:orientation': orientation.toUpperCase(), 'appium:automationName': 'UIAutomator2', + // @ts-expect-error + 'appium:nativeWebScreenshot': true, 'wdio-ics:options': { logName: `app-Emulator${deviceName.replace( /(\s+|\(+|\)+|Emulator)/g, diff --git a/tests/lambdaTestBaseline/desktop_chrome/tabbable-chrome-latest-1366x768.png b/tests/lambdaTestBaseline/desktop_chrome/tabbable-chrome-latest-1366x768.png index 4a3209b30..d6d2b3a4b 100644 Binary files a/tests/lambdaTestBaseline/desktop_chrome/tabbable-chrome-latest-1366x768.png and b/tests/lambdaTestBaseline/desktop_chrome/tabbable-chrome-latest-1366x768.png differ diff --git a/tests/lambdaTestBaseline/desktop_safari/fullPage-SafariLatest-1366x768.png b/tests/lambdaTestBaseline/desktop_safari/fullPage-SafariLatest-1366x768.png index ea13f6b3c..3c61c9285 100644 Binary files a/tests/lambdaTestBaseline/desktop_safari/fullPage-SafariLatest-1366x768.png and b/tests/lambdaTestBaseline/desktop_safari/fullPage-SafariLatest-1366x768.png differ diff --git a/tests/lambdaTestBaseline/desktop_safari/tabbable-SafariLatest-1366x768.png b/tests/lambdaTestBaseline/desktop_safari/tabbable-SafariLatest-1366x768.png index 599cf0584..0d79f2000 100644 Binary files a/tests/lambdaTestBaseline/desktop_safari/tabbable-SafariLatest-1366x768.png and b/tests/lambdaTestBaseline/desktop_safari/tabbable-SafariLatest-1366x768.png differ diff --git a/tests/lambdaTestBaseline/galaxy_tab_s8/screenshot-landscape-EmulatorGalaxyTabS8PortraitNativeWebScreenshot13-1707x1067.png b/tests/lambdaTestBaseline/galaxy_tab_s8/screenshot-landscape-EmulatorGalaxyTabS8PortraitNativeWebScreenshot13-1707x1067.png new file mode 100644 index 000000000..920d6568e Binary files /dev/null and b/tests/lambdaTestBaseline/galaxy_tab_s8/screenshot-landscape-EmulatorGalaxyTabS8PortraitNativeWebScreenshot13-1707x1067.png differ diff --git a/tests/lambdaTestBaseline/galaxy_tab_s8/screenshot-landscape-EmulatorGalaxyTabS8PortraitNativeWebScreenshot14-1707x1067.png b/tests/lambdaTestBaseline/galaxy_tab_s8/screenshot-landscape-EmulatorGalaxyTabS8PortraitNativeWebScreenshot14-1707x1067.png new file mode 100644 index 000000000..b1eabd64e Binary files /dev/null and b/tests/lambdaTestBaseline/galaxy_tab_s8/screenshot-landscape-EmulatorGalaxyTabS8PortraitNativeWebScreenshot14-1707x1067.png differ diff --git a/tests/lambdaTestBaseline/galaxy_tab_s8/screenshot-portrait-EmulatorGalaxyTabS8LandscapeNativeWebScreenshot13-1067x1707.png b/tests/lambdaTestBaseline/galaxy_tab_s8/screenshot-portrait-EmulatorGalaxyTabS8LandscapeNativeWebScreenshot13-1067x1707.png new file mode 100644 index 000000000..89947ce80 Binary files /dev/null and b/tests/lambdaTestBaseline/galaxy_tab_s8/screenshot-portrait-EmulatorGalaxyTabS8LandscapeNativeWebScreenshot13-1067x1707.png differ diff --git a/tests/lambdaTestBaseline/galaxy_tab_s8/screenshot-portrait-EmulatorGalaxyTabS8LandscapeNativeWebScreenshot14-1067x1707.png b/tests/lambdaTestBaseline/galaxy_tab_s8/screenshot-portrait-EmulatorGalaxyTabS8LandscapeNativeWebScreenshot14-1067x1707.png new file mode 100644 index 000000000..410698160 Binary files /dev/null and b/tests/lambdaTestBaseline/galaxy_tab_s8/screenshot-portrait-EmulatorGalaxyTabS8LandscapeNativeWebScreenshot14-1067x1707.png differ diff --git a/tests/lambdaTestBaseline/iphone_13_mini/screenshot-landscape-Iphone13MiniPortrait17-375x812.png b/tests/lambdaTestBaseline/iphone_13_mini/screenshot-landscape-Iphone13MiniPortrait17-375x812.png new file mode 100644 index 000000000..388a99d74 Binary files /dev/null and b/tests/lambdaTestBaseline/iphone_13_mini/screenshot-landscape-Iphone13MiniPortrait17-375x812.png differ diff --git a/tests/lambdaTestBaseline/iphone_13_mini/screenshot-portrait-Iphone13MiniLandscape17-375x812.png b/tests/lambdaTestBaseline/iphone_13_mini/screenshot-portrait-Iphone13MiniLandscape17-375x812.png new file mode 100644 index 000000000..5542f7b57 Binary files /dev/null and b/tests/lambdaTestBaseline/iphone_13_mini/screenshot-portrait-Iphone13MiniLandscape17-375x812.png differ diff --git a/tests/lambdaTestBaseline/iphone_13_mini/wdioLogo-Iphone13MiniLandscape17-375x812.png b/tests/lambdaTestBaseline/iphone_13_mini/wdioLogo-Iphone13MiniLandscape17-375x812.png index 7261de994..d4e512f54 100644 Binary files a/tests/lambdaTestBaseline/iphone_13_mini/wdioLogo-Iphone13MiniLandscape17-375x812.png and b/tests/lambdaTestBaseline/iphone_13_mini/wdioLogo-Iphone13MiniLandscape17-375x812.png differ diff --git a/tests/lambdaTestBaseline/iphone_13_mini/wdioLogo-Iphone13MiniPortrait17-375x812.png b/tests/lambdaTestBaseline/iphone_13_mini/wdioLogo-Iphone13MiniPortrait17-375x812.png index 559194ad4..f1d8c1d4f 100644 Binary files a/tests/lambdaTestBaseline/iphone_13_mini/wdioLogo-Iphone13MiniPortrait17-375x812.png and b/tests/lambdaTestBaseline/iphone_13_mini/wdioLogo-Iphone13MiniPortrait17-375x812.png differ diff --git a/tests/lambdaTestBaseline/iphone_13_pro/screenshot-landscape-Iphone13ProPortrait16-390x844.png b/tests/lambdaTestBaseline/iphone_13_pro/screenshot-landscape-Iphone13ProPortrait16-390x844.png new file mode 100644 index 000000000..23ee3a6d2 Binary files /dev/null and b/tests/lambdaTestBaseline/iphone_13_pro/screenshot-landscape-Iphone13ProPortrait16-390x844.png differ diff --git a/tests/lambdaTestBaseline/iphone_13_pro/screenshot-portrait-Iphone13ProLandscape16-390x844.png b/tests/lambdaTestBaseline/iphone_13_pro/screenshot-portrait-Iphone13ProLandscape16-390x844.png new file mode 100644 index 000000000..54cc8670f Binary files /dev/null and b/tests/lambdaTestBaseline/iphone_13_pro/screenshot-portrait-Iphone13ProLandscape16-390x844.png differ diff --git a/tests/lambdaTestBaseline/iphone_13_pro/wdioLogo-Iphone13ProLandscape16-390x844.png b/tests/lambdaTestBaseline/iphone_13_pro/wdioLogo-Iphone13ProLandscape16-390x844.png index 6efe14907..d4e512f54 100644 Binary files a/tests/lambdaTestBaseline/iphone_13_pro/wdioLogo-Iphone13ProLandscape16-390x844.png and b/tests/lambdaTestBaseline/iphone_13_pro/wdioLogo-Iphone13ProLandscape16-390x844.png differ diff --git a/tests/lambdaTestBaseline/iphone_13_pro/wdioLogo-Iphone13ProPortrait16-390x844.png b/tests/lambdaTestBaseline/iphone_13_pro/wdioLogo-Iphone13ProPortrait16-390x844.png index 6efe14907..d4e512f54 100644 Binary files a/tests/lambdaTestBaseline/iphone_13_pro/wdioLogo-Iphone13ProPortrait16-390x844.png and b/tests/lambdaTestBaseline/iphone_13_pro/wdioLogo-Iphone13ProPortrait16-390x844.png differ diff --git a/tests/lambdaTestBaseline/iphone_14_pro/screenshot-landscape-Iphone14ProPortrait17-393x852.png b/tests/lambdaTestBaseline/iphone_14_pro/screenshot-landscape-Iphone14ProPortrait17-393x852.png new file mode 100644 index 000000000..8f9f1ab76 Binary files /dev/null and b/tests/lambdaTestBaseline/iphone_14_pro/screenshot-landscape-Iphone14ProPortrait17-393x852.png differ diff --git a/tests/lambdaTestBaseline/iphone_14_pro/screenshot-portrait-Iphone14ProLandscape17-393x852.png b/tests/lambdaTestBaseline/iphone_14_pro/screenshot-portrait-Iphone14ProLandscape17-393x852.png new file mode 100644 index 000000000..c93206191 Binary files /dev/null and b/tests/lambdaTestBaseline/iphone_14_pro/screenshot-portrait-Iphone14ProLandscape17-393x852.png differ diff --git a/tests/lambdaTestBaseline/iphone_14_pro/wdioLogo-Iphone14ProLandscape17-393x852.png b/tests/lambdaTestBaseline/iphone_14_pro/wdioLogo-Iphone14ProLandscape17-393x852.png index 7261de994..d4e512f54 100644 Binary files a/tests/lambdaTestBaseline/iphone_14_pro/wdioLogo-Iphone14ProLandscape17-393x852.png and b/tests/lambdaTestBaseline/iphone_14_pro/wdioLogo-Iphone14ProLandscape17-393x852.png differ diff --git a/tests/lambdaTestBaseline/iphone_14_pro/wdioLogo-Iphone14ProPortrait17-393x852.png b/tests/lambdaTestBaseline/iphone_14_pro/wdioLogo-Iphone14ProPortrait17-393x852.png index 559194ad4..f1d8c1d4f 100644 Binary files a/tests/lambdaTestBaseline/iphone_14_pro/wdioLogo-Iphone14ProPortrait17-393x852.png and b/tests/lambdaTestBaseline/iphone_14_pro/wdioLogo-Iphone14ProPortrait17-393x852.png differ diff --git a/tests/lambdaTestBaseline/iphone_15_pro_max/screenshot-landscape-Iphone15ProMaxPortrait18-430x932.png b/tests/lambdaTestBaseline/iphone_15_pro_max/screenshot-landscape-Iphone15ProMaxPortrait18-430x932.png new file mode 100644 index 000000000..6dd6dbff8 Binary files /dev/null and b/tests/lambdaTestBaseline/iphone_15_pro_max/screenshot-landscape-Iphone15ProMaxPortrait18-430x932.png differ diff --git a/tests/lambdaTestBaseline/iphone_15_pro_max/screenshot-portrait-Iphone15ProMaxLandscape18-430x932.png b/tests/lambdaTestBaseline/iphone_15_pro_max/screenshot-portrait-Iphone15ProMaxLandscape18-430x932.png new file mode 100644 index 000000000..c61923049 Binary files /dev/null and b/tests/lambdaTestBaseline/iphone_15_pro_max/screenshot-portrait-Iphone15ProMaxLandscape18-430x932.png differ diff --git a/tests/lambdaTestBaseline/iphone_15_pro_max/wdioLogo-Iphone15ProMaxLandscape18-430x932.png b/tests/lambdaTestBaseline/iphone_15_pro_max/wdioLogo-Iphone15ProMaxLandscape18-430x932.png index 6efe14907..d4e512f54 100644 Binary files a/tests/lambdaTestBaseline/iphone_15_pro_max/wdioLogo-Iphone15ProMaxLandscape18-430x932.png and b/tests/lambdaTestBaseline/iphone_15_pro_max/wdioLogo-Iphone15ProMaxLandscape18-430x932.png differ diff --git a/tests/lambdaTestBaseline/iphone_15_pro_max/wdioLogo-Iphone15ProMaxPortrait18-430x932.png b/tests/lambdaTestBaseline/iphone_15_pro_max/wdioLogo-Iphone15ProMaxPortrait18-430x932.png index 6efe14907..d4e512f54 100644 Binary files a/tests/lambdaTestBaseline/iphone_15_pro_max/wdioLogo-Iphone15ProMaxPortrait18-430x932.png and b/tests/lambdaTestBaseline/iphone_15_pro_max/wdioLogo-Iphone15ProMaxPortrait18-430x932.png differ diff --git a/tests/lambdaTestBaseline/pixel_4/screenshot-landscape-EmulatorPixel4PortraitNativeWebScreenshot11-652x309.png b/tests/lambdaTestBaseline/pixel_4/screenshot-landscape-EmulatorPixel4PortraitNativeWebScreenshot11-652x309.png new file mode 100644 index 000000000..23e7b3cad Binary files /dev/null and b/tests/lambdaTestBaseline/pixel_4/screenshot-landscape-EmulatorPixel4PortraitNativeWebScreenshot11-652x309.png differ diff --git a/tests/lambdaTestBaseline/pixel_4/screenshot-landscape-EmulatorPixel4PortraitNativeWebScreenshot12-760x360.png b/tests/lambdaTestBaseline/pixel_4/screenshot-landscape-EmulatorPixel4PortraitNativeWebScreenshot12-760x360.png new file mode 100644 index 000000000..622bd5c15 Binary files /dev/null and b/tests/lambdaTestBaseline/pixel_4/screenshot-landscape-EmulatorPixel4PortraitNativeWebScreenshot12-760x360.png differ diff --git a/tests/lambdaTestBaseline/pixel_4/screenshot-landscape-EmulatorPixel4PortraitNativeWebScreenshot13-760x360.png b/tests/lambdaTestBaseline/pixel_4/screenshot-landscape-EmulatorPixel4PortraitNativeWebScreenshot13-760x360.png new file mode 100644 index 000000000..874476c9b Binary files /dev/null and b/tests/lambdaTestBaseline/pixel_4/screenshot-landscape-EmulatorPixel4PortraitNativeWebScreenshot13-760x360.png differ diff --git a/tests/lambdaTestBaseline/pixel_4/screenshot-portrait-EmulatorPixel4LandscapeNativeWebScreenshot11-309x652.png b/tests/lambdaTestBaseline/pixel_4/screenshot-portrait-EmulatorPixel4LandscapeNativeWebScreenshot11-309x652.png new file mode 100644 index 000000000..0fe2d4c4b Binary files /dev/null and b/tests/lambdaTestBaseline/pixel_4/screenshot-portrait-EmulatorPixel4LandscapeNativeWebScreenshot11-309x652.png differ diff --git a/tests/lambdaTestBaseline/pixel_4/screenshot-portrait-EmulatorPixel4LandscapeNativeWebScreenshot12-360x760.png b/tests/lambdaTestBaseline/pixel_4/screenshot-portrait-EmulatorPixel4LandscapeNativeWebScreenshot12-360x760.png new file mode 100644 index 000000000..99b8e60b4 Binary files /dev/null and b/tests/lambdaTestBaseline/pixel_4/screenshot-portrait-EmulatorPixel4LandscapeNativeWebScreenshot12-360x760.png differ diff --git a/tests/lambdaTestBaseline/pixel_4/screenshot-portrait-EmulatorPixel4LandscapeNativeWebScreenshot13-360x760.png b/tests/lambdaTestBaseline/pixel_4/screenshot-portrait-EmulatorPixel4LandscapeNativeWebScreenshot13-360x760.png new file mode 100644 index 000000000..4db99a3f2 Binary files /dev/null and b/tests/lambdaTestBaseline/pixel_4/screenshot-portrait-EmulatorPixel4LandscapeNativeWebScreenshot13-360x760.png differ diff --git a/tests/lambdaTestBaseline/pixel_9_pro/screenshot-landscape-EmulatorPixel9ProPortraitNativeWebScreenshot14-952x427.png b/tests/lambdaTestBaseline/pixel_9_pro/screenshot-landscape-EmulatorPixel9ProPortraitNativeWebScreenshot14-952x427.png new file mode 100644 index 000000000..1de1f0d7a Binary files /dev/null and b/tests/lambdaTestBaseline/pixel_9_pro/screenshot-landscape-EmulatorPixel9ProPortraitNativeWebScreenshot14-952x427.png differ diff --git a/tests/lambdaTestBaseline/pixel_9_pro/screenshot-landscape-EmulatorPixel9ProPortraitNativeWebScreenshot15-952x427.png b/tests/lambdaTestBaseline/pixel_9_pro/screenshot-landscape-EmulatorPixel9ProPortraitNativeWebScreenshot15-952x427.png new file mode 100644 index 000000000..3a103fe24 Binary files /dev/null and b/tests/lambdaTestBaseline/pixel_9_pro/screenshot-landscape-EmulatorPixel9ProPortraitNativeWebScreenshot15-952x427.png differ diff --git a/tests/lambdaTestBaseline/pixel_9_pro/screenshot-portrait-EmulatorPixel9ProLandscapeNativeWebScreenshot14-427x952.png b/tests/lambdaTestBaseline/pixel_9_pro/screenshot-portrait-EmulatorPixel9ProLandscapeNativeWebScreenshot14-427x952.png new file mode 100644 index 000000000..f2acc63fe Binary files /dev/null and b/tests/lambdaTestBaseline/pixel_9_pro/screenshot-portrait-EmulatorPixel9ProLandscapeNativeWebScreenshot14-427x952.png differ diff --git a/tests/lambdaTestBaseline/pixel_9_pro/screenshot-portrait-EmulatorPixel9ProLandscapeNativeWebScreenshot15-427x952.png b/tests/lambdaTestBaseline/pixel_9_pro/screenshot-portrait-EmulatorPixel9ProLandscapeNativeWebScreenshot15-427x952.png new file mode 100644 index 000000000..f6e4875f3 Binary files /dev/null and b/tests/lambdaTestBaseline/pixel_9_pro/screenshot-portrait-EmulatorPixel9ProLandscapeNativeWebScreenshot15-427x952.png differ diff --git a/tests/sauceLabsBaseline/google_pixel_4_xl_googleapi_emulator/app-forms-app-EmulatorGooglePixel4XLGoogleAPIPortrait11.0-1440x3040.png b/tests/sauceLabsBaseline/google_pixel_4_xl_googleapi_emulator/app-forms-app-EmulatorGooglePixel4XLGoogleAPIPortrait11.0-1440x3040.png new file mode 100644 index 000000000..7c309293c Binary files /dev/null and b/tests/sauceLabsBaseline/google_pixel_4_xl_googleapi_emulator/app-forms-app-EmulatorGooglePixel4XLGoogleAPIPortrait11.0-1440x3040.png differ diff --git a/tests/sauceLabsBaseline/google_pixel_4_xl_googleapi_emulator/app-forms-app-EmulatorGooglePixel4XLGoogleAPIPortrait12.0-1440x3040.png b/tests/sauceLabsBaseline/google_pixel_4_xl_googleapi_emulator/app-forms-app-EmulatorGooglePixel4XLGoogleAPIPortrait12.0-1440x3040.png new file mode 100644 index 000000000..0b8740fcf Binary files /dev/null and b/tests/sauceLabsBaseline/google_pixel_4_xl_googleapi_emulator/app-forms-app-EmulatorGooglePixel4XLGoogleAPIPortrait12.0-1440x3040.png differ diff --git a/tests/sauceLabsBaseline/google_pixel_4_xl_googleapi_emulator/app-forms-app-EmulatorGooglePixel4XLGoogleAPIPortrait13.0-1440x3040.png b/tests/sauceLabsBaseline/google_pixel_4_xl_googleapi_emulator/app-forms-app-EmulatorGooglePixel4XLGoogleAPIPortrait13.0-1440x3040.png new file mode 100644 index 000000000..dbf199796 Binary files /dev/null and b/tests/sauceLabsBaseline/google_pixel_4_xl_googleapi_emulator/app-forms-app-EmulatorGooglePixel4XLGoogleAPIPortrait13.0-1440x3040.png differ diff --git a/tests/sauceLabsBaseline/google_pixel_4_xl_googleapi_emulator/app-forms-app-EmulatorGooglePixel4XLGoogleAPIPortrait14.0-1440x3040.png b/tests/sauceLabsBaseline/google_pixel_4_xl_googleapi_emulator/app-forms-app-EmulatorGooglePixel4XLGoogleAPIPortrait14.0-1440x3040.png new file mode 100644 index 000000000..3f234085f Binary files /dev/null and b/tests/sauceLabsBaseline/google_pixel_4_xl_googleapi_emulator/app-forms-app-EmulatorGooglePixel4XLGoogleAPIPortrait14.0-1440x3040.png differ diff --git a/tests/sauceLabsBaseline/google_pixel_4_xl_googleapi_emulator/app-login-button-app-EmulatorGooglePixel4XLGoogleAPIPortrait11.0-1440x3040.png b/tests/sauceLabsBaseline/google_pixel_4_xl_googleapi_emulator/app-login-button-app-EmulatorGooglePixel4XLGoogleAPIPortrait11.0-1440x3040.png new file mode 100644 index 000000000..9007254ee Binary files /dev/null and b/tests/sauceLabsBaseline/google_pixel_4_xl_googleapi_emulator/app-login-button-app-EmulatorGooglePixel4XLGoogleAPIPortrait11.0-1440x3040.png differ diff --git a/tests/sauceLabsBaseline/google_pixel_4_xl_googleapi_emulator/app-login-button-app-EmulatorGooglePixel4XLGoogleAPIPortrait12.0-1440x3040.png b/tests/sauceLabsBaseline/google_pixel_4_xl_googleapi_emulator/app-login-button-app-EmulatorGooglePixel4XLGoogleAPIPortrait12.0-1440x3040.png new file mode 100644 index 000000000..9007254ee Binary files /dev/null and b/tests/sauceLabsBaseline/google_pixel_4_xl_googleapi_emulator/app-login-button-app-EmulatorGooglePixel4XLGoogleAPIPortrait12.0-1440x3040.png differ diff --git a/tests/sauceLabsBaseline/google_pixel_4_xl_googleapi_emulator/app-login-button-resized-app-EmulatorGooglePixel4XLGoogleAPIPortrait11.0-1440x3040.png b/tests/sauceLabsBaseline/google_pixel_4_xl_googleapi_emulator/app-login-button-resized-app-EmulatorGooglePixel4XLGoogleAPIPortrait11.0-1440x3040.png new file mode 100644 index 000000000..c0f3275d7 Binary files /dev/null and b/tests/sauceLabsBaseline/google_pixel_4_xl_googleapi_emulator/app-login-button-resized-app-EmulatorGooglePixel4XLGoogleAPIPortrait11.0-1440x3040.png differ diff --git a/tests/sauceLabsBaseline/google_pixel_4_xl_googleapi_emulator/app-login-button-resized-app-EmulatorGooglePixel4XLGoogleAPIPortrait12.0-1440x3040.png b/tests/sauceLabsBaseline/google_pixel_4_xl_googleapi_emulator/app-login-button-resized-app-EmulatorGooglePixel4XLGoogleAPIPortrait12.0-1440x3040.png new file mode 100644 index 000000000..c0f3275d7 Binary files /dev/null and b/tests/sauceLabsBaseline/google_pixel_4_xl_googleapi_emulator/app-login-button-resized-app-EmulatorGooglePixel4XLGoogleAPIPortrait12.0-1440x3040.png differ diff --git a/tests/sauceLabsBaseline/google_pixel_4_xl_googleapi_emulator/web-app-app-EmulatorGooglePixel4XLGoogleAPIPortrait13.0-412x869.png b/tests/sauceLabsBaseline/google_pixel_4_xl_googleapi_emulator/web-app-app-EmulatorGooglePixel4XLGoogleAPIPortrait13.0-412x869.png new file mode 100644 index 000000000..bb4478407 Binary files /dev/null and b/tests/sauceLabsBaseline/google_pixel_4_xl_googleapi_emulator/web-app-app-EmulatorGooglePixel4XLGoogleAPIPortrait13.0-412x869.png differ diff --git a/tests/sauceLabsBaseline/google_pixel_4_xl_googleapi_emulator/web-app-app-EmulatorGooglePixel4XLGoogleAPIPortrait14.0-412x869.png b/tests/sauceLabsBaseline/google_pixel_4_xl_googleapi_emulator/web-app-app-EmulatorGooglePixel4XLGoogleAPIPortrait14.0-412x869.png new file mode 100644 index 000000000..454749a47 Binary files /dev/null and b/tests/sauceLabsBaseline/google_pixel_4_xl_googleapi_emulator/web-app-app-EmulatorGooglePixel4XLGoogleAPIPortrait14.0-412x869.png differ diff --git a/tests/sauceLabsBaseline/google_pixel_4_xl_googleapi_emulator/web-app-app-EmulatorGooglePixel4XLGoogleAPIPortrait15.0-412x869.png b/tests/sauceLabsBaseline/google_pixel_4_xl_googleapi_emulator/web-app-app-EmulatorGooglePixel4XLGoogleAPIPortrait15.0-412x869.png new file mode 100644 index 000000000..8066ffc2c Binary files /dev/null and b/tests/sauceLabsBaseline/google_pixel_4_xl_googleapi_emulator/web-app-app-EmulatorGooglePixel4XLGoogleAPIPortrait15.0-412x869.png differ diff --git a/tests/sauceLabsBaseline/iphone_11_simulator/web-app-app-Iphone11Portrait15-414x896.png b/tests/sauceLabsBaseline/iphone_11_simulator/web-app-app-Iphone11Portrait15-414x896.png new file mode 100644 index 000000000..cf3c6c218 Binary files /dev/null and b/tests/sauceLabsBaseline/iphone_11_simulator/web-app-app-Iphone11Portrait15-414x896.png differ diff --git a/tests/sauceLabsBaseline/iphone_12_pro_max_simulator/web-app-app-Iphone12ProMaxPortrait16-428x926.png b/tests/sauceLabsBaseline/iphone_12_pro_max_simulator/web-app-app-Iphone12ProMaxPortrait16-428x926.png new file mode 100644 index 000000000..25134bc99 Binary files /dev/null and b/tests/sauceLabsBaseline/iphone_12_pro_max_simulator/web-app-app-Iphone12ProMaxPortrait16-428x926.png differ diff --git a/tests/sauceLabsBaseline/iphone_12_simulator/web-app-app-Iphone12Portrait16-390x844.png b/tests/sauceLabsBaseline/iphone_12_simulator/web-app-app-Iphone12Portrait16-390x844.png new file mode 100644 index 000000000..b6001fdd6 Binary files /dev/null and b/tests/sauceLabsBaseline/iphone_12_simulator/web-app-app-Iphone12Portrait16-390x844.png differ diff --git a/tests/sauceLabsBaseline/iphone_8_plus_simulator/web-app-app-Iphone8PlusPortrait14-414x736.png b/tests/sauceLabsBaseline/iphone_8_plus_simulator/web-app-app-Iphone8PlusPortrait14-414x736.png new file mode 100644 index 000000000..36a5cbbe4 Binary files /dev/null and b/tests/sauceLabsBaseline/iphone_8_plus_simulator/web-app-app-Iphone8PlusPortrait14-414x736.png differ diff --git a/tests/sauceLabsBaseline/iphone_8_simulator/web-app-app-Iphone8Portrait14-375x667.png b/tests/sauceLabsBaseline/iphone_8_simulator/web-app-app-Iphone8Portrait14-375x667.png new file mode 100644 index 000000000..4ada6c029 Binary files /dev/null and b/tests/sauceLabsBaseline/iphone_8_simulator/web-app-app-Iphone8Portrait14-375x667.png differ diff --git a/tests/sauceLabsBaseline/iphone_xs_simulator/web-app-app-IphoneXsPortrait15-375x812.png b/tests/sauceLabsBaseline/iphone_xs_simulator/web-app-app-IphoneXsPortrait15-375x812.png new file mode 100644 index 000000000..d967f8375 Binary files /dev/null and b/tests/sauceLabsBaseline/iphone_xs_simulator/web-app-app-IphoneXsPortrait15-375x812.png differ diff --git a/tests/specs/__snapshots__/desktop.ocr.spec.ts.snap b/tests/specs/__snapshots__/desktop.ocr.spec.ts.snap deleted file mode 100644 index a677a01ed..000000000 --- a/tests/specs/__snapshots__/desktop.ocr.spec.ts.snap +++ /dev/null @@ -1,19 +0,0 @@ -// Snapshot v1 - -exports[`@wdio/visual-service:ocr desktop > should get the position of matching text on the screen based on OCR on local 1`] = ` -{ - "bottom": 111.3037037037037, - "left": 1193.9851851851852, - "right": 1255.7081481481482, - "top": 98.14962962962963, -} -`; - -exports[`@wdio/visual-service:ocr desktop > should get the position of matching text on the screen based on OCR on local 2`] = ` -{ - "bottom": 110, - "left": 1180, - "right": 1241, - "top": 97, -} -`; diff --git a/tests/specs/desktop.ocr.spec.ts b/tests/specs/desktop.ocr.spec.ts index b889bdfb5..a162a78f9 100644 --- a/tests/specs/desktop.ocr.spec.ts +++ b/tests/specs/desktop.ocr.spec.ts @@ -23,10 +23,22 @@ describe('@wdio/visual-service:ocr desktop', () => { text: string, }) const matchesFilePath = /ocr\/desktop-\d+\.png/.test(elementPosition.filePath) - expect(elementPosition.dprPosition).toMatchSnapshot() + // We don't care about the exact position now, just that it returns a position + expect(elementPosition.dprPosition).toEqual(expect.objectContaining({ + left: expect.any(Number), + top: expect.any(Number), + right: expect.any(Number), + bottom: expect.any(Number), + })) + + expect(elementPosition.originalPosition).toEqual(expect.objectContaining({ + left: expect.any(Number), + top: expect.any(Number), + right: expect.any(Number), + bottom: expect.any(Number), + })) expect(matchesFilePath) expect(elementPosition.matchedString).toEqual(string) - expect(elementPosition.originalPosition).toMatchSnapshot() expect(elementPosition.score).toEqual(100) expect(elementPosition.searchValue).toEqual(string) }) diff --git a/tests/specs/mobile.app.spec.ts b/tests/specs/mobile.app.spec.ts index 6895230b5..5006b6a5d 100644 --- a/tests/specs/mobile.app.spec.ts +++ b/tests/specs/mobile.app.spec.ts @@ -6,12 +6,10 @@ import { driver } from '@wdio/globals' describe('@wdio/visual-service mobile app', () => { // Get the commands that need to be executed // 0 means all, otherwise it will only execute the commands that are specified - // @ts-ignore const wdioIcsCommands = driver.requestedCapabilities['wdio-ics:options'].commands - // @ts-ignore const deviceName = driver.requestedCapabilities.deviceName - // @ts-ignore const orientation = driver.requestedCapabilities.orientation + const platformVersion = driver.requestedCapabilities.platformVersion beforeEach(async () => { await relaunchApp() @@ -41,6 +39,25 @@ describe('@wdio/visual-service mobile app', () => { // We're accepting 0.05%, which is 500 pixels, to be a max difference expect(result < 0.05 ? 0 : result).toEqual(0) }) + + if (driver.isIOS || (driver.isAndroid && parseFloat(platformVersion) >= 13)) { + it(`should compare a webview screenshot successful for '${deviceName}' in ${orientation}-mode`, async () => { + await $('~Webview').click() + await driver.pause(2000) + await driver.switchContext({ title: /.*WebdriverIO.*/ }) + await driver.pause(2000) + await driver.url('https://guinea-pig.webdriver.io/image-compare.html') + const result = await browser.checkScreen('web-app', { + ignore: [$('.navbar__brand')], + hideElements: [$('.hero__title')] + } + ) as number + // Rest the context because the rest will be for native + await driver.switchContext('NATIVE_APP') + + await expect(result < 0.05 ? 0 : result).toEqual(0) + }) + } } if ( diff --git a/tests/specs/mobile.web.spec.ts b/tests/specs/mobile.web.spec.ts index ffafe81b7..fe3829578 100644 --- a/tests/specs/mobile.web.spec.ts +++ b/tests/specs/mobile.web.spec.ts @@ -38,6 +38,19 @@ describe('@wdio/visual-service mobile web', () => { console.log(`\n\n\n'Screenshot for ${deviceName}' with ${platformName}:${platformVersion} in ${orientation}-mode has a difference of ${result}%\n\n\n`) } await expect(result < 0.05 ? 0 : result).toEqual(0) + + const newOrientation = orientation.toUpperCase() === 'LANDSCAPE' ? 'PORTRAIT' : 'LANDSCAPE' + + await browser.pause(2000) + await browser.setOrientation(newOrientation) + await browser.pause(2000) + const newResult = await browser.checkScreen(`screenshot-${newOrientation.toLowerCase()}`) as number + if (newResult > 0 && result < 0.05) { + console.log(`\n\n\n'Screenshot for ${deviceName}' with ${platformName}:${platformVersion} in new orientation mode ${newOrientation} has a difference of ${result}%\n\n\n`) + } + // Before the expect we need to revert the orientation otherwise the next test will not start in the default orientation + await browser.setOrientation(orientation) + await expect(newResult < 0.05 ? 0 : newResult).toEqual(0) }) }