import { afterEach, beforeEach, describe, expect, it, vi } from "vitest"; import { flushPromises, mount } from "@vue/test-utils"; import AccessDialog from "./CollaborationAccessDialog.vue"; import { createCollaborationAccessApi, type CollaborationAccessFile, type CollaborationAccessTarget } from "../../api/collaborationAccess"; const api = vi.hoisted(() => ({ file: vi.fn(), members: vi.fn(), candidates: vi.fn(), requests: vi.fn(), share: vi.fn(), setMember: vi.fn(), removeMember: vi.fn(), updateFile: vi.fn(), updateShare: vi.fn(), resolveRequest: vi.fn(), transfer: vi.fn(), })); vi.mock("../../api/collaborationAccess", () => ({ createCollaborationAccessApi: vi.fn(() => api) })); vi.mock("../../runtime", () => ({ clientRuntime: { apiBaseUrl: () => "http://localhost:8888" } })); vi.mock("../../composables/useProjectNotifications", () => ({ notifyProjectNotificationsChanged: vi.fn() })); vi.mock("../../utils/apiErrorMessage", () => ({ getApiErrorMessage: async (_error: unknown, fallback: string) => fallback })); const file: CollaborationAccessFile = { id: "file-a", title: "台账.xlsx", owner_id: "owner", owner_name: "Owner", file_type: "cell", status: "ACTIVE", generation: 4, can_manage: true, can_transfer_ownership: true, allow_export: false, allow_edit_request: true, allow_sheet_structure_edit: false, }; const owner = { user_id: "owner", full_name: "Owner", email: "owner@example.com", role: "MANAGER", protected_label: "所有者" }; const candidate = { user_id: "candidate", full_name: "Candidate", email: "candidate@example.com", role_in_study: "系统账号", can_be_editor: true, can_be_manager: true }; const share = { enabled: false, has_password: false, access_mode: "VIEW", expiry_policy: "SEVEN_DAYS", share_path: "/collaboration/share", share_token: null }; let wrapper: ReturnType | undefined; const render = (target: CollaborationAccessTarget = { scope: "ledger" }) => { wrapper = mount(AccessDialog, { props: { file, target }, global: { directives: { loading: () => {} }, stubs: { ElDialog: { props: ["modelValue", "title"], template: '
{{ title }}
' }, ElButton: { template: '' }, ElAvatar: { template: '' }, ElTag: { template: '' }, ElIcon: { template: '' }, ElEmpty: true, ElInput: true, ElSelect: { props: ["modelValue"], emits: ["update:modelValue", "change"], template: '' }, ElOption: { props: ["label", "value"], template: '' }, ElSwitch: { props: ["modelValue", "disabled"], emits: ["update:modelValue", "change"], template: '' }, }, } }); return wrapper; }; describe("shared collaboration access dialog", () => { beforeEach(() => { vi.clearAllMocks(); api.file.mockResolvedValue({ data: { ...file } }); api.members.mockResolvedValue({ data: [owner] }); api.candidates.mockResolvedValue({ data: [candidate] }); api.requests.mockResolvedValue({ data: [] }); api.share.mockResolvedValue({ data: share }); api.setMember.mockResolvedValue({}); }); afterEach(() => { wrapper?.unmount(); wrapper = undefined; }); it.each(["ledger", "project"] as const)("uses the same picker with the %s account directory and role boundary", async (scope) => { const target: CollaborationAccessTarget = scope === "project" ? { scope, studyId: "study-a" } : { scope }; const page = render(target); await flushPromises(); expect(createCollaborationAccessApi).toHaveBeenCalledWith(target); expect(page.get(".access-member-row").text()).toContain("所有者"); expect(page.find(".access-member-remove").exists()).toBe(false); expect(page.get('[aria-label="启用匿名访问"]').element).toHaveProperty("checked", false); await page.get(".access-section__manage-button").trigger("click"); expect(page.get('[role="listbox"]').attributes("aria-label")).toBe(scope === "ledger" ? "可添加的系统账号" : "可添加的项目成员"); const select = page.get('[aria-label="文件授权角色"]'); expect(select.find('option[value="VIEWER"]').exists()).toBe(scope === "ledger"); if (scope === "ledger") await select.setValue("VIEWER"); await page.get(".member-picker-contact").trigger("click"); await page.findAll(".member-picker-selection__footer button").find((button) => button.text() === "确定")!.trigger("click"); await flushPromises(); expect(api.setMember).toHaveBeenCalledWith("file-a", { user_id: "candidate", role: scope === "ledger" ? "VIEWER" : "EDITOR" }); expect(page.emitted("changed")).toBeTruthy(); }); it("uses the refreshed generation after a share change before saving file permissions", async () => { const page = render(); await flushPromises(); api.updateShare.mockResolvedValue({ data: { ...share, enabled: true, share_token: "test-token" } }); api.file.mockResolvedValue({ data: { ...file, generation: 5 } }); await page.get('[aria-label="启用匿名访问"]').setValue(true); await flushPromises(); expect(api.updateShare).toHaveBeenCalledWith("file-a", expect.objectContaining({ enabled: true, password_mode: "KEEP" })); await page.get(".access-setting-navigation").trigger("click"); api.updateFile.mockResolvedValue({ data: { ...file, generation: 6, allow_export: true } }); await page.get(".access-permission-row input").setValue(true); await flushPromises(); expect(api.updateFile).toHaveBeenCalledWith(expect.objectContaining({ generation: 5 }), { allow_export: true }); }); it("closes access controls when a removed manager no longer has access", async () => { api.members.mockResolvedValue({ data: [{ ...owner, user_id: "manager", protected_label: null }] }); const page = render(); await flushPromises(); api.removeMember.mockResolvedValue({}); api.members.mockRejectedValue({ response: { status: 404 } }); await page.get(".access-member-remove").trigger("click"); await flushPromises(); expect(page.find('[role="dialog"]').exists()).toBe(false); expect(page.emitted("changed")).toBeTruthy(); }); });