Skip to content

Commit

Permalink
reset event listeners on re-init
Browse files Browse the repository at this point in the history
  • Loading branch information
Yen Truong committed Dec 6, 2024
1 parent b3dab3d commit d448c10
Show file tree
Hide file tree
Showing 4 changed files with 169 additions and 119 deletions.
2 changes: 1 addition & 1 deletion packages/chat-core-zendesk/package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "@yext/chat-core-zendesk",
"version": "0.3.0",
"version": "0.3.1",
"description": "Typescript Networking Library for the Yext Chat API Integration with Zendesk",
"main": "./dist/commonjs/index.js",
"module": "./dist/esm/index.mjs",
Expand Down
25 changes: 17 additions & 8 deletions packages/chat-core-zendesk/src/infra/ChatCoreZendeskImpl.ts
Original file line number Diff line number Diff line change
Expand Up @@ -63,7 +63,7 @@ export class ChatCoreZendeskImpl implements ChatCoreZendesk {
this.tags = [...new Set(this.tags)];
this.jwt = config.jwt;
this.externalId = config.externalId;
this.onInvalidAuth = config.onInvalidAuth
this.onInvalidAuth = config.onInvalidAuth;
}

/**
Expand All @@ -80,7 +80,9 @@ export class ChatCoreZendeskImpl implements ChatCoreZendesk {

private async initializeZendeskSdk(): Promise<void> {
const divId = "yext-chat-core-zendesk-container";
// If the div already exists, assume the SDK is already initialized
if (window.document.getElementById(divId)) {
this.setupEventListeners();
return;
}
const div = window.document.createElement("div");
Expand All @@ -101,13 +103,15 @@ export class ChatCoreZendeskImpl implements ChatCoreZendesk {
delegate: {
onInvalidAuth: this.onInvalidAuth,
},
}).then(() => {
this.setupEventListeners();
resolve();
}).catch((e) => {
console.error("Zendesk SDK init error", e);
reject(e);
});
})
.then(() => {
this.setupEventListeners();
resolve();
})
.catch((e) => {
console.error("Zendesk SDK init error", e);
reject(e);
});
});
}

Expand Down Expand Up @@ -167,9 +171,14 @@ export class ChatCoreZendeskImpl implements ChatCoreZendesk {
}

private setupEventListeners() {
// @ts-ignore - off() is not in the Smooch types, but does exist
Smooch.off(); // Unbind all previous event listeners, if any, before setting up new ones
Smooch.on(
"message:received",
(message: Message, data: ConversationData) => {
if (data.conversation.id !== this.conversationId) {
return;
}
if (message.type !== "text" || message.role !== "business") {
return;
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -14,14 +14,14 @@ export interface ChatCoreZendeskConfig {
ticketTags?: string[];
/**
* The JWT token to authenticate the user with Zendesk.
*
*
* @remarks
* Should be provided along with the {@link ChatCoreZendeskConfig.externalId} to authenticate the user.
*/
jwt?: string;
/**
* The external ID to associate with the user in Zendesk.
*
*
* @remarks
* Should be provided along with the {@link ChatCoreZendeskConfig.jwt} token to authenticate the user.
*/
Expand Down
257 changes: 149 additions & 108 deletions packages/chat-core-zendesk/tests/ChatCoreZendesk.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -40,9 +40,7 @@ beforeEach(() => {
jest
.mocked(SmoochLib.createConversation)
.mockResolvedValue({ id: mockConversationId } as Conversation);
jest
.mocked(SmoochLib.init)
.mockResolvedValue(Promise.resolve());
jest.mocked(SmoochLib.init).mockResolvedValue(Promise.resolve());
document.body.innerHTML = "";
});

Expand Down Expand Up @@ -96,129 +94,172 @@ describe("chat session initialization", () => {
mockConversationId
);
});
});

it("emits typing event", async () => {
const startTypingSpy = jest.spyOn(SmoochLib, "startTyping");
const stopTypingSpy = jest.spyOn(SmoochLib, "stopTyping");
const chatCoreZendesk = provideChatCoreZendesk(mockConfig);
await chatCoreZendesk.init(mockMessageResponse());
it("sets event listeners on chat session re-initialization", async () => {
const onCbSpy = jest.spyOn(SmoochLib, "on");
const chatCoreZendesk = provideChatCoreZendesk(mockConfig);
const dummyFn = jest.fn();
chatCoreZendesk.on("message", dummyFn);

chatCoreZendesk.emit("typing", true);
expect(startTypingSpy).toBeCalledTimes(1);
expect(stopTypingSpy).toBeCalledTimes(0);
// first initialization, should receive message from the created conversation
const firstConvoId = "first-convo-id";
jest
.mocked(SmoochLib.createConversation)
.mockResolvedValue({ id: firstConvoId } as Conversation);
await chatCoreZendesk.init(mockMessageResponse());
// get "message:received" callback
const onMessageFn = onCbSpy.mock.calls[0][1] as any;
// simulate a message event
onMessageFn(
{ text: "message1", type: "text", role: "business" },
{ conversation: { id: firstConvoId } }
);
expect(dummyFn).toBeCalledWith("message1");

chatCoreZendesk.emit("typing", false);
expect(startTypingSpy).toBeCalledTimes(1);
expect(stopTypingSpy).toBeCalledTimes(1);
// re-initialization, should receive ONLY message from the new conversation
dummyFn.mockClear();
const secondConvoId = "second-convo-id";
jest
.mocked(SmoochLib.createConversation)
.mockResolvedValue({ id: secondConvoId } as Conversation);
await chatCoreZendesk.init(mockMessageResponse());
// simulate a message event from old convo and new convo
onMessageFn(
{ text: "message1", type: "text", role: "business" },
{ conversation: { id: firstConvoId } }
);
expect(dummyFn).not.toBeCalled();
onMessageFn(
{ text: "message2", type: "text", role: "business" },
{ conversation: { id: secondConvoId } }
);
expect(dummyFn).toBeCalledWith("message2");
});
});

it("sends message on processMessage", async () => {
const sendMessageSpy = jest.spyOn(SmoochLib, "sendMessage");
const chatCoreZendesk = provideChatCoreZendesk(mockConfig);
await chatCoreZendesk.init(mockMessageResponse());
describe("chat session events", () => {
it("emits typing event", async () => {
const startTypingSpy = jest.spyOn(SmoochLib, "startTyping");
const stopTypingSpy = jest.spyOn(SmoochLib, "stopTyping");
const chatCoreZendesk = provideChatCoreZendesk(mockConfig);
await chatCoreZendesk.init(mockMessageResponse());

const msg = "hello world!";
await chatCoreZendesk.processMessage({
messages: [
{
source: "USER",
text: msg,
},
],
chatCoreZendesk.emit("typing", true);
expect(startTypingSpy).toBeCalledTimes(1);
expect(stopTypingSpy).toBeCalledTimes(0);

chatCoreZendesk.emit("typing", false);
expect(startTypingSpy).toBeCalledTimes(1);
expect(stopTypingSpy).toBeCalledTimes(1);
});
expect(sendMessageSpy).toBeCalledWith(msg, mockConversationId);
});

it("returns session on getSession", async () => {
const chatCoreZendesk = provideChatCoreZendesk(mockConfig);
await chatCoreZendesk.init(mockMessageResponse());
expect(chatCoreZendesk.getSession()).toBe(mockConversationId);
});
it("sends message on processMessage", async () => {
const sendMessageSpy = jest.spyOn(SmoochLib, "sendMessage");
const chatCoreZendesk = provideChatCoreZendesk(mockConfig);
await chatCoreZendesk.init(mockMessageResponse());

it("triggers message event callbacks", async () => {
const onCbSpy = jest.spyOn(SmoochLib, "on");
const chatCoreZendesk = provideChatCoreZendesk(mockConfig);
await chatCoreZendesk.init(mockMessageResponse());
const msg = "hello world!";
await chatCoreZendesk.processMessage({
messages: [
{
source: "USER",
text: msg,
},
],
});
expect(sendMessageSpy).toBeCalledWith(msg, mockConversationId);
});

const text = "hello world!";
const dummyFn = jest.fn();
chatCoreZendesk.on("message", dummyFn);
expect(onCbSpy).toBeCalled();

// get "message:received" callback
const onMessageFn = onCbSpy.mock.calls[0][1] as any;
// simulate a message event
onMessageFn(
{ text, type: "text", role: "business" },
{ conversation: { id: mockConversationId } }
);
expect(dummyFn).toBeCalledWith(text);
});
it("triggers message event callbacks", async () => {
const onCbSpy = jest.spyOn(SmoochLib, "on");
const chatCoreZendesk = provideChatCoreZendesk(mockConfig);
await chatCoreZendesk.init(mockMessageResponse());

it("triggers close event callbacks", async () => {
const onCbSpy = jest.spyOn(SmoochLib, "on");
const chatCoreZendesk = provideChatCoreZendesk(mockConfig);
await chatCoreZendesk.init(mockMessageResponse());
const text = "hello world!";
const dummyFn = jest.fn();
chatCoreZendesk.on("message", dummyFn);
expect(onCbSpy).toBeCalled();

const text = "hello world!";
const conversation = { conversation: { id: mockConversationId } };
const dummyFn = jest.fn();
chatCoreZendesk.on("close", dummyFn);
expect(onCbSpy).toBeCalled();

// get "message:received" callback
const onCloseFn = onCbSpy.mock.calls[0][1] as any;
// simulate a message event from internal bot indicating agent has left
onCloseFn(
{ text, type: "text", role: "business", subroles: ["AI"] },
conversation
);
expect(dummyFn).toBeCalledWith(conversation);
});
// get "message:received" callback
const onMessageFn = onCbSpy.mock.calls[0][1] as any;
// simulate a message event
onMessageFn(
{ text, type: "text", role: "business" },
{ conversation: { id: mockConversationId } }
);
expect(dummyFn).toBeCalledWith(text);
});

it("triggers typing event callbacks", async () => {
const onTypingSpy = jest.spyOn(SmoochLib, "on");
const chatCoreZendesk = provideChatCoreZendesk(mockConfig);
await chatCoreZendesk.init(mockMessageResponse());
it("triggers close event callbacks", async () => {
const onCbSpy = jest.spyOn(SmoochLib, "on");
const chatCoreZendesk = provideChatCoreZendesk(mockConfig);
await chatCoreZendesk.init(mockMessageResponse());

const text = "hello world!";
const conversation = { conversation: { id: mockConversationId } };
const dummyFn = jest.fn();
chatCoreZendesk.on("close", dummyFn);
expect(onCbSpy).toBeCalled();

// get "message:received" callback
const onCloseFn = onCbSpy.mock.calls[0][1] as any;
// simulate a message event from internal bot indicating agent has left
onCloseFn(
{ text, type: "text", role: "business", subroles: ["AI"] },
conversation
);
expect(dummyFn).toBeCalledWith(conversation);
});

it("triggers typing event callbacks", async () => {
const onTypingSpy = jest.spyOn(SmoochLib, "on");
const chatCoreZendesk = provideChatCoreZendesk(mockConfig);
await chatCoreZendesk.init(mockMessageResponse());

const dummyFn = jest.fn();
chatCoreZendesk.on("typing", dummyFn);
expect(onTypingSpy).toBeCalled();

// get "typing:start" callback
const onStartTypingFn = onTypingSpy.mock.calls[1][1] as any;
// simulate a typing event
onStartTypingFn();
expect(dummyFn).toBeCalledWith(true);

// get "typing:stop" callback
const onStopTypingFn = onTypingSpy.mock.calls[2][1] as any;
// simulate a typing event
onStopTypingFn();
expect(dummyFn).toBeCalledWith(false);
});

it("clear session on close event", async () => {
const onCbSpy = jest.spyOn(SmoochLib, "on");
const chatCoreZendesk = provideChatCoreZendesk(mockConfig);
await chatCoreZendesk.init(mockMessageResponse());
expect(chatCoreZendesk.getSession()).toBeDefined();

const dummyFn = jest.fn();
chatCoreZendesk.on("typing", dummyFn);
expect(onTypingSpy).toBeCalled();

// get "typing:start" callback
const onStartTypingFn = onTypingSpy.mock.calls[1][1] as any;
// simulate a typing event
onStartTypingFn();
expect(dummyFn).toBeCalledWith(true);

// get "typing:stop" callback
const onStopTypingFn = onTypingSpy.mock.calls[2][1] as any;
// simulate a typing event
onStopTypingFn();
expect(dummyFn).toBeCalledWith(false);
// get the parameter passed to the onSpy callback
const text = "hello world!";
const conversation = { conversation: { id: mockConversationId } };
const dummyFn = jest.fn();
chatCoreZendesk.on("close", dummyFn);
const onCbFn = onCbSpy.mock.calls[0][1] as any;

// simulate a session close event via a message event from internal bot
onCbFn(
{ text, type: "text", role: "business", subroles: ["AI"] },
conversation
);
expect(dummyFn).toBeCalledWith(conversation);
expect(chatCoreZendesk.getSession()).toBeUndefined();
});
});

it("clear session on close event", async () => {
const onCbSpy = jest.spyOn(SmoochLib, "on");
it("returns session on getSession", async () => {
const chatCoreZendesk = provideChatCoreZendesk(mockConfig);
await chatCoreZendesk.init(mockMessageResponse());
expect(chatCoreZendesk.getSession()).toBeDefined();

// get the parameter passed to the onSpy callback
const text = "hello world!";
const conversation = { conversation: { id: mockConversationId } };
const dummyFn = jest.fn();
chatCoreZendesk.on("close", dummyFn);
const onCbFn = onCbSpy.mock.calls[0][1] as any;

// simulate a session close event via a message event from internal bot
onCbFn(
{ text, type: "text", role: "business", subroles: ["AI"] },
conversation
);
expect(dummyFn).toBeCalledWith(conversation);
expect(chatCoreZendesk.getSession()).toBeUndefined();
expect(chatCoreZendesk.getSession()).toBe(mockConversationId);
});

it("clears session on resetSession", async () => {
Expand Down

0 comments on commit d448c10

Please sign in to comment.