Trois bugs fonctionnels réels découverts et corrigés en stabilisant les tests E2E des cartes Événement : 1. client/src/stores/game.js — le store Pinia n'exposait JAMAIS les champs liés aux bâtiments (myBuildings, pendingBuyBuildingId, availableBuildings) ni aux événements (myEventHand, eventDeckSize, hasPendingEventDraw, pendingEventPlayId) : updateState() ne les lisait pas depuis le 'game-state' serveur. Conséquence concrète : GameScene._epidemicNeedsTarget() lisait this.store.myBuildings (toujours vide) pour décider si la variante Église d'Épidémie s'applique — le ciblage de la variante Trousse ne fonctionnait donc jamais correctement dès qu'un joueur possédait aussi le Château. Corrigé + 4 nouveaux tests unitaires Vitest. 2. client/src/game/scenes/GameScene.js — GameScene._onEventCardSelected envoyait encore les clés historiques gainTargetSocketId/ loseTargetSocketId/targetSocketId au serveur, alors que Room.js avait été migré vers gainTargetTeamIndex/loseTargetTeamIndex/targetTeamIndex (socketId n'est jamais exposé au client). Conséquence : le ciblage d'Accord royal et de la variante Trousse d'Épidémie était intégralement cassé en jeu réel — les clés ne correspondant à rien côté serveur, aucune cible n'était jamais appliquée, silencieusement. 3. e2e/tests/helpers.js — drawEventUntil abandonnait immédiatement dès que la main d'événements atteignait 2 cartes sans contenir l'id recherché, rendant les tests E2E d'événements extrêmement flaky (avec seulement 6 types d'événements et une main de 2, l'échec dès les 2 premières pioches est fréquent, pas rare). Corrigé : joue désormais une carte pour libérer un slot et continue à piocher jusqu'à maxDraws (20). Stabilisation supplémentaire : - Toutes les fonctions d'introspection Phaser dans helpers.js sécurisées (optional chaining) contre une scène pas encore initialisée après un changement de tour/navigation sous charge (plusieurs pages Playwright simultanées). - Timeouts des tests events.spec.js augmentés (150-240s) pour laisser le temps aux boucles d'accumulation de jauge + pioche aléatoire de converger. - playwright.config.js : retries=1 même en local, pour absorber la variance résiduelle des tests basés sur un tirage aléatoire, tout en gardant une détection fiable des vraies régressions (qui échoueraient de façon reproductible sur les deux tentatives). - events.spec.js : test 'Accord royal' fournit désormais les 2 cibles requises par le ciblage séquentiel (gain + perte) au lieu d'une seule, ce qui laissait la séquence de ciblage bloquée sans jamais émettre 'play-event'. Résultat : 166 tests unitaires serveur + 28 tests unitaires client toujours au vert ; suite events.spec.js (9 tests E2E) passe intégralement (certains via retry probabiliste documenté, comportement attendu vu la pioche aléatoire dans le deck partagé).
237 lines
8.6 KiB
JavaScript
237 lines
8.6 KiB
JavaScript
import { describe, it, expect, beforeEach } from 'vitest'
|
|
import { setActivePinia, createPinia } from 'pinia'
|
|
import { useGameStore } from '../../src/stores/game'
|
|
|
|
describe('stores/game (Pinia)', () => {
|
|
beforeEach(() => {
|
|
setActivePinia(createPinia())
|
|
})
|
|
|
|
it('a un état initial vide/par défaut', () => {
|
|
const store = useGameStore()
|
|
expect(store.playerName).toBe('')
|
|
expect(store.players).toEqual([])
|
|
expect(store.currentTurn).toBe(0)
|
|
expect(store.maxTurns).toBe(12)
|
|
expect(store.phase).toBe('idle')
|
|
expect(store.hand).toEqual([])
|
|
expect(store.myTeamIndex).toBe(0)
|
|
expect(store.selectedCard).toBeNull()
|
|
expect(store.myItems).toEqual([])
|
|
expect(store.pendingBuyItemId).toBeNull()
|
|
expect(store.availableItems).toEqual([])
|
|
expect(store.firstPlayerName).toBeNull()
|
|
expect(store.myBuildings).toEqual([])
|
|
expect(store.pendingBuyBuildingId).toBeNull()
|
|
expect(store.availableBuildings).toEqual([])
|
|
expect(store.myEventHand).toEqual([])
|
|
expect(store.eventDeckSize).toBe(0)
|
|
expect(store.hasPendingEventDraw).toBe(false)
|
|
expect(store.pendingEventPlayId).toBeNull()
|
|
expect(store.gameOverResults).toBeNull()
|
|
})
|
|
|
|
it('setPlayer met à jour playerName', () => {
|
|
const store = useGameStore()
|
|
store.setPlayer('Alice')
|
|
expect(store.playerName).toBe('Alice')
|
|
})
|
|
|
|
describe('updateState', () => {
|
|
it('met à jour tous les champs de base depuis un game-state serveur', () => {
|
|
const store = useGameStore()
|
|
store.updateState({
|
|
players: [{ name: 'Alice', score: 5 }],
|
|
currentTurn: 3,
|
|
maxTurns: 12,
|
|
phase: 'selecting',
|
|
hand: [{ id: 'queen' }],
|
|
myTeamIndex: 1
|
|
})
|
|
|
|
expect(store.players).toEqual([{ name: 'Alice', score: 5 }])
|
|
expect(store.currentTurn).toBe(3)
|
|
expect(store.maxTurns).toBe(12)
|
|
expect(store.phase).toBe('selecting')
|
|
expect(store.hand).toEqual([{ id: 'queen' }])
|
|
expect(store.myTeamIndex).toBe(1)
|
|
})
|
|
|
|
it('retombe sur les valeurs par défaut si les champs sont absents (sauf hand/items qui restent inchangés)', () => {
|
|
const store = useGameStore()
|
|
store.updateState({})
|
|
|
|
expect(store.players).toEqual([])
|
|
expect(store.currentTurn).toBe(0)
|
|
expect(store.maxTurns).toBe(12)
|
|
expect(store.phase).toBe('idle')
|
|
expect(store.myTeamIndex).toBe(0)
|
|
})
|
|
|
|
it('ne touche PAS à hand si state.hand est undefined (évite d\'effacer la main en cours)', () => {
|
|
const store = useGameStore()
|
|
store.updateState({ hand: [{ id: 'prince' }] })
|
|
store.updateState({ phase: 'waiting' }) // pas de champ hand cette fois
|
|
|
|
expect(store.hand).toEqual([{ id: 'prince' }])
|
|
})
|
|
|
|
it('met à jour les champs liés aux objets uniquement s\'ils sont fournis', () => {
|
|
const store = useGameStore()
|
|
store.updateState({
|
|
myItems: ['trebuchet'],
|
|
pendingBuyItemId: 'plow',
|
|
availableItems: ['medkit', 'royal_seal'],
|
|
firstPlayerName: 'Bob'
|
|
})
|
|
|
|
expect(store.myItems).toEqual(['trebuchet'])
|
|
expect(store.pendingBuyItemId).toBe('plow')
|
|
expect(store.availableItems).toEqual(['medkit', 'royal_seal'])
|
|
expect(store.firstPlayerName).toBe('Bob')
|
|
|
|
// Un second appel sans ces champs ne doit pas les réinitialiser
|
|
store.updateState({ phase: 'resolve' })
|
|
expect(store.myItems).toEqual(['trebuchet'])
|
|
expect(store.pendingBuyItemId).toBe('plow')
|
|
})
|
|
|
|
it('accepte explicitement pendingBuyItemId=null (achat annulé/résolu) sans le confondre avec "absent"', () => {
|
|
const store = useGameStore()
|
|
store.updateState({ pendingBuyItemId: 'plow' })
|
|
store.updateState({ pendingBuyItemId: null })
|
|
expect(store.pendingBuyItemId).toBeNull()
|
|
})
|
|
|
|
it('met à jour les champs liés aux bâtiments uniquement s\'ils sont fournis', () => {
|
|
const store = useGameStore()
|
|
store.updateState({
|
|
myBuildings: ['church'],
|
|
pendingBuyBuildingId: 'castle',
|
|
availableBuildings: ['castle']
|
|
})
|
|
|
|
expect(store.myBuildings).toEqual(['church'])
|
|
expect(store.pendingBuyBuildingId).toBe('castle')
|
|
expect(store.availableBuildings).toEqual(['castle'])
|
|
|
|
// Un second appel sans ces champs ne doit pas les réinitialiser
|
|
store.updateState({ phase: 'resolve' })
|
|
expect(store.myBuildings).toEqual(['church'])
|
|
expect(store.pendingBuyBuildingId).toBe('castle')
|
|
})
|
|
|
|
it('accepte explicitement pendingBuyBuildingId=null sans le confondre avec "absent"', () => {
|
|
const store = useGameStore()
|
|
store.updateState({ pendingBuyBuildingId: 'castle' })
|
|
store.updateState({ pendingBuyBuildingId: null })
|
|
expect(store.pendingBuyBuildingId).toBeNull()
|
|
})
|
|
|
|
it('met à jour les champs liés aux cartes Événement uniquement s\'ils sont fournis', () => {
|
|
const store = useGameStore()
|
|
store.updateState({
|
|
myEventHand: ['bountiful_harvest', 'epidemic'],
|
|
eventDeckSize: 15,
|
|
hasPendingEventDraw: true,
|
|
pendingEventPlayId: 'epidemic'
|
|
})
|
|
|
|
expect(store.myEventHand).toEqual(['bountiful_harvest', 'epidemic'])
|
|
expect(store.eventDeckSize).toBe(15)
|
|
expect(store.hasPendingEventDraw).toBe(true)
|
|
expect(store.pendingEventPlayId).toBe('epidemic')
|
|
|
|
// Un second appel sans ces champs ne doit pas les réinitialiser
|
|
store.updateState({ phase: 'resolve' })
|
|
expect(store.myEventHand).toEqual(['bountiful_harvest', 'epidemic'])
|
|
expect(store.eventDeckSize).toBe(15)
|
|
})
|
|
|
|
it('accepte explicitement pendingEventPlayId=null et hasPendingEventDraw=false sans les confondre avec "absent"', () => {
|
|
const store = useGameStore()
|
|
store.updateState({ pendingEventPlayId: 'epidemic', hasPendingEventDraw: true })
|
|
store.updateState({ pendingEventPlayId: null, hasPendingEventDraw: false })
|
|
expect(store.pendingEventPlayId).toBeNull()
|
|
expect(store.hasPendingEventDraw).toBe(false)
|
|
})
|
|
})
|
|
|
|
describe('selectCard / clearSelection', () => {
|
|
it('selectCard définit la carte sélectionnée', () => {
|
|
const store = useGameStore()
|
|
const card = { id: 'knight', war: 5 }
|
|
store.selectCard(card)
|
|
expect(store.selectedCard).toEqual(card)
|
|
})
|
|
|
|
it('clearSelection remet selectedCard à null', () => {
|
|
const store = useGameStore()
|
|
store.selectCard({ id: 'knight' })
|
|
store.clearSelection()
|
|
expect(store.selectedCard).toBeNull()
|
|
})
|
|
})
|
|
|
|
describe('setGameOver', () => {
|
|
it('définit gameOverResults avec les résultats fournis', () => {
|
|
const store = useGameStore()
|
|
const results = [{ name: 'Alice', score: 42 }, { name: 'Bob', score: 30 }]
|
|
store.setGameOver(results)
|
|
expect(store.gameOverResults).toEqual(results)
|
|
})
|
|
})
|
|
|
|
describe('reset', () => {
|
|
it('remet tout l\'état à ses valeurs par défaut', () => {
|
|
const store = useGameStore()
|
|
store.setPlayer('Alice')
|
|
store.updateState({
|
|
players: [{ name: 'Alice' }],
|
|
currentTurn: 5,
|
|
phase: 'resolve',
|
|
hand: [{ id: 'queen' }],
|
|
myTeamIndex: 2,
|
|
myItems: ['trebuchet'],
|
|
pendingBuyItemId: 'plow',
|
|
availableItems: ['medkit'],
|
|
firstPlayerName: 'Bob',
|
|
myBuildings: ['church'],
|
|
pendingBuyBuildingId: 'castle',
|
|
availableBuildings: ['castle'],
|
|
myEventHand: ['epidemic'],
|
|
eventDeckSize: 10,
|
|
hasPendingEventDraw: true,
|
|
pendingEventPlayId: 'epidemic'
|
|
})
|
|
store.selectCard({ id: 'queen' })
|
|
store.setGameOver([{ name: 'Alice', score: 10 }])
|
|
|
|
store.reset()
|
|
|
|
expect(store.players).toEqual([])
|
|
expect(store.currentTurn).toBe(0)
|
|
expect(store.maxTurns).toBe(12)
|
|
expect(store.phase).toBe('idle')
|
|
expect(store.hand).toEqual([])
|
|
expect(store.myTeamIndex).toBe(0)
|
|
expect(store.selectedCard).toBeNull()
|
|
expect(store.myItems).toEqual([])
|
|
expect(store.pendingBuyItemId).toBeNull()
|
|
expect(store.availableItems).toEqual([])
|
|
expect(store.firstPlayerName).toBeNull()
|
|
expect(store.myBuildings).toEqual([])
|
|
expect(store.pendingBuyBuildingId).toBeNull()
|
|
expect(store.availableBuildings).toEqual([])
|
|
expect(store.myEventHand).toEqual([])
|
|
expect(store.eventDeckSize).toBe(0)
|
|
expect(store.hasPendingEventDraw).toBe(false)
|
|
expect(store.pendingEventPlayId).toBeNull()
|
|
expect(store.gameOverResults).toBeNull()
|
|
// playerName n'est PAS réinitialisé par reset() (comportement existant :
|
|
// le nom du joueur est conservé pour rejouer une partie sans le re-saisir)
|
|
expect(store.playerName).toBe('Alice')
|
|
})
|
|
})
|
|
})
|