@stefanobalocco/jfsmrouter 2.0.0
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/LICENSE.md +22 -0
- package/README.md +182 -0
- package/build.mjs +102 -0
- package/jFSMRouter.d.ts +53 -0
- package/jFSMRouter.js +398 -0
- package/jFSMRouter.min.js +1 -0
- package/jFSMRouter.test.js +408 -0
- package/jFSMRouter.test.ts +505 -0
- package/jFSMRouter.ts +442 -0
- package/jsdom.d.ts +6 -0
- package/package.json +53 -0
- package/tsconfig.json +22 -0
- package/tsconfig.tests.json +25 -0
|
@@ -0,0 +1,408 @@
|
|
|
1
|
+
import test from 'ava';
|
|
2
|
+
import { JSDOM } from 'jsdom';
|
|
3
|
+
const dom = new JSDOM('<!DOCTYPE html><html><body></body></html>', { url: 'http://localhost/' });
|
|
4
|
+
globalThis.document = dom.window.document;
|
|
5
|
+
globalThis.window = dom.window;
|
|
6
|
+
const routerModule = await import('./jFSMRouter.js');
|
|
7
|
+
const router = routerModule.default;
|
|
8
|
+
let nameCounter = 0;
|
|
9
|
+
function nextName(prefix) {
|
|
10
|
+
return prefix + '_' + (++nameCounter);
|
|
11
|
+
}
|
|
12
|
+
async function moveToState(state) {
|
|
13
|
+
const currentState = router.state;
|
|
14
|
+
router.stateAdd(state);
|
|
15
|
+
if ((undefined !== currentState) && (currentState !== state)) {
|
|
16
|
+
router.transitionAdd(currentState, state);
|
|
17
|
+
const changed = await router.stateSet(state);
|
|
18
|
+
if (!changed) {
|
|
19
|
+
throw new Error('Failed to transition to state: ' + state);
|
|
20
|
+
}
|
|
21
|
+
}
|
|
22
|
+
}
|
|
23
|
+
test.serial('Default export is a router instance', (t) => {
|
|
24
|
+
t.is(typeof router, 'object');
|
|
25
|
+
t.truthy(router);
|
|
26
|
+
t.truthy(router.stateAdd);
|
|
27
|
+
});
|
|
28
|
+
test.serial('stateAdd and initial state', (t) => {
|
|
29
|
+
const stateName = nextName('initial');
|
|
30
|
+
t.true(router.stateAdd(stateName));
|
|
31
|
+
t.is(router.state, stateName);
|
|
32
|
+
t.false(router.stateAdd(stateName));
|
|
33
|
+
});
|
|
34
|
+
test.serial('State deletion removes inbound transitions', async (t) => {
|
|
35
|
+
const fromState = nextName('del_from');
|
|
36
|
+
const toState = nextName('del_to');
|
|
37
|
+
await moveToState(fromState);
|
|
38
|
+
router.stateAdd(toState);
|
|
39
|
+
router.transitionAdd(fromState, toState);
|
|
40
|
+
t.true(router.checkTransition(toState));
|
|
41
|
+
t.true(router.stateDel(toState));
|
|
42
|
+
t.false(router.checkTransition(toState));
|
|
43
|
+
t.false(router.stateDel(toState));
|
|
44
|
+
});
|
|
45
|
+
test.serial('Transition add/delete booleans', async (t) => {
|
|
46
|
+
const fromState = nextName('tadd_from');
|
|
47
|
+
const toState = nextName('tadd_to');
|
|
48
|
+
const missingState = nextName('tadd_missing');
|
|
49
|
+
await moveToState(fromState);
|
|
50
|
+
router.stateAdd(toState);
|
|
51
|
+
t.true(router.transitionAdd(fromState, toState));
|
|
52
|
+
t.false(router.transitionAdd(fromState, toState));
|
|
53
|
+
t.true(router.transitionDel(fromState, toState));
|
|
54
|
+
t.false(router.transitionDel(fromState, toState));
|
|
55
|
+
t.false(router.transitionAdd(fromState, missingState));
|
|
56
|
+
t.false(router.transitionAdd(missingState, toState));
|
|
57
|
+
});
|
|
58
|
+
test.serial('stateSet hook order with async hooks', async (t) => {
|
|
59
|
+
const fromState = nextName('hook_from');
|
|
60
|
+
const toState = nextName('hook_to');
|
|
61
|
+
const events = [];
|
|
62
|
+
await moveToState(fromState);
|
|
63
|
+
router.stateAdd(toState);
|
|
64
|
+
router.transitionAdd(fromState, toState);
|
|
65
|
+
router.transitionOnBeforeAdd(fromState, toState, () => {
|
|
66
|
+
events.push('before');
|
|
67
|
+
});
|
|
68
|
+
router.stateOnLeaveAdd(fromState, async (currentState, nextState) => {
|
|
69
|
+
events.push('leave:' + currentState + ':' + nextState);
|
|
70
|
+
});
|
|
71
|
+
router.transitionOnAfterAdd(fromState, toState, () => {
|
|
72
|
+
events.push('after');
|
|
73
|
+
});
|
|
74
|
+
router.stateOnEnterAdd(toState, async (currentState, previousState) => {
|
|
75
|
+
events.push('enter:' + currentState + ':' + previousState);
|
|
76
|
+
});
|
|
77
|
+
const result = await router.stateSet(toState);
|
|
78
|
+
t.true(result);
|
|
79
|
+
t.is(router.state, toState);
|
|
80
|
+
t.deepEqual(events, ['before', 'leave:' + fromState + ':' + toState, 'after', 'enter:' + toState + ':' + fromState]);
|
|
81
|
+
});
|
|
82
|
+
test.serial('OnBefore returning false aborts transition', async (t) => {
|
|
83
|
+
const fromState = nextName('abort_from');
|
|
84
|
+
const toState = nextName('abort_to');
|
|
85
|
+
const events = [];
|
|
86
|
+
await moveToState(fromState);
|
|
87
|
+
router.stateAdd(toState);
|
|
88
|
+
router.transitionAdd(fromState, toState);
|
|
89
|
+
router.transitionOnBeforeAdd(fromState, toState, () => {
|
|
90
|
+
events.push('before');
|
|
91
|
+
return false;
|
|
92
|
+
});
|
|
93
|
+
router.stateOnLeaveAdd(fromState, () => {
|
|
94
|
+
events.push('leave');
|
|
95
|
+
});
|
|
96
|
+
router.transitionOnAfterAdd(fromState, toState, () => {
|
|
97
|
+
events.push('after');
|
|
98
|
+
});
|
|
99
|
+
router.stateOnEnterAdd(toState, () => {
|
|
100
|
+
events.push('enter');
|
|
101
|
+
});
|
|
102
|
+
const result = await router.stateSet(toState);
|
|
103
|
+
t.false(result);
|
|
104
|
+
t.is(router.state, fromState);
|
|
105
|
+
t.deepEqual(events, ['before']);
|
|
106
|
+
});
|
|
107
|
+
test.serial('Hook add/delete APIs dedupe and report missing states', (t) => {
|
|
108
|
+
const existingState = nextName('hookdd');
|
|
109
|
+
const missingState = nextName('hookdd_missing');
|
|
110
|
+
router.stateAdd(existingState);
|
|
111
|
+
const enterFn = () => { };
|
|
112
|
+
const leaveFn = () => { };
|
|
113
|
+
t.true(router.stateOnEnterAdd(existingState, enterFn));
|
|
114
|
+
t.false(router.stateOnEnterAdd(existingState, enterFn));
|
|
115
|
+
t.true(router.stateOnEnterDel(existingState, enterFn));
|
|
116
|
+
t.false(router.stateOnEnterDel(existingState, enterFn));
|
|
117
|
+
t.true(router.stateOnLeaveAdd(existingState, leaveFn));
|
|
118
|
+
t.false(router.stateOnLeaveAdd(existingState, leaveFn));
|
|
119
|
+
t.true(router.stateOnLeaveDel(existingState, leaveFn));
|
|
120
|
+
t.false(router.stateOnLeaveDel(existingState, leaveFn));
|
|
121
|
+
t.false(router.stateOnEnterAdd(missingState, enterFn));
|
|
122
|
+
t.false(router.stateOnEnterDel(missingState, enterFn));
|
|
123
|
+
t.false(router.stateOnLeaveAdd(missingState, leaveFn));
|
|
124
|
+
t.false(router.stateOnLeaveDel(missingState, leaveFn));
|
|
125
|
+
});
|
|
126
|
+
test.serial('Transition hook add/delete APIs report booleans', async (t) => {
|
|
127
|
+
const fromState = nextName('thook_from');
|
|
128
|
+
const toState = nextName('thook_to');
|
|
129
|
+
const missingState = nextName('thook_missing');
|
|
130
|
+
await moveToState(fromState);
|
|
131
|
+
router.stateAdd(toState);
|
|
132
|
+
router.transitionAdd(fromState, toState);
|
|
133
|
+
const beforeFn = () => { };
|
|
134
|
+
const afterFn = () => { };
|
|
135
|
+
t.true(router.transitionOnBeforeAdd(fromState, toState, beforeFn));
|
|
136
|
+
t.true(router.transitionOnBeforeDel(fromState, toState, beforeFn));
|
|
137
|
+
t.false(router.transitionOnBeforeDel(fromState, toState, beforeFn));
|
|
138
|
+
t.true(router.transitionOnAfterAdd(fromState, toState, afterFn));
|
|
139
|
+
t.true(router.transitionOnAfterDel(fromState, toState, afterFn));
|
|
140
|
+
t.false(router.transitionOnAfterDel(fromState, toState, afterFn));
|
|
141
|
+
t.false(router.transitionOnBeforeAdd(fromState, missingState, beforeFn));
|
|
142
|
+
t.false(router.transitionOnBeforeDel(fromState, missingState, beforeFn));
|
|
143
|
+
});
|
|
144
|
+
test.serial('No match with no 404 falls back to 500', async (t) => {
|
|
145
|
+
let errorHandlerCalled = 0;
|
|
146
|
+
router.routeSpecialAdd(500, () => {
|
|
147
|
+
errorHandlerCalled++;
|
|
148
|
+
});
|
|
149
|
+
await router.route('/nomatch-' + nextName('nofallback'));
|
|
150
|
+
t.is(errorHandlerCalled, 1);
|
|
151
|
+
});
|
|
152
|
+
test.serial('routeAdd captures typed params and dispatches route function', async (t) => {
|
|
153
|
+
const stateName = nextName('params');
|
|
154
|
+
let callCount = 0;
|
|
155
|
+
let capturedPath = '';
|
|
156
|
+
let capturedHashPath = '';
|
|
157
|
+
let capturedParams = null;
|
|
158
|
+
await moveToState(stateName);
|
|
159
|
+
router.routeAdd(stateName, '/params-' + stateName + '/:id[09]/:slug[AZ]', (path, hashPath, params) => {
|
|
160
|
+
callCount++;
|
|
161
|
+
capturedPath = path;
|
|
162
|
+
capturedHashPath = hashPath;
|
|
163
|
+
capturedParams = params ?? null;
|
|
164
|
+
});
|
|
165
|
+
await router.route('/params-' + stateName + '/123/ABC');
|
|
166
|
+
t.is(callCount, 1);
|
|
167
|
+
t.is(capturedPath, '/params-' + stateName + '/:09/:AZ');
|
|
168
|
+
t.is(capturedHashPath, '/params-' + stateName + '/123/ABC');
|
|
169
|
+
t.deepEqual(capturedParams, { id: '123', slug: 'ABC' });
|
|
170
|
+
});
|
|
171
|
+
test.serial('Route constraints reject non-matching params and call 404', async (t) => {
|
|
172
|
+
const stateName = nextName('constraint');
|
|
173
|
+
let routeCalled = 0;
|
|
174
|
+
let notFoundCalled = 0;
|
|
175
|
+
await moveToState(stateName);
|
|
176
|
+
router.routeAdd(stateName, '/numeric-' + stateName + '/:id[09]', () => {
|
|
177
|
+
routeCalled++;
|
|
178
|
+
});
|
|
179
|
+
router.routeSpecialAdd(404, () => {
|
|
180
|
+
notFoundCalled++;
|
|
181
|
+
});
|
|
182
|
+
await router.route('/numeric-' + stateName + '/abc');
|
|
183
|
+
t.is(routeCalled, 0);
|
|
184
|
+
t.is(notFoundCalled, 1);
|
|
185
|
+
});
|
|
186
|
+
test.serial('Static route wins over variable route', async (t) => {
|
|
187
|
+
const stateName = nextName('priority');
|
|
188
|
+
let variableCalled = 0;
|
|
189
|
+
let staticCalled = 0;
|
|
190
|
+
await moveToState(stateName);
|
|
191
|
+
router.routeAdd(stateName, '/priority-' + stateName + '/:id[AZ09]', () => {
|
|
192
|
+
variableCalled++;
|
|
193
|
+
});
|
|
194
|
+
router.routeAdd(stateName, '/priority-' + stateName + '/profile', () => {
|
|
195
|
+
staticCalled++;
|
|
196
|
+
});
|
|
197
|
+
await router.route('/priority-' + stateName + '/profile');
|
|
198
|
+
t.is(variableCalled, 0);
|
|
199
|
+
t.is(staticCalled, 1);
|
|
200
|
+
});
|
|
201
|
+
test.serial('Equivalent route definitions are rejected and deletable', async (t) => {
|
|
202
|
+
const stateName = nextName('equiv');
|
|
203
|
+
await moveToState(stateName);
|
|
204
|
+
t.true(router.routeAdd(stateName, '/equiv-' + stateName + '/:id[AZ09]', () => { }));
|
|
205
|
+
t.false(router.routeAdd(stateName, '/equiv-' + stateName + '/:name[AZ]', () => { }));
|
|
206
|
+
t.true(router.routeDel('/equiv-' + stateName + '/:code[09]'));
|
|
207
|
+
t.false(router.routeDel('/equiv-' + stateName + '/:code[09]'));
|
|
208
|
+
});
|
|
209
|
+
test.serial('routeAdd throws for invalid definitions', (t) => {
|
|
210
|
+
const missingState = nextName('throw_missing');
|
|
211
|
+
t.throws(() => {
|
|
212
|
+
router.routeAdd(missingState, '/throw-' + missingState, () => { });
|
|
213
|
+
}, { instanceOf: SyntaxError, message: 'Non-existent state' });
|
|
214
|
+
const stateName = nextName('throw_dup');
|
|
215
|
+
router.stateAdd(stateName);
|
|
216
|
+
t.throws(() => {
|
|
217
|
+
router.routeAdd(stateName, '/duplicate-' + stateName + '/:id/path/:id', () => { });
|
|
218
|
+
}, { instanceOf: SyntaxError, message: 'Duplicate path id' });
|
|
219
|
+
});
|
|
220
|
+
test.serial('Availability false uses route-specific 403 before global 403', async (t) => {
|
|
221
|
+
const stateName = nextName('spec403');
|
|
222
|
+
let specificForbidden = 0;
|
|
223
|
+
let globalForbidden = 0;
|
|
224
|
+
let normalRoute = 0;
|
|
225
|
+
await moveToState(stateName);
|
|
226
|
+
router.routeSpecialAdd(403, () => {
|
|
227
|
+
globalForbidden++;
|
|
228
|
+
});
|
|
229
|
+
router.routeAdd(stateName, '/spec403-' + stateName, () => {
|
|
230
|
+
normalRoute++;
|
|
231
|
+
}, () => {
|
|
232
|
+
return false;
|
|
233
|
+
}, () => {
|
|
234
|
+
specificForbidden++;
|
|
235
|
+
});
|
|
236
|
+
await router.route('/spec403-' + stateName);
|
|
237
|
+
t.is(specificForbidden, 1);
|
|
238
|
+
t.is(globalForbidden, 0);
|
|
239
|
+
t.is(normalRoute, 0);
|
|
240
|
+
});
|
|
241
|
+
test.serial('Availability false falls back to global 403', async (t) => {
|
|
242
|
+
const stateName = nextName('glob403');
|
|
243
|
+
let globalForbidden = 0;
|
|
244
|
+
let normalRoute = 0;
|
|
245
|
+
await moveToState(stateName);
|
|
246
|
+
router.routeSpecialAdd(403, () => {
|
|
247
|
+
globalForbidden++;
|
|
248
|
+
});
|
|
249
|
+
router.routeAdd(stateName, '/glob403-' + stateName, () => {
|
|
250
|
+
normalRoute++;
|
|
251
|
+
}, () => {
|
|
252
|
+
return false;
|
|
253
|
+
});
|
|
254
|
+
await router.route('/glob403-' + stateName);
|
|
255
|
+
t.is(globalForbidden, 1);
|
|
256
|
+
t.is(normalRoute, 0);
|
|
257
|
+
});
|
|
258
|
+
test.serial('Async availability true dispatches normal route function', async (t) => {
|
|
259
|
+
const stateName = nextName('asyncavail');
|
|
260
|
+
let routeCalled = 0;
|
|
261
|
+
await moveToState(stateName);
|
|
262
|
+
router.routeAdd(stateName, '/asyncavail-' + stateName, () => {
|
|
263
|
+
routeCalled++;
|
|
264
|
+
}, async () => {
|
|
265
|
+
return true;
|
|
266
|
+
});
|
|
267
|
+
await router.route('/asyncavail-' + stateName);
|
|
268
|
+
t.is(routeCalled, 1);
|
|
269
|
+
});
|
|
270
|
+
test.serial('Valid-state mismatch uses 500 handler', async (t) => {
|
|
271
|
+
const currentState = nextName('vs_current');
|
|
272
|
+
const requiredState = nextName('vs_required');
|
|
273
|
+
let errorHandlerCalled = 0;
|
|
274
|
+
let routeCalled = 0;
|
|
275
|
+
await moveToState(currentState);
|
|
276
|
+
router.stateAdd(requiredState);
|
|
277
|
+
router.routeSpecialAdd(500, () => {
|
|
278
|
+
errorHandlerCalled++;
|
|
279
|
+
});
|
|
280
|
+
router.routeAdd(requiredState, '/valstate-' + requiredState, () => {
|
|
281
|
+
routeCalled++;
|
|
282
|
+
});
|
|
283
|
+
await router.route('/valstate-' + requiredState);
|
|
284
|
+
t.is(errorHandlerCalled, 1);
|
|
285
|
+
t.is(routeCalled, 0);
|
|
286
|
+
});
|
|
287
|
+
test.serial('No route match uses 404 handler', async (t) => {
|
|
288
|
+
let notFoundCalled = 0;
|
|
289
|
+
router.routeSpecialAdd(404, () => {
|
|
290
|
+
notFoundCalled++;
|
|
291
|
+
});
|
|
292
|
+
await router.route('/unmatched-' + nextName('404'));
|
|
293
|
+
t.is(notFoundCalled, 1);
|
|
294
|
+
});
|
|
295
|
+
test.serial('routeSpecialAdd rejects unsupported status codes', (t) => {
|
|
296
|
+
t.throws(() => {
|
|
297
|
+
router.routeSpecialAdd(418, () => { });
|
|
298
|
+
}, { instanceOf: RangeError });
|
|
299
|
+
});
|
|
300
|
+
test.serial('trigger sets hash and checkHash dispatches current hash', async (t) => {
|
|
301
|
+
const stateName = nextName('hashroute');
|
|
302
|
+
let routeCalls = 0;
|
|
303
|
+
await moveToState(stateName);
|
|
304
|
+
router.routeAdd(stateName, '/hash-' + stateName, () => {
|
|
305
|
+
routeCalls++;
|
|
306
|
+
});
|
|
307
|
+
router.trigger('/hash-' + stateName);
|
|
308
|
+
t.is(dom.window.location.hash, '#/hash-' + stateName);
|
|
309
|
+
await router.checkHash();
|
|
310
|
+
t.true(0 < routeCalls);
|
|
311
|
+
});
|
|
312
|
+
test.serial('Routing queue drains hash changes during active routing', async (t) => {
|
|
313
|
+
const stateName = nextName('queue');
|
|
314
|
+
const events = [];
|
|
315
|
+
await moveToState(stateName);
|
|
316
|
+
router.routeAdd(stateName, '/queue-' + stateName + '/first', async () => {
|
|
317
|
+
events.push('first');
|
|
318
|
+
dom.window.location.hash = '#/queue-' + stateName + '/second';
|
|
319
|
+
await router.checkHash();
|
|
320
|
+
});
|
|
321
|
+
router.routeAdd(stateName, '/queue-' + stateName + '/second', () => {
|
|
322
|
+
events.push('second');
|
|
323
|
+
});
|
|
324
|
+
await router.route('/queue-' + stateName + '/first');
|
|
325
|
+
t.deepEqual(events, ['first', 'second']);
|
|
326
|
+
});
|
|
327
|
+
test.serial('stateSet reentrancy guard rejects nested transition', async (t) => {
|
|
328
|
+
const fromState = nextName('re_from');
|
|
329
|
+
const toState = nextName('re_to');
|
|
330
|
+
const otherState = nextName('re_other');
|
|
331
|
+
let nestedResult = false;
|
|
332
|
+
await moveToState(fromState);
|
|
333
|
+
router.stateAdd(toState);
|
|
334
|
+
router.stateAdd(otherState);
|
|
335
|
+
router.transitionAdd(fromState, toState);
|
|
336
|
+
router.transitionAdd(fromState, otherState);
|
|
337
|
+
router.transitionOnBeforeAdd(fromState, toState, async () => {
|
|
338
|
+
nestedResult = await router.stateSet(otherState);
|
|
339
|
+
});
|
|
340
|
+
const outerResult = await router.stateSet(toState);
|
|
341
|
+
t.true(outerResult);
|
|
342
|
+
t.false(nestedResult);
|
|
343
|
+
t.is(router.state, toState);
|
|
344
|
+
});
|
|
345
|
+
test.serial('All hook sync/async variants', async (t) => {
|
|
346
|
+
const fromState = nextName('syncasync_from');
|
|
347
|
+
const toState = nextName('syncasync_to');
|
|
348
|
+
const events = [];
|
|
349
|
+
await moveToState(fromState);
|
|
350
|
+
router.stateAdd(toState);
|
|
351
|
+
router.transitionAdd(fromState, toState);
|
|
352
|
+
router.transitionOnAfterAdd(fromState, toState, async () => {
|
|
353
|
+
events.push('after:async');
|
|
354
|
+
});
|
|
355
|
+
router.stateOnLeaveAdd(fromState, (currentState, nextState) => {
|
|
356
|
+
events.push('leave:sync:' + currentState + ':' + nextState);
|
|
357
|
+
});
|
|
358
|
+
router.stateOnEnterAdd(toState, (currentState, previousState) => {
|
|
359
|
+
events.push('enter:sync:' + currentState + ':' + previousState);
|
|
360
|
+
});
|
|
361
|
+
const result = await router.stateSet(toState);
|
|
362
|
+
t.true(result);
|
|
363
|
+
t.is(router.state, toState);
|
|
364
|
+
t.deepEqual(events, [
|
|
365
|
+
'leave:sync:' + fromState + ':' + toState,
|
|
366
|
+
'after:async',
|
|
367
|
+
'enter:sync:' + toState + ':' + fromState
|
|
368
|
+
]);
|
|
369
|
+
});
|
|
370
|
+
test.serial('routeDel throws on duplicate path id', (t) => {
|
|
371
|
+
t.throws(() => {
|
|
372
|
+
router.routeDel('/dupdel-' + nextName('dupdel') + '/:id/path/:id');
|
|
373
|
+
}, { instanceOf: SyntaxError, message: 'Duplicate path id' });
|
|
374
|
+
});
|
|
375
|
+
test.serial('Route with untyped param defaults to AZ09', async (t) => {
|
|
376
|
+
const stateName = nextName('untyped');
|
|
377
|
+
let callCount = 0;
|
|
378
|
+
let capturedParams = null;
|
|
379
|
+
await moveToState(stateName);
|
|
380
|
+
router.routeAdd(stateName, '/untyped-' + stateName + '/:name', (_path, _hashPath, params) => {
|
|
381
|
+
callCount++;
|
|
382
|
+
capturedParams = params ?? null;
|
|
383
|
+
});
|
|
384
|
+
await router.route('/untyped-' + stateName + '/hello123');
|
|
385
|
+
t.is(callCount, 1);
|
|
386
|
+
t.deepEqual(capturedParams, { name: 'hello123' });
|
|
387
|
+
});
|
|
388
|
+
test.serial('routeDel with untyped param', async (t) => {
|
|
389
|
+
const stateName = nextName('untypeddel');
|
|
390
|
+
await moveToState(stateName);
|
|
391
|
+
t.true(router.routeAdd(stateName, '/untypeddel-' + stateName + '/:name', () => { }));
|
|
392
|
+
t.true(router.routeDel('/untypeddel-' + stateName + '/:name'));
|
|
393
|
+
});
|
|
394
|
+
test.serial('Non-function available triggers 403', async (t) => {
|
|
395
|
+
const stateName = nextName('nonfn');
|
|
396
|
+
let forbiddenCalled = 0;
|
|
397
|
+
let routeCalled = 0;
|
|
398
|
+
await moveToState(stateName);
|
|
399
|
+
router.routeSpecialAdd(403, () => {
|
|
400
|
+
forbiddenCalled++;
|
|
401
|
+
});
|
|
402
|
+
router.routeAdd(stateName, '/nonfn-' + stateName, () => {
|
|
403
|
+
routeCalled++;
|
|
404
|
+
}, 'not-a-function');
|
|
405
|
+
await router.route('/nonfn-' + stateName);
|
|
406
|
+
t.is(forbiddenCalled, 1);
|
|
407
|
+
t.is(routeCalled, 0);
|
|
408
|
+
});
|