memos/web/tests/insert-menu.test.tsx

100 lines
3.8 KiB
TypeScript

import { fireEvent, render, screen } from "@testing-library/react";
import { beforeAll, describe, expect, test, vi } from "vitest";
import { createInitialState, EditorProvider } from "@/components/MemoEditor/state";
import { EditorToolbar } from "@/components/MemoEditor/Toolbar/EditorToolbar";
import InsertMenu from "@/components/MemoEditor/Toolbar/InsertMenu";
vi.mock("@/utils/i18n", async (importOriginal) => ({
...(await importOriginal<typeof import("@/utils/i18n")>()),
useTranslate: () => (key: string) => key,
}));
vi.mock("@/hooks/useCurrentUser", () => ({ default: () => undefined }));
vi.mock("@/contexts/AuthContext", () => ({ useAuth: () => ({ userGeneralSetting: undefined }) }));
vi.mock("@/components/map/useReverseGeocoding", () => ({ useReverseGeocoding: () => ({ data: undefined }) }));
beforeAll(() => {
Element.prototype.scrollIntoView = vi.fn();
Element.prototype.hasPointerCapture = vi.fn(() => false);
Element.prototype.setPointerCapture = vi.fn();
Element.prototype.releasePointerCapture = vi.fn();
});
const renderMenu = (onInsertImages = vi.fn(), isSaving = false) =>
render(
<EditorProvider>
<InsertMenu
isSaving={isSaving}
onLocationChange={vi.fn()}
onInsertImages={onInsertImages}
onAudioRecorderClick={vi.fn()}
isFormattingToolbarVisible={false}
/>
</EditorProvider>,
);
describe("InsertMenu", () => {
test("shows attachment and inline-image actions in the intended order", () => {
renderMenu();
const trigger = screen.getByRole("button", { name: "common.add" });
expect(trigger).toHaveAttribute("tabindex", "0");
fireEvent.click(trigger);
expect(screen.getAllByRole("menuitem").map((item) => item.textContent)).toEqual([
"editor.insert-menu.add-attachment",
"editor.insert-menu.insert-image",
"editor.audio-recorder.trigger",
"editor.insert-menu.link-memo",
"editor.insert-menu.add-location",
"editor.focus-mode",
"editor.formatting-toolbar",
]);
});
test("uses separate unrestricted and multi-image file inputs", () => {
const onInsertImages = vi.fn();
const { container } = renderMenu(onInsertImages);
const inputs = Array.from(container.querySelectorAll<HTMLInputElement>('input[type="file"]'));
const attachmentInput = inputs.find((input) => input.accept === "");
const inlineImageInput = inputs.find((input) => input.accept === "image/*");
expect(attachmentInput).toBeDefined();
expect(attachmentInput).toHaveAttribute("multiple");
expect(inlineImageInput).toBeDefined();
expect(inlineImageInput).toHaveAttribute("multiple");
const image = new File(["image"], "photo.png", { type: "image/png" });
fireEvent.change(inlineImageInput!, { target: { files: [image] } });
expect(onInsertImages).toHaveBeenCalledWith([image]);
});
test("disables insertion controls while saving", () => {
const { container } = renderMenu(vi.fn(), true);
expect(screen.getByRole("button", { name: "common.add" })).toBeDisabled();
for (const input of container.querySelectorAll('input[type="file"]')) {
expect(input).toBeDisabled();
}
});
test("exposes a localized save-blocking reason from a focusable wrapper", () => {
const state = createInitialState();
state.content = "memo";
state.ui.pendingInlineImageInsertions = 1;
render(
<EditorProvider initialEditorState={state}>
<EditorToolbar
onSave={vi.fn()}
onAudioRecorderClick={vi.fn()}
isFormattingToolbarVisible={false}
onToggleFormattingToolbar={vi.fn()}
onInsertImages={vi.fn()}
/>
</EditorProvider>,
);
expect(screen.getByRole("button", { name: "editor.save" })).toBeDisabled();
expect(screen.getByLabelText("editor.validation.resolve-image-uploads")).toHaveAttribute("tabindex", "0");
});
});