@planning-inspectorate/dynamic-forms 1.13.0 → 1.14.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/.prettierignore +2 -1
- package/README.md +9 -1
- package/package.json +8 -3
- package/src/components/address/question.js +1 -1
- package/src/components/address/question.test.js +3 -3
- package/src/components/multi-field-input/index.njk +2 -2
- package/src/components/multi-field-input/question.js +5 -1
- package/src/components/radio/index.njk +2 -2
- package/src/components/select/index.njk +11 -1
- package/src/components/text-entry-redact/question.test.js +9 -9
- package/src/controller.test.js +2 -1
- package/src/index.js +8 -2
- package/src/lib/session-answer-store.test.js +1 -1
- package/src/questions/question-props.d.ts +5 -24
- package/src/questions/question.test.js +1 -1
- package/src/section.js +3 -0
- package/src/validator/validator.js +4 -1
- package/test/check-your-answers.test.js +69 -0
- package/test/journey.js +41 -0
- package/test/journey.test.js +21 -0
- package/test/questions.js +189 -0
- package/test/questions.test.js +246 -0
- package/test/snapshots/add-insurance.html +44 -0
- package/test/snapshots/check-your-answers-not-started.html +201 -0
- package/test/snapshots/check-your-answers-with-answers.html +201 -0
- package/test/snapshots/companions.html +51 -0
- package/test/snapshots/departure-date.html +57 -0
- package/test/snapshots/holiday-activities.html +49 -0
- package/test/snapshots/holiday-arrival.html +101 -0
- package/test/snapshots/holiday-description.html +33 -0
- package/test/snapshots/holiday-destination.html +56 -0
- package/test/snapshots/holiday-period.html +102 -0
- package/test/snapshots/holiday-snack.html +33 -0
- package/test/snapshots/hotel-address.html +65 -0
- package/test/snapshots/luggage-weight-units.html +75 -0
- package/test/snapshots/nights.html +28 -0
- package/test/snapshots/secret-wish.html +173 -0
- package/test/snapshots/travel-class.html +52 -0
- package/test/snapshots/travel-insurance-type.html +56 -0
- package/test/utils/app.js +41 -0
- package/test/utils/nunjucks.js +20 -0
- package/test/utils/test-server.js +167 -0
- package/test/utils/test-server.test.js +75 -0
- package/{src/lib/test-utils.js → test/utils/utils.js} +15 -16
- package/test/views/layout-check-your-answers.njk +1 -0
- /package/{src/lib/test-layout.njk → test/views/layout-question.njk} +0 -0
|
@@ -0,0 +1,167 @@
|
|
|
1
|
+
import http from 'http';
|
|
2
|
+
|
|
3
|
+
const DEFAULT_OPTIONS = {
|
|
4
|
+
credentials: 'include'
|
|
5
|
+
};
|
|
6
|
+
|
|
7
|
+
export class TestServer {
|
|
8
|
+
// Private cookie jar for storing cookies between requests
|
|
9
|
+
/** @type {Object<string, string>} */
|
|
10
|
+
#cookieJar = {};
|
|
11
|
+
|
|
12
|
+
/**
|
|
13
|
+
* @param {import('http').RequestListener} requestListener
|
|
14
|
+
* @param {object} [options]
|
|
15
|
+
* @param {boolean} [options.rememberCookies] - If true, cookies are stored and sent with each request
|
|
16
|
+
*/
|
|
17
|
+
constructor(requestListener, options = {}) {
|
|
18
|
+
this.requestListener = requestListener;
|
|
19
|
+
this.server = null;
|
|
20
|
+
this.port = null;
|
|
21
|
+
this.rememberCookies = Boolean(options.rememberCookies);
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
/**
|
|
25
|
+
* Starts the server on a random port.
|
|
26
|
+
* @returns {Promise<void>}
|
|
27
|
+
*/
|
|
28
|
+
async start() {
|
|
29
|
+
if (this.server) return;
|
|
30
|
+
this.server = http.createServer(this.requestListener);
|
|
31
|
+
await new Promise((resolve, reject) => {
|
|
32
|
+
this.server.listen(0, '127.0.0.1');
|
|
33
|
+
this.server.on('listening', () => {
|
|
34
|
+
this.port = this.server.address().port;
|
|
35
|
+
resolve();
|
|
36
|
+
});
|
|
37
|
+
this.server.on('error', reject);
|
|
38
|
+
});
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
/**
|
|
42
|
+
* Stops the server.
|
|
43
|
+
* @returns {Promise<void>}
|
|
44
|
+
*/
|
|
45
|
+
async stop() {
|
|
46
|
+
if (!this.server) return;
|
|
47
|
+
await new Promise((resolve, reject) => {
|
|
48
|
+
this.server.close((err) => {
|
|
49
|
+
if (err) reject(err);
|
|
50
|
+
else resolve();
|
|
51
|
+
});
|
|
52
|
+
});
|
|
53
|
+
this.server = null;
|
|
54
|
+
this.port = null;
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
/**
|
|
58
|
+
* Performs a GET request to the server.
|
|
59
|
+
* @param {string} path
|
|
60
|
+
* @param {RequestInit} [options]
|
|
61
|
+
* @returns {Promise<Response>}
|
|
62
|
+
*/
|
|
63
|
+
async get(path, options = {}) {
|
|
64
|
+
if (!this.server || !this.port) {
|
|
65
|
+
throw new Error('Server is not started');
|
|
66
|
+
}
|
|
67
|
+
const fetchOptions = { headers: {}, ...DEFAULT_OPTIONS, ...options, method: 'GET' };
|
|
68
|
+
this.#addCookies(fetchOptions.headers);
|
|
69
|
+
const response = await fetch(`http://localhost:${this.port}${path}`, fetchOptions);
|
|
70
|
+
if (this.rememberCookies) {
|
|
71
|
+
this.#updateCookies(response);
|
|
72
|
+
}
|
|
73
|
+
return response;
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
/**
|
|
77
|
+
* Performs a GET request to the server, following a maximum number of redirects.
|
|
78
|
+
* @param {string} path
|
|
79
|
+
* @param {number} maxRedirects
|
|
80
|
+
* @param {RequestInit} [options]
|
|
81
|
+
* @returns {Promise<Response>}
|
|
82
|
+
*/
|
|
83
|
+
async getWithRedirects(path, maxRedirects, options = {}) {
|
|
84
|
+
if (!this.server || !this.port) {
|
|
85
|
+
throw new Error('Server is not started');
|
|
86
|
+
}
|
|
87
|
+
if (maxRedirects === 0) {
|
|
88
|
+
// can just use get() directly otherwise
|
|
89
|
+
throw new Error('maxRedirects must be greater than 0');
|
|
90
|
+
}
|
|
91
|
+
let response;
|
|
92
|
+
let currentPath = path;
|
|
93
|
+
let numRedirects = 0;
|
|
94
|
+
while (true) {
|
|
95
|
+
response = await this.get(currentPath, { ...options, redirect: 'manual' });
|
|
96
|
+
if (
|
|
97
|
+
response.status >= 300 &&
|
|
98
|
+
response.status < 400 &&
|
|
99
|
+
response.headers.has('location') &&
|
|
100
|
+
numRedirects < maxRedirects
|
|
101
|
+
) {
|
|
102
|
+
currentPath = response.headers.get('location');
|
|
103
|
+
numRedirects++;
|
|
104
|
+
continue;
|
|
105
|
+
}
|
|
106
|
+
break;
|
|
107
|
+
}
|
|
108
|
+
if (this.rememberCookies) {
|
|
109
|
+
this.#updateCookies(response);
|
|
110
|
+
}
|
|
111
|
+
return response;
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
/**
|
|
115
|
+
* Performs a POST request to the server.
|
|
116
|
+
* @param {string} path
|
|
117
|
+
* @param {object} [body]
|
|
118
|
+
* @param {RequestInit} [options]
|
|
119
|
+
* @returns {Promise<Response>}
|
|
120
|
+
*/
|
|
121
|
+
async post(path, body = {}, options = {}) {
|
|
122
|
+
if (!this.server || !this.port) {
|
|
123
|
+
throw new Error('Server is not started');
|
|
124
|
+
}
|
|
125
|
+
const headers = { 'Content-Type': 'application/json', ...(options.headers || {}) };
|
|
126
|
+
const fetchOptions = {
|
|
127
|
+
...DEFAULT_OPTIONS,
|
|
128
|
+
...options,
|
|
129
|
+
method: 'POST',
|
|
130
|
+
headers,
|
|
131
|
+
body: JSON.stringify(body)
|
|
132
|
+
};
|
|
133
|
+
this.#addCookies(fetchOptions.headers);
|
|
134
|
+
const response = await fetch(`http://localhost:${this.port}${path}`, fetchOptions);
|
|
135
|
+
if (this.rememberCookies) {
|
|
136
|
+
this.#updateCookies(response);
|
|
137
|
+
}
|
|
138
|
+
return response;
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
#addCookies(headers) {
|
|
142
|
+
if (!this.rememberCookies) return;
|
|
143
|
+
if (this.#cookies) {
|
|
144
|
+
headers.Cookie = this.#cookies;
|
|
145
|
+
}
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
get #cookies() {
|
|
149
|
+
return Object.entries(this.#cookieJar)
|
|
150
|
+
.map(([k, v]) => `${k}=${v}`)
|
|
151
|
+
.join('; ');
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
/**
|
|
155
|
+
* Update the internal cookie jar from a fetch Response
|
|
156
|
+
* @param {Response} response
|
|
157
|
+
*/
|
|
158
|
+
#updateCookies(response) {
|
|
159
|
+
const cookies = response.headers.getSetCookie();
|
|
160
|
+
if (!cookies) return;
|
|
161
|
+
const cookieKv = cookies.map((c) => c.split(';')[0]);
|
|
162
|
+
for (const c of cookieKv) {
|
|
163
|
+
const [k, v] = c.split('=');
|
|
164
|
+
this.#cookieJar[k] = v;
|
|
165
|
+
}
|
|
166
|
+
}
|
|
167
|
+
}
|
|
@@ -0,0 +1,75 @@
|
|
|
1
|
+
import assert from 'node:assert/strict';
|
|
2
|
+
import { describe, it } from 'node:test';
|
|
3
|
+
import express from 'express';
|
|
4
|
+
import cookieParser from 'cookie-parser';
|
|
5
|
+
import { TestServer } from './test-server.js';
|
|
6
|
+
|
|
7
|
+
// Basic express app for testing
|
|
8
|
+
function createApp() {
|
|
9
|
+
const app = express();
|
|
10
|
+
app.use(express.json());
|
|
11
|
+
app.use(cookieParser());
|
|
12
|
+
|
|
13
|
+
app.get('/hello', (req, res) => {
|
|
14
|
+
res.send('world');
|
|
15
|
+
});
|
|
16
|
+
|
|
17
|
+
app.post('/echo', (req, res) => {
|
|
18
|
+
res.json(req.body);
|
|
19
|
+
});
|
|
20
|
+
|
|
21
|
+
app.get('/set-cookie', (req, res) => {
|
|
22
|
+
res.cookie('foo', 'bar');
|
|
23
|
+
res.send('cookie set');
|
|
24
|
+
});
|
|
25
|
+
|
|
26
|
+
app.get('/check-cookie', (req, res) => {
|
|
27
|
+
const foo = req.cookies ? req.cookies.foo : undefined;
|
|
28
|
+
res.json({ foo });
|
|
29
|
+
});
|
|
30
|
+
|
|
31
|
+
return app;
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
/**
|
|
35
|
+
* @param {import('node:test').TestContext} ctx
|
|
36
|
+
* @returns {Promise<TestServer>}
|
|
37
|
+
*/
|
|
38
|
+
async function newTestServer(ctx) {
|
|
39
|
+
const server = new TestServer(createApp());
|
|
40
|
+
await server.start();
|
|
41
|
+
ctx.after(async () => await server.stop());
|
|
42
|
+
return server;
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
describe('TestServer', () => {
|
|
46
|
+
it('should GET /hello', async (ctx) => {
|
|
47
|
+
const server = await newTestServer(ctx);
|
|
48
|
+
const res = await server.get('/hello');
|
|
49
|
+
assert.equal(res.status, 200);
|
|
50
|
+
const text = await res.text();
|
|
51
|
+
assert.equal(text, 'world');
|
|
52
|
+
});
|
|
53
|
+
|
|
54
|
+
it('should POST /echo', async (ctx) => {
|
|
55
|
+
const server = await newTestServer(ctx);
|
|
56
|
+
const payload = { foo: 'bar' };
|
|
57
|
+
const res = await server.post('/echo', payload);
|
|
58
|
+
assert.equal(res.status, 200);
|
|
59
|
+
const json = await res.json();
|
|
60
|
+
assert.deepEqual(json, payload);
|
|
61
|
+
});
|
|
62
|
+
|
|
63
|
+
it('should handle cookies', async (ctx) => {
|
|
64
|
+
const server = await newTestServer(ctx);
|
|
65
|
+
server.rememberCookies = true;
|
|
66
|
+
// Set cookie
|
|
67
|
+
let res = await server.get('/set-cookie');
|
|
68
|
+
assert.equal(res.status, 200);
|
|
69
|
+
// Now check cookie is sent back
|
|
70
|
+
res = await server.get('/check-cookie');
|
|
71
|
+
assert.equal(res.status, 200);
|
|
72
|
+
const json = await res.json();
|
|
73
|
+
assert.equal(json.foo, 'bar');
|
|
74
|
+
});
|
|
75
|
+
});
|
|
@@ -1,11 +1,13 @@
|
|
|
1
|
-
import { mock } from 'node:test';
|
|
2
|
-
import { createRequire } from 'node:module';
|
|
3
|
-
import nunjucks from 'nunjucks';
|
|
4
1
|
import path from 'path';
|
|
5
|
-
import {
|
|
2
|
+
import { mock } from 'node:test';
|
|
3
|
+
|
|
4
|
+
export function testDir() {
|
|
5
|
+
return path.join(path.dirname(new URL(import.meta.url).pathname), '..');
|
|
6
|
+
}
|
|
6
7
|
|
|
7
|
-
|
|
8
|
-
|
|
8
|
+
export function snapshotsDir() {
|
|
9
|
+
return path.join(testDir(), 'snapshots');
|
|
10
|
+
}
|
|
9
11
|
|
|
10
12
|
/**
|
|
11
13
|
* @returns {import('pino').BaseLogger}
|
|
@@ -28,7 +30,6 @@ export const mockReq = () => ({
|
|
|
28
30
|
params: {},
|
|
29
31
|
body: {}
|
|
30
32
|
});
|
|
31
|
-
|
|
32
33
|
export const mockRes = () => {
|
|
33
34
|
const res = {
|
|
34
35
|
locals: {},
|
|
@@ -43,13 +44,11 @@ export const mockRes = () => {
|
|
|
43
44
|
return res;
|
|
44
45
|
};
|
|
45
46
|
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
];
|
|
54
|
-
return nunjucks.configure(paths);
|
|
47
|
+
/**
|
|
48
|
+
* escapes all RegExp meta-characters in a string
|
|
49
|
+
* @param {string} s
|
|
50
|
+
* @returns {string}
|
|
51
|
+
*/
|
|
52
|
+
export function escapeForRegExp(s) {
|
|
53
|
+
return s.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
|
|
55
54
|
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{% extends "components/task-list/index.njk" %}
|
|
File without changes
|