Skip to content

Commit

Permalink
feat: add additional Earn components (#1883)
Browse files Browse the repository at this point in the history
Co-authored-by: Alissa Crane <[email protected]>
  • Loading branch information
abcrane123 and alissacrane-cb authored Jan 28, 2025
1 parent 74f27a8 commit 8cd8eb2
Show file tree
Hide file tree
Showing 25 changed files with 1,010 additions and 3 deletions.
2 changes: 2 additions & 0 deletions src/earn/components/DepositBalance.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,8 @@ const baseContext = {
depositedAmount: '0',
withdrawAmount: '0',
setWithdrawAmount: vi.fn(),
depositCalls: [],
withdrawCalls: [],
};

describe('DepositBalance', () => {
Expand Down
123 changes: 123 additions & 0 deletions src/earn/components/DepositButton.test.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,123 @@
import type { Call } from '@/transaction/types';
import { render, screen } from '@testing-library/react';
import type { Address } from 'viem';
import { describe, expect, it, vi } from 'vitest';
import { DepositButton } from './DepositButton';
import { useEarnContext } from './EarnProvider';

vi.mock('./EarnProvider', () => ({
useEarnContext: vi.fn(),
}));

vi.mock('wagmi', () => ({
useAccount: vi.fn(),
useConfig: vi.fn(),
useCapabilities: vi.fn(),
}));

const baseContext = {
convertedBalance: '1000',
setDepositAmount: vi.fn(),
vaultAddress: '0x123' as Address,
depositAmount: '0',
depositedAmount: '0',
withdrawAmount: '0',
setWithdrawAmount: vi.fn(),
depositCalls: [],
withdrawCalls: [],
};

vi.mock('@/transaction', async (importOriginal) => {
return {
...(await importOriginal<typeof import('@/transaction')>()),
TransactionLifecycleStatus: vi.fn(),
TransactionButton: ({
text,
disabled,
}: { text: string; disabled: boolean }) => (
<button type="button" disabled={disabled} data-testid="transactionButton">
{text}
</button>
),
Transaction: ({
onStatus,
children,
capabilities,
}: {
onStatus: (status: { statusName: string }) => void;
children: React.ReactNode;
capabilities: { paymasterService: { url: string } };
}) => (
<>
<div data-testid="transaction">
<button
type="button"
data-testid="transaction-button"
onClick={() => onStatus({ statusName: 'transactionPending' })}
>
TransactionPending
</button>
<button
type="button"
data-testid="transaction-button"
onClick={() =>
onStatus({ statusName: 'transactionLegacyExecuted' })
}
>
TransactionLegacyExecuted
</button>
<button
type="button"
data-testid="transaction-button"
onClick={() => onStatus({ statusName: 'success' })}
>
Success
</button>
<button
type="button"
data-testid="transaction-button"
onClick={() => onStatus({ statusName: 'error' })}
>
Error
</button>
<div>{capabilities?.paymasterService?.url}</div>
</div>
{children}
</>
),
TransactionSponsor: vi.fn(),
TransactionStatus: vi.fn(),
TransactionStatusAction: vi.fn(),
TransactionStatusLabel: vi.fn(),
};
});

vi.mock('@/internal/hooks/useTheme', () => ({
useTheme: vi.fn(),
}));

describe('DepositButton Component', () => {
it('renders Transaction with depositCalls from EarnProvider', () => {
const mockDepositCalls = [{ to: '0x123', data: '0x456' }] as Call[];
vi.mocked(useEarnContext).mockReturnValue({
...baseContext,
depositCalls: mockDepositCalls,
});

render(<DepositButton />);

const transactionElement = screen.getByTestId('transaction');
expect(transactionElement).toBeInTheDocument();
});

it('renders TransactionButton with the correct text', () => {
vi.mocked(useEarnContext).mockReturnValue({
...baseContext,
depositCalls: [],
});

const { container } = render(<DepositButton />);

expect(container).toHaveTextContent('Deposit');
});
});
13 changes: 13 additions & 0 deletions src/earn/components/DepositButton.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
import { Transaction, TransactionButton } from '@/transaction';
import type { DepositButtonReact } from '../types';
import { useEarnContext } from './EarnProvider';

export function DepositButton({ className }: DepositButtonReact) {
const { depositCalls } = useEarnContext();

return (
<Transaction className={className} calls={depositCalls}>
<TransactionButton text="Deposit" />
</Transaction>
);
}
60 changes: 60 additions & 0 deletions src/earn/components/DepositDetails.test.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,60 @@
import { usdcToken } from '@/token/constants';
import { render, screen } from '@testing-library/react';
import type { Address } from 'viem';
import { type Mock, describe, expect, it, vi } from 'vitest';
import { DepositDetails } from './DepositDetails';
import { useEarnContext } from './EarnProvider';

vi.mock('./EarnProvider', () => ({
useEarnContext: vi.fn(),
}));

vi.mock('@/internal/hooks/useTheme', () => ({
useTheme: vi.fn(),
}));

const baseContext = {
convertedBalance: '1000',
setDepositAmount: vi.fn(),
vaultAddress: '0x123' as Address,
depositAmount: '0',
depositedAmount: '0',
withdrawAmount: '0',
setWithdrawAmount: vi.fn(),
depositCalls: [],
withdrawCalls: [],
};

describe('DepositDetails Component', () => {
it('renders EarnDetails with default APY tag when APY is provided', () => {
const mockApy = '5.00%';
vi.mocked(useEarnContext).mockReturnValue({ ...baseContext, apy: mockApy });

const { container } = render(<DepositDetails />);

const tokenElement = screen.getByTestId('ockTokenChip_Button');
expect(tokenElement).toHaveTextContent(usdcToken.name);

// const tagElement = screen.getByTestId('tag');
expect(container).toHaveTextContent(`APY ${mockApy}`);
});

it('renders EarnDetails with an empty tag when APY is not provided', () => {
vi.mocked(useEarnContext).mockReturnValue({ ...baseContext, apy: '' });

render(<DepositDetails />);

const tokenElement = screen.getByTestId('ockTokenChip_Button');
expect(tokenElement).toHaveTextContent(usdcToken.name);
});

it('applies custom className to the EarnDetails container', () => {
const customClass = 'custom-class';
(useEarnContext as Mock).mockReturnValue({ apy: null });

render(<DepositDetails className={customClass} />);

const earnDetails = screen.getByTestId('ockEarnDetails');
expect(earnDetails).toHaveClass(customClass);
});
});
26 changes: 26 additions & 0 deletions src/earn/components/DepositDetails.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
import { usdcToken } from '@/token/constants';
import { useMemo } from 'react';
import type { DepositDetailsReact } from '../types';
import { EarnDetails } from './EarnDetails';
import { useEarnContext } from './EarnProvider';

export function DepositDetails({ className }: DepositDetailsReact) {
const { apy } = useEarnContext();

const tag = useMemo(() => {
if (apy) {
return `APY ${apy}`;
}
return '';
}, [apy]);

// TODO: update token when we have logic to fetch vault info
return (
<EarnDetails
className={className}
token={usdcToken}
tag={tag}
tagVariant="default"
/>
);
}
72 changes: 72 additions & 0 deletions src/earn/components/Earn.test.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,72 @@
import type { Call } from '@/transaction/types';
import { useGetTokenBalance } from '@/wallet/hooks/useGetTokenBalance';
import { render, screen } from '@testing-library/react';
import { type Mock, beforeEach, describe, expect, it, vi } from 'vitest';
import { useAccount, useConfig } from 'wagmi';
import { Earn } from './Earn';

vi.mock('wagmi', () => ({
useAccount: vi.fn(),
useConfig: vi.fn(),
useCapabilities: vi.fn(),
}));

vi.mock('@/transaction', () => ({
Transaction: ({
className,
calls,
children,
}: { className: string; calls: Call[]; children: React.ReactNode }) => (
<div
data-testid="transaction"
className={className}
data-calls={JSON.stringify(calls)}
>
{children}
</div>
),
TransactionButton: ({ text }: { text: string }) => (
<button data-testid="transaction-button" type="button">
{text}
</button>
),
}));

vi.mock('@/wallet/hooks/useGetTokenBalance', () => ({
useGetTokenBalance: vi.fn(),
}));

vi.mock('@/internal/hooks/useTheme', () => ({
useTheme: vi.fn(),
}));

describe('Earn Component', () => {
beforeEach(() => {
(useAccount as Mock).mockReturnValue({
address: '0x123',
});
(useGetTokenBalance as Mock).mockReturnValue({
convertedBalance: '0.0',
error: null,
});
(useConfig as Mock).mockReturnValue({});
});

it('renders custom children when provided', () => {
const customChildren = <p>Custom Children</p>;
render(<Earn vaultAddress="0x123">{customChildren}</Earn>);

expect(screen.getByText('Custom Children')).toBeInTheDocument();
expect(screen.queryByTestId('tabs')).not.toBeInTheDocument();
});

it('renders default tabs and their contents when children are not provided', () => {
const { container } = render(<Earn vaultAddress="0x123" />);

const tabs = screen.getByTestId('ockTabs');
expect(tabs).toBeInTheDocument();

expect(container).toHaveTextContent('Deposit');
expect(container).toHaveTextContent('Withdraw');
});
});
56 changes: 56 additions & 0 deletions src/earn/components/Earn.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,56 @@
import { Tab, TabContent, Tabs, TabsList } from '@/internal';
import { border, cn } from '@/styles/theme';
import type { EarnReact } from '../types';
import { EarnDeposit } from './EarnDeposit';
import { EarnProvider } from './EarnProvider';
import { EarnWithdraw } from './EarnWithdraw';

export function Earn({
children = <EarnDefaultContent />,
className,
vaultAddress,
}: EarnReact) {
return (
<EarnProvider vaultAddress={vaultAddress}>
<div
className={cn(
'flex w-[375px] flex-col overflow-hidden',
border.radius,
border.lineDefault,
className,
)}
>
{children}
</div>
</EarnProvider>
);
}

function EarnDefaultContent() {
return (
<Tabs defaultValue="deposit">
<TabsList>
<Tab value="deposit">Deposit</Tab>
<Tab value="withdraw">Withdraw</Tab>
</TabsList>
<TabContent
value="deposit"
className={cn(
border.lineDefault,
'!border-l-0 !border-b-0 !border-r-0',
)}
>
<EarnDeposit />
</TabContent>
<TabContent
value="withdraw"
className={cn(
border.lineDefault,
'!border-l-0 !border-b-0 !border-r-0',
)}
>
<EarnWithdraw />
</TabContent>
</Tabs>
);
}
5 changes: 4 additions & 1 deletion src/earn/components/EarnAmountInput.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,10 @@ export function EarnAmountInput({
'aria-label': ariaLabel,
}: EarnAmountInputReact) {
return (
<div className={cn('flex flex-col', className)}>
<div
data-testid="ockEarnAmountInput"
className={cn('flex flex-col', className)}
>
<TextInput
className={cn(
'w-full border-none bg-transparent font-display text-5xl',
Expand Down
Loading

0 comments on commit 8cd8eb2

Please sign in to comment.