11import { afterEach , beforeAll , beforeEach , describe , expect , test , vi } from "vitest" ;
22import { LocationFull } from "./LocationFull.js" ;
33import type { State , Location } from "$lib/types.js" ;
4- import { flushSync } from "svelte" ;
54import { joinPaths } from "./RouterEngine.svelte.js" ;
65
76describe ( "LocationFull" , ( ) => {
@@ -58,14 +57,46 @@ describe("LocationFull", () => {
5857 test ( "Should register the provided callback for the 'beforeNavigate' event." , ( ) => {
5958 // Arrange.
6059 const callback = vi . fn ( ) ;
61- location . on ( 'beforeNavigate' , callback ) ;
60+ const unSub = location . on ( 'beforeNavigate' , callback ) ;
6261
6362 // Act.
6463 globalThis . window . history . pushState ( null , '' , 'http://example.com/other' ) ;
65- flushSync ( ) ;
6664
6765 // Assert.
6866 expect ( callback ) . toHaveBeenCalledOnce ( ) ;
67+
68+ // Cleanup.
69+ unSub ( ) ;
70+ } ) ;
71+ test ( "Should unregister the provided callback when the returned function is called." , ( ) => {
72+ // Arrange.
73+ const callback = vi . fn ( ) ;
74+ const unSub = location . on ( 'beforeNavigate' , callback ) ;
75+
76+ // Act.
77+ unSub ( ) ;
78+
79+ // Assert.
80+ globalThis . window . history . pushState ( null , '' , 'http://example.com/other' ) ;
81+ expect ( callback ) . not . toHaveBeenCalled ( ) ;
82+ } ) ;
83+ test ( "Should not affect other handlers when unregistering one of the event handlers." , ( ) => {
84+ // Arrange.
85+ const callback1 = vi . fn ( ) ;
86+ const callback2 = vi . fn ( ) ;
87+ const unSub1 = location . on ( 'beforeNavigate' , callback1 ) ;
88+ const unSub2 = location . on ( 'beforeNavigate' , callback2 ) ;
89+
90+ // Act.
91+ unSub1 ( ) ;
92+
93+ // Assert.
94+ globalThis . window . history . pushState ( null , '' , 'http://example.com/other' ) ;
95+ expect ( callback1 ) . not . toHaveBeenCalled ( ) ;
96+ expect ( callback2 ) . toHaveBeenCalledOnce ( ) ;
97+
98+ // Cleanup.
99+ unSub2 ( ) ;
69100 } ) ;
70101 test . each ( [
71102 {
@@ -79,13 +110,12 @@ describe("LocationFull", () => {
79110 ] ) ( "Should provide the URL, state and method $method via the event object of 'beforeNavigate'." , ( { method, stateFn } ) => {
80111 // Arrange.
81112 const callback = vi . fn ( ) ;
82- const state = { test : 'value' } ;
83- location . on ( 'beforeNavigate' , callback ) ;
113+ const state = { path : { test : 'value' } , hash : { } } ;
114+ const unSub = location . on ( 'beforeNavigate' , callback ) ;
84115
85116 // Act.
86117 // @ts -expect-error stateFn cannot enumerate history.
87118 globalThis . window . history [ stateFn ] ( state , '' , 'http://example.com/other' ) ;
88- flushSync ( ) ;
89119
90120 // Assert.
91121 expect ( callback ) . toHaveBeenCalledWith ( {
@@ -96,16 +126,18 @@ describe("LocationFull", () => {
96126 cancelReason : undefined ,
97127 cancel : expect . any ( Function )
98128 } ) ;
129+
130+ // Cleanup.
131+ unSub ( ) ;
99132 } ) ;
100133 test ( "Should set wasCancelled to true and cancelReason to the provided reason when the event is cancelled to subsequent callbacks." , ( ) => {
101134 // Arrange.
102135 const callback = vi . fn ( ) ;
103- location . on ( 'beforeNavigate' , ( event ) => event . cancel ( 'test' ) ) ;
104- location . on ( 'beforeNavigate' , callback ) ;
136+ const unSub1 = location . on ( 'beforeNavigate' , ( event ) => event . cancel ( 'test' ) ) ;
137+ const unSub2 = location . on ( 'beforeNavigate' , callback ) ;
105138
106139 // Act.
107140 globalThis . window . history . pushState ( null , '' , 'http://example.com/other' ) ;
108- flushSync ( ) ;
109141
110142 // Assert.
111143 expect ( callback ) . toHaveBeenCalledWith ( {
@@ -116,17 +148,20 @@ describe("LocationFull", () => {
116148 cancelReason : 'test' ,
117149 cancel : expect . any ( Function )
118150 } ) ;
151+
152+ // Cleanup.
153+ unSub1 ( ) ;
154+ unSub2 ( ) ;
119155 } ) ;
120156 test ( "Should ignore cancellation reasons from callbacks if the event has already been cancelled." , ( ) => {
121157 // Arrange.
122158 const callback = vi . fn ( ) ;
123- location . on ( 'beforeNavigate' , ( event ) => event . cancel ( 'test' ) ) ;
124- location . on ( 'beforeNavigate' , ( event ) => event . cancel ( 'ignored' ) ) ;
125- location . on ( 'beforeNavigate' , callback ) ;
159+ const unSub1 = location . on ( 'beforeNavigate' , ( event ) => event . cancel ( 'test' ) ) ;
160+ const unSub2 = location . on ( 'beforeNavigate' , ( event ) => event . cancel ( 'ignored' ) ) ;
161+ const unSub3 = location . on ( 'beforeNavigate' , callback ) ;
126162
127163 // Act.
128164 globalThis . window . history . pushState ( null , '' , 'http://example.com/other' ) ;
129- flushSync ( ) ;
130165
131166 // Assert.
132167 expect ( callback ) . toHaveBeenCalledWith ( {
@@ -137,34 +172,66 @@ describe("LocationFull", () => {
137172 cancelReason : 'test' ,
138173 cancel : expect . any ( Function )
139174 } ) ;
175+
176+ // Cleanup.
177+ unSub1 ( ) ;
178+ unSub2 ( ) ;
179+ unSub3 ( ) ;
180+ } ) ;
181+ test . each ( [
182+ 'pushState' as const ,
183+ 'replaceState' as const ,
184+ ] ) ( "Should ultimately push the state data via the %s method set by beforeNavigate handlers in event.state." , ( stateFn ) => {
185+ // Arrange.
186+ const state = { path : { test : 'value' } , hash : { } } ;
187+ const callback = vi . fn ( ( event ) => {
188+ event . state = state ;
189+ } ) ;
190+ const unSub = location . on ( 'beforeNavigate' , callback ) ;
191+
192+ // Act.
193+ globalThis . window . history [ stateFn ] ( null , '' , 'http://example.com/other' ) ;
194+
195+ // Assert.
196+ expect ( callback ) . toHaveBeenCalledOnce ( ) ;
197+ expect ( globalThis . window . history . state ) . deep . equal ( state ) ;
198+
199+ // Cleanup.
200+ unSub ( ) ;
140201 } ) ;
141202 test ( "Should register the provided callback for the 'navigationCancelled' event." , ( ) => {
142203 // Arrange.
143204 const callback = vi . fn ( ) ;
144- location . on ( 'beforeNavigate' , ( event ) => event . cancel ( ) ) ;
145- location . on ( 'navigationCancelled' , callback ) ;
205+ const unSub1 = location . on ( 'beforeNavigate' , ( event ) => event . cancel ( ) ) ;
206+ const unSub2 = location . on ( 'navigationCancelled' , callback ) ;
146207
147208 // Act.
148209 globalThis . window . history . pushState ( null , '' , 'http://example.com/other' ) ;
149- flushSync ( ) ;
150210
151211 // Assert.
152212 expect ( callback ) . toHaveBeenCalledOnce ( ) ;
213+
214+ // Cleanup.
215+ unSub1 ( ) ;
216+ unSub2 ( ) ;
153217 } ) ;
154218 test ( "Should transfer the cause of cancellation and the state to the 'navigationCancelled' event." , ( ) => {
155219 // Arrange.
156220 const callback = vi . fn ( ) ;
157221 const reason = 'test' ;
158222 const state = { test : 'value' } ;
159- location . on ( 'beforeNavigate' , ( event ) => event . cancel ( reason ) ) ;
160- location . on ( 'navigationCancelled' , callback ) ;
223+ const unSub1 = location . on ( 'beforeNavigate' , ( event ) => event . cancel ( reason ) ) ;
224+ const unSub2 = location . on ( 'navigationCancelled' , callback ) ;
161225
162226 // Act.
163227 globalThis . window . history . pushState ( state , '' , 'http://example.com/other' ) ;
164- flushSync ( ) ;
165228
166229 // Assert.
167230 expect ( callback ) . toHaveBeenCalledWith ( { url : 'http://example.com/other' , cause : 'test' , method : 'push' , state } ) ;
231+
232+ // Cleanup.
233+ unSub1 ( ) ;
234+ unSub2 ( ) ;
168235 } ) ;
169236 } ) ;
170237 describe ( 'url' , ( ) => {
@@ -177,7 +244,6 @@ describe("LocationFull", () => {
177244
178245 // Act.
179246 globalThis . window . history [ fn ] ( null , '' , newUrl ) ;
180- flushSync ( ) ;
181247
182248 // Assert.
183249 expect ( location . url . href ) . toBe ( newUrl ) ;
@@ -193,12 +259,28 @@ describe("LocationFull", () => {
193259
194260 // Act.
195261 globalThis . window . history [ fn ] ( state , '' , 'http://example.com/new' ) ;
196- flushSync ( ) ;
197262
198263 // Assert.
199264 expect ( location . getState ( false ) ) . toEqual ( state . path ) ;
200265 expect ( location . getState ( true ) ) . toEqual ( state . hash . single ) ;
201266 expect ( location . getState ( 'p1' ) ) . toEqual ( state . hash . p1 ) ;
202267 } ) ;
203268 } ) ;
269+ describe ( 'Navigation Interception' , ( ) => {
270+ test . each ( [
271+ 'pushState' as const ,
272+ 'replaceState' as const ,
273+ ] ) ( "Should preserve the previous valid state whenever %s is called with non-conformant state." , ( stateFn ) => {
274+ // Arrange.
275+ const validState = { path : { test : 'value' } , hash : { } } ;
276+ globalThis . window . history [ stateFn ] ( validState , '' , 'http://example.com/' ) ;
277+ const state = { test : 'value' } ;
278+
279+ // Act.
280+ globalThis . window . history [ stateFn ] ( state , '' , 'http://example.com/other' ) ;
281+
282+ // Assert.
283+ expect ( globalThis . window . history . state ) . deep . equals ( validState ) ;
284+ } ) ;
285+ } ) ;
204286} ) ;
0 commit comments