Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
19 commits
Select commit Hold shift + click to select a range
7fc3cce
feat(a11y): add accessibility identifiers to Main.qml
hikaps Mar 22, 2026
d37e634
feat(a11y): add accessibility identifiers to all pages
hikaps Mar 22, 2026
836e34a
feat(a11y): add accessibility identifiers to components and dialogs
hikaps Mar 22, 2026
6306d86
feat(e2e): add test infrastructure and smoke tests
hikaps Mar 22, 2026
974acbe
feat(e2e): add session lifecycle tests with mock D-Bus helper
hikaps Mar 22, 2026
be69f3d
ci: add e2e test job and update documentation
hikaps Mar 22, 2026
40366d9
ci: fix e2e-tests job — build selenium-webdriver-at-spi from source
hikaps Mar 22, 2026
ebe3930
ci: add wayland-devel for selenium-webdriver-at-spi build
hikaps Mar 22, 2026
ea41b73
ci: create /usr/bin/pip3 wrapper for selenium-webdriver-at-spi-run
hikaps Mar 22, 2026
3c7d832
ci: move pip3 wrapper to system deps step
hikaps Mar 22, 2026
2c4fdfe
ci: add 'which' package — selenium-webdriver-at-spi-run needs it
hikaps Mar 23, 2026
944f968
ci: set XDG_RUNTIME_DIR for selenium-webdriver-at-spi-run
hikaps Mar 23, 2026
9dedc65
ci: add kwin-wayland for virtual compositor
hikaps Mar 23, 2026
ab5d265
ci: skip kwin_wayland, use offscreen platform in CI
hikaps Mar 23, 2026
9893df1
ci: add python3-numpy for selenium-webdriver-at-spi driver
hikaps Mar 23, 2026
cfe3ed0
ci: set pytest rootdir to fix module resolution
hikaps Mar 23, 2026
b1d1477
fix: add appiumtests/ to sys.path for module resolution
hikaps Mar 23, 2026
48a1cb8
ci: install desktop file to /usr/share/applications
hikaps Mar 23, 2026
9c01387
ci: symlink app binary as desktop ID name
hikaps Mar 24, 2026
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
79 changes: 79 additions & 0 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -53,3 +53,82 @@ jobs:
with:
name: couchplay-build
path: couchplay-*.tar.gz

e2e-tests:
runs-on: ubuntu-latest
container:
image: fedora:41
needs: build
steps:
- uses: actions/checkout@v4

- name: Install system dependencies
run: |
dnf install -y --skip-unavailable \
cmake gcc-c++ git make ruby which \
qt6-qtbase-devel qt6-qtdeclarative-devel qt6-qt5compat-devel qt6-qtwayland-devel \
kf6-kirigami-devel kf6-ki18n-devel kf6-kcoreaddons-devel \
kf6-kconfig-devel kf6-kiconthemes-devel kf6-qqc2-desktop-style \
kf6-kglobalaccel-devel kf6-kwindowsystem-devel \
extra-cmake-modules \
pipewire-devel polkit-devel \
kwayland-devel kpipewire-devel plasma-wayland-protocols-devel \
kwin-wayland \
wayland-devel wayland-protocols-devel \
at-spi2-core at-spi2-atk-devel \
python3-pip python3-dbus python3-evdev python3-numpy \
python3-flask python3-pyatspi python3-gobject-base \
gobject-introspection-devel dbus-x11 \
xcb-util-devel
modprobe uinput || true
# selenium-webdriver-at-spi-run requires /usr/bin/pip3 in PATH
# Fedora's python3-pip only provides 'python3 -m pip', not /usr/bin/pip3
printf '#!/bin/sh\nexec python3 -m pip "$@"\n' > /usr/bin/pip3
chmod +x /usr/bin/pip3

- name: Build selenium-webdriver-at-spi
run: |
git clone --depth 1 https://invent.kde.org/sdk/selenium-webdriver-at-spi.git /tmp/selenium-webdriver-at-spi
# Patch out videorecorder — it needs Qt6GuiPrivate which conflicts with qt6-qtbase-devel on F41
sed -i '/add_subdirectory(videorecorder)/d' /tmp/selenium-webdriver-at-spi/CMakeLists.txt
cmake -B /tmp/selenium-webdriver-at-spi/build \
-S /tmp/selenium-webdriver-at-spi \
-DCMAKE_INSTALL_PREFIX=/usr \
-DQT_MIN_VERSION=6.8
cmake --build /tmp/selenium-webdriver-at-spi/build --parallel 2
cmake --install /tmp/selenium-webdriver-at-spi/build

- name: Install Python dependencies
run: pip install -r appiumtests/requirements.txt

- name: Configure Git safe directory
run: git config --global --add safe.directory /__w/couchplay/couchplay

- name: Configure CMake
run: cmake -B build -DBUILD_TESTING=ON

- name: Build
run: cmake --build build --parallel 2

- name: Install desktop file
run: |
ln -sf "$(pwd)/build/bin/couchplay" /usr/local/bin/couchplay
ln -sf "$(pwd)/build/bin/couchplay" /usr/local/bin/io.github.hikaps.couchplay
cp data/io.github.hikaps.couchplay.desktop \
/usr/share/applications/io.github.hikaps.couchplay.desktop

- name: Run e2e tests
run: selenium-webdriver-at-spi-run pytest appiumtests/ -v
env:
QT_LINUX_ACCESSIBILITY_ALWAYS_ON: 1
XDG_RUNTIME_DIR: /tmp/runtime-runner
QT_QPA_PLATFORM: offscreen
TEST_WITH_KWIN_WAYLAND: "0"

- name: Upload failure screenshots
if: failure()
uses: actions/upload-artifact@v4
with:
name: e2e-failure-screenshots
path: appiumtests/screenshots/
if-no-files-found: ignore
11 changes: 9 additions & 2 deletions AGENTS.md
Original file line number Diff line number Diff line change
Expand Up @@ -16,10 +16,14 @@ distrobox enter fedora-dev -- cmake --build build
# Run (on HOST - gamescope requires host environment)
./build/bin/couchplay

# Tests
# Unit tests
distrobox enter fedora-dev -- ctest --test-dir build --output-on-failure

# Single test: ctest --test-dir build -R DeviceManagerTest --output-on-failure
# E2E tests (requires KDE Plasma Wayland session + selenium-webdriver-at-spi)
pip install -r appiumtests/requirements.txt
QT_LINUX_ACCESSIBILITY_ALWAYS_ON=1 selenium-webdriver-at-spi-run pytest appiumtests/ -v

# Single unit test: ctest --test-dir build -R DeviceManagerTest --output-on-failure
# List tests: ctest --test-dir build -N
# Direct run: ./build/bin/test_devicemanager
```
Expand All @@ -32,6 +36,7 @@ distrobox enter fedora-dev -- ctest --test-dir build --output-on-failure
├── src/qml/ # UI layer (pages + components) - SEE ./src/qml/AGENTS.md
├── helper/ # Privileged D-Bus service - SEE ./helper/AGENTS.md
├── tests/ # QtTest unit tests (11 files, 7.2K lines) - SEE ./tests/AGENTS.md
├── appiumtests/ # E2E tests (selenium-webdriver-at-spi) - SEE ./appiumtests/AGENTS.md
├── src/dbus/ # D-Bus client for helper service
└── data/ # Icons, polkit policy, D-Bus service files
```
Expand All @@ -43,6 +48,7 @@ distrobox enter fedora-dev -- ctest --test-dir build --output-on-failure
| Manager architecture | `./src/core/AGENTS.md` | DeviceManager, SessionManager, etc. |
| QML layer | `./src/qml/AGENTS.md` | Kirigami components, page patterns |
| Test patterns | `./tests/AGENTS.md` | Test naming, fixtures, mocking |
| E2E test patterns | `./appiumtests/AGENTS.md` | selenium-webdriver-at-spi, AT-SPI |
| Privileged helper | `./helper/AGENTS.md` | D-Bus service, user mgmt, device ownership |
| Device detection | `src/core/DeviceManager.{cpp,h}` | Parses `/proc/bus/input/devices` |
| Session orchestration | `src/core/SessionRunner.{cpp,h}` | Starts/stops multiple GamescopeInstance |
Expand Down Expand Up @@ -99,6 +105,7 @@ distrobox enter fedora-dev -- ctest --test-dir build --output-on-failure
- Use `i18nc()` for user-visible strings with context
- Component IDs: camelCase (`id: deviceManager`)
- Properties: `required property` for mandatory injections
- Accessibility: All interactive elements must have `objectName`, `Accessible.role`, `Accessible.name`, and `Accessible.onPressAction`

### Class Declaration Order
1. Q_OBJECT macro
Expand Down
110 changes: 110 additions & 0 deletions appiumtests/AGENTS.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,110 @@
# AGENTS.md - E2E Testing Guidelines

## Overview

E2E tests use [selenium-webdriver-at-spi](https://invent.kde.org/sdk/selenium-webdriver-at-spi) to drive the CouchPlay UI via the Linux accessibility bus (AT-SPI2). Tests run on a virtual Wayland session managed by the runner.

## Running Tests

### Locally (requires KDE Plasma Wayland session)

```bash
pip install -r appiumtests/requirements.txt
QT_LINUX_ACCESSIBILITY_ALWAYS_ON=1 selenium-webdriver-at-spi-run pytest appiumtests/ -v
```

### Skip helper-dependent tests (CI mode)

```bash
selenium-webdriver-at-spi-run pytest appiumtests/ -v -m "not requires_helper"
```

### Run a single test file

```bash
selenium-webdriver-at-spi-run pytest appiumtests/test_home.py -v
```

### Run a single test

```bash
selenium-webdriver-at-spi-run pytest appiumtests/test_home.py::TestHomePage::test_app_launches_home_visible -v
```

## Structure

```
appiumtests/
├── conftest.py # Pytest fixtures, driver lifecycle, failure screenshots
├── helpers/
│ ├── base_test.py # Shared wait/click/navigation utilities
│ ├── mock_helper.py # Mock D-Bus helper service (29 methods)
│ ├── test_users.py # Linux user creation/cleanup for tests
│ └── virtual_devices.py # Virtual gamepad creation via uinput
├── test_home.py # HomePage smoke tests (P0)
├── test_session_setup.py # SessionSetupPage tests (P1)
├── test_session.py # Session lifecycle with mock helper
├── test_profiles.py # Profile management (P2)
├── test_settings.py # Settings tests (P2)
├── test_devices.py # Device assignment (CI-skipped)
├── test_users.py # User management (CI-skipped)
└── requirements.txt
```

## Conventions

### Test Organization

- Class per page: `Test<PageName>`
- Method naming: `test_<action>_<expected_result>()`
- Priority tiers: P0 (smoke), P1 (core flows), P2 (secondary pages), P3 (helper-dependent)

### Session Testing

Session tests (`test_session.py`) use a **mock D-Bus helper** that runs on the system bus. The mock implements all 29 helper methods — `LaunchInstance()` returns a fake PID without spawning gamescope. This enables full session lifecycle testing without real hardware.

**Session fixtures** (session-scoped, in conftest.py):
- `mock_helper` — starts the mock D-Bus helper Python process
- `test_users` — creates `player2` and `player3` Linux users
- `virtual_gamepads` — creates 2 virtual gamepads via uinput (requires root or uinput module)

Tests that use these fixtures are automatically opted into session testing.

### Element Selection

Priority order:
1. `AppiumBy.ACCESSIBILITY_ID` → maps to `objectName` (most reliable)
2. `AppiumBy.NAME` → maps to `Accessible.name` (localized text)
3. `AppiumBy.CLASS_NAME` → last resort (`[role | name]` format)

### Markers

- `@pytest.mark.requires_helper` — tests needing D-Bus helper service (Polkit). Skipped in CI via `-m "not requires_helper"`.

### Timing

- Default timeout: 10 seconds (conftest.py `DEFAULT_TIMEOUT`)
- Always use `WebDriverWait` — never `time.sleep()` except in `go_home()` for page transition delays

### Test Isolation

- `clean_state` fixture (autouse) navigates to home page before and after each test
- Tests must not depend on state from other tests

## Adding New Tests

1. Add `objectName` and `Accessible.*` to the QML element (see root AGENTS.md naming conventions)
2. Create or extend the test file for the target page
3. Inherit from `BaseTest` for shared utilities
4. Use `self.wait_for_element()` / `self.click_by_object_name()` instead of raw driver calls
5. If the test needs the D-Bus helper, add `pytestmark = pytest.mark.requires_helper` at module level

## CI

The `e2e-tests` job in `.github/workflows/ci.yml` runs after the `build` job:
- Fedora 41 container with `selenium-webdriver-at-spi`, `kwayland-devel`, `python3-dbus`, `uinput`
- Builds the app, installs the desktop file
- Mock D-Bus helper runs automatically via session fixture
- Test users created automatically via session fixture
- Runs `selenium-webdriver-at-spi-run` which spawns a virtual KWin Wayland session
- Uploads failure screenshots as artifacts
2 changes: 2 additions & 0 deletions appiumtests/__init__.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
# SPDX-License-Identifier: GPL-3.0-or-later
# SPDX-FileCopyrightText: 2025 CouchPlay Contributors
Loading
Loading