@@ -4,7 +4,9 @@ import { NotFoundException } from '@danet/core';
44import { createMockLogger } from '@scope/common/testing' ;
55import { ConfigService } from './config.service.ts' ;
66import { validateNavigation } from './config.validation.ts' ;
7+ import { sortNavigation } from './config.transformer.ts' ;
78import type { Config } from './config.types.ts' ;
9+ import type { NavigationItemInput } from './config.document.ts' ;
810import type { ConfigRepository } from './config.repository.ts' ;
911import type { ExperimentService } from '@scope/experiment' ;
1012
@@ -190,6 +192,119 @@ describe('validateNavigation', () => {
190192 } ) ;
191193} ) ;
192194
195+ // ---------------------------------------------------------------------------
196+ // Unit: sortNavigation
197+ // ---------------------------------------------------------------------------
198+
199+ function makeDocNavItem (
200+ overrides ?: Partial < NavigationItemInput > ,
201+ ) : NavigationItemInput {
202+ return {
203+ criteria : 'test' ,
204+ destination : '/test' ,
205+ i18n : 'nav.test' ,
206+ icon : 'test' ,
207+ group : { authenticated : false , i18n : 'nav.group.main' } ,
208+ ...overrides ,
209+ } ;
210+ }
211+
212+ describe ( 'sortNavigation' , ( ) => {
213+ it ( 'sorts by group.rank ascending, then rank ascending' , ( ) => {
214+ const nav : NavigationItemInput [ ] = [
215+ makeDocNavItem ( {
216+ key : 'catalogs-1' ,
217+ group : { authenticated : false , i18n : 'nav.group.catalogs' , rank : 2 } ,
218+ rank : 1 ,
219+ } ) ,
220+ makeDocNavItem ( {
221+ key : 'general-1' ,
222+ group : { authenticated : false , i18n : 'nav.group.general' , rank : 0 } ,
223+ rank : 0 ,
224+ } ) ,
225+ makeDocNavItem ( {
226+ key : 'general-2' ,
227+ group : { authenticated : false , i18n : 'nav.group.general' , rank : 0 } ,
228+ rank : 1 ,
229+ } ) ,
230+ makeDocNavItem ( {
231+ key : 'manage-1' ,
232+ group : { authenticated : true , i18n : 'nav.group.manage' , rank : 1 } ,
233+ rank : 0 ,
234+ } ) ,
235+ makeDocNavItem ( {
236+ key : 'support-1' ,
237+ group : { authenticated : false , i18n : 'nav.group.support' , rank : 3 } ,
238+ rank : 0 ,
239+ } ) ,
240+ ] ;
241+ const sorted = sortNavigation ( nav ) ;
242+ assertEquals ( sorted . map ( ( i ) => i . key ) , [
243+ 'general-1' ,
244+ 'general-2' ,
245+ 'manage-1' ,
246+ 'catalogs-1' ,
247+ 'support-1' ,
248+ ] ) ;
249+ } ) ;
250+
251+ it ( 'sorts items with undefined rank after ranked items' , ( ) => {
252+ const nav : NavigationItemInput [ ] = [
253+ makeDocNavItem ( {
254+ key : 'has-rank' ,
255+ group : { authenticated : false , i18n : 'group' , rank : 0 } ,
256+ rank : 0 ,
257+ } ) ,
258+ makeDocNavItem ( {
259+ key : 'no-rank' ,
260+ group : { authenticated : false , i18n : 'group' } ,
261+ } ) ,
262+ ] ;
263+ const sorted = sortNavigation ( nav ) ;
264+ assertEquals ( sorted . map ( ( i ) => i . key ) , [ 'has-rank' , 'no-rank' ] ) ;
265+ } ) ;
266+
267+ it ( 'does not mutate the input array' , ( ) => {
268+ const nav : NavigationItemInput [ ] = [
269+ makeDocNavItem ( {
270+ key : 'b' ,
271+ group : { authenticated : false , i18n : 'group' , rank : 0 } ,
272+ rank : 1 ,
273+ } ) ,
274+ makeDocNavItem ( {
275+ key : 'a' ,
276+ group : { authenticated : false , i18n : 'group' , rank : 0 } ,
277+ rank : 0 ,
278+ } ) ,
279+ ] ;
280+ const copy = [ ...nav ] ;
281+ sortNavigation ( nav ) ;
282+ assertEquals ( nav , copy ) ;
283+ } ) ;
284+
285+ it ( 'uses key as tiebreaker for deterministic ordering' , ( ) => {
286+ const nav : NavigationItemInput [ ] = [
287+ makeDocNavItem ( {
288+ key : 'z' ,
289+ group : { authenticated : false , i18n : 'group' , rank : 0 } ,
290+ rank : 0 ,
291+ } ) ,
292+ makeDocNavItem ( {
293+ key : 'a' ,
294+ group : { authenticated : false , i18n : 'group' , rank : 0 } ,
295+ rank : 0 ,
296+ } ) ,
297+ makeDocNavItem ( {
298+ key : 'm' ,
299+ group : { authenticated : false , i18n : 'group' , rank : 0 } ,
300+ rank : 0 ,
301+ } ) ,
302+ ] ;
303+ const sorted = sortNavigation ( nav ) ;
304+ assertEquals ( sorted . map ( ( i ) => i . key ) , [ 'a' , 'm' , 'z' ] ) ;
305+ } ) ;
306+ } ) ;
307+
193308// ---------------------------------------------------------------------------
194309// Stub classes for integration tests
195310// ---------------------------------------------------------------------------
@@ -207,7 +322,7 @@ type ConfigDocumentStub = {
207322 _id : { toString ( ) : string } ;
208323 image : Record < string , string > ;
209324 genres : Array < { name : string ; mediaId : number } > ;
210- navigation : Config [ 'navigation' ] ;
325+ navigation : NavigationItemInput [ ] ;
211326} ;
212327
213328function makeDocumentStub (
@@ -265,6 +380,65 @@ describe('ConfigService.getConfig', () => {
265380 assertEquals ( result . navigation [ 1 ] . key , 'search' ) ;
266381 } ) ;
267382
383+ it ( 'returns navigation sorted by group.rank then rank, without rank fields in output' , async ( ) => {
384+ const repository = {
385+ getConfig : async ( ) =>
386+ makeDocumentStub ( {
387+ navigation : [
388+ makeDocNavItem ( {
389+ key : 'support' ,
390+ destination : '/support' ,
391+ group : { authenticated : false , i18n : 'support' , rank : 3 } ,
392+ rank : 0 ,
393+ } ) ,
394+ makeDocNavItem ( {
395+ key : 'general' ,
396+ destination : '/home' ,
397+ group : { authenticated : false , i18n : 'general' , rank : 0 } ,
398+ rank : 0 ,
399+ } ) ,
400+ makeDocNavItem ( {
401+ key : 'catalogs' ,
402+ destination : '/discover' ,
403+ group : { authenticated : false , i18n : 'catalogs' , rank : 2 } ,
404+ rank : 0 ,
405+ } ) ,
406+ makeDocNavItem ( {
407+ key : 'manage' ,
408+ destination : '/animelist' ,
409+ group : { authenticated : true , i18n : 'manage' , rank : 1 } ,
410+ rank : 0 ,
411+ } ) ,
412+ ] ,
413+ } ) ,
414+ } ;
415+ const experiment = new ExperimentStub ( ) ;
416+ const { logger } = createMockLogger ( ) ;
417+ const service = new ConfigService (
418+ repository as unknown as ConfigRepository ,
419+ experiment as unknown as ExperimentService ,
420+ logger ,
421+ ) ;
422+
423+ const result = await service . getConfig ( ) ;
424+ assertEquals ( result . navigation . length , 4 ) ;
425+ // Expected order: General(0) → Manage(1) → Catalogs(2) → Support(3)
426+ assertEquals ( result . navigation . map ( ( i ) => i . key ) , [
427+ 'general' ,
428+ 'manage' ,
429+ 'catalogs' ,
430+ 'support' ,
431+ ] ) ;
432+ // Verify rank is not in the output
433+ for ( const item of result . navigation ) {
434+ assertEquals ( ( item as Record < string , unknown > ) . rank , undefined ) ;
435+ assertEquals (
436+ ( ( item . group as unknown ) as Record < string , unknown > ) . rank ,
437+ undefined ,
438+ ) ;
439+ }
440+ } ) ;
441+
268442 it ( 'throws Error when navigation has duplicate keys' , async ( ) => {
269443 const repository = {
270444 getConfig : async ( ) =>
0 commit comments