Skip to content

Commit

Permalink
ISSUE-282 keep history messages in sync with storage (#283)
Browse files Browse the repository at this point in the history
Co-authored-by: Alex Peelman <alex.peelman@cashforce.com>
  • Loading branch information
alexpeelman and Alex Peelman authored Feb 11, 2025
1 parent 2747c08 commit 8f16e2f
Show file tree
Hide file tree
Showing 4 changed files with 121 additions and 101 deletions.
196 changes: 104 additions & 92 deletions __tests__/hooks/internal/useFlowInternal.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,106 +7,118 @@ import { usePathsContext } from "../../../src/context/PathsContext";
import { useToastsContext } from "../../../src/context/ToastsContext";
import { useBotStatesContext } from "../../../src/context/BotStatesContext";
import { useBotRefsContext } from "../../../src/context/BotRefsContext";
import { useSettingsContext } from "../../../src/context/SettingsContext";
import { setHistoryStorageValues } from "../../../src/services/ChatHistoryService";

jest.mock("../../../src/context/MessagesContext");
jest.mock("../../../src/context/PathsContext");
jest.mock("../../../src/context/ToastsContext");
jest.mock("../../../src/context/BotStatesContext");
jest.mock("../../../src/context/BotRefsContext");
jest.mock("../../../src/context/SettingsContext");
jest.mock("../../../src/services/ChatHistoryService");

describe("useFlowInternal Hook", () => {
const setMessagesMock = jest.fn();
const setPathsMock = jest.fn();
const setPathsMock = jest.fn();
const setToastsMock = jest.fn();
const flowRefMock = { current: { id: "test-flow" } };
const hasFlowStartedMock = true;

beforeEach(() => {
jest.clearAllMocks();


(useMessagesContext as jest.Mock).mockReturnValue({ setMessages: setMessagesMock });
(usePathsContext as jest.Mock).mockReturnValue({ setPaths: setPathsMock });
(useToastsContext as jest.Mock).mockReturnValue({ setToasts: setToastsMock });
(useBotRefsContext as jest.Mock).mockReturnValue({ flowRef: flowRefMock });
(useBotStatesContext as jest.Mock).mockReturnValue({ hasFlowStarted: hasFlowStartedMock });
});


// Test to ensure initial values (hasFlowStarted and flowRef) are returned correctly from the hook
it("should return initial values from context", () => {
const { result } = renderHook(() => useFlowInternal());

expect(result.current.hasFlowStarted).toBe(true);
expect(result.current.getFlow()).toEqual({ id: "test-flow" });
});

// Test to ensure that restartFlow clears messages, toasts, and resets paths
it("should restart the flow by clearing messages, toasts, and resetting paths", () => {
const { result } = renderHook(() => useFlowInternal());

act(() => {
result.current.restartFlow();
});

expect(setMessagesMock).toHaveBeenCalledWith([]);
expect(setToastsMock).toHaveBeenCalledWith([]);
expect(setPathsMock).toHaveBeenCalledWith(["start"]);
});

// Test to ensure that getFlow returns the current flow from flowRef
it("should get the current flow from flowRef", () => {
const { result } = renderHook(() => useFlowInternal());

const flow = result.current.getFlow();
expect(flow).toEqual({ id: "test-flow" });
});

// Test to ensure that calling restartFlow multiple times works correctly
it("should handle multiple restarts correctly", () => {
const { result } = renderHook(() => useFlowInternal());

act(() => {
result.current.restartFlow();
});

act(() => {
result.current.restartFlow();
});

expect(setMessagesMock).toHaveBeenCalledTimes(2);
expect(setToastsMock).toHaveBeenCalledTimes(2);
expect(setPathsMock).toHaveBeenCalledTimes(2);
});

// Test to check flow state when hasFlowStarted is false
it("should correctly reflect flow state when it hasn't started", () => {
(useBotStatesContext as jest.Mock).mockReturnValue({ hasFlowStarted: false });
const { result } = renderHook(() => useFlowInternal());

expect(result.current.hasFlowStarted).toBe(false);
});

// Test to ensure messages, toasts, and paths are initialized correctly when restarting the flow
it("should initialize messages, toasts, and paths correctly", () => {
const { result } = renderHook(() => useFlowInternal());

act(() => {
result.current.restartFlow();
});

expect(setMessagesMock).toHaveBeenCalledWith([]);
expect(setToastsMock).toHaveBeenCalledWith([]);
expect(setPathsMock).toHaveBeenCalledWith(["start"]);
});

// Test to check that getFlow returns different flowRef values correctly
it("should handle different flowRef values", () => {
const differentFlowRefMock = { current: { id: "different-flow" } };
(useBotRefsContext as jest.Mock).mockReturnValue({ flowRef: differentFlowRefMock });
const { result } = renderHook(() => useFlowInternal());

const flow = result.current.getFlow();
expect(flow).toEqual({ id: "different-flow" });
});
const flowRefMock = { current: { id: "test-flow" } };
const hasFlowStartedMock = true;
const mockSettings = {
chatHistory: {
storageType: "localStorage",
storageKey: "rcb-history",
},
};

beforeEach(() => {
jest.clearAllMocks();

(useMessagesContext as jest.Mock).mockReturnValue({ setMessages: setMessagesMock });
(usePathsContext as jest.Mock).mockReturnValue({ setPaths: setPathsMock });
(useToastsContext as jest.Mock).mockReturnValue({ setToasts: setToastsMock });
(useBotRefsContext as jest.Mock).mockReturnValue({ flowRef: flowRefMock });
(useBotStatesContext as jest.Mock).mockReturnValue({ hasFlowStarted: hasFlowStartedMock });
(useSettingsContext as jest.Mock).mockReturnValue({ settings: mockSettings });
});


// Test to ensure initial values (hasFlowStarted and flowRef) are returned correctly from the hook
it("should return initial values from context", () => {
const { result } = renderHook(() => useFlowInternal());

expect(result.current.hasFlowStarted).toBe(true);
expect(result.current.getFlow()).toEqual({ id: "test-flow" });
});

// Test to ensure that restartFlow clears messages, toasts, and resets paths
it("should restart the flow by clearing messages, toasts, resetting paths and loading history", () => {
const { result } = renderHook(() => useFlowInternal());

act(() => {
result.current.restartFlow();
});

expect(setMessagesMock).toHaveBeenCalledWith([]);
expect(setToastsMock).toHaveBeenCalledWith([]);
expect(setPathsMock).toHaveBeenCalledWith(["start"]);
expect(setHistoryStorageValues)
.toHaveBeenCalledWith({ chatHistory: { storageType: "localStorage", storageKey: "rcb-history" } });
});

// Test to ensure that getFlow returns the current flow from flowRef
it("should get the current flow from flowRef", () => {
const { result } = renderHook(() => useFlowInternal());

const flow = result.current.getFlow();
expect(flow).toEqual({ id: "test-flow" });
});

// Test to ensure that calling restartFlow multiple times works correctly
it("should handle multiple restarts correctly", () => {
const { result } = renderHook(() => useFlowInternal());

act(() => {
result.current.restartFlow();
});

act(() => {
result.current.restartFlow();
});

expect(setMessagesMock).toHaveBeenCalledTimes(2);
expect(setToastsMock).toHaveBeenCalledTimes(2);
expect(setPathsMock).toHaveBeenCalledTimes(2);
});

// Test to check flow state when hasFlowStarted is false
it("should correctly reflect flow state when it hasn't started", () => {
(useBotStatesContext as jest.Mock).mockReturnValue({ hasFlowStarted: false });
const { result } = renderHook(() => useFlowInternal());

expect(result.current.hasFlowStarted).toBe(false);
});

// Test to ensure messages, toasts, and paths are initialized correctly when restarting the flow
it("should initialize messages, toasts, and paths correctly", () => {
const { result } = renderHook(() => useFlowInternal());

act(() => {
result.current.restartFlow();
});

expect(setMessagesMock).toHaveBeenCalledWith([]);
expect(setToastsMock).toHaveBeenCalledWith([]);
expect(setPathsMock).toHaveBeenCalledWith(["start"]);
});

// Test to check that getFlow returns different flowRef values correctly
it("should handle different flowRef values", () => {
const differentFlowRefMock = { current: { id: "different-flow" } };
(useBotRefsContext as jest.Mock).mockReturnValue({ flowRef: differentFlowRefMock });
const { result } = renderHook(() => useFlowInternal());

const flow = result.current.getFlow();
expect(flow).toEqual({ id: "different-flow" });
});
});
2 changes: 1 addition & 1 deletion __tests__/services/ChatHistoryService.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -66,7 +66,7 @@ describe("ChatHistoryService", () => {
it("should save history if not disabled", async () => {
setHistoryStorageValues(mockSettings(storageType));
await saveChatHistory([mockMessage]);

expect(storage.setItem).toHaveBeenCalled();
});

Expand Down
22 changes: 15 additions & 7 deletions src/hooks/internal/useFlowInternal.ts
Original file line number Diff line number Diff line change
@@ -1,10 +1,12 @@
import { useCallback } from "react";
import {useCallback} from "react";

import { useMessagesInternal } from "./useMessagesInternal";
import { usePathsInternal } from "./usePathsInternal";
import { useToastsInternal } from "./useToastsInternal";
import { useBotRefsContext } from "../../context/BotRefsContext";
import { useBotStatesContext } from "../../context/BotStatesContext";
import {useMessagesInternal} from "./useMessagesInternal";
import {usePathsInternal} from "./usePathsInternal";
import {useToastsInternal} from "./useToastsInternal";
import {useBotRefsContext} from "../../context/BotRefsContext";
import {useBotStatesContext} from "../../context/BotStatesContext";
import {setHistoryStorageValues} from "../../services/ChatHistoryService";
import {useSettingsContext} from "../../context/SettingsContext";

/**
* Internal custom hook for managing flow.
Expand All @@ -24,15 +26,21 @@ export const useFlowInternal = () => {

// handles bot refs
const { flowRef } = useBotRefsContext();

// handles bot settings
const { settings } = useSettingsContext();

/**
* Restarts the conversation flow for the chatbot.
*/
const restartFlow = useCallback(() => {
//reload the chat history from storage
setHistoryStorageValues(settings)

replaceMessages([]);
replaceToasts([]);
replacePaths(["start"]);
}, [replaceMessages, replaceToasts, replacePaths]);
}, [replaceMessages, replaceToasts, replacePaths, setHistoryStorageValues]);

/**
* Retrieves the conversation flow for the chatbot.
Expand Down
2 changes: 1 addition & 1 deletion src/hooks/internal/useMessagesInternal.ts
Original file line number Diff line number Diff line change
Expand Up @@ -323,7 +323,7 @@ export const useMessagesInternal = () => {
// defer update to next event loop, handles edge case where messages are sent too fast
// and the scrolling does not properly reach the bottom
setTimeout(() => {
if (!chatBodyRef.current) {
if (!chatBodyRef?.current) {
return;
}

Expand Down

0 comments on commit 8f16e2f

Please sign in to comment.