115 lines
7.1 KiB
TypeScript
115 lines
7.1 KiB
TypeScript
import type { AskThreadProps } from './AskThread'
|
|
import type { Ask } from './qa'
|
|
import type { ProfileChoice, QaEntryView } from './types'
|
|
import { renderToStaticMarkup } from 'react-dom/server'
|
|
import { describe, expect, it } from 'vitest'
|
|
import { AskThread } from './AskThread'
|
|
|
|
const NOW = new Date(2026, 8, 26, 15, 0).getTime()
|
|
|
|
const HISTORY: QaEntryView[] = [
|
|
{ id: 'e1', question: '为什么普通用户不能看别人的资料?', answer: '这是**权限控制**:不是管理员、又不是查自己时抛 `Forbidden`。', createdAt: new Date(2026, 8, 23, 10, 0).toISOString() },
|
|
{ id: 'e2', question: 'UserRole 还有哪些角色?', answer: '只有 ADMIN 和 USER。', createdAt: new Date(2026, 8, 25, 10, 0).toISOString() },
|
|
]
|
|
|
|
const PROFILES: ProfileChoice = { options: [{ id: 'offical', label: 'offical' }, { id: 'sonnet', label: 'sonnet' }], selected: 'sonnet' }
|
|
|
|
function render(props: Partial<AskThreadProps>): string {
|
|
return renderToStaticMarkup(<AskThread history={[]} asks={[]} open={false} profiles={PROFILES} now={NOW} onSelectProfile={() => {}} onAsk={() => {}} onStop={() => {}} onDelete={() => {}} onClose={() => {}} {...props} />)
|
|
}
|
|
|
|
const text = (html: string): string => html.replace(/<[^>]*>/g, '')
|
|
|
|
function ask(overrides: Partial<Ask>): Ask {
|
|
return { askId: 'a1', anchor: 'x', question: 'Forbidden 抛出去以后前端收到什么?', text: '', status: 'running', ...overrides }
|
|
}
|
|
|
|
describe('块下面的问答区', () => {
|
|
it('没问过、没展开:什么都不画', () => {
|
|
expect(render({})).toBe('')
|
|
})
|
|
|
|
it('问过、没展开:一行「问过 N 次」,下面默认摊开灰字历史,没有输入框', () => {
|
|
const html = render({ history: HISTORY })
|
|
expect(html).toContain('data-collapsed')
|
|
expect(text(html)).toMatch(/^问过 2 次/)
|
|
expect(html).toContain('aria-expanded="true"')
|
|
expect(html.match(/data-history=""/g)).toHaveLength(2)
|
|
expect(text(html)).toContain('为什么普通用户不能看别人的资料?3 天前')
|
|
expect(text(html)).toContain('UserRole 还有哪些角色?昨天')
|
|
expect(html).not.toContain('<textarea')
|
|
})
|
|
|
|
it('展开:灰字的历史(问题、时间、「已理解,删除」、回答前两行),当前这一轮照常颜色,底下输入框', () => {
|
|
const html = render({ open: true, history: HISTORY, asks: [ask({ text: '`Forbidden` 定义在 **core**', activity: '读取 exceptions.py' })] })
|
|
expect(html.match(/data-history=""/g)).toHaveLength(2)
|
|
expect(text(html)).toContain('为什么普通用户不能看别人的资料?3 天前')
|
|
expect(text(html)).toContain('UserRole 还有哪些角色?昨天')
|
|
expect(html.match(/data-tooltip="已理解,删除" aria-label="已理解,删除"/g)).toHaveLength(2)
|
|
expect(text(html)).toContain('这是权限控制:不是管理员、又不是查自己时抛 Forbidden。')
|
|
expect(html).toContain('<hr class="cr-qa-divider"/>')
|
|
expect(html).toContain('data-current=""')
|
|
expect(html).toContain('data-streaming=""><p><code>Forbidden</code> 定义在 <strong>core</strong></p>')
|
|
expect(text(html)).toContain('读取 exceptions.py')
|
|
expect(html).toContain('placeholder="继续问… Enter 发送,Esc 收起"')
|
|
expect(html).toContain('data-tooltip="停止" aria-label="停止"')
|
|
expect(html).not.toContain('aria-label="发送"')
|
|
})
|
|
|
|
it('还没问过:输入框提示问这块代码,发送按钮在没字时不能点;答完的回答按 Markdown 排、不再闪光标', () => {
|
|
expect(render({ open: true })).toContain('placeholder="问这块代码… Enter 发送,Esc 收起"')
|
|
expect(render({ open: true })).toMatch(/aria-label="发送" disabled=""/)
|
|
const done = render({ open: true, asks: [ask({ status: 'done', text: '答完了' })] })
|
|
expect(done).toContain('<div class="cr-markdown cr-qa-answer"><p>答完了</p>')
|
|
expect(done).not.toContain('data-streaming')
|
|
expect(done).not.toContain('role="status"')
|
|
})
|
|
|
|
it('出错、停止写在这一问下面', () => {
|
|
const failed = render({ open: true, asks: [ask({ status: 'failed', error: 'Claude Code 没登录或登录过期了' })] })
|
|
expect(failed).toContain('role="alert"')
|
|
expect(text(failed)).toContain('Claude Code 没登录或登录过期了')
|
|
expect(text(render({ open: true, asks: [ask({ status: 'stopped', text: '一半' })] }))).toContain('已停止,这次没存。')
|
|
expect(text(render({ open: true, asks: [ask({})] }))).toContain('正在想…')
|
|
})
|
|
|
|
it('输入框右边、发送按钮前面是 profile 下拉:显示选中的名字,键盘能打开(按钮、弹出列表);没收到选项时写「默认」', () => {
|
|
const html = render({ open: true })
|
|
const row = html.slice(html.indexOf('<div class="cr-qa-input">'))
|
|
const [textarea, select, send] = ['<textarea', 'cr-qa-profile', 'aria-label="发送"'].map(mark => row.indexOf(mark))
|
|
expect(textarea).toBeGreaterThanOrEqual(0)
|
|
expect(select).toBeGreaterThan(textarea)
|
|
expect(send).toBeGreaterThan(select)
|
|
const trigger = row.slice(row.lastIndexOf('<button', select), row.indexOf('</button>', select))
|
|
expect(trigger).toContain('aria-haspopup="listbox"')
|
|
expect(trigger).toContain('aria-expanded="false"')
|
|
expect(trigger).toContain('data-tooltip="用哪个 Claude Code 配置(profile)回答,下一个问题起用它"')
|
|
expect(text(trigger)).toBe('sonnet')
|
|
const none = render({ open: true, profiles: { options: [], selected: '' } })
|
|
expect(text(none.slice(none.indexOf('cr-qa-profile')))).toContain('默认')
|
|
})
|
|
|
|
it('历史每条的灰字写时间和用哪个 profile 答的;以前存的没有 profile 只写时间', () => {
|
|
const html = render({ open: true, history: [{ ...HISTORY[0]!, profile: 'sonnet' }, HISTORY[1]!] })
|
|
expect(text(html)).toContain('为什么普通用户不能看别人的资料?3 天前 · sonnet')
|
|
expect(text(html)).toContain('UserRole 还有哪些角色?昨天')
|
|
expect(text(html)).not.toContain('昨天 ·')
|
|
})
|
|
|
|
it('问题带的图片:历史和当前这一轮在问题下面一排缩略图,点开看大图,不能去掉;没带图片的不画这一排', () => {
|
|
const stored = ['https://file+.vscode-resource.vscode-cdn.net/work/.spx/code-qa/e1-1.png', 'https://file+.vscode-resource.vscode-cdn.net/work/.spx/code-qa/e1-2.jpg']
|
|
const html = render({ open: true, history: [{ ...HISTORY[0]!, images: stored }, HISTORY[1]!], asks: [ask({ images: ['data:image/png;base64,AAA'] })] })
|
|
const [first, second] = html.split('data-history=""').slice(1)
|
|
expect(first).toContain('<div class="cr-qa-thumbs">')
|
|
expect(first.indexOf('cr-qa-thumbs')).toBeGreaterThan(first.indexOf('cr-qa-head'))
|
|
expect(first.indexOf('cr-qa-thumbs')).toBeLessThan(first.indexOf('cr-qa-preview'))
|
|
expect(first).toContain(`<img src="${stored[0]}" alt=""/>`)
|
|
expect(first).toContain('aria-label="看第 2 张图片的大图"')
|
|
expect(second.slice(0, second.indexOf('data-current'))).not.toContain('cr-qa-thumbs')
|
|
const current = html.slice(html.indexOf('data-current'))
|
|
expect(current).toContain('<img src="data:image/png;base64,AAA" alt=""/>')
|
|
expect(html).not.toContain('cr-qa-thumb-remove')
|
|
expect(html).not.toContain('data-pending')
|
|
})
|
|
})
|