hmpo-form-wizard 15.0.6 → 17.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/.github/workflows/ci.yaml +1 -1
- package/.nvmrc +1 -0
- package/README.md +86 -0
- package/lib/controller/index.js +21 -11
- package/lib/controller/mixins/check-progress.js +14 -0
- package/lib/controller/mixins/edit-step.js +9 -0
- package/package.json +7 -7
- package/test/controller/mixins/spec.check-progress.js +92 -0
- package/test/controller/mixins/spec.edit-step.js +35 -0
- package/test/controller/spec.index.js +58 -41
- package/test/controller/spec.lifecycle.js +3 -28
package/.nvmrc
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
v24.14.0
|
package/README.md
CHANGED
|
@@ -177,11 +177,14 @@ Any of these options can also be provided as a third argument to the wizard to c
|
|
|
177
177
|
* `resetJourney` - Reset the journey `journeyModel` when this step is accessed.
|
|
178
178
|
* `skip` - A template is not rendered on a GET request. The `post()` lifecycle is called instead. Defaults to `false`.
|
|
179
179
|
* `noPost` - Don't allow posting to this step. The post method is set to null and the step is completed if there is a next step
|
|
180
|
+
* `hub` - Mark this step as a hub: it is recorded in journey history on GET without storing a `next` pointer. This allows other steps to declare `prereqs` referencing this step without the hub influencing skip-ahead navigation. Use with `nonLinearJourney: true` on the wizard. Defaults to `false`.
|
|
181
|
+
* `setValuesOnSave` - Array of `{ key, value }` pairs to unconditionally set on the session when this step is submitted (in `saveValues`). Useful for recording completion state flags. e.g. `setValuesOnSave: [{ key: 'sectionComplete', value: 'completed' }]`
|
|
180
182
|
* `forwardQuery` - forward the query params when internally redirecting. Defaults to `false`.
|
|
181
183
|
* `editable` - This step is editable. This allows accessing this step with the `editSuffix` and sets the back link and next step to the `editBackStep`. Defaults to `false`.
|
|
182
184
|
* `editSuffix` - Suffix to use for editing steps. Defaults to `/edit`.
|
|
183
185
|
* `editBackStep` - Location to return to after editing a step. Defaults to `confirm`
|
|
184
186
|
* `continueOnEdit` - While editing, if the step marked with this is evaluated to be the next step, continue to editing it instead of returning to `editBackStep`. Defaults to `false`.
|
|
187
|
+
* `nonLinearJourney` - Enable support for branching journeys. When `true`, edit mode allows access to previously-completed steps even when not reachable by normal chain-walking. Defaults to `false`. Set as a wizard-level option.
|
|
185
188
|
* `fields` - specifies which of the fields from the field definition list are applied to this step. Form inputs which are not named on this list will not be processed. Default: `[]`
|
|
186
189
|
* `template` - Specifies the template to render for GET requests to this step. Defaults to the route (without trailing slash)
|
|
187
190
|
* `templatePath` - provides the location within `app.get('views')` that templates are stored.
|
|
@@ -360,6 +363,89 @@ These controllers can be overridden in a custom controller to provide additional
|
|
|
360
363
|
> #### - `errorHandler(err, req, res, next)`
|
|
361
364
|
> Additional error handling can be performed by overriding the `errorHandler`.
|
|
362
365
|
|
|
366
|
+
## Non-linear journeys
|
|
367
|
+
|
|
368
|
+
By default, the wizard validates step access by walking a linear chain of `next` pointers in journey history. This works well for sequential journeys but breaks when a journey branches into multiple independent sections from a central step.
|
|
369
|
+
|
|
370
|
+
To support this pattern, two opt-in options are provided:
|
|
371
|
+
|
|
372
|
+
### `hub: true` (step option)
|
|
373
|
+
|
|
374
|
+
Marks a step as a hub. On each GET request, the step is recorded in journey history with only `path` and `wizard` — no `next`. This allows other steps to declare `prereqs: ['hub-step']` to validate that the hub has been visited, without the hub interfering with skip-ahead navigation.
|
|
375
|
+
|
|
376
|
+
```js
|
|
377
|
+
'/dashboard': {
|
|
378
|
+
noPost: true,
|
|
379
|
+
checkJourney: false,
|
|
380
|
+
hub: true
|
|
381
|
+
}
|
|
382
|
+
```
|
|
383
|
+
|
|
384
|
+
The `checkJourney: false` is typically set alongside `hub: true` since hub steps are dispatch points, not part of a linear sequence.
|
|
385
|
+
|
|
386
|
+
### `nonLinearJourney: true` (wizard option)
|
|
387
|
+
|
|
388
|
+
Enables two fallbacks for edit mode that the standard chain walk cannot handle:
|
|
389
|
+
|
|
390
|
+
1. **Progress check**: When editing, a step that was previously completed (present in history with a `next` and not invalid) is allowed even if the chain walk cannot reach it.
|
|
391
|
+
2. **Next step**: When editing, `editBackStep` is returned if the current step was previously visited, bypassing the normal reachability check.
|
|
392
|
+
|
|
393
|
+
Set at the wizard level so it applies to all steps:
|
|
394
|
+
|
|
395
|
+
```js
|
|
396
|
+
app.use(wizard(steps, fields, {
|
|
397
|
+
name: 'my-wizard',
|
|
398
|
+
editBackStep: 'summary',
|
|
399
|
+
nonLinearJourney: true
|
|
400
|
+
}));
|
|
401
|
+
```
|
|
402
|
+
|
|
403
|
+
### Full example
|
|
404
|
+
|
|
405
|
+
```js
|
|
406
|
+
// steps.js
|
|
407
|
+
module.exports = {
|
|
408
|
+
'/dashboard': {
|
|
409
|
+
noPost: true,
|
|
410
|
+
checkJourney: false,
|
|
411
|
+
hub: true
|
|
412
|
+
},
|
|
413
|
+
'/section-a': {
|
|
414
|
+
prereqs: ['dashboard'],
|
|
415
|
+
editable: true,
|
|
416
|
+
next: 'section-a-details'
|
|
417
|
+
},
|
|
418
|
+
'/section-a-details': {
|
|
419
|
+
editable: true,
|
|
420
|
+
next: 'dashboard',
|
|
421
|
+
setValuesOnSave: [{ key: 'sectionAComplete', value: 'completed' }]
|
|
422
|
+
},
|
|
423
|
+
'/section-b': {
|
|
424
|
+
prereqs: ['dashboard'],
|
|
425
|
+
editable: true,
|
|
426
|
+
next: 'section-b-details'
|
|
427
|
+
},
|
|
428
|
+
'/section-b-details': {
|
|
429
|
+
editable: true,
|
|
430
|
+
next: 'dashboard',
|
|
431
|
+
setValuesOnSave: [{ key: 'sectionBComplete', value: 'completed' }]
|
|
432
|
+
},
|
|
433
|
+
'/summary': {
|
|
434
|
+
checkJourney: false,
|
|
435
|
+
prereqs: ['dashboard']
|
|
436
|
+
}
|
|
437
|
+
}
|
|
438
|
+
|
|
439
|
+
// index.js
|
|
440
|
+
app.use(wizard(steps, fields, {
|
|
441
|
+
name: 'my-wizard',
|
|
442
|
+
editBackStep: 'summary',
|
|
443
|
+
nonLinearJourney: true
|
|
444
|
+
}));
|
|
445
|
+
```
|
|
446
|
+
|
|
447
|
+
In this pattern, a user visits `/dashboard`, enters any section, completes it (which sets a flag via `setValuesOnSave`), and returns to `/dashboard`. They can later edit any previously-completed section step from `/summary` and be returned to `/summary` afterwards.
|
|
448
|
+
|
|
363
449
|
## Example app
|
|
364
450
|
|
|
365
451
|
An example application can be found in [the ./example directory](./example). To run this, follow the instructions in the [README](./example/README.md).
|
package/lib/controller/index.js
CHANGED
|
@@ -94,11 +94,8 @@ class BaseController {
|
|
|
94
94
|
rejectUnsupportedMethods(req, res, next) {
|
|
95
95
|
const method = req.method.toLowerCase();
|
|
96
96
|
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
const unsupported = typeof this[method] !== 'function' || postBlocked;
|
|
100
|
-
|
|
101
|
-
if (unsupported) {
|
|
97
|
+
if ((typeof this[method] !== 'function') ||
|
|
98
|
+
(req.form.options.skip && method === 'post')) {
|
|
102
99
|
return this.methodNotSupported(req, res, next);
|
|
103
100
|
}
|
|
104
101
|
|
|
@@ -237,20 +234,28 @@ class BaseController {
|
|
|
237
234
|
|
|
238
235
|
_checkStatus(req, res, next) {
|
|
239
236
|
debug('%s #_checkStatus', req.originalUrl);
|
|
240
|
-
|
|
241
|
-
const noPost = req.form.options.noPost === true || typeof this.post !== 'function';
|
|
242
237
|
if (req.form.options.skip) {
|
|
243
|
-
if (
|
|
244
|
-
return this.
|
|
238
|
+
if (typeof this.post === 'function') {
|
|
239
|
+
return this.post(req, res, next);
|
|
245
240
|
}
|
|
246
|
-
return this.
|
|
241
|
+
return this.successHandler(req, res, next);
|
|
247
242
|
}
|
|
248
243
|
|
|
249
244
|
const pathname = url(req).pathname;
|
|
250
|
-
if (
|
|
245
|
+
if (typeof this.post !== 'function' && res.locals.nextPage !== pathname && req.form.options.checkJourney) {
|
|
251
246
|
this.setStepComplete(req, res);
|
|
252
247
|
}
|
|
253
248
|
|
|
249
|
+
if (req.form.options.hub) {
|
|
250
|
+
let journeyHistory = req.journeyModel.get('history') || [];
|
|
251
|
+
if (!_.find(journeyHistory, { path: req.form.options.fullPath })) {
|
|
252
|
+
this.addJourneyHistoryStep(req, res, {
|
|
253
|
+
path: req.form.options.fullPath,
|
|
254
|
+
wizard: req.form.options.name
|
|
255
|
+
});
|
|
256
|
+
}
|
|
257
|
+
}
|
|
258
|
+
|
|
254
259
|
next();
|
|
255
260
|
}
|
|
256
261
|
|
|
@@ -354,6 +359,11 @@ class BaseController {
|
|
|
354
359
|
|
|
355
360
|
saveValues(req, res, next) {
|
|
356
361
|
debug('%s #saveValues', req.originalUrl);
|
|
362
|
+
if (req.form.options?.setValuesOnSave) {
|
|
363
|
+
for (const { key, value } of req.form.options.setValuesOnSave) {
|
|
364
|
+
req.form.values[key] = value;
|
|
365
|
+
}
|
|
366
|
+
}
|
|
357
367
|
req.sessionModel.set(req.form.values);
|
|
358
368
|
req.sessionModel.unset('errorValues');
|
|
359
369
|
next();
|
|
@@ -36,6 +36,14 @@ module.exports = Controller => class extends Controller {
|
|
|
36
36
|
return next();
|
|
37
37
|
}
|
|
38
38
|
|
|
39
|
+
if (req.isEditing && req.form.options.nonLinearJourney) {
|
|
40
|
+
let visited = this.visitedJourneyStep(req, res, path);
|
|
41
|
+
if (visited && visited.next && !visited.invalid) {
|
|
42
|
+
debug('Step is allowed by edit history', path);
|
|
43
|
+
return next();
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
|
|
39
47
|
debug('Step missing prereq', path);
|
|
40
48
|
|
|
41
49
|
let err = new Error('Missing prereq for this step');
|
|
@@ -92,6 +100,12 @@ module.exports = Controller => class extends Controller {
|
|
|
92
100
|
return this.walkJourneyHistory(req, res, step => step.path === path);
|
|
93
101
|
}
|
|
94
102
|
|
|
103
|
+
// return step from history by direct lookup (no chain walking)
|
|
104
|
+
visitedJourneyStep(req, res, path) {
|
|
105
|
+
let journeyHistory = req.journeyModel.get('history') || [];
|
|
106
|
+
return _.find(journeyHistory, step => step.path === path);
|
|
107
|
+
}
|
|
108
|
+
|
|
95
109
|
// return step that allows a given path
|
|
96
110
|
allowedJourneyStep(req, res, path) {
|
|
97
111
|
return this.walkJourneyHistory(req, res, step => step.next === path);
|
|
@@ -89,6 +89,15 @@ module.exports = Controller => class extends Controller {
|
|
|
89
89
|
return editBackStep;
|
|
90
90
|
}
|
|
91
91
|
|
|
92
|
+
// if the current step has been previously visited: return to edit back step
|
|
93
|
+
if (req.form.options.nonLinearJourney) {
|
|
94
|
+
let visitedStep = this.visitedJourneyStep(req, res, req.form.options.fullPath);
|
|
95
|
+
if (visitedStep && !visitedStep.invalid) {
|
|
96
|
+
debug('edit journey returning to edit back step via visited step', editBackStep);
|
|
97
|
+
return editBackStep;
|
|
98
|
+
}
|
|
99
|
+
}
|
|
100
|
+
|
|
92
101
|
// go to the last step in history in edit mode to allow returning to edit-back-step
|
|
93
102
|
let lastAllowedStep = this.lastAllowedStep(req, res);
|
|
94
103
|
if (lastAllowedStep) {
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "hmpo-form-wizard",
|
|
3
|
-
"version": "
|
|
3
|
+
"version": "17.0.0",
|
|
4
4
|
"description": "Routing and request handling for a multi-step form processes",
|
|
5
5
|
"main": "index.js",
|
|
6
6
|
"scripts": {
|
|
@@ -17,7 +17,7 @@
|
|
|
17
17
|
"author": "HMPO",
|
|
18
18
|
"license": "MIT",
|
|
19
19
|
"engines": {
|
|
20
|
-
"node": "
|
|
20
|
+
"node": "24.x"
|
|
21
21
|
},
|
|
22
22
|
"bugs": {
|
|
23
23
|
"url": "https://github.com/HMPO/hmpo-form-wizard/issues"
|
|
@@ -28,7 +28,7 @@
|
|
|
28
28
|
"csrf": "^3.1.0",
|
|
29
29
|
"debug": "4.3.7",
|
|
30
30
|
"deep-clone-merge": "^1.5.5",
|
|
31
|
-
"hmpo-model": "^
|
|
31
|
+
"hmpo-model": "^7.0.1",
|
|
32
32
|
"moment": "^2.30.1",
|
|
33
33
|
"underscore": "^1.13.7"
|
|
34
34
|
},
|
|
@@ -40,15 +40,15 @@
|
|
|
40
40
|
"chai": "^4.5.0",
|
|
41
41
|
"eslint": "^9.12.0",
|
|
42
42
|
"express": "^4.21.2",
|
|
43
|
+
"globals": "^15.11.0",
|
|
44
|
+
"hmpo-reqres": "^3.0.0",
|
|
45
|
+
"husky": "^9.1.6",
|
|
43
46
|
"json5": "^2.2.3",
|
|
44
47
|
"mocha": "^10.7.3",
|
|
45
48
|
"nyc": "^17.1.0",
|
|
46
49
|
"proxyquire": "^2.1.3",
|
|
47
|
-
"hmpo-reqres": "^2.0.0",
|
|
48
50
|
"sinon": "^21.0.2",
|
|
49
|
-
"sinon-chai": "^3.7.0"
|
|
50
|
-
"husky": "^9.1.6",
|
|
51
|
-
"globals": "^15.11.0"
|
|
51
|
+
"sinon-chai": "^3.7.0"
|
|
52
52
|
},
|
|
53
53
|
"overrides": {
|
|
54
54
|
"serialize-javascript": "7.0.4"
|
|
@@ -182,6 +182,66 @@ describe('mixins/check-progress', () => {
|
|
|
182
182
|
next.should.have.been.calledOnce;
|
|
183
183
|
next.should.have.been.calledWithExactly();
|
|
184
184
|
});
|
|
185
|
+
|
|
186
|
+
it('calls callback with no arguments if editing and step is visited in history with a next', () => {
|
|
187
|
+
req.isEditing = true;
|
|
188
|
+
controller.options.nonLinearJourney = true;
|
|
189
|
+
req.journeyModel.set('history', [
|
|
190
|
+
{ path: '/base/hub', wizard: 'wizard' },
|
|
191
|
+
{ path: '/base/teststep', next: '/base/nextstep', wizard: 'wizard' }
|
|
192
|
+
]);
|
|
193
|
+
controller.checkJourneyProgress(req, res, next);
|
|
194
|
+
next.should.have.been.calledOnce;
|
|
195
|
+
next.should.have.been.calledWithExactly();
|
|
196
|
+
});
|
|
197
|
+
|
|
198
|
+
it('calls callback with MISSING_PREREQ if editing but step has no next in history', () => {
|
|
199
|
+
req.isEditing = true;
|
|
200
|
+
controller.options.nonLinearJourney = true;
|
|
201
|
+
req.journeyModel.set('history', [
|
|
202
|
+
{ path: '/base/teststep', wizard: 'wizard' }
|
|
203
|
+
]);
|
|
204
|
+
controller.checkJourneyProgress(req, res, next);
|
|
205
|
+
next.should.have.been.calledOnce;
|
|
206
|
+
next.args[0][0].should.be.an.instanceOf(Error);
|
|
207
|
+
next.args[0][0].code.should.equal('MISSING_PREREQ');
|
|
208
|
+
});
|
|
209
|
+
|
|
210
|
+
it('calls callback with MISSING_PREREQ if editing but step is invalid in history', () => {
|
|
211
|
+
req.isEditing = true;
|
|
212
|
+
controller.options.nonLinearJourney = true;
|
|
213
|
+
req.journeyModel.set('history', [
|
|
214
|
+
{ path: '/base/teststep', next: '/base/nextstep', invalid: true }
|
|
215
|
+
]);
|
|
216
|
+
controller.checkJourneyProgress(req, res, next);
|
|
217
|
+
next.should.have.been.calledOnce;
|
|
218
|
+
next.args[0][0].should.be.an.instanceOf(Error);
|
|
219
|
+
next.args[0][0].code.should.equal('MISSING_PREREQ');
|
|
220
|
+
});
|
|
221
|
+
|
|
222
|
+
it('does not use edit history fallback if not editing', () => {
|
|
223
|
+
controller.options.nonLinearJourney = true;
|
|
224
|
+
req.journeyModel.set('history', [
|
|
225
|
+
{ path: '/base/hub', wizard: 'wizard' },
|
|
226
|
+
{ path: '/base/teststep', next: '/base/nextstep', wizard: 'wizard' }
|
|
227
|
+
]);
|
|
228
|
+
controller.checkJourneyProgress(req, res, next);
|
|
229
|
+
next.should.have.been.calledOnce;
|
|
230
|
+
next.args[0][0].should.be.an.instanceOf(Error);
|
|
231
|
+
next.args[0][0].code.should.equal('MISSING_PREREQ');
|
|
232
|
+
});
|
|
233
|
+
|
|
234
|
+
it('does not use edit history fallback if nonLinearJourney is not set', () => {
|
|
235
|
+
req.isEditing = true;
|
|
236
|
+
req.journeyModel.set('history', [
|
|
237
|
+
{ path: '/base/hub', wizard: 'wizard' },
|
|
238
|
+
{ path: '/base/teststep', next: '/base/nextstep', wizard: 'wizard' }
|
|
239
|
+
]);
|
|
240
|
+
controller.checkJourneyProgress(req, res, next);
|
|
241
|
+
next.should.have.been.calledOnce;
|
|
242
|
+
next.args[0][0].should.be.an.instanceOf(Error);
|
|
243
|
+
next.args[0][0].code.should.equal('MISSING_PREREQ');
|
|
244
|
+
});
|
|
185
245
|
});
|
|
186
246
|
|
|
187
247
|
describe('checkProceedToNextStep', () => {
|
|
@@ -480,6 +540,38 @@ describe('mixins/check-progress', () => {
|
|
|
480
540
|
|
|
481
541
|
});
|
|
482
542
|
|
|
543
|
+
describe('visitedJourneyStep', () => {
|
|
544
|
+
it('returns the step if it exists in history', () => {
|
|
545
|
+
req.journeyModel.set('history', [
|
|
546
|
+
{ path: '/base/hub', wizard: 'wizard' },
|
|
547
|
+
{ path: '/base/teststep', next: '/base/nextstep', wizard: 'wizard' }
|
|
548
|
+
]);
|
|
549
|
+
let result = controller.visitedJourneyStep(req, res, '/base/teststep');
|
|
550
|
+
result.should.deep.equal({ path: '/base/teststep', next: '/base/nextstep', wizard: 'wizard' });
|
|
551
|
+
});
|
|
552
|
+
|
|
553
|
+
it('returns the step even if it has no next', () => {
|
|
554
|
+
req.journeyModel.set('history', [
|
|
555
|
+
{ path: '/base/hub', wizard: 'wizard' }
|
|
556
|
+
]);
|
|
557
|
+
let result = controller.visitedJourneyStep(req, res, '/base/hub');
|
|
558
|
+
result.should.deep.equal({ path: '/base/hub', wizard: 'wizard' });
|
|
559
|
+
});
|
|
560
|
+
|
|
561
|
+
it('returns undefined if step is not in history', () => {
|
|
562
|
+
req.journeyModel.set('history', [
|
|
563
|
+
{ path: '/base/other', next: '/base/nextstep' }
|
|
564
|
+
]);
|
|
565
|
+
let result = controller.visitedJourneyStep(req, res, '/base/teststep');
|
|
566
|
+
expect(result).to.be.undefined;
|
|
567
|
+
});
|
|
568
|
+
|
|
569
|
+
it('returns undefined if history is empty', () => {
|
|
570
|
+
let result = controller.visitedJourneyStep(req, res, '/base/teststep');
|
|
571
|
+
expect(result).to.be.undefined;
|
|
572
|
+
});
|
|
573
|
+
});
|
|
574
|
+
|
|
483
575
|
describe('addJourneyHistoryStep', () => {
|
|
484
576
|
it('creates a step history and adds step if there is no existing step history', () => {
|
|
485
577
|
controller.addJourneyHistoryStep(req, res,
|
|
@@ -221,6 +221,41 @@ describe('mixins/edit-step', () => {
|
|
|
221
221
|
controller.getNextStep(req, res).should.equal('/base/url/backstep');
|
|
222
222
|
});
|
|
223
223
|
|
|
224
|
+
it('returns editBackStep if the current step has been previously visited', () => {
|
|
225
|
+
req.journeyModel.set('history', [
|
|
226
|
+
{ path: '/base/url/hub' },
|
|
227
|
+
{ path: '/base/url/path', next: '/base/url/nextstep' }
|
|
228
|
+
]);
|
|
229
|
+
req.isEditing = true;
|
|
230
|
+
options.nonLinearJourney = true;
|
|
231
|
+
options.fullPath = '/base/url/path';
|
|
232
|
+
controller.getNextStepObject.returns({ url: 'nextstep', condition: {} });
|
|
233
|
+
controller.getNextStep(req, res).should.equal('/base/url/backstep');
|
|
234
|
+
});
|
|
235
|
+
|
|
236
|
+
it('does not return editBackStep via visited step if step is invalid', () => {
|
|
237
|
+
req.journeyModel.set('history', [
|
|
238
|
+
{ path: '/base/url/hub' },
|
|
239
|
+
{ path: '/base/url/path', next: '/base/url/nextstep', invalid: true }
|
|
240
|
+
]);
|
|
241
|
+
req.isEditing = true;
|
|
242
|
+
options.nonLinearJourney = true;
|
|
243
|
+
options.fullPath = '/base/url/path';
|
|
244
|
+
controller.getNextStepObject.returns({ url: 'nextstep', condition: {} });
|
|
245
|
+
controller.getNextStep(req, res).should.not.equal('/base/url/backstep');
|
|
246
|
+
});
|
|
247
|
+
|
|
248
|
+
it('does not use visited step fallback if nonLinearJourney is not set', () => {
|
|
249
|
+
req.journeyModel.set('history', [
|
|
250
|
+
{ path: '/base/url/hub' },
|
|
251
|
+
{ path: '/base/url/path', next: '/base/url/nextstep' }
|
|
252
|
+
]);
|
|
253
|
+
req.isEditing = true;
|
|
254
|
+
options.fullPath = '/base/url/path';
|
|
255
|
+
controller.getNextStepObject.returns({ url: 'nextstep', condition: {} });
|
|
256
|
+
controller.getNextStep(req, res).should.not.equal('/base/url/backstep');
|
|
257
|
+
});
|
|
258
|
+
|
|
224
259
|
it('returns last valid next in history in edit mode with next arg if backstep is not valid', () => {
|
|
225
260
|
req.journeyModel.set('history', [
|
|
226
261
|
{ path: '/base/url/a', next: '/base/url/b' },
|
|
@@ -4,6 +4,7 @@ const Controller = require('../../lib/controller');
|
|
|
4
4
|
const ErrorClass = require('../../lib/error');
|
|
5
5
|
const formatting = require('../../lib/formatting');
|
|
6
6
|
const validation = require('../../lib/validation');
|
|
7
|
+
// const _ = require('underscore');
|
|
7
8
|
|
|
8
9
|
const proxyquire = require('proxyquire');
|
|
9
10
|
const express = require('express');
|
|
@@ -16,8 +17,6 @@ describe('Form Controller', () => {
|
|
|
16
17
|
options = {
|
|
17
18
|
route: '/route',
|
|
18
19
|
checkJourney: true,
|
|
19
|
-
skip: false,
|
|
20
|
-
noPost: false,
|
|
21
20
|
next: 'nextstep',
|
|
22
21
|
template: 'template',
|
|
23
22
|
fields: {
|
|
@@ -95,19 +94,12 @@ describe('Form Controller', () => {
|
|
|
95
94
|
});
|
|
96
95
|
|
|
97
96
|
it('should remove post method if noPost option is set', () =>{
|
|
98
|
-
options.noPost
|
|
97
|
+
options.noPost = true;
|
|
99
98
|
let controller = new Controller(options);
|
|
100
99
|
expect(controller.post).to.be.null;
|
|
101
100
|
});
|
|
102
101
|
|
|
103
|
-
it('should leave post method if noPost option is not set', () =>
|
|
104
|
-
delete options.noPost;
|
|
105
|
-
let controller = new Controller(options);
|
|
106
|
-
controller.post.should.be.a('function');
|
|
107
|
-
});
|
|
108
|
-
|
|
109
|
-
it('should leave post method if noPost option is set to false', () =>{
|
|
110
|
-
options.noPost = false;
|
|
102
|
+
it('should leave post method if noPost option is not set', () =>{
|
|
111
103
|
let controller = new Controller(options);
|
|
112
104
|
controller.post.should.be.a('function');
|
|
113
105
|
});
|
|
@@ -332,14 +324,6 @@ describe('Form Controller', () => {
|
|
|
332
324
|
next.should.not.have.been.called;
|
|
333
325
|
});
|
|
334
326
|
|
|
335
|
-
it('calls methodNotSupported for POSTing when noPost is set dynamically', () => {
|
|
336
|
-
req.form.options.noPost = true;
|
|
337
|
-
req.method = 'POST';
|
|
338
|
-
controller.rejectUnsupportedMethods(req, res, next);
|
|
339
|
-
controller.methodNotSupported.should.have.been.calledWithExactly(req, res, next);
|
|
340
|
-
next.should.not.have.been.called;
|
|
341
|
-
});
|
|
342
|
-
|
|
343
327
|
it('does not call methodNotSupported for supported methods', () => {
|
|
344
328
|
req.method = 'POST';
|
|
345
329
|
controller.rejectUnsupportedMethods(req, res, next);
|
|
@@ -502,6 +486,7 @@ describe('Form Controller', () => {
|
|
|
502
486
|
});
|
|
503
487
|
});
|
|
504
488
|
|
|
489
|
+
|
|
505
490
|
describe('_getErrors', () => {
|
|
506
491
|
let controller;
|
|
507
492
|
|
|
@@ -745,9 +730,8 @@ describe('Form Controller', () => {
|
|
|
745
730
|
sinon.stub(controller, 'successHandler');
|
|
746
731
|
});
|
|
747
732
|
|
|
748
|
-
it('should call post if skip is true
|
|
733
|
+
it('should call post if skip is true', () => {
|
|
749
734
|
options.skip = true;
|
|
750
|
-
options.noPost = false;
|
|
751
735
|
controller._checkStatus(req, res, next);
|
|
752
736
|
next.should.not.have.been.called;
|
|
753
737
|
controller.post.should.have.been.calledWithExactly(req, res, next);
|
|
@@ -759,27 +743,17 @@ describe('Form Controller', () => {
|
|
|
759
743
|
next.should.have.been.calledWithExactly();
|
|
760
744
|
});
|
|
761
745
|
|
|
762
|
-
it('should call the successHandler if skip is set
|
|
746
|
+
it('should call the successHandler if skip is set but there is no post method', () => {
|
|
763
747
|
options.skip = true;
|
|
764
|
-
options.noPost = true;
|
|
765
|
-
controller._checkStatus(req, res, next);
|
|
766
|
-
next.should.not.have.been.called;
|
|
767
|
-
controller.successHandler.should.have.been.calledWithExactly(req, res, next);
|
|
768
|
-
});
|
|
769
|
-
|
|
770
|
-
it('should call successHandler if skip is true and post is not a function', () => {
|
|
771
|
-
options.skip = true;
|
|
772
|
-
options.noPost = false;
|
|
773
748
|
controller.post = null;
|
|
774
749
|
controller._checkStatus(req, res, next);
|
|
775
750
|
next.should.not.have.been.called;
|
|
776
751
|
controller.successHandler.should.have.been.calledWithExactly(req, res, next);
|
|
777
752
|
});
|
|
778
753
|
|
|
779
|
-
it('should call setStepComplete if the step has a next page
|
|
754
|
+
it('should call setStepComplete if the step has a next page and no post method', () => {
|
|
780
755
|
res.locals.nextPage = '/next/page';
|
|
781
|
-
|
|
782
|
-
options.checkJourney = true;
|
|
756
|
+
controller.post = null;
|
|
783
757
|
controller._checkStatus(req, res, next);
|
|
784
758
|
controller.setStepComplete.should.have.been.calledOnce;
|
|
785
759
|
controller.setStepComplete.should.have.been.calledWithExactly(req, res);
|
|
@@ -788,19 +762,40 @@ describe('Form Controller', () => {
|
|
|
788
762
|
|
|
789
763
|
it('should not call setStepComplete if the next page is the same as the current url', () => {
|
|
790
764
|
res.locals.nextPage = '/base/route';
|
|
765
|
+
controller.post = null;
|
|
791
766
|
controller._checkStatus(req, res, next);
|
|
792
767
|
controller.setStepComplete.should.not.have.been.called;
|
|
793
768
|
next.should.have.been.calledWithExactly();
|
|
794
769
|
});
|
|
795
770
|
|
|
796
|
-
it('should
|
|
797
|
-
|
|
798
|
-
options.
|
|
799
|
-
options.
|
|
800
|
-
|
|
771
|
+
it('should add hub step to history on first visit when hub option is true', () => {
|
|
772
|
+
sinon.stub(controller, 'addJourneyHistoryStep');
|
|
773
|
+
options.hub = true;
|
|
774
|
+
options.fullPath = '/base/route';
|
|
775
|
+
options.name = 'test-wizard';
|
|
801
776
|
controller._checkStatus(req, res, next);
|
|
802
|
-
controller.
|
|
803
|
-
controller.
|
|
777
|
+
controller.addJourneyHistoryStep.should.have.been.calledOnce;
|
|
778
|
+
controller.addJourneyHistoryStep.should.have.been.calledWithExactly(req, res, {
|
|
779
|
+
path: '/base/route',
|
|
780
|
+
wizard: 'test-wizard'
|
|
781
|
+
});
|
|
782
|
+
next.should.have.been.calledWithExactly();
|
|
783
|
+
});
|
|
784
|
+
|
|
785
|
+
it('should not add hub step to history if already present', () => {
|
|
786
|
+
sinon.stub(controller, 'addJourneyHistoryStep');
|
|
787
|
+
options.hub = true;
|
|
788
|
+
options.fullPath = '/base/route';
|
|
789
|
+
req.journeyModel.set('history', [{ path: '/base/route' }]);
|
|
790
|
+
controller._checkStatus(req, res, next);
|
|
791
|
+
controller.addJourneyHistoryStep.should.not.have.been.called;
|
|
792
|
+
next.should.have.been.calledWithExactly();
|
|
793
|
+
});
|
|
794
|
+
|
|
795
|
+
it('should not add hub step to history when hub option is not set', () => {
|
|
796
|
+
sinon.stub(controller, 'addJourneyHistoryStep');
|
|
797
|
+
controller._checkStatus(req, res, next);
|
|
798
|
+
controller.addJourneyHistoryStep.should.not.have.been.called;
|
|
804
799
|
next.should.have.been.calledWithExactly();
|
|
805
800
|
});
|
|
806
801
|
});
|
|
@@ -1199,6 +1194,28 @@ describe('Form Controller', () => {
|
|
|
1199
1194
|
controller.saveValues(req, res, next);
|
|
1200
1195
|
next.should.have.been.calledWithExactly();
|
|
1201
1196
|
});
|
|
1197
|
+
|
|
1198
|
+
it('sets setValuesOnSave values in form values before saving', () => {
|
|
1199
|
+
req.form.options = { setValuesOnSave: [{ key: 'sectionComplete', value: true }] };
|
|
1200
|
+
controller.saveValues(req, res, next);
|
|
1201
|
+
req.sessionModel.get('sectionComplete').should.equal(true);
|
|
1202
|
+
});
|
|
1203
|
+
|
|
1204
|
+
it('sets multiple setValuesOnSave values', () => {
|
|
1205
|
+
req.form.options = { setValuesOnSave: [
|
|
1206
|
+
{ key: 'sectionComplete', value: true },
|
|
1207
|
+
{ key: 'sectionStatus', value: 'done' }
|
|
1208
|
+
] };
|
|
1209
|
+
controller.saveValues(req, res, next);
|
|
1210
|
+
req.sessionModel.get('sectionComplete').should.equal(true);
|
|
1211
|
+
req.sessionModel.get('sectionStatus').should.equal('done');
|
|
1212
|
+
});
|
|
1213
|
+
|
|
1214
|
+
it('does not set values when setValuesOnSave is not configured', () => {
|
|
1215
|
+
req.form.options = {};
|
|
1216
|
+
controller.saveValues(req, res, next);
|
|
1217
|
+
expect(req.sessionModel.get('sectionComplete')).to.be.undefined;
|
|
1218
|
+
});
|
|
1202
1219
|
});
|
|
1203
1220
|
|
|
1204
1221
|
describe('successHandler', () => {
|
|
@@ -10,8 +10,6 @@ describe('Controller Lifecycle', () => {
|
|
|
10
10
|
options = {
|
|
11
11
|
route: '/route',
|
|
12
12
|
checkJourney: true,
|
|
13
|
-
skip: false,
|
|
14
|
-
noPost: false,
|
|
15
13
|
next: 'nextstep',
|
|
16
14
|
template: 'template',
|
|
17
15
|
fields: {
|
|
@@ -305,31 +303,8 @@ describe('Controller Lifecycle', () => {
|
|
|
305
303
|
handler(req, res, next);
|
|
306
304
|
});
|
|
307
305
|
|
|
308
|
-
it('runs the successHandler if the skip
|
|
306
|
+
it('runs the successHandler if the skip options is set and there is no post handler', done => {
|
|
309
307
|
controller.options.skip = true;
|
|
310
|
-
controller.options.noPost = true;
|
|
311
|
-
|
|
312
|
-
assert = () => {
|
|
313
|
-
controller.successHandler.should.have.been
|
|
314
|
-
.calledOnce
|
|
315
|
-
.calledAfter(controller._checkStatus)
|
|
316
|
-
.and.calledOn(controller)
|
|
317
|
-
.and.calledWithExactly(req, res, sinon.match.func);
|
|
318
|
-
controller.setStepComplete.should.have.been
|
|
319
|
-
.calledOnce
|
|
320
|
-
.calledAfter(controller.successHandler)
|
|
321
|
-
.and.calledOn(controller)
|
|
322
|
-
.and.calledWithExactly(req, res);
|
|
323
|
-
controller.render.should.not.have.been.called;
|
|
324
|
-
done();
|
|
325
|
-
};
|
|
326
|
-
|
|
327
|
-
handler(req, res, next);
|
|
328
|
-
});
|
|
329
|
-
|
|
330
|
-
it('runs the successHandler if skip is set and post is not a function', done => {
|
|
331
|
-
controller.options.skip = true;
|
|
332
|
-
controller.options.noPost = false;
|
|
333
308
|
controller.post = null;
|
|
334
309
|
|
|
335
310
|
assert = () => {
|
|
@@ -350,8 +325,8 @@ describe('Controller Lifecycle', () => {
|
|
|
350
325
|
handler(req, res, next);
|
|
351
326
|
});
|
|
352
327
|
|
|
353
|
-
it('sets the step as complete if
|
|
354
|
-
controller.
|
|
328
|
+
it('sets the step as complete if there is no post handler', done => {
|
|
329
|
+
controller.post = null;
|
|
355
330
|
|
|
356
331
|
assert = () => {
|
|
357
332
|
controller.setStepComplete.should.have.been
|