Files
gochat/frontend/app/javascript/widget/helpers/specs/actionCable.spec.js
T
2026-08-24 10:16:22 +08:00

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);
});
});