Repository: tjtanjin/react-chatbotify
Pull request #283 (2024-12-09 11:00:57)

ISSUE-282 keep history messages in sync with storage
#### Description

Changed saveChatHistory by assigning the latest state to ChatHistoryService it's historyMessages.

Closes https://github.com/tjtanjin/react-chatbotify/issues/282

#### 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)
- [X] 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?

Please give a short overview/explanation on the approach taken to resolve the issue(s).

- Fix the bug
-Change / Add tests

#### Checklist:

- [ ] 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)
- [ ] Relevant comments/docs have been added/updated (for bug fixes/features)


Patch:
diff --git a/package-lock.json b/package-lock.json
index 6ae7367..249d5fb 100644
--- a/package-lock.json
+++ b/package-lock.json
@@ -8911,7 +8911,9 @@
       "license": "MIT"
     },
     "node_modules/nanoid": {
-      "version": "3.3.6",
+      "version": "3.3.8",
+      "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.8.tgz",
+      "integrity": "sha512-WNLf5Sd8oZxOm+TzppcYk8gVOgP+l58xNy58D0nbUnOxOWRWvlcCV4kUF7ltmI6PsrLl/BgKEyS4mqsGChFN0w==",
       "dev": true,
       "funding": [
         {
@@ -8919,7 +8921,6 @@
           "url": "https://github.com/sponsors/ai"
         }
       ],
-      "license": "MIT",
       "bin": {
         "nanoid": "bin/nanoid.cjs"
       },
@@ -10829,10 +10830,11 @@
       "license": "MIT"
     },
     "node_modules/vite": {
-      "version": "4.5.5",
-      "resolved": "https://registry.npmjs.org/vite/-/vite-4.5.5.tgz",
-      "integrity": "sha512-ifW3Lb2sMdX+WU91s3R0FyQlAyLxOzCSCP37ujw0+r5POeHPwe6udWVIElKQq8gk3t7b8rkmvqC6IHBpCff4GQ==",
+      "version": "4.5.9",
+      "resolved": "https://registry.npmjs.org/vite/-/vite-4.5.9.tgz",
+      "integrity": "sha512-qK9W4xjgD3gXbC0NmdNFFnVFLMWSNiR3swj957yutwzzN16xF/E7nmtAyp1rT9hviDroQANjE4HK3H4WqWdFtw==",
       "dev": true,
+      "license": "MIT",
       "dependencies": {
         "esbuild": "^0.18.10",
         "postcss": "^8.4.27",
diff --git a/src/components/Buttons/AudioButton/AudioButton.tsx b/src/components/Buttons/AudioButton/AudioButton.tsx
index b2576bc..28f51ce 100644
--- a/src/components/Buttons/AudioButton/AudioButton.tsx
+++ b/src/components/Buttons/AudioButton/AudioButton.tsx
@@ -68,7 +68,7 @@ const AudioButton = () => {
 				await toggleAudio();
 			}}
 			style={audioToggledOn
-				? styles.audioButtonStyle
+				? {...styles.audioButtonStyle}
 				: {...styles.audioButtonStyle, ...styles.audioButtonDisabledStyle}
 			}
 		>
diff --git a/src/components/Buttons/CloseChatButton/CloseChatButton.tsx b/src/components/Buttons/CloseChatButton/CloseChatButton.tsx
index 324131c..3f3cd4e 100644
--- a/src/components/Buttons/CloseChatButton/CloseChatButton.tsx
+++ b/src/components/Buttons/CloseChatButton/CloseChatButton.tsx
@@ -57,7 +57,7 @@ const CloseChatButton = () => {
 				event.stopPropagation();
 				openChat(false);
 			}}
-			style={styles.closeChatButtonStyle}
+			style={{...styles.closeChatButtonStyle}}
 		>
 			{renderButton()}
 		</div>
diff --git a/src/components/Buttons/EmojiButton/EmojiButton.tsx b/src/components/Buttons/EmojiButton/EmojiButton.tsx
index efb58a2..1df61b7 100644
--- a/src/components/Buttons/EmojiButton/EmojiButton.tsx
+++ b/src/components/Buttons/EmojiButton/EmojiButton.tsx
@@ -157,7 +157,7 @@ const EmojiButton = () => {
 				role="button" 
 				ref={iconContainerRef}
 				className={`${textAreaDisabled ? "rcb-emoji-button-disabled" : "rcb-emoji-button-enabled"}`}
-				style={textAreaDisabled ? emojiButtonDisabledStyle : styles.emojiButtonStyle}
+				style={textAreaDisabled ? emojiButtonDisabledStyle : {...styles.emojiButtonStyle}}
 				onMouseDown={togglePopup}
 			>
 				{renderButton()}
diff --git a/src/components/Buttons/FileAttachmentButton/FileAttachmentButton.tsx b/src/components/Buttons/FileAttachmentButton/FileAttachmentButton.tsx
index 2792df8..78b0c3c 100644
--- a/src/components/Buttons/FileAttachmentButton/FileAttachmentButton.tsx
+++ b/src/components/Buttons/FileAttachmentButton/FileAttachmentButton.tsx
@@ -161,7 +161,7 @@ const FileAttachmentButton = () => {
 			role="button" 
 			className={blockAllowsAttachment? "rcb-attach-button-enabled" : "rcb-attach-button-disabled"}
 			style={blockAllowsAttachment
-				? styles.fileAttachmentButtonStyle
+				? {...styles.fileAttachmentButtonStyle}
 				: fileAttachmentButtonDisabledStyle
 			}
 		>
diff --git a/src/components/Buttons/NotificationButton/NotificationButton.tsx b/src/components/Buttons/NotificationButton/NotificationButton.tsx
index 298120f..f53e292 100644
--- a/src/components/Buttons/NotificationButton/NotificationButton.tsx
+++ b/src/components/Buttons/NotificationButton/NotificationButton.tsx
@@ -70,7 +70,7 @@ const NotificationButton = () => {
 				await toggleNotifications();
 			}}
 			style={notificationsToggledOn
-				? styles.notificationButtonStyle
+				? {...styles.notificationButtonStyle}
 				: {...styles.notificationButtonStyle, ...styles.notificationButtonDisabledStyle}
 			}
 		>
diff --git a/src/components/Buttons/VoiceButton/VoiceButton.tsx b/src/components/Buttons/VoiceButton/VoiceButton.tsx
index c2144d1..d982283 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}
 				/>
 			)
@@ -134,7 +135,7 @@ const VoiceButton = () => {
 				await toggleVoice();
 			}}
 			style={voiceToggledOn && !textAreaDisabled
-				? styles.voiceButtonStyle
+				? {...styles.voiceButtonStyle}
 				: {...styles.voiceButtonStyle, ...styles.voiceButtonDisabledStyle}
 			}
 			className={voiceToggledOn && !textAreaDisabled ? "rcb-voice-button-enabled" : "rcb-voice-button-disabled"}
diff --git a/src/components/ChatBotBody/ChatMessagePrompt/ChatMessagePrompt.tsx b/src/components/ChatBotBody/ChatMessagePrompt/ChatMessagePrompt.tsx
index 1fc7a92..8929b6c 100644
--- a/src/components/ChatBotBody/ChatMessagePrompt/ChatMessagePrompt.tsx
+++ b/src/components/ChatBotBody/ChatMessagePrompt/ChatMessagePrompt.tsx
@@ -105,7 +105,7 @@ const ChatMessagePrompt = () => {
 			<div
 				onMouseEnter={handleMouseEnter}
 				onMouseLeave={handleMouseLeave} 
-				style={isHovered ? chatMessagePromptHoveredStyle : styles.chatMessagePromptStyle}
+				style={isHovered ? chatMessagePromptHoveredStyle : {...styles.chatMessagePromptStyle}}
 				onMouseDown={(event: MouseEvent) => {
 					event.preventDefault();
 					scrollToBottom(600);
diff --git a/src/components/ChatBotButton/ChatBotButton.tsx b/src/components/ChatBotButton/ChatBotButton.tsx
index 317d610..8eded9c 100644
--- a/src/components/ChatBotButton/ChatBotButton.tsx
+++ b/src/components/ChatBotButton/ChatBotButton.tsx
@@ -70,7 +70,7 @@ const ChatBotButton = () => {
 				>
 					{renderButton()}
 					{!settings.notification?.disabled && settings.notification?.showCount &&
-						<span style={styles.notificationBadgeStyle} className="rcb-badge">
+						<span style={{...styles.notificationBadgeStyle}} className="rcb-badge">
 							{unreadCount}
 						</span>
 					}
diff --git a/src/components/ChatBotContainer.tsx b/src/components/ChatBotContainer.tsx
index 303cc1a..3029923 100644
--- a/src/components/ChatBotContainer.tsx
+++ b/src/components/ChatBotContainer.tsx
@@ -101,7 +101,7 @@ const ChatBotContainer = ({
 			};
 		}
 
-		return styles.chatWindowStyle;
+		return {...styles.chatWindowStyle};
 	}
 
 	/**
diff --git a/src/components/ChatBotFooter/ChatBotFooter.tsx b/src/components/ChatBotFooter/ChatBotFooter.tsx
index 712dd34..d9f1aa6 100644
--- a/src/components/ChatBotFooter/ChatBotFooter.tsx
+++ b/src/components/ChatBotFooter/ChatBotFooter.tsx
@@ -19,7 +19,11 @@ const ChatBotFooter = ({ buttons }: { buttons: JSX.Element[] }) => {
 	const { styles } = useStylesContext();
 
 	return (
-		<div data-testid="chatbot-footer-container" style={styles.footerStyle} className="rcb-chat-footer-container">
+		<div
+			data-testid="chatbot-footer-container"
+			style={{...styles.footerStyle}}
+			className="rcb-chat-footer-container"
+		>
 			<div className="rcb-chat-footer">
 				{buttons?.map((button: JSX.Element, index: number) =>
 					<Fragment key={index}>{button}</Fragment>
diff --git a/src/components/ChatBotInput/ChatBotInput.tsx b/src/components/ChatBotInput/ChatBotInput.tsx
index 25c3071..74a7087 100644
--- a/src/components/ChatBotInput/ChatBotInput.tsx
+++ b/src/components/ChatBotInput/ChatBotInput.tsx
@@ -175,7 +175,7 @@ const ChatBotInput = ({ buttons }: { buttons: JSX.Element[] }) => {
 					setHasFlowStarted(true);
 				}
 			}}
-			style={styles.chatInputContainerStyle} 
+			style={{...styles.chatInputContainerStyle}}
 			className="rcb-chat-input"
 		>
 			{/* textarea intentionally does not use the disabled property to prevent keyboard from closing on mobile */}
diff --git a/src/components/ChatHistoryButton/ChatHistoryButton.tsx b/src/components/ChatHistoryButton/ChatHistoryButton.tsx
index be55bb6..a5c15c4 100644
--- a/src/components/ChatHistoryButton/ChatHistoryButton.tsx
+++ b/src/components/ChatHistoryButton/ChatHistoryButton.tsx
@@ -49,7 +49,7 @@ const ChatHistoryButton = () => {
 			<div
 				onMouseEnter={handleMouseEnter}
 				onMouseLeave={handleMouseLeave} 
-				style={isHovered ? chatHistoryButtonHoveredStyle : styles.chatHistoryButtonStyle}
+				style={isHovered ? chatHistoryButtonHoveredStyle : {...styles.chatHistoryButtonStyle}}
 				onMouseDown={(event: MouseEvent) => {
 					event.preventDefault();
 					showChatHistory();
diff --git a/src/components/ChatHistoryLineBreak/ChatHistoryLineBreak.tsx b/src/components/ChatHistoryLineBreak/ChatHistoryLineBreak.tsx
index 5d7bfd6..0d6ff6a 100644
--- a/src/components/ChatHistoryLineBreak/ChatHistoryLineBreak.tsx
+++ b/src/components/ChatHistoryLineBreak/ChatHistoryLineBreak.tsx
@@ -17,7 +17,7 @@ const ChatHistoryLineBreak = () => {
 	return (
 		<div className="rcb-line-break-container">
 			<div
-				style={styles.chatHistoryLineBreakStyle}
+				style={{...styles.chatHistoryLineBreakStyle}}
 				className="rcb-line-break-text"
 				data-testid="chat-history-line-break-text"
 			>
diff --git a/src/hooks/internal/useFlowInternal.ts b/src/hooks/internal/useFlowInternal.ts
index bec3236..8822dd6 100644
--- a/src/hooks/internal/useFlowInternal.ts
+++ b/src/hooks/internal/useFlowInternal.ts
@@ -1,10 +1,12 @@
-import { useCallback } from "react";
+import {useCallback} from "react";
 
-import { useMessagesInternal } from "./useMessagesInternal";
-import { usePathsInternal } from "./usePathsInternal";
-import { useToastsInternal } from "./useToastsInternal";
-import { useBotRefsContext } from "../../context/BotRefsContext";
-import { useBotStatesContext } from "../../context/BotStatesContext";
+import {useMessagesInternal} from "./useMessagesInternal";
+import {usePathsInternal} from "./usePathsInternal";
+import {useToastsInternal} from "./useToastsInternal";
+import {useBotRefsContext} from "../../context/BotRefsContext";
+import {useBotStatesContext} from "../../context/BotStatesContext";
+import {setHistoryStorageValues} from "../../services/ChatHistoryService";
+import {useSettingsContext} from "../../context/SettingsContext";
 
 /**
  * Internal custom hook for managing flow.
@@ -24,15 +26,21 @@ export const useFlowInternal = () => {
 	
 	// handles bot refs
 	const { flowRef } = useBotRefsContext();
+
+	// handles bot settings
+	const { settings } = useSettingsContext();
 	
 	/**
 	 * Restarts the conversation flow for the chatbot.
 	 */
 	const restartFlow = useCallback(() => {
+		//reload the chat history from storage
+		setHistoryStorageValues(settings)
+
 		replaceMessages([]);
 		replaceToasts([]);
 		replacePaths(["start"]);
-	}, [replaceMessages, replaceToasts, replacePaths]);
+	}, [replaceMessages, replaceToasts, replacePaths, setHistoryStorageValues]);
 	
 	/**
 	 * Retrieves the conversation flow for the chatbot.
diff --git a/src/hooks/internal/useMessagesInternal.ts b/src/hooks/internal/useMessagesInternal.ts
index 0a8fd27..67d25ed 100644
--- a/src/hooks/internal/useMessagesInternal.ts
+++ b/src/hooks/internal/useMessagesInternal.ts
@@ -323,7 +323,7 @@ export const useMessagesInternal = () => {
 			// defer update to next event loop, handles edge case where messages are sent too fast
 			// and the scrolling does not properly reach the bottom
 			setTimeout(() => {
-				if (!chatBodyRef.current) {
+				if (!chatBodyRef?.current) {
 					return;
 				}
 
diff --git a/src/hooks/internal/useTextAreaInternal.ts b/src/hooks/internal/useTextAreaInternal.ts
index ac555c0..c2d05e4 100644
--- a/src/hooks/internal/useTextAreaInternal.ts
+++ b/src/hooks/internal/useTextAreaInternal.ts
@@ -105,7 +105,16 @@ export const useTextAreaInternal = () => {
 	}, [textAreaDisabled]);
 
 	/**
-	 * Retrieves text area value.
+	 * Blurs on text area.
+	 */
+	const blurTextArea = useCallback(() => {
+		if (inputRef.current) {
+			inputRef.current.blur();
+		}
+	}, []);
+	/**
+	
+	* Retrieves text area value.
 	 */
 	const getTextAreaValue = useCallback(() => {
 		if (inputRef && inputRef.current) {
@@ -140,7 +149,8 @@ export const useTextAreaInternal = () => {
 		setTextAreaValue,
 		updateTextAreaFocus,
 		focusTextArea,
+		blurTextArea,
 		toggleTextAreaDisabled,
 		toggleTextAreaSensitiveMode
 	};
-};
+};
\ No newline at end of file
diff --git a/src/hooks/useTextArea.ts b/src/hooks/useTextArea.ts
index d808e9a..91e5394 100644
--- a/src/hooks/useTextArea.ts
+++ b/src/hooks/useTextArea.ts
@@ -17,7 +17,8 @@ export const useTextArea = () => {
 		toggleTextAreaSensitiveMode,
 		getTextAreaValue,
 		setTextAreaValue,
-		focusTextArea
+		focusTextArea,
+		blurTextArea
 	} = useTextAreaInternal();
 
 	return {
@@ -27,6 +28,7 @@ export const useTextArea = () => {
 		toggleTextAreaSensitiveMode,
 		getTextAreaValue,
 		setTextAreaValue,
-		focusTextArea
+		focusTextArea,
+		blurTextArea
 	};
-};
+};
\ No newline at end of file
diff --git a/src/services/VoiceService.ts b/src/services/VoiceService.ts
index a0fe297..0a1a129 100644
--- a/src/services/VoiceService.ts
+++ b/src/services/VoiceService.ts
@@ -106,7 +106,10 @@ const startSpeechRecognition = (
 			setInputLength(inputRef.current.value.length);
 		}
 
-		inactivityTimer = setTimeout(async () => await handleTimeout(toggleVoice, inputRef), inactivityPeriod);
+		// only have timer for timeout if inactivityPeriod is > 0
+		if (inactivityPeriod) {
+			inactivityTimer = setTimeout(async () => await handleTimeout(toggleVoice, inputRef), inactivityPeriod);
+		}
 		if (!settings.voice?.autoSendDisabled) {
 			autoSendTimer = setTimeout(triggerSendVoiceInput, autoSendPeriod);
 		}
@@ -115,7 +118,8 @@ const startSpeechRecognition = (
 	recognition.onend = () => {
 		if (toggleOn) {
 			recognition.start();
-			if (!inactivityTimer) {
+			// only have timer for timeout if inactivityPeriod is > 0
+			if (!inactivityTimer && inactivityPeriod) {
 				inactivityTimer = setTimeout(async () => await handleTimeout(toggleVoice, inputRef), inactivityPeriod);
 			}
 		} else {
@@ -125,7 +129,10 @@ const startSpeechRecognition = (
 		}
 	};
 
-	inactivityTimer = setTimeout(async () => await handleTimeout(toggleVoice, inputRef), inactivityPeriod);
+	// only have timer for timeout if inactivityPeriod is > 0
+	if (inactivityPeriod) {
+		inactivityTimer = setTimeout(async () => await handleTimeout(toggleVoice, inputRef), inactivityPeriod);
+	}
 }
 
 /**


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/ChatBot.test.tsx b/__tests__/components/ChatBot.test.tsx
new file mode 100644
index 0000000..4be4da2
--- /dev/null
+++ b/__tests__/components/ChatBot.test.tsx
@@ -0,0 +1,90 @@
+import React from "react";
+
+import { render, screen } from "@testing-library/react";
+import { expect } from "@jest/globals";
+import "@testing-library/jest-dom/jest-globals";
+
+import ChatBot from "../../src/components/ChatBot";
+
+import { WelcomeFlow } from "../../src/constants/internal/WelcomeFlow";
+
+import { useChatBotContext } from "../../src/context/ChatBotContext";
+
+import { Settings } from "../../src/types/Settings";
+import { Styles } from "../../src/types/Styles";
+import { Theme } from "../../src/types/Theme";
+
+// Mock dependencies
+jest.mock("../../src/utils/idGenerator", () => ({
+	generateSecureUUID: jest.fn(() => "mocked-uuid"),
+}));
+
+jest.mock("../../src/context/ChatBotContext", () => ({
+	useChatBotContext: jest.fn(),
+	ChatBotProvider: ({ children }: { children: React.ReactNode }) => (
+		<div>{children}</div>
+	),
+}));
+
+jest.mock("../../src/components/ChatBotLoader", () => () => (
+	<div data-testid="chatbot-loader" />
+));
+
+jest.mock("../../src/components/ChatBotContainer", () => () => (
+	<div data-testid="chatbot-container" />
+));
+
+describe("ChatBot Component", () => {
+	it("renders ChatBot with default settings when no props are provided", () => {
+		render(<ChatBot />);
+
+		// Check if ChatBotLoader is rendered with default settings
+		expect(screen.getByTestId("chatbot-loader")).toBeInTheDocument();
+		expect(screen.queryByTestId("chatbot-container")).not.toBeInTheDocument();
+	});
+
+	it("renders ChatBot with provided settings", () => {
+		const customFlow = { ...WelcomeFlow };
+		const customSettings: Settings = {
+			/* provide valid properties for Settings type */
+		};
+		const customStyles: Styles = {
+			/* provide valid properties for Styles type */
+		};
+		const customThemes: Theme[] = [
+			/* provide valid properties for Theme type */
+		];
+		const customPlugins: [] = [];
+
+		render(
+			<ChatBot
+				id="custom-id"
+				flow={customFlow}
+				settings={customSettings}
+				styles={customStyles}
+				themes={customThemes}
+				plugins={customPlugins}
+			/>
+		);
+
+		// Check if ChatBotLoader is rendered with provided settings
+		expect(screen.getByTestId("chatbot-loader")).toBeInTheDocument();
+		expect(screen.queryByTestId("chatbot-container")).not.toBeInTheDocument();
+	});
+	it("renders ChatBot with ChatBotProvider when context is not provided", () => {
+		render(<ChatBot />);
+
+		// Check if ChatBotProvider is rendered
+		expect(screen.getByTestId("chatbot-loader")).toBeInTheDocument();
+	});
+
+	// useChatBotContext is already imported at the top
+	(useChatBotContext as jest.Mock).mockReturnValue({});
+
+	it("renders ChatBot without ChatBotProvider when context is provided", () => {
+		render(<ChatBot />);
+
+		// Check if ChatBotProvider is not rendered
+		expect(screen.getByTestId("chatbot-loader")).toBeInTheDocument();
+	});
+});
diff --git a/__tests__/components/ChatBotBody/ChatMessagePrompt.test.tsx b/__tests__/components/ChatBotBody/ChatMessagePrompt.test.tsx
index a484fcd..79d5617 100644
--- a/__tests__/components/ChatBotBody/ChatMessagePrompt.test.tsx
+++ b/__tests__/components/ChatBotBody/ChatMessagePrompt.test.tsx
@@ -6,131 +6,131 @@ import { useBotStatesContext } from "../../../src/context/BotStatesContext";
 
 // Mock contexts
 jest.mock("../../../src/context/BotRefsContext", () => ({
-  useBotRefsContext: jest.fn(() => ({
-    chatBodyRef: {
-      current: {
-        scrollTop: 0,
-        scrollHeight: 1000,
-        clientHeight: 400,
-      },
-    },
-  })),
+	useBotRefsContext: jest.fn(() => ({
+		chatBodyRef: {
+			current: {
+				scrollTop: 0,
+				scrollHeight: 1000,
+				clientHeight: 400,
+			},
+		},
+	})),
 }));
 
 jest.mock("../../../src/context/BotStatesContext", () => ({
-  useBotStatesContext: jest.fn(),
+	useBotStatesContext: jest.fn(),
 }));
 
 jest.mock("../../../src/context/SettingsContext", () => ({
-  useSettingsContext: jest.fn(() => ({
-    settings: {
-      general: { primaryColor: "#000" },
-      chatWindow: {
-        showMessagePrompt: true,
-        messagePromptText: "Scroll to new messages",
-      },
-    },
-  })),
+	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" },
-    },
-  })),
+	useStylesContext: jest.fn(() => ({
+		styles: {
+			chatMessagePromptStyle: { color: "#fff", borderColor: "#ccc" },
+			chatMessagePromptHoveredStyle: { color: "#000", borderColor: "#000" },
+		},
+	})),
 }));
 
 describe("ChatMessagePrompt Component", () => {
-  const mockSetIsScrolling = jest.fn();
-
-  beforeEach(() => {
-    jest.clearAllMocks();
-    jest.useFakeTimers();
-  });
-
-  afterEach(() => {
-    jest.runOnlyPendingTimers();
-    jest.useRealTimers();
-  });
-
-  const renderComponent = () => render(<ChatMessagePrompt />);
-
-  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", () => {
-    (useBotStatesContext as jest.Mock).mockReturnValue({
-      unreadCount: 2,
-      isScrolling: true,
-      setIsScrolling: mockSetIsScrolling,
-    });
-
-    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", () => {
-    (useBotStatesContext as jest.Mock).mockReturnValue({
-      unreadCount: 0,
-      isScrolling: false,
-      setIsScrolling: mockSetIsScrolling,
-    });
-
-    renderComponent();
-    const messagePromptContainer = screen.queryByText("Scroll to new messages")?.parentElement;
-    expect(messagePromptContainer).toHaveClass("rcb-message-prompt-container hidden");
-  });
-
-  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");
-
-    // 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", () => {
-    (useBotStatesContext as jest.Mock).mockReturnValue({
-      unreadCount: 2,
-      isScrolling: true,
-      setIsScrolling: mockSetIsScrolling,
-    });
-
-    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);
-  });
+	const mockSetIsScrolling = jest.fn();
+
+	beforeEach(() => {
+		jest.clearAllMocks();
+		jest.useFakeTimers();
+	});
+
+	afterEach(() => {
+		jest.runOnlyPendingTimers();
+		jest.useRealTimers();
+	});
+
+	const renderComponent = () => render(<ChatMessagePrompt />);
+
+	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", () => {
+		(useBotStatesContext as jest.Mock).mockReturnValue({
+			unreadCount: 2,
+			isScrolling: true,
+			setIsScrolling: mockSetIsScrolling,
+		});
+
+		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", () => {
+		(useBotStatesContext as jest.Mock).mockReturnValue({
+			unreadCount: 0,
+			isScrolling: false,
+			setIsScrolling: mockSetIsScrolling,
+		});
+
+		renderComponent();
+		const messagePromptContainer = screen.queryByText("Scroll to new messages")?.parentElement;
+		expect(messagePromptContainer).toHaveClass("rcb-message-prompt-container hidden");
+	});
+
+	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");
+
+		// 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", () => {
+		(useBotStatesContext as jest.Mock).mockReturnValue({
+			unreadCount: 2,
+			isScrolling: true,
+			setIsScrolling: mockSetIsScrolling,
+		});
+
+		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__/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})`);
+	});
+});
diff --git a/__tests__/hooks/internal/useFlowInternal.test.ts b/__tests__/hooks/internal/useFlowInternal.test.ts
index e040f39..ad7e3be 100644
--- a/__tests__/hooks/internal/useFlowInternal.test.ts
+++ b/__tests__/hooks/internal/useFlowInternal.test.ts
@@ -7,106 +7,118 @@ import { usePathsContext } from "../../../src/context/PathsContext";
 import { useToastsContext } from "../../../src/context/ToastsContext";
 import { useBotStatesContext } from "../../../src/context/BotStatesContext";
 import { useBotRefsContext } from "../../../src/context/BotRefsContext";
+import { useSettingsContext } from "../../../src/context/SettingsContext";
+import { setHistoryStorageValues } from "../../../src/services/ChatHistoryService";
 
 jest.mock("../../../src/context/MessagesContext");
 jest.mock("../../../src/context/PathsContext");
 jest.mock("../../../src/context/ToastsContext");
 jest.mock("../../../src/context/BotStatesContext");
 jest.mock("../../../src/context/BotRefsContext");
+jest.mock("../../../src/context/SettingsContext");
+jest.mock("../../../src/services/ChatHistoryService");
 
 describe("useFlowInternal Hook", () => {
 	const setMessagesMock = jest.fn();
-  const setPathsMock = jest.fn();
+	const setPathsMock = jest.fn();
 	const setToastsMock = jest.fn();
-  const flowRefMock = { current: { id: "test-flow" } };
-  const hasFlowStartedMock = true;
-
-  beforeEach(() => {
-    jest.clearAllMocks();
-
-
-    (useMessagesContext as jest.Mock).mockReturnValue({ setMessages: setMessagesMock });
-    (usePathsContext as jest.Mock).mockReturnValue({ setPaths: setPathsMock });
-    (useToastsContext as jest.Mock).mockReturnValue({ setToasts: setToastsMock });
-    (useBotRefsContext as jest.Mock).mockReturnValue({ flowRef: flowRefMock });
-    (useBotStatesContext as jest.Mock).mockReturnValue({ hasFlowStarted: hasFlowStartedMock });
-  });
-
-
-// Test to ensure initial values (hasFlowStarted and flowRef) are returned correctly from the hook
-  it("should return initial values from context", () => {
-    const { result } = renderHook(() => useFlowInternal());
-
-    expect(result.current.hasFlowStarted).toBe(true);
-    expect(result.current.getFlow()).toEqual({ id: "test-flow" });
-  });
-
- // Test to ensure that restartFlow clears messages, toasts, and resets paths
-  it("should restart the flow by clearing messages, toasts, and resetting paths", () => {
-    const { result } = renderHook(() => useFlowInternal());
-
-    act(() => {
-      result.current.restartFlow();
-    });
-
-    expect(setMessagesMock).toHaveBeenCalledWith([]);
-    expect(setToastsMock).toHaveBeenCalledWith([]);
-    expect(setPathsMock).toHaveBeenCalledWith(["start"]);
-  });
-
-// Test to ensure that getFlow returns the current flow from flowRef
-  it("should get the current flow from flowRef", () => {
-    const { result } = renderHook(() => useFlowInternal());
-
-    const flow = result.current.getFlow();
-    expect(flow).toEqual({ id: "test-flow" });
-  });
-
-// Test to ensure that calling restartFlow multiple times works correctly
-  it("should handle multiple restarts correctly", () => {
-    const { result } = renderHook(() => useFlowInternal());
-
-    act(() => {
-      result.current.restartFlow();
-    });
-
-    act(() => {
-      result.current.restartFlow();
-    });
-
-    expect(setMessagesMock).toHaveBeenCalledTimes(2);
-    expect(setToastsMock).toHaveBeenCalledTimes(2);
-    expect(setPathsMock).toHaveBeenCalledTimes(2);
-  });
-
-// Test to check flow state when hasFlowStarted is false
-  it("should correctly reflect flow state when it hasn't started", () => {
-    (useBotStatesContext as jest.Mock).mockReturnValue({ hasFlowStarted: false });
-    const { result } = renderHook(() => useFlowInternal());
-
-    expect(result.current.hasFlowStarted).toBe(false);
-  });
-
-// Test to ensure messages, toasts, and paths are initialized correctly when restarting the flow
-  it("should initialize messages, toasts, and paths correctly", () => {
-    const { result } = renderHook(() => useFlowInternal());
-
-    act(() => {
-      result.current.restartFlow();
-    });
-
-    expect(setMessagesMock).toHaveBeenCalledWith([]);
-    expect(setToastsMock).toHaveBeenCalledWith([]);
-    expect(setPathsMock).toHaveBeenCalledWith(["start"]);
-  });
-
-// Test to check that getFlow returns different flowRef values correctly
-  it("should handle different flowRef values", () => {
-    const differentFlowRefMock = { current: { id: "different-flow" } };
-    (useBotRefsContext as jest.Mock).mockReturnValue({ flowRef: differentFlowRefMock });
-    const { result } = renderHook(() => useFlowInternal());
-
-    const flow = result.current.getFlow();
-    expect(flow).toEqual({ id: "different-flow" });
-  });
+	const flowRefMock = { current: { id: "test-flow" } };
+	const hasFlowStartedMock = true;
+	const mockSettings = {
+		chatHistory: {
+			storageType: "localStorage",
+			storageKey: "rcb-history",
+		},
+	};
+
+	beforeEach(() => {
+		jest.clearAllMocks();
+
+		(useMessagesContext as jest.Mock).mockReturnValue({ setMessages: setMessagesMock });
+		(usePathsContext as jest.Mock).mockReturnValue({ setPaths: setPathsMock });
+		(useToastsContext as jest.Mock).mockReturnValue({ setToasts: setToastsMock });
+		(useBotRefsContext as jest.Mock).mockReturnValue({ flowRef: flowRefMock });
+		(useBotStatesContext as jest.Mock).mockReturnValue({ hasFlowStarted: hasFlowStartedMock });
+		(useSettingsContext as jest.Mock).mockReturnValue({ settings: mockSettings });
+	});
+
+
+	// Test to ensure initial values (hasFlowStarted and flowRef) are returned correctly from the hook
+	it("should return initial values from context", () => {
+		const { result } = renderHook(() => useFlowInternal());
+
+		expect(result.current.hasFlowStarted).toBe(true);
+		expect(result.current.getFlow()).toEqual({ id: "test-flow" });
+	});
+
+	// Test to ensure that restartFlow clears messages, toasts, and resets paths
+	it("should restart the flow by clearing messages, toasts, resetting paths and loading history", () => {
+		const { result } = renderHook(() => useFlowInternal());
+
+		act(() => {
+			result.current.restartFlow();
+		});
+
+		expect(setMessagesMock).toHaveBeenCalledWith([]);
+		expect(setToastsMock).toHaveBeenCalledWith([]);
+		expect(setPathsMock).toHaveBeenCalledWith(["start"]);
+		expect(setHistoryStorageValues)
+			.toHaveBeenCalledWith({ chatHistory: { storageType: "localStorage", storageKey: "rcb-history" } });
+	});
+
+	// Test to ensure that getFlow returns the current flow from flowRef
+	it("should get the current flow from flowRef", () => {
+		const { result } = renderHook(() => useFlowInternal());
+
+		const flow = result.current.getFlow();
+		expect(flow).toEqual({ id: "test-flow" });
+	});
+
+	// Test to ensure that calling restartFlow multiple times works correctly
+	it("should handle multiple restarts correctly", () => {
+		const { result } = renderHook(() => useFlowInternal());
+
+		act(() => {
+			result.current.restartFlow();
+		});
+
+		act(() => {
+			result.current.restartFlow();
+		});
+
+		expect(setMessagesMock).toHaveBeenCalledTimes(2);
+		expect(setToastsMock).toHaveBeenCalledTimes(2);
+		expect(setPathsMock).toHaveBeenCalledTimes(2);
+	});
+
+	// Test to check flow state when hasFlowStarted is false
+	it("should correctly reflect flow state when it hasn't started", () => {
+		(useBotStatesContext as jest.Mock).mockReturnValue({ hasFlowStarted: false });
+		const { result } = renderHook(() => useFlowInternal());
+
+		expect(result.current.hasFlowStarted).toBe(false);
+	});
+
+	// Test to ensure messages, toasts, and paths are initialized correctly when restarting the flow
+	it("should initialize messages, toasts, and paths correctly", () => {
+		const { result } = renderHook(() => useFlowInternal());
+
+		act(() => {
+			result.current.restartFlow();
+		});
+
+		expect(setMessagesMock).toHaveBeenCalledWith([]);
+		expect(setToastsMock).toHaveBeenCalledWith([]);
+		expect(setPathsMock).toHaveBeenCalledWith(["start"]);
+	});
+
+	// Test to check that getFlow returns different flowRef values correctly
+	it("should handle different flowRef values", () => {
+		const differentFlowRefMock = { current: { id: "different-flow" } };
+		(useBotRefsContext as jest.Mock).mockReturnValue({ flowRef: differentFlowRefMock });
+		const { result } = renderHook(() => useFlowInternal());
+
+		const flow = result.current.getFlow();
+		expect(flow).toEqual({ id: "different-flow" });
+	});
 });
\ No newline at end of file
diff --git a/__tests__/hooks/internal/useTextAreaInternal.test.ts b/__tests__/hooks/internal/useTextAreaInternal.test.ts
index 26a135a..3dcd854 100644
--- a/__tests__/hooks/internal/useTextAreaInternal.test.ts
+++ b/__tests__/hooks/internal/useTextAreaInternal.test.ts
@@ -23,7 +23,7 @@ describe("useTextAreaInternal Hook", () => {
   let mocksettingsPreventDefault: any;
   beforeEach(() => {
     
-    mockInputRef = { current: { value: "", focus: jest.fn() } }; 
+    mockInputRef = { current: { value: "", focus: jest.fn(), blur: jest.fn() } }; 
     mockPrevInputRef = { current: "" }; 
     mocksettingsRefT = { chatInput: { allowNewline: true } };
     mocksettingsRefF = { chatInput: { allowNewline: false } };
@@ -158,6 +158,21 @@ describe("useTextAreaInternal Hook", () => {
     expect(result.current.textAreaDisabled).toBe(false);
   });
 
+  it("should blur on text area when blurTextArea is called", () => {
+    const callRcbEventMock = jest.fn();
+    mockUseRcbEventInternal.mockReturnValue({
+      callRcbEvent: callRcbEventMock,
+    });
+
+    const { result } = renderHook(() => useTextAreaInternal());
+
+    act(() => {
+      result.current.blurTextArea();
+    });
+
+    expect(mockInputRef.current.blur).toHaveBeenCalled();
+  });
+
  // let initialTextAreaDisabled = false;
 
  /* it("should toggle textAreaDisabled state", () => {
@@ -222,4 +237,4 @@ describe("useTextAreaInternal Hook", () => {
     // Assume focus happens correctly, verify it updated focus (mocks internal behavior)
     expect(callRcbEventMock).not.toHaveBeenCalledWith(RcbEvent.TEXT_AREA_CHANGE_VALUE);
   });
-});
\ No newline at end of file
+});
diff --git a/__tests__/services/ChatHistoryService.test.ts b/__tests__/services/ChatHistoryService.test.ts
index 9b6f494..3eaae99 100644
--- a/__tests__/services/ChatHistoryService.test.ts
+++ b/__tests__/services/ChatHistoryService.test.ts
@@ -66,7 +66,7 @@ describe("ChatHistoryService", () => {
 		it("should save history if not disabled", async () => {
 			setHistoryStorageValues(mockSettings(storageType));
 			await saveChatHistory([mockMessage]);
-			
+
 			expect(storage.setItem).toHaveBeenCalled();
 		});