11import React from 'react' ;
22import { render , screen , fireEvent , waitFor } from '@testing-library/react' ;
3+ import '@testing-library/jest-dom/vitest' ;
34import { describe , it , expect , vi , beforeEach } from 'vitest' ;
45import { PortfolioChartWidget , PortfolioAsset } from './PortfolioChartWidget' ;
56
6- // Mock recharts to avoid canvas issues in tests
7+ const translations = {
8+ en : {
9+ portfolioChartWidget : {
10+ title : 'Portfolio Value' ,
11+ allocation : 'Allocation' ,
12+ trend : 'Trend' ,
13+ loading : 'Loading portfolio chart...' ,
14+ historyLoading : 'Loading performance history...' ,
15+ emptyAllocation : 'No allocation data available yet.' ,
16+ emptyAssets : 'No portfolio assets available yet.' ,
17+ emptyHistory : 'No performance history available yet.' ,
18+ } ,
19+ } ,
20+ es : {
21+ portfolioChartWidget : {
22+ title : 'Valor del portafolio' ,
23+ allocation : 'Distribucion' ,
24+ trend : 'Tendencia' ,
25+ loading : 'Cargando grafico del portafolio...' ,
26+ historyLoading : 'Cargando historial de rendimiento...' ,
27+ emptyAllocation : 'Todavia no hay datos de asignacion.' ,
28+ emptyAssets : 'Todavia no hay activos en el portafolio.' ,
29+ emptyHistory : 'Todavia no hay historial de rendimiento.' ,
30+ } ,
31+ } ,
32+ } as const ;
33+
34+ let mockLocale : keyof typeof translations = 'en' ;
35+
36+ vi . mock ( 'next-intl' , ( ) => ( {
37+ useLocale : ( ) => mockLocale ,
38+ useTranslations : ( namespace : keyof ( typeof translations ) [ 'en' ] ) => ( key : string ) =>
39+ translations [ mockLocale ] [ namespace ] [ key as keyof ( typeof translations ) [ 'en' ] [ typeof namespace ] ] ?? key ,
40+ } ) ) ;
41+
742vi . mock ( 'recharts' , ( ) => ( {
843 PieChart : ( { children } : any ) => < div data-testid = "pie-chart" > { children } </ div > ,
944 Pie : ( { children, onClick, data } : any ) => (
10- < div
11- data-testid = "pie"
12- onClick = { ( ) => onClick && onClick ( data [ 0 ] ) }
13- >
45+ < div data-testid = "pie" onClick = { ( ) => onClick && data ?. [ 0 ] && onClick ( data [ 0 ] ) } >
1446 { children }
1547 </ div >
1648 ) ,
@@ -49,6 +81,11 @@ describe('PortfolioChartWidget', () => {
4981 } ,
5082 ] ;
5183
84+ const historyData = [
85+ { timestamp : Date . UTC ( 2026 , 0 , 1 ) , value : 3200 } ,
86+ { timestamp : Date . UTC ( 2026 , 0 , 2 ) , value : 4000 } ,
87+ ] ;
88+
5289 const defaultProps = {
5390 assets : mockAssets ,
5491 totalValue : 4000 ,
@@ -58,103 +95,53 @@ describe('PortfolioChartWidget', () => {
5895
5996 beforeEach ( ( ) => {
6097 vi . clearAllMocks ( ) ;
98+ mockLocale = 'en' ;
6199 } ) ;
62100
63- it ( 'renders the component with portfolio value' , ( ) => {
101+ it ( 'renders the localized title and portfolio value' , ( ) => {
64102 render ( < PortfolioChartWidget { ...defaultProps } /> ) ;
65103
66104 expect ( screen . getByText ( 'Portfolio Value' ) ) . toBeInTheDocument ( ) ;
67105 expect ( screen . getByText ( '$4,000.00' ) ) . toBeInTheDocument ( ) ;
68106 } ) ;
69107
70- it ( 'displays the correct currency format' , ( ) => {
71- render (
72- < PortfolioChartWidget
73- { ...defaultProps }
74- totalValue = { 5000 }
75- currency = "EUR"
76- />
77- ) ;
78-
79- // The component should format currency, checking for the value in the DOM
80- const portfolioValue = screen . getByText ( / P o r t f o l i o V a l u e / i) . parentElement ;
81- expect ( portfolioValue ) . toBeInTheDocument ( ) ;
82- } ) ;
83-
84108 it ( 'renders all assets in the list' , ( ) => {
85109 render ( < PortfolioChartWidget { ...defaultProps } /> ) ;
86110
87111 expect ( screen . getByText ( 'XLM' ) ) . toBeInTheDocument ( ) ;
88112 expect ( screen . getByText ( 'USDC' ) ) . toBeInTheDocument ( ) ;
89- } ) ;
90-
91- it ( 'displays asset percentages correctly' , ( ) => {
92- render ( < PortfolioChartWidget { ...defaultProps } /> ) ;
93-
94- const percentageElements = screen . getAllByText ( / 5 0 \. 0 % / ) ;
95- expect ( percentageElements . length ) . toBeGreaterThan ( 0 ) ;
96- } ) ;
97-
98- it ( 'displays asset amounts' , ( ) => {
99- render ( < PortfolioChartWidget { ...defaultProps } /> ) ;
100-
101113 expect ( screen . getByText ( '1000.0000 XLM' ) ) . toBeInTheDocument ( ) ;
102- expect ( screen . getByText ( '500.0000 USDC' ) ) . toBeInTheDocument ( ) ;
103114 } ) ;
104115
105- it ( 'switches between chart types when buttons are clicked ' , async ( ) => {
106- render ( < PortfolioChartWidget { ...defaultProps } /> ) ;
116+ it ( 'switches to the history view when history data is available ' , async ( ) => {
117+ render ( < PortfolioChartWidget { ...defaultProps } historyData = { historyData } /> ) ;
107118
108- const trendButton = screen . getByText ( 'Trend' ) ;
109- fireEvent . click ( trendButton ) ;
119+ fireEvent . click ( screen . getByRole ( 'button' , { name : 'Trend' } ) ) ;
110120
111121 await waitFor ( ( ) => {
112122 expect ( screen . getByTestId ( 'line-chart' ) ) . toBeInTheDocument ( ) ;
113123 } ) ;
114-
115- const allocationButton = screen . getByText ( 'Allocation' ) ;
116- fireEvent . click ( allocationButton ) ;
117-
118- await waitFor ( ( ) => {
119- expect ( screen . getByTestId ( 'pie-chart' ) ) . toBeInTheDocument ( ) ;
120- } ) ;
121124 } ) ;
122125
123- it ( 'calls onAssetClick when an asset is clicked' , ( ) => {
124- const onAssetClick = vi . fn ( ) ;
125- render (
126- < PortfolioChartWidget
127- { ...defaultProps }
128- onAssetClick = { onAssetClick }
129- />
130- ) ;
126+ it ( 'shows an accessible loading state and disables chart toggles' , ( ) => {
127+ render ( < PortfolioChartWidget { ...defaultProps } loading /> ) ;
131128
132- const assetElement = screen . getByText ( 'XLM' ) . closest ( 'div[class*="p-3"]' ) ;
133- if ( assetElement ) {
134- fireEvent . click ( assetElement ) ;
135- }
136-
137- // Should be called (exact behavior depends on component implementation)
138- expect ( screen . getByText ( 'XLM' ) ) . toBeInTheDocument ( ) ;
129+ expect ( screen . getByRole ( 'status' ) ) . toHaveTextContent ( 'Loading portfolio chart...' ) ;
130+ expect ( screen . getByRole ( 'button' , { name : 'Allocation' } ) ) . toBeDisabled ( ) ;
131+ expect ( screen . getByRole ( 'button' , { name : 'Trend' } ) ) . toBeDisabled ( ) ;
139132 } ) ;
140133
141- it ( 'toggles asset selection on click' , ( ) => {
142- render ( < PortfolioChartWidget { ...defaultProps } /> ) ;
143-
144- const assetElement = screen . getByText ( 'XLM' ) . closest ( 'div[class*="p-3"]' ) ;
134+ it ( 'shows an empty history state when no trend data exists' , async ( ) => {
135+ render ( < PortfolioChartWidget { ...defaultProps } historyData = { [ ] } /> ) ;
145136
146- if ( assetElement ) {
147- fireEvent . click ( assetElement ) ;
148- // Check that the element has selected styling (bg-blue-50)
149- expect ( assetElement ) . toHaveClass ( 'bg-blue-50' ) ;
137+ fireEvent . click ( screen . getByRole ( 'button' , { name : 'Trend' } ) ) ;
150138
151- fireEvent . click ( assetElement ) ;
152- // The selection might be toggled off
153- expect ( assetElement ) . toBeInTheDocument ( ) ;
154- }
139+ await waitFor ( ( ) => {
140+ expect ( screen . getByText ( 'No performance history available yet.' ) ) . toBeInTheDocument ( ) ;
141+ } ) ;
155142 } ) ;
156143
157- it ( 'renders with empty assets array ' , ( ) => {
144+ it ( 'shows the empty assets state when no assets are provided ' , ( ) => {
158145 render (
159146 < PortfolioChartWidget
160147 assets = { [ ] }
@@ -164,127 +151,61 @@ describe('PortfolioChartWidget', () => {
164151 />
165152 ) ;
166153
167- expect ( screen . getByText ( 'Portfolio Value ' ) ) . toBeInTheDocument ( ) ;
168- expect ( screen . getByText ( '$0.00 ' ) ) . toBeInTheDocument ( ) ;
154+ expect ( screen . getByText ( 'No allocation data available yet. ' ) ) . toBeInTheDocument ( ) ;
155+ expect ( screen . getByText ( 'No portfolio assets available yet. ' ) ) . toBeInTheDocument ( ) ;
169156 } ) ;
170157
171- it ( 'assigns colors from palette to assets without color' , ( ) => {
172- const assetsWithoutColor : PortfolioAsset [ ] = [
173- {
174- id : '1' ,
175- symbol : 'XLM' ,
176- name : 'Stellar' ,
177- amount : 100 ,
178- value : 1000 ,
179- percentage : 50 ,
180- } ,
181- {
182- id : '2' ,
183- symbol : 'USDC' ,
184- name : 'USD Coin' ,
185- amount : 100 ,
186- value : 1000 ,
187- percentage : 50 ,
188- } ,
189- ] ;
158+ it ( 'renders a locale-aware translation and currency format' , ( ) => {
159+ mockLocale = 'es' ;
160+ const formattedValue = new Intl . NumberFormat ( 'es-ES' , {
161+ style : 'currency' ,
162+ currency : 'EUR' ,
163+ minimumFractionDigits : 2 ,
164+ maximumFractionDigits : 2 ,
165+ } ) . format ( 5000 ) ;
190166
191167 render (
192- < PortfolioChartWidget
193- assets = { assetsWithoutColor }
194- totalValue = { 2000 }
195- showAnimation = { false }
196- />
197- ) ;
198-
199- expect ( screen . getByText ( 'XLM' ) ) . toBeInTheDocument ( ) ;
200- expect ( screen . getByText ( 'USDC' ) ) . toBeInTheDocument ( ) ;
201- } ) ;
202-
203- it ( 'handles custom className' , ( ) => {
204- const { container } = render (
205168 < PortfolioChartWidget
206169 { ...defaultProps }
207- className = "custom-class"
170+ totalValue = { 5000 }
171+ currency = "EUR"
208172 />
209173 ) ;
210174
211- const mainDiv = container . querySelector ( '.custom-class' ) ;
212- expect ( mainDiv ) . toBeInTheDocument ( ) ;
175+ expect ( screen . getByText ( 'Valor del portafolio' ) ) . toBeInTheDocument ( ) ;
176+ expect ( screen . getByText ( formattedValue ) ) . toBeInTheDocument ( ) ;
213177 } ) ;
214178
215- it ( 'respects showAnimation prop' , ( ) => {
216- const { rerender } = render (
179+ it ( 'handles asset selection and onAssetClick callbacks' , ( ) => {
180+ const onAssetClick = vi . fn ( ) ;
181+ render (
217182 < PortfolioChartWidget
218183 { ...defaultProps }
219- showAnimation = { true }
184+ onAssetClick = { onAssetClick }
220185 />
221186 ) ;
222187
223- expect ( screen . getByText ( 'Portfolio Value' ) ) . toBeInTheDocument ( ) ;
188+ const assetElement = screen . getByText ( 'XLM' ) . closest ( 'div[class*="p-3"]' ) ;
189+ expect ( assetElement ) . toBeInTheDocument ( ) ;
224190
225- rerender (
226- < PortfolioChartWidget
227- { ...defaultProps }
228- showAnimation = { false }
229- />
230- ) ;
191+ if ( assetElement ) {
192+ fireEvent . click ( assetElement ) ;
193+ expect ( assetElement ) . toHaveClass ( 'bg-blue-50' ) ;
194+ }
231195
232- expect ( screen . getByText ( 'Portfolio Value' ) ) . toBeInTheDocument ( ) ;
196+ expect ( onAssetClick ) . toHaveBeenCalledWith ( mockAssets [ 0 ] ) ;
233197 } ) ;
234198
235- it ( 'handles currency formatting for different currencies' , ( ) => {
236- const { rerender } = render (
237- < PortfolioChartWidget
238- { ...defaultProps }
239- currency = "USD"
240- totalValue = { 1000 }
241- />
242- ) ;
243-
244- expect ( screen . getByText ( '$1,000.00' ) ) . toBeInTheDocument ( ) ;
245-
246- rerender (
199+ it ( 'renders an error message when provided' , ( ) => {
200+ render (
247201 < PortfolioChartWidget
248202 { ...defaultProps }
249- currency = "GBP"
250- totalValue = { 1000 }
203+ error = "Unable to load the latest portfolio snapshot."
251204 />
252205 ) ;
253206
254- // Should render with different currency formatting
255- expect ( screen . getByText ( / P o r t f o l i o V a l u e / ) ) . toBeInTheDocument ( ) ;
256- } ) ;
257-
258- it ( 'handles large portfolio values' , ( ) => {
259- const largeAssets : PortfolioAsset [ ] = [
260- {
261- id : '1' ,
262- symbol : 'BTC' ,
263- name : 'Bitcoin' ,
264- amount : 0.5 ,
265- value : 20000 ,
266- percentage : 100 ,
267- } ,
268- ] ;
269-
270- const { container } = render (
271- < PortfolioChartWidget
272- assets = { largeAssets }
273- totalValue = { 20000 }
274- showAnimation = { false }
275- />
207+ expect ( screen . getByRole ( 'alert' ) ) . toHaveTextContent (
208+ 'Unable to load the latest portfolio snapshot.'
276209 ) ;
277-
278- // Find the total portfolio value (first $20,000.00 in the portfolio value section)
279- const portfolioValueTexts = screen . getAllByText ( '$20,000.00' ) ;
280- expect ( portfolioValueTexts . length ) . toBeGreaterThan ( 0 ) ;
281- } ) ;
282-
283- it ( 'displays asset color indicators' , ( ) => {
284- render ( < PortfolioChartWidget { ...defaultProps } /> ) ;
285-
286- const colorDots = screen . getAllByTestId ( 'cell' ) . length ;
287- // Should have color cells for each asset
288- expect ( colorDots ) . toBeGreaterThanOrEqual ( 0 ) ;
289210 } ) ;
290211} ) ;
0 commit comments