diff --git a/__tests__/components/streams/qr-share-dialog.test.tsx b/__tests__/components/streams/qr-share-dialog.test.tsx new file mode 100644 index 0000000..efe9e6f --- /dev/null +++ b/__tests__/components/streams/qr-share-dialog.test.tsx @@ -0,0 +1,107 @@ +import { describe, it, expect, vi, beforeEach } from 'vitest' +import { fireEvent, render, screen } from '@testing-library/react' +import { QrShareDialog } from '@/components/streams/qr-share-dialog' +import type { StreamData } from '@/types/stream' + +const mockToastSuccess = vi.fn() +const mockWriteText = vi.fn() + +vi.mock('sonner', () => ({ + toast: { + success: mockToastSuccess, + }, +})) + +vi.mock('qrcode.react', () => ({ + QRCodeSVG: ({ value, ...props }: any) => ( + + ), + QRCodeCanvas: ({ value, ...props }: any) => ( + + ), +})) + +const STREAM_URL = 'https://flowstar.app/stream/stream-123' + +function makeStream(overrides?: Partial): StreamData { + return { + id: 'stream-123', + sender: 'GABC1111111111111111111111111111111111111111111111111111', + recipient: 'GDEF2222222222222222222222222222222222222222222222222222', + token: { address: 'CCUSDC', symbol: 'USDC', decimals: 7 }, + depositedAmount: 100_000_000n, + withdrawnAmount: 0n, + startTime: 1_700_000_000n, + endTime: 1_700_500_000n, + cliffTime: 1_700_000_100n, + cliffAmount: 0n, + amountPerSecond: 100n, + linearAmount: 100_000_000n, + duration: 600n, + cancelled: false, + metadata: { name: 'Q2 Salary', category: 'payroll', memo: 'memo' }, + ...overrides, + } +} + +describe('QrShareDialog', () => { + beforeEach(() => { + vi.clearAllMocks() + Object.defineProperty(window.navigator, 'clipboard', { + value: { writeText: mockWriteText }, + configurable: true, + }) + }) + + it('renders a QR code for the given stream URL', () => { + const { container } = render( + , + ) + + expect(screen.getByText('Share stream')).toBeInTheDocument() + expect(screen.getByText(STREAM_URL)).toBeInTheDocument() + + const qrSvg = container.querySelector('[data-testid="qr-svg"]') + expect(qrSvg).toBeInTheDocument() + expect(qrSvg).toHaveAttribute('data-value', STREAM_URL) + expect(container.querySelector('[data-testid="qr-canvas"]')).toHaveAttribute( + 'data-value', + STREAM_URL, + ) + }) + + it('copies the stream URL and downloads the QR image when actions are triggered', () => { + const clickSpy = vi.spyOn(HTMLAnchorElement.prototype, 'click').mockImplementation(() => {}) + const toDataURLSpy = vi.spyOn(HTMLCanvasElement.prototype, 'toDataURL').mockImplementation(() => { + return 'data:image/png;base64,abc123' + }) + + render( + , + ) + + fireEvent.click(screen.getByRole('button', { name: /copy stream link/i })) + expect(mockWriteText).toHaveBeenCalledWith(STREAM_URL) + expect(mockToastSuccess).toHaveBeenCalledWith('Link copied to clipboard') + + fireEvent.click(screen.getByRole('button', { name: /download qr/i })) + expect(toDataURLSpy).toHaveBeenCalledWith('image/png') + expect(clickSpy).toHaveBeenCalledTimes(1) + expect(mockToastSuccess).toHaveBeenCalledWith('QR code downloaded') + + clickSpy.mockRestore() + toDataURLSpy.mockRestore() + }) +}) diff --git a/__tests__/components/streams/stream-gantt-view.test.tsx b/__tests__/components/streams/stream-gantt-view.test.tsx new file mode 100644 index 0000000..99ac584 --- /dev/null +++ b/__tests__/components/streams/stream-gantt-view.test.tsx @@ -0,0 +1,73 @@ +import { describe, it, expect, vi } from 'vitest' +import { render, screen } from '@testing-library/react' +import { StreamGanttView } from '@/components/streams/stream-gantt-view' +import type { StreamData } from '@/types/stream' + +vi.mock('@/hooks/use-wallet', () => ({ + useWallet: vi.fn(() => ({ address: 'GOUTGOING1111111111111111111111111111111111111111111' })), +})) + +const TOKEN = { address: 'CCUSDC', symbol: 'USDC', decimals: 7 } + +function makeStream(overrides?: Partial): StreamData { + return { + id: 'stream-1', + sender: 'GOUTGOING1111111111111111111111111111111111111111111111', + recipient: 'GRECEIVING2222222222222222222222222222222222222222222222', + token: TOKEN, + depositedAmount: 10_000_000n, + withdrawnAmount: 0n, + startTime: 1_000n, + endTime: 6_000n, + cliffTime: 1_500n, + cliffAmount: 0n, + amountPerSecond: 2_000n, + linearAmount: 10_000_000n, + duration: 5_000n, + cancelled: false, + metadata: { name: 'Alpha', category: 'payroll', memo: 'memo' }, + ...overrides, + } +} + +describe('StreamGanttView', () => { + it('renders one bar per stream and positions each bar based on its start and end times', () => { + const streams = [ + makeStream({ id: 'stream-1', startTime: 1_000n, endTime: 3_000n, cliffTime: 1_500n }), + makeStream({ + id: 'stream-2', + sender: 'GOTHER1111111111111111111111111111111111111111111111', + recipient: 'GOUTGOING1111111111111111111111111111111111111111111111', + startTime: 2_000n, + endTime: 8_000n, + cliffTime: 3_000n, + }), + ] + + const nowSeconds = 4_000 + const { container } = render() + + const bars = container.querySelectorAll('.group\\/bar') + expect(bars).toHaveLength(2) + + const rawMin = Math.min(...streams.map((s) => Number(s.startTime)), nowSeconds) + const rawMax = Math.max(...streams.map((s) => Number(s.endTime)), nowSeconds) + const pad = Math.max((rawMax - rawMin) * 0.05, 3600) + const rangeStart = rawMin - pad + const rangeEnd = rawMax + pad + const rangeSpan = Math.max(rangeEnd - rangeStart, 1) + const pct = (value: number) => ((value - rangeStart) / rangeSpan) * 100 + + const [first, second] = Array.from(bars) + + expect(first).toHaveStyle({ left: `${pct(Number(streams[0].startTime))}%` }) + expect(first).toHaveStyle({ width: `${pct(Number(streams[0].endTime)) - pct(Number(streams[0].startTime))}%` }) + + expect(second).toHaveStyle({ left: `${pct(Number(streams[1].startTime))}%` }) + expect(second).toHaveStyle({ width: `${pct(Number(streams[1].endTime)) - pct(Number(streams[1].startTime))}%` }) + + expect(screen.getByText('Today')).toBeInTheDocument() + expect(screen.getByText('Alpha')).toBeInTheDocument() + expect(screen.getByText('Streaming')).toBeInTheDocument() + }) +})