Skip to content

Commit

Permalink
Merge branch 'master' into update_swipe_none_to_int
Browse files Browse the repository at this point in the history
  • Loading branch information
AlainMiranda authored Feb 29, 2024
2 parents 5716ddb + 5720890 commit 7349b94
Show file tree
Hide file tree
Showing 14 changed files with 238 additions and 29 deletions.
2 changes: 1 addition & 1 deletion .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@ jobs:
runs-on: ubuntu-latest
strategy:
matrix:
python-version: ['3.8', '3.9', '3.10', '3.11']
python-version: ['3.8', '3.9', '3.10', '3.11', '3.12']
fail-fast: false

steps:
Expand Down
29 changes: 27 additions & 2 deletions CHANGELOG.rst
Original file line number Diff line number Diff line change
@@ -1,14 +1,39 @@
Toolium Changelog
=================

v3.1.1
v3.1.4
------

*Release date: In development*

- Upgrade Sphinx version from 4.* to 7.* to fix readthedocs theme format
- Add `ignore_empty` optional parameter to POEditor configuration to ignore empty translations
- Fix on swipe method. Duration from None to 0.

v3.1.3
------

*Release date: 2024-02-06*

- Fix `PageElements` class initialization when custom page element classes don't have all optional attributes

v3.1.2
------

*Release date: 2024-02-05*

- Upgrade Pillow version to 10.1.* to fix compatibility with Python 3.12

v3.1.1
------

*Release date: 2024-02-02*

- Add support for Python 3.12
- Upgrade Sphinx version from 4.* to 7.* to fix readthedocs theme format
- Upgrade readthedocs-sphinx-search to 0.3.2 to fix security vulnerability
- Do not log warning messages when toolium system properties are used
- Allow to initialize a `PageElements` class with webview attributes

v3.1.0
------

Expand Down
2 changes: 1 addition & 1 deletion VERSION
Original file line number Diff line number Diff line change
@@ -1 +1 @@
3.1.1.dev0
3.1.4.dev0
2 changes: 1 addition & 1 deletion docs/requirements.txt
Original file line number Diff line number Diff line change
@@ -1,3 +1,3 @@
Sphinx~=7.2
sphinx_rtd_theme~=1.3
readthedocs-sphinx-search~=0.3
readthedocs-sphinx-search~=0.3.2
4 changes: 2 additions & 2 deletions requirements.txt
Original file line number Diff line number Diff line change
@@ -1,8 +1,8 @@
requests~=2.27 # api tests
selenium~=4.0 # web tests
Appium-Python-Client~=2.3 # mobile tests
Pillow~=9.4 # visual testing
Pillow~=10.1 # visual testing
screeninfo~=0.8
lxml==4.9.2
lxml~=5.1
Faker~=18.3
phonenumbers~=8.13
1 change: 1 addition & 0 deletions setup.py
Original file line number Diff line number Diff line change
Expand Up @@ -67,6 +67,7 @@ def get_long_description():
'Programming Language :: Python :: 3.9',
'Programming Language :: Python :: 3.10',
'Programming Language :: Python :: 3.11',
'Programming Language :: Python :: 3.12',
'Topic :: Software Development :: Libraries :: Python Modules',
'Topic :: Software Development :: Quality Assurance',
'Topic :: Software Development :: Testing',
Expand Down
9 changes: 7 additions & 2 deletions toolium/config_parser.py
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,11 @@
logger = logging.getLogger(__name__)


SPECIAL_SYSTEM_PROPERTIES = ['TOOLIUM_CONFIG_ENVIRONMENT', 'TOOLIUM_OUTPUT_DIRECTORY', 'TOOLIUM_OUTPUT_LOG_FILENAME',
'TOOLIUM_CONFIG_DIRECTORY', 'TOOLIUM_CONFIG_LOG_FILENAME',
'TOOLIUM_CONFIG_PROPERTIES_FILENAMES', 'TOOLIUM_VISUAL_BASELINE_DIRECTORY']


class ExtendedConfigParser(ConfigParser):
def optionxform(self, optionstr):
"""Override default optionxform in ConfigParser to allow case sensitive options"""
Expand Down Expand Up @@ -120,10 +125,10 @@ def update_toolium_system_properties(self, new_properties):
if not self.has_section(section):
self.add_section(section)
self.set(section, option, value)
elif property_name.startswith('TOOLIUM'):
elif property_name.startswith('TOOLIUM') and property_name not in SPECIAL_SYSTEM_PROPERTIES:
logger.warning('A toolium system property is configured but its name does not math with section'
' and option in value (use TOOLIUM_[SECTION]_[OPTION]=[Section]_[option]=value):'
' %s=%s' % (property_name, property_value))
' %s=%s', property_name, property_value)

def translate_config_variables(self, str_with_variables):
"""
Expand Down
15 changes: 9 additions & 6 deletions toolium/pageelements/page_elements.py
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,8 @@
See the License for the specific language governing permissions and
limitations under the License.
"""
from typing import List, Any
import inspect
from typing import Any, List

from toolium.driver_wrapper import DriverWrappersPool
from toolium.pageelements.button_page_element import Button
Expand Down Expand Up @@ -120,11 +121,13 @@ def page_elements(self) -> List[Any]:
self._page_elements = []
for order, web_element in enumerate(self.web_elements):
# Create multiple PageElement with original locator and order
page_element =\
self.page_element_class(self.locator[0], self.locator[1], parent=self.parent,
order=order, webview=self.webview,
webview_context_selection_callback=self.webview_context_selection_callback,
webview_csc_args=self.webview_csc_args)
# Optional parameters are passed only if they are defined in the PageElement constructor
signature = inspect.signature(self.page_element_class.__init__)
opt_names = ['parent', 'webview', 'webview_context_selection_callback', 'webview_csc_args']
opt_params = {name: getattr(self, name) for name in opt_names if name in signature.parameters}
if 'order' in signature.parameters:
opt_params['order'] = order
page_element = self.page_element_class(self.locator[0], self.locator[1], **opt_params)
page_element.reset_object(self.driver_wrapper)
page_element._web_element = web_element
self._page_elements.append(page_element)
Expand Down
116 changes: 107 additions & 9 deletions toolium/test/pageelements/test_page_elements.py
Original file line number Diff line number Diff line change
Expand Up @@ -36,13 +36,43 @@ def init_page_elements(self):
self.links = PageElements(By.XPATH, '//a')
self.inputs_with_parent = PageElements(By.XPATH, '//input', parent=(By.ID, 'parent'))
self.inputs_with_webview = PageElements(By.XPATH, '//input', webview=True)
self.inputs_with_webview_callback = \
PageElements(By.XPATH, '//input', webview_context_selection_callback=lambda a, b: (a, b),
webview_csc_args=['WEBVIEW_fake.other', "CDwindow-0123456789"], webview=True)
self.inputs_with_webview_callback = PageElements(By.XPATH, '//input',
webview_context_selection_callback=lambda a, b: (a, b),
webview_csc_args=['WEBVIEW_fake.other', "CDwindow-0123456789"],
webview=True)
self.parent_webview = PageElement(By.XPATH, '//parent', webview=True)
self.inputs_with_webview_parent = PageElements(By.XPATH, '//input', parent=self.parent_webview, webview=True)


class CustomElementAllAttributes(PageElement):
def __init__(self, by, value, parent=None, order=None, wait=False, shadowroot=None, webview=False,
webview_context_selection_callback=None, webview_csc_args=None):
super(CustomElementAllAttributes, self).__init__(by, value, parent, order, wait, shadowroot, webview,
webview_context_selection_callback, webview_csc_args)


class CustomElementSomeAttributes(PageElement):
def __init__(self, by, value, parent=None, order=None, wait=False, shadowroot=None):
super(CustomElementSomeAttributes, self).__init__(by, value, parent, order, wait, shadowroot)


class CustomElementMandatoryAttributes(PageElement):
def __init__(self, by, value):
super(CustomElementMandatoryAttributes, self).__init__(by, value)


class LoginWithPageElementsPageObject(PageObject):
def init_page_elements(self):
self.all_optional_attrs = PageElements(By.XPATH, '//input', page_element_class=CustomElementAllAttributes,
webview_context_selection_callback=lambda a, b: (a, b),
webview_csc_args=['WEBVIEW_fake.other', "CDwindow-0123456789"],
webview=True)
self.some_optional_attrs = PageElements(By.XPATH, '//input', page_element_class=CustomElementSomeAttributes,
parent=(By.ID, 'parent'))
self.only_mandatory_attrs = PageElements(By.XPATH, '//input',
page_element_class=CustomElementMandatoryAttributes)


@pytest.fixture
def driver_wrapper():
# Reset wrappers pool values
Expand Down Expand Up @@ -171,13 +201,81 @@ def test_reset_object(driver_wrapper):
assert page_element_21._web_element is not None


def test_get_page_elements_custom_element_class_all_optional(driver_wrapper):
driver_wrapper.driver.find_elements.return_value = child_elements
page_elements = LoginWithPageElementsPageObject().all_optional_attrs.page_elements

# Check that find_elements has been called just one time
driver_wrapper.driver.find_elements.assert_called_once_with(By.XPATH, '//input')
driver_wrapper.driver.find_element.assert_not_called()

# Check that the response is a list of 2 CustomElementAllFields with the expected web element
assert len(page_elements) == 2
assert isinstance(page_elements[0], CustomElementAllAttributes)
assert page_elements[0]._web_element == child_elements[0]
assert isinstance(page_elements[1], CustomElementAllAttributes)
assert page_elements[1]._web_element == child_elements[1]

# Check that the optional attributes are set correctly
assert page_elements[0].order == 0
assert page_elements[0].webview is True
assert page_elements[0].webview_context_selection_callback
assert page_elements[0].webview_csc_args == ['WEBVIEW_fake.other', "CDwindow-0123456789"]
assert page_elements[1].order == 1
assert page_elements[1].webview is True
assert page_elements[1].webview_context_selection_callback
assert page_elements[1].webview_csc_args == ['WEBVIEW_fake.other', "CDwindow-0123456789"]


def test_get_page_elements_custom_element_class_some_optional(driver_wrapper):
# Create a mock element
mock_element = mock.MagicMock(spec=WebElement)
mock_element.find_elements.return_value = child_elements

driver_wrapper.driver.find_element.return_value = mock_element
page_elements = LoginWithPageElementsPageObject().some_optional_attrs.page_elements

# Check that find_elements has been called just one time
driver_wrapper.driver.find_element.assert_called_once_with(By.ID, 'parent')
mock_element.find_elements.assert_called_once_with(By.XPATH, '//input')

# Check that the response is a list of 2 CustomElementSomeAttributes with the expected web element
assert len(page_elements) == 2
assert isinstance(page_elements[0], CustomElementSomeAttributes)
assert page_elements[0]._web_element == child_elements[0]
assert isinstance(page_elements[1], CustomElementSomeAttributes)
assert page_elements[1]._web_element == child_elements[1]

# Check that the optional attributes are set correctly
assert page_elements[0].order == 0
assert page_elements[0].parent == (By.ID, 'parent')
assert page_elements[1].order == 1
assert page_elements[0].parent == (By.ID, 'parent')


def test_get_page_elements_custom_element_class_only_mandatory(driver_wrapper):
driver_wrapper.driver.find_elements.return_value = child_elements
page_elements = LoginWithPageElementsPageObject().only_mandatory_attrs.page_elements

# Check that find_elements has been called just one time
driver_wrapper.driver.find_elements.assert_called_once_with(By.XPATH, '//input')
driver_wrapper.driver.find_element.assert_not_called()

# Check that the response is a list of 2 CustomElementMandatoryAttributes with the expected web element
assert len(page_elements) == 2
assert isinstance(page_elements[0], CustomElementMandatoryAttributes)
assert page_elements[0]._web_element == child_elements[0]
assert isinstance(page_elements[1], CustomElementMandatoryAttributes)
assert page_elements[1]._web_element == child_elements[1]


def test_get_page_elements_without_webview(driver_wrapper):
driver_wrapper.driver.find_elements.return_value = child_elements
page_elements = LoginPageObject().inputs.page_elements

# Check webview attribute is set to false by default in child elements
assert not page_elements[0].webview
assert not page_elements[1].webview
assert page_elements[0].webview is False
assert page_elements[1].webview is False


def test_get_page_elements_with_webview(driver_wrapper):
Expand All @@ -186,8 +284,8 @@ def test_get_page_elements_with_webview(driver_wrapper):

# Check webview attribute is set to true in child element when a Pagelements element
# is created with the webview attribute
assert page_elements[0].webview
assert page_elements[1].webview
assert page_elements[0].webview is True
assert page_elements[1].webview is True


def test_get_page_elements_with_context_selection_callback_provided(driver_wrapper):
Expand All @@ -196,9 +294,9 @@ def test_get_page_elements_with_context_selection_callback_provided(driver_wrapp

# Check context selection callback provided is set correctly to pageelements
assert page_elements[0].webview_context_selection_callback
assert page_elements[0].webview_csc_args
assert page_elements[0].webview_csc_args == ['WEBVIEW_fake.other', "CDwindow-0123456789"]
assert page_elements[1].webview_context_selection_callback
assert page_elements[1].webview_csc_args
assert page_elements[1].webview_csc_args == ['WEBVIEW_fake.other', "CDwindow-0123456789"]


def test_mobile_automatic_context_selection_switch_to_new_webview_context_in_pagelements_without_parent(driver_wrapper):
Expand Down
29 changes: 27 additions & 2 deletions toolium/test/test_config_parser.py
Original file line number Diff line number Diff line change
Expand Up @@ -16,8 +16,9 @@
limitations under the License.
"""

import mock
import os

import mock
import pytest

from toolium.config_parser import ExtendedConfigParser
Expand Down Expand Up @@ -285,11 +286,35 @@ def test_update_toolium_system_properties_wrong_format(config, logger, property_
if property_name.startswith('TOOLIUM'):
logger.warning.assert_called_once_with('A toolium system property is configured but its name does not math with'
' section and option in value (use TOOLIUM_[SECTION]_[OPTION]=[Section]_'
'[option]=value): %s=%s' % (property_name, property_value))
'[option]=value): %s=%s', property_name, property_value)
else:
logger.warning.assert_not_called()


toolium_system_properties_special = (
('TOOLIUM_CONFIG_ENVIRONMENT', 'value1'),
('TOOLIUM_OUTPUT_DIRECTORY', 'value2'),
('TOOLIUM_OUTPUT_LOG_FILENAME', 'value3'),
('TOOLIUM_CONFIG_DIRECTORY', 'value4'),
('TOOLIUM_CONFIG_LOG_FILENAME', 'value5'),
('TOOLIUM_CONFIG_PROPERTIES_FILENAMES', 'value6'),
('TOOLIUM_VISUAL_BASELINE_DIRECTORY', 'value7'),
)


@pytest.mark.parametrize("property_name, property_value", toolium_system_properties_special)
def test_update_toolium_system_properties_special(config, logger, property_name, property_value):
# Change system property and update config
environment_properties.append(property_name)
os.environ[property_name] = property_value
previous_config = config.deepcopy()
config.update_toolium_system_properties(os.environ)

# Check that config has not been updated and error message is not logged
assert previous_config == config, 'Config has been updated'
logger.warning.assert_not_called()


def test_update_properties_behave(config):
section = 'Capabilities'
option = 'platformName'
Expand Down
49 changes: 49 additions & 0 deletions toolium/test/utils/test_dataset_map_param.py
Original file line number Diff line number Diff line change
Expand Up @@ -212,6 +212,22 @@ def test_a_poe_param_single_result():
assert result == expected


def test_a_poe_param_with_empty_definition_single_result():
"""
Verification of a POE mapped parameter with empty definition
"""
dataset.poeditor_terms = [
{
"term": "Poniendo mute",
"definition": "",
"reference": "home:home.tv.mute",
}
]
result = map_param('[POE:home.tv.mute]')
expected = ""
assert result == expected


def test_a_poe_param_no_result_assertion():
"""
Verification of a POE mapped parameter without result
Expand All @@ -228,6 +244,39 @@ def test_a_poe_param_no_result_assertion():
assert "No translations found in POEditor for reference home.tv.off" in str(excinfo.value)


def test_a_poe_param_with_no_definition_no_result_assertion_():
"""
Verification of a POE mapped parameter without definition and without result
"""
dataset.poeditor_terms = [
{
"term": "Poniendo mute",
"definition": None,
"reference": "home:home.tv.mute",
}
]
with pytest.raises(Exception) as excinfo:
map_param('[POE:home.tv.mute]')
assert "No translations found in POEditor for reference home.tv.mute" in str(excinfo.value)


def test_a_poe_param_with_empty_definition_no_result_assertion():
"""
Verification of a POE mapped parameter with empty definition and without result (configured ignore_empty)
"""
dataset.project_config = {'poeditor': {'key_field': 'reference', 'search_type': 'contains', 'ignore_empty': True}}
dataset.poeditor_terms = [
{
"term": "Poniendo mute",
"definition": "",
"reference": "home:home.tv.mute",
}
]
with pytest.raises(Exception) as excinfo:
map_param('[POE:home.tv.mute]')
assert "No translations found in POEditor for reference home.tv.mute" in str(excinfo.value)


def test_a_poe_param_prefix_with_no_definition():
"""
Verification of a POE mapped parameter with a single result for a reference
Expand Down
Loading

0 comments on commit 7349b94

Please sign in to comment.