Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
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
87 changes: 79 additions & 8 deletions .github/workflows/tests.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -8,34 +8,105 @@ on:
types: ["opened", "synchronize", "reopened"]
create:

# Auto-cancel outdated runs when new commits are pushed
concurrency:
group: ${{ github.workflow }}-${{ github.ref }}
cancel-in-progress: true

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
test:
name: pf-${{ matrix.pf-version }} (🐍 ${{ matrix.python-version }}, ${{ matrix.browser }})
runs-on: ubuntu-24.04
runs-on: ubuntu-latest
needs: setup-browsers
timeout-minutes: 30
strategy:
fail-fast: false
matrix:
browser: [chrome, firefox]
browser: [chromium, firefox]
python-version: ["3.12", "3.13"]
pf-version: ["v5", "v6"]
# Reduce redundancy: only run coverage for one combination
include:
- browser: chromium
python-version: "3.13"
pf-version: "v6"
run-coverage: true
exclude: []
steps:
- name: Pull selenium-standalone
run: podman pull selenium/standalone-${{ matrix.browser }}:latest
- uses: actions/checkout@v4

- uses: actions/setup-python@v5
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: Install dependencies
run: |
pip install -U pip wheel
pip install .[dev]
- name: Test with pytest

- name: Test with pytest (with coverage)
if: matrix.run-coverage == true
timeout-minutes: 25
run: |
pytest -v -n 2 --headless --browser=${{ matrix.browser }} --pf-version=${{ matrix.pf-version }} --cov=./src --cov-report=xml --reruns 2 --reruns-delay 5

- name: Test with pytest (without coverage)
if: matrix.run-coverage != true
timeout-minutes: 25
run: |
pytest -v -n 3 --browser-name=${{ matrix.browser }} --pf-version=${{ matrix.pf-version }} --cov=./ --cov-report=xml
pytest -v -n 2 --headless --browser=${{ matrix.browser }} --pf-version=${{ matrix.pf-version }} --reruns 2 --reruns-delay 5

- name: Upload coverage to Codecov
uses: codecov/codecov-action@v4
if: matrix.run-coverage == true
uses: codecov/codecov-action@v5
Comment thread
sourcery-ai[bot] marked this conversation as resolved.
Comment thread
digitronik marked this conversation as resolved.
with:
token: ${{ secrets.CODECOV_TOKEN }}
file: coverage.xml
files: coverage.xml
flags: unittests
name: ${{ github.run_id }}-py-${{ matrix.python-version }}-${{ matrix.browser }}
2 changes: 1 addition & 1 deletion .pre-commit-config.yaml
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
repos:
- repo: https://github.com/astral-sh/ruff-pre-commit
rev: v0.13.3
rev: v0.14.0
hooks:
- id: ruff
args:
Expand Down
110 changes: 98 additions & 12 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -18,11 +18,29 @@
</a>
</p>

## Overview

This library offers Widgetastic Widgets for [PatternFly v5/v6](https://www.patternfly.org/), serving as an extended
itteration of [widgetastic.patternfly4](https://github.com/RedHatQE/widgetastic.patternfly4).
iteration of [widgetastic.patternfly4](https://github.com/RedHatQE/widgetastic.patternfly4).

Built on top of [widgetastic.core](https://github.com/RedHatQE/widgetastic.core) with **Playwright** as the browser
automation engine, this library provides a robust and modern approach to UI testing for PatternFly components.

## Installation

```bash
# Install from PyPI
pip install widgetastic.patternfly5

# Or install from source
git clone https://github.com/RedHatQE/widgetastic.patternfly5.git
cd widgetastic.patternfly5
pip install -e .
```

## Supported Components

### Components:
### Components
- [alert](https://www.patternfly.org/components/alert)
- [breadcrumb](https://www.patternfly.org/components/breadcrumb)
- [button](https://www.patternfly.org/components/button)
Expand Down Expand Up @@ -65,9 +83,10 @@ itteration of [widgetastic.patternfly4](https://github.com/RedHatQE/widgetastic.
- [line-chart](https://www.patternfly.org/charts/line-chart)
- [pie-chart](https://www.patternfly.org/charts/pie-chart)

### Patterns:
### Patterns
- [card-view](https://www.patternfly.org/patterns/card-view)

## Development

### Contribution guide

Expand All @@ -88,24 +107,91 @@ pip install -e .[dev]
# if you use zsh, pip install will fail. Use this instead:
pip install -e ".[dev]"

# install Playwright browsers for testing
playwright install chromium firefox
playwright install-deps

# setup pre-commit hooks
pre-commit install
```

### Testing

The library has selenium tests that are performed against [Patternfly v6 docs](https://www.patternfly.org) and [Patternfly v5 docs](https://v5-archive.patternfly.org).
It's also configured to run the tests every time when a new version of that page is released.
The library includes comprehensive tests that run against the official PatternFly documentation pages:
- [PatternFly v6](https://www.patternfly.org) (latest)
- [PatternFly v5](https://v5-archive.patternfly.org) (archived)

Tests are powered by **Playwright**, providing fast, reliable, and modern browser automation.

#### Prerequisites

Before running tests, install Playwright browsers:

```bash
# Install Playwright (included in dev dependencies)
pip install -e ".[dev]"

# Install Playwright browsers
playwright install chromium firefox

# Install system dependencies (if needed)
playwright install-deps
```

#### Running Tests

**Basic test execution:**

```bash
# Run tests with default settings (chromium, v6, headed mode)
pytest -v

# Run tests against PatternFly v5
pytest -v --pf-version v5

# Run tests with Firefox
pytest -v --browser firefox

# Run tests in headless mode (no browser window)
pytest -v --headless
```

**Advanced options:**

```bash
# Run tests in parallel (speeds up execution)
pytest -v -n 3 --browser chromium --pf-version v6

# Run with slow motion for debugging (100ms delay between actions)
pytest -v --slowmo 100

# Run specific test file
pytest testing/components/test_button.py -v --browser firefox

# Run tests with coverage
pytest -v --cov=./ --cov-report=html
```

**Available test options:**

Tests spawn a container from official selenium image - [selenium/standalone-{chrome/firefox}](https://hub.docker.com/u/selenium).
We can check local runs via vnc `http://localhost:7900`
| Option | Choices | Default | Description |
|--------|---------|---------|-------------|
| `--browser` | `chromium`, `firefox` | `chromium` | Browser to use for testing |
| `--pf-version` | `v5`, `v6` | `v6` | PatternFly version to test against |
| `--headless` | flag | `False` | Run in headless mode (no UI) |
| `--slowmo` | milliseconds | `0` | Slow down operations for debugging |
| `-n` | number | `1` | Number of parallel workers (requires pytest-xdist) |

**Note:** Tests use `podman` to manage containers. Please install it before running.

It's possible to run tests in parallel to speed up the execution. Make sure that you have **xdist** python plugin installed.
#### Debugging Tests

Use `-n` key to specify a number
of workers:
When debugging, it's helpful to:
1. Run tests in **headed mode** (without `--headless`) to see browser interactions
2. Use `--slowmo` to slow down actions and observe what's happening
3. Run a single test file or test function instead of the entire suite
4. Reduce parallelism (`-n 1` or remove `-n` flag) to avoid race conditions

```bash
pytest --browser-name firefox --pf-version v5 -n 2 -vv
# Debug specific test with visible browser and slow execution
pytest testing/components/test_modal.py::test_modal_basic -v --slowmo 1000
```
3 changes: 2 additions & 1 deletion pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,7 @@ readme = "README.md"
requires-python = ">=3.11"

dependencies = [
"widgetastic.core>=1.0.6",
"widgetastic.core>=2.0.0a2",
]

[project.optional-dependencies]
Expand All @@ -33,6 +33,7 @@ dev = [
"pytest-cov",
"pytest-xdist",
"pytest-rerunfailures",
"pytest-timeout",
"codecov",
]
doc = ["sphinx"]
Expand Down
9 changes: 5 additions & 4 deletions src/widgetastic_patternfly5/charts/alerts_timeline_chart.py
Original file line number Diff line number Diff line change
Expand Up @@ -11,10 +11,10 @@ class AlertsTimelineChart(LineChart):
locator: If you have specific locator else it will take pf-chart.
"""

Y_AXIS_ROW = "./*[name()='svg']/*[name()='g'][3]/*[name()='g']"
Y_AXIS_ROW_LINE = "./*[name()='path']"
Y_AXIS_ROW = ".//*[name()='svg']/*[name()='g'][3]/*[name()='g']"
Y_AXIS_ROW_LINE = ".//*[name()='path']"

TOOLTIP = "./*[name()='svg']/*[name()='g'][5]"
TOOLTIP = ".//*[name()='svg']/*[name()='g'][5]"
TOOLTIP_X_AXIS_LABLE = None
TOOLTIP_LABLES = None
TOOLTIP_VALUES = ".//*[name()='text']/*[name()='tspan']"
Expand Down Expand Up @@ -48,7 +48,8 @@ def read(self):
_row_data = []
for line_el in lines_el:
self.browser.move_to_element(line_el)
self.browser.click(line_el)
# Sometime path elements are not interactable so use force click.
self.browser.click(line_el, force=True)
tooltip_el = self.browser.wait_for_element(self.TOOLTIP)

label_data = {}
Expand Down
16 changes: 9 additions & 7 deletions src/widgetastic_patternfly5/charts/bullet_chart.py
Original file line number Diff line number Diff line change
@@ -1,5 +1,7 @@
import re
import time

from cached_property import cached_property
from widgetastic.utils import ParametrizedLocator
from widgetastic.widget import Text, View
from widgetastic.xpath import quote
Expand All @@ -21,7 +23,7 @@ class BulletChart(View):
ROOT = ParametrizedLocator("{@locator}")

DEFAULT_LOCATOR = ".//div[contains(@class, 'chartbullet')]"
ITEMS = ".//*[name()='g']/*[name()='path' and not(contains(@style, 'type:square'))]"
ITEMS = "//*[name()='g' and not(./*[name()='rect'])]/*[name()='path']"
TOOLTIP_REGEX = re.compile(r"(.*?): ([\d]+)")
APPLY_OFFSET = True

Expand Down Expand Up @@ -73,28 +75,28 @@ def get_legend(self, label):
except StopIteration:
return None

@property
@cached_property
Comment thread
digitronik marked this conversation as resolved.
def data(self):
"""Read graph and returns all Data Point objects."""
_data = []
# focus away from graph
self.parent_browser.move_to_element("//body")

for el in self.browser.elements(self.ITEMS):
self.browser.move_to_element(el)
self.browser.click(el)
time.sleep(0.2)
Comment thread
digitronik marked this conversation as resolved.
# Sometime path elements are not interactable so use force click.
self.browser.click(el, force=True)

if self.APPLY_OFFSET:
dx, dy = self._offsets(el)
self.browser.move_by_offset(dx, dy)
self.browser.move_by_offset(origin=el, x=dx, y=dy)
Comment thread
digitronik marked this conversation as resolved.

match = self.TOOLTIP_REGEX.match(self.tooltip.text)
if match:
_data.append(
DataPoint(
label=match.groups()[0],
value=int(match.groups()[1]),
color=el.value_of_css_property("fill"),
color=self.browser.value_of_css_property(el, "fill"),
)
)
return _data
Expand Down
6 changes: 3 additions & 3 deletions src/widgetastic_patternfly5/charts/legend.py
Original file line number Diff line number Diff line change
Expand Up @@ -32,9 +32,9 @@ def _legend_color_map(self):
self.browser.elements(self.LEGEND_ICON_ITEMS),
self.browser.elements(self.LEGEND_LABEL_ITEMS),
):
color = icon.value_of_css_property("fill")
if not color:
color = icon.value_of_css_property("color")
color = self.browser.value_of_css_property(
icon, "fill"
) or self.browser.value_of_css_property(icon, "color")
_data[self.browser.text(label_el)] = color
return _data

Expand Down
6 changes: 3 additions & 3 deletions src/widgetastic_patternfly5/charts/line_chart.py
Original file line number Diff line number Diff line change
Expand Up @@ -83,9 +83,9 @@ def read(self, offset=(0, -100)):
_data = {}

for lab_el in self._x_axis_labels_map.values():
self.browser.move_to_element(lab_el)
self.browser.click(lab_el)
self.browser.move_by_offset(*offset)
# Sometime path elements are not interactable so use force click.
self.browser.click(lab_el, force=True)
self.browser.move_by_offset(lab_el, *offset)
Comment thread
digitronik marked this conversation as resolved.
tooltip_el = self.browser.wait_for_element(self.TOOLTIP)

x_axis_label = self.browser.text(self.TOOLTIP_X_AXIS_LABLE, parent=tooltip_el)
Expand Down
5 changes: 1 addition & 4 deletions src/widgetastic_patternfly5/components/clipboard_copy.py
Original file line number Diff line number Diff line change
Expand Up @@ -20,10 +20,7 @@ class BaseClipboardCopy:
def is_editable(self):
if self.is_inline:
return False
if self.browser.get_attribute("readonly", self.text):
return False
else:
return True
return "readonly" not in self.browser.attributes(self.text)

@property
def is_inline(self):
Expand Down
Loading