11import { describe , test , expect , beforeAll , afterAll , beforeEach } from "vitest" ;
22import { calculateHref , type CalculateHrefOptions } from "./calculateHref.js" ;
33import { init , location } from "$lib/index.js" ;
4+ import { ROUTING_UNIVERSES , ALL_HASHES } from "../../testing/test-utils.js" ;
45
56describe ( "calculateHref" , ( ) => {
67 describe ( "(...paths) Overload" , ( ) => {
@@ -24,151 +25,231 @@ describe("calculateHref", () => {
2425 inputPaths : [ "path#hash" , "anotherPath#hash2" ] ,
2526 expectedHref : "path/anotherPath#hash" ,
2627 }
27- ] ) ( "Should combine paths $inputPaths as $expectedHref . " , ( { inputPaths, expectedHref } ) => {
28- // Act.
28+ ] ) ( "Should combine paths $inputPaths as $expectedHref" , ( { inputPaths, expectedHref } ) => {
29+ // Act
2930 const href = calculateHref ( ...inputPaths ) ;
3031
31- // Assert.
32+ // Assert
3233 expect ( href ) . toBe ( expectedHref ) ;
3334 } ) ;
3435 } ) ;
35- describe ( "(options, ...paths) Overload" , ( ) => {
36+
37+ // Test across all routing universes for comprehensive coverage
38+ ROUTING_UNIVERSES . forEach ( ( universe ) => {
39+ describe ( `(options, ...paths) Overload - ${ universe . text } ` , ( ) => {
40+ let cleanup : Function ;
41+
42+ beforeAll ( ( ) => {
43+ cleanup = init ( {
44+ implicitMode : universe . implicitMode ,
45+ hashMode : universe . hashMode
46+ } ) ;
47+ } ) ;
48+
49+ afterAll ( ( ) => {
50+ cleanup ( ) ;
51+ } ) ;
52+
53+ const basePath = "/base/path" ;
54+ const baseHash = universe . hashMode === 'multi'
55+ ? "#p1=path/one;p2=path/two"
56+ : "#base/hash" ;
57+
58+ beforeEach ( ( ) => {
59+ location . url . href = `https://example.com${ basePath } ${ baseHash } ` ;
60+ } ) ;
61+
62+ describe ( "Basic navigation" , ( ) => {
63+ test . each ( [
64+ {
65+ opts : { hash : universe . hash , preserveHash : false } ,
66+ url : '/sample/path' ,
67+ expectedHref : ( ( ) => {
68+ if ( universe . hash === ALL_HASHES . path ) return '/sample/path' ;
69+ if ( universe . hash === ALL_HASHES . single ) return '#/sample/path' ;
70+ if ( universe . hash === ALL_HASHES . implicit ) {
71+ return universe . implicitMode === 'path' ? '/sample/path' : '#/sample/path' ;
72+ }
73+ // Multi-hash routing - preserves existing paths and adds/updates the specified hash
74+ if ( typeof universe . hash === 'string' ) {
75+ // This will preserve existing paths and update/add the new one
76+ return `#${ universe . hash } =/sample/path;p2=path/two` ;
77+ }
78+ return '/sample/path' ;
79+ } ) ( ) ,
80+ text : "create correct href without preserving hash" ,
81+ } ,
82+ {
83+ opts : { hash : universe . hash , preserveHash : true } ,
84+ url : '/sample/path' ,
85+ expectedHref : ( ( ) => {
86+ if ( universe . hash === ALL_HASHES . path ) return `/sample/path${ baseHash } ` ;
87+ if ( universe . hash === ALL_HASHES . single ) return '#/sample/path' ;
88+ if ( universe . hash === ALL_HASHES . implicit ) {
89+ return universe . implicitMode === 'path' ? `/sample/path${ baseHash } ` : '#/sample/path' ;
90+ }
91+ // Multi-hash routing - preserveHash doesn't apply to hash routing
92+ if ( typeof universe . hash === 'string' ) {
93+ return `#${ universe . hash } =/sample/path;p2=path/two` ;
94+ }
95+ return `/sample/path${ baseHash } ` ;
96+ } ) ( ) ,
97+ text : "handle hash preservation correctly" ,
98+ } ,
99+ ] ) ( "Should $text in ${universe.text}" , ( { opts, url, expectedHref } ) => {
100+ // Act
101+ const href = calculateHref ( opts , url ) ;
102+
103+ // Assert
104+ expect ( href ) . toBe ( expectedHref ) ;
105+ } ) ;
106+ } ) ;
107+
108+ describe ( "Query string preservation" , ( ) => {
109+ test . each ( [
110+ { preserveQuery : true , text : 'preserve' } ,
111+ { preserveQuery : false , text : 'not preserve' } ,
112+ ] ) ( "Should $text the query string in ${universe.text}" , ( { preserveQuery } ) => {
113+ // Arrange
114+ const newPath = "/sample/path" ;
115+ const query = "a=b&c=d" ;
116+ location . url . search = `?${ query } ` ;
117+
118+ const expectedHref = ( ( ) => {
119+ const baseHref = ( ( ) => {
120+ if ( universe . hash === ALL_HASHES . path ) return newPath ;
121+ if ( universe . hash === ALL_HASHES . single ) return `#${ newPath } ` ;
122+ if ( universe . hash === ALL_HASHES . implicit ) {
123+ return universe . implicitMode === 'path' ? newPath : `#${ newPath } ` ;
124+ }
125+ // Multi-hash routing
126+ if ( typeof universe . hash === 'string' ) {
127+ return `#${ universe . hash } =${ newPath } ;p2=path/two` ;
128+ }
129+ return newPath ;
130+ } ) ( ) ;
131+
132+ if ( ! preserveQuery ) return baseHref ;
133+
134+ // Add query string
135+ if ( baseHref . startsWith ( '#' ) ) {
136+ return `?${ query } ${ baseHref } ` ;
137+ } else {
138+ return `${ baseHref } ?${ query } ` ;
139+ }
140+ } ) ( ) ;
141+
142+ // Act
143+ const href = calculateHref ( { hash : universe . hash , preserveQuery } , newPath ) ;
144+
145+ // Assert
146+ expect ( href ) . toBe ( expectedHref ) ;
147+ } ) ;
148+ } ) ;
149+
150+ if ( universe . hashMode === 'multi' ) {
151+ describe ( "Multi-hash routing behavior" , ( ) => {
152+ test ( "Should preserve all existing paths when adding a new path" , ( ) => {
153+ // Arrange
154+ const newPath = "/sample/path" ;
155+ const newHashId = 'new' ;
156+
157+ // Act
158+ const href = calculateHref ( { hash : newHashId } , newPath ) ;
159+
160+ // Assert
161+ expect ( href ) . toBe ( `${ baseHash } ;${ newHashId } =${ newPath } ` ) ;
162+ } ) ;
163+
164+ test ( "Should preserve all existing paths when updating an existing path" , ( ) => {
165+ // Arrange
166+ const newPath = "/sample/path" ;
167+ const existingHashId = 'p1' ;
168+ const expected = baseHash . replace ( / ( p 1 = ) .+ ; / i, `$1${ newPath } ;` ) ;
169+
170+ // Act
171+ const href = calculateHref ( { hash : existingHashId } , newPath ) ;
172+
173+ // Assert
174+ expect ( href ) . toEqual ( expected ) ;
175+ } ) ;
176+ } ) ;
177+ }
178+
179+ if ( universe . hash === ALL_HASHES . implicit ) {
180+ describe ( "Implicit hash resolution" , ( ) => {
181+ test ( "Should resolve implicit hash according to implicitMode" , ( ) => {
182+ // Arrange
183+ const newPath = "/sample/path" ;
184+ const expectedHref = universe . implicitMode === 'path' ? newPath : `#${ newPath } ` ;
185+
186+ // Act
187+ const href = calculateHref ( { hash : universe . hash } , newPath ) ;
188+
189+ // Assert
190+ expect ( href ) . toBe ( expectedHref ) ;
191+ } ) ;
192+ } ) ;
193+ }
194+ } ) ;
195+ } ) ;
196+
197+ describe ( "HREF Validation" , ( ) => {
36198 let cleanup : Function ;
199+
37200 beforeAll ( ( ) => {
38201 cleanup = init ( ) ;
39202 } ) ;
203+
40204 afterAll ( ( ) => {
41205 cleanup ( ) ;
42206 } ) ;
43- const basePath = "/base/path" ;
44- const baseHash = "#base/hash" ;
45- beforeEach ( ( ) => {
46- location . url . href = `https://example.com${ basePath } ${ baseHash } ` ;
47- } ) ;
48- test . each < {
49- opts : CalculateHrefOptions ;
50- url : string ;
51- expectedHref : string ;
52- text : string ;
53- textMode : string ;
54- } > ( [
55- {
56- opts : { hash : false , preserveHash : false } ,
57- url : '/sample/path' ,
58- expectedHref : '/sample/path' ,
59- text : "not preserve hash" ,
60- textMode : "path routing" ,
61- } ,
62- {
63- opts : { hash : false , preserveHash : true } ,
64- url : '/sample/path' ,
65- expectedHref : `/sample/path${ baseHash } ` ,
66- text : "preserve hash" ,
67- textMode : "path routing" ,
68- } ,
69- {
70- opts : { hash : true } ,
71- url : '/sample/path' ,
72- expectedHref : '#/sample/path' ,
73- text : "ignore hash" ,
74- textMode : "hash routing" ,
75- } ,
76- ] ) ( "Should $text from the URL for $textMode ." , ( { opts, url, expectedHref } ) => {
77- // Arrange.
78-
79- // Act.
80- const href = calculateHref ( opts , url ) ;
81207
82- // Assert.
83- expect ( href ) . toBe ( expectedHref ) ;
208+ test ( "Should reject HREF with http protocol" , ( ) => {
209+ expect ( ( ) => calculateHref ( "http://example.com/path" ) )
210+ . toThrow ( 'HREF cannot contain protocol, host, or port. Received: "http://example.com/path"' ) ;
84211 } ) ;
85- test ( "Should create a hash HREF when the 'hash' property is set to true." , ( ) => {
86- // Arrange.
87- const newPath = "/sample/path" ;
88- const hash = true ;
89212
90- // Act.
91- const href = calculateHref ( { hash } , newPath ) ;
92-
93- // Assert.
94- expect ( href ) . toBe ( `#${ newPath } ` ) ;
213+ test ( "Should reject HREF with https protocol" , ( ) => {
214+ expect ( ( ) => calculateHref ( "https://example.com/path" ) )
215+ . toThrow ( 'HREF cannot contain protocol, host, or port. Received: "https://example.com/path"' ) ;
95216 } ) ;
96- test . each ( [
97- {
98- hash : false ,
99- preserveQuery : true ,
100- text : 'preserve' ,
101- textMode : 'path routing' ,
102- } ,
103- {
104- hash : false ,
105- preserveQuery : false ,
106- text : 'not preserve' ,
107- textMode : 'path routing' ,
108- } ,
109- {
110- hash : true ,
111- preserveQuery : true ,
112- text : 'preserve' ,
113- textMode : 'hash routing' ,
114- } ,
115- {
116- hash : true ,
117- preserveQuery : false ,
118- text : 'not preserve' ,
119- textMode : 'hash routing' ,
120- } ,
121- ] ) ( "Should $text the query string when 'preserveQuery' is $preserveQuery under the $textMode mode." , ( { hash, preserveQuery } ) => {
122- // Arrange.
123- const newPath = "/sample/path" ;
124- const query = "a=b&c=d" ;
125- location . url . search = query ;
126- const expected = hash ?
127- ( preserveQuery ? `?${ query } #${ newPath } ` : `#${ newPath } ` ) :
128- ( preserveQuery ? `${ newPath } ?${ query } ` : newPath ) ;
129-
130- // Act.
131- const href = calculateHref ( { hash, preserveQuery } , newPath ) ;
132217
133- // Assert.
134- expect ( href ) . toBe ( expected ) ;
218+ test ( "Should reject HREF with ftp protocol" , ( ) => {
219+ expect ( ( ) => calculateHref ( "ftp://example.com/path" ) )
220+ . toThrow ( 'HREF cannot contain protocol, host, or port. Received: "ftp://example.com/path"' ) ;
135221 } ) ;
136- } ) ;
137- describe ( "(options, ...paths) Overload - Multi Hash Routing" , ( ) => {
138- let cleanup : Function ;
139- beforeAll ( ( ) => {
140- cleanup = init ( { hashMode : 'multi' } ) ;
222+
223+ test ( "Should reject HREF with protocol-relative URL" , ( ) => {
224+ expect ( ( ) => calculateHref ( "//example.com/path" ) )
225+ . toThrow ( 'HREF cannot contain protocol, host, or port. Received: "//example.com/path"' ) ;
141226 } ) ;
142- afterAll ( ( ) => {
143- cleanup ( ) ;
227+
228+ test ( "Should reject HREF with custom protocol" , ( ) => {
229+ expect ( ( ) => calculateHref ( "custom-protocol://example.com/path" ) )
230+ . toThrow ( 'HREF cannot contain protocol, host, or port. Received: "custom-protocol://example.com/path"' ) ;
144231 } ) ;
145- const basePath = "/base/path" ;
146- const baseHash = "#p1=path/one;p2=path/two" ;
147- beforeEach ( ( ) => {
148- location . url . href = ` https://example.com${ basePath } ${ baseHash } ` ;
232+
233+ test ( "Should reject HREF when passed in options overload" , ( ) => {
234+ expect ( ( ) => calculateHref ( { } , "https://example.com/path" ) )
235+ . toThrow ( 'HREF cannot contain protocol, host, or port. Received: " https://example.com/path"' ) ;
149236 } ) ;
150- test ( "Should preserve all existing paths in the URL's hash when adding a new path." , ( ) => {
151- // Arrange.
152- const newPath = "/sample/path" ;
153- const hash = 'new' ;
154237
155- // Act.
156- const href = calculateHref ( { hash } , newPath ) ;
238+ test ( "Should reject HREF among multiple valid paths" , ( ) => {
239+ expect ( ( ) => calculateHref ( "/valid/path" , "https://example.com/invalid" , "/another/valid" ) )
240+ . toThrow ( 'HREF cannot contain protocol, host, or port. Received: "https://example.com/invalid"' ) ;
241+ } ) ;
157242
158- // Assert.
159- expect ( href ) . toBe ( ` ${ baseHash } ; ${ hash } = ${ newPath } ` ) ;
243+ test ( "Should allow valid relative paths" , ( ) => {
244+ expect ( ( ) => calculateHref ( "/path" , "relative/path" , "../other/path" ) ) . not . toThrow ( ) ;
160245 } ) ;
161- test ( "Should preserve all existing paths in the URL's hash when updating an existing path." , ( ) => {
162- // Arrange.
163- const newPath = "/sample/path" ;
164- const hash = 'p1' ;
165- const expected = baseHash . replace ( / ( p 1 = ) .+ ; / i, `$1${ newPath } ;` ) ;
166246
167- // Act.
168- const href = calculateHref ( { hash } , newPath ) ;
247+ test ( "Should allow valid paths with query and hash" , ( ) => {
248+ expect ( ( ) => calculateHref ( "/path?query=value" , "relative/path#hash" ) ) . not . toThrow ( ) ;
249+ } ) ;
169250
170- // Assert.
171- expect ( href ) . toEqual ( expected ) ;
251+ test ( "Should allow paths that start with protocol-like strings but are not URLs" , ( ) => {
252+ expect ( ( ) => calculateHref ( "/http-endpoint" , "/https-folder" ) ) . not . toThrow ( ) ;
172253 } ) ;
173254 } ) ;
174255} ) ;
0 commit comments