1+ import { describe , test , expect , beforeAll , afterAll , beforeEach } from 'vitest' ;
2+ import { buildHref } from './buildHref.js' ;
3+ import { init } from './init.js' ;
4+ import { location } from './kernel/Location.js' ;
5+
6+ describe ( 'buildHref' , ( ) => {
7+ let cleanup : Function ;
8+ beforeAll ( ( ) => {
9+ cleanup = init ( ) ;
10+ } ) ;
11+ afterAll ( ( ) => {
12+ cleanup ( ) ;
13+ } ) ;
14+
15+ beforeEach ( ( ) => {
16+ // Reset to a clean base URL for each test
17+ location . url . href = 'https://example.com/current?currentParam=value' ;
18+ } ) ;
19+
20+ describe ( 'Basic functionality' , ( ) => {
21+ test ( 'Should combine path from first HREF and hash from second HREF.' , ( ) => {
22+ const pathPiece = 'https://example.com/new-path' ;
23+ const hashPiece = 'https://example.com/any-path#new-hash' ;
24+
25+ const result = buildHref ( pathPiece , hashPiece ) ;
26+
27+ expect ( result ) . toBe ( '/new-path#new-hash' ) ;
28+ } ) ;
29+
30+ test ( 'Should handle relative URLs correctly.' , ( ) => {
31+ const pathPiece = '/relative-path' ;
32+ const hashPiece = '/any-path#relative-hash' ;
33+
34+ const result = buildHref ( pathPiece , hashPiece ) ;
35+
36+ expect ( result ) . toBe ( '/relative-path#relative-hash' ) ;
37+ } ) ;
38+
39+ test ( 'Should work when pathPiece has no path component.' , ( ) => {
40+ const pathPiece = 'https://example.com/' ;
41+ const hashPiece = 'https://example.com/#hash-only' ;
42+
43+ const result = buildHref ( pathPiece , hashPiece ) ;
44+
45+ expect ( result ) . toBe ( '/#hash-only' ) ;
46+ } ) ;
47+
48+ test ( 'Should work when hashPiece has no hash component.' , ( ) => {
49+ const pathPiece = 'https://example.com/path-only' ;
50+ const hashPiece = 'https://example.com/any-path' ;
51+
52+ const result = buildHref ( pathPiece , hashPiece ) ;
53+
54+ expect ( result ) . toBe ( '/path-only' ) ;
55+ } ) ;
56+
57+ test ( 'Should handle empty hash correctly.' , ( ) => {
58+ const pathPiece = 'https://example.com/path' ;
59+ const hashPiece = 'https://example.com/any-path#' ;
60+
61+ const result = buildHref ( pathPiece , hashPiece ) ;
62+
63+ expect ( result ) . toBe ( '/path' ) ;
64+ } ) ;
65+ } ) ;
66+
67+ describe ( 'Query parameter merging' , ( ) => {
68+ test ( 'Should merge query parameters from both pieces.' , ( ) => {
69+ const pathPiece = 'https://example.com/path?pathParam=pathValue' ;
70+ const hashPiece = 'https://example.com/any-path?hashParam=hashValue#hash' ;
71+
72+ const result = buildHref ( pathPiece , hashPiece ) ;
73+
74+ expect ( result ) . toBe ( '/path?pathParam=pathValue&hashParam=hashValue#hash' ) ;
75+ } ) ;
76+
77+ test ( 'Should handle query parameters in pathPiece only.' , ( ) => {
78+ const pathPiece = 'https://example.com/path?onlyPath=value' ;
79+ const hashPiece = 'https://example.com/any-path#hash' ;
80+
81+ const result = buildHref ( pathPiece , hashPiece ) ;
82+
83+ expect ( result ) . toBe ( '/path?onlyPath=value#hash' ) ;
84+ } ) ;
85+
86+ test ( 'Should handle query parameters in hashPiece only.' , ( ) => {
87+ const pathPiece = 'https://example.com/path' ;
88+ const hashPiece = 'https://example.com/any-path?onlyHash=value#hash' ;
89+
90+ const result = buildHref ( pathPiece , hashPiece ) ;
91+
92+ expect ( result ) . toBe ( '/path?onlyHash=value#hash' ) ;
93+ } ) ;
94+
95+ test ( 'Should handle duplicate parameter names by keeping both values.' , ( ) => {
96+ const pathPiece = 'https://example.com/path?shared=pathValue' ;
97+ const hashPiece = 'https://example.com/any-path?shared=hashValue#hash' ;
98+
99+ const result = buildHref ( pathPiece , hashPiece ) ;
100+
101+ expect ( result ) . toBe ( '/path?shared=pathValue&shared=hashValue#hash' ) ;
102+ } ) ;
103+
104+ test ( 'Should handle multiple parameters in both pieces.' , ( ) => {
105+ const pathPiece = 'https://example.com/path?param1=value1¶m2=value2' ;
106+ const hashPiece = 'https://example.com/any-path?param3=value3¶m4=value4#hash' ;
107+
108+ const result = buildHref ( pathPiece , hashPiece ) ;
109+
110+ expect ( result ) . toBe ( '/path?param1=value1¶m2=value2¶m3=value3¶m4=value4#hash' ) ;
111+ } ) ;
112+
113+ test ( 'Should work with empty query strings.' , ( ) => {
114+ const pathPiece = 'https://example.com/path?' ;
115+ const hashPiece = 'https://example.com/any-path?#hash' ;
116+
117+ const result = buildHref ( pathPiece , hashPiece ) ;
118+
119+ expect ( result ) . toBe ( '/path#hash' ) ;
120+ } ) ;
121+ } ) ;
122+
123+ describe ( 'preserveQuery option' , ( ) => {
124+ beforeEach ( ( ) => {
125+ // Set up current URL with query parameters to preserve
126+ location . url . href = 'https://example.com/current?preserve1=value1&preserve2=value2&preserve3=value3' ;
127+ } ) ;
128+
129+ test ( 'Should preserve all current query parameters when preserveQuery is true.' , ( ) => {
130+ const pathPiece = 'https://example.com/path?new=param' ;
131+ const hashPiece = 'https://example.com/any-path#hash' ;
132+
133+ const result = buildHref ( pathPiece , hashPiece , { preserveQuery : true } ) ;
134+
135+ expect ( result ) . toBe ( '/path?new=param&preserve1=value1&preserve2=value2&preserve3=value3#hash' ) ;
136+ } ) ;
137+
138+ test ( 'Should preserve specific query parameter when preserveQuery is a string.' , ( ) => {
139+ const pathPiece = 'https://example.com/path' ;
140+ const hashPiece = 'https://example.com/any-path#hash' ;
141+
142+ const result = buildHref ( pathPiece , hashPiece , { preserveQuery : 'preserve2' } ) ;
143+
144+ expect ( result ) . toBe ( '/path?preserve2=value2#hash' ) ;
145+ } ) ;
146+
147+ test ( 'Should preserve specific query parameters when preserveQuery is an array.' , ( ) => {
148+ const pathPiece = 'https://example.com/path' ;
149+ const hashPiece = 'https://example.com/any-path#hash' ;
150+
151+ const result = buildHref ( pathPiece , hashPiece , { preserveQuery : [ 'preserve1' , 'preserve3' ] } ) ;
152+
153+ expect ( result ) . toBe ( '/path?preserve1=value1&preserve3=value3#hash' ) ;
154+ } ) ;
155+
156+ test ( 'Should not preserve any parameters when preserveQuery is false.' , ( ) => {
157+ const pathPiece = 'https://example.com/path?new=param' ;
158+ const hashPiece = 'https://example.com/any-path#hash' ;
159+
160+ const result = buildHref ( pathPiece , hashPiece , { preserveQuery : false } ) ;
161+
162+ expect ( result ) . toBe ( '/path?new=param#hash' ) ;
163+ } ) ;
164+
165+ test ( 'Should not preserve any parameters when preserveQuery is not specified.' , ( ) => {
166+ const pathPiece = 'https://example.com/path?new=param' ;
167+ const hashPiece = 'https://example.com/any-path#hash' ;
168+
169+ const result = buildHref ( pathPiece , hashPiece ) ;
170+
171+ expect ( result ) . toBe ( '/path?new=param#hash' ) ;
172+ } ) ;
173+
174+ test ( 'Should handle preserveQuery with existing merged parameters.' , ( ) => {
175+ const pathPiece = 'https://example.com/path?fromPath=pathVal' ;
176+ const hashPiece = 'https://example.com/any-path?fromHash=hashVal#hash' ;
177+
178+ const result = buildHref ( pathPiece , hashPiece , { preserveQuery : 'preserve2' } ) ;
179+
180+ expect ( result ) . toBe ( '/path?fromPath=pathVal&fromHash=hashVal&preserve2=value2#hash' ) ;
181+ } ) ;
182+
183+ test ( 'Should handle non-existent preserve parameter gracefully.' , ( ) => {
184+ const pathPiece = 'https://example.com/path' ;
185+ const hashPiece = 'https://example.com/any-path#hash' ;
186+
187+ const result = buildHref ( pathPiece , hashPiece , { preserveQuery : 'nonExistent' } ) ;
188+
189+ expect ( result ) . toBe ( '/path#hash' ) ;
190+ } ) ;
191+ } ) ;
192+
193+ describe ( 'Edge cases' , ( ) => {
194+ test ( 'Should handle both pieces being the same URL.' , ( ) => {
195+ const sameUrl = 'https://example.com/same?param=value#hash' ;
196+
197+ const result = buildHref ( sameUrl , sameUrl ) ;
198+
199+ expect ( result ) . toBe ( '/same?param=value¶m=value#hash' ) ;
200+ } ) ;
201+
202+ test ( 'Should handle URLs with different domains.' , ( ) => {
203+ const pathPiece = 'https://other-domain.com/path?param=value' ;
204+ const hashPiece = 'https://another-domain.com/any-path#hash' ;
205+
206+ const result = buildHref ( pathPiece , hashPiece ) ;
207+
208+ expect ( result ) . toBe ( '/path?param=value#hash' ) ;
209+ } ) ;
210+
211+ test ( 'Should handle URLs with special characters in parameters.' , ( ) => {
212+ const pathPiece = 'https://example.com/path?special=hello%20world' ;
213+ const hashPiece = 'https://example.com/any-path?encoded=test%2Bvalue#hash%20with%20spaces' ;
214+
215+ const result = buildHref ( pathPiece , hashPiece ) ;
216+
217+ expect ( result ) . toBe ( '/path?special=hello+world&encoded=test%2Bvalue#hash%20with%20spaces' ) ;
218+ } ) ;
219+
220+ test ( 'Should handle root paths correctly.' , ( ) => {
221+ const pathPiece = 'https://example.com/' ;
222+ const hashPiece = 'https://example.com/#root-hash' ;
223+
224+ const result = buildHref ( pathPiece , hashPiece ) ;
225+
226+ expect ( result ) . toBe ( '/#root-hash' ) ;
227+ } ) ;
228+
229+ test ( 'Should handle complex hash fragments.' , ( ) => {
230+ const pathPiece = 'https://example.com/path' ;
231+ const hashPiece = 'https://example.com/any-path#/complex/hash/route?hashParam=value' ;
232+
233+ const result = buildHref ( pathPiece , hashPiece ) ;
234+
235+ expect ( result ) . toBe ( '/path#/complex/hash/route?hashParam=value' ) ;
236+ } ) ;
237+ } ) ;
238+
239+ describe ( 'Cross-universe redirection use case' , ( ) => {
240+ test ( 'Should support typical cross-universe redirection scenario.' , ( ) => {
241+ // Simulate getting path piece from path router and hash piece from hash router
242+ const pathUniverseHref = 'https://example.com/users/profile?pathParam=value' ;
243+ const hashUniverseHref = 'https://example.com/current#/dashboard/settings?hashParam=value' ;
244+
245+ const result = buildHref ( pathUniverseHref , hashUniverseHref ) ;
246+
247+ expect ( result ) . toBe ( '/users/profile?pathParam=value#/dashboard/settings?hashParam=value' ) ;
248+ } ) ;
249+
250+ test ( 'Should handle preserving current query in cross-universe scenario.' , ( ) => {
251+ location . url . href = 'https://example.com/current?globalParam=global&session=active' ;
252+
253+ const pathUniverseHref = 'https://example.com/users/profile' ;
254+ const hashUniverseHref = 'https://example.com/current#/dashboard' ;
255+
256+ const result = buildHref ( pathUniverseHref , hashUniverseHref , { preserveQuery : [ 'session' ] } ) ;
257+
258+ expect ( result ) . toBe ( '/users/profile?session=active#/dashboard' ) ;
259+ } ) ;
260+ } ) ;
261+
262+ describe ( 'Additional edge cases' , ( ) => {
263+ test ( 'Should handle URL fragments with encoded characters.' , ( ) => {
264+ const pathPiece = 'https://example.com/path' ;
265+ const hashPiece = 'https://example.com/any#%20encoded%20hash' ;
266+
267+ const result = buildHref ( pathPiece , hashPiece ) ;
268+
269+ expect ( result ) . toBe ( '/path#%20encoded%20hash' ) ;
270+ } ) ;
271+
272+ test ( 'Should handle when both pieces have same domain but different protocols.' , ( ) => {
273+ const pathPiece = 'http://example.com/path' ;
274+ const hashPiece = 'https://example.com/other#hash' ;
275+
276+ const result = buildHref ( pathPiece , hashPiece ) ;
277+
278+ expect ( result ) . toBe ( '/path#hash' ) ;
279+ } ) ;
280+
281+ test ( 'Should handle query parameters with empty values.' , ( ) => {
282+ const pathPiece = 'https://example.com/path?empty=' ;
283+ const hashPiece = 'https://example.com/other?also=&blank=#hash' ;
284+
285+ const result = buildHref ( pathPiece , hashPiece ) ;
286+
287+ expect ( result ) . toBe ( '/path?empty=&also=&blank=#hash' ) ;
288+ } ) ;
289+
290+ test ( 'Should handle preserveQuery with empty current URL query.' , ( ) => {
291+ location . url . href = 'https://example.com/current' ; // No query parameters
292+
293+ const pathPiece = 'https://example.com/path?new=param' ;
294+ const hashPiece = 'https://example.com/other#hash' ;
295+
296+ const result = buildHref ( pathPiece , hashPiece , { preserveQuery : true } ) ;
297+
298+ expect ( result ) . toBe ( '/path?new=param#hash' ) ;
299+ } ) ;
300+
301+ test ( 'Should handle complex multi-hash routing fragment.' , ( ) => {
302+ const pathPiece = 'https://example.com/app' ;
303+ const hashPiece = 'https://example.com/other#main=/dashboard;sidebar=/menu' ;
304+
305+ const result = buildHref ( pathPiece , hashPiece ) ;
306+
307+ expect ( result ) . toBe ( '/app#main=/dashboard;sidebar=/menu' ) ;
308+ } ) ;
309+ } ) ;
310+ } ) ;
0 commit comments