1+
2+ import React , { useState } from 'react'
3+ import { describe , it , expect , vi , beforeEach } from 'vitest'
4+ import { renderWithProviders , screen , waitFor } from '@/testing'
5+ import { createMockUser , asyncMock , asyncErrorMock } from '@/testing/utils/mocks'
6+
7+ // ── Service mock ──────────────────────────────────────────────────────────
8+ const mockSendTip = vi . fn ( )
9+
10+ vi . mock ( '@/services/tipService' , ( ) => ( {
11+ sendTip : ( ...args : unknown [ ] ) => mockSendTip ( ...args ) ,
12+ } ) )
13+
14+ // ── Inline minimal TipForm (replace with real import) ─────────────────────
15+ interface TipFormProps {
16+ recipient : ReturnType < typeof createMockUser >
17+ }
18+
19+ function TipForm ( { recipient } : TipFormProps ) {
20+ const [ amount , setAmount ] = useState ( '' )
21+ const [ loading , setLoading ] = useState ( false )
22+ const [ error , setError ] = useState < string | null > ( null )
23+ const [ success , setSuccess ] = useState ( false )
24+
25+ async function handleSubmit ( e : React . FormEvent ) {
26+ e . preventDefault ( )
27+ setError ( null )
28+
29+ if ( ! amount || isNaN ( Number ( amount ) ) || Number ( amount ) <= 0 ) {
30+ setError ( 'Please enter a valid tip amount.' )
31+ return
32+ }
33+
34+ setLoading ( true )
35+ try {
36+ await mockSendTip ( { recipientId : recipient . id , amount : Number ( amount ) } )
37+ setSuccess ( true )
38+ } catch ( err : unknown ) {
39+ setError ( err instanceof Error ? err . message : 'Transaction failed.' )
40+ } finally {
41+ setLoading ( false )
42+ }
43+ }
44+
45+ if ( success ) {
46+ return < p data-testid = "success-msg" > Tip sent successfully! 🎉</ p >
47+ }
48+
49+ return (
50+ < form onSubmit = { handleSubmit } data-testid = "tip-form" >
51+ < label htmlFor = "tip-amount" > Tip { recipient . name } </ label >
52+ < input
53+ id = "tip-amount"
54+ type = "number"
55+ step = "0.001"
56+ min = "0"
57+ value = { amount }
58+ onChange = { ( e ) => setAmount ( e . target . value ) }
59+ placeholder = "0.01 ETH"
60+ data-testid = "tip-amount-input"
61+ />
62+ { error && < p role = "alert" data-testid = "tip-error" > { error } </ p > }
63+ < button type = "submit" disabled = { loading } data-testid = "tip-submit" >
64+ { loading ? 'Sending…' : 'Send Tip' }
65+ </ button >
66+ </ form >
67+ )
68+ }
69+ // ─────────────────────────────────────────────────────────────────────────
70+
71+ describe ( 'TipForm' , ( ) => {
72+ const recipient = createMockUser ( { id : 'user-99' , name : 'Alice' } )
73+
74+ beforeEach ( ( ) => {
75+ vi . clearAllMocks ( )
76+ } )
77+
78+ it ( 'renders form with recipient name' , ( ) => {
79+ renderWithProviders ( < TipForm recipient = { recipient } /> )
80+ expect ( screen . getByLabelText ( / T i p A l i c e / i) ) . toBeInTheDocument ( )
81+ expect ( screen . getByTestId ( 'tip-submit' ) ) . toHaveTextContent ( 'Send Tip' )
82+ } )
83+
84+ it ( 'shows validation error for empty amount' , async ( ) => {
85+ const { user } = renderWithProviders ( < TipForm recipient = { recipient } /> )
86+ await user . click ( screen . getByTestId ( 'tip-submit' ) )
87+ expect ( screen . getByTestId ( 'tip-error' ) ) . toHaveTextContent ( / v a l i d t i p a m o u n t / i)
88+ expect ( mockSendTip ) . not . toHaveBeenCalled ( )
89+ } )
90+
91+ it ( 'shows validation error for zero amount' , async ( ) => {
92+ const { user } = renderWithProviders ( < TipForm recipient = { recipient } /> )
93+ await user . type ( screen . getByTestId ( 'tip-amount-input' ) , '0' )
94+ await user . click ( screen . getByTestId ( 'tip-submit' ) )
95+ expect ( screen . getByTestId ( 'tip-error' ) ) . toBeInTheDocument ( )
96+ } )
97+
98+ it ( 'shows loading state while sending tip' , async ( ) => {
99+ mockSendTip . mockImplementation ( ( ) => new Promise ( ( ) => { } ) ) // never resolves
100+ const { user } = renderWithProviders ( < TipForm recipient = { recipient } /> )
101+ await user . type ( screen . getByTestId ( 'tip-amount-input' ) , '0.01' )
102+ await user . click ( screen . getByTestId ( 'tip-submit' ) )
103+ expect ( screen . getByTestId ( 'tip-submit' ) ) . toBeDisabled ( )
104+ expect ( screen . getByTestId ( 'tip-submit' ) ) . toHaveTextContent ( 'Sending…' )
105+ } )
106+
107+ it ( 'shows success message after successful tip' , async ( ) => {
108+ mockSendTip . mockResolvedValueOnce ( { txHash : '0xabc' } )
109+ const { user } = renderWithProviders ( < TipForm recipient = { recipient } /> )
110+ await user . type ( screen . getByTestId ( 'tip-amount-input' ) , '0.05' )
111+ await user . click ( screen . getByTestId ( 'tip-submit' ) )
112+ await waitFor ( ( ) =>
113+ expect ( screen . getByTestId ( 'success-msg' ) ) . toBeInTheDocument ( ) ,
114+ )
115+ } )
116+
117+ it ( 'shows error message when tip transaction fails' , async ( ) => {
118+ mockSendTip . mockRejectedValueOnce ( new Error ( 'Insufficient funds' ) )
119+ const { user } = renderWithProviders ( < TipForm recipient = { recipient } /> )
120+ await user . type ( screen . getByTestId ( 'tip-amount-input' ) , '999' )
121+ await user . click ( screen . getByTestId ( 'tip-submit' ) )
122+ await waitFor ( ( ) =>
123+ expect ( screen . getByTestId ( 'tip-error' ) ) . toHaveTextContent ( / I n s u f f i c i e n t f u n d s / i) ,
124+ )
125+ } )
126+ } )
0 commit comments