1- import { render } from "@/utils/test-utils"
1+ import { render , act } from "@/utils/test-utils"
2+ import React from "react"
23
34import TranslationProvider , { useAppTranslation } from "../TranslationContext"
45
6+ // Hoisted mock definitions — must be defined before vi.mock calls
7+ // because vi.mock is hoisted to the top of the file.
8+ const mockTranslations = vi . hoisted ( ( ) : Record < string , Record < string , string > > => ( {
9+ en : {
10+ "settings.autoApprove.title" : "Auto-Approve" ,
11+ "notifications.error" : "Operation failed" ,
12+ "common:confirmation.editMessage" : "Edit Message" ,
13+ "common:confirmation.deleteMessage" : "Delete Message" ,
14+ "common:confirmation.editWarning" :
15+ "Editing this message will delete all subsequent messages in the conversation. Do you want to proceed?" ,
16+ "common:confirmation.deleteWarning" :
17+ "Deleting this message will delete all subsequent messages in the conversation. Do you want to proceed?" ,
18+ "common:answers.cancel" : "Cancel" ,
19+ "common:confirmation.proceed" : "Proceed" ,
20+ } ,
21+ ru : {
22+ "settings.autoApprove.title" : "Авто-Одобрение" ,
23+ "notifications.error" : "Ошибка операции" ,
24+ "common:confirmation.editMessage" : "Редактировать Сообщение" ,
25+ "common:confirmation.deleteMessage" : "Удалить Сообщение" ,
26+ "common:confirmation.editWarning" :
27+ "Редактирование этого сообщения приведет к удалению всех последующих сообщений в разговоре. Хотите продолжить?" ,
28+ "common:confirmation.deleteWarning" :
29+ "Удаление этого сообщения приведет к удалению всех последующих сообщений в разговоре. Хотите продолжить?" ,
30+ "common:answers.cancel" : "Отмена" ,
31+ "common:confirmation.proceed" : "Продолжить" ,
32+ } ,
33+ } ) )
34+
35+ const mockLanguageState = vi . hoisted ( ( ) => ( {
36+ current : "en" as string ,
37+ reset ( ) {
38+ this . current = "en"
39+ } ,
40+ } ) )
41+
42+ const mockI18n = vi . hoisted ( ( ) => ( {
43+ t : ( key : string , options ?: Record < string , any > ) => {
44+ const langTranslations = mockTranslations [ mockLanguageState . current ] || mockTranslations . en
45+ let result = langTranslations [ key ] || key
46+ if ( options ?. message ) {
47+ result = result . replace ( "{{message}}" , options . message )
48+ }
49+ return result
50+ } ,
51+ get language ( ) {
52+ return mockLanguageState . current
53+ } ,
54+ changeLanguage : vi . fn ( ( lang : string ) => {
55+ mockLanguageState . current = lang
56+ } ) ,
57+ } ) )
58+
559vi . mock ( "@/context/ExtensionStateContext" , ( ) => ( {
660 useExtensionState : ( ) => ( {
7- language : "en" ,
61+ language : mockI18n . language ,
862 } ) ,
963} ) )
1064
1165vi . mock ( "react-i18next" , ( ) => ( {
12- useTranslation : ( ) => ( {
13- i18n : {
14- t : ( key : string , options ?: Record < string , any > ) => {
15- // Mock specific translations used in tests
16- if ( key === "settings.autoApprove.title" ) return "Auto-Approve"
17- if ( key === "notifications.error" ) {
18- return options ?. message ? `Operation failed: ${ options . message } ` : "Operation failed"
19- }
20- return key
21- } ,
22- changeLanguage : vi . fn ( ) ,
23- } ,
24- } ) ,
66+ useTranslation : ( ) => ( { i18n : mockI18n } ) ,
2567} ) )
2668
2769vi . mock ( "../setup" , ( ) => ( {
28- default : {
29- t : ( key : string , options ?: Record < string , any > ) => {
30- // Mock specific translations used in tests
31- if ( key === "settings.autoApprove.title" ) return "Auto-Approve"
32- if ( key === "notifications.error" ) {
33- return options ?. message ? `Operation failed: ${ options . message } ` : "Operation failed"
34- }
35- return key
36- } ,
37- changeLanguage : vi . fn ( ) ,
38- } ,
70+ default : mockI18n ,
3971 loadTranslations : vi . fn ( ) ,
4072} ) )
4173
@@ -44,20 +76,25 @@ const TestComponent = () => {
4476 return (
4577 < div >
4678 < h1 data-testid = "translation-test" > { t ( "settings.autoApprove.title" ) } </ h1 >
47- < p data-testid = "translation-interpolation" > { t ( "notifications.error" , { message : "Test error" } ) } </ p >
79+ < p data-testid = "translation-interpolation" >
80+ { t ( "notifications.error" , { message : "Test error" } ) }
81+ </ p >
4882 </ div >
4983 )
5084}
5185
5286describe ( "TranslationContext" , ( ) => {
87+ beforeEach ( ( ) => {
88+ mockLanguageState . reset ( )
89+ } )
90+
5391 it ( "should provide translations via context" , ( ) => {
5492 const { getByTestId } = render (
5593 < TranslationProvider >
5694 < TestComponent />
5795 </ TranslationProvider > ,
5896 )
5997
60- // Check if translation is provided correctly
6198 expect ( getByTestId ( "translation-test" ) ) . toHaveTextContent ( "Auto-Approve" )
6299 } )
63100
@@ -68,7 +105,106 @@ describe("TranslationContext", () => {
68105 </ TranslationProvider > ,
69106 )
70107
71- // Check if interpolation works
72- expect ( getByTestId ( "translation-interpolation" ) ) . toHaveTextContent ( "Operation failed: Test error" )
108+ expect ( getByTestId ( "translation-interpolation" ) ) . toHaveTextContent ( "Operation failed" )
109+ } )
110+
111+ it ( "should re-render consumers when language changes (regression for memoized components)" , ( ) => {
112+ const MemoizedConsumer = React . memo ( ( ) => {
113+ const { t } = useAppTranslation ( )
114+ return (
115+ < div >
116+ < span data-testid = "memo-title" > { t ( "common:confirmation.editMessage" ) } </ span >
117+ < span data-testid = "memo-desc" > { t ( "common:confirmation.editWarning" ) } </ span >
118+ < span data-testid = "memo-cancel" > { t ( "common:answers.cancel" ) } </ span >
119+ < span data-testid = "memo-proceed" > { t ( "common:confirmation.proceed" ) } </ span >
120+ </ div >
121+ )
122+ } )
123+
124+ const NormalConsumer = ( ) => {
125+ const { t } = useAppTranslation ( )
126+ return (
127+ < div >
128+ < span data-testid = "normal-title" > { t ( "common:confirmation.editMessage" ) } </ span >
129+ < span data-testid = "normal-desc" > { t ( "common:confirmation.editWarning" ) } </ span >
130+ </ div >
131+ )
132+ }
133+
134+ const { getByTestId, rerender } = render (
135+ < TranslationProvider >
136+ < MemoizedConsumer />
137+ < NormalConsumer />
138+ </ TranslationProvider > ,
139+ )
140+
141+ // Initial render — English
142+ expect ( getByTestId ( "memo-title" ) ) . toHaveTextContent ( "Edit Message" )
143+ expect ( getByTestId ( "memo-desc" ) ) . toHaveTextContent (
144+ "Editing this message will delete all subsequent messages" ,
145+ )
146+ expect ( getByTestId ( "memo-cancel" ) ) . toHaveTextContent ( "Cancel" )
147+ expect ( getByTestId ( "memo-proceed" ) ) . toHaveTextContent ( "Proceed" )
148+ expect ( getByTestId ( "normal-title" ) ) . toHaveTextContent ( "Edit Message" )
149+
150+ // Change language to Russian
151+ act ( ( ) => {
152+ mockI18n . changeLanguage ( "ru" )
153+ } )
154+
155+ // Re-render to pick up context change
156+ rerender (
157+ < TranslationProvider >
158+ < MemoizedConsumer />
159+ < NormalConsumer />
160+ </ TranslationProvider > ,
161+ )
162+
163+ // Both memoized and normal consumers should show Russian
164+ expect ( getByTestId ( "memo-title" ) ) . toHaveTextContent ( "Редактировать Сообщение" )
165+ expect ( getByTestId ( "memo-desc" ) ) . toHaveTextContent (
166+ "Редактирование этого сообщения приведет к удалению" ,
167+ )
168+ expect ( getByTestId ( "memo-cancel" ) ) . toHaveTextContent ( "Отмена" )
169+ expect ( getByTestId ( "memo-proceed" ) ) . toHaveTextContent ( "Продолжить" )
170+ expect ( getByTestId ( "normal-title" ) ) . toHaveTextContent ( "Редактировать Сообщение" )
171+ } )
172+
173+ it ( "should re-render delete dialog with correct translations after language switch" , ( ) => {
174+ const DeleteDialog = React . memo ( ( ) => {
175+ const { t } = useAppTranslation ( )
176+ return (
177+ < div >
178+ < span data-testid = "delete-title" > { t ( "common:confirmation.deleteMessage" ) } </ span >
179+ < span data-testid = "delete-desc" > { t ( "common:confirmation.deleteWarning" ) } </ span >
180+ </ div >
181+ )
182+ } )
183+
184+ const { getByTestId, rerender } = render (
185+ < TranslationProvider >
186+ < DeleteDialog />
187+ </ TranslationProvider > ,
188+ )
189+
190+ expect ( getByTestId ( "delete-title" ) ) . toHaveTextContent ( "Delete Message" )
191+ expect ( getByTestId ( "delete-desc" ) ) . toHaveTextContent (
192+ "Deleting this message will delete all subsequent messages" ,
193+ )
194+
195+ act ( ( ) => {
196+ mockI18n . changeLanguage ( "ru" )
197+ } )
198+
199+ rerender (
200+ < TranslationProvider >
201+ < DeleteDialog />
202+ </ TranslationProvider > ,
203+ )
204+
205+ expect ( getByTestId ( "delete-title" ) ) . toHaveTextContent ( "Удалить Сообщение" )
206+ expect ( getByTestId ( "delete-desc" ) ) . toHaveTextContent (
207+ "Удаление этого сообщения приведет к удалению" ,
208+ )
73209 } )
74210} )
0 commit comments