From 0b467300bfd490ce5f8b32f05d65c4e4946ffe05 Mon Sep 17 00:00:00 2001 From: sergiorodriguezgarcia <113514397+sergiorodriguezgarcia@users.noreply.github.com> Date: Mon, 22 Apr 2024 17:10:40 +0200 Subject: [PATCH 1/9] feat: Adding descriptive error messages --- webapp/public/locales/en/translation.json | 6 +++--- webapp/public/locales/es/translation.json | 6 +++--- webapp/src/components/auth/AuthManager.js | 17 +++-------------- webapp/src/pages/Login.jsx | 19 +++++++++++++++++-- webapp/src/pages/Signup.jsx | 9 +++++---- 5 files changed, 31 insertions(+), 26 deletions(-) diff --git a/webapp/public/locales/en/translation.json b/webapp/public/locales/en/translation.json index b62b16d6..5871cfa4 100644 --- a/webapp/public/locales/en/translation.json +++ b/webapp/public/locales/en/translation.json @@ -40,11 +40,11 @@ "login-send": "Your email or password are not found in our database", "validation": { "type": "Validation Error: ", - "message": "Incorrect data" + "message": "Invalid email format" }, "conflict": { "type": "Conflict Error: ", - "message": "User already exists" + "message": "Invalid email format or credentials (username or email) already in use" }, "unknown": { "type": "Unknown Error", @@ -52,7 +52,7 @@ }, "authorized": { "type": "Authorization Error: ", - "message": "Incorrect password" + "message": "Invalid email or password, check for them to be correct" } }, "rules": { diff --git a/webapp/public/locales/es/translation.json b/webapp/public/locales/es/translation.json index 8cfdd594..32c37486 100644 --- a/webapp/public/locales/es/translation.json +++ b/webapp/public/locales/es/translation.json @@ -40,11 +40,11 @@ "login-send": "Tu email o contraseña no se encuentran en nuestra base de datos", "validation": { "type": "Error de Validación: ", - "message": "Datos incorrectos" + "message": "El formato del correo electrónico no es correcto" }, "conflict": { "type": "Error de Conflicto: ", - "message": "El usuario ya existe" + "message": "Formato de correo electrónico no válido o credenciales (nombre de usuario o correo electrónico) ya en uso" }, "unknown": { "type": "Error Desconocido", @@ -52,7 +52,7 @@ }, "authorized": { "type": "Error de Autorización: ", - "message": "Contraseña incorrecta" + "message": "Correo electrónico o contraseña no válidos, verifique que sean correctos" } }, "rules": { diff --git a/webapp/src/components/auth/AuthManager.js b/webapp/src/components/auth/AuthManager.js index b94db36e..09699b00 100644 --- a/webapp/src/components/auth/AuthManager.js +++ b/webapp/src/components/auth/AuthManager.js @@ -5,6 +5,7 @@ export default class AuthManager { static #instance = null; #isLoggedIn = false; #axiosInstance = null; + constructor() { if (!AuthManager.#instance) { @@ -46,7 +47,7 @@ export default class AuthManager { throw requestAnswer; } } catch (error) { - onError(error); + onError(error); } } @@ -100,19 +101,7 @@ export default class AuthManager { throw requestAnswer; } } catch (error) { - let errorType; - switch (error.response ? error.response.status : null) { - case 400: - errorType = { type: "error.validation.type", message: "error.validation.message"}; - break; - case 409: - errorType = { type: "error.conflict.type", message: "error.conflict.message"}; - break; - default: - errorType = { type: "error.unknown.type", message: "error.unknown.message"}; - break; - } - onError(errorType); + onError(error); } } } diff --git a/webapp/src/pages/Login.jsx b/webapp/src/pages/Login.jsx index 96d47505..8e2b9a86 100644 --- a/webapp/src/pages/Login.jsx +++ b/webapp/src/pages/Login.jsx @@ -46,9 +46,24 @@ export default function Login() { "password": password }; try { - await new AuthManager().login(loginData, navigateToDashboard, setErrorMessage); + await new AuthManager().login(loginData, navigateToDashboard, setLocalizedErrorMessage); } catch { - setErrorMessage("Error desconocido"); + const message = { type: t("error.login"), message: t("error.login-desc")}; + setErrorMessage(message); + } + } + + const setLocalizedErrorMessage = (error) => { + switch (error.response.status) { + case 400: + setErrorMessage({ type: t("error.validation.type"), message: t("error.validation.message")}); + break; + case 401: + setErrorMessage({ type: t("error.authorized.type"), message: t("error.authorized.message")}); + break; + default: + setErrorMessage({ type: t("error.login"), message: t("error.login-desc")}); + break; } } diff --git a/webapp/src/pages/Signup.jsx b/webapp/src/pages/Signup.jsx index 192a4f61..9708f9bc 100644 --- a/webapp/src/pages/Signup.jsx +++ b/webapp/src/pages/Signup.jsx @@ -41,20 +41,21 @@ export default function Signup() { try { await new AuthManager().register(registerData, navigateToDashboard, setLocalizedErrorMessage); } catch { - setErrorMessage("Error desconocido"); + const message = { type: t("error.register"), message: t("error.register-desc")}; + setErrorMessage(message); } }; const setLocalizedErrorMessage = (error) => { - switch (error.response ? error.response.status : null) { + switch (error.response.status) { case 400: - setErrorMessage({ type: t("error.validation.type"), message: t("error.validation.message")}); + setErrorMessage({ type: t("error.conflict.type"), message: t("error.conflict.message")}); break; case 401: setErrorMessage({ type: t("error.authorized.type"), message: t("error.authorized.message")}); break; default: - setErrorMessage({ type: t("error.unknown.type"), message: t("error.unknown.message")}); + setErrorMessage({ type: t("error.register"), message: t("error.register-desc")}); break; } } From 0a6b8b2e30096045a25e68d4df46cdb3892d3b09 Mon Sep 17 00:00:00 2001 From: sergiorodriguezgarcia <113514397+sergiorodriguezgarcia@users.noreply.github.com> Date: Mon, 22 Apr 2024 18:09:09 +0200 Subject: [PATCH 2/9] feat: Adding colorMode button and first configuration --- webapp/src/components/menu/LateralMenu.jsx | 8 ++++++-- webapp/src/index.js | 3 ++- webapp/src/styles/theme.js | 2 ++ 3 files changed, 10 insertions(+), 3 deletions(-) diff --git a/webapp/src/components/menu/LateralMenu.jsx b/webapp/src/components/menu/LateralMenu.jsx index f51a592b..833b3694 100644 --- a/webapp/src/components/menu/LateralMenu.jsx +++ b/webapp/src/components/menu/LateralMenu.jsx @@ -2,9 +2,9 @@ import React, { useState, useEffect } from 'react'; import PropTypes from 'prop-types'; import { useNavigate } from "react-router-dom"; import { useTranslation } from "react-i18next"; -import { Box, Drawer, DrawerOverlay, DrawerContent, DrawerCloseButton, DrawerHeader, DrawerBody, DrawerFooter, Select, Button, Text, IconButton, Flex, Image } from '@chakra-ui/react'; +import { Box, Drawer, DrawerOverlay, DrawerContent, DrawerCloseButton, DrawerHeader, DrawerBody, DrawerFooter, Select, Button, Text, IconButton, Flex, Image, useColorMode } from '@chakra-ui/react'; import { FaChartBar, FaBook, FaTachometerAlt } from 'react-icons/fa'; -import { InfoIcon, SettingsIcon } from '@chakra-ui/icons'; +import { InfoIcon, SettingsIcon, SunIcon, MoonIcon } from '@chakra-ui/icons'; import AuthManager from "components/auth/AuthManager"; @@ -13,6 +13,7 @@ const LateralMenu = ({ isOpen, onClose, changeLanguage, isDashboard }) => { const [selectedLanguage, setSelectedLanguage] = useState(''); const [isLoggedIn, setIsLoggedIn] = useState(false); const { t, i18n } = useTranslation(); + const { colorMode, toggleColorMode } = useColorMode(); useEffect(() => { checkIsLoggedIn(); @@ -55,6 +56,9 @@ const LateralMenu = ({ isOpen, onClose, changeLanguage, isDashboard }) => { App icon KIWIQ + { + colorMode === "light" ? } aria-label={'DarkMode'} /> : } aria-label={'LightMode'} /> + } diff --git a/webapp/src/index.js b/webapp/src/index.js index 0b32c148..6ad4954f 100644 --- a/webapp/src/index.js +++ b/webapp/src/index.js @@ -4,7 +4,7 @@ import './index.css'; import reportWebVitals from './reportWebVitals'; import {createBrowserRouter, RouterProvider} from 'react-router-dom'; import router from 'components/Router'; -import { ChakraProvider } from '@chakra-ui/react'; +import { ChakraProvider, ColorModeScript } from '@chakra-ui/react'; import "./i18n"; import theme from "./styles/theme"; @@ -13,6 +13,7 @@ const browserRouter = createBrowserRouter(router); root.render( + ); diff --git a/webapp/src/styles/theme.js b/webapp/src/styles/theme.js index c7126e63..85e00e36 100644 --- a/webapp/src/styles/theme.js +++ b/webapp/src/styles/theme.js @@ -117,5 +117,7 @@ const theme = extendTheme({ } }, }, + initialColorMode: 'system', + useSystemColorMode: true, }); export default theme; \ No newline at end of file From a502223348610367cc5e7353c2c98a206d7c2a07 Mon Sep 17 00:00:00 2001 From: Sergio Rodriguez Date: Tue, 23 Apr 2024 11:14:59 +0200 Subject: [PATCH 3/9] feat: Deleting the first config of colorMode --- webapp/src/components/menu/LateralMenu.jsx | 8 ++------ webapp/src/index.js | 3 +-- webapp/src/styles/theme.js | 2 -- 3 files changed, 3 insertions(+), 10 deletions(-) diff --git a/webapp/src/components/menu/LateralMenu.jsx b/webapp/src/components/menu/LateralMenu.jsx index 833b3694..f51a592b 100644 --- a/webapp/src/components/menu/LateralMenu.jsx +++ b/webapp/src/components/menu/LateralMenu.jsx @@ -2,9 +2,9 @@ import React, { useState, useEffect } from 'react'; import PropTypes from 'prop-types'; import { useNavigate } from "react-router-dom"; import { useTranslation } from "react-i18next"; -import { Box, Drawer, DrawerOverlay, DrawerContent, DrawerCloseButton, DrawerHeader, DrawerBody, DrawerFooter, Select, Button, Text, IconButton, Flex, Image, useColorMode } from '@chakra-ui/react'; +import { Box, Drawer, DrawerOverlay, DrawerContent, DrawerCloseButton, DrawerHeader, DrawerBody, DrawerFooter, Select, Button, Text, IconButton, Flex, Image } from '@chakra-ui/react'; import { FaChartBar, FaBook, FaTachometerAlt } from 'react-icons/fa'; -import { InfoIcon, SettingsIcon, SunIcon, MoonIcon } from '@chakra-ui/icons'; +import { InfoIcon, SettingsIcon } from '@chakra-ui/icons'; import AuthManager from "components/auth/AuthManager"; @@ -13,7 +13,6 @@ const LateralMenu = ({ isOpen, onClose, changeLanguage, isDashboard }) => { const [selectedLanguage, setSelectedLanguage] = useState(''); const [isLoggedIn, setIsLoggedIn] = useState(false); const { t, i18n } = useTranslation(); - const { colorMode, toggleColorMode } = useColorMode(); useEffect(() => { checkIsLoggedIn(); @@ -56,9 +55,6 @@ const LateralMenu = ({ isOpen, onClose, changeLanguage, isDashboard }) => { App icon KIWIQ - { - colorMode === "light" ? } aria-label={'DarkMode'} /> : } aria-label={'LightMode'} /> - } diff --git a/webapp/src/index.js b/webapp/src/index.js index 6ad4954f..0b32c148 100644 --- a/webapp/src/index.js +++ b/webapp/src/index.js @@ -4,7 +4,7 @@ import './index.css'; import reportWebVitals from './reportWebVitals'; import {createBrowserRouter, RouterProvider} from 'react-router-dom'; import router from 'components/Router'; -import { ChakraProvider, ColorModeScript } from '@chakra-ui/react'; +import { ChakraProvider } from '@chakra-ui/react'; import "./i18n"; import theme from "./styles/theme"; @@ -13,7 +13,6 @@ const browserRouter = createBrowserRouter(router); root.render( - ); diff --git a/webapp/src/styles/theme.js b/webapp/src/styles/theme.js index 85e00e36..c7126e63 100644 --- a/webapp/src/styles/theme.js +++ b/webapp/src/styles/theme.js @@ -117,7 +117,5 @@ const theme = extendTheme({ } }, }, - initialColorMode: 'system', - useSystemColorMode: true, }); export default theme; \ No newline at end of file From dbe67ff50b1af51d4fa3701badf7a920320ef70d Mon Sep 17 00:00:00 2001 From: sergiorodriguezgarcia <113514397+sergiorodriguezgarcia@users.noreply.github.com> Date: Tue, 23 Apr 2024 16:16:27 +0200 Subject: [PATCH 4/9] feat: Adding more tests for the Login page --- webapp/src/tests/Login.test.js | 31 +++++++++++++++++++++++++++++-- 1 file changed, 29 insertions(+), 2 deletions(-) diff --git a/webapp/src/tests/Login.test.js b/webapp/src/tests/Login.test.js index 84459963..abf22641 100644 --- a/webapp/src/tests/Login.test.js +++ b/webapp/src/tests/Login.test.js @@ -1,5 +1,5 @@ import React from 'react'; -import { render, fireEvent, waitFor, act } from '@testing-library/react'; +import { render, fireEvent, waitFor } from '@testing-library/react'; import '@testing-library/jest-dom/extend-expect'; import { MemoryRouter } from 'react-router'; import Login from '../pages/Login'; @@ -8,7 +8,6 @@ import MockAdapter from 'axios-mock-adapter'; import { HttpStatusCode } from 'axios'; import { ChakraProvider } from '@chakra-ui/react'; import theme from '../styles/theme'; -import Signup from 'pages/Signup'; jest.mock('react-router-dom', () => ({ ...jest.requireActual('react-router-dom'), @@ -87,4 +86,32 @@ describe('Login Component', () => { expect(getByTestId('error-message')).toBeInTheDocument(); }); }); + + it('renders button "GoBack"', () => { + const { getByTestId } = render( + + + + + + ); + const goBackButton = getByTestId('GoBack'); + expect(goBackButton).toBeInTheDocument(); + }); + + it('displays error message on failed format login attempt', async () => { + mockAxios.onPost().replyOnce(HttpStatusCode.BadRequest); + const { getByPlaceholderText, getByTestId } = render(); + const emailInput = getByPlaceholderText('session.email'); + const passwordInput = getByPlaceholderText('session.password'); + const loginButton = getByTestId('Login'); + + fireEvent.change(emailInput, { target: { value: 'test' } }); + fireEvent.change(passwordInput, { target: { value: 'test' } }); + fireEvent.click(loginButton); + + await waitFor(() => { + expect(getByTestId('error-message')).toBeInTheDocument(); + }); + }); }); \ No newline at end of file From 42c7cb335e47cfeb873e345b9f108235a0127ec4 Mon Sep 17 00:00:00 2001 From: sergiorodriguezgarcia <113514397+sergiorodriguezgarcia@users.noreply.github.com> Date: Tue, 23 Apr 2024 16:20:59 +0200 Subject: [PATCH 5/9] feat: Adding more tests for the Signup page --- webapp/src/tests/Signup.test.js | 43 ++++++++++++++++++++++++++++++++- 1 file changed, 42 insertions(+), 1 deletion(-) diff --git a/webapp/src/tests/Signup.test.js b/webapp/src/tests/Signup.test.js index 9b2112b6..550fe67a 100644 --- a/webapp/src/tests/Signup.test.js +++ b/webapp/src/tests/Signup.test.js @@ -4,9 +4,14 @@ import { MemoryRouter } from 'react-router'; import Signup from '../pages/Signup'; import { ChakraProvider } from '@chakra-ui/react'; import theme from '../styles/theme'; -import MockAdapter from 'axios-mock-adapter'; import AuthManager from 'components/auth/AuthManager'; import { HttpStatusCode } from 'axios'; +import MockAdapter from 'axios-mock-adapter'; + +jest.mock('react-router-dom', () => ({ + ...jest.requireActual('react-router-dom'), + useNavigate: jest.fn(), +})); jest.mock('react-i18next', () => ({ useTranslation: () => { @@ -19,8 +24,17 @@ jest.mock('react-i18next', () => ({ }, })); +const authManager = new AuthManager(); +let mockAxios = new MockAdapter(authManager.getAxiosInstance()); + describe('Signup Component', () => { + beforeEach(() => { + authManager.reset(); + jest.clearAllMocks(); + mockAxios = new MockAdapter(authManager.getAxiosInstance()); + }); + it('renders form elements correctly', () => { const { getByPlaceholderText } = render(); @@ -74,4 +88,31 @@ describe('Signup Component', () => { expect(confirmPasswordInput.value).toBe('newPassword'); }); + it('displays error message on failed login attempt', async () => { + mockAxios.onPost().replyOnce(HttpStatusCode.BadRequest); + const { getByPlaceholderText, getByTestId } = render(); + const emailInput = getByPlaceholderText('session.email'); + const passwordInput = getByPlaceholderText('session.password'); + const loginButton = getByTestId('Sign up'); + + fireEvent.change(emailInput, { target: { value: 'test@example.com' } }); + fireEvent.change(passwordInput, { target: { value: 'password123' } }); + fireEvent.click(loginButton); + + await waitFor(() => { + expect(getByTestId('error-message')).toBeInTheDocument(); + }); + }); + + it('renders button "GoBack"', () => { + const { getByTestId } = render( + + + + + + ); + const goBackButton = getByTestId('GoBack'); + expect(goBackButton).toBeInTheDocument(); + }); }); \ No newline at end of file From ca2557be3561d86c8788f30ae4aa8bb91a925bbb Mon Sep 17 00:00:00 2001 From: sergiorodriguezgarcia <113514397+sergiorodriguezgarcia@users.noreply.github.com> Date: Tue, 23 Apr 2024 16:37:24 +0200 Subject: [PATCH 6/9] feat: Adding more tests to complete the coverage --- webapp/src/tests/Login.test.js | 16 ++++++++++++++++ webapp/src/tests/Signup.test.js | 30 +++++++++++++++++------------- 2 files changed, 33 insertions(+), 13 deletions(-) diff --git a/webapp/src/tests/Login.test.js b/webapp/src/tests/Login.test.js index abf22641..a2fa1a88 100644 --- a/webapp/src/tests/Login.test.js +++ b/webapp/src/tests/Login.test.js @@ -114,4 +114,20 @@ describe('Login Component', () => { expect(getByTestId('error-message')).toBeInTheDocument(); }); }); + + it('displays error message on unauthorized login attempt', async () => { + mockAxios.onPost().replyOnce(HttpStatusCode.Unauthorized); + const { getByPlaceholderText, getByTestId } = render(); + const emailInput = getByPlaceholderText('session.email'); + const passwordInput = getByPlaceholderText('session.password'); + const loginButton = getByTestId('Login'); + + fireEvent.change(emailInput, { target: { value: 'test@example.com' } }); + fireEvent.change(passwordInput, { target: { value: 'test' } }); + fireEvent.click(loginButton); + + await waitFor(() => { + expect(getByTestId('error-message')).toBeInTheDocument(); + }); + }); }); \ No newline at end of file diff --git a/webapp/src/tests/Signup.test.js b/webapp/src/tests/Signup.test.js index 550fe67a..18f91ecf 100644 --- a/webapp/src/tests/Signup.test.js +++ b/webapp/src/tests/Signup.test.js @@ -88,31 +88,35 @@ describe('Signup Component', () => { expect(confirmPasswordInput.value).toBe('newPassword'); }); - it('displays error message on failed login attempt', async () => { + it('displays error message on failed register attempt', async () => { mockAxios.onPost().replyOnce(HttpStatusCode.BadRequest); const { getByPlaceholderText, getByTestId } = render(); const emailInput = getByPlaceholderText('session.email'); const passwordInput = getByPlaceholderText('session.password'); - const loginButton = getByTestId('Sign up'); + const registerButton = getByTestId('Sign up'); fireEvent.change(emailInput, { target: { value: 'test@example.com' } }); fireEvent.change(passwordInput, { target: { value: 'password123' } }); - fireEvent.click(loginButton); + fireEvent.click(registerButton); await waitFor(() => { expect(getByTestId('error-message')).toBeInTheDocument(); }); }); - it('renders button "GoBack"', () => { - const { getByTestId } = render( - - - - - - ); - const goBackButton = getByTestId('GoBack'); - expect(goBackButton).toBeInTheDocument(); + it('displays error message on unauthorized register attempt', async () => { + mockAxios.onPost().replyOnce(HttpStatusCode.Unauthorized); + const { getByPlaceholderText, getByTestId } = render(); + const emailInput = getByPlaceholderText('session.email'); + const passwordInput = getByPlaceholderText('session.password'); + const registerButton = getByTestId('Sign up'); + + fireEvent.change(emailInput, { target: { value: 'test@example.com' } }); + fireEvent.change(passwordInput, { target: { value: 'test' } }); + fireEvent.click(registerButton); + + await waitFor(() => { + expect(getByTestId('error-message')).toBeInTheDocument(); + }); }); }); \ No newline at end of file From 296161e9c5133134a176636c8db161fe9c6a2b89 Mon Sep 17 00:00:00 2001 From: Diego Villanueva Date: Wed, 24 Apr 2024 00:46:09 +0200 Subject: [PATCH 7/9] Chore: Added country flag questions --- .../questions/answer/AnswerCategory.java | 2 +- questiongenerator/src/main/java/Main.java | 5 ++ .../src/main/java/model/AnswerCategory.java | 2 +- .../java/templates/CountryFlagQuestion.java | 77 +++++++++++++++++++ 4 files changed, 84 insertions(+), 2 deletions(-) create mode 100644 questiongenerator/src/main/java/templates/CountryFlagQuestion.java diff --git a/api/src/main/java/lab/en2b/quizapi/questions/answer/AnswerCategory.java b/api/src/main/java/lab/en2b/quizapi/questions/answer/AnswerCategory.java index cc8239b0..f74bd1b0 100644 --- a/api/src/main/java/lab/en2b/quizapi/questions/answer/AnswerCategory.java +++ b/api/src/main/java/lab/en2b/quizapi/questions/answer/AnswerCategory.java @@ -1,6 +1,6 @@ package lab.en2b.quizapi.questions.answer; public enum AnswerCategory { - CAPITAL_CITY, COUNTRY, SONG, STADIUM, BALLON_DOR, GAMES_PUBLISHER, PAINTING, WTPOKEMON, GAMES_COUNTRY, GAMES_GENRE, BASKETBALL_VENUE + CAPITAL_CITY, COUNTRY, SONG, STADIUM, BALLON_DOR, GAMES_PUBLISHER, PAINTING, WTPOKEMON, GAMES_COUNTRY, GAMES_GENRE, BASKETBALL_VENUE, COUNTRY_FLAG } diff --git a/questiongenerator/src/main/java/Main.java b/questiongenerator/src/main/java/Main.java index e9f76e02..3b7a256b 100644 --- a/questiongenerator/src/main/java/Main.java +++ b/questiongenerator/src/main/java/Main.java @@ -55,6 +55,11 @@ public static void main(String[] args) { new WhosThatPokemonQuestion("es"); } + if (GeneralRepositoryStorer.doesntExist(AnswerCategory.COUNTRY_FLAG)) { + new CountryFlagQuestion("en"); + new CountryFlagQuestion("es"); + } + /* // VIDEOS not yet supported if(GeneralRepositoryStorer.doesntExist(AnswerCategory.SONG.toString())) { diff --git a/questiongenerator/src/main/java/model/AnswerCategory.java b/questiongenerator/src/main/java/model/AnswerCategory.java index 7de719bb..c4b0f5a1 100644 --- a/questiongenerator/src/main/java/model/AnswerCategory.java +++ b/questiongenerator/src/main/java/model/AnswerCategory.java @@ -1,6 +1,6 @@ package model; public enum AnswerCategory { - CAPITAL_CITY, COUNTRY, SONG, STADIUM, BALLON_DOR, GAMES_PUBLISHER, PAINTING, WTPOKEMON, GAMES_COUNTRY, GAMES_GENRE, BASKETBALL_VENUE + CAPITAL_CITY, COUNTRY, SONG, STADIUM, BALLON_DOR, GAMES_PUBLISHER, PAINTING, WTPOKEMON, GAMES_COUNTRY, GAMES_GENRE, BASKETBALL_VENUE, COUNTRY_FLAG } diff --git a/questiongenerator/src/main/java/templates/CountryFlagQuestion.java b/questiongenerator/src/main/java/templates/CountryFlagQuestion.java new file mode 100644 index 00000000..3b254737 --- /dev/null +++ b/questiongenerator/src/main/java/templates/CountryFlagQuestion.java @@ -0,0 +1,77 @@ +package templates; + +import model.QuestionCategory; +import model.QuestionType; +import model.Answer; +import model.AnswerCategory; +import model.Question; +import org.json.JSONObject; + +import java.util.ArrayList; +import java.util.List; + +public class CountryFlagQuestion extends QuestionTemplate { + + private static final String[] spanishStringsIni = {"¿Que país tiene esta bandera? ", "¿A qué país pertenece esta bandera? ", "¿De qué país es esta bandera? ", "¿Cuál es el país de esta bandera? "}; + private static final String[] englishStringsIni= {"Which country has this flag? ", "To which country belongs this flag? ", "From which country is this flag? ", "What is the country represented by this flag? "}; + + List countryLabels; + + public CountryFlagQuestion(String langCode) { + super(langCode); + } + + @Override + public void setQuery() { + this.sparqlQuery = "SELECT ?countryLabel ?flagLabel\n" + + "WHERE " + + "{ " + + " ?country wdt:P31 wd:Q6256; " + + " wdt:P41 ?flag. " + + " SERVICE wikibase:label { bd:serviceParam wikibase:language \"" + langCode + "\". } " + + "}"; + } + + @Override + public void processResults() { + countryLabels = new ArrayList<>(); + List questions = new ArrayList<>(); + List answers = new ArrayList<>(); + + for (int i = 0; i < results.length(); i++) { + JSONObject result = results.getJSONObject(i); + String countryLabel = result.getJSONObject("countryLabel").getString("value"); + String flagLabel = result.getJSONObject("flagLabel").getString("value"); + + if (needToSkip(countryLabel, flagLabel)) { + continue; + } + + Answer a = new Answer(countryLabel, AnswerCategory.COUNTRY_FLAG, langCode); + answers.add(a); + + if (langCode.equals("es")){ + String questionString = spanishStringsIni[i%4] + QGHelper.LINKCONCAT + flagLabel; + questions.add(new Question(a, questionString, QuestionCategory.GEOGRAPHY, QuestionType.IMAGE)); + } else { + String questionString = englishStringsIni[i%4] + QGHelper.LINKCONCAT + flagLabel; + questions.add(new Question(a, questionString, QuestionCategory.GEOGRAPHY, QuestionType.IMAGE)); + } + } + repository.saveAll(new ArrayList<>(answers)); + repository.saveAll(new ArrayList<>(questions)); + } + + private boolean needToSkip(String countryLabel, String venueLabel){ + if (countryLabels.contains(countryLabel)) { + return true; + } + countryLabels.add(countryLabel); + + if (QGHelper.isEntityName(countryLabel) || QGHelper.isEntityName(venueLabel)) { + return true; + } + + return false; + } +} From 60c5883942a40c1e9aec10cdbeff510215d83984 Mon Sep 17 00:00:00 2001 From: jjgancfer Date: Wed, 24 Apr 2024 20:11:39 +0200 Subject: [PATCH 8/9] 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 9/9] 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