From 65b20554a4dfc922b0b518fa31e315a5445a6a4c Mon Sep 17 00:00:00 2001 From: Priyansh Garg <39924567+garg3133@users.noreply.github.com> Date: Fri, 10 Feb 2023 02:01:47 +0530 Subject: [PATCH] Fix visible commands for Appium. (#3566) --- lib/core/client.js | 18 +++- lib/element/appium-locator.js | 31 +++++++ lib/element/locator.js | 32 +++++-- .../selenium-webdriver/method-mappings.js | 17 +++- test/lib/nocks.js | 25 ++++++ .../src/api/commands/element/testIsVisible.js | 84 ++++++++++++++++++- .../element/testWaitForElementVisible.js | 42 ++++++++++ test/src/api/expect/testExpectVisible.js | 19 +++++ test/src/core/testCreateSession.js | 8 ++ test/src/index/transport/testMobileOptions.js | 13 ++- 10 files changed, 274 insertions(+), 15 deletions(-) create mode 100644 lib/element/appium-locator.js diff --git a/lib/core/client.js b/lib/core/client.js index 86efaa2c57..4e59c3e22d 100644 --- a/lib/core/client.js +++ b/lib/core/client.js @@ -10,6 +10,7 @@ const Transport = require('../transport'); const Element = require('../element'); const ApiLoader = require('../api'); const ElementGlobal = require('../api/_loaders/element-global.js'); +const Factory = require('../transport/factory.js'); const {isAndroid, isIos} = require('../utils/mobile'); const {LocateStrategy, Locator} = Element; @@ -76,7 +77,7 @@ class NightwatchAPI { return false; } - return this.platformName.toLowerCase() === platform.toLowerCase(); + return this.platformName.toLowerCase() === platform.toLowerCase(); } isIOS() { @@ -114,6 +115,21 @@ class NightwatchAPI { isOpera() { return this.capabilities.browserName === Browser.OPERA; } + + isAppiumClient() { + if (this.options.selenium && this.options.selenium.use_appium) { + return true; + } + + // Handle BrowserStack case + // (BrowserStack always returns platformName in capabilities) + const isMobile = this.__isPlatformName('android') || this.__isPlatformName('ios'); + if (Factory.usingBrowserstack(this.options) && isMobile) { + return true; + } + + return false; + } } class NightwatchClient extends EventEmitter { diff --git a/lib/element/appium-locator.js b/lib/element/appium-locator.js new file mode 100644 index 0000000000..6caf9e06f6 --- /dev/null +++ b/lib/element/appium-locator.js @@ -0,0 +1,31 @@ +const {By, RelativeBy} = require('selenium-webdriver'); + +const LocateElement = require('./locator.js'); +const {AVAILABLE_LOCATORS} = LocateElement; + +class AppiumLocator extends LocateElement { + /** + * @param {object|string} element + * @return {By|RelativeBy} + */ + static create(element) { + if (!element) { + throw new Error(`Error while trying to locate element: missing element definition; got: "${element}".`); + } + + const byInstance = LocateElement.locateInstanceOfBy(element); + if (byInstance !== null) { + return byInstance; + } + + const elementInstance = LocateElement.createElementInstance(element); + + if (elementInstance.locateStrategy === 'id') { + return new By(elementInstance.locateStrategy, elementInstance.selector); + } + + return By[AVAILABLE_LOCATORS[elementInstance.locateStrategy]](elementInstance.selector); + } +} + +module.exports = AppiumLocator; diff --git a/lib/element/locator.js b/lib/element/locator.js index 600105ea29..bd5dd56a65 100644 --- a/lib/element/locator.js +++ b/lib/element/locator.js @@ -3,7 +3,7 @@ const Element = require('./index.js'); const ElementsByRecursion = require('./locate/elements-by-recursion.js'); const SingleElementByRecursion = require('./locate/single-element-by-recursion.js'); -const availableLocators = { +const AVAILABLE_LOCATORS = { 'css selector': 'css', 'id': 'id', 'link text': 'linkText', @@ -17,13 +17,28 @@ const availableLocators = { class LocateElement { /** * @param {object|string} element - * @return {*} + * @return {By|RelativeBy} */ static create(element) { if (!element) { throw new Error(`Error while trying to locate element: missing element definition; got: "${element}".`); } + const byInstance = LocateElement.locateInstanceOfBy(element); + if (byInstance !== null) { + return byInstance; + } + + const elementInstance = LocateElement.createElementInstance(element); + + return By[AVAILABLE_LOCATORS[elementInstance.locateStrategy]](elementInstance.selector); + } + + /** + * @param {object} element + * @return {By|RelativeBy|null} + */ + static locateInstanceOfBy(element) { if (element instanceof By) { return element; } @@ -36,6 +51,14 @@ class LocateElement { return element.value; } + return null; + } + + /** + * @param {object|string} element + * @return {Element} + */ + static createElementInstance(element) { if (typeof element != 'object' && typeof element != 'string') { throw new Error(`Invalid element definition type; expected string or object, but got: ${typeof element}.`); } @@ -50,9 +73,7 @@ class LocateElement { selector = element; } - const elementInstance = Element.createFromSelector(selector, strategy); - - return By[availableLocators[elementInstance.locateStrategy]](elementInstance.selector); + return Element.createFromSelector(selector, strategy); } get api() { @@ -316,3 +337,4 @@ class NoSuchElementError extends Error { module.exports = LocateElement; module.exports.NoSuchElementError = NoSuchElementError; +module.exports.AVAILABLE_LOCATORS = AVAILABLE_LOCATORS; diff --git a/lib/transport/selenium-webdriver/method-mappings.js b/lib/transport/selenium-webdriver/method-mappings.js index 961ab1e1f1..3a01e22ac8 100644 --- a/lib/transport/selenium-webdriver/method-mappings.js +++ b/lib/transport/selenium-webdriver/method-mappings.js @@ -1,5 +1,6 @@ const {WebElement, WebDriver, Origin, By} = require('selenium-webdriver'); const {Locator} = require('../../element'); +const AppiumLocator = require('../../element/appium-locator.js'); const {isString} = require('../../utils'); const fs = require('fs'); const cdp = require('./cdp.js'); @@ -367,7 +368,12 @@ module.exports = class MethodMappings { }, async locateMultipleElements(element) { - const locator = Locator.create(element); + let locator; + if (this.transport.api.isAppiumClient()) { + locator = AppiumLocator.create(element); + } else { + locator = Locator.create(element); + } const resultValue = await this.driver.findElements(locator); if (Array.isArray(resultValue) && resultValue.length === 0) { @@ -531,11 +537,14 @@ module.exports = class MethodMappings { return `/element/${id}/location_in_view`; }, - async isElementDisplayed(webElementOrId) { + isElementDisplayed(webElementOrId) { + if (this.transport.api.isAppiumClient()) { + return `/element/${webElementOrId}/displayed`; + } + const element = this.getWebElement(webElementOrId); - const result = await element.isDisplayed(); - return result; + return element.isDisplayed(); }, async isElementEnabled(webElementOrId) { diff --git a/test/lib/nocks.js b/test/lib/nocks.js index f841d488a8..e40d4f5195 100644 --- a/test/lib/nocks.js +++ b/test/lib/nocks.js @@ -138,6 +138,18 @@ module.exports = { return this; }, + appiumElementFound() { + nock('http://localhost:10195') + .post('/wd/hub/session/1352110219202/elements', {'using': 'id', 'value': 'com.app:id/web-login'}) + .reply(200, { + status: 0, + state: 'success', + value: [{'element-6066-11e4-a52e-4f735466cecf': '0'}] + }); + + return this; + }, + click() { nock('http://localhost:10195') .post('/wd/hub/session/1352110219202/element/0/click') @@ -370,6 +382,19 @@ module.exports = { return this; }, + appiumElementVisible() { + nock('http://localhost:10195') + .get('/wd/hub/session/1352110219202/element/0/displayed') + .reply(200, { + status: 0, + sessionId: '1352110219202', + value: true, + state: 'success' + }); + + return this; + }, + notVisible(times) { var mock = nock('http://localhost:10195') .post('/wd/hub/session/1352110219202/execute/sync'); diff --git a/test/src/api/commands/element/testIsVisible.js b/test/src/api/commands/element/testIsVisible.js index 3807dc8d94..b50f665eb2 100644 --- a/test/src/api/commands/element/testIsVisible.js +++ b/test/src/api/commands/element/testIsVisible.js @@ -12,13 +12,21 @@ describe('isVisible', function () { url: '/wd/hub/session/1352110219202/execute/sync', method: 'POST' }); + MockServer.removeMock({ + url: '/wd/hub/session/1352110219202/elements', + method: 'POST' + }); + MockServer.removeMock({ + url: '/wd/hub/session/1352110219202/element/999/displayed', + method: 'GET' + }); }); after(function (done) { CommandGlobals.afterEach.call(this, done); }); - it('client.isVisible()', function (done) { + it('client.isVisible() [visible]', function (done) { MockServer.addMock({ url: '/wd/hub/session/1352110219202/execute/sync', method: 'POST', @@ -38,7 +46,7 @@ describe('isVisible', function () { this.client.start(done); }); - it('client.isVisible()', function (done) { + it('client.isVisible() [not visible]', function (done) { MockServer.addMock({ url: '/wd/hub/session/1352110219202/execute/sync', method: 'POST', @@ -57,4 +65,76 @@ describe('isVisible', function () { this.client.start(done); }); + + it('client.isVisible() [visible] -- appium', function (done) { + MockServer + .addMock({ + url: '/wd/hub/session/1352110219202/elements', + postdata: { + using: 'id', + value: 'com.app:id/weblogin' + }, + method: 'POST', + response: JSON.stringify({ + status: 0, + state: 'success', + value: [{'element-6066-11e4-a52e-4f735466cecf': '999'}] + }) + }) + .addMock({ + url: '/wd/hub/session/1352110219202/element/999/displayed', + method: 'GET', + response: JSON.stringify({ + value: true + }) + }); + + // Make appium client + this.client.api.options.selenium.use_appium = true; + + this.client.api.isVisible('id', 'com.app:id/weblogin', function callback(result) { + assert.strictEqual(result.value, true); + }).isVisible({selector: 'com.app:id/weblogin', locateStrategy: 'id'}, function callback(result) { + assert.strictEqual(result.value, true); + }); + + this.client.start(done); + }); + + it('client.isVisible() [not visible] -- appium', function (done) { + MockServer + .addMock({ + url: '/wd/hub/session/1352110219202/elements', + postdata: { + using: 'id', + value: 'com.app:id/weblogin' + }, + method: 'POST', + response: JSON.stringify({ + status: 0, + state: 'success', + value: [{'element-6066-11e4-a52e-4f735466cecf': '999'}] + }) + }) + .addMock({ + url: '/wd/hub/session/1352110219202/element/999/displayed', + method: 'GET', + response: JSON.stringify({ + value: false + }) + }); + + // Make appium client + this.client.api.options.selenium.use_appium = true; + + assert.strictEqual(this.client.api.isAppiumClient(), true); + + this.client.api.isVisible('id', 'com.app:id/weblogin', function callback(result) { + assert.strictEqual(result.value, false); + }).isVisible({selector: 'com.app:id/weblogin', locateStrategy: 'id'}, function callback(result) { + assert.strictEqual(result.value, false); + }); + + this.client.start(done); + }); }); diff --git a/test/src/api/commands/element/testWaitForElementVisible.js b/test/src/api/commands/element/testWaitForElementVisible.js index aeb4e94888..e5966c16c3 100644 --- a/test/src/api/commands/element/testWaitForElementVisible.js +++ b/test/src/api/commands/element/testWaitForElementVisible.js @@ -336,4 +336,46 @@ describe('waitForElementVisible', function () { }); }); + it('client.waitForElementVisible() success with appium client', function () { + MockServer + .addMock({ + url: '/wd/hub/session/1352110219202/elements', + postdata: { + using: 'id', + value: 'com.app:id/web-login' + }, + method: 'POST', + response: JSON.stringify({ + status: 0, + state: 'success', + value: [{'element-6066-11e4-a52e-4f735466cecf': '99'}] + }) + }, undefined, true) + .addMock({ + url: '/wd/hub/session/1352110219202/element/99/displayed', + method: 'GET', + response: JSON.stringify({ + value: true + }) + }, undefined, true); + + // Make appium client + this.client.api.options.selenium.use_appium = true; + + assert.strictEqual(this.client.api.isAppiumClient(), true); + + this.client.api.waitForElementVisible('id', 'com.app:id/web-login', function callback(result, instance) { + assert.strictEqual(instance.elementId, '99'); + assert.strictEqual(result.value, true); + }).waitForElementVisible({selector: 'com.app:id/web-login', locateStrategy: 'id'}, function callback(result, instance) { + assert.strictEqual(instance.elementId, '99'); + assert.strictEqual(result.value, true); + }); + + return this.client.start(function (err) { + if (err) { + throw err; + } + }); + }); }); diff --git a/test/src/api/expect/testExpectVisible.js b/test/src/api/expect/testExpectVisible.js index ba170e29c8..50afb93f83 100644 --- a/test/src/api/expect/testExpectVisible.js +++ b/test/src/api/expect/testExpectVisible.js @@ -80,6 +80,25 @@ describe('expect.visible', function () { }); }); + it('to be visible [PASSED] -- appium', function () { + Nocks.appiumElementFound().appiumElementVisible(); + + // Make appium client + this.client.api.options.selenium.use_appium = true; + strictEqual(this.client.api.isAppiumClient(), true); + + let expect = this.client.api.expect.element({selector: 'com.app:id/web-login', locateStrategy: 'id'}).to.be.visible; + + return this.client.start(function () { + strictEqual(expect.assertion.selector, 'com.app:id/web-login'); + strictEqual(expect.assertion.negate, false); + strictEqual(expect.assertion.passed, true); + strictEqual(expect.assertion.resultValue, true); + strictEqual(expect.assertion.message, `Expected element to be visible (${expect.assertion.elapsedTime}ms)`); + strictEqual(expect.assertion.messageParts.length, 1); + }); + }); + it('to be visible with waitFor [PASSED]', function () { Nocks.elementFound().visible(); diff --git a/test/src/core/testCreateSession.js b/test/src/core/testCreateSession.js index 6c4737c456..6c6dd49319 100644 --- a/test/src/core/testCreateSession.js +++ b/test/src/core/testCreateSession.js @@ -582,6 +582,8 @@ describe('test Request With Credentials', function () { } } }); + + assert.strictEqual(client.api.isAppiumClient(), false); }); it('Test create session with browserstack and browserName set to null (App Automate)', async function () { @@ -693,6 +695,9 @@ describe('test Request With Credentials', function () { }); assert.strictEqual(client.transport.uploadedAppUrl, 'bs://878bdf21505f0004ce'); + + assert.strictEqual(client.settings.selenium.use_appium, undefined); + assert.strictEqual(client.api.isAppiumClient(), true); }); it('Test create session with Browserstack App Automate using custom id', async function () { @@ -818,6 +823,9 @@ describe('test Request With Credentials', function () { }); assert.strictEqual(client.transport.uploadedAppUrl, undefined); + + assert.strictEqual(client.settings.selenium.use_appium, undefined); + assert.strictEqual(client.api.isAppiumClient(), true); }); it('Test create session with browserstack and when buildName is not set', async function () { diff --git a/test/src/index/transport/testMobileOptions.js b/test/src/index/transport/testMobileOptions.js index 5a88af3f9d..f66dbabc07 100644 --- a/test/src/index/transport/testMobileOptions.js +++ b/test/src/index/transport/testMobileOptions.js @@ -26,6 +26,7 @@ describe('Test mobile options in Nightwatch/Appium client', function () { assert.strictEqual(client.api.isIOS(), true); assert.strictEqual(client.api.isAndroid(), false); assert.strictEqual(client.api.isMobile(), true); + assert.strictEqual(client.api.isAppiumClient(), false); }); it('have isAndroid() for web testing on Android', function () { @@ -48,6 +49,7 @@ describe('Test mobile options in Nightwatch/Appium client', function () { assert.strictEqual(client.api.isIOS(), false); assert.strictEqual(client.api.isAndroid(), true); assert.strictEqual(client.api.isMobile(), true); + assert.strictEqual(client.api.isAppiumClient(), false); }); it('doesn\'t have isIOS(), isAndroid() and isMobile() for web testing on desktop', function () { @@ -66,9 +68,10 @@ describe('Test mobile options in Nightwatch/Appium client', function () { assert.strictEqual(client.api.isIOS(), false); assert.strictEqual(client.api.isAndroid(), false); assert.strictEqual(client.api.isMobile(), false); + assert.strictEqual(client.api.isAppiumClient(), false); }); - it('have isIOS() for native testing on iOS', function () { + it('have isIOS() and isAppiumClient() for native testing on iOS', function () { const client = Nightwatch.createClient({ selenium: { use_appium: true @@ -86,12 +89,13 @@ describe('Test mobile options in Nightwatch/Appium client', function () { assert.strictEqual(client.api.isIOS(), true); assert.strictEqual(client.api.isAndroid(), false); assert.strictEqual(client.api.isMobile(), true); + assert.strictEqual(client.api.isAppiumClient(), true); }); it('have isAndroid() for native testing on Android', function () { const client = Nightwatch.createClient({ selenium: { - use_appium: true + use_appium: false }, desiredCapabilities: { browserName: null, @@ -105,9 +109,11 @@ describe('Test mobile options in Nightwatch/Appium client', function () { assert.strictEqual(client.api.isIOS(), false); assert.strictEqual(client.api.isAndroid(), true); assert.strictEqual(client.api.isMobile(), true); + // backward compatibility with browserName: null + assert.strictEqual(client.api.isAppiumClient(), true); }); - it('have isAndroid() for web testing on Android using Appium', function () { + it('have isAndroid() and isAppiumClient() for web testing on Android using Appium', function () { const client = Nightwatch.createClient({ selenium: { use_appium: true @@ -124,5 +130,6 @@ describe('Test mobile options in Nightwatch/Appium client', function () { assert.strictEqual(client.api.isIOS(), false); assert.strictEqual(client.api.isAndroid(), true); assert.strictEqual(client.api.isMobile(), true); + assert.strictEqual(client.api.isAppiumClient(), true); }); });