Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
47 commits
Select commit Hold shift + click to select a range
313d378
Smart locator implementation
digitronik Aug 5, 2025
42eb152
Basic setting for unittest testing with playwright.
digitronik Aug 6, 2025
93049b4
Merge pull request #272 from digitronik/playwright_migration_basics
digitronik Aug 7, 2025
98a15a4
Playwright migration for utils, types and exceptions files
digitronik Aug 7, 2025
64aeb39
Playwright migration for base widget and descriptor file
digitronik Aug 7, 2025
acbb7f3
Playwright migration for browser and browser plugin
digitronik Aug 7, 2025
a104b75
Unit test fixing for browser,base,utiles file changes
digitronik Aug 7, 2025
3ee2be9
Restor importing style
digitronik Aug 8, 2025
41533a7
Merge pull request #273 from digitronik/pw_browser_base
digitronik Aug 12, 2025
1e95691
Migrate checkbox widget
digitronik Aug 12, 2025
93f44cc
Migrate input widgets
digitronik Aug 12, 2025
6af4bf9
Migrate select widget
digitronik Aug 12, 2025
0ea4faf
Migrate table widget
digitronik Aug 12, 2025
1244311
Fix tests
digitronik Aug 12, 2025
270eac6
Merge pull request #275 from digitronik/widgets
digitronik Aug 14, 2025
c562300
Fix iframe handling
digitronik Aug 14, 2025
28aa14b
Adding extra iframe unit tests.
digitronik Aug 14, 2025
87e4980
Merge pull request #276 from digitronik/iframe_handling
digitronik Aug 14, 2025
d6d9e14
Windows/Browsers handling
digitronik Aug 19, 2025
189cb2c
Windows/Browsers handling unit tests
digitronik Aug 19, 2025
2a569b2
Some browser methods is_closed and go_to wrapp
digitronik Aug 20, 2025
edfd1c1
Merge pull request #278 from digitronik/window_manager_handling
digitronik Aug 20, 2025
174d4c3
Refactor html pages with css
digitronik Aug 20, 2025
6557b52
Merge pull request #279 from digitronik/unit_test_improvement
digitronik Aug 22, 2025
d0c9b13
Fix send_keys_to_focused_element
digitronik Aug 28, 2025
5e7c548
Added code coverage for browser.py
digitronik Aug 28, 2025
aa9f01c
browser.py methods/property re-organized
digitronik Aug 30, 2025
cfbfff1
reorganized browser tests and added new corner cases
digitronik Sep 2, 2025
0a91dbe
Use isolated windows manager for windows tests
digitronik Sep 3, 2025
2412a09
Added missing unit tests for table.py file
digitronik Sep 3, 2025
d3e252f
Imporve select unit tests for missing lines
digitronik Sep 4, 2025
6ebbacd
Imporve base-input unit tests for missing lines
digitronik Sep 4, 2025
8d02bdf
Add unit tests for Image widget
digitronik Sep 4, 2025
392d4fb
Add unit tests for fill statergy, utils, version and version_pick
digitronik Sep 5, 2025
d25bae3
Improve and add new tests for base.py
digitronik Sep 5, 2025
7f4cf91
Improve test for ouia widgets
digitronik Sep 5, 2025
88ef3cc
Handle frame switching for different playwright versions
digitronik Sep 8, 2025
6af3f5e
Merge pull request #281 from digitronik/coverage_improvement
digitronik Sep 8, 2025
23105ef
Improve github action test pipeline
digitronik Sep 8, 2025
0260ba3
Merge pull request #282 from digitronik/pipelines
digitronik Sep 9, 2025
f7d41c9
Migrate existing docs to playwright.
digitronik Sep 9, 2025
5a780d7
Merge pull request #283 from digitronik/docs
digitronik Sep 9, 2025
d5a63a2
[pre-commit.ci] pre-commit autoupdate
pre-commit-ci[bot] Aug 18, 2025
6d1198a
[pre-commit.ci] pre-commit autoupdate
pre-commit-ci[bot] Sep 8, 2025
3c0b768
Correct copyright
digitronik Sep 11, 2025
50d9856
Add brower cache ability to make run fast
digitronik Sep 15, 2025
fa1ae68
Merge pull request #285 from digitronik/browser_cache
digitronik Sep 15, 2025
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
88 changes: 65 additions & 23 deletions .github/workflows/test_suite.yml
Original file line number Diff line number Diff line change
@@ -1,40 +1,69 @@
name: 🕵️ Test suite

concurrency:
group: ${{ github.workflow }}-${{ github.ref }}
cancel-in-progress: true

on:
push:
branches:
- main
- playwright
pull_request:
types: ["opened", "synchronize", "reopened"]
schedule:
# Run every Friday at 23:59 UTC
- cron: 59 23 * * 5

jobs:
setup-browsers:
name: 🌐 Setup Playwright Browsers
runs-on: ubuntu-latest
timeout-minutes: 15
outputs:
playwright-version: ${{ steps.playwright-version.outputs.version }}
steps:
- name: Checkout
uses: actions/checkout@v4

- name: Set up Python
uses: actions/setup-python@v5
with:
python-version: "3.11"

- name: Install Playwright
run: pip install playwright

- name: Get Playwright version
id: playwright-version
run: echo "version=$(pip show playwright | grep Version | cut -d' ' -f2)" >> $GITHUB_OUTPUT

- name: Cache Playwright browsers
uses: actions/cache@v4
id: playwright-cache
with:
path: ~/.cache/ms-playwright
key: playwright-${{ runner.os }}-${{ steps.playwright-version.outputs.version }}-browsers
restore-keys: |
playwright-${{ runner.os }}-${{ steps.playwright-version.outputs.version }}-

- name: Install browsers
if: steps.playwright-cache.outputs.cache-hit != 'true'
run: |
playwright install chromium firefox
playwright install-deps

tests:
# Run unit tests on different version of python and browser
name: 🐍 Python-${{ matrix.python-version }}-${{ matrix.browser }}
runs-on: ubuntu-latest
timeout-minutes: 30
needs: setup-browsers
strategy:
matrix:
browser: [chrome, firefox]
python-version: ["3.8", "3.9", "3.10", "3.11", "3.12", "3.13"]
browser: [chromium, firefox]
python-version: ["3.11", "3.12", "3.13"]

steps:
- name: Pull selenium-standalone:latest
run: podman pull selenium/standalone-${{ matrix.browser }}:latest

- name: Pull nginx:alpine
run: podman pull docker.io/library/nginx:alpine

- name: Pull pause
run: |
# something screwy is going on in the github kube node
# seeing 404s pulling from k8s.gcr, which was deprecated a year ago anyway
# the registry name is hardcoded somewhere on the infra side, but the pull from the new registry works consistently
podman pull registry.k8s.io/pause:3.5
podman tag registry.k8s.io/pause:3.5 k8s.gcr.io/pause:3.5

- name: Checkout
uses: actions/checkout@v4

Expand All @@ -43,15 +72,20 @@ jobs:
with:
python-version: ${{ matrix.python-version }}

- name: Install Playwright
run: pip install playwright

- name: Restore Playwright browsers cache
uses: actions/cache/restore@v4
with:
path: ~/.cache/ms-playwright
key: playwright-${{ runner.os }}-${{ needs.setup-browsers.outputs.playwright-version }}-browsers
fail-on-cache-miss: true

- name: UnitTest - Python-${{ matrix.python-version }}-${{ matrix.browser }}
env:
BROWSER: ${{ matrix.browser }}
XDG_RUNTIME_DIR: ${{ github.workspace }}
run: |
pip install -e .[test]
mkdir -p ${XDG_RUNTIME_DIR}/podman
podman system service --time=0 unix://${XDG_RUNTIME_DIR}/podman/podman.sock &
pytest -n 5
pytest -n 3 -sqvv --browser=${{ matrix.browser }} --headless --cov=src --cov-report=xml

- name: Upload coverage to Codecov
uses: codecov/codecov-action@v4
Expand All @@ -64,6 +98,7 @@ jobs:
docs:
name: Docs Build
runs-on: ubuntu-latest
timeout-minutes: 15
steps:
- name: Checkout
uses: actions/checkout@v4
Expand All @@ -72,6 +107,7 @@ jobs:
uses: actions/setup-python@v5
with:
python-version: "3.x"
cache: 'pip'

- name: Install Deps
run: |
Expand All @@ -91,6 +127,8 @@ jobs:
# Check package properly install on different platform (dev setup)
name: 💻 Platform-${{ matrix.os }}
runs-on: ${{ matrix.os }}
timeout-minutes: 20
needs: [setup-browsers, tests]
strategy:
matrix:
os: [windows-latest, macos-latest] # We are running test on ubuntu linux.
Expand All @@ -104,6 +142,7 @@ jobs:
with:
python-version: "3.x"
architecture: "x64"
cache: 'pip'

- name: Development setup on ${{ matrix.os }}
run: |
Expand All @@ -113,6 +152,8 @@ jobs:
package:
name: ⚙️ Build & Verify Package
runs-on: ubuntu-latest
timeout-minutes: 15
needs: [setup-browsers, tests, docs]

steps:
- name: Checkout
Expand All @@ -123,6 +164,7 @@ jobs:
with:
python-version: "3.x"
architecture: "x64"
cache: 'pip'

- name: Build and verify with twine
run: |
Expand Down
4 changes: 2 additions & 2 deletions .pre-commit-config.yaml
Original file line number Diff line number Diff line change
@@ -1,13 +1,13 @@
repos:
- repo: https://github.com/astral-sh/ruff-pre-commit
rev: v0.12.7
rev: v0.12.12
hooks:
- id: ruff
args:
- '--fix'
- id: ruff-format
- repo: https://github.com/pre-commit/pre-commit-hooks
rev: v5.0.0
rev: v6.0.0
hooks:
- id: trailing-whitespace
- id: end-of-file-fixer
Expand Down
45 changes: 28 additions & 17 deletions docs/basic_usage.rst
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@ This sample only represents simple UI interaction.

.. code-block:: python

from selenium import webdriver
from playwright.sync_api import sync_playwright
from widgetastic.browser import Browser
from widgetastic.widget import View, Text, TextInput

Expand All @@ -29,22 +29,33 @@ This sample only represents simple UI interaction.
ROOT = 'div#somediv'
another_text = Text(locator='#h2') # See "Automatic simple CSS locator detection"

selenium = webdriver.Firefox() # For example
browser = CustomBrowser(selenium)

# Now we have the widgetastic browser ready for work
# Let's instantiate a view.
a_view = MyView(browser)
# ^^ you would typically come up with some way of integrating this in your framework.

# The defined widgets now work as you would expect
a_view.read() # returns a recursive dictionary of values that all widgets provide via read()
a_view.a_text.text # Accesses the text
# but the .text is widget-specific, so you might like to use just .read()
a_view.fill({'an_input': 'foo'}) # Fills an_input with foo and returns boolean whether anything changed
# Basically equivalent to:
a_view.an_input.fill('foo') # Since views just dispatch fill to the widgets based on the order
a_view.an_input.is_displayed
# Initialize Playwright and create browser instance
with sync_playwright() as p:
playwright_browser = p.chromium.launch() # or p.firefox.launch()
context = playwright_browser.new_context()
page = context.new_page()
browser = CustomBrowser(page)

# Navigate
browser.url = "https://foo.com"

# Now we have the widgetastic browser ready for work
# Let's instantiate a view.
a_view = MyView(browser)
# ^^ you would typically come up with some way of integrating this in your framework.

# The defined widgets now work as you would expect
a_view.read() # returns a recursive dictionary of values that all widgets provide via read()
a_view.a_text.text # Accesses the text
# but the .text is widget-specific, so you might like to use just .read()
a_view.fill({'an_input': 'foo'}) # Fills an_input with foo and returns boolean whether anything changed
# Basically equivalent to:
a_view.an_input.fill('foo') # Since views just dispatch fill to the widgets based on the order
a_view.an_input.is_displayed

# Clean up resources
context.close()
playwright_browser.close()


Typically, you want to incorporate a system that would do the navigation (like
Expand Down
8 changes: 5 additions & 3 deletions docs/conf.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,8 +3,10 @@
# -- Project information -----------------------------------------------------

project = "widgetastic.core"
copyright = f"2016-{datetime.now().year}, Milan Falešník (Apache license 2)"
author = "Milan Falešník"
copyright = (
f"2016-2019, Milan Falešník; 2020-{datetime.now().year}, Red Hat, Inc. (Apache license 2)"
)
author = "Milan Falešník, Red Hat, Inc."

# -- General configuration ---------------------------------------------------

Expand All @@ -17,7 +19,7 @@

intersphinx_mapping = {
"python": ("http://docs.python.org/3.12/", None),
"selenium": ("http://selenium-python.readthedocs.org/", None),
"playwright": ("https://playwright.dev/python/", None),
}

templates_path = ["_templates"]
Expand Down
18 changes: 9 additions & 9 deletions docs/guidelines.rst
Original file line number Diff line number Diff line change
Expand Up @@ -50,19 +50,19 @@ Anyone using this library should consult these guidelines whether one is not vio

- et cetera

- ``__locator__`` MUST NOT return ``WebElement`` instances to prevent ``StaleElementReferenceException``
- ``__locator__`` MUST NOT return ``ElementHandle`` instances to prevent stale element issues

- If you use a ``ROOT`` class attribute, especially in combination with ``ParametrizedLocator``, a ``__locator__`` is generated automatically for you.

- Widgets should keep its internal state in reasonable size. Ideally none, but eg. caching header names of tables is perfectly acceptable. Saving ``WebElement`` instances in the widget instance is not recommended.
- Widgets should keep its internal state in reasonable size. Ideally none, but eg. caching header names of tables is perfectly acceptable. Saving ``ElementHandle`` instances in the widget instance is not recommended.

- Think about what to cache and when to invalidate

- Never store ``WebElement`` objects.
- Never store ``ElementHandle`` objects.

- Try to shorten the lifetime of any single ``WebElement`` as much as possible
- Try to shorten the lifetime of any single ``ElementHandle`` as much as possible

- This will help against ``StaleElementReferenceException``
- This will help against stale element issues

- Widgets shall log using ``self.logger``. That ensures the log message is prefixed with the widget name and location and gives more insight about what is happening.

Expand Down Expand Up @@ -96,17 +96,17 @@ Anyone using this library should consult these guidelines whether one is not vio

- When using ``Browser`` (also applies when writing Widgets)

- Ensure you don't invoke methods or attributes on the ``WebElement`` instances returned by ``element()`` or ``elements()``
- Ensure you use the widgetastic Browser methods rather than direct Playwright Locator methods where possible

- Eg. instead of ``element.text`` use ``browser.text(element)`` (applies for all such circumstances). These calls usually do not invoke more than their original counterparts. They only invoke some workarounds if some know issue arises. Check what the ``Browser`` (sub)class offers and if you miss something, create a PR
- Eg. instead of ``locator.text_content()`` use ``browser.text(locator)`` (applies for all such circumstances). These calls usually do not invoke more than their original counterparts. They only invoke some workarounds if some known issue arises. Check what the ``Browser`` (sub)class offers and if you miss something, create a PR

- You don't necessarily have to specify ``self.browser.element(..., parent=self)`` when you are writing a query inside a widget implementation as widgetastic figures this out and does it automatically.

- Most of the methods that implement the getters, that would normally be on the element object, take an argument or two for themselves and the rest of ``*args`` and ``**kwargs`` is shoved inside ``element()`` method for resolution, so constructs like ``self.browser.get_attribute('id', self.browser.element('locator', parent=foo))`` are not needed. Just write ``self.browser.get_attribute('id', 'locator', parent=foo)``. Check the method definitions on the ``Browser`` class to see that.

- ``element()`` method tries to apply a rudimentary intelligence on the element it resolves. If a locator resolves to a single element, it returns it. If the locator resolves to multiple elements, it tries to filter out the invisible elements and return the first visible one. If none of them is visible, it just returns the first one. Under normal circumstances, standard selenium resolution always returns the first of the resolved elements.
- ``element()`` method tries to apply a rudimentary intelligence on the element it resolves. If a locator resolves to a single element, it returns it. If the locator resolves to multiple elements, it tries to filter out the invisible elements and return the first visible one. If none of them is visible, it just returns the first one. Under normal circumstances, standard Playwright resolution returns all matching elements.

- DO NOT use ``element.find_elements_by_<method>('locator')``, use ``self.browser.element('locator', parent=element)``. It is about as same long and safer.
- DO NOT use nested locator calls, use ``self.browser.element('locator', parent=element)``. This approach is safer and more consistent with the framework architecture.

- Eventually I might wrap the elements as well but I decided to not complicate things for now.

Expand Down
21 changes: 9 additions & 12 deletions docs/internals.rst
Original file line number Diff line number Diff line change
Expand Up @@ -3,15 +3,15 @@ Internal structure of Widgetastic

Widgetastic consists of 2 main parts:

* `Selenium browser wrapper`_
* `Playwright browser wrapper`_
* `Widget system`_

.. `Selenium browser wrapper`:
.. `Playwright browser wrapper`:

Selenium browser wrapper
========================
Playwright browser wrapper
===========================

This part of the framework serves the purpose of simplifying the interactions with Selenium and also handling some of the quirks we have discovered during development of our testing framework. It also supports "nesting" of the browsers in relation to specific widgets, so it is then easier in the widget layer to implement the lookup fencing. Majority of this functionality is implemented in :py:class:`widgetastic.browser.Browser`.
This part of the framework serves the purpose of simplifying the interactions with Playwright and also handling some of the quirks we have discovered during development of our testing framework. It also supports "nesting" of the browsers in relation to specific widgets, so it is then easier in the widget layer to implement the lookup fencing. Majority of this functionality is implemented in :py:class:`widgetastic.browser.Browser`.

Lookup fencing is a technique that enables the programmer to write locators that are relative to its hosting object. When such locator gets resolved, the parent element is resolved first (and it continues recursively until you hit an "unwrapped" browser that is just a browser). This behaviour is not visible to the outside under normal circumstances and it is achieved by :py:class:`widgetastic.browser.BrowserParentWrapper`.

Expand All @@ -29,20 +29,17 @@ the pattern of `tagname#id.class1.class2` where the tag is optional and at least
is present, it considers it a CSS locator.

If you want to use a complex CSS locator or a different lookup type, you can use
`selenium-smart-locator <https://pypi.python.org/pypi/selenium-smart-locator>`_ library that is used
underneath to process all the locators. You can consult the documentation and pass instances of
``Locator`` instead of a string.

This library is already in the requirements, so it is not necessary to install it.
the built-in ``SmartLocator`` functionality that processes all the locators. You can pass instances of
``SmartLocator`` or raw locator strings, and the system will automatically handle the conversion.

.. `Automatic visibility precedence selection`:

Automatic visibility precedence selection
-----------------------------------------

Under normal circumstances, Selenium's ``find_element`` always returns the first element from the query result. But what if there are multiple elements matching the query, the first one being for some reason invisible, and the second one displayed?
Under normal circumstances, Playwright's ``locator.all()`` returns all elements from the query result. But what if there are multiple elements matching the query, the first one being for some reason invisible, and the second one displayed?

Widgetastic's :py:meth:`widgetastic.browser.Browser.element`, :py:meth:`widgetastic.browser.Browser.elements`, and :py:meth:`widgetastic.browser.Browser.wait_for_element` browser methods allow for visibility checking in this case. If the keyword argument ``check_visiblity=True`` is passed to ``elements``, if ``visible=True`` is passed to ``wait_for_elements``, or if the locator object (e.g., a :py:class:`widgetastic.widget.Widget` object) passed to ``element`` contains a ``__locator__`` method as well as a ``CHECK_VISIBILITY=True`` attribute, then the method will return only the visible matching element(s).
Widgetastic's :py:meth:`widgetastic.browser.Browser.element`, :py:meth:`widgetastic.browser.Browser.elements`, and :py:meth:`widgetastic.browser.Browser.wait_for_element` browser methods allow for visibility checking in this case. If the keyword argument ``check_visiblity=True`` is passed to ``elements``, if ``visible=True`` is passed to ``wait_for_element``, or if the locator object (e.g., a :py:class:`widgetastic.widget.Widget` object) passed to ``element`` contains a ``__locator__`` method as well as a ``CHECK_VISIBILITY=True`` attribute, then the method will return only the visible matching element(s).

.. `Widget system`:

Expand Down
6 changes: 3 additions & 3 deletions docs/intro.rst
Original file line number Diff line number Diff line change
Expand Up @@ -19,8 +19,8 @@ Features
- Widgets defined on Views are read/filled in exact order that they were defined. The only exception
to this default behaviour is for nested Views as there is limitation in the language. However, this
can be worked around by using ``View.nested`` decorator on the nested View.
- Includes a wrapper around selenium functionality that tries to make the experience as hassle-free
as possible including customizable hooks and built-in "JavaScript wait" code.
- Includes a wrapper around Playwright functionality that tries to make the experience as hassle-free
as possible including customizable hooks and built-in network activity monitoring.
- Views can define their root locators and those are automatically honoured in the element lookup
in the child Widgets.
- Supports :ref:`parametrized-views`.
Expand All @@ -29,7 +29,7 @@ Features
- Supports :ref:`version-picking`.
- Supports automatic :ref:`constructor-object-collapsing` for objects passed into the widget constructors.
- Supports :ref:`fillable-objects` that can coerce themselves into an appropriate filling value.
- Supports many Pythons! 2.7, 3.5, 3.6 and PyPy are officially supported and unit-tested in CI.
- Supports modern Python versions (specify in pyproject.toml) are officially supported and unit-tested in CI.

What this project does NOT do
-----------------------------
Expand Down
Loading
Loading