Files
ss-tools/frontend/src/lib/models/__tests__/DatasetDetailModel.test.ts
busya 2b303f92b3 test: bring frontend test coverage to 98% across core lib modules
## 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
2026-06-10 14:59:40 +03:00

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