import { test, expect } from '@playwright/test' import { installPhaserDebugHook, createRoom, joinRoom, startGame, playFullTurn, getShopBuildingIds, getMyGauges, buyShopBuilding, hoverShopBuilding } from './helpers.js' /** * Tests d'intégration E2E : boutique de bâtiments (Église, Château Royal). * * Couvre notamment la régression détectée manuellement : la carte détaillée * du bâtiment (affichée au survol) s'étendait par défaut vers la DROITE * depuis le jeton, comme pour la boutique d'objets à gauche du plateau — * mais la boutique de bâtiments est sur le bord DROIT du canvas, donc la * carte débordait hors du canvas et son bouton ACHETER devenait injoignable * au clic (coordonnées calculées hors des limites du canvas). Corrigé via le * paramètre `hAnchor='right'` sur ItemBoardToken pour cette boutique. */ test.describe('Boutique de bâtiments', () => { test('la boutique affiche les 2 bâtiments (Église, Château) 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 getShopBuildingIds(p1) expect(ids.sort()).toEqual(['castle', 'church'].sort()) await ctx1.close() await ctx2.close() }) test('les jetons de bâtiment sont positionnés sur le bord DROIT du canvas', 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 canvasWidth = await p1.evaluate(() => { const game = window.__ALL_GAMES__[window.__ALL_GAMES__.length - 1] return game.canvas.width }) const tokenX = await p1.evaluate(() => { const game = window.__ALL_GAMES__[window.__ALL_GAMES__.length - 1] const scene = game.scene.getScene('GameScene') return scene.buildingTokens[0].x }) // Doit être proche du bord droit (dans le dernier quart du canvas) expect(tokenX).toBeGreaterThan(canvasWidth * 0.75) await ctx1.close() await ctx2.close() }) test('la carte détaillée d\'un bâtiment reste entièrement visible dans le canvas au survol', 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 canvas = p1.locator('#phaser-container canvas') const box = await canvas.boundingBox() const tokenPos = await p1.evaluate(() => { const game = window.__ALL_GAMES__[window.__ALL_GAMES__.length - 1] const scene = game.scene.getScene('GameScene') const t = scene.buildingTokens.find(t => t.itemData.id === 'church') return { x: t.x, y: t.y } }) // Le survol (_scheduleShow, HOVER_SHOW_DELAY=80ms) puis le hide-timer // (_scheduleHide, HOVER_HIDE_DELAY=100ms) sont tous deux de courts // setTimeout côté navigateur ; sous charge (exécution juste après un // autre test dans la même suite), le timing réel peut légèrement // dépasser une seule attente fixe. On répète le mouvement de souris // jusqu'à ce que la carte soit confirmée ouverte, plutôt que de se fier // à un unique délai figé. let expandedBounds = null for (let attempt = 0; attempt < 5 && !expandedBounds; attempt++) { await p1.mouse.move(box.x + tokenPos.x, box.y + tokenPos.y + (attempt % 2)) // micro-mouvement pour re-déclencher pointerover si besoin await p1.waitForTimeout(300) expandedBounds = await p1.evaluate(() => { const game = window.__ALL_GAMES__[window.__ALL_GAMES__.length - 1] const scene = game.scene.getScene('GameScene') const t = scene.buildingTokens.find(t => t.itemData.id === 'church') if (!t._expanded) return null const b = t._expanded.getBounds() return { x: b.x, y: b.y, right: b.right, bottom: b.bottom } }) } expect(expandedBounds).not.toBeNull() expect(expandedBounds.x).toBeGreaterThanOrEqual(0) expect(expandedBounds.y).toBeGreaterThanOrEqual(0) expect(expandedBounds.right).toBeLessThanOrEqual(box.width) expect(expandedBounds.bottom).toBeLessThanOrEqual(box.height) await ctx1.close() await ctx2.close() }) test('la carte détaillée affiche le coût réparti sur les 2 jauges et la description', 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 canvas = p1.locator('#phaser-container canvas') const box = await canvas.boundingBox() const tokenPos = await p1.evaluate(() => { const game = window.__ALL_GAMES__[window.__ALL_GAMES__.length - 1] const scene = game.scene.getScene('GameScene') const t = scene.buildingTokens.find(t => t.itemData.id === 'church') return { x: t.x, y: t.y } }) await p1.mouse.move(box.x + tokenPos.x, box.y + tokenPos.y) await p1.waitForTimeout(300) const texts = await p1.evaluate(() => { const game = window.__ALL_GAMES__[window.__ALL_GAMES__.length - 1] const scene = game.scene.getScene('GameScene') const t = scene.buildingTokens.find(t => t.itemData.id === 'church') return t._expanded.list.filter(o => o.type === 'Text').map(o => o.text) }) expect(texts).toContain('Église') expect(texts.some(t => /3 Santé.*2 Diplomatie/.test(t))).toBe(true) expect(texts.some(t => t.includes('Garantit +1 en Santé'))).toBe(true) await ctx1.close() await ctx2.close() }) test('le bouton ACHETER n\'est pas interactif si UNE SEULE des 2 jauges requises 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) // Jauges à 0 en tout début de partie : Église (3 santé + 2 diplo) n'est pas achetable const btnCenter = await hoverShopBuilding(p1, 'church') expect(btnCenter).toBeNull() await ctx1.close() await ctx2.close() }) test('acheter un bâtiment le retire de la boutique pour TOUS les joueurs, débite les 2 jauges, et applique son effet', async ({ browser }) => { test.setTimeout(60_000) 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 Santé ET de Diplomatie pour Alice (coût de l'Église : // 3 santé + 2 diplo), pendant que Bob joue explicitement sa carte la plus // FAIBLE sur ces deux mêmes attributs (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 à chaque partie. let gauges = await getMyGauges(p1) let attempts = 0 while ((gauges.health < 3 || gauges.diplo < 2) && attempts < 12) { await playFullTurn([p1, p2], [ ['health', 'diplo'], { attr: ['health', 'diplo'], mode: 'min' } ]) gauges = await getMyGauges(p1) attempts++ } expect(gauges.health).toBeGreaterThanOrEqual(3) expect(gauges.diplo).toBeGreaterThanOrEqual(2) const idsBefore = await getShopBuildingIds(p1) expect(idsBefore).toContain('church') const bought = await buyShopBuilding(p1, 'church') expect(bought).toBe(true) // Jouer le tour pour déclencher la résolution de l'achat en attente await playFullTurn([p1, p2]) const idsAfterP1 = await getShopBuildingIds(p1) const idsAfterP2 = await getShopBuildingIds(p2) expect(idsAfterP1).not.toContain('church') expect(idsAfterP2).not.toContain('church') expect(idsAfterP1).toContain('castle') // l'autre bâtiment reste disponible // Le bâtiment apparaît dans "mes bâtiments" (posés à gauche de ma main) const myBuildingsCount = await p1.evaluate(() => { const game = window.__ALL_GAMES__[window.__ALL_GAMES__.length - 1] return game.scene.getScene('GameScene').myBuildingTokens.length }) expect(myBuildingsCount).toBe(1) // Et sous le slot d'Alice depuis la perspective de Bob (adversaire) const opponentBuildingsCount = await p2.evaluate(() => { const game = window.__ALL_GAMES__[window.__ALL_GAMES__.length - 1] return game.scene.getScene('GameScene').opponentBuildingTokens.length }) expect(opponentBuildingsCount).toBe(1) await ctx1.close() await ctx2.close() }) })