- r3-07-01 「跳过」无效:补齐事件转发链 MessageList(新增 skip-reveal emit)→ ChatPanel(@skip-reveal 监听)→ store.skipReveal(); 新增 skipReveal.test.js 三层断言(点击定格终态/组件转发/ChatPanel 监听) - r3-52-02 终态文案残留:docgen 轮询 tick 的 done 分支把 generation 步 desc 还原 DOCGEN_STEPS 默认文案「按大纲逐节生成正文」;docgenPoll done 用例改两阶段 mock(running 进度文案 → done 还原)复现原缺陷 - 测试债:b3 bench.store.test.js 删除已下线 sessionSourceItems 引用, 按 #27 修复后语义改断言(后端去重结果原样透出) 自测:r3debug 32 项 + 全套 373 项 + private_docs 23 项全绿; playwright 实测(mock SSE/docgen 202):跳过按钮点击后全文定格、 skip-btn/cursor 消失;docgen done 后第 4 步 desc 还原默认文案;无 JS 错误。 未 push。 Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
152 lines
5.5 KiB
JavaScript
152 lines
5.5 KiB
JavaScript
// @vitest-environment jsdom
|
||
// tests/r3debug/skipReveal.test.js —— r3-07-01「跳过」按钮事件链全链路
|
||
// 断言:AssistantMessage skip-btn 点击 → MessageList 转发 skip-reveal →
|
||
// ChatPanel 落 store.skipReveal() → 消息定格终态(stream 块换 text 块)。
|
||
import { describe, it, expect, vi, beforeEach } from 'vitest';
|
||
import { mount, flushPromises } from '@vue/test-utils';
|
||
import { setActivePinia, createPinia } from 'pinia';
|
||
|
||
// jsdom 无 scrollTo:polyfill(MessageList 挂载滚动用)
|
||
if (typeof window !== 'undefined' && typeof window.Element !== 'undefined' && !window.Element.prototype.scrollTo) {
|
||
window.Element.prototype.scrollTo = vi.fn();
|
||
}
|
||
|
||
vi.mock('../../src/stores/health.js', () => ({
|
||
useHealthStore: vi.fn(() => ({ data: null, qaOk: true })),
|
||
}));
|
||
vi.mock('../../src/api/sessions.js', () => ({
|
||
listSessions: vi.fn(async () => []),
|
||
createSession: vi.fn(async () => ({ id: 's1' })),
|
||
getSession: vi.fn(async () => ({ id: 's1', status: 'done', meta: {}, messages: [] })),
|
||
updateSession: vi.fn(async (id, b) => ({ id, ...b })),
|
||
deleteSession: vi.fn(async () => {}),
|
||
createMessage: vi.fn(async () => ({ id: 'm1' })),
|
||
updateMessage: vi.fn(async () => ({ updated_at: new Date().toISOString() })),
|
||
}));
|
||
vi.mock('../../src/api/qa.js', () => ({
|
||
chatStream: vi.fn(async () => {}),
|
||
chat: vi.fn(),
|
||
search: vi.fn(),
|
||
info: vi.fn(),
|
||
}));
|
||
vi.mock('../../src/api/private.js', () => ({
|
||
askPrivate: vi.fn(async () => ({ hit: false })),
|
||
}));
|
||
vi.mock('../../src/api/docgen.js', () => ({
|
||
generate: vi.fn(),
|
||
getTask: vi.fn(),
|
||
listTasks: vi.fn(),
|
||
getArtifactContent: vi.fn(async () => ({ markdown: '' })),
|
||
getArtifactHtml: vi.fn(async () => ({ ok: true, html: '', css: '' })),
|
||
saveArtifactContent: vi.fn(async () => ({})),
|
||
editArtifact: vi.fn(async () => ({})),
|
||
exportArtifact: vi.fn(async () => ({})),
|
||
uploadLibrary: vi.fn(),
|
||
listLibrary: vi.fn(async () => ({ items: [] })),
|
||
removeLibrary: vi.fn(async () => ({ ok: true })),
|
||
}));
|
||
|
||
import ChatPanel from '../../src/components/chat/ChatPanel.vue';
|
||
import MessageList from '../../src/components/chat/MessageList.vue';
|
||
import AssistantMessage from '../../src/components/chat/AssistantMessage.vue';
|
||
import { useChatStore } from '../../src/stores/chat.js';
|
||
import { useSessionsStore } from '../../src/stores/sessions.js';
|
||
|
||
/** 构造揭示中的会话态:aMsg 带 stream 块 + ctx.reveal */
|
||
function seedRevealing(chat) {
|
||
const msgList = [
|
||
{ id: 'u1', role: 'user', status: 'done', text: '问题', blocks: [{ type: 'text', html: '问题' }] },
|
||
{
|
||
id: 'a1',
|
||
role: 'assistant',
|
||
kind: 'qa',
|
||
status: 'streaming',
|
||
streamErr: null,
|
||
revealId: 1, // revealText 正常流程会写入;skipReveal 据此定位目标消息
|
||
blocks: [
|
||
{ type: 'pipeline', status: 'running', steps: [] },
|
||
{ type: 'stream', text: '部分内容' },
|
||
],
|
||
},
|
||
];
|
||
chat.sessionId = 's1';
|
||
chat.messages = msgList;
|
||
chat.sessionMsgs['s1'] = msgList;
|
||
const aMsg = msgList[1];
|
||
chat.streamCtx['s1'] = {
|
||
sessionId: 's1',
|
||
msgList,
|
||
aMsg,
|
||
controller: null,
|
||
completedOnce: false,
|
||
events: [],
|
||
checkpointTimer: null,
|
||
lastCheckpointAt: 0,
|
||
reveal: { text: '全文内容', aMsg },
|
||
abortAnn: null,
|
||
};
|
||
return aMsg;
|
||
}
|
||
|
||
async function mountPanel() {
|
||
const w = mount(ChatPanel, { global: { stubs: { teleport: true } } });
|
||
await flushPromises();
|
||
return w;
|
||
}
|
||
|
||
describe('r3-07-01 跳过按钮事件链', () => {
|
||
beforeEach(() => {
|
||
setActivePinia(createPinia());
|
||
const sessions = useSessionsStore();
|
||
sessions.items = [{ id: 's1', title: 't', status: 'done', created_at: '', updated_at: '' }];
|
||
});
|
||
|
||
it('全链路:skip-btn 点击 → MessageList 转发 → ChatPanel → store.skipReveal 定格终态', async () => {
|
||
const chat = useChatStore();
|
||
seedRevealing(chat);
|
||
|
||
const w = await mountPanel();
|
||
// 揭示中:skip-btn 存在
|
||
const skipBtn = w.find('.skip-btn');
|
||
expect(skipBtn.exists()).toBe(true);
|
||
|
||
await skipBtn.trigger('click');
|
||
await flushPromises();
|
||
|
||
// store 效果:stream 块移除、text 块定格、状态 done、reveal 清空
|
||
const aMsg = chat.messages[1];
|
||
expect(aMsg.status).toBe('done');
|
||
expect(aMsg.blocks.some((b) => b.type === 'stream')).toBe(false);
|
||
expect(aMsg.blocks.some((b) => b.type === 'text')).toBe(true);
|
||
expect(chat.reveal).toBeNull();
|
||
});
|
||
|
||
it('MessageList 层:AssistantMessage skip-reveal 事件被转发为同名 emit', async () => {
|
||
const chat = useChatStore();
|
||
seedRevealing(chat);
|
||
const w = mount(MessageList, {
|
||
props: { messages: chat.messages, busy: false, reveal: chat.reveal, scrollSeq: 0 },
|
||
global: { stubs: { transition: false } },
|
||
});
|
||
const am = w.findComponent(AssistantMessage);
|
||
expect(am.exists()).toBe(true);
|
||
await am.vm.$emit('skip-reveal');
|
||
expect(w.emitted('skip-reveal')).toBeTruthy();
|
||
expect(w.emitted('skip-reveal')).toHaveLength(1);
|
||
});
|
||
|
||
it('ChatPanel 层:MessageList skip-reveal 事件被监听(组件契约存在)', async () => {
|
||
const chat = useChatStore();
|
||
seedRevealing(chat);
|
||
const w = await mountPanel();
|
||
const ml = w.findComponent(MessageList);
|
||
expect(ml.exists()).toBe(true);
|
||
// 直接对 MessageList 发事件:ChatPanel 的 @skip-reveal 监听应落 store
|
||
await ml.vm.$emit('skip-reveal');
|
||
await flushPromises();
|
||
const aMsg = chat.messages[1];
|
||
expect(aMsg.status).toBe('done');
|
||
expect(aMsg.blocks.some((b) => b.type === 'text')).toBe(true);
|
||
});
|
||
});
|