// #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((k) => k) })); 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"; import { addToast } from "$lib/toasts.svelte.js"; import { fetchAllRuns, fetchRunDetail, fetchAllMetrics, fetchJobs, downloadSkippedCsv, downloadFailedCsv, cancelRun, retryFailedBatches, } from "$lib/api/translate.js"; // #region TranslateHistoryModel.InvariantTests [C:2] [TYPE Function] describe("TranslateHistoryModel — L1 invariants", () => { let model: TranslateHistoryModel; beforeEach(() => { vi.clearAllMocks(); model = new TranslateHistoryModel(); }); // #region TranslateHistoryModel.InitialState [C:2] [TYPE Test] it("starts in idle state", () => { expect(model.uxState).toBe("idle"); expect(model.runs).toEqual([]); }); // #endregion // #region TranslateHistoryModel.ApplyFilters [C:2] [TYPE Test] it("applyFilters resets currentPage to 1", () => { model.currentPage = 3; model.applyFilters(); expect(model.currentPage).toBe(1); }); // #endregion // #region TranslateHistoryModel.DetailPanel [C:2] [TYPE Test] it("openDetail sets selectedRun and loads detail", async () => { vi.mocked(fetchRunDetail).mockResolvedValue({ id: "r1", metrics: {} }); const run = { id: "r1", status: "completed" }; await model.openDetail(run); expect(model.selectedRun).toEqual(run); expect(model.uxState).toBe("detail_open"); }); it("openDetail handles API error", async () => { vi.mocked(fetchRunDetail).mockRejectedValue(new Error("Detail failed")); await model.openDetail({ id: "r1" }); expect(addToast).toHaveBeenCalled(); }); 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(); expect(model.uxState).toBe("empty"); }); it("closeDetail restores populated uxState when runs exist", () => { model.selectedRun = { id: "r1" } as any; model.selectedRunDetail = { metrics: {} } as any; model.runs = [{ id: "r1" }] as any; model.closeDetail(); expect(model.uxState).toBe("populated"); }); // #endregion }); // #endregion // #region TranslateHistoryModel.DataLoading [C:2] [TYPE Function] describe("TranslateHistoryModel — Data Loading", () => { let model: TranslateHistoryModel; beforeEach(() => { vi.clearAllMocks(); model = new TranslateHistoryModel(); }); // #region TranslateHistoryModel.LoadRuns [C:2] [TYPE Test] it("loadRuns sets loading state and populates runs", async () => { vi.mocked(fetchAllRuns).mockResolvedValue({ items: [{ id: "r1" }, { id: "r2" }], total: 2 }); model.filterDateFrom = "2026-07-01"; model.filterDateTo = "2026-07-08"; const promise = model.loadRuns(); expect(model.isLoading).toBe(true); expect(model.uxState).toBe("loading"); await promise; expect(fetchAllRuns).toHaveBeenCalledWith(expect.objectContaining({ date_from: "2026-07-01", date_to: "2026-07-08", })); expect(model.runs).toHaveLength(2); expect(model.total).toBe(2); expect(model.uxState).toBe("populated"); expect(model.isLoading).toBe(false); }); it("loadRuns sets empty state when no results", async () => { vi.mocked(fetchAllRuns).mockResolvedValue({ items: [], total: 0 }); await model.loadRuns(); expect(model.uxState).toBe("empty"); }); it("loadRuns handles API error", async () => { vi.mocked(fetchAllRuns).mockRejectedValue(new Error("Load failed")); await model.loadRuns(); expect(model.uxState).toBe("empty"); expect(addToast).toHaveBeenCalled(); expect(model.isLoading).toBe(false); }); // #endregion // #region TranslateHistoryModel.LoadMetrics [C:2] [TYPE Test] it("loadMetrics populates metrics array", async () => { vi.mocked(fetchAllMetrics).mockResolvedValue([{ total_runs: 10, successful_runs: 8, failed_runs: 2, total_records: 100 }]); await model.loadMetrics(); expect(model.metrics).toHaveLength(1); }); it("loadMetrics silently ignores error", async () => { vi.mocked(fetchAllMetrics).mockRejectedValue(new Error("Metrics down")); await model.loadMetrics(); expect(model.metrics).toEqual([]); }); // #endregion // #region TranslateHistoryModel.LoadJobs [C:2] [TYPE Test] it("loadJobs accepts array result", async () => { vi.mocked(fetchJobs).mockResolvedValue([{ id: "j1", name: "Job 1" }]); await model.loadJobs(); expect(model.jobs).toHaveLength(1); }); it("loadJobs accepts items format result", async () => { vi.mocked(fetchJobs).mockResolvedValue({ items: [{ id: "j1", name: "Job 1" }] }); await model.loadJobs(); expect(model.jobs).toHaveLength(1); }); it("loadJobs fetches all pages when total exceeds first page", async () => { vi.mocked(fetchJobs) .mockResolvedValueOnce({ items: Array.from({ length: 100 }, (_, i) => ({ id: `j${i}` })), total: 101 }) .mockResolvedValueOnce({ items: [{ id: "j100", name: "Job 100" }], total: 101 }); await model.loadJobs(); expect(fetchJobs).toHaveBeenCalledWith({ page: 1, page_size: 100 }); expect(fetchJobs).toHaveBeenCalledWith({ page: 2, page_size: 100 }); expect(model.jobs).toHaveLength(101); }); it("loadJobs accepts results format result", async () => { vi.mocked(fetchJobs).mockResolvedValue({ results: [{ id: "j1", name: "Job 1" }] }); await model.loadJobs(); expect(model.jobs).toHaveLength(1); }); it("loadJobs silently ignores error", async () => { vi.mocked(fetchJobs).mockRejectedValue(new Error("Jobs down")); await model.loadJobs(); expect(model.jobs).toEqual([]); }); it("loadJobs handles response without items or results field", async () => { vi.mocked(fetchJobs).mockResolvedValue({ other_field: "no items" }); await model.loadJobs(); expect(model.jobs).toEqual([]); }); // #endregion // #region TranslateHistoryModel.NonErrorExceptions [C:2] [TYPE Test] it("loadRuns handles non-Error rejections", async () => { vi.mocked(fetchAllRuns).mockRejectedValue("string error"); await model.loadRuns(); expect(model.uxState).toBe("empty"); expect(model.isLoading).toBe(false); }); it("loadMetrics handles non-Error rejections", async () => { vi.mocked(fetchAllMetrics).mockRejectedValue("metrics error"); await model.loadMetrics(); expect(model.metrics).toEqual([]); }); it("loadJobs handles non-Error rejections", async () => { vi.mocked(fetchJobs).mockRejectedValue("jobs error"); await model.loadJobs(); expect(model.jobs).toEqual([]); }); // #endregion }); // #endregion // #region TranslateHistoryModel.RunActions [C:2] [TYPE Function] describe("TranslateHistoryModel — Run Actions", () => { let model: TranslateHistoryModel; beforeEach(() => { vi.clearAllMocks(); model = new TranslateHistoryModel(); }); // #region TranslateHistoryModel.CancelRun [C:2] [TYPE Test] it("handleCancelRun calls cancel API and refreshes runs", async () => { vi.mocked(cancelRun).mockResolvedValue({}); vi.mocked(fetchAllRuns).mockResolvedValue({ items: [], total: 0 }); await model.handleCancelRun("r1"); expect(cancelRun).toHaveBeenCalledWith("r1"); expect(model.cancellingRunId).toBeNull(); }); it("handleCancelRun handles error", async () => { vi.mocked(cancelRun).mockRejectedValue(new Error("Cancel failed")); await model.handleCancelRun("r1"); expect(addToast).toHaveBeenCalled(); expect(model.cancellingRunId).toBeNull(); }); it("handleCancelRun refreshes selectedRunDetail if matches", async () => { vi.mocked(cancelRun).mockResolvedValue({}); vi.mocked(fetchRunDetail).mockResolvedValue({ id: "r1", status: "cancelled" }); vi.mocked(fetchAllRuns).mockResolvedValue({ items: [], total: 0 }); model.selectedRunDetail = { id: "r1" } as any; await model.handleCancelRun("r1"); expect(fetchRunDetail).toHaveBeenCalled(); }); // #endregion // #region TranslateHistoryModel.RetryRun [C:2] [TYPE Test] it("handleRetryRun calls retry API and refreshes runs", async () => { vi.mocked(retryFailedBatches).mockResolvedValue({}); vi.mocked(fetchAllRuns).mockResolvedValue({ items: [], total: 0 }); await model.handleRetryRun("r1"); expect(retryFailedBatches).toHaveBeenCalledWith("r1"); expect(model.retryingRunId).toBeNull(); }); it("handleRetryRun handles error", async () => { vi.mocked(retryFailedBatches).mockRejectedValue(new Error("Retry failed")); await model.handleRetryRun("r1"); expect(addToast).toHaveBeenCalled(); expect(model.retryingRunId).toBeNull(); }); // #endregion // #region TranslateHistoryModel.Downloads [C:2] [TYPE Test] it("handleDownloadSkipped calls API", async () => { vi.mocked(downloadSkippedCsv).mockResolvedValue(undefined); await model.handleDownloadSkipped("r1"); expect(downloadSkippedCsv).toHaveBeenCalledWith("r1"); }); it("handleDownloadSkipped handles error", async () => { vi.mocked(downloadSkippedCsv).mockRejectedValue(new Error("Download failed")); await model.handleDownloadSkipped("r1"); expect(addToast).toHaveBeenCalled(); }); it("handleDownloadSkipped handles non-Error rejection", async () => { vi.mocked(downloadSkippedCsv).mockRejectedValue("string error"); await model.handleDownloadSkipped("r1"); expect(addToast).toHaveBeenCalled(); }); it("handleDownloadFailed calls API", async () => { vi.mocked(downloadFailedCsv).mockResolvedValue(undefined); await model.handleDownloadFailed("r1"); expect(downloadFailedCsv).toHaveBeenCalledWith("r1"); }); it("handleDownloadFailed handles error", async () => { vi.mocked(downloadFailedCsv).mockRejectedValue(new Error("Download failed")); await model.handleDownloadFailed("r1"); expect(addToast).toHaveBeenCalled(); }); it("handleDownloadFailed handles non-Error rejection", async () => { vi.mocked(downloadFailedCsv).mockRejectedValue("string error"); await model.handleDownloadFailed("r1"); expect(addToast).toHaveBeenCalled(); }); it("handleCancelRun handles non-Error rejection", async () => { vi.mocked(cancelRun).mockRejectedValue("cancel string error"); await model.handleCancelRun("r1"); expect(addToast).toHaveBeenCalled(); expect(model.cancellingRunId).toBeNull(); }); it("handleRetryRun handles non-Error rejection", async () => { vi.mocked(retryFailedBatches).mockRejectedValue("retry string error"); await model.handleRetryRun("r1"); expect(addToast).toHaveBeenCalled(); expect(model.retryingRunId).toBeNull(); }); it("openDetail handles non-Error rejection", async () => { vi.mocked(fetchRunDetail).mockRejectedValue("string error"); await model.openDetail({ id: "r1" }); expect(addToast).toHaveBeenCalled(); }); // #endregion }); // #endregion // #region TranslateHistoryModel.Utilities [C:2] [TYPE Function] describe("TranslateHistoryModel — Utilities", () => { let model: TranslateHistoryModel; beforeEach(() => { vi.clearAllMocks(); model = new TranslateHistoryModel(); }); // #region TranslateHistoryModel.GetStatusClass [C:2] [TYPE Test] it("getStatusClass returns correct classes for PENDING", () => { expect(model.getStatusClass("PENDING")).toContain("warning"); }); it("getStatusClass returns correct classes for COMPLETED", () => { expect(model.getStatusClass("COMPLETED")).toContain("success"); }); it("getStatusClass returns correct classes for FAILED", () => { expect(model.getStatusClass("FAILED")).toContain("destructive"); }); it("getStatusClass returns muted for unknown status", () => { expect(model.getStatusClass("UNKNOWN")).toContain("muted"); }); // #endregion // #region TranslateHistoryModel.GetJobName [C:2] [TYPE Test] it("getJobName returns job name when found", () => { model.jobs = [{ id: "j1", name: "My Job" }] as any; expect(model.getJobName("j1")).toBe("My Job"); }); it("getJobName returns truncated id when not found", () => { model.jobs = []; expect(model.getJobName("abcdef123456")).toBe("abcdef12..."); }); it("getJobName returns default for undefined", () => { const name = model.getJobName(undefined); expect(name).toContain("..."); }); // #endregion // #region TranslateHistoryModel.TotalMetric [C:2] [TYPE Test] it("totalMetric returns null when no metrics", () => { expect(model.totalMetric).toBeNull(); }); it("totalMetric aggregates metrics", () => { model.metrics = [ { total_runs: 10, successful_runs: 8, failed_runs: 2, total_records: 100 }, { total_runs: 5, successful_runs: 4, failed_runs: 1, total_records: 50 }, ] as any; expect(model.totalMetric.total_runs).toBe(15); expect(model.totalMetric.successful_runs).toBe(12); expect(model.totalMetric.failed_runs).toBe(3); expect(model.totalMetric.total_records).toBe(150); }); // #endregion // #region TranslateHistoryModel.StatusClassEdge [C:2] [TYPE Test] it("getStatusClass returns default for undefined status", () => { expect(model.getStatusClass(undefined)).toContain("muted"); }); it("getStatusClass returns default for empty string status", () => { expect(model.getStatusClass("")).toContain("muted"); }); // #endregion // #region TranslateHistoryModel.JobNameEdge [C:2] [TYPE Test] it("getJobName returns String(undefined) when job has no name", () => { model.jobs = [{ id: "j1" }] as any; // job is found but name is undefined => String(undefined) => "undefined" expect(model.getJobName("j1")).toBe("undefined"); }); it("getJobName returns empty for undefined job id", () => { expect(model.getJobName(undefined)).toBe("..."); }); // #endregion // #region TranslateHistoryModel.RunDetailNonMatch [C:2] [TYPE Test] it("handleCancelRun does not refresh detail when detail id differs", async () => { vi.mocked(cancelRun).mockResolvedValue({}); vi.mocked(fetchAllRuns).mockResolvedValue({ items: [], total: 0 }); model.selectedRunDetail = { id: "other-run" } as any; await model.handleCancelRun("r1"); // fetchRunDetail should NOT be called because selectedRunDetail.id !== runId expect(fetchRunDetail).not.toHaveBeenCalled(); }); it("handleRetryRun does not refresh detail when detail id differs", async () => { vi.mocked(retryFailedBatches).mockResolvedValue({}); vi.mocked(fetchAllRuns).mockResolvedValue({ items: [], total: 0 }); model.selectedRunDetail = { id: "other-run" } as any; await model.handleRetryRun("r1"); expect(fetchRunDetail).not.toHaveBeenCalled(); }); // #endregion }); // #endregion