diff --git a/apps/trading-e2e/src/integration/closed-markets.cy.ts b/apps/trading-e2e/src/integration/closed-markets.cy.ts
deleted file mode 100644
index f0861de49..000000000
--- a/apps/trading-e2e/src/integration/closed-markets.cy.ts
+++ /dev/null
@@ -1,475 +0,0 @@
-import { aliasGQLQuery } from '@vegaprotocol/cypress';
-import type { DataSourceDefinition } from '@vegaprotocol/types';
-import {
- MarketState,
- MarketStateMapping,
- PropertyKeyType,
-} from '@vegaprotocol/types';
-import { addDays, subDays } from 'date-fns';
-import {
- chainIdQuery,
- statisticsQuery,
- createDataConnection,
- oracleSpecDataConnectionQuery,
- createMarketFragment,
- marketsQuery,
- marketsDataQuery,
- createMarketsDataFragment,
- assetQuery,
- networkParamsQuery,
- nodeGuardQuery,
-} from '@vegaprotocol/mock';
-import {
- addDecimalsFormatNumber,
- getDateTimeFormat,
-} from '@vegaprotocol/utils';
-
-describe('Closed markets', { tags: '@smoke' }, () => {
- const settlementDataProperty = 'settlement-data-property';
- const settlementDataPropertyKey = {
- __typename: 'PropertyKey' as const,
- name: settlementDataProperty,
- type: PropertyKeyType.TYPE_INTEGER,
- numberDecimalPlaces: 2,
- };
- const settlementDataSourceData: DataSourceDefinition = {
- sourceType: {
- sourceType: {
- filters: [
- {
- __typename: 'Filter',
- key: settlementDataPropertyKey,
- },
- ],
- },
- },
- };
- const rowSelector =
- '[data-testid="tab-closed-markets"] .ag-center-cols-container .ag-row';
-
- const assetsResult = assetQuery();
- // @ts-ignore asset definitely exists
- const settlementAsset = assetsResult.assetsConnection.edges[0].node;
-
- const settledMarket = createMarketFragment({
- id: '0',
- state: MarketState.STATE_SETTLED,
- marketTimestamps: {
- open: subDays(new Date(), 10).toISOString(),
- close: subDays(new Date(), 4).toISOString(),
- },
- tradableInstrument: {
- instrument: {
- product: {
- dataSourceSpecBinding: {
- settlementDataProperty,
- },
- dataSourceSpecForTradingTermination: {
- id: 'market-1-trading-termination-oracle-id',
- },
- dataSourceSpecForSettlementData: {
- id: 'market-1-settlement-data-oracle-id',
- data: settlementDataSourceData,
- },
- settlementAsset,
- },
- },
- },
- });
-
- const terminatedMarket = createMarketFragment({
- id: '1',
- state: MarketState.STATE_TRADING_TERMINATED,
- marketTimestamps: {
- open: subDays(new Date(), 10).toISOString(),
- close: null, // market
- },
- tradableInstrument: {
- instrument: {
- metadata: {
- tags: [
- `settlement-expiry-date:${addDays(new Date(), 4).toISOString()}`,
- ],
- },
- product: {
- dataSourceSpecBinding: {
- settlementDataProperty,
- },
- dataSourceSpecForSettlementData: {
- id: 'market-1-settlement-data-oracle-id',
- data: settlementDataSourceData,
- },
- },
- },
- },
- });
-
- const delayedSettledMarket = createMarketFragment({
- id: '2',
- state: MarketState.STATE_TRADING_TERMINATED,
- marketTimestamps: {
- open: subDays(new Date(), 10).toISOString(),
- close: null, // market
- },
- tradableInstrument: {
- instrument: {
- metadata: {
- tags: [
- `settlement-expiry-date:${subDays(new Date(), 2).toISOString()}`,
- ],
- },
- product: {
- dataSourceSpecBinding: {
- settlementDataProperty,
- },
- dataSourceSpecForSettlementData: {
- id: 'market-1-settlement-data-oracle-id',
- data: settlementDataSourceData,
- },
- },
- },
- },
- });
-
- const unknownMarket = createMarketFragment({
- id: '3',
- state: MarketState.STATE_SETTLED,
- });
-
- const closedMarketsResult = [
- {
- node: settledMarket,
- },
- {
- node: terminatedMarket,
- },
- {
- node: delayedSettledMarket,
- },
- { node: unknownMarket },
- {
- node: createMarketFragment({ id: '4', state: MarketState.STATE_PENDING }),
- },
- {
- node: createMarketFragment({ id: '5', state: MarketState.STATE_ACTIVE }),
- },
- ];
-
- const settledMarketData = createMarketsDataFragment({
- market: {
- id: settledMarket.id,
- },
- bestBidPrice: '1000',
- bestOfferPrice: '2000',
- markPrice: '1500',
- });
-
- const closedMarketsDataResult = [
- {
- node: {
- data: settledMarketData,
- },
- },
- {
- node: {
- data: createMarketsDataFragment({
- market: {
- id: terminatedMarket.id,
- },
- }),
- },
- },
- {
- node: {
- data: createMarketsDataFragment({
- market: {
- id: delayedSettledMarket.id,
- },
- }),
- },
- },
- {
- node: {
- data: createMarketsDataFragment({
- market: {
- id: unknownMarket.id,
- },
- }),
- },
- },
- ];
-
- const specDataConnection = createDataConnection();
-
- before(() => {
- cy.setOnBoardingViewed();
- cy.mockGQL((req) => {
- aliasGQLQuery(req, 'ChainId', chainIdQuery());
- aliasGQLQuery(req, 'Statistics', statisticsQuery());
- aliasGQLQuery(req, 'NodeGuard', nodeGuardQuery());
- aliasGQLQuery(req, 'NetworkParams', networkParamsQuery());
- aliasGQLQuery(
- req,
- 'Markets',
- marketsQuery({
- marketsConnection: {
- edges: closedMarketsResult,
- },
- })
- );
- aliasGQLQuery(
- req,
- 'MarketsData',
- marketsDataQuery({
- marketsConnection: {
- edges: closedMarketsDataResult,
- },
- })
- );
- aliasGQLQuery(
- req,
- 'OracleSpecDataConnection',
- oracleSpecDataConnectionQuery()
- );
- });
-
- cy.mockSubscription();
-
- cy.visit('/#/markets/all');
- cy.get('[data-testid="Closed markets"]').click();
- });
-
- it('renders a settled market', () => {
- const expectedMarkets = closedMarketsResult.filter((edge) => {
- return [
- MarketState.STATE_SETTLED,
- MarketState.STATE_TRADING_TERMINATED,
- ].includes(edge.node.state);
- });
- const product = settledMarket.tradableInstrument.instrument.product;
-
- // rows should be filtered to only include settled/terminated markets
- cy.get(rowSelector).should('have.length', expectedMarkets.length);
-
- // check each column in the first row renders correctly
- // 6001-MARK-001
- cy.get(rowSelector)
- .first()
- .find('[col-id="code"]')
- .find('[data-testid="market-code"]')
- .should('have.text', settledMarket.tradableInstrument.instrument.code);
-
- // 6001-MARK-071
- cy.get(rowSelector)
- .first()
- .find('[title="Future"]')
- .should('have.text', 'Futr');
-
- // 6001-MARK-002
- cy.get(rowSelector)
- .first()
- .find('[col-id="name"]')
- .should('have.text', settledMarket.tradableInstrument.instrument.name);
-
- // 6001-MARK-003
- cy.get(rowSelector)
- .first()
- .find('[col-id="state"]')
- .should('have.text', MarketStateMapping[settledMarket.state]);
-
- // 6001-MARK-004
- // 6001-MARK-005
- // 6001-MARK-009
- // 6001-MARK-008
- // 6001-MARK-010
- cy.get(rowSelector)
- .first()
- .find('[col-id="settlementDate"]')
- .find('[data-testid="link"]')
- .should(($el) => {
- const href = $el.attr('href');
- expect(href).to.match(
- new RegExp(
- `/oracles/${product.dataSourceSpecForTradingTermination.id}`
- )
- );
- })
- .should('have.text', '4 days ago')
- .should(
- 'have.attr',
- 'title',
- getDateTimeFormat().format(
- new Date(settledMarket.marketTimestamps.close)
- )
- );
-
- // 6001-MARK-011
- cy.get(rowSelector)
- .first()
- .find('[col-id="bestBidPrice"]')
- .should(
- 'have.text',
- addDecimalsFormatNumber(
- settledMarketData.bestBidPrice,
- settledMarket.decimalPlaces
- )
- );
-
- // 6001-MARK-012
- cy.get(rowSelector)
- .first()
- .find('[col-id="bestOfferPrice"]')
- .should(
- 'have.text',
- addDecimalsFormatNumber(
- settledMarketData.bestOfferPrice,
- settledMarket.decimalPlaces
- )
- );
-
- // 6001-MARK-013
- cy.get(rowSelector).first().find('[col-id="markPrice"]').should(
- 'have.text',
-
- addDecimalsFormatNumber(
- settledMarketData.markPrice,
- settledMarket.decimalPlaces
- )
- );
-
- // 6001-MARK-014
- // 6001-MARK-015
- // 6001-MARK-016
- cy.get(rowSelector)
- .first()
- .find('[col-id="settlementDataOracleId"]')
- .find('[data-testid="link"]')
- .should(($el) => {
- const href = $el.attr('href');
- expect(href).to.match(
- new RegExp(`/oracles/${product.dataSourceSpecForSettlementData.id}`)
- );
- })
- .should(
- 'have.text',
- addDecimalsFormatNumber(
- // @ts-ignore cannot deep un-partial
- specDataConnection.externalData.data.data[0].value,
- settlementDataPropertyKey.numberDecimalPlaces
- )
- );
-
- // 6001-MARK-018
- cy.get(rowSelector)
- .first()
- .find('[col-id="settlementAsset"]')
- .should('have.text', product.settlementAsset.symbol);
-
- // 6001-MARK-020
- cy.get('.ag-pinned-right-cols-container')
- .find('[col-id="market-actions"]')
- .first()
- .find('button svg')
- .should('exist');
- if (Cypress.env('NX_SUCCESSOR_MARKETS')) {
- cy.get(rowSelector)
- .find('[col-id="successorMarket"]')
- .first()
- .should('have.text', '-');
- }
- });
-
- // test market list for market in terminated state
- it('renders a terminated market', () => {
- cy.get(rowSelector)
- .eq(1)
- .find('[col-id="state"]')
- .should('have.text', MarketStateMapping[terminatedMarket.state]);
-
- // 6001-MARK-006
- // 6001-MARK-007
- cy.get(rowSelector)
- .eq(1)
- .find('[col-id="settlementDate"]')
- .find('[data-testid="link"]')
- .should('have.text', 'Expected in 4 days');
- });
-
- it('renders a terminated market which was expected to have settled', () => {
- cy.get(rowSelector)
- .eq(2)
- .find('[col-id="settlementDate"]')
- .should('have.class', 'text-danger')
- .find('[data-testid="link"]')
- .should('have.text', 'Expected 2 days ago');
- });
-
- it('renders terminated market which doesnt have settlement date metadata', () => {
- cy.get(rowSelector)
- .eq(3)
- .find('[col-id="settlementDate"]')
- .find('[data-testid="link"]')
- .should('have.text', 'Unknown');
- });
-
- it('can open asset detail dialog', () => {
- cy.mockGQL((req) => {
- aliasGQLQuery(req, 'Asset', assetsResult);
- });
-
- cy.get(rowSelector)
- .first()
- .find('[col-id="settlementAsset"]')
- .find('button')
- .click();
-
- // 6001-MARK-019
- cy.get('[data-testid="dialog-title"]').should(
- 'have.text',
- `Asset details - ${settlementAsset.symbol}`
- );
-
- cy.get('[data-testid="dialog-close"]').click();
- });
-
- it('can open row actions', () => {
- cy.get('.ag-pinned-right-cols-container')
- .find('[col-id="market-actions"]')
- .first()
- .find('button')
- .click();
-
- const dropdownContent = '[data-testid="market-actions-content"]';
- const dropdownContentItem = '[role="menuitem"]';
- cy.get(dropdownContent)
- .find(dropdownContentItem)
- .eq(0)
- // Cannot click the copy button as it falls back to window.prompt, blocking the test.
- .should('have.text', 'Copy Market ID');
-
- cy.get(dropdownContent)
- .find(dropdownContentItem)
- .eq(1)
- .find('a')
- .then(($el) => {
- const href = $el.attr('href');
- expect(/\/markets\/0/.test(href || '')).to.equal(true);
- })
- .should('have.text', 'View on Explorer');
- });
-});
-
-describe('no closed markets', { tags: '@smoke', testIsolation: true }, () => {
- before(() => {
- cy.mockTradingPage();
- cy.mockSubscription();
- cy.setOnBoardingViewed();
- cy.visit('/#/markets/all');
- cy.get('[data-testid="Closed markets"]').click();
- });
-
- it('can see no markets message', () => {
- // 6001-MARK-034
- cy.getByTestId('tab-closed-markets').should('contain.text', 'No markets');
- });
-});
diff --git a/apps/trading-e2e/src/integration/trading-deal-ticket-order.cy.ts b/apps/trading-e2e/src/integration/trading-deal-ticket-order.cy.ts
index 9b83ed530..fe82aee3e 100644
--- a/apps/trading-e2e/src/integration/trading-deal-ticket-order.cy.ts
+++ b/apps/trading-e2e/src/integration/trading-deal-ticket-order.cy.ts
@@ -33,9 +33,7 @@ describe('deal ticker order validation', { tags: '@smoke' }, () => {
it('must see the price unit', function () {
// 7002-SORD-018
- cy.getByTestId(orderPriceField)
- .siblings('label')
- .should('have.text', 'Price (DAI)');
+ cy.getByTestId(orderPriceField).next().should('have.text', 'DAI');
});
it('must see warning when placing an order with expiry date in past', () => {
diff --git a/libs/deal-ticket/src/components/deal-ticket/deal-ticket-button.tsx b/libs/deal-ticket/src/components/deal-ticket/deal-ticket-button.tsx
deleted file mode 100644
index 39070688c..000000000
--- a/libs/deal-ticket/src/components/deal-ticket/deal-ticket-button.tsx
+++ /dev/null
@@ -1,25 +0,0 @@
-import { t } from '@vegaprotocol/i18n';
-import { Side } from '@vegaprotocol/types';
-import classNames from 'classnames';
-
-interface Props {
- side: Side;
- label?: string;
-}
-
-export const DealTicketButton = ({ side, label }: Props) => {
- const buttonClasses = classNames(
- 'px-10 py-2 uppercase rounded-md text-white w-full',
- {
- 'bg-market-red': side === Side.SIDE_SELL,
- 'bg-market-green-550': side === Side.SIDE_BUY,
- }
- );
- return (
-
-
-
- );
-};
diff --git a/libs/deal-ticket/src/components/deal-ticket/deal-ticket-fee-details.tsx b/libs/deal-ticket/src/components/deal-ticket/deal-ticket-fee-details.tsx
index 7df3041f3..182ab55fa 100644
--- a/libs/deal-ticket/src/components/deal-ticket/deal-ticket-fee-details.tsx
+++ b/libs/deal-ticket/src/components/deal-ticket/deal-ticket-fee-details.tsx
@@ -1,7 +1,4 @@
import { useCallback, useState } from 'react';
-import { Tooltip } from '@vegaprotocol/ui-toolkit';
-import classnames from 'classnames';
-import type { ReactNode } from 'react';
import { t } from '@vegaprotocol/i18n';
import { FeesBreakdown } from '@vegaprotocol/markets';
import type { OrderSubmissionBody } from '@vegaprotocol/wallet';
@@ -16,7 +13,6 @@ import { marketMarginDataProvider } from '@vegaprotocol/accounts';
import { useDataProvider } from '@vegaprotocol/data-provider';
import {
- NOTIONAL_SIZE_TOOLTIP_TEXT,
MARGIN_DIFF_TOOLTIP_TEXT,
DEDUCTION_FROM_COLLATERAL_TOOLTIP_TEXT,
TOTAL_MARGIN_AVAILABLE,
@@ -25,114 +21,54 @@ import {
MARGIN_ACCOUNT_TOOLTIP_TEXT,
} from '../../constants';
import { useEstimateFees } from '../../hooks';
+import { KeyValue } from './key-value';
const emptyValue = '-';
-export interface DealTicketFeeDetailPros {
- label: string;
- value?: string | null | undefined;
- symbol: string;
- indent?: boolean | undefined;
- labelDescription?: ReactNode;
- formattedValue?: string;
- onClick?: () => void;
-}
-
-export const DealTicketFeeDetail = ({
- label,
- value,
- labelDescription,
- symbol,
- indent,
- onClick,
- formattedValue,
-}: DealTicketFeeDetailPros) => {
- const displayValue = `${formattedValue ?? '-'} ${symbol || ''}`;
- const valueElement = onClick ? (
-
- ) : (
- {displayValue}
- );
- return (
-
-
- {label}
-
-
- {valueElement}
-
-
- );
-};
-
export interface DealTicketFeeDetailsProps {
assetSymbol: string;
order: OrderSubmissionBody['orderSubmission'];
market: Market;
- notionalSize: string | null;
}
export const DealTicketFeeDetails = ({
assetSymbol,
order,
market,
- notionalSize,
}: DealTicketFeeDetailsProps) => {
const feeEstimate = useEstimateFees(order);
const { settlementAsset: asset } =
market.tradableInstrument.instrument.product;
const { decimals: assetDecimals, quantum } = asset;
- const marketDecimals = market.decimalPlaces;
- const quoteName = market.tradableInstrument.instrument.product.quoteName;
return (
- <>
-
-
-
- {t(
- `An estimate of the most you would be expected to pay in fees, in the market's settlement asset ${assetSymbol}.`
- )}
-
-
- >
- }
- symbol={assetSymbol}
- />
- >
+
+
+ {t(
+ `An estimate of the most you would be expected to pay in fees, in the market's settlement asset ${assetSymbol}.`
+ )}
+
+
+ >
+ }
+ symbol={assetSymbol}
+ />
);
};
@@ -209,7 +145,7 @@ export const DealTicketMarginDetails = ({
BigInt(marginAccountBalance);
deductionFromCollateral = (
-
);
projectedMargin = (
-
-
-
{deductionFromCollateral}
-
{projectedMargin}
- {
if (peakSizeError) {
return (
-
+
{peakSizeError}
);
@@ -44,7 +44,7 @@ export const DealTicketSizeIceberg = ({
const renderMinimumSizeError = () => {
if (minimumVisibleSizeError) {
return (
-
+
{minimumVisibleSizeError}
);
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 c389b0541..ce1762733 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
@@ -1,6 +1,6 @@
/* eslint-disable @typescript-eslint/no-explicit-any */
import { VegaWalletContext } from '@vegaprotocol/wallet';
-import { render, screen, waitFor } from '@testing-library/react';
+import { fireEvent, render, screen, waitFor } from '@testing-library/react';
import userEvent from '@testing-library/user-event';
import { generateMarket } from '../../test-helpers';
import { StopOrder } from './deal-ticket-stop-order';
@@ -12,6 +12,7 @@ import {
useDealTicketFormValues,
} from '../../hooks/use-form-values';
import type { FeatureFlags } from '@vegaprotocol/environment';
+import { formatForInput } from '@vegaprotocol/utils';
jest.mock('zustand');
jest.mock('./deal-ticket-fee-details', () => ({
@@ -57,7 +58,7 @@ const orderSideBuy = 'order-side-SIDE_BUY';
const orderSideSell = 'order-side-SIDE_SELL';
const triggerDirectionRisesAbove = 'triggerDirection-risesAbove';
-// const triggerDirectionFallsBelow = 'triggerDirection-fallsBelow';
+const triggerDirectionFallsBelow = 'triggerDirection-fallsBelow';
const expiryStrategySubmit = 'expiryStrategy-submit';
const expiryStrategyCancel = 'expiryStrategy-cancel';
@@ -65,6 +66,7 @@ const expiryStrategyCancel = 'expiryStrategy-cancel';
const triggerTypePrice = 'triggerType-price';
const triggerTypeTrailingPercentOffset = 'triggerType-trailingPercentOffset';
+const oco = 'oco';
const expire = 'expire';
const datePicker = 'date-picker-field';
const timeInForce = 'order-tif';
@@ -76,6 +78,8 @@ const triggerPriceWarningMessage = 'stop-order-warning-message-trigger-price';
const triggerTrailingPercentOffsetErrorMessage =
'stop-order-error-message-trigger-trailing-percent-offset';
+const ocoPostfix = (id: string, postfix = true) => (postfix ? `${id}-oco` : id);
+
describe('StopOrder', () => {
beforeEach(() => {
localStorage.clear();
@@ -107,6 +111,7 @@ describe('StopOrder', () => {
'checked'
);
expect(screen.getByTestId(expire).dataset.state).toEqual('unchecked');
+ expect(screen.getByTestId(oco).dataset.state).toEqual('unchecked');
await userEvent.click(screen.getByTestId(expire));
await waitFor(() => {
expect(screen.getByTestId(expiryStrategySubmit).dataset.state).toEqual(
@@ -115,6 +120,32 @@ describe('StopOrder', () => {
});
});
+ it('calculate notional for market limit', async () => {
+ render(generateJsx());
+ await userEvent.type(screen.getByTestId(sizeInput), '10');
+ await userEvent.type(screen.getByTestId(priceInput), '10');
+ expect(screen.getByTestId('deal-ticket-fee-notional')).toHaveTextContent(
+ 'Notional100.00 BTC'
+ );
+ });
+
+ it('calculates notional for limit order', async () => {
+ render(generateJsx());
+ await userEvent.click(screen.getByTestId(orderTypeTrigger));
+ await userEvent.click(screen.getByTestId(orderTypeMarket));
+ await userEvent.type(screen.getByTestId(sizeInput), '10');
+ // price trigger is selected but it's empty, calculate base on size and marketPrice prop
+ expect(screen.getByTestId('deal-ticket-fee-notional')).toHaveTextContent(
+ 'Notional20.00 BTC'
+ );
+
+ await userEvent.type(screen.getByTestId(triggerPriceInput), '3');
+ // calculate base on size and price trigger
+ expect(screen.getByTestId('deal-ticket-fee-notional')).toHaveTextContent(
+ 'Notional30.00 BTC'
+ );
+ });
+
it('should use local storage state for initial values', async () => {
const values: Partial = {
type: Schema.OrderType.TYPE_LIMIT,
@@ -125,6 +156,11 @@ describe('StopOrder', () => {
expire: true,
expiryStrategy: Schema.StopOrderExpiryStrategy.EXPIRY_STRATEGY_CANCELS,
expiresAt: '2023-07-27T16:43:27.000',
+ oco: true,
+ ocoType: Schema.OrderType.TYPE_LIMIT,
+ ocoSize: '0.2',
+ ocoPrice: '300.23',
+ ocoTimeInForce: Schema.OrderTimeInForce.TIME_IN_FORCE_FOK,
};
useDealTicketFormValues.setState({
@@ -143,10 +179,22 @@ describe('StopOrder', () => {
expect(screen.getByTestId(sizeInput)).toHaveDisplayValue(
values.size as string
);
- expect(screen.getByTestId('order-tif')).toHaveValue(values.timeInForce);
+ expect(screen.getByTestId(timeInForce)).toHaveValue(values.timeInForce);
expect(screen.getByTestId(priceInput)).toHaveDisplayValue(
values.price as string
);
+
+ expect(screen.getByTestId(ocoPostfix(sizeInput))).toHaveDisplayValue(
+ values.ocoSize as string
+ );
+ expect(screen.getByTestId(ocoPostfix(timeInForce))).toHaveValue(
+ values.ocoTimeInForce
+ );
+ expect(screen.getByTestId(ocoPostfix(priceInput))).toHaveDisplayValue(
+ values.ocoPrice as string
+ );
+ expect(screen.getByTestId('ocoTypeLimit').dataset.state).toEqual('checked');
+
expect(screen.getByTestId(expire).dataset.state).toEqual('checked');
expect(screen.getByTestId(expiryStrategyCancel).dataset.state).toEqual(
'checked'
@@ -154,6 +202,9 @@ describe('StopOrder', () => {
expect(screen.getByTestId(datePicker)).toHaveDisplayValue(
values.expiresAt as string
);
+
+ await userEvent.click(screen.getByTestId(orderTypeMarket));
+ expect(screen.getByTestId(oco).dataset.state).toEqual('unchecked');
});
it('does not submit if no wallet connected', async () => {
@@ -174,145 +225,239 @@ describe('StopOrder', () => {
expect(submit).toBeCalled();
});
- it('validates size field', async () => {
+ it.each([
+ { fieldName: 'size', ocoValue: false },
+ { fieldName: 'ocoSize', ocoValue: true },
+ ])('validates $fieldName field', async ({ ocoValue }) => {
render(generateJsx());
-
+ if (ocoValue) {
+ await userEvent.click(screen.getByTestId(oco));
+ }
await userEvent.click(screen.getByTestId(submitButton));
-
+ const getByTestId = (id: string) =>
+ screen.getByTestId(ocoPostfix(id, ocoValue));
+ const queryByTestId = (id: string) =>
+ screen.queryByTestId(ocoPostfix(id, ocoValue));
// default value should be invalid
- expect(screen.getByTestId(sizeErrorMessage)).toBeInTheDocument();
+ expect(getByTestId(sizeErrorMessage)).toBeInTheDocument();
// to small value should be invalid
- await userEvent.type(screen.getByTestId(sizeInput), '0.01');
- expect(screen.getByTestId(sizeErrorMessage)).toBeInTheDocument();
+ await userEvent.type(getByTestId(sizeInput), '0.01');
+ expect(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();
+ await userEvent.clear(getByTestId(sizeInput));
+ await userEvent.type(getByTestId(sizeInput), '0.1');
+ expect(queryByTestId(sizeErrorMessage)).toBeNull();
});
- it('validates price field', async () => {
+ it.each([
+ { fieldName: 'price', ocoValue: false },
+ { fieldName: 'ocoPrice', ocoValue: true },
+ ])('validates $fieldName field', async ({ ocoValue }) => {
render(generateJsx());
-
+ if (ocoValue) {
+ await userEvent.click(screen.getByTestId(oco));
+ }
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();
+
+ const getByTestId = (id: string) =>
+ screen.getByTestId(ocoPostfix(id, ocoValue));
+ const queryByTestId = (id: string) =>
+ screen.queryByTestId(ocoPostfix(id, ocoValue));
+
+ expect(getByTestId(priceErrorMessage)).toBeInTheDocument();
+ await userEvent.type(getByTestId(priceInput), '0.001');
+ expect(getByTestId(priceErrorMessage)).toBeInTheDocument();
// switch to market order type error should disappear
await userEvent.click(screen.getByTestId(orderTypeTrigger));
await userEvent.click(screen.getByTestId(orderTypeMarket));
await userEvent.click(screen.getByTestId(submitButton));
- expect(screen.queryByTestId(priceErrorMessage)).toBeNull();
+ expect(queryByTestId(priceErrorMessage)).toBeNull();
// switch back to limit type
await userEvent.click(screen.getByTestId(orderTypeTrigger));
await userEvent.click(screen.getByTestId(orderTypeLimit));
await userEvent.click(screen.getByTestId(submitButton));
- expect(screen.getByTestId(priceErrorMessage)).toBeInTheDocument();
+ expect(getByTestId(priceErrorMessage)).toBeInTheDocument();
// to small value should be invalid
- await userEvent.type(screen.getByTestId(priceInput), '0.001');
- expect(screen.getByTestId(priceErrorMessage)).toBeInTheDocument();
+ await userEvent.type(getByTestId(priceInput), '0.001');
+ expect(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();
+ await userEvent.clear(getByTestId(priceInput));
+ await userEvent.type(getByTestId(priceInput), '0.01');
+ expect(queryByTestId(priceErrorMessage)).toBeNull();
});
- it('validates trigger price field', async () => {
+ it.each([
+ { fieldName: 'triggerPrice', ocoValue: false },
+ { fieldName: 'ocoTriggerPrice', ocoValue: true },
+ ])('validates $fieldName field', async ({ ocoValue }) => {
render(generateJsx());
+ if (ocoValue) {
+ await userEvent.click(screen.getByTestId(oco));
+ await userEvent.click(screen.getByTestId(triggerDirectionFallsBelow));
+ }
await userEvent.click(screen.getByTestId(submitButton));
- expect(screen.getByTestId(triggerPriceErrorMessage)).toBeInTheDocument();
+ const getByTestId = (id: string) =>
+ screen.getByTestId(ocoPostfix(id, ocoValue));
+ const queryByTestId = (id: string) =>
+ screen.queryByTestId(ocoPostfix(id, ocoValue));
+ expect(getByTestId(triggerPriceErrorMessage)).toBeInTheDocument();
// switch to trailing percentage offset trigger type
- await userEvent.click(screen.getByTestId(triggerTypeTrailingPercentOffset));
- expect(screen.queryByTestId(triggerPriceErrorMessage)).toBeNull();
+ await userEvent.click(getByTestId(triggerTypeTrailingPercentOffset));
+ expect(queryByTestId(triggerPriceErrorMessage)).toBeNull();
// switch back to price trigger type
- await userEvent.click(screen.getByTestId(triggerTypePrice));
- expect(screen.getByTestId(triggerPriceErrorMessage)).toBeInTheDocument();
+ await userEvent.click(getByTestId(triggerTypePrice));
+ expect(getByTestId(triggerPriceErrorMessage)).toBeInTheDocument();
// to small value should be invalid
- await userEvent.type(screen.getByTestId(triggerPriceInput), '0.001');
- expect(screen.getByTestId(triggerPriceErrorMessage)).toBeInTheDocument();
+ await userEvent.type(getByTestId(triggerPriceInput), '0.001');
+ expect(getByTestId(triggerPriceErrorMessage)).toBeInTheDocument();
// clear and fill using value causing immediate trigger
- await userEvent.clear(screen.getByTestId(triggerPriceInput));
- await userEvent.type(screen.getByTestId(triggerPriceInput), '0.01');
- expect(screen.queryByTestId(triggerPriceErrorMessage)).toBeNull();
- expect(
- screen.queryByTestId(triggerPriceWarningMessage)
- ).toBeInTheDocument();
+ await userEvent.clear(getByTestId(triggerPriceInput));
+ await userEvent.type(getByTestId(triggerPriceInput), '0.01');
+ expect(queryByTestId(triggerPriceErrorMessage)).toBeNull();
+ expect(queryByTestId(triggerPriceWarningMessage)).toBeInTheDocument();
// change to correct value
- await userEvent.type(screen.getByTestId(triggerPriceInput), '2');
- expect(screen.queryByTestId(triggerPriceWarningMessage)).toBeNull();
+ await userEvent.type(getByTestId(triggerPriceInput), '2');
+ expect(queryByTestId(triggerPriceWarningMessage)).toBeNull();
});
- it('validates trigger trailing percentage offset field', async () => {
+ it.each([
+ { fieldName: 'trailingPercentageOffset', ocoValue: false },
+ { fieldName: 'ocoTrailingPercentageOffset', ocoValue: true },
+ ])('validates $fieldName field', async ({ ocoValue }) => {
render(generateJsx());
+ if (ocoValue) {
+ await userEvent.click(screen.getByTestId(oco));
+ }
+ await userEvent.click(screen.getByTestId(submitButton));
+ const getByTestId = (id: string) =>
+ screen.getByTestId(ocoPostfix(id, ocoValue));
+ const queryByTestId = (id: string) =>
+ screen.queryByTestId(ocoPostfix(id, ocoValue));
// should not show error with default form values
- await userEvent.click(screen.getByTestId(submitButton));
- expect(
- screen.queryByTestId(triggerTrailingPercentOffsetErrorMessage)
- ).toBeNull();
+ expect(queryByTestId(triggerTrailingPercentOffsetErrorMessage)).toBeNull();
// switch to trailing percentage offset trigger type
- await userEvent.click(screen.getByTestId(triggerTypeTrailingPercentOffset));
+ await userEvent.click(getByTestId(triggerTypeTrailingPercentOffset));
expect(
- screen.getByTestId(triggerTrailingPercentOffsetErrorMessage)
+ getByTestId(triggerTrailingPercentOffsetErrorMessage)
).toBeInTheDocument();
// to small value should be invalid
await userEvent.type(
- screen.getByTestId(triggerTrailingPercentOffsetInput),
+ getByTestId(triggerTrailingPercentOffsetInput),
'0.09'
);
expect(
- screen.getByTestId(triggerTrailingPercentOffsetErrorMessage)
+ 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();
+ await userEvent.clear(getByTestId(triggerTrailingPercentOffsetInput));
+ await userEvent.type(getByTestId(triggerTrailingPercentOffsetInput), '0.1');
+ expect(queryByTestId(triggerTrailingPercentOffsetErrorMessage)).toBeNull();
// to big value should be invalid
- await userEvent.clear(
- screen.getByTestId(triggerTrailingPercentOffsetInput)
- );
+ await userEvent.clear(getByTestId(triggerTrailingPercentOffsetInput));
await userEvent.type(
- screen.getByTestId(triggerTrailingPercentOffsetInput),
+ getByTestId(triggerTrailingPercentOffsetInput),
'99.91'
);
expect(
- screen.getByTestId(triggerTrailingPercentOffsetErrorMessage)
+ getByTestId(triggerTrailingPercentOffsetErrorMessage)
).toBeInTheDocument();
// clear and fill using valid value
- await userEvent.clear(
- screen.getByTestId(triggerTrailingPercentOffsetInput)
- );
+ await userEvent.clear(getByTestId(triggerTrailingPercentOffsetInput));
await userEvent.type(
- screen.getByTestId(triggerTrailingPercentOffsetInput),
+ getByTestId(triggerTrailingPercentOffsetInput),
'99.9'
);
+ expect(queryByTestId(triggerTrailingPercentOffsetErrorMessage)).toBeNull();
+ });
+
+ it('sync oco trigger', async () => {
+ render(generateJsx());
+ await userEvent.click(screen.getByTestId(oco));
expect(
- screen.queryByTestId(triggerTrailingPercentOffsetErrorMessage)
- ).toBeNull();
+ screen.getByTestId(triggerDirectionRisesAbove).dataset.state
+ ).toEqual('checked');
+ expect(
+ screen.getByTestId(ocoPostfix(triggerDirectionFallsBelow)).dataset.state
+ ).toEqual('checked');
+ await userEvent.click(screen.getByTestId(triggerDirectionFallsBelow));
+ expect(
+ screen.getByTestId(triggerDirectionRisesAbove).dataset.state
+ ).toEqual('unchecked');
+ expect(
+ screen.getByTestId(ocoPostfix(triggerDirectionFallsBelow)).dataset.state
+ ).toEqual('unchecked');
+ await userEvent.click(
+ screen.getByTestId(ocoPostfix(triggerDirectionFallsBelow))
+ );
+ expect(
+ screen.getByTestId(triggerDirectionRisesAbove).dataset.state
+ ).toEqual('checked');
+ expect(
+ screen.getByTestId(ocoPostfix(triggerDirectionFallsBelow)).dataset.state
+ ).toEqual('checked');
+ });
+
+ it('disables submit expiry strategy when OCO selected', async () => {
+ render(generateJsx());
+ await userEvent.click(screen.getByTestId(expire));
+ await userEvent.click(screen.getByTestId(expiryStrategySubmit));
+ await userEvent.click(screen.getByTestId(oco));
+ expect(screen.getByTestId(expiryStrategySubmit).dataset.state).toEqual(
+ 'unchecked'
+ );
+ expect(screen.getByTestId(expiryStrategySubmit)).toBeDisabled();
+ await userEvent.click(screen.getByTestId(oco));
+ await userEvent.click(screen.getByTestId(expiryStrategySubmit));
+ expect(screen.getByTestId(expiryStrategySubmit).dataset.state).toEqual(
+ 'checked'
+ );
+ expect(screen.getByTestId(expiryStrategySubmit)).not.toBeDisabled();
+ });
+
+ it('sets expiry time/date to now if expiry is changed to checked', async () => {
+ const now = Math.round(Date.now() / 1000) * 1000;
+ render(generateJsx());
+ jest.spyOn(global.Date, 'now').mockImplementationOnce(() => now);
+ await userEvent.click(screen.getByTestId(expire));
+
+ // expiry time/date was empty it should be set to now
+ expect(
+ new Date(screen.getByTestId(datePicker).value).getTime()
+ ).toEqual(now);
+
+ // set to the value in the past (now - 1s)
+ fireEvent.change(screen.getByTestId(datePicker), {
+ target: { value: formatForInput(new Date(now - 1000)) },
+ });
+ expect(
+ new Date(
+ screen.getByTestId(datePicker).value
+ ).getTime() + 1000
+ ).toEqual(now);
+
+ // switch expiry off and on
+ await userEvent.click(screen.getByTestId(expire));
+ await userEvent.click(screen.getByTestId(expire));
+ // expiry time/date was in the past it should be set to now
+ expect(
+ new Date(screen.getByTestId(datePicker).value).getTime()
+ ).toEqual(now);
});
});
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 b18e72e4d..cf9368dad 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
@@ -1,8 +1,12 @@
import { useRef, useCallback, useEffect } from 'react';
import { useVegaWallet } from '@vegaprotocol/wallet';
-import type { StopOrdersSubmission } from '@vegaprotocol/wallet';
+import type {
+ OrderSubmissionBody,
+ StopOrdersSubmission,
+} from '@vegaprotocol/wallet';
import {
formatForInput,
+ formatValue,
removeDecimal,
toDecimal,
validateAmount,
@@ -19,6 +23,9 @@ import {
TradingInputError as InputError,
TradingSelect as Select,
Tooltip,
+ TradingButton as Button,
+ Pill,
+ Intent,
} from '@vegaprotocol/ui-toolkit';
import { getDerivedPrice } from '@vegaprotocol/markets';
import type { Market } from '@vegaprotocol/markets';
@@ -31,6 +38,7 @@ import {
REDUCE_ONLY_TOOLTIP,
stopSubmit,
getNotionalSize,
+ getAssetUnit,
} from './deal-ticket';
import { TypeToggle } from './type-selector';
import {
@@ -41,9 +49,10 @@ import {
} from '../../hooks/use-form-values';
import type { StopOrderFormValues } from '../../hooks/use-form-values';
import { mapFormValuesToStopOrdersSubmission } from '../../utils/map-form-values-to-submission';
-import { DealTicketButton } from './deal-ticket-button';
import { DealTicketFeeDetails } from './deal-ticket-fee-details';
import { validateExpiration } from '../../utils';
+import { NOTIONAL_SIZE_TOOLTIP_TEXT } from '../../constants';
+import { KeyValue } from './key-value';
export interface StopOrderProps {
market: Market;
@@ -78,7 +87,7 @@ const Trigger = ({
control,
watch,
priceStep,
- assetSymbol,
+ quoteName,
oco,
marketPrice,
decimalPlaces,
@@ -86,7 +95,7 @@ const Trigger = ({
control: Control;
watch: UseFormWatch;
priceStep: string;
- assetSymbol: string;
+ quoteName: string;
oco?: boolean;
marketPrice?: string | null;
decimalPlaces: number;
@@ -181,7 +190,7 @@ const Trigger = ({
data-testid={`triggerPrice${oco ? '-oco' : ''}`}
type="number"
step={priceStep}
- appendElement={assetSymbol}
+ appendElement={{quoteName}}
value={value || ''}
hasError={!!fieldState.error}
{...props}
@@ -249,7 +258,7 @@ const Trigger = ({
%}
data-testid={`triggerTrailingPercentOffset${
oco ? '-oco' : ''
}`}
@@ -311,10 +320,14 @@ const Size = ({
control,
sizeStep,
oco,
+ isLimitType,
+ assetUnit,
}: {
control: Control;
sizeStep: string;
oco?: boolean;
+ isLimitType: boolean;
+ assetUnit?: string;
}) => {
return (
+
e.currentTarget.blur()}
+ appendElement={assetUnit && {assetUnit}}
data-testid={id}
value={value || ''}
hasError={!!fieldState.error}
@@ -394,12 +408,8 @@ const Price = ({
const { value, ...props } = field;
const id = `order-price${oco ? '-oco' : ''}`;
return (
-
-
+
+
e.currentTarget.blur()}
value={value || ''}
hasError={!!fieldState.error}
+ appendElement={{quoteName}}
{...props}
/>
@@ -434,17 +445,17 @@ const TimeInForce = ({
oco?: boolean;
}) => (
{
- const id = `select-time-in-force${oco ? '-oco' : ''}`;
+ const id = `order-tif${oco ? '-oco' : ''}`;
return (