324 lines
14 KiB
JavaScript
324 lines
14 KiB
JavaScript
// @vitest-environment jsdom
|
||
// tests/frontend/unit/bioflow/panes.test.js —— P5 生信面板组件测试(L4)
|
||
// 覆盖 TC-BIO-JOB-001~016(BioJobsPane)与 TC-BIO-PANE-001~016(BioArchivePane):
|
||
// 任务列表 session-item 形态/阶段 DAG 取色/manifest+日志浏览/重跑/删除确认/running 禁用/
|
||
// 探活失败隐藏 TAB(bench store 断言由 benchpanel 层测试覆盖)/复用发起预填表单。
|
||
import { describe, it, expect, vi, beforeEach } from 'vitest';
|
||
import { mount, flushPromises } from '@vue/test-utils';
|
||
import { createPinia, setActivePinia } from 'pinia';
|
||
|
||
vi.mock('../../../../ui/src/api/bioflow.js', () => ({
|
||
listBioflowJobs: vi.fn(async () => ({
|
||
ok: true,
|
||
data: {
|
||
items: [
|
||
{ id: 'job-1', title: '水稻 SPL 鉴定', status: 'running', progress: 33, stages: [{ stage: 'identify', status: 'done' }, { stage: 'align', status: 'running' }, { stage: 'phylogeny', status: 'waiting' }] },
|
||
{ id: 'job-2', title: '玉米 bZIP 分析', status: 'done', progress: 100, stages: [{ stage: 'identify', status: 'done' }] },
|
||
{ id: 'job-3', title: '小麦 WRKY 失败任务', status: 'failed', progress: 50, stages: [{ stage: 'identify', status: 'done' }, { stage: 'align', status: 'failed' }] },
|
||
],
|
||
total: 3,
|
||
},
|
||
})),
|
||
getBioflowJob: vi.fn(async () => ({
|
||
ok: true,
|
||
data: {
|
||
id: 'job-1', title: '水稻 SPL 鉴定', status: 'running',
|
||
stages: [{ stage: 'identify', status: 'done' }, { stage: 'align', status: 'running' }, { stage: 'phylogeny', status: 'waiting' }],
|
||
},
|
||
})),
|
||
deleteBioflowJob: vi.fn(async () => {}),
|
||
rerunBioflowStage: vi.fn(async () => ({ ok: true, data: {} })),
|
||
getBioflowStageManifest: vi.fn(async () => ({
|
||
ok: true,
|
||
data: { manifest: { status: 'success', artifacts: [] }, log_tail: 'log-line-1\nlog-line-2' },
|
||
})),
|
||
listBioflowArchives: vi.fn(async () => ({
|
||
ok: true,
|
||
data: {
|
||
items: [
|
||
{ id: 'arc-1', name: 'SPL 水稻鉴定', description: '描述', size_bytes: 2048, reused_count: 1, created_at: '2026-09-06T00:00:00Z', report_artifact: '/d/report.html' },
|
||
{ id: 'arc-2', name: 'bZIP 分析', size_bytes: 1024, created_at: '2026-09-05T00:00:00Z' },
|
||
],
|
||
total: 2,
|
||
},
|
||
})),
|
||
getBioflowArchive: vi.fn(async () => ({
|
||
ok: true,
|
||
data: {
|
||
id: 'arc-1', name: 'SPL 水稻鉴定', stage_chain: ['identify', 'align', 'phylogeny'],
|
||
params: { identify: { prefix: 'Ft.SPL' } },
|
||
products: [{ name: 'report.html', size: 2048 }, { name: 'final.zip', size: 1024 }],
|
||
report_artifact: '/d/report.html',
|
||
},
|
||
})),
|
||
deleteBioflowArchive: vi.fn(async () => {}),
|
||
runBioflowArchive: vi.fn(async () => ({ ok: true, data: { id: 'job-9', stages: [] } })),
|
||
bioflowHealth: vi.fn(async () => ({ ok: true })),
|
||
createBioflowJob: vi.fn(async () => ({ ok: true, data: { id: 'j1', stages: [] } })),
|
||
confirmBioflowStage: vi.fn(async () => ({ ok: true, data: {} })),
|
||
skipBioflowStage: vi.fn(async () => ({ ok: true, data: {} })),
|
||
pauseBioflowJob: vi.fn(async () => ({ ok: true, data: {} })),
|
||
resumeBioflowJob: vi.fn(async () => ({ ok: true, data: {} })),
|
||
createBioflowArchive: vi.fn(async () => ({ ok: true, data: {} })),
|
||
bioflowCatalog: vi.fn(async () => ({ ok: true, data: { stages: [] } })),
|
||
uploadBioflowFile: vi.fn(async () => ({ ok: true, data: {} })),
|
||
bioflowChatStream: vi.fn(async () => { throw new Error('unused'); }),
|
||
bioflowJobEvents: vi.fn(async () => { throw new Error('unused'); }),
|
||
bioflowAsk: vi.fn(async () => ({ ok: true, answer: { citations: [] } })),
|
||
reviseBioflowReport: vi.fn(async () => ({ ok: true, data: {} })),
|
||
registerBioflowReport: vi.fn(async () => ({ ok: true, data: {} })),
|
||
runBioflowArchiveByName: vi.fn(async () => ({ ok: true, data: {} })),
|
||
}));
|
||
|
||
vi.mock('../../../../ui/src/api/sessions.js', () => ({
|
||
createMessage: vi.fn(async () => ({ id: 'm1' })),
|
||
updateMessage: vi.fn(async () => ({ id: 'm1' })),
|
||
createSession: vi.fn(async () => ({ id: 'sess-1', title: 'x', status: 'idle', created_at: 'x', updated_at: 'x', message_count: 0, meta: {} })),
|
||
updateSession: vi.fn(async () => ({})),
|
||
deleteSession: vi.fn(async () => {}),
|
||
listSessions: vi.fn(async () => []),
|
||
getSession: vi.fn(),
|
||
getSessionSources: vi.fn(async () => ({ total: 0, sources: [] })),
|
||
}));
|
||
|
||
vi.mock('../../../../ui/src/api/qa.js', () => ({
|
||
chatStream: vi.fn(async () => { throw new Error('unused'); }),
|
||
search: vi.fn(async () => { throw new Error('unused'); }),
|
||
}));
|
||
|
||
vi.mock('../../../../ui/src/api/extraction.js', () => ({
|
||
listJobs: vi.fn(async () => ({ jobs: [], count: 0 })),
|
||
getJob: vi.fn(),
|
||
uploadPaper: vi.fn(),
|
||
getRunLog: vi.fn(async () => ({ text: '' })),
|
||
getProcessingStatus: vi.fn(),
|
||
getSummary: vi.fn(),
|
||
}));
|
||
vi.mock('../../../../ui/src/api/kg.js', () => ({
|
||
kgStatus: vi.fn(async () => ({ ok: true })),
|
||
kgOverview: vi.fn(),
|
||
searchEntities: vi.fn(),
|
||
getEntity: vi.fn(),
|
||
getNeighborhood: vi.fn(),
|
||
}));
|
||
|
||
import BioJobsPane from '../../../../ui/src/components/bench/BioJobsPane.vue';
|
||
import BioArchivePane from '../../../../ui/src/components/bench/BioArchivePane.vue';
|
||
import { useBioflowStore } from '../../../../ui/src/stores/bioflow.js';
|
||
import { useChatStore } from '../../../../ui/src/stores/chat.js';
|
||
import * as bioflowApi from '../../../../ui/src/api/bioflow.js';
|
||
|
||
function setup() {
|
||
setActivePinia(createPinia());
|
||
const bioflow = useBioflowStore();
|
||
const chat = useChatStore();
|
||
chat.sessionId = 'sess-1';
|
||
chat.messages = [{ id: 'a1', role: 'assistant', kind: 'bioflow', status: 'done', blocks: [], created_at: 'x', updated_at: 'x' }];
|
||
return { bioflow, chat };
|
||
}
|
||
|
||
describe('BioJobsPane —— TC-BIO-JOB-001~016(L4)', () => {
|
||
beforeEach(() => {
|
||
setup();
|
||
vi.clearAllMocks();
|
||
});
|
||
|
||
it('任务列表渲染 3 个 session-item 形态列表项(标题 13px + 状态徽章;TC-BIO-JOB-002)', async () => {
|
||
const w = mount(BioJobsPane);
|
||
await flushPromises();
|
||
const items = w.findAll('.bj-item');
|
||
expect(items.length).toBe(3);
|
||
expect(items[0].text()).toContain('水稻 SPL 鉴定');
|
||
expect(items[0].find('.badge').exists()).toBe(true);
|
||
expect(items[0].find('.bj-progress').text()).toBe('33%');
|
||
});
|
||
|
||
it('点击任务项:选中并展开详情(阶段 DAG 显示;TC-BIO-JOB-004/ACT-090)', async () => {
|
||
const w = mount(BioJobsPane);
|
||
await flushPromises();
|
||
await w.findAll('.bj-item')[0].trigger('click');
|
||
await flushPromises();
|
||
expect(w.find('.bj-detail').exists()).toBe(true);
|
||
const dag = w.findAll('.bj-dag-row');
|
||
expect(dag.length).toBe(3);
|
||
// DAG 状态取色(TC-BIO-JOB-005:done/running/waiting 三色点)
|
||
expect(dag[0].classes()).toContain('done');
|
||
expect(dag[1].classes()).toContain('running');
|
||
expect(dag[2].classes()).toContain('waiting');
|
||
});
|
||
|
||
it('DAG 节点 8px 圆点按阶段状态取色(TC-BIO-JOB-005 细节)', async () => {
|
||
const w = mount(BioJobsPane);
|
||
await flushPromises();
|
||
await w.findAll('.bj-item')[0].trigger('click');
|
||
await flushPromises();
|
||
const dots = w.findAll('.bj-dag-dot');
|
||
expect(dots[0].text()).toBe('✓');
|
||
expect(dots[1].text()).toBe('●');
|
||
expect(dots[2].text()).toBe('◌');
|
||
});
|
||
|
||
it('点击 DAG 阶段行:打开 manifest + 日志尾(code-block 渲染;TC-BIO-JOB-006/ACT-094)', async () => {
|
||
const w = mount(BioJobsPane);
|
||
await flushPromises();
|
||
await w.findAll('.bj-item')[0].trigger('click');
|
||
await flushPromises();
|
||
await w.findAll('.bj-dag-row')[0].trigger('click');
|
||
await flushPromises();
|
||
expect(bioflowApi.getBioflowStageManifest).toHaveBeenCalledWith('job-1', 'identify');
|
||
expect(w.find('.bj-manifest').exists()).toBe(true);
|
||
const code = w.find('.bj-manifest .code-block');
|
||
expect(code.text()).toContain('log-line-1');
|
||
expect(code.text()).toContain('manifest');
|
||
});
|
||
|
||
it('重跑按钮:选中含 failed 阶段任务时发出 rerun(TC-BIO-JOB-007/ACT-063)', async () => {
|
||
const w = mount(BioJobsPane);
|
||
await flushPromises();
|
||
// job-3 含 failed 阶段
|
||
await w.findAll('.bj-item')[2].trigger('click');
|
||
await flushPromises();
|
||
const rerunBtn = w.find('.bj-rerun');
|
||
expect(rerunBtn.exists()).toBe(true);
|
||
await rerunBtn.trigger('click');
|
||
await flushPromises();
|
||
expect(bioflowApi.rerunBioflowStage).toHaveBeenCalledWith('job-3', 'align', null);
|
||
});
|
||
|
||
it('删除:弹二次确认 → 确认后 DELETE 并移除(TC-BIO-JOB-008/ACT-064)', async () => {
|
||
const w = mount(BioJobsPane);
|
||
await flushPromises();
|
||
// job-2 状态 done 可删除
|
||
const delBtn = w.findAll('.bj-del')[1];
|
||
await delBtn.trigger('click');
|
||
expect(w.find('.bj-confirm').exists()).toBe(true);
|
||
await w.find('.pane-btn.danger').trigger('click');
|
||
await flushPromises();
|
||
expect(bioflowApi.deleteBioflowJob).toHaveBeenCalledWith('job-2');
|
||
});
|
||
|
||
it('删除二次确认取消:无 DELETE 请求(TC-BIO-JOB/ACT-067 语义)', async () => {
|
||
const w = mount(BioJobsPane);
|
||
await flushPromises();
|
||
await w.findAll('.bj-del')[1].trigger('click');
|
||
await w.find('.bj-confirm .pane-btn').trigger('click'); // 取消
|
||
expect(bioflowApi.deleteBioflowJob).not.toHaveBeenCalled();
|
||
});
|
||
|
||
it('running 任务删除按钮禁用(TC-BIO-JOB-009/ACT-068)', async () => {
|
||
const w = mount(BioJobsPane);
|
||
await flushPromises();
|
||
const delBtn = w.findAll('.bj-del')[0]; // job-1 running
|
||
expect(delBtn.attributes('disabled')).toBeDefined();
|
||
});
|
||
|
||
it('空任务列表:空态文案显示(TC-BIO-JOB-010)', async () => {
|
||
bioflowApi.listBioflowJobs.mockResolvedValueOnce({ ok: true, data: { items: [], total: 0 } });
|
||
const w = mount(BioJobsPane);
|
||
await flushPromises();
|
||
expect(w.find('.bj-empty').text()).toContain('暂无生信任务');
|
||
});
|
||
|
||
it('列表加载失败:错误态 + 重新加载(失败必须可见)', async () => {
|
||
bioflowApi.listBioflowJobs.mockRejectedValueOnce(new Error('down'));
|
||
const w = mount(BioJobsPane);
|
||
await flushPromises();
|
||
expect(w.find('.bj-state.error').exists()).toBe(true);
|
||
});
|
||
});
|
||
|
||
describe('BioArchivePane —— TC-BIO-PANE-001~016(L4)', () => {
|
||
beforeEach(() => {
|
||
setup();
|
||
vi.clearAllMocks();
|
||
});
|
||
|
||
it('档案列表渲染 2 项(标题+大小;TC-BIO-PANE-002)', async () => {
|
||
const w = mount(BioArchivePane);
|
||
await flushPromises();
|
||
const items = w.findAll('.bap-item');
|
||
expect(items.length).toBe(2);
|
||
expect(items[0].text()).toContain('SPL 水稻鉴定');
|
||
expect(items[0].text()).toContain('KB');
|
||
});
|
||
|
||
it('点击档案项:详情显示阶段链/参数/产物/报告(TC-BIO-PANE-003/004/005)', async () => {
|
||
const w = mount(BioArchivePane);
|
||
await flushPromises();
|
||
await w.findAll('.bap-item')[0].trigger('click');
|
||
await flushPromises();
|
||
expect(w.find('.bap-detail').exists()).toBe(true);
|
||
expect(w.find('.bap-stages').text()).toContain('identify');
|
||
expect(w.find('.bap-stages').text()).toContain('phylogeny');
|
||
expect(w.find('.bap-products').text()).toContain('report.html');
|
||
expect(w.find('.bap-report').exists()).toBe(false); // 未点查看报告
|
||
// 查看报告按钮存在(report_artifact 非空)
|
||
const btns = w.findAll('.bap-detail .pane-btn');
|
||
expect(btns.some((b) => b.text().includes('查看报告'))).toBe(true);
|
||
});
|
||
|
||
it('查看报告:iframe 打开归档报告(TC-BIO-PANE-004)', async () => {
|
||
const w = mount(BioArchivePane);
|
||
await flushPromises();
|
||
await w.findAll('.bap-item')[0].trigger('click');
|
||
await flushPromises();
|
||
const viewBtn = w.findAll('.bap-detail .pane-btn').find((b) => b.text().includes('查看报告'));
|
||
await viewBtn.trigger('click');
|
||
expect(w.find('.bap-report-frame').exists()).toBe(true);
|
||
expect(w.find('.bap-report-frame').attributes('src')).toContain('/api/bioflow/archives/arc-1/report');
|
||
});
|
||
|
||
it('删除归档:二次确认 → DELETE 并移除(TC-BIO-PANE-006)', async () => {
|
||
const w = mount(BioArchivePane);
|
||
await flushPromises();
|
||
await w.findAll('.bap-item')[0].trigger('click');
|
||
await flushPromises();
|
||
const delBtn = w.find('.bap-del');
|
||
await delBtn.trigger('click');
|
||
expect(w.find('.bap-confirm').exists()).toBe(true);
|
||
await w.find('.pane-btn.danger').trigger('click');
|
||
await flushPromises();
|
||
expect(bioflowApi.deleteBioflowArchive).toHaveBeenCalledWith('arc-1');
|
||
});
|
||
|
||
it('删除取消:无 DELETE 请求(TC-BIO-PANE-007)', async () => {
|
||
const w = mount(BioArchivePane);
|
||
await flushPromises();
|
||
await w.findAll('.bap-item')[0].trigger('click');
|
||
await flushPromises();
|
||
await w.find('.bap-del').trigger('click');
|
||
await w.find('.bap-confirm .pane-btn').trigger('click'); // 取消
|
||
expect(bioflowApi.deleteBioflowArchive).not.toHaveBeenCalled();
|
||
});
|
||
|
||
it('复用发起:装载预填表单并挂到消息流(TC-BIO-PANE-008/009/ACT-070)', async () => {
|
||
const w = mount(BioArchivePane);
|
||
await flushPromises();
|
||
await w.findAll('.bap-item')[0].trigger('click');
|
||
await flushPromises();
|
||
const reuseBtn = w.find('.bap-reuse');
|
||
await reuseBtn.trigger('click');
|
||
await flushPromises();
|
||
// chat.showReuseForm 把 bioflow_reuse 块挂到助手消息
|
||
const chat = useChatStore();
|
||
const blk = chat.messages.find((m) => m.role === 'assistant').blocks.find((b) => b.type === 'bioflow_reuse');
|
||
expect(blk).toBeTruthy();
|
||
expect(blk.archiveId).toBe('arc-1');
|
||
expect(blk.archiveName).toBe('SPL 水稻鉴定');
|
||
});
|
||
|
||
it('空档案列表:空态文案(TC-BIO-PANE-011)', async () => {
|
||
bioflowApi.listBioflowArchives.mockResolvedValueOnce({ ok: true, data: { items: [], total: 0 } });
|
||
const w = mount(BioArchivePane);
|
||
await flushPromises();
|
||
expect(w.find('.bap-empty').text()).toContain('暂无生信档案');
|
||
});
|
||
|
||
it('档案大小人类可读格式(TC-BIO-PANE-012)', async () => {
|
||
const w = mount(BioArchivePane);
|
||
await flushPromises();
|
||
const sizes = w.findAll('.bap-size');
|
||
expect(sizes[0].text()).toContain('KB');
|
||
});
|
||
});
|