const { chromium } = require('C:/Users/phenom/.cache/codex-runtimes/codex-primary-runtime/dependencies/node/node_modules/playwright'); const assert = require('node:assert/strict'); const fs = require('node:fs'); const path = require('node:path'); const out = path.resolve('playtest-artifacts/dungeon-race-affixes'); fs.mkdirSync(out, { recursive: true }); const base = process.env.RACE_TEST_URL || 'http://127.0.0.1:5173'; const cases = [ ['volatile-remains', 'falling-stars', 'volatile', 'star'], ['last-stand', 'stormcall', 'last-stand', 'storm'], ['lingering-ash', 'arcane-pulse', 'ash', 'ring'], ['unstable-orb', 'predator-mark', 'orb', 'predator'], ['risky-boon', 'falling-stars', 'boon', 'star'], ]; (async () => { const browser = await chromium.launch({ channel: 'msedge', headless: true }); const errors = []; try { for (const [role, className, specialization] of [ ['Tank', 'Warrior', 'Protection'], ['Healer', 'Priest', 'Discipline'], ['Damage', 'Hunter', 'Marksmanship'], ].filter(([role]) => !process.env.RACE_TEST_ROLE || role === process.env.RACE_TEST_ROLE)) { const page = await browser.newPage({ viewport: { width: 1280, height: 800 } }); 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.locator('.class-grid .class-option').filter({ hasText: className }).first().click(); await page.locator('.creator-specializations button').filter({ hasText: specialization }).first().click(); await page.getByRole('button', { name: /Customize & name/ }).click(); await page.getByLabel('Character name').fill(`Affix${role}`); 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(); for (const pool of ['authored', 'randomized']) { await page.getByRole('button', { name: /Dungeon Race/ }).click(); await page.locator('.dungeon-race-screen').waitFor(); if (pool === 'randomized') { await page.getByRole('button', { name: 'Randomized Bite-Sized' }).click(); await page.locator('.dungeon-race-screen__setup select').first() .selectOption('randomized-bite-sized-stormwind-stockade-available'); } else { await page.locator('.dungeon-race-screen__setup select').first() .selectOption('bite-sized-stormwind-stockade'); } await page.getByRole('group', { name: 'Your role' }) .getByRole('button', { name: role === 'Damage' ? 'DPS' : role, exact: true }).click(); await page.getByRole('button', { name: 'Start practice' }).click(); 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 }); const boot = await page.evaluate(async () => { const { useDungeonRaceStore } = await import('/src/app/dungeonRaceStore.ts'); const { dungeonRaceAffix } = await import('/src/game/dungeonRaceAffixes.ts'); const race = useDungeonRaceStore.getState(); return { dungeonId: race.dungeonId, pair: race.affixIds, status: race.status, names: race.affixIds.map((id) => dungeonRaceAffix(id).name) }; }); assert.equal(boot.status, 'active'); assert.equal(boot.dungeonId.startsWith(pool === 'authored' ? 'bite-sized-' : 'randomized-bite-sized-'), true); assert.equal(boot.pair.length, 2); const timerText = await page.locator('.dungeon-race-hud').innerText(); assert.ok(timerText.includes('Mechanics:')); for (const name of boot.names) assert.ok(timerText.includes(name), `${pool}/${role}: timer omits ${name}`); for (const [killId, movementId, killKind, movementKind] of cases) { const observed = await page.evaluate(async ({ killId, movementId }) => { const { useDungeonRaceStore } = await import('/src/app/dungeonRaceStore.ts'); const { useDungeonRaceAffixStore, clearDungeonRaceAffixes, advanceDungeonRaceAffixes } = await import('/src/game/dungeonRaceAffixRuntime.ts'); const { useCombatStore } = await import('/src/game/combatStore.ts'); const { dungeonDefinitionById } = await import('/src/game/dungeonRegistry.ts'); const { setMobPosition } = await import('/src/game/mobRuntimeRegistry.ts'); const race = useDungeonRaceStore.getState(); const definition = dungeonDefinitionById(race.dungeonId); const trash = definition.staticSpawns.find((spawn) => !definition.bosses.some((boss) => boss.id === spawn.id)); if (!trash) throw new Error('No original trash spawn for affix playtest'); const now = Date.now(); const player = useCombatStore.getState().playerPosition; clearDungeonRaceAffixes(); useDungeonRaceStore.setState({ affixIds: [killId, movementId] }); advanceDungeonRaceAffixes(now, []); setMobPosition(trash.id, player); useCombatStore.getState().registerMob(trash.id, { name: 'Affix trigger', maxHealth: 100, boss: false }); const original = useCombatStore.getState().mobs[trash.id]; useCombatStore.setState((state) => ({ mobs: { ...state.mobs, [trash.id]: { ...original, dead: true, health: 0 } } })); advanceDungeonRaceAffixes(now + 100, [trash.id]); useCombatStore.setState((state) => ({ mobs: { ...state.mobs, [trash.id]: { ...state.mobs[trash.id], dead: false, health: 100 } } })); useDungeonRaceAffixStore.setState({ nextMovementAt: now + 101 }); advanceDungeonRaceAffixes(now + 101, []); return { kinds: useDungeonRaceAffixStore.getState().effects.map((effect) => effect.kind), pair: useDungeonRaceStore.getState().affixIds }; }, { killId, movementId }); if (killKind !== 'last-stand') assert.ok(observed.kinds.includes(killKind), `${pool}/${role}: ${killId}`); assert.ok(observed.kinds.includes(movementKind), `${pool}/${role}: ${movementId}`); assert.deepEqual(observed.pair, [killId, movementId]); await page.waitForTimeout(150); if (role === 'Healer' || (role === 'Damage' && pool === 'randomized')) { await page.screenshot({ path: path.join(out, `${pool}-${role.toLowerCase()}-${killId}-${movementId}.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(); } } catch (error) { await page.screenshot({ path: path.join(out, `failure-${role.toLowerCase()}.png`) }); console.error((await page.locator('body').innerText()).slice(0, 4000)); throw error; } finally { await page.close(); } } assert.deepEqual(errors, []); console.log(JSON.stringify({ ok: true, runs: process.env.RACE_TEST_ROLE ? 2 : 6, mechanicsPerRun: 9, screenshots: out })); } finally { await browser.close(); } })().catch((error) => { console.error(error); process.exitCode = 1; });