diff --git a/package-lock.json b/package-lock.json index 1c751e2c..4634195c 100644 --- a/package-lock.json +++ b/package-lock.json @@ -53,7 +53,7 @@ "@typescript-eslint/eslint-plugin": "^7.17.0", "@typescript-eslint/parser": "^7.17.0", "@wdio/browserstack-service": "9.2.14", - "@wdio/cli": "8.35.1", + "@wdio/cli": "8.41.0", "@wdio/globals": "8.35.1", "@wdio/local-runner": "8.35.1", "@wdio/mocha-framework": "8.35.0", @@ -5107,9 +5107,9 @@ "license": "ISC" }, "node_modules/@vitest/pretty-format": { - "version": "2.1.5", - "resolved": "https://registry.npmjs.org/@vitest/pretty-format/-/pretty-format-2.1.5.tgz", - "integrity": "sha512-4ZOwtk2bqG5Y6xRGHcveZVr+6txkH7M2e+nPFd6guSoN638v/1XQ0K06eOpi0ptVU/2tW/pIU4IoPotY/GZ9fw==", + "version": "2.1.8", + "resolved": "https://registry.npmjs.org/@vitest/pretty-format/-/pretty-format-2.1.8.tgz", + "integrity": "sha512-9HiSZ9zpqNLKlbIDRWOnAWqgcA7xu+8YxXSekhr0Ykab7PAYFkhkwoqVArPOtJhPmYeE2YHgKZlj3CP36z2AJQ==", "dev": true, "dependencies": { "tinyrainbow": "^1.2.0" @@ -5119,14 +5119,14 @@ } }, "node_modules/@vitest/snapshot": { - "version": "1.6.0", - "resolved": "https://registry.npmjs.org/@vitest/snapshot/-/snapshot-1.6.0.tgz", - "integrity": "sha512-+Hx43f8Chus+DCmygqqfetcAZrDJwvTj0ymqjQq4CvmpKFSTVteEOBzCusu1x2tt4OJcvBflyHUE0DZSLgEMtQ==", + "version": "2.1.8", + "resolved": "https://registry.npmjs.org/@vitest/snapshot/-/snapshot-2.1.8.tgz", + "integrity": "sha512-20T7xRFbmnkfcmgVEz+z3AU/3b0cEzZOt/zmnvZEctg64/QZbSDJEVm9fLnnlSi74KibmRsO9/Qabi+t0vCRPg==", "dev": true, "dependencies": { - "magic-string": "^0.30.5", - "pathe": "^1.1.1", - "pretty-format": "^29.7.0" + "@vitest/pretty-format": "2.1.8", + "magic-string": "^0.30.12", + "pathe": "^1.1.2" }, "funding": { "url": "https://opencollective.com/vitest" @@ -5585,22 +5585,22 @@ } }, "node_modules/@wdio/cli": { - "version": "8.35.1", - "resolved": "https://registry.npmjs.org/@wdio/cli/-/cli-8.35.1.tgz", - "integrity": "sha512-cdFmd6P/eQJdP2lChQ+Fa9b1c2p0bDIPmetVHGCuHiW8ZPkanrvBFtHMUhMu44a1koni9LvN/hu7vIJ/aAC+Rg==", - "dev": true, - "dependencies": { - "@types/node": "^20.1.1", - "@vitest/snapshot": "^1.2.1", - "@wdio/config": "8.35.0", - "@wdio/globals": "8.35.1", - "@wdio/logger": "8.28.0", - "@wdio/protocols": "8.32.0", - "@wdio/types": "8.32.4", - "@wdio/utils": "8.35.0", + "version": "8.41.0", + "resolved": "https://registry.npmjs.org/@wdio/cli/-/cli-8.41.0.tgz", + "integrity": "sha512-+f4McBz6M8/oEJLeoYxHNyfvQ4NTGRACKjtw/FdTJ+GYRu8DkU9sgXOo70NIPCMUajVIOtbDVUbLjJKuxWTEFQ==", + "dev": true, + "dependencies": { + "@types/node": "^22.2.0", + "@vitest/snapshot": "^2.0.4", + "@wdio/config": "8.41.0", + "@wdio/globals": "8.41.0", + "@wdio/logger": "8.38.0", + "@wdio/protocols": "8.40.3", + "@wdio/types": "8.41.0", + "@wdio/utils": "8.41.0", "async-exit-hook": "^2.0.1", "chalk": "^5.2.0", - "chokidar": "^3.5.3", + "chokidar": "^4.0.0", "cli-spinners": "^2.9.0", "dotenv": "^16.3.1", "ejs": "^3.1.9", @@ -5612,7 +5612,7 @@ "lodash.union": "^4.6.0", "read-pkg-up": "10.0.0", "recursive-readdir": "^2.2.3", - "webdriverio": "8.35.1", + "webdriverio": "8.41.0", "yargs": "^17.7.2" }, "bin": { @@ -5622,6 +5622,127 @@ "node": "^16.13 || >=18" } }, + "node_modules/@wdio/cli/node_modules/@types/node": { + "version": "22.10.5", + "resolved": "https://registry.npmjs.org/@types/node/-/node-22.10.5.tgz", + "integrity": "sha512-F8Q+SeGimwOo86fiovQh8qiXfFEh2/ocYv7tU5pJ3EXMSSxk1Joj5wefpFK2fHTf/N6HKGSxIDBT9f3gCxXPkQ==", + "dev": true, + "dependencies": { + "undici-types": "~6.20.0" + } + }, + "node_modules/@wdio/cli/node_modules/@wdio/config": { + "version": "8.41.0", + "resolved": "https://registry.npmjs.org/@wdio/config/-/config-8.41.0.tgz", + "integrity": "sha512-/6Z3sfSyhX5oVde0l01fyHimbqRYIVUDBnhDG2EMSCoC2lsaJX3Bm3IYpYHYHHFsgoDCi3B3Gv++t9dn2eSZZw==", + "dev": true, + "dependencies": { + "@wdio/logger": "8.38.0", + "@wdio/types": "8.41.0", + "@wdio/utils": "8.41.0", + "decamelize": "^6.0.0", + "deepmerge-ts": "^5.0.0", + "glob": "^10.2.2", + "import-meta-resolve": "^4.0.0" + }, + "engines": { + "node": "^16.13 || >=18" + } + }, + "node_modules/@wdio/cli/node_modules/@wdio/globals": { + "version": "8.41.0", + "resolved": "https://registry.npmjs.org/@wdio/globals/-/globals-8.41.0.tgz", + "integrity": "sha512-xfUpEppdKzMHy4qoSoQN1cXoBPPh7oMeX+U/jtdvOtla+dd/YZ8pu47zLhQ/GM3gDVrBGnO4w3u4L6Zf/P3KEw==", + "dev": true, + "engines": { + "node": "^16.13 || >=18" + }, + "optionalDependencies": { + "expect-webdriverio": "^4.11.2", + "webdriverio": "8.41.0" + } + }, + "node_modules/@wdio/cli/node_modules/@wdio/logger": { + "version": "8.38.0", + "resolved": "https://registry.npmjs.org/@wdio/logger/-/logger-8.38.0.tgz", + "integrity": "sha512-kcHL86RmNbcQP+Gq/vQUGlArfU6IIcbbnNp32rRIraitomZow+iEoc519rdQmSVusDozMS5DZthkgDdxK+vz6Q==", + "dev": true, + "dependencies": { + "chalk": "^5.1.2", + "loglevel": "^1.6.0", + "loglevel-plugin-prefix": "^0.8.4", + "strip-ansi": "^7.1.0" + }, + "engines": { + "node": "^16.13 || >=18" + } + }, + "node_modules/@wdio/cli/node_modules/@wdio/protocols": { + "version": "8.40.3", + "resolved": "https://registry.npmjs.org/@wdio/protocols/-/protocols-8.40.3.tgz", + "integrity": "sha512-wK7+eyrB3TAei8RwbdkcyoNk2dPu+mduMBOdPJjp8jf/mavd15nIUXLID1zA+w5m1Qt1DsT1NbvaeO9+aJQ33A==", + "dev": true + }, + "node_modules/@wdio/cli/node_modules/@wdio/repl": { + "version": "8.40.3", + "resolved": "https://registry.npmjs.org/@wdio/repl/-/repl-8.40.3.tgz", + "integrity": "sha512-mWEiBbaC7CgxvSd2/ozpbZWebnRIc8KRu/J81Hlw/txUWio27S7IpXBlZGVvhEsNzq0+cuxB/8gDkkXvMPbesw==", + "dev": true, + "dependencies": { + "@types/node": "^22.2.0" + }, + "engines": { + "node": "^16.13 || >=18" + } + }, + "node_modules/@wdio/cli/node_modules/@wdio/types": { + "version": "8.41.0", + "resolved": "https://registry.npmjs.org/@wdio/types/-/types-8.41.0.tgz", + "integrity": "sha512-t4NaNTvJZci3Xv/yUZPH4eTL0hxrVTf5wdwNnYIBrzMnlRDbNefjQ0P7FM7ZjQCLaH92AEH6t/XanUId7Webug==", + "dev": true, + "dependencies": { + "@types/node": "^22.2.0" + }, + "engines": { + "node": "^16.13 || >=18" + } + }, + "node_modules/@wdio/cli/node_modules/@wdio/utils": { + "version": "8.41.0", + "resolved": "https://registry.npmjs.org/@wdio/utils/-/utils-8.41.0.tgz", + "integrity": "sha512-0TcTjBiax1VxtJQ/iQA0ZyYOSHjjX2ARVmEI0AMo9+AuIq+xBfnY561+v8k9GqOMPKsiH/HrK3xwjx8xCVS03g==", + "dev": true, + "dependencies": { + "@puppeteer/browsers": "^1.6.0", + "@wdio/logger": "8.38.0", + "@wdio/types": "8.41.0", + "decamelize": "^6.0.0", + "deepmerge-ts": "^5.1.0", + "edgedriver": "^5.5.0", + "geckodriver": "~4.2.0", + "get-port": "^7.0.0", + "import-meta-resolve": "^4.0.0", + "locate-app": "^2.1.0", + "safaridriver": "^0.1.0", + "split2": "^4.2.0", + "wait-port": "^1.0.4" + }, + "engines": { + "node": "^16.13 || >=18" + } + }, + "node_modules/@wdio/cli/node_modules/ansi-regex": { + "version": "6.1.0", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-6.1.0.tgz", + "integrity": "sha512-7HSX4QQb4CspciLpVFwyRe79O3xsIZDDLER21kERQ71oaPodF8jL725AgJMFAYbooIqolJoRLuM81SpeUkpkvA==", + "dev": true, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/chalk/ansi-regex?sponsor=1" + } + }, "node_modules/@wdio/cli/node_modules/chalk": { "version": "5.3.0", "resolved": "https://registry.npmjs.org/chalk/-/chalk-5.3.0.tgz", @@ -5634,6 +5755,57 @@ "url": "https://github.com/chalk/chalk?sponsor=1" } }, + "node_modules/@wdio/cli/node_modules/chokidar": { + "version": "4.0.3", + "resolved": "https://registry.npmjs.org/chokidar/-/chokidar-4.0.3.tgz", + "integrity": "sha512-Qgzu8kfBvo+cA4962jnP1KkS6Dop5NS6g7R5LFYJr4b8Ub94PPQXUksCw9PvXoeXPRRddRNC5C1JQUR2SMGtnA==", + "dev": true, + "dependencies": { + "readdirp": "^4.0.1" + }, + "engines": { + "node": ">= 14.16.0" + }, + "funding": { + "url": "https://paulmillr.com/funding/" + } + }, + "node_modules/@wdio/cli/node_modules/chromium-bidi": { + "version": "0.5.8", + "resolved": "https://registry.npmjs.org/chromium-bidi/-/chromium-bidi-0.5.8.tgz", + "integrity": "sha512-blqh+1cEQbHBKmok3rVJkBlBxt9beKBgOsxbFgs7UJcoVbbeZ+K7+6liAsjgpc8l1Xd55cQUy14fXZdGSb4zIw==", + "dev": true, + "dependencies": { + "mitt": "3.0.1", + "urlpattern-polyfill": "10.0.0" + }, + "peerDependencies": { + "devtools-protocol": "*" + } + }, + "node_modules/@wdio/cli/node_modules/debug": { + "version": "4.3.4", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.4.tgz", + "integrity": "sha512-PRWFHuSU3eDtQJPvnNY7Jcket1j0t5OuOsFzPPzsekD52Zl8qUfFIPEiswXqIvHWGVHOgX+7G/vCNNhehwxfkQ==", + "dev": true, + "dependencies": { + "ms": "2.1.2" + }, + "engines": { + "node": ">=6.0" + }, + "peerDependenciesMeta": { + "supports-color": { + "optional": true + } + } + }, + "node_modules/@wdio/cli/node_modules/devtools-protocol": { + "version": "0.0.1359167", + "resolved": "https://registry.npmjs.org/devtools-protocol/-/devtools-protocol-0.0.1359167.tgz", + "integrity": "sha512-f/9PeTaSH3weS/WAwrQb5/s9R3KMOeTGe+Jkhg5952yInub7iDPjdlzRdrDgpLZfxHbTrBuG9aUkAMM+ocVkXQ==", + "dev": true + }, "node_modules/@wdio/cli/node_modules/execa": { "version": "8.0.1", "resolved": "https://registry.npmjs.org/execa/-/execa-8.0.1.tgz", @@ -5657,6 +5829,29 @@ "url": "https://github.com/sindresorhus/execa?sponsor=1" } }, + "node_modules/@wdio/cli/node_modules/geckodriver": { + "version": "4.2.1", + "resolved": "https://registry.npmjs.org/geckodriver/-/geckodriver-4.2.1.tgz", + "integrity": "sha512-4m/CRk0OI8MaANRuFIahvOxYTSjlNAO2p9JmE14zxueknq6cdtB5M9UGRQ8R9aMV0bLGNVHHDnDXmoXdOwJfWg==", + "dev": true, + "hasInstallScript": true, + "dependencies": { + "@wdio/logger": "^8.11.0", + "decamelize": "^6.0.0", + "http-proxy-agent": "^7.0.0", + "https-proxy-agent": "^7.0.1", + "node-fetch": "^3.3.1", + "tar-fs": "^3.0.4", + "unzipper": "^0.10.14", + "which": "^4.0.0" + }, + "bin": { + "geckodriver": "bin/geckodriver.js" + }, + "engines": { + "node": "^16.13 || >=18 || >=20" + } + }, "node_modules/@wdio/cli/node_modules/get-stream": { "version": "8.0.1", "resolved": "https://registry.npmjs.org/get-stream/-/get-stream-8.0.1.tgz", @@ -5669,6 +5864,26 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/@wdio/cli/node_modules/glob": { + "version": "10.4.5", + "resolved": "https://registry.npmjs.org/glob/-/glob-10.4.5.tgz", + "integrity": "sha512-7Bv8RF0k6xjo7d4A/PxYLbUCfb6c+Vpd2/mB2yRDlew7Jb5hEXiCD9ibfO7wpk8i4sevK6DFny9h7EYbM3/sHg==", + "dev": true, + "dependencies": { + "foreground-child": "^3.1.0", + "jackspeak": "^3.1.2", + "minimatch": "^9.0.4", + "minipass": "^7.1.2", + "package-json-from-dist": "^1.0.0", + "path-scurry": "^1.11.1" + }, + "bin": { + "glob": "dist/esm/bin.mjs" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, "node_modules/@wdio/cli/node_modules/human-signals": { "version": "5.0.0", "resolved": "https://registry.npmjs.org/human-signals/-/human-signals-5.0.0.tgz", @@ -5690,6 +5905,15 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/@wdio/cli/node_modules/isexe": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/isexe/-/isexe-3.1.1.tgz", + "integrity": "sha512-LpB/54B+/2J5hqQ7imZHfdU31OlgQqx7ZicVlkm9kzg9/w8GKLEcFfJl/t7DCEDueOyBAD6zCCwTO6Fzs0NoEQ==", + "dev": true, + "engines": { + "node": ">=16" + } + }, "node_modules/@wdio/cli/node_modules/mimic-fn": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/mimic-fn/-/mimic-fn-4.0.0.tgz", @@ -5702,6 +5926,18 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/@wdio/cli/node_modules/mitt": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/mitt/-/mitt-3.0.1.tgz", + "integrity": "sha512-vKivATfr97l2/QBCYAkXYDbrIWPM2IIKEl7YPhjCvKlG3kE2gm+uBo6nEXK3M5/Ffh/FLpKExzOQ3JJoJGFKBw==", + "dev": true + }, + "node_modules/@wdio/cli/node_modules/ms": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz", + "integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==", + "dev": true + }, "node_modules/@wdio/cli/node_modules/npm-run-path": { "version": "5.3.0", "resolved": "https://registry.npmjs.org/npm-run-path/-/npm-run-path-5.3.0.tgz", @@ -5744,6 +5980,42 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/@wdio/cli/node_modules/puppeteer-core": { + "version": "21.11.0", + "resolved": "https://registry.npmjs.org/puppeteer-core/-/puppeteer-core-21.11.0.tgz", + "integrity": "sha512-ArbnyA3U5SGHokEvkfWjW+O8hOxV1RSJxOgriX/3A4xZRqixt9ZFHD0yPgZQF05Qj0oAqi8H/7stDorjoHY90Q==", + "dev": true, + "dependencies": { + "@puppeteer/browsers": "1.9.1", + "chromium-bidi": "0.5.8", + "cross-fetch": "4.0.0", + "debug": "4.3.4", + "devtools-protocol": "0.0.1232444", + "ws": "8.16.0" + }, + "engines": { + "node": ">=16.13.2" + } + }, + "node_modules/@wdio/cli/node_modules/puppeteer-core/node_modules/devtools-protocol": { + "version": "0.0.1232444", + "resolved": "https://registry.npmjs.org/devtools-protocol/-/devtools-protocol-0.0.1232444.tgz", + "integrity": "sha512-pM27vqEfxSxRkTMnF+XCmxSEb6duO5R+t8A9DEEJgy4Wz2RVanje2mmj99B6A3zv2r/qGfYlOvYznUhuokizmg==", + "dev": true + }, + "node_modules/@wdio/cli/node_modules/readdirp": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/readdirp/-/readdirp-4.0.2.tgz", + "integrity": "sha512-yDMz9g+VaZkqBYS/ozoBJwaBhTbZo3UNYQHNRw1D3UFQB8oHB4uS/tAODO+ZLjGWmUbKnIlOWO+aaIiAxrUWHA==", + "dev": true, + "engines": { + "node": ">= 14.16.0" + }, + "funding": { + "type": "individual", + "url": "https://paulmillr.com/funding/" + } + }, "node_modules/@wdio/cli/node_modules/signal-exit": { "version": "4.1.0", "resolved": "https://registry.npmjs.org/signal-exit/-/signal-exit-4.1.0.tgz", @@ -5756,6 +6028,21 @@ "url": "https://github.com/sponsors/isaacs" } }, + "node_modules/@wdio/cli/node_modules/strip-ansi": { + "version": "7.1.0", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-7.1.0.tgz", + "integrity": "sha512-iq6eVVI64nQQTRYq2KtEg2d2uU7LElhTJwsH4YzIHZshxlgZms/wIc4VoDQTlG/IvVIrBKG06CrZnp0qv7hkcQ==", + "dev": true, + "dependencies": { + "ansi-regex": "^6.0.1" + }, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/chalk/strip-ansi?sponsor=1" + } + }, "node_modules/@wdio/cli/node_modules/strip-final-newline": { "version": "3.0.0", "resolved": "https://registry.npmjs.org/strip-final-newline/-/strip-final-newline-3.0.0.tgz", @@ -5768,6 +6055,114 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/@wdio/cli/node_modules/undici-types": { + "version": "6.20.0", + "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-6.20.0.tgz", + "integrity": "sha512-Ny6QZ2Nju20vw1SRHe3d9jVu6gJ+4e3+MMpqu7pqE5HT6WsTSlce++GQmK5UXS8mzV8DSYHrQH+Xrf2jVcuKNg==", + "dev": true + }, + "node_modules/@wdio/cli/node_modules/webdriver": { + "version": "8.41.0", + "resolved": "https://registry.npmjs.org/webdriver/-/webdriver-8.41.0.tgz", + "integrity": "sha512-n8OrFnVT4hAaGa0Advr3T8ObJdeKNTRklHIEzM2CYVx/5DZt+2KwaKSxWsURNd4zU7FbsfaJUU4rQWCmvozQLg==", + "dev": true, + "dependencies": { + "@types/node": "^22.2.0", + "@types/ws": "^8.5.3", + "@wdio/config": "8.41.0", + "@wdio/logger": "8.38.0", + "@wdio/protocols": "8.40.3", + "@wdio/types": "8.41.0", + "@wdio/utils": "8.41.0", + "deepmerge-ts": "^5.1.0", + "got": "^12.6.1", + "ky": "^0.33.0", + "ws": "^8.8.0" + }, + "engines": { + "node": "^16.13 || >=18" + } + }, + "node_modules/@wdio/cli/node_modules/webdriverio": { + "version": "8.41.0", + "resolved": "https://registry.npmjs.org/webdriverio/-/webdriverio-8.41.0.tgz", + "integrity": "sha512-WlQfw0mUEhTS8DPr+TBSYMhEnqXkFr2dcUwPb5XkffTB+i0wftf+BLXJPSVD9M1PTLyYcFdCIu68pqR54dq5BA==", + "dev": true, + "dependencies": { + "@types/node": "^22.2.0", + "@wdio/config": "8.41.0", + "@wdio/logger": "8.38.0", + "@wdio/protocols": "8.40.3", + "@wdio/repl": "8.40.3", + "@wdio/types": "8.41.0", + "@wdio/utils": "8.41.0", + "archiver": "^7.0.0", + "aria-query": "^5.0.0", + "css-shorthand-properties": "^1.1.1", + "css-value": "^0.0.1", + "devtools-protocol": "^0.0.1359167", + "grapheme-splitter": "^1.0.2", + "import-meta-resolve": "^4.0.0", + "is-plain-obj": "^4.1.0", + "jszip": "^3.10.1", + "lodash.clonedeep": "^4.5.0", + "lodash.zip": "^4.2.0", + "minimatch": "^9.0.0", + "puppeteer-core": "^21.11.0", + "query-selector-shadow-dom": "^1.0.0", + "resq": "^1.9.1", + "rgb2hex": "0.2.5", + "serialize-error": "^11.0.1", + "webdriver": "8.41.0" + }, + "engines": { + "node": "^16.13 || >=18" + }, + "peerDependencies": { + "devtools": "^8.14.0" + }, + "peerDependenciesMeta": { + "devtools": { + "optional": true + } + } + }, + "node_modules/@wdio/cli/node_modules/which": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/which/-/which-4.0.0.tgz", + "integrity": "sha512-GlaYyEb07DPxYCKhKzplCWBJtvxZcZMrL+4UkrTSJHHPyZU4mYYTv3qaOe77H7EODLSSopAUFAc6W8U4yqvscg==", + "dev": true, + "dependencies": { + "isexe": "^3.1.1" + }, + "bin": { + "node-which": "bin/which.js" + }, + "engines": { + "node": "^16.13.0 || >=18.0.0" + } + }, + "node_modules/@wdio/cli/node_modules/ws": { + "version": "8.16.0", + "resolved": "https://registry.npmjs.org/ws/-/ws-8.16.0.tgz", + "integrity": "sha512-HS0c//TP7Ina87TfiPUz1rQzMhHrl/SG2guqRcTOIUYD2q8uhUdNHZYJUaQ8aTGPzCh+c6oawMKW35nFl1dxyQ==", + "dev": true, + "engines": { + "node": ">=10.0.0" + }, + "peerDependencies": { + "bufferutil": "^4.0.1", + "utf-8-validate": ">=5.0.2" + }, + "peerDependenciesMeta": { + "bufferutil": { + "optional": true + }, + "utf-8-validate": { + "optional": true + } + } + }, "node_modules/@wdio/config": { "version": "8.35.0", "resolved": "https://registry.npmjs.org/@wdio/config/-/config-8.35.0.tgz", @@ -7254,6 +7649,28 @@ "node": ">=10.0.0" } }, + "node_modules/big-integer": { + "version": "1.6.52", + "resolved": "https://registry.npmjs.org/big-integer/-/big-integer-1.6.52.tgz", + "integrity": "sha512-QxD8cf2eVqJOOz63z6JIN9BzvVs/dlySa5HGSBH5xtR8dPteIRQnBxxKqkNTiT6jbDTF6jAfrd4oMcND9RGbQg==", + "dev": true, + "engines": { + "node": ">=0.6" + } + }, + "node_modules/binary": { + "version": "0.3.0", + "resolved": "https://registry.npmjs.org/binary/-/binary-0.3.0.tgz", + "integrity": "sha512-D4H1y5KYwpJgK8wk1Cue5LLPgmwHKYSChkbspQg5JtVuR5ulGckxfR62H3AE9UDkdMC8yyXlqYihuz3Aqg2XZg==", + "dev": true, + "dependencies": { + "buffers": "~0.1.1", + "chainsaw": "~0.1.0" + }, + "engines": { + "node": "*" + } + }, "node_modules/binary-extensions": { "version": "2.3.0", "resolved": "https://registry.npmjs.org/binary-extensions/-/binary-extensions-2.3.0.tgz", @@ -7316,6 +7733,12 @@ "node": ">= 6" } }, + "node_modules/bluebird": { + "version": "3.4.7", + "resolved": "https://registry.npmjs.org/bluebird/-/bluebird-3.4.7.tgz", + "integrity": "sha512-iD3898SR7sWVRHbiQv+sHUtHnMvC1o3nW5rAcqnq3uOn07DSAppZYUkIGslDz6gXC7HfunPe7YVBgoEJASPcHA==", + "dev": true + }, "node_modules/boolbase": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/boolbase/-/boolbase-1.0.0.tgz", @@ -7481,6 +7904,24 @@ "dev": true, "license": "MIT" }, + "node_modules/buffer-indexof-polyfill": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/buffer-indexof-polyfill/-/buffer-indexof-polyfill-1.0.2.tgz", + "integrity": "sha512-I7wzHwA3t1/lwXQh+A5PbNvJxgfo5r3xulgpYDB5zckTu/Z9oUK9biouBKQUjEqzaz3HnAT6TYoovmE+GqSf7A==", + "dev": true, + "engines": { + "node": ">=0.10" + } + }, + "node_modules/buffers": { + "version": "0.1.1", + "resolved": "https://registry.npmjs.org/buffers/-/buffers-0.1.1.tgz", + "integrity": "sha512-9q/rDEGSb/Qsvv2qvzIzdluL5k7AaJOTrw23z9reQthrbF7is4CtlT0DXyO1oei2DCp4uojjzQ7igaSHp1kAEQ==", + "dev": true, + "engines": { + "node": ">=0.2.0" + } + }, "node_modules/builtin-modules": { "version": "3.3.0", "resolved": "https://registry.npmjs.org/builtin-modules/-/builtin-modules-3.3.0.tgz", @@ -7616,6 +8057,18 @@ ], "license": "CC-BY-4.0" }, + "node_modules/chainsaw": { + "version": "0.1.0", + "resolved": "https://registry.npmjs.org/chainsaw/-/chainsaw-0.1.0.tgz", + "integrity": "sha512-75kWfWt6MEKNC8xYXIdRpDehRYY/tNSgwKaJq+dbbDcxORuVrrQ+SEHoWsniVn9XPYfP4gmdWIeDk/4YNp1rNQ==", + "dev": true, + "dependencies": { + "traverse": ">=0.3.0 <0.4" + }, + "engines": { + "node": "*" + } + }, "node_modules/chalk": { "version": "2.4.2", "resolved": "https://registry.npmjs.org/chalk/-/chalk-2.4.2.tgz", @@ -9069,6 +9522,51 @@ "dev": true, "license": "MIT" }, + "node_modules/duplexer2": { + "version": "0.1.4", + "resolved": "https://registry.npmjs.org/duplexer2/-/duplexer2-0.1.4.tgz", + "integrity": "sha512-asLFVfWWtJ90ZyOUHMqk7/S2w2guQKxUI2itj3d92ADHhxUSbCMGi1f1cBcJ7xM1To+pE/Khbwo1yuNbMEPKeA==", + "dev": true, + "dependencies": { + "readable-stream": "^2.0.2" + } + }, + "node_modules/duplexer2/node_modules/isarray": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/isarray/-/isarray-1.0.0.tgz", + "integrity": "sha512-VLghIWNM6ELQzo7zwmcg0NmTVyWKYjvIeM83yjp0wRDTmUnrM678fQbcKBo6n2CJEF0szoG//ytg+TKla89ALQ==", + "dev": true + }, + "node_modules/duplexer2/node_modules/readable-stream": { + "version": "2.3.8", + "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-2.3.8.tgz", + "integrity": "sha512-8p0AUk4XODgIewSi0l8Epjs+EVnWiK7NoDIEGU0HhE7+ZyY8D1IMY7odu5lRrFXGg71L15KG8QrPmum45RTtdA==", + "dev": true, + "dependencies": { + "core-util-is": "~1.0.0", + "inherits": "~2.0.3", + "isarray": "~1.0.0", + "process-nextick-args": "~2.0.0", + "safe-buffer": "~5.1.1", + "string_decoder": "~1.1.1", + "util-deprecate": "~1.0.1" + } + }, + "node_modules/duplexer2/node_modules/safe-buffer": { + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.1.2.tgz", + "integrity": "sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g==", + "dev": true + }, + "node_modules/duplexer2/node_modules/string_decoder": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.1.1.tgz", + "integrity": "sha512-n/ShnvDi6FHbbVfviro+WojiFzv+s8MPMHBczVePfUpDJLwoLT0ht1l4YwBCbi8pJAveEEdnkHyPyTP/mzRfwg==", + "dev": true, + "dependencies": { + "safe-buffer": "~5.1.0" + } + }, "node_modules/eastasianwidth": { "version": "0.2.0", "resolved": "https://registry.npmjs.org/eastasianwidth/-/eastasianwidth-0.2.0.tgz", @@ -10576,20 +11074,6 @@ "webdriverio": "^8.29.3" } }, - "node_modules/expect-webdriverio/node_modules/@vitest/snapshot": { - "version": "2.1.5", - "resolved": "https://registry.npmjs.org/@vitest/snapshot/-/snapshot-2.1.5.tgz", - "integrity": "sha512-zmYw47mhfdfnYbuhkQvkkzYroXUumrwWDGlMjpdUr4jBd3HZiV2w7CQHj+z7AAS4VOtWxI4Zt4bWt4/sKcoIjg==", - "dev": true, - "dependencies": { - "@vitest/pretty-format": "2.1.5", - "magic-string": "^0.30.12", - "pathe": "^1.1.2" - }, - "funding": { - "url": "https://opencollective.com/vitest" - } - }, "node_modules/external-editor": { "version": "3.1.0", "resolved": "https://registry.npmjs.org/external-editor/-/external-editor-3.1.0.tgz", @@ -11178,6 +11662,22 @@ "node": "^8.16.0 || ^10.6.0 || >=11.0.0" } }, + "node_modules/fstream": { + "version": "1.0.12", + "resolved": "https://registry.npmjs.org/fstream/-/fstream-1.0.12.tgz", + "integrity": "sha512-WvJ193OHa0GHPEL+AycEJgxvBEwyfRkN1vhjca23OaPVMCaLCXTd5qAu82AjTcgP1UJmytkOKb63Ypde7raDIg==", + "deprecated": "This package is no longer supported.", + "dev": true, + "dependencies": { + "graceful-fs": "^4.1.2", + "inherits": "~2.0.0", + "mkdirp": ">=0.5 0", + "rimraf": "2" + }, + "engines": { + "node": ">=0.6" + } + }, "node_modules/function-bind": { "version": "1.1.2", "resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.2.tgz", @@ -15593,6 +16093,12 @@ "dev": true, "license": "MIT" }, + "node_modules/listenercount": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/listenercount/-/listenercount-1.0.1.tgz", + "integrity": "sha512-3mk/Zag0+IJxeDrxSgaDPy4zZ3w05PRZeJNnlWhzFz5OkX49J4krc+A8X2d2M69vGMBEX0uyl8M+W+8gH+kBqQ==", + "dev": true + }, "node_modules/load-json-file": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/load-json-file/-/load-json-file-4.0.0.tgz", @@ -16180,6 +16686,18 @@ "integrity": "sha512-7dX2/10ITVyqh4aOSVI9gdape+t9l2/8QxHrFmUXu4EEUpdlxl6RudZUPZoc+zuY2hk1j7XxVroIVIan/pD/SQ==", "dev": true }, + "node_modules/mkdirp": { + "version": "0.5.6", + "resolved": "https://registry.npmjs.org/mkdirp/-/mkdirp-0.5.6.tgz", + "integrity": "sha512-FP+p8RB8OWpF3YZBCrP5gtADmtXApB5AMLn+vdyA+PyxCjrCs00mjyUozssO33cwDeT3wNGdLxJ5M//YqtHAJw==", + "dev": true, + "dependencies": { + "minimist": "^1.2.6" + }, + "bin": { + "mkdirp": "bin/cmd.js" + } + }, "node_modules/mkdirp-classic": { "version": "0.5.3", "resolved": "https://registry.npmjs.org/mkdirp-classic/-/mkdirp-classic-0.5.3.tgz", @@ -21726,6 +22244,15 @@ "node": ">=18" } }, + "node_modules/traverse": { + "version": "0.3.9", + "resolved": "https://registry.npmjs.org/traverse/-/traverse-0.3.9.tgz", + "integrity": "sha512-iawgk0hLP3SxGKDfnDJf8wTz4p2qImnyihM5Hh/sGvQ3K37dPi/w8sRhdNIxYA1TwFwc5mDhIJq+O0RsvXBKdQ==", + "dev": true, + "engines": { + "node": "*" + } + }, "node_modules/triple-beam": { "version": "1.4.1", "resolved": "https://registry.npmjs.org/triple-beam/-/triple-beam-1.4.1.tgz", @@ -22079,6 +22606,60 @@ "node": ">= 4.0.0" } }, + "node_modules/unzipper": { + "version": "0.10.14", + "resolved": "https://registry.npmjs.org/unzipper/-/unzipper-0.10.14.tgz", + "integrity": "sha512-ti4wZj+0bQTiX2KmKWuwj7lhV+2n//uXEotUmGuQqrbVZSEGFMbI68+c6JCQ8aAmUWYvtHEz2A8K6wXvueR/6g==", + "dev": true, + "dependencies": { + "big-integer": "^1.6.17", + "binary": "~0.3.0", + "bluebird": "~3.4.1", + "buffer-indexof-polyfill": "~1.0.0", + "duplexer2": "~0.1.4", + "fstream": "^1.0.12", + "graceful-fs": "^4.2.2", + "listenercount": "~1.0.1", + "readable-stream": "~2.3.6", + "setimmediate": "~1.0.4" + } + }, + "node_modules/unzipper/node_modules/isarray": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/isarray/-/isarray-1.0.0.tgz", + "integrity": "sha512-VLghIWNM6ELQzo7zwmcg0NmTVyWKYjvIeM83yjp0wRDTmUnrM678fQbcKBo6n2CJEF0szoG//ytg+TKla89ALQ==", + "dev": true + }, + "node_modules/unzipper/node_modules/readable-stream": { + "version": "2.3.8", + "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-2.3.8.tgz", + "integrity": "sha512-8p0AUk4XODgIewSi0l8Epjs+EVnWiK7NoDIEGU0HhE7+ZyY8D1IMY7odu5lRrFXGg71L15KG8QrPmum45RTtdA==", + "dev": true, + "dependencies": { + "core-util-is": "~1.0.0", + "inherits": "~2.0.3", + "isarray": "~1.0.0", + "process-nextick-args": "~2.0.0", + "safe-buffer": "~5.1.1", + "string_decoder": "~1.1.1", + "util-deprecate": "~1.0.1" + } + }, + "node_modules/unzipper/node_modules/safe-buffer": { + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.1.2.tgz", + "integrity": "sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g==", + "dev": true + }, + "node_modules/unzipper/node_modules/string_decoder": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.1.1.tgz", + "integrity": "sha512-n/ShnvDi6FHbbVfviro+WojiFzv+s8MPMHBczVePfUpDJLwoLT0ht1l4YwBCbi8pJAveEEdnkHyPyTP/mzRfwg==", + "dev": true, + "dependencies": { + "safe-buffer": "~5.1.0" + } + }, "node_modules/update-browserslist-db": { "version": "1.1.0", "resolved": "https://registry.npmjs.org/update-browserslist-db/-/update-browserslist-db-1.1.0.tgz", diff --git a/package.json b/package.json index 33998738..ff450e36 100644 --- a/package.json +++ b/package.json @@ -124,7 +124,7 @@ "webpack-cli": "^5.1.4", "eslint-plugin-wdio": "8.24.12", "@babel/register": "7.23.7", - "@wdio/cli": "8.35.1", + "@wdio/cli": "8.41.0", "@wdio/globals": "8.35.1", "@wdio/local-runner": "8.35.1", "@wdio/mocha-framework": "8.35.0", diff --git a/src/server/check-answers/controller.js b/src/server/check-answers/controller.js deleted file mode 100644 index 8b27abc6..00000000 --- a/src/server/check-answers/controller.js +++ /dev/null @@ -1,84 +0,0 @@ -import { calculateNextPage } from '../common/helpers/next-page.js' -import { OriginSection } from '../common/model/section/origin/origin.js' -import { LicenceSection } from '../common/model/section/licence/licence.js' -import { ConfirmationAnswer } from '../common/model/answer/confirmation/confirmation.js' -import { ApplicationModel } from '../common/model/application/application.js' -import { sectionToSummary } from '../common/templates/macros/create-summary.js' - -export const pageTitle = 'Check your answers before sending your application' -const heading = pageTitle - -/** - * @import {NextPage} from '../common/helpers/next-page.js' - * @import {ConfirmationPayload} from '../common/model/answer/confirmation/confirmation.js' - */ - -const checkAnswersUrlPath = '/submit/check-answers' - -/** - * @satisfies {Partial} - */ -export const checkAnswersGetController = { - handler(req, res) { - const tasks = { - origin: OriginSection.fromState(req.yar.get('origin')), - licence: LicenceSection.fromState(req.yar.get('licence')) - } - - const application = ApplicationModel.fromState({ - origin: req.yar.get('origin'), - licence: req.yar.get('licence') - }) - - const { isValid } = application.validate() - - if (!isValid) { - return res.redirect('/task-list-incomplete') - } - - return res.view('check-answers/index', { - nextPage: req.query.redirect_uri, - heading, - pageTitle, - origin: sectionToSummary(tasks.origin, checkAnswersUrlPath), - licence: sectionToSummary(tasks.licence, checkAnswersUrlPath) - }) - } -} - -/** - * @satisfies {Partial} - */ -export const checkAnswersPostController = { - handler(req, res) { - const tasks = { - origin: OriginSection.fromState(req.yar.get('origin')), - licence: LicenceSection.fromState(req.yar.get('licence')) - } - - const payload = /** @type {ConfirmationPayload & NextPage} */ (req.payload) - const confirmation = new ConfirmationAnswer(payload) - - const { isValid, errors } = confirmation.validate() - - if (!isValid) { - return res.view('check-answers/index', { - pageTitle: `Error: ${pageTitle}`, - heading, - confirmation, - errorMessages: ConfirmationAnswer.errorMessages(errors), - errorMessage: errors.confirmation, - origin: sectionToSummary(tasks.origin, checkAnswersUrlPath), - licence: sectionToSummary(tasks.licence, checkAnswersUrlPath) - }) - } - - return res.redirect( - calculateNextPage(payload.nextPage, '/submit/confirmation') - ) - } -} - -/** - * @import { ServerRoute } from '@hapi/hapi' - */ diff --git a/src/server/check-answers/controller.test.js b/src/server/check-answers/controller.test.js deleted file mode 100644 index ae7c695f..00000000 --- a/src/server/check-answers/controller.test.js +++ /dev/null @@ -1,198 +0,0 @@ -import { createServer } from '~/src/server/index.js' -import { statusCodes } from '~/src/server/common/constants/status-codes.js' -import { pageTitle } from './controller.js' -import { withCsrfProtection } from '~/src/server/common/test-helpers/csrf.js' -import { parseDocument } from '~/src/server/common/test-helpers/dom.js' -import SessionTestHelper from '../common/test-helpers/session-helper.js' - -describe('#CheckAnswers', () => { - /** @type {Server} */ - let server - let session - - const originDefaultState = { - onOffFarm: 'off', - cphNumber: '12/123/1234', - originType: 'afu', - address: { - addressLine1: 'Starfleet Headquarters', - addressLine2: '24-593 Federation Drive', - addressTown: 'San Francisco', - addressCounty: 'San Francisco', - addressPostcode: 'RG24 8RR' - } - } - - const licenceDefaultState = { - emailAddress: 'name@example.com' - } - - beforeAll(async () => { - server = await createServer() - await server.initialize() - }) - - beforeEach(async () => { - session = await SessionTestHelper.create(server) - await session.setState('origin', originDefaultState) - await session.setState('licence', licenceDefaultState) - }) - - afterAll(async () => { - await server.stop({ timeout: 0 }) - }) - - it('Should provide expected response', async () => { - const { payload, statusCode } = await server.inject( - withCsrfProtection( - { - method: 'GET', - url: '/submit/check-answers', - payload: {} - }, - { - Cookie: session.sessionID - } - ) - ) - - const document = parseDocument(payload) - expect(document.title).toEqual(pageTitle) - - const taskListValues = document.querySelectorAll( - '.govuk-summary-list__value' - ) - - expect(taskListValues[0].innerHTML).toContain('Off the farm or premises') - expect(taskListValues[1].innerHTML).toContain( - 'Approved finishing unit (AFU)' - ) - expect(taskListValues[2].innerHTML).toContain('12/123/1234') - expect(taskListValues[3].innerHTML).toContain('Starfleet Headquarters') - expect(taskListValues[4].innerHTML).toContain('name@example.com') - - expect(statusCode).toBe(statusCodes.ok) - }) - - it('Should redirect to task-incomplete page if not all answers valid', async () => { - await session.setState('origin', {}) - const { headers, statusCode } = await server.inject( - withCsrfProtection( - { - method: 'GET', - url: '/submit/check-answers', - payload: {} - }, - { - Cookie: session.sessionID - } - ) - ) - - expect(statusCode).toBe(statusCodes.redirect) - expect(headers.location).toBe('/task-list-incomplete') - }) - - it('Should stay in check-answers if all tasks are valid', async () => { - const { statusCode } = await server.inject( - withCsrfProtection( - { - method: 'GET', - url: '/submit/check-answers' - }, - { - Cookie: session.sessionID - } - ) - ) - - expect(statusCode).toBe(statusCodes.ok) - }) - - it('Should redirect to task-list-incomplete if any task is invalid', async () => { - await session.setState('licence', {}) - - const { statusCode, headers } = await server.inject( - withCsrfProtection( - { - method: 'GET', - url: '/submit/check-answers' - }, - { - Cookie: session.sessionID - } - ) - ) - - expect(statusCode).toBe(statusCodes.redirect) - expect(headers.location).toBe('/task-list-incomplete') - }) - - it('Should display an error', async () => { - const { payload, statusCode } = await server.inject( - withCsrfProtection({ - method: 'POST', - url: '/submit/check-answers', - payload: {} - }) - ) - - expect(parseDocument(payload).title).toBe(`Error: ${pageTitle}`) - expect(payload).toEqual( - expect.stringContaining( - 'You need to tick a declaration box' - ) - ) - - expect(statusCode).toBe(statusCodes.ok) - }) - - it('Should redirect correctly when there is no error', async () => { - const { headers, statusCode } = await server.inject( - withCsrfProtection({ - method: 'POST', - url: '/submit/check-answers', - payload: { - confirmation: ['confirm', 'other'] - } - }) - ) - - expect(statusCode).toBe(statusCodes.redirect) - expect(headers.location).toBe('/submit/confirmation') - }) - - it('Should redirect correctly when only `confirm` present', async () => { - const { headers, statusCode } = await server.inject( - withCsrfProtection({ - method: 'POST', - url: '/submit/check-answers', - payload: { - confirmation: 'confirm' - } - }) - ) - - expect(statusCode).toBe(statusCodes.redirect) - expect(headers.location).toBe('/submit/confirmation') - }) - - it('Should redirect correctly when only `other` present', async () => { - const { headers, statusCode } = await server.inject( - withCsrfProtection({ - method: 'POST', - url: '/submit/check-answers', - payload: { - confirmation: 'other' - } - }) - ) - - expect(statusCode).toBe(statusCodes.redirect) - expect(headers.location).toBe('/submit/confirmation') - }) -}) - -/** - * @import { Server } from '@hapi/hapi' - */ diff --git a/src/server/check-answers/index.js b/src/server/check-answers/index.js index 1bf12644..ed1995b5 100644 --- a/src/server/check-answers/index.js +++ b/src/server/check-answers/index.js @@ -1,36 +1,84 @@ -import { - checkAnswersGetController, - checkAnswersPostController -} from '~/src/server/check-answers/controller.js' +import { sectionToSummary } from '../common/templates/macros/create-summary.js' +import { OriginSection } from '../common/model/section/origin/origin.js' +import { LicenceSection } from '../common/model/section/licence/licence.js' +import { DestinationSection } from '../common/model/section/destination/destination.js' +import { QuestionPage } from '../common/model/page/question-page-model.js' +import { QuestionPageController } from '../common/controller/question-page-controller/question-page-controller.js' +import { ConfirmationAnswer } from '../common/model/answer/confirmation/confirmation.js' +import { Page } from '../common/model/page/page-model.js' +import { ApplicationModel } from '../common/model/application/application.js' -/** - * Sets up the routes used in the home page. - * These routes are registered in src/server/router.js. - */ +const checkAnswersUrlPath = '/submit/check-answers' -/** - * @satisfies {ServerRegisterPluginObject} - */ -export const checkAnswers = { - plugin: { - name: 'check-answers', - register(server) { - server.route([ - { - method: 'GET', - path: '/submit/check-answers', - ...checkAnswersGetController - }, - { - method: 'POST', - path: '/submit/check-answers', - ...checkAnswersPostController - } - ]) +class ConfirmationPage extends Page { + urlPath = `/submit/confirmation` +} + +export class SubmitSummaryPage extends QuestionPage { + question = 'Check your answers before sending your application' + sectionKey = 'submit' + questionKey = 'check-answers' + urlPath = `/${this.sectionKey}/${this.questionKey}` + + view = `check-answers/index` + + Answer = ConfirmationAnswer + + nextPage() { + return new ConfirmationPage() + } + + viewProps(req) { + const tasks = { + origin: OriginSection.fromState(req.yar.get('origin')), + licence: LicenceSection.fromState(req.yar.get('licence')), + destination: DestinationSection.fromState(req.yar.get('destination')) + } + + return { + origin: sectionToSummary(tasks.origin, checkAnswersUrlPath), + licence: sectionToSummary(tasks.licence, checkAnswersUrlPath), + destination: sectionToSummary(tasks.destination, checkAnswersUrlPath) } } } +export const submitSummaryPage = new SubmitSummaryPage() + +export class SubmitPageController extends QuestionPageController { + constructor() { + super(new SubmitSummaryPage()) + } + + getHandler(req, h) { + const application = ApplicationModel.fromState({ + origin: req.yar.get('origin'), + licence: req.yar.get('licence'), + destination: req.yar.get('destination') + }) + + const { isValid } = application.validate() + + if (!isValid) { + return h.redirect('/task-list-incomplete') + } + + return super.getHandler(req, h) + } + + postHandler(req, h) { + // eslint-disable-next-line no-console + console.info('do custom logic here') + + return super.postHandler(req, h) + } +} + +/** + * @satisfies {ServerRegisterPluginObject} + */ +export const submitSummary = new SubmitPageController().plugin() + /** * @import { ServerRegisterPluginObject } from '@hapi/hapi' */ diff --git a/src/server/check-answers/index.njk b/src/server/check-answers/index.njk index cf451337..865d06b4 100644 --- a/src/server/check-answers/index.njk +++ b/src/server/check-answers/index.njk @@ -1,7 +1,6 @@ {% from "govuk/components/checkboxes/macro.njk" import govukCheckboxes %} {% extends 'layouts/questions.njk' %} {% from "macros/create-summary.njk" import createSummary %} - {% block beforequestion %}
@@ -9,6 +8,8 @@

Movement origin

{{ createSummary(origin) }} +

Movement destination

+ {{ createSummary(destination) }}

Receiving the licence

{{ createSummary(licence) }}
@@ -16,12 +17,9 @@
{% endblock %} - {% block questions %}

Your declaration

-

Before you submit your application, you need to confirm:

-
  • all relevant sections are complete
  • @@ -29,14 +27,13 @@ knowledge
- {{ govukCheckboxes ({ name: "confirmation", id: "confirmation", - errorMessage: errorMessage, + errorMessage: errors.confirmation, fieldset: { legend:{ text: "", diff --git a/src/server/check-answers/index.test.js b/src/server/check-answers/index.test.js new file mode 100644 index 00000000..1ab5b00a --- /dev/null +++ b/src/server/check-answers/index.test.js @@ -0,0 +1,83 @@ +import { createServer } from '~/src/server/index.js' +import { submitSummaryPage } from './index.js' +import { statusCodes } from '../common/constants/status-codes.js' +import SessionTester from '../common/test-helpers/session-helper.js' +import { withCsrfProtection } from '../common/test-helpers/csrf.js' +import { parseDocument } from '../common/test-helpers/dom.js' + +describe('#checkAnswers', () => { + let server + let session + + beforeAll(async () => { + server = await createServer() + await server.initialize() + }) + + beforeEach(async () => { + session = await SessionTester.create(server) + }) + + it('should return the correct urlPath for SubmitSummaryPage', () => { + expect(submitSummaryPage.urlPath).toBe('/submit/check-answers') + }) + + it('should return the correct view for SubmitSummaryPage', () => { + expect(submitSummaryPage.view).toBe('check-answers/index') + }) + + it('should return the correct nextPage for SubmitSummaryPage', () => { + const nextPage = submitSummaryPage.nextPage() + expect(nextPage.urlPath).toBe('/submit/confirmation') + }) + + it('Should redirect to incomplete if no state given', async () => { + const { headers, statusCode } = await server.inject({ + method: 'GET', + url: '/submit/check-answers' + }) + + expect(statusCode).toBe(statusCodes.redirect) + expect(headers.location).toBe('/task-list-incomplete') + }) + + it('Should provide expected response', async () => { + await session.setState('origin', { + onOffFarm: 'off', + originType: 'afu', + cphNumber: '12/345/6789', + address: { + addressLine1: '73 OCEANA CRESCENT', + addressLine2: 'Archronos Ltd', + addressTown: 'Basingstoke', + addressCounty: 'Hampshire', + addressPostcode: 'RG224FF' + } + }) + + await session.setState('destination', { + destinationType: 'afu' + }) + + await session.setState('licence', { + emailAddress: 'here@there.com' + }) + + const { payload, statusCode } = await server.inject( + withCsrfProtection( + { + method: 'GET', + url: '/submit/check-answers' + }, + { + Cookie: session.sessionID + } + ) + ) + + expect(statusCode).toBe(statusCodes.ok) + expect(parseDocument(payload).title).toBe( + 'Check your answers before sending your application' + ) + }) +}) diff --git a/src/server/common/controller/page-controller/page-controller.js b/src/server/common/controller/page-controller/page-controller.js index 0694eb7d..cf6e808a 100644 --- a/src/server/common/controller/page-controller/page-controller.js +++ b/src/server/common/controller/page-controller/page-controller.js @@ -57,7 +57,7 @@ export class PageController { pageTitle: this.page.title, heading: this.page.heading, hideQuestion: true, - ...this.page.viewProps + ...this.page.viewProps(req) }) } diff --git a/src/server/common/controller/page-controller/page-controller.test.js b/src/server/common/controller/page-controller/page-controller.test.js index b5605183..691b376c 100644 --- a/src/server/common/controller/page-controller/page-controller.test.js +++ b/src/server/common/controller/page-controller/page-controller.test.js @@ -32,7 +32,7 @@ class TestPage extends Page { return new TestNextPage() } - get viewProps() { + viewProps() { return { continueUrl: this.nextPage().urlPath } diff --git a/src/server/common/controller/question-page-controller/question-page-controller.js b/src/server/common/controller/question-page-controller/question-page-controller.js index 5a0a9033..107d895c 100644 --- a/src/server/common/controller/question-page-controller/question-page-controller.js +++ b/src/server/common/controller/question-page-controller/question-page-controller.js @@ -48,7 +48,8 @@ export class QuestionPageController { nextPage: req.query.redirect_uri, pageTitle: this.page.title, heading: this.page.heading, - value: answer.value + value: answer.value, + ...this.page.viewProps(req) }) } @@ -70,7 +71,8 @@ export class QuestionPageController { heading: this.page.heading, value: answer.value, errors, - errorMessages: Answer.errorMessages(errors) + errorMessages: Answer.errorMessages(errors), + ...this.page.viewProps(req) }) } @@ -85,7 +87,13 @@ export class QuestionPageController { return h.redirect(nextPage.urlPath) } else { if (nextPage.overrideRedirects) { - return h.redirect(nextPage.urlPath) + const query = new URLSearchParams(req.query) + let url = nextPage.urlPath + + if (query.size > 0) { + url += `?${query.toString()}` + } + return h.redirect(url) } return h.redirect(calculateNextPage(payload.nextPage, nextPage.urlPath)) diff --git a/src/server/common/controller/question-page-controller/question-page-controller.test.js b/src/server/common/controller/question-page-controller/question-page-controller.test.js index 4a42356a..f22aea23 100644 --- a/src/server/common/controller/question-page-controller/question-page-controller.test.js +++ b/src/server/common/controller/question-page-controller/question-page-controller.test.js @@ -378,5 +378,30 @@ describe('QuestionPageController', () => { expect(headers.location).not.toBe(redirectUrl) expect(headers.location).toBe(overriddenQuestionUrl) }) + + it('should add a query string onto tthe overriden url', async () => { + await session.setState(sectionKey, { [questionKey]: 'block-redirect' }) + + const redirectUrl = '/dummy/incorrect-url' + const { statusCode, headers } = await server.inject( + withCsrfProtection( + { + method: 'POST', + url: `${questionUrl}?redirect=true`, + payload: { + nextPage: redirectUrl, + [questionKey]: 'block-redirect' + } + }, + { + Cookie: session.sessionID + } + ) + ) + + expect(statusCode).toBe(statusCodes.redirect) + expect(headers.location).not.toBe(redirectUrl) + expect(headers.location).toBe(`${overriddenQuestionUrl}?redirect=true`) + }) }) }) diff --git a/src/server/common/model/answer/confirmation/confirmation.js b/src/server/common/model/answer/confirmation/confirmation.js index d44ca663..07c0b239 100644 --- a/src/server/common/model/answer/confirmation/confirmation.js +++ b/src/server/common/model/answer/confirmation/confirmation.js @@ -22,10 +22,16 @@ const ensureArray = (value) => (Array.isArray(value) ? value : [value]) */ export class ConfirmationAnswer extends AnswerModel { get value() { - return { - confirm: this._data?.confirmation.includes('confirm'), - other: this._data?.confirmation.includes('other') + const data = Array.isArray(this._data?.confirmation) + ? this._data.confirmation + : [this._data?.confirmation] + + const value = { + confirm: data.includes('confirm'), + other: data.includes('other') } + + return value } get html() { diff --git a/src/server/common/model/application/application.js b/src/server/common/model/application/application.js index 4d6af922..f327e027 100644 --- a/src/server/common/model/application/application.js +++ b/src/server/common/model/application/application.js @@ -1,3 +1,4 @@ +import { DestinationSection } from '../section/destination/destination.js' import { LicenceSection } from '../section/licence/licence.js' import { OriginSection } from '../section/origin/origin.js' import { validateApplication } from './validation.js' @@ -10,10 +11,11 @@ import { validateApplication } from './validation.js' * export @typedef {{ * origin: OriginData | undefined; * licence: LicenceData | undefined; + * destination: DestinationData | undefined; * }} ApplicationData * @import {OriginData} from '../section/origin/origin.js' * @import {LicenceData} from '../section/licence/licence.js' - * @import {AddressData} from '../answer/address/address.js' + * @import {DestinationData} from '../section/destination/destination.js' */ export class ApplicationModel { @@ -42,6 +44,13 @@ export class ApplicationModel { return LicenceSection.fromState(this._data.licence) } + /** + * @returns {DestinationSection} + */ + get destination() { + return DestinationSection.fromState(this._data.destination) + } + /* eslint-disable @typescript-eslint/no-unused-vars */ /** @@ -51,7 +60,8 @@ export class ApplicationModel { static fromState(state) { return new ApplicationModel({ origin: OriginSection.fromState(state?.origin), - licence: LicenceSection.fromState(state?.licence) + licence: LicenceSection.fromState(state?.licence), + destination: DestinationSection.fromState(state?.destination) }) } diff --git a/src/server/common/model/application/application.test.js b/src/server/common/model/application/application.test.js index 1b4f8646..d2006d92 100644 --- a/src/server/common/model/application/application.test.js +++ b/src/server/common/model/application/application.test.js @@ -2,6 +2,7 @@ import { ApplicationModel } from './application.js' import { OnOffFarmAnswer } from '../answer/on-off-farm/on-off-farm.js' import { LicenceSection } from '../section/licence/licence.js' import { OriginSection } from '../section/origin/origin.js' +import { DestinationSection } from '../section/destination/destination.js' const originDefaultState = { onOffFarm: new OnOffFarmAnswer({ onOffFarm: 'on' }).toState(), @@ -19,11 +20,17 @@ const licenceDefaultState = { emailAddress: 'name@example.com' } +/** @type {import('../section/destination/destination.js').DestinationData} */ +const destinationDefaultState = { + destinationType: 'dedicated-sale' +} + describe('Application', () => { it('should create an Application instance from a valid state', () => { const state = { origin: originDefaultState, - licence: licenceDefaultState + licence: licenceDefaultState, + destination: destinationDefaultState } const application = ApplicationModel.fromState(state) @@ -32,5 +39,6 @@ describe('Application', () => { expect(application.origin).toBeInstanceOf(OriginSection) expect(application.licence).toBeInstanceOf(LicenceSection) + expect(application.destination).toBeInstanceOf(DestinationSection) }) }) diff --git a/src/server/common/model/page/page-model.js b/src/server/common/model/page/page-model.js index e5c1ecb9..873d8e01 100644 --- a/src/server/common/model/page/page-model.js +++ b/src/server/common/model/page/page-model.js @@ -36,8 +36,12 @@ export class Page { return this.pageTitle } - /** @returns {Record} */ - get viewProps() { + /** + * @param {import('@hapi/hapi').Request} _req + * @returns {Record} + * */ + // eslint-disable-next-line @typescript-eslint/no-unused-vars + viewProps(_req) { return {} } diff --git a/src/server/common/model/page/page-model.test.js b/src/server/common/model/page/page-model.test.js index 2b708649..226905e0 100644 --- a/src/server/common/model/page/page-model.test.js +++ b/src/server/common/model/page/page-model.test.js @@ -48,6 +48,6 @@ describe('Page', () => { }) it('should return an empty object for viewProps', () => { - expect(page.viewProps).toEqual({}) + expect(page.viewProps()).toEqual({}) }) }) diff --git a/src/server/destination/general-licence/index.js b/src/server/destination/general-licence/index.js index 33e8852d..98d74b0d 100644 --- a/src/server/destination/general-licence/index.js +++ b/src/server/destination/general-licence/index.js @@ -1,6 +1,7 @@ import { Page } from '../../common/model/page/page-model.js' import { PageController } from '../../common/controller/page-controller/page-controller.js' import { DestinationSummaryPage } from '../summary/index.js' +import { calculateNextPage } from '../../common/helpers/next-page.js' /** * @import { ServerRegisterPluginObject } from '@hapi/hapi' @@ -21,9 +22,12 @@ export class DestinationGeneralLicencePage extends Page { return new DestinationSummaryPage() } - get viewProps() { + viewProps(req) { return { - continueUrl: this.nextPage().urlPath + continueUrl: calculateNextPage( + req?.query?.redirect_uri, + this.nextPage().urlPath + ) } } } diff --git a/src/server/destination/general-licence/index.test.js b/src/server/destination/general-licence/index.test.js index 438db811..de09ab59 100644 --- a/src/server/destination/general-licence/index.test.js +++ b/src/server/destination/general-licence/index.test.js @@ -41,6 +41,6 @@ describe('DestinationGeneralLicencePage', () => { }) it('should be able ot calculate the next page URL as a string for the template', () => { - expect(page.viewProps.continueUrl).toBe(nextPage.urlPath) + expect(page.viewProps().continueUrl).toBe(nextPage.urlPath) }) }) diff --git a/src/server/router.js b/src/server/router.js index db10b00b..16be895b 100644 --- a/src/server/router.js +++ b/src/server/router.js @@ -6,10 +6,10 @@ import { serveStaticFiles } from '~/src/server/common/helpers/serve-static-files import { origin } from './origin/index.js' import { taskList } from './task-list/index.js' import { taskListIncomplete } from './task-list-incomplete/index.js' -import { checkAnswers } from './check-answers/index.js' import { licence } from './licence/index.js' import { destination } from './destination/index.js' import { privacyPolicy } from './privacy-policy/index.js' +import { submitSummary } from './check-answers/index.js' /** * @satisfies {ServerRegisterPluginObject} @@ -32,7 +32,7 @@ export const router = { licence, taskList, taskListIncomplete, - checkAnswers + submitSummary ]) // Static assets diff --git a/user-journey-tests/helpers/testHelpers/destination.js b/user-journey-tests/helpers/testHelpers/destination.js index 797ab172..4e145875 100644 --- a/user-journey-tests/helpers/testHelpers/destination.js +++ b/user-journey-tests/helpers/testHelpers/destination.js @@ -6,7 +6,7 @@ import destinationSelectionPage from '../../page-objects/destination/destination import generalLicencePage from '../../page-objects/destination/generalLicencePage.js' // Helper function to complete the origin task -const completeDestinationTest = async (radioType) => { +const completeDestinationTask = async (radioType) => { await landingPage.navigateToPageAndVerifyTitle() await landingPage.verifyStartNowButton('Start now', true) await taskListPage.selectMovementDestination() @@ -29,4 +29,4 @@ const completeDestinationTest = async (radioType) => { } } -export default completeDestinationTest +export default completeDestinationTask diff --git a/user-journey-tests/page-objects/destination/generalLicencePage.js b/user-journey-tests/page-objects/destination/generalLicencePage.js index 1f2a63ef..e7b5ce26 100644 --- a/user-journey-tests/page-objects/destination/generalLicencePage.js +++ b/user-journey-tests/page-objects/destination/generalLicencePage.js @@ -1,3 +1,4 @@ +import { selectElement } from '../../helpers/page.js' import { Page } from '../page.js' const pageHeadingAndTitle = 'Check if you have a general licence' @@ -10,6 +11,10 @@ class GeneralLicencePage extends Page { get continueLink() { return $('a=Continue') } + + selectContinueLink() { + return selectElement(this.continueLink) + } } export default new GeneralLicencePage() diff --git a/user-journey-tests/specs/destination/destinationAnswers.spec.js b/user-journey-tests/specs/destination/destinationAnswers.spec.js index 5a7aebfb..818660e7 100644 --- a/user-journey-tests/specs/destination/destinationAnswers.spec.js +++ b/user-journey-tests/specs/destination/destinationAnswers.spec.js @@ -3,7 +3,7 @@ import { validateElementVisibleAndText, waitForPagePath } from '../../helpers/page.js' -import completeDestinationTest from '../../helpers/testHelpers/destination.js' +import completeDestinationTask from '../../helpers/testHelpers/destination.js' import completeOriginTaskAnswers from '../../helpers/testHelpers/movementLicence.js' import canNotUseServicePage from '../../page-objects/destination/canNotUseServicePage.js' import destinationAnswersPage from '../../page-objects/destination/destinationAnswersPage.js' @@ -21,7 +21,7 @@ describe('Check your answers test - destination', () => { }) it('Should verify slaughter answer and change link', async () => { - await completeDestinationTest('slaughter') + await completeDestinationTask('slaughter') await validateElementVisibleAndText( destinationAnswersPage.destinationValue, 'Slaughter' @@ -31,7 +31,7 @@ describe('Check your answers test - destination', () => { await expect(destinationSelectionPage.slaughterRadio).toBeSelected() await destinationSelectionPage.selectContinue() - await selectElement(generalLicencePage.continueLink) + await generalLicencePage.selectContinueLink() await validateElementVisibleAndText( destinationAnswersPage.destinationValue, 'Slaughter' @@ -39,7 +39,7 @@ describe('Check your answers test - destination', () => { }) it('Should verify dedicated sale for tb answer and change link', async () => { - await completeDestinationTest('dedicated') + await completeDestinationTask('dedicated') await validateElementVisibleAndText( destinationAnswersPage.destinationValue, 'Dedicated sale for TB (orange market)' @@ -56,7 +56,7 @@ describe('Check your answers test - destination', () => { }) it('Should verify approved finishing unit answer and change link', async () => { - await completeDestinationTest('approved') + await completeDestinationTask('approved') await validateElementVisibleAndText( destinationAnswersPage.destinationValue, 'Approved finishing unit (AFU)' @@ -73,7 +73,7 @@ describe('Check your answers test - destination', () => { }) it('Should verify continue link', async () => { - await completeDestinationTest('approved') + await completeDestinationTask('approved') await validateElementVisibleAndText( destinationAnswersPage.destinationValue, 'Approved finishing unit (AFU)' @@ -84,7 +84,7 @@ describe('Check your answers test - destination', () => { }) it('Should handle "Another destination" as an exit page & redirect users back to the preceding question', async () => { - await completeDestinationTest('dedicated') + await completeDestinationTask('dedicated') await validateElementVisibleAndText( destinationAnswersPage.destinationValue, 'Dedicated sale for TB (orange market)' diff --git a/user-journey-tests/specs/finalAnswers.spec.js b/user-journey-tests/specs/finalAnswers.spec.js index 50b093e0..f47a21ea 100644 --- a/user-journey-tests/specs/finalAnswers.spec.js +++ b/user-journey-tests/specs/finalAnswers.spec.js @@ -1,4 +1,4 @@ -import { waitForPagePath } from '../helpers/page.js' +import { selectElement, waitForPagePath } from '../helpers/page.js' import landingPage from '../page-objects/landingPage.js' import { validateAndAdjustAddress, @@ -15,6 +15,10 @@ import licenceAnswersPage from '../page-objects/receiving-the-licence/licenceAns import checkAnswersPage from '../page-objects/origin/checkAnswersPage.js' import taskListPage from '../page-objects/taskListPage.js' import submissionConfirmationPage from '../page-objects/submissionConfirmationPage.js' +import completeDestinationTask from '../helpers/testHelpers/destination.js' +import destinationAnswersPage from '../page-objects/destination/destinationAnswersPage.js' +import destinationSelectionPage from '../page-objects/destination/destinationSelectionPage.js' +import generalLicencePage from '../page-objects/destination/generalLicencePage.js' const emailDefault = 'default@email.com' const editedEmail = 'edited@email.com' @@ -48,7 +52,15 @@ describe('Check your final answers test', () => { taskTitle: 'Movement origin', expectedStatus: 'Completed' }) - await landingPage.navigateToPageAndVerifyTitle() + + await completeDestinationTask('approved') + await destinationAnswersPage.selectContinue() + await taskListPage.verifyStatus({ + position: 2, + taskTitle: 'Movement destination', + expectedStatus: 'Completed' + }) + await completeLicenceTaskAnswersCustom(emailDefault) await licenceAnswersPage.selectContinue() await taskListPage.verifyPageHeadingAndTitle() @@ -137,6 +149,19 @@ describe('Check your final answers test', () => { await finalAnswersPage.submissionErrorTest() }) + it('Should go via the general licence page if the destination type is changed to "slaughter"', async () => { + await finalAnswersPage.navigateToPageAndVerifyTitle() + + await selectElement(finalAnswersPage.movementDestinationChange) + + await expect(destinationSelectionPage.approvedFinishingRadio).toBeSelected() + + await destinationSelectionPage.selectSlaughterRadioAndContinue() + await generalLicencePage.verifyPageHeadingAndTitle() + await generalLicencePage.selectContinueLink() + await finalAnswersPage.verifyPageHeadingAndTitle() + }) + it('Should verify changing the value to on the farm and navigating back', async () => { await finalAnswersPage.navigateToPageAndVerifyTitle() await validateOnFarmErrorHandling(finalAnswersPage.onOffFarmChange, true) diff --git a/user-journey-tests/specs/taskList.spec.js b/user-journey-tests/specs/taskList.spec.js index 385ea018..ec17aa7b 100644 --- a/user-journey-tests/specs/taskList.spec.js +++ b/user-journey-tests/specs/taskList.spec.js @@ -7,7 +7,7 @@ import taskListIncompletePage from '../page-objects/taskListIncompletePage.js' import completeOriginTaskAnswers from '../helpers/testHelpers/movementLicence.js' import completeLicenceTaskAnswers from '../helpers/testHelpers/receivingLicence.js' import licenceAnswersPage from '../page-objects/receiving-the-licence/licenceAnswersPage.js' -import completeDestinationTest from '../helpers/testHelpers/destination.js' +import completeDestinationTask from '../helpers/testHelpers/destination.js' import destinationAnswersPage from '../page-objects/destination/destinationAnswersPage.js' describe('Task list page test', () => { @@ -85,7 +85,7 @@ describe('Task list page test', () => { it('Should verify completed destination task', async () => { await browser.reloadSession() await completeOriginTaskAnswers() - await completeDestinationTest('slaughter') + await completeDestinationTask('slaughter') await taskListPage.navigateToPageAndVerifyTitle() await taskListPage.verifyAllStatus([ {