175 lines
5.8 KiB
TypeScript
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,
|
|
},
|
|
};
|
|
}
|