Files
regnum/client/test/unit/eventHandLayout.test.js
Kevin L'Hereec 1a91a0f876 fix: cartes Événement illisibles + objets/bâtiments adversaires au-dessus de leur carte
Deux problèmes de lisibilité/placement signalés :

1. Le texte des cartes Événement (nom + description) restait illisible
   malgré le passage précédent de l'échelle 0.55 à 0.8 — le composant dédié
   EventCardObject affichait une grande carte réduite, écrasant
   systématiquement le texte quelle que soit l'échelle choisie.

   Fix : suppression complète d'EventCardObject, remplacé par le même
   pattern que les objets/bâtiments (ItemBoardToken, nouveau kind='event') :
   un petit jeton compact (46x46) toujours visible sur le plateau, avec la
   carte détaillée affichée en TAILLE RÉELLE (police 13-16px, jamais
   réduite) au survol de la souris, et un bouton "JOUER" (au lieu
   d'"ACHETER") pour confirmer. ItemBoardToken généralisé pour supporter ce
   3e kind (pas de badge de coût affiché — déjà payé à la pioche — mais
   badge "!" repris pour les événements à effet négatif type Épidémie).

   Extraction de client/src/game/objects/tokenLayout.js (ICON_SIZE) dans un
   module SANS dépendance à Phaser, pour que eventHandLayout.js (et ses
   tests Vitest) puisse l'importer sans jamais charger Phaser — qui échoue
   à l'import en environnement jsdom (canvas natif requis, absent de jsdom).

2. Les mini-cartes objets/bâtiments déjà achetés par un ADVERSAIRE étaient
   affichées SOUS son slot de jeu, visuellement en dessous/après sa carte
   jouée. Déplacées AU-DESSUS de son slot pour les mettre davantage en
   évidence, comme posées sur sa carte plutôt que cachées dessous.

- 43 tests unitaires client mis à jour (eventHandLayout.test.js migré vers
  ICON_SIZE au lieu de CARD_W/CARD_H/scale, désormais indépendant de Phaser).
- e2e/tests/helpers.js : playEventCard adapté au nouveau flux
  survol->bouton JOUER (au lieu d'un simple clic direct sur la carte) ;
  drawEventUntil corrigé pour ne jamais défausser une carte 'royal_accord'
  sans fournir ses 2 cibles de ciblage séquentiel obligatoire (le jeu sans
  cible bloquait silencieusement la séquence, sans jamais émettre
  'play-event', rendant la boucle de pioche très peu fiable).
- Vérification ciblée via un test E2E existant (pas de nouvelle suite E2E
  lourde, conformément à la demande de privilégier les tests unitaires) :
  le nouveau flux jeton->survol->JOUER->résolution fonctionne bout en bout
  (Récolte exceptionnelle +2 Nourriture confirmé après résolution).
2026-08-24 16:01:04 +02:00

140 lines
6.5 KiB
JavaScript

import { describe, it, expect } from 'vitest'
import {
eventDrawButtonPosition,
eventHandGeometry,
eventCardPosition
} from '../../src/game/objects/eventHandLayout'
import { ICON_SIZE } from '../../src/game/objects/tokenLayout'
// Dimensions de canvas typiques utilisées dans les tests (cohérentes avec
// GameFactory.js : width = innerWidth, height = innerHeight - 60).
const CANVAS_W = 1920
const CANVAS_H = 1080
describe('game/objects/eventHandLayout', () => {
describe('eventDrawButtonPosition', () => {
it('positionne le bouton dans le quart bas-droit du canvas', () => {
const { x, y } = eventDrawButtonPosition(CANVAS_W, CANVAS_H)
expect(x).toBeGreaterThan(CANVAS_W / 2)
expect(y).toBeGreaterThan(CANVAS_H / 2)
})
it('reste entièrement à l\'intérieur du canvas (ne déborde jamais à droite ni en bas)', () => {
const buttonWidth = 190
const buttonHeight = 30
const { x, y } = eventDrawButtonPosition(CANVAS_W, CANVAS_H, buttonWidth)
expect(x + buttonWidth / 2).toBeLessThanOrEqual(CANVAS_W)
expect(x - buttonWidth / 2).toBeGreaterThanOrEqual(0)
expect(y + buttonHeight / 2).toBeLessThanOrEqual(CANVAS_H)
})
it('est positionné strictement au-dessus de la main de jetons événement (pas de chevauchement)', () => {
const buttonHeight = 30
const { y: btnY } = eventDrawButtonPosition(CANVAS_W, CANVAS_H)
const { y: handY } = eventHandGeometry(CANVAS_W, CANVAS_H, 1)
const buttonBottom = btnY + buttonHeight / 2
const handTop = handY - ICON_SIZE / 2
expect(buttonBottom).toBeLessThanOrEqual(handTop)
})
it('reste bien à droite du bouton "CONFIRMER MA CARTE" (centré, ~280px de large) — pas de chevauchement', () => {
const confirmButtonWidth = 280
const confirmButtonRightEdge = CANVAS_W / 2 + confirmButtonWidth / 2
const drawButtonWidth = 190
const { x } = eventDrawButtonPosition(CANVAS_W, CANVAS_H, drawButtonWidth)
const drawButtonLeftEdge = x - drawButtonWidth / 2
// Régression du bug signalé : l'ancien placement (x = width/2 + 210,
// même Y que le bouton confirmer) chevauchait physiquement son bord
// droit de ~25px. Le nouveau placement bas-droite doit être
// verticalement séparé (voir test précédent) ET ne pas non plus se
// chevaucher horizontalement s'ils finissaient à la même hauteur.
expect(drawButtonLeftEdge).toBeGreaterThanOrEqual(confirmButtonRightEdge)
})
it('accepte une largeur de bouton personnalisée', () => {
const narrow = eventDrawButtonPosition(CANVAS_W, CANVAS_H, 100)
const wide = eventDrawButtonPosition(CANVAS_W, CANVAS_H, 300)
// Un bouton plus large doit être centré plus loin du bord droit
expect(narrow.x).toBeGreaterThan(wide.x)
})
})
describe('eventHandGeometry', () => {
it('positionne la main dans la moitié basse du canvas', () => {
const { y } = eventHandGeometry(CANVAS_W, CANVAS_H, 2)
expect(y).toBeGreaterThan(CANVAS_H / 2)
})
it('le dernier jeton est ancré sur le bord droit du canvas (avec la marge)', () => {
const { startX, spacing } = eventHandGeometry(CANVAS_W, CANVAS_H, 2)
const lastCenterX = startX + spacing // index 1 = dernier jeton pour cardCount=2
const lastRightEdge = lastCenterX + ICON_SIZE / 2
expect(lastRightEdge).toBeLessThanOrEqual(CANVAS_W)
expect(lastRightEdge).toBeGreaterThan(CANVAS_W - 40) // proche du bord (marge ~20px)
})
it('avec 1 jeton, il est ancré sur le bord droit (pas de décalage vers la gauche)', () => {
const { startX } = eventHandGeometry(CANVAS_W, CANVAS_H, 1)
const { startX: startXTwo, spacing } = eventHandGeometry(CANVAS_W, CANVAS_H, 2)
// La position du DERNIER jeton doit être identique quel que soit le
// nombre de jetons (ancrage à droite stable) — avec 1 jeton, startX
// doit correspondre à la position du dernier jeton du cas à 2.
expect(startX).toBeCloseTo(startXTwo + spacing, 5)
})
it('gère cardCount=0 sans produire de position invalide (NaN/Infinity)', () => {
const { startX, y, spacing } = eventHandGeometry(CANVAS_W, CANVAS_H, 0)
expect(Number.isFinite(startX)).toBe(true)
expect(Number.isFinite(y)).toBe(true)
expect(Number.isFinite(spacing)).toBe(true)
})
it('l\'espacement entre jetons est toujours positif et couvre au moins la largeur d\'un jeton', () => {
const { spacing } = eventHandGeometry(CANVAS_W, CANVAS_H, 2)
expect(spacing).toBeGreaterThan(ICON_SIZE)
})
it('les 2 jetons ne se chevauchent jamais (distance entre centres >= largeur d\'un jeton)', () => {
const { spacing } = eventHandGeometry(CANVAS_W, CANVAS_H, 2)
expect(spacing).toBeGreaterThanOrEqual(ICON_SIZE)
})
it('reste entièrement dans le canvas sur une petite résolution (1280x720)', () => {
const { startX, y, spacing } = eventHandGeometry(1280, 720, 2)
const firstLeftEdge = startX - ICON_SIZE / 2
const lastRightEdge = startX + spacing + ICON_SIZE / 2
expect(firstLeftEdge).toBeGreaterThanOrEqual(0)
expect(lastRightEdge).toBeLessThanOrEqual(1280)
expect(y + ICON_SIZE / 2).toBeLessThanOrEqual(720)
expect(y - ICON_SIZE / 2).toBeGreaterThanOrEqual(0)
})
})
describe('eventCardPosition', () => {
it('retourne la même position que eventHandGeometry pour un index donné', () => {
const { startX, y, spacing } = eventHandGeometry(CANVAS_W, CANVAS_H, 2)
const pos0 = eventCardPosition(CANVAS_W, CANVAS_H, 2, 0)
const pos1 = eventCardPosition(CANVAS_W, CANVAS_H, 2, 1)
expect(pos0).toEqual({ x: startX, y })
expect(pos1).toEqual({ x: startX + spacing, y })
})
it('les positions de 2 jetons sont distinctes', () => {
const pos0 = eventCardPosition(CANVAS_W, CANVAS_H, 2, 0)
const pos1 = eventCardPosition(CANVAS_W, CANVAS_H, 2, 1)
expect(pos0.x).not.toBe(pos1.x)
})
})
describe('non-régression : séparation avec la légende d\'attributs (bas-gauche)', () => {
it('la zone événement (bas-droite) ne recouvre pas la zone légende (bas-gauche, x proche de 0)', () => {
const { startX } = eventHandGeometry(CANVAS_W, CANVAS_H, 2)
const firstLeftEdge = startX - ICON_SIZE / 2
// La légende occupe le coin bas-gauche (x ancré à 10px du bord gauche,
// largeur de texte ~180px max) — la zone événement doit rester dans
// la moitié droite du canvas pour ne jamais s'en approcher.
expect(firstLeftEdge).toBeGreaterThan(CANVAS_W / 2)
})
})
})