forked from phenom/healer-man
243 lines
14 KiB
JavaScript
243 lines
14 KiB
JavaScript
const { DEFAULT_BOSS_TEST_BOSS } = require('./playtest-defaults.cjs');
|
|
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 crypto = require('node:crypto');
|
|
const selectedBossName = process.env.BOSS_TEST_BOSS || DEFAULT_BOSS_TEST_BOSS;
|
|
const historicalWailingBosses = [
|
|
'Lady Anacondra', 'Lord Cobrahn', 'Kresh', 'Lord Pythas', 'Skum',
|
|
'Lord Serpentis', 'Verdan the Everliving', 'Mutanus the Devourer',
|
|
];
|
|
if (historicalWailingBosses.includes(selectedBossName) && !process.argv.includes('--historical-wailing')) {
|
|
throw new Error('A Wailing boss override requires an explicit owner request and --historical-wailing. Isolated Boss Testing defaults to Hamhock.');
|
|
}
|
|
const directory = path.resolve(process.env.BOSS_TEST_ARTIFACT_DIR || path.join(
|
|
'playtest-artifacts',
|
|
selectedBossName === 'Celebras the Cursed' ? 'boss-testing-scaling' : 'boss-testing',
|
|
));
|
|
fs.mkdirSync(directory, { recursive: true });
|
|
const base = process.env.BOSS_TEST_URL || 'http://127.0.0.1:4187';
|
|
(async () => {
|
|
const { createGameServer } = await import('../server/server.mjs');
|
|
const runtime = createGameServer({ dataDir: path.join(directory, 'test-database'), enableShelvedModes: false });
|
|
await new Promise((resolve) => runtime.server.listen(4187, '127.0.0.1', resolve));
|
|
console.log('Started isolated production test server');
|
|
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(60000);
|
|
const errors = [];
|
|
const assetFiles = fs.readdirSync(path.resolve('dist/assets'));
|
|
const mainModule = `/assets/${assetFiles.find((file) => /^index-.*\.js$/.test(file))}`;
|
|
const combatModule = `/assets/${assetFiles.find((file) => /^combatStore-.*\.js$/.test(file))}`;
|
|
await page.addInitScript(({ mainModule, combatModule }) => {
|
|
window.bossTestingStores = async () => {
|
|
const main = Object.values(await import(mainModule));
|
|
const combat = Object.values(await import(combatModule));
|
|
return {
|
|
useGameStore: main.find((value) => value?.getState && 'bossTest' in value.getState()),
|
|
useShellStore: main.find((value) => value?.getState && 'characters' in value.getState()),
|
|
useCombatStore: combat.find((value) => value?.getState && 'damageMob' in value.getState()),
|
|
};
|
|
};
|
|
}, { mainModule, combatModule });
|
|
page.on('pageerror', (error) => errors.push(error.message));
|
|
const capture = async (name) => {
|
|
await page.screenshot({ path: path.join(directory, `${name}.png`) });
|
|
console.log(`Captured ${name}`);
|
|
};
|
|
const canvasHash = async () => {
|
|
const canvas = page.locator('canvas');
|
|
await canvas.evaluate((element) => {
|
|
const hidden = [];
|
|
let current = element;
|
|
while (current.parentElement) {
|
|
for (const sibling of current.parentElement.children) {
|
|
if (sibling === current || !(sibling instanceof HTMLElement)) continue;
|
|
hidden.push([sibling, sibling.style.visibility]);
|
|
sibling.style.visibility = 'hidden';
|
|
}
|
|
current = current.parentElement;
|
|
}
|
|
window.__restoreCanvasCapture = () => {
|
|
for (const [node, visibility] of hidden) node.style.visibility = visibility;
|
|
delete window.__restoreCanvasCapture;
|
|
};
|
|
});
|
|
try {
|
|
return crypto.createHash('sha256').update(await canvas.screenshot()).digest('hex');
|
|
} finally {
|
|
await page.evaluate(() => window.__restoreCanvasCapture?.());
|
|
}
|
|
};
|
|
try {
|
|
await page.goto(base, { waitUntil: 'domcontentloaded' });
|
|
console.log('Production menu loaded');
|
|
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('Bosstester');
|
|
await page.getByRole('button', { name: /Create character/ }).click();
|
|
await page.getByRole('button', { name: /Continue|Enter world/ }).first().click();
|
|
await page.locator('.main-menu-screen').waitFor();
|
|
await capture('01-main-menu');
|
|
await page.getByRole('button', { name: /Boss Testing/ }).click();
|
|
await page.getByRole('heading', { name: 'Boss Testing', exact: true }).waitFor();
|
|
const picker = page.getByLabel(/Choose a boss/);
|
|
const option = await picker.locator('option').evaluateAll((options, name) => options.find((entry) => entry.textContent.startsWith(`${name} ·`))?.value, selectedBossName);
|
|
assert(option, `${selectedBossName} is selectable`);
|
|
await picker.selectOption(option);
|
|
await page.getByRole('button', { name: 'Test selected boss', exact: true }).waitFor({ state: 'visible' });
|
|
await capture('02-boss-selection');
|
|
await page.getByRole('button', { name: 'Test selected boss', exact: true }).click();
|
|
if (await page.getByRole('alert').count()) throw new Error(await page.getByRole('alert').innerText());
|
|
await page.locator('canvas').waitFor();
|
|
await page.waitForFunction(async () => {
|
|
const { useGameStore, useCombatStore } = await window.bossTestingStores();
|
|
const game = useGameStore.getState();
|
|
return game.assetStatus === 'ready' && game.bossTest?.runtimeIds.every((id) => useCombatStore.getState().mobs[id]);
|
|
}, null, { timeout: 90000 });
|
|
await page.waitForTimeout(4000);
|
|
const bossFrameBefore = await canvasHash();
|
|
await page.waitForTimeout(500);
|
|
assert.notEqual(
|
|
await canvasHash(),
|
|
bossFrameBefore,
|
|
'Boss Testing redraws the WebGL scene independently from its HUD and nameplates',
|
|
);
|
|
await capture('03-empty-room');
|
|
const before = await page.evaluate(async () => {
|
|
const { useGameStore, useCombatStore, useShellStore } = await window.bossTestingStores();
|
|
const game = useGameStore.getState();
|
|
const combat = useCombatStore.getState();
|
|
const boss = game.bossTest;
|
|
return { boss, xp: combat.xp, saved: JSON.stringify(useShellStore.getState().characters), count: Object.keys(combat.mobs).length,
|
|
classId: combat.classId, level: combat.level, bossLevel: combat.mobs[boss.runtimeIds[0]].level,
|
|
bossMaxHealth: combat.mobs[boss.runtimeIds[0]].maxHealth };
|
|
});
|
|
assert.equal(before.count, 1, 'Only the selected boss spawns initially');
|
|
if (selectedBossName === 'Celebras the Cursed') {
|
|
assert.equal(before.classId, 'priest');
|
|
assert.equal(before.level, 1);
|
|
assert.equal(before.bossLevel, 2);
|
|
assert.equal(before.bossMaxHealth, 575, 'Level-one Celebras uses scaled native health without campaign floors');
|
|
}
|
|
await page.keyboard.press('Escape');
|
|
await page.getByRole('button', { name: 'Boss readiness & report', exact: true }).click();
|
|
await page.getByRole('button', { name: 'Mark not ready', exact: true }).click();
|
|
await page.getByRole('status').filter({ hasText: 'Marked not ready.' }).waitFor();
|
|
await page.getByRole('button', { name: 'Mark needs testing', exact: true }).click();
|
|
await page.getByRole('status').filter({ hasText: 'Marked needs testing.' }).waitFor();
|
|
await capture('04-pause-review');
|
|
await page.getByRole('button', { name: 'Submit a boss report', exact: true }).click();
|
|
await page.getByLabel('Describe the issue', { exact: true }).fill('Boss Testing playtest: verify mechanic telegraphs.');
|
|
await page.getByRole('button', { name: 'Submit report', exact: true }).click();
|
|
await page.getByText('Report submitted.', { exact: false }).waitFor();
|
|
await capture('05-boss-report');
|
|
await page.getByRole('button', { name: 'Back to boss review', exact: true }).click();
|
|
await page.getByRole('button', { name: 'Fight this boss again', exact: true }).click();
|
|
await page.waitForTimeout(1000);
|
|
await page.evaluate(async () => {
|
|
const { useGameStore, useCombatStore } = await window.bossTestingStores();
|
|
const game = useGameStore.getState();
|
|
const combat = useCombatStore.getState();
|
|
combat.selectMob(game.bossTest.runtimeIds[0]);
|
|
combat.damageMob(game.bossTest.runtimeIds[0], 1000000, Date.now());
|
|
});
|
|
await page.getByText('Boss defeated', { exact: true }).waitFor();
|
|
await page.getByRole('button', { name: 'Mark ready', exact: true }).click();
|
|
await page.getByRole('status').filter({ hasText: 'Marked ready.' }).waitFor();
|
|
await capture('06-victory-review');
|
|
const after = await page.evaluate(async () => {
|
|
const { useCombatStore, useShellStore } = await window.bossTestingStores();
|
|
return { xp: useCombatStore.getState().xp, saved: JSON.stringify(useShellStore.getState().characters) };
|
|
});
|
|
assert.equal(after.xp, before.xp);
|
|
assert.equal(after.saved, before.saved);
|
|
const reports = runtime.database.listDungeonReports().reports;
|
|
assert.equal(reports[0].bossKey, before.boss.key);
|
|
assert.equal(runtime.database.listBossTestingReviews().reviews[0].status, 'ready');
|
|
await page.evaluate(async (previousKey) => {
|
|
window.previousBossTestingKey = previousKey;
|
|
window.liveBossTestingStores = await window.bossTestingStores();
|
|
}, before.boss.key);
|
|
await page.getByRole('button', { name: 'Fight another random boss', exact: true }).click();
|
|
await page.waitForFunction(() => {
|
|
const { useGameStore, useCombatStore } = window.liveBossTestingStores;
|
|
const game = useGameStore.getState();
|
|
return game.assetStatus === 'ready' && game.bossTest?.key !== window.previousBossTestingKey
|
|
&& game.bossTest?.runtimeIds.every((id) => useCombatStore.getState().mobs[id]);
|
|
}, null, { timeout: 120000 });
|
|
const nextBoss = await page.evaluate(() => {
|
|
const { useGameStore } = window.liveBossTestingStores;
|
|
return useGameStore.getState().bossTest;
|
|
});
|
|
assert.notEqual(nextBoss.key, before.boss.key, 'Post-fight random selection must choose another boss');
|
|
await page.waitForTimeout(1000);
|
|
await capture('06b-another-random-boss');
|
|
await page.keyboard.press('Escape');
|
|
await page.getByRole('button', { name: 'Return to main menu', exact: true }).click();
|
|
await page.locator('.main-menu-screen').waitFor();
|
|
await page.getByRole('button', { name: /Boss Testing/ }).click();
|
|
await picker.selectOption(option);
|
|
await page.getByRole('article').getByText('Ready', { exact: true }).waitFor();
|
|
await page.setViewportSize({ width: 390, height: 844 });
|
|
await capture('07-mobile-selection');
|
|
assert(await page.evaluate(() => document.documentElement.scrollWidth <= innerWidth + 1), 'No mobile horizontal overflow');
|
|
await page.setViewportSize({ width: 1440, height: 1000 });
|
|
await page.getByLabel('Search bosses').fill(selectedBossName);
|
|
await page.getByRole('button', { name: 'Fight a random boss', exact: true }).click();
|
|
await page.locator('canvas').waitFor();
|
|
await page.waitForTimeout(3000);
|
|
await capture('08-random-fight');
|
|
await page.keyboard.press('Escape');
|
|
await page.getByRole('button', { name: 'Return to main menu', exact: true }).click();
|
|
await page.getByRole('button', { name: /Boss Testing/ }).click();
|
|
const missing = await picker.locator('option').evaluateAll((options) => options.find((entry) => /Taragaman the Hungerer.*Ragefire Chasm/.test(entry.textContent))?.value);
|
|
assert(missing, 'An uninstalled boss is selectable');
|
|
await picker.selectOption(missing);
|
|
await capture('09-automatic-download');
|
|
await page.waitForFunction(() => {
|
|
const button = [...document.querySelectorAll('button')].find((entry) => entry.textContent === 'Test selected boss');
|
|
const error = document.querySelector('[role="alert"]');
|
|
if (error) throw new Error(error.textContent);
|
|
return button && !button.disabled;
|
|
}, null, { timeout: 120000 });
|
|
const installed = await page.evaluate(() => JSON.parse(localStorage.getItem('healer-man.content-state.v2') || '{}'));
|
|
assert(installed.packs['dungeon:ragefire-chasm'], 'The selected boss pack downloads automatically');
|
|
await page.getByRole('button', { name: 'Test selected boss', exact: true }).click();
|
|
await page.waitForFunction(async () => {
|
|
const { useGameStore, useCombatStore } = await window.bossTestingStores();
|
|
const game = useGameStore.getState();
|
|
return game.assetStatus === 'ready' && game.bossTest?.runtimeIds.every((id) => useCombatStore.getState().mobs[id]);
|
|
}, null, { timeout: 90000 });
|
|
await page.waitForTimeout(1500);
|
|
await capture('10-downloaded-boss-room');
|
|
const bottom = await page.context().newPage();
|
|
bottom.on('pageerror', (error) => errors.push(error.message));
|
|
await bottom.setViewportSize({ width: 1240, height: 1080 });
|
|
await bottom.goto(`${base}/?display=bottom&nativeLayout=1`, { waitUntil: 'domcontentloaded' });
|
|
await bottom.getByRole('button', { name: 'Boss review', exact: true }).waitFor();
|
|
await bottom.screenshot({ path: path.join(directory, '11-bottom-display.png') });
|
|
await bottom.getByRole('button', { name: 'Boss review', exact: true }).click();
|
|
await page.getByRole('heading', { name: 'Taragaman the Hungerer', exact: true }).waitFor();
|
|
await capture('12-bottom-display-review');
|
|
await bottom.close();
|
|
assert.deepEqual(errors, [], 'No browser runtime errors');
|
|
fs.writeFileSync(path.join(directory, 'results.json'), `${JSON.stringify({ passed: true, boss: before.boss, classId: before.classId,
|
|
characterLevel: before.level, bossLevel: before.bossLevel, bossMaxHealth: before.bossMaxHealth,
|
|
initialMobCount: before.count, nextRandomBoss: nextBoss, xpUnchanged: true, profileUnchanged: true,
|
|
automaticDownload: 'dungeon:ragefire-chasm', report: reports[0], errors }, null, 2)}\n`);
|
|
console.log('Boss Testing live flow passed');
|
|
} catch (error) {
|
|
await capture('failure');
|
|
console.log((await page.locator('body').innerText()).slice(-2000));
|
|
console.log('Browser errors:', errors);
|
|
throw error;
|
|
} finally {
|
|
await browser.close();
|
|
await new Promise((resolve) => runtime.server.close(resolve));
|
|
}
|
|
})().catch((error) => { console.error(error); process.exitCode = 1; });
|