diff --git a/libs/deal-ticket/src/components/deal-ticket/deal-ticket-stop-order.spec.tsx b/libs/deal-ticket/src/components/deal-ticket/deal-ticket-stop-order.spec.tsx index a898acd0b..bc1f625ab 100644 --- a/libs/deal-ticket/src/components/deal-ticket/deal-ticket-stop-order.spec.tsx +++ b/libs/deal-ticket/src/components/deal-ticket/deal-ticket-stop-order.spec.tsx @@ -28,6 +28,37 @@ function generateJsx(pubKey: string | null = 'pubKey', isReadOnly = false) { ); } +const submitButton = 'place-order'; +const sizeInput = 'order-size'; +const priceInput = 'order-price'; +const triggerPriceInput = 'triggerPrice'; +const triggerTrailingPercentOffsetInput = 'triggerTrailingPercentOffset'; + +const orderTypeLimit = 'order-type-StopLimit'; +const orderTypeMarket = 'order-type-StopMarket'; + +const orderSideBuy = 'order-side-SIDE_BUY'; +const orderSideSell = 'order-side-SIDE_SELL'; + +const triggerDirectionRisesAbove = 'triggerDirection-risesAbove'; +// const triggerDirectionFallsBelow = 'triggerDirection-fallsBelow'; + +const expiryStrategySubmit = 'expiryStrategy-submit'; +const expiryStrategyCancel = 'expiryStrategy-cancel'; + +const triggerTypePrice = 'triggerType-price'; +const triggerTypeTrailingPercentOffset = 'triggerType-trailingPercentOffset'; + +const expire = 'expire'; +const datePicker = 'date-picker-field'; +const timeInForce = 'order-tif'; + +const sizeErrorMessage = 'stop-order-error-message-size'; +const priceErrorMessage = 'stop-order-error-message-price'; +const triggerPriceErrorMessage = 'stop-order-error-message-trigger-price'; +const triggerTrailingPercentOffsetErrorMessage = + 'stop-order-error-message-trigger-trailing-percent-offset'; + describe('StopOrder', () => { beforeEach(() => { localStorage.clear(); @@ -41,35 +72,33 @@ describe('StopOrder', () => { it('should display ticket defaults', async () => { render(generateJsx()); // place order button should always be enabled - expect(screen.getByTestId('place-order')).toBeEnabled(); + expect(screen.getByTestId(submitButton)).toBeEnabled(); // Assert defaults are used - expect(screen.getByTestId('order-type-StopLimit').dataset.state).toEqual( - 'checked' - ); + expect(screen.getByTestId(orderTypeLimit).dataset.state).toEqual('checked'); expect( - screen.queryByTestId('order-side-SIDE_BUY')?.querySelector('input') + screen.queryByTestId(orderSideBuy)?.querySelector('input') ).toBeChecked(); expect( - screen.queryByTestId('order-side-SIDE_SELL')?.querySelector('input') + screen.queryByTestId(orderSideSell)?.querySelector('input') ).not.toBeChecked(); - expect(screen.getByTestId('order-size')).toHaveDisplayValue('0'); - expect(screen.getByTestId('order-tif')).toHaveValue( + expect(screen.getByTestId(sizeInput)).toHaveDisplayValue('0'); + expect(screen.getByTestId(timeInForce)).toHaveValue( Schema.OrderTimeInForce.TIME_IN_FORCE_FOK ); expect( - screen.getByTestId('triggerDirection-risesAbove').dataset.state + screen.getByTestId(triggerDirectionRisesAbove).dataset.state ).toEqual('checked'); - expect(screen.getByTestId('triggerType-price').dataset.state).toEqual( + expect(screen.getByTestId(triggerTypePrice).dataset.state).toEqual( 'checked' ); - expect(screen.getByTestId('expire').dataset.state).toEqual('unchecked'); + expect(screen.getByTestId(expire).dataset.state).toEqual('unchecked'); act(() => { - screen.getByTestId('expire').click(); + screen.getByTestId(expire).click(); }); await waitFor(() => { - expect(screen.getByTestId('expiryStrategy-submit').dataset.state).toEqual( + expect(screen.getByTestId(expiryStrategySubmit).dataset.state).toEqual( 'checked' ); }); @@ -77,8 +106,8 @@ describe('StopOrder', () => { it('should display trigger price as price for market type order', async () => { render(generateJsx()); - screen.getByTestId('order-type-StopMarket').click(); - await userEvent.type(screen.getByTestId('triggerPrice'), '10'); + screen.getByTestId(orderTypeMarket).click(); + await userEvent.type(screen.getByTestId(triggerPriceInput), '10'); expect(screen.getByTestId('price')).toHaveTextContent('10.0'); }); @@ -102,38 +131,178 @@ describe('StopOrder', () => { render(generateJsx()); // Assert correct defaults are used from store - expect(screen.queryByTestId('order-type-StopLimit')).toBeChecked(); + expect(screen.queryByTestId(orderTypeLimit)).toBeChecked(); expect( - screen.queryByTestId('order-side-SIDE_SELL')?.querySelector('input') + screen.queryByTestId(orderSideSell)?.querySelector('input') ).toBeChecked(); expect( - screen.queryByTestId('order-side-SIDE_BUY')?.querySelector('input') + screen.queryByTestId(orderSideBuy)?.querySelector('input') ).not.toBeChecked(); - expect(screen.getByTestId('order-size')).toHaveDisplayValue( + expect(screen.getByTestId(sizeInput)).toHaveDisplayValue( values.size as string ); expect(screen.getByTestId('order-tif')).toHaveValue(values.timeInForce); - expect(screen.getByTestId('order-price')).toHaveDisplayValue( + expect(screen.getByTestId(priceInput)).toHaveDisplayValue( values.price as string ); expect(screen.getByTestId('expire').dataset.state).toEqual('checked'); - expect(screen.getByTestId('expiryStrategy-cancel').dataset.state).toEqual( + expect(screen.getByTestId(expiryStrategyCancel).dataset.state).toEqual( 'checked' ); - expect(screen.getByTestId('date-picker-field')).toHaveDisplayValue( + expect(screen.getByTestId(datePicker)).toHaveDisplayValue( values.expiresAt as string ); }); - it('shows no wallet warning and do not submit', async () => { + it('shows no wallet warning and do not submit if no wallet connected', async () => { render(generateJsx(null)); - await userEvent.type(screen.getByTestId('order-size'), '1'); - await userEvent.type(screen.getByTestId('order-price'), '1'); - await userEvent.type(screen.getByTestId('triggerPrice'), '1'); - await userEvent.click(screen.getByTestId('place-order')); + await userEvent.type(screen.getByTestId(sizeInput), '1'); + await userEvent.type(screen.getByTestId(priceInput), '1'); + await userEvent.type(screen.getByTestId(triggerPriceInput), '1'); + await userEvent.click(screen.getByTestId(submitButton)); expect(submit).not.toBeCalled(); expect( screen.getByTestId('deal-ticket-connect-wallet') ).toBeInTheDocument(); }); + + it('calls submit if form is valid', async () => { + render(generateJsx()); + await userEvent.type(screen.getByTestId(sizeInput), '1'); + await userEvent.type(screen.getByTestId(priceInput), '1'); + await userEvent.type(screen.getByTestId(triggerPriceInput), '1'); + await userEvent.click(screen.getByTestId(submitButton)); + expect(submit).toBeCalled(); + }); + + it('validates size field', async () => { + render(generateJsx()); + + await userEvent.click(screen.getByTestId(submitButton)); + + // default value should be invalid + expect(screen.getByTestId(sizeErrorMessage)).toBeInTheDocument(); + // to small value should be invalid + await userEvent.type(screen.getByTestId(sizeInput), '0.01'); + expect(screen.getByTestId(sizeErrorMessage)).toBeInTheDocument(); + + // clear and fill using valid value + await userEvent.clear(screen.getByTestId(sizeInput)); + await userEvent.type(screen.getByTestId(sizeInput), '0.1'); + expect(screen.queryByTestId(sizeErrorMessage)).toBeNull(); + }); + + it('validates price field', async () => { + render(generateJsx()); + + await userEvent.click(screen.getByTestId(submitButton)); + // price error message should not show if size has error + expect(screen.queryByTestId(priceErrorMessage)).toBeNull(); + await userEvent.type(screen.getByTestId(sizeInput), '0.1'); + expect(screen.getByTestId(priceErrorMessage)).toBeInTheDocument(); + await userEvent.type(screen.getByTestId(priceInput), '0.001'); + expect(screen.getByTestId(priceErrorMessage)).toBeInTheDocument(); + + // switch to market order type error should disappear + await userEvent.click(screen.getByTestId(orderTypeMarket)); + expect(screen.queryByTestId(priceErrorMessage)).toBeNull(); + + // switch back to limit type + await userEvent.click(screen.getByTestId(orderTypeLimit)); + expect(screen.getByTestId(priceErrorMessage)).toBeInTheDocument(); + + // to small value should be invalid + await userEvent.type(screen.getByTestId(priceInput), '0.001'); + expect(screen.getByTestId(priceErrorMessage)).toBeInTheDocument(); + + // clear and fill using valid value + await userEvent.clear(screen.getByTestId(priceInput)); + await userEvent.type(screen.getByTestId(priceInput), '0.01'); + expect(screen.queryByTestId(priceErrorMessage)).toBeNull(); + }); + + it('validates trigger price field', async () => { + render(generateJsx()); + + await userEvent.click(screen.getByTestId(submitButton)); + expect(screen.getByTestId(triggerPriceErrorMessage)).toBeInTheDocument(); + + // switch to trailing percentage offset trigger type + await userEvent.click(screen.getByTestId(triggerTypeTrailingPercentOffset)); + expect(screen.queryByTestId(triggerPriceErrorMessage)).toBeNull(); + + // switch back to price trigger type + await userEvent.click(screen.getByTestId(triggerTypePrice)); + expect(screen.getByTestId(triggerPriceErrorMessage)).toBeInTheDocument(); + + // to small value should be invalid + await userEvent.type(screen.getByTestId(triggerPriceInput), '0.001'); + expect(screen.getByTestId(triggerPriceErrorMessage)).toBeInTheDocument(); + + // clear and fill using valid value + await userEvent.clear(screen.getByTestId(triggerPriceInput)); + await userEvent.type(screen.getByTestId(triggerPriceInput), '0.01'); + expect(screen.queryByTestId(triggerPriceErrorMessage)).toBeNull(); + }); + + it('validates trigger trailing percentage offset field', async () => { + render(generateJsx()); + + // should not show error with default form values + await userEvent.click(screen.getByTestId(submitButton)); + expect( + screen.queryByTestId(triggerTrailingPercentOffsetErrorMessage) + ).toBeNull(); + + // switch to trailing percentage offset trigger type + await userEvent.click(screen.getByTestId(triggerTypeTrailingPercentOffset)); + expect( + screen.getByTestId(triggerTrailingPercentOffsetErrorMessage) + ).toBeInTheDocument(); + + // to small value should be invalid + await userEvent.type( + screen.getByTestId(triggerTrailingPercentOffsetInput), + '0.09' + ); + expect( + screen.getByTestId(triggerTrailingPercentOffsetErrorMessage) + ).toBeInTheDocument(); + + // clear and fill using valid value + await userEvent.clear( + screen.getByTestId(triggerTrailingPercentOffsetInput) + ); + await userEvent.type( + screen.getByTestId(triggerTrailingPercentOffsetInput), + '0.1' + ); + expect( + screen.queryByTestId(triggerTrailingPercentOffsetErrorMessage) + ).toBeNull(); + + // to big value should be invalid + await userEvent.clear( + screen.getByTestId(triggerTrailingPercentOffsetInput) + ); + await userEvent.type( + screen.getByTestId(triggerTrailingPercentOffsetInput), + '99.91' + ); + expect( + screen.getByTestId(triggerTrailingPercentOffsetErrorMessage) + ).toBeInTheDocument(); + + // clear and fill using valid value + await userEvent.clear( + screen.getByTestId(triggerTrailingPercentOffsetInput) + ); + await userEvent.type( + screen.getByTestId(triggerTrailingPercentOffsetInput), + '99.9' + ); + expect( + screen.queryByTestId(triggerTrailingPercentOffsetErrorMessage) + ).toBeNull(); + }); }); diff --git a/libs/deal-ticket/src/components/deal-ticket/deal-ticket-stop-order.tsx b/libs/deal-ticket/src/components/deal-ticket/deal-ticket-stop-order.tsx index 5735716a9..99d0369d1 100644 --- a/libs/deal-ticket/src/components/deal-ticket/deal-ticket-stop-order.tsx +++ b/libs/deal-ticket/src/components/deal-ticket/deal-ticket-stop-order.tsx @@ -196,9 +196,6 @@ export const StopOrder = ({ market, marketPrice, submit }: StopOrderProps) => { }); return; } - if (type === DealTicketType.StopMarket) { - clearErrors('price'); - } setValue( 'type', type === DealTicketType.StopLimit @@ -277,6 +274,7 @@ export const StopOrder = ({ market, marketPrice, submit }: StopOrderProps) => { { ), }} render={({ field }) => { + const { value, ...props } = field; return (