import { describe, it, expect, beforeEach, vi } from 'vitest'
import { nextTick } from 'vue'
import { setActivePinia, createPinia } from 'pinia'
import { mount } from '@vue/test-utils'
import InputBar from '@/components/chat/InputBar.vue'
import { useChatStore } from '@/stores/chat.js'
vi.mock('@/api/index.js', () => ({
stopSession: vi.fn(async () => ({ ok: true })),
}))
function mountBar() {
const ws = { send: vi.fn(() => true) }
const wrapper = mount(InputBar, { props: { ws } })
return { wrapper, ws }
}
async function typeAndSend(wrapper, text) {
const textarea = wrapper.find('.input-textarea')
await textarea.setValue(text)
await textarea.trigger('keydown', { key: 'Enter' })
await nextTick()
}
// happy-dom in this setup has no localStorage global; InputBar restores the
// draft from it on mount.
function stubStorage() {
const store = new Map()
vi.stubGlobal('localStorage', {
getItem: k => (store.has(k) ? store.get(k) : null),
setItem: (k, v) => store.set(k, String(v)),
removeItem: k => store.delete(k),
clear: () => store.clear(),
})
}
describe('InputBar while a turn is running', () => {
beforeEach(() => {
stubStorage()
setActivePinia(createPinia())
})
it('keeps sending available instead of swapping send for stop', async () => {
const chat = useChatStore()
chat.currentId = 's1'
chat.streaming = true
const { wrapper } = mountBar()
// Both controls: stop must not cost the user the ability to write.
expect(wrapper.find('.ph-paper-plane-tilt').exists()).toBe(true)
expect(wrapper.find('.ph-stop-circle').exists()).toBe(true)
})
it('sends the message and marks it queued', async () => {
const chat = useChatStore()
chat.currentId = 's1'
chat.streaming = true
const { wrapper, ws } = mountBar()
await typeAndSend(wrapper, 'и добавь тесты')
expect(ws.send).toHaveBeenCalledWith({ type: 'message', content: 'и добавь тесты' })
const last = chat.messages[chat.messages.length - 1]
expect(last.text).toBe('и добавь тесты')
expect(last.queued).toBe(true)
expect(chat.queuedCount).toBe(1)
})
it('does not mark a message sent while nothing is running', async () => {
const chat = useChatStore()
chat.currentId = 's1'
chat.streaming = false
const { wrapper, ws } = mountBar()
await typeAndSend(wrapper, 'обычное сообщение')
expect(ws.send).toHaveBeenCalled()
const last = chat.messages[chat.messages.length - 1]
expect(last.queued).toBe(false)
expect(chat.queuedCount).toBe(0)
})
it('clears the queued mark when the next turn starts', async () => {
const chat = useChatStore()
chat.currentId = 's1'
chat.streaming = true
const { wrapper } = mountBar()
await typeAndSend(wrapper, 'первое')
await typeAndSend(wrapper, 'второе')
expect(chat.queuedCount).toBe(2)
// The server finished the running turn and picked the first message up.
chat.onStreamStart()
expect(chat.messages.filter(m => m.queued).map(m => m.text)).toEqual(['второе'])
chat.onStreamEnd({})
})
})