Skip to content

Add Element.source_locator attribute#96

Merged
VladimirPodolian merged 3 commits into
CustomEnv:masterfrom
wwakabobik:feature/source-locator
Mar 24, 2026
Merged

Add Element.source_locator attribute#96
VladimirPodolian merged 3 commits into
CustomEnv:masterfrom
wwakabobik:feature/source-locator

Conversation

@wwakabobik

Copy link
Copy Markdown
Contributor

Summary

  • Add source_locator attribute to Element that preserves the original locator before platform-specific transformations
  • Add documentation (Key Features section + Sphinx docstring) and integration tests
  • Add CHANGELOG entry

Problem

During element initialization, self.locator is overwritten by platform-specific transformations:

  • Playwright: //div[@id='x'] becomes xpath=//div[@id='x'] (via set_playwright_locator)
  • Selenium: an ID-based locator my-id becomes [id="my-id"] CSS selector (via set_selenium_selector)
  • Multi-platform Locator dataclass is resolved to a single platform string (via get_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:

class LoginForm(Group):
    def __init__(self):
        super().__init__('//form[@id="login"]', name='Login Form')
        # self.locator is already transformed here — can't use it
        self.username = Element(f'{self.source_locator}//input[@name="user"]', name='Username')

Without source_locator, every downstream project has to manually save the original locator before calling super().__init__():

self.base_locator = locator  # workaround
super().__init__(locator, name)

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__:

self.locator = locator
self.source_locator = locator  # <-- new

Set once before any transformation runs. Preserves the exact type passed in: str stays str, Locator dataclass stays Locator.

Backward Compatibility

Purely additive — no existing attributes, methods, or signatures change.

Test Plan

  • 14 new integration tests covering: string locators (XPath, CSS, ID), Locator dataclass (desktop + mobile), Group children inheritance
  • All 349 existing static tests pass
  • Sphinx docs build without new warnings

Made with Cursor

self.password = Element(
f'{self.source_locator}//input[@name="password"]',
name='Password Input',
)

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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!

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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
@wwakabobik wwakabobik force-pushed the feature/source-locator branch from 93b4a76 to 05dbd86 Compare March 23, 2026 19:05
@VladimirPodolian VladimirPodolian merged commit 826cc2b into CustomEnv:master Mar 24, 2026
18 of 20 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants