diff --git a/__tests__/components/ChatBotBody/ChatMessagePrompt.test.tsx b/__tests__/components/ChatBotBody/ChatMessagePrompt.test.tsx
new file mode 100644
index 0000000..4745dc1
--- /dev/null
+++ b/__tests__/components/ChatBotBody/ChatMessagePrompt.test.tsx
@@ -0,0 +1,116 @@
+import React from "react";
+import { render, screen, fireEvent } from "@testing-library/react";
+import "@testing-library/jest-dom";
+import ChatMessagePrompt from "../../../src/components/ChatBotBody/ChatMessagePrompt/ChatMessagePrompt";
+
+// Mock contexts
+jest.mock("../../../src/context/BotRefsContext", () => ({
+ useBotRefsContext: jest.fn(() => ({
+ chatBodyRef: {
+ current: {
+ scrollTop: 0,
+ scrollHeight: 1000,
+ clientHeight: 400,
+ },
+ },
+ })),
+}));
+
+const mockSetIsScrolling = jest.fn();
+let unreadCountMock = 0;
+let isScrollingMock = false;
+
+jest.mock("../../../src/context/BotStatesContext", () => ({
+ useBotStatesContext: jest.fn(() => ({
+ unreadCount: unreadCountMock,
+ isScrolling: isScrollingMock,
+ setIsScrolling: mockSetIsScrolling,
+ })),
+}));
+
+jest.mock("../../../src/context/SettingsContext", () => ({
+ useSettingsContext: jest.fn(() => ({
+ settings: {
+ general: { primaryColor: "#000" },
+ chatWindow: {
+ showMessagePrompt: true,
+ messagePromptText: "Scroll to new messages",
+ },
+ },
+ })),
+}));
+
+jest.mock("../../../src/context/StylesContext", () => ({
+ useStylesContext: jest.fn(() => ({
+ styles: {
+ chatMessagePromptStyle: { color: "#fff", borderColor: "#ccc" },
+ chatMessagePromptHoveredStyle: { color: "#000", borderColor: "#000" },
+ },
+ })),
+}));
+
+describe("ChatMessagePrompt Component", () => {
+ beforeEach(() => {
+ jest.clearAllMocks();
+ jest.useFakeTimers();
+ });
+
+ afterEach(() => {
+ jest.useRealTimers();
+ });
+
+ const renderComponent = () => render();
+
+ it("renders with the correct message prompt text", () => {
+ renderComponent();
+ const messagePrompt = screen.getByText("Scroll to new messages");
+ expect(messagePrompt).toBeInTheDocument();
+ });
+
+ it("applies visible class when conditions are met", () => {
+ unreadCountMock = 2;
+ isScrollingMock = true;
+
+ renderComponent();
+ const messagePrompt = screen.getByText("Scroll to new messages");
+ expect(messagePrompt.parentElement).toHaveClass("rcb-message-prompt-container visible");
+ });
+
+ it("applies hidden class when conditions are not met", () => {
+ unreadCountMock = 0;
+ isScrollingMock = false;
+
+ renderComponent();
+ const messagePromptContainer = screen.queryByText("Scroll to new messages")?.parentElement;
+ expect(messagePromptContainer).toHaveClass("rcb-message-prompt-container hidden");
+ });
+
+ it("applies hover styles when hovered", () => {
+ renderComponent();
+ const messagePrompt = screen.getByText("Scroll to new messages");
+
+ // Before hover
+ expect(messagePrompt).toHaveStyle({ color: "#fff", borderColor: "#ccc" });
+
+ // Hover
+ fireEvent.mouseEnter(messagePrompt);
+ expect(messagePrompt).toHaveStyle({ color: "#000", borderColor: "#000" });
+
+ // Leave hover
+ fireEvent.mouseLeave(messagePrompt);
+ expect(messagePrompt).toHaveStyle({ color: "#fff", borderColor: "#ccc" });
+ });
+
+ it("scrolls to the bottom when clicked", () => {
+ renderComponent();
+ const messagePrompt = screen.getByText("Scroll to new messages");
+
+ fireEvent.mouseDown(messagePrompt);
+
+ // Simulate scrolling completion
+ jest.advanceTimersByTime(600);
+
+ // Verify that setIsScrolling was called
+ expect(mockSetIsScrolling).toHaveBeenCalledWith(false);
+ });
+});
diff --git a/__tests__/services/VoiceService.test.ts b/__tests__/services/VoiceService.test.ts
new file mode 100644
index 0000000..2413c91
--- /dev/null
+++ b/__tests__/services/VoiceService.test.ts
@@ -0,0 +1,138 @@
+import { jest, SpyInstance } from "@jest/globals";
+import * as VoiceService from "../../src/services/VoiceService";
+
+describe("VoiceService", () => {
+ let mockRecognition: jest.Mocked;
+ let mockMediaRecorder: jest.Mocked;
+ let mockStopVoiceRecording: jest.SpyInstance;
+
+ beforeAll(() => {
+ // Mock navigator.mediaDevices if undefined
+ if (!navigator.mediaDevices) {
+ Object.defineProperty(navigator, "mediaDevices", {
+ value: {
+ getUserMedia: jest.fn(),
+ },
+ writable: true,
+ });
+ }
+ });
+
+ beforeEach(() => {
+ // Mock SpeechRecognition
+ mockRecognition = {
+ start: jest.fn(),
+ stop: jest.fn(),
+ onresult: null,
+ onend: null,
+ } as unknown as jest.Mocked;
+
+ window.SpeechRecognition = jest.fn(() => mockRecognition) as any;
+
+ // Mock MediaRecorder
+ mockMediaRecorder = {
+ start: jest.fn(),
+ stop: jest.fn(),
+ ondataavailable: null,
+ onstop: null,
+ state: "inactive",
+ } as unknown as jest.Mocked;
+
+ global.MediaRecorder = jest.fn(() => mockMediaRecorder) as any;
+
+ // Mock getUserMedia
+ jest.spyOn(navigator.mediaDevices, "getUserMedia").mockResolvedValue({
+ active: true,
+ id: "mockStreamId",
+ onaddtrack: null,
+ onremovetrack: null,
+ getTracks: jest.fn(() => [{ kind: "audio", stop: jest.fn() }]),
+ addTrack: jest.fn(),
+ removeTrack: jest.fn(),
+ getAudioTracks: jest.fn(() => []),
+ getVideoTracks: jest.fn(() => []),
+ clone: jest.fn(),
+ addEventListener: jest.fn(),
+ removeEventListener: jest.fn(),
+ dispatchEvent: jest.fn(),
+ } as unknown as MediaStream);
+
+ mockStopVoiceRecording = jest.spyOn(VoiceService, "stopVoiceRecording");
+ });
+
+ afterEach(() => {
+ jest.restoreAllMocks(); // Clean up mocks after each test
+ });
+
+ describe("startVoiceRecording", () => {
+ it("should start SpeechRecognition when sendAsAudio is false", () => {
+ const settings = { voice: { sendAsAudio: false, language: "en-US" } } as any;
+ const mockToggleVoice = jest.fn(() => Promise.resolve());
+
+ VoiceService.startVoiceRecording(
+ settings,
+ mockToggleVoice,
+ jest.fn(),
+ jest.fn(),
+ jest.fn(),
+ { current: [] },
+ { current: null }
+ );
+
+ expect(mockRecognition.start).toHaveBeenCalled();
+ });
+
+ it("should start MediaRecorder when sendAsAudio is true", async () => {
+ const settings = { voice: { sendAsAudio: true } } as any;
+ const mockToggleVoice = jest.fn(() => Promise.resolve());
+
+ await VoiceService.startVoiceRecording(
+ settings,
+ mockToggleVoice,
+ jest.fn(),
+ jest.fn(),
+ jest.fn(),
+ { current: [] },
+ { current: null }
+ );
+
+ expect(navigator.mediaDevices.getUserMedia).toHaveBeenCalledWith({ audio: true });
+ expect(mockMediaRecorder.start).toHaveBeenCalled();
+ });
+ });
+
+ describe("stopVoiceRecording", () => {
+ it("should stop SpeechRecognition and MediaRecorder", () => {
+ VoiceService.stopVoiceRecording();
+
+ expect(mockRecognition.stop).toHaveBeenCalled();
+ expect(mockMediaRecorder.stop).toHaveBeenCalled();
+ });
+ });
+
+ describe("syncVoiceWithChatInput", () => {
+ it("should start MediaRecorder if keepVoiceOn is true and sendAsAudio is enabled", () => {
+ const settings = { voice: { sendAsAudio: true, disabled: false } } as any;
+
+ VoiceService.syncVoiceWithChatInput(true, settings);
+
+ expect(mockMediaRecorder.start).toHaveBeenCalled();
+ });
+
+ it("should start SpeechRecognition if keepVoiceOn is true and sendAsAudio is disabled", () => {
+ const settings = { voice: { sendAsAudio: false, disabled: false } } as any;
+
+ VoiceService.syncVoiceWithChatInput(true, settings);
+
+ expect(mockRecognition.start).toHaveBeenCalled();
+ });
+
+ it("should stop all voice recording if keepVoiceOn is false", () => {
+ const settings = { voice: { sendAsAudio: false, disabled: false } } as any;
+
+ VoiceService.syncVoiceWithChatInput(false, settings);
+
+ expect(mockStopVoiceRecording).toHaveBeenCalled();
+ });
+ });
+});
diff --git a/package-lock.json b/package-lock.json
index 5b46189..6ae7367 100644
--- a/package-lock.json
+++ b/package-lock.json
@@ -12,7 +12,7 @@
"@testing-library/jest-dom": "^6.6.2",
"@testing-library/react": "^16.0.1",
"@types/dom-speech-recognition": "^0.0.4",
- "@types/jest": "^29.5.13",
+ "@types/jest": "^29.5.14",
"@types/react": "^18.3.6",
"@types/react-dom": "^18.3.0",
"@typescript-eslint/eslint-plugin": "^5.60.1",
@@ -2320,10 +2320,11 @@
}
},
"node_modules/@types/jest": {
- "version": "29.5.13",
- "resolved": "https://registry.npmjs.org/@types/jest/-/jest-29.5.13.tgz",
- "integrity": "sha512-wd+MVEZCHt23V0/L642O5APvspWply/rGY5BcW4SUETo2UzPU3Z26qr8jC2qxpimI2jjx9h7+2cj2FwIr01bXg==",
+ "version": "29.5.14",
+ "resolved": "https://registry.npmjs.org/@types/jest/-/jest-29.5.14.tgz",
+ "integrity": "sha512-ZN+4sdnLUbo8EVvVc2ao0GFW6oVrQRPn4K2lglySj7APvSrgzxHiNNK99us4WDMi57xxA2yggblIAMNhXOotLQ==",
"dev": true,
+ "license": "MIT",
"dependencies": {
"expect": "^29.0.0",
"pretty-format": "^29.0.0"
diff --git a/package.json b/package.json
index 8548f7d..7ce8ac0 100644
--- a/package.json
+++ b/package.json
@@ -54,7 +54,7 @@
"@testing-library/jest-dom": "^6.6.2",
"@testing-library/react": "^16.0.1",
"@types/dom-speech-recognition": "^0.0.4",
- "@types/jest": "^29.5.13",
+ "@types/jest": "^29.5.14",
"@types/react": "^18.3.6",
"@types/react-dom": "^18.3.0",
"@typescript-eslint/eslint-plugin": "^5.60.1",