diff --git a/.github/workflows/deploy.yml b/.github/workflows/deploy.yml index 1a225c9283..2a7d690971 100644 --- a/.github/workflows/deploy.yml +++ b/.github/workflows/deploy.yml @@ -30,7 +30,7 @@ jobs: with: fetch-depth: 0 - name: Use Node.js ${{ matrix.node-version }} - uses: actions/setup-node@v3 + uses: actions/setup-node@v4 with: node-version: ${{ matrix.node-version }} - id: get-repo-values @@ -39,7 +39,7 @@ jobs: url=https://$(echo "${{github.repository}}" | sed "s/\//.github.io\//") echo "url=$url" >> $GITHUB_OUTPUT - name: Update package.json homepage - uses: jossef/action-set-json-field@v1 + uses: jossef/action-set-json-field@v2.1 with: file: package.json field: homepage @@ -134,14 +134,14 @@ jobs: # echo "" >> ./dist/index.html # Deploy - name: Setup Pages - uses: actions/configure-pages@v3 + uses: actions/configure-pages@v5 if: ${{ !cancelled() }} - name: Upload artifact - uses: actions/upload-pages-artifact@v2 + uses: actions/upload-pages-artifact@v3 if: ${{ !cancelled() }} with: path: "dist/" - name: Deploy to GitHub Pages id: deployment - uses: actions/deploy-pages@v2 + uses: actions/deploy-pages@v4 if: ${{ !cancelled() }} diff --git a/package-lock.json b/package-lock.json index 8c3779f487..1e6922997e 100644 --- a/package-lock.json +++ b/package-lock.json @@ -2153,7 +2153,7 @@ "version": "0.8.1", "resolved": "https://registry.npmjs.org/@cspotcode/source-map-support/-/source-map-support-0.8.1.tgz", "integrity": "sha512-IchNf6dN4tHoMFIn/7OE8LWZ19Y6q/67Bmf6vnGREv8RSbBVb9LPJxEcnwrcwX6ixSvaiGoomAUvu4YSxXrVgw==", - "dev": true, + "devOptional": true, "license": "MIT", "dependencies": { "@jridgewell/trace-mapping": "0.3.9" @@ -2166,7 +2166,7 @@ "version": "0.3.9", "resolved": "https://registry.npmjs.org/@jridgewell/trace-mapping/-/trace-mapping-0.3.9.tgz", "integrity": "sha512-3Belt6tdc8bPgAtbcmdtNJlirVoTmEb5e2gC94PnkwEW9jI6CAHUeoG85tjWP5WquqfavoMtMwiG4P926ZKKuQ==", - "dev": true, + "devOptional": true, "license": "MIT", "dependencies": { "@jridgewell/resolve-uri": "^3.0.3", @@ -3273,7 +3273,7 @@ "version": "29.6.3", "resolved": "https://registry.npmjs.org/@jest/schemas/-/schemas-29.6.3.tgz", "integrity": "sha512-mo5j5X+jIZmJQveBKeS/clAueipV7KgiX1vMgCxam1RNYiqE1w62n0/tJJnHtjW8ZHcQco5gY85jA3mi0L+nSA==", - "dev": true, + "devOptional": true, "license": "MIT", "dependencies": { "@sinclair/typebox": "^0.27.8" @@ -3436,7 +3436,7 @@ "version": "29.6.3", "resolved": "https://registry.npmjs.org/@jest/types/-/types-29.6.3.tgz", "integrity": "sha512-u3UPsIilWKOM3F9CXtrG8LEJmNxwoCQC/XVj4IKYXvvpx7QIi/Kg1LI5uDmDpKlac62NUtX7eLjRh+jVZcLOzw==", - "dev": true, + "devOptional": true, "license": "MIT", "dependencies": { "@jest/schemas": "^29.6.3", @@ -3454,7 +3454,7 @@ "version": "4.3.0", "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", - "dev": true, + "devOptional": true, "license": "MIT", "dependencies": { "color-convert": "^2.0.1" @@ -3470,7 +3470,7 @@ "version": "4.1.2", "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz", "integrity": "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==", - "dev": true, + "devOptional": true, "license": "MIT", "dependencies": { "ansi-styles": "^4.1.0", @@ -3487,7 +3487,7 @@ "version": "2.0.1", "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", - "dev": true, + "devOptional": true, "license": "MIT", "dependencies": { "color-name": "~1.1.4" @@ -3500,14 +3500,14 @@ "version": "1.1.4", "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==", - "dev": true, + "devOptional": true, "license": "MIT" }, "node_modules/@jest/types/node_modules/has-flag": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==", - "dev": true, + "devOptional": true, "license": "MIT", "engines": { "node": ">=8" @@ -3517,7 +3517,7 @@ "version": "7.2.0", "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz", "integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==", - "dev": true, + "devOptional": true, "license": "MIT", "dependencies": { "has-flag": "^4.0.0" @@ -3909,7 +3909,7 @@ "version": "0.27.8", "resolved": "https://registry.npmjs.org/@sinclair/typebox/-/typebox-0.27.8.tgz", "integrity": "sha512-+Fj43pSMwJs4KRrH/938Uf+uAELIgVBmQzg/q1YG10djyfA3TnrU8N8XzqCh/okZdszqBQTZf96idMfE5lnwTA==", - "dev": true, + "devOptional": true, "license": "MIT" }, "node_modules/@sinonjs/commons": { @@ -4222,7 +4222,6 @@ "version": "10.4.0", "resolved": "https://registry.npmjs.org/@testing-library/dom/-/dom-10.4.0.tgz", "integrity": "sha512-pemlzrSESWbdAloYml3bAJMEfNh1Z7EduzqPKprCH5S341frlpYnUEW0H72dLxa6IsYr+mPno20GiSm+h9dEdQ==", - "dev": true, "license": "MIT", "dependencies": { "@babel/code-frame": "^7.10.4", @@ -4242,7 +4241,6 @@ "version": "4.3.0", "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", - "dev": true, "license": "MIT", "dependencies": { "color-convert": "^2.0.1" @@ -4258,7 +4256,6 @@ "version": "4.1.2", "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz", "integrity": "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==", - "dev": true, "license": "MIT", "dependencies": { "ansi-styles": "^4.1.0", @@ -4275,7 +4272,6 @@ "version": "2.0.1", "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", - "dev": true, "license": "MIT", "dependencies": { "color-name": "~1.1.4" @@ -4288,14 +4284,12 @@ "version": "1.1.4", "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==", - "dev": true, "license": "MIT" }, "node_modules/@testing-library/dom/node_modules/has-flag": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==", - "dev": true, "license": "MIT", "engines": { "node": ">=8" @@ -4305,7 +4299,6 @@ "version": "7.2.0", "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz", "integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==", - "dev": true, "license": "MIT", "dependencies": { "has-flag": "^4.0.0" @@ -4470,35 +4463,34 @@ "version": "1.0.11", "resolved": "https://registry.npmjs.org/@tsconfig/node10/-/node10-1.0.11.tgz", "integrity": "sha512-DcRjDCujK/kCk/cUe8Xz8ZSpm8mS3mNNpta+jGCA6USEDfktlNvm1+IuZ9eTcDbNk41BHwpHHeW+N1lKCz4zOw==", - "dev": true, + "devOptional": true, "license": "MIT" }, "node_modules/@tsconfig/node12": { "version": "1.0.11", "resolved": "https://registry.npmjs.org/@tsconfig/node12/-/node12-1.0.11.tgz", "integrity": "sha512-cqefuRsh12pWyGsIoBKJA9luFu3mRxCA+ORZvA4ktLSzIuCUtWVxGIuXigEwO5/ywWFMZ2QEGKWvkZG1zDMTag==", - "dev": true, + "devOptional": true, "license": "MIT" }, "node_modules/@tsconfig/node14": { "version": "1.0.3", "resolved": "https://registry.npmjs.org/@tsconfig/node14/-/node14-1.0.3.tgz", "integrity": "sha512-ysT8mhdixWK6Hw3i1V2AeRqZ5WfXg1G43mqoYlM2nc6388Fq5jcXyr5mRsqViLx/GJYdoL0bfXD8nmF+Zn/Iow==", - "dev": true, + "devOptional": true, "license": "MIT" }, "node_modules/@tsconfig/node16": { "version": "1.0.4", "resolved": "https://registry.npmjs.org/@tsconfig/node16/-/node16-1.0.4.tgz", "integrity": "sha512-vxhUy4J8lyeyinH7Azl1pdd43GJhZH/tP2weN8TntQblOY+A0XbT8DJk1/oCPuOOyg/Ja757rG0CgHcWC8OfMA==", - "dev": true, + "devOptional": true, "license": "MIT" }, "node_modules/@types/aria-query": { "version": "5.0.4", "resolved": "https://registry.npmjs.org/@types/aria-query/-/aria-query-5.0.4.tgz", "integrity": "sha512-rfT93uj5s0PRL7EzccGMs3brplhcrghnDoV26NqKhCAS1hVo+WdNsPvE/yb6ilfr5hi2MEk6d5EWJTKdxg8jVw==", - "dev": true, "license": "MIT" }, "node_modules/@types/babel__core": { @@ -4911,7 +4903,7 @@ "version": "17.0.33", "resolved": "https://registry.npmjs.org/@types/yargs/-/yargs-17.0.33.tgz", "integrity": "sha512-WpxBCKWPLr4xSsHgz511rFJAM+wS28w2zEO1QDNY5zM/S8ok70NNfztH0xwhqKyaK0OHCbN98LDAZuy1ctxDkA==", - "dev": true, + "devOptional": true, "license": "MIT", "dependencies": { "@types/yargs-parser": "*" @@ -5508,7 +5500,7 @@ "version": "8.3.3", "resolved": "https://registry.npmjs.org/acorn-walk/-/acorn-walk-8.3.3.tgz", "integrity": "sha512-MxXdReSRhGO7VlFe1bRG/oI7/mdLV9B9JJT0N8vZOhF7gFRR5l3M8W9G8JxmKV+JC5mGqJ0QvqfSOLsCPa4nUw==", - "dev": true, + "devOptional": true, "license": "MIT", "dependencies": { "acorn": "^8.11.0" @@ -7210,7 +7202,7 @@ "version": "1.1.1", "resolved": "https://registry.npmjs.org/create-require/-/create-require-1.1.1.tgz", "integrity": "sha512-dcKFX3jn0MpIaXjisoRvexIJVEKzaq7z2rZKxf+MSr9TkdmHmsU4m2lcLojrj/FHl8mk5VxMmYA+ftRkP/3oKQ==", - "dev": true, + "devOptional": true, "license": "MIT" }, "node_modules/cross-spawn": { @@ -7947,7 +7939,7 @@ "version": "4.0.2", "resolved": "https://registry.npmjs.org/diff/-/diff-4.0.2.tgz", "integrity": "sha512-58lmxKSA4BNyLz+HHMUzlOEpg09FV+ev6ZMe3vJihgdxzgcwZ8VoEEPmALCZG9LmqfVoNMMKpttIYTVG6uDY7A==", - "dev": true, + "devOptional": true, "license": "BSD-3-Clause", "engines": { "node": ">=0.3.1" @@ -13021,7 +13013,7 @@ "version": "29.6.3", "resolved": "https://registry.npmjs.org/jest-get-type/-/jest-get-type-29.6.3.tgz", "integrity": "sha512-zrteXnqYxfQh7l5FHyL38jL39di8H8rHoecLH3JNxH3BwOrBsNeabdap5e0I23lD4HHI8W5VFBZqG4Eaq5LNcw==", - "dev": true, + "devOptional": true, "license": "MIT", "engines": { "node": "^14.15.0 || ^16.10.0 || >=18.0.0" @@ -13031,7 +13023,7 @@ "version": "29.7.0", "resolved": "https://registry.npmjs.org/jest-haste-map/-/jest-haste-map-29.7.0.tgz", "integrity": "sha512-fP8u2pyfqx0K1rGn1R9pyE0/KTn+G7PxktWidOBTqFPLYX0b9ksaMFkhK5vrS3DVun09pckLdlx90QthlW7AmA==", - "dev": true, + "devOptional": true, "license": "MIT", "dependencies": { "@jest/types": "^29.6.3", @@ -13949,7 +13941,7 @@ "version": "29.6.3", "resolved": "https://registry.npmjs.org/jest-regex-util/-/jest-regex-util-29.6.3.tgz", "integrity": "sha512-KJJBsRCyyLNWCNBOvZyRDnAIfUiRJ8v+hOBQYGn8gDyF3UegwiP4gwRR3/SDa42g1YbVycTidUF3rKjyLFDWbg==", - "dev": true, + "devOptional": true, "license": "MIT", "engines": { "node": "^14.15.0 || ^16.10.0 || >=18.0.0" @@ -13959,7 +13951,7 @@ "version": "29.7.0", "resolved": "https://registry.npmjs.org/jest-resolve/-/jest-resolve-29.7.0.tgz", "integrity": "sha512-IOVhZSrg+UvVAshDSDtHyFCCBUl/Q3AAJv8iZ6ZjnZ74xzvwuzLXid9IIIPgTnY62SJjfuupMKZsZQRsCvxEgA==", - "dev": true, + "devOptional": true, "license": "MIT", "dependencies": { "chalk": "^4.0.0", @@ -13994,7 +13986,7 @@ "version": "4.3.0", "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", - "dev": true, + "devOptional": true, "license": "MIT", "dependencies": { "color-convert": "^2.0.1" @@ -14010,7 +14002,7 @@ "version": "4.1.2", "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz", "integrity": "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==", - "dev": true, + "devOptional": true, "license": "MIT", "dependencies": { "ansi-styles": "^4.1.0", @@ -14027,7 +14019,7 @@ "version": "2.0.1", "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", - "dev": true, + "devOptional": true, "license": "MIT", "dependencies": { "color-name": "~1.1.4" @@ -14040,14 +14032,14 @@ "version": "1.1.4", "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==", - "dev": true, + "devOptional": true, "license": "MIT" }, "node_modules/jest-resolve/node_modules/has-flag": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==", - "dev": true, + "devOptional": true, "license": "MIT", "engines": { "node": ">=8" @@ -14057,7 +14049,7 @@ "version": "1.22.8", "resolved": "https://registry.npmjs.org/resolve/-/resolve-1.22.8.tgz", "integrity": "sha512-oKWePCxqpd6FlLvGV1VU0x7bkPmmCNolxzjMf4NczoDnQcIWrAF+cPtZn5i6n+RfD2d9i0tzpKnG6Yk168yIyw==", - "dev": true, + "devOptional": true, "license": "MIT", "dependencies": { "is-core-module": "^2.13.0", @@ -14075,7 +14067,7 @@ "version": "7.2.0", "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz", "integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==", - "dev": true, + "devOptional": true, "license": "MIT", "dependencies": { "has-flag": "^4.0.0" @@ -14518,7 +14510,7 @@ "version": "29.7.0", "resolved": "https://registry.npmjs.org/jest-util/-/jest-util-29.7.0.tgz", "integrity": "sha512-z6EbKajIpqGKU56y5KBUgy1dt1ihhQJgWzUlZHArA/+X2ad7Cb5iF+AK1EWVL/Bo7Rz9uurpqw6SiBCefUbCGA==", - "dev": true, + "devOptional": true, "license": "MIT", "dependencies": { "@jest/types": "^29.6.3", @@ -14536,7 +14528,7 @@ "version": "4.3.0", "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", - "dev": true, + "devOptional": true, "license": "MIT", "dependencies": { "color-convert": "^2.0.1" @@ -14552,7 +14544,7 @@ "version": "4.1.2", "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz", "integrity": "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==", - "dev": true, + "devOptional": true, "license": "MIT", "dependencies": { "ansi-styles": "^4.1.0", @@ -14569,7 +14561,7 @@ "version": "2.0.1", "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", - "dev": true, + "devOptional": true, "license": "MIT", "dependencies": { "color-name": "~1.1.4" @@ -14582,14 +14574,14 @@ "version": "1.1.4", "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==", - "dev": true, + "devOptional": true, "license": "MIT" }, "node_modules/jest-util/node_modules/has-flag": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==", - "dev": true, + "devOptional": true, "license": "MIT", "engines": { "node": ">=8" @@ -14599,7 +14591,7 @@ "version": "7.2.0", "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz", "integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==", - "dev": true, + "devOptional": true, "license": "MIT", "dependencies": { "has-flag": "^4.0.0" @@ -14612,7 +14604,7 @@ "version": "29.7.0", "resolved": "https://registry.npmjs.org/jest-validate/-/jest-validate-29.7.0.tgz", "integrity": "sha512-ZB7wHqaRGVw/9hST/OuFUReG7M8vKeq0/J2egIGLdvjHCmYqGARhzXmtgi+gVeZ5uXFF219aOc3Ls2yLg27tkw==", - "dev": true, + "devOptional": true, "license": "MIT", "dependencies": { "@jest/types": "^29.6.3", @@ -14630,7 +14622,7 @@ "version": "4.3.0", "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", - "dev": true, + "devOptional": true, "license": "MIT", "dependencies": { "color-convert": "^2.0.1" @@ -14646,7 +14638,7 @@ "version": "6.3.0", "resolved": "https://registry.npmjs.org/camelcase/-/camelcase-6.3.0.tgz", "integrity": "sha512-Gmy6FhYlCY7uOElZUSbxo2UCDH8owEk996gkbrpsgGtrJLM3J7jGxl9Ic7Qwwj4ivOE5AWZWRMecDdF7hqGjFA==", - "dev": true, + "devOptional": true, "license": "MIT", "engines": { "node": ">=10" @@ -14659,7 +14651,7 @@ "version": "4.1.2", "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz", "integrity": "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==", - "dev": true, + "devOptional": true, "license": "MIT", "dependencies": { "ansi-styles": "^4.1.0", @@ -14676,7 +14668,7 @@ "version": "2.0.1", "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", - "dev": true, + "devOptional": true, "license": "MIT", "dependencies": { "color-name": "~1.1.4" @@ -14689,14 +14681,14 @@ "version": "1.1.4", "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==", - "dev": true, + "devOptional": true, "license": "MIT" }, "node_modules/jest-validate/node_modules/has-flag": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==", - "dev": true, + "devOptional": true, "license": "MIT", "engines": { "node": ">=8" @@ -14706,7 +14698,7 @@ "version": "29.7.0", "resolved": "https://registry.npmjs.org/pretty-format/-/pretty-format-29.7.0.tgz", "integrity": "sha512-Pdlw/oPxN+aXdmM9R00JVC9WVFoCLTKJvDVLgmJ+qAffBMxsV85l/Lu7sNx4zSzPyoL2euImuEwHhOXdEgNFZQ==", - "dev": true, + "devOptional": true, "license": "MIT", "dependencies": { "@jest/schemas": "^29.6.3", @@ -14721,7 +14713,7 @@ "version": "5.2.0", "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-5.2.0.tgz", "integrity": "sha512-Cxwpt2SfTzTtXcfOlzGEee8O+c+MmUgGrNiBcXnuWxuFJHe6a5Hz7qwhwe5OgaSYI0IJvkLqWX1ASG+cJOkEiA==", - "dev": true, + "devOptional": true, "license": "MIT", "engines": { "node": ">=10" @@ -14734,14 +14726,14 @@ "version": "18.3.1", "resolved": "https://registry.npmjs.org/react-is/-/react-is-18.3.1.tgz", "integrity": "sha512-/LLMVyas0ljjAtoYiPqYiL8VWXzUUdThrmU5+n20DZv+a+ClRoevUzw5JxU+Ieh5/c87ytoTBV9G1FiKfNJdmg==", - "dev": true, + "devOptional": true, "license": "MIT" }, "node_modules/jest-validate/node_modules/supports-color": { "version": "7.2.0", "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz", "integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==", - "dev": true, + "devOptional": true, "license": "MIT", "dependencies": { "has-flag": "^4.0.0" @@ -14850,7 +14842,7 @@ "version": "29.7.0", "resolved": "https://registry.npmjs.org/jest-worker/-/jest-worker-29.7.0.tgz", "integrity": "sha512-eIz2msL/EzL9UFTFFx7jBTkeZfku0yUAyZZZmJ93H2TYEiroIx2PQjEXcwYtYl8zXCxb+PAmA2hLIt/6ZEkPHw==", - "dev": true, + "devOptional": true, "license": "MIT", "dependencies": { "@types/node": "*", @@ -14866,7 +14858,7 @@ "version": "4.0.0", "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==", - "dev": true, + "devOptional": true, "license": "MIT", "engines": { "node": ">=8" @@ -14876,7 +14868,7 @@ "version": "8.1.1", "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-8.1.1.tgz", "integrity": "sha512-MpUEN2OodtUzxvKQl72cUF7RQ5EiHsGvSsVG0ia9c5RbWGL2CI4C7EpPS8UTBIplnlzZiNuV56w+FuNxy3ty2Q==", - "dev": true, + "devOptional": true, "license": "MIT", "dependencies": { "has-flag": "^4.0.0" @@ -15368,7 +15360,6 @@ "version": "1.5.0", "resolved": "https://registry.npmjs.org/lz-string/-/lz-string-1.5.0.tgz", "integrity": "sha512-h5bgJWpxJNswbU7qCrV0tIKQCaS3blPDrqKWx+QxzuzL1zGUzij9XCWLrSLsJPu5t+eWA/ycetzYAO5IOMcWAQ==", - "dev": true, "license": "MIT", "bin": { "lz-string": "bin/bin.js" @@ -15402,7 +15393,7 @@ "version": "1.3.6", "resolved": "https://registry.npmjs.org/make-error/-/make-error-1.3.6.tgz", "integrity": "sha512-s8UhlNe7vPKomQhC1qFelMokr/Sc3AgNbso3n74mVPA5LTZwkB9NlXf4XPamLxJE8h0gh73rM94xvwRT2CVInw==", - "dev": true, + "devOptional": true, "license": "ISC" }, "node_modules/makeerror": { @@ -20668,7 +20659,7 @@ "version": "2.0.2", "resolved": "https://registry.npmjs.org/resolve.exports/-/resolve.exports-2.0.2.tgz", "integrity": "sha512-X2UW6Nw3n/aMgDVy+0rSqgHlv39WZAlZrXCdnbyEiKm17DSqHX4MmQMaST3FbeWR5FTuRcUwYAziZajji0Y7mg==", - "dev": true, + "devOptional": true, "license": "MIT", "engines": { "node": ">=10" @@ -22588,7 +22579,7 @@ "version": "10.9.2", "resolved": "https://registry.npmjs.org/ts-node/-/ts-node-10.9.2.tgz", "integrity": "sha512-f0FFpIdcHgn8zcPSbf1dRevwt047YMnaiJM3u2w2RewrB+fob/zePZcrOyQoLMMO7aBIddLcQIEK5dYjkLnGrQ==", - "dev": true, + "devOptional": true, "license": "MIT", "dependencies": { "@cspotcode/source-map-support": "^0.8.0", @@ -22632,7 +22623,7 @@ "version": "4.1.3", "resolved": "https://registry.npmjs.org/arg/-/arg-4.1.3.tgz", "integrity": "sha512-58S9QDqG0Xx27YwPSt9fJxivjYl432YCwfDMfZ+71RAqUrZef7LrKQZ3LHLOwCS4FLNBplP533Zx895SeOCHvA==", - "dev": true, + "devOptional": true, "license": "MIT" }, "node_modules/tsconfig-paths": { @@ -23056,7 +23047,7 @@ "version": "3.0.1", "resolved": "https://registry.npmjs.org/v8-compile-cache-lib/-/v8-compile-cache-lib-3.0.1.tgz", "integrity": "sha512-wa7YjyUGfNZngI/vtK0UHAN+lgDCxBPCylVXGp0zu59Fz5aiGtNXaq3DhIov063MorB+VfufLh3JlF2KdTK3xg==", - "dev": true, + "devOptional": true, "license": "MIT" }, "node_modules/v8-to-istanbul": { @@ -24118,7 +24109,7 @@ "version": "3.1.1", "resolved": "https://registry.npmjs.org/yn/-/yn-3.1.1.tgz", "integrity": "sha512-Ux4ygGWsu2c7isFWe8Yu1YluJmqVhxqK2cLXNQA5AcC3QfbGNpM7fu0Y8b/z16pXLnFxZYvWhd3fhBY9DLmC6Q==", - "dev": true, + "devOptional": true, "license": "MIT", "engines": { "node": ">=6" diff --git a/package.json b/package.json index cf6e1bc772..fc2b66a549 100644 --- a/package.json +++ b/package.json @@ -20,6 +20,7 @@ "build": "react-scripts build", "test": "react-scripts test", "test:cov": "react-scripts test --coverage --watchAll", + "test:json": "react-scripts test --json --watchAll=false --outputFile jest-output.json --coverage", "eject": "react-scripts eject", "lint": "eslint ./src --ext .tsx --ext .ts --max-warnings 0", "eslint-output": "eslint-output ./src --ext .tsx --ext .ts --max-warnings 0", diff --git a/public/tasks/task-components.md b/public/tasks/task-components.md new file mode 100644 index 0000000000..a6ff48694d --- /dev/null +++ b/public/tasks/task-components.md @@ -0,0 +1,5 @@ +# Task - Components + +Version: 0.0.1 + +Fix some components that are using state incorrectly. diff --git a/public/tasks/task-nested.md b/public/tasks/task-nested.md new file mode 100644 index 0000000000..6d29f9369f --- /dev/null +++ b/public/tasks/task-nested.md @@ -0,0 +1,5 @@ +# Task - Nested + +Version: 0.0.1 + +Implement functions that work with nested arrays and objects immutably. diff --git a/public/tasks/task-objects.md b/public/tasks/task-objects.md new file mode 100644 index 0000000000..480889da0d --- /dev/null +++ b/public/tasks/task-objects.md @@ -0,0 +1,5 @@ +# Task - Objects + +Version: 0.0.1 + +Implement functions that work with objects immutably. diff --git a/public/tasks/task-state.md b/public/tasks/task-state.md new file mode 100644 index 0000000000..ef8197ffcb --- /dev/null +++ b/public/tasks/task-state.md @@ -0,0 +1,5 @@ +# Task - State + +Version: 0.0.1 + +Create some new components that have React State. diff --git a/src/App.tsx b/src/App.tsx index a8b41197f2..9629f8c7f0 100644 --- a/src/App.tsx +++ b/src/App.tsx @@ -1,5 +1,12 @@ import React from "react"; import "./App.css"; +import { ChangeType } from "./components/ChangeType"; +import { RevealAnswer } from "./components/RevealAnswer"; +import { TwoDice } from "./components/TwoDice"; +import { Counter } from "./components/Counter"; +import { DoubleHalf } from "./bad-components/DoubleHalf"; +import { ColoredBox } from "./bad-components/ColoredBox"; +import { ChooseTeam } from "./bad-components/ChooseTeam"; function App(): React.JSX.Element { return ( @@ -7,10 +14,20 @@ function App(): React.JSX.Element {
UM COS420 with React Hooks and TypeScript
-

- Edit src/App.tsx and save. This page will - automatically reload. -

+
+ {/* */} +
+ +
+ +
+ +
+ +
+ +
+ ); } diff --git a/src/bad-components/ChooseTeam.test.tsx b/src/bad-components/ChooseTeam.test.tsx new file mode 100644 index 0000000000..66eee4be70 --- /dev/null +++ b/src/bad-components/ChooseTeam.test.tsx @@ -0,0 +1,81 @@ +import React, { act } from "react"; +import { render, screen } from "@testing-library/react"; +import { ChooseTeam } from "./ChooseTeam"; + +describe("ChooseTeam Component tests", () => { + beforeEach(() => { + render(); + }); + test("(2 pts) The initial team is empty", () => { + const currentTeam = screen.queryAllByRole("listitem"); + expect(currentTeam).toHaveLength(0); + }); + test("(2 pts) There are 7 buttons.", () => { + const adders = screen.queryAllByRole("button"); + expect(adders).toHaveLength(7); + }); + test("(2 pts) Clicking first team member works", async () => { + const first = screen.queryAllByRole("button")[0]; + await act(async () => { + first.click(); + }); + const currentTeam = screen.queryAllByRole("listitem"); + expect(currentTeam).toHaveLength(1); + expect(currentTeam[0].textContent).toEqual(first.textContent); + }); + test("(2 pts) Clicking the third team member works", async () => { + const third = screen.queryAllByRole("button")[2]; + await act(async () => { + third.click(); + }); + const currentTeam = screen.queryAllByRole("listitem"); + expect(currentTeam).toHaveLength(1); + expect(currentTeam[0].textContent).toEqual(third.textContent); + }); + test("(2 pts) Clicking three team members works", async () => { + const [, second, third, , fifth] = screen.queryAllByRole("button"); + await act(async () => { + third.click(); + }); + await act(async () => { + second.click(); + }); + await act(async () => { + fifth.click(); + }); + const currentTeam = screen.queryAllByRole("listitem"); + expect(currentTeam).toHaveLength(3); + expect(currentTeam[0].textContent).toEqual(third.textContent); + expect(currentTeam[1].textContent).toEqual(second.textContent); + expect(currentTeam[2].textContent).toEqual(fifth.textContent); + }); + test("(2 pts) Clearing the team works", async () => { + const [, second, third, fourth, fifth, , clear] = + screen.queryAllByRole("button"); + await act(async () => { + third.click(); + }); + await act(async () => { + second.click(); + }); + await act(async () => { + fifth.click(); + }); + let currentTeam = screen.queryAllByRole("listitem"); + expect(currentTeam).toHaveLength(3); + expect(currentTeam[0].textContent).toEqual(third.textContent); + expect(currentTeam[1].textContent).toEqual(second.textContent); + expect(currentTeam[2].textContent).toEqual(fifth.textContent); + await act(async () => { + clear.click(); + }); + currentTeam = screen.queryAllByRole("listitem"); + expect(currentTeam).toHaveLength(0); + await act(async () => { + fourth.click(); + }); + currentTeam = screen.queryAllByRole("listitem"); + expect(currentTeam).toHaveLength(1); + expect(currentTeam[0].textContent).toEqual(fourth.textContent); + }); +}); diff --git a/src/bad-components/ChooseTeam.tsx b/src/bad-components/ChooseTeam.tsx new file mode 100644 index 0000000000..7b5a9d3cec --- /dev/null +++ b/src/bad-components/ChooseTeam.tsx @@ -0,0 +1,56 @@ +import React, { useState } from "react"; +import { Button, Row, Col } from "react-bootstrap"; + +const PEOPLE = [ + "Alan Turing", + "Grace Hopper", + "Ada Lovelace", + "Charles Babbage", + "Barbara Liskov", + "Margaret Hamilton", +]; + +export function ChooseTeam(): React.JSX.Element { + const [allOptions] = useState(PEOPLE); + const [team, setTeam] = useState([]); + + function chooseMember(newMember: string) { + if (!team.includes(newMember)) { + setTeam([...team, newMember]); + } + } + + function clearTeam() { + setTeam([]); + } + + return ( +
+

Choose Team

+ + + {allOptions.map((option: string) => ( +
+ Add{" "} + +
+ ))} + + + Team: +
    + {team.map((member: string) => ( +
  • {member}
  • + ))} +
+ + +
+
+ ); +} diff --git a/src/bad-components/ColoredBox.test.tsx b/src/bad-components/ColoredBox.test.tsx new file mode 100644 index 0000000000..5762afefb6 --- /dev/null +++ b/src/bad-components/ColoredBox.test.tsx @@ -0,0 +1,37 @@ +import React, { act } from "react"; +import { render, screen } from "@testing-library/react"; +import { ColoredBox } from "./ColoredBox"; + +describe("ColoredBox Component tests", () => { + beforeEach(() => { + render(); + }); + test("(2 pts) The ColoredBox is initially red.", () => { + const box = screen.getByTestId("colored-box"); + expect(box).toHaveStyle({ backgroundColor: "red" }); + }); + test("(2 pts) There is a button", () => { + expect(screen.getByRole("button")).toBeInTheDocument(); + }); + test("(2 pts) Clicking the button advances the color.", async () => { + const nextColor = screen.getByRole("button"); + await act(async () => { + nextColor.click(); + }); + expect(screen.getByTestId("colored-box")).toHaveStyle({ + backgroundColor: "blue", + }); + await act(async () => { + nextColor.click(); + }); + expect(screen.getByTestId("colored-box")).toHaveStyle({ + backgroundColor: "green", + }); + await act(async () => { + nextColor.click(); + }); + expect(screen.getByTestId("colored-box")).toHaveStyle({ + backgroundColor: "red", + }); + }); +}); diff --git a/src/bad-components/ColoredBox.tsx b/src/bad-components/ColoredBox.tsx new file mode 100644 index 0000000000..2a2fe1f0a9 --- /dev/null +++ b/src/bad-components/ColoredBox.tsx @@ -0,0 +1,61 @@ +import React, { useState } from "react"; +import { Button } from "react-bootstrap"; + +export const COLORS = ["red", "blue", "green"]; +const DEFAULT_COLOR_INDEX = 0; + +function ChangeColor({ + colorIndex, + setColorIndex, +}: { + colorIndex: number; + setColorIndex: (index: number) => void; +}): React.JSX.Element { + return ( + + ); +} + +function ColorPreview({ + colorIndex, +}: { + colorIndex: number; +}): React.JSX.Element { + return ( +
+ ); +} + +export function ColoredBox(): React.JSX.Element { + const [colorIndex, setColorIndex] = useState(DEFAULT_COLOR_INDEX); + + return ( +
+

Colored Box

+ The current color is: {COLORS[colorIndex]} +
+ + +
+
+ ); +} diff --git a/src/bad-components/DoubleHalf.test.tsx b/src/bad-components/DoubleHalf.test.tsx new file mode 100644 index 0000000000..9b2a031acf --- /dev/null +++ b/src/bad-components/DoubleHalf.test.tsx @@ -0,0 +1,72 @@ +import React, { act } from "react"; +import { render, screen } from "@testing-library/react"; +import { DoubleHalf } from "./DoubleHalf"; + +describe("DoubleHalf Component tests", () => { + beforeEach(() => { + render(); + }); + test("(2 pts) The DoubleHalf value is initially 10", () => { + expect(screen.getByText("10")).toBeInTheDocument(); + expect(screen.queryByText("20")).not.toBeInTheDocument(); + expect(screen.queryByText("5")).not.toBeInTheDocument(); + }); + test("(2 pts) There are Double and Halve buttons", () => { + expect( + screen.getByRole("button", { name: /Double/i }), + ).toBeInTheDocument(); + expect( + screen.getByRole("button", { name: /Halve/i }), + ).toBeInTheDocument(); + }); + test("(2 pts) You can double the number.", async () => { + const double = screen.getByRole("button", { name: /Double/i }); + await act(async () => { + double.click(); + }); + expect(screen.getByText("20")).toBeInTheDocument(); + expect(screen.queryByText("10")).not.toBeInTheDocument(); + }); + test("(2 pts) You can halve the number.", async () => { + const halve = screen.getByRole("button", { name: /Halve/i }); + await act(async () => { + halve.click(); + }); + expect(screen.getByText("5")).toBeInTheDocument(); + expect(screen.queryByText("10")).not.toBeInTheDocument(); + }); + test("(2 pts) You can double AND halve the number.", async () => { + const double = screen.getByRole("button", { name: /Double/i }); + const halve = screen.getByRole("button", { name: /Halve/i }); + await act(async () => { + double.click(); + }); + expect(screen.getByText("20")).toBeInTheDocument(); + expect(screen.queryByText("10")).not.toBeInTheDocument(); + await act(async () => { + double.click(); + }); + expect(screen.getByText("40")).toBeInTheDocument(); + expect(screen.queryByText("20")).not.toBeInTheDocument(); + await act(async () => { + halve.click(); + }); + expect(screen.getByText("20")).toBeInTheDocument(); + expect(screen.queryByText("10")).not.toBeInTheDocument(); + await act(async () => { + halve.click(); + }); + expect(screen.getByText("10")).toBeInTheDocument(); + expect(screen.queryByText("20")).not.toBeInTheDocument(); + await act(async () => { + halve.click(); + }); + expect(screen.getByText("5")).toBeInTheDocument(); + expect(screen.queryByText("10")).not.toBeInTheDocument(); + await act(async () => { + halve.click(); + }); + expect(screen.getByText("2.5")).toBeInTheDocument(); + expect(screen.queryByText("5")).not.toBeInTheDocument(); + }); +}); diff --git a/src/bad-components/DoubleHalf.tsx b/src/bad-components/DoubleHalf.tsx new file mode 100644 index 0000000000..23b390ca95 --- /dev/null +++ b/src/bad-components/DoubleHalf.tsx @@ -0,0 +1,26 @@ +import React, { useState } from "react"; +import { Button } from "react-bootstrap"; + + +function Doubler({ onDouble }: { onDouble: () => void }): React.JSX.Element { + return ; +} + +function Halver({ onHalve }: { onHalve: () => void }): React.JSX.Element { + return ; +} + +export function DoubleHalf(): React.JSX.Element { + const [dhValue, setDhValue] = useState(10); // Start from 1 or 0 + + return ( +
+

Double Half

+
+ The current value is: {dhValue} +
+ { setDhValue(dhValue * 2); }} /> + { setDhValue(dhValue * 0.5); }} /> +
+ ); +} diff --git a/src/components/ChangeType.test.tsx b/src/components/ChangeType.test.tsx new file mode 100644 index 0000000000..f0ee545cc3 --- /dev/null +++ b/src/components/ChangeType.test.tsx @@ -0,0 +1,59 @@ +import React, { act } from "react"; +import { render, screen } from "@testing-library/react"; +import { ChangeType } from "./ChangeType"; + +describe("ChangeType Component tests", () => { + beforeEach(() => { + render(); + }); + test("(1 pts) The initial type is Short Answer", () => { + // We use `getByText` because the text MUST be there + const typeText = screen.getByText(/Short Answer/i); + expect(typeText).toBeInTheDocument(); + }); + test("(1 pts) The initial type is not Multiple Choice", () => { + // We use `queryByText` because the text might not be there + const typeText = screen.queryByText(/Multiple Choice/i); + expect(typeText).toBeNull(); + }); + + test("(1 pts) There is a button labeled Change Type", () => { + const changeTypeButton = screen.getByRole("button", { + name: /Change Type/i + }); + expect(changeTypeButton).toBeInTheDocument(); + }); + + test("(1 pts) Clicking the button changes the type.", async () => { + const changeTypeButton = screen.getByRole("button", { + name: /Change Type/i + }); + await act(async () => { + changeTypeButton.click(); + }); + // Should be Multiple Choice + const typeTextMC = screen.getByText(/Multiple Choice/i); + expect(typeTextMC).toBeInTheDocument(); + // Should NOT be Short Answer + const typeTextSA = screen.queryByText(/Short Answer/i); + expect(typeTextSA).toBeNull(); + }); + + test("(1 pts) Clicking the button twice keeps the type the same.", async () => { + const changeTypeButton = screen.getByRole("button", { + name: /Change Type/i + }); + await act(async () => { + changeTypeButton.click(); + }); + await act(async () => { + changeTypeButton.click(); + }); + // Should be Short Answer + const typeTextSA = screen.getByText(/Short Answer/i); + expect(typeTextSA).toBeInTheDocument(); + // Should NOT be Multiple Choice + const typeTextMC = screen.queryByText(/Multiple Choice/i); + expect(typeTextMC).toBeNull(); + }); +}); diff --git a/src/components/ChangeType.tsx b/src/components/ChangeType.tsx new file mode 100644 index 0000000000..f9dd4b51d2 --- /dev/null +++ b/src/components/ChangeType.tsx @@ -0,0 +1,31 @@ +import React, { useState } from "react"; +import { Button } from "react-bootstrap"; +import { QuestionType } from "../interfaces/question"; + +export function ChangeType(): React.JSX.Element { + // State to hold the current question type + const [type, setType] = useState("short_answer_question"); + + // Function to toggle between question types + const changeType = () => { + setType((prevType) => + prevType === "short_answer_question" ? + "multiple_choice_question" + : "short_answer_question", + ); + }; + + return ( +
+ {/* Button to change the question type */} + + + {/* Display the current question type */} +

+ {type === "short_answer_question" ? + "Short Answer" + : "Multiple Choice"} +

+
+ ); +} diff --git a/src/components/Counter.test.tsx b/src/components/Counter.test.tsx new file mode 100644 index 0000000000..d08773f5c6 --- /dev/null +++ b/src/components/Counter.test.tsx @@ -0,0 +1,45 @@ +import React, { act } from "react"; +import { render, screen } from "@testing-library/react"; +import { Counter } from "./Counter"; + +describe("Counter Component tests", () => { + beforeEach(() => { + render(); + }); + test("(1 pts) The initial value is 0", () => { + // We use `getByText` because the text MUST be there + const valueText = screen.getByText(/0/i); + expect(valueText).toBeInTheDocument(); + }); + test("(1 pts) The initial value is not 1", () => { + // We use `queryByText` because the text might not be there + const valueText = screen.queryByText(/1/i); + expect(valueText).toBeNull(); + }); + + test("(1 pts) There is a button named Add One", () => { + const addOneButton = screen.getByRole("button", { name: /Add One/i }); + expect(addOneButton).toBeInTheDocument(); + }); + + test("(1 pts) Clicking the button once adds one", async () => { + const addOneButton = screen.getByRole("button", { name: /Add One/i }); + await act(async () => { + addOneButton.click(); + }); + const valueText = screen.getByText(/1/i); + expect(valueText).toBeInTheDocument(); + }); + + test("(1 pts) Clicking the button twice adds two", async () => { + const addOneButton = screen.getByRole("button", { name: /Add One/i }); + await act(async () => { + addOneButton.click(); + }); + await act(async () => { + addOneButton.click(); + }); + const valueText = screen.getByText(/2/i); + expect(valueText).toBeInTheDocument(); + }); +}); diff --git a/src/components/Counter.tsx b/src/components/Counter.tsx new file mode 100644 index 0000000000..2a546c1747 --- /dev/null +++ b/src/components/Counter.tsx @@ -0,0 +1,12 @@ +import React, { useState } from "react"; +import { Button } from "react-bootstrap"; + +export function Counter(): React.JSX.Element { + const [value, setValue] = useState(0); + return ( + + + to {value}. + + ); +} diff --git a/src/components/CycleHoliday.test.tsx b/src/components/CycleHoliday.test.tsx new file mode 100644 index 0000000000..145e2cb3c8 --- /dev/null +++ b/src/components/CycleHoliday.test.tsx @@ -0,0 +1,55 @@ +import React from "react"; +import { render, screen } from "@testing-library/react"; +import { CycleHoliday } from "./CycleHoliday"; + +describe("CycleHoliday Component tests", () => { + beforeEach(() => { + render(); + }); + + test("An initial holiday is displayed", () => { + const initialHoliday = screen.getByText(/Holiday: (.*)/i); + expect(initialHoliday).toBeInTheDocument(); + }); + + test("There are two buttons", () => { + const alphabetButton = screen.getByRole("button", { + name: /Alphabet/i + }); + const yearButton = screen.getByRole("button", { + name: /Year/i + }); + expect(alphabetButton).toBeInTheDocument(); + expect(yearButton).toBeInTheDocument(); + }); + + test("Can cycle through 5 distinct holidays alphabetically", () => { + const alphabetButton = screen.getByRole("button", { + name: /Alphabet/i + }); + const initialHoliday = screen.getByText(/Holiday ?[:)-](.*)/i); + const states: string[] = []; + for (let i = 0; i < 6; i++) { + states.push(initialHoliday.textContent || ""); + alphabetButton.click(); + } + const uniqueStates = states.filter((x, y) => states.indexOf(x) == y); + expect(uniqueStates).toHaveLength(5); + expect(states[0]).toEqual(states[5]); + }); + + test("Can cycle through 5 distinct holidays by year", () => { + const yearButton = screen.getByRole("button", { + name: /Year/i + }); + const initialHoliday = screen.getByText(/Holiday ?[:)-](.*)/i); + const states: string[] = []; + for (let i = 0; i < 6; i++) { + states.push(initialHoliday.textContent || ""); + yearButton.click(); + } + const uniqueStates = states.filter((x, y) => states.indexOf(x) == y); + expect(uniqueStates).toHaveLength(5); + expect(states[0]).toEqual(states[5]); + }); +}); diff --git a/src/components/CycleHoliday.tsx b/src/components/CycleHoliday.tsx new file mode 100644 index 0000000000..4485797b7d --- /dev/null +++ b/src/components/CycleHoliday.tsx @@ -0,0 +1,55 @@ +import React, { useState } from "react"; +import { Button } from "react-bootstrap"; + +// Define a list of holidays with their emojis and dates (for chronological cycling) +const holidays = [ + { name: "New Year", emoji: "🎆", date: "2025-01-01" }, + { name: "Dragon Boat Festival", emoji: "🎏", date: "2025-06-14" }, + { name: "Halloween", emoji: "🎃", date: "2025-10-31" }, + { name: "Diwali", emoji: "🪔", date: "2025-11-14" }, + { name: "Christmas", emoji: "🎄", date: "2025-12-25" }, +]; + +export function CycleHoliday(): React.JSX.Element { + // State to hold the current holiday object + const [currentHoliday, setCurrentHoliday] = useState(holidays[0]); + + // Function to advance holidays alphabetically + const nextHolidayAlphabetically = () => { + const sortedHolidays = [...holidays].sort((a, b) => + a.name.localeCompare(b.name), + ); + const currentIndex = sortedHolidays.indexOf(currentHoliday); + setCurrentHoliday( + sortedHolidays[(currentIndex + 1) % sortedHolidays.length], + ); + }; + + // Function to advance holidays chronologically (by date) + const nextHolidayByYear = () => { + const sortedByDate = [...holidays].sort( + (a, b) => new Date(a.date).getTime() - new Date(b.date).getTime(), + ); + const currentIndex = sortedByDate.indexOf(currentHoliday); + setCurrentHoliday( + sortedByDate[(currentIndex + 1) % sortedByDate.length], + ); + }; + + return ( +
+ {/* Display the current holiday */} +

+ Holiday: {currentHoliday.emoji} {currentHoliday.name} +

+ + {/* Button to advance by alphabetical order */} + + + {/* Button to advance by chronological order */} + +
+ ); +} diff --git a/src/components/RevealAnswer.test.tsx b/src/components/RevealAnswer.test.tsx new file mode 100644 index 0000000000..6b2076ad1a --- /dev/null +++ b/src/components/RevealAnswer.test.tsx @@ -0,0 +1,42 @@ +import React, { act } from "react"; +import { render, screen } from "@testing-library/react"; +import { RevealAnswer } from "./RevealAnswer"; + +describe("RevealAnswer Component tests", () => { + beforeEach(() => { + render(); + }); + test("(1 pts) The answer '42' is not visible initially", () => { + const answerText = screen.queryByText(/42/); + expect(answerText).toBeNull(); + }); + test("(1 pts) There is a Reveal Answer button", () => { + const revealButton = screen.getByRole("button", { + name: /Reveal Answer/i + }); + expect(revealButton).toBeInTheDocument(); + }); + test("(1 pts) Clicking Reveal Answer button reveals the '42'", async () => { + const revealButton = screen.getByRole("button", { + name: /Reveal Answer/i + }); + await act(async () => { + revealButton.click(); + }); + const answerText = screen.getByText(/42/); + expect(answerText).toBeInTheDocument(); + }); + test("(1 pts) Clicking Reveal Answer button twice hides the '42'", async () => { + const revealButton = screen.getByRole("button", { + name: /Reveal Answer/i + }); + await act(async () => { + revealButton.click(); + }); + await act(async () => { + revealButton.click(); + }); + const answerText = screen.queryByText(/42/); + expect(answerText).toBeNull(); + }); +}); diff --git a/src/components/RevealAnswer.tsx b/src/components/RevealAnswer.tsx new file mode 100644 index 0000000000..3907f37061 --- /dev/null +++ b/src/components/RevealAnswer.tsx @@ -0,0 +1,22 @@ +import React, { useState } from "react"; +import { Button } from "react-bootstrap"; + +export function RevealAnswer(): React.JSX.Element { + // State to control the visibility of the answer + const [visible, setVisible] = useState(false); + + // Function to toggle visibility + const toggleAnswer = () => { + setVisible(!visible); + }; + + return ( +
+ {/* Button to reveal/hide the answer */} + + + {/* Display the answer when 'visible' state is true */} + {visible &&

42

} +
+ ); +} diff --git a/src/components/StartAttempt.test.tsx b/src/components/StartAttempt.test.tsx new file mode 100644 index 0000000000..3d41c953cf --- /dev/null +++ b/src/components/StartAttempt.test.tsx @@ -0,0 +1,196 @@ +import React from "react"; +import { render, screen } from "@testing-library/react"; +import { StartAttempt } from "./StartAttempt"; + +/*** + * Helper function to extract a number from an HTMLElement's textContent. + * + * If you aren't familiar with Regular Expressions: + * https://developer.mozilla.org/en-US/docs/Web/JavaScript/Guide/Regular_Expressions + */ +export function extractDigits(element: HTMLElement): number | null { + const attemptNumberText = element.textContent || ""; + // We use a "regular expression" to find digits and extract them as text + const attemptNumberDigitsMatched = attemptNumberText.match(/\d+/); + // Provides a Matched Regular Expression or null + if (attemptNumberDigitsMatched === null) { + // Should never be possible, since then there was no number to have found. + // But TypeScript is cautious and demands we provide SOMETHING. + return null; + } else { + // Not null, get the first matched value and convert to number + return parseInt(attemptNumberDigitsMatched[0]); + } +} + +describe("StartAttempt Component tests", () => { + beforeEach(() => { + render(); + }); + test("The Number of attempts is displayed initially, without other numbers", () => { + const attemptNumber = screen.getByText(/(\d+)/); + expect(attemptNumber).toBeInTheDocument(); + }); + test("The Number of attempts is more than 0", () => { + const attemptNumber = extractDigits(screen.getByText(/(\d+)/)); + expect(attemptNumber).toBeGreaterThan(0); + }); + test("The Number of attempts is less than 10", () => { + const attemptNumber = extractDigits(screen.getByText(/(\d+)/)); + expect(attemptNumber).toBeLessThan(10); + }); + test("There is an initially enabled Start Quiz button", () => { + const startButton = screen.getByRole("button", { name: /Start Quiz/i }); + expect(startButton).toBeInTheDocument(); + expect(startButton).toBeEnabled(); + }); + test("There is an initially disabled Stop Quiz button", () => { + const stopButton = screen.getByRole("button", { name: /Stop Quiz/i }); + expect(stopButton).toBeInTheDocument(); + expect(stopButton).toBeDisabled(); + }); + test("There is an initially enabled Mulligan button", () => { + const mulliganButton = screen.getByRole("button", { + name: /Mulligan/i + }); + expect(mulliganButton).toBeInTheDocument(); + expect(mulliganButton).toBeEnabled(); + }); + test("Clicking Mulligan increases attempts", () => { + const attemptNumber: number = + extractDigits(screen.getByText(/(\d+)/)) || 0; + const mulliganButton = screen.getByRole("button", { + name: /Mulligan/i + }); + mulliganButton.click(); + const attemptNumberLater = extractDigits(screen.getByText(/(\d+)/)); + expect(attemptNumber + 1).toEqual(attemptNumberLater); + }); + test("Clicking Mulligan twice increases attempts by two", () => { + const attemptNumber: number = + extractDigits(screen.getByText(/(\d+)/)) || 0; + const mulliganButton = screen.getByRole("button", { + name: /Mulligan/i + }); + mulliganButton.click(); + mulliganButton.click(); + const attemptNumberLater = extractDigits(screen.getByText(/(\d+)/)); + expect(attemptNumber + 2).toEqual(attemptNumberLater); + }); + test("Clicking Start Quiz decreases attempts", () => { + const attemptNumber: number = + extractDigits(screen.getByText(/(\d+)/)) || 0; + const startButton = screen.getByRole("button", { + name: /Start Quiz/i + }); + startButton.click(); + const attemptNumberLater = + extractDigits(screen.getByText(/(\d+)/)) || 0; + expect(attemptNumber - 1).toEqual(attemptNumberLater); + }); + test("Clicking Start Quiz changes enabled buttons", () => { + // Given the buttons... + const startButton = screen.getByRole("button", { + name: /Start Quiz/i + }); + const stopButton = screen.getByRole("button", { name: /Stop Quiz/i }); + const mulliganButton = screen.getByRole("button", { + name: /Mulligan/i + }); + // When the start button is clicked + startButton.click(); + // Then the start is disabled, stop is enabled, and mulligan is disabled + expect(startButton).toBeDisabled(); + expect(stopButton).toBeEnabled(); + expect(mulliganButton).toBeDisabled(); + }); + test("Clicking Start and Stop Quiz changes enabled buttons", () => { + // Given the buttons and initial attempt number... + const startButton = screen.getByRole("button", { + name: /Start Quiz/i + }); + const stopButton = screen.getByRole("button", { name: /Stop Quiz/i }); + const mulliganButton = screen.getByRole("button", { + name: /Mulligan/i + }); + // When we click the start button and then the stop button + startButton.click(); + stopButton.click(); + // Then the start is enabled, stop is disabled, and mulligan is enabled + expect(startButton).toBeEnabled(); + expect(stopButton).toBeDisabled(); + expect(mulliganButton).toBeEnabled(); + }); + test("Clicking Start, Stop, Mulligan sets attempts to original", () => { + // Given the buttons and initial attempt number... + const startButton = screen.getByRole("button", { + name: /Start Quiz/i + }); + const stopButton = screen.getByRole("button", { name: /Stop Quiz/i }); + const mulliganButton = screen.getByRole("button", { + name: /Mulligan/i + }); + const attemptNumber: number = + extractDigits(screen.getByText(/(\d+)/)) || 0; + // When we click the start button and then the stop button + startButton.click(); + stopButton.click(); + // Then the attempt is decreased + const attemptNumberLater: number = + extractDigits(screen.getByText(/(\d+)/)) || 0; + expect(attemptNumber - 1).toEqual(attemptNumberLater); + // And when we click the mulligan button + mulliganButton.click(); + // Then the attempt is increased back to starting value + const attemptNumberLatest: number = + extractDigits(screen.getByText(/(\d+)/)) || 0; + expect(attemptNumber).toEqual(attemptNumberLatest); + }); + test("Cannot click start quiz when out of attempts", () => { + // Given the buttons and initial attempt number... + const startButton = screen.getByRole("button", { + name: /Start Quiz/i + }); + const stopButton = screen.getByRole("button", { name: /Stop Quiz/i }); + const mulliganButton = screen.getByRole("button", { + name: /Mulligan/i + }); + let attemptNumber = extractDigits(screen.getByText(/(\d+)/)) || 0; + const initialAttempt = attemptNumber; + // Arbitrary number of times to try clicking; assume we do not have more than that number of attempts. + let maxAttempts = 10; + // While there are still attempts apparently available... + while (attemptNumber > 0) { + // Then the buttons + expect(startButton).toBeEnabled(); + expect(stopButton).toBeDisabled(); + expect(mulliganButton).toBeEnabled(); + // And when we Start and then immediately stop the quiz... + startButton.click(); + stopButton.click(); + // Then the number is going down, and doesn't go past 0 somehow + attemptNumber = extractDigits(screen.getByText(/(\d+)/)) || 0; + expect(attemptNumber).toBeGreaterThanOrEqual(0); + expect(attemptNumber).not.toEqual(initialAttempt); + // And then the maximum number of attempts does not exceed 10 + maxAttempts -= 1; + expect(maxAttempts).toBeGreaterThanOrEqual(0); + } + // Then the attempt is at zero + expect(attemptNumber).toEqual(0); + // And then the stop button is disabled, the start button is disabled, and mulligan is enabled + expect(startButton).toBeDisabled(); + expect(stopButton).toBeDisabled(); + expect(mulliganButton).toBeEnabled(); + // And when we click the mulligan button + mulliganButton.click(); + // Then the attempt is increased back to 1 + const attemptNumberLatest: number = + extractDigits(screen.getByText(/(\d+)/)) || 0; + expect(attemptNumberLatest).toEqual(1); + // And the start button is reenabled + expect(startButton).toBeEnabled(); + expect(stopButton).toBeDisabled(); + expect(mulliganButton).toBeEnabled(); + }); +}); diff --git a/src/components/StartAttempt.tsx b/src/components/StartAttempt.tsx new file mode 100644 index 0000000000..0543374f98 --- /dev/null +++ b/src/components/StartAttempt.tsx @@ -0,0 +1,50 @@ +import React, { useState } from "react"; +import { Button } from "react-bootstrap"; + +export function StartAttempt(): React.JSX.Element { + // State to track the number of attempts (starts at 4) + const [attempts, setAttempts] = useState(4); + + // State to track whether the quiz is in progress or not + const [inProgress, setInProgress] = useState(false); + + // Function to start the quiz (decrease attempts and set in progress to true) + const startQuiz = () => { + if (attempts > 0) { + setAttempts(attempts - 1); + setInProgress(true); + } + }; + + // Function to stop the quiz (set in progress to false) + const stopQuiz = () => { + setInProgress(false); + }; + + // Function to give a Mulligan (increase attempts by 1) + const mulligan = () => { + setAttempts(attempts + 1); + }; + + return ( +
+ {/* Display the number of attempts left */} +

Attempts Left: {attempts}

+ + {/* Start Quiz button (disabled when in progress or no attempts left) */} + + + {/* Stop Quiz button (disabled when not in progress) */} + + + {/* Mulligan button (disabled when quiz is in progress) */} + +
+ ); +} diff --git a/src/components/TwoDice.test.tsx b/src/components/TwoDice.test.tsx new file mode 100644 index 0000000000..01e5e732f0 --- /dev/null +++ b/src/components/TwoDice.test.tsx @@ -0,0 +1,194 @@ +import React, { act } from "react"; +import { render, screen } from "@testing-library/react"; +import { TwoDice } from "./TwoDice"; + +/*** + * Helper function to extract a number from an HTMLElement's textContent. + * + * If you aren't familiar with Regular Expressions: + * https://developer.mozilla.org/en-US/docs/Web/JavaScript/Guide/Regular_Expressions + */ +export function extractDigits(element: HTMLElement): number | null { + const attemptNumberText = element.textContent || ""; + // We use a "regular expression" to find digits and extract them as text + const attemptNumberDigitsMatched = attemptNumberText.match(/\d+/); + // Provides a Matched Regular Expression or null + if (attemptNumberDigitsMatched === null) { + // Should never be possible, since then there was no number to have found. + // But TypeScript is cautious and demands we provide SOMETHING. + return null; + } else { + // Not null, get the first matched value and convert to number + return parseInt(attemptNumberDigitsMatched[0]); + } +} + +describe("TwoDice Component tests", () => { + let mathRandomFunction: jest.SpyInstance; // eslint-disable-line + beforeEach(() => { + mathRandomFunction = jest + .spyOn(global.Math, "random") + .mockReturnValue(0.5) // 4 + .mockReturnValueOnce(0.0) // 1 + .mockReturnValueOnce(0.99) // 6 + .mockReturnValueOnce(0.75) // 5 + .mockReturnValueOnce(0.75) // 5 + .mockReturnValueOnce(0.1) // 1 + .mockReturnValueOnce(0.1); // 1 + }); + afterEach(() => { + jest.spyOn(global.Math, "random").mockRestore(); + }); + beforeEach(() => { + render(); + }); + test("(1 pts) There is a `left-die` and `right-die` testid", () => { + const leftDie = screen.getByTestId("left-die"); + const rightDie = screen.getByTestId("right-die"); + expect(leftDie).toBeInTheDocument(); + expect(rightDie).toBeInTheDocument(); + }); + test("(1 pts) The `left-die` and `right-die` are two different numbers", () => { + const leftDie = screen.getByTestId("left-die"); + const rightDie = screen.getByTestId("right-die"); + const leftNumber = extractDigits(leftDie); + const rightNumber = extractDigits(rightDie); + // Then they are two numbers + expect(leftNumber).not.toBeNull(); + expect(rightNumber).not.toBeNull(); + // Then they are two different numbers + expect(leftNumber).not.toEqual(rightNumber); + }); + test("(1 pts) There are two buttons present", () => { + const leftButton = screen.getByRole("button", { name: /Roll Left/i }); + const rightButton = screen.getByRole("button", { name: /Roll Right/i }); + expect(leftButton).toBeInTheDocument(); + expect(rightButton).toBeInTheDocument(); + }); + test("(1 pts) Clicking left button changes first number", async () => { + const leftButton = screen.getByRole("button", { name: /Roll Left/i }); + await act(async () => { + leftButton.click(); + }); + await act(async () => { + leftButton.click(); + }); + await act(async () => { + leftButton.click(); + }); + // Then the random function should be called 3 times + // expect(mathRandomFunction).toBeCalledTimes(3); + // And the number to be 5 + const leftNumber = extractDigits(screen.getByTestId("left-die")); + expect(leftNumber).toEqual(5); + }); + // Clicking right button changes second number + test("(1 pts) Clicking right button changes second number", async () => { + const rightButton = screen.getByRole("button", { name: /Roll Right/i }); + await act(async () => { + rightButton.click(); + }); + await act(async () => { + rightButton.click(); + }); + await act(async () => { + rightButton.click(); + }); + // Then the random function should be called 3 times + // expect(mathRandomFunction).toBeCalledTimes(3); + // And the number to be 5 + const rightNumber = extractDigits(screen.getByTestId("right-die")); + expect(rightNumber).toEqual(5); + }); + // Rolling two different numbers does not win or lose the game + test("(1 pts) Rolling two different numbers does not win or lose the game", async () => { + // Given + const leftButton = screen.getByRole("button", { name: /Roll Left/i }); + const rightButton = screen.getByRole("button", { name: /Roll Right/i }); + const leftDie = screen.getByTestId("left-die"); + const rightDie = screen.getByTestId("right-die"); + // When the left and right buttons are rolled once each + await act(async () => { + leftButton.click(); + }); + await act(async () => { + rightButton.click(); + }); + // Then the numbers are not equal + const leftNumber = extractDigits(leftDie); + const rightNumber = extractDigits(rightDie); + expect(leftNumber).toEqual(1); + expect(rightNumber).toEqual(6); + // And then the game is not won + const winText = screen.queryByText(/Win/i); + expect(winText).toBeNull(); + // And then nor is the game lost + const loseText = screen.queryByText(/Lose/i); + expect(loseText).toBeNull(); + }); + test("(1 pts) Getting snake eyes loses the game", async () => { + // Given + const leftButton = screen.getByRole("button", { name: /Roll Left/i }); + const rightButton = screen.getByRole("button", { name: /Roll Right/i }); + const leftDie = screen.getByTestId("left-die"); + const rightDie = screen.getByTestId("right-die"); + // When the left and right buttons are rolled once each + await act(async () => { + leftButton.click(); + }); + await act(async () => { + rightButton.click(); + }); + await act(async () => { + rightButton.click(); + }); + await act(async () => { + rightButton.click(); + }); + await act(async () => { + rightButton.click(); + }); + // Then the numbers are not equal + const leftNumber = extractDigits(leftDie); + const rightNumber = extractDigits(rightDie); + expect(leftNumber).toEqual(1); + expect(rightNumber).toEqual(1); + // And then the game is not won + const winText = screen.queryByText(/Win/i); + expect(winText).toBeNull(); + // And then the game is lost + const loseText = screen.getByText(/Lose/i); + expect(loseText).toBeInTheDocument(); + }); + test("(1 pts) Getting matching numbers wins the game", async () => { + // Given + const leftButton = screen.getByRole("button", { name: /Roll Left/i }); + const rightButton = screen.getByRole("button", { name: /Roll Right/i }); + const leftDie = screen.getByTestId("left-die"); + const rightDie = screen.getByTestId("right-die"); + // When the left and right buttons are rolled once each + await act(async () => { + leftButton.click(); + }); + await act(async () => { + leftButton.click(); + }); + await act(async () => { + leftButton.click(); + }); + await act(async () => { + rightButton.click(); + }); + // Then the numbers are not equal + const leftNumber = extractDigits(leftDie); + const rightNumber = extractDigits(rightDie); + expect(leftNumber).toEqual(5); + expect(rightNumber).toEqual(5); + // And then the game is not lost + const loseText = screen.queryByText(/Lose/i); + expect(loseText).toBeNull(); + // And then the game is won + const winText = screen.getByText(/Win/i); + expect(winText).toBeInTheDocument(); + }); +}); diff --git a/src/components/TwoDice.tsx b/src/components/TwoDice.tsx new file mode 100644 index 0000000000..3e3fc443a5 --- /dev/null +++ b/src/components/TwoDice.tsx @@ -0,0 +1,47 @@ +import React, { useState } from "react"; +import { Button } from "react-bootstrap"; + +export function d6(): number { + return 1 + Math.floor(Math.random() * 6); +} + +export function TwoDice(): React.JSX.Element { + // State to track the values of the dice + const [leftDie, setLeftDie] = useState(d6()); + const [rightDie, setRightDie] = useState(d6()); + + // Function to roll the left die + const rollLeft = () => { + setLeftDie(d6()); + }; + + // Function to roll the right die + const rollRight = () => { + setRightDie(d6()); + }; + + // Determine win/loss condition + const isWin = leftDie === rightDie; + const isLose = leftDie === 1 && rightDie === 1; + + return ( +
+
+ {/* Display left die */} + {leftDie} + {/* Roll button for the left die */} + +
+
+ {/* Display right die */} + {rightDie} + {/* Roll button for the right die */} + +
+ + {/* Display win/lose message */} + {isLose &&

Lose: Snake Eyes (1 and 1)!

} + {isWin && !isLose &&

Win: Both dice are the same!

} +
+ ); +} diff --git a/src/data/questions.json b/src/data/questions.json new file mode 100644 index 0000000000..0411f30afe --- /dev/null +++ b/src/data/questions.json @@ -0,0 +1,220 @@ +{ + "BLANK_QUESTIONS": [ + { + "id": 1, + "name": "Question 1", + "body": "", + "type": "multiple_choice_question", + "options": [], + "expected": "", + "points": 1, + "published": false + }, + { + "id": 47, + "name": "My New Question", + "body": "", + "type": "multiple_choice_question", + "options": [], + "expected": "", + "points": 1, + "published": false + }, + { + "id": 2, + "name": "Question 2", + "body": "", + "type": "short_answer_question", + "options": [], + "expected": "", + "points": 1, + "published": false + } + ], + "SIMPLE_QUESTIONS": [ + { + "id": 1, + "name": "Addition", + "body": "What is 2+2?", + "type": "short_answer_question", + "options": [], + "expected": "4", + "points": 1, + "published": true + }, + { + "id": 2, + "name": "Letters", + "body": "What is the last letter of the English alphabet?", + "type": "short_answer_question", + "options": [], + "expected": "Z", + "points": 1, + "published": false + }, + { + "id": 5, + "name": "Colors", + "body": "Which of these is a color?", + "type": "multiple_choice_question", + "options": ["red", "apple", "firetruck"], + "expected": "red", + "points": 1, + "published": true + }, + { + "id": 9, + "name": "Shapes", + "body": "What shape can you make with one line?", + "type": "multiple_choice_question", + "options": ["square", "triangle", "circle"], + "expected": "circle", + "points": 2, + "published": false + } + ], + "TRIVIA_QUESTIONS": [ + { + "id": 1, + "name": "Mascot", + "body": "What is the name of the UD Mascot?", + "type": "multiple_choice_question", + "options": ["Bluey", "YoUDee", "Charles the Wonder Dog"], + "expected": "YoUDee", + "points": 7, + "published": false + }, + { + "id": 2, + "name": "Motto", + "body": "What is the University of Delaware's motto?", + "type": "multiple_choice_question", + "options": [ + "Knowledge is the light of the mind", + "Just U Do it", + "Nothing, what's the motto with you?" + ], + "expected": "Knowledge is the light of the mind", + "points": 3, + "published": false + }, + { + "id": 3, + "name": "Goats", + "body": "How many goats are there usually on the Green?", + "type": "multiple_choice_question", + "options": [ + "Zero, why would there be goats on the green?", + "18420", + "Two" + ], + "expected": "Two", + "points": 10, + "published": false + } + ], + "EMPTY_QUESTIONS": [ + { + "id": 1, + "name": "Empty 1", + "body": "This question is not empty, right?", + "type": "multiple_choice_question", + "options": ["correct", "it is", "not"], + "expected": "correct", + "points": 5, + "published": true + }, + { + "id": 2, + "name": "Empty 2", + "body": "", + "type": "multiple_choice_question", + "options": ["this", "one", "is", "not", "empty", "either"], + "expected": "one", + "points": 5, + "published": true + }, + { + "id": 3, + "name": "Empty 3", + "body": "This questions is not empty either!", + "type": "short_answer_question", + "options": [], + "expected": "", + "points": 5, + "published": true + }, + { + "id": 4, + "name": "Empty 4", + "body": "", + "type": "short_answer_question", + "options": [], + "expected": "Even this one is not empty", + "points": 5, + "published": true + }, + { + "id": 5, + "name": "Empty 5 (Actual)", + "body": "", + "type": "short_answer_question", + "options": [], + "expected": "", + "points": 5, + "published": false + } + ], + "SIMPLE_QUESTIONS_2": [ + { + "id": 478, + "name": "Students", + "body": "How many students are taking CISC275 this semester?", + "type": "short_answer_question", + "options": [], + "expected": "90", + "points": 53, + "published": true + }, + { + "id": 1937, + "name": "Importance", + "body": "On a scale of 1 to 10, how important is this quiz for them?", + "type": "short_answer_question", + "options": [], + "expected": "10", + "points": 47, + "published": true + }, + { + "id": 479, + "name": "Sentience", + "body": "Is it technically possible for this quiz to become sentient?", + "type": "short_answer_question", + "options": [], + "expected": "Yes", + "points": 40, + "published": true + }, + { + "id": 777, + "name": "Danger", + "body": "If this quiz became sentient, would it pose a danger to others?", + "type": "short_answer_question", + "options": [], + "expected": "Yes", + "points": 60, + "published": true + }, + { + "id": 1937, + "name": "Listening", + "body": "Is this quiz listening to us right now?", + "type": "short_answer_question", + "options": [], + "expected": "Yes", + "points": 100, + "published": true + } + ] +} diff --git a/src/interfaces/answer.ts b/src/interfaces/answer.ts new file mode 100644 index 0000000000..743ee8dff9 --- /dev/null +++ b/src/interfaces/answer.ts @@ -0,0 +1,13 @@ +/*** + * A representation of a students' answer in a quizzing game + */ +export interface Answer { + /** The ID of the question being answered. */ + questionId: number; + /** The text that the student entered for their answer. */ + text: string; + /** Whether or not the student has submitted this answer. */ + submitted: boolean; + /** Whether or not the students' answer matched the expected. */ + correct: boolean; +} diff --git a/src/interfaces/question.ts b/src/interfaces/question.ts new file mode 100644 index 0000000000..5def48f2f7 --- /dev/null +++ b/src/interfaces/question.ts @@ -0,0 +1,22 @@ +/** QuestionType influences how a question is asked and what kinds of answers are possible */ +export type QuestionType = "multiple_choice_question" | "short_answer_question"; + +/** A representation of a Question in a quizzing application */ +export interface Question { + /** A unique identifier for the question */ + id: number; + /** The human-friendly title of the question */ + name: string; + /** The instructions and content of the Question */ + body: string; + /** The kind of Question; influences how the user answers and what options are displayed */ + type: QuestionType; + /** The possible answers for a Question (for Multiple Choice questions) */ + options: string[]; + /** The actually correct answer expected */ + expected: string; + /** How many points this question is worth, roughly indicating its importance and difficulty */ + points: number; + /** Whether or not this question is ready to display to students */ + published: boolean; +} diff --git a/src/nested.test.ts b/src/nested.test.ts new file mode 100644 index 0000000000..61b0282318 --- /dev/null +++ b/src/nested.test.ts @@ -0,0 +1,919 @@ +import { Question } from "./interfaces/question"; +import { + getPublishedQuestions, + getNonEmptyQuestions, + findQuestion, + removeQuestion, + getNames, + makeAnswers, + publishAll, + addNewQuestion, + renameQuestionById, + editOption, +} from "./nested"; +import testQuestionData from "./data/questions.json"; +import backupQuestionData from "./data/questions.json"; + +const { + BLANK_QUESTIONS, + SIMPLE_QUESTIONS, + TRIVIA_QUESTIONS, + EMPTY_QUESTIONS, + SIMPLE_QUESTIONS_2, +}: Record = + // Typecast the test data that we imported to be a record matching + // strings to the question list + testQuestionData as Record; + +// We have backup versions of the data to make sure all changes are immutable +const { + BLANK_QUESTIONS: BACKUP_BLANK_QUESTIONS, + SIMPLE_QUESTIONS: BACKUP_SIMPLE_QUESTIONS, + TRIVIA_QUESTIONS: BACKUP_TRIVIA_QUESTIONS, + EMPTY_QUESTIONS: BACKUP_EMPTY_QUESTIONS, + SIMPLE_QUESTIONS_2: BACKUP_SIMPLE_QUESTIONS_2, +}: Record = backupQuestionData as Record< + string, + Question[] +>; + +const NEW_BLANK_QUESTION = { + id: 142, + name: "A new question", + body: "", + type: "short_answer_question", + options: [], + expected: "", + points: 1, + published: false, +}; + +const NEW_TRIVIA_QUESTION = { + id: 449, + name: "Colors", + body: "", + type: "multiple_choice_question", + options: [], + expected: "", + /*body: "The official colors of UD are Blue and ...?", + type: "multiple_choice_question", + options: ["Black, like my soul", "Blue again, we're tricky.", "#FFD200"], + expected: "#FFD200",*/ + points: 1, + published: false, +}; + +//////////////////////////////////////////// +// Actual tests + +describe("Testing the Question[] functions", () => { + ////////////////////////////////// + // getPublishedQuestions + + test("(3 pts) Testing the getPublishedQuestions function", () => { + expect(getPublishedQuestions(BLANK_QUESTIONS)).toEqual([]); + expect(getPublishedQuestions(SIMPLE_QUESTIONS)).toEqual([ + { + id: 1, + name: "Addition", + body: "What is 2+2?", + type: "short_answer_question", + options: [], + expected: "4", + points: 1, + published: true, + }, + { + id: 5, + name: "Colors", + body: "Which of these is a color?", + type: "multiple_choice_question", + options: ["red", "apple", "firetruck"], + expected: "red", + points: 1, + published: true, + }, + ]); + expect(getPublishedQuestions(TRIVIA_QUESTIONS)).toEqual([]); + expect(getPublishedQuestions(SIMPLE_QUESTIONS_2)).toEqual( + BACKUP_SIMPLE_QUESTIONS_2, + ); + expect(getPublishedQuestions(EMPTY_QUESTIONS)).toEqual([ + { + id: 1, + name: "Empty 1", + body: "This question is not empty, right?", + type: "multiple_choice_question", + options: ["correct", "it is", "not"], + expected: "correct", + points: 5, + published: true, + }, + { + id: 2, + name: "Empty 2", + body: "", + type: "multiple_choice_question", + options: ["this", "one", "is", "not", "empty", "either"], + expected: "one", + points: 5, + published: true, + }, + { + id: 3, + name: "Empty 3", + body: "This questions is not empty either!", + type: "short_answer_question", + options: [], + expected: "", + points: 5, + published: true, + }, + { + id: 4, + name: "Empty 4", + body: "", + type: "short_answer_question", + options: [], + expected: "Even this one is not empty", + points: 5, + published: true, + }, + ]); + }); + + test("(3 pts) Testing the getNonEmptyQuestions functions", () => { + expect(getNonEmptyQuestions(BLANK_QUESTIONS)).toEqual([]); + expect(getNonEmptyQuestions(SIMPLE_QUESTIONS)).toEqual( + BACKUP_SIMPLE_QUESTIONS, + ); + expect(getNonEmptyQuestions(TRIVIA_QUESTIONS)).toEqual( + BACKUP_TRIVIA_QUESTIONS, + ); + expect(getNonEmptyQuestions(SIMPLE_QUESTIONS_2)).toEqual( + BACKUP_SIMPLE_QUESTIONS_2, + ); + expect(getNonEmptyQuestions(EMPTY_QUESTIONS)).toEqual([ + { + id: 1, + name: "Empty 1", + body: "This question is not empty, right?", + type: "multiple_choice_question", + options: ["correct", "it is", "not"], + expected: "correct", + points: 5, + published: true, + }, + { + id: 2, + name: "Empty 2", + body: "", + type: "multiple_choice_question", + options: ["this", "one", "is", "not", "empty", "either"], + expected: "one", + points: 5, + published: true, + }, + { + id: 3, + name: "Empty 3", + body: "This questions is not empty either!", + type: "short_answer_question", + options: [], + expected: "", + points: 5, + published: true, + }, + { + id: 4, + name: "Empty 4", + body: "", + type: "short_answer_question", + options: [], + expected: "Even this one is not empty", + points: 5, + published: true, + }, + ]); + }); + + test("(3 pts) Testing the findQuestion function", () => { + expect(findQuestion(BLANK_QUESTIONS, 1)).toEqual(BLANK_QUESTIONS[0]); + expect(findQuestion(BLANK_QUESTIONS, 47)).toEqual(BLANK_QUESTIONS[1]); + expect(findQuestion(BLANK_QUESTIONS, 2)).toEqual(BLANK_QUESTIONS[2]); + expect(findQuestion(BLANK_QUESTIONS, 3)).toEqual(null); + expect(findQuestion(SIMPLE_QUESTIONS, 1)).toEqual(SIMPLE_QUESTIONS[0]); + expect(findQuestion(SIMPLE_QUESTIONS, 2)).toEqual(SIMPLE_QUESTIONS[1]); + expect(findQuestion(SIMPLE_QUESTIONS, 5)).toEqual(SIMPLE_QUESTIONS[2]); + expect(findQuestion(SIMPLE_QUESTIONS, 9)).toEqual(SIMPLE_QUESTIONS[3]); + expect(findQuestion(SIMPLE_QUESTIONS, 6)).toEqual(null); + expect(findQuestion(SIMPLE_QUESTIONS_2, 478)).toEqual( + SIMPLE_QUESTIONS_2[0], + ); + expect(findQuestion([], 0)).toEqual(null); + }); + + test("(3 pts) Testing the removeQuestion", () => { + expect(removeQuestion(BLANK_QUESTIONS, 1)).toEqual([ + { + id: 47, + name: "My New Question", + body: "", + type: "multiple_choice_question", + options: [], + expected: "", + points: 1, + published: false, + }, + { + id: 2, + name: "Question 2", + body: "", + type: "short_answer_question", + options: [], + expected: "", + points: 1, + published: false, + }, + ]); + expect(removeQuestion(BLANK_QUESTIONS, 47)).toEqual([ + { + id: 1, + name: "Question 1", + body: "", + type: "multiple_choice_question", + options: [], + expected: "", + points: 1, + published: false, + }, + { + id: 2, + name: "Question 2", + body: "", + type: "short_answer_question", + options: [], + expected: "", + points: 1, + published: false, + }, + ]); + expect(removeQuestion(BLANK_QUESTIONS, 2)).toEqual([ + { + id: 1, + name: "Question 1", + body: "", + type: "multiple_choice_question", + options: [], + expected: "", + points: 1, + published: false, + }, + { + id: 47, + name: "My New Question", + body: "", + type: "multiple_choice_question", + options: [], + expected: "", + points: 1, + published: false, + }, + ]); + expect(removeQuestion(SIMPLE_QUESTIONS, 9)).toEqual([ + { + id: 1, + name: "Addition", + body: "What is 2+2?", + type: "short_answer_question", + options: [], + expected: "4", + points: 1, + published: true, + }, + { + id: 2, + name: "Letters", + body: "What is the last letter of the English alphabet?", + type: "short_answer_question", + options: [], + expected: "Z", + points: 1, + published: false, + }, + { + id: 5, + name: "Colors", + body: "Which of these is a color?", + type: "multiple_choice_question", + options: ["red", "apple", "firetruck"], + expected: "red", + points: 1, + published: true, + }, + ]); + expect(removeQuestion(SIMPLE_QUESTIONS, 5)).toEqual([ + { + id: 1, + name: "Addition", + body: "What is 2+2?", + type: "short_answer_question", + options: [], + expected: "4", + points: 1, + published: true, + }, + { + id: 2, + name: "Letters", + body: "What is the last letter of the English alphabet?", + type: "short_answer_question", + options: [], + expected: "Z", + points: 1, + published: false, + }, + { + id: 9, + name: "Shapes", + body: "What shape can you make with one line?", + type: "multiple_choice_question", + options: ["square", "triangle", "circle"], + expected: "circle", + points: 2, + published: false, + }, + ]); + }); + + test("(3 pts) Testing the getNames function", () => { + expect(getNames(BLANK_QUESTIONS)).toEqual([ + "Question 1", + "My New Question", + "Question 2", + ]); + expect(getNames(SIMPLE_QUESTIONS)).toEqual([ + "Addition", + "Letters", + "Colors", + "Shapes", + ]); + expect(getNames(TRIVIA_QUESTIONS)).toEqual([ + "Mascot", + "Motto", + "Goats", + ]); + expect(getNames(SIMPLE_QUESTIONS_2)).toEqual([ + "Students", + "Importance", + "Sentience", + "Danger", + "Listening", + ]); + expect(getNames(EMPTY_QUESTIONS)).toEqual([ + "Empty 1", + "Empty 2", + "Empty 3", + "Empty 4", + "Empty 5 (Actual)", + ]); + }); + + test("(3 pts) Testing the makeAnswers function", () => { + expect(makeAnswers(BLANK_QUESTIONS)).toEqual([ + { questionId: 1, correct: false, text: "", submitted: false }, + { questionId: 47, correct: false, text: "", submitted: false }, + { questionId: 2, correct: false, text: "", submitted: false }, + ]); + expect(makeAnswers(SIMPLE_QUESTIONS)).toEqual([ + { questionId: 1, correct: false, text: "", submitted: false }, + { questionId: 2, correct: false, text: "", submitted: false }, + { questionId: 5, correct: false, text: "", submitted: false }, + { questionId: 9, correct: false, text: "", submitted: false }, + ]); + expect(makeAnswers(TRIVIA_QUESTIONS)).toEqual([ + { questionId: 1, correct: false, text: "", submitted: false }, + { questionId: 2, correct: false, text: "", submitted: false }, + { questionId: 3, correct: false, text: "", submitted: false }, + ]); + expect(makeAnswers(SIMPLE_QUESTIONS_2)).toEqual([ + { questionId: 478, correct: false, text: "", submitted: false }, + { questionId: 1937, correct: false, text: "", submitted: false }, + { questionId: 479, correct: false, text: "", submitted: false }, + { questionId: 777, correct: false, text: "", submitted: false }, + { questionId: 1937, correct: false, text: "", submitted: false }, + ]); + expect(makeAnswers(EMPTY_QUESTIONS)).toEqual([ + { questionId: 1, correct: false, text: "", submitted: false }, + { questionId: 2, correct: false, text: "", submitted: false }, + { questionId: 3, correct: false, text: "", submitted: false }, + { questionId: 4, correct: false, text: "", submitted: false }, + { questionId: 5, correct: false, text: "", submitted: false }, + ]); + }); + + test("(3 pts) Testing the publishAll function", () => { + expect(publishAll(BLANK_QUESTIONS)).toEqual([ + { + id: 1, + name: "Question 1", + body: "", + type: "multiple_choice_question", + options: [], + expected: "", + points: 1, + published: true, + }, + { + id: 47, + name: "My New Question", + body: "", + type: "multiple_choice_question", + options: [], + expected: "", + points: 1, + published: true, + }, + { + id: 2, + name: "Question 2", + body: "", + type: "short_answer_question", + options: [], + expected: "", + points: 1, + published: true, + }, + ]); + expect(publishAll(SIMPLE_QUESTIONS)).toEqual([ + { + id: 1, + name: "Addition", + body: "What is 2+2?", + type: "short_answer_question", + options: [], + expected: "4", + points: 1, + published: true, + }, + { + id: 2, + name: "Letters", + body: "What is the last letter of the English alphabet?", + type: "short_answer_question", + options: [], + expected: "Z", + points: 1, + published: true, + }, + { + id: 5, + name: "Colors", + body: "Which of these is a color?", + type: "multiple_choice_question", + options: ["red", "apple", "firetruck"], + expected: "red", + points: 1, + published: true, + }, + { + id: 9, + name: "Shapes", + body: "What shape can you make with one line?", + type: "multiple_choice_question", + options: ["square", "triangle", "circle"], + expected: "circle", + points: 2, + published: true, + }, + ]); + expect(publishAll(TRIVIA_QUESTIONS)).toEqual([ + { + id: 1, + name: "Mascot", + body: "What is the name of the UD Mascot?", + type: "multiple_choice_question", + options: ["Bluey", "YoUDee", "Charles the Wonder Dog"], + expected: "YoUDee", + points: 7, + published: true, + }, + { + id: 2, + name: "Motto", + body: "What is the University of Delaware's motto?", + type: "multiple_choice_question", + options: [ + "Knowledge is the light of the mind", + "Just U Do it", + "Nothing, what's the motto with you?", + ], + expected: "Knowledge is the light of the mind", + points: 3, + published: true, + }, + { + id: 3, + name: "Goats", + body: "How many goats are there usually on the Green?", + type: "multiple_choice_question", + options: [ + "Zero, why would there be goats on the green?", + "18420", + "Two", + ], + expected: "Two", + points: 10, + published: true, + }, + ]); + expect(publishAll(EMPTY_QUESTIONS)).toEqual([ + { + id: 1, + name: "Empty 1", + body: "This question is not empty, right?", + type: "multiple_choice_question", + options: ["correct", "it is", "not"], + expected: "correct", + points: 5, + published: true, + }, + { + id: 2, + name: "Empty 2", + body: "", + type: "multiple_choice_question", + options: ["this", "one", "is", "not", "empty", "either"], + expected: "one", + points: 5, + published: true, + }, + { + id: 3, + name: "Empty 3", + body: "This questions is not empty either!", + type: "short_answer_question", + options: [], + expected: "", + points: 5, + published: true, + }, + { + id: 4, + name: "Empty 4", + body: "", + type: "short_answer_question", + options: [], + expected: "Even this one is not empty", + points: 5, + published: true, + }, + { + id: 5, + name: "Empty 5 (Actual)", + body: "", + type: "short_answer_question", + options: [], + expected: "", + points: 5, + published: true, + }, + ]); + expect(publishAll(SIMPLE_QUESTIONS_2)).toEqual(SIMPLE_QUESTIONS_2); + }); + + test("(3 pts) Testing the addNewQuestion function", () => { + expect( + addNewQuestion([], 142, "A new question", "short_answer_question"), + ).toEqual([NEW_BLANK_QUESTION]); + expect( + addNewQuestion( + BLANK_QUESTIONS, + 142, + "A new question", + "short_answer_question", + ), + ).toEqual([...BLANK_QUESTIONS, NEW_BLANK_QUESTION]); + expect( + addNewQuestion( + TRIVIA_QUESTIONS, + 449, + "Colors", + "multiple_choice_question", + ), + ).toEqual([...TRIVIA_QUESTIONS, NEW_TRIVIA_QUESTION]); + }); + + test("(3 pts) Testing the renameQuestionById function", () => { + expect(renameQuestionById(BLANK_QUESTIONS, 1, "New Name")).toEqual([ + { + id: 1, + name: "New Name", + body: "", + type: "multiple_choice_question", + options: [], + expected: "", + points: 1, + published: false, + }, + { + id: 47, + name: "My New Question", + body: "", + type: "multiple_choice_question", + options: [], + expected: "", + points: 1, + published: false, + }, + { + id: 2, + name: "Question 2", + body: "", + type: "short_answer_question", + options: [], + expected: "", + points: 1, + published: false, + }, + ]); + expect(renameQuestionById(BLANK_QUESTIONS, 47, "Another Name")).toEqual( + [ + { + id: 1, + name: "Question 1", + body: "", + type: "multiple_choice_question", + options: [], + expected: "", + points: 1, + published: false, + }, + { + id: 47, + name: "Another Name", + body: "", + type: "multiple_choice_question", + options: [], + expected: "", + points: 1, + published: false, + }, + { + id: 2, + name: "Question 2", + body: "", + type: "short_answer_question", + options: [], + expected: "", + points: 1, + published: false, + }, + ], + ); + expect(renameQuestionById(SIMPLE_QUESTIONS, 5, "Colours")).toEqual([ + { + id: 1, + name: "Addition", + body: "What is 2+2?", + type: "short_answer_question", + options: [], + expected: "4", + points: 1, + published: true, + }, + { + id: 2, + name: "Letters", + body: "What is the last letter of the English alphabet?", + type: "short_answer_question", + options: [], + expected: "Z", + points: 1, + published: false, + }, + { + id: 5, + name: "Colours", + body: "Which of these is a color?", + type: "multiple_choice_question", + options: ["red", "apple", "firetruck"], + expected: "red", + points: 1, + published: true, + }, + { + id: 9, + name: "Shapes", + body: "What shape can you make with one line?", + type: "multiple_choice_question", + options: ["square", "triangle", "circle"], + expected: "circle", + points: 2, + published: false, + }, + ]); + }); + + test("(3 pts) Testing the editOption function", () => { + expect(editOption(BLANK_QUESTIONS, 1, -1, "NEW OPTION")).toEqual([ + { + id: 1, + name: "Question 1", + body: "", + type: "multiple_choice_question", + options: ["NEW OPTION"], + expected: "", + points: 1, + published: false, + }, + { + id: 47, + name: "My New Question", + body: "", + type: "multiple_choice_question", + options: [], + expected: "", + points: 1, + published: false, + }, + { + id: 2, + name: "Question 2", + body: "", + type: "short_answer_question", + options: [], + expected: "", + points: 1, + published: false, + }, + ]); + expect(editOption(BLANK_QUESTIONS, 47, -1, "Another option")).toEqual([ + { + id: 1, + name: "Question 1", + body: "", + type: "multiple_choice_question", + options: [], + expected: "", + points: 1, + published: false, + }, + { + id: 47, + name: "My New Question", + body: "", + type: "multiple_choice_question", + options: ["Another option"], + expected: "", + points: 1, + published: false, + }, + { + id: 2, + name: "Question 2", + body: "", + type: "short_answer_question", + options: [], + expected: "", + points: 1, + published: false, + }, + ]); + expect(editOption(SIMPLE_QUESTIONS, 5, -1, "newspaper")).toEqual([ + { + id: 1, + name: "Addition", + body: "What is 2+2?", + type: "short_answer_question", + options: [], + expected: "4", + points: 1, + published: true, + }, + { + id: 2, + name: "Letters", + body: "What is the last letter of the English alphabet?", + type: "short_answer_question", + options: [], + expected: "Z", + points: 1, + published: false, + }, + { + id: 5, + name: "Colors", + body: "Which of these is a color?", + type: "multiple_choice_question", + options: ["red", "apple", "firetruck", "newspaper"], + expected: "red", + points: 1, + published: true, + }, + { + id: 9, + name: "Shapes", + body: "What shape can you make with one line?", + type: "multiple_choice_question", + options: ["square", "triangle", "circle"], + expected: "circle", + points: 2, + published: false, + }, + ]); + expect(editOption(SIMPLE_QUESTIONS, 5, 0, "newspaper")).toEqual([ + { + id: 1, + name: "Addition", + body: "What is 2+2?", + type: "short_answer_question", + options: [], + expected: "4", + points: 1, + published: true, + }, + { + id: 2, + name: "Letters", + body: "What is the last letter of the English alphabet?", + type: "short_answer_question", + options: [], + expected: "Z", + points: 1, + published: false, + }, + { + id: 5, + name: "Colors", + body: "Which of these is a color?", + type: "multiple_choice_question", + options: ["newspaper", "apple", "firetruck"], + expected: "red", + points: 1, + published: true, + }, + { + id: 9, + name: "Shapes", + body: "What shape can you make with one line?", + type: "multiple_choice_question", + options: ["square", "triangle", "circle"], + expected: "circle", + points: 2, + published: false, + }, + ]); + + expect(editOption(SIMPLE_QUESTIONS, 5, 2, "newspaper")).toEqual([ + { + id: 1, + name: "Addition", + body: "What is 2+2?", + type: "short_answer_question", + options: [], + expected: "4", + points: 1, + published: true, + }, + { + id: 2, + name: "Letters", + body: "What is the last letter of the English alphabet?", + type: "short_answer_question", + options: [], + expected: "Z", + points: 1, + published: false, + }, + { + id: 5, + name: "Colors", + body: "Which of these is a color?", + type: "multiple_choice_question", + options: ["red", "apple", "newspaper"], + expected: "red", + points: 1, + published: true, + }, + { + id: 9, + name: "Shapes", + body: "What shape can you make with one line?", + type: "multiple_choice_question", + options: ["square", "triangle", "circle"], + expected: "circle", + points: 2, + published: false, + }, + ]); + }); + + afterEach(() => { + expect(BLANK_QUESTIONS).toEqual(BACKUP_BLANK_QUESTIONS); + expect(SIMPLE_QUESTIONS).toEqual(BACKUP_SIMPLE_QUESTIONS); + expect(TRIVIA_QUESTIONS).toEqual(BACKUP_TRIVIA_QUESTIONS); + expect(SIMPLE_QUESTIONS_2).toEqual(BACKUP_SIMPLE_QUESTIONS_2); + expect(EMPTY_QUESTIONS).toEqual(BACKUP_EMPTY_QUESTIONS); + }); +}); diff --git a/src/nested.ts b/src/nested.ts new file mode 100644 index 0000000000..1946457f33 --- /dev/null +++ b/src/nested.ts @@ -0,0 +1,134 @@ +import { Answer } from "./interfaces/answer"; +import { Question, QuestionType } from "./interfaces/question"; + +/** + * Consumes an array of questions and returns a new array with only the questions + * that are `published`. + */ +export function getPublishedQuestions(questions: Question[]): Question[] { + return questions.filter((question) => question.published); +} + +/** + * Consumes an array of questions and returns a new array of only the questions that are + * considered "non-empty". An empty question has an empty string for its `body` and + * `expected`, and an empty array for its `options`. + */ +export function getNonEmptyQuestions(questions: Question[]): Question[] { + return questions.filter((question:Question):boolean=>!( + (question.expected.length ===0 ) && + (question.body.length === 0 ) && + (question.options.length ===0 ))) +} + +/*** + * Consumes an array of questions and returns the question with the given `id`. If the + * question is not found, return `null` instead. + */ +export function findQuestion( + questions: Question[], + id: number, +): Question | null { + return questions.find((question) => question.id === id) || null; +} + +/** + * Consumes an array of questions and returns a new array that does not contain the question + * with the given `id`. + * Hint: use filter + */ +export function removeQuestion(questions: Question[], id: number): Question[] { + return questions.filter((question) => question.id !== id); +} + +/*** + * Consumes an array of questions and returns a new array containing just the names of the + * questions, as an array. + * Do not modify the input array. + */ +export function getNames(questions: Question[]): string[] { + return questions.map((question) => question.name); +} + +/** + * Consumes an array of Questions and produces a corresponding array of + * Answers. Each Question gets its own Answer, copying over the `id` as the `questionId`, + * making the `text` an empty string, and using false for both `submitted` and `correct`. + */ +export function makeAnswers(questions: Question[]): Answer[] { + return questions.map((question) => ({ + questionId: question.id, + text: "", + submitted: false, + correct: false, + })); +} + +/*** + * Consumes an array of Questions and produces a new array of questions, where + * each question is now published, regardless of its previous published status. + * Hint: as usual, do not modify the input questions array + */ +export function publishAll(questions: Question[]): Question[] { + return questions.map((question) => ({ + ...question, // Spread the existing question properties + published: true, // Set the published property to true + })); +} + +/*** + * Consumes an array of Questions and produces a new array of the same Questions, + * except that a blank question has been added onto the end. Reuse the `makeBlankQuestion` + * you defined in the `objects.ts` file. + * Hint: as usual, do not modify the input questions array + */ +import { makeBlankQuestion } from "./objects"; +export function addNewQuestion( + questions: Question[], + id: number, + name: string, + type: QuestionType, +): Question[] { + const blankQuestion = makeBlankQuestion(id, name, type); + + return [...questions, blankQuestion]; +} + +/*** + * Consumes an array of Questions and produces a new array of Questions, where all + * the Questions are the same EXCEPT for the one with the given `targetId`. That + * Question should be the same EXCEPT that its name should now be `newName`. + * Hint: as usual, do not modify the input questions array, + * to make a new copy of a question with some changes, use the ... operator + */ +export function renameQuestionById( + questions: Question[], + targetId: number, + newName: string, +): Question[] { + return questions.map((question) => + question.id === targetId ? { ...question, name: newName } : question, + ); +} + +export function editOption( + questions: Question[], + targetId: number, + targetOptionIndex: number, + newOption: string, +): Question[] { + return []; +} +/** + * Consumes an array of Questions and produces a new array of Questions, where all + * the Questions are the same EXCEPT for the one with the given `targetId`. That + * Question should be the same EXCEPT that its `option` array should have a new element. + * If the `targetOptionIndex` is -1, the `newOption` should be added to the end of the list. + * Otherwise, it should *replace* the existing element at the `targetOptionIndex`. + * + * Remember, if a function starts getting too complicated, think about how a helper function + * can make it simpler! Break down complicated tasks into little pieces. + * + * + * Hint: you need to use the ... operator for both the question and the options array + */ diff --git a/src/objects.test.ts b/src/objects.test.ts new file mode 100644 index 0000000000..4d3117405d --- /dev/null +++ b/src/objects.test.ts @@ -0,0 +1,299 @@ +import { Question } from "./interfaces/question"; +import { + makeBlankQuestion, + isCorrect, + isValid, + toShortForm, + toMarkdown, + duplicateQuestion, + renameQuestion, + publishQuestion, + addOption, + mergeQuestion, +} from "./objects"; +import testQuestionData from "./data/questions.json"; +import backupQuestionData from "./data/questions.json"; + +//////////////////////////////////////////// +// Setting up the test data + +const { BLANK_QUESTIONS, SIMPLE_QUESTIONS }: Record = + // Typecast the test data that we imported to be a record matching + // strings to the question list + testQuestionData as Record; + +// We have backup versions of the data to make sure all changes are immutable +const { + BLANK_QUESTIONS: BACKUP_BLANK_QUESTIONS, + SIMPLE_QUESTIONS: BACKUP_SIMPLE_QUESTIONS, +}: Record = backupQuestionData as Record< + string, + Question[] +>; + +// Unpack the list of simple questions into convenient constants +const [ADDITION_QUESTION, LETTER_QUESTION, COLOR_QUESTION, SHAPE_QUESTION] = + SIMPLE_QUESTIONS; +const [ + BACKUP_ADDITION_QUESTION, + BACKUP_LETTER_QUESTION, + BACKUP_COLOR_QUESTION, + BACKUP_SHAPE_QUESTION, +] = BACKUP_SIMPLE_QUESTIONS; + +//////////////////////////////////////////// +// Actual tests + +describe("Testing the object functions", () => { + ////////////////////////////////// + // makeBlankQuestion + + test("(3 pts) Testing the makeBlankQuestion function", () => { + expect( + makeBlankQuestion(1, "Question 1", "multiple_choice_question"), + ).toEqual(BLANK_QUESTIONS[0]); + expect( + makeBlankQuestion( + 47, + "My New Question", + "multiple_choice_question", + ), + ).toEqual(BLANK_QUESTIONS[1]); + expect( + makeBlankQuestion(2, "Question 2", "short_answer_question"), + ).toEqual(BLANK_QUESTIONS[2]); + }); + + /////////////////////////////////// + // isCorrect + test("(3 pts) Testing the isCorrect function", () => { + expect(isCorrect(ADDITION_QUESTION, "4")).toEqual(true); + expect(isCorrect(ADDITION_QUESTION, "2")).toEqual(false); + expect(isCorrect(ADDITION_QUESTION, " 4\n")).toEqual(true); + expect(isCorrect(LETTER_QUESTION, "Z")).toEqual(true); + expect(isCorrect(LETTER_QUESTION, "z")).toEqual(true); + expect(isCorrect(LETTER_QUESTION, "4")).toEqual(false); + expect(isCorrect(LETTER_QUESTION, "0")).toEqual(false); + expect(isCorrect(LETTER_QUESTION, "zed")).toEqual(false); + expect(isCorrect(COLOR_QUESTION, "red")).toEqual(true); + expect(isCorrect(COLOR_QUESTION, "apple")).toEqual(false); + expect(isCorrect(COLOR_QUESTION, "firetruck")).toEqual(false); + expect(isCorrect(SHAPE_QUESTION, "square")).toEqual(false); + expect(isCorrect(SHAPE_QUESTION, "triangle")).toEqual(false); + expect(isCorrect(SHAPE_QUESTION, "circle")).toEqual(true); + }); + + /////////////////////////////////// + // isValid + test("(3 pts) Testing the isValid function", () => { + expect(isValid(ADDITION_QUESTION, "4")).toEqual(true); + expect(isValid(ADDITION_QUESTION, "2")).toEqual(true); + expect(isValid(ADDITION_QUESTION, " 4\n")).toEqual(true); + expect(isValid(LETTER_QUESTION, "Z")).toEqual(true); + expect(isValid(LETTER_QUESTION, "z")).toEqual(true); + expect(isValid(LETTER_QUESTION, "4")).toEqual(true); + expect(isValid(LETTER_QUESTION, "0")).toEqual(true); + expect(isValid(LETTER_QUESTION, "zed")).toEqual(true); + expect(isValid(COLOR_QUESTION, "red")).toEqual(true); + expect(isValid(COLOR_QUESTION, "apple")).toEqual(true); + expect(isValid(COLOR_QUESTION, "firetruck")).toEqual(true); + expect(isValid(COLOR_QUESTION, "RED")).toEqual(false); + expect(isValid(COLOR_QUESTION, "orange")).toEqual(false); + expect(isValid(SHAPE_QUESTION, "square")).toEqual(true); + expect(isValid(SHAPE_QUESTION, "triangle")).toEqual(true); + expect(isValid(SHAPE_QUESTION, "circle")).toEqual(true); + expect(isValid(SHAPE_QUESTION, "circle ")).toEqual(false); + expect(isValid(SHAPE_QUESTION, "rhombus")).toEqual(false); + }); + + /////////////////////////////////// + // toShortForm + test("(3 pts) Testing the toShortForm function", () => { + expect(toShortForm(ADDITION_QUESTION)).toEqual("1: Addition"); + expect(toShortForm(LETTER_QUESTION)).toEqual("2: Letters"); + expect(toShortForm(COLOR_QUESTION)).toEqual("5: Colors"); + expect(toShortForm(SHAPE_QUESTION)).toEqual("9: Shapes"); + expect(toShortForm(BLANK_QUESTIONS[1])).toEqual("47: My New Que"); + }); + + /////////////////////////////////// + // toMarkdown + test("(3 pts) Testing the toMarkdown function", () => { + expect(toMarkdown(ADDITION_QUESTION)).toEqual(`# Addition +What is 2+2?`); + expect(toMarkdown(LETTER_QUESTION)).toEqual(`# Letters +What is the last letter of the English alphabet?`); + expect(toMarkdown(COLOR_QUESTION)).toEqual(`# Colors +Which of these is a color? +- red +- apple +- firetruck`); + expect(toMarkdown(SHAPE_QUESTION)).toEqual(`# Shapes +What shape can you make with one line? +- square +- triangle +- circle`); + }); + + afterEach(() => { + expect(ADDITION_QUESTION).toEqual(BACKUP_ADDITION_QUESTION); + expect(LETTER_QUESTION).toEqual(BACKUP_LETTER_QUESTION); + expect(SHAPE_QUESTION).toEqual(BACKUP_SHAPE_QUESTION); + expect(COLOR_QUESTION).toEqual(BACKUP_COLOR_QUESTION); + expect(BLANK_QUESTIONS).toEqual(BACKUP_BLANK_QUESTIONS); + }); + + /////////////////////////////////// + // renameQuestion + test("(3 pts) Testing the renameQuestion function", () => { + expect( + renameQuestion(ADDITION_QUESTION, "My Addition Question"), + ).toEqual({ + id: 1, + name: "My Addition Question", + body: "What is 2+2?", + type: "short_answer_question", + options: [], + expected: "4", + points: 1, + published: true, + }); + expect( + renameQuestion(SHAPE_QUESTION, "I COMPLETELY CHANGED THIS NAME"), + ).toEqual({ + id: 9, + name: "I COMPLETELY CHANGED THIS NAME", + body: "What shape can you make with one line?", + type: "multiple_choice_question", + options: ["square", "triangle", "circle"], + expected: "circle", + points: 2, + published: false, + }); + }); + + /////////////////////////////////// + // publishQuestion + test("(3 pts) Testing the publishQuestion function", () => { + expect(publishQuestion(ADDITION_QUESTION)).toEqual({ + id: 1, + name: "Addition", + body: "What is 2+2?", + type: "short_answer_question", + options: [], + expected: "4", + points: 1, + published: false, + }); + expect(publishQuestion(LETTER_QUESTION)).toEqual({ + id: 2, + name: "Letters", + body: "What is the last letter of the English alphabet?", + type: "short_answer_question", + options: [], + expected: "Z", + points: 1, + published: true, + }); + expect(publishQuestion(publishQuestion(ADDITION_QUESTION))).toEqual({ + id: 1, + name: "Addition", + body: "What is 2+2?", + type: "short_answer_question", + options: [], + expected: "4", + points: 1, + published: true, + }); + }); + + /////////////////////////////////// + // duplicateQuestion + test("(3 pts) Testing the duplicateQuestion function", () => { + expect(duplicateQuestion(9, ADDITION_QUESTION)).toEqual({ + id: 9, + name: "Copy of Addition", + body: "What is 2+2?", + type: "short_answer_question", + options: [], + expected: "4", + points: 1, + published: false, + }); + expect(duplicateQuestion(55, LETTER_QUESTION)).toEqual({ + id: 55, + name: "Copy of Letters", + body: "What is the last letter of the English alphabet?", + type: "short_answer_question", + options: [], + expected: "Z", + points: 1, + published: false, + }); + }); + + /////////////////////////////////// + // addOption + test("(3 pts) Testing the addOption function", () => { + expect(addOption(SHAPE_QUESTION, "heptagon")).toEqual({ + id: 9, + name: "Shapes", + body: "What shape can you make with one line?", + type: "multiple_choice_question", + options: ["square", "triangle", "circle", "heptagon"], + expected: "circle", + points: 2, + published: false, + }); + expect(addOption(COLOR_QUESTION, "squiggles")).toEqual({ + id: 5, + name: "Colors", + body: "Which of these is a color?", + type: "multiple_choice_question", + options: ["red", "apple", "firetruck", "squiggles"], + expected: "red", + points: 1, + published: true, + }); + }); + + /////////////////////////////////// + // mergeQuestion + test("(3 pts) Testing the mergeQuestion function", () => { + expect( + mergeQuestion( + 192, + "More Points Addition", + ADDITION_QUESTION, + SHAPE_QUESTION, + ), + ).toEqual({ + id: 192, + name: "More Points Addition", + body: "What is 2+2?", + type: "short_answer_question", + options: [], + expected: "4", + points: 2, + published: false, + }); + + expect( + mergeQuestion( + 99, + "Less Points Shape", + SHAPE_QUESTION, + ADDITION_QUESTION, + ), + ).toEqual({ + id: 99, + name: "Less Points Shape", + body: "What shape can you make with one line?", + type: "multiple_choice_question", + options: ["square", "triangle", "circle"], + expected: "circle", + points: 1, + published: false, + }); + }); +}); diff --git a/src/objects.ts b/src/objects.ts new file mode 100644 index 0000000000..f4eb7c87e0 --- /dev/null +++ b/src/objects.ts @@ -0,0 +1,23 @@ +import { Question, QuestionType } from "./interfaces/question"; + +/** + * Create a new blank question with the given `id`, `name`, and `type. The `body` and + * `expected` should be empty strings, the `options` should be an empty list, the `points` + * should default to 1, and `published` should default to false. + */ +export function makeBlankQuestion( + id: number, + name: string, + type: QuestionType, +): Question { + return { + id, + name, + body: "", + expected: "", + options: [], + points: 1, + published: false, + type, + }; +}