136 lines
8.4 KiB
JavaScript
136 lines
8.4 KiB
JavaScript
// Local menu regression: run with browser_run_code_unsafe(filename: "tools/check-menu.js").
|
|
// Uses the browser tool's Playwright runtime; no package or test code ships to the game.
|
|
async (hostPage) => {
|
|
const context = await hostPage.context().browser().newContext({
|
|
viewport: { width: 320, height: 568 }, isMobile: true, hasTouch: true,
|
|
});
|
|
const page = await context.newPage(), errors = [], checks = [];
|
|
page.on('pageerror', e => errors.push(e.message));
|
|
const check = (ok, label) => { if (!ok) throw new Error(label); checks.push(label); };
|
|
const click = selector => page.locator(selector).click();
|
|
const stage = () => page.locator('#intro').getAttribute('data-stage');
|
|
const quit = async () => { await click('#qChip'); await click('#qYes'); };
|
|
const fits = async selector => {
|
|
await page.waitForTimeout(650); // let the intentional menu entrance finish
|
|
return page.locator(selector).evaluate(e => {
|
|
const r = e.getBoundingClientRect();
|
|
return r.left >= 0 && r.right <= innerWidth && r.top >= 0 && r.bottom <= innerHeight;
|
|
});
|
|
};
|
|
try {
|
|
await page.goto('http://localhost:8641');
|
|
await page.waitForFunction(() => !document.querySelector('#splash').classList.contains('busy'));
|
|
for (const selector of ['#mStart', '#mGore', '#mOther', '#mVs', '#mRanks'])
|
|
check(await fits(selector), '320px splash fits ' + selector);
|
|
check(await page.evaluate(() => document.querySelector('#mStart').offsetHeight ===
|
|
document.querySelector('#mGore').offsetHeight), 'Cup and Gore share one height');
|
|
check(await page.locator('.splashDoors button').evaluateAll(bs => bs.every(b =>
|
|
getComputedStyle(b).textAlign === 'center' && getComputedStyle(b).justifyContent === 'center')),
|
|
'secondary door labels are centered');
|
|
check(await page.evaluate(() => {
|
|
const v = document.querySelector('#mVs'), g = document.querySelector('#mGore');
|
|
const drop = getComputedStyle(v, '::before');
|
|
const animations = document.getAnimations().filter(a => a.animationName?.startsWith('menuBlood'));
|
|
return Math.abs(v.getBoundingClientRect().top + 2 + parseFloat(drop.top) - g.getBoundingClientRect().bottom) < 1
|
|
&& drop.pointerEvents === 'none' && getComputedStyle(v, '::after').pointerEvents === 'none'
|
|
&& animations.length === 2 && animations.every(a => a.effect.getTiming().duration === 18000);
|
|
}), 'blood starts at Gore edge with synchronized decorative animations');
|
|
await page.emulateMedia({ reducedMotion: 'reduce' });
|
|
check(await page.evaluate(() => !document.getAnimations().some(a => a.animationName?.startsWith('menuBlood'))),
|
|
'blood respects reduced motion');
|
|
await page.emulateMedia({ reducedMotion: 'no-preference' });
|
|
await click('#mStart');
|
|
check(await stage() === 'player', 'first cup requests identity');
|
|
await page.locator('#pName').fill('MENU CHECK');
|
|
await click('#pGo');
|
|
check(await page.evaluate(() => document.querySelector('#intro').hidden && !!T.G.match.cup),
|
|
'identity resumes the selected cup');
|
|
await quit();
|
|
await click('#mRanks');
|
|
check(await page.locator('#rkModes .on').innerText() === 'THE CUP', 'cup opens its own world board');
|
|
await click('#rkBack');
|
|
await click('#mOther');
|
|
check(await page.evaluate(() => !document.getAnimations().some(a => a.animationName?.startsWith('menuBlood'))),
|
|
'blood animation stops away from splash');
|
|
check((await page.locator('#modeList button').allTextContents()).join('|') ===
|
|
'THE CLIMB|TRAINING|CLASSIC TENNIS', 'other games has exactly three additional solo modes');
|
|
check(await fits('#modeList button:last-child'), 'all additional modes fit on small portrait');
|
|
await page.getByRole('button', { name: 'CLASSIC TENNIS', exact: true }).click();
|
|
check(await stage() === 'setup' && await page.locator('#sDifficulty').isVisible(),
|
|
'classic opens setup with inline difficulty');
|
|
await page.getByRole('button', { name: 'HARD', exact: true }).click();
|
|
check(await stage() === 'setup', 'difficulty selects without navigating');
|
|
check(await page.locator('#courtList button').count() === 1, 'unlocked court is shown first');
|
|
check(await page.locator('#lockedCourts').evaluate(e => e.open), 'locked courts start expanded');
|
|
check(await page.locator('#lockedCourtList button:disabled').count() === 4, 'locked courts cannot be selected');
|
|
await click('#lockedCourtLabel');
|
|
await click('#sEdit');
|
|
await page.locator('#pName').fill('MENU EDIT');
|
|
await page.locator('#pKitRow button[title="CLARET"]').click();
|
|
await click('#pGo');
|
|
check(await stage() === 'setup' && await page.locator('#sPlayerName').innerText() === 'MENU EDIT',
|
|
'player edit returns to setup with saved identity');
|
|
check(await page.locator('#sPlayerKit').innerText() === 'CLARET', 'setup reflects edited kit');
|
|
await click('#cStart');
|
|
check(await page.evaluate(() => T.G.classic && T.G.diff.id === 'hard'), 'serve starts chosen classic difficulty');
|
|
await quit();
|
|
await click('#mRanks');
|
|
check(await page.locator('#rkModes .on').count() === 1, 'classic fallback selects an existing world board');
|
|
await click('#rkBack');
|
|
await click('#mOther');
|
|
await page.getByRole('button', { name: 'TRAINING', exact: true }).click();
|
|
check(await stage() === 'train', 'training chooses opponent before setup');
|
|
await page.getByRole('button', { name: 'THE BALL BOY 02', exact: true }).click();
|
|
check(await stage() === 'setup' && await page.locator('#sOpponent b').innerText() === 'THE BALL BOY',
|
|
'training setup identifies chosen opponent');
|
|
await click('#sBack');
|
|
check(await stage() === 'train', 'training setup returns to opponents');
|
|
await page.getByRole('button', { name: 'THE BALL BOY 02', exact: true }).click();
|
|
await click('#cStart');
|
|
check(await page.evaluate(() => T.G.match.train && T.G.match.opp === 2), 'serve starts selected training opponent');
|
|
await quit();
|
|
await click('#mGore');
|
|
check(await page.evaluate(() => document.querySelector('#intro').hidden && T.G.mode === 'splatter' && !T.G.match),
|
|
'Gore remains a direct start');
|
|
await quit();
|
|
await click('#mCourt');
|
|
check(await stage() === 'setup' && await page.locator('#sTitle').innerText() === 'SPLATTER GORE',
|
|
'Gore court link opens its setup');
|
|
await click('#sBack');
|
|
check(await stage() === 'splash', 'Gore setup returns to splash');
|
|
await click('#mVs');
|
|
await click('#vsHostC');
|
|
await page.waitForFunction(() => /^[A-Z0-9]{4}$/.test(document.querySelector('#vsBig').textContent));
|
|
await click('#vsCancel');
|
|
await click('#vsBack');
|
|
check(await stage() === 'splash' && await page.evaluate(() => !T.G.net), 'friend lobby cancels and returns directly to splash');
|
|
await click('#plaque');
|
|
check(await page.locator('#cdLeagueDetails').evaluate(e => e.open), 'league details start expanded');
|
|
await click('#cdStats');
|
|
check(await stage() === 'stats' && await page.locator('#recorder').evaluate(e => e.open),
|
|
'stats belongs to profile and recording starts expanded');
|
|
await click('#recToggle');
|
|
check((await page.locator('#recLabel').innerText()).includes('ON'), 'recording state remains visible when collapsed');
|
|
await click('#recToggle');
|
|
await click('#recLabel');
|
|
await click('#stBack');
|
|
check(await stage() === 'card', 'stats returns to profile');
|
|
await click('#cdEdit');
|
|
check(await page.locator('#pGo').innerText() === 'SAVE PLAYER', 'existing identity uses save wording');
|
|
check(await page.locator('#pKitRow button').evaluateAll(bs => bs.every(b => {
|
|
const r = b.getBoundingClientRect(); return r.width >= 44 && r.height >= 44;
|
|
})), 'colour swatches meet 44px targets at 320px');
|
|
await click('#pBack');
|
|
check(await stage() === 'card', 'editing back returns to profile');
|
|
await click('#cdBack');
|
|
await page.setViewportSize({ width: 390, height: 844 });
|
|
check(await fits('#mVs'), 'friend door fits large portrait');
|
|
await page.setViewportSize({ width: 768, height: 1024 });
|
|
check(await fits('#intro .card') && await page.locator('#intro .card').evaluate(e =>
|
|
e.getBoundingClientRect().width === innerWidth && e.getBoundingClientRect().height >= innerHeight),
|
|
'menu card fills wider portrait viewport');
|
|
check(errors.length === 0, 'no browser runtime errors: ' + errors.join('; '));
|
|
return { passed: checks.length, checks };
|
|
} finally { await context.close(); }
|
|
}
|