diff --git a/CHANGELOG.rst b/CHANGELOG.rst index 94ac6486..a6d81f14 100644 --- a/CHANGELOG.rst +++ b/CHANGELOG.rst @@ -584,7 +584,8 @@ v1.2.0 | If it's false, the WebElement is searched whenever is needed (default value) | If it's true, the WebElement is saved in PageElement to avoid searching for the same element multiple times. Useful - in mobile testing when searching for an element can take a long time. + in mobile testing when searching for an element can take a long time. + - New config property 'restart_driver_fail' in [Driver] section to restart the driver when the test fails even though the value of *reuse_driver* property is *true* - System property 'Config_environment' is used to select config files, e.g., to read android-properties.cfg file: diff --git a/docs/conf.py b/docs/conf.py index be42a34c..340ca45c 100644 --- a/docs/conf.py +++ b/docs/conf.py @@ -286,7 +286,7 @@ # texinfo_no_detailmenu = False # Example configuration for intersphinx: refer to the Python standard library. -intersphinx_mapping = {'http://docs.python.org/': None} +intersphinx_mapping = {'python': ('https://docs.python.org/3', None)} def remove_module_docstring(app, what, name, obj, options, lines): diff --git a/docs/page_objects.rst b/docs/page_objects.rst index 6b4f80d9..fea86893 100644 --- a/docs/page_objects.rst +++ b/docs/page_objects.rst @@ -121,7 +121,7 @@ If this default behaviour is not valid for our app (for example has more than on following optional parameters to define a custom logic that is executed at runtime: - webview_context_selection_callback: Method provided to select the desired webview context if -automatic_context_selection is enabled. Must return a tuple (context, window_handle) for android, and a context for ios. + automatic_context_selection is enabled. Must return a tuple (context, window_handle) for android, and a context for ios. - webview_csc_args: arguments list for webview_context_selection_callback. To use this functionality appium version must be greater or equal to 1.17. (where mobile:getContexts functionality was diff --git a/docs/toolium.utils.rst b/docs/toolium.utils.rst index 09642e84..450b488d 100644 --- a/docs/toolium.utils.rst +++ b/docs/toolium.utils.rst @@ -38,7 +38,7 @@ driver_utils driver_wait_utils ----------------- -.. automodule:: toolium.utils.driver_utils +.. automodule:: toolium.utils.driver_wait_utils :members: :undoc-members: :show-inheritance: diff --git a/toolium/selenoid.py b/toolium/selenoid.py index a2ced162..8d378a00 100644 --- a/toolium/selenoid.py +++ b/toolium/selenoid.py @@ -132,62 +132,66 @@ def is_the_session_still_active(self): """ Is the GGR session still active? Associated to a browser and the sessionId Example of GGR status: - { - "browsers": { - "MicrosoftEdge": { - "latest": {} - }, - "android": { - "8.1": {} - }, - "chrome": { - "70.0": {}, - "latest": { - "test_tef": { - "count": 1, - "sessions": [ - { - "caps": { - "browserName": "chrome", - "enableVNC": true, - "enableVideo": true, - "platformName": "ANY", - "screenResolution": "1280x1024x24", - 'browserVersion': "latest", - "videoName": "selenoide952e551bb9395e16d060f28c54e5d31.mp4", - "videoScreenSize": "1280x1024" - }, - "container": "8489205e28c9781472e99c3921a6240de3894a3603ed9e187ad6360b6b013b8b", - "containerInfo": { - "id": "8489205e28c9781472e99c3921a6240de3894a3603ed9e187ad6360b6b013b8b", - "ip": "172.17.0.4" - }, - "id": "1345506093dfed8dbcef610da476911a228ca315978e5464ae49fb1142bbc49b", - "screen": "1280x1024x24", - "vnc": true + + .. code-block:: json + + { + "browsers": { + "MicrosoftEdge": { + "latest": {} + }, + "android": { + "8.1": {} + }, + "chrome": { + "70.0": {}, + "latest": { + "test_tef": { + "count": 1, + "sessions": [ + { + "caps": { + "browserName": "chrome", + "enableVNC": true, + "enableVideo": true, + "platformName": "ANY", + "screenResolution": "1280x1024x24", + "browserVersion": "latest", + "videoName": "selenoide952e551bb9395e16d060f28c54e5d31.mp4", + "videoScreenSize": "1280x1024" + }, + "container": "8489205e28c9781472e99c3921a6240de3894a3603ed9e187ad6360b6b013b8b", + "containerInfo": { + "id": "8489205e28c9781472e99c3921a6240de3894a3603ed9e187ad6360b6b013b8b", + "ip": "172.17.0.4" + }, + "id": "1345506093dfed8dbcef610da476911a228ca315978e5464ae49fb1142bbc49b", + "screen": "1280x1024x24", + "vnc": true + } + ] } - ] } - } - }, - "firefox": { - "59.0": {}, - "63.0": {}, - "64.0": {}, - "latest": {} - }, - "internet explorer": { - "11": {} + }, + "firefox": { + "59.0": {}, + "63.0": {}, + "64.0": {}, + "latest": {} + }, + "internet explorer": { + "11": {} + }, + "safari": { + "latest": {} + } }, - "safari": { - "latest": {} + "pending": 0, + "queued": 0, + "total": 30, + "used": 1 } - }, - "pending": 0, - "queued": 0, - "total": 30, - "used": 1 - } + :return boolean (although in case of error in the request will be returned None) """ server_url_splitted = self.server_url.split(':') @@ -224,7 +228,7 @@ def download_session_video(self, scenario_name, timeout=5): """ download the execution video file if the scenario fails or the video is enabled, renaming the file to scenario name and removing the video file in the server. - GGR request: http://:@:/video/ + GGR request: http://:@:/video/ selenoid request: http://:@:/video/.mp4 :param scenario_name: scenario name :param timeout: threshold until the video file is downloaded @@ -249,7 +253,7 @@ def download_session_log(self, scenario_name, timeout=5): """ download the session log file from remote selenoid, renaming the file to scenario name and removing the log file in the server. - GGR request: http://:@:/logs/ + GGR request: http://:@:/logs/ selenoid request: http://:@:/logs/.log :param scenario_name: scenario name :param timeout: threshold until the log file is downloaded diff --git a/toolium/test/utils/test_dataset_replace_param.py b/toolium/test/utils/test_dataset_replace_param.py index 716caa08..8ef0055b 100644 --- a/toolium/test/utils/test_dataset_replace_param.py +++ b/toolium/test/utils/test_dataset_replace_param.py @@ -19,6 +19,8 @@ import datetime from uuid import UUID +import pytest + from toolium.utils import dataset from toolium.utils.dataset import replace_param @@ -333,18 +335,30 @@ def test_replace_param_now_offsets_with_and_without_format_and_more(): assert param == f'The date {offset_date} was yesterday and I have an appointment at {offset_datetime}' -def test_replace_param_round_with_type_inference(): - param = replace_param('[ROUND:7.5::2]') - assert param == 7.5 - param = replace_param('[ROUND:3.33333333::3]') - assert param == 3.333 - - -def test_replace_param_round_without_type_inference(): - param = replace_param('[ROUND:7.500::2]', infer_param_type=False) - assert param == '7.50' - param = replace_param('[ROUND:3.33333333::3]', infer_param_type=False) - assert param == '3.333' +@pytest.mark.parametrize('in_param, number_of_digits_in_fractional_part, out_param', + [['7.5', '2', 7.5], + ['3.33333333', '3', 3.333], + ['123', '5', 123.0], + ['0.001', '2', 0.0], + ['0.4', '0', 0], + ['0.6', '0', 1] + ]) +def test_replace_param_round_with_type_inference(in_param, number_of_digits_in_fractional_part, out_param): + param = replace_param(f'[ROUND:{in_param}::{number_of_digits_in_fractional_part}]') + assert param == out_param + + +@pytest.mark.parametrize('in_param, number_of_digits_in_fractional_part, out_param', + [['7.5', '2', '7.50'], + ['3.33333333', '3', '3.333'], + ['123', '5', '123.00000'], + ['0.001', '2', '0.00'], + ['0.4', '0', '0'], + ['0.6', '0', '1'] + ]) +def test_replace_param_round_without_type_inference(in_param, number_of_digits_in_fractional_part, out_param): + param = replace_param(f'[ROUND:{in_param}::{number_of_digits_in_fractional_part}]', infer_param_type=False) + assert param == out_param def test_replace_param_str_int(): diff --git a/toolium/utils/dataset.py b/toolium/utils/dataset.py index 58ab089d..a86dd97b 100644 --- a/toolium/utils/dataset.py +++ b/toolium/utils/dataset.py @@ -56,41 +56,43 @@ def replace_param(param, language='es', infer_param_type=True): """ Apply transformations to the given param based on specific patterns. Available replacements: - [STRING_WITH_LENGTH_XX] Generates a fixed length string - [INTEGER_WITH_LENGTH_XX] Generates a fixed length integer - [STRING_ARRAY_WITH_LENGTH_XX] Generates a fixed length array of strings - [INTEGER_ARRAY_WITH_LENGTH_XX] Generates a fixed length array of integers - [JSON_WITH_LENGTH_XX] Generates a fixed length JSON - [MISSING_PARAM] Generates a None object - [NULL] Generates a None object - [TRUE] Generates a boolean True - [FALSE] Generates a boolean False - [EMPTY] Generates an empty string - [B] Generates a blank space - [UUID] Generates a v4 UUID - [RANDOM] Generates a random value - [RANDOM_PHONE_NUMBER] Generates a random phone number for language and country configured in dataset.language - and dataset.country - [TIMESTAMP] Generates a timestamp from the current time - [DATETIME] Generates a datetime from the current time - [NOW] Similar to DATETIME without milliseconds; the format depends on the language - [NOW(%Y-%m-%dT%H:%M:%SZ)] Same as NOW but using an specific format by the python strftime function of the - datetime module - [NOW + 2 DAYS] Similar to NOW but two days later - [NOW - 1 MINUTES] Similar to NOW but one minute earlier - [NOW(%Y-%m-%dT%H:%M:%SZ) - 7 DAYS] Similar to NOW but seven days before and with the indicated format - [TODAY] Similar to NOW without time; the format depends on the language - [TODAY + 2 DAYS] Similar to NOW, but two days later - [ROUND:xxxx::y] Generates a string from a float number (xxxx) with the indicated number of decimals (y) - [STR:xxxx] Cast xxxx to a string - [INT:xxxx] Cast xxxx to an int - [FLOAT:xxxx] Cast xxxx to a float - [LIST:xxxx] Cast xxxx to a list - [DICT:xxxx] Cast xxxx to a dict - [UPPER:xxxx] Converts xxxx to upper case - [LOWER:xxxx] Converts xxxx to lower case - [REPLACE:xxxxx::yy::zz] Replace elements in string. Example: [REPLACE:[CONTEXT:some_url]::https::http] - [TITLE:xxxxx] Apply .title() to string value. Example: [TITLE:the title] + + - [STRING_WITH_LENGTH_XX] Generates a fixed length string + - [INTEGER_WITH_LENGTH_XX] Generates a fixed length integer + - [STRING_ARRAY_WITH_LENGTH_XX] Generates a fixed length array of strings + - [INTEGER_ARRAY_WITH_LENGTH_XX] Generates a fixed length array of integers + - [JSON_WITH_LENGTH_XX] Generates a fixed length JSON + - [MISSING_PARAM] Generates a None object + - [NULL] Generates a None object + - [TRUE] Generates a boolean True + - [FALSE] Generates a boolean False + - [EMPTY] Generates an empty string + - [B] Generates a blank space + - [UUID] Generates a v4 UUID + - [RANDOM] Generates a random value + - [RANDOM_PHONE_NUMBER] Generates a random phone number for language and country configured + in dataset.language and dataset.country + - [TIMESTAMP] Generates a timestamp from the current time + - [DATETIME] Generates a datetime from the current time + - [NOW] Similar to DATETIME without milliseconds; the format depends on the language + - [NOW(%Y-%m-%dT%H:%M:%SZ)] Same as NOW but using an specific format by the python strftime function of + the datetime module + - [NOW + 2 DAYS] Similar to NOW but two days later + - [NOW - 1 MINUTES] Similar to NOW but one minute earlier + - [NOW(%Y-%m-%dT%H:%M:%SZ) - 7 DAYS] Similar to NOW but seven days before and with the indicated format + - [TODAY] Similar to NOW without time; the format depends on the language + - [TODAY + 2 DAYS] Similar to NOW, but two days later + - [ROUND:xxxx::y] Generates a string from a float number (xxxx) with the indicated number of decimals (y) + - [STR:xxxx] Cast xxxx to a string + - [INT:xxxx] Cast xxxx to an int + - [FLOAT:xxxx] Cast xxxx to a float + - [LIST:xxxx] Cast xxxx to a list + - [DICT:xxxx] Cast xxxx to a dict + - [UPPER:xxxx] Converts xxxx to upper case + - [LOWER:xxxx] Converts xxxx to lower case + - [REPLACE:xxxxx::yy::zz] Replace elements in string. Example: [REPLACE:[CONTEXT:some_url]::https::http] + - [TITLE:xxxxx] Apply .title() to string value. Example: [TITLE:the title] + If infer_param_type is True and the result of the replacement process is a string, this function also tries to infer and cast the result to the most appropriate data type, attempting first the direct conversion to a Python built-in data type and then, @@ -437,18 +439,19 @@ def map_one_param(param): """ Analyze the pattern in the given string and find out its transformed value. Available tags and replacement values: - [CONF:xxxx] Value from the config dict in project_config global variable for the key xxxx (dot notation is used - for keys, e.g. key_1.key_2.0.key_3) - [LANG:xxxx] String from the texts dict in language_terms global variable for the key xxxx, using the language - specified in language global variable (dot notation is used for keys, e.g. button.label) - [POE:xxxx] Definition(s) from the POEditor terms list in poeditor_terms global variable for the term xxxx - [TOOLIUM:xxxx] Value from the toolium config in toolium_config global variable for the key xxxx (key format is - section_option, e.g. Driver_type) - [CONTEXT:xxxx] Value from the behave context storage dict in behave_context global variable for the key xxxx, or - value of the behave context attribute xxxx, if the former does not exist - [ENV:xxxx] Value of the OS environment variable xxxx - [FILE:xxxx] String with the content of the file in the path xxxx - [BASE64:xxxx] String with the base64 representation of the file content in the path xxxx + + - [CONF:xxxx] Value from the config dict in project_config global variable for the key xxxx (dot notation is used + for keys, e.g. key_1.key_2.0.key_3) + - [LANG:xxxx] String from the texts dict in language_terms global variable for the key xxxx, using the language + specified in language global variable (dot notation is used for keys, e.g. button.label) + - [POE:xxxx] Definition(s) from the POEditor terms list in poeditor_terms global variable for the term xxxx + - [TOOLIUM:xxxx] Value from the toolium config in toolium_config global variable for the key xxxx (key format is + section_option, e.g. Driver_type) + - [CONTEXT:xxxx] Value from the behave context storage dict in behave_context global variable for the key xxxx, or + value of the behave context attribute xxxx, if the former does not exist + - [ENV:xxxx] Value of the OS environment variable xxxx + - [FILE:xxxx] String with the content of the file in the path xxxx + - [BASE64:xxxx] String with the base64 representation of the file content in the path xxxx :param param: string parameter :return: transformed value or the original string if no transformation could be applied @@ -525,19 +528,22 @@ def map_json_param(param, config, copy=True): """ Find the value of the given param using it as a key in the given dictionary. Dot notation is used, so for example "service.vamps.user" could be used to retrieve the email in the following config example: - { - "services":{ - "vamps":{ - "user": "cyber-sec-user@11paths.com", - "password": "MyPassword" + + .. code-block:: json + + { + "services":{ + "vamps":{ + "user": "cyber-sec-user@11paths.com", + "password": "MyPassword" + } } } - } :param param: key to be searched (dot notation is used, e.g. "service.vamps.user"). :param config: configuration dictionary :param copy: boolean value to indicate whether to work with a copy of the given dictionary or not, - in which case, the dictionary content might be changed by this function (True by default) + in which case, the dictionary content might be changed by this function (True by default) :return: mapped value """ properties_list = param.split(".") @@ -630,18 +636,19 @@ def get_value_from_context(param, context): If the resolved element at one of the tokens is a list and the next token is a key=value expression, then the element in the list that matches the key=value expression is selected, e.g. "list.key=value" returns the element in the list "list" that has the value for key attribute. So, for example, if the list is: - [ - {"key": "value1", "attr": "attr1"}, - {"key": "value2", "attr": "attr2"} - ] + + .. code-block:: json + + [ + {"key": "value1", "attr": "attr1"}, + {"key": "value2", "attr": "attr2"} + ] + then "list.key=value2" returns the second element in the list. Also does "list.'key'='value2'", "list.'key'=\"value2\"", "list.\"key\"='value2'" or "list.\"key\"=\"value2\"". There is not limit in the nested levels of dotted tokens, so a key like a.b.c.d will be tried to be resolved as: - - context.storage['a'].b.c.d - or - context.a.b.c.d + context.storage['a'].b.c.d or context.a.b.c.d :param param: key to be searched (e.g. "last_request_result" / "last_request.result") :param context: behave context diff --git a/toolium/utils/driver_wait_utils.py b/toolium/utils/driver_wait_utils.py index 87f2aa06..ff4a2fbd 100644 --- a/toolium/utils/driver_wait_utils.py +++ b/toolium/utils/driver_wait_utils.py @@ -306,7 +306,7 @@ def wait_until_element_stops(self, element, times=1000, timeout=None): :param element: PageElement or element locator as a tuple (locator_type, locator_value) to be found :param times: number of iterations checking the element's location that must be the same for all of them - in order to considering the element has stopped + in order to considering the element has stopped :returns: the web element if the element is stopped :rtype: selenium.webdriver.remote.webelement.WebElement or appium.webdriver.webelement.WebElement :raises TimeoutException: If the element does not stop after the timeout