- Fix: UIScene n'était jamais lancée (active: false, jamais scene.launch)
donc l'event 'game-over' du serveur n'était jamais affiché — la partie
semblait ne pas avoir de fin. Remplacé par un panneau Vue dans GameView
(vainqueur, classement trié, bouton Quitter via vue-router). UIScene.js
supprimée (code mort).
- Extraction de client/src/utils/hudUtils.js (constantes ATTRS/couleurs et
gaugePercent) depuis GameView.vue pour les rendre testables isolément.
- Tests unitaires client (Vitest) : store Pinia (game.js) et hudUtils
(24 tests, client/test/unit/).
- Tests d'intégration E2E (Playwright, e2e/) avec de vrais clients
Socket.io et rendu Phaser réel dans Chromium :
* game-flow.spec.js : lobby → démarrage → main de 4 cartes → résolution
* item-shop.spec.js : boutique, bouton désactivé si jauge insuffisante,
achat retire l'objet pour tous + effet appliqué + affiché sur la table
* game-over.spec.js : partie complète (12 tours) → panneau de fin
webServer démarre automatiquement client+serveur ; ajout d'une route
GET /health côté Express pour permettre à Playwright de détecter un
serveur déjà démarré.
- README : documentation des 3 niveaux de tests (unitaires serveur,
unitaires client, intégration E2E) et de la nouvelle arborescence.
195 lines
7.3 KiB
JavaScript
195 lines
7.3 KiB
JavaScript
/**
|
|
* Fonctions d'aide partagées par les tests E2E Regnum.
|
|
*
|
|
* Toutes les interactions avec le canvas Phaser passent par un hook de debug
|
|
* injecté via `page.addInitScript` (pas de modification du code source de
|
|
* l'application) qui expose chaque instance `Phaser.Game` créée sur
|
|
* `window.__ALL_GAMES__`. Cela permet d'introspecter l'état réel de la scène
|
|
* (position des cartes/boutons, contenu de la main, etc.) plutôt que de
|
|
* deviner des coordonnées fixes à l'écran — ce qui serait fragile face aux
|
|
* changements de mise en page (voir historique des bugs de resize corrigés).
|
|
*/
|
|
|
|
/** Script à injecter via page.addInitScript avant toute navigation. */
|
|
export function phaserDebugHookInitScript() {
|
|
window.__ALL_GAMES__ = []
|
|
const patchWhenReady = () => {
|
|
if (window.Phaser && window.Phaser.Game && !window.Phaser.Game.__patched__) {
|
|
const OrigGame = window.Phaser.Game
|
|
function PatchedGame(config) {
|
|
const inst = new OrigGame(config)
|
|
window.__ALL_GAMES__.push(inst)
|
|
return inst
|
|
}
|
|
PatchedGame.prototype = OrigGame.prototype
|
|
PatchedGame.__patched__ = true
|
|
window.Phaser.Game = PatchedGame
|
|
} else {
|
|
setTimeout(patchWhenReady, 5)
|
|
}
|
|
}
|
|
patchWhenReady()
|
|
}
|
|
|
|
/** Installe le hook de debug Phaser sur une page (à appeler avant page.goto). */
|
|
export async function installPhaserDebugHook(page) {
|
|
await page.addInitScript(phaserDebugHookInitScript)
|
|
}
|
|
|
|
/** Récupère la scène GameScene active dans le dernier jeu Phaser créé sur la page. */
|
|
export async function getGameScene(page) {
|
|
return page.evaluateHandle(() => {
|
|
const game = window.__ALL_GAMES__[window.__ALL_GAMES__.length - 1]
|
|
return game.scene.getScene('GameScene')
|
|
})
|
|
}
|
|
|
|
/** Crée une salle (P1) et retourne son roomId. */
|
|
export async function createRoom(page, playerName) {
|
|
await page.goto('/')
|
|
await page.fill('input[placeholder="Ton nom de joueur"]', playerName)
|
|
await page.click('button:has-text("Créer une partie")')
|
|
await page.waitForURL('**/lobby/**')
|
|
return page.url().split('/lobby/')[1]
|
|
}
|
|
|
|
/** Rejoint une salle existante. */
|
|
export async function joinRoom(page, playerName, roomId) {
|
|
await page.goto('/')
|
|
await page.fill('input[placeholder="Ton nom de joueur"]', playerName)
|
|
await page.fill('input[placeholder="Code de la salle"]', roomId)
|
|
await page.click('button:has-text("Rejoindre")')
|
|
await page.waitForURL('**/lobby/**')
|
|
}
|
|
|
|
/** Démarre la partie depuis le lobby (doit être appelé par l'hôte, P1). */
|
|
export async function startGame(page) {
|
|
await page.click('button:has-text("Lancer la partie")')
|
|
await page.waitForURL('**/game/**')
|
|
}
|
|
|
|
/**
|
|
* Joue une carte de la main du joueur et confirme. Sélectionne dynamiquement
|
|
* la meilleure carte pour l'attribut donné (ou la première carte si aucun
|
|
* attribut n'est précisé), en lisant l'état réel de la scène — jamais de
|
|
* coordonnées fixes, pour rester robuste face aux changements de layout.
|
|
* Ne fait rien si la main est vide (fin de partie).
|
|
*/
|
|
export async function playCard(page, attr = null) {
|
|
const canvas = page.locator('#phaser-container canvas')
|
|
const box = await canvas.boundingBox()
|
|
|
|
const handLen = await page.evaluate(() => {
|
|
const game = window.__ALL_GAMES__[window.__ALL_GAMES__.length - 1]
|
|
const scene = game?.scene?.getScene('GameScene')
|
|
return scene?.handCards?.length ?? 0
|
|
})
|
|
if (handLen === 0) return false
|
|
|
|
const cardPos = await page.evaluate((attr) => {
|
|
const game = window.__ALL_GAMES__[window.__ALL_GAMES__.length - 1]
|
|
const scene = game.scene.getScene('GameScene')
|
|
let idx = 0
|
|
if (attr) {
|
|
let bestVal = -1
|
|
scene.handCards.forEach((c, i) => {
|
|
if (c.cardData[attr] > bestVal) { bestVal = c.cardData[attr]; idx = i }
|
|
})
|
|
}
|
|
const b = scene.handCards[idx].getBounds()
|
|
return { x: b.centerX, y: b.centerY }
|
|
}, attr)
|
|
|
|
await page.mouse.click(box.x + cardPos.x, box.y + cardPos.y)
|
|
await page.waitForTimeout(200)
|
|
|
|
const btnPos = await page.evaluate(() => {
|
|
const game = window.__ALL_GAMES__[window.__ALL_GAMES__.length - 1]
|
|
const scene = game.scene.getScene('GameScene')
|
|
const b = scene.playBtn.getBounds()
|
|
return { x: b.centerX, y: b.centerY }
|
|
})
|
|
await page.mouse.click(box.x + btnPos.x, box.y + btnPos.y)
|
|
await page.waitForTimeout(200)
|
|
return true
|
|
}
|
|
|
|
/**
|
|
* Joue un tour complet pour une liste de pages (dans l'ordre donné), puis
|
|
* attend la résolution + le début du tour suivant (délai serveur fixe de
|
|
* 3.5s dans index.js + marge de sécurité).
|
|
*/
|
|
export async function playFullTurn(pages, attrsByPage = []) {
|
|
for (let i = 0; i < pages.length; i++) {
|
|
await playCard(pages[i], attrsByPage[i] ?? null)
|
|
}
|
|
await pages[0].waitForTimeout(4200)
|
|
}
|
|
|
|
/**
|
|
* Survole un jeton d'objet de la boutique (colonne à gauche du plateau) par
|
|
* son id, et retourne le centre du bouton ACHETER si l'objet est disponible
|
|
* et achetable. Retourne null si le jeton n'existe pas (déjà acheté) ou si le
|
|
* bouton n'est pas interactif (jauge insuffisante).
|
|
*/
|
|
export async function hoverShopItem(page, itemId) {
|
|
const canvas = page.locator('#phaser-container canvas')
|
|
const box = await canvas.boundingBox()
|
|
|
|
const tokenPos = await page.evaluate((itemId) => {
|
|
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 === itemId)
|
|
return token ? { x: token.x, y: token.y } : null
|
|
}, itemId)
|
|
|
|
if (!tokenPos) return null
|
|
|
|
await page.mouse.move(box.x + tokenPos.x, box.y + tokenPos.y)
|
|
await page.waitForTimeout(300)
|
|
|
|
return page.evaluate((itemId) => {
|
|
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 === itemId)
|
|
if (!token || !token._expanded) return null
|
|
const btnBg = token._expanded.list.find(o => o.type === 'Graphics' && o.input)
|
|
if (!btnBg) return null // bouton non interactif (jauge insuffisante)
|
|
const worldPoint = btnBg.getWorldTransformMatrix().transformPoint(
|
|
btnBg.input.hitArea.centerX, btnBg.input.hitArea.centerY
|
|
)
|
|
return { x: worldPoint.x, y: worldPoint.y }
|
|
}, itemId)
|
|
}
|
|
|
|
/** Clique sur le bouton ACHETER d'un objet actuellement affiché en survol. */
|
|
export async function buyShopItem(page, itemId) {
|
|
const btnCenter = await hoverShopItem(page, itemId)
|
|
if (!btnCenter) return false
|
|
|
|
const canvas = page.locator('#phaser-container canvas')
|
|
const box = await canvas.boundingBox()
|
|
await page.mouse.click(box.x + btnCenter.x, box.y + btnCenter.y)
|
|
await page.waitForTimeout(400)
|
|
return true
|
|
}
|
|
|
|
/** Lit les jauges du joueur local depuis le store Pinia exposé par la scène. */
|
|
export async function getMyGauges(page) {
|
|
return page.evaluate(() => {
|
|
const game = window.__ALL_GAMES__[window.__ALL_GAMES__.length - 1]
|
|
const scene = game.scene.getScene('GameScene')
|
|
const me = scene.store.players.find(p => p.teamIndex === scene.store.myTeamIndex)
|
|
return me?.gauges ?? { food: 0, war: 0, health: 0, diplo: 0 }
|
|
})
|
|
}
|
|
|
|
/** Lit l'état de la boutique (ids d'objets encore affichés dans la colonne). */
|
|
export async function getShopItemIds(page) {
|
|
return page.evaluate(() => {
|
|
const game = window.__ALL_GAMES__[window.__ALL_GAMES__.length - 1]
|
|
const scene = game.scene.getScene('GameScene')
|
|
return scene.itemTokens.map(t => t.itemData.id)
|
|
})
|
|
}
|