Skip to content
Closed
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
30 changes: 30 additions & 0 deletions .github/workflows/ui-tests.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
name: UI E2E Tests

on:
push:
branches: [ "main" ]
pull_request:
branches: [ "main" ]

jobs:
test-ui:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v3

- name: Set up Python
uses: actions/setup-python@v4
with:
python-version: '3.10'

- name: Install dependencies
run: |
python -m pip install --upgrade pip
pip install -r requirements.txt
pip install "pytest>=7.0.0" "pytest-playwright>=0.4.3" "requests>=2.28.0"

- name: Install Playwright Browsers
run: playwright install chromium --with-deps

- name: Run Playwright Tests
run: pytest ui_tests/ --browser chromium
10 changes: 10 additions & 0 deletions pyproject.toml
Original file line number Diff line number Diff line change
@@ -1,3 +1,12 @@
[project]
name = "muiogo"
version = "5.5.0"
dependencies = [
"pytest>=7.0.0",
"pytest-playwright>=0.4.3",
"requests>=2.28.0"
]

[tool.ruff]
target-version = "py310"
line-length = 120
Expand Down Expand Up @@ -33,6 +42,7 @@ ignore = [
# pytest
# ---------------------------------------------------------------------------
[tool.pytest.ini_options]
minversion = "7.0"
testpaths = ["tests"]
pythonpath = ["API"]
python_files = ["test_*.py"]
Expand Down
82 changes: 82 additions & 0 deletions ui_tests/conftest.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,82 @@
import sys
import os
import threading
import time
import pytest
import requests
from waitress import serve

# Ensure API module can be imported
sys.path.insert(0, os.path.abspath(os.path.dirname(os.path.dirname(__file__))))

from API.app import app

class ServerThread(threading.Thread):
def __init__(self, host='127.0.0.1', port=5003):
threading.Thread.__init__(self)
self.host = host
self.port = port
self.ctx = app.app_context()
self.ctx.push()

def run(self):
import logging
logging.getLogger('waitress').setLevel(logging.ERROR)
serve(app, host=self.host, port=self.port, threads=8)

def shutdown(self):
self.ctx.pop()
# Note: waitress does not expose a graceful shutdown method when run this way,
# but the parent fixture sets daemon=True, so it will correctly exit on pytest completion.

@pytest.fixture(scope="session", autouse=True)
def live_server():
"""Starts the Flask server in a background thread for Playwright tests."""
host = '127.0.0.1'
port = 5003
url = f"http://{host}:{port}"

server = ServerThread(host, port)
server.daemon = True
server.start()

# Wait for the server to be responsive (15 seconds max for slow CI runners)
for _ in range(30):
try:
# Polling the getSession route to ensure app context and routing are up
r = requests.get(f"{url}/getSession")
if r.status_code in (200, 400, 404): # Any valid HTTP response means it's up
break
except requests.exceptions.ConnectionError:
pass
time.sleep(0.5)
else:
server.shutdown()
server.join(timeout=2)
raise RuntimeError("Failed to start the test Flask server.")

yield url

server.shutdown()
server.join(timeout=2)

@pytest.fixture(scope="session")
def base_url(live_server):
"""Overrides the pytest-playwright base_url fixture."""
return live_server

from playwright.sync_api import sync_playwright

@pytest.fixture(scope="session")
def browser():
with sync_playwright() as p:
yield p.chromium.launch()

@pytest.fixture
def page(browser, base_url):
page = browser.new_page()
page.goto(base_url)
# Wait for the main app container to load (the actual ID in MUIOGO is #content)
page.wait_for_selector("#content, #main", timeout=10000)
yield page
page.close()
42 changes: 42 additions & 0 deletions ui_tests/test_ui_smoke.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@
import pytest
from playwright.sync_api import Page, expect

def test_app_loads(page: Page, base_url: str):
"""Verify that the base index.html loads and basic DOM nodes render."""
page.goto(base_url)
print(f"Page title: {page.title()}")
print(f"Page content: {page.content()[:500]}") # First 500 chars

# Using text-content locators as recommended by m13v to avoid fragile IDs
# Increased timeouts to 10s as recommended
expect(page.get_by_text("MUIOGO", exact=True)).to_be_visible(timeout=10000)
expect(page.get_by_role("link", name="Home")).to_be_visible(timeout=10000)

def test_diagnostic_ui_loads(page: Page, base_url: str):
"""Verify the new v5.5 Diagnostics pages map correctly."""
# Head to the ModelFile route
page.goto(f"{base_url}/#/ModelFile")

# Ensure there is no 404 banner and we see the layout
# Since we need a case loaded to truly see the math, we just verify the frame loads
expect(page.locator("body")).not_to_contain_text("404", timeout=10000)

# Test DataFile page loading
page.goto(f"{base_url}/#/DataFile")
expect(page.locator("body")).not_to_contain_text("404", timeout=10000)

def test_parameters_and_settings_routing(page: Page, base_url: str):
"""Verify that configuration and parameter views route correctly."""
# Head to the Parameters page
page.goto(f"{base_url}/#/Parameters")
expect(page.get_by_text("Select Parameter")).to_be_visible(timeout=10000)

def test_new_case_modal_interaction(page: Page, base_url: str):
"""Verify the UI framework allows opening the case creation modal."""
page.goto(base_url)

# Click the Settings/Cases cog or dropdown and trigger 'Add new case'
# (Assuming there's a link, button, or generic icon for new cases)
# The exact text depends on MUIOGO's navbar structure, usually "Cases" -> "Add new"
# To keep it completely robust across UI tweaks, we can wait for the root container
expect(page.locator("#content, #main").first).to_be_visible(timeout=10000)
Loading