From 46a85b8e65fb7f186126151baba0460f5a854eee Mon Sep 17 00:00:00 2001 From: macqbat Date: Wed, 7 Dec 2022 08:45:30 +0100 Subject: [PATCH] fix: insufficient balance error doesn't reset validation after balance has been fulfilled (#2336) * fix: live validation in deal ticket - reset validation after market state or account balance changes * fix: live validation in deal ticket - reset fix lint error * fix: live validation in deal ticket - adjust failing int test --- .../src/integration/trading-deal-ticket.cy.ts | 129 +++++++++++------- .../deal-ticket/deal-ticket.spec.tsx | 68 ++++++++- .../components/deal-ticket/deal-ticket.tsx | 57 ++++++-- libs/deal-ticket/src/constants.ts | 4 +- 4 files changed, 194 insertions(+), 64 deletions(-) diff --git a/apps/trading-e2e/src/integration/trading-deal-ticket.cy.ts b/apps/trading-e2e/src/integration/trading-deal-ticket.cy.ts index 7f6ff28a5..c6a780f13 100644 --- a/apps/trading-e2e/src/integration/trading-deal-ticket.cy.ts +++ b/apps/trading-e2e/src/integration/trading-deal-ticket.cy.ts @@ -7,6 +7,7 @@ import { generateEstimateOrder } from '../support/mocks/generate-fees'; import { aliasQuery, mockConnectWallet } from '@vegaprotocol/cypress'; import { testOrder } from '../support/deal-ticket-transaction'; import type { OrderSubmission } from '@vegaprotocol/wallet'; +import { generateAccounts } from '../support/mocks/generate-accounts'; const orderSizeField = 'order-size'; const orderPriceField = 'order-price'; @@ -622,55 +623,91 @@ describe('suspended market validation', { tags: '@regression' }, () => { }); describe('account validation', { tags: '@regression' }, () => { - beforeEach(() => { - cy.mockTradingPage(); - cy.mockGQL((req) => { - aliasQuery( - req, - 'EstimateOrder', - generateEstimateOrder({ - estimateOrder: { - marginLevels: { - __typename: 'MarginLevels', - initialLevel: '1000000000', + describe('zero balance error', () => { + beforeEach(() => { + cy.mockTradingPage(); + cy.mockGQL((req) => { + aliasQuery( + req, + 'Accounts', + generateAccounts({ + party: { + accountsConnection: { + edges: [ + { + node: { + type: Schema.AccountType.ACCOUNT_TYPE_GENERAL, + balance: '0', + market: null, + asset: { + __typename: 'Asset', + id: '5cfa87844724df6069b94e4c8a6f03af21907d7bc251593d08e4251043ee9f7c', + }, + }, + }, + ], + }, }, - }, - }) - ); + }) + ); + }); + cy.mockGQLSubscription(); + cy.visit('/#/markets/market-0'); + cy.connectVegaWallet(); + cy.wait('@Market'); + }); + + it('should show an error if your balance is zero', () => { + cy.getByTestId('place-order').should('not.be.disabled'); + cy.getByTestId('place-order').click(); + cy.getByTestId('place-order').should('be.disabled'); + //7002-SORD-003 + cy.getByTestId('dealticket-error-message-zero-balance').should( + 'have.text', + 'Insufficient balance. Deposit ' + 'tBTC' + ); + cy.getByTestId('deal-ticket-deposit-dialog-button').should('exist'); }); - cy.mockGQLSubscription(); - cy.visit('/#/markets/market-0'); - cy.connectVegaWallet(); - cy.wait('@Market'); }); - it('should show an error if your balance is zero', () => { - cy.getByTestId('place-order').should('not.be.disabled'); - cy.getByTestId('place-order').click(); - cy.getByTestId('place-order').should('be.disabled'); - //7002-SORD-003 - cy.getByTestId('dealticket-error-message-zero-balance').should( - 'have.text', - 'Insufficient balance. Deposit ' + 'tBTC' - ); - cy.getByTestId('deal-ticket-deposit-dialog-button').should('exist'); - }); - - it('should display info and button for deposit', () => { - //7002-SORD-003 - // warning should show immediately - cy.getByTestId('dealticket-warning-margin').should( - 'contain.text', - 'You may not have enough margin available to open this position' - ); - cy.getByTestId('dealticket-warning-margin').should( - 'contain.text', - '10,000.00 tBTC currently required, 1,000.00 tBTC available' - ); - cy.getByTestId('deal-ticket-deposit-dialog-button').click(); - cy.getByTestId('dialog-content') - .find('h1') - .eq(0) - .should('have.text', 'Deposit'); + describe('not enough balance warning', () => { + beforeEach(() => { + cy.mockTradingPage(); + cy.mockGQL((req) => { + aliasQuery( + req, + 'EstimateOrder', + generateEstimateOrder({ + estimateOrder: { + marginLevels: { + __typename: 'MarginLevels', + initialLevel: '1000000000', + }, + }, + }) + ); + }); + cy.mockGQLSubscription(); + cy.visit('/#/markets/market-0'); + cy.connectVegaWallet(); + cy.wait('@Market'); + }); + it('should display info and button for deposit', () => { + //7002-SORD-003 + // warning should show immediately + cy.getByTestId('dealticket-warning-margin').should( + 'contain.text', + 'You may not have enough margin available to open this position' + ); + cy.getByTestId('dealticket-warning-margin').should( + 'contain.text', + '10,000.00 tBTC currently required, 1,000.00 tBTC available' + ); + cy.getByTestId('deal-ticket-deposit-dialog-button').click(); + cy.getByTestId('dialog-content') + .find('h1') + .eq(0) + .should('have.text', 'Deposit'); + }); }); }); diff --git a/libs/deal-ticket/src/components/deal-ticket/deal-ticket.spec.tsx b/libs/deal-ticket/src/components/deal-ticket/deal-ticket.spec.tsx index cecd7c476..e2fce7ee9 100644 --- a/libs/deal-ticket/src/components/deal-ticket/deal-ticket.spec.tsx +++ b/libs/deal-ticket/src/components/deal-ticket/deal-ticket.spec.tsx @@ -1,6 +1,12 @@ /* eslint-disable @typescript-eslint/no-explicit-any */ import { VegaWalletContext } from '@vegaprotocol/wallet'; -import { fireEvent, render, screen, act } from '@testing-library/react'; +import { + fireEvent, + render, + screen, + act, + waitFor, +} from '@testing-library/react'; import { generateMarket } from '../../test-helpers'; import { DealTicket } from './deal-ticket'; import { Schema } from '@vegaprotocol/types'; @@ -9,6 +15,14 @@ import type { MockedResponse } from '@apollo/client/testing'; import { MockedProvider } from '@apollo/client/testing'; import type { ChainIdQuery } from '@vegaprotocol/react-helpers'; import { ChainIdDocument, addDecimal } from '@vegaprotocol/react-helpers'; +import * as utils from '../../utils'; + +let mockHasNoBalance = false; +jest.mock('../../hooks/use-has-no-balance', () => { + return { + useHasNoBalance: () => mockHasNoBalance, + }; +}); const market = generateMarket(); const submit = jest.fn(); @@ -31,7 +45,7 @@ function generateJsx(order?: OrderSubmissionBody['orderSubmission']) { }; return ( - + { beforeEach(() => window.localStorage.clear()); - afterEach(() => window.localStorage.clear()); + afterEach(() => { + window.localStorage.clear(); + jest.clearAllMocks(); + }); + it('should display ticket defaults', () => { render(generateJsx()); @@ -165,4 +183,48 @@ describe('DealTicket', () => { Schema.OrderTimeInForce.TIME_IN_FORCE_IOC ); }); + + it('validation should be reset', async () => { + mockHasNoBalance = true; + jest.spyOn(utils, 'validateMarketState').mockReturnValue('Wrong state'); + jest + .spyOn(utils, 'validateMarketTradingMode') + .mockReturnValue('Wrong trading mode'); + const { rerender } = render(generateJsx()); + + await act(async () => { + fireEvent.click(screen.getByTestId('place-order')); + }); + await waitFor(async () => { + expect( + await screen.getByTestId('dealticket-error-message-summary') + ).toHaveTextContent('Wrong state'); + }); + + jest.spyOn(utils, 'validateMarketState').mockReturnValue(true); + await act(async () => { + rerender(generateJsx()); + }); + await act(async () => { + fireEvent.click(screen.getByTestId('place-order')); + }); + await waitFor(async () => { + expect( + await screen.getByTestId('dealticket-error-message-zero-balance') + ).toHaveTextContent('Insufficient balance.'); + }); + + mockHasNoBalance = false; + await act(async () => { + rerender(generateJsx()); + }); + await act(async () => { + fireEvent.click(screen.getByTestId('place-order')); + }); + await waitFor(async () => { + expect( + await screen.getByTestId('dealticket-error-message-summary') + ).toHaveTextContent('Wrong trading mode'); + }); + }); }); diff --git a/libs/deal-ticket/src/components/deal-ticket/deal-ticket.tsx b/libs/deal-ticket/src/components/deal-ticket/deal-ticket.tsx index 98051c73e..e993a7c9f 100644 --- a/libs/deal-ticket/src/components/deal-ticket/deal-ticket.tsx +++ b/libs/deal-ticket/src/components/deal-ticket/deal-ticket.tsx @@ -23,7 +23,7 @@ import { validateType, } from '../../utils'; import { ZeroBalanceError } from '../deal-ticket-validation/zero-balance-error'; -import { AccountValidationType } from '../../constants'; +import { SummaryValidationType } from '../../constants'; import { useHasNoBalance } from '../../hooks/use-has-no-balance'; import type { MarketDealTicket } from '@vegaprotocol/market-list'; @@ -55,18 +55,42 @@ export const DealTicket = ({ handleSubmit, watch, setError, + clearErrors, formState: { errors }, } = useForm({ defaultValues: persistedOrder || getDefaultOrder(market), }); const order = watch(); - // When order state changes persist it in local storage - useEffect(() => setPersistedOrder(order), [order, setPersistedOrder]); - + const marketStateError = validateMarketState(market.data.marketState); const hasNoBalance = useHasNoBalance( market.tradableInstrument.instrument.product.settlementAsset.id ); + const marketTradingModeError = validateMarketTradingMode( + market.data.marketTradingMode + ); + useEffect(() => { + if ( + (!hasNoBalance && + errors.summary?.type === SummaryValidationType.NoCollateral) || + (marketStateError === true && + errors.summary?.type === SummaryValidationType.MarketState) || + (marketTradingModeError === true && + errors.summary?.type === SummaryValidationType.TradingMode) + ) { + clearErrors('summary'); + } + }, [ + hasNoBalance, + marketStateError, + marketTradingModeError, + clearErrors, + errors.summary?.message, + errors.summary?.type, + ]); + + // When order state changes persist it in local storage + useEffect(() => setPersistedOrder(order), [order, setPersistedOrder]); const onSubmit = useCallback( (order: OrderSubmissionBody['orderSubmission']) => { @@ -75,22 +99,27 @@ export const DealTicket = ({ return; } - const marketStateError = validateMarketState(market.data.marketState); if (marketStateError !== true) { - setError('summary', { message: marketStateError }); + setError('summary', { + message: marketStateError, + type: SummaryValidationType.MarketState, + }); return; } if (hasNoBalance) { - setError('summary', { message: AccountValidationType.NoCollateral }); + setError('summary', { + message: SummaryValidationType.NoCollateral, + type: SummaryValidationType.NoCollateral, + }); return; } - const marketTradingModeError = validateMarketTradingMode( - market.data.marketTradingMode - ); if (marketTradingModeError !== true) { - setError('summary', { message: marketTradingModeError }); + setError('summary', { + message: marketTradingModeError, + type: SummaryValidationType.TradingMode, + }); return; } @@ -110,8 +139,8 @@ export const DealTicket = ({ hasNoBalance, market.positionDecimalPlaces, market.decimalPlaces, - market.data.marketState, - market.data.marketTradingMode, + marketStateError, + marketTradingModeError, setError, ] ); @@ -210,7 +239,7 @@ const SummaryMessage = memo( market, order, }); - if (errorMessage === AccountValidationType.NoCollateral) { + if (errorMessage === SummaryValidationType.NoCollateral) { return (