Skip to content

Commit

Permalink
chat list tests
Browse files Browse the repository at this point in the history
  • Loading branch information
nbonamy committed May 10, 2024
1 parent 49fb77d commit a1f6e4c
Show file tree
Hide file tree
Showing 3 changed files with 171 additions and 5 deletions.
10 changes: 5 additions & 5 deletions src/components/ChatList.vue
Original file line number Diff line number Diff line change
@@ -1,15 +1,15 @@
<template>
<div class="chats" ref="divChats" :set="day = null">
<div class="chats" ref="divChats" :set="currDay = null">
<div v-for="c in visibleChats" :key="c.uuid" :set="chatDay=getDay(c)">
<div v-if="chatDay != day" :set="day = chatDay" class="day">{{ chatDay }}</div>
<div class="chat" :class="c.uuid == chat?.uuid ? 'selected': ''" @click="onSelectChat(c)" @contextmenu.prevent="showContextMenu($event, c)">
<div v-if="chatDay != currDay" :set="currDay = chatDay" class="day">{{ currDay }}</div>
<div class="chat" :class="c.uuid == chat?.uuid ? 'selected': ''" @click="onSelectChat(c)" @contextmenu.prevent="showContextMenu($event, c)" :data-day="chatDay">
<EngineLogo :engine="engine(c)" :background="true" />
<div class="info">
<div class="title">{{ c.title }}</div>
<div class="subtitle">{{ c.subtitle() }}</div>
</div>
<div v-if="selectMode" class="select">
<BIconCheckCircleFill v-if="selection.includes(c.uuid)" />
<BIconCheckCircleFill v-if="selection.includes(c.uuid)" class="selected"/>
<BIconCircle v-else />
</div>
</div>
Expand Down Expand Up @@ -79,7 +79,7 @@ const contextMenuActions = [
let scrollEndTimeout = null
onMounted(() => {
divChats.value.addEventListener('scroll', (ev) => {
divChats.value?.addEventListener('scroll', (ev) => {
ev.target.classList.add('scrolling')
clearTimeout(scrollEndTimeout)
scrollEndTimeout = setTimeout(() => {
Expand Down
12 changes: 12 additions & 0 deletions src/types/index.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,13 @@ interface Chat {
engine: string
model: string
messages: Message[]
fromJson(jsonChat: any): void
patchFromJson(jsonChat: any): boolean
setEngineModel(engine: string, model: string): void
addMessage(message: Message): void
lastMessage(): Message
subtitle(): string
delete(): void
}

interface Message {
Expand All @@ -22,6 +28,12 @@ interface Message {
content: string
attachment: Attachment
transient: boolean
fromJson(jsonMessage: any): void
setText(text: string|null): void
setImage(url: string): void
appendText(chunk: LlmChunk): void
attachFile(file: Attachment): void
setToolCall(toolCall: string|null): void
}

interface Attachment {
Expand Down
154 changes: 154 additions & 0 deletions tests/components/chat_list.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,154 @@

import { mount, VueWrapper, enableAutoUnmount } from '@vue/test-utils'
import { vi, beforeAll, beforeEach, afterAll, expect, test } from 'vitest'
import ChatList from '../../src/components/ChatList.vue'
import { store } from '../../src/services/store'
import defaults from '../../defaults/settings.json'
import Chat from '../../src/models/chat'
import Message from '../../src/models/message'
import { renderMarkdown } from '../../src/main/markdown'

enableAutoUnmount(afterAll)

const onEventMock = vi.fn()
const emitEventMock = vi.fn()

const stubTeleport = { global: { stubs: { teleport: true } } }

vi.mock('../../src/composables/useEventBus.js', async () => {
return { default: () => {
return {
onEvent: onEventMock,
emitEvent: emitEventMock
}
}}
})

beforeAll(() => {

// api
window.api = {
history: {
save: vi.fn()
},
markdown: {
render: renderMarkdown
}
}

// init store
store.config = defaults
store.config.getActiveModel = () => {
return 'chat'
}
})

beforeEach(() => {
store.chats = []
for (let i = 0; i < 10; i++) {
const chat = new Chat()
chat.title = `Chat ${i}`
chat.setEngineModel('mock', 'chat')
chat.lastModified = Date.now() - i * i/2 * 86400000
chat.messages.push(new Message('system', 'System Prompt'))
chat.messages.push(new Message('user', `Question ${i}`))
chat.messages.push(new Message('assistant', `Subtitle ${i}`))
store.chats.push(chat)
}
})

test('No chat', async () => {
store.chats = []
const wrapper: VueWrapper<any> = mount(ChatList, { props: { chat: null, filter: '' } } )
expect(wrapper.exists()).toBe(true)
expect(wrapper.find('.chats').classes()).not.toContain('standalone')
})

test('Shows chats', async () => {
const wrapper: VueWrapper<any> = mount(ChatList, { props: { chat: null, filter: '' } } )
expect(wrapper.findAll('.chat')).toHaveLength(10)
expect(wrapper.findAll('.selected')).toHaveLength(0)
expect(wrapper.findAll('.select')).toHaveLength(0)
wrapper.findAll('.chat').forEach((chat, i) => {
expect(chat.find('.title').text()).toBe(`Chat ${i}`)
expect(chat.find('.subtitle').text()).toBe(`Subtitle ${i}`)
})
})

test('Shows day indicator', async () => {
const wrapper: VueWrapper<any> = mount(ChatList, { props: { chat: null, filter: '' } } )
expect(wrapper.findAll('.day')).toHaveLength(6)

Check failure on line 80 in tests/components/chat_list.test.ts

View workflow job for this annotation

GitHub Actions / build

tests/components/chat_list.test.ts > Shows day indicator

AssertionError: expected [ DOMWrapper{ …(2) }, …(4) ] to have a length of 6 but got 5 - Expected + Received - 6 + 5 ❯ tests/components/chat_list.test.ts:80:35
expect(wrapper.findAll('.day').at(0).text()).toBe('Today')
expect(wrapper.findAll('.day').at(1).text()).toBe('Yesterday')
expect(wrapper.findAll('.day').at(2).text()).toBe('Last 7 days')
expect(wrapper.findAll('.day').at(3).text()).toBe('Last 14 days')
expect(wrapper.findAll('.day').at(4).text()).toBe('Last 30 days')
expect(wrapper.findAll('.day').at(5).text()).toBe('Earlier')
expect(wrapper.findAll('[data-day=Today]')).toHaveLength(1)
expect(wrapper.findAll('[data-day=Yesterday]')).toHaveLength(1)
expect(wrapper.findAll('[data-day="Last 7 days"]')).toHaveLength(2)
expect(wrapper.findAll('[data-day="Last 14 days"]')).toHaveLength(2)
expect(wrapper.findAll('[data-day="Last 30 days"]')).toHaveLength(2)
expect(wrapper.findAll('[data-day="Earlier"]')).toHaveLength(2)
})

test('Select chat', async () => {
const wrapper: VueWrapper<any> = mount(ChatList, { props: { chat: null, filter: '' } } )
expect(wrapper.findAll('.selected')).toHaveLength(0)
await wrapper.findAll('.chat').at(3).trigger('click')
expect(emitEventMock).toHaveBeenCalledWith('selectChat', store.chats[3])
})

test('Shows selection', async () => {
const wrapper: VueWrapper<any> = mount(ChatList, { props: { chat: store.chats[3], filter: '' } } )
expect(wrapper.findAll('.selected')).toHaveLength(1)
expect(wrapper.findAll('.selected').at(0).find('.title').text()).toBe('Chat 3')
})

test('Filter All', async () => {
const wrapper: VueWrapper<any> = mount(ChatList, { props: { chat: null, filter: 'Subtitle' } } )
expect(wrapper.findAll('.chat')).toHaveLength(10)
})

test('Filter Single', async () => {
const wrapper: VueWrapper<any> = mount(ChatList, { props: { chat: null, filter: '9' } } )
expect(wrapper.findAll('.chat')).toHaveLength(1)
expect(wrapper.findAll('.chat').at(0).find('.title').text()).toBe('Chat 9')
})

test('Multiselect', async () => {
const wrapper: VueWrapper<any> = mount(ChatList, { props: { chat: null, filter: '', selectMode: true } } )
expect(wrapper.findAll('.select')).toHaveLength(10)
expect(wrapper.findAll('.select .selected')).toHaveLength(0)
await wrapper.findAll('.chat').at(3).trigger('click')
expect(wrapper.findAll('.select .selected')).toHaveLength(1)
await wrapper.findAll('.chat').at(5).trigger('click')
expect(wrapper.findAll('.select .selected')).toHaveLength(2)
await wrapper.findAll('.chat').at(5).trigger('click')
expect(wrapper.findAll('.select .selected')).toHaveLength(1)
})

test('Context Menu', async () => {
const wrapper: VueWrapper<any> = mount(ChatList, { ...stubTeleport, props: { chat: null, filter: '' } } )
expect(wrapper.findAll('.context-menu')).toHaveLength(0)
await wrapper.findAll('.chat').at(3).trigger('contextmenu')
expect(wrapper.findAll('.context-menu')).toHaveLength(1)
expect(wrapper.vm.contextMenuActions).toStrictEqual([
{ label: 'Rename Chat', action: 'rename' },
{ label: 'Delete', action: 'delete' },
])
})

test('Rename Chat', async () => {
const wrapper: VueWrapper<any> = mount(ChatList, { ...stubTeleport, props: { chat: null, filter: '' } } )
await wrapper.findAll('.chat').at(3).trigger('contextmenu')
await wrapper.findAll('.context-menu .item')[0].trigger('click')
expect(emitEventMock).toHaveBeenCalledWith('renameChat', store.chats[3])
})

test('Delete Chat', async () => {
const wrapper: VueWrapper<any> = mount(ChatList, { ...stubTeleport, props: { chat: null, filter: '' } } )
await wrapper.findAll('.chat').at(3).trigger('contextmenu')
await wrapper.findAll('.context-menu .item')[1].trigger('click')
expect(emitEventMock).toHaveBeenCalledWith('deleteChat', store.chats[3].uuid)
})

0 comments on commit a1f6e4c

Please sign in to comment.