This commit is contained in:
@@ -1,4 +1,4 @@
|
|||||||
# Circular Dependencies
|
# Circular Dependencies
|
||||||
Generated: 2026-05-30T08:40:53.794Z
|
Generated: 2026-05-30T13:27:24.782Z
|
||||||
|
|
||||||
No circular dependencies found. ✅
|
No circular dependencies found. ✅
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"_meta": {
|
"_meta": {
|
||||||
"generated": "2026-05-30T08:40:52.033Z",
|
"generated": "2026-05-30T13:27:23.117Z",
|
||||||
"description": "Import dependency graph for src/lib/game. Keys are files, values are arrays of files they import.",
|
"description": "Import dependency graph for src/lib/game. Keys are files, values are arrays of files they import.",
|
||||||
"usage": "To find what a file affects, search for its path in the VALUES. To find what a file depends on, look at its KEY entry."
|
"usage": "To find what a file affects, search for its path in the VALUES. To find what a file depends on, look at its KEY entry."
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -0,0 +1,621 @@
|
|||||||
|
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(', ')}`);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
@@ -2,15 +2,16 @@ import { defineConfig, devices } from '@playwright/test';
|
|||||||
|
|
||||||
export default defineConfig({
|
export default defineConfig({
|
||||||
testDir: 'e2e',
|
testDir: 'e2e',
|
||||||
fullyParallel: true,
|
fullyParallel: false,
|
||||||
forbidOnly: !!process.env.CI,
|
forbidOnly: !!process.env.CI,
|
||||||
retries: process.env.CI ? 2 : 0,
|
retries: 0,
|
||||||
workers: process.env.CI ? 1 : undefined,
|
workers: 1,
|
||||||
|
timeout: 60000,
|
||||||
reporter: 'html',
|
reporter: 'html',
|
||||||
use: {
|
use: {
|
||||||
baseURL: 'http://localhost:3000',
|
baseURL: 'https://manaloop.tailf367e3.ts.net/',
|
||||||
trace: 'on-first-retry',
|
trace: 'on-first-retry',
|
||||||
screenshot: 'only-on-failure',
|
screenshot: 'on',
|
||||||
video: 'retain-on-failure',
|
video: 'retain-on-failure',
|
||||||
},
|
},
|
||||||
projects: [
|
projects: [
|
||||||
|
|||||||
Binary file not shown.
|
Before Width: | Height: | Size: 87 KiB After Width: | Height: | Size: 92 KiB |
@@ -10,7 +10,7 @@ import {
|
|||||||
RotateCcw, AlertTriangle, Zap, Clock, Settings, Eye,
|
RotateCcw, AlertTriangle, Zap, Clock, Settings, Eye,
|
||||||
} from 'lucide-react';
|
} from 'lucide-react';
|
||||||
import { DebugName, useDebug } from '@/components/game/debug/debug-context';
|
import { DebugName, useDebug } from '@/components/game/debug/debug-context';
|
||||||
import { useGameStore, useManaStore, useUIStore, useCombatStore } from '@/lib/game/stores';
|
import { useGameStore, useManaStore, useUIStore } from '@/lib/game/stores';
|
||||||
import { computeMaxMana } from '@/lib/game/stores';
|
import { computeMaxMana } from '@/lib/game/stores';
|
||||||
|
|
||||||
// ─── Warning Banner ──────────────────────────────────────────────────────────
|
// ─── Warning Banner ──────────────────────────────────────────────────────────
|
||||||
|
|||||||
@@ -5,7 +5,6 @@ import { ATTUNEMENTS_DEF, ATTUNEMENT_SLOT_NAMES, getAttunementXPForLevel, MAX_AT
|
|||||||
import type { AttunementDef, AttunementState } from '@/lib/game/types';
|
import type { AttunementDef, AttunementState } from '@/lib/game/types';
|
||||||
import { Card, CardContent } from '@/components/ui/card';
|
import { Card, CardContent } from '@/components/ui/card';
|
||||||
import { Badge } from '@/components/ui/badge';
|
import { Badge } from '@/components/ui/badge';
|
||||||
import { Progress } from '@/components/ui/progress';
|
|
||||||
import { DebugName } from '@/components/game/debug/debug-context';
|
import { DebugName } from '@/components/game/debug/debug-context';
|
||||||
import { fmt } from '@/lib/game/stores';
|
import { fmt } from '@/lib/game/stores';
|
||||||
|
|
||||||
|
|||||||
@@ -9,12 +9,7 @@ import { ScrollArea } from '@/components/ui/scroll-area';
|
|||||||
import { Separator } from '@/components/ui/separator';
|
import { Separator } from '@/components/ui/separator';
|
||||||
import { Anvil, FlaskConical, Hammer, Package, Sparkles, Sword } from 'lucide-react';
|
import { Anvil, FlaskConical, Hammer, Package, Sparkles, Sword } from 'lucide-react';
|
||||||
import { MaterialRecipeCard } from './MaterialRecipeCard';
|
import { MaterialRecipeCard } from './MaterialRecipeCard';
|
||||||
import {
|
import { FABRICATOR_RECIPES, MATERIAL_RECIPES, canCraftRecipe } from '@/lib/game/data/fabricator-recipes';
|
||||||
FABRICATOR_RECIPES,
|
|
||||||
MATERIAL_RECIPES,
|
|
||||||
getRecipesByManaType,
|
|
||||||
canCraftRecipe,
|
|
||||||
} from '@/lib/game/data/fabricator-recipes';
|
|
||||||
import { MANA_TYPE_LABELS } from '@/lib/game/data/fabricator-recipe-types';
|
import { MANA_TYPE_LABELS } from '@/lib/game/data/fabricator-recipe-types';
|
||||||
import { LOOT_DROPS, LOOT_RARITY_COLORS } from '@/lib/game/data/loot-drops';
|
import { LOOT_DROPS, LOOT_RARITY_COLORS } from '@/lib/game/data/loot-drops';
|
||||||
import { useCraftingStore, useManaStore } from '@/lib/game/stores';
|
import { useCraftingStore, useManaStore } from '@/lib/game/stores';
|
||||||
|
|||||||
@@ -10,7 +10,7 @@ import {
|
|||||||
RotateCcw, AlertTriangle, Zap, Clock, Eye,
|
RotateCcw, AlertTriangle, Zap, Clock, Eye,
|
||||||
} from 'lucide-react';
|
} from 'lucide-react';
|
||||||
import { DebugName, useDebug } from '@/components/game/debug/debug-context';
|
import { DebugName, useDebug } from '@/components/game/debug/debug-context';
|
||||||
import { useGameStore, useManaStore, useUIStore, useCombatStore } from '@/lib/game/stores';
|
import { useGameStore, useManaStore, useUIStore } from '@/lib/game/stores';
|
||||||
import { computeMaxMana } from '@/lib/game/stores';
|
import { computeMaxMana } from '@/lib/game/stores';
|
||||||
|
|
||||||
// ─── Display Options ─────────────────────────────────────────────────────────
|
// ─── Display Options ─────────────────────────────────────────────────────────
|
||||||
|
|||||||
@@ -1,6 +1,5 @@
|
|||||||
import React, { useMemo } from 'react';
|
import React, { useMemo } from 'react';
|
||||||
import type { DisciplineDefinition } from '@/lib/game/types/disciplines';
|
import type { DisciplineDefinition } from '@/lib/game/types/disciplines';
|
||||||
import type { ManaType } from '@/lib/game/types/elements';
|
|
||||||
import { ELEMENTS } from '@/lib/game/constants/elements';
|
import { ELEMENTS } from '@/lib/game/constants/elements';
|
||||||
import { calculateStatBonus, calculateManaDrain } from '@/lib/game/utils/discipline-math';
|
import { calculateStatBonus, calculateManaDrain } from '@/lib/game/utils/discipline-math';
|
||||||
import clsx from 'clsx';
|
import clsx from 'clsx';
|
||||||
@@ -43,7 +42,7 @@ export const DisciplineCard: React.FC<DisciplineCardProps> = ({
|
|||||||
const manaIcon = elementDef?.sym ?? '✦';
|
const manaIcon = elementDef?.sym ?? '✦';
|
||||||
const manaName = elementDef?.name ?? manaType;
|
const manaName = elementDef?.name ?? manaType;
|
||||||
const isActive = activeIds.includes(id);
|
const isActive = activeIds.includes(id);
|
||||||
const activeNotPaused = activeIds.filter((aid) => {
|
const _activeNotPaused = activeIds.filter((aid) => {
|
||||||
// Count how many active disciplines are not paused
|
// Count how many active disciplines are not paused
|
||||||
return aid === id ? !isPaused : true;
|
return aid === id ? !isPaused : true;
|
||||||
}).length;
|
}).length;
|
||||||
|
|||||||
@@ -6,8 +6,6 @@ import type { ElementState } from './types';
|
|||||||
import type { FabricatorRecipe } from './data/fabricator-recipes';
|
import type { FabricatorRecipe } from './data/fabricator-recipes';
|
||||||
import { FABRICATOR_RECIPES } from './data/fabricator-recipes';
|
import { FABRICATOR_RECIPES } from './data/fabricator-recipes';
|
||||||
import { useManaStore } from './stores/manaStore';
|
import { useManaStore } from './stores/manaStore';
|
||||||
import { useCombatStore } from './stores/combatStore';
|
|
||||||
import { useUIStore } from './stores/uiStore';
|
|
||||||
|
|
||||||
// ─── Lookup ───────────────────────────────────────────────────────────────────
|
// ─── Lookup ───────────────────────────────────────────────────────────────────
|
||||||
|
|
||||||
|
|||||||
@@ -73,7 +73,6 @@ export function getGuardianHP(floor: number): number {
|
|||||||
|
|
||||||
// ─── Tier Helpers ───────────────────────────────────────────────────────────────
|
// ─── Tier Helpers ───────────────────────────────────────────────────────────────
|
||||||
|
|
||||||
const BASE_ELEMENTS = ['fire', 'water', 'air', 'earth', 'light', 'dark', 'death'];
|
|
||||||
const COMPOSITE_ELEMENTS = ['metal', 'sand', 'lightning', 'frost', 'blackflame', 'radiantflames', 'miasma', 'shadowglass'];
|
const COMPOSITE_ELEMENTS = ['metal', 'sand', 'lightning', 'frost', 'blackflame', 'radiantflames', 'miasma', 'shadowglass'];
|
||||||
const EXOTIC_ELEMENTS = ['crystal', 'stellar', 'void', 'soul', 'time', 'plasma'];
|
const EXOTIC_ELEMENTS = ['crystal', 'stellar', 'void', 'soul', 'time', 'plasma'];
|
||||||
|
|
||||||
|
|||||||
@@ -1,4 +0,0 @@
|
|||||||
{
|
|
||||||
"status": "passed",
|
|
||||||
"failedTests": []
|
|
||||||
}
|
|
||||||
Reference in New Issue
Block a user