@equinor/fusion-framework-module-navigation 7.0.9-next.0 → 7.0.10
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/esm/version.js +1 -1
- package/dist/esm/version.js.map +1 -1
- package/dist/tsconfig.tsbuildinfo +1 -1
- package/dist/types/version.d.ts +1 -1
- package/package.json +10 -7
- package/CHANGELOG.md +0 -600
- package/src/NavigateEvent.ts +0 -47
- package/src/NavigatedEvent.ts +0 -35
- package/src/NavigationConfigurator.interface.ts +0 -41
- package/src/NavigationConfigurator.ts +0 -226
- package/src/NavigationProvider.interface.ts +0 -99
- package/src/NavigationProvider.ts +0 -436
- package/src/__tests__/BrowserHistory.test.ts +0 -151
- package/src/__tests__/HashHistory.test.ts +0 -121
- package/src/__tests__/MemoryHistory.test.ts +0 -185
- package/src/__tests__/NavigationProvider.test.ts +0 -302
- package/src/__tests__/ProxyHistory.test.ts +0 -149
- package/src/__tests__/setup.ts +0 -8
- package/src/enable-navigation.ts +0 -60
- package/src/index.ts +0 -51
- package/src/lib/BaseHistory.ts +0 -255
- package/src/lib/BrowserHistory.ts +0 -124
- package/src/lib/BrowserHistoryHashStack.ts +0 -57
- package/src/lib/BrowserHistoryStack.ts +0 -96
- package/src/lib/MemoryHistory.ts +0 -76
- package/src/lib/MemoryHistoryStack.ts +0 -118
- package/src/lib/ProxyHistory.ts +0 -145
- package/src/lib/create-history.ts +0 -58
- package/src/lib/index.ts +0 -33
- package/src/lib/state/actions.ts +0 -108
- package/src/lib/state/check-blockers.ts +0 -52
- package/src/lib/state/create-flow.ts +0 -36
- package/src/lib/state/create-history-reducer.ts +0 -77
- package/src/lib/state/create-store.ts +0 -39
- package/src/lib/state/flow-creators.ts +0 -7
- package/src/lib/state/go.ts +0 -20
- package/src/lib/state/history.state.ts +0 -14
- package/src/lib/state/index.ts +0 -4
- package/src/lib/state/navigate.ts +0 -58
- package/src/lib/state/pop.ts +0 -24
- package/src/lib/state/validate-current-location.ts +0 -33
- package/src/lib/types.ts +0 -190
- package/src/lib/utils/encode-trailing-whitespace.ts +0 -18
- package/src/lib/utils/has-protocol.ts +0 -21
- package/src/lib/utils/index.ts +0 -5
- package/src/lib/utils/path-to-string.ts +0 -24
- package/src/lib/utils/path-to-url.ts +0 -52
- package/src/lib/utils/resolve-browser-location.ts +0 -1
- package/src/lib/utils/resolve-hash-location.ts +0 -26
- package/src/lib/utils/resolve-path.ts +0 -22
- package/src/lib/utils/resolve-window-location.ts +0 -24
- package/src/module.ts +0 -80
- package/src/version.ts +0 -2
- package/tsconfig.json +0 -24
- package/vitest.config.ts +0 -14
|
@@ -1,121 +0,0 @@
|
|
|
1
|
-
import { describe, it, expect, beforeEach, afterEach } from 'vitest';
|
|
2
|
-
import { firstValueFrom, skip } from 'rxjs';
|
|
3
|
-
import { createHistory } from '../lib/create-history';
|
|
4
|
-
import type { History } from '../lib/types';
|
|
5
|
-
|
|
6
|
-
describe('HashHistory', () => {
|
|
7
|
-
let history: History;
|
|
8
|
-
|
|
9
|
-
beforeEach(() => {
|
|
10
|
-
history = createHistory('hash');
|
|
11
|
-
});
|
|
12
|
-
|
|
13
|
-
afterEach(() => {
|
|
14
|
-
history[Symbol.dispose]();
|
|
15
|
-
});
|
|
16
|
-
|
|
17
|
-
describe('hash-based navigation', () => {
|
|
18
|
-
it('should update hash instead of pathname on push', async () => {
|
|
19
|
-
const initialPathname = window.location.pathname;
|
|
20
|
-
|
|
21
|
-
const updatePromise = firstValueFrom(history.state$.pipe(skip(1)));
|
|
22
|
-
history.push('/about', { test: 'test' });
|
|
23
|
-
await updatePromise;
|
|
24
|
-
|
|
25
|
-
expect(history.location.pathname).toBe('/about');
|
|
26
|
-
expect(history.action).toBe('PUSH');
|
|
27
|
-
expect(history.location.state).toEqual({ test: 'test' });
|
|
28
|
-
// Pathname should remain unchanged
|
|
29
|
-
expect(window.location.pathname).toBe(initialPathname);
|
|
30
|
-
// Hash should contain the route
|
|
31
|
-
expect(window.location.hash).toBe('#/about');
|
|
32
|
-
});
|
|
33
|
-
|
|
34
|
-
it('should update hash instead of pathname on replace', async () => {
|
|
35
|
-
const initialPathname = window.location.pathname;
|
|
36
|
-
|
|
37
|
-
const updatePromise = firstValueFrom(history.state$.pipe(skip(1)));
|
|
38
|
-
history.replace('/profile', { userId: 123 });
|
|
39
|
-
await updatePromise;
|
|
40
|
-
|
|
41
|
-
expect(history.location.pathname).toBe('/profile');
|
|
42
|
-
expect(history.action).toBe('REPLACE');
|
|
43
|
-
expect(history.location.state).toEqual({ userId: 123 });
|
|
44
|
-
// Pathname should remain unchanged
|
|
45
|
-
expect(window.location.pathname).toBe(initialPathname);
|
|
46
|
-
// Hash should contain the route
|
|
47
|
-
expect(window.location.hash).toBe('#/profile');
|
|
48
|
-
});
|
|
49
|
-
|
|
50
|
-
it('should resolve location from hash', async () => {
|
|
51
|
-
const updatePromise = firstValueFrom(history.state$.pipe(skip(1)));
|
|
52
|
-
history.push('/test?query=1#section');
|
|
53
|
-
await updatePromise;
|
|
54
|
-
|
|
55
|
-
const location = history.location;
|
|
56
|
-
expect(location.pathname).toBe('/test');
|
|
57
|
-
expect(location.search).toBe('?query=1');
|
|
58
|
-
expect(location.hash).toBe('#section');
|
|
59
|
-
expect(window.location.hash).toBe('#/test?query=1#section');
|
|
60
|
-
});
|
|
61
|
-
|
|
62
|
-
it('should handle hashchange events for back/forward', async () => {
|
|
63
|
-
const initialPathname = window.location.pathname;
|
|
64
|
-
|
|
65
|
-
// Push multiple entries
|
|
66
|
-
history.push('/page1', { page: 1 });
|
|
67
|
-
await firstValueFrom(history.state$.pipe(skip(1)));
|
|
68
|
-
expect(window.location.hash).toBe('#/page1');
|
|
69
|
-
|
|
70
|
-
history.push('/page2', { page: 2 });
|
|
71
|
-
await firstValueFrom(history.state$.pipe(skip(1)));
|
|
72
|
-
expect(window.location.hash).toBe('#/page2');
|
|
73
|
-
|
|
74
|
-
// Go back - should trigger hashchange event
|
|
75
|
-
const backPromise = firstValueFrom(history.state$.pipe(skip(1)));
|
|
76
|
-
window.history.back();
|
|
77
|
-
await backPromise;
|
|
78
|
-
expect(history.location.pathname).toBe('/page1');
|
|
79
|
-
expect(history.action).toBe('POP');
|
|
80
|
-
expect(history.location.state).toEqual({ page: 1 });
|
|
81
|
-
expect(window.location.hash).toBe('#/page1');
|
|
82
|
-
expect(window.location.pathname).toBe(initialPathname);
|
|
83
|
-
|
|
84
|
-
// Go forward
|
|
85
|
-
const forwardPromise = firstValueFrom(history.state$.pipe(skip(1)));
|
|
86
|
-
window.history.forward();
|
|
87
|
-
await forwardPromise;
|
|
88
|
-
expect(history.location.pathname).toBe('/page2');
|
|
89
|
-
expect(history.action).toBe('POP');
|
|
90
|
-
expect(history.location.state).toEqual({ page: 2 });
|
|
91
|
-
expect(window.location.hash).toBe('#/page2');
|
|
92
|
-
expect(window.location.pathname).toBe(initialPathname);
|
|
93
|
-
});
|
|
94
|
-
|
|
95
|
-
it('should handle hash with search and hash fragments', async () => {
|
|
96
|
-
const initialPathname = window.location.pathname;
|
|
97
|
-
|
|
98
|
-
const updatePromise = firstValueFrom(history.state$.pipe(skip(1)));
|
|
99
|
-
history.push('/users?filter=active#list', { filter: 'active' });
|
|
100
|
-
await updatePromise;
|
|
101
|
-
|
|
102
|
-
expect(history.location.pathname).toBe('/users');
|
|
103
|
-
expect(history.location.search).toBe('?filter=active');
|
|
104
|
-
expect(history.location.hash).toBe('#list');
|
|
105
|
-
// Hash should contain full path with search and hash
|
|
106
|
-
expect(window.location.hash).toBe('#/users?filter=active#list');
|
|
107
|
-
expect(window.location.pathname).toBe(initialPathname);
|
|
108
|
-
});
|
|
109
|
-
});
|
|
110
|
-
|
|
111
|
-
describe('state$ observable', () => {
|
|
112
|
-
it('should emit state updates on navigation', async () => {
|
|
113
|
-
const updatePromise = firstValueFrom(history.state$.pipe(skip(1)));
|
|
114
|
-
history.push('/test');
|
|
115
|
-
const update = await updatePromise;
|
|
116
|
-
|
|
117
|
-
expect(update.action).toBe('PUSH');
|
|
118
|
-
expect(update.location.pathname).toBe('/test');
|
|
119
|
-
});
|
|
120
|
-
});
|
|
121
|
-
});
|
|
@@ -1,185 +0,0 @@
|
|
|
1
|
-
import { describe, it, expect, beforeEach, afterEach } from 'vitest';
|
|
2
|
-
import { firstValueFrom, skip } from 'rxjs';
|
|
3
|
-
import { MemoryHistory } from '../lib/MemoryHistory';
|
|
4
|
-
|
|
5
|
-
describe('MemoryHistory', () => {
|
|
6
|
-
let history: MemoryHistory;
|
|
7
|
-
|
|
8
|
-
beforeEach(() => {
|
|
9
|
-
history = new MemoryHistory();
|
|
10
|
-
});
|
|
11
|
-
|
|
12
|
-
afterEach(() => {
|
|
13
|
-
history[Symbol.dispose]();
|
|
14
|
-
});
|
|
15
|
-
|
|
16
|
-
describe('initialization', () => {
|
|
17
|
-
it('should initialize with default location', () => {
|
|
18
|
-
expect(history.location.pathname).toBe('/');
|
|
19
|
-
expect(history.action).toBe('POP');
|
|
20
|
-
});
|
|
21
|
-
|
|
22
|
-
it('should initialize with custom initial location', () => {
|
|
23
|
-
const customHistory = new MemoryHistory({
|
|
24
|
-
initialLocation: {
|
|
25
|
-
action: 'POP',
|
|
26
|
-
location: {
|
|
27
|
-
pathname: '/custom',
|
|
28
|
-
search: '',
|
|
29
|
-
hash: '',
|
|
30
|
-
key: 'custom-key',
|
|
31
|
-
state: { test: 'value' },
|
|
32
|
-
},
|
|
33
|
-
},
|
|
34
|
-
});
|
|
35
|
-
|
|
36
|
-
expect(customHistory.location.pathname).toBe('/custom');
|
|
37
|
-
expect(customHistory.location.state).toEqual({ test: 'value' });
|
|
38
|
-
expect(customHistory.action).toBe('POP');
|
|
39
|
-
|
|
40
|
-
customHistory[Symbol.dispose]();
|
|
41
|
-
});
|
|
42
|
-
});
|
|
43
|
-
|
|
44
|
-
describe('in-memory navigation', () => {
|
|
45
|
-
it('should push without affecting browser', async () => {
|
|
46
|
-
const initialPathname = window.location.pathname;
|
|
47
|
-
const initialHash = window.location.hash;
|
|
48
|
-
|
|
49
|
-
const updatePromise = firstValueFrom(history.state$.pipe(skip(1)));
|
|
50
|
-
history.push('/about', { test: 'test' });
|
|
51
|
-
await updatePromise;
|
|
52
|
-
|
|
53
|
-
expect(history.location.pathname).toBe('/about');
|
|
54
|
-
expect(history.action).toBe('PUSH');
|
|
55
|
-
expect(history.location.state).toEqual({ test: 'test' });
|
|
56
|
-
// Browser should not be affected
|
|
57
|
-
expect(window.location.pathname).toBe(initialPathname);
|
|
58
|
-
expect(window.location.hash).toBe(initialHash);
|
|
59
|
-
});
|
|
60
|
-
|
|
61
|
-
it('should replace without affecting browser', async () => {
|
|
62
|
-
const initialPathname = window.location.pathname;
|
|
63
|
-
const initialHash = window.location.hash;
|
|
64
|
-
|
|
65
|
-
const updatePromise = firstValueFrom(history.state$.pipe(skip(1)));
|
|
66
|
-
history.replace('/profile', { userId: 123 });
|
|
67
|
-
await updatePromise;
|
|
68
|
-
|
|
69
|
-
expect(history.location.pathname).toBe('/profile');
|
|
70
|
-
expect(history.action).toBe('REPLACE');
|
|
71
|
-
expect(history.location.state).toEqual({ userId: 123 });
|
|
72
|
-
// Browser should not be affected
|
|
73
|
-
expect(window.location.pathname).toBe(initialPathname);
|
|
74
|
-
expect(window.location.hash).toBe(initialHash);
|
|
75
|
-
});
|
|
76
|
-
|
|
77
|
-
it('should maintain in-memory history stack', async () => {
|
|
78
|
-
// Push multiple entries
|
|
79
|
-
history.push('/page1', { page: 1 });
|
|
80
|
-
await firstValueFrom(history.state$.pipe(skip(1)));
|
|
81
|
-
expect(history.location.pathname).toBe('/page1');
|
|
82
|
-
|
|
83
|
-
history.push('/page2', { page: 2 });
|
|
84
|
-
await firstValueFrom(history.state$.pipe(skip(1)));
|
|
85
|
-
expect(history.location.pathname).toBe('/page2');
|
|
86
|
-
|
|
87
|
-
history.push('/page3', { page: 3 });
|
|
88
|
-
await firstValueFrom(history.state$.pipe(skip(1)));
|
|
89
|
-
expect(history.location.pathname).toBe('/page3');
|
|
90
|
-
|
|
91
|
-
// Go back
|
|
92
|
-
const goBackPromise1 = firstValueFrom(history.state$.pipe(skip(1)));
|
|
93
|
-
history.go(-1);
|
|
94
|
-
await goBackPromise1;
|
|
95
|
-
expect(history.location.pathname).toBe('/page2');
|
|
96
|
-
expect(history.action).toBe('POP');
|
|
97
|
-
expect(history.location.state).toEqual({ page: 2 });
|
|
98
|
-
|
|
99
|
-
// Go back again
|
|
100
|
-
const goBackPromise2 = firstValueFrom(history.state$.pipe(skip(1)));
|
|
101
|
-
history.go(-1);
|
|
102
|
-
await goBackPromise2;
|
|
103
|
-
expect(history.location.pathname).toBe('/page1');
|
|
104
|
-
expect(history.action).toBe('POP');
|
|
105
|
-
expect(history.location.state).toEqual({ page: 1 });
|
|
106
|
-
|
|
107
|
-
// Go forward
|
|
108
|
-
const goForwardPromise = firstValueFrom(history.state$.pipe(skip(1)));
|
|
109
|
-
history.go(1);
|
|
110
|
-
await goForwardPromise;
|
|
111
|
-
expect(history.location.pathname).toBe('/page2');
|
|
112
|
-
expect(history.action).toBe('POP');
|
|
113
|
-
expect(history.location.state).toEqual({ page: 2 });
|
|
114
|
-
});
|
|
115
|
-
|
|
116
|
-
it('should clamp go() to history bounds', async () => {
|
|
117
|
-
history.push('/page1');
|
|
118
|
-
await firstValueFrom(history.state$.pipe(skip(1)));
|
|
119
|
-
expect(history.location.pathname).toBe('/page1');
|
|
120
|
-
|
|
121
|
-
// Try to go back beyond start
|
|
122
|
-
const goBackPromise = firstValueFrom(history.state$.pipe(skip(1)));
|
|
123
|
-
history.go(-10);
|
|
124
|
-
await goBackPromise;
|
|
125
|
-
// Should clamp to first entry
|
|
126
|
-
expect(history.location.pathname).toBe('/');
|
|
127
|
-
expect(history.action).toBe('POP');
|
|
128
|
-
|
|
129
|
-
// Try to go forward beyond end
|
|
130
|
-
const goForwardPromise = firstValueFrom(history.state$.pipe(skip(1)));
|
|
131
|
-
history.go(10);
|
|
132
|
-
await goForwardPromise;
|
|
133
|
-
// Should clamp to last entry
|
|
134
|
-
expect(history.location.pathname).toBe('/page1');
|
|
135
|
-
expect(history.action).toBe('POP');
|
|
136
|
-
});
|
|
137
|
-
});
|
|
138
|
-
|
|
139
|
-
describe('createURL', () => {
|
|
140
|
-
it('should create URLs with memory:// protocol', () => {
|
|
141
|
-
const url = history.createURL('/test');
|
|
142
|
-
expect(url.protocol).toBe('memory:');
|
|
143
|
-
expect(url.pathname).toBe('/test');
|
|
144
|
-
expect(url.href).toContain('memory://');
|
|
145
|
-
});
|
|
146
|
-
|
|
147
|
-
it('should create URLs with search and hash', () => {
|
|
148
|
-
const url = history.createURL('/test?query=1#section');
|
|
149
|
-
expect(url.protocol).toBe('memory:');
|
|
150
|
-
expect(url.pathname).toBe('/test');
|
|
151
|
-
expect(url.search).toBe('?query=1');
|
|
152
|
-
expect(url.hash).toBe('#section');
|
|
153
|
-
expect(url.href).toContain('memory://');
|
|
154
|
-
});
|
|
155
|
-
});
|
|
156
|
-
|
|
157
|
-
describe('state$ observable', () => {
|
|
158
|
-
it('should emit state updates on navigation', async () => {
|
|
159
|
-
const updatePromise = firstValueFrom(history.state$.pipe(skip(1)));
|
|
160
|
-
history.push('/test', { data: 'test' });
|
|
161
|
-
const update = await updatePromise;
|
|
162
|
-
|
|
163
|
-
expect(update.action).toBe('PUSH');
|
|
164
|
-
expect(update.location.pathname).toBe('/test');
|
|
165
|
-
expect(update.location.state).toEqual({ data: 'test' });
|
|
166
|
-
});
|
|
167
|
-
|
|
168
|
-
it('should emit state updates on go()', async () => {
|
|
169
|
-
history.push('/page1');
|
|
170
|
-
await firstValueFrom(history.state$.pipe(skip(1)));
|
|
171
|
-
expect(history.location.pathname).toBe('/page1');
|
|
172
|
-
|
|
173
|
-
history.push('/page2');
|
|
174
|
-
await firstValueFrom(history.state$.pipe(skip(1)));
|
|
175
|
-
expect(history.location.pathname).toBe('/page2');
|
|
176
|
-
|
|
177
|
-
const goPromise = firstValueFrom(history.state$.pipe(skip(1)));
|
|
178
|
-
history.go(-1);
|
|
179
|
-
const update = await goPromise;
|
|
180
|
-
|
|
181
|
-
expect(update.action).toBe('POP');
|
|
182
|
-
expect(update.location.pathname).toBe('/page1');
|
|
183
|
-
});
|
|
184
|
-
});
|
|
185
|
-
});
|
|
@@ -1,302 +0,0 @@
|
|
|
1
|
-
import { describe, it, expect, beforeEach, afterEach, vi } from 'vitest';
|
|
2
|
-
import { NavigationProvider } from '../NavigationProvider';
|
|
3
|
-
import { MemoryHistory } from '../lib/MemoryHistory';
|
|
4
|
-
import type { INavigationConfigurator } from '../NavigationConfigurator.interface';
|
|
5
|
-
import type { History } from '../lib/types';
|
|
6
|
-
|
|
7
|
-
describe('NavigationProvider', () => {
|
|
8
|
-
let history: History;
|
|
9
|
-
let provider: NavigationProvider;
|
|
10
|
-
let config: INavigationConfigurator;
|
|
11
|
-
|
|
12
|
-
beforeEach(() => {
|
|
13
|
-
history = new MemoryHistory();
|
|
14
|
-
config = { history };
|
|
15
|
-
provider = new NavigationProvider({
|
|
16
|
-
version: '1.0.0',
|
|
17
|
-
config,
|
|
18
|
-
});
|
|
19
|
-
});
|
|
20
|
-
|
|
21
|
-
afterEach(() => {
|
|
22
|
-
provider?.dispose();
|
|
23
|
-
history[Symbol.dispose]();
|
|
24
|
-
});
|
|
25
|
-
|
|
26
|
-
it('should have version', () => {
|
|
27
|
-
expect(provider.version).toBeDefined();
|
|
28
|
-
expect(provider.version.toString()).toBe('1.0.0');
|
|
29
|
-
});
|
|
30
|
-
|
|
31
|
-
it('should have history property', () => {
|
|
32
|
-
expect(provider.history).toBe(history);
|
|
33
|
-
});
|
|
34
|
-
|
|
35
|
-
it('should have path property', () => {
|
|
36
|
-
expect(provider.path).toBeDefined();
|
|
37
|
-
expect(provider.path.pathname).toBeDefined();
|
|
38
|
-
});
|
|
39
|
-
|
|
40
|
-
it('should have state$ observable', () => {
|
|
41
|
-
expect(provider.state$).toBeDefined();
|
|
42
|
-
expect(typeof provider.state$.subscribe).toBe('function');
|
|
43
|
-
});
|
|
44
|
-
|
|
45
|
-
it('should throw error if no history provided', () => {
|
|
46
|
-
config = {};
|
|
47
|
-
expect(() => {
|
|
48
|
-
new NavigationProvider({
|
|
49
|
-
version: '1.0.0',
|
|
50
|
-
config,
|
|
51
|
-
});
|
|
52
|
-
}).toThrow('no history provided!');
|
|
53
|
-
});
|
|
54
|
-
|
|
55
|
-
it('should dispose correctly', () => {
|
|
56
|
-
const disposeSpy = vi.spyOn(history as { [Symbol.dispose]: () => void }, Symbol.dispose);
|
|
57
|
-
provider.dispose();
|
|
58
|
-
expect(disposeSpy).toHaveBeenCalled();
|
|
59
|
-
});
|
|
60
|
-
|
|
61
|
-
it('should normalize root basename "/" to empty string', () => {
|
|
62
|
-
const providerWithRootBasename = new NavigationProvider({
|
|
63
|
-
version: '1.0.0',
|
|
64
|
-
config: { history, basename: '/' },
|
|
65
|
-
});
|
|
66
|
-
|
|
67
|
-
// Root basename '/' should be treated as "no basename"
|
|
68
|
-
expect(providerWithRootBasename.basename).toBe('');
|
|
69
|
-
|
|
70
|
-
providerWithRootBasename.dispose();
|
|
71
|
-
});
|
|
72
|
-
|
|
73
|
-
describe('basename normalization', () => {
|
|
74
|
-
it('should strip trailing slashes from basename', () => {
|
|
75
|
-
const provider1 = new NavigationProvider({
|
|
76
|
-
version: '1.0.0',
|
|
77
|
-
config: { history, basename: '/apps/my-app/' },
|
|
78
|
-
});
|
|
79
|
-
expect(provider1.basename).toBe('/apps/my-app');
|
|
80
|
-
provider1.dispose();
|
|
81
|
-
|
|
82
|
-
const provider2 = new NavigationProvider({
|
|
83
|
-
version: '1.0.0',
|
|
84
|
-
config: { history, basename: '/apps/my-app///' },
|
|
85
|
-
});
|
|
86
|
-
expect(provider2.basename).toBe('/apps/my-app');
|
|
87
|
-
provider2.dispose();
|
|
88
|
-
});
|
|
89
|
-
|
|
90
|
-
it('should collapse consecutive slashes in basename', () => {
|
|
91
|
-
const provider = new NavigationProvider({
|
|
92
|
-
version: '1.0.0',
|
|
93
|
-
config: { history, basename: '/apps//my-app' },
|
|
94
|
-
});
|
|
95
|
-
expect(provider.basename).toBe('/apps/my-app');
|
|
96
|
-
provider.dispose();
|
|
97
|
-
});
|
|
98
|
-
|
|
99
|
-
it('should handle pathological input efficiently (ReDoS protection)', () => {
|
|
100
|
-
// Create a string with many consecutive slashes to test performance
|
|
101
|
-
// This would cause ReDoS with certain regex patterns
|
|
102
|
-
const manySlashes = `/apps/${'/'.repeat(10000)}my-app`;
|
|
103
|
-
|
|
104
|
-
const startTime = Date.now();
|
|
105
|
-
const provider = new NavigationProvider({
|
|
106
|
-
version: '1.0.0',
|
|
107
|
-
config: { history, basename: manySlashes },
|
|
108
|
-
});
|
|
109
|
-
const endTime = Date.now();
|
|
110
|
-
|
|
111
|
-
// Should complete in reasonable time (< 100ms for 10k slashes)
|
|
112
|
-
expect(endTime - startTime).toBeLessThan(100);
|
|
113
|
-
expect(provider.basename).toBe('/apps/my-app');
|
|
114
|
-
provider.dispose();
|
|
115
|
-
});
|
|
116
|
-
|
|
117
|
-
it('should handle pathological trailing slashes efficiently (ReDoS protection)', () => {
|
|
118
|
-
// Create a string with many trailing slashes
|
|
119
|
-
// The /\/+$/ regex pattern would cause ReDoS with this input
|
|
120
|
-
const manyTrailingSlashes = `/apps/my-app${'/'.repeat(10000)}`;
|
|
121
|
-
|
|
122
|
-
const startTime = Date.now();
|
|
123
|
-
const provider = new NavigationProvider({
|
|
124
|
-
version: '1.0.0',
|
|
125
|
-
config: { history, basename: manyTrailingSlashes },
|
|
126
|
-
});
|
|
127
|
-
const endTime = Date.now();
|
|
128
|
-
|
|
129
|
-
// Should complete in reasonable time (< 100ms for 10k trailing slashes)
|
|
130
|
-
expect(endTime - startTime).toBeLessThan(100);
|
|
131
|
-
expect(provider.basename).toBe('/apps/my-app');
|
|
132
|
-
provider.dispose();
|
|
133
|
-
});
|
|
134
|
-
|
|
135
|
-
it('should handle undefined basename', () => {
|
|
136
|
-
const provider = new NavigationProvider({
|
|
137
|
-
version: '1.0.0',
|
|
138
|
-
config: { history, basename: undefined },
|
|
139
|
-
});
|
|
140
|
-
expect(provider.basename).toBe('');
|
|
141
|
-
provider.dispose();
|
|
142
|
-
});
|
|
143
|
-
});
|
|
144
|
-
|
|
145
|
-
describe('_isWithinBasenameScope', () => {
|
|
146
|
-
it('should allow all paths when basename is "/" (no basename)', () => {
|
|
147
|
-
const provider = new NavigationProvider({
|
|
148
|
-
version: '1.0.0',
|
|
149
|
-
config: { history, basename: '/' },
|
|
150
|
-
});
|
|
151
|
-
|
|
152
|
-
// All paths should be in scope
|
|
153
|
-
// biome-ignore lint/suspicious/noExplicitAny: Allow usage of 'any' in test files for mocking purposes
|
|
154
|
-
expect((provider as any)._isWithinBasenameScope('/')).toBe(true);
|
|
155
|
-
// biome-ignore lint/suspicious/noExplicitAny: Allow usage of 'any' in test files for mocking purposes
|
|
156
|
-
expect((provider as any)._isWithinBasenameScope('/apps')).toBe(true);
|
|
157
|
-
// biome-ignore lint/suspicious/noExplicitAny: Allow usage of 'any' in test files for mocking purposes
|
|
158
|
-
expect((provider as any)._isWithinBasenameScope('/apps/my-app')).toBe(true);
|
|
159
|
-
// biome-ignore lint/suspicious/noExplicitAny: Allow usage of 'any' in test files for mocking purposes
|
|
160
|
-
expect((provider as any)._isWithinBasenameScope('/apps/my-app/users')).toBe(true);
|
|
161
|
-
|
|
162
|
-
provider.dispose();
|
|
163
|
-
});
|
|
164
|
-
|
|
165
|
-
it('should check path boundaries correctly with basename', () => {
|
|
166
|
-
const provider = new NavigationProvider({
|
|
167
|
-
version: '1.0.0',
|
|
168
|
-
config: { history, basename: '/apps/my-app' },
|
|
169
|
-
});
|
|
170
|
-
|
|
171
|
-
// Exact match should be in scope
|
|
172
|
-
// biome-ignore lint/suspicious/noExplicitAny: Allow usage of 'any' in test files for mocking purposes
|
|
173
|
-
expect((provider as any)._isWithinBasenameScope('/apps/my-app')).toBe(true);
|
|
174
|
-
|
|
175
|
-
// Paths starting with basename/ should be in scope
|
|
176
|
-
// biome-ignore lint/suspicious/noExplicitAny: Allow usage of 'any' in test files for mocking purposes
|
|
177
|
-
expect((provider as any)._isWithinBasenameScope('/apps/my-app/')).toBe(true);
|
|
178
|
-
// biome-ignore lint/suspicious/noExplicitAny: Allow usage of 'any' in test files for mocking purposes
|
|
179
|
-
expect((provider as any)._isWithinBasenameScope('/apps/my-app/users')).toBe(true);
|
|
180
|
-
|
|
181
|
-
// Similar but different path should NOT be in scope (path boundary check)
|
|
182
|
-
// biome-ignore lint/suspicious/noExplicitAny: Allow usage of 'any' in test files for mocking purposes
|
|
183
|
-
expect((provider as any)._isWithinBasenameScope('/apps/my-app-other')).toBe(false);
|
|
184
|
-
// biome-ignore lint/suspicious/noExplicitAny: Allow usage of 'any' in test files for mocking purposes
|
|
185
|
-
expect((provider as any)._isWithinBasenameScope('/apps/my-app-other/users')).toBe(false);
|
|
186
|
-
|
|
187
|
-
// Completely different paths should NOT be in scope
|
|
188
|
-
// biome-ignore lint/suspicious/noExplicitAny: Allow usage of 'any' in test files for mocking purposes
|
|
189
|
-
expect((provider as any)._isWithinBasenameScope('/other')).toBe(false);
|
|
190
|
-
// biome-ignore lint/suspicious/noExplicitAny: Allow usage of 'any' in test files for mocking purposes
|
|
191
|
-
expect((provider as any)._isWithinBasenameScope('/')).toBe(false);
|
|
192
|
-
|
|
193
|
-
provider.dispose();
|
|
194
|
-
});
|
|
195
|
-
|
|
196
|
-
it('should handle consecutive slashes in pathname', () => {
|
|
197
|
-
const provider = new NavigationProvider({
|
|
198
|
-
version: '1.0.0',
|
|
199
|
-
config: { history, basename: '/apps/my-app' },
|
|
200
|
-
});
|
|
201
|
-
|
|
202
|
-
// Pathname with consecutive slashes should be normalized
|
|
203
|
-
// biome-ignore lint/suspicious/noExplicitAny: Allow usage of 'any' in test files for mocking purposes
|
|
204
|
-
expect((provider as any)._isWithinBasenameScope('/apps//my-app')).toBe(true);
|
|
205
|
-
// biome-ignore lint/suspicious/noExplicitAny: Allow usage of 'any' in test files for mocking purposes
|
|
206
|
-
expect((provider as any)._isWithinBasenameScope('/apps/my-app//users')).toBe(true);
|
|
207
|
-
|
|
208
|
-
provider.dispose();
|
|
209
|
-
});
|
|
210
|
-
});
|
|
211
|
-
|
|
212
|
-
describe('_localizePath', () => {
|
|
213
|
-
it('should strip basename from paths on boundary', () => {
|
|
214
|
-
const provider = new NavigationProvider({
|
|
215
|
-
version: '1.0.0',
|
|
216
|
-
config: { history, basename: '/apps/my-app' },
|
|
217
|
-
});
|
|
218
|
-
|
|
219
|
-
// Exact match should become '/'
|
|
220
|
-
expect(
|
|
221
|
-
// biome-ignore lint/suspicious/noExplicitAny: Allow usage of 'any' in test files for mocking purposes
|
|
222
|
-
(provider as any)._localizePath({ pathname: '/apps/my-app', search: '', hash: '' }),
|
|
223
|
-
).toEqual({ pathname: '/', search: '', hash: '' });
|
|
224
|
-
|
|
225
|
-
// Path with basename prefix should have it stripped
|
|
226
|
-
expect(
|
|
227
|
-
// biome-ignore lint/suspicious/noExplicitAny: Allow usage of 'any' in test files for mocking purposes
|
|
228
|
-
(provider as any)._localizePath({
|
|
229
|
-
pathname: '/apps/my-app/users',
|
|
230
|
-
search: '?q=test',
|
|
231
|
-
hash: '#section',
|
|
232
|
-
}),
|
|
233
|
-
).toEqual({ pathname: '/users', search: '?q=test', hash: '#section' });
|
|
234
|
-
|
|
235
|
-
provider.dispose();
|
|
236
|
-
});
|
|
237
|
-
|
|
238
|
-
it('should not strip similar paths without boundary match', () => {
|
|
239
|
-
const provider = new NavigationProvider({
|
|
240
|
-
version: '1.0.0',
|
|
241
|
-
config: { history, basename: '/apps/my-app' },
|
|
242
|
-
});
|
|
243
|
-
|
|
244
|
-
// Path that starts similarly but isn't a real match should not be stripped
|
|
245
|
-
// biome-ignore lint/suspicious/noExplicitAny: Allow usage of 'any' in test files for mocking purposes
|
|
246
|
-
const result = (provider as any)._localizePath({
|
|
247
|
-
pathname: '/apps/my-app-other/users',
|
|
248
|
-
search: '',
|
|
249
|
-
hash: '',
|
|
250
|
-
});
|
|
251
|
-
|
|
252
|
-
// Should not strip anything since it doesn't match on path boundary
|
|
253
|
-
expect(result.pathname).toBe('/apps/my-app-other/users');
|
|
254
|
-
|
|
255
|
-
provider.dispose();
|
|
256
|
-
});
|
|
257
|
-
|
|
258
|
-
it('should handle root basename "/" correctly', () => {
|
|
259
|
-
const provider = new NavigationProvider({
|
|
260
|
-
version: '1.0.0',
|
|
261
|
-
config: { history, basename: '/' },
|
|
262
|
-
});
|
|
263
|
-
|
|
264
|
-
// With no basename, paths should be returned normalized
|
|
265
|
-
// biome-ignore lint/suspicious/noExplicitAny: Allow usage of 'any' in test files for mocking purposes
|
|
266
|
-
expect((provider as any)._localizePath({ pathname: '/', search: '', hash: '' })).toEqual({
|
|
267
|
-
pathname: '/',
|
|
268
|
-
search: '',
|
|
269
|
-
hash: '',
|
|
270
|
-
});
|
|
271
|
-
|
|
272
|
-
// biome-ignore lint/suspicious/noExplicitAny: Allow usage of 'any' in test files for mocking purposes
|
|
273
|
-
expect((provider as any)._localizePath({ pathname: '/apps', search: '', hash: '' })).toEqual({
|
|
274
|
-
pathname: '/apps',
|
|
275
|
-
search: '',
|
|
276
|
-
hash: '',
|
|
277
|
-
});
|
|
278
|
-
|
|
279
|
-
expect(
|
|
280
|
-
// biome-ignore lint/suspicious/noExplicitAny: Allow usage of 'any' in test files for mocking purposes
|
|
281
|
-
(provider as any)._localizePath({ pathname: '/apps/my-app', search: '', hash: '' }),
|
|
282
|
-
).toEqual({ pathname: '/apps/my-app', search: '', hash: '' });
|
|
283
|
-
|
|
284
|
-
provider.dispose();
|
|
285
|
-
});
|
|
286
|
-
|
|
287
|
-
it('should normalize consecutive slashes', () => {
|
|
288
|
-
const provider = new NavigationProvider({
|
|
289
|
-
version: '1.0.0',
|
|
290
|
-
config: { history, basename: '/apps/my-app' },
|
|
291
|
-
});
|
|
292
|
-
|
|
293
|
-
// Consecutive slashes should be collapsed
|
|
294
|
-
expect(
|
|
295
|
-
// biome-ignore lint/suspicious/noExplicitAny: Allow usage of 'any' in test files for mocking purposes
|
|
296
|
-
(provider as any)._localizePath({ pathname: '/apps//my-app//users', search: '', hash: '' }),
|
|
297
|
-
).toEqual({ pathname: '/users', search: '', hash: '' });
|
|
298
|
-
|
|
299
|
-
provider.dispose();
|
|
300
|
-
});
|
|
301
|
-
});
|
|
302
|
-
});
|