feat(FN-4849): complete Step 2 — route room thread reads and sends

Fusion-Task-Id: FN-4849
Fusion-Task-Lineage: 624f86c4-0b0f-4543-90d2-381f890da401
This commit is contained in:
Fusion (runfusion.ai)
2026-05-17 00:20:03 -07:00
committed by gsxdsm
parent 2adc9fa0da
commit b97709a3d4
2 changed files with 237 additions and 27 deletions

View File

@@ -1129,7 +1129,19 @@ export function QuickChatFAB({
}, [chatMode, selectedAgentId, targetModelSelection]); }, [chatMode, selectedAgentId, targetModelSelection]);
const hasChatTarget = chatMode === "agent" ? Boolean(selectedAgentId) : Boolean(targetModelSelection); const hasChatTarget = chatMode === "agent" ? Boolean(selectedAgentId) : Boolean(targetModelSelection);
const inputDisabled = !hasChatTarget || !activeSession; const roomThreadActive = chatRoomsEnabled && Boolean(roomsState.activeRoom);
const displayedMessages = useMemo<ChatMessageInfo[]>(() => {
if (!roomThreadActive) {
return messages;
}
return roomsState.messages.map((message) => ({
id: message.id,
role: message.role,
content: message.content,
toolCalls: undefined,
}));
}, [messages, roomThreadActive, roomsState.messages]);
const inputDisabled = roomThreadActive ? false : (!hasChatTarget || !activeSession);
const hasPersistedAgentSessionSelection = useMemo( const hasPersistedAgentSessionSelection = useMemo(
() => Boolean(selectedAgentId) && sessions.some((session) => !session.modelProvider && !session.modelId && session.agentId === selectedAgentId), () => Boolean(selectedAgentId) && sessions.some((session) => !session.modelProvider && !session.modelId && session.agentId === selectedAgentId),
[selectedAgentId, sessions], [selectedAgentId, sessions],
@@ -1578,16 +1590,16 @@ export function QuickChatFAB({
}, [anchorToBottom]); }, [anchorToBottom]);
useLayoutEffect(() => { useLayoutEffect(() => {
const sessionId = activeSession?.id ?? null; const threadId = roomThreadActive ? (roomsState.activeRoom?.id ?? null) : (activeSession?.id ?? null);
const previousState = previousOpenStateRef.current; const previousState = previousOpenStateRef.current;
previousOpenStateRef.current = { isOpen, sessionId }; previousOpenStateRef.current = { isOpen, sessionId: threadId };
if (!isOpen || !sessionId) { if (!isOpen || !threadId) {
return; return;
} }
const openingNow = !previousState.isOpen && isOpen; const openingNow = !previousState.isOpen && isOpen;
const sessionChangedWhileOpen = previousState.isOpen && previousState.sessionId !== sessionId; const sessionChangedWhileOpen = previousState.isOpen && previousState.sessionId !== threadId;
if (!openingNow && !sessionChangedWhileOpen) { if (!openingNow && !sessionChangedWhileOpen) {
return; return;
} }
@@ -1596,7 +1608,7 @@ export function QuickChatFAB({
if (!messagesEl) return; if (!messagesEl) return;
anchorToBottom(messagesEl); anchorToBottom(messagesEl);
}, [isOpen, activeSession?.id, anchorToBottom]); }, [isOpen, activeSession?.id, anchorToBottom, roomThreadActive, roomsState.activeRoom?.id]);
useEffect(() => { useEffect(() => {
if (!isMobile || !isOpen || !activeSession) { if (!isMobile || !isOpen || !activeSession) {
@@ -1633,7 +1645,7 @@ export function QuickChatFAB({
if (!isUserScrollingRef.current) { if (!isUserScrollingRef.current) {
scrollToBottom(); scrollToBottom();
} }
}, [messages, streamingText, streamingThinking, isStreaming, isOpen, scrollToBottom]); }, [displayedMessages, streamingText, streamingThinking, isStreaming, isOpen, roomThreadActive, scrollToBottom]);
useEffect(() => { useEffect(() => {
if (!activeSession?.id) { if (!activeSession?.id) {
@@ -1716,7 +1728,7 @@ export function QuickChatFAB({
const showRoomGroups = chatRoomsEnabled && roomOptions.length > 0; const showRoomGroups = chatRoomsEnabled && roomOptions.length > 0;
const activeSessionLabel = useMemo(() => { const activeSessionLabel = useMemo(() => {
if (showRoomGroups && roomsState.activeRoom) { if (showRoomGroups && roomThreadActive && roomsState.activeRoom) {
return `#${roomsState.activeRoom.name}`; return `#${roomsState.activeRoom.name}`;
} }
const activeOption = sessionOptions.find((option) => option.id === activeSession?.id); const activeOption = sessionOptions.find((option) => option.id === activeSession?.id);
@@ -1727,7 +1739,7 @@ export function QuickChatFAB({
return "Loading sessions…"; return "Loading sessions…";
} }
return "Select a session"; return "Select a session";
}, [activeSession?.id, roomsState.activeRoom, sessionOptions, sessionsLoading, showRoomGroups]); }, [activeSession?.id, roomThreadActive, roomsState.activeRoom, sessionOptions, sessionsLoading, showRoomGroups]);
useEffect(() => { useEffect(() => {
if (!isOpen) { if (!isOpen) {
@@ -1763,7 +1775,7 @@ export function QuickChatFAB({
}, [sessionMenuOpen]); }, [sessionMenuOpen]);
const inputPlaceholder = useMemo(() => { const inputPlaceholder = useMemo(() => {
if (chatRoomsEnabled && roomsState.activeRoom) { if (roomThreadActive && roomsState.activeRoom) {
return `Message #${roomsState.activeRoom.name}`; return `Message #${roomsState.activeRoom.name}`;
} }
if (chatMode === "agent") { if (chatMode === "agent") {
@@ -1777,7 +1789,7 @@ export function QuickChatFAB({
return `Message ${selectedModelTag}`; return `Message ${selectedModelTag}`;
} }
return "Select a model to start chatting"; return "Select a model to start chatting";
}, [chatMode, chatRoomsEnabled, roomsState.activeRoom?.name, selectedAgent, selectedModelTag]); }, [chatMode, roomThreadActive, roomsState.activeRoom?.name, selectedAgent, selectedModelTag]);
const handleSessionSwitch = useCallback((sessionId: string) => { const handleSessionSwitch = useCallback((sessionId: string) => {
const selectedSession = sessions.find((session) => session.id === sessionId); const selectedSession = sessions.find((session) => session.id === sessionId);
@@ -1785,7 +1797,7 @@ export function QuickChatFAB({
return; return;
} }
if (chatRoomsEnabled && roomsState.activeRoom) { if (roomThreadActive) {
roomsState.selectRoom(null); roomsState.selectRoom(null);
} }
@@ -1808,7 +1820,7 @@ export function QuickChatFAB({
void selectSession(selectedSession); void selectSession(selectedSession);
setSessionMenuOpen(false); setSessionMenuOpen(false);
}, [chatRoomsEnabled, markRead, roomsState, selectSession, sessions]); }, [markRead, roomThreadActive, roomsState, selectSession, sessions]);
const handleRoomSwitch = useCallback((roomId: string) => { const handleRoomSwitch = useCallback((roomId: string) => {
const selectedRoom = roomsState.rooms.find((room) => room.id === roomId); const selectedRoom = roomsState.rooms.find((room) => room.id === roomId);
@@ -1926,9 +1938,14 @@ export function QuickChatFAB({
return; return;
} }
if (roomThreadActive && attachmentsToSend.length > 0) {
addToast("Attachments are not supported in chat rooms yet", "warning");
focusComposerInput();
preserveComposerFocusRef.current = false;
return;
}
if (trimmed === "/clear" || trimmed === "/new") { if (trimmed === "/clear" || trimmed === "/new") {
stopStreaming();
clearPendingMessage();
attachmentsToSend.forEach((attachment) => { attachmentsToSend.forEach((attachment) => {
if (attachment.previewUrl) { if (attachment.previewUrl) {
URL.revokeObjectURL(attachment.previewUrl); URL.revokeObjectURL(attachment.previewUrl);
@@ -1937,13 +1954,20 @@ export function QuickChatFAB({
setPendingAttachments((previous) => previous.filter((attachment) => !attachmentsToSend.includes(attachment))); setPendingAttachments((previous) => previous.filter((attachment) => !attachmentsToSend.includes(attachment)));
try { try {
if (chatMode === "model") { if (roomThreadActive && roomsState.activeRoom?.id) {
await roomsState.clearRoom(roomsState.activeRoom.id);
setHelpMessageVisible(false);
} else if (chatMode === "model") {
stopStreaming();
clearPendingMessage();
const parsed = parseModelSelection(resolvedModelSelection); const parsed = parseModelSelection(resolvedModelSelection);
if (!parsed) { if (!parsed) {
return; return;
} }
await startFreshSession(FN_AGENT_ID, parsed.modelProvider, parsed.modelId); await startFreshSession(FN_AGENT_ID, parsed.modelProvider, parsed.modelId);
} else if (selectedAgentId) { } else if (selectedAgentId) {
stopStreaming();
clearPendingMessage();
await startFreshSession(selectedAgentId); await startFreshSession(selectedAgentId);
} }
} catch { } catch {
@@ -1957,7 +1981,11 @@ export function QuickChatFAB({
try { try {
setHelpMessageVisible(false); setHelpMessageVisible(false);
await sendMessage(trimmed, attachmentsToSend.map((attachment) => attachment.file)); if (roomThreadActive) {
await roomsState.sendRoomMessage(trimmed);
} else {
await sendMessage(trimmed, attachmentsToSend.map((attachment) => attachment.file));
}
attachmentsToSend.forEach((attachment) => { attachmentsToSend.forEach((attachment) => {
if (attachment.previewUrl) { if (attachment.previewUrl) {
URL.revokeObjectURL(attachment.previewUrl); URL.revokeObjectURL(attachment.previewUrl);
@@ -1978,6 +2006,8 @@ export function QuickChatFAB({
inputDisabled, inputDisabled,
messageInput, messageInput,
resolvedModelSelection, resolvedModelSelection,
roomThreadActive,
roomsState,
selectedAgentId, selectedAgentId,
sendMessage, sendMessage,
startFreshSession, startFreshSession,
@@ -2492,7 +2522,7 @@ export function QuickChatFAB({
<div className="quick-chat-panel-header"> <div className="quick-chat-panel-header">
<div className="quick-chat-panel-title-wrap"> <div className="quick-chat-panel-title-wrap">
<h3>Quick Chat</h3> <h3>Quick Chat</h3>
{chatRoomsEnabled && roomsState.activeRoom ? ( {roomThreadActive && roomsState.activeRoom ? (
<span className="quick-chat-model-tag" data-testid="quick-chat-room-tag" title={`#${roomsState.activeRoom.name}`}> <span className="quick-chat-model-tag" data-testid="quick-chat-room-tag" title={`#${roomsState.activeRoom.name}`}>
#{roomsState.activeRoom.name} #{roomsState.activeRoom.name}
</span> </span>
@@ -2553,7 +2583,7 @@ export function QuickChatFAB({
<input <input
type="hidden" type="hidden"
data-testid="quick-chat-session-dropdown" data-testid="quick-chat-session-dropdown"
value={showRoomGroups && roomsState.activeRoom ? "" : activeSession?.id ?? ""} value={showRoomGroups && roomThreadActive ? "" : activeSession?.id ?? ""}
readOnly readOnly
/> />
<button <button
@@ -2565,7 +2595,7 @@ export function QuickChatFAB({
data-testid="quick-chat-session-dropdown-trigger" data-testid="quick-chat-session-dropdown-trigger"
onClick={() => setSessionMenuOpen((current) => !current)} onClick={() => setSessionMenuOpen((current) => !current)}
> >
{chatRoomsEnabled && roomsState.activeRoom ? ( {roomThreadActive && roomsState.activeRoom ? (
<Hash size={16} aria-hidden="true" /> <Hash size={16} aria-hidden="true" />
) : activeSession?.modelProvider ? ( ) : activeSession?.modelProvider ? (
<ProviderIcon provider={activeSession.modelProvider} size="sm" /> <ProviderIcon provider={activeSession.modelProvider} size="sm" />
@@ -2608,7 +2638,7 @@ export function QuickChatFAB({
</> </>
)} )}
{sessionOptions.map((sessionOption) => { {sessionOptions.map((sessionOption) => {
const isActiveSession = roomsState.activeRoom === null && activeSession?.id === sessionOption.id; const isActiveSession = !roomThreadActive && activeSession?.id === sessionOption.id;
const session = sessions.find((item) => item.id === sessionOption.id); const session = sessions.find((item) => item.id === sessionOption.id);
const showUnreadDot = !isActiveSession && isUnread("direct", sessionOption.id, session?.lastMessageAt ?? session?.updatedAt); const showUnreadDot = !isActiveSession && isUnread("direct", sessionOption.id, session?.lastMessageAt ?? session?.updatedAt);
return ( return (
@@ -2717,9 +2747,9 @@ export function QuickChatFAB({
<div className="quick-chat-panel-messages" ref={messagesRef} data-testid="quick-chat-messages" onScroll={updateScrollState}> <div className="quick-chat-panel-messages" ref={messagesRef} data-testid="quick-chat-messages" onScroll={updateScrollState}>
{sessionsLoading ? ( {sessionsLoading ? (
<div className="quick-chat-panel-empty">Loading conversation</div> <div className="quick-chat-panel-empty">Loading conversation</div>
) : isStreaming ? ( ) : !roomThreadActive && isStreaming ? (
<> <>
{messages.map((message: ChatMessageInfo) => ( {displayedMessages.map((message: ChatMessageInfo) => (
<QuickChatMessageItem <QuickChatMessageItem
key={message.id} key={message.id}
message={message} message={message}
@@ -2767,13 +2797,35 @@ export function QuickChatFAB({
)} )}
</div> </div>
</> </>
) : messagesLoading ? ( ) : roomThreadActive ? roomsState.messagesLoading ? (
<div className="quick-chat-panel-empty">Loading conversation</div> <div className="quick-chat-panel-empty">Loading conversation</div>
) : messages.length === 0 && !streamingText && !streamingThinking && !isStreaming && !helpMessageVisible ? ( ) : displayedMessages.length === 0 && !helpMessageVisible ? (
<div className="quick-chat-panel-empty">No messages yet. Start the conversation!</div> <div className="quick-chat-panel-empty">No messages yet. Start the conversation!</div>
) : ( ) : (
<> <>
{messages.map((message: ChatMessageInfo) => ( {displayedMessages.map((message: ChatMessageInfo) => (
<QuickChatMessageItem
key={message.id}
message={message}
forcePlain={message.role !== "user" && plainTextMessageIds.has(message.id)}
mentionAgentsByName={mentionAgentsByName}
roomContext={roomContext}
onToggleRender={toggleMessageRenderMode}
/>
))}
{helpMessageVisible && (
<div className="quick-chat-panel-message quick-chat-panel-message--received" data-testid="quick-chat-help-message">
{renderAssistantMessageContent("Available commands:\n- `/new` or `/clear` — Clear conversation and start fresh\n- `/skill:{name}` — Use a specific skill\n- `/help` — Show this help")}
</div>
)}
</>
) : messagesLoading ? (
<div className="quick-chat-panel-empty">Loading conversation</div>
) : displayedMessages.length === 0 && !streamingText && !streamingThinking && !isStreaming && !helpMessageVisible ? (
<div className="quick-chat-panel-empty">No messages yet. Start the conversation!</div>
) : (
<>
{displayedMessages.map((message: ChatMessageInfo) => (
<QuickChatMessageItem <QuickChatMessageItem
key={message.id} key={message.id}
message={message} message={message}
@@ -3018,7 +3070,7 @@ export function QuickChatFAB({
)} )}
</div> </div>
)} )}
{pendingMessage && ( {!roomThreadActive && pendingMessage && (
<div className="chat-pending-message" data-testid="chat-pending-indicator"> <div className="chat-pending-message" data-testid="chat-pending-indicator">
<span>{`Queued: ${pendingPreview}`}</span> <span>{`Queued: ${pendingPreview}`}</span>
<button <button

View File

@@ -1444,4 +1444,162 @@ describe("QuickChatFAB session-first UX", () => {
expect(openFile).toHaveBeenCalledWith("packages/dashboard/app/components/QuickChatFAB.tsx", { line: undefined, col: undefined }); expect(openFile).toHaveBeenCalledWith("packages/dashboard/app/components/QuickChatFAB.tsx", { line: undefined, col: undefined });
}); });
describe("FN-4849 room switching", () => {
const room = {
id: "room-1",
name: "engineering",
slug: "engineering",
memberCount: 2,
createdAt: "2026-05-16T00:00:00.000Z",
updatedAt: "2026-05-16T00:00:03.000Z",
};
it("switching to a room renders room messages, not session messages", async () => {
const selectRoom = vi.fn();
mockUseAppSettings.mockReturnValue({ experimentalFeatures: { chatRooms: true } } as ReturnType<typeof useAppSettings>);
mockUseChatRooms.mockReturnValue({
rooms: [room],
roomsLoading: false,
roomsError: null,
activeRoom: room,
activeRoomMembers: [],
messages: [
{ id: "room-msg-1", roomId: room.id, role: "assistant", content: "room msg 1", createdAt: "2026-05-16T00:00:01.000Z" },
{ id: "room-msg-2", roomId: room.id, role: "user", content: "room msg 2", createdAt: "2026-05-16T00:00:02.000Z" },
],
messagesLoading: false,
selectRoom,
createRoom: vi.fn(),
deleteRoom: vi.fn(),
sendRoomMessage: vi.fn(),
clearRoom: vi.fn(),
refreshRooms: vi.fn(),
});
mockFetchChatMessages.mockResolvedValueOnce({
messages: [{ id: "session-msg", sessionId: "session-model", role: "assistant", content: "hello from session", createdAt: "2026-05-16T00:00:00.000Z" }],
});
render(<QuickChatFAB addToast={vi.fn()} projectId="proj-1" />);
fireEvent.click(screen.getByTestId("quick-chat-fab"));
const messages = await screen.findByTestId("quick-chat-messages");
expect(messages).toHaveTextContent("room msg 1");
expect(messages).toHaveTextContent("room msg 2");
expect(messages).not.toHaveTextContent("hello from session");
expect(screen.getByTestId("quick-chat-session-dropdown-trigger")).toHaveTextContent("#engineering");
expect(screen.getByTestId("quick-chat-input")).toHaveAttribute("placeholder", "Message #engineering");
});
it("sending while in a room routes to sendRoomMessage, not sendMessage", async () => {
const sendRoomMessage = vi.fn().mockResolvedValue(undefined);
mockUseAppSettings.mockReturnValue({ experimentalFeatures: { chatRooms: true } } as ReturnType<typeof useAppSettings>);
mockUseChatRooms.mockReturnValue({
rooms: [room], roomsLoading: false, roomsError: null, activeRoom: room, activeRoomMembers: [], messages: [], messagesLoading: false,
selectRoom: vi.fn(), createRoom: vi.fn(), deleteRoom: vi.fn(), sendRoomMessage, clearRoom: vi.fn(), refreshRooms: vi.fn(),
});
render(<QuickChatFAB addToast={vi.fn()} projectId="proj-1" />);
fireEvent.click(screen.getByTestId("quick-chat-fab"));
const input = await screen.findByTestId("quick-chat-input");
fireEvent.change(input, { target: { value: "room dispatch" } });
fireEvent.click(screen.getByTestId("quick-chat-send"));
await waitFor(() => {
expect(sendRoomMessage).toHaveBeenCalledWith("room dispatch");
});
expect(mockStreamChatResponse).not.toHaveBeenCalled();
});
it("/clear while in a room calls clearRoom, not startFreshSession", async () => {
const clearRoom = vi.fn().mockResolvedValue(undefined);
mockUseAppSettings.mockReturnValue({ experimentalFeatures: { chatRooms: true } } as ReturnType<typeof useAppSettings>);
mockUseChatRooms.mockReturnValue({
rooms: [room], roomsLoading: false, roomsError: null, activeRoom: room, activeRoomMembers: [], messages: [], messagesLoading: false,
selectRoom: vi.fn(), createRoom: vi.fn(), deleteRoom: vi.fn(), sendRoomMessage: vi.fn(), clearRoom, refreshRooms: vi.fn(),
});
render(<QuickChatFAB addToast={vi.fn()} projectId="proj-1" />);
fireEvent.click(screen.getByTestId("quick-chat-fab"));
const input = await screen.findByTestId("quick-chat-input");
fireEvent.change(input, { target: { value: "/clear" } });
fireEvent.click(screen.getByTestId("quick-chat-send"));
await waitFor(() => {
expect(clearRoom).toHaveBeenCalledWith("room-1");
});
expect(mockCreateChatSession).not.toHaveBeenCalled();
});
it("composer is enabled in a room even without an activeSession", async () => {
mockFetchResumeChatSession.mockRejectedValueOnce(new Error("resume failed"));
mockUseAppSettings.mockReturnValue({ experimentalFeatures: { chatRooms: true } } as ReturnType<typeof useAppSettings>);
mockUseChatRooms.mockReturnValue({
rooms: [room], roomsLoading: false, roomsError: null, activeRoom: room, activeRoomMembers: [], messages: [], messagesLoading: false,
selectRoom: vi.fn(), createRoom: vi.fn(), deleteRoom: vi.fn(), sendRoomMessage: vi.fn(), clearRoom: vi.fn(), refreshRooms: vi.fn(),
});
render(<QuickChatFAB addToast={vi.fn()} projectId="proj-1" />);
fireEvent.click(screen.getByTestId("quick-chat-fab"));
expect(await screen.findByTestId("quick-chat-input")).not.toBeDisabled();
});
it("switching back from a room to a session restores session messages", async () => {
const selectRoom = vi.fn();
mockUseAppSettings.mockReturnValue({ experimentalFeatures: { chatRooms: true } } as ReturnType<typeof useAppSettings>);
mockUseChatRooms.mockReturnValue({
rooms: [room], roomsLoading: false, roomsError: null, activeRoom: room, activeRoomMembers: [],
messages: [{ id: "room-msg-1", roomId: room.id, role: "assistant", content: "room msg 1", createdAt: "2026-05-16T00:00:01.000Z" }],
messagesLoading: false,
selectRoom, createRoom: vi.fn(), deleteRoom: vi.fn(), sendRoomMessage: vi.fn(), clearRoom: vi.fn(), refreshRooms: vi.fn(),
});
mockFetchChatMessages.mockResolvedValueOnce({
messages: [{ id: "session-msg", sessionId: "session-model", role: "assistant", content: "hello from session", createdAt: "2026-05-16T00:00:00.000Z" }],
});
const view = render(<QuickChatFAB addToast={vi.fn()} projectId="proj-1" />);
fireEvent.click(screen.getByTestId("quick-chat-fab"));
fireEvent.click(await screen.findByTestId("quick-chat-session-dropdown-trigger"));
fireEvent.click(screen.getByTestId("quick-chat-session-option-session-model"));
await waitFor(() => {
expect(selectRoom).toHaveBeenCalledWith(null);
});
mockUseChatRooms.mockReturnValue({
rooms: [room], roomsLoading: false, roomsError: null, activeRoom: null, activeRoomMembers: [],
messages: [], messagesLoading: false,
selectRoom, createRoom: vi.fn(), deleteRoom: vi.fn(), sendRoomMessage: vi.fn(), clearRoom: vi.fn(), refreshRooms: vi.fn(),
});
view.rerender(<QuickChatFAB addToast={vi.fn()} projectId="proj-1" />);
expect(await screen.findByTestId("quick-chat-messages")).toHaveTextContent("hello from session");
});
it("attachment-in-room is blocked with a warning toast", async () => {
const addToast = vi.fn();
const sendRoomMessage = vi.fn().mockResolvedValue(undefined);
mockUseAppSettings.mockReturnValue({ experimentalFeatures: { chatRooms: true } } as ReturnType<typeof useAppSettings>);
mockUseChatRooms.mockReturnValue({
rooms: [room], roomsLoading: false, roomsError: null, activeRoom: room, activeRoomMembers: [], messages: [], messagesLoading: false,
selectRoom: vi.fn(), createRoom: vi.fn(), deleteRoom: vi.fn(), sendRoomMessage, clearRoom: vi.fn(), refreshRooms: vi.fn(),
});
render(<QuickChatFAB addToast={addToast} projectId="proj-1" />);
fireEvent.click(screen.getByTestId("quick-chat-fab"));
const attachmentInput = document.querySelector(".quick-chat-attachment-input") as HTMLInputElement | null;
const input = screen.getByTestId("quick-chat-input");
const file = new File(["hi"], "note.txt", { type: "text/plain" });
expect(attachmentInput).not.toBeNull();
fireEvent.change(attachmentInput!, { target: { files: [file] } });
fireEvent.change(input, { target: { value: "try send" } });
fireEvent.click(screen.getByTestId("quick-chat-send"));
expect(addToast).toHaveBeenCalledWith("Attachments are not supported in chat rooms yet", "warning");
expect(sendRoomMessage).not.toHaveBeenCalled();
});
});
}); });