Repository: tjtanjin/react-chatbotify (ts)

Issue:
[Task] Add unit test cases for ChatBotContainer component
**Task Description:**
Currently, we run unit tests using jest which can be found [**here**](https://github.com/tjtanjin/react-chatbotify/tree/main/__tests__). 

However, the test cases are not yet comprehensive enough and work can be done to cover more grounds. Specifically for this issue, we hope to add test cases for the `ChatBotContainer` component.

**Deliverable(s):**
New unit test cases that cover the testing of the `ChatBotContainer` component.

**Additional Context:**
- The `ChatBotContainer` component can be found [**here**](https://github.com/tjtanjin/react-chatbotify/tree/main/src/components/ChatBotContainer.tsx)
- An example for `AudioButton` can be found [**here**](https://github.com/tjtanjin/react-chatbotify/blob/main/__tests__/components/buttons/AudioButton.test.tsx) which may serve as a good source for reference

Pull request description:
Test: unit test cases for ChatBotContainer component
#### Description

This pull request introduces unit tests for the ChatBotContainer component, ensuring that the component correctly renders the header, input, and footer buttons based on the settings provided by the useSettingsContext.

Closes #222

#### What change does this PR introduce?

Please select the relevant option(s).

- [ ] Bug fix (non-breaking change which fixes an issue)
- [x] 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)

#### What is the proposed approach?

The approach taken involves mocking the useSettingsContext hook to control the settings for the ChatBotContainer component. Three separate tests have been implemented to verify that the correct buttons are rendered based on the configuration of showHeader, showInputRow, and showFooter settings. Each test checks the presence or absence of the relevant buttons in the rendered output.

#### 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/package-lock.json b/package-lock.json
index f5f874f..5e63d2d 100644
--- a/package-lock.json
+++ b/package-lock.json
@@ -15,7 +15,6 @@
         "@types/jest": "^29.5.13",
         "@types/react": "^18.3.6",
         "@types/react-dom": "^18.3.0",
-        "@types/testing-library__react-hooks": "^3.4.1",
         "@typescript-eslint/eslint-plugin": "^5.60.1",
         "@vitejs/plugin-react": "^4.0.4",
         "cypress": "^13.14.2",
@@ -2325,7 +2324,6 @@
       "resolved": "https://registry.npmjs.org/@types/jest/-/jest-29.5.13.tgz",
       "integrity": "sha512-wd+MVEZCHt23V0/L642O5APvspWply/rGY5BcW4SUETo2UzPU3Z26qr8jC2qxpimI2jjx9h7+2cj2FwIr01bXg==",
       "dev": true,
-      "license": "MIT",
       "dependencies": {
         "expect": "^29.0.0",
         "pretty-format": "^29.0.0"
@@ -2384,16 +2382,6 @@
         "@types/react": "*"
       }
     },
-    "node_modules/@types/react-test-renderer": {
-      "version": "18.3.0",
-      "resolved": "https://registry.npmjs.org/@types/react-test-renderer/-/react-test-renderer-18.3.0.tgz",
-      "integrity": "sha512-HW4MuEYxfDbOHQsVlY/XtOvNHftCVEPhJF2pQXXwcUiUF+Oyb0usgp48HSgpK5rt8m9KZb22yqOeZm+rrVG8gw==",
-      "dev": true,
-      "license": "MIT",
-      "dependencies": {
-        "@types/react": "*"
-      }
-    },
     "node_modules/@types/semver": {
       "version": "7.5.0",
       "dev": true,
@@ -2420,16 +2408,6 @@
       "dev": true,
       "license": "MIT"
     },
-    "node_modules/@types/testing-library__react-hooks": {
-      "version": "3.4.1",
-      "resolved": "https://registry.npmjs.org/@types/testing-library__react-hooks/-/testing-library__react-hooks-3.4.1.tgz",
-      "integrity": "sha512-G4JdzEcq61fUyV6wVW9ebHWEiLK2iQvaBuCHHn9eMSbZzVh4Z4wHnUGIvQOYCCYeu5DnUtFyNYuAAgbSaO/43Q==",
-      "dev": true,
-      "license": "MIT",
-      "dependencies": {
-        "@types/react-test-renderer": "*"
-      }
-    },
     "node_modules/@types/tough-cookie": {
       "version": "4.0.5",
       "resolved": "https://registry.npmjs.org/@types/tough-cookie/-/tough-cookie-4.0.5.tgz",
@@ -2498,8 +2476,6 @@
     },
     "node_modules/@typescript-eslint/parser": {
       "version": "5.62.0",
-      "resolved": "https://registry.npmjs.org/@typescript-eslint/parser/-/parser-5.62.0.tgz",
-      "integrity": "sha512-VlJEV0fOQ7BExOsHYAGrgbEiZoi8D+Bl2+f6V2RrXerRSylnp+ZBHmPvaIa8cz0Ajx7WO7Z5RqfgYg7ED1nRhA==",
       "dev": true,
       "license": "BSD-2-Clause",
       "peer": true,
diff --git a/src/components/ChatBot.tsx b/src/components/ChatBot.tsx
index 1da5af1..b141364 100644
--- a/src/components/ChatBot.tsx
+++ b/src/components/ChatBot.tsx
@@ -69,7 +69,7 @@ const ChatBot = ({
 			<ChatBotLoader styleRootRef={styleRootRef} id={finalBotId} flow={finalFlow} settings={finalSettings}
 				styles={finalStyles} themes={finalThemes} plugins={finalPlugins} setConfigLoaded={setConfigLoaded}
 			/>
-			{configLoaded && <ChatBotContainer plugins={plugins} setFinalThemes={setFinalThemes} />}
+			{configLoaded && <ChatBotContainer plugins={plugins} />}
 		</>
 	)
 
diff --git a/src/components/ChatBotBody/ToastPrompt/ToastPrompt.tsx b/src/components/ChatBotBody/ToastPrompt/ToastPrompt.tsx
index 08d7c1b..1e1e24a 100644
--- a/src/components/ChatBotBody/ToastPrompt/ToastPrompt.tsx
+++ b/src/components/ChatBotBody/ToastPrompt/ToastPrompt.tsx
@@ -29,6 +29,12 @@ const Toast = ({
 	// handles styles
 	const { styles } = useStylesContext();
 
+	// localized styles to prevent conflicting toast style updates
+	const [localizedStyles] = useState({
+		toastPromptStyle: { ...styles.toastPromptStyle },
+		toastPromptHoveredStyle: { ...styles.toastPromptHoveredStyle }
+	});
+
 	// handles toasts
 	const { dismissToast } = useToastsInternal();
 
@@ -39,7 +45,7 @@ const Toast = ({
 	const toastPromptHoveredStyle: React.CSSProperties = {
 		color: settings.general?.primaryColor,
 		borderColor: settings.general?.primaryColor,
-		...styles.toastPromptHoveredStyle
+		...localizedStyles.toastPromptHoveredStyle
 	};
 
 	useEffect(() => {
@@ -71,7 +77,7 @@ const Toast = ({
 			<div
 				onMouseEnter={handleMouseEnter}
 				onMouseLeave={handleMouseLeave} 
-				style={isHovered ? toastPromptHoveredStyle : styles.toastPromptStyle}
+				style={isHovered ? toastPromptHoveredStyle : localizedStyles.toastPromptStyle}
 				onMouseDown={(event: MouseEvent) => {
 					if (settings.toast?.dismissOnClick) {
 						event.preventDefault();
diff --git a/src/components/ChatBotContainer.tsx b/src/components/ChatBotContainer.tsx
index 745805d..40cfdf8 100644
--- a/src/components/ChatBotContainer.tsx
+++ b/src/components/ChatBotContainer.tsx
@@ -1,4 +1,4 @@
-import { Dispatch, MouseEvent, SetStateAction, useMemo } from "react";
+import { MouseEvent, useMemo } from "react";
 
 import ChatBotHeader from "./ChatBotHeader/ChatBotHeader";
 import ChatBotBody from "./ChatBotBody/ChatBotBody";
@@ -16,7 +16,6 @@ import { useBotStatesContext } from "../context/BotStatesContext";
 import { useSettingsContext } from "../context/SettingsContext";
 import { useStylesContext } from "../context/StylesContext";
 import { Plugin } from "../types/Plugin";
-import { Theme } from "../types/Theme";
 
 import "./ChatBotContainer.css";
 
@@ -27,10 +26,8 @@ import "./ChatBotContainer.css";
  */
 const ChatBotContainer = ({
 	plugins,
-	setFinalThemes
 }: {
 	plugins?: Array<Plugin>;
-	setFinalThemes: Dispatch<SetStateAction<Theme | Array<Theme>>>;
 }) => {
 	// handles platform
 	const isDesktop = useIsDesktopInternal();
@@ -62,7 +59,7 @@ const ChatBotContainer = ({
 	useBotEffectsInternal();
 
 	// loads plugins
-	usePluginsInternal(plugins, setFinalThemes);
+	usePluginsInternal(plugins);
 
 	/**
 	 * Retrieves class name for window state.
diff --git a/src/hooks/internal/useBotEffectsInternal.tsx b/src/hooks/internal/useBotEffectsInternal.tsx
index 1181be2..04bb14e 100644
--- a/src/hooks/internal/useBotEffectsInternal.tsx
+++ b/src/hooks/internal/useBotEffectsInternal.tsx
@@ -5,7 +5,6 @@ import { preProcessBlock } from "../../services/BlockService/BlockService";
 import {
 	clearHistoryMessages,
 	getHistoryMessages,
-	saveChatHistory,
 	setHistoryStorageValues
 } from "../../services/ChatHistoryService";
 import { createMessage } from "../../utils/messageBuilder";
@@ -42,7 +41,6 @@ export const useBotEffectsInternal = () => {
 		injectMessage,
 		removeMessage,
 		streamMessage,
-		messages,
 		replaceMessages,
 	} = useMessagesInternal();
 
@@ -258,7 +256,6 @@ export const useBotEffectsInternal = () => {
 			// auto cleanup streaming and save messages on path change (not ideal)
 			// todo: remove this in v3, users should call `params.endStreamMessage()`
 			streamMessageMap.current.clear();
-			saveChatHistory(messages);
 		}
 		callNewBlock(currPath, block, params);
 	}, [paths]);
diff --git a/src/hooks/internal/usePluginsInternal.ts b/src/hooks/internal/usePluginsInternal.ts
index 83ef2c6..de29ccf 100644
--- a/src/hooks/internal/usePluginsInternal.ts
+++ b/src/hooks/internal/usePluginsInternal.ts
@@ -1,15 +1,14 @@
-import { Dispatch, SetStateAction, useEffect } from "react";
+import { useEffect } from "react";
 
+import { getCombinedConfig } from "../../utils/configParser";
 import { useSettingsInternal } from "./useSettingsInternal";
 import { useStylesInternal } from "./useStylesInternal";
 import { Plugin } from "../../types/Plugin";
-import { Theme } from "../../types/Theme";
 
 /**
  * Internal custom hook to handle plugins.
  */
-export const usePluginsInternal = (plugins: Array<Plugin> | undefined,
-	setFinalThemes: Dispatch<SetStateAction<Theme | Array<Theme>>>) => {
+export const usePluginsInternal = (plugins: Array<Plugin> | undefined) => {
 
 	const { updateSettings } = useSettingsInternal();
 	const { updateStyles } = useStylesInternal();
@@ -18,33 +17,19 @@ export const usePluginsInternal = (plugins: Array<Plugin> | undefined,
 	const setUpInfo = plugins?.map((pluginHook) => pluginHook());
 
 	useEffect(() => {
+		let pluginSettings = {};
+		let pluginStyles = {};
 		// applies plugin themes, settings and styles if specified
 		setUpInfo?.forEach((setUpInfo) => {
-			if (setUpInfo.themes) {
-				if (Array.isArray(setUpInfo.themes)) {
-					setFinalThemes(prev => {
-						if (Array.isArray(prev)) {
-							return [...prev, ...setUpInfo.themes as Array<Theme>];
-						} else {
-							return [prev, ...setUpInfo.themes as Array<Theme>];
-						}
-					});
-				} else {
-					setFinalThemes(prev => {
-						if (Array.isArray(prev)) {
-							return [...prev, setUpInfo.themes as Theme];
-						} else {
-							return [prev, setUpInfo.themes as Theme];
-						}
-					});
-				}
+			if (setUpInfo?.settings && Object.keys(setUpInfo?.settings).length !== 0) {
+				pluginSettings = getCombinedConfig(setUpInfo.settings, pluginSettings);
 			}
-			if (setUpInfo?.settings) {
-				updateSettings(setUpInfo.settings);
-			}
-			if (setUpInfo?.styles) {
-				updateStyles(setUpInfo.styles);
+			if (setUpInfo?.styles && Object.keys(setUpInfo?.styles).length !== 0) {
+				pluginStyles = getCombinedConfig(setUpInfo.styles, pluginStyles);
 			}
 		});
+
+		updateSettings(pluginSettings);
+		updateStyles(pluginStyles);
 	}, [plugins])
 };
diff --git a/src/hooks/internal/useSettingsInternal.ts b/src/hooks/internal/useSettingsInternal.ts
index d5a7458..25874da 100644
--- a/src/hooks/internal/useSettingsInternal.ts
+++ b/src/hooks/internal/useSettingsInternal.ts
@@ -15,6 +15,9 @@ export const useSettingsInternal = () => {
 	 * @param fields fields to update
 	 */
 	const updateSettings = (fields: object) => {
+		if (!fields || Object.keys(fields).length === 0) {
+			return;
+		}
 		setSettings(deepClone(getCombinedConfig(fields, settings) as Settings));
 	}
 
diff --git a/src/hooks/internal/useStylesInternal.ts b/src/hooks/internal/useStylesInternal.ts
index 78d5a0c..9f32037 100644
--- a/src/hooks/internal/useStylesInternal.ts
+++ b/src/hooks/internal/useStylesInternal.ts
@@ -15,6 +15,9 @@ export const useStylesInternal = () => {
 	 * @param fields fields to update
 	 */
 	const updateStyles = (fields: object) => {
+		if (!fields || Object.keys(fields).length === 0) {
+			return;
+		}
 		setStyles(deepClone(getCombinedConfig(fields, styles) as Styles));
 	}
 
diff --git a/src/services/ChatHistoryService.tsx b/src/services/ChatHistoryService.tsx
index 904162e..a65207e 100644
--- a/src/services/ChatHistoryService.tsx
+++ b/src/services/ChatHistoryService.tsx
@@ -9,7 +9,7 @@ import { Settings } from "../types/Settings";
 import { Styles } from "../types/Styles";
 
 // variables used to track history, updated when settings.chatHistory value changes
-let storage = localStorage;
+let storage: Storage;
 let historyLoaded = false;
 let historyStorageKey = "rcb-history";
 let historyMaxEntries = 30;
@@ -22,7 +22,7 @@ let historyMessages: Message[] = [];
  * @param messages messages containing current conversation with the bot
  */
 const saveChatHistory = async (messages: Message[]) => {
-	if (historyDisabled) {
+	if (historyDisabled || !storage) {
 		return;
 	}
 	
@@ -83,6 +83,9 @@ const getHistoryMessages = () => {
  * @param messages chat history messages to set
  */
 const setHistoryMessages = (messages: Message[]) => {
+	if (!storage) {
+		return;
+	}
 	storage.setItem(historyStorageKey, JSON.stringify(messages));
 }
 
@@ -90,6 +93,9 @@ const setHistoryMessages = (messages: Message[]) => {
  * Clears existing history messages.
  */
 const clearHistoryMessages = () => {
+	if (!storage) {
+		return;
+	}
 	storage.removeItem(historyStorageKey);
 }
 
@@ -101,6 +107,8 @@ const clearHistoryMessages = () => {
 const setHistoryStorageValues = (settings: Settings) => {
 	if (settings.chatHistory?.storageType?.toUpperCase() === "SESSION_STORAGE") {
 		storage = sessionStorage;
+	} else {
+		storage = localStorage;
 	}
 	historyStorageKey = settings.chatHistory?.storageKey as string;
 	historyMaxEntries = settings.chatHistory?.maxEntries as number;
@@ -261,9 +269,10 @@ const renderHTML = (html: string, settings: Settings, styles: Styles): ReactNode
 const addStyleToContainers = (classList: DOMTokenList, attributes: {[key: string]: string | CSSProperties}) => {
 	if (classList.contains("rcb-options-container") || classList.contains("rcb-checkbox-container")) {
 		if (Object.prototype.hasOwnProperty.call(attributes, "class")) {
-			attributes["class"] = `${classList.toString()} rcb-options-offset`;
+			attributes["className"] = `${classList.toString()} rcb-options-offset`;
+			delete attributes["class"];
 		} else {
-			attributes["class"] = "rcb-options-offset"
+			attributes["className"] = "rcb-options-offset"
 		}
 	}
 	return attributes;
@@ -286,6 +295,8 @@ const addStyleToOptions = (classList: DOMTokenList, attributes: {[key: string]:
 			cursor: `url(${settings.general?.actionDisabledIcon}), auto`,
 			...styles.botOptionStyle
 		}
+		attributes["className"] = classList.toString();
+		delete attributes["class"];
 	}
 	return attributes;
 }
@@ -307,6 +318,8 @@ const addStyleToCheckboxRows = (classList: DOMTokenList, attributes: {[key: stri
 			cursor: `url(${settings.general?.actionDisabledIcon}), auto`,
 			...styles.botCheckboxRowStyle
 		}
+		attributes["className"] = classList.toString();
+		delete attributes["class"];
 	}
 	return attributes;
 }
@@ -328,6 +341,8 @@ const addStyleToCheckboxNextButton = (classList: DOMTokenList, attributes: {[key
 			cursor: `url(${settings.general?.actionDisabledIcon}), auto`,
 			...styles.botCheckboxNextStyle
 		}
+		attributes["className"] = classList.toString();
+		delete attributes["class"];
 	}
 	return attributes;
 }
@@ -349,6 +364,8 @@ const addStyleToMediaDisplayContainer = (classList: DOMTokenList, attributes: {[
 			maxWidth: settings.userBubble?.showAvatar ? "65%" : "70%",
 			...styles.mediaDisplayContainerStyle
 		}
+		attributes["className"] = classList.toString();
+		delete attributes["class"];
 	}
 	return attributes;
 }


Test patch:
diff --git a/__tests__/components/ChatBotContainer/ChatBotContainer.test.tsx b/__tests__/components/ChatBotContainer/ChatBotContainer.test.tsx
new file mode 100644
index 0000000..3e40e27
--- /dev/null
+++ b/__tests__/components/ChatBotContainer/ChatBotContainer.test.tsx
@@ -0,0 +1,213 @@
+import React from "react";
+import { render, screen } from "@testing-library/react";
+import "@testing-library/jest-dom";
+import ChatBotContainer from "../../../src/components/ChatBotContainer";
+import { useButtonInternal } from "../../../src/hooks/internal/useButtonsInternal";
+import { useChatWindowInternal } from "../../../src/hooks/internal/useChatWindowInternal";
+import { useIsDesktopInternal } from "../../../src/hooks/internal/useIsDesktopInternal";
+import { useBotStatesContext } from "../../../src/context/BotStatesContext";
+import { useSettingsContext } from "../../../src/context/SettingsContext";
+import { useStylesContext } from "../../../src/context/StylesContext";
+import { useBotRefsContext } from "../../../src/context/BotRefsContext";
+import { useNotificationInternal } from "../../../src/hooks/internal/useNotificationsInternal"; 
+
+jest.mock("../../../src/hooks/internal/useButtonsInternal");
+jest.mock("../../../src/hooks/internal/useChatWindowInternal");
+jest.mock("../../../src/hooks/internal/useIsDesktopInternal");
+jest.mock("../../../src/context/BotStatesContext");
+jest.mock("../../../src/context/SettingsContext");
+jest.mock("../../../src/context/StylesContext");
+jest.mock("../../../src/context/BotRefsContext");
+jest.mock("../../../src/hooks/internal/useNotificationsInternal"); 
+
+describe("ChatBotContainer Component", () => {
+	beforeEach(() => {
+		(useButtonInternal as jest.Mock).mockReturnValue({
+			headerButtons: [<button key="header-btn">Header Button</button>],
+			chatInputButtons: [<button key="input-btn">Input Button</button>],
+			footerButtons: [<button key="footer-btn">Footer Button</button>],
+		});
+
+		(useChatWindowInternal as jest.Mock).mockReturnValue({
+			isChatWindowOpen: true,
+			chatScrollHeight: 100,
+			viewportHeight: 600,
+			viewportWidth: 300,
+			setChatScrollHeight: jest.fn(),
+		});
+
+		(useIsDesktopInternal as jest.Mock).mockReturnValue(true);
+
+		(useBotStatesContext as jest.Mock).mockReturnValue({
+			hasFlowStarted: false,
+			setHasFlowStarted: jest.fn(),
+			setUnreadCount: jest.fn(),
+			setTextAreaDisabled: jest.fn(), 
+			setIsChatWindowOpen: jest.fn(),
+			setAudioToggledOn: jest.fn(),
+		});
+
+		(useStylesContext as jest.Mock).mockReturnValue({
+			styles: {
+				chatWindowStyle: { background: 'white', color: 'black' },
+			},
+		});
+
+		(useBotRefsContext as jest.Mock).mockReturnValue({
+			flowRef: { current: {} },
+			chatBodyRef: { current: document.createElement('div') },
+			streamMessageMap: new Map(),
+			paramsInputRef: { current: null },
+			keepVoiceOnRef: { current: false },
+		});
+
+		(useNotificationInternal as jest.Mock).mockReturnValue({
+			audioContextRef: { current: {} },
+			setUpNotifications: jest.fn(),
+		});
+	});
+
+	it("renders ChatBotContainer with header, body, input, and footer", () => {
+		(useSettingsContext as jest.Mock).mockReturnValue({
+			settings: {
+				general: {
+					showHeader: true,
+					showFooter: true,
+					showInputRow: true,
+					flowStartTrigger: "ON_CHATBOT_INTERACT",
+					desktopEnabled: true,
+					mobileEnabled: true,
+				},
+			},
+		});
+		render(<ChatBotContainer />);
+
+		const headerButton = screen.getByText("Header Button");
+		const inputButton = screen.getByText("Input Button");
+		const footerButton = screen.getByText("Footer Button");
+
+		expect(headerButton).toBeInTheDocument();
+		expect(inputButton).toBeInTheDocument();
+		expect(footerButton).toBeInTheDocument();
+	});
+
+	it("renders header when settings.general.showHeader is true", () => {
+		(useSettingsContext as jest.Mock).mockReturnValue({
+			settings: {
+				general: {
+					showHeader: true,
+					showFooter: false,
+					showInputRow: false,
+					flowStartTrigger: "ON_CHATBOT_INTERACT",
+					desktopEnabled: true,
+					mobileEnabled: true,
+				},
+			},
+		});
+		render(<ChatBotContainer />);
+
+		expect(screen.queryByText("Header Button")).toBeInTheDocument();
+		expect(screen.queryByText("Input Button")).not.toBeInTheDocument();
+		expect(screen.queryByText("Footer Button")).not.toBeInTheDocument();
+	});
+
+	it("renders input button when settings.general.showInputRow is true", () => {
+		(useSettingsContext as jest.Mock).mockReturnValue({
+			settings: {
+				general: {
+					showHeader: false,
+					showFooter: false,
+					showInputRow: true,
+					flowStartTrigger: "ON_CHATBOT_INTERACT",
+					desktopEnabled: true,
+					mobileEnabled: true,
+				},
+			},
+		});
+		
+		render(<ChatBotContainer />);
+	
+		expect(screen.queryByText("Input Button")).toBeInTheDocument();
+		expect(screen.queryByText("Header Button")).not.toBeInTheDocument();
+		expect(screen.queryByText("Footer Button")).not.toBeInTheDocument();
+	});
+
+	it("renders footer button when settings.general.showFooter is true", () => {
+		(useSettingsContext as jest.Mock).mockReturnValue({
+			settings: {
+				general: {
+					showHeader: false,
+					showFooter: true,
+					showInputRow: false,
+					flowStartTrigger: "ON_CHATBOT_INTERACT",
+					desktopEnabled: true,
+					mobileEnabled: true,
+				},
+			},
+		});
+		
+		render(<ChatBotContainer />);
+	
+		expect(screen.queryByText("Footer Button")).toBeInTheDocument();
+		expect(screen.queryByText("Header Button")).not.toBeInTheDocument();
+		expect(screen.queryByText("Input Button")).not.toBeInTheDocument();
+	});	
+
+	it("sets flow started on mouse down if it hasn't started", () => {
+		const setHasFlowStarted = jest.fn(); // Mock function for setHasFlowStarted
+		const setUnreadCount = jest.fn(); // Mock function for setUnreadCount
+		const setTextAreaDisabled = jest.fn(); // Mock function for setTextAreaDisabled
+		const setIsChatWindowOpen = jest.fn(); // Mock function for setIsChatWindowOpen
+		const setAudioToggledOn = jest.fn(); // Mock function for setAudioToggledOn
+
+		(useSettingsContext as jest.Mock).mockReturnValue({
+			settings: {
+				general: {
+					showHeader: true,
+					showFooter: true,
+					showInputRow: true,
+					flowStartTrigger: "ON_CHATBOT_INTERACT",
+					desktopEnabled: true,
+					mobileEnabled: true,
+				},
+			},
+		});
+
+		(useBotStatesContext as jest.Mock).mockReturnValue({
+			hasFlowStarted: false,
+			setHasFlowStarted,
+			setUnreadCount,
+			setTextAreaDisabled,
+			setIsChatWindowOpen,
+			setAudioToggledOn,
+			// Add other necessary mock return values here
+		});
+
+		render(<ChatBotContainer />);
+        
+		// Use 'textbox' role instead of 'dialog'
+		const chatInput = screen.getByRole('textbox', { name: 'input text area' });
+		chatInput.dispatchEvent(new MouseEvent('mousedown', { bubbles: true }));
+
+		// Check that setHasFlowStarted was called with true
+		expect(setHasFlowStarted).toHaveBeenCalledWith(true);
+	});
+
+	it("does not render chatbot when shouldShowChatBot is false", () => {
+		(useIsDesktopInternal as jest.Mock).mockReturnValue(false);
+		(useSettingsContext as jest.Mock).mockReturnValue({
+			settings: {
+				general: {
+					showHeader: false,
+					showFooter: false,
+					showInputRow: false,
+					embedded: false,
+					mobileEnabled: false, // Setting this to false
+				},
+			},
+		});
+		render(<ChatBotContainer />);
+		
+		expect(screen.queryByText("Header Button")).not.toBeInTheDocument();
+	});
+});
diff --git a/__tests__/hooks/internal/useSettingsInternal.test.ts b/__tests__/hooks/internal/useSettingsInternal.test.ts
index 84c247e..ca3ff68 100644
--- a/__tests__/hooks/internal/useSettingsInternal.test.ts
+++ b/__tests__/hooks/internal/useSettingsInternal.test.ts
@@ -74,8 +74,8 @@ describe("useSettingsInternal", () => {
             result.current.updateSettings({});
         });
 
-        expect(getCombinedConfig).toHaveBeenCalledWith({}, mockSettings);
-        expect(deepClone).toHaveBeenCalledWith(mockSettings);
-        expect(mockSetSettings).toHaveBeenCalledWith(mockSettings);
+        expect(getCombinedConfig).not.toHaveBeenCalledWith({}, mockSettings);
+        expect(deepClone).not.toHaveBeenCalledWith(mockSettings);
+        expect(mockSetSettings).not.toHaveBeenCalledWith(mockSettings);
     });
 });
diff --git a/__tests__/utils/buttonBuilder.test.ts b/__tests__/utils/buttonBuilder.test.ts
new file mode 100644
index 0000000..d42cc8c
--- /dev/null
+++ b/__tests__/utils/buttonBuilder.test.ts
@@ -0,0 +1,95 @@
+import { getButtonConfig, createAudioButton, createNotificationButton, createCloseChatButton, createVoiceButton,
+	createSendButton, createFileAttachmentButton, createEmojiButton} from "../../src/utils/buttonBuilder";
+import { Button} from "../../src";
+
+jest.mock("../../src/viteconfig", () => ({
+	viteConfig: {
+		DEFAULT_URL: "http://localhost:mock",
+		DEFAULT_EXPIRATION: "60",
+		CACHE_KEY_PREFIX: "VITE_THEME_CACHE_KEY_PREFIX",
+	},
+}));
+
+const mockLocalStorage = (() => {
+	let store: { [key: string]: string } = {};
+	return {
+		getItem: (key: string) => store[key] || null,
+		setItem: (key: string, value: string) => {
+			store[key] = value.toString();
+		},
+		clear: () => {
+			store = {};
+		},
+	};
+})();
+
+if (typeof window !== 'undefined') {
+    Object.defineProperty(window, 'localStorage', {
+        value: mockLocalStorage,
+    });
+}
+
+describe('getButtonConfig', () => {
+	const buttonComponentMap = {
+		[Button.AUDIO_BUTTON]: createAudioButton,
+		[Button.NOTIFICATION_BUTTON]: createNotificationButton,
+		[Button.CLOSE_CHAT_BUTTON]: createCloseChatButton,
+		[Button.VOICE_MESSAGE_BUTTON]: createVoiceButton,
+		[Button.SEND_MESSAGE_BUTTON]: createSendButton,
+		[Button.FILE_ATTACHMENT_BUTTON]: createFileAttachmentButton,
+		[Button.EMOJI_PICKER_BUTTON]: createEmojiButton
+	};
+
+	const baseSettings = {
+		audio: { disabled: false },
+		general: { embedded: false },
+		emoji: { disabled: false },
+		fileAttachment: { disabled: false },
+		notification: { disabled: false },
+		voice: { disabled: false },
+		header: { buttons: [Button.AUDIO_BUTTON, Button.EMOJI_PICKER_BUTTON] },
+		chatInput: { buttons: [Button.SEND_MESSAGE_BUTTON, Button.CLOSE_CHAT_BUTTON] },
+		footer: { buttons: [Button.VOICE_MESSAGE_BUTTON]}
+	};
+
+	it('should return buttons based on settings', () => {
+		const buttonConfig = getButtonConfig(baseSettings, buttonComponentMap);
+
+		expect(buttonConfig.header).toHaveLength(2); // Expect two header buttons
+		expect(buttonConfig.header[0]).toBeTruthy(); // Check if the first header button is rendered
+		expect(buttonConfig.chatInput).toHaveLength(2); // Expect two chat input buttons
+		expect(buttonConfig.footer).toHaveLength(1); // Expect one footer button
+	});
+
+	it('should handle disabled buttons correctly', () => {
+		const alteredSettings = {
+			...baseSettings,
+			audio: { disabled: true }, // Disabling audio button
+			closeChat: { disabled: true },
+			general: {
+				embedded: true
+			}// Disabling close chat button
+		};
+
+		const buttonConfig = getButtonConfig(alteredSettings, buttonComponentMap);
+
+		expect(buttonConfig.header).toHaveLength(1); // Only emoji button should be present
+		expect(buttonConfig.chatInput).toHaveLength(1); // Only the send button should be present
+		expect(buttonConfig.footer).toHaveLength(1); // Voice button should still be there
+	});
+
+	it('should handle missing button configurations', () => {
+		const emptySettings = {
+			...baseSettings,
+			header: { buttons: [] }, // No header buttons defined
+			chatInput: { buttons: [] }, // No chat input buttons defined
+			footer: { buttons: [] } // No footer buttons defined
+		};
+
+		const buttonConfig = getButtonConfig(emptySettings, buttonComponentMap);
+
+		expect(buttonConfig.header).toHaveLength(0); // Should be empty
+		expect(buttonConfig.chatInput).toHaveLength(0); // Should be empty
+		expect(buttonConfig.footer).toHaveLength(0); // Should be empty
+	});
+});