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
17 changes: 17 additions & 0 deletions tests/.env.example
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
# Rename this file to .env and fill in your credentials.
# .env is gitignored – never commit real passwords.

# URL of the OSBv2 instance under test.
# Default: the v2dev staging environment.
APP_URL=https://v2dev.opensourcebrain.org/

# Test user's Keycloak username (must have an active account).
OSB_USERNAME=your-osb-username

# Test user's Keycloak password.
OSB_PASSWORD=your-osb-password

# Authentication method: "ui" or "cookie".
# ui – type credentials into the Keycloak login form (realistic, slower).
# cookie – pre-set the kc-access JWT cookie before tests (faster, fragile).
AUTH_METHOD=ui
9 changes: 9 additions & 0 deletions tests/.gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
# Never commit real credentials
.env

# Python bytecode
__pycache__/
*.py[cod]

# pytest
.pytest_cache/
127 changes: 127 additions & 0 deletions tests/AGENTS.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,127 @@
# AGENTS.md — AI Agent Instructions for OSBv2 Selenium Test Suite

## Overview

This directory contains end-to-end browser tests for the OSBv2 portal
(`https://v2dev.opensourcebrain.org/`). Stack: **Python + Selenium 4 +
pytest + Firefox/Chrome (headless by default)**. Architecture: **Page
Object Model**.

Tests target the **staging instance** (v2dev). Keycloak credentials
required for login-dependent tests.

**Key env vars** (set in `tests/.env`):
- `OSB_USERNAME` / `OSB_PASSWORD` — Keycloak credentials (NOT `USERNAME`/`PASSWORD` — those clash with Linux system env vars)
- `APP_URL` — `https://v2dev.opensourcebrain.org/`
- `AUTH_METHOD` — `ui` (default) or `cookie`
- `BROWSER` — `chrome` (default, falls back to `firefox` if Chrome not found)

## Running Tests

```bash
# First time
cp tests/.env.example tests/.env # then edit with your credentials

# Install deps
uv pip install -r tests/requirements.txt

# Collect / list all tests
pytest tests/ --collect-only

# Run fast smoke tests
pytest tests/ -m smoke -n 2

# Run everything except slow app-open tests
pytest tests/ -m "not slow" -n 2

# Single file
pytest tests/tests/test_auth.py -v

# Single test
pytest tests/tests/test_auth.py::TestAuth::test_login -v

# Show browser window
PUPPETEER_DISPLAY=1 pytest tests/tests/test_auth.py

# Run with Firefox explicitly
BROWSER=firefox pytest tests/
```

## File Map

| File | Role |
|---|---|
| `config.py` | Env-var config. Uses `Path(__file__)` to find `.env`. **Uses `OSB_USERNAME`/`OSB_PASSWORD` — not `USERNAME`/`PASSWORD`.** |
| `css_selectors.py` | CSS / XPath selectors. **Renamed from `selectors.py` to avoid shadowing Python stdlib `selectors`.** |
| `conftest.py` | Fixtures: `driver` (Chrome or Firefox), `base_url`, `authenticated_user`. Firefox fallback if Chrome missing. |
| `helpers/auth.py` | `login()`, `logout()`, `ensure_logged_in()`. `AUTH_METHOD=ui` drives Keycloak form via JS; `AUTH_METHOD=cookie` sets `kc-access` JWT cookie. |
| `helpers/wait_conditions.py` | Custom expected conditions placeholder. |
| **Page Objects** | |
| `pages/base_page.py` | `BasePage` — `wait_for`, `click` (with JS fallback for obscured elements), `type_text`, `switch_to_app_frame`, `switch_to_main`. |
| `pages/home_page.py` | Workspaces listing, tabs, sidebar nav (About/Workspaces/Repositories), create-new menu, templates, search/filter, about dialog open/close. |
| `pages/workspace_page.py` | Workspace detail, open-with split button, actions menu (delete/clone/edit/make-public/make-private), clone snackbar handling, workspace editor form (name, visibility select, save). |
| `pages/workspace_open_page.py` | `/workspaces/open/:id/:app` — application iframe view. |
| `pages/repository_page.py` | `RepositoriesPage` (listing, tabs, grid/list toggle, add-repository dialog, search/filter) + `RepositoryPage` (detail, file tree, select-all, individual file click, workspace creation dialogs). |
| `pages/user_page.py` | User profile, tabs, groups, edit profile dialog (picture URL, save, cancel). |
| `pages/auth_page.py` | Keycloak login form page object. |
| **Test Files** | |
| `tests/test_auth.py` | Home page load, login, logout, round-trip, **protected route redirect**. |
| `tests/test_about_dialog.py` | About dialog open and close. |
| `tests/test_workspaces.py` | View public, create, view detail, delete, **clone, edit, make public, make private**. |
| `tests/test_applications.py` | Open with NetPyNE / NWB Explorer / JupyterLab (iframe, **`@slow`**). |
| `tests/test_repositories.py` | Listing, detail, **add repository, grid/list toggle, file tree browsing, create workspace from repo (with/without selection)**. |
| `tests/test_user_profile.py` | Profile load, tabs, edit button, **edit profile dialog**. |
| `tests/test_search_filter.py` | Search fields, filter popover. |

## Conventions

### When adding a new test

1. **Add selectors to `css_selectors.py`** — never inline them. Prefer `id` attrs over MUI classes. Use XPath (`//button[contains(., "text")]`) for text-based matching — `:has-text()` pseudo-selectors are NOT valid CSS and fail in Selenium.
2. **Use page objects** — add methods to the relevant page class.
3. **Document flows** — every test function must have a docstring with numbered steps.
4. **Mark slow tests** `@pytest.mark.slow` (JupyterHub server spawn, >1 min).
5. **Mark smoke tests** `@pytest.mark.smoke` (fast, always-pass).
6. **Use `pytest.skip()`** for optional/missing data, not hard failures.
7. **Use unique names** — prefix with `selenium-test-{uuid_suffix}` for parallel safety.
8. **Clean up** — creation tests should delete what they create.
9. **Use the click() with JS fallback** — `base_page.py`'s `click()` catches `ElementClickInterceptedException` and falls back to `execute_script("arguments[0].click()")`.

### Auth

- **`OSB_USERNAME` / `OSB_PASSWORD`** — not `USERNAME`/`PASSWORD` (which is a standard Linux env var).
- `authenticated_user` fixture (session-scoped, once per worker). Calls `ensure_logged_in()`.
- `AUTH_METHOD=ui`: drives real Keycloak form via `execute_script()` to set field values + dispatch `input` events.
- `AUTH_METHOD=cookie`: sets `kc-access` JWT cookie directly.
- **Error detection**: The auth helper checks for "Invalid username or password" and other Keycloak error messages and raises a clear `RuntimeError`.
- The `.env` path is resolved explicitly via `Path(__file__).resolve().parent / ".env"`.

### Iframes

- `<iframe id="workspace-frame">` holds the Jupyter application.
- Use `page.switch_to_app_frame()` / `page.switch_to_main()`.

### Timeouts (config.py)

| Setting | Default | Meaning |
|---|---|---|
| `IMPLICIT_WAIT` | 15s | Element wait fallback |
| `PAGE_LOAD_TIMEOUT` | 120s | Full page navigation |
| `WORKSPACE_LOAD_TIMEOUT` | 600s | Application iframe readiness |
| Auth helper waits | 120s (Keycloak), 60s (post-login redirect) | Hardcoded for slow clusters |

### Parallel execution

- `pytest-xdist` with `-n 2`. Session-scoped `driver` per worker.
- Workspace names use `uuid.uuid4().hex[:8]` suffix.

## Known Issues / Gotchas

- **`css_selectors.py` naming**: Don't rename to `selectors.py` — shadows Python stdlib `selectors`.
- **`:has-text()` is NOT valid CSS**: Selenium WebDriver doesn't support jQuery pseudo-selectors. Use XPath `//button[contains(., "text")]` instead.
- **Element click intercepted**: MUI overlays obscure buttons. Fixed in `base_page.py` with JS fallback.
- **Stale JupyterHub servers**: Clean up at `/hub/home` before running `@slow` app tests.
- **Firefox fallback**: `conftest.py` auto-detects missing Chrome and switches to Firefox.
- **Slow cluster**: All timeouts are set generously (120s+). If running on a faster instance, reduce `PAGE_LOAD_TIMEOUT`.
- **Staging API (503)**: The workspace/repository/user APIs on staging may return 503 when under load. Tests that need API data gracefully `pytest.skip()`.
- **Selenium 4 API only**: `find_element("css selector", ...)`, not the legacy `find_element_by_css_selector(...)`.
221 changes: 221 additions & 0 deletions tests/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,221 @@
# OSBv2 Selenium Test Suite

End-to-end browser tests for the [Open Source Brain v2](https://www.v2dev.opensourcebrain.org/) web portal, built with **Python**, **Selenium 4**, **pytest**, and **Chrome**.

These tests port and expand upon the existing Puppeteer e2e suite in `applications/osb-portal/test/e2e/`. The architecture follows the Page Object Model pattern for readability and maintainability.

---

## Quick Start

```bash
# 1. Create and activate a virtual environment
uv venv .venv --python 3.12
source .venv/bin/activate

# 2. Install test dependencies
uv pip install -r tests/requirements.txt

# 3. Set credentials
cp tests/.env.example tests/.env
# Edit tests/.env with your Keycloak username and password.

# 4. Run the smoke tests (no auth needed)
pytest tests/tests/test_auth.py::TestAuth::test_home_page_loads

# 5. Run all fast tests
pytest tests/ -m smoke -n 2

# 6. Run everything (including slow application-open tests)
pytest tests/ -n 2
```

---

## Configuration

All settings come from environment variables. Create a `tests/.env` file for local development (this file is gitignored).

| Variable | Default | Description |
|---|---|---|
| `APP_URL` | `https://www.v2dev.opensourcebrain.org/` | Instance under test. Must include the trailing slash. |
| `USERNAME` | (empty) | Keycloak username for UI login. |
| `PASSWORD` | (empty) | Keycloak password for UI login. |
| `AUTH_METHOD` | `ui` | `ui` to drive the login form, `cookie` to bypass it. |
| `KC_ACCESS_TOKEN` | (empty) | Pre-obtained JWT token for `AUTH_METHOD=cookie`. |
| `PUPPETEER_DISPLAY` | (unset) | Set to any value to see the browser window (not headless). |
| `CAPTURE_SCREENSHOTS` | `0` | Set to `1` to save screenshots of failures under `tests/screenshots/`. |
| `PARALLEL_WORKERS` | `2` | Number of `pytest-xdist` parallel workers. |

---

## Directory Layout

```
tests/
├── README.md # This file
├── requirements.txt # Python dependencies
├── .env.example # Template for credentials (copy to .env)
├── .gitignore # Ignores .env, caches, screenshots
├── config.py # Configuration loader (env vars → Python)
├── css_selectors.py # CSS selectors (ported from Puppeteer suite + new ones)
├── conftest.py # Pytest fixtures: driver, base_url, authenticated_user
├── pages/ # Page Object Model
│ ├── base_page.py # BasePage: WebDriverWait, click, type, iframe helpers
│ ├── auth_page.py # Keycloak login form
│ ├── home_page.py # Workspaces listing, sidebar, create-workspace dialog
│ ├── workspace_page.py # Workspace detail, open-with, actions menu
│ ├── workspace_open_page.py # Workspace-open page (application iframe)
│ ├── repository_page.py # RepositoriesPage + RepositoryPage (listing & detail)
│ └── user_page.py # User profile, tabs, groups
├── helpers/ # Utility modules
│ ├── auth.py # login(), logout(), ensure_logged_in()
│ └── wait_conditions.py # Re-exports Selenium EC for convenience
└── tests/ # Test cases (one file per feature area)
├── test_auth.py # Home page, login, logout, round-trip
├── test_workspaces.py # Create, view, delete workspaces
├── test_applications.py # Open workspace with NetPyNE/NWB/JupyterLab (slow)
├── test_repositories.py # Repository listing, detail, navigation
├── test_user_profile.py # Profile page, tabs, edit button
└── test_search_filter.py # Search fields, filter popover
```

---

## Test Selection

```bash
# Run everything (2 parallel workers)
pytest tests/ -n 2

# Run a specific file
pytest tests/tests/test_auth.py

# Run a specific test class
pytest tests/tests/test_auth.py::TestAuth

# Run a specific test function
pytest tests/tests/test_auth.py::TestAuth::test_login

# Run smoke-only tests (fast, header-level checks)
pytest tests/ -m smoke

# Skip slow tests (application-open flows need minutes)
pytest tests/ -m "not slow"

# Extra verbose output
pytest tests/ -v -s

# See the browser while it runs
PUPPETEER_DISPLAY=1 pytest tests/tests/test_auth.py
```

---

## Authentication Modes

### UI Login (`AUTH_METHOD=ui`, default)

The test clicks "Sign in" on the portal header, fills the Keycloak `#username` and `#password` fields, and submits. This is the most realistic option.

```bash
USERNAME=myuser PASSWORD=mypass AUTH_METHOD=ui pytest tests/
```

### Cookie Login (`AUTH_METHOD=cookie`)

Bypasses the login form entirely. Faster for iterative development, but requires a valid JWT token. Obtain one from Keycloak or your browser's DevTools (Application → Cookies → `kc-access`).

```bash
KC_ACCESS_TOKEN=eyJhbGci... AUTH_METHOD=cookie pytest tests/
```

---

## Page Object Model

Every page in the portal has a corresponding page object class. They all inherit from `BasePage`, which provides:

| Method | What it does |
|---|---|
| `wait_for(by, value)` | Wait for element to be present in the DOM |
| `wait_for_visible(by, value)` | Wait for element to be visible |
| `wait_for_clickable(by, value)` | Wait for element to be clickable |
| `click(by, value)` | Wait for clickable, then click |
| `type_text(by, value, text)` | Wait for element, optionally clear, then type |
| `switch_to_app_frame()` | Enter the `#workspace-frame` iframe |
| `switch_to_main()` | Return from iframe to main page context |

### Example: Adding a new test

```python
import pytest
from pages.home_page import HomePage

def test_my_new_feature(self, driver, base_url, authenticated_user):
"""Descriptive docstring that explains the flow in steps."""
# Navigate to home
driver.get(base_url)
home = HomePage(driver)
home.wait_for_workspaces_list()

# Interact with the page through the page object
home.click_featured_tab()
cards = home.get_workspace_cards()
assert len(cards) >= 1, "Expected at least one workspace card"

# Page object methods document themselves
# Use selectors.py constants instead of hardcoded strings
from css_selectors import WORKSPACES
assert home.find("css selector", WORKSPACES) is not None
```

---

## Pre-existing Test Data

The test suite assumes certain data already exists on the staging instance:

- **Smoke Test Workspace**: A workspace with `aria-label="Smoke Test Workspace"` on the "Featured" tab, used by the application-open tests.
- **Test user account**: A Keycloak user with at least one workspace. The existing Puppeteer suite uses `simao-osb`.

If these don't exist, the tests may skip or fail. Ask a team member for credentials to the staging test account.

---

## Troubleshooting

### Tests fail with `TimeoutException` on element waits

- Make sure `APP_URL` is correct and the instance is reachable.
- Check that your test user has the expected workspaces/repositories.
- The staging instance may be restarting or under load — retry.
- Increase the timeout value in `config.py` if needed.

### `webdriver-manager` can't find Chrome

Install Chrome or Chromium on your system. If you're running in CI (GitHub Actions, etc.), use the `setup-chrome` action or equivalent.

### "No workspace cards found"

The home page loads differently for authenticated vs. anonymous users. Make sure the `authenticated_user` fixture runs if your test needs private workspaces.

### Application-open tests timeout (10+ minutes)

JupyterHub server spawning is slow. This is expected. Check that the user has no stale servers running by visiting the JupyterHub control panel (`/hub/home`) before running tests.

### Stale servers blocking new sessions

If the user has the maximum number of notebook servers already running, the application-open tests will fail. Visit the JupyterHub hub page and stop/delete old servers manually.

---

## Writing New Tests

1. **Add selectors** to `selectors.py` rather than hardcoding CSS strings in test files.
2. **Use page objects** — add new methods to existing page classes, or create new page classes under `pages/`.
3. **Document flows** — every test function should have a docstring listing its step-by-step flow. Comment non-obvious Selenium interactions.
4. **Mark slow tests** with `@pytest.mark.slow` so they can be filtered out for fast smoke runs.
5. **Be tolerant of missing data** — use `pytest.skip()` rather than hard-failing when optional data isn't present.
6. **Use unique names** for test-created workspaces (e.g. `selenium-test-{uuid}`) so parallel workers don't collide.
7. **Clean up** — test workspace creation tests should also delete created workspaces.
2 changes: 2 additions & 0 deletions tests/__init__.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
# coding: utf-8
# OSBv2 Selenium test suite.
Loading
Loading