Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
154 changes: 153 additions & 1 deletion app/components/UI/Perps/hooks/usePerpsTPSLUpdate.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,9 +4,11 @@ import { ToastContext } from '../../../../component-library/components/Toast';
import { usePerpsTPSLUpdate } from './usePerpsTPSLUpdate';
import { usePerpsTrading } from './usePerpsTrading';
import usePerpsToasts from './usePerpsToasts';
import { usePerpsStream } from '../providers/PerpsStreamManager';

jest.mock('./usePerpsTrading');
jest.mock('./usePerpsToasts');
jest.mock('../providers/PerpsStreamManager');
jest.mock('../../../../../locales/i18n', () => ({
strings: jest.fn((key) => key),
}));
Expand Down Expand Up @@ -40,6 +42,7 @@ jest.mock('expo-haptics', () => ({
describe('usePerpsTPSLUpdate', () => {
const mockUpdatePositionTPSL = jest.fn();
const mockShowToast = jest.fn();
const mockUpdatePositionTPSLOptimistic = jest.fn();
const mockToastContext = {
toastRef: {
current: {
Expand All @@ -49,6 +52,12 @@ describe('usePerpsTPSLUpdate', () => {
},
};

const mockStream = {
positions: {
updatePositionTPSLOptimistic: mockUpdatePositionTPSLOptimistic,
},
};

const mockPerpsToastOptions = {
positionManagement: {
tpsl: {
Expand Down Expand Up @@ -126,6 +135,8 @@ describe('usePerpsTPSLUpdate', () => {
showToast: mockShowToast,
PerpsToastOptions: mockPerpsToastOptions,
});

(usePerpsStream as jest.Mock).mockReturnValue(mockStream);
});

const renderHookWithToast = (options = {}) => {
Expand Down Expand Up @@ -267,7 +278,7 @@ describe('usePerpsTPSLUpdate', () => {
);
});

it('should use toast configurations with correct haptics types', () => {
it('uses toast configurations with correct haptics types', () => {
// Verify success toast has correct haptics type
expect(
mockPerpsToastOptions.positionManagement.tpsl.updateTPSLSuccess
Expand All @@ -281,4 +292,145 @@ describe('usePerpsTPSLUpdate', () => {
);
expect(errorToast.hapticsType).toBe('error');
});

describe('optimistic updates', () => {
it('applies optimistic update to position cache on successful API response', async () => {
const { result } = renderHookWithToast();
const position = createMockPosition();
const takeProfitPrice = '3300';
const stopLossPrice = '2700';

mockUpdatePositionTPSL.mockResolvedValue({ success: true });

await act(async () => {
await result.current.handleUpdateTPSL(
position,
takeProfitPrice,
stopLossPrice,
);
});

expect(mockUpdatePositionTPSLOptimistic).toHaveBeenCalledWith(
'ETH',
takeProfitPrice,
stopLossPrice,
);
});

it('applies optimistic update with undefined take profit price', async () => {
const { result } = renderHookWithToast();
const position = createMockPosition();
const stopLossPrice = '2700';

mockUpdatePositionTPSL.mockResolvedValue({ success: true });

await act(async () => {
await result.current.handleUpdateTPSL(
position,
undefined,
stopLossPrice,
);
});

expect(mockUpdatePositionTPSLOptimistic).toHaveBeenCalledWith(
'ETH',
undefined,
stopLossPrice,
);
});

it('applies optimistic update with undefined stop loss price', async () => {
const { result } = renderHookWithToast();
const position = createMockPosition();
const takeProfitPrice = '3300';

mockUpdatePositionTPSL.mockResolvedValue({ success: true });

await act(async () => {
await result.current.handleUpdateTPSL(
position,
takeProfitPrice,
undefined,
);
});

expect(mockUpdatePositionTPSLOptimistic).toHaveBeenCalledWith(
'ETH',
takeProfitPrice,
undefined,
);
});

it('applies optimistic update with both TP/SL undefined (removing both)', async () => {
const { result } = renderHookWithToast();
const position = createMockPosition({
takeProfitPrice: '3500',
stopLossPrice: '2500',
});

mockUpdatePositionTPSL.mockResolvedValue({ success: true });

await act(async () => {
await result.current.handleUpdateTPSL(position, undefined, undefined);
});

expect(mockUpdatePositionTPSLOptimistic).toHaveBeenCalledWith(
'ETH',
undefined,
undefined,
);
});

it('does not apply optimistic update on API failure response', async () => {
const { result } = renderHookWithToast();
const position = createMockPosition();

mockUpdatePositionTPSL.mockResolvedValue({
success: false,
error: 'Network error',
});

await act(async () => {
await result.current.handleUpdateTPSL(position, '3300', '2700');
});

expect(mockUpdatePositionTPSLOptimistic).not.toHaveBeenCalled();
});

it('does not apply optimistic update on exception', async () => {
const { result } = renderHookWithToast();
const position = createMockPosition();
const error = new Error('Network error');

mockUpdatePositionTPSL.mockRejectedValue(error);

await act(async () => {
await result.current.handleUpdateTPSL(position, '3300', '2700');
});

expect(mockUpdatePositionTPSLOptimistic).not.toHaveBeenCalled();
});

it('applies optimistic update before showing success toast', async () => {
const callOrder: string[] = [];

mockUpdatePositionTPSLOptimistic.mockImplementation(() => {
callOrder.push('optimistic');
});
mockShowToast.mockImplementation(() => {
callOrder.push('toast');
});

const { result } = renderHookWithToast();
const position = createMockPosition();

mockUpdatePositionTPSL.mockResolvedValue({ success: true });

await act(async () => {
await result.current.handleUpdateTPSL(position, '3300', '2700');
});

expect(callOrder).toEqual(['optimistic', 'toast']);
});
});
});
11 changes: 11 additions & 0 deletions app/components/UI/Perps/hooks/usePerpsTPSLUpdate.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ import { usePerpsTrading } from './usePerpsTrading';
import type { Position, TPSLTrackingData } from '../controllers/types';
import { captureException } from '@sentry/react-native';
import usePerpsToasts from './usePerpsToasts';
import { usePerpsStream } from '../providers/PerpsStreamManager';

interface UseTPSLUpdateOptions {
onSuccess?: () => void;
Expand All @@ -19,6 +20,7 @@ interface UseTPSLUpdateOptions {
export function usePerpsTPSLUpdate(options?: UseTPSLUpdateOptions) {
const { updatePositionTPSL } = usePerpsTrading();
const [isUpdating, setIsUpdating] = useState(false);
const stream = usePerpsStream();

const { showToast, PerpsToastOptions } = usePerpsToasts();

Expand All @@ -43,6 +45,14 @@ export function usePerpsTPSLUpdate(options?: UseTPSLUpdateOptions) {
if (result.success) {
DevLogger.log('Position TP/SL updated successfully:', result);

// Apply optimistic update immediately for better UX
// This updates the UI before the WebSocket confirms the change
stream.positions.updatePositionTPSLOptimistic(
position.coin,
takeProfitPrice,
stopLossPrice,
);

showToast(
PerpsToastOptions.positionManagement.tpsl.updateTPSLSuccess,
);
Expand Down Expand Up @@ -112,6 +122,7 @@ export function usePerpsTPSLUpdate(options?: UseTPSLUpdateOptions) {
showToast,
PerpsToastOptions.positionManagement.tpsl,
options,
stream,
],
);

Expand Down
Loading
Loading