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}
diff --git a/src/components/ChatBotBody/UserCheckboxes/UserCheckboxes.css b/src/components/ChatBotBody/UserCheckboxes/UserCheckboxes.css index af6b42c1..8e6554e6 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 64060af8..4984dd50 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 e73ca285..6a294215 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 ad4e66f4..05df8b94 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 3cecd7ab..f2d4733f 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 4938a3f8..7b23a870 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 From c35b8b2f03f0bab661941391db5261beedc2f6f4 Mon Sep 17 00:00:00 2001 From: Pratik Padalkar <62217310+Pratik00019@users.noreply.github.com> Date: Sun, 13 Oct 2024 00:25:09 +0530 Subject: [PATCH 14/36] Wrote test cases for FunctionProcessor (#181) --- __tests__/services/FunctionProcessor.test.ts | 60 ++++++++++++++++++++ 1 file changed, 60 insertions(+) create mode 100644 __tests__/services/FunctionProcessor.test.ts diff --git a/__tests__/services/FunctionProcessor.test.ts b/__tests__/services/FunctionProcessor.test.ts new file mode 100644 index 00000000..586f6e05 --- /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'); + }); +}); From ce8b4580e7fa63260a38b8f69449dddb9d3c76a1 Mon Sep 17 00:00:00 2001 From: Andrey Kishtov Date: Sat, 12 Oct 2024 21:56:51 +0300 Subject: [PATCH 15/36] [Task] Add unit test cases for CheckboxProcessor #175 (#200) Co-authored-by: Andrey Kishtov --- .../BlockService/CheckboxProcessor.test.tsx | 88 +++++++++++++++++++ 1 file changed, 88 insertions(+) create mode 100644 __tests__/services/BlockService/CheckboxProcessor.test.tsx diff --git a/__tests__/services/BlockService/CheckboxProcessor.test.tsx b/__tests__/services/BlockService/CheckboxProcessor.test.tsx new file mode 100644 index 00000000..86e1e349 --- /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(); + }); +}); From dd1b08dd7fa21f5db924f52147b9883fe077ee5c Mon Sep 17 00:00:00 2001 From: Liudmyla Savonik Date: Sat, 12 Oct 2024 21:57:36 +0300 Subject: [PATCH 16/36] [Task] Add unit test cases for OptionProcessor #185 (#201) --- .../BlockService/OptionProcessor.test.tsx | 89 +++++++++++++++++++ 1 file changed, 89 insertions(+) create mode 100644 __tests__/services/BlockService/OptionProcessor.test.tsx diff --git a/__tests__/services/BlockService/OptionProcessor.test.tsx b/__tests__/services/BlockService/OptionProcessor.test.tsx new file mode 100644 index 00000000..c95f550c --- /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( + + ); + }); + + 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( + + ); + }); + + 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( + + ); + }); + + 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( + + ); + }); + + 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(); + }); +}); From 442389ad36dd486d80f06673dc4c8e39b1458e78 Mon Sep 17 00:00:00 2001 From: tjtanjin Date: Sun, 13 Oct 2024 02:53:28 +0800 Subject: [PATCH 17/36] fix: Fix tooltip text color style --- src/components/ChatBotTooltip/ChatBotTooltip.tsx | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/components/ChatBotTooltip/ChatBotTooltip.tsx b/src/components/ChatBotTooltip/ChatBotTooltip.tsx index 28d2cc99..d06af401 100644 --- a/src/components/ChatBotTooltip/ChatBotTooltip.tsx +++ b/src/components/ChatBotTooltip/ChatBotTooltip.tsx @@ -74,7 +74,7 @@ const ChatBotTooltip = () => { right: (styles.chatButtonStyle?.width as number ?? 75) + 40, bottom: 30, backgroundColor: settings.general?.secondaryColor, - color: settings.general?.secondaryColor, + color: "#fff", ...styles.tooltipStyle }; @@ -92,7 +92,7 @@ const ChatBotTooltip = () => { className={`rcb-chat-tooltip ${showTooltip ? "rcb-tooltip-show" : "rcb-tooltip-hide"}`} onClick={() => openChat(true)} > - {settings.tooltip?.text} + {settings.tooltip?.text}
} From 6531b92d4fe468abb6ba38c77375b0605afbddcc Mon Sep 17 00:00:00 2001 From: tjtanjin Date: Sun, 13 Oct 2024 15:35:41 +0800 Subject: [PATCH 18/36] feat: Add svg component support for audio icon --- src/assets/audio_icon.svg | 2 +- src/assets/audio_icon_disabled.svg | 1 + .../Buttons/AudioButton/AudioButton.tsx | 26 ++++++++++++++++--- src/constants/internal/DefaultSettings.tsx | 4 ++- src/types/Settings.ts | 3 ++- types/image.d.ts | 8 +++++- 6 files changed, 36 insertions(+), 8 deletions(-) create mode 100644 src/assets/audio_icon_disabled.svg diff --git a/src/assets/audio_icon.svg b/src/assets/audio_icon.svg index 7c4f57df..68b524c1 100644 --- a/src/assets/audio_icon.svg +++ b/src/assets/audio_icon.svg @@ -1 +1 @@ - \ No newline at end of file + \ No newline at end of file diff --git a/src/assets/audio_icon_disabled.svg b/src/assets/audio_icon_disabled.svg new file mode 100644 index 00000000..885a3cf3 --- /dev/null +++ b/src/assets/audio_icon_disabled.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/src/components/Buttons/AudioButton/AudioButton.tsx b/src/components/Buttons/AudioButton/AudioButton.tsx index 55bf0f15..f2f04afb 100644 --- a/src/components/Buttons/AudioButton/AudioButton.tsx +++ b/src/components/Buttons/AudioButton/AudioButton.tsx @@ -32,6 +32,27 @@ const AudioButton = () => { ...styles.audioIconDisabledStyle }; + /** + * Renders button depending on whether an svg component or image url is provided. + */ + const renderButton = () => { + const IconComponent = audioToggledOn ? settings.audio?.icon : settings.audio?.iconDisabled; + if (typeof IconComponent === "string") { + return ( + + ) + } + return ( + IconComponent && + + + + ) + } + return (
{ : {...styles.audioButtonStyle, ...styles.audioButtonDisabledStyle} } > - + {renderButton()}
); }; diff --git a/src/constants/internal/DefaultSettings.tsx b/src/constants/internal/DefaultSettings.tsx index c0f0c38d..4aaeace3 100644 --- a/src/constants/internal/DefaultSettings.tsx +++ b/src/constants/internal/DefaultSettings.tsx @@ -11,7 +11,8 @@ import closeChatIcon from "../../assets/close_chat_icon.svg"; import sendButtonIcon from "../../assets/send_icon.svg"; import voiceIcon from "../../assets/voice_icon.svg"; import emojiIcon from "../../assets/emoji_icon.svg"; -import audioIcon from "../../assets/audio_icon.svg"; +import { ReactComponent as audioIcon } from '../../assets/audio_icon.svg'; +import { ReactComponent as audioIconDisabled } from '../../assets/audio_icon_disabled.svg'; import notificationSound from "../../assets/notification_sound.wav"; // default settings provided to the bot @@ -67,6 +68,7 @@ export const DefaultSettings: Settings = { rate: 1, volume: 1, icon: audioIcon, + iconDisabled: audioIconDisabled, }, chatHistory: { disabled: false, diff --git a/src/types/Settings.ts b/src/types/Settings.ts index af5ccb27..81df5400 100644 --- a/src/types/Settings.ts +++ b/src/types/Settings.ts @@ -45,7 +45,8 @@ export type Settings = { voiceNames?: string[]; rate?: number; volume?: number; - icon?: string; + icon?: string | React.FC>; + iconDisabled?: string | React.FC>; }, chatHistory?: { disabled?: boolean; diff --git a/types/image.d.ts b/types/image.d.ts index eafef247..31512c97 100644 --- a/types/image.d.ts +++ b/types/image.d.ts @@ -1,2 +1,8 @@ declare module '*.png'; -declare module '*.svg'; \ No newline at end of file + +declare module '*.svg' { + import React = require('react'); + export const ReactComponent: React.FC>; + const src: string; + export default src; +} \ No newline at end of file From 109c02ce29d408ae0f7c3624a0ee118ca477ff8b Mon Sep 17 00:00:00 2001 From: tjtanjin Date: Sun, 13 Oct 2024 15:56:18 +0800 Subject: [PATCH 19/36] feat: Add svg component support for voice icon --- src/assets/audio_icon.svg | 2 +- src/assets/audio_icon_disabled.svg | 2 +- src/assets/voice_icon.svg | 2 +- src/assets/voice_icon_disabled.svg | 1 + .../Buttons/AudioButton/AudioButton.tsx | 2 ++ .../Buttons/VoiceButton/VoiceButton.css | 4 --- .../Buttons/VoiceButton/VoiceButton.tsx | 28 ++++++++++++++++--- src/constants/internal/DefaultSettings.tsx | 4 ++- src/types/Settings.ts | 3 +- 9 files changed, 35 insertions(+), 13 deletions(-) create mode 100644 src/assets/voice_icon_disabled.svg diff --git a/src/assets/audio_icon.svg b/src/assets/audio_icon.svg index 68b524c1..66af5718 100644 --- a/src/assets/audio_icon.svg +++ b/src/assets/audio_icon.svg @@ -1 +1 @@ - \ No newline at end of file + \ No newline at end of file diff --git a/src/assets/audio_icon_disabled.svg b/src/assets/audio_icon_disabled.svg index 885a3cf3..dd72a2c8 100644 --- a/src/assets/audio_icon_disabled.svg +++ b/src/assets/audio_icon_disabled.svg @@ -1 +1 @@ - \ No newline at end of file + \ No newline at end of file diff --git a/src/assets/voice_icon.svg b/src/assets/voice_icon.svg index cdaa6e41..ff456200 100644 --- a/src/assets/voice_icon.svg +++ b/src/assets/voice_icon.svg @@ -1 +1 @@ - \ No newline at end of file + \ No newline at end of file diff --git a/src/assets/voice_icon_disabled.svg b/src/assets/voice_icon_disabled.svg new file mode 100644 index 00000000..58b67c62 --- /dev/null +++ b/src/assets/voice_icon_disabled.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/src/components/Buttons/AudioButton/AudioButton.tsx b/src/components/Buttons/AudioButton/AudioButton.tsx index f2f04afb..5c9dafa5 100644 --- a/src/components/Buttons/AudioButton/AudioButton.tsx +++ b/src/components/Buttons/AudioButton/AudioButton.tsx @@ -22,12 +22,14 @@ const AudioButton = () => { // styles for audio icon const audioIconStyle: React.CSSProperties = { backgroundImage: `url(${settings.audio?.icon})`, + fill: "#e8eaed", ...styles.audioIconStyle }; // styles for audio disabled icon const audioIconDisabledStyle: React.CSSProperties = { backgroundImage: `url(${settings.audio?.icon})`, + fill: "#e8eaed", ...styles.audioIconStyle, // by default inherit the base style ...styles.audioIconDisabledStyle }; diff --git a/src/components/Buttons/VoiceButton/VoiceButton.css b/src/components/Buttons/VoiceButton/VoiceButton.css index 3208bacf..83406480 100644 --- a/src/components/Buttons/VoiceButton/VoiceButton.css +++ b/src/components/Buttons/VoiceButton/VoiceButton.css @@ -50,8 +50,4 @@ .rcb-voice-icon-on { animation: rcb-animation-ping 1s infinite; -} - -.rcb-voice-icon-off { - filter: grayscale(100%); } \ No newline at end of file diff --git a/src/components/Buttons/VoiceButton/VoiceButton.tsx b/src/components/Buttons/VoiceButton/VoiceButton.tsx index ab3aff59..e77eff42 100644 --- a/src/components/Buttons/VoiceButton/VoiceButton.tsx +++ b/src/components/Buttons/VoiceButton/VoiceButton.tsx @@ -66,12 +66,14 @@ const VoiceButton = () => { // styles for voice icon const voiceIconStyle: React.CSSProperties = { backgroundImage: `url(${settings.voice?.icon})`, + fill: "#9aa0a6", ...styles.voiceIconStyle }; // styles for voice disabled icon const voiceIconDisabledStyle: React.CSSProperties = { backgroundImage: `url(${settings.voice?.icon})`, + fill: "#9aa0a6", ...styles.voiceIconStyle, // by default inherit the base style ...styles.voiceIconDisabledStyle }; @@ -99,6 +101,27 @@ const VoiceButton = () => { fileUrl={fileDetails.fileUrl}/>, "user"); } + /** + * Renders button depending on whether an svg component or image url is provided. + */ + const renderButton = () => { + const IconComponent = voiceToggledOn ? settings.voice?.icon : settings.voice?.iconDisabled; + if (typeof IconComponent === "string") { + return ( + + ) + } + return ( + IconComponent && + + + + ) + } + return (
{ } className={voiceToggledOn && !textAreaDisabled ? "rcb-voice-button-enabled" : "rcb-voice-button-disabled"} > - + {renderButton()}
); }; diff --git a/src/constants/internal/DefaultSettings.tsx b/src/constants/internal/DefaultSettings.tsx index 4aaeace3..087567dc 100644 --- a/src/constants/internal/DefaultSettings.tsx +++ b/src/constants/internal/DefaultSettings.tsx @@ -9,7 +9,8 @@ import fileAttachmentIcon from "../../assets/file_attachment_icon.svg"; import notificationIcon from "../../assets/notification_icon.svg"; import closeChatIcon from "../../assets/close_chat_icon.svg"; import sendButtonIcon from "../../assets/send_icon.svg"; -import voiceIcon from "../../assets/voice_icon.svg"; +import { ReactComponent as voiceIcon } from '../../assets/voice_icon.svg'; +import { ReactComponent as voiceIconDisabled } from '../../assets/voice_icon_disabled.svg'; import emojiIcon from "../../assets/emoji_icon.svg"; import { ReactComponent as audioIcon } from '../../assets/audio_icon.svg'; import { ReactComponent as audioIconDisabled } from '../../assets/audio_icon_disabled.svg'; @@ -131,6 +132,7 @@ export const DefaultSettings: Settings = { autoSendPeriod: 1000, sendAsAudio: false, icon: voiceIcon, + iconDisabled: voiceIconDisabled, }, footer: { text: ( diff --git a/src/types/Settings.ts b/src/types/Settings.ts index 81df5400..3ebb8c70 100644 --- a/src/types/Settings.ts +++ b/src/types/Settings.ts @@ -108,7 +108,8 @@ export type Settings = { autoSendDisabled?: boolean; autoSendPeriod?: number; sendAsAudio?: boolean; - icon?: string; + icon?: string | React.FC>; + iconDisabled?: string | React.FC>; }, footer?: { text?: string | JSX.Element; From 1df0b97c45e47a1eedd260a3e35f51751565274e Mon Sep 17 00:00:00 2001 From: tjtanjin Date: Sun, 13 Oct 2024 16:04:28 +0800 Subject: [PATCH 20/36] feat: Add svg component support for notification icon --- src/assets/notification_icon.svg | 2 +- src/assets/notification_icon_disabled.svg | 1 + .../NotificationButton/NotificationButton.tsx | 32 +++++++++++++++---- src/constants/internal/DefaultSettings.tsx | 4 ++- src/types/Settings.ts | 3 +- 5 files changed, 33 insertions(+), 9 deletions(-) create mode 100644 src/assets/notification_icon_disabled.svg diff --git a/src/assets/notification_icon.svg b/src/assets/notification_icon.svg index b38f705d..4fa90de5 100644 --- a/src/assets/notification_icon.svg +++ b/src/assets/notification_icon.svg @@ -1 +1 @@ - \ No newline at end of file + \ No newline at end of file diff --git a/src/assets/notification_icon_disabled.svg b/src/assets/notification_icon_disabled.svg new file mode 100644 index 00000000..bfc8e45a --- /dev/null +++ b/src/assets/notification_icon_disabled.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/src/components/Buttons/NotificationButton/NotificationButton.tsx b/src/components/Buttons/NotificationButton/NotificationButton.tsx index c27cda55..260a5e03 100644 --- a/src/components/Buttons/NotificationButton/NotificationButton.tsx +++ b/src/components/Buttons/NotificationButton/NotificationButton.tsx @@ -22,16 +22,41 @@ const NotificationButton = () => { // styles for notification icon const notificationIconStyle: React.CSSProperties = { backgroundImage: `url(${settings.notification?.icon})`, + fill: "#e8eaed", ...styles.notificationIconStyle }; // styles for notification disabled icon const notificationIconDisabledStyle: React.CSSProperties = { backgroundImage: `url(${settings.notification?.icon})`, + fill: "#e8eaed", ...styles.notificationIconStyle, // by default inherit the base style ...styles.notificationIconDisabledStyle }; + /** + * Renders button depending on whether an svg component or image url is provided. + */ + const renderButton = () => { + const IconComponent = notificationsToggledOn + ? settings.notification?.icon + : settings.notification?.iconDisabled; + if (typeof IconComponent === "string") { + return ( + + ) + } + return ( + IconComponent && + + + + ) + } + return (
{ : {...styles.notificationButtonStyle, ...styles.notificationButtonDisabledStyle} } > - + {renderButton()}
); }; diff --git a/src/constants/internal/DefaultSettings.tsx b/src/constants/internal/DefaultSettings.tsx index 087567dc..88ad2a9b 100644 --- a/src/constants/internal/DefaultSettings.tsx +++ b/src/constants/internal/DefaultSettings.tsx @@ -6,7 +6,8 @@ import botAvatar from "../../assets/bot_avatar.svg"; import userAvatar from "../../assets/user_avatar.svg"; import chatIcon from "../../assets/chat_icon.svg"; import fileAttachmentIcon from "../../assets/file_attachment_icon.svg"; -import notificationIcon from "../../assets/notification_icon.svg"; +import { ReactComponent as notificationIcon } from '../../assets/notification_icon.svg'; +import { ReactComponent as notificationIconDisabled } from '../../assets/notification_icon_disabled.svg'; import closeChatIcon from "../../assets/close_chat_icon.svg"; import sendButtonIcon from "../../assets/send_icon.svg"; import { ReactComponent as voiceIcon } from '../../assets/voice_icon.svg'; @@ -58,6 +59,7 @@ export const DefaultSettings: Settings = { defaultToggledOn: true, volume: 0.2, icon: notificationIcon, + iconDisabled: notificationIconDisabled, sound: notificationSound, showCount: true, }, diff --git a/src/types/Settings.ts b/src/types/Settings.ts index 3ebb8c70..d8049fd4 100644 --- a/src/types/Settings.ts +++ b/src/types/Settings.ts @@ -34,7 +34,8 @@ export type Settings = { disabled?: boolean; defaultToggledOn?: boolean; volume?: number; - icon?: string; + icon?: string | React.FC>; + iconDisabled?: string | React.FC>; sound?: string; showCount?: boolean; }, From 01cf5c4404840747c5810ff25fdfac94e8b0f989 Mon Sep 17 00:00:00 2001 From: tjtanjin Date: Sun, 13 Oct 2024 16:15:00 +0800 Subject: [PATCH 21/36] chore: Minor css cleanup for buttons --- src/components/Buttons/AudioButton/AudioButton.css | 13 +++---------- src/components/Buttons/AudioButton/AudioButton.tsx | 4 ++-- .../NotificationButton/NotificationButton.css | 13 +++---------- .../NotificationButton/NotificationButton.tsx | 4 ++-- src/components/Buttons/VoiceButton/VoiceButton.css | 5 ++--- src/components/Buttons/VoiceButton/VoiceButton.tsx | 4 ++-- 6 files changed, 14 insertions(+), 29 deletions(-) diff --git a/src/components/Buttons/AudioButton/AudioButton.css b/src/components/Buttons/AudioButton/AudioButton.css index 80307da2..0bff0154 100644 --- a/src/components/Buttons/AudioButton/AudioButton.css +++ b/src/components/Buttons/AudioButton/AudioButton.css @@ -1,7 +1,6 @@ /* Audio Icon */ -.rcb-audio-icon-on, -.rcb-audio-icon-off { +.rcb-audio-icon { position: relative; display: inline-block; background-size: cover; @@ -12,12 +11,7 @@ margin-left: 5px; } -.rcb-audio-icon-off { - filter: grayscale(100%); -} - -.rcb-audio-icon-on::after, -.rcb-audio-icon-off::after { +.rcb-audio-icon::after { content: ""; position: absolute; top: 50%; @@ -31,8 +25,7 @@ transition: width 0.2s ease-out, height 0.2s ease-out, opacity 0.2s ease-out; } -.rcb-audio-icon-on:hover::after, -.rcb-audio-icon-off:hover::after { +.rcb-audio-icon:hover::after { width: 130%; height: 130%; opacity: 1; diff --git a/src/components/Buttons/AudioButton/AudioButton.tsx b/src/components/Buttons/AudioButton/AudioButton.tsx index 5c9dafa5..a122398f 100644 --- a/src/components/Buttons/AudioButton/AudioButton.tsx +++ b/src/components/Buttons/AudioButton/AudioButton.tsx @@ -42,14 +42,14 @@ const AudioButton = () => { if (typeof IconComponent === "string") { return ( ) } return ( IconComponent && - + ) diff --git a/src/components/Buttons/NotificationButton/NotificationButton.css b/src/components/Buttons/NotificationButton/NotificationButton.css index 77493609..f21caad2 100644 --- a/src/components/Buttons/NotificationButton/NotificationButton.css +++ b/src/components/Buttons/NotificationButton/NotificationButton.css @@ -1,7 +1,6 @@ /* Notification Icon */ -.rcb-notification-icon-on, -.rcb-notification-icon-off { +.rcb-notification-icon { position: relative; display: inline-block; background-size: cover; @@ -12,12 +11,7 @@ margin-left: 5px; } -.rcb-notification-icon-off { - filter: grayscale(100%); -} - -.rcb-notification-icon-on::after, -.rcb-notification-icon-off::after { +.rcb-notification-icon::after { content: ""; position: absolute; top: 50%; @@ -31,8 +25,7 @@ transition: width 0.2s ease-out, height 0.2s ease-out, opacity 0.2s ease-out; } -.rcb-notification-icon-on:hover::after, -.rcb-notification-icon-off:hover::after { +.rcb-notification-icon:hover::after { width: 130%; height: 130%; opacity: 1; diff --git a/src/components/Buttons/NotificationButton/NotificationButton.tsx b/src/components/Buttons/NotificationButton/NotificationButton.tsx index 260a5e03..eed89fbe 100644 --- a/src/components/Buttons/NotificationButton/NotificationButton.tsx +++ b/src/components/Buttons/NotificationButton/NotificationButton.tsx @@ -44,14 +44,14 @@ const NotificationButton = () => { if (typeof IconComponent === "string") { return ( ) } return ( IconComponent && - + ) diff --git a/src/components/Buttons/VoiceButton/VoiceButton.css b/src/components/Buttons/VoiceButton/VoiceButton.css index 83406480..553f5dc0 100644 --- a/src/components/Buttons/VoiceButton/VoiceButton.css +++ b/src/components/Buttons/VoiceButton/VoiceButton.css @@ -37,8 +37,7 @@ /* Voice Icon */ -.rcb-voice-icon-on, -.rcb-voice-icon-off { +.rcb-voice-icon { width: 60%; height: 60%; background-size: cover; @@ -48,6 +47,6 @@ background-position: center; } -.rcb-voice-icon-on { +.rcb-voice-icon.on { animation: rcb-animation-ping 1s infinite; } \ No newline at end of file diff --git a/src/components/Buttons/VoiceButton/VoiceButton.tsx b/src/components/Buttons/VoiceButton/VoiceButton.tsx index e77eff42..771b4323 100644 --- a/src/components/Buttons/VoiceButton/VoiceButton.tsx +++ b/src/components/Buttons/VoiceButton/VoiceButton.tsx @@ -109,14 +109,14 @@ const VoiceButton = () => { if (typeof IconComponent === "string") { return ( ) } return ( IconComponent && - + ) From 1dcfa53427dba56d08f174d88f2bdcc7b469724f Mon Sep 17 00:00:00 2001 From: tjtanjin Date: Sun, 13 Oct 2024 16:20:29 +0800 Subject: [PATCH 22/36] chore: Keep existing button colors for audio and notification --- src/components/Buttons/AudioButton/AudioButton.tsx | 2 +- .../Buttons/NotificationButton/NotificationButton.tsx | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/src/components/Buttons/AudioButton/AudioButton.tsx b/src/components/Buttons/AudioButton/AudioButton.tsx index a122398f..b4818a06 100644 --- a/src/components/Buttons/AudioButton/AudioButton.tsx +++ b/src/components/Buttons/AudioButton/AudioButton.tsx @@ -22,7 +22,7 @@ const AudioButton = () => { // styles for audio icon const audioIconStyle: React.CSSProperties = { backgroundImage: `url(${settings.audio?.icon})`, - fill: "#e8eaed", + fill: "#fcec3d", ...styles.audioIconStyle }; diff --git a/src/components/Buttons/NotificationButton/NotificationButton.tsx b/src/components/Buttons/NotificationButton/NotificationButton.tsx index eed89fbe..84f3a524 100644 --- a/src/components/Buttons/NotificationButton/NotificationButton.tsx +++ b/src/components/Buttons/NotificationButton/NotificationButton.tsx @@ -22,7 +22,7 @@ const NotificationButton = () => { // styles for notification icon const notificationIconStyle: React.CSSProperties = { backgroundImage: `url(${settings.notification?.icon})`, - fill: "#e8eaed", + fill: "#fcec3d", ...styles.notificationIconStyle }; From d8523133478b89895ca6ce993223bbc74d8a80a0 Mon Sep 17 00:00:00 2001 From: tjtanjin Date: Sun, 13 Oct 2024 16:30:03 +0800 Subject: [PATCH 23/36] feat: Add svg component support for emoji icon --- src/assets/emoji_icon.svg | 2 +- .../Buttons/EmojiButton/EmojiButton.tsx | 28 ++++++++++++++++--- src/constants/internal/DefaultSettings.tsx | 3 +- src/types/Settings.ts | 5 ++-- 4 files changed, 30 insertions(+), 8 deletions(-) diff --git a/src/assets/emoji_icon.svg b/src/assets/emoji_icon.svg index c2ab28a6..3316c553 100644 --- a/src/assets/emoji_icon.svg +++ b/src/assets/emoji_icon.svg @@ -1 +1 @@ - \ No newline at end of file + \ No newline at end of file diff --git a/src/components/Buttons/EmojiButton/EmojiButton.tsx b/src/components/Buttons/EmojiButton/EmojiButton.tsx index 21f8f6b0..e38d5040 100644 --- a/src/components/Buttons/EmojiButton/EmojiButton.tsx +++ b/src/components/Buttons/EmojiButton/EmojiButton.tsx @@ -43,12 +43,14 @@ const EmojiButton = () => { // styles for emoji icon const emojiIconStyle: React.CSSProperties = { backgroundImage: `url(${settings.emoji?.icon})`, + fill: "#a6a6a6", ...styles.emojiIconStyle }; // styles for emoji disabled icon const emojiIconDisabledStyle: React.CSSProperties = { backgroundImage: `url(${settings.emoji?.icon})`, + fill: "#a6a6a6", ...styles.emojiIconStyle, // by default inherit the base style ...styles.emojiIconDisabledStyle }; @@ -127,6 +129,27 @@ const EmojiButton = () => { } }; + /** + * Renders button depending on whether an svg component or image url is provided. + */ + const renderButton = () => { + const IconComponent = textAreaDisabled ? settings.emoji?.iconDisabled : settings.emoji?.icon; + if (typeof IconComponent === "string") { + return ( + + ) + } + return ( + IconComponent && + + + + ) + } + return ( <>
{ style={textAreaDisabled ? emojiButtonDisabledStyle : styles.emojiButtonStyle} onMouseDown={togglePopup} > - + {renderButton()}
{showPopup && (
diff --git a/src/constants/internal/DefaultSettings.tsx b/src/constants/internal/DefaultSettings.tsx index 88ad2a9b..5391c133 100644 --- a/src/constants/internal/DefaultSettings.tsx +++ b/src/constants/internal/DefaultSettings.tsx @@ -12,7 +12,7 @@ import closeChatIcon from "../../assets/close_chat_icon.svg"; import sendButtonIcon from "../../assets/send_icon.svg"; import { ReactComponent as voiceIcon } from '../../assets/voice_icon.svg'; import { ReactComponent as voiceIconDisabled } from '../../assets/voice_icon_disabled.svg'; -import emojiIcon from "../../assets/emoji_icon.svg"; +import { ReactComponent as emojiIcon } from '../../assets/emoji_icon.svg'; import { ReactComponent as audioIcon } from '../../assets/audio_icon.svg'; import { ReactComponent as audioIconDisabled } from '../../assets/audio_icon_disabled.svg'; import notificationSound from "../../assets/notification_sound.wav"; @@ -164,6 +164,7 @@ export const DefaultSettings: Settings = { emoji: { disabled: false, icon: emojiIcon, + iconDisabled: emojiIcon, list: ["😀", "😃", "😄", "😅", "😊", "😌", "😇", "🙃", "🤣", "😍", "🥰", "🥳", "🎉", "🎈", "🚀", "⭐️"] }, toast: { diff --git a/src/types/Settings.ts b/src/types/Settings.ts index d8049fd4..9ba3dbc3 100644 --- a/src/types/Settings.ts +++ b/src/types/Settings.ts @@ -21,7 +21,7 @@ export type Settings = { text?: string; }, chatButton?: { - icon: string; + icon?: string; }, header?: { title?: string | JSX.Element; @@ -126,7 +126,8 @@ export type Settings = { } emoji?: { disabled?: boolean; - icon?: string; + icon?: string | React.FC>; + iconDisabled?: string | React.FC>; list?: string[] ; }, toast?: { From 1b16354fece29e26b5804142b3ba706af3dab114 Mon Sep 17 00:00:00 2001 From: tjtanjin Date: Sun, 13 Oct 2024 16:42:29 +0800 Subject: [PATCH 24/36] feat: Add svg component support for file attachment icon --- src/assets/file_attachment_icon.svg | 2 +- .../Buttons/EmojiButton/EmojiButton.css | 2 +- .../FileAttachmentButton.css | 2 +- .../FileAttachmentButton.tsx | 81 +++++++++++-------- src/constants/internal/DefaultSettings.tsx | 3 +- src/types/Settings.ts | 3 +- 6 files changed, 54 insertions(+), 39 deletions(-) diff --git a/src/assets/file_attachment_icon.svg b/src/assets/file_attachment_icon.svg index 27210be2..b6e05498 100644 --- a/src/assets/file_attachment_icon.svg +++ b/src/assets/file_attachment_icon.svg @@ -1 +1 @@ - \ No newline at end of file + \ No newline at end of file diff --git a/src/components/Buttons/EmojiButton/EmojiButton.css b/src/components/Buttons/EmojiButton/EmojiButton.css index 15a27fd2..0f83845a 100644 --- a/src/components/Buttons/EmojiButton/EmojiButton.css +++ b/src/components/Buttons/EmojiButton/EmojiButton.css @@ -31,7 +31,7 @@ } .rcb-emoji-icon-disabled { - opacity: 0.4; + opacity: 0.5; } .rcb-emoji-button-enabled::after { diff --git a/src/components/Buttons/FileAttachmentButton/FileAttachmentButton.css b/src/components/Buttons/FileAttachmentButton/FileAttachmentButton.css index 4760611a..d707466a 100644 --- a/src/components/Buttons/FileAttachmentButton/FileAttachmentButton.css +++ b/src/components/Buttons/FileAttachmentButton/FileAttachmentButton.css @@ -24,7 +24,7 @@ } .rcb-attach-button-disabled { - opacity: 0.4; + opacity: 0.5; } .rcb-attach-button-enabled::after { diff --git a/src/components/Buttons/FileAttachmentButton/FileAttachmentButton.tsx b/src/components/Buttons/FileAttachmentButton/FileAttachmentButton.tsx index d4487bae..cf0fb243 100644 --- a/src/components/Buttons/FileAttachmentButton/FileAttachmentButton.tsx +++ b/src/components/Buttons/FileAttachmentButton/FileAttachmentButton.tsx @@ -62,12 +62,14 @@ const FileAttachmentButton = () => { // styles for file attachment icon const fileAttachmentIconStyle: React.CSSProperties = { backgroundImage: `url(${settings.fileAttachment?.icon})`, + fill: "#a6a6a6", ...styles.fileAttachmentIconStyle }; // styles for file attachment disabled icon const fileAttachmentIconDisabledStyle: React.CSSProperties = { backgroundImage: `url(${settings.fileAttachment?.icon})`, + fill: "#a6a6a6", ...styles.fileAttachmentIconStyle, // by default inherit the base style ...styles.fileAttachmentIconDisabledStyle }; @@ -127,41 +129,52 @@ const FileAttachmentButton = () => { } }; + /** + * Renders button depending on whether an svg component or image url is provided. + */ + const renderButton = () => { + const IconComponent = blockAllowsAttachment + ? settings.fileAttachment?.icon + : settings.fileAttachment?.iconDisabled; + if (typeof IconComponent === "string") { + return ( + + ) + } + return ( + IconComponent && + + + + ) + } + return ( - <> - {blockAllowsAttachment ? ( - - ) : ( - - )} - + ); }; diff --git a/src/constants/internal/DefaultSettings.tsx b/src/constants/internal/DefaultSettings.tsx index 5391c133..a14787fc 100644 --- a/src/constants/internal/DefaultSettings.tsx +++ b/src/constants/internal/DefaultSettings.tsx @@ -5,7 +5,7 @@ import actionDisabledIcon from "../../assets/action_disabled_icon.svg"; import botAvatar from "../../assets/bot_avatar.svg"; import userAvatar from "../../assets/user_avatar.svg"; import chatIcon from "../../assets/chat_icon.svg"; -import fileAttachmentIcon from "../../assets/file_attachment_icon.svg"; +import { ReactComponent as fileAttachmentIcon } from '../../assets/file_attachment_icon.svg'; import { ReactComponent as notificationIcon } from '../../assets/notification_icon.svg'; import { ReactComponent as notificationIconDisabled } from '../../assets/notification_icon_disabled.svg'; import closeChatIcon from "../../assets/close_chat_icon.svg"; @@ -158,6 +158,7 @@ export const DefaultSettings: Settings = { multiple: true, accept: ".png", icon: fileAttachmentIcon, + iconDisabled: fileAttachmentIcon, sendFileName: true, showMediaDisplay: false, }, diff --git a/src/types/Settings.ts b/src/types/Settings.ts index 9ba3dbc3..19e7318a 100644 --- a/src/types/Settings.ts +++ b/src/types/Settings.ts @@ -120,7 +120,8 @@ export type Settings = { disabled?: boolean; multiple?: boolean; accept?: string; - icon?: string; + icon?: string | React.FC>; + iconDisabled?: string | React.FC>; sendFileName?: boolean; showMediaDisplay?: boolean; } From ecb13d188f36873fb6fd152aa3e85a76f975b058 Mon Sep 17 00:00:00 2001 From: tjtanjin Date: Sun, 13 Oct 2024 17:00:33 +0800 Subject: [PATCH 25/36] feat: Add svg component support for close chat icon --- src/assets/close_chat_icon.svg | 2 +- .../CloseChatButton/CloseChatButton.tsx | 28 ++++++++++++++++--- src/constants/internal/DefaultSettings.tsx | 2 +- src/types/Settings.ts | 2 +- 4 files changed, 27 insertions(+), 7 deletions(-) diff --git a/src/assets/close_chat_icon.svg b/src/assets/close_chat_icon.svg index d378da60..6ca5a7de 100644 --- a/src/assets/close_chat_icon.svg +++ b/src/assets/close_chat_icon.svg @@ -1 +1 @@ - \ No newline at end of file + \ No newline at end of file diff --git a/src/components/Buttons/CloseChatButton/CloseChatButton.tsx b/src/components/Buttons/CloseChatButton/CloseChatButton.tsx index 1a405c0f..1a2a92ba 100644 --- a/src/components/Buttons/CloseChatButton/CloseChatButton.tsx +++ b/src/components/Buttons/CloseChatButton/CloseChatButton.tsx @@ -22,9 +22,32 @@ const CloseChatButton = () => { // styles for close chat icon const closeChatIconStyle: React.CSSProperties = { backgroundImage: `url(${settings.header?.closeChatIcon})`, + fill: "#e8eaed", + stroke: "#e8eaed", ...styles.closeChatIconStyle }; + /** + * Renders button depending on whether an svg component or image url is provided. + */ + const renderButton = () => { + const IconComponent = settings.header?.closeChatIcon; + if (typeof IconComponent === "string") { + return ( + + ) + } + return ( + IconComponent && + + + + ) + } + return (
{ }} style={styles.closeChatButtonStyle} > - + {renderButton()}
); }; diff --git a/src/constants/internal/DefaultSettings.tsx b/src/constants/internal/DefaultSettings.tsx index a14787fc..aea877fb 100644 --- a/src/constants/internal/DefaultSettings.tsx +++ b/src/constants/internal/DefaultSettings.tsx @@ -8,7 +8,7 @@ import chatIcon from "../../assets/chat_icon.svg"; import { ReactComponent as fileAttachmentIcon } from '../../assets/file_attachment_icon.svg'; import { ReactComponent as notificationIcon } from '../../assets/notification_icon.svg'; import { ReactComponent as notificationIconDisabled } from '../../assets/notification_icon_disabled.svg'; -import closeChatIcon from "../../assets/close_chat_icon.svg"; +import { ReactComponent as closeChatIcon } from '../../assets/close_chat_icon.svg'; import sendButtonIcon from "../../assets/send_icon.svg"; import { ReactComponent as voiceIcon } from '../../assets/voice_icon.svg'; import { ReactComponent as voiceIconDisabled } from '../../assets/voice_icon_disabled.svg'; diff --git a/src/types/Settings.ts b/src/types/Settings.ts index 19e7318a..d47e2c45 100644 --- a/src/types/Settings.ts +++ b/src/types/Settings.ts @@ -28,7 +28,7 @@ export type Settings = { showAvatar?: boolean; avatar?: string; buttons?: Array; - closeChatIcon?: string; + closeChatIcon?: string | React.FC>; }, notification?: { disabled?: boolean; From 27e81b3184a1f268ac247248adcef99a23ba27f0 Mon Sep 17 00:00:00 2001 From: tjtanjin Date: Sun, 13 Oct 2024 17:21:38 +0800 Subject: [PATCH 26/36] feat: Add svg component support for send icon --- src/assets/send_icon.svg | 4 +-- .../Buttons/SendButton/SendButton.css | 6 ++-- .../Buttons/SendButton/SendButton.tsx | 29 ++++++++++++++++++- src/constants/internal/DefaultSettings.tsx | 2 +- src/constants/internal/DefaultStyles.tsx | 1 + src/types/Settings.ts | 2 +- src/types/Styles.ts | 1 + 7 files changed, 36 insertions(+), 9 deletions(-) diff --git a/src/assets/send_icon.svg b/src/assets/send_icon.svg index 57cefbb3..1aef9475 100644 --- a/src/assets/send_icon.svg +++ b/src/assets/send_icon.svg @@ -1,3 +1 @@ - - - + \ No newline at end of file diff --git a/src/components/Buttons/SendButton/SendButton.css b/src/components/Buttons/SendButton/SendButton.css index 37efe9aa..74e70f96 100644 --- a/src/components/Buttons/SendButton/SendButton.css +++ b/src/components/Buttons/SendButton/SendButton.css @@ -2,7 +2,6 @@ .rcb-send-button { display: inline-flex; - align-items: center; justify-content: center; text-transform: uppercase; border: none; @@ -18,8 +17,9 @@ /* Send Icon */ .rcb-send-icon { - width: 60%; - height: 60%; + width: 50%; + height: 50%; + transform: translateY(20%); background-size: cover; object-fit: cover; background-size: contain; diff --git a/src/components/Buttons/SendButton/SendButton.tsx b/src/components/Buttons/SendButton/SendButton.tsx index ea6cf8e3..ff22074e 100644 --- a/src/components/Buttons/SendButton/SendButton.tsx +++ b/src/components/Buttons/SendButton/SendButton.tsx @@ -50,9 +50,18 @@ const SendButton = () => { // styles for send icon const sendIconStyle: React.CSSProperties = { backgroundImage: `url(${settings.chatInput?.sendButtonIcon})`, + fill: "#fff", ...styles.sendIconStyle }; + // styles for disabled send icon + const sendIconDisabledStyle: React.CSSProperties = { + backgroundImage: `url(${settings.chatInput?.sendButtonIcon})`, + fill: "#fff", + ...styles.sendIconStyle, // by default inherit the base style + ...styles.sendIconDisabledStyle + }; + /** * Handles mouse enter event on send button. */ @@ -67,6 +76,24 @@ const SendButton = () => { setIsHovered(false); }; + /** + * Renders button depending on whether an svg component or image url is provided. + */ + const renderButton = () => { + const IconComponent = settings.chatInput?.sendButtonIcon; + if (typeof IconComponent === "string") { + return ( + + ) + } + return ( + IconComponent && + + + + ) + } + return (
{ : (isHovered ? sendButtonHoveredStyle : sendButtonStyle)} className="rcb-send-button" > - + {renderButton()}
); }; diff --git a/src/constants/internal/DefaultSettings.tsx b/src/constants/internal/DefaultSettings.tsx index aea877fb..a0f67a8d 100644 --- a/src/constants/internal/DefaultSettings.tsx +++ b/src/constants/internal/DefaultSettings.tsx @@ -9,7 +9,7 @@ import { ReactComponent as fileAttachmentIcon } from '../../assets/file_attachme import { ReactComponent as notificationIcon } from '../../assets/notification_icon.svg'; import { ReactComponent as notificationIconDisabled } from '../../assets/notification_icon_disabled.svg'; import { ReactComponent as closeChatIcon } from '../../assets/close_chat_icon.svg'; -import sendButtonIcon from "../../assets/send_icon.svg"; +import { ReactComponent as sendButtonIcon } from '../../assets/send_icon.svg'; import { ReactComponent as voiceIcon } from '../../assets/voice_icon.svg'; import { ReactComponent as voiceIconDisabled } from '../../assets/voice_icon_disabled.svg'; import { ReactComponent as emojiIcon } from '../../assets/emoji_icon.svg'; diff --git a/src/constants/internal/DefaultStyles.tsx b/src/constants/internal/DefaultStyles.tsx index ffbbc8f5..b9e69797 100644 --- a/src/constants/internal/DefaultStyles.tsx +++ b/src/constants/internal/DefaultStyles.tsx @@ -57,6 +57,7 @@ export const DefaultStyles: Styles = { voiceIconStyle: {}, voiceIconDisabledStyle: {}, sendIconStyle: {}, + sendIconDisabledStyle: {}, rcbTypingIndicatorContainerStyle: {}, rcbTypingIndicatorDotStyle: {}, toastPromptContainerStyle: {}, diff --git a/src/types/Settings.ts b/src/types/Settings.ts index d47e2c45..27ebb4e7 100644 --- a/src/types/Settings.ts +++ b/src/types/Settings.ts @@ -65,7 +65,7 @@ export type Settings = { showCharacterCount?: boolean; characterLimit?: number; botDelay?: number; - sendButtonIcon?: string; + sendButtonIcon?: string | React.FC>; blockSpam?: boolean; sendOptionOutput?: boolean; sendCheckboxOutput?: boolean; diff --git a/src/types/Styles.ts b/src/types/Styles.ts index 54b38492..9e8f286f 100644 --- a/src/types/Styles.ts +++ b/src/types/Styles.ts @@ -58,6 +58,7 @@ export type Styles = { voiceIconStyle?: React.CSSProperties; voiceIconDisabledStyle?: React.CSSProperties; sendIconStyle?: React.CSSProperties; + sendIconDisabledStyle?: React.CSSProperties; rcbTypingIndicatorContainerStyle?: React.CSSProperties; rcbTypingIndicatorDotStyle?: React.CSSProperties; toastPromptContainerStyle?: React.CSSProperties; From dbf6033d9c147313350b54e3e59c07da4c376ec9 Mon Sep 17 00:00:00 2001 From: tjtanjin Date: Sun, 13 Oct 2024 17:25:13 +0800 Subject: [PATCH 27/36] feat: Add svg component support for chat button icon --- .../ChatBotButton/ChatBotButton.tsx | 26 ++++++++++++++++--- src/constants/internal/DefaultSettings.tsx | 2 +- src/types/Settings.ts | 2 +- 3 files changed, 24 insertions(+), 6 deletions(-) diff --git a/src/components/ChatBotButton/ChatBotButton.tsx b/src/components/ChatBotButton/ChatBotButton.tsx index 79ea182b..888bf0e9 100644 --- a/src/components/ChatBotButton/ChatBotButton.tsx +++ b/src/components/ChatBotButton/ChatBotButton.tsx @@ -33,6 +33,27 @@ const ChatBotButton = () => { backgroundImage: `url(${settings.chatButton?.icon})`, ...styles.chatIconStyle }; + + /** + * Renders button depending on whether an svg component or image url is provided. + */ + const renderButton = () => { + const IconComponent = settings.chatButton?.icon; + if (typeof IconComponent === "string") { + return ( + + ) + } + return ( + IconComponent && + + + + ) + } return ( <> @@ -44,10 +65,7 @@ const ChatBotButton = () => { className={`rcb-toggle-button ${isChatWindowOpen ? "rcb-button-hide" : "rcb-button-show"}`} onClick={toggleChatWindow} > - + {renderButton()} {!settings.notification?.disabled && settings.notification?.showCount && {unreadCount} diff --git a/src/constants/internal/DefaultSettings.tsx b/src/constants/internal/DefaultSettings.tsx index a0f67a8d..fb858a35 100644 --- a/src/constants/internal/DefaultSettings.tsx +++ b/src/constants/internal/DefaultSettings.tsx @@ -4,7 +4,7 @@ import { Button } from "../Button"; import actionDisabledIcon from "../../assets/action_disabled_icon.svg"; import botAvatar from "../../assets/bot_avatar.svg"; import userAvatar from "../../assets/user_avatar.svg"; -import chatIcon from "../../assets/chat_icon.svg"; +import { ReactComponent as chatIcon } from "../../assets/chat_icon.svg"; import { ReactComponent as fileAttachmentIcon } from '../../assets/file_attachment_icon.svg'; import { ReactComponent as notificationIcon } from '../../assets/notification_icon.svg'; import { ReactComponent as notificationIconDisabled } from '../../assets/notification_icon_disabled.svg'; diff --git a/src/types/Settings.ts b/src/types/Settings.ts index 27ebb4e7..bb015291 100644 --- a/src/types/Settings.ts +++ b/src/types/Settings.ts @@ -21,7 +21,7 @@ export type Settings = { text?: string; }, chatButton?: { - icon?: string; + icon?: string | React.FC>; }, header?: { title?: string | JSX.Element; From 56f2b6bc3de77d608ce0447c4ffb618fcaecb02a Mon Sep 17 00:00:00 2001 From: tjtanjin Date: Sun, 13 Oct 2024 17:55:20 +0800 Subject: [PATCH 28/36] fix: Fix close chat button unit test --- __tests__/__mocks__/fileMock.ts | 3 ++- .../buttons/CloseChatButton.test.tsx | 21 +++++++++---------- .../CloseChatButton/CloseChatButton.tsx | 3 ++- 3 files changed, 14 insertions(+), 13 deletions(-) diff --git a/__tests__/__mocks__/fileMock.ts b/__tests__/__mocks__/fileMock.ts index 406f6426..5e0e7e91 100644 --- a/__tests__/__mocks__/fileMock.ts +++ b/__tests__/__mocks__/fileMock.ts @@ -1,4 +1,5 @@ // __mocks__/fileMock.ts 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 +export const emojiIcon = "../../assets/emoji_icon.svg"; +export const closeChatIcon = "../../assets/close_chat_icon.svg"; \ No newline at end of file diff --git a/__tests__/components/buttons/CloseChatButton.test.tsx b/__tests__/components/buttons/CloseChatButton.test.tsx index 71bf39c0..87e1967e 100644 --- a/__tests__/components/buttons/CloseChatButton.test.tsx +++ b/__tests__/components/buttons/CloseChatButton.test.tsx @@ -6,6 +6,7 @@ import "@testing-library/jest-dom/jest-globals"; import CloseChatButton from "../../../src/components/Buttons/CloseChatButton/CloseChatButton"; import { DefaultSettings } from "../../../src/constants/internal/DefaultSettings"; +import { closeChatIcon } from "../../__mocks__/fileMock"; import { useChatWindowInternal } from "../../../src/hooks/internal/useChatWindowInternal"; import { useSettingsContext } from "../../../src/context/SettingsContext"; @@ -31,7 +32,7 @@ describe("CloseChatButton", () => { // Mock the return value of useSettingsContext hook (useSettingsContext as jest.Mock).mockReturnValue({ settings: { - header: { closeChatIcon: DefaultSettings.header?.closeChatIcon }, + header: { closeChatIcon }, ariaLabel: { closeChatButton: DefaultSettings.ariaLabel?.closeChatButton }, }, }); @@ -69,13 +70,11 @@ describe("CloseChatButton", () => { it("applies the correct background image to the close chat icon", () => { // Render the CloseChatButton component render(); - // Get the button element by its role - const button = screen.getByRole("button"); - // Get the span element inside the button (assumed to be the icon) - const icon = button.querySelector("span"); + // Get the icon element by its data test id + const icon = screen.getByTestId("rcb-close-chat-icon"); - // Check if the background image is set correctly - expect(icon).toHaveStyle(`background-image: url(${DefaultSettings.header?.closeChatIcon})`); + // Check if the fill is set correctly + expect(icon).toHaveStyle("fill: #e8eaed"); }); it("applies the default aria-label when none is provided in settings", () => { @@ -99,15 +98,15 @@ describe("CloseChatButton", () => { render(); // Get the button element by its role const button = screen.getByRole("button"); - // Get the span element inside the button (assumed to be the icon) - const icon = button.querySelector("span"); + // Get the icon element by its data test id + const icon = screen.getByTestId("rcb-close-chat-icon"); // Assert that the button has the correct background color expect(button).toHaveStyle("background-color: gray"); // Assert that the icon has the correct color expect(icon).toHaveStyle("color: red"); - // Assert that the icon has the correct background image - expect(icon).toHaveStyle(`background-image: url(${DefaultSettings.header?.closeChatIcon})`); + // Assert that the icon has correct fill + expect(icon).toHaveStyle("fill: #e8eaed"); }); it("calls openChat(false) when the button is clicked", () => { diff --git a/src/components/Buttons/CloseChatButton/CloseChatButton.tsx b/src/components/Buttons/CloseChatButton/CloseChatButton.tsx index 1a2a92ba..433fb081 100644 --- a/src/components/Buttons/CloseChatButton/CloseChatButton.tsx +++ b/src/components/Buttons/CloseChatButton/CloseChatButton.tsx @@ -36,13 +36,14 @@ const CloseChatButton = () => { return ( ) } return ( IconComponent && - + ) From 4c1619f23c1a34477279fa5fa86cefc41e299652 Mon Sep 17 00:00:00 2001 From: tjtanjin Date: Mon, 14 Oct 2024 00:51:18 +0800 Subject: [PATCH 29/36] fix: Fix notification button unit test --- __tests__/__mocks__/fileMock.ts | 4 +- .../buttons/CloseChatButton.test.tsx | 6 +- .../buttons/NotificationButton.test.tsx | 52 ++++---- .../BlockService/CheckboxProcessor.test.tsx | 118 +++++++++--------- .../NotificationButton/NotificationButton.tsx | 5 +- 5 files changed, 97 insertions(+), 88 deletions(-) diff --git a/__tests__/__mocks__/fileMock.ts b/__tests__/__mocks__/fileMock.ts index 5e0e7e91..bac24030 100644 --- a/__tests__/__mocks__/fileMock.ts +++ b/__tests__/__mocks__/fileMock.ts @@ -2,4 +2,6 @@ export const botAvatar = "../../assets/bot_avatar.svg"; export const actionDisabledIcon = "../../assets/action_disabled_icon.svg"; export const emojiIcon = "../../assets/emoji_icon.svg"; -export const closeChatIcon = "../../assets/close_chat_icon.svg"; \ No newline at end of file +export const closeChatIcon = "../../assets/close_chat_icon.svg"; +export const notificationIcon = "../../assets/notification_icon.svg"; +export const notificationIconDisabled = "../../assets/notification_icon_disabled.svg"; \ No newline at end of file diff --git a/__tests__/components/buttons/CloseChatButton.test.tsx b/__tests__/components/buttons/CloseChatButton.test.tsx index 87e1967e..f4e91d0a 100644 --- a/__tests__/components/buttons/CloseChatButton.test.tsx +++ b/__tests__/components/buttons/CloseChatButton.test.tsx @@ -5,12 +5,12 @@ import { render, screen, fireEvent } from "@testing-library/react"; import "@testing-library/jest-dom/jest-globals"; import CloseChatButton from "../../../src/components/Buttons/CloseChatButton/CloseChatButton"; -import { DefaultSettings } from "../../../src/constants/internal/DefaultSettings"; -import { closeChatIcon } from "../../__mocks__/fileMock"; - import { useChatWindowInternal } from "../../../src/hooks/internal/useChatWindowInternal"; import { useSettingsContext } from "../../../src/context/SettingsContext"; import { useStylesContext } from "../../../src/context/StylesContext"; +import { DefaultSettings } from "../../../src/constants/internal/DefaultSettings"; + +import { closeChatIcon } from "../../__mocks__/fileMock"; // Mock the hooks used in the component jest.mock("../../../src/hooks/internal/useChatWindowInternal"); diff --git a/__tests__/components/buttons/NotificationButton.test.tsx b/__tests__/components/buttons/NotificationButton.test.tsx index ef552395..30a9deb1 100644 --- a/__tests__/components/buttons/NotificationButton.test.tsx +++ b/__tests__/components/buttons/NotificationButton.test.tsx @@ -7,14 +7,24 @@ import "@testing-library/jest-dom/jest-globals"; import NotificationButton from "../../../src/components/Buttons/NotificationButton/NotificationButton"; import { DefaultSettings } from "../../../src/constants/internal/DefaultSettings"; -import { TestChatBotProvider } from "../../__mocks__/TestChatBotContext"; +import { TestChatBotProvider } from "../../__mocks__/TestChatBotContext" +import { notificationIcon, notificationIconDisabled } from "../../__mocks__/fileMock"; /** * Helper function to render NotificationButton with different settings. * - * @param initialSettings initial settings for the TestChatBotProvider + * @param disabled boolean indicating if notification is disabled + * @param defaultToggledOn boolean idnicating if notification is toggled on by default */ -const renderNotificationButton = (initialSettings = { notification: { disabled: false, defaultToggledOn: false } }) => { +const renderNotificationButton = (disabled: boolean, defaultToggledOn: boolean) => { + const initialSettings = { + notification: { + disabled: disabled, + defaultToggledOn: defaultToggledOn, + icon: notificationIcon, + iconDisabled: notificationIconDisabled + } + }; return render( @@ -28,79 +38,75 @@ const renderNotificationButton = (initialSettings = { notification: { disabled: describe("NotificationButton Component", () => { it("renders with correct aria-label and initial state when defaultToggledOn is false and not disabled", () => { // settings used for this test to render notification button - const initialSettings = { notification: { disabled: false, defaultToggledOn: false } }; - renderNotificationButton(initialSettings); + renderNotificationButton(false, false); // retrieves button and icon const button = screen.getByRole("button", { name: DefaultSettings.ariaLabel?.notificationButton }); - const icon = button.querySelector("span"); + const icon = screen.getByTestId("rcb-notification-icon"); expect(button).toBeInTheDocument(); // checks new state - expect(icon).toHaveClass("rcb-notification-icon-off"); + expect(icon).toHaveStyle("fill: #e8eaed"); }); it("toggles notification state when clicked (initially off)", () => { // settings used for this test to render notification button - const initialSettings = { notification: { disabled: false, defaultToggledOn: false } }; - renderNotificationButton(initialSettings); + renderNotificationButton(false, false); // retrieves button and icon const button = screen.getByRole("button", { name: DefaultSettings.ariaLabel?.notificationButton }); - const icon = button.querySelector("span"); + const icon = screen.getByTestId("rcb-notification-icon"); expect(button).toBeInTheDocument(); // checks initial state - expect(icon).toHaveClass("rcb-notification-icon-off"); + expect(icon).toHaveStyle("fill: #e8eaed"); // clicks the button to toggle notification fireEvent.mouseDown(button); // checks new state - expect(icon).toHaveClass("rcb-notification-icon-on"); + expect(icon).toHaveStyle("fill: #fcec3d"); }); it("renders with notification toggled on initially and toggles to off when clicked", () => { // settings used for this test to render notification button - const initialSettings = { notification: { disabled: false, defaultToggledOn: true } }; - renderNotificationButton(initialSettings); + renderNotificationButton(false, true); // retrieves button and icon const button = screen.getByRole("button", { name: DefaultSettings.ariaLabel?.notificationButton }); - const icon = button.querySelector("span"); + const icon = screen.getByTestId("rcb-notification-icon"); expect(button).toBeInTheDocument(); // checks initial state - expect(icon).toHaveClass("rcb-notification-icon-on"); + expect(icon).toHaveStyle("fill: #fcec3d"); // clicks the button to toggle notification fireEvent.mouseDown(button); // checks new state - expect(icon).toHaveClass("rcb-notification-icon-off"); + expect(icon).toHaveStyle("fill: #e8eaed"); }); it("toggles notification back to on after being toggled off", () => { // settings used for this test to render notification button - const initialSettings = { notification: { disabled: false, defaultToggledOn: true } }; - renderNotificationButton(initialSettings); + renderNotificationButton(false, true); // retrieves button and icon const button = screen.getByRole("button", { name: DefaultSettings.ariaLabel?.notificationButton }); - const icon = button.querySelector("span"); + const icon = screen.getByTestId("rcb-notification-icon"); expect(button).toBeInTheDocument(); // checks initial state - expect(icon).toHaveClass("rcb-notification-icon-on"); + expect(icon).toHaveStyle("fill: #fcec3d"); // clicks the button to toggle notification fireEvent.mouseDown(button); // checks new state - expect(icon).toHaveClass("rcb-notification-icon-off"); + expect(icon).toHaveStyle("fill: #e8eaed"); // checks new state fireEvent.mouseDown(button); - expect(icon).toHaveClass("rcb-notification-icon-on"); + expect(icon).toHaveStyle("fill: #fcec3d"); }); }); \ No newline at end of file diff --git a/__tests__/services/BlockService/CheckboxProcessor.test.tsx b/__tests__/services/BlockService/CheckboxProcessor.test.tsx index 86e1e349..07774de0 100644 --- a/__tests__/services/BlockService/CheckboxProcessor.test.tsx +++ b/__tests__/services/BlockService/CheckboxProcessor.test.tsx @@ -4,85 +4,85 @@ import { Flow } from "../../../src/types/Flow"; import { Params } from "../../../src/types/Params"; describe("processCheckboxes", () => { - let mockFlow: Flow; - let mockBlock: Block; - let mockParams: Params; + let mockFlow: Flow; + let mockBlock: Block; + let mockParams: Params; - beforeEach(() => { - mockFlow = {} as Flow; - mockBlock = {} as Block; - mockParams = { - injectMessage: jest.fn(), - } as unknown as 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); + it("should return early if block has no checkboxes", async () => { + await processCheckboxes(mockFlow, mockBlock, "somePath", mockParams); - expect(mockParams.injectMessage).not.toHaveBeenCalled(); - }); + 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); + 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); + await processCheckboxes(mockFlow, mockBlock, "somePath", mockParams); - expect(mockBlock.checkboxes).toHaveBeenCalledWith(mockParams); - expect(mockParams.injectMessage).toHaveBeenCalled(); - }); + 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); + 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); + await processCheckboxes(mockFlow, mockBlock, "somePath", mockParams); - expect(mockBlock.checkboxes).toHaveBeenCalledWith(mockParams); - expect(mockParams.injectMessage).toHaveBeenCalled(); - }); + 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"]; + it("should convert checkbox array to object with items", async () => { + mockBlock.checkboxes = ["Checkbox 1", "Checkbox 2"]; - await processCheckboxes(mockFlow, mockBlock, "somePath", mockParams); + 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); - }); + 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"] }; + it("should set min and max values when not provided", async () => { + mockBlock.checkboxes = { items: ["Item 1", "Item 2"] }; - await processCheckboxes(mockFlow, mockBlock, "somePath", mockParams); + 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); - }); + 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 }; + 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); + 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); + 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); - }); + expect(content.props.checkboxes).toMatchObject(expectedCheckboxes); + }); - it("should not inject message if no items are present in checkboxes", async () => { - mockBlock.checkboxes = { items: [] }; + it("should not inject message if no items are present in checkboxes", async () => { + mockBlock.checkboxes = { items: [] }; - await processCheckboxes(mockFlow, mockBlock, "somePath", mockParams); + await processCheckboxes(mockFlow, mockBlock, "somePath", mockParams); - expect(mockParams.injectMessage).not.toHaveBeenCalled(); - }); + expect(mockParams.injectMessage).not.toHaveBeenCalled(); + }); }); diff --git a/src/components/Buttons/NotificationButton/NotificationButton.tsx b/src/components/Buttons/NotificationButton/NotificationButton.tsx index 84f3a524..f4a92801 100644 --- a/src/components/Buttons/NotificationButton/NotificationButton.tsx +++ b/src/components/Buttons/NotificationButton/NotificationButton.tsx @@ -28,7 +28,7 @@ const NotificationButton = () => { // styles for notification disabled icon const notificationIconDisabledStyle: React.CSSProperties = { - backgroundImage: `url(${settings.notification?.icon})`, + backgroundImage: `url(${settings.notification?.iconDisabled})`, fill: "#e8eaed", ...styles.notificationIconStyle, // by default inherit the base style ...styles.notificationIconDisabledStyle @@ -45,13 +45,14 @@ const NotificationButton = () => { return ( ) } return ( IconComponent && - + ) From c09929b76076854cdd0758a89feb003a721cb949 Mon Sep 17 00:00:00 2001 From: tjtanjin Date: Mon, 14 Oct 2024 00:53:01 +0800 Subject: [PATCH 30/36] fix: Fix disabled voice and audio icons --- src/components/Buttons/AudioButton/AudioButton.tsx | 2 +- src/components/Buttons/VoiceButton/VoiceButton.tsx | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/src/components/Buttons/AudioButton/AudioButton.tsx b/src/components/Buttons/AudioButton/AudioButton.tsx index b4818a06..4b497504 100644 --- a/src/components/Buttons/AudioButton/AudioButton.tsx +++ b/src/components/Buttons/AudioButton/AudioButton.tsx @@ -28,7 +28,7 @@ const AudioButton = () => { // styles for audio disabled icon const audioIconDisabledStyle: React.CSSProperties = { - backgroundImage: `url(${settings.audio?.icon})`, + backgroundImage: `url(${settings.audio?.iconDisabled})`, fill: "#e8eaed", ...styles.audioIconStyle, // by default inherit the base style ...styles.audioIconDisabledStyle diff --git a/src/components/Buttons/VoiceButton/VoiceButton.tsx b/src/components/Buttons/VoiceButton/VoiceButton.tsx index 771b4323..efe952bc 100644 --- a/src/components/Buttons/VoiceButton/VoiceButton.tsx +++ b/src/components/Buttons/VoiceButton/VoiceButton.tsx @@ -72,7 +72,7 @@ const VoiceButton = () => { // styles for voice disabled icon const voiceIconDisabledStyle: React.CSSProperties = { - backgroundImage: `url(${settings.voice?.icon})`, + backgroundImage: `url(${settings.voice?.iconDisabled})`, fill: "#9aa0a6", ...styles.voiceIconStyle, // by default inherit the base style ...styles.voiceIconDisabledStyle From 8afe82df0849e4a547755b47357caeb3a993011a Mon Sep 17 00:00:00 2001 From: tjtanjin Date: Mon, 14 Oct 2024 01:00:19 +0800 Subject: [PATCH 31/36] fix: Fix audio button unit test --- __tests__/__mocks__/fileMock.ts | 4 +- .../components/buttons/AudioButton.test.tsx | 50 +++++++++++-------- .../Buttons/AudioButton/AudioButton.tsx | 3 +- 3 files changed, 33 insertions(+), 24 deletions(-) diff --git a/__tests__/__mocks__/fileMock.ts b/__tests__/__mocks__/fileMock.ts index bac24030..a6f97c11 100644 --- a/__tests__/__mocks__/fileMock.ts +++ b/__tests__/__mocks__/fileMock.ts @@ -4,4 +4,6 @@ export const actionDisabledIcon = "../../assets/action_disabled_icon.svg"; export const emojiIcon = "../../assets/emoji_icon.svg"; export const closeChatIcon = "../../assets/close_chat_icon.svg"; export const notificationIcon = "../../assets/notification_icon.svg"; -export const notificationIconDisabled = "../../assets/notification_icon_disabled.svg"; \ No newline at end of file +export const notificationIconDisabled = "../../assets/notification_icon_disabled.svg"; +export const audioIcon = "../../assets/audio_icon.svg"; +export const audioIconDisabled = "../../assets/audio_icon_disabled.svg"; \ No newline at end of file diff --git a/__tests__/components/buttons/AudioButton.test.tsx b/__tests__/components/buttons/AudioButton.test.tsx index 947aa318..dc2165b7 100644 --- a/__tests__/components/buttons/AudioButton.test.tsx +++ b/__tests__/components/buttons/AudioButton.test.tsx @@ -8,13 +8,23 @@ import AudioButton from "../../../src/components/Buttons/AudioButton/AudioButton import { DefaultSettings } from "../../../src/constants/internal/DefaultSettings"; import { TestChatBotProvider } from "../../__mocks__/TestChatBotContext"; +import { audioIcon, audioIconDisabled } from "../../__mocks__/fileMock"; /** * Helper function to render AudioButton with different settings. * - * @param initialSettings initial settings for the TestChatBotProvider + * @param disabled boolean indicating if audio is disabled + * @param defaultToggledOn boolean idnicating if audio is toggled on by default */ -const renderAudioButton = (initialSettings = { audio: { disabled: false, defaultToggledOn: false } }) => { +const renderAudioButton = (disabled: boolean, defaultToggledOn: boolean) => { + const initialSettings = { + audio: { + disabled: disabled, + defaultToggledOn: defaultToggledOn, + icon: audioIcon, + iconDisabled: audioIconDisabled + } + }; return render( @@ -28,79 +38,75 @@ const renderAudioButton = (initialSettings = { audio: { disabled: false, default describe("AudioButton Component", () => { it("renders with correct aria-label and initial state when defaultToggledOn is false and not disabled", () => { // settings used for this test to render audio button - const initialSettings = { audio: { disabled: false, defaultToggledOn: false } }; - renderAudioButton(initialSettings); + renderAudioButton(false, false); // retrieves button and icon const button = screen.getByRole("button", { name: DefaultSettings.ariaLabel?.audioButton }); - const icon = button.querySelector("span"); + const icon = screen.getByTestId("rcb-audio-icon"); expect(button).toBeInTheDocument(); // checks new state - expect(icon).toHaveClass("rcb-audio-icon-off"); + expect(icon).toHaveStyle("fill: #e8eaed"); }); it("toggles audio state when clicked (initially off)", () => { // settings used for this test to render audio button - const initialSettings = { audio: { disabled: false, defaultToggledOn: false } }; - renderAudioButton(initialSettings); + renderAudioButton(false, false); // retrieves button and icon const button = screen.getByRole("button", { name: DefaultSettings.ariaLabel?.audioButton }); - const icon = button.querySelector("span"); + const icon = screen.getByTestId("rcb-audio-icon"); expect(button).toBeInTheDocument(); // checks initial state - expect(icon).toHaveClass("rcb-audio-icon-off"); + expect(icon).toHaveStyle("fill: #e8eaed"); // clicks the button to toggle audio fireEvent.mouseDown(button); // checks new state - expect(icon).toHaveClass("rcb-audio-icon-on"); + expect(icon).toHaveStyle("fill: #fcec3d"); }); it("renders with audio toggled on initially and toggles to off when clicked", () => { // settings used for this test to render audio button - const initialSettings = { audio: { disabled: false, defaultToggledOn: true } }; - renderAudioButton(initialSettings); + renderAudioButton(false, true); // retrieves button and icon const button = screen.getByRole("button", { name: DefaultSettings.ariaLabel?.audioButton }); - const icon = button.querySelector("span"); + const icon = screen.getByTestId("rcb-audio-icon"); expect(button).toBeInTheDocument(); // checks initial state - expect(icon).toHaveClass("rcb-audio-icon-on"); + expect(icon).toHaveStyle("fill: #fcec3d"); // clicks the button to toggle audio fireEvent.mouseDown(button); // checks new state - expect(icon).toHaveClass("rcb-audio-icon-off"); + expect(icon).toHaveStyle("fill: #e8eaed"); }); it("toggles audio back to on after being toggled off", () => { // settings used for this test to render audio button - const initialSettings = { audio: { disabled: false, defaultToggledOn: true } }; - renderAudioButton(initialSettings); + renderAudioButton(false, true); // retrieves button and icon const button = screen.getByRole("button", { name: DefaultSettings.ariaLabel?.audioButton }); - const icon = button.querySelector("span"); + const icon = screen.getByTestId("rcb-audio-icon"); expect(button).toBeInTheDocument(); // checks initial state - expect(icon).toHaveClass("rcb-audio-icon-on"); + expect(icon).toHaveStyle("fill: #fcec3d"); // clicks the button to toggle audio fireEvent.mouseDown(button); // checks new state - expect(icon).toHaveClass("rcb-audio-icon-off"); + expect(icon).toHaveStyle("fill: #e8eaed"); // checks new state fireEvent.mouseDown(button); - expect(icon).toHaveClass("rcb-audio-icon-on"); + expect(icon).toHaveStyle("fill: #fcec3d"); }); }); \ No newline at end of file diff --git a/src/components/Buttons/AudioButton/AudioButton.tsx b/src/components/Buttons/AudioButton/AudioButton.tsx index 4b497504..53101850 100644 --- a/src/components/Buttons/AudioButton/AudioButton.tsx +++ b/src/components/Buttons/AudioButton/AudioButton.tsx @@ -43,13 +43,14 @@ const AudioButton = () => { return ( ) } return ( IconComponent && - + ) From fbec2194ab0f0dbed4455cde3c58599426fb8763 Mon Sep 17 00:00:00 2001 From: tjtanjin Date: Mon, 14 Oct 2024 01:06:57 +0800 Subject: [PATCH 32/36] fix: Fix send button unit test --- __tests__/__mocks__/fileMock.ts | 3 ++- .../components/buttons/SendButton.test.tsx | 17 +++++++++++------ .../Buttons/AudioButton/AudioButton.tsx | 2 +- .../Buttons/CloseChatButton/CloseChatButton.tsx | 2 +- .../Buttons/EmojiButton/EmojiButton.tsx | 2 +- .../FileAttachmentButton.tsx | 2 +- .../NotificationButton/NotificationButton.tsx | 2 +- .../Buttons/SendButton/SendButton.tsx | 10 +++++++--- .../Buttons/VoiceButton/VoiceButton.tsx | 2 +- src/components/ChatBotButton/ChatBotButton.tsx | 2 +- 10 files changed, 27 insertions(+), 17 deletions(-) diff --git a/__tests__/__mocks__/fileMock.ts b/__tests__/__mocks__/fileMock.ts index a6f97c11..92ede8ee 100644 --- a/__tests__/__mocks__/fileMock.ts +++ b/__tests__/__mocks__/fileMock.ts @@ -6,4 +6,5 @@ export const closeChatIcon = "../../assets/close_chat_icon.svg"; export const notificationIcon = "../../assets/notification_icon.svg"; export const notificationIconDisabled = "../../assets/notification_icon_disabled.svg"; export const audioIcon = "../../assets/audio_icon.svg"; -export const audioIconDisabled = "../../assets/audio_icon_disabled.svg"; \ No newline at end of file +export const audioIconDisabled = "../../assets/audio_icon_disabled.svg"; +export const sendIcon = "../../assets/send_icon.svg"; \ No newline at end of file diff --git a/__tests__/components/buttons/SendButton.test.tsx b/__tests__/components/buttons/SendButton.test.tsx index c33cac32..401f532d 100644 --- a/__tests__/components/buttons/SendButton.test.tsx +++ b/__tests__/components/buttons/SendButton.test.tsx @@ -12,6 +12,8 @@ import { useSubmitInputInternal } from "../../../src/hooks/internal/useSubmitInp import { useStylesContext } from "../../../src/context/StylesContext"; import { DefaultStyles } from "../../../src/constants/internal/DefaultStyles"; +import { sendIcon } from "../../__mocks__/fileMock"; + jest.mock("../../../src/context/SettingsContext"); jest.mock("../../../src/context/BotStatesContext"); jest.mock("../../../src/hooks/internal/useSubmitInputInternal"); @@ -29,6 +31,9 @@ describe("SendButton Component", () => { secondaryColor: DefaultSettings.general?.secondaryColor, }, ariaLabel: { sendButton: DefaultSettings.ariaLabel?.sendButton }, + chatInput: { + sendButtonIcon: sendIcon + } } }); @@ -56,7 +61,7 @@ describe("SendButton Component", () => { render(); const button = screen.getByRole("button", { name: DefaultSettings.ariaLabel?.sendButton }); - const icon = button.querySelector("span"); + const icon = screen.getByTestId("rcb-send-icon"); expect(button).toBeInTheDocument(); expect(icon).toBeInTheDocument(); @@ -95,7 +100,7 @@ describe("SendButton Component", () => { render(); const button = screen.getByRole("button", { name: DefaultSettings.ariaLabel?.sendButton }); - const icon = button.querySelector("span"); + const icon = screen.getByTestId("rcb-send-icon"); expect(button).toBeInTheDocument(); expect(icon).toBeInTheDocument(); @@ -129,7 +134,7 @@ describe("SendButton Component", () => { render(); const button = screen.getByRole("button", { name: DefaultSettings.ariaLabel?.sendButton }); - const icon = button.querySelector("span"); + const icon = screen.getByTestId("rcb-send-icon"); expect(button).toBeInTheDocument(); expect(icon).toBeInTheDocument(); @@ -158,7 +163,7 @@ describe("SendButton Component", () => { render(); const button = screen.getByRole("button", { name: DefaultSettings.ariaLabel?.sendButton }); - const icon = button.querySelector("span"); + const icon = screen.getByTestId("rcb-send-icon"); expect(button).toBeInTheDocument(); expect(icon).toBeInTheDocument(); @@ -197,7 +202,7 @@ describe("SendButton Component", () => { render(); const button = screen.getByRole('button', { name: DefaultSettings.ariaLabel?.sendButton }); - const icon = button.querySelector("span"); + const icon = screen.getByTestId("rcb-send-icon"); expect(button).toBeInTheDocument(); expect(icon).toBeInTheDocument(); @@ -218,7 +223,7 @@ describe("SendButton Component", () => { render(); const button = screen.getByRole('button', { name: DefaultSettings.ariaLabel?.sendButton }); - const icon = button.querySelector("span"); + const icon = screen.getByTestId("rcb-send-icon"); expect(button).toBeInTheDocument(); expect(icon).toBeInTheDocument(); diff --git a/src/components/Buttons/AudioButton/AudioButton.tsx b/src/components/Buttons/AudioButton/AudioButton.tsx index 53101850..a72bcc01 100644 --- a/src/components/Buttons/AudioButton/AudioButton.tsx +++ b/src/components/Buttons/AudioButton/AudioButton.tsx @@ -39,7 +39,7 @@ const AudioButton = () => { */ const renderButton = () => { const IconComponent = audioToggledOn ? settings.audio?.icon : settings.audio?.iconDisabled; - if (typeof IconComponent === "string") { + if (!IconComponent || typeof IconComponent === "string") { return ( { */ const renderButton = () => { const IconComponent = settings.header?.closeChatIcon; - if (typeof IconComponent === "string") { + if (!IconComponent || typeof IconComponent === "string") { return ( { */ const renderButton = () => { const IconComponent = textAreaDisabled ? settings.emoji?.iconDisabled : settings.emoji?.icon; - if (typeof IconComponent === "string") { + if (!IconComponent || typeof IconComponent === "string") { return ( { const IconComponent = blockAllowsAttachment ? settings.fileAttachment?.icon : settings.fileAttachment?.iconDisabled; - if (typeof IconComponent === "string") { + if (!IconComponent || typeof IconComponent === "string") { return ( { const IconComponent = notificationsToggledOn ? settings.notification?.icon : settings.notification?.iconDisabled; - if (typeof IconComponent === "string") { + if (!IconComponent || typeof IconComponent === "string") { return ( { */ const renderButton = () => { const IconComponent = settings.chatInput?.sendButtonIcon; - if (typeof IconComponent === "string") { + if (!IconComponent || typeof IconComponent === "string") { return ( - + ) } return ( IconComponent && - + ) diff --git a/src/components/Buttons/VoiceButton/VoiceButton.tsx b/src/components/Buttons/VoiceButton/VoiceButton.tsx index efe952bc..e611e012 100644 --- a/src/components/Buttons/VoiceButton/VoiceButton.tsx +++ b/src/components/Buttons/VoiceButton/VoiceButton.tsx @@ -106,7 +106,7 @@ const VoiceButton = () => { */ const renderButton = () => { const IconComponent = voiceToggledOn ? settings.voice?.icon : settings.voice?.iconDisabled; - if (typeof IconComponent === "string") { + if (!IconComponent || typeof IconComponent === "string") { return ( { */ const renderButton = () => { const IconComponent = settings.chatButton?.icon; - if (typeof IconComponent === "string") { + if (!IconComponent || typeof IconComponent === "string") { return ( Date: Mon, 14 Oct 2024 01:41:06 +0800 Subject: [PATCH 33/36] docs: Update changelog and version --- CHANGELOG.md | 14 ++++++++++++++ package-lock.json | 4 ++-- package.json | 2 +- 3 files changed, 17 insertions(+), 3 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index ae77767e..d5209084 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,19 @@ # CHANGELOG.md +## v2.0.0-beta.17 (13-10-2024) + +**Fixed:** +- Fixed improper parsing of css files in themes +- Fixed toast animation not working + +**Added:** +- Updated button with svgs from: https://fonts.google.com/ +- Added disabled icon support for all buttons (now possible to have different icons for enabled/disabled state) +- Added svg component support for button icons (conveniently use the `fill` attribute to recolor icons!) +- Added a new `sendIconDisabledStyle` +- Loading of chat history no longer locks the text area +- Standardized keyframe naming conventions + ## v2.0.0-beta.16 (08-10-2024) **Fixed:** diff --git a/package-lock.json b/package-lock.json index f304ba3c..8738c155 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "react-chatbotify", - "version": "2.0.0-beta.16", + "version": "2.0.0-beta.17", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "react-chatbotify", - "version": "2.0.0-beta.16", + "version": "2.0.0-beta.17", "license": "MIT", "devDependencies": { "@testing-library/jest-dom": "^6.5.0", diff --git a/package.json b/package.json index 30028fbd..190d8311 100644 --- a/package.json +++ b/package.json @@ -12,7 +12,7 @@ "files": [ "./dist" ], - "version": "2.0.0-beta.16", + "version": "2.0.0-beta.17", "description": "A modern React library for creating flexible and extensible chatbots.", "type": "module", "main": "./dist/index.cjs", From 9b93706022d6e0bcea28943cb2d87369ba25177c Mon Sep 17 00:00:00 2001 From: tjtanjin Date: Mon, 14 Oct 2024 01:42:35 +0800 Subject: [PATCH 34/36] fix: Fix int test --- cypress/e2e/tests.cy.ts | 10 ++-- .../Buttons/AudioButton/AudioButton.tsx | 5 +- .../NotificationButton/NotificationButton.tsx | 5 +- .../ChatBotHeader/ChatBotHeader.css | 47 ------------------- 4 files changed, 14 insertions(+), 53 deletions(-) diff --git a/cypress/e2e/tests.cy.ts b/cypress/e2e/tests.cy.ts index a04fe3bf..bb98870c 100644 --- a/cypress/e2e/tests.cy.ts +++ b/cypress/e2e/tests.cy.ts @@ -150,13 +150,15 @@ describe("Chat Bot Test Suite", () => { }); it("Toggles notifications", () => { - cy.get(".rcb-notification-icon-on").click(); - cy.get(".rcb-notification-icon-off").should("be.visible"); + cy.get("[data-testid='rcb-notification-icon-svg']").click(); + cy.wait(100); + cy.get("[data-testid='rcb-notification-icon-svg']").should('have.css', 'fill', 'rgb(232, 234, 237)'); }); it("Toggles audio", () => { - cy.get(".rcb-audio-icon-off").click(); - cy.get(".rcb-audio-icon-on").should("be.visible"); + cy.get("[data-testid='rcb-audio-icon-svg']").click(); + cy.wait(100); + cy.get("[data-testid='rcb-audio-icon-svg']").should('have.css', 'fill', 'rgb(252, 236, 61)'); }); it("Toggles voice", () => { diff --git a/src/components/Buttons/AudioButton/AudioButton.tsx b/src/components/Buttons/AudioButton/AudioButton.tsx index a72bcc01..900190f0 100644 --- a/src/components/Buttons/AudioButton/AudioButton.tsx +++ b/src/components/Buttons/AudioButton/AudioButton.tsx @@ -51,7 +51,10 @@ const AudioButton = () => { return ( IconComponent && - + ) } diff --git a/src/components/Buttons/NotificationButton/NotificationButton.tsx b/src/components/Buttons/NotificationButton/NotificationButton.tsx index d4a8da6c..1d2b0c30 100644 --- a/src/components/Buttons/NotificationButton/NotificationButton.tsx +++ b/src/components/Buttons/NotificationButton/NotificationButton.tsx @@ -53,7 +53,10 @@ const NotificationButton = () => { return ( IconComponent && - + ) } diff --git a/src/components/ChatBotHeader/ChatBotHeader.css b/src/components/ChatBotHeader/ChatBotHeader.css index 64a7a907..ee73a154 100644 --- a/src/components/ChatBotHeader/ChatBotHeader.css +++ b/src/components/ChatBotHeader/ChatBotHeader.css @@ -22,51 +22,4 @@ height: 30px; border-radius: 50%; margin-right: 12px; -} - -/* Notification & Audio Icon */ - -.rcb-notification-icon-on, -.rcb-notification-icon-off, -.rcb-audio-icon-on, -.rcb-audio-icon-off { - position: relative; - display: inline-block; - background-size: cover; - width: 30px; - height: 30px; - border: none; - cursor: pointer; - margin-left: 5px; -} - -.rcb-notification-icon-off, -.rcb-audio-icon-off { - filter: grayscale(100%); -} - -.rcb-notification-icon-on::after, -.rcb-notification-icon-off::after, -.rcb-audio-icon-on::after, -.rcb-audio-icon-off::after { - content: ""; - position: absolute; - top: 50%; - left: 50%; - transform: translate(-50%, -50%); - width: 0; - height: 0; - background-color: rgba(0, 0, 0, 0.1); - border-radius: 50%; - opacity: 0; - transition: width 0.2s ease-out, height 0.2s ease-out, opacity 0.2s ease-out; -} - -.rcb-notification-icon-on:hover::after, -.rcb-notification-icon-off:hover::after, -.rcb-audio-icon-on:hover::after, -.rcb-audio-icon-off:hover::after { - width: 130%; - height: 130%; - opacity: 1; } \ No newline at end of file From c07ce9d9f0a227aaec6fa08e47efa59b1c6ff76d Mon Sep 17 00:00:00 2001 From: tjtanjin Date: Mon, 14 Oct 2024 01:43:00 +0800 Subject: [PATCH 35/36] docs: Update readme --- README.md | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/README.md b/README.md index c811eecd..af954243 100644 --- a/README.md +++ b/README.md @@ -111,6 +111,9 @@ If there are any questions pertaining to the application itself (usage or implem Credits are to be given for the following images: - [Logo](https://www.craiyon.com/) - [Bot Avatar (v1)](https://www.craiyon.com/) +- [Buttons](https://fonts.google.com/) + +Note: Some buttons are hand-drawn. #### Sound Credits are to be given for the notification sound: @@ -119,6 +122,4 @@ Credits are to be given for the notification sound: #### Inspirations As I have used similar alternatives at some point in my developer journey, some inspirations have been taken from them and they ought to be credited here: - [Tidio](https://www.tidio.com/) -- [React Simple Chatbot](https://github.com/LucasBassetti/react-simple-chatbot) - -Note: All other media content are hand-drawn unless otherwise stated, feel free to use them! \ No newline at end of file +- [React Simple Chatbot](https://github.com/LucasBassetti/react-simple-chatbot) \ No newline at end of file From 3c1ac0c087bff1fce7c9aee7a35e045b9dd85f62 Mon Sep 17 00:00:00 2001 From: heinthetaung-dev Date: Sun, 13 Oct 2024 13:53:21 -0400 Subject: [PATCH 36/36] test: add unit test for MessageProcessor service (#202) --- __tests__/services/MessageProcessor.test.ts | 94 +++++++++++++++++++++ 1 file changed, 94 insertions(+) create mode 100644 __tests__/services/MessageProcessor.test.ts diff --git a/__tests__/services/MessageProcessor.test.ts b/__tests__/services/MessageProcessor.test.ts new file mode 100644 index 00000000..3bcdfd3b --- /dev/null +++ b/__tests__/services/MessageProcessor.test.ts @@ -0,0 +1,94 @@ +import { expect } from "@jest/globals"; +import { processMessage } from "../../src/services/BlockService/MessageProcessor"; +import { Params } from "../../src/types/Params"; +import { Block } from "../../src/types/Block"; + +describe("MessageProcessor", () => { + 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; + }); + + // No message in block + it("should not inject message if block has no message", async () => { + await processMessage(mockBlock, mockParams); + + // Make sure injectMessage was not called + expect(mockParams.injectMessage).not.toHaveBeenCalled(); + }); + + // Empty string message + it("should not inject message if block message is an empty string", async () => { + mockBlock.message = " "; + await processMessage(mockBlock, mockParams); + + // Make sure injectMessage was not called if the message just contains whitespace + expect(mockParams.injectMessage).not.toHaveBeenCalled(); + }); + + // Valid string message + it("should inject message if block has a non-empty string message", async () => { + const message = "Test Message"; + mockBlock.message = message; + await processMessage(mockBlock, mockParams); + + // Make sure injectMessage was called with the correct message + expect(mockParams.injectMessage).toHaveBeenCalledWith(message); + }); + + // Function returning invalid content + it("should not inject message if block message is a function returning invalid content", async () => { + const functionResult = null; + mockBlock.message = jest.fn().mockReturnValue(functionResult); + + await processMessage(mockBlock, mockParams); + + // Make sure injectMessage was not called if function returns null + expect(mockParams.injectMessage).not.toHaveBeenCalledWith(functionResult); + }); + + // Function returning valid content + it("should inject message if block message is a function returning valid content", async () => { + const functionResult = "Function Result"; + mockBlock.message = jest.fn().mockReturnValue(functionResult); + + await processMessage(mockBlock, mockParams); + + // Check if the message function was called with correct params + expect(mockBlock.message).toHaveBeenCalledWith(mockParams); + + // Make sure injectMessage was called with the function's return value + expect(mockParams.injectMessage).toHaveBeenCalledWith(functionResult); + }); + + // Function returning a promise with invalid content + it("should not inject message if block message is a function returning a promise with invalid content", async () => { + mockBlock.message = jest.fn().mockResolvedValue(null); + + await processMessage(mockBlock, mockParams); + + // Make sure injectMessage was not called if content is invalid (null) + expect(mockParams.injectMessage).not.toHaveBeenCalled(); + }); + + // Function returning a promise with valid content + it("should inject message if block message is a function returning a promise with valid content", async () => { + const promiseResult = "Async Function Result"; + mockBlock.message = jest.fn().mockResolvedValue(promiseResult); + + await processMessage(mockBlock, mockParams); + + // Check if the message function was called with correct params + expect(mockBlock.message).toHaveBeenCalledWith(mockParams); + + // Make sure injectMessage was called with the resolved promise value + expect(mockParams.injectMessage).toHaveBeenCalledWith(promiseResult); + }); +});