From 60c5883942a40c1e9aec10cdbeff510215d83984 Mon Sep 17 00:00:00 2001 From: jjgancfer Date: Wed, 24 Apr 2024 20:11:39 +0200 Subject: [PATCH 1/2] test: add more tests to the game --- webapp/src/pages/Game.jsx | 3 +- webapp/src/tests/Game.test.js | 548 +++++++++++++++++++--------------- 2 files changed, 308 insertions(+), 243 deletions(-) diff --git a/webapp/src/pages/Game.jsx b/webapp/src/pages/Game.jsx index 6b3b2dd0..fec21d9e 100644 --- a/webapp/src/pages/Game.jsx +++ b/webapp/src/pages/Game.jsx @@ -150,7 +150,6 @@ export default function Game() { navigate("/dashboard"); } } catch (error) { - console.error("Error initializing game:", error); navigate("/dashboard"); } }; @@ -198,7 +197,7 @@ export default function Game() { { (!loading && hasImage) && - {t("game.image")} + {t("game.image")} } diff --git a/webapp/src/tests/Game.test.js b/webapp/src/tests/Game.test.js index 14255d74..4b0ef25f 100644 --- a/webapp/src/tests/Game.test.js +++ b/webapp/src/tests/Game.test.js @@ -1,241 +1,307 @@ -import React from 'react'; -import {render, fireEvent, screen, act, waitFor} from '@testing-library/react'; -import { MemoryRouter } from 'react-router'; -import Game from '../pages/Game'; -import { ChakraProvider } from '@chakra-ui/react'; -import theme from '../styles/theme'; -import AuthManager from "../components/auth/AuthManager"; -import MockAdapter from "axios-mock-adapter"; -import {HttpStatusCode} from "axios"; - -jest.mock('react-i18next', () => ({ - useTranslation: () => { - return { - t: (str) => str, - i18n: { - changeLanguage: () => new Promise(() => {}), - language: "en" - }, - } - }, -})); - -const authManager = new AuthManager(); -let mockAxios = new MockAdapter(authManager.getAxiosInstance()); -const api = process.env.REACT_APP_API_ENDPOINT; - -const game = { - "id": 23483743, - "user": { - "id": 1, - "username": "Hordi Jurtado", - "email": "chipiChipi@chapaChapa.es " - }, - "rounds": 9, - "gamemode": "KIWI_QUEST", - "gameOver": false, - "actual_round": 0, - "correctly_answered_questions": 0, - "round_start_time": new Date(), - "round_duration": 20 -}; -const round = { - "id": 23483743, - "user": { - "id": 1, - "username": "Hordi Jurtado", - "email": "chipiChipi@chapaChapa.es " - }, - "rounds": 9, - "gamemode": "KIWI_QUEST", - "gameOver": false, - "actual_round": 1, - "correctly_answered_questions": 0, - "round_start_time": new Date(), - "round_duration": 20 -}; -const question = { - "id": 1, - "content": "What is the capital of France?", - "answers": [ - { - "id": 1, - "text": "Paris", - "category": "CITY" - }, - { - "id": 2, - "text": "London", - "category": "CITY" - }, - { - "id": 3, - "text": "Berlin", - "category": "CITY" - }, - { - "id": 4, - "text": "Madrid", - "category": "CITY" - } - ], - "questionCategory": "GEOGRAPHY", - "answerCategory": "CITY", - "language": "en", - "type": "MULTIPLE_CHOICE", - "image": "https://www.example.com/image.jpg" -} - -describe('Game component', () => { - - describe("there is no prior game", () => { - - beforeEach(() => { - mockAxios.onGet(`${api}/games/play`).reply(HttpStatusCode.Ok, game); - mockAxios.onGet(`${api}/games/${game.id}/question`).replyOnce(HttpStatusCode.Conflict); - mockAxios.onPost(`${api}/games/${game.id}/startRound`).reply(HttpStatusCode.Ok, round); - mockAxios.onGet(`${api}/games/${game.id}/question`).reply(HttpStatusCode.Ok, question); - }); - - afterEach(() => { - authManager.reset(); - mockAxios = new MockAdapter(authManager.getAxiosInstance()); - }); - - it("renders correctly", async () => { - const { container } = render(); - - await waitFor(() => { - expect(container.querySelectorAll(".question-answer").length).toBe(4); - expect(container.querySelector("#question").textContent).toBe(question.content); - expect(mockAxios.history.get.length).toBe(3); - expect(mockAxios.history.post.length).toBe(1); - }); - }); - - test('disables next button when no option is selected', async () => { - render(); - const nextButton = await screen.findByTestId('Next'); - - expect(nextButton).toBeDisabled(); - }); - - test('enables next button when an option is selected', async () => { - render(); - const option1Button = await screen.findByTestId('Option1'); - const nextButton = await screen.findByTestId('Next'); - - await act(() => fireEvent.click(option1Button)); - - expect(nextButton).toBeEnabled(); - }); - - test("only the last selection is chosen", async () => { - mockAxios.onPost(`${api}/games/${game.id}/answer`).replyOnce(HttpStatusCode.Ok, { - was_correct: true - }); - - render(); - await screen.findByTestId('Option1'); - - await act(() => fireEvent.click(screen.getByTestId('Option1'))); - await act(() => fireEvent.click(screen.getByTestId('Option3'))); - await act(() => fireEvent.click(screen.getByTestId('Next'))); - - await waitFor(() => { - expect(mockAxios.history.post[1].data).toBe(JSON.stringify({"answer_id": question.answers[2].id})); - }) - }); - - test("after answering with an answer that is true, confetti is shown", async () => { - mockAxios.onPost(`${api}/games/${game.id}/answer`).replyOnce(HttpStatusCode.Ok, { - was_correct: true - }); - - render(); - await screen.findByTestId('Option1'); - - await act(() => fireEvent.click(screen.getByTestId('Option1'))); - await act(() => fireEvent.click(screen.getByTestId('Next'))); - - await waitFor(() => { - expect(screen.getByTestId("confetti")).toBeEnabled(); - }); - }); - }); - - describe("there is a prior game", () => { - - beforeEach(() => { - mockAxios.onGet(`${api}/games/play`).reply(HttpStatusCode.Ok, game); - mockAxios.onGet(`${api}/games/${game.id}/question`).reply(HttpStatusCode.Ok, question); - mockAxios.onPost(`${api}/games/${game.id}/startRound`).reply(HttpStatusCode.Ok, round); - }); - - afterEach(() => { - authManager.reset(); - mockAxios = new MockAdapter(authManager.getAxiosInstance()); - }); - - it("renders correctly", async () => { - const { container } = render(); - - await waitFor(() => { - expect(container.querySelectorAll(".question-answer").length).toBe(4); - expect(container.querySelector("#question").textContent).toBe(question.content); - expect(mockAxios.history.get.length).toBe(2); - expect(mockAxios.history.post.length).toBe(0); - }); - }); - - test('disables next button when no option is selected', async () => { - render(); - const nextButton = await screen.findByTestId('Next'); - - expect(nextButton).toBeDisabled(); - }); - - test('enables next button when an option is selected', async () => { - render(); - const option1Button = await screen.findByTestId('Option1'); - const nextButton = await screen.findByTestId('Next'); - - await act(() => fireEvent.click(option1Button)); - - expect(nextButton).toBeEnabled(); - }); - - test("only the last selection is chosen", async () => { - mockAxios.onPost(`${api}/games/${game.id}/answer`).replyOnce(HttpStatusCode.Ok, { - was_correct: true - }); - - render(); - await screen.findByTestId('Option1'); - - await act(() => fireEvent.click(screen.getByTestId('Option1'))); - await act(() => fireEvent.click(screen.getByTestId('Option3'))); - await act(() => fireEvent.click(screen.getByTestId('Next'))); - - await waitFor(() => { - expect(mockAxios.history.post[0].data).toBe(JSON.stringify({"answer_id": question.answers[2].id})); - }) - }); - - test("there is no confetti if the answer is wrong", async () => { - mockAxios.onPost(`${api}/games/${game.id}/answer`).replyOnce(HttpStatusCode.Ok, { - was_correct: false - }); - - const {container} = render(); - await screen.findByTestId('Option1'); - - await act(() => fireEvent.click(screen.getByTestId('Option1'))); - await act(() => fireEvent.click(screen.getByTestId('Next'))); - - await waitFor(() => { - expect(container.querySelectorAll("[data-testid='confetti']").length).toBe(0); - }); - }); - }); -}); +import React from 'react'; +import {render, fireEvent, screen, act, waitFor} from '@testing-library/react'; +import { MemoryRouter } from 'react-router'; +import Game from '../pages/Game'; +import { ChakraProvider } from '@chakra-ui/react'; +import theme from '../styles/theme'; +import AuthManager from "../components/auth/AuthManager"; +import MockAdapter from "axios-mock-adapter"; +import {HttpStatusCode} from "axios"; + +jest.mock('react-i18next', () => ({ + useTranslation: () => { + return { + t: (str) => str, + i18n: { + changeLanguage: () => new Promise(() => {}), + language: "en" + }, + } + }, +})); + +const authManager = new AuthManager(); +let mockAxios = new MockAdapter(authManager.getAxiosInstance()); +const api = process.env.REACT_APP_API_ENDPOINT; + +const game = { + "id": 23483743, + "user": { + "id": 1, + "username": "Hordi Jurtado", + "email": "chipiChipi@chapaChapa.es " + }, + "rounds": 9, + "gamemode": "KIWI_QUEST", + "gameOver": false, + "actual_round": 0, + "correctly_answered_questions": 0, + "round_start_time": new Date(), + "round_duration": 20 +}; +const round = { + "id": 23483743, + "user": { + "id": 1, + "username": "Hordi Jurtado", + "email": "chipiChipi@chapaChapa.es " + }, + "rounds": 9, + "gamemode": "KIWI_QUEST", + "gameOver": false, + "actual_round": 1, + "correctly_answered_questions": 0, + "round_start_time": new Date(), + "round_duration": 20 +}; +const question = { + "id": 1, + "content": "What is the capital of France?", + "answers": [ + { + "id": 1, + "text": "Paris", + "category": "CITY" + }, + { + "id": 2, + "text": "London", + "category": "CITY" + }, + { + "id": 3, + "text": "Berlin", + "category": "CITY" + }, + { + "id": 4, + "text": "Madrid", + "category": "CITY" + } + ], + "questionCategory": "GEOGRAPHY", + "answerCategory": "CITY", + "language": "en", + "type": "MULTIPLE_CHOICE", + "image": "https://www.example.com/image.jpg" +} + +const mockFunction = jest.fn(); +jest.mock('react-router-dom', () => ({ + ...jest.requireActual('react-router-dom'), + useNavigate: () => mockFunction, +})); + + + +describe('Game component', () => { + + describe("there is no prior game", () => { + + beforeEach(() => { + mockAxios.onGet(`${api}/games/play`).reply(HttpStatusCode.Ok, game); + mockAxios.onGet(`${api}/games/${game.id}/question`).replyOnce(HttpStatusCode.Conflict); + mockAxios.onPost(`${api}/games/${game.id}/startRound`).reply(HttpStatusCode.Ok, round); + mockAxios.onGet(`${api}/games/${game.id}/question`).reply(HttpStatusCode.Ok, question); + }); + + afterEach(() => { + authManager.reset(); + mockAxios = new MockAdapter(authManager.getAxiosInstance()); + }); + + it("renders correctly", async () => { + const { container } = render(); + + await waitFor(() => { + expect(container.querySelectorAll(".question-answer").length).toBe(4); + expect(container.querySelector("#question").textContent).toBe(question.content); + expect(mockAxios.history.get.length).toBe(3); + expect(mockAxios.history.post.length).toBe(1); + }); + }); + + test('disables next button when no option is selected', async () => { + render(); + const nextButton = await screen.findByTestId('Next'); + + expect(nextButton).toBeDisabled(); + }); + + test('enables next button when an option is selected', async () => { + render(); + const option1Button = await screen.findByTestId('Option1'); + const nextButton = await screen.findByTestId('Next'); + + await act(() => fireEvent.click(option1Button)); + + expect(nextButton).toBeEnabled(); + }); + + test("only the last selection is chosen", async () => { + mockAxios.onPost(`${api}/games/${game.id}/answer`).replyOnce(HttpStatusCode.Ok, { + was_correct: true + }); + + render(); + await screen.findByTestId('Option1'); + + await act(() => fireEvent.click(screen.getByTestId('Option1'))); + await act(() => fireEvent.click(screen.getByTestId('Option3'))); + await act(() => fireEvent.click(screen.getByTestId('Next'))); + + await waitFor(() => { + expect(mockAxios.history.post[1].data).toBe(JSON.stringify({"answer_id": question.answers[2].id})); + }) + }); + + test("after answering with an answer that is true, confetti is shown", async () => { + mockAxios.onPost(`${api}/games/${game.id}/answer`).replyOnce(HttpStatusCode.Ok, { + was_correct: true + }); + + render(); + await screen.findByTestId('Option1'); + + await act(() => fireEvent.click(screen.getByTestId('Option1'))); + await act(() => fireEvent.click(screen.getByTestId('Next'))); + + await waitFor(() => { + expect(screen.getByTestId("confetti")).toBeEnabled(); + }); + }); + + describe("it tries to navigate to dashboard after", () => { + + beforeEach(() => { + mockAxios.reset(); + }); + + test("a not valid object is returned when trying to create a game", async () => { + mockAxios.onGet(`${api}/games/play`).reply(HttpStatusCode.Ok, undefined); + render(); + + await waitFor(() => { + expect(mockFunction).toHaveBeenCalled(); + expect(mockFunction).toHaveBeenCalledWith("/dashboard"); + }); + }); + + test("an object is returned when trying to create a game", async () => { + mockAxios.onGet(`${api}/games/play`).reply(HttpStatusCode.Ok, {}); + render(); + + await waitFor(() => { + expect(mockFunction).toHaveBeenCalled(); + expect(mockFunction).toHaveBeenCalledWith("/dashboard"); + }); + }); + + test("the petition to get the game fails", async () => { + mockAxios.onGet(`${api}/games/play`).reply(HttpStatusCode.InternalServerError); + render(); + + await waitFor(() => { + expect(mockFunction).toHaveBeenCalled(); + expect(mockFunction).toHaveBeenCalledWith("/dashboard"); + }); + }) + }) + }); + + describe("there is a prior game", () => { + + beforeEach(() => { + mockAxios.onGet(`${api}/games/play`).reply(HttpStatusCode.Ok, game); + mockAxios.onGet(`${api}/games/${game.id}/question`).reply(HttpStatusCode.Ok, question); + mockAxios.onPost(`${api}/games/${game.id}/startRound`).reply(HttpStatusCode.Ok, round); + }); + + afterEach(() => { + authManager.reset(); + mockAxios = new MockAdapter(authManager.getAxiosInstance()); + }); + + it("renders correctly", async () => { + const { container } = render(); + + await waitFor(() => { + expect(container.querySelectorAll(".question-answer").length).toBe(4); + expect(container.querySelector("#question").textContent).toBe(question.content); + expect(mockAxios.history.get.length).toBe(2); + expect(mockAxios.history.post.length).toBe(0); + }); + }); + + test('disables next button when no option is selected', async () => { + render(); + const nextButton = await screen.findByTestId('Next'); + + expect(nextButton).toBeDisabled(); + }); + + test('enables next button when an option is selected', async () => { + render(); + const option1Button = await screen.findByTestId('Option1'); + const nextButton = await screen.findByTestId('Next'); + + await act(() => fireEvent.click(option1Button)); + + expect(nextButton).toBeEnabled(); + }); + + test("only the last selection is chosen", async () => { + mockAxios.onPost(`${api}/games/${game.id}/answer`).replyOnce(HttpStatusCode.Ok, { + was_correct: true + }); + + render(); + await screen.findByTestId('Option1'); + + await act(() => fireEvent.click(screen.getByTestId('Option1'))); + await act(() => fireEvent.click(screen.getByTestId('Option3'))); + await act(() => fireEvent.click(screen.getByTestId('Next'))); + + await waitFor(() => { + expect(mockAxios.history.post[0].data).toBe(JSON.stringify({"answer_id": question.answers[2].id})); + }) + }); + + test("there is no confetti if the answer is wrong", async () => { + mockAxios.onPost(`${api}/games/${game.id}/answer`).replyOnce(HttpStatusCode.Ok, { + was_correct: false + }); + + const {container} = render(); + await screen.findByTestId('Option1'); + + await act(() => fireEvent.click(screen.getByTestId('Option1'))); + await act(() => fireEvent.click(screen.getByTestId('Next'))); + + await waitFor(() => { + expect(container.querySelectorAll("[data-testid='confetti']").length).toBe(0); + }); + }); + + test("there is no image shown if the URI is not passed", async () => { + mockAxios.reset(); + let clone = {...question}; + delete clone.image; + mockAxios.onGet(`${api}/games/play`).reply(HttpStatusCode.Ok, game); + mockAxios.onGet(`${api}/games/${game.id}/question`).reply(HttpStatusCode.Ok, clone); + mockAxios.onPost(`${api}/games/${game.id}/startRound`).reply(HttpStatusCode.Ok, round); + + const {container} = render(); + await waitFor(() => { + expect(container.querySelectorAll("[data-testid='image']").length).toBe(0); + }); + }); + + test("there is image shown if it is passed the URI", async () => { + const {container} = render(); + await waitFor(() => { + expect(container.querySelectorAll("[data-testid='image']").length).toBe(1); + }); + }); + }); +}); From 50f03afb275d5e5ef79e47edbbb74972fd17e883 Mon Sep 17 00:00:00 2001 From: jjgancfer Date: Wed, 24 Apr 2024 20:46:11 +0200 Subject: [PATCH 2/2] test: DashboardButton tests --- .../components/dashboard/DashboardButton.jsx | 122 +++++++++--------- webapp/src/pages/Game.jsx | 1 - webapp/src/tests/DashboardButton.test.js | 25 ++++ 3 files changed, 86 insertions(+), 62 deletions(-) create mode 100644 webapp/src/tests/DashboardButton.test.js diff --git a/webapp/src/components/dashboard/DashboardButton.jsx b/webapp/src/components/dashboard/DashboardButton.jsx index c4e93af5..9323c09b 100644 --- a/webapp/src/components/dashboard/DashboardButton.jsx +++ b/webapp/src/components/dashboard/DashboardButton.jsx @@ -1,62 +1,62 @@ -import React from "react"; -import PropTypes from 'prop-types'; -import { Button, Box } from "@chakra-ui/react"; -import { FaKiwiBird, FaRandom, FaPalette } from "react-icons/fa"; -import { TbWorld } from "react-icons/tb"; -import { IoIosFootball, IoLogoGameControllerB } from "react-icons/io"; - -const DashboardButton = ({ label, selectedButton, onClick, iconName }) => { - const isSelected = label === selectedButton; - let icon = null; - - switch (iconName) { - case "FaKiwiBird": - icon = ; - break; - case "IoIosFootball": - icon = ; - break; - case "FaGlobeAmericas": - icon = ; - break; - case "IoLogoGameControllerB": - icon = ; - break; - case "FaPalette": - icon = ; - break; - case "FaRandom": - icon = ; - break; - default: - break; - } - - return ( - - ); -}; - -DashboardButton.propTypes = { - label: PropTypes.string.isRequired, - selectedButton: PropTypes.string.isRequired, - onClick: PropTypes.func.isRequired, - iconName: PropTypes.string.isRequired -}; - +import React from "react"; +import PropTypes from 'prop-types'; +import { Button, Box } from "@chakra-ui/react"; +import { FaKiwiBird, FaRandom, FaPalette } from "react-icons/fa"; +import { TbWorld } from "react-icons/tb"; +import { IoIosFootball, IoLogoGameControllerB } from "react-icons/io"; + +const DashboardButton = ({ label, selectedButton, onClick, iconName }) => { + const isSelected = label === selectedButton; + let icon = null; + + switch (iconName) { + case "FaKiwiBird": + icon = ; + break; + case "IoIosFootball": + icon = ; + break; + case "FaGlobeAmericas": + icon = ; + break; + case "IoLogoGameControllerB": + icon = ; + break; + case "FaPalette": + icon = ; + break; + case "FaRandom": + icon = ; + break; + default: + break; + } + + return ( + + ); +}; + +DashboardButton.propTypes = { + label: PropTypes.string.isRequired, + selectedButton: PropTypes.string.isRequired, + onClick: PropTypes.func.isRequired, + iconName: PropTypes.string.isRequired +}; + export default DashboardButton; \ No newline at end of file diff --git a/webapp/src/pages/Game.jsx b/webapp/src/pages/Game.jsx index fec21d9e..029d884e 100644 --- a/webapp/src/pages/Game.jsx +++ b/webapp/src/pages/Game.jsx @@ -56,7 +56,6 @@ export default function Game() { if (error.response.status === HttpStatusCode.Conflict) { throw error; } else { - console.error("Error fetching question:", error); navigate("/dashboard"); } } diff --git a/webapp/src/tests/DashboardButton.test.js b/webapp/src/tests/DashboardButton.test.js new file mode 100644 index 00000000..6ad74319 --- /dev/null +++ b/webapp/src/tests/DashboardButton.test.js @@ -0,0 +1,25 @@ +import each from "jest-each"; +import {act, fireEvent, render, screen, waitFor} from "@testing-library/react"; +import DashboardButton from "../components/dashboard/DashboardButton"; + + +describe("Dashboard Button", () => { + + each(["FaKiwiBird", "IoIosFootball", "FaGlobeAmericas", + "IoLogoGameControllerB", "FaPalette", + "FaRandom"]).test("the proper icon renderized", async (iconName) => { + const props = { + "label": "label", + "selectedButton": "label", + "onClick": () => {}, + "iconName": iconName + }; + + render(); + + await waitFor( async () => { + expect(await screen.findByTestId(iconName)).toBeEnabled(); + }); + }); + +}); \ No newline at end of file