Files
regnum/e2e/tests/item-shop.spec.js
T
Kevin L'Hereec 29cc728f09 feat: distinction visuelle "achat en attente" avant résolution du tour
Après avoir cliqué sur ACHETER, aucune indication ne montrait que l'achat
était pris en compte avant que la mini-carte définitive n'apparaisse à
côté de la main (à la résolution du tour, plusieurs secondes plus tard).
Le serveur exposait déjà pendingBuyItemId/pendingBuyBuildingId via
getStateFor(), mais rien côté client ne les utilisait.

- ItemBoardToken.js : nouveau paramètre `pending`. Quand actif, le jeton
  compact affiche un halo doré pulsant (tween alpha 1<->0.25 en boucle) et
  un texte "En attente..." sous l'icône, tant que l'achat n'est pas résolu.
  Le bouton ACHETER de la carte détaillée passe alors sur un libellé
  distinct "EN ATTENTE..." (au lieu de "INSUFFISANT", qui suggérerait à
  tort un problème de jauge) et devient non-cliquable, cohérent avec le
  refus déjà appliqué côté serveur pour une seconde tentative le même tour.
  Le tween est proprement arrêté dans destroy() (les jetons sont détruits/
  recréés à chaque game-state).
- GameScene.js : _buildItemTokens()/_buildBuildingTokens() déterminent
  `pending` à partir de state.pendingBuyItemId/pendingBuyBuildingId (déjà
  reçus du serveur, jamais consommés auparavant) et le passent au jeton
  correspondant, désactivant aussi son affordabilité pendant l'attente.
  shopChanged/buildingShopChanged comparent désormais aussi ces deux champs
  pour déclencher la reconstruction dès le clic sur ACHETER (avant, seul
  un changement de myItems/availableItems/jauges provoquait un rebuild).
- Nouveau test E2E (item-shop.spec.js) : après achat, vérifie que le jeton
  correspondant a pending=true, affordable=false et un tween actif chez
  l'acheteur, que l'ADVERSAIRE ne voit PAS cet état "pending" (propre au
  joueur), et qu'après résolution du tour le jeton disparaît normalement
  de la boutique (comportement de stock unique déjà existant, inchangé).

54 tests unitaires client + 166 tests unitaires serveur + suite
item-shop.spec.js (4 tests) au vert.
2026-08-25 15:29:17 +02:00

184 lines
7.0 KiB
JavaScript

import { test, expect } from '@playwright/test'
import {
installPhaserDebugHook, createRoom, joinRoom, startGame,
playCard, playFullTurn, getShopItemIds, getMyGauges, buyShopItem, hoverShopItem
} from './helpers.js'
/**
* Tests d'intégration E2E : boutique d'objets (achat, effets permanents,
* disparition de la boutique une fois acheté, retrait bloqué si jauge
* insuffisante).
*/
test.describe('Boutique d\'objets', () => {
test('la boutique affiche les 4 objets au démarrage de la partie', async ({ browser }) => {
const ctx1 = await browser.newContext()
const ctx2 = await browser.newContext()
const p1 = await ctx1.newPage()
const p2 = await ctx2.newPage()
await installPhaserDebugHook(p1)
await installPhaserDebugHook(p2)
const roomId = await createRoom(p1, 'Alice')
await joinRoom(p2, 'Bob', roomId)
await startGame(p1)
await p1.waitForTimeout(2000)
const ids = await getShopItemIds(p1)
expect(ids.sort()).toEqual(['medkit', 'plow', 'royal_seal', 'trebuchet'].sort())
await ctx1.close()
await ctx2.close()
})
test('le bouton ACHETER n\'est pas interactif tant que la jauge est insuffisante', async ({ browser }) => {
const ctx1 = await browser.newContext()
const ctx2 = await browser.newContext()
const p1 = await ctx1.newPage()
const p2 = await ctx2.newPage()
await installPhaserDebugHook(p1)
await installPhaserDebugHook(p2)
const roomId = await createRoom(p1, 'Alice')
await joinRoom(p2, 'Bob', roomId)
await startGame(p1)
await p1.waitForTimeout(2000)
// Jauge à 0 en tout début de partie : le bouton ACHETER du Trébuchet ne
// doit pas être interactif (hoverShopItem retourne null dans ce cas).
const btnCenter = await hoverShopItem(p1, 'trebuchet')
expect(btnCenter).toBeNull()
await ctx1.close()
await ctx2.close()
})
test('acheter un objet le retire de la boutique pour TOUS les joueurs, et applique son effet permanent', async ({ browser }) => {
const ctx1 = await browser.newContext()
const ctx2 = await browser.newContext()
const p1 = await ctx1.newPage()
const p2 = await ctx2.newPage()
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 guerre pour P1 en jouant sa meilleure carte de guerre
// à chaque tour, pendant que P2 joue explicitement sa carte la plus
// FAIBLE en guerre (mode 'min') — indispensable pour garantir un gain
// non-nul à chaque tour (gain = max(0, ma_valeur - valeur_voisin_gauche)),
// sinon le test devient flaky selon les cartes distribuées aléatoirement.
let gauges = await getMyGauges(p1)
let attempts = 0
while (gauges.war < 3 && attempts < 12) {
await playFullTurn([p1, p2], ['war', { attr: 'war', mode: 'min' }])
gauges = await getMyGauges(p1)
attempts++
}
expect(gauges.war).toBeGreaterThanOrEqual(3)
const idsBefore = await getShopItemIds(p1)
expect(idsBefore).toContain('trebuchet')
const bought = await buyShopItem(p1, 'trebuchet')
expect(bought).toBe(true)
// Jouer le tour pour déclencher la résolution de l'achat en attente
await playFullTurn([p1, p2], ['war', 'health'])
const idsAfterP1 = await getShopItemIds(p1)
const idsAfterP2 = await getShopItemIds(p2)
expect(idsAfterP1).not.toContain('trebuchet')
expect(idsAfterP2).not.toContain('trebuchet')
expect(idsAfterP1.length).toBe(3)
// Vérifier que l'objet apparaît dans "mes objets" (posés à côté de ma main)
const myItemsCount = await p1.evaluate(() => {
const game = window.__ALL_GAMES__[window.__ALL_GAMES__.length - 1]
return game.scene.getScene('GameScene').myItemTokens.length
})
expect(myItemsCount).toBe(1)
// Et sous le slot de P1 depuis la perspective de P2 (adversaire)
const opponentItemsCount = await p2.evaluate(() => {
const game = window.__ALL_GAMES__[window.__ALL_GAMES__.length - 1]
return game.scene.getScene('GameScene').opponentItemTokens.length
})
expect(opponentItemsCount).toBe(1)
await ctx1.close()
await ctx2.close()
})
test('un achat en attente affiche une pulsation dorée sur le jeton, avant que le tour ne soit résolu', async ({ browser }) => {
const ctx1 = await browser.newContext()
const ctx2 = await browser.newContext()
const p1 = await ctx1.newPage()
const p2 = await ctx2.newPage()
await installPhaserDebugHook(p1)
await installPhaserDebugHook(p2)
const roomId = await createRoom(p1, 'Alice')
await joinRoom(p2, 'Bob', roomId)
await startGame(p1)
await p1.waitForTimeout(2000)
let gauges = await getMyGauges(p1)
let attempts = 0
while (gauges.war < 3 && attempts < 12) {
await playFullTurn([p1, p2], ['war', { attr: 'war', mode: 'min' }])
gauges = await getMyGauges(p1)
attempts++
}
expect(gauges.war).toBeGreaterThanOrEqual(3)
const bought = await buyShopItem(p1, 'trebuchet')
expect(bought).toBe(true)
// AVANT la résolution du tour : le jeton reste dans la boutique (encore
// dans availableItems côté serveur), mais doit maintenant afficher l'état
// "pending" (pulsation dorée + bouton grisé "EN ATTENTE...").
const pendingState = await p1.evaluate(() => {
const game = window.__ALL_GAMES__[window.__ALL_GAMES__.length - 1]
const scene = game.scene.getScene('GameScene')
const token = scene.itemTokens.find(t => t.itemData.id === 'trebuchet')
return {
exists: !!token,
pending: token?.pending ?? null,
affordable: token?.affordable ?? null,
hasPulseTween: !!token?._pulseTween
}
})
expect(pendingState.exists).toBe(true)
expect(pendingState.pending).toBe(true)
expect(pendingState.affordable).toBe(false)
expect(pendingState.hasPulseTween).toBe(true)
// Une seconde salle voit-elle aussi cet état ? Non — "pending" est
// propre à CE joueur (pendingBuyItemId scoped par socketId côté serveur),
// Bob ne doit pas voir le jeton de Trébuchet en état "pending".
const bobView = await p2.evaluate(() => {
const game = window.__ALL_GAMES__[window.__ALL_GAMES__.length - 1]
const scene = game.scene.getScene('GameScene')
const token = scene.itemTokens.find(t => t.itemData.id === 'trebuchet')
return token?.pending ?? null
})
expect(bobView).toBe(false)
// APRÈS résolution du tour : le jeton disparaît complètement de la
// boutique (objet acheté, stock épuisé) — plus d'état "pending" à vérifier.
await playFullTurn([p1, p2], ['war', 'health'])
const afterResolve = await p1.evaluate(() => {
const game = window.__ALL_GAMES__[window.__ALL_GAMES__.length - 1]
const scene = game.scene.getScene('GameScene')
return scene.itemTokens.find(t => t.itemData.id === 'trebuchet')
})
expect(afterResolve).toBeUndefined()
await ctx1.close()
await ctx2.close()
})
})