memos/web/tests/memo-editor-timestamps.test.tsx

104 lines
4.4 KiB
TypeScript

import { create } from "@bufbuild/protobuf";
import { timestampFromDate } from "@bufbuild/protobuf/wkt";
import { act, fireEvent, render, screen, waitFor } from "@testing-library/react";
import { beforeEach, describe, expect, it, vi } from "vitest";
import { TimestampPopover } from "@/components/MemoEditor/components/TimestampPopover";
import { memoService } from "@/components/MemoEditor/services/memoService";
import { createInitialState, EditorProvider, useEditorContext } from "@/components/MemoEditor/state";
import { type Memo, MemoSchema } from "@/types/proto/api/v1/memo_service_pb";
const clients = vi.hoisted(() => ({ getMemo: vi.fn(), updateMemo: vi.fn() }));
vi.mock("@/connect", () => ({
attachmentServiceClient: { createAttachment: vi.fn() },
memoServiceClient: clients,
}));
vi.mock("@/utils/i18n", () => ({ useTranslate: () => (key: string) => key }));
const originalCreate = new Date(2020, 0, 1, 10, 20, 30);
const originalUpdate = new Date(2020, 0, 2, 11, 22, 33);
const customTime = new Date(2024, 1, 29, 12, 34, 56);
let editor: ReturnType<typeof useEditorContext>;
function Probe() {
editor = useEditorContext();
return <TimestampPopover />;
}
function renderEditor() {
const memo = create(MemoSchema, {
name: "memos/timestamps",
content: "Original content",
createTime: timestampFromDate(originalCreate),
updateTime: timestampFromDate(originalUpdate),
});
clients.getMemo.mockResolvedValue(memo);
render(
<EditorProvider initialEditorState={{ ...createInitialState(), ...memoService.fromMemo(memo) }}>
<Probe />
</EditorProvider>,
);
fireEvent.click(screen.getByRole("button"));
return memo;
}
describe("memo editor timestamps", () => {
beforeEach(() => {
clients.updateMemo.mockImplementation(async ({ memo }: { memo: Memo }) => memo);
});
it.each([
[0, "createTime", "create_time"],
[1, "updateTime", "update_time"],
] as const)("saves %s after dismissing the focused input with Escape", async (index, field, path) => {
const memo = renderEditor();
const input = screen.getAllByRole("textbox")[index];
act(() => input.focus());
fireEvent.change(input, { target: { value: "2024-02-29 12:34:56" } });
fireEvent.keyDown(input, { key: "Escape" });
await waitFor(() => expect(screen.queryByRole("textbox")).not.toBeInTheDocument());
await memoService.save(editor.getState(), { memoName: memo.name });
expect(clients.updateMemo).toHaveBeenCalledOnce();
const request = clients.updateMemo.mock.calls[0][0];
expect(request.updateMask.paths).toEqual([path]);
expect(request.memo[field]).toEqual(timestampFromDate(customTime));
});
it.each([
"2024-02-30 12:34:56",
"2024-13-01 12:34:56",
"2024-02-29 24:34:56",
"invalid",
])("rejects invalid local time %s without changing the saved timestamp", async (value) => {
const memo = renderEditor();
const input = screen.getAllByRole("textbox")[0];
fireEvent.change(input, { target: { value } });
fireEvent.blur(input);
expect(input).toHaveAttribute("data-invalid", "true");
expect(editor.getState().timestamps.createTime).toEqual(originalCreate);
await memoService.save(editor.getState(), { memoName: memo.name });
expect(clients.updateMemo).not.toHaveBeenCalled();
});
it("automatically updates the modification time for content edits", async () => {
const memo = renderEditor();
const state = { ...editor.getState(), content: "Changed content" };
await memoService.save(state, { memoName: memo.name });
const request = clients.updateMemo.mock.calls[0][0];
expect(request.updateMask.paths).toEqual(["content", "update_time"]);
expect(request.memo.updateTime).toBeUndefined();
expect(request.memo.createTime).toBeUndefined();
});
it("keeps the explicitly chosen modification time when content also changes", async () => {
const memo = renderEditor();
const input = screen.getAllByRole("textbox")[1];
fireEvent.change(input, { target: { value: "2024-02-29 12:34:56" } });
// Saving must see the edit even if the input has not blurred yet.
await memoService.save({ ...editor.getState(), content: "Changed content" }, { memoName: memo.name });
const request = clients.updateMemo.mock.calls[0][0];
expect(request.updateMask.paths).toEqual(["content", "update_time"]);
expect(request.memo.updateTime).toEqual(timestampFromDate(customTime));
});
});