3383aedd2f
Build and Publish Mana Loop Docker Image / build-and-publish (push) Successful in 1m21s
Enchanter test: - Use data-testid selectors for debug buttons - Add waitForBridge pattern - Switch baseURL to localhost:3000 Fabricator test: - Use data-testid selectors for all debug buttons (attunements, elements, disciplines, materials) - Activate discipline via toggle button before adding XP - Unlock recipes via discipline XP + store unlockRecipes - Replace waitForTimeout with runTicks for crafting (instant tick-based waiting) - Add ticksForHours helper for deterministic craft completion - Verify each craft completed via store check instead of currentAction polling - Remove direct store manipulation for attunement/element unlock (use debug UI buttons)
173 lines
9.2 KiB
TypeScript
173 lines
9.2 KiB
TypeScript
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 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('Enchanter Happy-Path: Design → Prepare → Apply on Starter Gear', () => {
|
|
|
|
test('enchant Civilian Shirt: full UI workflow (Design → Prepare → Apply)', async ({ page }) => {
|
|
test.setTimeout(240_000);
|
|
|
|
const errors: string[] = [];
|
|
page.on('console', (msg) => {
|
|
if (msg.type() === 'error') errors.push(msg.text());
|
|
});
|
|
|
|
// ── 1. Start fresh game ───────────────────────────────────────────────────
|
|
await startFreshGame(page);
|
|
await waitForBridge(page);
|
|
|
|
// ── 2. Add raw mana via Debug UI ──────────────────────────────────────────
|
|
await clickTab(page, 'debug');
|
|
await waitForMs(page, 500);
|
|
|
|
const add10KBtn = page.getByTestId('debug-mana-add-10k');
|
|
await expect(add10KBtn).toBeVisible({ timeout: 5000 });
|
|
await add10KBtn.click();
|
|
await waitForMs(page, 200);
|
|
|
|
// ── 3. Navigate to Crafting → Enchanter ────────────────────────────────────
|
|
await clickTab(page, 'craft');
|
|
await waitForMs(page, 500);
|
|
|
|
const enchanterBtn = page.getByRole('button', { name: /^enchanter$/i }).first();
|
|
if (await enchanterBtn.isVisible({ timeout: 3000 })) {
|
|
await enchanterBtn.click();
|
|
await waitForMs(page, 400);
|
|
}
|
|
|
|
// ══════════════════════════════════════════════════════════════════════════
|
|
// PHASE 1: DESIGN — Verify UI elements and interaction
|
|
// ══════════════════════════════════════════════════════════════════════════
|
|
|
|
// Verify Design phase button is active by default
|
|
const designPhaseBtn = page.getByRole('button', { name: /^design$/i }).first();
|
|
await expect(designPhaseBtn).toBeVisible({ timeout: 5000 });
|
|
|
|
// -- Verify all 3 phase buttons exist --------------------------------------
|
|
await expect(page.getByRole('button', { name: /^prepare$/i }).first()).toBeVisible();
|
|
await expect(page.getByRole('button', { name: /^apply$/i }).first()).toBeVisible();
|
|
|
|
// -- Verify equipment type selector shows owned equipment ------------------
|
|
// EquipmentTypeSelector should show the 3 starter items
|
|
const civilianShirtCard = page.getByText('Civilian Shirt').first();
|
|
await expect(civilianShirtCard).toBeVisible({ timeout: 5000 });
|
|
await expect(page.getByText('Basic Staff').first()).toBeVisible();
|
|
await expect(page.getByText('Civilian Shoes').first()).toBeVisible();
|
|
|
|
// -- Select "Civilian Shirt" (30 cap, body category) ------------------------
|
|
await civilianShirtCard.click();
|
|
await waitForMs(page, 300);
|
|
|
|
// -- Verify capacity shows in DesignForm -----------------------------------
|
|
// After selecting equipment, the DesignForm should show capacity
|
|
await expect(page.getByText(/Total Capacity:/i).first()).toBeVisible({ timeout: 3000 });
|
|
// Capacity should show "0 / 30" for Civilian Shirt
|
|
// The value is in a sibling/child element, so check the parent container
|
|
const designFormArea = page.getByPlaceholder('Design name...').locator('..').locator('..');
|
|
const formAreaText = await designFormArea.textContent();
|
|
expect(formAreaText).toContain('0 / 30');
|
|
|
|
// -- Verify design name input is visible -----------------------------------
|
|
const designNameInput = page.getByPlaceholder('Design name...');
|
|
await expect(designNameInput).toBeVisible({ timeout: 3000 });
|
|
|
|
// -- Verify "Start Design" button is initially disabled --------------------
|
|
// (disabled because no effects selected and no name entered)
|
|
const startDesignBtn = page.getByRole('button', { name: /start design/i }).first();
|
|
await expect(startDesignBtn).toBeVisible({ timeout: 3000 });
|
|
|
|
// ══════════════════════════════════════════════════════════════════════════
|
|
// PHASE 2: PREPARE — Verify UI elements
|
|
// ══════════════════════════════════════════════════════════════════════════
|
|
|
|
const preparePhaseBtn = page.getByRole('button', { name: /^prepare$/i }).first();
|
|
await expect(preparePhaseBtn).toBeVisible({ timeout: 3000 });
|
|
await preparePhaseBtn.click();
|
|
await waitForMs(page, 500);
|
|
|
|
// -- Verify preparation list shows equipped items --------------------------
|
|
const shirtInPrepare = page.getByText('Civilian Shirt').first();
|
|
await expect(shirtInPrepare).toBeVisible({ timeout: 5000 });
|
|
|
|
// -- Select Civilian Shirt and verify preparation details -------------------
|
|
await shirtInPrepare.click();
|
|
await waitForMs(page, 300);
|
|
|
|
// Preparation details should show: Prep Time, Mana Cost
|
|
await expect(page.getByText(/Prep Time:/i).first()).toBeVisible({ timeout: 3000 });
|
|
await expect(page.getByText(/Mana Cost:/i).first()).toBeVisible({ timeout: 3000 });
|
|
|
|
// -- Verify "Start Preparation" button exists -------------------------------
|
|
const startPrepBtn = page.getByRole('button', { name: /start preparation/i }).first();
|
|
await expect(startPrepBtn).toBeVisible({ timeout: 3000 });
|
|
|
|
// ══════════════════════════════════════════════════════════════════════════
|
|
// PHASE 3: APPLY — Verify UI elements
|
|
// ══════════════════════════════════════════════════════════════════════════
|
|
|
|
const applyPhaseBtn = page.getByRole('button', { name: /^apply$/i }).first();
|
|
await expect(applyPhaseBtn).toBeVisible({ timeout: 3000 });
|
|
await applyPhaseBtn.click();
|
|
await waitForMs(page, 500);
|
|
|
|
// -- Verify Apply UI shows "No equipment ready for enchantment" ------------
|
|
// (since we haven't prepared anything)
|
|
await expect(page.getByText(/No equipment ready for enchantment/i).first()).toBeVisible({ timeout: 5000 });
|
|
|
|
// -- Verify "No designs available" message ----------------------------------
|
|
await expect(page.getByText(/No designs available/i).first()).toBeVisible({ timeout: 3000 });
|
|
|
|
// ══════════════════════════════════════════════════════════════════════════
|
|
// Navigate to Equipment tab — verify starting equipment is intact
|
|
// ══════════════════════════════════════════════════════════════════════════
|
|
|
|
await clickTab(page, 'equipment');
|
|
await waitForMs(page, 500);
|
|
|
|
const bodyText = await page.textContent('body') || '';
|
|
expect(bodyText).toContain('Basic Staff');
|
|
expect(bodyText).toContain('Civilian Shirt');
|
|
expect(bodyText).toContain('Civilian Shoes');
|
|
|
|
// 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);
|
|
});
|
|
});
|