Skip to content

Commit

Permalink
fix(frontend): refactor ChatMessageFooter and bubble components to la…
Browse files Browse the repository at this point in the history
…yout children rather than knowing implementation details
  • Loading branch information
rav3n11 committed Jan 23, 2025
1 parent 7d442f0 commit e3541aa
Show file tree
Hide file tree
Showing 23 changed files with 413 additions and 107 deletions.
2 changes: 2 additions & 0 deletions frontend-new/src/_test_utilities/VisualMock.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@ interface VisualMockProps {
| "button"
| "overline";
text: string;
maxWidth?: string;
}

export const VisualMock = (props: VisualMockProps) => {
Expand All @@ -37,6 +38,7 @@ export const VisualMock = (props: VisualMockProps) => {
// otherwise the component will overflow
// e.g. height: //`calc(100% - ${2 * borderWidth}px)`,
height: "100%",
maxWidth: props.maxWidth ?? "100%",
position: "relative",
}}
>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,6 @@ import { nanoid } from "nanoid";
import { ChatMessageType } from "src/chat/Chat.types";

const meta: Meta<typeof BasicChatMessage> = {
// REVIEW: Chat/ChatMessage/Basic
title: "Chat/ChatMessage/Basic",
component: BasicChatMessage,
tags: ["autodocs"],
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,19 +2,20 @@
import "src/_test_utilities/consoleMock";

import BasicChatMessage, { DATA_TEST_ID } from "./BasicChatMessage";
import ChatMessageFooter, { DATA_TEST_ID as CHAT_MESSAGE_FOOTER_DATA_TEST_ID } from "src/chat/chatMessage/components/chatMessageFooter/ChatMessageFooter";
import ChatMessageFooter, { DATA_TEST_ID as CHAT_MESSAGE_FOOTER_DATA_TEST_ID } from "src/chat/chatMessage/components/chatMessageFooter/ChatMessageFooterLayout";
import ChatBubble, { DATA_TEST_ID as CHAT_BUBBLE_DATA_TEST_ID } from "src/chat/chatMessage/components/chatBubble/ChatBubble";
import { render, screen } from "src/_test_utilities/test-utils";
import { ConversationMessageSender } from "src/chat/ChatService/ChatService.types";
import { nanoid } from "nanoid";
import { ChatMessageType, IChatMessage } from "src/chat/Chat.types";
import Timestamp from "../components/chatMessageFooter/components/timestamp/Timestamp";

jest.mock("src/chat/chatMessage/components/chatMessageFooter/ChatMessageFooter", () => {
const originalModule = jest.requireActual("src/chat/chatMessage/components/chatMessageFooter/ChatMessageFooter");
jest.mock("src/chat/chatMessage/components/chatMessageFooter/ChatMessageFooterLayout", () => {
const originalModule = jest.requireActual("src/chat/chatMessage/components/chatMessageFooter/ChatMessageFooterLayout");
return {
__esModule: true,
...originalModule,
default: jest.fn(() => <div data-testid={originalModule.DATA_TEST_ID.CHAT_MESSAGE_TIMESTAMP}></div>),
default: jest.fn(() => <div data-testid={originalModule.DATA_TEST_ID.CHAT_MESSAGE_FOOTER_LAYOUT_CONTAINER}></div>),
}
})

Expand All @@ -27,6 +28,15 @@ jest.mock("src/chat/chatMessage/components/chatBubble/ChatBubble", () => {
}
})

jest.mock("src/chat/chatMessage/components/chatMessageFooter/components/timestamp/Timestamp", () => {
const originalModule = jest.requireActual("src/chat/chatMessage/components/chatMessageFooter/components/timestamp/Timestamp");
return {
__esModule: true,
...originalModule,
default: jest.fn(() => <div data-testid={originalModule.DATA_TEST_ID.TIMESTAMP}></div>),
}
})

describe("render tests", () => {
beforeAll(() => {
jest.useFakeTimers();
Expand Down Expand Up @@ -55,17 +65,14 @@ describe("render tests", () => {
expect(screen.getByTestId(DATA_TEST_ID.CHAT_MESSAGE_CONTAINER)).toBeInTheDocument();
// AND expect the message bubble to be visible
expect(screen.getByTestId(CHAT_BUBBLE_DATA_TEST_ID.CHAT_MESSAGE_BUBBLE_CONTAINER)).toBeInTheDocument();
// AND expect the timestamp to be visible
expect(screen.getByTestId(CHAT_MESSAGE_FOOTER_DATA_TEST_ID.CHAT_MESSAGE_TIMESTAMP)).toBeInTheDocument();
// AND expect the footer to be visible
expect(screen.getByTestId(CHAT_MESSAGE_FOOTER_DATA_TEST_ID.CHAT_MESSAGE_FOOTER_LAYOUT_CONTAINER)).toBeInTheDocument();

// AND the footer to have been called with a timestamp component
const footerCall = (ChatMessageFooter as jest.Mock).mock.calls[0][0];
expect(footerCall.children.type).toBe(Timestamp);
expect(footerCall.children.props).toEqual({ sentAt: givenDate });

// AND the correct date to have been displayed
expect(ChatMessageFooter).toHaveBeenNthCalledWith(
1,
{
sentAt: givenDate
},
{}
)
// AND expect the Chat bubble to have been rendered with the expected message
expect(ChatBubble).toHaveBeenNthCalledWith(
1,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,8 +2,9 @@ import React from "react";
import { Box, styled } from "@mui/material";
import { IChatMessage } from "src/chat/Chat.types";
import { ConversationMessageSender } from "src/chat/ChatService/ChatService.types";
import ChatMessageFooter from "src/chat/chatMessage/components/chatMessageFooter/ChatMessageFooter";
import ChatMessageFooter from "src/chat/chatMessage/components/chatMessageFooter/ChatMessageFooterLayout";
import ChatBubble from "src/chat/chatMessage/components/chatBubble/ChatBubble";
import Timestamp from "src/chat/chatMessage/components/chatMessageFooter/components/timestamp/Timestamp";

const uniqueId = "2fbaf2ef-9eab-485a-bd28-b4a164e18b06";

Expand All @@ -26,8 +27,21 @@ type BasicChatMessageProps = {
const BasicChatMessage: React.FC<BasicChatMessageProps> = ({ chatMessage }) => {
return (
<MessageContainer origin={chatMessage.sender} data-testid={DATA_TEST_ID.CHAT_MESSAGE_CONTAINER}>
<ChatBubble message={chatMessage.message} sender={chatMessage.sender} />
<ChatMessageFooter sentAt={chatMessage.sent_at} />
<Box sx={{
width: "fit-content",
minWidth: "30%",
maxWidth: "80%",
display: "flex",
flexDirection: "column",
alignItems: chatMessage.sender === ConversationMessageSender.COMPASS ? "flex-start" : "flex-end"
}}>
<Box sx={{ width: "100%" }}>
<ChatBubble message={chatMessage.message} sender={chatMessage.sender} />
<ChatMessageFooter sender={chatMessage.sender}>
<Timestamp sentAt={chatMessage.sent_at} />
</ChatMessageFooter>
</Box>
</Box>
</MessageContainer>
);
};
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -7,10 +7,18 @@ exports[`render tests should render the Chat message with a timestamp 1`] = `
origin="COMPASS"
>
<div
data-testid="chat-message-bubble-container-6e685eeb-2b54-432a-8b66-8a81633b3981"
/>
<div
data-testid="chat-message-sent_at-7772f20a-9d0c-4072-b24f-97eca2f43d7b"
/>
class="MuiBox-root css-14jedwi"
>
<div
class="MuiBox-root css-8atqhb"
>
<div
data-testid="chat-message-bubble-container-6e685eeb-2b54-432a-8b66-8a81633b3981"
/>
<div
data-testid="chat-message-footer-layout-container-7772f20a-9d0c-4072-b24f-97eca2f43d7b"
/>
</div>
</div>
</div>
`;
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,6 @@ export const ShownWithFooter: Story = {
args: {
message: "Hello, how can I help you?",
sender: ConversationMessageSender.COMPASS,
footer: <VisualMock text={"Foo Footer"} />
children: <VisualMock text={"Foo Footer"} />
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -25,7 +25,7 @@ describe("render tests", () => {
expect(screen.getByTestId(DATA_TEST_ID.CHAT_MESSAGE_BUBBLE_CONTAINER)).toMatchSnapshot();
});

test("should render the Chat Bubble with a footer if one is passed", () => {
test("should render the Chat Bubble with a child if one is passed", () => {
// GIVEN a message
const givenMessage: string = "Hello, I'm Compass";
// AND a sender
Expand All @@ -34,14 +34,16 @@ describe("render tests", () => {
const givenFooter = <div data-testid={"foo-footer"}>foo child</div>

// WHEN the chat bubble is rendered
render(<ChatBubble message={givenMessage} sender={givenSender} footer={givenFooter}/>);
render(
<ChatBubble message={givenMessage} sender={givenSender}>
{givenFooter}
</ChatBubble>
);

// THEN expect the message container to be visible
expect(screen.getByTestId(DATA_TEST_ID.CHAT_MESSAGE_BUBBLE_CONTAINER)).toBeInTheDocument();
// AND expect the message text to be visible
expect(screen.getByTestId(DATA_TEST_ID.CHAT_MESSAGE_BUBBLE_MESSAGE_TEXT)).toBeInTheDocument();
// AND expect the divider to be visible
expect(screen.getByTestId(DATA_TEST_ID.CHAT_MESSAGE_BUBBLE_MESSAGE_DIVIDER)).toBeInTheDocument();
// AND expect the child container to be visible
expect(screen.getByTestId(DATA_TEST_ID.CHAT_MESSAGE_BUBBLE_MESSAGE_FOOTER_CONTAINER)).toBeInTheDocument();
// AND expect the child to be visible
Expand Down
Original file line number Diff line number Diff line change
@@ -1,24 +1,22 @@
import React from "react";
import { ConversationMessageSender } from "src/chat/ChatService/ChatService.types";
import { Box, Typography, styled, alpha, Divider, useTheme } from "@mui/material";
import { Box, Typography, styled, alpha } from "@mui/material";

export interface ChatBubbleProps {
message: string,
sender: ConversationMessageSender,
footer?: React.ReactNode
children?: React.ReactNode,
}

const uniqueId = "6e685eeb-2b54-432a-8b66-8a81633b3981";

export const DATA_TEST_ID = {
CHAT_MESSAGE_BUBBLE_CONTAINER: `chat-message-bubble-container-${uniqueId}`,
CHAT_MESSAGE_BUBBLE_MESSAGE_TEXT: `chat-message-bubble-message-text-${uniqueId}`,
CHAT_MESSAGE_BUBBLE_MESSAGE_DIVIDER: `chat-message-bubble-message-divider-${uniqueId}`,
CHAT_MESSAGE_BUBBLE_MESSAGE_FOOTER_CONTAINER: `chat-message-bubble-message-footer-container-${uniqueId}`,
}

const MessageBubble = styled(Box)<{ origin: ConversationMessageSender }>(({ theme, origin }) => ({
maxWidth: "80%",
minWidth: "30%",
variants: "outlined",
wordWrap: "break-word",
padding: theme.fixedSpacing(theme.tabiyaSpacing.sm),
Expand All @@ -33,25 +31,13 @@ const MessageBubble = styled(Box)<{ origin: ConversationMessageSender }>(({ them
flexDirection: "column",
}));

const ChatBubble: React.FC<ChatBubbleProps> = ({ message, sender, footer }) => {
const theme = useTheme();
const ChatBubble: React.FC<ChatBubbleProps> = ({ message, sender, children }) => {
return (
<MessageBubble origin={sender} data-testid={DATA_TEST_ID.CHAT_MESSAGE_BUBBLE_CONTAINER}>
<Typography whiteSpace="pre-line" data-testid={DATA_TEST_ID.CHAT_MESSAGE_BUBBLE_MESSAGE_TEXT}>{message}</Typography>
{
footer && (
<>
<Divider
color={theme.palette.grey[100]}
sx={{ marginY: theme.spacing(1) }}
data-testid={DATA_TEST_ID.CHAT_MESSAGE_BUBBLE_MESSAGE_DIVIDER}
/>
<Box data-testid={DATA_TEST_ID.CHAT_MESSAGE_BUBBLE_MESSAGE_FOOTER_CONTAINER}>
{footer}
</Box>
</>
)
}
<Box data-testid={DATA_TEST_ID.CHAT_MESSAGE_BUBBLE_MESSAGE_FOOTER_CONTAINER}>
{children}
</Box>
</MessageBubble>
)
}
Expand Down
Original file line number Diff line number Diff line change
@@ -1,8 +1,8 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP

exports[`render tests should render the Chat Bubble with a footer if one is passed 1`] = `
exports[`render tests should render the Chat Bubble with a child if one is passed 1`] = `
<div
class="MuiBox-root css-1ltd7lq"
class="MuiBox-root css-1y1uy12"
data-testid="chat-message-bubble-container-6e685eeb-2b54-432a-8b66-8a81633b3981"
origin="COMPASS"
>
Expand All @@ -12,11 +12,6 @@ exports[`render tests should render the Chat Bubble with a footer if one is pass
>
Hello, I'm Compass
</p>
<hr
class="MuiDivider-root MuiDivider-fullWidth css-ohsnik-MuiDivider-root"
color="#F3F1EE"
data-testid="chat-message-bubble-message-divider-6e685eeb-2b54-432a-8b66-8a81633b3981"
/>
<div
class="MuiBox-root css-0"
data-testid="chat-message-bubble-message-footer-container-6e685eeb-2b54-432a-8b66-8a81633b3981"
Expand All @@ -32,7 +27,7 @@ exports[`render tests should render the Chat Bubble with a footer if one is pass

exports[`render tests should render the Chat Bubble without a child if none is passed 1`] = `
<div
class="MuiBox-root css-1ltd7lq"
class="MuiBox-root css-1y1uy12"
data-testid="chat-message-bubble-container-6e685eeb-2b54-432a-8b66-8a81633b3981"
origin="COMPASS"
>
Expand All @@ -42,5 +37,9 @@ exports[`render tests should render the Chat Bubble without a child if none is p
>
Hello, I'm Compass
</p>
<div
class="MuiBox-root css-0"
data-testid="chat-message-bubble-message-footer-container-6e685eeb-2b54-432a-8b66-8a81633b3981"
/>
</div>
`;

This file was deleted.

Original file line number Diff line number Diff line change
@@ -0,0 +1,57 @@
import type { Meta, StoryObj } from "@storybook/react";
import ChatMessageFooter from "./ChatMessageFooterLayout";
import Timestamp from "./components/timestamp/Timestamp";
import { VisualMock } from "src/_test_utilities/VisualMock";
import { ConversationMessageSender } from "src/chat/ChatService/ChatService.types";

const meta: Meta<typeof ChatMessageFooter> = {
title: "Chat/ChatMessageFooter/Layout",
component: ChatMessageFooter,
tags: ["autodocs"],
};

export default meta;

type Story = StoryObj<typeof ChatMessageFooter>;

export const ShownWithTimestampCompassSender: Story = {
args: {
sender: ConversationMessageSender.COMPASS,
children: <Timestamp sentAt={new Date().toISOString()} />,
},
}
export const ShownWithTimestampUserSender: Story = {
args: {
sender: ConversationMessageSender.USER,
children: <Timestamp sentAt={new Date().toISOString()} />,
},
}

export const OneFullWidthChild: Story = {
args: {
children: <VisualMock text={"only child"} />,
},
};
export const OneFixedWidthChild: Story = {
args: {
children: <VisualMock maxWidth={"200px"} text={"only child"} />,
},
}

export const MultipleFullWidthChildren: Story = {
args: {
children: [
<VisualMock text={"child 1"} key={"1"}/>,
<VisualMock text={"child 2"} key={"2"}/>,
],
},
};

export const MultipleFixedWidthChildren: Story = {
args: {
children: [
<VisualMock maxWidth={"200px"} text={"child 1"} key={"1"}/>,
<VisualMock maxWidth={"200px"} text={"child 2"} key={"2"}/>,
],
},
};
Loading

0 comments on commit e3541aa

Please sign in to comment.