187 lines
5.3 KiB
JavaScript
187 lines
5.3 KiB
JavaScript
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('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('rejects an older numeric-string timestamp against an ISO 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);
|
|
|
|
await actionCable.onMessageUpdated({
|
|
...currentMessage,
|
|
content: 'older',
|
|
updated_at: '1787533200',
|
|
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);
|
|
});
|
|
});
|