Skip to content

Commit

Permalink
test: Add initial unit tests
Browse files Browse the repository at this point in the history
  • Loading branch information
tjtanjin committed Oct 1, 2024
1 parent 85acd70 commit 55fe03a
Show file tree
Hide file tree
Showing 20 changed files with 7,991 additions and 2,159 deletions.
2 changes: 1 addition & 1 deletion .github/workflows/integration_test.yml
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@ run-name: Integration Test

on:
workflow_run:
workflows: [Build]
workflows: [Unit Test]
types: [completed]
branches:
- main
Expand Down
40 changes: 40 additions & 0 deletions .github/workflows/unit_test.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
name: Unit Test
run-name: Unit Test

on:
workflow_run:
workflows: [Build]
types: [completed]
branches:
- main
workflow_dispatch:

jobs:
unit-test:
name: Run Unit Test
runs-on: ubuntu-latest

if: ${{ github.event.workflow_run.conclusion == 'success' }}
steps:
- name: Checkout code
uses: actions/checkout@v4

- name: Setup Node.js
uses: actions/setup-node@v4
with:
node-version: '21.7.3'

- name: Install Dependencies
run: npm ci

- name: Install React 18.3.1
run: |
npm uninstall react react-dom
npm install [email protected] [email protected]
- name: Start App
run: |
npm run start &
- name: Run Unit Tests
run: npm run test
3 changes: 3 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -26,5 +26,8 @@ yarn-error.log*
# cypress
/cypress/screenshots

# jest
/coverage

# temp file generated for ssr test
./ssr/ssrTestIndexNoCssImport.js
1 change: 1 addition & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@
<p align="center">
<a href="https://github.com/tjtanjin/react-chatbotify/actions/workflows/lint.yml"> <img src="https://github.com/tjtanjin/react-chatbotify/actions/workflows/lint.yml/badge.svg" /> </a>
<a href="https://github.com/tjtanjin/react-chatbotify/actions/workflows/build.yml"> <img src="https://github.com/tjtanjin/react-chatbotify/actions/workflows/build.yml/badge.svg" /> </a>
<a href="https://github.com/tjtanjin/react-chatbotify/actions/workflows/unit_test.yml"> <img src="https://github.com/tjtanjin/react-chatbotify/actions/workflows/unit_test.yml/badge.svg" /> </a>
<a href="https://github.com/tjtanjin/react-chatbotify/actions/workflows/integration_test.yml"> <img src="https://github.com/tjtanjin/react-chatbotify/actions/workflows/integration_test.yml/badge.svg" /> </a>
<a href="https://github.com/tjtanjin/react-chatbotify/actions/workflows/library_test.yml"> <img src="https://github.com/tjtanjin/react-chatbotify/actions/workflows/library_test.yml/badge.svg" /> </a>
<a href="https://www.npmjs.com/package/react-chatbotify"> <img src="https://img.shields.io/npm/v/react-chatbotify?logo=semver&label=version&color=%2331c854" /> </a>
Expand Down
62 changes: 62 additions & 0 deletions __tests__/__mocks__/TestChatBotProvider.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,62 @@
import React, { createContext, useState } from 'react';
import { SettingsProvider } from '../../src/context/SettingsContext';
import { StylesProvider } from '../../src/context/StylesContext';
import { ToastsProvider } from '../../src/context/ToastsContext';
import { BotRefsProvider } from '../../src/context/BotRefsContext';
import { BotStatesProvider } from '../../src/context/BotStatesContext';
import { PathsProvider } from '../../src/context/PathsContext';
import { MessagesProvider } from '../../src/context/MessagesContext';
import { ChatBotProviderContextType } from '../../src/context/ChatBotProvider';
import { Settings } from '../../src/types/Settings';
import { Styles } from '../../src/types/Styles';
import { DefaultStyles } from '../../src/constants/internal/DefaultStyles';
import { MockDefaultSettings } from './constants';

/**
* Defines the structure for TestChatBotProvider's props.
*/
type TestChatBotProviderProps = {
children: React.ReactNode;
initialSettings?: Partial<Settings>;
initialStyles?: Partial<Styles>;
// Add other initial states or props as needed
};
// Create a context to detect whether ChatBotProvider is present
const ChatBotContext = createContext<ChatBotProviderContextType | undefined>(undefined);

/**
* TestChatBotProvider component that wraps children with all necessary context providers.
*/
const TestChatBotProvider: React.FC<TestChatBotProviderProps> = ({
children,
initialSettings = MockDefaultSettings,
initialStyles = DefaultStyles,
}) => {
// Initialize settings state
const [settings, setSettings] = useState<Settings>(initialSettings);

// Initialize styles state
const [styles, setStyles] = useState<Styles>(initialStyles);

return (
<ChatBotContext.Provider value={{ loadConfig: jest.fn() }}>
<SettingsProvider settings={settings} setSettings={setSettings}>
<StylesProvider styles={styles} setStyles={setStyles}>
<ToastsProvider>
<BotRefsProvider botIdRef={{ current: '' }} flowRef={{ current: {} }}>
<PathsProvider>
<BotStatesProvider settings={{ ...initialSettings }}>
<MessagesProvider>
{children}
</MessagesProvider>
</BotStatesProvider>
</PathsProvider>
</BotRefsProvider>
</ToastsProvider>
</StylesProvider>
</SettingsProvider>
</ChatBotContext.Provider>
);
};

export { TestChatBotProvider };
25 changes: 25 additions & 0 deletions __tests__/__mocks__/constants.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
import { DefaultSettings } from "../../src/constants/internal/DefaultSettings";

export const MockDefaultSettings = {
...DefaultSettings,
// enable all events for testing
event: {
rcbPreInjectMessage: true,
rcbPostInjectMessage: true,
rcbStartStreamMessage: true,
rcbChunkStreamMessage: true,
rcbStopStreamMessage: true,
rcbRemoveMessage: true,
rcbLoadChatHistory: true,
rcbToggleChatWindow: true,
rcbToggleAudio: true,
rcbToggleNotifications: true,
rcbToggleVoice: true,
rcbChangePath: true,
rcbShowToast: true,
rcbDismissToast: true,
rcbUserSubmitText: true,
rcbUserUploadFile: true,
rcbTextAreaChangeValue: true,
}
}
55 changes: 55 additions & 0 deletions __tests__/__mocks__/context/BotStatesContext.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,55 @@
import { BotStatesContextType } from '../../../src/context/BotStatesContext'; // Adjust the path to the correct location

// Mock implementation of the `useBotStatesContext` function
export const useBotStatesContext = jest.fn<BotStatesContextType, []>(() => ({
isBotTyping: false,
setIsBotTyping: jest.fn(),

isChatWindowOpen: false,
setIsChatWindowOpen: jest.fn(),

audioToggledOn: false,
setAudioToggledOn: jest.fn(),

voiceToggledOn: false,
setVoiceToggledOn: jest.fn(),

notificationsToggledOn: true,
setNotificationsToggledOn: jest.fn(),

isLoadingChatHistory: false,
setIsLoadingChatHistory: jest.fn(),

isScrolling: false,
setIsScrolling: jest.fn(),

textAreaDisabled: true,
setTextAreaDisabled: jest.fn(),

textAreaSensitiveMode: false,
setTextAreaSensitiveMode: jest.fn(),

hasInteractedPage: false,
setHasInteractedPage: jest.fn(),

hasFlowStarted: false,
setHasFlowStarted: jest.fn(),

unreadCount: 0,
setUnreadCount: jest.fn(),

inputLength: 0,
setInputLength: jest.fn(),

blockAllowsAttachment: false,
setBlockAllowsAttachment: jest.fn(),

timeoutId: null,
setTimeoutId: jest.fn(),

viewportHeight: window.innerHeight,
setViewportHeight: jest.fn(),

viewportWidth: window.innerWidth,
setViewportWidth: jest.fn(),
}));
1 change: 1 addition & 0 deletions __tests__/__mocks__/context/SettingsContext.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
export const useSettingsContext = jest.fn();
2 changes: 2 additions & 0 deletions __tests__/__mocks__/fileMock.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
// __mocks__/fileMock.ts
export default "";
3 changes: 3 additions & 0 deletions __tests__/__mocks__/hooks/useRcbEventInternal.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
export const useRcbEventInternal = jest.fn(() => ({
callRcbEvent: jest.fn().mockReturnValue({ defaultPrevented: false }),
}));
84 changes: 84 additions & 0 deletions __tests__/hooks/useAudioInternal.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,84 @@
import { renderHook, act } from "@testing-library/react";

import { useAudioInternal } from "../../src/hooks/internal/useAudioInternal";
import { useRcbEventInternal } from "../../src/hooks/internal/useRcbEventInternal";
import { RcbEvent } from "../../src/constants/RcbEvent";

import { TestChatBotProvider } from "../__mocks__/TestChatBotProvider";
import { MockDefaultSettings } from "../__mocks__/constants";

// mocks internal hooks
jest.mock("../../src/hooks/internal/useRcbEventInternal");
const mockUseRcbEventInternal = useRcbEventInternal as jest.MockedFunction<typeof useRcbEventInternal>;

/**
* Test for useAudioInternal hook.
*/
describe("useAudioInternal Hook", () => {
beforeEach(() => {
jest.clearAllMocks();
});

// initial values
const initialAudioToggledOn = MockDefaultSettings.audio?.defaultToggledOn;

it("should toggle audio correctly, change state and emit rcb-toggle-audio event", () => {
// mocks rcb event handler
const callRcbEventMock = jest.fn().mockReturnValue({ defaultPrevented: false });
mockUseRcbEventInternal.mockReturnValue({
callRcbEvent: callRcbEventMock,
});

// renders the hook within the TestChatBotProvider
const { result } = renderHook(() => useAudioInternal(), {
wrapper: TestChatBotProvider,
});

// checks initial value
expect(result.current.audioToggledOn).toBe(initialAudioToggledOn);

// simulates clicking the toggle action
act(() => {
result.current.toggleAudio();
});

// checks if callRcbEvent was called with rcb-toggle-audio and correct arguments
expect(callRcbEventMock).toHaveBeenCalledWith(RcbEvent.TOGGLE_AUDIO, {
currState: initialAudioToggledOn,
newState: !initialAudioToggledOn,
});

// checks if audio state was updated
expect(result.current.audioToggledOn).toBe(!initialAudioToggledOn);
});

it("should prevent toggling when event is defaultPrevented", () => {
// mocks rcb event handler
const callRcbEventMock = jest.fn().mockReturnValue({ defaultPrevented: true });
mockUseRcbEventInternal.mockReturnValue({
callRcbEvent: callRcbEventMock,
});

// renders the hook within the TestChatBotProvider
const { result } = renderHook(() => useAudioInternal(), {
wrapper: TestChatBotProvider,
});

// checks initial state
expect(result.current.audioToggledOn).toBe(initialAudioToggledOn);

// simulates clicking the toggle action
act(() => {
result.current.toggleAudio();
});

// checks if callRcbEvent was called with rcb-toggle-audio and correct arguments
expect(callRcbEventMock).toHaveBeenCalledWith(RcbEvent.TOGGLE_AUDIO, {
currState: initialAudioToggledOn,
newState: !initialAudioToggledOn,
});

// checks if audio state stayed the same
expect(result.current.audioToggledOn).toBe(initialAudioToggledOn);
});
});
Loading

0 comments on commit 55fe03a

Please sign in to comment.