Files
ss-tools/frontend/src/lib/models/__tests__/DashboardDetailModel.test.ts
busya 3b4ac807a5 feat(agent+ui): fullstack agent module refactoring + UI/UX improvements
## Backend: agent module GRACE-Poly compliance
- Split app.py (749→~280 lines) into _tool_resolver, _confirmation, _persistence
- All 18 naked functions wrapped in #region/#endregion contracts
- Fixed @DEFGROUP→@defgroup typos; added @DATA_CONTRACT, @SIDE_EFFECT, CoT logs
- Conversation list API: added last_role, has_tool_calls, has_error, risk_level fields
- Message state detection: Russian/English error patterns (недоступен, unavailable)
- State field preserved in save_conversation messages
- HITL titles: descriptive tool names instead of generic "HITL resume"

## Backend: conversation title generation (two-layer)
- Layer 1: clean_title() — rule-based, strips file markers, pre-fetch blocks, JSON/CSV,
  URLs, code; truncates at 80 chars word boundary (25 unit tests, all edge cases)
- Layer 2: generate_llm_title() — async best-effort LLM titling via /v1/chat/completions
  with per-conversation lock, graceful degradation on failure

## Frontend: conversation list indicators (orthogonal system)
- Status dot (green/yellow/red/blue) per conversation state
- Icon column: tool activity, errors, waiting, completed
- Risk stripe (left border accent) + message count badge + relative time
- Fixed group labels: "Сегодня"/"Вчера" instead of "3 ч"/"5 ч"
- Hide "Окружение: —" when env is empty

## Frontend: guardrails card verification + fixes
- Confirmed all interaction modes: Enter/click confirm, Escape/click deny
- Auto-populate envId from environmentContextStore in DashboardDetailModel
- Better error message: missing_context_hint with recovery guidance

## Design system: semantic tokens
- Added category-* gradient tokens to tailwind.config.js
- Sidebar + Breadcrumbs use semantic tokens (10 categories)
- Raw Tailwind reduced from ~50 to 6 occurrences
- Added skip-to-content link in root layout (+layout.svelte)
- Added aria-label on DashboardDataGrid row checkboxes

## Protocol: INV_7 pragmatic exception
- Modules may exceed 400 lines when contract-dense (every function has #region)
- Recorded in semantics-core SKILL.md with rationale

Total: 5841+ contracts, 2993+ edges, backend 41/41, frontend 2501/2501
2026-06-30 15:21:05 +03:00

639 lines
27 KiB
TypeScript

// #region DashboardDetailModelTests [C:3] [TYPE Module] [SEMANTICS test,model,dashboard-detail]
// @BRIEF L1 unit tests for DashboardDetailModel — no DOM render, mocks API + toast + i18n + navigation.
// @RELATION BINDS_TO -> [Dashboards.DetailModel]
// @TEST_INVARIANT: load-gate -> VERIFIED_BY: [test_load_guarded_by_context, test_load_detail_guarded]
// @TEST_INVARIANT: thumbnail-cleanup -> VERIFIED_BY: [test_cleanup_releases_thumbnail, test_load_thumbnail_releases_previous]
// @TEST_EDGE: missing_field -> dashboardRef not set on load
// @TEST_EDGE: invalid_type -> API returns non-object
// @TEST_EDGE: external_fail -> API throws on each fetch
import { describe, it, expect, vi, beforeEach } from "vitest";
vi.mock("$app/navigation", () => ({ goto: vi.fn() }));
vi.mock("$lib/api.js", () => ({
api: {
getDashboardDetail: vi.fn(),
getDashboardTaskHistory: vi.fn(),
getDashboardThumbnail: vi.fn(),
postApi: vi.fn(),
},
fetchApi: vi.fn(),
}));
vi.mock("$lib/cot-logger", () => ({ log: vi.fn() }));
vi.mock("$lib/toasts.svelte.js", () => ({ addToast: vi.fn() }));
vi.mock("$lib/stores/taskDrawer.svelte.js", () => ({
openDrawerForTaskIfPreferred: vi.fn(),
openDrawerForTask: vi.fn(),
}));
vi.mock("$lib/stores/environmentContext.svelte.js", () => ({
environmentContextStore: {
value: { selectedEnvId: "" },
},
}));
vi.mock("$lib/routes.js", () => ({
ROUTES: {
dashboards: { list: vi.fn(() => "/dashboards"), detail: vi.fn(() => "/dashboards/1") },
datasets: { detail: vi.fn(() => "/datasets/1") },
},
}));
vi.mock("$lib/i18n/index.svelte.js", () => ({
getT: () => ({
dashboard: {
missing_context: "Missing context",
missing_context_hint: "No environment selected. Choose an environment from the top bar and reload the page.",
load_detail_failed: "Failed",
thumbnail_generating: "Generating...",
thumbnail_failed: "Thumbnail failed",
backup_started: "Backup started",
backup_task_failed: "Backup failed",
backup: "Backup",
llm_check: "LLM Check",
},
}),
}));
// Mock URL.createObjectURL / revokeObjectURL
const mockObjectUrl = "blob:mock-url-1";
URL.createObjectURL = vi.fn(() => mockObjectUrl);
URL.revokeObjectURL = vi.fn();
import { DashboardDetailModel } from "../DashboardDetailModel.svelte.ts";
import { api } from "$lib/api.js";
import { fetchApi } from "$lib/api";
import { goto } from "$app/navigation";
import { addToast } from "$lib/toasts.svelte.js";
import { openDrawerForTaskIfPreferred } from "$lib/stores/taskDrawer.svelte.js";
describe("DashboardDetailModel — L1 invariants (no render)", () => {
let model: DashboardDetailModel;
beforeEach(() => {
vi.clearAllMocks();
model = new DashboardDetailModel();
});
// ═══════════════════════════════════════════════════════════════
// Initial state
// ═══════════════════════════════════════════════════════════════
describe("initial state", () => {
it("starts with default values", () => {
expect(model.screenState).toBe("idle");
expect(model.dashboard).toBeNull();
expect(model.error).toBeNull();
expect(model.dashboardRef).toBe("");
expect(model.envId).toBe("");
expect(model.activeTab).toBe("resources");
expect(model.taskHistory).toEqual([]);
expect(model.isTaskHistoryLoading).toBe(false);
expect(model.validationHistory).toEqual([]);
expect(model.thumbnailUrl).toBe("");
expect(model.isStartingBackup).toBe(false);
expect(model.showGitManager).toBe(false);
});
it("derived isLoading equals screenState === 'loading'", () => {
expect(model.isLoading).toBe(false);
model.screenState = "loading";
expect(model.isLoading).toBe(true);
});
it("derived gitDashboardRef falls through slug -> dashboardRef -> empty", () => {
expect(model.gitDashboardRef).toBe("");
model.dashboardRef = "my-ref";
expect(model.gitDashboardRef).toBe("my-ref");
model.dashboard = { id: 1, title: "Sales", slug: "sales-dash" };
expect(model.gitDashboardRef).toBe("sales-dash");
});
it("derived resolvedDashboardId from numeric dashboardRef", () => {
model.dashboardRef = "42";
expect(model.resolvedDashboardId).toBe(42);
});
it("derived resolvedDashboardId from dashboard.id", () => {
model.dashboard = { id: 99, title: "Sales" };
expect(model.resolvedDashboardId).toBe(99);
});
it("derived resolvedDashboardId is null when ref is non-numeric and no dashboard", () => {
model.dashboardRef = "slug-ref";
expect(model.resolvedDashboardId).toBeNull();
});
});
// ═══════════════════════════════════════════════════════════════
// @INVARIANT: loadDashboardDetail guarded by context
// ═══════════════════════════════════════════════════════════════
describe("loadDashboardDetail", () => {
it("sets error when dashboardRef is empty", async () => {
model.envId = "env-1";
await model.loadDashboardDetail();
expect(model.screenState).toBe("error");
expect(model.error).toBeTruthy();
});
it("sets error when envId is empty", async () => {
model.dashboardRef = "dash-1";
await model.loadDashboardDetail();
expect(model.screenState).toBe("error");
expect(model.error).toBeTruthy();
});
it("loads detail when dashboardRef and envId are set", async () => {
vi.mocked(api.getDashboardDetail).mockResolvedValue({ id: 1, title: "Sales" });
model.dashboardRef = "sales-dash";
model.envId = "env-1";
await model.loadDashboardDetail();
expect(model.screenState).toBe("loaded");
expect(model.dashboard?.title).toBe("Sales");
expect(model.error).toBeNull();
});
it("sets loading state before fetch, then loaded after", async () => {
vi.mocked(api.getDashboardDetail).mockImplementation(() => new Promise((r) => setTimeout(r, 10)));
model.dashboardRef = "sales-dash";
model.envId = "env-1";
const promise = model.loadDashboardDetail();
expect(model.screenState).toBe("loading");
await promise;
expect(model.screenState).toBe("loaded");
});
it("sets error state on API failure", async () => {
vi.mocked(api.getDashboardDetail).mockRejectedValue(new Error("404 Not Found"));
model.dashboardRef = "sales-dash";
model.envId = "env-1";
await model.loadDashboardDetail();
expect(model.screenState).toBe("error");
expect(model.error).toBe("404 Not Found");
});
it("handles non-Error rejection gracefully", async () => {
vi.mocked(api.getDashboardDetail).mockRejectedValue("string error");
model.dashboardRef = "sales-dash";
model.envId = "env-1";
await model.loadDashboardDetail();
expect(model.screenState).toBe("error");
});
// ── P0-1 fix: auto-populate envId from environmentContextStore ──
it("auto-populates envId from store when URL param is missing", async () => {
const { environmentContextStore } = await import("$lib/stores/environmentContext.svelte.js");
(environmentContextStore as any).value = { selectedEnvId: "ss-dev" };
vi.mocked(api.getDashboardDetail).mockResolvedValue({ id: 1, title: "Test" });
model.dashboardRef = "test-dash";
model.envId = ""; // missing from URL
await model.loadDashboardDetail();
expect(model.screenState).toBe("loaded");
expect(model.envId).toBe("ss-dev");
expect(api.getDashboardDetail).toHaveBeenCalledWith("ss-dev", "test-dash");
});
it("does NOT override envId from URL when already present", async () => {
const { environmentContextStore } = await import("$lib/stores/environmentContext.svelte.js");
(environmentContextStore as any).value = { selectedEnvId: "ss-dev" };
vi.mocked(api.getDashboardDetail).mockResolvedValue({ id: 2, title: "Test2" });
model.dashboardRef = "test-dash";
model.envId = "ss-prod"; // explicitly set from URL
await model.loadDashboardDetail();
// envId from URL should take priority over store fallback
expect(api.getDashboardDetail).toHaveBeenCalledWith("ss-prod", "test-dash");
});
it("shows helpful error hint when no envId from URL or store", async () => {
const { environmentContextStore } = await import("$lib/stores/environmentContext.svelte.js");
(environmentContextStore as any).value = { selectedEnvId: "" };
model.dashboardRef = "test-dash";
model.envId = "";
await model.loadDashboardDetail();
expect(model.screenState).toBe("error");
// Error should contain the recovery hint from missing_context_hint
expect(model.error).toBeTruthy();
expect(model.error).toEqual(expect.stringContaining("environment"));
});
it("handles undefined store value gracefully", async () => {
const { environmentContextStore } = await import("$lib/stores/environmentContext.svelte.js");
(environmentContextStore as any).value = undefined;
vi.mocked(api.getDashboardDetail).mockResolvedValue({ id: 3, title: "Test3" });
model.dashboardRef = "test-dash";
model.envId = "ss-prod"; // envId from URL
await model.loadDashboardDetail();
// Should still work with explicit envId
expect(model.screenState).toBe("loaded");
});
});
// ═══════════════════════════════════════════════════════════════
// loadDashboardPage — parallel fetch
// ═══════════════════════════════════════════════════════════════
describe("loadDashboardPage", () => {
it("loads detail, task history, thumbnail, validation in parallel", async () => {
vi.mocked(api.getDashboardDetail).mockResolvedValue({ id: 1, title: "Sales", slug: "sales-dash" });
vi.mocked(api.getDashboardTaskHistory).mockResolvedValue({ items: [] });
vi.mocked(api.getDashboardThumbnail).mockRejectedValue({ status: 202 });
vi.mocked(fetchApi).mockResolvedValue({ items: [] });
model.dashboardRef = "sales-dash";
model.envId = "env-1";
await model.loadDashboardPage();
expect(api.getDashboardDetail).toHaveBeenCalled();
expect(api.getDashboardTaskHistory).toHaveBeenCalled();
expect(api.getDashboardThumbnail).toHaveBeenCalled();
expect(fetchApi).toHaveBeenCalledWith(expect.stringContaining("/validation-tasks/runs/all"));
});
});
// ═══════════════════════════════════════════════════════════════
// loadTaskHistory
// ═══════════════════════════════════════════════════════════════
describe("loadTaskHistory", () => {
it("returns early when ref is empty", async () => {
await model.loadTaskHistory();
expect(api.getDashboardTaskHistory).not.toHaveBeenCalled();
});
it("loads task history on success", async () => {
vi.mocked(api.getDashboardTaskHistory).mockResolvedValue({
items: [{ id: "task-1", status: "success" }],
});
model.dashboardRef = "dash-1";
model.envId = "env-1";
await model.loadTaskHistory();
expect(api.getDashboardTaskHistory).toHaveBeenCalledWith("env-1", "dash-1", { limit: 30 });
expect(model.taskHistory).toHaveLength(1);
expect(model.isTaskHistoryLoading).toBe(false);
});
it("sets error on API failure", async () => {
vi.mocked(api.getDashboardTaskHistory).mockRejectedValue(new Error("History failed"));
model.dashboardRef = "dash-1";
model.envId = "env-1";
await model.loadTaskHistory();
expect(model.taskHistoryError).toBe("History failed");
expect(model.taskHistory).toEqual([]);
expect(model.isTaskHistoryLoading).toBe(false);
});
});
// ═══════════════════════════════════════════════════════════════
// loadThumbnail
// ═══════════════════════════════════════════════════════════════
describe("loadThumbnail", () => {
it("returns early when ref is empty", async () => {
await model.loadThumbnail();
expect(api.getDashboardThumbnail).not.toHaveBeenCalled();
});
it("loads thumbnail and creates blob URL", async () => {
const fakeBlob = new Blob(["fake"], { type: "image/png" });
vi.mocked(api.getDashboardThumbnail).mockResolvedValue(fakeBlob);
model.dashboardRef = "dash-1";
model.envId = "env-1";
await model.loadThumbnail();
expect(api.getDashboardThumbnail).toHaveBeenCalledWith("env-1", "dash-1", { force: false });
expect(URL.createObjectURL).toHaveBeenCalledWith(fakeBlob);
expect(model.thumbnailUrl).toBe(mockObjectUrl);
expect(model.isThumbnailLoading).toBe(false);
});
it("handles 202 status as generating", async () => {
vi.mocked(api.getDashboardThumbnail).mockRejectedValue({ status: 202 });
model.dashboardRef = "dash-1";
model.envId = "env-1";
await model.loadThumbnail();
expect(model.thumbnailError).toBe("Generating...");
expect(model.isThumbnailLoading).toBe(false);
});
it("handles other errors", async () => {
vi.mocked(api.getDashboardThumbnail).mockRejectedValue(new Error("Network"));
model.dashboardRef = "dash-1";
model.envId = "env-1";
await model.loadThumbnail();
expect(model.thumbnailError).toBe("Network");
expect(model.isThumbnailLoading).toBe(false);
});
// @INVARIANT: thumbnail blob URL released on reassignment
it("releases previous thumbnail URL when loading new one", async () => {
const fakeBlob = new Blob(["fake"], { type: "image/png" });
model.thumbnailUrl = "blob:old-url";
vi.mocked(api.getDashboardThumbnail).mockResolvedValue(fakeBlob);
model.dashboardRef = "dash-1";
model.envId = "env-1";
await model.loadThumbnail();
expect(URL.revokeObjectURL).toHaveBeenCalledWith("blob:old-url");
});
});
// ═══════════════════════════════════════════════════════════════
// loadValidationHistory
// ═══════════════════════════════════════════════════════════════
describe("loadValidationHistory", () => {
it("returns early when ref is empty", async () => {
await model.loadValidationHistory();
expect(fetchApi).not.toHaveBeenCalled();
});
it("loads validation history on success via items", async () => {
vi.mocked(fetchApi).mockResolvedValue({ items: [{ id: "vr-1", status: "completed" }] });
model.dashboardRef = "dash-1";
model.envId = "env-1";
await model.loadValidationHistory();
expect(model.validationHistory).toHaveLength(1);
expect(model.isValidationHistoryLoading).toBe(false);
});
it("falls back to results field when items is absent", async () => {
vi.mocked(fetchApi).mockResolvedValue({ results: [{ id: "vr-2" }] });
model.dashboardRef = "dash-1";
model.envId = "env-1";
await model.loadValidationHistory();
expect(model.validationHistory).toHaveLength(1);
});
it("sets error on API failure", async () => {
vi.mocked(fetchApi).mockRejectedValue(new Error("Val failed"));
model.dashboardRef = "dash-1";
model.envId = "env-1";
await model.loadValidationHistory();
expect(model.validationHistoryError).toBe("Val failed");
expect(model.validationHistory).toEqual([]);
expect(model.isValidationHistoryLoading).toBe(false);
});
});
// ═══════════════════════════════════════════════════════════════
// runBackupTask
// ═══════════════════════════════════════════════════════════════
describe("runBackupTask", () => {
it("returns early when isStartingBackup is true", async () => {
model.isStartingBackup = true;
await model.runBackupTask();
expect(api.postApi).not.toHaveBeenCalled();
});
it("returns early when envId is empty", async () => {
await model.runBackupTask();
expect(api.postApi).not.toHaveBeenCalled();
});
it("returns early when resolvedDashboardId is null", async () => {
model.envId = "env-1";
model.dashboardRef = "slug-only";
await model.runBackupTask();
expect(api.postApi).not.toHaveBeenCalled();
});
it("posts backup and shows success toast", async () => {
model.envId = "env-1";
model.dashboard = { id: 1, title: "Sales" };
vi.mocked(api.postApi).mockResolvedValue({ task_id: "task-1" });
vi.mocked(api.getDashboardTaskHistory).mockResolvedValue({ items: [] });
await model.runBackupTask();
expect(api.postApi).toHaveBeenCalledWith("/dashboards/backup", {
env_id: "env-1",
dashboard_ids: [1],
});
expect(openDrawerForTaskIfPreferred).toHaveBeenCalledWith("task-1");
expect(addToast).toHaveBeenCalledWith("Backup started", "success");
expect(model.isStartingBackup).toBe(false);
});
it("shows error toast on API failure", async () => {
model.envId = "env-1";
model.dashboard = { id: 1, title: "Sales" };
vi.mocked(api.postApi).mockRejectedValue(new Error("Backup failed"));
await model.runBackupTask();
expect(addToast).toHaveBeenCalledWith("Backup failed", "error");
expect(model.isStartingBackup).toBe(false);
});
});
// ═══════════════════════════════════════════════════════════════
// Git-related actions
// ═══════════════════════════════════════════════════════════════
describe("handleSyncAndOpenCommit", () => {
it("opens git manager on sync success", async () => {
model.gitModel.syncRepository = vi.fn().mockResolvedValue(true);
await model.handleSyncAndOpenCommit();
expect(model.showGitManager).toBe(true);
});
it("does not open git manager on sync failure", async () => {
model.gitModel.syncRepository = vi.fn().mockResolvedValue(false);
await model.handleSyncAndOpenCommit();
expect(model.showGitManager).toBe(false);
});
});
describe("trackGitManagerClose", () => {
it("sets wasOpen when showGitManager becomes true", () => {
model.showGitManager = true;
model.trackGitManagerClose();
// no assertion needed — internal flag set
});
it("reloads status when git manager closes after having been open", () => {
model.showGitManager = true;
model.trackGitManagerClose(); // marks wasOpen
model.showGitManager = false;
model.gitModel.loadStatus = vi.fn().mockResolvedValue(undefined);
model.trackGitManagerClose(); // triggers reload
expect(model.gitModel.loadStatus).toHaveBeenCalled();
});
});
// ═══════════════════════════════════════════════════════════════
// cleanup
// ═══════════════════════════════════════════════════════════════
// @INVARIANT: cleanup releases thumbnail
it("cleanup releases thumbnail URL", () => {
model.thumbnailUrl = "blob:some-url";
model.cleanup();
expect(URL.revokeObjectURL).toHaveBeenCalledWith("blob:some-url");
expect(model.thumbnailUrl).toBe("");
});
// ═══════════════════════════════════════════════════════════════
// Navigation
// ═══════════════════════════════════════════════════════════════
describe("goBack", () => {
it("navigates to dashboard list", () => {
model.envId = "env-1";
model.goBack();
expect(goto).toHaveBeenCalled();
});
});
describe("openDataset", () => {
it("navigates to dataset detail", () => {
model.envId = "env-1";
model.openDataset("ds-42");
expect(goto).toHaveBeenCalled();
});
});
// ═══════════════════════════════════════════════════════════════
// Static utilities
// ═══════════════════════════════════════════════════════════════
describe("static methods", () => {
describe("toTaskTypeLabel", () => {
it("returns backup label for superset-backup", () => {
expect(DashboardDetailModel.toTaskTypeLabel("superset-backup")).toBe("Backup");
});
it("returns LLM check label for llm_dashboard_validation", () => {
expect(DashboardDetailModel.toTaskTypeLabel("llm_dashboard_validation")).toBe("LLM Check");
});
it("returns pluginId as-is for unknown plugins", () => {
expect(DashboardDetailModel.toTaskTypeLabel("custom-plugin")).toBe("custom-plugin");
});
it("returns dash for undefined", () => {
expect(DashboardDetailModel.toTaskTypeLabel(undefined)).toBe("-");
});
});
describe("getTaskStatusClasses", () => {
it("returns primary class for running/pending", () => {
expect(DashboardDetailModel.getTaskStatusClasses("running")).toContain("bg-primary-light");
expect(DashboardDetailModel.getTaskStatusClasses("pending")).toContain("bg-primary-light");
expect(DashboardDetailModel.getTaskStatusClasses("PENDING")).toContain("bg-primary-light");
});
it("returns success class for success", () => {
expect(DashboardDetailModel.getTaskStatusClasses("success")).toContain("bg-success-light");
});
it("returns destructive class for failed/error", () => {
expect(DashboardDetailModel.getTaskStatusClasses("failed")).toContain("bg-destructive-light");
expect(DashboardDetailModel.getTaskStatusClasses("error")).toContain("bg-destructive-light");
});
it("returns warning class for awaiting_input", () => {
expect(DashboardDetailModel.getTaskStatusClasses("awaiting_input")).toContain("bg-warning-light");
expect(DashboardDetailModel.getTaskStatusClasses("waiting_input")).toContain("bg-warning-light");
});
it("returns muted class for unknown status", () => {
expect(DashboardDetailModel.getTaskStatusClasses("unknown")).toContain("bg-surface-muted");
});
it("handles undefined status", () => {
expect(DashboardDetailModel.getTaskStatusClasses(undefined)).toContain("bg-surface-muted");
});
});
describe("getValidationStatus", () => {
it("returns na for non-validation tasks", () => {
expect(DashboardDetailModel.getValidationStatus({ plugin_id: "backup" }).level).toBe("na");
});
it("returns fail for FAIL status", () => {
const r = DashboardDetailModel.getValidationStatus({ plugin_id: "llm_dashboard_validation", validation_status: "FAIL" });
expect(r.level).toBe("fail");
expect(r.label).toBe("FAIL");
});
it("returns warn for WARN status", () => {
const r = DashboardDetailModel.getValidationStatus({ plugin_id: "llm_dashboard_validation", validation_status: "WARN" });
expect(r.level).toBe("warn");
});
it("returns pass for PASS status", () => {
const r = DashboardDetailModel.getValidationStatus({ plugin_id: "llm_dashboard_validation", validation_status: "PASS" });
expect(r.level).toBe("pass");
expect(r.icon).toBe("OK");
});
it("returns unknown for unrecognized status", () => {
const r = DashboardDetailModel.getValidationStatus({ plugin_id: "llm_dashboard_validation", validation_status: "ERROR" });
expect(r.level).toBe("unknown");
});
it("handles missing validation_status", () => {
const r = DashboardDetailModel.getValidationStatus({ plugin_id: "llm_dashboard_validation" });
expect(r.level).toBe("unknown");
});
});
describe("getValidationStatusClasses", () => {
it("returns destructive for fail", () => {
expect(DashboardDetailModel.getValidationStatusClasses("fail")).toContain("bg-destructive-light");
});
it("returns warning for warn", () => {
expect(DashboardDetailModel.getValidationStatusClasses("warn")).toContain("bg-warning-light");
});
it("returns success for pass", () => {
expect(DashboardDetailModel.getValidationStatusClasses("pass")).toContain("bg-success-light");
});
it("returns muted for unknown", () => {
expect(DashboardDetailModel.getValidationStatusClasses("unknown")).toContain("bg-surface-muted");
});
it("returns page bg for na", () => {
expect(DashboardDetailModel.getValidationStatusClasses("na")).toContain("bg-surface-page");
});
});
describe("formatDate", () => {
it("returns dash for falsy input", () => {
expect(DashboardDetailModel.formatDate(null)).toBe("-");
expect(DashboardDetailModel.formatDate(undefined)).toBe("-");
expect(DashboardDetailModel.formatDate("")).toBe("-");
});
it("returns formatted date for valid input", () => {
const result = DashboardDetailModel.formatDate("2024-06-10T12:30:00Z");
expect(result).toBeTruthy();
expect(result).not.toBe("-");
});
it("returns dash for invalid date string", () => {
expect(DashboardDetailModel.formatDate("not-a-date")).toBe("-");
});
});
});
});
// #endregion DashboardDetailModelTests