2025-09-09 18:15:30 -07:00
|
|
|
# Playwright End-to-End Test Suite (M3: Cypress/Playwright Smoke Tests)
|
|
|
|
# Simple smoke tests for the MTG Deckbuilder web UI
|
|
|
|
# Tests critical user flows: deck creation, include/exclude, fuzzy matching
|
|
|
|
|
|
|
|
import asyncio
|
|
|
|
import pytest
|
|
|
|
from playwright.async_api import async_playwright, Page, Browser, BrowserContext
|
|
|
|
import os
|
|
|
|
|
|
|
|
class TestConfig:
|
|
|
|
"""Test configuration"""
|
2025-09-09 20:18:03 -07:00
|
|
|
BASE_URL = os.getenv('TEST_BASE_URL', 'http://localhost:8080')
|
2025-09-09 18:15:30 -07:00
|
|
|
TIMEOUT = 30000 # 30 seconds
|
|
|
|
|
|
|
|
# Test data
|
|
|
|
COMMANDER_NAME = "Alania, Divergent Storm"
|
|
|
|
INCLUDE_CARDS = ["Sol Ring", "Lightning Bolt"]
|
|
|
|
EXCLUDE_CARDS = ["Mana Crypt", "Force of Will"]
|
|
|
|
|
|
|
|
@pytest.fixture(scope="session")
|
|
|
|
async def browser():
|
|
|
|
"""Browser fixture for all tests"""
|
|
|
|
async with async_playwright() as p:
|
|
|
|
browser = await p.chromium.launch(headless=True)
|
|
|
|
yield browser
|
|
|
|
await browser.close()
|
|
|
|
|
|
|
|
@pytest.fixture
|
|
|
|
async def context(browser: Browser):
|
|
|
|
"""Browser context fixture"""
|
|
|
|
context = await browser.new_context(
|
|
|
|
viewport={"width": 1280, "height": 720},
|
|
|
|
user_agent="Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36"
|
|
|
|
)
|
|
|
|
yield context
|
|
|
|
await context.close()
|
|
|
|
|
|
|
|
@pytest.fixture
|
|
|
|
async def page(context: BrowserContext):
|
|
|
|
"""Page fixture"""
|
|
|
|
page = await context.new_page()
|
|
|
|
yield page
|
|
|
|
await page.close()
|
|
|
|
|
|
|
|
class TestWebUISmoke:
|
|
|
|
"""Smoke tests for web UI functionality"""
|
|
|
|
|
|
|
|
async def test_homepage_loads(self, page: Page):
|
|
|
|
"""Test that the homepage loads successfully"""
|
|
|
|
await page.goto(TestConfig.BASE_URL)
|
|
|
|
await page.wait_for_load_state('networkidle')
|
|
|
|
|
|
|
|
# Check for key elements
|
|
|
|
assert await page.is_visible("h1, h2")
|
|
|
|
assert await page.locator("button, .btn").count() > 0
|
|
|
|
|
|
|
|
async def test_build_page_loads(self, page: Page):
|
|
|
|
"""Test that the build page loads"""
|
|
|
|
await page.goto(f"{TestConfig.BASE_URL}/build")
|
|
|
|
await page.wait_for_load_state('networkidle')
|
|
|
|
|
|
|
|
# Check for build elements
|
|
|
|
assert await page.is_visible("text=Build a Deck")
|
|
|
|
assert await page.is_visible("button:has-text('Build a New Deck')")
|
|
|
|
|
|
|
|
async def test_new_deck_modal_opens(self, page: Page):
|
|
|
|
"""Test that the new deck modal opens correctly"""
|
|
|
|
await page.goto(f"{TestConfig.BASE_URL}/build")
|
|
|
|
await page.wait_for_load_state('networkidle')
|
|
|
|
|
|
|
|
# Click new deck button
|
|
|
|
await page.click("button:has-text('Build a New Deck')")
|
|
|
|
await page.wait_for_timeout(1000) # Wait for modal animation
|
|
|
|
|
|
|
|
# Check modal is visible
|
|
|
|
modal_locator = page.locator('.modal-content')
|
|
|
|
await modal_locator.wait_for(state='visible', timeout=TestConfig.TIMEOUT)
|
|
|
|
|
|
|
|
# Check for modal contents
|
|
|
|
assert await page.is_visible("text=Commander")
|
|
|
|
assert await page.is_visible("input[name='commander']")
|
|
|
|
|
|
|
|
async def test_commander_search(self, page: Page):
|
|
|
|
"""Test commander search functionality"""
|
|
|
|
await page.goto(f"{TestConfig.BASE_URL}/build")
|
|
|
|
await page.wait_for_load_state('networkidle')
|
|
|
|
|
|
|
|
# Open new deck modal
|
|
|
|
await page.click("button:has-text('Build a New Deck')")
|
|
|
|
await page.wait_for_selector('.modal-content')
|
|
|
|
|
|
|
|
# Enter commander name
|
|
|
|
commander_input = page.locator("input[name='commander']")
|
|
|
|
await commander_input.fill(TestConfig.COMMANDER_NAME)
|
|
|
|
await page.wait_for_timeout(500)
|
|
|
|
|
|
|
|
# Look for search results or feedback
|
|
|
|
# This depends on the exact implementation
|
|
|
|
# Check if commander search worked (could be immediate or require button click)
|
|
|
|
|
|
|
|
async def test_include_exclude_fields_exist(self, page: Page):
|
|
|
|
"""Test that include/exclude fields are present in the form"""
|
|
|
|
await page.goto(f"{TestConfig.BASE_URL}/build")
|
|
|
|
await page.wait_for_load_state('networkidle')
|
|
|
|
|
|
|
|
# Open new deck modal
|
|
|
|
await page.click("button:has-text('Build a New Deck')")
|
|
|
|
await page.wait_for_selector('.modal-content')
|
|
|
|
|
|
|
|
# Check include/exclude sections exist
|
|
|
|
assert await page.is_visible("text=Include") or await page.is_visible("text=Must Include")
|
|
|
|
assert await page.is_visible("text=Exclude") or await page.is_visible("text=Must Exclude")
|
|
|
|
|
|
|
|
# Check for textareas
|
|
|
|
assert await page.locator("textarea[name='include_cards'], #include_cards_textarea").count() > 0
|
|
|
|
assert await page.locator("textarea[name='exclude_cards'], #exclude_cards_textarea").count() > 0
|
|
|
|
|
|
|
|
async def test_include_exclude_validation(self, page: Page):
|
|
|
|
"""Test include/exclude validation feedback"""
|
|
|
|
await page.goto(f"{TestConfig.BASE_URL}/build")
|
|
|
|
await page.wait_for_load_state('networkidle')
|
|
|
|
|
|
|
|
# Open new deck modal
|
|
|
|
await page.click("button:has-text('Build a New Deck')")
|
|
|
|
await page.wait_for_selector('.modal-content')
|
|
|
|
|
|
|
|
# Fill include cards
|
|
|
|
include_textarea = page.locator("textarea[name='include_cards'], #include_cards_textarea").first
|
|
|
|
if await include_textarea.count() > 0:
|
|
|
|
await include_textarea.fill("\\n".join(TestConfig.INCLUDE_CARDS))
|
|
|
|
await page.wait_for_timeout(500)
|
|
|
|
|
|
|
|
# Look for validation feedback (chips, badges, etc.)
|
|
|
|
# Check if cards are being validated
|
|
|
|
|
|
|
|
# Fill exclude cards
|
|
|
|
exclude_textarea = page.locator("textarea[name='exclude_cards'], #exclude_cards_textarea").first
|
|
|
|
if await exclude_textarea.count() > 0:
|
|
|
|
await exclude_textarea.fill("\\n".join(TestConfig.EXCLUDE_CARDS))
|
|
|
|
await page.wait_for_timeout(500)
|
|
|
|
|
|
|
|
async def test_fuzzy_matching_modal_can_open(self, page: Page):
|
|
|
|
"""Test that fuzzy matching modal can be triggered (if conditions are met)"""
|
|
|
|
await page.goto(f"{TestConfig.BASE_URL}/build")
|
|
|
|
await page.wait_for_load_state('networkidle')
|
|
|
|
|
|
|
|
# Open new deck modal
|
|
|
|
await page.click("button:has-text('Build a New Deck')")
|
|
|
|
await page.wait_for_selector('.modal-content')
|
|
|
|
|
|
|
|
# Fill in a slightly misspelled card name to potentially trigger fuzzy matching
|
|
|
|
include_textarea = page.locator("textarea[name='include_cards'], #include_cards_textarea").first
|
|
|
|
if await include_textarea.count() > 0:
|
|
|
|
await include_textarea.fill("Lightning Boltt") # Intentional typo
|
|
|
|
await page.wait_for_timeout(1000)
|
|
|
|
|
|
|
|
# Try to proceed (this would depend on the exact flow)
|
|
|
|
# The fuzzy modal should only appear when validation runs
|
|
|
|
|
|
|
|
async def test_mobile_responsive_layout(self, page: Page):
|
|
|
|
"""Test mobile responsive layout"""
|
|
|
|
# Set mobile viewport
|
|
|
|
await page.set_viewport_size({"width": 375, "height": 667})
|
|
|
|
|
|
|
|
await page.goto(f"{TestConfig.BASE_URL}/build")
|
|
|
|
await page.wait_for_load_state('networkidle')
|
|
|
|
|
|
|
|
# Check that elements are still visible and usable on mobile
|
|
|
|
assert await page.is_visible("text=Build a Deck")
|
|
|
|
|
|
|
|
# Open modal
|
|
|
|
await page.click("button:has-text('Build a New Deck')")
|
|
|
|
await page.wait_for_selector('.modal-content')
|
|
|
|
|
|
|
|
# Check modal is responsive
|
|
|
|
modal = page.locator('.modal-content')
|
|
|
|
modal_box = await modal.bounding_box()
|
|
|
|
|
|
|
|
if modal_box:
|
|
|
|
# Modal should fit within mobile viewport with some margin
|
|
|
|
assert modal_box['width'] <= 375 - 20 # Allow 10px margin on each side
|
|
|
|
|
|
|
|
async def test_configs_page_loads(self, page: Page):
|
|
|
|
"""Test that the configs page loads"""
|
|
|
|
await page.goto(f"{TestConfig.BASE_URL}/configs")
|
|
|
|
await page.wait_for_load_state('networkidle')
|
|
|
|
|
|
|
|
# Check for config page elements
|
|
|
|
assert await page.is_visible("text=Build from JSON") or await page.is_visible("text=Configuration")
|
|
|
|
|
|
|
|
class TestWebUIFull:
|
|
|
|
"""More comprehensive tests (optional, slower)"""
|
|
|
|
|
|
|
|
async def test_full_deck_creation_flow(self, page: Page):
|
|
|
|
"""Test complete deck creation flow (if server is running)"""
|
|
|
|
# This would test the complete flow but requires a running server
|
|
|
|
# and would be much slower
|
|
|
|
pass
|
|
|
|
|
|
|
|
async def test_include_exclude_end_to_end(self, page: Page):
|
|
|
|
"""Test include/exclude functionality end-to-end"""
|
|
|
|
# This would test the complete include/exclude flow
|
|
|
|
# including fuzzy matching and result display
|
|
|
|
pass
|
|
|
|
|
|
|
|
# Helper functions for running tests
|
|
|
|
async def run_smoke_tests():
|
|
|
|
"""Run all smoke tests"""
|
|
|
|
print("Starting MTG Deckbuilder Web UI Smoke Tests...")
|
|
|
|
|
|
|
|
async with async_playwright() as p:
|
|
|
|
browser = await p.chromium.launch(headless=True)
|
|
|
|
context = await browser.new_context()
|
|
|
|
page = await context.new_page()
|
|
|
|
|
|
|
|
try:
|
|
|
|
# Basic connectivity test
|
|
|
|
await page.goto(TestConfig.BASE_URL, timeout=TestConfig.TIMEOUT)
|
|
|
|
print("✓ Server is reachable")
|
|
|
|
|
|
|
|
# Run individual test methods
|
|
|
|
test_instance = TestWebUISmoke()
|
|
|
|
|
|
|
|
await test_instance.test_homepage_loads(page)
|
|
|
|
print("✓ Homepage loads")
|
|
|
|
|
|
|
|
await test_instance.test_build_page_loads(page)
|
|
|
|
print("✓ Build page loads")
|
|
|
|
|
|
|
|
await test_instance.test_new_deck_modal_opens(page)
|
|
|
|
print("✓ New deck modal opens")
|
|
|
|
|
|
|
|
await test_instance.test_include_exclude_fields_exist(page)
|
|
|
|
print("✓ Include/exclude fields exist")
|
|
|
|
|
|
|
|
await test_instance.test_mobile_responsive_layout(page)
|
|
|
|
print("✓ Mobile responsive layout works")
|
|
|
|
|
|
|
|
await test_instance.test_configs_page_loads(page)
|
|
|
|
print("✓ Configs page loads")
|
|
|
|
|
|
|
|
print("\\n🎉 All smoke tests passed!")
|
|
|
|
|
|
|
|
except Exception as e:
|
|
|
|
print(f"❌ Test failed: {e}")
|
|
|
|
raise
|
|
|
|
|
|
|
|
finally:
|
|
|
|
await browser.close()
|
|
|
|
|
|
|
|
if __name__ == "__main__":
|
|
|
|
asyncio.run(run_smoke_tests())
|