memos/web/tests/attachment-library-stats.test.ts
amblued cdd13ca24e feat(spaces): unify collection scope across resource pages
- Treat All as an unfiltered Space collection while preserving exact Space filters.
- Keep global resources outside remembered Space context and preserve memo navigation origins.
- Surface Space placement consistently across memo, attachment, and visibility UI.
2026-08-26 09:16:28 +08:00

107 lines
4.5 KiB
TypeScript

import { create } from "@bufbuild/protobuf";
import { QueryClient, QueryClientProvider } from "@tanstack/react-query";
import { renderHook, waitFor } from "@testing-library/react";
import { createElement, type PropsWithChildren } from "react";
import { beforeEach, describe, expect, it, vi } from "vitest";
import { buildAttachmentLibraryStats, useAttachmentLibraryStats, useUnusedAttachmentLibrary } from "@/hooks/useAttachmentLibrary";
import {
type Attachment,
AttachmentSchema,
MotionMediaFamily,
MotionMediaRole,
MotionMediaSchema,
} from "@/types/proto/api/v1/attachment_service_pb";
const clients = vi.hoisted(() => ({ listAttachments: vi.fn() }));
vi.mock("@/connect", () => ({
attachmentServiceClient: { listAttachments: clients.listAttachments },
}));
const attachment = (name: string, type: string, memo?: string): Attachment =>
create(AttachmentSchema, { name: `attachments/${name}`, filename: name, type, memo });
describe("buildAttachmentLibraryStats", () => {
beforeEach(() => clients.listAttachments.mockReset());
it("counts linked attachment types without expanding live-photo pairs", () => {
const still = attachment("live.jpg", "image/jpeg", "memos/one");
still.motionMedia = create(MotionMediaSchema, {
family: MotionMediaFamily.APPLE_LIVE_PHOTO,
role: MotionMediaRole.STILL,
groupId: "live-photo",
});
const video = attachment("live.mov", "video/quicktime", "memos/one");
video.motionMedia = create(MotionMediaSchema, {
family: MotionMediaFamily.APPLE_LIVE_PHOTO,
role: MotionMediaRole.VIDEO,
groupId: "live-photo",
});
expect(
buildAttachmentLibraryStats([
still,
video,
attachment("photo.png", "image/png", "memos/one"),
attachment("recording.mp3", "audio/mpeg", "memos/one"),
attachment("notes.pdf", "application/pdf", "memos/one"),
attachment("unused.png", "image/png"),
]),
).toEqual({
unused: 1,
media: 2,
documents: 1,
audio: 1,
});
});
it("loads every page before exposing complete sidebar counts", async () => {
clients.listAttachments
.mockResolvedValueOnce({
attachments: [attachment("photo.png", "image/png", "memos/one")],
nextPageToken: "1000",
})
.mockResolvedValueOnce({
attachments: [attachment("unused.pdf", "application/pdf")],
nextPageToken: "",
});
const queryClient = new QueryClient({ defaultOptions: { queries: { retry: false } } });
const wrapper = ({ children }: PropsWithChildren) => createElement(QueryClientProvider, { client: queryClient }, children);
const filter = 'space == "spaces/team"';
const { result } = renderHook(() => useAttachmentLibraryStats(filter), { wrapper });
await waitFor(() => expect(result.current.isComplete).toBe(true));
expect(result.current.stats).toEqual({ media: 1, documents: 0, audio: 0, unused: 1 });
expect(clients.listAttachments).toHaveBeenCalledTimes(2);
expect(clients.listAttachments.mock.calls.map(([request]) => request.filter)).toEqual([filter, filter]);
});
it("uses only the unused predicate in the All collection", async () => {
clients.listAttachments.mockResolvedValue({ attachments: [], nextPageToken: "" });
const queryClient = new QueryClient({ defaultOptions: { queries: { retry: false } } });
const wrapper = ({ children }: PropsWithChildren) => createElement(QueryClientProvider, { client: queryClient }, children);
const { result } = renderHook(() => useUnusedAttachmentLibrary("en", undefined), { wrapper });
await waitFor(() => expect(result.current.isComplete).toBe(true));
expect(clients.listAttachments).toHaveBeenCalledWith(
expect.objectContaining({
filter: "(memo_id == null)",
}),
);
});
it("combines an explicit unassigned filter with the unused attachment predicate", async () => {
clients.listAttachments.mockResolvedValue({ attachments: [], nextPageToken: "" });
const queryClient = new QueryClient({ defaultOptions: { queries: { retry: false } } });
const wrapper = ({ children }: PropsWithChildren) => createElement(QueryClientProvider, { client: queryClient }, children);
const filter = "space == null";
const { result } = renderHook(() => useUnusedAttachmentLibrary("en", filter), { wrapper });
await waitFor(() => expect(result.current.isComplete).toBe(true));
expect(clients.listAttachments).toHaveBeenCalledWith(
expect.objectContaining({
filter: "(memo_id == null) && (space == null)",
}),
);
});
});