From 84d7daf1391ebc755444cd9ab8051ff1161b17d7 Mon Sep 17 00:00:00 2001 From: claude_dev Date: Fri, 9 Oct 2026 19:59:26 +0800 Subject: [PATCH] =?UTF-8?q?feat(frontend):=20=E5=9B=A0=E5=AD=90=E8=AF=A6?= =?UTF-8?q?=E6=83=85=E9=A1=B5=E6=A1=A3=E6=A1=88=E8=87=AA=E8=BF=B0=E8=A1=8C?= =?UTF-8?q?+=E5=8D=B1=E9=99=A9=E5=8C=BA=E9=80=81=E5=A2=93=E5=9B=AD(?= =?UTF-8?q?=E7=94=9F=E4=BA=A7=E6=80=81=E9=87=8D=E7=A1=AE=E8=AE=A4/?= =?UTF-8?q?=E6=AD=BB=E5=9B=A0=E5=BF=85=E5=A1=AB)=20[vps]?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../src/views/pipeline/FactorDetail.spec.ts | 74 ++++++++++++++++--- frontend/src/views/pipeline/FactorDetail.vue | 48 +++++++++++- 2 files changed, 110 insertions(+), 12 deletions(-) diff --git a/frontend/src/views/pipeline/FactorDetail.spec.ts b/frontend/src/views/pipeline/FactorDetail.spec.ts index 6bbb8db6..edcc8e9c 100644 --- a/frontend/src/views/pipeline/FactorDetail.spec.ts +++ b/frontend/src/views/pipeline/FactorDetail.spec.ts @@ -1,6 +1,6 @@ import { describe, expect, it, vi, beforeEach } from 'vitest' import { mount, flushPromises } from '@vue/test-utils' -import ElementPlus from 'element-plus' +import ElementPlus, { ElMessageBox } from 'element-plus' import FactorDetail from './FactorDetail.vue' import * as api from '@/api/pipeline' @@ -12,7 +12,12 @@ vi.mock('vue-router', () => ({ })) vi.mock('@/api/pipeline', async (importOriginal) => { const actual = await importOriginal() - return { ...actual, getFactorDetail: vi.fn(), getIcTrend: vi.fn() } + return { + ...actual, + getFactorDetail: vi.fn(), + getIcTrend: vi.fn(), + postFactorVerdict: vi.fn(), + } }) const DETAIL = { @@ -20,35 +25,82 @@ const DETAIL = { name: 'insider_buy_decay60', status: 'incubating', origin: 'decomposer', hypothesis: 'hyp-1', expression: 'zscore(ts_mean(x, 60))', source: 'fundamentals_pit', justification: '增持强度衰减承载假设', + description: '高管增持后 60 日强度衰减,量度信息优势的持续期', createdAt: '2026-10-09', version: '1', icRecentT: null, lastEvalDate: '', }, birth: { jobId: 'job-20261009101513', startedAt: '2026-10-09T10:15:13', rounds: 3, hypId: 'hyp-1' }, } +async function mountDetail(override: object | undefined = undefined) { + vi.mocked(api.getFactorDetail).mockResolvedValue( + override ?? (DETAIL as Awaited>)) + vi.mocked(api.getIcTrend).mockRejectedValue(new Error('no data')) + const w = mount(FactorDetail, { global: { plugins: [ElementPlus] } }) + await flushPromises() + return w +} + describe('FactorDetail.vue(出生档案,验收反馈①)', () => { beforeEach(() => vi.clearAllMocks()) - it('渲染出生档案:来源假设回链/出生方式/批次/表达式', async () => { - vi.mocked(api.getFactorDetail).mockResolvedValue(DETAIL) - vi.mocked(api.getIcTrend).mockRejectedValue(new Error('no data')) - const w = mount(FactorDetail, { global: { plugins: [ElementPlus] } }) - await flushPromises() + it('渲染出生档案:来源假设回链/出生方式/批次/表达式/因子自述', async () => { + const w = await mountDetail() expect(w.text()).toContain('insider_buy_decay60') expect(w.text()).toContain('⚡ AI 分解') expect(w.text()).toContain('hyp-1') expect(w.text()).toContain('job-20261009101513') expect(w.text()).toContain('zscore(ts_mean(x, 60))') expect(w.text()).toContain('3 轮通过') + expect(w.text()).toContain('因子自述') + expect(w.text()).toContain('高管增持后 60 日强度衰减') }) it('manual 因子无出生批次显示 —', async () => { - vi.mocked(api.getFactorDetail).mockResolvedValue({ + const w = await mountDetail({ factor: { ...DETAIL.factor, origin: 'manual', hypothesis: null }, birth: null, }) - vi.mocked(api.getIcTrend).mockRejectedValue(new Error('no data')) - const w = mount(FactorDetail, { global: { plugins: [ElementPlus] } }) - await flushPromises() expect(w.text()).toContain('✍ 人工实现注册') expect(w.text()).toContain('无假设卡') }) }) + +describe('FactorDetail.vue 危险区送墓园(2026-10-09 补丁)', () => { + beforeEach(() => vi.clearAllMocks()) + it('incubating:确认+死因→postFactorVerdict(name,graveyard,[],note)', async () => { + let captured: Record = {} + const promptSpy = vi.spyOn(ElMessageBox, 'prompt').mockImplementation( + (async (_m: string, _t: string, opts: Record = {}) => { + captured = opts + return { value: '概念与既有因子重复' } + }) as typeof ElMessageBox.prompt) + vi.mocked(api.postFactorVerdict).mockResolvedValue({ ok: true, action: 'graveyard' }) + const w = await mountDetail() + await w.find('.grave-btn').trigger('click') + // validator:空死因拒/非空过 + const validator = captured.inputValidator as (v: string) => string | boolean + expect(validator('')).toBeTruthy() + expect(validator(' ')).toBeTruthy() + expect(validator('死因')).toBe(true) + expect(api.postFactorVerdict).toHaveBeenCalledWith( + 'insider_buy_decay60', 'graveyard', [], '概念与既有因子重复') + promptSpy.mockRestore() + }) + it('取消弹窗不发判死请求', async () => { + const promptSpy = vi.spyOn(ElMessageBox, 'prompt').mockRejectedValue(new Error('cancel')) + const w = await mountDetail() + await w.find('.grave-btn').trigger('click') + expect(api.postFactorVerdict).not.toHaveBeenCalled() + promptSpy.mockRestore() + }) + it('promoted 因子先过生产重确认,取消即止', async () => { + const confirmSpy = vi.spyOn(ElMessageBox, 'confirm').mockRejectedValue(new Error('cancel')) + const w = await mountDetail({ + factor: { ...DETAIL.factor, status: 'promoted' }, + birth: null, + }) + await w.find('.grave-btn').trigger('click') + expect(confirmSpy).toHaveBeenCalled() + expect(api.postFactorVerdict).not.toHaveBeenCalled() + confirmSpy.mockRestore() + }) +}) diff --git a/frontend/src/views/pipeline/FactorDetail.vue b/frontend/src/views/pipeline/FactorDetail.vue index e4a94229..98b96cd9 100644 --- a/frontend/src/views/pipeline/FactorDetail.vue +++ b/frontend/src/views/pipeline/FactorDetail.vue @@ -1,7 +1,8 @@ @@ -114,4 +158,6 @@ onMounted(async () => { .stat .v { font-size: 16px; font-weight: 600; font-variant-numeric: tabular-nums; } .stat .v.sm { font-size: 13px; } .empty-box { color: var(--text-3); font-size: 12px; padding: 14px 4px; } +/* 危险区:送墓园 */ +.grave-row { display: flex; align-items: center; gap: 10px; flex-wrap: wrap; }