story-kit: merge 163_story_remove_bubble_styling_from_streaming_chat_messages
This commit is contained in:
@@ -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(<Chat projectPath="/tmp/project" onCloseProject={vi.fn()} />);
|
||||
|
||||
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(<Chat projectPath="/tmp/project" onCloseProject={vi.fn()} />);
|
||||
|
||||
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(<Chat projectPath="/tmp/project" onCloseProject={vi.fn()} />);
|
||||
|
||||
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(<Chat projectPath="/tmp/project" onCloseProject={vi.fn()} />);
|
||||
|
||||
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(<Chat projectPath="/tmp/project" onCloseProject={vi.fn()} />);
|
||||
|
||||
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");
|
||||
});
|
||||
});
|
||||
|
||||
@@ -761,25 +761,28 @@ export function Chat({ projectPath, onCloseProject }: ChatProps) {
|
||||
>
|
||||
<div
|
||||
style={{
|
||||
maxWidth: "85%",
|
||||
padding: "16px 20px",
|
||||
borderRadius: "12px",
|
||||
background: "#262626",
|
||||
color: "#fff",
|
||||
border: "1px solid #404040",
|
||||
fontFamily: "system-ui, -apple-system, sans-serif",
|
||||
fontSize: "0.95rem",
|
||||
fontWeight: 400,
|
||||
whiteSpace: "pre-wrap",
|
||||
lineHeight: 1.6,
|
||||
maxWidth: "100%",
|
||||
padding: "0",
|
||||
borderRadius: "0",
|
||||
background: "transparent",
|
||||
color: "#ececec",
|
||||
border: "none",
|
||||
fontFamily: "inherit",
|
||||
fontSize: "1em",
|
||||
fontWeight: "500",
|
||||
whiteSpace: "normal",
|
||||
lineHeight: "1.6",
|
||||
}}
|
||||
>
|
||||
<div className="markdown-body">
|
||||
<Markdown
|
||||
components={{
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
// biome-ignore lint/suspicious/noExplicitAny: react-markdown requires any for component props
|
||||
code: ({ className, children, ...props }: any) => {
|
||||
const match = /language-(\w+)/.exec(className || "");
|
||||
const match = /language-(\w+)/.exec(
|
||||
className || "",
|
||||
);
|
||||
const isInline = !className;
|
||||
return !isInline && match ? (
|
||||
<SyntaxHighlighter
|
||||
@@ -802,6 +805,7 @@ export function Chat({ projectPath, onCloseProject }: ChatProps) {
|
||||
</Markdown>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
{loading && (activityStatus != null || !streamingContent) && (
|
||||
<div
|
||||
|
||||
Reference in New Issue
Block a user