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)
})
})