355 lines
10 KiB
JavaScript
355 lines
10 KiB
JavaScript
import assert from 'node:assert/strict'
|
|
import { io } from 'socket.io-client'
|
|
|
|
const SOCKET_SERVER_PORT = process.env.SOCKET_SERVER_PORT || '31873'
|
|
const SOCKET_SERVER_URL = `http://127.0.0.1:${SOCKET_SERVER_PORT}`
|
|
|
|
/**
|
|
* 테스트에 사용할 유효한 50장 덱 URL을 만든다.
|
|
*
|
|
* @returns {string} 덱 등록용 결과 페이지 URL
|
|
*/
|
|
function createValidDeckUrl() {
|
|
const cards = Array.from({ length: 13 }, (_, cardIndex) => ({
|
|
imgSrc: `/ua01/TEST-${String(cardIndex + 1).padStart(3, '0')}.png`,
|
|
Number: `TEST-${String(cardIndex + 1).padStart(3, '0')}`,
|
|
count: cardIndex === 12 ? 2 : 4,
|
|
color: 'Blue',
|
|
trigger: null,
|
|
}))
|
|
const deckUrl = new URL('http://uniare.sori.studio/result.html')
|
|
|
|
deckUrl.searchParams.set('series', 'ua01')
|
|
deckUrl.searchParams.set('seriesName', 'Code Geass')
|
|
deckUrl.searchParams.set('cards', JSON.stringify(cards))
|
|
|
|
return deckUrl.toString()
|
|
}
|
|
|
|
/**
|
|
* 연결 완료 또는 오류를 Promise로 기다린다.
|
|
*
|
|
* @param {import('socket.io-client').Socket} socket 확인할 클라이언트 소켓
|
|
* @returns {Promise<void>} 연결 완료 Promise
|
|
*/
|
|
function waitForConnection(socket) {
|
|
return new Promise((resolve, reject) => {
|
|
socket.once('connect', resolve)
|
|
socket.once('connect_error', reject)
|
|
})
|
|
}
|
|
|
|
/**
|
|
* Socket.IO 승인 응답이 있는 이벤트를 전송한다.
|
|
*
|
|
* @param {import('socket.io-client').Socket} socket 이벤트를 전송할 소켓
|
|
* @param {string} eventName 이벤트 이름
|
|
* @param {unknown[]} eventArguments 이벤트 인자
|
|
* @returns {Promise<object>} 서버 승인 응답
|
|
*/
|
|
function emitWithAcknowledgement(socket, eventName, ...eventArguments) {
|
|
return new Promise((resolve, reject) => {
|
|
socket.timeout(3000).emit(
|
|
eventName,
|
|
...eventArguments,
|
|
(timeoutError, response) => {
|
|
if (timeoutError) {
|
|
reject(timeoutError)
|
|
return
|
|
}
|
|
|
|
resolve(response)
|
|
},
|
|
)
|
|
})
|
|
}
|
|
|
|
/**
|
|
* 게임 시작 이벤트를 한 번 기다린다.
|
|
*
|
|
* @param {import('socket.io-client').Socket} socket 이벤트를 기다릴 소켓
|
|
* @returns {Promise<object>} 게임 시작 정보
|
|
*/
|
|
function waitForGameStart(socket) {
|
|
return new Promise((resolve) => {
|
|
socket.once('game:started', resolve)
|
|
})
|
|
}
|
|
|
|
/**
|
|
* 지정한 Socket.IO 이벤트를 한 번 기다린다.
|
|
*
|
|
* @param {import('socket.io-client').Socket} socket 이벤트를 기다릴 소켓
|
|
* @param {string} eventName 기다릴 이벤트 이름
|
|
* @returns {Promise<object>} 수신한 이벤트 데이터
|
|
*/
|
|
function waitForSocketEvent(socket, eventName) {
|
|
return new Promise((resolve) => {
|
|
socket.once(eventName, resolve)
|
|
})
|
|
}
|
|
|
|
/**
|
|
* 두 클라이언트의 방 생성부터 게임 시작까지 검증한다.
|
|
*
|
|
* @returns {Promise<void>} 검증 완료 Promise
|
|
*/
|
|
async function runIntegrationTest() {
|
|
const firstPlayerSocket = io(SOCKET_SERVER_URL, {
|
|
path: '/socket.io',
|
|
transports: ['websocket', 'polling'],
|
|
})
|
|
const secondPlayerSocket = io(SOCKET_SERVER_URL, {
|
|
path: '/socket.io',
|
|
transports: ['websocket', 'polling'],
|
|
})
|
|
let resumedSecondPlayerSocket = null
|
|
|
|
try {
|
|
await Promise.all([
|
|
waitForConnection(firstPlayerSocket),
|
|
waitForConnection(secondPlayerSocket),
|
|
])
|
|
|
|
const createRoomResponse = await emitWithAcknowledgement(
|
|
firstPlayerSocket,
|
|
'room:create',
|
|
)
|
|
assert.equal(createRoomResponse.ok, true)
|
|
assert.equal(createRoomResponse.room.players.length, 1)
|
|
|
|
const joinRoomResponse = await emitWithAcknowledgement(
|
|
secondPlayerSocket,
|
|
'room:join',
|
|
createRoomResponse.room.roomCode,
|
|
)
|
|
assert.equal(joinRoomResponse.ok, true)
|
|
assert.equal(joinRoomResponse.room.players.length, 2)
|
|
|
|
const validDeckUrl = createValidDeckUrl()
|
|
const firstDeckResponse = await emitWithAcknowledgement(
|
|
firstPlayerSocket,
|
|
'player:register-deck',
|
|
validDeckUrl,
|
|
)
|
|
const secondDeckResponse = await emitWithAcknowledgement(
|
|
secondPlayerSocket,
|
|
'player:register-deck',
|
|
validDeckUrl,
|
|
)
|
|
assert.equal(firstDeckResponse.ok, true)
|
|
assert.equal(secondDeckResponse.ok, true)
|
|
|
|
const firstGameStart = waitForGameStart(firstPlayerSocket)
|
|
const secondGameStart = waitForGameStart(secondPlayerSocket)
|
|
|
|
await emitWithAcknowledgement(
|
|
firstPlayerSocket,
|
|
'player:set-ready',
|
|
true,
|
|
)
|
|
await emitWithAcknowledgement(
|
|
secondPlayerSocket,
|
|
'player:set-ready',
|
|
true,
|
|
)
|
|
|
|
const [firstStartPayload, secondStartPayload] = await Promise.all([
|
|
firstGameStart,
|
|
secondGameStart,
|
|
])
|
|
|
|
assert.equal(firstStartPayload.seat, 'player-one')
|
|
assert.equal(secondStartPayload.seat, 'player-two')
|
|
assert.equal(firstStartPayload.roomCode, secondStartPayload.roomCode)
|
|
assert.match(firstStartPayload.gameQuery, /series=ua01/)
|
|
assert.ok(
|
|
[createRoomResponse.playerId, joinRoomResponse.playerId].includes(
|
|
firstStartPayload.gameState.firstPlayerId,
|
|
),
|
|
)
|
|
|
|
const privateDeckInstanceIds = Array.from(
|
|
{ length: 50 },
|
|
(_, cardIndex) => `private-card-${cardIndex + 1}`,
|
|
)
|
|
const shuffledDeckResponse = await emitWithAcknowledgement(
|
|
firstPlayerSocket,
|
|
'game:shuffle-private-deck',
|
|
privateDeckInstanceIds,
|
|
)
|
|
assert.equal(shuffledDeckResponse.ok, true)
|
|
assert.equal(shuffledDeckResponse.instanceIds.length, 50)
|
|
assert.equal(new Set(shuffledDeckResponse.instanceIds).size, 50)
|
|
|
|
const drawnDeckResponse = await emitWithAcknowledgement(
|
|
firstPlayerSocket,
|
|
'game:draw-private-deck',
|
|
2,
|
|
)
|
|
assert.deepEqual(drawnDeckResponse.instanceIds, [
|
|
shuffledDeckResponse.instanceIds[49],
|
|
shuffledDeckResponse.instanceIds[48],
|
|
])
|
|
|
|
const returnedCardInstanceId = drawnDeckResponse.instanceIds[0]
|
|
await emitWithAcknowledgement(
|
|
firstPlayerSocket,
|
|
'game:place-private-deck-card',
|
|
{
|
|
instanceId: returnedCardInstanceId,
|
|
placement: 'top',
|
|
},
|
|
)
|
|
const redrawnDeckResponse = await emitWithAcknowledgement(
|
|
firstPlayerSocket,
|
|
'game:draw-private-deck',
|
|
1,
|
|
)
|
|
assert.equal(redrawnDeckResponse.instanceIds[0], returnedCardInstanceId)
|
|
|
|
await emitWithAcknowledgement(
|
|
firstPlayerSocket,
|
|
'game:confirm-setup',
|
|
)
|
|
const startedTurnStatePromise = waitForSocketEvent(
|
|
firstPlayerSocket,
|
|
'game:state',
|
|
)
|
|
await emitWithAcknowledgement(
|
|
secondPlayerSocket,
|
|
'game:confirm-setup',
|
|
)
|
|
const startedTurnState = await startedTurnStatePromise
|
|
assert.equal(
|
|
startedTurnState.currentTurnPlayerId,
|
|
startedTurnState.firstPlayerId,
|
|
)
|
|
|
|
const opponentStatePromise = waitForSocketEvent(
|
|
secondPlayerSocket,
|
|
'game:opponent-state',
|
|
)
|
|
await emitWithAcknowledgement(
|
|
firstPlayerSocket,
|
|
'game:publish-player-state',
|
|
{
|
|
revision: 2,
|
|
cards: [
|
|
{
|
|
instanceId: 'published-card-1',
|
|
cardNumber: 'TEST-001',
|
|
imageUrl: '/card-assets/ua01/TEST-001.png',
|
|
zone: 'field',
|
|
position: { x: 0.4, y: 0.7 },
|
|
isFaceUp: true,
|
|
isTapped: false,
|
|
isUntapLocked: false,
|
|
stackOrder: 10,
|
|
},
|
|
{
|
|
instanceId: 'hidden-hand-card',
|
|
cardNumber: 'SECRET-001',
|
|
imageUrl: '/card-assets/ua01/SECRET-001.png',
|
|
zone: 'hand',
|
|
position: { x: 0.5, y: 0.9 },
|
|
isFaceUp: true,
|
|
isTapped: false,
|
|
isUntapLocked: false,
|
|
stackOrder: 11,
|
|
},
|
|
],
|
|
counts: {
|
|
deck: 35,
|
|
hand: 7,
|
|
life: 7,
|
|
},
|
|
},
|
|
)
|
|
const opponentState = await opponentStatePromise
|
|
assert.equal(opponentState.cards.length, 1)
|
|
assert.equal(opponentState.cards[0].instanceId, 'published-card-1')
|
|
assert.equal(opponentState.counts.hand, 7)
|
|
assert.equal(opponentState.revision, 2)
|
|
|
|
const staleStateResponse = await emitWithAcknowledgement(
|
|
firstPlayerSocket,
|
|
'game:publish-player-state',
|
|
{
|
|
revision: 1,
|
|
cards: [],
|
|
counts: {
|
|
deck: 0,
|
|
hand: 0,
|
|
life: 0,
|
|
},
|
|
},
|
|
)
|
|
assert.equal(staleStateResponse.ignored, true)
|
|
assert.equal(staleStateResponse.revision, 2)
|
|
|
|
const currentTurnSocket =
|
|
startedTurnState.currentTurnPlayerId === createRoomResponse.playerId
|
|
? firstPlayerSocket
|
|
: secondPlayerSocket
|
|
const nextTurnStatePromise = waitForSocketEvent(
|
|
firstPlayerSocket,
|
|
'game:state',
|
|
)
|
|
const endTurnResponse = await emitWithAcknowledgement(
|
|
currentTurnSocket,
|
|
'game:end-turn',
|
|
)
|
|
const nextTurnState = await nextTurnStatePromise
|
|
assert.equal(endTurnResponse.ok, true)
|
|
assert.notEqual(
|
|
nextTurnState.currentTurnPlayerId,
|
|
startedTurnState.currentTurnPlayerId,
|
|
)
|
|
|
|
secondPlayerSocket.disconnect()
|
|
resumedSecondPlayerSocket = io(SOCKET_SERVER_URL, {
|
|
path: '/socket.io',
|
|
transports: ['websocket', 'polling'],
|
|
})
|
|
await waitForConnection(resumedSecondPlayerSocket)
|
|
const resumeResponse = await emitWithAcknowledgement(
|
|
resumedSecondPlayerSocket,
|
|
'room:resume',
|
|
{
|
|
roomCode: joinRoomResponse.room.roomCode,
|
|
playerId: joinRoomResponse.playerId,
|
|
resumeToken: joinRoomResponse.resumeToken,
|
|
},
|
|
)
|
|
assert.equal(resumeResponse.ok, true)
|
|
assert.equal(resumeResponse.playerId, joinRoomResponse.playerId)
|
|
assert.equal(resumeResponse.room.hasStarted, true)
|
|
assert.equal(
|
|
resumeResponse.gameStartPayload.roomCode,
|
|
joinRoomResponse.room.roomCode,
|
|
)
|
|
assert.match(resumeResponse.gameStartPayload.gameQuery, /series=ua01/)
|
|
|
|
const roomClosedPromise = waitForSocketEvent(
|
|
firstPlayerSocket,
|
|
'room:closed',
|
|
)
|
|
const leaveRoomResponse = await emitWithAcknowledgement(
|
|
resumedSecondPlayerSocket,
|
|
'room:leave',
|
|
)
|
|
const roomClosedInformation = await roomClosedPromise
|
|
assert.equal(leaveRoomResponse.ok, true)
|
|
assert.match(roomClosedInformation.message, /방이 종료/)
|
|
|
|
console.log('Socket.IO 2인 게임 통합 테스트를 통과했습니다.')
|
|
} finally {
|
|
firstPlayerSocket.disconnect()
|
|
secondPlayerSocket.disconnect()
|
|
resumedSecondPlayerSocket?.disconnect()
|
|
}
|
|
}
|
|
|
|
await runIntegrationTest()
|