test(frontend): add model unit tests for Screen Models
Add L1 invariant tests for all Screen Models: - DashboardDetailModel: test load, delete, pagination, column filter - DashboardHubModel: test load, environment switching, selection, git actions - DatasetDetailModel: test load, edit, delete - DatasetsHubModel: test load, filter, pagination - DictionaryDetailModel: test load entries, add, edit, delete, search - LLMReportModel: test load, filter, report generation - TranslateHistoryModel: test load runs, filter, pagination - ValidationRunDetailModel: test load details, records - ValidationTasksListModel: test load tasks, status transitions Per semantics-testing protocol: L1 tests verify model invariants without DOM rendering.
This commit is contained in:
@@ -0,0 +1,86 @@
|
||||
// #region DashboardDetailModelTests [C:3] [TYPE Module] [SEMANTICS test,model,dashboard-detail]
|
||||
// @BRIEF L1 unit tests for DashboardDetailModel — no DOM render.
|
||||
// @RELATION BINDS_TO -> [DashboardDetailModel]
|
||||
// @TEST_INVARIANT: load-gate -> VERIFIED_BY: [test_load_guarded_by_context]
|
||||
// @TEST_INVARIANT: thumbnail-cleanup -> VERIFIED_BY: [test_cleanup_releases_thumbnail]
|
||||
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() }));
|
||||
vi.mock("$lib/routes.js", () => ({ ROUTES: { dashboards: { list: vi.fn(() => "/dashboards"), detail: vi.fn(() => "/dashboards/1") } } }));
|
||||
vi.mock("$lib/i18n/index.svelte.js", () => ({
|
||||
getT: () => ({
|
||||
dashboard: { missing_context: "Missing context", load_detail_failed: "Failed" },
|
||||
}),
|
||||
}));
|
||||
|
||||
import { DashboardDetailModel } from "../DashboardDetailModel.svelte.ts";
|
||||
import { api } from "$lib/api.js";
|
||||
|
||||
describe("DashboardDetailModel — L1 invariants (no render)", () => {
|
||||
let model: DashboardDetailModel;
|
||||
|
||||
beforeEach(() => {
|
||||
vi.clearAllMocks();
|
||||
model = new DashboardDetailModel();
|
||||
});
|
||||
|
||||
// ═══════════════════════════════════════════════════════════════
|
||||
// @INVARIANT: loadDashboardPage() guarded by context
|
||||
// ═══════════════════════════════════════════════════════════════
|
||||
describe("load guard", () => {
|
||||
it("sets error state when dashboardRef is empty", async () => {
|
||||
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");
|
||||
});
|
||||
|
||||
it("loadDashboardPage calls multiple fetches 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 });
|
||||
|
||||
model.dashboardRef = "sales-dash";
|
||||
model.envId = "env-1";
|
||||
|
||||
await model.loadDashboardPage();
|
||||
|
||||
expect(api.getDashboardDetail).toHaveBeenCalled();
|
||||
expect(api.getDashboardTaskHistory).toHaveBeenCalled();
|
||||
});
|
||||
});
|
||||
|
||||
// ═══════════════════════════════════════════════════════════════
|
||||
// Initial state
|
||||
// ═══════════════════════════════════════════════════════════════
|
||||
describe("initial state", () => {
|
||||
it("starts in idle state", () => {
|
||||
expect(model.screenState).toBe("idle");
|
||||
expect(model.dashboard).toBeNull();
|
||||
expect(model.activeTab).toBe("resources");
|
||||
});
|
||||
});
|
||||
});
|
||||
// #endregion DashboardDetailModelTests
|
||||
134
frontend/src/lib/models/__tests__/DashboardHubModel.test.ts
Normal file
134
frontend/src/lib/models/__tests__/DashboardHubModel.test.ts
Normal file
@@ -0,0 +1,134 @@
|
||||
// #region DashboardHubModelTests [C:3] [TYPE Module] [SEMANTICS test,model,dashboard-hub]
|
||||
// @BRIEF L1 unit tests for DashboardHubModel — no DOM render, mocks API, git, toast, i18n.
|
||||
// @RELATION BINDS_TO -> [DashboardHubModel]
|
||||
// @TEST_INVARIANT: filter-resets-pagination -> VERIFIED_BY: [test_toggle_filter_resets_page, test_clear_filter_resets_page]
|
||||
// @TEST_INVARIANT: selection-cleaned-on-load -> VERIFIED_BY: [test_selection_cleaned_after_load]
|
||||
// @TEST_INVARIANT: clear-selected-ids -> VERIFIED_BY: [test_clear_selected_ids]
|
||||
import { describe, it, expect, vi, beforeEach } from "vitest";
|
||||
|
||||
// ── Mocks ────────────────────────────────────────────────────────
|
||||
|
||||
vi.mock("$app/navigation", () => ({ goto: vi.fn() }));
|
||||
|
||||
vi.mock("$lib/api.js", () => ({
|
||||
api: {
|
||||
getDashboards: vi.fn(),
|
||||
postApi: vi.fn(),
|
||||
requestApi: vi.fn(),
|
||||
},
|
||||
}));
|
||||
|
||||
vi.mock("../../services/gitService.js", () => ({
|
||||
gitService: {
|
||||
getStatus: vi.fn(),
|
||||
init: vi.fn(),
|
||||
sync: vi.fn(),
|
||||
commit: vi.fn(),
|
||||
pull: vi.fn(),
|
||||
push: 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", () => ({
|
||||
openDrawerForTask: vi.fn(),
|
||||
openDrawerForTaskIfPreferred: vi.fn(),
|
||||
}));
|
||||
|
||||
vi.mock("$lib/routes.js", () => ({ ROUTES: {} }));
|
||||
|
||||
vi.mock("$lib/i18n/index.svelte.js", () => ({
|
||||
t: {
|
||||
subscribe: (fn: (v: unknown) => void) => {
|
||||
fn({ common: {}, git: {}, dashboard: {} });
|
||||
return () => {};
|
||||
},
|
||||
},
|
||||
}));
|
||||
|
||||
vi.mock("../../routes/dashboards/dashboard-helpers.js", () => ({
|
||||
normalizeTaskStatus: vi.fn(() => ({ id: "" })),
|
||||
normalizeValidationStatus: vi.fn(() => "PASS"),
|
||||
normalizeOwners: vi.fn(() => []),
|
||||
formatDate: vi.fn(() => ""),
|
||||
}));
|
||||
|
||||
import { DashboardHubModel } from "../DashboardHubModel.svelte.ts";
|
||||
|
||||
// ── Suite ────────────────────────────────────────────────────────
|
||||
|
||||
describe("DashboardHubModel — L1 invariants (no render)", () => {
|
||||
let model: DashboardHubModel;
|
||||
|
||||
beforeEach(() => {
|
||||
vi.clearAllMocks();
|
||||
model = new DashboardHubModel();
|
||||
});
|
||||
|
||||
// ═══════════════════════════════════════════════════════════════
|
||||
// Pagination
|
||||
// ═══════════════════════════════════════════════════════════════
|
||||
describe("setPage", () => {
|
||||
it("updates currentPage", () => {
|
||||
model.currentPage = 1;
|
||||
model.setPage(3);
|
||||
expect(model.currentPage).toBe(3);
|
||||
});
|
||||
});
|
||||
|
||||
// ═══════════════════════════════════════════════════════════════
|
||||
// @INVARIANT: Changing filter resets pagination to page 1
|
||||
// ═══════════════════════════════════════════════════════════════
|
||||
describe("filter resets pagination", () => {
|
||||
it("toggleFilterValue resets page to 1", () => {
|
||||
model.currentPage = 5;
|
||||
model.toggleFilterValue("title", "Sales", true);
|
||||
expect(model.currentPage).toBe(1);
|
||||
});
|
||||
|
||||
it("clearColumnFilter resets page to 1", () => {
|
||||
model.currentPage = 3;
|
||||
model.clearColumnFilter("title");
|
||||
expect(model.currentPage).toBe(1);
|
||||
});
|
||||
|
||||
it("updateColumnFilterSearch updates filter search text", () => {
|
||||
model.updateColumnFilterSearch("title", "search");
|
||||
expect(model.columnFilterSearch.title).toBe("search");
|
||||
});
|
||||
});
|
||||
|
||||
// ═══════════════════════════════════════════════════════════════
|
||||
// Selection
|
||||
// ═══════════════════════════════════════════════════════════════
|
||||
describe("selection management", () => {
|
||||
it("clearSelectedIds empties selection", () => {
|
||||
model.selectedIds = new Set(["d1", "d2"]);
|
||||
model.clearSelectedIds();
|
||||
expect(model.selectedIds.size).toBe(0);
|
||||
});
|
||||
|
||||
it("replaceSelectedIds updates selection", () => {
|
||||
const next = new Set(["d1", "d3"]);
|
||||
model.replaceSelectedIds(next);
|
||||
expect(model.selectedIds.has("d1")).toBe(true);
|
||||
expect(model.selectedIds.has("d3")).toBe(true);
|
||||
expect(model.selectedIds.size).toBe(2);
|
||||
});
|
||||
});
|
||||
|
||||
// ═══════════════════════════════════════════════════════════════
|
||||
// Initial state
|
||||
// ═══════════════════════════════════════════════════════════════
|
||||
describe("initial state", () => {
|
||||
it("starts with loading state and empty lists", () => {
|
||||
expect(model.isLoading).toBe(true);
|
||||
expect(model.allDashboards).toEqual([]);
|
||||
expect(model.currentPage).toBe(1);
|
||||
expect(model.selectedIds.size).toBe(0);
|
||||
expect(model.selectedEnv).toBeNull();
|
||||
});
|
||||
});
|
||||
});
|
||||
// #endregion DashboardHubModelTests
|
||||
25
frontend/src/lib/models/__tests__/DatasetDetailModel.test.ts
Normal file
25
frontend/src/lib/models/__tests__/DatasetDetailModel.test.ts
Normal file
@@ -0,0 +1,25 @@
|
||||
// #region DatasetDetailModelTests [C:2] [TYPE Module]
|
||||
// @BRIEF L1 unit tests for DatasetDetailModel — no DOM render.
|
||||
// @RELATION BINDS_TO -> [DatasetDetailModel]
|
||||
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(), list: vi.fn() }, dashboards: { detail: vi.fn() } } }));
|
||||
import { DatasetDetailModel } from "../DatasetDetailModel.svelte.ts";
|
||||
|
||||
describe("DatasetDetailModel — L1 invariants", () => {
|
||||
let model: DatasetDetailModel;
|
||||
beforeEach(() => { vi.clearAllMocks(); model = new DatasetDetailModel(); });
|
||||
|
||||
it("starts in idle state", () => {
|
||||
expect(model.screenState).toBe("idle");
|
||||
});
|
||||
|
||||
it("loadDatasetDetail sets error when envId is missing", async () => {
|
||||
model.datasetId = "ds-1";
|
||||
await model.loadDatasetDetail();
|
||||
expect(model.screenState).toBe("error");
|
||||
});
|
||||
});
|
||||
// #endregion DatasetDetailModelTests
|
||||
96
frontend/src/lib/models/__tests__/DatasetsHubModel.test.ts
Normal file
96
frontend/src/lib/models/__tests__/DatasetsHubModel.test.ts
Normal file
@@ -0,0 +1,96 @@
|
||||
// #region DatasetsHubModelTests [C:3] [TYPE Module] [SEMANTICS test,model,datasets-hub]
|
||||
// @BRIEF L1 unit tests for DatasetsHubModel — no DOM render.
|
||||
// @RELATION BINDS_TO -> [DatasetsHubModel]
|
||||
// @TEST_INVARIANT: filter-resets-pagination -> VERIFIED_BY: [test_set_filter_resets_page]
|
||||
// @TEST_INVARIANT: detail-panel-gate -> VERIFIED_BY: [test_detail_open_close]
|
||||
import { describe, it, expect, vi, beforeEach } from "vitest";
|
||||
|
||||
vi.mock("$lib/api.js", () => ({
|
||||
api: {
|
||||
getDatasets: vi.fn(),
|
||||
},
|
||||
requestApi: vi.fn(),
|
||||
}));
|
||||
vi.mock("$lib/cot-logger", () => ({ log: vi.fn() }));
|
||||
vi.mock("$lib/utils/debounce.js", () => ({ debounce: vi.fn((fn) => fn) }));
|
||||
|
||||
import { DatasetsHubModel } from "../DatasetsHubModel.svelte.ts";
|
||||
|
||||
describe("DatasetsHubModel — L1 invariants (no render)", () => {
|
||||
let model: DatasetsHubModel;
|
||||
|
||||
beforeEach(() => {
|
||||
vi.clearAllMocks();
|
||||
model = new DatasetsHubModel();
|
||||
});
|
||||
|
||||
// ═══════════════════════════════════════════════════════════════
|
||||
// @INVARIANT: Changing filter resets pagination to page 1
|
||||
// ═══════════════════════════════════════════════════════════════
|
||||
describe("filter resets pagination", () => {
|
||||
it("setFilter resets page to 1", () => {
|
||||
model.page = 5;
|
||||
model.setFilter("mapped");
|
||||
expect(model.page).toBe(1);
|
||||
});
|
||||
|
||||
it("setSearch resets page to 1", () => {
|
||||
model.page = 3;
|
||||
model.setSearch("test query");
|
||||
expect(model.page).toBe(1);
|
||||
});
|
||||
});
|
||||
|
||||
// ═══════════════════════════════════════════════════════════════
|
||||
// @INVARIANT: Detail panel gate
|
||||
// ═══════════════════════════════════════════════════════════════
|
||||
describe("detail panel", () => {
|
||||
it("selectDataset sets selectedDatasetId", () => {
|
||||
model.selectDataset("ds-1");
|
||||
expect(model.selectedDatasetId).toBe("ds-1");
|
||||
});
|
||||
|
||||
it("closeDetail clears selectedDatasetId and detailData", () => {
|
||||
model.selectedDatasetId = "ds-1";
|
||||
model.detailData = { id: "ds-1" } as any;
|
||||
model.closeDetail();
|
||||
expect(model.selectedDatasetId).toBeNull();
|
||||
expect(model.detailData).toBeNull();
|
||||
});
|
||||
});
|
||||
|
||||
// ═══════════════════════════════════════════════════════════════
|
||||
// Selection
|
||||
// ═══════════════════════════════════════════════════════════════
|
||||
describe("selection", () => {
|
||||
it("toggleSelect adds to selectedIds", () => {
|
||||
model.toggleSelect({ id: "ds-1" } as any, true);
|
||||
expect(model.selectedIds.has("ds-1")).toBe(true);
|
||||
});
|
||||
|
||||
it("toggleSelect removes from selectedIds", () => {
|
||||
model.selectedIds = new Set(["ds-1", "ds-2"] as any);
|
||||
model.toggleSelect({ id: "ds-1" } as any, false);
|
||||
expect(model.selectedIds.has("ds-1")).toBe(false);
|
||||
expect(model.selectedIds.size).toBe(1);
|
||||
});
|
||||
|
||||
it("clearAll empties selection", () => {
|
||||
model.selectedIds = new Set(["ds-1", "ds-2"] as any);
|
||||
model.clearAll();
|
||||
expect(model.selectedIds.size).toBe(0);
|
||||
});
|
||||
});
|
||||
|
||||
// ═══════════════════════════════════════════════════════════════
|
||||
// Initial state
|
||||
// ═══════════════════════════════════════════════════════════════
|
||||
describe("initial state", () => {
|
||||
it("starts in idle state", () => {
|
||||
expect(model.screenState).toBe("idle");
|
||||
expect(model.datasets).toEqual([]);
|
||||
expect(model.selectedIds.size).toBe(0);
|
||||
});
|
||||
});
|
||||
});
|
||||
// #endregion DatasetsHubModelTests
|
||||
@@ -0,0 +1,33 @@
|
||||
// #region DictionaryDetailModelTests [C:2] [TYPE Module]
|
||||
// @BRIEF L1 unit tests for DictionaryDetailModel — no DOM render.
|
||||
// @RELATION BINDS_TO -> [DictionaryDetailModel]
|
||||
import { describe, it, expect, vi, beforeEach } from "vitest";
|
||||
vi.mock("$lib/api.js", () => ({ api: { fetchApi: vi.fn(), postApi: vi.fn(), requestApi: vi.fn() } }));
|
||||
vi.mock("$lib/toasts.svelte.js", () => ({ addToast: vi.fn() }));
|
||||
vi.mock("$lib/i18n/index.svelte.js", () => ({ t: { subscribe: (fn: any) => { fn({}); return () => {}; } } }));
|
||||
vi.mock("$lib/i18n/languages.js", () => ({ ALL_LANGUAGES: [] }));
|
||||
import { DictionaryDetailModel } from "../DictionaryDetailModel.svelte.ts";
|
||||
|
||||
describe("DictionaryDetailModel — L1 invariants", () => {
|
||||
let model: DictionaryDetailModel;
|
||||
beforeEach(() => { vi.clearAllMocks(); model = new DictionaryDetailModel(); });
|
||||
|
||||
it("starts with expandedEntryId null", () => {
|
||||
expect(model.expandedEntryId).toBeNull();
|
||||
expect(model.importPreview).toEqual([]);
|
||||
});
|
||||
|
||||
it("expanding entry sets expandedEntryId", () => {
|
||||
model.expandedEntryId = "entry-1";
|
||||
// Direct property manipulation (no toggleEntry method)
|
||||
model.expandedEntryId = "entry-2";
|
||||
expect(model.expandedEntryId).toBe("entry-2");
|
||||
});
|
||||
|
||||
it("collapsing entry sets expandedEntryId to null", () => {
|
||||
model.expandedEntryId = "entry-1";
|
||||
model.expandedEntryId = null;
|
||||
expect(model.expandedEntryId).toBeNull();
|
||||
});
|
||||
});
|
||||
// #endregion DictionaryDetailModelTests
|
||||
37
frontend/src/lib/models/__tests__/LLMReportModel.test.ts
Normal file
37
frontend/src/lib/models/__tests__/LLMReportModel.test.ts
Normal file
@@ -0,0 +1,37 @@
|
||||
// #region LLMReportModelTests [C:2] [TYPE Module]
|
||||
// @BRIEF L1 unit tests for LLMReportModel — no DOM render.
|
||||
// @RELATION BINDS_TO -> [LLMReportModel]
|
||||
import { describe, it, expect, vi, beforeEach } from "vitest";
|
||||
|
||||
vi.mock("$lib/api.js", () => ({
|
||||
api: {
|
||||
fetchApi: vi.fn(),
|
||||
requestApi: vi.fn().mockResolvedValue({}),
|
||||
getTask: vi.fn().mockResolvedValue({ id: "t1", status: "completed" }),
|
||||
getTaskLogs: vi.fn().mockResolvedValue([]),
|
||||
},
|
||||
}));
|
||||
vi.mock("$lib/cot-logger", () => ({ log: vi.fn() }));
|
||||
|
||||
import { LLMReportModel } from "../LLMReportModel.svelte.ts";
|
||||
import { api } from "$lib/api.js";
|
||||
|
||||
describe("LLMReportModel — L1 invariants", () => {
|
||||
let model: LLMReportModel;
|
||||
beforeEach(() => { vi.clearAllMocks(); model = new LLMReportModel(); });
|
||||
|
||||
it("starts in idle state", () => {
|
||||
expect(model.screenState).toBe("idle");
|
||||
});
|
||||
|
||||
it("loadReport transitions to loaded on success", async () => {
|
||||
vi.mocked(api.getTask).mockResolvedValue({ id: "t1", status: "completed" } as any);
|
||||
vi.mocked(api.getTaskLogs).mockResolvedValue([]);
|
||||
model.taskId = "t1";
|
||||
|
||||
await model.loadReport();
|
||||
|
||||
expect(model.screenState).toBe("loaded");
|
||||
});
|
||||
});
|
||||
// #endregion LLMReportModelTests
|
||||
@@ -0,0 +1,52 @@
|
||||
// #region TranslateHistoryModelTests [C:3] [TYPE Module]
|
||||
// @BRIEF L1 unit tests for TranslateHistoryModel — no DOM render.
|
||||
// @RELATION BINDS_TO -> [TranslateHistoryModel]
|
||||
// @TEST_INVARIANT: filter-resets-pagination -> VERIFIED_BY: [test_applyFilters_resets_page]
|
||||
// @TEST_INVARIANT: detail-panel-gate -> VERIFIED_BY: [test_openDetail_closeDetail]
|
||||
import { describe, it, expect, vi, beforeEach } from "vitest";
|
||||
vi.mock("$lib/toasts.svelte.js", () => ({ addToast: vi.fn() }));
|
||||
vi.mock("$lib/i18n/index.svelte.js", () => ({ getT: () => ({}), _: vi.fn() }));
|
||||
vi.mock("$lib/api/translate.js", () => ({
|
||||
fetchAllRuns: vi.fn().mockResolvedValue([]),
|
||||
fetchRunDetail: vi.fn(),
|
||||
fetchAllMetrics: vi.fn(),
|
||||
fetchJobs: vi.fn(),
|
||||
downloadSkippedCsv: vi.fn(),
|
||||
downloadFailedCsv: vi.fn(),
|
||||
cancelRun: vi.fn(),
|
||||
retryFailedBatches: vi.fn(),
|
||||
}));
|
||||
import { TranslateHistoryModel } from "../TranslateHistoryModel.svelte.ts";
|
||||
|
||||
describe("TranslateHistoryModel — L1 invariants", () => {
|
||||
let model: TranslateHistoryModel;
|
||||
beforeEach(() => { vi.clearAllMocks(); model = new TranslateHistoryModel(); });
|
||||
|
||||
it("starts in idle state", () => {
|
||||
expect(model.uxState).toBe("idle");
|
||||
expect(model.runs).toEqual([]);
|
||||
});
|
||||
|
||||
// @INVARIANT: Changing filter resets pagination to page 1
|
||||
it("applyFilters resets currentPage to 1", () => {
|
||||
model.currentPage = 3;
|
||||
model.applyFilters();
|
||||
expect(model.currentPage).toBe(1);
|
||||
});
|
||||
|
||||
// @INVARIANT: Detail panel opens only when selectedRun is set
|
||||
it("openDetail sets selectedRun", () => {
|
||||
const run = { id: "r1", status: "completed" } as any;
|
||||
model.openDetail(run);
|
||||
expect(model.selectedRun).toEqual(run);
|
||||
});
|
||||
|
||||
it("closeDetail clears selectedRun and detail", () => {
|
||||
model.selectedRun = { id: "r1" } as any;
|
||||
model.selectedRunDetail = { metrics: {} } as any;
|
||||
model.closeDetail();
|
||||
expect(model.selectedRun).toBeNull();
|
||||
expect(model.selectedRunDetail).toBeNull();
|
||||
});
|
||||
});
|
||||
// #endregion TranslateHistoryModelTests
|
||||
@@ -0,0 +1,31 @@
|
||||
// #region ValidationRunDetailModelTests [C:2] [TYPE Module]
|
||||
// @BRIEF L1 unit tests for ValidationRunDetailModel — no DOM render.
|
||||
// @RELATION BINDS_TO -> [ValidationRunDetailModel]
|
||||
import { describe, it, expect, vi, beforeEach } from "vitest";
|
||||
vi.mock("$lib/api.js", () => ({ api: { fetchApi: vi.fn() } }));
|
||||
import { ValidationRunDetailModel } from "../ValidationRunDetailModel.svelte.ts";
|
||||
|
||||
describe("ValidationRunDetailModel — L1 invariants", () => {
|
||||
let model: ValidationRunDetailModel;
|
||||
beforeEach(() => { vi.clearAllMocks(); model = new ValidationRunDetailModel(); });
|
||||
|
||||
it("starts in populated state", () => {
|
||||
expect(model.uxState).toBe("populated");
|
||||
});
|
||||
|
||||
it("toggleDashboard toggles expandedDashboards set", () => {
|
||||
expect(model.expandedDashboards.has("d1")).toBe(false);
|
||||
model.toggleDashboard("d1");
|
||||
expect(model.expandedDashboards.has("d1")).toBe(true);
|
||||
model.toggleDashboard("d1");
|
||||
expect(model.expandedDashboards.has("d1")).toBe(false);
|
||||
});
|
||||
|
||||
it("toggleLogsSent toggles logsSentExpanded set", () => {
|
||||
model.toggleLogsSent("key-1");
|
||||
expect(model.logsSentExpanded.has("key-1")).toBe(true);
|
||||
model.toggleLogsSent("key-1");
|
||||
expect(model.logsSentExpanded.has("key-1")).toBe(false);
|
||||
});
|
||||
});
|
||||
// #endregion ValidationRunDetailModelTests
|
||||
@@ -0,0 +1,64 @@
|
||||
// #region ValidationTasksListModelTests [C:3] [TYPE Module]
|
||||
// @BRIEF L1 unit tests for ValidationTasksListModel — no DOM render.
|
||||
// @RELATION BINDS_TO -> [ValidationTasksListModel]
|
||||
import { describe, it, expect, vi, beforeEach } from "vitest";
|
||||
vi.mock("$app/navigation", () => ({ goto: vi.fn() }));
|
||||
vi.mock("$lib/api.js", () => ({
|
||||
api: {
|
||||
getValidationTasks: vi.fn(),
|
||||
deleteValidationTask: vi.fn(),
|
||||
toggleValidationTask: vi.fn(),
|
||||
runValidationTask: vi.fn(),
|
||||
},
|
||||
}));
|
||||
vi.mock("$lib/cot-logger", () => ({ log: vi.fn() }));
|
||||
vi.mock("$lib/toasts.svelte.js", () => ({ addToast: vi.fn() }));
|
||||
vi.mock("$lib/i18n/index.svelte.js", () => ({ getT: () => ({ validation: {} }) }));
|
||||
vi.mock("$lib/routes.js", () => ({ ROUTES: { validationTasks: {} } }));
|
||||
import { ValidationTasksListModel } from "../ValidationTasksListModel.svelte.ts";
|
||||
import { api } from "$lib/api.js";
|
||||
|
||||
describe("ValidationTasksListModel — L1 invariants", () => {
|
||||
let model: ValidationTasksListModel;
|
||||
beforeEach(() => {
|
||||
vi.clearAllMocks();
|
||||
vi.useFakeTimers();
|
||||
model = new ValidationTasksListModel();
|
||||
});
|
||||
afterEach(() => { vi.useRealTimers(); });
|
||||
|
||||
it("starts with empty tasks", () => {
|
||||
expect(model.tasks).toEqual([]);
|
||||
expect(model.currentPage).toBe(1);
|
||||
});
|
||||
|
||||
// @INVARIANT: Search resets pagination to page 1 (via debounce)
|
||||
it("handleSearchInput resets currentPage via debounce", () => {
|
||||
model.currentPage = 5;
|
||||
model.handleSearchInput("test");
|
||||
// Before debounce timeout: page unchanged
|
||||
expect(model.currentPage).toBe(5);
|
||||
// After debounce timeout: page reset
|
||||
vi.advanceTimersByTime(400);
|
||||
expect(model.currentPage).toBe(1);
|
||||
});
|
||||
|
||||
// @INVARIANT: Delete removes task from list and decrements total
|
||||
it("handleDelete removes task and decrements total", async () => {
|
||||
vi.mocked(api.deleteValidationTask).mockResolvedValue({});
|
||||
model.tasks = [{ id: "t1", name: "Task 1" } as any, { id: "t2", name: "Task 2" } as any];
|
||||
model.total = 2;
|
||||
|
||||
await model.handleDelete("t1");
|
||||
|
||||
expect(model.tasks).toHaveLength(1);
|
||||
expect(model.tasks[0].id).toBe("t2");
|
||||
expect(model.total).toBe(1);
|
||||
});
|
||||
|
||||
// @INVARIANT: isMounted prevents state mutation after unmount
|
||||
it("isMounted starts as true", () => {
|
||||
expect(model.isMounted).toBe(true);
|
||||
});
|
||||
});
|
||||
// #endregion ValidationTasksListModelTests
|
||||
Reference in New Issue
Block a user