diff --git a/apps/test-site/src/App.tsx b/apps/test-site/src/App.tsx index b9c4372..780e445 100644 --- a/apps/test-site/src/App.tsx +++ b/apps/test-site/src/App.tsx @@ -24,6 +24,7 @@ function App() { function ChatComponent() { const isLoading = useChatState((s) => s.conversation.isLoading); + const canSendMessage = useChatState((s) => s.conversation.canSendMessage); const messages = useChatState((s) => s.conversation.messages); const [input, setInput] = useState(""); const actions = useChatActions(); @@ -57,7 +58,12 @@ function ChatComponent() {

{`${m.source}: ${m.text}`}

))} {isLoading &&

loading...

} - + diff --git a/package-lock.json b/package-lock.json index 58cffb5..3730a5c 100644 --- a/package-lock.json +++ b/package-lock.json @@ -18447,7 +18447,7 @@ }, "packages/chat-headless": { "name": "@yext/chat-headless", - "version": "0.3.0", + "version": "0.3.1", "license": "BSD-3-Clause", "dependencies": { "@reduxjs/toolkit": "^1.9.5", diff --git a/packages/chat-headless/docs/chat-headless.chatheadless._constructor_.md b/packages/chat-headless/docs/chat-headless.chatheadless._constructor_.md index 77d2f02..c8c1ae1 100644 --- a/packages/chat-headless/docs/chat-headless.chatheadless._constructor_.md +++ b/packages/chat-headless/docs/chat-headless.chatheadless._constructor_.md @@ -9,13 +9,12 @@ Constructs a new instance of the [ChatHeadless](./chat-headless.chatheadless.md) **Signature:** ```typescript -constructor(config: ChatConfig, saveToSessionStorage?: boolean); +constructor(config: HeadlessConfig); ``` ## Parameters | Parameter | Type | Description | | --- | --- | --- | -| config | ChatConfig | The configuration for the [ChatHeadless](./chat-headless.chatheadless.md) instance | -| saveToSessionStorage | boolean | _(Optional)_ Whether to save the instance's [ConversationState](./chat-headless.conversationstate.md) to session storage. Defaults to true. | +| config | [HeadlessConfig](./chat-headless.headlessconfig.md) | The configuration for the [ChatHeadless](./chat-headless.chatheadless.md) instance | diff --git a/packages/chat-headless/docs/chat-headless.chatheadless.getnextmessage.md b/packages/chat-headless/docs/chat-headless.chatheadless.getnextmessage.md index f0f4bd9..7a430e7 100644 --- a/packages/chat-headless/docs/chat-headless.chatheadless.getnextmessage.md +++ b/packages/chat-headless/docs/chat-headless.chatheadless.getnextmessage.md @@ -9,7 +9,7 @@ Performs a Chat API request for the next message generated by chat bot using the **Signature:** ```typescript -getNextMessage(text?: string, source?: MessageSource): Promise; +getNextMessage(text?: string, source?: MessageSource): Promise; ``` ## Parameters @@ -21,7 +21,7 @@ getNextMessage(text?: string, source?: MessageSource): Promise; **Returns:** -Promise<MessageResponse> +Promise<MessageResponse \| undefined> a Promise of a response from the Chat API diff --git a/packages/chat-headless/docs/chat-headless.chatheadless.md b/packages/chat-headless/docs/chat-headless.chatheadless.md index cb6e6bc..7bcc37c 100644 --- a/packages/chat-headless/docs/chat-headless.chatheadless.md +++ b/packages/chat-headless/docs/chat-headless.chatheadless.md @@ -16,7 +16,7 @@ export declare class ChatHeadless | Constructor | Modifiers | Description | | --- | --- | --- | -| [(constructor)(config, saveToSessionStorage)](./chat-headless.chatheadless._constructor_.md) | | Constructs a new instance of the [ChatHeadless](./chat-headless.chatheadless.md) class. | +| [(constructor)(config)](./chat-headless.chatheadless._constructor_.md) | | Constructs a new instance of the [ChatHeadless](./chat-headless.chatheadless.md) class. | ## Properties diff --git a/packages/chat-headless/docs/chat-headless.chatheadless.streamnextmessage.md b/packages/chat-headless/docs/chat-headless.chatheadless.streamnextmessage.md index 6a9bdb4..99ef110 100644 --- a/packages/chat-headless/docs/chat-headless.chatheadless.streamnextmessage.md +++ b/packages/chat-headless/docs/chat-headless.chatheadless.streamnextmessage.md @@ -9,7 +9,7 @@ Performs a Chat Stream API request for the next message generated by chat bot us **Signature:** ```typescript -streamNextMessage(text?: string, source?: MessageSource): Promise; +streamNextMessage(text?: string, source?: MessageSource): Promise; ``` ## Parameters @@ -21,7 +21,7 @@ streamNextMessage(text?: string, source?: MessageSource): Promise + +[Home](./index.md) > [@yext/chat-headless](./chat-headless.md) > [ConversationState](./chat-headless.conversationstate.md) > [canSendMessage](./chat-headless.conversationstate.cansendmessage.md) + +## ConversationState.canSendMessage property + +Whether a new message can be sent to Chat API. This is set to false when a previous message is being processed. + +**Signature:** + +```typescript +canSendMessage: boolean; +``` diff --git a/packages/chat-headless/docs/chat-headless.conversationstate.isloading.md b/packages/chat-headless/docs/chat-headless.conversationstate.isloading.md index 1eda62d..25cd1da 100644 --- a/packages/chat-headless/docs/chat-headless.conversationstate.isloading.md +++ b/packages/chat-headless/docs/chat-headless.conversationstate.isloading.md @@ -4,10 +4,10 @@ ## ConversationState.isLoading property -Whether the next message is currently processing or has finished processing. +Whether the next message is currently processing or has started responding. **Signature:** ```typescript -isLoading?: boolean; +isLoading: boolean; ``` diff --git a/packages/chat-headless/docs/chat-headless.conversationstate.md b/packages/chat-headless/docs/chat-headless.conversationstate.md index c1a00a6..790af63 100644 --- a/packages/chat-headless/docs/chat-headless.conversationstate.md +++ b/packages/chat-headless/docs/chat-headless.conversationstate.md @@ -16,8 +16,9 @@ export interface ConversationState | Property | Modifiers | Type | Description | | --- | --- | --- | --- | +| [canSendMessage](./chat-headless.conversationstate.cansendmessage.md) | | boolean | Whether a new message can be sent to Chat API. This is set to false when a previous message is being processed. | | [conversationId?](./chat-headless.conversationstate.conversationid.md) | | string | _(Optional)_ The id of the current conversation. | -| [isLoading?](./chat-headless.conversationstate.isloading.md) | | boolean | _(Optional)_ Whether the next message is currently processing or has finished processing. | +| [isLoading](./chat-headless.conversationstate.isloading.md) | | boolean | Whether the next message is currently processing or has started responding. | | [messages](./chat-headless.conversationstate.messages.md) | | Message\[\] | The messages in a conversation. | | [notes?](./chat-headless.conversationstate.notes.md) | | MessageNotes | _(Optional)_ Information relevant to the current state of the conversation, generated and provided by Chat API. | diff --git a/packages/chat-headless/docs/chat-headless.headlessconfig.md b/packages/chat-headless/docs/chat-headless.headlessconfig.md new file mode 100644 index 0000000..da5a390 --- /dev/null +++ b/packages/chat-headless/docs/chat-headless.headlessconfig.md @@ -0,0 +1,21 @@ + + +[Home](./index.md) > [@yext/chat-headless](./chat-headless.md) > [HeadlessConfig](./chat-headless.headlessconfig.md) + +## HeadlessConfig interface + +The configuration for a SearchHeadless instance. + +**Signature:** + +```typescript +export interface HeadlessConfig extends ChatConfig +``` +**Extends:** ChatConfig + +## Properties + +| Property | Modifiers | Type | Description | +| --- | --- | --- | --- | +| [saveToSessionStorage?](./chat-headless.headlessconfig.savetosessionstorage.md) | | boolean | _(Optional)_ Whether to save the instance's [ConversationState](./chat-headless.conversationstate.md) to session storage. Defaults to true. | + diff --git a/packages/chat-headless/docs/chat-headless.headlessconfig.savetosessionstorage.md b/packages/chat-headless/docs/chat-headless.headlessconfig.savetosessionstorage.md new file mode 100644 index 0000000..08cba97 --- /dev/null +++ b/packages/chat-headless/docs/chat-headless.headlessconfig.savetosessionstorage.md @@ -0,0 +1,13 @@ + + +[Home](./index.md) > [@yext/chat-headless](./chat-headless.md) > [HeadlessConfig](./chat-headless.headlessconfig.md) > [saveToSessionStorage](./chat-headless.headlessconfig.savetosessionstorage.md) + +## HeadlessConfig.saveToSessionStorage property + +Whether to save the instance's [ConversationState](./chat-headless.conversationstate.md) to session storage. Defaults to true. + +**Signature:** + +```typescript +saveToSessionStorage?: boolean; +``` diff --git a/packages/chat-headless/docs/chat-headless.md b/packages/chat-headless/docs/chat-headless.md index e79d723..3b0d023 100644 --- a/packages/chat-headless/docs/chat-headless.md +++ b/packages/chat-headless/docs/chat-headless.md @@ -15,6 +15,7 @@ | Interface | Description | | --- | --- | | [ConversationState](./chat-headless.conversationstate.md) | Maintains the data for the current conversation. | +| [HeadlessConfig](./chat-headless.headlessconfig.md) | The configuration for a SearchHeadless instance. | | [MetaState](./chat-headless.metastate.md) | Maintains the metadata for Chat Headless. | | [State](./chat-headless.state.md) | The state representing a ChatHeadless instance. | | [StateListener](./chat-headless.statelistener.md) | Represents a listener for a specific value of type T in the state. | diff --git a/packages/chat-headless/etc/chat-headless.api.md b/packages/chat-headless/etc/chat-headless.api.md index 974dd76..b16d23f 100644 --- a/packages/chat-headless/etc/chat-headless.api.md +++ b/packages/chat-headless/etc/chat-headless.api.md @@ -25,9 +25,9 @@ export { ChatConfig } // @public export class ChatHeadless { - constructor(config: ChatConfig, saveToSessionStorage?: boolean); + constructor(config: HeadlessConfig); addListener(listener: StateListener): Unsubscribe; - getNextMessage(text?: string, source?: MessageSource): Promise; + getNextMessage(text?: string, source?: MessageSource): Promise; restartConversation(): void; setChatLoadingStatus(isLoading: boolean): void; setContext(context: any): void; @@ -37,19 +37,25 @@ export class ChatHeadless { get state(): State; // @internal get store(): Store; - streamNextMessage(text?: string, source?: MessageSource): Promise; + streamNextMessage(text?: string, source?: MessageSource): Promise; } // @public export interface ConversationState { + canSendMessage: boolean; conversationId?: string; - isLoading?: boolean; + isLoading: boolean; messages: Message[]; notes?: MessageNotes; } export { EndEvent } +// @public +export interface HeadlessConfig extends ChatConfig { + saveToSessionStorage?: boolean; +} + export { Message } export { MessageNotes } diff --git a/packages/chat-headless/package.json b/packages/chat-headless/package.json index 0be208d..a72a63c 100644 --- a/packages/chat-headless/package.json +++ b/packages/chat-headless/package.json @@ -1,6 +1,6 @@ { "name": "@yext/chat-headless", - "version": "0.3.0", + "version": "0.3.1", "description": "A library for powering UI components for Yext Chat integrations", "main": "./dist/commonjs/index.js", "module": "./dist/esm/index.js", diff --git a/packages/chat-headless/src/ChatHeadless.ts b/packages/chat-headless/src/ChatHeadless.ts index 4aa6dda..c454b3f 100644 --- a/packages/chat-headless/src/ChatHeadless.ts +++ b/packages/chat-headless/src/ChatHeadless.ts @@ -1,5 +1,4 @@ import { - ChatConfig, ChatCore, Message, MessageNotes, @@ -11,6 +10,7 @@ import { State } from "./models/state"; import { ReduxStateManager } from "./ReduxStateManager"; import { loadSessionState, + setCanSendMessage, setConversationId, setIsLoading, setMessageNotes, @@ -20,6 +20,7 @@ import { import { Store, Unsubscribe } from "@reduxjs/toolkit"; import { StateListener } from "./models"; import { setContext } from "./slices/meta"; +import { HeadlessConfig } from "./models/HeadlessConfig"; /** * Provides the functionality for interacting with a Chat Bot @@ -37,12 +38,15 @@ export class ChatHeadless { * @public * * @param config - The configuration for the {@link ChatHeadless} instance - * @param saveToSessionStorage - Whether to save the instance's {@link ConversationState} to session storage. Defaults to true. */ - constructor(config: ChatConfig, saveToSessionStorage = true) { - this.chatCore = new ChatCore(config); + constructor(config: HeadlessConfig) { + const defaultConfig: Partial = { + saveToSessionStorage: true, + }; + const mergedConfig = { ...defaultConfig, ...config }; + this.chatCore = new ChatCore(mergedConfig); this.stateManager = new ReduxStateManager(); - if (saveToSessionStorage) { + if (mergedConfig.saveToSessionStorage) { this.setState({ ...this.state, conversation: loadSessionState(), @@ -149,6 +153,17 @@ export class ChatHeadless { this.stateManager.dispatch(setConversationId(id)); } + /** + * Sets {@link ConversationState.canSendMessage} to the specified state + * + * @internal + * + * @param canSendMessage - the state to set + */ + private setCanSendMessage(canSendMessage: boolean) { + this.stateManager.dispatch(setCanSendMessage(canSendMessage)); + } + /** * Resets all fields within {@link ConversationState} * @@ -157,6 +172,7 @@ export class ChatHeadless { restartConversation() { this.setConversationId(undefined); this.setChatLoadingStatus(false); + this.setCanSendMessage(true); this.setMessageNotes({}); this.setMessages([]); } @@ -190,7 +206,7 @@ export class ChatHeadless { async getNextMessage( text?: string, source: MessageSource = MessageSource.USER - ): Promise { + ): Promise { return this.nextMessageHandler( async () => { const { messages, conversationId, notes } = this.state.conversation; @@ -229,7 +245,7 @@ export class ChatHeadless { async streamNextMessage( text?: string, source: MessageSource = MessageSource.USER - ): Promise { + ): Promise { return this.nextMessageHandler( async () => { let messageResponse: MessageResponse | undefined = undefined; @@ -245,6 +261,7 @@ export class ChatHeadless { context: this.state.meta.context, }); stream.addEventListener(StreamEventName.StartEvent, ({ data }) => { + this.setChatLoadingStatus(false); this.setMessageNotes(data); }); stream.addEventListener( @@ -277,8 +294,10 @@ export class ChatHeadless { /** * Setup relevant state before hitting Chat API endpoint for next message, such as - * setting loading status and appending new user's message in conversation state. - * Also update loading state when the next message is received or an error occurred. + * setting loading status, "canSendMessage" status, and appending new user's message + * in conversation state. + * + * @internal * * @param nextMessageFn - function to invoke to get next message * @param text - the text of the next message @@ -289,7 +308,14 @@ export class ChatHeadless { nextMessageFn: () => Promise, text?: string, source: MessageSource = MessageSource.USER - ): Promise { + ): Promise { + if (!this.state.conversation.canSendMessage) { + console.warn( + "Unable to process new message at the moment. Another message is still being processed." + ); + return; + } + this.setCanSendMessage(false); this.setChatLoadingStatus(true); let messages: Message[] = this.state.conversation.messages; if (text && text.length > 0) { @@ -307,9 +333,11 @@ export class ChatHeadless { try { messageResponse = await nextMessageFn(); } catch (e) { + this.setCanSendMessage(true); this.setChatLoadingStatus(false); return Promise.reject(e as Error); } + this.setCanSendMessage(true); this.setChatLoadingStatus(false); return messageResponse; } diff --git a/packages/chat-headless/src/models/HeadlessConfig.ts b/packages/chat-headless/src/models/HeadlessConfig.ts new file mode 100644 index 0000000..51289f8 --- /dev/null +++ b/packages/chat-headless/src/models/HeadlessConfig.ts @@ -0,0 +1,11 @@ +import { ChatConfig } from "@yext/chat-core"; + +/** + * The configuration for a SearchHeadless instance. + * + * @public + */ +export interface HeadlessConfig extends ChatConfig { + /** Whether to save the instance's {@link ConversationState} to session storage. Defaults to true. */ + saveToSessionStorage?: boolean; +} diff --git a/packages/chat-headless/src/models/index.ts b/packages/chat-headless/src/models/index.ts index 4375a99..1aff6a0 100644 --- a/packages/chat-headless/src/models/index.ts +++ b/packages/chat-headless/src/models/index.ts @@ -1,4 +1,5 @@ export { State } from "./state"; -export { ConversationState } from "./slices/conversation"; -export { MetaState } from "./slices/meta"; +export { ConversationState } from "./slices/ConversationState"; +export { MetaState } from "./slices/MetaState"; export { StateListener } from "./utils/StateListeners"; +export { HeadlessConfig } from "./HeadlessConfig"; diff --git a/packages/chat-headless/src/models/slices/conversation.ts b/packages/chat-headless/src/models/slices/ConversationState.ts similarity index 62% rename from packages/chat-headless/src/models/slices/conversation.ts rename to packages/chat-headless/src/models/slices/ConversationState.ts index 9cb6eee..df5e2dc 100644 --- a/packages/chat-headless/src/models/slices/conversation.ts +++ b/packages/chat-headless/src/models/slices/ConversationState.ts @@ -12,6 +12,11 @@ export interface ConversationState { messages: Message[]; /** Information relevant to the current state of the conversation, generated and provided by Chat API. */ notes?: MessageNotes; - /** Whether the next message is currently processing or has finished processing. */ - isLoading?: boolean; + /** Whether the next message is currently processing or has started responding. */ + isLoading: boolean; + /** + * Whether a new message can be sent to Chat API. + * This is set to false when a previous message is being processed. + */ + canSendMessage: boolean; } diff --git a/packages/chat-headless/src/models/slices/meta.ts b/packages/chat-headless/src/models/slices/MetaState.ts similarity index 100% rename from packages/chat-headless/src/models/slices/meta.ts rename to packages/chat-headless/src/models/slices/MetaState.ts diff --git a/packages/chat-headless/src/models/state.ts b/packages/chat-headless/src/models/state.ts index 8168192..69a3baa 100644 --- a/packages/chat-headless/src/models/state.ts +++ b/packages/chat-headless/src/models/state.ts @@ -1,5 +1,5 @@ -import { ConversationState } from "./slices/conversation"; -import { MetaState } from "./slices/meta"; +import { ConversationState } from "./slices/ConversationState"; +import { MetaState } from "./slices/MetaState"; /** * The state representing a ChatHeadless instance. diff --git a/packages/chat-headless/src/slices/conversation.ts b/packages/chat-headless/src/slices/conversation.ts index e7bfd9c..6da1030 100644 --- a/packages/chat-headless/src/slices/conversation.ts +++ b/packages/chat-headless/src/slices/conversation.ts @@ -1,5 +1,5 @@ import { createSlice, PayloadAction } from "@reduxjs/toolkit"; -import { ConversationState } from "../models/slices/conversation"; +import { ConversationState } from "../models/slices/ConversationState"; import { Message, MessageNotes } from "@yext/chat-core"; export const STATE_SESSION_STORAGE_KEY = "yext_chat_conversation_state"; @@ -7,6 +7,7 @@ export const STATE_SESSION_STORAGE_KEY = "yext_chat_conversation_state"; export const initialState: ConversationState = { messages: [], isLoading: false, + canSendMessage: true, }; /** @@ -55,9 +56,20 @@ export const conversationSlice = createSlice({ ) => { state.isLoading = action.payload; }, + setCanSendMessage: ( + state: ConversationState, + action: PayloadAction + ) => { + state.canSendMessage = action.payload; + }, }, }); -export const { setMessages, setMessageNotes, setIsLoading, setConversationId } = - conversationSlice.actions; +export const { + setMessages, + setMessageNotes, + setIsLoading, + setConversationId, + setCanSendMessage, +} = conversationSlice.actions; export default conversationSlice.reducer; diff --git a/packages/chat-headless/src/slices/meta.ts b/packages/chat-headless/src/slices/meta.ts index 077654b..55a2fa8 100644 --- a/packages/chat-headless/src/slices/meta.ts +++ b/packages/chat-headless/src/slices/meta.ts @@ -1,5 +1,5 @@ import { createSlice, PayloadAction } from "@reduxjs/toolkit"; -import { MetaState } from "../models/slices/meta"; +import { MetaState } from "../models/slices/MetaState"; export const initialState: MetaState = {}; diff --git a/packages/chat-headless/tests/chatheadless.chatapi.test.ts b/packages/chat-headless/tests/chatheadless.chatapi.test.ts index 204e387..1003f5b 100644 --- a/packages/chat-headless/tests/chatheadless.chatapi.test.ts +++ b/packages/chat-headless/tests/chatheadless.chatapi.test.ts @@ -51,7 +51,7 @@ describe("Chat API methods work as expected", () => { async function testAPI( chatHeadless: ChatHeadless, - testFn: (text: string) => Promise, + testFn: (text: string) => Promise, coreTestFnSpy: unknown ) { chatHeadless.setState({ @@ -64,6 +64,7 @@ describe("Chat API methods work as expected", () => { conversation: { messages: [expectedUserMessage], isLoading: true, + canSendMessage: false, }, meta: mockedMetaState, }; @@ -77,6 +78,7 @@ describe("Chat API methods work as expected", () => { messages: [expectedUserMessage, expectedResponse.message], notes: expectedResponse.notes, isLoading: false, + canSendMessage: true, }, meta: mockedMetaState, }; @@ -201,7 +203,32 @@ describe("Chat API methods work as expected", () => { expect(coreStreamNextMessageSpy).toBeCalledTimes(1); }); - it("updates loading status and throw error when an API request returns an error", async () => { + it("logs warning when attempt to send next message to API when it is still processing", async () => { + const chatHeadless = new ChatHeadless(config); + const coreGetNextMessageSpy = jest + .spyOn(ChatCore.prototype, "getNextMessage") + .mockResolvedValueOnce(expectedResponse); + const consoleWarnSpy = jest.spyOn(console, "warn").mockImplementation(); + chatHeadless.getNextMessage("message 1"); + const secondResponse = await chatHeadless.getNextMessage("message 2"); + expect(consoleWarnSpy).toBeCalledTimes(1); + expect(consoleWarnSpy).toBeCalledWith( + "Unable to process new message at the moment. Another message is still being processed." + ); + expect(secondResponse).toBeUndefined(); + expect(coreGetNextMessageSpy).toBeCalledTimes(1); + expect(coreGetNextMessageSpy).toBeCalledWith({ + messages: [ + { + source: MessageSource.USER, + text: "message 1", + timestamp: expect.any(String), + }, + ], + }); + }); + + it("updates state and throw error when an API request returns an error", async () => { const errorMessage = "Chat API error: FATAL_ERROR: Invalid API Key. (code: 1)"; const chatHeadless = new ChatHeadless(config); @@ -215,14 +242,16 @@ describe("Chat API methods work as expected", () => { } catch (e) { // eslint-disable-next-line jest/no-conditional-expect expect(e).toEqual(errorMessage); - // eslint-disable-next-line jest/no-conditional-expect - expect(chatHeadless.state).toEqual({ + const expectedState: State = { conversation: { messages: [expectedUserMessage], isLoading: false, + canSendMessage: true, }, meta: {}, - }); + }; + // eslint-disable-next-line jest/no-conditional-expect + expect(chatHeadless.state).toEqual(expectedState); } expect(coreGetNextMessageSpy).toBeCalledTimes(1); }); diff --git a/packages/chat-headless/tests/chatheadless.test.ts b/packages/chat-headless/tests/chatheadless.test.ts index d020827..ddda6f5 100644 --- a/packages/chat-headless/tests/chatheadless.test.ts +++ b/packages/chat-headless/tests/chatheadless.test.ts @@ -1,13 +1,13 @@ import { ChatHeadless, ConversationState, + HeadlessConfig, Message, MessageNotes, MessageSource, MetaState, State, } from "../src"; -import { ChatConfig } from "@yext/chat-core"; import { ReduxStateManager } from "../src/ReduxStateManager"; import { initialState, @@ -16,7 +16,7 @@ import { jest.mock("@yext/chat-core"); -const config: ChatConfig = { +const config: HeadlessConfig = { botId: "MY_BOT", apiKey: "MY_API_KEY", }; @@ -51,6 +51,7 @@ describe("setters work as expected", () => { currentGoal: "NEW_GOAL", }, isLoading: true, + canSendMessage: false, }, meta: mockedMetaState, }; @@ -209,12 +210,13 @@ it("restartConversation works as expected", () => { currentGoal: "GOAL", }, isLoading: true, + canSendMessage: false, }, meta: mockedMetaState, }); const stateDispatchSpy = jest.spyOn(ReduxStateManager.prototype, "dispatch"); chatHeadless.restartConversation(); - expect(stateDispatchSpy).toBeCalledTimes(4); + expect(stateDispatchSpy).toBeCalledTimes(5); expect(stateDispatchSpy).toHaveBeenCalledWith({ type: "conversation/setConversationId", payload: undefined, @@ -231,12 +233,17 @@ it("restartConversation works as expected", () => { type: "conversation/setMessages", payload: [], }); + expect(stateDispatchSpy).toHaveBeenCalledWith({ + type: "conversation/setCanSendMessage", + payload: true, + }); const expectedState: State = { conversation: { messages: [], notes: {}, isLoading: false, + canSendMessage: true, }, meta: mockedMetaState, }; @@ -257,6 +264,7 @@ describe("loadSessionState works as expected", () => { currentGoal: "GOAL", }, isLoading: true, + canSendMessage: true, }; it("loads valid state from session storage", () => { sessionStorage.setItem( @@ -275,7 +283,10 @@ describe("loadSessionState works as expected", () => { STATE_SESSION_STORAGE_KEY, JSON.stringify(expectedState) ); - const chatHeadless = new ChatHeadless(config, false); + const chatHeadless = new ChatHeadless({ + ...config, + saveToSessionStorage: false, + }); expect(chatHeadless.state).toEqual({ conversation: initialState, meta: {},