Skip to content

Commons

qa_pytest_commons

TElement = TypeVar('TElement', covariant=True) module-attribute

__all__ = ['AbstractTestsBase', 'BaseConfiguration', 'BddKeywords', 'By', 'Configuration', 'GenericSteps', 'Selector', 'TElement', 'UiConfiguration', 'UiContext', 'UiElement', 'UiSteps'] module-attribute

AbstractTestsBase

Bases: ABC, LoggerMixin, ImmutableMixin

Basic test scenario implementation, holding some type of steps and a logger facility.

Subtypes must set _steps_type to the actual type of steps implementation::

                    +---------------+
                    |  BddKeyWords  |
                    +---------------+
                                    ^
                                    |
                                implements
                                    |
+-------------------+               +--------------+
| AbstractTestsBase |---contains--->| GenericSteps |
|                   |               +--------------+
|                   |                       +---------------+
|                   |---contains----------->| Configuration |
+-------------------+                       +---------------+

IMPORTANT: pytest classes must not define an init method.

Attributes:

Name Type Description
_steps_type Type[AbstractTestsBase[TSteps]]

The type of the steps implementation. Must be set by subtypes.

_configuration AbstractTestsBase[TConfiguration]

The configuration instance for the test scenario. Must be set by subtypes.

Source code in qa-pytest-commons/src/qa_pytest_commons/abstract_tests_base.py
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
class AbstractTestsBase[
        TSteps: GenericSteps[Any],
        TConfiguration: Configuration](ABC, LoggerMixin, ImmutableMixin):
    """
    Basic test scenario implementation, holding some type of steps and a logger
    facility.

    Subtypes must set `_steps_type` to the actual type of steps implementation::

                            +---------------+
                            |  BddKeyWords  |
                            +---------------+
                                            ^
                                            |
                                        implements
                                            |
        +-------------------+               +--------------+
        | AbstractTestsBase |---contains--->| GenericSteps |
        |                   |               +--------------+
        |                   |                       +---------------+
        |                   |---contains----------->| Configuration |
        +-------------------+                       +---------------+

    IMPORTANT: pytest classes must not define an __init__ method.

    Type Parameters:
        TSteps (TSteps:GenericSteps): The actual steps implementation, or partial implementation.
        TConfiguration (TConfiguration:Configuration): The configuration type for the test scenario.

    Attributes:
        _steps_type (Type[TSteps]): The type of the steps implementation. Must be set by subtypes.
        _configuration (TConfiguration): The configuration instance for the test scenario. Must be set by subtypes.
    """
    _steps_type: Type[TSteps]
    _configuration: TConfiguration

    @property
    def configuration(self) -> TConfiguration:
        '''
        Returns the configuration instance.

        Returns:
            TConfiguration: The configuration instance.
        '''
        return self._configuration

    @final
    @cached_property
    def steps(self) -> TSteps:
        '''
        Lazily initializes and returns an instance of steps implementation.

        Returns:
            TSteps: The instance of steps implementation.
        '''
        self.log.debug(f"initiating {self._steps_type}")
        return self._steps_type(self._configuration)

    def setup_method(self):
        """
        Override in subtypes with specific setup, if any.
        """
        self.log.debug("setup")

    def teardown_method(self):
        """
        Override in subtypes with specific teardown, if any.
        """
        self.log.debug("teardown")

configuration property

Returns the configuration instance.

Returns:

Name Type Description
TConfiguration AbstractTestsBase[TConfiguration]

The configuration instance.

steps cached property

Lazily initializes and returns an instance of steps implementation.

Returns:

Name Type Description
TSteps AbstractTestsBase[TSteps]

The instance of steps implementation.

setup_method()

Override in subtypes with specific setup, if any.

Source code in qa-pytest-commons/src/qa_pytest_commons/abstract_tests_base.py
73
74
75
76
77
def setup_method(self):
    """
    Override in subtypes with specific setup, if any.
    """
    self.log.debug("setup")

teardown_method()

Override in subtypes with specific teardown, if any.

Source code in qa-pytest-commons/src/qa_pytest_commons/abstract_tests_base.py
79
80
81
82
83
def teardown_method(self):
    """
    Override in subtypes with specific teardown, if any.
    """
    self.log.debug("teardown")

BaseConfiguration

Bases: Configuration, LoggerMixin, ImmutableMixin

Base class for all types of configurations, providing a parser for a pre-specified configuration file.

Source code in qa-pytest-commons/src/qa_pytest_commons/base_configuration.py
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
class BaseConfiguration(Configuration, LoggerMixin, ImmutableMixin):
    """
    Base class for all types of configurations, providing a parser for a pre-specified configuration file.
    """
    _path: Path

    def __init__(self, path: Path | None = None):
        """
        Initializes the configuration by loading the associated `.ini` file.

        If `path` is not provided, the file is inferred based on the module name
        of the subclass and loaded from a structured configuration directory.

        The default lookup path follows this structure:
            <module_dir>/configurations/${TEST_ENVIRONMENT}/<module_name>.ini

        Where:
            - <module_dir> is the directory where the subclass's module is located
            - ${TEST_ENVIRONMENT} is an optional environment variable that specifies
            the subdirectory (e.g., "dev", "ci", "prod"). If unset, it defaults
            to an empty string (i.e., no subdirectory)
            - <module_name> is the name of the `.py` file defining the subclass

        Args:
            path (Path, optional): Explicit path to the configuration file. If provided,
                                overrides automatic inference.

        Raises:
            FileNotFoundError: If the resolved configuration file does not exist.
        """
        if path is None:
            module_file = Path(inspect.getfile(self.__class__))
            module_stem = module_file.stem
            resources_dir = module_file.parent / "configurations" / \
                os.environ.get("TEST_ENVIRONMENT", EMPTY_STRING)
            ini_file = resources_dir / f"{module_stem}.ini"
            self._path = ini_file
        else:
            self._path = path

        if not self._path.exists():
            raise FileNotFoundError(
                f"configuration file not found: {self._path.resolve()}")

        self.log.debug(f"using configuration from {self._path}")

    # NOTE if properties cannot be cached, this is a red-flag
    # configuration properties should be immutable.
    @final
    @cached_property
    def parser(self) -> configparser.ConfigParser:
        """
        Parser that reads this configuration.
        """
        self.log.debug(f"reading configuration from {self._path}")
        parser = configparser.ConfigParser()
        config_files = parser.read(self._path)
        self.log.debug(f"successfully read {config_files}")

        for section, pairs in get_config_overrides().items():
            if not parser.has_section(section):
                parser.add_section(section)
            for key, value in pairs.items():
                self.log.debug(f"overriding [{section}] {key} = {value}")
                parser.set(section, key, value)

        return parser

parser cached property

Parser that reads this configuration.

__init__(path=None)

Initializes the configuration by loading the associated .ini file.

If path is not provided, the file is inferred based on the module name of the subclass and loaded from a structured configuration directory.

The default lookup path follows this structure

/configurations/${TEST_ENVIRONMENT}/.ini

Where
  • is the directory where the subclass's module is located
  • ${TEST_ENVIRONMENT} is an optional environment variable that specifies the subdirectory (e.g., "dev", "ci", "prod"). If unset, it defaults to an empty string (i.e., no subdirectory)
  • is the name of the .py file defining the subclass

Parameters:

Name Type Description Default
path Path

Explicit path to the configuration file. If provided, overrides automatic inference.

None

Raises:

Type Description
FileNotFoundError

If the resolved configuration file does not exist.

Source code in qa-pytest-commons/src/qa_pytest_commons/base_configuration.py
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
def __init__(self, path: Path | None = None):
    """
    Initializes the configuration by loading the associated `.ini` file.

    If `path` is not provided, the file is inferred based on the module name
    of the subclass and loaded from a structured configuration directory.

    The default lookup path follows this structure:
        <module_dir>/configurations/${TEST_ENVIRONMENT}/<module_name>.ini

    Where:
        - <module_dir> is the directory where the subclass's module is located
        - ${TEST_ENVIRONMENT} is an optional environment variable that specifies
        the subdirectory (e.g., "dev", "ci", "prod"). If unset, it defaults
        to an empty string (i.e., no subdirectory)
        - <module_name> is the name of the `.py` file defining the subclass

    Args:
        path (Path, optional): Explicit path to the configuration file. If provided,
                            overrides automatic inference.

    Raises:
        FileNotFoundError: If the resolved configuration file does not exist.
    """
    if path is None:
        module_file = Path(inspect.getfile(self.__class__))
        module_stem = module_file.stem
        resources_dir = module_file.parent / "configurations" / \
            os.environ.get("TEST_ENVIRONMENT", EMPTY_STRING)
        ini_file = resources_dir / f"{module_stem}.ini"
        self._path = ini_file
    else:
        self._path = path

    if not self._path.exists():
        raise FileNotFoundError(
            f"configuration file not found: {self._path.resolve()}")

    self.log.debug(f"using configuration from {self._path}")

BddKeywords

Bases: ABC

Base class for defining Behavior-Driven Development (BDD) keywords.

This class provides a set of properties that represent the common BDD keywords such as given, when, then, and_, with_. Implementations might be of two types: step implementations (GenericSteps) or scenario implementations (AbstractTestsBase). In both cases, these properties must return an object that provides same step implementation, allowing a fluent-style coding.

Source code in qa-pytest-commons/src/qa_pytest_commons/bdd_keywords.py
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
class BddKeywords[TSteps:BddKeywords](ABC):
    """
    Base class for defining Behavior-Driven Development (BDD) keywords.

    This class provides a set of properties that represent the common BDD keywords
    such as `given`, `when`, `then`, `and_`, `with_`. Implementations might be
    of two types: step implementations (GenericSteps) or scenario implementations
    (AbstractTestsBase). In both cases, these properties must return an object
    that provides same step implementation, allowing a fluent-style coding.

    Type Parameters:
        TSteps (TSteps:BddKeywords): The actual steps implementation, or partial implementation.
    """

    @property
    @abstractmethod
    def given(self) -> TSteps:
        """
        Use to start definition of given stage.

        The given stage is the start-up point of a test.
        """
        pass

    @property
    @abstractmethod
    def when(self) -> TSteps:
        """
        Use to start definition of operations stage.

        The operations stage is the part that triggers some behavior on the SUT.
        """
        pass

    @property
    @abstractmethod
    def then(self) -> TSteps:
        """
        Use to start definition of verifications stage.

        The verifications stage is the part that samples actual output of the
        SUT and compares it against a predefined condition (a.k.a. rule).
        """
        pass

    @property
    @abstractmethod
    def and_(self) -> TSteps:
        """
        Use to continue definition of previous stage.
        """
        pass

    @property
    @abstractmethod
    def with_(self) -> TSteps:
        """
        Same as `and_`, sometimes it just sounds better.
        """
        pass

and_ abstractmethod property

Use to continue definition of previous stage.

given abstractmethod property

Use to start definition of given stage.

The given stage is the start-up point of a test.

then abstractmethod property

Use to start definition of verifications stage.

The verifications stage is the part that samples actual output of the SUT and compares it against a predefined condition (a.k.a. rule).

when abstractmethod property

Use to start definition of operations stage.

The operations stage is the part that triggers some behavior on the SUT.

with_ abstractmethod property

Same as and_, sometimes it just sounds better.

By

Backend-agnostic factory for element selectors. Provides static methods to create Selector objects for each locator strategy.

Source code in qa-pytest-commons/src/qa_pytest_commons/selector.py
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
class By:
    """
    Backend-agnostic factory for element selectors.
    Provides static methods to create Selector objects for each locator strategy.
    """

    @staticmethod
    def id(value: str) -> Selector:
        return Selector("id", value)

    @staticmethod
    def xpath(value: str) -> Selector:
        return Selector("xpath", value)

    @staticmethod
    def link_text(value: str) -> Selector:
        return Selector("link text", value)

    @staticmethod
    def partial_link_text(value: str) -> Selector:
        return Selector("partial link text", value)

    @staticmethod
    def name(value: str) -> Selector:
        return Selector("name", value)

    @staticmethod
    def tag_name(value: str) -> Selector:
        return Selector("tag name", value)

    @staticmethod
    def class_name(value: str) -> Selector:
        return Selector("class name", value)

    @staticmethod
    def css_selector(value: str) -> Selector:
        return Selector("css selector", value)

class_name(value) staticmethod

Source code in qa-pytest-commons/src/qa_pytest_commons/selector.py
54
55
56
@staticmethod
def class_name(value: str) -> Selector:
    return Selector("class name", value)

css_selector(value) staticmethod

Source code in qa-pytest-commons/src/qa_pytest_commons/selector.py
58
59
60
@staticmethod
def css_selector(value: str) -> Selector:
    return Selector("css selector", value)

id(value) staticmethod

Source code in qa-pytest-commons/src/qa_pytest_commons/selector.py
30
31
32
@staticmethod
def id(value: str) -> Selector:
    return Selector("id", value)
Source code in qa-pytest-commons/src/qa_pytest_commons/selector.py
38
39
40
@staticmethod
def link_text(value: str) -> Selector:
    return Selector("link text", value)

name(value) staticmethod

Source code in qa-pytest-commons/src/qa_pytest_commons/selector.py
46
47
48
@staticmethod
def name(value: str) -> Selector:
    return Selector("name", value)
Source code in qa-pytest-commons/src/qa_pytest_commons/selector.py
42
43
44
@staticmethod
def partial_link_text(value: str) -> Selector:
    return Selector("partial link text", value)

tag_name(value) staticmethod

Source code in qa-pytest-commons/src/qa_pytest_commons/selector.py
50
51
52
@staticmethod
def tag_name(value: str) -> Selector:
    return Selector("tag name", value)

xpath(value) staticmethod

Source code in qa-pytest-commons/src/qa_pytest_commons/selector.py
34
35
36
@staticmethod
def xpath(value: str) -> Selector:
    return Selector("xpath", value)

Configuration

Empty configuration base class for scenarios that do not require configuration.

Source code in qa-pytest-commons/src/qa_pytest_commons/base_configuration.py
18
19
20
21
22
class Configuration():
    """
    Empty configuration base class for scenarios that do not require configuration.
    """
    pass

GenericSteps

Bases: BddKeywords['GenericSteps'], LoggerMixin, ImmutableMixin

Generic steps base class for BDD-style test implementations. Provides retrying, assertion, and step chaining utilities for all step types.

Attributes:

Name Type Description
_retrying Retrying

The tenacity.Retrying instance used for retry logic.

_configuration GenericSteps[TConfiguration]

The configuration instance for these steps.

Source code in qa-pytest-commons/src/qa_pytest_commons/generic_steps.py
 28
 29
 30
 31
 32
 33
 34
 35
 36
 37
 38
 39
 40
 41
 42
 43
 44
 45
 46
 47
 48
 49
 50
 51
 52
 53
 54
 55
 56
 57
 58
 59
 60
 61
 62
 63
 64
 65
 66
 67
 68
 69
 70
 71
 72
 73
 74
 75
 76
 77
 78
 79
 80
 81
 82
 83
 84
 85
 86
 87
 88
 89
 90
 91
 92
 93
 94
 95
 96
 97
 98
 99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
class GenericSteps[TConfiguration: BaseConfiguration](
        BddKeywords['GenericSteps'],
        LoggerMixin,
        ImmutableMixin):
    """
    Generic steps base class for BDD-style test implementations.
    Provides retrying, assertion, and step chaining utilities for all step types.

    Type Parameters:
        TConfiguration: The configuration type for the steps implementation.

    Attributes:
        _retrying (Retrying): The tenacity.Retrying instance used for retry logic.
        _configuration (TConfiguration): The configuration instance for these steps.
    """

    _retrying: Retrying
    _configuration: TConfiguration

    def __init__(self, configuration: TConfiguration):
        """
        Initializes the steps with the given configuration and default retry policy.

        Args:
            configuration (TConfiguration): The configuration instance.
        """
        self._configuration = configuration
        # NOTE: waits 1 sec after 1st failure, 2, 4, and 8 secs on subsequent;
        # see BddScenarioTests#should_retry
        self._retrying = Retrying(
            stop=stop_after_attempt(4),
            wait=wait_exponential(min=1, max=10),
            retry=retry_if_exception_type(Exception),
            before_sleep=before_sleep_log(self.log, logging.DEBUG)
        )

    @final
    @property
    def configured(self) -> TConfiguration:
        """
        Returns the configuration instance for these steps.

        Returns:
            TConfiguration: The configuration instance.
        """
        return self._configuration

    @final
    @property
    def retry_policy(self) -> Retrying:
        """
        Returns the retry policy used for retrying steps.

        Returns:
            Retrying: The tenacity.Retrying instance.
        """
        return self._retrying

    @final
    @property
    @override
    def given(self) -> Self:
        Context.set(lambda m: f"Given {m}")
        return self

    @final
    @property
    @override
    def when(self) -> Self:
        Context.set(lambda m: f"When {m}")
        return self

    @final
    @property
    @override
    def then(self) -> Self:
        Context.set(lambda m: f"Then {m}")
        return self

    @final
    @property
    @override
    def and_(self) -> Self:
        Context.set(lambda m: f"And {m}")
        return self

    @final
    @property
    @override
    def with_(self) -> Self:
        Context.set(lambda m: f"With {m}")
        return self

    @final
    @property
    @Context.traced
    def nothing(self) -> Self:
        """
        Intended to support self-testing which does not rely on outer world system.

        Returns:
            Self: these steps
        """
        return self

    # DELETEME
    # # @Context.traced -- nothing to trace here...
    # def configuration(self, configuration: TConfiguration) -> Self:
    #     """
    #     Sets the configuration to use.

    #     Args:
    #         configuration (TConfiguration): the configuration

    #     Returns:
    #         Self: these steps
    #     """
    #     self._configuration = configuration
    #     return self

    def set[T:Valid](self, field_name: str, field_value: T) -> T:
        """
        Sets field to specified value, validating it if possible.

        Args:
            field_name (str): name of field; the field should be defined as annotation
            field_value (T:Valid): value of field that can be validated

        Raises:
            AttributeError: if the field is not defined
            TypeError: if the object does not support the Valid protocol
            InvalidValueException: if the object is invalid

        Returns:
            T: the value of set field
        """
        if field_name not in self.__class__.__annotations__:
            raise AttributeError(
                f"{field_name} is not a valid attribute of "
                f"{self.__class__.__name__}.")

        setattr(self, field_name, valid(field_value))
        return field_value

    @final
    def step(self, *args: Any) -> Self:
        """
        Casts anything to a step.

        Returns:
            Self: these steps
        """
        return self

    @final
    def tracing(self, value: Any) -> Self:
        """
        Logs value at DEBUG level using the logger of this steps class.

        Args:
            value (Any): The value to log.
        Returns:
            Self: these steps
        """
        self.log.debug(f"=== {value}")
        return self

    @final
    @Context.traced
    def waiting(self, duration: timedelta = timedelta(seconds=0)) -> Self:
        """
        Blocks current thread for specified duration.

        Args:
            duration (timedelta, optional): How long to wait. Defaults to 0 seconds.
        Returns:
            Self: these steps
        """
        sleep_for(duration)
        return self

    @final
    @Context.traced
    def failing(self, exception: Exception) -> Self:
        """
        Raises the given exception, for self-testing of retrying and eventually_assert_that.

        Args:
            exception (Exception): The exception to raise.
        Raises:
            exception: That exception.
        Returns:
            Self: these steps
        """
        raise exception

    @final
    @Context.traced
    def repeating(self, range: range, step: Callable[[int], Self]) -> Self:
        """
        Repeats the specified step for each value in the range.

        Args:
            range (range): The range to iterate over.
            step (Callable[[int], Self]): The step to repeat.
        Returns:
            Self: these steps
        """
        seq(range).for_each(step)  # type: ignore
        return self

    # TODO parallel_repeating

    @final
    @Context.traced
    def safely(self, step: Callable[[], Self]) -> Self:
        """
        Executes specified step, swallowing its exceptions.

        Args:
            step (Callable[[], Self]): The step to execute.
        Returns:
            Self: these steps
        """
        return safely(lambda: step()).value_or(self)

    # TODO implement a raises decorator to mark method as raising some exception
    # at run-time the decorator shall check if raised exception matches the declared list.
    # This one would be:
    # @raises(tenacity.RetryError)
    @final
    # @Context.traced
    def retrying(self, step: Callable[[], Self]) -> Self:
        '''
        Retries specified step according to _retry_policy.

        Args:
            step (Callable[[], Self]): The step to retry.
        Returns:
            Self: these steps
        '''
        return self._retrying(step)

    @final
    def eventually_assert_that[T](
            self, supplier: Supplier[T],
            by_rule: Matcher[T]) -> Self:
        '''
        Repeatedly applies specified rule on specified supplier, according to _retry_policy.

        Args:
            supplier (Callable[[], T]): The value supplier.
            by_rule (Matcher[T]): The matcher to apply.
        Returns:
            Self: these steps
        '''
        return self._retrying(lambda: self._assert_that(supplier(), by_rule))

    @final
    @Context.traced
    def it_works(self, matcher: Matcher[bool]) -> Self:
        """
        Intended to support self-testing of reports.

        Args:
            matcher (Matcher[bool]): Matcher for the boolean result.
        Returns:
            Self: these steps
        """
        assert_that(True, matcher)
        return self

    @final
    # NOTE @Context.traced here is redundant
    def _assert_that[T](self, value: T, by_rule: Matcher[T]) -> Self:
        """
        Adapts PyHamcrest's assert_that to the BDD world by returning Self.

        Args:
            value (T): The value to assert upon.
            by_rule (Matcher[T]): The matcher to apply.
        Returns:
            Self: these steps
        """
        assert_that(value, by_rule)
        return self

and_ property

configured property

Returns the configuration instance for these steps.

Returns:

Name Type Description
TConfiguration GenericSteps[TConfiguration]

The configuration instance.

given property

nothing property

Intended to support self-testing which does not rely on outer world system.

Returns:

Name Type Description
Self Self

these steps

retry_policy property

Returns the retry policy used for retrying steps.

Returns:

Name Type Description
Retrying Retrying

The tenacity.Retrying instance.

then property

when property

with_ property

__init__(configuration)

Initializes the steps with the given configuration and default retry policy.

Parameters:

Name Type Description Default
configuration GenericSteps[TConfiguration]

The configuration instance.

required
Source code in qa-pytest-commons/src/qa_pytest_commons/generic_steps.py
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
def __init__(self, configuration: TConfiguration):
    """
    Initializes the steps with the given configuration and default retry policy.

    Args:
        configuration (TConfiguration): The configuration instance.
    """
    self._configuration = configuration
    # NOTE: waits 1 sec after 1st failure, 2, 4, and 8 secs on subsequent;
    # see BddScenarioTests#should_retry
    self._retrying = Retrying(
        stop=stop_after_attempt(4),
        wait=wait_exponential(min=1, max=10),
        retry=retry_if_exception_type(Exception),
        before_sleep=before_sleep_log(self.log, logging.DEBUG)
    )

eventually_assert_that(supplier, by_rule)

Repeatedly applies specified rule on specified supplier, according to _retry_policy.

Parameters:

Name Type Description Default
supplier Callable[[], eventually_assert_that[T]]

The value supplier.

required
by_rule Matcher[eventually_assert_that[T]]

The matcher to apply.

required

Returns: Self: these steps

Source code in qa-pytest-commons/src/qa_pytest_commons/generic_steps.py
271
272
273
274
275
276
277
278
279
280
281
282
283
284
@final
def eventually_assert_that[T](
        self, supplier: Supplier[T],
        by_rule: Matcher[T]) -> Self:
    '''
    Repeatedly applies specified rule on specified supplier, according to _retry_policy.

    Args:
        supplier (Callable[[], T]): The value supplier.
        by_rule (Matcher[T]): The matcher to apply.
    Returns:
        Self: these steps
    '''
    return self._retrying(lambda: self._assert_that(supplier(), by_rule))

failing(exception)

Raises the given exception, for self-testing of retrying and eventually_assert_that.

Parameters:

Name Type Description Default
exception Exception

The exception to raise.

required

Raises: exception: That exception. Returns: Self: these steps

Source code in qa-pytest-commons/src/qa_pytest_commons/generic_steps.py
209
210
211
212
213
214
215
216
217
218
219
220
221
222
@final
@Context.traced
def failing(self, exception: Exception) -> Self:
    """
    Raises the given exception, for self-testing of retrying and eventually_assert_that.

    Args:
        exception (Exception): The exception to raise.
    Raises:
        exception: That exception.
    Returns:
        Self: these steps
    """
    raise exception

it_works(matcher)

Intended to support self-testing of reports.

Parameters:

Name Type Description Default
matcher Matcher[bool]

Matcher for the boolean result.

required

Returns: Self: these steps

Source code in qa-pytest-commons/src/qa_pytest_commons/generic_steps.py
286
287
288
289
290
291
292
293
294
295
296
297
298
@final
@Context.traced
def it_works(self, matcher: Matcher[bool]) -> Self:
    """
    Intended to support self-testing of reports.

    Args:
        matcher (Matcher[bool]): Matcher for the boolean result.
    Returns:
        Self: these steps
    """
    assert_that(True, matcher)
    return self

repeating(range, step)

Repeats the specified step for each value in the range.

Parameters:

Name Type Description Default
range range

The range to iterate over.

required
step Callable[[int], Self]

The step to repeat.

required

Returns: Self: these steps

Source code in qa-pytest-commons/src/qa_pytest_commons/generic_steps.py
224
225
226
227
228
229
230
231
232
233
234
235
236
237
@final
@Context.traced
def repeating(self, range: range, step: Callable[[int], Self]) -> Self:
    """
    Repeats the specified step for each value in the range.

    Args:
        range (range): The range to iterate over.
        step (Callable[[int], Self]): The step to repeat.
    Returns:
        Self: these steps
    """
    seq(range).for_each(step)  # type: ignore
    return self

retrying(step)

Retries specified step according to _retry_policy.

Parameters:

Name Type Description Default
step Callable[[], Self]

The step to retry.

required

Returns: Self: these steps

Source code in qa-pytest-commons/src/qa_pytest_commons/generic_steps.py
258
259
260
261
262
263
264
265
266
267
268
269
@final
# @Context.traced
def retrying(self, step: Callable[[], Self]) -> Self:
    '''
    Retries specified step according to _retry_policy.

    Args:
        step (Callable[[], Self]): The step to retry.
    Returns:
        Self: these steps
    '''
    return self._retrying(step)

safely(step)

Executes specified step, swallowing its exceptions.

Parameters:

Name Type Description Default
step Callable[[], Self]

The step to execute.

required

Returns: Self: these steps

Source code in qa-pytest-commons/src/qa_pytest_commons/generic_steps.py
241
242
243
244
245
246
247
248
249
250
251
252
@final
@Context.traced
def safely(self, step: Callable[[], Self]) -> Self:
    """
    Executes specified step, swallowing its exceptions.

    Args:
        step (Callable[[], Self]): The step to execute.
    Returns:
        Self: these steps
    """
    return safely(lambda: step()).value_or(self)

set(field_name, field_value)

Sets field to specified value, validating it if possible.

Parameters:

Name Type Description Default
field_name str

name of field; the field should be defined as annotation

required
field_value (T

Valid): value of field that can be validated

required

Raises:

Type Description
AttributeError

if the field is not defined

TypeError

if the object does not support the Valid protocol

InvalidValueException

if the object is invalid

Returns:

Name Type Description
T set[T]

the value of set field

Source code in qa-pytest-commons/src/qa_pytest_commons/generic_steps.py
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
def set[T:Valid](self, field_name: str, field_value: T) -> T:
    """
    Sets field to specified value, validating it if possible.

    Args:
        field_name (str): name of field; the field should be defined as annotation
        field_value (T:Valid): value of field that can be validated

    Raises:
        AttributeError: if the field is not defined
        TypeError: if the object does not support the Valid protocol
        InvalidValueException: if the object is invalid

    Returns:
        T: the value of set field
    """
    if field_name not in self.__class__.__annotations__:
        raise AttributeError(
            f"{field_name} is not a valid attribute of "
            f"{self.__class__.__name__}.")

    setattr(self, field_name, valid(field_value))
    return field_value

step(*args)

Casts anything to a step.

Returns:

Name Type Description
Self Self

these steps

Source code in qa-pytest-commons/src/qa_pytest_commons/generic_steps.py
172
173
174
175
176
177
178
179
180
@final
def step(self, *args: Any) -> Self:
    """
    Casts anything to a step.

    Returns:
        Self: these steps
    """
    return self

tracing(value)

Logs value at DEBUG level using the logger of this steps class.

Parameters:

Name Type Description Default
value Any

The value to log.

required

Returns: Self: these steps

Source code in qa-pytest-commons/src/qa_pytest_commons/generic_steps.py
182
183
184
185
186
187
188
189
190
191
192
193
@final
def tracing(self, value: Any) -> Self:
    """
    Logs value at DEBUG level using the logger of this steps class.

    Args:
        value (Any): The value to log.
    Returns:
        Self: these steps
    """
    self.log.debug(f"=== {value}")
    return self

waiting(duration=timedelta(seconds=0))

Blocks current thread for specified duration.

Parameters:

Name Type Description Default
duration timedelta

How long to wait. Defaults to 0 seconds.

timedelta(seconds=0)

Returns: Self: these steps

Source code in qa-pytest-commons/src/qa_pytest_commons/generic_steps.py
195
196
197
198
199
200
201
202
203
204
205
206
207
@final
@Context.traced
def waiting(self, duration: timedelta = timedelta(seconds=0)) -> Self:
    """
    Blocks current thread for specified duration.

    Args:
        duration (timedelta, optional): How long to wait. Defaults to 0 seconds.
    Returns:
        Self: these steps
    """
    sleep_for(duration)
    return self

Selector dataclass

Represents an element selector as a (by, value) pair.

Attributes:

Name Type Description
by str

The locator strategy (e.g., "id", "xpath", "css selector").

value str

The selector value.

Source code in qa-pytest-commons/src/qa_pytest_commons/selector.py
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
@dataclass(frozen=True)
class Selector:
    """
    Represents an element selector as a (by, value) pair.

    Attributes:
        by (str): The locator strategy (e.g., "id", "xpath", "css selector").
        value (str): The selector value.
    """
    by: str
    value: str

    def as_tuple(self) -> Tuple[str, str]:
        """
        Returns the selector as a tuple (by, value), suitable for backend adapters.
        """
        return (self.by, self.value)

by instance-attribute

value instance-attribute

__init__(by, value)

as_tuple()

Returns the selector as a tuple (by, value), suitable for backend adapters.

Source code in qa-pytest-commons/src/qa_pytest_commons/selector.py
17
18
19
20
21
def as_tuple(self) -> Tuple[str, str]:
    """
    Returns the selector as a tuple (by, value), suitable for backend adapters.
    """
    return (self.by, self.value)

UiConfiguration

Bases: BaseConfiguration

UI configuration base class exposing entry point.

Backend-specific browser settings are read directly from the configuration parser by the respective adapters (Selenium, Playwright) to avoid forcing abstraction over incompatible configuration models.

Source code in qa-pytest-commons/src/qa_pytest_commons/ui_configuration.py
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
class UiConfiguration(BaseConfiguration):
    """
    UI configuration base class exposing entry point.

    Backend-specific browser settings are read directly from the configuration
    parser by the respective adapters (Selenium, Playwright) to avoid forcing
    abstraction over incompatible configuration models.
    """
    @cached_property
    @final
    def entry_point(self) -> str:
        """
        Returns the UI URL from the configuration parser.

        Returns:
            str: The URL string specified under the "ui/entry_point" in the configuration.

        Raises:
            KeyError: If the "ui" section or "entry_point" key is not present in the configuration parser.
        """
        return self.parser["ui"]["entry_point"]

entry_point cached property

Returns the UI URL from the configuration parser.

Returns:

Name Type Description
str str

The URL string specified under the "ui/entry_point" in the configuration.

Raises:

Type Description
KeyError

If the "ui" section or "entry_point" key is not present in the configuration parser.

UiContext

Bases: Protocol[TElement]

Source code in qa-pytest-commons/src/qa_pytest_commons/ui_protocols.py
39
40
41
42
43
44
45
46
class UiContext(Protocol[TElement]):
    def find_element(self, by: str, value: Optional[str]) -> TElement: ...

    def find_elements(
        self, by: str, value: Optional[str]) -> Iterator[TElement]: ...

    def get(self, url: str) -> None: ...
    def execute_script(self, script: str, *args: UiElement) -> Any: ...

execute_script(script, *args)

Source code in qa-pytest-commons/src/qa_pytest_commons/ui_protocols.py
46
def execute_script(self, script: str, *args: UiElement) -> Any: ...

find_element(by, value)

Source code in qa-pytest-commons/src/qa_pytest_commons/ui_protocols.py
40
def find_element(self, by: str, value: Optional[str]) -> TElement: ...

find_elements(by, value)

Source code in qa-pytest-commons/src/qa_pytest_commons/ui_protocols.py
42
43
def find_elements(
    self, by: str, value: Optional[str]) -> Iterator[TElement]: ...

get(url)

Source code in qa-pytest-commons/src/qa_pytest_commons/ui_protocols.py
45
def get(self, url: str) -> None: ...

UiElement

Bases: Protocol

Source code in qa-pytest-commons/src/qa_pytest_commons/ui_protocols.py
29
30
31
32
33
34
35
36
class UiElement(Protocol):
    def click(self) -> None: ...
    def type(self, text: str) -> None: ...
    def clear(self) -> None: ...
    def scroll_into_view(self) -> None: ...

    @property
    def text(self) -> str: ...

text property

clear()

Source code in qa-pytest-commons/src/qa_pytest_commons/ui_protocols.py
32
def clear(self) -> None: ...

click()

Source code in qa-pytest-commons/src/qa_pytest_commons/ui_protocols.py
30
def click(self) -> None: ...

scroll_into_view()

Source code in qa-pytest-commons/src/qa_pytest_commons/ui_protocols.py
33
def scroll_into_view(self) -> None: ...

type(text)

Source code in qa-pytest-commons/src/qa_pytest_commons/ui_protocols.py
31
def type(self, text: str) -> None: ...

UiSteps

Bases: GenericSteps[UiSteps[TConfiguration]]

Common BDD-style step definitions for UI automation (backend-agnostic).

This base class provides shared UI operations for both Selenium and Playwright implementations, leveraging the UiContext and UiElement protocols.

Attributes:

Name Type Description
_ui_context UiContext[UiElement]

The UI context instance used for browser automation.

Source code in qa-pytest-commons/src/qa_pytest_commons/ui_steps.py
 20
 21
 22
 23
 24
 25
 26
 27
 28
 29
 30
 31
 32
 33
 34
 35
 36
 37
 38
 39
 40
 41
 42
 43
 44
 45
 46
 47
 48
 49
 50
 51
 52
 53
 54
 55
 56
 57
 58
 59
 60
 61
 62
 63
 64
 65
 66
 67
 68
 69
 70
 71
 72
 73
 74
 75
 76
 77
 78
 79
 80
 81
 82
 83
 84
 85
 86
 87
 88
 89
 90
 91
 92
 93
 94
 95
 96
 97
 98
 99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
class UiSteps[TConfiguration: BaseConfiguration](GenericSteps[TConfiguration]):
    """
    Common BDD-style step definitions for UI automation (backend-agnostic).

    This base class provides shared UI operations for both Selenium and Playwright
    implementations, leveraging the UiContext and UiElement protocols.

    Type Parameters:
        TConfiguration: The configuration type for the test steps.

    Attributes:
        _ui_context (UiContext[UiElement]): The UI context instance used for browser automation.
    """

    _ui_context: UiContext[UiElement]

    @final
    @Context.traced
    def ui_context(self, context: UiContext[UiElement]) -> Self:
        """
        Sets the UI context instance (backend-agnostic).

        Args:
            context (UiContext[UiElement]): The UI context instance.
        Returns:
            Self: The current step instance for chaining.
        """
        self._ui_context = context
        return self

    @final
    @Context.traced
    def at(self, url: str) -> Self:
        """
        Navigates to the specified URL with retry logic.

        Args:
            url (str): The URL to navigate to.
        Returns:
            Self: The current step instance for chaining.
        """
        def _navigate() -> Self:
            self._ui_context.get(url)
            return self

        return self.retrying(_navigate)

    @final
    @Context.traced
    def clicking_once(self, element_supplier: ElementSupplier) -> Self:
        """
        Clicks the element supplied by the given callable.

        Args:
            element_supplier (ElementSupplier): Callable returning a UI element.
        Returns:
            Self: The current step instance for chaining.
        """
        element_supplier().click()
        return self

    @overload
    def clicking(self, element: Selector) -> Self: ...

    @overload
    def clicking(self, element: ElementSupplier) -> Self: ...

    @final
    def clicking(self, element: SelectorOrSupplier) -> Self:
        """
        Clicks the element specified by a selector or supplier, with retry logic.

        Args:
            element (SelectorOrSupplier): Selector or callable returning a UI element.
        Returns:
            Self: The current step instance for chaining.
        """
        return self.retrying(lambda: self.clicking_once(
            self._resolve_element(element)))

    @final
    @Context.traced
    def typing_once(self, element_supplier: ElementSupplier, text: str) -> Self:
        """
        Types the given text into the element supplied by the callable.

        Args:
            element_supplier (ElementSupplier): Callable returning a UI element.
            text (str): The text to type.
        Returns:
            Self: The current step instance for chaining.
        """
        element_supplier().type(text)
        return self

    @overload
    def typing(self, element: Selector, text: str) -> Self: ...

    @overload
    def typing(self, element: ElementSupplier, text: str) -> Self: ...

    @final
    def typing(self, element: SelectorOrSupplier, text: str) -> Self:
        """
        Types the given text into the element specified by a selector or supplier, with retry logic.

        Args:
            element (SelectorOrSupplier): Selector or callable returning a UI element.
            text (str): The text to type.
        Returns:
            Self: The current step instance for chaining.
        """
        return self.retrying(
            lambda: self.typing_once(self._resolve_element(element), text)
        )

    @final
    def the_element(
        self,
        selector: Selector,
        by_rule: Matcher[UiElement],
        context: Optional[UiContext[UiElement]] = None,
    ) -> Self:
        """
        Asserts that the element found by the selector matches the given matcher.

        Args:
            selector (Selector): The selector to find the element.
            by_rule (Matcher[UiElement]): Matcher for the element.
            context (Optional[UiContext[UiElement]]): Optional UI context.
        Returns:
            Self: The current step instance for chaining.
        """
        return self.eventually_assert_that(
            lambda: self._element(selector, context), by_rule
        )

    @final
    def the_elements(
        self,
        selector: Selector,
        by_rule: Matcher[Iterator[UiElement]],
        context: Optional[UiContext[UiElement]] = None,
    ) -> Self:
        """
        Asserts that the elements found by the selector match the given matcher.

        Args:
            selector (Selector): The selector to find the elements.
            by_rule (Matcher[Iterator[UiElement]]): Matcher for the elements iterator.
            context (Optional[UiContext[UiElement]]): Optional UI context.
        Returns:
            Self: The current step instance for chaining.
        """
        return self.eventually_assert_that(
            lambda: self._elements(selector, context), by_rule
        )

    @final
    @Context.traced
    def _elements(
        self, selector: Selector, context: Optional[UiContext[UiElement]] = None
    ) -> Iterator[UiElement]:
        return self._resolve_context(context).find_elements(
            *selector.as_tuple())

    @final
    @Context.traced
    def _element(
        self, selector: Selector, context: Optional[UiContext[UiElement]] = None
    ) -> UiElement:  # type: ignore[override]
        return self._scroll_into_view(
            self._resolve_context(context).find_element(*selector.as_tuple())
        )

    def _scroll_into_view(self, element: UiElement) -> UiElement:
        element.scroll_into_view()
        return element

    def _resolve_context(
        self, context: Optional[UiContext[UiElement]] = None
    ) -> UiContext[UiElement]:
        return context or self._ui_context

    @final
    def _resolve_element(self, element: SelectorOrSupplier) -> ElementSupplier:
        if isinstance(element, Selector):
            return lambda: self._element(element)
        return element

at(url)

Navigates to the specified URL with retry logic.

Parameters:

Name Type Description Default
url str

The URL to navigate to.

required

Returns: Self: The current step instance for chaining.

Source code in qa-pytest-commons/src/qa_pytest_commons/ui_steps.py
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
@final
@Context.traced
def at(self, url: str) -> Self:
    """
    Navigates to the specified URL with retry logic.

    Args:
        url (str): The URL to navigate to.
    Returns:
        Self: The current step instance for chaining.
    """
    def _navigate() -> Self:
        self._ui_context.get(url)
        return self

    return self.retrying(_navigate)

clicking(element)

clicking(element: Selector) -> Self
clicking(element: ElementSupplier) -> Self

Clicks the element specified by a selector or supplier, with retry logic.

Parameters:

Name Type Description Default
element SelectorOrSupplier

Selector or callable returning a UI element.

required

Returns: Self: The current step instance for chaining.

Source code in qa-pytest-commons/src/qa_pytest_commons/ui_steps.py
87
88
89
90
91
92
93
94
95
96
97
98
@final
def clicking(self, element: SelectorOrSupplier) -> Self:
    """
    Clicks the element specified by a selector or supplier, with retry logic.

    Args:
        element (SelectorOrSupplier): Selector or callable returning a UI element.
    Returns:
        Self: The current step instance for chaining.
    """
    return self.retrying(lambda: self.clicking_once(
        self._resolve_element(element)))

clicking_once(element_supplier)

Clicks the element supplied by the given callable.

Parameters:

Name Type Description Default
element_supplier ElementSupplier

Callable returning a UI element.

required

Returns: Self: The current step instance for chaining.

Source code in qa-pytest-commons/src/qa_pytest_commons/ui_steps.py
67
68
69
70
71
72
73
74
75
76
77
78
79
@final
@Context.traced
def clicking_once(self, element_supplier: ElementSupplier) -> Self:
    """
    Clicks the element supplied by the given callable.

    Args:
        element_supplier (ElementSupplier): Callable returning a UI element.
    Returns:
        Self: The current step instance for chaining.
    """
    element_supplier().click()
    return self

the_element(selector, by_rule, context=None)

Asserts that the element found by the selector matches the given matcher.

Parameters:

Name Type Description Default
selector Selector

The selector to find the element.

required
by_rule Matcher[UiElement]

Matcher for the element.

required
context Optional[UiContext[UiElement]]

Optional UI context.

None

Returns: Self: The current step instance for chaining.

Source code in qa-pytest-commons/src/qa_pytest_commons/ui_steps.py
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
@final
def the_element(
    self,
    selector: Selector,
    by_rule: Matcher[UiElement],
    context: Optional[UiContext[UiElement]] = None,
) -> Self:
    """
    Asserts that the element found by the selector matches the given matcher.

    Args:
        selector (Selector): The selector to find the element.
        by_rule (Matcher[UiElement]): Matcher for the element.
        context (Optional[UiContext[UiElement]]): Optional UI context.
    Returns:
        Self: The current step instance for chaining.
    """
    return self.eventually_assert_that(
        lambda: self._element(selector, context), by_rule
    )

the_elements(selector, by_rule, context=None)

Asserts that the elements found by the selector match the given matcher.

Parameters:

Name Type Description Default
selector Selector

The selector to find the elements.

required
by_rule Matcher[Iterator[UiElement]]

Matcher for the elements iterator.

required
context Optional[UiContext[UiElement]]

Optional UI context.

None

Returns: Self: The current step instance for chaining.

Source code in qa-pytest-commons/src/qa_pytest_commons/ui_steps.py
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
@final
def the_elements(
    self,
    selector: Selector,
    by_rule: Matcher[Iterator[UiElement]],
    context: Optional[UiContext[UiElement]] = None,
) -> Self:
    """
    Asserts that the elements found by the selector match the given matcher.

    Args:
        selector (Selector): The selector to find the elements.
        by_rule (Matcher[Iterator[UiElement]]): Matcher for the elements iterator.
        context (Optional[UiContext[UiElement]]): Optional UI context.
    Returns:
        Self: The current step instance for chaining.
    """
    return self.eventually_assert_that(
        lambda: self._elements(selector, context), by_rule
    )

typing(element, text)

typing(element: Selector, text: str) -> Self
typing(element: ElementSupplier, text: str) -> Self

Types the given text into the element specified by a selector or supplier, with retry logic.

Parameters:

Name Type Description Default
element SelectorOrSupplier

Selector or callable returning a UI element.

required
text str

The text to type.

required

Returns: Self: The current step instance for chaining.

Source code in qa-pytest-commons/src/qa_pytest_commons/ui_steps.py
121
122
123
124
125
126
127
128
129
130
131
132
133
134
@final
def typing(self, element: SelectorOrSupplier, text: str) -> Self:
    """
    Types the given text into the element specified by a selector or supplier, with retry logic.

    Args:
        element (SelectorOrSupplier): Selector or callable returning a UI element.
        text (str): The text to type.
    Returns:
        Self: The current step instance for chaining.
    """
    return self.retrying(
        lambda: self.typing_once(self._resolve_element(element), text)
    )

typing_once(element_supplier, text)

Types the given text into the element supplied by the callable.

Parameters:

Name Type Description Default
element_supplier ElementSupplier

Callable returning a UI element.

required
text str

The text to type.

required

Returns: Self: The current step instance for chaining.

Source code in qa-pytest-commons/src/qa_pytest_commons/ui_steps.py
100
101
102
103
104
105
106
107
108
109
110
111
112
113
@final
@Context.traced
def typing_once(self, element_supplier: ElementSupplier, text: str) -> Self:
    """
    Types the given text into the element supplied by the callable.

    Args:
        element_supplier (ElementSupplier): Callable returning a UI element.
        text (str): The text to type.
    Returns:
        Self: The current step instance for chaining.
    """
    element_supplier().type(text)
    return self

ui_context(context)

Sets the UI context instance (backend-agnostic).

Parameters:

Name Type Description Default
context UiContext[UiElement]

The UI context instance.

required

Returns: Self: The current step instance for chaining.

Source code in qa-pytest-commons/src/qa_pytest_commons/ui_steps.py
36
37
38
39
40
41
42
43
44
45
46
47
48
@final
@Context.traced
def ui_context(self, context: UiContext[UiElement]) -> Self:
    """
    Sets the UI context instance (backend-agnostic).

    Args:
        context (UiContext[UiElement]): The UI context instance.
    Returns:
        Self: The current step instance for chaining.
    """
    self._ui_context = context
    return self