diff --git a/git b/git new file mode 100644 index 0000000000..e69de29bb2 diff --git a/package-lock.json b/package-lock.json index 8c3779f487..8c4e862b08 100644 --- a/package-lock.json +++ b/package-lock.json @@ -100,6 +100,7 @@ "resolved": "https://registry.npmjs.org/@babel/core/-/core-7.25.2.tgz", "integrity": "sha512-BBt3opiCOxUr9euZ5/ro/Xv8/V7yJ5bjYMqG/C1YAo8MIKAnumZalCN+msbci3Pigy4lIQfPUpfMM27HMGaYEA==", "license": "MIT", + "peer": true, "dependencies": { "@ampproject/remapping": "^2.2.0", "@babel/code-frame": "^7.24.7", @@ -785,6 +786,7 @@ "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-flow/-/plugin-syntax-flow-7.24.7.tgz", "integrity": "sha512-9G8GYT/dxn/D1IIKOUBmGX0mnmj46mGH9NnZyJLwtCpgh5f7D2VbuKodb+2s9m1Yavh1s7ASQN8lf0eqrb1LTw==", "license": "MIT", + "peer": true, "dependencies": { "@babel/helper-plugin-utils": "^7.24.7" }, @@ -1663,6 +1665,7 @@ "resolved": "https://registry.npmjs.org/@babel/plugin-transform-react-jsx/-/plugin-transform-react-jsx-7.25.2.tgz", "integrity": "sha512-KQsqEAVBpU82NM/B/N9j9WOdphom1SZH3R+2V7INrQUH+V9EBFwZsEJl8eBIVeQE62FxJCc70jzEZwqU7RcVqA==", "license": "MIT", + "peer": true, "dependencies": { "@babel/helper-annotate-as-pure": "^7.24.7", "@babel/helper-module-imports": "^7.24.7", @@ -2153,7 +2156,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 +2169,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 +3276,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 +3439,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 +3457,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 +3473,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 +3490,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 +3503,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 +3520,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 +3912,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,8 +4225,8 @@ "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", + "peer": true, "dependencies": { "@babel/code-frame": "^7.10.4", "@babel/runtime": "^7.12.5", @@ -4242,7 +4245,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 +4260,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 +4276,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 +4288,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 +4303,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 +4467,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": { @@ -4728,7 +4724,8 @@ "version": "16.18.105", "resolved": "https://registry.npmjs.org/@types/node/-/node-16.18.105.tgz", "integrity": "sha512-w2d0Z9yMk07uH3+Cx0N8lqFyi3yjXZxlbYappPj+AsOlT02OyxyiuNoNHdGt6EuiSm8Wtgp2YV7vWg+GMFrvFA==", - "license": "MIT" + "license": "MIT", + "peer": true }, "node_modules/@types/node-forge": { "version": "1.3.11", @@ -4791,6 +4788,7 @@ "integrity": "sha512-EhwApuTmMBmXuFOikhQLIBUn6uFg81SwLMOAUgodJF14SOBOCMdU04gDoYi0WOJJHD144TL32z4yDqCW3dnkQg==", "dev": true, "license": "MIT", + "peer": true, "dependencies": { "@types/react": "*" } @@ -4911,7 +4909,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": "*" @@ -5116,6 +5114,7 @@ "integrity": "sha512-j3Di+o0lHgPrb7FxL3fdEy6LJ/j2NE8u+AP/5cQ9SKb+JLH6V6UHDqJ+e0hXBkHP1wn1YDFjYCS9LBQsZDlDEg==", "dev": true, "license": "BSD-2-Clause", + "peer": true, "dependencies": { "@typescript-eslint/scope-manager": "8.2.0", "@typescript-eslint/types": "8.2.0", @@ -5468,6 +5467,7 @@ "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.12.1.tgz", "integrity": "sha512-tcpGyI9zbizT9JbV6oYE477V6mTlXvvi0T0G3SNIYE2apm/G5huBa1+K89VGeovbg+jycCrfhl3ADxErOuO6Jg==", "license": "MIT", + "peer": true, "bin": { "acorn": "bin/acorn" }, @@ -5508,7 +5508,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" @@ -5569,6 +5569,7 @@ "resolved": "https://registry.npmjs.org/ajv/-/ajv-6.12.6.tgz", "integrity": "sha512-j3fVLgvTo527anyYyJOGTYJbG+vnnQYvE0m5mmkc1TK+nxAppkCLMIL0aZ4dblVCNoGShhm+kzE4ZUykBoMg4g==", "license": "MIT", + "peer": true, "dependencies": { "fast-deep-equal": "^3.1.1", "fast-json-stable-stringify": "^2.0.0", @@ -6556,6 +6557,7 @@ } ], "license": "MIT", + "peer": true, "dependencies": { "caniuse-lite": "^1.0.30001646", "electron-to-chromium": "^1.5.4", @@ -7210,7 +7212,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 +7949,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" @@ -8499,6 +8501,7 @@ "resolved": "https://registry.npmjs.org/eslint/-/eslint-8.57.0.tgz", "integrity": "sha512-dZ6+mexnaTIbSBZWgou51U6OmzIhYM2VcNdtiTtI7qPNZm35Akpr0f6vtw3w1Kmn5PYo+tZVfh13WrhpS6oLqQ==", "license": "MIT", + "peer": true, "dependencies": { "@eslint-community/eslint-utils": "^4.2.0", "@eslint-community/regexpp": "^4.6.1", @@ -8555,6 +8558,7 @@ "integrity": "sha512-NSWl5BFQWEPi1j4TjVNItzYV7dZXZ+wP6I6ZhrBGpChQhZRUaElihE9uRRkcbRnNb76UMKDF3r+WTmNcGPKsqw==", "dev": true, "license": "MIT", + "peer": true, "bin": { "eslint-config-prettier": "bin/cli.js" }, @@ -8595,6 +8599,7 @@ "resolved": "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-5.62.0.tgz", "integrity": "sha512-TiZzBSJja/LbhNPvk6yc0JrX9XqhQ0hdh6M2svYfsHGejaKFIAGd9MQ+ERIMzLGlN/kZoYIgdxFV0PuljTKXag==", "license": "MIT", + "peer": true, "dependencies": { "@eslint-community/regexpp": "^4.4.0", "@typescript-eslint/scope-manager": "5.62.0", @@ -8629,6 +8634,7 @@ "resolved": "https://registry.npmjs.org/@typescript-eslint/parser/-/parser-5.62.0.tgz", "integrity": "sha512-VlJEV0fOQ7BExOsHYAGrgbEiZoi8D+Bl2+f6V2RrXerRSylnp+ZBHmPvaIa8cz0Ajx7WO7Z5RqfgYg7ED1nRhA==", "license": "BSD-2-Clause", + "peer": true, "dependencies": { "@typescript-eslint/scope-manager": "5.62.0", "@typescript-eslint/types": "5.62.0", @@ -12248,6 +12254,7 @@ "integrity": "sha512-NIy3oAFp9shda19hy4HK0HRTWKtPJmGdnvywu01nOqNC2vZg+Z+fvJDxpMQA88eb2I9EcafcdjYgsDthnYTvGw==", "dev": true, "license": "MIT", + "peer": true, "dependencies": { "@jest/core": "^29.7.0", "@jest/types": "^29.6.3", @@ -13021,7 +13028,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 +13038,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 +13956,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 +13966,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 +14001,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 +14017,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 +14034,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 +14047,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 +14064,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 +14082,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 +14525,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 +14543,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 +14559,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 +14576,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 +14589,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 +14606,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 +14619,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 +14637,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 +14653,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 +14666,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 +14683,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 +14696,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 +14713,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 +14728,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 +14741,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 +14857,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 +14873,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 +14883,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 +15375,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 +15408,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": { @@ -16444,6 +16450,7 @@ } ], "license": "MIT", + "peer": true, "dependencies": { "nanoid": "^3.3.7", "picocolors": "^1.0.1", @@ -17650,6 +17657,7 @@ "resolved": "https://registry.npmjs.org/postcss-selector-parser/-/postcss-selector-parser-6.1.2.tgz", "integrity": "sha512-Q8qQfPiZ+THO/3ZrOrO0cJJKfpYCagtMUkXbnEfmgUjwXg6z/WBeOyS9APBBPCTSiDV+s4SwQGu8yFsiMRIudg==", "license": "MIT", + "peer": true, "dependencies": { "cssesc": "^3.0.0", "util-deprecate": "^1.0.2" @@ -17759,6 +17767,7 @@ "integrity": "sha512-i2tDNA0O5IrMO757lfrdQZCc2jPNDVntV0m/+4whiDfWaTKfMNgR7Qz0NAeGz/nRqF4m5/6CLzbP4/liHt12Ew==", "dev": true, "license": "MIT", + "peer": true, "bin": { "prettier": "bin/prettier.cjs" }, @@ -18290,6 +18299,7 @@ "resolved": "https://registry.npmjs.org/react/-/react-18.3.1.tgz", "integrity": "sha512-wS+hAgJShR0KhEvPJArfuPVN1+Hz1t0Y6n5jLrGQbkb4urgPE/0Rve+1kMB1v/oWgHgm4WIcV+i7F2pTVj+2iQ==", "license": "MIT", + "peer": true, "dependencies": { "loose-envify": "^1.1.0" }, @@ -18481,6 +18491,7 @@ "resolved": "https://registry.npmjs.org/react-dom/-/react-dom-18.3.1.tgz", "integrity": "sha512-5m4nQKp+rZRb09LNH59GM4BxTh9251/ylbKIbpe7TpGxfJ+9kv6BLkLBXIjjspbgbnIBNqlI23tRnTWT0snUIw==", "license": "MIT", + "peer": true, "dependencies": { "loose-envify": "^1.1.0", "scheduler": "^0.23.2" @@ -18512,6 +18523,7 @@ "resolved": "https://registry.npmjs.org/react-refresh/-/react-refresh-0.11.0.tgz", "integrity": "sha512-F27qZr8uUqwhWZboondsPx8tnC3Ct3SxZA3V5WyEvujRyyNv0VYPhoBg1gZ8/MV5tubQp76Trw8lTv9hzRBa+A==", "license": "MIT", + "peer": true, "engines": { "node": ">=0.10.0" } @@ -19225,6 +19237,7 @@ "resolved": "https://registry.npmjs.org/jest/-/jest-27.5.1.tgz", "integrity": "sha512-Yn0mADZB89zTtjkPJEXwrac3LHudkQMR+Paqa8uxJHCBr9agxztUifWCyiYrjhMPBoUVBjyny0I7XH6ozDr7QQ==", "license": "MIT", + "peer": true, "dependencies": { "@jest/core": "^27.5.1", "import-local": "^3.0.2", @@ -20668,7 +20681,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" @@ -20714,6 +20727,7 @@ "resolved": "https://registry.npmjs.org/rollup/-/rollup-2.79.1.tgz", "integrity": "sha512-uKxbd0IhMZOhjAiD5oAFp7BqvkA4Dv47qpOCtaNvng4HBwdbWtdOh8f5nZNuk2rp51PMGk3bzfWu5oayNEuYnw==", "license": "MIT", + "peer": true, "bin": { "rollup": "dist/bin/rollup" }, @@ -20964,6 +20978,7 @@ "resolved": "https://registry.npmjs.org/ajv/-/ajv-8.17.1.tgz", "integrity": "sha512-B/gBuNg5SiMTrPkC+A2+cW0RszwxYmn6VYxB/inlBStS5nx6xHIt/ehKRhIMhqusl7a8LjQoZnjCs5vhwxOQ1g==", "license": "MIT", + "peer": true, "dependencies": { "fast-deep-equal": "^3.1.3", "fast-uri": "^3.0.1", @@ -22588,8 +22603,9 @@ "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", + "peer": true, "dependencies": { "@cspotcode/source-map-support": "^0.8.0", "@tsconfig/node10": "^1.0.7", @@ -22632,7 +22648,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": { @@ -22721,6 +22737,7 @@ "resolved": "https://registry.npmjs.org/type-fest/-/type-fest-0.21.3.tgz", "integrity": "sha512-t0rzBq87m3fVcduHDUFhKmyyX+9eo6WQjZvf51Ea/M0Q7+T374Jp1aUiyUl0GKxp8M/OETVHSDvmkyPgvX+X2w==", "license": "(MIT OR CC0-1.0)", + "peer": true, "engines": { "node": ">=10" }, @@ -22828,6 +22845,7 @@ "resolved": "https://registry.npmjs.org/typescript/-/typescript-4.9.5.tgz", "integrity": "sha512-1FXk9E2Hm+QzZQ7z+McJiHL4NW1F2EzMu9Nq9i3zAaGqibafqYwCVU6WyWAuyQRRzOlxou8xZSyXLEN8oKj24g==", "license": "Apache-2.0", + "peer": true, "bin": { "tsc": "bin/tsc", "tsserver": "bin/tsserver" @@ -23056,7 +23074,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": { @@ -23205,6 +23223,7 @@ "resolved": "https://registry.npmjs.org/webpack/-/webpack-5.93.0.tgz", "integrity": "sha512-Y0m5oEY1LRuwly578VqluorkXbvXKh7U3rLoQCEO04M97ScRr44afGVkI0FQFsXzysk5OgFAxjZAb9rsGQVihA==", "license": "MIT", + "peer": true, "dependencies": { "@types/eslint-scope": "^3.7.3", "@types/estree": "^1.0.5", @@ -23275,6 +23294,7 @@ "resolved": "https://registry.npmjs.org/webpack-dev-server/-/webpack-dev-server-4.15.2.tgz", "integrity": "sha512-0XavAZbNJ5sDrCbkpWL8mia0o5WPOd2YGtxrEiZkBK9FjLppIUK2TgxK6qGD2P3hUXTJNNPVibrerKcx5WkR1g==", "license": "MIT", + "peer": true, "dependencies": { "@types/bonjour": "^3.5.9", "@types/connect-history-api-fallback": "^1.3.5", @@ -23665,6 +23685,7 @@ "resolved": "https://registry.npmjs.org/ajv/-/ajv-8.17.1.tgz", "integrity": "sha512-B/gBuNg5SiMTrPkC+A2+cW0RszwxYmn6VYxB/inlBStS5nx6xHIt/ehKRhIMhqusl7a8LjQoZnjCs5vhwxOQ1g==", "license": "MIT", + "peer": true, "dependencies": { "fast-deep-equal": "^3.1.3", "fast-uri": "^3.0.1", @@ -24118,7 +24139,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-arrays.md b/public/tasks/task-arrays.md new file mode 100644 index 0000000000..c2fbf80f8d --- /dev/null +++ b/public/tasks/task-arrays.md @@ -0,0 +1,5 @@ +# Task - Arrays + +Version: 0.0.1 + +Implement functions that work with arrays immutably. 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-first-branch.md b/public/tasks/task-first-branch.md new file mode 100644 index 0000000000..94333338a0 --- /dev/null +++ b/public/tasks/task-first-branch.md @@ -0,0 +1,5 @@ +# Task - First Branch + +Version: 0.0.1 + +Pass a short test to have certain text on the page. diff --git a/public/tasks/task-functions.md b/public/tasks/task-functions.md new file mode 100644 index 0000000000..36e7926bb7 --- /dev/null +++ b/public/tasks/task-functions.md @@ -0,0 +1,5 @@ +# Task - Functions + +Version: 0.0.1 + +Implement a bunch of functions that work on primitives. diff --git a/public/tasks/task-html-css.md b/public/tasks/task-html-css.md new file mode 100644 index 0000000000..ebc0efcba5 --- /dev/null +++ b/public/tasks/task-html-css.md @@ -0,0 +1,5 @@ +# Task - HTML/CSS + +Version: 0.0.1 + +Add in some HTML and CSS, including a fancy looking button. 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 b77558eaac..4cc85c7ea4 100644 --- a/src/App.tsx +++ b/src/App.tsx @@ -1,5 +1,20 @@ import React from "react"; import "./App.css"; +import { ChangeType } from "./components/ChangeType"; +import { RevealAnswer } from "./components/RevealAnswer"; +import { StartAttempt } from "./components/StartAttempt"; +import { TwoDice } from "./components/TwoDice"; +import { CycleHoliday } from "./components/CycleHoliday"; +import { Counter } from "./components/Counter"; +import { DoubleHalf } from "./bad-components/DoubleHalf"; +import { ColoredBox } from "./bad-components/ColoredBox"; +import { ShoveBox } from "./bad-components/ShoveBox"; +import { ChooseTeam } from "./bad-components/ChooseTeam"; +import { CheckAnswer } from "./form-components/CheckAnswer"; +import { GiveAttempts } from "./form-components/GiveAttempts"; +import { EditMode } from "./form-components/EditMode"; +import { MultipleChoiceQuestion } from "./form-components/MultipleChoiceQuestion"; +import { ChangeColor } from "./form-components/ChangeColor"; function App(): React.JSX.Element { return ( @@ -7,10 +22,137 @@ function App(): React.JSX.Element {
UD CISC275 with React Hooks and TypeScript
-

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

+
+ +
+ +
+ +
+ +
+ +
+ +
+ +
+ +
+ +
+ +
+ +
+ +
+ +
+ +
+ + + +
+ +
+
+ UD CISC275 with React Hooks and TypeScript By + Valerie Owens + Hello World +
+ + +
+

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

+ +
+
+ +
+ +
+

+ Dab on the h8rs +

+ + +
+ Garfield dressed in goth +

Reasons Garfield would be a bad husband:

+ + +
+
    +
  • He is a cat
  • +
  • He is lazy
  • +
  • He does not pay taxes
  • +
+ + +
+ + +
+
+
); } diff --git a/src/HtmlCss.test.tsx b/src/HtmlCss.test.tsx new file mode 100644 index 0000000000..320cb97524 --- /dev/null +++ b/src/HtmlCss.test.tsx @@ -0,0 +1,83 @@ +import React from "react"; +import { render, screen } from "@testing-library/react"; +import App from "./App"; +import userEvent from "@testing-library/user-event"; + +describe("Some HTML Elements are added.", () => { + test("(2 pts) There is a header", () => { + render(); + const header = screen.getByRole("heading"); + expect(header).toBeInTheDocument(); + }); + + test("(2 pts) There is an image with alt text", () => { + render(); + const image = screen.getByRole("img"); + expect(image).toBeInTheDocument(); + expect(image).toHaveAttribute("alt"); + }); + + test("(2 pts) There is a list with at least three elements", () => { + render(); + const list = screen.getByRole("list"); + expect(list).toBeInTheDocument(); + expect(list.children.length).toBeGreaterThanOrEqual(3); + }); +}); + +describe("(2 pts) Some basic CSS is added.", () => { + test("The background color of the header area is different", () => { + render(); + const banner = screen.getByRole("banner"); + expect(banner).not.toHaveStyle({ + "background-color": "rgb(40, 44, 52)", + }); + }); +}); + +describe("(2 pts) Some Bootstrap Elements are added", () => { + test("There is one bootstrap button with the text 'Log Hello World'", () => { + render(); + const button = screen.getByRole("button", { name: /Log Hello World/i }); + expect(button).toBeInTheDocument(); + expect(button).toHaveClass("btn"); + expect(button).toHaveClass("btn-primary"); + }); + + test("(2 pts) Not clicking the bootstrap button does not logs 'Hello World!'", () => { + const consoleSpy = jest.spyOn(console, "log"); + render(); + expect(consoleSpy).not.toHaveBeenCalledWith("Hello World!"); + }); + + test("(2 pts) Clicking the bootstrap button logs 'Hello World!'", () => { + const consoleSpy = jest.spyOn(console, "log"); + render(); + const button = screen.getByRole("button", { name: /Log Hello World/i }); + userEvent.click(button); + expect(consoleSpy).toHaveBeenCalledWith("Hello World!"); + }); +}); + +describe("Some additional CSS was added", () => { + test("(2 pts) checks if any element has a background color of red", () => { + const { container } = render(); + // Get all elements in the rendered container + const elements = container.querySelectorAll("*"); + + // Check if any element has a background color of red + let foundRedBackground = false; + + elements.forEach((element) => { + const style = getComputedStyle(element); + if ( + style.backgroundColor === "red" || + style.backgroundColor === "rgb(255, 0, 0)" + ) { + foundRedBackground = true; + } + }); + + expect(foundRedBackground).toBe(true); + }); +}); diff --git a/src/arrays.test.ts b/src/arrays.test.ts new file mode 100644 index 0000000000..c2847517bd --- /dev/null +++ b/src/arrays.test.ts @@ -0,0 +1,273 @@ +import { + allRGB, + bookEndList, + countShortWords, + injectPositive, + makeMath, + removeDollars, + shoutIfExclaiming, + stringsToIntegers, + tripleNumbers, +} from "./arrays"; + +describe("Testing the array functions", () => { + ////////////////////////////////// + // bookEndList and tripleNumbers + + const NUMBERS_1 = [1, 2, 3]; + const NUMBERS_2 = [100, 300, 200]; + const NUMBERS_3 = [5]; + const NUMBERS_4: number[] = []; + const NUMBERS_5 = [100, 199, 1, -5, 7, 3]; + const NUMBERS_6 = [-100, -200, 100, 200]; + const NUMBERS_7 = [199, 1, 550, 50, 200]; + + // Ensure that none of the arrays were changed mutably + // If you fail these, you aren't using map/filter/reduce/etc. properly! + afterEach(() => { + expect(NUMBERS_1).toEqual([1, 2, 3]); + expect(NUMBERS_2).toEqual([100, 300, 200]); + expect(NUMBERS_3).toEqual([5]); + expect(NUMBERS_4).toEqual([]); + expect(NUMBERS_5).toEqual([100, 199, 1, -5, 7, 3]); + expect(NUMBERS_6).toEqual([-100, -200, 100, 200]); + expect(NUMBERS_7).toEqual([199, 1, 550, 50, 200]); + }); + + test("(3 pts) Testing the bookEndList function", () => { + expect(bookEndList(NUMBERS_1)).toEqual([1, 3]); + expect(bookEndList(NUMBERS_2)).toEqual([100, 200]); + expect(bookEndList(NUMBERS_3)).toEqual([5, 5]); + expect(bookEndList(NUMBERS_4)).toEqual([]); + expect(bookEndList(NUMBERS_5)).toEqual([100, 3]); + expect(bookEndList(NUMBERS_6)).toEqual([-100, 200]); + }); + + test("(3 pts) Testing the tripleNumbers function", () => { + expect(tripleNumbers(NUMBERS_1)).toEqual([3, 6, 9]); + expect(tripleNumbers(NUMBERS_2)).toEqual([300, 900, 600]); + expect(tripleNumbers(NUMBERS_3)).toEqual([15]); + expect(tripleNumbers(NUMBERS_4)).toEqual([]); + expect(tripleNumbers(NUMBERS_5)).toEqual([300, 597, 3, -15, 21, 9]); + expect(tripleNumbers(NUMBERS_6)).toEqual([-300, -600, 300, 600]); + }); + + ////////////////////////////////// + // stringsToIntegers + + const VALUES_1 = ["1", "2", "3"]; + const VALUES_2 = ["100", "200", "300"]; + const VALUES_3 = ["5"]; + const VALUES_4: string[] = []; + const VALUES_5 = ["100", "?", "27", "$44"]; + const VALUES_6 = ["-1", "0", "1", "*1"]; + const VALUES_7 = ["apple", "banana", "cactus"]; + + // Ensure that none of the arrays were changed mutably + // If you fail these, you aren't using map/filter/reduce/etc. properly! + afterEach(() => { + expect(VALUES_1).toEqual(["1", "2", "3"]); + expect(VALUES_2).toEqual(["100", "200", "300"]); + expect(VALUES_3).toEqual(["5"]); + expect(VALUES_4).toEqual([]); + expect(VALUES_5).toEqual(["100", "?", "27", "$44"]); + expect(VALUES_6).toEqual(["-1", "0", "1", "*1"]); + expect(VALUES_7).toEqual(["apple", "banana", "cactus"]); + }); + + test("(3 pts) Testing the stringsToIntegers function", () => { + expect(stringsToIntegers(VALUES_1)).toEqual([1, 2, 3]); + expect(stringsToIntegers(VALUES_2)).toEqual([100, 200, 300]); + expect(stringsToIntegers(VALUES_3)).toEqual([5]); + expect(stringsToIntegers(VALUES_4)).toEqual([]); + expect(stringsToIntegers(VALUES_5)).toEqual([100, 0, 27, 0]); + expect(stringsToIntegers(VALUES_6)).toEqual([-1, 0, 1, 0]); + expect(stringsToIntegers(VALUES_7)).toEqual([0, 0, 0]); + }); + + ////////////////////////////////// + // removeDollars + + const AMOUNTS_1 = ["$1", "$2", "$3"]; + const AMOUNTS_2 = ["$100", "$200", "$300", "$400"]; + const AMOUNTS_3 = ["$5"]; + const AMOUNTS_4 = ["$"]; + const AMOUNTS_5 = ["100", "200", "$300", "$400"]; + const AMOUNTS_6: string[] = []; + const AMOUNTS_7 = ["100", "???", "7", "$233", "", "$"]; + const AMOUNTS_8 = ["$one", "two", "$three"]; + + // Ensure that none of the arrays were changed mutably + // If you fail these, you aren't using map/filter/reduce/etc. properly! + afterEach(() => { + expect(AMOUNTS_1).toEqual(["$1", "$2", "$3"]); + expect(AMOUNTS_2).toEqual(["$100", "$200", "$300", "$400"]); + expect(AMOUNTS_3).toEqual(["$5"]); + expect(AMOUNTS_4).toEqual(["$"]); + expect(AMOUNTS_5).toEqual(["100", "200", "$300", "$400"]); + expect(AMOUNTS_6).toEqual([]); + expect(AMOUNTS_7).toEqual(["100", "???", "7", "$233", "", "$"]); + expect(AMOUNTS_8).toEqual(["$one", "two", "$three"]); + }); + + test("(3 pts) Testing the removeDollars function", () => { + expect(removeDollars(AMOUNTS_1)).toEqual([1, 2, 3]); + expect(removeDollars(AMOUNTS_2)).toEqual([100, 200, 300, 400]); + expect(removeDollars(AMOUNTS_3)).toEqual([5]); + expect(removeDollars(AMOUNTS_4)).toEqual([0]); + expect(removeDollars(AMOUNTS_5)).toEqual([100, 200, 300, 400]); + expect(removeDollars(AMOUNTS_6)).toEqual([]); + expect(removeDollars(AMOUNTS_7)).toEqual([100, 0, 7, 233, 0, 0]); + expect(removeDollars(AMOUNTS_8)).toEqual([0, 0, 0]); + }); + + ////////////////////////////////// + // shoutIfExclaiming + + const MESSAGE_1 = ["Hello", "you", "are", "great!"]; + const MESSAGE_2 = ["oho!", "Oho!", "oHo!", "oHO!", "OHO!"]; + const MESSAGE_3 = ["Wait?", "What?", "Lo", "How?", "High!"]; + const MESSAGE_4 = ["??????"]; + const MESSAGE_5: string[] = ["This one is very long!"]; + const MESSAGE_6 = ["No", "Caps", "here.", "Right?"]; + + // Ensure that none of the arrays were changed mutably + // If you fail these, you aren't using map/filter/reduce/etc. properly! + afterEach(() => { + expect(MESSAGE_1).toEqual(["Hello", "you", "are", "great!"]); + expect(MESSAGE_2).toEqual(["oho!", "Oho!", "oHo!", "oHO!", "OHO!"]); + expect(MESSAGE_3).toEqual(["Wait?", "What?", "Lo", "How?", "High!"]); + expect(MESSAGE_4).toEqual(["??????"]); + expect(MESSAGE_5).toEqual(["This one is very long!"]); + expect(MESSAGE_6).toEqual(["No", "Caps", "here.", "Right?"]); + }); + + test("(3 pts) Testing the shoutIfExclaiming function", () => { + expect(shoutIfExclaiming(MESSAGE_1)).toEqual([ + "Hello", + "you", + "are", + "GREAT!", + ]); + expect(shoutIfExclaiming(MESSAGE_2)).toEqual([ + "OHO!", + "OHO!", + "OHO!", + "OHO!", + "OHO!", + ]); + expect(shoutIfExclaiming(MESSAGE_3)).toEqual(["Lo", "HIGH!"]); + expect(shoutIfExclaiming(MESSAGE_4)).toEqual([]); + expect(shoutIfExclaiming(MESSAGE_5)).toEqual([ + "THIS ONE IS VERY LONG!", + ]); + expect(shoutIfExclaiming(MESSAGE_6)).toEqual(["No", "Caps", "here."]); + }); + + ////////////////////////////////// + // countShortWords + + const WORDS_1 = ["the", "cat", "in", "the", "hat"]; + const WORDS_2 = ["one", "two", "three", "four", "five", "six", "seven"]; + const WORDS_3 = ["alpha", "beta", "gamma"]; + const WORDS_4 = ["Longest", "Words", "Possible"]; + const WORDS_5: string[] = []; + const WORDS_6 = ["", "", "", ""]; + + // Ensure that none of the arrays were changed mutably + // If you fail these, you aren't using map/filter/reduce/etc. properly! + afterEach(() => { + expect(WORDS_1).toEqual(["the", "cat", "in", "the", "hat"]); + expect(WORDS_2).toEqual([ + "one", + "two", + "three", + "four", + "five", + "six", + "seven", + ]); + expect(WORDS_3).toEqual(["alpha", "beta", "gamma"]); + expect(WORDS_4).toEqual(["Longest", "Words", "Possible"]); + expect(WORDS_5).toEqual([]); + expect(WORDS_6).toEqual(["", "", "", ""]); + }); + + test("(3 pts) Testing the countShortWords function", () => { + expect(countShortWords(WORDS_1)).toEqual(5); + expect(countShortWords(WORDS_2)).toEqual(3); + expect(countShortWords(WORDS_3)).toEqual(0); + expect(countShortWords(WORDS_4)).toEqual(0); + expect(countShortWords(WORDS_5)).toEqual(0); + expect(countShortWords(WORDS_6)).toEqual(4); + }); + + ////////////////////////////////// + // allRGB + + const COLORS_1 = ["red", "green", "blue"]; + const COLORS_2 = ["red", "red", "red"]; + const COLORS_3 = ["red", "red", "blue", "blue", "green", "red"]; + const COLORS_4 = ["purple", "orange", "violet"]; + const COLORS_5 = ["red", "blue", "yellow"]; + const COLORS_6 = ["green"]; + const COLORS_7 = ["red"]; + const COLORS_8 = ["kabluey"]; + const COLORS_9: string[] = []; + + // Ensure that none of the arrays were changed mutably + // If you fail these, you aren't using map/filter/reduce/etc. properly! + afterEach(() => { + expect(COLORS_1).toEqual(["red", "green", "blue"]); + expect(COLORS_2).toEqual(["red", "red", "red"]); + expect(COLORS_3).toEqual([ + "red", + "red", + "blue", + "blue", + "green", + "red", + ]); + expect(COLORS_4).toEqual(["purple", "orange", "violet"]); + expect(COLORS_5).toEqual(["red", "blue", "yellow"]); + expect(COLORS_6).toEqual(["green"]); + expect(COLORS_7).toEqual(["red"]); + expect(COLORS_8).toEqual(["kabluey"]); + expect(COLORS_9).toEqual([]); + }); + + test("(3 pts) Testing the allRGB function", () => { + expect(allRGB(COLORS_1)).toEqual(true); + expect(allRGB(COLORS_2)).toEqual(true); + expect(allRGB(COLORS_3)).toEqual(true); + expect(allRGB(COLORS_4)).toEqual(false); + expect(allRGB(COLORS_5)).toEqual(false); + expect(allRGB(COLORS_6)).toEqual(true); + expect(allRGB(COLORS_7)).toEqual(true); + expect(allRGB(COLORS_8)).toEqual(false); + expect(allRGB(COLORS_9)).toEqual(true); + }); + + ////////////////////////////////// + // makeMath + + test("(3 pts) Testing the makeMath function", () => { + expect(makeMath(NUMBERS_1)).toEqual("6=1+2+3"); + expect(makeMath(NUMBERS_2)).toEqual("600=100+300+200"); + expect(makeMath(NUMBERS_3)).toEqual("5=5"); + expect(makeMath(NUMBERS_4)).toEqual("0=0"); + expect(makeMath(NUMBERS_7)).toEqual("1000=199+1+550+50+200"); + }); + + ////////////////////////////////// + // injectPositive + test("(3 pts) Testing the injectPositive function", () => { + expect(injectPositive(NUMBERS_1)).toEqual([1, 2, 3, 6]); + expect(injectPositive(NUMBERS_2)).toEqual([100, 300, 200, 600]); + expect(injectPositive(NUMBERS_3)).toEqual([5, 5]); + expect(injectPositive(NUMBERS_4)).toEqual([0]); + expect(injectPositive(NUMBERS_5)).toEqual([100, 199, 1, -5, 300, 7, 3]); + expect(injectPositive(NUMBERS_6)).toEqual([-100, 0, -200, 100, 200]); + expect(injectPositive(NUMBERS_7)).toEqual([199, 1, 550, 50, 200, 1000]); + }); +}); diff --git a/src/arrays.ts b/src/arrays.ts new file mode 100644 index 0000000000..f964636208 --- /dev/null +++ b/src/arrays.ts @@ -0,0 +1,153 @@ +/** + * Consume an array of numbers, and return a new array containing + * JUST the first and last number. If there are no elements, return + * an empty array. If there is one element, the resulting list should + * the number twice. + */ +export function bookEndList(numbers: number[]): number[] { + if (numbers.length === 0) { + return []; + } + if (numbers.length === 1) { + return [numbers[0], numbers[0]]; + } + let numbers2: number[] = [numbers[0], numbers[numbers.length - 1]]; + return numbers2; +} + +/** + * Consume an array of numbers, and return a new array where each + * number has been tripled (multiplied by 3). + */ +export function tripleNumbers(numbers: number[]): number[] { + let triple = numbers.map((number: number): number => number * 3); + return triple; +} + +/** + * Consume an array of strings and convert them to integers. If + * the number cannot be parsed as an integer, convert it to 0 instead. + */ +export function stringsToIntegers(numbers: string[]): number[] { + let newList = numbers.map((num: string): number => + isNaN(Number(num)) ? 0 : Number(num), + ); + return newList; +} + +/** + * Consume an array of strings and return them as numbers. Note that + * the strings MAY have "$" symbols at the beginning, in which case + * those should be removed. If the result cannot be parsed as an integer, + * convert it to 0 instead. + */ +// Remember, you can write functions as lambdas too! They work exactly the same. +export const removeDollars = (amounts: string[]): number[] => { + let newList = amounts.map((amount: string): number => + amount[0] === "$" ? Number(amount.slice(1)) : Number(amount), + ); + let newList2 = newList.map((thing: number): number => + isNaN(thing) ? 0 : thing, + ); + return newList2; +}; + +/** + * Consume an array of messages and return a new list of the messages. However, any + * string that ends in "!" should be made uppercase. Also, remove any strings that end + * in question marks ("?"). + */ +export const shoutIfExclaiming = (messages: string[]): string[] => { + let newlist: string[] = []; + newlist = messages.filter( + (message: string): boolean => !message.endsWith("?"), + ); + let newList2 = newlist.map((thing: string): string => + thing.endsWith("!") ? thing.toUpperCase() : thing, + ); + return newList2; +}; + +/** + * Consumes an array of words and returns the number of words that are LESS THAN + * 4 letters long. + */ +export function countShortWords(words: string[]): number { + let newList = words.filter((word: string): boolean => word.length < 4); + return newList.length; +} + +/** + * Consumes an array of colors (e.g., 'red', 'purple') and returns true if ALL + * the colors are either 'red', 'blue', or 'green'. If an empty list is given, + * then return true. + */ +export function allRGB(colors: string[]): boolean { + let newList = colors.filter( + (color: string): boolean => + color === "red" || color === "blue" || color === "green", + ); + if (newList.length === colors.length) { + return true; + } else { + return false; + } +} + +/** + * Consumes an array of numbers, and produces a string representation of the + * numbers being added together along with their actual sum. + * + * For instance, the array [1, 2, 3] would become "6=1+2+3". + * And the array [] would become "0=0". + */ +export function makeMath(addends: number[]): string { + let sum: number = addends.reduce((accumulate, currentVal) => { + return accumulate + currentVal; + }, 0); + let newList: string[] = addends.map((add: number): string => + add === addends[addends.length - 1] ? + add.toString() + : add.toString() + "+", + ); + let sumString: string = sum.toString() + "="; + newList = [sumString, ...newList]; + let newString: string = newList.join(""); + if (newString === "0=") { + return "0=0"; + } else { + return newString; + } +} + +/** + * Consumes an array of numbers and produces a new array of the same numbers, + * with one difference. After the FIRST negative number, insert the sum of all + * previous numbers in the list. If there are no negative numbers, then append + * the sum to the list. + * + * For instance, the array [1, 9, -5, 7] would become [1, 9, -5, 10, 7] + * And the array [1, 9, 7] would become [1, 9, 7, 17] + */ +export function injectPositive(values: number[]): number[] { + let valuesClone: number[] = [...values]; + let negativeIndex = valuesClone.some((num: number) => num < 0); + + if (negativeIndex) { + let foundIndex: number = valuesClone.findIndex( + (num: number) => num < 0, + ); + let sumList: number[] = valuesClone.slice(0, foundIndex); + let sum: number = sumList.reduce((accumulate, currentValue) => { + return accumulate + currentValue; + }, 0); + valuesClone.splice(foundIndex + 1, 0, sum); + return valuesClone; + } else { + let sum: number = valuesClone.reduce((accumulate, currentValue) => { + return accumulate + currentValue; + }, 0); + valuesClone.push(sum); + return valuesClone; + } +} diff --git a/src/assets/gothfield.jpg b/src/assets/gothfield.jpg new file mode 100644 index 0000000000..0b3c7351d6 Binary files /dev/null and b/src/assets/gothfield.jpg differ 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..c4fa4c75d3 --- /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..8e865f56dc --- /dev/null +++ b/src/bad-components/ColoredBox.tsx @@ -0,0 +1,57 @@ +import React, { useState } from "react"; +import { Button } from "react-bootstrap"; + +export const COLORS = ["red", "blue", "green"]; +const DEFAULT_COLOR_INDEX = 0; + +interface colorInterface { + colorList: string[]; + colIndex: number; +} + +// onClick={() => {setColorIndex((1 + colorIndex) % COLORS.length);}} + +function ChangeColor({ + colorChange, +}: { + colorChange: () => void; +}): React.JSX.Element { + return ; +} + +function ColorPreview(props: colorInterface): 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]} +
    + { + setColorIndex((1 + colorIndex) % COLORS.length); + }} + > + +
    +
    + ); +} 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..fde5ef0066 --- /dev/null +++ b/src/bad-components/DoubleHalf.tsx @@ -0,0 +1,36 @@ +import React, { useState } from "react"; +import { Button } from "react-bootstrap"; + +function Doubler({ times2 }: { times2: () => void }): React.JSX.Element { + return ; +} + +function Halver({ + timesOneHalf, +}: { + timesOneHalf: () => void; +}): React.JSX.Element { + return ; +} + +export function DoubleHalf(): React.JSX.Element { + const [dhValue, setDhValue] = useState(10); + return ( +
    +

    Double Half

    +
    + The current value is: {dhValue} +
    + { + setDhValue(dhValue * 2); + }} + > + { + setDhValue(dhValue * 0.5); + }} + > +
    + ); +} diff --git a/src/bad-components/ShoveBox.test.tsx b/src/bad-components/ShoveBox.test.tsx new file mode 100644 index 0000000000..e89abf2751 --- /dev/null +++ b/src/bad-components/ShoveBox.test.tsx @@ -0,0 +1,37 @@ +import React, { act } from "react"; +import { render, screen } from "@testing-library/react"; +import { ShoveBox } from "./ShoveBox"; + +describe("ShoveBox Component tests", () => { + beforeEach(() => { + render(); + }); + test("(2 pts) The MoveableBox is initially nearby.", () => { + const box = screen.getByTestId("moveable-box"); + expect(box).toHaveStyle({ marginLeft: "10px" }); + }); + test("(2 pts) There is a button", () => { + expect(screen.getByRole("button")).toBeInTheDocument(); + }); + test("(2 pts) Clicking the button moves the box.", async () => { + const shoveBox = screen.getByRole("button"); + await act(async () => { + shoveBox.click(); + }); + expect(screen.getByTestId("moveable-box")).toHaveStyle({ + marginLeft: "14px", + }); + await act(async () => { + shoveBox.click(); + }); + expect(screen.getByTestId("moveable-box")).toHaveStyle({ + marginLeft: "18px", + }); + await act(async () => { + shoveBox.click(); + }); + expect(screen.getByTestId("moveable-box")).toHaveStyle({ + marginLeft: "22px", + }); + }); +}); diff --git a/src/bad-components/ShoveBox.tsx b/src/bad-components/ShoveBox.tsx new file mode 100644 index 0000000000..ce3ff952f8 --- /dev/null +++ b/src/bad-components/ShoveBox.tsx @@ -0,0 +1,49 @@ +import React, { useState } from "react"; +import { Button } from "react-bootstrap"; + +// {position,setPosition,}: {position: number;setPosition: (newPosition: number) => void;} + +interface where { + place: number; +} + +function ShoveBoxButton({ shove }: { shove: () => void }) { + return ; +} + +function MoveableBox(props: where): React.JSX.Element { + return ( +
    + ); +} + +export function ShoveBox(): React.JSX.Element { + const [position, setPosition] = useState(10); + // const box = MoveableBox(); + + return ( +
    +

    Shove Box

    + The box is at: {position} +
    + { + setPosition(4 + position); + }} + > + +
    +
    + ); +} 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..ca058203be --- /dev/null +++ b/src/components/ChangeType.tsx @@ -0,0 +1,28 @@ +import React, { useState } from "react"; +import { Button } from "react-bootstrap"; +import { QuestionType } from "../interfaces/question"; + +export function ChangeType(): React.JSX.Element { + const [question, setQuestion] = useState( + "short_answer_question", + ); + + function flipQuestion(): void { + setQuestion( + question === "short_answer_question" ? + "multiple_choice_question" + : "short_answer_question", + ); + } + + return ( +
    + +
    + {question === "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..3c603c7143 --- /dev/null +++ b/src/components/Counter.tsx @@ -0,0 +1,18 @@ +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..ae364a0b5b --- /dev/null +++ b/src/components/CycleHoliday.test.tsx @@ -0,0 +1,59 @@ +import React, { act } from "react"; +import { render, screen } from "@testing-library/react"; +import { CycleHoliday } from "./CycleHoliday"; + +describe("CycleHoliday Component tests", () => { + beforeEach(() => { + render(); + }); + + test("(1 pts) An initial holiday is displayed", () => { + const initialHoliday = screen.getByText(/Holiday: (.*)/i); + expect(initialHoliday).toBeInTheDocument(); + }); + + test("(1 pts) 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("(1 pts) Can cycle through 5 distinct holidays alphabetically", async () => { + 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 || ""); + await act(async () => { + alphabetButton.click(); + }); + } + const uniqueStates = states.filter((x, y) => states.indexOf(x) == y); + expect(uniqueStates).toHaveLength(5); + expect(states[0]).toEqual(states[5]); + }); + + test("(1 pts) Can cycle through 5 distinct holidays by year", async () => { + 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 || ""); + await act(async () => { + 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..35160f50e9 --- /dev/null +++ b/src/components/CycleHoliday.tsx @@ -0,0 +1,36 @@ +import React, { useState } from "react"; +import { Button } from "react-bootstrap"; + +export function CycleHoliday(): React.JSX.Element { + const [holidays, setHoliday] = useState("🐲"); + + function sortByAlphabet(): void { + setHoliday( + holidays === "🐲" ? "🎁" + : holidays === "🎁" ? "🌎" + : holidays === "🌎" ? "🍀" + : holidays === "🍀" ? "💖" + : "🐲", + ); + } + + function sortByDate(): void { + setHoliday( + holidays === "💖" ? "🐲" + : holidays === "🐲" ? "🍀" + : holidays === "🍀" ? "🌎" + : holidays === "🌎" ? "🎁" + : "💖", + ); + } + + return ( +
    + Holiday: {holidays} +
    + + +
    +
    + ); +} 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..4a57bf064a --- /dev/null +++ b/src/components/RevealAnswer.tsx @@ -0,0 +1,17 @@ +import React, { useState } from "react"; +import { Button } from "react-bootstrap"; + +export function RevealAnswer(): React.JSX.Element { + const [visible, setVisible] = useState(false); + + function flipVisible(): void { + setVisible(!visible); + } + + return ( +
    + + {visible &&
    42
    } +
    + ); +} diff --git a/src/components/StartAttempt.test.tsx b/src/components/StartAttempt.test.tsx new file mode 100644 index 0000000000..fd326936e6 --- /dev/null +++ b/src/components/StartAttempt.test.tsx @@ -0,0 +1,225 @@ +import React, { act } from "react"; +import { render, screen, cleanup } 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(); + }); + afterEach(() => { + cleanup(); + }); + test("(1 pts) The Number of attempts is displayed initially, without other numbers", () => { + const attemptNumber = screen.getByText(/(\d+)/); + expect(attemptNumber).toBeInTheDocument(); + }); + test("(1 pts) The Number of attempts is more than 0", () => { + const attemptNumber = extractDigits(screen.getByText(/(\d+)/)); + expect(attemptNumber).toBeGreaterThan(0); + }); + test("(1 pts) The Number of attempts is less than 10", () => { + const attemptNumber = extractDigits(screen.getByText(/(\d+)/)); + expect(attemptNumber).toBeLessThan(10); + }); + test("(1 pts) There is an initially enabled Start Quiz button", () => { + const startButton = screen.getByRole("button", { name: /Start Quiz/i }); + expect(startButton).toBeInTheDocument(); + expect(startButton).toBeEnabled(); + }); + test("(1 pts) There is an initially disabled Stop Quiz button", () => { + const stopButton = screen.getByRole("button", { name: /Stop Quiz/i }); + expect(stopButton).toBeInTheDocument(); + expect(stopButton).toBeDisabled(); + }); + test("(1 pts) There is an initially enabled Mulligan button", () => { + const mulliganButton = screen.getByRole("button", { + name: /Mulligan/i + }); + expect(mulliganButton).toBeInTheDocument(); + expect(mulliganButton).toBeEnabled(); + }); + test("(1 pts) Clicking Mulligan increases attempts", async () => { + const attemptNumber: number = + extractDigits(screen.getByText(/(\d+)/)) || 0; + const mulliganButton = screen.getByRole("button", { + name: /Mulligan/i + }); + await act(async () => { + mulliganButton.click(); + }); + const attemptNumberLater = extractDigits(screen.getByText(/(\d+)/)); + expect(attemptNumber + 1).toEqual(attemptNumberLater); + }); + test("(1 pts) Clicking Mulligan twice increases attempts by two", async () => { + const attemptNumber: number = + extractDigits(screen.getByText(/(\d+)/)) || 0; + const mulliganButton = screen.getByRole("button", { + name: /Mulligan/i + }); + await act(async () => { + mulliganButton.click(); + }); + await act(async () => { + mulliganButton.click(); + }); + const attemptNumberLater = extractDigits(screen.getByText(/(\d+)/)); + expect(attemptNumber + 2).toEqual(attemptNumberLater); + }); + test("(1 pts) Clicking Start Quiz decreases attempts", async () => { + const attemptNumber: number = + extractDigits(screen.getByText(/(\d+)/)) || 0; + const startButton = screen.getByRole("button", { + name: /Start Quiz/i + }); + await act(async () => { + startButton.click(); + }); + const attemptNumberLater = + extractDigits(screen.getByText(/(\d+)/)) || 0; + expect(attemptNumber - 1).toEqual(attemptNumberLater); + }); + test("(1 pts) Clicking Start Quiz changes enabled buttons", async () => { + // 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 + await act(async () => { + startButton.click(); + }); + // Then the start is disabled, stop is enabled, and mulligan is disabled + expect(startButton).toBeDisabled(); + expect(stopButton).toBeEnabled(); + expect(mulliganButton).toBeDisabled(); + }); + test("(1 pts) Clicking Start and Stop Quiz changes enabled buttons", async () => { + // 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 + await act(async () => { + startButton.click(); + }); + await act(async () => { + stopButton.click(); + }); + // Then the start is enabled, stop is disabled, and mulligan is enabled + expect(startButton).toBeEnabled(); + expect(stopButton).toBeDisabled(); + expect(mulliganButton).toBeEnabled(); + }); + test("(1 pts) Clicking Start, Stop, Mulligan sets attempts to original", async () => { + // 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 + await act(async () => { + startButton.click(); + }); + await act(async () => { + 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 + await act(async () => { + mulliganButton.click(); + }); + // Then the attempt is increased back to starting value + const attemptNumberLatest: number = + extractDigits(screen.getByText(/(\d+)/)) || 0; + expect(attemptNumber).toEqual(attemptNumberLatest); + }); + test("(1 pts) Cannot click start quiz when out of attempts", async () => { + // 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... + await act(async () => { + startButton.click(); + }); + await act(async () => { + 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 + await act(async () => { + 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..bd5817d83e --- /dev/null +++ b/src/components/StartAttempt.tsx @@ -0,0 +1,37 @@ +import React, { useState } from "react"; +import { Button } from "react-bootstrap"; + +export function StartAttempt(): React.JSX.Element { + const [attempts, setAttempt] = useState(4); + const [inProgress, setProgress] = useState(false); + + function start(): void { + setAttempt(attempts - 1); + setProgress(true); + } + + function upAttempt(): void { + setAttempt(attempts + 1); + } + + function stop(): void { + setProgress(false); + } + + return ( +
    + {attempts} +
    + + + +
    +
    + ); +} diff --git a/src/components/TwoDice.test.tsx b/src/components/TwoDice.test.tsx new file mode 100644 index 0000000000..e5f9966deb --- /dev/null +++ b/src/components/TwoDice.test.tsx @@ -0,0 +1,174 @@ +import React, { act } from "react"; +import { render, screen } from "@testing-library/react"; +import { TwoDice } from "./TwoDice"; +import { extractDigits } from "./StartAttempt.test"; + +describe("TwoDice Component tests", () => { + let mathRandomFunction: jest.SpyInstance; + 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..b9b2027e5e --- /dev/null +++ b/src/components/TwoDice.tsx @@ -0,0 +1,40 @@ +import React, { useState } from "react"; +import { Button } from "react-bootstrap"; + +/** + * Here is a helper function you *must* use to "roll" your die. + * The function uses the builtin `random` function of the `Math` + * module (which returns a random decimal between 0 up until 1) in order + * to produce a random integer between 1 and 6 (inclusive). + */ +export function d6(): number { + return 1 + Math.floor(Math.random() * 6); +} + +export function TwoDice(): React.JSX.Element { + const [dice1, setDice1] = useState(1); + const [dice2, setDice2] = useState(2); + + function rollRight(): void { + setDice2(d6()); + } + + function rollLeft(): void { + setDice1(d6()); + } + + return ( +
    + {dice1} + {dice2} +
    + + +
    +
    + {dice1 === dice2 && dice1 !== 1 && Win} + {dice1 === dice2 && dice1 === 1 && Lose} +
    +
    + ); +} diff --git a/src/data/questions.json b/src/data/questions.json new file mode 100644 index 0000000000..3b19537526 --- /dev/null +++ b/src/data/questions.json @@ -0,0 +1,79 @@ +{ + "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 + } + ], + "SIMPLE_QUESTIONS_2": [], + "EMPTY_QUESTIONS": [], + "TRIVIA_QUESTIONS": [] +} diff --git a/src/form-components/ChangeColor.test.tsx b/src/form-components/ChangeColor.test.tsx new file mode 100644 index 0000000000..4055ee0508 --- /dev/null +++ b/src/form-components/ChangeColor.test.tsx @@ -0,0 +1,43 @@ +import React, { act } from "react"; +import { render, screen } from "@testing-library/react"; +import { ChangeColor } from "./ChangeColor"; + +describe("ChangeColor Component tests", () => { + beforeEach(() => render()); + test("(2 pts) There are at least 8 radio buttons and the colored box", () => { + const radios = screen.getAllByRole("radio"); + expect(radios.length).toBeGreaterThanOrEqual(8); + expect(screen.getByTestId("colored-box")).toBeInTheDocument(); + }); + test("(2 pts) Switching the color switches the displayed color.", async () => { + const radios: HTMLInputElement[] = screen.getAllByRole("radio"); + // Switch to first + await act(async () => { + radios[0].click(); + }); + let coloredBox = screen.getByTestId("colored-box"); + expect(coloredBox).toHaveTextContent(radios[0].value); + expect(coloredBox).toHaveStyle({ backgroundColor: radios[0].value }); + // Switch to third + await act(async () => { + radios[2].click(); + }); + coloredBox = screen.getByTestId("colored-box"); + expect(coloredBox).toHaveTextContent(radios[2].value); + expect(coloredBox).toHaveStyle({ backgroundColor: radios[2].value }); + // Switch to 8th + await act(async () => { + radios[7].click(); + }); + coloredBox = screen.getByTestId("colored-box"); + expect(coloredBox).toHaveTextContent(radios[7].value); + expect(coloredBox).toHaveStyle({ backgroundColor: radios[7].value }); + // Switch back to first + await act(async () => { + radios[0].click(); + }); + coloredBox = screen.getByTestId("colored-box"); + expect(coloredBox).toHaveTextContent(radios[0].value); + expect(coloredBox).toHaveStyle({ backgroundColor: radios[0].value }); + }); +}); diff --git a/src/form-components/ChangeColor.tsx b/src/form-components/ChangeColor.tsx new file mode 100644 index 0000000000..6b45834e0a --- /dev/null +++ b/src/form-components/ChangeColor.tsx @@ -0,0 +1,46 @@ +import React, { useState } from "react"; +import { Form } from "react-bootstrap"; + +const COLOR_LIST = [ + "red", + "green", + "blue", + "yellow", + "purple", + "orange", + "cyan", + "white", +]; +const COLOR_DEFAULT = COLOR_LIST[0]; + +export function ChangeColor(): React.JSX.Element { + const [colors, setColors] = useState(COLOR_DEFAULT); + + function updateColor(event: React.ChangeEvent) { + setColors(event.target.value); + } + + return ( +
    +

    Change Color

    +
    + {COLOR_LIST.map((color: string) => ( + + ))} +
    +
    + {colors} +
    +
    + ); +} diff --git a/src/form-components/CheckAnswer.test.tsx b/src/form-components/CheckAnswer.test.tsx new file mode 100644 index 0000000000..df11c436b6 --- /dev/null +++ b/src/form-components/CheckAnswer.test.tsx @@ -0,0 +1,53 @@ +import React, { act } from "react"; +import { render, screen } from "@testing-library/react"; +import { CheckAnswer } from "./CheckAnswer"; +import userEvent from "@testing-library/user-event"; + +describe("CheckAnswer Component tests", () => { + test("(2 pts) There is an input box", () => { + render(); + const inputBox = screen.getByRole("textbox"); + expect(inputBox).toBeInTheDocument(); + }); + test("(2 pts) The answer is originally incorrect.", () => { + render(); + expect(screen.getByText(/❌/i)).toBeInTheDocument(); + expect(screen.queryByText(/✔️/i)).not.toBeInTheDocument(); + }); + test("(2 pts) Entering the right answer makes it correct.", async () => { + render(); + const inputBox = screen.getByRole("textbox"); + await act(async () => { + userEvent.type(inputBox, "42"); + }); + expect(screen.getByText(/✔️/i)).toBeInTheDocument(); + expect(screen.queryByText(/❌/i)).not.toBeInTheDocument(); + }); + test("(2 pts) Entering the wrong answer makes it incorrect.", async () => { + render(); + const inputBox = screen.getByRole("textbox"); + await act(async () => { + userEvent.type(inputBox, "43"); + }); + expect(screen.getByText(/❌/i)).toBeInTheDocument(); + expect(screen.queryByText(/✔️/i)).not.toBeInTheDocument(); + }); + test("(2 pts) Entering a different right answer makes it correct.", async () => { + render(); + const inputBox = screen.getByRole("textbox"); + await act(async () => { + userEvent.type(inputBox, "Hello"); + }); + expect(screen.getByText(/✔️/i)).toBeInTheDocument(); + expect(screen.queryByText(/❌/i)).not.toBeInTheDocument(); + }); + test("(2 pts) Entering a different wrong answer still makes it incorrect.", async () => { + render(); + const inputBox = screen.getByRole("textbox"); + await act(async () => { + userEvent.type(inputBox, "42"); + }); + expect(screen.getByText(/❌/i)).toBeInTheDocument(); + expect(screen.queryByText(/✔️/i)).not.toBeInTheDocument(); + }); +}); diff --git a/src/form-components/CheckAnswer.tsx b/src/form-components/CheckAnswer.tsx new file mode 100644 index 0000000000..678c007f79 --- /dev/null +++ b/src/form-components/CheckAnswer.tsx @@ -0,0 +1,29 @@ +import React, { useState } from "react"; +import { Form } from "react-bootstrap"; + +export function CheckAnswer({ + expectedAnswer, +}: { + expectedAnswer: string; +}): React.JSX.Element { + const [answer, setAnswer] = useState(); + + function update(event: React.ChangeEvent) { + setAnswer(event.target.value); + } + + return ( +
    +

    Check Answer

    + + Answer: + + +
    + {answer === expectedAnswer ? +
    ✔️
    + :
    } +
    +
    + ); +} diff --git a/src/form-components/EditMode.test.tsx b/src/form-components/EditMode.test.tsx new file mode 100644 index 0000000000..a9ffcf6cf9 --- /dev/null +++ b/src/form-components/EditMode.test.tsx @@ -0,0 +1,68 @@ +import React, { act } from "react"; +import { render, screen } from "@testing-library/react"; +import { EditMode } from "./EditMode"; +import userEvent from "@testing-library/user-event"; + +describe("EditMode Component tests", () => { + beforeEach(() => render()); + test("(2 pts) There is one checkbox and no textboxes", () => { + const switchButton = screen.getByRole("checkbox"); + expect(switchButton).toBeInTheDocument(); + expect(switchButton.parentElement).toHaveClass("form-switch"); + expect(screen.queryByRole("textbox")).not.toBeInTheDocument(); + }); + test("(2 pts) Initial text should be 'Your Name is a student'.", () => { + expect(screen.getByText(/Your Name is a student/i)).toBeInTheDocument(); + }); + test("(2 pts) Can switch into Edit Mode", async () => { + const switchButton = screen.getByRole("checkbox"); + await act(async () => { + switchButton.click(); + }); + expect(screen.getByRole("textbox")).toBeInTheDocument(); + expect(screen.getAllByRole("checkbox")).toHaveLength(2); + }); + test("(2 pts) Editing the name and student status changes the text", async () => { + const switchButton = screen.getByRole("checkbox"); + await act(async () => { + switchButton.click(); + }); + const nameBox = screen.getByRole("textbox"); + await act(async () => { + userEvent.type(nameBox, "Ada Lovelace"); + }); + const studentBox = screen.getByRole("checkbox", { name: /student/i }); + await act(async () => { + studentBox.click(); + }); + await act(async () => { + switchButton.click(); + }); + expect( + screen.getByText(/Ada Lovelace is not a student/i), + ).toBeInTheDocument(); + }); + test("(2 pts) Different name, click student box twice changes the text", async () => { + const switchButton = screen.getByRole("checkbox"); + await act(async () => { + switchButton.click(); + }); + const nameBox = screen.getByRole("textbox"); + await act(async () => { + userEvent.type(nameBox, "Alan Turing"); + }); + const studentBox = screen.getByRole("checkbox", { name: /student/i }); + await act(async () => { + studentBox.click(); + }); + await act(async () => { + studentBox.click(); + }); + await act(async () => { + switchButton.click(); + }); + expect( + screen.getByText(/Alan Turing is a student/i), + ).toBeInTheDocument(); + }); +}); diff --git a/src/form-components/EditMode.tsx b/src/form-components/EditMode.tsx new file mode 100644 index 0000000000..f947d99597 --- /dev/null +++ b/src/form-components/EditMode.tsx @@ -0,0 +1,58 @@ +import React, { useState } from "react"; +import { Form } from "react-bootstrap"; + +export function EditMode(): React.JSX.Element { + const [editMode, setEditMode] = useState(false); + const [isStudent, setStudent] = useState(true); + const [userName, setName] = useState("Your Name"); + + function updateEdit(event: React.ChangeEvent) { + setEditMode(event.target.checked); + } + + function updateName(event: React.ChangeEvent) { + setName(event.target.value); + } + + function updateStudent(event: React.ChangeEvent) { + setStudent(event.target.checked); + } + + return ( +
    +

    Edit Mode

    +
    + +
    +
    + {isStudent ? +
    {userName} is a student.
    + :
    {userName} is not a student.
    } +
    +
    + {editMode ? + + Change Name: + + + + :
    } +
    +
    + ); +} diff --git a/src/form-components/GiveAttempts.test.tsx b/src/form-components/GiveAttempts.test.tsx new file mode 100644 index 0000000000..588d959708 --- /dev/null +++ b/src/form-components/GiveAttempts.test.tsx @@ -0,0 +1,64 @@ +import React, { act } from "react"; +import { fireEvent, render, screen } from "@testing-library/react"; +import { GiveAttempts } from "./GiveAttempts"; +import userEvent from "@testing-library/user-event"; + +describe("GiveAttempts Component tests", () => { + beforeEach(() => { + render(); + }); + + test("(2 pts) There is a number entry box and two buttons", () => { + expect(screen.getByRole("spinbutton")).toBeInTheDocument(); + expect(screen.getAllByRole("button")).toHaveLength(2); + }); + + test("(2 pts) There is are initially 3 attempts", () => { + expect(screen.getByText(/3/i)).toBeInTheDocument(); + }); + + test("(2 pts) You can use attempts", async () => { + const use = screen.getByRole("button", { name: /use/i }); + await act(async () => { + use.click(); + }); + expect(screen.getByText(/2/i)).toBeInTheDocument(); + await act(async () => { + use.click(); + }); + await act(async () => { + use.click(); + }); + expect(screen.getByText(/0/i)).toBeInTheDocument(); + expect(use).toBeDisabled(); + }); + test("(2 pts) You can gain arbitrary attempts", async () => { + const gain = screen.getByRole("button", { name: /gain/i }); + const amountToGive = screen.getByRole("spinbutton"); + await act(async () => { + userEvent.type(amountToGive, "10"); + }); + await act(async () => { + gain.click(); + }); + expect(screen.getByText(/13/i)).toBeInTheDocument(); + await act(async () => { + userEvent.type(amountToGive, "100"); + }); + await act(async () => { + gain.click(); + }); + expect(screen.getByText(/113/i)).toBeInTheDocument(); + }); + test("(2 pts) Cannot gain blank amounts", async () => { + const gain = screen.getByRole("button", { name: /gain/i }); + const amountToGive = screen.getByRole("spinbutton"); + await act(async () => { + fireEvent.change(amountToGive, { target: { value: "" } }); + }); + await act(async () => { + gain.click(); + }); + expect(screen.getByText(/3/i)).toBeInTheDocument(); + }); +}); diff --git a/src/form-components/GiveAttempts.tsx b/src/form-components/GiveAttempts.tsx new file mode 100644 index 0000000000..a9835faced --- /dev/null +++ b/src/form-components/GiveAttempts.tsx @@ -0,0 +1,39 @@ +import React, { useState } from "react"; +import { Form, Button } from "react-bootstrap"; + +export function GiveAttempts(): React.JSX.Element { + const [attemptsLeft, setAttemptsLeft] = useState(3); + const [attemptsMore, setAttemptsMore] = useState(""); + const addedInt = parseInt(attemptsMore) || 0; + + function updateMore(event: React.ChangeEvent) { + setAttemptsMore(event.target.value); + } + + function updateLeft(): void { + setAttemptsLeft(attemptsLeft - 1); + } + + function addMore(): void { + setAttemptsLeft(addedInt + attemptsLeft); + } + + return ( +
    +

    Give Attempts

    + + Add Attempts + + +
    Attempts Left: {attemptsLeft}
    + + +
    + ); +} diff --git a/src/form-components/MultipleChoiceQuestion.test.tsx b/src/form-components/MultipleChoiceQuestion.test.tsx new file mode 100644 index 0000000000..69ba4da806 --- /dev/null +++ b/src/form-components/MultipleChoiceQuestion.test.tsx @@ -0,0 +1,89 @@ +import React, { act } from "react"; +import { render, screen } from "@testing-library/react"; +import { MultipleChoiceQuestion } from "./MultipleChoiceQuestion"; +import userEvent from "@testing-library/user-event"; + +describe("MultipleChoiceQuestion Component tests", () => { + test("(2 pts) There is a select box", () => { + render( + , + ); + expect(screen.getByRole("combobox")).toBeInTheDocument(); + }); + test("(2 pts) The answer is initially incorrect", () => { + render( + , + ); + expect(screen.getByText(/❌/i)).toBeInTheDocument(); + expect(screen.queryByText(/✔️/i)).not.toBeInTheDocument(); + }); + test("(2 pts) Can choose the correct answer", async () => { + render( + , + ); + const select = screen.getByRole("combobox"); + await act(async () => { + userEvent.selectOptions(select, "2"); + }); + expect(screen.getByText(/✔️/i)).toBeInTheDocument(); + expect(screen.queryByText(/❌/i)).not.toBeInTheDocument(); + }); + test("(2 pts) Can choose the correct answer and then incorrect", async () => { + render( + , + ); + const select = screen.getByRole("combobox"); + await act(async () => { + userEvent.selectOptions(select, "2"); + }); + expect(screen.getByText(/✔️/i)).toBeInTheDocument(); + expect(screen.queryByText(/❌/i)).not.toBeInTheDocument(); + await act(async () => { + userEvent.selectOptions(select, "3"); + }); + expect(screen.getByText(/❌/i)).toBeInTheDocument(); + expect(screen.queryByText(/✔️/i)).not.toBeInTheDocument(); + }); + test("(2 pts) Can start off initially correct", async () => { + render( + , + ); + const select = screen.getByRole("combobox"); + await act(async () => { + userEvent.selectOptions(select, "Alpha"); + }); + expect(screen.getByText(/✔️/i)).toBeInTheDocument(); + expect(screen.queryByText(/❌/i)).not.toBeInTheDocument(); + }); + test("(2 pts) One more test of choosing the right answer", async () => { + render( + , + ); + expect(screen.getByText(/❌/i)).toBeInTheDocument(); + expect(screen.queryByText(/✔️/i)).not.toBeInTheDocument(); + const select = screen.getByRole("combobox"); + await act(async () => { + userEvent.selectOptions(select, "World"); + }); + expect(screen.getByText(/✔️/i)).toBeInTheDocument(); + expect(screen.queryByText(/❌/i)).not.toBeInTheDocument(); + }); +}); diff --git a/src/form-components/MultipleChoiceQuestion.tsx b/src/form-components/MultipleChoiceQuestion.tsx new file mode 100644 index 0000000000..cc95cac9a2 --- /dev/null +++ b/src/form-components/MultipleChoiceQuestion.tsx @@ -0,0 +1,35 @@ +import React, { useState } from "react"; +import { Form } from "react-bootstrap"; + +export function MultipleChoiceQuestion({ + options, + expectedAnswer, +}: { + options: string[]; + expectedAnswer: string; +}): React.JSX.Element { + const [answer, setAnswer] = useState(options[0]); + + function update(event: React.ChangeEvent) { + setAnswer(event.target.value); + } + + return ( +
    +

    Multiple Choice Question

    +
    + + Pick An Option: + + {options.map((option: string) => ( + + ))} + + +
    +
    {answer === expectedAnswer ? "✔️" : "❌"}
    +
    + ); +} diff --git a/src/functions.test.ts b/src/functions.test.ts new file mode 100644 index 0000000000..3d921f5d64 --- /dev/null +++ b/src/functions.test.ts @@ -0,0 +1,59 @@ +import { + add3, + fahrenheitToCelius, + shout, + isQuestion, + convertYesNo, +} from "./functions"; + +describe("Testing the basic functions", () => { + test("(3 pts) Testing the fahrenheitToCelius function", () => { + expect(fahrenheitToCelius(32)).toBe(0); + expect(fahrenheitToCelius(-40)).toBe(-40); + expect(fahrenheitToCelius(-22)).toBe(-30); + expect(fahrenheitToCelius(14)).toBe(-10); + expect(fahrenheitToCelius(68)).toBe(20); + expect(fahrenheitToCelius(86)).toBe(30); + expect(fahrenheitToCelius(212)).toBe(100); + }); + + test("(3 pts) Testing the add3 function", () => { + expect(add3(1, 2, 3)).toBe(6); + expect(add3(9, 7, 4)).toBe(20); + expect(add3(6, -3, 9)).toBe(15); + expect(add3(10, 1, -9)).toBe(11); + expect(add3(-9, -100, -4)).toBe(0); + expect(add3(-1, -1, 1)).toBe(1); + }); + + test("(3 pts) Testing the shout function", () => { + expect(shout("Hello")).toBe("HELLO!"); + expect(shout("What?")).toBe("WHAT?!"); + expect(shout("oHo")).toBe("OHO!"); + expect(shout("AHHHH!!!")).toBe("AHHHH!!!!"); + expect(shout("")).toBe("!"); + expect(shout("Please go outside")).toBe("PLEASE GO OUTSIDE!"); + }); + + test("(3 pts) Testing the isQuestion function", () => { + expect(isQuestion("Is this a question?")).toBe(true); + expect(isQuestion("Who are you?")).toBe(true); + expect(isQuestion("WHAT ARE YOU !?")).toBe(true); + expect(isQuestion("WHAT IS THIS?!")).toBe(false); + expect(isQuestion("OH GOD!")).toBe(false); + expect(isQuestion("Oh nevermind, it's fine.")).toBe(false); + expect(isQuestion("")).toBe(false); + }); + + test("(3 pts) Testing the convertYesNo function", () => { + expect(convertYesNo("yes")).toBe(true); + expect(convertYesNo("YES")).toBe(true); + expect(convertYesNo("NO")).toBe(false); + expect(convertYesNo("no")).toBe(false); + expect(convertYesNo("Apple")).toBe(null); + expect(convertYesNo("Bananas")).toBe(null); + expect(convertYesNo("Nope")).toBe(null); + expect(convertYesNo("Yesterday")).toBe(null); + expect(convertYesNo("Maybe")).toBe(null); + }); +}); diff --git a/src/functions.ts b/src/functions.ts new file mode 100644 index 0000000000..f8e154210a --- /dev/null +++ b/src/functions.ts @@ -0,0 +1,58 @@ +/** + * Consumes a single temperature in Fahrenheit (a number) and converts to Celsius + * using this formula: + * C = (F - 32) * 5/9 + */ +export function fahrenheitToCelius(temperature: number): number { + temperature = (temperature - 32) * (5 / 9); + return temperature; +} + +/** + * Consumes three numbers and produces their sum. BUT you should only add a number + * if the number is greater than zero. + */ +export function add3(first: number, second: number, third: number): number { + let sum = 0; + if (first > 0) { + sum += first; + } + if (second > 0) { + sum += second; + } + if (third > 0) { + sum += third; + } + return sum; +} + +/** + * Consumes a string and produces the same string in UPPERCASE and with an exclamation + * mark added to the end. + */ +export function shout(message: string): string { + return message.toUpperCase() + "!"; +} + +/** + * Consumes a string (a message) and returns a boolean if the string ends in a question + * mark. Do not use an `if` statement in solving this question. + */ +export function isQuestion(message: string): boolean { + return message.endsWith("?"); +} + +/** + * Consumes a word (a string) and returns either `true`, `false`, or `null`. If the string + * is "yes" (upper or lower case), then return `true`. If the string is "no" (again, either + * upper or lower case), then return `false`. Otherwise, return `null`. + */ +export function convertYesNo(word: string): boolean | null { + if (word.toLowerCase() === "yes") { + return true; + } else if (word.toLowerCase() === "no") { + return false; + } else { + return null; + } +} 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..a39431565e --- /dev/null +++ b/src/interfaces/question.ts @@ -0,0 +1,21 @@ +/** QuestionType influences how a question is asked and what kinds of answers are possible */ +export type QuestionType = "multiple_choice_question" | "short_answer_question"; + +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..23ce72fbb5 --- /dev/null +++ b/src/nested.test.ts @@ -0,0 +1,1246 @@ +import { Question } from "./interfaces/question"; +import { + getPublishedQuestions, + getNonEmptyQuestions, + findQuestion, + removeQuestion, + getNames, + sumPoints, + sumPublishedPoints, + toCSV, + makeAnswers, + publishAll, + sameType, + addNewQuestion, + renameQuestionById, + changeQuestionTypeById, + editOption, + duplicateQuestionInArray, +} 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("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("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("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("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("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("Testing the sumPoints function", () => { + expect(sumPoints(BLANK_QUESTIONS)).toEqual(3); + expect(sumPoints(SIMPLE_QUESTIONS)).toEqual(5); + expect(sumPoints(TRIVIA_QUESTIONS)).toEqual(20); + expect(sumPoints(EMPTY_QUESTIONS)).toEqual(25); + expect(sumPoints(SIMPLE_QUESTIONS_2)).toEqual(300); + }); + + test("Testing the sumPublishedPoints function", () => { + expect(sumPublishedPoints(BLANK_QUESTIONS)).toEqual(0); + expect(sumPublishedPoints(SIMPLE_QUESTIONS)).toEqual(2); + expect(sumPublishedPoints(TRIVIA_QUESTIONS)).toEqual(0); + expect(sumPublishedPoints(EMPTY_QUESTIONS)).toEqual(20); + expect(sumPublishedPoints(SIMPLE_QUESTIONS_2)).toEqual(300); + }); + + test("Testing the toCSV function", () => { + expect(toCSV(BLANK_QUESTIONS)).toEqual(`id,name,options,points,published +1,Question 1,0,1,false +47,My New Question,0,1,false +2,Question 2,0,1,false`); + expect(toCSV(SIMPLE_QUESTIONS)) + .toEqual(`id,name,options,points,published +1,Addition,0,1,true +2,Letters,0,1,false +5,Colors,3,1,true +9,Shapes,3,2,false`); + expect(toCSV(TRIVIA_QUESTIONS)) + .toEqual(`id,name,options,points,published +1,Mascot,3,7,false +2,Motto,3,3,false +3,Goats,3,10,false`); + expect(toCSV(EMPTY_QUESTIONS)).toEqual(`id,name,options,points,published +1,Empty 1,3,5,true +2,Empty 2,6,5,true +3,Empty 3,0,5,true +4,Empty 4,0,5,true +5,Empty 5 (Actual),0,5,false`); + expect(toCSV(SIMPLE_QUESTIONS_2)) + .toEqual(`id,name,options,points,published +478,Students,0,53,true +1937,Importance,0,47,true +479,Sentience,0,40,true +777,Danger,0,60,true +1937,Listening,0,100,true`); + }); + + test("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("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("Testing the sameType function", () => { + expect(sameType([])).toEqual(true); + expect(sameType(BLANK_QUESTIONS)).toEqual(false); + expect(sameType(SIMPLE_QUESTIONS)).toEqual(false); + expect(sameType(TRIVIA_QUESTIONS)).toEqual(true); + expect(sameType(EMPTY_QUESTIONS)).toEqual(false); + expect(sameType(SIMPLE_QUESTIONS_2)).toEqual(true); + }); + + test("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("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("Test the changeQuestionTypeById function", () => { + expect( + changeQuestionTypeById( + BLANK_QUESTIONS, + 1, + "multiple_choice_question", + ), + ).toEqual(BLANK_QUESTIONS); + expect( + changeQuestionTypeById(BLANK_QUESTIONS, 1, "short_answer_question"), + ).toEqual([ + { + id: 1, + name: "Question 1", + body: "", + type: "short_answer_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( + changeQuestionTypeById( + BLANK_QUESTIONS, + 47, + "short_answer_question", + ), + ).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: "short_answer_question", + options: [], + expected: "", + points: 1, + published: false, + }, + { + id: 2, + name: "Question 2", + body: "", + type: "short_answer_question", + options: [], + expected: "", + points: 1, + published: false, + }, + ]); + expect( + changeQuestionTypeById( + TRIVIA_QUESTIONS, + 3, + "short_answer_question", + ), + ).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: 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: "short_answer_question", + options: [], + expected: "Two", + points: 10, + published: false, + }, + ]); + }); + + test("Testing the addEditQuestionOption 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, + }, + ]); + }); + + test("Testing the duplicateQuestionInArray function", () => { + expect(duplicateQuestionInArray(BLANK_QUESTIONS, 1, 27)).toEqual([ + { + id: 1, + name: "Question 1", + body: "", + type: "multiple_choice_question", + options: [], + expected: "", + points: 1, + published: false, + }, + { + id: 27, + name: "Copy of 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, + }, + ]); + expect(duplicateQuestionInArray(BLANK_QUESTIONS, 47, 19)).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, + }, + { + id: 19, + name: "Copy of 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(duplicateQuestionInArray(TRIVIA_QUESTIONS, 3, 111)).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: 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, + }, + { + id: 111, + name: "Copy of 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, + }, + ]); + }); + + 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..e451149db8 --- /dev/null +++ b/src/nested.ts @@ -0,0 +1,329 @@ +import { Answer } from "./interfaces/answer"; +import { Question, QuestionType } from "./interfaces/question"; +import { duplicateQuestion, makeBlankQuestion } from "./objects"; + +/** + * Consumes an array of questions and returns a new array with only the questions + * that are `published`. + */ +export function getPublishedQuestions(questions: Question[]): Question[] { + let newQuestions: Question[] = questions.filter( + (question: Question): boolean => question.published, + ); + return newQuestions; +} + +/** + * 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[] { + let newQuestions: Question[] = questions.filter( + (question: Question): boolean => + !( + question.body === "" && + question.expected === "" && + question.options.length === 0 + ), + ); + return newQuestions; +} + +/*** + * 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 { + if (questions.some((n: Question) => n.id === id)) { + let location: number = questions.findIndex( + (question: Question): boolean => question.id === id, + ); + return questions[location]; + } else { + return null; + } +} + +/** + * Consumes an array of questions and returns a new array that does not contain the question + * with the given `id`. + */ +export function removeQuestion(questions: Question[], id: number): Question[] { + let newQuestions: Question[] = questions.filter( + (question: Question): boolean => question.id != id, + ); + return newQuestions; +} + +/*** + * Consumes an array of questions and returns a new array containing just the names of the + * questions, as an array. + */ +export function getNames(questions: Question[]): string[] { + let questionNames: string[] = questions.map( + (question: Question): string => question.name, + ); + return questionNames; +} + +/*** + * Consumes an array of questions and returns the sum total of all their points added together. + */ +export function sumPoints(questions: Question[]): number { + let sum: number = questions.reduce( + (num, question) => num + question.points, + 0, + ); + return sum; +} + +/*** + * Consumes an array of questions and returns the sum total of the PUBLISHED questions. + */ +export function sumPublishedPoints(questions: Question[]): number { + let newQuestions: Question[] = questions.filter( + (question: Question): boolean => question.published, + ); + let sum: number = newQuestions.reduce( + (num, question) => num + question.points, + 0, + ); + return sum; +} + +/*** + * Consumes an array of questions, and produces a Comma-Separated Value (CSV) string representation. + * A CSV is a type of file frequently used to share tabular data; we will use a single string + * to represent the entire file. The first line of the file is the headers "id", "name", "options", + * "points", and "published". The following line contains the value for each question, separated by + * commas. For the `options` field, use the NUMBER of options. + * + * Here is an example of what this will look like (do not include the border). + *` +id,name,options,points,published +1,Addition,0,1,true +2,Letters,0,1,false +5,Colors,3,1,true +9,Shapes,3,2,false +` * + * Check the unit tests for more examples! + */ +export function toCSV(questions: Question[]): string { + let bigList: string = questions + .reduce( + (list, question) => + list + + question.id.toString() + + "," + + question.name + + "," + + question.options.length.toString() + + "," + + question.points.toString() + + "," + + question.published + + "\n", + "id,name,options,points,published\n", + ) + .trim(); + return bigList; +} + +/** + * 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[] { + let answerList: Answer[] = questions.map( + (question: Question): Answer => ({ + questionId: question.id, + text: "", + submitted: false, + correct: false, + }), + ); + return answerList; +} + +/*** + * Consumes an array of Questions and produces a new array of questions, where + * each question is now published, regardless of its previous published status. + */ +export function publishAll(questions: Question[]): Question[] { + let questionList: Question[] = questions.map( + (question: Question): Question => ({ ...question, published: true }), + ); + return questionList; +} + +/*** + * Consumes an array of Questions and produces whether or not all the questions + * are the same type. They can be any type, as long as they are all the SAME type. + */ +export function sameType(questions: Question[]): boolean { + let questionTypes: string[] = questions.map( + (question: Question): string => question.type, + ); + if (questionTypes[0] === "multiple_choice_question") { + if ( + questionTypes.some( + (question: string): boolean => + question === "short_answer_question", + ) + ) { + return false; + } else { + return true; + } + } else { + if ( + questionTypes.some( + (question: string): boolean => + question === "multiple_choice_question", + ) + ) + return false; + else { + return 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. + */ +export function addNewQuestion( + questions: Question[], + id: number, + name: string, + type: QuestionType, +): Question[] { + let dummyQuestions: Question[] = questions.map( + (question: Question): Question => ({ + ...question, + options: [...question.options], + }), + ); + dummyQuestions.push(makeBlankQuestion(id, name, type)); + return dummyQuestions; +} + +/*** + * 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`. + */ +export function renameQuestionById( + questions: Question[], + targetId: number, + newName: string, +): Question[] { + let newQuestion: Question[] = questions.map( + (question: Question): Question => + question.id === targetId ? + { ...question, options: [...question.options], name: newName } + : { ...question, options: [...question.options] }, + ); + return newQuestion; +} + +/*** + * 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 `type` should now be the `newQuestionType` + * AND if the `newQuestionType` is no longer "multiple_choice_question" than the `options` + * must be set to an empty list. + */ +export function changeQuestionTypeById( + questions: Question[], + targetId: number, + newQuestionType: QuestionType, +): Question[] { + let newList: Question[] = questions.map( + (question: Question): Question => + question.id === targetId ? + { + ...question, + options: [...question.options], + type: newQuestionType, + } + : { ...question, options: [...question.options] }, + ); + let skimOptionsList: Question[] = newList.map( + (question: Question): Question => + question.type === "short_answer_question" ? + { ...question, options: [] } + : { ...question, options: [...question.options] }, + ); + return skimOptionsList; +} + +/** + * 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. + */ +export function editOption( + questions: Question[], + targetId: number, + targetOptionIndex: number, + newOption: string, +): Question[] { + if (targetOptionIndex === -1) { + let newList: Question[] = questions.map( + (question: Question): Question => + question.id === targetId ? + { ...question, options: [...question.options, newOption] } + : { ...question, options: [...question.options] }, + ); + return newList; + } else { + let newList: Question[] = questions.map( + (question: Question): Question => ({ + ...question, + options: [...question.options], + }), + ); + let temp: number = questions.findIndex( + (question: Question): boolean => question.id === targetId, + ); + newList[temp].options[targetOptionIndex] = newOption; + return newList; + } +} + +/*** + * Consumes an array of questions, and produces a new array based on the original array. + * The only difference is that the question with id `targetId` should now be duplicated, with + * the duplicate inserted directly after the original question. Use the `duplicateQuestion` + * function you defined previously; the `newId` is the parameter to use for the duplicate's ID. + */ +export function duplicateQuestionInArray( + questions: Question[], + targetId: number, + newId: number, +): Question[] { + let newList: Question[] = questions.map( + (question: Question): Question => ({ + ...question, + options: [...question.options], + }), + ); + let temp: number = newList.findIndex( + (question: Question): boolean => question.id === targetId, + ); + newList.splice(temp + 1, 0, duplicateQuestion(newId, newList[temp])); + return newList; +} diff --git a/src/objects.test.ts b/src/objects.test.ts new file mode 100644 index 0000000000..a9c76a334e --- /dev/null +++ b/src/objects.test.ts @@ -0,0 +1,295 @@ +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("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("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("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("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("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("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("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("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("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("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..6f2e9dbe1f --- /dev/null +++ b/src/objects.ts @@ -0,0 +1,185 @@ +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 { + let newQuestion: Question = { + id: id, + name: name, + type: type, + body: "", + expected: "", + options: [], + points: 1, + published: false, + }; + return newQuestion; +} + +/** + * Consumes a question and a potential `answer`, and returns whether or not + * the `answer` is correct. You should check that the `answer` is equal to + * the `expected`, ignoring capitalization and trimming any whitespace. + * + * HINT: Look up the `trim` and `toLowerCase` functions. + */ +export function isCorrect(question: Question, answer: string): boolean { + let realAnswer: string = question.expected.trim(); + realAnswer = realAnswer.toLowerCase(); + let givenAnswer: string = answer.trim(); + givenAnswer = givenAnswer.toLowerCase(); + if (realAnswer === givenAnswer) { + return true; + } else { + return false; + } +} + +/** + * Consumes a question and a potential `answer`, and returns whether or not + * the `answer` is valid (but not necessarily correct). For a `short_answer_question`, + * any answer is valid. But for a `multiple_choice_question`, the `answer` must + * be exactly one of the options. + */ +export function isValid(question: Question, answer: string): boolean { + if (question.type === "multiple_choice_question") { + if (question.options.some((n: string) => n === answer)) { + return true; + } else { + return false; + } + } else { + return true; + } +} + +/** + * Consumes a question and produces a string representation combining the + * `id` and first 10 characters of the `name`. The two strings should be + * separated by ": ". So for example, the question with id 9 and the + * name "My First Question" would become "9: My First Q". + */ +export function toShortForm(question: Question): string { + let first10char: string = question.name.slice(0, 10); + let idChar: string = question.id.toString(); + return idChar + ": " + first10char; +} + +/** + * Consumes a question and returns a formatted string representation as follows: + * - The first line should be a hash sign, a space, and then the `name` + * - The second line should be the `body` + * - If the question is a `multiple_choice_question`, then the following lines + * need to show each option on its line, preceded by a dash and space. + * + * The example below might help, but don't include the border! + * ----------Example------------- + * |# Name | + * |The body goes here! | + * |- Option 1 | + * |- Option 2 | + * |- Option 3 | + * ------------------------------ + * Check the unit tests for more examples of what this looks like! + */ +export function toMarkdown(question: Question): string { + let name: string = question.name; + let body: string = question.body; + if (question.type === "multiple_choice_question") { + let options: string = question.options + .reduce((addedString, currentValue) => { + return addedString + "- " + currentValue + "\n"; + }, "") + .trim(); + return "# " + name + "\n" + body + "\n" + options; + } else { + return "# " + name + "\n" + body; + } +} + +/** + * Return a new version of the given question, except the name should now be + * `newName`. + */ +export function renameQuestion(question: Question, newName: string): Question { + let newQuestion: Question = { ...question, name: newName }; + return newQuestion; +} + +/** + * Return a new version of the given question, except the `published` field + * should be inverted. If the question was not published, now it should be + * published; if it was published, now it should be not published. + */ +export function publishQuestion(question: Question): Question { + let newQuestion: Question = { ...question, published: false }; + if (question.published) { + newQuestion.published = false; + } else { + newQuestion.published = true; + } + return newQuestion; +} + +/** + * Create a new question based on the old question, copying over its `body`, `type`, + * `options`, `expected`, and `points` without changes. The `name` should be copied + * over as "Copy of ORIGINAL NAME" (e.g., so "Question 1" would become "Copy of Question 1"). + * The `published` field should be reset to false. + */ +export function duplicateQuestion(id: number, oldQuestion: Question): Question { + let newQuestion: Question = { ...oldQuestion }; + newQuestion.published = false; + newQuestion.name = "Copy of " + oldQuestion.name; + newQuestion.id = id; + return newQuestion; +} + +/** + * Return a new version of the given question, with the `newOption` added to + * the list of existing `options`. Remember that the new Question MUST have + * its own separate copy of the `options` list, rather than the same reference + * to the original question's list! + * Check out the subsection about "Nested Fields" for more information. + */ +export function addOption(question: Question, newOption: string): Question { + let newQuestion: Question = { + ...question, + options: [...question.options, newOption], + }; + return newQuestion; +} + +/** + * Consumes an id, name, and two questions, and produces a new question. + * The new question will use the `body`, `type`, `options`, and `expected` of the + * `contentQuestion`. The second question will provide the `points`. + * The `published` status should be set to false. + * Notice that the second Question is provided as just an object with a `points` + * field; but the function call would be the same as if it were a `Question` type! + */ +export function mergeQuestion( + id: number, + name: string, + contentQuestion: Question, + { points }: { points: number }, +): Question { + let newQuestion: Question = { + id: id, + name: name, + body: contentQuestion.body, + type: contentQuestion.type, + options: [...contentQuestion.options], + expected: contentQuestion.expected, + points: points, + published: false, + }; + return newQuestion; +} diff --git a/src/text.test.tsx b/src/text.test.tsx new file mode 100644 index 0000000000..f99a063e76 --- /dev/null +++ b/src/text.test.tsx @@ -0,0 +1,9 @@ +import React from "react"; +import { render, screen } from "@testing-library/react"; +import App from "./App"; + +test("renders the text 'Hello World' somewhere", () => { + render(); + const texts = screen.getAllByText(/Hello World/); + expect(texts.length).toBeGreaterThanOrEqual(1); +});