- 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.
107 lines
4.5 KiB
TypeScript
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)",
|
|
}),
|
|
);
|
|
});
|
|
});
|