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()
+ })
+})