import { beforeEach, describe, expect, it, vi } from 'vitest'; import ActionCableConnector from '../actionCable'; import { playNewMessageNotificationInWidget } from '../WidgetAudioNotificationHelper'; import { IFrameHelper } from '../utils'; import { emitter } from 'shared/helpers/mitt'; import { actions } from 'widget/store/modules/conversation/actions'; import { mutations } from 'widget/store/modules/conversation/mutations'; vi.mock('../WidgetAudioNotificationHelper', () => ({ playNewMessageNotificationInWidget: vi.fn(), })); vi.mock('../utils', () => ({ IFrameHelper: { sendMessage: vi.fn(), }, })); vi.mock('shared/helpers/mitt', () => ({ emitter: { emit: vi.fn(), }, })); describe('Widget ActionCableConnector', () => { let actionCable; let dispatch; beforeEach(() => { dispatch = vi.fn(); actionCable = new ActionCableConnector( { $store: { dispatch, getters: { getCurrentAccountId: 1, getCurrentUserID: 1, 'conversationAttributes/getConversationParams': { id: 7 }, }, }, }, 'test-token' ); }); const useRealConversationDispatch = state => { const commit = (type, payload) => mutations[type](state, payload); dispatch.mockImplementation((action, payload) => { if (action === 'conversation/addOrUpdateMessage') { return actions.addOrUpdateMessage({ commit, state }, payload); } return undefined; }); }; it('uses visitor-token auth even when a dashboard session shares the origin', () => { expect(actionCable.useSessionAuth).toBe(false); }); it('emits message side effects only once when realtime replays a message', async () => { const message = { id: 42, conversation_id: 7, sender_type: 'User', }; dispatch .mockResolvedValueOnce({ applied: true, isNew: true }) .mockResolvedValueOnce({ applied: false, isNew: false }); await actionCable.onMessageCreated(message); await actionCable.onMessageCreated(message); expect(emitter.emit).toHaveBeenCalledTimes(1); expect(IFrameHelper.sendMessage).toHaveBeenCalledTimes(1); expect(playNewMessageNotificationInWidget).toHaveBeenCalledTimes(1); }); it('does not emit the SDK callback for an older message update', async () => { const message = { id: 42, conversation_id: 7, previous_changes: { content_attributes: [{}, { submitted_values: ['yes'] }], }, }; dispatch.mockResolvedValue({ applied: false, isNew: false }); await actionCable.onMessageUpdated(message); expect(IFrameHelper.sendMessage).not.toHaveBeenCalled(); }); it('rejects malformed timestamps through the real widget mutation path', async () => { const currentMessage = { id: 42, conversation_id: 7, content: 'current', updated_at: '2026-08-24T02:00:00Z', }; const state = { conversations: { 42: currentMessage } }; useRealConversationDispatch(state); await actionCable.onMessageUpdated({ ...currentMessage, content: 'malformed', updated_at: 'not-a-timestamp', previous_changes: { content_attributes: [{}, { submitted_values: ['yes'] }], }, }); expect(state.conversations[42]).toBe(currentMessage); expect(IFrameHelper.sendMessage).not.toHaveBeenCalled(); }); it('treats an exact 1e10 numeric string as milliseconds', async () => { const currentMessage = { id: 42, conversation_id: 7, content: 'current', updated_at: '2026-08-24T02:00:00Z', }; const state = { conversations: { 42: currentMessage } }; useRealConversationDispatch(state); await actionCable.onMessageUpdated({ ...currentMessage, content: 'older', updated_at: '10000000000', previous_changes: { content_attributes: [{}, { submitted_values: ['yes'] }], }, }); expect(state.conversations[42]).toBe(currentMessage); expect(IFrameHelper.sendMessage).not.toHaveBeenCalled(); }); it('replaces a malformed stored timestamp with a valid update', async () => { const state = { conversations: { 42: { id: 42, conversation_id: 7, content: 'malformed stored message', updated_at: 'not-a-timestamp', }, }, }; useRealConversationDispatch(state); const message = { id: 42, conversation_id: 7, content: 'valid update', updated_at: '2026-08-24T02:00:00Z', previous_changes: { content_attributes: [{}, { submitted_values: ['yes'] }], }, }; await actionCable.onMessageUpdated(message); expect(state.conversations[42]).toEqual(message); expect(IFrameHelper.sendMessage).toHaveBeenCalledTimes(1); }); it('keeps accepting widget updates with a missing timestamp', async () => { const currentMessage = { id: 42, conversation_id: 7, content: 'current', updated_at: '2026-08-24T02:00:00Z', }; const state = { conversations: { 42: currentMessage } }; useRealConversationDispatch(state); const message = { id: 42, conversation_id: 7, content: 'compatible update', previous_changes: { content_attributes: [{}, { submitted_values: ['yes'] }], }, }; await actionCable.onMessageUpdated(message); expect(state.conversations[42]).toEqual(message); expect(IFrameHelper.sendMessage).toHaveBeenCalledTimes(1); }); });