Repository: tjtanjin/react-chatbotify
Pull request #201 (2024-10-12 10:36:22)

[Task] Add unit test cases for OptionProcessor #185
#### Description

Please include a brief summary of the change and include the relevant issue(s).

Closes #(issue)

#### 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)

#### What is the proposed approach?

Please give a short overview/explanation on the approach taken to resolve the issue(s).

#### Checklist:

- [ ] The commit message follows our adopted [guidelines](https://www.conventionalcommits.org/en/v1.0.0/)
- [ ] 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/src/components/Buttons/VoiceButton/VoiceButton.css b/src/components/Buttons/VoiceButton/VoiceButton.css
index 0df02ac..3208bac 100644
--- a/src/components/Buttons/VoiceButton/VoiceButton.css
+++ b/src/components/Buttons/VoiceButton/VoiceButton.css
@@ -49,20 +49,9 @@
 }
 
 .rcb-voice-icon-on {
-    animation: ping 1s infinite;
+    animation: rcb-animation-ping 1s infinite;
 }
 
 .rcb-voice-icon-off {
     filter: grayscale(100%);
-}
-
-@keyframes ping {
-    0% {
-        filter: brightness(100%);
-        opacity: 1;
-    }
-    50% {
-        filter: brightness(50%);
-        opacity: 0.8;
-    }
 }
\ No newline at end of file
diff --git a/src/components/ChatBotBody/ChatBotBody.css b/src/components/ChatBotBody/ChatBotBody.css
index 93afed8..40f8fcf 100644
--- a/src/components/ChatBotBody/ChatBotBody.css
+++ b/src/components/ChatBotBody/ChatBotBody.css
@@ -66,18 +66,7 @@
 }
 
 .rcb-bot-message-entry {
-	animation: bot-entry 0.3s ease-in backwards;
-}
-
-@keyframes bot-entry {
-	0% {
-		transform: translate(-100%, 50%) scale(0);
-		opacity: 0;
-	}
-	100% {
-		transform: translate(0, 0) scale(1);
-		opacity: 1;
-	}
+	animation: rcb-animation-bot-message-entry 0.3s ease-in backwards;
 }
 
 .rcb-user-message {
@@ -90,18 +79,7 @@
 }
 
 .rcb-user-message-entry {
-	animation: user-entry 0.3s ease-in backwards;
-}
-
-@keyframes user-entry {
-	0% {
-		transform: translate(100%, 50%) scale(0);
-		opacity: 0;
-	}
-	100% {
-		transform: translate(0, 0) scale(1);
-		opacity: 1;
-	}
+	animation: rcb-animation-user-message-entry 0.3s ease-in backwards;
 }
 
 .rcb-message-bot-avatar,
@@ -136,7 +114,7 @@
 	border-radius: 50%;
 	background-color: #ccc;
 	margin-right: 4px;
-	animation: rcb-typing 1s infinite;
+	animation: rcb-animation-bot-typing 1s infinite;
 }
 
 .rcb-dot:nth-child(2) {
@@ -147,18 +125,6 @@
   	animation-delay: 0.4s;
 }
 
-@keyframes rcb-typing {
-	0% {
-		opacity: 0.4;
-	}
-	50% {
-		opacity: 1
-	}
-	100% {
-		opacity: 0.4;
-	}
-}
-
 /* Toast Container */
 .rcb-toast-prompt-container {
     position: absolute;
diff --git a/src/components/ChatBotBody/ChatMessagePrompt/ChatMessagePrompt.css b/src/components/ChatBotBody/ChatMessagePrompt/ChatMessagePrompt.css
index 8e7bcd0..cacaef2 100644
--- a/src/components/ChatBotBody/ChatMessagePrompt/ChatMessagePrompt.css
+++ b/src/components/ChatBotBody/ChatMessagePrompt/ChatMessagePrompt.css
@@ -1,48 +1,34 @@
 .rcb-message-prompt-container.visible {
-    position: sticky;
-    bottom: 0px;
-    margin: auto;
-    display: flex;
-    align-items: center;
-    justify-content: center;
-    opacity: 1;
-    animation: popIn 0.3s ease-in-out;
-    pointer-events: auto;
+	position: sticky;
+	bottom: 0px;
+	margin: auto;
+	display: flex;
+	align-items: center;
+	justify-content: center;
+	opacity: 1;
+	animation: rcb-animation-pop-in 0.3s ease-in-out;
+	pointer-events: auto;
 }
 
 .rcb-message-prompt-container.hidden {
-    opacity: 0;
-    height: 0px; /* work around for hiding element while still having animation */
-    visibility: hidden;
-    pointer-events: none;
+	opacity: 0;
+	height: 0px; /* work around for hiding element while still having animation */
+	visibility: hidden;
+	pointer-events: none;
 }
 
 .rcb-message-prompt-text {
-    padding: 6px 12px;
-    border-radius: 20px;
-    color: #adadad;
-    font-size: 12px;
-    background-color: #fff;
-    border: 0.5px solid #adadad;
-    cursor: pointer;
-    transition: color 0.3s ease, border-color 0.3s ease;
-    z-index: 9999;
+	padding: 6px 12px;
+	border-radius: 20px;
+	color: #adadad;
+	font-size: 12px;
+	background-color: #fff;
+	border: 0.5px solid #adadad;
+	cursor: pointer;
+	transition: color 0.3s ease, border-color 0.3s ease;
+	z-index: 9999;
 }
 
 .rcb-message-prompt-container.hidden .rcb-message-prompt-text {
-    padding: 0px;
-}
-
-@keyframes popIn {
-    0% {
-      transform: scale(0.8);
-      opacity: 0;
-    }
-    70% {
-      transform: scale(1.1);
-      opacity: 1;
-    }
-    100% {
-      transform: scale(1);
-    }
-}
+	padding: 0px;
+}
\ No newline at end of file
diff --git a/src/components/ChatBotBody/MediaDisplay/MediaDisplay.css b/src/components/ChatBotBody/MediaDisplay/MediaDisplay.css
index a1629ac..31045cb 100644
--- a/src/components/ChatBotBody/MediaDisplay/MediaDisplay.css
+++ b/src/components/ChatBotBody/MediaDisplay/MediaDisplay.css
@@ -43,16 +43,5 @@
 /* Media Display Entry Animations */
 
 .rcb-media-entry {
-	animation: media-entry 0.3s ease-in backwards;
-}
-
-@keyframes media-entry {
-	0% {
-		transform: translate(100%, 50%) scale(0);
-		opacity: 0;
-	}
-	100% {
-		transform: translate(0, 0) scale(1);
-		opacity: 1;
-	}
+	animation: rcb-animation-user-message-entry 0.3s ease-in backwards;
 }
diff --git a/src/components/ChatBotBody/ToastPrompt/ToastPrompt.css b/src/components/ChatBotBody/ToastPrompt/ToastPrompt.css
index 219e650..c351f77 100644
--- a/src/components/ChatBotBody/ToastPrompt/ToastPrompt.css
+++ b/src/components/ChatBotBody/ToastPrompt/ToastPrompt.css
@@ -1,28 +1,15 @@
-.rcb-toast-prompt-text {
-    padding: 6px 12px;
-    border-radius: 5px;
-    color: #7a7a7a;
-    font-size: 12px;
-    text-align: center;
-    background-color: #fff;
-    border: 0.5px solid #7a7a7a;
-    cursor: pointer;
-    transition: color 0.3s ease, border-color 0.3s ease;
-    z-index: 9999;
-    width: 100%;
-    margin-top: 6px;
-}
-
-@keyframes popIn {
-    0% {
-      transform: scale(0.8);
-      opacity: 0;
-    }
-    70% {
-      transform: scale(1.1);
-      opacity: 1;
-    }
-    100% {
-      transform: scale(1);
-    }
-}
+.rcb-toast-prompt {
+	padding: 6px 12px;
+	border-radius: 5px;
+	color: #7a7a7a;
+	font-size: 12px;
+	text-align: center;
+	background-color: #fff;
+	border: 0.5px solid #7a7a7a;
+	cursor: pointer;
+	transition: color 0.3s ease, border-color 0.3s ease;
+	z-index: 9999;
+	width: 100%;
+	margin-top: 6px;
+	animation: rcb-animation-pop-in 0.3s ease-in-out;
+}
\ No newline at end of file
diff --git a/src/components/ChatBotBody/ToastPrompt/ToastPrompt.tsx b/src/components/ChatBotBody/ToastPrompt/ToastPrompt.tsx
index c8ca5dd..21d1b12 100644
--- a/src/components/ChatBotBody/ToastPrompt/ToastPrompt.tsx
+++ b/src/components/ChatBotBody/ToastPrompt/ToastPrompt.tsx
@@ -78,7 +78,7 @@ const Toast = ({
 						dismissToast(id);
 					}
 				}}
-				className="rcb-toast-prompt-text"
+				className="rcb-toast-prompt"
 			>
 				{content}
 			</div>
diff --git a/src/components/ChatBotBody/UserCheckboxes/UserCheckboxes.css b/src/components/ChatBotBody/UserCheckboxes/UserCheckboxes.css
index af6b42c..8e6554e 100644
--- a/src/components/ChatBotBody/UserCheckboxes/UserCheckboxes.css
+++ b/src/components/ChatBotBody/UserCheckboxes/UserCheckboxes.css
@@ -26,7 +26,7 @@
 	width: 80%;
 	cursor: pointer;
 	background-color: #fff;
-	animation: checkboxes-entry 0.5s ease-out;
+	animation: rcb-animations-checkboxes-entry 0.5s ease-out;
 	overflow: hidden;
 }
 
@@ -34,17 +34,6 @@
 	box-shadow: 0 0 5px rgba(0, 0, 0, 0.2); 
 }
 
-@keyframes checkboxes-entry {
-	0% {
-	  transform: translateX(-100%);
-	  opacity: 0;
-	}
-	100% {
-	  transform: translateX(0);
-	  opacity: 1;
-	}
-}
-
 /* Checkbox Row */
 
 .rcb-checkbox-row {
@@ -100,7 +89,7 @@
 	width: 80%;
 	cursor: pointer;
 	background-color: #fff;
-	animation: checkboxes-entry 0.5s ease-out;
+	animation: rcb-animations-checkboxes-entry 0.5s ease-out;
 }
 
 .rcb-checkbox-next-button::before {
diff --git a/src/components/ChatBotBody/UserOptions/UserOptions.css b/src/components/ChatBotBody/UserOptions/UserOptions.css
index 64060af..4984dd5 100644
--- a/src/components/ChatBotBody/UserOptions/UserOptions.css
+++ b/src/components/ChatBotBody/UserOptions/UserOptions.css
@@ -26,21 +26,10 @@
 	border-style: solid;
 	cursor: pointer;
 	transition: background-color 0.3s ease;
-	animation: options-entry 0.5s ease-out;
+	animation: rcb-animation-options-entry 0.5s ease-out;
 	overflow: hidden;
 }
 
-@keyframes options-entry {
-	0% {
-	  transform: scale(0);
-	  opacity: 0;
-	}
-	100% {
-	  transform: scale(1);
-	  opacity: 1;
-	}
-}
-
 .rcb-options:hover {
 	box-shadow: 0 0 5px rgba(0, 0, 0, 0.2); 
 }
\ No newline at end of file
diff --git a/src/components/ChatBotButton/ChatBotButton.css b/src/components/ChatBotButton/ChatBotButton.css
index e73ca28..6a29421 100644
--- a/src/components/ChatBotButton/ChatBotButton.css
+++ b/src/components/ChatBotButton/ChatBotButton.css
@@ -18,13 +18,13 @@
 .rcb-toggle-button.rcb-button-hide {
 	opacity: 0;
 	visibility: hidden;
-	animation: collapse 0.3s ease-in-out forwards;
+	animation: rcb-animation-collapse 0.3s ease-in-out forwards;
 }
 
 .rcb-toggle-button.rcb-button-show {
 	opacity: 1;
 	visibility: visible;
-	animation: expand 0.3s ease-in-out forwards;
+	animation: rcb-animation-expand 0.3s ease-in-out forwards;
 }
 
 /* ChatBot Toggle Icon */
@@ -42,28 +42,6 @@
 	border-radius: inherit;
 }
 
-@keyframes expand {
-	0% {
-		transform: translate(100%, 100%) scale(0);
-		opacity: 0;
-	}
-	100% {
-		transform: translate(0, 0) scale(1);
-		opacity: 1;
-	}
-}
-
-@keyframes collapse {
-	0% {
-		transform: translate(0, 0) scale(1);
-		opacity: 1;
-	}
-	100% {
-		transform: translate(100%, 100%) scale(0);
-		opacity: 0;
-	}
-}
-
 /* ChatBot Notification Badge*/
 
 .rcb-badge {
diff --git a/src/components/ChatBotContainer.css b/src/components/ChatBotContainer.css
index ad4e66f..05df8b9 100644
--- a/src/components/ChatBotContainer.css
+++ b/src/components/ChatBotContainer.css
@@ -38,16 +38,18 @@
 .rcb-window-open .rcb-chat-window {
 	opacity: 1;
 	visibility: visible;
-	animation: expand 0.3s ease-in-out forwards;
+	animation: rcb-animation-expand 0.3s ease-in-out forwards;
 }
 
 .rcb-window-close .rcb-chat-window {
 	opacity: 0;
 	visibility: hidden;
-	animation: collapse 0.3s ease-in-out forwards;
+	animation: rcb-animation-collapse 0.3s ease-in-out forwards;
 }
 
-@keyframes expand {
+/* Animations used throughout the entire chatbot */
+
+@keyframes rcb-animation-expand {
 	0% {
 		transform: translate(100%, 100%) scale(0);
 		opacity: 0;
@@ -58,7 +60,7 @@
 	}
 }
 
-@keyframes collapse {
+@keyframes rcb-animation-collapse {
 	0% {
 		transform: translate(0, 0) scale(1);
 		opacity: 1;
@@ -68,3 +70,115 @@
 		opacity: 0;
 	}
 }
+
+@keyframes rcb-animation-ping {
+    0% {
+        filter: brightness(100%);
+        opacity: 1;
+    }
+    50% {
+        filter: brightness(50%);
+        opacity: 0.8;
+    }
+}
+
+@keyframes rcb-animation-bot-message-entry {
+	0% {
+		transform: translate(-100%, 50%) scale(0);
+		opacity: 0;
+	}
+	100% {
+		transform: translate(0, 0) scale(1);
+		opacity: 1;
+	}
+}
+
+@keyframes rcb-animation-user-message-entry {
+	0% {
+		transform: translate(100%, 50%) scale(0);
+		opacity: 0;
+	}
+	100% {
+		transform: translate(0, 0) scale(1);
+		opacity: 1;
+	}
+}
+
+@keyframes rcb-animation-bot-typing {
+	0% {
+		opacity: 0.4;
+	}
+	50% {
+		opacity: 1
+	}
+	100% {
+		opacity: 0.4;
+	}
+}
+
+@keyframes rcb-animation-pop-in {
+	0% {
+		transform: scale(0.8);
+		opacity: 0;
+	}
+	70% {
+		transform: scale(1.1);
+		opacity: 1;
+	}
+	100% {
+		transform: scale(1);
+	}
+}
+
+@keyframes rcb-animations-checkboxes-entry {
+	0% {
+	  transform: translateX(-100%);
+	  opacity: 0;
+	}
+	100% {
+	  transform: translateX(0);
+	  opacity: 1;
+	}
+}
+
+@keyframes rcb-animation-options-entry {
+	0% {
+	  transform: scale(0);
+	  opacity: 0;
+	}
+	100% {
+	  transform: scale(1);
+	  opacity: 1;
+	}
+}
+
+@keyframes rcb-animation-tooltip-in {
+	0% {
+	  opacity: 0;
+	  transform: translateY(-5px);
+	}
+	100% {
+	  opacity: 1;
+	  transform: translateY(0);
+	}
+  }
+  
+@keyframes rcb-animation-tooltip-out {
+	0% {
+	  opacity: 1;
+	  transform: translateY(0);
+	}
+	100% {
+	  opacity: 0;
+	  transform: translateY(-5px);
+	}
+}
+
+@keyframes rcb-animation-spin {
+	0% {
+		transform: rotate(0deg);
+	}
+	100% {
+		transform: rotate(360deg);
+	}
+}
\ No newline at end of file
diff --git a/src/components/ChatBotTooltip/ChatBotTooltip.css b/src/components/ChatBotTooltip/ChatBotTooltip.css
index 3cecd7a..f2d4733 100644
--- a/src/components/ChatBotTooltip/ChatBotTooltip.css
+++ b/src/components/ChatBotTooltip/ChatBotTooltip.css
@@ -24,33 +24,11 @@
 .rcb-chat-tooltip.rcb-tooltip-hide {
 	opacity: 0;
 	visibility: hidden;
-	animation: tooltip-out 0.5s ease-in-out;
+	animation: rcb-animation-tooltip-out 0.5s ease-in-out;
 }
 
 .rcb-chat-tooltip.rcb-tooltip-show {
 	opacity: 1;
 	visibility: visible;
-	animation: tooltip-in 0.5s ease-in-out;
-}
-
-@keyframes tooltip-in {
-	0% {
-	  opacity: 0;
-	  transform: translateY(-5px);
-	}
-	100% {
-	  opacity: 1;
-	  transform: translateY(0);
-	}
-  }
-  
-@keyframes tooltip-out {
-	0% {
-	  opacity: 1;
-	  transform: translateY(0);
-	}
-	100% {
-	  opacity: 0;
-	  transform: translateY(-5px);
-	}
+	animation: rcb-animation-tooltip-in 0.5s ease-in-out;
 }
\ No newline at end of file
diff --git a/src/components/LoadingSpinner/LoadingSpinner.css b/src/components/LoadingSpinner/LoadingSpinner.css
index 4938a3f..7b23a87 100644
--- a/src/components/LoadingSpinner/LoadingSpinner.css
+++ b/src/components/LoadingSpinner/LoadingSpinner.css
@@ -17,14 +17,5 @@
 	height: 22px;
 	border-radius: 50%;
 	border: 4px solid #f3f3f3;
-	animation: rcb-spin 1s linear infinite;
-}
-  
-@keyframes rcb-spin {
-	0% {
-		transform: rotate(0deg);
-	}
-	100% {
-		transform: rotate(360deg);
-	}
+	animation: rcb-animation-spin 1s linear infinite;
 }
\ No newline at end of file
diff --git a/src/hooks/internal/useChatHistoryInternal.ts b/src/hooks/internal/useChatHistoryInternal.ts
index 7101eff..da3022b 100644
--- a/src/hooks/internal/useChatHistoryInternal.ts
+++ b/src/hooks/internal/useChatHistoryInternal.ts
@@ -25,8 +25,6 @@ export const useChatHistoryInternal = () => {
 	const {
 		isLoadingChatHistory,
 		setIsLoadingChatHistory,
-		textAreaDisabled,
-		setTextAreaDisabled
 	} = useBotStatesContext();
 
 	// handles rcb events
@@ -51,10 +49,8 @@ export const useChatHistoryInternal = () => {
 			}
 		}
 		setIsLoadingChatHistory(true);
-		const prevTextAreaDisabled = textAreaDisabled;
-		setTextAreaDisabled(true);
-		loadChatHistory(settings, styles, chatHistory, setMessages, prevTextAreaDisabled, setTextAreaDisabled);
-	}, [settings, styles, setMessages, setTextAreaDisabled]);
+		loadChatHistory(settings, styles, chatHistory, setMessages);
+	}, [settings, styles, setMessages]);
 
 	return { isLoadingChatHistory, setIsLoadingChatHistory, showChatHistory };
 };
diff --git a/src/services/ChatHistoryService.tsx b/src/services/ChatHistoryService.tsx
index bba2112..6c39834 100644
--- a/src/services/ChatHistoryService.tsx
+++ b/src/services/ChatHistoryService.tsx
@@ -128,8 +128,7 @@ const parseMessageToString = (message: Message) => {
  * @param setTextAreaDisabled setter for enabling/disabling user text area
  */
 const loadChatHistory = (settings: Settings, styles: Styles, chatHistory: Message[],
-	setMessages: Dispatch<SetStateAction<Message[]>>, prevTextAreaDisabled: boolean,
-	setTextAreaDisabled: Dispatch<SetStateAction<boolean>>) => {
+	setMessages: Dispatch<SetStateAction<Message[]>>) => {
 
 	historyLoaded = true;
 	if (chatHistory != null) {
@@ -160,7 +159,6 @@ const loadChatHistory = (settings: Settings, styles: Styles, chatHistory: Messag
 					}
 					return [...parsedMessages, lineBreakMessage, ...prevMessages];
 				});
-				setTextAreaDisabled(prevTextAreaDisabled ?? settings.chatInput?.disabled ?? false);
 			}, 500)
 		} catch {
 			// remove chat history on error (to address corrupted storage values)


Test patch:
diff --git a/__tests__/hooks/internal/useChatHistoryInternal.test.ts b/__tests__/hooks/internal/useChatHistoryInternal.test.ts
index 61e33f8..3bace45 100644
--- a/__tests__/hooks/internal/useChatHistoryInternal.test.ts
+++ b/__tests__/hooks/internal/useChatHistoryInternal.test.ts
@@ -27,25 +27,25 @@ describe("useChatHistoryInternal Hook", () => {
 
 	// initial values
 	const initialIsLoadingChatHistory = false;
-    const initialChatHistory = [
-        {
-            id: generateSecureUUID(),
-            sender: "user",
-            content: "Hello",
-            type: "string",
-            timestamp: new Date().toUTCString()
-        },
-        {
-            id: generateSecureUUID(),
-            sender: "bot",
-            content: "Hi there!",
-            type: "string",
-            timestamp: new Date().toUTCString()
-        },
-    ];
+	const initialChatHistory = [
+		{
+			id: generateSecureUUID(),
+			sender: "user",
+			content: "Hello",
+			type: "string",
+			timestamp: new Date().toUTCString()
+		},
+		{
+			id: generateSecureUUID(),
+			sender: "bot",
+			content: "Hi there!",
+			type: "string",
+			timestamp: new Date().toUTCString()
+		},
+	];
 
 	it("should load chat history correctly, change state and emit rcb-load-chat-history event", async () => {
-        // mocks rcb event handler
+		// mocks rcb event handler
 		const callRcbEventMock = jest.fn().mockReturnValue({ defaultPrevented: false });
 		mockUseRcbEventInternal.mockReturnValue({
 			callRcbEvent: callRcbEventMock,
@@ -56,9 +56,8 @@ describe("useChatHistoryInternal Hook", () => {
 
 		// mocks loadChatHistory to resolve successfully
 		mockLoadChatHistory.mockImplementation(
-			(settings, styles, chatHistory, setMessages, prevTextAreaDisabled, setTextAreaDisabled) => {
+			(settings, styles, chatHistory, setMessages) => {
 				setMessages(chatHistory);
-				setTextAreaDisabled(false);
 				return Promise.resolve();
 			}
 		);
@@ -82,20 +81,18 @@ describe("useChatHistoryInternal Hook", () => {
 		// checks if get history messages was called
 		expect(mockGetHistoryMessages).toHaveBeenCalledTimes(1);
 
-        // checks if callRcbEvent was called with rcb-load-chat-history and correct arguments
+		// checks if callRcbEvent was called with rcb-load-chat-history and correct arguments
 		expect(callRcbEventMock).toHaveBeenCalledWith(RcbEvent.LOAD_CHAT_HISTORY, {});
-		
-        // checks if load chat history was called
-        expect(mockLoadChatHistory).toHaveBeenCalledWith(
+
+		// checks if load chat history was called
+		expect(mockLoadChatHistory).toHaveBeenCalledWith(
 			MockDefaultSettings,
 			expect.any(Object),
 			initialChatHistory,
 			expect.any(Function),
-			expect.any(Boolean),
-			expect.any(Function)
 		);
 
-        // checks if history is being loaded
+		// checks if history is being loaded
 		expect(result.current.isLoadingChatHistory).toBe(true);
 	});
 
@@ -125,13 +122,13 @@ describe("useChatHistoryInternal Hook", () => {
 		// checks if get history messages was called
 		expect(mockGetHistoryMessages).toHaveBeenCalledTimes(1);
 
-        // checks if callRcbEvent was called with rcb-load-chat-history and correct arguments
+		// checks if callRcbEvent was called with rcb-load-chat-history and correct arguments
 		expect(callRcbEventMock).toHaveBeenCalledWith(RcbEvent.LOAD_CHAT_HISTORY, {});
 
-        // checks if load chat history was not called
+		// checks if load chat history was not called
 		expect(mockLoadChatHistory).not.toHaveBeenCalled();
 
-        // checks if history is being loaded
+		// checks if history is being loaded
 		expect(result.current.isLoadingChatHistory).toBe(false);
 	});
 });
diff --git a/__tests__/services/BlockService/CheckboxProcessor.test.tsx b/__tests__/services/BlockService/CheckboxProcessor.test.tsx
new file mode 100644
index 0000000..86e1e34
--- /dev/null
+++ b/__tests__/services/BlockService/CheckboxProcessor.test.tsx
@@ -0,0 +1,88 @@
+import { processCheckboxes } from "../../../src/services/BlockService/CheckboxProcessor";
+import { Block } from "../../../src/types/Block";
+import { Flow } from "../../../src/types/Flow";
+import { Params } from "../../../src/types/Params";
+
+describe("processCheckboxes", () => {
+  let mockFlow: Flow;
+  let mockBlock: Block;
+  let mockParams: Params;
+
+  beforeEach(() => {
+    mockFlow = {} as Flow;
+    mockBlock = {} as Block;
+    mockParams = {
+      injectMessage: jest.fn(),
+    } as unknown as Params;
+  });
+
+  it("should return early if block has no checkboxes", async () => {
+    await processCheckboxes(mockFlow, mockBlock, "somePath", mockParams);
+
+    expect(mockParams.injectMessage).not.toHaveBeenCalled();
+  });
+
+  it("should process checkboxes when checkboxes are provided as a function", async () => {
+    const checkboxItems = ["Option 1", "Option 2"];
+    mockBlock.checkboxes = jest.fn().mockReturnValue(checkboxItems);
+
+    await processCheckboxes(mockFlow, mockBlock, "somePath", mockParams);
+
+    expect(mockBlock.checkboxes).toHaveBeenCalledWith(mockParams);
+    expect(mockParams.injectMessage).toHaveBeenCalled();
+  });
+
+  it("should handle async checkbox functions", async () => {
+    const asyncCheckboxItems = ["Option A", "Option B"];
+    mockBlock.checkboxes = jest.fn().mockResolvedValue(asyncCheckboxItems);
+
+    await processCheckboxes(mockFlow, mockBlock, "somePath", mockParams);
+
+    expect(mockBlock.checkboxes).toHaveBeenCalledWith(mockParams);
+    expect(mockParams.injectMessage).toHaveBeenCalled();
+  });
+
+  it("should convert checkbox array to object with items", async () => {
+    mockBlock.checkboxes = ["Checkbox 1", "Checkbox 2"];
+
+    await processCheckboxes(mockFlow, mockBlock, "somePath", mockParams);
+
+    const expectedCheckboxes = { items: ["Checkbox 1", "Checkbox 2"] };
+    expect(mockParams.injectMessage).toHaveBeenCalled();
+    const [content] = (mockParams.injectMessage as jest.Mock).mock.calls[0];
+    expect(content.props.checkboxes).toMatchObject(expectedCheckboxes);
+  });
+
+  it("should set min and max values when not provided", async () => {
+    mockBlock.checkboxes = { items: ["Item 1", "Item 2"] };
+
+    await processCheckboxes(mockFlow, mockBlock, "somePath", mockParams);
+
+    const expectedCheckboxes = { items: ["Item 1", "Item 2"], min: 1, max: 2 };
+    expect(mockParams.injectMessage).toHaveBeenCalled();
+    const [content] = (mockParams.injectMessage as jest.Mock).mock.calls[0];
+    expect(content.props.checkboxes).toMatchObject(expectedCheckboxes);
+  });
+
+  it("should handle invalid min/max values", async () => {
+    mockBlock.checkboxes = { items: ["Item 1", "Item 2"], min: 3, max: 2 };
+
+    await processCheckboxes(mockFlow, mockBlock, "somePath", mockParams);
+
+    const expectedCheckboxes = { items: ["Item 1", "Item 2"], min: 2, max: 2 };
+    expect(mockParams.injectMessage).toHaveBeenCalled();
+    const [content] = (mockParams.injectMessage as jest.Mock).mock.calls[0];
+    console.log("content.props.checkboxes:", content.props.checkboxes);
+    console.log("expectedCheckboxes:", expectedCheckboxes);
+
+    expect(content.props.checkboxes).toMatchObject(expectedCheckboxes);
+  });
+
+  it("should not inject message if no items are present in checkboxes", async () => {
+    mockBlock.checkboxes = { items: [] };
+
+    await processCheckboxes(mockFlow, mockBlock, "somePath", mockParams);
+
+    expect(mockParams.injectMessage).not.toHaveBeenCalled();
+  });
+});
diff --git a/__tests__/services/BlockService/OptionProcessor.test.tsx b/__tests__/services/BlockService/OptionProcessor.test.tsx
new file mode 100644
index 0000000..c95f550
--- /dev/null
+++ b/__tests__/services/BlockService/OptionProcessor.test.tsx
@@ -0,0 +1,89 @@
+import React from "react";
+import { expect } from "@jest/globals";
+
+import { processOptions } from "../../../src/services/BlockService/OptionProcessor";
+import { Params } from "../../../src/types/Params";
+import { Block } from "../../../src/types/Block";
+import { Flow } from "../../../src/types/Flow";
+import UserOptions from "../../../src/components/ChatBotBody/UserOptions/UserOptions";
+
+describe("processOptions", () => {
+	let mockParams: Params;
+	let mockBlock: Block;
+	let mockFlow: Flow;
+
+	// Mock Params with injectMessage function
+	beforeEach(() => {
+		mockParams = {
+			injectMessage: jest.fn() as Params["injectMessage"],
+		} as Params;
+
+		mockBlock = {
+			options: undefined,
+		} as Block;
+
+		mockFlow = {} as Flow;
+	});
+
+	it("should not call injectMessage if block has no options", async () => {
+		await processOptions(mockFlow, mockBlock, "somePath", mockParams);
+		expect(mockParams.injectMessage).not.toHaveBeenCalled();
+	});
+
+	it("should process static options and call injectMessage", async () => {
+		const staticOptions = ["Option1", "Option2"];
+		mockBlock.options = staticOptions;
+
+		await processOptions(mockFlow, mockBlock, "somePath", mockParams);
+		expect(mockParams.injectMessage).toHaveBeenCalledWith(
+			<UserOptions options={{ items: staticOptions, reusable: false }} path={"somePath"} />
+		);
+	});
+
+	it("should process dynamic options (function) and call injectMessage", async () => {
+		const dynamicOptions = ["DynamicOption1", "DynamicOption2"];
+		mockBlock.options = jest.fn().mockReturnValue(dynamicOptions);
+
+		await processOptions(mockFlow, mockBlock, "somePath", mockParams);
+    
+		expect(mockBlock.options).toHaveBeenCalledWith(mockParams);
+		expect(mockParams.injectMessage).toHaveBeenCalledWith(
+			<UserOptions options={{ items: dynamicOptions, reusable: false }} path={"somePath"} />
+		);
+	});
+
+	it("should await async function options and call injectMessage with resolved value", async () => {
+		const asyncOptions = ["AsyncOption1", "AsyncOption2"];
+		mockBlock.options = jest.fn().mockResolvedValue(asyncOptions);
+
+		await processOptions(mockFlow, mockBlock, "somePath", mockParams);
+		expect(mockBlock.options).toHaveBeenCalledWith(mockParams);
+		expect(mockParams.injectMessage).toHaveBeenCalledWith(
+			<UserOptions options={{ items: asyncOptions, reusable: false }} path={"somePath"} />
+		);
+	});
+
+	it("should set reusable to false by default if not provided", async () => {
+		const staticOptions = ["Option1", "Option2"];
+		mockBlock.options = staticOptions;
+
+		await processOptions(mockFlow, mockBlock, "somePath", mockParams);
+		expect(mockParams.injectMessage).toHaveBeenCalledWith(
+			<UserOptions options={{ items: staticOptions, reusable: false }} path={"somePath"} />
+		);
+	});
+
+	it("should not inject message if options is empty array", async () => {
+		mockBlock.options = [];
+
+		await processOptions(mockFlow, mockBlock, "somePath", mockParams);
+		expect(mockParams.injectMessage).not.toHaveBeenCalled();
+	});
+
+	it("should not inject message if options has no 'items'", async () => {
+		mockBlock.options = [];
+
+		await processOptions(mockFlow, mockBlock, "somePath", mockParams);
+		expect(mockParams.injectMessage).not.toHaveBeenCalled();
+	});
+});
diff --git a/__tests__/services/FunctionProcessor.test.ts b/__tests__/services/FunctionProcessor.test.ts
new file mode 100644
index 0000000..586f6e0
--- /dev/null
+++ b/__tests__/services/FunctionProcessor.test.ts
@@ -0,0 +1,60 @@
+import { processFunction } from "../../src/services/BlockService/FunctionProcessor";
+import { Block } from "../../src/types/Block";
+import { Params } from "../../src/types/Params";
+
+describe('processFunction', () => {
+    let params: Params;
+
+    beforeEach(() => {
+        params = {
+            userInput: "",
+            currPath: null,
+            prevPath: null,
+            goToPath: jest.fn(),
+            setTextAreaValue: jest.fn(),
+            injectMessage: jest.fn(),
+            streamMessage: jest.fn(),
+            removeMessage: jest.fn(),
+            endStreamMessage: jest.fn(),
+            showToast: jest.fn(),
+            dismissToast: jest.fn(),
+            openChat: jest.fn()
+        };
+    });
+
+    it('should return undefined if block has no function', async () => {
+        const block: Block = { function: undefined };
+
+        const result = await processFunction(block, params);
+
+        expect(result).toBeUndefined();
+    });
+
+    it('should return the result of the function if it is not a promise', async () => {
+        const block: Block = { function: jest.fn(() => 'result') };
+
+        const result = await processFunction(block, params);
+
+        expect(result).toBe('result');
+    });
+
+    it('should return the resolved value of the function if it is a promise', async () => {
+        const block: Block = { function: jest.fn(() => Promise.resolve('resolved value')) };
+
+        const result = await processFunction(block, params);
+
+        expect(result).toBe('resolved value');
+    });
+
+    it('should handle function throwing an error', async () => {
+        const block: Block = { function: jest.fn(() => { throw new Error('error'); }) };
+
+        await expect(processFunction(block, params)).rejects.toThrow('error');
+    });
+
+    it('should handle function returning a rejected promise', async () => {
+        const block: Block = { function: jest.fn(() => Promise.reject('rejected value')) };
+
+        await expect(processFunction(block, params)).rejects.toBe('rejected value');
+    });
+});