diff --git a/.github/workflows/appium_Android.yml b/.github/workflows/appium_Android.yml index 58cd61fce..8a1a30000 100644 --- a/.github/workflows/appium_Android.yml +++ b/.github/workflows/appium_Android.yml @@ -4,6 +4,7 @@ on: push: branches: - 3.x + - 5228-fix-appium-tests env: CI: true diff --git a/lib/helper/Appium.js b/lib/helper/Appium.js index a092e1b31..b62fdda49 100644 --- a/lib/helper/Appium.js +++ b/lib/helper/Appium.js @@ -391,6 +391,29 @@ class Appium extends Webdriver { return `${protocol}://${hostname}:${port}${normalizedPath}/session/${this.browser.sessionId}` } + /** + * Helper method to safely call isDisplayed() on mobile elements. + * Handles the case where webdriverio tries to use execute/sync which isn't supported in Appium. + * @private + */ + async _isDisplayedSafe(element) { + if (this.isWeb) { + // For web contexts, use the normal isDisplayed + return element.isDisplayed() + } + + try { + return await element.isDisplayed() + } catch (err) { + // If isDisplayed fails due to execute/sync not being supported in native mobile contexts, + // fall back to assuming the element is displayed (since we found it) + if (err.message && err.message.includes('Method is not implemented')) { + return true + } + throw err + } + } + /** * Execute code only on iOS * @@ -619,6 +642,7 @@ class Appium extends Webdriver { */ async resetApp() { onlyForApps.call(this) + this.isWeb = false // Reset to native context after app reset return this.axios({ method: 'post', url: `${this._buildAppiumEndpoint()}/appium/app/reset`, @@ -1134,7 +1158,7 @@ class Appium extends Webdriver { ], }, ]) - await this.browser.pause(1000) + await this.browser.pause(2000) } /** @@ -1296,28 +1320,26 @@ class Appium extends Webdriver { let currentSource return browser .waitUntil( - () => { + async () => { if (err) { return new Error(`Scroll to the end and element ${searchableLocator} was not found`) } - return browser - .$$(parseLocator.call(this, searchableLocator)) - .then(els => els.length && els[0].isDisplayed()) - .then(res => { - if (res) { - return true - } - return this[direction](scrollLocator, offset, speed) - .getSource() - .then(source => { - if (source === currentSource) { - err = true - } else { - currentSource = source - return false - } - }) - }) + const els = await browser.$$(parseLocator.call(this, searchableLocator)) + if (els.length) { + const displayed = await this._isDisplayedSafe(els[0]) + if (displayed) { + return true + } + } + + await this[direction](scrollLocator, offset, speed) + const source = await this.browser.getPageSource() + if (source === currentSource) { + err = true + } else { + currentSource = source + return false + } }, timeout * 1000, errorMsg, @@ -1523,7 +1545,28 @@ class Appium extends Webdriver { */ async dontSeeElement(locator) { if (this.isWeb) return super.dontSeeElement(locator) - return super.dontSeeElement(parseLocator.call(this, locator)) + + // For mobile native apps, use safe isDisplayed wrapper + const parsedLocator = parseLocator.call(this, locator) + const res = await this._locate(parsedLocator, false) + const { truth } = require('../assert/truth') + const Locator = require('../locator') + + if (!res || res.length === 0) { + return truth(`elements of ${new Locator(parsedLocator)}`, 'to be seen').negate(false) + } + + const selected = [] + for (const el of res) { + const displayed = await this._isDisplayedSafe(el) + if (displayed) selected.push(true) + } + + try { + return truth(`elements of ${new Locator(parsedLocator)}`, 'to be seen').negate(selected) + } catch (err) { + throw err + } } /** @@ -1577,7 +1620,18 @@ class Appium extends Webdriver { */ async grabNumberOfVisibleElements(locator) { if (this.isWeb) return super.grabNumberOfVisibleElements(locator) - return super.grabNumberOfVisibleElements(parseLocator.call(this, locator)) + + // For mobile native apps, use safe isDisplayed wrapper + const parsedLocator = parseLocator.call(this, locator) + const res = await this._locate(parsedLocator) + + const selected = [] + for (const el of res) { + const displayed = await this._isDisplayedSafe(el) + if (displayed) selected.push(true) + } + + return selected.length } /** @@ -1656,7 +1710,30 @@ class Appium extends Webdriver { */ async seeElement(locator) { if (this.isWeb) return super.seeElement(locator) - return super.seeElement(parseLocator.call(this, locator)) + + // For mobile native apps, use safe isDisplayed wrapper + const parsedLocator = parseLocator.call(this, locator) + const res = await this._locate(parsedLocator, true) + const ElementNotFound = require('./errors/ElementNotFound') + const { truth } = require('../assert/truth') + const { dontSeeElementError } = require('./errors/ElementAssertion') + const Locator = require('../locator') + + if (!res || res.length === 0) { + throw new ElementNotFound(parsedLocator) + } + + const selected = [] + for (const el of res) { + const displayed = await this._isDisplayedSafe(el) + if (displayed) selected.push(true) + } + + try { + return truth(`elements of ${new Locator(parsedLocator)}`, 'to be seen').assert(selected) + } catch (e) { + dontSeeElementError(parsedLocator) + } } /** @@ -1703,7 +1780,30 @@ class Appium extends Webdriver { */ async waitForVisible(locator, sec = null) { if (this.isWeb) return super.waitForVisible(locator, sec) - return super.waitForVisible(parseLocator.call(this, locator), sec) + + // For mobile native apps, use safe isDisplayed wrapper + const parsedLocator = parseLocator.call(this, locator) + const aSec = sec || this.options.waitForTimeoutInSeconds + const Locator = require('../locator') + + return this.browser.waitUntil( + async () => { + const res = await this._res(parsedLocator) + if (!res || res.length === 0) return false + + const selected = [] + for (const el of res) { + const displayed = await this._isDisplayedSafe(el) + if (displayed) selected.push(true) + } + + return selected.length > 0 + }, + { + timeout: aSec * 1000, + timeoutMsg: `element (${new Locator(parsedLocator)}) still not visible after ${aSec} sec`, + }, + ) } /** @@ -1712,7 +1812,27 @@ class Appium extends Webdriver { */ async waitForInvisible(locator, sec = null) { if (this.isWeb) return super.waitForInvisible(locator, sec) - return super.waitForInvisible(parseLocator.call(this, locator), sec) + + // For mobile native apps, use safe isDisplayed wrapper + const parsedLocator = parseLocator.call(this, locator) + const aSec = sec || this.options.waitForTimeoutInSeconds + const Locator = require('../locator') + + return this.browser.waitUntil( + async () => { + const res = await this._res(parsedLocator) + if (!res || res.length === 0) return true + + const selected = [] + for (const el of res) { + const displayed = await this._isDisplayedSafe(el) + if (displayed) selected.push(true) + } + + return selected.length === 0 + }, + { timeout: aSec * 1000, timeoutMsg: `element (${new Locator(parsedLocator)}) still visible after ${aSec} sec` }, + ) } /** diff --git a/test/helper/Appium_test.js b/test/helper/Appium_test.js index 5d7f47f6d..6266e8127 100644 --- a/test/helper/Appium_test.js +++ b/test/helper/Appium_test.js @@ -31,6 +31,7 @@ describe('Appium', function () { platformName: 'Android', platformVersion: '7.0', deviceName: 'Android GoogleAPI Emulator', + automationName: 'UiAutomator2', androidInstallTimeout: 90000, appWaitDuration: 300000, }, @@ -182,6 +183,7 @@ describe('Appium', function () { it('should switch to native and web contexts @quick', async () => { await app.resetApp() await app.tap('~buttonStartWebviewCD') + await app.browser.pause(1000) await app.see('WebView location') await app.switchToWeb() let val = await app.grabContext() @@ -189,6 +191,7 @@ describe('Appium', function () { await app.see('Prefered Car') assert.ok(app.isWeb) await app.switchToNative() + await app.browser.pause(2000) val = await app.grabContext() assert.equal(val, 'NATIVE_APP') return assert.ok(!app.isWeb) @@ -292,7 +295,7 @@ describe('Appium', function () { await app.resetApp() await app.tap("//android.widget.Button[@resource-id = 'io.selendroid.testapp:id/touchTest']") await app.waitForText('Gesture Type', 10, "//android.widget.TextView[@resource-id = 'io.selendroid.testapp:id/gesture_type_text_view']") - await app.swipeDown("//android.widget.LinearLayout[@resource-id = 'io.selendroid.testapp:id/LinearLayout1']", 120, 100) + await app.swipeDown("//android.widget.LinearLayout[@resource-id = 'io.selendroid.testapp:id/LinearLayout1']", 1200, 1000) const type = await app.grabTextFrom("//android.widget.TextView[@resource-id = 'io.selendroid.testapp:id/gesture_type_text_view']") assert.equal(type, 'FLICK') }) @@ -362,9 +365,12 @@ describe('Appium', function () { describe('#performTouchAction', () => { it('should react on swipeUp action @second', async () => { + await app.resetApp() await app.tap("//android.widget.Button[@resource-id = 'io.selendroid.testapp:id/touchTest']") await app.waitForText('Gesture Type', 10, "//android.widget.TextView[@resource-id = 'io.selendroid.testapp:id/gesture_type_text_view']") - await app.swipeUp("//android.widget.TextView[@resource-id = 'io.selendroid.testapp:id/gesture_type_text_view']") + await app.swipeUp("//android.widget.LinearLayout[@resource-id = 'io.selendroid.testapp:id/LinearLayout1']", 1200, 1000) + await app.browser.pause(3000) + await app.waitForElement("//android.widget.TextView[@resource-id = 'io.selendroid.testapp:id/text_view4']", 20) const type = await app.grabTextFrom("//android.widget.TextView[@resource-id = 'io.selendroid.testapp:id/gesture_type_text_view']") const vy = await app.grabTextFrom("//android.widget.TextView[@resource-id = 'io.selendroid.testapp:id/text_view4']") assert.equal(type, 'FLICK') @@ -375,7 +381,9 @@ describe('Appium', function () { await app.resetApp() await app.tap("//android.widget.Button[@resource-id = 'io.selendroid.testapp:id/touchTest']") await app.waitForText('Gesture Type', 10, "//android.widget.TextView[@resource-id = 'io.selendroid.testapp:id/gesture_type_text_view']") - await app.swipeUp("//android.widget.TextView[@resource-id = 'io.selendroid.testapp:id/gesture_type_text_view']") + await app.swipeDown("//android.widget.LinearLayout[@resource-id = 'io.selendroid.testapp:id/LinearLayout1']", 1200, 1000) + await app.browser.pause(3000) + await app.waitForElement("//android.widget.TextView[@resource-id = 'io.selendroid.testapp:id/text_view4']", 20) const type = await app.grabTextFrom("//android.widget.TextView[@resource-id = 'io.selendroid.testapp:id/gesture_type_text_view']") const vy = await app.grabTextFrom("//android.widget.TextView[@resource-id = 'io.selendroid.testapp:id/text_view4']") assert.equal(type, 'FLICK') @@ -383,9 +391,11 @@ describe('Appium', function () { }) it('should react on swipeLeft action', async () => { + await app.resetApp() await app.tap("//android.widget.Button[@resource-id = 'io.selendroid.testapp:id/touchTest']") await app.waitForText('Gesture Type', 10, "//android.widget.TextView[@resource-id = 'io.selendroid.testapp:id/gesture_type_text_view']") await app.swipeLeft("//android.widget.TextView[@resource-id = 'io.selendroid.testapp:id/gesture_type_text_view']") + await app.waitForElement("//android.widget.TextView[@resource-id = 'io.selendroid.testapp:id/text_view4']", 5) const type = await app.grabTextFrom("//android.widget.TextView[@resource-id = 'io.selendroid.testapp:id/gesture_type_text_view']") const vy = await app.grabTextFrom("//android.widget.TextView[@resource-id = 'io.selendroid.testapp:id/text_view4']") assert.equal(type, 'FLICK') @@ -393,9 +403,11 @@ describe('Appium', function () { }) it('should react on swipeRight action', async () => { + await app.resetApp() await app.tap("//android.widget.Button[@resource-id = 'io.selendroid.testapp:id/touchTest']") await app.waitForText('Gesture Type', 10, "//android.widget.TextView[@resource-id = 'io.selendroid.testapp:id/gesture_type_text_view']") await app.swipeRight("//android.widget.TextView[@resource-id = 'io.selendroid.testapp:id/gesture_type_text_view']") + await app.waitForElement("//android.widget.TextView[@resource-id = 'io.selendroid.testapp:id/text_view4']", 5) const type = await app.grabTextFrom("//android.widget.TextView[@resource-id = 'io.selendroid.testapp:id/gesture_type_text_view']") const vy = await app.grabTextFrom("//android.widget.TextView[@resource-id = 'io.selendroid.testapp:id/text_view4']") assert.equal(type, 'FLICK') @@ -557,7 +569,7 @@ describe('Appium', function () { await app.tap('~email of the customer') await app.appendField('//android.widget.EditText[@content-desc="email of the customer"]', '1') await app.hideDeviceKeyboard('pressKey', 'Done') - await app.swipeTo('//android.widget.Button', '//android.widget.ScrollView/android.widget.LinearLayout', 'up', 30, 100, 700) + await app.swipeTo('//android.widget.Button', '//android.widget.ScrollView/android.widget.LinearLayout', 'up', 60, 100, 700) await app.tap('//android.widget.Button') await app.see('1', '//android.widget.TextView[@resource-id="io.selendroid.testapp:id/label_email_data"]') const id = await app.grabNumberOfVisibleElements('//android.widget.TextView[@resource-id="io.selendroid.testapp:id/label_email_data"]', 'contentDescription')