From d55b923672526c4607b9bc15810252d4eb92dead Mon Sep 17 00:00:00 2001 From: Jorge Galat Date: Fri, 22 Sep 2023 13:16:43 -0300 Subject: [PATCH] =?UTF-8?q?=E2=9C=85=20e2e:=20add=20rollover=20tests?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- components/DebtManager/Operation/index.tsx | 14 +- .../DebtManager/PositionTable/index.tsx | 1 + components/DebtManager/index.tsx | 2 + components/common/modal/Loading/index.tsx | 4 +- components/common/modal/ModalSheet/index.tsx | 8 +- .../TableRowFixedPool/index.tsx | 1 + .../TableRowFloatingPool/index.tsx | 3 +- components/operations/Faucet/index.tsx | 2 +- contexts/DebtManagerContext.tsx | 1 + e2e/common/balance.ts | 13 +- e2e/components/modal.ts | 1 + e2e/components/rollover.ts | 117 ++++++++++++++++ e2e/page/dashboard.ts | 6 + e2e/specs/4-fixed-borrow-repay/op.spec.ts | 2 +- e2e/specs/6-rollover/usdc.spec.ts | 131 ++++++++++++++++++ e2e/utils/contracts.ts | 7 +- hooks/useWeb3.ts | 2 +- 17 files changed, 296 insertions(+), 19 deletions(-) create mode 100644 e2e/components/rollover.ts create mode 100644 e2e/specs/6-rollover/usdc.spec.ts diff --git a/components/DebtManager/Operation/index.tsx b/components/DebtManager/Operation/index.tsx index fed15bd21..b42f8a8ae 100644 --- a/components/DebtManager/Operation/index.tsx +++ b/components/DebtManager/Operation/index.tsx @@ -460,6 +460,7 @@ function Operation() { open={fromSheetOpen} onClose={onClose} title={t('Select Current Debt')} + data-testid="rollover-sheet-from" > @@ -526,6 +528,7 @@ function Operation() { selected={Boolean(input.from)} onClick={() => setSheetOpen([true, false])} sx={{ ml: -0.5 }} + data-testid="rollover-from" > {input.from ? ( <> @@ -536,7 +539,12 @@ function Operation() { t('Current debt') )} - + {input.from ? input.from.maturity ? parseTimestamp(input.from.maturity) @@ -558,6 +566,7 @@ function Operation() { }} disabled={!input.from} sx={{ ml: -0.5, mr: -0.5 }} + data-testid="rollover-to" > {input.to ? ( <> @@ -568,7 +577,7 @@ function Operation() { t('New debt') )} - + {input.to ? (input.to.maturity ? parseTimestamp(input.to.maturity) : t('Open-ended')) : t('Maturity')} @@ -588,6 +597,7 @@ function Operation() { {errorData?.status && } onClick(row)} + data-testid={`rollover-sheet-row-${row.symbol}${row.maturity ? `-${row.maturity}` : ''}`} > diff --git a/components/DebtManager/index.tsx b/components/DebtManager/index.tsx index 119ba2f26..58ec6fb7a 100644 --- a/components/DebtManager/index.tsx +++ b/components/DebtManager/index.tsx @@ -70,9 +70,11 @@ function DebtManagerModal({ isOpen, close }: Props) { sx={isMobile ? { top: 'auto' } : { backdropFilter: tx ? 'blur(1.5px)' : '' }} BackdropProps={{ style: { backgroundColor: tx ? 'rgb(100, 100, 100 , 0.1)' : '' } }} disableEscapeKeyDown={loadingTx} + data-testid="rollover-modal" > {!loadingTx && ( )} - + {isLoading && t('Processing transaction...')} {isSuccess && t('Transaction completed')} {isError && t('Transaction error')} - + {isLoading && pending} {isSuccess && success} {isError && error} diff --git a/components/common/modal/ModalSheet/index.tsx b/components/common/modal/ModalSheet/index.tsx index 307b474f9..0377cc56c 100644 --- a/components/common/modal/ModalSheet/index.tsx +++ b/components/common/modal/ModalSheet/index.tsx @@ -1,5 +1,5 @@ import React, { forwardRef, type PropsWithChildren } from 'react'; -import { Box, IconButton, Typography, useTheme, Slide, SlideProps, Backdrop } from '@mui/material'; +import { Box, IconButton, Typography, useTheme, Slide, SlideProps, Backdrop, BoxProps } from '@mui/material'; import CloseIcon from '@mui/icons-material/Close'; type Props = PropsWithChildren<{ @@ -7,9 +7,10 @@ type Props = PropsWithChildren<{ title?: string; open: boolean; onClose: () => void; -}>; +}> & + BoxProps; -const ModalSheet = forwardRef(function ModalSheet({ title, container, open, onClose, children }: Props, ref) { +const ModalSheet = forwardRef(function ModalSheet({ title, container, open, onClose, children, ...props }: Props, ref) { const { spacing, palette } = useTheme(); return ( @@ -29,6 +30,7 @@ const ModalSheet = forwardRef(function ModalSheet({ title, container, open, onCl zIndex: 2, overflowY: 'hidden', }} + {...props} > startDebtManager({ from: { symbol, maturity: maturityDate } })} disabled={isRolloverDisabled()} + data-testid={`fixed-rollover-${maturityDate}-${symbol}`} > {t('Rollover')} diff --git a/components/dashboard/DashboardContent/FloatingPoolDashboard/FloatingPoolDashboardTable/TableRowFloatingPool/index.tsx b/components/dashboard/DashboardContent/FloatingPoolDashboard/FloatingPoolDashboardTable/TableRowFloatingPool/index.tsx index 71c1f96f6..0c77e3cc7 100644 --- a/components/dashboard/DashboardContent/FloatingPoolDashboard/FloatingPoolDashboardTable/TableRowFloatingPool/index.tsx +++ b/components/dashboard/DashboardContent/FloatingPoolDashboard/FloatingPoolDashboardTable/TableRowFloatingPool/index.tsx @@ -37,7 +37,7 @@ function TableRowFloatingPool({ symbol, valueUSD, depositedAmount, borrowedAmoun @@ -128,6 +128,7 @@ function TableRowFloatingPool({ symbol, valueUSD, depositedAmount, borrowedAmoun }} onClick={() => startDebtManager({ from: { symbol } })} disabled={isRolloverDisabled(borrowedAmount)} + data-testid={`floating-rollover-${symbol}`} > {t('Rollover')} diff --git a/components/operations/Faucet/index.tsx b/components/operations/Faucet/index.tsx index a3c1d2448..a4bfcbecc 100644 --- a/components/operations/Faucet/index.tsx +++ b/components/operations/Faucet/index.tsx @@ -44,7 +44,7 @@ function Faucet() { setLoading(symbol); const amounts: Record = { DAI: '50000', - USDC: '50000000', + USDC: '50000', WBTC: '2', }; diff --git a/contexts/DebtManagerContext.tsx b/contexts/DebtManagerContext.tsx index 6e54ec50a..34c732677 100644 --- a/contexts/DebtManagerContext.tsx +++ b/contexts/DebtManagerContext.tsx @@ -163,6 +163,7 @@ export const DebtManagerContextProvider: FC> = ({ args, await refreshAccountData(); } catch (e: unknown) { + console.log(e); track.removeFromCart('roll', input); setErrorData({ status: true, message: handleOperationError(e) }); } finally { diff --git a/e2e/common/balance.ts b/e2e/common/balance.ts index 7db3e095d..82237d387 100644 --- a/e2e/common/balance.ts +++ b/e2e/common/balance.ts @@ -10,19 +10,20 @@ export default function ({ test, publicClient }: CommonTest & { publicClient: Pu address: Address; symbol: ERC20TokenSymbol; amount: string; - delta?: number; + delta?: string; }; - const check = async ({ address, symbol, amount, delta }: BalanceParams) => { - await test.step(`checks ${symbol} balance to be ${delta ? 'near ' : ''}${amount}`, async () => { + const check = async ({ address, symbol, amount, delta: _delta }: BalanceParams) => { + await test.step(`checks ${symbol} balance to be ${_delta ? 'near ' : ''}${amount}`, async () => { const erc20Contract = await erc20(symbol, { publicClient }); const balance = await erc20Contract.read.balanceOf([address]); const decimals = await erc20Contract.read.decimals(); const expected = parseUnits(amount, decimals); - if (delta) { + if (_delta) { const wad = parseUnits('1', decimals); - const lower = (expected * parseUnits(String(1 - delta), decimals)) / wad; - const upper = (expected * parseUnits(String(1 + delta), decimals)) / wad; + const delta = parseUnits(_delta, decimals); + const lower = (expected * (wad - delta)) / wad; + const upper = (expected * (wad + delta)) / wad; expect(balance > lower).toBe(true); expect(balance < upper).toBe(true); diff --git a/e2e/components/modal.ts b/e2e/components/modal.ts index 6615b4a86..48fcdc086 100644 --- a/e2e/components/modal.ts +++ b/e2e/components/modal.ts @@ -30,6 +30,7 @@ export default function (page: Page) { await page.getByTestId(`${type}-${maturity ? `${maturity}-` : ''}${action}-${symbol}`).click(); await waitForModalReady(); }; + const close = async () => { await page.getByTestId('modal-close').click(); await expect(page.getByTestId('modal')).not.toBeVisible(); diff --git a/e2e/components/rollover.ts b/e2e/components/rollover.ts new file mode 100644 index 000000000..e4c760992 --- /dev/null +++ b/e2e/components/rollover.ts @@ -0,0 +1,117 @@ +import { type Page, expect } from '@playwright/test'; + +import type { ERC20TokenSymbol } from '../utils/contracts'; +import { formatMaturity } from '../utils/strings'; + +export default function (page: Page) { + const waitForModalReady = async () => { + const modal = page.getByTestId('rollover-modal'); + await expect(modal).toBeVisible(); + + await page.waitForFunction( + () => { + return ['submit', 'approve'].every((action) => { + const button = document.querySelector(`[data-testid="rollover-${action}"]`); + if (!button) return true; + return !button.classList.contains('MuiLoadingButton-loading'); + }); + }, + null, + { + timeout: 30_000, + polling: 1_000, + }, + ); + }; + + const cta = (type: 'floating' | 'fixed', symbol: ERC20TokenSymbol, maturity?: number) => { + return page.getByTestId(`${type}-rollover${maturity ? `-${maturity}` : ''}-${symbol}`); + }; + + const open = async (type: 'floating' | 'fixed', symbol: ERC20TokenSymbol, maturity?: number) => { + await cta(type, symbol, maturity).click(); + await waitForModalReady(); + }; + + const close = async () => { + await page.getByTestId('rollover-modal-close').click(); + await expect(page.getByTestId('rollover-modal')).not.toBeVisible(); + }; + + const checkOption = async ( + option: 'from' | 'to', + stats: { type: 'empty' } | { type: 'floating' } | { type: 'fixed'; maturity: number }, + ) => { + const button = page.getByTestId(`rollover-${option}`); + const label = page.getByTestId(`rollover-${option}-label`); + switch (stats.type) { + case 'empty': + await expect(button).toHaveText(option === 'from' ? 'Current debt' : 'New debt'); + await expect(label).toHaveText('Maturity'); + break; + case 'floating': + await expect(button).toHaveText('Variable'); + await expect(label).toHaveText('Open-ended'); + break; + case 'fixed': + await expect(button).toHaveText('Fixed'); + await expect(label).toHaveText(formatMaturity(stats.maturity)); + break; + } + }; + + const openSheet = async (option: 'from' | 'to') => { + await page.getByTestId(`rollover-${option}`).click(); + await expect(page.getByTestId(`rollover-sheet-${option}`)).toBeVisible(); + }; + + const selectDebt = async (symbol: ERC20TokenSymbol, maturity?: number) => { + const row = page.getByTestId(`rollover-sheet-row-${symbol}${maturity ? `-${maturity}` : ''}`); + await expect(row).toBeVisible(); + await row.click(); + }; + + const submit = async () => { + const button = page.getByTestId('rollover-submit'); + await expect(button).toBeVisible(); + await expect(button).not.toBeDisabled(); + + await button.click(); + }; + + const waitForTransaction = async () => { + const status = page.getByTestId('transaction-status'); + + await expect(status).toBeVisible({ timeout: 30_000 }); + + await page.waitForFunction( + (message) => { + const text = document.querySelector('[data-testid="transaction-status"]'); + if (!text) return false; + return text.textContent !== message; + }, + 'Processing transaction...', + { timeout: 30_000, polling: 1_000 }, + ); + }; + + const checkTransactionStatus = async (target: 'success' | 'error', summary: string) => { + const status = page.getByTestId('transaction-status'); + + await expect(status).toHaveText(`Transaction ${target === 'success' ? 'completed' : target}`); + await expect(page.getByTestId('transaction-summary')).toHaveText(summary); + }; + + return { + waitForModalReady, + cta, + open, + close, + checkOption, + openSheet, + selectDebt, + submit, + waitForTransaction, + checkTransactionStatus, + }; +} diff --git a/e2e/page/dashboard.ts b/e2e/page/dashboard.ts index de32f7003..56df1302c 100644 --- a/e2e/page/dashboard.ts +++ b/e2e/page/dashboard.ts @@ -56,6 +56,11 @@ export default function (page: Page) { await t.click(); }; + const checkFloatingTableRow = async (type: 'deposit' | 'borrow', symbol: ERC20TokenSymbol) => { + const row = page.getByTestId(`dashboard-floating-${type}-row-${symbol}`); + await expect(row).toBeVisible(); + }; + const checkFixedTableRow = async (type: 'deposit' | 'borrow', symbol: ERC20TokenSymbol, maturity: number) => { const row = page.getByTestId(`dashboard-fixed-${type}-row-${maturity}-${symbol}`); await expect(row).toBeVisible(); @@ -77,6 +82,7 @@ export default function (page: Page) { attemptEnterMarket, attemptExitMarket, switchTab, + checkFloatingTableRow, checkFixedTableRow, waitForTransaction, }; diff --git a/e2e/specs/4-fixed-borrow-repay/op.spec.ts b/e2e/specs/4-fixed-borrow-repay/op.spec.ts index 6f6b290d7..a711260ae 100644 --- a/e2e/specs/4-fixed-borrow-repay/op.spec.ts +++ b/e2e/specs/4-fixed-borrow-repay/op.spec.ts @@ -54,5 +54,5 @@ test('OP fixed borrow/repay', async ({ page, web3, setup }) => { maturity: pool, }); - await balance.check({ address: web3.account.address, symbol: 'OP', amount: '52', delta: 0.005 }); + await balance.check({ address: web3.account.address, symbol: 'OP', amount: '52', delta: '0.005' }); }); diff --git a/e2e/specs/6-rollover/usdc.spec.ts b/e2e/specs/6-rollover/usdc.spec.ts new file mode 100644 index 000000000..f105450e4 --- /dev/null +++ b/e2e/specs/6-rollover/usdc.spec.ts @@ -0,0 +1,131 @@ +import { expect } from '@playwright/test'; + +import base from '../../fixture/base'; +import _balance from '../../common/balance'; +import _dashboard from '../../page/dashboard'; +import _app from '../../common/app'; +import _rollover from '../../components/rollover'; +import { getFixedPools } from '../../utils/pools'; + +const test = base(); + +test.describe.configure({ mode: 'serial' }); + +// FIXME: Skipping until new contract is deployed +test.skip(); + +test('USDC rollover', async ({ page, web3, setup }) => { + const pools = getFixedPools(); + if (pools.length < 4) throw new Error('Not enough pools'); + + await web3.fork.setBalance(web3.account.address, { + ETH: 1, + USDC: 50_000, + }); + + await page.goto('/dashboard'); + + const app = _app({ test, page }); + const balance = _balance({ test, page, publicClient: web3.publicClient }); + const dashboard = _dashboard(page); + const rollover = _rollover(page); + + await setup.enterMarket('USDC'); + await setup.deposit({ symbol: 'USDC', amount: '10000', receiver: web3.account.address }); + await setup.borrow({ symbol: 'USDC', amount: '5000', receiver: web3.account.address }); + + await app.reload(); + + await test.step('Roll floating debt to fixed', async () => { + await dashboard.switchTab('borrow'); + await dashboard.checkFloatingTableRow('borrow', 'USDC'); + + await expect(rollover.cta('floating', 'OP')).toBeDisabled(); + await expect(rollover.cta('floating', 'USDC')).not.toBeDisabled(); + + await rollover.open('floating', 'USDC'); + + await rollover.checkOption('from', { type: 'floating' }); + await rollover.checkOption('to', { type: 'empty' }); + + await rollover.openSheet('to'); + await rollover.selectDebt('USDC', pools[0]); + + await rollover.checkOption('to', { type: 'fixed', maturity: pools[0] }); + + await rollover.waitForModalReady(); + + await rollover.submit(); + + await rollover.waitForTransaction(); + await rollover.checkTransactionStatus('success', 'Your position has been refinanced'); + + await rollover.close(); + + await balance.check({ address: web3.account.address, symbol: 'USDC', amount: '45000' }); + + await dashboard.checkFixedTableRow('borrow', 'USDC', pools[0]); + }); + + await test.step('Roll fixed debt to another fixed', async () => { + await dashboard.switchTab('borrow'); + + await expect(rollover.cta('floating', 'USDC')).toBeDisabled(); + await expect(rollover.cta('fixed', 'USDC', pools[0])).not.toBeDisabled(); + + await rollover.open('fixed', 'USDC', pools[0]); + + await rollover.checkOption('from', { type: 'fixed', maturity: pools[0] }); + await rollover.checkOption('to', { type: 'empty' }); + + await rollover.openSheet('to'); + await rollover.selectDebt('USDC', pools[1]); + + await rollover.checkOption('to', { type: 'fixed', maturity: pools[1] }); + + await rollover.waitForModalReady(); + + await rollover.submit(); + + await rollover.waitForTransaction(); + await rollover.checkTransactionStatus('success', 'Your position has been refinanced'); + + await rollover.close(); + + await balance.check({ address: web3.account.address, symbol: 'USDC', amount: '45000' }); + + await dashboard.checkFixedTableRow('borrow', 'USDC', pools[1]); + }); + + await test.step('Roll fixed debt to floating', async () => { + await dashboard.switchTab('borrow'); + + await expect(rollover.cta('floating', 'USDC')).toBeDisabled(); + await expect(rollover.cta('fixed', 'USDC', pools[1])).not.toBeDisabled(); + + await rollover.open('fixed', 'USDC', pools[1]); + + await rollover.checkOption('from', { type: 'fixed', maturity: pools[1] }); + await rollover.checkOption('to', { type: 'empty' }); + + await rollover.openSheet('to'); + await rollover.selectDebt('USDC'); + + await rollover.checkOption('to', { type: 'floating' }); + + await rollover.waitForModalReady(); + + await rollover.submit(); + + await rollover.waitForTransaction(); + await rollover.checkTransactionStatus('success', 'Your position has been refinanced'); + + await rollover.close(); + + await balance.check({ address: web3.account.address, symbol: 'USDC', amount: '45000' }); + + await dashboard.checkFloatingTableRow('borrow', 'USDC'); + }); + + await page.waitForTimeout(600_000); +}); diff --git a/e2e/utils/contracts.ts b/e2e/utils/contracts.ts index 195f6e267..5ad38a1d0 100644 --- a/e2e/utils/contracts.ts +++ b/e2e/utils/contracts.ts @@ -1,4 +1,4 @@ -import { getContract, isAddress } from 'viem'; +import { getContract, isAddress, type PublicClient, type WalletClient } from 'viem'; import auditorContract from '@exactly/protocol/deployments/optimism/Auditor.json' assert { type: 'json' }; import marketETHRouter from '@exactly/protocol/deployments/optimism/MarketETHRouter.json' assert { type: 'json' }; @@ -9,7 +9,10 @@ const ERC20TokenSymbols = ['WETH', 'USDC', 'OP'] as const; export type ERC20TokenSymbol = (typeof ERC20TokenSymbols)[number]; export type Coin = ERC20TokenSymbol | 'ETH'; -type Clients = Pick[0], 'walletClient' | 'publicClient'>; +type Clients = { + publicClient?: PublicClient; + walletClient?: WalletClient; +}; export const erc20 = async (symbol: ERC20TokenSymbol, clients: Clients = {}): Promise => { const { diff --git a/hooks/useWeb3.ts b/hooks/useWeb3.ts index f04de3caa..6e6a476c1 100644 --- a/hooks/useWeb3.ts +++ b/hooks/useWeb3.ts @@ -44,7 +44,7 @@ export const useWeb3 = (): Web3 => { const connectWallet = useCallback(() => { if (isE2E) { - const injected = connectors.find(({ id, ready, name }) => ready && id === 'injected' && name === 'E2E'); + const injected = connectors.find(({ id, ready, name }) => ready && id === 'mock' && name === 'Mock'); connect({ connector: injected, chainId: defaultChain.id }); } else { open({ route: 'ConnectWallet' });