Add Element.source_locator attribute#96
Conversation
| self.password = Element( | ||
| f'{self.source_locator}//input[@name="password"]', | ||
| name='Password Input', | ||
| ) |
There was a problem hiding this comment.
Thanks for introducing the source_locator concept — I think it will be really useful. Did you know that Element objects defined under a Group class already perform lookups not from the driver, but from the Group locator instead?
For example:
class LoginForm(Group):
def __init__(self):
super().__init__('//form[@id="login"]', name='Login Form')
username = Element('//input[@name="username"]', name='Username Input')
password = Element('//input[@name="password"]', name='Password Input')In this case, the parent argument is automatically set for the username and password Element objects. Each interaction with these elements will first locate the Group locator '//form[@id="login"]', and then search for the nested element locator within it
There was a problem hiding this comment.
Great point! Yes, I'm aware of the parent mechanism — it's the right approach for static child elements defined as class attributes.
However, source_locator targets a different use case: dynamic XPath construction at runtime via string concatenation. For example, when parsing a table:
class DataTable(Group):
def load(self):
row_locator = f'{self.source_locator}//tr'
row_elements = Element(row_locator, f'{self.name}: Rows').all_elements
for index, _ in enumerate(row_elements):
cell_locator = f'({row_locator})[{index + 1}]/td'
cells = Element(cell_locator, f'{self.name}: Row {index} cells')
This can't be solved with the parent mechanism because:
The XPath grouping operator (…)[n] requires a single complete expression — it can't be split into parent + child lookup.
New Element objects are created dynamically at runtime, not as class-level attributes.
After init, self.locator is already transformed with platform prefixes (xpath=, css=, etc.), so string concatenation would produce invalid locators.
I've updated the docs example to better illustrate this distinction — and added a note pointing users to the parent mechanism for the simpler static case. Thanks for the feedback!
There was a problem hiding this comment.
Thanks! Merged and published to pypi under 3.3.2 version: https://pypi.org/project/mops/3.3.2/
Problem: During element initialization, the `locator` attribute is overwritten by platform-specific transformations (e.g. an XPath "//div[@id='x']" becomes "xpath=//div[@id='x']" in Playwright, or gets converted to a CSS selector "[id='x']" in Selenium for ID-based locators). When using a `Locator` dataclass, the multi-platform object is resolved to a single string. After init, there is no way to access the original locator value. This forces downstream projects to manually save it before calling super().__init__(), e.g.: self.base_locator = locator super().__init__(locator, name, ...) This pattern is needed whenever child locators are built dynamically: self.input = Element(f'{self.base_locator}//input', name='Input') We encountered this in a production test suite where every Page, Group, and Element subclass required this workaround. Solution: Add `source_locator` — set once in Element.__init__ before any transformation runs. It preserves the exact type and value passed in (str or Locator dataclass). Purely additive, no existing behavior changes. Includes: - source_locator attribute with Sphinx-compatible docstring - Key Features documentation section with usage example - Integration tests for string, Locator dataclass, and Group children - CHANGELOG entry Made-with: Cursor
93b4a76 to
05dbd86
Compare
24196ca to
bf2b8bd
Compare
Summary
source_locatorattribute toElementthat preserves the original locator before platform-specific transformationsProblem
During element initialization,
self.locatoris overwritten by platform-specific transformations://div[@id='x']becomesxpath=//div[@id='x'](viaset_playwright_locator)my-idbecomes[id="my-id"]CSS selector (viaset_selenium_selector)Locatordataclass is resolved to a single platform string (viaget_platform_locator)After initialization, the original locator is lost. This is a problem when building child locators dynamically from the original value — a common pattern in Page Object Models:
Without
source_locator, every downstream project has to manually save the original locator before callingsuper().__init__():We encountered this in a production test suite where every Page, Group, and Element subclass required this workaround (3 base templates + all page objects using it).
Solution
A single line in
Element.__init__:Set once before any transformation runs. Preserves the exact type passed in:
strstaysstr,Locatordataclass staysLocator.Backward Compatibility
Purely additive — no existing attributes, methods, or signatures change.
Test Plan
Locatordataclass (desktop + mobile), Group children inheritanceMade with Cursor