H-43: fix WEB Captain takeover E2E flow (#8)
* test(shangwutong): cover CID rename reliability * H-43: fix WEB Captain takeover flow * H-48: preserve compatible provider model * H-49: make Captain takeover atomic * H-50: prevent duplicate widget initialization --------- Co-authored-by: Rogee <rogee@ipao.vip>
This commit is contained in:
+1
-1
@@ -68,7 +68,7 @@ const isHMACVerified = computed(() => {
|
||||
});
|
||||
|
||||
const currentContact = computed(() =>
|
||||
store.getters['contacts/getContact'](props.chat.meta.sender.id)
|
||||
store.getters['contacts/getContact'](props.chat?.meta?.sender?.id)
|
||||
);
|
||||
|
||||
const isSnoozed = computed(
|
||||
|
||||
@@ -245,7 +245,8 @@ export const mutations = {
|
||||
if (!conversation?.id) return;
|
||||
|
||||
const { allConversations } = _state;
|
||||
const index = allConversations.findIndex(c => c.id === conversation.id);
|
||||
const normalizedId = Number(conversation.id);
|
||||
const index = allConversations.findIndex(c => Number(c.id) === normalizedId);
|
||||
|
||||
if (index > -1) {
|
||||
const selectedConversation = allConversations[index];
|
||||
@@ -256,8 +257,12 @@ export const mutations = {
|
||||
}
|
||||
|
||||
const { messages, ...updates } = conversation;
|
||||
allConversations[index] = { ...selectedConversation, ...updates };
|
||||
if (_state.selectedChatId === conversation.id) {
|
||||
allConversations[index] = {
|
||||
...selectedConversation,
|
||||
...updates,
|
||||
id: selectedConversation.id,
|
||||
};
|
||||
if (Number(_state.selectedChatId) === normalizedId) {
|
||||
emitter.emit(BUS_EVENTS.SCROLL_TO_MESSAGE);
|
||||
}
|
||||
} else {
|
||||
|
||||
@@ -802,6 +802,28 @@ describe('#mutations', () => {
|
||||
});
|
||||
});
|
||||
|
||||
it('should update when route and API ids use different primitive types', () => {
|
||||
const state = {
|
||||
allConversations: [{ id: '3', status: 'pending', updated_at: 100 }],
|
||||
selectedChatId: '3',
|
||||
};
|
||||
|
||||
mutations[types.UPDATE_CONVERSATION](state, {
|
||||
id: 3,
|
||||
status: 'open',
|
||||
ai_takeover_active: false,
|
||||
updated_at: 200,
|
||||
});
|
||||
|
||||
expect(state.allConversations).toHaveLength(1);
|
||||
expect(state.allConversations[0]).toMatchObject({
|
||||
id: '3',
|
||||
status: 'open',
|
||||
ai_takeover_active: false,
|
||||
});
|
||||
expect(emitter.emit).toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('should add conversation if not found on normal view', () => {
|
||||
const state = {
|
||||
allConversations: [],
|
||||
|
||||
@@ -0,0 +1,58 @@
|
||||
import { beforeEach, describe, expect, it, vi } from 'vitest';
|
||||
|
||||
const { app, ActionCableConnector } = vi.hoisted(() => ({
|
||||
app: {
|
||||
use: vi.fn(),
|
||||
directive: vi.fn(),
|
||||
mount: vi.fn(() => ({ $store: {} })),
|
||||
},
|
||||
ActionCableConnector: vi.fn(),
|
||||
}));
|
||||
|
||||
vi.mock('vue', () => ({ createApp: () => app }));
|
||||
vi.mock('vue-i18n', () => ({ createI18n: vi.fn() }));
|
||||
vi.mock('vue-dompurify-html', () => ({ default: {} }));
|
||||
vi.mock('../../widget/store', () => ({ default: {} }));
|
||||
vi.mock('../../widget/App.vue', () => ({ default: {} }));
|
||||
vi.mock('../../widget/helpers/actionCable', () => ({
|
||||
default: ActionCableConnector,
|
||||
}));
|
||||
vi.mock('../../widget/i18n', () => ({ default: {} }));
|
||||
vi.mock('../../widget/router', () => ({ default: {} }));
|
||||
vi.mock('vue3-click-away', () => ({ directive: {} }));
|
||||
vi.mock('../../shared/helpers/HTMLSanitizer', () => ({
|
||||
domPurifyConfig: {},
|
||||
}));
|
||||
vi.mock('@formkit/vue', () => ({
|
||||
plugin: {},
|
||||
defaultConfig: vi.fn(),
|
||||
}));
|
||||
vi.mock('shared/helpers/Validators', () => ({
|
||||
startsWithPlus: vi.fn(),
|
||||
isPhoneNumberValidWithDialCode: vi.fn(),
|
||||
}));
|
||||
|
||||
describe('widget entrypoint', () => {
|
||||
beforeEach(() => {
|
||||
vi.resetModules();
|
||||
vi.clearAllMocks();
|
||||
delete window.WOOT_WIDGET;
|
||||
delete window.actionCable;
|
||||
window.chatwootPubsubToken = 'pubsub-token';
|
||||
});
|
||||
|
||||
it('mounts and connects only once when load fires repeatedly', async () => {
|
||||
vi.spyOn(document, 'readyState', 'get').mockReturnValue('loading');
|
||||
|
||||
await import('../widget');
|
||||
window.dispatchEvent(new Event('load'));
|
||||
window.dispatchEvent(new Event('load'));
|
||||
|
||||
expect(app.mount).toHaveBeenCalledOnce();
|
||||
expect(ActionCableConnector).toHaveBeenCalledOnce();
|
||||
expect(ActionCableConnector).toHaveBeenCalledWith(
|
||||
window.WOOT_WIDGET,
|
||||
'pubsub-token'
|
||||
);
|
||||
});
|
||||
});
|
||||
@@ -45,10 +45,18 @@ app.use(
|
||||
|
||||
// Vue.config.productionTip = false;
|
||||
|
||||
window.onload = () => {
|
||||
export const initWidget = () => {
|
||||
if (window.WOOT_WIDGET) return;
|
||||
|
||||
window.WOOT_WIDGET = app.mount('#app');
|
||||
window.actionCable = new ActionCableConnector(
|
||||
window.WOOT_WIDGET,
|
||||
window.chatwootPubsubToken
|
||||
);
|
||||
};
|
||||
|
||||
if (document.readyState === 'loading') {
|
||||
window.addEventListener('load', initWidget, { once: true });
|
||||
} else {
|
||||
initWidget();
|
||||
}
|
||||
|
||||
@@ -2,7 +2,7 @@ export default {
|
||||
computed: {
|
||||
messageContentAttributes() {
|
||||
const { content_attributes: attribute = {} } = this.message;
|
||||
return attribute;
|
||||
return attribute || {};
|
||||
},
|
||||
hasAttachments() {
|
||||
return !!(
|
||||
|
||||
@@ -34,4 +34,11 @@ describe('messageMixin', () => {
|
||||
expect(wrapper.vm.messageContentAttributes).toEqual({});
|
||||
expect(wrapper.vm.hasAttachments).toBe(true);
|
||||
});
|
||||
|
||||
it('normalizes null content attributes', () => {
|
||||
const wrapper = shallowMount(Component, {
|
||||
data: () => ({ message: { content_attributes: null } }),
|
||||
});
|
||||
expect(wrapper.vm.messageContentAttributes).toEqual({});
|
||||
});
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user