diff --git a/client/src/app/pages/admin/admin-page/admin-page.component.spec.ts b/client/src/app/pages/admin/admin-page/admin-page.component.spec.ts index ad8c93290494f54847f22ee3533d7c7fd6b2cc05..955d04ace8d321e4fa2db0410124d6ed3fe0a2d1 100644 --- a/client/src/app/pages/admin/admin-page/admin-page.component.spec.ts +++ b/client/src/app/pages/admin/admin-page/admin-page.component.spec.ts @@ -1,9 +1,9 @@ import { ComponentFixture, TestBed } from '@angular/core/testing'; import { AdminPageComponent } from './admin-page.component'; +import { HistoryManagementComponent } from './history-management/history-management.component'; +import { HistoryManagementDummyComponent } from './history-management/history-management.component.dummy'; import { QuizManagementComponent } from './quiz-management/quiz-management.component'; import { QuizManagementDummyComponent } from './quiz-management/quiz-management.component.dummy'; -import { HistoryManagementComponent } from './history-management/history-management.component'; -import { HistoryManagementDummyComponent } from './history-management/history-management.dummy.component'; describe('AdminPageComponent', () => { let component: AdminPageComponent; diff --git a/client/src/app/pages/admin/admin-page/history-management/history-management.dummy.component.ts b/client/src/app/pages/admin/admin-page/history-management/history-management.component.dummy.ts similarity index 100% rename from client/src/app/pages/admin/admin-page/history-management/history-management.dummy.component.ts rename to client/src/app/pages/admin/admin-page/history-management/history-management.component.dummy.ts diff --git a/client/src/app/pages/admin/admin-page/history-management/history-management.component.html b/client/src/app/pages/admin/admin-page/history-management/history-management.component.html index 6ef174baf02f956e0f28009cee6717574a4f8e44..1ed123243e361e908ff0ca9e14a115dff23ff677 100644 --- a/client/src/app/pages/admin/admin-page/history-management/history-management.component.html +++ b/client/src/app/pages/admin/admin-page/history-management/history-management.component.html @@ -1,11 +1,19 @@ <mat-card class="card-container card-list"> - <mat-card-header> + <mat-card-header class="history-header"> <mat-card-title>Historique des parties</mat-card-title> <mat-card-actions> - <button mat-fab color="warn" matTooltip="Vider l'historique"> + <button mat-fab color="warn" (click)="clear()" matTooltip="Vider l'historique" [disabled]="(history$ | async)?.data?.length === 0"> <mat-icon>delete_sweep</mat-icon> </button> </mat-card-actions> </mat-card-header> - <mat-divider></mat-divider> + + <mat-card-content class="history-content"> + <ng-container *ngIf="history$ | async as history"> + <app-history-table *ngIf="history.data.length > 0; else emptyHistory" class="history-table" [history]="history"></app-history-table> + <ng-template #emptyHistory> + <p>Aucune partie n'a été jouée.</p> + </ng-template> + </ng-container> + </mat-card-content> </mat-card> diff --git a/client/src/app/pages/admin/admin-page/history-management/history-management.component.scss b/client/src/app/pages/admin/admin-page/history-management/history-management.component.scss index e69de29bb2d1d6434b8b29ae775ad8c2e48c5391..fbf7801bce3157ab4e4cba76195d688642071426 100644 --- a/client/src/app/pages/admin/admin-page/history-management/history-management.component.scss +++ b/client/src/app/pages/admin/admin-page/history-management/history-management.component.scss @@ -0,0 +1,18 @@ +mat-card-actions { + /* Keep actions on top of table header */ + z-index: 1000; +} + +.history-header { + padding-bottom: 0; +} + +.history-content { + padding-top: 0; + + flex-grow: 1; +} + +.history-table { + flex-grow: 1; +} diff --git a/client/src/app/pages/admin/admin-page/history-management/history-management.component.spec.ts b/client/src/app/pages/admin/admin-page/history-management/history-management.component.spec.ts index c08ad529fa29801115e046f7cddcc12512aa1ad7..1d22d9b2a671eb5b4aca63360c589a7e96b768c1 100644 --- a/client/src/app/pages/admin/admin-page/history-management/history-management.component.spec.ts +++ b/client/src/app/pages/admin/admin-page/history-management/history-management.component.spec.ts @@ -1,15 +1,43 @@ import { ComponentFixture, TestBed } from '@angular/core/testing'; +import { MatTableDataSource } from '@angular/material/table'; +import { CommunicationService } from '@app/services/communication/communication.service'; +import { GameRecord } from '@common/game'; +import { of } from 'rxjs'; import { HistoryManagementComponent } from './history-management.component'; +import { HistoryTableComponent } from './history-table/history-table.component'; +import { HistoryTableDummyComponent } from './history-table/history-table.component.dummy'; describe('HistoryManagementComponent', () => { let component: HistoryManagementComponent; let fixture: ComponentFixture<HistoryManagementComponent>; + let communicationService: jasmine.SpyObj<CommunicationService>; + beforeEach(() => { + communicationService = jasmine.createSpyObj(CommunicationService.name, ['getHistory$', 'clearHistory$']); + communicationService.getHistory$.and.returnValue(of([])); + communicationService.clearHistory$.and.returnValue(of(null)); + TestBed.configureTestingModule({ imports: [HistoryManagementComponent], + providers: [ + { + provide: CommunicationService, + useValue: communicationService, + }, + ], + }); + + TestBed.overrideComponent(HistoryManagementComponent, { + remove: { + imports: [HistoryTableComponent], + }, + add: { + imports: [HistoryTableDummyComponent], + }, }); + fixture = TestBed.createComponent(HistoryManagementComponent); component = fixture.componentInstance; fixture.detectChanges(); @@ -18,4 +46,54 @@ describe('HistoryManagementComponent', () => { it('should create', () => { expect(component).toBeTruthy(); }); + + describe('history$', () => { + it('should get history', () => { + component.history$.subscribe(); + + expect(communicationService.getHistory$).toHaveBeenCalled(); + }); + + it('should emit the table data source', () => { + const spy = jasmine.createSpy('history$'); + component.history$.subscribe(spy); + + expect(spy).toHaveBeenCalledWith(jasmine.any(MatTableDataSource)); + + const dataSource = spy.calls.mostRecent().args[0] as MatTableDataSource<GameRecord>; + expect(dataSource.data).toEqual([]); + }); + + it('should update history on refresh', () => { + const spy = jasmine.createSpy('history$'); + component.history$.subscribe(spy); + + spy.calls.reset(); + communicationService.getHistory$.calls.reset(); + + component.refreshHistory$.next(); + + expect(spy).toHaveBeenCalledTimes(1); + expect(communicationService.getHistory$).toHaveBeenCalled(); + }); + }); + + describe('clear', () => { + it('should clear history', async () => { + await component.clear(); + + expect(communicationService.clearHistory$).toHaveBeenCalled(); + }); + + it('should refresh history', async () => { + const spy = jasmine.createSpy('history$'); + component.history$.subscribe(spy); + spy.calls.reset(); + + await component.clear(); + + expect(communicationService.getHistory$).toHaveBeenCalled(); + expect(spy).toHaveBeenCalledTimes(1); + }); + }); }); diff --git a/client/src/app/pages/admin/admin-page/history-management/history-management.component.ts b/client/src/app/pages/admin/admin-page/history-management/history-management.component.ts index 394617452156b730b6b32f0892680a888f29ca6a..b1872efa04d7ea4fb320fbcf11a810e1013db008 100644 --- a/client/src/app/pages/admin/admin-page/history-management/history-management.component.ts +++ b/client/src/app/pages/admin/admin-page/history-management/history-management.component.ts @@ -1,15 +1,35 @@ +import { AsyncPipe, NgIf } from '@angular/common'; import { Component } from '@angular/core'; import { MatButtonModule } from '@angular/material/button'; import { MatCardModule } from '@angular/material/card'; import { MatDividerModule } from '@angular/material/divider'; import { MatIconModule } from '@angular/material/icon'; +import { MatTableDataSource } from '@angular/material/table'; import { MatTooltipModule } from '@angular/material/tooltip'; +import { CommunicationService } from '@app/services/communication/communication.service'; +import { BehaviorSubject, lastValueFrom, map, shareReplay, switchMap } from 'rxjs'; +import { HistoryTableComponent } from './history-table/history-table.component'; @Component({ selector: 'app-history-management', standalone: true, - imports: [MatCardModule, MatIconModule, MatDividerModule, MatButtonModule, MatTooltipModule], + imports: [AsyncPipe, NgIf, HistoryTableComponent, MatCardModule, MatIconModule, MatDividerModule, MatButtonModule, MatTooltipModule], templateUrl: './history-management.component.html', styleUrls: ['./history-management.component.scss'], }) -export class HistoryManagementComponent {} +export class HistoryManagementComponent { + refreshHistory$ = new BehaviorSubject<void>(undefined); + + history$ = this.refreshHistory$.pipe( + switchMap(() => this.communicationService.getHistory$()), + map((history) => new MatTableDataSource(history)), + shareReplay(1), + ); + + constructor(private communicationService: CommunicationService) {} + + async clear(): Promise<void> { + await lastValueFrom(this.communicationService.clearHistory$()); + this.refreshHistory$.next(); + } +} diff --git a/client/src/app/pages/admin/admin-page/history-management/history-table/history-table.component.dummy.ts b/client/src/app/pages/admin/admin-page/history-management/history-table/history-table.component.dummy.ts new file mode 100644 index 0000000000000000000000000000000000000000..07bfee8fbfdcac3e3b24d2575dad462e6fb2403b --- /dev/null +++ b/client/src/app/pages/admin/admin-page/history-management/history-table/history-table.component.dummy.ts @@ -0,0 +1,12 @@ +import { Component, Input } from '@angular/core'; +import { MatTableDataSource } from '@angular/material/table'; +import { GameRecord } from '@common/game'; + +@Component({ + selector: 'app-history-table', + standalone: true, + template: '', +}) +export class HistoryTableDummyComponent { + @Input({ required: true }) history: MatTableDataSource<GameRecord>; +} diff --git a/client/src/app/pages/admin/admin-page/history-management/history-table/history-table.component.html b/client/src/app/pages/admin/admin-page/history-management/history-table/history-table.component.html new file mode 100644 index 0000000000000000000000000000000000000000..c7854ce0e39cb7123ae70e3656f681cfd105228d --- /dev/null +++ b/client/src/app/pages/admin/admin-page/history-management/history-table/history-table.component.html @@ -0,0 +1,24 @@ +<table mat-table [dataSource]="history" [trackBy]="trackRecord" matSort matSortActive="startTimestamp" matSortDirection="desc" matSortDisableClear> + <ng-container matColumnDef="startTimestamp"> + <th mat-header-cell *matHeaderCellDef mat-sort-header sortActionDescription="Trier par date">Début de la partie</th> + <td mat-cell *matCellDef="let element">{{ element.startTimestamp | date: 'yyyy-MM-dd HH:mm:ss' }}</td> + </ng-container> + + <ng-container matColumnDef="title"> + <th mat-header-cell *matHeaderCellDef mat-sort-header sortActionDescription="Trier par titre">Nom du jeu</th> + <td mat-cell *matCellDef="let element">{{ element.title }}</td> + </ng-container> + + <ng-container matColumnDef="nPlayers"> + <th mat-header-cell *matHeaderCellDef>Joueurs</th> + <td mat-cell *matCellDef="let element">{{ element.nPlayers }}</td> + </ng-container> + + <ng-container matColumnDef="highestScore"> + <th mat-header-cell *matHeaderCellDef>Meilleur score</th> + <td mat-cell *matCellDef="let element">{{ element.highestScore }}</td> + </ng-container> + + <tr mat-header-row *matHeaderRowDef="displayedColumns; sticky: true"></tr> + <tr mat-row *matRowDef="let row; columns: displayedColumns"></tr> +</table> diff --git a/client/src/app/pages/admin/admin-page/history-management/history-table/history-table.component.scss b/client/src/app/pages/admin/admin-page/history-management/history-table/history-table.component.scss new file mode 100644 index 0000000000000000000000000000000000000000..29ebc40d503b87165cac8752e28a19ea322edc16 --- /dev/null +++ b/client/src/app/pages/admin/admin-page/history-management/history-table/history-table.component.scss @@ -0,0 +1,3 @@ +th[mat-header-cell] { + user-select: none; +} diff --git a/client/src/app/pages/admin/admin-page/history-management/history-table/history-table.component.spec.ts b/client/src/app/pages/admin/admin-page/history-management/history-table/history-table.component.spec.ts new file mode 100644 index 0000000000000000000000000000000000000000..b084179c01450fb86530ea27916114445a7876f0 --- /dev/null +++ b/client/src/app/pages/admin/admin-page/history-management/history-table/history-table.component.spec.ts @@ -0,0 +1,54 @@ +import { ComponentFixture, TestBed } from '@angular/core/testing'; +import { MatTableDataSource } from '@angular/material/table'; +import { NoopAnimationsModule } from '@angular/platform-browser/animations'; +import { GameRecord } from '@common/game'; +import { BehaviorSubject } from 'rxjs'; +import { HistoryTableComponent } from './history-table.component'; + +describe('HistoryTableComponent', () => { + let component: HistoryTableComponent; + let fixture: ComponentFixture<HistoryTableComponent>; + + let history: jasmine.SpyObj<MatTableDataSource<GameRecord>>; + + beforeEach(() => { + history = jasmine.createSpyObj(MatTableDataSource.name, ['connect', 'disconnect'], ['sort']); + history.connect.and.returnValue(new BehaviorSubject<GameRecord[]>([])); + + TestBed.configureTestingModule({ + imports: [HistoryTableComponent, NoopAnimationsModule], + }); + fixture = TestBed.createComponent(HistoryTableComponent); + component = fixture.componentInstance; + component.history = history; + fixture.detectChanges(); + }); + + it('should create', () => { + expect(component).toBeTruthy(); + }); + + describe('ngAfterViewInit', () => { + it('should set sort', () => { + const setSortSpy = Object.getOwnPropertyDescriptor(history, 'sort')?.set as jasmine.Spy; + setSortSpy.calls.reset(); + component.ngAfterViewInit(); + + expect(setSortSpy).toHaveBeenCalled(); + }); + }); + + describe('trackRecord', () => { + it('should return record id', () => { + const record: GameRecord = { _id: 'id', startTimestamp: 0, title: 'title', nPlayers: 0, highestScore: 0 }; + // eslint-disable-next-line no-underscore-dangle + expect(component.trackRecord(0, record)).toBe(record._id); + }); + + it('should return the same value for the same record', () => { + const record: GameRecord = { _id: 'id', startTimestamp: 0, title: 'title', nPlayers: 0, highestScore: 0 }; + // eslint-disable-next-line no-underscore-dangle + expect(component.trackRecord(0, record)).toBe(component.trackRecord(2, record)); + }); + }); +}); diff --git a/client/src/app/pages/admin/admin-page/history-management/history-table/history-table.component.ts b/client/src/app/pages/admin/admin-page/history-management/history-table/history-table.component.ts new file mode 100644 index 0000000000000000000000000000000000000000..3f7aedc7cc2b1fd3409f329eec5f737677f23c73 --- /dev/null +++ b/client/src/app/pages/admin/admin-page/history-management/history-table/history-table.component.ts @@ -0,0 +1,29 @@ +import { DatePipe } from '@angular/common'; +import { AfterViewInit, Component, Input, ViewChild } from '@angular/core'; +import { MatSort, MatSortModule } from '@angular/material/sort'; +import { MatTableDataSource, MatTableModule } from '@angular/material/table'; +import { GameRecord } from '@common/game'; + +@Component({ + selector: 'app-history-table', + standalone: true, + imports: [DatePipe, MatTableModule, MatSortModule], + templateUrl: './history-table.component.html', + styleUrls: ['./history-table.component.scss'], +}) +export class HistoryTableComponent implements AfterViewInit { + @Input({ required: true }) history: MatTableDataSource<GameRecord>; + + @ViewChild(MatSort, { static: true }) sort: MatSort; + + readonly displayedColumns: (keyof GameRecord)[] = ['startTimestamp', 'title', 'nPlayers', 'highestScore']; + + ngAfterViewInit(): void { + this.history.sort = this.sort; + } + + trackRecord(index: number, record: GameRecord): string { + // eslint-disable-next-line no-underscore-dangle -- mongodb + return record._id; + } +} diff --git a/client/src/app/services/communication/communication.service.spec.ts b/client/src/app/services/communication/communication.service.spec.ts index 7719e901499e927ac80e8cf296e06845ff8b7f2f..7ff1366e25379c4c06b4cd9889f4e099fc72e8b7 100644 --- a/client/src/app/services/communication/communication.service.spec.ts +++ b/client/src/app/services/communication/communication.service.spec.ts @@ -272,6 +272,32 @@ describe('CommunicationService', () => { })); }); + describe('getHistory$', () => { + it('should send a GET request to /game/history', fakeAsync(() => { + service.getHistory$().subscribe({ error: fail }); + tick(); + + const req = httpMock.expectOne(`${baseUrl}/game/history`); + expect(req.request.method).toBe('GET'); + expect(req.request.withCredentials).toBeTrue(); + req.flush({}); + tick(); + })); + }); + + describe('clearHistory$', () => { + it('should send a DELETE request to /game/history', fakeAsync(() => { + service.clearHistory$().subscribe({ error: fail }); + tick(); + + const req = httpMock.expectOne(`${baseUrl}/game/history`); + expect(req.request.method).toBe('DELETE'); + expect(req.request.withCredentials).toBeTrue(); + req.flush(null); + tick(); + })); + }); + describe('getQuizDownloadUrl', () => { it('should return the download URL for the given quiz', () => { credentialsService.accessToken = 'bar'; diff --git a/client/src/app/services/communication/communication.service.ts b/client/src/app/services/communication/communication.service.ts index 8a5adb25ef22d605b32a7ff55e38447166cbc90f..c40eb8ca3ffc412c772e8fa0afd7fba7d1fb7f24 100644 --- a/client/src/app/services/communication/communication.service.ts +++ b/client/src/app/services/communication/communication.service.ts @@ -10,6 +10,7 @@ import { QuizAlreadyExistsError } from '@app/errors/quiz/quiz-already-exists.err import { QuizNotFoundError } from '@app/errors/quiz/quiz-not-found.error'; import { CredentialsService } from '@app/services/credentials/credentials.service'; import { AccessToken } from '@common/auth'; +import { GameHistory } from '@common/game'; import { CreateQuestion, StandaloneQuestion } from '@common/question'; import { CreateQuiz, PlayableQuiz, Quiz } from '@common/quiz'; import { MonoTypeOperatorFunction, Observable, of } from 'rxjs'; @@ -67,6 +68,14 @@ export class CommunicationService { ); } + getHistory$(): Observable<GameHistory> { + return this.http.get<GameHistory>(`${this.baseUrl}/game/history`, { withCredentials: true }).pipe(this.handleErrors()); + } + + clearHistory$(): Observable<null> { + return this.http.delete<null>(`${this.baseUrl}/game/history`, { withCredentials: true }).pipe(this.handleErrors()); + } + getQuizzes$(): Observable<Quiz[]> { return this.http.get<Quiz[]>(`${this.baseUrl}/quiz`, { withCredentials: true }).pipe( this.handleErrors(), diff --git a/common/game.ts b/common/game.ts index 19f91ca0f202181e56670474fbb968560e84a62e..55a4a217b4f93e7fa2b585de6a5e060d1c99f964 100644 --- a/common/game.ts +++ b/common/game.ts @@ -9,14 +9,16 @@ export interface Game extends BaseGame { nQuestions: number; } -interface BaseGameHistory extends BaseGame { +interface BaseGameRecord extends BaseGame { startTimestamp: number; nPlayers: number; highestScore: number; } -export interface CreateGameHistory extends BaseGameHistory {} +export interface CreateGameRecord extends BaseGameRecord {} -export interface GameHistory extends BaseGameHistory { +export interface GameRecord extends BaseGameRecord { _id: string; } + +export type GameHistory = GameRecord[]; diff --git a/server/app/game/classes/game-room/game-room.class.spec.ts b/server/app/game/classes/game-room/game-room.class.spec.ts index ee535d190a4fa7aea08286b087e362ada0b95e66..a3512f168d3690148a2e8c456eaef54c7087ccaa 100644 --- a/server/app/game/classes/game-room/game-room.class.spec.ts +++ b/server/app/game/classes/game-room/game-room.class.spec.ts @@ -165,7 +165,7 @@ describe('GameRoom', () => { answers: [{ score: 10 }, { score: 0 }] as Answer[], } as Player); - const statistics = gameRoom.getHistory(); + const statistics = gameRoom.getRecord(); expect(statistics).toEqual({ title: game.title, startTimestamp: 10, @@ -175,7 +175,7 @@ describe('GameRoom', () => { }); it('should throw an error if the game has not ended', () => { - expect(() => gameRoom.getHistory()).toThrowError(WsException); + expect(() => gameRoom.getRecord()).toThrowError(WsException); }); }); diff --git a/server/app/game/classes/game-room/game-room.class.ts b/server/app/game/classes/game-room/game-room.class.ts index 953f57271b43fe09929e5435d912386f2f44b9d5..101b71232a5fb9312bdb676904f871eeed937f10 100644 --- a/server/app/game/classes/game-room/game-room.class.ts +++ b/server/app/game/classes/game-room/game-room.class.ts @@ -1,6 +1,6 @@ import { Room } from '@app/game/classes/room/room.class'; import { GameServerEvent } from '@common/events/game.events'; -import { CreateGameHistory } from '@common/game'; +import { CreateGameRecord } from '@common/game'; import { WsException } from '@nestjs/websockets'; import { Socket } from 'socket.io'; import { DELAY_BEFORE_START_MS, HOST_USERNAME } from './game-room.class.constants'; @@ -25,7 +25,7 @@ export class GameRoom extends Room { return this.endTimestamp !== undefined; } - getHistory(): CreateGameHistory { + getRecord(): CreateGameRecord { if (this.startTimestamp === undefined || this.endTimestamp === undefined) { throw new WsException('Results are not available yet'); } diff --git a/server/app/game/controllers/game.controller.spec.ts b/server/app/game/controllers/game.controller.spec.ts index a6e66a11033d4286c725f7a3d830813972c1a9c0..c7fef9947d3d3113c3213deacaff22be9945a3d5 100644 --- a/server/app/game/controllers/game.controller.spec.ts +++ b/server/app/game/controllers/game.controller.spec.ts @@ -1,6 +1,6 @@ import { GameHistoryService } from '@app/game/services/game-history/game-history.service'; import { GameService } from '@app/game/services/game/game.service'; -import { GameHistory } from '@common/game'; +import { GameRecord } from '@common/game'; import { Test, TestingModule } from '@nestjs/testing'; import * as sinon from 'sinon'; import { GameController } from './game.controller'; @@ -48,7 +48,7 @@ describe('GameController', () => { describe('getHistory', () => { it('should return all played games', async () => { - const expected = [{ _id: '1' }, { _id: '2' }] as GameHistory[]; + const expected = [{ _id: '1' }, { _id: '2' }] as GameRecord[]; gameHistoryService.getAll.resolves(expected); const result = await controller.getHistory(); diff --git a/server/app/game/controllers/game.controller.ts b/server/app/game/controllers/game.controller.ts index 64bc915dd0148a262c532a0223e883bd3993bd85..84984278288103d61960b2f15cb8bcca2c9707c4 100644 --- a/server/app/game/controllers/game.controller.ts +++ b/server/app/game/controllers/game.controller.ts @@ -1,6 +1,6 @@ import { RequireRoles } from '@app/auth/decorators/require-roles/require-roles.decorator'; import { Role } from '@app/auth/enums/roles.enum'; -import { GameHistoryDto } from '@app/game/dto/game-history.dto'; +import { GameRecordDto } from '@app/game/dto/game-record.dto'; import { PlayableQuizDto } from '@app/game/dto/playable-quiz.dto'; import { GameHistoryService } from '@app/game/services/game-history/game-history.service'; import { GameService } from '@app/game/services/game/game.service'; @@ -29,12 +29,12 @@ export class GameController { @ApiOperation({ summary: 'Get game history' }) @ApiOkResponse({ description: 'All played games.', - type: GameHistoryDto, + type: GameRecordDto, isArray: true, }) @RequireRoles(Role.Admin) @Get('history') - async getHistory(): Promise<GameHistoryDto[]> { + async getHistory(): Promise<GameRecordDto[]> { return await this.gameHistoryService.getAll(); } diff --git a/server/app/game/dto/game-history.dto.ts b/server/app/game/dto/game-record.dto.ts similarity index 80% rename from server/app/game/dto/game-history.dto.ts rename to server/app/game/dto/game-record.dto.ts index e59ebea3edb7937f0a61378dc98718b2056f7a53..0006b98a00f6b143df895ee239e27320ba357ef5 100644 --- a/server/app/game/dto/game-history.dto.ts +++ b/server/app/game/dto/game-record.dto.ts @@ -1,8 +1,8 @@ -import { GameHistory } from '@common/game'; +import { GameRecord } from '@common/game'; import { ApiProperty } from '@nestjs/swagger'; import { IsNumber, IsString } from 'class-validator'; -export class GameHistoryDto implements GameHistory { +export class GameRecordDto implements GameRecord { @ApiProperty() @IsString() _id: string; diff --git a/server/app/game/game.module.ts b/server/app/game/game.module.ts index c2d3a26d64f5af2dc322250f7d2bda10dd3500fc..beb1f7b0161aa73127e5ea6bb8c56803773a56a3 100644 --- a/server/app/game/game.module.ts +++ b/server/app/game/game.module.ts @@ -3,7 +3,7 @@ import { Module } from '@nestjs/common'; import { MongooseModule } from '@nestjs/mongoose'; import { GameController } from './controllers/game.controller'; import { GameGateway } from './gateways/game.gateway'; -import { GameHistory, gameHistorySchema } from './schemas/game-history.schema'; +import { GameRecord, gameRecordSchema } from './schemas/game-record.schema'; import { GameHistoryService } from './services/game-history/game-history.service'; import { GameRoomService } from './services/game-room/game-room.service'; import { GameService } from './services/game/game.service'; @@ -13,8 +13,8 @@ import { GameService } from './services/game/game.service'; QuizModule, MongooseModule.forFeature([ { - name: GameHistory.name, - schema: gameHistorySchema, + name: GameRecord.name, + schema: gameRecordSchema, }, ]), ], diff --git a/server/app/game/gateways/game.gateway.spec.ts b/server/app/game/gateways/game.gateway.spec.ts index 5e003aa284641a6ddfa9ede97b502e0169fcb754..904a0933acf5d039d21914a69c34d157ab4b72b3 100644 --- a/server/app/game/gateways/game.gateway.spec.ts +++ b/server/app/game/gateways/game.gateway.spec.ts @@ -294,7 +294,7 @@ describe('GameGateway', () => { expect(gameHistoryService.add.called).toBe(false); }); - it('should add the history if it has ended', () => { + it('should add the record to the history if it has ended', () => { const room = sinon.createStubInstance(GameRoom); sinon.stub(room, 'hasEnded').get(() => true); diff --git a/server/app/game/gateways/game.gateway.ts b/server/app/game/gateways/game.gateway.ts index bf7f5e1fe8b69c399937e314b488d5489052e033..b0a7dad48f3cffd70f04868b6f64ea711750a5d8 100644 --- a/server/app/game/gateways/game.gateway.ts +++ b/server/app/game/gateways/game.gateway.ts @@ -110,7 +110,8 @@ export class GameGateway implements OnGatewayConnection, OnGatewayDisconnect { room.continue(client); if (room.hasEnded) { - this.gameHistoryService.add(room.getHistory()); + const record = room.getRecord(); + this.gameHistoryService.add(record); } } diff --git a/server/app/game/schemas/game-history.schema.ts b/server/app/game/schemas/game-record.schema.ts similarity index 62% rename from server/app/game/schemas/game-history.schema.ts rename to server/app/game/schemas/game-record.schema.ts index 783836b4a1d2c5f218d1e4df5418611d19caecbe..a2bc883881aa80e9bb7a364ca2409a29e08a44c0 100644 --- a/server/app/game/schemas/game-history.schema.ts +++ b/server/app/game/schemas/game-record.schema.ts @@ -1,9 +1,9 @@ -import { GameHistory as IGameHistory } from '@common/game'; +import { GameRecord as IGameHistory } from '@common/game'; import { Prop, Schema, SchemaFactory } from '@nestjs/mongoose'; import { HydratedDocument } from 'mongoose'; @Schema() -export class GameHistory implements IGameHistory { +export class GameRecord implements IGameHistory { @Prop({ type: String, required: true }) title: string; @@ -19,6 +19,6 @@ export class GameHistory implements IGameHistory { _id: string; } -export type GameHistoryDocument = HydratedDocument<GameHistory>; +export type GameRecordDocument = HydratedDocument<GameRecord>; -export const gameHistorySchema = SchemaFactory.createForClass(GameHistory); +export const gameRecordSchema = SchemaFactory.createForClass(GameRecord); diff --git a/server/app/game/services/game-history/game-history.service.spec.ts b/server/app/game/services/game-history/game-history.service.spec.ts index 5b3ddb866c074d2c228a22ac7ed6baba685d989d..59246f52297584cfe8dad0c1ef5ebdc4da0f113c 100644 --- a/server/app/game/services/game-history/game-history.service.spec.ts +++ b/server/app/game/services/game-history/game-history.service.spec.ts @@ -1,4 +1,4 @@ -import { GameHistory, gameHistorySchema } from '@app/game/schemas/game-history.schema'; +import { GameRecord, gameRecordSchema } from '@app/game/schemas/game-record.schema'; import { MongooseModule, getConnectionToken, getModelToken } from '@nestjs/mongoose'; import { Test, TestingModule } from '@nestjs/testing'; import { MongoMemoryServer } from 'mongodb-memory-server'; @@ -9,7 +9,7 @@ const DELAY_BEFORE_CLOSING_CONNECTION = 200; describe('GameHistoryService', () => { let service: GameHistoryService; - let model: Model<GameHistory>; + let model: Model<GameRecord>; let mongoServer: MongoMemoryServer; let connection: Connection; @@ -23,13 +23,13 @@ describe('GameHistoryService', () => { uri: mongoServer.getUri(), }), }), - MongooseModule.forFeature([{ name: GameHistory.name, schema: gameHistorySchema }]), + MongooseModule.forFeature([{ name: GameRecord.name, schema: gameRecordSchema }]), ], providers: [GameHistoryService], }).compile(); service = module.get<GameHistoryService>(GameHistoryService); - model = module.get<Model<GameHistory>>(getModelToken(GameHistory.name)); + model = module.get<Model<GameRecord>>(getModelToken(GameRecord.name)); connection = module.get<Connection>(getConnectionToken()); }); @@ -46,44 +46,44 @@ describe('GameHistoryService', () => { }); describe('getAll', () => { - it('should return all histories', async () => { + it('should return history', async () => { await model.create([ { title: 'Game 1', startTimestamp: Date.now(), nPlayers: 1, highestScore: 10 }, { title: 'Game 2', startTimestamp: Date.now(), nPlayers: 2, highestScore: 20 }, ]); - const histories = await service.getAll(); + const history = await service.getAll(); - expect(histories).toHaveLength(2); + expect(history).toHaveLength(2); }); - it('should return an empty array if there are no played games', async () => { + it('should return an empty array if there are no records', async () => { expect(await service.getAll()).toEqual([]); }); }); describe('add', () => { - it('should add a history to the database', async () => { - const history = { title: 'Game 1', startTimestamp: Date.now(), nPlayers: 2, highestScore: 10 }; + it('should add a record to the history database', async () => { + const record = { title: 'Game 1', startTimestamp: Date.now(), nPlayers: 2, highestScore: 10 }; - const createdHistory = await service.add(history); - expect(createdHistory.toObject()).toMatchObject(history); + const createdRecord = await service.add(record); + expect(createdRecord.toObject()).toMatchObject(record); - const histories = await model.find(); - expect(histories).toHaveLength(1); - expect(histories[0].toObject()).toMatchObject(history); + const history = await model.find(); + expect(history).toHaveLength(1); + expect(history[0].toObject()).toMatchObject(record); }); it('should ignore the _id field', async () => { - const history = { title: 'Game 1', startTimestamp: Date.now(), nPlayers: 2, highestScore: 10, _id: '123' }; + const record = { title: 'Game 1', startTimestamp: Date.now(), nPlayers: 2, highestScore: 10, _id: '123' }; - const createdHistory = await service.add(history); - expect(createdHistory.id).not.toEqual('123'); + const createdRecord = await service.add(record); + expect(createdRecord.id).not.toEqual('123'); }); }); describe('clear', () => { - it('should delete all histories', async () => { + it('should delete history', async () => { await model.create([ { title: 'Game 1', startTimestamp: Date.now(), nPlayers: 1, highestScore: 10 }, { title: 'Game 2', startTimestamp: Date.now(), nPlayers: 2, highestScore: 20 }, @@ -91,9 +91,9 @@ describe('GameHistoryService', () => { await service.clear(); - const histories = await model.find(); - expect(histories).toHaveLength(0); - expect(histories).toEqual([]); + const history = await model.find(); + expect(history).toHaveLength(0); + expect(history).toEqual([]); }); }); }); diff --git a/server/app/game/services/game-history/game-history.service.ts b/server/app/game/services/game-history/game-history.service.ts index 6854f893c511c84c129e799d7009ec639435c330..c006606bb689831453edf4b6fe90df916ebdd962 100644 --- a/server/app/game/services/game-history/game-history.service.ts +++ b/server/app/game/services/game-history/game-history.service.ts @@ -1,23 +1,23 @@ -import { GameHistory, GameHistoryDocument } from '@app/game/schemas/game-history.schema'; -import { CreateGameHistory } from '@common/game'; +import { GameRecord, GameRecordDocument } from '@app/game/schemas/game-record.schema'; +import { CreateGameRecord } from '@common/game'; import { Injectable } from '@nestjs/common'; import { InjectModel } from '@nestjs/mongoose'; import { Model } from 'mongoose'; @Injectable() export class GameHistoryService { - constructor(@InjectModel(GameHistory.name) public historyModel: Model<GameHistory>) {} + constructor(@InjectModel(GameRecord.name) public recordModel: Model<GameRecord>) {} - async getAll(): Promise<GameHistory[]> { - return await this.historyModel.find(); + async getAll(): Promise<GameRecord[]> { + return await this.recordModel.find(); } - async add(gameHistory: CreateGameHistory): Promise<GameHistoryDocument> { - const model = new this.historyModel({ ...gameHistory, _id: undefined }); + async add(gameRecord: CreateGameRecord): Promise<GameRecordDocument> { + const model = new this.recordModel({ ...gameRecord, _id: undefined }); return await model.save(); } async clear(): Promise<void> { - await this.historyModel.deleteMany({}); + await this.recordModel.deleteMany({}); } }