Newer
Older
navi-1 / webclient / tests / unit / components / chat / inputBar.test.js
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({})
  })
})