Skip to content
Open
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
31 changes: 21 additions & 10 deletions .github/workflows/e2e.yml
Original file line number Diff line number Diff line change
Expand Up @@ -8,9 +8,24 @@ on:
workflow_dispatch:

jobs:
coverage:
# Runs first; fast static check that gates the long device jobs so
# missing demos / flows fail before the emulator boots.
runs-on: ubuntu-latest
steps:
- name: Checkout
uses: actions/checkout@v4
- name: Set up Python
uses: actions/setup-python@v5
with:
python-version: '3.11'
- name: Check E2E coverage of pythonnative.__all__
run: python scripts/check-e2e-coverage.py

e2e-android:
needs: coverage
runs-on: ubuntu-latest
timeout-minutes: 30
timeout-minutes: 60

steps:
- name: Checkout
Expand Down Expand Up @@ -46,12 +61,12 @@ jobs:
with:
api-level: 31
arch: x86_64
script: >-
bash -lc "cd examples/hello-world && pn run android --no-logs && cd ../.. && maestro test tests/e2e/android.yaml"
script: bash -lc "./scripts/run-e2e.sh android"

e2e-ios:
needs: coverage
runs-on: macos-latest
timeout-minutes: 30
timeout-minutes: 60

steps:
- name: Checkout
Expand All @@ -71,11 +86,7 @@ jobs:
echo "$HOME/.maestro/bin" >> $GITHUB_PATH
brew tap facebook/fb && brew install idb-companion

- name: Build and launch iOS app
working-directory: examples/hello-world
run: pn run ios --no-logs
- name: Build and run E2E tests
run: ./scripts/run-e2e.sh ios
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}

- name: Run E2E tests
run: maestro --platform ios test tests/e2e/ios.yaml
44 changes: 32 additions & 12 deletions CONTRIBUTING.md
Original file line number Diff line number Diff line change
Expand Up @@ -35,10 +35,13 @@ cd examples/hello-world && pn run android
- `src/pythonnative/` – installable library and CLI
- `pythonnative/` – core cross‑platform UI components and utilities
- `cli/` – `pn` command
- `tests/` – unit tests for the library
- `tests/` – unit tests for the library, plus the Maestro E2E suite
- `e2e/` – the comprehensive E2E suite (see [E2E tests](#e2e-tests-maestro) below and `tests/e2e/AGENTS.md`)
- `templates/` – Android/iOS project templates and zips
- `examples/` – runnable example apps
- `hello-world/` – minimal demo app using the library
- `hello-world/` – minimal marketing demo
- `e2e-suite/` – comprehensive feature catalog that drives the Maestro E2E suite
- `scripts/` – helper scripts (`check.sh`, `run-e2e.sh`, `check-e2e-coverage.py`)
- `README.md`, `pyproject.toml` – repo docs and packaging

## Coding guidelines
Expand Down Expand Up @@ -269,7 +272,9 @@ fix/cli-regression

### E2E tests (Maestro)

End-to-end tests use [Maestro](https://maestro.dev/) to drive the hello-world example on real emulators and simulators.
End-to-end tests use [Maestro](https://maestro.dev/) to drive the dedicated `examples/e2e-suite` app on real emulators and simulators. That app contains one screen per public symbol in `pythonnative.__all__`; every flow under `tests/e2e/flows/<category>/` exercises one symbol.

The dedicated `examples/hello-world` app is left in place as a small marketing demo; it is **not** the E2E target.

```bash
# Install Maestro (one-time)
Expand All @@ -279,21 +284,36 @@ curl -Ls "https://get.maestro.mobile.dev" | bash
brew tap facebook/fb && brew install idb-companion
```

Build and launch the app first, then run the tests:
Build and run everything via the convenience script:

```bash
cd examples/hello-world

# Android (emulator must be running)
pn run android
maestro test ../../tests/e2e/android.yaml
./scripts/run-e2e.sh android

# iOS (simulator must be running)
./scripts/run-e2e.sh ios
```

For tight iteration, run a single category instead of the full pass:

# iOS (simulator must be running; --platform ios needed when an Android emulator is also connected)
pn run ios
maestro --platform ios test ../../tests/e2e/ios.yaml
```bash
./scripts/run-e2e.sh android hooks
./scripts/run-e2e.sh ios components
```

Test flows live in `tests/e2e/flows/` and cover the main screen rendering, counter interaction, and multi-screen navigation. The `e2e.yml` workflow runs these automatically on pushes to `main` and PRs.
Available categories: `components`, `hooks`, `navigation`, `layout`, `styling`, `animations`, `misc`.

A coverage checker, `scripts/check-e2e-coverage.py`, gates CI: every name in `pythonnative.__all__` must be covered by a demo + flow, or listed in `INTENTIONAL_EXEMPTIONS` with a justification.

When you add a new public symbol you must also:

1. Add a demo screen under `examples/e2e-suite/app/screens/<category>/`.
2. Append a `DemoEntry` in `examples/e2e-suite/app/registry.py`.
3. Add a Maestro flow at `tests/e2e/flows/<category>/<name>.yaml`.
4. Append the flow to the top-level `tests/e2e/android.yaml`, `tests/e2e/ios.yaml`, and the matching `tests/e2e/suites/<category>.yaml`.
5. Confirm `python scripts/check-e2e-coverage.py` exits 0.

`tests/e2e/AGENTS.md` is the deeper reference (label conventions, failure triage, naming rules); AI agents should read it before touching the suite. The `e2e.yml` workflow runs the suite automatically on pushes to `main` and PRs.

### CI

Expand Down
4 changes: 4 additions & 0 deletions examples/e2e-suite/.gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
build/
__pycache__/
*.pyc
.DS_Store
37 changes: 37 additions & 0 deletions examples/e2e-suite/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
# PythonNative E2E Suite

A comprehensive demo app that exercises every public feature in `pythonnative`. It is the target of the top-level Maestro E2E suite and doubles as a living reference for the framework's surface area.

Unlike `examples/hello-world`, this app is not a marketing demo: it is structured for automated testing. Each PythonNative feature gets a dedicated screen that:

- Renders a stable, unique title (so Maestro can wait for the screen to appear).
- Exposes interactive controls with stable, unique labels (so Maestro can tap them).
- Prints a "Result:" line that reflects the feature's state (so Maestro can assert behavior, not just rendering).

## Running locally

From the repo root:

```bash
cd examples/e2e-suite
pn run android # or: pn run ios
```

Then, in another shell:

```bash
# Android
maestro test tests/e2e/android.yaml

# iOS (use --platform ios if Android is also connected)
maestro --platform ios test tests/e2e/ios.yaml
```

## Adding a new feature demo

1. Add a screen module under `app/screens/<category>/<feature>.py` exporting a `pn.component`-decorated function.
2. Register it in `app/registry.py` with a unique `id`.
3. Add a Maestro flow at `tests/e2e/flows/<category>/<feature>.yaml` that opens the screen via its registry `id` and asserts the expected behavior.
4. Re-run `scripts/check-e2e-coverage.py` to make sure every public symbol in `pythonnative.__all__` is covered by a flow.

See `tests/e2e/AGENTS.md` for a deeper tour of how AI agents should interact with this suite.
Empty file.
39 changes: 39 additions & 0 deletions examples/e2e-suite/app/main.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
"""E2E suite entry point.

Wires every demo screen from :mod:`app.registry` into the root
[`Stack.Navigator`][pythonnative.create_stack_navigator]. The first
route, ``"Home"``, is a categorized list of buttons that opens the
rest of the demos. Each demo screen owns its own back navigation via
[`use_navigation().go_back()`][pythonnative.use_navigation].

The stack-only architecture keeps the navigation surface flat and
predictable for automated tests: every demo is reachable in exactly
one push, and every back press lands the user back on ``"Home"``.
"""

from __future__ import annotations

import pythonnative as pn
from app.registry import DEMOS
from app.screens.category import CategoryListScreen
from app.screens.home import HomeScreen

print("[e2e-suite] main module imported")

Stack = pn.create_stack_navigator()


@pn.component
def App() -> pn.Element:
"""Root component: a Stack with Home, every Category screen, and every demo."""
return pn.NavigationContainer(
Stack.Navigator(
Stack.Screen("Home", component=HomeScreen, options={"title": "PythonNative E2E Suite"}),
Stack.Screen(
"Category",
component=CategoryListScreen,
options={"title": "Category"},
),
*(Stack.Screen(demo.id, component=demo.component, options={"title": demo.title}) for demo in DEMOS),
)
)
Loading
Loading