pyseext 0.1.0__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- pyseext-0.1.0/LICENSE +21 -0
- pyseext-0.1.0/MANIFEST.in +1 -0
- pyseext-0.1.0/PKG-INFO +43 -0
- pyseext-0.1.0/README.md +27 -0
- pyseext-0.1.0/pyproject.toml +16 -0
- pyseext-0.1.0/setup.cfg +4 -0
- pyseext-0.1.0/setup.py +25 -0
- pyseext-0.1.0/src/pseext/__init__.py +9 -0
- pyseext-0.1.0/src/pseext/button_helper.py +115 -0
- pyseext-0.1.0/src/pseext/component_query.py +259 -0
- pyseext-0.1.0/src/pseext/core.py +151 -0
- pyseext-0.1.0/src/pseext/field_helper.py +607 -0
- pyseext-0.1.0/src/pseext/form_helper.py +97 -0
- pyseext-0.1.0/src/pseext/grid_helper.py +755 -0
- pyseext-0.1.0/src/pseext/has_referenced_javascript.py +91 -0
- pyseext-0.1.0/src/pseext/input_helper.py +163 -0
- pyseext-0.1.0/src/pseext/local_storage_helper.py +84 -0
- pyseext-0.1.0/src/pseext/menu_helper.py +155 -0
- pyseext-0.1.0/src/pseext/observable_helper.py +108 -0
- pyseext-0.1.0/src/pseext/store_helper.py +145 -0
- pyseext-0.1.0/src/pseext/tree_helper.py +573 -0
- pyseext-0.1.0/src/pyseext.egg-info/PKG-INFO +43 -0
- pyseext-0.1.0/src/pyseext.egg-info/SOURCES.txt +24 -0
- pyseext-0.1.0/src/pyseext.egg-info/dependency_links.txt +1 -0
- pyseext-0.1.0/src/pyseext.egg-info/requires.txt +1 -0
- pyseext-0.1.0/src/pyseext.egg-info/top_level.txt +1 -0
pyseext-0.1.0/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2022 Martyn West
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
include LICENSE
|
pyseext-0.1.0/PKG-INFO
ADDED
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: pyseext
|
|
3
|
+
Version: 0.1.0
|
|
4
|
+
Summary: A short description
|
|
5
|
+
Home-page: https://github.com/westy/pyseext
|
|
6
|
+
Author: Martyn West
|
|
7
|
+
Author-email: 657393+westy@users.noreply.github.com
|
|
8
|
+
Requires-Python: >=3.8
|
|
9
|
+
Description-Content-Type: text/markdown
|
|
10
|
+
License-File: LICENSE
|
|
11
|
+
Requires-Dist: selenium
|
|
12
|
+
Dynamic: author
|
|
13
|
+
Dynamic: author-email
|
|
14
|
+
Dynamic: home-page
|
|
15
|
+
Dynamic: license-file
|
|
16
|
+
|
|
17
|
+
# PySeExt Module Repository
|
|
18
|
+
|
|
19
|
+
This project contains a package to aid the testing of ExtJS applications from Python using Selenium.
|
|
20
|
+
|
|
21
|
+
---
|
|
22
|
+
## Naming Standards
|
|
23
|
+
|
|
24
|
+
Full details [here](https://namingconvention.org/python/).
|
|
25
|
+
|
|
26
|
+
### TL;DR
|
|
27
|
+
**Type** | **Public** | **Internal**
|
|
28
|
+
--- | --- | ---
|
|
29
|
+
Packages | `lower_with_under` |
|
|
30
|
+
Modules | `lower_with_under` | `_lower_with_under`
|
|
31
|
+
Classes | `CapWords` | `_CapWords`
|
|
32
|
+
Exceptions | `CapWords` |
|
|
33
|
+
Functions | `lower_with_under()` | `_lower_with_under()`
|
|
34
|
+
Global/Class Constants | `CAPS_WITH_UNDER` | `_CAPS_WITH_UNDER`
|
|
35
|
+
Global/Class Variables | `lower_with_under` | `_lower_with_under`
|
|
36
|
+
Instance Variables | `lower_with_under` | `_lower_with_under`
|
|
37
|
+
Method Names | `lower_with_under()` | `_lower_with_under()`
|
|
38
|
+
Function/Method Parameters | `lower_with_under` |
|
|
39
|
+
Local Variables | `lower_with_under` |
|
|
40
|
+
|
|
41
|
+
### Additional Notes
|
|
42
|
+
I have also settled on a standard of having a single class per source file (that I now understand are called modules), although inner classes are allowed.
|
|
43
|
+
This is pretty much standard practice in other languages, and makes source control and managing conflicts far easier.
|
pyseext-0.1.0/README.md
ADDED
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
# PySeExt Module Repository
|
|
2
|
+
|
|
3
|
+
This project contains a package to aid the testing of ExtJS applications from Python using Selenium.
|
|
4
|
+
|
|
5
|
+
---
|
|
6
|
+
## Naming Standards
|
|
7
|
+
|
|
8
|
+
Full details [here](https://namingconvention.org/python/).
|
|
9
|
+
|
|
10
|
+
### TL;DR
|
|
11
|
+
**Type** | **Public** | **Internal**
|
|
12
|
+
--- | --- | ---
|
|
13
|
+
Packages | `lower_with_under` |
|
|
14
|
+
Modules | `lower_with_under` | `_lower_with_under`
|
|
15
|
+
Classes | `CapWords` | `_CapWords`
|
|
16
|
+
Exceptions | `CapWords` |
|
|
17
|
+
Functions | `lower_with_under()` | `_lower_with_under()`
|
|
18
|
+
Global/Class Constants | `CAPS_WITH_UNDER` | `_CAPS_WITH_UNDER`
|
|
19
|
+
Global/Class Variables | `lower_with_under` | `_lower_with_under`
|
|
20
|
+
Instance Variables | `lower_with_under` | `_lower_with_under`
|
|
21
|
+
Method Names | `lower_with_under()` | `_lower_with_under()`
|
|
22
|
+
Function/Method Parameters | `lower_with_under` |
|
|
23
|
+
Local Variables | `lower_with_under` |
|
|
24
|
+
|
|
25
|
+
### Additional Notes
|
|
26
|
+
I have also settled on a standard of having a single class per source file (that I now understand are called modules), although inner classes are allowed.
|
|
27
|
+
This is pretty much standard practice in other languages, and makes source control and managing conflicts far easier.
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
[build-system]
|
|
2
|
+
requires = ["setuptools", "wheel"]
|
|
3
|
+
build-backend = "setuptools.build_meta"
|
|
4
|
+
|
|
5
|
+
[project]
|
|
6
|
+
name = "pyseext"
|
|
7
|
+
version = "0.1.0"
|
|
8
|
+
description = "A short description"
|
|
9
|
+
readme = "README.md"
|
|
10
|
+
requires-python = ">=3.8"
|
|
11
|
+
dependencies = [
|
|
12
|
+
"selenium",
|
|
13
|
+
]
|
|
14
|
+
|
|
15
|
+
[tool.setuptools.packages.find]
|
|
16
|
+
where = ["src"]
|
pyseext-0.1.0/setup.cfg
ADDED
pyseext-0.1.0/setup.py
ADDED
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
# -*- coding: utf-8 -*-
|
|
2
|
+
|
|
3
|
+
# Learn more: https://github.com/kennethreitz/setup.py
|
|
4
|
+
|
|
5
|
+
from setuptools import setup, find_packages
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
with open('README.md', encoding = 'utf-8') as f:
|
|
9
|
+
readme = f.read()
|
|
10
|
+
|
|
11
|
+
with open('LICENSE', encoding = 'utf-8') as f:
|
|
12
|
+
license_text = f.read()
|
|
13
|
+
|
|
14
|
+
setup(
|
|
15
|
+
name='pyseext',
|
|
16
|
+
version='1.4.1',
|
|
17
|
+
description='Python Selenium ExtJS - package for helping interact with an ExtJS application from Python using Selenium',
|
|
18
|
+
long_description=readme,
|
|
19
|
+
author='Martyn West',
|
|
20
|
+
author_email='657393+westy@users.noreply.github.com',
|
|
21
|
+
url='https://github.com/westy/pyseext',
|
|
22
|
+
license=license_text,
|
|
23
|
+
packages=find_packages(exclude=('tests', 'docs')),
|
|
24
|
+
package_data={'pyseext': ['js/*.js']}
|
|
25
|
+
)
|
|
@@ -0,0 +1,115 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Module that contains our ButtonHelper class.
|
|
3
|
+
"""
|
|
4
|
+
|
|
5
|
+
import logging
|
|
6
|
+
from typing import Union
|
|
7
|
+
|
|
8
|
+
from selenium.webdriver.common.action_chains import ActionChains
|
|
9
|
+
from selenium.webdriver.remote.webdriver import WebDriver
|
|
10
|
+
|
|
11
|
+
from pyseext.component_query import ComponentQuery
|
|
12
|
+
|
|
13
|
+
class ButtonHelper:
|
|
14
|
+
"""A class to help with interacting with Ext buttons"""
|
|
15
|
+
|
|
16
|
+
# Class variables
|
|
17
|
+
_ENABLED_BUTTON_TEMPLATE: str = 'button[text="{text}"][disabled=false]'
|
|
18
|
+
"""The component query template to use to find an enabled button.
|
|
19
|
+
Requires the inserts: {text}"""
|
|
20
|
+
|
|
21
|
+
_DISABLED_BUTTON_TEMPLATE: str = 'button[text="{text}"][disabled=true]'
|
|
22
|
+
"""The component query template to use to find a disabled button.
|
|
23
|
+
Requires the inserts: {text}"""
|
|
24
|
+
|
|
25
|
+
_MESSAGEBOX_BUTTON_TEMPLATE: str = 'messagebox{{isVisible(true)}} button[text="{text}"]'
|
|
26
|
+
"""The component query template to use to find a button on a visible message box.
|
|
27
|
+
Requires the inserts: {text}"""
|
|
28
|
+
|
|
29
|
+
def __init__(self, driver: WebDriver):
|
|
30
|
+
"""Initialises an instance of this class
|
|
31
|
+
|
|
32
|
+
Args:
|
|
33
|
+
driver (WebDriver): The webdriver to use
|
|
34
|
+
"""
|
|
35
|
+
# Instance variables
|
|
36
|
+
self._logger = logging.getLogger(__name__)
|
|
37
|
+
"""The Logger instance for this class instance"""
|
|
38
|
+
|
|
39
|
+
self._cq = ComponentQuery(driver)
|
|
40
|
+
"""The `ComponentQuery` instance for this class instance"""
|
|
41
|
+
|
|
42
|
+
self._action_chains = ActionChains(driver)
|
|
43
|
+
"""The ActionChains instance for this class instance"""
|
|
44
|
+
|
|
45
|
+
def click_button(self, cq: str, root_id: Union[str, None] = None):
|
|
46
|
+
"""Finds a button using the supplied component query and clicks it.
|
|
47
|
+
|
|
48
|
+
Args:
|
|
49
|
+
cq (str): The component query to find the button.
|
|
50
|
+
root_id (str, optional): The id of the container within which to perform the query.
|
|
51
|
+
If omitted, all components within the document are included in the search.
|
|
52
|
+
"""
|
|
53
|
+
button = self._cq.wait_for_single_query_visible(cq, root_id)
|
|
54
|
+
|
|
55
|
+
# Rather than call click, move mouse to button and click...
|
|
56
|
+
self._logger.info("Clicking button with CQ '%s'", cq)
|
|
57
|
+
|
|
58
|
+
self._action_chains.move_to_element(button)
|
|
59
|
+
self._action_chains.click()
|
|
60
|
+
self._action_chains.perform()
|
|
61
|
+
|
|
62
|
+
def click_button_by_text(self, text: str, root_id: Union[str, None] = None):
|
|
63
|
+
"""Finds a visible, enabled button with the specified text and clicks it.
|
|
64
|
+
|
|
65
|
+
Args:
|
|
66
|
+
text (str): The text on the button
|
|
67
|
+
root_id (str, optional): The id of the container within which to perform the query.
|
|
68
|
+
If omitted, all components within the document are included in the search.
|
|
69
|
+
"""
|
|
70
|
+
self.click_button(self._ENABLED_BUTTON_TEMPLATE.format(text=text), root_id)
|
|
71
|
+
|
|
72
|
+
def check_button_enabled(self, text: str, root_id: Union[str, None] = None):
|
|
73
|
+
"""Checks that we can find an enabled button with the specified text.
|
|
74
|
+
|
|
75
|
+
Args:
|
|
76
|
+
text (str): The text on the button
|
|
77
|
+
root_id (str, optional): The id of the container within which to perform the query.
|
|
78
|
+
If omitted, all components within the document are included in the search.
|
|
79
|
+
"""
|
|
80
|
+
self._cq.wait_for_single_query(self._ENABLED_BUTTON_TEMPLATE.format(text=text), root_id)
|
|
81
|
+
|
|
82
|
+
def check_button_disabled(self, text: str, root_id: Union[str, None] = None):
|
|
83
|
+
"""Checks that we can find a disabled button with the specified text.
|
|
84
|
+
|
|
85
|
+
Args:
|
|
86
|
+
text (str): The text on the button
|
|
87
|
+
root_id (str, optional): The id of the container within which to perform the query.
|
|
88
|
+
If omitted, all components within the document are included in the search.
|
|
89
|
+
"""
|
|
90
|
+
self._cq.wait_for_single_query(self._DISABLED_BUTTON_TEMPLATE.format(text=text), root_id)
|
|
91
|
+
|
|
92
|
+
def click_button_on_messagebox(self, text: str = 'OK'):
|
|
93
|
+
"""Clicks a button on a messagebox.
|
|
94
|
+
|
|
95
|
+
The messagebox must be visible.
|
|
96
|
+
|
|
97
|
+
Args:
|
|
98
|
+
text (str, optional): The text of the button to click. Defaults to 'OK'.
|
|
99
|
+
"""
|
|
100
|
+
self.click_button(self._MESSAGEBOX_BUTTON_TEMPLATE.format(text=text))
|
|
101
|
+
|
|
102
|
+
def click_button_arrow(self, button_text: str, root_id: Union[str, None] = None, css_selector: str = ".x-btn-arrow-el"):
|
|
103
|
+
"""Clicks the dropdown arrow of a split button to open its menu.
|
|
104
|
+
|
|
105
|
+
Args:
|
|
106
|
+
button_text (str): The text on the button
|
|
107
|
+
root_id (str, optional): The id of the container within which to perform the query.
|
|
108
|
+
css_selector (str, optional): The CSS selector to find the arrow element within the button. Defaults to ".x-btn-arrow-el".
|
|
109
|
+
"""
|
|
110
|
+
arrow = self._cq.wait_for_single_query_visible(cq = button_text, root_id = root_id, css_selector = css_selector)
|
|
111
|
+
|
|
112
|
+
# Move to the button and click on the arrow area
|
|
113
|
+
self._action_chains.move_to_element(arrow)
|
|
114
|
+
self._action_chains.click()
|
|
115
|
+
self._action_chains.perform()
|
|
@@ -0,0 +1,259 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Module that contains our ComponentQuery class.
|
|
3
|
+
"""
|
|
4
|
+
import logging
|
|
5
|
+
from typing import Union
|
|
6
|
+
|
|
7
|
+
from selenium.common.exceptions import TimeoutException
|
|
8
|
+
from selenium.webdriver.remote.webdriver import WebDriver
|
|
9
|
+
from selenium.webdriver.remote.webelement import WebElement
|
|
10
|
+
from selenium.webdriver.support.wait import WebDriverWait
|
|
11
|
+
|
|
12
|
+
from pyseext.has_referenced_javascript import HasReferencedJavaScript
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
class ComponentQuery(HasReferencedJavaScript):
|
|
16
|
+
"""A class to help with using Ext.ComponentQuery"""
|
|
17
|
+
|
|
18
|
+
# Class variables
|
|
19
|
+
_QUERY_TEMPLATE: str = "return globalThis.PySeExt.ComponentQuery.query('{cq}')"
|
|
20
|
+
"""The script template to use to execute a component query.
|
|
21
|
+
Requires the inserts: {cq}"""
|
|
22
|
+
|
|
23
|
+
_QUERY_TEMPLATE_WITH_ROOT: str = "return globalThis.PySeExt.ComponentQuery.query('{cq}', '{root_id}')"
|
|
24
|
+
"""The script template to use to execute a component query under a specified root.
|
|
25
|
+
Requires the inserts: {cq}, {root_id}"""
|
|
26
|
+
|
|
27
|
+
_QUERY_TEMPLATE_WITH_CSS_SELECTOR: str = "return globalThis.PySeExt.ComponentQuery.query('{cq}', undefined, '{css_selector}')"
|
|
28
|
+
"""The script template to use to execute a component query, and then execute a CSS selector query against each matched element.
|
|
29
|
+
Requires the inserts: {cq}, {css_selector}"""
|
|
30
|
+
|
|
31
|
+
_QUERY_TEMPLATE_WITH_ROOT_AND_CSS_SELECTOR: str = "return globalThis.PySeExt.ComponentQuery.query('{cq}', '{root_id}', '{css_selector}')"
|
|
32
|
+
"""The script template to use to execute a component query under a specified root, and then execute a CSS selector query against each matched element.
|
|
33
|
+
Requires the inserts: {cq}, {root_id}, {css_selector}"""
|
|
34
|
+
|
|
35
|
+
_IS_COMPONENT_INSTANCE_OF_CLASS_TEMPLATE: str = "return globalThis.PySeExt.ComponentQuery.isComponentInstanceOf('{class_name}', '{cq}')"
|
|
36
|
+
"""The script template to use to determine whether a component query matches a component of the specified class.
|
|
37
|
+
Requires the inserts: {class_name}, {cq}"""
|
|
38
|
+
|
|
39
|
+
_IS_COMPONENT_INSTANCE_OF_CLASS_TEMPLATE_WITH_ROOT: str = "return globalThis.PySeExt.ComponentQuery.isComponentInstanceOf('{class_name}', '{cq}', '{root_id}')"
|
|
40
|
+
"""The script template to use to determine whether a component query matches a component of the specified class.
|
|
41
|
+
Requires the inserts: {class_name}, {cq}, {root_id}"""
|
|
42
|
+
|
|
43
|
+
def __init__(self, driver: WebDriver):
|
|
44
|
+
"""Initialises an instance of this class
|
|
45
|
+
|
|
46
|
+
Args:
|
|
47
|
+
driver (WebDriver): The webdriver to use
|
|
48
|
+
"""
|
|
49
|
+
# Instance variables
|
|
50
|
+
self._logger = logging.getLogger(__name__)
|
|
51
|
+
"""The logger instance for this class instance"""
|
|
52
|
+
|
|
53
|
+
self._driver = driver
|
|
54
|
+
"""The WebDriver instance for this class instance"""
|
|
55
|
+
|
|
56
|
+
# Initialise our base class
|
|
57
|
+
super().__init__(driver, self._logger)
|
|
58
|
+
|
|
59
|
+
def query(self, cq: str, root_id: Union[str, None] = None, css_selector: Union[str, None] = None) -> list[WebElement]:
|
|
60
|
+
"""Executes a ComponentQuery and returns the result
|
|
61
|
+
|
|
62
|
+
Args:
|
|
63
|
+
cq (str): The query to execute
|
|
64
|
+
root_id (str, optional): The id of the container within which to perform the query.
|
|
65
|
+
If omitted, all components within the document are included in the search.
|
|
66
|
+
css_selector (str, optional): An optional CSS selector that can be used to get child elements of a found component,
|
|
67
|
+
e.g. a clear trigger on a field would be '.x-form-clear-trigger'.
|
|
68
|
+
Returns:
|
|
69
|
+
list[WebElement]: An array of DOM elements that match the query or an empty array if not found
|
|
70
|
+
"""
|
|
71
|
+
if root_id is None and css_selector is None:
|
|
72
|
+
self._logger.debug("Executing CQ '%s'", cq)
|
|
73
|
+
script = self._QUERY_TEMPLATE.format(cq=cq)
|
|
74
|
+
elif css_selector is None:
|
|
75
|
+
self._logger.debug("Executing CQ '%s' under root '%s'", cq, root_id)
|
|
76
|
+
script = self._QUERY_TEMPLATE_WITH_ROOT.format(cq=cq, root_id=root_id)
|
|
77
|
+
elif root_id is None:
|
|
78
|
+
self._logger.debug("Executing CQ '%s' with CSS selector '%s'", cq, css_selector)
|
|
79
|
+
script = self._QUERY_TEMPLATE_WITH_CSS_SELECTOR.format(cq=cq, css_selector=css_selector)
|
|
80
|
+
else:
|
|
81
|
+
self._logger.debug("Executing CQ '%s' under root '%s' with CSS selector '%s'", cq, root_id, css_selector)
|
|
82
|
+
script = self._QUERY_TEMPLATE_WITH_ROOT_AND_CSS_SELECTOR.format(cq=cq, root_id=root_id, css_selector=css_selector)
|
|
83
|
+
|
|
84
|
+
self.ensure_javascript_loaded()
|
|
85
|
+
query_result = self._driver.execute_script(script)
|
|
86
|
+
|
|
87
|
+
self._logger.debug("CQ '%s' gave results: %s", cq, query_result)
|
|
88
|
+
|
|
89
|
+
return query_result
|
|
90
|
+
|
|
91
|
+
def wait_for_query(self, cq: str, root_id: Union[str, None] = None, timeout: float = 10, throw_if_not_found: bool = True, css_selector: Union[str, None] = None) -> list[WebElement]:
|
|
92
|
+
"""Method that waits for the specified CQ to match something
|
|
93
|
+
|
|
94
|
+
Args:
|
|
95
|
+
cq (str): The query to execute
|
|
96
|
+
root_id (str, optional): The id of the container within which to perform the query.
|
|
97
|
+
If omitted, all components within the document are included in the search.
|
|
98
|
+
timeout (float): Number of seconds before timing out (default 10)
|
|
99
|
+
throw_if_not_found (bool): Indicates whether to throw an exception if not found (default True).
|
|
100
|
+
css_selector (str, optional): An optional CSS selector that can be used to get child elements of a found component,
|
|
101
|
+
e.g. a clear trigger on a field would be '.x-form-clear-trigger'.
|
|
102
|
+
|
|
103
|
+
Returns:
|
|
104
|
+
list[WebElement]: An array of DOM elements that match the query or an empty array if not found (and not configured to throw)
|
|
105
|
+
"""
|
|
106
|
+
try:
|
|
107
|
+
WebDriverWait(self._driver, timeout).until(ComponentQuery.ComponentQueryFoundExpectation(cq))
|
|
108
|
+
return self.query(cq, root_id, css_selector)
|
|
109
|
+
except TimeoutException as exc:
|
|
110
|
+
if throw_if_not_found:
|
|
111
|
+
raise ComponentQuery.QueryNotFoundException(cq, timeout, root_id) from exc
|
|
112
|
+
|
|
113
|
+
return []
|
|
114
|
+
|
|
115
|
+
def wait_for_single_query(self, cq: str, root_id: Union[str, None] = None, timeout: float = 10, css_selector: Union[str, None] = None) -> WebElement:
|
|
116
|
+
"""Method that waits for the specified CQ to match a single result.
|
|
117
|
+
If there are multiple matches then an error is thrown.
|
|
118
|
+
|
|
119
|
+
Args:
|
|
120
|
+
cq (str): The query to execute
|
|
121
|
+
root_id (str, optional): The id of the container within which to perform the query.
|
|
122
|
+
If omitted, all components within the document are included in the search.
|
|
123
|
+
timeout (float): Number of seconds before timing out (default 10)
|
|
124
|
+
css_selector (str, optional): An optional CSS selector that can be used to get child elements of a found component,
|
|
125
|
+
e.g. a clear trigger on a field would be '.x-form-clear-trigger'.
|
|
126
|
+
|
|
127
|
+
Returns:
|
|
128
|
+
WebElement: The DOM element that matches the query
|
|
129
|
+
"""
|
|
130
|
+
results = self.wait_for_query(cq, root_id, timeout, True, css_selector)
|
|
131
|
+
if len(results) > 1:
|
|
132
|
+
raise ComponentQuery.QueryMatchedMultipleElementsException(cq, len(results))
|
|
133
|
+
|
|
134
|
+
return results[0]
|
|
135
|
+
|
|
136
|
+
def wait_for_single_query_visible(self, cq: str, root_id: Union[str, None] = None, timeout: float = 10, css_selector: Union[str, None] = None) -> WebElement:
|
|
137
|
+
"""Method that waits for the specified CQ to match a single visible result.
|
|
138
|
+
If there are multiple matches then an error is thrown.
|
|
139
|
+
|
|
140
|
+
Args:
|
|
141
|
+
cq (str): The query to execute
|
|
142
|
+
root_id (str, optional): The id of the container within which to perform the query.
|
|
143
|
+
If omitted, all components within the document are included in the search.
|
|
144
|
+
timeout (float): Number of seconds before timing out (default 10)
|
|
145
|
+
css_selector (str, optional): An optional CSS selector that can be used to get child elements of a found component,
|
|
146
|
+
e.g. a clear trigger on a field would be '.x-form-clear-trigger'.
|
|
147
|
+
|
|
148
|
+
Returns:
|
|
149
|
+
WebElement: The DOM element that matches the query
|
|
150
|
+
"""
|
|
151
|
+
if not cq.endswith('{isVisible(true)}'):
|
|
152
|
+
cq = cq + '{isVisible(true)}'
|
|
153
|
+
|
|
154
|
+
return self.wait_for_single_query(cq, root_id, timeout, css_selector)
|
|
155
|
+
|
|
156
|
+
def is_component_instance_of_class(self, class_name: str, cq: str, root_id: Union[str, None] = None, timeout: float = 1) -> bool:
|
|
157
|
+
"""Determines whether the component for the specified CQ is an instance of the specified class name.
|
|
158
|
+
|
|
159
|
+
Note, will return True if the component is a subclass of the type too.
|
|
160
|
+
|
|
161
|
+
If the component is not found then an error is thrown.
|
|
162
|
+
|
|
163
|
+
Args:
|
|
164
|
+
class_name (str): The class name to test for, e.g. 'Ext.container.Container'.
|
|
165
|
+
cq (str): The query to find the component.
|
|
166
|
+
root_id (str, optional): The id of the container within which to perform the query.
|
|
167
|
+
If omitted, all components within the document are included in the search.
|
|
168
|
+
timeout (float): Number of seconds before timing out (default 1)
|
|
169
|
+
|
|
170
|
+
Returns:
|
|
171
|
+
bool: True if the component is an instance of the specified class (including a subclass). False otherwise.
|
|
172
|
+
"""
|
|
173
|
+
if root_id is None:
|
|
174
|
+
script = self._IS_COMPONENT_INSTANCE_OF_CLASS_TEMPLATE.format(class_name=class_name, cq=cq)
|
|
175
|
+
else:
|
|
176
|
+
script = self._IS_COMPONENT_INSTANCE_OF_CLASS_TEMPLATE_WITH_ROOT.format(class_name=class_name, cq=cq, root_id=root_id)
|
|
177
|
+
|
|
178
|
+
self.ensure_javascript_loaded()
|
|
179
|
+
result = self._driver.execute_script(script)
|
|
180
|
+
|
|
181
|
+
if result is None:
|
|
182
|
+
raise ComponentQuery.QueryNotFoundException(cq, timeout, root_id)
|
|
183
|
+
|
|
184
|
+
return result
|
|
185
|
+
|
|
186
|
+
class ComponentQueryFoundExpectation:
|
|
187
|
+
""" An expectation for checking that an Ext.ComponentQuery is found"""
|
|
188
|
+
|
|
189
|
+
def __init__(self, cq: str):
|
|
190
|
+
"""Initialises an instance of this class.
|
|
191
|
+
"""
|
|
192
|
+
self._cq = cq
|
|
193
|
+
|
|
194
|
+
def __call__(self, driver):
|
|
195
|
+
"""Method that determines whether a CQ is found
|
|
196
|
+
"""
|
|
197
|
+
results = ComponentQuery(driver).query(self._cq)
|
|
198
|
+
return results is not None and len(results) > 0
|
|
199
|
+
|
|
200
|
+
class QueryMatchedMultipleElementsException(Exception):
|
|
201
|
+
"""Exception class thrown when expecting a single component query match and get multiple"""
|
|
202
|
+
|
|
203
|
+
def __init__(self, cq: str, count: int, message: str = "Expected a single match from ComponentQuery '{cq}' but got {count}."):
|
|
204
|
+
"""Initialises an instance of this exception
|
|
205
|
+
|
|
206
|
+
Args:
|
|
207
|
+
cq (str): The component query that has been executed
|
|
208
|
+
timeout (float): Number of seconds before timing out
|
|
209
|
+
count (int): The number of results that we got
|
|
210
|
+
message (str, optional): The message for the exception. Must contain a 'cq' and 'count' format inserts.
|
|
211
|
+
Defaults to "Expected a single match from ComponentQuery '{cq}' but got '{count}'".
|
|
212
|
+
"""
|
|
213
|
+
self.message = message
|
|
214
|
+
self._cq = cq
|
|
215
|
+
self._count = count
|
|
216
|
+
|
|
217
|
+
super().__init__(self.message)
|
|
218
|
+
|
|
219
|
+
def __str__(self):
|
|
220
|
+
"""Returns a string representation of this exception"""
|
|
221
|
+
return self.message.format(cq=self._cq, count=self._count)
|
|
222
|
+
|
|
223
|
+
class QueryNotFoundException(Exception):
|
|
224
|
+
"""Exception class thrown when a component query could not be found"""
|
|
225
|
+
|
|
226
|
+
def __init__(self,
|
|
227
|
+
cq: str,
|
|
228
|
+
timeout: float,
|
|
229
|
+
root_id: Union[str, None] = None,
|
|
230
|
+
message_without_root: str = "Waiting for component query '{cq}' timed out after {timeout} seconds",
|
|
231
|
+
message_with_root: str = "Waiting for component query '{cq}' under root '{root_id}' timed out after {timeout} seconds"):
|
|
232
|
+
"""Initialises an instance of this exception
|
|
233
|
+
|
|
234
|
+
Args:
|
|
235
|
+
cq (str): The component query that has been executed
|
|
236
|
+
timeout (float): Number of seconds waited
|
|
237
|
+
root_id (str, optional): The id of the container within which the query was performed.
|
|
238
|
+
message_without_root (str, optional): The message for the exception when there is no root. Must contain a 'cq' and 'timeout' format inserts.
|
|
239
|
+
Defaults to "Waiting for component query '{cq}' timed out after {timeout} seconds".
|
|
240
|
+
message_with_root (str, optional): The message for the exception when there is a root. Must contain a 'cq', 'root_id' and 'timeout' format inserts.
|
|
241
|
+
Defaults to "Waiting for component query '{cq}' under root '{root_id}' timed out after {timeout} seconds".
|
|
242
|
+
"""
|
|
243
|
+
self._cq = cq
|
|
244
|
+
self._timeout = timeout
|
|
245
|
+
self._root_id = root_id
|
|
246
|
+
|
|
247
|
+
if root_id is None:
|
|
248
|
+
self.message = message_without_root
|
|
249
|
+
else:
|
|
250
|
+
self.message = message_with_root
|
|
251
|
+
|
|
252
|
+
super().__init__(self.message)
|
|
253
|
+
|
|
254
|
+
def __str__(self):
|
|
255
|
+
"""Returns a string representation of this exception"""
|
|
256
|
+
if self._root_id is None:
|
|
257
|
+
return self.message.format(cq=self._cq, timeout=self._timeout)
|
|
258
|
+
else:
|
|
259
|
+
return self.message.format(cq=self._cq, timeout=self._timeout, root_id=self._root_id)
|