added cypress e2e tests
This commit is contained in:
parent
97513eac2d
commit
ce9e93d68f
40
README.md
40
README.md
@ -1,55 +1,61 @@
|
|||||||
# <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) <<
|
## Table of content
|
||||||
|
|
||||||
## Table of content
|
- [About](#about)
|
||||||
|
|
||||||
- [About](#about)
|
- [Architecture](#architecture)
|
||||||
|
|
||||||
- [Architecture](#architecture)
|
- [Key Features and Challenges](#key-features-and-challenges)
|
||||||
|
|
||||||
- [Key Features and Challenges](#key-features-and-challenges)
|
- [Tech Stack](#tech-stack)
|
||||||
|
|
||||||
- [Tech Stack](#tech-stack)
|
- [Installation](#installation)
|
||||||
|
|
||||||
- [Installation](#installation)
|
- [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
|
||||||
|
|
||||||

|

|
||||||
## Tech Stack
|
|
||||||
|
## Tech Stack
|
||||||
|
|
||||||
Frontend:
|
Frontend:
|
||||||
  
|
  
|
||||||
  
|
  
|
||||||
Backend:
|
Backend:
|
||||||
   
|
   
|
||||||
Tests:
|
Tests:
|
||||||
 
|
  
|
||||||
Tools:
|
Tools:
|
||||||
    
|
    
|
||||||
|
|
||||||
## Key Features and Challenges
|
## Key Features and Challenges
|
||||||
|
|
||||||
- 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:
|
||||||
|
|
||||||
@ -61,6 +67,6 @@ npm i
|
|||||||
node server.js
|
node server.js
|
||||||
```
|
```
|
||||||
|
|
||||||
## Screenshots
|
## Screenshots
|
||||||
|
|
||||||

|

|
||||||
@ -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…');
|
||||||
|
|||||||
@ -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());
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
@ -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
9
cypress.config.js
Normal 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
29
cypress/e2e/game.cy.js
Normal 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
29
cypress/e2e/login.cy.js
Normal 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');
|
||||||
|
});
|
||||||
|
});
|
||||||
5
cypress/fixtures/example.json
Normal file
5
cypress/fixtures/example.json
Normal 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"
|
||||||
|
}
|
||||||
25
cypress/support/commands.js
Normal file
25
cypress/support/commands.js
Normal 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
20
cypress/support/e2e.js
Normal 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
2319
package-lock.json
generated
File diff suppressed because it is too large
Load Diff
@ -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"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -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>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user