diff --git a/.gitlab/client.gitlab-ci.yml b/.gitlab/client.gitlab-ci.yml index f35e73aa6da4b396dda7ca7b5847b83158078a41..a64f1bd85c1bba81556cc1f8fb8052cf14ac40cd 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 8b7286d134089b693c07ce77c748073701865719..45f88f114c1419ec24f9794c6d2568ad61c61e1a 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 2655ad2e5dbc1fb024ba896a3701f49adbfe399d..d857520c196037dc01bf9583d906ddaf9ed11d00 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 a0c8b7f5c74a6587f48cdf6c7d478311f2529185..7426424b33775f3845151df9163b10cd94f6f494 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 Binary files a/client/public/logo192.png and /dev/null differ diff --git a/client/public/logo512.png b/client/public/logo512.png deleted file mode 100644 index a4e47a6545bc15971f8f63fba70e4013df88a664..0000000000000000000000000000000000000000 Binary files a/client/public/logo512.png and /dev/null differ diff --git a/client/public/manifest.json b/client/public/manifest.json index 080d6c77ac21bb2ef88a6992b2b73ad93daaca92..73371289ff660726868cc23f5f2ab7f275bba3cc 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 0000000000000000000000000000000000000000..bf1825e7e02445c2b96b22b5bb0a169e723dc2a4 --- /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 d95d9db767b214e4e2583a1a903ed06b226eec16..095a352952680810cfcb3fc0f8f48a5eabc6b1a6 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 90b728c54aa18a483ed6ab000f718c407b46abfd..32e4d0a4334c4639624588f005b704dbca160ae9 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 7fa4aaeaab31902640232d60b25139a0c26f078e..0000000000000000000000000000000000000000 --- 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 0000000000000000000000000000000000000000..c9c67580ba029c41c9849a6b057cfcdba25dd2a0 --- /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 0000000000000000000000000000000000000000..6a60ff6bd50aec0965f670126437b393774da38b --- /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 0000000000000000000000000000000000000000..3122aa2126c17021211ddd88cf0936ec0111cdf9 --- /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 fdd9cf8e2bc1d9655a060c4270091fbf5f13e236..cb658ebf015a5f33f4844d280f358d348cf24a0e 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 9dfc1c058cebbef8b891c5062be6f31033d7d186..0000000000000000000000000000000000000000 --- 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 dc4e91ab6fd33627a7b19cb06be5ba20fee4598d..0000000000000000000000000000000000000000 --- 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 1b543083da7fbfb1e5c1a48d44a40a2d7fdcc17e..3a375210ae30a021d7a758e6ca9e3c9b18d4d45d 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 6053f2f8de9c3d13cde3403ec1a62317e477d047..e404b112a1df1e6ab4f9d8a8759fb946a20ccd52 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 7de930dceab457d43e6552bc5db2465a10a9bddd..9b72ae04055e5dccea5cbb6cd0826577a0f5068b 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 1d6afe732935bb3094ae1068179c7dc6ea58fb17..933a320b30e637d8100e29226f4bd41a49a48572 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 8bd5e562d2006962f5366722a207efe23c9cff55..10cb22bc5c4b7527b05d8dfeb9b0f748dd6f0c65 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 28a25cadf2cdc5785deb3c7de35f10dc45683939..5e2531bb44b6e2e6d7d6a3ce65941ecdc03a3ee3 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 d24bf130785110cb1dd039dde7dbab57107a4e9d..63549edcd3180033e897c8bfb3924e6a691225bd 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 9b4a5d1bd6aecd92a03f44221c2cb40d40e2ab70..5de773fe7a58ed8c0f252cda20f844c801819b32 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 e29a6786826e854bebf888be5b7cd904e0ec1063..0000000000000000000000000000000000000000 --- 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 4fe475245cb0539bd61c0987d8aa39fa4cc6e40d..c33f338da69a6d2f0d1733a6d5bb777461cdefe4 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 0000000000000000000000000000000000000000..17705308993ad89a8f7175549e9af3edd82e8010 --- /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 cb264712b0b4b57ef7938278471bc812085370cf..0000000000000000000000000000000000000000 --- 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 0000000000000000000000000000000000000000..b024ab3f14d82f8fb55bf336938a2fa17f911b15 --- /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 0000000000000000000000000000000000000000..655b21bfed8ac33c433bd6b152736a344aeaf02b --- /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 0000000000000000000000000000000000000000..917fa0c8642a308460e7fcc007c01ba1e91f5ad9 --- /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 49f41e7f87fa6c53dac59704540ca3c186ef708b..60c68fb2aa287620547b6b6df38a8ed9da211893 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 0000000000000000000000000000000000000000..fe745c839cc2bd9a59f07e4e97cbb85a90c19e98 --- /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 99b4b1d8d118e65595b38077e8fd15afa103ad05..de34bc205b37cc60d6c086388e4c4d0995933bcd 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 0000000000000000000000000000000000000000..75131257642df47d260ec795025e1c8bee759a6a --- /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 58053995856730b22a16d4401af17da5628ab87a..cf803d3a14588d4a172f6bc452ce4cb07dda6e20 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 0000000000000000000000000000000000000000..c9f13bcdd7e7b76d0e90de61cfc2a86c52dca194 --- /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 e917afbd19f1aac2d7256c1f5913d8a63f323ee1..f714fe2343e427a281d4e08aac50b895257d6424 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 0000000000000000000000000000000000000000..b0b44c6439cf8d2f2aa3d4dd8eb94deb0ed3ef0e --- /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 bef33d943883981ee62e9dc49dcd09902d907737..8fbae43816a55a2be1f1dcc838afffd9f3cf0b9e 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 0000000000000000000000000000000000000000..edd4a89f28c440abb20e406de84fb0121647c718 --- /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 0000000000000000000000000000000000000000..5ef0d2ed43af3fae53ad94816027d97091340a83 --- /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 01a0a6f29b51d9a194cb397fdc73c720202e0c1c..0000000000000000000000000000000000000000 --- 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 90cb24144612c5a45f611ad99871c54c51969bbd..038b172e3e308af71d5762ade8a644b67d7992d2 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 1d6f4ce5f5e67a15b6469d33ad838cf16f474f35..0000000000000000000000000000000000000000 --- 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 a832dfa7c24162e95463f27f3687de39a3c311bb..0000000000000000000000000000000000000000 --- 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 0000000000000000000000000000000000000000..0d0fdd90aea38f613a7a1b1347085a2201d793a6 --- /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 9877b674214fc5fae2899148fd9e57c9a0a3bc28..4d542dc34b5cb78cca7dbf134638a8b2649c48de 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 0000000000000000000000000000000000000000..ed82c2efcd8b880538a98176acead1b7221104b5 --- /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) + } +})