From 0e9034f4bf00b1d3035e58db85b11a07f70b4b50 Mon Sep 17 00:00:00 2001 From: Madhur Saluja <114940822+MadhurSaluja@users.noreply.github.com> Date: Thu, 28 Nov 2024 05:08:05 +0000 Subject: [PATCH 1/2] Solved issue-148 --- .../ChatBotBody/ChatMessagePrompt.test.tsx | 116 +++++++++++++++ __tests__/services/VoiceService.test.ts | 138 ++++++++++++++++++ package-lock.json | 9 +- package.json | 2 +- 4 files changed, 260 insertions(+), 5 deletions(-) create mode 100644 __tests__/components/ChatBotBody/ChatMessagePrompt.test.tsx create mode 100644 __tests__/services/VoiceService.test.ts 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", From 375ebaed9b490b31c82787ac28c2f94b57cb7907 Mon Sep 17 00:00:00 2001 From: Madhur Saluja <114940822+MadhurSaluja@users.noreply.github.com> Date: Thu, 28 Nov 2024 05:30:45 +0000 Subject: [PATCH 2/2] fixed the error casuing test failure --- .../ChatBotBody/ChatMessagePrompt.test.tsx | 46 ++++-- __tests__/services/VoiceService.test.ts | 138 ------------------ 2 files changed, 33 insertions(+), 151 deletions(-) delete mode 100644 __tests__/services/VoiceService.test.ts diff --git a/__tests__/components/ChatBotBody/ChatMessagePrompt.test.tsx b/__tests__/components/ChatBotBody/ChatMessagePrompt.test.tsx index 4745dc1..a484fcd 100644 --- a/__tests__/components/ChatBotBody/ChatMessagePrompt.test.tsx +++ b/__tests__/components/ChatBotBody/ChatMessagePrompt.test.tsx @@ -2,6 +2,7 @@ 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"; +import { useBotStatesContext } from "../../../src/context/BotStatesContext"; // Mock contexts jest.mock("../../../src/context/BotRefsContext", () => ({ @@ -16,16 +17,8 @@ jest.mock("../../../src/context/BotRefsContext", () => ({ })), })); -const mockSetIsScrolling = jest.fn(); -let unreadCountMock = 0; -let isScrollingMock = false; - jest.mock("../../../src/context/BotStatesContext", () => ({ - useBotStatesContext: jest.fn(() => ({ - unreadCount: unreadCountMock, - isScrolling: isScrollingMock, - setIsScrolling: mockSetIsScrolling, - })), + useBotStatesContext: jest.fn(), })); jest.mock("../../../src/context/SettingsContext", () => ({ @@ -50,26 +43,38 @@ jest.mock("../../../src/context/StylesContext", () => ({ })); describe("ChatMessagePrompt Component", () => { + const mockSetIsScrolling = jest.fn(); + beforeEach(() => { jest.clearAllMocks(); jest.useFakeTimers(); }); afterEach(() => { + jest.runOnlyPendingTimers(); jest.useRealTimers(); }); const renderComponent = () => render(); it("renders with the correct message prompt text", () => { + (useBotStatesContext as jest.Mock).mockReturnValue({ + unreadCount: 0, + isScrolling: false, + setIsScrolling: mockSetIsScrolling, + }); + renderComponent(); const messagePrompt = screen.getByText("Scroll to new messages"); expect(messagePrompt).toBeInTheDocument(); }); it("applies visible class when conditions are met", () => { - unreadCountMock = 2; - isScrollingMock = true; + (useBotStatesContext as jest.Mock).mockReturnValue({ + unreadCount: 2, + isScrolling: true, + setIsScrolling: mockSetIsScrolling, + }); renderComponent(); const messagePrompt = screen.getByText("Scroll to new messages"); @@ -77,8 +82,11 @@ describe("ChatMessagePrompt Component", () => { }); it("applies hidden class when conditions are not met", () => { - unreadCountMock = 0; - isScrollingMock = false; + (useBotStatesContext as jest.Mock).mockReturnValue({ + unreadCount: 0, + isScrolling: false, + setIsScrolling: mockSetIsScrolling, + }); renderComponent(); const messagePromptContainer = screen.queryByText("Scroll to new messages")?.parentElement; @@ -86,6 +94,12 @@ describe("ChatMessagePrompt Component", () => { }); it("applies hover styles when hovered", () => { + (useBotStatesContext as jest.Mock).mockReturnValue({ + unreadCount: 2, + isScrolling: true, + setIsScrolling: mockSetIsScrolling, + }); + renderComponent(); const messagePrompt = screen.getByText("Scroll to new messages"); @@ -102,6 +116,12 @@ describe("ChatMessagePrompt Component", () => { }); it("scrolls to the bottom when clicked", () => { + (useBotStatesContext as jest.Mock).mockReturnValue({ + unreadCount: 2, + isScrolling: true, + setIsScrolling: mockSetIsScrolling, + }); + renderComponent(); const messagePrompt = screen.getByText("Scroll to new messages"); diff --git a/__tests__/services/VoiceService.test.ts b/__tests__/services/VoiceService.test.ts deleted file mode 100644 index 2413c91..0000000 --- a/__tests__/services/VoiceService.test.ts +++ /dev/null @@ -1,138 +0,0 @@ -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(); - }); - }); -});