Skip to content

Commit 94e60d7

Browse files
Merge remote-tracking branch 'origin/master' into performance-improvements-version
# Conflicts: # mops/__init__.py # mops/base/element.py
2 parents c177b71 + 8ac61d3 commit 94e60d7

10 files changed

Lines changed: 162 additions & 7 deletions

File tree

.github/workflows/appium_android_tests.yml

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,14 +3,15 @@ name: Appium Android Chrome Tests
33
on:
44
push:
55
branches:
6-
- '**'
6+
- 'master'
77
paths-ignore:
88
- 'docs/**'
99
- 'tests/static_tests/**'
1010
- 'README.md'
1111
- 'CHANGELOG.md'
1212
- 'LICENSE'
1313
- '.gitignore'
14+
pull_request:
1415

1516
jobs:
1617
android-tests:

.github/workflows/appium_ios_tests.yml

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,14 +3,15 @@ name: Appium iOS Safari Tests
33
on:
44
push:
55
branches:
6-
- '**'
6+
- 'master'
77
paths-ignore:
88
- 'docs/**'
99
- 'tests/static_tests/**'
1010
- 'README.md'
1111
- 'CHANGELOG.md'
1212
- 'LICENSE'
1313
- '.gitignore'
14+
pull_request:
1415

1516
jobs:
1617
ios-tests:

.github/workflows/playwright_tests.yml

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,14 +3,15 @@ name: Playwright Tests
33
on:
44
push:
55
branches:
6-
- '**'
6+
- 'master'
77
paths-ignore:
88
- 'docs/**'
99
- 'tests/static_tests/**'
1010
- 'README.md'
1111
- 'CHANGELOG.md'
1212
- 'LICENSE'
1313
- '.gitignore'
14+
pull_request:
1415

1516
jobs:
1617
playwright-docker:

.github/workflows/selenium_safari_tests.yml

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,14 +3,16 @@ name: Selenium Safari Tests
33
on:
44
push:
55
branches:
6-
- '**'
6+
- 'master'
77
paths-ignore:
88
- 'docs/**'
99
- 'tests/static_tests/**'
1010
- 'README.md'
1111
- 'CHANGELOG.md'
1212
- 'LICENSE'
1313
- '.gitignore'
14+
pull_request:
15+
1416
jobs:
1517
selenium-safari:
1618
runs-on: macos-26

.github/workflows/selenium_tests.yml

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,14 +3,16 @@ name: Selenium Selenoid Tests
33
on:
44
push:
55
branches:
6-
- '**'
6+
- 'master'
77
paths-ignore:
88
- 'docs/**'
99
- 'tests/static_tests/**'
1010
- 'README.md'
1111
- 'CHANGELOG.md'
1212
- 'LICENSE'
1313
- '.gitignore'
14+
pull_request:
15+
1416

1517
jobs:
1618
selenium-selenoid:

.github/workflows/static_tests.yml

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,13 +3,15 @@ name: Static Tests
33
on:
44
push:
55
branches:
6-
- '**'
6+
- 'master'
77
paths-ignore:
88
- 'docs/**'
99
- 'README.md'
1010
- 'CHANGELOG.md'
1111
- 'LICENSE'
1212
- '.gitignore'
13+
pull_request:
14+
1315

1416
jobs:
1517
static:

CHANGELOG.md

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,14 @@
22

33
<br>
44

5+
## v3.3.2
6+
*Release date: 2026-03-24*
7+
8+
### Added
9+
- `Element.source_locator` attribute that preserves the original locator before platform-specific transformations
10+
11+
---
12+
513
## v3.3.1
614
*Release date: 2026-01-05*
715

docs/source/element_object/key_features.md

Lines changed: 53 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -39,3 +39,56 @@ particularly for negative checks (i.e., when an element is not present on the pa
3939
## 3. Built-in waits
4040
Most methods automatically wait for specific element states.
4141
For example, the framework will wait until a web element becomes clickable before executing `click` method on it.
42+
43+
---
44+
45+
<br>
46+
47+
48+
## 4. Original locator preservation
49+
The `source_locator` attribute stores the original locator exactly as it was provided to `Element.__init__`,
50+
before any platform-specific resolution or framework-specific transformations.
51+
52+
```{note}
53+
For **static** sub-elements, consider using the built-in parent mechanism instead —
54+
`Element` objects defined as class attributes of a `Group` automatically search within
55+
the Group locator (see {doc}`Group documentation <../group_object/index>`).
56+
57+
`source_locator` is designed for cases where you need the **raw locator string** for
58+
dynamic XPath construction at runtime — something the parent mechanism cannot do.
59+
```
60+
61+
**Example — dynamic table parsing:**
62+
63+
```python
64+
from mops.base.group import Group
65+
from mops.base.element import Element
66+
67+
68+
class DataTable(Group):
69+
70+
def load(self):
71+
row_locator = f'{self.source_locator}//tr'
72+
row_elements = Element(row_locator, f'{self.name}: Rows').all_elements
73+
74+
self.rows = []
75+
for index, _ in enumerate(row_elements):
76+
cell_locator = f'({row_locator})[{index + 1}]/td'
77+
cells = Element(cell_locator, f'{self.name}: Row {index} cells')
78+
self.rows.append(cells)
79+
```
80+
81+
Here `source_locator` is used to dynamically compose new XPath expressions
82+
via string concatenation. This cannot be achieved with the parent mechanism because:
83+
84+
- The XPath grouping operator `(…)[n]` requires building the full expression as a single string.
85+
- New `Element` objects are created at runtime, not as class-level attributes.
86+
- After initialization, `locator` is transformed with platform prefixes
87+
(e.g., `xpath=` for Playwright), making it unsuitable for string concatenation.
88+
89+
```{note}
90+
`source_locator` preserves the exact type passed in: if a `Locator` dataclass was given, it stays a `Locator`;
91+
if a string was given, it stays the original string.
92+
The `locator` attribute, by contrast, is resolved to a platform-specific string and may be further modified
93+
(e.g., prefixed with `xpath=` for Playwright or converted to a CSS selector for ID-based locators in Selenium).
94+
```

mops/base/element.py

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -70,12 +70,13 @@ class Element(DriverMixin, InternalMixin, Logging, ElementABC, metaclass=Element
7070
It dynamically adapts to different driver types (Playwright, Appium, Selenium)
7171
and provides a unified interface for UI interactions.
7272
"""
73-
7473
_object: str = 'element'
7574
_initialized: bool = False
7675
_is_locator_configured: bool = False
7776
_base_cls: Type[PlayElement, MobileElement, WebElement]
7877

78+
source_locator: Union[Locator, str]
79+
7980
def __new__(cls, *args, **kwargs):
8081
instance = super(Element, cls).__new__(cls)
8182
set_instance_frame(instance)
@@ -121,6 +122,7 @@ def __init__(
121122
"""
122123
self.driver_wrapper = get_driver_wrapper_from_object(driver_wrapper)
123124

125+
self.source_locator = locator
124126
self.locator = locator
125127
self.name = name or locator
126128
self.parent = parent
Lines changed: 83 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,83 @@
1+
import pytest
2+
3+
from mops.base.element import Element
4+
from mops.base.group import Group
5+
from mops.mixins.objects.locator import Locator
6+
from tests.static_tests.conftest import desktop_drivers, desktop_ids, mobile_drivers, mobile_ids
7+
8+
9+
xpath_locator = '//div[@class="test"]'
10+
css_locator = '.test-class'
11+
id_locator = 'test-id'
12+
ios_locator = 'ios_locator'
13+
android_locator = 'android_locator'
14+
mobile_locator = 'mobile_locator'
15+
16+
multi_platform_locator = Locator(
17+
default='default_locator',
18+
desktop='desktop_locator',
19+
mobile=mobile_locator,
20+
ios=ios_locator,
21+
android=android_locator,
22+
)
23+
24+
25+
class SourceLocatorGroup(Group):
26+
def __init__(self):
27+
super().__init__(xpath_locator, name='source locator group')
28+
29+
sub_element = Element(css_locator, name='sub element')
30+
multi_element = Element(multi_platform_locator, name='multi element')
31+
32+
33+
@pytest.mark.parametrize('driver', desktop_drivers, ids=desktop_ids)
34+
def test_source_locator_preserved_for_string_xpath(driver, request):
35+
request.getfixturevalue(driver)
36+
element = Element(xpath_locator, name='xpath element')
37+
assert element.source_locator == xpath_locator
38+
39+
40+
@pytest.mark.parametrize('driver', desktop_drivers, ids=desktop_ids)
41+
def test_source_locator_preserved_for_string_css(driver, request):
42+
request.getfixturevalue(driver)
43+
element = Element(css_locator, name='css element')
44+
assert element.source_locator == css_locator
45+
46+
47+
@pytest.mark.parametrize('driver', desktop_drivers, ids=desktop_ids)
48+
def test_source_locator_preserved_for_string_id(driver, request):
49+
request.getfixturevalue(driver)
50+
element = Element(id_locator, name='id element')
51+
assert element.source_locator == id_locator
52+
53+
54+
@pytest.mark.parametrize('driver', desktop_drivers, ids=desktop_ids)
55+
def test_source_locator_differs_from_transformed_locator(driver, request):
56+
request.getfixturevalue(driver)
57+
element = Element(xpath_locator, name='xpath element')
58+
assert element.source_locator == xpath_locator
59+
assert element.locator != xpath_locator or element.source_locator == element.locator
60+
61+
62+
@pytest.mark.parametrize('driver', desktop_drivers, ids=desktop_ids)
63+
def test_source_locator_preserved_for_locator_dataclass(driver, request):
64+
request.getfixturevalue(driver)
65+
element = Element(multi_platform_locator, name='multi element')
66+
assert element.source_locator is multi_platform_locator
67+
68+
69+
@pytest.mark.parametrize('driver', mobile_drivers, ids=mobile_ids)
70+
def test_source_locator_preserved_for_locator_dataclass_mobile(driver, request):
71+
request.getfixturevalue(driver)
72+
element = Element(multi_platform_locator, name='multi element')
73+
assert element.source_locator is multi_platform_locator
74+
assert isinstance(element.locator, str)
75+
76+
77+
@pytest.mark.parametrize('driver', desktop_drivers, ids=desktop_ids)
78+
def test_source_locator_preserved_in_sub_elements(driver, request):
79+
request.getfixturevalue(driver)
80+
group = SourceLocatorGroup()
81+
assert group.source_locator == xpath_locator
82+
assert group.sub_element.source_locator == css_locator
83+
assert group.multi_element.source_locator is multi_platform_locator

0 commit comments

Comments
 (0)