import { describe, it, expect, vi, beforeEach } from 'vitest' import { mount, flushPromises } from '@vue/test-utils' import { defineComponent, h } from 'vue' import AskPage from './AskPage.vue' const { api } = vi.hoisted(() => ({ api: { authMe: vi.fn(), listProfiles: vi.fn(), getAskQuota: vi.fn(), createAskThread: vi.fn(), sendAskMessage: vi.fn(), sendAskMessageStream: vi.fn(), }, })) vi.mock('../api/client', () => ({ api })) vi.mock('vue-router', () => ({ useRoute: () => ({ query: {}, fullPath: '/ask' }), useRouter: () => ({ back: vi.fn(), push: vi.fn(), replace: vi.fn() }), })) const RouterLinkStub = defineComponent({ props: ['to'], setup(props, { slots }) { return () => h('a', { href: String(props.to) }, slots.default?.()) }, }) describe('AskPage', () => { beforeEach(() => { vi.clearAllMocks() api.authMe.mockResolvedValue({ id: 'u1' }) }) it('shows empty state when no profiles', async () => { api.listProfiles.mockResolvedValue({ items: [] }) api.getAskQuota.mockResolvedValue({ remaining: 3, free_limit: 3, active_membership: false, source: 'free' }) const w = mount(AskPage, { global: { stubs: { RouterLink: RouterLinkStub } } }) await flushPromises() expect(w.text()).toContain('问答') expect(w.text()).toContain('AI 成长助手') expect(w.text()).toContain('先建立你的个人档案') expect(w.text()).toContain('去愈心解码') }) it('loads profiles and sends a message', async () => { api.listProfiles.mockResolvedValue({ items: [{ id: 'p1', relation: 'self', display_name: '我', birth_date: '1990-01-01' }], }) api.getAskQuota.mockResolvedValue({ remaining: 3, free_limit: 3, active_membership: false, source: 'free' }) api.createAskThread.mockResolvedValue({ id: 't1', profile_id: 'p1' }) api.sendAskMessageStream.mockImplementation( async ( _tid: string, _content: string, handlers: { onMeta?: (m: unknown) => void onDelta?: (t: string) => void onDone?: (p: unknown) => void }, ) => { handlers.onMeta?.({ id: 'm1', role: 'user', content: '你好', thread_id: 't1', created_at: '' }) handlers.onDelta?.('结合档案的回复') handlers.onDone?.({ assistant_message: { id: 'm2', role: 'assistant', content: '结合档案的回复', thread_id: 't1', created_at: '', }, quota: { remaining: 2, free_limit: 3, active_membership: false, source: 'free' }, }) }, ) const w = mount(AskPage, { global: { stubs: { RouterLink: RouterLinkStub } } }) await flushPromises() expect(w.text()).toContain('还可聊 3 次') await w.find('textarea').setValue('你好') await w.find('form').trigger('submit') await flushPromises() expect(api.sendAskMessageStream).toHaveBeenCalled() expect(w.text()).toContain('结合档案的回复') expect(w.text()).toContain('还可聊 2 次') }) it('shows boot error with retry', async () => { api.listProfiles.mockRejectedValueOnce(new Error('网络错误')) api.getAskQuota.mockRejectedValueOnce(new Error('网络错误')) const w = mount(AskPage, { global: { stubs: { RouterLink: RouterLinkStub } } }) await flushPromises() expect(w.text()).toContain('网络错误') api.listProfiles.mockResolvedValue({ items: [] }) api.getAskQuota.mockResolvedValue({ remaining: 3, free_limit: 3, active_membership: false, source: 'free' }) await w.find('button.retry').trigger('click') await flushPromises() expect(w.text()).toContain('先建立你的个人档案') }) })