import { mount } from '@vue/test-utils' import { afterEach, describe, expect, it, vi } from 'vitest' import MultiStrategyPanel from './components/MultiStrategyPanel.vue' afterEach(() => { vi.restoreAllMocks() }) const STRATEGIES = [ { key: 'sma_cross', name: 'SMA Crossover', description: '', params: [{ name: 'fast', type: 'int', default: 10, description: '' }], }, { key: 'ema_cross', name: 'EMA Crossover', description: '', params: [{ name: 'fast', type: 'int', default: 9, description: '' }], }, ] const EMPTY_STATUS = { running: false, strategies: [], total_pnl: 0, equity: 0 } const PRESETS = [ { id: 1, name: 'BTC fast', strategy_key: 'ema_cross', symbol: 'ETH/USDT', timeframe: '15m', params: { fast: 3 }, }, ] function mountPanel(multiStatus = EMPTY_STATUS, presets = []) { return mount(MultiStrategyPanel, { props: { strategies: STRATEGIES, multiStatus, presets, busy: false, loading: false }, }) } describe('MultiStrategyPanel', () => { it('seeds one row by default', () => { const wrapper = mountPanel() expect(wrapper.findAll('select')).toHaveLength(1) }) it('adds and removes rows', async () => { const wrapper = mountPanel() const addBtn = wrapper.findAll('button').find((b) => b.text().includes('Add strategy')) await addBtn.trigger('click') expect(wrapper.findAll('select')).toHaveLength(2) const removeBtn = wrapper.findAll('button').find((b) => b.text() === 'Remove') await removeBtn.trigger('click') expect(wrapper.findAll('select')).toHaveLength(1) }) it('emits start-many with normalised weights and params', async () => { const wrapper = mountPanel() const addBtn = wrapper.findAll('button').find((b) => b.text().includes('Add strategy')) await addBtn.trigger('click') const startBtn = wrapper.findAll('button').find((b) => b.text().includes('Start all')) await startBtn.trigger('click') const payload = wrapper.emitted('start-many')[0][0] expect(payload.strategies).toHaveLength(2) expect(payload.strategies[0].weight).toBe(1) expect(payload.strategies[0].params).toEqual({ fast: 10 }) expect(payload.starting_balance).toBe(10000) }) it('shows the per-strategy status table', () => { const wrapper = mountPanel({ running: true, total_pnl: 42, equity: 10042, strategies: [ { strategy_key: 'sma_cross', strategy_name: 'SMA Crossover', params: {}, symbol: 'BTC/USDT', timeframe: '5m', allocation: 0.4, cash: 4000, equity: 4200, realized_pnl: 200, unrealized_pnl: 0, total_pnl: 200, open_positions: 1, halted: false, halt_reason: null, trades_today: 2, error: null, }, ], }) const text = wrapper.text() expect(text).toContain('SMA Crossover') expect(text).toContain('40.0%') expect(text).toContain('running') }) it('loads a preset as an additional row', async () => { const wrapper = mountPanel(EMPTY_STATUS, PRESETS) const presetSelect = wrapper.findAll('select').find((s) => s.text().includes('BTC fast')) expect(presetSelect).toBeTruthy() await presetSelect.setValue('1') // A second row now exists, seeded from the preset. expect(wrapper.findAll('select').length).toBeGreaterThan(1) const inputs = wrapper.findAll('input') expect(inputs.some((i) => i.element.value === 'ETH/USDT')).toBe(true) expect(inputs.some((i) => i.element.value === '15m')).toBe(true) }) it('emits save-preset with the row config', async () => { vi.spyOn(window, 'prompt').mockReturnValue('my preset') const wrapper = mountPanel() const save = wrapper.findAll('button').find((b) => b.text() === 'Save') await save.trigger('click') const payload = wrapper.emitted('save-preset')[0][0] expect(payload.name).toBe('my preset') expect(payload.strategy_key).toBe('sma_cross') expect(payload.params).toEqual({ fast: 10 }) }) it('does not save when the prompt is cancelled', async () => { vi.spyOn(window, 'prompt').mockReturnValue(null) const wrapper = mountPanel() const save = wrapper.findAll('button').find((b) => b.text() === 'Save') await save.trigger('click') expect(wrapper.emitted('save-preset')).toBeUndefined() }) it('marks a halted strategy', () => { const wrapper = mountPanel({ running: true, total_pnl: -10, equity: 9990, strategies: [ { strategy_key: 'sma_cross', strategy_name: 'SMA Crossover', params: {}, symbol: 'BTC/USDT', timeframe: '5m', allocation: 1, cash: 0, equity: 9990, realized_pnl: -10, unrealized_pnl: 0, total_pnl: -10, open_positions: 0, halted: true, halt_reason: 'daily loss limit hit', trades_today: 1, error: null, }, ], }) expect(wrapper.text()).toContain('halted') }) })