// @vitest-environment jsdom import { flushPromises, mount, type VueWrapper } from "@vue/test-utils"; import { createPinia, setActivePinia } from "pinia"; import { beforeEach, describe, expect, it, vi } from "vitest"; import type { Directory, NoteShare } from "../adapters/repo"; import { useNotesStore, type Note } from "../stores/notes"; import ShareDialog from "./ShareDialog.vue"; // The dialog talks to the server only through repo.shares, so that is all this fakes. // Every call answers the note's shares as the server would. const shares = vi.hoisted(() => ({ directory: vi.fn(), list: vi.fn(), share: vi.fn(), unshare: vi.fn(), })); vi.mock("../adapters", () => ({ repo: { shares } })); const ana = { id: "u1", display_name: "Ana", email: "ana@example.com" }; const ben = { id: "u2", display_name: "Ben", email: "ben@example.com" }; const family = { id: "g1", name: "Family", member_count: 3 }; const DIRECTORY: Directory = { members: [ana, ben], groups: [family] }; const toAna = (permission: "view" | "edit" = "view"): NoteShare => ({ id: "s1", member: ana, group: null, permission, created_at: null, }); const toFamily: NoteShare = { id: "s2", member: null, group: family, permission: "edit", created_at: null }; async function open(existing: NoteShare[] = [], directory: Directory = DIRECTORY) { shares.directory.mockResolvedValue(directory); shares.list.mockResolvedValue(existing); const pinia = createPinia(); setActivePinia(pinia); const notes = useNotesStore(); notes.items.push({ id: "n1", shared: existing.length > 0 } as Note); const wrapper = mount(ShareDialog, { props: { noteId: "n1" }, global: { plugins: [pinia] } }); await flushPromises(); return { wrapper, notes }; } const picker = (w: VueWrapper) => w.get('select[aria-label="Person or group"]'); const choices = (w: VueWrapper) => picker(w) .findAll("option") .map((o) => o.attributes("value")) .filter(Boolean); beforeEach(() => vi.resetAllMocks()); describe("the Share dialog", () => { it("offers everyone, and says the note is only yours when it isn't shared", async () => { const { wrapper } = await open(); expect(shares.list).toHaveBeenCalledWith("n1"); expect(choices(wrapper)).toEqual(["u:u1", "u:u2", "g:g1"]); expect(wrapper.text()).toContain("Only you."); }); it("shares with a person at the chosen permission, and marks the note shared", async () => { const { wrapper, notes } = await open(); shares.share.mockResolvedValue([toAna("edit")]); await picker(wrapper).setValue("u:u1"); await wrapper.get('select[aria-label="Permission"]').setValue("edit"); await wrapper.get("form").trigger("submit"); await flushPromises(); expect(shares.share).toHaveBeenCalledWith("n1", { user_id: "u1" }, "edit"); expect(wrapper.text()).toContain("Ana"); expect(wrapper.text()).not.toContain("Only you."); // Someone it's already shared with isn't offered twice. expect(choices(wrapper)).toEqual(["u:u2", "g:g1"]); expect(notes.items[0].shared).toBe(true); }); it("shares with a group by its id, not as a person", async () => { const { wrapper } = await open(); shares.share.mockResolvedValue([toFamily]); await picker(wrapper).setValue("g:g1"); await wrapper.get("form").trigger("submit"); await flushPromises(); expect(shares.share).toHaveBeenCalledWith("n1", { group_id: "g1" }, "view"); expect(wrapper.text()).toContain("Group ยท 3 people"); }); it("changes a share's permission in place", async () => { const { wrapper } = await open([toAna()]); shares.share.mockResolvedValue([toAna("edit")]); await wrapper.get('select[aria-label="Permission for Ana"]').setValue("edit"); await flushPromises(); expect(shares.share).toHaveBeenCalledWith("n1", { user_id: "u1" }, "edit"); }); it("stops sharing, and the note is yours alone again", async () => { const { wrapper, notes } = await open([toAna()]); shares.unshare.mockResolvedValue([]); await wrapper.get('button[aria-label="Stop sharing with Ana"]').trigger("click"); await flushPromises(); expect(shares.unshare).toHaveBeenCalledWith("n1", "s1"); expect(wrapper.text()).toContain("Only you."); expect(notes.items[0].shared).toBe(false); }); it("says why a share failed and keeps the choice to try again", async () => { const { wrapper } = await open(); shares.share.mockRejectedValue({ error: "Ana can't be reached.", status: 400 }); await picker(wrapper).setValue("u:u1"); await wrapper.get("form").trigger("submit"); await flushPromises(); expect(wrapper.get('[role="alert"]').text()).toBe("Ana can't be reached."); expect((picker(wrapper).element as HTMLSelectElement).value).toBe("u:u1"); }); it("offers a retry when it couldn't load who it's shared with", async () => { shares.directory.mockRejectedValueOnce(new Error("")); const { wrapper } = await open(); expect(wrapper.text()).toContain("Couldn't load who this is shared with."); await wrapper.get("button:not([aria-label])").trigger("click"); await flushPromises(); expect(choices(wrapper)).toEqual(["u:u1", "u:u2", "g:g1"]); }); it("says there is no one to share with on a single-person instance", async () => { const { wrapper } = await open([], { members: [], groups: [] }); expect(wrapper.text()).toContain("No one else has an account here yet."); expect(wrapper.find("form").exists()).toBe(false); }); });