diff --git a/.eslintrc.json b/.eslintrc.json
new file mode 100644
index 0000000000..36f0947989
--- /dev/null
+++ b/.eslintrc.json
@@ -0,0 +1,50 @@
+{
+ "env": {
+ "browser": true,
+ "es2021": true,
+ "node": true
+ },
+ "settings": {
+ "react": {
+ "version": "detect"
+ }
+ },
+ "extends": [
+ "eslint:recommended",
+ "plugin:react/recommended",
+ "plugin:@typescript-eslint/eslint-recommended",
+ "plugin:@typescript-eslint/recommended",
+ "prettier"
+ ],
+ "parser": "@typescript-eslint/parser",
+ "parserOptions": {
+ "ecmaFeatures": {
+ "jsx": true
+ },
+ "ecmaVersion": 12,
+ "sourceType": "module"
+ },
+ "plugins": ["react", "@typescript-eslint", "prettier"],
+ "rules": {
+ "prettier/prettier": [
+ "error",
+ {
+ "endOfLine": "auto"
+ }
+ ],
+ "no-extra-parens": [
+ "warn",
+ "all",
+ {
+ "nestedBinaryExpressions": false,
+ "returnAssign": false,
+ "enforceForArrowConditionals": false,
+ "ignoreJSX": "all"
+ }
+ ],
+ "brace-style": ["error", "1tbs"],
+ "indent": ["error", 4],
+ "quotes": ["error", "double"],
+ "semi": ["error", "always"]
+ }
+}
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-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..10405d79e4 100644
--- a/src/App.tsx
+++ b/src/App.tsx
@@ -1,5 +1,11 @@
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";
function App(): React.JSX.Element {
return (
@@ -7,10 +13,18 @@ function App(): React.JSX.Element {
UD CISC275 with React Hooks and TypeScript
-
- Edit src/App.tsx and save. This page will
- automatically reload.
-
+
+
+
+
+
+
+
+
+
+
+
+
);
}
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..0eb59b4c6c
--- /dev/null
+++ b/src/components/ChangeType.tsx
@@ -0,0 +1,24 @@
+import React, { useState } from "react";
+import { Button } from "react-bootstrap";
+import { QuestionType } from "../interfaces/question";
+
+export function ChangeType(): React.JSX.Element {
+ const [type, setType] = useState("short_answer_question");
+
+ function changeType(): void {
+ if (type === "short_answer_question") {
+ setType("multiple_choice_question");
+ } else {
+ setType("short_answer_question");
+ }
+ }
+
+ return (
+
+
+
+ {type === "short_answer_question" &&
Short Answer
}
+ {type === "multiple_choice_question" &&
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..053758ca4f
--- /dev/null
+++ b/src/components/CycleHoliday.tsx
@@ -0,0 +1,32 @@
+import React, { useState } from "react";
+import { Button } from "react-bootstrap";
+
+type Holiday = { name: string; year: number };
+
+export function CycleHoliday(): React.JSX.Element {
+ const holidays: Holiday[] = [
+ { name: "April Fools", year: 2020 },
+ { name: "Boxing Day", year: 2021 },
+ { name: "Christmas", year: 2022 },
+ { name: "Diwali", year: 2023 },
+ { name: "Easter", year: 2024 },
+ ];
+
+ const [index, setIndex] = useState(0);
+
+ function cycleAlphabet(): void {
+ setIndex((index + 1) % holidays.length);
+ }
+
+ function cycleYear(): void {
+ setIndex((index + 1) % holidays.length);
+ }
+
+ return (
+
+
Holiday: {holidays[index].name}
+
+
+
+ );
+}
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..0bfaaff4fb
--- /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 toggleAnswer(): 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..a07c565e7a
--- /dev/null
+++ b/src/components/StartAttempt.tsx
@@ -0,0 +1,50 @@
+import React, { useState } from "react";
+import { Button } from "react-bootstrap";
+
+export function StartAttempt(): React.JSX.Element {
+ const originalAttempts = 3;
+
+ const [attempts, setAttempts] = useState(originalAttempts);
+ const [quizInProgress, setQuizInProgress] = useState(false);
+
+ function startQuiz(): void {
+ if (attempts > 0) {
+ setAttempts(attempts - 1);
+ setQuizInProgress(true);
+ }
+ }
+
+ function stopQuiz(): void {
+ setQuizInProgress(false);
+ }
+
+ function mulligan(): void {
+ if (quizInProgress) {
+ setAttempts(originalAttempts);
+ setQuizInProgress(false);
+ } else {
+ setAttempts(attempts + 1);
+ }
+ }
+
+ 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..52cd5757c2
--- /dev/null
+++ b/src/components/TwoDice.tsx
@@ -0,0 +1,32 @@
+import React, { useState } from "react";
+import { Button } from "react-bootstrap";
+
+export function d6(): number {
+ return 1 + Math.floor(Math.random() * 6);
+}
+
+export function TwoDice(): React.JSX.Element {
+ const [left, setLeft] = useState(1);
+ const [right, setRight] = useState(2);
+
+ function rollLeft(): void {
+ setLeft(d6());
+ }
+
+ function rollRight(): void {
+ setRight(d6());
+ }
+
+ return (
+
+
{left}
+
{right}
+
+
+
+
+ {left === right && left === 1 &&
Lose
}
+ {left === right && left !== 1 &&
Win
}
+
+ );
+}
diff --git a/src/data/questions.json b/src/data/questions.json
new file mode 100644
index 0000000000..0411f30afe
--- /dev/null
+++ b/src/data/questions.json
@@ -0,0 +1,220 @@
+{
+ "BLANK_QUESTIONS": [
+ {
+ "id": 1,
+ "name": "Question 1",
+ "body": "",
+ "type": "multiple_choice_question",
+ "options": [],
+ "expected": "",
+ "points": 1,
+ "published": false
+ },
+ {
+ "id": 47,
+ "name": "My New Question",
+ "body": "",
+ "type": "multiple_choice_question",
+ "options": [],
+ "expected": "",
+ "points": 1,
+ "published": false
+ },
+ {
+ "id": 2,
+ "name": "Question 2",
+ "body": "",
+ "type": "short_answer_question",
+ "options": [],
+ "expected": "",
+ "points": 1,
+ "published": false
+ }
+ ],
+ "SIMPLE_QUESTIONS": [
+ {
+ "id": 1,
+ "name": "Addition",
+ "body": "What is 2+2?",
+ "type": "short_answer_question",
+ "options": [],
+ "expected": "4",
+ "points": 1,
+ "published": true
+ },
+ {
+ "id": 2,
+ "name": "Letters",
+ "body": "What is the last letter of the English alphabet?",
+ "type": "short_answer_question",
+ "options": [],
+ "expected": "Z",
+ "points": 1,
+ "published": false
+ },
+ {
+ "id": 5,
+ "name": "Colors",
+ "body": "Which of these is a color?",
+ "type": "multiple_choice_question",
+ "options": ["red", "apple", "firetruck"],
+ "expected": "red",
+ "points": 1,
+ "published": true
+ },
+ {
+ "id": 9,
+ "name": "Shapes",
+ "body": "What shape can you make with one line?",
+ "type": "multiple_choice_question",
+ "options": ["square", "triangle", "circle"],
+ "expected": "circle",
+ "points": 2,
+ "published": false
+ }
+ ],
+ "TRIVIA_QUESTIONS": [
+ {
+ "id": 1,
+ "name": "Mascot",
+ "body": "What is the name of the UD Mascot?",
+ "type": "multiple_choice_question",
+ "options": ["Bluey", "YoUDee", "Charles the Wonder Dog"],
+ "expected": "YoUDee",
+ "points": 7,
+ "published": false
+ },
+ {
+ "id": 2,
+ "name": "Motto",
+ "body": "What is the University of Delaware's motto?",
+ "type": "multiple_choice_question",
+ "options": [
+ "Knowledge is the light of the mind",
+ "Just U Do it",
+ "Nothing, what's the motto with you?"
+ ],
+ "expected": "Knowledge is the light of the mind",
+ "points": 3,
+ "published": false
+ },
+ {
+ "id": 3,
+ "name": "Goats",
+ "body": "How many goats are there usually on the Green?",
+ "type": "multiple_choice_question",
+ "options": [
+ "Zero, why would there be goats on the green?",
+ "18420",
+ "Two"
+ ],
+ "expected": "Two",
+ "points": 10,
+ "published": false
+ }
+ ],
+ "EMPTY_QUESTIONS": [
+ {
+ "id": 1,
+ "name": "Empty 1",
+ "body": "This question is not empty, right?",
+ "type": "multiple_choice_question",
+ "options": ["correct", "it is", "not"],
+ "expected": "correct",
+ "points": 5,
+ "published": true
+ },
+ {
+ "id": 2,
+ "name": "Empty 2",
+ "body": "",
+ "type": "multiple_choice_question",
+ "options": ["this", "one", "is", "not", "empty", "either"],
+ "expected": "one",
+ "points": 5,
+ "published": true
+ },
+ {
+ "id": 3,
+ "name": "Empty 3",
+ "body": "This questions is not empty either!",
+ "type": "short_answer_question",
+ "options": [],
+ "expected": "",
+ "points": 5,
+ "published": true
+ },
+ {
+ "id": 4,
+ "name": "Empty 4",
+ "body": "",
+ "type": "short_answer_question",
+ "options": [],
+ "expected": "Even this one is not empty",
+ "points": 5,
+ "published": true
+ },
+ {
+ "id": 5,
+ "name": "Empty 5 (Actual)",
+ "body": "",
+ "type": "short_answer_question",
+ "options": [],
+ "expected": "",
+ "points": 5,
+ "published": false
+ }
+ ],
+ "SIMPLE_QUESTIONS_2": [
+ {
+ "id": 478,
+ "name": "Students",
+ "body": "How many students are taking CISC275 this semester?",
+ "type": "short_answer_question",
+ "options": [],
+ "expected": "90",
+ "points": 53,
+ "published": true
+ },
+ {
+ "id": 1937,
+ "name": "Importance",
+ "body": "On a scale of 1 to 10, how important is this quiz for them?",
+ "type": "short_answer_question",
+ "options": [],
+ "expected": "10",
+ "points": 47,
+ "published": true
+ },
+ {
+ "id": 479,
+ "name": "Sentience",
+ "body": "Is it technically possible for this quiz to become sentient?",
+ "type": "short_answer_question",
+ "options": [],
+ "expected": "Yes",
+ "points": 40,
+ "published": true
+ },
+ {
+ "id": 777,
+ "name": "Danger",
+ "body": "If this quiz became sentient, would it pose a danger to others?",
+ "type": "short_answer_question",
+ "options": [],
+ "expected": "Yes",
+ "points": 60,
+ "published": true
+ },
+ {
+ "id": 1937,
+ "name": "Listening",
+ "body": "Is this quiz listening to us right now?",
+ "type": "short_answer_question",
+ "options": [],
+ "expected": "Yes",
+ "points": 100,
+ "published": true
+ }
+ ]
+}
diff --git a/src/interfaces/answer.ts b/src/interfaces/answer.ts
new file mode 100644
index 0000000000..743ee8dff9
--- /dev/null
+++ b/src/interfaces/answer.ts
@@ -0,0 +1,13 @@
+/***
+ * A representation of a students' answer in a quizzing game
+ */
+export interface Answer {
+ /** The ID of the question being answered. */
+ questionId: number;
+ /** The text that the student entered for their answer. */
+ text: string;
+ /** Whether or not the student has submitted this answer. */
+ submitted: boolean;
+ /** Whether or not the students' answer matched the expected. */
+ correct: boolean;
+}
diff --git a/src/interfaces/question.ts b/src/interfaces/question.ts
new file mode 100644
index 0000000000..5def48f2f7
--- /dev/null
+++ b/src/interfaces/question.ts
@@ -0,0 +1,22 @@
+/** QuestionType influences how a question is asked and what kinds of answers are possible */
+export type QuestionType = "multiple_choice_question" | "short_answer_question";
+
+/** A representation of a Question in a quizzing application */
+export interface Question {
+ /** A unique identifier for the question */
+ id: number;
+ /** The human-friendly title of the question */
+ name: string;
+ /** The instructions and content of the Question */
+ body: string;
+ /** The kind of Question; influences how the user answers and what options are displayed */
+ type: QuestionType;
+ /** The possible answers for a Question (for Multiple Choice questions) */
+ options: string[];
+ /** The actually correct answer expected */
+ expected: string;
+ /** How many points this question is worth, roughly indicating its importance and difficulty */
+ points: number;
+ /** Whether or not this question is ready to display to students */
+ published: boolean;
+}
diff --git a/src/nested.test.ts b/src/nested.test.ts
new file mode 100644
index 0000000000..7f52bfdf94
--- /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("(3 pts) Testing the getPublishedQuestions function", () => {
+ expect(getPublishedQuestions(BLANK_QUESTIONS)).toEqual([]);
+ expect(getPublishedQuestions(SIMPLE_QUESTIONS)).toEqual([
+ {
+ id: 1,
+ name: "Addition",
+ body: "What is 2+2?",
+ type: "short_answer_question",
+ options: [],
+ expected: "4",
+ points: 1,
+ published: true,
+ },
+ {
+ id: 5,
+ name: "Colors",
+ body: "Which of these is a color?",
+ type: "multiple_choice_question",
+ options: ["red", "apple", "firetruck"],
+ expected: "red",
+ points: 1,
+ published: true,
+ },
+ ]);
+ expect(getPublishedQuestions(TRIVIA_QUESTIONS)).toEqual([]);
+ expect(getPublishedQuestions(SIMPLE_QUESTIONS_2)).toEqual(
+ BACKUP_SIMPLE_QUESTIONS_2,
+ );
+ expect(getPublishedQuestions(EMPTY_QUESTIONS)).toEqual([
+ {
+ id: 1,
+ name: "Empty 1",
+ body: "This question is not empty, right?",
+ type: "multiple_choice_question",
+ options: ["correct", "it is", "not"],
+ expected: "correct",
+ points: 5,
+ published: true,
+ },
+ {
+ id: 2,
+ name: "Empty 2",
+ body: "",
+ type: "multiple_choice_question",
+ options: ["this", "one", "is", "not", "empty", "either"],
+ expected: "one",
+ points: 5,
+ published: true,
+ },
+ {
+ id: 3,
+ name: "Empty 3",
+ body: "This questions is not empty either!",
+ type: "short_answer_question",
+ options: [],
+ expected: "",
+ points: 5,
+ published: true,
+ },
+ {
+ id: 4,
+ name: "Empty 4",
+ body: "",
+ type: "short_answer_question",
+ options: [],
+ expected: "Even this one is not empty",
+ points: 5,
+ published: true,
+ },
+ ]);
+ });
+
+ test("(3 pts) Testing the getNonEmptyQuestions functions", () => {
+ expect(getNonEmptyQuestions(BLANK_QUESTIONS)).toEqual([]);
+ expect(getNonEmptyQuestions(SIMPLE_QUESTIONS)).toEqual(
+ BACKUP_SIMPLE_QUESTIONS,
+ );
+ expect(getNonEmptyQuestions(TRIVIA_QUESTIONS)).toEqual(
+ BACKUP_TRIVIA_QUESTIONS,
+ );
+ expect(getNonEmptyQuestions(SIMPLE_QUESTIONS_2)).toEqual(
+ BACKUP_SIMPLE_QUESTIONS_2,
+ );
+ expect(getNonEmptyQuestions(EMPTY_QUESTIONS)).toEqual([
+ {
+ id: 1,
+ name: "Empty 1",
+ body: "This question is not empty, right?",
+ type: "multiple_choice_question",
+ options: ["correct", "it is", "not"],
+ expected: "correct",
+ points: 5,
+ published: true,
+ },
+ {
+ id: 2,
+ name: "Empty 2",
+ body: "",
+ type: "multiple_choice_question",
+ options: ["this", "one", "is", "not", "empty", "either"],
+ expected: "one",
+ points: 5,
+ published: true,
+ },
+ {
+ id: 3,
+ name: "Empty 3",
+ body: "This questions is not empty either!",
+ type: "short_answer_question",
+ options: [],
+ expected: "",
+ points: 5,
+ published: true,
+ },
+ {
+ id: 4,
+ name: "Empty 4",
+ body: "",
+ type: "short_answer_question",
+ options: [],
+ expected: "Even this one is not empty",
+ points: 5,
+ published: true,
+ },
+ ]);
+ });
+
+ test("(3 pts) Testing the findQuestion function", () => {
+ expect(findQuestion(BLANK_QUESTIONS, 1)).toEqual(BLANK_QUESTIONS[0]);
+ expect(findQuestion(BLANK_QUESTIONS, 47)).toEqual(BLANK_QUESTIONS[1]);
+ expect(findQuestion(BLANK_QUESTIONS, 2)).toEqual(BLANK_QUESTIONS[2]);
+ expect(findQuestion(BLANK_QUESTIONS, 3)).toEqual(null);
+ expect(findQuestion(SIMPLE_QUESTIONS, 1)).toEqual(SIMPLE_QUESTIONS[0]);
+ expect(findQuestion(SIMPLE_QUESTIONS, 2)).toEqual(SIMPLE_QUESTIONS[1]);
+ expect(findQuestion(SIMPLE_QUESTIONS, 5)).toEqual(SIMPLE_QUESTIONS[2]);
+ expect(findQuestion(SIMPLE_QUESTIONS, 9)).toEqual(SIMPLE_QUESTIONS[3]);
+ expect(findQuestion(SIMPLE_QUESTIONS, 6)).toEqual(null);
+ expect(findQuestion(SIMPLE_QUESTIONS_2, 478)).toEqual(
+ SIMPLE_QUESTIONS_2[0],
+ );
+ expect(findQuestion([], 0)).toEqual(null);
+ });
+
+ test("(3 pts) Testing the removeQuestion", () => {
+ expect(removeQuestion(BLANK_QUESTIONS, 1)).toEqual([
+ {
+ id: 47,
+ name: "My New Question",
+ body: "",
+ type: "multiple_choice_question",
+ options: [],
+ expected: "",
+ points: 1,
+ published: false,
+ },
+ {
+ id: 2,
+ name: "Question 2",
+ body: "",
+ type: "short_answer_question",
+ options: [],
+ expected: "",
+ points: 1,
+ published: false,
+ },
+ ]);
+ expect(removeQuestion(BLANK_QUESTIONS, 47)).toEqual([
+ {
+ id: 1,
+ name: "Question 1",
+ body: "",
+ type: "multiple_choice_question",
+ options: [],
+ expected: "",
+ points: 1,
+ published: false,
+ },
+ {
+ id: 2,
+ name: "Question 2",
+ body: "",
+ type: "short_answer_question",
+ options: [],
+ expected: "",
+ points: 1,
+ published: false,
+ },
+ ]);
+ expect(removeQuestion(BLANK_QUESTIONS, 2)).toEqual([
+ {
+ id: 1,
+ name: "Question 1",
+ body: "",
+ type: "multiple_choice_question",
+ options: [],
+ expected: "",
+ points: 1,
+ published: false,
+ },
+ {
+ id: 47,
+ name: "My New Question",
+ body: "",
+ type: "multiple_choice_question",
+ options: [],
+ expected: "",
+ points: 1,
+ published: false,
+ },
+ ]);
+ expect(removeQuestion(SIMPLE_QUESTIONS, 9)).toEqual([
+ {
+ id: 1,
+ name: "Addition",
+ body: "What is 2+2?",
+ type: "short_answer_question",
+ options: [],
+ expected: "4",
+ points: 1,
+ published: true,
+ },
+ {
+ id: 2,
+ name: "Letters",
+ body: "What is the last letter of the English alphabet?",
+ type: "short_answer_question",
+ options: [],
+ expected: "Z",
+ points: 1,
+ published: false,
+ },
+ {
+ id: 5,
+ name: "Colors",
+ body: "Which of these is a color?",
+ type: "multiple_choice_question",
+ options: ["red", "apple", "firetruck"],
+ expected: "red",
+ points: 1,
+ published: true,
+ },
+ ]);
+ expect(removeQuestion(SIMPLE_QUESTIONS, 5)).toEqual([
+ {
+ id: 1,
+ name: "Addition",
+ body: "What is 2+2?",
+ type: "short_answer_question",
+ options: [],
+ expected: "4",
+ points: 1,
+ published: true,
+ },
+ {
+ id: 2,
+ name: "Letters",
+ body: "What is the last letter of the English alphabet?",
+ type: "short_answer_question",
+ options: [],
+ expected: "Z",
+ points: 1,
+ published: false,
+ },
+ {
+ id: 9,
+ name: "Shapes",
+ body: "What shape can you make with one line?",
+ type: "multiple_choice_question",
+ options: ["square", "triangle", "circle"],
+ expected: "circle",
+ points: 2,
+ published: false,
+ },
+ ]);
+ });
+
+ test("(3 pts) Testing the getNames function", () => {
+ expect(getNames(BLANK_QUESTIONS)).toEqual([
+ "Question 1",
+ "My New Question",
+ "Question 2",
+ ]);
+ expect(getNames(SIMPLE_QUESTIONS)).toEqual([
+ "Addition",
+ "Letters",
+ "Colors",
+ "Shapes",
+ ]);
+ expect(getNames(TRIVIA_QUESTIONS)).toEqual([
+ "Mascot",
+ "Motto",
+ "Goats",
+ ]);
+ expect(getNames(SIMPLE_QUESTIONS_2)).toEqual([
+ "Students",
+ "Importance",
+ "Sentience",
+ "Danger",
+ "Listening",
+ ]);
+ expect(getNames(EMPTY_QUESTIONS)).toEqual([
+ "Empty 1",
+ "Empty 2",
+ "Empty 3",
+ "Empty 4",
+ "Empty 5 (Actual)",
+ ]);
+ });
+
+ test("(3 pts) Testing the 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("(3 pts) 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("(3 pts) 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("(3 pts) Testing the makeAnswers function", () => {
+ expect(makeAnswers(BLANK_QUESTIONS)).toEqual([
+ { questionId: 1, correct: false, text: "", submitted: false },
+ { questionId: 47, correct: false, text: "", submitted: false },
+ { questionId: 2, correct: false, text: "", submitted: false },
+ ]);
+ expect(makeAnswers(SIMPLE_QUESTIONS)).toEqual([
+ { questionId: 1, correct: false, text: "", submitted: false },
+ { questionId: 2, correct: false, text: "", submitted: false },
+ { questionId: 5, correct: false, text: "", submitted: false },
+ { questionId: 9, correct: false, text: "", submitted: false },
+ ]);
+ expect(makeAnswers(TRIVIA_QUESTIONS)).toEqual([
+ { questionId: 1, correct: false, text: "", submitted: false },
+ { questionId: 2, correct: false, text: "", submitted: false },
+ { questionId: 3, correct: false, text: "", submitted: false },
+ ]);
+ expect(makeAnswers(SIMPLE_QUESTIONS_2)).toEqual([
+ { questionId: 478, correct: false, text: "", submitted: false },
+ { questionId: 1937, correct: false, text: "", submitted: false },
+ { questionId: 479, correct: false, text: "", submitted: false },
+ { questionId: 777, correct: false, text: "", submitted: false },
+ { questionId: 1937, correct: false, text: "", submitted: false },
+ ]);
+ expect(makeAnswers(EMPTY_QUESTIONS)).toEqual([
+ { questionId: 1, correct: false, text: "", submitted: false },
+ { questionId: 2, correct: false, text: "", submitted: false },
+ { questionId: 3, correct: false, text: "", submitted: false },
+ { questionId: 4, correct: false, text: "", submitted: false },
+ { questionId: 5, correct: false, text: "", submitted: false },
+ ]);
+ });
+
+ test("(3 pts) Testing the publishAll function", () => {
+ expect(publishAll(BLANK_QUESTIONS)).toEqual([
+ {
+ id: 1,
+ name: "Question 1",
+ body: "",
+ type: "multiple_choice_question",
+ options: [],
+ expected: "",
+ points: 1,
+ published: true,
+ },
+ {
+ id: 47,
+ name: "My New Question",
+ body: "",
+ type: "multiple_choice_question",
+ options: [],
+ expected: "",
+ points: 1,
+ published: true,
+ },
+ {
+ id: 2,
+ name: "Question 2",
+ body: "",
+ type: "short_answer_question",
+ options: [],
+ expected: "",
+ points: 1,
+ published: true,
+ },
+ ]);
+ expect(publishAll(SIMPLE_QUESTIONS)).toEqual([
+ {
+ id: 1,
+ name: "Addition",
+ body: "What is 2+2?",
+ type: "short_answer_question",
+ options: [],
+ expected: "4",
+ points: 1,
+ published: true,
+ },
+ {
+ id: 2,
+ name: "Letters",
+ body: "What is the last letter of the English alphabet?",
+ type: "short_answer_question",
+ options: [],
+ expected: "Z",
+ points: 1,
+ published: true,
+ },
+ {
+ id: 5,
+ name: "Colors",
+ body: "Which of these is a color?",
+ type: "multiple_choice_question",
+ options: ["red", "apple", "firetruck"],
+ expected: "red",
+ points: 1,
+ published: true,
+ },
+ {
+ id: 9,
+ name: "Shapes",
+ body: "What shape can you make with one line?",
+ type: "multiple_choice_question",
+ options: ["square", "triangle", "circle"],
+ expected: "circle",
+ points: 2,
+ published: true,
+ },
+ ]);
+ expect(publishAll(TRIVIA_QUESTIONS)).toEqual([
+ {
+ id: 1,
+ name: "Mascot",
+ body: "What is the name of the UD Mascot?",
+ type: "multiple_choice_question",
+ options: ["Bluey", "YoUDee", "Charles the Wonder Dog"],
+ expected: "YoUDee",
+ points: 7,
+ published: true,
+ },
+ {
+ id: 2,
+ name: "Motto",
+ body: "What is the University of Delaware's motto?",
+ type: "multiple_choice_question",
+ options: [
+ "Knowledge is the light of the mind",
+ "Just U Do it",
+ "Nothing, what's the motto with you?",
+ ],
+ expected: "Knowledge is the light of the mind",
+ points: 3,
+ published: true,
+ },
+ {
+ id: 3,
+ name: "Goats",
+ body: "How many goats are there usually on the Green?",
+ type: "multiple_choice_question",
+ options: [
+ "Zero, why would there be goats on the green?",
+ "18420",
+ "Two",
+ ],
+ expected: "Two",
+ points: 10,
+ published: true,
+ },
+ ]);
+ expect(publishAll(EMPTY_QUESTIONS)).toEqual([
+ {
+ id: 1,
+ name: "Empty 1",
+ body: "This question is not empty, right?",
+ type: "multiple_choice_question",
+ options: ["correct", "it is", "not"],
+ expected: "correct",
+ points: 5,
+ published: true,
+ },
+ {
+ id: 2,
+ name: "Empty 2",
+ body: "",
+ type: "multiple_choice_question",
+ options: ["this", "one", "is", "not", "empty", "either"],
+ expected: "one",
+ points: 5,
+ published: true,
+ },
+ {
+ id: 3,
+ name: "Empty 3",
+ body: "This questions is not empty either!",
+ type: "short_answer_question",
+ options: [],
+ expected: "",
+ points: 5,
+ published: true,
+ },
+ {
+ id: 4,
+ name: "Empty 4",
+ body: "",
+ type: "short_answer_question",
+ options: [],
+ expected: "Even this one is not empty",
+ points: 5,
+ published: true,
+ },
+ {
+ id: 5,
+ name: "Empty 5 (Actual)",
+ body: "",
+ type: "short_answer_question",
+ options: [],
+ expected: "",
+ points: 5,
+ published: true,
+ },
+ ]);
+ expect(publishAll(SIMPLE_QUESTIONS_2)).toEqual(SIMPLE_QUESTIONS_2);
+ });
+
+ test("(3 pts) Testing the 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("(3 pts) Testing the addNewQuestion function", () => {
+ expect(
+ addNewQuestion([], 142, "A new question", "short_answer_question"),
+ ).toEqual([NEW_BLANK_QUESTION]);
+ expect(
+ addNewQuestion(
+ BLANK_QUESTIONS,
+ 142,
+ "A new question",
+ "short_answer_question",
+ ),
+ ).toEqual([...BLANK_QUESTIONS, NEW_BLANK_QUESTION]);
+ expect(
+ addNewQuestion(
+ TRIVIA_QUESTIONS,
+ 449,
+ "Colors",
+ "multiple_choice_question",
+ ),
+ ).toEqual([...TRIVIA_QUESTIONS, NEW_TRIVIA_QUESTION]);
+ });
+
+ test("(3 pts) Testing the renameQuestionById function", () => {
+ expect(renameQuestionById(BLANK_QUESTIONS, 1, "New Name")).toEqual([
+ {
+ id: 1,
+ name: "New Name",
+ body: "",
+ type: "multiple_choice_question",
+ options: [],
+ expected: "",
+ points: 1,
+ published: false,
+ },
+ {
+ id: 47,
+ name: "My New Question",
+ body: "",
+ type: "multiple_choice_question",
+ options: [],
+ expected: "",
+ points: 1,
+ published: false,
+ },
+ {
+ id: 2,
+ name: "Question 2",
+ body: "",
+ type: "short_answer_question",
+ options: [],
+ expected: "",
+ points: 1,
+ published: false,
+ },
+ ]);
+ expect(renameQuestionById(BLANK_QUESTIONS, 47, "Another Name")).toEqual(
+ [
+ {
+ id: 1,
+ name: "Question 1",
+ body: "",
+ type: "multiple_choice_question",
+ options: [],
+ expected: "",
+ points: 1,
+ published: false,
+ },
+ {
+ id: 47,
+ name: "Another Name",
+ body: "",
+ type: "multiple_choice_question",
+ options: [],
+ expected: "",
+ points: 1,
+ published: false,
+ },
+ {
+ id: 2,
+ name: "Question 2",
+ body: "",
+ type: "short_answer_question",
+ options: [],
+ expected: "",
+ points: 1,
+ published: false,
+ },
+ ],
+ );
+ expect(renameQuestionById(SIMPLE_QUESTIONS, 5, "Colours")).toEqual([
+ {
+ id: 1,
+ name: "Addition",
+ body: "What is 2+2?",
+ type: "short_answer_question",
+ options: [],
+ expected: "4",
+ points: 1,
+ published: true,
+ },
+ {
+ id: 2,
+ name: "Letters",
+ body: "What is the last letter of the English alphabet?",
+ type: "short_answer_question",
+ options: [],
+ expected: "Z",
+ points: 1,
+ published: false,
+ },
+ {
+ id: 5,
+ name: "Colours",
+ body: "Which of these is a color?",
+ type: "multiple_choice_question",
+ options: ["red", "apple", "firetruck"],
+ expected: "red",
+ points: 1,
+ published: true,
+ },
+ {
+ id: 9,
+ name: "Shapes",
+ body: "What shape can you make with one line?",
+ type: "multiple_choice_question",
+ options: ["square", "triangle", "circle"],
+ expected: "circle",
+ points: 2,
+ published: false,
+ },
+ ]);
+ });
+
+ test("(3 pts) 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("(3 pts) Testing the editOption function", () => {
+ expect(editOption(BLANK_QUESTIONS, 1, -1, "NEW OPTION")).toEqual([
+ {
+ id: 1,
+ name: "Question 1",
+ body: "",
+ type: "multiple_choice_question",
+ options: ["NEW OPTION"],
+ expected: "",
+ points: 1,
+ published: false,
+ },
+ {
+ id: 47,
+ name: "My New Question",
+ body: "",
+ type: "multiple_choice_question",
+ options: [],
+ expected: "",
+ points: 1,
+ published: false,
+ },
+ {
+ id: 2,
+ name: "Question 2",
+ body: "",
+ type: "short_answer_question",
+ options: [],
+ expected: "",
+ points: 1,
+ published: false,
+ },
+ ]);
+ expect(editOption(BLANK_QUESTIONS, 47, -1, "Another option")).toEqual([
+ {
+ id: 1,
+ name: "Question 1",
+ body: "",
+ type: "multiple_choice_question",
+ options: [],
+ expected: "",
+ points: 1,
+ published: false,
+ },
+ {
+ id: 47,
+ name: "My New Question",
+ body: "",
+ type: "multiple_choice_question",
+ options: ["Another option"],
+ expected: "",
+ points: 1,
+ published: false,
+ },
+ {
+ id: 2,
+ name: "Question 2",
+ body: "",
+ type: "short_answer_question",
+ options: [],
+ expected: "",
+ points: 1,
+ published: false,
+ },
+ ]);
+ expect(editOption(SIMPLE_QUESTIONS, 5, -1, "newspaper")).toEqual([
+ {
+ id: 1,
+ name: "Addition",
+ body: "What is 2+2?",
+ type: "short_answer_question",
+ options: [],
+ expected: "4",
+ points: 1,
+ published: true,
+ },
+ {
+ id: 2,
+ name: "Letters",
+ body: "What is the last letter of the English alphabet?",
+ type: "short_answer_question",
+ options: [],
+ expected: "Z",
+ points: 1,
+ published: false,
+ },
+ {
+ id: 5,
+ name: "Colors",
+ body: "Which of these is a color?",
+ type: "multiple_choice_question",
+ options: ["red", "apple", "firetruck", "newspaper"],
+ expected: "red",
+ points: 1,
+ published: true,
+ },
+ {
+ id: 9,
+ name: "Shapes",
+ body: "What shape can you make with one line?",
+ type: "multiple_choice_question",
+ options: ["square", "triangle", "circle"],
+ expected: "circle",
+ points: 2,
+ published: false,
+ },
+ ]);
+ expect(editOption(SIMPLE_QUESTIONS, 5, 0, "newspaper")).toEqual([
+ {
+ id: 1,
+ name: "Addition",
+ body: "What is 2+2?",
+ type: "short_answer_question",
+ options: [],
+ expected: "4",
+ points: 1,
+ published: true,
+ },
+ {
+ id: 2,
+ name: "Letters",
+ body: "What is the last letter of the English alphabet?",
+ type: "short_answer_question",
+ options: [],
+ expected: "Z",
+ points: 1,
+ published: false,
+ },
+ {
+ id: 5,
+ name: "Colors",
+ body: "Which of these is a color?",
+ type: "multiple_choice_question",
+ options: ["newspaper", "apple", "firetruck"],
+ expected: "red",
+ points: 1,
+ published: true,
+ },
+ {
+ id: 9,
+ name: "Shapes",
+ body: "What shape can you make with one line?",
+ type: "multiple_choice_question",
+ options: ["square", "triangle", "circle"],
+ expected: "circle",
+ points: 2,
+ published: false,
+ },
+ ]);
+
+ expect(editOption(SIMPLE_QUESTIONS, 5, 2, "newspaper")).toEqual([
+ {
+ id: 1,
+ name: "Addition",
+ body: "What is 2+2?",
+ type: "short_answer_question",
+ options: [],
+ expected: "4",
+ points: 1,
+ published: true,
+ },
+ {
+ id: 2,
+ name: "Letters",
+ body: "What is the last letter of the English alphabet?",
+ type: "short_answer_question",
+ options: [],
+ expected: "Z",
+ points: 1,
+ published: false,
+ },
+ {
+ id: 5,
+ name: "Colors",
+ body: "Which of these is a color?",
+ type: "multiple_choice_question",
+ options: ["red", "apple", "newspaper"],
+ expected: "red",
+ points: 1,
+ published: true,
+ },
+ {
+ id: 9,
+ name: "Shapes",
+ body: "What shape can you make with one line?",
+ type: "multiple_choice_question",
+ options: ["square", "triangle", "circle"],
+ expected: "circle",
+ points: 2,
+ published: false,
+ },
+ ]);
+ });
+
+ test("(3 pts) 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..686f5c3102
--- /dev/null
+++ b/src/nested.ts
@@ -0,0 +1,285 @@
+import { Answer } from "./interfaces/answer";
+import { Question, QuestionType } from "./interfaces/question";
+import { makeBlankQuestion } from "./objects";
+import { duplicateQuestion } 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[] {
+ return questions.filter(
+ (question: Question): boolean => question.published,
+ );
+}
+
+/**
+ * Consumes an array of questions and returns a new array of only the questions that are
+ * considered "non-empty". An empty question has an empty string for its `body` and
+ * `expected`, and an empty array for its `options`.
+ */
+export function getNonEmptyQuestions(questions: Question[]): Question[] {
+ return questions.filter(
+ (question: Question): boolean =>
+ question.body != "" ||
+ question.expected != "" ||
+ question.options.length > 0,
+ );
+}
+
+/***
+ * Consumes an array of questions and returns the question with the given `id`. If the
+ * question is not found, return `null` instead.
+ */
+export function findQuestion(
+ questions: Question[],
+ id: number,
+): Question | null {
+ const found = questions.find(
+ (question: Question): boolean => question.id === id,
+ );
+ return found ? found : 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[] {
+ return questions.filter(
+ (question: Question): boolean => question.id !== id,
+ );
+}
+
+/***
+ * 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[] {
+ return questions.map((question: Question): string => question.name);
+}
+
+/***
+ * Consumes an array of questions and returns the sum total of all their points added together.
+ */
+export function sumPoints(questions: Question[]): number {
+ return questions.reduce(
+ (total: number, question: Question): number => total + question.points,
+ 0,
+ );
+}
+
+/***
+ * Consumes an array of questions and returns the sum total of the PUBLISHED questions.
+ */
+export function sumPublishedPoints(questions: Question[]): number {
+ return questions
+ .filter((question: Question): boolean => question.published)
+ .reduce(
+ (total: number, question: Question): number =>
+ total + question.points,
+ 0,
+ );
+}
+
+/***
+ * 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 {
+ const header = "id,name,options,points,published";
+
+ const rows = questions.map(
+ (question: Question): string =>
+ question.id +
+ "," +
+ question.name +
+ "," +
+ question.options.length +
+ "," +
+ question.points +
+ "," +
+ question.published,
+ );
+
+ return [header, ...rows].join("\n");
+}
+
+/**
+ * Consumes an array of Questions and produces a corresponding array of
+ * Answers. Each Question gets its own Answer, copying over the `id` as the `questionId`,
+ * making the `text` an empty string, and using false for both `submitted` and `correct`.
+ */
+export function makeAnswers(questions: Question[]): Answer[] {
+ return questions.map(
+ (question: Question): Answer => ({
+ questionId: question.id,
+ text: "",
+ submitted: false,
+ correct: false,
+ }),
+ );
+}
+
+/***
+ * Consumes an array of Questions and produces a new array of questions, where
+ * each question is now published, regardless of its previous published status.
+ */
+export function publishAll(questions: Question[]): Question[] {
+ return questions.map(
+ (question: Question): Question => ({
+ ...question,
+ published: true,
+ }),
+ );
+}
+
+/***
+ * 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 {
+ if (questions.length === 0) {
+ return true;
+ }
+
+ const firstType = questions[0].type;
+
+ return questions.every(
+ (question: Question): boolean => question.type === firstType,
+ );
+}
+
+/***
+ * 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[] {
+ const newQuestion = makeBlankQuestion(id, name, type);
+
+ return [...questions, 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 name should now be `newName`.
+ */
+export function renameQuestionById(
+ questions: Question[],
+ targetId: number,
+ newName: string,
+): Question[] {
+ return questions.map(
+ (question: Question): Question =>
+ question.id === targetId ?
+ { ...question, name: newName }
+ : question,
+ );
+}
+
+/***
+ * 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[] {
+ return questions.map((question: Question): Question => {
+ if (question.id !== targetId) {
+ return question;
+ }
+
+ return {
+ ...question,
+ type: newQuestionType,
+ options:
+ newQuestionType === "multiple_choice_question" ?
+ question.options
+ : [],
+ };
+ });
+}
+
+/**
+ * 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[] {
+ return questions.map((question: Question): Question => {
+ if (question.id !== targetId) {
+ return question;
+ }
+
+ let newOptions = [...question.options];
+
+ if (targetOptionIndex === -1) {
+ newOptions = [...newOptions, newOption];
+ } else {
+ newOptions[targetOptionIndex] = newOption;
+ }
+
+ return { ...question, options: newOptions };
+ });
+}
+
+/***
+ * 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[] {
+ const index = questions.findIndex(
+ (question: Question): boolean => question.id === targetId,
+ );
+
+ if (index === -1) {
+ return questions;
+ }
+
+ const copy = duplicateQuestion(newId, questions[index]);
+
+ const result = [...questions];
+ result.splice(index + 1, 0, copy);
+
+ return result;
+}
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..0810e2c61a
--- /dev/null
+++ b/src/objects.ts
@@ -0,0 +1,162 @@
+import { Question, QuestionType } from "./interfaces/question";
+
+/**
+ * Create a new blank question with the given `id`, `name`, and `type. The `body` and
+ * `expected` should be empty strings, the `options` should be an empty list, the `points`
+ * should default to 1, and `published` should default to false.
+ */
+export function makeBlankQuestion(
+ id: number,
+ name: string,
+ type: QuestionType,
+): Question {
+ return {
+ id: id,
+ name: name,
+ type: type,
+ body: "",
+ expected: "",
+ options: [],
+ points: 1,
+ published: false,
+ };
+}
+
+/**
+ * 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 {
+ return (
+ answer.trim().toLowerCase() === question.expected.trim().toLowerCase()
+ );
+}
+
+/**
+ * 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 === "short_answer_question") {
+ return true;
+ } else {
+ return question.options.includes(answer);
+ }
+}
+
+/**
+ * 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 {
+ return `${question.id}: ${question.name.slice(0, 10)}`;
+}
+
+/**
+ * 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 {
+ const headerLine = `# ${question.name}`;
+ const bodyLine = question.body;
+
+ if (question.type === "multiple_choice_question") {
+ const optionLines = question.options.map(
+ (option: string): string => `- ${option}`,
+ );
+ return [headerLine, bodyLine, ...optionLines].join("\n");
+ } else {
+ return [headerLine, bodyLine].join("\n");
+ }
+}
+
+/**
+ * Return a new version of the given question, except the name should now be
+ * `newName`.
+ */
+export function renameQuestion(question: Question, newName: string): Question {
+ return { ...question, name: newName };
+}
+
+/**
+ * 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 {
+ return { ...question, published: !question.published };
+}
+
+/**
+ * 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 {
+ return {
+ id: id,
+ name: `Copy of ${oldQuestion.name}`,
+ type: oldQuestion.type,
+ body: oldQuestion.body,
+ expected: oldQuestion.expected,
+ options: [...oldQuestion.options],
+ points: oldQuestion.points,
+ published: false,
+ };
+}
+
+/**
+ * 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 {
+ return { ...question, options: [...question.options, newOption] };
+}
+
+/**
+ * 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 {
+ return {
+ ...contentQuestion,
+ id: id,
+ name: name,
+ points: points,
+ published: false,
+ options: [...contentQuestion.options],
+ };
+}