From 6697d93cfc7558de553b8ba2781f04fe83443113 Mon Sep 17 00:00:00 2001 From: Albin Henriksson <albhe428@student.liu.se> Date: Thu, 6 May 2021 20:06:20 +0000 Subject: [PATCH] Resolve "Increase client test coverage" --- .gitlab/client.gitlab-ci.yml | 2 +- .vscode/tasks.json | 209 +++++++++--------- client/package-lock.json | 204 +++++++++++++++++ client/package.json | 7 +- client/public/logo192.png | Bin 5347 -> 0 bytes client/public/logo512.png | Bin 9664 -> 0 bytes client/public/manifest.json | 14 +- client/src/actions/competitionLogin.test.ts | 76 +++++++ client/src/actions/presentation.test.ts | 4 +- client/src/actions/presentation.ts | 2 +- client/src/components/TestConnection.tsx | 18 -- client/src/e2e/AdminPage.test.tsx | 176 +++++++++++++++ client/src/e2e/LoginPage.test.tsx | 70 ++++++ client/src/e2e/TestingConstants.ts | 6 + client/src/index.tsx | 6 - client/src/logo.svg | 1 - .../src/middleware/Middleware_Explanation.txt | 6 - client/src/pages/admin/AdminPage.tsx | 1 + .../admin/competitions/AddCompetition.tsx | 11 +- .../admin/competitions/CompetitionManager.tsx | 13 +- .../dashboard/components/CurrentUser.tsx | 4 +- client/src/pages/admin/regions/AddRegion.tsx | 4 +- client/src/pages/admin/regions/Regions.tsx | 6 +- client/src/pages/admin/users/AddUser.tsx | 21 +- client/src/pages/admin/users/EditUser.tsx | 7 +- .../pages/admin/users/ResponsiveDialog.tsx | 53 ----- .../src/pages/login/components/AdminLogin.tsx | 3 + .../components/BackgroundImageSelect.test.tsx | 16 ++ .../components/CheckboxComponent.tsx | 31 --- .../components/RndComponent.test.tsx | 17 ++ .../components/TextComponentEdit.test.tsx | 17 ++ .../slideSettingsComponents/Images.test.tsx | 14 ++ .../slideSettingsComponents/Images.tsx | 10 +- .../Instructions.test.tsx | 14 ++ .../slideSettingsComponents/Instructions.tsx | 4 +- .../MultipleChoiceAlternatives.test.tsx | 14 ++ .../MultipleChoiceAlternatives.tsx | 43 ++-- .../QuestionSettings.test.tsx | 14 ++ .../QuestionSettings.tsx | 6 +- .../SlideType.test.tsx | 14 ++ .../slideSettingsComponents/SlideType.tsx | 6 +- .../slideSettingsComponents/Texts.test.tsx | 14 ++ .../slideSettingsComponents/Timer.test.tsx | 14 ++ .../src/pages/views/components/SocketTest.tsx | 61 ----- client/src/reducers/allReducers.ts | 2 - client/src/reducers/mediaReducer.ts | 41 ---- client/src/reportWebVitals.ts | 15 -- .../checkAuthenticationCompetition.test.ts | 79 +++++++ .../utils/checkAuthenticationCompetition.ts | 2 +- client/src/utils/renderSlideIcon.test.tsx | 63 ++++++ 50 files changed, 1027 insertions(+), 408 deletions(-) delete mode 100644 client/public/logo192.png delete mode 100644 client/public/logo512.png create mode 100644 client/src/actions/competitionLogin.test.ts delete mode 100644 client/src/components/TestConnection.tsx create mode 100644 client/src/e2e/AdminPage.test.tsx create mode 100644 client/src/e2e/LoginPage.test.tsx create mode 100644 client/src/e2e/TestingConstants.ts delete mode 100644 client/src/logo.svg delete mode 100644 client/src/middleware/Middleware_Explanation.txt delete mode 100644 client/src/pages/admin/users/ResponsiveDialog.tsx create mode 100644 client/src/pages/presentationEditor/components/BackgroundImageSelect.test.tsx delete mode 100644 client/src/pages/presentationEditor/components/CheckboxComponent.tsx create mode 100644 client/src/pages/presentationEditor/components/RndComponent.test.tsx create mode 100644 client/src/pages/presentationEditor/components/TextComponentEdit.test.tsx create mode 100644 client/src/pages/presentationEditor/components/slideSettingsComponents/Images.test.tsx create mode 100644 client/src/pages/presentationEditor/components/slideSettingsComponents/Instructions.test.tsx create mode 100644 client/src/pages/presentationEditor/components/slideSettingsComponents/MultipleChoiceAlternatives.test.tsx create mode 100644 client/src/pages/presentationEditor/components/slideSettingsComponents/QuestionSettings.test.tsx create mode 100644 client/src/pages/presentationEditor/components/slideSettingsComponents/SlideType.test.tsx create mode 100644 client/src/pages/presentationEditor/components/slideSettingsComponents/Texts.test.tsx create mode 100644 client/src/pages/presentationEditor/components/slideSettingsComponents/Timer.test.tsx delete mode 100644 client/src/pages/views/components/SocketTest.tsx delete mode 100644 client/src/reducers/mediaReducer.ts delete mode 100644 client/src/reportWebVitals.ts create mode 100644 client/src/utils/checkAuthenticationCompetition.test.ts create mode 100644 client/src/utils/renderSlideIcon.test.tsx diff --git a/.gitlab/client.gitlab-ci.yml b/.gitlab/client.gitlab-ci.yml index f35e73aa..a64f1bd8 100644 --- a/.gitlab/client.gitlab-ci.yml +++ b/.gitlab/client.gitlab-ci.yml @@ -46,7 +46,7 @@ client:test: - merge_requests script: - cd client - - npm run test:coverage + - npm run unit-test:coverage coverage: /All files\s*\|\s*([\d\.]+)/ artifacts: paths: diff --git a/.vscode/tasks.json b/.vscode/tasks.json index 8b7286d1..45f88f11 100644 --- a/.vscode/tasks.json +++ b/.vscode/tasks.json @@ -1,102 +1,109 @@ { - "version": "2.0.0", - "tasks": [ - { - "label": "Start client", - "type": "npm", - "script": "start", - "path": "client/", - "group": "build", - "problemMatcher": [], - "presentation": { - "group": "Client/Server" - } - }, - { - "label": "Test client", - "type": "npm", - "script": "test:coverage:html", - "path": "client/", - "group": "build", - "problemMatcher": [], - }, - { - "label": "Open client coverage", - "type": "shell", - "command": "start ./output/coverage/jest/index.html", - "problemMatcher": [], - "options": { - "cwd": "${workspaceFolder}/client" - }, - }, - { - "label": "Start server", - "type": "shell", - "group": "build", - "command": "env/Scripts/python main.py", - "problemMatcher": [], - "options": { - "cwd": "${workspaceFolder}/server" - }, - "presentation": { - "group": "Client/Server" - } - }, - { - "label": "Test server", - "type": "shell", - "group": "build", - "command": "env/Scripts/pytest.exe --cov-report html --cov app tests/", - "problemMatcher": [], - "options": { - "cwd": "${workspaceFolder}/server" - }, - }, - { - "label": "Populate database", - "type": "shell", - "group": "build", - "command": "env/Scripts/python populate.py", - "problemMatcher": [], - "options": { - "cwd": "${workspaceFolder}/server" - }, - }, - { - "label": "Open server coverage", - "type": "shell", - "command": "start ./htmlcov/index.html", - "problemMatcher": [], - "options": { - "cwd": "${workspaceFolder}/server" - }, - }, - { - "label": "Generate server documentation", - "type": "shell", - "command": "../env/Scripts/activate; ./make html", - "problemMatcher": [], - "options": { - "cwd": "${workspaceFolder}/server/docs" - }, - }, - { - "label": "Open server documentation", - "type": "shell", - "command": "start index.html", - "problemMatcher": [], - "options": { - "cwd": "${workspaceFolder}/server/docs/build/html" - }, - }, - { - "label": "Start client and server", - "group": "build", - "dependsOn": [ - "Start server", - "Start client" - ], - "problemMatcher": [] - } - ] -} \ No newline at end of file + "version": "2.0.0", + "tasks": [ + { + "label": "Start client", + "type": "npm", + "script": "start", + "path": "client/", + "group": "build", + "problemMatcher": [], + "presentation": { + "group": "Client/Server" + } + }, + { + "label": "Unit tests", + "type": "npm", + "script": "unit-test:coverage:html", + "path": "client/", + "group": "build", + "detail": "Run unit tests on client", + "problemMatcher": [] + }, + { + "label": "Run e2e tests", + "type": "npm", + "script": "e2e-test", + "path": "client/", + "group": "build", + "problemMatcher": [], + "detail": "Make sure client and server is running before executing." + }, + { + "label": "Open client coverage", + "type": "shell", + "command": "start ./output/coverage/jest/index.html", + "problemMatcher": [], + "options": { + "cwd": "${workspaceFolder}/client" + } + }, + { + "label": "Start server", + "type": "shell", + "group": "build", + "command": "env/Scripts/python main.py", + "problemMatcher": [], + "options": { + "cwd": "${workspaceFolder}/server" + }, + "presentation": { + "group": "Client/Server" + } + }, + { + "label": "Test server", + "type": "shell", + "group": "build", + "command": "env/Scripts/pytest.exe --cov-report html --cov app tests/", + "problemMatcher": [], + "options": { + "cwd": "${workspaceFolder}/server" + } + }, + { + "label": "Populate database", + "type": "shell", + "group": "build", + "command": "env/Scripts/python populate.py", + "problemMatcher": [], + "options": { + "cwd": "${workspaceFolder}/server" + } + }, + { + "label": "Open server coverage", + "type": "shell", + "command": "start ./htmlcov/index.html", + "problemMatcher": [], + "options": { + "cwd": "${workspaceFolder}/server" + } + }, + { + "label": "Generate server documentation", + "type": "shell", + "command": "../env/Scripts/activate; ./make html", + "problemMatcher": [], + "options": { + "cwd": "${workspaceFolder}/server/docs" + } + }, + { + "label": "Open server documentation", + "type": "shell", + "command": "start index.html", + "problemMatcher": [], + "options": { + "cwd": "${workspaceFolder}/server/docs/build/html" + } + }, + { + "label": "Start client and server", + "group": "build", + "dependsOn": ["Start server", "Start client"], + "problemMatcher": [] + } + ] +} diff --git a/client/package-lock.json b/client/package-lock.json index 2655ad2e..d857520c 100644 --- a/client/package-lock.json +++ b/client/package-lock.json @@ -2705,6 +2705,15 @@ "resolved": "https://registry.npmjs.org/@types/yargs-parser/-/yargs-parser-20.2.0.tgz", "integrity": "sha512-37RSHht+gzzgYeobbG+KWryeAW8J33Nhr69cjTqSYymXVZEN9NbRYWoYlRtDhHKPVT1FyNKwaTPC1NynKZpzRA==" }, + "@types/yauzl": { + "version": "2.9.1", + "resolved": "https://registry.npmjs.org/@types/yauzl/-/yauzl-2.9.1.tgz", + "integrity": "sha512-A1b8SU4D10uoPjwb0lnHmmu8wZhR9d+9o2PKBQT2jU5YPTKsxac6M2qGAdY7VcL+dHHhARVUDmeg0rOrcd9EjA==", + "optional": true, + "requires": { + "@types/node": "*" + } + }, "@typescript-eslint/eslint-plugin": { "version": "4.2.0", "resolved": "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-4.2.0.tgz", @@ -3134,6 +3143,14 @@ "regex-parser": "^2.2.11" } }, + "agent-base": { + "version": "6.0.2", + "resolved": "https://registry.npmjs.org/agent-base/-/agent-base-6.0.2.tgz", + "integrity": "sha512-RZNwNclF7+MS/8bDg70amg32dyeZGZxiDuQmZxKLAlQjr3jGyLx+4Kkk58UO7D2QdgFIQCovuSuZESne6RG6XQ==", + "requires": { + "debug": "4" + } + }, "aggregate-error": { "version": "3.1.0", "resolved": "https://registry.npmjs.org/aggregate-error/-/aggregate-error-3.1.0.tgz", @@ -4094,6 +4111,27 @@ "integrity": "sha512-jDctJ/IVQbZoJykoeHbhXpOlNBqGNcwXJKJog42E5HDPUwQTSdjCHdihjj0DlnheQ7blbT6dHOafNAiS8ooQKA==", "optional": true }, + "bl": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/bl/-/bl-4.1.0.tgz", + "integrity": "sha512-1W07cM9gS6DcLperZfFSj+bWLtaPGSOHWhPiGzXmvVJbRLdG82sH/Kn8EtW1VqWVA54AKf2h5k5BbnIbwF3h6w==", + "requires": { + "buffer": "^5.5.0", + "inherits": "^2.0.4", + "readable-stream": "^3.4.0" + }, + "dependencies": { + "buffer": { + "version": "5.7.1", + "resolved": "https://registry.npmjs.org/buffer/-/buffer-5.7.1.tgz", + "integrity": "sha512-EHcyIPBQ4BSGlvjB16k5KgAJ27CIsHY/2JBmCRReo48y9rQ3MaUzWX3KVlBa4U7MyX02HdVj0K7C3WaB3ju7FQ==", + "requires": { + "base64-js": "^1.3.1", + "ieee754": "^1.1.13" + } + } + } + }, "bluebird": { "version": "3.7.2", "resolved": "https://registry.npmjs.org/bluebird/-/bluebird-3.7.2.tgz", @@ -4295,6 +4333,11 @@ "isarray": "^1.0.0" } }, + "buffer-crc32": { + "version": "0.2.13", + "resolved": "https://registry.npmjs.org/buffer-crc32/-/buffer-crc32-0.2.13.tgz", + "integrity": "sha1-DTM+PwDqxQqhRUq9MO+MKl2ackI=" + }, "buffer-from": { "version": "1.1.1", "resolved": "https://registry.npmjs.org/buffer-from/-/buffer-from-1.1.1.tgz", @@ -5805,6 +5848,11 @@ } } }, + "devtools-protocol": { + "version": "0.0.869402", + "resolved": "https://registry.npmjs.org/devtools-protocol/-/devtools-protocol-0.0.869402.tgz", + "integrity": "sha512-VvlVYY+VDJe639yHs5PHISzdWTLL3Aw8rO4cvUtwvoxFd6FHbE4OpHHcde52M6096uYYazAmd4l0o5VuFRO2WA==" + }, "diff-sequences": { "version": "26.6.2", "resolved": "https://registry.npmjs.org/diff-sequences/-/diff-sequences-26.6.2.tgz", @@ -7357,6 +7405,27 @@ } } }, + "extract-zip": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/extract-zip/-/extract-zip-2.0.1.tgz", + "integrity": "sha512-GDhU9ntwuKyGXdZBUgTIe+vXnWj0fppUEtMDL0+idd5Sta8TGpHssn/eusA9mrPr9qNDym6SxAYZjNvCn/9RBg==", + "requires": { + "@types/yauzl": "^2.9.1", + "debug": "^4.1.1", + "get-stream": "^5.1.0", + "yauzl": "^2.10.0" + }, + "dependencies": { + "get-stream": { + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/get-stream/-/get-stream-5.2.0.tgz", + "integrity": "sha512-nBF+F1rAZVCu/p7rjzgA+Yb4lfYXrpl7a6VmJrU8wF9I1CKvP/QwPNZHnOlwbTkY6dvtFIzFMSyQXbLoTQPRpA==", + "requires": { + "pump": "^3.0.0" + } + } + } + }, "extsprintf": { "version": "1.3.0", "resolved": "https://registry.npmjs.org/extsprintf/-/extsprintf-1.3.0.tgz", @@ -7425,6 +7494,14 @@ "bser": "2.1.1" } }, + "fd-slicer": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/fd-slicer/-/fd-slicer-1.1.0.tgz", + "integrity": "sha1-JcfInLH5B3+IkbvmHY85Dq4lbx4=", + "requires": { + "pend": "~1.2.0" + } + }, "figgy-pudding": { "version": "3.5.2", "resolved": "https://registry.npmjs.org/figgy-pudding/-/figgy-pudding-3.5.2.tgz", @@ -7785,6 +7862,11 @@ } } }, + "fs-constants": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/fs-constants/-/fs-constants-1.0.0.tgz", + "integrity": "sha512-y6OAwoSIf7FyjMIv94u+b5rdheZEjzR63GTyZJm5qh4Bi+2YgwLCcI/fPFZkL5PSixOt6ZNKm+w+Hfp/Bciwow==" + }, "fs-extra": { "version": "9.1.0", "resolved": "https://registry.npmjs.org/fs-extra/-/fs-extra-9.1.0.tgz", @@ -8497,6 +8579,15 @@ "resolved": "https://registry.npmjs.org/https-browserify/-/https-browserify-1.0.0.tgz", "integrity": "sha1-7AbBDgo0wPL68Zn3/X/Hj//QPHM=" }, + "https-proxy-agent": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/https-proxy-agent/-/https-proxy-agent-5.0.0.tgz", + "integrity": "sha512-EkYm5BcKUGiduxzSt3Eppko+PiNWNEpa4ySk9vTC6wDsQJW9rHSa+UhGNJoRYp7bz6Ht1eaRIa6QaJqO5rCFbA==", + "requires": { + "agent-base": "6", + "debug": "4" + } + }, "human-signals": { "version": "1.1.1", "resolved": "https://registry.npmjs.org/human-signals/-/human-signals-1.1.1.tgz", @@ -11349,6 +11440,11 @@ "minimist": "^1.2.5" } }, + "mkdirp-classic": { + "version": "0.5.3", + "resolved": "https://registry.npmjs.org/mkdirp-classic/-/mkdirp-classic-0.5.3.tgz", + "integrity": "sha512-gKLcREMhtuZRwRAfqP3RFW+TK4JqApVBtOIftVgjuABpAtpxhPGaDcfvbhNvD0B8iD1oUr/txX35NjcaY6Ns/A==" + }, "moo": { "version": "0.5.1", "resolved": "https://registry.npmjs.org/moo/-/moo-0.5.1.tgz", @@ -11494,6 +11590,11 @@ } } }, + "node-fetch": { + "version": "2.6.1", + "resolved": "https://registry.npmjs.org/node-fetch/-/node-fetch-2.6.1.tgz", + "integrity": "sha512-V4aYg89jEoVRxRb2fJdAg8FHvI7cEyYdVAh94HH0UIK8oJxUfkjlDQN9RbMx+bEjP7+ggMiFRprSti032Oipxw==" + }, "node-forge": { "version": "0.10.0", "resolved": "https://registry.npmjs.org/node-forge/-/node-forge-0.10.0.tgz", @@ -12133,6 +12234,11 @@ "sha.js": "^2.4.8" } }, + "pend": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/pend/-/pend-1.2.0.tgz", + "integrity": "sha1-elfrVQpng/kRUzH89GY9XI4AelA=" + }, "performance-now": { "version": "2.1.0", "resolved": "https://registry.npmjs.org/performance-now/-/performance-now-2.1.0.tgz", @@ -13434,6 +13540,11 @@ "ipaddr.js": "1.9.1" } }, + "proxy-from-env": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/proxy-from-env/-/proxy-from-env-1.1.0.tgz", + "integrity": "sha512-D+zkORCbA9f1tdWRK0RaCR3GPv50cMxcrz4X8k5LTSUD1Dkw47mKJEZQNunItRTkWwgtaUSo1RVFRIG9ZXiFYg==" + }, "prr": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/prr/-/prr-1.0.1.tgz", @@ -13499,6 +13610,35 @@ "resolved": "https://registry.npmjs.org/punycode/-/punycode-2.1.1.tgz", "integrity": "sha512-XRsRjdf+j5ml+y/6GKHPZbrF/8p2Yga0JPtdqTIY2Xe5ohJPD9saDJJLPvp9+NSBprVvevdXZybnj2cv8OEd0A==" }, + "puppeteer": { + "version": "9.1.1", + "resolved": "https://registry.npmjs.org/puppeteer/-/puppeteer-9.1.1.tgz", + "integrity": "sha512-W+nOulP2tYd/ZG99WuZC/I5ljjQQ7EUw/jQGcIb9eu8mDlZxNY2SgcJXTLG9h5gRvqA3uJOe4hZXYsd3EqioMw==", + "requires": { + "debug": "^4.1.0", + "devtools-protocol": "0.0.869402", + "extract-zip": "^2.0.0", + "https-proxy-agent": "^5.0.0", + "node-fetch": "^2.6.1", + "pkg-dir": "^4.2.0", + "progress": "^2.0.1", + "proxy-from-env": "^1.1.0", + "rimraf": "^3.0.2", + "tar-fs": "^2.0.0", + "unbzip2-stream": "^1.3.3", + "ws": "^7.2.3" + }, + "dependencies": { + "pkg-dir": { + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/pkg-dir/-/pkg-dir-4.2.0.tgz", + "integrity": "sha512-HRDzbaKjC+AOWVXxAU/x54COGeIv9eb+6CkDSQoNTt4XyWoIJvuPsXizxu/Fr23EiekbtZwmh1IcIG/l/a10GQ==", + "requires": { + "find-up": "^4.0.0" + } + } + } + }, "q": { "version": "1.5.1", "resolved": "https://registry.npmjs.org/q/-/q-1.5.1.tgz", @@ -15976,6 +16116,36 @@ } } }, + "tar-fs": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/tar-fs/-/tar-fs-2.1.1.tgz", + "integrity": "sha512-V0r2Y9scmbDRLCNex/+hYzvp/zyYjvFbHPNgVTKfQvVrb6guiE/fxP+XblDNR011utopbkex2nM4dHNV6GDsng==", + "requires": { + "chownr": "^1.1.1", + "mkdirp-classic": "^0.5.2", + "pump": "^3.0.0", + "tar-stream": "^2.1.4" + }, + "dependencies": { + "chownr": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/chownr/-/chownr-1.1.4.tgz", + "integrity": "sha512-jJ0bqzaylmJtVnNgzTeSOs8DPavpbYgEr/b0YL8/2GO3xJEhInFmhKMUnEJQjZumK7KXGFhUy89PrsJWlakBVg==" + } + } + }, + "tar-stream": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/tar-stream/-/tar-stream-2.2.0.tgz", + "integrity": "sha512-ujeqbceABgwMZxEJnk2HDY2DlnUZ+9oEcb1KzTVfYHio0UE6dG71n60d8D2I4qNvleWrrXpmjpt7vZeF1LnMZQ==", + "requires": { + "bl": "^4.0.3", + "end-of-stream": "^1.4.1", + "fs-constants": "^1.0.0", + "inherits": "^2.0.3", + "readable-stream": "^3.1.1" + } + }, "temp-dir": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/temp-dir/-/temp-dir-1.0.0.tgz", @@ -16143,6 +16313,11 @@ "resolved": "https://registry.npmjs.org/throat/-/throat-5.0.0.tgz", "integrity": "sha512-fcwX4mndzpLQKBS1DVYhGAcYaYt7vsHNIvQV+WXMvnow5cgjPphq5CaayLaGsjRdSCKZFNGt7/GYAuXaNOiYCA==" }, + "through": { + "version": "2.3.8", + "resolved": "https://registry.npmjs.org/through/-/through-2.3.8.tgz", + "integrity": "sha1-DdTJ/6q8NXlgsbckEV1+Doai4fU=" + }, "through2": { "version": "2.0.5", "resolved": "https://registry.npmjs.org/through2/-/through2-2.0.5.tgz", @@ -16419,6 +16594,26 @@ "which-boxed-primitive": "^1.0.1" } }, + "unbzip2-stream": { + "version": "1.4.3", + "resolved": "https://registry.npmjs.org/unbzip2-stream/-/unbzip2-stream-1.4.3.tgz", + "integrity": "sha512-mlExGW4w71ebDJviH16lQLtZS32VKqsSfk80GCfUlwT/4/hNRFsoscrF/c++9xinkMzECL1uL9DDwXqFWkruPg==", + "requires": { + "buffer": "^5.2.1", + "through": "^2.3.8" + }, + "dependencies": { + "buffer": { + "version": "5.7.1", + "resolved": "https://registry.npmjs.org/buffer/-/buffer-5.7.1.tgz", + "integrity": "sha512-EHcyIPBQ4BSGlvjB16k5KgAJ27CIsHY/2JBmCRReo48y9rQ3MaUzWX3KVlBa4U7MyX02HdVj0K7C3WaB3ju7FQ==", + "requires": { + "base64-js": "^1.3.1", + "ieee754": "^1.1.13" + } + } + } + }, "unicode-canonical-property-names-ecmascript": { "version": "1.0.4", "resolved": "https://registry.npmjs.org/unicode-canonical-property-names-ecmascript/-/unicode-canonical-property-names-ecmascript-1.0.4.tgz", @@ -18280,6 +18475,15 @@ } } }, + "yauzl": { + "version": "2.10.0", + "resolved": "https://registry.npmjs.org/yauzl/-/yauzl-2.10.0.tgz", + "integrity": "sha1-x+sXyT4RLLEIb6bY5R+wZnt5pfk=", + "requires": { + "buffer-crc32": "~0.2.3", + "fd-slicer": "~1.1.0" + } + }, "yeast": { "version": "0.1.2", "resolved": "https://registry.npmjs.org/yeast/-/yeast-0.1.2.tgz", diff --git a/client/package.json b/client/package.json index a0c8b7f5..7426424b 100644 --- a/client/package.json +++ b/client/package.json @@ -22,6 +22,7 @@ "axios": "^0.21.1", "formik": "^2.2.6", "jwt-decode": "^3.1.2", + "puppeteer": "^9.1.1", "react": "^17.0.1", "react-axios": "^2.0.4", "react-beautiful-dnd": "^13.1.0", @@ -70,8 +71,9 @@ "test": "react-scripts test", "eject": "react-scripts eject", "lint": "eslint \"./src/**/*.{ts,tsx}\"", - "test:coverage": "react-scripts test --coverage --coverageDirectory=output/coverage/jest", - "test:coverage:html": "npm test -- --coverage --watchAll=false --coverageDirectory=output/coverage/jest" + "unit-test:coverage": "react-scripts test --coverage --testPathIgnorePatterns=src/e2e --coverageDirectory=output/coverage/jest", + "unit-test:coverage:html": "npm test -- --testPathIgnorePatterns=src/e2e --coverage --watchAll=false --coverageDirectory=output/coverage/jest", + "e2e-test": "npm test -- --testPathPattern=src/e2e" }, "browserslist": { "production": [ @@ -89,6 +91,7 @@ "collectCoverageFrom": [ "src/**/*.{tsx,ts}", "!src/index.tsx", + "!src/e2e/*", "!src/reportWebVitals.ts", "!src/components/TestConnection.tsx" ], diff --git a/client/public/logo192.png b/client/public/logo192.png deleted file mode 100644 index fc44b0a3796c0e0a64c3d858ca038bd4570465d9..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 5347 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 X@eDJUQo;Ye2mwlRs<JiGX2Jghdw)}T diff --git a/client/public/logo512.png b/client/public/logo512.png deleted file mode 100644 index a4e47a6545bc15971f8f63fba70e4013df88a664..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 9664 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 b{|{}U^kRn2wo}j7|0ATu<;8xA7zX}7|B6mN diff --git a/client/public/manifest.json b/client/public/manifest.json index 080d6c77..73371289 100644 --- a/client/public/manifest.json +++ b/client/public/manifest.json @@ -1,21 +1,11 @@ { - "short_name": "React App", - "name": "Create React App Sample", + "short_name": "Teknikåttan", + "name": "Teknikåttan Scoring System", "icons": [ { "src": "favicon.ico", "sizes": "64x64 32x32 24x24 16x16", "type": "image/x-icon" - }, - { - "src": "logo192.png", - "type": "image/png", - "sizes": "192x192" - }, - { - "src": "logo512.png", - "type": "image/png", - "sizes": "512x512" } ], "start_url": ".", diff --git a/client/src/actions/competitionLogin.test.ts b/client/src/actions/competitionLogin.test.ts new file mode 100644 index 00000000..bf1825e7 --- /dev/null +++ b/client/src/actions/competitionLogin.test.ts @@ -0,0 +1,76 @@ +import mockedAxios from 'axios' +import expect from 'expect' // You can use any testing library +import { createMemoryHistory } from 'history' +import configureMockStore from 'redux-mock-store' +import thunk from 'redux-thunk' +import { loginCompetition, logoutCompetition } from './competitionLogin' +import Types from './types' + +const middlewares = [thunk] +const mockStore = configureMockStore(middlewares) + +it('dispatches correct actions when logging into competition', async () => { + const compRes: any = { + data: { + id: 5, + slides: [], + }, + } + const compLoginDataRes: any = { + data: { + access_token: 'TEST_ACCESS_TOKEN', + competition_id: 'test_name', + team_id: 'test_team', + view: 'test_view', + }, + } + ;(mockedAxios.post as jest.Mock).mockImplementation(() => { + return Promise.resolve(compLoginDataRes) + }) + ;(mockedAxios.get as jest.Mock).mockImplementation(() => { + return Promise.resolve(compRes) + }) + const expectedActions = [ + { type: Types.LOADING_COMPETITION_LOGIN }, + { type: Types.CLEAR_COMPETITION_LOGIN_ERRORS }, + { + type: Types.SET_COMPETITION_LOGIN_DATA, + payload: { + competition_id: compLoginDataRes.data.competition_id, + team_id: compLoginDataRes.data.team_id, + view: compLoginDataRes.data.view, + }, + }, + { type: Types.SET_PRESENTATION_COMPETITION, payload: compRes.data }, + ] + const store = mockStore({}) + const history = createMemoryHistory() + await loginCompetition('code', history, true)(store.dispatch, store.getState as any) + expect(store.getActions()).toEqual(expectedActions) +}) + +it('dispatches correct action when logging out from competition', async () => { + ;(mockedAxios.post as jest.Mock).mockImplementation(() => { + return Promise.resolve({ data: {} }) + }) + const store = mockStore({}) + await logoutCompetition()(store.dispatch) + expect(store.getActions()).toEqual([{ type: Types.SET_COMPETITION_LOGIN_UNAUTHENTICATED }]) +}) + +it('dispatches correct action when failing to log in user', async () => { + console.log = jest.fn() + const errorMessage = 'getting teams failed' + ;(mockedAxios.post as jest.Mock).mockImplementation(() => { + return Promise.reject({ response: { data: errorMessage } }) + }) + const store = mockStore({}) + const history = createMemoryHistory() + const expectedActions = [ + { type: Types.LOADING_COMPETITION_LOGIN }, + { type: Types.SET_COMPETITION_LOGIN_ERRORS, payload: errorMessage }, + ] + await loginCompetition('code', history, true)(store.dispatch, store.getState as any) + expect(store.getActions()).toEqual(expectedActions) + expect(console.log).toHaveBeenCalled() +}) diff --git a/client/src/actions/presentation.test.ts b/client/src/actions/presentation.test.ts index d95d9db7..095a3529 100644 --- a/client/src/actions/presentation.test.ts +++ b/client/src/actions/presentation.test.ts @@ -20,13 +20,13 @@ it('dispatches no actions when failing to get competitions', async () => { return Promise.reject(new Error('getting competitions failed')) }) const store = mockStore({ competitions: { filterParams: [] } }) - await getPresentationCompetition('0')(store.dispatch) + await getPresentationCompetition('0')(store.dispatch, store.getState as any) expect(store.getActions()).toEqual([]) expect(console.log).toHaveBeenCalled() }) it('dispatches correct actions when setting slide', () => { - const testSlide: Slide = { competition_id: 0, id: 5, order: 5, timer: 20, title: '', background_image_id: 0 } + const testSlide: Slide = { competition_id: 0, id: 5, order: 5, timer: 20, title: '', background_image: undefined } const expectedActions = [{ type: Types.SET_PRESENTATION_SLIDE, payload: testSlide }] const store = mockStore({}) setCurrentSlide(testSlide)(store.dispatch) diff --git a/client/src/actions/presentation.ts b/client/src/actions/presentation.ts index 90b728c5..32e4d0a4 100644 --- a/client/src/actions/presentation.ts +++ b/client/src/actions/presentation.ts @@ -17,7 +17,7 @@ export const getPresentationCompetition = (id: string) => async (dispatch: AppDi type: Types.SET_PRESENTATION_COMPETITION, payload: res.data, }) - if (getState().presentation.slide.id === -1 && res.data.slides[0]) { + if (getState().presentation?.slide.id === -1 && res.data?.slides[0]) { setCurrentSlideByOrder(0)(dispatch) } }) diff --git a/client/src/components/TestConnection.tsx b/client/src/components/TestConnection.tsx deleted file mode 100644 index 7fa4aaea..00000000 --- a/client/src/components/TestConnection.tsx +++ /dev/null @@ -1,18 +0,0 @@ -import axios from 'axios' -import React, { useEffect, useState } from 'react' - -interface Message { - message: string -} - -const TestConnection: React.FC = () => { - const [currentMessage, setCurrentMessage] = useState<Message>() - useEffect(() => { - axios.get<Message>('users/test').then((response) => { - setCurrentMessage(response.data) - }) - }, []) - return <p>Connection with server is: {currentMessage?.message}</p> -} - -export default TestConnection diff --git a/client/src/e2e/AdminPage.test.tsx b/client/src/e2e/AdminPage.test.tsx new file mode 100644 index 00000000..c9c67580 --- /dev/null +++ b/client/src/e2e/AdminPage.test.tsx @@ -0,0 +1,176 @@ +import puppeteer from 'puppeteer' +import { CLIENT_URL, DEVTOOLS_ENABLED, HEADLESS_ENABLED, SLOW_DOWN_FACTOR } from './TestingConstants' + +describe('Admin page', () => { + const userEmailSelector = '[data-testid="userEmail"]' + const buttonSelector = '[data-testid="submit"]' + const emailSelector = '[data-testid="email"]' + const passwordSelector = '[data-testid="password"]' + let browser: puppeteer.Browser + let page: puppeteer.Page + jest.setTimeout(10000) + beforeEach(async () => { + // Set up testing environment + browser = await puppeteer.launch({ + headless: HEADLESS_ENABLED, + devtools: DEVTOOLS_ENABLED, + slowMo: SLOW_DOWN_FACTOR, + }) + page = await browser.newPage() + + //Navigate to login screen and log in + await page.goto(CLIENT_URL) + await page.waitForSelector('.MuiFormControl-root') + await page.click(emailSelector) + await page.keyboard.type('admin@test.se') + await page.click(passwordSelector) + await page.keyboard.type('password') + await page.click(buttonSelector) + await page.waitForTimeout(2000) + }) + + afterEach(async () => { + await browser.close() + }) + + it('Should show correct email on welcome screen', async () => { + const AdminTitle = await page.evaluate((sel) => { + return document.querySelector(sel).innerText + }, userEmailSelector) + expect(AdminTitle).toEqual('Email: admin@test.se') + }, 9000000) + + it('Should be able to add and remove region', async () => { + const regionTabSelector = '[data-testid="Regioner"]' + const regionTextFieldSelector = '[data-testid="regionTextField"]' + const regionSubmitButton = '[data-testid="regionSubmitButton"]' + const testRegionName = 'New region test' + const testRegionSelector = `[data-testid="${testRegionName}"]` + const removeRegionButtonSelector = '[data-testid="removeRegionButton"]' + //Navigate to region tab + + await page.click(regionTabSelector) + await page.waitForSelector('.MuiFormControl-root') + + //Make sure the test region isnt already in the list + let regions = await page.$$eval('.MuiTableRow-root > td', (elList) => elList.map((p) => p.textContent)) + expect(regions).not.toContain(testRegionName) + + //Add the test region to the list and make sure it's present + await page.click(regionTextFieldSelector) + await page.keyboard.type(testRegionName) + await page.click(regionSubmitButton) + await page.waitForTimeout(1000) + regions = await page.$$eval('.MuiTableRow-root > td', (elList) => elList.map((p) => p.textContent)) + expect(regions).toContain(testRegionName) + + //Remove the test region from the list and make sure it's gone + await page.click(testRegionSelector) + await page.click(removeRegionButtonSelector) + await page.waitForTimeout(1000) + regions = await page.$$eval('.MuiTableRow-root > td', (elList) => elList.map((p) => p.textContent)) + expect(regions).not.toContain(testRegionName) + }, 9000000) + + it('Should be able to add and remove a user', async () => { + const userTabSelector = '[data-testid="Användare"]' + const addUserButtonSelector = '[data-testid="addUserButton"]' + const addUserEmailSelector = '[data-testid="addUserEmail"]' + const addUserPasswordSelector = '[data-testid="addUserPassword"]' + const addUserNameSelector = '[data-testid="addUserName"]' + const userCitySelectSelector = '[data-testid="userCitySelect"]' + const userRoleSelectSelector = '[data-testid="userRoleSelect"]' + const addUserSubmitSelector = '[data-testid="addUserSubmit"]' + const removeUserSelector = '[data-testid="removeUser"]' + const accceptRemoveUserSelector = '[data-testid="acceptRemoveUser"]' + + const testUserEmail = 'NewUser@test.test' + const testUserPassword = 'TestPassword' + const testUserName = 'TestUserName' + const testUserCity = 'Linköping' + const testUserRole = 'Admin' + + const userCitySelector = `[data-testid="${testUserCity}"]` + const userRoleSelector = `[data-testid="${testUserRole}"]` + const moreSelector = `[data-testid="more-${testUserEmail}"]` + + //Navigate to user tab + await page.click(userTabSelector) + await page.waitForSelector(addUserButtonSelector) + //Make sure the test user isnt already in the list + let emails = await page.$$eval('.MuiTableRow-root > td', (elList) => elList.map((p) => p.textContent)) + expect(emails).not.toContain(testUserEmail) + + //Add the test user to the list and make sure it's present + await page.click(addUserButtonSelector) + await page.click(addUserEmailSelector) + await page.keyboard.type(testUserEmail) + await page.click(addUserPasswordSelector) + await page.keyboard.type(testUserPassword) + await page.click(addUserNameSelector) + await page.keyboard.type(testUserName) + await page.click(userCitySelectSelector) + await page.click(userCitySelector) + await page.waitForTimeout(100) + await page.click(userRoleSelectSelector) + await page.waitForTimeout(100) + await page.click(userRoleSelector) + await page.waitForTimeout(100) + await page.click(addUserSubmitSelector) + await page.waitForTimeout(1000) + emails = await page.$$eval('.MuiTableRow-root > td', (elList) => elList.map((p) => p.textContent)) + expect(emails).toContain(testUserEmail) + + //Remove the test user from the list and make sure it's gone + await page.click(moreSelector) + await page.click(removeUserSelector) + await page.click(accceptRemoveUserSelector) + await page.waitForTimeout(1000) + emails = await page.$$eval('.MuiTableRow-root > td', (elList) => elList.map((p) => p.textContent)) + expect(emails).not.toContain(testUserEmail) + }, 9000000) + + it('Should be able to add and remove competition', async () => { + const competitionsTabSelector = '[data-testid="Tävlingshanterare"]' + const addCompetitionsButtonSelector = '[data-testid="addCompetition"]' + const competitionNameSelector = '[data-testid="competitionName"]' + const competitionRegionSelectSelector = '[data-testid="competitionRegion"]' + const acceptAddCompetition = '[data-testid="acceptCompetition"]' + const removeCompetitionButtonSelector = '[data-testid="removeCompetitionButton"]' + + const testCompetitionName = 'New test competition' + const testCompetitionRegion = 'Linköping' + + const testCompetitionRegionSelector = `[data-testid="${testCompetitionRegion}"]` + const testCompetitionSelector = `[data-testid="${testCompetitionName}"]` + //Navigate to competitionManager tab + await page.click(competitionsTabSelector) + await page.waitForSelector('.MuiFormControl-root') + + //Make sure the test region isnt already in the list + let competitions = await page.$$eval('.MuiTableRow-root > td', (elList) => elList.map((p) => p.textContent)) + expect(competitions).not.toContain(testCompetitionName) + + //Add the test region to the list and make sure it's present + await page.click(addCompetitionsButtonSelector) + await page.click(competitionNameSelector) + await page.keyboard.type(testCompetitionName) + await page.click(competitionRegionSelectSelector) + await page.waitForTimeout(100) + await page.click(testCompetitionRegionSelector) + await page.waitForTimeout(100) + await page.click(acceptAddCompetition) + await page.waitForTimeout(1000) + + competitions = await page.$$eval('.MuiTableRow-root > td', (elList) => elList.map((p) => p.textContent)) + expect(competitions).toContain(testCompetitionName) + + //Remove the test region from the list and make sure it's gone + await page.click(testCompetitionSelector) + await page.waitForTimeout(100) + await page.click(removeCompetitionButtonSelector) + await page.waitForTimeout(1000) + competitions = await page.$$eval('.MuiTableRow-root > td', (elList) => elList.map((p) => p.textContent)) + expect(competitions).not.toContain(testCompetitionName) + }, 9000000) +}) diff --git a/client/src/e2e/LoginPage.test.tsx b/client/src/e2e/LoginPage.test.tsx new file mode 100644 index 00000000..6a60ff6b --- /dev/null +++ b/client/src/e2e/LoginPage.test.tsx @@ -0,0 +1,70 @@ +import puppeteer from 'puppeteer' +import { CLIENT_URL, DEVTOOLS_ENABLED, HEADLESS_ENABLED, SLOW_DOWN_FACTOR } from './TestingConstants' + +describe('Login page', () => { + const buttonSelector = '[data-testid="submit"]' + const emailSelector = '[data-testid="email"]' + const passwordSelector = '[data-testid="password"]' + let browser: puppeteer.Browser + let page: puppeteer.Page + beforeEach(async () => { + // Set up testing environment + browser = await puppeteer.launch({ + headless: HEADLESS_ENABLED, + devtools: DEVTOOLS_ENABLED, + slowMo: SLOW_DOWN_FACTOR, + }) + page = await browser.newPage() + + //Navigate to login screen + await page.goto(CLIENT_URL) + await page.waitForSelector('.MuiFormControl-root') + }) + + afterEach(async () => { + await browser.close() + }) + + it('Can submit login user with correct credentials', async () => { + await page.click(emailSelector) + await page.keyboard.type('admin@test.se') + await page.click(passwordSelector) + await page.keyboard.type('password') + await page.click(buttonSelector) + await page.waitForTimeout(4000) + const AdminTitle = await page.$eval('.MuiTypography-root', (el) => el.textContent) + expect(AdminTitle).toEqual('Startsida') + }, 9000000) + + it('Shows correct error message when logging in user with incorrect credentials', async () => { + await page.click(emailSelector) + await page.keyboard.type('wrong@wrong.se') + await page.click(passwordSelector) + await page.keyboard.type('wrongPassword') + await page.click(buttonSelector) + await page.waitForTimeout(1000) + const errorMessages = await page.$$eval('.MuiAlert-message > p', (elList) => elList.map((p) => p.textContent)) + // The error message is divided into two p elements + const errorMessageRow1 = errorMessages[0] + const errorMessageRow2 = errorMessages[1] + expect(errorMessageRow1).toEqual('Någonting gick fel. Kontrollera') + expect(errorMessageRow2).toEqual('dina användaruppgifter och försök igen') + }, 9000000) + + it('Shows correct error message when email is in incorrect format', async () => { + await page.click(emailSelector) + await page.keyboard.type('email') + await page.click(passwordSelector) + await page.waitForTimeout(1000) + const helperText = await page.$eval('.MuiFormHelperText-root', (el) => el.textContent) + expect(helperText).toEqual('Email inte giltig') + }, 9000000) + + it('Shows correct error message when password is too short (<6 chars)', async () => { + await page.click(passwordSelector) + await page.keyboard.type('short') + await page.click(emailSelector) + const helperText = await page.$eval('.MuiFormHelperText-root', (el) => el.textContent) + expect(helperText).toEqual('Lösenord måste vara minst 6 tecken') + }, 9000000) +}) diff --git a/client/src/e2e/TestingConstants.ts b/client/src/e2e/TestingConstants.ts new file mode 100644 index 00000000..3122aa21 --- /dev/null +++ b/client/src/e2e/TestingConstants.ts @@ -0,0 +1,6 @@ +/** This file includes constants for e2e testing */ + +export const HEADLESS_ENABLED = false +export const DEVTOOLS_ENABLED = false +export const SLOW_DOWN_FACTOR = 20 +export const CLIENT_URL = 'http://localhost:3000/' diff --git a/client/src/index.tsx b/client/src/index.tsx index fdd9cf8e..cb658ebf 100644 --- a/client/src/index.tsx +++ b/client/src/index.tsx @@ -3,7 +3,6 @@ import ReactDOM from 'react-dom' import { Provider } from 'react-redux' import App from './App' import './index.css' -import reportWebVitals from './reportWebVitals' import store from './store' // Provider wraps the app component so that it can access store @@ -15,8 +14,3 @@ ReactDOM.render( </Provider>, document.getElementById('root') ) - -// If you want to start measuring performance in your app, pass a function -// to log results (for example: reportWebVitals(console.log)) -// or send to an analytics endpoint. Learn more: https://bit.ly/CRA-vitals -reportWebVitals() diff --git a/client/src/logo.svg b/client/src/logo.svg deleted file mode 100644 index 9dfc1c05..00000000 --- a/client/src/logo.svg +++ /dev/null @@ -1 +0,0 @@ -<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 841.9 595.3"><g fill="#61DAFB"><path d="M666.3 296.5c0-32.5-40.7-63.3-103.1-82.4 14.4-63.6 8-114.2-20.2-130.4-6.5-3.8-14.1-5.6-22.4-5.6v22.3c4.6 0 8.3.9 11.4 2.6 13.6 7.8 19.5 37.5 14.9 75.7-1.1 9.4-2.9 19.3-5.1 29.4-19.6-4.8-41-8.5-63.5-10.9-13.5-18.5-27.5-35.3-41.6-50 32.6-30.3 63.2-46.9 84-46.9V78c-27.5 0-63.5 19.6-99.9 53.6-36.4-33.8-72.4-53.2-99.9-53.2v22.3c20.7 0 51.4 16.5 84 46.6-14 14.7-28 31.4-41.3 49.9-22.6 2.4-44 6.1-63.6 11-2.3-10-4-19.7-5.2-29-4.7-38.2 1.1-67.9 14.6-75.8 3-1.8 6.9-2.6 11.5-2.6V78.5c-8.4 0-16 1.8-22.6 5.6-28.1 16.2-34.4 66.7-19.9 130.1-62.2 19.2-102.7 49.9-102.7 82.3 0 32.5 40.7 63.3 103.1 82.4-14.4 63.6-8 114.2 20.2 130.4 6.5 3.8 14.1 5.6 22.5 5.6 27.5 0 63.5-19.6 99.9-53.6 36.4 33.8 72.4 53.2 99.9 53.2 8.4 0 16-1.8 22.6-5.6 28.1-16.2 34.4-66.7 19.9-130.1 62-19.1 102.5-49.9 102.5-82.3zm-130.2-66.7c-3.7 12.9-8.3 26.2-13.5 39.5-4.1-8-8.4-16-13.1-24-4.6-8-9.5-15.8-14.4-23.4 14.2 2.1 27.9 4.7 41 7.9zm-45.8 106.5c-7.8 13.5-15.8 26.3-24.1 38.2-14.9 1.3-30 2-45.2 2-15.1 0-30.2-.7-45-1.9-8.3-11.9-16.4-24.6-24.2-38-7.6-13.1-14.5-26.4-20.8-39.8 6.2-13.4 13.2-26.8 20.7-39.9 7.8-13.5 15.8-26.3 24.1-38.2 14.9-1.3 30-2 45.2-2 15.1 0 30.2.7 45 1.9 8.3 11.9 16.4 24.6 24.2 38 7.6 13.1 14.5 26.4 20.8 39.8-6.3 13.4-13.2 26.8-20.7 39.9zm32.3-13c5.4 13.4 10 26.8 13.8 39.8-13.1 3.2-26.9 5.9-41.2 8 4.9-7.7 9.8-15.6 14.4-23.7 4.6-8 8.9-16.1 13-24.1zM421.2 430c-9.3-9.6-18.6-20.3-27.8-32 9 .4 18.2.7 27.5.7 9.4 0 18.7-.2 27.8-.7-9 11.7-18.3 22.4-27.5 32zm-74.4-58.9c-14.2-2.1-27.9-4.7-41-7.9 3.7-12.9 8.3-26.2 13.5-39.5 4.1 8 8.4 16 13.1 24 4.7 8 9.5 15.8 14.4 23.4zM420.7 163c9.3 9.6 18.6 20.3 27.8 32-9-.4-18.2-.7-27.5-.7-9.4 0-18.7.2-27.8.7 9-11.7 18.3-22.4 27.5-32zm-74 58.9c-4.9 7.7-9.8 15.6-14.4 23.7-4.6 8-8.9 16-13 24-5.4-13.4-10-26.8-13.8-39.8 13.1-3.1 26.9-5.8 41.2-7.9zm-90.5 125.2c-35.4-15.1-58.3-34.9-58.3-50.6 0-15.7 22.9-35.6 58.3-50.6 8.6-3.7 18-7 27.7-10.1 5.7 19.6 13.2 40 22.5 60.9-9.2 20.8-16.6 41.1-22.2 60.6-9.9-3.1-19.3-6.5-28-10.2zM310 490c-13.6-7.8-19.5-37.5-14.9-75.7 1.1-9.4 2.9-19.3 5.1-29.4 19.6 4.8 41 8.5 63.5 10.9 13.5 18.5 27.5 35.3 41.6 50-32.6 30.3-63.2 46.9-84 46.9-4.5-.1-8.3-1-11.3-2.7zm237.2-76.2c4.7 38.2-1.1 67.9-14.6 75.8-3 1.8-6.9 2.6-11.5 2.6-20.7 0-51.4-16.5-84-46.6 14-14.7 28-31.4 41.3-49.9 22.6-2.4 44-6.1 63.6-11 2.3 10.1 4.1 19.8 5.2 29.1zm38.5-66.7c-8.6 3.7-18 7-27.7 10.1-5.7-19.6-13.2-40-22.5-60.9 9.2-20.8 16.6-41.1 22.2-60.6 9.9 3.1 19.3 6.5 28.1 10.2 35.4 15.1 58.3 34.9 58.3 50.6-.1 15.7-23 35.6-58.4 50.6zM320.8 78.4z"/><circle cx="420.9" cy="296.5" r="45.7"/><path d="M520.5 78.1z"/></g></svg> \ No newline at end of file diff --git a/client/src/middleware/Middleware_Explanation.txt b/client/src/middleware/Middleware_Explanation.txt deleted file mode 100644 index dc4e91ab..00000000 --- a/client/src/middleware/Middleware_Explanation.txt +++ /dev/null @@ -1,6 +0,0 @@ -Redux middleware provides a third-party extension point between dispatching an action, -and the moment it reaches the reducer. People use Redux middleware for logging, -crash reporting, talking to an asynchronous API, routing, and more. - - -https://redux.js.org/tutorials/fundamentals/part-4-store \ No newline at end of file diff --git a/client/src/pages/admin/AdminPage.tsx b/client/src/pages/admin/AdminPage.tsx index 1b543083..3a375210 100644 --- a/client/src/pages/admin/AdminPage.tsx +++ b/client/src/pages/admin/AdminPage.tsx @@ -91,6 +91,7 @@ const AdminView: React.FC = () => { const menuItems = isAdmin ? menuAdminItems : menuEditorItems return menuItems.map((value, index) => ( <ListItem + data-testid={value.text} button component={Link} key={value.text} diff --git a/client/src/pages/admin/competitions/AddCompetition.tsx b/client/src/pages/admin/competitions/AddCompetition.tsx index 6053f2f8..e404b112 100644 --- a/client/src/pages/admin/competitions/AddCompetition.tsx +++ b/client/src/pages/admin/competitions/AddCompetition.tsx @@ -88,6 +88,7 @@ const AddCompetition: React.FC = (props: any) => { return ( <div> <AddButton + data-testid="addCompetition" style={{ backgroundColor: '#4caf50', color: '#fcfcfc' }} color="default" variant="contained" @@ -124,6 +125,7 @@ const AddCompetition: React.FC = (props: any) => { {(formik) => ( <AddForm onSubmit={formik.handleSubmit}> <TextField + data-testid="competitionName" label="Namn" name="model.name" helperText={formik.touched.model?.name ? formik.errors.model?.name : ''} @@ -137,6 +139,7 @@ const AddCompetition: React.FC = (props: any) => { Region </InputLabel> <TextField + data-testid="competitionRegion" select name="model.city" id="standard-select-currency" @@ -152,7 +155,12 @@ const AddCompetition: React.FC = (props: any) => { </MenuItem> {cities && cities.map((city) => ( - <MenuItem key={city.name} value={city.name} onClick={() => setSelectedCity(city)}> + <MenuItem + key={city.name} + value={city.name} + onClick={() => setSelectedCity(city)} + data-testid={city.name} + > {city.name} </MenuItem> ))} @@ -170,6 +178,7 @@ const AddCompetition: React.FC = (props: any) => { margin="normal" /> <Button + data-testid="acceptCompetition" type="submit" fullWidth variant="contained" diff --git a/client/src/pages/admin/competitions/CompetitionManager.tsx b/client/src/pages/admin/competitions/CompetitionManager.tsx index 7de930dc..9b72ae04 100644 --- a/client/src/pages/admin/competitions/CompetitionManager.tsx +++ b/client/src/pages/admin/competitions/CompetitionManager.tsx @@ -251,12 +251,7 @@ const CompetitionManager: React.FC = (props: any) => { <div> <TopBar> <FilterContainer> - <TextField - className={classes.margin} - value={filterParams.name || ''} - onChange={onSearchChange} - label="Sök" - ></TextField> + <TextField className={classes.margin} value={filterParams.name || ''} onChange={onSearchChange} label="Sök" /> <FormControl className={classes.margin}> <InputLabel shrink id="demo-customized-select-native"> Region @@ -314,7 +309,7 @@ const CompetitionManager: React.FC = (props: any) => { <TableCell align="right">{cities.find((city) => city.id === row.city_id)?.name || ''}</TableCell> <TableCell align="right">{row.year}</TableCell> <TableCell align="right"> - <Button onClick={(event) => handleClick(event, row.id)}> + <Button onClick={(event) => handleClick(event, row.id)} data-testid={row.name}> <MoreHorizIcon /> </Button> </TableCell> @@ -339,7 +334,9 @@ const CompetitionManager: React.FC = (props: any) => { <MenuItem onClick={handleStartCompetition}>Starta</MenuItem> <MenuItem onClick={handleOpenDialog}>Visa koder</MenuItem> <MenuItem onClick={handleDuplicateCompetition}>Duplicera</MenuItem> - <RemoveMenuItem onClick={handleDeleteCompetition}>Ta bort</RemoveMenuItem> + <RemoveMenuItem onClick={handleDeleteCompetition} data-testid="removeCompetitionButton"> + Ta bort + </RemoveMenuItem> </Menu> <Dialog open={dialogIsOpen} diff --git a/client/src/pages/admin/dashboard/components/CurrentUser.tsx b/client/src/pages/admin/dashboard/components/CurrentUser.tsx index 1d6afe73..933a320b 100644 --- a/client/src/pages/admin/dashboard/components/CurrentUser.tsx +++ b/client/src/pages/admin/dashboard/components/CurrentUser.tsx @@ -15,7 +15,9 @@ const CurrentUser: React.FC = () => { </Typography> </div> <div> - <Typography variant="h6">Email: {currentUser && currentUser.email}</Typography> + <Typography data-testid="userEmail" variant="h6"> + Email: {currentUser && currentUser.email} + </Typography> </div> <div> <Typography variant="h6">Region: {currentUser && currentUser.city && currentUser.city.name}</Typography> diff --git a/client/src/pages/admin/regions/AddRegion.tsx b/client/src/pages/admin/regions/AddRegion.tsx index 8bd5e562..10cb22bc 100644 --- a/client/src/pages/admin/regions/AddRegion.tsx +++ b/client/src/pages/admin/regions/AddRegion.tsx @@ -79,6 +79,7 @@ const AddRegion: React.FC = (props: any) => { <FormControl className={classes.margin}> <Grid container={true}> <TextField + data-testid="regionTextField" className={classes.margin} helperText={formik.touched.model?.name ? formik.errors.model?.name : ''} error={Boolean(formik.touched.model?.name && formik.errors.model?.name)} @@ -86,8 +87,9 @@ const AddRegion: React.FC = (props: any) => { onBlur={formik.handleBlur} name="model.name" label="Region" - ></TextField> + /> <AddButton + data-testid="regionSubmitButton" style={{ backgroundColor: '#4caf50', color: '#fcfcfc' }} className={classes.button} color="default" diff --git a/client/src/pages/admin/regions/Regions.tsx b/client/src/pages/admin/regions/Regions.tsx index 28a25cad..5e2531bb 100644 --- a/client/src/pages/admin/regions/Regions.tsx +++ b/client/src/pages/admin/regions/Regions.tsx @@ -99,7 +99,7 @@ const RegionManager: React.FC = (props: any) => { <TableRow key={row.name}> <TableCell scope="row">{row.name}</TableCell> <TableCell align="right"> - <Button onClick={(event) => handleClick(event, row.id)}> + <Button onClick={(event) => handleClick(event, row.id)} data-testid={row.name}> <MoreHorizIcon /> </Button> </TableCell> @@ -110,7 +110,9 @@ const RegionManager: React.FC = (props: any) => { {(!cities || cities.length === 0) && <Typography>Inga regioner hittades</Typography>} </TableContainer> <Menu id="simple-menu" anchorEl={anchorEl} keepMounted open={Boolean(anchorEl)} onClose={handleClose}> - <RemoveMenuItem onClick={handleDeleteCity}>Ta bort</RemoveMenuItem> + <RemoveMenuItem onClick={handleDeleteCity} data-testid="removeRegionButton"> + Ta bort + </RemoveMenuItem> </Menu> </div> ) diff --git a/client/src/pages/admin/users/AddUser.tsx b/client/src/pages/admin/users/AddUser.tsx index d24bf130..63549edc 100644 --- a/client/src/pages/admin/users/AddUser.tsx +++ b/client/src/pages/admin/users/AddUser.tsx @@ -87,6 +87,7 @@ const AddUser: React.FC = (props: any) => { return ( <div> <AddButton + data-testid="addUserButton" style={{ backgroundColor: '#4caf50', color: '#fcfcfc' }} color="default" variant="contained" @@ -114,6 +115,7 @@ const AddUser: React.FC = (props: any) => { {(formik) => ( <AddForm onSubmit={formik.handleSubmit}> <TextField + data-testid="addUserEmail" label="Email" name="model.email" helperText={formik.touched.model?.email ? formik.errors.model?.email : ''} @@ -123,6 +125,7 @@ const AddUser: React.FC = (props: any) => { margin="normal" /> <TextField + data-testid="addUserPassword" label="Lösenord" name="model.password" helperText={formik.touched.model?.password ? formik.errors.model?.password : ''} @@ -132,6 +135,7 @@ const AddUser: React.FC = (props: any) => { margin="normal" /> <TextField + data-testid="addUserName" label="Namn" name="model.name" helperText={formik.touched.model?.name ? formik.errors.model?.name : ''} @@ -146,6 +150,7 @@ const AddUser: React.FC = (props: any) => { </InputLabel> <TextField select + data-testid="userCitySelect" name="model.city" id="standard-select-currency" value={selectedCity ? selectedCity.name : noCitySelected} @@ -160,7 +165,12 @@ const AddUser: React.FC = (props: any) => { </MenuItem> {cities && cities.map((city) => ( - <MenuItem key={city.name} value={city.name} onClick={() => setSelectedCity(city)}> + <MenuItem + key={city.name} + value={city.name} + onClick={() => setSelectedCity(city)} + data-testid={city.name} + > {city.name} </MenuItem> ))} @@ -173,6 +183,7 @@ const AddUser: React.FC = (props: any) => { </InputLabel> <TextField select + data-testid="userRoleSelect" name="model.role" id="standard-select-currency" value={selectedRole ? selectedRole.name : noRoleSelected} @@ -187,7 +198,12 @@ const AddUser: React.FC = (props: any) => { </MenuItem> {roles && roles.map((role) => ( - <MenuItem key={role.name} value={role.name} onClick={() => setSelectedRole(role)}> + <MenuItem + key={role.name} + value={role.name} + onClick={() => setSelectedRole(role)} + data-testid={role.name} + > {role.name} </MenuItem> ))} @@ -196,6 +212,7 @@ const AddUser: React.FC = (props: any) => { <Button type="submit" + data-testid="addUserSubmit" fullWidth variant="contained" color="secondary" diff --git a/client/src/pages/admin/users/EditUser.tsx b/client/src/pages/admin/users/EditUser.tsx index 9b4a5d1b..5de773fe 100644 --- a/client/src/pages/admin/users/EditUser.tsx +++ b/client/src/pages/admin/users/EditUser.tsx @@ -142,7 +142,7 @@ const EditUser = ({ user }: UserIdProps) => { } await axios .put('/api/users/' + user.id, req) - .then((res) => { + .then(() => { setAnchorEl(null) dispatch(getSearchUsers()) }) @@ -167,7 +167,7 @@ const EditUser = ({ user }: UserIdProps) => { } return ( <div> - <Button onClick={handleClick}> + <Button onClick={handleClick} data-testid={`more-${user.email}`}> <MoreHorizIcon /> </Button> <Popover @@ -289,6 +289,7 @@ const EditUser = ({ user }: UserIdProps) => { Ändra </Button> <Button + data-testid="removeUser" onClick={handleVerifyDelete} className={classes.deleteButton} fullWidth @@ -313,7 +314,7 @@ const EditUser = ({ user }: UserIdProps) => { <Button autoFocus onClick={handleClose} color="primary"> Avbryt </Button> - <Button onClick={handleDeleteUsers} color="primary" autoFocus> + <Button data-testid="acceptRemoveUser" onClick={handleDeleteUsers} color="primary" autoFocus> Ta bort </Button> </DialogActions> diff --git a/client/src/pages/admin/users/ResponsiveDialog.tsx b/client/src/pages/admin/users/ResponsiveDialog.tsx deleted file mode 100644 index e29a6786..00000000 --- a/client/src/pages/admin/users/ResponsiveDialog.tsx +++ /dev/null @@ -1,53 +0,0 @@ -import React from 'react'; -import Button from '@material-ui/core/Button'; -import Dialog from '@material-ui/core/Dialog'; -import DialogActions from '@material-ui/core/DialogActions'; -import DialogContent from '@material-ui/core/DialogContent'; -import DialogContentText from '@material-ui/core/DialogContentText'; -import DialogTitle from '@material-ui/core/DialogTitle'; -import useMediaQuery from '@material-ui/core/useMediaQuery'; -import { useTheme } from '@material-ui/core/styles'; - -export default function ResponsiveDialog() { - const [open, setOpen] = React.useState(false); - const theme = useTheme(); - const fullScreen = useMediaQuery(theme.breakpoints.down('sm')); - - const handleClickOpen = () => { - setOpen(true); - }; - - const handleClose = () => { - setOpen(false); - }; - - return ( - <div> - <Button variant="outlined" color="primary" onClick={handleClickOpen}> - Open responsive dialog - </Button> - <Dialog - fullScreen={fullScreen} - open={open} - onClose={handleClose} - aria-labelledby="responsive-dialog-title" - > - <DialogTitle id="responsive-dialog-title">{"Use Google's location service?"}</DialogTitle> - <DialogContent> - <DialogContentText> - Let Google help apps determine location. This means sending anonymous location data to - Google, even when no apps are running. - </DialogContentText> - </DialogContent> - <DialogActions> - <Button autoFocus onClick={handleClose} color="primary"> - Disagree - </Button> - <Button onClick={handleClose} color="primary" autoFocus> - Agree - </Button> - </DialogActions> - </Dialog> - </div> - ); -} \ No newline at end of file diff --git a/client/src/pages/login/components/AdminLogin.tsx b/client/src/pages/login/components/AdminLogin.tsx index 4fe47524..c33f338d 100644 --- a/client/src/pages/login/components/AdminLogin.tsx +++ b/client/src/pages/login/components/AdminLogin.tsx @@ -57,6 +57,7 @@ const AdminLogin: React.FC = () => { <TextField label="Email Adress" name="model.email" + data-testid="email" helperText={formik.touched.model?.email ? formik.errors.model?.email : ''} error={Boolean(formik.touched.model?.email && formik.errors.model?.email)} onChange={formik.handleChange} @@ -67,6 +68,7 @@ const AdminLogin: React.FC = () => { label="Lösenord" name="model.password" type="password" + data-testid="password" helperText={formik.touched.model?.password ? formik.errors.model?.password : ''} error={Boolean(formik.touched.model?.password && formik.errors.model?.password)} onChange={formik.handleChange} @@ -75,6 +77,7 @@ const AdminLogin: React.FC = () => { /> <Button type="submit" + data-testid="submit" fullWidth variant="contained" color="secondary" diff --git a/client/src/pages/presentationEditor/components/BackgroundImageSelect.test.tsx b/client/src/pages/presentationEditor/components/BackgroundImageSelect.test.tsx new file mode 100644 index 00000000..17705308 --- /dev/null +++ b/client/src/pages/presentationEditor/components/BackgroundImageSelect.test.tsx @@ -0,0 +1,16 @@ +import { render } from '@testing-library/react' +import React from 'react' +import { Provider } from 'react-redux' +import { BrowserRouter } from 'react-router-dom' +import store from '../../../store' +import BackgroundImageSelect from './BackgroundImageSelect' + +it('renders background image select', () => { + render( + <BrowserRouter> + <Provider store={store}> + <BackgroundImageSelect variant="competition" /> + </Provider> + </BrowserRouter> + ) +}) diff --git a/client/src/pages/presentationEditor/components/CheckboxComponent.tsx b/client/src/pages/presentationEditor/components/CheckboxComponent.tsx deleted file mode 100644 index cb264712..00000000 --- a/client/src/pages/presentationEditor/components/CheckboxComponent.tsx +++ /dev/null @@ -1,31 +0,0 @@ -import { Checkbox } from '@material-ui/core' -import React, { useState } from 'react' -import { Rnd } from 'react-rnd' -import { Component } from '../../../interfaces/ApiModels' -import { Position } from '../../../interfaces/Components' - -type CheckboxComponentProps = { - component: Component -} - -const CheckboxComponent = ({ component }: CheckboxComponentProps) => { - const [currentPos, setCurrentPos] = useState<Position>({ x: component.x, y: component.y }) - return ( - <Rnd - bounds="parent" - onDragStop={(e, d) => { - setCurrentPos({ x: d.x, y: d.y }) - }} - position={{ x: currentPos.x, y: currentPos.y }} - > - <Checkbox - disableRipple - style={{ - transform: 'scale(3)', - }} - /> - </Rnd> - ) -} - -export default CheckboxComponent diff --git a/client/src/pages/presentationEditor/components/RndComponent.test.tsx b/client/src/pages/presentationEditor/components/RndComponent.test.tsx new file mode 100644 index 00000000..b024ab3f --- /dev/null +++ b/client/src/pages/presentationEditor/components/RndComponent.test.tsx @@ -0,0 +1,17 @@ +import { render } from '@testing-library/react' +import React from 'react' +import { Provider } from 'react-redux' +import { BrowserRouter } from 'react-router-dom' +import { Component } from '../../../interfaces/ApiModels' +import store from '../../../store' +import RndComponent from './RndComponent' + +it('renders rnd component', () => { + render( + <BrowserRouter> + <Provider store={store}> + <RndComponent component={{ id: 2, x: 0, w: 15, h: 15 } as Component} width={50} height={50} scale={123} /> + </Provider> + </BrowserRouter> + ) +}) diff --git a/client/src/pages/presentationEditor/components/TextComponentEdit.test.tsx b/client/src/pages/presentationEditor/components/TextComponentEdit.test.tsx new file mode 100644 index 00000000..655b21bf --- /dev/null +++ b/client/src/pages/presentationEditor/components/TextComponentEdit.test.tsx @@ -0,0 +1,17 @@ +import { render } from '@testing-library/react' +import React from 'react' +import { Provider } from 'react-redux' +import { BrowserRouter } from 'react-router-dom' +import { TextComponent } from '../../../interfaces/ApiModels' +import store from '../../../store' +import TextComponentEdit from './TextComponentEdit' + +it('renders text component edit', () => { + render( + <BrowserRouter> + <Provider store={store}> + <TextComponentEdit component={{ id: 2, text: 'testtext' } as TextComponent} /> + </Provider> + </BrowserRouter> + ) +}) diff --git a/client/src/pages/presentationEditor/components/slideSettingsComponents/Images.test.tsx b/client/src/pages/presentationEditor/components/slideSettingsComponents/Images.test.tsx new file mode 100644 index 00000000..917fa0c8 --- /dev/null +++ b/client/src/pages/presentationEditor/components/slideSettingsComponents/Images.test.tsx @@ -0,0 +1,14 @@ +import { render } from '@testing-library/react' +import React from 'react' +import { Provider } from 'react-redux' +import { RichSlide } from '../../../../interfaces/ApiRichModels' +import store from '../../../../store' +import Images from './Images' + +it('renders images', () => { + render( + <Provider store={store}> + <Images activeSlide={{ id: 5 } as RichSlide} activeViewTypeId={5} competitionId="1" /> + </Provider> + ) +}) diff --git a/client/src/pages/presentationEditor/components/slideSettingsComponents/Images.tsx b/client/src/pages/presentationEditor/components/slideSettingsComponents/Images.tsx index 49f41e7f..60c68fb2 100644 --- a/client/src/pages/presentationEditor/components/slideSettingsComponents/Images.tsx +++ b/client/src/pages/presentationEditor/components/slideSettingsComponents/Images.tsx @@ -1,14 +1,14 @@ /* This file handles creating and removing image components, and uploading and removing image files from the server. */ -import { ListItem, ListItemText, Typography } from '@material-ui/core' +import { ListItem, ListItemText } from '@material-ui/core' import CloseIcon from '@material-ui/icons/Close' -import React from 'react' -import { Center, HiddenInput, SettingsList, AddImageButton, ImportedImage, AddButton } from '../styled' import axios from 'axios' +import React from 'react' import { getEditorCompetition } from '../../../../actions/editor' -import { RichSlide } from '../../../../interfaces/ApiRichModels' -import { ImageComponent, Media } from '../../../../interfaces/ApiModels' import { useAppDispatch, useAppSelector } from '../../../../hooks' +import { ImageComponent, Media } from '../../../../interfaces/ApiModels' +import { RichSlide } from '../../../../interfaces/ApiRichModels' +import { AddButton, AddImageButton, Center, HiddenInput, ImportedImage, SettingsList } from '../styled' type ImagesProps = { activeViewTypeId: number diff --git a/client/src/pages/presentationEditor/components/slideSettingsComponents/Instructions.test.tsx b/client/src/pages/presentationEditor/components/slideSettingsComponents/Instructions.test.tsx new file mode 100644 index 00000000..fe745c83 --- /dev/null +++ b/client/src/pages/presentationEditor/components/slideSettingsComponents/Instructions.test.tsx @@ -0,0 +1,14 @@ +import { render } from '@testing-library/react' +import React from 'react' +import { Provider } from 'react-redux' +import { RichSlide } from '../../../../interfaces/ApiRichModels' +import store from '../../../../store' +import Instructions from './Instructions' + +it('renders instructions', () => { + render( + <Provider store={store}> + <Instructions activeSlide={{ id: 5 } as RichSlide} competitionId="1" /> + </Provider> + ) +}) diff --git a/client/src/pages/presentationEditor/components/slideSettingsComponents/Instructions.tsx b/client/src/pages/presentationEditor/components/slideSettingsComponents/Instructions.tsx index 99b4b1d8..de34bc20 100644 --- a/client/src/pages/presentationEditor/components/slideSettingsComponents/Instructions.tsx +++ b/client/src/pages/presentationEditor/components/slideSettingsComponents/Instructions.tsx @@ -23,7 +23,7 @@ const Instructions = ({ activeSlide, competitionId }: InstructionsProps) => { //Only updates 250ms after last input was made to not spam setTimerHandle( window.setTimeout(async () => { - if (activeSlide && activeSlide.questions[0]) { + if (activeSlide && activeSlide.questions?.[0]) { await axios // TODO: Implement instructions field in question and add put API .put( @@ -56,7 +56,7 @@ const Instructions = ({ activeSlide, competitionId }: InstructionsProps) => { <TextField multiline id="outlined-basic" - defaultValue={activeSlide.questions[0].correcting_instructions} + defaultValue={activeSlide.questions?.[0].correcting_instructions} onChange={updateInstructionsText} variant="outlined" fullWidth={true} diff --git a/client/src/pages/presentationEditor/components/slideSettingsComponents/MultipleChoiceAlternatives.test.tsx b/client/src/pages/presentationEditor/components/slideSettingsComponents/MultipleChoiceAlternatives.test.tsx new file mode 100644 index 00000000..75131257 --- /dev/null +++ b/client/src/pages/presentationEditor/components/slideSettingsComponents/MultipleChoiceAlternatives.test.tsx @@ -0,0 +1,14 @@ +import { render } from '@testing-library/react' +import React from 'react' +import { Provider } from 'react-redux' +import { RichSlide } from '../../../../interfaces/ApiRichModels' +import store from '../../../../store' +import MultipleChoiceAlternatives from './MultipleChoiceAlternatives' + +it('renders multiple choice alternatives', () => { + render( + <Provider store={store}> + <MultipleChoiceAlternatives activeSlide={{ id: 5 } as RichSlide} competitionId="1" /> + </Provider> + ) +}) diff --git a/client/src/pages/presentationEditor/components/slideSettingsComponents/MultipleChoiceAlternatives.tsx b/client/src/pages/presentationEditor/components/slideSettingsComponents/MultipleChoiceAlternatives.tsx index 58053995..cf803d3a 100644 --- a/client/src/pages/presentationEditor/components/slideSettingsComponents/MultipleChoiceAlternatives.tsx +++ b/client/src/pages/presentationEditor/components/slideSettingsComponents/MultipleChoiceAlternatives.tsx @@ -34,7 +34,7 @@ const MultipleChoiceAlternatives = ({ activeSlide, competitionId }: MultipleChoi } const updateAlternativeValue = async (alternative: QuestionAlternative) => { - if (activeSlide && activeSlide.questions[0]) { + if (activeSlide && activeSlide.questions?.[0]) { let newValue: number if (alternative.value === 0) { newValue = 1 @@ -52,7 +52,7 @@ const MultipleChoiceAlternatives = ({ activeSlide, competitionId }: MultipleChoi } const updateAlternativeText = async (alternative_id: number, newText: string) => { - if (activeSlide && activeSlide.questions[0]) { + if (activeSlide && activeSlide.questions?.[0]) { await axios .put( `/api/competitions/${competitionId}/slides/${activeSlide?.id}/questions/${activeSlide?.questions[0].id}/alternatives/${alternative_id}`, @@ -66,7 +66,7 @@ const MultipleChoiceAlternatives = ({ activeSlide, competitionId }: MultipleChoi } const addAlternative = async () => { - if (activeSlide && activeSlide.questions[0]) { + if (activeSlide && activeSlide.questions?.[0]) { await axios .post( `/api/competitions/${competitionId}/slides/${activeSlide?.id}/questions/${activeSlide?.questions[0].id}/alternatives`, @@ -80,7 +80,7 @@ const MultipleChoiceAlternatives = ({ activeSlide, competitionId }: MultipleChoi } const handleCloseAnswerClick = async (alternative_id: number) => { - if (activeSlide && activeSlide.questions[0]) { + if (activeSlide && activeSlide.questions?.[0]) { await axios .delete( `/api/competitions/${competitionId}/slides/${activeSlideId}/questions/${activeSlide?.questions[0].id}/alternatives/${alternative_id}` @@ -102,25 +102,22 @@ const MultipleChoiceAlternatives = ({ activeSlide, competitionId }: MultipleChoi /> </Center> </ListItem> - {activeSlide && - activeSlide.questions[0] && - activeSlide.questions[0].alternatives && - activeSlide.questions[0].alternatives.map((alt) => ( - <div key={alt.id}> - <ListItem divider> - <AlternativeTextField - id="outlined-basic" - defaultValue={alt.text} - onChange={(event) => updateAlternativeText(alt.id, event.target.value)} - variant="outlined" - /> - <GreenCheckbox checked={numberToBool(alt.value)} onChange={() => updateAlternativeValue(alt)} /> - <Clickable> - <CloseIcon onClick={() => handleCloseAnswerClick(alt.id)} /> - </Clickable> - </ListItem> - </div> - ))} + {activeSlide?.questions?.[0]?.alternatives?.map((alt) => ( + <div key={alt.id}> + <ListItem divider> + <AlternativeTextField + id="outlined-basic" + defaultValue={alt.text} + onChange={(event) => updateAlternativeText(alt.id, event.target.value)} + variant="outlined" + /> + <GreenCheckbox checked={numberToBool(alt.value)} onChange={() => updateAlternativeValue(alt)} /> + <Clickable> + <CloseIcon onClick={() => handleCloseAnswerClick(alt.id)} /> + </Clickable> + </ListItem> + </div> + ))} <ListItem button onClick={addAlternative}> <Center> <AddButton variant="button">Lägg till svarsalternativ</AddButton> diff --git a/client/src/pages/presentationEditor/components/slideSettingsComponents/QuestionSettings.test.tsx b/client/src/pages/presentationEditor/components/slideSettingsComponents/QuestionSettings.test.tsx new file mode 100644 index 00000000..c9f13bcd --- /dev/null +++ b/client/src/pages/presentationEditor/components/slideSettingsComponents/QuestionSettings.test.tsx @@ -0,0 +1,14 @@ +import { render } from '@testing-library/react' +import React from 'react' +import { Provider } from 'react-redux' +import { RichSlide } from '../../../../interfaces/ApiRichModels' +import store from '../../../../store' +import QuestionSettings from './QuestionSettings' + +it('renders question settings', () => { + render( + <Provider store={store}> + <QuestionSettings activeSlide={{ id: 5 } as RichSlide} competitionId="1" /> + </Provider> + ) +}) diff --git a/client/src/pages/presentationEditor/components/slideSettingsComponents/QuestionSettings.tsx b/client/src/pages/presentationEditor/components/slideSettingsComponents/QuestionSettings.tsx index e917afbd..f714fe23 100644 --- a/client/src/pages/presentationEditor/components/slideSettingsComponents/QuestionSettings.tsx +++ b/client/src/pages/presentationEditor/components/slideSettingsComponents/QuestionSettings.tsx @@ -18,7 +18,7 @@ const QuestionSettings = ({ activeSlide, competitionId }: QuestionSettingsProps) updateTitle: boolean, event: React.ChangeEvent<HTMLTextAreaElement | HTMLInputElement> ) => { - if (activeSlide && activeSlide.questions[0]) { + if (activeSlide && activeSlide.questions?.[0]) { if (updateTitle) { await axios .put(`/api/competitions/${competitionId}/slides/${activeSlide.id}/questions/${activeSlide.questions[0].id}`, { @@ -44,7 +44,7 @@ const QuestionSettings = ({ activeSlide, competitionId }: QuestionSettingsProps) const [score, setScore] = useState<number | undefined>(0) useEffect(() => { - setScore(activeSlide?.questions[0]?.total_score) + setScore(activeSlide?.questions?.[0]?.total_score) }, [activeSlide]) return ( @@ -74,7 +74,7 @@ const QuestionSettings = ({ activeSlide, competitionId }: QuestionSettingsProps) label="Poäng" type="number" InputProps={{ inputProps: { min: 0 } }} - value={score} + value={score || 0} onChange={(event) => updateQuestion(false, event)} /> </Center> diff --git a/client/src/pages/presentationEditor/components/slideSettingsComponents/SlideType.test.tsx b/client/src/pages/presentationEditor/components/slideSettingsComponents/SlideType.test.tsx new file mode 100644 index 00000000..b0b44c64 --- /dev/null +++ b/client/src/pages/presentationEditor/components/slideSettingsComponents/SlideType.test.tsx @@ -0,0 +1,14 @@ +import { render } from '@testing-library/react' +import React from 'react' +import { Provider } from 'react-redux' +import { RichSlide } from '../../../../interfaces/ApiRichModels' +import store from '../../../../store' +import SlideType from './SlideType' + +it('renders slidetype', () => { + render( + <Provider store={store}> + <SlideType activeSlide={{ id: 5 } as RichSlide} competitionId="1" /> + </Provider> + ) +}) diff --git a/client/src/pages/presentationEditor/components/slideSettingsComponents/SlideType.tsx b/client/src/pages/presentationEditor/components/slideSettingsComponents/SlideType.tsx index bef33d94..8fbae438 100644 --- a/client/src/pages/presentationEditor/components/slideSettingsComponents/SlideType.tsx +++ b/client/src/pages/presentationEditor/components/slideSettingsComponents/SlideType.tsx @@ -46,8 +46,8 @@ const SlideType = ({ activeSlide, competitionId }: SlideTypeProps) => { const updateSlideType = async () => { closeSlideTypeDialog() if (activeSlide) { - deleteQuestionComponent(questionComponentId) - if (activeSlide.questions[0] && activeSlide.questions[0].type_id !== selectedSlideType) { + if (activeSlide.questions?.[0] && activeSlide.questions[0].type_id !== selectedSlideType) { + deleteQuestionComponent(questionComponentId) if (selectedSlideType === 0) { // Change slide type from a question type to information await axios @@ -124,7 +124,7 @@ const SlideType = ({ activeSlide, competitionId }: SlideTypeProps) => { <ListItem> <FormControl fullWidth variant="outlined"> <InputLabel>Sidtyp</InputLabel> - <Select fullWidth={true} value={activeSlide?.questions[0]?.type_id || 0} label="Sidtyp"> + <Select fullWidth={true} value={activeSlide?.questions?.[0]?.type_id || 0} label="Sidtyp"> <MenuItem value={0}> <Typography variant="button" onClick={() => openSlideTypeDialog(0)}> Informationssida diff --git a/client/src/pages/presentationEditor/components/slideSettingsComponents/Texts.test.tsx b/client/src/pages/presentationEditor/components/slideSettingsComponents/Texts.test.tsx new file mode 100644 index 00000000..edd4a89f --- /dev/null +++ b/client/src/pages/presentationEditor/components/slideSettingsComponents/Texts.test.tsx @@ -0,0 +1,14 @@ +import { render } from '@testing-library/react' +import React from 'react' +import { Provider } from 'react-redux' +import { RichSlide } from '../../../../interfaces/ApiRichModels' +import store from '../../../../store' +import Texts from './Texts' + +it('renders texts', () => { + render( + <Provider store={store}> + <Texts activeSlide={{ id: 5 } as RichSlide} activeViewTypeId={5} competitionId="1" /> + </Provider> + ) +}) diff --git a/client/src/pages/presentationEditor/components/slideSettingsComponents/Timer.test.tsx b/client/src/pages/presentationEditor/components/slideSettingsComponents/Timer.test.tsx new file mode 100644 index 00000000..5ef0d2ed --- /dev/null +++ b/client/src/pages/presentationEditor/components/slideSettingsComponents/Timer.test.tsx @@ -0,0 +1,14 @@ +import { render } from '@testing-library/react' +import React from 'react' +import { Provider } from 'react-redux' +import { RichSlide } from '../../../../interfaces/ApiRichModels' +import store from '../../../../store' +import Timer from './Timer' + +it('renders timer', () => { + render( + <Provider store={store}> + <Timer activeSlide={{ id: 5 } as RichSlide} competitionId="1" /> + </Provider> + ) +}) diff --git a/client/src/pages/views/components/SocketTest.tsx b/client/src/pages/views/components/SocketTest.tsx deleted file mode 100644 index 01a0a6f2..00000000 --- a/client/src/pages/views/components/SocketTest.tsx +++ /dev/null @@ -1,61 +0,0 @@ -import React, { useEffect } from 'react' -import { connect } from 'react-redux' -import { useAppDispatch } from '../../../hooks' -import { - socketConnect, - socketEndPresentation, - socketJoinPresentation, - socketSetSlideNext, - socketSetSlidePrev, - socketStartPresentation, - socketStartTimer, -} from '../../../sockets' - -const mapStateToProps = (state: any) => { - return { - slide_order: state.presentation.slide.order, - } -} - -const mapDispatchToProps = (dispatch: any) => { - return { - // tickTimer: () => dispatch(tickTimer(1)), - } -} - -const SocketTest: React.FC = (props: any) => { - const dispatch = useAppDispatch() - - useEffect(() => { - socketConnect() - // dispatch(getPresentationCompetition('1')) // TODO: Use ID of item_code gotten from auth/login/<code> api call - // dispatch(getPresentationTeams('1')) // TODO: Use ID of item_code gotten from auth/login/<code> api call - }, []) - - return ( - <> - <button onClick={socketStartPresentation}>Start presentation</button> - <button onClick={socketJoinPresentation}>Join presentation</button> - <button onClick={socketEndPresentation}>End presentation</button> - <button onClick={socketSetSlidePrev}>Prev slide</button> - <button onClick={socketSetSlideNext}>Next slide</button> - <button onClick={socketStartTimer}>Start timer</button> - <div>Current slide: {props.slide_order}</div> - {/* <div>Timer: {props.timer.value}</div> - <div>Enabled: {props.timer.enabled.toString()}</div> - <button onClick={syncTimer}>Sync</button> - <button onClick={() => dispatch(setTimer(5))}>5 Sec</button> - <button - onClick={() => { - dispatch(setTimer(5)) - dispatch(setTimerEnabled(true)) - syncTimer() - }} - > - Sync and 5 sec - </button> */} - </> - ) -} - -export default connect(mapStateToProps, mapDispatchToProps)(SocketTest) diff --git a/client/src/reducers/allReducers.ts b/client/src/reducers/allReducers.ts index 90cb2414..038b172e 100644 --- a/client/src/reducers/allReducers.ts +++ b/client/src/reducers/allReducers.ts @@ -5,7 +5,6 @@ import citiesReducer from './citiesReducer' import competitionLoginReducer from './competitionLoginReducer' import competitionsReducer from './competitionsReducer' import editorReducer from './editorReducer' -import mediaReducer from './mediaReducer' import presentationReducer from './presentationReducer' import rolesReducer from './rolesReducer' import searchUserReducer from './searchUserReducer' @@ -25,7 +24,6 @@ const allReducers = combineReducers({ roles: rolesReducer, searchUsers: searchUserReducer, types: typesReducer, - media: mediaReducer, statistics: statisticsReducer, competitionLogin: competitionLoginReducer, }) diff --git a/client/src/reducers/mediaReducer.ts b/client/src/reducers/mediaReducer.ts deleted file mode 100644 index 1d6f4ce5..00000000 --- a/client/src/reducers/mediaReducer.ts +++ /dev/null @@ -1,41 +0,0 @@ -import { AnyAction } from 'redux' -import Types from '../actions/types' - -interface MediaState { - id: number - filename: string - mediatype_id: number - user_id: number -} - -// Define the initial values for the media state -const initialState: MediaState = { - id: 0, - filename: '', - mediatype_id: 1, - user_id: 0, -} - -export default function (state = initialState, action: AnyAction) { - switch (action.type) { - case Types.SET_MEDIA_ID: - return { ...state, id: action.payload as number } - case Types.SET_MEDIA_FILENAME: - return { - ...state, - filename: action.payload as string, - } - case Types.SET_MEDIA_TYPE_ID: - return { - ...state, - mediatype_id: action.payload as number, - } - case Types.SET_MEDIA_USER_ID: - return { - ...state, - user_id: action.payload as number, - } - default: - return state - } -} diff --git a/client/src/reportWebVitals.ts b/client/src/reportWebVitals.ts deleted file mode 100644 index a832dfa7..00000000 --- a/client/src/reportWebVitals.ts +++ /dev/null @@ -1,15 +0,0 @@ -import { ReportHandler } from 'web-vitals' - -const reportWebVitals: () => void = (onPerfEntry?: ReportHandler) => { - if (onPerfEntry && onPerfEntry instanceof Function) { - import('web-vitals').then(({ getCLS, getFID, getFCP, getLCP, getTTFB }) => { - getCLS(onPerfEntry) - getFID(onPerfEntry) - getFCP(onPerfEntry) - getLCP(onPerfEntry) - getTTFB(onPerfEntry) - }) - } -} - -export default reportWebVitals diff --git a/client/src/utils/checkAuthenticationCompetition.test.ts b/client/src/utils/checkAuthenticationCompetition.test.ts new file mode 100644 index 00000000..0d0fdd90 --- /dev/null +++ b/client/src/utils/checkAuthenticationCompetition.test.ts @@ -0,0 +1,79 @@ +import mockedAxios from 'axios' +import Types from '../actions/types' +import store from '../store' +import { CheckAuthenticationCompetition } from './checkAuthenticationCompetition' + +it('dispatches correct actions when auth token is ok', async () => { + const compRes = { data: { id: 3, slides: [{ id: 2 }] } } + ;(mockedAxios.get as jest.Mock).mockImplementation(() => { + return Promise.resolve(compRes) + }) + ;(mockedAxios.post as jest.Mock).mockImplementation(() => { + return Promise.resolve({ data: {} }) + }) + const spy = jest.spyOn(store, 'dispatch') + const decodedToken = { + iat: 1620216181, + exp: 32514436993, + user_claims: { competition_id: 123123, team_id: 321321, view: 'Participant', code: 'ABCDEF' }, + } + + const testToken = + 'Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IkpvaG4gRG9lIiwiaWF0IjoxNTE2MjM5MDIyLCJleHAiOjMyNTE0NDM2OTkzLCJ1c2VyX2NsYWltcyI6eyJjb21wZXRpdGlvbl9pZCI6MTIzMTIzLCJ0ZWFtX2lkIjozMjEzMjEsInZpZXciOiJQYXJ0aWNpcGFudCIsImNvZGUiOiJBQkNERUYifX0.1gPRJcjn3xuPOcgUUffMngIQDoDtxS9RZczcbdyyaaA' + localStorage.setItem('competitionToken', testToken) + await CheckAuthenticationCompetition() + expect(spy).toBeCalledWith({ + type: Types.SET_COMPETITION_LOGIN_DATA, + payload: { + competition_id: decodedToken.user_claims.competition_id, + team_id: decodedToken.user_claims.team_id, + view: decodedToken.user_claims.view, + }, + }) + expect(spy).toBeCalledWith({ type: Types.SET_PRESENTATION_CODE, payload: decodedToken.user_claims.code }) + expect(spy).toBeCalledWith({ + type: Types.SET_PRESENTATION_COMPETITION, + payload: compRes.data, + }) + expect(spy).toBeCalledTimes(4) +}) + +it('dispatches correct actions when getting user data fails', async () => { + console.log = jest.fn() + ;(mockedAxios.get as jest.Mock).mockImplementation(() => { + return Promise.reject(new Error('failed getting user data')) + }) + ;(mockedAxios.post as jest.Mock).mockImplementation(() => { + return Promise.resolve({ data: {} }) + }) + const spy = jest.spyOn(store, 'dispatch') + const testToken = + 'Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IkpvaG4gRG9lIiwiaWF0IjoxNTE2MjM5MDIyLCJleHAiOjMyNTE0NDM2OTkzLCJ1c2VyX2NsYWltcyI6eyJjb21wZXRpdGlvbl9pZCI6MTIzMTIzLCJ0ZWFtX2lkIjozMjEzMjEsInZpZXciOiJQYXJ0aWNpcGFudCIsImNvZGUiOiJBQkNERUYifX0.1gPRJcjn3xuPOcgUUffMngIQDoDtxS9RZczcbdyyaaA' + localStorage.setItem('competitionToken', testToken) + await CheckAuthenticationCompetition() + expect(spy).toBeCalledWith({ type: Types.SET_COMPETITION_LOGIN_UNAUTHENTICATED }) + expect(spy).toBeCalledTimes(1) + expect(console.log).toHaveBeenCalled() +}) + +it('dispatches no actions when no token exists', async () => { + ;(mockedAxios.post as jest.Mock).mockImplementation(() => { + return Promise.resolve({ data: {} }) + }) + const spy = jest.spyOn(store, 'dispatch') + await CheckAuthenticationCompetition() + expect(spy).not.toBeCalled() +}) + +it('dispatches correct actions when token is expired', async () => { + ;(mockedAxios.post as jest.Mock).mockImplementation(() => { + return Promise.resolve({ data: {} }) + }) + const testToken = + 'Bearer eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJpc3MiOiJPbmxpbmUgSldUIEJ1aWxkZXIiLCJpYXQiOjE2MjAyMjE1OTgsImV4cCI6OTU3NTMzNTk4LCJhdWQiOiJ3d3cuZXhhbXBsZS5jb20iLCJzdWIiOiJqcm9ja2V0QGV4YW1wbGUuY29tIn0.uFXtkAsf-cTlKrTIdZ3E-gXnHkzS08iPrhS8iNCGV2E' + localStorage.setItem('competitionToken', testToken) + const spy = jest.spyOn(store, 'dispatch') + await CheckAuthenticationCompetition() + expect(spy).toBeCalledWith({ type: Types.SET_COMPETITION_LOGIN_UNAUTHENTICATED }) + expect(spy).toBeCalledTimes(1) +}) diff --git a/client/src/utils/checkAuthenticationCompetition.ts b/client/src/utils/checkAuthenticationCompetition.ts index 9877b674..4d542dc3 100644 --- a/client/src/utils/checkAuthenticationCompetition.ts +++ b/client/src/utils/checkAuthenticationCompetition.ts @@ -17,7 +17,7 @@ export const CheckAuthenticationCompetition = async () => { axios.defaults.headers.common['Authorization'] = authToken await axios .get('/api/auth/test') - .then((res) => { + .then(() => { store.dispatch({ type: Types.SET_COMPETITION_LOGIN_DATA, payload: { diff --git a/client/src/utils/renderSlideIcon.test.tsx b/client/src/utils/renderSlideIcon.test.tsx new file mode 100644 index 00000000..ed82c2ef --- /dev/null +++ b/client/src/utils/renderSlideIcon.test.tsx @@ -0,0 +1,63 @@ +import BuildOutlinedIcon from '@material-ui/icons/BuildOutlined' +import CheckBoxOutlinedIcon from '@material-ui/icons/CheckBoxOutlined' +import CreateOutlinedIcon from '@material-ui/icons/CreateOutlined' +import InfoOutlinedIcon from '@material-ui/icons/InfoOutlined' +import RadioButtonCheckedIcon from '@material-ui/icons/RadioButtonChecked' +import { shallow } from 'enzyme' +import { RichSlide } from '../interfaces/ApiRichModels' +import { renderSlideIcon } from './renderSlideIcon' + +it('returns CreateOutlinedIcon correctly ', async () => { + const testSlide = { questions: [{ id: 5, type_id: 1 }] } as RichSlide + const icon = renderSlideIcon(testSlide) + expect(icon).toBeDefined() + if (icon) { + const actualResult = shallow(icon) + const expectedResult = shallow(<CreateOutlinedIcon />) + expect(actualResult).toEqual(expectedResult) + } +}) + +it('returns BuildOutlinedIcon correctly ', async () => { + const testSlide = { questions: [{ id: 5, type_id: 2 }] } as RichSlide + const icon = renderSlideIcon(testSlide) + expect(icon).toBeDefined() + if (icon) { + const actualResult = shallow(icon) + const expectedResult = shallow(<BuildOutlinedIcon />) + expect(actualResult).toEqual(expectedResult) + } +}) + +it('returns DnsOutlinedIcon correctly ', async () => { + const testSlide = { questions: [{ id: 5, type_id: 3 }] } as RichSlide + const icon = renderSlideIcon(testSlide) + expect(icon).toBeDefined() + if (icon) { + const actualResult = shallow(icon) + const expectedResult = shallow(<CheckBoxOutlinedIcon />) + expect(actualResult).toEqual(expectedResult) + } +}) + +it('returns DnsOutlinedIcon correctly ', async () => { + const testSlide = { questions: [{ id: 5, type_id: 4 }] } as RichSlide + const icon = renderSlideIcon(testSlide) + expect(icon).toBeDefined() + if (icon) { + const actualResult = shallow(icon) + const expectedResult = shallow(<RadioButtonCheckedIcon />) + expect(actualResult).toEqual(expectedResult) + } +}) + +it('defaults to InfoOutlinedIcon', async () => { + const testSlide = {} as RichSlide + const icon = renderSlideIcon(testSlide) + expect(icon).toBeDefined() + if (icon) { + const actualResult = shallow(icon) + const expectedResult = shallow(<InfoOutlinedIcon />) + expect(actualResult).toEqual(expectedResult) + } +}) -- GitLab