| """ |
| Tests for Stage 90 β customer dashboard structure. |
| |
| This is mostly a static asset (React + Vite SPA). The Python |
| test suite verifies: |
| 1. Every key file exists at the expected path. |
| 2. package.json declares the expected runtime + dev deps. |
| 3. vite.config.ts proxies /api/v1 to the backend (dev-loop |
| sanity β without this, sign-in doesn't work locally). |
| 4. tsconfig.json has strict mode on. |
| 5. The API client (api.ts) references all the routes the |
| dashboard pages depend on β drift between pages and the |
| client surface would silently break a page. |
| 6. Each page file exists and is non-trivial. |
| 7. The dashboard README has the right anchors (dev / build / |
| deploy / API URL). |
| |
| Component-level tests live in JS land (future work via Vitest); |
| this file validates the contract that customer-facing code |
| expects the OrgState API to honor. |
| """ |
| import json |
| from pathlib import Path |
|
|
| import pytest |
|
|
| ROOT = Path(__file__).parent.parent |
| DASH = ROOT / "dashboard" |
| SRC = DASH / "src" |
| PAGES = SRC / "pages" |
|
|
|
|
| |
| |
| |
|
|
| def test_dashboard_dir_exists(): |
| assert DASH.is_dir() |
|
|
|
|
| def test_root_config_files_present(): |
| for name in ("package.json", "vite.config.ts", "tsconfig.json", |
| "tsconfig.node.json", "tailwind.config.js", |
| "postcss.config.js", "index.html", "README.md", |
| ".gitignore"): |
| assert (DASH / name).exists(), f"dashboard/{name} missing" |
|
|
|
|
| def test_src_files_present(): |
| for name in ("main.tsx", "App.tsx", "auth.ts", "api.ts", |
| "index.css"): |
| assert (SRC / name).exists(), f"dashboard/src/{name} missing" |
|
|
|
|
| def test_all_pages_present(): |
| for page in ("Login.tsx", "Overview.tsx", "Runs.tsx", |
| "RunDetail.tsx", "Webhooks.tsx"): |
| path = PAGES / page |
| assert path.exists(), f"dashboard/src/pages/{page} missing" |
| |
| assert len(path.read_text().splitlines()) > 30, \ |
| f"{page} too short β likely placeholder" |
|
|
|
|
| |
| |
| |
|
|
| def test_package_json_parses(): |
| spec = json.loads((DASH / "package.json").read_text()) |
| assert spec["name"] == "orgstate-dashboard" |
| assert spec["type"] == "module" |
|
|
|
|
| def test_package_json_has_expected_scripts(): |
| spec = json.loads((DASH / "package.json").read_text()) |
| for script in ("dev", "build", "preview", "typecheck"): |
| assert script in spec["scripts"], \ |
| f"package.json missing script: {script}" |
|
|
|
|
| def test_package_json_has_react_deps(): |
| spec = json.loads((DASH / "package.json").read_text()) |
| for dep in ("react", "react-dom", "react-router-dom"): |
| assert dep in spec["dependencies"], \ |
| f"missing runtime dep: {dep}" |
|
|
|
|
| def test_package_json_has_build_devdeps(): |
| spec = json.loads((DASH / "package.json").read_text()) |
| for dep in ("vite", "typescript", "@vitejs/plugin-react", |
| "tailwindcss"): |
| assert dep in spec["devDependencies"], \ |
| f"missing dev dep: {dep}" |
|
|
|
|
| |
| |
| |
|
|
| def test_vite_config_has_dev_proxy(): |
| """Without /api/v1 β :8080 proxy, `npm run dev` can't talk |
| to the local backend β sign-in fails with CORS.""" |
| text = (DASH / "vite.config.ts").read_text() |
| assert "/api/v1" in text |
| assert "8080" in text |
| assert "proxy" in text |
|
|
|
|
| def test_vite_config_uses_react_plugin(): |
| text = (DASH / "vite.config.ts").read_text() |
| assert "@vitejs/plugin-react" in text |
| assert "react()" in text |
|
|
|
|
| |
| |
| |
|
|
| def test_tsconfig_has_strict_mode(): |
| spec = json.loads((DASH / "tsconfig.json").read_text()) |
| assert spec["compilerOptions"]["strict"] is True |
| assert spec["compilerOptions"]["jsx"] == "react-jsx" |
|
|
|
|
| |
| |
| |
|
|
| |
| |
| |
| def _api_text() -> str: |
| return (SRC / "api.ts").read_text() |
|
|
|
|
| def _all_pages_text() -> str: |
| return "\n".join(p.read_text() for p in PAGES.glob("*.tsx")) |
|
|
|
|
| def test_api_has_all_methods_pages_use(): |
| api = _api_text() |
| pages = _all_pages_text() |
| |
| import re |
| used = set(re.findall(r"api\.(\w+)\(", pages)) |
| for method in used: |
| assert f"{method}:" in api, \ |
| f"page uses api.{method}() but api.ts doesn't export it" |
|
|
|
|
| def test_api_methods_have_known_routes(): |
| """Each API method's path should reference a route family |
| the backend actually exposes.""" |
| api = _api_text() |
| |
| for path in ("/tenants/", "/runs/", "/webhooks/", "/health", |
| "/usage"): |
| assert path in api, \ |
| f"api.ts missing path family {path}" |
|
|
|
|
| |
| |
| |
|
|
| def test_readme_has_install_dev_build_deploy_sections(): |
| text = (DASH / "README.md").read_text() |
| for anchor in ("## Dev", "## Build", "## Deploying", |
| "VITE_API_BASE_URL"): |
| assert anchor in text, f"README missing anchor: {anchor}" |
|
|
|
|
| def test_readme_links_to_onboarding(): |
| """Customer reading the dashboard README should know where |
| their API key comes from (Stage 85's `infra onboard`).""" |
| text = (DASH / "README.md").read_text() |
| assert "infra onboard" in text |
|
|
|
|
| |
| |
| |
|
|
| def test_auth_module_uses_localstorage(): |
| """Sanity β auth.ts must read/write localStorage, not |
| sessionStorage / cookies / etc. Stage 90 contract says |
| 'sign-in persists across page refreshes'.""" |
| text = (SRC / "auth.ts").read_text() |
| assert "localStorage" in text |
| assert "loadAuth" in text |
| assert "saveAuth" in text |
| assert "clearAuth" in text |
|
|
|
|
| |
| |
| |
|
|
| import shutil |
| import subprocess |
|
|
|
|
| @pytest.mark.skipif( |
| shutil.which("npm") is None, |
| reason="npm not installed; skipping JS build smoke", |
| ) |
| def test_npm_install_and_build(): |
| """Heavy gate β runs `npm install` and `npm run build` to |
| confirm the dashboard actually builds. Gated on `npm` |
| availability so CI workers without Node still pass. |
| |
| Skipped if node_modules absent AND we don't have time to |
| install (>30s on cold cache). We do `npm install --silent` |
| once, then `npm run build` β both must succeed.""" |
| |
| r = subprocess.run( |
| ["npm", "install", "--silent", "--no-audit", |
| "--no-fund", "--no-progress"], |
| cwd=str(DASH), capture_output=True, timeout=300, |
| ) |
| assert r.returncode == 0, \ |
| f"npm install failed:\n{r.stderr.decode()}" |
| |
| r = subprocess.run( |
| ["npm", "run", "build"], |
| cwd=str(DASH), capture_output=True, timeout=300, |
| ) |
| assert r.returncode == 0, ( |
| f"npm run build failed:\nSTDOUT: {r.stdout.decode()}" |
| f"\nSTDERR: {r.stderr.decode()}" |
| ) |
| |
| dist = DASH / "dist" |
| assert dist.is_dir() |
| assert (dist / "index.html").exists() |
|
|