diff --git a/api.planx.uk/.husky/pre-commit b/api.planx.uk/.husky/pre-commit index 58eb265738..2dca158161 100755 --- a/api.planx.uk/.husky/pre-commit +++ b/api.planx.uk/.husky/pre-commit @@ -1,4 +1,4 @@ #!/usr/bin/env sh . "$(dirname -- "$0")/_/husky.sh" cd api.planx.uk -pnpm dlx lint-staged \ No newline at end of file +pnpm dlx lint-staged diff --git a/api.planx.uk/package.json b/api.planx.uk/package.json index da3982bf48..dd6a3f7a74 100644 --- a/api.planx.uk/package.json +++ b/api.planx.uk/package.json @@ -11,7 +11,7 @@ }, "dependencies": { "@airbrake/node": "^2.1.8", - "@opensystemslab/planx-core": "git+https://github.com/theopensystemslab/planx-core#c515291", + "@opensystemslab/planx-core": "git+https://github.com/theopensystemslab/planx-core#c4a725f", "@types/isomorphic-fetch": "^0.0.36", "adm-zip": "^0.5.10", "aws-sdk": "^2.1467.0", @@ -116,7 +116,7 @@ "tsx": "^4.16.2", "typescript": "^5.5.2", "uuid": "^10.0.0", - "vitest": "^2.0.5" + "vitest": "^2.1.1" }, "pnpm": { "peerDependencyRules": { diff --git a/api.planx.uk/pnpm-lock.yaml b/api.planx.uk/pnpm-lock.yaml index fd7865e0aa..e8c91463cb 100644 --- a/api.planx.uk/pnpm-lock.yaml +++ b/api.planx.uk/pnpm-lock.yaml @@ -14,8 +14,8 @@ dependencies: specifier: ^2.1.8 version: 2.1.8 '@opensystemslab/planx-core': - specifier: git+https://github.com/theopensystemslab/planx-core#c515291 - version: github.com/theopensystemslab/planx-core/c515291 + specifier: git+https://github.com/theopensystemslab/planx-core#c4a725f + version: github.com/theopensystemslab/planx-core/c4a725f '@types/isomorphic-fetch': specifier: ^0.0.36 version: 0.0.36 @@ -221,13 +221,13 @@ devDependencies: version: 5.62.0(eslint@8.57.0)(typescript@5.5.2) '@vitest/coverage-istanbul': specifier: ^2.0.5 - version: 2.0.5(vitest@2.0.5) + version: 2.0.5(vitest@2.1.1) '@vitest/eslint-plugin': specifier: ^1.1.0 - version: 1.1.0(eslint@8.57.0)(typescript@5.5.2)(vitest@2.0.5) + version: 1.1.0(eslint@8.57.0)(typescript@5.5.2)(vitest@2.1.1) '@vitest/ui': specifier: ^2.0.5 - version: 2.0.5(vitest@2.0.5) + version: 2.0.5(vitest@2.1.1) dotenv: specifier: ^16.4.5 version: 16.4.5 @@ -268,8 +268,8 @@ devDependencies: specifier: ^10.0.0 version: 10.0.0 vitest: - specifier: ^2.0.5 - version: 2.0.5(@types/node@18.19.13)(@vitest/ui@2.0.5)(jsdom@24.1.0) + specifier: ^2.1.1 + version: 2.1.1(@types/node@18.19.13)(@vitest/ui@2.0.5)(jsdom@24.1.0) packages: @@ -1920,7 +1920,7 @@ packages: /@ungap/structured-clone@1.2.0: resolution: {integrity: sha512-zuVdFrMJiuCDQUMCzQaD6KL28MjnqqN8XnAqiEq9PNm/hCPTSGfrXCOfwj1ow4LFb/tNymJPwsNbVePc1xFqrQ==} - /@vitest/coverage-istanbul@2.0.5(vitest@2.0.5): + /@vitest/coverage-istanbul@2.0.5(vitest@2.1.1): resolution: {integrity: sha512-BvjWKtp7fiMAeYUD0mO5cuADzn1gmjTm54jm5qUEnh/O08riczun8rI4EtQlg3bWoRo2lT3FO8DmjPDX9ZthPw==} peerDependencies: vitest: 2.0.5 @@ -1935,12 +1935,12 @@ packages: magicast: 0.3.5 test-exclude: 7.0.1 tinyrainbow: 1.2.0 - vitest: 2.0.5(@types/node@18.19.13)(@vitest/ui@2.0.5)(jsdom@24.1.0) + vitest: 2.1.1(@types/node@18.19.13)(@vitest/ui@2.0.5)(jsdom@24.1.0) transitivePeerDependencies: - supports-color dev: true - /@vitest/eslint-plugin@1.1.0(eslint@8.57.0)(typescript@5.5.2)(vitest@2.0.5): + /@vitest/eslint-plugin@1.1.0(eslint@8.57.0)(typescript@5.5.2)(vitest@2.1.1): resolution: {integrity: sha512-Ur80Y27Wbw8gFHJ3cv6vypcjXmrx6QHfw+q435h6Q2L+tf+h4Xf5pJTCL4YU/Jps9EVeggQxS85OcUZU7sdXRw==} peerDependencies: '@typescript-eslint/utils': '>= 8.0' @@ -1957,46 +1957,70 @@ packages: dependencies: eslint: 8.57.0 typescript: 5.5.2 - vitest: 2.0.5(@types/node@18.19.13)(@vitest/ui@2.0.5)(jsdom@24.1.0) + vitest: 2.1.1(@types/node@18.19.13)(@vitest/ui@2.0.5)(jsdom@24.1.0) dev: true - /@vitest/expect@2.0.5: - resolution: {integrity: sha512-yHZtwuP7JZivj65Gxoi8upUN2OzHTi3zVfjwdpu2WrvCZPLwsJ2Ey5ILIPccoW23dd/zQBlJ4/dhi7DWNyXCpA==} + /@vitest/expect@2.1.1: + resolution: {integrity: sha512-YeueunS0HiHiQxk+KEOnq/QMzlUuOzbU1Go+PgAsHvvv3tUkJPm9xWt+6ITNTlzsMXUjmgm5T+U7KBPK2qQV6w==} dependencies: - '@vitest/spy': 2.0.5 - '@vitest/utils': 2.0.5 + '@vitest/spy': 2.1.1 + '@vitest/utils': 2.1.1 chai: 5.1.1 tinyrainbow: 1.2.0 dev: true + /@vitest/mocker@2.1.1(@vitest/spy@2.1.1)(vite@5.4.2): + resolution: {integrity: sha512-LNN5VwOEdJqCmJ/2XJBywB11DLlkbY0ooDJW3uRX5cZyYCrc4PI/ePX0iQhE3BiEGiQmK4GE7Q/PqCkkaiPnrA==} + peerDependencies: + '@vitest/spy': 2.1.1 + msw: ^2.3.5 + vite: ^5.0.0 + peerDependenciesMeta: + msw: + optional: true + vite: + optional: true + dependencies: + '@vitest/spy': 2.1.1 + estree-walker: 3.0.3 + magic-string: 0.30.11 + vite: 5.4.2(@types/node@18.19.13) + dev: true + /@vitest/pretty-format@2.0.5: resolution: {integrity: sha512-h8k+1oWHfwTkyTkb9egzwNMfJAEx4veaPSnMeKbVSjp4euqGSbQlm5+6VHwTr7u4FJslVVsUG5nopCaAYdOmSQ==} dependencies: tinyrainbow: 1.2.0 dev: true - /@vitest/runner@2.0.5: - resolution: {integrity: sha512-TfRfZa6Bkk9ky4tW0z20WKXFEwwvWhRY+84CnSEtq4+3ZvDlJyY32oNTJtM7AW9ihW90tX/1Q78cb6FjoAs+ig==} + /@vitest/pretty-format@2.1.1: + resolution: {integrity: sha512-SjxPFOtuINDUW8/UkElJYQSFtnWX7tMksSGW0vfjxMneFqxVr8YJ979QpMbDW7g+BIiq88RAGDjf7en6rvLPPQ==} dependencies: - '@vitest/utils': 2.0.5 + tinyrainbow: 1.2.0 + dev: true + + /@vitest/runner@2.1.1: + resolution: {integrity: sha512-uTPuY6PWOYitIkLPidaY5L3t0JJITdGTSwBtwMjKzo5O6RCOEncz9PUN+0pDidX8kTHYjO0EwUIvhlGpnGpxmA==} + dependencies: + '@vitest/utils': 2.1.1 pathe: 1.1.2 dev: true - /@vitest/snapshot@2.0.5: - resolution: {integrity: sha512-SgCPUeDFLaM0mIUHfaArq8fD2WbaXG/zVXjRupthYfYGzc8ztbFbu6dUNOblBG7XLMR1kEhS/DNnfCZ2IhdDew==} + /@vitest/snapshot@2.1.1: + resolution: {integrity: sha512-BnSku1WFy7r4mm96ha2FzN99AZJgpZOWrAhtQfoxjUU5YMRpq1zmHRq7a5K9/NjqonebO7iVDla+VvZS8BOWMw==} dependencies: - '@vitest/pretty-format': 2.0.5 + '@vitest/pretty-format': 2.1.1 magic-string: 0.30.11 pathe: 1.1.2 dev: true - /@vitest/spy@2.0.5: - resolution: {integrity: sha512-c/jdthAhvJdpfVuaexSrnawxZz6pywlTPe84LUB2m/4t3rl2fTo9NFGBG4oWgaD+FTgDDV8hJ/nibT7IfH3JfA==} + /@vitest/spy@2.1.1: + resolution: {integrity: sha512-ZM39BnZ9t/xZ/nF4UwRH5il0Sw93QnZXd9NAZGRpIgj0yvVwPpLd702s/Cx955rGaMlyBQkZJ2Ir7qyY48VZ+g==} dependencies: tinyspy: 3.0.0 dev: true - /@vitest/ui@2.0.5(vitest@2.0.5): + /@vitest/ui@2.0.5(vitest@2.1.1): resolution: {integrity: sha512-m+ZpVt/PVi/nbeRKEjdiYeoh0aOfI9zr3Ria9LO7V2PlMETtAXJS3uETEZkc8Be2oOl8mhd7Ew+5SRBXRYncNw==} peerDependencies: vitest: 2.0.5 @@ -2008,7 +2032,7 @@ packages: pathe: 1.1.2 sirv: 2.0.4 tinyrainbow: 1.2.0 - vitest: 2.0.5(@types/node@18.19.13)(@vitest/ui@2.0.5)(jsdom@24.1.0) + vitest: 2.1.1(@types/node@18.19.13)(@vitest/ui@2.0.5)(jsdom@24.1.0) dev: true /@vitest/utils@2.0.5: @@ -2020,6 +2044,14 @@ packages: tinyrainbow: 1.2.0 dev: true + /@vitest/utils@2.1.1: + resolution: {integrity: sha512-Y6Q9TsI+qJ2CC0ZKj6VBb+T8UPz593N113nnUykqwANqhgf3QkZeHFlusgKLTqrnVHbj/XDKZcDHol+dxVT+rQ==} + dependencies: + '@vitest/pretty-format': 2.1.1 + loupe: 3.1.1 + tinyrainbow: 1.2.0 + dev: true + /abort-controller@3.0.0: resolution: {integrity: sha512-h8lQ8tacZYnR3vNQTgibj+tODHI5/+l06Au2Pcriv/Gmet0eaj4TwWH41sO9wnHDiQsEj19q0drzdWdeAHtweg==} engines: {node: '>=6.5'} @@ -5584,6 +5616,10 @@ packages: resolution: {integrity: sha512-0+DUvqWMValLmha6lr4kD8iAMK1HzV0/aKnCtWb9v9641TnP/MFb7Pc2bxoxQjTXAErryXVgUOfv2YqNllqGeg==} dev: true + /tinyexec@0.3.0: + resolution: {integrity: sha512-tVGE0mVJPGb0chKhqmsoosjsS+qUnJVGJpZgsHYQcGoPlG3B51R3PouqTgEGH2Dc9jjFyOqOpix6ZHNMXp1FZg==} + dev: true + /tinypool@1.0.1: resolution: {integrity: sha512-URZYihUbRPcGv95En+sz6MfghfIc2OJ1sv/RmhWZLouPY0/8Vo80viwPvg3dlaS9fuq7fQMEfgRRK7BBZThBEA==} engines: {node: ^18.0.0 || >=20.0.0} @@ -5808,15 +5844,14 @@ packages: engines: {node: '>= 0.8'} dev: false - /vite-node@2.0.5(@types/node@18.19.13): - resolution: {integrity: sha512-LdsW4pxj0Ot69FAoXZ1yTnA9bjGohr2yNBU7QKRxpz8ITSkhuDl6h3zS/tvgz4qrNjeRnvrWeXQ8ZF7Um4W00Q==} + /vite-node@2.1.1(@types/node@18.19.13): + resolution: {integrity: sha512-N/mGckI1suG/5wQI35XeR9rsMsPqKXzq1CdUndzVstBj/HvyxxGctwnK6WX43NGt5L3Z5tcRf83g4TITKJhPrA==} engines: {node: ^18.0.0 || >=20.0.0} hasBin: true dependencies: cac: 6.7.14 debug: 4.3.6 pathe: 1.1.2 - tinyrainbow: 1.2.0 vite: 5.4.2(@types/node@18.19.13) transitivePeerDependencies: - '@types/node' @@ -5869,15 +5904,15 @@ packages: fsevents: 2.3.3 dev: true - /vitest@2.0.5(@types/node@18.19.13)(@vitest/ui@2.0.5)(jsdom@24.1.0): - resolution: {integrity: sha512-8GUxONfauuIdeSl5f9GTgVEpg5BTOlplET4WEDaeY2QBiN8wSm68vxN/tb5z405OwppfoCavnwXafiaYBC/xOA==} + /vitest@2.1.1(@types/node@18.19.13)(@vitest/ui@2.0.5)(jsdom@24.1.0): + resolution: {integrity: sha512-97We7/VC0e9X5zBVkvt7SGQMGrRtn3KtySFQG5fpaMlS+l62eeXRQO633AYhSTC3z7IMebnPPNjGXVGNRFlxBA==} engines: {node: ^18.0.0 || >=20.0.0} hasBin: true peerDependencies: '@edge-runtime/vm': '*' '@types/node': ^18.0.0 || >=20.0.0 - '@vitest/browser': 2.0.5 - '@vitest/ui': 2.0.5 + '@vitest/browser': 2.1.1 + '@vitest/ui': 2.1.1 happy-dom: '*' jsdom: '*' peerDependenciesMeta: @@ -5894,31 +5929,32 @@ packages: jsdom: optional: true dependencies: - '@ampproject/remapping': 2.3.0 '@types/node': 18.19.13 - '@vitest/expect': 2.0.5 - '@vitest/pretty-format': 2.0.5 - '@vitest/runner': 2.0.5 - '@vitest/snapshot': 2.0.5 - '@vitest/spy': 2.0.5 - '@vitest/ui': 2.0.5(vitest@2.0.5) - '@vitest/utils': 2.0.5 + '@vitest/expect': 2.1.1 + '@vitest/mocker': 2.1.1(@vitest/spy@2.1.1)(vite@5.4.2) + '@vitest/pretty-format': 2.1.1 + '@vitest/runner': 2.1.1 + '@vitest/snapshot': 2.1.1 + '@vitest/spy': 2.1.1 + '@vitest/ui': 2.0.5(vitest@2.1.1) + '@vitest/utils': 2.1.1 chai: 5.1.1 debug: 4.3.6 - execa: 8.0.1 jsdom: 24.1.0 magic-string: 0.30.11 pathe: 1.1.2 std-env: 3.7.0 tinybench: 2.9.0 + tinyexec: 0.3.0 tinypool: 1.0.1 tinyrainbow: 1.2.0 vite: 5.4.2(@types/node@18.19.13) - vite-node: 2.0.5(@types/node@18.19.13) + vite-node: 2.1.1(@types/node@18.19.13) why-is-node-running: 2.3.0 transitivePeerDependencies: - less - lightningcss + - msw - sass - sass-embedded - stylus @@ -6137,8 +6173,8 @@ packages: resolution: {integrity: sha512-XBx9AXhXktjUqnepgTiE5flcKIYWi/rme0Eaj+5Y0lftuGBq+jyRu/md4WnuxqgP1ubdpNCsYEYPxrzVHD8d6g==} dev: false - github.com/theopensystemslab/planx-core/c515291: - resolution: {tarball: https://codeload.github.com/theopensystemslab/planx-core/tar.gz/c515291} + github.com/theopensystemslab/planx-core/c4a725f: + resolution: {tarball: https://codeload.github.com/theopensystemslab/planx-core/tar.gz/c4a725f} name: '@opensystemslab/planx-core' version: 1.0.0 prepare: true diff --git a/docker-compose.yml b/docker-compose.yml index 1a0df54e32..4b222ac8e5 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -19,7 +19,7 @@ services: metabase: # if changing, also check infrastructure/application/index.ts - image: metabase/metabase:v0.50.22 + image: metabase/metabase:v0.50.26 profiles: ["analytics"] ports: - "${METABASE_PORT}:${METABASE_PORT}" diff --git a/e2e/tests/api-driven/package.json b/e2e/tests/api-driven/package.json index 4669e9198d..5749835145 100644 --- a/e2e/tests/api-driven/package.json +++ b/e2e/tests/api-driven/package.json @@ -7,7 +7,7 @@ "packageManager": "pnpm@8.6.6", "dependencies": { "@cucumber/cucumber": "^9.3.0", - "@opensystemslab/planx-core": "git+https://github.com/theopensystemslab/planx-core#c515291", + "@opensystemslab/planx-core": "git+https://github.com/theopensystemslab/planx-core#c4a725f", "axios": "^1.7.4", "dotenv": "^16.3.1", "dotenv-expand": "^10.0.0", diff --git a/e2e/tests/api-driven/pnpm-lock.yaml b/e2e/tests/api-driven/pnpm-lock.yaml index 8ba8e39375..29e079a319 100644 --- a/e2e/tests/api-driven/pnpm-lock.yaml +++ b/e2e/tests/api-driven/pnpm-lock.yaml @@ -9,8 +9,8 @@ dependencies: specifier: ^9.3.0 version: 9.3.0 '@opensystemslab/planx-core': - specifier: git+https://github.com/theopensystemslab/planx-core#c515291 - version: github.com/theopensystemslab/planx-core/c515291 + specifier: git+https://github.com/theopensystemslab/planx-core#c4a725f + version: github.com/theopensystemslab/planx-core/c4a725f axios: specifier: ^1.7.4 version: 1.7.4 @@ -2932,8 +2932,8 @@ packages: resolution: {integrity: sha512-XBx9AXhXktjUqnepgTiE5flcKIYWi/rme0Eaj+5Y0lftuGBq+jyRu/md4WnuxqgP1ubdpNCsYEYPxrzVHD8d6g==} dev: false - github.com/theopensystemslab/planx-core/c515291: - resolution: {tarball: https://codeload.github.com/theopensystemslab/planx-core/tar.gz/c515291} + github.com/theopensystemslab/planx-core/c4a725f: + resolution: {tarball: https://codeload.github.com/theopensystemslab/planx-core/tar.gz/c4a725f} name: '@opensystemslab/planx-core' version: 1.0.0 prepare: true diff --git a/e2e/tests/ui-driven/package.json b/e2e/tests/ui-driven/package.json index ed22c415be..61dbc79a08 100644 --- a/e2e/tests/ui-driven/package.json +++ b/e2e/tests/ui-driven/package.json @@ -8,7 +8,7 @@ "postinstall": "./install-dependencies.sh" }, "dependencies": { - "@opensystemslab/planx-core": "git+https://github.com/theopensystemslab/planx-core#c515291", + "@opensystemslab/planx-core": "git+https://github.com/theopensystemslab/planx-core#c4a725f", "axios": "^1.7.4", "dotenv": "^16.3.1", "eslint": "^8.56.0", diff --git a/e2e/tests/ui-driven/pnpm-lock.yaml b/e2e/tests/ui-driven/pnpm-lock.yaml index 8164ef6585..35a6b0923b 100644 --- a/e2e/tests/ui-driven/pnpm-lock.yaml +++ b/e2e/tests/ui-driven/pnpm-lock.yaml @@ -6,8 +6,8 @@ settings: dependencies: '@opensystemslab/planx-core': - specifier: git+https://github.com/theopensystemslab/planx-core#c515291 - version: github.com/theopensystemslab/planx-core/c515291 + specifier: git+https://github.com/theopensystemslab/planx-core#c4a725f + version: github.com/theopensystemslab/planx-core/c4a725f axios: specifier: ^1.7.4 version: 1.7.4 @@ -2674,8 +2674,8 @@ packages: resolution: {integrity: sha512-XBx9AXhXktjUqnepgTiE5flcKIYWi/rme0Eaj+5Y0lftuGBq+jyRu/md4WnuxqgP1ubdpNCsYEYPxrzVHD8d6g==} dev: false - github.com/theopensystemslab/planx-core/c515291: - resolution: {tarball: https://codeload.github.com/theopensystemslab/planx-core/tar.gz/c515291} + github.com/theopensystemslab/planx-core/c4a725f: + resolution: {tarball: https://codeload.github.com/theopensystemslab/planx-core/tar.gz/c4a725f} name: '@opensystemslab/planx-core' version: 1.0.0 prepare: true diff --git a/e2e/tests/ui-driven/src/create-flow/create-flow.spec.ts b/e2e/tests/ui-driven/src/create-flow/create-flow.spec.ts index 0db5895bcc..d505a4a6eb 100644 --- a/e2e/tests/ui-driven/src/create-flow/create-flow.spec.ts +++ b/e2e/tests/ui-driven/src/create-flow/create-flow.spec.ts @@ -1,21 +1,4 @@ import { Browser, expect, test } from "@playwright/test"; -import { - createAddressInput, - createChecklist, - createContactInput, - createDateInput, - createDrawBoundary, - createFileUpload, - createFindProperty, - createNextSteps, - createNotice, - createNumberInput, - createPlanningConstraints, - createQuestionWithOptions, - createReview, - createTaskList, - createTextInput, -} from "../helpers/addComponent"; import type { Context } from "../helpers/context"; import { contextDefaults, @@ -23,13 +6,21 @@ import { tearDownTestContext, } from "../helpers/context"; import { getTeamPage } from "../helpers/getPage"; +import { createAuthenticatedSession } from "../helpers/globalHelpers"; import { - createAuthenticatedSession, - isGetUserRequest, -} from "../helpers/globalHelpers"; -import { answerQuestion, clickContinue } from "../helpers/userActions"; - -test.describe("Navigation", () => { + answerAddressInput, + answerChecklist, + answerContactInput, + answerDateInput, + answerFindProperty, + answerNumberInput, + answerQuestion, + answerTextInput, + clickContinue, +} from "../helpers/userActions"; +import { PlaywrightEditor } from "../pages/Editor"; + +test.describe("Flow creation, publish and preview", () => { let context: Context = { ...contextDefaults, }; @@ -52,60 +43,6 @@ test.describe("Navigation", () => { await tearDownTestContext(context); }); - test("user data persists on page refresh @regression", async ({ - browser, - }) => { - const page = await createAuthenticatedSession({ - browser, - userId: context.user!.id!, - }); - - const initialRequest = page.waitForRequest(isGetUserRequest); - - Promise.all([await page.goto("/"), await initialRequest]); - - const team = page.locator("h3", { hasText: context.team.name }); - - let isRepeatedRequestMade = false; - page.on( - "request", - (req) => (isRepeatedRequestMade = isGetUserRequest(req)), - ); - - Promise.all([ - await team.click(), - expect(isRepeatedRequestMade).toBe(false), - ]); - - const reloadRequest = page.waitForRequest(isGetUserRequest); - - Promise.all([await page.reload(), await reloadRequest]); - }); - - test("team data persists on page refresh @regression", async ({ - browser, - }) => { - const page = await createAuthenticatedSession({ - browser, - userId: context.user!.id!, - }); - - await page.goto("/"); - const team = page.locator("h3", { hasText: context.team.name }); - await team.click(); - - const teamSlugInHeader = page.getByRole("link", { - name: context.team.slug, - }); - await expect(teamSlugInHeader).toBeVisible(); - - await page.reload(); - await expect(teamSlugInHeader).toBeVisible(); - - await page.goBack(); - await expect(teamSlugInHeader).toBeHidden(); - }); - test("Create a flow", async ({ browser }) => { const page = await getTeamPage({ browser, @@ -113,105 +50,48 @@ test.describe("Navigation", () => { teamName: context.team.name, }); + const editor = new PlaywrightEditor(page); + page.on("dialog", (dialog) => dialog.accept(serviceProps.name)); - await page.locator("button", { hasText: "Add a new service" }).click(); + await editor.addNewService(); // update context to allow flow to be torn down context.flow = { ...serviceProps }; - const firstNode = page.locator("li.hanger > a").first(); - - const questionText = "Is this a test?"; - await createQuestionWithOptions(page, firstNode, questionText, [ - "Yes", - "No", - ]); - await expect( - page.locator("a").filter({ hasText: questionText }), - ).toBeVisible(); - - // Add a notice to the "Yes" path - const yesBranch = page.locator("#flow .card .options .option").nth(0); - - const yesBranchNoticeText = "Yes! this is a test"; - await createNotice( - page, - yesBranch.locator(".hanger > a"), - yesBranchNoticeText, - ); - - // Add a notice to the "No" path - const noBranch = page.locator("#flow .card .options .option").nth(1); - const noBranchNoticeText = "Sorry, this is a test"; - await createNotice( - page, - noBranch.locator(".hanger > a"), - noBranchNoticeText, - ); - - const getNextNode = () => page.locator(".hanger > a").last(); - - await createChecklist(page, getNextNode(), "A checklist title", [ + await editor.createQuestion(); + await editor.createNoticeOnEachBranch(); + await editor.createChecklist(); + await editor.createTextInput(); + await editor.createNumberInput(); + await editor.createDateInput(); + await editor.createAddressInput(); + await editor.createContactInput(); + await editor.createTaskList(); + await editor.createFindProperty(); + await editor.createDrawBoundary(); + await editor.createPlanningConstraints(); + await editor.createFileUpload(); + await editor.createNextSteps(); + await editor.createReview(); + + await expect(editor.nodeList).toContainText([ + "Is this a test?", + "Yes! this is a test", + "Sorry, this is a test", "Checklist item 1", - "Second checklist item", - "The third checklist item", - ]); - - await createTextInput(page, getNextNode(), "Tell us about your trees."); - await createNumberInput(page, getNextNode(), "How old are you?", "years"); - await createDateInput(page, getNextNode(), "When is your birthday?"); - - await createAddressInput( - page, - getNextNode(), + "Tell us about your trees.", + "How old are you?", + "When is your birthday?", "What is your address?", - "some data field", - ); - - await createContactInput( - page, - getNextNode(), "What is your contact info?", - "some data field", - ); - - await createTaskList(page, getNextNode(), "What you should do next", [ - "Have a cup of tea", - "Continue through this flow", - ]); - - await createFindProperty(page, getNextNode()); - await createDrawBoundary(page, getNextNode()); - await createPlanningConstraints(page, getNextNode()); - await createFileUpload(page, getNextNode(), "some data field"); - - await createNextSteps(page, getNextNode(), [ - "A possible next step", - "Another option", + "What you should do next", + "Find property", + "Confirm your location plan", + "Planning constraints", + "File upload", + "Next steps", + "Check your answers before sending your application", ]); - - await createReview(page, getNextNode()); - - const nodes = page.locator(".card"); - await expect(nodes.getByText(questionText)).toBeVisible(); - await expect(nodes.getByText(yesBranchNoticeText)).toBeVisible(); - await expect(nodes.getByText(noBranchNoticeText)).toBeVisible(); - await expect(nodes.getByText("Checklist item 1")).toBeVisible(); - await expect(nodes.getByText("Tell us about your trees.")).toBeVisible(); - await expect(nodes.getByText("How old are you?")).toBeVisible(); - await expect(nodes.getByText("When is your birthday?")).toBeVisible(); - await expect(nodes.getByText("What is your address?")).toBeVisible(); - await expect(nodes.getByText("What is your contact info?")).toBeVisible(); - await expect(nodes.getByText("What you should do next")).toBeVisible(); - await expect(nodes.getByText("Find property")).toBeVisible(); - await expect(nodes.getByText("Confirm your location plan")).toBeVisible(); - await expect(nodes.getByText("Planning constraints")).toBeVisible(); - await expect(nodes.getByText("File upload")).toBeVisible(); - - await expect(nodes.getByText("Next steps")).toBeVisible(); - await expect( - nodes.getByText("Check your answers before sending your application"), - ).toBeVisible(); }); test("Cannot preview an unpublished flow", async ({ @@ -321,5 +201,69 @@ test.describe("Navigation", () => { await expect( page.locator("h1", { hasText: "Sorry, this is a test" }), ).toBeVisible(); + await clickContinue({ page }); + + await answerChecklist({ + page, + title: "A checklist title", + answers: ["Checklist item 1", "Second checklist item"], + }); + await clickContinue({ page }); + + await answerTextInput(page, { + expectedQuestion: "Tell us about your trees.", + answer: "My trees are lovely", + continueToNext: true, + }); + + await answerNumberInput(page, { + expectedQuestion: "How old are you?", + answer: 30, + continueToNext: true, + }); + + await answerDateInput(page, { + expectedQuestion: "When is your birthday?", + day: 30, + month: 12, + year: 1980, + continueToNext: true, + }); + + await answerAddressInput(page, { + expectedQuestion: "What is your address?", + addressLineOne: "1 Silver Street", + town: "Bamburgh", + postcode: "BG1 2SS", + continueToNext: true, + }); + + await expect( + page.locator("h1", { hasText: "What is your contact info?" }), + ).toBeVisible(); + await answerContactInput(page, { + firstName: "Freddie", + lastName: "Mercury", + phoneNumber: "01234 555555", + email: "freddie@queen.com", + }); + await clickContinue({ page }); + + await expect( + page.locator("h1", { hasText: "What you should do next" }), + ).toBeVisible(); + await expect( + page.locator("h2", { hasText: "Have a cup of tea" }), + ).toBeVisible(); + await expect( + page.locator("h2", { hasText: "Continue through this flow" }), + ).toBeVisible(); + await clickContinue({ page }); + + await expect( + page.locator("h1", { hasText: "Find the property" }), + ).toBeVisible(); + await answerFindProperty(page); + await clickContinue({ page }); }); }); diff --git a/e2e/tests/ui-driven/src/helpers/userActions.ts b/e2e/tests/ui-driven/src/helpers/userActions.ts index bb2a048125..1f488ec73d 100644 --- a/e2e/tests/ui-driven/src/helpers/userActions.ts +++ b/e2e/tests/ui-driven/src/helpers/userActions.ts @@ -1,6 +1,6 @@ import type { Locator, Page } from "@playwright/test"; import { expect } from "@playwright/test"; -import { mockOSPlacesResponse } from "../mocks/osPlacesResponse"; +import { setupOSMockResponse } from "../mocks/osPlacesResponse"; import type { Context } from "./context"; import { findSessionId, getGraphQLClient } from "./context"; import { TEST_EMAIL, log, waitForDebugLog } from "./globalHelpers"; @@ -121,7 +121,7 @@ export async function answerChecklist({ title: string; answers: string[]; }) { - const checklist = await page.getByRole("heading").filter({ + const checklist = page.getByRole("heading").filter({ hasText: title, }); await expect(checklist).toBeVisible(); @@ -201,18 +201,6 @@ export async function answerFindProperty(page: Page) { await page.getByRole("option").first().click(); } -async function setupOSMockResponse(page: Page) { - const ordnanceSurveryPlacesEndpoint = new RegExp( - /proxy\/ordnance-survey\/search\/places\/v1\/postcode\/*/, - ); - await page.route(ordnanceSurveryPlacesEndpoint, async (route) => { - await route.fulfill({ - status: 200, - body: JSON.stringify(mockOSPlacesResponse), - }); - }); -} - export async function answerContactInput( page: Page, { @@ -232,3 +220,96 @@ export async function answerContactInput( await page.getByLabel("Phone number").fill(phoneNumber); await page.getByLabel("Email address").fill(email); } + +export async function answerTextInput( + page: Page, + { + expectedQuestion, + answer, + continueToNext, + }: { + expectedQuestion: string; + answer: string; + continueToNext: boolean; + }, +) { + await expect(page.locator("p", { hasText: expectedQuestion })).toBeVisible(); + await page.locator("label div input[type='text']").fill(answer); + if (continueToNext) { + await clickContinue({ page }); + } +} + +export async function answerNumberInput( + page: Page, + { + expectedQuestion, + answer, + continueToNext, + }: { + expectedQuestion: string; + answer: number; + continueToNext: boolean; + }, +) { + await expect(page.locator("p", { hasText: expectedQuestion })).toBeVisible(); + await page.locator("label div input[type='number']").fill(answer.toString()); + if (continueToNext) { + await clickContinue({ page }); + } +} + +export async function answerDateInput( + page: Page, + { + expectedQuestion, + + day, + month, + year, + continueToNext, + }: { + expectedQuestion: string; + + day: number; + month: number; + year: number; + continueToNext: boolean; + }, +) { + await expect(page.locator("h1", { hasText: expectedQuestion })).toBeVisible(); + await page.getByLabel("Day").fill(day.toString()); + await page.getByLabel("Month").fill(month.toString()); + await page.getByLabel("Year").fill(year.toString()); + + if (continueToNext) { + await clickContinue({ page }); + } +} + +export async function answerAddressInput( + page: Page, + { + expectedQuestion, + + addressLineOne, + town, + postcode, + continueToNext, + }: { + expectedQuestion: string; + + addressLineOne: string; + town: string; + postcode: string; + continueToNext: boolean; + }, +) { + await expect(page.locator("h1", { hasText: expectedQuestion })).toBeVisible(); + await page.getByLabel("Address line 1").fill(addressLineOne); + await page.getByLabel("Town").fill(town); + await page.getByLabel("Postcode").fill(postcode); + if (continueToNext) { + await clickContinue({ page }); + } +} diff --git a/e2e/tests/ui-driven/src/mocks/osPlacesResponse.ts b/e2e/tests/ui-driven/src/mocks/osPlacesResponse.ts index 6b6baf7d4b..5f4547be28 100644 --- a/e2e/tests/ui-driven/src/mocks/osPlacesResponse.ts +++ b/e2e/tests/ui-driven/src/mocks/osPlacesResponse.ts @@ -1,3 +1,5 @@ +import { Page } from "@playwright/test"; + export const mockOSPlacesResponse = { header: { uri: "https://api.os.uk/search/places/v1/postcode?postcode=SW1%201AA&dataset=LPI&maxResults=100&output_srs=EPSG%3A4326&lr=EN&offset=0", @@ -58,3 +60,14 @@ export const mockOSPlacesResponse = { }, ], }; +export async function setupOSMockResponse(page: Page) { + const ordnanceSurveryPlacesEndpoint = new RegExp( + /proxy\/ordnance-survey\/search\/places\/v1\/postcode\/*/, + ); + await page.route(ordnanceSurveryPlacesEndpoint, async (route) => { + await route.fulfill({ + status: 200, + body: JSON.stringify(mockOSPlacesResponse), + }); + }); +} diff --git a/e2e/tests/ui-driven/src/pages/Editor.ts b/e2e/tests/ui-driven/src/pages/Editor.ts new file mode 100644 index 0000000000..accbd03265 --- /dev/null +++ b/e2e/tests/ui-driven/src/pages/Editor.ts @@ -0,0 +1,174 @@ +import { expect, type Locator, type Page } from "@playwright/test"; +import { + createAddressInput, + createChecklist, + createContactInput, + createDateInput, + createDrawBoundary, + createFileUpload, + createFindProperty, + createNextSteps, + createNotice, + createNumberInput, + createPlanningConstraints, + createQuestionWithOptions, + createReview, + createTaskList, + createTextInput, +} from "../helpers/addComponent"; + +export class PlaywrightEditor { + readonly page: Page; + readonly addNewServiceButton: Locator; + readonly firstNode: Locator; + readonly yesBranch: Locator; + readonly noBranch: Locator; + readonly nodeList: Locator; + readonly answers: { + questionText: string; + yesBranchNoticeText: string; + noBranchNoticeText: string; + }; + + constructor(page: Page) { + this.page = page; + this.addNewServiceButton = page.locator("button", { + hasText: "Add a new service", + }); + this.firstNode = page.locator("li.hanger > a").first(); + this.yesBranch = page.locator("#flow .card .options .option").nth(0); + this.noBranch = page.locator("#flow .card .options .option").nth(1); + this.nodeList = page.locator(".card"); + this.answers = { + questionText: "Is this a test?", + yesBranchNoticeText: "Yes! this is a test", + noBranchNoticeText: "Sorry, this is a test", + }; + } + + async addNewService() { + await this.addNewServiceButton.click(); + } + + async createQuestion() { + await createQuestionWithOptions( + this.page, + this.firstNode, + this.answers.questionText, + ["Yes", "No"], + ); + await expect( + this.page.locator("a").filter({ hasText: this.answers.questionText }), + ).toBeVisible(); + } + + async createNoticeOnEachBranch() { + // Add a notice to the "Yes" path + await createNotice( + this.page, + this.yesBranch.locator(".hanger > a"), + this.answers.yesBranchNoticeText, + ); + // Add a notice to the "No" path + await createNotice( + this.page, + this.noBranch.locator(".hanger > a"), + this.answers.noBranchNoticeText, + ); + + await expect( + this.page.locator("a").filter({ hasText: this.answers.questionText }), + ).toBeVisible(); + } + + getNextNode() { + return this.page.locator(".hanger > a").last(); + } + + async createChecklist() { + await createChecklist(this.page, this.getNextNode(), "A checklist title", [ + "Checklist item 1", + "Second checklist item", + "The third checklist item", + ]); + } + + async createTextInput() { + await createTextInput( + this.page, + this.getNextNode(), + "Tell us about your trees.", + ); + } + + async createNumberInput() { + await createNumberInput( + this.page, + this.getNextNode(), + "How old are you?", + "years", + ); + } + + async createDateInput() { + await createDateInput( + this.page, + this.getNextNode(), + "When is your birthday?", + ); + } + + async createAddressInput() { + await createAddressInput( + this.page, + this.getNextNode(), + "What is your address?", + "some data field", + ); + } + + async createContactInput() { + await createContactInput( + this.page, + this.getNextNode(), + "What is your contact info?", + "some data field", + ); + } + + async createTaskList() { + await createTaskList( + this.page, + this.getNextNode(), + "What you should do next", + ["Have a cup of tea", "Continue through this flow"], + ); + } + + async createFindProperty() { + await createFindProperty(this.page, this.getNextNode()); + } + + async createDrawBoundary() { + await createDrawBoundary(this.page, this.getNextNode()); + } + + async createPlanningConstraints() { + await createPlanningConstraints(this.page, this.getNextNode()); + } + + async createFileUpload() { + await createFileUpload(this.page, this.getNextNode(), "some data field"); + } + + async createNextSteps() { + await createNextSteps(this.page, this.getNextNode(), [ + "A possible next step", + "Another option", + ]); + } + + async createReview() { + await createReview(this.page, this.getNextNode()); + } +} diff --git a/e2e/tests/ui-driven/src/refresh-page.spec.ts b/e2e/tests/ui-driven/src/refresh-page.spec.ts new file mode 100644 index 0000000000..309d09c0f4 --- /dev/null +++ b/e2e/tests/ui-driven/src/refresh-page.spec.ts @@ -0,0 +1,85 @@ +import { expect, test } from "@playwright/test"; +import type { Context } from "./helpers/context"; +import { + contextDefaults, + setUpTestContext, + tearDownTestContext, +} from "./helpers/context"; +import { + createAuthenticatedSession, + isGetUserRequest, +} from "./helpers/globalHelpers"; + +test.describe("Refresh page", () => { + let context: Context = { + ...contextDefaults, + }; + + test.beforeAll(async () => { + try { + context = await setUpTestContext(context); + } catch (error) { + // ensure proper teardown if setup fails + await tearDownTestContext(context); + throw error; + } + }); + + test.afterAll(async () => { + await tearDownTestContext(context); + }); + + test("user data persists on page refresh @regression", async ({ + browser, + }) => { + const page = await createAuthenticatedSession({ + browser, + userId: context.user!.id!, + }); + + const initialRequest = page.waitForRequest(isGetUserRequest); + + Promise.all([await page.goto("/"), await initialRequest]); + + const team = page.locator("h3", { hasText: context.team.name }); + + let isRepeatedRequestMade = false; + page.on( + "request", + (req) => (isRepeatedRequestMade = isGetUserRequest(req)), + ); + + Promise.all([ + await team.click(), + expect(isRepeatedRequestMade).toBe(false), + ]); + + const reloadRequest = page.waitForRequest(isGetUserRequest); + + Promise.all([await page.reload(), await reloadRequest]); + }); + + test("team data persists on page refresh @regression", async ({ + browser, + }) => { + const page = await createAuthenticatedSession({ + browser, + userId: context.user!.id!, + }); + + await page.goto("/"); + const team = page.locator("h3", { hasText: context.team.name }); + await team.click(); + + const teamSlugInHeader = page.getByRole("link", { + name: context.team.slug, + }); + await expect(teamSlugInHeader).toBeVisible(); + + await page.reload(); + await expect(teamSlugInHeader).toBeVisible(); + + await page.goBack(); + await expect(teamSlugInHeader).toBeHidden(); + }); +}); diff --git a/editor.planx.uk/package.json b/editor.planx.uk/package.json index b16fa0b45f..e95cb51e7b 100644 --- a/editor.planx.uk/package.json +++ b/editor.planx.uk/package.json @@ -15,7 +15,7 @@ "@mui/material": "^5.15.10", "@mui/utils": "^5.15.11", "@opensystemslab/map": "1.0.0-alpha.3", - "@opensystemslab/planx-core": "git+https://github.com/theopensystemslab/planx-core#c515291", + "@opensystemslab/planx-core": "git+https://github.com/theopensystemslab/planx-core#c4a725f", "@tiptap/core": "^2.4.0", "@tiptap/extension-bold": "^2.0.3", "@tiptap/extension-bubble-menu": "^2.1.13", @@ -89,7 +89,7 @@ "swr": "^2.2.4", "tippy.js": "^6.3.7", "uuid": "^9.0.1", - "vite": "^5.2.11", + "vite": "^5.4.6", "vite-jest": "^0.1.4", "vite-plugin-svgr": "^4.2.0", "wkt": "^0.1.1", diff --git a/editor.planx.uk/pnpm-lock.yaml b/editor.planx.uk/pnpm-lock.yaml index ebf5fc1d36..38b2375cf1 100644 --- a/editor.planx.uk/pnpm-lock.yaml +++ b/editor.planx.uk/pnpm-lock.yaml @@ -47,8 +47,8 @@ dependencies: specifier: 1.0.0-alpha.3 version: 1.0.0-alpha.3 '@opensystemslab/planx-core': - specifier: git+https://github.com/theopensystemslab/planx-core#c515291 - version: github.com/theopensystemslab/planx-core/c515291(@types/react@18.2.45) + specifier: git+https://github.com/theopensystemslab/planx-core#c4a725f + version: github.com/theopensystemslab/planx-core/c4a725f(@types/react@18.2.45) '@tiptap/core': specifier: ^2.4.0 version: 2.4.0(@tiptap/pm@2.0.3) @@ -129,7 +129,7 @@ dependencies: version: 7.0.0 '@vitejs/plugin-react-swc': specifier: ^3.7.0 - version: 3.7.0(vite@5.2.11) + version: 3.7.0(vite@5.4.6) array-move: specifier: ^4.0.0 version: 4.0.0 @@ -269,14 +269,14 @@ dependencies: specifier: ^9.0.1 version: 9.0.1 vite: - specifier: ^5.2.11 - version: 5.2.11(@types/node@17.0.45)(sass@1.71.1) + specifier: ^5.4.6 + version: 5.4.6(@types/node@17.0.45)(sass@1.71.1) vite-jest: specifier: ^0.1.4 - version: 0.1.4(jest@27.5.1)(vite@5.2.11) + version: 0.1.4(jest@27.5.1)(vite@5.4.6) vite-plugin-svgr: specifier: ^4.2.0 - version: 4.2.0(rollup@2.79.1)(typescript@5.6.2)(vite@5.2.11) + version: 4.2.0(rollup@2.79.1)(typescript@5.6.2)(vite@5.4.6) wkt: specifier: ^0.1.1 version: 0.1.1 @@ -341,7 +341,7 @@ devDependencies: version: 8.3.1(@storybook/test@8.3.1)(react-dom@18.2.0)(react@18.2.0)(storybook@8.3.1)(typescript@5.6.2) '@storybook/react-vite': specifier: ^8.3.1 - version: 8.3.1(@storybook/test@8.3.1)(react-dom@18.2.0)(react@18.2.0)(rollup@2.79.1)(storybook@8.3.1)(typescript@5.6.2)(vite@5.2.11) + version: 8.3.1(@storybook/test@8.3.1)(react-dom@18.2.0)(react@18.2.0)(rollup@2.79.1)(storybook@8.3.1)(typescript@5.6.2)(vite@5.4.6) '@storybook/test': specifier: ^8.3.1 version: 8.3.1(storybook@8.3.1) @@ -488,7 +488,7 @@ devDependencies: version: 5.6.2 vite-tsconfig-paths: specifier: ^4.3.2 - version: 4.3.2(typescript@5.6.2)(vite@5.2.11) + version: 4.3.2(typescript@5.6.2)(vite@5.4.6) vitest: specifier: ^1.6.0 version: 1.6.0(@types/node@17.0.45)(sass@1.71.1) @@ -3959,29 +3959,12 @@ packages: resolution: {integrity: sha512-snKqtPW01tN0ui7yu9rGv69aJXr/a/Ywvl11sUjNtEcRc+ng/mQriFL0wLXMef74iHa/EkftbDzU9F8iFbH+zg==} dev: false - /@esbuild/aix-ppc64@0.20.2: - resolution: {integrity: sha512-D+EBOJHXdNZcLJRBkhENNG8Wji2kgc9AZ9KiPr1JuZjsNtyHzrsfLRrY0tk2H2aoFu6RANO1y1iPPUCDYWkb5g==} - engines: {node: '>=12'} - cpu: [ppc64] - os: [aix] - requiresBuild: true - optional: true - /@esbuild/aix-ppc64@0.21.3: resolution: {integrity: sha512-yTgnwQpFVYfvvo4SvRFB0SwrW8YjOxEoT7wfMT7Ol5v7v5LDNvSGo67aExmxOb87nQNeWPVvaGBNfQ7BXcrZ9w==} engines: {node: '>=12'} cpu: [ppc64] os: [aix] requiresBuild: true - dev: true - optional: true - - /@esbuild/android-arm64@0.20.2: - resolution: {integrity: sha512-mRzjLacRtl/tWU0SvD8lUEwb61yP9cqQo6noDZP/O8VkwafSYwZ4yWy24kan8jE/IMERpYncRt2dw438LP3Xmg==} - engines: {node: '>=12'} - cpu: [arm64] - os: [android] - requiresBuild: true optional: true /@esbuild/android-arm64@0.21.3: @@ -3990,15 +3973,6 @@ packages: cpu: [arm64] os: [android] requiresBuild: true - dev: true - optional: true - - /@esbuild/android-arm@0.20.2: - resolution: {integrity: sha512-t98Ra6pw2VaDhqNWO2Oph2LXbz/EJcnLmKLGBJwEwXX/JAN83Fym1rU8l0JUWK6HkIbWONCSSatf4sf2NBRx/w==} - engines: {node: '>=12'} - cpu: [arm] - os: [android] - requiresBuild: true optional: true /@esbuild/android-arm@0.21.3: @@ -4007,15 +3981,6 @@ packages: cpu: [arm] os: [android] requiresBuild: true - dev: true - optional: true - - /@esbuild/android-x64@0.20.2: - resolution: {integrity: sha512-btzExgV+/lMGDDa194CcUQm53ncxzeBrWJcncOBxuC6ndBkKxnHdFJn86mCIgTELsooUmwUm9FkhSp5HYu00Rg==} - engines: {node: '>=12'} - cpu: [x64] - os: [android] - requiresBuild: true optional: true /@esbuild/android-x64@0.21.3: @@ -4024,15 +3989,6 @@ packages: cpu: [x64] os: [android] requiresBuild: true - dev: true - optional: true - - /@esbuild/darwin-arm64@0.20.2: - resolution: {integrity: sha512-4J6IRT+10J3aJH3l1yzEg9y3wkTDgDk7TSDFX+wKFiWjqWp/iCfLIYzGyasx9l0SAFPT1HwSCR+0w/h1ES/MjA==} - engines: {node: '>=12'} - cpu: [arm64] - os: [darwin] - requiresBuild: true optional: true /@esbuild/darwin-arm64@0.21.3: @@ -4041,15 +3997,6 @@ packages: cpu: [arm64] os: [darwin] requiresBuild: true - dev: true - optional: true - - /@esbuild/darwin-x64@0.20.2: - resolution: {integrity: sha512-tBcXp9KNphnNH0dfhv8KYkZhjc+H3XBkF5DKtswJblV7KlT9EI2+jeA8DgBjp908WEuYll6pF+UStUCfEpdysA==} - engines: {node: '>=12'} - cpu: [x64] - os: [darwin] - requiresBuild: true optional: true /@esbuild/darwin-x64@0.21.3: @@ -4058,15 +4005,6 @@ packages: cpu: [x64] os: [darwin] requiresBuild: true - dev: true - optional: true - - /@esbuild/freebsd-arm64@0.20.2: - resolution: {integrity: sha512-d3qI41G4SuLiCGCFGUrKsSeTXyWG6yem1KcGZVS+3FYlYhtNoNgYrWcvkOoaqMhwXSMrZRl69ArHsGJ9mYdbbw==} - engines: {node: '>=12'} - cpu: [arm64] - os: [freebsd] - requiresBuild: true optional: true /@esbuild/freebsd-arm64@0.21.3: @@ -4075,15 +4013,6 @@ packages: cpu: [arm64] os: [freebsd] requiresBuild: true - dev: true - optional: true - - /@esbuild/freebsd-x64@0.20.2: - resolution: {integrity: sha512-d+DipyvHRuqEeM5zDivKV1KuXn9WeRX6vqSqIDgwIfPQtwMP4jaDsQsDncjTDDsExT4lR/91OLjRo8bmC1e+Cw==} - engines: {node: '>=12'} - cpu: [x64] - os: [freebsd] - requiresBuild: true optional: true /@esbuild/freebsd-x64@0.21.3: @@ -4092,15 +4021,6 @@ packages: cpu: [x64] os: [freebsd] requiresBuild: true - dev: true - optional: true - - /@esbuild/linux-arm64@0.20.2: - resolution: {integrity: sha512-9pb6rBjGvTFNira2FLIWqDk/uaf42sSyLE8j1rnUpuzsODBq7FvpwHYZxQ/It/8b+QOS1RYfqgGFNLRI+qlq2A==} - engines: {node: '>=12'} - cpu: [arm64] - os: [linux] - requiresBuild: true optional: true /@esbuild/linux-arm64@0.21.3: @@ -4109,15 +4029,6 @@ packages: cpu: [arm64] os: [linux] requiresBuild: true - dev: true - optional: true - - /@esbuild/linux-arm@0.20.2: - resolution: {integrity: sha512-VhLPeR8HTMPccbuWWcEUD1Az68TqaTYyj6nfE4QByZIQEQVWBB8vup8PpR7y1QHL3CpcF6xd5WVBU/+SBEvGTg==} - engines: {node: '>=12'} - cpu: [arm] - os: [linux] - requiresBuild: true optional: true /@esbuild/linux-arm@0.21.3: @@ -4126,15 +4037,6 @@ packages: cpu: [arm] os: [linux] requiresBuild: true - dev: true - optional: true - - /@esbuild/linux-ia32@0.20.2: - resolution: {integrity: sha512-o10utieEkNPFDZFQm9CoP7Tvb33UutoJqg3qKf1PWVeeJhJw0Q347PxMvBgVVFgouYLGIhFYG0UGdBumROyiig==} - engines: {node: '>=12'} - cpu: [ia32] - os: [linux] - requiresBuild: true optional: true /@esbuild/linux-ia32@0.21.3: @@ -4143,15 +4045,6 @@ packages: cpu: [ia32] os: [linux] requiresBuild: true - dev: true - optional: true - - /@esbuild/linux-loong64@0.20.2: - resolution: {integrity: sha512-PR7sp6R/UC4CFVomVINKJ80pMFlfDfMQMYynX7t1tNTeivQ6XdX5r2XovMmha/VjR1YN/HgHWsVcTRIMkymrgQ==} - engines: {node: '>=12'} - cpu: [loong64] - os: [linux] - requiresBuild: true optional: true /@esbuild/linux-loong64@0.21.3: @@ -4160,15 +4053,6 @@ packages: cpu: [loong64] os: [linux] requiresBuild: true - dev: true - optional: true - - /@esbuild/linux-mips64el@0.20.2: - resolution: {integrity: sha512-4BlTqeutE/KnOiTG5Y6Sb/Hw6hsBOZapOVF6njAESHInhlQAghVVZL1ZpIctBOoTFbQyGW+LsVYZ8lSSB3wkjA==} - engines: {node: '>=12'} - cpu: [mips64el] - os: [linux] - requiresBuild: true optional: true /@esbuild/linux-mips64el@0.21.3: @@ -4177,15 +4061,6 @@ packages: cpu: [mips64el] os: [linux] requiresBuild: true - dev: true - optional: true - - /@esbuild/linux-ppc64@0.20.2: - resolution: {integrity: sha512-rD3KsaDprDcfajSKdn25ooz5J5/fWBylaaXkuotBDGnMnDP1Uv5DLAN/45qfnf3JDYyJv/ytGHQaziHUdyzaAg==} - engines: {node: '>=12'} - cpu: [ppc64] - os: [linux] - requiresBuild: true optional: true /@esbuild/linux-ppc64@0.21.3: @@ -4194,15 +4069,6 @@ packages: cpu: [ppc64] os: [linux] requiresBuild: true - dev: true - optional: true - - /@esbuild/linux-riscv64@0.20.2: - resolution: {integrity: sha512-snwmBKacKmwTMmhLlz/3aH1Q9T8v45bKYGE3j26TsaOVtjIag4wLfWSiZykXzXuE1kbCE+zJRmwp+ZbIHinnVg==} - engines: {node: '>=12'} - cpu: [riscv64] - os: [linux] - requiresBuild: true optional: true /@esbuild/linux-riscv64@0.21.3: @@ -4211,15 +4077,6 @@ packages: cpu: [riscv64] os: [linux] requiresBuild: true - dev: true - optional: true - - /@esbuild/linux-s390x@0.20.2: - resolution: {integrity: sha512-wcWISOobRWNm3cezm5HOZcYz1sKoHLd8VL1dl309DiixxVFoFe/o8HnwuIwn6sXre88Nwj+VwZUvJf4AFxkyrQ==} - engines: {node: '>=12'} - cpu: [s390x] - os: [linux] - requiresBuild: true optional: true /@esbuild/linux-s390x@0.21.3: @@ -4228,15 +4085,6 @@ packages: cpu: [s390x] os: [linux] requiresBuild: true - dev: true - optional: true - - /@esbuild/linux-x64@0.20.2: - resolution: {integrity: sha512-1MdwI6OOTsfQfek8sLwgyjOXAu+wKhLEoaOLTjbijk6E2WONYpH9ZU2mNtR+lZ2B4uwr+usqGuVfFT9tMtGvGw==} - engines: {node: '>=12'} - cpu: [x64] - os: [linux] - requiresBuild: true optional: true /@esbuild/linux-x64@0.21.3: @@ -4245,15 +4093,6 @@ packages: cpu: [x64] os: [linux] requiresBuild: true - dev: true - optional: true - - /@esbuild/netbsd-x64@0.20.2: - resolution: {integrity: sha512-K8/DhBxcVQkzYc43yJXDSyjlFeHQJBiowJ0uVL6Tor3jGQfSGHNNJcWxNbOI8v5k82prYqzPuwkzHt3J1T1iZQ==} - engines: {node: '>=12'} - cpu: [x64] - os: [netbsd] - requiresBuild: true optional: true /@esbuild/netbsd-x64@0.21.3: @@ -4262,15 +4101,6 @@ packages: cpu: [x64] os: [netbsd] requiresBuild: true - dev: true - optional: true - - /@esbuild/openbsd-x64@0.20.2: - resolution: {integrity: sha512-eMpKlV0SThJmmJgiVyN9jTPJ2VBPquf6Kt/nAoo6DgHAoN57K15ZghiHaMvqjCye/uU4X5u3YSMgVBI1h3vKrQ==} - engines: {node: '>=12'} - cpu: [x64] - os: [openbsd] - requiresBuild: true optional: true /@esbuild/openbsd-x64@0.21.3: @@ -4279,15 +4109,6 @@ packages: cpu: [x64] os: [openbsd] requiresBuild: true - dev: true - optional: true - - /@esbuild/sunos-x64@0.20.2: - resolution: {integrity: sha512-2UyFtRC6cXLyejf/YEld4Hajo7UHILetzE1vsRcGL3earZEW77JxrFjH4Ez2qaTiEfMgAXxfAZCm1fvM/G/o8w==} - engines: {node: '>=12'} - cpu: [x64] - os: [sunos] - requiresBuild: true optional: true /@esbuild/sunos-x64@0.21.3: @@ -4296,15 +4117,6 @@ packages: cpu: [x64] os: [sunos] requiresBuild: true - dev: true - optional: true - - /@esbuild/win32-arm64@0.20.2: - resolution: {integrity: sha512-GRibxoawM9ZCnDxnP3usoUDO9vUkpAxIIZ6GQI+IlVmr5kP3zUq+l17xELTHMWTWzjxa2guPNyrpq1GWmPvcGQ==} - engines: {node: '>=12'} - cpu: [arm64] - os: [win32] - requiresBuild: true optional: true /@esbuild/win32-arm64@0.21.3: @@ -4313,15 +4125,6 @@ packages: cpu: [arm64] os: [win32] requiresBuild: true - dev: true - optional: true - - /@esbuild/win32-ia32@0.20.2: - resolution: {integrity: sha512-HfLOfn9YWmkSKRQqovpnITazdtquEW8/SoHW7pWpuEeguaZI4QnCRW6b+oZTztdBnZOS2hqJ6im/D5cPzBTTlQ==} - engines: {node: '>=12'} - cpu: [ia32] - os: [win32] - requiresBuild: true optional: true /@esbuild/win32-ia32@0.21.3: @@ -4330,15 +4133,6 @@ packages: cpu: [ia32] os: [win32] requiresBuild: true - dev: true - optional: true - - /@esbuild/win32-x64@0.20.2: - resolution: {integrity: sha512-N49X4lJX27+l9jbLKSqZ6bKNjzQvHaT8IIFUy+YIqmXQdjYCToGWwOItDrfby14c78aDd5NHQl29xingXfCdLQ==} - engines: {node: '>=12'} - cpu: [x64] - os: [win32] - requiresBuild: true optional: true /@esbuild/win32-x64@0.21.3: @@ -4347,7 +4141,6 @@ packages: cpu: [x64] os: [win32] requiresBuild: true - dev: true optional: true /@eslint-community/eslint-utils@4.4.0(eslint@8.44.0): @@ -4855,7 +4648,7 @@ packages: chalk: 4.1.2 dev: true - /@joshwooding/vite-plugin-react-docgen-typescript@0.3.1(typescript@5.6.2)(vite@5.2.11): + /@joshwooding/vite-plugin-react-docgen-typescript@0.3.1(typescript@5.6.2)(vite@5.4.6): resolution: {integrity: sha512-pdoMZ9QaPnVlSM+SdU/wgg0nyD/8wQ7y90ttO2CMCyrrm7RxveYIJ5eNfjPaoMFqW41LZra7QO9j+xV4Y18Glw==} peerDependencies: typescript: '>= 4.3.x' @@ -4869,7 +4662,7 @@ packages: magic-string: 0.27.0 react-docgen-typescript: 2.2.2(typescript@5.6.2) typescript: 5.6.2 - vite: 5.2.11(@types/node@17.0.45)(sass@1.71.1) + vite: 5.4.6(@types/node@17.0.45)(sass@1.71.1) dev: true /@jridgewell/gen-mapping@0.3.5: @@ -6250,7 +6043,7 @@ packages: util-deprecate: 1.0.2 dev: true - /@storybook/builder-vite@8.3.1(storybook@8.3.1)(typescript@5.6.2)(vite@5.2.11): + /@storybook/builder-vite@8.3.1(storybook@8.3.1)(typescript@5.6.2)(vite@5.4.6): resolution: {integrity: sha512-IxfgIuQo9R+zcwoBE85PkCSKWGbPVStJgm1VHO/mixIdZExanbAhDS+L21nAZCelTvcsObTN76BN953v2LjVGg==} peerDependencies: '@preact/preset-vite': '*' @@ -6277,7 +6070,7 @@ packages: storybook: 8.3.1 ts-dedent: 2.2.0 typescript: 5.6.2 - vite: 5.2.11(@types/node@17.0.45)(sass@1.71.1) + vite: 5.4.6(@types/node@17.0.45)(sass@1.71.1) transitivePeerDependencies: - supports-color - webpack-sources @@ -6294,7 +6087,7 @@ packages: /@storybook/client-logger@6.5.16: resolution: {integrity: sha512-pxcNaCj3ItDdicPTXTtmYJE3YC1SjxFrBmHcyrN+nffeNyiMuViJdOOZzzzucTUG0wcOOX8jaSyak+nnHg5H1Q==} dependencies: - core-js: 3.31.0 + core-js: 3.38.1 global: 4.4.0 dev: true @@ -6464,7 +6257,7 @@ packages: storybook: 8.3.1 dev: true - /@storybook/react-vite@8.3.1(@storybook/test@8.3.1)(react-dom@18.2.0)(react@18.2.0)(rollup@2.79.1)(storybook@8.3.1)(typescript@5.6.2)(vite@5.2.11): + /@storybook/react-vite@8.3.1(@storybook/test@8.3.1)(react-dom@18.2.0)(react@18.2.0)(rollup@2.79.1)(storybook@8.3.1)(typescript@5.6.2)(vite@5.4.6): resolution: {integrity: sha512-WjLnYzaiLHCv09UnnMfjJL9RnjmReXbPpRs3VklH87UH8L6j4WLHw7JAEItnyS6ugTxFjcpEg1P1ud4D8c75nA==} engines: {node: '>=18.0.0'} peerDependencies: @@ -6473,9 +6266,9 @@ packages: storybook: ^8.3.1 vite: ^4.0.0 || ^5.0.0 dependencies: - '@joshwooding/vite-plugin-react-docgen-typescript': 0.3.1(typescript@5.6.2)(vite@5.2.11) + '@joshwooding/vite-plugin-react-docgen-typescript': 0.3.1(typescript@5.6.2)(vite@5.4.6) '@rollup/pluginutils': 5.1.0(rollup@2.79.1) - '@storybook/builder-vite': 8.3.1(storybook@8.3.1)(typescript@5.6.2)(vite@5.2.11) + '@storybook/builder-vite': 8.3.1(storybook@8.3.1)(typescript@5.6.2)(vite@5.4.6) '@storybook/react': 8.3.1(@storybook/test@8.3.1)(react-dom@18.2.0)(react@18.2.0)(storybook@8.3.1)(typescript@5.6.2) find-up: 5.0.0 magic-string: 0.30.11 @@ -6485,7 +6278,7 @@ packages: resolve: 1.22.8 storybook: 8.3.1 tsconfig-paths: 4.2.0 - vite: 5.2.11(@types/node@17.0.45)(sass@1.71.1) + vite: 5.4.6(@types/node@17.0.45)(sass@1.71.1) transitivePeerDependencies: - '@preact/preset-vite' - '@storybook/test' @@ -8156,13 +7949,13 @@ packages: - encoding dev: true - /@vitejs/plugin-react-swc@3.7.0(vite@5.2.11): + /@vitejs/plugin-react-swc@3.7.0(vite@5.4.6): resolution: {integrity: sha512-yrknSb3Dci6svCd/qhHqhFPDSw0QtjumcqdKMoNNzmOl5lMXTTiqzjWtG4Qask2HdvvzaNgSunbQGet8/GrKdA==} peerDependencies: vite: ^4 || ^5 dependencies: '@swc/core': 1.7.26 - vite: 5.2.11(@types/node@17.0.45)(sass@1.71.1) + vite: 5.4.6(@types/node@17.0.45)(sass@1.71.1) transitivePeerDependencies: - '@swc/helpers' dev: false @@ -9247,7 +9040,7 @@ packages: dev: true /batch@0.6.1: - resolution: {integrity: sha512-x+VAiMRL6UPkx+kudNvxTl6hB2XNNCG2r+7wixVfIYwu/2HKRXimwQyaumLjMveWvT2Hkd/cAJw+QBMfJ/EKVw==} + resolution: {integrity: sha1-3DQxT05nkxgJP8dgJyUl+UvyXBY=} dev: true /better-opn@3.0.2: @@ -9379,7 +9172,7 @@ packages: dev: true /bytes@3.0.0: - resolution: {integrity: sha512-pMhOfFDPiv9t5jjIXkHosWmkSyQbvsgEVNkz0ERHbuLh2T/7j4Mqqpz523Fe8MVY89KC6Sh/QfS2sM+SjgFDcw==} + resolution: {integrity: sha1-0ygVQE1olpn4Wk6k+odV3ROpYEg=} engines: {node: '>= 0.8'} dev: true @@ -9476,7 +9269,7 @@ packages: dependencies: '@babel/runtime': 7.25.6 '@types/raf': 3.4.3 - core-js: 3.31.0 + core-js: 3.38.1 raf: 3.4.1 regenerator-runtime: 0.13.11 rgbcolor: 1.0.1 @@ -9992,7 +9785,6 @@ packages: /core-js@3.38.1: resolution: {integrity: sha512-OP35aUorbU3Zvlx7pjsFdu1rGNnD4pgw/CWoYzRY3t2EzoVT7shKHY1dlAy3f41cGIO7ZDPQimhGFTlEYkG/Hw==} requiresBuild: true - dev: true /core-util-is@1.0.3: resolution: {integrity: sha512-ZQBvi1DcpJ4GDqanjucZ2Hj3wEO5pZDS89BWbkcrvdxksJorwUDDZamX9ldFkp9aw2lmBDLgkObEA4DWNJ9FYQ==} @@ -11118,36 +10910,6 @@ packages: - supports-color dev: true - /esbuild@0.20.2: - resolution: {integrity: sha512-WdOOppmUNU+IbZ0PaDiTst80zjnrOkyJNHoKupIcVyU8Lvla3Ugx94VzkQ32Ijqd7UhHJy75gNWDMUekcrSJ6g==} - engines: {node: '>=12'} - hasBin: true - requiresBuild: true - optionalDependencies: - '@esbuild/aix-ppc64': 0.20.2 - '@esbuild/android-arm': 0.20.2 - '@esbuild/android-arm64': 0.20.2 - '@esbuild/android-x64': 0.20.2 - '@esbuild/darwin-arm64': 0.20.2 - '@esbuild/darwin-x64': 0.20.2 - '@esbuild/freebsd-arm64': 0.20.2 - '@esbuild/freebsd-x64': 0.20.2 - '@esbuild/linux-arm': 0.20.2 - '@esbuild/linux-arm64': 0.20.2 - '@esbuild/linux-ia32': 0.20.2 - '@esbuild/linux-loong64': 0.20.2 - '@esbuild/linux-mips64el': 0.20.2 - '@esbuild/linux-ppc64': 0.20.2 - '@esbuild/linux-riscv64': 0.20.2 - '@esbuild/linux-s390x': 0.20.2 - '@esbuild/linux-x64': 0.20.2 - '@esbuild/netbsd-x64': 0.20.2 - '@esbuild/openbsd-x64': 0.20.2 - '@esbuild/sunos-x64': 0.20.2 - '@esbuild/win32-arm64': 0.20.2 - '@esbuild/win32-ia32': 0.20.2 - '@esbuild/win32-x64': 0.20.2 - /esbuild@0.21.3: resolution: {integrity: sha512-Kgq0/ZsAPzKrbOjCQcjoSmPoWhlcVnGAUo7jvaLHoxW1Drto0KGkR1xBNg2Cp43b9ImvxmPEJZ9xkfcnqPsfBw==} engines: {node: '>=12'} @@ -11177,7 +10939,6 @@ packages: '@esbuild/win32-arm64': 0.21.3 '@esbuild/win32-ia32': 0.21.3 '@esbuild/win32-x64': 0.21.3 - dev: true /escalade@3.2.0: resolution: {integrity: sha512-WUj2qlxaQtO4g6Pq5c29GTcWGDyd8itL8zTlipgECz3JesAiiOKotd8JU6otB3PACgG6xkJUyVhboMS+bje/jA==} @@ -14323,7 +14084,7 @@ packages: fflate: 0.4.8 optionalDependencies: canvg: 3.0.10 - core-js: 3.31.0 + core-js: 3.38.1 dompurify: 2.5.6 html2canvas: 1.4.1 dev: false @@ -20160,7 +19921,7 @@ packages: vfile-message: 3.1.4 dev: false - /vite-jest@0.1.4(jest@27.5.1)(vite@5.2.11): + /vite-jest@0.1.4(jest@27.5.1)(vite@5.4.6): resolution: {integrity: sha512-tMM8tCbtTGHVyf9+LOKyUSkItL952NBafzIAnxfSl/m293rSBfU+KQ5ZtdDMPgWDcQfg49TG9AKadm5Dzgv1bg==} hasBin: true peerDependencies: @@ -20173,7 +19934,7 @@ packages: jest-transform-stub: 2.0.0 magic-string: 0.25.9 slash: 4.0.0 - vite: 5.2.11(@types/node@17.0.45)(sass@1.71.1) + vite: 5.4.6(@types/node@17.0.45)(sass@1.71.1) dev: false /vite-node@1.6.0(@types/node@17.0.45)(sass@1.71.1): @@ -20185,19 +19946,20 @@ packages: debug: 4.3.7 pathe: 1.1.2 picocolors: 1.1.0 - vite: 5.2.11(@types/node@17.0.45)(sass@1.71.1) + vite: 5.4.6(@types/node@17.0.45)(sass@1.71.1) transitivePeerDependencies: - '@types/node' - less - lightningcss - sass + - sass-embedded - stylus - sugarss - supports-color - terser dev: true - /vite-plugin-svgr@4.2.0(rollup@2.79.1)(typescript@5.6.2)(vite@5.2.11): + /vite-plugin-svgr@4.2.0(rollup@2.79.1)(typescript@5.6.2)(vite@5.4.6): resolution: {integrity: sha512-SC7+FfVtNQk7So0XMjrrtLAbEC8qjFPifyD7+fs/E6aaNdVde6umlVVh0QuwDLdOMu7vp5RiGFsB70nj5yo0XA==} peerDependencies: vite: ^2.6.0 || 3 || 4 || 5 @@ -20205,14 +19967,14 @@ packages: '@rollup/pluginutils': 5.1.0(rollup@2.79.1) '@svgr/core': 8.1.0(typescript@5.6.2) '@svgr/plugin-jsx': 8.1.0(@svgr/core@8.1.0) - vite: 5.2.11(@types/node@17.0.45)(sass@1.71.1) + vite: 5.4.6(@types/node@17.0.45)(sass@1.71.1) transitivePeerDependencies: - rollup - supports-color - typescript dev: false - /vite-tsconfig-paths@4.3.2(typescript@5.6.2)(vite@5.2.11): + /vite-tsconfig-paths@4.3.2(typescript@5.6.2)(vite@5.4.6): resolution: {integrity: sha512-0Vd/a6po6Q+86rPlntHye7F31zA2URZMbH8M3saAZ/xR9QoGN/L21bxEGfXdWmFdNkqPpRdxFT7nmNe12e9/uA==} peerDependencies: vite: '*' @@ -20223,14 +19985,14 @@ packages: debug: 4.3.7 globrex: 0.1.2 tsconfck: 3.1.3(typescript@5.6.2) - vite: 5.2.11(@types/node@17.0.45)(sass@1.71.1) + vite: 5.4.6(@types/node@17.0.45)(sass@1.71.1) transitivePeerDependencies: - supports-color - typescript dev: true - /vite@5.2.11(@types/node@17.0.45)(sass@1.71.1): - resolution: {integrity: sha512-HndV31LWW05i1BLPMUCE1B9E9GFbOu1MbenhS58FuK6owSO5qHm7GiCotrNY1YE5rMeQSFBGmT5ZaLEjFizgiQ==} + /vite@5.4.6(@types/node@17.0.45)(sass@1.71.1): + resolution: {integrity: sha512-IeL5f8OO5nylsgzd9tq4qD2QqI0k2CQLGrWD0rCN0EQJZpBK5vJAx0I+GDkMOXxQX/OfFHMuLIx6ddAxGX/k+Q==} engines: {node: ^18.0.0 || >=20.0.0} hasBin: true peerDependencies: @@ -20238,6 +20000,7 @@ packages: less: '*' lightningcss: ^1.21.0 sass: '*' + sass-embedded: '*' stylus: '*' sugarss: '*' terser: ^5.4.0 @@ -20250,6 +20013,8 @@ packages: optional: true sass: optional: true + sass-embedded: + optional: true stylus: optional: true sugarss: @@ -20258,7 +20023,7 @@ packages: optional: true dependencies: '@types/node': 17.0.45 - esbuild: 0.20.2 + esbuild: 0.21.3 postcss: 8.4.47 rollup: 4.21.3 sass: 1.71.1 @@ -20319,13 +20084,14 @@ packages: strip-literal: 2.1.0 tinybench: 2.9.0 tinypool: 0.8.4 - vite: 5.2.11(@types/node@17.0.45)(sass@1.71.1) + vite: 5.4.6(@types/node@17.0.45)(sass@1.71.1) vite-node: 1.6.0(@types/node@17.0.45)(sass@1.71.1) why-is-node-running: 2.3.0 transitivePeerDependencies: - less - lightningcss - sass + - sass-embedded - stylus - sugarss - supports-color @@ -21064,9 +20830,9 @@ packages: use-sync-external-store: 1.2.0(react@18.2.0) dev: false - github.com/theopensystemslab/planx-core/c515291(@types/react@18.2.45): - resolution: {tarball: https://codeload.github.com/theopensystemslab/planx-core/tar.gz/c515291} - id: github.com/theopensystemslab/planx-core/c515291 + github.com/theopensystemslab/planx-core/c4a725f(@types/react@18.2.45): + resolution: {tarball: https://codeload.github.com/theopensystemslab/planx-core/tar.gz/c4a725f} + id: github.com/theopensystemslab/planx-core/c4a725f name: '@opensystemslab/planx-core' version: 1.0.0 prepare: true diff --git a/editor.planx.uk/src/@planx/components/List/Public/index.tsx b/editor.planx.uk/src/@planx/components/List/Public/index.tsx index aef0b30a2c..6b7f1f6888 100644 --- a/editor.planx.uk/src/@planx/components/List/Public/index.tsx +++ b/editor.planx.uk/src/@planx/components/List/Public/index.tsx @@ -8,11 +8,8 @@ import TableBody from "@mui/material/TableBody"; import TableCell from "@mui/material/TableCell"; import TableRow from "@mui/material/TableRow"; import Typography from "@mui/material/Typography"; -import { SiteAddress } from "@planx/components/FindProperty/model"; -import { ErrorSummaryContainer } from "@planx/components/shared/Preview/ErrorSummaryContainer"; import { SchemaFields } from "@planx/components/shared/Schema/SchemaFields"; import { PublicProps } from "@planx/components/ui"; -import { useStore } from "pages/FlowEditor/lib/store"; import React, { useEffect, useRef } from "react"; import { FONT_WEIGHT_SEMI_BOLD } from "theme"; import FullWidthWrapper from "ui/public/FullWidthWrapper"; @@ -56,14 +53,8 @@ const InactiveListCardLayout = styled(Box)(({ theme }) => ({ const ActiveListCard: React.FC<{ index: number; }> = ({ index: i }) => { - const { - schema, - saveItem, - cancelEditItem, - errors, - formik, - activeIndex, - } = useListContext(); + const { schema, saveItem, cancelEditItem, errors, formik, activeIndex } = + useListContext(); const ref = useRef(null); useEffect(() => { @@ -119,8 +110,7 @@ const ActiveListCard: React.FC<{ const InactiveListCard: React.FC<{ index: number; }> = ({ index: i }) => { - const { schema, formik, removeItem, editItem } = - useListContext(); + const { schema, formik, removeItem, editItem } = useListContext(); const mapPreview = schema.fields.find((field) => field.type === "map"); @@ -189,8 +179,7 @@ const Root = () => { listProps, } = useListContext(); - const { title, description, info, policyRef, howMeasured, handleSubmit } = - listProps; + const { title, description, info, policyRef, howMeasured } = listProps; const rootError: string = (errors.min && `You must provide at least ${schema.min} response(s)`) || @@ -201,32 +190,7 @@ const Root = () => { const shouldShowAddAnotherButton = schema.max !== 1 || formik.values.schemaData.length < 1; - // If the selected schema has a "map" field, ensure there's a FindProperty component preceding it (eg address data in state to position map view) const hasMapField = schema.fields.some((field) => field.type === "map"); - const { longitude, latitude } = useStore( - (state) => - (state.computePassport()?.data?.["_address"] as SiteAddress) || {}, - ); - - if (hasMapField && (!longitude || !latitude)) { - return ( - - - - - Invalid graph - - - Edit this flow so that "List" is positioned after "FindProperty"; an - address is required for schemas that include a "map" field. - - - - ); - } const listContent = ( diff --git a/editor.planx.uk/src/@planx/components/MapAndLabel/Public/index.test.tsx b/editor.planx.uk/src/@planx/components/MapAndLabel/Public/index.test.tsx index c79f02117e..5addd9b4c3 100644 --- a/editor.planx.uk/src/@planx/components/MapAndLabel/Public/index.test.tsx +++ b/editor.planx.uk/src/@planx/components/MapAndLabel/Public/index.test.tsx @@ -6,7 +6,12 @@ import { setup } from "testUtils"; import { vi } from "vitest"; import { axe } from "vitest-axe"; -import { point1, point2, point3 } from "../test/mocks/geojson"; +import { + mockFeaturePointObj, + point1, + point2, + point3, +} from "../test/mocks/geojson"; import { props } from "../test/mocks/Trees"; import { addFeaturesToMap, @@ -18,6 +23,7 @@ import { fillOutForm, fillOutSecondHalfOfForm, } from "../test/utils"; +import { mockTreeData } from "../test/mocks/GenericValues"; beforeAll(() => { if (!window.customElements.get("my-map")) { @@ -54,14 +60,14 @@ describe("Basic UI", () => { await waitFor(() => expect( - queryByText("Plot a feature on the map to begin") - ).not.toBeInTheDocument() + queryByText("Plot a feature on the map to begin"), + ).not.toBeInTheDocument(), ); }); it("renders the schema name as the tab title", async () => { const { queryByText, getByRole, getByTestId } = setup( - + , ); expect(queryByText(/Tree 1/)).not.toBeInTheDocument(); @@ -75,7 +81,7 @@ describe("Basic UI", () => { it("should not have any accessibility violations", async () => { const { queryByText, getByTestId, container } = setup( - + , ); expect(queryByText(/Tree 1/)).not.toBeInTheDocument(); @@ -92,7 +98,7 @@ describe("Basic UI", () => { describe("validation and error handling", () => { it("shows all fields are required", async () => { const { getByTestId, user, queryByRole, getAllByTestId } = setup( - + , ); const map = getByTestId("map-and-label-map"); @@ -166,7 +172,7 @@ describe("validation and error handling", () => { // ?? it("an error state is applied to a tabpanel button, when it's associated feature is invalid", async () => { const { getByTestId, user, queryByRole } = setup( - + , ); const map = getByTestId("map-and-label-map"); @@ -188,7 +194,7 @@ describe("validation and error handling", () => { it("does not trigger handleSubmit when errors exist", async () => { const handleSubmit = vi.fn(); const { getByTestId, user } = setup( - + , ); const map = getByTestId("map-and-label-map"); @@ -205,13 +211,16 @@ test.todo("an error displays if the maximum number of items is exceeded"); describe("basic interactions - happy path", () => { it("adding an item to the map adds a feature tab", async () => { const { getByTestId } = setup(); - const map = getByTestId("map-and-label-map"); + let map = getByTestId("map-and-label-map"); addFeaturesToMap(map, [point1]); const firstTabPanel = getByTestId("vertical-tabpanel-0"); expect(firstTabPanel).toBeVisible(); + + map = getByTestId("map-and-label-map"); + expect(map).toHaveAttribute("drawgeojsondata", mockFeaturePointObj); }); it("a user can input details on a single feature and submit", async () => { @@ -332,25 +341,154 @@ describe("basic interactions - happy path", () => { }); describe("copy feature select", () => { - it.todo("is disabled if only a single feature is present"); - // no copy select if only one feature - it.todo("is enabled once multiple features are present"); - // copy select enabled once you add more features - it.todo( - "lists all other features as options (the current feature is not listed)" - ); - // current tree is not an option in the copy select - it.todo("copies all data from one feature to another"); - // all data fields are populated from one field to another - it.todo("should not have any accessibility violations"); - // axe checks -}); + it("is disabled if only a single feature is present", async () => { + const { getByTestId, getByTitle } = setup(); + const map = getByTestId("map-and-label-map"); + + addFeaturesToMap(map, [point1]); + + const copyTitle = getByTitle("Copy from"); + + const copyInput = within(copyTitle).getByRole("combobox"); + + expect(copyInput).toHaveAttribute("aria-disabled", "true"); + }); + + it("is enabled once multiple features are present", async () => { + const { getByTitle } = setup(); + + addMultipleFeatures([point1, point2]); + + const copyTitle = getByTitle("Copy from"); + + const copyInput = within(copyTitle).getByRole("combobox"); + + expect(copyInput).not.toHaveAttribute("aria-disabled", "true"); + }); + + it("lists all other features as options (the current feature is not listed)", async () => { + const { getByTitle, user, queryByRole } = setup(); + addMultipleFeatures([point1, point2]); + + const copyTitle = getByTitle("Copy from"); + + const copyInput = within(copyTitle).getByRole("combobox"); + + expect(copyInput).not.toHaveAttribute("aria-disabled", "true"); + + await user.click(copyInput); + + // Current item would be Tree 2 since we added two points + const listItemTwo = queryByRole("option", { name: "Tree 2" }); + + expect(listItemTwo).not.toBeInTheDocument(); + }); + + it("copies all data from one feature to another", async () => { + const { getByTitle, user, getByLabelText, getByRole } = setup( + + ); + addMultipleFeatures([point1, point2]); + const tabOne = getByRole("tab", { name: /Tree 1/ }); + + await fillOutForm(user); + + await user.click(tabOne); + + const copyTitle = getByTitle("Copy from"); + const copyInput = within(copyTitle).getByRole("combobox", { + name: "Copy from", + }); + + await user.click(copyInput); + + const listItemTwo = getByRole("option", { name: "Tree 2" }); + + await user.click(listItemTwo); + const urgencyDiv = getByTitle("Urgency"); + const urgencySelect = within(urgencyDiv).getByRole("combobox"); + + expect(getByLabelText("Species")).toHaveDisplayValue(mockTreeData.species); + expect(getByLabelText("Proposed work")).toHaveDisplayValue( + mockTreeData.work + ); + expect(getByLabelText("Justification")).toHaveDisplayValue( + mockTreeData.justification + ); + expect(urgencySelect).toHaveTextContent(mockTreeData.urgency); + }); + + it("should not have any accessibility violations", async () => { + const { getByTitle, user, container } = setup(); + addMultipleFeatures([point1, point2]); + + const copyTitle = getByTitle("Copy from"); + + const copyInput = within(copyTitle).getByRole("combobox"); + + await user.click(copyInput); + + const results = await axe(container); + expect(results).toHaveNoViolations(); + }); +}); describe("remove feature button", () => { - it.todo("removes a feature from the form"); - // click remove - feature is removed - // not tab - it.todo("removes a feature from the map"); + it("removes a feature from the form - single feature", async () => { + const { getByTestId, getByRole, user } = setup(); + const map = getByTestId("map-and-label-map"); + + addFeaturesToMap(map, [point1]); + + const tabOne = getByRole("tab", { name: /Tree 1/ }); + const tabOnePanel = getByRole("tabpanel", { name: /Tree 1/ }); + + const removeButton = getByRole("button", { name: "Remove" }); + + await user.click(removeButton); + + expect(tabOne).not.toBeInTheDocument(); + expect(tabOnePanel).not.toBeInTheDocument(); + }); + it("removes a feature from the form - multiple features", async () => { + const { getByRole, user } = setup(); + + addMultipleFeatures([point1, point2]); + + const tabOne = getByRole("tab", { name: /Tree 1/ }); + const tabTwo = getByRole("tab", { name: /Tree 2/ }); + const tabTwoPanel = getByRole("tabpanel", { name: /Tree 2/ }); + + const removeButton = getByRole("button", { name: "Remove" }); + + await user.click(removeButton); + + expect(tabTwo).not.toBeInTheDocument(); + expect(tabTwoPanel).not.toBeInTheDocument(); + + const tabOnePanel = getByRole("tabpanel", { name: /Tree 1/ }); + + // Ensure tab one remains + expect(tabOne).toBeInTheDocument(); + expect(tabOnePanel).toBeInTheDocument(); + }); + it("removes a feature from the map", async () => { + const { getByTestId, getByRole, user } = setup(); + let map = getByTestId("map-and-label-map"); + + addFeaturesToMap(map, [point1]); + + const removeButton = getByRole("button", { name: "Remove" }); + + await user.click(removeButton); + + map = getByTestId("map-and-label-map"); + + expect(map).toHaveAttribute( + "drawgeojsondata", + `{"type":"FeatureCollection","features":[]}` + ); + }); // click remove - feature is removed // no map icon }); @@ -359,11 +497,11 @@ describe("payload generation", () => { test.todo("a submitted payload contains a GeoJSON feature collection"); // check payload contains GeoJSON feature collection test.todo( - "the feature collection contains all geospatial data inputted by the user" + "the feature collection contains all geospatial data inputted by the user", ); // feature collection matches the mocked data test.todo( - "each feature's properties correspond with the details entered for that feature" + "each feature's properties correspond with the details entered for that feature", ); // feature properties contain the answers to inputs }); diff --git a/editor.planx.uk/src/@planx/components/MapAndLabel/Public/index.tsx b/editor.planx.uk/src/@planx/components/MapAndLabel/Public/index.tsx index 4b260271a7..f55921d07f 100644 --- a/editor.planx.uk/src/@planx/components/MapAndLabel/Public/index.tsx +++ b/editor.planx.uk/src/@planx/components/MapAndLabel/Public/index.tsx @@ -8,8 +8,8 @@ import Tab, { tabClasses, TabProps } from "@mui/material/Tab"; import Tabs from "@mui/material/Tabs"; import Typography from "@mui/material/Typography"; import { SiteAddress } from "@planx/components/FindProperty/model"; -import { ErrorSummaryContainer } from "@planx/components/shared/Preview/ErrorSummaryContainer"; import { SchemaFields } from "@planx/components/shared/Schema/SchemaFields"; +import { GraphError } from "components/Error/GraphError"; import { GeoJsonObject } from "geojson"; import sortBy from "lodash/sortBy"; import { useStore } from "pages/FlowEditor/lib/store"; @@ -305,24 +305,6 @@ export const Presentational: React.FC = (props) => ( ); -const GraphError = (props: Props) => ( - - - - - Invalid graph - - - Edit this flow so that "MapAndLabel" is positioned after "FindProperty"; - an initial address is required to correctly display the map. - - - -); - function MapAndLabelComponent(props: Props) { const teamSettings = useStore.getState().teamSettings; const passport = useStore((state) => state.computePassport()); @@ -330,7 +312,7 @@ function MapAndLabelComponent(props: Props) { (passport?.data?._address as SiteAddress) || {}; if (!latitude || !longitude) { - return ; + throw new GraphError("nodeMustFollowFindProperty"); } return ( diff --git a/editor.planx.uk/src/@planx/components/MapAndLabel/test/mocks/GenericValues.ts b/editor.planx.uk/src/@planx/components/MapAndLabel/test/mocks/GenericValues.ts index 61912b194d..a67af1c009 100644 --- a/editor.planx.uk/src/@planx/components/MapAndLabel/test/mocks/GenericValues.ts +++ b/editor.planx.uk/src/@planx/components/MapAndLabel/test/mocks/GenericValues.ts @@ -2,6 +2,5 @@ export const mockTreeData = { species: "Larch", work: "Chopping it down", justification: "Cause I can", - urgency: "High", - completionDate: { day: "", month: "", year: "" }, + urgency: "Low", }; diff --git a/editor.planx.uk/src/@planx/components/MapAndLabel/test/mocks/geojson.ts b/editor.planx.uk/src/@planx/components/MapAndLabel/test/mocks/geojson.ts index 7aff35a382..6fdd9fb026 100644 --- a/editor.planx.uk/src/@planx/components/MapAndLabel/test/mocks/geojson.ts +++ b/editor.planx.uk/src/@planx/components/MapAndLabel/test/mocks/geojson.ts @@ -32,3 +32,5 @@ export const point3: Feature = { coordinates: [-3.68689607119201, 57.15310833687542], }, }; + +export const mockFeaturePointObj = `{"type":"FeatureCollection","features":[{"type":"Feature","properties":{"label":"1"},"geometry":{"type":"Point","coordinates":[-3.685929607119201,57.15301433687542]}}]}`; diff --git a/editor.planx.uk/src/@planx/components/MapAndLabel/test/utils.ts b/editor.planx.uk/src/@planx/components/MapAndLabel/test/utils.ts index 1873246808..0b2424e603 100644 --- a/editor.planx.uk/src/@planx/components/MapAndLabel/test/utils.ts +++ b/editor.planx.uk/src/@planx/components/MapAndLabel/test/utils.ts @@ -11,7 +11,7 @@ import { mockTreeData } from "./mocks/GenericValues"; */ export const addFeaturesToMap = async ( map: HTMLElement, - features: Feature[] + features: Feature[], ) => { const mockEvent = new CustomEvent("geojsonChange", { detail: { @@ -22,7 +22,7 @@ export const addFeaturesToMap = async ( }; export const addMultipleFeatures = ( - featureArray: Feature[] + featureArray: Feature[], ) => { const map = screen.getByTestId("map-and-label-map"); const pointsAddedArray: Feature[] = []; diff --git a/editor.planx.uk/src/@planx/components/PlanningConstraints/Public.test.tsx b/editor.planx.uk/src/@planx/components/PlanningConstraints/Public.test.tsx index 2eefae94fe..86504baec4 100644 --- a/editor.planx.uk/src/@planx/components/PlanningConstraints/Public.test.tsx +++ b/editor.planx.uk/src/@planx/components/PlanningConstraints/Public.test.tsx @@ -1,64 +1,236 @@ -import { screen } from "@testing-library/react"; +import ErrorFallback from "components/Error/ErrorFallback"; +import { useStore } from "pages/FlowEditor/lib/store"; import React from "react"; +import { act } from "react-dom/test-utils"; +import { ErrorBoundary } from "react-error-boundary"; +import swr from "swr"; import { setup } from "testUtils"; import { vi } from "vitest"; import { axe } from "vitest-axe"; import classifiedRoadsResponseMock from "./mocks/classifiedRoadsResponseMock"; import digitalLandResponseMock from "./mocks/digitalLandResponseMock"; +import { breadcrumbsWithoutUSRN, simpleBreadcrumbs, simpleFlow } from "./mocks/simpleFlow"; import PlanningConstraints from "./Public"; +const { setState } = useStore; + +beforeEach(() => vi.clearAllMocks()); + +const swrMock = (swr as jest.Mock).mock; + vi.mock("swr", () => ({ - default: vi.fn((url: any) => { + default: vi.fn((url: () => string) => { const isGISRequest = url()?.startsWith( - `${import.meta.env.VITE_APP_API_URL}/gis/`, + `${import.meta.env.VITE_APP_API_URL}/gis`, ); const isRoadsRequest = url()?.startsWith( - `${import.meta.env.VITE_APP_API_URL}/roads/`, + `${import.meta.env.VITE_APP_API_URL}/roads`, ); - if (isGISRequest) return { data: digitalLandResponseMock } as any; - if (isRoadsRequest) return { data: classifiedRoadsResponseMock } as any; + if (isGISRequest) return { data: digitalLandResponseMock }; + if (isRoadsRequest) return { data: classifiedRoadsResponseMock }; return { data: null }; }), })); -it("renders correctly", async () => { - const handleSubmit = vi.fn(); +describe("error state", () => { + it("renders an error if no addres is present in the passport", async () => { + const { getByRole, getByTestId } = setup( + + + , + ); - const { user } = setup( - , - ); + expect(getByTestId("error-summary-invalid-graph")).toBeInTheDocument(); + expect(getByRole("heading", { name: "Invalid graph" })).toBeInTheDocument(); + }); - expect(screen.getByText("Planning constraints")).toBeInTheDocument(); + it("should not have any accessibility violations", async () => { + const { container } = setup( + + + , + ); + const results = await axe(container); + expect(results).toHaveNoViolations(); + }); +}); - // TODO mock passport _address so that SWR request is actually triggered to return mock response - expect(screen.getByTestId("error-summary-invalid-graph")).toBeInTheDocument(); +describe("following a FindProperty component", () => { + beforeEach(() => { + act(() => + setState({ + breadcrumbs: simpleBreadcrumbs, + flow: simpleFlow, + teamIntegrations: { + hasPlanningData: true, + }, + }), + ); + }); - await user.click(screen.getByTestId("continue-button")); - expect(handleSubmit).toHaveBeenCalledTimes(1); -}); + it("renders correctly", async () => { + const handleSubmit = vi.fn(); -it("should not have any accessibility violations", async () => { - const { container } = setup( - , - ); - const results = await axe(container); - expect(results).toHaveNoViolations(); -}); + const { user, getByRole, getByTestId } = setup( + , + ); + + expect( + getByRole("heading", { name: "Planning constraints" }), + ).toBeInTheDocument(); + + await user.click(getByTestId("continue-button")); + + expect(handleSubmit).toHaveBeenCalled(); + }); + + it("should not have any accessibility violations", async () => { + const { container } = setup( + , + ); + const results = await axe(container); + expect(results).toHaveNoViolations(); + }); + + it("fetches planning constraints when we have lng, lat or siteBoundary", async () => { + setup( + , + ); + + expect(swr).toHaveBeenCalled(); + + // Planning data is called first + const swrURL = swrMock.calls[0][0](); + const swrResponse = swrMock.results[0].value; + + expect(swrURL).toContain("/gis"); + expect(swrResponse).toEqual({ data: digitalLandResponseMock }); + }); + + it("fetches classified roads when a USRN is provided", () => { + setup( + , + ); + + expect(swr).toHaveBeenCalled(); + + // Classified roads are called second + const swrURL = swrMock.calls[1][0](); + const swrResponse = swrMock.results[1].value; + + expect(swrURL).toContain("/roads"); + expect(swrResponse).toEqual({ data: classifiedRoadsResponseMock }); + }); + + it("does not fetch classified roads when a USRN is not provided", async () => { + act(() => + setState({ + breadcrumbs: breadcrumbsWithoutUSRN, + flow: simpleFlow, + teamIntegrations: { + hasPlanningData: true, + }, + }) + ); + + setup( + , + ); + + expect(swr).toHaveBeenCalled(); + + // Planning constraints API still called + const planingConstraintsURL = swrMock.calls[0][0](); + const planingConstraintsResponse = swrMock.results[0].value; + + expect(planingConstraintsURL).toContain("/gis"); + expect(planingConstraintsResponse).toEqual({ data: digitalLandResponseMock }); + + // Classified roads API not called due to missing USRN + const swrURL = swrMock.calls[1][0](); + const swrResponse = swrMock.results[1].value; + + expect(swrURL).toBeNull(); + expect(swrResponse).toEqual({ data: null }); + }); + + test("basic layout and interactions", async () => { + const { user, getByRole, queryByRole, getByTestId } = setup( + , + ); + + // Positive constraints visible by default + expect( + getByRole("heading", { name: /These are the planning constraints/ }), + ).toBeVisible(); + expect(getByRole("button", { name: /Parks and gardens/ })).toBeVisible(); + + // Negative constraints hidden by default + const showNegativeConstraintsButton = getByRole("button", { + name: /Constraints that don't apply/, + }); + expect(showNegativeConstraintsButton).toBeVisible(); + + const negativeConstraintsContainer = getByTestId( + "negative-constraints-list", + ); + expect(negativeConstraintsContainer).not.toBeVisible(); + + expect(queryByRole("heading", { name: /Ecology/ })).not.toBeInTheDocument(); -it.todo("fetches classified roads only when we have a siteBoundary"); // using expect(spy).toHaveBeenCalled() ?? + // Negative constraints viewable on toggle + await user.click(showNegativeConstraintsButton); -it.todo("fetches planning constraints when we have lng,lat or siteBoundary"); + expect(negativeConstraintsContainer).toBeVisible(); + expect(getByRole("heading", { name: /Ecology/ })).toBeVisible(); + }); +}); \ No newline at end of file diff --git a/editor.planx.uk/src/@planx/components/PlanningConstraints/Public.tsx b/editor.planx.uk/src/@planx/components/PlanningConstraints/Public.tsx index f93b0a1edc..2cfcf8fbbd 100644 --- a/editor.planx.uk/src/@planx/components/PlanningConstraints/Public.tsx +++ b/editor.planx.uk/src/@planx/components/PlanningConstraints/Public.tsx @@ -9,6 +9,7 @@ import Card from "@planx/components/shared/Preview/Card"; import CardHeader from "@planx/components/shared/Preview/CardHeader"; import type { PublicProps } from "@planx/components/ui"; import DelayedLoadingIndicator from "components/DelayedLoadingIndicator"; +import { GraphError } from "components/Error/GraphError"; import capitalize from "lodash/capitalize"; import { useStore } from "pages/FlowEditor/lib/store"; import { HandleSubmit } from "pages/Preview/Node"; @@ -63,6 +64,8 @@ function Component(props: Props) { // PlanningConstraints must come after at least a FindProperty in the graph const showGraphError = !x || !y || !longitude || !latitude; + if (showGraphError) + throw new GraphError("mapInputFieldMustFollowFindProperty"); // Even though this component will fetch fresh GIS data when coming "back", // still prepopulate any previously marked inaccurateConstraints @@ -145,8 +148,6 @@ function Component(props: Props) { ...roads?.metadata, }; - if (showGraphError) return ; - const isLoading = isValidating || isValidatingRoads; if (isLoading) return ( @@ -301,6 +302,7 @@ export function PlanningConstraintsContent( {negativeConstraints.length > 0 && ( ( ); - -interface ConstraintsGraphErrorProps { - title: string; - description: string; - handleSubmit?: HandleSubmit; -} - -const ConstraintsGraphError = (props: ConstraintsGraphErrorProps) => ( - - - - - Invalid graph - - - Edit this flow so that "Planning constraints" is positioned after "Find - property"; an address or site boundary drawing is required to fetch - data. - - - -); diff --git a/editor.planx.uk/src/@planx/components/PlanningConstraints/mocks/simpleFlow.ts b/editor.planx.uk/src/@planx/components/PlanningConstraints/mocks/simpleFlow.ts new file mode 100644 index 0000000000..ae7a984bea --- /dev/null +++ b/editor.planx.uk/src/@planx/components/PlanningConstraints/mocks/simpleFlow.ts @@ -0,0 +1,98 @@ +import { cloneDeep, merge } from "lodash"; +import { Store } from "pages/FlowEditor/lib/store"; + +export const simpleFlow: Store.Flow = { + _root: { + edges: ["findProperty", "planningConstraints"], + }, + findProperty: { + type: 9, + data: { + title: "Find the property", + allowNewAddresses: false, + newAddressTitle: + "Click or tap at where the property is on the map and name it below", + newAddressDescription: + "You will need to select a location and provide a name to continue", + newAddressDescriptionLabel: "Name the site", + }, + }, + planningConstraints: { + type: 11, + data: { + title: "Planning constraints", + description: + "Planning constraints might limit how you can develop or use the property", + fn: "property.constraints.planning", + disclaimer: + "

This page does not include information about historic planning conditions that may apply to this property.

", + }, + }, +}; + +export const simpleBreadcrumbs: Store.Breadcrumbs = { + findProperty: { + auto: false, + data: { + _address: { + uprn: "100071417680", + usrn: "2702440", + blpu_code: "2", + latitude: 52.4804358, + longitude: -1.9034539, + organisation: null, + sao: "", + saoEnd: "", + pao: "COUNCIL HOUSE", + paoEnd: "", + street: "VICTORIA SQUARE", + town: "BIRMINGHAM", + postcode: "B1 1BB", + ward: "E05011151", + x: 406653.64, + y: 286948.41, + planx_description: "Local Government Service", + planx_value: "commercial.office.workspace.gov.local", + single_line_address: + "COUNCIL HOUSE, VICTORIA SQUARE, BIRMINGHAM, B1 1BB", + title: "COUNCIL HOUSE, VICTORIA SQUARE", + source: "os", + }, + "property.type": ["commercial.office.workspace.gov.local"], + "property.localAuthorityDistrict": ["Birmingham"], + "property.region": ["West Midlands"], + "property.boundary.title": { + geometry: { + type: "MultiPolygon", + coordinates: [ + [ + [ + [-1.903955, 52.480237], + [-1.903881, 52.480179], + [-1.903955, 52.480237], + ], + ], + ], + }, + type: "Feature", + properties: { + "entry-date": "2024-05-06", + "start-date": "2021-03-25", + "end-date": "", + entity: 12001049997, + name: "", + dataset: "title-boundary", + typology: "geography", + reference: "61385289", + prefix: "title-boundary", + "organisation-entity": "13", + }, + }, + "property.boundary.title.area": 8242.37, + "property.boundary.title.area.hectares": 0.8242370000000001, + "findProperty.action": "Selected an existing address", + }, + }, +}; + +export const breadcrumbsWithoutUSRN = merge(cloneDeep(simpleBreadcrumbs), { findProperty: { data: { _address: { usrn: null }}}}); \ No newline at end of file diff --git a/editor.planx.uk/src/@planx/components/PropertyInformation/Public.test.tsx b/editor.planx.uk/src/@planx/components/PropertyInformation/Public.test.tsx index 318aaf37f2..2e35f47189 100644 --- a/editor.planx.uk/src/@planx/components/PropertyInformation/Public.test.tsx +++ b/editor.planx.uk/src/@planx/components/PropertyInformation/Public.test.tsx @@ -1,6 +1,8 @@ import { MockedProvider } from "@apollo/client/testing"; import { screen } from "@testing-library/react"; +import ErrorFallback from "components/Error/ErrorFallback"; import React from "react"; +import { ErrorBoundary } from "react-error-boundary"; import { setup } from "testUtils"; import { vi } from "vitest"; @@ -18,10 +20,12 @@ const defaultPresentationalProps: PresentationalProps = { test("renders a warning for editors if address data is not in state", async () => { setup( - + + + , ); diff --git a/editor.planx.uk/src/@planx/components/PropertyInformation/Public.tsx b/editor.planx.uk/src/@planx/components/PropertyInformation/Public.tsx index 5502fc42aa..6b6bfc17d9 100644 --- a/editor.planx.uk/src/@planx/components/PropertyInformation/Public.tsx +++ b/editor.planx.uk/src/@planx/components/PropertyInformation/Public.tsx @@ -1,12 +1,12 @@ import { useQuery } from "@apollo/client"; import Box from "@mui/material/Box"; import Link from "@mui/material/Link"; -import Typography from "@mui/material/Typography"; import { visuallyHidden } from "@mui/utils"; import Card from "@planx/components/shared/Preview/Card"; import CardHeader from "@planx/components/shared/Preview/CardHeader"; import { SummaryListTable } from "@planx/components/shared/Preview/SummaryList"; import type { PublicProps } from "@planx/components/ui"; +import { GraphError } from "components/Error/GraphError"; import { Feature } from "geojson"; import { publicClient } from "lib/graphql"; import find from "lodash/find"; @@ -17,7 +17,6 @@ import React from "react"; import type { SiteAddress } from "../FindProperty/model"; import { FETCH_BLPU_CODES } from "../FindProperty/Public"; -import { ErrorSummaryContainer } from "../shared/Preview/ErrorSummaryContainer"; import { MapContainer } from "../shared/Preview/MapContainer"; import type { PropertyInformation } from "./model"; @@ -32,7 +31,10 @@ function Component(props: PublicProps) { client: publicClient, }); - return passport.data?._address ? ( + if (!passport.data?._address) + throw new GraphError("nodeMustFollowFindProperty"); + + return ( ) { }); }} /> - ) : ( - - - - Invalid graph - - - Edit this flow so that "Property information" is positioned after - "Find property"; an address is required to render. - - - ); } diff --git a/editor.planx.uk/src/@planx/components/shared/Preview/SimpleExpand.tsx b/editor.planx.uk/src/@planx/components/shared/Preview/SimpleExpand.tsx index beb2475701..2bc82c9396 100644 --- a/editor.planx.uk/src/@planx/components/shared/Preview/SimpleExpand.tsx +++ b/editor.planx.uk/src/@planx/components/shared/Preview/SimpleExpand.tsx @@ -48,7 +48,7 @@ const SimpleExpand: React.FC> = ({ /> - + {children} diff --git a/editor.planx.uk/src/@planx/components/shared/Schema/InputFields/MapFieldInput.tsx b/editor.planx.uk/src/@planx/components/shared/Schema/InputFields/MapFieldInput.tsx index 765ef75489..7ddc054bc9 100644 --- a/editor.planx.uk/src/@planx/components/shared/Schema/InputFields/MapFieldInput.tsx +++ b/editor.planx.uk/src/@planx/components/shared/Schema/InputFields/MapFieldInput.tsx @@ -1,6 +1,8 @@ import Box from "@mui/material/Box"; +import { SiteAddress } from "@opensystemslab/planx-core/types"; import { MapContainer } from "@planx/components/shared/Preview/MapContainer"; import type { MapField } from "@planx/components/shared/Schema/model"; +import { GraphError } from "components/Error/GraphError"; import { Feature } from "geojson"; import { useStore } from "pages/FlowEditor/lib/store"; import React, { useEffect, useState } from "react"; @@ -11,6 +13,15 @@ import { getFieldProps, Props } from "."; import { FieldInputDescription } from "./shared"; export const MapFieldInput: React.FC> = (props) => { + // Ensure there's a FindProperty component preceding this field (eg address data in state to position map view) + const { longitude, latitude } = useStore( + (state) => + (state.computePassport()?.data?.["_address"] as SiteAddress) || {}, + ); + + if (!longitude || !latitude) + throw new GraphError("mapInputFieldMustFollowFindProperty"); + const { formik, data: { title, description, mapOptions }, diff --git a/editor.planx.uk/src/components/ErrorFallback.stories.tsx b/editor.planx.uk/src/components/Error/ErrorFallback.stories.tsx similarity index 100% rename from editor.planx.uk/src/components/ErrorFallback.stories.tsx rename to editor.planx.uk/src/components/Error/ErrorFallback.stories.tsx diff --git a/editor.planx.uk/src/components/ErrorFallback.tsx b/editor.planx.uk/src/components/Error/ErrorFallback.tsx similarity index 70% rename from editor.planx.uk/src/components/ErrorFallback.tsx rename to editor.planx.uk/src/components/Error/ErrorFallback.tsx index 68a85ff170..ad9e822d91 100644 --- a/editor.planx.uk/src/components/ErrorFallback.tsx +++ b/editor.planx.uk/src/components/Error/ErrorFallback.tsx @@ -1,12 +1,15 @@ import Typography from "@mui/material/Typography"; import Card from "@planx/components/shared/Preview/Card"; import { ErrorSummaryContainer } from "@planx/components/shared/Preview/ErrorSummaryContainer"; +import { logger } from "airbrake"; import React from "react"; -import { logger } from "../airbrake"; +import { GraphErrorComponent, isGraphError } from "./GraphError"; -function ErrorFallback(props: { error: Error }) { - logger.notify(props.error); +const ErrorFallback: React.FC<{ error: Error }> = ({ error }) => { + if (isGraphError(error)) return ; + + logger.notify(error); return ( @@ -15,9 +18,9 @@ function ErrorFallback(props: { error: Error }) { Something went wrong - {props.error?.message && ( + {error.message && (
-              {props.error.message}
+              {error.message}
             
)}
@@ -27,6 +30,6 @@ function ErrorFallback(props: { error: Error }) {
); -} +}; export default ErrorFallback; diff --git a/editor.planx.uk/src/components/Error/GraphError.test.tsx b/editor.planx.uk/src/components/Error/GraphError.test.tsx new file mode 100644 index 0000000000..ec79de8dfc --- /dev/null +++ b/editor.planx.uk/src/components/Error/GraphError.test.tsx @@ -0,0 +1,81 @@ +import { logger } from "airbrake"; +import ErrorFallback from "components/Error/ErrorFallback"; +import React from "react"; +import { ErrorBoundary } from "react-error-boundary"; +import { setup } from "testUtils"; +import { vi } from "vitest"; +import { axe } from "vitest-axe"; + +import { GraphError } from "./GraphError"; + +vi.mock("airbrake", () => ({ + logger: { + notify: vi.fn(), + }, +})); + +const ThrowError: React.FC = () => { + throw new Error("Something broke"); +}; + +const ThrowGraphError: React.FC = () => { + throw new GraphError("nodeMustFollowFindProperty"); +}; + +it("does not render if a child does not throw an error", () => { + const { queryByRole } = setup( + +

No error

+
, + ); + expect( + queryByRole("heading", { name: /Invalid graph/ }), + ).not.toBeInTheDocument(); +}); + +it("does not render if a child throws a non-Graph error", () => { + const { queryByRole, getByText } = setup( + + + , + ); + // ErrorFallback displays... + expect(getByText(/Something went wrong/)).toBeInTheDocument(); + // ...but does not show a GraphError + expect( + queryByRole("heading", { name: /Invalid graph/ }), + ).not.toBeInTheDocument(); +}); + +it("renders if a child throws an error", () => { + const { queryByText, getByRole } = setup( + + + , + ); + + expect(queryByText(/Something went wrong/)).not.toBeInTheDocument(); + expect(getByRole("heading", { name: /Invalid graph/ })).toBeInTheDocument(); +}); + +it("does not call Airbrake", () => { + const loggerSpy = vi.spyOn(logger, "notify"); + + setup( + + + , + ); + + expect(loggerSpy).not.toHaveBeenCalled(); +}); + +it("should not have accessability violations", async () => { + const { container } = setup( + + + , + ); + const results = await axe(container); + expect(results).toHaveNoViolations(); +}); diff --git a/editor.planx.uk/src/components/Error/GraphError.tsx b/editor.planx.uk/src/components/Error/GraphError.tsx new file mode 100644 index 0000000000..2b86e7aaef --- /dev/null +++ b/editor.planx.uk/src/components/Error/GraphError.tsx @@ -0,0 +1,43 @@ +import Typography from "@mui/material/Typography"; +import Card from "@planx/components/shared/Preview/Card"; +import { ErrorSummaryContainer } from "@planx/components/shared/Preview/ErrorSummaryContainer"; +import React from "react"; + +type GraphErrorType = + | "nodeMustFollowFindProperty" + | "mapInputFieldMustFollowFindProperty"; + +const GRAPH_ERROR_MESSAGES: Record = { + nodeMustFollowFindProperty: + 'Edit this flow so that this node is positioned after "Find property"; an address or site boundary drawing is required to fetch data', + mapInputFieldMustFollowFindProperty: + 'Edit this flow so that this component is positioned after "FindProperty"; an address is required for schemas that include a "map" field.', +}; + +export class GraphError extends Error { + constructor(public type: GraphErrorType) { + super(); + this.type = type; + } +} + +export const isGraphError = (error: unknown): error is GraphError => + error instanceof GraphError; + +export const GraphErrorComponent: React.FC<{ error: GraphError }> = ({ + error, +}) => ( + + + + Invalid graph + + + {GRAPH_ERROR_MESSAGES[error.type]} + + + +); diff --git a/editor.planx.uk/src/components/RouteLoadingIndicator.tsx b/editor.planx.uk/src/components/RouteLoadingIndicator.tsx index 72c6852ce2..4417471a18 100644 --- a/editor.planx.uk/src/components/RouteLoadingIndicator.tsx +++ b/editor.planx.uk/src/components/RouteLoadingIndicator.tsx @@ -33,4 +33,4 @@ const RouteLoadingIndicator: React.FC<{ ); }; -export default RouteLoadingIndicator; \ No newline at end of file +export default RouteLoadingIndicator; diff --git a/editor.planx.uk/src/lib/featureFlags.ts b/editor.planx.uk/src/lib/featureFlags.ts index e065d941b1..b1e478c760 100644 --- a/editor.planx.uk/src/lib/featureFlags.ts +++ b/editor.planx.uk/src/lib/featureFlags.ts @@ -1,5 +1,5 @@ // add/edit/remove feature flags in array below -const AVAILABLE_FEATURE_FLAGS = ["SEARCH", "ADD_NEW_EDITOR", "PAGE"] as const; +const AVAILABLE_FEATURE_FLAGS = ["SEARCH", "ADD_NEW_EDITOR"] as const; type FeatureFlag = (typeof AVAILABLE_FEATURE_FLAGS)[number]; diff --git a/editor.planx.uk/src/pages/FlowEditor/components/Settings/Submissions/EventsLog.tsx b/editor.planx.uk/src/pages/FlowEditor/components/Settings/Submissions/EventsLog.tsx index 4de7b76aac..29d1994471 100644 --- a/editor.planx.uk/src/pages/FlowEditor/components/Settings/Submissions/EventsLog.tsx +++ b/editor.planx.uk/src/pages/FlowEditor/components/Settings/Submissions/EventsLog.tsx @@ -15,7 +15,7 @@ import TableHead from "@mui/material/TableHead"; import TableRow from "@mui/material/TableRow"; import Typography from "@mui/material/Typography"; import DelayedLoadingIndicator from "components/DelayedLoadingIndicator"; -import ErrorFallback from "components/ErrorFallback"; +import ErrorFallback from "components/Error/ErrorFallback"; import { format } from "date-fns"; import React, { useState } from "react"; import ErrorSummary from "ui/shared/ErrorSummary"; diff --git a/editor.planx.uk/src/pages/FlowEditor/components/Team/TeamMembers.tsx b/editor.planx.uk/src/pages/FlowEditor/components/Team/TeamMembers.tsx index 1ae158e124..e6b0535e30 100644 --- a/editor.planx.uk/src/pages/FlowEditor/components/Team/TeamMembers.tsx +++ b/editor.planx.uk/src/pages/FlowEditor/components/Team/TeamMembers.tsx @@ -16,7 +16,7 @@ import { TeamMember } from "./types"; export const TeamMembers = () => { const [teamMembers, teamSlug] = useStore((state) => [ - state.teamMembers, + state.teamMembers, state.teamSlug, ]); @@ -48,7 +48,11 @@ export const TeamMembers = () => { Editors have access to edit your services. - + @@ -57,7 +61,10 @@ export const TeamMembers = () => { Admins have editor access across all teams. - + {archivedMembers.length > 0 && ( diff --git a/editor.planx.uk/src/pages/FlowEditor/components/Team/tests/TeamMembers.addNewEditor.test.tsx b/editor.planx.uk/src/pages/FlowEditor/components/Team/tests/TeamMembers.addNewEditor.test.tsx index 1c9a54c6a6..c82b41cf71 100644 --- a/editor.planx.uk/src/pages/FlowEditor/components/Team/tests/TeamMembers.addNewEditor.test.tsx +++ b/editor.planx.uk/src/pages/FlowEditor/components/Team/tests/TeamMembers.addNewEditor.test.tsx @@ -94,14 +94,18 @@ describe("when the addNewEditor modal is rendered", () => { }); describe("'add a new editor' button is hidden from Templates team", () => { - beforeEach(async() => { - useStore.setState({ teamMembers: mockTeamMembersData, teamSlug: "templates" }); + beforeEach(async () => { + useStore.setState({ + teamMembers: mockTeamMembersData, + teamSlug: "templates", + }); }); it("hides the button on the Templates team", async () => { const { user: _user } = await setupTeamMembersScreen(); const teamEditorsTable = screen.getByTestId("team-editors"); - const addEditorButton = within(teamEditorsTable).queryByText("Add a new editor"); + const addEditorButton = + within(teamEditorsTable).queryByText("Add a new editor"); expect(addEditorButton).not.toBeInTheDocument(); }); }); diff --git a/editor.planx.uk/src/pages/FlowEditor/components/Team/tests/TeamMembers.updateEditor.test.tsx b/editor.planx.uk/src/pages/FlowEditor/components/Team/tests/TeamMembers.updateEditor.test.tsx index 7cba480610..7527470705 100644 --- a/editor.planx.uk/src/pages/FlowEditor/components/Team/tests/TeamMembers.updateEditor.test.tsx +++ b/editor.planx.uk/src/pages/FlowEditor/components/Team/tests/TeamMembers.updateEditor.test.tsx @@ -24,7 +24,7 @@ describe("when a user presses 'edit button'", () => { const teamEditorsTable = screen.getByTestId("team-editors"); const addEditorButton = await within(teamEditorsTable).findByTestId( - "edit-button-0" + "edit-button-0", ); user.click(addEditorButton); @@ -64,7 +64,7 @@ describe("when a user deletes an input value", () => { const teamEditorsTable = screen.getByTestId("team-editors"); const addEditorButton = await within(teamEditorsTable).findByTestId( - "edit-button-0" + "edit-button-0", ); await user.click(addEditorButton); @@ -98,7 +98,7 @@ describe("when a user updates a field correctly", () => { const teamEditorsTable = screen.getByTestId("team-editors"); const addEditorButton = await within(teamEditorsTable).findByTestId( - "edit-button-0" + "edit-button-0", ); await user.click(addEditorButton); @@ -112,7 +112,7 @@ describe("when a user updates a field correctly", () => { it("updates the field", async () => { const firstNameInput = await screen.findByLabelText("First name"); expect(firstNameInput).toHaveDisplayValue( - mockTeamMembersData[1].firstName + "bo" + mockTeamMembersData[1].firstName + "bo", ); }); it("enables the update user button", async () => { @@ -130,7 +130,7 @@ describe("when a user correctly updates an Editor", () => { const teamEditorsTable = screen.getByTestId("team-editors"); const addEditorButton = await within(teamEditorsTable).findByTestId( - "edit-button-0" + "edit-button-0", ); await user.click(addEditorButton); @@ -150,7 +150,7 @@ describe("when a user correctly updates an Editor", () => { expect(within(membersTable).getByText(/Billbo/)).toBeInTheDocument(); }); expect( - await screen.findByText(/Successfully updated a user/) + await screen.findByText(/Successfully updated a user/), ).toBeInTheDocument(); }); it("closes the modal", async () => { @@ -160,14 +160,18 @@ describe("when a user correctly updates an Editor", () => { }); it("shows a success message", async () => { expect( - await screen.findByText(/Successfully updated a user/) + await screen.findByText(/Successfully updated a user/), ).toBeInTheDocument(); }); }); describe("'edit' button is hidden from Templates team", () => { - beforeEach(async() => { - useStore.setState({ teamMembers: mockTeamMembersData, user: mockPlatformAdminUser, teamSlug: "templates" }); + beforeEach(async () => { + useStore.setState({ + teamMembers: mockTeamMembersData, + user: mockPlatformAdminUser, + teamSlug: "templates", + }); }); it("hides the button on the Templates team", async () => { diff --git a/editor.planx.uk/src/pages/FlowEditor/components/forms/FormModal.tsx b/editor.planx.uk/src/pages/FlowEditor/components/forms/FormModal.tsx index 9e400b8267..91ccc8e191 100644 --- a/editor.planx.uk/src/pages/FlowEditor/components/forms/FormModal.tsx +++ b/editor.planx.uk/src/pages/FlowEditor/components/forms/FormModal.tsx @@ -9,8 +9,7 @@ import IconButton from "@mui/material/IconButton"; import { styled } from "@mui/material/styles"; import { ComponentType as TYPES } from "@opensystemslab/planx-core/types"; import { parseFormValues } from "@planx/components/shared"; -import ErrorFallback from "components/ErrorFallback"; -import { hasFeatureFlag } from "lib/featureFlags"; +import ErrorFallback from "components/Error/ErrorFallback"; import React from "react"; import { ErrorBoundary } from "react-error-boundary"; import { useNavigation } from "react-navi"; @@ -62,7 +61,7 @@ const NodeTypeSelect: React.FC<{ - {hasFeatureFlag("PAGE") && } + diff --git a/editor.planx.uk/src/pages/FlowEditor/index.tsx b/editor.planx.uk/src/pages/FlowEditor/index.tsx index 4dd0e5a36c..9e580d5800 100644 --- a/editor.planx.uk/src/pages/FlowEditor/index.tsx +++ b/editor.planx.uk/src/pages/FlowEditor/index.tsx @@ -20,7 +20,8 @@ const EditorContainer = styled(Box)(() => ({ })); const FlowEditor = () => { - const [ flow, ...breadcrumbs ] = useCurrentRoute().url.pathname.split("/").at(-1)?.split(",") || []; + const [flow, ...breadcrumbs] = + useCurrentRoute().url.pathname.split("/").at(-1)?.split(",") || []; const scrollContainerRef = useRef(null); useScrollControlsAndRememberPosition(scrollContainerRef); diff --git a/editor.planx.uk/src/pages/Preview/Questions.tsx b/editor.planx.uk/src/pages/Preview/Questions.tsx index f287939af9..1819922734 100644 --- a/editor.planx.uk/src/pages/Preview/Questions.tsx +++ b/editor.planx.uk/src/pages/Preview/Questions.tsx @@ -12,7 +12,7 @@ import React, { useCallback, useEffect, useMemo, useState } from "react"; import { ErrorBoundary } from "react-error-boundary"; import { ApplicationPath, Session } from "types"; -import ErrorFallback from "../../components/ErrorFallback"; +import ErrorFallback from "../../components/Error/ErrorFallback"; import { useStore } from "../FlowEditor/lib/store"; import Node, { HandleSubmit } from "./Node"; diff --git a/editor.planx.uk/src/pages/layout/FlowEditorLayout.tsx b/editor.planx.uk/src/pages/layout/FlowEditorLayout.tsx index 327d8da975..d1d9e6eada 100644 --- a/editor.planx.uk/src/pages/layout/FlowEditorLayout.tsx +++ b/editor.planx.uk/src/pages/layout/FlowEditorLayout.tsx @@ -1,4 +1,4 @@ -import ErrorFallback from "components/ErrorFallback"; +import ErrorFallback from "components/Error/ErrorFallback"; import FlowEditor from "pages/FlowEditor"; import React, { PropsWithChildren } from "react"; import { ErrorBoundary } from "react-error-boundary"; diff --git a/editor.planx.uk/src/pages/layout/PublicLayout.tsx b/editor.planx.uk/src/pages/layout/PublicLayout.tsx index 23df5c9232..f7b21a23b1 100644 --- a/editor.planx.uk/src/pages/layout/PublicLayout.tsx +++ b/editor.planx.uk/src/pages/layout/PublicLayout.tsx @@ -6,7 +6,7 @@ import { ThemeProvider, } from "@mui/material/styles"; import Typography from "@mui/material/Typography"; -import ErrorFallback from "components/ErrorFallback"; +import ErrorFallback from "components/Error/ErrorFallback"; import Feedback from "components/Feedback"; import { useStore } from "pages/FlowEditor/lib/store"; import React, { PropsWithChildren } from "react"; diff --git a/editor.planx.uk/src/routes/team.tsx b/editor.planx.uk/src/routes/team.tsx index 02e14e96b4..864d158a59 100644 --- a/editor.planx.uk/src/routes/team.tsx +++ b/editor.planx.uk/src/routes/team.tsx @@ -85,7 +85,9 @@ const routes = compose( "/:flow/service": setFlowAndLazyLoad(() => import("./serviceSettings")), - "/:flow/submissions-log": setFlowAndLazyLoad(() => import("./submissionsLog")), + "/:flow/submissions-log": setFlowAndLazyLoad( + () => import("./submissionsLog"), + ), "/members": lazy(() => import("./teamMembers")), "/design": compose( diff --git a/infrastructure/application/index.ts b/infrastructure/application/index.ts index 77d2fca188..0bcaf4dba5 100644 --- a/infrastructure/application/index.ts +++ b/infrastructure/application/index.ts @@ -184,7 +184,7 @@ export = async () => { }), container: { // if changing, also check docker-compose.yml - image: "metabase/metabase:v0.50.22", + image: "metabase/metabase:v0.50.26", portMappings: [metabaseListenerHttp], // When changing `memory`, also update `JAVA_OPTS` below memory: 4096 /*MB*/,