diff --git a/gatewayservice/gateway-service.js b/gatewayservice/gateway-service.js index 009db707..317c55ae 100644 --- a/gatewayservice/gateway-service.js +++ b/gatewayservice/gateway-service.js @@ -58,9 +58,39 @@ app.get('/questions', async (req, res) => { } }); +app.get('/questions/:lang/:amount/:type', async (req, res) => { + try { + const lang = req.params.lang.toString(); + const amount = req.params.amount.toString(); + const type = req.params.type.toString(); + // Forward the question request to the quetion service + const questionResponse = await axios.get(questionServiceUrl+'/questions/' + lang + '/' + amount + '/' + type); + + res.json(questionResponse.data); + } catch (error) { + + res.status(error.response.status).json({ error: error.response.data.error }); + } +}); + + +app.get('/questions/:lang/:amount', async (req, res) => { + try { + const lang = req.params.lang.toString(); + const amount = req.params.amount.toString(); + // Forward the question request to the quetion service + const questionResponse = await axios.get(questionServiceUrl+'/questions/' + lang + '/' + amount); + + res.json(questionResponse.data); + } catch (error) { + + res.status(error.response.status).json({ error: error.response.data.error }); + } +}); + app.get('/questions/:lang', async (req, res) => { try { - const lang = req.params.lang; + const lang = req.params.lang.toString(); // Forward the question request to the quetion service const questionResponse = await axios.get(questionServiceUrl+'/questions/' + lang); @@ -81,6 +111,27 @@ app.post('/record', async(req, res) => { } }); +app.get('/record/ranking/top10', async(req, res)=>{ + try { + // Forward the record request to the record service + const recordResponse = await axios.get(recordServiceUrl + '/record/ranking/top10'); + res.json(recordResponse.data); + } catch (error) { + res.send(error); + } +}); + +app.get('/record/ranking/:user', async(req, res)=>{ + try { + const user = req.params.user; + // Forward the record request to the record service + const recordResponse = await axios.get(recordServiceUrl + '/record/ranking/' + user); + res.json(recordResponse.data); + } catch (error) { + res.send(error); + } +}); + app.get('/record/:user', async(req, res)=>{ try { const user = req.params.user; diff --git a/gatewayservice/gateway-service.test.js b/gatewayservice/gateway-service.test.js index bfed664f..971fead9 100644 --- a/gatewayservice/gateway-service.test.js +++ b/gatewayservice/gateway-service.test.js @@ -20,16 +20,28 @@ describe('Gateway Service', () => { } }); + const question = { data: [{question: "¿Cuál es la población de Oviedo?", + answers: ["225089","272357","267855","231841"]}] }; + + //Dont need to check a good record just that it redirects the call + const record = {data : {record:'undefined'}}; + axios.get.mockImplementation((url, data) => { if (url.endsWith('/questions')){ - return Promise.resolve({ data: [{question: "¿Cuál es la población de Oviedo?", - answers: ["225089","272357","267855","231841"]}] }); + return Promise.resolve(question); + } else if (url.endsWith('/questions/es/1/CAPITAL')){ + return Promise.resolve(question); + } else if (url.endsWith('/questions/es/1')){ + return Promise.resolve(question); } else if (url.endsWith('/questions/es')){ - return Promise.resolve({ data: [{question: "¿Cuál es la población de Oviedo?", - answers: ["225089","272357","267855","231841"]}] }); + return Promise.resolve(question); + } else if(url.endsWith('/record/testuser')){ - //Dont need to check a good record just that it redirects the call - return Promise.resolve({data : {record:'undefined'}}) + return Promise.resolve(record) + } else if(url.endsWith('/record/ranking/top10')){ + return Promise.resolve(record) + } else if(url.endsWith('/record/ranking/testuser')){ + return Promise.resolve(record) } }); @@ -59,8 +71,7 @@ describe('Gateway Service', () => { const response = await request(app) .get('/questions'); - expect(response.statusCode).toBe(200); - expect(response.body[0]).toHaveProperty('question', "¿Cuál es la población de Oviedo?"); + checkQuestion(response); }); // Test /questions/:lang endpoint @@ -68,8 +79,23 @@ describe('Gateway Service', () => { const response = await request(app) .get('/questions/es'); - expect(response.statusCode).toBe(200); - expect(response.body[0]).toHaveProperty('question', "¿Cuál es la población de Oviedo?"); + checkQuestion(response); + }); + + // Test /questions/:lang/:amount endpoint + it('should forward questions request to question service', async () => { + const response = await request(app) + .get('/questions/es/1'); + + checkQuestion(response); + }); + + // Test /questions/:lang/:amount/:type endpoint + it('should forward questions request to question service', async () => { + const response = await request(app) + .get('/questions/es/1/CAPITAL'); + + checkQuestion(response); }); // Test /record endpoint @@ -86,7 +112,32 @@ describe('Gateway Service', () => { const response = await request(app) .get('/record/testuser'); - expect(response.statusCode).toBe(200); - expect(response.body).toHaveProperty('record', "undefined"); + checkRecord(response); + }); + + // Test /record/ranking/:user endpoint + it('should forward record request to record service', async () => { + const response = await request(app) + .get('/record/ranking/testuser'); + + checkRecord(response); }); -}); \ No newline at end of file + + // Test /record/ranking/top10 endpoint + it('should forward record request to record service', async () => { + const response = await request(app) + .get('/record/ranking/top10'); + checkRecord(response); + + }); +}); + +function checkRecord(response){ + expect(response.statusCode).toBe(200); + expect(response.body).toHaveProperty('record', "undefined"); +} + +function checkQuestion(response){ + expect(response.statusCode).toBe(200); + expect(response.body[0]).toHaveProperty('question', "¿Cuál es la población de Oviedo?"); +} \ No newline at end of file diff --git a/questionservice/question-service.js b/questionservice/question-service.js index 026feb5c..effc5ac7 100644 --- a/questionservice/question-service.js +++ b/questionservice/question-service.js @@ -30,6 +30,60 @@ app.get('/questions', async (req, res) => { } }); +app.get('/questions/:lang/:amount/:type', async (req, res) => { + try { + const lang = req.params.lang.toString(); + let amount = checkAmount(parseInt(req.params.amount)); + const type = req.params.type.toString(); + + if(amount > 20 || amount < 1) + amount = 5; + + const questions = await Question.aggregate([ + {$match: {language : lang, type: type}}, //Condition + {$sample: {size:amount}} + ]); + + let jsonResult = {}; + for (let i = 0; i < questions.length; i++) { + const question = questions[i]; + jsonResult[i] = { + question : question.question, + answers : question.answers + } + } + res.json(jsonResult); + } catch (error) { + res.status(500).json({ error: 'Internal Server Error' }); + } +}); + +app.get('/questions/:lang/:amount', async (req, res) => { + try { + const lang = req.params.lang; + let amount = checkAmount(parseInt(req.params.amount)); + + + + const questions = await Question.aggregate([ + {$match: {language : lang}}, //Condition + {$sample: {size:amount}} + ]); + + let jsonResult = {}; + for (let i = 0; i < questions.length; i++) { + const question = questions[i]; + jsonResult[i] = { + question : question.question, + answers : question.answers + } + } + res.json(jsonResult); + } catch (error) { + res.status(500).json({ error: 'Internal Server Error' }); + } +}); + app.get('/questions/:lang', async (req, res) => { try { const lang = req.params.lang; @@ -53,6 +107,11 @@ app.get('/questions/:lang', async (req, res) => { } }); +function checkAmount(amount){ + if(amount > 20 || amount < 1) + return 5; + return amount; +} const server = app.listen(port, () => { console.log(`Question Service listening at http://localhost:${port}`); diff --git a/questionservice/question-service.test.js b/questionservice/question-service.test.js index 67b5ca66..35b70eae 100644 --- a/questionservice/question-service.test.js +++ b/questionservice/question-service.test.js @@ -12,7 +12,7 @@ beforeAll(async () => { app = require('./question-service'); //Populate db - for(let i = 0; i < 6 ; i++){ + for(let i = 0; i < 21 ; i++){ const question = new Question( { question: "¿Cuál es la población de Oviedo?", answers: [ @@ -60,4 +60,52 @@ describe('Question Service', () => { expect(response.body[0]).toHaveProperty('question', "¿Cuál es la población de Oviedo?"); expect(Object.keys(response.body).length).toBe(5); }); + + + it('Should give 20 questions /questions/es/20', async () => { + + let response = await request(app).get('/questions/es/20'); + expect(response.status).toBe(200); + expect(response.body[0]).toHaveProperty('question', "¿Cuál es la población de Oviedo?"); + expect(Object.keys(response.body).length).toBe(20); + }); + + it('Should give 1 questions /questions/es/1', async () => { + + let response = await request(app).get('/questions/es/20'); + expect(response.status).toBe(200); + expect(response.body[0]).toHaveProperty('question', "¿Cuál es la población de Oviedo?"); + expect(Object.keys(response.body).length).toBe(20); + }); + + it('Should give 5 questions as the max is 20 /questions/es/21', async () => { + + let response = await request(app).get('/questions/es/21'); + expect(response.status).toBe(200); + expect(response.body[0]).toHaveProperty('question', "¿Cuál es la población de Oviedo?"); + expect(Object.keys(response.body).length).toBe(5); + }); + + it('Should give 5 questions as the min is 1 /questions/es/0', async () => { + + let response = await request(app).get('/questions/es/0'); + expect(response.status).toBe(200); + expect(response.body[0]).toHaveProperty('question', "¿Cuál es la población de Oviedo?"); + expect(Object.keys(response.body).length).toBe(5); + }); + + it('Should give 10 questions /questions/es/10/POPULATION', async () => { + + let response = await request(app).get('/questions/es/10/POPULATION'); + expect(response.status).toBe(200); + expect(response.body[0]).toHaveProperty('question', "¿Cuál es la población de Oviedo?"); + expect(Object.keys(response.body).length).toBe(10); + }); + + it('Should give 0 questions /questions/es/10/CAPITAL', async () => { + + let response = await request(app).get('/questions/es/10/CAPITAL'); + expect(response.status).toBe(200); + expect(Object.keys(response.body).length).toBe(0); + }); }); \ No newline at end of file diff --git a/sonar-project.properties b/sonar-project.properties index 6db6137a..fad83281 100644 --- a/sonar-project.properties +++ b/sonar-project.properties @@ -14,4 +14,5 @@ sonar.coverage.exclusions=**/*.test.js sonar.sources=webapp/src/components,users/authservice,users/userservice,gatewayservice sonar.sourceEncoding=UTF-8 sonar.exclusions=node_modules/** -sonar.javascript.lcov.reportPaths=**/coverage/lcov.info \ No newline at end of file +sonar.javascript.lcov.reportPaths=**/coverage/lcov.info +sonar.cpd.exclusions=**/*.test.js,**/*steps.js,**/*Tests.java diff --git a/users/recordservice/record-model.js b/users/recordservice/record-model.js index a7a30d76..a302df52 100644 --- a/users/recordservice/record-model.js +++ b/users/recordservice/record-model.js @@ -1,16 +1,18 @@ const mongoose = require('mongoose'); +const { Schema } = mongoose; -const recordSchema = new mongoose.Schema({ - user: String, +const recordSchema = new Schema({ + user: { type: String, required: true }, games: [{ questions: [{ - question: String, - answers: [String], - answerGiven: String, - correctAnswer: String + question: { type: String, required: true }, + answers: { type: [String], required: true }, + answerGiven: { type: String, required: true }, + correctAnswer: { type: String, required: true } }], - points: Number, - date: String + points: { type: Number, required: true }, + date: { type: String, required: true }, + competitive: { type: Boolean, required: true } }] }); const Record = mongoose.model('Record', recordSchema); diff --git a/users/recordservice/record-service.js b/users/recordservice/record-service.js index dd383984..cf94544a 100644 --- a/users/recordservice/record-service.js +++ b/users/recordservice/record-service.js @@ -1,8 +1,7 @@ const express = require('express'); const mongoose = require('mongoose'); -const bcrypt = require('bcrypt'); const bodyParser = require('body-parser'); -const Record = require('./record-model') +const Record = require('./record-model'); const app = express(); const port = 8004; @@ -10,6 +9,10 @@ const port = 8004; // Middleware to parse JSON in request body app.use(bodyParser.json()); +var ranking = []; +var lastTime = new Date(); +const minTimeDifferenceInMiliseconds = 120_000; + // Connect to MongoDB const mongoUri = process.env.MONGODB_URI || 'mongodb://localhost:27017/userdb'; mongoose.connect(mongoUri); @@ -18,14 +21,12 @@ mongoose.connect(mongoUri); app.post('/record', async (req, res) => { const user = req.body.user; const game = req.body.game; - console.log(user) - console.log(game) if(user && game){ let record = await Record.findOne({ user : user }); if(record){ //If it exits record.games.push(game); } - else{ //Lo creamos + else{ //We make it record = new Record({ user:user, games:[game] @@ -45,9 +46,34 @@ app.post('/record', async (req, res) => { }); +app.get('/record/ranking/top10', async (req, res) => { + try { + let usersRanking = await getRanking(); + let usersCompetitiveStats = usersRanking.slice(0, 10); + + res.json({usersCompetitiveStats: usersCompetitiveStats }); + } catch (err) { + res.status(500).send(); + } +}); + +app.get('/record/ranking/:user', async (req, res) => { + try { + const user = req.params.user.toString(); + + let usersRanking = await getRanking(); + let userCompetitiveStats = usersRanking.filter(userData => userData._id === user); + + res.json({userCompetitiveStats: userCompetitiveStats[0] }); + } catch (err) { + console.log(err) + res.status(500).send(); + } +}); + app.get('/record/:user', async (req, res) => { try { - const recordFound = await Record.findOne({ user: req.params.user }, 'games'); + const recordFound = await Record.findOne({ user: req.params.user.toString() }, 'games'); if (!recordFound) { res.json({record: "undefined" }); } else { @@ -58,6 +84,37 @@ app.get('/record/:user', async (req, res) => { } }); + +async function getRanking(){ + const nowTime = new Date(); + let timeDifferenceInMiliseconds = nowTime - lastTime; + if(ranking.length == 0 || timeDifferenceInMiliseconds > minTimeDifferenceInMiliseconds){ + ranking = await Record.aggregate([ + // Unwind the games array to work with each game separately + { $unwind: "$games" }, + // Match only competitive games + { $match: { "games.competitive": true } }, + // Group by user and calculate total points and total competitive games per user + { + $group: { + _id: "$user", + totalPoints: { $sum: "$games.points" }, + totalCompetitiveGames: { $sum: 1 } // Count the number of competitive games + } + }, + // Sort by total points in descending order (top 1 will have the highest points) + { $sort: { totalPoints: -1 } } + ]); + + //The operator ... dumps the user json making it {_id , totalPoints, totalCompetitiveGames, position} + ranking = ranking.map((user, index) => ({ ...user, position: index + 1 })); + lastTime = new Date(); + } + + return ranking; + +} + const server = app.listen(port, () => { console.log(`Record Service listening at http://localhost:${port}`); }); diff --git a/users/recordservice/record-service.test.js b/users/recordservice/record-service.test.js index d2c2ca6e..5594f781 100644 --- a/users/recordservice/record-service.test.js +++ b/users/recordservice/record-service.test.js @@ -1,5 +1,6 @@ const request = require('supertest'); const { MongoMemoryServer } = require('mongodb-memory-server'); +const Record = require('./record-model') let mongoServer; let app; @@ -9,6 +10,8 @@ beforeAll(async () => { const mongoUri = mongoServer.getUri(); process.env.MONGODB_URI = mongoUri; app = require('./record-service'); + + await populateDatabase(); }); afterAll(async () => { @@ -99,6 +102,33 @@ describe('Record Service', () => { response = await request(app).post('/record').send(newUser); expect(response.status).toBe(400); + //Data lacks competitive field + newUser = { + user:"testuser", + game: + { + "questions": [ + { + "question": "¿Cuál es el río más largo del mundo?", + "answers": ["Nilo", "Amazonas", "Yangtsé", "Misisipi"], + "answerGiven": "Amazonas", + "correctAnswer": "Amazonas" + }, + { + "question": "¿Cuál es el elemento más abundante en la corteza terrestre?", + "answers": ["Hierro", "Oxígeno", "Silicio", "Aluminio"], + "answerGiven": "Oxígeno", + "correctAnswer": "Oxígeno" + } + ], + "points": 2500, + "date": "02/02/24" + } + }; + + response = await request(app).post('/record').send(newUser); + expect(response.status).toBe(500); + }); it('should add a new record on POST /record', async () => { const newUser = { @@ -120,7 +150,8 @@ describe('Record Service', () => { } ], "points": 2500, - "date": "02/02/24" + "date": "02/02/24", + "competitive": false } }; @@ -161,7 +192,8 @@ describe('Record Service', () => { } ], "points": 3000, - "date": "03/03/24" + "date": "03/03/24", + "competitive": false } }; @@ -174,4 +206,78 @@ describe('Record Service', () => { expect(responseGet.body.record.games[1]).toHaveProperty('date', '03/03/24'); }); -}); \ No newline at end of file + + it('should get back on GET /record/testuser', async () => { + const responseGet = await request(app).get('/record/testuser'); + expect(responseGet.status).toBe(200); + expect(responseGet.body.record.games[0]).toHaveProperty('date', '02/02/24'); + }); + + it('should get back on GET /record/testuser', async () => { + const responseGet = await request(app).get('/record/testuser'); + expect(responseGet.status).toBe(200); + expect(responseGet.body.record.games[0]).toHaveProperty('date', '02/02/24'); + }); + + it('should get back on GET /record/ranking/top10', async () => { + const responseGet = await request(app).get('/record/ranking/top10'); + expect(responseGet.status).toBe(200); + const usersStats = responseGet.body.usersCompetitiveStats; + expect(usersStats.length).toBe(10); //Only top 10 + + //Ordered by points + expect(usersStats[0]).toHaveProperty('_id', 'user10'); + expect(usersStats[9]).toHaveProperty('_id', 'user1'); + + expect(usersStats[0]).toHaveProperty('totalCompetitiveGames', 2); + expect(usersStats[0]).toHaveProperty('totalPoints', 200); + }); + + it('should get back on GET /record/ranking/user1', async () => { + const responseGet = await request(app).get('/record/ranking/user1'); + expect(responseGet.status).toBe(200); + const userStats = responseGet.body.userCompetitiveStats; + + expect(userStats).toHaveProperty('_id', 'user1'); + expect(userStats).toHaveProperty('position', 10); + expect(userStats).toHaveProperty('totalCompetitiveGames', 2); + expect(userStats).toHaveProperty('totalPoints', 20); //i * 10 * totalCompetitiveGames , i = 2 + }); +}); + + + +async function populateDatabase() { + try { + // Generate 10 users + for (let i = 1; i <= 10; i++) { + const user = `user${i}`; + const games = []; + + // Generate 3 games for each user + for (let j = 1; j <= 3; j++) { + const game = { + questions: [ + { + question: `Question ${j} for ${user}`, + answers: ["Answer 1", "Answer 2", "Answer 3", "Answer 4"], + answerGiven: "Answer 1", + correctAnswer: "Answer 1" + } + ], + points: i * 10, + date: "04/01/2024", + competitive: j <= 2 ? true : false // Only 2 games are competitive + }; + games.push(game); + } + + // Guardar el usuario en la base de datos + await Record.create({ user, games }); + } + + console.log('Database populated successfully'); + } catch (error) { + console.error('Error populating database:', error); + } +} \ No newline at end of file diff --git a/webapp/e2e/features/gameMenu.feature b/webapp/e2e/features/gameMenu.feature new file mode 100644 index 00000000..a156fbbc --- /dev/null +++ b/webapp/e2e/features/gameMenu.feature @@ -0,0 +1,8 @@ +Feature: Game Menu page functionality + Scenario: There should be visible three links + Given I am on the game menu + Then three buttons should be visible + Scenario: New Game should go to game configurator + Given I am on the game menu + When I click on New Game + Then I should be in the game configurator diff --git a/webapp/e2e/steps/gameMenu.steps.js b/webapp/e2e/steps/gameMenu.steps.js new file mode 100644 index 00000000..215af2f7 --- /dev/null +++ b/webapp/e2e/steps/gameMenu.steps.js @@ -0,0 +1,48 @@ +const puppeteer = require('puppeteer'); +const { defineFeature, loadFeature } = require('jest-cucumber'); +const setDefaultOptions = require('expect-puppeteer').setDefaultOptions; + +const feature = loadFeature('./features/gameMenu.feature'); + +let page; +let browser; + +defineFeature(feature, test => { + + beforeAll(async () => { + browser = await puppeteer.launch({ + slowMo: 20, + defaultViewport: { width: 1920, height: 1080 }, + args: ['--window-size=1920,1080'] + }); + + page = await browser.newPage(); + setDefaultOptions({ timeout: 10000 }); + }); + + test('There should be visible three links', ({ given, then }) => { + given('I am on the game menu', async () => { + await page.goto('http://localhost:3000/menu'); + await page.waitForSelector('.divMenu'); + }); + + then('three buttons should be visible', async () => { + //await expect(page).toMatchElement('.linkButton'); + const elements = await page.$$('.linkButton'); + expect(elements.length).toBeGreaterThan(0); // At least one element with class 'linkButton' + }); + }); + test('New Game should go to game configurator', ({ given, when, then }) => { + given('I am on the game menu', async () => { + await page.goto('http://localhost:3000/menu'); + await page.waitForSelector('.divMenu'); + }); + when('I click on New Game', async () => { + await page.click('.linkButton'); + }); + then('I should be in the game configurator', async () => { + await expect(page).toMatchElement('.GameConfiguratorDiv'); + }); + }); + +}); diff --git a/webapp/public/favicon.ico b/webapp/public/favicon.ico index a11777cc..5019a7d6 100644 Binary files a/webapp/public/favicon.ico and b/webapp/public/favicon.ico differ diff --git a/webapp/public/index.html b/webapp/public/index.html index aa069f27..a9a639d1 100644 --- a/webapp/public/index.html +++ b/webapp/public/index.html @@ -3,6 +3,7 @@ + { + document.title = 'WIQ'; + }, []); return ( @@ -29,6 +35,8 @@ function App() { } /> } /> } /> + }/> + } /> } /> diff --git a/webapp/src/components/GameConfigurator/GameConfigurator.js b/webapp/src/components/GameConfigurator/GameConfigurator.js new file mode 100644 index 00000000..4a536729 --- /dev/null +++ b/webapp/src/components/GameConfigurator/GameConfigurator.js @@ -0,0 +1,89 @@ +import React, { useState } from 'react'; + +import {useTranslation} from "react-i18next"; +import { Link } from "react-router-dom"; +import QuestionView from '../questionView/QuestionView' +import BackButtonToGameMenu from '../fragments/BackButtonToGameMenu'; + +function GameConfigurator(){ + const [tipoPregunta, setTipoPregunta] = useState('POPULATION'); + const [numeroPreguntas, setNumeroPreguntas] = useState(5); + const [clickedForNewGame, setClickedForNewGame]= useState(false); + const[t] = useTranslation("global"); + + function handleClick() { + setClickedForNewGame(true); + } + + function handleClickRandomize() { + const options = ['ALL', 'POPULATION', 'CAPITAL', 'LANGUAGE', 'SIZE']; + const randomOptionIndex = Math.floor(Math.random() * options.length); + setTipoPregunta(options[randomOptionIndex]); + + const randomNumQuestions = Math.floor(Math.random() * 20) + 1; // Random number between 1 and 20 + setNumeroPreguntas(randomNumQuestions); + } + return ( + clickedForNewGame ? : +
+ +

{t("gameConfigurator.game_config")}

+

{t("gameConfigurator.custo_game")}

+ + + +

+ + + {/* Spinner para seleccionar el número de preguntas */} + setNumeroPreguntas(e.target.value)} + min="1" max="20" + /> +

+ +

+
+

+

{t("gameConfigurator.competi_game")}

+

{t("gameConfigurator.rules_competi")}

+ {/* Botones para jugar un juego personalizado o competitivo */} + + +
+ ); +} + +function ButtonRandomizeCustom({t,handleClick}){ + return ( + + ); + +} + +function ButtonCustomized({t,handleClick}) { + return ( + + ); +} + + +function ButtonCompetitive({t}){ + + return ( + +

{t("gameConfigurator.play_competi")}

+ + ); +} + + +export default GameConfigurator; diff --git a/webapp/src/components/GameConfigurator/GameConfigurator.test.js b/webapp/src/components/GameConfigurator/GameConfigurator.test.js new file mode 100644 index 00000000..273b93c0 --- /dev/null +++ b/webapp/src/components/GameConfigurator/GameConfigurator.test.js @@ -0,0 +1,51 @@ +import React from 'react'; +import { render, screen, fireEvent } from '@testing-library/react'; +import '@testing-library/jest-dom/extend-expect'; // For additional matchers like toBeInTheDocument +import { BrowserRouter as Router } from 'react-router-dom'; +import GameConfigurator from './GameConfigurator'; +import { initReactI18next } from 'react-i18next'; +import i18en from 'i18next'; + + +i18en.use(initReactI18next).init({ + resources: {}, + lng: 'en', + interpolation:{ + escapeValue: false, + } +}); +global.i18en = i18en; + +describe('GameConfigurator', () => { + test('renders GameConfigurator component', () => { + render(); + expect(screen.getByText(i18en.t("gameConfigurator.game_config"))).toBeInTheDocument(); + }); + + + test('updates tipoPregunta state when select value changes', () => { + render(); + const selectElement = screen.getByLabelText(i18en.t("gameConfigurator.type_quest")); + fireEvent.change(selectElement, { target: { value: 'CAPITAL' } }); + expect(selectElement.value).toBe('CAPITAL'); + }); + + test('updates numeroPreguntas state when input value changes', () => { + render(); + const inputElement = screen.getByLabelText(i18en.t("gameConfigurator.num_quest")); + fireEvent.change(inputElement, { target: { value: '10' } }); + expect(inputElement.value).toBe('10'); + }); + it('renders option to play customized game', () => { + render(); + const text = screen.getByText(i18en.t('gameConfigurator.custo_game')); + expect(text).toBeInTheDocument(); +}); + +it('renders option to play Competitive game', () => { + render(); + const text = screen.getByText(i18en.t('gameConfigurator.competi_game')); + expect(text).toBeInTheDocument(); +}); + +}); diff --git a/webapp/src/components/GameMenu/GameMenu.js b/webapp/src/components/GameMenu/GameMenu.js index 7f7bd957..7a1f94ed 100644 --- a/webapp/src/components/GameMenu/GameMenu.js +++ b/webapp/src/components/GameMenu/GameMenu.js @@ -5,12 +5,15 @@ import ButtonHistoricalData from "../HistoricalData/ButtonHistoricalData"; export default function GameMenu() { const[t] = useTranslation("global"); + + return (

{t("gameMenu.title")}

- + +
); } @@ -18,8 +21,19 @@ export default function GameMenu() { function ButtonNewGame({ t }) { return ( - +

{t("gameMenu.new_game_button")}

); - } \ No newline at end of file + } + + function ButtonRanking({ t }) { + return ( + +

{t("gameMenu.view_ranking")}

+ + + ); + } + + \ No newline at end of file diff --git a/webapp/src/components/GameMenu/GameMenu.test.js b/webapp/src/components/GameMenu/GameMenu.test.js index 7d9b7e4d..a52ed153 100644 --- a/webapp/src/components/GameMenu/GameMenu.test.js +++ b/webapp/src/components/GameMenu/GameMenu.test.js @@ -33,6 +33,12 @@ describe('GameMenu component', () => { const text = screen.getByText(i18en.t('gameMenu.history_button')); expect(text).toBeInTheDocument(); }); + + it('renders option to view ranking data', () => { + render(); + const text = screen.getByText(i18en.t('gameMenu.view_ranking')); + expect(text).toBeInTheDocument(); + }); }); diff --git a/webapp/src/components/HistoricalData/HistoricalView.js b/webapp/src/components/HistoricalData/HistoricalView.js index f110430c..66dc8a2d 100644 --- a/webapp/src/components/HistoricalData/HistoricalView.js +++ b/webapp/src/components/HistoricalData/HistoricalView.js @@ -2,7 +2,7 @@ import React, { useState } from 'react'; import {useTranslation} from "react-i18next"; import HistoryRecordRetriever from './HistoryRecordRetriever'; import { useUserContext } from '../loginAndRegistration/UserContext'; - +import BackButtonToGameMenu from '../fragments/BackButtonToGameMenu'; import RecordList from './RecordList'; @@ -29,6 +29,7 @@ export default function HistoricalView() { return (
+ {(records && records.length !== 0) ? records.map((record, index) => ( )):

{t("historicalView.no_games_played")}

} @@ -53,4 +54,5 @@ function HistoricalGameElement({record,t}){
); -} \ No newline at end of file +} + diff --git a/webapp/src/components/HistoricalData/HistoryRecordRetriever.js b/webapp/src/components/HistoricalData/HistoryRecordRetriever.js index d586a867..0e65d6ff 100644 --- a/webapp/src/components/HistoricalData/HistoryRecordRetriever.js +++ b/webapp/src/components/HistoricalData/HistoryRecordRetriever.js @@ -11,15 +11,81 @@ class HistoryRecordRetriever{ try { const response = await axios.get(this.apiUrl + '/' + user); const receivedRecords = await response.data; - console.log(receivedRecords) - console.log(receivedRecords[0]) return receivedRecords.record; } catch (error) { console.log(error) throw new Error(error); } + /* + return { + userId: user, + games: [ + { + questions: [ + { + question: "¿Cuál es la capital de Francia?", + answers: ["Madrid", "París", "Londres", "Roma"], + answerGiven: "París", + correctAnswer: "París" + }, + { + question: "¿En qué año comenzó la Segunda Guerra Mundial?", + answers: ["1939", "1945", "1914", "1941"], + answerGiven: "1939", + correctAnswer: "1939" + }, + { + question: "¿Quién escribió 'Don Quijote de la Mancha'?", + answers: ["Miguel de Cervantes", "Gabriel García Márquez", "Federico García Lorca", "Jorge Luis Borges"], + answerGiven: "Miguel de Cervantes", + correctAnswer: "Miguel de Cervantes" + } + ], + points: 3000, + date: "01/02/24" + }, + { + questions: [ + { + question: "¿Cuál es el río más largo del mundo?", + answers: ["Nilo", "Amazonas", "Yangtsé", "Misisipi"], + answerGiven: "Amazonas", + correctAnswer: "Amazonas" + }, + { + question: "¿Cuál es el elemento más abundante en la corteza terrestre?", + answers: ["Hierro", "Oxígeno", "Silicio", "Aluminio"], + answerGiven: "Oxígeno", + correctAnswer: "Oxígeno" + } + ], + points: 2500, + date: "02/02/24" + }, + { + questions: [ + { + question: "¿Quién pintó la Mona Lisa?", + answers: ["Leonardo da Vinci", "Pablo Picasso", "Vincent van Gogh", "Rembrandt"], + answerGiven: "Leonardo da Vinci", + correctAnswer: "Leonardo da Vinci" + }, + { + question: "¿Cuál es el planeta más grande del sistema solar?", + answers: ["Júpiter", "Saturno", "Neptuno", "Urano"], + answerGiven: "Júpiter", + correctAnswer: "Júpiter" + } + ], + points: 3500, + date: "03/02/24" + } + ] + };*/ } + + } diff --git a/webapp/src/components/fragments/BackButtonToGameMenu.js b/webapp/src/components/fragments/BackButtonToGameMenu.js new file mode 100644 index 00000000..e9bab12c --- /dev/null +++ b/webapp/src/components/fragments/BackButtonToGameMenu.js @@ -0,0 +1,9 @@ +import { Link } from "react-router-dom"; + +export default function BackButton({t}){ + return( + +

⬅ {t("gameMenu.back")}

+ + ); + } \ No newline at end of file diff --git a/webapp/src/components/fragments/BackButtonToGameMenu.test.js b/webapp/src/components/fragments/BackButtonToGameMenu.test.js new file mode 100644 index 00000000..586cf695 --- /dev/null +++ b/webapp/src/components/fragments/BackButtonToGameMenu.test.js @@ -0,0 +1,31 @@ +import { render, screen } from '@testing-library/react'; +import BackButtonToGameMenu from './BackButtonToGameMenu'; +import { MemoryRouter } from 'react-router-dom'; + +import { initReactI18next } from 'react-i18next'; +import i18en from 'i18next'; + +i18en.use(initReactI18next).init({ + resources: {}, + lng: 'en', + interpolation:{ + escapeValue: false, + } +}); +global.i18en = i18en; + + +describe('BackButtonToGameMenu component', () => { + + it('renders option to go back to the game menu', () => { + render(); + const text = screen.getByText((content, element) => { + const regex = new RegExp(i18en.t("gameMenu.back")); + return regex.test(content); + }); + + expect(text).toBeInTheDocument(); + }); +}); + + diff --git a/webapp/src/components/fragments/Loader.js b/webapp/src/components/fragments/Loader.js new file mode 100644 index 00000000..2cd8af7c --- /dev/null +++ b/webapp/src/components/fragments/Loader.js @@ -0,0 +1,15 @@ +import React from 'react'; + +const Loader = () => { + return ( +
+
+
Status
+ +
+
Loading...
+
+ ); +} + +export default Loader; diff --git a/webapp/src/components/questionView/CreationHistoricalRecord.js b/webapp/src/components/questionView/CreationHistoricalRecord.js index 9da8f624..3bdb6370 100644 --- a/webapp/src/components/questionView/CreationHistoricalRecord.js +++ b/webapp/src/components/questionView/CreationHistoricalRecord.js @@ -29,35 +29,36 @@ class CreationHistoricalRecord{ this.record.game.date = date; } + setCompetitive(isCompetitive){ + this.record.game.competitive = isCompetitive; + } + getRecord() { return this.record; } async sendRecord(user) { - const apiUrl = (process.env.REACT_APP_API_ENDPOINT || 'http://localhost:8000') + "/record"; - - const body = { - user:user, - game:this.record.game - } - try { + const apiUrl = (process.env.REACT_APP_API_ENDPOINT || 'http://localhost:8000') + "/record"; + + const body = { + user: user, + game: this.record.game + }; + + try { const response = await axios.post(apiUrl, body, { - headers: { - 'Content-Type': 'application/json' - } + headers: { + 'Content-Type': 'application/json' + } }); - - if (!response.ok) { - throw new Error('Error al enviar el registro'); - } - - const data = await response.json(); - console.log(data); - } catch (error) { - console.error('Error:', error); - } + + + console.log('Registro enviado:', response.data); + } catch (error) { + console.error('Error al enviar el registro:', error.message); } + } } export default CreationHistoricalRecord; diff --git a/webapp/src/components/questionView/QuestionGenerator.js b/webapp/src/components/questionView/QuestionGenerator.js index 32facf1a..9d204004 100644 --- a/webapp/src/components/questionView/QuestionGenerator.js +++ b/webapp/src/components/questionView/QuestionGenerator.js @@ -8,37 +8,42 @@ class QuestionGenerator{ } - async generateQuestions(lang) { - - // try { - // //const response = await fetch(this.apiUrl); - // //const receivedQuestions = await response.json(); + async generateQuestions(lang, type, amount) { + /* + try { + //const response = await fetch(this.apiUrl); + //const receivedQuestions = await response.json(); - // //Mockup - // // console.log("type: "+type+" amount: "+amount) - // const receivedQuestions = JSON.parse('{"0":{"question":"¿Cuál es la población de Oviedo?","answers":["225089","191325","220587","121548"]},'+ - // '"1":{"question":"¿Which is the population of Gijon?","answers":["275274","159658","233982","305554"]},'+ - // '"2":{"question":"¿Cuál es la población de Avilés?","answers":["82568","115595","41284","122200"]},'+ - // '"3":{"question":"¿Cuál es la capital de Asturias?","answers":["Ciudad de Oviedo","a","b","c"]},'+ - // '"4":{"question":"¿Cuál es la capital de España?","answers":["Madrid","a","b","c"]},'+ - // '"5":{"question":"¿Cuál es la capital de Turquía?","answers":["Ankara","a","b","c"]}}') + //Mockup + console.log("type: "+type+" amount: "+amount) + const receivedQuestions = JSON.parse('{"0":{"question":"¿Cuál es la población de Oviedo?","answers":["225089","191325","220587","121548"]},'+ + '"1":{"question":"¿Cuál es la población de Gijón?","answers":["275274","159658","233982","305554"]},'+ + '"2":{"question":"¿Cuál es la población de Avilés?","answers":["82568","115595","41284","122200"]},'+ + '"3":{"question":"¿Cuál es la capital de Asturias?","answers":["Ciudad de Oviedo","a","b","c"]},'+ + '"4":{"question":"¿Cuál es la capital de España?","answers":["Madrid","a","b","c"]},'+ + '"5":{"question":"¿Cuál es la capital de Turquía?","answers":["Ankara","a","b","c"]}}') - // let i = 0; - // var questions = []; - // for (const key in receivedQuestions) { - // questions[i] = new Question(receivedQuestions[key]); - // i += 1; - // } - // console.log(questions); - // return questions; - // } catch (error) { - // throw new Error(error); - // } - + let i = 0; + var questions = []; + for (const key in receivedQuestions) { + questions[i] = new Question(receivedQuestions[key]); + i += 1; + } + console.log(questions); + return questions; + } catch (error) { + throw new Error(error); + } + */ try { - const response = await axios.get(this.apiUrl + '/' + lang); + let response; + if(type==="COMPETITIVE"){ + response = await axios.get(this.apiUrl + '/' + lang); + }else{ + response = await axios.get(this.apiUrl + '/' + lang + '/' +amount + '/' + type); + } const receivedQuestions = await response.data; let i = 0; var questions = []; diff --git a/webapp/src/components/questionView/QuestionView.js b/webapp/src/components/questionView/QuestionView.js index ca598e11..b84d4c4a 100644 --- a/webapp/src/components/questionView/QuestionView.js +++ b/webapp/src/components/questionView/QuestionView.js @@ -9,12 +9,12 @@ import $ from 'jquery'; import RecordList from '../HistoricalData/RecordList'; import ButtonHistoricalData from "../HistoricalData/ButtonHistoricalData"; import { useUserContext } from '../loginAndRegistration/UserContext'; +import BackButtonToGameMenu from '../fragments/BackButtonToGameMenu'; const creationHistoricalRecord = new CreationHistoricalRecord(); const questionGenerator = new QuestionGenerator(); var points = 0; -function QuestionView(){ - +function QuestionView({type= "COMPETITIVE", amount=5}){ const [numQuestion, setnumQuestion] = useState(-1); const [questions, setQuestions] = useState(null); const[t, i18n] = useTranslation("global"); @@ -25,7 +25,7 @@ function QuestionView(){ const generateQuestions = async (numQuestion) => { if (numQuestion < 0) { try { - var generatedQuestions = await questionGenerator.generateQuestions(i18n.language); + var generatedQuestions = await questionGenerator.generateQuestions(i18n.language, type, amount); setQuestions(generatedQuestions); setnumQuestion(0); } catch (error) { @@ -106,6 +106,7 @@ function QuestionView(){ //Last question sends the record if(!(numQuestion < questions.length - 1)){ audio.pause(); + creationHistoricalRecord.setCompetitive(type === 'COMPETITIVE'); creationHistoricalRecord.setDate(Date.now()); creationHistoricalRecord.setPoints(points); creationHistoricalRecord.sendRecord(user.username); @@ -204,6 +205,7 @@ function QuestionComponent({questions, numQuestion, handleClick, t, points, audi <>

{t("questionView.finished_game")}

+

{points} {t("questionView.point")}

    < RecordList record={creationHistoricalRecord.getRecord().game}/>
diff --git a/webapp/src/components/questionView/QuestionView.test.js b/webapp/src/components/questionView/QuestionView.test.js index a3a34531..90248453 100644 --- a/webapp/src/components/questionView/QuestionView.test.js +++ b/webapp/src/components/questionView/QuestionView.test.js @@ -51,9 +51,9 @@ global.i18en = i18en; describe('Question View component', () => { - beforeEach(() => { - mockAxios.reset(); - }); + mockAxios.onGet('http://localhost:8000/questions/en').reply(200, + [{question: "What is the population of Oviedo?", + answers: ["225089","272357","267855","231841"]}]); it('shows the no_questions_message as the endpoint does not exist',async () => { render(); @@ -65,10 +65,6 @@ describe('Question View component', () => { // Test for sound functionality it('speaks the question when the speaker button is clicked', async () => { - const questionText = "What is the population of Oviedo?"; - mockAxios.onGet('http://localhost:8000/questions/en').reply(200, - [{question: questionText, - answers: ["225089","272357","267855","231841"]}]); await act(async () => { render(); @@ -83,10 +79,6 @@ describe('Question View component', () => { }); it('shows a question and answers',async () => { - - mockAxios.onGet('http://localhost:8000/questions/en').reply(200, - [{question: "What is the population of Oviedo?", - answers: ["225089","272357","267855","231841"]}]); //It gives an error as we are not wrapping it by act, however by doing this we simulate a no questions situation await act(async () =>{ @@ -104,9 +96,6 @@ describe('Question View component', () => { }); it('shows colors to reveal correct answer and it sounds', async () => { setupAudioMock(); - mockAxios.onGet('http://localhost:8000/questions/en').reply(200, - [{question: "What is the population of Oviedo?", - answers: ["225089","272357","267855","231841"]}]); await act(async () =>{ await render(); @@ -125,9 +114,6 @@ describe('Question View component', () => { }); it('shows colors to reveal false answer and it sounds', async () => { setupAudioMock() - mockAxios.onGet('http://localhost:8000/questions/en').reply(200, - [{question: "What is the population of Oviedo?", - answers: ["225089","272357","267855","231841"]}]); await act(async () =>{ await render(); @@ -146,9 +132,6 @@ describe('Question View component', () => { it('shows timer and tiktak sound', async () => { setupAudioMock() - mockAxios.onGet('http://localhost:8000/questions/en').reply(200, - [{question: "What is the population of Oviedo?", - answers: ["225089","272357","267855","231841"]}]); await act(async () =>{ await render(); @@ -158,7 +141,24 @@ describe('Question View component', () => { const timerElement = screen.getByText(new RegExp(`(\\d+) ${i18en.t('questionView.seconds')}`)); expect(timerElement).toBeInTheDocument(); // Verificar que el temporizador esté presente en el DOM - }); + }); + + it('shows finish game review',async () => { + mockAxios.onGet('http://localhost:8000/questions/en').reply(200, []); + mockAxios.onPost('http://localhost:8000/record').reply(200, {user:'myUser'}); + + const user = { username: 'myUser' }; + + //It gives an error as we are not wrapping it by act, however by doing this we simulate a no questions situation + await act(async () =>{ + await render(); + }) + + await waitFor(() => expect(screen.getByText(i18en.t('questionView.finished_game'))).toBeInTheDocument()); + + expect(screen.getByText('What is the population of Oviedo?')).toBeInTheDocument() + + }); // it('renders end message when countdown completes', async() => { diff --git a/webapp/src/components/ranking/RankingRetriever.js b/webapp/src/components/ranking/RankingRetriever.js new file mode 100644 index 00000000..a84de7e6 --- /dev/null +++ b/webapp/src/components/ranking/RankingRetriever.js @@ -0,0 +1,101 @@ +import axios from 'axios'; + +class RankingRetriever{ + + constructor(){ + this.apiUrl = (process.env.REACT_APP_API_ENDPOINT || 'http://localhost:8000')+ "/record/ranking"; + + } + + async getTopTen() { + + try { + const response = await axios.get(this.apiUrl + '/top10');//finding the top ten + const receivedTopTenRanking = await response.data; + return receivedTopTenRanking; + } catch (error) { + console.log(error) + throw new Error(error); + + } + /* + return { + "usersCompetitiveStats": [ + { + "_id": "user", + "totalPoints": 1000, + "totalCompetitiveGames": 4 + }, + { + "_id": "user2", + "totalPoints": 900, + "totalCompetitiveGames": 2 + }, + { + "_id": "user3", + "totalPoints": 800, + "totalCompetitiveGames": 3 + }, + { + "_id": "user4", + "totalPoints": 700, + "totalCompetitiveGames": 5 + }, + { + "_id": "user5", + "totalPoints": 600, + "totalCompetitiveGames": 6 + }, + { + "_id": "user6", + "totalPoints": 500, + "totalCompetitiveGames": 7 + }, + { + "_id": "user7", + "totalPoints": 400, + "totalCompetitiveGames": 8 + }, + { + "_id": "user8", + "totalPoints": 300, + "totalCompetitiveGames": 9 + }, + { + "_id": "user9", + "totalPoints": 200, + "totalCompetitiveGames": 10 + }, + { + "_id": "user10", + "totalPoints": 100, + "totalCompetitiveGames": 11 + } + ] + }*/ + } + async getUser(user){ + + try { + const response = await axios.get(this.apiUrl + '/'+user);//finding the top ten + const receivedMyRanking = await response.data; + return receivedMyRanking.userCompetitiveStats; + } catch (error) { + console.log(error) + throw new Error(error); + + } + /* + return { + "_id": "myUser", + "totalPoints": 250, + "totalCompetitiveGames": 1 + };*/ + } + + + +} + +export default RankingRetriever; + diff --git a/webapp/src/components/ranking/RankingView.js b/webapp/src/components/ranking/RankingView.js new file mode 100644 index 00000000..f2aad16e --- /dev/null +++ b/webapp/src/components/ranking/RankingView.js @@ -0,0 +1,105 @@ +import React, { useState } from 'react'; +import RankingRetriever from './RankingRetriever'; +import {useTranslation} from "react-i18next"; +import Loader from "../fragments/Loader" +import BackButton from '../fragments/BackButtonToGameMenu'; +import { useUserContext } from '../loginAndRegistration/UserContext'; + +const retriever = new RankingRetriever(); + +const RankingView = () => { + const[t] = useTranslation("global"); + const {user} = useUserContext(); + + const [rankingData, setRankingData] = useState(null); + const [myRankingData, setMyRankingData] = useState(null); + const [searchTerm, setSearchTerm] = useState(user.username); + + + + const getRanking = async () => { + try { + var ranking = await retriever.getTopTen(); + setRankingData(ranking.usersCompetitiveStats); + var myrank = await retriever.getUser(user.username); + setMyRankingData(myrank); + } catch (error) { + console.log(error); + } + } + const handleSearch = async (e) => { + e.preventDefault(); + if(searchTerm.length!==0){ + try { + const rank = await retriever.getUser(searchTerm); + setMyRankingData(rank); + } catch (error) { + console.log(error); + } + } + + } + if(rankingData==null || myRankingData == null){ + getRanking(); + } + + return ( +
+ +

{t("ranking.ranking")}

+ {rankingData && rankingData.length > 0 && myRankingData ? ( + <> +
+ + + + + + + + + + + {rankingData.map((user, index) => ( + + + + + + + ))} + {/* Blank row */} + + + + + + + + + + + + +
{t("ranking.position")}{t("ranking.username")}{t("ranking.points")}{t("ranking.num_games")}
{index + 1}{user._id}{user.totalPoints}{user.totalCompetitiveGames}
+ setSearchTerm(e.target.value)} + placeholder={t("ranking.enter_username")} + /> + +
+ +
+
{myRankingData.position}{myRankingData._id}{myRankingData.totalPoints}{myRankingData.totalCompetitiveGames}
+
+ + ) : ( + < Loader /> + )} +
+ ); +}; + +export default RankingView; diff --git a/webapp/src/components/ranking/RankingView.test.js b/webapp/src/components/ranking/RankingView.test.js new file mode 100644 index 00000000..76c0506a --- /dev/null +++ b/webapp/src/components/ranking/RankingView.test.js @@ -0,0 +1,182 @@ +import { render , screen, waitFor, fireEvent } from '@testing-library/react'; +import { MemoryRouter } from 'react-router-dom'; +import axios from 'axios'; +import { initReactI18next } from 'react-i18next'; +import i18en from 'i18next'; +import RankingView from './RankingView'; +import MockAdapter from 'axios-mock-adapter'; +import { act } from 'react-dom/test-utils'; +import { UserContextProvider} from '../loginAndRegistration/UserContext'; +i18en.use(initReactI18next).init({ + resources: {}, + lng: 'en', + interpolation:{ + escapeValue: false, + } +}); +global.i18en = i18en; + +const mockAxios = new MockAdapter(axios); +describe('RankingView component', () => { + const user = { username: 'myUser' }; + + it('renders title', () => { + act(()=>{ + render(); + }) + const text = screen.getByText(i18en.t('ranking.ranking')); + expect(text).toBeInTheDocument(); + }); + it('renders Loading if the call to the gateway has not been done', () => { + act(()=>{ + render(); + }) + const text = screen.getByText('Loading...'); + expect(text).toBeInTheDocument(); + }); +}); + describe('RankingView component with endpoint', ()=>{ + mockAxios.onGet('http://localhost:8000/record/ranking/top10').reply(200, + { + "usersCompetitiveStats": [ + { + "_id": "user", + "totalPoints": 1000, + "totalCompetitiveGames": 4 + }, + { + "_id": "user2", + "totalPoints": 900, + "totalCompetitiveGames": 2 + }, + { + "_id": "user3", + "totalPoints": 800, + "totalCompetitiveGames": 3 + }, + { + "_id": "user4", + "totalPoints": 700, + "totalCompetitiveGames": 5 + }, + { + "_id": "user5", + "totalPoints": 600, + "totalCompetitiveGames": 6 + }, + { + "_id": "user6", + "totalPoints": 500, + "totalCompetitiveGames": 7 + }, + { + "_id": "user7", + "totalPoints": 400, + "totalCompetitiveGames": 8 + }, + { + "_id": "user8", + "totalPoints": 300, + "totalCompetitiveGames": 9 + }, + { + "_id": "user9", + "totalPoints": 200, + "totalCompetitiveGames": 10 + }, + { + "_id": "user10", + "totalPoints": 100, + "totalCompetitiveGames": 11 + } + ] + }); + + mockAxios.onGet('http://localhost:8000/record/ranking/myUser').reply(200, + {userCompetitiveStats: + { + "_id": "myUser", + "totalPoints": 250, + "totalCompetitiveGames": 1, + "position":10 + } + } + ); + const user = { username: 'myUser' }; + + it('renders position all headers in the table',async ()=>{ + + await act(async () =>{ + await render(); + + }) + await waitFor(() => expect(screen.getByText(i18en.t('ranking.position'))).toBeInTheDocument()); + expect(screen.getByText(i18en.t('ranking.username'))).toBeInTheDocument() + expect(screen.getByText(i18en.t('ranking.points'))).toBeInTheDocument() + expect(screen.getByText(i18en.t('ranking.num_games'))).toBeInTheDocument() + }); + + it('renders position all users usernames',async ()=>{ + await act(async () =>{ + await render(); + + }) + await waitFor(() => expect(screen.getByText(i18en.t('ranking.position'))).toBeInTheDocument()); + //expect(screen.getByText("user1")).toBeInTheDocument() + expect(screen.getByText("user2")).toBeInTheDocument() + expect(screen.getByText("user3")).toBeInTheDocument() + expect(screen.getByText("user4")).toBeInTheDocument() + expect(screen.getByText("user5")).toBeInTheDocument() + expect(screen.getByText("user6")).toBeInTheDocument() + expect(screen.getByText("user7")).toBeInTheDocument() + expect(screen.getByText("user8")).toBeInTheDocument() + expect(screen.getByText("user9")).toBeInTheDocument() + expect(screen.getByText("user10")).toBeInTheDocument() + }); + it('renders position all users totalPoints',async ()=>{ + + await act(async () =>{ + await render(); + + }) + await waitFor(() => expect(screen.getByText(i18en.t('ranking.position'))).toBeInTheDocument()); + expect(screen.getByText("1000")).toBeInTheDocument() + expect(screen.getByText("900")).toBeInTheDocument() + expect(screen.getByText("800")).toBeInTheDocument() + expect(screen.getByText("700")).toBeInTheDocument() + expect(screen.getByText("600")).toBeInTheDocument() + expect(screen.getByText("500")).toBeInTheDocument() + expect(screen.getByText("400")).toBeInTheDocument() + expect(screen.getByText("300")).toBeInTheDocument() + expect(screen.getByText("200")).toBeInTheDocument() + }); + it('renders position all users competitive games',async ()=>{ + + await act(async () =>{ + await render(); + + }) + await waitFor(() => expect(screen.getByText(i18en.t('ranking.position'))).toBeInTheDocument()); + + expect(screen.getAllByText(/2/).length).toBeGreaterThanOrEqual(2);//hay dos pq hay una posicion + expect(screen.getAllByText(/5/).length).toBeGreaterThanOrEqual(2);//hay dos pq hay una posicion + expect(screen.getAllByText(/6/).length).toBeGreaterThanOrEqual(2);//hay dos pq hay una posicion + expect(screen.getAllByText(/7/).length).toBeGreaterThanOrEqual(2);//hay dos pq hay una posicion + }); + it('renders position all users competitive games',async ()=>{ + + await act(async () =>{ + await render(); + + }) + await waitFor(() => expect(screen.getByText(i18en.t('ranking.position'))).toBeInTheDocument()); + + expect(screen.getByText("myUser")).toBeInTheDocument() + expect(screen.getByText("250")).toBeInTheDocument() + //should be one if only your rank is shown + expect(screen.getAllByText(/1/).length).toBeGreaterThanOrEqual(2);//hay dos pq hay una posicion + + }); + +}) + diff --git a/webapp/src/custom.css b/webapp/src/custom.css index 453ce6c4..f6660f91 100644 --- a/webapp/src/custom.css +++ b/webapp/src/custom.css @@ -1362,6 +1362,26 @@ svg { /*------------------------------Historical--------------------------------------------*/ /* Estilos para los botones */ +.linkButtonHistorical{ + display: flex; + justify-content: center; + align-items: center; + width: 10em; + height: 45px; + background:#00b8ff; + border: none; + outline: none; + border-radius: 40px; + box-shadow: 0 0 10px black; + cursor: pointer; + font-size: 1em; + color: black; + font-weight: 700; + margin-top: 1.5em; + margin-bottom: 1em; + text-decoration: none; + +} .historicalButton { color: white; width: 50em; @@ -1426,3 +1446,235 @@ svg { h2{ text-align: center; } +/*------------------------Loader------------------------------*/ +@keyframes blinkCursor { + 50% { + border-right-color: transparent; + } +} + +@keyframes typeAndDelete { + 0%, + 10% { + width: 0; + } + 45%, + 55% { + width: 6.2em; + } /* adjust width based on content */ + 90%, + 100% { + width: 0; + } +} + +.terminal-loader { + border: 0.1em solid #333; + background-color: #1a1a1a; + color: #0f0; + font-family: "Courier New", Courier, monospace; + font-size: 1em; + padding: 1.5em 1em; + width: 12em; + margin: 100px auto; + box-shadow: 0 4px 8px rgba(0, 0, 0, 0.2); + border-radius: 4px; + position: relative; + overflow: hidden; + box-sizing: border-box; +} + +.terminal-header { + position: absolute; + top: 0; + left: 0; + right: 0; + height: 1.5em; + background-color: #333; + border-top-left-radius: 4px; + border-top-right-radius: 4px; + padding: 0 0.4em; + box-sizing: border-box; +} + + + +.terminal-title { + float: left; + line-height: 1.5em; + color: #eee; +} + +.text { + display: inline-block; + white-space: nowrap; + overflow: hidden; + border-right: 0.2em solid green; /* Cursor */ + animation: typeAndDelete 4s steps(11) infinite, + blinkCursor 0.5s step-end infinite alternate; + margin-top: 1.5em; +} + +/*--------------------------------------------------Configurator---------------------------------*/ +/* Estilo para el elemento select */ +.buttonRandomize{ + display: flex; + justify-content: center; + align-items: center; + width: 15em; + height: 45px; + background:#00b8ff; + border: none; + outline: none; + border-radius: 40px; + box-shadow: 0 0 10px black; + cursor: pointer; + font-size: 1em; + color: black; + font-weight: 700; + margin-top: 1.5em; + margin-bottom: 1em; + text-decoration: none; +} +.select-style { + width: 200px; + height: 35px; + border: 1px solid #ccc; + border-radius: 5px; + background-color: #f8f8f8; + padding: 5px; + font-size: 16px; + color: #333; +} + +.select-style:focus { + border-color: #6e9ecf; + outline: none; +} + +/* Estilo para el spinner */ +.spinner-style { + width: 40px; + height: 30px; + border: 1px solid #ccc; + background-color: #f8f8f8; + border-radius: 5px; + font-size: medium; +} +.GameConfiguratorDiv> * { + margin-top: 10px; + margin-bottom: 10px; +} +.GameConfiguratorDiv { + left: 50%; + top: 50%; +} +/* +.GameConfiguratorDiv { + display: flex; + flex-direction: column; +} + +.GameConfiguratorDiv > *:not(:first-child):not(:nth-child(4)) { + text-align: left; +}*/ + +.hr-style { + border: 0; + border-top: 1px solid #8e888885; /* Color gris */ + margin: 20px 0; /* Espacio antes y después de la línea */ +} + +.GameConfiguratorDiv h2 { + text-align: left; /* Alinea el texto a la izquierda */ + margin-top:20px; + margin-bottom:20px; +} +.table tbody tr.penultimate-row td { + background-color: black; + padding: 15px; + /* + border: 1px solid rgba(205, 187, 187, 0.455); + box-shadow: 0 0 10px rgba(197, 191, 191, 0.726);*/ +} + +.table tbody tr.penultimate-row td div{ + background-color: #171717; + padding: 0.5em; +} + +.table th,td{ + padding-left: 30px; + padding-right: 30px; + padding-top: 10px; + padding-bottom: 10px; + text-align: center; + +} +/* Estilo para todos los td excepto el penúltimo */ +.table tbody tr:not(:nth-last-child(2)) td:nth-child(1) { + background-color: #4d1e51a8; +} + + +.table tr:nth-child(even) {background-color: #f2f2f22a;} +.table th { + background-color: #69276f; + color: white; +} +.table tr:hover {background-color: #a59c9ca2;} + +.table table{ + border-collapse: collapse; +} +/* Estilo para la fila de headers */ +.table th { + border-top: 2px solid rgba(205, 187, 187, 0.455); /* Borde superior para todas las celdas de la fila de headers */ + border-left: 2px solid rgba(205, 187, 187, 0.455); /* Borde izquierdo en la primera celda */ + border-right: 2px solid rgba(205, 187, 187, 0.455); /* Borde derecho en la última celda */ +} + +/* Estilo para las filas de datos (excepto la penúltima) */ +.table tbody tr:not(:nth-last-child(2)) td { + border-left: 2px solid rgba(205, 187, 187, 0.455); /* Borde izquierdo en la primera celda */ + border-right: 2px solid rgba(205, 187, 187, 0.455); /* Borde derecho en la última celda */ +} + +/* Borde superior e inferior para la penúltima fila */ +.table tbody tr:nth-last-child(2) td { + border-top: 2px solid rgba(205, 187, 187, 0.455); /* Borde superior */ + border-bottom: 2px solid rgba(205, 187, 187, 0.455); /* Borde inferior */ +} + +/* Borde inferior para todas las celdas de la última fila */ +.table tbody tr:last-child td { + border-bottom: 2px solid rgba(205, 187, 187, 0.455); /* Borde inferior */ +} + +/* Borde izquierdo para la primera celda de la última fila */ +.table tbody tr:last-child td:first-child { + border-left: 2px solid rgba(205, 187, 187, 0.455); /* Borde izquierdo */ +} + +/* Borde derecho para la última celda de la última fila */ +.table tbody tr:last-child td:last-child { + border-right: 2px solid rgba(205, 187, 187, 0.455); /* Borde derecho */ +} + +/* Estilo para el cuadro de búsqueda */ +input[type="text"] { + border: 2px solid #636262; /* Borde sólido */ + padding: 8px; /* Espaciado interno */ + font-size: 16px; /* Tamaño de letra */ + color: #e5e0e0; /* Color de letra */ + background-color: #2a2929; /* Color de fondo */ +} + +/* Estilo para el botón */ +#search { + font-size: 18px; /* Tamaño de letra */ + width: 7em; + height: 2em; +} + + diff --git a/webapp/src/index.js b/webapp/src/index.js index 409a8681..103dc805 100644 --- a/webapp/src/index.js +++ b/webapp/src/index.js @@ -32,12 +32,14 @@ i18next.init({ const root = ReactDOM.createRoot(document.getElementById('root')); root.render( + {/* envolviendo la app con el sistema de traducciones */} + ); // If you want to start measuring performance in your app, pass a function diff --git a/webapp/src/translations/en/global.json b/webapp/src/translations/en/global.json index d352e7a8..c076edbd 100644 --- a/webapp/src/translations/en/global.json +++ b/webapp/src/translations/en/global.json @@ -65,7 +65,9 @@ "gameMenu":{ "history_button":"View Historical Data", "new_game_button":"Create New Game", - "title":"Game Menu" + "view_ranking":"Ranking", + "title":"Game Menu", + "back":"Back" }, "questionView":{ "seconds":"seconds", @@ -80,6 +82,31 @@ "points":"points", "no_games_played":"No games played yet" }, + "gameConfigurator":{ + "game_config":"Game configuration", + "type_quest":"Type of question: ", + "num_quest":"Number of questions: ", + "play_custom":"Play Customized Game", + "rules_competi":"Play with all kinds of questions and a quantity of 5", + "play_competi":"Play competitive Game", + "option_all":"All", + "option_population":"Population", + "option_capital":"Capital", + "option_language":"Language", + "option_size":"Size", + "custo_game":"Create custom game", + "competi_game":"Play Competitive", + "randomize":"Randomize Parameters" + }, + "ranking":{ + "ranking":"Ranking", + "position":"Position", + "username":"Username", + "points":"Points", + "num_games":"Competitive games", + "search":"Search", + "enter_username":"Enter Username..." + }, "error":{ "error":"Error", "sorry":"We're sorry, this page does not exist. Don't be angry, I'm just a little cat." diff --git a/webapp/src/translations/es/global.json b/webapp/src/translations/es/global.json index 73a4820f..898263d1 100644 --- a/webapp/src/translations/es/global.json +++ b/webapp/src/translations/es/global.json @@ -69,7 +69,9 @@ "gameMenu":{ "history_button":"Ver Historial", "new_game_button":"Crear nuevo juego", - "title":"Menú del Juego" + "view_ranking":"Ranking", + "title":"Menú del Juego", + "back":"Atrás" },"questionView":{ "seconds":"segundos", "question_counter":"Pregunta nº ", @@ -87,8 +89,32 @@ "error":{ "error":"Error", "sorry":"Lo sentimos, esta página no existe. No te enfades, solo soy un gatito." - } - + }, + "gameConfigurator":{ + "game_config":"Configuración del Juego", + "type_quest":"Tipo de Pregunta : ", + "num_quest":"Número de Preguntas : ", + "play_custom":"Jugar personalizado", + "rules_competi":"Jugar con todo tipo de preguntas siendo estas 5", + "play_competi":"Jugar Competitivo", + "option_all":"Todas", + "option_population":"Población", + "option_capital":"Capital", + "option_language":"Lenguaje", + "option_size":"Extensión", + "custo_game":"Crea una partida personalizada", + "competi_game":"Juega en modo Competitivo", + "randomize":"Randomiza Parámetros" + }, + "ranking":{ + "ranking":"Ranking", + "position":"Posición", + "username":"Username", + "points":"Puntos", + "num_games":"Juegos Competitivos", + "search":"Buscar", + "enter_username":"Inserta usuario..." + } } \ No newline at end of file