Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion src/components/common/CreatorCard.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -122,7 +122,7 @@
cardElement.addEventListener('keydown', handleKeyDown);
return () => cardElement.removeEventListener('keydown', handleKeyDown);
}
}, [isConnected, isNetworkMismatch, displayCreatorName]);

Check warning on line 125 in src/components/common/CreatorCard.tsx

View workflow job for this annotation

GitHub Actions / verify

React Hook useEffect has a missing dependency: 'handleBuy'. Either include it or remove the dependency array

const runPurchaseAttempt = () => {
setTransactionState('submitting');
Expand Down Expand Up @@ -162,7 +162,7 @@
});
showToast.transactionSuccess(
'Purchase Successful!',
`You successfully bought a key for ${displayCreatorName}`,
`Bought 1 key from ${displayCreatorName}`,
'0x1234567890abcdef1234567890abcdef1234567890abcdef1234567890abcdef',
'https://stellar.expert/explorer/testnet/tx/0x1234567890abcdef1234567890abcdef1234567890abcdef1234567890abcdef'
);
Expand Down
5 changes: 3 additions & 2 deletions src/pages/LandingPage.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -720,6 +720,7 @@ function LandingPage() {

const handleConfirmTrade = async (amount: number) => {
const previousHoldings = featuredHoldings;
const creatorName = featuredCreator?.title ?? 'Unknown creator';
setTradeSubmitting(true);

try {
Expand All @@ -742,8 +743,8 @@ function LandingPage() {
showToast.transactionSuccess(
'Trade confirmed',
tradeSide === 'buy'
? `Holdings refreshed: +${formatNumber(amount)} keys.`
: `Holdings refreshed: -${formatNumber(amount)} keys.`
? `Bought ${formatNumber(amount)} key${amount === 1 ? '' : 's'} from ${creatorName}`
: `Sold ${formatNumber(amount)} key${amount === 1 ? '' : 's'} from ${creatorName}`
);
setTradeDialogOpen(false);
} catch (error) {
Expand Down
184 changes: 184 additions & 0 deletions src/pages/__tests__/LandingPage.tradeConfirmToast.integration.test.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,184 @@
import type { ComponentProps, ReactNode } from 'react';
import { cleanup, render, screen, waitFor } from '@testing-library/react';
import userEvent from '@testing-library/user-event';
import { MemoryRouter } from 'react-router';
import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest';
import LandingPage from '@/pages/LandingPage';
import { courseService } from '@/services/course.service';
import showToast from '@/utils/toast.util';

vi.mock('@/services/course.service', () => ({
courseService: { getCourses: vi.fn() },
}));

vi.mock('@/utils/toast.util', () => ({
default: {
message: vi.fn(),
success: vi.fn(),
error: vi.fn(),
loading: vi.fn(),
transactionSuccess: vi.fn(),
},
}));

vi.mock('@/hooks/useNetworkMismatch', () => ({
useNetworkMismatch: () => ({
isMismatch: false,
expectedChainName: 'Stellar Testnet',
}),
}));

vi.mock('@/hooks/useStaleData', () => ({
useStaleData: () => ({
stale: false,
ageMs: 0,
msUntilStale: 60_000,
revalidate: vi.fn(),
}),
}));

vi.mock('@/components/common/StellarConnectionQualityBadge', async () => {
const React = await import('react');

return {
default: () => React.createElement('div', { role: 'status' }, 'RPC good'),
};
});

vi.mock('@/components/common/CreatorCard', async () => {
const React = await import('react');

return {
default: ({ creator }: { creator: { title: string } }) =>
React.createElement(
'article',
{ 'aria-label': `Creator ${creator.title}` },
creator.title
),
};
});

vi.mock('framer-motion', async () => {
const React = await import('react');
type MotionDivProps = ComponentProps<'div'> & {
layout?: boolean;
transition?: unknown;
};

return {
AnimatePresence: ({ children }: { children: ReactNode }) =>
React.createElement(React.Fragment, null, children),
LayoutGroup: ({ children }: { children: ReactNode }) =>
React.createElement(React.Fragment, null, children),
motion: {
div: ({ children, ...props }: MotionDivProps) => {
const { layout, transition, ...divProps } = props;
void layout;
void transition;

return React.createElement('div', divProps, children);
},
h1: ({ children, ...props }: ComponentProps<'h1'>) =>
React.createElement('h1', props, children),
button: ({ children, ...props }: ComponentProps<'button'>) =>
React.createElement('button', props, children),
},
};
});

const mockGetCourses = vi.mocked(courseService.getCourses);
const mockShowToast = vi.mocked(showToast);

const mockMatchMedia = () => {
Object.defineProperty(window, 'matchMedia', {
writable: true,
value: vi.fn().mockImplementation((query: string) => ({
matches: false,
media: query,
onchange: null,
addEventListener: vi.fn(),
removeEventListener: vi.fn(),
addListener: vi.fn(),
removeListener: vi.fn(),
dispatchEvent: vi.fn(),
})),
});
};

describe('LandingPage trade confirmation toast (#540)', () => {
beforeEach(() => {
mockMatchMedia();
window.localStorage.clear();
window.sessionStorage.clear();
mockGetCourses.mockReset();
vi.clearAllMocks();
});

afterEach(() => {
cleanup();
});

it('shows a success toast with quantity and creator name after a confirmed buy', async () => {
mockGetCourses.mockResolvedValue([]);
const user = userEvent.setup();
render(
<MemoryRouter>
<LandingPage />
</MemoryRouter>
);

const buyButtons = await screen.findAllByRole('button', {
name: 'Buy',
hidden: true,
});
await user.click(buyButtons[0]);

const amountInput = await screen.findByTestId('trade-dialog-amount');
await user.clear(amountInput);
await user.type(amountInput, '5');

await user.click(screen.getByTestId('trade-dialog-confirm'));

await waitFor(
() =>
expect(mockShowToast.transactionSuccess).toHaveBeenCalledTimes(1),
{ timeout: 3000 }
);
expect(mockShowToast.transactionSuccess).toHaveBeenCalledWith(
'Trade confirmed',
expect.stringMatching(/^Bought 5 keys from .+$/)
);
});

it('shows a success toast with quantity and creator name after a confirmed sell', async () => {
mockGetCourses.mockResolvedValue([]);
const user = userEvent.setup();
render(
<MemoryRouter>
<LandingPage />
</MemoryRouter>
);

const sellButtons = await screen.findAllByRole('button', {
name: 'Sell',
hidden: true,
});
await user.click(sellButtons[0]);

const amountInput = await screen.findByTestId('trade-dialog-amount');
await user.clear(amountInput);
await user.type(amountInput, '1');

await user.click(screen.getByTestId('trade-dialog-confirm'));

await waitFor(
() =>
expect(mockShowToast.transactionSuccess).toHaveBeenCalledTimes(1),
{ timeout: 3000 }
);
expect(mockShowToast.transactionSuccess).toHaveBeenCalledWith(
'Trade confirmed',
expect.stringMatching(/^Sold 1 key from .+$/)
);
});
});
Loading