fix(D6): clean up strategy editor timers
This commit is contained in:
parent
d36c2bae5e
commit
2089b9aa16
@ -32,6 +32,7 @@ describe('CodeStrategyEditor save behavior', () => {
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
vi.useRealTimers();
|
||||
setItemSpy.mockRestore();
|
||||
});
|
||||
|
||||
@ -71,4 +72,18 @@ describe('CodeStrategyEditor save behavior', () => {
|
||||
|
||||
expect(await screen.findByText('Profile save failed')).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('clears the saved status timeout when unmounted', async () => {
|
||||
const clearTimeoutSpy = vi.spyOn(globalThis, 'clearTimeout');
|
||||
const user = userEvent.setup();
|
||||
|
||||
const { unmount } = render(<CodeStrategyEditor symbol="NVDA" />);
|
||||
await user.click(screen.getByRole('button', { name: /^save$/i }));
|
||||
|
||||
await waitFor(() => expect(screen.getByRole('button', { name: /saved!/i })).toBeInTheDocument());
|
||||
unmount();
|
||||
|
||||
expect(clearTimeoutSpy).toHaveBeenCalled();
|
||||
clearTimeoutSpy.mockRestore();
|
||||
});
|
||||
});
|
||||
|
||||
@ -2,7 +2,7 @@
|
||||
* Monaco-based code strategy editor.
|
||||
* Users write a JS strategy function; "Run Backtest" posts it to /api/backtest.
|
||||
*/
|
||||
import { useState } from 'react';
|
||||
import { useCallback, useEffect, useRef, useState } from 'react';
|
||||
import Editor from '@monaco-editor/react';
|
||||
import { Play, Save, Copy, RotateCcw } from 'lucide-react';
|
||||
import { getPlatformAccessToken } from '../../lib/authSession';
|
||||
@ -66,6 +66,23 @@ export function CodeStrategyEditor({
|
||||
const [result, setResult] = useState<BacktestResult | null>(null);
|
||||
const [error, setError] = useState<string | null>(null);
|
||||
const [saved, setSaved] = useState(false);
|
||||
const mountedRef = useRef(true);
|
||||
const savedResetTimeoutRef = useRef<ReturnType<typeof setTimeout> | null>(null);
|
||||
|
||||
const clearSavedResetTimeout = useCallback(() => {
|
||||
if (savedResetTimeoutRef.current) {
|
||||
clearTimeout(savedResetTimeoutRef.current);
|
||||
savedResetTimeoutRef.current = null;
|
||||
}
|
||||
}, []);
|
||||
|
||||
useEffect(() => {
|
||||
mountedRef.current = true;
|
||||
return () => {
|
||||
mountedRef.current = false;
|
||||
clearSavedResetTimeout();
|
||||
};
|
||||
}, [clearSavedResetTimeout]);
|
||||
|
||||
const handleRunBacktest = async () => {
|
||||
setRunning(true);
|
||||
@ -97,11 +114,17 @@ export function CodeStrategyEditor({
|
||||
throw new Error(data?.error ?? `Backtest failed (${res.status})`);
|
||||
}
|
||||
// Backend may wrap results in { success, results } or return them flat.
|
||||
setResult(data?.results ?? data);
|
||||
if (mountedRef.current) {
|
||||
setResult(data?.results ?? data);
|
||||
}
|
||||
} catch (err: any) {
|
||||
setError(err?.message ?? 'Backtest failed');
|
||||
if (mountedRef.current) {
|
||||
setError(err?.message ?? 'Backtest failed');
|
||||
}
|
||||
} finally {
|
||||
setRunning(false);
|
||||
if (mountedRef.current) {
|
||||
setRunning(false);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
@ -122,12 +145,24 @@ export function CodeStrategyEditor({
|
||||
code,
|
||||
},
|
||||
});
|
||||
if (!mountedRef.current) return;
|
||||
setSaved(true);
|
||||
clearSavedResetTimeout();
|
||||
savedResetTimeoutRef.current = setTimeout(() => {
|
||||
if (mountedRef.current) {
|
||||
setSaved(false);
|
||||
}
|
||||
savedResetTimeoutRef.current = null;
|
||||
}, 3000);
|
||||
} catch (err: any) {
|
||||
setSaved(false);
|
||||
setError(err?.message ?? 'Failed to save strategy');
|
||||
if (mountedRef.current) {
|
||||
setSaved(false);
|
||||
setError(err?.message ?? 'Failed to save strategy');
|
||||
}
|
||||
} finally {
|
||||
setSaving(false);
|
||||
if (mountedRef.current) {
|
||||
setSaving(false);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
|
||||
26
web/src/components/strategy/VisualRuleBuilder.dom.test.tsx
Normal file
26
web/src/components/strategy/VisualRuleBuilder.dom.test.tsx
Normal file
@ -0,0 +1,26 @@
|
||||
// @vitest-environment jsdom
|
||||
import { render, screen, waitFor } from '@testing-library/react';
|
||||
import userEvent from '@testing-library/user-event';
|
||||
import { afterEach, describe, expect, it, vi } from 'vitest';
|
||||
import { VisualRuleBuilder } from './VisualRuleBuilder';
|
||||
|
||||
describe('VisualRuleBuilder save behavior', () => {
|
||||
afterEach(() => {
|
||||
vi.useRealTimers();
|
||||
});
|
||||
|
||||
it('clears the saved message timeout when unmounted', async () => {
|
||||
const clearTimeoutSpy = vi.spyOn(globalThis, 'clearTimeout');
|
||||
const user = userEvent.setup();
|
||||
const onSave = vi.fn().mockResolvedValue(undefined);
|
||||
|
||||
const { unmount } = render(<VisualRuleBuilder symbol="AAPL" onSave={onSave} />);
|
||||
await user.click(screen.getByRole('button', { name: /save strategy/i }));
|
||||
|
||||
await waitFor(() => expect(screen.getByText('Strategy saved!')).toBeInTheDocument());
|
||||
unmount();
|
||||
|
||||
expect(clearTimeoutSpy).toHaveBeenCalled();
|
||||
clearTimeoutSpy.mockRestore();
|
||||
});
|
||||
});
|
||||
@ -2,7 +2,7 @@
|
||||
* Visual drag-and-drop strategy rule builder using @dnd-kit.
|
||||
* Lets users compose IF/THEN trading rules without writing code.
|
||||
*/
|
||||
import { useState, useCallback } from 'react';
|
||||
import { useState, useCallback, useEffect, useRef } from 'react';
|
||||
import {
|
||||
DndContext,
|
||||
closestCenter,
|
||||
@ -209,6 +209,23 @@ export function VisualRuleBuilder({ symbol, onSave, onBacktest }: Props) {
|
||||
const [name, setName] = useState('My Strategy');
|
||||
const [saving, setSaving] = useState(false);
|
||||
const [savedMsg, setSavedMsg] = useState('');
|
||||
const mountedRef = useRef(true);
|
||||
const savedMessageTimeoutRef = useRef<ReturnType<typeof setTimeout> | null>(null);
|
||||
|
||||
const clearSavedMessageTimeout = useCallback(() => {
|
||||
if (savedMessageTimeoutRef.current) {
|
||||
clearTimeout(savedMessageTimeoutRef.current);
|
||||
savedMessageTimeoutRef.current = null;
|
||||
}
|
||||
}, []);
|
||||
|
||||
useEffect(() => {
|
||||
mountedRef.current = true;
|
||||
return () => {
|
||||
mountedRef.current = false;
|
||||
clearSavedMessageTimeout();
|
||||
};
|
||||
}, [clearSavedMessageTimeout]);
|
||||
|
||||
const sensors = useSensors(useSensor(PointerSensor, {
|
||||
activationConstraint: { distance: 5 },
|
||||
@ -238,12 +255,23 @@ export function VisualRuleBuilder({ symbol, onSave, onBacktest }: Props) {
|
||||
setSaving(true);
|
||||
try {
|
||||
await onSave(name.trim(), rules);
|
||||
if (!mountedRef.current) return;
|
||||
setSavedMsg('Strategy saved!');
|
||||
setTimeout(() => setSavedMsg(''), 3000);
|
||||
clearSavedMessageTimeout();
|
||||
savedMessageTimeoutRef.current = setTimeout(() => {
|
||||
if (mountedRef.current) {
|
||||
setSavedMsg('');
|
||||
}
|
||||
savedMessageTimeoutRef.current = null;
|
||||
}, 3000);
|
||||
} catch {
|
||||
setSavedMsg('Save failed — try again');
|
||||
if (mountedRef.current) {
|
||||
setSavedMsg('Save failed — try again');
|
||||
}
|
||||
} finally {
|
||||
setSaving(false);
|
||||
if (mountedRef.current) {
|
||||
setSaving(false);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
|
||||
Loading…
Reference in New Issue
Block a user