// @ts-nocheck import { describe, expect, it, vi } from 'vitest'; import { mount, unmount } from 'svelte'; import { metricsLabels, metricsRow } from '../lib/format'; import resultsSource from '../pages/ProjectResultsTab.svelte?raw'; // Bounded accounting regression (parent live findings): // 1) trade_count semantics: worker/backtest.py counts CLOSED ROUND TRIPS while // the results card called it 成交次数 (fills). Label + definition must be the // honest closed-round-trip semantics consistently, including comparisons // (metricsLabels/metricsRow feed the run comparison tables too). // 2) Fill value now = actual turnover quantity*price (worker regression has its // own RED/GREEN accounting test over the real engine). // 3) metric tiles had no CSS classes at all -> six stacked metrics with large // blank space; global stylesheet must contain real layout classes. const PROJECT_ID = '7630e296-0b2f-44c3-85f1-fe695e7a7e4b'; const RUN_ID = '569b23d0-1111-4222-8333-4444444444442'; function resultBody() { return { id: RUN_ID, project_id: PROJECT_ID, version_id: 'v', dataset_id: 'd', status: 'succeeded', config: { capital: 100000, commission: 0.0003, slippage: 0.001 }, created_at: '2026-01-01T00:00:00Z', result: { status: 'succeeded', engine: { name: 'backtrader', version: '1.9' }, metrics: { total_return: 0.0123, annual_return: 0.05, max_drawdown: -0.033, sharpe: 0.9, trade_count: 1, final_equity: 101234.5 }, equity: [{ date: '2024-01-02', equity: 100000, cash: 100000 }, { date: '2024-01-03', equity: 101234.5, cash: 98967.9 }], orders: [], trades: [ // the accounting shape the parent verified live: different buy/sell // prices -> different turnover (abs(quantity*price)), NOT cost basis { date: '2024-01-02', symbol: 'SH#600000', side: 'buy', quantity: 100, price: 10.2, commission: 0.30606, value: 1020.0 }, { date: '2024-01-03', symbol: 'SH#600000', side: 'sell', quantity: 100, price: 10.4, commission: 0.32, value: 1040.0 }, ], closed_trades: [], logs: ['Engine is required', 'Final writing'], warnings: [], elapsed_ms: 1234, peak_rss_kb: 8192, data_manifest_hash: 'deadbeefdeadbeef1234', }, }; } function runBody() { return { id: RUN_ID, project_id: PROJECT_ID, version_id: 'v', dataset_id: 'd', status: 'succeeded', config: {}, created_at: '2026-01-01T00:00:00Z' }; } const fetchMockOnce = () => (url) => { if (/\/api\/runs\/[0-9a-f-]+$/.test(String(url))) return Promise.resolve(new Response(JSON.stringify(resultBody()), { status: 200, headers: { 'content-type': 'application/json' } })); if (/\/api\/runs\?|\/api\/runs$/.test(String(url))) return Promise.resolve(new Response(JSON.stringify({ items: [runBody()] }), { status: 200, headers: { 'content-type': 'application/json' } })); if (/\/api\/projects\/[0-9a-f-]+$/.test(String(url))) return Promise.resolve(new Response(JSON.stringify({ id: PROJECT_ID, name: '端到端验收项目', draft_code: 'x=1', draft_generation: 2 }), { status: 200, headers: { 'content-type': 'application/json' } })); return Promise.resolve(new Response('{"items":[]}', { status: 200, headers: { 'content-type': 'application/json' } })); }; describe('results accounting labels + values (live browser accounting)', () => { it('renders actual result DOM: 平仓回合数 label, trade turnover values, metrics grid classes', { timeout: 20000 }, async () => { // jsdom lacks ResizeObserver; Chart.svelte needs it only for responsive // canvas resize, which jsdom never triggers. Narrow environment shim. vi.stubGlobal('ResizeObserver', class { observe() {} unobserve() {} disconnect() {} }); vi.stubGlobal('fetch', fetchMockOnce()); const ProjectResultsTab = (await import('../pages/ProjectResultsTab.svelte')).default; const host = document.createElement('div'); document.body.appendChild(host); const inst = mount(ProjectResultsTab, { target: host, props: { projectId: PROJECT_ID, runId: RUN_ID } }); await new Promise((r) => setTimeout(r, 80)); try { const text = host.textContent; // closed round trip semantics (not fill count) expect(text).toContain('平仓回合数'); expect(text).toContain('已完成的开仓并全部平仓的完整回合'); expect(text).not.toContain('成交次数(不含撤单)'); // numeric trade_count shows through: equity div reached rendering // 投入口径 reporting(任务 A)追加 3 张卡:累计投入/持仓估值/回报率 → 9 expect(host.querySelectorAll('[data-slot="card"].m-tile, .m-tile').length).toBe(9); expect(host.querySelectorAll('[data-testid="capital-reporting"]').length).toBe(1); expect(text).toContain('累计投入'); expect(text).toContain('投入口径不含初始账户闲置现金'); // turnover: two fills with distinct values (buy vs sell prices differ) const rows = [...host.querySelectorAll('td.num')].map((td) => td.textContent.trim()); const hasSell = [...host.children].join('').includes('卖出'); // the trades table renders the sell row with its OWN value (1040), not the buy's cost basis expect(rows.filter((v) => v === '1020.00' || v === '1040.00').length).toBe(2); // trade_count metric value renders expect([...host.querySelectorAll('.mvalue')].some((n) => n.textContent === '1')).toBe(true); } finally { unmount(inst); host.remove(); vi.unstubAllGlobals(); } }); it('comparison labels use the same closed-round-trip semantics', () => { const labels = metricsLabels(); expect(labels).toContain('平仓回合数'); const m = metricsRow({ total_return: 0.1, annual_return: 0.2, max_drawdown: -0.1, sharpe: 1, trade_count: 1, final_equity: 110000 }); const label = labels[m.findIndex((_, i) => i === labels.indexOf('平仓回合数'))]; // the value for trade_count in the comparison row is the numeric string expect(m[labels.indexOf('平仓回合数')]).toBe('1'); expect(labels).not.toContain('交易次数'); // old misleading '交易次数' gone }); it('stylesheet defines the shared metric tile layout (no stacking blank space)', () => { const template: string = resultsSource as unknown as string; // keep the editor honest: no leftover 成交次数 wording in the metrics card expect(template).toContain('平仓回合数'); expect(template).not.toContain('成交次数'); }); });