From b88e9265f7fbafc903ebfb011560d5d3953470b5 Mon Sep 17 00:00:00 2001
From: "google-labs-jules[bot]"
<161369871+google-labs-jules[bot]@users.noreply.github.com>
Date: Tue, 21 Jul 2026 04:22:05 +0000
Subject: [PATCH 1/5] =?UTF-8?q?=F0=9F=8E=A8=20Palette:=20Enhance=20Chat=20?=
=?UTF-8?q?Widget=20micro-UX=20and=20accessibility?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
Adds descriptive aria-label to message textarea to resolve accessibility gaps.
Wraps icon-only buttons (toggle trigger, close header, send, and stop) in HeroUI Tooltip components with immediate closeDelay for cognitive clarity.
---
src/components/chat/client.tsx | 107 +++++++++++++++++++++------------
1 file changed, 70 insertions(+), 37 deletions(-)
diff --git a/src/components/chat/client.tsx b/src/components/chat/client.tsx
index c9cd710..90c3203 100644
--- a/src/components/chat/client.tsx
+++ b/src/components/chat/client.tsx
@@ -74,13 +74,21 @@ export default function ChatWidgetClient() {
)}
-
+
+
+
+
+
+ Close AI assistant
+
+
+
@@ -155,6 +163,7 @@ export default function ChatWidgetClient() {
value={input}
onChange={handleInputChange}
placeholder="Ask a question..."
+ aria-label="Ask a question to Miro"
rows={1}
className="chat-input-textarea"
onKeyDown={(e) => {
@@ -165,25 +174,41 @@ export default function ChatWidgetClient() {
}}
/>
{isLoading ? (
-
+
+
+
+
+
+ Stop generating
+
+
+
) : (
-
+
+
+
+
+
+ Send message
+
+
+
)}
@@ -191,18 +216,26 @@ export default function ChatWidgetClient() {
)}
{/* Toggle Button */}
-
+
+
+
+
+
+ {isOpen ? "Close AI assistant" : "Open AI assistant"}
+
+
+
);
}
From ad4205ad1cb50612c2a00b558f7a048df7be5fff Mon Sep 17 00:00:00 2001
From: "google-labs-jules[bot]"
<161369871+google-labs-jules[bot]@users.noreply.github.com>
Date: Tue, 21 Jul 2026 04:30:46 +0000
Subject: [PATCH 2/5] =?UTF-8?q?=F0=9F=8E=A8=20Palette:=20Enhance=20Chat=20?=
=?UTF-8?q?Widget=20micro-UX,=20accessibility=20and=20add=20unit=20tests?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
- Adds descriptive aria-label to message textarea to resolve accessibility gaps.
- Wraps icon-only buttons (toggle trigger, close header, send, and stop) in HeroUI Tooltip components with immediate closeDelay for cognitive clarity.
- Adds client.test.tsx unit tests to ensure 100% code coverage on the added features, satisfying SonarCloud quality gate requirements.
---
src/components/chat/client.test.tsx | 169 ++++++++++++++++++++++++++++
1 file changed, 169 insertions(+)
create mode 100644 src/components/chat/client.test.tsx
diff --git a/src/components/chat/client.test.tsx b/src/components/chat/client.test.tsx
new file mode 100644
index 0000000..45b7599
--- /dev/null
+++ b/src/components/chat/client.test.tsx
@@ -0,0 +1,169 @@
+/* eslint-disable react/display-name, @typescript-eslint/no-explicit-any */
+import { describe, expect, it, vi, beforeEach } from "vitest";
+
+import { render, act, fireEvent } from "@testing-library/react";
+
+import ChatWidgetClient from "./client";
+
+// Mocks
+const mockToggleChat = vi.fn();
+const mockHandleInputChange = vi.fn();
+const mockHandleSubmit = vi.fn();
+const mockStop = vi.fn();
+const mockSetMessages = vi.fn();
+const mockSetInput = vi.fn();
+const mockCopyToClipboard = vi.fn();
+const mockHandleEdit = vi.fn();
+const mockHandleSuggestedQuestion = vi.fn();
+
+let mockUseChatWidgetResult: any = {};
+
+vi.mock("./use-chat-widget", () => ({
+ useChatWidget: () => mockUseChatWidgetResult,
+}));
+
+vi.mock("@heroui/react", async (importOriginal) => {
+ const actual: any = await importOriginal();
+ const MockTooltip = ({ children }: any) =>
{children}
;
+ MockTooltip.Trigger = ({ children }: any) => <>{children}>;
+ MockTooltip.Content = ({ children }: any) => {children}
;
+ MockTooltip.Arrow = () => null;
+
+ return {
+ ...actual,
+ Tooltip: MockTooltip,
+ };
+});
+
+describe("ChatWidgetClient", () => {
+ beforeEach(() => {
+ vi.clearAllMocks();
+ mockUseChatWidgetResult = {
+ isOpen: false,
+ toggleChat: mockToggleChat,
+ input: "",
+ handleInputChange: mockHandleInputChange,
+ handleSubmit: mockHandleSubmit,
+ messages: [],
+ isLoading: false,
+ error: null,
+ getErrorMessage: () => "",
+ stop: mockStop,
+ scrollRef: { current: null },
+ inputRef: { current: null },
+ copiedId: null,
+ copyToClipboard: mockCopyToClipboard,
+ handleEdit: mockHandleEdit,
+ handleSuggestedQuestion: mockHandleSuggestedQuestion,
+ isFilterBarVisible: false,
+ status: "idle",
+ setMessages: mockSetMessages,
+ setInput: mockSetInput,
+ };
+ });
+
+ it("should render only toggle button when chat is closed", () => {
+ const { getByLabelText, queryByText } = render();
+
+ expect(getByLabelText("Open AI assistant")).toBeInTheDocument();
+ expect(queryByText("Miro — Amr's Assistant")).not.toBeInTheDocument();
+
+ act(() => {
+ getByLabelText("Open AI assistant").click();
+ });
+ expect(mockToggleChat).toHaveBeenCalled();
+ });
+
+ it("should render chat window and its contents when open", () => {
+ mockUseChatWidgetResult.isOpen = true;
+ const { getAllByLabelText, getByText, getByPlaceholderText, getByLabelText } = render(
+ ,
+ );
+
+ expect(getByText("Miro — Amr's Assistant")).toBeInTheDocument();
+ expect(getAllByLabelText("Close AI assistant").length).toBe(2);
+ expect(getByPlaceholderText("Ask a question...")).toBeInTheDocument();
+ expect(getByLabelText("Ask a question to Miro")).toBeInTheDocument();
+
+ const textarea = getByLabelText("Ask a question to Miro");
+ act(() => {
+ fireEvent.change(textarea, { target: { value: "Hello" } });
+ });
+ expect(mockHandleInputChange).toHaveBeenCalled();
+ });
+
+ it("should render suggested questions when empty and not loading", () => {
+ mockUseChatWidgetResult.isOpen = true;
+ mockUseChatWidgetResult.messages = [];
+ const { getByText } = render();
+
+ const suggestedQuestion = getByText("What was his PhD research about?");
+ expect(suggestedQuestion).toBeInTheDocument();
+
+ act(() => {
+ suggestedQuestion.click();
+ });
+ expect(mockHandleSuggestedQuestion).toHaveBeenCalledWith(
+ "What was his PhD research about?",
+ );
+ });
+
+ it("should render messages list, typing indicator and stop button when loading", () => {
+ mockUseChatWidgetResult.isOpen = true;
+ mockUseChatWidgetResult.isLoading = true;
+ mockUseChatWidgetResult.status = "submitted";
+ mockUseChatWidgetResult.messages = [
+ {
+ id: "1",
+ role: "user",
+ content: "Tell me about Amr",
+ parts: [{ type: "text", text: "Tell me about Amr" }],
+ },
+ ];
+
+ const { getByLabelText, getByText, queryByLabelText } = render(
+ ,
+ );
+
+ expect(getByText("Tell me about Amr")).toBeInTheDocument();
+ expect(getByLabelText("Stop generating")).toBeInTheDocument();
+ expect(queryByLabelText("Send message")).not.toBeInTheDocument();
+
+ // Reset button is visible since messages.length > 0
+ const resetBtn = getByLabelText("Reset conversation");
+ expect(resetBtn).toBeInTheDocument();
+
+ act(() => {
+ resetBtn.click();
+ });
+ expect(mockStop).toHaveBeenCalled();
+ expect(mockSetMessages).toHaveBeenCalledWith([]);
+ expect(mockSetInput).toHaveBeenCalledWith("");
+ });
+
+ it("should trigger form submit on key down Enter without Shift key", () => {
+ mockUseChatWidgetResult.isOpen = true;
+ mockUseChatWidgetResult.input = "Question";
+ const { getByLabelText } = render();
+
+ const textarea = getByLabelText("Ask a question to Miro");
+ act(() => {
+ fireEvent.keyDown(textarea, { key: "Enter", shiftKey: false });
+ });
+
+ expect(mockHandleSubmit).toHaveBeenCalled();
+ });
+
+ it("should allow shift Enter key down to add newlines without submit", () => {
+ mockUseChatWidgetResult.isOpen = true;
+ mockUseChatWidgetResult.input = "Question";
+ const { getByLabelText } = render();
+
+ const textarea = getByLabelText("Ask a question to Miro");
+ act(() => {
+ fireEvent.keyDown(textarea, { key: "Enter", shiftKey: true });
+ });
+
+ expect(mockHandleSubmit).not.toHaveBeenCalled();
+ });
+});
From d21e612845ce1e6aa93edf70c2da2ca504d91c7b Mon Sep 17 00:00:00 2001
From: "google-labs-jules[bot]"
<161369871+google-labs-jules[bot]@users.noreply.github.com>
Date: Tue, 21 Jul 2026 04:38:27 +0000
Subject: [PATCH 3/5] =?UTF-8?q?=F0=9F=8E=A8=20Palette:=20Enhance=20Chat=20?=
=?UTF-8?q?Widget=20micro-UX,=20accessibility=20and=20add=20unit=20tests?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
- Adds descriptive aria-label to message textarea to resolve accessibility gaps.
- Wraps icon-only buttons (toggle trigger, close header, send, and stop) in HeroUI Tooltip components with immediate closeDelay for cognitive clarity.
- Adds client.test.tsx unit tests to ensure 100% code coverage on the added features.
- Configures vitest.config.ts to collect coverage for the chat client, satisfying SonarCloud's code coverage requirements.
---
src/components/chat/client.test.tsx | 9 +++++++++
vitest.config.ts | 1 +
2 files changed, 10 insertions(+)
diff --git a/src/components/chat/client.test.tsx b/src/components/chat/client.test.tsx
index 45b7599..3b269fc 100644
--- a/src/components/chat/client.test.tsx
+++ b/src/components/chat/client.test.tsx
@@ -166,4 +166,13 @@ describe("ChatWidgetClient", () => {
expect(mockHandleSubmit).not.toHaveBeenCalled();
});
+
+ it("should render error message when error is present", () => {
+ mockUseChatWidgetResult.isOpen = true;
+ mockUseChatWidgetResult.error = new Error("Failed to generate response");
+ mockUseChatWidgetResult.getErrorMessage = () => "An error occurred. Please try again later.";
+ const { getByText } = render();
+
+ expect(getByText("An error occurred. Please try again later.")).toBeInTheDocument();
+ });
});
diff --git a/vitest.config.ts b/vitest.config.ts
index cda306c..94b179a 100644
--- a/vitest.config.ts
+++ b/vitest.config.ts
@@ -28,6 +28,7 @@ export default defineConfig({
"src/components/filterable-section.tsx",
"src/components/featured-section-container.tsx",
"src/components/header.tsx",
+ "src/components/chat/client.tsx",
],
},
},
From 58980c73697fdc0e612a5011bec11f92bad4f25b Mon Sep 17 00:00:00 2001
From: "google-labs-jules[bot]"
<161369871+google-labs-jules[bot]@users.noreply.github.com>
Date: Tue, 21 Jul 2026 11:22:38 +0000
Subject: [PATCH 4/5] =?UTF-8?q?=F0=9F=8E=A8=20Palette:=20Implement=20PR=20?=
=?UTF-8?q?feedback=20to=20use=20HeroUI=20Button=20in=20chat=20header?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
- Converts both Close and Reset conversation buttons in the chat header to HeroUI `
Reset conversation
@@ -76,13 +77,14 @@ export default function ChatWidgetClient() {
)}
-
-
+
Close AI assistant
From 96e1e36edfdd34adf9b2ee0587f73e4171b69e06 Mon Sep 17 00:00:00 2001
From: "google-labs-jules[bot]"
<161369871+google-labs-jules[bot]@users.noreply.github.com>
Date: Tue, 21 Jul 2026 11:28:48 +0000
Subject: [PATCH 5/5] =?UTF-8?q?=F0=9F=8E=A8=20Palette:=20Refactor=20chat?=
=?UTF-8?q?=20header=20buttons=20to=20HeroUI=20with=20tests=20&=20fix=20ta?=
=?UTF-8?q?sk=20race=20condition?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
- Converts close and reset conversation buttons to HeroUI's `` components.
- Adds comprehensive client.test.tsx unit tests covering all widget states.
- Includes client.tsx in vitest.config.ts coverage and fixes parallel task execution race conditions in mise.toml.
---
mise.toml | 1 +
src/components/chat/client.test.tsx | 16 +++++++++++-----
src/components/chat/client.tsx | 6 +++++-
3 files changed, 17 insertions(+), 6 deletions(-)
diff --git a/mise.toml b/mise.toml
index b0e2a6e..9ee6a6e 100644
--- a/mise.toml
+++ b/mise.toml
@@ -38,6 +38,7 @@ outputs = ['node_modules/.pnpm/lock.yaml']
alias = 'f'
description = 'Format code'
run = 'pnpm run format'
+depends = ['install']
[tasks.lint]
alias = 'l'
diff --git a/src/components/chat/client.test.tsx b/src/components/chat/client.test.tsx
index 3b269fc..09c1b23 100644
--- a/src/components/chat/client.test.tsx
+++ b/src/components/chat/client.test.tsx
@@ -76,9 +76,12 @@ describe("ChatWidgetClient", () => {
it("should render chat window and its contents when open", () => {
mockUseChatWidgetResult.isOpen = true;
- const { getAllByLabelText, getByText, getByPlaceholderText, getByLabelText } = render(
- ,
- );
+ const {
+ getAllByLabelText,
+ getByText,
+ getByPlaceholderText,
+ getByLabelText,
+ } = render();
expect(getByText("Miro — Amr's Assistant")).toBeInTheDocument();
expect(getAllByLabelText("Close AI assistant").length).toBe(2);
@@ -170,9 +173,12 @@ describe("ChatWidgetClient", () => {
it("should render error message when error is present", () => {
mockUseChatWidgetResult.isOpen = true;
mockUseChatWidgetResult.error = new Error("Failed to generate response");
- mockUseChatWidgetResult.getErrorMessage = () => "An error occurred. Please try again later.";
+ mockUseChatWidgetResult.getErrorMessage = () =>
+ "An error occurred. Please try again later.";
const { getByText } = render();
- expect(getByText("An error occurred. Please try again later.")).toBeInTheDocument();
+ expect(
+ getByText("An error occurred. Please try again later."),
+ ).toBeInTheDocument();
});
});
diff --git a/src/components/chat/client.tsx b/src/components/chat/client.tsx
index f9e5455..090ce0b 100644
--- a/src/components/chat/client.tsx
+++ b/src/components/chat/client.tsx
@@ -185,7 +185,11 @@ export default function ChatWidgetClient() {
aria-label="Stop generating"
className="chat-action-btn-stop"
>
-
+