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>
This commit is contained in:
145
api/tests/qa/test_qa_responsive.py
Normal file
145
api/tests/qa/test_qa_responsive.py
Normal file
@@ -0,0 +1,145 @@
|
||||
"""
|
||||
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()
|
||||
Reference in New Issue
Block a user