don't use windowHandles for switching to/from webview

This commit is contained in:
Jonathan Lipps
2014-04-08 12:02:44 -07:00
parent 17af02875e
commit 6725332707
8 changed files with 46 additions and 182 deletions
+18 -25
View File
@@ -12,11 +12,11 @@ One of the core principles of Appium is that you shouldn't have to change your a
Here are the steps required to talk to a web view in your Appium test:
1. Navigate to a portion of your app where a web view is active
1. Call [GET session/:sessionId/window_handles](http://code.google.com/p/selenium/wiki/JsonWireProtocol#/session/:sessionId/window_handles)
1. This returns a list of web view ids we can access
1. Call [POST session/:sessionId/window](http://code.google.com/p/selenium/wiki/JsonWireProtocol#/session/:sessionId/window) with the id of the web view you want to access
1. Call [GET session/:sessionId/contexts](https://code.google.com/p/selenium/source/browse/spec-draft.md?repo=mobile)
1. This returns a list of contexts we can access, like 'NATIVE_APP' or 'WEBVIEW_1'
1. Call [POST session/:sessionId/context](https://code.google.com/p/selenium/source/browse/spec-draft.md?repo=mobile) with the id of the context you want to access
1. (This puts your Appium session into a mode where all commands are interpreted as being intended for automating the web view, rather than the native portion of the app. For example, if you run getElementByTagName, it will operate on the DOM of the web view, rather than return UIAElements. Of course, certain WebDriver methods only make sense in one context or another, so in the wrong context you will receive an error message).
1. To stop automating in the web view context and go back to automating the native portion of the app, simply call `"mobile: leaveWebView"` with execute_script to leave the web frame.
1. To stop automating in the web view context and go back to automating the native portion of the app, simply call `context` again with the native context id to leave the web frame.
## Execution against a real iOS device
To interrogate and interact with a web view appium establishes a connection using a remote debugger. When executing the examples below against a simulator this connection can be established directly as the simulator and the appium server are on the same machine. When executing against a real device appium is unable to access the web view directly. Therefore the connection has to be established through the USB lead. To establish this connection we use the [ios-webkit-debugger-proxy](https://github.com/google/ios-webkit-debug-proxy).
@@ -55,13 +55,13 @@ Once installed you can start the proxy with the following command:
// assuming we have an initialized `driver` object working on the UICatalog app
driver.elementByName('Web, Use of UIWebView', function(err, el) { // find button to nav to view
el.click(function(err) { // nav to UIWebView
driver.windowHandles(function(err, handles) { // get list of available views
driver.window(handles[0], function(err) { // choose the only available view
driver.contexts(function(err, contexts) { // get list of available views
driver.context(contexts[1], function(err) { // choose what is probably the webview context
driver.elementsByCss('.some-class', function(err, els) { // get webpage elements by css
els.length.should.be.above(0); // there should be some!
els[0].text(function(elText) { // get text of the first element
elText.should.eql("My very own text"); // it should be extremely personal and awesome
driver.execute("mobile: leaveWebView", function(err) { // leave webview context
driver.context('NATIVE_APP', function(err) { // leave webview context
// do more native stuff here if we want
driver.quit(); // stop webdrivage
});
@@ -87,8 +87,8 @@ Once installed you can start the proxy with the following command:
RemoteWebDriver remoteWebDriver = new RemoteWebDriver(url, desiredCapabilities);
//switch to the latest web view
for(String winHandle : remoteWebDriver.getWindowHandles()){
remoteWebDriver.switchTo().window(winHandle);
for(String contextHandle : remoteWebDriver.getContexts()){
remoteWebDriver.switchTo().context(contextHandle);
}
//Interact with the elements on the guinea-pig page using id.
@@ -97,7 +97,7 @@ Once installed you can start the proxy with the following command:
remoteWebDriver.findElement(By.id("comments")).sendKeys("My comment"); //populate the comments field by id.
//leave the webview to go back to native app.
remoteWebDriver.executeScript("mobile: leaveWebView");
remoteWebDriver.switchTo().context('NATIVE_APP')
//close the app.
remoteWebDriver.quit();
@@ -124,12 +124,12 @@ capabilities =
## Then switch to it using @driver.switch_to_window("6")
Given(/^I switch to webview$/) do
webview = @driver.window_handles.last
@driver.switch_to.window(webview)
webview = @driver.contexts.last
@driver.switch_to.context(webview)
end
Given(/^I switch out of webview$/) do
@driver.execute_script("mobile: leaveWebView")
@driver.switch_to(@driver.contexts.first)
end
# Now you can use CSS to select an element inside your webview
@@ -145,25 +145,18 @@ https://gist.github.com/feelobot/7309729
<a name="android"></a>Automating hybrid Android apps
--------------------------
Appium comes with built-in hybrid support via Chromedriver. Appium also uses Selendroid under the hood for webview support on devices older than 4.4. (In that case, you'll want to specify `"device": "selendroid"` as a desired capability). Then:
1. Navigate to a portion of your app where a web view is active
1. Call [POST session/:sessionId/window](http://code.google.com/p/selenium/wiki/JsonWireProtocol#/session/:sessionId/window) with the string "WEBVIEW" as the window handle, e.g., `driver.window("WEBVIEW")`.
1. (This puts your Appium session into a mode where all commands are interpreted as being intended for automating the web view, rather than the native portion of the app. For example, if you run getElementByTagName, it will operate on the DOM of the web view, rather than return UIAElements. Of course, certain WebDriver methods only make sense in one context or another, so in the wrong context you will receive an error message).
1. To stop automating in the web view context and go back to automating the native portion of the app, simply call `window` again with the string "NATIVE_APP", e.g., `driver.window("NATIVE_APP")`.
Note: We could have used the same strategy as above for leaving the webview (calling `mobile: leaveWebView`), however Selendroid uses the `WEBVIEW`/`NATIVE_APP` window setting strategy, which also works with regular Appium hybrid support, so we show that here for parity.
Appium comes with built-in hybrid support via Chromedriver. Appium also uses Selendroid under the hood for webview support on devices older than 4.4. (In that case, you'll want to specify `"device": "selendroid"` as a desired capability). Then follow all the same steps as above for iOS, i.e., switching contexts, etc...
## Wd.js Code example
```js
// assuming we have an initialized `driver` object working on a hybrid app
driver.window("WEBVIEW", function(err) { // choose the only available view
driver.context("WEBVIEW", function(err) { // choose the only available view
driver.elementsByCss('.some-class', function(err, els) { // get webpage elements by css
els.length.should.be.above(0); // there should be some!
els[0].text(function(elText) { // get text of the first element
elText.should.eql("My very own text"); // it should be extremely personal and awesome
driver.window("NATIVE_APP", function(err) { // leave webview context
driver.context("NATIVE_APP", function(err) { // leave webview context
// do more native stuff here if we want
driver.quit(); // stop webdrivage
});
@@ -183,7 +176,7 @@ driver.window("WEBVIEW", function(err) { // choose the only available view
RemoteWebDriver remoteWebDriver = new RemoteWebDriver(url, desiredCapabilities);
//switch to the web view
remoteWebDriver.switchTo().window("WEBVIEW");
remoteWebDriver.switchTo().context("WEBVIEW");
//Interact with the elements on the guinea-pig page using id.
WebElement div = remoteWebDriver.findElement(By.id("i_am_an_id"));
@@ -191,7 +184,7 @@ driver.window("WEBVIEW", function(err) { // choose the only available view
remoteWebDriver.findElement(By.id("comments")).sendKeys("My comment"); //populate the comments field by id.
//leave the webview to go back to native app.
remoteWebDriver.switchTo().window("NATIVE_APP");
remoteWebDriver.switchTo().context("NATIVE_APP");
//close the app.
remoteWebDriver.quit();
+3 -37
View File
@@ -867,50 +867,16 @@ androidController.setContext = function (name, cb) {
}.bind(this));
};
// TODO: remove in appium 1.0
androidController.getWindowHandle = function (cb) {
warnDeprecated('function', 'getWindowHandle', 'getCurrentContext()');
jwpSuccess(this.curContext, cb);
cb(new NotYetImplementedError(), null);
};
// TODO: remove in appium 1.0
androidController.getWindowHandles = function (cb) {
warnDeprecated('function', 'getWindowHandles', 'getContexts()');
this.listWebviews(function (err, webviews) {
if (err) return cb(err);
if (webviews.length) {
this.contexts = [NATIVE_WIN, WEBVIEW_WIN];
} else {
this.contexts = [NATIVE_WIN];
}
jwpSuccess(this.contexts, cb);
}.bind(this));
cb(new NotYetImplementedError(), null);
};
// TODO: remove in appium 1.0
androidController.setWindow = function (name, cb) {
warnDeprecated('function', 'setWindow', 'setContext(name)');
this.getWindowHandles(function () {
if (!_.contains(this.contexts, name)) {
return cb(null, {
status: status.codes.NoSuchWindow.code
, value: "That window doesn't exist"
});
}
if (name === this.curContext) {
return cb();
}
var next = function (err) {
if (err) return cb(err);
this.curContext = name;
jwpSuccess(cb);
}.bind(this);
if (name === WEBVIEW_WIN) {
this.startChromedriverProxy(next);
} else {
this.stopChromedriverProxy(next);
}
}.bind(this));
cb(new NotYetImplementedError(), null);
};
androidController.closeWindow = function (cb) {
+11 -29
View File
@@ -1532,20 +1532,16 @@ iOSController.setContext = function (name, cb, skipReadyCheck) {
};
iOSController.getWindowHandle = function (cb) {
var err = null, response = null;
if (this.isWebContext()) {
var windowHandle = this.curContext;
response = {
var response = {
status: status.codes.Success.code
, value: windowHandle
};
cb(null, response);
} else {
response = {
status: status.codes.NoSuchWindow.code
, value: null
};
cb(new NotImplementedError(), null);
}
cb(err, response);
};
iOSController.massagePage = function (page) {
@@ -1554,6 +1550,10 @@ iOSController.massagePage = function (page) {
};
iOSController.getWindowHandles = function (cb) {
if (!this.isWebContext()) {
return cb(new NotImplementedError(), null);
}
this.listWebFrames(function (pageArray) {
this.windowHandleCache = _.map(pageArray, this.massagePage);
var idArray = _.pluck(this.windowHandleCache, 'id');
@@ -1571,6 +1571,10 @@ iOSController.getWindowHandles = function (cb) {
};
iOSController.setWindow = function (name, cb, skipReadyCheck) {
if (!this.isWebContext()) {
return cb(new NotImplementedError(), null);
}
if (_.contains(_.pluck(this.windowHandleCache, 'id'), name)) {
var pageIdKey = parseInt(name, 10);
var next = function () {
@@ -1668,28 +1672,6 @@ iOSController.checkSuccess = function (err, res, cb) {
return true;
};
iOSController.leaveWebView = function (cb) {
warnDeprecated('function', 'leaveWebView', 'context(null');
if (this.isWebContext()) {
this.curWindowHandle = null;
this.curContext = null;
//TODO: this condition should be changed to check if the webkit protocol is being used.
if (this.args.udid) {
this.remote.disconnect();
this.curWindowHandle = null;
}
cb(null, {
status: status.codes.Success.code
, value: ''
});
} else {
cb(null, {
status: status.codes.NoSuchFrame.code
, value: "We are not in a webview, so can't leave one!"
});
}
};
iOSController.execute = function (script, args, cb) {
if (this.isWebContext()) {
this.convertElementForAtoms(args, function (err, res) {
-5
View File
@@ -649,10 +649,6 @@ exports.frame = function (req, res) {
req.device.frame(frame, getResponseHandler(req, res));
};
exports.leaveWebView = function (req, res) {
req.device.leaveWebView(getResponseHandler(req, res));
};
exports.elementDisplayed = function (req, res) {
var elementId = req.params.elementId;
req.device.elementDisplayed(elementId, getResponseHandler(req, res));
@@ -1111,7 +1107,6 @@ var mobileCmdMap = {
, 'lock' : exports.lock
, 'background' : exports.background
, 'keyevent' : exports.keyevent
, 'leaveWebView': exports.leaveWebView
, 'fireEvent': exports.fireEvent
, 'source': exports.mobileSource
, 'find': exports.find
+1 -4
View File
@@ -3,7 +3,4 @@
process.env.DEVICE = process.env.DEVICE || "android";
var androidWebviewTests = require('../../helpers/android-webview');
describe('android - web_view - contexts -', androidWebviewTests.contexts);
// TODO: remove in Appium 1.0
describe('android - web_view - windows -', androidWebviewTests.windows);
describe('android - web_view - ', androidWebviewTests);
@@ -1,3 +1,5 @@
/*globals should:true */
"use strict";
var env = require('../../../helpers/env')
, setup = require("../../common/setup-base")
@@ -11,10 +13,10 @@ describe("safari - windows-frame -", function () {
var driver;
setup(this, {browserName: 'safari'}).then(function (d) { driver = d; });
it('getting current window should work initially', function (done) {
it('getting current context should work initially', function (done) {
driver
.windowHandle().then(function (handleId) {
parseInt(handleId, 10).should.be.above(0);
.currentContext().then(function (contextId) {
should.exist(contextId);
}).nodeify(done);
});
describe('within webview', function () {
+7 -10
View File
@@ -3,16 +3,13 @@
var setup = require("../../common/setup-base"),
desired = require('./desired');
describe('uicatalog - reset -', function () {
describe('uicatalog - contexts', function () {
var driver;
setup(this, desired).then(function (d) { driver = d; });
describe('window handles', function () {
var driver;
setup(this, desired).then(function (d) { driver = d; });
it('getting handles should do nothing when no webview open', function (done) {
driver
.windowHandles().should.eventually.have.length(0)
.nodeify(done);
});
it('getting contexts should do nothing when no webview open', function (done) {
driver
.contexts().should.eventually.have.length(1)
.nodeify(done);
});
});
+1 -69
View File
@@ -11,7 +11,7 @@ var desired = {
'app-activity': '.HomeScreenActivity'
};
module.exports.contexts = function () {
module.exports = function () {
var driver;
setup(this, desired).then(function (d) { driver = d; });
@@ -91,71 +91,3 @@ module.exports.contexts = function () {
.nodeify(done);
});
};
// TODO: remove in Appium 1.0
module.exports.windows = function () {
var driver;
setup(this, desired).then(function (d) { driver = d; });
beforeEach(function (done) {
driver
.waitForElementByName('buttonStartWebviewCD').click()
.sleep(500)
.window('WEBVIEW')
.nodeify(done);
});
if (env.FAST_TESTS) {
afterEach(function (done) {
driver
.window('NATIVE_APP')
.then(function () {
if (env.DEVICE === "selendroid") {
return driver.elementByIdOrNull('goBack');
} else {
return driver.elementByTagNameOrNull('button');
}
})
.then(function (el) {
if (el) return el.click().sleep(1000);
}).nodeify(done);
});
}
it('should be web view', function (done) {
// todo: add some sort of check here
done();
});
it('should find and click an element', function (done) {
driver
.elementByCssSelector('input[type=submit]').click()
.waitForElementByXPath("//h1[contains(., 'This is my way')]")
.nodeify(done);
});
// selendroid test app is busted
it('should clear input @skip-selendroid-all', function (done) {
driver
.elementById('name_input').click().clear().getValue().should.become("")
.nodeify(done);
});
// selendroid test app is busted
it('should find and enter key sequence in input @skip-selendroid-all', function (done) {
driver
.elementById('name_input').clear()
.type("Mathieu").getValue().should.become("Mathieu")
.nodeify(done);
});
it('should be able to handle selendroid special keys @skip-android-all', function (done) {
driver.keys('\uE102').nodeify(done);
});
it('should get web source', function (done) {
driver
.source().should.eventually.include("<title>Say Hello Demo<")
.nodeify(done);
});
};