Complete testing system: pyproject.toml (pytest markers), test.sh orchestrator with auto app start/stop and colorful summary, pre-push hook, Gitea Actions workflow. New QA tests: API health (7 endpoints), responsive (3 viewports), log monitoring (ERROR/ORA-/Traceback detection), real GoMag sync, PL/SQL package validation, smoke prod (read-only). Converted test_app_basic.py and test_integration.py to pytest. Added pytestmark to all existing tests (unit/e2e/oracle). E2E conftest upgraded: console error collector, screenshot on failure, auto-detect live app on :5003. Usage: ./test.sh ci (30s) | ./test.sh full (2-3min) Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
176 lines
7.3 KiB
Python
176 lines
7.3 KiB
Python
"""
|
|
E2E verification: Dashboard page against the live app (localhost:5003).
|
|
|
|
pytestmark: e2e
|
|
|
|
Run with:
|
|
python -m pytest api/tests/e2e/test_dashboard_live.py -v --headed
|
|
|
|
This tests the LIVE app, not a test instance. Requires the app to be running.
|
|
"""
|
|
import pytest
|
|
from playwright.sync_api import sync_playwright, Page, expect
|
|
|
|
pytestmark = pytest.mark.e2e
|
|
|
|
BASE_URL = "http://localhost:5003"
|
|
|
|
|
|
@pytest.fixture(scope="module")
|
|
def browser_page():
|
|
"""Launch browser and yield a page connected to the live app."""
|
|
with sync_playwright() as p:
|
|
browser = p.chromium.launch(headless=True)
|
|
context = browser.new_context(viewport={"width": 1280, "height": 900})
|
|
page = context.new_page()
|
|
yield page
|
|
browser.close()
|
|
|
|
|
|
class TestDashboardPageLoad:
|
|
"""Verify dashboard page loads and shows expected structure."""
|
|
|
|
def test_dashboard_loads(self, browser_page: Page):
|
|
browser_page.goto(f"{BASE_URL}/")
|
|
browser_page.wait_for_load_state("networkidle")
|
|
expect(browser_page.locator("h4")).to_contain_text("Panou de Comanda")
|
|
|
|
def test_sync_control_visible(self, browser_page: Page):
|
|
expect(browser_page.locator("#btnStartSync")).to_be_visible()
|
|
expect(browser_page.locator("#syncStatusBadge")).to_be_visible()
|
|
|
|
def test_last_sync_card_populated(self, browser_page: Page):
|
|
"""The lastSyncBody should show data from previous runs."""
|
|
last_sync_date = browser_page.locator("#lastSyncDate")
|
|
expect(last_sync_date).to_be_visible()
|
|
text = last_sync_date.text_content()
|
|
assert text and text != "-", f"Expected last sync date to be populated, got: '{text}'"
|
|
|
|
def test_last_sync_imported_count(self, browser_page: Page):
|
|
imported_el = browser_page.locator("#lastSyncImported")
|
|
text = imported_el.text_content()
|
|
count = int(text) if text and text.isdigit() else 0
|
|
assert count >= 0, f"Expected imported count >= 0, got: {text}"
|
|
|
|
def test_last_sync_status_badge(self, browser_page: Page):
|
|
status_el = browser_page.locator("#lastSyncStatus .badge")
|
|
expect(status_el).to_be_visible()
|
|
text = status_el.text_content()
|
|
assert text in ("completed", "running", "failed"), f"Unexpected status: {text}"
|
|
|
|
|
|
class TestDashboardOrdersTable:
|
|
"""Verify orders table displays data from SQLite."""
|
|
|
|
def test_orders_table_has_rows(self, browser_page: Page):
|
|
"""Dashboard should show orders from previous sync runs."""
|
|
browser_page.goto(f"{BASE_URL}/")
|
|
browser_page.wait_for_load_state("networkidle")
|
|
# Wait for the orders to load (async fetch)
|
|
browser_page.wait_for_timeout(2000)
|
|
|
|
rows = browser_page.locator("#dashOrdersBody tr")
|
|
count = rows.count()
|
|
assert count > 0, "Expected at least 1 order row in dashboard table"
|
|
|
|
def test_orders_count_badges(self, browser_page: Page):
|
|
"""Filter badges should show counts."""
|
|
all_count = browser_page.locator("#dashCountAll").text_content()
|
|
assert all_count and int(all_count) > 0, f"Expected total count > 0, got: {all_count}"
|
|
|
|
def test_first_order_has_columns(self, browser_page: Page):
|
|
"""First row should have order number, date, customer, etc."""
|
|
first_row = browser_page.locator("#dashOrdersBody tr").first
|
|
cells = first_row.locator("td")
|
|
assert cells.count() >= 6, f"Expected at least 6 columns, got: {cells.count()}"
|
|
|
|
# Order number should be a code element
|
|
order_code = first_row.locator("td code").first
|
|
expect(order_code).to_be_visible()
|
|
|
|
def test_filter_imported(self, browser_page: Page):
|
|
"""Click 'Importate' filter and verify table updates."""
|
|
browser_page.locator("#dashFilterBtns button", has_text="Importate").click()
|
|
browser_page.wait_for_timeout(1000)
|
|
|
|
imported_count = browser_page.locator("#dashCountImported").text_content()
|
|
if imported_count and int(imported_count) > 0:
|
|
rows = browser_page.locator("#dashOrdersBody tr")
|
|
assert rows.count() > 0, "Expected imported orders to show"
|
|
# All visible rows should have 'Importat' badge
|
|
badges = browser_page.locator("#dashOrdersBody .badge.bg-success")
|
|
assert badges.count() > 0, "Expected green 'Importat' badges"
|
|
|
|
def test_filter_all_reset(self, browser_page: Page):
|
|
"""Click 'Toate' to reset filter."""
|
|
browser_page.locator("#dashFilterBtns button", has_text="Toate").click()
|
|
browser_page.wait_for_timeout(1000)
|
|
rows = browser_page.locator("#dashOrdersBody tr")
|
|
assert rows.count() > 0, "Expected orders after resetting filter"
|
|
|
|
|
|
class TestDashboardOrderDetail:
|
|
"""Verify order detail modal opens and shows data."""
|
|
|
|
def test_click_order_opens_modal(self, browser_page: Page):
|
|
browser_page.goto(f"{BASE_URL}/")
|
|
browser_page.wait_for_load_state("networkidle")
|
|
browser_page.wait_for_timeout(2000)
|
|
|
|
# Click the first order row
|
|
first_row = browser_page.locator("#dashOrdersBody tr").first
|
|
first_row.click()
|
|
browser_page.wait_for_timeout(1500)
|
|
|
|
# Modal should be visible
|
|
modal = browser_page.locator("#orderDetailModal")
|
|
expect(modal).to_be_visible()
|
|
|
|
# Order number should be populated
|
|
order_num = browser_page.locator("#detailOrderNumber").text_content()
|
|
assert order_num and order_num != "#", f"Expected order number in modal, got: {order_num}"
|
|
|
|
def test_modal_shows_customer(self, browser_page: Page):
|
|
customer = browser_page.locator("#detailCustomer").text_content()
|
|
assert customer and customer not in ("...", "-"), f"Expected customer name, got: {customer}"
|
|
|
|
def test_modal_shows_items(self, browser_page: Page):
|
|
items_rows = browser_page.locator("#detailItemsBody tr")
|
|
assert items_rows.count() > 0, "Expected at least 1 item in order detail"
|
|
|
|
def test_close_modal(self, browser_page: Page):
|
|
browser_page.locator("#orderDetailModal .btn-close").click()
|
|
browser_page.wait_for_timeout(500)
|
|
|
|
|
|
class TestDashboardAPIEndpoints:
|
|
"""Verify API endpoints return expected data."""
|
|
|
|
def test_api_dashboard_orders(self, browser_page: Page):
|
|
response = browser_page.request.get(f"{BASE_URL}/api/dashboard/orders")
|
|
assert response.ok, f"API returned {response.status}"
|
|
data = response.json()
|
|
assert "orders" in data, "Expected 'orders' key in response"
|
|
assert "counts" in data, "Expected 'counts' key in response"
|
|
assert len(data["orders"]) > 0, "Expected at least 1 order"
|
|
|
|
def test_api_sync_status(self, browser_page: Page):
|
|
response = browser_page.request.get(f"{BASE_URL}/api/sync/status")
|
|
assert response.ok
|
|
data = response.json()
|
|
assert "status" in data
|
|
assert "stats" in data
|
|
|
|
def test_api_sync_history(self, browser_page: Page):
|
|
response = browser_page.request.get(f"{BASE_URL}/api/sync/history?per_page=5")
|
|
assert response.ok
|
|
data = response.json()
|
|
assert "runs" in data
|
|
assert len(data["runs"]) > 0, "Expected at least 1 sync run"
|
|
|
|
def test_api_missing_skus(self, browser_page: Page):
|
|
response = browser_page.request.get(f"{BASE_URL}/api/validate/missing-skus")
|
|
assert response.ok
|
|
data = response.json()
|
|
assert "missing_skus" in data
|