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",