diff --git a/tests/test_frontend_assets.py b/tests/test_frontend_assets.py new file mode 100644 index 0000000..292d819 --- /dev/null +++ b/tests/test_frontend_assets.py @@ -0,0 +1,177 @@ +"""Frontend-Asset Tests: verify JS modules and CSS are syntactically valid +and export / register the expected symbols. + +These tests perform static text-level checks on the JS / CSS files — no +runtime execution, no nodejs dependency. They are sufficient to catch: + - Missing exports (e.g. `export { api, ApiError }` removed from api.js) + - Missing global Alpine.data() registrations for the components + - CSS variables removed from the custom-overrides file +""" + +from __future__ import annotations + +from pathlib import Path + +import pytest + +WEBUI = Path(__file__).resolve().parent.parent / "app" / "webui" + + +def _read(rel: str) -> str: + p = WEBUI / rel + assert p.exists(), f"Missing asset: {p}" + return p.read_text(encoding="utf-8") + + +# --------------------------------------------------------------------------- +# api.js +# --------------------------------------------------------------------------- + +def test_api_js_exports_api_and_ApiError(): + content = _read("js/api.js") + assert "export class ApiError" in content + assert "export const api" in content or "export default api" in content + + +def test_api_js_uses_localStorage_for_jwt(): + """R-1 / architecture: JWT must live in localStorage.""" + content = _read("js/api.js") + assert "localStorage" in content + assert "jwt" in content + + +def test_api_js_handles_401_redirect(): + """401 must clear the token and redirect to /index.html.""" + content = _read("js/api.js") + assert "401" in content + assert "/index.html" in content + + +def test_api_js_handles_204_no_content(): + content = _read("js/api.js") + assert "204" in content + + +# --------------------------------------------------------------------------- +# store.js +# --------------------------------------------------------------------------- + +def test_store_js_defines_auth_and_notifications(): + content = _read("js/store.js") + assert "Alpine.store('auth'" in content + assert "Alpine.store('notifications'" in content + + +def test_store_js_auth_has_login_and_logout(): + content = _read("js/store.js") + assert "login" in content + assert "logout" in content + assert "fetchUser" in content + + +def test_store_js_notifications_has_push_and_dismiss(): + content = _read("js/store.js") + assert "push" in content + assert "dismiss" in content + assert "success" in content + assert "error" in content + + +# --------------------------------------------------------------------------- +# app.css +# --------------------------------------------------------------------------- + +def test_app_css_contains_tailwind_overrides(): + content = _read("css/app.css") + # The file defines custom CSS variables (--crm-primary, etc.) + assert "--crm-primary" in content + assert "--crm-success" in content + assert "--crm-danger" in content + + +def test_app_css_has_loading_spinner(): + content = _read("css/app.css") + assert "crm-spinner" in content + assert "@keyframes crm-spin" in content + + +def test_app_css_has_toast_styles(): + content = _read("css/app.css") + assert "crm-toast" in content + assert "toast-success" in content + assert "toast-error" in content + + +# --------------------------------------------------------------------------- +# Alpine components +# --------------------------------------------------------------------------- + +COMPONENTS = [ + ("account-list.js", "accountList"), + ("deal-kanban.js", "dealKanban"), + ("activity-list.js", "activityList"), + ("dashboard-kpis.js", "dashboardKpis"), +] + + +@pytest.mark.parametrize("filename,factory", COMPONENTS) +def test_alpine_component_defined(filename, factory): + """Each component file must define a named factory and register it + globally with Alpine.data(name, factory).""" + content = _read(f"components/{filename}") + # factory function definition + assert f"export function {factory}" in content, \ + f"{filename} missing `export function {factory}()`" + # Alpine.data() registration + assert f"Alpine.data('{factory}'" in content, \ + f"{filename} missing `Alpine.data('{factory}', …)` registration" + + +def test_deal_kanban_handles_drag_and_drop(): + content = _read("components/deal-kanban.js") + assert "draggable" in content or "dataTransfer" in content + assert "onDrop" in content + assert "/deals/" in content and "/stage" in content + + +def test_activity_list_supports_overdue_filter(): + content = _read("components/activity-list.js") + assert "overdue" in content + assert "/activities/" in content + assert "/complete" in content + + +def test_dashboard_kpis_loads_both_kpis_and_feed(): + content = _read("components/dashboard-kpis.js") + assert "/dashboard/kpis" in content + assert "/dashboard/feed" in content + + +def test_account_list_uses_pagination(): + content = _read("components/account-list.js") + assert "skip" in content + assert "limit" in content + assert "q" in content + assert "/accounts/" in content + + +# --------------------------------------------------------------------------- +# notifications.js (toast component) +# --------------------------------------------------------------------------- + +def test_notifications_js_registers_toast_container(): + content = _read("js/components/notifications.js") + assert "toastContainer" in content + assert "Alpine.data" in content + + +# --------------------------------------------------------------------------- +# auth.js +# --------------------------------------------------------------------------- + +def test_auth_js_exports_login_logout_register(): + content = _read("js/auth.js") + assert "export async function login" in content + assert "export async function logout" in content + assert "export async function register" in content + assert "export async function refreshToken" in content