137 lines
5.4 KiB
TypeScript
137 lines
5.4 KiB
TypeScript
import { describe, expect, it, vi } from 'vitest';
|
|
import {
|
|
filterHistoryByProfileAndDate,
|
|
paginateHistory,
|
|
parseDateEnd,
|
|
parseDateStart,
|
|
sortHistoryByTimestamp,
|
|
toTimestampMs
|
|
} from './HistoryTab';
|
|
import { buildClonedEntryPayload, filterEntriesByTab } from './EntriesTab';
|
|
import { buildConfigUpsertPayload, cloneConfigItems, hasConfigChanges } from './ConfigTab';
|
|
import { mapProfileToFormData, parseNumericSettings } from './SettingsTab';
|
|
|
|
vi.mock('../lib/supabaseClient', () => ({
|
|
supabase: {
|
|
from: vi.fn(() => ({
|
|
select: vi.fn(),
|
|
order: vi.fn(),
|
|
eq: vi.fn(),
|
|
insert: vi.fn(),
|
|
update: vi.fn(),
|
|
upsert: vi.fn(),
|
|
delete: vi.fn()
|
|
}))
|
|
}
|
|
}));
|
|
|
|
vi.mock('../components/AuthContext', () => ({
|
|
useAuth: () => ({
|
|
user: null,
|
|
profile: { role: 'admin' },
|
|
refreshProfile: vi.fn()
|
|
})
|
|
}));
|
|
|
|
describe('tab helper coverage', () => {
|
|
it('parses and normalizes history timestamps and date bounds', () => {
|
|
expect(toTimestampMs(1700000000000)).toBe(1700000000000);
|
|
expect(toTimestampMs('2026-02-15T10:00:00.000Z')).toBeGreaterThan(0);
|
|
expect(toTimestampMs(undefined, '2026-02-15T10:00:00.000Z')).toBeGreaterThan(0);
|
|
expect(toTimestampMs('invalid')).toBe(0);
|
|
|
|
expect(parseDateStart('')).toBeNull();
|
|
expect(parseDateEnd('')).toBeNull();
|
|
expect(parseDateStart('2026-02-15')).toBeGreaterThan(0);
|
|
expect(parseDateEnd('2026-02-15')).toBeGreaterThan(0);
|
|
});
|
|
|
|
it('filters, sorts and paginates history rows deterministically', () => {
|
|
const records = [
|
|
{ id: 'b', profile_id: 'p1', timestamp: 20, created_at: '', symbol: 'BTC', side: 'BUY', entry_price: 1, exit_price: 2, pnl: 1, pnl_percent: 1, reason: 'r' },
|
|
{ id: 'a', profile_id: 'p1', timestamp: 20, created_at: '', symbol: 'BTC', side: 'BUY', entry_price: 1, exit_price: 2, pnl: 1, pnl_percent: 1, reason: 'r' },
|
|
{ id: 'c', profile_id: 'p2', timestamp: 10, created_at: '', symbol: 'ETH', side: 'SELL', entry_price: 2, exit_price: 1, pnl: -1, pnl_percent: -1, reason: 'r' }
|
|
] as any;
|
|
|
|
const filtered = filterHistoryByProfileAndDate(records, 'p1', { from: 0, to: 25 });
|
|
expect(filtered).toHaveLength(2);
|
|
|
|
const desc = sortHistoryByTimestamp(filtered, 'desc');
|
|
const asc = sortHistoryByTimestamp(filtered, 'asc');
|
|
expect(desc[0].id).toBe('b');
|
|
expect(asc[0].id).toBe('a');
|
|
|
|
const paged = paginateHistory(desc, 1, 1);
|
|
expect(paged).toHaveLength(1);
|
|
expect(paged[0].id).toBe('b');
|
|
});
|
|
|
|
it('filters entries by tab and builds clone payload safely', () => {
|
|
const entries = [
|
|
{ stock_instance_id: '1', is_real_trade: false, active: true, status: 'active' },
|
|
{ stock_instance_id: '2', is_real_trade: false, active: false, status: 'sellCompleted' },
|
|
{ stock_instance_id: '3', is_real_trade: true, active: true, status: 'active' }
|
|
] as any;
|
|
|
|
expect(filterEntriesByTab(entries, 'paperActive')).toHaveLength(1);
|
|
expect(filterEntriesByTab(entries, 'paperCompleted')).toHaveLength(1);
|
|
expect(filterEntriesByTab(entries, 'realActive')).toHaveLength(1);
|
|
expect(filterEntriesByTab(entries, 'unknown')).toHaveLength(0);
|
|
|
|
const cloned = buildClonedEntryPayload({
|
|
stock_instance_id: 'old',
|
|
id: 'legacy',
|
|
created_at: 't1',
|
|
updated_at: 't2',
|
|
symbol: 'BTC/USDT',
|
|
active: true,
|
|
user_id: 'u1',
|
|
status: 'active',
|
|
is_crypto: true,
|
|
is_real_trade: false
|
|
} as any, 'new-id');
|
|
expect(cloned.stock_instance_id).toBe('new-id');
|
|
expect(cloned.active).toBe(false);
|
|
expect(cloned.status).toBe('active');
|
|
expect((cloned as any).id).toBeUndefined();
|
|
});
|
|
|
|
it('clones, diffs and maps config upsert payloads', () => {
|
|
const configs = [{ key: 'A', value: '1', description: 'd' }];
|
|
const cloned = cloneConfigItems(configs as any);
|
|
cloned[0].value = '2';
|
|
|
|
expect(configs[0].value).toBe('1');
|
|
expect(hasConfigChanges(configs as any, cloned as any)).toBe(true);
|
|
expect(hasConfigChanges(configs as any, configs as any)).toBe(false);
|
|
|
|
const payload = buildConfigUpsertPayload(configs as any, '2026-02-16T00:00:00.000Z');
|
|
expect(payload[0]).toEqual({ key: 'A', value: '1', updated_at: '2026-02-16T00:00:00.000Z' });
|
|
});
|
|
|
|
it('maps settings form data and validates numeric fields', () => {
|
|
const mapped = mapProfileToFormData({
|
|
first_name: 'Sam',
|
|
last_name: 'Trader',
|
|
trade_enable: true,
|
|
drop_threshold_for_buy: 0.05,
|
|
gain_threshold_for_sell: 0.02,
|
|
market_poll_interval_in_seconds: 30000
|
|
});
|
|
|
|
expect(mapped.first_name).toBe('Sam');
|
|
expect(mapped.trade_enable).toBe(true);
|
|
expect(mapped.drop_threshold_for_buy).toBe('0.05');
|
|
|
|
const parsed = parseNumericSettings(mapped);
|
|
expect(parsed.buyThreshold).toBe(0.05);
|
|
expect(parsed.sellThreshold).toBe(0.02);
|
|
expect(parsed.interval).toBe(30000);
|
|
|
|
expect(() => parseNumericSettings({
|
|
...mapped,
|
|
drop_threshold_for_buy: 'x'
|
|
})).toThrow('Please enter valid numbers for thresholds and interval.');
|
|
});
|
|
});
|