2026-06-03 23:52:07 +00:00
|
|
|
"""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
|
|
|
|
|
# ---------------------------------------------------------------------------
|
|
|
|
|
|
2026-06-10 21:24:24 +00:00
|
|
|
|
2026-06-03 23:52:07 +00:00
|
|
|
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
|
|
|
|
|
# ---------------------------------------------------------------------------
|
|
|
|
|
|
2026-06-10 21:24:24 +00:00
|
|
|
|
2026-06-03 23:52:07 +00:00
|
|
|
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
|
|
|
|
|
# ---------------------------------------------------------------------------
|
|
|
|
|
|
2026-06-10 21:24:24 +00:00
|
|
|
|
2026-06-03 23:52:07 +00:00
|
|
|
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 = [
|
2026-06-10 21:24:24 +00:00
|
|
|
("account-list.js", "accountList"),
|
|
|
|
|
("deal-kanban.js", "dealKanban"),
|
|
|
|
|
("activity-list.js", "activityList"),
|
|
|
|
|
("dashboard-kpis.js", "dashboardKpis"),
|
2026-06-03 23:52:07 +00:00
|
|
|
]
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
@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
|
2026-06-10 21:24:24 +00:00
|
|
|
assert f"export function {factory}" in content, (
|
2026-06-03 23:52:07 +00:00
|
|
|
f"{filename} missing `export function {factory}()`"
|
2026-06-10 21:24:24 +00:00
|
|
|
)
|
2026-06-03 23:52:07 +00:00
|
|
|
# Alpine.data() registration
|
2026-06-10 21:24:24 +00:00
|
|
|
assert f"Alpine.data('{factory}'" in content, (
|
2026-06-03 23:52:07 +00:00
|
|
|
f"{filename} missing `Alpine.data('{factory}', …)` registration"
|
2026-06-10 21:24:24 +00:00
|
|
|
)
|
2026-06-03 23:52:07 +00:00
|
|
|
|
|
|
|
|
|
|
|
|
|
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)
|
|
|
|
|
# ---------------------------------------------------------------------------
|
|
|
|
|
|
2026-06-10 21:24:24 +00:00
|
|
|
|
2026-06-03 23:52:07 +00:00
|
|
|
def test_notifications_js_registers_toast_container():
|
|
|
|
|
content = _read("js/components/notifications.js")
|
|
|
|
|
assert "toastContainer" in content
|
|
|
|
|
assert "Alpine.data" in content
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
# ---------------------------------------------------------------------------
|
|
|
|
|
# auth.js
|
|
|
|
|
# ---------------------------------------------------------------------------
|
|
|
|
|
|
2026-06-10 21:24:24 +00:00
|
|
|
|
2026-06-03 23:52:07 +00:00
|
|
|
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
|