@@ -13,7 +13,9 @@ vi.mock('@fortawesome/react-fontawesome', () => ({
1313} ) ) ;
1414
1515vi . mock ( 'calcite-react/Slider' , ( ) => ( { default : 'input' } ) ) ;
16- vi . mock ( '../widgets/components/LanguageIcon' , ( ) => ( { default : ( ) => null } ) ) ;
16+ vi . mock ( '../widgets/components/LanguageIcon' , ( ) => ( {
17+ default : ( { lang } : { lang ?: string } ) => < span data-testid = { `language-icon-${ lang } ` } /> ,
18+ } ) ) ;
1719vi . mock ( 'react-bootstrap/Alert' , ( ) => ( {
1820 __esModule : true ,
1921 default : ( {
@@ -37,7 +39,12 @@ vi.mock('../i18n', () => ({
3739 getLocale : vi . fn ( ( ) => 'en' ) ,
3840 getSupportedLocale : vi . fn ( ( locale ) => ( [ 'en' , 'ru' ] . includes ( locale ) ? locale : 'en' ) ) ,
3941 default : {
40- t : vi . fn ( ( key ) => key ) ,
42+ t : vi . fn ( ( key : string , params : Record < string , unknown > = { } ) =>
43+ Object . entries ( params ) . reduce (
44+ ( result , [ name , value ] ) => result . replace ( `%{${ name } }` , String ( value ) ) ,
45+ key ,
46+ ) ,
47+ ) ,
4148 changeLanguage : vi . fn ( ( ) => Promise . resolve ( ) ) ,
4249 } ,
4350} ) ) ;
@@ -72,6 +79,24 @@ vi.mock('@/inertia/pageProps', () => {
7279 local : 'en' ,
7380 current_user : { sound_settings : { } } ,
7481 game_id : 10 ,
82+ user_sessions : [
83+ {
84+ id : 'current-session' ,
85+ current : true ,
86+ user_agent : 'Current browser' ,
87+ ip : '127.0.0.1' ,
88+ last_seen_at : '2026-07-23T12:00:00Z' ,
89+ created_at : '2026-07-23T11:00:00Z' ,
90+ } ,
91+ {
92+ id : 'other-session' ,
93+ current : false ,
94+ user_agent : 'Other browser' ,
95+ ip : '10.0.0.2' ,
96+ last_seen_at : '2026-07-22T12:00:00Z' ,
97+ created_at : '2026-07-22T11:00:00Z' ,
98+ } ,
99+ ] ,
75100 } ;
76101 return {
77102 getPageProp : ( key : keyof typeof pageProps , fallback ?: unknown ) => pageProps [ key ] ?? fallback ,
@@ -107,7 +132,7 @@ describe('UserSettings test cases', () => {
107132 ok : true ,
108133 json : async ( ) => ( { } ) ,
109134 } ) ;
110- const { getByRole, getByLabelText, getByTestId, user } = setup (
135+ const { getByRole, getByLabelText, getByTestId, findByText , user } = setup (
111136 < Provider store = { store } >
112137 < UserSettings />
113138 </ Provider > ,
@@ -118,7 +143,8 @@ describe('UserSettings test cases', () => {
118143
119144 await user . clear ( nameInput ) ;
120145 await user . type ( nameInput , 'Dmitry' ) ;
121- await user . selectOptions ( codeLangSelect , 'Javascript' ) ;
146+ await user . click ( codeLangSelect ) ;
147+ await user . click ( await findByText ( 'Javascript' ) ) ;
122148 await user . click ( submitButton ) ;
123149
124150 await waitFor ( ( ) => {
@@ -228,6 +254,112 @@ describe('UserSettings test cases', () => {
228254 expect ( await findByRole ( 'alert' ) ) . toHaveClass ( 'alert-danger' ) ;
229255 } ) ;
230256
257+ test ( 'enables saving the Silent sound option' , async ( ) => {
258+ fetchMock . mockResolvedValueOnce ( {
259+ ok : true ,
260+ json : async ( ) => ( { locale : 'en' } ) ,
261+ } ) ;
262+
263+ const { getByLabelText, user } = setup (
264+ < Provider store = { store } >
265+ < UserSettings />
266+ </ Provider > ,
267+ ) ;
268+
269+ const submitButton = getByLabelText ( 'SubmitForm' ) ;
270+ await user . click ( getByLabelText ( 'Silent' ) ) ;
271+
272+ expect ( submitButton ) . toBeEnabled ( ) ;
273+ await user . click ( submitButton ) ;
274+
275+ await waitFor ( ( ) => {
276+ const [ , requestOptions ] = fetchMock . mock . calls [ 0 ] ;
277+ expect ( JSON . parse ( requestOptions . body ) . sound_settings . type ) . toBe ( 'silent' ) ;
278+ } ) ;
279+ } ) ;
280+
281+ test ( 'shows language icons in the settings language menu' , async ( ) => {
282+ const { getByTestId, getAllByTestId, user } = setup (
283+ < Provider store = { store } >
284+ < UserSettings />
285+ </ Provider > ,
286+ ) ;
287+
288+ await user . click ( getByTestId ( 'code-langSelect' ) ) ;
289+
290+ expect ( getAllByTestId ( 'language-icon-js' ) . length ) . toBeGreaterThan ( 0 ) ;
291+ expect ( getAllByTestId ( 'language-icon-python' ) . length ) . toBeGreaterThan ( 0 ) ;
292+ } ) ;
293+
294+ test ( 'changes a password without sending password fields to the settings endpoint' , async ( ) => {
295+ const passwordStore = configureStore ( {
296+ reducer,
297+ preloadedState : {
298+ user : {
299+ settings : {
300+ ...preloadedState . user . settings ,
301+ hasPassword : true ,
302+ } ,
303+ } ,
304+ } as never ,
305+ } ) ;
306+
307+ fetchMock
308+ . mockResolvedValueOnce ( {
309+ ok : true ,
310+ json : async ( ) => ( { locale : 'en' } ) ,
311+ } )
312+ . mockResolvedValueOnce ( {
313+ ok : true ,
314+ json : async ( ) => ( { status : 'ok' , has_password : true } ) ,
315+ } ) ;
316+
317+ const { getByLabelText, getByTestId, user } = setup (
318+ < Provider store = { passwordStore } >
319+ < UserSettings />
320+ </ Provider > ,
321+ ) ;
322+
323+ await user . type ( getByTestId ( 'currentPasswordInput' ) , 'old-password-secure!' ) ;
324+ await user . type ( getByTestId ( 'passwordInput' ) , 'new-password-secure!' ) ;
325+ await user . type ( getByTestId ( 'passwordConfirmationInput' ) , 'new-password-secure!' ) ;
326+ await user . click ( getByLabelText ( 'SubmitForm' ) ) ;
327+
328+ await waitFor ( ( ) => expect ( fetchMock ) . toHaveBeenCalledTimes ( 2 ) ) ;
329+
330+ const settingsBody = JSON . parse ( fetchMock . mock . calls [ 0 ] [ 1 ] . body ) ;
331+ expect ( settingsBody ) . not . toHaveProperty ( 'current_password' ) ;
332+ expect ( settingsBody ) . not . toHaveProperty ( 'password' ) ;
333+ expect ( fetchMock . mock . calls [ 1 ] [ 0 ] ) . toBe ( '/api/v1/settings/password' ) ;
334+ expect ( JSON . parse ( fetchMock . mock . calls [ 1 ] [ 1 ] . body ) ) . toEqual ( {
335+ current_password : 'old-password-secure!' ,
336+ password : 'new-password-secure!' ,
337+ password_confirmation : 'new-password-secure!' ,
338+ } ) ;
339+ } ) ;
340+
341+ test ( 'removes another active device' , async ( ) => {
342+ fetchMock . mockResolvedValueOnce ( {
343+ ok : true ,
344+ json : async ( ) => ( { status : 'ok' , current : false } ) ,
345+ } ) ;
346+
347+ const { getByLabelText, queryByText, user } = setup (
348+ < Provider store = { store } >
349+ < UserSettings />
350+ </ Provider > ,
351+ ) ;
352+
353+ expect ( queryByText ( 'Other browser' ) ) . toBeInTheDocument ( ) ;
354+ await user . click ( getByLabelText ( 'Remove device Other browser' ) ) ;
355+
356+ await waitFor ( ( ) => expect ( queryByText ( 'Other browser' ) ) . not . toBeInTheDocument ( ) ) ;
357+ expect ( fetchMock ) . toHaveBeenCalledWith (
358+ '/api/v1/settings/sessions/other-session' ,
359+ expect . objectContaining ( { method : 'DELETE' } ) ,
360+ ) ;
361+ } ) ;
362+
231363 test ( 'unlink button is disabled when it is the last sign-in method' , ( ) => {
232364 const customStore = configureStore ( {
233365 reducer,
0 commit comments