added tests and css modules to LoginPage components

This commit is contained in:
Wenszel 2023-12-13 16:31:52 +01:00
parent caf6bc7d91
commit a33ebeccdd
27 changed files with 614 additions and 298 deletions

View File

@ -16,13 +16,13 @@ module.exports = socket => {
}; };
const handleGetAllRooms = async () => { const handleGetAllRooms = async () => {
let rooms = await getRooms(); const rooms = await getRooms();
sendToOnePlayerRooms(socket.id, rooms); sendToOnePlayerRooms(socket.id, rooms);
}; };
const handleCreateRoom = async data => { const handleCreateRoom = async data => {
createNewRoom(data); createNewRoom(data);
socket.to(socket.id).emit('room:created'); sendToOnePlayerRooms(socket.id, await getRooms());
}; };
socket.on('room:data', handleGetData); socket.on('room:data', handleGetData);

View File

@ -0,0 +1,14 @@
import ReactLoading from 'react-loading';
const withLoading = Component => {
return function WithLoading({ isLoading, ...props }) {
if (!isLoading) {
return <Component {...props} />;
}
return (
<ReactLoading type='spinningBubbles' color='white' height={50} width={50} />
);
};
};
export default withLoading;

View File

@ -1,35 +0,0 @@
.refresh {
display: flex;
margin-left: auto;
justify-content: center;
align-items: center;
width: 40px;
height: 100%;
border: 1px solid white;
}
.refresh > img {
width: 20px;
height: 20px;
cursor: pointer;
}
form {
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
width: 100%;
}
.private-container {
margin-left: 10px;
display: flex;
flex-direction: row;
align-items: center;
width: 100%;
}
input:disabled {
background-color: black;
color: #999;
border: 1px solid #ddd;
}

View File

@ -1,56 +1,50 @@
import React, { useState, useContext, useEffect } from 'react'; import React, { useState, useContext } from 'react';
import './AddServer.css';
import Switch from '@mui/material/Switch'; import Switch from '@mui/material/Switch';
import { SocketContext } from '../../../App'; import { SocketContext } from '../../../App';
import WindowLayout from '../WindowLayout/WindowLayout';
import useInput from '../../../hooks/useInput';
import styles from './AddServer.module.css';
const AddServer = () => { const AddServer = () => {
const socket = useContext(SocketContext); const socket = useContext(SocketContext);
const [isPrivate, setIsPrivate] = useState(false); const [isPrivate, setIsPrivate] = useState(false);
const [serverName, setServerName] = useState(''); const [isIncorrect, setIsIncorrect] = useState(false);
const [password, setPassword] = useState(''); const serverName = useInput('');
const password = useInput('');
useEffect(() => {
socket.on('room:created', () => {
socket.emit('room:rooms');
});
}, [socket]);
const handleButtonClick = e => { const handleButtonClick = e => {
e.preventDefault(); e.preventDefault();
if (!serverName.value) setIsIncorrect(true);
else
socket.emit('room:create', { socket.emit('room:create', {
name: serverName, name: serverName.value,
password: password.value,
private: isPrivate, private: isPrivate,
password: password,
}); });
}; };
return ( return (
<div className='lp-container'> <WindowLayout
<div className='title-container'> title='Host A Server'
<h1>Host A Server</h1> content={
</div> <form className={styles.formContainer}>
<div className='content-container'>
<form>
<input <input
type='text' type='text'
value={serverName}
onChange={e => setServerName(e.target.value)}
placeholder='Server Name' placeholder='Server Name'
{...serverName}
style={{
border: isIncorrect ? '1px solid red' : '1px solid white',
}}
/> />
<div className='private-container'> <div className={styles.privateContainer}>
<p>Private</p> <label>Private</label>
<Switch checked={isPrivate} color='primary' onChange={() => setIsPrivate(!isPrivate)} /> <Switch checked={isPrivate} color='primary' onChange={() => setIsPrivate(!isPrivate)} />
</div> </div>
<input <input type='text' placeholder='password' disabled={!isPrivate} {...password} />
type='text'
value={password}
onChange={e => setPassword(e.target.value)}
placeholder='password'
disabled={!isPrivate}
/>
<button onClick={handleButtonClick}>Host</button> <button onClick={handleButtonClick}>Host</button>
</form> </form>
</div> }
</div> />
); );
}; };

View File

@ -0,0 +1,15 @@
.formContainer {
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
width: 100%;
}
.privateContainer {
margin-left: 10px;
display: flex;
flex-direction: row;
align-items: center;
width: 100%;
}

View File

@ -0,0 +1,82 @@
import React from 'react';
import { render, screen, fireEvent } from '@testing-library/react';
import '@testing-library/jest-dom';
import { SocketContext } from '../../../App';
import AddServer from './AddServer';
const mockSocket = {
emit: jest.fn(),
};
describe('AddServer component', () => {
it('should renders without crashing', () => {
render(
<SocketContext.Provider value={mockSocket}>
<AddServer />
</SocketContext.Provider>
);
expect(screen.getByText('Host A Server')).toBeInTheDocument();
});
it('should handles form submission with valid data when private', () => {
render(
<SocketContext.Provider value={mockSocket}>
<AddServer />
</SocketContext.Provider>
);
const serverNameInput = screen.getByPlaceholderText('Server Name');
fireEvent.change(serverNameInput, { target: { value: 'Test Server' } });
const privateSwitch = screen.getByRole('checkbox');
fireEvent.click(privateSwitch);
const passwordInput = screen.getByPlaceholderText('password');
fireEvent.change(passwordInput, { target: { value: 'TestPassword' } });
const hostButton = screen.getByText('Host');
fireEvent.click(hostButton);
expect(mockSocket.emit).toHaveBeenCalledWith('room:create', {
name: 'Test Server',
password: 'TestPassword',
private: true,
});
});
it('should handles form submission with valid data when not private', () => {
render(
<SocketContext.Provider value={mockSocket}>
<AddServer />
</SocketContext.Provider>
);
const serverNameInput = screen.getByPlaceholderText('Server Name');
fireEvent.change(serverNameInput, { target: { value: 'Test Server' } });
const hostButton = screen.getByText('Host');
fireEvent.click(hostButton);
expect(mockSocket.emit).toHaveBeenCalledWith('room:create', {
name: 'Test Server',
password: '',
private: false,
});
});
it('should handles form submission with missing server name', () => {
render(
<SocketContext.Provider value={mockSocket}>
<AddServer />
</SocketContext.Provider>
);
const hostButton = screen.getByText('Host');
fireEvent.click(hostButton);
expect(mockSocket.emit).not.toHaveBeenCalled();
const serverNameInput = screen.getByPlaceholderText('Server Name');
expect(serverNameInput).toHaveStyle('border: 1px solid red');
});
});

View File

@ -0,0 +1,66 @@
import React, { useContext, useEffect, useState } from 'react';
import { SocketContext } from '../../../App';
import refresh from '../../../images/login-page/refresh.png';
import NameInput from '../NameInput/NameInput';
import Overlay from '../../Overlay/Overlay';
import WindowLayout from '../WindowLayout/WindowLayout';
import ServersTable from './ServersTable/ServersTable';
import withLoading from '../../HOC/withLoading';
import useSocketData from '../../../hooks/useSocketData';
import styles from './JoinServer.module.css';
const JoinServer = () => {
const socket = useContext(SocketContext);
const [rooms, setRooms] = useSocketData('room:rooms');
const [joining, setJoining] = useState(false);
const [clickedRoom, setClickedRoom] = useState(null);
const [isLoading, setIsLoading] = useState(true);
useEffect(() => {
socket.emit('room:rooms');
socket.on('room:rooms', () => {
setIsLoading(false);
});
}, [socket]);
const getRooms = () => {
setRooms([]);
socket.emit('room:rooms');
};
const handleJoinClick = room => {
setClickedRoom(room);
setJoining(true);
};
const ServersTableWithLoading = withLoading(ServersTable);
return (
<>
<WindowLayout
title='Join A Server'
titleComponent={
<div className={styles.refresh}>
<img src={refresh} alt='refresh' onClick={getRooms} />
</div>
}
content={
<div className={styles.serversTableContainer}>
<ServersTableWithLoading
isLoading={isLoading}
rooms={rooms}
handleJoinClick={handleJoinClick}
/>
</div>
}
/>
{joining ? (
<Overlay handleOverlayClose={() => setJoining(false)}>
<NameInput roomId={clickedRoom._id} isRoomPrivate={clickedRoom.private} />
</Overlay>
) : null}
</>
);
};
export default JoinServer;

View File

@ -0,0 +1,20 @@
.serversTableContainer {
display: flex;
height: 500px;
overflow: scroll;
width: 100%;
}
.refresh {
display: flex;
margin-left: auto;
justify-content: center;
align-items: center;
width: 40px;
height: 100%;
border: 1px solid white;
}
.refresh > img {
width: 20px;
height: 20px;
cursor: pointer;
}

View File

@ -0,0 +1,33 @@
import lock from '../../../../images/login-page/lock.png';
import styles from './ServersTable.module.css';
const ServerListTable = ({ rooms, handleJoinClick }) => {
return (
<table className={styles.rooms}>
<thead>
<tr>
<th className={styles.firstColumn}></th>
<th>Server</th>
<th>#/#</th>
<th>Status</th>
<th className={styles.lastColumn}></th>
</tr>
</thead>
<tbody>
{rooms.map((room, index) => (
<tr key={index}>
<td>{room.private ? <img src={lock} alt='private' /> : null}</td>
<td className={styles.roomName}>{room.name}</td>
<td>{`${room.players.length}/4`}</td>
<td>{room.isStarted ? 'started' : 'waiting'}</td>
<td className={styles.lastColumn}>
<button onClick={() => handleJoinClick(room)}>Join</button>
</td>
</tr>
))}
</tbody>
</table>
);
};
export default ServerListTable;

View File

@ -0,0 +1,40 @@
.roomName {
max-width: 150px;
overflow: hidden;
text-align: left !important;
}
.rooms > thead > tr :nth-child(2) {
text-align: left;
}
.rooms > tbody > tr > td > img {
margin-right: 5px;
width: 20px;
height: 20px;
}
.rooms > th {
padding: 8px;
text-align: center;
height: 50px;
}
.rooms > tbody > tr > td {
padding: 4px;
text-align: center;
height: 50px;
}
.rooms > tbody > tr > td {
max-height: 50px;
height: 10px;
}
.rooms {
border-collapse: collapse;
width: 100%;
}
.lastColumn {
width: 70px;
}
.firstColumn {
width: 40px;
}

View File

@ -0,0 +1,34 @@
import React from 'react';
import { render, screen, fireEvent } from '@testing-library/react';
import '@testing-library/jest-dom';
import ServersTable from './ServersTable';
const mockRooms = [
{ _id: '1', name: 'Room 1', private: false, players: [], isStarted: false },
{ _id: '2', name: 'Room 2', private: true, players: [], isStarted: true },
];
describe('ServersTable component', () => {
it('should renders without crashing', () => {
render(<ServersTable rooms={mockRooms} handleJoinClick={() => {}} />);
expect(screen.getByText('Server')).toBeInTheDocument();
expect(screen.getByText('#/#')).toBeInTheDocument();
expect(screen.getByText('Status')).toBeInTheDocument();
});
it('should renders the list of rooms', () => {
render(<ServersTable rooms={mockRooms} handleJoinClick={() => {}} />);
expect(screen.getByText('Room 1')).toBeInTheDocument();
expect(screen.getByText('Room 2')).toBeInTheDocument();
});
it('should handles join click for each room', () => {
const handleJoinClick = jest.fn();
render(<ServersTable rooms={mockRooms} handleJoinClick={handleJoinClick} />);
const joinButtons = screen.getAllByText('Join');
fireEvent.click(joinButtons[0]);
expect(handleJoinClick).toHaveBeenCalledWith(mockRooms[0]);
});
});

View File

@ -1,14 +1,13 @@
import './LoginPage.css';
import AddServer from './AddServer/AddServer'; import AddServer from './AddServer/AddServer';
import ServerList from './ServerList/ServerList'; import JoinServer from './JoinServer/JoinServer';
import styles from './LoginPage.module.css';
const LoginPage = () => { const LoginPage = () => {
return ( return (
<> <div className={styles.container}>
<div className='login-page-container'> <JoinServer />
<ServerList />
<AddServer /> <AddServer />
</div> </div>
</>
); );
}; };

View File

@ -0,0 +1,8 @@
.container {
display: flex;
flex-direction: row;
justify-content: center;
align-items: flex-start;
height: 50%;
width: 100%;
}

View File

@ -0,0 +1,19 @@
import React from 'react';
import { render, screen } from '@testing-library/react';
import '@testing-library/jest-dom';
import LoginPage from './LoginPage';
jest.mock('./JoinServer/JoinServer', () => () => <div data-testid="join-server" />);
jest.mock('./AddServer/AddServer', () => () => <div data-testid="add-server" />);
describe('LoginPage component', () => {
it('should renders JoinServer component ', () => {
render(<LoginPage />);
expect(screen.getByTestId('join-server')).toBeInTheDocument();
});
it('should renders AddServer component', () => {
render(<LoginPage />);
expect(screen.getByTestId('add-server')).toBeInTheDocument();
});
});

View File

@ -1,59 +0,0 @@
.name-input-container {
display: flex;
flex-direction: column;
padding: 10px 20px 60px 20px;
width: 300px;
background: radial-gradient(circle, rgba(0, 138, 255, 1) 5%, rgba(9, 9, 121, 1) 81%);
border: 1px solid white;
border-radius: 8px;
margin: 20px;
z-index: 2;
}
.name-input-container > button {
margin-top: 5px;
text-align: center;
width: 100px;
align-self: center;
}
.name-input-container > input {
margin-top: 10px;
}
input,
button {
padding: 0;
border: none;
outline: none;
box-sizing: border-box;
}
input {
width: 100%;
padding: 12px;
font-size: 16px;
border-radius: 8px;
color: white;
border: 1px solid #ccc;
background-color: rgba(0, 0, 0, 0.2);
transition: border-color 0.3s ease-in-out, background-color 0.3s ease-in-out;
}
input:focus {
color: black;
border-color: #4a90e2;
background-color: #fff;
}
button {
padding: 12px 20px;
font-size: 16px;
border-radius: 8px;
border: none;
color: #fff;
background-color: rgba(0, 0, 0, 0.4);
cursor: pointer;
transition: background-color 0.3s ease-in-out;
}
button:hover {
background-color: rgba(0, 0, 0, 1);
}

View File

@ -1,8 +1,8 @@
import React, { useState, useContext, useEffect, useCallback } from 'react'; import React, { useState, useContext, useEffect, useCallback } from 'react';
import { SocketContext } from '../../../App'; import { SocketContext } from '../../../App';
import useInput from '../../../hooks/useInput'; import useInput from '../../../hooks/useInput';
import './NameInput.css';
import useKeyPress from '../../../hooks/useKeyPress'; import useKeyPress from '../../../hooks/useKeyPress';
import styles from './NameInput.module.css';
const NameInput = ({ isRoomPrivate, roomId }) => { const NameInput = ({ isRoomPrivate, roomId }) => {
const socket = useContext(SocketContext); const socket = useContext(SocketContext);
@ -10,11 +10,12 @@ const NameInput = ({ isRoomPrivate, roomId }) => {
const password = useInput(''); const password = useInput('');
const [isPasswordWrong, setIsPasswordWrong] = useState(false); const [isPasswordWrong, setIsPasswordWrong] = useState(false);
const handleButtonClick = useCallback(() => { const handleButtonClick = () => {
socket.emit('player:login', { name: nickname.value, password: password.value, roomId: roomId }); socket.emit('player:login', { name: nickname.value, password: password.value, roomId: roomId });
}, [socket, nickname.value, password.value, roomId]); };
useKeyPress('Enter', handleButtonClick); useKeyPress('Enter', handleButtonClick);
useEffect(() => { useEffect(() => {
socket.on('error:wrongPassword', () => { socket.on('error:wrongPassword', () => {
setIsPasswordWrong(true); setIsPasswordWrong(true);
@ -22,13 +23,13 @@ const NameInput = ({ isRoomPrivate, roomId }) => {
}, [socket]); }, [socket]);
return ( return (
<div className='name-input-container' style={{ height: isRoomPrivate ? '100px' : '50px' }}> <div className={styles.container} style={{ height: isRoomPrivate ? '100px' : '50px' }}>
<input placeholder='Nickname' type='text' onChange={nickname.onChange} /> <input placeholder='Nickname' type='text' {...nickname} />
{isRoomPrivate ? ( {isRoomPrivate ? (
<input <input
placeholder='Room password' placeholder='Room password'
type='text' type='text'
onChange={password.onChange} {...password}
style={{ backgroundColor: isPasswordWrong ? 'red' : null }} style={{ backgroundColor: isPasswordWrong ? 'red' : null }}
/> />
) : null} ) : null}

View File

@ -0,0 +1,20 @@
.container {
display: flex;
flex-direction: column;
padding: 10px 20px 60px 20px;
width: 300px;
background: radial-gradient(circle, rgba(0, 138, 255, 1) 5%, rgba(9, 9, 121, 1) 81%);
border: 1px solid white;
border-radius: 8px;
margin: 20px;
z-index: 2;
}
.container > button {
margin-top: 5px;
text-align: center;
width: 100px;
align-self: center;
}
.container > input {
margin-top: 10px;
}

View File

@ -0,0 +1,89 @@
import React from 'react';
import { render, screen, fireEvent } from '@testing-library/react';
import '@testing-library/jest-dom';
import NameInput from './NameInput';
import { SocketContext } from '../../../App';
const mockSocket = {
on: jest.fn(),
emit: jest.fn(),
};
describe('NameInput component', () => {
it('should renders password field when room is private', () => {
render(
<SocketContext.Provider value={mockSocket}>
<NameInput isRoomPrivate={true} />
</SocketContext.Provider>
);
expect(screen.getByPlaceholderText('Room password')).toBeInTheDocument();
});
it('should not renders password field when room is not private', () => {
render(
<SocketContext.Provider value={mockSocket}>
<NameInput isRoomPrivate={false} />
</SocketContext.Provider>
);
expect(screen.queryByPlaceholderText('Room password')).not.toBeInTheDocument();
});
it('should handles input change', () => {
render(
<SocketContext.Provider value={mockSocket}>
<NameInput isRoomPrivate={false} />
</SocketContext.Provider>
);
const nicknameInput = screen.getByPlaceholderText('Nickname');
fireEvent.change(nicknameInput, { target: { value: 'TestName' } });
expect(nicknameInput.value).toBe('TestName');
});
it('should handles password change', () => {
render(
<SocketContext.Provider value={mockSocket}>
<NameInput isRoomPrivate={true} />
</SocketContext.Provider>
);
const passwordInput = screen.getByPlaceholderText('Room password');
fireEvent.change(passwordInput, { target: { value: 'TestPassword' } });
expect(passwordInput.value).toBe('TestPassword');
});
it('should handles button click', () => {
render(
<SocketContext.Provider value={mockSocket}>
<NameInput isRoomPrivate={true} roomId={123} />
</SocketContext.Provider>
);
const nicknameInput = screen.getByPlaceholderText('Nickname');
fireEvent.change(nicknameInput, { target: { value: 'TestName' } });
const passwordInput = screen.getByPlaceholderText('Room password');
fireEvent.change(passwordInput, { target: { value: 'TestPassword' } });
const button = screen.getByText('JOIN');
fireEvent.click(button);
expect(mockSocket.emit).toHaveBeenCalledWith('player:login', {
name: 'TestName',
password: 'TestPassword',
roomId: 123,
});
});
it('should handles Enter key press', () => {
render(
<SocketContext.Provider value={mockSocket}>
<NameInput isRoomPrivate={true} roomId={123} />
</SocketContext.Provider>
);
const nicknameInput = screen.getByPlaceholderText('Nickname');
fireEvent.change(nicknameInput, { target: { value: 'TestName' } });
const passwordInput = screen.getByPlaceholderText('Room password');
fireEvent.change(passwordInput, { target: { value: 'TestPassword' } });
fireEvent.keyDown(nicknameInput, { key: 'Enter' });
expect(mockSocket.emit).toHaveBeenCalledWith('player:login', {
name: 'TestName',
password: 'TestPassword',
roomId: 123,
});
});
});

View File

@ -1,51 +0,0 @@
th {
text-align: left;
}
img {
margin-right: 5px;
width: 20px;
height: 20px;
}
th,
td {
padding: 8px;
text-align: left;
height: 50px;
}
tr {
max-height: 50px;
}
table {
border-collapse: collapse;
width: 100%;
}
.server-container {
display: flex;
height: 500px;
overflow: scroll;
}
.room-name {
max-width: 150px;
overflow: hidden;
}
/* Firefox */
* {
scrollbar-width: auto;
scrollbar-color: #ffffff rgba(0, 0, 0, 0.1);
}
/* Chrome, Edge, and Safari */
*::-webkit-scrollbar {
background: rgba(0, 0, 0, 0);
width: 10px;
}
*::-webkit-scrollbar-track {
background: rgba(0, 0, 0, 0);
}
*::-webkit-scrollbar-thumb {
background-color: #ffffff;
border-radius: 10px;
}

View File

@ -1,82 +0,0 @@
import React, { useContext, useEffect, useState } from 'react';
import { SocketContext } from '../../../App';
import lock from '../../../images/login-page/lock.png';
import refresh from '../../../images/login-page/refresh.png';
import ReactLoading from 'react-loading';
import './ServerList.css';
import NameInput from '../NameInput/NameInput';
import Overlay from '../../Overlay/Overlay';
const ServerList = () => {
const socket = useContext(SocketContext);
const [rooms, setRooms] = useState([]);
const [joining, setJoining] = useState(false);
const [clickedRoom, setClickedRoom] = useState(null);
useEffect(() => {
socket.emit('room:rooms');
socket.on('room:rooms', data => {
data = JSON.parse(data);
setRooms(data);
});
}, [socket]);
const getRooms = () => {
setRooms(null);
socket.emit('room:rooms');
};
const handleJoinClick = room => {
setClickedRoom(room);
setJoining(true);
};
return (
<div className='lp-container'>
<div className='title-container'>
<h1>Server List</h1>
<div className='refresh'>
<img src={refresh} alt='refresh' onClick={getRooms}></img>
</div>
</div>
<div className='server-container content-container'>
{rooms ? (
<table className='rooms'>
<thead>
<tr>
<th></th>
<th>Server</th>
<th>#/#</th>
<th>Status</th>
<th></th>
</tr>
</thead>
<tbody>
{rooms.map((room, index) => (
<tr key={index}>
<td>{room.private ? <img src={lock} alt='private' /> : null}</td>
<td className='room-name'>{room.name}</td>
<td>{`${room.players.length}/4`}</td>
<td>{room.isStarted ? 'started' : 'waiting'}</td>
<td>
<button onClick={() => handleJoinClick(room)}>Join</button>
</td>
</tr>
))}
</tbody>
</table>
) : (
<div style={{ alignSelf: 'center' }}>
<ReactLoading type='spinningBubbles' color='white' height={50} width={50} />
</div>
)}
</div>
{joining ? (
<Overlay handleOverlayClose={() => setJoining(false)}>
<NameInput roomId={clickedRoom._id} isRoomPrivate={clickedRoom.private} />
</Overlay>
) : null}
</div>
);
};
export default ServerList;

View File

@ -0,0 +1,15 @@
import styles from './WindowLayout.module.css';
const WindowLayout = ({ title, titleComponent, content }) => {
return (
<div className={styles.container}>
<div className={styles.title}>
<h1>{title}</h1>
{titleComponent}
</div>
<div className={styles.content}>{content}</div>
</div>
);
};
export default WindowLayout;

View File

@ -1,25 +1,14 @@
.login-page-container { .container {
display: flex;
flex-direction: row;
justify-content: center;
align-items: flex-start;
height: 50%;
width: 100%;
}
.lp-container {
margin: 50px; margin: 50px;
display: flex; display: flex;
flex-direction: column; flex-direction: column;
align-items: center; align-items: center;
justify-content: center; justify-content: center;
width: 500px; width: 500px;
padding: 20px;
color: white; color: white;
} }
.title-container { .title {
display: flex; display: flex;
flex-direction: row; flex-direction: row;
justify-content: center; justify-content: center;
@ -34,17 +23,21 @@
text-align: center; text-align: center;
} }
.title-container > h1 { .title > h1 {
width: 100%; width: 100%;
margin: 0; margin: 0;
padding: 0; padding: 0;
} }
.content-container { .content {
display: flex; display: flex;
flex-direction: column; flex-direction: column;
justify-content: center;
align-items: center;
width: 100%; width: 100%;
padding: 10px; padding-left: 5px;
padding-right: 5px;
padding-top: 10px;
background-color: rgba(0, 0, 0, 0.5); background-color: rgba(0, 0, 0, 0.5);
box-shadow: 0 0 10px rgba(0, 0, 0, 0.5); box-shadow: 0 0 10px rgba(0, 0, 0, 0.5);
border-left: 1px solid black; border-left: 1px solid black;

View File

@ -0,0 +1,28 @@
import React from 'react';
import { render, screen } from '@testing-library/react';
import '@testing-library/jest-dom';
import WindowLayout from './WindowLayout';
jest.mock('./WindowLayout', () => ({ title, titleComponent, content }) => (
<div data-testid='mocked-window-layout'>
<div data-testid='mocked-title'>{title}</div>
<div data-testid='mocked-title-component'>{titleComponent}</div>
<div data-testid='mocked-content'>{content}</div>
</div>
));
describe('WindowLayout component', () => {
it('should render without crashing', () => {
render(
<WindowLayout
title='Test Title'
titleComponent={<div>Test Title Component</div>}
content={<div>Test Content</div>}
/>
);
expect(screen.getByTestId('mocked-window-layout')).toBeInTheDocument();
expect(screen.getByTestId('mocked-title')).toHaveTextContent('Test Title');
expect(screen.getByTestId('mocked-title-component')).toHaveTextContent('Test Title Component');
expect(screen.getByTestId('mocked-content')).toHaveTextContent('Test Content');
});
});

View File

@ -1,11 +1,11 @@
import { useState } from 'react'; import { useState } from 'react';
export default function useInput({ initialValue }) { export default function useInput(initialValue = '') {
const [value, setValue] = useState(initialValue); const [value, setValue] = useState(initialValue);
const handleChange = e => { const handleChange = e => {
setValue(e.target.value); setValue(e.target.value);
}; };
return { return {
value, value: value,
onChange: handleChange, onChange: handleChange,
}; };
} }

View File

@ -11,5 +11,5 @@ export default function useKeyPress(targetKey, callback) {
return () => { return () => {
window.removeEventListener('keydown', keyPressHandler); window.removeEventListener('keydown', keyPressHandler);
}; };
}, []); }, [keyPressHandler]);
} }

View File

@ -4,8 +4,14 @@ import { SocketContext } from '../App';
const useSocketData = port => { const useSocketData = port => {
const socket = useContext(SocketContext); const socket = useContext(SocketContext);
const [data, setData] = useState(null); const [data, setData] = useState(null);
socket.on(port, data => { socket.on(port, res => {
setData(data); let parsedData;
try {
parsedData = JSON.parse(res);
} catch (error) {
parsedData = res;
}
setData(parsedData);
}); });
return [data, setData]; return [data, setData];
}; };

View File

@ -67,3 +67,70 @@ canvas {
grid-column: 1 / span 2; grid-column: 1 / span 2;
grid-row: 2 / span 2; grid-row: 2 / span 2;
} }
input,
button {
padding: 0;
border: none;
outline: none;
box-sizing: border-box;
}
input {
width: 100%;
padding: 12px;
font-size: 16px;
border-radius: 8px;
color: white;
border: 1px solid #ccc;
background-color: rgba(0, 0, 0, 0.2);
transition: border-color 0.3s ease-in-out, background-color 0.3s ease-in-out;
}
input:disabled {
background-color: black;
color: #999;
border: 1px solid #ddd;
}
input:focus {
color: black;
border-color: #4a90e2;
background-color: #fff;
}
button {
padding: 12px 20px;
font-size: 16px;
border-radius: 8px;
border: none;
color: #fff;
background-color: rgba(0, 0, 0, 0.4);
cursor: pointer;
transition: background-color 0.3s ease-in-out;
}
button:hover {
background-color: rgba(0, 0, 0, 1);
}
/* Firefox */
* {
scrollbar-width: auto;
scrollbar-color: #ffffff rgba(0, 0, 0, 0.1);
}
/* Chrome, Edge, and Safari */
*::-webkit-scrollbar {
background: rgba(0, 0, 0, 0);
width: 10px;
}
*::-webkit-scrollbar-track {
background: rgba(0, 0, 0, 0);
}
*::-webkit-scrollbar-thumb {
background-color: #ffffff;
border-radius: 10px;
}