import { afterEach, beforeEach, describe, expect, it, vi } from "vitest"; import { flushPromises, mount } from "@vue/test-utils"; import HistoryDialog from "./CollaborationHistoryDialog.vue"; import type { CollaborationHistoryFile } from "../../api/collaborationHistory"; import { downloadLedgerRevision } from "../../api/ledgers"; import { prepareSaveFile } from "../../runtime"; import { saveFileWithFeedback } from "../../utils/fileTaskFeedback"; const state = vi.hoisted(() => ({ api: { file: vi.fn(), revisions: vi.fn(), name: vi.fn(), restore: vi.fn() }, resolve: vi.fn(() => ({ href: "/ledger-preview" })), prompt: vi.fn(), confirm: vi.fn(), })); vi.mock("../../api/collaborationHistory", () => ({ createCollaborationHistoryApi: () => state.api })); vi.mock("../../api/collaboration", () => ({ copyCollaborationRevision: vi.fn(), deleteCollaborationRevision: vi.fn() })); vi.mock("../../api/ledgers", () => ({ downloadLedgerRevision: vi.fn(), importLedger: vi.fn() })); vi.mock("../../runtime", () => ({ prepareSaveFile: vi.fn(), pickFiles: vi.fn() })); vi.mock("../../utils/fileTaskFeedback", () => ({ saveFileWithFeedback: vi.fn() })); vi.mock("../../store/auth", () => ({ useAuthStore: () => ({ user: { id: "u", full_name: "Owner" } }) })); vi.mock("vue-router", () => ({ useRouter: () => ({ resolve: state.resolve }) })); vi.mock("element-plus", () => ({ ElMessage: { error: vi.fn(), success: vi.fn() }, ElMessageBox: { prompt: state.prompt, confirm: state.confirm } })); vi.mock("./CollaborationSaveAsDialog.vue", () => ({ default: { template: "
" } })); const file: CollaborationHistoryFile = { id: "ledger-a", title: "台账.xlsx", extension: "xlsx", file_type: "cell", status: "ACTIVE", generation: 7, can_manage: true, can_edit: true, can_export: true, current_revision_id: "r2", owner_name: "Owner" }; const revisions = [ { id: "r2", revision_no: 2, source: "FORCE_SAVE", created_at: "2026-09-03T01:00:00Z", file_size: 200, mime_type: "application/xlsx", change_summary: "", created_by_name: "Owner" }, { id: "r1", revision_no: 1, source: "IMPORT", created_at: "2026-09-02T01:00:00Z", file_size: 100, mime_type: "application/xlsx", change_summary: "提交版", created_by_name: "Owner" }, ]; let wrapper: ReturnType | undefined; const render = (overrides = {}, scope: "project" | "ledger" = "ledger") => { const current = { ...file, ...overrides }; state.api.file.mockResolvedValue({ data: current }); wrapper = mount(HistoryDialog, { props: { file: current, target: scope === "ledger" ? { scope } : { scope, studyId: "study-a" }, canCreate: true }, global: { directives: { loading: () => {} }, stubs: { ElDialog: { props: ["modelValue"], template: '
' }, ElButton: { template: '' }, ElAvatar: { template: '' }, ElTag: { template: '' }, ElIcon: { template: '' }, ElTooltip: { template: '' }, ElEmpty: true, ElDropdown: { name: "ElDropdown", emits: ["command"], template: '
' }, ElDropdownMenu: { template: '
' }, ElDropdownItem: { template: '' }, ElSelect: { props: ["modelValue"], emits: ["update:modelValue"], template: '' }, ElOption: { props: ["value", "label"], template: '' }, ElSwitch: { props: ["modelValue"], emits: ["update:modelValue"], template: '' }, }, } }); return wrapper; }; describe("shared project and ledger history", () => { beforeEach(() => { vi.clearAllMocks(); vi.useFakeTimers(); state.api.revisions.mockResolvedValue({ data: revisions.map(item => ({ ...item })) }); state.prompt.mockResolvedValue({ value: "复核版" }); state.confirm.mockResolvedValue("confirm"); }); afterEach(() => { wrapper?.unmount(); wrapper = undefined; vi.useRealTimers(); vi.restoreAllMocks(); }); it.each(["project", "ledger"] as const)("filters and names versions in the same %s history UI", async (scope) => { const page = render({}, scope); await flushPromises(); expect(page.findAll(".history-day-group")).toHaveLength(2); await page.get(".history-described-filter input").setValue(true); expect(page.findAll(".history-row")).toHaveLength(1); await page.get(".history-source-filter").setValue("FORCE_SAVE"); expect(page.findAll(".history-row")).toHaveLength(0); await page.get(".history-described-filter input").setValue(false); state.api.name.mockResolvedValue({ data: { ...revisions[0], change_summary: "复核版" } }); await page.findAll(".history-row button").find(button => button.text() === "命名")!.trigger("click"); await flushPromises(); expect(state.api.name).toHaveBeenCalledWith("ledger-a", "r2", { change_summary: "复核版" }); expect(page.get(".history-description").text()).toContain("复核版"); }); it("preserves ledger no-delete and manager-only restore while using the current generation", async () => { const editor = render({ can_manage: false, can_export: false }); await flushPromises(); expect(editor.text()).toContain("命名"); expect(editor.text()).not.toContain("恢复此版本"); expect(editor.text()).not.toContain("另存为"); editor.unmount(); const manager = render(); await flushPromises(); expect(manager.text()).not.toContain("删除版本"); const restored = { ...revisions[0], id: "r3", revision_no: 3, source: "RESTORE" }; state.api.restore.mockResolvedValue({ data: restored }); state.api.revisions.mockResolvedValue({ data: [restored, ...revisions] }); state.api.file.mockResolvedValue({ data: { ...file, generation: 8, current_revision_id: "r3" } }); manager.findComponent({ name: "ElDropdown" }).vm.$emit("command", "restore"); // Emit the same command as the real row menu with its bound revision. await flushPromises(); expect(state.api.restore).toHaveBeenCalledWith(expect.objectContaining({ generation: 7 }), "r1"); expect(manager.findAll(".history-row")).toHaveLength(3); expect(manager.get(".history-row.is-current").text()).toContain("历史恢复"); }); it("opens a global read-only preview and saves a historical XLSX without project context", async () => { const open = vi.spyOn(window, "open").mockReturnValue(null); const page = render(); await flushPromises(); await page.findAll(".history-row button").find(button => button.text() === "预览")!.trigger("click"); expect(state.resolve).toHaveBeenCalledWith({ name: "LedgerRevisionPreview", params: { fileId: "ledger-a", id: "r2" } }); expect(open).toHaveBeenCalledWith("/ledger-preview", "_blank", "noopener,noreferrer"); const destination = { kind: "web-download" }; vi.mocked(prepareSaveFile).mockResolvedValue(destination as any); const blob = new Blob(["historical bytes"]); vi.mocked(downloadLedgerRevision).mockResolvedValue({ data: blob } as any); await page.findAll(".history-row button").find(button => button.text() === "另存为")!.trigger("click"); await flushPromises(); expect(downloadLedgerRevision).toHaveBeenCalledWith("ledger-a", "r2"); expect(saveFileWithFeedback).toHaveBeenCalledWith(expect.objectContaining({ data: blob, suggestedName: expect.stringMatching(/台账\[.*\]\.xlsx$/) }), expect.anything(), destination); }); });