crosslocator 0.2.0__py3-none-any.whl
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.
- crosslocator/__init__.py +29 -0
- crosslocator/appium.py +55 -0
- crosslocator/locator.py +107 -0
- crosslocator/platform.py +89 -0
- crosslocator/py.typed +0 -0
- crosslocator/robot.py +51 -0
- crosslocator-0.2.0.dist-info/METADATA +152 -0
- crosslocator-0.2.0.dist-info/RECORD +10 -0
- crosslocator-0.2.0.dist-info/WHEEL +4 -0
- crosslocator-0.2.0.dist-info/licenses/LICENSE +21 -0
crosslocator/__init__.py
ADDED
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
"""crosslocator — define a UI selector once, resolve it on every platform."""
|
|
2
|
+
from .appium import use_appium
|
|
3
|
+
from .locator import (
|
|
4
|
+
CrossLocatorError,
|
|
5
|
+
Locator,
|
|
6
|
+
LocatorNotFound,
|
|
7
|
+
PlatformNotSet,
|
|
8
|
+
)
|
|
9
|
+
from .platform import (
|
|
10
|
+
Platform,
|
|
11
|
+
get_current_platform,
|
|
12
|
+
set_current_platform,
|
|
13
|
+
set_platform_provider,
|
|
14
|
+
)
|
|
15
|
+
|
|
16
|
+
__version__ = "0.2.0"
|
|
17
|
+
|
|
18
|
+
__all__ = [
|
|
19
|
+
"Locator",
|
|
20
|
+
"Platform",
|
|
21
|
+
"get_current_platform",
|
|
22
|
+
"set_current_platform",
|
|
23
|
+
"set_platform_provider",
|
|
24
|
+
"use_appium",
|
|
25
|
+
"CrossLocatorError",
|
|
26
|
+
"LocatorNotFound",
|
|
27
|
+
"PlatformNotSet",
|
|
28
|
+
"__version__",
|
|
29
|
+
]
|
crosslocator/appium.py
ADDED
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
"""Automatic platform detection from a live Appium session.
|
|
2
|
+
|
|
3
|
+
crosslocator stays framework-agnostic: this module only reads the session
|
|
4
|
+
capabilities and translates them into a :class:`~crosslocator.platform.Platform`.
|
|
5
|
+
"""
|
|
6
|
+
from __future__ import annotations
|
|
7
|
+
|
|
8
|
+
from typing import Any, Mapping, Optional
|
|
9
|
+
|
|
10
|
+
from .platform import Platform, set_platform_provider
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
def platform_from_capabilities(
|
|
14
|
+
capabilities: Optional[Mapping[str, Any]],
|
|
15
|
+
) -> Optional[Platform]:
|
|
16
|
+
"""Translate Appium capabilities into a :class:`Platform`.
|
|
17
|
+
|
|
18
|
+
Returns ``None`` when the platform cannot be determined.
|
|
19
|
+
"""
|
|
20
|
+
if not capabilities:
|
|
21
|
+
return None
|
|
22
|
+
name = str(
|
|
23
|
+
capabilities.get("platformName")
|
|
24
|
+
or capabilities.get("appium:platformName")
|
|
25
|
+
or ""
|
|
26
|
+
).strip().lower()
|
|
27
|
+
device = str(
|
|
28
|
+
capabilities.get("deviceName")
|
|
29
|
+
or capabilities.get("appium:deviceName")
|
|
30
|
+
or ""
|
|
31
|
+
).lower()
|
|
32
|
+
if name == "android":
|
|
33
|
+
return Platform.ANDROID
|
|
34
|
+
if name in ("ios", "iphoneos", "iphone os"):
|
|
35
|
+
return Platform.IPAD if "ipad" in device else Platform.IOS
|
|
36
|
+
if name in ("windows", "win"):
|
|
37
|
+
return Platform.WINDOWS
|
|
38
|
+
return None
|
|
39
|
+
|
|
40
|
+
|
|
41
|
+
def use_appium(driver: Optional[Any]) -> None:
|
|
42
|
+
"""Detect the current platform automatically from an Appium ``driver``.
|
|
43
|
+
|
|
44
|
+
Registers a provider that reads ``driver.capabilities`` on each resolution,
|
|
45
|
+
so you no longer need to call :func:`set_current_platform` by hand. Pass
|
|
46
|
+
``None`` to stop using the driver.
|
|
47
|
+
"""
|
|
48
|
+
if driver is None:
|
|
49
|
+
set_platform_provider(None)
|
|
50
|
+
return
|
|
51
|
+
|
|
52
|
+
def provider() -> Optional[Platform]:
|
|
53
|
+
return platform_from_capabilities(getattr(driver, "capabilities", None))
|
|
54
|
+
|
|
55
|
+
set_platform_provider(provider)
|
crosslocator/locator.py
ADDED
|
@@ -0,0 +1,107 @@
|
|
|
1
|
+
"""The :class:`Locator` type: define a selector once, resolve it per platform."""
|
|
2
|
+
from __future__ import annotations
|
|
3
|
+
|
|
4
|
+
from typing import Dict, Optional
|
|
5
|
+
|
|
6
|
+
from .platform import (
|
|
7
|
+
Platform,
|
|
8
|
+
PlatformLike,
|
|
9
|
+
get_current_platform,
|
|
10
|
+
normalize_platform,
|
|
11
|
+
)
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
class CrossLocatorError(Exception):
|
|
15
|
+
"""Base class for all crosslocator errors."""
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
class PlatformNotSet(CrossLocatorError):
|
|
19
|
+
"""Raised when a resolution needs the current platform but none is set."""
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
class LocatorNotFound(CrossLocatorError, LookupError):
|
|
23
|
+
"""Raised when no selector is available for the requested platform."""
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
# A platform inherits from the next one in its chain when it has no own selector.
|
|
27
|
+
_FALLBACK_CHAINS: Dict[Platform, tuple] = {
|
|
28
|
+
Platform.IPAD: (Platform.IPAD, Platform.IOS),
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
|
|
32
|
+
class Locator:
|
|
33
|
+
"""A UI selector declared once for several platforms.
|
|
34
|
+
|
|
35
|
+
Define a selector per platform plus an optional shared ``default``. At
|
|
36
|
+
runtime, :meth:`resolve` (or :meth:`for_platform`) returns the right
|
|
37
|
+
selector string. crosslocator only *routes* strings — it never parses them
|
|
38
|
+
nor touches your driver.
|
|
39
|
+
"""
|
|
40
|
+
|
|
41
|
+
__slots__ = ("_selectors", "_default")
|
|
42
|
+
|
|
43
|
+
def __init__(
|
|
44
|
+
self,
|
|
45
|
+
*,
|
|
46
|
+
android: Optional[str] = None,
|
|
47
|
+
ios: Optional[str] = None,
|
|
48
|
+
ipad: Optional[str] = None,
|
|
49
|
+
windows: Optional[str] = None,
|
|
50
|
+
web: Optional[str] = None,
|
|
51
|
+
default: Optional[str] = None,
|
|
52
|
+
) -> None:
|
|
53
|
+
self._selectors: Dict[Platform, Optional[str]] = {
|
|
54
|
+
Platform.ANDROID: android,
|
|
55
|
+
Platform.IOS: ios,
|
|
56
|
+
Platform.IPAD: ipad,
|
|
57
|
+
Platform.WINDOWS: windows,
|
|
58
|
+
Platform.WEB: web,
|
|
59
|
+
}
|
|
60
|
+
self._default = default
|
|
61
|
+
if default is None and not any(self._selectors.values()):
|
|
62
|
+
raise ValueError(
|
|
63
|
+
"A Locator needs at least one platform selector or a default."
|
|
64
|
+
)
|
|
65
|
+
|
|
66
|
+
def for_platform(self, platform: PlatformLike) -> str:
|
|
67
|
+
"""Return the selector for ``platform``, applying fallbacks.
|
|
68
|
+
|
|
69
|
+
Resolution order: the platform's own selector, then its fallback chain
|
|
70
|
+
(e.g. iPad -> iOS), then ``default``. Raises :class:`LocatorNotFound`
|
|
71
|
+
if nothing matches.
|
|
72
|
+
"""
|
|
73
|
+
target = normalize_platform(platform)
|
|
74
|
+
for candidate in _FALLBACK_CHAINS.get(target, (target,)):
|
|
75
|
+
value = self._selectors.get(candidate)
|
|
76
|
+
if value is not None:
|
|
77
|
+
return value
|
|
78
|
+
if self._default is not None:
|
|
79
|
+
return self._default
|
|
80
|
+
raise LocatorNotFound(
|
|
81
|
+
f"No selector defined for platform {target.value!r} and no default set."
|
|
82
|
+
)
|
|
83
|
+
|
|
84
|
+
def resolve(self) -> str:
|
|
85
|
+
"""Return the selector for the current platform.
|
|
86
|
+
|
|
87
|
+
Raises :class:`PlatformNotSet` when the current platform is unknown.
|
|
88
|
+
"""
|
|
89
|
+
platform = get_current_platform()
|
|
90
|
+
if platform is None:
|
|
91
|
+
raise PlatformNotSet(
|
|
92
|
+
"No current platform set. Use set_current_platform(...) or the "
|
|
93
|
+
"CROSSLOCATOR_PLATFORM environment variable."
|
|
94
|
+
)
|
|
95
|
+
return self.for_platform(platform)
|
|
96
|
+
|
|
97
|
+
def __repr__(self) -> str:
|
|
98
|
+
parts = {p.value: v for p, v in self._selectors.items() if v is not None}
|
|
99
|
+
if self._default is not None:
|
|
100
|
+
parts["default"] = self._default
|
|
101
|
+
inner = ", ".join(f"{k}={v!r}" for k, v in parts.items())
|
|
102
|
+
return f"Locator({inner})"
|
|
103
|
+
|
|
104
|
+
def __eq__(self, other: object) -> bool:
|
|
105
|
+
if not isinstance(other, Locator):
|
|
106
|
+
return NotImplemented
|
|
107
|
+
return self._selectors == other._selectors and self._default == other._default
|
crosslocator/platform.py
ADDED
|
@@ -0,0 +1,89 @@
|
|
|
1
|
+
"""Platform identification and current-platform state.
|
|
2
|
+
|
|
3
|
+
crosslocator resolves selectors against a *current platform*. This module owns
|
|
4
|
+
the small enum of supported platforms, alias normalization, and the resolution
|
|
5
|
+
of the current platform from (in priority order):
|
|
6
|
+
|
|
7
|
+
1. an explicit value set via :func:`set_current_platform`
|
|
8
|
+
2. the ``CROSSLOCATOR_PLATFORM`` environment variable
|
|
9
|
+
3. a registered provider (e.g. reading a live Appium session)
|
|
10
|
+
"""
|
|
11
|
+
from __future__ import annotations
|
|
12
|
+
|
|
13
|
+
import os
|
|
14
|
+
from enum import Enum
|
|
15
|
+
from typing import Callable, Optional, Union
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
class Platform(Enum):
|
|
19
|
+
"""A UI platform a selector can target."""
|
|
20
|
+
|
|
21
|
+
ANDROID = "android"
|
|
22
|
+
IOS = "ios"
|
|
23
|
+
IPAD = "ipad"
|
|
24
|
+
WINDOWS = "windows"
|
|
25
|
+
WEB = "web"
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
PlatformLike = Union[Platform, str]
|
|
29
|
+
|
|
30
|
+
ENV_VAR = "CROSSLOCATOR_PLATFORM"
|
|
31
|
+
|
|
32
|
+
_ALIASES = {
|
|
33
|
+
"android": Platform.ANDROID,
|
|
34
|
+
"ios": Platform.IOS,
|
|
35
|
+
"iphone": Platform.IOS,
|
|
36
|
+
"ipad": Platform.IPAD,
|
|
37
|
+
"ipados": Platform.IPAD,
|
|
38
|
+
"windows": Platform.WINDOWS,
|
|
39
|
+
"win": Platform.WINDOWS,
|
|
40
|
+
"web": Platform.WEB,
|
|
41
|
+
"browser": Platform.WEB,
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
_current: Optional[Platform] = None
|
|
45
|
+
_provider: Optional[Callable[[], Optional[PlatformLike]]] = None
|
|
46
|
+
|
|
47
|
+
|
|
48
|
+
def normalize_platform(value: PlatformLike) -> Platform:
|
|
49
|
+
"""Return the :class:`Platform` for ``value``, accepting common aliases.
|
|
50
|
+
|
|
51
|
+
Raises :class:`ValueError` for unknown platforms.
|
|
52
|
+
"""
|
|
53
|
+
if isinstance(value, Platform):
|
|
54
|
+
return value
|
|
55
|
+
key = str(value).strip().lower()
|
|
56
|
+
try:
|
|
57
|
+
return _ALIASES[key]
|
|
58
|
+
except KeyError:
|
|
59
|
+
raise ValueError(f"Unknown platform: {value!r}") from None
|
|
60
|
+
|
|
61
|
+
|
|
62
|
+
def set_current_platform(value: Optional[PlatformLike]) -> None:
|
|
63
|
+
"""Set (or clear, with ``None``) the platform used to resolve selectors."""
|
|
64
|
+
global _current
|
|
65
|
+
_current = None if value is None else normalize_platform(value)
|
|
66
|
+
|
|
67
|
+
|
|
68
|
+
def set_platform_provider(
|
|
69
|
+
provider: Optional[Callable[[], Optional[PlatformLike]]],
|
|
70
|
+
) -> None:
|
|
71
|
+
"""Register a callable returning the current platform (e.g. from Appium).
|
|
72
|
+
|
|
73
|
+
Pass ``None`` to unregister.
|
|
74
|
+
"""
|
|
75
|
+
global _provider
|
|
76
|
+
_provider = provider
|
|
77
|
+
|
|
78
|
+
|
|
79
|
+
def get_current_platform() -> Optional[Platform]:
|
|
80
|
+
"""Resolve the current platform, or ``None`` if it cannot be determined."""
|
|
81
|
+
if _current is not None:
|
|
82
|
+
return _current
|
|
83
|
+
env = os.environ.get(ENV_VAR)
|
|
84
|
+
if env:
|
|
85
|
+
return normalize_platform(env)
|
|
86
|
+
if _provider is not None:
|
|
87
|
+
result = _provider()
|
|
88
|
+
return None if result is None else normalize_platform(result)
|
|
89
|
+
return None
|
crosslocator/py.typed
ADDED
|
File without changes
|
crosslocator/robot.py
ADDED
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
"""Robot Framework integration for crosslocator.
|
|
2
|
+
|
|
3
|
+
Import in a suite with::
|
|
4
|
+
|
|
5
|
+
Library crosslocator.robot.CrossLocator
|
|
6
|
+
|
|
7
|
+
Locators are plain Python objects: define them in a Python variable file and
|
|
8
|
+
import them, then resolve them with the keywords below.
|
|
9
|
+
"""
|
|
10
|
+
from __future__ import annotations
|
|
11
|
+
|
|
12
|
+
from typing import Optional
|
|
13
|
+
|
|
14
|
+
from .locator import Locator
|
|
15
|
+
from .platform import get_current_platform, set_current_platform
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
class CrossLocator:
|
|
19
|
+
"""Robot Framework library exposing crosslocator as keywords."""
|
|
20
|
+
|
|
21
|
+
ROBOT_LIBRARY_SCOPE = "GLOBAL"
|
|
22
|
+
|
|
23
|
+
def set_current_platform(self, platform: str) -> None:
|
|
24
|
+
"""Set the platform used to resolve locators.
|
|
25
|
+
|
|
26
|
+
Accepts ``android``, ``ios``, ``ipad``, ``windows`` or ``web``.
|
|
27
|
+
"""
|
|
28
|
+
set_current_platform(platform)
|
|
29
|
+
|
|
30
|
+
def get_current_platform(self) -> str:
|
|
31
|
+
"""Return the current platform name, or an empty string if unset."""
|
|
32
|
+
platform = get_current_platform()
|
|
33
|
+
return platform.value if platform is not None else ""
|
|
34
|
+
|
|
35
|
+
def resolve_locator(self, locator: Locator, platform: Optional[str] = None) -> str:
|
|
36
|
+
"""Resolve a ``Locator`` to a selector string.
|
|
37
|
+
|
|
38
|
+
Uses ``platform`` when given, otherwise the current platform.
|
|
39
|
+
"""
|
|
40
|
+
if platform:
|
|
41
|
+
return locator.for_platform(platform)
|
|
42
|
+
return locator.resolve()
|
|
43
|
+
|
|
44
|
+
def use_appium_session(self) -> None:
|
|
45
|
+
"""Auto-detect the platform from the active AppiumLibrary session."""
|
|
46
|
+
from robot.libraries.BuiltIn import BuiltIn
|
|
47
|
+
|
|
48
|
+
from .appium import use_appium
|
|
49
|
+
|
|
50
|
+
appium_lib = BuiltIn().get_library_instance("AppiumLibrary")
|
|
51
|
+
use_appium(appium_lib._current_application())
|
|
@@ -0,0 +1,152 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: crosslocator
|
|
3
|
+
Version: 0.2.0
|
|
4
|
+
Summary: Define a UI selector once, resolve it on every platform (Android, iOS, iPadOS, Windows, web).
|
|
5
|
+
Project-URL: Homepage, https://github.com/julien-becheny/crosslocator
|
|
6
|
+
Project-URL: Repository, https://github.com/julien-becheny/crosslocator
|
|
7
|
+
Project-URL: Issues, https://github.com/julien-becheny/crosslocator/issues
|
|
8
|
+
Author: Julien Becheny
|
|
9
|
+
License: MIT
|
|
10
|
+
License-File: LICENSE
|
|
11
|
+
Keywords: android,appium,ios,locators,mobile-testing,playwright,robotframework,selectors,test-automation,windows
|
|
12
|
+
Classifier: Development Status :: 3 - Alpha
|
|
13
|
+
Classifier: Framework :: Robot Framework :: Library
|
|
14
|
+
Classifier: Intended Audience :: Developers
|
|
15
|
+
Classifier: License :: OSI Approved :: MIT License
|
|
16
|
+
Classifier: Operating System :: OS Independent
|
|
17
|
+
Classifier: Programming Language :: Python :: 3
|
|
18
|
+
Classifier: Programming Language :: Python :: 3.9
|
|
19
|
+
Classifier: Programming Language :: Python :: 3.10
|
|
20
|
+
Classifier: Programming Language :: Python :: 3.11
|
|
21
|
+
Classifier: Programming Language :: Python :: 3.12
|
|
22
|
+
Classifier: Topic :: Software Development :: Testing
|
|
23
|
+
Requires-Python: >=3.9
|
|
24
|
+
Provides-Extra: dev
|
|
25
|
+
Requires-Dist: pytest>=7.0; extra == 'dev'
|
|
26
|
+
Description-Content-Type: text/markdown
|
|
27
|
+
|
|
28
|
+
# crosslocator
|
|
29
|
+
|
|
30
|
+
**Define a UI selector once. Resolve it on every platform.**
|
|
31
|
+
|
|
32
|
+
Cross-platform test automation means maintaining the same element for Android,
|
|
33
|
+
iOS, iPadOS, Windows and the web. Most teams end up either duplicating selectors
|
|
34
|
+
or scattering `if platform == ...` branches through their page objects.
|
|
35
|
+
|
|
36
|
+
`crosslocator` lets you declare a selector **once**, per platform, and resolves
|
|
37
|
+
the right one at runtime — from **pure Python (Appium), Robot Framework, or any
|
|
38
|
+
other framework**. It only routes selector *strings*; it never wraps your driver.
|
|
39
|
+
|
|
40
|
+
```python
|
|
41
|
+
from crosslocator import Locator
|
|
42
|
+
|
|
43
|
+
ADD_PATIENT = Locator(
|
|
44
|
+
android="accessibility_id=add_patient",
|
|
45
|
+
ios="add_patient",
|
|
46
|
+
windows='//Button[@Name="Add"]',
|
|
47
|
+
default="id=add",
|
|
48
|
+
)
|
|
49
|
+
|
|
50
|
+
ADD_PATIENT.for_platform("ios") # -> "add_patient"
|
|
51
|
+
```
|
|
52
|
+
|
|
53
|
+
## Installation
|
|
54
|
+
|
|
55
|
+
```bash
|
|
56
|
+
pip install crosslocator
|
|
57
|
+
```
|
|
58
|
+
|
|
59
|
+
Zero runtime dependencies.
|
|
60
|
+
|
|
61
|
+
## Why
|
|
62
|
+
|
|
63
|
+
- **One source of truth** per element — no duplicated selectors, no platform `if/else`.
|
|
64
|
+
- **Framework-agnostic** — Appium in plain Python, Robot Framework, or anything else.
|
|
65
|
+
- **Sensible fallbacks** — iPad inherits from iOS, and a shared `default` covers the rest.
|
|
66
|
+
- **Typed** and dependency-free.
|
|
67
|
+
|
|
68
|
+
## Quick start (Python)
|
|
69
|
+
|
|
70
|
+
```python
|
|
71
|
+
from crosslocator import Locator, set_current_platform
|
|
72
|
+
|
|
73
|
+
ADD_PATIENT = Locator(
|
|
74
|
+
android="accessibility_id=add_patient",
|
|
75
|
+
ios="add_patient",
|
|
76
|
+
windows='//Button[@Name="Add"]',
|
|
77
|
+
)
|
|
78
|
+
|
|
79
|
+
set_current_platform("android") # usually done once, at session start
|
|
80
|
+
ADD_PATIENT.resolve() # -> "accessibility_id=add_patient"
|
|
81
|
+
```
|
|
82
|
+
|
|
83
|
+
## Robot Framework
|
|
84
|
+
|
|
85
|
+
```robotframework
|
|
86
|
+
*** Settings ***
|
|
87
|
+
Library crosslocator.robot.CrossLocator
|
|
88
|
+
Variables locators.py # ADD_PATIENT defined as a Locator here
|
|
89
|
+
|
|
90
|
+
*** Test Cases ***
|
|
91
|
+
Open Add Patient
|
|
92
|
+
Set Current Platform android
|
|
93
|
+
${selector}= Resolve Locator ${ADD_PATIENT}
|
|
94
|
+
# -> pass ${selector} to AppiumLibrary / Browser as usual
|
|
95
|
+
```
|
|
96
|
+
|
|
97
|
+
## How the platform is resolved
|
|
98
|
+
|
|
99
|
+
`resolve()` determines the current platform in this order:
|
|
100
|
+
|
|
101
|
+
1. an explicit `set_current_platform(...)`
|
|
102
|
+
2. the `CROSSLOCATOR_PLATFORM` environment variable
|
|
103
|
+
3. a registered provider — typically an Appium session (see below)
|
|
104
|
+
|
|
105
|
+
## Automatic platform detection (Appium)
|
|
106
|
+
|
|
107
|
+
Let crosslocator read the platform straight from a live Appium session, so you
|
|
108
|
+
never call `set_current_platform` by hand.
|
|
109
|
+
|
|
110
|
+
**Python (Appium-Python-Client):**
|
|
111
|
+
|
|
112
|
+
```python
|
|
113
|
+
from crosslocator import use_appium
|
|
114
|
+
|
|
115
|
+
use_appium(driver) # your Appium webdriver
|
|
116
|
+
LOGIN_BUTTON.resolve() # platform detected from the session capabilities
|
|
117
|
+
```
|
|
118
|
+
|
|
119
|
+
**Robot Framework (AppiumLibrary):**
|
|
120
|
+
|
|
121
|
+
```robotframework
|
|
122
|
+
Open Application ${REMOTE_URL} platformName=Android ...
|
|
123
|
+
Use Appium Session
|
|
124
|
+
${selector}= Resolve Locator ${LOGIN_BUTTON}
|
|
125
|
+
```
|
|
126
|
+
|
|
127
|
+
iPad is detected from the session device name and resolves to iPad selectors
|
|
128
|
+
(falling back to iOS when none are defined).
|
|
129
|
+
|
|
130
|
+
## Fallback rules
|
|
131
|
+
|
|
132
|
+
- `ipad` → falls back to `ios` when no iPad-specific selector is set
|
|
133
|
+
- any platform → falls back to `default`
|
|
134
|
+
- nothing found → `LocatorNotFound`
|
|
135
|
+
|
|
136
|
+
## Examples
|
|
137
|
+
|
|
138
|
+
Runnable examples (no device needed) live in the [`examples/`](examples/) folder —
|
|
139
|
+
a login screen resolved across Android, iOS, iPadOS and Windows, plus a Robot
|
|
140
|
+
Framework suite.
|
|
141
|
+
|
|
142
|
+
## Roadmap
|
|
143
|
+
|
|
144
|
+
- **v0.1:** the core `Locator` type, platform resolution, Robot Framework keywords.
|
|
145
|
+
- **v0.2 (current):** automatic platform detection from a live Appium session.
|
|
146
|
+
- **Next:** Playwright/Browser helpers, and declarative loading of whole screens from YAML/dict.
|
|
147
|
+
|
|
148
|
+
Issues and contributions are welcome.
|
|
149
|
+
|
|
150
|
+
## License
|
|
151
|
+
|
|
152
|
+
MIT © 2026 Julien Becheny
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
crosslocator/__init__.py,sha256=APhm-eLujcLCQDuSEZMbzQlL7g0iryzuElSkulXXtHQ,622
|
|
2
|
+
crosslocator/appium.py,sha256=F-nzbMgO6Ennf7XTWJTSrtSgNTrLDSqBnVRSmgK65Yw,1793
|
|
3
|
+
crosslocator/locator.py,sha256=PoNZeHAkD4bGlVIQZwC8D5aVrU_aXyFTMNEftz3sR2k,3762
|
|
4
|
+
crosslocator/platform.py,sha256=c9or-XWVoG4GA6pIMuFKh2oG070d8G1-PJcKxo_W-Zc,2601
|
|
5
|
+
crosslocator/py.typed,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
6
|
+
crosslocator/robot.py,sha256=g8e3GKhxb9qGY86GRqG23br2wg49ts-dao4mALQiUEo,1729
|
|
7
|
+
crosslocator-0.2.0.dist-info/METADATA,sha256=9vGHNdMnu9N5Ceb-2KK5dVEijOmQQLRkzI2NANToN2k,4823
|
|
8
|
+
crosslocator-0.2.0.dist-info/WHEEL,sha256=lCkmxWfQsSc9CfIClYeavTdQeEX2toPqufh9gI35EQA,87
|
|
9
|
+
crosslocator-0.2.0.dist-info/licenses/LICENSE,sha256=FWUI76-_E-wL4KfLcr4hKvTb_2-iS6XTMdLHfV8atfs,1092
|
|
10
|
+
crosslocator-0.2.0.dist-info/RECORD,,
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Julien Becheny
|
|
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.
|