1
0
This repository has been archived on 2026-03-24. You can view files and clone it. You cannot open issues or pull requests or push a commit.
Files
Timmy-time-dashboard/tests/functional/test_ui_selenium.py

295 lines
11 KiB
Python

"""Selenium UI tests — green-path smoke tests for the dashboard.
Requires:
- Dashboard running at http://localhost:8000 (make up DEV=1)
- Chrome installed (headless mode, no display needed)
- selenium pip package
Run:
SELENIUM_UI=1 pytest tests/functional/test_ui_selenium.py -v
"""
import os
import pytest
try:
from selenium import webdriver
from selenium.webdriver.chrome.options import Options
from selenium.webdriver.common.by import By
from selenium.webdriver.common.keys import Keys
from selenium.webdriver.support import expected_conditions as EC
from selenium.webdriver.support.ui import WebDriverWait
HAS_SELENIUM = True
except ImportError:
HAS_SELENIUM = False
# Skip entire module unless SELENIUM_UI=1 is set and Selenium is installed
pytestmark = pytest.mark.skipif(
not HAS_SELENIUM or os.environ.get("SELENIUM_UI") != "1",
reason="Selenium not installed or SELENIUM_UI not set to 1",
)
DASHBOARD_URL = os.environ.get("DASHBOARD_URL", "http://localhost:8000")
@pytest.fixture(scope="module")
def driver():
"""Headless Chrome WebDriver, shared across tests in this module."""
opts = Options()
opts.add_argument("--headless=new")
opts.add_argument("--no-sandbox")
opts.add_argument("--disable-dev-shm-usage")
opts.add_argument("--disable-gpu")
opts.add_argument("--window-size=1280,900")
d = webdriver.Chrome(options=opts)
d.implicitly_wait(5)
yield d
d.quit()
@pytest.fixture(autouse=True)
def _check_dashboard():
"""Skip all tests if the dashboard isn't reachable."""
import httpx
try:
r = httpx.get(f"{DASHBOARD_URL}/health", timeout=5)
if r.status_code != 200:
pytest.skip("Dashboard not healthy")
except Exception:
pytest.skip("Dashboard not reachable at " + DASHBOARD_URL)
def _load_dashboard(driver):
"""Navigate to dashboard and wait for Timmy panel to load."""
driver.get(DASHBOARD_URL)
WebDriverWait(driver, 15).until(
EC.presence_of_element_located((By.XPATH, "//*[contains(text(), 'TIMMY INTERFACE')]"))
)
def _wait_for_sidebar(driver):
"""Wait for the agent sidebar to finish its HTMX load."""
WebDriverWait(driver, 15).until(
EC.presence_of_element_located((By.XPATH, "//*[contains(text(), 'SWARM AGENTS')]"))
)
def _has_registered_agents(driver):
"""Check if there are any registered agent cards in the sidebar."""
cards = driver.find_elements(By.CSS_SELECTOR, ".mc-agent-card")
return len(cards) > 0
def _send_chat_and_wait(driver, message):
"""Send a chat message and wait for the NEW agent response.
Returns the number of agent messages before and after sending.
"""
existing = len(driver.find_elements(By.CSS_SELECTOR, ".chat-message.agent"))
inp = driver.find_element(By.CSS_SELECTOR, "input[name='message']")
inp.send_keys(message)
inp.send_keys(Keys.RETURN)
# Wait for a NEW agent response (not one from a prior test)
WebDriverWait(driver, 30).until(
lambda d: len(d.find_elements(By.CSS_SELECTOR, ".chat-message.agent")) > existing
)
return existing
# ── Page load tests ─────────────────────────────────────────────────────────
class TestPageLoad:
"""Dashboard loads and shows expected structure."""
def test_homepage_loads(self, driver):
driver.get(DASHBOARD_URL)
assert driver.title != ""
def test_header_visible(self, driver):
_load_dashboard(driver)
header = driver.find_element(By.CSS_SELECTOR, ".mc-header, header, nav")
assert header.is_displayed()
def test_sidebar_loads(self, driver):
_load_dashboard(driver)
_wait_for_sidebar(driver)
def test_timmy_panel_loads(self, driver):
_load_dashboard(driver)
def test_chat_input_exists(self, driver):
_load_dashboard(driver)
inp = driver.find_element(By.CSS_SELECTOR, "input[name='message']")
assert inp.is_displayed()
assert "timmy" in inp.get_attribute("placeholder").lower()
def test_send_button_exists(self, driver):
_load_dashboard(driver)
btn = driver.find_element(By.CSS_SELECTOR, "button.mc-btn-send")
assert btn.is_displayed()
assert "SEND" in btn.text
def test_health_panel_loads(self, driver):
_load_dashboard(driver)
WebDriverWait(driver, 10).until(
EC.presence_of_element_located((By.XPATH, "//*[contains(text(), 'SYSTEM HEALTH')]"))
)
# ── Chat interaction tests ──────────────────────────────────────────────────
class TestChatInteraction:
"""Send a single message and verify all chat-related behaviors at once.
We only send ONE message to avoid spamming Ollama and crashing the browser.
"""
def test_chat_roundtrip(self, driver):
"""Full chat roundtrip: send message, get response, input clears, chat scrolls."""
_load_dashboard(driver)
# Wait for page to be ready
WebDriverWait(driver, 10).until(
lambda d: d.execute_script("return document.readyState") == "complete"
)
existing_agents = len(driver.find_elements(By.CSS_SELECTOR, ".chat-message.agent"))
inp = driver.find_element(By.CSS_SELECTOR, "input[name='message']")
inp.send_keys("hello from selenium")
inp.send_keys(Keys.RETURN)
# 1. User bubble appears immediately
WebDriverWait(driver, 5).until(
EC.presence_of_element_located((By.CSS_SELECTOR, ".chat-message.user"))
)
# 2. Agent response arrives
WebDriverWait(driver, 30).until(
lambda d: len(d.find_elements(By.CSS_SELECTOR, ".chat-message.agent")) > existing_agents
)
# 3. Input cleared (regression test)
# Already waited for agent response via WebDriverWait above
inp = driver.find_element(By.CSS_SELECTOR, "input[name='message']")
assert inp.get_attribute("value") == "", "Input should be empty after sending"
# 4. Chat scrolled to bottom (regression test)
chat_log = driver.find_element(By.ID, "chat-log")
scroll_top = driver.execute_script("return arguments[0].scrollTop", chat_log)
scroll_height = driver.execute_script("return arguments[0].scrollHeight", chat_log)
client_height = driver.execute_script("return arguments[0].clientHeight", chat_log)
if scroll_height > client_height:
gap = scroll_height - scroll_top - client_height
assert gap < 50, f"Chat not scrolled to bottom (gap: {gap}px)"
# ── Task panel tests ────────────────────────────────────────────────────────
class TestTaskPanel:
"""Task creation panel works correctly."""
def test_task_panel_via_url(self, driver):
"""Task panel loads correctly when navigated to directly."""
driver.get(f"{DASHBOARD_URL}/swarm/tasks/panel")
WebDriverWait(driver, 10).until(
EC.presence_of_element_located((By.XPATH, "//*[contains(text(), 'CREATE TASK')]"))
)
def test_task_panel_has_form(self, driver):
"""Task creation panel has description and agent fields."""
driver.get(f"{DASHBOARD_URL}/swarm/tasks/panel")
WebDriverWait(driver, 10).until(
EC.presence_of_element_located((By.XPATH, "//*[contains(text(), 'CREATE TASK')]"))
)
driver.find_element(By.CSS_SELECTOR, "textarea[name='description']")
driver.find_element(By.CSS_SELECTOR, "select[name='agent_id']")
def test_task_button_on_agent_card(self, driver):
"""If agents are registered, TASK button on agent card opens task panel."""
_load_dashboard(driver)
_wait_for_sidebar(driver)
if not _has_registered_agents(driver):
pytest.skip("No agents registered — TASK button not available")
task_btn = driver.find_element(
By.XPATH,
"//div[contains(@class, 'mc-agent-card')]//button[contains(text(), 'TASK')]",
)
task_btn.click()
WebDriverWait(driver, 10).until(
EC.presence_of_element_located((By.XPATH, "//*[contains(text(), 'CREATE TASK')]"))
)
# ── Agent sidebar tests ─────────────────────────────────────────────────────
class TestAgentSidebar:
"""Agent sidebar displays correctly."""
def test_sidebar_header_shows(self, driver):
_load_dashboard(driver)
_wait_for_sidebar(driver)
header = driver.find_element(By.XPATH, "//*[contains(text(), 'SWARM AGENTS')]")
assert header.is_displayed()
def test_sidebar_shows_status_when_agents_exist(self, driver):
"""If agents are registered, cards show status dots."""
_load_dashboard(driver)
_wait_for_sidebar(driver)
if not _has_registered_agents(driver):
pytest.skip("No agents registered — skipping card test")
cards = driver.find_elements(By.CSS_SELECTOR, ".mc-agent-card")
for card in cards:
dots = card.find_elements(By.CSS_SELECTOR, ".status-dot")
assert len(dots) >= 1, "Agent card should show a status dot"
def test_no_agents_fallback(self, driver):
"""When no agents registered, sidebar shows fallback message."""
_load_dashboard(driver)
_wait_for_sidebar(driver)
if _has_registered_agents(driver):
pytest.skip("Agents are registered — fallback not shown")
body = driver.find_element(By.CSS_SELECTOR, ".mc-sidebar").text
assert "NO AGENTS REGISTERED" in body
# ── Navigation tests ────────────────────────────────────────────────────────
class TestNavigation:
"""Basic navigation flows work end-to-end."""
def test_clear_chat_button(self, driver):
_load_dashboard(driver)
clear_btn = driver.find_element(By.XPATH, "//button[contains(text(), 'CLEAR')]")
assert clear_btn.is_displayed()
def test_health_endpoint_returns_200(self, driver):
driver.get(f"{DASHBOARD_URL}/health")
assert "ok" in driver.page_source
def test_nav_links_visible(self, driver):
_load_dashboard(driver)
links = driver.find_elements(By.CSS_SELECTOR, ".mc-desktop-nav .mc-test-link")
assert len(links) >= 3, "Navigation should have multiple links"