Release Healer Man 0.1.43

This commit is contained in:
phenom committed 2026-09-14 13:20:20 -04:00
1 parent c7e8163790
commit ccc01396cd
86 files changed
+2951 -198

No files matched your search

+168
View File
@@ -0,0 +1,168 @@
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 directory = path.resolve('playtest-artifacts/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}`);
};
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) => options.find((entry) => /Lady Anacondra.*Wailing Caverns/.test(entry.textContent))?.value);
assert(option, 'Lady Anacondra 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);
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 };
});
assert.equal(before.count, 1, 'Only the selected boss spawns initially');
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.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('Lady Anacondra');
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, initialMobCount: before.count, 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; });
+249
View File
@@ -0,0 +1,249 @@
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 directory = path.resolve('playtest-artifacts/thor-ui');
fs.mkdirSync(directory, { recursive: true });
const base = process.env.THOR_TEST_URL || 'http://127.0.0.1:5176';
const results = [];
(async () => {
console.log('Launching Thor UI browser');
const browser = await chromium.launch({ channel: 'msedge', headless: true });
console.log('Browser launched');
const context = await browser.newContext({ hasTouch: true, userAgent: 'Mozilla/5.0 (Linux; Android 13; AYN Thor) AppleWebKit/537.36 Chrome/130.0.0.0 Mobile Safari/537.36' });
await context.addInitScript(() => performance.setResourceTimingBufferSize(20000));
const top = await context.newPage();
const bottom = await context.newPage();
console.log('Two pages ready');
const errors = [];
for (const [page, width, height, diagonal] of [[top, 1920, 1080, 6], [bottom, 1240, 1080, 3.92]]) {
const ppi = Math.hypot(width, height) / diagonal;
const viewport = { width: Math.round(width / ppi * 160), height: Math.round(height / ppi * 160) };
await page.setViewportSize(viewport);
const cdp = await context.newCDPSession(page);
await cdp.send('Emulation.setDeviceMetricsOverride', { ...viewport, deviceScaleFactor: ppi / 160, mobile: false });
console.log('Configured panel', viewport);
page.setDefaultTimeout(60000);
page.on('pageerror', (error) => errors.push(error.message));
}
const capture = async (name) => {
for (const [surface, page] of [['top', top], ['bottom', bottom]]) {
await page.screenshot({ path: path.join(directory, `${name}-${surface}.png`) });
const overflow = await page.evaluate(() => {
const root = document.querySelector('#root');
const buttons = [...document.querySelectorAll('button')].filter((node) => node.getBoundingClientRect().width && getComputedStyle(node).visibility !== 'hidden');
const clipped = buttons.filter((node) => {
const rect = node.getBoundingClientRect();
if (rect.right <= innerWidth + 1 && rect.left >= -1 && rect.bottom <= innerHeight + 1 && rect.top >= -1) return false;
for (let parent = node.parentElement; parent && parent !== root; parent = parent.parentElement) {
const css = getComputedStyle(parent);
if (/(auto|scroll)/.test(css.overflow + css.overflowY + css.overflowX)) return false;
}
return true;
});
return { width: innerWidth, height: innerHeight, clipped: clipped.map((node) => node.textContent.trim().slice(0, 60)) };
});
results.push({ name, surface, ...overflow });
}
console.log(`Captured ${name}`);
};
try {
console.log('Opening top');
await top.goto(`${base}/?display=top&nativeLayout=1`, { waitUntil: 'domcontentloaded' });
console.log('Top DOM ready');
await top.getByLabel('Username', { exact: true }).waitFor();
await bottom.goto(`${base}/?display=bottom&nativeLayout=1`, { waitUntil: 'domcontentloaded' });
await bottom.locator('.companion-panel').waitFor();
await capture('01-login');
await top.getByRole('button', { name: /Play offline/ }).click();
await top.locator('.character-select-screen').waitFor();
await capture('02-roster-empty');
await top.getByRole('button', { name: /Create character/ }).click();
await top.locator('.character-create-screen').waitFor();
await capture('03-character-create');
await top.getByRole('button', { name: /Customize & name/ }).tap();
await top.getByLabel('Character name').fill('Thortester');
await capture('03b-appearance');
await top.getByRole('button', { name: /Create character/ }).tap();
await top.locator('.character-select-screen').waitFor();
await capture('04-roster');
await top.bringToFront();
await top.waitForTimeout(2000);
await capture('04b-roster-rendered');
await top.getByRole('button', { name: /Continue|Enter world/ }).first().click();
await top.locator('.main-menu-screen').waitFor();
await capture('05-main-menu');
await top.getByRole('button', { name: /Class Hall/ }).tap();
await top.locator('.class-hall-screen').waitFor();
await capture('05b-class-hall');
await top.locator('.class-hall-header button').tap();
for (const name of ['Talents', 'Keybinds', 'Button assignments']) {
await top.getByRole('button', { name: new RegExp(name) }).tap();
await top.locator('.main-menu-character-editor').waitFor();
await top.locator('.main-menu-character-editor .game-panel').waitFor();
await capture(`05-editor-${name.toLowerCase().replaceAll(' ', '-')}`);
await top.getByRole('button', { name: 'Back to main menu', exact: true }).tap();
}
await top.getByRole('button', { name: /View Reports/ }).tap();
await top.getByRole('heading', { name: 'View Reports' }).waitFor();
await capture('05c-reports');
await top.getByRole('button', { name: /Back/ }).first().tap();
await top.getByRole('button', { name: /Downloads/ }).tap();
await top.locator('.content-manager-screen').waitFor();
await capture('05d-downloads');
await top.getByRole('button', { name: 'Back', exact: true }).tap();
await top.getByRole('button', { name: /^Dungeons/ }).click();
await top.locator('.dungeon-select-screen').waitFor();
await capture('06-dungeon-picker');
await top.getByRole('button', { name: /Choose role/ }).click();
await capture('07-role-picker');
await top.locator('.dungeon-role-option').first().click();
await top.locator('.game-shell').waitFor();
await bottom.locator('.companion-hud-shell').waitFor();
await top.waitForFunction(async () => { const { useGameStore } = await import(performance.getEntriesByType('resource').findLast((entry) => new URL(entry.name).pathname === '/src/game/store.ts').name); return ['ready', 'proxy', 'error'].includes(useGameStore.getState().assetStatus); }, null, { timeout: 120000 });
await top.locator('.loading-screen').waitFor({ state: 'hidden', timeout: 120000 });
await top.waitForTimeout(1500);
await capture('08-dungeon');
assert.equal(await bottom.locator('[data-hud-element="party"]').count(), 0);
assert.equal(await top.locator('[data-hud-element="actions"]').count(), 0);
assert.equal(await top.locator('[data-hud-element="player"]').count(), 0);
assert.equal(await bottom.locator('.action-bar').count(), 1);
assert.equal(await bottom.locator('.player-status-panel').count(), 1);
await bottom.locator('.aura--inspectable').first().tap();
await bottom.getByRole('tooltip').waitFor();
await capture('08b-buff-inspection');
await bottom.getByRole('button', { name: /^Fade,/ }).tap();
await top.waitForFunction(async () => {
const { useCombatStore } = await import(performance.getEntriesByType('resource').findLast((entry) => new URL(entry.name).pathname === '/src/game/combatStore.ts').name);
return Object.values(useCombatStore.getState().cooldowns).some((endsAt) => endsAt > Date.now());
});
for (const overlay of ['pause', 'map', 'party', 'boss-guide', 'spellbook', 'talents', 'bindings', 'inventory', 'boss-cards', 'options', 'keybinds', 'report-issue']) {
await top.evaluate(async (overlay) => { const { useGameStore } = await import(performance.getEntriesByType('resource').findLast((entry) => new URL(entry.name).pathname === '/src/game/store.ts').name); useGameStore.getState().openOverlay(overlay); }, overlay);
await top.waitForTimeout(500);
await capture(`09-${overlay}`);
}
await bottom.getByRole('button', { name: 'Edit UI', exact: true }).tap();
await top.locator('.ui-layout-editor').waitFor();
assert.equal(await top.getByLabel('Party frames screen').inputValue(), 'top');
assert.equal(await top.getByLabel('Buffs and debuffs screen').inputValue(), 'bottom');
await top.getByLabel('Party frames screen').selectOption('bottom');
await bottom.getByRole('button', { name: 'Move Party frames', exact: true }).waitFor();
assert.equal(await top.locator('[data-hud-element="party"]').count(), 0);
await capture('10-editor-party-bottom');
const handle = bottom.getByRole('button', { name: 'Move Party frames', exact: true });
const rect = await handle.boundingBox();
const cdp = await context.newCDPSession(bottom);
await cdp.send('Input.dispatchTouchEvent', { type: 'touchStart', touchPoints: [{ x: rect.x + 10, y: rect.y + 10 }] });
await cdp.send('Input.dispatchTouchEvent', { type: 'touchMove', touchPoints: [{ x: rect.x + 90, y: rect.y + 60 }] });
await cdp.send('Input.dispatchTouchEvent', { type: 'touchEnd', touchPoints: [] });
await top.waitForFunction(async () => {
const { useUiLayoutStore } = await import(performance.getEntriesByType('resource').findLast((entry) => new URL(entry.name).pathname === '/src/game/uiLayoutStore.ts').name);
return useUiLayoutStore.getState().draft.elements.party.position !== null;
});
await top.getByLabel('Party frames screen').selectOption('hidden');
await bottom.waitForFunction(() => !document.querySelector('[data-hud-element="party"]'));
await top.getByLabel('Party frames screen').selectOption('bottom');
await top.getByRole('button', { name: 'Cancel edits', exact: true }).tap();
await top.locator('.ui-layout-editor').waitFor({ state: 'hidden' });
await bottom.waitForFunction(() => !document.querySelector('[data-hud-element="party"]'));
await bottom.getByRole('button', { name: 'Edit UI', exact: true }).tap();
await top.getByLabel('Party frames screen').selectOption('bottom');
await capture('11-editor');
await top.getByRole('button', { name: 'Save UI', exact: true }).tap();
await top.waitForFunction(() => !document.querySelector('.ui-layout-editor'));
await capture('12-saved');
await top.reload({ waitUntil: 'domcontentloaded' });
await top.locator('.character-select-screen').waitFor();
await top.getByRole('button', { name: /Continue/ }).tap();
await top.getByRole('button', { name: /^Dungeons/ }).tap();
await top.getByRole('button', { name: /Choose role/ }).tap();
await top.locator('.dungeon-role-option').first().tap();
await bottom.locator('[data-hud-element="party"] .party-frame').first().waitFor();
assert.equal(await top.locator('[data-hud-element="party"]').count(), 0);
await bottom.locator('.party-frame--player').tap();
await top.waitForFunction(async () => {
const { usePartyStore } = await import(performance.getEntriesByType('resource').findLast((entry) => new URL(entry.name).pathname === '/src/game/partyStore.ts').name);
return usePartyStore.getState().selfSelected;
});
const partyBounds = await bottom.locator('.party-frames').boundingBox();
const menuBounds = await bottom.locator('.companion-hud-menus').boundingBox();
assert.ok(partyBounds.y + partyBounds.height <= menuBounds.y, 'Relocated party frames must fit above the bottom navigation');
await capture('13-persisted-party-bottom');
await bottom.getByRole('button', { name: 'Edit UI', exact: true }).tap();
await top.getByRole('button', { name: 'Reset all elements', exact: true }).tap();
await top.getByRole('button', { name: 'Save UI', exact: true }).tap();
await capture('14-final-defaults');
await top.locator('.loading-screen').waitFor({ state: 'hidden', timeout: 120000 });
await top.evaluate(async () => {
const load = (pathname) => import(performance.getEntriesByType('resource').findLast((entry) => new URL(entry.name).pathname === pathname).name);
const { useGameStore } = await load('/src/game/store.ts');
const { useCombatStore } = await load('/src/game/combatStore.ts');
useGameStore.setState({ paused: true, overlay: null });
useCombatStore.setState({ health: 0 });
});
await top.locator('.defeated-prompt').waitFor();
assert.equal(await bottom.locator('.defeated-prompt').count(), 0);
await capture('14b-defeated');
await top.getByRole('button', { name: 'Respawn at entrance', exact: true }).tap();
await top.locator('.defeated-prompt').waitFor({ state: 'hidden' });
// Isolated offline presentation fixture: exercise the completed-run UI without a live boss clear.
await top.evaluate(async () => {
const load = (pathname) => import(performance.getEntriesByType('resource').findLast((entry) => new URL(entry.name).pathname === pathname).name);
const { useGameStore } = await load('/src/game/store.ts');
const { useDungeonRunStore } = await load('/src/game/dungeonRunStore.ts');
const { usePartyStore } = await load('/src/game/partyStore.ts');
const { useCombatStore } = await load('/src/game/combatStore.ts');
const progress = useDungeonRunStore.getState().progress;
const now = performance.now();
const party = [{ name: 'Thortester', classId: useCombatStore.getState().classId, level: 1, role: 'healer', npc: false },
...usePartyStore.getState().members.map((member) => ({ name: member.name, classId: member.classId, level: member.level, role: member.role, npc: true }))];
useGameStore.setState({ paused: true, overlay: null });
useDungeonRunStore.setState({ progress: { ...progress, startedAt: now - 60000, finishedAt: now, durationMs: 60000, party } });
});
await top.locator('.dungeon-complete-banner').waitFor();
assert.equal(await bottom.locator('.dungeon-complete-banner').count(), 0);
await capture('14c-complete');
await top.locator('.dungeon-results').waitFor();
await capture('14d-results');
await top.getByRole('button', { name: /Stay and loot/ }).tap();
await top.getByRole('button', { name: 'Run results', exact: true }).tap();
await top.locator('.dungeon-results').waitFor();
await top.getByRole('button', { name: /Stay and loot/ }).tap();
await bottom.getByRole('button', { name: 'Edit UI', exact: true }).tap();
await top.getByLabel('Dungeon completion screen').selectOption('bottom');
await top.getByRole('button', { name: 'Save UI', exact: true }).tap();
await bottom.locator('.dungeon-complete-banner').waitFor();
assert.equal(await top.locator('.dungeon-complete-banner').count(), 0);
await capture('14e-complete-bottom');
await bottom.getByRole('button', { name: 'Finish dungeon', exact: true }).tap();
await top.locator('.main-menu-screen').waitFor();
await top.evaluate(async () => {
const { useUiLayoutStore } = await import(performance.getEntriesByType('resource').findLast((entry) => new URL(entry.name).pathname === '/src/game/uiLayoutStore.ts').name);
const state = useUiLayoutStore.getState(); state.begin(); state.updateDraft((draft) => ({ ...draft, elements: { ...draft.elements, completion: { visible: true, position: null, width: null } } })); state.finish(true);
});
const previewCdp = await context.newCDPSession(top);
await top.setViewportSize({ width: 1200, height: 1100 });
await previewCdp.send('Emulation.setDeviceMetricsOverride', { width: 1200, height: 1100, deviceScaleFactor: 1, mobile: false });
await top.goto(`${base}/?layout=thor-preview`, { waitUntil: 'domcontentloaded' });
await top.getByRole('button', { name: /Continue/ }).first().tap();
await top.locator('.thor-preview-top').getByRole('button', { name: /^Dungeons/ }).tap();
await top.locator('.thor-preview-top').getByRole('button', { name: /Choose role/ }).tap();
await top.locator('.thor-preview-top .dungeon-role-option').first().tap();
await top.locator('.thor-preview-top .loading-screen').waitFor({ state: 'hidden', timeout: 120000 });
await top.waitForTimeout(1000);
assert.equal((await top.locator('.thor-preview-top').boundingBox()).width, 837);
assert.equal((await top.locator('.thor-preview-bottom').boundingBox()).width, 473);
assert.equal(await top.locator('.thor-preview-bottom [data-hud-element="party"]').count(), 0);
assert.equal(await top.locator('.thor-preview-top .action-bar').count(), 0);
await top.screenshot({ path: path.join(directory, '15-combined-preview.png'), fullPage: true });
results.push({ name: '15-combined-preview', surface: 'both', clipped: [] });
assert.deepEqual(results.filter((result) => result.clipped.length), [], 'Controls must fit each screen or an intentional scroll region');
assert.equal(errors.length, 0, errors.join('\n'));
fs.writeFileSync(path.join(directory, 'results.json'), `${JSON.stringify({ results, errors }, null, 2)}\n`);
} finally {
console.log('Browser errors', errors);
await top.screenshot({ path: path.join(directory, 'last-top.png') });
fs.writeFileSync(path.join(directory, 'results.json'), `${JSON.stringify({ results, errors }, null, 2)}\n`);
await browser.close();
}
})().catch((error) => { console.error(error); process.exitCode = 1; });