Metadata-Version: 2.3
Name: selenium-query
Version: 0.3.0
Summary: Python equivalent of jQuery, to write selenium tests.
License: GPL-3.0-or-later
Author: Frédéric Zinelli
Author-email: frederic.zinelli@gmail.com
Requires-Python: >=3.9, <4.0
Classifier: License :: OSI Approved :: GNU General Public License v3 or later (GPLv3+)
Classifier: Programming Language :: Python :: 3
Classifier: Programming Language :: Python :: 3.9
Classifier: Programming Language :: Python :: 3.10
Classifier: Programming Language :: Python :: 3.11
Classifier: Programming Language :: Python :: 3.12
Classifier: Programming Language :: Python :: 3.13
Requires-Dist: selenium (>=4.36)
Description-Content-Type: text/markdown

# Selenium Query


> **Selenium Query is an augmented wrapper around python-selenium, inspired by jQuery's collection-oriented model. Its main goal is to hide technicalities when writing tests, to obtain a more declarative code.**
>
> Selenium Query is heavily inspired by the way jQuery manipulates collections of elements: instead of thinking about one Selenium WebElement at a time, the library lets you build queries representing collections of matching elements and apply operations to them as a whole.
>
> The important distinction is therefore:
>
> ```
> selenium query → collection of matching elements → operation on the collection
> ```
>
> This is closer to:
>
> $(".button").click()
>
> than to manually retrieving and manipulating one element at a time.
>
> The main user-facing entry point is `BaseSeleniumPage.find(...)`.
>
> Users should generally **not instantiate `Getter` directly**. `Getter` is the central query object returned by the page API and provides access to the other query behaviors.

You may find below a very short overview of the possibilities. See the implementation for more details.

---

## `BaseSeleniumPage`

This is the normal entry point for using Selenium Query.

A page object inherits from `BaseSeleniumPage` and uses `find(...)` to create queries:

```python
from selenium_query import BaseSeleniumPage


class MyPage(BaseSeleniumPage):

    def click_submit(self):
        self.find("#submit").click().go
        # .go is an alias for Selenium ActionChain.perform()
```

`find(...)` internally creates a page-scoped query:

```python
self.find("#submit") # -> Getter
```

This is equivalent to using the underlying `Getter.find(..., in_page=True)` mechanism, but `BaseSeleniumPage.find(...)` is the normal user-facing API.

`BaseSeleniumPage` also provides the page-level Selenium helpers used by the package, such as:

```python
MyPage.wait_for_element("#submit")
MyPage.wait_until(lambda driver: driver.title == "Ready")
MyPage.run_js("return arguments[0] + arguments[1]", 1, 2)
```

In normal usage, page classes inherit from `BaseSeleniumPage` and expose application-specific operations.

---

## `Getter`

`Getter` is the central query object.

Again, the usual way to obtain one is:

```python
query: Getter = MyPage.find(".item")
```

not:

```python
Getter(...)
```

A `Getter` represents the current collection of matching elements and provides access to the other query behaviors:

* It relays most of the usual selenium properties/methods, as well as some "extras":

    Observers :

    ```
    id
    class_                  - extra
    tag_name
    text
    rect
    is_enabled()
    is_selected()
    is_displayed()
    exists()                - extra
    count(n)                - extra
    ```

    Note: when used without "helpers" properties (see below), these behave like predicates. See implementation details.

    It also makes most of the `ActionChain` properties/methods (also with extras) directly accessible from the `Getter` object, Selenium Query handling the underlying logistic fore the user:

    ```
    go                      - extra / alias for perform(), as a @property
    reset_actions
    swap_duration           - extra / context manager
    context_chain_and_go    - extra / context manager
    click
    click_and_hold
    context_click
    double_click
    drag_and_drop
    drag_and_drop_by_offset
    key_down
    key_up
    send_keys_to_element
    send_keys
    send_shortcut           - extra
    move_by_offset
    move_to_element
    move_to                 - extra / alias for move_to_element
    move_to_element_with_offset
    release
    pause
    scroll_to               - behaves differently from selenium
    __getitem__             - extra (index or slice)
    ```

* It also adds its own helpers, to change the behaviors of the main observers (see above):

    ```python
    items = MyPage.find(".item")

    items.get       # extract values from the collection
    items.check     # assert various things about the elements of the collections
    items.filter    # filter the current collection (new instance)
    items.order_by  # sort the collection (new instance)
    items.map       # build a collection of collections (MapperGetter)
    items.any       # verify at least one element in the collection matches a condition
    items.all       # verify all elements in the collection match a condition
    items.not_      # revert the behavior (for check.not_, filter.not_, all.not_ or not_.all, ...)
    ```

For example (each method is a different way to use selenium query objects):

```python
class MyPage(BaseSeleniumPage):

    def get_items_contents(self):
        return self.find(".item").get.text

    def test_items_count(self):
        self.find(".item").check.count(4)

    def test_items_contents(self):
        self.find(".item").check.text(['a', 'b', 'c', 'd'])
```

`Getter` also supports merging collections:

```python
first = MyPage.find(".first")
second = MyPage.find(".second")

merged = first.merge(second, ...)
# or as a classmethod:
merged2 = Getter.merge_cls(first, second, ...)
```

The resulting query contains the elements from all collections passed as argument.

---

## `ValuesGetter`

`ValuesGetter` is accessed through the `.get` property:

```python
items = MyPage.find(".item")

texts = items.get.text
ids = items.get.id
classes = items.get.class_
...
```

For example:

```python
class MyPage(BaseSeleniumPage):

    def item_texts(self):
        return self.find(".item").get.text

    def item_ids(self):
        return self.find(".item").get.id
```

The available value accessors include:

```python
query.get.id
query.get.class_
query.get.tag_name
query.get.text
query.get.rect
query.get.is_enabled()
query.get.is_selected()
query.get.is_displayed()
query.get.count()
```

CSS properties, `data-*` attributes and arbitrary properties can also be extracted:

```python
query.get.css("display", "color")
query.get.data("status")
query.get.props("href", "class")
```

For example:

```python
class MyPage(BaseSeleniumPage):

    def statuses(self):
        return self.find(".item").get.data("status")
```

The output shape depends on the number of matched elements and requested values:

```text
one element + one value      → value
many elements + one value    → list of values
many values requested        → dictionary or list of dictionaries
```

---

## `AsserterGetter`

`AsserterGetter` is accessed through `.check`.

It performs assertions on the current collection and returns the query, allowing assertions to be chained.

For example:

```python
class MyPage(BaseSeleniumPage):

    def check_page(self):
        self.find(".item").check.exists()
```

A value can be checked against every element:

```python
class MyPage(BaseSeleniumPage):

    def check_items(self):
        self.find(".item").check.text("Expected text")
```

Different expected values for various elements of the collection can also be supplied with a list/tuple of appropriate length:

```python
self.find(".item").check.text(
    ["First item", "Second item", "Third item"]
)
```

A custom assertion message can be added by calling `.check(...)`:

```python
self.find(".item").check(
    "The item list should contain the expected entries."
).text(
    ["First item", "Second item"]
)
```

The `.check` API is particularly useful when the test needs to validate a collection as a whole rather than manually retrieving every `WebElement`.

---

## `FilterGetter`

`FilterGetter` is accessed through `.filter`.

It transforms a collection into a new collection containing only the matching elements.

For example:

```python
class MyPage(BaseSeleniumPage):

    def active_items(self):
        return self.find(".item").filter.data(status="active")
```

The result is another query containing only elements whose `data-status` value matches `"active"`.

The same filtering mechanism can be used with other value-based accessors, such as:

```python
self.find(".item").filter.id("item-1")
self.find(".item").filter.class_("selected")
self.find(".item").filter.text("Expected text")
```

Custom predicates are also supported:

```python
class MyPage(BaseSeleniumPage):

    def visible_items(self):
        return self.find(".item").filter(
            lambda item: item.get.text.startswith("specifics")
        )
```

The predicate receives a `Getter` for each individual element of the original collection

---

## `OrderGetter`

`OrderGetter` is accessed through `.order_by`.

It sorts the current collection and returns a new query with the elements reordered.

For example:

```python
class MyPage(BaseSeleniumPage):

    def items_alphabetically(self):
        return self.find(".item").order_by.text
```

The ordering can also use CSS properties, data attributes or other values:

```python
self.find(".item").order_by.data("priority")
```

A custom key function can be provided:

```python
class MyPage(BaseSeleniumPage):

    def items_by_text_length(self):
        return self.find(".item").order_by(
            lambda item: len(item.get.text)
        )
```

The important point is that `.order_by` changes the order of the query's collection, not the order of the elements in the page itself.

---

## `MapperGetter`

`MapperGetter` is accessed through `.map`.

It is useful for hierarchical DOM structures: take a collection of parent elements and apply a query to each parent.

For example, given:

```html
<div class="card">
    <span class="title">First</span>
</div>

<div class="card">
    <span class="title">Second</span>
</div>
```

the following query maps `.title` inside each card:

```python
titles = self.find(".card").map(".title")
```

The resulting `MapperGetter` behaves like a collection of queries, one per original `.card` (these are `Getter` objects).

Values can then be extracted from every mapped result:

```python
titles.get.text
```

```python
class MyPage(BaseSeleniumPage):

    def card_titles(self):
        return (
            self.find(".card")
                .map(".title")
                .get.text
        )
```
A mapper can also transmit operations to each mapped query:

```python
self.find(".card").map(".title").check.text(
    ["First", "Second"]
)
```

The above example is not very useful, as this could be done with a simple `find(".card .title").check(["First", "Second"])`, but this becomes especially useful when each Getter sub element has its own internal hierarchical structure.

`MapperGetter` is intended for hierarchical collections. It is not a general-purpose replacement for ordinary `Getter` queries.

---

## `BoolAllGetter`

`BoolAllGetter` is accessed through `.all`.

It evaluates a boolean condition over the collection and combines the results with `all(...)`. This answers the question:

> Are all matching elements displayed?

For example:

```python
class MyPage(BaseSeleniumPage):

    def all_items_are_visible(self):
        return self.find(".item").all.is_displayed()
```

The `.all` accessor is useful when the condition must be true for every element in the current collection.

Note that, when no behavior property is given (`get`, `check`, ...), the `Getter` behaves as if `all` was used.

---

## `BoolAnyGetter`

`BoolAnyGetter` is accessed through `.any`.

It evaluates a boolean condition over the collection and combines the results with `any(...)`.

For example:

```python
class MyPage(BaseSeleniumPage):

    def has_a_selected_item(self):
        return self.find(".item").any.is_selected()
```

---

## `NotTransmitter`

`NotTransmitter` is accessed through `.not_`.

It negates the next boolean or assertion behavior.

For example:

```python
class MyPage(BaseSeleniumPage):

    def no_item_is_selected(self):
        return self.find(".item").not_.any.is_selected()
```

This expresses:

> It is not the case that any item is selected.

It can also be used with assertions:

```python
self.find(".item").check.not_.exists()
```

The `not_` accessor is intended as a negation layer over the following query operation. It can not be followed with `.map` or `.order_by`.

Also, it behaves differently when used "around" the boolean accessors:

* `.all.not_.predicate` is `all( not predicate(elt) for elt in collection)`
* `.not_.all.predicate` is `not all( predicate(elt) for elt in collection)`
* `.any.not_.predicate` is `any( not predicate(elt) for elt in collection)`
* `.not_.any.predicate` is `not any( predicate(elt) for elt in collection)`

In this regard, `getter.not_.predicate` behaves like `getter.all.not_.predicate`.

---




## A complete example

Putting the main pieces together (but leaving aside the logistic related to the `BaseSeleniumPage` extension):

```python
from selenium_query import BaseSeleniumPage


class ProductsPage(BaseSeleniumPage):

    def product_names(self):
        return self.find(".product").get.text

    def active_products(self):
        return (
            self.find(".product")
                .filter
                .data(status="active")
        )

    def products_by_name(self):
        return self.find(".product").order_by.text

    def product_titles(self):
        return (
            self.find(".product")
                .map(".title")
                .get.text
        )

    def check_products(self):
        (
            self.find(".product")
                .check
                .exists()
        )

    def has_visible_product(self):
        return self.find(".product").any.is_displayed()

    def all_products_visible(self):
        return self.find(".product").all.is_displayed()

    def no_product_is_selected(self):
        return self.find(".product").not_.any.is_selected()
```

The general model is:

```text
BaseSeleniumPage
        │
        └── find(...)
                │
              Getter
                │
        ┌───────┼────────┬────────┬────────┬────────┬────────┬────────┬────────┐
        │       │        │        │        │        │        │        │        │
       get    check    actions  filter    all      any      not_     map    order_by
        │
     values
```

The most important thing to remember is that `Getter` is the central abstraction, but the user normally reaches it through:

```python
page.find("...")
```

The library then provides specialized views of that same collection:

```python
query.get
query.check
query.filter
query.map
query.order_by
query.any
query.all
query.not_
```

This is the core of the jQuery-like collection model: **select once, then transform, inspect, assert, order or map the resulting collection.**

<br>

---

---


<br>


## `BaseSeleniumPage`: the page-level entry point

`BaseSeleniumPage` is the main entry point for using Selenium Query.

A page object should inherit from it:

```python
from selenium_query import BaseSeleniumPage


class MyPage(BaseSeleniumPage):
    URL = "my-page"
```

The class centralizes the Selenium objects and page-level operations needed by the tests:

* the current `WebDriver`;
* the `WebDriverWait` instance;
* the root `Getter` used to create queries;
* page loading and readiness;
* data gathering;
* CSS-based element lookup;
* alert handling;
* JavaScript execution;
* test dependencies and execution tracking.

The important relationship is:

```text
BaseSeleniumPage
        │
        ├── driver
        ├── waiter
        └── Getter
              │
              └── find(...)
                    │
                 query
```

In normal user code, the query API starts with:

```python
self.find("...")
```

rather than by manually instantiating a `Getter`.

---

### Defining a page

A subclass declares the relative URL of the page it represents:

```python
class SearchPage(BaseSeleniumPage):
    URL = "search"
```

The URL is combined with the server address supplied by the Selenium environment:

```text
server address + URL
```

For example:

```text
http://localhost:8000/ + search
→ http://localhost:8000/search
```

The URL must be relative to the server root and must not start with `docs/`.

A page can customize how its final address is built by overriding:

```python
@classmethod
def get_address_from_class_url(cls, server_address):
    return server_address + cls.URL
```

This is useful when the URL needs custom construction.

---

### Loading a page

`BaseSeleniumPage` separates page loading from page-specific data gathering.

The expected model is:

```text
load_url()
    │
    ├── load the URL
    ├── wait until the page is ready
    └── gather shared page data

data()
    │
    └── build data specific to the subclass
```

The base `load_url` and `data` methods are intentionally abstract and should be overloaded as pytest fixtures:

```python
@pytest.fixture(scope='module' autouse=True)
@classmethod
def load_url(cls, clean_cache, selenium_env):
    cls._load_url(selenium_env)

@pytest.fixture(scope='class' autouse=True)
@classmethod
def data(cls, load_url):
    cls._build_data()
```

Here:

* `clear_cache` is a fixture (you shouldn't need to interact with it)
* `selenium_env` has to be a 3-uples: `(stdout:str, server_address:str, driver:WebDriver)`.

    - `stdout` is not used in Selenium Query and is present as a convenience for the user. It can be None if not needed.
    - `server_address` is a string giving the address of the current live server (something like `http://localhost:8000/`). It should end with a slash.
    - `driver` is the selenium WebDriver instance which will be used during the tests.


A concrete page class therefore needs to provide the appropriate fixture implementations for its test environment, with the appropriate scope levels, the initial intent for this being to test:

- One URL per python module
- Split the tests over different subclasses, each one gathering the data it needs.

<br>

An important point is that `_load_url` is the shared implementation, while subclasses decide how it is exposed to their test/fixture system.

Various methods can be overloaded in the class extending `BaseSeleniumPage`, to easily change the behaviors to match the project's needs:

```python
class SearchPage(BaseSeleniumPage):

    URL: ClassVar[str]

    @classmethod
    def get_address_from_class_url(cls, server_address:str) -> str:
        '''
        Builds the full address of the page to load, using the server address and cls.URL.
        Called from `cls._load_url`.
        `server_address` should end with a slash.
        '''


    @classmethod
    def base_gather_data(cls) -> None:
        '''
        Module level data handling.
        Called from `cls._load_url`.

        WARNING: any information/data that would be stored at this point must be "attached"
        to the current class directly (`SearchPage`, here), so that any subclass or instance
        of a subclass can actually access the data.
        '''
        ...
        super.base_gather_data()
        ...

    @classmethod
    def gather_data(cls) -> None:
        '''
        Module level data handling, as a second entry point to tweak the logic differently if needed.
        Called from `super.base_gather_data`.

        WARNING: any information/data that would be stored at this point must be "attached"
        to the current class directly (`SearchPage`, here), so that any subclass or instance
        of a subclass can actually access the data.
        '''

    @classmethod
    def _build_data(cls):
        '''
        Override cls._build_data() in subclasses to define data specific to THAT class (without
        reloading the page).
        Must be called from the `cls.data` fixture.
        '''
```



### Gathering shared page data

`BaseSeleniumPage` has two levels of data gathering:

```text
base_gather_data()
        ↓
gather_data()
```

The default implementation is:

```python
@classmethod
def base_gather_data(cls):
    cls.gather_data()
```

A subclass can override `gather_data` to build data shared across subclasses using the same page:

```python
class SearchPage(BaseSeleniumPage):

    @classmethod
    def gather_data(cls):
        SearchPage.search_results = cls.find(".result").get.text
```

The source code deliberately emphasizes that data gathered at this level should be attached to the class extending `BaseSeleniumPage` itself when it needs to be accessible to all subclasses and instances.

This is important because:

```text
different subclass
        ↓
different class object
        ↓
class-level data on the subclass may not be shared
```

Whereas:

```python
SearchPage.some_shared_data
```

is accessible from every subclass.

---

### Building subclass-specific data

`_build_data` is intended for data specific to one subclass:

```python
@classmethod
def _build_data(cls):
    ...
```

A subclass can override it:

```python
class SearchPage(BaseSeleniumPage):

    @classmethod
    def _build_data(cls):
        cls.search_input = cls.find("#search")
        cls.submit_button = cls.find("#submit")
```

This is different from shared data gathered at the base level:

```text
gather_data()
    → data shared by the page hierarchy

_build_data()
    → data specific to this subclass/batch of tests
```

The exact fixture wiring depends on the test environment, but the conceptual separation is intentional.

---


### Page readiness

After loading the URL, `_load_url` waits for the page to be ready:

```python
cls.wait_until(page_is_ready or cls.page_is_ready)
```

The default readiness method is deliberately simple:

```python
@classmethod
def page_is_ready(cls, driver):
    cls.pause(0.8)
    return True
```

A page can override the readiness condition when loading is asynchronous:

```python
class SearchPage(BaseSeleniumPage):
    URL = "search"

    @classmethod
    def page_is_ready(cls, _driver):
        return cls.find("css selector, #search-ready").is_displayed()
```

The actual condition depends on the page being tested.

A more general mechanism is:

```python
cls.wait_until(predicate)
```

where `predicate` must receive the WebDriver instance and return a bool.

---

### `find`: the normal way to start a query

The most important user-facing method is:

```python
@classmethod
def find(cls, css_selector, as_=None):
    return cls._getter.find(
        css_selector,
        in_page=True,
        as_=as_
    )
```

This is the normal entry point into Selenium Query:

```python
class SearchPage(BaseSeleniumPage):
    URL = "search"

    @classmethod
    def search(cls, value):
        cls.find("#search").send_keys(value).go
        cls.find("#submit").go
```

The page owns the root `Getter`, and `find(...)` creates a page-scoped query.

This is why users should normally write:

```python
MyPage.find(".item")
```

or:

```python
self.find(".item")
```

rather than:

```python
Getter(...)
```

`BaseSeleniumPage.find` is the bridge between the page object and the collection-oriented query API.

---

### Waiting for elements

For a direct element wait, the page exposes:

```python
@classmethod
def wait_for_element(cls, css_selector):
    return cls._getter.wait_for_element(css_selector)
```

Example:

```python
class SearchPage(BaseSeleniumPage):

    @classmethod
    def wait_until_loaded(cls):
        cls.wait_for_element("#search")
```

For arbitrary conditions, use:

```python
cls.wait_until(...)
```

For example:

```python
class SearchPage(BaseSeleniumPage):

    @classmethod
    def wait_until_results_are_loaded(cls):
        cls.wait_until(
            lambda driver: len(
                driver.find_elements(
                    "css selector",
                    ".result"
                )
            ) > 0
        )
```

The difference is:

```text
wait_for_element(...)
    → wait for a CSS-selected element

wait_until(...)
    → wait for any user-defined truthy condition
```

---

### Handling JavaScript alerts

Some pages display an alert immediately when loaded.

This is controlled by:

```python
WITH_ALERT = True
```

and:

```python
alert_msg
```

For example:

```python
class AlertPage(BaseSeleniumPage):
    URL = "alert-page"
    WITH_ALERT = True
    alert_msg: str = None
```

The dedicated readiness helper is:

```python
page_with_alert_is_ready
```

Its behavior is:

1. detect the alert;
2. store its text in `alert_msg`;
3. accept the alert;
4. return `False` so the wait continues;
5. once the alert has been handled, continue with the normal page readiness logic.

---

An alert can also be explicitly awaited from various places in the code of the tests with:

```python
alert: Alert = AlertPage.wait_for_alert()
```

---

### Pausing execution

The page exposes:

```python
BaseSeleniumPage.pause(seconds)
```

which uses internally Selenium's `ActionChains`:

```python
class MyPage(BaseSeleniumPage):

    @classmethod
    def wait_for_animation(cls):
        cls.pause(1.0)
```

The method returns the class itself, allowing a fluent style:

```python
MyPage.pause(0.5)
```

This should generally be reserved for situations where a real timed pause is required. For normal synchronization, a condition-based wait is preferable:

```python
MyPage.wait_until(...)
```

---

### Executing JavaScript

`run_js` delegates JavaScript execution to the underlying query system:

```python
BaseSeleniumPage.run_js(
    "return document.title"
)
```

Arguments are passed to JavaScript through Selenium's `arguments` array:

```python
result = MyPage.run_js(
    "return arguments[0] + arguments[1]",
    1,
    2
)
```

Inside JavaScript:

```javascript
arguments[0] // 1
arguments[1] // 2
```

The method also supports asynchronous execution:

```python
MyPage.run_js(
    "return fetch('/api/data').then(response => response.json())",
    async_=True
)
```

When `async_=True` and `with_promise=True`, the library expects the JavaScript expression to be a Promise and automatically adds the necessary continuation to resume Selenium's asynchronous execution.

For custom asynchronous scripts, `with_promise=False` can be used when the script explicitly manages the Selenium callback: a call to `done()` must be present in the given JS code (`done` is an alias for the last argument provided by selenium, when executing async scripts).

---

### Test execution tracking

`BaseSeleniumPage` also provides infrastructure for test dependencies.

When a subclass is created, its methods whose names start with:

```text
test_
```

are automatically wrapped.

The wrapper records that the method has run:

```python
_methods_run
```

A test can then declare a dependency:

```python
@BaseSeleniumPage.depends_on(test_setup)
def test_feature(self):
    ...
```

If `test_setup` has not run yet, it is executed first.

Arguments can optionally be passed through:

```python
@BaseSeleniumPage.depends_on(
    test_setup,
    pass_args=True
)
def test_feature(self, value):
    ...
```

The dependency mechanism is primarily intended to make dependent tests work correctly when individual tests are launched directly.

It is not intended as a general-purpose “run this method only once” mechanism.

---

### A complete page skeleton

A page class can combine these mechanisms:

```python
from selenium_query import BaseSeleniumPage


class SearchPage(BaseSeleniumPage):
    URL = "search"

    @classmethod
    def load_url(cls, clean_cache, selenium_env):
        cls._load_url(selenium_env)

    @classmethod
    def data(cls, load_url):
        cls._build_data()

    @classmethod
    def _build_data(cls):
        cls.search_input = cls.find("#search")
        cls.submit_button = cls.find("#submit")

    @classmethod
    def search(cls, value):
        cls.search_input.send_keys(value)
        cls.submit_button.go

    @classmethod
    def wait_for_results(cls):
        cls.wait_for_element(".result")

    @classmethod
    def result_titles(cls):
        return cls.find(".result").get.text
```

The page class owns the browser lifecycle and page-level synchronization, while `Getter` and its specialized query interfaces handle collection-oriented DOM operations.


For actual/concrete examples of use, you might refer to these:

* [Pyodide-MkDocs-Theme tests](https://gitlab.com/frederic-zinelli/pyodide-mkdocs-theme/-/tree/main/tests/tests/selenium?ref_type=heads) (selenium part)
* [Codex tests](https://forge.apps.education.fr/codex/codex.forge.apps.education.fr/-/tree/main/tests?ref_type=heads) (testing the integration within Capytale)

Both make also use of the [`pmt-tools`](https://gitlab.com/frederic-zinelli/pmt_tools) package, which is a second layer around `selenium-query`, building dedicated base objects to test:

* IDEs, terminals, ...
* Tabbed contents (see mkdocs-material `=== "tabbed"` syntaxes)
* ...
* `BasePmtPage`, which is extending `BaseSeleniumPage` with functionalities dedicated to test websites generated with Pyodide-MkDocs-Theme.


---

### In short

`BaseSeleniumPage` is the layer that connects the Selenium environment to Selenium Query:

```text
Selenium environment
        ↓
  BaseSeleniumPage
        │
        ├── load URL
        ├── wait for readiness
        ├── gather data
        ├── manage alerts
        ├── execute JavaScript
        └── create queries
                ↓
            find(...)
                ↓
              Getter
                ↓
          DOM collections
```

For application code, the key entry point is:

```python
page.find("css selector")
```

`BaseSeleniumPage` is therefore not just a convenience wrapper around `WebDriver`: it is the page-level abstraction that owns the browser context, synchronization, page loading, caching and the root from which Selenium Query collections are created.

