diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index 1167a28f..3b79addb 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -1,7 +1,7 @@ name: Build on: push: - branches: [master, develop, develop-deploy, develop-teresa] + branches: [master, develop, develop-deploy, develop-teresa, develop_samuel] pull_request: types: [opened, synchronize, reopened] diff --git a/docker-compose.yml b/docker-compose.yml index c431c1c6..57e27250 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -10,6 +10,7 @@ services: - "27017:27017" networks: - mynetwork + restart: always mariadb: container_name: mariadb @@ -24,6 +25,7 @@ services: - "3306:3306" networks: - mynetwork + restart: always users: container_name: users @@ -38,6 +40,7 @@ services: - mynetwork environment: DATABASE_URL: mariadb:3306/userdb + restart: always questions: container_name: questions @@ -53,6 +56,7 @@ services: environment: DATABASE_URI: mongodb://mongodb:27017/questionDB TEST_DATABASE_URI: mongodb://mongodb:27017/test + restart: always gatewayservice: container_name: gatewayservice @@ -69,6 +73,7 @@ services: environment: USER_SERVICE_URL: http://users:8001 QUESTION_SERVICE_URL: http://questions:8010 + restart: always webapp: container_name: webapp @@ -85,6 +90,7 @@ services: environment: REACT_APP_API_ENDPOINT: http://gatewayservice:8000 REACT_APP_MULTIPLAYER_ENDPOINT: http://multiplayer:5010 + restart: always multiplayer: container_name: multiplayer @@ -100,6 +106,7 @@ services: environment: WEBAPP_ENDPOINT: http://webapp:3000 GATEWAY_SERVICE_ENDPOINT: http://gatewayservice:8000 + restart: always prometheus: image: prom/prometheus @@ -114,6 +121,7 @@ services: - "9090:9090" depends_on: - gatewayservice + restart: always grafana: image: grafana/grafana @@ -133,6 +141,7 @@ services: - "9091:9091" depends_on: - prometheus + restart: always volumes: mariadb_data: diff --git a/gatewayservice/monitoring/grafana/provisioning/dashboards/example-service-dashboard.json b/gatewayservice/monitoring/grafana/provisioning/dashboards/wiq-es04-dashboard.json similarity index 97% rename from gatewayservice/monitoring/grafana/provisioning/dashboards/example-service-dashboard.json rename to gatewayservice/monitoring/grafana/provisioning/dashboards/wiq-es04-dashboard.json index 61da159b..28caba1c 100644 --- a/gatewayservice/monitoring/grafana/provisioning/dashboards/example-service-dashboard.json +++ b/gatewayservice/monitoring/grafana/provisioning/dashboards/wiq-es04-dashboard.json @@ -80,9 +80,9 @@ "steppedLine": false, "targets": [ { - "expr": "sum(increase(http_request_duration_seconds_count[1m]))", + "expr": "sum(increase(http_request_duration_seconds_count[1h]))", "interval": "", - "legendFormat": "Requests per minute", + "legendFormat": "Requests per hour", "refId": "A" } ], @@ -90,7 +90,7 @@ "timeFrom": null, "timeRegions": [], "timeShift": null, - "title": "Rate (R): Number of requests per minute", + "title": "Rate (R): Number of requests per hour", "tooltip": { "shared": true, "sort": 0, @@ -189,7 +189,7 @@ "steppedLine": false, "targets": [ { - "expr": "sum(increase(http_request_duration_seconds_count{code=~\"5.*\"}[1m]))", + "expr": "sum(increase(http_request_duration_seconds_count{status_code=~\"4.*\"}[1m]))", "interval": "", "legendFormat": "", "refId": "A" @@ -369,7 +369,7 @@ ] }, "timezone": "", - "title": "Example Service Dashboard", + "title": "Wiq_es04 Dashboard", "uid": "1DYaynomMk", "version": 2 } diff --git a/gatewayservice/monitoring/prometheus/prometheus.yml b/gatewayservice/monitoring/prometheus/prometheus.yml index 3093655b..10930610 100644 --- a/gatewayservice/monitoring/prometheus/prometheus.yml +++ b/gatewayservice/monitoring/prometheus/prometheus.yml @@ -1,6 +1,6 @@ global: scrape_interval: 5s scrape_configs: - - job_name: "example-nodejs-app" + - job_name: "wiq-es04-nodejs-app" static_configs: - targets: ["gatewayservice:8000"] \ No newline at end of file diff --git a/multiplayer/index.js b/multiplayer/index.js index 7eb722ee..c2b2dbc4 100644 --- a/multiplayer/index.js +++ b/multiplayer/index.js @@ -9,10 +9,10 @@ const app = express(); const server = http.createServer(app); const io = socketIO(server, { cors: { //permit connections from webapp - origin: [process.env.WEBAPP_ENDPOINT, "http://localhost:3000"], - //origin: "*", //this should be changed to improve security + //origin: [process.env.WEBAPP_ENDPOINT, "http://localhost:3000"], + origin: "*", //this should be changed to improve security methods: ["GET", "POST"], - //allowedHeaders: "*" //this should be changed to improve security + allowedHeaders: "*" //this should be changed to improve security } }); diff --git a/questions/__tests/routes/question-routes.test.js b/questions/__tests/routes/question-routes.test.js index 6baea678..73ed7dec 100644 --- a/questions/__tests/routes/question-routes.test.js +++ b/questions/__tests/routes/question-routes.test.js @@ -8,6 +8,7 @@ const bodyParser = require('body-parser'); let mongoServer; let questionFunctions; let questionRoutes; +let generateQuestionsService; //let mongoServer; let app = express(); @@ -21,11 +22,11 @@ const questionData1 = { }; const questionData2 = { - question: "Which is the capital of UK?", + question: "Which is the capital of Spain?", options: ["Madrid", "Barcelona", "Paris", "London"], correctAnswer: "London", categories: ["Geography"], - language: "es" + language: "en" }; async function addingQuestion(questionData) { @@ -40,6 +41,12 @@ async function addingQuestion(questionData) { await questionFunctions.addQuestion(newQuestion); } +async function addingQuestions() { + for(var i = 0; i < 24; i++) { + await addingQuestion(questionData1); + await addingQuestion(questionData2); + } +} beforeAll(async () => { mongoServer = await MongoMemoryServer.create(); @@ -48,19 +55,17 @@ beforeAll(async () => { await mongoose.connect(mongoURI); questionFunctions = require('../../services/question-data-service'); questionRoutes = require('../../routes/question-routes.js'); + generateQuestionsService = require('../../services/generate-questions-service'); + jest.mock('../../services/generate-questions-service'); app.use(bodyParser.json()); app.use('/questions', questionRoutes); - }); beforeEach(async () => { //Load database with initial conditions //await mongoose.connection.dropDatabase(); await Question.deleteMany({}); - for(var i = 0; i < 100; i++) { - await addingQuestion(questionData1); - await addingQuestion(questionData2); - } + await addingQuestions(); }); afterAll(async () => { @@ -71,25 +76,92 @@ beforeEach(async () => { describe('Question routes', function() { - it('It should get a question from the database', async function() { + + describe('Get a question from the database', function() { + it('get question when 0 questions', async function() { + await Question.deleteMany({}); + await generateQuestionsService.generateQuestions.mockResolvedValue({"response":{"status":"200"}}); + const response = await request(app).get('/questions/es'); + await expect(response.status).toBe(200); + await expect(response.body.question).toBe(undefined); + }); + + it('get question when less than 50 questions', async function() { + await generateQuestionsService.generateQuestions.mockResolvedValue({"response":{"status":"200"}}); const response = await request(app).get('/questions/en'); await expect(response.status).toBe(200); await expect(response.body.question).toBe('Which is the capital of Spain?'); }); - - it('It should get n questions from the database', async function() { - const response = await request(app).get('/questions/getQuestionsFromDb/3/en'); + it('get question when less than 100 questions', async function() { + await addingQuestions(); + await generateQuestionsService.generateQuestions.mockResolvedValue({"response":{"status":"200"}}); + const response = await request(app).get('/questions/en'); await expect(response.status).toBe(200); - await expect(response.body.length).toBe(3); - }); + await expect(response.body.question).toBe('Which is the capital of Spain?'); + }); + + it('get question when more than 100 questions', async function() { + await addingQuestions(); + await addingQuestions(); + await generateQuestionsService.generateQuestions.mockResolvedValue({"response":{"status":"200"}}); + const response = await request(app).get('/questions/en'); + await expect(response.status).toBe(200); + await expect(response.body.question).toBe('Which is the capital of Spain?'); + }); + }); + describe('Get n questions from the database', function() { + it('get question when less than 50 questions', async function() { + await generateQuestionsService.generateQuestions.mockResolvedValue({"response":{"status":"200"}}); + const response = await request(app).get('/questions/getQuestionsFromDb/3/en'); + await expect(response.status).toBe(200); + await expect(response.body.length).toBe(3); + }); + + it('get question when less than 100 questions', async function() { + await addingQuestions(); + await generateQuestionsService.generateQuestions.mockResolvedValue({"response":{"status":"200"}}); + const response = await request(app).get('/questions/getQuestionsFromDb/3/en'); + await expect(response.status).toBe(200); + await expect(response.body.length).toBe(3); + }); + + it('get question when more than 100 questions', async function() { + await addingQuestions(); + await addingQuestions(); + await generateQuestionsService.generateQuestions.mockResolvedValue({"response":{"status":"200"}}); + const response = await request(app).get('/questions/getQuestionsFromDb/3/en'); + await expect(response.status).toBe(200); + await expect(response.body.length).toBe(3); + }); it('It should not get n questions from the database', async function() { const response = await request(app).get('/questions/getQuestionsFromDb/-1/en'); await expect(response.status).toBe(400); }); - - it('It should get n questions of certain category from the database', async function() { + }); + describe('Get n questions from the database filtered by category', function() { + + it('get question when less than 50 questions', async function() { + await generateQuestionsService.generateQuestions.mockResolvedValue({"response":{"status":"200"}}); + const response = await request(app).get('/questions/getQuestionsFromDb/2/Geography/en'); + await expect(response.status).toBe(200); + await expect(response.body.length).toBe(2); + await expect(response.body[0].categories[0]).toBe("Geography"); + }); + it('get question when less than 100 questions', async function() { + await addingQuestions(); + await generateQuestionsService.generateQuestions.mockResolvedValue({"response":{"status":"200"}}); + const response = await request(app).get('/questions/getQuestionsFromDb/2/Geography/en'); + await expect(response.status).toBe(200); + await expect(response.body.length).toBe(2); + await expect(response.body[0].categories[0]).toBe("Geography"); + }); + + it('get question when more than 100 questions', async function() { + await addingQuestions(); + await addingQuestions(); + await generateQuestionsService.generateQuestions.mockResolvedValue({"response":{"status":"200"}}); const response = await request(app).get('/questions/getQuestionsFromDb/2/Geography/en'); await expect(response.status).toBe(200); await expect(response.body.length).toBe(2); @@ -106,4 +178,5 @@ describe('Question routes', function() { it('should connect to the MongoDB server in memory', async () => { expect(mongoose.connection.readyState).toBe(1); // 1 means connected }); - }); \ No newline at end of file + }); + }); \ No newline at end of file diff --git a/questions/__tests/services/question-generation-service.test.js b/questions/__tests/services/question-generation-service.test.js index 2024e0dd..a999966c 100644 --- a/questions/__tests/services/question-generation-service.test.js +++ b/questions/__tests/services/question-generation-service.test.js @@ -52,7 +52,7 @@ describe('Question generation', function() { dbService.addQuestion.mockResolvedValue(); // Llama a la función que deseas probar - await generator.generateQuestions(1); + await generator.generateQuestions(1,"en","Geography"); // Verifica que la función haya realizado las operaciones esperadas expect(dbService.addQuestion).toHaveBeenCalledTimes(1); @@ -76,7 +76,7 @@ describe('Question generation', function() { console.error = jest.fn(); // Llama a la función que deseas probar - await generator.generateQuestions(1); + await generator.generateQuestions(1,"en"); // Verifica que la función haya realizado las operaciones esperadas expect(dbService.addQuestion).toHaveBeenCalledTimes(0); diff --git a/questions/routes/question-routes.js b/questions/routes/question-routes.js index 28b36f58..bcb15c5a 100644 --- a/questions/routes/question-routes.js +++ b/questions/routes/question-routes.js @@ -42,25 +42,48 @@ router.get('/:lang', async (req, res) => { router.get('/getQuestionsFromDb/:n/:lang', async(_req, res) => { const n = parseInt(_req.params.n, 10); const language = _req.params.lang; + const questionCount = await dbService.getQuestionCount(language); //Verify is n is a correct number if (isNaN(n) || n <= 0) { return res.status(400).json({ error: 'Parameter "n" must be > 0.' }); } - if (await dbService.getQuestionCount(language) < n) { - //Must generate n questions - await generateQuestionsService.generateQuestions(n + 1, language); + // 0: Await till it creates 2, creates 50 async and do not delete + if (questionCount == 0) { + await generateQuestionsService.generateQuestions(2, language); + generateQuestionsService.generateQuestions(50, language); + const questions = await dbService.getRandomQuestions(n, language); + res.json(questions); + + // < 50: async creates 10 and do not delete + } else if (questionCount < 50) { //Do not wait to generate the others - generateQuestionsService.generateQuestions(n * 5, language); + generateQuestionsService.generateQuestions(10, language); + const questions = await dbService.getRandomQuestions(n, language); + res.json(questions); + + // < 100: async creates 5 and delete + } else if (questionCount < 100) { + generateQuestionsService.generateQuestions(10, language); + const questions = await dbService.getRandomQuestions(n, language); + res.json(questions); + if(questions != null) { + for(var i = 0; i < questions.length; i++) { + dbService.deleteQuestionById(questions[i]._id); + } + } + // >= 100: do not create and delete + } else { + const questions = await dbService.getRandomQuestions(n, language); + res.json(questions); + if(questions != null) { + for(var i = 0; i < questions.length; i++) { + dbService.deleteQuestionById(questions[i]._id); + } + } } - questions = await dbService.getRandomQuestions(n, language); - if(questions != null) - questions.map(question => { - dbService.deleteQuestionById(question._id); - }) - res.json(questions); }); //Get random questions from db with category filter: http://localhost:8010/questions/getQuestionsFromDb/2/Geografía @@ -68,26 +91,48 @@ router.get('/getQuestionsFromDb/:n/:category/:lang', async(_req, res) => { const n = parseInt(_req.params.n, 10); const category = _req.params.category; const language = _req.params.lang; + const questionCount = await dbService.getQuestionCountByCategory(category, language); //Verify is n is a correct number if (isNaN(n) || n <= 0) { return res.status(400).json({ error: 'Parameter "n" must be > 0.' }); } - if (await dbService.getQuestionCountByCategory(category, language) < n) { - //Must generate n questions (n because some can fail at generation at the moment 18/04) - await generateQuestionsService.generateQuestions(n + 2, language, category); + // 0: Await till it creates 2, creates 50 async and do not delete + if (questionCount == 0) { + await generateQuestionsService.generateQuestions(2, language, category); + generateQuestionsService.generateQuestions(50, language, category); + questions = await dbService.getRandomQuestionsByCategory(n, category, language); + res.json(questions); + + // < 50: async creates 10 and do not delete + } else if (questionCount < 50) { //Do not wait to generate the others - generateQuestionsService.generateQuestions(n, language, category); - } - questions = await dbService.getRandomQuestionsByCategory(n, category, language); - if(questions != null) { - for(var i = 0; i < questions.length; i++) { - dbService.deleteQuestionById(questions[i]._id); + generateQuestionsService.generateQuestions(10, language, category); + questions = await dbService.getRandomQuestionsByCategory(n, category, language); + res.json(questions); + + // < 100: async creates 5 and delete + } else if (questionCount < 100) { + generateQuestionsService.generateQuestions(10, language, category); + questions = await dbService.getRandomQuestionsByCategory(n, category, language); + res.json(questions); + if(questions != null) { + for(var i = 0; i < questions.length; i++) { + dbService.deleteQuestionById(questions[i]._id); + } + } + + // >= 100: do not create and delete + } else { + questions = await dbService.getRandomQuestionsByCategory(n, category, language); + res.json(questions); + if(questions != null) { + for(var i = 0; i < questions.length; i++) { + dbService.deleteQuestionById(questions[i]._id); + } } } - res.json(questions); - }); module.exports = router; \ No newline at end of file diff --git a/questions/services/question-data-service.js b/questions/services/question-data-service.js index 9eb956d5..ef6a054e 100644 --- a/questions/services/question-data-service.js +++ b/questions/services/question-data-service.js @@ -103,7 +103,7 @@ module.exports = { }, - // Get random questions TODO: refactor to use common code with get questions by category + // Get random questions getRandomQuestions : async function(n, wantedLanguage) { try { // Obtain total number of questions in database diff --git a/users/__tests/routes/user-routes.test.js b/users/__tests/routes/user-routes.test.js index 634b77dd..157ba73d 100644 --- a/users/__tests/routes/user-routes.test.js +++ b/users/__tests/routes/user-routes.test.js @@ -1023,13 +1023,6 @@ describe('User Routes', () => { .expect(403); expect(responseWithoutLoggedUser.body).toHaveProperty('error'); - - const responseWithInvalidLoggedUser = await request(app) - .get('/user/statistics/testuser2') - .query({ loggedUser: 'testuser1' }) - .expect(403); - - expect(responseWithInvalidLoggedUser.body).toHaveProperty('error'); }); it('Should return the user when the username is valid when getting the profile', async () => { diff --git a/users/routes/user-routes.js b/users/routes/user-routes.js index 663759c5..ab6ed088 100644 --- a/users/routes/user-routes.js +++ b/users/routes/user-routes.js @@ -588,12 +588,14 @@ router.get('/statistics/:username', async (req,res) => { } }); - const hasCommonGroup = userGroups.some(userGroup => { - return loggedUserGroups.some(loggedUserGroup => loggedUserGroup.groupName === userGroup.groupName); - }); - - if(!hasCommonGroup){ - return res.status(403).json({ error: 'You are not allowed to see this user statistics' }); + if (loggedUserGroups.length != 0 && userGroups != 0){ + const hasCommonGroup = userGroups.some(userGroup => { + return loggedUserGroups.some(loggedUserGroup => loggedUserGroup.groupName === userGroup.groupName); + }); + + if(!hasCommonGroup){ + return res.status(403).json({ error: 'You are not allowed to see this user statistics' }); + } } } diff --git a/users/services/user-model.js b/users/services/user-model.js index 89a59234..446a59f5 100644 --- a/users/services/user-model.js +++ b/users/services/user-model.js @@ -208,7 +208,7 @@ const QuestionsRecord = sequelize.define('QuestionsRecord', { }); // Synchronize the model with the database -sequelize.sync() +sequelize.sync({force:true}) .then(() => { console.log('Model synchronized successfully with the database'); }) diff --git a/webapp/e2e/features/discoveringCitiesGame.feature b/webapp/e2e/features/discoveringCitiesGame.feature index 6a63bd96..415f3d2e 100644 --- a/webapp/e2e/features/discoveringCitiesGame.feature +++ b/webapp/e2e/features/discoveringCitiesGame.feature @@ -3,9 +3,9 @@ Feature: Answer a question Scenario: Answering a question correctly Given A question When I click on the correct answer button - Then The button turns green + Then The selected answer is marked as right Scenario: Answering a question incorrectly Given A question When I click on an incorrect answer button - Then The button turns red \ No newline at end of file + Then The selected answer is marked as wrong \ No newline at end of file diff --git a/webapp/e2e/features/theChallangeGame.feauture b/webapp/e2e/features/theChallangeGame.feauture new file mode 100644 index 00000000..6a63bd96 --- /dev/null +++ b/webapp/e2e/features/theChallangeGame.feauture @@ -0,0 +1,11 @@ +Feature: Answer a question + +Scenario: Answering a question correctly + Given A question + When I click on the correct answer button + Then The button turns green + +Scenario: Answering a question incorrectly + Given A question + When I click on an incorrect answer button + Then The button turns red \ No newline at end of file diff --git a/webapp/e2e/features/wiseMenStackGame.feature b/webapp/e2e/features/wiseMenStackGame.feature index 6a63bd96..da9e1fa6 100644 --- a/webapp/e2e/features/wiseMenStackGame.feature +++ b/webapp/e2e/features/wiseMenStackGame.feature @@ -3,9 +3,4 @@ Feature: Answer a question Scenario: Answering a question correctly Given A question When I click on the correct answer button - Then The button turns green - -Scenario: Answering a question incorrectly - Given A question - When I click on an incorrect answer button - Then The button turns red \ No newline at end of file + Then The selected answer is marked as right \ No newline at end of file diff --git a/webapp/e2e/steps/discoveringCitiesGame.steps.js b/webapp/e2e/steps/discoveringCitiesGame.steps.js index 379bd386..12ccb40e 100644 --- a/webapp/e2e/steps/discoveringCitiesGame.steps.js +++ b/webapp/e2e/steps/discoveringCitiesGame.steps.js @@ -5,7 +5,7 @@ const feature = loadFeature('./features/discoveringCitiesGame.feature'); let page; let browser; - + defineFeature(feature, test => { beforeAll(async () => { @@ -29,7 +29,7 @@ defineFeature(feature, test => { question: 'Which is the capital of Spain?', options: ['Madrid', 'Barcelona', 'Paris', 'London'], correctAnswer: 'Madrid', - categories: ['Geography'], + categories: ['Cities'], language: 'en' }]) }); @@ -39,8 +39,6 @@ defineFeature(feature, test => { }); //Way of setting up the timeout setDefaultOptions({ timeout: 10000 }) - - }); beforeEach(async () => { @@ -67,52 +65,48 @@ defineFeature(feature, test => { test('Answering a question correctly', ({given,when,then}) => { given('A question', async () => { - //await expect(page.findByText('Which is the capital of Spain?')); - const question = await page.$['data-testid="question"']; - await expect(page).toMatchElement("div", { text: 'Which is the capital of Spain?'}); - expect(question).not.toBeNull(); - - const answers = await page.$x('//*[@data-testid="answer"]'); - expect(answers.length).toBe(4); + const question = await page.$['data-testid="question"']; + await expect(page).toMatchElement("div", { text: 'WHICH IS THE CAPITAL OF SPAIN?'}); + expect(question).not.toBeNull(); + + const answers = await page.$x('//*[contains(@data-testid, "answer")]'); + expect(answers.length).toBe(4); }); when('I click on the correct answer button', async () => { - const answers = await page.$x('(//*[@data-testid="answer"])[1]'); - await answers[0].click(); + const answers = await page.$x('//*[contains(@data-testid, "answer")]'); + await answers[0].click(); }); - then('The button turns green', async () => { - const answerButton = await page.$x('(//*[@data-testid="answer"])[1]'); - const textoBoton = await page.evaluate(button => button.innerText, answerButton[0]); - await expect(textoBoton).toMatch(/Madrid/i); - await expect(page).toMatchElement("button", { style: { color: 'green' } }); + then('The selected answer is marked as right', async () => { + const answer = await page.$x('//*[contains(@data-testid, "success")]'); + expect(answer.length).toBe(1); + const textoBoton = await page.evaluate(button => button.innerText, answer[0]); + await expect(textoBoton).toMatch(/Madrid/i); }); }) test('Answering a question incorrectly', ({given,when,then}) => { given('A question', async () => { - //await expect(page.findByText('Which is the capital of Spain?')); - const question = await page.$['data-testid="question"']; - await expect(page).toMatchElement("div", { text: 'Which is the capital of Spain?'}); - expect(question).not.toBeNull(); - - const answers = await page.$x('//*[@data-testid="answer"]'); - expect(answers.length).toBe(4); + const question = await page.$['data-testid="question"']; + await expect(page).toMatchElement("div", { text: 'WHICH IS THE CAPITAL OF SPAIN?'}); + expect(question).not.toBeNull(); + + const answers = await page.$x('//*[contains(@data-testid, "answer")]'); + expect(answers.length).toBe(4); }); when('I click on an incorrect answer button', async () => { - const answers = await page.$x('(//*[@data-testid="answer"])[2]'); - await answers[0].click(); + const answers = await page.$x('//*[contains(@data-testid, "answer")]'); + await answers[1].click(); }); - then('The button turns red', async () => { - const answerButton = await page.$x('(//*[@data-testid="answer"])[2]'); - const textoBoton = await page.evaluate(button => button.innerText, answerButton[0]); - await expect(textoBoton).toMatch(/Barcelona/i); - await expect(page).toMatchElement("button", { style: { color: 'red' } }); - await expect(page).toMatchElement("button", { style: { color: 'green' } }); - + then('The selected answer is marked as wrong', async () => { + const answer = await page.$x('//*[contains(@data-testid, "fail")]'); + expect(answer.length).toBe(1); + const textoBoton = await page.evaluate(button => button.innerText, answer[0]); + await expect(textoBoton).toMatch(/Barcelona/i); }); }) diff --git a/webapp/e2e/steps/game.steps.js b/webapp/e2e/steps/game.steps.js index f15c626b..7886f189 100644 --- a/webapp/e2e/steps/game.steps.js +++ b/webapp/e2e/steps/game.steps.js @@ -1,11 +1,12 @@ const puppeteer = require('puppeteer'); const { defineFeature, loadFeature }=require('jest-cucumber'); +const { expect } = require('expect-puppeteer'); const setDefaultOptions = require('expect-puppeteer').setDefaultOptions const feature = loadFeature('./features/game.feature'); let page; let browser; - + defineFeature(feature, test => { beforeAll(async () => { @@ -45,11 +46,12 @@ defineFeature(feature, test => { beforeEach(async () => { await page - .goto("http://localhost:3000/game", { + .goto("http://localhost:3000/discoveringCitiesGame", { waitUntil: "networkidle0", }) .catch(() => {}); + //"mock" login await page.evaluate(() => { localStorage.clear(); diff --git a/webapp/e2e/steps/theCallange.steps.js b/webapp/e2e/steps/theCallange.steps.js new file mode 100644 index 00000000..8b3d7d59 --- /dev/null +++ b/webapp/e2e/steps/theCallange.steps.js @@ -0,0 +1,139 @@ +const puppeteer = require('puppeteer'); +const { defineFeature, loadFeature }=require('jest-cucumber'); +const setDefaultOptions = require('expect-puppeteer').setDefaultOptions +const feature = loadFeature('./features/theChallangeGame.feauture'); + +let page; +let browser; + +defineFeature(feature, test => { + + beforeAll(async () => { + + browser = process.env.GITHUB_ACTIONS + ? await puppeteer.launch() + : await puppeteer.launch({ headless: false, slowMo: 40 }); + page = await browser.newPage(); + + await page.setRequestInterception(true); + + page.on('request', (req) => { + if(req.url().endsWith('/Geography')) { + req.respond({ + status: 200, + headers: { + 'Access-Control-Allow-Origin': '*' + }, + contentType: 'application/json', + body: JSON.stringify([{ + question: 'Which is the capital of Spain?', + options: ['Madrid', 'Barcelona', 'Paris', 'London'], + correctAnswer: 'Madrid', + categories: ['Geography'], + language: 'en' + }]) + }); + } else { + req.continue(); + } + }); + //Way of setting up the timeout + setDefaultOptions({ timeout: 10000 }) + + + }); + + beforeEach(async () => { + await page + .goto("http://localhost:3000/theChallengeGame", { + waitUntil: "networkidle0", + }) + .catch(() => {}); + + //"mock" login + await page.evaluate(() => { + localStorage.clear(); + localStorage.setItem('sessionId', 'fictitiousSessionId12345'); + }); + + await page + .goto("http://localhost:3000/theChallengeGame", { + waitUntil: "networkidle0", + }) + .catch(() => {}); + }); + + + test('Answering a question correctly', ({given,when,then}) => { + + given('A question', async () => { + const button = await page.$('[data-testid="start-button"]'); + await button.click(); + + //await expect(page.findByText('Which is the capital of Spain?')); + const question = await page.$['data-testid="question"']; + await expect(page).toMatchElement("div", { text: 'Which is the capital of Spain?'.toUpperCase()}); + expect(question).not.toBeNull(); + + const answers = await page.$x('//*[contains(@data-testid, "success") or contains(@data-testid, "failure") or contains(@data-testid, "answer")]'); + expect(answers.length).toBe(4); + }); + + when('I click on the correct answer button', async () => { + const answers = await page.$x('//*[contains(@data-testid, "success") or contains(@data-testid, "failure") or contains(@data-testid, "answer")]'); + await answers[0].click(); + + }); + + then('The button turns green', async () => { + /*const answerButton = await page.$x('(//*[@data-testid="answer"])[1]'); + const textoBoton1 = await page.evaluate(button => button.innerText, answerButton[0]); + const textoBoton2 = await page.evaluate(button => button.innerText, answerButton[1]); + if(textoBoton1 === "Madrid") { + await expect(textoBoton1).toMatch(/Madrid/i); + } else { + await expect(textoBoton2).toMatch(/Madrid/i); + }*/ + await expect(page).toMatchElement("button", { style: { color: 'green' } }); + }); + }) + + test('Answering a question incorrectly', ({given,when,then}) => { + + given('A question', async () => { + const button = await page.$('[data-testid="start-button"]'); + await button.click(); + + //await expect(page.findByText('Which is the capital of Spain?')); + const question = await page.$['data-testid="question"']; + await expect(page).toMatchElement("div", { text: 'Which is the capital of Spain?'.toUpperCase()}); + expect(question).not.toBeNull(); + + const answers = await page.$x('//*[contains(@data-testid, "success") or contains(@data-testid, "failure") or contains(@data-testid, "answer")]'); + expect(answers.length).toBe(4); + }); + + when('I click on an incorrect answer button', async () => { + const answers = await page.$x('//*[contains(@data-testid, "success") or contains(@data-testid, "failure") or contains(@data-testid, "answer")]'); + await answers[1].click(); + }); + + then('The button turns red', async () => { + /*const answerButton = await page.$x('(//*[@data-testid="answer"])[2]'); + const textoBoton1 = await page.evaluate(button => button.innerText, answerButton[0]); + const textoBoton2 = await page.evaluate(button => button.innerText, answerButton[1]); + if(textoBoton1 !== "Madrid") { + await expect(textoBoton1).not.toMatch(/Madrid/i); + } else { + await expect(textoBoton2).toMatch(/Madrid/i); + }*/ + await expect(page).toMatchElement("button", { style: { color: 'red' } }); + await expect(page).toMatchElement("button", { style: { color: 'green' } }); + }); + }) + + afterAll(async ()=>{ + browser.close() + }) + +}); \ No newline at end of file diff --git a/webapp/e2e/steps/wiseMenStackGame.steps.js b/webapp/e2e/steps/wiseMenStackGame.steps.js index 22f11b35..974dd592 100644 --- a/webapp/e2e/steps/wiseMenStackGame.steps.js +++ b/webapp/e2e/steps/wiseMenStackGame.steps.js @@ -10,7 +10,7 @@ defineFeature(feature, test => { beforeAll(async () => { - browser = process.env.GITHUB_ACTIONS + browser = process.env.GITHUB_ACTIONS ? await puppeteer.launch() : await puppeteer.launch({ headless: false, slowMo: 40 }); page = await browser.newPage(); @@ -39,8 +39,6 @@ defineFeature(feature, test => { }); //Way of setting up the timeout setDefaultOptions({ timeout: 10000 }) - - }); beforeEach(async () => { @@ -49,7 +47,7 @@ defineFeature(feature, test => { waitUntil: "networkidle0", }) .catch(() => {}); - + //"mock" login await page.evaluate(() => { localStorage.clear(); @@ -67,77 +65,32 @@ defineFeature(feature, test => { test('Answering a question correctly', ({given,when,then}) => { given('A question', async () => { - const button = await page.$('[data-testid="start-button"]'); - await button.click(); + const button = await page.$('[data-testid="start-button"]'); + await button.click(); - //await expect(page.findByText('Which is the capital of Spain?')); - const question = await page.$['data-testid="question"']; - await expect(page).toMatchElement("div", { text: 'Which is the capital of Spain?'}); - expect(question).not.toBeNull(); - - const answers = await page.$x('//*[@data-testid="answer"]'); - expect(answers.length).toBe(2); + const question = await page.$['data-testid="question"']; + expect(question).not.toBeNull(); + await expect(page).toMatchElement("div", { text: 'WHICH IS THE CAPITAL OF SPAIN?'}); + + const answers = await page.$x('//*[contains(@data-testid, "answer")]'); + expect(answers.length).toBe(2); }); when('I click on the correct answer button', async () => { - const answers = await page.$x('//*[@data-testid="answer"]'); - const textoBoton1 = await page.evaluate(button => button.innerText, answers[0]); - if(textoBoton1 === "Madrid") { - await answers[0].click(); - } else { - await answers[1].click(); - } - }); - - then('The button turns green', async () => { - /*const answerButton = await page.$x('(//*[@data-testid="answer"])[1]'); - const textoBoton1 = await page.evaluate(button => button.innerText, answerButton[0]); - const textoBoton2 = await page.evaluate(button => button.innerText, answerButton[1]); - if(textoBoton1 === "Madrid") { - await expect(textoBoton1).toMatch(/Madrid/i); - } else { - await expect(textoBoton2).toMatch(/Madrid/i); - }*/ - await expect(page).toMatchElement("button", { style: { color: 'green' } }); - }); - }) - - test('Answering a question incorrectly', ({given,when,then}) => { - - given('A question', async () => { - const button = await page.$('[data-testid="start-button"]'); - await button.click(); - - //await expect(page.findByText('Which is the capital of Spain?')); - const question = await page.$['data-testid="question"']; - await expect(page).toMatchElement("div", { text: 'Which is the capital of Spain?'}); - expect(question).not.toBeNull(); - - const answers = await page.$x('//*[@data-testid="answer"]'); - expect(answers.length).toBe(2); - }); - - when('I click on an incorrect answer button', async () => { - const answers = await page.$x('//*[@data-testid="answer"]'); - const textoBoton1 = await page.evaluate(button => button.innerText, answers[0]); - if(textoBoton1 !== "Madrid") { - await answers[0].click(); - } else { - await answers[1].click(); - } + const answers = await page.$x('//*[contains(@data-testid, "answer")]'); + const textoBoton1 = await page.evaluate(button => button.innerText, answers[0]); + if(textoBoton1 === "MADRID") { + await answers[0].click(); + } else { + await answers[1].click(); + } }); - then('The button turns red', async () => { - /*const answerButton = await page.$x('(//*[@data-testid="answer"])[2]'); - const textoBoton1 = await page.evaluate(button => button.innerText, answerButton[0]); - const textoBoton2 = await page.evaluate(button => button.innerText, answerButton[1]); - if(textoBoton1 !== "Madrid") { - await expect(textoBoton1).not.toMatch(/Madrid/i); - } else { - await expect(textoBoton2).toMatch(/Madrid/i); - }*/ - await expect(page).toMatchElement("button", { style: { color: 'red' } }); - await expect(page).toMatchElement("button", { style: { color: 'green' } }); + then('The selected answer is marked as right', async () => { + const answer = await page.$x('//*[contains(@data-testid, "success")]'); + expect(answer.length).toBe(1); + const textoBoton = await page.evaluate(button => button.innerText, answer[0]); + await expect(textoBoton).toMatch(/Madrid/i); }); }) diff --git a/webapp/package-lock.json b/webapp/package-lock.json index 8e61c028..b85d1ed4 100644 --- a/webapp/package-lock.json +++ b/webapp/package-lock.json @@ -21,6 +21,7 @@ "babel": "^6.23.0", "cross-env": "^7.0.3", "express": "^4.19.2", + "get-uri": "^6.0.3", "i18next": "^23.11.0", "pupeteer": "^0.0.1", "react": "^18.2.0", @@ -30,7 +31,7 @@ "react-i18next": "^14.1.0", "react-router-dom": "^6.22.1", "react-scripts": "5.0.1", - "sequelize": "^6.37.2", + "sequelize": "^6.37.3", "socket.io-client": "^4.7.5", "sqlite3": "^5.1.7", "web-vitals": "^3.5.1" @@ -7589,7 +7590,6 @@ "version": "5.0.5", "resolved": "https://registry.npmjs.org/basic-ftp/-/basic-ftp-5.0.5.tgz", "integrity": "sha512-4Bcg1P8xhUuqcii/S0Z9wiHIrQVPMermM1any+MX5GeGD7faD3/msQUDGLol9wOcz4/jbg/WJnGqoJF6LiBdtg==", - "dev": true, "engines": { "node": ">=10.0.0" } @@ -9438,7 +9438,6 @@ "version": "6.0.2", "resolved": "https://registry.npmjs.org/data-uri-to-buffer/-/data-uri-to-buffer-6.0.2.tgz", "integrity": "sha512-7hvf7/GW8e86rW0ptuwS3OcBGDjIi6SZva7hCyWC0yYry2cOPmLIjXAUHI6DK2HsnwJd9ifmt57i8eV2n4YNpw==", - "dev": true, "engines": { "node": ">= 14" } @@ -12214,7 +12213,6 @@ "version": "6.0.3", "resolved": "https://registry.npmjs.org/get-uri/-/get-uri-6.0.3.tgz", "integrity": "sha512-BzUrJBS9EcUb4cFol8r4W3v1cPsSyajLSthNkz5BxbpDcHN5tIrM10E2eNvfnvBn3DaT3DUgx0OpsBKkaOpanw==", - "dev": true, "dependencies": { "basic-ftp": "^5.0.2", "data-uri-to-buffer": "^6.0.2", @@ -12229,7 +12227,6 @@ "version": "11.2.0", "resolved": "https://registry.npmjs.org/fs-extra/-/fs-extra-11.2.0.tgz", "integrity": "sha512-PmDi3uwK5nFuXh7XDTlVnS17xJS7vW36is2+w3xcv8SVxiB4NyATf4ctkVY5bkSjX0Y4nbvZCq1/EjtEyr9ktw==", - "dev": true, "dependencies": { "graceful-fs": "^4.2.0", "jsonfile": "^6.0.1", @@ -25548,9 +25545,9 @@ "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==" }, "node_modules/sequelize": { - "version": "6.37.2", - "resolved": "https://registry.npmjs.org/sequelize/-/sequelize-6.37.2.tgz", - "integrity": "sha512-bnb7swGANONXCTrVyebpOOZssLwQrVkYX2tcC6qOIvH+P+OhsoMBi7c3GXI5bC+Z4b4tOl+kQy6yeqLCZ1YQAQ==", + "version": "6.37.3", + "resolved": "https://registry.npmjs.org/sequelize/-/sequelize-6.37.3.tgz", + "integrity": "sha512-V2FTqYpdZjPy3VQrZvjTPnOoLm0KudCRXfGWp48QwhyPPp2yW8z0p0sCYZd/em847Tl2dVxJJ1DR+hF+O77T7A==", "funding": [ { "type": "opencollective", @@ -34722,8 +34719,7 @@ "basic-ftp": { "version": "5.0.5", "resolved": "https://registry.npmjs.org/basic-ftp/-/basic-ftp-5.0.5.tgz", - "integrity": "sha512-4Bcg1P8xhUuqcii/S0Z9wiHIrQVPMermM1any+MX5GeGD7faD3/msQUDGLol9wOcz4/jbg/WJnGqoJF6LiBdtg==", - "dev": true + "integrity": "sha512-4Bcg1P8xhUuqcii/S0Z9wiHIrQVPMermM1any+MX5GeGD7faD3/msQUDGLol9wOcz4/jbg/WJnGqoJF6LiBdtg==" }, "batch": { "version": "0.6.1", @@ -36042,8 +36038,7 @@ "data-uri-to-buffer": { "version": "6.0.2", "resolved": "https://registry.npmjs.org/data-uri-to-buffer/-/data-uri-to-buffer-6.0.2.tgz", - "integrity": "sha512-7hvf7/GW8e86rW0ptuwS3OcBGDjIi6SZva7hCyWC0yYry2cOPmLIjXAUHI6DK2HsnwJd9ifmt57i8eV2n4YNpw==", - "dev": true + "integrity": "sha512-7hvf7/GW8e86rW0ptuwS3OcBGDjIi6SZva7hCyWC0yYry2cOPmLIjXAUHI6DK2HsnwJd9ifmt57i8eV2n4YNpw==" }, "data-urls": { "version": "2.0.0", @@ -38100,7 +38095,6 @@ "version": "6.0.3", "resolved": "https://registry.npmjs.org/get-uri/-/get-uri-6.0.3.tgz", "integrity": "sha512-BzUrJBS9EcUb4cFol8r4W3v1cPsSyajLSthNkz5BxbpDcHN5tIrM10E2eNvfnvBn3DaT3DUgx0OpsBKkaOpanw==", - "dev": true, "requires": { "basic-ftp": "^5.0.2", "data-uri-to-buffer": "^6.0.2", @@ -38112,7 +38106,6 @@ "version": "11.2.0", "resolved": "https://registry.npmjs.org/fs-extra/-/fs-extra-11.2.0.tgz", "integrity": "sha512-PmDi3uwK5nFuXh7XDTlVnS17xJS7vW36is2+w3xcv8SVxiB4NyATf4ctkVY5bkSjX0Y4nbvZCq1/EjtEyr9ktw==", - "dev": true, "requires": { "graceful-fs": "^4.2.0", "jsonfile": "^6.0.1", @@ -47937,9 +47930,9 @@ } }, "sequelize": { - "version": "6.37.2", - "resolved": "https://registry.npmjs.org/sequelize/-/sequelize-6.37.2.tgz", - "integrity": "sha512-bnb7swGANONXCTrVyebpOOZssLwQrVkYX2tcC6qOIvH+P+OhsoMBi7c3GXI5bC+Z4b4tOl+kQy6yeqLCZ1YQAQ==", + "version": "6.37.3", + "resolved": "https://registry.npmjs.org/sequelize/-/sequelize-6.37.3.tgz", + "integrity": "sha512-V2FTqYpdZjPy3VQrZvjTPnOoLm0KudCRXfGWp48QwhyPPp2yW8z0p0sCYZd/em847Tl2dVxJJ1DR+hF+O77T7A==", "requires": { "@types/debug": "^4.1.8", "@types/validator": "^13.7.17", diff --git a/webapp/package.json b/webapp/package.json index 442dc637..3ccc8398 100644 --- a/webapp/package.json +++ b/webapp/package.json @@ -16,6 +16,7 @@ "babel": "^6.23.0", "cross-env": "^7.0.3", "express": "^4.19.2", + "get-uri": "^6.0.3", "i18next": "^23.11.0", "pupeteer": "^0.0.1", "react": "^18.2.0", @@ -25,7 +26,7 @@ "react-i18next": "^14.1.0", "react-router-dom": "^6.22.1", "react-scripts": "5.0.1", - "sequelize": "^6.37.2", + "sequelize": "^6.37.3", "socket.io-client": "^4.7.5", "sqlite3": "^5.1.7", "web-vitals": "^3.5.1" diff --git a/webapp/src/App.js b/webapp/src/App.js index eb8254d5..d5c768ba 100644 --- a/webapp/src/App.js +++ b/webapp/src/App.js @@ -6,22 +6,22 @@ import NavBar from './components/NavBar'; import Footer from './components/Footer'; import Home from './pages/Home'; import Homepage from './pages/Homepage'; -import Game from './pages/Game'; -import DiscoveringCitiesGame from './pages/DiscoveringCitiesGame'; -import WiseMenStackGame from './pages/WiseMenStackGame'; +import Game from './pages/games/Game'; +import DiscoveringCitiesGame from './pages/games/DiscoveringCitiesGame'; +import WiseMenStackGame from './pages/games/WiseMenStackGame'; import Groups from './pages/Groups'; import GroupDetails from './pages/GroupDetails'; import Statistics from './pages/Statistics'; import Ranking from './pages/Ranking' import Profile from './pages/Profile' -import MultiplayerRoom from './pages/MultiplayerRoom'; -import MultiplayerGame from './pages/MultiplayerGame'; -import TheChallengeGame from './pages/TheChallengeGame'; -import WarmQuestion from './pages/WarmQuestionGame'; +import MultiplayerRoom from './pages/games/MultiplayerRoom'; +import MultiplayerGame from './pages/games/MultiplayerGame'; +import TheChallengeGame from './pages/games/TheChallengeGame'; +import WarmQuestionGame from './pages/games/WarmQuestionGame'; import {Route, Routes} from 'react-router-dom'; import { createTheme, ThemeProvider } from '@mui/material/styles'; import { Box } from '@mui/material'; -import PrivateRoute from './PrivateRoute'; +import PrivateRoute from './pages/PrivateRoute'; import NotFound from './pages/NotFound'; const theme = createTheme({ @@ -66,7 +66,7 @@ function App() { }/> }/> }/> - }/> + }/> }/> } /> }/> diff --git a/webapp/src/__tests__/components/Chat.test.js b/webapp/src/__tests__/components/Chat.test.js index 999e419a..6e786cf0 100644 --- a/webapp/src/__tests__/components/Chat.test.js +++ b/webapp/src/__tests__/components/Chat.test.js @@ -33,8 +33,8 @@ describe('Chat component', () => { test('sends a message', async () => { - jest.mock('../../pages/MultiplayerRoom', () => ({ - ...jest.requireActual('../../pages/MultiplayerRoom'), + jest.mock('../../pages/games/MultiplayerRoom', () => ({ + ...jest.requireActual('../../pages/games/MultiplayerRoom'), generateRoomCode: jest.fn().mockReturnValue('AAAAA'), })); diff --git a/webapp/src/__tests__/components/NavBar.test.js b/webapp/src/__tests__/components/NavBar.test.js index a19aa416..cd547778 100644 --- a/webapp/src/__tests__/components/NavBar.test.js +++ b/webapp/src/__tests__/components/NavBar.test.js @@ -26,8 +26,10 @@ describe('NavBar component', () => { ); - const logo = screen.getByAltText('Logo'); - await expect(logo).toBeInTheDocument(); + const logo = screen.getAllByAltText('Logo'); + // There should be the one for mobile and the one for normal devices + await expect(logo[0]).toBeInTheDocument(); + await expect(logo[1]).toBeInTheDocument(); }); it('should render log-in option', async () => { @@ -42,6 +44,7 @@ describe('NavBar component', () => { await expect(logIn).toBeInTheDocument(); }); + // As there s a link for the menu (mobiles version) and in the propper nav, we have to check 2 elements appear it('should render navigation links', async () => { render( diff --git a/webapp/src/__tests__/pages/GroupDetails.test.js b/webapp/src/__tests__/pages/GroupDetails.test.js index 48d01a99..8add14e0 100644 --- a/webapp/src/__tests__/pages/GroupDetails.test.js +++ b/webapp/src/__tests__/pages/GroupDetails.test.js @@ -42,9 +42,9 @@ describe('GroupDetails component', () => { await waitFor(() => { expect(getByText(groupInfo.name)).toBeInTheDocument(); - expect(getByText(`Creator: ${groupInfo.creator}`)).toBeInTheDocument(); - expect(getByText(`Created in: ${new Date(groupInfo.createdAt).toLocaleDateString()}`)).toBeInTheDocument(); - expect(getByText(`Members (${groupInfo.users.length}/20):`)).toBeInTheDocument(); + expect(getByText(`${groupInfo.creator}`)).toBeInTheDocument(); + expect(getByText(`${new Date(groupInfo.createdAt).toLocaleDateString()}`)).toBeInTheDocument(); + expect(getByText(`${groupInfo.users.length}/20`)).toBeInTheDocument(); groupInfo.users.forEach(user => { expect(getByText(user)).toBeInTheDocument(); }); diff --git a/webapp/src/__tests__/pages/Home.test.js b/webapp/src/__tests__/pages/Home.test.js index 21fe3cfc..2e6dd511 100644 --- a/webapp/src/__tests__/pages/Home.test.js +++ b/webapp/src/__tests__/pages/Home.test.js @@ -1,6 +1,7 @@ import React from 'react'; import { render, screen } from '@testing-library/react'; import Home from '../../pages/Home'; +import '../../localize/i18n'; // Hacemos un mock del módulo '@mui/material' y su hook useMediaQuery jest.mock('@mui/material/useMediaQuery', () => ({ diff --git a/webapp/src/__tests__/pages/Homepage.test.js b/webapp/src/__tests__/pages/Homepage.test.js index c9f4992b..b607699c 100644 --- a/webapp/src/__tests__/pages/Homepage.test.js +++ b/webapp/src/__tests__/pages/Homepage.test.js @@ -1,6 +1,7 @@ import React from 'react'; import { render, fireEvent, screen } from '@testing-library/react'; import Homepage from '../../pages/Homepage'; +import '../../localize/i18n'; // Hacemos un mock del módulo '@mui/material' y su hook useMediaQuery jest.mock('@mui/material/useMediaQuery', () => ({ @@ -16,7 +17,7 @@ describe('Homepage', () => { it('renders video and play button', () => { render(); expect(screen.getByTestId('video')).toBeInTheDocument(); - expect(screen.getByText('PLAY')).toBeInTheDocument(); + expect(screen.getByText("PLAY")).toBeInTheDocument(); }); it('loads game buttons dynamically based on data', async () => { diff --git a/webapp/src/__tests__/pages/Instructions.test.js b/webapp/src/__tests__/pages/Instructions.test.js index e217269f..c5d779c5 100644 --- a/webapp/src/__tests__/pages/Instructions.test.js +++ b/webapp/src/__tests__/pages/Instructions.test.js @@ -2,32 +2,33 @@ import React from 'react'; import { render, fireEvent, screen, waitFor } from '@testing-library/react'; import Instructions from '../../pages/Instructions'; import { BrowserRouter as Router } from 'react-router-dom'; +import '../../localize/i18n'; jest.mock('../../data/gameInfo.json', () => ({ __esModule: true, default: [ { - nombre: "WISE MEN STACK", + nombre: "Wise Men Stack", descripcion: "The player chooses a topic from five available options and must answer a battery of questions related to it within 60 seconds. For each question, the host provides two options. If the contestant guesses correctly, they win €20; otherwise, they move on to the next question (as the correct answer would be the other option). If the time runs out before the question is fully asked and both possible answers are provided, the contestant may still answer it; however, if the statement hasn't been completed (or the options weren't provided), they cannot answer.", foto: "../gameImg/foto0.png" }, { - nombre: "WARM QUESTION", + nombre: "Warm Question", descripcion: "It consists of ten topics of varied themes. For each correct answer, €100 is earned, and €10 are lost if the contestant passes, does not respond, or answers incorrectly.", foto: "../gameImg/foto1.jpg" }, { - nombre: "DISCOVERING CITIES", + nombre: "Discovering Cities", descripcion: "In the 'Discovering Cities' game mode, the contestant will face a challenge where they will be repeatedly asked questions referring to different cities around the world. To successfully overcome the challenge, the contestant must answer as many questions as possible correctly throughout the test.", foto: "../gameImg/foto2.png" }, { - nombre: "THE CHALLENGE", + nombre: "Challenge", descripcion: "The 'Challenge' game mode is the quintessential game mode, as it allows you to customize the match to your liking. This game mode is tailored for those who wish to practice certain game formats before engaging in our various other game modes.", foto: "../gameImg/foto3.jpg" }, { - nombre: "ONLINE MODE", + nombre: "Multiplayer", descripcion: "Create a room for other player to join and play 1vs1", foto: "../gameImg/foto3.jpg" } @@ -55,10 +56,10 @@ describe('Instructions component', () => { fireEvent.click(screen.getAllByRole('button')[0]); // Click the first game button - const gameNames = await screen.findAllByText("WISE MEN STACK"); //Look for the text "WISE MEN STACK" + const gameNames = await screen.findAllByText("Wise Men Stack"); //Look for the text "WISE MEN STACK" expect(gameNames).toHaveLength(2); // Check the expected number of matches - expect(gameNames[0]).toHaveTextContent("WISE MEN STACK"); + expect(gameNames[0]).toHaveTextContent("Wise Men Stack"); }); it('Hides game information when the same game button is clicked again', async () => { @@ -71,12 +72,12 @@ describe('Instructions component', () => { const gameButton = screen.getAllByRole('button')[0]; //Selecciona el primer boton fireEvent.click(gameButton); // Hace click en el boton indicado - const gameNames = await screen.findAllByText("WISE MEN STACK"); //Finds all components that have the indicated text + const gameNames = await screen.findAllByText("Wise Men Stack"); //Finds all components that have the indicated text expect(gameNames).toHaveLength(2); // Check the expected number of matches fireEvent.click(gameButton); // Hide info - const gameNames2 = await screen.findAllByText("WISE MEN STACK"); //Finds all components that have the indicated text + const gameNames2 = await screen.findAllByText("Wise Men Stack"); //Finds all components that have the indicated text expect(gameNames2).toHaveLength(1); // Check the expected number of matches await waitFor(() => { @@ -93,12 +94,12 @@ describe('Instructions component', () => { fireEvent.click(screen.getAllByRole('button')[0]); // Display first game info - const gameNames = await screen.findAllByText("WISE MEN STACK"); //Finds all components that have the indicated text + const gameNames = await screen.findAllByText("Wise Men Stack"); //Finds all components that have the indicated text expect(gameNames).toHaveLength(2); // Check the expected number of matches fireEvent.click(screen.getAllByRole('button')[1]); // Switch to second game info - const gameName = await screen.findAllByText("WARM QUESTION"); //Finds all components that have the indicated text + const gameName = await screen.findAllByText("Warm Question"); //Finds all components that have the indicated text expect(gameName).toHaveLength(2); // Check the expected number of matches await waitFor(() => { @@ -109,27 +110,27 @@ describe('Instructions component', () => { const mockGameData = [ { - nombre: "WISE MEN STACK", + nombre: "Wise Men Stack", descripcion: "The player chooses a topic from five available options and must answer a battery of questions related to it within 60 seconds. For each question, the host provides two options. If the contestant guesses correctly, they win €20; otherwise, they move on to the next question (as the correct answer would be the other option). If the time runs out before the question is fully asked and both possible answers are provided, the contestant may still answer it; however, if the statement hasn't been completed (or the options weren't provided), they cannot answer.", foto: "../gameImg/foto0.png" }, { - nombre: "WARM QUESTION", + nombre: "Warm Question", descripcion: "It consists of ten topics of varied themes. For each correct answer, €100 is earned, and €10 are lost if the contestant passes, does not respond, or answers incorrectly.", foto: "../gameImg/foto1.jpg" }, { - nombre: "DISCOVERING CITIES", + nombre: "Discovering Cities", descripcion: "In the 'Discovering Cities' game mode, the contestant will face a challenge where they will be repeatedly asked questions referring to different cities around the world. To successfully overcome the challenge, the contestant must answer as many questions as possible correctly throughout the test.", foto: "../gameImg/foto2.png" }, { - nombre: "THE CHALLENGE", + nombre: "Challenge", descripcion: "The 'Challenge' game mode is the quintessential game mode, as it allows you to customize the match to your liking. This game mode is tailored for those who wish to practice certain game formats before engaging in our various other game modes.", foto: "../gameImg/foto3.jpg" }, { - nombre: "ONLINE MODE", + nombre: "Multiplayer", descripcion: "Create a room for other player to join and play 1vs1", foto: "../gameImg/foto3.jpg" } @@ -142,6 +143,7 @@ describe('Instructions component', () => { // Click in the game button fireEvent.click(screen.getAllByRole('button')[index]); + console.log(game.descripcion); // Verify that the game name and description are in the document const gameName = await screen.findAllByText(game.nombre); expect(gameName).toHaveLength(2); // Check the expected number of matches diff --git a/webapp/src/__tests__/pages/Profile.test.js b/webapp/src/__tests__/pages/Profile.test.js index 2c50ceef..3f11cc61 100644 --- a/webapp/src/__tests__/pages/Profile.test.js +++ b/webapp/src/__tests__/pages/Profile.test.js @@ -7,6 +7,12 @@ import { SessionContext } from '../../SessionContext'; import Profile from '../../pages/Profile'; const mockAxios = new MockAdapter(axios); +const mockNavigate = jest.fn(); + +jest.mock('react-router-dom', () => ({ + ...jest.requireActual('react-router-dom'), + useNavigate: () => mockNavigate, +})); describe('Profile component', () => { const username = 'testuser'; @@ -23,9 +29,10 @@ describe('Profile component', () => { }); it('should fetch and display user information', async () => { + const updateAvatar = jest.fn(); mockAxios.onGet(`http://localhost:8000/profile`, { params: { username } }).reply(200, initialUserInfo); render( - + @@ -41,24 +48,30 @@ describe('Profile component', () => { }); it('should display an error if fetching user info fails', async () => { + const updateAvatar = jest.fn(); mockAxios.onGet(`http://localhost:8000/profile`, { params: { username } }).reply(400, { error: 'Error fetching user information' }); render( - + ); + + await waitFor(() => { + expect(screen.getByText('Error fetching user information')).toBeInTheDocument(); + }); }); it('should handle avatar selection and update', async () => { + const updateAvatar = jest.fn(); const newAvatar = 'bertinIcon.jpg'; mockAxios.onGet(`http://localhost:8000/profile`, { params: { username } }).reply(200, initialUserInfo); mockAxios.onPut(`http://localhost:8000/profile/${username}`, { imageUrl: newAvatar }).reply(200); render( - + @@ -72,15 +85,21 @@ describe('Profile component', () => { fireEvent.click(screen.getByTestId('alberto-button')); fireEvent.click(screen.getByTestId('confirm-button')); + + await waitFor(() => { + expect(mockAxios.history.put.length).toBe(1); + expect(mockAxios.history.put[0].data).toContain(newAvatar); + }); }); it('should handle avatar selection and update after choosing different characters', async () => { + const updateAvatar = jest.fn(); const newAvatar = 'teresaIcon.jpg'; mockAxios.onGet(`http://localhost:8000/profile`, { params: { username } }).reply(200, initialUserInfo); mockAxios.onPut(`http://localhost:8000/profile/${username}`, { imageUrl: newAvatar }).reply(200); render( - + @@ -101,15 +120,21 @@ describe('Profile component', () => { fireEvent.click(screen.getByTestId('maite-button')); fireEvent.click(screen.getByTestId('confirm-button')); + await waitFor(() => { + expect(mockAxios.history.put.length).toBe(1); + expect(mockAxios.history.put[0].data).toContain(newAvatar); + }); + }); it('should display an error if avatar update fails', async () => { + const updateAvatar = jest.fn(); const newAvatar = 'bertinIcon.jpg'; mockAxios.onGet(`http://localhost:8000/profile`, { params: { username } }).reply(200, initialUserInfo); mockAxios.onPost(`http://localhost:8000/profile/${username}`, { imageUrl: newAvatar }).reply(400, { error: 'Error updating user information' }); render( - + @@ -123,8 +148,10 @@ describe('Profile component', () => { fireEvent.click(screen.getByText('ALBERT')); fireEvent.click(screen.getByTestId('confirm-button')); - }); - + await waitFor(() => { + expect(screen.getByText('Error updating user information')).toBeInTheDocument(); + }); + }); }); \ No newline at end of file diff --git a/webapp/src/__tests__/pages/Statistics.test.js b/webapp/src/__tests__/pages/Statistics.test.js index ec49a0e9..ed24cbd5 100644 --- a/webapp/src/__tests__/pages/Statistics.test.js +++ b/webapp/src/__tests__/pages/Statistics.test.js @@ -239,9 +239,9 @@ describe('Statistics component', () => { await screen.findByText('STATISTICS'); - fireEvent.click(screen.getByText('Online Mode')); + fireEvent.click(screen.getByText('Multiplayer')); - expect(screen.getByText('Online Mode')).toBeInTheDocument(); + expect(screen.getByText('Multiplayer')).toBeInTheDocument(); expect(screen.getByText('Total Points:')).toBeInTheDocument(); expect(screen.getByText('15')).toBeInTheDocument(); expect(screen.getByText('Correctly Answered Questions:')).toBeInTheDocument(); @@ -253,7 +253,7 @@ describe('Statistics component', () => { expect(screen.getByText('Games Played:')).toBeInTheDocument(); expect(screen.getAllByText('12')); - fireEvent.click(screen.getByText('Show Questions Record')); + fireEvent.click(screen.getByText("Show Questions Record")); await screen.findByText('Game 04/11/2024, 14:00'); expect(screen.getByText('What is 1 + 1?')).toBeInTheDocument(); diff --git a/webapp/src/__tests__/pages/TheChallengeGame.test.js b/webapp/src/__tests__/pages/TheChallengeGame.test.js deleted file mode 100644 index 2b8296d8..00000000 --- a/webapp/src/__tests__/pages/TheChallengeGame.test.js +++ /dev/null @@ -1,163 +0,0 @@ -import React from 'react'; -import { render, fireEvent, screen, waitFor } from '@testing-library/react'; -import { SessionContext } from '../../SessionContext'; -import { BrowserRouter as Router } from 'react-router-dom'; -import axios from 'axios'; -import MockAdapter from 'axios-mock-adapter'; -import '../../localize/i18n'; -import Game from '../../pages/TheChallengeGame'; - -const mockAxios = new MockAdapter(axios); - -describe('Game component', () => { - beforeEach(() => { - mockAxios.reset(); - // Mockear respuestas de la API - mockAxios.onGet(`http://localhost:8000/questions/en/Geography`).reply(200, - [ - { - question: 'Which is the capital of Spain?', - options: ['Madrid', 'Barcelona', 'Paris', 'London'], - correctAnswer: 'Madrid' - } - ] - ); - - mockAxios.onPut(`http://localhost:8000/statistics`).reply(200, { success: true }); - mockAxios.onPut(`http://localhost:8000/questionsRecord`).reply(200, { success: true }); - - }); - - it('should render configuration window and start game', async () => { - render( - - - - - - ); - - // Espera a que aparezca la ventana de configuración - await waitFor(() => screen.getByText('Game Configuration')); - - // Simula la configuración del juego - const increaseButtons = screen.getAllByRole('button', { name: '+' }); - fireEvent.click(increaseButtons[0]); // Aumenta el número de rondas - fireEvent.click(increaseButtons[1]); // Aumenta el tiempo por pregunta - //fireEvent.change(screen.getByLabelText('Category:'), { target: { value: 'Sports' } }); - - // Inicia el juego - fireEvent.click(screen.getByRole('button', { name: 'Start game' })); - - // Espera a que aparezca la pregunta - await waitFor(() => screen.getByText('Which is the capital of Spain?')); - - expect(screen.findByText('1')); - //expect(screen.findByText('1/4')); - - // Verifica que el juego haya comenzado correctamente mostrando la pregunta y las opciones - expect(screen.getByText('Which is the capital of Spain?')).toBeInTheDocument(); - expect(screen.getByText('Madrid')).toBeInTheDocument(); - expect(screen.getByText('Barcelona')).toBeInTheDocument(); - expect(screen.getByText('Paris')).toBeInTheDocument(); - expect(screen.getByText('London')).toBeInTheDocument(); - }); - - it('should guess correct answer', async () => { - render( - - - - - - ); - - // Espera a que aparezca la ventana de configuración - await waitFor(() => screen.getByText('Game Configuration')); - - // Simula la configuración del juego - const increaseButtons = screen.getAllByRole('button', { name: '+' }); - fireEvent.click(increaseButtons[0]); // Aumenta el número de rondas - - // Inicia el juego - fireEvent.click(screen.getByRole('button', { name: 'Start game' })); - - // waits for the question to appear - await waitFor(() => screen.getByText('Which is the capital of Spain?')); - const correctAnswer = screen.getByRole('button', { name: 'Madrid' }); - - expect(correctAnswer).not.toHaveStyle({ backgroundColor: 'green' }); - - //selects correct answer - fireEvent.click(correctAnswer); - - //expect(screen.findByText('1')).toHaveStyle({ backgroundColor: 'lightgreen' }); - - expect(correctAnswer).toHaveStyle({ backgroundColor: 'green' }); - - }); - - - it('should choose incorrect answer', async () => { - render( - - - - - - ); - - // Espera a que aparezca la ventana de configuración - await waitFor(() => screen.getByText('Game Configuration')); - - // Simula la configuración del juego - const increaseButtons = screen.getAllByRole('button', { name: '+' }); - fireEvent.click(increaseButtons[0]); // Aumenta el número de rondas - - // Inicia el juego - fireEvent.click(screen.getByRole('button', { name: 'Start game' })); - - // waits for the question to appear - await waitFor(() => screen.getByText('Which is the capital of Spain?')); - const incorrectAnswer = screen.getByRole('button', { name: 'Barcelona' }); - - expect(incorrectAnswer).not.toHaveStyle({ backgroundColor: 'red' }); - - //selects correct answer - fireEvent.click(incorrectAnswer); - - expect(incorrectAnswer).toHaveStyle({ backgroundColor: 'red' }); - //expect(screen.findByText('1')).toHaveStyle({ backgroundColor: 'salmon' }); - - }); - - it('should not answer the question', async () => { - render( - - - - - - ); - - // Espera a que aparezca la ventana de configuración - await waitFor(() => screen.getByText('Game Configuration')); - - // Simula la configuración del juego - const increaseButtons = screen.getAllByRole('button', { name: '+' }); - fireEvent.click(increaseButtons[0]); // Aumenta el número de rondas - - // Inicia el juego - fireEvent.click(screen.getByRole('button', { name: 'Start game' })); - - // waits for the question to appear - await waitFor(() => screen.getByText('Which is the capital of Spain?')); - - setTimeout(() => { - // Comprobamos que el callback ha sido llamado después del tiempo especificado - done(); // Llamamos a done para indicar que la prueba ha terminado - }, 4000); - - }, 4500); - -}); \ No newline at end of file diff --git a/webapp/src/__tests__/pages/WiseMenStackGame.test.js b/webapp/src/__tests__/pages/WiseMenStackGame.test.js deleted file mode 100644 index ff4961cf..00000000 --- a/webapp/src/__tests__/pages/WiseMenStackGame.test.js +++ /dev/null @@ -1,131 +0,0 @@ -import React from 'react'; -import { render, fireEvent, screen, waitFor } from '@testing-library/react'; -import { SessionContext } from '../../SessionContext'; // Importa el contexto necesario -import { BrowserRouter as Router } from 'react-router-dom'; -import axios from 'axios'; -import MockAdapter from 'axios-mock-adapter'; -import Game from '../../pages/WiseMenStackGame'; -import '../../localize/i18n'; -import { expect } from 'expect-puppeteer'; - -const mockAxios = new MockAdapter(axios); - -describe('Wise Men Stack Game component', () => { - beforeEach(() => { - mockAxios.reset(); - // Mock the axios.post request to simulate a successful response - mockAxios.onGet('http://localhost:8000/questions/en/Geography').reply(200, - [{ - question: 'Which is the capital of Spain?', - options: ['Madrid', 'Barcelona', 'Paris', 'London'], - correctAnswer: 'Madrid', - categories: ['Geography'], - language: 'en' - }] - ); - - mockAxios.onPut('http://localhost:8000/statistics').reply(200, { success: true }); - mockAxios.onPut('http://localhost:8000/user/questionsRecord').reply(200, { success: true }); - - }); - - it('should render configuration, question, answers and other ', async () => { - render( - - - - - - ); - - await waitFor(() => screen.getByText('Wise Men Stack')); - - const button = screen.getByText('Start game'); - fireEvent.click(button); - - //expect(screen.getByRole('progressbar')); - expect(screen.findByText('1')); - expect(screen.findByText('Question 1')); - //expect(screen.findByText('1/3')); - - // waits for the question to appear - await waitFor(() => screen.getByText('Which is the capital of Spain?')); - - expect(screen.findByText('Which is the capital of Spain?')); - expect(screen.findByText('Madrid')); - - }); - - it('should guess correct answer', async () => { - render( - - - - - - ); - await waitFor(() => screen.getByText('Wise Men Stack')); - - const button = screen.getByText('Start game'); - fireEvent.click(button); - - // waits for the question to appear - await waitFor(() => screen.getByText('Which is the capital of Spain?')); - const correctAnswer = screen.getByRole('button', { name: 'Madrid' }); - - expect(correctAnswer).not.toHaveStyle({ backgroundColor: 'green' }); - - //selects correct answer - fireEvent.click(correctAnswer); - - //expect(screen.findByText('1')).toHaveStyle({ backgroundColor: 'lightgreen' }); - - expect(correctAnswer).toHaveStyle({ backgroundColor: 'green' }); - - }); - - - it('should choose incorrect answer', async () => { - render( - - - - - - ); - await waitFor(() => screen.getByText('Wise Men Stack')); - - const button = screen.getByText('Start game'); - fireEvent.click(button); - - // waits for the question to appear - await waitFor(() => screen.getByText('Which is the capital of Spain?')); - const buttons = screen.getAllByRole('button'); - expect(buttons.length).toBe(2); - }); - - it('should not answer the question', async () => { - render( - - - - - - ); - - await waitFor(() => screen.getByText('Wise Men Stack')); - - const button = screen.getByText('Start game'); - fireEvent.click(button); - - // waits for the question to appear - await waitFor(() => screen.getByText('Which is the capital of Spain?')); - - setTimeout(() => { - // Comprobamos que el callback ha sido llamado después del tiempo especificado - done(); // Llamamos a done para indicar que la prueba ha terminado - }, 4000); - - }, 4500); - -}); diff --git a/webapp/src/__tests__/pages/DiscoveringCitiesGame.test.js b/webapp/src/__tests__/pages/games/DiscoveringCitiesGame.test.js similarity index 62% rename from webapp/src/__tests__/pages/DiscoveringCitiesGame.test.js rename to webapp/src/__tests__/pages/games/DiscoveringCitiesGame.test.js index ff54afdc..fd0336f2 100644 --- a/webapp/src/__tests__/pages/DiscoveringCitiesGame.test.js +++ b/webapp/src/__tests__/pages/games/DiscoveringCitiesGame.test.js @@ -1,15 +1,15 @@ import React from 'react'; import { render, fireEvent, screen, waitFor } from '@testing-library/react'; -import { SessionContext } from '../../SessionContext'; // Importa el contexto necesario +import { SessionContext } from '../../../SessionContext'; import { BrowserRouter as Router } from 'react-router-dom'; import axios from 'axios'; import MockAdapter from 'axios-mock-adapter'; -import Game from '../../pages/DiscoveringCitiesGame'; -import '../../localize/i18n'; +import DiscoveringCitiesGame from '../../../pages/games/DiscoveringCitiesGame'; +import '../../../localize/i18n'; const mockAxios = new MockAdapter(axios); - -describe('Game component', () => { + +describe('Discovering Cities component', () => { beforeEach(() => { mockAxios.reset(); // Mock the axios.post request to simulate a successful response @@ -32,25 +32,21 @@ describe('Game component', () => { render( - + ); expect(screen.getByRole('progressbar')); - //expect(screen.findByText('1')); - //expect(screen.findByText('1/5')); // waits for the question to appear - await waitFor(() => screen.getByText('Which is the capital of Spain?')); + await waitFor(() => screen.getByTestId('question')); - expect(screen.findByText('Which is the capital of Spain?')); + expect(screen.getByTestId('question')); expect(screen.findByText('Madrid')); expect(screen.findByText('Barcelona')); expect(screen.findByText('Paris')); expect(screen.findByText('London')); - - expect(screen.getByRole('button', { name: /Pause/i })); }); @@ -58,23 +54,21 @@ describe('Game component', () => { render( - + ); // waits for the question to appear - await waitFor(() => screen.getByText('Which is the capital of Spain?')); + await waitFor(() => screen.getByTestId('question')); const correctAnswer = screen.getByRole('button', { name: 'Madrid' }); - expect(correctAnswer).not.toHaveStyle({ backgroundColor: 'green' }); + expect(correctAnswer).toHaveStyle({ backgroundColor: 'rgb(0, 102, 153);' }); //selects correct answer fireEvent.click(correctAnswer); - //expect(screen.findByText('1')).toHaveStyle({ backgroundColor: 'lightgreen' }); - - expect(correctAnswer).toHaveStyle({ backgroundColor: 'green' }); + expect(correctAnswer).toHaveStyle({ backgroundColor: 'rgb(51, 153, 102);' }); }); @@ -82,21 +76,20 @@ describe('Game component', () => { render( - + ); // waits for the question to appear - await waitFor(() => screen.getByText('Which is the capital of Spain?')); + await waitFor(() => screen.getByTestId('question')); const incorrectAnswer = screen.getByRole('button', { name: 'Barcelona' }); - expect(incorrectAnswer).not.toHaveStyle({ backgroundColor: 'red' }); + expect(incorrectAnswer).toHaveStyle({ backgroundColor: 'rgb(0, 102, 153);' }); //selects correct answer fireEvent.click(incorrectAnswer); - expect(incorrectAnswer).toHaveStyle({ backgroundColor: 'red' }); - //expect(screen.findByText('1')).toHaveStyle({ backgroundColor: 'salmon' }); + expect(incorrectAnswer).toHaveStyle({ backgroundColor: 'rgb(153, 0, 0);' }); }); @@ -104,13 +97,13 @@ describe('Game component', () => { render( - + ); // waits for the question to appear - await waitFor(() => screen.getByText('Which is the capital of Spain?')); + await waitFor(() => screen.getByTestId('question')); setTimeout(() => { // Comprobamos que el callback ha sido llamado después del tiempo especificado @@ -119,4 +112,24 @@ describe('Game component', () => { }, 4500); + it('should pause and resume the game after answering a question', async () => { + render( + + + + + + ); + + await waitFor(() => screen.getByTestId('question')); + + fireEvent.click(screen.getByRole('button', { name: 'Madrid' })); + + await waitFor(() => screen.getByTestId('pause')); + + fireEvent.click(screen.getByTestId('pause')); + + expect(screen.getByTestId('play')).toBeInTheDocument(); + }); + }); diff --git a/webapp/src/__tests__/pages/Game.test.js b/webapp/src/__tests__/pages/games/Game.test.js similarity index 86% rename from webapp/src/__tests__/pages/Game.test.js rename to webapp/src/__tests__/pages/games/Game.test.js index c339aa55..2aa4e4d2 100644 --- a/webapp/src/__tests__/pages/Game.test.js +++ b/webapp/src/__tests__/pages/games/Game.test.js @@ -1,11 +1,11 @@ import React from 'react'; import { render, fireEvent, screen, waitFor } from '@testing-library/react'; -import { SessionContext } from '../../SessionContext'; // Importa el contexto necesario +import { SessionContext } from '../../../SessionContext'; // Importa el contexto necesario import { BrowserRouter as Router } from 'react-router-dom'; import axios from 'axios'; import MockAdapter from 'axios-mock-adapter'; -import Game from '../../pages/Game'; -import '../../localize/i18n'; +import Game from '../../../pages/games/Game'; +import '../../../localize/i18n'; const mockAxios = new MockAdapter(axios); @@ -35,11 +35,7 @@ describe('Game component', () => { ); }); - it('should render question, answers and other ', async () => { - expect(screen.getByRole('progressbar')); - expect(screen.findByText('1')); - //expect(screen.findByText('1/3')); - + it('should render question', async () => { // waits for the question to appear await waitFor(() => screen.getByText('Which is the capital of Spain?'.toUpperCase())); @@ -94,6 +90,12 @@ describe('Game component', () => { expect(pauseButton); fireEvent.click(pauseButton); expect(screen.getByTestId("play")); - }) + }); + + it('should render progress bar', async () => { + await waitFor(() => screen.getByText('Which is the capital of Spain?'.toUpperCase())); + const progressBar = screen.getByTestId('prog_bar0'); + await expect(progressBar).toBeInTheDocument(); + }); }); diff --git a/webapp/src/__tests__/pages/MultiplayerGame.test.js b/webapp/src/__tests__/pages/games/MultiplayerGame.test.js similarity index 59% rename from webapp/src/__tests__/pages/MultiplayerGame.test.js rename to webapp/src/__tests__/pages/games/MultiplayerGame.test.js index eb828baf..9dc95380 100644 --- a/webapp/src/__tests__/pages/MultiplayerGame.test.js +++ b/webapp/src/__tests__/pages/games/MultiplayerGame.test.js @@ -1,10 +1,10 @@ import React from 'react'; import { render, fireEvent, screen, waitFor } from '@testing-library/react'; -import { SessionContext } from '../../SessionContext'; +import { SessionContext } from '../../../SessionContext'; import { BrowserRouter, useLocation } from 'react-router-dom'; -import MultiplayerGame from '../../pages/MultiplayerGame'; +import MultiplayerGame from '../../../pages/games/MultiplayerGame'; import io from 'socket.io-client'; -import '../../localize/i18n'; +import '../../../localize/i18n'; jest.mock('react-router-dom', () => ({ ...jest.requireActual('react-router-dom'), @@ -24,7 +24,21 @@ describe('Game component', () => { let socket; beforeEach(() => { - socket = io(); + socket = io(); + useLocation.mockReturnValue({ + state: { + gameQuestions: mockgameQuestions, + roomCode: "AAAAA", + }, + }) + + render( + + + + + + ); }); const questionObject = { @@ -46,95 +60,47 @@ describe('Game component', () => { const mockgameQuestions = generateQuestionArray(questionObject, 3); it('should render with recieved room code and questions ', async () => { - - //mock info that room sent to the game - useLocation.mockReturnValue({ - state: { - gameQuestions: mockgameQuestions, - roomCode: "AAAAA", - }, - }) - - render( - - - - - - ); - - expect(screen.findByText('1')); - expect(screen.findByText('1/3')); - // waits for the question to appear - await waitFor(() => screen.getByText('Which is the capital of Spain?')); + await waitFor(() => screen.getByTestId('question')); - expect(screen.findByText('Which is the capital of Spain?')); + expect(screen.getByTestId('question')); expect(screen.findByText('Madrid')); expect(screen.findByText('Barcelona')); expect(screen.findByText('Paris')); expect(screen.findByText('London')); }); - it('should guess correct answer', async () => { - - useLocation.mockReturnValue({ - state: { - gameQuestions: mockgameQuestions, - roomCode: "AAAAA", - }, - }) - - render( - - - - - - ); - + it('should guess correct answer', async () => { // waits for the question to appear - await waitFor(() => screen.getByText('Which is the capital of Spain?')); + await waitFor(() => screen.getByTestId('question')); const correctAnswer = screen.getByRole('button', { name: 'Madrid' }); - expect(correctAnswer).not.toHaveStyle({ backgroundColor: 'green' }); + expect(correctAnswer).toHaveStyle({ backgroundColor: 'rgb(0, 102, 153);' }); //selects correct answer fireEvent.click(correctAnswer); - expect(correctAnswer).toHaveStyle({ backgroundColor: 'green' }); - + expect(correctAnswer).toHaveStyle({ backgroundColor: 'rgb(51, 153, 102);' }); }); it('should choose incorrect answer', async () => { - - useLocation.mockReturnValue({ - state: { - gameQuestions: mockgameQuestions, - roomCode: "AAAAA", - }, - }) - - render( - - - - - - ); - // waits for the question to appear - await waitFor(() => screen.getByText('Which is the capital of Spain?')); + await waitFor(() => screen.getByTestId('question')); const incorrectAnswer = screen.getByRole('button', { name: 'Barcelona' }); - expect(incorrectAnswer).not.toHaveStyle({ backgroundColor: 'red' }); + expect(incorrectAnswer).toHaveStyle({ backgroundColor: 'rgb(0, 102, 153);' }); //selects correct answer fireEvent.click(incorrectAnswer); - expect(incorrectAnswer).toHaveStyle({ backgroundColor: 'red' }); - + expect(incorrectAnswer).toHaveStyle({ backgroundColor: 'rgb(153, 0, 0);' }); }); + it('should render progress bar', async () => { + await waitFor(() => screen.getByText('Which is the capital of Spain?'.toUpperCase())); + const progressBar = screen.getByTestId('prog_bar0'); + await expect(progressBar).toBeInTheDocument(); + }) + }); diff --git a/webapp/src/__tests__/pages/MultiplayerRoom.test.js b/webapp/src/__tests__/pages/games/MultiplayerRoom.test.js similarity index 88% rename from webapp/src/__tests__/pages/MultiplayerRoom.test.js rename to webapp/src/__tests__/pages/games/MultiplayerRoom.test.js index 29a10208..50508669 100644 --- a/webapp/src/__tests__/pages/MultiplayerRoom.test.js +++ b/webapp/src/__tests__/pages/games/MultiplayerRoom.test.js @@ -1,10 +1,10 @@ import React from 'react'; import { render, fireEvent, waitFor, screen } from '@testing-library/react'; -import MultiplayerRoom from '../../pages/MultiplayerRoom'; +import MultiplayerRoom from '../../../pages/games/MultiplayerRoom'; import io from 'socket.io-client'; -import { SessionContext } from '../../SessionContext'; +import { SessionContext } from '../../../SessionContext'; import { BrowserRouter as Router } from 'react-router-dom'; -import '../../localize/i18n'; +import '../../../localize/i18n'; //mock some socket behaviour jest.mock('socket.io-client', () => { @@ -34,8 +34,8 @@ describe('MultiplayerRoom component', () => { test('creates a room', async () => { - jest.mock('../../pages/MultiplayerRoom', () => ({ - ...jest.requireActual('../../pages/MultiplayerRoom'), + jest.mock('../../../pages/games/MultiplayerRoom', () => ({ + ...jest.requireActual('../../../pages/games/MultiplayerRoom'), generateRoomCode: jest.fn().mockReturnValue('AAAAA'), })); diff --git a/webapp/src/__tests__/pages/games/TheChallengeGame.test.js b/webapp/src/__tests__/pages/games/TheChallengeGame.test.js new file mode 100644 index 00000000..039dd58f --- /dev/null +++ b/webapp/src/__tests__/pages/games/TheChallengeGame.test.js @@ -0,0 +1,143 @@ +import React from 'react'; +import { render, fireEvent, screen, waitFor } from '@testing-library/react'; +import { SessionContext } from '../../../SessionContext'; +import { BrowserRouter as Router } from 'react-router-dom'; +import axios from 'axios'; +import MockAdapter from 'axios-mock-adapter'; +import '../../../localize/i18n'; +import TheChallengeGame from '../../../pages/games/TheChallengeGame'; + +const mockAxios = new MockAdapter(axios); + +describe('The Challenge component', () => { + beforeEach(() => { + mockAxios.reset(); + // Mockear respuestas de la API + mockAxios.onGet(`http://localhost:8000/questions/en/Geography`).reply(200, + [ + { + question: 'Which is the capital of Spain?', + options: ['Madrid', 'Barcelona', 'Paris', 'London'], + correctAnswer: 'Madrid' + } + ] + ); + + mockAxios.onPut(`http://localhost:8000/statistics`).reply(200, { success: true }); + mockAxios.onPut(`http://localhost:8000/questionsRecord`).reply(200, { success: true }); + + render( + + + + + + ); + }); + + it('should render configuration window and start game', async () => { + // Espera a que aparezca la ventana de configuración + await waitFor(() => screen.getByText('GAME CONFIGURATION')); + + // Simula la configuración del juego + const increaseButton = screen.getByTestId('addRound'); + fireEvent.click(increaseButton); // Aumenta el número de rondas + + // Inicia el juego + fireEvent.click(screen.getByRole('button', { name: 'Start Game' })); + + // Espera a que aparezca la pregunta + await waitFor(() => screen.getByTestId('question')); + + // Verifica que el juego haya comenzado correctamente mostrando la pregunta y las opciones + expect(screen.getByTestId('question')).toBeInTheDocument(); + expect(screen.getByText('Madrid')).toBeInTheDocument(); + expect(screen.getByText('Barcelona')).toBeInTheDocument(); + expect(screen.getByText('Paris')).toBeInTheDocument(); + expect(screen.getByText('London')).toBeInTheDocument(); + }); + + it('should guess correct answer', async () => { + // Espera a que aparezca la ventana de configuración + await waitFor(() => screen.getByText('GAME CONFIGURATION')); + + // Simula la configuración del juego + const increaseRoundButton = screen.getByTestId('addRound'); + fireEvent.click(increaseRoundButton); // Aumenta el número de rondas + const increaseSecondsButton = screen.getByTestId('addSecond'); + fireEvent.click(increaseSecondsButton); // Aumenta los segundos por ronda + + // Inicia el juego + fireEvent.click(screen.getByRole('button', { name: 'Start Game' })); + + // waits for the question to appear + await waitFor(() => screen.getByTestId('question')); + const correctAnswer = screen.getByRole('button', { name: 'Madrid' }); + + //selects correct answer + fireEvent.click(correctAnswer); + + expect(screen.findByTestId("success0")); + + }); + + + it('should choose incorrect answer', async () => { + // Espera a que aparezca la ventana de configuración + await waitFor(() => screen.getByText('GAME CONFIGURATION')); + + // Simula la configuración del juego + const increaseButton = screen.getByTestId('addRound'); + fireEvent.click(increaseButton); // Aumenta el número de rondas + + // Inicia el juego + fireEvent.click(screen.getByRole('button', { name: 'Start Game' })); + + // waits for the question to appear + await waitFor(() => screen.getByTestId('question')); + const incorrectAnswer = screen.getByRole('button', { name: 'Barcelona' }); + + //selects correct answer + fireEvent.click(incorrectAnswer); + expect(screen.findByTestId("fail1")); + + }); + + it('should not answer the question', async () => { + // Espera a que aparezca la ventana de configuración + await waitFor(() => screen.getByText('GAME CONFIGURATION')); + + // Simula la configuración del juego + const increaseButton = screen.getByTestId('addRound'); + fireEvent.click(increaseButton); // Aumenta el número de rondas + + // Inicia el juego + fireEvent.click(screen.getByRole('button', { name: 'Start Game' })); + + // waits for the question to appear + await waitFor(() => screen.getByTestId('question')); + + setTimeout(() => { + // Comprobamos que el callback ha sido llamado después del tiempo especificado + done(); // Llamamos a done para indicar que la prueba ha terminado + }, 4000); + + }, 4500); + + it('should pause and resume the game after answering a question', async () => { + await waitFor(() => screen.getByText('GAME CONFIGURATION')); + fireEvent.click(screen.getByTestId('addRound')); + fireEvent.click(screen.getByRole('button', { name: 'Start Game' })); + + await waitFor(() => screen.getByTestId('question')); + + fireEvent.click(screen.getByRole('button', { name: 'Madrid' })); + + await waitFor(() => screen.getByTestId('pause')); + + fireEvent.click(screen.getByTestId('pause')); + + expect(screen.getByTestId('play')).toBeInTheDocument(); + }); + +}); \ No newline at end of file diff --git a/webapp/src/__tests__/pages/WarmQuestionGame.test.js b/webapp/src/__tests__/pages/games/WarmQuestionGame.test.js similarity index 55% rename from webapp/src/__tests__/pages/WarmQuestionGame.test.js rename to webapp/src/__tests__/pages/games/WarmQuestionGame.test.js index 6297a0d5..0711d1e4 100644 --- a/webapp/src/__tests__/pages/WarmQuestionGame.test.js +++ b/webapp/src/__tests__/pages/games/WarmQuestionGame.test.js @@ -1,14 +1,14 @@ import React from 'react'; import { render, fireEvent, screen, waitFor } from '@testing-library/react'; -import { SessionContext } from '../../SessionContext'; +import { SessionContext } from '../../../SessionContext'; import { BrowserRouter as Router } from 'react-router-dom'; import axios from 'axios'; import MockAdapter from 'axios-mock-adapter'; -import Game from '../../pages/WarmQuestionGame'; +import WarmQuestionGame from '../../../pages/games/WarmQuestionGame'; const mockAxios = new MockAdapter(axios); -describe('Game component', () => { +describe('WarmQuestionGame component', () => { beforeEach(() => { mockAxios.reset(); // Mock the axios.post request to simulate a successful response @@ -24,24 +24,22 @@ describe('Game component', () => { mockAxios.onPut('http://localhost:8000/statistics').reply(200, { success: true }); mockAxios.onPut('http://localhost:8000/questionsRecord').reply(200, { success: true }); - }); - it('should render question, answers and other', async () => { render( - - - - - + + + + + ); + }); + it('should render question, answers and other', async () => { // Espera a que aparezca la pregunta - await waitFor(() => screen.getByText('Which is the capital of Spain?')); - - expect(screen.findByText('1')); + await waitFor(() => screen.getByTestId('question')); // Verifica que el juego haya comenzado correctamente mostrando la pregunta y las opciones - expect(screen.getByText('Which is the capital of Spain?')).toBeInTheDocument(); + expect(screen.getByTestId('question')).toBeInTheDocument(); expect(screen.getByText('Madrid')).toBeInTheDocument(); expect(screen.getByText('Barcelona')).toBeInTheDocument(); expect(screen.getByText('Paris')).toBeInTheDocument(); @@ -49,64 +47,37 @@ describe('Game component', () => { }); it('should guess correct answer', async () => { - render( - - - - - - ); - // waits for the question to appear - await waitFor(() => screen.getByText('Which is the capital of Spain?')); + await waitFor(() => screen.getByTestId('question')); const correctAnswer = screen.getByRole('button', { name: 'Madrid' }); - expect(correctAnswer).not.toHaveStyle({ backgroundColor: 'green' }); + expect(correctAnswer).toHaveStyle({ backgroundColor: 'rgb(0, 102, 153);' }); //selects correct answer fireEvent.click(correctAnswer); - //expect(screen.findByText('1')).toHaveStyle({ backgroundColor: 'lightgreen' }); - - expect(correctAnswer).toHaveStyle({ backgroundColor: 'green' }); + expect(correctAnswer).toHaveStyle({ backgroundColor: 'rgb(51, 153, 102);' }); }); it('should choose incorrect answer', async () => { - render( - - - - - - ); - // waits for the question to appear - await waitFor(() => screen.getByText('Which is the capital of Spain?')); + await waitFor(() => screen.getByTestId('question')); const incorrectAnswer = screen.getByRole('button', { name: 'Barcelona' }); - expect(incorrectAnswer).not.toHaveStyle({ backgroundColor: 'red' }); + expect(incorrectAnswer).toHaveStyle({ backgroundColor: 'rgb(0, 102, 153);' }); //selects correct answer fireEvent.click(incorrectAnswer); - expect(incorrectAnswer).toHaveStyle({ backgroundColor: 'red' }); - //expect(screen.findByText('1')).toHaveStyle({ backgroundColor: 'salmon' }); + expect(incorrectAnswer).toHaveStyle({ backgroundColor: 'rgb(153, 0, 0);' }); }); it('should not answer the question', async () => { - render( - - - - - - ); - // waits for the question to appear - await waitFor(() => screen.getByText('Which is the capital of Spain?')); + await waitFor(() => screen.getByTestId('question')); setTimeout(() => { // Comprobamos que el callback ha sido llamado después del tiempo especificado @@ -116,29 +87,35 @@ describe('Game component', () => { }, 4500); it('should pass the question', async () => { - render( - - - - - - ); - // waits for the question to appear - await waitFor(() => screen.getByText('Which is the capital of Spain?')); + await waitFor(() => screen.getByTestId('question')); const correctAnswer = screen.getByRole('button', { name: 'Madrid' }); const skip = screen.getByRole('button', { name: 'Skip' }); - expect(correctAnswer).not.toHaveStyle({ backgroundColor: 'green' }); + expect(correctAnswer).toHaveStyle({ backgroundColor: 'rgb(0, 102, 153);' }); //selects correct answer fireEvent.click(skip); - //expect(screen.findByText('1')).toHaveStyle({ backgroundColor: 'lightgreen' }); - - expect(correctAnswer).toHaveStyle({ backgroundColor: 'green' }); + expect(correctAnswer).toHaveStyle({ backgroundColor: 'rgb(51, 153, 102);' }); }); + it('should render pause & play buttons when answered', async () => { + await waitFor(() => screen.getByText('Which is the capital of Spain?'.toUpperCase())); + const correctAnswer = screen.getByRole('button', { name: 'Madrid' }); + fireEvent.click(correctAnswer); + + const pauseButton = screen.getByTestId("pause"); + expect(pauseButton); + fireEvent.click(pauseButton); + expect(screen.getByTestId("play")); + }) + + it('should render progress bar', async () => { + await waitFor(() => screen.getByText('Which is the capital of Spain?'.toUpperCase())); + const progressBar = screen.getByTestId('prog_bar0'); + await expect(progressBar).toBeInTheDocument(); + }) }); \ No newline at end of file diff --git a/webapp/src/__tests__/pages/games/WiseMenStackGame.test.js b/webapp/src/__tests__/pages/games/WiseMenStackGame.test.js new file mode 100644 index 00000000..55588acb --- /dev/null +++ b/webapp/src/__tests__/pages/games/WiseMenStackGame.test.js @@ -0,0 +1,144 @@ +import React from 'react'; +import { render, fireEvent, screen, waitFor } from '@testing-library/react'; +import { SessionContext } from '../../../SessionContext'; // Importa el contexto necesario +import { BrowserRouter as Router } from 'react-router-dom'; +import axios from 'axios'; +import MockAdapter from 'axios-mock-adapter'; +import WiseMenStack from '../../../pages/games/WiseMenStackGame'; +import '../../../localize/i18n'; +import { expect } from 'expect-puppeteer'; + +const mockAxios = new MockAdapter(axios); + +describe('Wise Men Stack Game component', () => { + beforeEach(() => { + mockAxios.reset(); + // Mock the axios.post request to simulate a successful response + mockAxios.onGet('http://localhost:8000/questions/en/Geography').reply(200, + [{ + question: 'Which is the capital of Spain?', + options: ['Madrid', 'Barcelona', 'Paris', 'London'], + correctAnswer: 'Madrid', + categories: ['Geography'], + language: 'en' + }] + ); + + mockAxios.onPut('http://localhost:8000/statistics').reply(200, { success: true }); + mockAxios.onPut('http://localhost:8000/user/questionsRecord').reply(200, { success: true }); + + render( + + + + + + ); + }); + + it('should render configuration, question, answers and other ', async () => { + await waitFor(() => screen.getByText('GAME CONFIGURATION')); + + const button = screen.getByTestId('start-button'); + // clicks the start button to show the first question + fireEvent.click(button); + + // waits for the question to appear + await waitFor(() => screen.getByText('Which is the capital of Spain?'.toUpperCase())); + + expect(screen.findByText('Which is the capital of Spain?'.toUpperCase())); + expect(screen.findByText('Madrid')); + + }); + + it('should mark as correct right answer', async () => { + await waitFor(() => screen.getByText('GAME CONFIGURATION')); + + const button = screen.getByTestId('start-button'); + fireEvent.click(button); + + // waits for the question to appear + await waitFor(() => screen.getByText('Which is the capital of Spain?'.toUpperCase())); + + const correctAnswer = screen.getByRole('button', { name: 'Madrid' }); + + // after clicking it has changed to succeeded: + fireEvent.click(correctAnswer); + expect(screen.findByTestId("success0")); + + }); + + it('should mark as incorrect another answer', async () => { + await waitFor(() => screen.getByText('GAME CONFIGURATION')); + + const button = screen.getByTestId('start-button'); + fireEvent.click(button); + + // waits for the question to appear + await waitFor(() => screen.getByText('Which is the capital of Spain?'.toUpperCase())); + + const answers = screen.getAllByRole('button'); + const incorrectAnswer = answers[0].name === 'Madrid' ? answers[1] : answers[0]; + const id = answers[0].name === 'Madrid' ? 1 : 0; + + // now the answer is not selected: + expect(screen.findByTestId(`success${id}`)); + // after clicking it has changed to succeeded: + fireEvent.click(incorrectAnswer); + expect(screen.findByTestId(`failure${id}`)); + + }); + + + it('should only show 2 answers', async () => { await waitFor(() => screen.getByText('GAME CONFIGURATION')); + const button = screen.getByTestId('start-button'); + fireEvent.click(button); + + // waits for the question to appear + await waitFor(() => screen.getByText('Which is the capital of Spain?'.toUpperCase())); + const buttons = screen.getAllByRole('button'); + expect(buttons.length).toBe(2); + }); + + it('should not answer the question', async () => { + await waitFor(() => screen.getByText('GAME CONFIGURATION')); + + const button = screen.getByTestId('start-button'); + fireEvent.click(button); + + // waits for the question to appear + await waitFor(() => screen.getByText('Which is the capital of Spain?'.toUpperCase())); + + setTimeout(() => { + // Comprobamos que el callback ha sido llamado después del tiempo especificado + done(); // Llamamos a done para indicar que la prueba ha terminado + }, 4000); + + }, 4500); + + it('should render pause & play buttons when answered', async () => { + await waitFor(() => screen.getByText('GAME CONFIGURATION')); + const button = screen.getByTestId('start-button'); + fireEvent.click(button); + + await waitFor(() => screen.getByText('Which is the capital of Spain?'.toUpperCase())); + const correctAnswer = screen.getByRole('button', { name: 'Madrid' }); + fireEvent.click(correctAnswer); + + const pauseButton = screen.getByTestId("pause"); + expect(pauseButton); + fireEvent.click(pauseButton); + expect(screen.getByTestId("play")); + }) + + it('should render progress bar', async () => { + await waitFor(() => screen.getByText('GAME CONFIGURATION')); + const button = screen.getByTestId('start-button'); + fireEvent.click(button); + + await waitFor(() => screen.getByText('Which is the capital of Spain?'.toUpperCase())); + const progressBar = screen.getByTestId('prog_bar0'); + await expect(progressBar).toBeInTheDocument(); + }) + +}); diff --git a/webapp/src/components/Footer.js b/webapp/src/components/Footer.js index db375b97..68ce0462 100644 --- a/webapp/src/components/Footer.js +++ b/webapp/src/components/Footer.js @@ -2,6 +2,7 @@ import * as React from 'react'; import { AppBar, Toolbar, Typography, Link } from '@mui/material'; import { useTranslation } from 'react-i18next'; + const Footer = () => { const { t } = useTranslation(); diff --git a/webapp/src/components/NavBar.js b/webapp/src/components/NavBar.js index 79f4dc2d..1de1d56d 100644 --- a/webapp/src/components/NavBar.js +++ b/webapp/src/components/NavBar.js @@ -53,6 +53,12 @@ function NavBar() { // Add an object for each new page ]; + const logo = ( + + ) + return ( // position="static" => Barra se desplaza con scroll down @@ -98,23 +104,22 @@ function NavBar() { ))} + { logo } ):( - + <> + { logo } + )} {/* Pages list in NavBar, only displayed when menu button is not, i.e., in larger devices */} {isLoggedIn ? ( - - - + + { logo } + {pages.map((page) => ( - ))} @@ -124,12 +129,12 @@ function NavBar() { )} - + {/* Internacionalization */} - + setCategory(event.target.value)} - style={{ minWidth: '120px' }} - > - Geography - Political - Sports - - - - - - ); - } - - - // circular loading - if (!questionData) { - return ( - - - - - ); - } - - // redirect to / if game over -if (shouldRedirect) { - // Redirect after 3 seconds - setTimeout(() => { - navigate('/homepage'); - }, 4000); - - - return ( - - - incorrectlyAnsweredQuestions ? 'green' : 'red', - fontSize: '4rem', // Tamaño de fuente - marginTop: '20px', // Espaciado superior - marginBottom: '50px', // Espaciado inferior - }} - > - {correctlyAnsweredQuestions > incorrectlyAnsweredQuestions ? "Great Job!" : "Game Over"} - -
- Correct Answers: {correctlyAnsweredQuestions} - Incorrect Answers: {incorrectlyAnsweredQuestions} - Total money: {totalScore} - Game time: {totalTimePlayed} seconds -
- {showConfetti && } -
- ); -} - return ( - - - - - - {questionHistorialBar()} - - - - - {round} / {numRounds} - - - {questionData.question} - selectResponse(0, "FAILED")} //when time ends always fail question - > - {({ remainingTime }) => { - return ( -
-
{remainingTime}
-
- ); - }} -
-
- - - {questionData.options.map((option, index) => ( - - - - ))} - -
- ); -}; - -export default Game; diff --git a/webapp/src/pages/WiseMenStackGame.js b/webapp/src/pages/WiseMenStackGame.js deleted file mode 100644 index 2e9a78f5..00000000 --- a/webapp/src/pages/WiseMenStackGame.js +++ /dev/null @@ -1,424 +0,0 @@ -import * as React from 'react'; -import axios from 'axios'; -import { Container, Button, CssBaseline, Grid, Typography, CircularProgress, Card, CardContent, Select, MenuItem, useTheme } from '@mui/material'; -import CheckIcon from '@mui/icons-material/Check'; -import ClearIcon from '@mui/icons-material/Clear'; -import { useNavigate } from 'react-router-dom'; -import { SessionContext } from '../SessionContext'; -import { useContext } from 'react'; -import Confetti from 'react-confetti'; -import { CountdownCircleTimer } from "react-countdown-circle-timer"; -import { useTranslation } from 'react-i18next'; -import i18n from '../localize/i18n'; - -const apiEndpoint = process.env.REACT_APP_API_ENDPOINT || 'http://localhost:8000'; - - -const WiseMenStackGame = () => { - const navigate = useNavigate(); - const SUCCESS_SOUND_ROUTE = "/sounds/success_sound.mp3"; - const FAILURE_SOUND_ROUTE = "/sounds/wrong_sound.mp3"; - - //sesion information - const {username} = useContext(SessionContext); - const theme = useTheme(); - - - // Traductions - const { t } = useTranslation(); - - // state initialization - const [round, setRound] = React.useState(1); - const [questionData, setQuestionData] = React.useState(null); - const [buttonStates, setButtonStates] = React.useState([]); - const [answered, setAnswered] = React.useState(false); - const [shouldRedirect, setShouldRedirect] = React.useState(false); - const [totalScore, setTotalScore] = React.useState(0); - const [correctlyAnsweredQuestions, setCorrectlyAnsweredQuestions] = React.useState(0); - const [incorrectlyAnsweredQuestions, setIncorrectlyAnsweredQuestions] = React.useState(0); - const [totalTimePlayed, setTotalTimePlayed] = React.useState(0); - const [timerRunning, setTimerRunning] = React.useState(true); // indicate if the timer is working - const [showConfetti, setShowConfetti] = React.useState(false); //indicates if the confetti must appear - const [questionCountdownKey, ] = React.useState(60); //key to update question timer - const [questionCountdownRunning, setQuestionCountdownRunning] = React.useState(false); //property to start and stop question timer - const [userResponses, setUserResponses] = React.useState([]); - const [language, setCurrentLanguage] = React.useState(i18n.language); - - const [category, setCategory] = React.useState('Geography'); - const [possibleAnswers, setPossibleAnswers] = React.useState([]); - const [isConfigured, setConfiguration] = React.useState(false); - - const [questionHistorial, setQuestionHistorial] = React.useState(Array(round).fill(null)); - - React.useEffect(() => { - let timer; - if (timerRunning) { - timer = setInterval(() => { - setTotalTimePlayed((prevTotalTime) => prevTotalTime + 1); - }, 1000); - } - - return () => clearInterval(timer); - }, [timerRunning]); - - // hook to initiating new rounds if the current number of rounds is less than or equal to 3 - React.useEffect(() => { - if (totalTimePlayed <= questionCountdownKey) { - startNewRound(); - setQuestionCountdownRunning(true); - } else { - setTimerRunning(false); - setShouldRedirect(true); - setQuestionCountdownRunning(false); - updateStatistics(); - updateQuestionsRecord(); - } - // eslint-disable-next-line - }, [round]); - - // stablish if the confetti must show or not - React.useEffect(() => { - if (correctlyAnsweredQuestions > incorrectlyAnsweredQuestions) { - setShowConfetti(true); - } else { - setShowConfetti(false); - } - }, [correctlyAnsweredQuestions, incorrectlyAnsweredQuestions]); - - - // gets a random question from the database and initializes button states to null - const startNewRound = async () => { - setAnswered(false); - // It works deploying using git repo from machine with: axios.get(`http://20.80.235.188:8000/questions`)7 - - // Updates current language - setCurrentLanguage(i18n.language); - axios.get(`${apiEndpoint}/questions/${language}/${category}`) - .then(quest => { - // every new round it gets a new question from db - setQuestionData(quest.data[0]); - setButtonStates(new Array(2).fill(null)); - getPossibleOptions(quest.data[0]); - - }).catch(error => { - console.error("Could not get questions", error); - }); - - }; - - // It puts 2 possible answers into an array making sure that the correct answer is not repeated - const getPossibleOptions = async (question) => { - var options = []; - options.push(question.correctAnswer); - let randomNumber ; - do { - randomNumber = Math.floor(Math.random() * question.options.length); - } while (question.options[randomNumber] === question.correctAnswer); - options.push(question.options[randomNumber]); - options = shuffleArray(options); - setPossibleAnswers(options); - } - - // Shuffles array - function shuffleArray(array) { - const random = Math.random(); - const randomFactor = random < 0.5 ? -1 : 1; - return array.sort(() => randomFactor); - } - - - const updateStatistics = async() => { - try { - await axios.put(`${apiEndpoint}/statistics`, { - username:username, - wise_men_stack_earned_money:totalScore, - wise_men_stack_correctly_answered_questions:correctlyAnsweredQuestions, - wise_men_stack_incorrectly_answered_questions:incorrectlyAnsweredQuestions, - wise_men_stack_games_played:1 - }); - } catch (error) { - console.error("Error:", error); - }; - } - - const updateQuestionsRecord = async() => { - try { - await axios.put(`${apiEndpoint}/questionsRecord`, { - questions: userResponses, - username: username, - gameMode: "WiseMenStack" - }); - } catch (error) { - console.error("Error:", error); - }; - } - - // this function is called when a user selects a response. - const selectResponse = async (index, response) => { - setAnswered(true); - const newButtonStates = [...buttonStates]; - - //setQuestionCountdownRunning(false); - - //check answer - if (response === questionData.correctAnswer) { - const userResponse = { - question: questionData.question, - response: response, - options: questionData.options, - correctAnswer: questionData.correctAnswer - }; - setUserResponses(prevResponses => [...prevResponses, userResponse]); - - newButtonStates[index] = "success" - const sucessSound = new Audio(SUCCESS_SOUND_ROUTE); - sucessSound.volume = 0.40; - sucessSound.play(); - setCorrectlyAnsweredQuestions(correctlyAnsweredQuestions + 1); - setTotalScore(totalScore + 20); - - const newQuestionHistorial = [...questionHistorial]; - newQuestionHistorial[round-1] = true; - setQuestionHistorial(newQuestionHistorial); - } else { - const userResponse = { - question: questionData.question, - response: response, - options: questionData.options, - correctAnswer: questionData.correctAnswer - }; - setUserResponses(prevResponses => [...prevResponses, userResponse]); - newButtonStates[index] = "failure"; - const failureSound = new Audio(FAILURE_SOUND_ROUTE); - failureSound.volume = 0.40; - failureSound.play(); - for (let i = 0; i < questionData.options.length; i++) { - if (questionData.options[i] === questionData.correctAnswer) { - newButtonStates[i] = "success"; - } - } - setIncorrectlyAnsweredQuestions(incorrectlyAnsweredQuestions + 1); - - const newQuestionHistorial = [...questionHistorial]; - newQuestionHistorial[round-1] = false; - setQuestionHistorial(newQuestionHistorial); - } - - setButtonStates(newButtonStates); - - setTimeout(async() => { - setRound(round + 1); - setButtonStates([]); - setCurrentLanguage(i18n.language); - }, 2000); - }; - - const questionHistorialBar = () => { - return questionHistorial.map((isCorrect, index) => ( - - {index + 1} - - )); - }; - - - // circular loading - if (!questionData) { - return ( - - - - - ); - } - -if(!isConfigured) { - return ( - - Wise Men Stack - {t("Wise_Men.instructions")} - - {/* Dropdown for selecting category */} -
- - -
- - -
- ); -} - - // redirect to / if game over -if (shouldRedirect) { - // Redirect after 3 seconds - setTimeout(() => { - navigate('/homepage'); - }, 4000); - - - return ( - - - incorrectlyAnsweredQuestions ? 'green' : 'red', - fontSize: '4rem', // Tamaño de fuente - marginTop: '20px', // Espaciado superior - marginBottom: '50px', // Espaciado inferior - }} - > - {correctlyAnsweredQuestions > incorrectlyAnsweredQuestions ? t("Game.win_msg") : t("Game.lose_msg") } - -
- { t("Game.correct") }: {correctlyAnsweredQuestions} - { t("Game.incorrect") }: {incorrectlyAnsweredQuestions} - { t("Game.money") }: {totalScore} - { t("Game.time") }: {totalTimePlayed} -
- {showConfetti && } -
- ); -} - return ( - - - - - - {questionHistorialBar()} - - - - - {t("Game.round")} {round} - - - {questionData.question} - selectResponse(0, "FAILED")} //when time ends always fail question - > - {({ remainingTime }) => { - return ( -
-
{remainingTime}
-
- ); - }} -
-
- - - {possibleAnswers.map((option, index) => ( - - - - ))} - -
- ); -}; - -export default WiseMenStackGame; diff --git a/webapp/src/pages/DiscoveringCitiesGame.js b/webapp/src/pages/games/DiscoveringCitiesGame.js similarity index 53% rename from webapp/src/pages/DiscoveringCitiesGame.js rename to webapp/src/pages/games/DiscoveringCitiesGame.js index 7575ff67..303912b0 100644 --- a/webapp/src/pages/DiscoveringCitiesGame.js +++ b/webapp/src/pages/games/DiscoveringCitiesGame.js @@ -1,16 +1,16 @@ import * as React from 'react'; import axios from 'axios'; -import { Container, Button, CssBaseline, Grid, Typography, CircularProgress, Card, CardContent } from '@mui/material'; +import { useTheme, Container, Button, CssBaseline, Grid, Typography, CircularProgress, Card, Box, IconButton } from '@mui/material'; import { PlayArrow, Pause } from '@mui/icons-material'; import CheckIcon from '@mui/icons-material/Check'; import ClearIcon from '@mui/icons-material/Clear'; import { useNavigate } from 'react-router-dom'; -import { SessionContext } from '../SessionContext'; +import { SessionContext } from '../../SessionContext'; import { useContext } from 'react'; import Confetti from 'react-confetti'; import { CountdownCircleTimer } from "react-countdown-circle-timer"; import { useTranslation } from 'react-i18next'; -import i18n from '../localize/i18n'; +import i18n from '../../localize/i18n'; const apiEndpoint = process.env.REACT_APP_API_ENDPOINT || 'http://localhost:8000'; @@ -23,9 +23,11 @@ const DiscovertingCitiesGame = () => { //sesion information const {username} = useContext(SessionContext); - // Traductions + // Translations const { t } = useTranslation(); + const theme = useTheme(); + // state initialization const [round, setRound] = React.useState(1); const [questionData, setQuestionData] = React.useState(null); @@ -36,13 +38,16 @@ const DiscovertingCitiesGame = () => { const [correctlyAnsweredQuestions, setCorrectlyAnsweredQuestions] = React.useState(0); const [incorrectlyAnsweredQuestions, setIncorrectlyAnsweredQuestions] = React.useState(0); const [totalTimePlayed, setTotalTimePlayed] = React.useState(0); - const [timerRunning, setTimerRunning] = React.useState(true); // indicate if the timer is working - const [showConfetti, setShowConfetti] = React.useState(false); //indicates if the confetti must appear - const [questionCountdownKey, setQuestionCountdownKey] = React.useState(15); //key to update question timer - const [questionCountdownRunning, setQuestionCountdownRunning] = React.useState(false); //property to start and stop question timer + const [timerRunning, setTimerRunning] = React.useState(true); + const [showConfetti, setShowConfetti] = React.useState(false); + const [questionCountdownKey, setQuestionCountdownKey] = React.useState(15); + const [questionCountdownRunning, setQuestionCountdownRunning] = React.useState(false); const [userResponses, setUserResponses] = React.useState([]); + const [paused, setPaused] = React.useState(false); + const [passNewRound, setPassNewRound] = React.useState(false); const [language, setCurrentLanguage] = React.useState(i18n.language); + const [questionHistorial, setQuestionHistorial] = React.useState(Array(MAX_ROUNDS).fill(null)); React.useEffect(() => { @@ -74,28 +79,33 @@ const DiscovertingCitiesGame = () => { // stablish if the confetti must show or not React.useEffect(() => { - if (correctlyAnsweredQuestions > incorrectlyAnsweredQuestions) { - setShowConfetti(true); - } else { - setShowConfetti(false); + correctlyAnsweredQuestions > incorrectlyAnsweredQuestions ? setShowConfetti(true) : setShowConfetti(false); + }, [correctlyAnsweredQuestions, incorrectlyAnsweredQuestions]); + + React.useEffect(() => { + if (passNewRound && !paused) { + setRound(prevRound => { + return prevRound + 1; + }); + setButtonStates([]); } - }, [correctlyAnsweredQuestions, incorrectlyAnsweredQuestions]); - + }, [paused, passNewRound]); // gets a random question from the database and initializes button states to null const startNewRound = async () => { setAnswered(false); - // It works deploying using git repo from machine with: axios.get(`http://20.80.235.188:8000/questions`) + setPassNewRound(false); + // Updates current language setCurrentLanguage(i18n.language); axios.get(`${apiEndpoint}/questions/${language}/Cities`) .then(quest => { // every new round it gets a new question from db - setQuestionData(quest.data[0]); + setQuestionData(quest.data[0]); setButtonStates(new Array(quest.data[0].options.length).fill(null)); }).catch(error => { console.error(error); - }); + }); }; const updateStatistics = async() => { @@ -125,6 +135,7 @@ const DiscovertingCitiesGame = () => { online_incorrectly_answered_questions: 0, online_total_time_played: 0, online_games_played: 0, + online_games_won: 0 }); } catch (error) { console.error("Error:", error); @@ -197,55 +208,26 @@ const DiscovertingCitiesGame = () => { setButtonStates(newButtonStates); setTimeout(async() => { - setRound(round + 1); - setButtonStates([]); + setPassNewRound(true); setCurrentLanguage(i18n.language); - }, 4000); }; const questionHistorialBar = () => { return questionHistorial.map((isCorrect, index) => ( - - {index + 1} - + )); - }; + }; const togglePause = () => { setTimerRunning(!timerRunning); - setQuestionCountdownRunning(!timerRunning); - if (timerRunning) { - // Si el juego estaba en marcha y se pausa, deshabilitar los botones - setButtonStates(new Array(questionData.options.length).fill(true)); - } else { - // Si el juego estaba pausado y se reanuda, habilitar los botones - setButtonStates(new Array(questionData.options.length).fill(null)); - } + setPaused(!paused); } - // circular loading if (!questionData) { return ( - + @@ -253,135 +235,79 @@ const DiscovertingCitiesGame = () => { } // redirect to / if game over -if (shouldRedirect) { - // Redirect after 3 seconds - setTimeout(() => { - navigate('/homepage'); - }, 4000); + if (shouldRedirect) { + // Redirect after 4 seconds + setTimeout(() => { + navigate('/homepage'); + }, 4000); + return ( + + + incorrectlyAnsweredQuestions ? theme.palette.success.main : theme.palette.error.main }}> + {correctlyAnsweredQuestions > incorrectlyAnsweredQuestions ? t("Game.win_msg") : t("Game.lose_msg") } + + + { t("Game.correct") }: {correctlyAnsweredQuestions} + { t("Game.incorrect") }: {incorrectlyAnsweredQuestions} + { t("Game.money") }: {totalScore} + { t("Game.time") }: {totalTimePlayed} + + {showConfetti && } + + ); + } return ( - - - incorrectlyAnsweredQuestions ? 'green' : 'red', - fontSize: '4rem', // Tamaño de fuente - marginTop: '20px', // Espaciado superior - marginBottom: '50px', // Espaciado inferior - }} - > - {correctlyAnsweredQuestions > incorrectlyAnsweredQuestions ? t("Game.win_msg") : t("Game.lose_msg") } - -
- { t("Game.correct") }: {correctlyAnsweredQuestions} - { t("Game.incorrect") }: {incorrectlyAnsweredQuestions} - { t("Game.money") }: {totalScore} - { t("Game.time") }: {totalTimePlayed} -
- {showConfetti && } -
- ); -} - return ( - + - - - - - {questionHistorialBar()} - + + + { answered ? + // Pausa + togglePause()} sx={{ height: 100, width: 100, border: `2px solid ${theme.palette.primary.main}` }} + data-testid={ paused ? "play" : "pause"} > + { paused ? : } + + : + // Cronómetro + selectResponse(-1, "FAILED")}> + {({ remainingTime }) => { + return ( + + {remainingTime} + + ); + }} + + } + + + + + {questionData.question.toUpperCase()} + + + + {questionData.options.map((option, index) => ( + + + + ))} + - - {round} / {MAX_ROUNDS} - - - {questionData.question} - selectResponse(0, "FAILED")} //when time ends always fail question - > - {({ remainingTime }) => { - return ( -
-
{remainingTime}
-
- ); - }} -
-
- - - {questionData.options.map((option, index) => ( - - - - ))} - + {/* Progress Cards */} + + {questionHistorialBar()} +
); }; diff --git a/webapp/src/pages/Game.js b/webapp/src/pages/games/Game.js similarity index 73% rename from webapp/src/pages/Game.js rename to webapp/src/pages/games/Game.js index 0310eeaf..19e231f4 100644 --- a/webapp/src/pages/Game.js +++ b/webapp/src/pages/games/Game.js @@ -1,16 +1,16 @@ import * as React from 'react'; import axios from 'axios'; -import { useTheme, Container, Button, CssBaseline, Grid, Typography, CircularProgress, Card } from '@mui/material'; +import { useTheme, Container, Button, CssBaseline, Grid, Typography, CircularProgress, Card, Box, IconButton } from '@mui/material'; import { PlayArrow, Pause } from '@mui/icons-material'; import CheckIcon from '@mui/icons-material/Check'; import ClearIcon from '@mui/icons-material/Clear'; import { useNavigate } from 'react-router-dom'; -import { SessionContext } from '../SessionContext'; +import { SessionContext } from '../../SessionContext'; import { useContext } from 'react'; import Confetti from 'react-confetti'; import { CountdownCircleTimer } from "react-countdown-circle-timer"; import { useTranslation } from 'react-i18next'; -import i18n from '../localize/i18n'; +import i18n from '../../localize/i18n'; const apiEndpoint = process.env.REACT_APP_API_ENDPOINT || 'http://localhost:8000'; @@ -38,16 +38,15 @@ const Game = () => { const [correctlyAnsweredQuestions, setCorrectlyAnsweredQuestions] = React.useState(0); const [incorrectlyAnsweredQuestions, setIncorrectlyAnsweredQuestions] = React.useState(0); const [totalTimePlayed, setTotalTimePlayed] = React.useState(0); - const [timerRunning, setTimerRunning] = React.useState(true); // indicate if the timer is working - const [showConfetti, setShowConfetti] = React.useState(false); //indicates if the confetti must appear - const [questionCountdownKey, setQuestionCountdownKey] = React.useState(15); //key to update question timer - const [questionCountdownRunning, setQuestionCountdownRunning] = React.useState(false); //property to start and stop question timer + const [timerRunning, setTimerRunning] = React.useState(true); + const [showConfetti, setShowConfetti] = React.useState(false); + const [questionCountdownKey, setQuestionCountdownKey] = React.useState(15); + const [questionCountdownRunning, setQuestionCountdownRunning] = React.useState(false); const [userResponses, setUserResponses] = React.useState([]); const [paused, setPaused] = React.useState(false); const [passNewRound, setPassNewRound] = React.useState(false); const [language, setCurrentLanguage] = React.useState(i18n.language); - const [questionHistorial, setQuestionHistorial] = React.useState(Array(MAX_ROUNDS).fill(null)); React.useEffect(() => { @@ -70,7 +69,7 @@ const Game = () => { } else { setTimerRunning(false); setShouldRedirect(true); - setQuestionCountdownRunning(false); // Isnt this redundant as it is stablished when answering? + setQuestionCountdownRunning(false); updateStatistics(); updateQuestionsRecord(); } @@ -215,12 +214,7 @@ const Game = () => { const questionHistorialBar = () => { return questionHistorial.map((isCorrect, index) => ( - - + )); }; @@ -232,44 +226,24 @@ const Game = () => { // circular loading if (!questionData) { return ( - + ); } - // redirect to / if game over + // redirect to homepage if game over if (shouldRedirect) { // Redirect after 4 seconds setTimeout(() => { navigate('/homepage'); }, 4000); - return ( - + - incorrectlyAnsweredQuestions ? theme.palette.success.main : theme.palette.error.main }}> + incorrectlyAnsweredQuestions ? theme.palette.success.main : theme.palette.error.main }}> {correctlyAnsweredQuestions > incorrectlyAnsweredQuestions ? t("Game.win_msg") : t("Game.lose_msg") } @@ -284,73 +258,43 @@ const Game = () => { } return ( - + { answered ? // Pausa - + togglePause()} sx={{ height: 100, width: 100, border: `2px solid ${theme.palette.primary.main}` }} + data-testid={ paused ? "play" : "pause"} > + { paused ? : } + : // Cronómetro - selectResponse(-1, "FAILED")} //when time ends always fail question - > + selectResponse(-1, "FAILED")}> {({ remainingTime }) => { return ( -
-
{remainingTime}
-
+ + {remainingTime} + ); }}
}
- - + + {questionData.question.toUpperCase()} - + {questionData.options.map((option, index) => ( - diff --git a/webapp/src/pages/MultiplayerGame.js b/webapp/src/pages/games/MultiplayerGame.js similarity index 56% rename from webapp/src/pages/MultiplayerGame.js rename to webapp/src/pages/games/MultiplayerGame.js index 2c71b34a..2a594768 100644 --- a/webapp/src/pages/MultiplayerGame.js +++ b/webapp/src/pages/games/MultiplayerGame.js @@ -1,24 +1,21 @@ import * as React from 'react'; import axios from 'axios'; -import { Container, Button, CssBaseline, Grid, Typography, CircularProgress, Card, CardContent } from '@mui/material'; +import { useTheme, Container, Button, CssBaseline, Grid, Typography, CircularProgress, Card, Box } from '@mui/material'; import CheckIcon from '@mui/icons-material/Check'; import ClearIcon from '@mui/icons-material/Clear'; -import { SessionContext } from '../SessionContext'; +import { SessionContext } from '../../SessionContext'; import { useContext } from 'react'; import Confetti from 'react-confetti'; import { CountdownCircleTimer } from "react-countdown-circle-timer"; import { useLocation } from 'react-router-dom'; import io from 'socket.io-client'; import { useTranslation } from 'react-i18next'; -import i18n from '../localize/i18n'; - +import i18n from '../../localize/i18n'; const apiEndpoint = process.env.REACT_APP_API_ENDPOINT || 'http://localhost:8000'; const socketEndpoint = process.env.REACT_APP_MULTIPLAYER_ENDPOINT || 'ws://localhost:5010'; -const Game = () => { - const { t } = useTranslation(); - +const Game = () => { const MAX_ROUNDS = 3; const SUCCESS_SOUND_ROUTE = "/sounds/success_sound.mp3"; const FAILURE_SOUND_ROUTE = "/sounds/wrong_sound.mp3"; @@ -26,6 +23,11 @@ const Game = () => { //sesion information const {username} = useContext(SessionContext); + // Translations + const { t } = useTranslation(); + + const theme = useTheme(); + // state initialization const [round, setRound] = React.useState(1); const [questionData, setQuestionData] = React.useState(null); @@ -44,7 +46,6 @@ const Game = () => { const [userResponses, setUserResponses] = React.useState([]); const [, setCurrentLanguage] = React.useState(i18n.language); - const location = useLocation(); const { gameQuestions, roomCode} = location.state; const [socket, setSocket] = React.useState(null); @@ -95,11 +96,7 @@ const Game = () => { // stablish if the confetti must show or not React.useEffect(() => { - if (winnerPlayer === username) { - setShowConfetti(true); - } else { - setShowConfetti(false); - } + winnerPlayer === username ? setShowConfetti(true) : setShowConfetti(false); }, [winnerPlayer, username]); @@ -107,16 +104,12 @@ const Game = () => { const startNewRound = async () => { setAnswered(false); const quest = gameQuestions[round-1] - setQuestionData(quest); setButtonStates(new Array(quest.options.length).fill(null)); - }; const updateStatistics = async() => { try { - //const winner = winner === username ? 1 : 0; - await axios.put(`${apiEndpoint}/statistics`, { username:username, the_callenge_earned_money:0, @@ -223,180 +216,92 @@ const Game = () => { const questionHistorialBar = () => { return questionHistorial.map((isCorrect, index) => ( - - {index + 1} - + )); - }; + }; // circular loading if (!questionData) { return ( - + ); } -if (shouldRedirect) { - socket.emit("finished-game", username, correctlyAnsweredQuestions, totalTimePlayed); + if (shouldRedirect) { + socket.emit("finished-game", username, correctlyAnsweredQuestions, totalTimePlayed); - return ( - - - - - - {winnerPlayer === "" ? t("Multiplayer.Game.waiting_players_end") : "Game Over"} - - -
- - - - {t("Game.correct")}: {correctlyAnsweredQuestions} - - - - - {t("Game.incorrect")}: {incorrectlyAnsweredQuestions} - - - - - {t("Game.money")}: {totalScore} - - - - - {t("Game.time")}: {totalTimePlayed} - - - + return ( + + + + + {winnerPlayer === "" ? t("Multiplayer.Game.waiting_players_end") : t("Game.lose_msg")} + + + + { t("Game.correct") }: {correctlyAnsweredQuestions} + { t("Game.incorrect") }: {incorrectlyAnsweredQuestions} + { t("Game.money") }: {totalScore} + { t("Game.time") }: {totalTimePlayed} + {winnerPlayer === "" ? ( - { t("Multiplayer.Game.waiting") } + { t("Multiplayer.Game.waiting") } ) : ( - { t("Multiplayer.Game.winner_1") }: {winnerPlayer} { t("Multiplayer.Game.winner_2") } {winnerCorrect} { t("Multiplayer.Game.winner_3") } {winnerTime} { t("Multiplayer.Game.winner_4") } + + { t("Multiplayer.Game.winner_1") }: {winnerPlayer} + { t("Multiplayer.Game.winner_2") }: {winnerCorrect} { t("Multiplayer.Game.winner_3") } {winnerTime} { t("Multiplayer.Game.winner_4") } + )} -
- {showConfetti && } -
- ); -} + + {showConfetti && } +
+ ); + } + return ( - + - - + selectResponse(-1, "FAILED")}> + {({ remainingTime }) => { + return ( + + {remainingTime} + + ); }} - > - {questionHistorialBar()} - + + + + + + {questionData.question.toUpperCase()} + + + + {questionData.options.map((option, index) => ( + + + + ))} + - - {round} / {MAX_ROUNDS} - - - {questionData.question} - selectResponse(0, "FAILED")} //when time ends always fail question - > - {({ remainingTime }) => { - return ( -
-
{remainingTime}
-
- ); - }} -
-
- - - {questionData.options.map((option, index) => ( - - - - ))} - + {/* Progress Cards */} + + {questionHistorialBar()} +
); }; diff --git a/webapp/src/pages/games/MultiplayerRoom.js b/webapp/src/pages/games/MultiplayerRoom.js new file mode 100644 index 00000000..423db081 --- /dev/null +++ b/webapp/src/pages/games/MultiplayerRoom.js @@ -0,0 +1,214 @@ +import React, { useState, useEffect } from 'react'; +import { useTheme, Button, TextField, Typography, Grid, Paper, List, ListItem, CircularProgress, Container, Box } from '@mui/material'; +import io from 'socket.io-client'; +import { useContext } from 'react'; +import { SessionContext } from '../../SessionContext'; +import { useNavigate } from 'react-router-dom'; +import { useTranslation } from 'react-i18next'; +import Chat from '../../components/Chat'; + +const socketEndpoint = process.env.REACT_APP_MULTIPLAYER_ENDPOINT || 'http://localhost:5010'; + +const MultiplayerRoom = () => { + const { t } = useTranslation(); + const theme = useTheme(); + + const [roomCode, setRoomCode] = useState(""); + const [error, setError] = useState(""); + const [socket, setSocket] = useState(null); + const [writtenCode, setWrittenCode] = useState(""); + const [roomPlayers, setRoomPlayers] = useState([]); + const {username} = useContext(SessionContext); + const [gameReady, setGameReady] = useState(false); + const [roomCreator, setRoomCreator] = useState(false); + const [gameQuestions, setGameQuestions] = useState(null); + const [loadingQuestions, setLoadingQuestions] = useState(false); + const navigate = useNavigate(); + const [gameLoaded, setGameLoaded] = useState(false); + + useEffect(() => { + const newSocket = io(socketEndpoint); + setSocket(newSocket); + + newSocket.on('game-ready', (msg) => { + if(msg === "ready") { + setGameReady(true); + setLoadingQuestions(true); + } else { + setGameReady(false); + } + + }); + + newSocket.on('update-players', (roomPlayers) => { + setRoomPlayers(roomPlayers); + }); + + newSocket.on('questions-ready', (questions, roomCode) => { + setGameQuestions(questions); + setLoadingQuestions(false); + setGameLoaded(true); + }); + + newSocket.on('join-error', errorMessage => { + setError(errorMessage); + }); + + // clean at component dismount + return () => { + newSocket.disconnect(); + }; + }, [navigate]); + + useEffect(() => { + if(socket !== null) { + socket.on('btn-start-pressed', () => { + navigate('/multiplayerGame', { state: {gameQuestions, roomCode} }); + }); + } + }, [socket, navigate, gameQuestions, roomCode]) + + const handleCreateRoom = () => { + setError(""); + const code = generateRoomCode(); + setRoomCreator(true); + + socket.on('connection', () => { + + }); + + socket.emit('join-room', code, username, "create"); + }; + + const handleJoinRoom = () => { + socket.emit('join-room', writtenCode, username, "join"); + setRoomCreator(false); + setRoomCode(writtenCode); + }; + + const generateRoomCode = () => { + const characters = 'ABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789'; + const codeLength = 5; + + let code = ''; + for (let i = 0; i < codeLength; i++) { + const randomIndex = Math.floor(Math.random() * characters.length); + code += characters[randomIndex]; + } + setRoomCode(code); + + return code; + } + + const startGame = () => { + setGameReady(false); + socket.emit("started-game", true); + } + + return ( + + + + {t("Games.Multiplayer.name").toUpperCase()} + + + {roomCode && error === "" ? ( + <> + + { t("Multiplayer.Room.code") }: + + + {roomCode} + + + + { t("Multiplayer.Room.participants") }: + + + {roomPlayers.map((player, index) => ( + + {player} + + ))} + + + {loadingQuestions && ( +
+ + Loading questions... +
+ )} + + ) : ( + + + + { t("Multiplayer.Room.join") } + + setWrittenCode(e.target.value)} + /> + + {error && ( + + {error} + + )} + + + + + { t("Multiplayer.Room.new_game") } + + + + + )} + {roomCode && error === "" && ( + + + + )} +
+
+ ); + } + +export default MultiplayerRoom; \ No newline at end of file diff --git a/webapp/src/pages/games/TheChallengeGame.js b/webapp/src/pages/games/TheChallengeGame.js new file mode 100644 index 00000000..8ea6bf80 --- /dev/null +++ b/webapp/src/pages/games/TheChallengeGame.js @@ -0,0 +1,396 @@ +import * as React from 'react'; +import axios from 'axios'; +import { Container, Box, Button, CssBaseline, Grid, Typography, CircularProgress, useTheme, MenuItem, Select, IconButton, Paper } from '@mui/material'; +import CheckIcon from '@mui/icons-material/Check'; +import ClearIcon from '@mui/icons-material/Clear'; +import { useNavigate } from 'react-router-dom'; +import { SessionContext } from '../../SessionContext'; +import { useContext } from 'react'; +import Confetti from 'react-confetti'; +import { CountdownCircleTimer } from "react-countdown-circle-timer"; +import Card from '@mui/material/Card'; +import { useTranslation } from 'react-i18next'; +import i18n from '../../localize/i18n'; +import { PlayArrow, Pause } from '@mui/icons-material'; +import AddIcon from '@mui/icons-material/Add'; +import RemoveIcon from '@mui/icons-material/Remove'; + +const apiEndpoint = process.env.REACT_APP_API_ENDPOINT || 'http://localhost:8000'; + +const TheChallengeGame = () => { + const navigate = useNavigate(); + const SUCCESS_SOUND_ROUTE = "/sounds/success_sound.mp3"; + const FAILURE_SOUND_ROUTE = "/sounds/wrong_sound.mp3"; + + //sesion information + const {username} = useContext(SessionContext); + const theme = useTheme(); + + const { t } = useTranslation(); + + // Game configuration state + const [numRounds, setNumRounds] = React.useState(3); + const [timerConfig, setTimerConfig] = React.useState(15); + const [configModalOpen, setConfigModalOpen] = React.useState(true); + const [category, setCategory] = React.useState("Geography"); + + // state initialization + const [round, setRound] = React.useState(1); + const [questionData, setQuestionData] = React.useState(null); + const [buttonStates, setButtonStates] = React.useState([]); + const [answered, setAnswered] = React.useState(false); + const [shouldRedirect, setShouldRedirect] = React.useState(false); + const [totalScore, setTotalScore] = React.useState(0); + const [correctlyAnsweredQuestions, setCorrectlyAnsweredQuestions] = React.useState(0); + const [incorrectlyAnsweredQuestions, setIncorrectlyAnsweredQuestions] = React.useState(0); + const [totalTimePlayed, setTotalTimePlayed] = React.useState(0); + const [timerRunning, setTimerRunning] = React.useState(true); // indicate if the timer is working + const [showConfetti, setShowConfetti] = React.useState(false); //indicates if the confetti must appear + const [questionCountdownKey, setQuestionCountdownKey] = React.useState(15); //key to update question timer + const [questionCountdownRunning, setQuestionCountdownRunning] = React.useState(false); //property to start and stop question timer + const [userResponses, setUserResponses] = React.useState([]); + const [language, setCurrentLanguage] = React.useState(i18n.language); + const [paused, setPaused] = React.useState(false); + const [passNewRound, setPassNewRound] = React.useState(false); + + const [questionHistorial, setQuestionHistorial] = React.useState(Array(numRounds).fill(null)); + + React.useEffect(() => { + let timer; + if (timerRunning) { + timer = setInterval(() => { + setTotalTimePlayed((prevTotalTime) => prevTotalTime + 1); + }, 1000); + } + + return () => clearInterval(timer); + }, [timerRunning]); + + // hook to initiating new rounds if the current number of rounds is less than or equal to 3 + React.useEffect(() => { + if (round <= numRounds) { + startNewRound(); + setQuestionCountdownRunning(true); + setQuestionCountdownKey(questionCountdownKey => questionCountdownKey + 1); //code to reset countdown timer + } else { + setTimerRunning(false); + setShouldRedirect(true); + setQuestionCountdownRunning(false); + updateStatistics(); + updateQuestionsRecord(); + } + // eslint-disable-next-line + }, [round, numRounds]); + + // stablish if the confetti must show or not + React.useEffect(() => { + if (correctlyAnsweredQuestions > incorrectlyAnsweredQuestions) { + setShowConfetti(true); + } else { + setShowConfetti(false); + } + }, [correctlyAnsweredQuestions, incorrectlyAnsweredQuestions]); + + React.useEffect(() => { + if (passNewRound && !paused) { + setRound(prevRound => { + return prevRound + 1; + }); + setButtonStates([]); + } + }, [paused, passNewRound]); + + + const startGame = () => { + setConfigModalOpen(false); + startNewRound(); + }; + + const startNewRound = async () => { + setAnswered(false); + setPassNewRound(false); + setCurrentLanguage(i18n.language); + axios.get(`${apiEndpoint}/questions/${language}/${category}`) + .then(quest => { + // every new round it gets a new question from db + setQuestionData(quest.data[0]); + setButtonStates(new Array(quest.data[0].options.length).fill(null)); + }).catch(error => { + console.error(error); + }); + + }; + + const updateStatistics = async() => { + try { + await axios.put(`${apiEndpoint}/statistics`, { + username:username, + the_callenge_earned_money:totalScore, + the_callenge_correctly_answered_questions:correctlyAnsweredQuestions, + the_callenge_incorrectly_answered_questions:incorrectlyAnsweredQuestions, + the_callenge_total_time_played:totalTimePlayed, + the_callenge_games_played:1, + wise_men_stack_earned_money: 0, + wise_men_stack_correctly_answered_questions: 0, + wise_men_stack_incorrectly_answered_questions: 0, + wise_men_stack_games_played: 0, + warm_question_earned_money: 0, + warm_question_correctly_answered_questions: 0, + warm_question_incorrectly_answered_questions: 0, + warm_question_passed_questions: 0, + warm_question_games_played: 0, + discovering_cities_earned_money: 0, + discovering_cities_correctly_answered_questions: 0, + discovering_cities_incorrectly_answered_questions: 0, + discovering_cities_games_played: 0, + online_earned_money: 0, + online_correctly_answered_questions: 0, + online_incorrectly_answered_questions: 0, + online_total_time_played: 0, + online_games_played: 0, + }); + } catch (error) { + console.error("Error:", error); + }; + } + + const updateQuestionsRecord = async() => { + try { + await axios.put(`${apiEndpoint}/questionsRecord`, { + questions: userResponses, + username: username, + gameMode: "TheChallenge" + }); + } catch (error) { + console.error("Error:", error); + }; + } + + // this function is called when a user selects a response. + const selectResponse = async (index, response) => { + setAnswered(true); + const newButtonStates = [...buttonStates]; + + setQuestionCountdownRunning(false); + + //check answer + if (response === questionData.correctAnswer) { + const userResponse = { + question: questionData.question, + response: response, + options: questionData.options, + correctAnswer: questionData.correctAnswer + }; + setUserResponses(prevResponses => [...prevResponses, userResponse]); + + newButtonStates[index] = "success" + const sucessSound = new Audio(SUCCESS_SOUND_ROUTE); + sucessSound.volume = 0.40; + sucessSound.play(); + setCorrectlyAnsweredQuestions(correctlyAnsweredQuestions + 1); + setTotalScore(totalScore + 20); + + const newQuestionHistorial = [...questionHistorial]; + newQuestionHistorial[round-1] = true; + setQuestionHistorial(newQuestionHistorial); + } else { + const userResponse = { + question: questionData.question, + response: response, + options: questionData.options, + correctAnswer: questionData.correctAnswer + }; + setUserResponses(prevResponses => [...prevResponses, userResponse]); + newButtonStates[index] = "failure"; + const failureSound = new Audio(FAILURE_SOUND_ROUTE); + failureSound.volume = 0.40; + failureSound.play(); + for (let i = 0; i < questionData.options.length; i++) { + if (questionData.options[i] === questionData.correctAnswer) { + newButtonStates[i] = "success"; + } + } + setIncorrectlyAnsweredQuestions(incorrectlyAnsweredQuestions + 1); + + const newQuestionHistorial = [...questionHistorial]; + newQuestionHistorial[round-1] = false; + setQuestionHistorial(newQuestionHistorial); + } + + setButtonStates(newButtonStates); + + setTimeout(async() => { + setPassNewRound(true); + setCurrentLanguage(i18n.language); + }, 4000); + }; + + // Render the configuration window before starting the game + if (configModalOpen) { + + return( + + + + {t("Game.config.title")} + + + + + + {t("Game.config.num_rounds")}: + + setNumRounds(numRounds - 1)} variant="outlined" data-testId="removeRound"> + + + + {numRounds} + + setNumRounds(numRounds + 1)} variant="outlined" data-testId="addRound"> + + + + + + + {t("Game.config.time")}: + + setTimerConfig(timerConfig - 1)} variant="outlined" data-testId="removeSecond"> + + + + {timerConfig} + + setTimerConfig(timerConfig + 1)} variant="outlined" data-testId="addSecond"> + + + + + {/* Dropdown for selecting category */} + + + {t("Game.config.category")}: + + + + + + + + ); + } + + const questionHistorialBar = () => { + return questionHistorial.map((isCorrect, index) => ( + + )); + }; + + const togglePause = () => { + setTimerRunning(!timerRunning); + setPaused(!paused); + } + + + // circular loading + if (!questionData) { + return ( + + + + + ); + } + + // redirect to homepage if game over + if (shouldRedirect) { + // Redirect after 4 seconds + setTimeout(() => { + navigate('/homepage'); + }, 4000); + + + return ( + + + incorrectlyAnsweredQuestions ? theme.palette.success.main : theme.palette.error.main }}> + {correctlyAnsweredQuestions > incorrectlyAnsweredQuestions ? t("Game.win_msg") : t("Game.lose_msg") } + + + { t("Game.correct") }: {correctlyAnsweredQuestions} + { t("Game.incorrect") }: {incorrectlyAnsweredQuestions} + { t("Game.money") }: {totalScore} + { t("Game.time") }: {totalTimePlayed} + + {showConfetti && } + + ); + } + + return ( + + + + + { answered ? + // Pausa + togglePause()} sx={{ height: 100, width: 100, border: `2px solid ${theme.palette.primary.main}` }} + data-testid={ paused ? "play" : "pause"} > + { paused ? : } + + : + // Cronómetro + selectResponse(-1, "FAILED")}> + {({ remainingTime }) => { + return ( + + {remainingTime} + + ); + }} + + } + + + + + {questionData.question.toUpperCase()} + + + + {questionData.options.map((option, index) => ( + + + + ))} + + + + {/* Progress Cards */} + + {questionHistorialBar()} + + + ); +}; + +export default TheChallengeGame; \ No newline at end of file diff --git a/webapp/src/pages/WarmQuestionGame.js b/webapp/src/pages/games/WarmQuestionGame.js similarity index 54% rename from webapp/src/pages/WarmQuestionGame.js rename to webapp/src/pages/games/WarmQuestionGame.js index bcd63cf9..cf6b4b58 100644 --- a/webapp/src/pages/WarmQuestionGame.js +++ b/webapp/src/pages/games/WarmQuestionGame.js @@ -1,23 +1,20 @@ import * as React from 'react'; import axios from 'axios'; - -import { Container, Button, CssBaseline, Grid, Typography, CircularProgress } from '@mui/material'; +import { useTheme, Container, Button, CssBaseline, Grid, Typography, CircularProgress, Card, Box, IconButton } from '@mui/material'; import { PlayArrow, Pause } from '@mui/icons-material'; import CheckIcon from '@mui/icons-material/Check'; import ClearIcon from '@mui/icons-material/Clear'; import { useNavigate } from 'react-router-dom'; -import { SessionContext } from '../SessionContext'; +import { SessionContext } from '../../SessionContext'; import { useContext } from 'react'; import Confetti from 'react-confetti'; import { CountdownCircleTimer } from "react-countdown-circle-timer"; -import Card from '@mui/material/Card'; -import CardContent from '@mui/material/CardContent'; -import i18n from '../localize/i18n'; +import { useTranslation } from 'react-i18next'; +import i18n from '../../localize/i18n'; const apiEndpoint = process.env.REACT_APP_API_ENDPOINT || 'http://localhost:8000'; - -const Game = () => { +const WarmQuestionGame = () => { const navigate = useNavigate(); const MAX_ROUNDS = 10; const SUCCESS_SOUND_ROUTE = "/sounds/success_sound.mp3"; @@ -26,6 +23,11 @@ const Game = () => { //sesion information const {username} = useContext(SessionContext); + // Translations + const { t } = useTranslation(); + + const theme = useTheme(); + // state initialization const [round, setRound] = React.useState(1); const [questionData, setQuestionData] = React.useState(null); @@ -42,11 +44,12 @@ const Game = () => { const [questionCountdownKey, setQuestionCountdownKey] = React.useState(15); //key to update question timer const [questionCountdownRunning, setQuestionCountdownRunning] = React.useState(false); //property to start and stop question timer const [userResponses, setUserResponses] = React.useState([]); + const [paused, setPaused] = React.useState(false); + const [passNewRound, setPassNewRound] = React.useState(false); const [language, setCurrentLanguage] = React.useState(i18n.language); const [questionHistorial, setQuestionHistorial] = React.useState(Array(MAX_ROUNDS).fill(null)); - React.useEffect(() => { let timer; if (timerRunning) { @@ -76,18 +79,23 @@ const Game = () => { // stablish if the confetti must show or not React.useEffect(() => { - if (correctlyAnsweredQuestions > incorrectlyAnsweredQuestions) { - setShowConfetti(true); - } else { - setShowConfetti(false); + correctlyAnsweredQuestions > incorrectlyAnsweredQuestions ? setShowConfetti(true) : setShowConfetti(false); + }, [correctlyAnsweredQuestions, incorrectlyAnsweredQuestions]); + + React.useEffect(() => { + if (passNewRound && !paused) { + setRound(prevRound => { + return prevRound + 1; + }); + setButtonStates([]); } - }, [correctlyAnsweredQuestions, incorrectlyAnsweredQuestions]); - + }, [paused, passNewRound]); // gets a random question from the database and initializes button states to null const startNewRound = async () => { setAnswered(false); - // It works deploying using git repo from machine with: axios.get(`http://20.80.235.188:8000/questions`) + setPassNewRound(false); + // Updates current language setCurrentLanguage(i18n.language); axios.get(`${apiEndpoint}/questions/${language}`) @@ -98,7 +106,6 @@ const Game = () => { }).catch(error => { console.error(error); }); - }; const updateStatistics = async() => { @@ -233,214 +240,112 @@ const Game = () => { const questionHistorialBar = () => { return questionHistorial.map((isCorrect, index) => ( - - {index + 1} - + )); - }; + }; const togglePause = () => { setTimerRunning(!timerRunning); - setQuestionCountdownRunning(!timerRunning); - if (timerRunning) { - // Si el juego estaba en marcha y se pausa, deshabilitar los botones - setButtonStates(new Array(questionData.options.length).fill(true)); - } else { - // Si el juego estaba pausado y se reanuda, habilitar los botones - setButtonStates(new Array(questionData.options.length).fill(null)); - } + setPaused(!paused); } - // circular loading if (!questionData) { return ( - + ); } - // redirect to / if game over -if (shouldRedirect) { - // Redirect after 3 seconds - setTimeout(() => { - navigate('/homepage'); - }, 4000); + // redirect to homepage if game over + if (shouldRedirect) { + // Redirect after 4 seconds + setTimeout(() => { + navigate('/homepage'); + }, 4000); + return ( + + + incorrectlyAnsweredQuestions ? theme.palette.success.main : theme.palette.error.main }}> + {correctlyAnsweredQuestions > incorrectlyAnsweredQuestions ? t("Game.win_msg") : t("Game.lose_msg") } + + + { t("Game.correct") }: {correctlyAnsweredQuestions} + { t("Game.incorrect") }: {incorrectlyAnsweredQuestions} + { t("Game.money") }: {totalScore} + { t("Game.time") }: {totalTimePlayed} + + {showConfetti && } + + ); + } return ( - - - incorrectlyAnsweredQuestions ? 'green' : 'red', - fontSize: '4rem', // Tamaño de fuente - marginTop: '20px', // Espaciado superior - marginBottom: '50px', // Espaciado inferior - }} - > - {correctlyAnsweredQuestions > incorrectlyAnsweredQuestions ? "Great Job!" : "Game Over"} - -
- Correct Answers: {correctlyAnsweredQuestions} - Incorrect Answers: {incorrectlyAnsweredQuestions} - Skipped Questions: {passedQuestions} - Total money: {totalScore} - Game time: {totalTimePlayed} seconds -
- {showConfetti && } -
- ); -} - return ( - + - - Game time: {totalTimePlayed} s - - - - - - - - {questionHistorialBar()} - + + { answered ? + // Pausa + togglePause()} sx={{ height: 100, width: 100, border: `2px solid ${theme.palette.primary.main}` }} + data-testid={ paused ? "play" : "pause"} > + { paused ? : } + + : + // Cronómetro + selectResponse(-1, "FAILED")}> + {({ remainingTime }) => { + return ( + + {remainingTime} + + ); + }} + + } + + + + + {questionData.question.toUpperCase()} + + + + {questionData.options.map((option, index) => ( + + + + ))} + - - {round} / {MAX_ROUNDS} - - - {questionData.question} - selectResponse(0, "FAILED")} //when time ends always fail question - > - {({ remainingTime }) => { - return ( -
-
{remainingTime}
-
- ); - }} -
-
- - - {questionData.options.map((option, index) => ( - - - - ))} - - - {!answered && ( - - )} + { answered ? + + : + + } - + {/* Progress Cards */} + + {questionHistorialBar()} +
); }; -export default Game; \ No newline at end of file +export default WarmQuestionGame; \ No newline at end of file diff --git a/webapp/src/pages/games/WiseMenStackGame.js b/webapp/src/pages/games/WiseMenStackGame.js new file mode 100644 index 00000000..f056c511 --- /dev/null +++ b/webapp/src/pages/games/WiseMenStackGame.js @@ -0,0 +1,360 @@ +import * as React from 'react'; +import axios from 'axios'; +import { Container, Box, Button, CssBaseline, Grid, Typography, CircularProgress, Card, Select, MenuItem, IconButton, useTheme, Paper } from '@mui/material'; +import CheckIcon from '@mui/icons-material/Check'; +import ClearIcon from '@mui/icons-material/Clear'; +import { PlayArrow, Pause } from '@mui/icons-material'; +import { useNavigate } from 'react-router-dom'; +import { SessionContext } from '../../SessionContext'; +import { useContext } from 'react'; +import Confetti from 'react-confetti'; +import { CountdownCircleTimer } from "react-countdown-circle-timer"; +import { useTranslation } from 'react-i18next'; +import i18n from '../../localize/i18n'; + +const apiEndpoint = process.env.REACT_APP_API_ENDPOINT || 'http://localhost:8000'; + +const WiseMenStackGame = () => { + const navigate = useNavigate(); + const SUCCESS_SOUND_ROUTE = "/sounds/success_sound.mp3"; + const FAILURE_SOUND_ROUTE = "/sounds/wrong_sound.mp3"; + + //sesion information + const {username} = useContext(SessionContext); + const theme = useTheme(); + + // Traductions + const { t } = useTranslation(); + + // state initialization + const [round, setRound] = React.useState(1); + const [questionData, setQuestionData] = React.useState(null); + const [buttonStates, setButtonStates] = React.useState([]); + const [answered, setAnswered] = React.useState(false); + const [shouldRedirect, setShouldRedirect] = React.useState(false); + const [totalScore, setTotalScore] = React.useState(0); + const [correctlyAnsweredQuestions, setCorrectlyAnsweredQuestions] = React.useState(0); + const [incorrectlyAnsweredQuestions, setIncorrectlyAnsweredQuestions] = React.useState(0); + const [totalTimePlayed, setTotalTimePlayed] = React.useState(0); + const [timerRunning, setTimerRunning] = React.useState(true); // indicate if the timer is working + const [showConfetti, setShowConfetti] = React.useState(false); //indicates if the confetti must appear + const [questionCountdownKey, ] = React.useState(60); //key to update question timer + const [questionCountdownRunning, setQuestionCountdownRunning] = React.useState(false); //property to start and stop question timer + const [userResponses, setUserResponses] = React.useState([]); + const [language, setCurrentLanguage] = React.useState(i18n.language); + + const [category, setCategory] = React.useState('Geography'); + const [possibleAnswers, setPossibleAnswers] = React.useState([]); + const [isConfigured, setConfiguration] = React.useState(false); + const [paused, setPaused] = React.useState(false); + const [passNewRound, setPassNewRound] = React.useState(false); + + const [questionHistorial, setQuestionHistorial] = React.useState(Array(round).fill(null)); + + React.useEffect(() => { + let timer; + if (timerRunning) { + timer = setInterval(() => { + setTotalTimePlayed((prevTotalTime) => prevTotalTime + 1); + }, 1000); + } + + return () => clearInterval(timer); + }, [timerRunning]); + + // hook to initiating new rounds if the current number of rounds is less than or equal to 3 + React.useEffect(() => { + if (totalTimePlayed <= questionCountdownKey) { + startNewRound(); + setQuestionCountdownRunning(true); + } else { + setTimerRunning(false); + setShouldRedirect(true); + setQuestionCountdownRunning(false); + updateStatistics(); + updateQuestionsRecord(); + } + // eslint-disable-next-line + }, [round]); + + // stablish if the confetti must show or not + React.useEffect(() => { + if (correctlyAnsweredQuestions > incorrectlyAnsweredQuestions) { + setShowConfetti(true); + } else { + setShowConfetti(false); + } + }, [correctlyAnsweredQuestions, incorrectlyAnsweredQuestions]); + + React.useEffect(() => { + if (passNewRound && !paused) { + setRound(prevRound => { + return prevRound + 1; + }); + setButtonStates([]); + } + }, [paused, passNewRound]); + + const startNewRound = async () => { + setAnswered(false); + + setPassNewRound(false); + + // Updates current language + setCurrentLanguage(i18n.language); + axios.get(`${apiEndpoint}/questions/${language}/${category}`) + .then(quest => { + // every new round it gets a new question from db + setQuestionData(quest.data[0]); + setButtonStates(new Array(2).fill(null)); + getPossibleOptions(quest.data[0]); + + }).catch(error => { + console.error("Could not get questions", error); + }); + + }; + + // It puts 2 possible answers into an array making sure that the correct answer is not repeated + const getPossibleOptions = async (question) => { + var options = []; + options.push(question.correctAnswer); + let randomNumber ; + do { + randomNumber = Math.floor(Math.random() * question.options.length); + } while (question.options[randomNumber] === question.correctAnswer); + options.push(question.options[randomNumber]); + options = shuffleArray(options); + setPossibleAnswers(options); + } + + // Shuffles array + function shuffleArray(array) { + const random = Math.random(); + const randomFactor = random < 0.5 ? -1 : 1; + return array.sort(() => randomFactor); + } + + const updateStatistics = async() => { + try { + await axios.put(`${apiEndpoint}/statistics`, { + username:username, + wise_men_stack_earned_money:totalScore, + wise_men_stack_correctly_answered_questions:correctlyAnsweredQuestions, + wise_men_stack_incorrectly_answered_questions:incorrectlyAnsweredQuestions, + wise_men_stack_games_played:1 + }); + } catch (error) { + console.error("Error:", error); + }; + } + + const updateQuestionsRecord = async() => { + try { + await axios.put(`${apiEndpoint}/questionsRecord`, { + questions: userResponses, + username: username, + gameMode: "WiseMenStack" + }); + } catch (error) { + console.error("Error:", error); + }; + } + + // this function is called when a user selects a response. + const selectResponse = async (index, response) => { + setAnswered(true); + const newButtonStates = [...buttonStates]; + + //setQuestionCountdownRunning(false); + + //check answer + if (response === questionData.correctAnswer) { + const userResponse = { + question: questionData.question, + response: response, + options: questionData.options, + correctAnswer: questionData.correctAnswer + }; + setUserResponses(prevResponses => [...prevResponses, userResponse]); + + newButtonStates[index] = "success" + const sucessSound = new Audio(SUCCESS_SOUND_ROUTE); + sucessSound.volume = 0.40; + sucessSound.play(); + setCorrectlyAnsweredQuestions(correctlyAnsweredQuestions + 1); + setTotalScore(totalScore + 20); + + const newQuestionHistorial = [...questionHistorial]; + newQuestionHistorial[round-1] = true; + setQuestionHistorial(newQuestionHistorial); + } else { + const userResponse = { + question: questionData.question, + response: response, + options: questionData.options, + correctAnswer: questionData.correctAnswer + }; + setUserResponses(prevResponses => [...prevResponses, userResponse]); + newButtonStates[index] = "failure"; + const failureSound = new Audio(FAILURE_SOUND_ROUTE); + failureSound.volume = 0.40; + failureSound.play(); + for (let i = 0; i < questionData.options.length; i++) { + if (questionData.options[i] === questionData.correctAnswer) { + newButtonStates[i] = "success"; + } + } + setIncorrectlyAnsweredQuestions(incorrectlyAnsweredQuestions + 1); + + const newQuestionHistorial = [...questionHistorial]; + newQuestionHistorial[round-1] = false; + setQuestionHistorial(newQuestionHistorial); + } + + setButtonStates(newButtonStates); + + setTimeout(async() => { + setPassNewRound(true); + setCurrentLanguage(i18n.language); + }, 4000); + }; + + const questionHistorialBar = () => { + return questionHistorial.map((isCorrect, index) => ( + + )); + }; + + const togglePause = () => { + setTimerRunning(!timerRunning); + setPaused(!paused); + } + + if(!isConfigured) { + return ( + + + + {t("Game.config.title")} + + + + + {t("Wise_Men.instructions1")} + {t("Wise_Men.instructions2")} + {t("Wise_Men.instructions3")} + + + {/* Dropdown for selecting category */} + + + {t("Game.config.category")}: + + + + + + + + + ); + } + + // circular loading + if (!questionData) { + return ( + + + + + ); + } + + // redirect to homepage if game over + if (shouldRedirect) { + // Redirect after 4 seconds + setTimeout(() => { + navigate('/homepage'); + }, 4000); + + return ( + + + incorrectlyAnsweredQuestions ? theme.palette.success.main : theme.palette.error.main }}> + {correctlyAnsweredQuestions > incorrectlyAnsweredQuestions ? t("Game.win_msg") : t("Game.lose_msg") } + + + { t("Game.correct") }: {correctlyAnsweredQuestions} + { t("Game.incorrect") }: {incorrectlyAnsweredQuestions} + { t("Game.money") }: {totalScore} + { t("Game.time") }: {totalTimePlayed} + + {showConfetti && } + + ); + } + + return ( + + + + + { answered ? + // Pausa + togglePause()} sx={{ height: 100, width: 100, border: `2px solid ${theme.palette.primary.main}` }} + data-testid={ paused ? "play" : "pause"} > + { paused ? : } + + : + // Cronómetro + selectResponse(-1, "FAILED")}> + {({ remainingTime }) => { + return ( + + {remainingTime} + + ); + }} + + } + + + + + {questionData.question.toUpperCase()} + + + + {possibleAnswers.map((option, index) => ( + + + + ))} + + + + + {questionHistorialBar()} + { answered || round === 1 ? : } + + + ); +}; + +export default WiseMenStackGame; \ No newline at end of file