Repository: tjtanjin/react-chatbotify
Pull request #163 (2024-10-08 04:28:18)

test: add unit test for SendButton component
#### Description

Added unit test for SendButton component.

Closes #([142](https://github.com/tjtanjin/react-chatbotify/issues/142))

#### What change does this PR introduce?

Please select the relevant option(s).

- [ ] Bug fix (non-breaking change which fixes an issue)
- [ ] 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)
- [x] Unit Test

#### What is the proposed approach?

Implement unit test using Jest and react testing library.

#### Checklist:

- [x] The commit message follows our adopted [guidelines](https://www.conventionalcommits.org/en/v1.0.0/)
- [x] Testing has been done for the change(s) added (for bug fixes/features)
- [x] Relevant comments/docs have been added/updated (for bug fixes/features)


Patch:
diff --git a/.github/workflows/lint-and-build.yml b/.github/workflows/lint-and-build.yml
index f3aafb2..8d1cab4 100644
--- a/.github/workflows/lint-and-build.yml
+++ b/.github/workflows/lint-and-build.yml
@@ -35,7 +35,7 @@ jobs:
 
     strategy:
       matrix:
-        node-version: [16.x, 17.x, 18.x, 19.x, 20.x, 21.x, 22.x]
+        node-version: [18.x, 20.x, 22.x]
 
     steps:
       - name: Checkout code
diff --git a/src/components/ChatBotBody/MediaDisplay/MediaDisplay.tsx b/src/components/ChatBotBody/MediaDisplay/MediaDisplay.tsx
index bee4d91..a3fcea0 100644
--- a/src/components/ChatBotBody/MediaDisplay/MediaDisplay.tsx
+++ b/src/components/ChatBotBody/MediaDisplay/MediaDisplay.tsx
@@ -43,6 +43,7 @@ const MediaDisplay = ({
 						<div
 							style={mediaDisplayContainerStyle}
 							className="rcb-media-display-image-container rcb-media-entry"
+							data-testid="media-display-image-container"
 						>
 							<img src={fileUrl} alt="Image Display" className="rcb-media-display-image" />
 						</div>
@@ -51,6 +52,7 @@ const MediaDisplay = ({
 						<div 
 							style={mediaDisplayContainerStyle}
 							className="rcb-media-display-video-container rcb-media-entry"
+							data-testid="media-display-video-container"
 						>
 							<video controls className="rcb-media-display-video">
 								<source src={fileUrl} type={file.type} />
@@ -60,6 +62,7 @@ const MediaDisplay = ({
 					)}
 					{fileType === "audio" && fileUrl && (
 						<audio
+							data-testid="media-display-audio-container" 
 							style={{
 								maxWidth: settings.userBubble?.showAvatar ? "65%" : "70%",
 							}}


Test patch:
diff --git a/__tests__/components/ChatBotBody/MediaDisplay.test.tsx b/__tests__/components/ChatBotBody/MediaDisplay.test.tsx
new file mode 100644
index 0000000..8a69660
--- /dev/null
+++ b/__tests__/components/ChatBotBody/MediaDisplay.test.tsx
@@ -0,0 +1,89 @@
+
+import React from "react";
+import "@testing-library/jest-dom";
+import { render, screen } from "@testing-library/react";
+import MediaDisplay from "../../../src/components/ChatBotBody/MediaDisplay/MediaDisplay";
+import { useSettingsContext } from "../../../src/context/SettingsContext";
+import { useStylesContext } from "../../../src/context/StylesContext";
+
+// Mock the contexts
+jest.mock("../../../src/context/SettingsContext", () => ({
+	useSettingsContext: jest.fn(),
+}));
+
+jest.mock("../../../src/context/StylesContext", () => ({
+	useStylesContext: jest.fn(),
+}));
+
+const mockSettingsContext = {
+	settings: {
+		general: { primaryColor: "#000000" },
+		userBubble: { showAvatar: true },
+	}
+};
+
+const mockStylesContext = {
+	styles: {
+		mediaDisplayContainerStyle: { border: "1px solid red" }
+	}
+};
+
+// Helper function to render MediaDisplay with mocked contexts
+const renderMediaDisplay = (file: File, fileType: string, fileUrl: string | null) => {
+	(useSettingsContext as jest.Mock).mockReturnValue(mockSettingsContext);
+	(useStylesContext as jest.Mock).mockReturnValue(mockStylesContext);
+	render(<MediaDisplay file={file} fileType={fileType} fileUrl={fileUrl} />);
+};
+
+describe("MediaDisplay", () => {
+	it("renders an image correctly when fileType is 'image'", () => {
+		const file = new File(["dummy content"], "tanjin.png", { type: "image/png" });
+		renderMediaDisplay(file, "image", "https://react-chatbot.com/tanjin.png");
+
+		const imageElement = screen.getByRole("img");
+		expect(imageElement).toBeInTheDocument();
+		expect(imageElement).toHaveAttribute("src", "https://react-chatbot.com/tanjin.png");
+
+		const container = screen.getByTestId("media-display-image-container");
+		expect(container).toHaveStyle({
+			backgroundColor: "#000000",
+			maxWidth: "65%",
+			border: "1px solid red",
+		});
+	});
+
+	it("renders a video correctly when fileType is 'video'", () => {
+		const file = new File(["dummy content"], "tanjin.mp4", { type: "video/mp4" });
+		renderMediaDisplay(file, "video", "https://react-chatbot.com/tanjin.mp4");
+
+		const videoContainer = screen.getByTestId("media-display-video-container");
+		expect(videoContainer).toBeInTheDocument();
+
+		const sourceElement = videoContainer.querySelector("source");
+		expect(sourceElement).toBeInTheDocument();
+		expect(sourceElement).toHaveAttribute("src", "https://react-chatbot.com/tanjin.mp4");
+		expect(sourceElement).toHaveAttribute("type", "video/mp4");
+	});
+
+	it("renders audio correctly when fileType is 'audio'", () => {
+		const file = new File(["dummy content"], "tanjin.mp3", { type: "audio/mp3" });
+		renderMediaDisplay(file, "audio", "https://react-chatbot.com/tanjin.mp3");
+
+		const audioContainer = screen.getByTestId("media-display-audio-container");
+		expect(audioContainer).toBeInTheDocument();
+
+		const sourceElement = audioContainer.querySelector("source");
+		expect(sourceElement).toBeInTheDocument();
+		expect(sourceElement).toHaveAttribute("src", "https://react-chatbot.com/tanjin.mp3");
+		expect(sourceElement).toHaveAttribute("type", "audio/mp3");
+	});
+
+	it("renders nothing if fileUrl is null", () => {
+		const file = new File(["dummy content"], "tanjin.png", { type: "image/png" });
+		renderMediaDisplay(file, "image", null);
+
+		expect(screen.queryByRole("img")).not.toBeInTheDocument();
+		expect(screen.queryByRole("video")).not.toBeInTheDocument();
+		expect(screen.queryByRole("audio")).not.toBeInTheDocument();
+	});
+});
\ No newline at end of file
diff --git a/__tests__/components/buttons/SendButton.test.tsx b/__tests__/components/buttons/SendButton.test.tsx
new file mode 100644
index 0000000..c33cac3
--- /dev/null
+++ b/__tests__/components/buttons/SendButton.test.tsx
@@ -0,0 +1,232 @@
+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 SendButton from "../../../src/components/Buttons/SendButton/SendButton";
+import { DefaultSettings } from "../../../src/constants/internal/DefaultSettings";
+import { useSettingsContext } from "../../../src/context/SettingsContext";
+import { useBotStatesContext } from "../../../src/context/BotStatesContext";
+import { useSubmitInputInternal } from "../../../src/hooks/internal/useSubmitInputInternal";
+import { useStylesContext } from "../../../src/context/StylesContext";
+import { DefaultStyles } from "../../../src/constants/internal/DefaultStyles";
+
+jest.mock("../../../src/context/SettingsContext");
+jest.mock("../../../src/context/BotStatesContext");
+jest.mock("../../../src/hooks/internal/useSubmitInputInternal");
+jest.mock("../../../src/context/StylesContext");
+
+/**
+ * Test for SendButton component.
+ */
+describe("SendButton Component", () => {
+	beforeEach(() => {
+		(useSettingsContext as jest.Mock).mockReturnValue({
+			settings: {
+				general: {
+					primaryColor: DefaultSettings.general?.primaryColor,
+					secondaryColor: DefaultSettings.general?.secondaryColor,
+				},
+				ariaLabel: { sendButton: DefaultSettings.ariaLabel?.sendButton },
+			}
+		});
+
+		// disable textArea as default
+		(useBotStatesContext as jest.Mock).mockReturnValue({
+			textAreaDisabled: true
+		});
+
+		(useSubmitInputInternal as jest.Mock).mockReturnValue({
+			handleSubmitText: jest.fn()
+		});
+
+		(useStylesContext as jest.Mock).mockReturnValue({
+			styles: {}
+		});
+	});
+
+
+	it("renders with correct aria-label and changes color on hover when textArea is enabled", () => {
+		// enable textArea
+		(useBotStatesContext as jest.Mock).mockReturnValue({
+			textAreaDisabled: false
+		});
+
+		render(<SendButton />);
+
+		const button = screen.getByRole("button", { name: DefaultSettings.ariaLabel?.sendButton });
+		const icon = button.querySelector("span");
+
+		expect(button).toBeInTheDocument();
+		expect(icon).toBeInTheDocument();
+
+		// Check color when button is enabled
+		expect(button).toHaveStyle(`backgroundColor: ${DefaultSettings.general?.primaryColor}`);
+
+		// Simulate hover
+		fireEvent.mouseEnter(button);
+
+		// Check hover color
+		expect(button).toHaveStyle(`backgroundColor: ${DefaultSettings.general?.secondaryColor}`);
+
+		// Simulate mouse leave
+		fireEvent.mouseLeave(button);
+
+		// Check initial color after mouse leave
+		expect(button).toHaveStyle(`backgroundColor: ${DefaultSettings.general?.primaryColor}`);
+	});
+
+	it("renders default aria-label and default style color while textArea is enabled", () => {
+		// remove provided settings
+		(useSettingsContext as jest.Mock).mockReturnValue({
+			settings: {}
+		});
+
+		(useStylesContext as jest.Mock).mockReturnValue({
+			styles: DefaultStyles
+		});
+
+		// enable textArea
+		(useBotStatesContext as jest.Mock).mockReturnValue({
+			textAreaDisabled: false
+		});
+
+		render(<SendButton />);
+
+		const button = screen.getByRole("button", { name: DefaultSettings.ariaLabel?.sendButton });
+		const icon = button.querySelector("span");
+
+		expect(button).toBeInTheDocument();
+		expect(icon).toBeInTheDocument();
+
+		// Check color when button is enabled
+		expect(button).toHaveStyle(`backgroundColor: ${DefaultStyles.sendButtonStyle?.backgroundColor}`);
+
+		// Simulate hover
+		fireEvent.mouseEnter(button);
+
+		// Check hover color
+		expect(button).toHaveStyle(`backgroundColor: ${DefaultStyles.sendButtonHoveredStyle?.backgroundColor}`);
+
+		// Simulate mouse leave
+		fireEvent.mouseLeave(button);
+
+		// Check initial color after mouse leave
+		expect(button).toHaveStyle(`backgroundColor: ${DefaultStyles.sendButtonStyle?.backgroundColor}`);
+	});
+
+	it("renders with disabled default style color and doesn't change color on hover when textArea is disabled", () => {
+		// remove provided settings
+		(useSettingsContext as jest.Mock).mockReturnValue({
+			settings: {}
+		});
+
+		(useStylesContext as jest.Mock).mockReturnValue({
+			styles: DefaultStyles
+		});
+
+		render(<SendButton />);
+
+		const button = screen.getByRole("button", { name: DefaultSettings.ariaLabel?.sendButton });
+		const icon = button.querySelector("span");
+
+		expect(button).toBeInTheDocument();
+		expect(icon).toBeInTheDocument();
+
+		// Check color when button is disabled
+		expect(button).toHaveStyle(`backgroundColor: ${DefaultStyles.sendButtonDisabledStyle?.backgroundColor}`);
+
+		// Check cursor style when button is disabled
+		expect(button).toHaveStyle(`cursor: url(undefined), auto`);
+
+		// Simulate hover (should not change color when textArea is disabled)
+		fireEvent.mouseEnter(button);
+
+		// Check color when button is disabled and hovered
+		expect(button).toHaveStyle(`backgroundColor: ${DefaultStyles.sendButtonDisabledStyle?.backgroundColor}`);
+
+		// Simulate mouse leave
+		fireEvent.mouseLeave(button);
+
+		// Check color after mouse leave
+		expect(button).toHaveStyle(`backgroundColor: ${DefaultStyles.sendButtonDisabledStyle?.backgroundColor}`);
+
+	});
+
+	it("renders with default disabled style and doesn't change color on hover when textArea is disabled", () => {
+		render(<SendButton />);
+
+		const button = screen.getByRole("button", { name: DefaultSettings.ariaLabel?.sendButton });
+		const icon = button.querySelector("span");
+
+		expect(button).toBeInTheDocument();
+		expect(icon).toBeInTheDocument();
+
+		// Check color when button is disabled
+		expect(button).toHaveStyle(`backgroundColor: ${DefaultSettings.general?.primaryColor}`);
+
+		// Check cursor style when button is disabled
+		expect(button).toHaveStyle(`cursor: url(undefined), auto`);
+
+		// Simulate hover (should not change color when textArea is disabled)
+		fireEvent.mouseEnter(button);
+
+		// Check color when button is disabled and hovered
+		expect(button).toHaveStyle(`backgroundColor: ${DefaultSettings.general?.primaryColor}`);
+
+		// Simulate mouse leave
+		fireEvent.mouseLeave(button);
+
+		// Check color after mouse leave
+		expect(button).toHaveStyle(`backgroundColor: ${DefaultSettings.general?.primaryColor}`);
+
+	});
+
+	it('calls handleSubmitText on mouse down when textArea is enabled', async () => {
+		// enable textArea
+		(useBotStatesContext as jest.Mock).mockReturnValue({
+			textAreaDisabled: false
+		});
+
+		const mockHandleSubmitText = jest.fn();
+		(useSubmitInputInternal as jest.Mock).mockReturnValue({ 
+			handleSubmitText: mockHandleSubmitText 
+		});
+
+		render(<SendButton />);
+
+		const button = screen.getByRole('button', { name: DefaultSettings.ariaLabel?.sendButton });
+		const icon = button.querySelector("span");
+
+		expect(button).toBeInTheDocument();
+		expect(icon).toBeInTheDocument();
+
+		// Simulate mouse down
+		fireEvent.mouseDown(button);
+
+		// Check if the function was called once
+		expect(mockHandleSubmitText).toHaveBeenCalledTimes(1);
+	});
+
+	it('does not call handleSubmitText on mouse down when textArea is disabled', async () => {
+		const mockHandleSubmitText = jest.fn();
+		(useSubmitInputInternal as jest.Mock).mockReturnValue({ 
+			handleSubmitText: mockHandleSubmitText 
+		});
+
+		render(<SendButton />);
+
+		const button = screen.getByRole('button', { name: DefaultSettings.ariaLabel?.sendButton });
+		const icon = button.querySelector("span");
+
+		expect(button).toBeInTheDocument();
+		expect(icon).toBeInTheDocument();
+
+		// Simulate mouse down
+		fireEvent.mouseDown(button);
+
+		// Check to make sure the function was not called
+		expect(mockHandleSubmitText).not.toHaveBeenCalled();
+	});
+});
\ No newline at end of file
diff --git a/__tests__/hooks/internal/useFirstInteractionInternal.test.ts b/__tests__/hooks/internal/useFirstInteractionInternal.test.ts
index 4ebefe9..76fd19c 100644
--- a/__tests__/hooks/internal/useFirstInteractionInternal.test.ts
+++ b/__tests__/hooks/internal/useFirstInteractionInternal.test.ts
@@ -22,24 +22,24 @@ jest.mock("../../../src/context/SettingsContext", () => ({
 	})),
 }));
 class MockSpeechSynthesisUtterance {
-		text = "";
-		onend: () => void = () => {};
-	
-		constructor() {
-			setTimeout(() => {
-				this.onend();
-			}, 0);
-		}
+	text = "";
+	onend: () => void = () => {};
+
+	constructor() {
+		setTimeout(() => {
+			this.onend();
+		}, 0);
 	}
+}
 	
-	const MockSpeechSynthesis = {
-		speak: jest.fn().mockImplementation((utterance: MockSpeechSynthesisUtterance) => {
-			utterance.onend();
-		}),
-	};
-	
-	global.SpeechSynthesisUtterance = MockSpeechSynthesisUtterance as any;
-	global.speechSynthesis = MockSpeechSynthesis as any;
+const MockSpeechSynthesis = {
+	speak: jest.fn().mockImplementation((utterance: MockSpeechSynthesisUtterance) => {
+		utterance.onend();
+	}),
+};
+
+global.SpeechSynthesisUtterance = MockSpeechSynthesisUtterance as any;
+global.speechSynthesis = MockSpeechSynthesis as any;
 
 describe("useFirstInteractionInternal", () => {
 	let setHasInteractedPage: jest.Mock;
diff --git a/__tests__/services/AudioService.test.ts b/__tests__/services/AudioService.test.ts
new file mode 100644
index 0000000..4171b5e
--- /dev/null
+++ b/__tests__/services/AudioService.test.ts
@@ -0,0 +1,251 @@
+import React from "react";
+
+import { processAudio } from "../../src/services/AudioService";
+import { generateSecureUUID } from "../../src/utils/idGenerator";
+import { Settings } from "../../src/types/Settings";
+import { Message } from "../../src/types/Message";
+
+// mocks SpeechSynthesisUtterance
+class MockSpeechSynthesisUtterance {
+	text: string = "";
+	lang: string = "";
+	rate: number = 1;
+	volume: number = 1;
+	voice: SpeechSynthesisVoice | null = null;
+}
+  
+// sample voices
+const availableVoices: SpeechSynthesisVoice[] = [
+	{
+		name: "Google US English",
+		lang: "en-US",
+		voiceURI: "Google US English",
+		default: true,
+		localService: true,
+	},
+	{
+		name: "Google UK English",
+		lang: "en-GB",
+		voiceURI: "Google UK English",
+		default: false,
+		localService: true,
+	},
+];
+
+// mocks speechSynthesis methods
+const mockSpeak = jest.fn();
+const mockGetVoices = jest.fn().mockReturnValue(availableVoices);
+
+/**
+ * Test for AudioService.
+ */
+describe("AudioService.processAudio (Inline Mocks)", () => {
+	beforeAll(() => {
+		(global as any).SpeechSynthesisUtterance = MockSpeechSynthesisUtterance;
+		(global as any).speechSynthesis = {
+			speak: mockSpeak,
+			getVoices: mockGetVoices,
+		};
+	});
+
+	beforeEach(() => {
+		jest.clearAllMocks();
+		mockGetVoices.mockReturnValue(availableVoices);
+	});
+  
+	it("does not call speak when audio is disabled", () => {
+		// settings for audio
+		const settings: Settings = {
+			audio: {
+				disabled: true,
+				voiceNames: ["Google US English"]
+			}
+		};
+
+		// calls process audio
+		const message: Message = {
+			id: generateSecureUUID(),
+			sender: "user",
+			content: "Hello, how can I assist you today?",
+			type: "string",
+			timestamp: Date.now().toString(),
+		};
+		const voiceToggledOn = true;
+		const isChatWindowOpen = true;
+		processAudio(settings, voiceToggledOn, isChatWindowOpen, message);
+
+		// checks that speak is not called
+		expect(mockSpeak).not.toHaveBeenCalled();
+	});
+
+	it("does not call speak when message is from the user", () => {
+		// settings for audio
+		const settings: Settings = {
+			audio: {
+				disabled: false,
+				voiceNames: ["Google US English"]
+			}
+		};
+
+		// defines message to be from user
+		const message: Message = {
+			id: generateSecureUUID(),
+			sender: "user",
+			content: "I need help with my account.",
+			type: "string",
+			timestamp: Date.now().toString(),
+		};
+
+		// calls process audio
+		const voiceToggledOn = true;
+		const isChatWindowOpen = true;
+		processAudio(settings, voiceToggledOn, isChatWindowOpen, message);
+
+		// checks that speak is not called
+		expect(mockSpeak).not.toHaveBeenCalled();
+	});
+
+	it("does not call speak when message content is not a string", () => {
+		// settings for audio
+		const settings: Settings = {
+			audio: {
+				disabled: false,
+				voiceNames: ["Google US English"]
+			}
+		};
+
+		// defines message content as an object
+		const message: Message = {
+			id: generateSecureUUID(),
+			sender: "bot",
+			content: React.createElement("div"),
+			type: "object",
+			timestamp: Date.now().toString(),
+		};
+
+		// calls process audio
+		const voiceToggledOn = true;
+		const isChatWindowOpen = true;
+		processAudio(settings, voiceToggledOn, isChatWindowOpen, message);
+
+		// checks that speak is not called
+		expect(mockSpeak).not.toHaveBeenCalled();
+	});
+
+	it("does not call speak when chat window is closed and not embedded", () => {
+		// settings for audio and general
+		const settings: Settings = {
+			audio: {
+				disabled: false,
+				voiceNames: ["Google US English"]
+			}, 
+			general: {
+				embedded: false
+			}
+		};
+
+		// calls process audio
+		const message: Message = {
+			id: generateSecureUUID(),
+			sender: "bot",
+			content: "Welcome back! How can I help you today?",
+			type: "string",
+			timestamp: Date.now().toString(),
+		};
+		const voiceToggledOn = true;
+		const isChatWindowOpen = false;
+		processAudio(settings, voiceToggledOn, isChatWindowOpen, message);
+
+		// checks that speak is not called
+		expect(mockSpeak).not.toHaveBeenCalled();
+	});
+
+	it("does not call speak when voice is toggled off", () => {
+		// settings for audio
+		const settings: Settings = {
+			audio: {
+				disabled: false,
+				voiceNames: ["Google US English"]
+			}
+		};
+
+		// calls process audio, but voice is toggled off
+		const message: Message = {
+			id: generateSecureUUID(),
+			sender: "bot",
+			content: "Let me know if you need any assistance.",
+			type: "string",
+			timestamp: Date.now().toString(),
+		};
+		const voiceToggledOn = false;
+		const isChatWindowOpen = true;
+		processAudio(settings, voiceToggledOn, isChatWindowOpen, message);
+
+		// checks that speak is not called
+		expect(mockSpeak).not.toHaveBeenCalled();
+	});
+
+	it("calls speak with the specified voice when all conditions are met and voice is found", () => {
+		// settings for audio
+		const settings: Settings = {
+			audio: {
+				disabled: false,
+				voiceNames: ["Google US English"]
+			}
+		};
+
+		// calls process audio
+		const message: Message = {
+			id: generateSecureUUID(),
+			sender: "bot",
+			content: "Hello! How can I assist you today?",
+			type: "string",
+			timestamp: Date.now().toString(),
+		};
+		const voiceToggledOn = true;
+		const isChatWindowOpen = true;
+		processAudio(settings, voiceToggledOn, isChatWindowOpen, message);
+
+		// checks that speak is called correctly
+		expect(mockSpeak).toHaveBeenCalledTimes(1);
+		expect(mockSpeak).toHaveBeenCalledWith(expect.any(SpeechSynthesisUtterance));
+		const utterance = mockSpeak.mock.calls[0][0] as SpeechSynthesisUtterance;
+		expect(utterance.text).toBe(message.content);
+		expect(utterance.lang).toBe(settings.audio?.language);
+		expect(utterance.rate).toBe(settings.audio?.rate);
+		expect(utterance.volume).toBe(settings.audio?.volume);
+		expect(utterance.voice?.name).toEqual(availableVoices[0].name);
+	});
+
+	it("calls speak without a specified voice when all conditions are met but voice is not found", () => {
+		// settings for audio
+		const settings: Settings = {
+			audio: {
+				disabled: false,
+				voiceNames: ["Non-Existent Voice"],
+			}
+		};
+
+		// calls process audio
+		const message: Message = {
+			id: generateSecureUUID(),
+			sender: "bot",
+			content: "This is a test message without a specified voice.",
+			type: "string",
+			timestamp: Date.now().toString(),
+		};
+		const voiceToggledOn = true;
+		const isChatWindowOpen = true;
+		processAudio(settings, voiceToggledOn, isChatWindowOpen, message);
+
+		// checks that speak is called correctly
+		expect(mockSpeak).toHaveBeenCalledTimes(1);
+		expect(mockSpeak).toHaveBeenCalledWith(expect.any(SpeechSynthesisUtterance));
+		const utterance = mockSpeak.mock.calls[0][0] as SpeechSynthesisUtterance;
+		expect(utterance.text).toBe(message.content);
+		expect(utterance.lang).toBe(settings.audio?.language);
+		expect(utterance.rate).toBe(settings.audio?.rate);
+		expect(utterance.volume).toBe(settings.audio?.volume);
+		expect(utterance.voice).toBeNull();
+	});
+});
\ No newline at end of file