From 1b44e456c6dda55fd388725380ffefd37ecc2c7e Mon Sep 17 00:00:00 2001 From: matitos Date: Mon, 21 Sep 2026 16:40:48 +0000 Subject: [PATCH] Collapse the strategy, backtest and sweep panels Adds a reusable CollapsiblePanel (chevron header, optional summary shown when collapsed, state persisted in localStorage) and wraps the Strategy, Backtest and Parameter sweep panels with it, so any single part of the dashboard can be focused on quickly. Panels stay mounted (v-show) so their form/results state is preserved across collapse. Includes component tests. --- frontend/src/collapsible.test.js | 64 ++++++++++++++++++ frontend/src/components/BacktestPanel.vue | 24 +++---- frontend/src/components/CollapsiblePanel.vue | 67 +++++++++++++++++++ frontend/src/components/StrategyPanel.vue | 69 ++++++++++---------- frontend/src/components/SweepPanel.vue | 21 +++--- 5 files changed, 190 insertions(+), 55 deletions(-) create mode 100644 frontend/src/collapsible.test.js create mode 100644 frontend/src/components/CollapsiblePanel.vue diff --git a/frontend/src/collapsible.test.js b/frontend/src/collapsible.test.js new file mode 100644 index 0000000..c3dac0c --- /dev/null +++ b/frontend/src/collapsible.test.js @@ -0,0 +1,64 @@ +import { mount } from '@vue/test-utils' +import { beforeEach, describe, expect, it } from 'vitest' +import CollapsiblePanel from './components/CollapsiblePanel.vue' + +describe('CollapsiblePanel', () => { + beforeEach(() => { + localStorage.clear() + }) + + it('renders the title and slot when open by default', () => { + const wrapper = mount(CollapsiblePanel, { + props: { title: 'Strategy' }, + slots: { default: '

content

' }, + }) + expect(wrapper.text()).toContain('Strategy') + expect(wrapper.text()).toContain('content') + expect(wrapper.find('button').attributes('aria-expanded')).toBe('true') + }) + + it('hides the slot after clicking the header', async () => { + const wrapper = mount(CollapsiblePanel, { + props: { title: 'Strategy' }, + slots: { default: '

content

' }, + }) + await wrapper.find('button').trigger('click') + expect(wrapper.find('button').attributes('aria-expanded')).toBe('false') + // v-show keeps the node but hides it. + expect(wrapper.find('div[style*="display: none"]').exists()).toBe(true) + }) + + it('shows the summary only when collapsed', async () => { + const wrapper = mount(CollapsiblePanel, { + props: { title: 'Backtest', summary: 'BTC/USDT 5m' }, + slots: { default: '

content

' }, + }) + expect(wrapper.text()).not.toContain('BTC/USDT 5m') + await wrapper.find('button').trigger('click') + expect(wrapper.text()).toContain('BTC/USDT 5m') + }) + + it('persists the collapsed state in localStorage', async () => { + const wrapper = mount(CollapsiblePanel, { + props: { title: 'Backtest', storageKey: 'backtest' }, + slots: { default: '

content

' }, + }) + await wrapper.find('button').trigger('click') + expect(localStorage.getItem('panel:backtest')).toBe('0') + + // A fresh mount with the same key starts collapsed. + const remounted = mount(CollapsiblePanel, { + props: { title: 'Backtest', storageKey: 'backtest' }, + slots: { default: '

content

' }, + }) + expect(remounted.find('button').attributes('aria-expanded')).toBe('false') + }) + + it('honours defaultOpen when nothing is stored', () => { + const wrapper = mount(CollapsiblePanel, { + props: { title: 'Strategy', defaultOpen: false }, + slots: { default: '

content

' }, + }) + expect(wrapper.find('button').attributes('aria-expanded')).toBe('false') + }) +}) diff --git a/frontend/src/components/BacktestPanel.vue b/frontend/src/components/BacktestPanel.vue index 4e64e96..6a2f365 100644 --- a/frontend/src/components/BacktestPanel.vue +++ b/frontend/src/components/BacktestPanel.vue @@ -1,7 +1,8 @@ - + + diff --git a/frontend/src/components/CollapsiblePanel.vue b/frontend/src/components/CollapsiblePanel.vue new file mode 100644 index 0000000..2c107e2 --- /dev/null +++ b/frontend/src/components/CollapsiblePanel.vue @@ -0,0 +1,67 @@ + + + diff --git a/frontend/src/components/StrategyPanel.vue b/frontend/src/components/StrategyPanel.vue index 137a85b..2b1c885 100644 --- a/frontend/src/components/StrategyPanel.vue +++ b/frontend/src/components/StrategyPanel.vue @@ -1,4 +1,6 @@ diff --git a/frontend/src/components/SweepPanel.vue b/frontend/src/components/SweepPanel.vue index bae0453..51b5149 100644 --- a/frontend/src/components/SweepPanel.vue +++ b/frontend/src/components/SweepPanel.vue @@ -2,6 +2,7 @@ import { computed, ref, watch } from 'vue' import * as api from '../api' import { fmtMoney, pnlClass, signed } from '../format' +import CollapsiblePanel from './CollapsiblePanel.vue' const props = defineProps({ strategies: { type: Array, required: true }, @@ -21,6 +22,13 @@ const gridText = ref({}) const selected = computed(() => props.strategies.find((s) => s.key === selectedKey.value)) +const summary = computed(() => { + if (!result.value) return '' + const top = result.value.rows[0] + const best = top ? ` · best ${signed(top.metrics.total_return_pct)}%` : '' + return `${result.value.combinations} combinations${best}` +}) + watch( () => props.strategies, (list) => { @@ -84,14 +92,8 @@ async function run() {