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

175 lines
5.8 KiB
TypeScript

import { buildChatChannelDetailVM, type ChatMessageCandidate } from "@hivegrid/view-models";
import { describe, expect, it } from "vitest";
import { overlayChatChannelDetailRealtime } from "./chatRealtimeViewModel";
const message: ChatMessageCandidate = {
id: "message_1",
clientMessageId: "client_1",
source: "websocket",
deliveryState: "sent",
failureReasonKey: null,
author: {
userId: "user_1",
displayName: "Ada",
avatar: { imageUrl: null, fallbackLabel: "A" },
isCurrentUser: false,
},
bodyMarkdown: "Hallo",
bodyFormat: "markdown",
systemEvent: null,
createdAtIso: "2026-07-15T08:00:00.000Z",
updatedAtIso: "2026-07-15T08:00:00.000Z",
editedAtIso: null,
deletedAtIso: null,
parentMessageId: null,
mentions: [],
reactions: [],
thread: null,
attachments: [],
linkPreviews: [],
readAggregate: { readCount: 0, recipientCount: 1, readerUserIds: [], readerLabels: [], labelKey: "chat.message.read.none" },
pin: null,
actions: { canReact: false, canReply: false, canEdit: false, canDelete: false, canPin: false, canMarkAsTask: false },
};
function readyVm() {
return buildChatChannelDetailVM({
header: {
id: "chat_1",
kind: "group",
title: "Team",
subtitle: null,
avatar: { imageUrl: null, fallbackLabel: "T" },
project: null,
memberCount: 2,
isPrivate: false,
isArchived: false,
isAuditMode: false,
canManage: false,
settingsHref: null,
pinnedHref: null,
searchHref: "/tenant/chat/chat_1/search",
},
messages: [],
pinnedMessages: [],
typingLabels: [],
history: { olderCursor: null, hasOlder: false },
composer: {
disabledReasonKey: null,
canSend: true,
canAttachFiles: false,
canRecordVoice: false,
canMention: false,
canMarkAsTask: false,
draftStorageKey: "chat:draft",
maxBodyLength: 8_000,
},
connection: { state: "connecting", reconnectAttempt: 0, retryAtIso: null },
});
}
describe("overlayChatChannelDetailRealtime", () => {
it("overlays transport state on a ready loader VM without replacing domain data", () => {
const renderedMessage = buildChatChannelDetailVM({ ...readyVmInput(), messages: [message] });
if (renderedMessage.status !== "ready") throw new Error("fixture must be ready");
const vm = readyVm();
const result = overlayChatChannelDetailRealtime(vm, {
messages: renderedMessage.data.messages,
messageGroups: renderedMessage.data.messageGroups,
presence: [{ userId: "user_2", status: "online" }],
typingLabels: ["Ada"],
connection: { state: "reconnecting", reconnectAttempt: 2, retryAtIso: "2026-07-15T08:01:00.000Z" },
});
expect(result.status).toBe("ready");
if (result.status !== "ready" || vm.status !== "ready") throw new Error("fixture must be ready");
expect(result.data.header).toBe(vm.data.header);
expect(result.data.messages).toEqual(renderedMessage.data.messages);
expect(result.data.presence).toEqual([{ userId: "user_2", status: "online" }]);
expect(result.data.typingLabels).toEqual(["Ada"]);
expect(result.data.connection).toEqual({
state: "reconnecting",
stateLabelKey: "chat.connection.reconnecting",
reconnectAttempt: 2,
retryAtIso: "2026-07-15T08:01:00.000Z",
canRetry: true,
});
});
it("leaves non-ready loader states untouched", () => {
const vm = { status: "loading" } as const;
expect(
overlayChatChannelDetailRealtime(vm, {
messages: [],
messageGroups: [],
presence: [],
typingLabels: [],
connection: { state: "online", reconnectAttempt: 0, retryAtIso: null },
})
).toBe(vm);
});
it("makes a terminally blocked connection read-only without hiding the draft", () => {
const vm = readyVm();
const result = overlayChatChannelDetailRealtime(vm, {
messages: [],
messageGroups: [],
presence: [],
typingLabels: [],
connection: { state: "blocked", reconnectAttempt: 0, retryAtIso: null },
});
expect(result.status).toBe("ready");
if (result.status !== "ready") return;
expect(result.data.composer).toMatchObject({
enabled: false,
canSend: false,
disabledReasonKey: "chat.composer.connectionBlocked",
});
expect(result.data.connection.canRetry).toBe(false);
});
});
function readyVmInput(): Parameters<typeof buildChatChannelDetailVM>[0] {
const vm = readyVm();
if (vm.status !== "ready") throw new Error("fixture must be ready");
return {
header: {
id: vm.data.header.id,
kind: vm.data.header.kind,
title: vm.data.header.title,
subtitle: vm.data.header.subtitle,
avatar: vm.data.header.avatar,
project: vm.data.header.project,
memberCount: vm.data.header.memberCount,
isPrivate: vm.data.header.isPrivate,
isArchived: vm.data.header.isArchived,
isAuditMode: vm.data.header.isAuditMode,
canManage: vm.data.header.canManage,
settingsHref: vm.data.header.settingsHref,
pinnedHref: vm.data.header.pinnedHref,
searchHref: vm.data.header.searchHref,
},
messages: [],
pinnedMessages: [],
typingLabels: [],
history: vm.data.history,
composer: {
disabledReasonKey: vm.data.composer.disabledReasonKey,
canSend: vm.data.composer.canSend,
canAttachFiles: vm.data.composer.canAttachFiles,
canRecordVoice: vm.data.composer.canRecordVoice,
canMention: vm.data.composer.canMention,
canMarkAsTask: vm.data.composer.canMarkAsTask,
draftStorageKey: vm.data.composer.draftStorageKey,
maxBodyLength: vm.data.composer.maxBodyLength,
},
connection: {
state: vm.data.connection.state,
reconnectAttempt: vm.data.connection.reconnectAttempt,
retryAtIso: vm.data.connection.retryAtIso,
},
};
}