@planning-inspectorate/dynamic-forms 1.14.0 → 1.15.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.
Files changed (46) hide show
  1. package/.prettierignore +2 -1
  2. package/README.md +9 -1
  3. package/package.json +7 -2
  4. package/src/components/address/question.js +1 -1
  5. package/src/components/address/question.test.js +3 -3
  6. package/src/components/multi-field-input/index.njk +2 -2
  7. package/src/components/multi-field-input/question.js +5 -1
  8. package/src/components/radio/index.njk +2 -2
  9. package/src/components/select/index.njk +2 -2
  10. package/src/components/text-entry-redact/question.test.js +9 -9
  11. package/src/controller.test.js +2 -1
  12. package/src/index.js +8 -2
  13. package/src/lib/session-answer-store.test.js +1 -1
  14. package/src/questions/question-props.d.ts +5 -24
  15. package/src/questions/question.test.js +1 -1
  16. package/src/section.js +3 -0
  17. package/src/validator/validator.js +4 -1
  18. package/test/check-your-answers.test.js +69 -0
  19. package/test/journey.js +41 -0
  20. package/test/journey.test.js +21 -0
  21. package/test/questions.js +189 -0
  22. package/test/questions.test.js +246 -0
  23. package/test/snapshots/add-insurance.html +44 -0
  24. package/test/snapshots/check-your-answers-not-started.html +201 -0
  25. package/test/snapshots/check-your-answers-with-answers.html +201 -0
  26. package/test/snapshots/companions.html +51 -0
  27. package/test/snapshots/departure-date.html +57 -0
  28. package/test/snapshots/holiday-activities.html +49 -0
  29. package/test/snapshots/holiday-arrival.html +101 -0
  30. package/test/snapshots/holiday-description.html +33 -0
  31. package/test/snapshots/holiday-destination.html +56 -0
  32. package/test/snapshots/holiday-period.html +102 -0
  33. package/test/snapshots/holiday-snack.html +33 -0
  34. package/test/snapshots/hotel-address.html +65 -0
  35. package/test/snapshots/luggage-weight-units.html +75 -0
  36. package/test/snapshots/nights.html +28 -0
  37. package/test/snapshots/secret-wish.html +173 -0
  38. package/test/snapshots/travel-class.html +52 -0
  39. package/test/snapshots/travel-insurance-type.html +56 -0
  40. package/test/utils/app.js +41 -0
  41. package/test/utils/nunjucks.js +20 -0
  42. package/test/utils/test-server.js +167 -0
  43. package/test/utils/test-server.test.js +75 -0
  44. package/{src/lib/test-utils.js → test/utils/utils.js} +15 -16
  45. package/test/views/layout-check-your-answers.njk +1 -0
  46. /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 { fileURLToPath } from 'url';
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
- const __filename = fileURLToPath(import.meta.url);
8
- const __dirname = path.dirname(__filename);
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
- export function configureNunjucks() {
47
- const require = createRequire(import.meta.url);
48
- const paths = [
49
- // get the path to the govuk-frontend folder, in node_modules, using the node require resolution
50
- path.resolve(require.resolve('govuk-frontend'), '../..'),
51
- // path to src folder
52
- path.join(__dirname, '..')
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" %}