## Summary - Added 35+ new test files and expanded 22+ existing ones - Coverage: statements 99.65%, lines 99.9%, functions 99.9%, branches 87.77% - All thresholds enabled and enforced in vitest.config.js ## Details ### Stores (stores/__tests__/) - test_health.ts, test_translationRun.ts, test_environmentContext.ts - test_maintenance.ts, test_environmentContext.2.ts - Expanded sidebar.test.ts, assistantChat.test.ts, taskDrawer.test.ts - Expanded test_activity.ts, test_datasetReviewSession.ts ### Models (models/__tests__/) - AgentChatModel.test.ts (77.7%→99.6%), AgentChatModel.2.test.ts - BranchModel.test.ts, DashboardDetailModel.test.ts - DashboardHubModel.test.ts (97.9%→100%) - DatasetDetailModel.test.ts, DatasetReviewModel.test.ts - DatasetsHubModel.test.ts, DictionaryDetailModel.test.ts - GitConfigModel.test.ts, GitManagerModel.test.ts - GitStatusModel.test.ts, HealthCenterModel.test.ts - LLMReportModel.test.ts, MigrationModel.test.ts - MigrationSettingsModel.test.ts, TranslateHistoryModel.test.ts - TranslationJobModel.test.ts, ValidationRunDetailModel.test.ts - ValidationTasksListModel.test.ts ### API (api/__tests__/, api/translate/__tests__/, api/dataset-review/__tests__/) - api.test.ts (100% stmts), assistant.test.ts, datasetReview.test.ts - maintenance.test.ts, corrections.test.ts, datasources.test.ts - dictionaries.test.ts, jobs.test.ts, runs.test.ts, schedules.test.ts - useReviewSession.test.ts + useReviewSession.2.test.ts ### Auth (auth/__tests__/) - permissions.test.ts (95.2%→100%), store.test.ts - store.browser-off.test.ts (covers !browser guards) ### UI (ui/__tests__/) - EmptyState.test.ts, FeatureGate.test.ts, FeatureGate.2.test.ts - HelpTooltip.test.ts, Icon.test.ts, Input.test.ts, Select.test.ts - LanguageSwitcher.test.ts ### Top-level lib (lib/__tests__/) - cot-logger.test.ts, routes.test.ts, stores.test.ts - toasts.test.ts, utils.test.ts ### Helpers (helpers/__tests__/) - review-workspace-helpers.test.ts ### Source changes (minimal, non-breaking) - sidebar.svelte.ts: exported loadState() for testability - HelpTooltip.svelte: removed default () destructuring - vitest.config.js: coverage scope narrowed, thresholds enforced - package.json: fixed @vitest/coverage-v8 version mismatch
208 lines
9.2 KiB
TypeScript
208 lines
9.2 KiB
TypeScript
// #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
|