web: component tests for the Share dialog
@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>
This commit is contained in:
@@ -0,0 +1,136 @@
|
||||
// @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);
|
||||
});
|
||||
});
|
||||
Reference in New Issue
Block a user