Skip to content

Commit 1ca5abe

Browse files
authored
feat: Allow extension packages to disallow routing modes (#79)
1 parent e5a137d commit 1ca5abe

File tree

15 files changed

+392
-21
lines changed

15 files changed

+392
-21
lines changed

src/lib/Fallback/Fallback.svelte

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@
22
import { resolveHashValue } from '$lib/core/resolveHashValue.js';
33
import { getRouterContext } from '$lib/Router/Router.svelte';
44
import type { RouteStatus, WhenPredicate } from '$lib/types.js';
5+
import { assertAllowedRoutingMode } from '$lib/utils.js';
56
import type { Snippet } from 'svelte';
67
78
type Props = {
@@ -63,7 +64,10 @@
6364
6465
let { hash, when, children }: Props = $props();
6566
66-
const router = getRouterContext(resolveHashValue(hash));
67+
const resolvedHash = resolveHashValue(hash);
68+
assertAllowedRoutingMode(resolvedHash);
69+
70+
const router = getRouterContext(resolvedHash);
6771
</script>
6872

6973
{#if (router && when?.(router.routeStatus, router.noMatches)) || (!when && router?.noMatches)}

src/lib/Fallback/Fallback.svelte.test.ts

Lines changed: 54 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,8 +2,10 @@ import { init } from "$lib/init.js";
22
import { describe, test, expect, beforeAll, afterAll, beforeEach } from "vitest";
33
import { render } from "@testing-library/svelte";
44
import Fallback from "./Fallback.svelte";
5-
import { addMatchingRoute, addRoutes, createRouterTestSetup, createTestSnippet, ROUTING_UNIVERSES } from "../../testing/test-utils.js";
5+
import { addMatchingRoute, addRoutes, createRouterTestSetup, createTestSnippet, ROUTING_UNIVERSES, ALL_HASHES } from "../../testing/test-utils.js";
66
import { flushSync } from "svelte";
7+
import { resetRoutingOptions, setRoutingOptions } from "$lib/core/options.js";
8+
import type { ExtendedRoutingOptions } from "$lib/types.js";
79

810
function defaultPropsTests(setup: ReturnType<typeof createRouterTestSetup>) {
911
const contentText = "Fallback content.";
@@ -161,6 +163,57 @@ function reactivityTests(setup: ReturnType<typeof createRouterTestSetup>) {
161163
});
162164
}
163165

166+
describe("Routing Mode Assertions", () => {
167+
const contentText = "Fallback content.";
168+
const content = createTestSnippet(contentText);
169+
let cleanup: () => void;
170+
171+
beforeAll(() => {
172+
cleanup = init();
173+
});
174+
175+
beforeEach(() => {
176+
resetRoutingOptions();
177+
});
178+
179+
afterAll(() => {
180+
resetRoutingOptions();
181+
cleanup();
182+
});
183+
184+
test.each<{
185+
options: Partial<ExtendedRoutingOptions>;
186+
hash: typeof ALL_HASHES[keyof typeof ALL_HASHES];
187+
description: string;
188+
}>([
189+
{
190+
options: { disallowHashRouting: true },
191+
hash: ALL_HASHES.single,
192+
description: 'hash routing is disallowed'
193+
},
194+
{
195+
options: { disallowMultiHashRouting: true },
196+
hash: ALL_HASHES.multi,
197+
description: 'multi-hash routing is disallowed'
198+
},
199+
{
200+
options: { disallowPathRouting: true },
201+
hash: ALL_HASHES.path,
202+
description: 'path routing is disallowed'
203+
}
204+
])("Should throw error when $description and hash=$hash .", ({ options, hash }) => {
205+
// Arrange
206+
setRoutingOptions(options);
207+
208+
// Act & Assert
209+
expect(() => {
210+
render(Fallback, {
211+
props: { hash, children: content },
212+
});
213+
}).toThrow();
214+
});
215+
});
216+
164217
ROUTING_UNIVERSES.forEach(ru => {
165218
describe(`Fallback - ${ru.text}`, () => {
166219
const setup = createRouterTestSetup(ru.hash);

src/lib/Link/Link.svelte

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@
66
import { getLinkContext, type ILinkContext } from '$lib/LinkContext/LinkContext.svelte';
77
import { getRouterContext } from '$lib/Router/Router.svelte';
88
import type { ActiveState, Hash, RouteStatus } from '$lib/types.js';
9+
import { assertAllowedRoutingMode } from '$lib/utils.js';
910
import { type Snippet } from 'svelte';
1011
import type { HTMLAnchorAttributes } from 'svelte/elements';
1112
@@ -85,6 +86,7 @@
8586
}: Props = $props();
8687
8788
const resolvedHash = resolveHashValue(hash);
89+
assertAllowedRoutingMode(resolvedHash);
8890
const router = getRouterContext(resolvedHash);
8991
const linkContext = getLinkContext();
9092
const calcReplace = $derived(replace ?? linkContext?.replace ?? false);

src/lib/Link/Link.svelte.test.ts

Lines changed: 60 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,12 @@
11
import { init } from "$lib/init.js";
22
import { location } from "$lib/core/Location.js";
3-
import { describe, test, expect, beforeAll, afterAll, beforeEach, vi } from "vitest";
3+
import { describe, test, expect, beforeAll, afterAll, beforeEach, vi, afterEach } from "vitest";
44
import { render, fireEvent } from "@testing-library/svelte";
55
import Link from "./Link.svelte";
6-
import { createRouterTestSetup, createTestSnippet, ROUTING_UNIVERSES } from "../../testing/test-utils.js";
6+
import { createRouterTestSetup, createTestSnippet, ROUTING_UNIVERSES, ALL_HASHES } from "../../testing/test-utils.js";
77
import { flushSync } from "svelte";
8+
import { resetRoutingOptions, setRoutingOptions } from "$lib/core/options.js";
9+
import type { ExtendedRoutingOptions } from "$lib/types.js";
810

911
function basicLinkTests(setup: ReturnType<typeof createRouterTestSetup>) {
1012
const linkText = "Test Link";
@@ -636,6 +638,61 @@ function reactivityTests(setup: ReturnType<typeof createRouterTestSetup>) {
636638
});
637639
}
638640

641+
describe("Routing Mode Assertions", () => {
642+
const linkText = "Test Link";
643+
const content = createTestSnippet(linkText);
644+
let cleanup: () => void;
645+
646+
beforeAll(() => {
647+
cleanup = init();
648+
});
649+
650+
afterEach(() => {
651+
resetRoutingOptions();
652+
});
653+
654+
afterAll(() => {
655+
cleanup();
656+
});
657+
658+
test.each<{
659+
options: Partial<ExtendedRoutingOptions>;
660+
hash: typeof ALL_HASHES[keyof typeof ALL_HASHES];
661+
description: string;
662+
}>([
663+
{
664+
options: { disallowHashRouting: true },
665+
hash: ALL_HASHES.single,
666+
description: 'hash routing is disallowed'
667+
},
668+
{
669+
options: { disallowMultiHashRouting: true },
670+
hash: ALL_HASHES.multi,
671+
description: 'multi-hash routing is disallowed'
672+
},
673+
{
674+
options: { disallowPathRouting: true },
675+
hash: ALL_HASHES.path,
676+
description: 'path routing is disallowed'
677+
}
678+
])("Should throw error when $description and hash=$hash .", ({ options, hash }) => {
679+
// Arrange
680+
setRoutingOptions(options);
681+
682+
// Act & Assert
683+
expect(() => {
684+
render(Link, {
685+
props: {
686+
href: "/test",
687+
hash,
688+
children: content
689+
},
690+
});
691+
}).toThrow();
692+
});
693+
});
694+
695+
639696
ROUTING_UNIVERSES.forEach(ru => {
640697
describe(`Link - ${ru.text}`, () => {
641698
const setup = createRouterTestSetup(ru.hash);
@@ -649,7 +706,7 @@ ROUTING_UNIVERSES.forEach(ru => {
649706
});
650707

651708
afterAll(() => {
652-
cleanup();
709+
cleanup?.();
653710
});
654711

655712
describe("Basic Link Functionality", () => {

src/lib/Route/Route.svelte

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,7 @@
2121
import { getRouterContext } from '../Router/Router.svelte';
2222
import { resolveHashValue } from '$lib/core/resolveHashValue.js';
2323
import type { ParameterValue, RouteStatus } from '$lib/types.js';
24+
import { assertAllowedRoutingMode } from '$lib/utils.js';
2425
2526
type Props = {
2627
/**
@@ -132,7 +133,10 @@
132133
children
133134
}: Props = $props();
134135
135-
const router = getRouterContext(resolveHashValue(hash));
136+
const resolvedHash = resolveHashValue(hash);
137+
assertAllowedRoutingMode(resolvedHash);
138+
139+
const router = getRouterContext(resolvedHash);
136140
if (!router) {
137141
throw new Error(
138142
'Route components must be used inside a Router component that matches the hash setting.'

src/lib/Route/Route.svelte.test.ts

Lines changed: 76 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,12 @@
1-
import { describe, test, expect, beforeEach, vi, beforeAll, afterAll } from "vitest";
1+
import { describe, test, expect, beforeEach, vi, beforeAll, afterAll, afterEach } from "vitest";
22
import { render } from "@testing-library/svelte";
33
import Route from "./Route.svelte";
4-
import { createTestSnippet, createRouterTestSetup, ROUTING_UNIVERSES } from "../../testing/test-utils.js";
4+
import { createTestSnippet, createRouterTestSetup, ROUTING_UNIVERSES, ALL_HASHES } from "../../testing/test-utils.js";
55
import { init } from "$lib/init.js";
66
import { location } from "$lib/core/Location.js";
77
import TestRouteWithRouter from "../../testing/TestRouteWithRouter.svelte";
8+
import { resetRoutingOptions, setRoutingOptions } from "$lib/core/options.js";
9+
import type { ExtendedRoutingOptions, InitOptions } from "$lib/types.js";
810

911
function basicRouteTests(setup: ReturnType<typeof createRouterTestSetup>) {
1012
beforeEach(() => {
@@ -646,6 +648,78 @@ function routeBindingTestsForUniverse(setup: ReturnType<typeof createRouterTestS
646648
});
647649
}
648650

651+
describe("Routing Mode Assertions", () => {
652+
let cleanup: () => void;
653+
654+
beforeAll(() => {
655+
cleanup = init();
656+
});
657+
658+
afterEach(() => {
659+
resetRoutingOptions();
660+
});
661+
662+
afterAll(() => {
663+
cleanup();
664+
});
665+
666+
test.each<{
667+
options: Partial<ExtendedRoutingOptions>;
668+
hash: typeof ALL_HASHES[keyof typeof ALL_HASHES];
669+
description: string;
670+
}>([
671+
{
672+
options: { disallowHashRouting: true },
673+
hash: ALL_HASHES.single,
674+
description: 'hash routing is disallowed'
675+
},
676+
{
677+
options: { disallowMultiHashRouting: true },
678+
hash: ALL_HASHES.multi,
679+
description: 'multi-hash routing is disallowed'
680+
},
681+
{
682+
options: { disallowPathRouting: true },
683+
hash: ALL_HASHES.path,
684+
description: 'path routing is disallowed'
685+
}
686+
])("Should throw error when $description and hash=$hash .", ({ options, hash }) => {
687+
// Arrange
688+
setRoutingOptions(options);
689+
690+
// Act & Assert
691+
expect(() => {
692+
render(Route, {
693+
props: {
694+
key: 'r1',
695+
hash,
696+
},
697+
});
698+
}).toThrow();
699+
});
700+
701+
test("Should not throw error when all routing modes are allowed.", () => {
702+
// Arrange
703+
const hash = ALL_HASHES.single;
704+
const setup = createRouterTestSetup(hash);
705+
setup.init();
706+
707+
// Act & Assert
708+
expect(() => {
709+
render(Route, {
710+
props: {
711+
hash,
712+
key: "test-route",
713+
},
714+
context: setup.context
715+
});
716+
}).not.toThrow();
717+
718+
// Cleanup
719+
setup.dispose();
720+
});
721+
});
722+
649723
// Run tests for each routing universe
650724
for (const ru of ROUTING_UNIVERSES) {
651725
describe(`Route - ${ru.text}`, () => {

src/lib/RouterTrace/RouterTrace.svelte

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@
88
import { getRouterContext } from '$lib/Router/Router.svelte';
99
import type { PatternRouteInfo } from '$lib/types.js';
1010
import type { HTMLTableAttributes } from 'svelte/elements';
11+
import { assertAllowedRoutingMode } from '$lib/utils.js';
1112
1213
type Props = Omit<HTMLTableAttributes, 'children'> & {
1314
/**
@@ -56,7 +57,9 @@
5657
}: Props = $props();
5758
5859
if (!router) {
59-
router = getRouterContext(resolveHashValue(hash));
60+
const resolvedHash = resolveHashValue(hash);
61+
assertAllowedRoutingMode(resolvedHash);
62+
router = getRouterContext(resolvedHash);
6063
if (!router) {
6164
throw new Error(
6265
'There is no router to trace. Make sure a Router component is an ancestor of this RouterTrace component instance, or provide a router using the "router" property.'

0 commit comments

Comments
 (0)