Files
HiveGrid-Wireframes/03 Quellen und Referenzen/hivegrid-cloud/apps/web/app/modules/chat/client/chatMessageMutationOptimistic.test.ts
T

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([]);
});
});