Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
71 changes: 70 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
@@ -1,6 +1,75 @@
Appium Python Client
====================

An extension library for adding [Selenium 3.0 draft](https://code.google.com/p/selenium/source/browse/spec-draft.md?repo=mobile) functionality to [Appium](https://github.com/appium/appium).
An extension library for adding [Selenium 3.0 draft](https://dvcs.w3.org/hg/webdriver/raw-file/tip/webdriver-spec.html) and [Mobile JSON Wire Protocol Specification draft](https://code.google.com/p/selenium/source/browse/spec-draft.md?repo=mobile)
functionality to the Python language bindings, for use with the mobile testing
framework [Appium](https://appium.io).

# Usage

The Appium Python Client is fully compliant with the Selenium 3.0 specification
draft, with some helpers to make mobile testing in Python easier. The majority of
the usage remains as it has been for Selenium 2 (WebDriver), and as the [official
Selenium Python bindings](https://pypi.python.org/pypi/selenium) begins to
implement the new specification that implementation will be used underneath, so
test code can be written that is utilizable with both bindings.

To use the new functionality now, and to use the superset of functions, instead of
including the Selenium `webdriver` module in your test code, use that from
Appium instead.

```python
from appium import webdriver
```

From there much of your test code will work with no change.

As a base for the following code examples, the following sets up the [UnitTest](https://docs.python.org/2/library/unittest.html)
environment:

```python
from appium import webdriver

desired_caps = {}
desired_caps['device'] = 'Android'
desired_caps['browserName'] = ''
desired_caps['version'] = '4.2'
desired_caps['app'] = PATH('../../../apps/selendroid-test-app.apk')
desired_caps['app-package'] = 'io.selendroid.testapp'
desired_caps['app-activity'] = '.HomeScreenActivity'

self.driver = webdriver.Remote('http://localhost:4723/wd/hub', desired_caps)
```

## Changed or added functionality

The methods that do change are...


### Switching between 'Native' and 'Webview'

For mobile testing the Selnium methods for switching between windows was previously
commandeered for switching between native applications and webview contexts. Methods
explicitly for this have been added to the Selenium 3 specification, so moving
forward these 'context' methods are to be used.

To get the current context, rather than calling `driver.current_window_handle` you
use

```python
current = driver.context
```

The available contexts are not retrieved using `driver.window_handles` but with

```python
driver.contexts
```

Finally, to switch to a new context, rather than `driver.switch_to.window(name)`,
use the comparable context method

```python
context_name = "WEBVIEW_1"
driver.switch_to.context(context_name)
```
12 changes: 12 additions & 0 deletions appium/__init__.py
Original file line number Diff line number Diff line change
@@ -1 +1,13 @@
#!/usr/bin/env python

# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
13 changes: 13 additions & 0 deletions appium/common/__init__.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
#!/usr/bin/env python

# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
27 changes: 27 additions & 0 deletions appium/common/exceptions.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
#!/usr/bin/env python

# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.

from selenium.common.exceptions import InvalidSwitchToTargetException

class NoSuchContextException(InvalidSwitchToTargetException):
"""
Thrown when window target to be switched doesn't exist.

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

maybe "window" should be changed to "context" in this doc

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yup. I'll put that in the next commit. Thanks!


To find the current set of active window handles, you can get a list
of the active window handles in the following way::

print driver.window_handles

"""
pass
2 changes: 0 additions & 2 deletions appium/mobilecommand.py

This file was deleted.

14 changes: 14 additions & 0 deletions appium/webdriver/__init__.py
Original file line number Diff line number Diff line change
@@ -1 +1,15 @@
#!/usr/bin/env python

# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.

from webdriver import WebDriver as Remote
29 changes: 29 additions & 0 deletions appium/webdriver/errorhandler.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
#!/usr/bin/env python

# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.

from selenium.webdriver.remote import errorhandler
from selenium.common.exceptions import WebDriverException

from appium.common.exceptions import NoSuchContextException

class MobileErrorHandler(errorhandler.ErrorHandler):
def check_response(self, response):
try:
super(MobileErrorHandler, self).check_response(response)
except WebDriverException as wde:
if wde.msg == 'No such context found.':
raise NoSuchContextException(wde.msg, wde.screen, wde.stacktrace)
else:
raise wde

18 changes: 18 additions & 0 deletions appium/webdriver/mobilecommand.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
#!/usr/bin/env python

# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.

class MobileCommand(object):
CONTEXTS = 'getContexts',
GET_CURRENT_CONTEXT = 'getCurrentContext',
SWITCH_TO_CONTEXT = 'switchToContext'
30 changes: 30 additions & 0 deletions appium/webdriver/switch_to.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
#!/usr/bin/env python

# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.

from selenium.webdriver.remote.switch_to import SwitchTo

from .mobilecommand import MobileCommand

class MobileSwitchTo(SwitchTo):
def context(self, context_name):
"""
Sets the context for the current session.

:Args:
- context_name: The name of the context to switch to.

:Usage:
driver.switch_to.context('WEBVIEW_1')
"""
self._driver.execute(MobileCommand.SWITCH_TO_CONTEXT, {'name': context_name})
53 changes: 51 additions & 2 deletions appium/webdriver/webdriver.py
Original file line number Diff line number Diff line change
@@ -1,10 +1,59 @@
#!/usr/bin/python
#!/usr/bin/env python

# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.

from selenium import webdriver

from .mobilecommand import MobileCommand as Command
from .errorhandler import MobileErrorHandler
from .switch_to import MobileSwitchTo

class WebDriver(webdriver.Remote):
def __init__(self, command_executor='http://127.0.0.1:4444/wd/hub',
desired_capabilities=None, browser_profile=None, proxy=None, keep_alive=False):

# we have no new initialization to do
super(WebDriver, self).__init__(command_executor, desired_capabilities, browser_profile, proxy, keep_alive)

if self.command_executor is not None:
self._addCommands()

self.error_handler = MobileErrorHandler()
self._switch_to = MobileSwitchTo(self)

@property
def contexts(self):
"""
Returns the contexts within the current session.

:Usage:
driver.contexts
"""
return self.execute(Command.CONTEXTS)['value'];

@property
def current_context(self):
"""
Returns the current context of the current session.

:Usage:
driver.current_context
"""
return self.execute(Command.GET_CURRENT_CONTEXT)['value']

def _addCommands(self):
self.command_executor._commands[Command.CONTEXTS] = \
('GET', '/session/$sessionId/contexts')
self.command_executor._commands[Command.GET_CURRENT_CONTEXT] = \
('GET', '/session/$sessionId/context')
self.command_executor._commands[Command.SWITCH_TO_CONTEXT] = \
('POST', '/session/$sessionId/context')
14 changes: 13 additions & 1 deletion setup.py
Original file line number Diff line number Diff line change
@@ -1,5 +1,17 @@
#!/usr/bin/env python

# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.

from distutils.core import setup

setup(name='Python-Client',
Expand All @@ -8,6 +20,6 @@
author='Isaac Murchie',
author_email='isaac@saucelabs.com',
url='http://appium.io/',
packages=['appium', 'appium.webdriver'],
packages=['appium', 'appium.webdriver', 'appium.common'],
license='Apache 2.0'
)
Binary file added test/apps/selendroid-test-app.apk
Binary file not shown.
76 changes: 76 additions & 0 deletions test/functional/android/context_switching_tests.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,76 @@
#!/usr/bin/env python

# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.

import os
from time import sleep

from appium import webdriver
from appium.common.exceptions import NoSuchContextException

import unittest

# Returns abs path relative to this file and not cwd
PATH = lambda p: os.path.abspath(
os.path.join(os.path.dirname(__file__), p)
)

class ContextSwitchingTests(unittest.TestCase):
def setUp(self):
desired_caps = {}
desired_caps['device'] = 'Android'
desired_caps['browserName'] = ''
desired_caps['version'] = '4.2'
desired_caps['app'] = PATH('../../apps/selendroid-test-app.apk')
desired_caps['app-package'] = 'io.selendroid.testapp'
desired_caps['app-activity'] = '.HomeScreenActivity'

self.driver = webdriver.Remote('http://localhost:4723/wd/hub', desired_caps)

def test_contexts_list(self):
self._enter_webview()
contexts = self.driver.contexts
self.assertEqual(2, len(contexts));

def test_move_to_correct_context(self):
self._enter_webview()
self.assertEqual('WEBVIEW_1', self.driver.current_context)

def test_actually_in_webview(self):
self._enter_webview()
self.driver.find_element_by_css_selector('input[type=submit]').click()
el = self.driver.find_element_by_xpath("//h1[contains(., 'This is my way')]")
self.assertIsNot(None, el)

def test_move_back_to_native_context(self):
self._enter_webview()
self.driver.switch_to.context(None)
self.assertEqual('NATIVE_APP', self.driver.current_context)

def test_set_invalid_context(self):
try:
self.driver.switch_to.context("invalid name")
self.fail("NoSuchContextException expected")
except NoSuchContextException:
pass # Expected

def tearDown(self):
self.driver.quit()

def _enter_webview(self):
btn = self.driver.find_element_by_name('buttonStartWebviewCD')
btn.click()
self.driver.switch_to.context('WEBVIEW')

if __name__ == "__main__":
unittest.main()