diff --git a/webapp/src/components/game/Game.js b/webapp/src/components/game/Game.js index ffe3ac04..edcf768b 100644 --- a/webapp/src/components/game/Game.js +++ b/webapp/src/components/game/Game.js @@ -1,4 +1,4 @@ -import {HttpStatusCode} from "axios"; +import { HttpStatusCode } from "axios"; import AuthManager from "components/auth/AuthManager"; const authManager = new AuthManager(); @@ -15,11 +15,25 @@ export async function newGame() { } export async function startRound(gameId) { - return await authManager.getAxiosInstance().post(process.env.REACT_APP_API_ENDPOINT + "/games/" + gameId + "/startRound"); + try { + let requestAnswer = await authManager.getAxiosInstance().post(process.env.REACT_APP_API_ENDPOINT + "/games/" + gameId + "/startRound"); + if (HttpStatusCode.Ok === requestAnswer.status) { + return requestAnswer.data; + } + } catch { + + } } export async function getCurrentQuestion(gameId) { - return await authManager.getAxiosInstance().get(process.env.REACT_APP_API_ENDPOINT + "/games/" + gameId + "/question"); + try { + let requestAnswer = await authManager.getAxiosInstance().get(process.env.REACT_APP_API_ENDPOINT + "/games/" + gameId + "/question"); + if (HttpStatusCode.Ok === requestAnswer.status) { + return requestAnswer.data; + } + } catch { + + } } export async function changeLanguage(gameId, language) { @@ -34,7 +48,14 @@ export async function changeLanguage(gameId, language) { } export async function answerQuestion(gameId, aId) { - return await authManager.getAxiosInstance().post(process.env.REACT_APP_API_ENDPOINT + "/games/" + gameId + "/answer", {answer_id:aId}); + try { + let requestAnswer = await authManager.getAxiosInstance().post(process.env.REACT_APP_API_ENDPOINT + "/games/" + gameId + "/answer", {answer_id:aId}); + if (HttpStatusCode.Ok === requestAnswer.status) { + return requestAnswer.data; + } + } catch { + + } } export async function getGameDetails(gameId) { @@ -47,4 +68,3 @@ export async function getGameDetails(gameId) { } } - diff --git a/webapp/src/pages/Game.jsx b/webapp/src/pages/Game.jsx index d5f875b2..4d41edb9 100644 --- a/webapp/src/pages/Game.jsx +++ b/webapp/src/pages/Game.jsx @@ -1,232 +1,232 @@ -import React, { useState, useEffect } from "react"; -import { Grid, Flex, Heading, Button, Box, Text, Spinner, CircularProgress } from "@chakra-ui/react"; -import { Center } from "@chakra-ui/layout"; -import { useNavigate } from "react-router-dom"; -import { useTranslation } from "react-i18next"; -import Confetti from "react-confetti"; -import { newGame, startRound, getCurrentQuestion, answerQuestion } from '../components/game/Game'; -import LateralMenu from '../components/LateralMenu'; -import MenuButton from '../components/MenuButton'; -import { HttpStatusCode } from "axios"; - -export default function Game() { - const navigate = useNavigate(); - - const [loading, setLoading] = useState(true); - const [gameId, setGameId] = useState(null); - const [question, setQuestion] = useState(null); - const [answer, setAnswer] = useState({}); - const [selectedOption, setSelectedOption] = useState(null); - const [nextDisabled, setNextDisabled] = useState(true); - const [roundNumber, setRoundNumber] = useState(1); - const [correctAnswers, setCorrectAnswers] = useState(0); - const [showConfetti, setShowConfetti] = useState(false); - const [timeElapsed, setTimeElapsed] = useState(0); - const [timeStartRound, setTimeStartRound] = useState(-1); - const [roundDuration, setRoundDuration] = useState(0); - const [maxRoundNumber, setMaxRoundNumber] = useState(9); - - const { t, i18n } = useTranslation(); - const [isMenuOpen, setIsMenuOpen] = useState(false); - - const changeLanguage = (selectedLanguage) => { - i18n.changeLanguage(selectedLanguage); - }; - - const calculateProgress = () => { - const percentage = (timeElapsed / roundDuration) * 100; - return Math.min(Math.max(percentage, 0), 100); - }; - - const assignQuestion = async (gameId) => { - try { - const result = await getCurrentQuestion(gameId); - if (result.status === HttpStatusCode.Ok) { - setQuestion(result.data); - setNextDisabled(false); - setTimeElapsed(0); - } else { - navigate("/dashboard"); - } - } catch (error) { - console.error("Error fetching question:", error); - navigate("/dashboard"); - } - } - - const answerButtonClick = async (optionIndex, answer) => { - const selectedOptionIndex = selectedOption === optionIndex ? null : optionIndex; - setSelectedOption(selectedOptionIndex); - await setAnswer(answer); - const anyOptionSelected = selectedOptionIndex !== null; - setNextDisabled(!anyOptionSelected); - }; - - const startNewRound = async (gameId) => { - try{ - const result = await startRound(gameId); - setTimeStartRound(new Date(result.data.round_start_time).getTime()); - setRoundNumber(result.data.actual_round ) - setRoundDuration(result.data.round_duration); - await assignQuestion(gameId); - setLoading(false); - } - catch(error){ - console.log(error) - if(error.status === 409){ - if(roundNumber >= 9){ - navigate("/dashboard/game/results", { state: { correctAnswers: correctAnswers } }); - } else { - await assignQuestion(gameId) - } - } - - } - - } - - /* - Initialize game when loading the page - */ - const initializeGame = async () => { - try { - const newGameResponse = await newGame(); - if (newGameResponse) { - setGameId(newGameResponse.id); - setTimeStartRound(new Date(newGameResponse.round_start_time).getTime()); - setRoundDuration(newGameResponse.round_duration) - setMaxRoundNumber(newGameResponse.rounds); - try{ - const result = await getCurrentQuestion(newGameResponse.id); - if (result.status === HttpStatusCode.Ok) { - setQuestion(result.data); - setNextDisabled(false); - setLoading(false); - } - }catch(error){ - startNewRound(newGameResponse.id); - } - - - } else { - navigate("/dashboard"); - } - } catch (error) { - console.error("Error initializing game:", error); - navigate("/dashboard"); - } - }; - - const nextRound = async () => { - if (roundNumber + 1 > maxRoundNumber) { - navigate("/dashboard/game/results", { state: { correctAnswers: correctAnswers } }); - } else { - setAnswer({}); - setNextDisabled(true); - await startNewRound(gameId); - } - } - - const nextButtonClick = async () => { - try { - const result = await answerQuestion(gameId, answer.id); - let isCorrect = result.data.was_correct; - if (isCorrect) { - setCorrectAnswers(correctAnswers + (isCorrect ? 1 : 0)); - setShowConfetti(true); - } - setNextDisabled(true); - setSelectedOption(null); - await nextRound() - - } catch (error) { - if(error.response.status === 400){ - setTimeout(nextButtonClick, 2000) - }else{ - console.log('xd'+error.response.status) - } - } - }; - useEffect(() => { - // Empty dependency array [] ensures this effect runs only once after initial render - initializeGame(); - // eslint-disable-next-line - }, []); - useEffect(() => { - let timeout; - if (showConfetti) - timeout = setTimeout(() => { setShowConfetti(false); }, 3000); - return () => clearTimeout(timeout); - }, [showConfetti]); - - useEffect(() => { - let timeout; - if (timeElapsed >= roundDuration && timeStartRound !== -1) { - timeout = setTimeout(() => nextRound(), 1000); - - } else { - timeout = setTimeout(() => { - setTimeElapsed((prevTime) => prevTime + 1); - }, 1000); - } - return () => clearTimeout(timeout); - // eslint-disable-next-line - }, [timeElapsed, timeStartRound, roundDuration]); - - - return ( -
- setIsMenuOpen(true)} /> - setIsMenuOpen(false)} changeLanguage={changeLanguage} isDashboard={false}/> - - {t("game.round") + `${roundNumber}`} - - {`Correct answers: ${correctAnswers}`} - - - - - {loading ? ( - - ) : ( - question && ( - <> - {question.content} - - - {question.answers.map((answer, index) => ( - - ))} - - - - - - - {showConfetti && ( - - )} - - ) - )} - -
- ); +import React, { useState, useEffect } from "react"; +import { Grid, Flex, Heading, Button, Box, Text, Spinner, CircularProgress } from "@chakra-ui/react"; +import { Center } from "@chakra-ui/layout"; +import { useNavigate } from "react-router-dom"; +import { useTranslation } from "react-i18next"; +import Confetti from "react-confetti"; +import { newGame, startRound, getCurrentQuestion, answerQuestion } from '../components/game/Game'; +import LateralMenu from '../components/LateralMenu'; +import MenuButton from '../components/MenuButton'; +import { HttpStatusCode } from "axios"; + +export default function Game() { + const navigate = useNavigate(); + + const [loading, setLoading] = useState(true); + const [gameId, setGameId] = useState(null); + const [question, setQuestion] = useState(null); + const [answer, setAnswer] = useState({}); + const [selectedOption, setSelectedOption] = useState(null); + const [nextDisabled, setNextDisabled] = useState(true); + const [roundNumber, setRoundNumber] = useState(1); + const [correctAnswers, setCorrectAnswers] = useState(0); + const [showConfetti, setShowConfetti] = useState(false); + const [timeElapsed, setTimeElapsed] = useState(0); + const [timeStartRound, setTimeStartRound] = useState(-1); + const [roundDuration, setRoundDuration] = useState(0); + const [maxRoundNumber, setMaxRoundNumber] = useState(9); + + const { t, i18n } = useTranslation(); + const [isMenuOpen, setIsMenuOpen] = useState(false); + + const changeLanguage = (selectedLanguage) => { + i18n.changeLanguage(selectedLanguage); + }; + + const calculateProgress = () => { + const percentage = (timeElapsed / roundDuration) * 100; + return Math.min(Math.max(percentage, 0), 100); + }; + + const assignQuestion = async (gameId) => { + try { + const result = await getCurrentQuestion(gameId); + if (result.status === HttpStatusCode.Ok) { + setQuestion(result.data); + setNextDisabled(false); + setTimeElapsed(0); + } else { + navigate("/dashboard"); + } + } catch (error) { + console.error("Error fetching question:", error); + navigate("/dashboard"); + } + } + + const answerButtonClick = async (optionIndex, answer) => { + const selectedOptionIndex = selectedOption === optionIndex ? null : optionIndex; + setSelectedOption(selectedOptionIndex); + await setAnswer(answer); + const anyOptionSelected = selectedOptionIndex !== null; + setNextDisabled(!anyOptionSelected); + }; + + const startNewRound = async (gameId) => { + try{ + const result = await startRound(gameId); + setTimeStartRound(new Date(result.data.round_start_time).getTime()); + setRoundNumber(result.data.actual_round ) + setRoundDuration(result.data.round_duration); + await assignQuestion(gameId); + setLoading(false); + } + catch(error){ + console.log(error) + if(error.status === 409){ + if(roundNumber >= 9){ + navigate("/dashboard/game/results", { state: { correctAnswers: correctAnswers } }); + } else { + await assignQuestion(gameId) + } + } + + } + + } + + /* + Initialize game when loading the page + */ + const initializeGame = async () => { + try { + const newGameResponse = await newGame(); + if (newGameResponse) { + setGameId(newGameResponse.id); + setTimeStartRound(new Date(newGameResponse.round_start_time).getTime()); + setRoundDuration(newGameResponse.round_duration) + setMaxRoundNumber(newGameResponse.rounds); + try{ + const result = await getCurrentQuestion(newGameResponse.id); + if (result.status === HttpStatusCode.Ok) { + setQuestion(result.data); + setNextDisabled(false); + setLoading(false); + } + }catch(error){ + startNewRound(newGameResponse.id); + } + + + } else { + navigate("/dashboard"); + } + } catch (error) { + console.error("Error initializing game:", error); + navigate("/dashboard"); + } + }; + + const nextRound = async () => { + if (roundNumber + 1 > maxRoundNumber) { + navigate("/dashboard/game/results", { state: { correctAnswers: correctAnswers } }); + } else { + setAnswer({}); + setNextDisabled(true); + await startNewRound(gameId); + } + } + + const nextButtonClick = async () => { + try { + const result = await answerQuestion(gameId, answer.id); + let isCorrect = result.data.was_correct; + if (isCorrect) { + setCorrectAnswers(correctAnswers + (isCorrect ? 1 : 0)); + setShowConfetti(true); + } + setNextDisabled(true); + setSelectedOption(null); + await nextRound() + + } catch (error) { + if(error.response.status === 400){ + setTimeout(nextButtonClick, 2000) + }else{ + console.log('xd'+error.response.status) + } + } + }; + useEffect(() => { + // Empty dependency array [] ensures this effect runs only once after initial render + initializeGame(); + // eslint-disable-next-line + }, []); + useEffect(() => { + let timeout; + if (showConfetti) + timeout = setTimeout(() => { setShowConfetti(false); }, 3000); + return () => clearTimeout(timeout); + }, [showConfetti]); + + useEffect(() => { + let timeout; + if (timeElapsed >= roundDuration && timeStartRound !== -1) { + timeout = setTimeout(() => nextRound(), 1000); + + } else { + timeout = setTimeout(() => { + setTimeElapsed((prevTime) => prevTime + 1); + }, 1000); + } + return () => clearTimeout(timeout); + // eslint-disable-next-line + }, [timeElapsed, timeStartRound, roundDuration]); + + + return ( +
+ setIsMenuOpen(true)} /> + setIsMenuOpen(false)} changeLanguage={changeLanguage} isDashboard={false}/> + + {t("game.round") + `${roundNumber}`} + + {`Correct answers: ${correctAnswers}`} + + + + + {loading ? ( + + ) : ( + question && ( + <> + {question.content} + + + {question.answers.map((answer, index) => ( + + ))} + + + + + + + {showConfetti && ( + + )} + + ) + )} + +
+ ); } \ No newline at end of file diff --git a/webapp/src/tests/Game.test.js b/webapp/src/tests/Game.test.js index 58c8e60d..74c8e69a 100644 --- a/webapp/src/tests/Game.test.js +++ b/webapp/src/tests/Game.test.js @@ -1,79 +1,94 @@ -import React from 'react'; -import { render, fireEvent, screen, act } from '@testing-library/react'; -import { MemoryRouter } from 'react-router'; -import Game from '../pages/Game'; +import React from "react"; +import { render, fireEvent, waitFor } from "@testing-library/react"; +import "@testing-library/jest-dom/extend-expect"; import { ChakraProvider } from '@chakra-ui/react'; import theme from '../styles/theme'; -import { getQuestion } from '../components/game/Questions'; - -jest.mock('react-i18next', () => ({ - useTranslation: () => { - return { - t: (str) => str, - i18n: { - changeLanguage: () => new Promise(() => {}), - }, - } - }, -})); +import { MemoryRouter } from 'react-router'; +import Game from "../pages/Game"; +import { HttpStatusCode } from "axios"; +import AuthManager from "components/auth/AuthManager"; +import MockAdapter from "axios-mock-adapter"; -jest.mock('../components/game/Questions', () => ({ - getQuestion: jest.fn(), -})); +describe("Game Component", () => { + const authManager = new AuthManager(); + let mockAxios; -describe('Game component', () => { - /* beforeEach(() => { - getQuestion.mockResolvedValue({ - content: 'Test question', - answers: [ - { id: 1, text: 'Test answer 1', category: 'Test category 1' }, - { id: 2, text: 'Test answer 2', category: 'Test category 2' }, - ], - }); + authManager.reset(); + mockAxios = new MockAdapter(authManager.getAxiosInstance()); }); - afterEach(() => { - jest.restoreAllMocks(); + afterAll(() => { + mockAxios = null; + authManager.reset(); }); - */ - test('selects an option when clicked', async () => { - /* - render(); - const option1Button = await screen.findByTestId('Option1'); - act(() => fireEvent.click(option1Button)); - - expect(option1Button).toHaveClass('chakra-button custom-button effect1 css-m4hh83'); - */ + it("renders loading spinner initially", () => { + const { getByTestId } = render(); + expect(getByTestId("loading-spinner")).toBeInTheDocument(); }); - /* - test('disables next button when no option is selected', async () => { - render(); - const nextButton = await screen.findByTestId('Next'); - expect(nextButton).toBeDisabled(); + it("renders round number and correct answers count", async () => { + const { getByText } = render(); + expect(getByText("game.round1")).toBeInTheDocument(); + expect(getByText("Correct answers: 0")).toBeInTheDocument(); }); - test('enables next button when an option is selected', async () => { - render(); - const option1Button = await screen.findByTestId('Option1'); - const nextButton = await screen.findByTestId('Next'); - - act(() => fireEvent.click(option1Button)); - - expect(nextButton).toBeEnabled(); + it("displays question and options after loading", async () => { + const data = { + question: "What is the capital of Spain?", + options: ["Madrid", "Barcelona", "Seville", "Valencia"], + }; + mockAxios.onGet().reply(HttpStatusCode.Ok, data); + const { container } = render(); + waitFor(() => { + expect(container).toHaveTextContent("What is the capital of Spain?"); + expect(container).toHaveTextContent("Madrid"); + expect(container).toHaveTextContent("Barcelona"); + expect(container).toHaveTextContent("Seville"); + expect(container).toHaveTextContent("Valencia"); + }); }); - test('renders ButtonEf component correctly', async () => { - render(); - const option2Button = await screen.findByTestId('Option2'); - - expect(option2Button).toHaveClass('chakra-button custom-button effect1 css-147pzm2'); - - act(() => fireEvent.click(option2Button)); - - expect(option2Button).toHaveClass('chakra-button custom-button effect1 css-m4hh83'); + it("allows selecting an answer and enables next button", async () => { + const data = { + question: "What is the capital of Spain?", + options: ["Madrid", "Barcelona", "Seville", "Valencia"], + }; + mockAxios.onGet().reply(HttpStatusCode.Ok, data); + const { container } = render(); + waitFor(() => { + const optionButton = container.querySelector("button"); + fireEvent.click(optionButton); + expect(optionButton).toHaveStyle("background-color: green"); + const nextButton = container.querySelector("button"); + expect(nextButton).not.toBeDisabled(); + }); + }); + it("displays correct answer after selecting wrong answer", async () => { + const data = { + question: "What is the capital of Spain?", + options: ["Madrid", "Barcelona", "Seville", "Valencia"], + }; + mockAxios.onGet().reply(HttpStatusCode.Ok, data); + const { container } = render(); + waitFor(() => { + const optionButton = container.querySelector("button"); + fireEvent.click(optionButton); + expect(optionButton).toHaveStyle("background-color: red"); + }); + }); + it("displays correct answer after selecting correct answer", async () => { + const data = { + question: "What is the capital of Spain?", + options: ["Madrid", "Barcelona", "Seville", "Valencia"], + }; + mockAxios.onGet().reply(HttpStatusCode.Ok, data); + const { container } = render(); + waitFor(() => { + const optionButton = container.querySelector("button"); + fireEvent.click(optionButton); + expect(optionButton).toHaveStyle("background-color: green"); + }); }); - */ }); diff --git a/webapp/src/tests/GameAPI.test.js b/webapp/src/tests/GameAPI.test.js new file mode 100644 index 00000000..c67109dd --- /dev/null +++ b/webapp/src/tests/GameAPI.test.js @@ -0,0 +1,193 @@ +import MockAdapter from "axios-mock-adapter"; +import { newGame, startRound, getCurrentQuestion, changeLanguage, answerQuestion, getGameDetails } from "components/game/Game"; +import axios, { HttpStatusCode } from "axios"; +import AuthManager from "components/auth/AuthManager"; + +jest.mock('react-i18next', () => ({ + useTranslation: () => { + return { + t: (str) => str, + i18n: { + changeLanguage: () => new Promise(() => {}), + }, + } + }, +})); + +const authManager = new AuthManager(); +let mockAxios; + +describe("Game Service tests", () => { + beforeEach(() => { + authManager.reset(); + mockAxios = new MockAdapter(authManager.getAxiosInstance()); + }); + + describe("newGame function", () => { + it("successfully creates a new game", async () => { + const mockResponse = { gameId: 123, status: "created" }; + + mockAxios.onPost(process.env.REACT_APP_API_ENDPOINT + "/games/new").replyOnce( + HttpStatusCode.Ok, + mockResponse + ); + + const result = await newGame(); + + expect(result).toEqual(mockResponse); + }); + + it("handles errors when creating a new game", async () => { + mockAxios.onPost(process.env.REACT_APP_API_ENDPOINT + "/games/new").replyOnce( + HttpStatusCode.InternalServerError + ); + + const result = await newGame(); + + expect(result).toBeUndefined(); + }); + }); + + describe("startRound function", () => { + it("successfully starts a new round", async () => { + const gameId = 123; + const mockResponse = { roundId: 456, status: "started" }; + + mockAxios.onPost(process.env.REACT_APP_API_ENDPOINT + `/games/${gameId}/startRound`).replyOnce( + HttpStatusCode.Ok, + mockResponse + ); + + const result = await startRound(gameId); + + expect(result).toEqual(mockResponse); + }); + + it("handles errors when starting a new round", async () => { + const gameId = 123; + + mockAxios.onPost(process.env.REACT_APP_API_ENDPOINT + `/games/${gameId}/startRound`).replyOnce( + HttpStatusCode.NotFound + ); + + const result = await startRound(gameId); + + expect(result).toBeUndefined(); + }); + }); + describe("getCurrentQuestion function", () => { + it("successfully retrieves current question", async () => { + const gameId = 123; + const mockResponse = { questionId: 456, text: "What's your name?" }; + + mockAxios.onGet(process.env.REACT_APP_API_ENDPOINT + `/games/${gameId}/question`).replyOnce( + HttpStatusCode.Ok, + mockResponse + ); + + const result = await getCurrentQuestion(gameId); + + expect(result).toEqual(mockResponse); + }); + + it("handles errors when retrieving current question", async () => { + const gameId = 123; + + mockAxios.onGet(process.env.REACT_APP_API_ENDPOINT + `/games/${gameId}/question`).replyOnce( + HttpStatusCode.NotFound + ); + + const result = await getCurrentQuestion(gameId); + + expect(result).toBeUndefined(); + }); + }); + + describe("changeLanguage function", () => { + it("successfully changes language", async () => { + const gameId = 123; + const language = "en"; + const mockResponse = { success: true, message: "Language changed successfully." }; + + mockAxios.onPut(process.env.REACT_APP_API_ENDPOINT + `/games/${gameId}/language?language=${language}`).replyOnce( + HttpStatusCode.Ok, + mockResponse + ); + + const result = await changeLanguage(gameId, language); + + expect(result).toEqual(mockResponse); + }); + + it("handles errors when changing language", async () => { + const gameId = 123; + const language = "en"; + + mockAxios.onPut(process.env.REACT_APP_API_ENDPOINT + `/games/${gameId}/language?language=${language}`).replyOnce( + HttpStatusCode.BadRequest + ); + + const result = await changeLanguage(gameId, language); + + expect(result).toBeUndefined(); + }); + }); + + describe("answerQuestion function", () => { + it("successfully submits an answer", async () => { + const gameId = 123; + const answerId = "a1"; + const mockResponse = { success: true, message: "Answer submitted successfully." }; + + mockAxios.onPost(process.env.REACT_APP_API_ENDPOINT + `/games/${gameId}/answer`, { answer_id: answerId }).replyOnce( + HttpStatusCode.Ok, + mockResponse + ); + + const result = await answerQuestion(gameId, answerId); + + expect(result).toEqual(mockResponse); + }); + + it("handles errors when submitting an answer", async () => { + const gameId = 123; + const answerId = "a1"; + + mockAxios.onPost(process.env.REACT_APP_API_ENDPOINT + `/games/${gameId}/answer`, { answer_id: answerId }).replyOnce( + HttpStatusCode.InternalServerError + ); + + const result = await answerQuestion(gameId, answerId); + + expect(result).toBeUndefined(); + }); + }); + + describe("getGameDetails function", () => { + it("successfully retrieves game details", async () => { + const gameId = 123; + const mockResponse = { gameId: 123, status: "started" }; + + mockAxios.onGet(process.env.REACT_APP_API_ENDPOINT + `/games/${gameId}/details`).replyOnce( + HttpStatusCode.Ok, + mockResponse + ); + + const result = await getGameDetails(gameId); + + expect(result).toEqual(mockResponse); + }); + + it("handles errors when retrieving game details", async () => { + const gameId = 123; + + mockAxios.onGet(process.env.REACT_APP_API_ENDPOINT + `/games/${gameId}/details`).replyOnce( + HttpStatusCode.NotFound + ); + + const result = await getGameDetails(gameId); + + expect(result).toBeUndefined(); + }); + }); +});