176 lines
11 KiB
JavaScript
176 lines
11 KiB
JavaScript
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.getByText('Server runtime ready').waitFor();
|
|
assert.match(await page.locator('.balance-lab-runtime').innerText(), /Python 3\./);
|
|
assert.match(await page.locator('.balance-lab-runtime').innerText(), /0\/4 workers active/);
|
|
await page.getByLabel('Party source').selectOption('wow');
|
|
await page.getByLabel('Level coverage').selectOption('brackets');
|
|
assert.equal(await page.getByLabel('Bracket spacing').inputValue(), '10');
|
|
assert.equal(await page.locator('.balance-lab-estimate span').filter({ hasText: 'Level jobs' }).locator('b').innerText(), '9');
|
|
await page.getByLabel('Bracket spacing').selectOption('5');
|
|
assert.equal(await page.locator('.balance-lab-estimate span').filter({ hasText: 'Level jobs' }).locator('b').innerText(), '17');
|
|
await page.getByLabel('Bracket spacing').selectOption('1');
|
|
assert.equal(await page.locator('.balance-lab-estimate span').filter({ hasText: 'Level jobs' }).locator('b').innerText(), '80');
|
|
await page.getByLabel('Bracket spacing').selectOption('10');
|
|
await capture('02-level-batch-config');
|
|
await page.getByLabel('Level coverage').selectOption('single');
|
|
await page.getByLabel('Level', { exact: true }).fill('20');
|
|
await page.getByLabel('Class/spec selection').selectOption('all');
|
|
assert(Number(await page.getByLabel('Coverage parties').inputValue()) > 1, 'all-spec coverage should derive multiple parties');
|
|
await page.getByLabel('Class/spec selection').selectOption('specific');
|
|
await page.getByLabel('Search Balance Lab class specs').fill('Paladin Holy');
|
|
await page.getByRole('checkbox', { name: /Paladin · Holy.*Holy Playerbots PvE/ }).check();
|
|
assert.equal(await page.getByLabel('Coverage parties').inputValue(), '1');
|
|
await page.getByLabel('Class/spec selection').selectOption('random');
|
|
await page.getByLabel('Parties per source').fill('10');
|
|
await page.getByText(/eligible pools provide 1 unique party configuration/i).waitFor();
|
|
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('03-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((await page.locator('.balance-lab-artifacts > button').count()) >= 12);
|
|
await capture('04-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(), 7);
|
|
assert.match(await page.locator('.balance-results-chart').filter({ hasText: 'Boss damage to party' }).innerText(), /Eck the Ferocious/);
|
|
await capture('05-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.equal(allClassRows, 4, 'the four NPC teammates should be present in the NPC-only 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');
|
|
const damageSpecRow = page.locator('.balance-results-table-panel tbody tr').filter({
|
|
has: page.locator('td:nth-child(3)').getByText('damage', { exact: true }),
|
|
}).first();
|
|
const damageSpecClass = await damageSpecRow.locator('td:first-child strong').innerText();
|
|
const damageSpecName = await damageSpecRow.locator('td:first-child small').innerText();
|
|
await damageSpecRow.getByRole('button', { name: `View ${damageSpecClass} ${damageSpecName} ability damage` }).click();
|
|
await page.getByRole('heading', { name: `${damageSpecClass} · ${damageSpecName} ability damage` }).waitFor();
|
|
assert.notEqual(await page.getByLabel('Filter ability class spec').inputValue(), 'all');
|
|
assert.equal(await page.getByLabel('Filter ability usage').inputValue(), 'damage');
|
|
const abilityRows = page.locator('.balance-results-abilities tbody tr');
|
|
assert(await abilityRows.count() > 0, 'spec drill-down should show abilities that dealt damage');
|
|
assert(Number((await abilityRows.first().locator('td:nth-child(12)').innerText()).replaceAll(',', '')) > 0, 'drilled ability rows should include effective damage');
|
|
const abilityHeaders = await page.locator('.balance-results-abilities thead').innerText();
|
|
assert.match(abilityHeaders, /CASTS/);
|
|
assert.match(abilityHeaders, /EFFECTIVE DAMAGE/);
|
|
assert.match(abilityHeaders, /DAMAGE SHARE/);
|
|
assert.match(abilityHeaders, /DPS/);
|
|
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('05b-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('06-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('07-mobile-abilities');
|
|
|
|
await page.setViewportSize({ width: 1440, height: 1000 });
|
|
await page.getByLabel('Level coverage').selectOption('brackets');
|
|
await page.getByLabel('Bracket spacing').selectOption('10');
|
|
assert.equal(await page.locator('.balance-lab-estimate span').filter({ hasText: 'Level jobs' }).locator('b').innerText(), '9');
|
|
await page.getByRole('button', { name: 'Queue 9 level jobs' }).click();
|
|
await page.waitForFunction(() => (
|
|
document.querySelectorAll('.balance-lab-job-list em.is-complete').length >= 10
|
|
), null, { timeout: 240_000 });
|
|
assert.equal(await page.locator('.balance-lab-job-list em.is-failed').count(), 0);
|
|
await capture('08-nine-bracket-jobs-complete');
|
|
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;
|
|
});
|