added cypress e2e tests

This commit is contained in:
Wenszel 2023-12-19 09:27:47 +01:00
parent 97513eac2d
commit ce9e93d68f
13 changed files with 2467 additions and 35 deletions

View File

@ -1,6 +1,5 @@
# <center>Online Multiplayer Ludo Game</center> # <center>Online Multiplayer Ludo Game</center>
\>\> [Play Online here](www.github.com/wenszel/mern-ludo) << \>\> [Play Online here](www.github.com/wenszel/mern-ludo) <<
\>\> [Watch YouTube Video here](www.github.com/wenszel/mern-ludo) << \>\> [Watch YouTube Video here](www.github.com/wenszel/mern-ludo) <<
@ -20,17 +19,22 @@
- [Screenshots](#screenshots) - [Screenshots](#screenshots)
## About ## About
Ludo Online is a multiplayer web-based implementation of the classic board game Ludo, built using the MERN stack and integrated with SocketIO for real-time communication. Ludo Online is a multiplayer web-based implementation of the classic board game Ludo, built using the MERN stack and integrated with SocketIO for real-time communication.
## Architecture ## Architecture
![Interface](https://github.com/Wenszel/mern-ludo/blob/main/src/images/architecture.png?raw=true) ![Interface](https://github.com/Wenszel/mern-ludo/blob/main/src/images/architecture.png?raw=true)
## Tech Stack ## Tech Stack
Frontend: Frontend:
![JavaScript](https://img.shields.io/badge/javascript-%23323330.svg?style=for-the-badge&logo=javascript&logoColor=%23F7DF1E) ![React](https://img.shields.io/badge/react-%2320232a.svg?style=for-the-badge&logo=react&logoColor=%2361DAFB) ![React Router](https://img.shields.io/badge/React_Router-CA4245?style=for-the-badge&logo=react-router&logoColor=white) ![JavaScript](https://img.shields.io/badge/javascript-%23323330.svg?style=for-the-badge&logo=javascript&logoColor=%23F7DF1E) ![React](https://img.shields.io/badge/react-%2320232a.svg?style=for-the-badge&logo=react&logoColor=%2361DAFB) ![React Router](https://img.shields.io/badge/React_Router-CA4245?style=for-the-badge&logo=react-router&logoColor=white)
![CSS3](https://img.shields.io/badge/css3-%231572B6.svg?style=for-the-badge&logo=css3&logoColor=white) ![HTML5](https://img.shields.io/badge/html5-%23E34F26.svg?style=for-the-badge&logo=html5&logoColor=white) ![MUI](https://img.shields.io/badge/MUI-%230081CB.svg?style=for-the-badge&logo=mui&logoColor=white) ![CSS3](https://img.shields.io/badge/css3-%231572B6.svg?style=for-the-badge&logo=css3&logoColor=white) ![HTML5](https://img.shields.io/badge/html5-%23E34F26.svg?style=for-the-badge&logo=html5&logoColor=white) ![MUI](https://img.shields.io/badge/MUI-%230081CB.svg?style=for-the-badge&logo=mui&logoColor=white)
Backend: Backend:
![MongoDB](https://img.shields.io/badge/MongoDB-%234ea94b.svg?style=for-the-badge&logo=mongodb&logoColor=white) ![Express.js](https://img.shields.io/badge/express.js-%23404d59.svg?style=for-the-badge&logo=express&logoColor=%2361DAFB) ![Socket.io](https://img.shields.io/badge/Socket.io-black?style=for-the-badge&logo=socket.io&badgeColor=010101) ![NodeJS](https://img.shields.io/badge/node.js-6DA55F?style=for-the-badge&logo=node.js&logoColor=white) ![MongoDB](https://img.shields.io/badge/MongoDB-%234ea94b.svg?style=for-the-badge&logo=mongodb&logoColor=white) ![Express.js](https://img.shields.io/badge/express.js-%23404d59.svg?style=for-the-badge&logo=express&logoColor=%2361DAFB) ![Socket.io](https://img.shields.io/badge/Socket.io-black?style=for-the-badge&logo=socket.io&badgeColor=010101) ![NodeJS](https://img.shields.io/badge/node.js-6DA55F?style=for-the-badge&logo=node.js&logoColor=white)
Tests: Tests:
![Mocha](https://img.shields.io/badge/-mocha-%238D6748?style=for-the-badge&logo=mocha&logoColor=white) ![Jest](https://img.shields.io/badge/-jest-%23C21325?style=for-the-badge&logo=jest&logoColor=white) ![cypress](https://img.shields.io/badge/-cypress-%23E5E5E5?style=for-the-badge&logo=cypress&logoColor=058a5e) ![Mocha](https://img.shields.io/badge/-mocha-%238D6748?style=for-the-badge&logo=mocha&logoColor=white) ![Jest](https://img.shields.io/badge/-jest-%23C21325?style=for-the-badge&logo=jest&logoColor=white)
Tools: Tools:
![Docker](https://img.shields.io/badge/docker-%230db7ed.svg?style=for-the-badge&logo=docker&logoColor=white) ![AWS](https://img.shields.io/badge/AWS-%23FF9900.svg?style=for-the-badge&logo=amazon-aws&logoColor=white) ![CircleCI](https://img.shields.io/badge/circle%20ci-%23161616.svg?style=for-the-badge&logo=circleci&logoColor=white) ![Git](https://img.shields.io/badge/git-%23F05033.svg?style=for-the-badge&logo=git&logoColor=white) ![Jira](https://img.shields.io/badge/jira-%230A0FFF.svg?style=for-the-badge&logo=jira&logoColor=white) ![Docker](https://img.shields.io/badge/docker-%230db7ed.svg?style=for-the-badge&logo=docker&logoColor=white) ![AWS](https://img.shields.io/badge/AWS-%23FF9900.svg?style=for-the-badge&logo=amazon-aws&logoColor=white) ![CircleCI](https://img.shields.io/badge/circle%20ci-%23161616.svg?style=for-the-badge&logo=circleci&logoColor=white) ![Git](https://img.shields.io/badge/git-%23F05033.svg?style=for-the-badge&logo=git&logoColor=white) ![Jira](https://img.shields.io/badge/jira-%230A0FFF.svg?style=for-the-badge&logo=jira&logoColor=white)
@ -38,18 +42,20 @@ Tools:
- Maintained session consistency with **Express Session** and **MongoDB**. - Maintained session consistency with **Express Session** and **MongoDB**.
- Enabled real-time communication via **WebSocket** and **SocketIO**. - Enabled real-time communication via **WebSocket** and **SocketIO**.
- Ensured code reliability with testing using **Mocha**, **Chai**, and **Jest**. - Maintained code reliability by implementing unit and integration tests using **Mocha**, **Chai**, and **Jest**.
- Hosted in a **Docker** container on **AWS EC2**. - Hosted in a **Docker** container on **AWS EC2**.
- Established CI/CD using **CircleCI**. - Established CI/CD using **CircleCI**.
- Implemented E2E tests utilizing **Cypress**, addressing challenges related to [testing collaboration](https://docs.cypress.io/guides/references/trade-offs#Multiple-browsers-open-at-the-same-time) and canvas functionality in the application.
## Installation ## Installation
1. Download this repository 1. Download this repository
2. Generate your own [mongoDB atlas](https://www.mongodb.com) credential URL. It should looks like this: 2. Generate your own [mongoDB atlas](https://www.mongodb.com) credential URL. It should looks like this:
``` ```
mongodb+srv://madmin:<password>@clustername.mongodb.net/<dbname>?retryWrites=true&w=majority mongodb+srv://madmin:<password>@clustername.mongodb.net/<dbname>?retryWrites=true&w=majority
``` ```
3. Add this URL to the /backend/credentials.js file 3. Add this URL to the /backend/credentials.js file
4. Perform these commands in the main directory: 4. Perform these commands in the main directory:

View File

@ -6,6 +6,7 @@ module.exports = function (mongoose) {
.connect(CONNECTION_URI, { .connect(CONNECTION_URI, {
useNewUrlParser: true, useNewUrlParser: true,
useUnifiedTopology: true, useUnifiedTopology: true,
dbName: 'test',
}) })
.then(() => { .then(() => {
console.log('MongoDB Connected…'); console.log('MongoDB Connected…');

View File

@ -21,7 +21,7 @@ module.exports = socket => {
}; };
const handleCreateRoom = async data => { const handleCreateRoom = async data => {
createNewRoom(data); await createNewRoom(data);
sendToOnePlayerRooms(socket.id, await getRooms()); sendToOnePlayerRooms(socket.id, await getRooms());
}; };

View File

@ -17,9 +17,9 @@ const getJoinableRoom = async () => {
return await Room.findOne({ full: false, started: false }).exec(); return await Room.findOne({ full: false, started: false }).exec();
}; };
const createNewRoom = data => { const createNewRoom = async data => {
const room = new Room(data); const room = new Room(data);
room.save(); await room.save();
return room; return room;
}; };

9
cypress.config.js Normal file
View File

@ -0,0 +1,9 @@
const { defineConfig } = require('cypress');
module.exports = defineConfig({
e2e: {
setupNodeEvents(on, config) {
// implement node event listeners here
},
},
});

29
cypress/e2e/game.cy.js Normal file
View File

@ -0,0 +1,29 @@
const io = require('socket.io-client');
const socket = io.connect('http://localhost:8080', { withCredentials: true });
const uniqName = Date.now().toString();
describe('game', () => {
before(() => {
cy.visit('http://localhost:3000');
cy.get('[placeholder="Server Name"]').type(uniqName);
cy.get('button:contains("Host")').click();
const room = cy.contains(`${uniqName}`).should('exist');
room.closest('tr').find('button:contains("Join")').click();
const e = cy.get('[placeholder="Nickname"]').type('player1');
e.type('{enter}');
setTimeout(() => {
socket.emit('room:rooms');
socket.on('room:rooms', rooms => {
const roomId = JSON.parse(rooms).find(r => r.name === uniqName)._id;
socket.emit('player:login', { roomId: roomId, name: 'player2', password: '' });
});
}, 1000);
});
it('starts game correctly', () => {
socket.emit('player:ready');
cy.get('.PrivateSwitchBase-input').click();
cy.get('[data-testid="animated-overlay"]').should('exist');
socket.emit('');
});
});

29
cypress/e2e/login.cy.js Normal file
View File

@ -0,0 +1,29 @@
describe('login', () => {
it('should change color of input if the server name is invalid', () => {
cy.visit('http://localhost:3000');
cy.get('button:contains("Host")').click();
cy.get('[placeholder="Server Name"]').should('have.css', 'border-color', 'rgb(255, 0, 0)');
});
it('should redirect user to game when filling inputs correctly', () => {
cy.visit('http://localhost:3000');
const uniqName = Date.now().toString();
cy.get('[placeholder="Server Name"]').type(uniqName);
cy.get('.PrivateSwitchBase-input').click();
cy.get('[placeholder="password"]').type('123456');
cy.get('button:contains("Host")').click();
const room = cy
.contains(`${uniqName}`)
.should('exist')
.then($room => {
cy.wrap($room).scrollIntoView();
});
room.closest('tr').find('button:contains("Join")').click();
cy.get('[placeholder="Nickname"]').type('player1');
const e = cy.get('[placeholder="Room password"]').type('123456');
e.type('{enter}');
cy.url().should('include', '/game');
cy.contains('player1').should('exist');
cy.get('canvas').should('exist');
});
});

View File

@ -0,0 +1,5 @@
{
"name": "Using fixtures to represent data",
"email": "hello@cypress.io",
"body": "Fixtures are a great way to mock data for responses to routes"
}

View File

@ -0,0 +1,25 @@
// ***********************************************
// This example commands.js shows you how to
// create various custom commands and overwrite
// existing commands.
//
// For more comprehensive examples of custom
// commands please read more here:
// https://on.cypress.io/custom-commands
// ***********************************************
//
//
// -- This is a parent command --
// Cypress.Commands.add('login', (email, password) => { ... })
//
//
// -- This is a child command --
// Cypress.Commands.add('drag', { prevSubject: 'element'}, (subject, options) => { ... })
//
//
// -- This is a dual command --
// Cypress.Commands.add('dismiss', { prevSubject: 'optional'}, (subject, options) => { ... })
//
//
// -- This will overwrite an existing command --
// Cypress.Commands.overwrite('visit', (originalFn, url, options) => { ... })

20
cypress/support/e2e.js Normal file
View File

@ -0,0 +1,20 @@
// ***********************************************************
// This example support/e2e.js is processed and
// loaded automatically before your test files.
//
// This is a great place to put global configuration and
// behavior that modifies Cypress.
//
// You can change the location of this file or turn off
// automatically serving support files with the
// 'supportFile' configuration option.
//
// You can read more here:
// https://on.cypress.io/configuration
// ***********************************************************
// Import commands.js using ES2015 syntax:
import './commands'
// Alternatively you can use CommonJS syntax:
// require('./commands')

2319
package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

@ -51,6 +51,8 @@
"proxy": "http://localhost:5000", "proxy": "http://localhost:5000",
"devDependencies": { "devDependencies": {
"@testing-library/jest-dom": "^6.1.5", "@testing-library/jest-dom": "^6.1.5",
"@testing-library/react": "^14.1.2" "@testing-library/react": "^14.1.2",
"cypress": "^13.6.1",
"@babel/plugin-proposal-private-property-in-object": "^7.16.7"
} }
} }

View File

@ -11,16 +11,17 @@ const Dice = ({ rolledNumber, nowMoving, playerColor, movingPlayer }) => {
}; };
const isCurrentPlayer = movingPlayer === playerColor; const isCurrentPlayer = movingPlayer === playerColor;
const hasRolledNumber = rolledNumber !== null; const hasRolledNumber = rolledNumber !== null && rolledNumber !== undefined;
return ( return (
<div className={styles.container}> <div className={styles.container}>
{isCurrentPlayer && {isCurrentPlayer ? (
(hasRolledNumber ? ( hasRolledNumber ? (
<img src={images[rolledNumber - 1]} alt={rolledNumber} /> <img src={images[rolledNumber - 1]} alt={rolledNumber} />
) : ( ) : nowMoving ? (
nowMoving && <img src={images[6]} alt='roll' onClick={handleClick} /> <img src={images[6]} className='roll' alt='roll' onClick={handleClick} />
))} ) : null
) : null}
</div> </div>
); );
}; };