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 os = require('node:os'); const path = require('node:path'); const directory = path.resolve(process.env.BALANCE_LAB_PLAYTEST_DIR || 'playtest-artifacts/balance-lab'); const port = Number(process.env.BALANCE_LAB_PLAYTEST_PORT || 4191); const base = `http://127.0.0.1:${port}`; fs.mkdirSync(directory, { recursive: true }); const runtimeData = fs.mkdtempSync(path.join(os.tmpdir(), 'healer-man-balance-ui-')); (async () => { const { createGameServer } = await import('../server/server.mjs'); const runtime = createGameServer({ dataDir: runtimeData, enableShelvedModes: false, pythonExecutable: process.env.BALANCE_LAB_PYTHON, }); await new Promise((resolve) => runtime.server.listen(port, '127.0.0.1', resolve)); const browser = await chromium.launch({ channel: 'msedge', headless: true }); const context = await browser.newContext({ viewport: { width: 1440, height: 1000 } }); const page = await context.newPage(); page.setDefaultTimeout(90_000); const pageErrors = []; page.on('pageerror', (error) => pageErrors.push(error.message)); const capture = async (name) => { await page.screenshot({ path: path.join(directory, `${name}.png`), fullPage: true }); console.log(`Captured ${name}`); }; try { await page.goto(base, { waitUntil: 'domcontentloaded' }); await page.getByLabel('Username').fill('Phenom'); await page.getByLabel('Password').fill('balance-lab-test'); await page.getByRole('button', { name: /Create account/ }).click(); await page.locator('.character-select-screen').waitFor(); await page.getByRole('button', { name: /Create character/ }).click(); await page.getByRole('button', { name: /Customize & name/ }).click(); await page.getByLabel('Character name').fill('Balancer'); await page.getByRole('button', { name: /Create character/ }).click(); await page.getByRole('button', { name: /Continue/ }).first().click(); await page.locator('.main-menu-screen').waitFor(); const gmTools = page.getByRole('button', { name: /GM Tools/ }); const balanceLab = page.getByRole('button', { name: /Balance Lab/ }); await gmTools.waitFor(); await balanceLab.waitFor(); const gmBox = await gmTools.boundingBox(); const labBox = await balanceLab.boundingBox(); assert(gmBox && labBox && labBox.y > gmBox.y, 'Balance Lab is placed underneath GM Tools'); await capture('01-gm-menu'); await balanceLab.click(); await page.locator('.balance-lab-screen').waitFor(); await page.getByLabel('Party source').selectOption('wow'); await page.getByLabel('Level').fill('20'); await page.getByLabel('Parties per source').fill('1'); await page.getByLabel('Runs per party/boss').fill('1'); await page.getByLabel('Fight limit (seconds)').fill('5'); await page.getByLabel('Bosses').selectOption('specific'); await page.getByLabel('Search Balance Lab bosses').fill('Eck the Ferocious'); await page.getByRole('checkbox', { name: /Eck the Ferocious/ }).check(); const totalSimulations = page.locator('.balance-lab-estimate span').filter({ hasText: 'Total simulations' }).locator('b'); await totalSimulations.waitFor(); assert.equal(await totalSimulations.innerText(), '1'); await capture('02-server-job-config'); await page.getByRole('button', { name: 'Run on server' }).click(); await page.locator('.balance-lab-job-list em.is-complete').waitFor({ timeout: 240_000 }); await page.getByRole('button', { name: 'Open full report' }).waitFor(); assert.equal(await page.locator('.balance-lab-artifacts > button').count(), 12); await capture('03-server-job-complete'); await page.getByRole('button', { name: 'View graphs and filterable data' }).click(); await page.locator('.balance-results').waitFor(); assert.equal(await page.locator('.balance-results-chart').count(), 5); assert.match(await page.locator('.balance-results-chart').filter({ hasText: 'Boss damage to party' }).innerText(), /Eck the Ferocious/); await capture('04-run-analytics'); await page.getByRole('button', { name: 'bosses', exact: true }).click(); assert.equal(await page.locator('.balance-results-table-panel tbody tr').count(), 1); assert.match(await page.locator('.balance-results-table-panel tbody').innerText(), /Eck the Ferocious/); await page.getByRole('button', { name: 'Class / spec', exact: true }).click(); const allClassRows = await page.locator('.balance-results-table-panel tbody tr').count(); assert(allClassRows >= 5, 'the complete five-member party should be present in the class/spec table'); await page.getByLabel('Sort class specs').selectOption('totalDamageTaken'); assert.equal(await page.getByLabel('Sort class specs').inputValue(), 'totalDamageTaken'); await page.getByLabel('Class role').selectOption('damage'); const damageRows = await page.locator('.balance-results-table-panel tbody tr').count(); assert(damageRows > 0 && damageRows < allClassRows, 'role filtering should narrow the complete class/spec table'); await page.getByLabel('Class role').selectOption('all'); await page.getByRole('button', { name: 'abilities', exact: true }).click(); const abilityRows = page.locator('.balance-results-abilities tbody tr'); assert(await abilityRows.count() > 0, 'ability breakdown should retain observed and available abilities'); await page.getByLabel('Filter ability usage').selectOption('unused'); assert(await abilityRows.count() > 0, 'available but unused abilities should remain visible'); await page.getByLabel('Filter ability usage').selectOption('all'); await page.getByLabel('Sort abilities').selectOption('effectiveDamage'); await capture('04b-ability-breakdown'); await page.getByRole('button', { name: 'parties', exact: true }).click(); assert.equal(await page.locator('.balance-results-table-panel tbody tr').count(), 1); await page.getByLabel('Search Balance Lab run history').fill('does-not-match'); await page.getByText('No saved runs match these filters.').waitFor(); await page.getByLabel('Search Balance Lab run history').fill('wow'); assert.equal(await page.locator('.balance-lab-job-list > button').count(), 1); await page.setViewportSize({ width: 480, height: 900 }); await page.waitForTimeout(250); assert.equal(await page.evaluate(() => document.documentElement.scrollWidth <= window.innerWidth), true); await page.locator('#balance-lab-analysis').scrollIntoViewIfNeeded(); await page.getByRole('button', { name: 'overview', exact: true }).click(); await page.getByRole('heading', { name: 'Boss damage to party' }).waitFor(); await page.getByRole('button', { name: 'Main menu' }).waitFor(); await capture('05-mobile-analytics'); await page.getByRole('button', { name: 'abilities', exact: true }).click(); await page.getByText('Every class/spec ability').scrollIntoViewIfNeeded(); assert.equal(await page.evaluate(() => document.documentElement.scrollWidth <= window.innerWidth), true); await capture('06-mobile-abilities'); assert.deepEqual(pageErrors, []); console.log('Balance Lab GM playtest passed'); } finally { await browser.close(); await new Promise((resolve) => runtime.server.close(resolve)); fs.rmSync(runtimeData, { recursive: true, force: true }); } })().catch((error) => { console.error(error); process.exitCode = 1; });