110 lines
5.0 KiB
TypeScript
110 lines
5.0 KiB
TypeScript
import { describe, expect, it } from "vitest";
|
|
|
|
import type { ChatChannelDetailVM, ChatMessageVM } from "@hivegrid/view-models";
|
|
|
|
import {
|
|
beginChatMessageMutation,
|
|
overlayChatMessageMutations,
|
|
reconcileChatMessageMutations,
|
|
retryChatMessageMutation,
|
|
settleChatMessageMutation,
|
|
} from "./chatMessageMutationOptimistic";
|
|
|
|
const message = {
|
|
id: "message_1",
|
|
clientMessageId: "client_1",
|
|
source: "persisted",
|
|
deliveryState: "sent",
|
|
deliveryStateKey: "chat.message.sent",
|
|
failureReasonKey: null,
|
|
retryable: false,
|
|
author: { userId: "user_1", displayName: "Raffael", avatar: { imageUrl: null, fallbackLabel: "R" }, isCurrentUser: true },
|
|
bodyMarkdown: "Alt",
|
|
bodyFormat: "markdown",
|
|
systemEvent: null,
|
|
createdAtIso: "2026-07-16T01:00:00.000Z",
|
|
updatedAtIso: "2026-07-16T01:00:00.000Z",
|
|
editedAtIso: null,
|
|
deletedAtIso: null,
|
|
parentMessageId: null,
|
|
mentions: [],
|
|
reactions: [{ emoji: "👍", count: 1, reactedByViewer: true, reactorLabels: ["Raffael"] }],
|
|
thread: null,
|
|
attachments: [{ id: "a", fileId: "f", kind: "file", fileName: "a.pdf", mimeType: "application/pdf", sizeBytes: 1, downloadHref: "/f", previewHref: null, altText: null, durationSeconds: null, scanStatus: "clean" }],
|
|
linkPreviews: [],
|
|
readAggregate: null,
|
|
pin: { pinnedAtIso: "2026-07-16T01:00:00.000Z", pinnedByLabel: "Raffael", note: null },
|
|
actions: { canReact: true, canReply: true, canEdit: true, canDelete: true, canPin: true, canMarkAsTask: true },
|
|
tombstoneKey: null,
|
|
} satisfies ChatMessageVM;
|
|
|
|
function vm(currentMessage: ChatMessageVM = message): ChatChannelDetailVM {
|
|
return {
|
|
status: "ready",
|
|
data: {
|
|
header: { id: "chat_1", kind: "group", title: "Büro", subtitle: null, avatar: { imageUrl: null, fallbackLabel: "B" }, project: null, memberCount: 1, isPrivate: true, isArchived: false, isAuditMode: false, dmAuditBannerKey: null, canManage: false, settingsHref: null, pinnedHref: null, searchHref: "/chat" },
|
|
messages: [currentMessage],
|
|
messageGroups: [{ dateIso: "2026-07-16", messages: [currentMessage] }],
|
|
presence: [],
|
|
typingLabels: [],
|
|
pinnedMessages: [{ messageId: "message_1", authorLabel: "Raffael", snippet: "Alt", pinnedAtIso: "2026-07-16T01:00:00.000Z", pinnedByLabel: "Raffael", href: "#message-1" }],
|
|
history: { olderCursor: null, hasOlder: false },
|
|
composer: { enabled: true, disabledReasonKey: null, canSend: true, canAttachFiles: false, canRecordVoice: false, canMention: true, canMarkAsTask: false, draftStorageKey: "draft", maxBodyLength: 8000, placeholderKey: "chat.composer.placeholder" },
|
|
connection: { state: "online", stateLabelKey: "chat.connection.online", reconnectAttempt: 0, retryAtIso: null, canRetry: false },
|
|
emptyHeadlineKey: "chat.empty.title",
|
|
emptyBodyKey: "chat.empty.body",
|
|
loadOlderLabelKey: "chat.loadOlder",
|
|
newMessagesLabelKey: "chat.newMessages",
|
|
},
|
|
};
|
|
}
|
|
|
|
describe("optimistic chat message mutations", () => {
|
|
it("preserves the state reference when reconciliation has nothing to remove", () => {
|
|
const current = [] as const;
|
|
expect(reconcileChatMessageMutations(current, vm())).toBe(current);
|
|
});
|
|
|
|
it("overlays an edit and rolls it back when the request fails", () => {
|
|
const pending = beginChatMessageMutation([], {
|
|
kind: "edit_message",
|
|
requestId: "request_1",
|
|
messageId: "message_1",
|
|
startedAtIso: "2026-07-16T02:00:00.000Z",
|
|
bodyMarkdown: "Neu",
|
|
mentions: [],
|
|
});
|
|
expect(overlayChatMessageMutations(vm(), pending).data.messages[0]?.bodyMarkdown).toBe("Neu");
|
|
const failed = settleChatMessageMutation(pending, { ok: false, requestId: "request_1", errorKey: "chat.errors.invalidInput" });
|
|
expect(overlayChatMessageMutations(vm(), failed).data.messages[0]?.bodyMarkdown).toBe("Alt");
|
|
expect(retryChatMessageMutation(failed, "request_1")[0]).toMatchObject({ requestId: "request_1", state: "pending" });
|
|
});
|
|
|
|
it("tombstones optimistically and removes attachments, reactions and pins", () => {
|
|
const pending = beginChatMessageMutation([], {
|
|
kind: "delete_message",
|
|
requestId: "request_delete",
|
|
messageId: "message_1",
|
|
startedAtIso: "2026-07-16T02:00:00.000Z",
|
|
});
|
|
const overlaid = overlayChatMessageMutations(vm(), pending);
|
|
expect(overlaid.data.messages[0]).toMatchObject({ bodyMarkdown: null, tombstoneKey: "chat.message.deleted", attachments: [], reactions: [], pin: null });
|
|
expect(overlaid.data.pinnedMessages).toEqual([]);
|
|
});
|
|
|
|
it("drops a confirmed overlay once the authoritative echo is hydrated", () => {
|
|
const confirmed = settleChatMessageMutation(
|
|
beginChatMessageMutation([], {
|
|
kind: "edit_message",
|
|
requestId: "request_1",
|
|
messageId: "message_1",
|
|
startedAtIso: "2026-07-16T02:00:00.000Z",
|
|
bodyMarkdown: "Neu",
|
|
mentions: [],
|
|
}),
|
|
{ ok: true, requestId: "request_1", errorKey: null }
|
|
);
|
|
expect(reconcileChatMessageMutations(confirmed, vm({ ...message, bodyMarkdown: "Neu", editedAtIso: "2026-07-16T02:00:00.000Z" }))).toEqual([]);
|
|
});
|
|
});
|