From 8316366867781207b40c23a8274c29386ceac0b1 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ramirez=20Vargas=2C=20Jos=C3=A9=20Pablo?= Date: Sat, 27 Sep 2025 16:13:37 -0600 Subject: [PATCH] feat: Respect disallowed route modes in `Location.navigate()` --- src/lib/core/LocationLite.svelte.test.ts | 130 ++++++++++++++++++++++- src/lib/core/LocationLite.svelte.ts | 2 + src/testing/test-utils.ts | 1 + 3 files changed, 132 insertions(+), 1 deletion(-) diff --git a/src/lib/core/LocationLite.svelte.test.ts b/src/lib/core/LocationLite.svelte.test.ts index c816158..c9ea7fb 100644 --- a/src/lib/core/LocationLite.svelte.test.ts +++ b/src/lib/core/LocationLite.svelte.test.ts @@ -1,8 +1,10 @@ import { describe, test, expect, beforeEach, afterEach, vi } from "vitest"; import { LocationLite } from "./LocationLite.svelte.js"; -import type { Hash, HistoryApi } from "../types.js"; +import type { ExtendedRoutingOptions, Hash, HistoryApi, PreserveQuery } from "../types.js"; import { setupBrowserMocks, ALL_HASHES } from "../../testing/test-utils.js"; import { SvelteURL } from "svelte/reactivity"; +import { setLocation } from "./Location.js"; +import { resetRoutingOptions, setRoutingOptions } from "./options.js"; describe("LocationLite", () => { const initialUrl = "http://example.com/"; @@ -12,10 +14,12 @@ describe("LocationLite", () => { beforeEach(() => { browserMocks = setupBrowserMocks(initialUrl); location = new LocationLite(); + setLocation(location); }); afterEach(() => { location.dispose(); + setLocation(null); browserMocks.cleanup(); }); @@ -125,4 +129,128 @@ describe("LocationLite", () => { expect(location.getState('abc')).toBe(abcHashState); }); }); + describe('goTo', () => { + test.each<{ + qs: string; + preserveQuery: PreserveQuery; + text: string; + expectedQs: string; + }>([ + { + qs: 'some=value', + preserveQuery: false, + text: 'not preserve', + expectedQs: '', + }, + { + qs: 'some=value', + preserveQuery: true, + text: 'preserve', + expectedQs: '?some=value', + }, + { + qs: 'some=value&plus=another', + preserveQuery: 'plus', + text: 'preserve', + expectedQs: '?plus=another', + }, + { + qs: 'some=value&plus=another&extra=thing', + preserveQuery: ['plus', 'extra'], + text: 'preserve', + expectedQs: '?plus=another&extra=thing', + }, + ]) + ("Should $text the query string when instructed by the value $preserveQuery in the preserveQuery option.", ({ qs, preserveQuery, expectedQs }) => { + // Arrange. + location.url.search = window.location.search = qs; + const newPath = '/new/path'; + + // Act. + location.goTo(newPath, { preserveQuery }); + + // Assert. + expect(window.location.pathname).to.equal(newPath); + expect(window.location.search).to.equal(`${expectedQs}`); + }); + }); + describe('navigate', () => { + afterEach(() => { + resetRoutingOptions(); + }); + + test.each<{ + qs: string; + preserveQuery: PreserveQuery; + text: string; + expectedQs: string; + }>([ + { + qs: 'some=value', + preserveQuery: false, + text: 'not preserve', + expectedQs: '', + }, + { + qs: 'some=value', + preserveQuery: true, + text: 'preserve', + expectedQs: '?some=value', + }, + { + qs: 'some=value&plus=another', + preserveQuery: 'plus', + text: 'preserve', + expectedQs: '?plus=another', + }, + { + qs: 'some=value&plus=another&extra=thing', + preserveQuery: ['plus', 'extra'], + text: 'preserve', + expectedQs: '?plus=another&extra=thing', + }, + ])("Should $text the query string when instructed by the value $preserveQuery in the preserveQuery option.", ({ qs, preserveQuery, expectedQs }) => { + // Arrange. + location.url.search = window.location.search = qs; + const newPath = '/new/path'; + + // Act. + location.navigate(newPath, { preserveQuery }); + + // Assert. + expect(window.location.pathname).to.equal(newPath); + expect(window.location.search).to.equal(`${expectedQs}`); + }); + test.each<{ + hash: Hash; + desc: string; + options: ExtendedRoutingOptions; + }>([ + { + hash: ALL_HASHES.path, + desc: 'path', + options: { disallowPathRouting: true} + }, + { + hash: ALL_HASHES.single, + desc: 'hash', + options: { disallowHashRouting: true} + }, + { + hash: ALL_HASHES.multi, + desc: 'multi hash', + options: { disallowMultiHashRouting: true} + }, + ])("Should throw an error when the hash option is $hash and $desc routing is disallowed.", ({ hash, options }) => { + // Arrange. + const newPath = '/new/path'; + setRoutingOptions(options); + + // Act. + const act = () => location.navigate(newPath, { hash }); + + // Assert. + expect(act).toThrow(); + }); + }); }); diff --git a/src/lib/core/LocationLite.svelte.ts b/src/lib/core/LocationLite.svelte.ts index 35f76df..3cf33c3 100644 --- a/src/lib/core/LocationLite.svelte.ts +++ b/src/lib/core/LocationLite.svelte.ts @@ -6,6 +6,7 @@ import { resolveHashValue } from "./resolveHashValue.js"; import { calculateHref } from "./calculateHref.js"; import { calculateState } from "./calculateState.js"; import { preserveQueryInUrl } from "./preserveQuery.js"; +import { assertAllowedRoutingMode } from "$lib/utils.js"; /** * A lite version of the location object. It does not support event listeners or state-setting call interceptions, @@ -83,6 +84,7 @@ export class LocationLite implements Location { navigate(url: string, options?: NavigateOptions): void { const resolvedHash = resolveHashValue(options?.hash); + assertAllowedRoutingMode(resolvedHash); if (url !== '') { url = calculateHref({ ...options, diff --git a/src/testing/test-utils.ts b/src/testing/test-utils.ts index 191b66b..05cd76a 100644 --- a/src/testing/test-utils.ts +++ b/src/testing/test-utils.ts @@ -170,6 +170,7 @@ export function createLocationMock(initialUrl = "http://example.com/") { // Add other location properties as needed get pathname() { return _url.pathname; }, get search() { return _url.search; }, + set search(value: string) { _url.search = value; }, get hash() { return _url.hash; }, get origin() { return _url.origin; }, get protocol() { return _url.protocol; },