import axios from 'axios'; import { flushPromises, mount } from '@vue/test-utils'; import { createStore } from 'vuex'; import { ref } from 'vue'; import conversationActions from 'dashboard/store/modules/conversations/actions'; import types from 'dashboard/store/mutation-types'; import CopilotMenuBar from './CopilotMenuBar.vue'; import ReplyTopPanel from './ReplyTopPanel.vue'; vi.mock('axios'); vi.mock('dashboard/composables/useCaptain', () => ({ useCaptain: () => ({ captainTasksEnabled: ref(true) }), })); vi.mock('dashboard/composables/useKeyboardEvents', () => ({ useKeyboardEvents: vi.fn(), })); const createWrapper = ({ aiTakeoverActive = false, inboxAssistant = { id: 7 }, useRealAssistantAction = false, props = {}, } = {}) => { const startAITakeover = vi.fn(); const exitAITakeover = vi.fn(); const getInboxCaptainAssistantById = vi.fn(); const store = createStore({ state: { currentChat: { id: 42, ai_takeover_active: aiTakeoverActive }, copilotAssistant: inboxAssistant, }, getters: { getSelectedChat: state => state.currentChat, getCopilotAssistant: state => state.copilotAssistant, }, mutations: { [types.SET_INBOX_CAPTAIN_ASSISTANT](state, data) { state.copilotAssistant = data.assistant; }, }, actions: { startAITakeover, exitAITakeover, getInboxCaptainAssistantById: useRealAssistantAction ? conversationActions.getInboxCaptainAssistantById : getInboxCaptainAssistantById, }, modules: { draftMessages: { namespaced: true, getters: { getReplyEditorMode: () => 'REPLY' }, }, }, }); return { store, exitAITakeover, startAITakeover, getInboxCaptainAssistantById, wrapper: mount(ReplyTopPanel, { props: { conversationId: 42, ...props }, global: { plugins: [store], stubs: { NextButton: false }, }, }), }; }; describe('ReplyTopPanel', () => { it('shows AI takeover and flat Copilot actions in the same action row', () => { const { wrapper } = createWrapper(); const actions = wrapper.get('[data-testid="reply-top-panel-actions"]'); const copilotActions = actions.getComponent(CopilotMenuBar); expect(copilotActions.props('inline')).toBe(true); expect(actions.text()).toContain('AI takeover'); expect(actions.get('[data-testid="copilot-inline-actions"]')).toBeTruthy(); }); it('wraps all eight primary actions on narrow screens when content is present', () => { const { wrapper } = createWrapper({ props: { hasContent: true } }); const actions = wrapper.get('[data-testid="reply-top-panel-actions"]'); const copilotActions = actions.get( '[data-testid="copilot-inline-actions"]' ); expect(wrapper.classes()).toEqual( expect.arrayContaining(['flex-wrap', 'sm:flex-nowrap']) ); expect(actions.classes()).toEqual( expect.arrayContaining([ 'w-full', 'flex-wrap', 'sm:w-auto', 'sm:flex-nowrap', ]) ); expect(actions.findAll(':scope > button')).toHaveLength(2); expect(copilotActions.findAll(':scope > div > button')).toHaveLength(6); }); it('keeps AI takeover behavior in the new action row', async () => { const { startAITakeover, wrapper } = createWrapper(); const takeoverButton = wrapper .get('[data-testid="reply-top-panel-actions"]') .findAll('button') .find(button => button.text() === 'AI takeover'); await takeoverButton.trigger('click'); expect(startAITakeover).toHaveBeenCalledWith(expect.any(Object), 42); }); it('hides AI takeover when the inbox has no AI configured', () => { const { getInboxCaptainAssistantById, startAITakeover, wrapper } = createWrapper({ inboxAssistant: null }); expect(getInboxCaptainAssistantById).toHaveBeenCalledWith( expect.any(Object), 42 ); expect(wrapper.text()).not.toContain('AI takeover'); expect(startAITakeover).not.toHaveBeenCalled(); }); it('updates after the inbox assistant API returns null', async () => { axios.get.mockResolvedValue({ data: { assistant: null } }); const { store, wrapper } = createWrapper({ useRealAssistantAction: true, }); await flushPromises(); expect(store.getters.getCopilotAssistant).toBeNull(); expect(wrapper.text()).not.toContain('AI takeover'); }); it('keeps the exit action when takeover is active without an assistant', async () => { const { exitAITakeover, wrapper } = createWrapper({ aiTakeoverActive: true, inboxAssistant: null, }); const exitButton = wrapper .get('[data-testid="reply-top-panel-actions"]') .findAll('button') .find(button => button.text() === 'Exit AI takeover'); await exitButton.trigger('click'); expect(exitAITakeover).toHaveBeenCalledWith(expect.any(Object), 42); }); it('renders Copilot actions inline and forwards the selected action', async () => { const store = createStore({ modules: { draftMessages: { namespaced: true, getters: { getReplyEditorMode: () => 'REPLY' }, }, }, }); const wrapper = mount(CopilotMenuBar, { props: { inline: true, conversationId: 42 }, global: { plugins: [store], stubs: { NextButton: false }, }, }); const suggestionButton = wrapper.get( '[data-testid="copilot-inline-actions"] button[aria-label="Suggest a reply"]' ); await suggestionButton.trigger('click'); expect(wrapper.emitted('executeCopilotAction')).toEqual([ ['reply_suggestion'], ]); }); it('disables every flat Copilot action with the editor', () => { const { wrapper } = createWrapper({ props: { hasContent: true, isEditorDisabled: true }, }); const copilotButtons = wrapper .get('[data-testid="copilot-inline-actions"]') .findAll('button'); expect(copilotButtons.length).toBeGreaterThan(0); expect( copilotButtons.every(button => button.attributes('disabled') === '') ).toBe(true); }); });