Spaces:
Running
Running
| """ | |
| Pytest configuration and shared fixtures for DemoPrep E2E tests. | |
| """ | |
| import os | |
| import time | |
| import pytest | |
| import requests | |
| from pathlib import Path | |
| from dotenv import load_dotenv | |
| from playwright.sync_api import Page, BrowserContext, Browser, expect | |
| load_dotenv(os.path.join(os.path.dirname(__file__), '..', '.env')) | |
| BASE_URL = "https://thoughtspot-dp-demoprep.hf.space" | |
| TEST_USER = os.getenv("TEST_USER") | |
| TEST_PASSWORD = os.getenv("TEST_PASSWORD") | |
| if not TEST_USER or not TEST_PASSWORD: | |
| raise EnvironmentError("TEST_USER and TEST_PASSWORD must be set in .env") | |
| def _wake_space(max_wait: int = 180) -> None: | |
| """ | |
| Ping HF Spaces until it responds with HTTP 200. | |
| HF free-tier spaces go to sleep after inactivity and take 30-60s to wake. | |
| Raises if the space doesn't wake within max_wait seconds. | |
| """ | |
| deadline = time.time() + max_wait | |
| last_status = None | |
| while time.time() < deadline: | |
| try: | |
| r = requests.get(BASE_URL, timeout=10, allow_redirects=True) | |
| last_status = r.status_code | |
| if r.status_code == 200: | |
| return | |
| except requests.RequestException: | |
| pass | |
| time.sleep(5) | |
| raise RuntimeError( | |
| f"HF Space did not wake within {max_wait}s (last HTTP status: {last_status})" | |
| ) | |
| def _do_login(page: Page) -> None: | |
| """Perform the login flow on a page.""" | |
| page.goto(BASE_URL, timeout=90000) | |
| page.wait_for_selector('input[placeholder="Type here..."]', timeout=90000) | |
| page.fill('input[type=text]', TEST_USER) | |
| page.fill('input[type=password]', TEST_PASSWORD) | |
| page.click('button:has-text("Login")') | |
| page.wait_for_selector('.gradio-container', timeout=90000) | |
| page.wait_for_timeout(3000) # allow Gradio JS to settle | |
| def login(page: Page) -> None: | |
| """Log in to the app with test credentials (for tests that need a fresh login).""" | |
| _do_login(page) | |
| def wake_space(request): | |
| """Wake the HF Space before any tests run. Blocks until it's responsive.""" | |
| e2e_files = { | |
| "e2e_chat.py", | |
| "e2e_settings.py", | |
| "e2e_smoke.py", | |
| "e2e_z_auth.py", | |
| "test_mcp_liveboard.py", | |
| } | |
| collected_files = {Path(str(item.fspath)).name for item in request.session.items} | |
| if not collected_files.intersection(e2e_files): | |
| return | |
| print("\n⏳ Waiting for HF Space to be ready...", flush=True) | |
| _wake_space(max_wait=180) | |
| print("✅ HF Space is up.", flush=True) | |
| def auth_context(browser: Browser, tmp_path_factory): | |
| """ | |
| Session-scoped authenticated browser context. | |
| Logs in once, saves storage state, and yields a persistent context. | |
| All logged_in_page fixtures share this context, avoiding repeated expensive | |
| logins and connection setup against HF Spaces. | |
| """ | |
| state_file = str(tmp_path_factory.mktemp("auth") / "state.json") | |
| # Log in on a temp page to capture auth state | |
| setup_ctx = browser.new_context(viewport={"width": 1280, "height": 900}) | |
| setup_page = setup_ctx.new_page() | |
| _do_login(setup_page) | |
| setup_ctx.storage_state(path=state_file) | |
| setup_page.close() | |
| setup_ctx.close() | |
| # Create the long-lived context with saved auth state | |
| ctx = browser.new_context( | |
| storage_state=state_file, | |
| viewport={"width": 1280, "height": 900}, | |
| ) | |
| yield ctx | |
| ctx.close() | |
| def logged_in_page(auth_context: BrowserContext): | |
| """ | |
| Function-scoped: opens a fresh page in the shared authenticated context. | |
| Each test gets an isolated page (own URL, own Gradio session) without | |
| the overhead of a full login or new browser context. | |
| """ | |
| page = auth_context.new_page() | |
| for attempt in range(2): | |
| try: | |
| page.goto(BASE_URL, timeout=120000) | |
| # Wait for tabs — the minimum signal that Gradio has initialized. | |
| # Individual tests wait for their own specific elements. | |
| page.wait_for_selector('button[role=tab]', timeout=120000) | |
| page.wait_for_timeout(3000) | |
| break | |
| except Exception: | |
| if attempt == 1: | |
| raise | |
| page.wait_for_timeout(10000) | |
| yield page | |
| page.close() | |
| def get_chat_input(page: Page): | |
| """Return the main chat message input field.""" | |
| return page.locator('input[placeholder*="Amazon.com"]') | |
| # Alias for backwards compatibility | |
| def get_chat_textarea(page: Page): | |
| return get_chat_input(page) | |
| def wait_for_response(page: Page, timeout_ms: int = 90000) -> None: | |
| """Wait until the assistant has finished responding.""" | |
| send_btn = page.locator('button:has-text("Send")') | |
| expect(send_btn).to_be_enabled(timeout=timeout_ms) | |
| page.wait_for_timeout(500) | |