diff --git a/frontend/src/components/BacktestPanel.vue b/frontend/src/components/BacktestPanel.vue
index 4ab0a9d..9562ea7 100644
--- a/frontend/src/components/BacktestPanel.vue
+++ b/frontend/src/components/BacktestPanel.vue
@@ -9,6 +9,7 @@ import StrategyForm from './StrategyForm.vue'
defineProps({
strategies: { type: Array, required: true },
status: { type: Object, required: true },
+ loading: { type: Boolean, default: false },
})
const running = ref(false)
@@ -44,6 +45,7 @@ async function run(payload) {
:busy="running"
:symbol="status.symbol"
:timeframe="status.timeframe"
+ :loading="loading"
submit-label="Run backtest"
@submit="run"
/>
diff --git a/frontend/src/components/StrategyForm.vue b/frontend/src/components/StrategyForm.vue
index a3ef9dc..e6c63f8 100644
--- a/frontend/src/components/StrategyForm.vue
+++ b/frontend/src/components/StrategyForm.vue
@@ -10,6 +10,9 @@ const props = defineProps({
symbol: { type: String, default: 'BTC/USDT' },
timeframe: { type: String, default: '5m' },
presets: { type: Array, default: () => [] },
+ // True while the strategy list is still being fetched, so the dropdown can
+ // show a loading state instead of appearing empty.
+ loading: { type: Boolean, default: false },
})
const emit = defineEmits(['submit', 'save-preset', 'delete-preset', 'load-preset'])
@@ -99,7 +102,14 @@ function submit() {
diff --git a/frontend/src/components/StrategyPanel.vue b/frontend/src/components/StrategyPanel.vue
index 2b1c885..b93862f 100644
--- a/frontend/src/components/StrategyPanel.vue
+++ b/frontend/src/components/StrategyPanel.vue
@@ -8,6 +8,7 @@ const props = defineProps({
status: { type: Object, required: true },
busy: { type: Boolean, default: false },
presets: { type: Array, default: () => [] },
+ loading: { type: Boolean, default: false },
})
const emit = defineEmits(['start', 'stop', 'kill', 'save-preset', 'delete-preset'])
@@ -34,6 +35,7 @@ function confirmKill() {
:symbol="status.symbol"
:timeframe="status.timeframe"
:presets="presets"
+ :loading="loading"
@submit="emit('start', $event)"
@save-preset="emit('save-preset', $event)"
@delete-preset="emit('delete-preset', $event)"
diff --git a/frontend/src/components/SweepPanel.vue b/frontend/src/components/SweepPanel.vue
index fb8f295..e77692c 100644
--- a/frontend/src/components/SweepPanel.vue
+++ b/frontend/src/components/SweepPanel.vue
@@ -7,6 +7,7 @@ import CollapsiblePanel from './CollapsiblePanel.vue'
const props = defineProps({
strategies: { type: Array, required: true },
status: { type: Object, required: true },
+ loading: { type: Boolean, default: false },
})
const running = ref(false)
@@ -97,7 +98,10 @@ async function run() {
diff --git a/frontend/src/strategy-form.test.js b/frontend/src/strategy-form.test.js
new file mode 100644
index 0000000..359f301
--- /dev/null
+++ b/frontend/src/strategy-form.test.js
@@ -0,0 +1,42 @@
+import { mount } from '@vue/test-utils'
+import { describe, expect, it } from 'vitest'
+import StrategyForm from './components/StrategyForm.vue'
+
+function mountForm(props) {
+ return mount(StrategyForm, { props: { strategies: [], ...props } })
+}
+
+describe('StrategyForm strategy dropdown states', () => {
+ it('shows a loading option while the list is being fetched', () => {
+ const wrapper = mountForm({ loading: true })
+ const select = wrapper.find('select')
+ expect(select.text()).toContain('Loading strategies…')
+ expect(select.attributes('disabled')).toBeDefined()
+ expect(wrapper.find('option[value=""]').attributes('disabled')).toBeDefined()
+ })
+
+ it('shows a no-strategies option when loaded but empty', () => {
+ const wrapper = mountForm({ loading: false })
+ expect(wrapper.find('select').text()).toContain('No strategies available')
+ })
+
+ it('lists strategies and enables the select once loaded', () => {
+ const wrapper = mountForm({
+ loading: false,
+ strategies: [
+ {
+ key: 'sma_cross',
+ name: 'SMA Crossover',
+ description: '',
+ params: [{ name: 'fast', type: 'int', default: 10, description: '' }],
+ },
+ { key: 'ema_cross', name: 'EMA Crossover', description: '', params: [] },
+ ],
+ })
+ const select = wrapper.find('select')
+ expect(select.attributes('disabled')).toBeUndefined()
+ expect(select.findAll('option')).toHaveLength(2)
+ expect(select.text()).toContain('SMA Crossover')
+ expect(select.element.value).toBe('sma_cross')
+ })
+})