From 318d7f048caf844d7a8997c7dbbed11c81b399e5 Mon Sep 17 00:00:00 2001 From: JIN Date: Tue, 7 Jan 2025 15:21:47 +0900 Subject: [PATCH 01/47] =?UTF-8?q?=E2=99=BB=EF=B8=8F=20refactor:=20?= =?UTF-8?q?=EA=B2=BD=EB=A1=9C=EB=A5=BC=20=EC=83=81=EB=8C=80=20=EA=B2=BD?= =?UTF-8?q?=EB=A1=9C=EB=A1=9C=20=EC=88=98=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- server/src/drawing/drawing.repository.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/server/src/drawing/drawing.repository.ts b/server/src/drawing/drawing.repository.ts index 817cf9be..61e6e2e1 100644 --- a/server/src/drawing/drawing.repository.ts +++ b/server/src/drawing/drawing.repository.ts @@ -1,5 +1,5 @@ import { Injectable } from '@nestjs/common'; -import { RedisService } from 'src/redis/redis.service'; +import { RedisService } from '../redis/redis.service'; @Injectable() export class DrawingRepository { From d6214690e118c0570f999652e8503f94d1dcfe92 Mon Sep 17 00:00:00 2001 From: JIN Date: Tue, 7 Jan 2025 15:30:32 +0900 Subject: [PATCH 02/47] =?UTF-8?q?=F0=9F=A7=AA=20test:=20drawingRepository?= =?UTF-8?q?=20=EB=8B=A8=EC=9C=84=20=ED=85=8C=EC=8A=A4=ED=8A=B8?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- server/src/drawing/drawing.repository.spec.ts | 63 +++++++++++++++++++ 1 file changed, 63 insertions(+) create mode 100644 server/src/drawing/drawing.repository.spec.ts diff --git a/server/src/drawing/drawing.repository.spec.ts b/server/src/drawing/drawing.repository.spec.ts new file mode 100644 index 00000000..dea542e1 --- /dev/null +++ b/server/src/drawing/drawing.repository.spec.ts @@ -0,0 +1,63 @@ +import { Test, TestingModule } from '@nestjs/testing'; +import { DrawingRepository } from './drawing.repository'; +import { RedisService } from '../redis/redis.service'; + +describe('DrawingRepository 단위 테스트', () => { + let repository: DrawingRepository; + let redisService: RedisService; + + beforeEach(async () => { + const module: TestingModule = await Test.createTestingModule({ + providers: [ + DrawingRepository, + { + provide: RedisService, + useValue: { + exists: jest.fn(), + }, + }, + ], + }).compile(); + + repository = module.get(DrawingRepository); + redisService = module.get(RedisService); + }); + + describe('existsRoom', () => { + it('room이 존재한다면 true 반환', async () => { + jest.spyOn(redisService, 'exists').mockResolvedValue(1); + + const exists = await repository.existsRoom('success'); + + expect(exists).toBe(true); + expect(redisService.exists).toHaveBeenCalledWith('room:success'); + }); + + it('room이 존재하지 않는다면 false 반환', async () => { + jest.spyOn(redisService, 'exists').mockResolvedValue(0); + + const exists = await repository.existsRoom('failed'); + + expect(exists).toBe(false); + }); + }); + + describe('existsPlayer', () => { + it('player가 존재한다면 true 반환', async () => { + jest.spyOn(redisService, 'exists').mockResolvedValue(1); + + const exists = await repository.existsPlayer('success', 'player'); + + expect(exists).toBe(true); + expect(redisService.exists).toHaveBeenCalledWith('room:success:player:player'); + }); + + it('player가 존재하지 않는다면 false 반환', async () => { + jest.spyOn(redisService, 'exists').mockResolvedValue(0); + + const exists = await repository.existsPlayer('failed', 'non'); + + expect(exists).toBe(false); + }); + }); +}); From 4539eb7e926439833d66fb0b19c38fff992a7e40 Mon Sep 17 00:00:00 2001 From: JIN Date: Tue, 7 Jan 2025 16:00:35 +0900 Subject: [PATCH 03/47] =?UTF-8?q?=F0=9F=A7=AA=20test:=20drawingRepository?= =?UTF-8?q?=20=EB=8B=A8=EC=9C=84=20=ED=85=8C=EC=8A=A4=ED=8A=B8?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- server/src/drawing/drawing.service.spec.ts | 62 ++++++++++++++++++++++ 1 file changed, 62 insertions(+) create mode 100644 server/src/drawing/drawing.service.spec.ts diff --git a/server/src/drawing/drawing.service.spec.ts b/server/src/drawing/drawing.service.spec.ts new file mode 100644 index 00000000..332d8614 --- /dev/null +++ b/server/src/drawing/drawing.service.spec.ts @@ -0,0 +1,62 @@ +import { Test, TestingModule } from '@nestjs/testing'; +import { DrawingRepository } from './drawing.repository'; +import { DrawingService } from './drawing.service'; + +describe('DrawingService 단위 테스트', () => { + let repository: DrawingRepository; + let service: DrawingService; + + beforeEach(async () => { + const module: TestingModule = await Test.createTestingModule({ + providers: [ + DrawingService, + { + provide: DrawingRepository, + useValue: { + existsRoom: jest.fn(), + existsPlayer: jest.fn(), + }, + }, + ], + }).compile(); + + repository = module.get(DrawingRepository); + service = module.get(DrawingService); + }); + + describe('existsRoom', () => { + it('room이 존재한다면 true 반환', async () => { + jest.spyOn(repository, 'existsRoom').mockResolvedValue(true); + + const exists = await service.existsRoom('success'); + + expect(exists).toBe(true); + }); + + it('room이 존재하지 않는다면 false 반환', async () => { + jest.spyOn(repository, 'existsRoom').mockResolvedValue(false); + + const exists = await service.existsRoom('failed'); + + expect(exists).toBe(false); + }); + }); + + describe('existsPlayer', () => { + it('player가 존재한다면 true 반환', async () => { + jest.spyOn(repository, 'existsPlayer').mockResolvedValue(true); + + const exists = await service.existsPlayer('success', 'player'); + + expect(exists).toBe(true); + }); + + it('player가 존재하지 않는다면 false 반환', async () => { + jest.spyOn(repository, 'existsPlayer').mockResolvedValue(false); + + const exists = await service.existsPlayer('failed', 'non'); + + expect(exists).toBe(false); + }); + }); +}); From 0076d17cf75a3f844c66e3c49257d1c9e7adaba9 Mon Sep 17 00:00:00 2001 From: JIN Date: Tue, 7 Jan 2025 18:58:28 +0900 Subject: [PATCH 04/47] =?UTF-8?q?=E2=99=BB=EF=B8=8F=20refactor:=20?= =?UTF-8?q?=EC=83=81=EB=8C=80=20=EA=B2=BD=EB=A1=9C=EB=A1=9C=20=EC=88=98?= =?UTF-8?q?=EC=A0=95,=20async/await=20=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- server/src/drawing/drawing.gateway.ts | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/server/src/drawing/drawing.gateway.ts b/server/src/drawing/drawing.gateway.ts index c369c519..8cfb133b 100644 --- a/server/src/drawing/drawing.gateway.ts +++ b/server/src/drawing/drawing.gateway.ts @@ -8,8 +8,8 @@ import { WebSocketServer, } from '@nestjs/websockets'; import { Server, Socket } from 'socket.io'; -import { BadRequestException, PlayerNotFoundException, RoomNotFoundException } from 'src/exceptions/game.exception'; -import { WsExceptionFilter } from 'src/filters/ws-exception.filter'; +import { BadRequestException, PlayerNotFoundException, RoomNotFoundException } from '../exceptions/game.exception'; +import { WsExceptionFilter } from '../filters/ws-exception.filter'; import { DrawingService } from './drawing.service'; @WebSocketGateway({ @@ -23,15 +23,15 @@ export class DrawingGateway implements OnGatewayConnection { constructor(private readonly drawingService: DrawingService) {} - handleConnection(client: Socket) { + async handleConnection(client: Socket) { const roomId = client.handshake.auth.roomId; const playerId = client.handshake.auth.playerId; if (!roomId || !playerId) throw new BadRequestException('Room ID and Player ID are required'); - const roomExists = this.drawingService.existsRoom(roomId); + const roomExists = await this.drawingService.existsRoom(roomId); if (!roomExists) throw new RoomNotFoundException('Room not found'); - const playerExists = this.drawingService.existsPlayer(roomId, playerId); + const playerExists = await this.drawingService.existsPlayer(roomId, playerId); if (!playerExists) throw new PlayerNotFoundException('Player not found in room'); client.data.roomId = roomId; From e5092074926cb507ab3fe6868541b92b33d94f6a Mon Sep 17 00:00:00 2001 From: JIN Date: Tue, 7 Jan 2025 18:59:44 +0900 Subject: [PATCH 05/47] =?UTF-8?q?=F0=9F=A7=AA=20test:=20drawingGateway=20?= =?UTF-8?q?=EB=8B=A8=EC=9C=84=20=ED=85=8C=EC=8A=A4=ED=8A=B8?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- server/src/drawing/drawing.gateway.spec.ts | 100 ++++++++++++++++++++- 1 file changed, 96 insertions(+), 4 deletions(-) diff --git a/server/src/drawing/drawing.gateway.spec.ts b/server/src/drawing/drawing.gateway.spec.ts index ee5ae08a..2fd4f1db 100644 --- a/server/src/drawing/drawing.gateway.spec.ts +++ b/server/src/drawing/drawing.gateway.spec.ts @@ -1,18 +1,110 @@ import { Test, TestingModule } from '@nestjs/testing'; import { DrawingGateway } from './drawing.gateway'; +import { DrawingService } from './drawing.service'; +import { BadRequestException, PlayerNotFoundException, RoomNotFoundException } from '../exceptions/game.exception'; +import { Socket } from 'socket.io'; -describe('DrawingGateway', () => { +describe('DrawingGateway 단위 테스트', () => { let gateway: DrawingGateway; + let service: DrawingService; + let client: Socket; beforeEach(async () => { const module: TestingModule = await Test.createTestingModule({ - providers: [DrawingGateway], + providers: [ + DrawingGateway, + { + provide: DrawingService, + useValue: { + existsRoom: jest.fn(), + existsPlayer: jest.fn(), + }, + }, + ], }).compile(); gateway = module.get(DrawingGateway); + service = module.get(DrawingService); + client = { + handshake: { + auth: { roomId: '', playerId: '' }, + }, + data: {}, + join: jest.fn(), + to: jest.fn().mockReturnValue({ + emit: jest.fn(), + }), + } as any; }); - it('should be defined', () => { - expect(gateway).toBeDefined(); + describe('handleConnection', () => { + it('roomId 또는 playerId의 값이 존재하지 않는 경우', async () => { + const authInfo = [ + { roomId: '', playerId: '' }, + { roomId: 'room1', playerId: '' }, + { roomId: '', playerId: 'player1' }, + ]; + + for (const auth of authInfo) { + client.handshake.auth = auth; + await expect(gateway.handleConnection(client)).rejects.toThrowError( + new BadRequestException('Room ID and Player ID are required'), + ); + } + }); + + it('room이 redis 내에 존재하지 않는 경우', async () => { + client.handshake.auth = { roomId: 'room1', playerId: 'player1' }; + jest.spyOn(service, 'existsRoom').mockResolvedValue(false); + jest.spyOn(service, 'existsPlayer').mockResolvedValue(true); + + await expect(gateway.handleConnection(client)).rejects.toThrowError(new RoomNotFoundException('Room not found')); + }); + + it('player가 redis 내에 존재하지 않는 경우', async () => { + client.handshake.auth = { roomId: 'room1', playerId: 'player1' }; + jest.spyOn(service, 'existsRoom').mockResolvedValue(true); + jest.spyOn(service, 'existsPlayer').mockResolvedValue(false); + + await expect(gateway.handleConnection(client)).rejects.toThrowError( + new PlayerNotFoundException('Player not found in room'), + ); + }); + + it('room과 player가 정상적으로 존재하는 경우', async () => { + client.handshake.auth = { roomId: 'room1', playerId: 'player1' }; + jest.spyOn(service, 'existsRoom').mockResolvedValue(true); + jest.spyOn(service, 'existsPlayer').mockResolvedValue(true); + + await gateway.handleConnection(client); + + expect(client.join).toHaveBeenCalledWith('room1'); + expect(client.data.roomId).toBe('room1'); + expect(client.data.playerId).toBe('player1'); + }); + }); + + describe('handleDraw', () => { + it('roomId 값이 존재하지 않는 경우', async () => { + client.data = {}; + const data = { drawingData: {} }; + + await expect(gateway.handleDraw(client, data)).rejects.toThrowError( + new BadRequestException('Room ID is required'), + ); + }); + + it('should emit drawUpdated event to the room', async () => { + client.data = { roomId: 'room1', playerId: 'player1' }; + const data = { drawingData: { pos: 56, fillColor: { R: 0, G: 0, B: 0, A: 0 } } }; + + await gateway.handleDraw(client, data); + + expect(client.to).toHaveBeenCalledWith('room1'); + expect(client.to('room1').emit).toHaveBeenCalledWith('drawUpdated', { + playerId: 'player1', + drawingData: data.drawingData, + }); + }); }); }); From bc9a0b4516d57a1a324dc10f231c6e0a1479ef94 Mon Sep 17 00:00:00 2001 From: JIN Date: Wed, 8 Jan 2025 12:12:41 +0900 Subject: [PATCH 06/47] =?UTF-8?q?=F0=9F=A7=AA=20test:=20description?= =?UTF-8?q?=EC=9D=84=20=ED=95=9C=EA=B8=80=EB=A1=9C=20=EB=B3=80=EA=B2=BD?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- server/src/drawing/drawing.gateway.spec.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/server/src/drawing/drawing.gateway.spec.ts b/server/src/drawing/drawing.gateway.spec.ts index 2fd4f1db..024bd979 100644 --- a/server/src/drawing/drawing.gateway.spec.ts +++ b/server/src/drawing/drawing.gateway.spec.ts @@ -94,7 +94,7 @@ describe('DrawingGateway 단위 테스트', () => { ); }); - it('should emit drawUpdated event to the room', async () => { + it('정상적으로 그림이 그려지는 경우', async () => { client.data = { roomId: 'room1', playerId: 'player1' }; const data = { drawingData: { pos: 56, fillColor: { R: 0, G: 0, B: 0, A: 0 } } }; From 4e18673633b0ef1e54db7326736acd53dfb29f4e Mon Sep 17 00:00:00 2001 From: JIN Date: Wed, 8 Jan 2025 21:44:00 +0900 Subject: [PATCH 07/47] =?UTF-8?q?=F0=9F=A7=AA=20test:=20drawingGateway=20?= =?UTF-8?q?=ED=86=B5=ED=95=A9=20=ED=85=8C=EC=8A=A4=ED=8A=B8?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- server/docker-compose.yml | 7 + .../drawing.gateway.integration.spec.ts | 125 ++++++++++++++++++ server/src/redis/redis.service.ts | 5 + 3 files changed, 137 insertions(+) create mode 100644 server/docker-compose.yml create mode 100644 server/src/drawing/drawing.gateway.integration.spec.ts diff --git a/server/docker-compose.yml b/server/docker-compose.yml new file mode 100644 index 00000000..0aa283ec --- /dev/null +++ b/server/docker-compose.yml @@ -0,0 +1,7 @@ +version: '3' +services: + redis: + image: redis:latest + container_name: redis_test + ports: + - "6379:6379" \ No newline at end of file diff --git a/server/src/drawing/drawing.gateway.integration.spec.ts b/server/src/drawing/drawing.gateway.integration.spec.ts new file mode 100644 index 00000000..1c6479df --- /dev/null +++ b/server/src/drawing/drawing.gateway.integration.spec.ts @@ -0,0 +1,125 @@ +import { Test, TestingModule } from '@nestjs/testing'; +import { ConfigService } from '@nestjs/config'; +import { Socket } from 'socket.io'; +import { RedisService } from '../redis/redis.service'; +import { DrawingGateway } from './drawing.gateway'; +import { DrawingService } from './drawing.service'; +import { DrawingRepository } from './drawing.repository'; +import { BadRequestException, PlayerNotFoundException, RoomNotFoundException } from '../exceptions/game.exception'; + +describe('DrawingGateway 통합 테스트', () => { + let gateway: DrawingGateway; + let service: DrawingService; + let redisService: RedisService; + let client: Socket; + + beforeAll(async () => { + const module: TestingModule = await Test.createTestingModule({ + providers: [ + DrawingGateway, + DrawingService, + DrawingRepository, + RedisService, + { + provide: ConfigService, + useValue: { + get: jest.fn().mockImplementation((key: string) => { + if (key === 'REDIS_HOST') return 'localhost'; + if (key === 'REDIS_PORT') return '6379'; + return null; + }), + }, + }, + ], + }).compile(); + + gateway = module.get(DrawingGateway); + service = module.get(DrawingService); + redisService = module.get(RedisService); + }); + + beforeEach(async () => { + client = { + handshake: { + auth: { roomId: '', playerId: '' }, + }, + data: {}, + join: jest.fn(), + to: jest.fn().mockReturnValue({ + emit: jest.fn(), + }), + } as any; + + await redisService.hset('room:room1', { roomId: 'room1' }); + await redisService.hset('room:room1:player:player1', { playerId: 'player1' }); + }); + + // 테스트가 수행될 때마다 DB를 비워줌 + afterEach(async () => { + await redisService.flushAll(); + }); + + describe('handleConnection', () => { + it('roomId 또는 playerId의 값이 존재하지 않는 경우', async () => { + const authInfo = [ + { roomId: '', playerId: '' }, + { roomId: 'room1', playerId: '' }, + { roomId: '', playerId: 'player1' }, + ]; + + for (const auth of authInfo) { + client.handshake.auth = auth; + await expect(gateway.handleConnection(client)).rejects.toThrowError( + new BadRequestException('Room ID and Player ID are required'), + ); + } + }); + + it('room이 redis 내에 존재하지 않는 경우', async () => { + client.handshake.auth = { roomId: 'failed-room', playerId: 'player1' }; + + await expect(gateway.handleConnection(client)).rejects.toThrowError(new RoomNotFoundException('Room not found')); + }); + + it('player가 redis 내에 존재하지 않는 경우', async () => { + client.handshake.auth = { roomId: 'room1', playerId: 'failed-player' }; + + await expect(gateway.handleConnection(client)).rejects.toThrowError( + new PlayerNotFoundException('Player not found in room'), + ); + }); + + it('room과 player가 정상적으로 존재하는 경우', async () => { + client.handshake.auth = { roomId: 'room1', playerId: 'player1' }; + await gateway.handleConnection(client); + + expect(client.join).toHaveBeenCalledWith('room1'); + expect(client.data.roomId).toBe('room1'); + expect(client.data.playerId).toBe('player1'); + }); + }); + + describe('handleDraw', () => { + it('roomId 값이 존재하지 않는 경우', async () => { + client.data = {}; + const data = { drawingData: {} }; + + await expect(gateway.handleDraw(client, data)).rejects.toThrowError( + new BadRequestException('Room ID is required'), + ); + }); + + it('정상적으로 그림이 그려지는 경우', async () => { + client.data = { roomId: 'room1', playerId: 'player1' }; + const data = { drawingData: { pos: 56, fillColor: { R: 0, G: 0, B: 0, A: 0 } } }; + + await gateway.handleDraw(client, data); + + expect(client.to).toHaveBeenCalledWith('room1'); + expect(client.to('room1').emit).toHaveBeenCalledWith('drawUpdated', { + playerId: 'player1', + drawingData: data.drawingData, + }); + }); + }); +}); diff --git a/server/src/redis/redis.service.ts b/server/src/redis/redis.service.ts index 6059fb53..5fc98eb1 100644 --- a/server/src/redis/redis.service.ts +++ b/server/src/redis/redis.service.ts @@ -55,4 +55,9 @@ export class RedisService { multi() { return this.redis.multi(); } + + // 원활한 테스트 진행을 위해 redis 내 저장된 값을 지워주는 코드 추가 + async flushAll() { + await this.redis.flushall(); + } } From e29bb64c075d2bc5bb1a1746fef7bedbaa2ea457 Mon Sep 17 00:00:00 2001 From: JIN Date: Wed, 8 Jan 2025 22:19:45 +0900 Subject: [PATCH 08/47] =?UTF-8?q?=F0=9F=90=9B=20fix:=20=ED=85=8C=EC=8A=A4?= =?UTF-8?q?=ED=8A=B8=EA=B0=80=20=EC=A2=85=EB=A3=8C=EB=90=98=EC=97=88?= =?UTF-8?q?=EC=9D=8C=EC=97=90=EB=8F=84=20=EC=8B=A4=ED=96=89=EC=9D=B4=20?= =?UTF-8?q?=EB=A9=88=EC=B6=94=EC=A7=80=20=EC=95=8A=EB=8A=94=20=ED=98=84?= =?UTF-8?q?=EC=83=81=20=EC=88=98=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- server/src/drawing/drawing.gateway.integration.spec.ts | 5 +++++ server/src/redis/redis.service.ts | 4 ++++ 2 files changed, 9 insertions(+) diff --git a/server/src/drawing/drawing.gateway.integration.spec.ts b/server/src/drawing/drawing.gateway.integration.spec.ts index 1c6479df..729026d0 100644 --- a/server/src/drawing/drawing.gateway.integration.spec.ts +++ b/server/src/drawing/drawing.gateway.integration.spec.ts @@ -59,6 +59,11 @@ describe('DrawingGateway 통합 테스트', () => { await redisService.flushAll(); }); + // 테스트가 종료되면 Redis를 종료 + afterAll(() => { + redisService.quit(); + }); + describe('handleConnection', () => { it('roomId 또는 playerId의 값이 존재하지 않는 경우', async () => { const authInfo = [ diff --git a/server/src/redis/redis.service.ts b/server/src/redis/redis.service.ts index 5fc98eb1..3611ade4 100644 --- a/server/src/redis/redis.service.ts +++ b/server/src/redis/redis.service.ts @@ -60,4 +60,8 @@ export class RedisService { async flushAll() { await this.redis.flushall(); } + + quit() { + this.redis.quit(); + } } From 22f1024128da5bd5846fcacaf67ab84aa7217de5 Mon Sep 17 00:00:00 2001 From: JIN Date: Wed, 8 Jan 2025 22:20:35 +0900 Subject: [PATCH 09/47] =?UTF-8?q?=F0=9F=A7=B9=20chore:=20=EB=8F=84?= =?UTF-8?q?=EC=BB=A4=20=EC=BB=A8=ED=85=8C=EC=9D=B4=EB=84=88=20=EC=8B=A4?= =?UTF-8?q?=ED=96=89=EA=B3=BC=20=EC=A2=85=EB=A3=8C=EB=A5=BC=20=ED=8F=AC?= =?UTF-8?q?=ED=95=A8=ED=95=9C=20=ED=85=8C=EC=8A=A4=ED=8A=B8=20=EC=8B=A4?= =?UTF-8?q?=ED=96=89=20=EC=8A=A4=ED=81=AC=EB=A6=BD=ED=8A=B8=20=EC=9E=91?= =?UTF-8?q?=EC=84=B1?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- server/src/test-docker.sh | 13 +++++++++++++ 1 file changed, 13 insertions(+) create mode 100644 server/src/test-docker.sh diff --git a/server/src/test-docker.sh b/server/src/test-docker.sh new file mode 100644 index 00000000..e2845a73 --- /dev/null +++ b/server/src/test-docker.sh @@ -0,0 +1,13 @@ +#!/bin/bash + +# Redis Container 실행 +docker-compose up -d + +# Redis 내 데이터 초기화 +docker exec redis_test redis-cli FLUSHALL + +# 테스트 실행 +npx jest drawing.gateway.integration.spec.ts + +# 테스트 종료 후 Docker Container 삭제 +docker-compose down \ No newline at end of file From 78a196deb06a6efb8d3fb1978e2590e2841c3153 Mon Sep 17 00:00:00 2001 From: gimseonghwan Date: Wed, 8 Jan 2025 23:24:03 +0900 Subject: [PATCH 10/47] =?UTF-8?q?=F0=9F=8F=97=EF=B8=8F=20build(package?= =?UTF-8?q?=EC=97=90=20jest-mock=20=EC=B6=94=EA=B0=80):=20jest-mock=20?= =?UTF-8?q?=EC=B6=94=EA=B0=80=EB=A1=9C=20=EB=B3=B4=EB=8B=A4=20=EC=89=AC?= =?UTF-8?q?=EC=9A=B4=20=EB=AA=A8=ED=82=B9=20=EC=82=AC=EC=9A=A9=20=EA=B0=80?= =?UTF-8?q?=EB=8A=A5?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- pnpm-lock.yaml | 62 ++++++++++++++++++++++++++++++++++++++------------ 1 file changed, 48 insertions(+), 14 deletions(-) diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 790d0233..da12e9b5 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -223,6 +223,9 @@ importers: ioredis: specifier: ^5.4.1 version: 5.4.1 + jest-mock: + specifier: ^29.7.0 + version: 29.7.0 reflect-metadata: specifier: ^0.2.0 version: 0.2.2 @@ -5779,7 +5782,7 @@ snapshots: '@jest/console@29.7.0': dependencies: '@jest/types': 29.6.3 - '@types/node': 20.17.6 + '@types/node': 22.9.1 chalk: 4.1.2 jest-message-util: 29.7.0 jest-util: 29.7.0 @@ -5792,14 +5795,14 @@ snapshots: '@jest/test-result': 29.7.0 '@jest/transform': 29.7.0 '@jest/types': 29.6.3 - '@types/node': 20.17.6 + '@types/node': 22.9.1 ansi-escapes: 4.3.2 chalk: 4.1.2 ci-info: 3.9.0 exit: 0.1.2 graceful-fs: 4.2.11 jest-changed-files: 29.7.0 - jest-config: 29.7.0(@types/node@20.17.6)(ts-node@10.9.2(@types/node@20.17.6)(typescript@5.6.3)) + jest-config: 29.7.0(@types/node@22.9.1)(ts-node@10.9.2(@types/node@20.17.6)(typescript@5.6.3)) jest-haste-map: 29.7.0 jest-message-util: 29.7.0 jest-regex-util: 29.6.3 @@ -5864,7 +5867,7 @@ snapshots: '@jest/transform': 29.7.0 '@jest/types': 29.6.3 '@jridgewell/trace-mapping': 0.3.25 - '@types/node': 20.17.6 + '@types/node': 22.9.1 chalk: 4.1.2 collect-v8-coverage: 1.0.2 exit: 0.1.2 @@ -5934,7 +5937,7 @@ snapshots: '@jest/schemas': 29.6.3 '@types/istanbul-lib-coverage': 2.0.6 '@types/istanbul-reports': 3.0.4 - '@types/node': 20.17.6 + '@types/node': 22.9.1 '@types/yargs': 17.0.33 chalk: 4.1.2 @@ -6541,7 +6544,7 @@ snapshots: '@types/connect@3.4.38': dependencies: - '@types/node': 20.17.6 + '@types/node': 22.9.1 '@types/cookie@0.4.1': {} @@ -6549,7 +6552,7 @@ snapshots: '@types/cors@2.8.17': dependencies: - '@types/node': 20.17.6 + '@types/node': 22.9.1 '@types/doctrine@0.0.9': {} @@ -6664,7 +6667,7 @@ snapshots: '@types/send@0.17.4': dependencies: '@types/mime': 1.3.5 - '@types/node': 20.17.6 + '@types/node': 22.9.1 '@types/serve-static@1.15.7': dependencies: @@ -7690,7 +7693,7 @@ snapshots: dependencies: '@types/cookie': 0.4.1 '@types/cors': 2.8.17 - '@types/node': 20.17.6 + '@types/node': 22.9.1 accepts: 1.3.8 base64id: 2.0.0 cookie: 0.7.2 @@ -8930,6 +8933,37 @@ snapshots: - babel-plugin-macros - supports-color + jest-config@29.7.0(@types/node@22.9.1)(ts-node@10.9.2(@types/node@20.17.6)(typescript@5.6.3)): + dependencies: + '@babel/core': 7.26.0 + '@jest/test-sequencer': 29.7.0 + '@jest/types': 29.6.3 + babel-jest: 29.7.0(@babel/core@7.26.0) + chalk: 4.1.2 + ci-info: 3.9.0 + deepmerge: 4.3.1 + glob: 7.2.3 + graceful-fs: 4.2.11 + jest-circus: 29.7.0 + jest-environment-node: 29.7.0 + jest-get-type: 29.6.3 + jest-regex-util: 29.6.3 + jest-resolve: 29.7.0 + jest-runner: 29.7.0 + jest-util: 29.7.0 + jest-validate: 29.7.0 + micromatch: 4.0.8 + parse-json: 5.2.0 + pretty-format: 29.7.0 + slash: 3.0.0 + strip-json-comments: 3.1.1 + optionalDependencies: + '@types/node': 22.9.1 + ts-node: 10.9.2(@types/node@20.17.6)(typescript@5.6.3) + transitivePeerDependencies: + - babel-plugin-macros + - supports-color + jest-diff@29.7.0: dependencies: chalk: 4.1.2 @@ -8964,7 +8998,7 @@ snapshots: dependencies: '@jest/types': 29.6.3 '@types/graceful-fs': 4.1.9 - '@types/node': 20.17.6 + '@types/node': 22.9.1 anymatch: 3.1.3 fb-watchman: 2.0.2 graceful-fs: 4.2.11 @@ -9038,7 +9072,7 @@ snapshots: '@jest/test-result': 29.7.0 '@jest/transform': 29.7.0 '@jest/types': 29.6.3 - '@types/node': 20.17.6 + '@types/node': 22.9.1 chalk: 4.1.2 emittery: 0.13.1 graceful-fs: 4.2.11 @@ -9066,7 +9100,7 @@ snapshots: '@jest/test-result': 29.7.0 '@jest/transform': 29.7.0 '@jest/types': 29.6.3 - '@types/node': 20.17.6 + '@types/node': 22.9.1 chalk: 4.1.2 cjs-module-lexer: 1.4.1 collect-v8-coverage: 1.0.2 @@ -9112,7 +9146,7 @@ snapshots: jest-util@29.7.0: dependencies: '@jest/types': 29.6.3 - '@types/node': 20.17.6 + '@types/node': 22.9.1 chalk: 4.1.2 ci-info: 3.9.0 graceful-fs: 4.2.11 @@ -9131,7 +9165,7 @@ snapshots: dependencies: '@jest/test-result': 29.7.0 '@jest/types': 29.6.3 - '@types/node': 20.17.6 + '@types/node': 22.9.1 ansi-escapes: 4.3.2 chalk: 4.1.2 emittery: 0.13.1 From 5c26a81ca187edb34f5bff716b0b4b0cb24e2e92 Mon Sep 17 00:00:00 2001 From: gimseonghwan Date: Wed, 8 Jan 2025 23:39:37 +0900 Subject: [PATCH 11/47] =?UTF-8?q?=F0=9F=A7=AAtest(chat.service.spec.ts):?= =?UTF-8?q?=20chat=20service=20=ED=85=8C=EC=8A=A4=ED=8A=B8=20=EC=B6=94?= =?UTF-8?q?=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- server/src/chat/chat.service.spec.ts | 13 +++++++------ 1 file changed, 7 insertions(+), 6 deletions(-) diff --git a/server/src/chat/chat.service.spec.ts b/server/src/chat/chat.service.spec.ts index 110cd7d3..285251e5 100644 --- a/server/src/chat/chat.service.spec.ts +++ b/server/src/chat/chat.service.spec.ts @@ -1,18 +1,19 @@ import { Test, TestingModule } from '@nestjs/testing'; import { ChatService } from './chat.service'; +import { ChatRepository } from './chat.repository'; +import { ModuleMocker } from 'jest-mock'; + +const moduleMocker = new ModuleMocker(global); describe('ChatService', () => { - let service: ChatService; + let chatService: ChatService; + let chatRepository: ChatRepository; beforeEach(async () => { const module: TestingModule = await Test.createTestingModule({ providers: [ChatService], }).compile(); - service = module.get(ChatService); - }); - - it('should be defined', () => { - expect(service).toBeDefined(); + chatService = module.get(ChatService); }); }); From b06fb06446fec1caa3afe10ec284702e855c5e14 Mon Sep 17 00:00:00 2001 From: gimseonghwan Date: Thu, 9 Jan 2025 00:19:15 +0900 Subject: [PATCH 12/47] =?UTF-8?q?=F0=9F=A7=AAtest(chat.service.spec.ts):?= =?UTF-8?q?=20chat=20service=20=ED=85=8C=EC=8A=A4=ED=8A=B8=20=EC=B6=94?= =?UTF-8?q?=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- server/src/chat/chat.service.spec.ts | 58 +++++++++++++++++++++++++--- 1 file changed, 52 insertions(+), 6 deletions(-) diff --git a/server/src/chat/chat.service.spec.ts b/server/src/chat/chat.service.spec.ts index 285251e5..feb53c63 100644 --- a/server/src/chat/chat.service.spec.ts +++ b/server/src/chat/chat.service.spec.ts @@ -1,19 +1,65 @@ import { Test, TestingModule } from '@nestjs/testing'; import { ChatService } from './chat.service'; import { ChatRepository } from './chat.repository'; -import { ModuleMocker } from 'jest-mock'; - -const moduleMocker = new ModuleMocker(global); +import { describe } from 'node:test'; +import { BadRequestException, PlayerNotFoundException } from '../exceptions/game.exception'; +import { Player } from '../common/types/game.types'; +import { PlayerRole, PlayerStatus } from '../common/enums/game.status.enum'; describe('ChatService', () => { let chatService: ChatService; - let chatRepository: ChatRepository; + + const mockChatRepository = { + getPlayer: jest.fn(), + existsRoom: jest.fn(), + }; beforeEach(async () => { const module: TestingModule = await Test.createTestingModule({ - providers: [ChatService], + providers: [ChatService, { provide: ChatRepository, useValue: mockChatRepository }], }).compile(); - chatService = module.get(ChatService); + chatService = module.get(ChatService); + }); + + afterEach(() => { + jest.clearAllMocks(); + }); + + describe('sendMessage 테스트', async () => { + it('메시지가 공백일 때', async () => { + await expect(async () => { + await chatService.sendMessage('room1', 'player1', ''); + }).rejects.toThrowError(BadRequestException); + }); + + it('플레이어가 존재하지 않을 때', async () => { + mockChatRepository.getPlayer.mockResolvedValue(null); + + await expect(async () => { + await chatService.sendMessage('room1', 'player1', 'hello world'); + }).rejects.toThrowError(PlayerNotFoundException); + + // 에러 캐치로 인해 순서를 바꿔서 배치 + expect(mockChatRepository.getPlayer).toHaveBeenCalled(); + }); + + it('플레이어가 정상적으로 존재할 때', async () => { + const player: Player = { + playerId: 'player1', + role: PlayerRole.GUESSER, + status: PlayerStatus.PLAYING, + nickname: 'player', + profileImage: null, + score: 10, + }; + mockChatRepository.getPlayer.mockResolvedValue(player); + + const result = await chatService.sendMessage('room1', 'player1', 'hello world'); + + // TODO : 타입 narrowing 필요 + expect(result).toBeDefined(); + expect(mockChatRepository.getPlayer).toHaveBeenCalled(); + }); }); }); From aaaebce3ae52e60db8ab49ac360368ab83934826 Mon Sep 17 00:00:00 2001 From: gimseonghwan Date: Thu, 9 Jan 2025 00:20:34 +0900 Subject: [PATCH 13/47] =?UTF-8?q?=E2=9C=A8feat(server/packages,=20tsconfig?= =?UTF-8?q?):=20jest=20=EC=A0=88=EB=8C=80=EA=B2=BD=EB=A1=9C=20=EC=98=A4?= =?UTF-8?q?=EB=A5=98=20=ED=95=B4=EA=B2=B0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 절대경로로 제대로 실행되지 않는 문제 해결 notion에 작성했습니다~ --- server/package.json | 6 +++++- server/tsconfig.json | 3 +++ 2 files changed, 8 insertions(+), 1 deletion(-) diff --git a/server/package.json b/server/package.json index 93f97d4f..1145a838 100644 --- a/server/package.json +++ b/server/package.json @@ -29,6 +29,7 @@ "@troublepainter/core": "workspace:*", "axios": "^1.7.7", "ioredis": "^5.4.1", + "jest-mock": "^29.7.0", "reflect-metadata": "^0.2.0", "rxjs": "^7.8.1", "socket.io": "^4.8.1", @@ -75,6 +76,9 @@ "**/*.(t|j)s" ], "coverageDirectory": "../coverage", - "testEnvironment": "node" + "testEnvironment": "node", + "moduleNameMapper": { + "^src/(.*)$": "/$1" + } } } diff --git a/server/tsconfig.json b/server/tsconfig.json index 95f5641c..1efd6fec 100644 --- a/server/tsconfig.json +++ b/server/tsconfig.json @@ -17,5 +17,8 @@ "strictBindCallApply": false, "forceConsistentCasingInFileNames": false, "noFallthroughCasesInSwitch": false + }, + "paths": { + "src/*" : ["./src/*"] } } From 079a0345295cd6bbee51ac92f35b3095c072e421 Mon Sep 17 00:00:00 2001 From: gimseonghwan Date: Thu, 9 Jan 2025 00:48:12 +0900 Subject: [PATCH 14/47] =?UTF-8?q?=F0=9F=A7=AAtest(server/chat.repository.s?= =?UTF-8?q?pec.ts):=20repository=20=ED=85=8C=EC=8A=A4=ED=8A=B8=20=EC=B6=94?= =?UTF-8?q?=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 각각의 임계 조건에 대해 검증하는 테스트 추가 --- server/src/chat/chat.repository.spec.ts | 94 +++++++++++++++++++++++++ 1 file changed, 94 insertions(+) create mode 100644 server/src/chat/chat.repository.spec.ts diff --git a/server/src/chat/chat.repository.spec.ts b/server/src/chat/chat.repository.spec.ts new file mode 100644 index 00000000..4faa5522 --- /dev/null +++ b/server/src/chat/chat.repository.spec.ts @@ -0,0 +1,94 @@ +import { ChatRepository } from './chat.repository'; +import { Test, TestingModule } from '@nestjs/testing'; +import { RedisService } from '../redis/redis.service'; +import { describe } from 'node:test'; +import { PlayerRole } from '../common/enums/game.status.enum'; + +describe('chat repository tests', () => { + let chatRepository: ChatRepository; + + const mockRedisService = { + hgetall: jest.fn(), + exists: jest.fn(), + }; + + beforeEach(async () => { + const module: TestingModule = await Test.createTestingModule({ + providers: [ + ChatRepository, + { + provide: RedisService, + useValue: mockRedisService, + }, + ], + }).compile(); + + chatRepository = module.get(ChatRepository); + }); + + afterEach(() => { + jest.clearAllMocks(); + }); + + describe('getPlayer 테스트', () => { + it('플레이어 데이터가 없을 때 null을 리턴', async () => { + mockRedisService.hgetall.mockResolvedValue(null); + + const result = await chatRepository.getPlayer('room1', 'player1'); + expect(mockRedisService.hgetall).toHaveBeenCalled(); + expect(result).toBeNull(); + }); + + it('플레이어 데이터가 있을 때 Player로 데이터를 변환해 리턴', async () => { + const player = { + role: PlayerRole.GUESSER, + userImg: '', + score: 15, + }; + + mockRedisService.hgetall.mockResolvedValue(player); + + const result = await chatRepository.getPlayer('room1', 'player1'); + expect(result).toEqual({ + ...player, + profileImage: null, + }); + }); + }); + + describe('existsRoom 테스트', () => { + it('방이 존재하지 않을 때 false를 리턴', async () => { + mockRedisService.exists.mockResolvedValue(0); + + const result = await chatRepository.existsRoom('room1'); + expect(mockRedisService.exists).toHaveBeenCalled(); + expect(result).toBe(false); + }); + }); + + it('방이 존재할 때 true를 리턴', async () => { + mockRedisService.exists.mockResolvedValue(1); + + const result = await chatRepository.existsRoom('room1'); + expect(mockRedisService.exists).toHaveBeenCalled(); + expect(result).toBe(true); + }); + + describe('existsPlayer 테스트', () => { + it('플레이어가 존재하지 않을 때 false를 리턴', async () => { + mockRedisService.exists.mockResolvedValue(0); + + const result = await chatRepository.existsPlayer('room1', 'player1'); + expect(mockRedisService.exists).toHaveBeenCalled(); + expect(result).toBe(false); + }); + + it('플레이어가 존재할 때 true를 리턴한다', async () => { + mockRedisService.exists.mockResolvedValue(1); + + const result = await chatRepository.existsPlayer('room1', 'player1'); + expect(mockRedisService.exists).toHaveBeenCalled(); + expect(result).toBe(true); + }); + }); +}); From b86e52e7c99668d32c560bc5ffc1ef51eb0c75a5 Mon Sep 17 00:00:00 2001 From: gimseonghwan Date: Thu, 9 Jan 2025 00:53:01 +0900 Subject: [PATCH 15/47] =?UTF-8?q?=F0=9F=A7=AAtest(server/chat.gateway.spec?= =?UTF-8?q?.ts):=20toThrow=EC=82=AC=EC=9A=A9=ED=95=B4=20error=20=EC=B2=B4?= =?UTF-8?q?=ED=81=AC,=20=ED=85=8C=EC=8A=A4=ED=8A=B8=20=EA=B5=AC=EB=AC=B8?= =?UTF-8?q?=20=EC=9D=BC=EA=B4=80=ED=99=94?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit toThrowError는 deprecated되어 toThrow를 사용해 변경 --- server/src/chat/chat.service.spec.ts | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/server/src/chat/chat.service.spec.ts b/server/src/chat/chat.service.spec.ts index feb53c63..323a452d 100644 --- a/server/src/chat/chat.service.spec.ts +++ b/server/src/chat/chat.service.spec.ts @@ -27,18 +27,18 @@ describe('ChatService', () => { }); describe('sendMessage 테스트', async () => { - it('메시지가 공백일 때', async () => { + it('메시지가 공백일 때 BadRequestException 발생', async () => { await expect(async () => { await chatService.sendMessage('room1', 'player1', ''); - }).rejects.toThrowError(BadRequestException); + }).rejects.toThrow(BadRequestException); }); - it('플레이어가 존재하지 않을 때', async () => { + it('플레이어가 존재하지 않을 때 PlayerNotFoundException 발생', async () => { mockChatRepository.getPlayer.mockResolvedValue(null); await expect(async () => { await chatService.sendMessage('room1', 'player1', 'hello world'); - }).rejects.toThrowError(PlayerNotFoundException); + }).rejects.toThrow(PlayerNotFoundException); // 에러 캐치로 인해 순서를 바꿔서 배치 expect(mockChatRepository.getPlayer).toHaveBeenCalled(); From 57ec421e50e5598b428c5fe2d7feb32741bc28d7 Mon Sep 17 00:00:00 2001 From: gimseonghwan Date: Thu, 9 Jan 2025 00:54:23 +0900 Subject: [PATCH 16/47] =?UTF-8?q?=F0=9F=A7=AAtest(server/chat.gateway.spec?= =?UTF-8?q?.ts):=20gateway=20=ED=85=8C=EC=8A=A4=ED=8A=B8=20=EC=B6=94?= =?UTF-8?q?=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit as unknown as 를 이용해 Socket 타입 문제 해결, 각각의 if나 throw에 대해 테스트 작성 --- server/src/chat/chat.gateway.spec.ts | 89 ++++++++++++++++++++++++++-- 1 file changed, 85 insertions(+), 4 deletions(-) diff --git a/server/src/chat/chat.gateway.spec.ts b/server/src/chat/chat.gateway.spec.ts index 34daca94..9c3cf3a0 100644 --- a/server/src/chat/chat.gateway.spec.ts +++ b/server/src/chat/chat.gateway.spec.ts @@ -1,18 +1,99 @@ import { Test, TestingModule } from '@nestjs/testing'; import { ChatGateway } from './chat.gateway'; +import { ChatService } from './chat.service'; +import { BadRequestException, PlayerNotFoundException, RoomNotFoundException } from 'src/exceptions/game.exception'; +import { Socket } from 'socket.io'; describe('ChatGateway', () => { let gateway: ChatGateway; + let mockSocket: Partial; + + const mockChatService = { + existsRoom: jest.fn(), + existsPlayer: jest.fn(), + sendMessage: jest.fn(), + }; beforeEach(async () => { const module: TestingModule = await Test.createTestingModule({ - providers: [ChatGateway], + providers: [ChatGateway, { provide: ChatService, useValue: mockChatService }], }).compile(); - gateway = module.get(ChatGateway); + gateway = module.get(ChatGateway); + + /** + * as unknown as 를 이용해 타입을 먼저 unknown으로 바꾼 다음, + * 원하는 타입에 type assertion을 한다. + */ + mockSocket = { + handshake: { auth: { roomId: 'room1', playerId: 'player1' } }, + data: {}, + join: jest.fn(), + to: jest.fn().mockReturnThis(), + emit: jest.fn(), + } as unknown as Socket; + }); + + afterEach(() => { + jest.clearAllMocks(); + }); + + describe('handleConnection 테스트', () => { + it('roomId가 null일 때 BadRequestException을 발생', () => { + mockSocket.handshake.auth = { roomId: null }; + + expect(() => gateway.handleConnection(mockSocket as Socket)).toThrow(BadRequestException); + }); + + it('playerId가 null일 때 BadRequestException을 발생', () => { + mockSocket.handshake.auth = { playerId: null }; + + expect(() => gateway.handleConnection(mockSocket as Socket)).toThrow(BadRequestException); + }); + + it('room이 존재하지 않을 때 RoomNotFoundException을 발생', () => { + mockChatService.existsRoom.mockReturnValue(false); + + expect(() => gateway.handleConnection(mockSocket as Socket)).toThrow(RoomNotFoundException); + expect(mockChatService.existsRoom).toHaveBeenCalled(); + }); + + it('플레이어가 룸에 존재하지 않을 때 PlayerNotFoundException을 발생', () => { + mockChatService.existsRoom.mockReturnValue(true); + mockChatService.existsPlayer.mockReturnValue(false); + + expect(() => gateway.handleConnection(mockSocket as Socket)).toThrow(PlayerNotFoundException); + expect(mockChatService.existsPlayer).toHaveBeenCalled(); + }); + + it('플레이어와 방이 정상적으로 할당되어 있을 때', () => { + mockChatService.existsRoom.mockReturnValue(true); + mockChatService.existsPlayer.mockReturnValue(true); + + gateway.handleConnection(mockSocket as Socket); + + expect(mockSocket.join).toHaveBeenCalled(); + expect(mockSocket.data).toEqual({ roomId: 'room1', playerId: 'player1' }); + }); }); - it('should be defined', () => { - expect(gateway).toBeDefined(); + describe('handleSendMessage 테스트', () => { + it('데이터가 없을 때 BadRequestException을 발생', async () => { + mockSocket.data = {}; + + await expect(gateway.handleSendMessage(mockSocket as Socket, { message: 'hello world' })).rejects.toThrow( + BadRequestException, + ); + }); + + it('정상적으로 메시지를 발신할 수 있을 때', async () => { + mockSocket.data = { roomId: 'room1', playerId: 'player1' }; + mockChatService.sendMessage.mockResolvedValue({ message: 'hello world', sender: 'player1' }); + + await gateway.handleSendMessage(mockSocket as Socket, { message: 'hello world' }); + + expect(mockChatService.sendMessage).toHaveBeenCalled(); + expect(mockSocket.to('room1').emit).toHaveBeenCalled(); + }); }); }); From f37389cd24d110432fa4baefb3f20a3aa9434cf3 Mon Sep 17 00:00:00 2001 From: gimseonghwan Date: Thu, 9 Jan 2025 09:50:03 +0900 Subject: [PATCH 17/47] =?UTF-8?q?=F0=9F=A7=AAtest(server/chat.module.spec.?= =?UTF-8?q?ts):=20=EB=AA=A8=EB=93=88=20=ED=85=8C=EC=8A=A4=ED=8A=B8=20?= =?UTF-8?q?=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit module 컴파일 후 imports, providers를 체크하는 방식으로 테스트 --- server/src/chat/chat.module.spec.ts | 20 ++++++++++++++++++++ 1 file changed, 20 insertions(+) create mode 100644 server/src/chat/chat.module.spec.ts diff --git a/server/src/chat/chat.module.spec.ts b/server/src/chat/chat.module.spec.ts new file mode 100644 index 00000000..bc9367f1 --- /dev/null +++ b/server/src/chat/chat.module.spec.ts @@ -0,0 +1,20 @@ +import { ChatModule } from './chat.module'; +import { Test } from '@nestjs/testing'; +import { ChatService } from './chat.service'; +import { ChatRepository } from './chat.repository'; +import { RedisModule } from '../redis/redis.module'; +import { ChatGateway } from './chat.gateway'; + +describe('ChatModule', () => { + it('컴파일 확인', async () => { + const module = await Test.createTestingModule({ + imports: [ChatModule], + }).compile(); + + expect(module).toBeDefined(); + expect(module.get(ChatService)).toBeInstanceOf(ChatService); + expect(module.get(ChatRepository)).toBeInstanceOf(ChatRepository); + expect(module.get(ChatGateway)).toBeInstanceOf(ChatGateway); + expect(module.get(RedisModule)).toBeInstanceOf(RedisModule); + }); +}); From 9df25b78bc3e02788e7832aff57e5b59aa5ee13d Mon Sep 17 00:00:00 2001 From: gimseonghwan Date: Thu, 9 Jan 2025 09:53:26 +0900 Subject: [PATCH 18/47] =?UTF-8?q?=F0=9F=A7=AAtest(server/chat.repository.s?= =?UTF-8?q?pec.ts):=20=ED=85=8C=EC=8A=A4=ED=8A=B8=20=EB=B6=80=EC=A1=B1?= =?UTF-8?q?=ED=95=9C=20=EB=B6=80=EB=B6=84=20=EC=B6=94=EA=B0=80,=20describe?= =?UTF-8?q?=20=EB=8B=A4=EB=A5=B8=20=EA=B3=B3=EA=B3=BC=20=ED=86=B5=EC=9D=BC?= =?UTF-8?q?=ED=99=94?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- server/src/chat/chat.repository.spec.ts | 64 ++++++++++++++++++++++++- 1 file changed, 62 insertions(+), 2 deletions(-) diff --git a/server/src/chat/chat.repository.spec.ts b/server/src/chat/chat.repository.spec.ts index 4faa5522..a3d5fe17 100644 --- a/server/src/chat/chat.repository.spec.ts +++ b/server/src/chat/chat.repository.spec.ts @@ -4,7 +4,7 @@ import { RedisService } from '../redis/redis.service'; import { describe } from 'node:test'; import { PlayerRole } from '../common/enums/game.status.enum'; -describe('chat repository tests', () => { +describe('ChatRepository', () => { let chatRepository: ChatRepository; const mockRedisService = { @@ -39,7 +39,24 @@ describe('chat repository tests', () => { expect(result).toBeNull(); }); - it('플레이어 데이터가 있을 때 Player로 데이터를 변환해 리턴', async () => { + it('플레이어 데이터 중 role이 없을 때 null을 리턴', async () => { + const player = { + role: '', + userImg: 'test', + score: 15, + }; + + mockRedisService.hgetall.mockResolvedValue(player); + + const result = await chatRepository.getPlayer('room1', 'player1'); + expect(result).toEqual({ + ...player, + role: null, + profileImage: player.userImg, + }); + }); + + it('플레이어 데이터 중 이미지가 없을 때 profileImage가 null을 리턴', async () => { const player = { role: PlayerRole.GUESSER, userImg: '', @@ -54,6 +71,49 @@ describe('chat repository tests', () => { profileImage: null, }); }); + + it('플레이어 데이터 중 이미지가 있을 때 profileImage가 userImg와 동일하게 리턴', async () => { + const player = { + role: PlayerRole.GUESSER, + userImg: 'test', + score: 15, + }; + + mockRedisService.hgetall.mockResolvedValue(player); + + const result = await chatRepository.getPlayer('room1', 'player1'); + expect(result).toEqual({ + ...player, + profileImage: player.userImg, + }); + }); + + it('플레이어 데이터에서 score 임계값을 처리', async () => { + const testCases = [ + { inputScore: '15', expectedScore: 15 }, + { inputScore: '-5', expectedScore: -5 }, + { inputScore: '', expectedScore: 0 }, + { inputScore: null, expectedScore: 0 }, + { inputScore: 'abc', expectedScore: 0 }, + ]; + + for (const testCase of testCases) { + const player = { + role: PlayerRole.GUESSER, + userImg: 'test', + score: testCase.inputScore, + }; + + mockRedisService.hgetall.mockResolvedValue(player); + + const result = await chatRepository.getPlayer('room1', 'player1'); + expect(result).toEqual({ + ...player, + profileImage: player.userImg, + score: testCase.expectedScore, + }); + } + }); }); describe('existsRoom 테스트', () => { From eb1b7163e2ec1418effc1af18529702670ae4def Mon Sep 17 00:00:00 2001 From: gimseonghwan Date: Thu, 9 Jan 2025 09:54:13 +0900 Subject: [PATCH 19/47] =?UTF-8?q?=F0=9F=A7=AAtest(server/chat.service.spec?= =?UTF-8?q?.ts):=20chat.service=20existsRoom,=20existsPlayer=20=ED=85=8C?= =?UTF-8?q?=EC=8A=A4=ED=8A=B8=20=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- server/src/chat/chat.service.spec.ts | 37 ++++++++++++++++++++++++++++ 1 file changed, 37 insertions(+) diff --git a/server/src/chat/chat.service.spec.ts b/server/src/chat/chat.service.spec.ts index 323a452d..a075956b 100644 --- a/server/src/chat/chat.service.spec.ts +++ b/server/src/chat/chat.service.spec.ts @@ -12,6 +12,7 @@ describe('ChatService', () => { const mockChatRepository = { getPlayer: jest.fn(), existsRoom: jest.fn(), + existsPlayer: jest.fn(), }; beforeEach(async () => { @@ -62,4 +63,40 @@ describe('ChatService', () => { expect(mockChatRepository.getPlayer).toHaveBeenCalled(); }); }); + + describe('existsRoom 테스트', () => { + it('존재하는 방일 때 true를 리턴', async () => { + mockChatRepository.existsRoom.mockResolvedValue(true); + + const result = await chatService.existsRoom('room1'); + expect(result).toBe(true); + expect(mockChatRepository.existsRoom).toHaveBeenCalledWith('room1'); + }); + + it('존재하지 않는 방일 때 false를 리턴', async () => { + mockChatRepository.existsRoom.mockResolvedValue(false); + + const result = await chatService.existsRoom('room2'); + expect(result).toBe(false); + expect(mockChatRepository.existsRoom).toHaveBeenCalledWith('room2'); + }); + }); + + describe('existsPlayer 테스트', () => { + it('존재하는 플레이어일 때 true를 리턴', async () => { + mockChatRepository.existsPlayer.mockResolvedValue(true); + + const result = await chatService.existsPlayer('room1', 'player1'); + expect(result).toBe(true); + expect(mockChatRepository.existsPlayer).toHaveBeenCalledWith('room1', 'player1'); + }); + + it('존재하지 않는 플레이어일 때 false를 리턴', async () => { + mockChatRepository.existsPlayer.mockResolvedValue(false); + + const result = await chatService.existsPlayer('room1', 'player2'); + expect(result).toBe(false); + expect(mockChatRepository.existsPlayer).toHaveBeenCalledWith('room1', 'player2'); + }); + }); }); From fa53adc5cdc95bd7dd37e966e1b4c551c151f9e1 Mon Sep 17 00:00:00 2001 From: gimseonghwan Date: Thu, 9 Jan 2025 09:55:46 +0900 Subject: [PATCH 20/47] =?UTF-8?q?=F0=9F=93=9Ddocs(chat.service.spec.ts):?= =?UTF-8?q?=20todo=20=EC=82=AD=EC=A0=9C?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- server/src/chat/chat.service.spec.ts | 1 - 1 file changed, 1 deletion(-) diff --git a/server/src/chat/chat.service.spec.ts b/server/src/chat/chat.service.spec.ts index a075956b..a14b32c2 100644 --- a/server/src/chat/chat.service.spec.ts +++ b/server/src/chat/chat.service.spec.ts @@ -58,7 +58,6 @@ describe('ChatService', () => { const result = await chatService.sendMessage('room1', 'player1', 'hello world'); - // TODO : 타입 narrowing 필요 expect(result).toBeDefined(); expect(mockChatRepository.getPlayer).toHaveBeenCalled(); }); From fee0d3a79301edbe31cccf2e15227f6ad057704d Mon Sep 17 00:00:00 2001 From: JIN Date: Thu, 9 Jan 2025 16:46:18 +0900 Subject: [PATCH 21/47] =?UTF-8?q?=F0=9F=8F=97=EF=B8=8F=20build:=20socket.i?= =?UTF-8?q?o-client=20=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- pnpm-lock.yaml | 6 ++++++ server/package.json | 2 ++ 2 files changed, 8 insertions(+) diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 790d0233..175e59ac 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -260,6 +260,9 @@ importers: '@types/socket.io': specifier: ^3.0.2 version: 3.0.2 + '@types/socket.io-client': + specifier: ^3.0.0 + version: 3.0.0 '@types/supertest': specifier: ^6.0.0 version: 6.0.2 @@ -287,6 +290,9 @@ importers: prettier: specifier: ^3.0.0 version: 3.3.3 + socket.io-client: + specifier: ^4.8.1 + version: 4.8.1 source-map-support: specifier: ^0.5.21 version: 0.5.21 diff --git a/server/package.json b/server/package.json index 93f97d4f..406e9db2 100644 --- a/server/package.json +++ b/server/package.json @@ -43,6 +43,7 @@ "@types/jest": "^29.5.2", "@types/node": "^20.3.1", "@types/socket.io": "^3.0.2", + "@types/socket.io-client": "^3.0.0", "@types/supertest": "^6.0.0", "@typescript-eslint/eslint-plugin": "^8.0.0", "@typescript-eslint/parser": "^8.0.0", @@ -52,6 +53,7 @@ "ioredis-mock": "^8.9.0", "jest": "^29.5.0", "prettier": "^3.0.0", + "socket.io-client": "^4.8.1", "source-map-support": "^0.5.21", "supertest": "^7.0.0", "ts-jest": "^29.1.0", From 5ee119717cf6c6d11a6f26e6d16ac1216860f67f Mon Sep 17 00:00:00 2001 From: JIN Date: Thu, 9 Jan 2025 20:17:38 +0900 Subject: [PATCH 22/47] =?UTF-8?q?=F0=9F=A7=AA=20test(server/drawing.gatewa?= =?UTF-8?q?y.e2e.spec.ts):=20drawingGateway=20e2e=20=ED=85=8C=EC=8A=A4?= =?UTF-8?q?=ED=8A=B8=20=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../src/drawing/drawing.gateway.e2e.spec.ts | 167 ++++++++++++++++++ 1 file changed, 167 insertions(+) create mode 100644 server/src/drawing/drawing.gateway.e2e.spec.ts diff --git a/server/src/drawing/drawing.gateway.e2e.spec.ts b/server/src/drawing/drawing.gateway.e2e.spec.ts new file mode 100644 index 00000000..def5f40a --- /dev/null +++ b/server/src/drawing/drawing.gateway.e2e.spec.ts @@ -0,0 +1,167 @@ +import { Test, TestingModule } from '@nestjs/testing'; +import { ConfigService } from '@nestjs/config'; +import { INestApplication } from '@nestjs/common'; +import { io, Socket } from 'socket.io-client'; +import { RedisService } from '../redis/redis.service'; +import { DrawingGateway } from './drawing.gateway'; +import { DrawingService } from './drawing.service'; +import { DrawingRepository } from './drawing.repository'; + +describe('DrawingGateway e2e 테스트', () => { + let app: INestApplication; + let redisService: RedisService; + let clientA: Socket; + + const URL = 'http://localhost:3001'; + + const mockConfigService = { + provide: ConfigService, + useValue: { + get: jest.fn().mockImplementation((key: string) => { + if (key === 'REDIS_HOST') return 'localhost'; + if (key === 'REDIS_PORT') return '6379'; + return null; + }), + }, + }; + + beforeAll(async () => { + const module: TestingModule = await Test.createTestingModule({ + providers: [DrawingGateway, DrawingService, DrawingRepository, RedisService, mockConfigService], + }).compile(); + + app = module.createNestApplication(); + redisService = module.get(RedisService); + + // 테스트용 서버 실행 + await app.listen(3001); + }); + + beforeEach(async () => { + clientA = io(URL, { + auth: { + roomId: 'room1', + playerId: 'player1', + }, + }); + + await new Promise((resolve) => { + clientA.on('connect', () => { + resolve(); + }); + }); + + await redisService.hset('room:room1', { roomId: 'room1' }); + await redisService.hset('room:room1:player:player1', { playerId: 'player1' }); + }); + + afterEach(async () => { + clientA.close(); + await redisService.flushAll(); + }); + + afterAll(async () => { + await app.close(); + redisService.quit(); + }); + + describe('handleConnection', () => { + it('roomId 또는 playerId의 값이 존재하지 않는 경우 "Room ID and Player ID are required" 에러가 발생한다.', async () => { + const authInfo = [ + { roomId: '', playerId: '' }, + { roomId: 'room1', playerId: '' }, + { roomId: '', playerId: 'player1' }, + ]; + + for (const auth of authInfo) { + const invalidClient = io(URL, { + auth: { auth }, + }); + + invalidClient.on('connect_error', (e) => { + expect(e.message).toBe('Room ID and Player ID are required'); + invalidClient.close(); + }); + } + }); + + it('room이 redis 내에 존재하지 않는 경우 "Room not found" 에러가 발생한다.', async () => { + const invalidRoomClient = io(URL, { + auth: { + roomId: 'failed-room', + playerId: 'player1', + }, + }); + + invalidRoomClient.on('connect_error', (e) => { + expect(e.message).toBe('Room not found'); + invalidRoomClient.close(); + }); + }); + + it('player가 redis 내에 존재하지 않는 경우 "Player not found in room" 에러가 발생한다.', async () => { + const invalidPlayerClient = io(URL, { + auth: { + roomId: 'room1', + playerId: 'failed-player', + }, + }); + + invalidPlayerClient.on('connect_error', (e) => { + expect(e.message).toBe('Player not found in room'); + invalidPlayerClient.close(); + }); + }); + + it('room과 player가 정상적으로 존재하는 경우 정상적으로 연결된다.', async () => { + expect(clientA.connected).toBe(true); + }); + }); + + describe('handleDraw', () => { + it('roomId 값이 존재하지 않는 경우 "Room ID is required" 에러가 발생한다.', async () => { + const invalidRoomClient = io(URL, { + auth: { + roomId: 'failed-room', + playerId: 'player1', + }, + }); + + invalidRoomClient.on('connect_error', (e) => { + expect(e.message).toBe('Room ID is required'); + invalidRoomClient.close(); + }); + }); + + it('정상적으로 그림이 그려지는 경우', async () => { + const drawingData = { + pos: 56, + fillColor: { R: 0, G: 0, B: 0, A: 0 }, + }; + + /** + * client.to(roomId).emit('drawUpdated') 이므로 + * 그림 그린 사람을 제외하고 다른 사람이 존재해야 이벤트를 제대로 수신받는지 확인이 가능함 + * 이를 위해 clientB를 생성 + */ + const clientB = io(URL, { + auth: { + roomId: 'room1', + playerId: 'player2', + }, + }); + + // clientB가 이벤트를 수신받을 준비를 함 + clientB.on('drawUpdated', (data) => { + expect(data).toEqual({ + playerId: 'player1', + drawingData: drawingData, + }); + clientB.close(); + }); + + // clientA가 실제로 이벤트를 발생시킴 + clientA.emit('draw', { drawingData }); + }); + }); +}); From b19040470856776e4545d2d975c6af8059e18e26 Mon Sep 17 00:00:00 2001 From: gimseonghwan Date: Mon, 13 Jan 2025 15:19:17 +0900 Subject: [PATCH 23/47] =?UTF-8?q?=F0=9F=90=9Bfix(Dockerfile.nginx,=20serve?= =?UTF-8?q?r):=20node=20=EB=B2=84=EC=A0=84=2020=20-=2022=EB=B2=84=EC=A0=84?= =?UTF-8?q?=EC=9C=BC=EB=A1=9C=20=EC=97=85=EA=B7=B8=EB=A0=88=EC=9D=B4?= =?UTF-8?q?=EB=93=9C?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- Dockerfile.nginx | 2 +- Dockerfile.server | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/Dockerfile.nginx b/Dockerfile.nginx index 55efa50a..5c4bf8ae 100644 --- a/Dockerfile.nginx +++ b/Dockerfile.nginx @@ -1,4 +1,4 @@ -FROM node:20-alpine AS builder +FROM node:22-alpine AS builder RUN corepack enable && corepack prepare pnpm@9.12.3 --activate diff --git a/Dockerfile.server b/Dockerfile.server index 8971ea5e..3e7619c7 100644 --- a/Dockerfile.server +++ b/Dockerfile.server @@ -15,7 +15,7 @@ COPY . . RUN pnpm --filter @troublepainter/core build RUN pnpm --filter server build -FROM node:20-alpine AS production +FROM node:22-alpine AS production WORKDIR /app COPY --from=builder /app/pnpm-workspace.yaml ./ From c0117bcfc0e2c163c91cb95d679becb3664b5d9e Mon Sep 17 00:00:00 2001 From: gimseonghwan Date: Mon, 13 Jan 2025 15:19:43 +0900 Subject: [PATCH 24/47] =?UTF-8?q?=F0=9F=90=9Bfix(nginx.conf):=20nginx=20re?= =?UTF-8?q?-troublepainter.kro.kr=EB=A1=9C=20=EC=A3=BC=EC=86=8C=20?= =?UTF-8?q?=EB=B3=80=EA=B2=BD?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- nginx.conf | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/nginx.conf b/nginx.conf index 78cbbc02..21fbd542 100644 --- a/nginx.conf +++ b/nginx.conf @@ -1,14 +1,14 @@ server { listen 80; - server_name www.troublepainter.site; + server_name re-troublepainter.kro.kr; return 301 https://$server_name$request_uri; } server { listen 443 ssl; - server_name www.troublepainter.site; + server_name re-troublepainter.kro.kr; - ssl_certificate /etc/letsencrypt/live/www.troublepainter.site/fullchain.pem; - ssl_certificate_key /etc/letsencrypt/live/www.troublepainter.site/privkey.pem; + ssl_certificate /etc/letsencrypt/live/re-troublepainter.kro.kr/fullchain.pem; + ssl_certificate_key /etc/letsencrypt/live/re-troublepainter.kro.kr/privkey.pem; gzip on; gzip_types text/plain text/css application/json application/javascript text/xml application/xml application/xml+rss text/javascript; From 4d010af08fd10cdbf0e9b9663f2439fd314ad433 Mon Sep 17 00:00:00 2001 From: gimseonghwan Date: Mon, 13 Jan 2025 15:26:09 +0900 Subject: [PATCH 25/47] =?UTF-8?q?=F0=9F=90=9Bfix(.github/workflows=20-=20c?= =?UTF-8?q?lient-ci-cd,=20server-ci-cd):=20node=20=EB=B2=84=EC=A0=84=20?= =?UTF-8?q?=EB=B3=80=EA=B2=BD=20=EB=B0=8F=20deploy=20path=20=EB=B3=80?= =?UTF-8?q?=EA=B2=BD?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .github/workflows/client-ci-cd.yml | 6 +++--- .github/workflows/server-ci-cd.yml | 6 +++--- 2 files changed, 6 insertions(+), 6 deletions(-) diff --git a/.github/workflows/client-ci-cd.yml b/.github/workflows/client-ci-cd.yml index 7edc0bdd..b91f7c01 100644 --- a/.github/workflows/client-ci-cd.yml +++ b/.github/workflows/client-ci-cd.yml @@ -19,7 +19,7 @@ jobs: - name: Setup Node.js uses: actions/setup-node@v4 with: - node-version: '20' + node-version: '22' - name: Setup pnpm uses: pnpm/action-setup@v3 @@ -61,10 +61,10 @@ jobs: uses: appleboy/ssh-action@v1.0.0 with: host: ${{ secrets.SSH_HOST }} - username: mira + username: root key: ${{ secrets.SSH_PRIVATE_KEY }} script: | - cd /home/mira/web30-stop-troublepainter + cd /root/deploy export DOCKERHUB_USERNAME=${{ secrets.DOCKERHUB_USERNAME }} docker pull ${{ secrets.DOCKERHUB_USERNAME }}/troublepainter-nginx:latest docker compose up -d nginx \ No newline at end of file diff --git a/.github/workflows/server-ci-cd.yml b/.github/workflows/server-ci-cd.yml index fdf15419..24d55196 100644 --- a/.github/workflows/server-ci-cd.yml +++ b/.github/workflows/server-ci-cd.yml @@ -19,7 +19,7 @@ jobs: - name: Setup Node.js uses: actions/setup-node@v4 with: - node-version: '20' + node-version: '22' - name: Setup pnpm uses: pnpm/action-setup@v3 with: @@ -69,10 +69,10 @@ jobs: uses: appleboy/ssh-action@v1.0.0 with: host: ${{ secrets.SSH_HOST }} - username: mira + username: root key: ${{ secrets.SSH_PRIVATE_KEY }} script: | - cd /home/mira/web30-stop-troublepainter + cd /root/deploy export DOCKERHUB_USERNAME=${{ secrets.DOCKERHUB_USERNAME }} docker pull ${{ secrets.DOCKERHUB_USERNAME }}/troublepainter-server:latest docker compose up -d server \ No newline at end of file From dcc75e2e8100b3cd760caa989c3b7ea6c747a300 Mon Sep 17 00:00:00 2001 From: JIN Date: Tue, 14 Jan 2025 17:31:39 +0900 Subject: [PATCH 26/47] =?UTF-8?q?=F0=9F=90=9B=20fix(drawing.gateway.ts):?= =?UTF-8?q?=20handleConnection=20=EC=97=90=EB=9F=AC=20=EC=B2=98=EB=A6=AC?= =?UTF-8?q?=20=EB=A1=9C=EC=A7=81=20=EB=B3=80=EA=B2=BD?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit gateway에서 @SubscribeMessage 데코레이터를 제외한 메서드는 filter의 영향을 받지 않는다. 따라서 filter의 영향을 받을 것을 기대하고 handleConnection 메서드를 작성한 것은 명백하게 잘못 작성된 코드이므로 error 이벤트를 발생시키고 연결을 종료시키는 방법으로 변경하였다. --- server/src/drawing/drawing.gateway.ts | 29 +++++++++++++++++++++++---- 1 file changed, 25 insertions(+), 4 deletions(-) diff --git a/server/src/drawing/drawing.gateway.ts b/server/src/drawing/drawing.gateway.ts index 8cfb133b..dcb627ce 100644 --- a/server/src/drawing/drawing.gateway.ts +++ b/server/src/drawing/drawing.gateway.ts @@ -8,7 +8,7 @@ import { WebSocketServer, } from '@nestjs/websockets'; import { Server, Socket } from 'socket.io'; -import { BadRequestException, PlayerNotFoundException, RoomNotFoundException } from '../exceptions/game.exception'; +import { BadRequestException } from '../exceptions/game.exception'; import { WsExceptionFilter } from '../filters/ws-exception.filter'; import { DrawingService } from './drawing.service'; @@ -27,12 +27,33 @@ export class DrawingGateway implements OnGatewayConnection { const roomId = client.handshake.auth.roomId; const playerId = client.handshake.auth.playerId; - if (!roomId || !playerId) throw new BadRequestException('Room ID and Player ID are required'); + if (!roomId || !playerId) { + client.emit('error', { + code: 4000, + message: 'Room ID and Player ID are required', + }); + client.disconnect(); + return; + } const roomExists = await this.drawingService.existsRoom(roomId); - if (!roomExists) throw new RoomNotFoundException('Room not found'); + if (!roomExists) { + client.emit('error', { + code: 6005, + message: 'Room not found', + }); + client.disconnect(); + return; + } const playerExists = await this.drawingService.existsPlayer(roomId, playerId); - if (!playerExists) throw new PlayerNotFoundException('Player not found in room'); + if (!playerExists) { + client.emit('error', { + code: 6006, + message: 'Player not found in room', + }); + client.disconnect(); + return; + } client.data.roomId = roomId; client.data.playerId = playerId; From eff17eb51d5d93e2d45438bbe82cdb20e9152222 Mon Sep 17 00:00:00 2001 From: JIN Date: Tue, 14 Jan 2025 17:32:54 +0900 Subject: [PATCH 27/47] =?UTF-8?q?=F0=9F=A7=AA=20test(drawing.gateway.e2e.s?= =?UTF-8?q?pec.ts):=20=EA=B8=B0=EC=A1=B4=20=EC=BD=94=EB=93=9C=20=EB=B3=80?= =?UTF-8?q?=EA=B2=BD=EC=9C=BC=EB=A1=9C=20=EC=9D=B8=ED=95=9C=20=ED=85=8C?= =?UTF-8?q?=EC=8A=A4=ED=8A=B8=20=EC=BD=94=EB=93=9C=20=EC=88=98=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 기존에는 filter의 영향을 받을 것이라 예상해 filter에서 catch한 에러를 반환받아야 한다는 내용의 테스트를 작성했지만, 기존 코드를 변경하였으므로 그에 맞는 테스트 코드로 변경하였다. --- .../src/drawing/drawing.gateway.e2e.spec.ts | 49 ++++++++++++------- 1 file changed, 31 insertions(+), 18 deletions(-) diff --git a/server/src/drawing/drawing.gateway.e2e.spec.ts b/server/src/drawing/drawing.gateway.e2e.spec.ts index def5f40a..9c4464fa 100644 --- a/server/src/drawing/drawing.gateway.e2e.spec.ts +++ b/server/src/drawing/drawing.gateway.e2e.spec.ts @@ -12,7 +12,7 @@ describe('DrawingGateway e2e 테스트', () => { let redisService: RedisService; let clientA: Socket; - const URL = 'http://localhost:3001'; + const URL = 'http://localhost:3001/socket.io/drawing'; const mockConfigService = { provide: ConfigService, @@ -38,6 +38,9 @@ describe('DrawingGateway e2e 테스트', () => { }); beforeEach(async () => { + await redisService.hset('room:room1', { roomId: 'room1' }); + await redisService.hset('room:room1:player:player1', { playerId: 'player1' }); + clientA = io(URL, { auth: { roomId: 'room1', @@ -46,13 +49,8 @@ describe('DrawingGateway e2e 테스트', () => { }); await new Promise((resolve) => { - clientA.on('connect', () => { - resolve(); - }); + clientA.on('connect', resolve); }); - - await redisService.hset('room:room1', { roomId: 'room1' }); - await redisService.hset('room:room1:player:player1', { playerId: 'player1' }); }); afterEach(async () => { @@ -74,14 +72,19 @@ describe('DrawingGateway e2e 테스트', () => { ]; for (const auth of authInfo) { - const invalidClient = io(URL, { - auth: { auth }, + const socket = io(URL, { + auth, }); - invalidClient.on('connect_error', (e) => { - expect(e.message).toBe('Room ID and Player ID are required'); - invalidClient.close(); + await new Promise((resolve) => { + socket.on('error', (e) => { + expect(e.code).toBe(4000); + expect(e.message).toBe('Room ID and Player ID are required'); + resolve(); + }); }); + + socket.close(); } }); @@ -93,10 +96,15 @@ describe('DrawingGateway e2e 테스트', () => { }, }); - invalidRoomClient.on('connect_error', (e) => { - expect(e.message).toBe('Room not found'); - invalidRoomClient.close(); + await new Promise((resolve) => { + invalidRoomClient.on('error', (e) => { + expect(e.code).toBe(6005); + expect(e.message).toBe('Room not found'); + resolve(); + }); }); + + invalidRoomClient.close(); }); it('player가 redis 내에 존재하지 않는 경우 "Player not found in room" 에러가 발생한다.', async () => { @@ -107,10 +115,15 @@ describe('DrawingGateway e2e 테스트', () => { }, }); - invalidPlayerClient.on('connect_error', (e) => { - expect(e.message).toBe('Player not found in room'); - invalidPlayerClient.close(); + await new Promise((resolve) => { + invalidPlayerClient.on('error', (e) => { + expect(e.code).toBe(6006); + expect(e.message).toBe('Player not found in room'); + resolve(); + }); }); + + invalidPlayerClient.close(); }); it('room과 player가 정상적으로 존재하는 경우 정상적으로 연결된다.', async () => { From b1b93d8e9454c0fea8b4175d01b272b63bc92ad5 Mon Sep 17 00:00:00 2001 From: JIN Date: Tue, 14 Jan 2025 18:04:30 +0900 Subject: [PATCH 28/47] =?UTF-8?q?=F0=9F=A7=AA=20test(drawing.gateway.e2e.s?= =?UTF-8?q?pec.ts):=20=EC=A0=95=EC=83=81=EC=A0=81=EC=9C=BC=EB=A1=9C=20?= =?UTF-8?q?=EC=9D=B4=EB=B2=A4=ED=8A=B8=EB=A5=BC=20=EC=88=98=EC=8B=A0?= =?UTF-8?q?=EB=B0=9B=EC=9D=84=20=EC=88=98=20=EC=9E=88=EB=8F=84=EB=A1=9D=20?= =?UTF-8?q?=EB=B3=80=EA=B2=BD?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 기존 코드에서는 이벤트를 수신받을 준비만 하고, 실제로 이벤트를 발행하면 테스트 코드가 끝나버리는 문제가 있어서 Promise를 생성해 이벤트를 제대로 수신받는지 확인할 수 있도록 변경했다. --- .../src/drawing/drawing.gateway.e2e.spec.ts | 24 ++++++++++++++----- 1 file changed, 18 insertions(+), 6 deletions(-) diff --git a/server/src/drawing/drawing.gateway.e2e.spec.ts b/server/src/drawing/drawing.gateway.e2e.spec.ts index 9c4464fa..229c4e46 100644 --- a/server/src/drawing/drawing.gateway.e2e.spec.ts +++ b/server/src/drawing/drawing.gateway.e2e.spec.ts @@ -157,6 +157,8 @@ describe('DrawingGateway e2e 테스트', () => { * 그림 그린 사람을 제외하고 다른 사람이 존재해야 이벤트를 제대로 수신받는지 확인이 가능함 * 이를 위해 clientB를 생성 */ + await redisService.hset('room:room1:player:player2', { playerId: 'player2' }); + const clientB = io(URL, { auth: { roomId: 'room1', @@ -164,17 +166,27 @@ describe('DrawingGateway e2e 테스트', () => { }, }); - // clientB가 이벤트를 수신받을 준비를 함 - clientB.on('drawUpdated', (data) => { - expect(data).toEqual({ - playerId: 'player1', - drawingData: drawingData, + // clientB가 연결이 완료될 때까지 기다림 + await new Promise((resolve) => { + clientB.on('connect', resolve); + }); + + // drawUpdated 이벤트 수신을 위한 Promise 생성 + const drawUpdatePromise = new Promise((resolve) => { + clientB.on('drawUpdated', (data) => { + expect(data).toEqual({ + playerId: 'player1', + drawingData: drawingData, + }); + resolve(); }); - clientB.close(); }); // clientA가 실제로 이벤트를 발생시킴 clientA.emit('draw', { drawingData }); + + await drawUpdatePromise; + clientB.close(); }); }); }); From 45e2c2aac551915c2ce788b4a5ff25d7acf9c859 Mon Sep 17 00:00:00 2001 From: JIN Date: Tue, 14 Jan 2025 23:07:18 +0900 Subject: [PATCH 29/47] =?UTF-8?q?=F0=9F=A7=B9=20chore(docker-compose.yml):?= =?UTF-8?q?=20=EB=B6=88=ED=95=84=EC=9A=94=ED=95=9C=20=EB=B2=84=EC=A0=84=20?= =?UTF-8?q?=EC=A0=95=EB=B3=B4=20=EC=82=AD=EC=A0=9C?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit docker compose 최신 형식 적용 --- server/docker-compose.yml | 1 - 1 file changed, 1 deletion(-) diff --git a/server/docker-compose.yml b/server/docker-compose.yml index 0aa283ec..cc43120e 100644 --- a/server/docker-compose.yml +++ b/server/docker-compose.yml @@ -1,4 +1,3 @@ -version: '3' services: redis: image: redis:latest From d4053980372146b5f6f04670b53bf81335b0a133 Mon Sep 17 00:00:00 2001 From: sunghwki <52474291+swkim12345@users.noreply.github.com> Date: Wed, 15 Jan 2025 15:38:55 +0900 Subject: [PATCH 30/47] Delete .github/workflows/apply-issue-template.yml --- .github/workflows/apply-issue-template.yml | 25 ---------------------- 1 file changed, 25 deletions(-) delete mode 100644 .github/workflows/apply-issue-template.yml diff --git a/.github/workflows/apply-issue-template.yml b/.github/workflows/apply-issue-template.yml deleted file mode 100644 index 17f59e95..00000000 --- a/.github/workflows/apply-issue-template.yml +++ /dev/null @@ -1,25 +0,0 @@ -name: Apply Issue Template -on: - issues: - types: [opened] -jobs: - apply-template: - runs-on: ubuntu-latest - steps: - - uses: actions/checkout@v3 - - - uses: actions/github-script@v6 - with: - github-token: ${{ secrets.GITHUB_TOKEN }} - script: | - const fs = require('fs'); - const issue = context.payload.issue; - const fullTemplate = fs.readFileSync('.github/ISSUE_TEMPLATE/feature-template.md', 'utf8'); - const templateContent = fullTemplate.split('---').slice(2).join('---').trim(); - - await github.rest.issues.update({ - owner: context.repo.owner, - repo: context.repo.repo, - issue_number: issue.number, - body: templateContent - }); From 43b3a3aca924fbdc546bd432de66e1cd78fceab0 Mon Sep 17 00:00:00 2001 From: JIN Date: Thu, 16 Jan 2025 18:39:25 +0900 Subject: [PATCH 31/47] =?UTF-8?q?=F0=9F=90=9B=20fix(chat.gateway.ts):=20?= =?UTF-8?q?=EB=B9=84=EB=8F=99=EA=B8=B0=20=ED=95=A8=EC=88=98=EC=97=90=20awa?= =?UTF-8?q?it=20=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 비동기함수임에도 await 키워드가 존재하지 않아 Promise 형태로 반환되어 바로 다음 줄의 if 조건문에 에러 체크가 되지 않는 문제 해결 --- server/src/chat/chat.gateway.ts | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/server/src/chat/chat.gateway.ts b/server/src/chat/chat.gateway.ts index 08c7f10a..11b2bc5a 100644 --- a/server/src/chat/chat.gateway.ts +++ b/server/src/chat/chat.gateway.ts @@ -16,15 +16,15 @@ export class ChatGateway { @WebSocketServer() server: Server; - handleConnection(client: Socket) { + async handleConnection(client: Socket) { const roomId = client.handshake.auth.roomId; const playerId = client.handshake.auth.playerId; if (!roomId || !playerId) throw new BadRequestException('Room ID and Player ID are required'); - const roomExists = this.chatService.existsRoom(roomId); + const roomExists = await this.chatService.existsRoom(roomId); if (!roomExists) throw new RoomNotFoundException('Room not found'); - const playerExists = this.chatService.existsPlayer(roomId, playerId); + const playerExists = await this.chatService.existsPlayer(roomId, playerId); if (!playerExists) throw new PlayerNotFoundException('Player not found in room'); client.data.roomId = roomId; From 7241425ed0fcc3ce1ac17dc09b44169af49e28ef Mon Sep 17 00:00:00 2001 From: JIN Date: Thu, 16 Jan 2025 18:41:12 +0900 Subject: [PATCH 32/47] =?UTF-8?q?=F0=9F=90=9B=20fix(chat.gateway.ts):=20?= =?UTF-8?q?=EC=97=90=EB=9F=AC=20=EC=B2=98=EB=A6=AC=20=EB=B0=A9=EB=B2=95=20?= =?UTF-8?q?=EB=B3=80=EA=B2=BD?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit handleConnecion 메서드는 UseFilters의 영향을 받지 않아 제대로 된 에러 처리가 되지 않는다. 이 문제를 해결하기 위해 직접적으로 에러 이벤트를 발행하고, 연결을 끊는 코드를 추가했다. --- server/src/chat/chat.gateway.ts | 29 +++++++++++++++++++++++++---- 1 file changed, 25 insertions(+), 4 deletions(-) diff --git a/server/src/chat/chat.gateway.ts b/server/src/chat/chat.gateway.ts index 11b2bc5a..a7f26d83 100644 --- a/server/src/chat/chat.gateway.ts +++ b/server/src/chat/chat.gateway.ts @@ -3,7 +3,7 @@ import { Server, Socket } from 'socket.io'; import { ChatService } from './chat.service'; import { UseFilters } from '@nestjs/common'; import { WsExceptionFilter } from 'src/filters/ws-exception.filter'; -import { BadRequestException, PlayerNotFoundException, RoomNotFoundException } from 'src/exceptions/game.exception'; +import { BadRequestException } from 'src/exceptions/game.exception'; @WebSocketGateway({ cors: '*', @@ -20,12 +20,33 @@ export class ChatGateway { const roomId = client.handshake.auth.roomId; const playerId = client.handshake.auth.playerId; - if (!roomId || !playerId) throw new BadRequestException('Room ID and Player ID are required'); + if (!roomId || !playerId) { + client.emit('error', { + code: 4000, + message: 'Room ID and Player ID are required', + }); + client.disconnect(); + return; + } const roomExists = await this.chatService.existsRoom(roomId); - if (!roomExists) throw new RoomNotFoundException('Room not found'); + if (!roomExists) { + client.emit('error', { + code: 6005, + message: 'Room not found', + }); + client.disconnect(); + return; + } const playerExists = await this.chatService.existsPlayer(roomId, playerId); - if (!playerExists) throw new PlayerNotFoundException('Player not found in room'); + if (!playerExists) { + client.emit('error', { + code: 6006, + message: 'Player not found in room', + }); + client.disconnect(); + return; + } client.data.roomId = roomId; client.data.playerId = playerId; From 307df0662595443d99823ffab65039ab582e50f0 Mon Sep 17 00:00:00 2001 From: JIN Date: Thu, 16 Jan 2025 18:57:21 +0900 Subject: [PATCH 33/47] =?UTF-8?q?=F0=9F=90=9B=20fix(chat.repository.ts,=20?= =?UTF-8?q?drawing.repository.ts):=20=EC=98=A4=ED=83=80=20=EC=88=98?= =?UTF-8?q?=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Redis에 저장되는 값은 players인데 오타로 인해 s가 빠진 player로 검색하고 있어 오류가 발생했다. 해당 문제를 해결하기 위해 오타를 수정했다. --- server/src/chat/chat.repository.ts | 2 +- server/src/drawing/drawing.repository.ts | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/server/src/chat/chat.repository.ts b/server/src/chat/chat.repository.ts index 3b3a4a8e..04a74aec 100644 --- a/server/src/chat/chat.repository.ts +++ b/server/src/chat/chat.repository.ts @@ -24,7 +24,7 @@ export class ChatRepository { } async existsPlayer(roomId: string, playerId: string) { - const exists = await this.redisService.exists(`room:${roomId}:player:${playerId}`); + const exists = await this.redisService.exists(`room:${roomId}:players:${playerId}`); return exists === 1; } } diff --git a/server/src/drawing/drawing.repository.ts b/server/src/drawing/drawing.repository.ts index 61e6e2e1..4c19baa0 100644 --- a/server/src/drawing/drawing.repository.ts +++ b/server/src/drawing/drawing.repository.ts @@ -11,7 +11,7 @@ export class DrawingRepository { } async existsPlayer(roomId: string, playerId: string) { - const exists = await this.redisService.exists(`room:${roomId}:player:${playerId}`); + const exists = await this.redisService.exists(`room:${roomId}:players:${playerId}`); return exists === 1; } } From 1e41d65955dcb8a9f6ff233da453b260fb242e16 Mon Sep 17 00:00:00 2001 From: dannysir <48199716+dannysir@users.noreply.github.com> Date: Fri, 17 Jan 2025 16:55:18 +0900 Subject: [PATCH 34/47] Update README.md --- README.md | 27 ++++++++++++--------------- 1 file changed, 12 insertions(+), 15 deletions(-) diff --git a/README.md b/README.md index 970adbf4..5dee0b2b 100644 --- a/README.md +++ b/README.md @@ -307,24 +307,21 @@ - - - - - + + + + - - - - - + + + + - - - - - + + + +
곽수정윤태연유미라정다솔최선아김성환김준기김진서산
FE
👑 팀장
FE
부팀장
FE, BE
BE 팀장
FE
캔버스 팀장
FE
FE 팀장
BEFEBEFE
From daf36ad99ae938541befcfa8205bd2689d11fa70 Mon Sep 17 00:00:00 2001 From: ijun17 Date: Fri, 17 Jan 2025 17:15:43 +0900 Subject: [PATCH 35/47] =?UTF-8?q?fix:=20=EC=95=88=EC=93=B0=EB=8A=94=20?= =?UTF-8?q?=ED=95=A8=EC=88=98=20=EC=A3=BC=EC=84=9D=20=EC=B2=98=EB=A6=AC=20?= =?UTF-8?q?=EB=B0=8F=20=EC=BA=94=EB=B2=84=EC=8A=A4=20=EC=85=80=EB=A0=89?= =?UTF-8?q?=ED=84=B0=20=EB=B3=80=EA=B2=BD?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 안쓰이는 대부분의 함수를 주석 처리 - 캔버스 셀렉터를 'canvas'에서 'canvas + canvas'로 변경(두 번째 캔버스) --- core/crdt/test/drawing-utils.ts | 314 +++++++++++----------- core/crdt/test/test-utils.ts | 450 ++++++++++++++++---------------- 2 files changed, 386 insertions(+), 378 deletions(-) diff --git a/core/crdt/test/drawing-utils.ts b/core/crdt/test/drawing-utils.ts index 14cadc61..b0e8e8ed 100644 --- a/core/crdt/test/drawing-utils.ts +++ b/core/crdt/test/drawing-utils.ts @@ -1,6 +1,12 @@ import { Page } from '@playwright/test'; import { DrawingFunction } from './test-types'; +let SEED = 7777; +function seedRandom() { + SEED = (SEED * 16807) % 2147483647; + return (SEED - 1) / 2147483646; +} + export async function clearCanvas(page: Page): Promise { await page.evaluate(() => { const canvas = document.querySelector('canvas'); @@ -34,145 +40,146 @@ async function drawWithFillMode(page: Page, point: { x: number; y: number }): Pr export const drawingPatterns: Record = { // 동일한 사각형 그리기 (fillRect) - identicalByRect: async (page: Page) => { - await page.evaluate(() => { - const canvas = document.querySelector('canvas'); - if (!canvas) throw new Error('Canvas not found'); - const ctx = canvas.getContext('2d'); - if (!ctx) throw new Error('Cannot get 2d context'); - - ctx.beginPath(); - ctx.fillStyle = 'black'; - ctx.fillRect(0, 0, canvas.width, canvas.height); - ctx.stroke(); - }); - }, + // identicalByRect: async (page: Page) => { + // await page.evaluate(() => { + // const canvas = document.querySelector('canvas'); + // if (!canvas) throw new Error('Canvas not found'); + // const ctx = canvas.getContext('2d'); + // if (!ctx) throw new Error('Cannot get 2d context'); + + // ctx.beginPath(); + // ctx.fillStyle = 'black'; + // ctx.fillRect(0, 0, canvas.width, canvas.height); + // ctx.stroke(); + // }); + // }, // 전체 50% 채워지는 줄무늬 그리기 (fillRect) - differentByRect: async (page: Page, clientIndex?: number) => { - if (!clientIndex) return; - - await page.evaluate( - ({ index }) => { - const canvas = document.querySelector('canvas'); - if (!canvas) throw new Error('Canvas not found'); - const ctx = canvas.getContext('2d'); - if (!ctx) throw new Error('Cannot get 2d context'); - - const isEvenClient = index % 2 === 0; - ctx.beginPath(); - - if (isEvenClient) { - for (let y = 0; y < canvas.height; y += 20) { - ctx.fillStyle = 'black'; - ctx.fillRect(0, y, canvas.width, 10); - } - } else { - for (let x = 0; x < canvas.width; x += 20) { - ctx.fillStyle = 'black'; - ctx.fillRect(x, 0, 10, canvas.height); - } - } - - ctx.stroke(); - }, - { index: clientIndex }, - ); - }, + // differentByRect: async (page: Page, clientIndex?: number) => { + // if (!clientIndex) return; + + // await page.evaluate( + // ({ index }) => { + // const canvas = document.querySelector('canvas'); + // if (!canvas) throw new Error('Canvas not found'); + // const ctx = canvas.getContext('2d'); + // if (!ctx) throw new Error('Cannot get 2d context'); + + // const isEvenClient = index % 2 === 0; + // ctx.beginPath(); + + // if (isEvenClient) { + // for (let y = 0; y < canvas.height; y += 20) { + // ctx.fillStyle = 'black'; + // ctx.fillRect(0, y, canvas.width, 10); + // } + // } else { + // for (let x = 0; x < canvas.width; x += 20) { + // ctx.fillStyle = 'black'; + // ctx.fillRect(x, 0, 10, canvas.height); + // } + // } + + // ctx.stroke(); + // }, + // { index: clientIndex }, + // ); + // }, // 동일한 사각형 그리기 (Mouse events) - identicalByMouse: async (page: Page) => { - const canvas = await page.locator('canvas'); - const box = await canvas.boundingBox(); - if (!box) throw new Error('Canvas not found'); - - await page.dispatchEvent('canvas', 'mousedown', { - bubbles: true, - cancelable: true, - clientX: box.x, - clientY: box.y, - }); - - for (let x = 0; x < box.width; x += 10) { - for (let y = 0; y < box.height; y += 10) { - await page.dispatchEvent('canvas', 'mousemove', { - bubbles: true, - cancelable: true, - clientX: box.x + x, - clientY: box.y + y, - }); - await page.waitForTimeout(10); - } - } - - await page.dispatchEvent('canvas', 'mouseup', { - bubbles: true, - cancelable: true, - clientX: box.x + box.width, - clientY: box.y + box.height, - }); - }, + // identicalByMouse: async (page: Page) => { + // const canvas = await page.locator('canvas'); + // const box = await canvas.boundingBox(); + // if (!box) throw new Error('Canvas not found'); + + // await page.dispatchEvent('canvas', 'mousedown', { + // bubbles: true, + // cancelable: true, + // clientX: box.x, + // clientY: box.y, + // }); + + // for (let x = 0; x < box.width; x += 10) { + // for (let y = 0; y < box.height; y += 10) { + // await page.dispatchEvent('canvas', 'mousemove', { + // bubbles: true, + // cancelable: true, + // clientX: box.x + x, + // clientY: box.y + y, + // }); + // await page.waitForTimeout(10); + // } + // } + + // await page.dispatchEvent('canvas', 'mouseup', { + // bubbles: true, + // cancelable: true, + // clientX: box.x + box.width, + // clientY: box.y + box.height, + // }); + // }, // 전체 50% 채워지는 줄무늬 그리기 (Mouse events) - differentByMouse: async (page: Page, clientIndex?: number) => { - if (!clientIndex) return; - const canvas = await page.locator('canvas'); - const box = await canvas.boundingBox(); - if (!box) throw new Error('Canvas not found'); - - const isEvenClient = clientIndex % 2 === 0; - - await page.dispatchEvent('canvas', 'mousedown', { - bubbles: true, - cancelable: true, - clientX: box.x, - clientY: box.y, - }); - - if (isEvenClient) { - for (let y = 0; y < box.height; y += 20) { - for (let x = 0; x < box.width; x += 10) { - await page.dispatchEvent('canvas', 'mousemove', { - bubbles: true, - cancelable: true, - clientX: box.x + x, - clientY: box.y + y, - }); - await page.waitForTimeout(10); - } - } - } else { - for (let x = 0; x < box.width; x += 20) { - for (let y = 0; y < box.height; y += 10) { - await page.dispatchEvent('canvas', 'mousemove', { - bubbles: true, - cancelable: true, - clientX: box.x + x, - clientY: box.y + y, - }); - await page.waitForTimeout(10); - } - } - } - - await page.dispatchEvent('canvas', 'mouseup', { - bubbles: true, - cancelable: true, - clientX: box.x + (isEvenClient ? box.width : 20), - clientY: box.y + (isEvenClient ? 20 : box.height), - }); - }, + // differentByMouse: async (page: Page, clientIndex?: number) => { + // if (!clientIndex) return; + // const canvas = await page.locator('canvas'); + // const box = await canvas.boundingBox(); + // if (!box) throw new Error('Canvas not found'); + + // const isEvenClient = clientIndex % 2 === 0; + + // await page.dispatchEvent('canvas', 'mousedown', { + // bubbles: true, + // cancelable: true, + // clientX: box.x, + // clientY: box.y, + // }); + + // if (isEvenClient) { + // for (let y = 0; y < box.height; y += 20) { + // for (let x = 0; x < box.width; x += 10) { + // await page.dispatchEvent('canvas', 'mousemove', { + // bubbles: true, + // cancelable: true, + // clientX: box.x + x, + // clientY: box.y + y, + // }); + // await page.waitForTimeout(10); + // } + // } + // } else { + // for (let x = 0; x < box.width; x += 20) { + // for (let y = 0; y < box.height; y += 10) { + // await page.dispatchEvent('canvas', 'mousemove', { + // bubbles: true, + // cancelable: true, + // clientX: box.x + x, + // clientY: box.y + y, + // }); + // await page.waitForTimeout(10); + // } + // } + // } + + // await page.dispatchEvent('canvas', 'mouseup', { + // bubbles: true, + // cancelable: true, + // clientX: box.x + (isEvenClient ? box.width : 20), + // clientY: box.y + (isEvenClient ? 20 : box.height), + // }); + // }, // 랜덤 드로잉 (Mouse events) randomByMouse: async (page: Page) => { - const canvas = await page.locator('canvas'); + const CANVAS_SELECTOR = 'canvas + canvas'; + const canvas = await page.locator(CANVAS_SELECTOR); const box = await canvas.boundingBox(); if (!box) throw new Error('Canvas not found'); // 1. 랜덤 설정 적용 - if (Math.random() > 0.5) await selectRandomColor(page); - if (Math.random() > 0.7) await setRandomLineWidth(page); - if (Math.random() > 0.8) await toggleFillMode(page); + if (seedRandom() > 0.5) await selectRandomColor(page); + if (seedRandom() > 0.7) await setRandomLineWidth(page); + // if (seedRandom() > 0.8) await toggleFillMode(page); const margin = { x: box.width * 0.05, @@ -187,35 +194,35 @@ export const drawingPatterns: Record = { }; // 2. 랜덤 드로잉 패턴 선택 - const patternType = Math.floor(Math.random() * 4); // 0-3까지로 확장 + const patternType = Math.floor(seedRandom() * 4); // 0-3까지로 확장 switch (patternType) { case 0: // 단일 선 { const startPoint = { - x: safeArea.x + Math.random() * safeArea.width, - y: safeArea.y + Math.random() * safeArea.height, + x: safeArea.x + seedRandom() * safeArea.width, + y: safeArea.y + seedRandom() * safeArea.height, }; const endPoint = { - x: safeArea.x + Math.random() * safeArea.width, - y: safeArea.y + Math.random() * safeArea.height, + x: safeArea.x + seedRandom() * safeArea.width, + y: safeArea.y + seedRandom() * safeArea.height, }; - await page.dispatchEvent('canvas', 'mousedown', { + await page.dispatchEvent(CANVAS_SELECTOR, 'mousedown', { bubbles: true, cancelable: true, clientX: startPoint.x, clientY: startPoint.y, }); - await page.dispatchEvent('canvas', 'mousemove', { + await page.dispatchEvent(CANVAS_SELECTOR, 'mousemove', { bubbles: true, cancelable: true, clientX: endPoint.x, clientY: endPoint.y, }); - await page.dispatchEvent('canvas', 'mouseup', { + await page.dispatchEvent(CANVAS_SELECTOR, 'mouseup', { bubbles: true, cancelable: true, clientX: endPoint.x, @@ -226,12 +233,12 @@ export const drawingPatterns: Record = { case 1: // 여러 점 연결 { - const points = Array.from({ length: Math.floor(Math.random() * 5) + 3 }, () => ({ - x: safeArea.x + Math.random() * safeArea.width, - y: safeArea.y + Math.random() * safeArea.height, + const points = Array.from({ length: Math.floor(seedRandom() * 5) + 3 }, () => ({ + x: safeArea.x + seedRandom() * safeArea.width, + y: safeArea.y + seedRandom() * safeArea.height, })); - await page.dispatchEvent('canvas', 'mousedown', { + await page.dispatchEvent(CANVAS_SELECTOR, 'mousedown', { bubbles: true, cancelable: true, clientX: points[0].x, @@ -239,7 +246,7 @@ export const drawingPatterns: Record = { }); for (let i = 1; i < points.length; i++) { - await page.dispatchEvent('canvas', 'mousemove', { + await page.dispatchEvent(CANVAS_SELECTOR, 'mousemove', { bubbles: true, cancelable: true, clientX: points[i].x, @@ -248,7 +255,7 @@ export const drawingPatterns: Record = { await page.waitForTimeout(50); } - await page.dispatchEvent('canvas', 'mouseup', { + await page.dispatchEvent(CANVAS_SELECTOR, 'mouseup', { bubbles: true, cancelable: true, clientX: points[points.length - 1].x, @@ -265,12 +272,12 @@ export const drawingPatterns: Record = { const points = Array.from({ length: 20 }, (_, i) => { const angle = (i / 20) * Math.PI * 2; return { - x: centerX + Math.cos(angle) * radius * (0.8 + Math.random() * 0.4), - y: centerY + Math.sin(angle) * radius * (0.8 + Math.random() * 0.4), + x: centerX + Math.cos(angle) * radius * (0.8 + seedRandom() * 0.4), + y: centerY + Math.sin(angle) * radius * (0.8 + seedRandom() * 0.4), }; }); - await page.dispatchEvent('canvas', 'mousedown', { + await page.dispatchEvent(CANVAS_SELECTOR, 'mousedown', { bubbles: true, cancelable: true, clientX: points[0].x, @@ -278,7 +285,7 @@ export const drawingPatterns: Record = { }); for (const point of points) { - await page.dispatchEvent('canvas', 'mousemove', { + await page.dispatchEvent(CANVAS_SELECTOR, 'mousemove', { bubbles: true, cancelable: true, clientX: point.x, @@ -287,7 +294,7 @@ export const drawingPatterns: Record = { await page.waitForTimeout(20); } - await page.dispatchEvent('canvas', 'mouseup', { + await page.dispatchEvent(CANVAS_SELECTOR, 'mouseup', { bubbles: true, cancelable: true, clientX: points[points.length - 1].x, @@ -297,10 +304,11 @@ export const drawingPatterns: Record = { break; case 3: // 채우기 모드로 랜덤한 위치 채우기 + return; { const fillPoint = { - x: safeArea.x + Math.random() * safeArea.width, - y: safeArea.y + Math.random() * safeArea.height, + x: safeArea.x + seedRandom() * safeArea.width, + y: safeArea.y + seedRandom() * safeArea.height, }; // 채우기 모드 활성화 @@ -313,21 +321,21 @@ export const drawingPatterns: Record = { } // 3. 랜덤하게 되돌리기/다시실행 수행 - if (Math.random() > 0.7) { - await performUndoRedo(page); - } + // if (seedRandom() > 0.9) { + // await performUndoRedo(page); + // } }, }; async function selectRandomColor(page: Page): Promise { const colors = ['검정', '분홍', '노랑', '하늘', '회색']; - const randomColor = colors[Math.floor(Math.random() * colors.length)]; + const randomColor = colors[Math.floor(seedRandom() * colors.length)]; await page.getByLabel(`${randomColor} 색상 선택`).click(); } async function setRandomLineWidth(page: Page): Promise { await page.getByLabel('펜 모드').click(); - const lineWidth = Math.floor(Math.random() * 9) * 2 + 4; // 4-20 사이의 짝수 값 + const lineWidth = Math.floor(seedRandom() * 9) * 2 + 4; // 4-20 사이의 짝수 값 await page.getByLabel('선 굵기 조절').fill(lineWidth.toString()); } @@ -345,7 +353,7 @@ async function performUndoRedo(page: Page): Promise { const redoButton = page.getByLabel('다시실행'); const isRedoEnabled = await redoButton.isEnabled(); - if (isRedoEnabled && Math.random() > 0.5) { + if (isRedoEnabled && seedRandom() > 0.5) { await redoButton.click(); } } diff --git a/core/crdt/test/test-utils.ts b/core/crdt/test/test-utils.ts index 3c8c035e..a4f7087e 100644 --- a/core/crdt/test/test-utils.ts +++ b/core/crdt/test/test-utils.ts @@ -3,78 +3,78 @@ import { DrawingClient, TEST_CONFIG } from './test-types'; import { PNG } from 'pngjs'; import { clearCanvas } from './drawing-utils'; -export const compareCanvasPixels = async (basePage: Page, targetPage: Page): Promise => { - const getCanvasData = async (page: Page) => { - return await page.evaluate(() => { - const canvas = document.querySelector('canvas'); - if (!canvas) throw new Error('Canvas not found'); - const ctx = canvas.getContext('2d'); - if (!ctx) throw new Error('Could not get 2d context'); - - const imageData = ctx.getImageData(0, 0, canvas.width, canvas.height); - return { - data: Array.from(imageData.data), - width: canvas.width, - height: canvas.height, - }; - }); - }; - - // Promise.all을 사용하여 두 페이지의 데이터를 병렬로 가져오기 - const [pixels1, pixels2] = await Promise.all([getCanvasData(basePage), getCanvasData(targetPage)]); - - // 다른 픽셀 수 계산 - const differentPixels = pixels1.data.reduce((count, pixel, index) => { - if (pixel !== pixels2.data[index]) { - return count + 1; - } - return count; - }, 0); - - const totalPixels = pixels1.data.length; - const diffRatio = differentPixels / totalPixels; - - return diffRatio; -}; +// export const compareCanvasPixels = async (basePage: Page, targetPage: Page): Promise => { +// const getCanvasData = async (page: Page) => { +// return await page.evaluate(() => { +// const canvas = document.querySelector('canvas'); +// if (!canvas) throw new Error('Canvas not found'); +// const ctx = canvas.getContext('2d'); +// if (!ctx) throw new Error('Could not get 2d context'); + +// const imageData = ctx.getImageData(0, 0, canvas.width, canvas.height); +// return { +// data: Array.from(imageData.data), +// width: canvas.width, +// height: canvas.height, +// }; +// }); +// }; + +// // Promise.all을 사용하여 두 페이지의 데이터를 병렬로 가져오기 +// const [pixels1, pixels2] = await Promise.all([getCanvasData(basePage), getCanvasData(targetPage)]); + +// // 다른 픽셀 수 계산 +// const differentPixels = pixels1.data.reduce((count, pixel, index) => { +// if (pixel !== pixels2.data[index]) { +// return count + 1; +// } +// return count; +// }, 0); + +// const totalPixels = pixels1.data.length; +// const diffRatio = differentPixels / totalPixels; + +// return diffRatio; +// }; // 1. 픽셀 데이터 직접 비교 -export const compareByPixelData = async (basePage: Page, targetPage: Page): Promise => { - const getPixelData = async (page: Page) => { - return page.evaluate(() => { - const canvas = document.querySelector('canvas'); - if (!canvas) throw new Error('Canvas not found'); - const ctx = canvas.getContext('2d'); - if (!ctx) throw new Error('Cannot get 2d context'); - - const imageData = ctx.getImageData(0, 0, canvas.width, canvas.height); - return Array.from(imageData.data); - }); - }; - - const [basePixels, targetPixels] = await Promise.all([getPixelData(basePage), getPixelData(targetPage)]); - - let differentPixels = 0; - for (let i = 0; i < basePixels.length; i += 4) { - if ( - basePixels[i] !== targetPixels[i] || - basePixels[i + 1] !== targetPixels[i + 1] || - basePixels[i + 2] !== targetPixels[i + 2] || - basePixels[i + 3] !== targetPixels[i + 3] - ) { - differentPixels++; - } - } - - return differentPixels / (basePixels.length / 4); -}; +// export const compareByPixelData = async (basePage: Page, targetPage: Page): Promise => { +// const getPixelData = async (page: Page) => { +// return page.evaluate(() => { +// const canvas = document.querySelector('canvas'); +// if (!canvas) throw new Error('Canvas not found'); +// const ctx = canvas.getContext('2d'); +// if (!ctx) throw new Error('Cannot get 2d context'); + +// const imageData = ctx.getImageData(0, 0, canvas.width, canvas.height); +// return Array.from(imageData.data); +// }); +// }; + +// const [basePixels, targetPixels] = await Promise.all([getPixelData(basePage), getPixelData(targetPage)]); + +// let differentPixels = 0; +// for (let i = 0; i < basePixels.length; i += 4) { +// if ( +// basePixels[i] !== targetPixels[i] || +// basePixels[i + 1] !== targetPixels[i + 1] || +// basePixels[i + 2] !== targetPixels[i + 2] || +// basePixels[i + 3] !== targetPixels[i + 3] +// ) { +// differentPixels++; +// } +// } + +// return differentPixels / (basePixels.length / 4); +// }; // 2. PNG 해시값 비교 export const compareByPng = async (basePage: Page, targetPage: Page): Promise => { // 임시 대기 시간 추가 await Promise.all([basePage.waitForTimeout(100), targetPage.waitForTimeout(100)]); - const screenshot1 = await basePage.locator('canvas').screenshot(); - const screenshot2 = await targetPage.locator('canvas').screenshot(); + const screenshot1 = await basePage.locator('canvas + canvas').screenshot(); + const screenshot2 = await targetPage.locator('canvas + canvas').screenshot(); const img1 = PNG.sync.read(screenshot1); const img2 = PNG.sync.read(screenshot2); @@ -104,184 +104,184 @@ export const compareByPng = async (basePage: Page, targetPage: Page): Promise => { - try { - // 기준 스냅샷 생성 및 저장 - await expect(basePage.locator('canvas')).toHaveScreenshot('base-canvas.png'); - // 비교 스냅샷 생성 및 저장 - await expect(targetPage.locator('canvas')).toHaveScreenshot('target-canvas.png'); - - // Playwright의 스냅샷 비교는 자동으로 처리되므로, catch로 결과 분석 - return 0; // 두 스크린샷이 일치 - // eslint-disable-next-line @typescript-eslint/no-explicit-any - } catch (error: any) { - // 에러 메시지에서 차이점 퍼센트 추출 - const match = error.message.match(/(\d+\.\d+)% of all pixels/); - return match ? parseFloat(match[1]) : 100; // 차이점 반환 - } -}; +// export const compareByPlaywright = async (basePage: Page, targetPage: Page): Promise => { +// try { +// // 기준 스냅샷 생성 및 저장 +// await expect(basePage.locator('canvas')).toHaveScreenshot('base-canvas.png'); +// // 비교 스냅샷 생성 및 저장 +// await expect(targetPage.locator('canvas')).toHaveScreenshot('target-canvas.png'); + +// // Playwright의 스냅샷 비교는 자동으로 처리되므로, catch로 결과 분석 +// return 0; // 두 스크린샷이 일치 +// // eslint-disable-next-line @typescript-eslint/no-explicit-any +// } catch (error: any) { +// // 에러 메시지에서 차이점 퍼센트 추출 +// const match = error.message.match(/(\d+\.\d+)% of all pixels/); +// return match ? parseFloat(match[1]) : 100; // 차이점 반환 +// } +// }; // 공통 비교 함수 타입 정의 export type CompareFunction = (basePage: Page, targetPage: Page) => Promise; // 공통 테스트 실행 함수 -export const runCanvasComparisonTest = async ( - clients: DrawingClient[], - drawingFunction: (page: Page, clientIndex?: number) => Promise, - compareFunction: CompareFunction, - acceptanceCriteria: number, - testMode: 'identical' | 'different', -): Promise => { - console.log(`Starting sequential test with ${clients.length} clients`); - - const diffResults = []; - for (let i = 1; i < clients.length; i++) { - // 1. 첫 번째 클라이언트 그리기 및 결과 저장 - await clearCanvas(clients[0].page); - await drawingFunction(clients[0].page, 0); - const baseCanvas = await clients[0].page.evaluate(() => { - const canvas = document.querySelector('canvas'); - if (!canvas) throw new Error('Canvas not found'); - // 현재 캔버스 상태를 새로운 캔버스에 복사 - const tempCanvas = document.createElement('canvas'); - tempCanvas.width = canvas.width; - tempCanvas.height = canvas.height; - const ctx = tempCanvas.getContext('2d'); - if (!ctx) throw new Error('Canvas context not found'); - ctx.drawImage(canvas, 0, 0); - return tempCanvas.toDataURL(); // base64로 변환 - }); - - // 2. 두 번째 클라이언트 그리기 및 결과 저장 - await clearCanvas(clients[i].page); - await drawingFunction(clients[i].page, i); - const compareCanvas = await clients[i].page.evaluate(() => { - const canvas = document.querySelector('canvas'); - if (!canvas) throw new Error('Canvas not found'); - const tempCanvas = document.createElement('canvas'); - tempCanvas.width = canvas.width; - tempCanvas.height = canvas.height; - const ctx = tempCanvas.getContext('2d'); - if (!ctx) throw new Error('Canvas context not found'); - ctx.drawImage(canvas, 0, 0); - return tempCanvas.toDataURL(); - }); - - // 3. 저장된 결과를 임시 캔버스에 그리고 비교 - await clients[0].page.evaluate((base64Image: string) => { - const canvas = document.querySelector('canvas'); - if (!canvas) throw new Error('Canvas not found'); - const ctx = canvas.getContext('2d'); - if (!ctx) throw new Error('Canvas context not found'); - const img = new Image(); - return new Promise((resolve) => { - img.onload = () => { - ctx.clearRect(0, 0, canvas.width, canvas.height); - ctx.drawImage(img, 0, 0); - resolve(Promise); - }; - img.src = base64Image; - }); - }, baseCanvas); - - await clients[i].page.evaluate((base64Image: string) => { - const canvas = document.querySelector('canvas'); - if (!canvas) throw new Error('Canvas not found'); - const ctx = canvas.getContext('2d'); - if (!ctx) throw new Error('Canvas context not found'); - const img = new Image(); - return new Promise((resolve) => { - img.onload = () => { - ctx.clearRect(0, 0, canvas.width, canvas.height); - ctx.drawImage(img, 0, 0); - resolve(Promise); - }; - img.src = base64Image; - }); - }, compareCanvas); - - const diffRatio = await compareFunction(clients[0].page, clients[i].page); - diffResults.push({ - clientIndex: i, - diffRatio, - }); - } - - // 결과 검증 - diffResults.forEach(({ clientIndex, diffRatio }) => { - console.log(`Client ${clientIndex} comparison:`, { - diffRatio, - acceptanceCriteria, - passes: testMode === 'identical' ? diffRatio <= acceptanceCriteria : diffRatio >= acceptanceCriteria, - }); - - if (testMode === 'identical') { - expect(diffRatio).toBeLessThanOrEqual(acceptanceCriteria); - } else { - expect(diffRatio).toBeGreaterThanOrEqual(acceptanceCriteria); - } - }); -}; +// export const runCanvasComparisonTest = async ( +// clients: DrawingClient[], +// drawingFunction: (page: Page, clientIndex?: number) => Promise, +// compareFunction: CompareFunction, +// acceptanceCriteria: number, +// testMode: 'identical' | 'different', +// ): Promise => { +// console.log(`Starting sequential test with ${clients.length} clients`); + +// const diffResults = []; +// for (let i = 1; i < clients.length; i++) { +// // 1. 첫 번째 클라이언트 그리기 및 결과 저장 +// await clearCanvas(clients[0].page); +// await drawingFunction(clients[0].page, 0); +// const baseCanvas = await clients[0].page.evaluate(() => { +// const canvas = document.querySelector('canvas'); +// if (!canvas) throw new Error('Canvas not found'); +// // 현재 캔버스 상태를 새로운 캔버스에 복사 +// const tempCanvas = document.createElement('canvas'); +// tempCanvas.width = canvas.width; +// tempCanvas.height = canvas.height; +// const ctx = tempCanvas.getContext('2d'); +// if (!ctx) throw new Error('Canvas context not found'); +// ctx.drawImage(canvas, 0, 0); +// return tempCanvas.toDataURL(); // base64로 변환 +// }); + +// // 2. 두 번째 클라이언트 그리기 및 결과 저장 +// await clearCanvas(clients[i].page); +// await drawingFunction(clients[i].page, i); +// const compareCanvas = await clients[i].page.evaluate(() => { +// const canvas = document.querySelector('canvas'); +// if (!canvas) throw new Error('Canvas not found'); +// const tempCanvas = document.createElement('canvas'); +// tempCanvas.width = canvas.width; +// tempCanvas.height = canvas.height; +// const ctx = tempCanvas.getContext('2d'); +// if (!ctx) throw new Error('Canvas context not found'); +// ctx.drawImage(canvas, 0, 0); +// return tempCanvas.toDataURL(); +// }); + +// // 3. 저장된 결과를 임시 캔버스에 그리고 비교 +// await clients[0].page.evaluate((base64Image: string) => { +// const canvas = document.querySelector('canvas'); +// if (!canvas) throw new Error('Canvas not found'); +// const ctx = canvas.getContext('2d'); +// if (!ctx) throw new Error('Canvas context not found'); +// const img = new Image(); +// return new Promise((resolve) => { +// img.onload = () => { +// ctx.clearRect(0, 0, canvas.width, canvas.height); +// ctx.drawImage(img, 0, 0); +// resolve(Promise); +// }; +// img.src = base64Image; +// }); +// }, baseCanvas); + +// await clients[i].page.evaluate((base64Image: string) => { +// const canvas = document.querySelector('canvas'); +// if (!canvas) throw new Error('Canvas not found'); +// const ctx = canvas.getContext('2d'); +// if (!ctx) throw new Error('Canvas context not found'); +// const img = new Image(); +// return new Promise((resolve) => { +// img.onload = () => { +// ctx.clearRect(0, 0, canvas.width, canvas.height); +// ctx.drawImage(img, 0, 0); +// resolve(Promise); +// }; +// img.src = base64Image; +// }); +// }, compareCanvas); + +// const diffRatio = await compareFunction(clients[0].page, clients[i].page); +// diffResults.push({ +// clientIndex: i, +// diffRatio, +// }); +// } + +// // 결과 검증 +// diffResults.forEach(({ clientIndex, diffRatio }) => { +// console.log(`Client ${clientIndex} comparison:`, { +// diffRatio, +// acceptanceCriteria, +// passes: testMode === 'identical' ? diffRatio <= acceptanceCriteria : diffRatio >= acceptanceCriteria, +// }); + +// if (testMode === 'identical') { +// expect(diffRatio).toBeLessThanOrEqual(acceptanceCriteria); +// } else { +// expect(diffRatio).toBeGreaterThanOrEqual(acceptanceCriteria); +// } +// }); +// }; // 테스트 설정 초기화 함수 -export async function setupSameURL(clientCount: number, browser: Browser): Promise { - const clients: DrawingClient[] = []; +// export async function setupSameURL(clientCount: number, browser: Browser): Promise { +// const clients: DrawingClient[] = []; - for (let i = 0; i < clientCount; i++) { - const context = await browser.newContext({ - viewport: TEST_CONFIG.viewport, - }); - const page = await context.newPage(); +// for (let i = 0; i < clientCount; i++) { +// const context = await browser.newContext({ +// viewport: TEST_CONFIG.viewport, +// }); +// const page = await context.newPage(); - // 테스트 페이지로 접속 - await page.goto(TEST_CONFIG.url); +// // 테스트 페이지로 접속 +// await page.goto(TEST_CONFIG.url); - // 캔버스 요소가 로드될 때까지 대기 - await page.waitForSelector('canvas'); +// // 캔버스 요소가 로드될 때까지 대기 +// await page.waitForSelector('canvas'); - clients.push({ context, page }); - console.log(`Client ${i} connected to test page`); - } +// clients.push({ context, page }); +// console.log(`Client ${i} connected to test page`); +// } - return clients; -} +// return clients; +// } -export async function setupDifferentURL(clientCount: number, browser: Browser): Promise { - const clients: DrawingClient[] = []; +// export async function setupDifferentURL(clientCount: number, browser: Browser): Promise { +// const clients: DrawingClient[] = []; - for (let i = 0; i < clientCount; i++) { - const context = await browser.newContext({ - viewport: TEST_CONFIG.viewport, - }); - const page = await context.newPage(); +// for (let i = 0; i < clientCount; i++) { +// const context = await browser.newContext({ +// viewport: TEST_CONFIG.viewport, +// }); +// const page = await context.newPage(); - // 각 클라이언트마다 다른 URL로 접속 - const uniqueUrl = `${TEST_CONFIG.url}/canvas?id=test-${i}`; - // 또는 완전히 다른 경로도 가능 - // const uniqueUrl = `${TEST_CONFIG.baseUrl}/canvas-${i}`; +// // 각 클라이언트마다 다른 URL로 접속 +// const uniqueUrl = `${TEST_CONFIG.url}/canvas?id=test-${i}`; +// // 또는 완전히 다른 경로도 가능 +// // const uniqueUrl = `${TEST_CONFIG.baseUrl}/canvas-${i}`; - await page.goto(uniqueUrl); - await page.waitForSelector('canvas'); +// await page.goto(uniqueUrl); +// await page.waitForSelector('canvas'); - clients.push({ context, page }); - console.log(`Client ${i} connected to independent canvas at ${uniqueUrl}`); - } +// clients.push({ context, page }); +// console.log(`Client ${i} connected to independent canvas at ${uniqueUrl}`); +// } - return clients; -} +// return clients; +// } // 리소스 정리 함수 -export async function cleanupClients(clients: DrawingClient[]) { - for (const client of clients) { - try { - if (client.page && !client.page.isClosed()) { - await client.page.close().catch(() => {}); - } - if (client.context) { - await client.context.close().catch(() => {}); - } - } catch (error) { - console.error('Error closing client:', error); - } - } -} +// export async function cleanupClients(clients: DrawingClient[]) { +// for (const client of clients) { +// try { +// if (client.page && !client.page.isClosed()) { +// await client.page.close().catch(() => {}); +// } +// if (client.context) { +// await client.context.close().catch(() => {}); +// } +// } catch (error) { +// console.error('Error closing client:', error); +// } +// } +// } From b63d2aeddb026ab3a60469ff7403c9fbb17e30bf Mon Sep 17 00:00:00 2001 From: ijun17 Date: Fri, 17 Jan 2025 17:16:13 +0900 Subject: [PATCH 36/47] =?UTF-8?q?feat:=20=ED=94=8C=EB=A0=88=EC=9D=B4?= =?UTF-8?q?=EB=9D=BC=EC=9D=B4=ED=8A=B8=20=EA=B2=B0=EA=B3=BC=20=ED=8C=8C?= =?UTF-8?q?=EC=9D=BC=EC=9D=84=20gitignore=EC=97=90=20=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .gitignore | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/.gitignore b/.gitignore index c1d7f412..1d0962fd 100644 --- a/.gitignore +++ b/.gitignore @@ -12,6 +12,15 @@ dist dist-ssr *.local +# playwright +playwright-report +test-results +test-user-data-1 +test-user-data-2 +test-user-data-3 +test-user-data-4 +test-user-data-5 + # Editor directories and files .vscode/* !.vscode/extensions.json From 9a363ff0cb7bf894415f5e31c8177eb5d3f0f136 Mon Sep 17 00:00:00 2001 From: ijun17 Date: Fri, 17 Jan 2025 17:16:59 +0900 Subject: [PATCH 37/47] =?UTF-8?q?test:=20=ED=94=8C=EB=A0=88=EC=9D=B4?= =?UTF-8?q?=EB=9D=BC=EC=9D=B4=ED=8A=B8=EC=9D=98=20newCDPSession=EB=A1=9C?= =?UTF-8?q?=20=EC=84=B1=EB=8A=A5=20=EC=B8=A1=EC=A0=95=20=EC=8B=9C=EB=82=98?= =?UTF-8?q?=EB=A6=AC=EC=98=A4=20=ED=85=8C=EC=8A=A4=ED=8A=B8?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../test/random-drawing-performance.test.ts | 180 ++++++++++++++++++ 1 file changed, 180 insertions(+) create mode 100644 core/crdt/test/random-drawing-performance.test.ts diff --git a/core/crdt/test/random-drawing-performance.test.ts b/core/crdt/test/random-drawing-performance.test.ts new file mode 100644 index 00000000..3dd70b25 --- /dev/null +++ b/core/crdt/test/random-drawing-performance.test.ts @@ -0,0 +1,180 @@ +import { test as base, expect, Page, chromium, BrowserContext, firefox, webkit } from '@playwright/test'; +import { compareByPng } from './test-utils'; +import { drawingPatterns } from './drawing-utils'; + +interface TestClient { + page: Page; + context: BrowserContext; + role?: string; + isHost: boolean; +} + +const test = base.extend({}); + +async function setupTestRoom(baseUrl: string): Promise { + const clients: TestClient[] = []; + + const contexts = await Promise.all([ + chromium.launchPersistentContext('./test-user-data-1', {}), + chromium.launchPersistentContext('./test-user-data-2', {}), + chromium.launchPersistentContext('./test-user-data-3', {}), + chromium.launchPersistentContext('./test-user-data-4', {}), + chromium.launchPersistentContext('./test-user-data-5', {}), + ]); + + // 호스트 설정 + const hostPage = await contexts[0].newPage(); + await hostPage.goto(baseUrl); + await hostPage.getByRole('button', { name: '방 만들기' }).click(); + await hostPage.getByRole('button', { name: '복사 완료! 🔗 초대' }).click(); + const roomUrl = hostPage.url(); + + clients.push({ + page: hostPage, + context: contexts[0], + isHost: true, + }); + + // 나머지 클라이언트 접속 + for (let i = 1; i < contexts.length; i++) { + const page = await contexts[i].newPage(); + await page.goto(roomUrl); + clients.push({ + page, + context: contexts[i], + isHost: false, + }); + } + + // 호스트가 게임 시작 + await clients[0].page.getByRole('button', { name: '게임 시작' }).click(); + await clients[0].page.getByText('곧 게임이 시작됩니다!').waitFor({ state: 'visible' }); + + // 게임 화면으로 전환 + await Promise.all(clients.map((client) => client.page.waitForURL((url) => url.toString().includes('/game/')))); + + // 각 클라이언트의 역할 모달 대기 및 역할 확인 + await Promise.all( + clients.map(async (client) => { + try { + await client.page.waitForSelector('#modal-root > *', { + timeout: 30000, + state: 'visible', + }); + + const painterRole = await client.page.locator('#modal-root').getByText('그림꾼', { exact: true }); + const devilRole = await client.page.locator('#modal-root').getByText('방해꾼', { exact: true }); + const guesserRole = await client.page.locator('#modal-root').getByText('구경꾼', { exact: true }); + + const isPainter = (await painterRole.count()) > 0; + const isDevil = (await devilRole.count()) > 0; + const isGuesser = (await guesserRole.count()) > 0; + + if (isPainter) { + client.role = 'PAINTER'; + await painterRole.click(); + } else if (isDevil) { + client.role = 'DEVIL'; + await devilRole.click(); + } else if (isGuesser) { + client.role = 'GUESSER'; + await guesserRole.click(); + } + + console.log(`Client assigned role: ${client.role}`); + } catch (error) { + console.error(`Modal detection failed for client:`, error); + throw error; + } + }), + ); + + return clients; +} + +test.describe('Game Room Drawing Test', () => { + let clients: TestClient[] = []; + + test.afterEach(async () => { + for (const client of clients) { + try { + if (!client.page.isClosed()) { + await client.page.close(); + } + await client.context.close(); + } catch (error) { + console.error('Cleanup failed:', error); + } + } + clients = []; + }); + + test('Drawing synchronization test with multiple browsers', async () => { + try { + // 셋업 및 모달 처리 + clients = await setupTestRoom('http://localhost:5173'); + const drawers = clients.filter((client) => ['PAINTER', 'DEVIL'].includes(client.role || '')); + + // 모달 닫힌 후 시작 시간 기록 + const testStartTime = Date.now(); + + // 1단계: 처음 5초 대기 + // const waitEndTime = testStartTime + 5000; + const waitEndTime = testStartTime + 1000; + console.log('Waiting 5 seconds before drawing...'); + while (Date.now() < waitEndTime) { + await new Promise((resolve) => setTimeout(resolve, 100)); + } + + // 2단계: 30초 동안 드로잉 + const drawingEndTime = waitEndTime + 30000; + console.log('Starting 30 seconds drawing phase...'); + + console.log(clients.filter((client) => ['PAINTER', 'DEVIL'].includes(client.role || '')).map((e) => e.role)); + + while (Date.now() < drawingEndTime) { + console.log('성능 측정 시작'); + const cdpSessions = await Promise.all( + clients + .filter((client) => ['PAINTER', 'DEVIL'].includes(client.role || '')) + .map((e) => e.context.newCDPSession(e.page)), + ); + await Promise.all(cdpSessions.map((e) => e.send('Performance.enable'))); + + await Promise.all( + drawers.map(async (drawer) => { + try { + if (!drawer.page.isClosed()) { + await drawingPatterns.randomByMouse(drawer.page); + await drawer.page.waitForTimeout(100); + } + } catch (error) { + console.error(`Drawing failed for ${drawer.role}:`, error); + } + }), + ); + + const performanceMetrics = await Promise.all(cdpSessions.map((e) => e.send('Performance.getMetrics'))); + const formattedMetrics = performanceMetrics.map((e) => + e.metrics.reduce( + (acc, cur) => { + acc[cur.name] = cur.value; + return acc; + }, + {} as Record, + ), + ); + + console.log(JSON.stringify(formattedMetrics, null, 4)); + console.log('성능 측정 종료'); + break; + } + + // 테스트 종료 + await Promise.all(clients.map((e) => e.context.close())); + } catch (error) { + console.error('Test failed:', error); + throw error; + } + }); +}); From f06b27b5796c01fe4a14ec60be98879ad0092fc6 Mon Sep 17 00:00:00 2001 From: ijun17 Date: Sat, 18 Jan 2025 03:30:30 +0900 Subject: [PATCH 38/47] =?UTF-8?q?test:=20=EC=84=B1=EB=8A=A5=20=ED=85=8C?= =?UTF-8?q?=EC=8A=A4=ED=8A=B8=20=EC=8B=9C=EB=82=98=EB=A6=AC=EC=98=A4=20?= =?UTF-8?q?=EB=B3=80=EA=B2=BD?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - cpu 성능 4배 속도 제한 - 성능 메트릭 제한 - 드로잉 시간 20초로 변경 --- .../test/random-drawing-performance.test.ts | 75 +++++++++++-------- 1 file changed, 44 insertions(+), 31 deletions(-) diff --git a/core/crdt/test/random-drawing-performance.test.ts b/core/crdt/test/random-drawing-performance.test.ts index 3dd70b25..41ecdfdf 100644 --- a/core/crdt/test/random-drawing-performance.test.ts +++ b/core/crdt/test/random-drawing-performance.test.ts @@ -109,7 +109,7 @@ test.describe('Game Room Drawing Test', () => { clients = []; }); - test('Drawing synchronization test with multiple browsers', async () => { + test('Drawing performance test with multiple browsers', async () => { try { // 셋업 및 모달 처리 clients = await setupTestRoom('http://localhost:5173'); @@ -119,57 +119,70 @@ test.describe('Game Room Drawing Test', () => { const testStartTime = Date.now(); // 1단계: 처음 5초 대기 - // const waitEndTime = testStartTime + 5000; const waitEndTime = testStartTime + 1000; console.log('Waiting 5 seconds before drawing...'); while (Date.now() < waitEndTime) { await new Promise((resolve) => setTimeout(resolve, 100)); } + // 성능 측정 시작 + const cdpSessions = await Promise.all(drawers.map((e) => e.context.newCDPSession(e.page))); + await Promise.all(cdpSessions.map((e) => e.send('Emulation.setCPUThrottlingRate', { rate: 4 }))); + console.log('Emulation.setCPUThrottlingRate'); + await Promise.all(cdpSessions.map((e) => e.send('Performance.enable'))); + console.log('Performance.enable'); + // 2단계: 30초 동안 드로잉 - const drawingEndTime = waitEndTime + 30000; + const drawingTime = 20000; + const drawingStartTime = Date.now(); console.log('Starting 30 seconds drawing phase...'); - - console.log(clients.filter((client) => ['PAINTER', 'DEVIL'].includes(client.role || '')).map((e) => e.role)); - - while (Date.now() < drawingEndTime) { - console.log('성능 측정 시작'); - const cdpSessions = await Promise.all( - clients - .filter((client) => ['PAINTER', 'DEVIL'].includes(client.role || '')) - .map((e) => e.context.newCDPSession(e.page)), - ); - await Promise.all(cdpSessions.map((e) => e.send('Performance.enable'))); - + const DRAW_COUNT = 20; + let curDrawCount = 0; + while (curDrawCount < DRAW_COUNT) { + if (Date.now() - drawingStartTime < drawingTime * (curDrawCount / DRAW_COUNT)) continue; + console.log(curDrawCount++); await Promise.all( drawers.map(async (drawer) => { try { if (!drawer.page.isClosed()) { await drawingPatterns.randomByMouse(drawer.page); - await drawer.page.waitForTimeout(100); } } catch (error) { console.error(`Drawing failed for ${drawer.role}:`, error); } }), ); - - const performanceMetrics = await Promise.all(cdpSessions.map((e) => e.send('Performance.getMetrics'))); - const formattedMetrics = performanceMetrics.map((e) => - e.metrics.reduce( - (acc, cur) => { - acc[cur.name] = cur.value; - return acc; - }, - {} as Record, - ), - ); - - console.log(JSON.stringify(formattedMetrics, null, 4)); - console.log('성능 측정 종료'); - break; } + // 성능 측정 종료 + const METRIC_FILTER = [ + 'Timestamp', + 'LayoutCount', + 'RecalcStyleCount', + 'LayoutDuration', + 'RecalcStyleDuration', + 'ScriptDuration', + 'V8CompileDuration', + 'TaskDuration', + 'TaskOtherDuration', + 'DevToolsCommandDuration', + 'ThreadTime', + 'ProcessTime', + 'JSHeapUsedSize', + 'JSHeapTotalSize', + ]; + const performanceMetrics = await Promise.all(cdpSessions.map((e) => e.send('Performance.getMetrics'))); + const formattedMetrics = performanceMetrics.map((e) => + e.metrics.reduce( + (acc, cur) => { + if (METRIC_FILTER.includes(cur.name)) acc[cur.name] = cur.value; + return acc; + }, + {} as Record, + ), + ); + console.log(JSON.stringify(formattedMetrics, null, 4)); + // 테스트 종료 await Promise.all(clients.map((e) => e.context.close())); } catch (error) { From bac049ee28743438ff75bc393d2d7664f9278df6 Mon Sep 17 00:00:00 2001 From: ijun17 Date: Sat, 18 Jan 2025 03:33:28 +0900 Subject: [PATCH 39/47] =?UTF-8?q?test:=20playwright=20=ED=8C=8C=EC=9D=BC?= =?UTF-8?q?=20=EC=9D=B4=EB=A6=84=20=ED=98=95=EC=8B=9D=20=EB=B3=80=EA=B2=BD?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - .test.ts에서 .spec.ts로 변경 - 기존의 vitest랑 겹치는 문제로 인해 --- ...ng-performance.test.ts => random-drawing-performance.spec.ts} | 0 .../crdt/test/{random-drawing.test.ts => random-drawing.spec.ts} | 0 core/playwright.config.ts | 1 + 3 files changed, 1 insertion(+) rename core/crdt/test/{random-drawing-performance.test.ts => random-drawing-performance.spec.ts} (100%) rename core/crdt/test/{random-drawing.test.ts => random-drawing.spec.ts} (100%) diff --git a/core/crdt/test/random-drawing-performance.test.ts b/core/crdt/test/random-drawing-performance.spec.ts similarity index 100% rename from core/crdt/test/random-drawing-performance.test.ts rename to core/crdt/test/random-drawing-performance.spec.ts diff --git a/core/crdt/test/random-drawing.test.ts b/core/crdt/test/random-drawing.spec.ts similarity index 100% rename from core/crdt/test/random-drawing.test.ts rename to core/crdt/test/random-drawing.spec.ts diff --git a/core/playwright.config.ts b/core/playwright.config.ts index ceabbca7..b5f388f6 100644 --- a/core/playwright.config.ts +++ b/core/playwright.config.ts @@ -2,6 +2,7 @@ import { PlaywrightTestConfig, devices } from '@playwright/test'; const config: PlaywrightTestConfig = { testDir: './crdt/test', + testMatch: '**/*.spec.ts', workers: 5, fullyParallel: true, timeout: 180000, From 9bbb0f5307bdef2a29ad6f6f44937e7da73e292a Mon Sep 17 00:00:00 2001 From: ijun17 Date: Mon, 20 Jan 2025 10:56:12 +0900 Subject: [PATCH 40/47] =?UTF-8?q?fix:=20=ED=94=8C=EB=A0=88=EC=9D=B4?= =?UTF-8?q?=EB=9D=BC=EC=9D=B4=ED=8A=B8=20=ED=85=8C=EC=8A=A4=ED=8A=B8=20?= =?UTF-8?q?=EC=BA=94=EB=B2=84=EC=8A=A4=20=EB=B9=84=EA=B5=90=20=ED=95=A8?= =?UTF-8?q?=EC=88=98=20=EC=85=80=EB=A0=89=ED=84=B0=20=EB=B3=80=EA=B2=BD?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 두번째 캔버스에서 첫번째 캔버스를 가리키도록 변경경 --- core/crdt/test/test-utils.ts | 8 +++----- 1 file changed, 3 insertions(+), 5 deletions(-) diff --git a/core/crdt/test/test-utils.ts b/core/crdt/test/test-utils.ts index a4f7087e..8e5261b4 100644 --- a/core/crdt/test/test-utils.ts +++ b/core/crdt/test/test-utils.ts @@ -1,7 +1,5 @@ -import { Browser, expect, Page } from '@playwright/test'; -import { DrawingClient, TEST_CONFIG } from './test-types'; +import { Page } from '@playwright/test'; import { PNG } from 'pngjs'; -import { clearCanvas } from './drawing-utils'; // export const compareCanvasPixels = async (basePage: Page, targetPage: Page): Promise => { // const getCanvasData = async (page: Page) => { @@ -73,8 +71,8 @@ export const compareByPng = async (basePage: Page, targetPage: Page): Promise Date: Mon, 20 Jan 2025 11:32:49 +0900 Subject: [PATCH 41/47] =?UTF-8?q?test:=20seed=20=EA=B8=B0=EB=B0=98=20?= =?UTF-8?q?=EB=9E=9C=EB=8D=A4=20=ED=95=A8=EC=88=98=EB=A1=9C=20=ED=95=AD?= =?UTF-8?q?=EC=83=81=20=EA=B1=B0=EC=9D=98=20=EB=8F=99=EC=9D=BC=ED=95=9C=20?= =?UTF-8?q?=EA=B7=B8=EB=A6=AC=EA=B8=B0=EB=A5=BC=20=ED=95=98=EA=B2=8C=20?= =?UTF-8?q?=EB=B3=80=EA=B2=BD?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 단 플레이어 별로 그리는 순서는 보장하지 않음 --- core/crdt/test/drawing-utils.ts | 71 ++++++++++++++++++--------------- 1 file changed, 38 insertions(+), 33 deletions(-) diff --git a/core/crdt/test/drawing-utils.ts b/core/crdt/test/drawing-utils.ts index b0e8e8ed..cdc5beb8 100644 --- a/core/crdt/test/drawing-utils.ts +++ b/core/crdt/test/drawing-utils.ts @@ -1,10 +1,15 @@ import { Page } from '@playwright/test'; import { DrawingFunction } from './test-types'; -let SEED = 7777; -function seedRandom() { - SEED = (SEED * 16807) % 2147483647; - return (SEED - 1) / 2147483646; +let initialSeed = 825347; +const seedMap = new Map(); +function seedRandom(key: any) { + if (!seedMap.has(key)) seedMap.set(key, initialSeed); + let seed = seedMap.get(key); + initialSeed *= 6807; + seed = (seed * 16807) % 2147483647; + seedMap.set(key, seed); + return (seed - 1) / 2147483646; } export async function clearCanvas(page: Page): Promise { @@ -172,14 +177,14 @@ export const drawingPatterns: Record = { // 랜덤 드로잉 (Mouse events) randomByMouse: async (page: Page) => { const CANVAS_SELECTOR = 'canvas + canvas'; - const canvas = await page.locator(CANVAS_SELECTOR); + const canvas = page.locator(CANVAS_SELECTOR); const box = await canvas.boundingBox(); if (!box) throw new Error('Canvas not found'); // 1. 랜덤 설정 적용 - if (seedRandom() > 0.5) await selectRandomColor(page); - if (seedRandom() > 0.7) await setRandomLineWidth(page); - // if (seedRandom() > 0.8) await toggleFillMode(page); + if (seedRandom(page) > 0.5) await selectRandomColor(page); + if (seedRandom(page) > 0.7) await setRandomLineWidth(page); + // if (seedRandom(page) > 0.8) await toggleFillMode(page); const margin = { x: box.width * 0.05, @@ -194,35 +199,35 @@ export const drawingPatterns: Record = { }; // 2. 랜덤 드로잉 패턴 선택 - const patternType = Math.floor(seedRandom() * 4); // 0-3까지로 확장 + const patternType = Math.floor(seedRandom(page) * 4); // 0-3까지로 확장 switch (patternType) { case 0: // 단일 선 { const startPoint = { - x: safeArea.x + seedRandom() * safeArea.width, - y: safeArea.y + seedRandom() * safeArea.height, + x: safeArea.x + seedRandom(page) * safeArea.width, + y: safeArea.y + seedRandom(page) * safeArea.height, }; const endPoint = { - x: safeArea.x + seedRandom() * safeArea.width, - y: safeArea.y + seedRandom() * safeArea.height, + x: safeArea.x + seedRandom(page) * safeArea.width, + y: safeArea.y + seedRandom(page) * safeArea.height, }; - await page.dispatchEvent(CANVAS_SELECTOR, 'mousedown', { + await canvas.dispatchEvent('mousedown', { bubbles: true, cancelable: true, clientX: startPoint.x, clientY: startPoint.y, }); - await page.dispatchEvent(CANVAS_SELECTOR, 'mousemove', { + await canvas.dispatchEvent('mousemove', { bubbles: true, cancelable: true, clientX: endPoint.x, clientY: endPoint.y, }); - await page.dispatchEvent(CANVAS_SELECTOR, 'mouseup', { + await canvas.dispatchEvent('mouseup', { bubbles: true, cancelable: true, clientX: endPoint.x, @@ -233,12 +238,12 @@ export const drawingPatterns: Record = { case 1: // 여러 점 연결 { - const points = Array.from({ length: Math.floor(seedRandom() * 5) + 3 }, () => ({ - x: safeArea.x + seedRandom() * safeArea.width, - y: safeArea.y + seedRandom() * safeArea.height, + const points = Array.from({ length: Math.floor(seedRandom(page) * 5) + 3 }, () => ({ + x: safeArea.x + seedRandom(page) * safeArea.width, + y: safeArea.y + seedRandom(page) * safeArea.height, })); - await page.dispatchEvent(CANVAS_SELECTOR, 'mousedown', { + await canvas.dispatchEvent('mousedown', { bubbles: true, cancelable: true, clientX: points[0].x, @@ -246,7 +251,7 @@ export const drawingPatterns: Record = { }); for (let i = 1; i < points.length; i++) { - await page.dispatchEvent(CANVAS_SELECTOR, 'mousemove', { + await canvas.dispatchEvent('mousemove', { bubbles: true, cancelable: true, clientX: points[i].x, @@ -255,7 +260,7 @@ export const drawingPatterns: Record = { await page.waitForTimeout(50); } - await page.dispatchEvent(CANVAS_SELECTOR, 'mouseup', { + await canvas.dispatchEvent('mouseup', { bubbles: true, cancelable: true, clientX: points[points.length - 1].x, @@ -272,12 +277,12 @@ export const drawingPatterns: Record = { const points = Array.from({ length: 20 }, (_, i) => { const angle = (i / 20) * Math.PI * 2; return { - x: centerX + Math.cos(angle) * radius * (0.8 + seedRandom() * 0.4), - y: centerY + Math.sin(angle) * radius * (0.8 + seedRandom() * 0.4), + x: centerX + Math.cos(angle) * radius * (0.8 + seedRandom(page) * 0.4), + y: centerY + Math.sin(angle) * radius * (0.8 + seedRandom(page) * 0.4), }; }); - await page.dispatchEvent(CANVAS_SELECTOR, 'mousedown', { + await canvas.dispatchEvent('mousedown', { bubbles: true, cancelable: true, clientX: points[0].x, @@ -285,7 +290,7 @@ export const drawingPatterns: Record = { }); for (const point of points) { - await page.dispatchEvent(CANVAS_SELECTOR, 'mousemove', { + await canvas.dispatchEvent('mousemove', { bubbles: true, cancelable: true, clientX: point.x, @@ -294,7 +299,7 @@ export const drawingPatterns: Record = { await page.waitForTimeout(20); } - await page.dispatchEvent(CANVAS_SELECTOR, 'mouseup', { + await canvas.dispatchEvent('mouseup', { bubbles: true, cancelable: true, clientX: points[points.length - 1].x, @@ -307,8 +312,8 @@ export const drawingPatterns: Record = { return; { const fillPoint = { - x: safeArea.x + seedRandom() * safeArea.width, - y: safeArea.y + seedRandom() * safeArea.height, + x: safeArea.x + seedRandom(page) * safeArea.width, + y: safeArea.y + seedRandom(page) * safeArea.height, }; // 채우기 모드 활성화 @@ -321,7 +326,7 @@ export const drawingPatterns: Record = { } // 3. 랜덤하게 되돌리기/다시실행 수행 - // if (seedRandom() > 0.9) { + // if (seedRandom(page) > 0.9) { // await performUndoRedo(page); // } }, @@ -329,13 +334,13 @@ export const drawingPatterns: Record = { async function selectRandomColor(page: Page): Promise { const colors = ['검정', '분홍', '노랑', '하늘', '회색']; - const randomColor = colors[Math.floor(seedRandom() * colors.length)]; + const randomColor = colors[Math.floor(seedRandom(page) * colors.length)]; await page.getByLabel(`${randomColor} 색상 선택`).click(); } async function setRandomLineWidth(page: Page): Promise { await page.getByLabel('펜 모드').click(); - const lineWidth = Math.floor(seedRandom() * 9) * 2 + 4; // 4-20 사이의 짝수 값 + const lineWidth = Math.floor(seedRandom(page) * 9) * 2 + 4; // 4-20 사이의 짝수 값 await page.getByLabel('선 굵기 조절').fill(lineWidth.toString()); } @@ -353,7 +358,7 @@ async function performUndoRedo(page: Page): Promise { const redoButton = page.getByLabel('다시실행'); const isRedoEnabled = await redoButton.isEnabled(); - if (isRedoEnabled && seedRandom() > 0.5) { + if (isRedoEnabled && seedRandom(page) > 0.5) { await redoButton.click(); } } From dcbf1f53cc523f160bd4393e34e2d5e7febc3963 Mon Sep 17 00:00:00 2001 From: ijun17 Date: Mon, 20 Jan 2025 12:11:39 +0900 Subject: [PATCH 42/47] =?UTF-8?q?test:=20=ED=94=8C=EB=A0=88=EC=9D=B4=20?= =?UTF-8?q?=EB=9D=BC=EC=9D=B4=ED=8A=B8=20=EC=84=B1=EB=8A=A5=20=ED=85=8C?= =?UTF-8?q?=EC=8A=A4=ED=8A=B8=20=EB=B3=80=EA=B2=BD?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 게스트 페이지 로딩 promise all로 처리 - 불필요한 await 제거 - cpu 성능 4배 -> 2배 변경 - 메트릭의 timestamp 제거 - 드로워 뿐만 아니라 게서도 성능 측정 --- .../test/random-drawing-performance.spec.ts | 78 +++++++++++-------- 1 file changed, 44 insertions(+), 34 deletions(-) diff --git a/core/crdt/test/random-drawing-performance.spec.ts b/core/crdt/test/random-drawing-performance.spec.ts index 41ecdfdf..c5893d5d 100644 --- a/core/crdt/test/random-drawing-performance.spec.ts +++ b/core/crdt/test/random-drawing-performance.spec.ts @@ -1,5 +1,4 @@ -import { test as base, expect, Page, chromium, BrowserContext, firefox, webkit } from '@playwright/test'; -import { compareByPng } from './test-utils'; +import { test as base, Page, chromium, BrowserContext } from '@playwright/test'; import { drawingPatterns } from './drawing-utils'; interface TestClient { @@ -14,19 +13,18 @@ const test = base.extend({}); async function setupTestRoom(baseUrl: string): Promise { const clients: TestClient[] = []; - const contexts = await Promise.all([ - chromium.launchPersistentContext('./test-user-data-1', {}), - chromium.launchPersistentContext('./test-user-data-2', {}), - chromium.launchPersistentContext('./test-user-data-3', {}), - chromium.launchPersistentContext('./test-user-data-4', {}), - chromium.launchPersistentContext('./test-user-data-5', {}), - ]); + const browser = await chromium.launch(); + const contexts = await Promise.all( + Array(5) + .fill(browser) + .map((e) => e.newContext()), + ); // 호스트 설정 const hostPage = await contexts[0].newPage(); await hostPage.goto(baseUrl); await hostPage.getByRole('button', { name: '방 만들기' }).click(); - await hostPage.getByRole('button', { name: '복사 완료! 🔗 초대' }).click(); + await hostPage.waitForURL('**/lobby/*'); const roomUrl = hostPage.url(); clients.push({ @@ -36,15 +34,19 @@ async function setupTestRoom(baseUrl: string): Promise { }); // 나머지 클라이언트 접속 - for (let i = 1; i < contexts.length; i++) { - const page = await contexts[i].newPage(); - await page.goto(roomUrl); - clients.push({ - page, - context: contexts[i], - isHost: false, - }); - } + clients.push( + ...(await Promise.all( + contexts.slice(1).map(async (context) => { + const page = await context.newPage(); + await page.goto(roomUrl); + return { + page, + context, + isHost: false, + }; + }), + )), + ); // 호스트가 게임 시작 await clients[0].page.getByRole('button', { name: '게임 시작' }).click(); @@ -62,9 +64,9 @@ async function setupTestRoom(baseUrl: string): Promise { state: 'visible', }); - const painterRole = await client.page.locator('#modal-root').getByText('그림꾼', { exact: true }); - const devilRole = await client.page.locator('#modal-root').getByText('방해꾼', { exact: true }); - const guesserRole = await client.page.locator('#modal-root').getByText('구경꾼', { exact: true }); + const painterRole = client.page.locator('#modal-root').getByText('그림꾼', { exact: true }); + const devilRole = client.page.locator('#modal-root').getByText('방해꾼', { exact: true }); + const guesserRole = client.page.locator('#modal-root').getByText('구경꾼', { exact: true }); const isPainter = (await painterRole.count()) > 0; const isDevil = (await devilRole.count()) > 0; @@ -126,8 +128,8 @@ test.describe('Game Room Drawing Test', () => { } // 성능 측정 시작 - const cdpSessions = await Promise.all(drawers.map((e) => e.context.newCDPSession(e.page))); - await Promise.all(cdpSessions.map((e) => e.send('Emulation.setCPUThrottlingRate', { rate: 4 }))); + const cdpSessions = await Promise.all(clients.map((e) => e.context.newCDPSession(e.page))); + await Promise.all(cdpSessions.map((e) => e.send('Emulation.setCPUThrottlingRate', { rate: 2 }))); console.log('Emulation.setCPUThrottlingRate'); await Promise.all(cdpSessions.map((e) => e.send('Performance.enable'))); console.log('Performance.enable'); @@ -156,7 +158,6 @@ test.describe('Game Room Drawing Test', () => { // 성능 측정 종료 const METRIC_FILTER = [ - 'Timestamp', 'LayoutCount', 'RecalcStyleCount', 'LayoutDuration', @@ -171,17 +172,26 @@ test.describe('Game Room Drawing Test', () => { 'JSHeapUsedSize', 'JSHeapTotalSize', ]; + const compareMetric = METRIC_FILTER.reduce( + (acc, cur) => { + acc[cur] = [0, 0]; + return acc; + }, + {} as Record, + ); const performanceMetrics = await Promise.all(cdpSessions.map((e) => e.send('Performance.getMetrics'))); - const formattedMetrics = performanceMetrics.map((e) => - e.metrics.reduce( - (acc, cur) => { - if (METRIC_FILTER.includes(cur.name)) acc[cur.name] = cur.value; - return acc; - }, - {} as Record, - ), + performanceMetrics.forEach((e, index) => + e.metrics.forEach((metric) => { + if (METRIC_FILTER.includes(metric.name)) { + compareMetric[metric.name][clients[index].role === 'GUESSER' ? 1 : 0] += metric.value; + } + }), ); - console.log(JSON.stringify(formattedMetrics, null, 4)); + Object.values(compareMetric).forEach((e) => { + e[0] /= drawers.length; //DRAWER 3명 + e[1] /= clients.length - drawers.length; //GUESSER 2명 + }); + console.log(JSON.stringify(compareMetric, null, 4)); // 테스트 종료 await Promise.all(clients.map((e) => e.context.close())); From 0ba2906726c4fb28884eed96c8a1a23de972e8dc Mon Sep 17 00:00:00 2001 From: ijun17 Date: Mon, 20 Jan 2025 17:27:27 +0900 Subject: [PATCH 43/47] =?UTF-8?q?fix:=20=ED=94=8C=EB=A0=88=EC=9D=B4?= =?UTF-8?q?=EB=9D=BC=EC=9D=B4=ED=8A=B8=20=ED=85=8C=EC=8A=A4=ED=8A=B8=20?= =?UTF-8?q?=EC=B4=88=EA=B8=B0=20=EC=83=89=EC=83=81=20=EC=84=A0=ED=83=9D=20?= =?UTF-8?q?=EA=B0=95=EC=A0=9C=20=ED=81=B4=EB=A6=AD=EC=9C=BC=EB=A1=9C=20?= =?UTF-8?q?=EB=B3=80=EA=B2=BD?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- core/crdt/test/drawing-utils.ts | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/core/crdt/test/drawing-utils.ts b/core/crdt/test/drawing-utils.ts index cdc5beb8..1c28f949 100644 --- a/core/crdt/test/drawing-utils.ts +++ b/core/crdt/test/drawing-utils.ts @@ -335,17 +335,17 @@ export const drawingPatterns: Record = { async function selectRandomColor(page: Page): Promise { const colors = ['검정', '분홍', '노랑', '하늘', '회색']; const randomColor = colors[Math.floor(seedRandom(page) * colors.length)]; - await page.getByLabel(`${randomColor} 색상 선택`).click(); + await page.getByLabel(`${randomColor} 색상 선택`).click({ force: true }); } async function setRandomLineWidth(page: Page): Promise { - await page.getByLabel('펜 모드').click(); + await page.getByLabel('펜 모드').click({ force: true }); const lineWidth = Math.floor(seedRandom(page) * 9) * 2 + 4; // 4-20 사이의 짝수 값 await page.getByLabel('선 굵기 조절').fill(lineWidth.toString()); } async function toggleFillMode(page: Page): Promise { - await page.getByLabel('채우기 모드').click(); + await page.getByLabel('채우기 모드').click({ force: true }); } async function performUndoRedo(page: Page): Promise { From 4a1a1871e84077c41e1d65d0dce66d917b65c2eb Mon Sep 17 00:00:00 2001 From: ijun17 Date: Mon, 20 Jan 2025 17:27:58 +0900 Subject: [PATCH 44/47] =?UTF-8?q?style:=20=ED=85=8C=EC=8A=A4=ED=8A=B8=20ur?= =?UTF-8?q?l=20=EB=B3=80=EC=88=98=EB=A1=9C=20=EB=B6=84=EB=A6=AC=EB=A6=AC?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- core/crdt/test/random-drawing-performance.spec.ts | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/core/crdt/test/random-drawing-performance.spec.ts b/core/crdt/test/random-drawing-performance.spec.ts index c5893d5d..50e27893 100644 --- a/core/crdt/test/random-drawing-performance.spec.ts +++ b/core/crdt/test/random-drawing-performance.spec.ts @@ -114,7 +114,8 @@ test.describe('Game Room Drawing Test', () => { test('Drawing performance test with multiple browsers', async () => { try { // 셋업 및 모달 처리 - clients = await setupTestRoom('http://localhost:5173'); + const TEST_URL = 'http://localhost:5173'; + clients = await setupTestRoom(TEST_URL); const drawers = clients.filter((client) => ['PAINTER', 'DEVIL'].includes(client.role || '')); // 모달 닫힌 후 시작 시간 기록 From 99636e70278822db2abbec87b6073caf4954e953 Mon Sep 17 00:00:00 2001 From: ijun17 Date: Mon, 20 Jan 2025 17:42:01 +0900 Subject: [PATCH 45/47] =?UTF-8?q?style:=20=EC=95=88=EC=93=B0=EC=9D=B4?= =?UTF-8?q?=EB=8A=94=20=EC=A3=BC=EC=84=9D=EC=B2=98=EB=A6=AC=EB=90=9C=20?= =?UTF-8?q?=ED=95=A8=EC=88=98=20=EC=A0=9C=EA=B1=B0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- core/crdt/test/drawing-utils.ts | 130 ----------------- core/crdt/test/test-utils.ts | 245 -------------------------------- 2 files changed, 375 deletions(-) diff --git a/core/crdt/test/drawing-utils.ts b/core/crdt/test/drawing-utils.ts index 1c28f949..4feb879a 100644 --- a/core/crdt/test/drawing-utils.ts +++ b/core/crdt/test/drawing-utils.ts @@ -44,136 +44,6 @@ async function drawWithFillMode(page: Page, point: { x: number; y: number }): Pr } export const drawingPatterns: Record = { - // 동일한 사각형 그리기 (fillRect) - // identicalByRect: async (page: Page) => { - // await page.evaluate(() => { - // const canvas = document.querySelector('canvas'); - // if (!canvas) throw new Error('Canvas not found'); - // const ctx = canvas.getContext('2d'); - // if (!ctx) throw new Error('Cannot get 2d context'); - - // ctx.beginPath(); - // ctx.fillStyle = 'black'; - // ctx.fillRect(0, 0, canvas.width, canvas.height); - // ctx.stroke(); - // }); - // }, - - // 전체 50% 채워지는 줄무늬 그리기 (fillRect) - // differentByRect: async (page: Page, clientIndex?: number) => { - // if (!clientIndex) return; - - // await page.evaluate( - // ({ index }) => { - // const canvas = document.querySelector('canvas'); - // if (!canvas) throw new Error('Canvas not found'); - // const ctx = canvas.getContext('2d'); - // if (!ctx) throw new Error('Cannot get 2d context'); - - // const isEvenClient = index % 2 === 0; - // ctx.beginPath(); - - // if (isEvenClient) { - // for (let y = 0; y < canvas.height; y += 20) { - // ctx.fillStyle = 'black'; - // ctx.fillRect(0, y, canvas.width, 10); - // } - // } else { - // for (let x = 0; x < canvas.width; x += 20) { - // ctx.fillStyle = 'black'; - // ctx.fillRect(x, 0, 10, canvas.height); - // } - // } - - // ctx.stroke(); - // }, - // { index: clientIndex }, - // ); - // }, - - // 동일한 사각형 그리기 (Mouse events) - // identicalByMouse: async (page: Page) => { - // const canvas = await page.locator('canvas'); - // const box = await canvas.boundingBox(); - // if (!box) throw new Error('Canvas not found'); - - // await page.dispatchEvent('canvas', 'mousedown', { - // bubbles: true, - // cancelable: true, - // clientX: box.x, - // clientY: box.y, - // }); - - // for (let x = 0; x < box.width; x += 10) { - // for (let y = 0; y < box.height; y += 10) { - // await page.dispatchEvent('canvas', 'mousemove', { - // bubbles: true, - // cancelable: true, - // clientX: box.x + x, - // clientY: box.y + y, - // }); - // await page.waitForTimeout(10); - // } - // } - - // await page.dispatchEvent('canvas', 'mouseup', { - // bubbles: true, - // cancelable: true, - // clientX: box.x + box.width, - // clientY: box.y + box.height, - // }); - // }, - - // 전체 50% 채워지는 줄무늬 그리기 (Mouse events) - // differentByMouse: async (page: Page, clientIndex?: number) => { - // if (!clientIndex) return; - // const canvas = await page.locator('canvas'); - // const box = await canvas.boundingBox(); - // if (!box) throw new Error('Canvas not found'); - - // const isEvenClient = clientIndex % 2 === 0; - - // await page.dispatchEvent('canvas', 'mousedown', { - // bubbles: true, - // cancelable: true, - // clientX: box.x, - // clientY: box.y, - // }); - - // if (isEvenClient) { - // for (let y = 0; y < box.height; y += 20) { - // for (let x = 0; x < box.width; x += 10) { - // await page.dispatchEvent('canvas', 'mousemove', { - // bubbles: true, - // cancelable: true, - // clientX: box.x + x, - // clientY: box.y + y, - // }); - // await page.waitForTimeout(10); - // } - // } - // } else { - // for (let x = 0; x < box.width; x += 20) { - // for (let y = 0; y < box.height; y += 10) { - // await page.dispatchEvent('canvas', 'mousemove', { - // bubbles: true, - // cancelable: true, - // clientX: box.x + x, - // clientY: box.y + y, - // }); - // await page.waitForTimeout(10); - // } - // } - // } - - // await page.dispatchEvent('canvas', 'mouseup', { - // bubbles: true, - // cancelable: true, - // clientX: box.x + (isEvenClient ? box.width : 20), - // clientY: box.y + (isEvenClient ? 20 : box.height), - // }); - // }, - // 랜덤 드로잉 (Mouse events) randomByMouse: async (page: Page) => { const CANVAS_SELECTOR = 'canvas + canvas'; diff --git a/core/crdt/test/test-utils.ts b/core/crdt/test/test-utils.ts index 8e5261b4..baf7c0ec 100644 --- a/core/crdt/test/test-utils.ts +++ b/core/crdt/test/test-utils.ts @@ -1,71 +1,6 @@ import { Page } from '@playwright/test'; import { PNG } from 'pngjs'; -// export const compareCanvasPixels = async (basePage: Page, targetPage: Page): Promise => { -// const getCanvasData = async (page: Page) => { -// return await page.evaluate(() => { -// const canvas = document.querySelector('canvas'); -// if (!canvas) throw new Error('Canvas not found'); -// const ctx = canvas.getContext('2d'); -// if (!ctx) throw new Error('Could not get 2d context'); - -// const imageData = ctx.getImageData(0, 0, canvas.width, canvas.height); -// return { -// data: Array.from(imageData.data), -// width: canvas.width, -// height: canvas.height, -// }; -// }); -// }; - -// // Promise.all을 사용하여 두 페이지의 데이터를 병렬로 가져오기 -// const [pixels1, pixels2] = await Promise.all([getCanvasData(basePage), getCanvasData(targetPage)]); - -// // 다른 픽셀 수 계산 -// const differentPixels = pixels1.data.reduce((count, pixel, index) => { -// if (pixel !== pixels2.data[index]) { -// return count + 1; -// } -// return count; -// }, 0); - -// const totalPixels = pixels1.data.length; -// const diffRatio = differentPixels / totalPixels; - -// return diffRatio; -// }; - -// 1. 픽셀 데이터 직접 비교 -// export const compareByPixelData = async (basePage: Page, targetPage: Page): Promise => { -// const getPixelData = async (page: Page) => { -// return page.evaluate(() => { -// const canvas = document.querySelector('canvas'); -// if (!canvas) throw new Error('Canvas not found'); -// const ctx = canvas.getContext('2d'); -// if (!ctx) throw new Error('Cannot get 2d context'); - -// const imageData = ctx.getImageData(0, 0, canvas.width, canvas.height); -// return Array.from(imageData.data); -// }); -// }; - -// const [basePixels, targetPixels] = await Promise.all([getPixelData(basePage), getPixelData(targetPage)]); - -// let differentPixels = 0; -// for (let i = 0; i < basePixels.length; i += 4) { -// if ( -// basePixels[i] !== targetPixels[i] || -// basePixels[i + 1] !== targetPixels[i + 1] || -// basePixels[i + 2] !== targetPixels[i + 2] || -// basePixels[i + 3] !== targetPixels[i + 3] -// ) { -// differentPixels++; -// } -// } - -// return differentPixels / (basePixels.length / 4); -// }; - // 2. PNG 해시값 비교 export const compareByPng = async (basePage: Page, targetPage: Page): Promise => { // 임시 대기 시간 추가 @@ -101,185 +36,5 @@ export const compareByPng = async (basePage: Page, targetPage: Page): Promise => { -// try { -// // 기준 스냅샷 생성 및 저장 -// await expect(basePage.locator('canvas')).toHaveScreenshot('base-canvas.png'); -// // 비교 스냅샷 생성 및 저장 -// await expect(targetPage.locator('canvas')).toHaveScreenshot('target-canvas.png'); - -// // Playwright의 스냅샷 비교는 자동으로 처리되므로, catch로 결과 분석 -// return 0; // 두 스크린샷이 일치 -// // eslint-disable-next-line @typescript-eslint/no-explicit-any -// } catch (error: any) { -// // 에러 메시지에서 차이점 퍼센트 추출 -// const match = error.message.match(/(\d+\.\d+)% of all pixels/); -// return match ? parseFloat(match[1]) : 100; // 차이점 반환 -// } -// }; - // 공통 비교 함수 타입 정의 export type CompareFunction = (basePage: Page, targetPage: Page) => Promise; - -// 공통 테스트 실행 함수 -// export const runCanvasComparisonTest = async ( -// clients: DrawingClient[], -// drawingFunction: (page: Page, clientIndex?: number) => Promise, -// compareFunction: CompareFunction, -// acceptanceCriteria: number, -// testMode: 'identical' | 'different', -// ): Promise => { -// console.log(`Starting sequential test with ${clients.length} clients`); - -// const diffResults = []; -// for (let i = 1; i < clients.length; i++) { -// // 1. 첫 번째 클라이언트 그리기 및 결과 저장 -// await clearCanvas(clients[0].page); -// await drawingFunction(clients[0].page, 0); -// const baseCanvas = await clients[0].page.evaluate(() => { -// const canvas = document.querySelector('canvas'); -// if (!canvas) throw new Error('Canvas not found'); -// // 현재 캔버스 상태를 새로운 캔버스에 복사 -// const tempCanvas = document.createElement('canvas'); -// tempCanvas.width = canvas.width; -// tempCanvas.height = canvas.height; -// const ctx = tempCanvas.getContext('2d'); -// if (!ctx) throw new Error('Canvas context not found'); -// ctx.drawImage(canvas, 0, 0); -// return tempCanvas.toDataURL(); // base64로 변환 -// }); - -// // 2. 두 번째 클라이언트 그리기 및 결과 저장 -// await clearCanvas(clients[i].page); -// await drawingFunction(clients[i].page, i); -// const compareCanvas = await clients[i].page.evaluate(() => { -// const canvas = document.querySelector('canvas'); -// if (!canvas) throw new Error('Canvas not found'); -// const tempCanvas = document.createElement('canvas'); -// tempCanvas.width = canvas.width; -// tempCanvas.height = canvas.height; -// const ctx = tempCanvas.getContext('2d'); -// if (!ctx) throw new Error('Canvas context not found'); -// ctx.drawImage(canvas, 0, 0); -// return tempCanvas.toDataURL(); -// }); - -// // 3. 저장된 결과를 임시 캔버스에 그리고 비교 -// await clients[0].page.evaluate((base64Image: string) => { -// const canvas = document.querySelector('canvas'); -// if (!canvas) throw new Error('Canvas not found'); -// const ctx = canvas.getContext('2d'); -// if (!ctx) throw new Error('Canvas context not found'); -// const img = new Image(); -// return new Promise((resolve) => { -// img.onload = () => { -// ctx.clearRect(0, 0, canvas.width, canvas.height); -// ctx.drawImage(img, 0, 0); -// resolve(Promise); -// }; -// img.src = base64Image; -// }); -// }, baseCanvas); - -// await clients[i].page.evaluate((base64Image: string) => { -// const canvas = document.querySelector('canvas'); -// if (!canvas) throw new Error('Canvas not found'); -// const ctx = canvas.getContext('2d'); -// if (!ctx) throw new Error('Canvas context not found'); -// const img = new Image(); -// return new Promise((resolve) => { -// img.onload = () => { -// ctx.clearRect(0, 0, canvas.width, canvas.height); -// ctx.drawImage(img, 0, 0); -// resolve(Promise); -// }; -// img.src = base64Image; -// }); -// }, compareCanvas); - -// const diffRatio = await compareFunction(clients[0].page, clients[i].page); -// diffResults.push({ -// clientIndex: i, -// diffRatio, -// }); -// } - -// // 결과 검증 -// diffResults.forEach(({ clientIndex, diffRatio }) => { -// console.log(`Client ${clientIndex} comparison:`, { -// diffRatio, -// acceptanceCriteria, -// passes: testMode === 'identical' ? diffRatio <= acceptanceCriteria : diffRatio >= acceptanceCriteria, -// }); - -// if (testMode === 'identical') { -// expect(diffRatio).toBeLessThanOrEqual(acceptanceCriteria); -// } else { -// expect(diffRatio).toBeGreaterThanOrEqual(acceptanceCriteria); -// } -// }); -// }; - -// 테스트 설정 초기화 함수 -// export async function setupSameURL(clientCount: number, browser: Browser): Promise { -// const clients: DrawingClient[] = []; - -// for (let i = 0; i < clientCount; i++) { -// const context = await browser.newContext({ -// viewport: TEST_CONFIG.viewport, -// }); -// const page = await context.newPage(); - -// // 테스트 페이지로 접속 -// await page.goto(TEST_CONFIG.url); - -// // 캔버스 요소가 로드될 때까지 대기 -// await page.waitForSelector('canvas'); - -// clients.push({ context, page }); -// console.log(`Client ${i} connected to test page`); -// } - -// return clients; -// } - -// export async function setupDifferentURL(clientCount: number, browser: Browser): Promise { -// const clients: DrawingClient[] = []; - -// for (let i = 0; i < clientCount; i++) { -// const context = await browser.newContext({ -// viewport: TEST_CONFIG.viewport, -// }); -// const page = await context.newPage(); - -// // 각 클라이언트마다 다른 URL로 접속 -// const uniqueUrl = `${TEST_CONFIG.url}/canvas?id=test-${i}`; -// // 또는 완전히 다른 경로도 가능 -// // const uniqueUrl = `${TEST_CONFIG.baseUrl}/canvas-${i}`; - -// await page.goto(uniqueUrl); -// await page.waitForSelector('canvas'); - -// clients.push({ context, page }); -// console.log(`Client ${i} connected to independent canvas at ${uniqueUrl}`); -// } - -// return clients; -// } - -// 리소스 정리 함수 -// export async function cleanupClients(clients: DrawingClient[]) { -// for (const client of clients) { -// try { -// if (client.page && !client.page.isClosed()) { -// await client.page.close().catch(() => {}); -// } -// if (client.context) { -// await client.context.close().catch(() => {}); -// } -// } catch (error) { -// console.error('Error closing client:', error); -// } -// } -// } From c8ffb7f81cd7bf7652b8e464140169da4900d6d8 Mon Sep 17 00:00:00 2001 From: ijun17 Date: Mon, 20 Jan 2025 17:42:32 +0900 Subject: [PATCH 46/47] =?UTF-8?q?test:=20Date.now=20=EB=8C=80=EC=8B=A0=20p?= =?UTF-8?q?erformance.now=EB=A1=9C=20=EB=8D=94=20=EC=A0=95=EB=B0=80?= =?UTF-8?q?=ED=95=98=EA=B2=8C=20=EC=B8=A1=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- core/crdt/test/random-drawing-performance.spec.ts | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/core/crdt/test/random-drawing-performance.spec.ts b/core/crdt/test/random-drawing-performance.spec.ts index 50e27893..979de627 100644 --- a/core/crdt/test/random-drawing-performance.spec.ts +++ b/core/crdt/test/random-drawing-performance.spec.ts @@ -119,12 +119,12 @@ test.describe('Game Room Drawing Test', () => { const drawers = clients.filter((client) => ['PAINTER', 'DEVIL'].includes(client.role || '')); // 모달 닫힌 후 시작 시간 기록 - const testStartTime = Date.now(); + const testStartTime = performance.now(); // 1단계: 처음 5초 대기 const waitEndTime = testStartTime + 1000; console.log('Waiting 5 seconds before drawing...'); - while (Date.now() < waitEndTime) { + while (performance.now() < waitEndTime) { await new Promise((resolve) => setTimeout(resolve, 100)); } @@ -137,12 +137,12 @@ test.describe('Game Room Drawing Test', () => { // 2단계: 30초 동안 드로잉 const drawingTime = 20000; - const drawingStartTime = Date.now(); + const drawingStartTime = performance.now(); console.log('Starting 30 seconds drawing phase...'); const DRAW_COUNT = 20; let curDrawCount = 0; while (curDrawCount < DRAW_COUNT) { - if (Date.now() - drawingStartTime < drawingTime * (curDrawCount / DRAW_COUNT)) continue; + if (performance.now() - drawingStartTime < drawingTime * (curDrawCount / DRAW_COUNT)) continue; console.log(curDrawCount++); await Promise.all( drawers.map(async (drawer) => { From 4ff1ad345dba9d5a8de5f4e52339cb3e92b06627 Mon Sep 17 00:00:00 2001 From: ijun17 Date: Tue, 21 Jan 2025 13:50:00 +0900 Subject: [PATCH 47/47] =?UTF-8?q?feat:=20=EA=B5=AC=EA=B8=80=20=EC=95=A0?= =?UTF-8?q?=EB=84=90=EB=A6=AC=ED=8B=B1=EC=8A=A4=20=EC=8A=A4=ED=81=AC?= =?UTF-8?q?=EB=A6=BD=ED=8A=B8=20=EC=82=BD=EC=9E=85=EC=9E=85?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- client/index.html | 205 +++++++++++++++++++++++++--------------------- 1 file changed, 113 insertions(+), 92 deletions(-) diff --git a/client/index.html b/client/index.html index a64e8420..2342cf41 100644 --- a/client/index.html +++ b/client/index.html @@ -1,105 +1,126 @@ - - - - - - - - - - + + + + + + + + + + + - - + + + + - - - - + + + + + - - - - - + + + + + + + - - - - - - - + + + + + + - - - - - - + + + + + + + + + + + + - - - - - - - - - - - - + - + 방해꾼은 못말려 : 그림꾼들의 역습 - 방해꾼은 못말려 : 그림꾼들의 역습 - + + + - + gtag('config', 'G-L747FWX3VY'); + + - \ No newline at end of file + +
+ + + +