diff --git a/frontend/src/components/Chat.test.tsx b/frontend/src/components/Chat.test.tsx index d59f1ab..174fa22 100644 --- a/frontend/src/components/Chat.test.tsx +++ b/frontend/src/components/Chat.test.tsx @@ -892,3 +892,164 @@ describe("Chat message queue (Story 155)", () => { }); }); }); + +describe("Remove bubble styling from streaming messages (Story 163)", () => { + beforeEach(() => { + capturedWsHandlers = null; + setupMocks(); + }); + + it("AC1: streaming assistant message uses transparent background, no extra padding, no border-radius", async () => { + render(); + + await waitFor(() => expect(capturedWsHandlers).not.toBeNull()); + + // Send a message to put chat into loading state + const input = screen.getByPlaceholderText("Send a message..."); + await act(async () => { + fireEvent.change(input, { target: { value: "Hello" } }); + }); + await act(async () => { + fireEvent.keyDown(input, { key: "Enter", shiftKey: false }); + }); + + // Simulate streaming tokens arriving + act(() => { + capturedWsHandlers?.onToken("Streaming response text"); + }); + + // Find the streaming message container (the inner div wrapping the Markdown) + const streamingText = await screen.findByText("Streaming response text"); + // The markdown-body wrapper is the parent, and the styled div is its parent + const styledDiv = streamingText.closest(".markdown-body") + ?.parentElement as HTMLElement; + + expect(styledDiv).toBeTruthy(); + const styleAttr = styledDiv.getAttribute("style") ?? ""; + expect(styleAttr).toContain("background: transparent"); + expect(styleAttr).toContain("padding: 0px"); + expect(styleAttr).toContain("border-radius: 0px"); + expect(styleAttr).toContain("max-width: 100%"); + }); + + it("AC1: streaming message wraps Markdown in markdown-body class", async () => { + render(); + + await waitFor(() => expect(capturedWsHandlers).not.toBeNull()); + + const input = screen.getByPlaceholderText("Send a message..."); + await act(async () => { + fireEvent.change(input, { target: { value: "Hello" } }); + }); + await act(async () => { + fireEvent.keyDown(input, { key: "Enter", shiftKey: false }); + }); + + act(() => { + capturedWsHandlers?.onToken("Some markdown content"); + }); + + const streamingText = await screen.findByText("Some markdown content"); + const markdownBody = streamingText.closest(".markdown-body"); + expect(markdownBody).toBeTruthy(); + }); + + it("AC2: no visual change when streaming ends and message transitions to completed", async () => { + render(); + + await waitFor(() => expect(capturedWsHandlers).not.toBeNull()); + + // Send a message to start streaming + const input = screen.getByPlaceholderText("Send a message..."); + await act(async () => { + fireEvent.change(input, { target: { value: "Hello" } }); + }); + await act(async () => { + fireEvent.keyDown(input, { key: "Enter", shiftKey: false }); + }); + + // Simulate streaming tokens + act(() => { + capturedWsHandlers?.onToken("Final response"); + }); + + // Capture streaming message style attribute + const streamingText = await screen.findByText("Final response"); + const streamingStyledDiv = streamingText.closest(".markdown-body") + ?.parentElement as HTMLElement; + const streamingStyleAttr = streamingStyledDiv.getAttribute("style") ?? ""; + + // Transition: onUpdate completes the message + act(() => { + capturedWsHandlers?.onUpdate([ + { role: "user", content: "Hello" }, + { role: "assistant", content: "Final response" }, + ]); + }); + + // Find the completed message — it should have the same styling + const completedText = await screen.findByText("Final response"); + const completedMarkdownBody = completedText.closest(".markdown-body"); + const completedStyledDiv = + completedMarkdownBody?.parentElement as HTMLElement; + + expect(completedStyledDiv).toBeTruthy(); + const completedStyleAttr = completedStyledDiv.getAttribute("style") ?? ""; + + // Both streaming and completed use transparent bg, 0 padding, 0 border-radius + expect(completedStyleAttr).toContain("background: transparent"); + expect(completedStyleAttr).toContain("padding: 0px"); + expect(completedStyleAttr).toContain("border-radius: 0px"); + expect(streamingStyleAttr).toContain("background: transparent"); + expect(streamingStyleAttr).toContain("padding: 0px"); + expect(streamingStyleAttr).toContain("border-radius: 0px"); + + // Both have the markdown-body class wrapper + expect(streamingStyledDiv.querySelector(".markdown-body")).toBeTruthy(); + }); + + it("AC3: completed assistant messages retain transparent background and no border-radius", async () => { + render(); + + await waitFor(() => expect(capturedWsHandlers).not.toBeNull()); + + act(() => { + capturedWsHandlers?.onUpdate([ + { role: "user", content: "Hi" }, + { role: "assistant", content: "Hello there!" }, + ]); + }); + + const assistantText = await screen.findByText("Hello there!"); + const markdownBody = assistantText.closest(".markdown-body"); + const styledDiv = markdownBody?.parentElement as HTMLElement; + + expect(styledDiv).toBeTruthy(); + const styleAttr = styledDiv.getAttribute("style") ?? ""; + expect(styleAttr).toContain("background: transparent"); + expect(styleAttr).toContain("padding: 0px"); + expect(styleAttr).toContain("border-radius: 0px"); + expect(styleAttr).toContain("max-width: 100%"); + }); + + it("AC3: completed user messages still have their bubble styling", async () => { + render(); + + await waitFor(() => expect(capturedWsHandlers).not.toBeNull()); + + act(() => { + capturedWsHandlers?.onUpdate([ + { role: "user", content: "I am a user message" }, + { role: "assistant", content: "I am a response" }, + ]); + }); + + // findByText returns the styled div itself for user messages (text is direct child) + const userStyledDiv = await screen.findByText("I am a user message"); + const styleAttr = userStyledDiv.getAttribute("style") ?? ""; + // User messages retain bubble: distinct background, padding, rounded corners + expect(styleAttr).toContain("padding: 10px 16px"); + expect(styleAttr).toContain("border-radius: 20px"); + expect(styleAttr).not.toContain("background: transparent"); + }); +}); diff --git a/frontend/src/components/Chat.tsx b/frontend/src/components/Chat.tsx index 3a385da..c9ea9b4 100644 --- a/frontend/src/components/Chat.tsx +++ b/frontend/src/components/Chat.tsx @@ -761,45 +761,49 @@ export function Chat({ projectPath, onCloseProject }: ChatProps) { >
- { - const match = /language-(\w+)/.exec(className || ""); - const isInline = !className; - return !isInline && match ? ( - - {String(children).replace(/\n$/, "")} - - ) : ( - - {children} - - ); - }, - }} - > - {streamingContent} - +
+ { + const match = /language-(\w+)/.exec( + className || "", + ); + const isInline = !className; + return !isInline && match ? ( + + {String(children).replace(/\n$/, "")} + + ) : ( + + {children} + + ); + }, + }} + > + {streamingContent} + +
)}