import { firstOfEach, firstOfAll, ifWe, lastOfEach, lastOfAll, ifWeMight, iReckon, respecfully } from '@ibgib/helper-gib/dist/respec-gib/respec-gib.mjs'; import { IbGibRouterServiceBase } from './ibgib-router-service-base.mjs'; import { RouterNavigateOptions } from './router-types.mjs'; const maam = `[${import.meta.url}]`, sir = maam; interface TestRouteInfo { view: string; id?: string; query?: Record; } class TestRouterService extends IbGibRouterServiceBase { public parseCalls = 0; public formatCalls = 0; public sameRouteCalls = 0; public fallbackCalls = 0; protected override parseRoute(pathname: string): TestRouteInfo { this.parseCalls++; const p = pathname.startsWith('/') ? pathname.slice(1) : pathname; const [pathOnly, queryString] = p.split('?'); const segments = pathOnly.split('/').filter(Boolean); if (segments.length === 0) { return { view: 'home' }; } if (segments[0] === 'error') { throw new Error('Simulated route error'); } const query: Record = {}; if (queryString) { const params = new URLSearchParams(queryString); params.forEach((v, k) => { query[k] = v; }); } return { view: segments[0], id: segments[1], query: Object.keys(query).length > 0 ? query : undefined, }; } protected override formatRoute(route: TestRouteInfo): string { this.formatCalls++; let res = `/${route.view}`; if (route.id) { res += `/${route.id}`; } if (route.query) { const params = new URLSearchParams(route.query); res += `?${params.toString()}`; } return res; } protected override isSameRoute(a?: TestRouteInfo, b?: TestRouteInfo): boolean { this.sameRouteCalls++; if (!a || !b) { return a === b; } if (a.view !== b.view || a.id !== b.id) { return false; } if (JSON.stringify(a.query || {}) !== JSON.stringify(b.query || {})) { return false; } return true; } protected override getFallbackRoute(pathname: string, error: unknown): TestRouteInfo { this.fallbackCalls++; return { view: 'fallback' }; } } await respecfully(sir, 'IbGibRouterServiceBase Specs', async () => { await ifWe(sir, 'should update currentRoute and notify subscribers on navigate', async () => { const router = new TestRouterService(); const received: TestRouteInfo[] = []; router.subscribe(async (route) => { received.push(route); }); await router.navigate({ view: 'notes', id: '123' }); iReckon(sir, router.getCurrentRoute()?.view).isGonnaBe('notes'); iReckon(sir, router.getCurrentRoute()?.id).isGonnaBe('123'); iReckon(sir, received.length).isGonnaBe(1); iReckon(sir, received[0].view).isGonnaBe('notes'); iReckon(sir, received[0].id).isGonnaBe('123'); }); await ifWe(sir, 'should suppress notifications when silent: true', async () => { const router = new TestRouterService(); const received: TestRouteInfo[] = []; router.subscribe((route) => { received.push(route); }); await router.navigate({ view: 'chat', id: 'lobby' }, { silent: true }); iReckon(sir, router.getCurrentRoute()?.view).isGonnaBe('chat'); iReckon(sir, router.getCurrentRoute()?.id).isGonnaBe('lobby'); iReckon(sir, received.length).isGonnaBe(0); }); await ifWe(sir, 'should not notify when navigating to the same route', async () => { const router = new TestRouterService(); let callCount = 0; router.subscribe(() => { callCount++; }); await router.navigate({ view: 'profile', id: 'user-1' }); iReckon(sir, callCount).isGonnaBe(1); // Navigating to identical route info await router.navigate({ view: 'profile', id: 'user-1' }); iReckon(sir, callCount).isGonnaBe(1); }); await ifWe(sir, 'should cleanly unsubscribe listeners', async () => { const router = new TestRouterService(); let callCount = 0; const unsubscribe = router.subscribe(() => { callCount++; }); await router.navigate({ view: 'tab1' }); iReckon(sir, callCount).isGonnaBe(1); unsubscribe(); await router.navigate({ view: 'tab2' }); iReckon(sir, callCount).isGonnaBe(1); iReckon(sir, router.getCurrentRoute()?.view).isGonnaBe('tab2'); }); await ifWe(sir, 'should parse pathname on navigatePath', async () => { const router = new TestRouterService(); const received: TestRouteInfo[] = []; router.subscribe((route) => { received.push(route); }); await router.navigatePath('/gallery/artwork-42'); iReckon(sir, router.getCurrentRoute()?.view).isGonnaBe('gallery'); iReckon(sir, router.getCurrentRoute()?.id).isGonnaBe('artwork-42'); iReckon(sir, received.length).isGonnaBe(1); }); await ifWe(sir, 'should invoke fallback on loadCurrentURL when parser throws', async () => { const router = new TestRouterService(); const received: TestRouteInfo[] = []; router.subscribe((route) => { received.push(route); }); // TestRouterService throws on 'error' const route = await router.loadCurrentURL(); // in node environment without window, loadCurrentURL uses '/' iReckon(sir, route.view).isGonnaBe('home'); iReckon(sir, received.length).isGonnaBe(1); }); });