Cause : le listener 'game-state' de GameScene se déclenche à CHAQUE action de N'IMPORTE QUEL joueur dans la salle (jouer une carte, acheter un objet ou un bâtiment, piocher/jouer un événement) — pas seulement au vrai changement de tour. À chaque déclenchement, dès que phase==='selecting', _deactivateBtn() était appelé INCONDITIONNELLEMENT, même si le joueur avait déjà sélectionné une carte de sa main (this.selectedCard non-null) sans encore l'avoir confirmée. Conséquence concrète : sélectionner une carte, PUIS effectuer n'importe quelle autre action (typiquement piocher une carte Événement) désactivait visuellement le bouton "CONFIRMER MA CARTE" (fond terne, texte grisé) alors que la carte restait réellement sélectionnée en mémoire ET visuellement surélevée sur le plateau (CardObject._selected reste true, jamais désélectionnée par ce chemin). Un premier clic sur la carte la DÉSÉLECTIONNAIT alors (CardObject._onDown bascule select/deselect selon _selected, déjà à true), obligeant à cliquer une seconde fois pour la vraiment resélectionner — exactement le symptôme rapporté. Fix : ne réinitialiser visuellement le bouton (_deactivateBtn()) que si aucune carte n'est actuellement sélectionnée (), respectant l'état réel de sélection au lieu de le réinitialiser à chaque game-state reçu. Nouveau test E2E de régression (e2e/tests/card-selection-persistence.spec.js) reproduisant précisément le scénario : sélectionner une carte, piocher un événement, vérifier que selectedCard ET le pulse-tween du bouton restent actifs, puis confirmer directement sans recliquer sur la carte. Confirmé discriminant : échoue de façon reproductible (2/2 tentatives) quand le fix est retiré isolément, passe systématiquement avec le fix en place. Ajout des helpers e2e/tests/helpers.js: selectHandCard() et confirmSelectedCard() (extraits de playCard(), qui reste inchangée en façade — les 5 usages existants de playCard() dans la suite E2E restent compatibles sans modification). 54 tests unitaires client + 166 tests unitaires serveur + suite game-flow.spec.js (4 tests, non liés à ce fix) toujours au vert.
105 lines
4.3 KiB
JavaScript
105 lines
4.3 KiB
JavaScript
import { test, expect } from '@playwright/test'
|
|
import {
|
|
installPhaserDebugHook, createRoom, joinRoom, startGame,
|
|
selectHandCard, confirmSelectedCard, playFullTurn, getMyGauges, clickEventDrawButton
|
|
} from './helpers.js'
|
|
|
|
/**
|
|
* Régression : sélectionner une carte de la main, puis effectuer une autre
|
|
* action réseau AVANT de confirmer (ex: piocher un événement) ne devait pas
|
|
* désynchroniser le bouton "CONFIRMER MA CARTE" de l'état réel de sélection.
|
|
*
|
|
* Cause du bug : le listener 'game-state' (déclenché à CHAQUE action de
|
|
* N'IMPORTE QUEL joueur, pas seulement au changement de tour) appelait
|
|
* inconditionnellement _deactivateBtn() dès que phase==='selecting', même si
|
|
* this.selectedCard restait valide en mémoire. La carte restait visuellement
|
|
* sélectionnée (surélevée, CardObject._selected=true) mais le bouton de
|
|
* confirmation apparaissait grisé — un premier clic sur la carte la
|
|
* DÉSÉLECTIONNAIT (déjà _selected=true côté CardObject) au lieu de la
|
|
* confirmer, obligeant à cliquer une seconde fois pour la re-sélectionner.
|
|
*/
|
|
test.describe('Persistance de la sélection de carte personnage', () => {
|
|
test('sélectionner une carte puis piocher un événement ne désactive pas le bouton de confirmation', async ({ browser }) => {
|
|
test.setTimeout(90_000)
|
|
|
|
const ctx1 = await browser.newContext()
|
|
const ctx2 = await browser.newContext()
|
|
const p1 = await ctx1.newPage()
|
|
const p2 = await ctx2.newPage()
|
|
const errors = []
|
|
p1.on('pageerror', e => errors.push(e.message))
|
|
await installPhaserDebugHook(p1)
|
|
await installPhaserDebugHook(p2)
|
|
|
|
const roomId = await createRoom(p1, 'Alice')
|
|
await joinRoom(p2, 'Bob', roomId)
|
|
await startGame(p1)
|
|
await p1.waitForTimeout(2000)
|
|
|
|
// Accumuler assez de points sur les 4 jauges pour pouvoir piocher un événement.
|
|
let gauges = await getMyGauges(p1)
|
|
let attempts = 0
|
|
while (!['food', 'war', 'health', 'diplo'].every(a => gauges[a] >= 1) && attempts < 12) {
|
|
await playFullTurn([p1, p2], [
|
|
['food', 'war', 'health', 'diplo'],
|
|
{ attr: ['food', 'war', 'health', 'diplo'], mode: 'min' }
|
|
])
|
|
gauges = await getMyGauges(p1)
|
|
attempts++
|
|
}
|
|
expect(['food', 'war', 'health', 'diplo'].every(a => gauges[a] >= 1)).toBe(true)
|
|
|
|
// Sélectionner une carte personnage SANS la confirmer.
|
|
const selected = await selectHandCard(p1)
|
|
expect(selected).toBe(true)
|
|
|
|
const selectedCardIdBefore = await p1.evaluate(() => {
|
|
const game = window.__ALL_GAMES__[window.__ALL_GAMES__.length - 1]
|
|
const scene = game.scene.getScene('GameScene')
|
|
return scene.selectedCard?.cardData?.id ?? null
|
|
})
|
|
expect(selectedCardIdBefore).not.toBeNull()
|
|
|
|
// Déclencher une action réseau qui provoque un game-state broadcast à
|
|
// tous les joueurs (piocher un événement), SANS toucher à la carte.
|
|
const drew = await clickEventDrawButton(p1)
|
|
expect(drew).toBe(true)
|
|
await p1.waitForTimeout(300)
|
|
|
|
// La carte doit rester sélectionnée EN MÉMOIRE...
|
|
const selectedCardIdAfter = await p1.evaluate(() => {
|
|
const game = window.__ALL_GAMES__[window.__ALL_GAMES__.length - 1]
|
|
const scene = game.scene.getScene('GameScene')
|
|
return scene.selectedCard?.cardData?.id ?? null
|
|
})
|
|
expect(selectedCardIdAfter).toBe(selectedCardIdBefore)
|
|
|
|
// ...ET le bouton de confirmation doit rester visuellement ACTIF (pas de
|
|
// pulse-tween arrêté par un _deactivateBtn() intempestif).
|
|
const btnActive = await p1.evaluate(() => {
|
|
const game = window.__ALL_GAMES__[window.__ALL_GAMES__.length - 1]
|
|
const scene = game.scene.getScene('GameScene')
|
|
return !!scene._pulseTween && scene._pulseTween.isPlaying()
|
|
})
|
|
expect(btnActive).toBe(true)
|
|
|
|
// Confirmer directement (SANS recliquer sur la carte au préalable) doit
|
|
// fonctionner du premier coup.
|
|
const confirmed = await confirmSelectedCard(p1)
|
|
expect(confirmed).toBe(true)
|
|
await p1.waitForTimeout(300)
|
|
|
|
const phaseAfterConfirm = await p1.evaluate(() => {
|
|
const game = window.__ALL_GAMES__[window.__ALL_GAMES__.length - 1]
|
|
const scene = game.scene.getScene('GameScene')
|
|
return scene.phaseText.text
|
|
})
|
|
expect(phaseAfterConfirm).toMatch(/attente/)
|
|
|
|
expect(errors).toEqual([])
|
|
|
|
await ctx1.close()
|
|
await ctx2.close()
|
|
})
|
|
})
|