Newer
Older
navi-1 / webclient / tests / unit / components / chat / messageList.test.js
import { describe, it, expect, beforeEach } from 'vitest'
import { nextTick } from 'vue'
import { setActivePinia, createPinia } from 'pinia'
import { mount } from '@vue/test-utils'
import MessageList from '@/components/chat/MessageList.vue'
import { useChatStore } from '@/stores/chat.js'

// jsdom has no layout engine, so the scroller's metrics are stubbed: 800px of
// content in a 400px viewport. scrollTop is a plain writable property there.
function openSession(chat) {
  chat.messages = [{ id: 'u1', role: 'user', text: 'привет' }]
  chat.loading = false
  chat.currentId = 's1'
}

function mountList(chat) {
  openSession(chat)
  const wrapper = mount(MessageList)
  const el = wrapper.find('.message-scroller').element
  Object.defineProperty(el, 'scrollHeight', { value: 800, configurable: true })
  Object.defineProperty(el, 'clientHeight', { value: 400, configurable: true })
  return { wrapper, el }
}

describe('MessageList auto-scroll', () => {
  beforeEach(() => {
    setActivePinia(createPinia())
  })

  it('re-pins to the bottom when the stream finishes', async () => {
    const chat = useChatStore()
    const { el } = mountList(chat)

    chat.streaming = true
    await nextTick()
    // The last per-delta pin ran before the footer/copy buttons landed, so the
    // view sits short of the bottom when the stream closes.
    el.scrollTop = 0

    chat.streaming = false
    await nextTick()

    expect(el.scrollTop).toBe(800)
  })

  it('leaves the position alone when the user scrolled up', async () => {
    const chat = useChatStore()
    const { wrapper, el } = mountList(chat)
    await wrapper.find('.message-scroller').trigger('scroll') // 800-0-400 > 100

    chat.streaming = true
    await nextTick()
    el.scrollTop = 0

    chat.streaming = false
    await nextTick()

    expect(el.scrollTop).toBe(0)
  })
})