Skip to content

Playwright test dependencies OS install#131

Merged
mshriver merged 1 commit into
RedHatQE:mainfrom
mshriver:pw-tests-os-install
Jul 21, 2026
Merged

Playwright test dependencies OS install#131
mshriver merged 1 commit into
RedHatQE:mainfrom
mshriver:pw-tests-os-install

Conversation

@mshriver

@mshriver mshriver commented Jul 21, 2026

Copy link
Copy Markdown
Collaborator

Summary by Sourcery

Introduce Hatch-based Playwright test environments and improve PatternFly widget/browser compatibility and reliability across components and tests.

New Features:

  • Add Hatch-managed test and lint environments with reusable pytest scripts for PF5/PF6 runs and headed debugging.
  • Provide an OS-aware script to install Playwright system dependencies on Linux distributions beyond Debian/Ubuntu.

Enhancements:

  • Rework GitHub Actions test workflow to use Hatch environments, cache them per OS/Python, and drive Playwright installation and pytest via Hatch scripts.
  • Increase robustness of menu, dropdown, select, slider, chip, navigation, and radio widgets by using wait-for helpers, more accurate locators, and event-driven interactions compatible with PF5/PF6.
  • Tighten Playwright browser context defaults by setting a per-session timeout and adjusting CI matrix naming and pf-version handling for PF5/PF6.
  • Update alert and dropdown tests to use PF-version-agnostic locators and section scoping so they work correctly on PatternFly 6.

Build:

  • Add Playwright as a development dependency and configure Hatch environments and pytest options in pyproject.toml.

Tests:

  • Adjust existing dropdown and alert tests to align with updated locators and PatternFly 6 behaviours, ensuring stability across framework versions.

Chores:

  • Replace usage of the external cached_property package with functools.cached_property in relevant components.

@sourcery-ai

sourcery-ai Bot commented Jul 21, 2026

Copy link
Copy Markdown

Reviewer's Guide

This PR migrates Playwright test setup and execution to Hatch-managed environments, adds OS-aware Playwright dependency installation, and makes multiple widget locators and interactions robust across PatternFly 5/6 and different browsers.

Sequence diagram for robust Dropdown.open interaction with wait_for

sequenceDiagram
  actor Tester
  participant Dropdown
  participant browser
  participant wait_for as _wait_for

  Tester->>Dropdown: open()
  Dropdown->>wait_for: _wait_for(_click, timeout=10, delay=0.5)
  loop until is_open or timeout
    wait_for->>Dropdown: _click()
    Dropdown->>browser: click(BUTTON_LOCATOR)
    Dropdown->>browser: is_open
    browser-->>Dropdown: is_open
    Dropdown-->>wait_for: is_open
  end
  Dropdown-->>Tester: is_open
Loading

File-Level Changes

Change Details Files
Switch CI Playwright workflows from bare pip usage to Hatch-managed test environments, with caching and new scripts for browser install and test execution.
  • Replace direct pip installs of Playwright with pip install hatch and hatch env create test in setup and test jobs.
  • Add caching of Hatch environments keyed by OS, Python version, and pyproject.toml hash.
  • Use hatch run test:python to discover the Playwright version for cache keys.
  • Replace direct playwright install/install-deps calls with hatch run test:install-browsers.
  • Replace raw pytest invocations with hatch run test:pf{pf-version} scripts and adjust pf-version matrix values accordingly.
  • Rename CI job display name to reflect numeric pf-version and update coverage matrix entry.
.github/workflows/tests.yaml
pyproject.toml
Introduce Hatch test/lint environments and pytest configuration, including a script to install Playwright system dependencies in an OS-aware way.
  • Add dev dependency on playwright for tests.
  • Define Hatch test environment with dev features and scripts for installing browsers, PF5/PF6-specific test runs, an all-tests script, and a debug script.
  • Add Hatch lint environment with pre-commit dependency and check script.
  • Configure pytest defaults (testpaths and timeout).
  • Add scripts/install_playwright_deps.py implementing cross-platform Playwright system dependency installation for Debian/Ubuntu, Fedora/RHEL, and no-op on macOS/Windows.
pyproject.toml
scripts/install_playwright_deps.py
Improve Dropdown, Menu, Select, Slider, Navigation, Chip, and Radio widgets to be resilient to PF5/PF6 structural differences and asynchronous state changes.
  • Use wait_for-based polling for Dropdown.open, Menu item select/deselect, and Select create-item to handle async UI updates.
  • Add special handling for always-open Menus when resolving item elements and raise MenuItemNotFound with available items when missing.
  • Refine Dropdown.item_enabled to consider PF6 aria-disabled on child elements and the pf-m-aria-disabled class.
  • Broaden Dropdown TEXT_LOCATOR/DEFAULT_LOCATOR to support PF6 MenuToggle markup.
  • Update BulletChart to use functools.cached_property instead of the cached_property library.
  • Change Chip.close to dispatch a click event via browser.element rather than calling close_button.click directly.
  • Tighten Select.fill to wait for create-item suggestion elements before clicking and returning.
  • Adjust Slider.fill to focus input, fill, dispatch change, press Enter, and wait for displayed value to update.
  • Update Navigation.select to click the first link/button inside li and explicitly raise when SUB_ITEMS_ROOT cannot be found.
  • Refactor Radio.selected to use browser.is_checked and implement a more explicit fill that sets checked via JS and dispatches click/input/change events, returning whether state changed.
src/widgetastic_patternfly5/components/menus/menu.py
src/widgetastic_patternfly5/components/menus/dropdown.py
src/widgetastic_patternfly5/components/menus/select.py
src/widgetastic_patternfly5/components/slider.py
src/widgetastic_patternfly5/charts/bullet_chart.py
src/widgetastic_patternfly5/components/chip.py
src/widgetastic_patternfly5/components/navigation.py
src/widgetastic_patternfly5/components/forms/radio.py
Update tests to align with new locators, PF6 behavior, and Playwright context defaults.
  • Adjust dropdown-disabled test view to use a PF-version-agnostic MenuToggle-based locator constant instead of an OUIA ID and older MenuToggle locator.
  • Scope alert tests to the alert variants section to avoid interference from global site notifications on PF6.
  • Set a default Playwright browser context timeout of 5 seconds for all tests.
testing/components/menus/test_dropdown_disabled.py
testing/components/test_alert.py
testing/conftest.py

Tips and commands

Interacting with Sourcery

  • Trigger a new review: Comment @sourcery-ai review on the pull request.
  • Continue discussions: Reply directly to Sourcery's review comments.
  • Generate a GitHub issue from a review comment: Ask Sourcery to create an
    issue from a review comment by replying to it. You can also reply to a
    review comment with @sourcery-ai issue to create an issue from it.
  • Generate a pull request title: Write @sourcery-ai anywhere in the pull
    request title to generate a title at any time. You can also comment
    @sourcery-ai title on the pull request to (re-)generate the title at any time.
  • Generate a pull request summary: Write @sourcery-ai summary anywhere in
    the pull request body to generate a PR summary at any time exactly where you
    want it. You can also comment @sourcery-ai summary on the pull request to
    (re-)generate the summary at any time.
  • Generate reviewer's guide: Comment @sourcery-ai guide on the pull
    request to (re-)generate the reviewer's guide at any time.
  • Resolve all Sourcery comments: Comment @sourcery-ai resolve on the
    pull request to resolve all Sourcery comments. Useful if you've already
    addressed all the comments and don't want to see them anymore.
  • Dismiss all Sourcery reviews: Comment @sourcery-ai dismiss on the pull
    request to dismiss all existing Sourcery reviews. Especially useful if you
    want to start fresh with a new review - don't forget to comment
    @sourcery-ai review to trigger a new review!

Customizing Your Experience

Access your dashboard to:

  • Enable or disable review features such as the Sourcery-generated pull request
    summary, the reviewer's guide, and others.
  • Change the review language.
  • Add, remove or edit custom review instructions.
  • Adjust other review settings.

Getting Help

@mshriver mshriver changed the title Pw tests os install Playwright test dependencies OS install Jul 21, 2026

@sourcery-ai sourcery-ai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Hey - I've found 1 security issue, 3 other issues, and left some high level feedback:

Security issues:

  • Detected subprocess function 'run' without a static string. If this data can be controlled by a malicious actor, it may be an instance of command injection. Audit the use of this call to ensure it is not controllable by an external resource. You may consider using 'shlex.escape()'. (link)

General comments:

  • In Chip.close, self.browser.element(self.close_button).dispatch_event("click") assumes self.close_button is a locator rather than a widget; if close_button is a widget, you likely want to dispatch the event on its underlying element/locator or keep using its .click() method to avoid type/lookup issues.
  • The new Radio.fill implementation uses a hard-coded JS snippet with evaluate and synthetic events; consider centralizing this pattern or wrapping it in a helper to avoid duplication and make it easier to adjust if Playwright’s evaluation/interaction semantics change.
Prompt for AI Agents
Please address the comments from this code review:

## Overall Comments
- In `Chip.close`, `self.browser.element(self.close_button).dispatch_event("click")` assumes `self.close_button` is a locator rather than a widget; if `close_button` is a widget, you likely want to dispatch the event on its underlying element/locator or keep using its `.click()` method to avoid type/lookup issues.
- The new `Radio.fill` implementation uses a hard-coded JS snippet with `evaluate` and synthetic events; consider centralizing this pattern or wrapping it in a helper to avoid duplication and make it easier to adjust if Playwright’s evaluation/interaction semantics change.

## Individual Comments

### Comment 1
<location path="src/widgetastic_patternfly5/components/forms/radio.py" line_range="57-66" />
<code_context>
+        Returns:
+            ``True`` if the state changed, ``False`` otherwise.
+        """
+        if not values or self.selected:
+            return False
+        el = self.browser.element(self.RADIO_LOC)
+        el.evaluate(
+            "e => {"
+            "  const nativeSetter = Object.getOwnPropertyDescriptor("
+            "    window.HTMLInputElement.prototype, 'checked'"
+            "  ).set;"
+            "  nativeSetter.call(e, true);"
+            "  e.dispatchEvent(new Event('click', {bubbles: true}));"
+            "  e.dispatchEvent(new Event('input', {bubbles: true}));"
+            "  e.dispatchEvent(new Event('change', {bubbles: true}));"
+            "}"
+        )
+        return True
</code_context>
<issue_to_address>
**issue (bug_risk):** Radio.fill should probably respect disabled state before changing selection.

This implementation bypasses `self.radio.fill` and directly manipulates the DOM without checking `self.disabled` or `el.is_enabled()`. As a result, disabled radios can still be programmatically activated, which is inconsistent with other widgets and typical `fill` semantics. Please short‑circuit when the control is disabled and return `False` instead of dispatching events in that case.
</issue_to_address>

### Comment 2
<location path="src/widgetastic_patternfly5/components/menus/dropdown.py" line_range="169-178" />
<code_context>
-            # input element don't have such disabled attributes it at level of session.
             is_el_enabled = el.is_enabled()
         else:
+            el_classes = self.browser.classes(el)
             aria_disabled = str(self.browser.get_attribute("aria-disabled", el)).lower()
+            # PF6 puts aria-disabled on a child element (button/a) rather than the li
+            if aria_disabled != "true":
+                child_els = self.browser.elements(".//*[@aria-disabled='true']", parent=el)
+                if child_els:
+                    aria_disabled = "true"
             is_el_enabled = (
-                "pf-m-disabled" not in self.browser.classes(el) and aria_disabled != "true"
+                "pf-m-disabled" not in el_classes
+                and "pf-m-aria-disabled" not in el_classes
+                and aria_disabled != "true"
             )

</code_context>
<issue_to_address>
**suggestion (bug_risk):** Checkbox handling in item_enabled ignores aria-disabled and modifier classes.

In the `type == 'checkbox'` branch, we only use `el.is_enabled()` and skip the ARIA / `pf-m-disabled` / `pf-m-aria-disabled` checks. Since PatternFly can represent disabled state via ARIA or CSS rather than the native `disabled` attribute, a checkbox that is visually/semantically disabled may still be treated as enabled. Please apply the same ARIA/class-based logic to checkboxes (or factor it out for reuse) so disabled checkboxes are not reported as enabled.

Suggested implementation:

```python
        _wait_for(_click, timeout=10, delay=0.5)
        return self.is_open

    def close(self, ignore_nonpresent=False):
        el = self.item_element(item, close=False, **kwargs)

        if self.browser.get_attribute("type", el) == "checkbox":
            el_classes = self.browser.classes(el)
            aria_disabled = str(self.browser.get_attribute("aria-disabled", el)).lower()
            # PF6 puts aria-disabled on a child element (button/a) rather than the li
            if aria_disabled != "true":
                child_els = self.browser.elements(".//*[@aria-disabled='true']", parent=el)
                if child_els:
                    aria_disabled = "true"
            is_el_enabled = (
                el.is_enabled()
                and "pf-m-disabled" not in el_classes
                and "pf-m-aria-disabled" not in el_classes
                and aria_disabled != "true"
            )
        else:

from wait_for import wait_for as _wait_for
from widgetastic.exceptions import NoSuchElementException
from widgetastic.utils import ParametrizedLocator
from widgetastic.widget import Widget
        if self.is_open:
            return

```

You may want to:
1. Factor the ARIA/class-based enabled check into a small helper (e.g. `_is_item_enabled(el)`) and reuse it in both the checkbox and non-checkbox branches to avoid duplication.
2. Ensure that in the non-checkbox branch, the same `el_classes` / `aria_disabled` logic is being used consistently (if the code there diverged or changed elsewhere in the file, align it with this implementation).
</issue_to_address>

### Comment 3
<location path="src/widgetastic_patternfly5/components/chip.py" line_range="251" />
<code_context>

     def close(self):
-        self.close_button.click()
+        self.browser.element(self.close_button).dispatch_event("click")

     @classmethod
</code_context>
<issue_to_address>
**issue (bug_risk):** Using close_button directly as a locator for browser.element may be fragile.

The previous `self.close_button.click()` relied on the widget’s own click behavior. Now `self.close_button` is passed to `browser.element`, which usually expects a locator/XPath, not a widget instance. If `close_button` isn’t guaranteed to be a valid locator or resolvable by `browser.element`, this could fail at runtime. Consider dispatching the event on the underlying element (e.g. `self.close_button._widget` / `self.close_button.element`) or using the locator that `close_button` wraps instead.
</issue_to_address>

### Comment 4
<location path="scripts/install_playwright_deps.py" line_range="47" />
<code_context>
    subprocess.run(cmd, check=True)
</code_context>
<issue_to_address>
**security (python.lang.security.audit.dangerous-subprocess-use-audit):** Detected subprocess function 'run' without a static string. If this data can be controlled by a malicious actor, it may be an instance of command injection. Audit the use of this call to ensure it is not controllable by an external resource. You may consider using 'shlex.escape()'.

*Source: opengrep*
</issue_to_address>

Sourcery is free for open source - if you like our reviews please consider sharing them ✨
Help me be more useful! Please click 👍 or 👎 on each comment and I'll use the feedback to improve your reviews.

Comment thread src/widgetastic_patternfly5/components/forms/radio.py Outdated
Comment thread src/widgetastic_patternfly5/components/menus/dropdown.py Outdated
Comment thread src/widgetastic_patternfly5/components/chip.py Outdated
Comment thread scripts/install_playwright_deps.py Outdated
@mshriver
mshriver force-pushed the pw-tests-os-install branch 2 times, most recently from 633f413 to aa3e962 Compare July 21, 2026 14:54
the playwright dependency installer only handles apt-get and explicitly
will not be handling any other operating systems

provide a script, run through hatch, to handle playwright dependency
install

Co-authored-by: Claude <noreply@anthropic.com>
@mshriver
mshriver force-pushed the pw-tests-os-install branch from 1d6890a to 9bb26bd Compare July 21, 2026 15:05
@mshriver
mshriver merged commit 8a03600 into RedHatQE:main Jul 21, 2026
2 of 11 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant