@@ -340,22 +340,33 @@ describe("ConfirmModal withdraw variant", () => {
340340 } ) ;
341341} ) ;
342342
343- describe ( "ConfirmModal protocol fee display" , ( ) => {
344- it ( "matches calculateProtocolFee output for the same market, action, and amount" , async ( ) => {
343+ function formatModalAmount ( amount : number , asset : string ) : string {
344+ return `${ amount . toLocaleString ( undefined , {
345+ minimumFractionDigits : 2 ,
346+ maximumFractionDigits : 4 ,
347+ } ) } ${ asset } `;
348+ }
349+
350+ describe ( "ConfirmModal protocol fee breakdown" , ( ) => {
351+ it ( "shows gross, fee, and net matching calculateProtocolFee for lend/borrow/repay" , async ( ) => {
345352 const { calculateProtocolFee } = await import ( "@/lib/fee-calculator" ) ;
346- const user = userEvent . setup ( ) ;
347- const testCases : Array < { asset : string ; type : "lend" | "borrow" | "repay" ; amount : number } > = [
353+ const testCases : Array < {
354+ asset : string ;
355+ type : "lend" | "borrow" | "repay" ;
356+ amount : number ;
357+ } > = [
348358 { asset : "XLM" , type : "lend" , amount : 1000 } ,
349359 { asset : "USDC" , type : "borrow" , amount : 500 } ,
350360 { asset : "XLM" , type : "repay" , amount : 200 } ,
351361 ] ;
352362
353363 for ( const testCase of testCases ) {
354- const expectedFeeResult = calculateProtocolFee ( testCase . asset , testCase . type , testCase . amount ) ;
355- const formattedFee = `${ expectedFeeResult . feeAmount . toLocaleString ( undefined , {
356- minimumFractionDigits : 2 ,
357- maximumFractionDigits : 4 ,
358- } ) } ${ testCase . asset } `;
364+ const expectedFeeResult = calculateProtocolFee (
365+ testCase . asset ,
366+ testCase . type ,
367+ testCase . amount ,
368+ ) ;
369+ const net = Math . max ( 0 , testCase . amount - expectedFeeResult . feeAmount ) ;
359370
360371 const { unmount } = render (
361372 < ConfirmModal
@@ -373,11 +384,125 @@ describe("ConfirmModal protocol fee display", () => {
373384 ) ;
374385
375386 const dialog = screen . getByRole ( "dialog" ) ;
376- expect ( within ( dialog ) . getByText ( "Protocol Fee" ) ) . toBeInTheDocument ( ) ;
377- expect ( within ( dialog ) . getByText ( formattedFee ) ) . toBeInTheDocument ( ) ;
387+ const breakdown = within ( dialog ) . getByTestId ( "fee-breakdown" ) ;
388+ expect ( breakdown ) . toHaveAttribute ( "aria-label" , "Fee breakdown" ) ;
389+
390+ expect ( within ( breakdown ) . getByText ( "Gross Amount" ) ) . toBeInTheDocument ( ) ;
391+ expect (
392+ within ( breakdown ) . getByText (
393+ formatModalAmount ( testCase . amount , testCase . asset ) ,
394+ ) ,
395+ ) . toBeInTheDocument ( ) ;
396+
397+ expect ( within ( breakdown ) . getByText ( / P r o t o c o l F e e / ) ) . toBeInTheDocument ( ) ;
398+ expect (
399+ within ( breakdown ) . getByText (
400+ formatModalAmount ( expectedFeeResult . feeAmount , testCase . asset ) ,
401+ ) ,
402+ ) . toBeInTheDocument ( ) ;
403+
404+ expect ( within ( breakdown ) . getByText ( "Net Amount" ) ) . toBeInTheDocument ( ) ;
405+ expect (
406+ within ( breakdown ) . getByText ( formatModalAmount ( net , testCase . asset ) ) ,
407+ ) . toBeInTheDocument ( ) ;
378408
379409 unmount ( ) ;
380410 }
381411 } ) ;
412+
413+ it ( "recomputes the breakdown when the amount changes" , async ( ) => {
414+ const { calculateProtocolFee } = await import ( "@/lib/fee-calculator" ) ;
415+ const baseProps = {
416+ isOpen : true ,
417+ onClose : vi . fn ( ) ,
418+ onConfirm : vi . fn ( ) ,
419+ calculation : { dailyEarnings : 0.1 , totalEarnings : 10 } ,
420+ type : "lend" as const ,
421+ } ;
422+
423+ const { rerender } = render (
424+ < ConfirmModal
425+ { ...baseProps }
426+ data = { { asset : "XLM" , amount : 1000 , interestRate : 5 } }
427+ /> ,
428+ ) ;
429+
430+ const fee1000 = calculateProtocolFee ( "XLM" , "lend" , 1000 ) ;
431+ expect (
432+ screen . getByText ( formatModalAmount ( fee1000 . feeAmount , "XLM" ) ) ,
433+ ) . toBeInTheDocument ( ) ;
434+
435+ rerender (
436+ < ConfirmModal
437+ { ...baseProps }
438+ data = { { asset : "XLM" , amount : 250 , interestRate : 5 } }
439+ /> ,
440+ ) ;
441+
442+ const fee250 = calculateProtocolFee ( "XLM" , "lend" , 250 ) ;
443+ const breakdown = screen . getByTestId ( "fee-breakdown" ) ;
444+ expect (
445+ within ( breakdown ) . getByText ( formatModalAmount ( fee250 . feeAmount , "XLM" ) ) ,
446+ ) . toBeInTheDocument ( ) ;
447+ expect (
448+ within ( breakdown ) . getByText ( formatModalAmount ( 250 , "XLM" ) ) ,
449+ ) . toBeInTheDocument ( ) ;
450+ expect (
451+ within ( breakdown ) . getByText (
452+ formatModalAmount ( Math . max ( 0 , 250 - fee250 . feeAmount ) , "XLM" ) ,
453+ ) ,
454+ ) . toBeInTheDocument ( ) ;
455+ } ) ;
456+
457+ it ( "shows a zero fee and zero net for a zero amount" , ( ) => {
458+ render (
459+ < ConfirmModal
460+ isOpen = { true }
461+ onClose = { vi . fn ( ) }
462+ onConfirm = { vi . fn ( ) }
463+ data = { { asset : "XLM" , amount : 0 , interestRate : 5 } }
464+ calculation = { null }
465+ type = "lend"
466+ /> ,
467+ ) ;
468+
469+ const breakdown = screen . getByTestId ( "fee-breakdown" ) ;
470+ // Gross, fee, and net all format as 0.00 XLM — three rows.
471+ const zeros = within ( breakdown ) . getAllByText ( formatModalAmount ( 0 , "XLM" ) ) ;
472+ expect ( zeros . length ) . toBeGreaterThanOrEqual ( 3 ) ;
473+ } ) ;
474+
475+ it ( "omits the fee breakdown for withdraw (no fee schedule)" , ( ) => {
476+ render (
477+ < ConfirmModal
478+ isOpen = { true }
479+ onClose = { vi . fn ( ) }
480+ onConfirm = { vi . fn ( ) }
481+ data = { { asset : "XLM" , amount : 100 , interestRate : 0 } }
482+ calculation = { null }
483+ type = "withdraw"
484+ /> ,
485+ ) ;
486+
487+ expect ( screen . queryByTestId ( "fee-breakdown" ) ) . not . toBeInTheDocument ( ) ;
488+ } ) ;
489+
490+ it ( "omits the fee breakdown for an unknown market instead of blocking confirm" , ( ) => {
491+ render (
492+ < ConfirmModal
493+ isOpen = { true }
494+ onClose = { vi . fn ( ) }
495+ onConfirm = { vi . fn ( ) }
496+ data = { { asset : "UNKNOWN" , amount : 100 , interestRate : 5 } }
497+ calculation = { null }
498+ type = "lend"
499+ /> ,
500+ ) ;
501+
502+ expect ( screen . queryByTestId ( "fee-breakdown" ) ) . not . toBeInTheDocument ( ) ;
503+ expect (
504+ screen . getByRole ( "button" , { name : / c o n f i r m / i } ) ,
505+ ) . toBeInTheDocument ( ) ;
506+ } ) ;
382507} ) ;
383508
0 commit comments