diff --git a/appium/webdriver/command_method.py b/appium/webdriver/command_method.py new file mode 100644 index 000000000..dde45cb44 --- /dev/null +++ b/appium/webdriver/command_method.py @@ -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. + +import enum + + +class CommandMethod(enum.Enum): + GET = 'GET' + HEAD = 'HEAD' + POST = 'POST' + PUT = 'PUT' + DELETE = 'DELETE' + CONNECT = 'CONNECT' + OPTIONS = 'OPTIONS' + TRACE = 'TRACE' + PATCH = 'PATCH' diff --git a/appium/webdriver/webdriver.py b/appium/webdriver/webdriver.py index 9b17d43cb..393684841 100644 --- a/appium/webdriver/webdriver.py +++ b/appium/webdriver/webdriver.py @@ -23,6 +23,7 @@ from selenium.webdriver.remote.remote_connection import RemoteConnection from appium.common.logger import logger +from appium.webdriver.command_method import CommandMethod from appium.webdriver.common.mobileby import MobileBy from .appium_connection import AppiumConnection @@ -356,6 +357,96 @@ def switch_to(self) -> MobileSwitchTo: return MobileSwitchTo(self) + def add_command(self, method: CommandMethod, url: str, name: str) -> T: + """Add a custom command as 'name' + + Args: + method: The method of HTTP request. Available methods are CommandMethod. + url: The url is URL template as https://www.w3.org/TR/webdriver/#endpoints. + `$sessionId` is a placeholder of current session id. + Other placeholders can be specified with `$` prefix like `$id`. + Then, `{'id': 'some id'}` argument in `execute_custom_command` replaces + the `$id` with the value, `some id`, in the request. + name: The name of a command to call in `execute_custom_command`. + + Returns: + `appium.webdriver.webdriver.WebDriver`: Self instance + + Raises: + ValueError + + Examples: + Define 'test_command' as GET and 'session/$sessionId/path/to/custom/url' + + >>> from appium.webdriver.command_method import CommandMethod + >>> driver.add_command( + method=CommandMethod.GET, + url='session/$sessionId/path/to/custom/url', + name='test_command' + ) + + """ + if name in self.command_executor._commands: + raise ValueError("{} is already defined".format(name)) + + if not isinstance(method, CommandMethod): + raise ValueError( + "'{}' is invalid. Valid method should be one of '{}'.".format(method, CommandMethod.__name__) + ) + + self.command_executor._commands[name] = (method.value, url) + return self + + def execute_custom_command(self, name: str, args: Dict = {}) -> Any: + """Execute a custom command defined as 'name' with args command. + + Args: + name: The name of command defined by `add_command`. + args: The argument as this command body + + Returns: + 'value' JSON object field in the response body. + + Raises: + ValueError, selenium.common.exceptions.WebDriverException + + Examples: + Calls 'test_command' command without arguments. + + >>> from appium.webdriver.command_method import CommandMethod + >>> driver.add_command( + method=CommandMethod.GET, + url='session/$sessionId/path/to/custom/url', + name='test_command' + ) + >>> result = driver.execute_custom_command('test_command') + + Calls 'test_command' command with arguments. + + >>> from appium.webdriver.command_method import CommandMethod + >>> driver.add_command( + method=CommandMethod.POST, + url='session/$sessionId/path/to/custom/url', + name='test_command' + ) + >>> result = driver.execute_custom_command('test_command', {'dummy': 'test argument'}) + + Calls 'test_command' command with arguments, and the path has 'element id'. + The '$id' will be 'element id' by 'id' key in the given argument. + + >>> from appium.webdriver.command_method import CommandMethod + >>> driver.add_command( + method=CommandMethod.POST, + url='session/$sessionId/path/to/$id/url', + name='test_command' + ) + >>> result = driver.execute_custom_command('test_command', {'id': 'element id', 'dummy': 'test argument'}) + + """ + if name not in self.command_executor._commands: + raise ValueError("No {} custom command. Please add it by 'add_command'".format(name)) + return self.execute(name, args)['value'] + # pylint: disable=protected-access def _addCommands(self) -> None: diff --git a/test/unit/webdriver/webdriver_test.py b/test/unit/webdriver/webdriver_test.py index bcd71d974..df7d5e88f 100644 --- a/test/unit/webdriver/webdriver_test.py +++ b/test/unit/webdriver/webdriver_test.py @@ -15,12 +15,14 @@ import json import httpretty +import pytest from mock import patch from appium import version as appium_version from appium import webdriver +from appium.webdriver.command_method import CommandMethod from appium.webdriver.webdriver import WebDriver -from test.unit.helper.test_helper import android_w3c_driver, appium_command, ios_w3c_driver +from test.unit.helper.test_helper import android_w3c_driver, appium_command, get_httpretty_request_body, ios_w3c_driver class TestWebDriverWebDriver(object): @@ -250,6 +252,72 @@ def exceptionCallback(request, uri, headers): mock_warning.assert_called_once() assert events == {} + @httpretty.activate + def test_add_command(self): + driver = ios_w3c_driver() + httpretty.register_uri( + httpretty.GET, + appium_command('session/1234567890/path/to/custom/url'), + body=json.dumps({'value': {}}), + ) + driver.add_command(method=CommandMethod.GET, url='session/$sessionId/path/to/custom/url', name='test_command') + result = driver.execute_custom_command('test_command') + + assert result == {} + + @httpretty.activate + def test_add_command_body(self): + driver = ios_w3c_driver() + httpretty.register_uri( + httpretty.POST, + appium_command('session/1234567890/path/to/custom/url'), + body=json.dumps({'value': {}}), + ) + driver.add_command(method=CommandMethod.POST, url='session/$sessionId/path/to/custom/url', name='test_command') + result = driver.execute_custom_command('test_command', {'dummy': 'test argument'}) + assert result == {} + + d = get_httpretty_request_body(httpretty.last_request()) + assert d['dummy'] == 'test argument' + + @httpretty.activate + def test_add_command_with_element_id(self): + driver = ios_w3c_driver() + httpretty.register_uri( + httpretty.GET, + appium_command('session/1234567890/path/to/custom/element_id/url'), + body=json.dumps({'value': {}}), + ) + driver.add_command( + method=CommandMethod.GET, url='session/$sessionId/path/to/custom/$id/url', name='test_command' + ) + result = driver.execute_custom_command('test_command', {'id': 'element_id'}) + assert result == {} + + @httpretty.activate + def test_add_command_already_defined(self): + driver = ios_w3c_driver() + driver.add_command(method=CommandMethod.GET, url='session/$sessionId/path/to/custom/url', name='test_command') + with pytest.raises(ValueError): + driver.add_command( + method=CommandMethod.GET, url='session/$sessionId/path/to/custom/url', name='test_command' + ) + + @httpretty.activate + def test_execute_custom_command(self): + driver = ios_w3c_driver() + driver.add_command(method=CommandMethod.GET, url='session/$sessionId/path/to/custom/url', name='test_command') + with pytest.raises(ValueError): + driver.add_command( + method=CommandMethod.GET, url='session/$sessionId/path/to/custom/url', name='test_command' + ) + + @httpretty.activate + def test_invalid_method(self): + driver = ios_w3c_driver() + with pytest.raises(ValueError): + driver.add_command(method='error', url='session/$sessionId/path/to/custom/url', name='test_command') + class SubWebDriver(WebDriver): def __init__(self, command_executor, desired_capabilities, direct_connection=False):