@uistate/router 1.0.0 → 1.0.1
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/README.md +21 -0
- package/package.json +7 -1
- package/router.js +1 -1
- package/self-test.js +163 -0
package/README.md
CHANGED
|
@@ -236,6 +236,27 @@ The router sets attributes on `<html>` for CSS-driven transitions:
|
|
|
236
236
|
}
|
|
237
237
|
```
|
|
238
238
|
|
|
239
|
+
## Testing
|
|
240
|
+
|
|
241
|
+
Two-layer testing architecture:
|
|
242
|
+
|
|
243
|
+
**`self-test.js`** — Zero-dependency self-test (35 assertions). Runs automatically on `npm install` via `postinstall`. Tests the pure-function core: pattern compilation, path normalization, route resolution, and URL-encoded param decoding.
|
|
244
|
+
|
|
245
|
+
```bash
|
|
246
|
+
node self-test.js
|
|
247
|
+
```
|
|
248
|
+
|
|
249
|
+
**`tests/router.test.js`** — Integration tests via `@uistate/event-test` (13 tests). Tests the store-driven routing patterns: `setMany` for atomic route updates, wildcard subscriptions, `ui.route.go` navigation, transition state, and type generation.
|
|
250
|
+
|
|
251
|
+
```bash
|
|
252
|
+
npm test
|
|
253
|
+
```
|
|
254
|
+
|
|
255
|
+
| Suite | Assertions | Dependencies |
|
|
256
|
+
|-------|-----------|-------------|
|
|
257
|
+
| `self-test.js` | 35 | none (zero-dep) |
|
|
258
|
+
| `tests/router.test.js` | 13 | `@uistate/event-test`, `@uistate/core` |
|
|
259
|
+
|
|
239
260
|
## Philosophy
|
|
240
261
|
|
|
241
262
|
Routing is not special. It's a `set` call to a path in a JSON tree. The router writes `ui.route.*`, and anything that cares about routing subscribes to `ui.route.*`. The router doesn't know about your nav, your breadcrumbs, your analytics, or your loading spinners. They all subscribe independently. That's UIState: EventState + Routing.
|
package/package.json
CHANGED
|
@@ -1,16 +1,22 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@uistate/router",
|
|
3
|
-
"version": "1.0.
|
|
3
|
+
"version": "1.0.1",
|
|
4
4
|
"description": "SPA router for EventState stores — routing is just state",
|
|
5
5
|
"main": "index.js",
|
|
6
6
|
"module": "index.js",
|
|
7
7
|
"type": "module",
|
|
8
|
+
"scripts": {
|
|
9
|
+
"postinstall": "node self-test.js",
|
|
10
|
+
"test": "node tests/router.test.js",
|
|
11
|
+
"self-test": "node self-test.js"
|
|
12
|
+
},
|
|
8
13
|
"exports": {
|
|
9
14
|
".": "./index.js"
|
|
10
15
|
},
|
|
11
16
|
"files": [
|
|
12
17
|
"index.js",
|
|
13
18
|
"router.js",
|
|
19
|
+
"self-test.js",
|
|
14
20
|
"README.md",
|
|
15
21
|
"LICENSE"
|
|
16
22
|
],
|
package/router.js
CHANGED
package/self-test.js
ADDED
|
@@ -0,0 +1,163 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @uistate/router: zero-dependency self-test
|
|
3
|
+
*
|
|
4
|
+
* Tests the pure-function core of the router: pattern compilation,
|
|
5
|
+
* path normalization, and route resolution.
|
|
6
|
+
* DOM-dependent features (navigate, start, link interception) are
|
|
7
|
+
* tested in the integration test suite.
|
|
8
|
+
*/
|
|
9
|
+
|
|
10
|
+
let passed = 0;
|
|
11
|
+
let failed = 0;
|
|
12
|
+
|
|
13
|
+
function assert(label, condition) {
|
|
14
|
+
if (condition) {
|
|
15
|
+
console.log(` ✓ ${label}`);
|
|
16
|
+
passed++;
|
|
17
|
+
} else {
|
|
18
|
+
console.error(` ✗ ${label}`);
|
|
19
|
+
failed++;
|
|
20
|
+
}
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
function section(title) {
|
|
24
|
+
console.log(`\n${title}`);
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
// -- Pure functions extracted from router.js for testing -------------
|
|
28
|
+
|
|
29
|
+
function compilePattern(pattern) {
|
|
30
|
+
const paramNames = [];
|
|
31
|
+
const parts = pattern.split(/:([a-zA-Z_][a-zA-Z0-9_]*)/);
|
|
32
|
+
const regexStr = parts
|
|
33
|
+
.map((part, i) => {
|
|
34
|
+
if (i % 2 === 1) {
|
|
35
|
+
paramNames.push(part);
|
|
36
|
+
return '([^/]+)';
|
|
37
|
+
}
|
|
38
|
+
return part.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
|
|
39
|
+
})
|
|
40
|
+
.join('');
|
|
41
|
+
return { regex: new RegExp('^' + regexStr + '$'), paramNames };
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
function normalizePath(p) {
|
|
45
|
+
if (!p) return '/';
|
|
46
|
+
if (p[0] !== '/') p = '/' + p;
|
|
47
|
+
if (p === '/index.html') return '/';
|
|
48
|
+
if (p.length > 1 && p.endsWith('/')) p = p.slice(0, -1);
|
|
49
|
+
return p;
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
function resolve(compiled, compiledFallback, pathname) {
|
|
53
|
+
const p = normalizePath(pathname);
|
|
54
|
+
for (const route of compiled) {
|
|
55
|
+
const match = p.match(route.regex);
|
|
56
|
+
if (match) {
|
|
57
|
+
const params = {};
|
|
58
|
+
route.paramNames.forEach((name, i) => {
|
|
59
|
+
params[name] = decodeURIComponent(match[i + 1]);
|
|
60
|
+
});
|
|
61
|
+
return { path: route.path, view: route.view, params };
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
if (compiledFallback) return { ...compiledFallback, params: {} };
|
|
65
|
+
return null;
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
// -- 1. compilePattern -----------------------------------------------
|
|
69
|
+
|
|
70
|
+
section('1. compilePattern');
|
|
71
|
+
|
|
72
|
+
const r1 = compilePattern('/');
|
|
73
|
+
assert('root: matches /', r1.regex.test('/'));
|
|
74
|
+
assert('root: no params', r1.paramNames.length === 0);
|
|
75
|
+
assert('root: rejects /foo', !r1.regex.test('/foo'));
|
|
76
|
+
|
|
77
|
+
const r2 = compilePattern('/users');
|
|
78
|
+
assert('static: matches /users', r2.regex.test('/users'));
|
|
79
|
+
assert('static: rejects /users/1', !r2.regex.test('/users/1'));
|
|
80
|
+
assert('static: rejects /', !r2.regex.test('/'));
|
|
81
|
+
|
|
82
|
+
const r3 = compilePattern('/users/:id');
|
|
83
|
+
assert('single param: matches /users/42', r3.regex.test('/users/42'));
|
|
84
|
+
assert('single param: extracts id', r3.paramNames[0] === 'id');
|
|
85
|
+
const m3 = '/users/42'.match(r3.regex);
|
|
86
|
+
assert('single param: id = 42', m3[1] === '42');
|
|
87
|
+
assert('single param: rejects /users', !r3.regex.test('/users'));
|
|
88
|
+
assert('single param: rejects /users/', !r3.regex.test('/users/'));
|
|
89
|
+
|
|
90
|
+
const r4 = compilePattern('/users/:id/posts/:postId');
|
|
91
|
+
assert('multi param: matches /users/1/posts/99', r4.regex.test('/users/1/posts/99'));
|
|
92
|
+
assert('multi param: paramNames', r4.paramNames[0] === 'id' && r4.paramNames[1] === 'postId');
|
|
93
|
+
const m4 = '/users/1/posts/99'.match(r4.regex);
|
|
94
|
+
assert('multi param: id = 1', m4[1] === '1');
|
|
95
|
+
assert('multi param: postId = 99', m4[2] === '99');
|
|
96
|
+
|
|
97
|
+
const r5 = compilePattern('/posts/:id/edit');
|
|
98
|
+
assert('mixed: matches /posts/5/edit', r5.regex.test('/posts/5/edit'));
|
|
99
|
+
assert('mixed: rejects /posts/5', !r5.regex.test('/posts/5'));
|
|
100
|
+
|
|
101
|
+
// -- 2. normalizePath ------------------------------------------------
|
|
102
|
+
|
|
103
|
+
section('2. normalizePath');
|
|
104
|
+
|
|
105
|
+
assert('null → /', normalizePath(null) === '/');
|
|
106
|
+
assert('empty → /', normalizePath('') === '/');
|
|
107
|
+
assert('/ → /', normalizePath('/') === '/');
|
|
108
|
+
assert('/users → /users', normalizePath('/users') === '/users');
|
|
109
|
+
assert('/users/ → /users', normalizePath('/users/') === '/users');
|
|
110
|
+
assert('users → /users', normalizePath('users') === '/users');
|
|
111
|
+
assert('/index.html → /', normalizePath('/index.html') === '/');
|
|
112
|
+
assert('/a/b/c/ → /a/b/c', normalizePath('/a/b/c/') === '/a/b/c');
|
|
113
|
+
|
|
114
|
+
// -- 3. resolve ------------------------------------------------------
|
|
115
|
+
|
|
116
|
+
section('3. resolve');
|
|
117
|
+
|
|
118
|
+
const routes = [
|
|
119
|
+
{ path: '/', view: 'home' },
|
|
120
|
+
{ path: '/users', view: 'users' },
|
|
121
|
+
{ path: '/users/:id', view: 'user' },
|
|
122
|
+
{ path: '/users/:id/posts/:postId', view: 'post' },
|
|
123
|
+
].map(r => ({ ...r, ...compilePattern(r.path) }));
|
|
124
|
+
|
|
125
|
+
const fallback = { view: '404', params: {} };
|
|
126
|
+
|
|
127
|
+
const res1 = resolve(routes, fallback, '/');
|
|
128
|
+
assert('resolve /: view = home', res1.view === 'home');
|
|
129
|
+
|
|
130
|
+
const res2 = resolve(routes, fallback, '/users');
|
|
131
|
+
assert('resolve /users: view = users', res2.view === 'users');
|
|
132
|
+
|
|
133
|
+
const res3 = resolve(routes, fallback, '/users/42');
|
|
134
|
+
assert('resolve /users/42: view = user', res3.view === 'user');
|
|
135
|
+
assert('resolve /users/42: params.id = 42', res3.params.id === '42');
|
|
136
|
+
|
|
137
|
+
const res4 = resolve(routes, fallback, '/users/1/posts/99');
|
|
138
|
+
assert('resolve /users/1/posts/99: view = post', res4.view === 'post');
|
|
139
|
+
assert('resolve /users/1/posts/99: params.id = 1', res4.params.id === '1');
|
|
140
|
+
assert('resolve /users/1/posts/99: params.postId = 99', res4.params.postId === '99');
|
|
141
|
+
|
|
142
|
+
const res5 = resolve(routes, fallback, '/unknown');
|
|
143
|
+
assert('resolve /unknown: falls back to 404', res5.view === '404');
|
|
144
|
+
|
|
145
|
+
const res6 = resolve(routes, null, '/unknown');
|
|
146
|
+
assert('resolve /unknown no fallback: returns null', res6 === null);
|
|
147
|
+
|
|
148
|
+
// -- 4. URL-encoded params -------------------------------------------
|
|
149
|
+
|
|
150
|
+
section('4. URL-encoded params');
|
|
151
|
+
|
|
152
|
+
const res7 = resolve(routes, null, '/users/hello%20world');
|
|
153
|
+
assert('URL-encoded param decoded', res7.params.id === 'hello world');
|
|
154
|
+
|
|
155
|
+
// -- Summary ---------------------------------------------------------
|
|
156
|
+
|
|
157
|
+
console.log(`\n@uistate/router v1.0.1 self-test`);
|
|
158
|
+
if (failed > 0) {
|
|
159
|
+
console.error(`✗ ${failed} assertion(s) failed, ${passed} passed`);
|
|
160
|
+
process.exit(1);
|
|
161
|
+
} else {
|
|
162
|
+
console.log(`✓ ${passed} assertions passed`);
|
|
163
|
+
}
|