"""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