// #region DatasetDetailModelTests [C:3] [TYPE Module] [SEMANTICS test,model,dataset-detail] // @BRIEF L1 unit tests for DatasetDetailModel — no DOM render, mocks API + i18n. // @RELATION BINDS_TO -> [DatasetDetailModel] // @TEST_INVARIANT: load-gate -> VERIFIED_BY: [test_load_guarded_by_context, test_load_idle_state] // @TEST_EDGE: missing_field -> TypeError gracefully handled as error // @TEST_EDGE: invalid_type -> API returns non-object, error state // @TEST_EDGE: external_fail -> API throws, error state set import { describe, it, expect, vi, beforeEach } from "vitest"; vi.mock("$app/navigation", () => ({ goto: vi.fn() })); vi.mock("$lib/api.js", () => ({ api: { getDatasetDetail: vi.fn() }, requestApi: vi.fn() })); vi.mock("$lib/cot-logger", () => ({ log: vi.fn() })); vi.mock("$lib/routes.js", () => ({ ROUTES: { datasets: { detail: vi.fn(() => "/datasets/1"), list: vi.fn(() => "/datasets") }, dashboards: { detail: vi.fn(() => "/dashboards/1") } } })); import { DatasetDetailModel } from "../DatasetDetailModel.svelte.ts"; import { api } from "$lib/api.js"; import { goto } from "$app/navigation"; describe("DatasetDetailModel — L1 invariants (no render)", () => { let model: DatasetDetailModel; beforeEach(() => { vi.clearAllMocks(); model = new DatasetDetailModel(); }); // ═══════════════════════════════════════════════════════════════ // Initial state // ═══════════════════════════════════════════════════════════════ describe("initial state", () => { it("starts in idle state with default values", () => { expect(model.screenState).toBe("idle"); expect(model.dataset).toBeNull(); expect(model.error).toBeNull(); expect(model.datasetId).toBe(""); expect(model.envId).toBe(""); }); it("derived isLoading is false when idle", () => { expect(model.isLoading).toBe(false); }); it("derived columnCount is 0 when dataset is null", () => { expect(model.columnCount).toBe(0); }); it("derived linkedDashCount is 0 when dataset is null", () => { expect(model.linkedDashCount).toBe(0); }); it("derived columnCount from dataset data", () => { model.dataset = { table_name: "t", database: "d", column_count: 15 }; expect(model.columnCount).toBe(15); }); it("derived linkedDashCount from dataset data", () => { model.dataset = { table_name: "t", database: "d", linked_dashboard_count: 3 }; expect(model.linkedDashCount).toBe(3); }); }); // ═══════════════════════════════════════════════════════════════ // @INVARIANT: loadDatasetDetail() guarded by context // ═══════════════════════════════════════════════════════════════ describe("loadDatasetDetail", () => { it("sets error state when datasetId is empty", async () => { model.envId = "env-1"; await model.loadDatasetDetail(); expect(model.screenState).toBe("error"); expect(model.error).toContain("Missing context"); }); it("sets error state when envId is empty", async () => { model.datasetId = "ds-1"; await model.loadDatasetDetail(); expect(model.screenState).toBe("error"); expect(model.error).toContain("Missing context"); }); it("loads dataset when datasetId and envId are set", async () => { vi.mocked(api.getDatasetDetail).mockResolvedValue({ table_name: "sales", schema: "public", database: "prod", column_count: 10, }); model.datasetId = "ds-1"; model.envId = "env-1"; await model.loadDatasetDetail(); expect(api.getDatasetDetail).toHaveBeenCalledWith("env-1", "ds-1"); expect(model.screenState).toBe("loaded"); expect(model.dataset?.table_name).toBe("sales"); expect(model.error).toBeNull(); }); it("sets loading state before fetch, then loaded after", async () => { let resolvePromise!: (v: unknown) => void; vi.mocked(api.getDatasetDetail).mockImplementation(() => new Promise((r) => { resolvePromise = r; })); model.datasetId = "ds-1"; model.envId = "env-1"; const promise = model.loadDatasetDetail(); expect(model.screenState).toBe("loading"); expect(model.isLoading).toBe(true); resolvePromise({ table_name: "sales" }); await promise; expect(model.screenState).toBe("loaded"); }); it("sets error state on API failure", async () => { vi.mocked(api.getDatasetDetail).mockRejectedValue(new Error("Network error")); model.datasetId = "ds-1"; model.envId = "env-1"; await model.loadDatasetDetail(); expect(model.screenState).toBe("error"); expect(model.error).toBe("Network error"); }); it("handles non-Error rejection gracefully", async () => { vi.mocked(api.getDatasetDetail).mockRejectedValue("string error"); model.datasetId = "ds-1"; model.envId = "env-1"; await model.loadDatasetDetail(); expect(model.screenState).toBe("error"); expect(model.error).toBeTruthy(); }); }); // ═══════════════════════════════════════════════════════════════ // Navigation // ═══════════════════════════════════════════════════════════════ describe("navigateToDashboard", () => { it("navigates to dashboard detail with slug", () => { model.envId = "env-1"; model.navigateToDashboard({ id: 1, title: "Sales", slug: "sales-dash" }); expect(goto).toHaveBeenCalled(); }); it("navigates to dashboard detail with id when no slug", () => { model.envId = "env-1"; model.navigateToDashboard({ id: "42", title: "Test" }); expect(goto).toHaveBeenCalled(); }); it("does nothing when dashboard ref is falsy", () => { model.navigateToDashboard({} as any); expect(goto).not.toHaveBeenCalled(); }); }); describe("goBack", () => { it("navigates to datasets list", () => { model.envId = "env-1"; model.goBack(); expect(goto).toHaveBeenCalled(); }); }); // ═══════════════════════════════════════════════════════════════ // Static utilities — getColumnTypeClass // ═══════════════════════════════════════════════════════════════ describe("getColumnTypeClass (static)", () => { it("returns numeric class for int/float/num types", () => { expect(DatasetDetailModel.getColumnTypeClass("int")).toContain("text-primary"); expect(DatasetDetailModel.getColumnTypeClass("integer")).toContain("text-primary"); expect(DatasetDetailModel.getColumnTypeClass("float")).toContain("text-primary"); expect(DatasetDetailModel.getColumnTypeClass("number")).toContain("text-primary"); expect(DatasetDetailModel.getColumnTypeClass("NUMERIC")).toContain("text-primary"); }); it("returns date class for date/time types", () => { expect(DatasetDetailModel.getColumnTypeClass("date")).toContain("text-success"); expect(DatasetDetailModel.getColumnTypeClass("datetime")).toContain("text-success"); expect(DatasetDetailModel.getColumnTypeClass("timestamp")).toContain("text-success"); expect(DatasetDetailModel.getColumnTypeClass("TIME")).toContain("text-success"); }); it("returns string class for str/text/char types", () => { expect(DatasetDetailModel.getColumnTypeClass("string")).toContain("text-info"); expect(DatasetDetailModel.getColumnTypeClass("text")).toContain("text-info"); expect(DatasetDetailModel.getColumnTypeClass("varchar")).toContain("text-info"); expect(DatasetDetailModel.getColumnTypeClass("CHAR")).toContain("text-info"); }); it("returns bool class for bool types", () => { expect(DatasetDetailModel.getColumnTypeClass("bool")).toContain("text-warning"); expect(DatasetDetailModel.getColumnTypeClass("boolean")).toContain("text-warning"); expect(DatasetDetailModel.getColumnTypeClass("BOOLEAN")).toContain("text-warning"); }); it("returns muted class for unknown types", () => { expect(DatasetDetailModel.getColumnTypeClass("json")).toContain("text-text-muted"); expect(DatasetDetailModel.getColumnTypeClass("uuid")).toContain("bg-surface-muted"); }); it("returns muted class for undefined input", () => { expect(DatasetDetailModel.getColumnTypeClass(undefined)).toContain("text-text-muted"); }); }); }); // #endregion DatasetDetailModelTests