import { test, expect, type Page } from '@playwright/test'; // Use the deployed production URL test.use({ baseURL: 'https://manaloop.tailf367e3.ts.net/', }); // Helper: Clear localStorage and reload for fresh game async function startFreshGame(page: Page) { await page.goto('/'); await page.evaluate(() => localStorage.clear()); await page.reload(); await page.waitForLoadState('networkidle'); } // Helper: Run debug command via console async function runDebug(page: Page, cmd: string) { await page.evaluate((c) => { // @ts-expect-error - debug function on window if (typeof window.__debug === 'function') window.__debug(c); }, cmd); } // Helper: Wait for game to tick a few times async function waitForTicks(page: Page, ms = 1000) { await page.waitForTimeout(ms); } test.describe('Mana Loop - Comprehensive Playtest', () => { // ========================================================================= // SECTION 1: Basic UI & Starting State // ========================================================================= test.describe('1 - Basic UI & Starting State', () => { test.beforeEach(async ({ page }) => { await startFreshGame(page); }); test('game loads without console errors', async ({ page }) => { const errors: string[] = []; page.on('console', (msg) => { if (msg.type() === 'error') errors.push(msg.text()); }); await waitForTicks(page, 2000); const reactErrors = errors.filter(e => e.includes('React') || e.includes('Minified') || e.includes('Error #') || e.includes('Maximum update depth') ); expect(reactErrors, `React errors found: ${JSON.stringify(reactErrors)}`).toHaveLength(0); }); test('ManaDisplay is visible and shows Transference mana', async ({ page }) => { await waitForTicks(page, 500); // Mana display should show Transference mana pool const manaDisplay = page.locator('text=Transference').first(); await expect(manaDisplay).toBeVisible({ timeout: 10000 }); }); test('TimeDisplay shows correct starting time', async ({ page }) => { await waitForTicks(page, 500); // Should start at day 1 const bodyText = await page.textContent('body'); expect(bodyText).toContain('Day 1'); }); test('Activity log is present and shows start message', async ({ page }) => { await waitForTicks(page, 500); const bodyText = await page.textContent('body'); // Activity log should have some content expect(bodyText).toBeTruthy(); }); }); // ========================================================================= // SECTION 2 - Stats Tab (Known bugs #208 and #210) // ========================================================================= test.describe('2 - Stats Tab', () => { test.beforeEach(async ({ page }) => { await startFreshGame(page); }); test('navigate to Stats tab', async ({ page }) => { await waitForTicks(page, 500); const statsTab = page.getByRole('tab', { name: /stats/i }); if (await statsTab.isVisible()) { await statsTab.click(); await waitForTicks(page, 300); // Should not crash const bodyText = await page.textContent('body'); expect(bodyText).toBeTruthy(); } }); test('KNOWN BUG #208: Meditation multiplier shows 0x instead of 1x', async ({ page }) => { await waitForTicks(page, 500); const statsTab = page.getByRole('tab', { name: /stats/i }); if (await statsTab.isVisible()) { await statsTab.click(); await waitForTicks(page, 500); const bodyText = await page.textContent('body') || ''; // The bug: Meditation Multiplier shows "0x" instead of "1.00x" // This test documents the current state if (bodyText.includes('Meditation')) { console.log('STATS: Meditation text found, checking value...'); // Capture the actual state for reporting } } }); test('KNOWN BUG #208: Effective Regen shows 0/hr', async ({ page }) => { await waitForTicks(page, 500); const statsTab = page.getByRole('tab', { name: /stats/i }); if (await statsTab.isVisible()) { await statsTab.click(); await waitForTicks(page, 500); const bodyText = await page.textContent('body') || ''; if (bodyText.includes('Effective Regen') || bodyText.includes('Base Regen')) { console.log('STATS: Regen stats found'); } } }); test('KNOWN BUG #210: Total Max Mana ignores discipline bonuses', async ({ page }) => { await waitForTicks(page, 500); // Navigate to stats const statsTab = page.getByRole('tab', { name: /stats/i }); if (await statsTab.isVisible()) { await statsTab.click(); await waitForTicks(page, 300); // Check if Total Max Mana is shown const bodyText = await page.textContent('body') || ''; console.log('STATS: Max Mana section - checking for discipline bonus inclusion'); } }); }); // ========================================================================= // SECTION 3 - Spire/Climbing (Known bug #209) // ========================================================================= test.describe('3 - Spire / Climbing', () => { test.beforeEach(async ({ page }) => { await startFreshGame(page); }); test('KNOWN BUG #209: Climb the Spire should not crash with React error #185', async ({ page }) => { const errors: string[] = []; page.on('console', (msg) => { if (msg.type() === 'error') errors.push(msg.text()); }); await waitForTicks(page, 500); // Look for "Climb the Spire" button or Spire tab const spireTab = page.getByRole('tab', { name: /spire/i }); const climbButton = page.getByRole('button', { name: /climb/i }); if (await spireTab.isVisible({ timeout: 5000 })) { await spireTab.click(); await waitForTicks(page, 300); } if (await climbButton.isVisible({ timeout: 5000 })) { await climbButton.click(); await waitForTicks(page, 2000); const reactErrors = errors.filter(e => e.includes('Maximum update depth') || e.includes('Error #185') ); // This is a known bug - we expect it to fail if (reactErrors.length > 0) { console.log('KNOWN BUG #209 CONFIRMED: Spire crash detected'); } else { console.log('KNOWN BUG #209: No crash detected - may be fixed'); } } else { console.log('Climb the Spire button not found - may need setup'); } }); }); // ========================================================================= // SECTION 4 - Disciplines Tab // ========================================================================= test.describe('4 - Disciplines', () => { test.beforeEach(async ({ page }) => { await startFreshGame(page); }); test('navigate to Disciplines tab without crash', async ({ page }) => { const errors: string[] = []; page.on('console', (msg) => { if (msg.type() === 'error') errors.push(msg.text()); }); await waitForTicks(page, 500); const discTab = page.getByRole('tab', { name: /disciplines/i }); if (await discTab.isVisible({ timeout: 5000 })) { await discTab.click(); await waitForTicks(page, 500); const reactErrors = errors.filter(e => e.includes('React') || e.includes('Minified') || e.includes('Error #') ); expect(reactErrors, `React errors in Disciplines: ${JSON.stringify(reactErrors)}`).toHaveLength(0); } }); test('Raw Mana Mastery discipline is available', async ({ page }) => { await waitForTicks(page, 500); const discTab = page.getByRole('tab', { name: /disciplines/i }); if (await discTab.isVisible({ timeout: 5000 })) { await discTab.click(); await waitForTicks(page, 300); const bodyText = await page.textContent('body') || ''; // Raw Mana Mastery should be available since Enchanter is attuned if (bodyText.includes('Raw Mana Mastery')) { console.log('DISCIPLINE: Raw Mana Mastery found'); } } }); }); // ========================================================================= // SECTION 5 - Crafting Tab // ========================================================================= test.describe('5 - Crafting System', () => { test.beforeEach(async ({ page }) => { await startFreshGame(page); }); test('navigate to Crafting tab without crash', async ({ page }) => { const errors: string[] = []; page.on('console', (msg) => { if (msg.type() === 'error') errors.push(msg.text()); }); await waitForTicks(page, 500); const craftTab = page.getByRole('tab', { name: /craft/i }); if (await craftTab.isVisible({ timeout: 5000 })) { await craftTab.click(); await waitForTicks(page, 500); const reactErrors = errors.filter(e => e.includes('React') || e.includes('Minified') || e.includes('Error #') ); expect(reactErrors, `React errors in Crafting: ${JSON.stringify(reactErrors)}`).toHaveLength(0); } }); test('Enchant sub-tab exists and is clickable', async ({ page }) => { await waitForTicks(page, 500); const craftTab = page.getByRole('tab', { name: /craft/i }); if (await craftTab.isVisible({ timeout: 5000 })) { await craftTab.click(); await waitForTicks(page, 300); // Look for Enchant sub-tab or section const bodyText = await page.textContent('body') || ''; expect(bodyText).toBeTruthy(); } }); }); // ========================================================================= // SECTION 6 - Equipment Tab // ========================================================================= test.describe('6 - Equipment & Inventory', () => { test.beforeEach(async ({ page }) => { await startFreshGame(page); }); test('navigate to Equipment tab without crash', async ({ page }) => { const errors: string[] = []; page.on('console', (msg) => { if (msg.type() === 'error') errors.push(msg.text()); }); await waitForTicks(page, 500); const equipTab = page.getByRole('tab', { name: /equipment/i }); if (await equipTab.isVisible({ timeout: 5000 })) { await equipTab.click(); await waitForTicks(page, 500); const reactErrors = errors.filter(e => e.includes('React') || e.includes('Minified') || e.includes('Error #') ); expect(reactErrors, `React errors in Equipment: ${JSON.stringify(reactErrors)}`).toHaveLength(0); } }); test('starting equipment includes Basic Staff, Civilian Shirt, Civilian Shoes', async ({ page }) => { await waitForTicks(page, 500); const equipTab = page.getByRole('tab', { name: /equipment/i }); if (await equipTab.isVisible({ timeout: 5000 })) { await equipTab.click(); await waitForTicks(page, 300); const bodyText = await page.textContent('body') || ''; // Check for starting equipment console.log('EQUIPMENT: Checking starting equipment...'); if (bodyText.includes('Basic Staff')) { console.log('EQUIPMENT: Basic Staff found ✓'); } if (bodyText.includes('Civilian Shirt')) { console.log('EQUIPMENT: Civilian Shirt found ✓'); } if (bodyText.includes('Civilian Shoes') || bodyText.includes('Civilian')) { console.log('EQUIPMENT: Civilian gear found ✓'); } } }); }); // ========================================================================= // SECTION 7 - Attunements Tab // ========================================================================= test.describe('7 - Attunements', () => { test.beforeEach(async ({ page }) => { await startFreshGame(page); }); test('navigate to Attunements tab without crash', async ({ page }) => { const errors: string[] = []; page.on('console', (msg) => { if (msg.type() === 'error') errors.push(msg.text()); }); await waitForTicks(page, 500); const attuneTab = page.getByRole('tab', { name: /attun/i }); if (await attuneTab.isVisible({ timeout: 5000 })) { await attuneTab.click(); await waitForTicks(page, 500); const reactErrors = errors.filter(e => e.includes('React') || e.includes('Minified') || e.includes('Error #') ); expect(reactErrors, `React errors in Attunements: ${JSON.stringify(reactErrors)}`).toHaveLength(0); } }); test('Enchanter is attuned at level 1 by default', async ({ page }) => { await waitForTicks(page, 500); const attuneTab = page.getByRole('tab', { name: /attun/i }); if (await attuneTab.isVisible({ timeout: 5000 })) { await attuneTab.click(); await waitForTicks(page, 300); const bodyText = await page.textContent('body') || ''; if (bodyText.includes('Enchanter')) { console.log('ATTUNEMENT: Enchanter found ✓'); } } }); }); // ========================================================================= // SECTION 8 - Spells Tab // ========================================================================= test.describe('8 - Spells Tab', () => { test.beforeEach(async ({ page }) => { await startFreshGame(page); }); test('navigate to Spells tab without crash', async ({ page }) => { const errors: string[] = []; page.on('console', (msg) => { if (msg.type() === 'error') errors.push(msg.text()); }); await waitForTicks(page, 500); const spellsTab = page.getByRole('tab', { name: /spell/i }); if (await spellsTab.isVisible({ timeout: 5000 })) { await spellsTab.click(); await waitForTicks(page, 500); const reactErrors = errors.filter(e => e.includes('React') || e.includes('Minified') || e.includes('Error #') ); expect(reactErrors, `React errors in Spells: ${JSON.stringify(reactErrors)}`).toHaveLength(0); } }); }); // ========================================================================= // SECTION 9 - Prestige Tab // ========================================================================= test.describe('9 - Prestige Tab', () => { test.beforeEach(async ({ page }) => { await startFreshGame(page); }); test('navigate to Prestige tab without crash', async ({ page }) => { const errors: string[] = []; page.on('console', (msg) => { if (msg.type() === 'error') errors.push(msg.text()); }); await waitForTicks(page, 500); const prestigeTab = page.getByRole('tab', { name: /prestige/i }); if (await prestigeTab.isVisible({ timeout: 5000 })) { await prestigeTab.click(); await waitForTicks(page, 500); const reactErrors = errors.filter(e => e.includes('React') || e.includes('Minified') || e.includes('Error #') ); expect(reactErrors, `React errors in Prestige: ${JSON.stringify(reactErrors)}`).toHaveLength(0); } }); }); // ========================================================================= // SECTION 10 - Golemancy Tab // ========================================================================= test.describe('10 - Golemancy Tab', () => { test.beforeEach(async ({ page }) => { await startFreshGame(page); }); test('navigate to Golemancy tab without crash', async ({ page }) => { const errors: string[] = []; page.on('console', (msg) => { if (msg.type() === 'error') errors.push(msg.text()); }); await waitForTicks(page, 500); const golemTab = page.getByRole('tab', { name: /golem/i }); if (await golemTab.isVisible({ timeout: 5000 })) { await golemTab.click(); await waitForTicks(page, 500); const reactErrors = errors.filter(e => e.includes('React') || e.includes('Minified') || e.includes('Error #') ); expect(reactErrors, `React errors in Golemancy: ${JSON.stringify(reactErrors)}`).toHaveLength(0); } }); }); // ========================================================================= // SECTION 11 - Guardian Pacts Tab // ========================================================================= test.describe('11 - Guardian Pacts Tab', () => { test.beforeEach(async ({ page }) => { await startFreshGame(page); }); test('navigate to Guardian Pacts tab without crash', async ({ page }) => { const errors: string[] = []; page.on('console', (msg) => { if (msg.type() === 'error') errors.push(msg.text()); }); await waitForTicks(page, 500); const pactsTab = page.getByRole('tab', { name: /pact/i }); if (await pactsTab.isVisible({ timeout: 5000 })) { await pactsTab.click(); await waitForTicks(page, 500); const reactErrors = errors.filter(e => e.includes('React') || e.includes('Minified') || e.includes('Error #') ); expect(reactErrors, `React errors in Guardian Pacts: ${JSON.stringify(reactErrors)}`).toHaveLength(0); } }); }); // ========================================================================= // SECTION 12 - Grimoire Tab // ========================================================================= test.describe('12 - Grimoire Tab', () => { test.beforeEach(async ({ page }) => { await startFreshGame(page); }); test('navigate to Grimoire tab without crash', async ({ page }) => { const errors: string[] = []; page.on('console', (msg) => { if (msg.type() === 'error') errors.push(msg.text()); }); await waitForTicks(page, 500); const grimoireTab = page.getByRole('tab', { name: /grimoire/i }); if (await grimoireTab.isVisible({ timeout: 5000 })) { await grimoireTab.click(); await waitForTicks(page, 500); const reactErrors = errors.filter(e => e.includes('React') || e.includes('Minified') || e.includes('Error #') ); expect(reactErrors, `React errors in Grimoire: ${JSON.stringify(reactErrors)}`).toHaveLength(0); } }); }); // ========================================================================= // SECTION 13 - Achievements Tab // ========================================================================= test.describe('13 - Achievements Tab', () => { test.beforeEach(async ({ page }) => { await startFreshGame(page); }); test('navigate to Achievements tab without crash', async ({ page }) => { const errors: string[] = []; page.on('console', (msg) => { if (msg.type() === 'error') errors.push(msg.text()); }); await waitForTicks(page, 500); const achTab = page.getByRole('tab', { name: /achievement/i }); if (await achTab.isVisible({ timeout: 5000 })) { await achTab.click(); await waitForTicks(page, 500); const reactErrors = errors.filter(e => e.includes('React') || e.includes('Minified') || e.includes('Error #') ); expect(reactErrors, `React errors in Achievements: ${JSON.stringify(reactErrors)}`).toHaveLength(0); } }); }); // ========================================================================= // SECTION 14 - Debug Tab & Cheats // ========================================================================= test.describe('14 - Debug Tab & Cheats', () => { test.beforeEach(async ({ page }) => { await startFreshGame(page); }); test('navigate to Debug tab without crash', async ({ page }) => { const errors: string[] = []; page.on('console', (msg) => { if (msg.type() === 'error') errors.push(msg.text()); }); await waitForTicks(page, 500); const debugTab = page.getByRole('tab', { name: /debug/i }); if (await debugTab.isVisible({ timeout: 5000 })) { await debugTab.click(); await waitForTicks(page, 500); const reactErrors = errors.filter(e => e.includes('React') || e.includes('Minified') || e.includes('Error #') ); expect(reactErrors, `React errors in Debug: ${JSON.stringify(reactErrors)}`).toHaveLength(0); } }); }); // ========================================================================= // SECTION 15 - Deep Bug Hunting with Debug Console // ========================================================================= test.describe('15 - Deep Bug Hunting (Debug Mode)', () => { test.beforeEach(async ({ page }) => { await startFreshGame(page); }); test('mana regen values in ManaDisplay are correct', async ({ page }) => { await waitForTicks(page, 1000); const bodyText = await page.textContent('body') || ''; // Check that mana regen shows positive values for Transference // Look for regen rate patterns like "+X/hr" console.log('HUNT: Checking mana regen display values'); const matches = bodyText.match(/\+[\d.]+(\/hr)?/g); console.log(`HUNT: Found regen patterns: ${JSON.stringify(matches)}`); }); test('element tab shows correct element unlock status', async ({ page }) => { const errors: string[] = []; page.on('console', (msg) => { if (msg.type() === 'error') errors.push(msg.text()); }); await waitForTicks(page, 500); // Try to find element-related tabs const elemTab = page.getByRole('tab', { name: /element/i }); if (await elemTab.isVisible({ timeout: 3000 })) { await elemTab.click(); await waitForTicks(page, 500); const reactErrors = errors.filter(e => e.includes('React') || e.includes('Minified') || e.includes('Error #') ); expect(reactErrors, `React errors in Elements: ${JSON.stringify(reactErrors)}`).toHaveLength(0); } }); test('mana values stay consistent after multiple ticks', async ({ page }) => { await waitForTicks(page, 500); // Take a snapshot of mana values const bodyBefore = await page.textContent('body') || ''; await waitForTicks(page, 2000); const bodyAfter = await page.textContent('body') || ''; // Game should still be running (no crash) expect(bodyAfter).toBeTruthy(); console.log('HUNT: Game still running after 2 seconds of ticking ✓'); }); test('all navigations work in sequence without crash', async ({ page }) => { const errors: string[] = []; page.on('console', (msg) => { if (msg.type() === 'error') errors.push(msg.text()); }); await waitForTicks(page, 500); const tabs = [ 'stats', 'equipment', 'attunements', 'crafting', 'disciplines', 'spells', 'prestige', 'golemancy', 'pacts', 'achievements', 'grimoire', 'debug' ]; const visitedTabs: string[] = []; const crashTabs: string[] = []; for (const tabName of tabs) { const tab = page.getByRole('tab', { name: new RegExp(tabName, 'i') }); if (await tab.isVisible({ timeout: 2000 })) { const preErrors = [...errors]; await tab.click(); await waitForTicks(page, 300); const newErrors = errors.filter(e => !preErrors.includes(e)); const reactErrors = newErrors.filter(e => e.includes('React') || e.includes('Minified') || e.includes('Error #') ); if (reactErrors.length > 0) { crashTabs.push(tabName); } visitedTabs.push(tabName); } } console.log(`HUNT: Visited tabs: ${visitedTabs.join(', ')}`); console.log(`HUNT: Tabs with React errors: ${crashTabs.join(', ')}`); }); }); });