Files
gomag-vending/api/tests/qa/test_qa_responsive.py
Claude Agent 419464a62c feat: add CI/CD testing infrastructure with test.sh orchestrator
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>
2026-03-24 10:40:25 +00:00

146 lines
5.5 KiB
Python

"""
Responsive layout tests across 3 viewports.
Tests each page on desktop / tablet / mobile using Playwright sync API.
"""
import pytest
from pathlib import Path
from playwright.sync_api import sync_playwright, expect
pytestmark = pytest.mark.qa
# ---------------------------------------------------------------------------
# Viewport definitions
# ---------------------------------------------------------------------------
VIEWPORTS = {
"desktop": {"width": 1280, "height": 900},
"tablet": {"width": 768, "height": 1024},
"mobile": {"width": 375, "height": 812},
}
# ---------------------------------------------------------------------------
# Pages to test: (path, expected_text_fragment)
# expected_text_fragment is matched loosely against page title or any <h4>/<h1>
# ---------------------------------------------------------------------------
PAGES = [
("/", "Panou"),
("/logs", "Jurnale"),
("/mappings", "Mapari"),
("/missing-skus", "SKU"),
("/settings", "Setari"),
]
# ---------------------------------------------------------------------------
# Session-scoped browser (reused across all parametrized tests)
# ---------------------------------------------------------------------------
@pytest.fixture(scope="session")
def pw_browser():
"""Launch a Chromium browser for the full QA session."""
with sync_playwright() as pw:
browser = pw.chromium.launch(headless=True)
yield browser
browser.close()
# ---------------------------------------------------------------------------
# Parametrized test: viewport x page
# ---------------------------------------------------------------------------
@pytest.mark.parametrize("viewport_name", list(VIEWPORTS.keys()))
@pytest.mark.parametrize("page_path,expected_text", PAGES)
def test_responsive_page(
pw_browser,
base_url: str,
screenshots_dir: Path,
viewport_name: str,
page_path: str,
expected_text: str,
):
"""Each page renders without error on every viewport and contains expected text."""
viewport = VIEWPORTS[viewport_name]
context = pw_browser.new_context(viewport=viewport)
page = context.new_page()
try:
page.goto(f"{base_url}{page_path}", wait_until="networkidle", timeout=15_000)
# Screenshot
page_name = page_path.strip("/") or "dashboard"
screenshot_path = screenshots_dir / f"{page_name}-{viewport_name}.png"
page.screenshot(path=str(screenshot_path), full_page=True)
# Basic content check: title or any h1/h4 contains expected text
title = page.title()
headings = page.locator("h1, h4").all_text_contents()
all_text = " ".join([title] + headings)
assert expected_text.lower() in all_text.lower(), (
f"Expected '{expected_text}' in page text on {viewport_name} {page_path}. "
f"Got title='{title}', headings={headings}"
)
finally:
context.close()
# ---------------------------------------------------------------------------
# Mobile-specific: navbar toggler
# ---------------------------------------------------------------------------
def test_mobile_navbar_visible(pw_browser, base_url: str):
"""Mobile viewport: navbar should still be visible and functional."""
context = pw_browser.new_context(viewport=VIEWPORTS["mobile"])
page = context.new_page()
try:
page.goto(base_url, wait_until="networkidle", timeout=15_000)
# Custom navbar: .top-navbar with .navbar-brand
navbar = page.locator(".top-navbar")
expect(navbar).to_be_visible()
finally:
context.close()
# ---------------------------------------------------------------------------
# Mobile-specific: tables wrapped in .table-responsive or scrollable
# ---------------------------------------------------------------------------
@pytest.mark.parametrize("page_path", ["/logs", "/mappings", "/missing-skus"])
def test_mobile_table_responsive(pw_browser, base_url: str, page_path: str):
"""
On mobile, any <table> should live inside a .table-responsive wrapper
OR the page should have a horizontal scroll container around it.
If no table is present (empty state), the test is skipped.
"""
context = pw_browser.new_context(viewport=VIEWPORTS["mobile"])
page = context.new_page()
try:
page.goto(f"{base_url}{page_path}", wait_until="networkidle", timeout=15_000)
tables = page.locator("table").all()
if not tables:
pytest.skip(f"No tables on {page_path} (empty state)")
# Check each table has an ancestor with overflow-x scroll or .table-responsive class
for table in tables:
# Check direct parent chain for .table-responsive
wrapped = page.evaluate(
"""(el) => {
let node = el.parentElement;
for (let i = 0; i < 6 && node; i++) {
if (node.classList.contains('table-responsive')) return true;
const style = window.getComputedStyle(node);
if (style.overflowX === 'auto' || style.overflowX === 'scroll') return true;
node = node.parentElement;
}
return false;
}""",
table.element_handle(),
)
assert wrapped, (
f"Table on {page_path} is not inside a .table-responsive wrapper "
f"or overflow-x:auto/scroll container on mobile viewport"
)
finally:
context.close()