Skip to content

Commit 3d1780f

Browse files
authored
feat: Add runtime check that validates the defaultHash/hashMode combo (#119)
1 parent f5be942 commit 3d1780f

File tree

3 files changed

+165
-41
lines changed

3 files changed

+165
-41
lines changed

src/lib/kernel/initCore.test.ts

Lines changed: 32 additions & 36 deletions
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@ import { initCore } from './initCore.js';
44
import { SvelteURL } from "svelte/reactivity";
55
import { location } from "./Location.js";
66
import { defaultTraceOptions, traceOptions } from "./trace.svelte.js";
7-
import { defaultRoutingOptions, routingOptions } from "./options.js";
7+
import { defaultRoutingOptions, resetRoutingOptions, routingOptions } from "./options.js";
88
import { logger } from "./Logger.js";
99

1010
const initialUrl = 'http://example.com/';
@@ -19,13 +19,15 @@ const locationMock: Location = {
1919
on: vi.fn(),
2020
go: vi.fn(),
2121
navigate: vi.fn(),
22+
get path() { return this.url.pathname; },
2223
};
2324

2425
describe('initCore', () => {
2526
let cleanup: (() => void) | undefined;
2627
afterEach(() => {
2728
vi.resetAllMocks();
2829
cleanup?.();
30+
resetRoutingOptions();
2931
});
3032
test("Should initialize with all the expected default values.", () => {
3133
// Act.
@@ -53,20 +55,10 @@ describe('initCore', () => {
5355
error: () => { }
5456
};
5557

56-
// Capture initial state
57-
const initialLoggerIsOffLogger = logger !== globalThis.console;
58-
const initialRoutingOptions = {
59-
hashMode: routingOptions.hashMode,
60-
defaultHash: routingOptions.defaultHash
61-
};
62-
const initialTraceOptions = {
63-
routerHierarchy: traceOptions.routerHierarchy
64-
};
65-
66-
// Act - Initialize with custom options
58+
// Act - Initialize with custom options (use valid combo)
6759
cleanup = initCore(locationMock, {
6860
hashMode: 'multi',
69-
defaultHash: true,
61+
defaultHash: 'customHash',
7062
logger: customLogger,
7163
trace: {
7264
routerHierarchy: true
@@ -76,19 +68,19 @@ describe('initCore', () => {
7668
// Assert - Check that options were applied
7769
expect(logger).toBe(customLogger);
7870
expect(routingOptions.hashMode).toBe('multi');
79-
expect(routingOptions.defaultHash).toBe(true);
71+
expect(routingOptions.defaultHash).toBe('customHash');
8072
expect(traceOptions.routerHierarchy).toBe(true);
8173
expect(location).toBeDefined();
8274

8375
// Act - Cleanup
8476
cleanup();
8577
cleanup = undefined;
8678

87-
// Assert - Check that everything was rolled back
88-
expect(logger !== globalThis.console).toBe(initialLoggerIsOffLogger); // Back to offLogger
89-
expect(routingOptions.hashMode).toBe(initialRoutingOptions.hashMode);
90-
expect(routingOptions.defaultHash).toBe(initialRoutingOptions.defaultHash);
91-
expect(traceOptions.routerHierarchy).toBe(initialTraceOptions.routerHierarchy);
79+
// Assert - Check that everything was rolled back to library defaults
80+
expect(logger).not.toBe(customLogger); // Should revert to offLogger
81+
expect(routingOptions.hashMode).toBe(defaultRoutingOptions.hashMode);
82+
expect(routingOptions.defaultHash).toBe(defaultRoutingOptions.defaultHash);
83+
expect(traceOptions.routerHierarchy).toBe(defaultTraceOptions.routerHierarchy);
9284
expect(location).toBeNull();
9385
});
9486
test("Should throw an error when called a second time without proper prior cleanup.", () => {
@@ -103,53 +95,57 @@ describe('initCore', () => {
10395
});
10496
describe('cleanup', () => {
10597
test("Should rollback everything to defaults.", async () => {
106-
// Arrange.
107-
const uninitializedLogger = logger;
98+
// Arrange - Initialize with custom options
10899
cleanup = initCore(locationMock, {
109100
hashMode: 'multi',
110-
defaultHash: true,
101+
defaultHash: 'abc',
111102
trace: {
112103
routerHierarchy: !defaultTraceOptions.routerHierarchy
113104
}
114105
});
115-
// Verify options were applied
106+
// Verify options were applied (no type conversion occurs)
116107
expect(routingOptions.hashMode).toBe('multi');
117-
expect(routingOptions.defaultHash).toBe(true);
118-
expect(logger).not.toBe(uninitializedLogger);
108+
expect(routingOptions.defaultHash).toBe('abc');
109+
expect(logger).toBe(globalThis.console); // Default logger when none specified
119110
expect(traceOptions.routerHierarchy).toBe(!defaultTraceOptions.routerHierarchy);
120111

121112
// Act - Cleanup
122113
cleanup();
123114
cleanup = undefined;
124115

125-
// Assert - Check that routing options were reset to defaults
126-
expect(routingOptions.hashMode).toBe('single');
127-
expect(routingOptions.defaultHash).toBe(false);
128-
expect(logger).toBe(uninitializedLogger);
116+
// Assert - Check that all options were reset to library defaults
117+
expect(routingOptions.hashMode).toBe(defaultRoutingOptions.hashMode);
118+
expect(routingOptions.defaultHash).toBe(defaultRoutingOptions.defaultHash);
119+
expect(logger).not.toBe(globalThis.console); // Reverts to offLogger (uninitialized state)
129120
expect(traceOptions).toEqual(defaultTraceOptions);
130121
});
131122
test("Should handle multiple init/cleanup cycles properly.", async () => {
132-
// Arrange.
133-
const uninitializedLogger = logger;
123+
// Capture initial logger state for comparison
124+
const initialLogger = logger;
125+
126+
// First cycle
134127
cleanup = initCore(locationMock, {
135128
logger: { debug: () => { }, log: () => { }, warn: () => { }, error: () => { } }
136129
});
137-
expect(logger).not.toBe(uninitializedLogger);
130+
expect(logger).not.toBe(initialLogger);
138131
expect(location).toBeDefined();
132+
139133
cleanup();
140134
cleanup = undefined;
141-
expect(logger).toBe(uninitializedLogger);
135+
expect(logger).toBe(initialLogger); // Back to initial state
142136
expect(location).toBeNull();
137+
138+
// Second cycle
143139
cleanup = initCore(locationMock, { hashMode: 'multi' });
144140
expect(routingOptions.hashMode).toBe('multi');
145141
expect(location).toBeDefined();
146142

147-
// Act.
143+
// Act - Final cleanup
148144
cleanup();
149145
cleanup = undefined;
150146

151-
// Assert.
152-
expect(routingOptions.hashMode).toBe('single');
147+
// Assert - Should revert to library defaults
148+
expect(routingOptions.hashMode).toBe(defaultRoutingOptions.hashMode);
153149
expect(location).toBeNull();
154150
});
155151
});

src/lib/kernel/options.test.ts

Lines changed: 127 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
1-
import { describe, expect, test } from "vitest";
2-
import { resetRoutingOptions, routingOptions } from "./options.js";
1+
import { describe, expect, test, beforeEach } from "vitest";
2+
import { resetRoutingOptions, routingOptions, setRoutingOptions } from "./options.js";
33

44
describe("options", () => {
55
test("Should have correct default value for hashMode option.", () => {
@@ -47,10 +47,132 @@ describe("options", () => {
4747
expect(typeof routingOptions.defaultHash).toBe('boolean');
4848
});
4949

50+
describe('setRoutingOptions', () => {
51+
beforeEach(() => {
52+
// Reset to defaults before each test
53+
resetRoutingOptions();
54+
});
55+
56+
test("Should merge options with current values when partial options provided.", () => {
57+
// Arrange - Set initial non-default values
58+
routingOptions.hashMode = 'multi';
59+
routingOptions.defaultHash = 'customHash';
60+
61+
// Act - Set only one option
62+
setRoutingOptions({ disallowPathRouting: true });
63+
64+
// Assert - Only specified option changed, others preserved
65+
expect(routingOptions.hashMode).toBe('multi');
66+
expect(routingOptions.defaultHash).toBe('customHash');
67+
expect(routingOptions.disallowPathRouting).toBe(true);
68+
expect(routingOptions.disallowHashRouting).toBe(false);
69+
});
70+
71+
test("Should set all options when full configuration provided.", () => {
72+
// Arrange & Act
73+
setRoutingOptions({
74+
hashMode: 'multi',
75+
defaultHash: 'namedHash',
76+
disallowPathRouting: true,
77+
disallowHashRouting: true,
78+
disallowMultiHashRouting: false
79+
});
80+
81+
// Assert
82+
expect(routingOptions.hashMode).toBe('multi');
83+
expect(routingOptions.defaultHash).toBe('namedHash');
84+
expect(routingOptions.disallowPathRouting).toBe(true);
85+
expect(routingOptions.disallowHashRouting).toBe(true);
86+
expect(routingOptions.disallowMultiHashRouting).toBe(false);
87+
});
88+
89+
test("Should do nothing when called with undefined options.", () => {
90+
// Arrange - Set initial values
91+
const original = structuredClone(routingOptions);
92+
93+
// Act
94+
setRoutingOptions(undefined);
95+
96+
// Assert - No changes
97+
expect(routingOptions).deep.equal(original);
98+
});
99+
100+
test("Should do nothing when called with empty options.", () => {
101+
// Arrange - Set initial values
102+
const original = structuredClone(routingOptions);
103+
104+
// Act
105+
setRoutingOptions({});
106+
107+
// Assert - No changes
108+
expect(routingOptions).deep.equal(original);
109+
});
110+
111+
describe('Runtime validation', () => {
112+
test("Should throw error when hashMode is 'single' and defaultHash is a string.", () => {
113+
// Arrange & Act & Assert
114+
expect(() => {
115+
setRoutingOptions({
116+
hashMode: 'single',
117+
defaultHash: 'namedHash'
118+
});
119+
}).toThrow("Using a named hash path as the default path can only be done when 'hashMode' is set to 'multi'.");
120+
});
121+
122+
test("Should throw error when hashMode is 'multi' and defaultHash is true.", () => {
123+
// Arrange & Act & Assert
124+
expect(() => {
125+
setRoutingOptions({
126+
hashMode: 'multi',
127+
defaultHash: true
128+
});
129+
}).toThrow("Using classic hash routing as default can only be done when 'hashMode' is set to 'single'.");
130+
});
131+
132+
test("Should throw error when existing hashMode is 'single' and setting defaultHash to string.", () => {
133+
// Arrange
134+
routingOptions.hashMode = 'single';
135+
136+
// Act & Assert
137+
expect(() => {
138+
setRoutingOptions({ defaultHash: 'namedHash' });
139+
}).toThrow("Using a named hash path as the default path can only be done when 'hashMode' is set to 'multi'.");
140+
});
141+
142+
test("Should throw error when existing defaultHash is true and setting hashMode to 'multi'.", () => {
143+
// Arrange
144+
routingOptions.defaultHash = true;
145+
146+
// Act & Assert
147+
expect(() => {
148+
setRoutingOptions({ hashMode: 'multi' });
149+
}).toThrow("Using classic hash routing as default can only be done when 'hashMode' is set to 'single'.");
150+
});
151+
152+
test.each([
153+
{ hashMode: 'single' as const, defaultHash: false, scenario: 'single hash mode with defaultHash false' },
154+
{ hashMode: 'single' as const, defaultHash: true, scenario: 'single hash mode with defaultHash true' },
155+
{ hashMode: 'multi' as const, defaultHash: false, scenario: 'multi hash mode with defaultHash false' },
156+
{ hashMode: 'multi' as const, defaultHash: 'namedHash', scenario: 'multi hash mode with named hash' }
157+
])("Should allow valid combination: $scenario .", ({ hashMode, defaultHash }) => {
158+
// Arrange & Act & Assert
159+
expect(() => {
160+
setRoutingOptions({ hashMode, defaultHash });
161+
}).not.toThrow();
162+
163+
expect(routingOptions.hashMode).toBe(hashMode);
164+
expect(routingOptions.defaultHash).toBe(defaultHash);
165+
});
166+
});
167+
});
168+
50169
describe('resetRoutingOptions', () => {
51170
test("Should reset all options to defaults when resetRoutingOptions is called.", () => {
52-
// Arrange.
53-
const original = structuredClone(routingOptions);
171+
// Arrange - First reset to ensure we start from defaults, then capture the baseline
172+
resetRoutingOptions();
173+
const expectedDefaults = structuredClone(routingOptions);
174+
175+
// Modify all options to non-default values
54176
routingOptions.hashMode = 'multi';
55177
routingOptions.defaultHash = true;
56178
routingOptions.disallowPathRouting = true;
@@ -61,7 +183,7 @@ describe("options", () => {
61183
resetRoutingOptions();
62184

63185
// Assert.
64-
expect(routingOptions).deep.equal(original);
186+
expect(routingOptions).deep.equal(expectedDefaults);
65187
});
66188
});
67189
});

src/lib/kernel/options.ts

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -28,6 +28,12 @@ export function setRoutingOptions(options?: Partial<ExtendedRoutingOptions>): vo
2828
routingOptions.disallowPathRouting = options?.disallowPathRouting ?? routingOptions.disallowPathRouting;
2929
routingOptions.disallowHashRouting = options?.disallowHashRouting ?? routingOptions.disallowHashRouting;
3030
routingOptions.disallowMultiHashRouting = options?.disallowMultiHashRouting ?? routingOptions.disallowMultiHashRouting;
31+
if (routingOptions.hashMode === 'single' && typeof routingOptions.defaultHash === 'string') {
32+
throw new Error("Using a named hash path as the default path can only be done when 'hashMode' is set to 'multi'.");
33+
}
34+
else if (routingOptions.hashMode === 'multi' && routingOptions.defaultHash === true) {
35+
throw new Error("Using classic hash routing as default can only be done when 'hashMode' is set to 'single'.");
36+
}
3137
}
3238

3339
/**

0 commit comments

Comments
 (0)