@@ -46,22 +46,37 @@ interface RouterEvent {
4646interface RouterMock {
4747 events : Subject < RouterEvent > ;
4848 url : string ;
49+ // Mirrors `Router.getCurrentNavigation()` enough that the tracker can read
50+ // `extras.clearHistory` off it — the same hook NS's page-router-outlet uses
51+ // to enable `clearHistory: true` end-to-end.
52+ currentNavigation : { extras ?: { clearHistory ?: boolean } } | null ;
53+ getCurrentNavigation ( ) : { extras ?: { clearHistory ?: boolean } } | null ;
4954 emitNavigationEnd ( url : string ) : void ;
5055 emitNavigationStart (
5156 url : string ,
5257 options ?: {
5358 trigger ?: 'imperative' | 'popstate' | 'hashchange' ;
5459 restoredState ?: { navigationId : number } | null ;
60+ clearHistory ?: boolean ;
5561 } ,
5662 ) : void ;
5763}
5864
5965function createRouterMock ( initialUrl : string ) : RouterMock {
6066 const events = new Subject < RouterEvent > ( ) ;
61- return {
67+ const router : RouterMock = {
6268 events,
6369 url : initialUrl ,
70+ currentNavigation : null ,
71+ getCurrentNavigation ( ) {
72+ return this . currentNavigation ;
73+ } ,
6474 emitNavigationStart ( url , options ) {
75+ // Mirror Angular: `getCurrentNavigation()` returns the active navigation
76+ // between NavigationStart and NavigationEnd, then is cleared.
77+ this . currentNavigation = {
78+ extras : options ?. clearHistory ? { clearHistory : true } : { } ,
79+ } ;
6580 events . next (
6681 new MockNavigationStart (
6782 1 ,
@@ -74,8 +89,10 @@ function createRouterMock(initialUrl: string): RouterMock {
7489 emitNavigationEnd ( url ) {
7590 this . url = url ;
7691 events . next ( new MockNavigationEnd ( 1 , url , url ) as unknown as RouterEvent ) ;
92+ this . currentNavigation = null ;
7793 } ,
7894 } ;
95+ return router ;
7996}
8097
8198describe ( 'NativeScriptAngularHmrRouteTracker' , ( ) => {
@@ -170,6 +187,74 @@ describe('NativeScriptAngularHmrRouteTracker', () => {
170187 } ) ;
171188 } ) ;
172189
190+ describe ( 'clearHistory navigation extra' , ( ) => {
191+ it ( 'collapses the live mirror to the destination URL when clearHistory is set' , ( ) => {
192+ // Reproduces the canonical HeyKiddo auth flow: user passes through
193+ // /signup-landing, /login on the way to a clearHistory navigation
194+ // that drops the iOS back-stack down to /talk. Without mirroring
195+ // that on the HMR side, a subsequent .ts edit replays NavigationEnd
196+ // for every URL the user passed through — including login pages
197+ // that the auth-gate now redirects away from.
198+ const router = createRouterMock ( '/' ) ;
199+
200+ // eslint-disable-next-line @typescript-eslint/no-unused-vars
201+ const tracker = new NativeScriptAngularHmrRouteTracker ( router as never ) ;
202+
203+ router . emitNavigationStart ( '/signup-landing' ) ;
204+ router . emitNavigationEnd ( '/signup-landing' ) ;
205+ router . emitNavigationStart ( '/login' ) ;
206+ router . emitNavigationEnd ( '/login' ) ;
207+
208+ expect ( readAngularHmrRouteHistory ( ) ) . toEqual ( [ '/signup-landing' , '/login' ] ) ;
209+
210+ router . emitNavigationStart ( '/talk/(todayTab:today)' , { clearHistory : true } ) ;
211+ router . emitNavigationEnd ( '/talk/(todayTab:today)' ) ;
212+
213+ expect ( readAngularHmrRouteHistory ( ) ) . toEqual ( [ '/talk/(todayTab:today)' ] ) ;
214+ } ) ;
215+
216+ it ( 'keeps subsequent non-clearHistory navigations on top of the destination after a reset' , ( ) => {
217+ // After a clearHistory reset, normal forward navigation (e.g. tab
218+ // switches) should still grow the mirror so the back-stack is
219+ // restored across HMR cycles for the post-reset session.
220+ const router = createRouterMock ( '/' ) ;
221+
222+ // eslint-disable-next-line @typescript-eslint/no-unused-vars
223+ const tracker = new NativeScriptAngularHmrRouteTracker ( router as never ) ;
224+
225+ router . emitNavigationStart ( '/login' ) ;
226+ router . emitNavigationEnd ( '/login' ) ;
227+ router . emitNavigationStart ( '/talk/(todayTab:today)' , { clearHistory : true } ) ;
228+ router . emitNavigationEnd ( '/talk/(todayTab:today)' ) ;
229+ router . emitNavigationStart ( '/talk/(progressTab:progress//todayTab:today)' ) ;
230+ router . emitNavigationEnd ( '/talk/(progressTab:progress//todayTab:today)' ) ;
231+
232+ expect ( readAngularHmrRouteHistory ( ) ) . toEqual ( [
233+ '/talk/(todayTab:today)' ,
234+ '/talk/(progressTab:progress//todayTab:today)' ,
235+ ] ) ;
236+ } ) ;
237+
238+ it ( 'does not throw when the router mock omits getCurrentNavigation' , ( ) => {
239+ // Defensive: the tracker should fall back to the legacy push path on
240+ // routers that don't expose `getCurrentNavigation()`. This protects
241+ // older Angular versions, edge-case mocks, and hardened proxy wrappers
242+ // that strip non-public methods.
243+ const router = createRouterMock ( '/' ) ;
244+ // Force a router shape without the API; the tracker must treat it
245+ // as "no clearHistory".
246+ ( router as { getCurrentNavigation ?: unknown } ) . getCurrentNavigation = undefined ;
247+
248+ // eslint-disable-next-line @typescript-eslint/no-unused-vars
249+ const tracker = new NativeScriptAngularHmrRouteTracker ( router as never ) ;
250+
251+ router . emitNavigationStart ( '/login' , { clearHistory : true } ) ;
252+ router . emitNavigationEnd ( '/login' ) ;
253+
254+ expect ( readAngularHmrRouteHistory ( ) ) . toEqual ( [ '/login' ] ) ;
255+ } ) ;
256+ } ) ;
257+
173258 describe ( 'clear-after-snapshot integration' , ( ) => {
174259 it ( 'a snapshot during HMR reboot leaves the live mirror empty for the next bootstrap to rebuild' , ( ) => {
175260 // Cycle 1: real boot, walk forward to /profile.
0 commit comments