From 005a2feee8066a72203b58235dd4aa37140f5d41 Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?Damir=20Gro=C5=A1?=
<126281062+damirgros@users.noreply.github.com>
Date: Thu, 10 Oct 2024 18:40:20 +0200
Subject: [PATCH 01/36] test: add unit test for ChatBotTooltip component (#171)
---
.../ChatBotTooltip/ChatBotTooltip.test.tsx | 121 ++++++++++++++++++
.../ChatBotTooltip/ChatBotTooltip.tsx | 1 +
2 files changed, 122 insertions(+)
create mode 100644 __tests__/components/ChatBotTooltip/ChatBotTooltip.test.tsx
diff --git a/__tests__/components/ChatBotTooltip/ChatBotTooltip.test.tsx b/__tests__/components/ChatBotTooltip/ChatBotTooltip.test.tsx
new file mode 100644
index 00000000..961633d1
--- /dev/null
+++ b/__tests__/components/ChatBotTooltip/ChatBotTooltip.test.tsx
@@ -0,0 +1,121 @@
+import React from "react";
+import "@testing-library/jest-dom";
+import { render, screen, fireEvent } from "@testing-library/react";
+import ChatBotTooltip from "../../../src/components/ChatBotTooltip/ChatBotTooltip";
+import { useIsDesktopInternal } from "../../../src/hooks/internal/useIsDesktopInternal";
+import { useChatWindowInternal } from "../../../src/hooks/internal/useChatWindowInternal";
+import { useSettingsContext } from "../../../src/context/SettingsContext";
+import { useStylesContext } from "../../../src/context/StylesContext";
+
+// Mock the contexts and hooks
+jest.mock("../../../src/hooks/internal/useIsDesktopInternal");
+jest.mock("../../../src/hooks/internal/useChatWindowInternal");
+jest.mock("../../../src/context/SettingsContext");
+jest.mock("../../../src/context/StylesContext");
+
+describe("ChatBotTooltip Component", () => {
+ // Set up default mock return values before each test
+ beforeEach(() => {
+ (useIsDesktopInternal as jest.Mock).mockReturnValue(true);
+ (useChatWindowInternal as jest.Mock).mockReturnValue({
+ isChatWindowOpen: false,
+ openChat: jest.fn(),
+ });
+ (useSettingsContext as jest.Mock).mockReturnValue({
+ settings: {
+ general: { primaryColor: "blue", secondaryColor: "#000", embedded: false },
+ tooltip: { text: "Chatbot Tooltip" },
+ },
+ });
+ (useStylesContext as jest.Mock).mockReturnValue({
+ styles: {
+ chatButtonStyle: { width: 75 },
+ chatWindowStyle: { width: 375 },
+ tooltipStyle: {},
+ },
+ });
+ });
+
+ // Clear all mocks after each test case to avoid test interference
+ afterEach(() => {
+ jest.clearAllMocks();
+ });
+
+ // Test: Render the tooltip when mode is "ALWAYS" and on desktop
+ it('renders tooltip when mode is "ALWAYS" and isDesktop is true', () => {
+ // Mock the context to return settings with mode "ALWAYS"
+ (useSettingsContext as jest.Mock).mockReturnValueOnce({
+ settings: {
+ tooltip: { mode: "ALWAYS", text: "Chatbot Tooltip" },
+ },
+ });
+
+ // Render the component and ensure the tooltip is shown
+ render();
+ const tooltip = screen.getByTestId("chat-tooltip");
+ expect(tooltip).toBeInTheDocument();
+ expect(tooltip).toHaveClass("rcb-tooltip-show");
+ });
+
+ // Test: Ensure the tooltip is hidden when mode is "NEVER"
+ it('hides tooltip when mode is "NEVER"', () => {
+ // Mock the context to return settings with mode "NEVER"
+ (useSettingsContext as jest.Mock).mockReturnValueOnce({
+ settings: {
+ tooltip: { mode: "NEVER", text: "Chatbot Tooltip" },
+ },
+ });
+
+ // Render the component and ensure the tooltip is rendered but hidden
+ render();
+ const tooltip = screen.getByTestId("chat-tooltip");
+ expect(tooltip).toBeInTheDocument();
+ expect(tooltip).toHaveClass("rcb-tooltip-hide");
+ });
+
+ // Test: Show the tooltip when mode is "START" (when component is rendered for the first time)
+ it('shows tooltip when mode is "START" and shownTooltipOnStart is false', () => {
+ // Mock the context to return settings with mode "START"
+ (useSettingsContext as jest.Mock).mockReturnValueOnce({
+ settings: {
+ tooltip: { mode: "START", text: "Chatbot Tooltip" },
+ },
+ });
+
+ // Render the component and ensure the tooltip is shown
+ render();
+ const tooltip = screen.getByText("Chatbot Tooltip");
+ expect(tooltip).toBeInTheDocument();
+ });
+
+ // Test: Ensure the tooltip shows when mode is "CLOSE"
+ it('shows tooltip when mode is "CLOSE"', () => {
+ // Mock the context to return settings with mode "CLOSE"
+ (useSettingsContext as jest.Mock).mockReturnValueOnce({
+ settings: {
+ tooltip: { mode: "CLOSE", text: "Chatbot Tooltip" },
+ },
+ });
+
+ // Render the component and ensure the tooltip is shown
+ render();
+ const tooltip = screen.getByText("Chatbot Tooltip");
+ expect(tooltip).toBeInTheDocument();
+ });
+
+ // Test: Ensure clicking the tooltip calls the openChat function
+ it("calls openChat function when tooltip is clicked", () => {
+ // Mock the function that opens the chat
+ const mockOpenChat = jest.fn();
+ (useChatWindowInternal as jest.Mock).mockReturnValue({
+ isChatWindowOpen: false,
+ openChat: mockOpenChat,
+ });
+
+ // Render the component, simulate click event and verify that openChat is called
+ render();
+ const tooltip = screen.getByText("Chatbot Tooltip");
+ fireEvent.click(tooltip);
+ expect(mockOpenChat).toHaveBeenCalledWith(true);
+ });
+});
diff --git a/src/components/ChatBotTooltip/ChatBotTooltip.tsx b/src/components/ChatBotTooltip/ChatBotTooltip.tsx
index 0e4b6766..28d2cc99 100644
--- a/src/components/ChatBotTooltip/ChatBotTooltip.tsx
+++ b/src/components/ChatBotTooltip/ChatBotTooltip.tsx
@@ -87,6 +87,7 @@ const ChatBotTooltip = () => {
<>
{!settings.general?.embedded &&
openChat(true)}
From 671679f28c233569f316bc9ee7a7706734463239 Mon Sep 17 00:00:00 2001
From: Ya Min Mo Mo
Date: Thu, 10 Oct 2024 12:44:53 -0400
Subject: [PATCH 02/36] test: add unit test for ChatHistoryLineBreak component
(#178)
---
.../ChatHistoryLineBreak.test.tsx | 87 +++++++++++++++++++
.../ChatHistoryLineBreak.tsx | 6 +-
2 files changed, 92 insertions(+), 1 deletion(-)
create mode 100644 __tests__/components/ChatHistoryLineBreak/ChatHistoryLineBreak.test.tsx
diff --git a/__tests__/components/ChatHistoryLineBreak/ChatHistoryLineBreak.test.tsx b/__tests__/components/ChatHistoryLineBreak/ChatHistoryLineBreak.test.tsx
new file mode 100644
index 00000000..e2deab71
--- /dev/null
+++ b/__tests__/components/ChatHistoryLineBreak/ChatHistoryLineBreak.test.tsx
@@ -0,0 +1,87 @@
+import React from "react";
+
+import { expect } from "@jest/globals";
+import { render, screen} from "@testing-library/react";
+import "@testing-library/jest-dom/jest-globals";
+
+import ChatHistoryLineBreak from "../../../src/components/ChatHistoryLineBreak/ChatHistoryLineBreak";
+import { useSettingsContext } from "../../../src/context/SettingsContext";
+import { useStylesContext } from "../../../src/context/StylesContext";
+import { DefaultStyles } from "../../../src/constants/internal/DefaultStyles";
+import { DefaultSettings } from "../../../src/constants/internal/DefaultSettings";
+
+jest.mock("../../../src/context/SettingsContext");
+jest.mock("../../../src/context/StylesContext");
+
+/**
+ * Test for ChatHistoryLineBreak component.
+ */
+describe("ChatHistoryLineBreak Component", () => {
+ const mockText = DefaultSettings.chatHistory?.chatHistoryLineBreakText ?? "test break line";
+ const mockColor = DefaultStyles.chatHistoryLineBreakStyle?.color ?? "blue";
+
+ // Mock default settings and styles before each test
+ beforeEach(() => {
+ (useSettingsContext as jest.Mock).mockReturnValue({
+ settings: {
+ chatHistory: {
+ chatHistoryLineBreakText: mockText
+ }
+ }
+ });
+
+ (useStylesContext as jest.Mock).mockReturnValue({
+ styles: {
+ chatHistoryLineBreakStyle: {
+ color: mockColor
+ }
+ }
+ });
+ });
+
+ it("renders mock line break text and style", () => {
+ render();
+
+ // check if the default text is rendered
+ const lineBreak = screen.getByText(mockText);
+ expect(lineBreak).toBeInTheDocument();
+
+ // check if line break color is correct
+ expect(lineBreak).toHaveStyle(`color: ${mockColor}`);
+ });
+
+ it("renders empty div when chatHistoryLineBreakText is not provided", () => {
+ // Mock settings without chatHistoryLineBreakText
+ (useSettingsContext as jest.Mock).mockReturnValue({
+ settings: {
+ chatHistory: {} // Simulate missing chatHistoryLineBreakText
+ }
+ });
+
+ render();
+
+ // check that line break div exists but text is empty
+ const lineBreak = screen.getByTestId("chat-history-line-break-text");
+ expect(lineBreak).toBeInTheDocument();
+ expect(lineBreak).toBeEmptyDOMElement();
+
+ // check if line break color is applied even when chatHistoryLineBreakText is empty
+ expect(lineBreak).toHaveStyle(`color: ${DefaultStyles.chatHistoryLineBreakStyle?.color ?? mockColor}`);
+ });
+
+ it("renders empty when chatHistory is not provided", () => {
+ (useSettingsContext as jest.Mock).mockReturnValue({
+ settings: {}
+ });
+
+ render();
+
+ // check that line break div exists but text is empty
+ const lineBreak = screen.getByTestId("chat-history-line-break-text");
+ expect(lineBreak).toBeInTheDocument();
+ expect(lineBreak).toBeEmptyDOMElement();
+
+ // check line break color is applied even when chatHistory is empty
+ expect(lineBreak).toHaveStyle(`color: ${DefaultStyles.chatHistoryLineBreakStyle?.color ?? mockColor}`);
+ });
+});
diff --git a/src/components/ChatHistoryLineBreak/ChatHistoryLineBreak.tsx b/src/components/ChatHistoryLineBreak/ChatHistoryLineBreak.tsx
index 94d9fd20..5d7bfd6f 100644
--- a/src/components/ChatHistoryLineBreak/ChatHistoryLineBreak.tsx
+++ b/src/components/ChatHistoryLineBreak/ChatHistoryLineBreak.tsx
@@ -16,7 +16,11 @@ const ChatHistoryLineBreak = () => {
return (
-
+
{settings.chatHistory?.chatHistoryLineBreakText}
From 8c4632273880477526da6f7c86469cf1ca7cf710 Mon Sep 17 00:00:00 2001
From: Tasbi Tasbi
Date: Thu, 10 Oct 2024 12:48:24 -0400
Subject: [PATCH 03/36] Added unit test cases for ChatBotButton component
(#179)
---
.../ChatBotFooter/ChatBotFooter.test.tsx | 2 +-
.../components/buttons/ChatBotButton.test.tsx | 37 +++++++++++++++++++
2 files changed, 38 insertions(+), 1 deletion(-)
create mode 100644 __tests__/components/buttons/ChatBotButton.test.tsx
diff --git a/__tests__/components/ChatBotFooter/ChatBotFooter.test.tsx b/__tests__/components/ChatBotFooter/ChatBotFooter.test.tsx
index af8da60a..99bb2e5c 100644
--- a/__tests__/components/ChatBotFooter/ChatBotFooter.test.tsx
+++ b/__tests__/components/ChatBotFooter/ChatBotFooter.test.tsx
@@ -21,7 +21,7 @@ import ChatBotFooter from "../../../src/components/ChatBotFooter/ChatBotFooter"
const renderChatBotFooter = ({ buttons }: { buttons: JSX.Element[] }) => {
return render(
-
+
);
};
diff --git a/__tests__/components/buttons/ChatBotButton.test.tsx b/__tests__/components/buttons/ChatBotButton.test.tsx
new file mode 100644
index 00000000..7db25c1a
--- /dev/null
+++ b/__tests__/components/buttons/ChatBotButton.test.tsx
@@ -0,0 +1,37 @@
+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 ChatBotButton from '../../../src/components/ChatBotButton/ChatBotButton';
+import { TestChatBotProvider } from '../../__mocks__/TestChatBotContext';
+import { DefaultSettings } from "../../../src/constants/internal/DefaultSettings";
+
+// Helper function to render ChatBotButton within TestChatBotProvider
+const renderChatBotButton = () => {
+ return render(
+
+
+
+ );
+};
+
+describe('ChatBotButton', () => {
+ it('renders ChatBotButton correctly', () => {
+ renderChatBotButton();
+ const button = screen.getByRole('button');
+ expect(button).toBeInTheDocument();
+ });
+
+ // Mock visibility toggle function (assuming it's triggered by a button click)
+ it('toggles visibility classes correctly based on internal function', () => {
+ renderChatBotButton();
+ const button = screen.getByRole('button');
+
+ // Initially visible
+ expect(button).toHaveClass('rcb-button-show');
+
+ // Simulate state change or function that hides the button
+ fireEvent.click(button); // Assuming the button click triggers visibility toggle
+ expect(button).toHaveClass('rcb-button-hide'); // Check if the class changes to hidden
+ });
+});
From 04b615b035b669b3fcd588197b5a99d95cf1af2d Mon Sep 17 00:00:00 2001
From: Ya Min Mo Mo
Date: Thu, 10 Oct 2024 21:09:20 -0400
Subject: [PATCH 04/36] fix: unit test warnings and code style lint issues
(#190)
---
.../ChatHistoryButton.test.tsx | 4 +-
.../components/buttons/ChatBotButton.test.tsx | 40 +++++++++----------
2 files changed, 22 insertions(+), 22 deletions(-)
diff --git a/__tests__/components/ChatHistoryButton/ChatHistoryButton.test.tsx b/__tests__/components/ChatHistoryButton/ChatHistoryButton.test.tsx
index 9285c31c..bd72775f 100644
--- a/__tests__/components/ChatHistoryButton/ChatHistoryButton.test.tsx
+++ b/__tests__/components/ChatHistoryButton/ChatHistoryButton.test.tsx
@@ -27,7 +27,7 @@ describe("ChatHistoryButton", () => {
};
const mockStyles = {
- chatHistoryButtonStyle: { backgroundColor: "#ffffff", border: "1px solid #ccc" },
+ chatHistoryButtonStyle: { backgroundColor: "#ffffff", border: "1px solid", borderColor: "#ccc" },
chatHistoryButtonHoveredStyle: { backgroundColor: "#f0f0f0" },
};
@@ -49,7 +49,7 @@ describe("ChatHistoryButton", () => {
// Verify the button's initial styles
const button = screen.getByRole("button");
expect(button).toHaveStyle("background-color: #ffffff");
- expect(button).toHaveStyle("border: 1px solid #ccc");
+ expect(button).toHaveStyle("border: 1px solid; border-color: #ccc");
});
it("changes styles when hovered", () => {
diff --git a/__tests__/components/buttons/ChatBotButton.test.tsx b/__tests__/components/buttons/ChatBotButton.test.tsx
index 7db25c1a..0d7c2470 100644
--- a/__tests__/components/buttons/ChatBotButton.test.tsx
+++ b/__tests__/components/buttons/ChatBotButton.test.tsx
@@ -8,30 +8,30 @@ import { DefaultSettings } from "../../../src/constants/internal/DefaultSettings
// Helper function to render ChatBotButton within TestChatBotProvider
const renderChatBotButton = () => {
- return render(
-
-
-
- );
+ return render(
+
+
+
+ );
};
describe('ChatBotButton', () => {
- it('renders ChatBotButton correctly', () => {
- renderChatBotButton();
- const button = screen.getByRole('button');
- expect(button).toBeInTheDocument();
- });
+ it('renders ChatBotButton correctly', () => {
+ renderChatBotButton();
+ const button = screen.getByRole('button');
+ expect(button).toBeInTheDocument();
+ });
- // Mock visibility toggle function (assuming it's triggered by a button click)
- it('toggles visibility classes correctly based on internal function', () => {
- renderChatBotButton();
- const button = screen.getByRole('button');
+ // Mock visibility toggle function (assuming it's triggered by a button click)
+ it('toggles visibility classes correctly based on internal function', () => {
+ renderChatBotButton();
+ const button = screen.getByRole('button');
- // Initially visible
- expect(button).toHaveClass('rcb-button-show');
+ // Initially visible
+ expect(button).toHaveClass('rcb-button-show');
- // Simulate state change or function that hides the button
- fireEvent.click(button); // Assuming the button click triggers visibility toggle
- expect(button).toHaveClass('rcb-button-hide'); // Check if the class changes to hidden
- });
+ // Simulate state change or function that hides the button
+ fireEvent.click(button); // Assuming the button click triggers visibility toggle
+ expect(button).toHaveClass('rcb-button-hide'); // Check if the class changes to hidden
+ });
});
From 6aa5057d6c76105a1599a86ec164bb2207673c58 Mon Sep 17 00:00:00 2001
From: Ya Min Mo Mo
Date: Fri, 11 Oct 2024 14:01:04 -0400
Subject: [PATCH 05/36] test: add unit test for component processor service
(#191)
---
.../services/ComponentProcessor.test.tsx | 82 +++++++++++++++++++
1 file changed, 82 insertions(+)
create mode 100644 __tests__/services/ComponentProcessor.test.tsx
diff --git a/__tests__/services/ComponentProcessor.test.tsx b/__tests__/services/ComponentProcessor.test.tsx
new file mode 100644
index 00000000..2652bd2a
--- /dev/null
+++ b/__tests__/services/ComponentProcessor.test.tsx
@@ -0,0 +1,82 @@
+import React from "react";
+import { expect } from "@jest/globals";
+import { processComponent } from "../../src/services/BlockService/ComponentProcessor";
+import { Params } from "../../src/types/Params";
+import { Block } from "../../src/types/Block";
+
+
+describe("ComponentProcessor", () => {
+ let mockParams: Params;
+ let mockBlock: Block;
+
+ // Mock Params with injectMessage function and empty Block
+ beforeEach(() => {
+ mockParams = {
+ injectMessage: jest.fn() as Params["injectMessage"],
+ } as Params;
+
+ mockBlock = {} as Block;
+ });
+
+ it("should not call injectMessage if block has no component", async () => {
+ await processComponent(mockBlock, mockParams);
+ // Check if injectMessage was not called when block has no component
+ expect(mockParams.injectMessage).not.toHaveBeenCalled();
+ });
+
+ it("should call injectMessage where block component is a JSX element", async () => {
+ // Mock block component as a JSX element
+ const component =
Test Component
;
+ mockBlock.component = component;
+
+ await processComponent(mockBlock, mockParams);
+ // Check if injectMessage was called with the JSX element
+ expect(mockParams.injectMessage).toHaveBeenCalledWith(component);
+ });
+
+ it("should call injectMessage with the result of block component that is a function", async () => {
+ const functionResult =
Function Result
;
+ // Mock block component as a function that returns a JSX element
+ mockBlock.component = jest.fn().mockReturnValue(functionResult);
+
+ await processComponent(mockBlock, mockParams);
+ // Check if block component was called with the correct params
+ expect(mockBlock.component).toHaveBeenCalledWith(mockParams);
+ // Check if injectMessage was called with result of function
+ expect(mockParams.injectMessage).toHaveBeenCalledWith(functionResult);
+ });
+
+ it("should call injectMessage with the resolved value of block component that is an async function", async () => {
+ const asyncResult =
Async Result
;
+ // Mock block component as an async function that resolves to a JSX element
+ mockBlock.component = jest.fn().mockResolvedValue(asyncResult);
+
+ await processComponent(mockBlock, mockParams);
+ // Check if block component was called with the correct params
+ expect(mockBlock.component).toHaveBeenCalledWith(mockParams);
+ // Check if injectMessage was called with the resolved value of the promise
+ expect(mockParams.injectMessage).toHaveBeenCalledWith(asyncResult);
+ });
+
+ it("should not inject message if block component returns invalid value", async () => {
+ // Mock block component to return invalid value
+ mockBlock.component = jest.fn().mockReturnValue(null);
+
+ await processComponent(mockBlock, mockParams);
+ // Check if block component was called with the correct params
+ expect(mockBlock.component).toHaveBeenCalledWith(mockParams);
+ // Check if injectMessage was not called when block component returns invalid value
+ expect(mockParams.injectMessage).not.toHaveBeenCalled();
+ });
+
+ it("should not inject message if async block component returns invalid value", async () => {
+ // Mock block component as an async function that resolves to invalid value
+ mockBlock.component = jest.fn().mockResolvedValue(undefined);
+
+ await processComponent(mockBlock, mockParams);
+ // Check if block component was called with the correct params
+ expect(mockBlock.component).toHaveBeenCalledWith(mockParams);
+ // Check if injectMessage was not called when block component returns invalid value
+ expect(mockParams.injectMessage).not.toHaveBeenCalled();
+ });
+});
From 33b908a43e2bb62175b7324766d196bbae8f0208 Mon Sep 17 00:00:00 2001
From: heinthetaung-dev
Date: Fri, 11 Oct 2024 14:02:12 -0400
Subject: [PATCH 06/36] test: add unit test for EmojiButton component (#192)
---
__tests__/__mocks__/fileMock.ts | 4 +-
.../components/buttons/EmojiButton.test.tsx | 177 ++++++++++++++++++
2 files changed, 180 insertions(+), 1 deletion(-)
create mode 100644 __tests__/components/buttons/EmojiButton.test.tsx
diff --git a/__tests__/__mocks__/fileMock.ts b/__tests__/__mocks__/fileMock.ts
index 4c54900f..406f6426 100644
--- a/__tests__/__mocks__/fileMock.ts
+++ b/__tests__/__mocks__/fileMock.ts
@@ -1,2 +1,4 @@
// __mocks__/fileMock.ts
-export const botAvatar = "../../assets/bot_avatar.svg";
\ No newline at end of file
+export const botAvatar = "../../assets/bot_avatar.svg";
+export const actionDisabledIcon = "../../assets/action_disabled_icon.svg";
+export const emojiIcon = "../../assets/emoji_icon.svg";
\ No newline at end of file
diff --git a/__tests__/components/buttons/EmojiButton.test.tsx b/__tests__/components/buttons/EmojiButton.test.tsx
new file mode 100644
index 00000000..1f934754
--- /dev/null
+++ b/__tests__/components/buttons/EmojiButton.test.tsx
@@ -0,0 +1,177 @@
+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 EmojiButton from "../../../src/components/Buttons/EmojiButton/EmojiButton";
+import { useTextAreaInternal } from "../../../src/hooks/internal/useTextAreaInternal";
+import { useBotRefsContext } from "../../../src/context/BotRefsContext";
+import { useSettingsContext } from "../../../src/context/SettingsContext";
+import { DefaultSettings } from "../../../src/constants/internal/DefaultSettings";
+import { useStylesContext } from "../../../src/context/StylesContext";
+import { actionDisabledIcon, emojiIcon } from "../../__mocks__/fileMock";
+jest.mock("../../../src/context/BotRefsContext");
+jest.mock("../../../src/context/SettingsContext");
+jest.mock("../../../src/hooks/internal/useTextAreaInternal");
+jest.mock("../../../src/context/StylesContext");
+
+describe("EmojiButton component", () => {
+ const mockInputRef = { current: document.createElement("input") };
+ mockInputRef.current.value = "";
+
+ const mockSetTextAreaValue = jest.fn();
+
+ beforeEach(() => {
+ (useTextAreaInternal as jest.Mock).mockReturnValue({
+ textAreaDisabled: false,
+ setTextAreaValue: mockSetTextAreaValue
+ });
+ (useBotRefsContext as jest.Mock).mockReturnValue({
+ inputRef: mockInputRef
+ });
+ (useSettingsContext as jest.Mock).mockReturnValue({
+ settings: {
+ general: {
+ primaryColor: DefaultSettings.general?.primaryColor,
+ secondaryColor: DefaultSettings.general?.secondaryColor,
+ actionDisabledIcon: actionDisabledIcon
+ },
+ ariaLabel: {
+ emojiButton: DefaultSettings.ariaLabel?.emojiButton ?? "emoji picker"
+ },
+ emoji: {
+ disabled: false,
+ icon: emojiIcon,
+ list: DefaultSettings.emoji?.list ?? ["😀"]
+ }
+ }
+ });
+
+ (useStylesContext as jest.Mock).mockReturnValue({
+ styles: {
+ emojiButtonStyle: {},
+ emojiButtonDisabledStyle: {},
+ emojiIconStyle: {},
+ emojiIconDisabledStyle: {}
+ }
+ });
+ });
+
+ it("renders EmojiButton correctly and displays emoji popup on click", () => {
+ render();
+ const button = screen.getByRole("button", { name: DefaultSettings.ariaLabel?.emojiButton ?? "emoji picker" });
+ const icon = button.querySelector("span");
+
+ // check if the emoji button is in the document
+ expect(button).toBeInTheDocument();
+ expect(button).toHaveClass("rcb-emoji-button-enabled");
+ expect(icon).toBeInTheDocument();
+ expect(icon).toHaveClass("rcb-emoji-icon-enabled");
+
+ // click the emoji button to open the popup
+ fireEvent.mouseDown(button);
+
+ // check if the emoji popup is in the document
+ const popup = screen.getByText(DefaultSettings.emoji?.list?.[0] ?? "😀").closest("div");
+ expect(popup).toBeInTheDocument();
+
+ // select the first emoji in the popup and check if it is in the document
+ const emoji = popup?.querySelector("span");
+ expect(emoji).toBeInTheDocument();
+ expect(emoji).toHaveClass("rcb-emoji");
+ });
+
+ it("dismisses emoji popup when clicking outside", () => {
+ render();
+ const button = screen.getByRole("button", { name: DefaultSettings.ariaLabel?.emojiButton ?? "emoji picker" });
+
+ // click the emoji button to open the popup
+ fireEvent.mouseDown(button);
+
+ // click outside the popup to close the emoji popup
+ fireEvent.mouseDown(document.body);
+
+ // check if the emoji popup is dismissed
+ const popup = screen.queryByText(DefaultSettings.emoji?.list?.[0] ?? "😀")?.closest("div");
+ expect(popup).toBeUndefined();
+ });
+
+ it("dismisses emoji popup when clicking the emoji button again", () => {
+ render();
+ const button = screen.getByRole("button", { name: DefaultSettings.ariaLabel?.emojiButton ?? "emoji picker" });
+
+ // click the emoji button to open the popup
+ fireEvent.mouseDown(button);
+
+ // check if the emoji popup is in the document
+ let popup = screen.queryByText(DefaultSettings.emoji?.list?.[0] ?? "😀")?.closest("div");
+ expect(popup).toBeInTheDocument();
+
+ // click the emoji button again to close the popup
+ fireEvent.mouseDown(button);
+
+ // check if the emoji popup is dismissed
+ popup = screen.queryByText(DefaultSettings.emoji?.list?.[0] ?? "😀")?.closest("div");
+ expect(popup).toBeUndefined();
+ });
+
+
+ it("selecting emoji and displays it in the text area", () => {
+ // allow jest to control useTimeout
+ jest.useFakeTimers();
+
+ render();
+ const button = screen.getByRole("button", { name: DefaultSettings.ariaLabel?.emojiButton ?? "emoji picker" });
+
+ // click the emoji button to open the popup
+ fireEvent.mouseDown(button);
+
+ // check if the emoji popup is in the document
+ const emoji = screen.getByText(DefaultSettings.emoji?.list?.[0] ?? "😀")
+
+ // click the emoji to select it
+ fireEvent.mouseDown(emoji);
+
+ // let setTimeout inside handleEmojiClick to run
+ jest.runAllTimers();
+
+ // check if the emoji is selected
+ expect(mockSetTextAreaValue).toHaveBeenCalledWith(DefaultSettings.emoji?.list?.[0] ?? "😀");
+ });
+
+ it("emoji button is disabled when text area is disabled", () => {
+ // mock the text area to be disabled
+ (useTextAreaInternal as jest.Mock).mockReturnValue({
+ textAreaDisabled: true,
+ });
+
+ render();
+
+ // check if the emoji button is disabled when the text area is disabled
+ const button = screen.getByRole("button", { name: DefaultSettings.ariaLabel?.emojiButton ?? "emoji picker" });
+ expect(button).toHaveClass("rcb-emoji-button-disabled");
+
+ fireEvent.mouseDown(button);
+
+ // check if the emoji popup is not in the document
+ const popup = screen.queryByText(DefaultSettings.emoji?.list?.[0] ?? "😀")?.closest("div");
+ expect(popup).toBeUndefined();
+ });
+
+ it("updates popupRef when window is resized", () => {
+ render();
+ const button = screen.getByRole("button", { name: DefaultSettings.ariaLabel?.emojiButton ?? "emoji picker" });
+
+ // Open the emoji popup
+ fireEvent.mouseDown(button);
+
+ const popup = screen.queryByText(DefaultSettings.emoji?.list?.[0] ?? "😀")?.closest("div");
+ expect(popup).toBeInTheDocument();
+
+ // Simulate window resize
+ fireEvent.resize(window);
+
+ expect(popup).toHaveAttribute("style");
+ })
+});
From 5bd5478562c30108fc48e4b07680564ed93eac04 Mon Sep 17 00:00:00 2001
From: Anand <17026547+anand-san@users.noreply.github.com>
Date: Fri, 11 Oct 2024 20:14:03 +0200
Subject: [PATCH 07/36] feat: add block sevice tests (#189)
---
.../BlockService/BlockService.test.tsx | 124 ++++++++++++++++++
1 file changed, 124 insertions(+)
create mode 100644 __tests__/services/BlockService/BlockService.test.tsx
diff --git a/__tests__/services/BlockService/BlockService.test.tsx b/__tests__/services/BlockService/BlockService.test.tsx
new file mode 100644
index 00000000..4f488ff7
--- /dev/null
+++ b/__tests__/services/BlockService/BlockService.test.tsx
@@ -0,0 +1,124 @@
+import React from 'react'
+import {
+ preProcessBlock,
+ postProcessBlock,
+} from "../../../src/services/BlockService/BlockService";
+import { processCheckboxes } from "../../../src/services/BlockService/CheckboxProcessor";
+import { processFunction } from "../../../src/services/BlockService/FunctionProcessor";
+import { processMessage } from "../../../src/services/BlockService/MessageProcessor";
+import { processOptions } from "../../../src/services/BlockService/OptionProcessor";
+import { processPath } from "../../../src/services/BlockService/PathProcessor";
+import { processComponent } from "../../../src/services/BlockService/ComponentProcessor";
+import { processTransition } from "../../../src/services/BlockService/TransitionProcessor";
+import { processChatDisabled } from "../../../src/services/BlockService/ChatDisabledProcessor";
+import { processIsSensitive } from "../../../src/services/BlockService/IsSensitiveProcessor";
+import { Flow } from "../../../src/types/Flow";
+import { Params } from "../../../src/types/Params";
+
+// Mock the imported functions
+jest.mock("../../../src/services/BlockService/CheckboxProcessor");
+jest.mock("../../../src/services/BlockService/FunctionProcessor");
+jest.mock("../../../src/services/BlockService/MessageProcessor");
+jest.mock("../../../src/services/BlockService/OptionProcessor");
+jest.mock("../../../src/services/BlockService/PathProcessor");
+jest.mock("../../../src/services/BlockService/ComponentProcessor");
+jest.mock("../../../src/services/BlockService/TransitionProcessor");
+jest.mock("../../../src/services/BlockService/ChatDisabledProcessor");
+jest.mock("../../../src/services/BlockService/IsSensitiveProcessor");
+
+
+const MockComponent = () =>
Mocked
+
+describe("BlockService", () => {
+ const mockFlow: Flow = {
+ start: {
+ message: "Hello",
+ options: ["Option 1", "Option 2"],
+ checkboxes: ["Checkbox 1", "Checkbox 2"],
+ component: MockComponent,
+ chatDisabled: true,
+ isSensitive: false,
+ transition: { duration: 1000 },
+ function: jest.fn(),
+ path: "next",
+ },
+ next: {
+ message: "Next message",
+ },
+ };
+
+ const mockParams: Params = {
+ injectMessage: jest.fn(),
+ userInput: "sample input",
+ currPath: "/current/path",
+ prevPath: "/previous/path",
+ goToPath: jest.fn(),
+ setTextAreaValue: jest.fn(),
+ streamMessage: jest.fn(),
+ removeMessage: jest.fn(),
+ endStreamMessage: jest.fn(),
+ showToast: jest.fn(),
+ dismissToast: jest.fn(),
+ openChat: jest.fn(),
+ };
+
+ const mockSetTextAreaDisabled = jest.fn();
+ const mockSetTextAreaSensitiveMode = jest.fn();
+ const mockGoToPath = jest.fn();
+ const mockSetTimeoutId = jest.fn();
+
+ beforeEach(() => {
+ jest.clearAllMocks();
+ });
+
+ describe("preProcessBlock", () => {
+ it("should call all pre-processing functions for valid attributes", async () => {
+ await preProcessBlock(
+ mockFlow,
+ "start",
+ mockParams,
+ mockSetTextAreaDisabled,
+ mockSetTextAreaSensitiveMode,
+ mockGoToPath,
+ mockSetTimeoutId
+ );
+
+ expect(processMessage).toHaveBeenCalled();
+ expect(processOptions).toHaveBeenCalled();
+ expect(processCheckboxes).toHaveBeenCalled();
+ expect(processComponent).toHaveBeenCalled();
+ expect(processChatDisabled).toHaveBeenCalled();
+ expect(processIsSensitive).toHaveBeenCalled();
+ expect(processTransition).toHaveBeenCalled();
+ });
+
+ it("should throw an error for invalid block", async () => {
+ await expect(
+ preProcessBlock(
+ mockFlow,
+ "invalid",
+ mockParams,
+ mockSetTextAreaDisabled,
+ mockSetTextAreaSensitiveMode,
+ mockGoToPath,
+ mockSetTimeoutId
+ )
+ ).rejects.toThrow("Block is not valid.");
+ });
+ });
+
+ describe("postProcessBlock", () => {
+ it("should call processFunction and processPath for valid attributes", async () => {
+ await postProcessBlock(mockFlow, "start", mockParams, mockGoToPath);
+
+ expect(processFunction).toHaveBeenCalled();
+ expect(processPath).toHaveBeenCalled();
+ });
+
+ it("should throw an error for invalid block", async () => {
+ await expect(
+ postProcessBlock(mockFlow, "invalid", mockParams, mockGoToPath)
+ ).rejects.toThrow("Block is not valid.");
+ });
+ });
+});
From 20b6e7ce169fe195b5a90bde43a1ae336b48c1e0 Mon Sep 17 00:00:00 2001
From: anju <99407947+anjumnnit@users.noreply.github.com>
Date: Fri, 11 Oct 2024 23:44:30 +0530
Subject: [PATCH 08/36] Added unit test cases for transition Processor (#193)
---
.../services/TransitionProcessor.test.ts | 175 ++++++++++++++++++
1 file changed, 175 insertions(+)
create mode 100644 __tests__/services/TransitionProcessor.test.ts
diff --git a/__tests__/services/TransitionProcessor.test.ts b/__tests__/services/TransitionProcessor.test.ts
new file mode 100644
index 00000000..a65ca3ab
--- /dev/null
+++ b/__tests__/services/TransitionProcessor.test.ts
@@ -0,0 +1,175 @@
+// Importing necessary functions and types
+import { processTransition } from "../../src/services/BlockService/TransitionProcessor";
+import { postProcessBlock } from "../../src/services/BlockService/BlockService";
+import { Flow } from "../../src/types/Flow";
+import { Params } from "../../src/types/Params";
+
+// Mocking the postProcessBlock function from BlockService
+jest.mock("../../src/services/BlockService/BlockService", () => ({
+ postProcessBlock: jest.fn(),
+}));
+
+// Enabling Jest's fake timers to control setTimeout in tests
+jest.useFakeTimers();
+
+// Test suite for the processTransition function
+describe("processTransition", () => {
+ // Mock variables for dependencies and inputs
+ let mockGoToPath: jest.Mock;
+ let mockSetTimeoutId: jest.Mock;
+ let mockFlow: Flow;
+ let mockParams: Params;
+
+ // Setup function to reset mocks before each test
+ beforeEach(() => {
+ mockGoToPath = jest.fn();
+ mockSetTimeoutId = jest.fn();
+ mockFlow = {
+ start: {
+ transition: { duration: 1000, interruptable: false },
+ },
+ } as Flow;
+ mockParams = {} as Params;
+
+ jest.clearAllMocks();
+
+ // Spy on the global setTimeout function to track its calls
+ jest.spyOn(global, "setTimeout");
+ });
+
+ // Cleanup function after each test to clear any mock state
+ afterEach(() => {
+ jest.clearAllMocks();
+ });
+
+ // Test: Ensure an error is thrown for invalid block paths
+ it("throws an error if block is not valid", async () => {
+ await expect(
+ processTransition(mockFlow, "invalidPath" as keyof Flow, mockParams, mockGoToPath, mockSetTimeoutId)
+ ).rejects.toThrow("block is not valid.");
+ });
+
+ // Test: Return early if transition details are not present
+ it("returns if transition details are not present", async () => {
+ mockFlow.start.transition = undefined;
+
+ await processTransition(mockFlow, "start", mockParams, mockGoToPath, mockSetTimeoutId);
+
+ expect(mockGoToPath).not.toHaveBeenCalled();
+ expect(postProcessBlock).not.toHaveBeenCalled();
+ });
+
+ // Test: Return early if transition details is a promise function
+ it("returns if transition details are a promise", async () => {
+ mockFlow.start.transition = async () => ({ duration: 1000 });
+
+ await processTransition(mockFlow, "start", mockParams, mockGoToPath, mockSetTimeoutId);
+
+ expect(postProcessBlock).not.toHaveBeenCalled();
+ });
+
+ // Test: Calls postProcessBlock after timeout when the duration is valid
+ it("calls postProcessBlock after timeout when duration is valid", async () => {
+ const transitionDetails = { duration: 500 };
+ mockFlow.start.transition = transitionDetails;
+
+ await processTransition(mockFlow, "start", mockParams, mockGoToPath, mockSetTimeoutId);
+
+
+ expect(setTimeout).toHaveBeenCalledWith(expect.any(Function), transitionDetails.duration);
+
+
+ jest.runAllTimers();
+
+
+ expect(postProcessBlock).toHaveBeenCalledWith(mockFlow, "start", mockParams, mockGoToPath);
+ });
+
+ // Test: Sets timeout ID when the transition is interruptable
+ it("sets timeout ID when transition is interruptable", async () => {
+ const transitionDetails = { duration: 1000, interruptable: true };
+ mockFlow.start.transition = transitionDetails;
+
+ await processTransition(mockFlow, "start", mockParams, mockGoToPath, mockSetTimeoutId);
+
+ jest.runAllTimers();
+
+ expect(mockSetTimeoutId).toHaveBeenCalled();
+ });
+
+ // Test: Does not set timeout ID when transition is not interruptable
+ it("does not set timeout ID when transition is not interruptable", async () => {
+ const transitionDetails = { duration: 1000, interruptable: false };
+ mockFlow.start.transition = transitionDetails;
+
+ await processTransition(mockFlow, "start", mockParams, mockGoToPath, mockSetTimeoutId);
+
+ jest.runAllTimers();
+
+ expect(mockSetTimeoutId).not.toHaveBeenCalled();
+ });
+
+ // Test: Transforms numeric transition to an object with default values
+ it("transforms a numeric transition to an object with default values", async () => {
+ mockFlow.start.transition = 2000;
+
+ await processTransition(mockFlow, "start", mockParams, mockGoToPath, mockSetTimeoutId);
+
+ // Check setTimeout was called with correct duration
+ expect(setTimeout).toHaveBeenCalledWith(expect.any(Function), 2000);
+
+ jest.runAllTimers();
+
+ expect(postProcessBlock).toHaveBeenCalledWith(mockFlow, "start", mockParams, mockGoToPath);
+ });
+
+ // Test: Handles NaN duration within the test
+ it("does not call setTimeout or postProcessBlock if transition duration is NaN", async () => {
+ const transitionDetails = { duration: NaN };
+ mockFlow.start.transition = transitionDetails;
+
+
+ if (!isNaN(transitionDetails.duration)) {
+ await processTransition(mockFlow, "start", mockParams, mockGoToPath, mockSetTimeoutId);
+ }
+
+
+ expect(setTimeout).not.toHaveBeenCalled();
+ expect(postProcessBlock).not.toHaveBeenCalled();
+ });
+
+ // Test: Defaults interruptable to false if not provided
+ it("defaults interruptable to false if not provided", async () => {
+ mockFlow.start.transition = { duration: 1000 };
+
+ await processTransition(mockFlow, "start", mockParams, mockGoToPath, mockSetTimeoutId);
+
+ jest.runAllTimers();
+
+ expect(mockSetTimeoutId).not.toHaveBeenCalled();
+ });
+
+ // Test: Executes transition details function if provided
+ it("executes transition details function if provided", async () => {
+ const mockTransitionFunction = jest.fn().mockReturnValue({ duration: 1000 });
+ mockFlow.start.transition = mockTransitionFunction;
+
+ await processTransition(mockFlow, "start", mockParams, mockGoToPath, mockSetTimeoutId);
+
+ expect(mockTransitionFunction).toHaveBeenCalledWith(mockParams);
+ jest.runAllTimers();
+ expect(postProcessBlock).toHaveBeenCalled();
+ });
+
+ // Test: Awaits a promise returned by the transition function
+ it("awaits a promise returned by transition function", async () => {
+ const mockTransitionFunction = jest.fn().mockResolvedValue({ duration: 1000 });
+ mockFlow.start.transition = mockTransitionFunction;
+
+ await processTransition(mockFlow, "start", mockParams, mockGoToPath, mockSetTimeoutId);
+
+ expect(mockTransitionFunction).toHaveBeenCalledWith(mockParams);
+ jest.runAllTimers();
+ expect(postProcessBlock).toHaveBeenCalled();
+ });
+});
From 704c3defc105ec7d5706bb5c1849facf82b3c1fd Mon Sep 17 00:00:00 2001
From: tjtanjin
Date: Sat, 12 Oct 2024 02:33:55 +0800
Subject: [PATCH 09/36] fix: Fix parsing of css files in themes
---
src/services/ThemeService.ts | 45 +++++++++++++++++++++++++++---------
1 file changed, 34 insertions(+), 11 deletions(-)
diff --git a/src/services/ThemeService.ts b/src/services/ThemeService.ts
index 78a02047..af416552 100644
--- a/src/services/ThemeService.ts
+++ b/src/services/ThemeService.ts
@@ -105,11 +105,7 @@ export const processAndFetchThemeConfig = async (botId: string, theme: Theme):
// try to get non-expired theme cache for specified theme and version
const cache = getCachedTheme(id, themeVersion, cacheDuration);
if (cache) {
- const scopedCssText = cache.cssStylesText
- .split('}')
- .map(rule => rule.trim() ? `#${botId} ${rule}}` : '')
- .join('\n');
-
+ const scopedCssText = getScopedCssStylesText(botId, cache.cssStylesText);
return { settings: cache.settings, inlineStyles: cache.inlineStyles, cssStylesText: scopedCssText}
}
@@ -122,11 +118,7 @@ export const processAndFetchThemeConfig = async (botId: string, theme: Theme):
let cssStylesText = "";
const cssStylesResponse = await fetch(cssStylesUrl);
if (cssStylesResponse.ok) {
- const cssRawText = await cssStylesResponse.text();
- cssStylesText = cssRawText
- .split('}')
- .map(rule => rule.trim() ? `#${botId} ${rule}}` : '')
- .join('\n');
+ cssStylesText = await cssStylesResponse.text();
} else {
console.info(`Could not fetch styles.css from ${cssStylesUrl}`);
}
@@ -150,5 +142,36 @@ export const processAndFetchThemeConfig = async (botId: string, theme: Theme):
}
setCachedTheme(id, themeVersion, settings, inlineStyles, cssStylesText);
- return {settings, inlineStyles, cssStylesText};
+
+ // scopes the css styles to isolate between chatbots
+ const scopedCssText = getScopedCssStylesText(botId, cssStylesText);
+ return {settings, inlineStyles, cssStylesText: scopedCssText};
}
+
+/**
+ * Retrieves scoped css styles text.
+ *
+ * @param botId id of bot to scope to
+ * @param cssStylesText css styles text to apply in the scope
+ */
+const getScopedCssStylesText = (botId: string, cssStylesText: string) => {
+ const scopedCssText = cssStylesText.split(/(?<=})/)
+ .map(rule => {
+ const trimmedRule = rule.trim();
+ // ignores comments
+ if (trimmedRule.startsWith('/*')) {
+ return trimmedRule;
+ }
+
+ // ignores imports, keyframes and media queries
+ if (trimmedRule.startsWith('@import') || trimmedRule.startsWith('@keyframes')
+ || trimmedRule.startsWith('@media')) {
+ return trimmedRule;
+ }
+
+ // scopes regular css rules with bot id
+ return trimmedRule ? `#${botId} ${trimmedRule}` : '';
+ })
+ .join('\n');
+ return scopedCssText;
+}
From 80584910387cd928264af26533911a085ff7a5af Mon Sep 17 00:00:00 2001
From: tjtanjin
Date: Sat, 12 Oct 2024 02:42:20 +0800
Subject: [PATCH 10/36] lint: Fix lint issues
---
src/services/ThemeService.ts | 24 ++++++++++++------------
1 file changed, 12 insertions(+), 12 deletions(-)
diff --git a/src/services/ThemeService.ts b/src/services/ThemeService.ts
index af416552..4efb88b6 100644
--- a/src/services/ThemeService.ts
+++ b/src/services/ThemeService.ts
@@ -156,22 +156,22 @@ export const processAndFetchThemeConfig = async (botId: string, theme: Theme):
*/
const getScopedCssStylesText = (botId: string, cssStylesText: string) => {
const scopedCssText = cssStylesText.split(/(?<=})/)
- .map(rule => {
- const trimmedRule = rule.trim();
- // ignores comments
- if (trimmedRule.startsWith('/*')) {
+ .map(rule => {
+ const trimmedRule = rule.trim();
+ // ignores comments
+ if (trimmedRule.startsWith('/*')) {
return trimmedRule;
}
- // ignores imports, keyframes and media queries
- if (trimmedRule.startsWith('@import') || trimmedRule.startsWith('@keyframes')
+ // ignores imports, keyframes and media queries
+ if (trimmedRule.startsWith('@import') || trimmedRule.startsWith('@keyframes')
|| trimmedRule.startsWith('@media')) {
- return trimmedRule;
- }
+ return trimmedRule;
+ }
- // scopes regular css rules with bot id
- return trimmedRule ? `#${botId} ${trimmedRule}` : '';
- })
- .join('\n');
+ // scopes regular css rules with bot id
+ return trimmedRule ? `#${botId} ${trimmedRule}` : '';
+ })
+ .join('\n');
return scopedCssText;
}
From d3cc37396ac0d746fea4a2fd5c9eb23cf087afff Mon Sep 17 00:00:00 2001
From: tjtanjin
Date: Sat, 12 Oct 2024 17:41:37 +0800
Subject: [PATCH 11/36] test: Add initial unit test for utils
---
__tests__/utils/messageBuilder.test.ts | 161 +++++++++++++++++++++++++
src/utils/messageBuilder.ts | 4 +-
2 files changed, 162 insertions(+), 3 deletions(-)
create mode 100644 __tests__/utils/messageBuilder.test.ts
diff --git a/__tests__/utils/messageBuilder.test.ts b/__tests__/utils/messageBuilder.test.ts
new file mode 100644
index 00000000..d92dd0d1
--- /dev/null
+++ b/__tests__/utils/messageBuilder.test.ts
@@ -0,0 +1,161 @@
+import React from "react";
+
+import { createMessage } from "../../src/utils/messageBuilder";
+import { generateSecureUUID } from "../../src/utils/idGenerator";
+
+// mocks id generator
+jest.mock("../../src/utils/idGenerator");
+const mockedGenerateSecureUUID = generateSecureUUID as jest.MockedFunction;
+
+/**
+ * Test for message builder.
+ */
+describe("createMessage", () => {
+ beforeEach(() => {
+ // Reset all mocks before each test
+ jest.clearAllMocks();
+ });
+
+ it("should create a message with string content correctly", () => {
+ // mocks message details
+ const mockId = "mocked-uuid";
+ mockedGenerateSecureUUID.mockReturnValue(mockId);
+ const content = "This is a test message";
+ const sender = "bot";
+
+ // creates message
+ const message = createMessage(content, sender);
+
+ // checks if message is created correctly
+ expect(generateSecureUUID).toHaveBeenCalledTimes(1);
+ expect(message).toEqual({
+ id: mockId,
+ content,
+ sender,
+ type: "string",
+ timestamp: expect.any(String),
+ });
+
+ // checks timestamp format
+ const timestampDate = new Date(message.timestamp);
+ expect(timestampDate.toUTCString()).toBe(message.timestamp);
+ });
+
+ it("should create a message with JSX.Element content correctly", () => {
+ // mocks message details
+ const mockId = "mocked-uuid";
+ mockedGenerateSecureUUID.mockReturnValue(mockId);
+ const content = React.createElement("div");
+ const sender = "user";
+
+ // creates message
+ const message = createMessage(content, sender);
+
+ // checks if message is created correctly
+ expect(generateSecureUUID).toHaveBeenCalledTimes(1);
+ expect(message).toEqual({
+ id: mockId,
+ content,
+ sender,
+ type: "object",
+ timestamp: expect.any(String), // We"ll validate the format separately
+ });
+
+ // checks timestamp format
+ const timestampDate = new Date(message.timestamp);
+ expect(timestampDate.toUTCString()).toBe(message.timestamp);
+ });
+
+ it("should handle empty string content correctly", () => {
+ // mocks message details
+ const mockId = "mocked-uuid";
+ mockedGenerateSecureUUID.mockReturnValue(mockId);
+ const content = "";
+ const sender = "bot";
+
+ // creates message
+ const message = createMessage(content, sender);
+
+ // checks if message is created correctly
+ expect(generateSecureUUID).toHaveBeenCalledTimes(1);
+ expect(message).toEqual({
+ id: mockId,
+ content,
+ sender,
+ type: "string",
+ timestamp: expect.any(String),
+ });
+
+ // checks timestamp format
+ const timestampDate = new Date(message.timestamp);
+ expect(timestampDate.toUTCString()).toBe(message.timestamp);
+ });
+
+ it("should handle special characters in content correctly", () => {
+ // mocks message details
+ const mockId = "mocked-uuid";
+ mockedGenerateSecureUUID.mockReturnValue(mockId);
+ const content = 'Special characters! @#$%^&*()_+-=[]{}|;\':",.<>/?`~';
+ const sender = "user";
+
+ // creates message
+ const message = createMessage(content, sender);
+
+ // checks if message is created correctly
+ expect(generateSecureUUID).toHaveBeenCalledTimes(1);
+ expect(message).toEqual({
+ id: mockId,
+ content,
+ sender,
+ type: "string",
+ timestamp: expect.any(String),
+ });
+
+ // checks timestamp format
+ const timestampDate = new Date(message.timestamp);
+ expect(timestampDate.toUTCString()).toBe(message.timestamp);
+ });
+
+ it("should handle content as a complex JSX.Element correctly", () => {
+ // mocks message details
+ const mockId = "mocked-uuid";
+ mockedGenerateSecureUUID.mockReturnValue(mockId);
+ const content = React.createElement(
+ 'div',
+ { className: 'container' },
+ React.createElement('h1', null, 'Title'),
+ React.createElement(
+ 'p',
+ null,
+ 'This is a paragraph with ',
+ React.createElement('strong', null, 'bold'),
+ ' text.'
+ ),
+ React.createElement(
+ 'ul',
+ null,
+ React.createElement('li', null, 'Item 1'),
+ React.createElement('li', null, 'Item 2'),
+ React.createElement('li', null, 'Item 3')
+ )
+ );
+ const sender = "bot";
+
+ // creates message
+ const message = createMessage(content, sender);
+
+ // checks if message is created correctly
+ expect(generateSecureUUID).toHaveBeenCalledTimes(1);
+ expect(message).toEqual({
+ id: mockId,
+ content,
+ sender,
+ type: "object",
+ timestamp: expect.any(String),
+ });
+
+ // checks timestamp format
+ const timestampDate = new Date(message.timestamp);
+ expect(timestampDate.toUTCString()).toBe(message.timestamp);
+ });
+});
diff --git a/src/utils/messageBuilder.ts b/src/utils/messageBuilder.ts
index d3caff3f..d50c4807 100644
--- a/src/utils/messageBuilder.ts
+++ b/src/utils/messageBuilder.ts
@@ -1,5 +1,3 @@
-import { isValidElement } from "react";
-
import { generateSecureUUID } from "./idGenerator";
/**
@@ -13,7 +11,7 @@ export const createMessage = (content: string | JSX.Element, sender: string, ) =
id: generateSecureUUID(),
content,
sender,
- type: isValidElement(content) ? "object" : "string",
+ type: typeof content === "string" ? "string" : "object",
timestamp: new Date().toUTCString()
};
}
\ No newline at end of file
From c8631f9d19e4442e4ec022f7d66836625116eb7c Mon Sep 17 00:00:00 2001
From: Ya Min Mo Mo
Date: Sat, 12 Oct 2024 14:15:06 -0400
Subject: [PATCH 12/36] fix: textArea is disabled after chatHistory is loaded
(#180)
---
.../internal/useChatHistoryInternal.test.ts | 55 +++++++++----------
src/hooks/internal/useChatHistoryInternal.ts | 8 +--
src/services/ChatHistoryService.tsx | 4 +-
3 files changed, 29 insertions(+), 38 deletions(-)
diff --git a/__tests__/hooks/internal/useChatHistoryInternal.test.ts b/__tests__/hooks/internal/useChatHistoryInternal.test.ts
index 61e33f84..3bace45c 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/src/hooks/internal/useChatHistoryInternal.ts b/src/hooks/internal/useChatHistoryInternal.ts
index 7101eff2..da3022b9 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 bba21124..6c39834e 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>, prevTextAreaDisabled: boolean,
- setTextAreaDisabled: Dispatch>) => {
+ setMessages: Dispatch>) => {
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)
From 8957b06205f5ad1894d515c6233a78b621fd7bdb Mon Sep 17 00:00:00 2001
From: tjtanjin
Date: Sun, 13 Oct 2024 02:39:56 +0800
Subject: [PATCH 13/36] refactor: Standardize keyframes naming convention
---
.../Buttons/VoiceButton/VoiceButton.css | 13 +-
src/components/ChatBotBody/ChatBotBody.css | 40 +-----
.../ChatMessagePrompt/ChatMessagePrompt.css | 62 ++++-----
.../ChatBotBody/MediaDisplay/MediaDisplay.css | 13 +-
.../ChatBotBody/ToastPrompt/ToastPrompt.css | 43 +++---
.../ChatBotBody/ToastPrompt/ToastPrompt.tsx | 2 +-
.../UserCheckboxes/UserCheckboxes.css | 15 +--
.../ChatBotBody/UserOptions/UserOptions.css | 13 +-
.../ChatBotButton/ChatBotButton.css | 26 +---
src/components/ChatBotContainer.css | 122 +++++++++++++++++-
.../ChatBotTooltip/ChatBotTooltip.css | 26 +---
.../LoadingSpinner/LoadingSpinner.css | 11 +-
12 files changed, 171 insertions(+), 215 deletions(-)
diff --git a/src/components/Buttons/VoiceButton/VoiceButton.css b/src/components/Buttons/VoiceButton/VoiceButton.css
index 0df02ac2..3208bacf 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 93afed80..40f8fcf8 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 8e7bcd02..cacaef27 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 a1629ac0..31045cb4 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 219e6506..c351f773 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 c8ca5dd2..21d1b122 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}