133 lines
6 KiB
TypeScript
133 lines
6 KiB
TypeScript
import { create } from "@bufbuild/protobuf";
|
|
import { act, renderHook, waitFor } from "@testing-library/react";
|
|
import type { PropsWithChildren, RefObject } from "react";
|
|
import { beforeEach, describe, expect, test, vi } from "vitest";
|
|
import type { UploadAnchorDescriptor } from "@/components/MemoEditor/Editor/uploadAnchors";
|
|
import { useInlineImageUpload } from "@/components/MemoEditor/hooks/useInlineImageUpload";
|
|
import { uploadService } from "@/components/MemoEditor/services";
|
|
import { EditorProvider, useEditorContext } from "@/components/MemoEditor/state";
|
|
import type { LocalFile } from "@/components/MemoEditor/types/attachment";
|
|
import type { EditorController } from "@/components/MemoEditor/types/editorController";
|
|
import { AttachmentSchema } from "@/types/proto/api/v1/attachment_service_pb";
|
|
|
|
vi.mock("@/utils/i18n", async (importOriginal) => ({
|
|
...(await importOriginal<typeof import("@/utils/i18n")>()),
|
|
useTranslate: () => (key: string) => key,
|
|
}));
|
|
vi.mock("react-hot-toast", () => ({ toast: { error: vi.fn() } }));
|
|
|
|
const wrapper = ({ children }: PropsWithChildren) => <EditorProvider>{children}</EditorProvider>;
|
|
|
|
const localImage = (filename: string): LocalFile => ({
|
|
file: new File([filename], filename, { type: "image/png" }),
|
|
previewUrl: `blob:${filename}`,
|
|
origin: "upload",
|
|
});
|
|
|
|
const remoteImage = (uid: string, filename: string) =>
|
|
create(AttachmentSchema, {
|
|
name: `attachments/${uid}`,
|
|
filename,
|
|
type: "image/png",
|
|
});
|
|
|
|
const makeController = () => {
|
|
const descriptors: UploadAnchorDescriptor[] = [];
|
|
const controller = {
|
|
createUploadAnchor: vi.fn((descriptor: UploadAnchorDescriptor) => descriptors.push(descriptor)),
|
|
updateUploadAnchor: vi.fn((descriptor: UploadAnchorDescriptor) => descriptors.push(descriptor)),
|
|
resolveUploadAnchor: vi.fn(),
|
|
cancelUploadAnchor: vi.fn(),
|
|
insertMarkdown: vi.fn(),
|
|
} as unknown as EditorController;
|
|
return { controller, descriptors };
|
|
};
|
|
|
|
describe("useInlineImageUpload", () => {
|
|
beforeEach(() => {
|
|
vi.restoreAllMocks();
|
|
});
|
|
|
|
test("inserts multiple uploaded images once and in selection order", async () => {
|
|
const first = remoteImage("first", "first.png");
|
|
const second = remoteImage("second", "second.png");
|
|
vi.spyOn(uploadService, "uploadFile").mockResolvedValueOnce(first).mockResolvedValueOnce(second);
|
|
const { controller } = makeController();
|
|
const editorRef = { current: controller } as RefObject<EditorController>;
|
|
const { result } = renderHook(() => useInlineImageUpload(editorRef), { wrapper });
|
|
|
|
act(() => result.current.insertLocalImages([localImage("first.png"), localImage("second.png")], 7));
|
|
|
|
await waitFor(() => expect(controller.resolveUploadAnchor).toHaveBeenCalledTimes(1));
|
|
expect(controller.createUploadAnchor).toHaveBeenCalledWith(expect.any(Object), 7);
|
|
expect(controller.resolveUploadAnchor).toHaveBeenCalledWith(
|
|
expect.any(String),
|
|
"\n\n",
|
|
);
|
|
});
|
|
|
|
test("passes the ingested local file, with its media metadata, straight to uploads", async () => {
|
|
const upload = vi.spyOn(uploadService, "uploadFile").mockResolvedValue(remoteImage("first", "first.png"));
|
|
const { controller } = makeController();
|
|
const editorRef = { current: controller } as RefObject<EditorController>;
|
|
const { result } = renderHook(() => useInlineImageUpload(editorRef), { wrapper });
|
|
const localFile: LocalFile = { ...localImage("first.png"), mediaMetadata: Promise.resolve(undefined) };
|
|
|
|
act(() => result.current.insertLocalImages([localFile]));
|
|
|
|
await waitFor(() => expect(upload).toHaveBeenCalledTimes(1));
|
|
expect(upload).toHaveBeenCalledWith(localFile);
|
|
});
|
|
|
|
test("retries only failed files and preserves their original placement", async () => {
|
|
const first = remoteImage("first", "first.png");
|
|
const second = remoteImage("second", "second.png");
|
|
const upload = vi
|
|
.spyOn(uploadService, "uploadFile")
|
|
.mockResolvedValueOnce(first)
|
|
.mockRejectedValueOnce(new Error("network"))
|
|
.mockResolvedValueOnce(second);
|
|
const { controller, descriptors } = makeController();
|
|
const editorRef = { current: controller } as RefObject<EditorController>;
|
|
const { result } = renderHook(() => useInlineImageUpload(editorRef), { wrapper });
|
|
const files = [localImage("first.png"), localImage("second.png")];
|
|
|
|
act(() => result.current.insertLocalImages(files));
|
|
await waitFor(() => expect(descriptors.at(-1)?.status).toBe("failed"));
|
|
expect(controller.resolveUploadAnchor).not.toHaveBeenCalled();
|
|
|
|
act(() => descriptors.at(-1)?.onRetry?.());
|
|
await waitFor(() => expect(controller.resolveUploadAnchor).toHaveBeenCalledTimes(1));
|
|
|
|
expect(upload).toHaveBeenCalledTimes(3);
|
|
expect(upload.mock.calls.map(([file]) => file.file.name)).toEqual(["first.png", "second.png", "second.png"]);
|
|
expect(controller.resolveUploadAnchor).toHaveBeenCalledWith(
|
|
expect.any(String),
|
|
"\n\n",
|
|
);
|
|
});
|
|
|
|
test("does not start local or remote insertion while a save is in progress", () => {
|
|
const upload = vi.spyOn(uploadService, "uploadFile");
|
|
const { controller } = makeController();
|
|
const editorRef = { current: controller } as RefObject<EditorController>;
|
|
const { result } = renderHook(
|
|
() => {
|
|
const inlineUpload = useInlineImageUpload(editorRef);
|
|
const editor = useEditorContext();
|
|
return { inlineUpload, editor };
|
|
},
|
|
{ wrapper },
|
|
);
|
|
|
|
act(() => result.current.editor.dispatch(result.current.editor.actions.setLoading("saving", true)));
|
|
act(() => {
|
|
result.current.inlineUpload.insertLocalImages([localImage("late.png")]);
|
|
result.current.inlineUpload.insertRemoteImages([remoteImage("late", "late.png")]);
|
|
});
|
|
|
|
expect(upload).not.toHaveBeenCalled();
|
|
expect(controller.createUploadAnchor).not.toHaveBeenCalled();
|
|
expect(controller.insertMarkdown).not.toHaveBeenCalled();
|
|
});
|
|
});
|