@vue/test-utils and jsdom as dev dependencies, jsdom chosen per file with the vitest environment comment so the existing unit tests stay on node. The dialog's repo.shares is faked; the tests cover offering everyone, sharing with a person and a group, changing and removing a share (and the board's shared flag that follows), a refused share keeping the choice, the load retry, and the single-person instance. #5313. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
137 lines
5.4 KiB
TypeScript
137 lines
5.4 KiB
TypeScript
// @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<any>) => w.get('select[aria-label="Person or group"]');
|
|
const choices = (w: VueWrapper<any>) =>
|
|
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);
|
|
});
|
|
});
|