const { chromium } = require('C:/Users/phenom/.cache/codex-runtimes/codex-primary-runtime/dependencies/node/node_modules/playwright'); const fs = require('node:fs'); const path = require('node:path'); const assert = require('node:assert/strict'); const out = path.resolve('playtest-artifacts/dungeon-race'); fs.mkdirSync(out, { recursive: true }); const base = process.env.RACE_TEST_URL || 'http://127.0.0.1:5173'; (async () => { const browser = await chromium.launch({ channel: 'msedge', headless: true }); const page = await browser.newPage({ viewport: { width: 1280, height: 800 } }); const errors = []; page.on('pageerror', (error) => errors.push(error.message)); try { await page.goto(base, { waitUntil: 'domcontentloaded', timeout: 120_000 }); await page.getByRole('button', { name: /Play offline/ }).click(); await page.getByRole('button', { name: /Create character/ }).click(); await page.getByRole('button', { name: /Customize & name/ }).click(); await page.getByLabel('Character name').fill('RaceTester'); await page.getByRole('button', { name: /Create character/ }).click(); await page.getByRole('button', { name: /Continue|Enter world/ }).first().click({ force: true }); await page.getByRole('button', { name: 'Continue', exact: true }).click(); await page.locator('.main-menu-screen').waitFor(); await page.getByRole('button', { name: /Dungeon Race/ }).click(); await page.locator('.dungeon-race-screen').waitFor(); await page.screenshot({ path: path.join(out, '01-setup-desktop.png') }); assert.equal(await page.getByRole('button', { name: 'Offline practice' }).getAttribute('aria-pressed'), 'true'); await page.locator('.dungeon-race-screen__setup select').first() .selectOption('bite-sized-stormwind-stockade'); await page.getByRole('button', { name: 'Start practice' }).click(); await page.locator('.game-shell').waitFor({ timeout: 120_000 }); await page.locator('.dungeon-race-hud').waitFor({ timeout: 120_000 }); await page.locator('.dungeon-boss-intro').waitFor({ state: 'visible', timeout: 120_000 }); await page.locator('.dungeon-boss-intro').waitFor({ state: 'hidden', timeout: 120_000 }); await page.locator('.dungeon-race-countdown').waitFor({ state: 'visible', timeout: 30_000 }); await page.locator('.dungeon-race-countdown').waitFor({ state: 'hidden', timeout: 30_000 }); await page.screenshot({ path: path.join(out, '02-active-desktop.png') }); const state = await page.evaluate(async () => { const { useDungeonRaceStore } = await import('/src/app/dungeonRaceStore.ts'); const { useGameStore } = await import('/src/game/store.ts'); const race = useDungeonRaceStore.getState(); return { mode: race.mode, status: race.status, inGame: race.inGame, dungeonId: race.dungeonId, activeDungeonId: useGameStore.getState().activeDungeonId, opponentSeats: race.opponent?.type === 'bot' ? race.opponent.roster.length : 0 }; }); assert.equal(state.mode, 'practice'); assert.equal(state.status, 'active'); assert.equal(state.inGame, true); assert.equal(state.opponentSeats, 5); assert.equal(state.activeDungeonId, state.dungeonId); await page.evaluate(async () => { const { useDungeonRaceStore } = await import('/src/app/dungeonRaceStore.ts'); const race = useDungeonRaceStore.getState(); useDungeonRaceStore.setState({ startAt: Date.now() - race.opponent.finishMs - 1000, result: null }); useDungeonRaceStore.getState().tick(); }); await page.locator('.dungeon-race-toast--loss').waitFor(); assert.equal(await page.locator('.game-shell').count(), 1); await page.screenshot({ path: path.join(out, '03-loss-continues-desktop.png') }); await page.setViewportSize({ width: 960, height: 540 }); await page.screenshot({ path: path.join(out, '04-loss-thor-size.png') }); const card = await page.evaluate(async () => { const { useCombatStore } = await import('/src/game/combatStore.ts'); const { BOSS_CARD_CATALOG } = await import('/src/game/bossCardCatalog.ts'); const definition = BOSS_CARD_CATALOG.find((entry) => entry.dungeons.some((dungeon) => dungeon.id === 'stormwind-stockade')); const mob = Object.values(useCombatStore.getState().mobs)[0]; if (!definition || !mob) throw new Error(`Boss-card fixture is unavailable: definition=${Boolean(definition)} mob=${Boolean(mob)}.`); const before = useCombatStore.getState().bossCards.counts[definition.key]?.normal ?? 0; useCombatStore.setState((state) => ({ mobs: { ...state.mobs, [mob.id]: { ...mob, name: definition.name, boss: true, dead: true, hasLoot: true, looted: false, despawned: false, pendingLoot: [], bossCardDrop: { bossKey: definition.key, spawnId: mob.id, claimId: `${Date.now()}:1`, rarities: ['normal'] } } } })); const looted = useCombatStore.getState().lootMob(mob.id); return { looted, before, after: useCombatStore.getState().bossCards.counts[definition.key]?.normal ?? 0 }; }); console.log('Post-loss boss-card result:', card); assert.equal(card.after, card.before + 1); await page.waitForTimeout(300); await page.screenshot({ path: path.join(out, '05-card-after-loss-fixture.png') }); await page.keyboard.press('Escape'); await page.getByRole('dialog', { name: 'Paused' }).getByRole('button', { name: 'Main menu' }).click(); await page.locator('.main-menu-screen').waitFor(); const afterExit = await page.evaluate(async () => { const { useDungeonRaceStore } = await import('/src/app/dungeonRaceStore.ts'); const race = useDungeonRaceStore.getState(); return { mode: race.mode, inGame: race.inGame, matchId: race.matchId }; }); assert.deepEqual(afterExit, { mode: null, inGame: false, matchId: null }); await page.getByRole('button', { name: /Dungeon Race/ }).click(); await page.getByRole('button', { name: 'Start practice' }).click(); await page.locator('.game-shell').waitFor({ timeout: 120_000 }); await page.locator('.dungeon-race-hud').waitFor({ timeout: 120_000 }); assert.deepEqual(errors, []); console.log(JSON.stringify({ ok: true, state, card, afterExit, screenshots: out }, null, 2)); } catch (error) { await page.screenshot({ path: path.join(out, 'failure.png') }); console.error((await page.locator('body').innerText()).slice(0, 5000)); throw error; } finally { await browser.close(); } })().catch((error) => { console.error(error); process.exitCode = 1; });