import { test, expect, type Page } from '@playwright/test'; test.use({ baseURL: 'http://localhost:3000/', }); // ─── Helpers ───────────────────────────────────────────────────────────────── async function waitForMs(page: Page, ms: number) { await page.waitForTimeout(ms); } async function startFreshGame(page: Page) { await page.goto('/'); await page.evaluate(() => localStorage.clear()); await page.reload(); await page.waitForLoadState('networkidle'); await waitForMs(page, 3000); } async function clickTab(page: Page, label: string) { const tab = page.getByRole('tab', { name: new RegExp(label, 'i') }).first(); await tab.click(); await waitForMs(page, 400); } async function clickBtn(page: Page, text: string) { const btn = page.getByRole('button', { name: new RegExp(text, 'i') }).first(); await btn.click(); await waitForMs(page, 200); } async function waitForBridge(page: Page) { for (let attempt = 0; attempt < 30; attempt++) { const ready = await page.evaluate(() => !!(window as any).__TEST__); if (ready) return; await waitForMs(page, 1000); } throw new Error('Debug bridge (window.__TEST__) not available after 30s'); } // ─── Test ───────────────────────────────────────────────────────────────────── test.describe('Combat Happy-Path: Spire Climb → Combat → Mana Recovery → Exit', () => { test('climb spire, fight until mana drains, gather mana to recover, descend, exit', async ({ page }) => { test.setTimeout(600_000); const errors: string[] = []; page.on('console', (msg) => { if (msg.type() === 'error') errors.push(msg.text()); }); // ══════════════════════════════════════════════════════════════════════════ // STEP 1: Start fresh game and wait for bridge // ══════════════════════════════════════════════════════════════════════════ console.log('[TEST] Step 1: Starting fresh game...'); await startFreshGame(page); await waitForMs(page, 1500); await waitForBridge(page); console.log('[TEST] Bridge ready!'); // ══════════════════════════════════════════════════════════════════════════ // STEP 2: Set up prerequisites via Debug tab // ══════════════════════════════════════════════════════════════════════════ console.log('[TEST] Step 2: Setting up prerequisites...'); await clickTab(page, 'debug'); await waitForMs(page, 500); // ── 2a. Unlock all elements ────────────────────────────────────────────── console.log('[TEST] 2a. Unlocking all elements...'); const elementsHeader = page.locator('button', { hasText: /^Elements$/ }).first(); if (await elementsHeader.isVisible({ timeout: 3000 })) { await elementsHeader.click(); await waitForMs(page, 300); } await clickBtn(page, 'Unlock All Elements'); await waitForMs(page, 500); // ── 2b. Boost max mana via Raw Mana Mastery discipline XP ──────────────── console.log('[TEST] 2b. Boosting max mana via Raw Mana Mastery XP...'); await page.evaluate(() => { const disc = (window as any).__TEST__.useDisciplineStore; if (!disc) return; const state = disc.getState(); const existing = state.disciplines['raw-mastery']; const newXP = (existing?.xp || 0) + 20000; disc.setState({ disciplines: { ...state.disciplines, 'raw-mastery': { id: 'raw-mastery', xp: newXP, paused: false }, }, totalXP: state.totalXP + 20000, concurrentLimit: Math.max( state.concurrentLimit, Math.min(4 + Math.floor((state.totalXP + 20000) / 500), 7), ), }); }); await waitForMs(page, 300); const rawMasteryXP = await page.evaluate(() => (window as any).__TEST__.useDisciplineStore.getState().disciplines?.['raw-mastery']?.xp || 0 ); console.log(`[TEST] Raw Mana Mastery XP: ${rawMasteryXP}`); expect(rawMasteryXP).toBe(20000); // ── 2c. Fill mana to max ───────────────────────────────────────────────── console.log('[TEST] 2c. Filling mana to max...'); await clickBtn(page, 'Fill Mana'); await waitForMs(page, 500); const manaAfterFill = await page.evaluate(() => (window as any).__TEST__.useManaStore.getState().rawMana ); console.log(`[TEST] Raw mana after fill: ${manaAfterFill}`); expect(manaAfterFill).toBeGreaterThan(0); // ══════════════════════════════════════════════════════════════════════════ // STEP 3: Enter the Spire via "Climb the Spire" button // ══════════════════════════════════════════════════════════════════════════ console.log('[TEST] Step 3: Entering the Spire...'); await clickTab(page, 'spells'); await waitForMs(page, 500); const climbBtn = page.getByRole('button', { name: /climb the spire/i }).first(); await expect(climbBtn).toBeVisible({ timeout: 10000 }); await climbBtn.click(); await waitForMs(page, 2000); // Verify SpireCombatPage is showing await expect(page.getByText('Floor 1').first()).toBeVisible({ timeout: 10000 }); console.log('[TEST] Spire combat page loaded!'); // ══════════════════════════════════════════════════════════════════════════ // STEP 4: Stay in combat — let the game auto-tick and fight // manaBolt costs 3 raw mana per cast, deals 5 damage. // Floor 1 HP = 151, so ~31 casts to clear = ~258 seconds. // We let it fight for 120 seconds to clear several floors. // ══════════════════════════════════════════════════════════════════════════ console.log('[TEST] Step 4: Fighting in the Spire...'); const startMana = await page.evaluate(() => (window as any).__TEST__.useManaStore.getState().rawMana ); const startFloor = await page.evaluate(() => (window as any).__TEST__.useCombatStore.getState().currentFloor ); console.log(`[TEST] Starting: Floor ${startFloor}, Mana ${startMana}`); console.log('[TEST] Letting combat run for 120 seconds...'); await waitForMs(page, 120000); const floorAfterCombat = await page.evaluate(() => (window as any).__TEST__.useCombatStore.getState().currentFloor ); const manaAfterCombat = await page.evaluate(() => (window as any).__TEST__.useManaStore.getState().rawMana ); console.log(`[TEST] After combat: Floor ${floorAfterCombat}, Mana ${manaAfterCombat}`); expect(floorAfterCombat).toBeGreaterThan(startFloor); // ══════════════════════════════════════════════════════════════════════════ // STEP 5: Exit the Spire to access the Gather button on the main page // The Gather button (ManaDisplay) is in the LeftPanel which is only // rendered on the main game page, not in the SpireCombatPage view. // We exit spire, gather mana, then re-enter. // ══════════════════════════════════════════════════════════════════════════ console.log('[TEST] Step 5: Exiting spire to gather mana...'); // First descend to floor 1 (Exit Spire button only shows on floor 1) for (let i = 0; i < 200; i++) { const floorNow = await page.evaluate(() => (window as any).__TEST__.useCombatStore.getState().currentFloor ); if (floorNow <= 1) break; const climbDownBtn = page.getByRole('button', { name: /climb down/i }).first(); const btnVisible = await climbDownBtn.isVisible({ timeout: 2000 }).catch(() => false); if (btnVisible) { await climbDownBtn.click(); await waitForMs(page, 500); } else { break; } } // Click Exit Spire button (visible on floor 1) const exitBtn = page.getByRole('button', { name: /exit spire/i }).first(); await expect(exitBtn).toBeVisible({ timeout: 10000 }); await exitBtn.click(); await waitForMs(page, 2000); const spireModeAfterExit = await page.evaluate(() => (window as any).__TEST__.useCombatStore.getState().spireMode ); expect(spireModeAfterExit).toBe(false); console.log('[TEST] Exited spire, back on main page'); // ══════════════════════════════════════════════════════════════════════════ // STEP 6: Hold "Gather +X Mana" button to recover mana quickly // The Gather button is in the LeftPanel's ManaDisplay component. // Holding it fires gatherMana() via requestAnimationFrame loop. // ══════════════════════════════════════════════════════════════════════════ console.log('[TEST] Step 6: Holding Gather button to recover mana...'); const gatherBtn = page.getByRole('button', { name: /gather.*mana/i }).first(); await expect(gatherBtn).toBeVisible({ timeout: 10000 }); const manaBeforeGather = await page.evaluate(() => (window as any).__TEST__.useManaStore.getState().rawMana ); console.log(`[TEST] Mana before gather: ${manaBeforeGather}`); // Hold the gather button for 10 seconds await gatherBtn.hover(); await page.mouse.down(); await waitForMs(page, 10000); await page.mouse.up(); console.log('[TEST] Released Gather button.'); const manaAfterGather = await page.evaluate(() => (window as any).__TEST__.useManaStore.getState().rawMana ); console.log(`[TEST] Mana after gathering: ${manaAfterGather}`); expect(manaAfterGather).toBeGreaterThanOrEqual(manaBeforeGather); // ══════════════════════════════════════════════════════════════════════════ // STEP 7: Re-enter the Spire and continue fighting with recovered mana // ══════════════════════════════════════════════════════════════════════════ console.log('[TEST] Step 7: Re-entering the Spire with recovered mana...'); await clickTab(page, 'spells'); await waitForMs(page, 500); const climbBtn2 = page.getByRole('button', { name: /climb the spire/i }).first(); await expect(climbBtn2).toBeVisible({ timeout: 10000 }); await climbBtn2.click(); await waitForMs(page, 2000); await expect(page.getByText('Floor 1').first()).toBeVisible({ timeout: 10000 }); console.log('[TEST] Re-entered spire!'); // Let combat continue console.log('[TEST] Letting combat run for 60 more seconds...'); await waitForMs(page, 60000); const floorAfterMoreCombat = await page.evaluate(() => (window as any).__TEST__.useCombatStore.getState().currentFloor ); console.log(`[TEST] After more combat: Floor ${floorAfterMoreCombat}`); // ══════════════════════════════════════════════════════════════════════════ // STEP 8: Descend the spire back to floor 1 // ══════════════════════════════════════════════════════════════════════════ console.log('[TEST] Step 8: Descending to floor 1...'); for (let i = 0; i < 200; i++) { const floorNow = await page.evaluate(() => (window as any).__TEST__.useCombatStore.getState().currentFloor ); if (floorNow <= 1) break; const climbDownBtn = page.getByRole('button', { name: /climb down/i }).first(); const btnVisible = await climbDownBtn.isVisible({ timeout: 2000 }).catch(() => false); if (btnVisible) { await climbDownBtn.click(); await waitForMs(page, 500); } else { break; } } const floorAfterDescend = await page.evaluate(() => (window as any).__TEST__.useCombatStore.getState().currentFloor ); console.log(`[TEST] Floor after descending: ${floorAfterDescend}`); expect(floorAfterDescend).toBe(1); // ══════════════════════════════════════════════════════════════════════════ // STEP 9: Exit the Spire // ══════════════════════════════════════════════════════════════════════════ console.log('[TEST] Step 9: Exiting the Spire...'); const exitBtn2 = page.getByRole('button', { name: /exit spire/i }).first(); await expect(exitBtn2).toBeVisible({ timeout: 10000 }); await exitBtn2.click(); await waitForMs(page, 2000); const spireModeFinal = await page.evaluate(() => (window as any).__TEST__.useCombatStore.getState().spireMode ); expect(spireModeFinal).toBe(false); // Verify we are back on the main game page await expect(page.getByRole('tab', { name: /spells/i }).first()).toBeVisible({ timeout: 10000 }); console.log('[TEST] Back on main game page!'); // ══════════════════════════════════════════════════════════════════════════ // STEP 10: Verify final state // ══════════════════════════════════════════════════════════════════════════ console.log('[TEST] Step 10: Verifying final state...'); const maxFloorReached = await page.evaluate(() => (window as any).__TEST__.useCombatStore.getState().maxFloorReached ); const gameOver = await page.evaluate(() => (window as any).__TEST__.useUIStore.getState().gameOver ); console.log(`[TEST] MaxFloorReached: ${maxFloorReached}, GameOver: ${gameOver}`); expect(maxFloorReached).toBeGreaterThanOrEqual(1); expect(gameOver).toBe(false); // No React errors throughout the test await waitForMs(page, 1000); const reactErrors = errors.filter(e => e.includes('React') || e.includes('Minified') || e.includes('Error #') || e.includes('Maximum update depth') ); expect(reactErrors, `React errors: ${JSON.stringify(reactErrors)}`).toHaveLength(0); console.log('[TEST] ✅ Combat happy-path test passed!'); }); });