fix: support windows/mac apps for isDisplayed (#14287)

* fix: support windows/mac apps for isDisplayed

- add 2 new session flags for windows and mac apps
- add UT's
- add Docs

* chore: forgot to push the docs
This commit is contained in:
Wim Selles 2025-03-20 08:00:44 +01:00 committed by GitHub
parent 3021c043ee
commit aa49a4707d
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
12 changed files with 162 additions and 14 deletions

View file

@ -108,6 +108,26 @@ const requestMock: any = vi.fn().mockImplementation((uri, params) => {
delete sessionResponse.capabilities.browserName
}
if (
body &&
body.capabilities &&
body.capabilities.alwaysMatch.mobileMode &&
body.capabilities.alwaysMatch.windowsAppMode
) {
sessionResponse.capabilities['appium:automationName'] = 'windows'
delete sessionResponse.capabilities.browserName
}
if (
body &&
body.capabilities &&
body.capabilities.alwaysMatch.mobileMode &&
body.capabilities.alwaysMatch.macAppMode
) {
sessionResponse.capabilities['appium:automationName'] = 'mac2'
delete sessionResponse.capabilities.browserName
}
if (
body &&
body.capabilities &&

View file

@ -95,11 +95,21 @@ function isFirefox(capabilities?: WebdriverIO.Capabilities) {
)
}
/**
* get the automation name value of the session
*
* @param {Object} capabilities capabilities
* @return {Boolean} true if platform is mobile device
*/
function getAutomationName(capabilities: WebdriverIO.Capabilities) {
return capabilities['appium:options']?.automationName || capabilities['appium:automationName']
}
/**
* check if current platform is mobile device
*
* @param {Object} caps capabilities
* @return {Boolean} true if platform is mobile device
* @param {Object} capabilities capabilities
* @return {Boolean} true if platform is mobile device
*/
function isMobile(capabilities: WebdriverIO.Capabilities) {
const browserName = (capabilities.browserName || '').toLowerCase()
@ -114,7 +124,7 @@ function isMobile(capabilities: WebdriverIO.Capabilities) {
* Return `isMobile:false` for those cases. There we also accepts that specific mobile browser
* tests (like the FF one on Android) are not seen as a mobile one
*/
const automationName = capabilities['appium:options']?.automationName || capabilities['appium:automationName']
const automationName = getAutomationName(capabilities)
if (automationName && ['gecko', 'safari', 'chrome', 'chromium'].includes(automationName.toLocaleLowerCase())) {
return false
}
@ -190,6 +200,44 @@ function isAndroid(capabilities?: WebdriverIO.Capabilities) {
)
}
/**
* Check if session uses a specific automation name
* @param {Object} capabilities caps of session response
* @param {String} platform platform to check for (e.g., 'windows', 'mac2')
* @return {Boolean} true if run for specified platform
*/
function matchesAppAutomationName(automationNameValue: string, capabilities?: WebdriverIO.Capabilities): boolean {
if (!capabilities) {
return false
}
const automationName = getAutomationName(capabilities)
if (!automationName) {
return false
}
return Boolean(automationName.match(new RegExp(automationNameValue, 'i')))
}
/**
* Check if session is run for Windows apps
* @param {Object} capabilities caps of session response
* @return {Boolean} true if run for Windows Apps
*/
function isWindowsApp(capabilities?: WebdriverIO.Capabilities): boolean {
return matchesAppAutomationName('windows', capabilities)
}
/**
* Check if session is run for Mac apps
* @param {Object} capabilities caps of session response
* @return {Boolean} true if run for Mac Apps
*/
function isMacApp(capabilities?: WebdriverIO.Capabilities): boolean {
return matchesAppAutomationName('mac2', capabilities)
}
/**
* detects if session is run on Sauce with extended debugging enabled
* @param {object} capabilities session capabilities
@ -270,7 +318,9 @@ export function capabilitiesEnvironmentDetector(capabilities: WebdriverIO.Capabi
isAndroid: isAndroid(capabilities),
isSauce: isSauce(capabilities),
isBidi: isBidi(capabilities),
isChromium: isChromium(capabilities)
isChromium: isChromium(capabilities),
isWindowsApp: isWindowsApp(capabilities),
isMacApp: isMacApp(capabilities)
}
}
@ -297,6 +347,8 @@ export function sessionEnvironmentDetector({
isSauce: isSauce(requestedCapabilities),
isSeleniumStandalone: isSeleniumStandalone(capabilities),
isBidi: isBidi(capabilities),
isChromium: isChromium(capabilities)
isChromium: isChromium(capabilities),
isWindowsApp: isWindowsApp(capabilities),
isMacApp: isMacApp(capabilities)
}
}

View file

@ -266,14 +266,30 @@ describe('sessionEnvironmentDetector', () => {
platformName: 'WINDOWS',
'ms:experimental-webdriver': true,
'ms:waitForAppLaunch': 10,
app: 'C:\\Program Files\\foo\\bar.exe',
appArguments: '-noCloseConfirmationPopUp -shouldDisplayDiesToTake'
'appium:app': 'C:\\Program Files\\foo\\bar.exe',
'appium:appArguments': '-noCloseConfirmationPopUp -shouldDisplayDiesToTake',
'appium:automationName': 'Windows'
}
const requestedCapabilities = { browserName: '' }
const { isMobile, isIOS, isAndroid } = sessionEnvironmentDetector({ capabilities, requestedCapabilities })
const { isMobile, isIOS, isAndroid, isWindowsApp } = sessionEnvironmentDetector({ capabilities, requestedCapabilities })
expect(isMobile).toEqual(true)
expect(isIOS).toEqual(false)
expect(isAndroid).toEqual(false)
expect(isWindowsApp).toEqual(true)
})
it('should detect a Mac application automated through Appium', () => {
const capabilities: any = {
platformName: 'mac',
'appium:appPath': '/Applications/MyAppName.app',
'appium:automationName': 'mac2'
}
const requestedCapabilities = { browserName: '' }
const { isMobile, isIOS, isAndroid, isMacApp } = sessionEnvironmentDetector({ capabilities, requestedCapabilities })
expect(isMobile).toEqual(true)
expect(isIOS).toEqual(false)
expect(isAndroid).toEqual(false)
expect(isMacApp).toEqual(true)
})
it('should detect Android mobile app', function () {

View file

@ -27,6 +27,8 @@ export interface SessionFlags {
isSauce: boolean
isSeleniumStandalone: boolean
isBidi: boolean
isWindowsApp: boolean
isMacApp: boolean
}
type Fn = (...args: unknown[]) => unknown

View file

@ -278,7 +278,7 @@ export function getPrototype ({ isW3C, isChromium, isFirefox, isMobile, isSauce,
* @param {Object} options driver instance or option object containing these flags
* @return {Object} prototype object
*/
export function getEnvironmentVars({ isW3C, isMobile, isIOS, isAndroid, isFirefox, isSauce, isSeleniumStandalone, isChromium }: Partial<SessionFlags>): PropertyDescriptorMap {
export function getEnvironmentVars({ isW3C, isMobile, isIOS, isAndroid, isFirefox, isSauce, isSeleniumStandalone, isChromium, isWindowsApp, isMacApp }: Partial<SessionFlags>): PropertyDescriptorMap {
return {
isW3C: { value: isW3C },
isMobile: { value: isMobile },
@ -298,6 +298,8 @@ export function getEnvironmentVars({ isW3C, isMobile, isIOS, isAndroid, isFirefo
}
},
isChromium: { value: isChromium },
isWindowsApp: { value: isWindowsApp },
isMacApp: { value: isMacApp }
}
}

View file

@ -85,6 +85,8 @@ class FakeClient extends EventEmitter {
isSauce = false
isFirefox = false
isBidi = false
isWindowsApp = false
isMacApp = false
isSeleniumStandalone = false
isNativeContext = false
mobileContext = ''

View file

@ -245,7 +245,9 @@ describe('WebDriver', () => {
isIOS: false,
isAndroid: false,
isChromium: false,
isSauce: false
isSauce: false,
isWindowsApp: false,
isMacApp: false
})
expect(client.isW3C).toBe(false)
expect(client.isMobile).toBe(false)
@ -253,6 +255,8 @@ describe('WebDriver', () => {
expect(client.isAndroid).toBe(false)
expect(client.isChromium).toBe(false)
expect(client.isSauce).toBe(false)
expect(client.isWindowsApp).toBe(false)
expect(client.isMacApp).toBe(false)
const anotherClient = WebDriver.attachToSession({ ...sessionOptions,
isW3C: true,
@ -260,7 +264,9 @@ describe('WebDriver', () => {
isIOS: true,
isAndroid: true,
isChromium: true,
isSauce: true
isSauce: true,
isWindowsApp: true,
isMacApp: true
})
expect(anotherClient.isW3C).toBe(true)
expect(anotherClient.isMobile).toBe(true)
@ -268,6 +274,8 @@ describe('WebDriver', () => {
expect(anotherClient.isAndroid).toBe(true)
expect(anotherClient.isChromium).toBe(true)
expect(anotherClient.isSauce).toBe(true)
expect(anotherClient.isWindowsApp).toBe(true)
expect(anotherClient.isMacApp).toBe(true)
})
it('should apply default connection details', () => {

View file

@ -110,7 +110,7 @@ export async function isDisplayed (
* For mobile sessions with Appium we continue to use the elementDisplayed command
* as we can't run JS in native apps
*/
if (browser.isMobile && browser.isNativeContext) {
if (browser.isMobile && (browser.isNativeContext || browser.isWindowsApp || browser.isMacApp)) {
/**
* there is no support yet for checking if an element is displayed within the
* viewport for native apps. We can only check if it's displayed at all.

View file

@ -108,6 +108,46 @@ describe('isDisplayed test', () => {
.toBe('/session/foobar-123/element/some-elem-123/displayed')
})
it('should allow to check if element is displayed in mobile mode for Windows Apps', async () => {
browser = await remote({
baseUrl: 'http://foobar.com',
capabilities: {
// @ts-ignore mock feature
keepBrowserName: true,
mobileMode: true,
windowsAppMode: true,
} as any
})
elem = await browser.$('#foo')
vi.mocked(fetch).mockClear()
expect(await elem.isDisplayed()).toBe(true)
// Due to mobileMode being enabled we will have extra calls to fetch
expect(fetch).toBeCalledTimes(1)
// @ts-expect-error mock implementation
expect(vi.mocked(fetch).mock.calls[0][0]!.pathname)
.toBe('/session/foobar-123/element/some-elem-123/displayed')
})
it('should allow to check if element is displayed in mobile mode for Mac Apps', async () => {
browser = await remote({
baseUrl: 'http://foobar.com',
capabilities: {
// @ts-ignore mock feature
keepBrowserName: true,
mobileMode: true,
macAppMode: true,
} as any
})
elem = await browser.$('#foo')
vi.mocked(fetch).mockClear()
expect(await elem.isDisplayed()).toBe(true)
// Due to mobileMode being enabled we will have extra calls to fetch
expect(fetch).toBeCalledTimes(1)
// @ts-expect-error mock implementation
expect(vi.mocked(fetch).mock.calls[0][0]!.pathname)
.toBe('/session/foobar-123/element/some-elem-123/displayed')
})
it('should throw if displayed check within viewport is done for native mobile apps', async () => {
browser = await remote({
baseUrl: 'http://foobar.com',

View file

@ -166,7 +166,9 @@ describe('WebdriverIO module interface', () => {
isIOS: false,
isMobile: false,
isSauce: false,
isBidi: false
isBidi: false,
isWindowsApp: false,
isMacApp: false,
})
})
})

View file

@ -19,7 +19,7 @@ describe('newSession', () => {
foo: 'bar'
}
})
expect(Object.keys(session)).toHaveLength(19)
expect(Object.keys(session)).toHaveLength(21)
expect(session.isAndroid).toBe(false)
expect(session.isChrome).toBe(false)
expect(session.isChromium).toBe(false)
@ -28,6 +28,8 @@ describe('newSession', () => {
expect(session.isMobile).toBe(true)
expect(session.isSauce).toBe(false)
expect(session.isBidi).toBe(false)
expect(session.isWindowsApp).toBe(false)
expect(session.isMacApp).toBe(false)
expect(session.capabilities).toEqual({
deviceName: 'Some Device',
platformName: 'iOS',

View file

@ -25,6 +25,8 @@ A browser object has the following properties:
| `isFirefox` | `Boolean` | Indicates if this Firefox instance |
| `isBidi` | `Boolean` | Indicates if this session uses Bidi |
| `isSauce` | `Boolean` | Indicates if this session is Running on Sauce Labs |
| `isMacApp` | `Boolean` | Indicates if this session is Running for a native Mac App |
| `isWindowsApp` | `Boolean` | Indicates if this session is Running for a native Windows App |
| `isMobile` | `Boolean` | Indicates a mobile session. See more under [Mobile Flags](#mobile-flags). |
| `isIOS` | `Boolean` | Indicates an iOS session. See more under [Mobile Flags](#mobile-flags). |
| `isAndroid` | `Boolean` | Indicates an Android session. See more under [Mobile Flags](#mobile-flags). |