UNPKG

1ff-chat-ui

Version:

chatbot to communicate with taught ai

748 lines (600 loc) 22.9 kB
import ChatUi, { CHAT_SEEN_KEY, STORAGE_KEY } from '../../src/lib/chat-ui'; import { assistant } from '../../src/lib/config/assistant'; import { roles } from '../../src/lib/config/roles'; import { loadingDots, messageIncrementor } from '../__mocks__/htmlFixtures'; import { loadingDots as loadingDotsObj } from '../../src/lib/utils'; import { customEventTypes } from '../../src/lib/custom/tracking-events'; import { actionService } from '../../src/lib/action-service'; import { getStringInAngleBrackets } from '../../src/lib/helpers'; import { experimentsPrompt } from '../../src/lib/config/prompts-affected'; const chatWidgets = require('../../src/lib/chat-widgets'); jest.mock('../../src/lib/helpers', () => { const originalModule = jest.requireActual('../../src/lib/helpers'); const mockHelpers = { __esModule: true, ...originalModule, getStringInAngleBrackets: jest.fn().mockReturnValue(['https://mysite.com']), }; // Define the different return values for getAnswerConfig let getAnswerConfigCounter = 0; mockHelpers.getAnswerConfig = jest.fn(() => { if (getAnswerConfigCounter === 0) { getAnswerConfigCounter++; return { list: [ { actions: ['1', '10'], }, ], answersType: 'word', }; } else { return { list: [ { actions: ['2', '3'], }, ], answersType: 'word', }; } }); return mockHelpers; }); jest.mock('../../src/lib/action-service', () => { const originalModule = jest.requireActual('../../src/lib/action-service'); return { __esModule: true, ...originalModule, actionService: { getActionCodes: jest.fn().mockReturnValue(['1111']), handleAction: jest.fn(), removeTextBetweenHashtags: jest.fn().mockReturnValue('text'), }, }; }); jest.mock('socket.io-client'); describe('ChatUi', () => { let sut; const term = 'test'; const userID = 'userID'; const translations = { paymentLoaderTexts: ['Loading...'] }; beforeEach(() => { setContainer(); jest.useFakeTimers(); // Enable fake timers localStorage.setItem('__cid', userID); sut = { ...ChatUi }; Object.defineProperty(window, 'location', { value: { search: `?utm_chat=${term}`, }, writable: true, }); }); afterEach(() => { localStorage.clear(); jest.useRealTimers(); jest.clearAllTimers(); delete window.location; document.body.innerHTML = ''; }); test('initializes the chatbot with default theme and container ID', () => { jest.spyOn(sut, 'setConfig'); jest.spyOn(sut, 'setMessageObject'); jest.spyOn(sut, 'setCustomVars'); jest.spyOn(sut, 'setDomContent'); jest.spyOn(sut, 'setSocket'); // Act sut.init({ translations }); // Assert expect(sut.mainContainer).toBeDefined(); expect(sut.theme).toEqual( expect.objectContaining({ '--ember': '#cacadb', '--enigma': '#FFAE19', '--lumina': '#f0f2f5', '--seraph': '#21bb5a', '--whisper': '#ffffff', '--zephyr': '43, 49, 57', }), ); expect(sut.mainContainer).not.toBeEmptyDOMElement(); expect(sut.setConfig).toHaveBeenCalledTimes(1); expect(sut.setMessageObject).toHaveBeenCalledTimes(1); expect(sut.setCustomVars).toHaveBeenCalledTimes(1); expect(sut.setSocket).toHaveBeenCalledTimes(1); expect(sut.socket).toBeDefined(); }); test('initializes the chatbot with custom theme and container ID', () => { // Arrange const customTheme = { '--lumina': '#252239', '--whisper': '#151226', '--seraph': '#f53373', '--ember': '#cacadb', '--zephyr': '255, 255, 255', }; const containerId = 'custom-container'; const url = 'http://localhost:3000'; const customConfig = { url, customTheme, containerId, translations }; document.body.innerHTML = `<div id="${containerId}"></div>`; // Act sut.init(customConfig); // Assert expect(sut.mainContainer.id).toBe(containerId); expect(sut.theme).toEqual(expect.objectContaining(customTheme)); }); test('should setConfig correctly and override default set ones', () => { // Arrange const config = { url: 'test.url', containerId: 'new-id', assistantConfig: {}, customTheme: { '--lumina': '0' }, translations: { error: 'some custom error message' }, socketConfig: { pingInterval: 0 }, }; // Act sut.setConfig(config); // Assert expect(sut.url).toEqual(config.url); expect(sut.containerId).toEqual(config.containerId); expect(sut.socketConfig.pingInterval).toEqual(config.socketConfig.pingInterval); expect(sut.theme['--lumina']).toEqual(config.customTheme['--lumina']); expect(sut.translations.error).toEqual(config.translations.error); }); test('sends an user message after 3 seconds of not typing', () => { // Act const containerId = 'custom-container'; document.body.innerHTML = `<div id="${containerId}"></div>`; sut.init({ containerId, translations }); sut.setLastMessageButtons = jest.fn(); sut.track = jest.fn(); // Arrange const messageInput = document.getElementById('chat-prompt'); const sendButton = document.getElementById('send-button'); // Simulate user input and button click messageInput.value = 'Hello, chatbot!'; sendButton.click(); // Assert expect(sut.socket.emit).not.toHaveBeenCalledWith(sut.events.chat, expect.any(Object)); expect(sut.elements.messageIncrementor.innerHTML).toContain('Hello, chatbot!'); jest.advanceTimersByTime(3000); expect(sut.socket.emit).toHaveBeenCalledWith(sut.events.chat, expect.any(Object)); expect(messageInput.value).toBe(''); expect(sut.setLastMessageButtons).toBeCalledTimes(1); expect(sut.setLastMessageButtons).toBeCalledWith(undefined, false); expect(sut.track).toBeCalledTimes(1); expect(sut.track).toBeCalledWith(customEventTypes.firstMessage); }); test('test last message buttons set for not last message', () => { sut.addOptions = jest.fn(); sut.setLastMessageButtons('extracted sting', false); expect(sut.addOptions).not.toBeCalled(); }); test('test last message buttons set for last message', () => { sut.init({ translations }); sut.addOptions = jest.fn(); sut.setLastMessageButtons('extracted sting', true); expect(sut.addOptions).toBeCalledTimes(1); }); test('that initial buttons are not appended from assistant if actions number is 10', () => { const element = document.createElement('span'); element.appendChild = jest.fn(); sut.getLastMessageElementConsistingMessage = jest.fn().mockReturnValue(element); actionService.handleAction = jest.fn().mockReturnValue(document.createElement('div')); sut.word = jest.fn(); sut.addOptions(); //Assert expect(element.appendChild).not.toBeCalled(); }); test('that initial buttons are appended from assistant if actions number is 10', () => { const element = document.createElement('span'); element.appendChild = jest.fn(); sut.getLastMessageElementConsistingMessage = jest.fn().mockReturnValue(element); actionService.handleAction = jest.fn().mockReturnValue(document.createElement('div')); sut.word = jest.fn(); sut.addOptions(); //Assert expect(element.appendChild).toBeCalledTimes(1); }); test('does not send an empty user message', () => { // Act sut.init({ containerId: 'chatbot-container', translations }); sut.track = jest.fn(); // Arrange const sendButton = document.getElementById('send-button'); sendButton.click(); jest.advanceTimersByTime(3000); // Assert expect(sut.socket.emit).not.toBeCalledWith('chat'); // this means that we only have the initiator profile as an element expect(sut.elements.messageIncrementor.children.length).toBe(1); expect(sut.track).toBeCalledTimes(1); expect(sut.track).toBeCalledWith(customEventTypes.firstMessage); }); test('does close the socket', () => { // Act sut.init({ translations }); sut.closeSocket(); // Assert expect(sut.socket.close).toHaveBeenCalled(); }); test('should setMessageObject correctly', () => { // Assert expect(sut.lastQuestionData).toEqual({ message: '', term, user_id: 'userID', role: roles.user, }); }); test('setCtaButton sets link to an element and disables the field', () => { // Act sut.init({ containerId: 'chatbot-container', translations }); sut.track = jest.fn(); sut.link = 'https://www.test.com'; sut.setCtaButton(); // Assert expect(sut.elements.ctaButton.classList.contains('hidden')).toBe(false); expect(sut.elements.promptContainer.classList.contains('hidden')).toBe(true); expect(sut.elements.messageInput.disabled).toBe(true); expect(sut.elements.ctaButton.getAttribute('href')).toBe(sut.link); expect(sut.track).toBeCalledTimes(1); expect(sut.track).toBeCalledWith(customEventTypes.linkProvided); }); test('should setLink', () => { // Arrange const link = 'https://example.com'; // Act sut.init({ containerId: 'chatbot-container', translations }); sut.track = jest.fn(); sut.link = link; sut.setCtaButton(); // Assert expect(sut.elements.ctaButton.getAttribute('href')).toBe(link); expect(sut.elements.ctaButton.classList.contains('hidden')).toBe(false); expect(sut.elements.promptContainer.classList.contains('hidden')).toBe(true); expect(sut.track).toBeCalledTimes(1); expect(sut.track).toBeCalledWith(customEventTypes.linkProvided); }); test('should call set cta button to close', () => { // Arrange Object.defineProperty(window, 'location', { value: { search: `?utm_chat=${experimentsPrompt.finalPage}`, }, writable: true, }); const link = 'https://example.com'; // Act sut.init({ containerId: 'chatbot-container', translations }); sut.track = jest.fn(); sut.setCtaButtonToClose = jest.fn(); sut.link = link; sut.setCtaButton(); // Assert // expect(sut.elements.ctaButton.getAttribute('href')).toBe('javascript:void(0)'); expect(sut.elements.ctaButton.classList.contains('hidden')).toBe(false); expect(sut.elements.promptContainer.classList.contains('hidden')).toBe(true); expect(sut.setCtaButtonToClose).toBeCalledTimes(1); expect(sut.track).toBeCalledTimes(1); expect(sut.track).toBeCalledWith(customEventTypes.linkProvided); }); test('should close the chat via cta button click', () => { // Act sut.init({ containerId: 'chatbot-container', translations }); sut.closeWidget = jest.fn(); sut.setCtaButtonToClose(); sut.elements.ctaButton.click(); // Assert expect(sut.elements.ctaButton.getAttribute('href')).toBe('javascript:void(0)'); expect(sut.closeWidget).toBeCalledTimes(1); }); test('shouldHideChat returns true when user has seen the chat and the history has not expired (24hrs)', () => { // Arrange localStorage.setItem( STORAGE_KEY, JSON.stringify([ { role: 'user', content: 'Get started', time: new Date(), }, ]), ); localStorage.setItem(CHAT_SEEN_KEY, true); // Act const expected = sut.shouldHideChat(); // Assert expect(expected).toBe(true); }); test('shouldHideChat returns false when user has seen the chat and the history has expired (24hrs)', () => { // Arrange const givenDate = new Date(); givenDate.setDate(givenDate.getDate() - 1); localStorage.setItem( STORAGE_KEY, JSON.stringify([ { role: 'user', content: 'Get started', time: givenDate, }, ]), ); localStorage.setItem(CHAT_SEEN_KEY, true); // Act const expected = sut.shouldHideChat(); // Assert expect(expected).toBe(false); }); test('shouldHideChat returns false when we have history but the user did not see cta button', () => { // Arrange const givenDate = new Date(); givenDate.setDate(givenDate.getDate() - 1); localStorage.setItem( STORAGE_KEY, JSON.stringify([ { role: 'user', content: 'Get started', time: givenDate, }, ]), ); // Act const expected = sut.shouldHideChat(); // Assert expect(expected).toBe(false); }); test('should load assistant initial message and send it', async () => { // Arrange sut.init({ translations: { paymentLoaderTexts: [] } }); sut.loadInitialMessageSettings = jest.fn(); sut.delay = jest.fn(); sut.elements = { chatbotContainer: { appendChild: jest.fn() }, messageIncrementor: { appendChild: jest.fn(), }, }; sut.appendHtml = jest.fn(); sut.hideInput = jest.fn(); jest.spyOn(sut, 'sendAssistantInitialMessage'); jest.spyOn(sut.socket, 'emit'); // Act await sut.loadAssistantInitialMessage(); // Assert expect(sut.sendAssistantInitialMessage).toHaveBeenCalled(); expect(sut.socket.emit).toHaveBeenCalledWith('chat', { message: assistant.initialMessage.content, role: roles.assistant, term, user_id: 'userID', }); }); describe('historyTraverse', () => { test('iterates over the history with one element', () => { sut.appendHtml = jest.fn(); sut.initMedia = jest.fn(); sut.historyTraverse([{ content: 'element1' }]); expect(sut.appendHtml).toBeCalledTimes(1); expect(sut.initMedia).toBeCalledTimes(1); expect(sut.appendHtml).toBeCalledWith({ content: 'element1' }, true); }); test('iterates over the history without action data with 2 elements', () => { sut.appendHtml = jest.fn(); sut.initMedia = jest.fn(); actionService.clearButtonCodes = jest.fn().mockReturnValue('element2', true); sut.historyTraverse([{ content: 'element1' }, { content: 'element2' }]); expect(sut.initMedia).toBeCalledTimes(2); expect(sut.appendHtml).toBeCalledTimes(2); expect(sut.appendHtml).toBeCalledWith({ content: 'element1' }, false); expect(sut.appendHtml).toBeCalledWith({ content: 'element2' }, true); expect(actionService.clearButtonCodes).toBeCalled(); }); test('iterates over the history with two element with caret', () => { sut.appendHtml = jest.fn(); sut.initMedia = jest.fn(); sut.initNewLine = jest.fn(); sut.historyTraverse([{ content: 'elem^ent1' }, { content: 'element2' }]); expect(sut.appendHtml).toBeCalledTimes(1); expect(sut.appendHtml).toBeCalledWith({ content: 'element2' }, true); expect(sut.initNewLine).toBeCalledTimes(1); expect(sut.initMedia).toBeCalledTimes(2); }); }); describe('initNewLine', () => { test('adds new line in case of no caret', () => { sut.appendHtml = jest.fn(); sut.initNewLine({ content: 'element1' }, false); expect(sut.appendHtml).toBeCalledTimes(1); expect(sut.appendHtml).toBeCalledWith({ content: 'element1' }, false); }); test('adds new line in case of caret', () => { sut.appendHtml = jest.fn(); sut.initNewLine({ content: 'elem^ent1' }, false); expect(sut.appendHtml).toBeCalledTimes(2); expect(sut.appendHtml).toBeCalledWith({ content: 'elem' }, false); expect(sut.appendHtml).toBeCalledWith({ content: 'ent1' }, false); }); }); describe('initMedia', () => { test('appends the media link to the dom', () => { sut.appendMedia = jest.fn(); sut.initMedia(`Hi! I'm here to assist you. {Are you ready?} <https://mylink.com> [Let's start]`); expect(getStringInAngleBrackets).toBeCalledTimes(1); expect(sut.appendMedia).toBeCalledWith('https://mysite.com'); }); }); test('that processMessageInCaseOfCaret formats the string in the correct format', () => { //Arrange const text = '^^ ^^ Do^^ you ^^^want^ ^^to ^^^ lose weight [Yes|No]^ ^ ^^^'; //Act const formattedText = ChatUi.formatInitialMessage(text); //Assert expect(formattedText).toBe('Do^ you^ want^ to^ lose weight [Yes|No]'); }); test('that processMessageInCaseOfCaret returns the same string if no carets are present', () => { //Arrange const text = 'NO CARETS PRESENT'; //Act const formattedText = ChatUi.formatInitialMessage(text); //Assert expect(formattedText).toBe('NO CARETS PRESENT'); }); test('that rebuildInitialMessage builds the correct string from an array', () => { //Arrange const arr = ['One', 'Two', 'Three']; //Act const stringResult = ChatUi.rebuildInitialMessage(arr); //Assert expect(stringResult).toBe('One^ Two^ Three'); }); test('that processMessageInCaseOfCaret formats the string in the correct format', () => { //Arrange const text = '^^ ^^ Do^^ you ^^^want^ ^^to ^^^ lose weight [Yes|No]^ ^ ^^^'; //Act const formattedText = ChatUi.formatInitialMessage(text); //Assert expect(formattedText).toBe('Do^ you^ want^ to^ lose weight [Yes|No]'); }); test('that processMessageInCaseOfCaret returns the same string if no carets are present', () => { //Arrange const text = 'NO CARETS PRESENT'; //Act const formattedText = ChatUi.formatInitialMessage(text); //Assert expect(formattedText).toBe('NO CARETS PRESENT'); }); test('that rebuildInitialMessage builds the correct string from an array', () => { //Arrange const arr = ['One', 'Two', 'Three']; //Act const stringResult = ChatUi.rebuildInitialMessage(arr); //Assert expect(stringResult).toBe('One^ Two^ Three'); }); }); describe('test appendHtmlInChunks and all its supporting functions', () => { beforeEach(() => { document.body.innerHTML = loadingDots + messageIncrementor; ChatUi.elements = { loadingDots: document.querySelector('.js-wave'), messageIncrementor: document.getElementById('message-incrementor'), }; jest.useFakeTimers(); }); afterEach(() => { jest.useRealTimers(); }); test('that delay() promise is resolved after a given time', async () => { //Arrange const delayTime = 1000; //Act const promise = ChatUi.delay(delayTime); jest.advanceTimersByTime(delayTime); //Assert await expect(promise).resolves.toBeUndefined(); }); test('appendHtmlInChunks calls appendHtml once for each element of the splitted message array', async () => { // Arrange const appendHtmlSpy = jest.spyOn(ChatUi, 'appendHtml'); const splitMessage = ['Do you', 'want', 'to lose weight', 'and have fun? [#1#Yes|#2#No]']; const data = { message: 'Do you^ want^ to lose weight^ and have fun? [#1#Yes|#2#No]', role: 'assistant', term: 'term', user_id: '123', }; ChatUi.setElements(); // Mock the delay function to resolve immediately jest.spyOn(ChatUi, 'delay').mockResolvedValue(); // Act await ChatUi.appendHtmlInChunks(splitMessage, data); // Assert expect(appendHtmlSpy).toBeCalledTimes(splitMessage.length); }); test('appendHtmlInChunks calls loadingDots.show() once for each element of the splitted message array', async () => { // Arrange const loadingDotsSpy = jest.spyOn(loadingDotsObj, 'show'); const splitMessage = ['one', 'two', 'three']; const data = { message: 'Do you^ want^ to lose weight^ and have fun? [#1#Yes|#2#No]', role: 'assistant', term: 'term', user_id: '123', }; ChatUi.setElements(); // Mock the delay function to resolve immediately jest.spyOn(ChatUi, 'delay').mockResolvedValue(); // Act await ChatUi.appendHtmlInChunks(splitMessage, data); // Assert expect(loadingDotsSpy).toBeCalledTimes(splitMessage.length); }); test('appendHtmlInChunks calls loadingDots.hide() once for each element of the splitted message array', async () => { // Arrange const loadingDotsSpy = jest.spyOn(loadingDotsObj, 'hide'); const splitMessage = ['one', 'two', 'three', 'four']; const data = { message: 'Do you^ want^ to lose weight^ and have fun? [#1#Yes|#2#No]', role: 'assistant', term: 'term', user_id: '123', }; ChatUi.setElements(); // Mock the delay function to resolve immediately jest.spyOn(ChatUi, 'delay').mockResolvedValue(); // Act await ChatUi.appendHtmlInChunks(splitMessage, data); // Assert expect(loadingDotsSpy).toBeCalledTimes(splitMessage.length); }); }); function setContainer() { const container = document.createElement('div'); container.id = 'chatbot-container'; document.body.appendChild(container); } describe('appendMedia calls the proper media markup', () => { test('appendMedia calls videoMarkup if the extractedLink is a video link', () => { //Arrange let extractedLink = 'https://www.youtube.com/embed/cptE_T8okoo'; const videoMarkupSpy = jest.spyOn(chatWidgets, 'videoMarkup'); const imageMarkupSpy = jest.spyOn(chatWidgets, 'imageMarkup'); //Act ChatUi.appendMedia(extractedLink); //Assert expect(videoMarkupSpy).toBeCalledTimes(1); expect(imageMarkupSpy).not.toBeCalled(); }); test('appendMedia calls imageMarkup if the extractedLink is of image', () => { //Arrange const extractedLink = 'www.google.com/image.jpg'; const imageMarkupSpy = jest.spyOn(chatWidgets, 'imageMarkup'); const videoMarkupSpy = jest.spyOn(chatWidgets, 'videoMarkup'); document.appendChild = jest.fn(); ChatUi.fullScreenImage = jest.fn(); //Act ChatUi.appendMedia(extractedLink); //Assert expect(imageMarkupSpy).toBeCalledTimes(1); expect(videoMarkupSpy).not.toBeCalled(); }); test('set image fullscreen', () => { document.body.innerHTML = '<img src="http://localhost/image-to-show" class="media-image"> <div class="fullscreen-background-filter"><img src="http://localhost/initial-src"></div>'; var fullScreenImageContainer = document.querySelector('.fullscreen-background-filter'); var fullScreenImage = fullScreenImageContainer.querySelector('img'); expect(fullScreenImageContainer.classList.contains("show-image")).toBeFalsy(); expect(fullScreenImage.src).toBe("http://localhost/initial-src") document.querySelector('.media-image').click(); var fullScreenImageContainer = document.querySelector('.fullscreen-background-filter'); var fullScreenImage = fullScreenImageContainer.querySelector('img'); expect(fullScreenImageContainer.classList.contains("show-image")).toBeTruthy(); expect(fullScreenImage.src).toBe("http://localhost/image-to-show") }); });