Repository: tjtanjin/react-chatbotify (ts)

Issue:
[Task] Add unit test cases for VoiceButton component
**Task Description:**
Currently, we run unit tests using jest which can be found [**here**](https://github.com/tjtanjin/react-chatbotify/tree/main/__tests__). 

However, the test cases are not yet comprehensive enough and work can be done to cover more grounds. Specifically for this issue, we hope to add test cases for the `VoiceButton` component.

**Deliverable(s):**
New unit test cases that cover the testing of the `VoiceButton` component.

**Additional Context:**
- Button components can be found [**here**](https://github.com/tjtanjin/react-chatbotify/tree/main/src/components/Buttons)
- An example for `AudioButton` can be found [**here**](https://github.com/tjtanjin/react-chatbotify/blob/main/__tests__/components/AudioButton.test.tsx) which may serve as a good source for reference


Pull request:
Add unit tests for Voice Button component
#### Description

Add unit tests for Voice Button component

Closes #137 

#### What change does this PR introduce?

Please select the relevant option(s).

- [ ] Bug fix (non-breaking change which fixes an issue)
- [x] New feature (non-breaking change which adds functionality)
- [ ] Breaking change (fix or feature that would cause existing functionality to not work as expected)
- [ ] Documentation update (changes to docs/code comments)

#### What is the proposed approach?

An approach similar to the implementation of unit tests for Notification Button component is followed.

<img width="843" alt="image" src="https://github.com/user-attachments/assets/2330b598-35a9-414e-8ebd-f655d24a3b6f" />

#### Checklist:

- [x] The commit message follows our adopted [guidelines](https://www.conventionalcommits.org/en/v1.0.0/)
- [n/a] Testing has been done for the change(s) added (for bug fixes/features)
- [n/a] Relevant comments/docs have been added/updated (for bug fixes/features)

Patch:
diff --git a/src/components/Buttons/VoiceButton/VoiceButton.tsx b/src/components/Buttons/VoiceButton/VoiceButton.tsx
index c2144d1..e291b06 100644
--- a/src/components/Buttons/VoiceButton/VoiceButton.tsx
+++ b/src/components/Buttons/VoiceButton/VoiceButton.tsx
@@ -110,6 +110,7 @@ const VoiceButton = () => {
 			return (
 				<span
 					className={`rcb-voice-icon${voiceToggledOn && !textAreaDisabled ? "-on" : ""}`}
+					data-testid="rcb-voice-icon"
 					style={voiceToggledOn && !textAreaDisabled ? voiceIconStyle : voiceIconDisabledStyle}
 				/>
 			)


Test patch:
diff --git a/__tests__/__mocks__/fileMock.ts b/__tests__/__mocks__/fileMock.ts
index 92ede8e..d12825e 100644
--- a/__tests__/__mocks__/fileMock.ts
+++ b/__tests__/__mocks__/fileMock.ts
@@ -1,10 +1,12 @@
 // __mocks__/fileMock.ts
-export const botAvatar = "../../assets/bot_avatar.svg";
+export const audioIcon = "../../assets/audio_icon.svg";
+export const audioIconDisabled = "../../assets/audio_icon_disabled.svg";
 export const actionDisabledIcon = "../../assets/action_disabled_icon.svg";
-export const emojiIcon = "../../assets/emoji_icon.svg";
+export const botAvatar = "../../assets/bot_avatar.svg";
 export const closeChatIcon = "../../assets/close_chat_icon.svg";
+export const emojiIcon = "../../assets/emoji_icon.svg";
 export const notificationIcon = "../../assets/notification_icon.svg";
 export const notificationIconDisabled = "../../assets/notification_icon_disabled.svg";
-export const audioIcon = "../../assets/audio_icon.svg";
-export const audioIconDisabled = "../../assets/audio_icon_disabled.svg";
-export const sendIcon = "../../assets/send_icon.svg";
\ No newline at end of file
+export const sendIcon = "../../assets/send_icon.svg";
+export const voiceIcon = "../../assets/voice_icon.svg";
+export const voiceIconDisabled = "../../assets/voice_icon_disabled.svg";
diff --git a/__tests__/components/buttons/VoiceButton.test.tsx b/__tests__/components/buttons/VoiceButton.test.tsx
new file mode 100644
index 0000000..03aaf68
--- /dev/null
+++ b/__tests__/components/buttons/VoiceButton.test.tsx
@@ -0,0 +1,110 @@
+import React from "react";
+
+import { expect } from "@jest/globals";
+import { render, screen, fireEvent } from "@testing-library/react";
+import "@testing-library/jest-dom/jest-globals";
+
+import VoiceButton from "../../../src/components/Buttons/VoiceButton/VoiceButton";
+import { DefaultSettings } from "../../../src/constants/internal/DefaultSettings";
+import { useTextAreaInternal } from "../../../src/hooks/internal/useTextAreaInternal";
+import { TestChatBotProvider } from "../../__mocks__/TestChatBotContext";
+import { voiceIcon, voiceIconDisabled } from "../../__mocks__/fileMock";
+
+jest.mock("../../../src/hooks/internal/useTextAreaInternal");
+
+/**
+ * Helper function to render VoiceButton with different settings.
+ *
+ * @param disabled boolean indicating if voice option is disabled
+ * @param defaultToggledOn boolean indicating if voice option is toggled on by default
+ */
+const renderVoiceButton = (disabled: boolean, defaultToggledOn: boolean) => {
+	const initialSettings = {
+		voice: {
+			disabled: disabled,
+			defaultToggledOn: defaultToggledOn,
+			icon: voiceIcon,
+			iconDisabled: voiceIconDisabled,
+		},
+	};
+	return render(
+		<TestChatBotProvider initialSettings={initialSettings}>
+			<VoiceButton />
+		</TestChatBotProvider>
+	);
+};
+
+/**
+ * Tests for VoiceButton component.
+ */
+
+describe("VoiceButton Component", () => {
+	const voiceIconTestId = "rcb-voice-icon";
+
+	beforeEach(() => {
+		(useTextAreaInternal as jest.Mock).mockReturnValue({
+			textAreaDisabled: false,
+		});
+	});
+
+	it("renders with aria-label and initial state when defaultToggledOn is false and not disabled", () => {
+		renderVoiceButton(false, false);
+
+		const button = screen.getByRole("button", {
+			name: DefaultSettings.ariaLabel?.voiceButton,
+		});
+		const icon = screen.getByTestId(voiceIconTestId);
+
+		expect(button).toBeInTheDocument();
+
+		expect(icon).toHaveStyle("fill: #9aa0a6");
+		expect(icon.style.backgroundImage).toBe(`url(${voiceIconDisabled})`);
+	});
+
+	it("toggles voice state when clicked (initially off)", () => {
+		renderVoiceButton(false, false);
+
+		const button = screen.getByRole("button", {
+			name: DefaultSettings.ariaLabel?.voiceButton,
+		});
+		const icon = screen.getByTestId(voiceIconTestId);
+
+		expect(icon.style.backgroundImage).toBe(`url(${voiceIconDisabled})`);
+
+		fireEvent.mouseDown(button);
+
+		expect(icon.style.backgroundImage).toBe(`url(${voiceIcon})`);
+	});
+
+	it("renders with voice toggled on initially and toggles to off when clicked", () => {
+		renderVoiceButton(false, true);
+
+		const button = screen.getByRole("button", {
+			name: DefaultSettings.ariaLabel?.voiceButton,
+		});
+		const icon = screen.getByTestId(voiceIconTestId);
+		expect(icon.style.backgroundImage).toBe(`url(${voiceIcon})`);
+
+		fireEvent.mouseDown(button);
+
+		expect(icon.style.backgroundImage).toBe(`url(${voiceIconDisabled})`);
+	});
+
+	it("toggles notification back to on after being toggled off", () => {
+		renderVoiceButton(false, true);
+
+		const button = screen.getByRole("button", {
+			name: DefaultSettings.ariaLabel?.voiceButton,
+		});
+		const icon = screen.getByTestId(voiceIconTestId);
+		expect(icon.style.backgroundImage).toBe(`url(${voiceIcon})`);
+
+		fireEvent.mouseDown(button);
+
+		expect(icon.style.backgroundImage).toBe(`url(${voiceIconDisabled})`);
+
+		fireEvent.mouseDown(button);
+
+		expect(icon.style.backgroundImage).toBe(`url(${voiceIcon})`);
+	});
+});