end to end tests for the game page

This commit is contained in:
AlecM33
2022-09-02 15:53:15 -04:00
parent a891251ac7
commit b05afb85ae
32 changed files with 1276 additions and 1585 deletions

View File

@@ -31,17 +31,6 @@ export const globals = {
MESSAGES: { MESSAGES: {
ENTER_NAME: 'Client must enter name.' ENTER_NAME: 'Client must enter name.'
}, },
EVENTS: {
PLAYER_JOINED: 'playerJoined',
SYNC_GAME_STATE: 'syncGameState',
START_TIMER: 'startTimer',
KILL_PLAYER: 'killPlayer',
REVEAL_PLAYER: 'revealPlayer',
CHANGE_NAME: 'changeName',
START_GAME: 'startGame',
PLAYER_LEFT: 'playerLeft',
NEW_SPECTATOR: 'newSpectator'
},
SOCKET_EVENTS: { SOCKET_EVENTS: {
IN_GAME_MESSAGE: 'inGameMessage' IN_GAME_MESSAGE: 'inGameMessage'
}, },
@@ -55,7 +44,12 @@ export const globals = {
REVEAL_PLAYER: 'revealPlayer', REVEAL_PLAYER: 'revealPlayer',
TRANSFER_MODERATOR: 'transferModerator', TRANSFER_MODERATOR: 'transferModerator',
CHANGE_NAME: 'changeName', CHANGE_NAME: 'changeName',
END_GAME: 'endGame' END_GAME: 'endGame',
PLAYER_JOINED: 'playerJoined',
SYNC_GAME_STATE: 'syncGameState',
START_TIMER: 'startTimer',
PLAYER_LEFT: 'playerLeft',
NEW_SPECTATOR: 'newSpectator'
}, },
USER_TYPES: { USER_TYPES: {
MODERATOR: 'moderator', MODERATOR: 'moderator',

View File

@@ -156,7 +156,7 @@ export const HTMLFragments = {
</div> </div>
<div class='player-action-buttons'> <div class='player-action-buttons'>
<button title='Kill' class='moderator-player-button kill-player-button'>\uD83D\uDC80</button> <button title='Kill' class='moderator-player-button kill-player-button'>\uD83D\uDC80</button>
<button title='Reveal' class='moderator-player-button reveal-role-button'><img alt='reveal' src='../images/eye.svg'/></button> <button title='Reveal' class='moderator-player-button reveal-role-button'><img alt='reveal' src='../../images/eye.svg'/></button>
</div>`, </div>`,
GAME_PLAYER: GAME_PLAYER:
`<div> `<div>

View File

@@ -38,7 +38,7 @@ function flipHamburger () {
function getNavbarLinks (page = null, device) { function getNavbarLinks (page = null, device) {
const linkClass = device === 'mobile' ? 'mobile-link' : 'desktop-link'; const linkClass = device === 'mobile' ? 'mobile-link' : 'desktop-link';
return '<a href="/" class="logo ' + linkClass + '">' + return '<a href="/" class="logo ' + linkClass + '">' +
'<img alt="logo" src="../images/Werewolf_Small.png"/>' + '<img alt="logo" src="../../images/Werewolf_Small.png"/>' +
'</a>' + '</a>' +
'<a class="' + linkClass + '" href="/">Home</a>' + '<a class="' + linkClass + '" href="/">Home</a>' +
'<a class="' + linkClass + '" href="/create">Create</a>' + '<a class="' + linkClass + '" href="/create">Create</a>' +

View File

@@ -1,7 +1,7 @@
import { globals } from '../config/globals.js'; import { globals } from '../../config/globals.js';
import { HTMLFragments } from './HTMLFragments.js'; import { HTMLFragments } from '../front_end_components/HTMLFragments.js';
import { toast } from './Toast.js'; import { toast } from '../front_end_components/Toast.js';
import { ModalManager } from './ModalManager.js'; import { ModalManager } from '../front_end_components/ModalManager.js';
export class DeckStateManager { export class DeckStateManager {
constructor () { constructor () {

View File

@@ -1,10 +1,10 @@
import { Game } from '../model/Game.js'; import { Game } from '../../model/Game.js';
import { cancelCurrentToast, toast } from './Toast.js'; import { cancelCurrentToast, toast } from '../front_end_components/Toast.js';
import { ModalManager } from './ModalManager.js'; import { ModalManager } from '../front_end_components/ModalManager.js';
import { XHRUtility } from './XHRUtility.js'; import { XHRUtility } from '../utility/XHRUtility.js';
import { globals } from '../config/globals.js'; import { globals } from '../../config/globals.js';
import { HTMLFragments } from './HTMLFragments.js'; import { HTMLFragments } from '../front_end_components/HTMLFragments.js';
import { UserUtility } from './UserUtility.js'; import { UserUtility } from '../utility/UserUtility.js';
import { RoleBox } from './RoleBox.js'; import { RoleBox } from './RoleBox.js';
export class GameCreationStepManager { export class GameCreationStepManager {
@@ -457,7 +457,7 @@ function showButtons (back, forward, forwardHandler, backHandler, builtGame = nu
document.querySelector('#create-game')?.remove(); document.querySelector('#create-game')?.remove();
if (back) { if (back) {
const backButton = document.createElement('button'); const backButton = document.createElement('button');
backButton.innerHTML = '<img alt="back" src="../images/caret-back.svg"/>'; backButton.innerHTML = '<img alt="back" src="../../images/caret-back.svg"/>';
backButton.addEventListener('click', backHandler); backButton.addEventListener('click', backHandler);
backButton.setAttribute('id', 'step-back-button'); backButton.setAttribute('id', 'step-back-button');
backButton.classList.add('cancel'); backButton.classList.add('cancel');
@@ -467,7 +467,7 @@ function showButtons (back, forward, forwardHandler, backHandler, builtGame = nu
if (forward && builtGame === null) { if (forward && builtGame === null) {
const fwdButton = document.createElement('button'); const fwdButton = document.createElement('button');
fwdButton.innerHTML = '<img alt="next" src="../images/caret-forward.svg"/>'; fwdButton.innerHTML = '<img alt="next" src="../../images/caret-forward.svg"/>';
fwdButton.addEventListener('click', forwardHandler); fwdButton.addEventListener('click', forwardHandler);
fwdButton.setAttribute('id', 'step-forward-button'); fwdButton.setAttribute('id', 'step-forward-button');
fwdButton.classList.add('app-button'); fwdButton.classList.add('app-button');

View File

@@ -1,8 +1,8 @@
import { HTMLFragments } from './HTMLFragments.js'; import { HTMLFragments } from '../front_end_components/HTMLFragments.js';
import { globals } from '../config/globals.js'; import { globals } from '../../config/globals.js';
import { defaultRoles } from '../config/defaultRoles.js'; import { defaultRoles } from '../../config/defaultRoles.js';
import { toast } from './Toast.js'; import { toast } from '../front_end_components/Toast.js';
import { ModalManager } from './ModalManager.js'; import { ModalManager } from '../front_end_components/ModalManager.js';
export class RoleBox { export class RoleBox {
constructor (container, deckManager) { constructor (container, deckManager) {

View File

@@ -1,11 +1,11 @@
import { globals } from '../config/globals.js'; import { globals } from '../../config/globals.js';
import { toast } from './Toast.js'; import { toast } from '../front_end_components/Toast.js';
import { HTMLFragments } from './HTMLFragments.js'; import { HTMLFragments } from '../front_end_components/HTMLFragments.js';
import { ModalManager } from './ModalManager.js'; import { ModalManager } from '../front_end_components/ModalManager.js';
import { XHRUtility } from './XHRUtility.js'; import { XHRUtility } from '../utility/XHRUtility.js';
import { UserUtility } from './UserUtility.js'; import { UserUtility } from '../utility/UserUtility.js';
// QRCode module via: https://github.com/soldair/node-qrcode // QRCode module via: https://github.com/soldair/node-qrcode
import { QRCode } from './third_party/qrcode.js'; import { QRCode } from '../third_party/qrcode.js';
export class GameStateRenderer { export class GameStateRenderer {
constructor (stateBucket, socket) { constructor (stateBucket, socket) {

View File

@@ -0,0 +1,8 @@
import { injectNavbar } from '../front_end_components/Navbar.js';
import createTemplate from '../../view_templates/CreateTemplate.js';
export const createHandler = (gameCreationStepManager) => {
injectNavbar();
document.getElementById('game-creation-container').innerHTML = createTemplate;
gameCreationStepManager.renderStep('creation-step-container', 1);
};

View File

@@ -0,0 +1,440 @@
import { injectNavbar } from '../front_end_components/Navbar.js';
import { stateBucket } from '../game_state/StateBucket.js';
import { GameTimerManager } from '../timer/GameTimerManager.js';
import { GameStateRenderer } from '../game_state/GameStateRenderer.js';
import { UserUtility } from '../utility/UserUtility.js';
import { toast } from '../front_end_components/Toast.js';
import { globals } from '../../config/globals.js';
import { HTMLFragments } from '../front_end_components/HTMLFragments.js';
import { ModalManager } from '../front_end_components/ModalManager.js';
export const gameHandler = async (socket, XHRUtility, window, gameDOM) => {
document.body.innerHTML = gameDOM + document.body.innerHTML;
injectNavbar();
const response = await XHRUtility.xhr(
'/api/games/environment',
'GET',
null,
null
).catch((res) => {
toast(res.content, 'error', true);
});
stateBucket.environment = response.content;
const timerWorker = new Worker(new URL('../timer/Timer.js', import.meta.url));
const gameTimerManager = new GameTimerManager(stateBucket, socket);
const gameStateRenderer = new GameStateRenderer(stateBucket, socket);
socket.on('connect', function () {
syncWithGame(
stateBucket,
gameTimerManager,
gameStateRenderer,
timerWorker,
socket,
UserUtility.validateAnonUserSignature(response.content),
window
);
});
socket.on('connect_error', (err) => {
toast(err, 'error', true, false);
});
socket.on('disconnect', () => {
toast('Disconnected. Attempting reconnect...', 'error', true, false);
});
setClientSocketHandlers(stateBucket, gameStateRenderer, socket, timerWorker, gameTimerManager);
};
function syncWithGame (stateBucket, gameTimerManager, gameStateRenderer, timerWorker, socket, cookie, window) {
const splitUrl = window.location.href.split('/game/');
const accessCode = splitUrl[1];
if (/^[a-zA-Z0-9]+$/.test(accessCode) && accessCode.length === globals.ACCESS_CODE_LENGTH) {
socket.emit(globals.SOCKET_EVENTS.IN_GAME_MESSAGE, globals.EVENT_IDS.FETCH_GAME_STATE, accessCode, { personId: cookie }, function (gameState) {
if (gameState === null) {
window.location = '/not-found?reason=' + encodeURIComponent('game-not-found');
} else {
stateBucket.currentGameState = gameState;
document.querySelector('.spinner-container')?.remove();
document.querySelector('.spinner-background')?.remove();
document.getElementById('game-content').innerHTML = HTMLFragments.INITIAL_GAME_DOM;
toast('You are connected.', 'success', true, true, 'short');
processGameState(stateBucket.currentGameState, cookie, socket, gameStateRenderer, gameTimerManager, timerWorker, true, true);
}
});
} else {
window.location = '/not-found?reason=' + encodeURIComponent('invalid-access-code');
}
}
function processGameState (
currentGameState,
userId,
socket,
gameStateRenderer,
gameTimerManager,
timerWorker,
refreshPrompt = true,
animateContainer = false
) {
const containerAnimation = document.getElementById('game-state-container').animate(
[
{ opacity: '0', transform: 'translateY(10px)' },
{ opacity: '1', transform: 'translateY(0px)' }
], {
duration: 500,
easing: 'ease-in-out',
fill: 'both'
});
if (animateContainer) {
containerAnimation.play();
}
displayClientInfo(currentGameState.client.name, currentGameState.client.userType);
if (refreshPrompt) {
removeStartGameFunctionalityIfPresent(gameStateRenderer);
document.querySelector('#end-game-prompt')?.remove();
}
switch (currentGameState.status) {
case globals.STATUS.LOBBY:
document.getElementById('game-state-container').innerHTML = HTMLFragments.LOBBY;
gameStateRenderer.renderLobbyHeader();
gameStateRenderer.renderLobbyPlayers();
if ((
currentGameState.client.userType === globals.USER_TYPES.MODERATOR
|| currentGameState.client.userType === globals.USER_TYPES.TEMPORARY_MODERATOR
)
&& refreshPrompt
) {
displayStartGamePromptForModerators(currentGameState, gameStateRenderer);
}
break;
case globals.STATUS.IN_PROGRESS:
gameStateRenderer.renderGameHeader();
switch (currentGameState.client.userType) {
case globals.USER_TYPES.PLAYER:
document.getElementById('game-state-container').innerHTML = HTMLFragments.PLAYER_GAME_VIEW;
gameStateRenderer.renderPlayerView();
break;
case globals.USER_TYPES.KILLED_PLAYER:
document.getElementById('game-state-container').innerHTML = HTMLFragments.PLAYER_GAME_VIEW;
gameStateRenderer.renderPlayerView(true);
break;
case globals.USER_TYPES.MODERATOR:
document.getElementById('transfer-mod-prompt').innerHTML = HTMLFragments.TRANSFER_MOD_MODAL;
document.getElementById('game-state-container').innerHTML = HTMLFragments.MODERATOR_GAME_VIEW;
gameStateRenderer.renderModeratorView();
break;
case globals.USER_TYPES.TEMPORARY_MODERATOR:
document.getElementById('transfer-mod-prompt').innerHTML = HTMLFragments.TRANSFER_MOD_MODAL;
document.getElementById('game-state-container').innerHTML = HTMLFragments.TEMP_MOD_GAME_VIEW;
gameStateRenderer.renderTempModView();
break;
case globals.USER_TYPES.SPECTATOR:
document.getElementById('game-state-container').innerHTML = HTMLFragments.SPECTATOR_GAME_VIEW;
gameStateRenderer.renderSpectatorView();
break;
default:
break;
}
if (currentGameState.timerParams) {
socket.emit(globals.SOCKET_EVENTS.IN_GAME_MESSAGE, globals.EVENT_IDS.GET_TIME_REMAINING, currentGameState.accessCode);
} else {
document.querySelector('#game-timer')?.remove();
document.querySelector('#timer-container-moderator')?.remove();
document.querySelector('label[for="game-timer"]')?.remove();
}
break;
case globals.STATUS.ENDED: {
const container = document.getElementById('game-state-container');
container.innerHTML = HTMLFragments.END_OF_GAME_VIEW;
gameStateRenderer.renderEndOfGame(currentGameState);
break;
}
default:
break;
}
activateRoleInfoButton(stateBucket.currentGameState.deck);
}
function activateRoleInfoButton (deck) {
deck.sort((a, b) => {
return a.team === globals.ALIGNMENT.GOOD ? -1 : 1;
});
document.getElementById('role-info-button').addEventListener('click', (e) => {
e.preventDefault();
document.getElementById('role-info-prompt').innerHTML = HTMLFragments.ROLE_INFO_MODAL;
const modalContent = document.getElementById('game-role-info-container');
for (const card of deck) {
const roleDiv = document.createElement('div');
const roleNameDiv = document.createElement('div');
roleNameDiv.classList.add('role-info-name');
const roleName = document.createElement('h5');
const roleQuantity = document.createElement('h5');
const roleDescription = document.createElement('p');
roleDescription.innerText = card.description;
roleName.innerText = card.role;
roleQuantity.innerText = card.quantity + 'x';
if (card.team === globals.ALIGNMENT.GOOD) {
roleName.classList.add(globals.ALIGNMENT.GOOD);
} else {
roleName.classList.add(globals.ALIGNMENT.EVIL);
}
roleNameDiv.appendChild(roleQuantity);
roleNameDiv.appendChild(roleName);
roleDiv.appendChild(roleNameDiv);
roleDiv.appendChild(roleDescription);
modalContent.appendChild(roleDiv);
}
ModalManager.displayModal('role-info-modal', 'role-info-modal-background', 'close-role-info-modal-button');
});
}
function displayClientInfo (name, userType) {
document.getElementById('client-name').innerText = name;
document.getElementById('client-user-type').innerText = userType;
document.getElementById('client-user-type').innerText += globals.USER_TYPE_ICONS[userType];
}
function setClientSocketHandlers (stateBucket, gameStateRenderer, socket, timerWorker, gameTimerManager) {
socket.on(globals.EVENT_IDS.PLAYER_JOINED, (player, gameIsFull) => {
toast(player.name + ' joined!', 'success', false, true, 'short');
stateBucket.currentGameState.people.push(player);
stateBucket.currentGameState.isFull = gameIsFull;
gameStateRenderer.renderLobbyPlayers();
if ((
stateBucket.currentGameState.client.userType === globals.USER_TYPES.MODERATOR
|| stateBucket.currentGameState.client.userType === globals.USER_TYPES.TEMPORARY_MODERATOR
)
) {
displayStartGamePromptForModerators(stateBucket.currentGameState, gameStateRenderer);
}
});
socket.on(globals.EVENT_IDS.NEW_SPECTATOR, (spectator) => {
stateBucket.currentGameState.spectators.push(spectator);
});
socket.on(globals.EVENT_IDS.PLAYER_LEFT, (player) => {
removeStartGameFunctionalityIfPresent(gameStateRenderer);
toast(player.name + ' has left!', 'error', false, true, 'short');
const index = stateBucket.currentGameState.people.findIndex(person => person.id === player.id);
if (index >= 0) {
stateBucket.currentGameState.people.splice(
index,
1
);
gameStateRenderer.renderLobbyPlayers();
}
});
socket.on(globals.EVENT_IDS.START_GAME, () => {
socket.emit(
globals.SOCKET_EVENTS.IN_GAME_MESSAGE,
globals.EVENT_IDS.FETCH_GAME_STATE,
stateBucket.currentGameState.accessCode,
{ personId: stateBucket.currentGameState.client.cookie },
function (gameState) {
stateBucket.currentGameState = gameState;
processGameState(
stateBucket.currentGameState,
gameState.client.cookie,
socket,
gameStateRenderer,
gameTimerManager,
timerWorker,
true,
true
);
}
);
});
socket.on(globals.EVENT_IDS.SYNC_GAME_STATE, () => {
socket.emit(
globals.SOCKET_EVENTS.IN_GAME_MESSAGE,
globals.EVENT_IDS.FETCH_GAME_STATE,
stateBucket.currentGameState.accessCode,
{ personId: stateBucket.currentGameState.client.cookie },
function (gameState) {
stateBucket.currentGameState = gameState;
processGameState(
stateBucket.currentGameState,
gameState.client.cookie,
socket,
gameStateRenderer,
gameTimerManager,
timerWorker,
true,
true
);
}
);
});
if (timerWorker && gameTimerManager) {
gameTimerManager.attachTimerSocketListeners(socket, timerWorker, gameStateRenderer);
}
socket.on(globals.EVENT_IDS.KILL_PLAYER, (id) => {
const killedPerson = stateBucket.currentGameState.people.find((person) => person.id === id);
if (killedPerson) {
killedPerson.out = true;
if (stateBucket.currentGameState.client.userType === globals.USER_TYPES.MODERATOR) {
toast(killedPerson.name + ' killed.', 'success', true, true, 'medium');
gameStateRenderer.renderPlayersWithRoleAndAlignmentInfo(stateBucket.currentGameState.status === globals.STATUS.ENDED);
} else {
if (killedPerson.id === stateBucket.currentGameState.client.id) {
const clientUserType = document.getElementById('client-user-type');
if (clientUserType) {
clientUserType.innerText = globals.USER_TYPES.KILLED_PLAYER + ' \uD83D\uDC80';
}
gameStateRenderer.updatePlayerCardToKilledState();
toast('You have been killed!', 'warning', true, true, 'medium');
} else {
toast(killedPerson.name + ' was killed!', 'warning', true, true, 'medium');
}
if (stateBucket.currentGameState.client.userType === globals.USER_TYPES.TEMPORARY_MODERATOR) {
gameStateRenderer.removePlayerListEventListeners(false);
} else {
gameStateRenderer.renderPlayersWithNoRoleInformationUnlessRevealed(false);
}
}
}
});
socket.on(globals.EVENT_IDS.REVEAL_PLAYER, (revealData) => {
const revealedPerson = stateBucket.currentGameState.people.find((person) => person.id === revealData.id);
if (revealedPerson) {
revealedPerson.revealed = true;
revealedPerson.gameRole = revealData.gameRole;
revealedPerson.alignment = revealData.alignment;
if (stateBucket.currentGameState.client.userType === globals.USER_TYPES.MODERATOR) {
toast(revealedPerson.name + ' revealed.', 'success', true, true, 'medium');
gameStateRenderer.renderPlayersWithRoleAndAlignmentInfo(stateBucket.currentGameState.status === globals.STATUS.ENDED);
} else {
if (revealedPerson.id === stateBucket.currentGameState.client.id) {
toast('Your role has been revealed!', 'warning', true, true, 'medium');
} else {
toast(revealedPerson.name + ' was revealed as a ' + revealedPerson.gameRole + '!', 'warning', true, true, 'medium');
}
if (stateBucket.currentGameState.client.userType === globals.USER_TYPES.TEMPORARY_MODERATOR) {
gameStateRenderer.renderPlayersWithNoRoleInformationUnlessRevealed(true);
} else {
gameStateRenderer.renderPlayersWithNoRoleInformationUnlessRevealed(false);
}
}
}
});
socket.on(globals.EVENT_IDS.CHANGE_NAME, (personId, name) => {
propagateNameChange(stateBucket.currentGameState, name, personId);
updateDOMWithNameChange(stateBucket.currentGameState, gameStateRenderer);
processGameState(
stateBucket.currentGameState,
stateBucket.currentGameState.client.cookie,
socket,
gameStateRenderer,
gameTimerManager,
timerWorker,
false,
false
);
});
socket.on(globals.COMMANDS.END_GAME, (people) => {
stateBucket.currentGameState.people = people;
stateBucket.currentGameState.status = globals.STATUS.ENDED;
processGameState(
stateBucket.currentGameState,
stateBucket.currentGameState.client.cookie,
socket,
gameStateRenderer,
gameTimerManager,
timerWorker,
true,
true
);
});
}
function displayStartGamePromptForModerators (gameState, gameStateRenderer) {
const existingPrompt = document.getElementById('start-game-prompt');
if (existingPrompt) {
enableOrDisableStartButton(gameState, existingPrompt, gameStateRenderer.startGameHandler);
} else {
const newPrompt = document.createElement('div');
newPrompt.setAttribute('id', 'start-game-prompt');
newPrompt.innerHTML = HTMLFragments.START_GAME_PROMPT;
document.body.appendChild(newPrompt);
enableOrDisableStartButton(gameState, newPrompt, gameStateRenderer.startGameHandler);
}
}
function enableOrDisableStartButton (gameState, buttonContainer, handler) {
if (gameState.isFull) {
buttonContainer.querySelector('#start-game-button').addEventListener('click', handler);
buttonContainer.querySelector('#start-game-button').classList.remove('disabled');
} else {
buttonContainer.querySelector('#start-game-button').removeEventListener('click', handler);
buttonContainer.querySelector('#start-game-button').classList.add('disabled');
}
}
function removeStartGameFunctionalityIfPresent (gameStateRenderer) {
document.querySelector('#start-game-prompt')?.removeEventListener('click', gameStateRenderer.startGameHandler);
document.querySelector('#start-game-prompt')?.remove();
}
function propagateNameChange (gameState, name, personId) {
if (gameState.client.id === personId) {
gameState.client.name = name;
}
const matchingPerson = gameState.people.find((person) => person.id === personId);
if (matchingPerson) {
matchingPerson.name = name;
}
if (gameState.moderator.id === personId) {
gameState.moderator.name = name;
}
const matchingSpectator = gameState.spectators?.find((spectator) => spectator.id === personId);
if (matchingSpectator) {
matchingSpectator.name = name;
}
}
function updateDOMWithNameChange (gameState, gameStateRenderer) {
if (gameState.status === globals.STATUS.IN_PROGRESS) {
switch (gameState.client.userType) {
case globals.USER_TYPES.PLAYER:
case globals.USER_TYPES.KILLED_PLAYER:
case globals.USER_TYPES.SPECTATOR:
gameStateRenderer.renderPlayersWithNoRoleInformationUnlessRevealed(false);
break;
case globals.USER_TYPES.MODERATOR:
gameStateRenderer.renderPlayersWithRoleAndAlignmentInfo(gameState.status === globals.STATUS.ENDED);
break;
case globals.USER_TYPES.TEMPORARY_MODERATOR:
gameStateRenderer.renderPlayersWithNoRoleInformationUnlessRevealed(true);
break;
default:
break;
}
} else {
gameStateRenderer.renderLobbyPlayers();
}
}

View File

@@ -1,4 +1,4 @@
import { globals } from '../config/globals.js'; import { globals } from '../../config/globals.js';
export class GameTimerManager { export class GameTimerManager {
constructor (stateBucket, socket) { constructor (stateBucket, socket) {

View File

@@ -1,4 +1,4 @@
import { globals } from '../config/globals.js'; import { globals } from '../../config/globals.js';
/* /*
we will use sessionStorage during local development to aid in testing, vs. localStorage for production. we will use sessionStorage during local development to aid in testing, vs. localStorage for production.

View File

@@ -1,18 +1,5 @@
import { DeckStateManager } from '../modules/DeckStateManager.js'; import { createHandler } from '../modules/page_handlers/createHandler.js';
import { GameCreationStepManager } from '../modules/GameCreationStepManager.js'; import { GameCreationStepManager } from '../modules/game_creation/GameCreationStepManager.js';
import { injectNavbar } from '../modules/Navbar.js'; import { DeckStateManager } from '../modules/game_creation/DeckStateManager.js';
import createTemplate from '../view_templates/CreateTemplate.js';
const create = () => { createHandler(new GameCreationStepManager(new DeckStateManager()));
injectNavbar();
document.getElementById('game-creation-container').innerHTML = createTemplate;
const deckManager = new DeckStateManager();
const gameCreationStepManager = new GameCreationStepManager(deckManager);
gameCreationStepManager.renderStep('creation-step-container', 1);
};
if (typeof module !== 'undefined' && typeof module.exports !== 'undefined') {
module.exports = create;
} else {
create();
}

View File

@@ -1,443 +1,8 @@
import { UserUtility } from '../modules/UserUtility.js'; import 'core-js/stable';
import { globals } from '../config/globals.js'; import 'regenerator-runtime/runtime';
import { HTMLFragments } from '../modules/HTMLFragments.js'; import { gameHandler } from '../modules/page_handlers/gameHandler';
import { GameStateRenderer } from '../modules/GameStateRenderer.js';
import { toast } from '../modules/Toast.js';
import { GameTimerManager } from '../modules/GameTimerManager.js';
import { ModalManager } from '../modules/ModalManager.js';
import { stateBucket } from '../modules/StateBucket.js';
import { io } from 'socket.io-client'; import { io } from 'socket.io-client';
import { injectNavbar } from '../modules/Navbar.js'; import { XHRUtility } from '../modules/utility/XHRUtility.js';
import { XHRUtility } from '../modules/XHRUtility.js'; import gameTemplate from '../view_templates/GameTemplate.js';
const game = () => { await gameHandler(io('/in-game'), XHRUtility, window, gameTemplate);
injectNavbar();
XHRUtility.xhr(
'/api/games/environment',
'GET',
null,
null
)
.then((res) => {
stateBucket.environment = res.content;
const socket = io('/in-game');
const timerWorker = new Worker(new URL('../modules/Timer.js', import.meta.url));
const gameTimerManager = new GameTimerManager(stateBucket, socket);
const gameStateRenderer = new GameStateRenderer(stateBucket, socket);
socket.on('connect', () => {
syncWithGame(
stateBucket,
gameTimerManager,
gameStateRenderer,
timerWorker,
socket,
UserUtility.validateAnonUserSignature(res.content)
);
});
socket.on('connect_error', (err) => {
toast(err, 'error', true, false);
});
socket.on('disconnect', () => {
toast('Disconnected. Attempting reconnect...', 'error', true, false);
});
setClientSocketHandlers(stateBucket, gameStateRenderer, socket, timerWorker, gameTimerManager);
}).catch((res) => {
toast(res.content, 'error', true);
});
};
function syncWithGame (stateBucket, gameTimerManager, gameStateRenderer, timerWorker, socket, cookie) {
const splitUrl = window.location.href.split('/game/');
const accessCode = splitUrl[1];
if (/^[a-zA-Z0-9]+$/.test(accessCode) && accessCode.length === globals.ACCESS_CODE_LENGTH) {
socket.emit(globals.SOCKET_EVENTS.IN_GAME_MESSAGE, globals.EVENT_IDS.FETCH_GAME_STATE, accessCode, { personId: cookie }, function (gameState) {
if (gameState === null) {
window.location = '/not-found?reason=' + encodeURIComponent('game-not-found');
} else {
stateBucket.currentGameState = gameState;
document.querySelector('.spinner-container')?.remove();
document.querySelector('.spinner-background')?.remove();
document.getElementById('game-content').innerHTML = HTMLFragments.INITIAL_GAME_DOM;
toast('You are connected.', 'success', true, true, 'short');
processGameState(stateBucket.currentGameState, cookie, socket, gameStateRenderer, gameTimerManager, timerWorker, true, true);
}
});
} else {
window.location = '/not-found?reason=' + encodeURIComponent('invalid-access-code');
}
}
function processGameState (
currentGameState,
userId,
socket,
gameStateRenderer,
gameTimerManager,
timerWorker,
refreshPrompt = true,
animateContainer = false
) {
const containerAnimation = document.getElementById('game-state-container').animate(
[
{ opacity: '0', transform: 'translateY(10px)' },
{ opacity: '1', transform: 'translateY(0px)' }
], {
duration: 500,
easing: 'ease-in-out',
fill: 'both'
});
if (animateContainer) {
containerAnimation.play();
}
displayClientInfo(currentGameState.client.name, currentGameState.client.userType);
if (refreshPrompt) {
removeStartGameFunctionalityIfPresent(gameStateRenderer);
document.querySelector('#end-game-prompt')?.remove();
}
switch (currentGameState.status) {
case globals.STATUS.LOBBY:
document.getElementById('game-state-container').innerHTML = HTMLFragments.LOBBY;
gameStateRenderer.renderLobbyHeader();
gameStateRenderer.renderLobbyPlayers();
if ((
currentGameState.client.userType === globals.USER_TYPES.MODERATOR
|| currentGameState.client.userType === globals.USER_TYPES.TEMPORARY_MODERATOR
)
&& refreshPrompt
) {
displayStartGamePromptForModerators(currentGameState, gameStateRenderer);
}
break;
case globals.STATUS.IN_PROGRESS:
gameStateRenderer.renderGameHeader();
switch (currentGameState.client.userType) {
case globals.USER_TYPES.PLAYER:
document.getElementById('game-state-container').innerHTML = HTMLFragments.PLAYER_GAME_VIEW;
gameStateRenderer.renderPlayerView();
break;
case globals.USER_TYPES.KILLED_PLAYER:
document.getElementById('game-state-container').innerHTML = HTMLFragments.PLAYER_GAME_VIEW;
gameStateRenderer.renderPlayerView(true);
break;
case globals.USER_TYPES.MODERATOR:
document.getElementById('transfer-mod-prompt').innerHTML = HTMLFragments.TRANSFER_MOD_MODAL;
document.getElementById('game-state-container').innerHTML = HTMLFragments.MODERATOR_GAME_VIEW;
gameStateRenderer.renderModeratorView();
break;
case globals.USER_TYPES.TEMPORARY_MODERATOR:
document.getElementById('transfer-mod-prompt').innerHTML = HTMLFragments.TRANSFER_MOD_MODAL;
document.getElementById('game-state-container').innerHTML = HTMLFragments.TEMP_MOD_GAME_VIEW;
gameStateRenderer.renderTempModView();
break;
case globals.USER_TYPES.SPECTATOR:
document.getElementById('game-state-container').innerHTML = HTMLFragments.SPECTATOR_GAME_VIEW;
gameStateRenderer.renderSpectatorView();
break;
default:
break;
}
if (currentGameState.timerParams) {
socket.emit(globals.SOCKET_EVENTS.IN_GAME_MESSAGE, globals.EVENT_IDS.GET_TIME_REMAINING, currentGameState.accessCode);
} else {
document.querySelector('#game-timer')?.remove();
document.querySelector('#timer-container-moderator')?.remove();
document.querySelector('label[for="game-timer"]')?.remove();
}
break;
case globals.STATUS.ENDED: {
const container = document.getElementById('game-state-container');
container.innerHTML = HTMLFragments.END_OF_GAME_VIEW;
gameStateRenderer.renderEndOfGame(currentGameState);
break;
}
default:
break;
}
activateRoleInfoButton(stateBucket.currentGameState.deck);
}
function displayClientInfo (name, userType) {
document.getElementById('client-name').innerText = name;
document.getElementById('client-user-type').innerText = userType;
document.getElementById('client-user-type').innerText += globals.USER_TYPE_ICONS[userType];
}
function setClientSocketHandlers (stateBucket, gameStateRenderer, socket, timerWorker, gameTimerManager) {
socket.on(globals.EVENTS.PLAYER_JOINED, (player, gameIsFull) => {
toast(player.name + ' joined!', 'success', false, true, 'short');
stateBucket.currentGameState.people.push(player);
stateBucket.currentGameState.isFull = gameIsFull;
gameStateRenderer.renderLobbyPlayers();
if ((
stateBucket.currentGameState.client.userType === globals.USER_TYPES.MODERATOR
|| stateBucket.currentGameState.client.userType === globals.USER_TYPES.TEMPORARY_MODERATOR
)
) {
displayStartGamePromptForModerators(stateBucket.currentGameState, gameStateRenderer);
}
});
socket.on(globals.EVENTS.NEW_SPECTATOR, (spectator) => {
stateBucket.currentGameState.spectators.push(spectator);
});
socket.on(globals.EVENTS.PLAYER_LEFT, (player) => {
removeStartGameFunctionalityIfPresent(gameStateRenderer);
toast(player.name + ' has left!', 'error', false, true, 'short');
const index = stateBucket.currentGameState.people.findIndex(person => person.id === player.id);
if (index >= 0) {
stateBucket.currentGameState.people.splice(
index,
1
);
gameStateRenderer.renderLobbyPlayers();
}
});
socket.on(globals.EVENTS.START_GAME, () => {
socket.emit(
globals.SOCKET_EVENTS.IN_GAME_MESSAGE,
globals.EVENT_IDS.FETCH_GAME_STATE,
stateBucket.currentGameState.accessCode,
{ personId: stateBucket.currentGameState.client.cookie },
function (gameState) {
stateBucket.currentGameState = gameState;
processGameState(
stateBucket.currentGameState,
gameState.client.cookie,
socket,
gameStateRenderer,
gameTimerManager,
timerWorker,
true,
true
);
}
);
});
socket.on(globals.EVENTS.SYNC_GAME_STATE, () => {
socket.emit(
globals.SOCKET_EVENTS.IN_GAME_MESSAGE,
globals.EVENT_IDS.FETCH_GAME_STATE,
stateBucket.currentGameState.accessCode,
{ personId: stateBucket.currentGameState.client.cookie },
function (gameState) {
stateBucket.currentGameState = gameState;
processGameState(
stateBucket.currentGameState,
gameState.client.cookie,
socket,
gameStateRenderer,
gameTimerManager,
timerWorker,
true,
true
);
}
);
});
if (timerWorker && gameTimerManager) {
gameTimerManager.attachTimerSocketListeners(socket, timerWorker, gameStateRenderer);
}
socket.on(globals.EVENTS.KILL_PLAYER, (id) => {
const killedPerson = stateBucket.currentGameState.people.find((person) => person.id === id);
if (killedPerson) {
killedPerson.out = true;
if (stateBucket.currentGameState.client.userType === globals.USER_TYPES.MODERATOR) {
toast(killedPerson.name + ' killed.', 'success', true, true, 'medium');
gameStateRenderer.renderPlayersWithRoleAndAlignmentInfo(stateBucket.currentGameState.status === globals.STATUS.ENDED);
} else {
if (killedPerson.id === stateBucket.currentGameState.client.id) {
const clientUserType = document.getElementById('client-user-type');
if (clientUserType) {
clientUserType.innerText = globals.USER_TYPES.KILLED_PLAYER + ' \uD83D\uDC80';
}
gameStateRenderer.updatePlayerCardToKilledState();
toast('You have been killed!', 'warning', true, true, 'medium');
} else {
toast(killedPerson.name + ' was killed!', 'warning', true, true, 'medium');
}
if (stateBucket.currentGameState.client.userType === globals.USER_TYPES.TEMPORARY_MODERATOR) {
gameStateRenderer.removePlayerListEventListeners(false);
} else {
gameStateRenderer.renderPlayersWithNoRoleInformationUnlessRevealed(false);
}
}
}
});
socket.on(globals.EVENTS.REVEAL_PLAYER, (revealData) => {
const revealedPerson = stateBucket.currentGameState.people.find((person) => person.id === revealData.id);
if (revealedPerson) {
revealedPerson.revealed = true;
revealedPerson.gameRole = revealData.gameRole;
revealedPerson.alignment = revealData.alignment;
if (stateBucket.currentGameState.client.userType === globals.USER_TYPES.MODERATOR) {
toast(revealedPerson.name + ' revealed.', 'success', true, true, 'medium');
gameStateRenderer.renderPlayersWithRoleAndAlignmentInfo(stateBucket.currentGameState.status === globals.STATUS.ENDED);
} else {
if (revealedPerson.id === stateBucket.currentGameState.client.id) {
toast('Your role has been revealed!', 'warning', true, true, 'medium');
} else {
toast(revealedPerson.name + ' was revealed as a ' + revealedPerson.gameRole + '!', 'warning', true, true, 'medium');
}
if (stateBucket.currentGameState.client.userType === globals.USER_TYPES.TEMPORARY_MODERATOR) {
gameStateRenderer.renderPlayersWithNoRoleInformationUnlessRevealed(true);
} else {
gameStateRenderer.renderPlayersWithNoRoleInformationUnlessRevealed(false);
}
}
}
});
socket.on(globals.EVENTS.CHANGE_NAME, (personId, name) => {
propagateNameChange(stateBucket.currentGameState, name, personId);
updateDOMWithNameChange(stateBucket.currentGameState, gameStateRenderer);
processGameState(
stateBucket.currentGameState,
stateBucket.currentGameState.client.cookie,
socket,
gameStateRenderer,
gameTimerManager,
timerWorker,
false,
false
);
});
socket.on(globals.COMMANDS.END_GAME, (people) => {
stateBucket.currentGameState.people = people;
stateBucket.currentGameState.status = globals.STATUS.ENDED;
processGameState(
stateBucket.currentGameState,
stateBucket.currentGameState.client.cookie,
socket,
gameStateRenderer,
gameTimerManager,
timerWorker,
true,
true
);
});
}
function displayStartGamePromptForModerators (gameState, gameStateRenderer) {
const existingPrompt = document.getElementById('start-game-prompt');
if (existingPrompt) {
enableOrDisableStartButton(gameState, existingPrompt, gameStateRenderer.startGameHandler);
} else {
const newPrompt = document.createElement('div');
newPrompt.setAttribute('id', 'start-game-prompt');
newPrompt.innerHTML = HTMLFragments.START_GAME_PROMPT;
document.body.appendChild(newPrompt);
enableOrDisableStartButton(gameState, newPrompt, gameStateRenderer.startGameHandler);
}
}
function enableOrDisableStartButton (gameState, buttonContainer, handler) {
if (gameState.isFull) {
buttonContainer.querySelector('#start-game-button').addEventListener('click', handler);
buttonContainer.querySelector('#start-game-button').classList.remove('disabled');
} else {
buttonContainer.querySelector('#start-game-button').removeEventListener('click', handler);
buttonContainer.querySelector('#start-game-button').classList.add('disabled');
}
}
function removeStartGameFunctionalityIfPresent (gameStateRenderer) {
document.querySelector('#start-game-prompt')?.removeEventListener('click', gameStateRenderer.startGameHandler);
document.querySelector('#start-game-prompt')?.remove();
}
function propagateNameChange (gameState, name, personId) {
if (gameState.client.id === personId) {
gameState.client.name = name;
}
const matchingPerson = gameState.people.find((person) => person.id === personId);
if (matchingPerson) {
matchingPerson.name = name;
}
if (gameState.moderator.id === personId) {
gameState.moderator.name = name;
}
const matchingSpectator = gameState.spectators?.find((spectator) => spectator.id === personId);
if (matchingSpectator) {
matchingSpectator.name = name;
}
}
function updateDOMWithNameChange (gameState, gameStateRenderer) {
if (gameState.status === globals.STATUS.IN_PROGRESS) {
switch (gameState.client.userType) {
case globals.USER_TYPES.PLAYER:
case globals.USER_TYPES.KILLED_PLAYER:
case globals.USER_TYPES.SPECTATOR:
gameStateRenderer.renderPlayersWithNoRoleInformationUnlessRevealed(false);
break;
case globals.USER_TYPES.MODERATOR:
gameStateRenderer.renderPlayersWithRoleAndAlignmentInfo(gameState.status === globals.STATUS.ENDED);
break;
case globals.USER_TYPES.TEMPORARY_MODERATOR:
gameStateRenderer.renderPlayersWithNoRoleInformationUnlessRevealed(true);
break;
default:
break;
}
} else {
gameStateRenderer.renderLobbyPlayers();
}
}
function activateRoleInfoButton (deck) {
deck.sort((a, b) => {
return a.team === globals.ALIGNMENT.GOOD ? -1 : 1;
});
document.getElementById('role-info-button').addEventListener('click', (e) => {
e.preventDefault();
document.getElementById('role-info-prompt').innerHTML = HTMLFragments.ROLE_INFO_MODAL;
const modalContent = document.getElementById('game-role-info-container');
for (const card of deck) {
const roleDiv = document.createElement('div');
const roleNameDiv = document.createElement('div');
roleNameDiv.classList.add('role-info-name');
const roleName = document.createElement('h5');
const roleQuantity = document.createElement('h5');
const roleDescription = document.createElement('p');
roleDescription.innerText = card.description;
roleName.innerText = card.role;
roleQuantity.innerText = card.quantity + 'x';
if (card.team === globals.ALIGNMENT.GOOD) {
roleName.classList.add(globals.ALIGNMENT.GOOD);
} else {
roleName.classList.add(globals.ALIGNMENT.EVIL);
}
roleNameDiv.appendChild(roleQuantity);
roleNameDiv.appendChild(roleName);
roleDiv.appendChild(roleNameDiv);
roleDiv.appendChild(roleDescription);
modalContent.appendChild(roleDiv);
}
ModalManager.displayModal('role-info-modal', 'role-info-modal-background', 'close-role-info-modal-button');
});
}
if (typeof module !== 'undefined' && typeof module.exports !== 'undefined') {
module.exports = game;
} else {
game();
}

View File

@@ -1,6 +1,6 @@
import { XHRUtility } from '../modules/XHRUtility.js'; import { XHRUtility } from '../modules/utility/XHRUtility.js';
import { toast } from '../modules/Toast.js'; import { toast } from '../modules/front_end_components/Toast.js';
import { injectNavbar } from '../modules/Navbar.js'; import { injectNavbar } from '../modules/front_end_components/Navbar.js';
const home = () => { const home = () => {
injectNavbar(); injectNavbar();

View File

@@ -1,4 +1,4 @@
import { injectNavbar } from '../modules/Navbar.js'; import { injectNavbar } from '../modules/front_end_components/Navbar.js';
const howToUse = () => { const howToUse = () => {
injectNavbar(); injectNavbar();

View File

@@ -1,7 +1,7 @@
import { injectNavbar } from '../modules/Navbar.js'; import { injectNavbar } from '../modules/front_end_components/Navbar.js';
import { toast } from '../modules/Toast.js'; import { toast } from '../modules/front_end_components/Toast.js';
import { XHRUtility } from '../modules/XHRUtility.js'; import { XHRUtility } from '../modules/utility/XHRUtility.js';
import { UserUtility } from '../modules/UserUtility.js'; import { UserUtility } from '../modules/utility/UserUtility.js';
import { globals } from '../config/globals.js'; import { globals } from '../config/globals.js';
const join = () => { const join = () => {

View File

@@ -1,4 +1,4 @@
import { injectNavbar } from '../modules/Navbar.js'; import { injectNavbar } from '../modules/front_end_components/Navbar.js';
const notFound = () => { const notFound = () => {
injectNavbar(); injectNavbar();

View File

@@ -0,0 +1,42 @@
const template =
`<div id="role-info-prompt"></div>
<div id="transfer-mod-prompt"></div>
<div class="spinner-background"></div>
<div class="spinner-container">
<div class="lds-spinner">
<div></div>
<div></div>
<div></div>
<div></div>
<div></div>
<div></div>
<div></div>
<div></div>
<div></div>
<div></div>
<div></div>
<div></div>
</div>
<p>Connecting to game...</p>
</div>
<div id="mobile-menu-background-overlay"></div>
<div id="navbar"></div>
<div id="game-content" class="container">
<div class="placeholder-row">
<div class="animated-placeholder animated-placeholder-short"></div>
<div class="animated-placeholder animated-placeholder-short animated-placeholder-invisible"></div>
</div>
<div class="placeholder-row">
<div class="animated-placeholder animated-placeholder-short"></div>
<div class="animated-placeholder animated-placeholder-short animated-placeholder-invisible"></div>
</div>
<div class="animated-placeholder animated-placeholder-long"></div>
<div class="animated-placeholder animated-placeholder-long"></div>
<div class="placeholder-row">
<div class="animated-placeholder animated-placeholder-short"></div>
<div class="animated-placeholder animated-placeholder-short animated-placeholder-invisible"></div>
</div>
<div class="animated-placeholder animated-placeholder-long"></div>
</div>`;
export default template;

View File

@@ -19,46 +19,8 @@
<link rel="preload" href="/webfonts/SignikaNegative-Light.woff2" as="font" type="font/woff2" crossorigin> <link rel="preload" href="/webfonts/SignikaNegative-Light.woff2" as="font" type="font/woff2" crossorigin>
</head> </head>
<body> <body>
<div id="role-info-prompt"></div>
<div id="transfer-mod-prompt"></div>
<div class="spinner-background"></div>
<div class="spinner-container">
<div class="lds-spinner">
<div></div>
<div></div>
<div></div>
<div></div>
<div></div>
<div></div>
<div></div>
<div></div>
<div></div>
<div></div>
<div></div>
<div></div>
</div>
<p>Connecting to game...</p>
</div>
<div id="mobile-menu-background-overlay"></div>
<div id="navbar"></div>
<div id="game-content" class="container">
<div class="placeholder-row">
<div class="animated-placeholder animated-placeholder-short"></div>
<div class="animated-placeholder animated-placeholder-short animated-placeholder-invisible"></div>
</div>
<div class="placeholder-row">
<div class="animated-placeholder animated-placeholder-short"></div>
<div class="animated-placeholder animated-placeholder-short animated-placeholder-invisible"></div>
</div>
<div class="animated-placeholder animated-placeholder-long"></div>
<div class="animated-placeholder animated-placeholder-long"></div>
<div class="placeholder-row">
<div class="animated-placeholder animated-placeholder-short"></div>
<div class="animated-placeholder animated-placeholder-short animated-placeholder-invisible"></div>
</div>
<div class="animated-placeholder animated-placeholder-long"></div>
</div>
<script src="/dist/game-bundle.js"></script> <script src="/dist/game-bundle.js"></script>
<script src="/socket.io/socket.io.js"></script>
</body> </body>
</html> </html>

View File

@@ -33,5 +33,8 @@ module.exports = {
loader: "webpack-remove-debug", // remove "debug" package loader: "webpack-remove-debug", // remove "debug" package
} }
], ],
},
experiments: {
topLevelAwait: true
} }
} }

View File

@@ -33,5 +33,8 @@ module.exports = {
loader: "webpack-remove-debug", // remove "debug" package loader: "webpack-remove-debug", // remove "debug" package
} }
], ],
},
experiments: {
topLevelAwait: true
} }
} }

View File

@@ -4,8 +4,8 @@ module.exports = function(config) {
frameworks: ['jasmine'], frameworks: ['jasmine'],
files: [ files: [
{ pattern: 'spec/e2e/*.js', type: 'module' }, { pattern: 'spec/e2e/*.js', type: 'module' },
{ pattern: 'client/src/modules/*.js', type: 'module', included: true, served: true }, { pattern: 'spec/support/*.js', type: 'module' },
{ pattern: 'client/src/modules/third_party/*.js', type: 'module', included: true, served: true }, { pattern: 'client/src/modules/*/*.js', type: 'module', included: true, served: true },
{ pattern: 'client/src/config/*.js', type: 'module', included: true, served: true }, { pattern: 'client/src/config/*.js', type: 'module', included: true, served: true },
{ pattern: 'client/src/model/*.js', type: 'module', included: true, served: true }, { pattern: 'client/src/model/*.js', type: 'module', included: true, served: true },
{ pattern: 'client/src/view_templates/*.js', type: 'module', included: true, served: true } { pattern: 'client/src/view_templates/*.js', type: 'module', included: true, served: true }

1422
package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

@@ -25,7 +25,7 @@
"author": "", "author": "",
"license": "ISC", "license": "ISC",
"dependencies": { "dependencies": {
"@babel/core": "^7.16.7", "@babel/core": "^7.18.13",
"@babel/eslint-parser": "^7.16.5", "@babel/eslint-parser": "^7.16.5",
"@babel/plugin-transform-object-assign": "^7.16.5", "@babel/plugin-transform-object-assign": "^7.16.5",
"@babel/preset-env": "^7.16.5", "@babel/preset-env": "^7.16.5",
@@ -33,6 +33,7 @@
"babel-eslint": "^10.1.0", "babel-eslint": "^10.1.0",
"babel-loader": "^8.2.3", "babel-loader": "^8.2.3",
"body-parser": "^1.19.1", "body-parser": "^1.19.1",
"core-js": "^3.25.0",
"cors": "^2.8.5", "cors": "^2.8.5",
"express": "^4.17.1", "express": "^4.17.1",
"express-force-https": "^1.0.0", "express-force-https": "^1.0.0",
@@ -41,6 +42,7 @@
"karma-jasmine": "^4.0.1", "karma-jasmine": "^4.0.1",
"open": "^7.0.3", "open": "^7.0.3",
"rate-limiter-flexible": "^2.3.6", "rate-limiter-flexible": "^2.3.6",
"regenerator-runtime": "^0.13.9",
"socket.io": "^4.4.0", "socket.io": "^4.4.0",
"socket.io-client": "^4.4.0", "socket.io-client": "^4.4.0",
"webpack": "^5.65.0", "webpack": "^5.65.0",

View File

@@ -1,21 +1,17 @@
// TODO: clean up these deep relative paths? jsconfig.json is not working... // TODO: clean up these deep relative paths? jsconfig.json is not working...
import { createHandler } from '../../client/src/modules/page_handlers/createHandler.js';
import { GameCreationStepManager } from '../../client/src/modules/GameCreationStepManager.js'; import { GameCreationStepManager } from '../../client/src/modules/game_creation/GameCreationStepManager.js';
import { DeckStateManager } from '../../client/src/modules/DeckStateManager.js'; import { DeckStateManager } from '../../client/src/modules/game_creation/DeckStateManager.js';
import createTemplate from '../../client/src/view_templates/CreateTemplate.js';
describe('Create page', function () { describe('Create page', function () {
let gameCreationStepManager; const gameCreationStepManager = new GameCreationStepManager(new DeckStateManager());
beforeAll(function () { beforeAll(function () {
spyOn(window, 'confirm').and.returnValue(true); spyOn(window, 'confirm').and.returnValue(true);
const container = document.createElement('div'); const container = document.createElement('div');
container.setAttribute('id', 'game-creation-container'); container.setAttribute('id', 'game-creation-container');
document.body.appendChild(container); document.body.appendChild(container);
document.getElementById('game-creation-container').innerHTML = createTemplate; createHandler(gameCreationStepManager);
const deckManager = new DeckStateManager();
gameCreationStepManager = new GameCreationStepManager(deckManager);
gameCreationStepManager.renderStep('creation-step-container', 1);
}); });
describe('deck builder page', function () { describe('deck builder page', function () {
@@ -119,5 +115,9 @@ describe('Create page', function () {
expect(gameCreationStepManager.deckManager.deck.length).toEqual(5); expect(gameCreationStepManager.deckManager.deck.length).toEqual(5);
expect(document.querySelectorAll('.added-role').length).toEqual(5); expect(document.querySelectorAll('.added-role').length).toEqual(5);
}); });
afterAll(() => {
document.body.innerHTML = '';
});
}); });
}); });

145
spec/e2e/game_spec.js Normal file
View File

@@ -0,0 +1,145 @@
import { gameHandler } from '../../client/src/modules/page_handlers/gameHandler.js';
import { mockGames } from '../support/MockGames.js';
import gameTemplate from '../../client/src/view_templates/GameTemplate.js';
import { globals } from '../../client/src/config/globals.js';
describe('game page', () => {
const XHRUtility = {
xhr (url, method = 'GET', headers, body = null) {
switch (url) {
case '/api/games/environment':
return new Promise((resolve, reject) => {
resolve({ content: 'production' });
});
}
}
};
describe('lobby game', () => {
const mockSocket = {
eventHandlers: {},
on: function (message, handler) {
console.log('REGISTERED MOCK SOCKET HANDLER: ' + message);
this.eventHandlers[message] = handler;
},
emit: function (eventName, ...args) {
switch (args[0]) { // eventName is currently always "inGameMessage" - the first arg after that is the specific message type
case globals.EVENT_IDS.FETCH_GAME_STATE:
args[args.length - 1](mockGames.gameInLobby);
}
},
hasListeners: function (listener) {
return false;
}
};
beforeAll(async () => {
await gameHandler(mockSocket, XHRUtility, { location: { href: 'host/game/ABCD' } }, gameTemplate);
mockSocket.eventHandlers.connect();
});
it('should display the connected client', () => {
expect(document.getElementById('client-name').innerText).toEqual('Alec');
expect(document.getElementById('client-user-type').innerText).toEqual('moderator \uD83D\uDC51');
});
it('should display the QR Code', () => {
expect(document.getElementById('canvas').innerText).not.toBeNull();
});
it('should display a new player when they join', () => {
mockSocket.eventHandlers[globals.EVENT_IDS.PLAYER_JOINED]({
name: 'Jane',
id: '123',
userType: globals.USER_TYPES.PLAYER,
out: false,
revealed: false
}, false);
expect(document.querySelectorAll('.lobby-player').length).toEqual(2);
expect(document.getElementById('current-info-message').innerText).toEqual('Jane joined!');
});
it('should activate the start button for the moderator when the game is full', () => {
expect(document.getElementById('start-game-button').classList.contains('disabled')).toBeTrue();
mockSocket.eventHandlers[globals.EVENT_IDS.PLAYER_JOINED]({
name: 'Jack',
id: '456',
userType: globals.USER_TYPES.PLAYER,
out: false,
revealed: false
}, true);
expect(document.getElementById('start-game-button').classList.contains('disabled')).toBeFalse();
});
afterAll(() => {
document.body.innerHTML = '';
});
});
describe('in-progress game', () => {
const mockSocket = {
eventHandlers: {},
on: function (message, handler) {
console.log('REGISTERED MOCK SOCKET HANDLER: ' + message);
this.eventHandlers[message] = handler;
},
emit: function (eventName, ...args) {
switch (args[0]) { // eventName is currently always "inGameMessage" - the first arg after that is the specific message type
case globals.EVENT_IDS.FETCH_GAME_STATE:
args[args.length - 1](mockGames.inProgressGame);
break;
default:
break;
}
},
hasListeners: function (listener) {
return false;
}
};
beforeAll(async () => {
await gameHandler(mockSocket, XHRUtility, { location: { href: 'host/game/ABCD' } }, gameTemplate);
mockSocket.eventHandlers.connect();
mockSocket.eventHandlers.getTimeRemaining(120000, true);
});
it('should display the game role of the client', () => {
expect(document.getElementById('role-name').innerText).toEqual('Parity Hunter');
expect(document.getElementById('role-image').getAttribute('src')).toEqual('../images/roles/ParityHunter.png');
expect(document.getElementById('game-timer').innerText).toEqual('00:02:00');
expect(document.getElementById('game-timer').classList.contains('paused')).toEqual(true);
expect(document.getElementById('players-alive-label').innerText).toEqual('Players: 4 / 5 Alive');
});
it('should flip the role card of the client', () => {
const clickEvent = document.createEvent('MouseEvents');
clickEvent.initEvent('dblclick', true, true);
document.getElementById('game-role-back').dispatchEvent(clickEvent);
expect(document.getElementById('game-role').style.display).toEqual('flex');
expect(document.getElementById('game-role-back').style.display).toEqual('none');
});
it('should display the timer', () => {
expect(document.getElementById('game-timer').innerText).toEqual('00:02:00');
expect(document.getElementById('game-timer').classList.contains('paused')).toEqual(true);
});
it('should display the number of alive players', () => {
expect(document.getElementById('players-alive-label').innerText).toEqual('Players: 4 / 5 Alive');
});
it('should display the role info modal when the button is clicked', () => {
document.getElementById('role-info-button').click();
expect(document.getElementById('role-info-modal').style.display).toEqual('flex');
});
it('should NOT display the ability to play/pause the timer when the client is NOT a moderator', () => {
expect(document.getElementById('play-pause')).toBeNull();
});
afterAll(() => {
document.body.innerHTML = '';
});
});
});

168
spec/support/MockGames.js Normal file
View File

@@ -0,0 +1,168 @@
export const mockGames = {
gameInLobby: {
accessCode: 'ZS6M',
status: 'lobby',
moderator: {
name: 'Alec',
id: 'HZM64BVGXCSXS9L5YMGK2WTTQ',
userType: 'moderator',
out: false,
revealed: false
},
client: {
name: 'Alec',
hasEnteredName: false,
id: 'HZM64BVGXCSXS9L5YMGK2WTTQ',
cookie: 'Q68BYSMM7DB5CH338TNPMF9CK',
userType: 'moderator'
},
deck: [
{
role: 'Parity Hunter',
team: 'good',
description: 'You beat a werewolf in a 1v1 situation, winning the game for the village.',
id: 'wli3r2i9zxxmnns5euvtc01v0',
quantity: 1
},
{
role: 'Seer',
team: 'good',
description: 'Each night, learn if a chosen person is a Werewolf.',
id: '7q0xxfuflsjetzit1elu5rd2k',
quantity: 1
},
{
role: 'Villager',
team: 'good',
description: 'During the day, find the wolves and kill them.',
id: '33pw77odkdt3042yumxtxbrda',
quantity: 1
},
{
role: 'Sorceress',
team: 'evil',
description: 'Each night, learn if a chosen person is the Seer.',
id: '6fboglgqwua8n0twgh2f4a0xh',
quantity: 1
},
{
role: 'Werewolf',
team: 'evil',
description: "During the night, choose a villager to kill. Don't get killed.",
id: 'ixpmpaouc3oj1llkm6gttxbor',
quantity: 1
}
],
people: [],
timerParams: {
hours: null,
minutes: 15,
paused: false
},
isFull: false,
spectators: []
},
inProgressGame: {
accessCode: 'VVVG',
status: 'in progress',
moderator: {
name: 'Alec',
id: 'H24358C4GQ238LFK66RYMST9P',
userType: 'moderator',
out: false,
revealed: false
},
client: {
name: 'Andrea',
hasEnteredName: false,
id: 'THCX9K6MCKZXBXYH95FPLP68Y',
cookie: 'ZLPHS946H33W7LVJ28M8XCRVZ',
userType: 'player',
gameRole: 'Parity Hunter',
gameRoleDescription: 'You beat a werewolf in a 1v1 situation, winning the game for the village.',
alignment: 'good',
out: false
},
deck: [
{
role: 'Parity Hunter',
team: 'good',
description: 'You beat a werewolf in a 1v1 situation, winning the game for the village.',
id: 'gw82x923gde5pcf3ru8y0w6mr',
quantity: 1
},
{
role: 'Seer',
team: 'good',
description: 'Each night, learn if a chosen person is a Werewolf.',
id: '0it2wybz7mdoatqs60b847x5v',
quantity: 1
},
{
role: 'Villager',
team: 'good',
description: 'During the day, find the wolves and kill them.',
id: 'v8oeyscxu53bg0a29uxsh4mzc',
quantity: 1
},
{
role: 'Sorceress',
team: 'evil',
description: 'Each night, learn if a chosen person is the Seer.',
id: '52ooljj12xpah0dgirxay2lma',
quantity: 1
},
{
role: 'Werewolf',
team: 'evil',
description: "During the night, choose a villager to kill. Don't get killed.",
id: '1oomauy0wc9pn5q55d2f4zq64',
quantity: 1
}
],
people: [
{
name: 'Andrea',
id: 'THCX9K6MCKZXBXYH95FPLP68Y',
userType: 'player',
out: false,
revealed: false
},
{
name: 'Greg',
id: 'SFVBXJZNF3G3QDML63X34KG5X',
userType: 'player',
out: false,
revealed: false
},
{
name: 'Lys',
id: 'S2496LVXL9CFP5B493XX6XMYL',
userType: 'player',
out: false,
revealed: false
},
{
name: 'Hannah',
id: 'Y7P2LGDZL6NV283525PL5GZTB',
userType: 'player',
out: true,
revealed: true
},
{
name: 'Matthew',
id: 'Z9YZ2JBM2GPRXFJB9J6ZFNSP9',
userType: 'player',
out: false,
revealed: false
}
],
timerParams: {
hours: null,
minutes: 2,
paused: true,
timeRemaining: 120000
},
isFull: true
}
};