Skip to content

Commit 4779b72

Browse files
authored
Merge pull request #277 from RedHatQE/playwright
Playwright Migration
2 parents d0d0f36 + fa1ae68 commit 4779b72

40 files changed

Lines changed: 7800 additions & 1718 deletions

.github/workflows/test_suite.yml

Lines changed: 65 additions & 23 deletions
Original file line numberDiff line numberDiff line change
@@ -1,40 +1,69 @@
11
name: 🕵️ Test suite
22

3+
concurrency:
4+
group: ${{ github.workflow }}-${{ github.ref }}
5+
cancel-in-progress: true
6+
37
on:
48
push:
59
branches:
6-
- main
10+
- playwright
711
pull_request:
812
types: ["opened", "synchronize", "reopened"]
913
schedule:
1014
# Run every Friday at 23:59 UTC
1115
- cron: 59 23 * * 5
1216

1317
jobs:
18+
setup-browsers:
19+
name: 🌐 Setup Playwright Browsers
20+
runs-on: ubuntu-latest
21+
timeout-minutes: 15
22+
outputs:
23+
playwright-version: ${{ steps.playwright-version.outputs.version }}
24+
steps:
25+
- name: Checkout
26+
uses: actions/checkout@v4
27+
28+
- name: Set up Python
29+
uses: actions/setup-python@v5
30+
with:
31+
python-version: "3.11"
32+
33+
- name: Install Playwright
34+
run: pip install playwright
35+
36+
- name: Get Playwright version
37+
id: playwright-version
38+
run: echo "version=$(pip show playwright | grep Version | cut -d' ' -f2)" >> $GITHUB_OUTPUT
39+
40+
- name: Cache Playwright browsers
41+
uses: actions/cache@v4
42+
id: playwright-cache
43+
with:
44+
path: ~/.cache/ms-playwright
45+
key: playwright-${{ runner.os }}-${{ steps.playwright-version.outputs.version }}-browsers
46+
restore-keys: |
47+
playwright-${{ runner.os }}-${{ steps.playwright-version.outputs.version }}-
48+
49+
- name: Install browsers
50+
if: steps.playwright-cache.outputs.cache-hit != 'true'
51+
run: |
52+
playwright install chromium firefox
53+
playwright install-deps
54+
1455
tests:
1556
# Run unit tests on different version of python and browser
1657
name: 🐍 Python-${{ matrix.python-version }}-${{ matrix.browser }}
1758
runs-on: ubuntu-latest
59+
timeout-minutes: 30
60+
needs: setup-browsers
1861
strategy:
1962
matrix:
20-
browser: [chrome, firefox]
21-
python-version: ["3.8", "3.9", "3.10", "3.11", "3.12", "3.13"]
63+
browser: [chromium, firefox]
64+
python-version: ["3.11", "3.12", "3.13"]
2265

2366
steps:
24-
- name: Pull selenium-standalone:latest
25-
run: podman pull selenium/standalone-${{ matrix.browser }}:latest
26-
27-
- name: Pull nginx:alpine
28-
run: podman pull docker.io/library/nginx:alpine
29-
30-
- name: Pull pause
31-
run: |
32-
# something screwy is going on in the github kube node
33-
# seeing 404s pulling from k8s.gcr, which was deprecated a year ago anyway
34-
# the registry name is hardcoded somewhere on the infra side, but the pull from the new registry works consistently
35-
podman pull registry.k8s.io/pause:3.5
36-
podman tag registry.k8s.io/pause:3.5 k8s.gcr.io/pause:3.5
37-
3867
- name: Checkout
3968
uses: actions/checkout@v4
4069

@@ -43,15 +72,20 @@ jobs:
4372
with:
4473
python-version: ${{ matrix.python-version }}
4574

75+
- name: Install Playwright
76+
run: pip install playwright
77+
78+
- name: Restore Playwright browsers cache
79+
uses: actions/cache/restore@v4
80+
with:
81+
path: ~/.cache/ms-playwright
82+
key: playwright-${{ runner.os }}-${{ needs.setup-browsers.outputs.playwright-version }}-browsers
83+
fail-on-cache-miss: true
84+
4685
- name: UnitTest - Python-${{ matrix.python-version }}-${{ matrix.browser }}
47-
env:
48-
BROWSER: ${{ matrix.browser }}
49-
XDG_RUNTIME_DIR: ${{ github.workspace }}
5086
run: |
5187
pip install -e .[test]
52-
mkdir -p ${XDG_RUNTIME_DIR}/podman
53-
podman system service --time=0 unix://${XDG_RUNTIME_DIR}/podman/podman.sock &
54-
pytest -n 5
88+
pytest -n 3 -sqvv --browser=${{ matrix.browser }} --headless --cov=src --cov-report=xml
5589
5690
- name: Upload coverage to Codecov
5791
uses: codecov/codecov-action@v4
@@ -64,6 +98,7 @@ jobs:
6498
docs:
6599
name: Docs Build
66100
runs-on: ubuntu-latest
101+
timeout-minutes: 15
67102
steps:
68103
- name: Checkout
69104
uses: actions/checkout@v4
@@ -72,6 +107,7 @@ jobs:
72107
uses: actions/setup-python@v5
73108
with:
74109
python-version: "3.x"
110+
cache: 'pip'
75111

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

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

117158
steps:
118159
- name: Checkout
@@ -123,6 +164,7 @@ jobs:
123164
with:
124165
python-version: "3.x"
125166
architecture: "x64"
167+
cache: 'pip'
126168

127169
- name: Build and verify with twine
128170
run: |

docs/basic_usage.rst

Lines changed: 28 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,7 @@ This sample only represents simple UI interaction.
77

88
.. code-block:: python
99
10-
from selenium import webdriver
10+
from playwright.sync_api import sync_playwright
1111
from widgetastic.browser import Browser
1212
from widgetastic.widget import View, Text, TextInput
1313
@@ -29,22 +29,33 @@ This sample only represents simple UI interaction.
2929
ROOT = 'div#somediv'
3030
another_text = Text(locator='#h2') # See "Automatic simple CSS locator detection"
3131
32-
selenium = webdriver.Firefox() # For example
33-
browser = CustomBrowser(selenium)
34-
35-
# Now we have the widgetastic browser ready for work
36-
# Let's instantiate a view.
37-
a_view = MyView(browser)
38-
# ^^ you would typically come up with some way of integrating this in your framework.
39-
40-
# The defined widgets now work as you would expect
41-
a_view.read() # returns a recursive dictionary of values that all widgets provide via read()
42-
a_view.a_text.text # Accesses the text
43-
# but the .text is widget-specific, so you might like to use just .read()
44-
a_view.fill({'an_input': 'foo'}) # Fills an_input with foo and returns boolean whether anything changed
45-
# Basically equivalent to:
46-
a_view.an_input.fill('foo') # Since views just dispatch fill to the widgets based on the order
47-
a_view.an_input.is_displayed
32+
# Initialize Playwright and create browser instance
33+
with sync_playwright() as p:
34+
playwright_browser = p.chromium.launch() # or p.firefox.launch()
35+
context = playwright_browser.new_context()
36+
page = context.new_page()
37+
browser = CustomBrowser(page)
38+
39+
# Navigate
40+
browser.url = "https://foo.com"
41+
42+
# Now we have the widgetastic browser ready for work
43+
# Let's instantiate a view.
44+
a_view = MyView(browser)
45+
# ^^ you would typically come up with some way of integrating this in your framework.
46+
47+
# The defined widgets now work as you would expect
48+
a_view.read() # returns a recursive dictionary of values that all widgets provide via read()
49+
a_view.a_text.text # Accesses the text
50+
# but the .text is widget-specific, so you might like to use just .read()
51+
a_view.fill({'an_input': 'foo'}) # Fills an_input with foo and returns boolean whether anything changed
52+
# Basically equivalent to:
53+
a_view.an_input.fill('foo') # Since views just dispatch fill to the widgets based on the order
54+
a_view.an_input.is_displayed
55+
56+
# Clean up resources
57+
context.close()
58+
playwright_browser.close()
4859
4960
5061
Typically, you want to incorporate a system that would do the navigation (like

docs/conf.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -19,7 +19,7 @@
1919

2020
intersphinx_mapping = {
2121
"python": ("http://docs.python.org/3.12/", None),
22-
"selenium": ("http://selenium-python.readthedocs.org/", None),
22+
"playwright": ("https://playwright.dev/python/", None),
2323
}
2424

2525
templates_path = ["_templates"]

docs/guidelines.rst

Lines changed: 9 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -50,19 +50,19 @@ Anyone using this library should consult these guidelines whether one is not vio
5050

5151
- et cetera
5252

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

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

57-
- 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.
57+
- 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.
5858

5959
- Think about what to cache and when to invalidate
6060

61-
- Never store ``WebElement`` objects.
61+
- Never store ``ElementHandle`` objects.
6262

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

65-
- This will help against ``StaleElementReferenceException``
65+
- This will help against stale element issues
6666

6767
- 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.
6868

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

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

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

101-
- 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
101+
- 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
102102

103103
- 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.
104104

105105
- 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.
106106

107-
- ``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.
107+
- ``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.
108108

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

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

docs/internals.rst

Lines changed: 9 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -3,15 +3,15 @@ Internal structure of Widgetastic
33

44
Widgetastic consists of 2 main parts:
55

6-
* `Selenium browser wrapper`_
6+
* `Playwright browser wrapper`_
77
* `Widget system`_
88

9-
.. `Selenium browser wrapper`:
9+
.. `Playwright browser wrapper`:
1010
11-
Selenium browser wrapper
12-
========================
11+
Playwright browser wrapper
12+
===========================
1313

14-
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`.
14+
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`.
1515

1616
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`.
1717

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

3131
If you want to use a complex CSS locator or a different lookup type, you can use
32-
`selenium-smart-locator <https://pypi.python.org/pypi/selenium-smart-locator>`_ library that is used
33-
underneath to process all the locators. You can consult the documentation and pass instances of
34-
``Locator`` instead of a string.
35-
36-
This library is already in the requirements, so it is not necessary to install it.
32+
the built-in ``SmartLocator`` functionality that processes all the locators. You can pass instances of
33+
``SmartLocator`` or raw locator strings, and the system will automatically handle the conversion.
3734

3835
.. `Automatic visibility precedence selection`:
3936
4037
Automatic visibility precedence selection
4138
-----------------------------------------
4239

43-
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?
40+
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?
4441

45-
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).
42+
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).
4643

4744
.. `Widget system`:
4845

docs/intro.rst

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

3434
What this project does NOT do
3535
-----------------------------

pyproject.toml

Lines changed: 3 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -3,8 +3,6 @@ classifiers = [
33
"License :: OSI Approved :: Apache Software License",
44
"Programming Language :: Python",
55
"Programming Language :: Python :: 3",
6-
"Programming Language :: Python :: 3.8",
7-
"Programming Language :: Python :: 3.9",
86
"Programming Language :: Python :: 3.10",
97
"Programming Language :: Python :: 3.11",
108
"Programming Language :: Python :: 3.12",
@@ -17,7 +15,7 @@ classifiers = [
1715
]
1816
description = "Making testing of UIs fantastic."
1917
dynamic = ["version"]
20-
keywords = ["widgetastic", "selenium"]
18+
keywords = ["widgetastic", "playwright", "ui-testing", "automation"]
2119
license = {file = "LICENSE"}
2220
maintainers = [
2321
{name = "Mike Shriver", email = "[email protected]"},
@@ -26,27 +24,24 @@ maintainers = [
2624
]
2725
name = "widgetastic.core"
2826
readme = "README.rst"
29-
requires-python = ">=3.8"
27+
requires-python = ">=3.10"
3028

3129
dependencies = [
3230
"anytree >= 2.9.0",
3331
"cached_property",
34-
"selenium >= 4.0.0",
35-
"selenium-smart-locator",
32+
"playwright >= 1.54.0",
3633
"wait_for",
3734
]
3835

3936
[project.optional-dependencies]
4037
dev = [
41-
"podman",
4238
"pre-commit",
4339
"pytest",
4440
"pytest-xdist",
4541
"pytest-cov",
4642
]
4743
docs = ["sphinx"]
4844
test = [
49-
"podman",
5045
"pytest",
5146
"pytest-xdist",
5247
"pytest-cov",

0 commit comments

Comments
 (0)