Files
regnum/client/test/unit/tokenLayout.test.js
Kevin L'Hereec 33a3c20dcb fix: la carte détaillée d'un événement débordait sous le canvas au survol
ItemBoardToken._showExpanded() ancrait TOUJOURS la carte détaillée vers le
BAS du jeton compact, quel que soit son emplacement sur le plateau —
adapté aux boutiques d'objets/bâtiments (positionnées en haut du canvas),
mais pas à la main de cartes Événement (positionnée en BAS du canvas,
voir eventHandLayout.js) : la carte (jusqu'à 266px de haut avec le bouton
JOUER) débordait alors largement sous le bas de l'écran, la rendant
invisible et son bouton totalement injoignable au clic.

- Ajout du paramètre vAnchor ('bottom' par défaut, 'top' pour les jetons
  proches du bas de l'écran) sur ItemBoardToken, symétrique au hAnchor déjà
  existant pour l'axe horizontal. La main d'événements passe désormais
  vAnchor='top' pour que la carte s'étende vers le HAUT au lieu du bas.
- Extraction du calcul de décalage (expandedCardOffset) et des constantes
  de dimensionnement (EXPANDED_W/H, BUY_BTN_H) dans tokenLayout.js (module
  pur, sans dépendance Phaser) pour pouvoir le couvrir par de vrais tests
  unitaires Vitest.
- 11 nouveaux tests unitaires (tokenLayout.test.js) : orientation dx/dy
  selon hAnchor/vAnchor, alignement exact du bord du jeton avec le bord de
  la carte, et surtout la régression elle-même — un test vérifie que
  vAnchor='top' maintient la carte dans les limites du canvas pour un
  jeton en bas d'écran (1080p et 720p), et qu'un test symétrique confirme
  que l'ancien comportement implicite (vAnchor='bottom') débordait bien
  sous le canvas dans ce même scénario.
- 54 tests unitaires client au vert au total (43 précédents + 11 nouveaux),
  vérification privilégiée par rapport à une nouvelle suite E2E (lente vu
  la pioche aléatoire à stabiliser dans ce projet).
2026-08-24 16:12:16 +02:00

123 lines
5.4 KiB
JavaScript

import { describe, it, expect } from 'vitest'
import {
ICON_SIZE,
EXPANDED_W,
EXPANDED_H,
BUY_BTN_H,
EXPANDED_H_WITH_BTN,
expandedCardOffset
} from '../../src/game/objects/tokenLayout'
describe('game/objects/tokenLayout', () => {
describe('constantes', () => {
it('EXPANDED_H_WITH_BTN est plus grand que EXPANDED_H (place pour le bouton)', () => {
expect(EXPANDED_H_WITH_BTN).toBeGreaterThan(EXPANDED_H)
expect(EXPANDED_H_WITH_BTN).toBe(EXPANDED_H + BUY_BTN_H + 10)
})
})
describe('expandedCardOffset', () => {
const cardH = EXPANDED_H_WITH_BTN
it('hAnchor="left" (défaut) : la carte s\'étend vers la DROITE (dx positif)', () => {
const { dx } = expandedCardOffset(cardH, 1, 'left', 'bottom')
expect(dx).toBeGreaterThan(0)
})
it('hAnchor="right" : la carte s\'étend vers la GAUCHE (dx négatif)', () => {
const { dx } = expandedCardOffset(cardH, 1, 'right', 'bottom')
expect(dx).toBeLessThan(0)
})
it('vAnchor="bottom" (défaut) : la carte s\'étend vers le BAS (dy positif)', () => {
const { dy } = expandedCardOffset(cardH, 1, 'left', 'bottom')
expect(dy).toBeGreaterThan(0)
})
it('vAnchor="top" : la carte s\'étend vers le HAUT (dy négatif) — régression corrigée', () => {
// Bug corrigé : la main de cartes Événement (positionnée en bas du
// canvas) utilisait implicitement vAnchor="bottom" (comportement par
// défaut, pensé pour les jetons en haut du canvas comme la boutique
// d'objets), faisant déborder la carte détaillée sous le bas du
// canvas — invisible et injoignable au clic (bouton JOUER inatteignable).
const { dy } = expandedCardOffset(cardH, 1, 'left', 'top')
expect(dy).toBeLessThan(0)
})
it('dx et dy ont la même magnitude quel que soit le signe de l\'ancrage (symétrie)', () => {
const left = expandedCardOffset(cardH, 1, 'left', 'bottom')
const right = expandedCardOffset(cardH, 1, 'right', 'bottom')
expect(Math.abs(left.dx)).toBeCloseTo(Math.abs(right.dx), 10)
const bottom = expandedCardOffset(cardH, 1, 'left', 'bottom')
const top = expandedCardOffset(cardH, 1, 'left', 'top')
expect(Math.abs(bottom.dy)).toBeCloseTo(Math.abs(top.dy), 10)
})
it('un displayScale réduit rapproche le décalage de 0 (jeton plus petit, moins de compensation nécessaire)', () => {
const fullScale = expandedCardOffset(cardH, 1, 'left', 'bottom')
const halfScale = expandedCardOffset(cardH, 0.5, 'left', 'bottom')
expect(halfScale.dx).toBeGreaterThan(fullScale.dx)
expect(halfScale.dy).toBeGreaterThan(fullScale.dy)
})
it('avec displayScale=1, le coin du jeton (ICON_SIZE/2) s\'aligne exactement sur le coin de la carte', () => {
// Vérifie la propriété fondamentale de ce calcul : le bord du jeton
// compact doit coïncider avec le bord correspondant de la carte
// détaillée, sans espace ni chevauchement.
const { dx, dy } = expandedCardOffset(cardH, 1, 'left', 'bottom')
const iconHalf = ICON_SIZE / 2
// hAnchor='left' : le bord gauche du jeton (-iconHalf, relatif au
// centre du jeton) coïncide avec le bord gauche de la carte
// (dx - EXPANDED_W/2, puisque le container carte est positionné à (dx, dy)).
expect(-iconHalf).toBeCloseTo(dx - EXPANDED_W / 2, 10)
// vAnchor='bottom' : le bord HAUT du jeton (-iconHalf) coïncide avec
// le bord HAUT de la carte (dy - cardH/2) — la carte s'étend vers le
// BAS à partir du haut du jeton.
expect(-iconHalf).toBeCloseTo(dy - cardH / 2, 10)
})
})
describe('non-régression : carte de la main d\'événements reste dans le canvas (vAnchor="top")', () => {
it('pour un jeton positionné en bas du canvas, la carte détaillée (vAnchor="top") ne déborde jamais sous le canvas', () => {
const canvasH = 1080
const marginBottom = 60
const tokenY = canvasH - marginBottom - ICON_SIZE / 2 // même formule que eventHandGeometry
const { dy } = expandedCardOffset(EXPANDED_H_WITH_BTN, 1, 'right', 'top')
const cardCenterY = tokenY + dy
const cardBottom = cardCenterY + EXPANDED_H_WITH_BTN / 2
const cardTop = cardCenterY - EXPANDED_H_WITH_BTN / 2
expect(cardBottom).toBeLessThanOrEqual(canvasH)
expect(cardTop).toBeGreaterThanOrEqual(0)
})
it('avec l\'ancien comportement implicite (vAnchor="bottom"), la carte aurait débordé — confirme la nécessité du fix', () => {
const canvasH = 1080
const marginBottom = 60
const tokenY = canvasH - marginBottom - ICON_SIZE / 2
const { dy } = expandedCardOffset(EXPANDED_H_WITH_BTN, 1, 'right', 'bottom')
const cardCenterY = tokenY + dy
const cardBottom = cardCenterY + EXPANDED_H_WITH_BTN / 2
expect(cardBottom).toBeGreaterThan(canvasH) // déborde bien, confirmant le bug d'origine
})
it('sur une petite résolution (720p), la carte (vAnchor="top") reste aussi dans les limites', () => {
const canvasH = 720
const marginBottom = 60
const tokenY = canvasH - marginBottom - ICON_SIZE / 2
const { dy } = expandedCardOffset(EXPANDED_H_WITH_BTN, 1, 'right', 'top')
const cardCenterY = tokenY + dy
const cardBottom = cardCenterY + EXPANDED_H_WITH_BTN / 2
const cardTop = cardCenterY - EXPANDED_H_WITH_BTN / 2
expect(cardBottom).toBeLessThanOrEqual(canvasH)
expect(cardTop).toBeGreaterThanOrEqual(0)
})
})
})