346 lines
36 KiB
JavaScript
346 lines
36 KiB
JavaScript
// Tahap 2.2, 2.3, 2.6: layar VS, layar hasil, kutipan.
|
|
// node tests/harness.mjs tests/t2-vsresult.mjs
|
|
// VSR_SHOTS=folder simpan screenshot (default: folder sementara sistem); VSR_ONLY=regex hanya bagian yang namanya cocok (quotes|vs|skip|abort|result|run|ui|focus|phone)
|
|
// Jendela 1280x520 dipakai sengaja: tampilan harus muat di tinggi 520 px.
|
|
import fs from 'node:fs';
|
|
import os from 'node:os';
|
|
import path from 'node:path';
|
|
export const options = { viewport: { width: 1280, height: 520 } };
|
|
|
|
export default async ({ page, game, errors, characters }) => {
|
|
const checks = [], ok = (name, pass, info) => checks.push({ name, pass: !!pass, ...(pass ? {} : { info }) });
|
|
const only = process.env.VSR_ONLY ? new RegExp(process.env.VSR_ONLY) : null, want = n => !only || only.test(n);
|
|
const dir = process.env.VSR_SHOTS || path.join(os.tmpdir(), 't2-vsresult-shots'); fs.mkdirSync(dir, { recursive: true });
|
|
const shot = async name => { await page.screenshot({ path: path.join(dir, name + '.png') }); };
|
|
const wait = ms => page.waitForTimeout(ms);
|
|
// Menyelesaikan pertandingan: sisi 'player' atau 'enemy' menang dua ronde (tanpa uji gerak).
|
|
const finish = (side, rounds = 2) => game(({ side, rounds }) => {
|
|
const g = window.__game; g.manual = true; let won = 0;
|
|
for (let i = 0; i < 900 && !g.snapshot().menuOpen; i++) {
|
|
const s = g.snapshot(); if (s.match?.phase === 'fight' && s.match.round >= 1 && won < 99) { if (side === 'player') g.dummy.damage = 1e4; else g.hero.hp = 0; }
|
|
g.advance(.1);
|
|
}
|
|
return { menuOpen: g.snapshot().menuOpen };
|
|
}, { side, rounds });
|
|
const startVS = (p, e, extra = {}) => game(({ p, e, extra }) => { const g = window.__game; g.manual = false; return g.startMatch({ player: p, enemy: e, stage: extra.stage || 'bellora', level: extra.level || 'medium', mode: extra.mode || 'versus', vs: extra.vs ?? true }); }, { p, e, extra });
|
|
const vsState = () => game(() => { const g = window.__game, el = document.querySelector('#vs-screen'); const s = g.snapshot(); return { el: !!el, out: el?.classList.contains('out') || false, active: g.vsActive(), phase: s.match?.phase, phaseTime: +(s.match?.phaseTime || 0).toFixed(3), cues: s.roundCues.map(c => c.cue + c.round), paused: s.paused, menuOpen: s.menuOpen, names: [...document.querySelectorAll('#vs-screen .vs-name')].map(n => n.textContent), quotes: [...document.querySelectorAll('#vs-screen .vs-quote')].map(n => n.textContent), roundCard: !document.querySelector('#round-call').hidden }; });
|
|
// Menu depan benar-benar hilang (bukan hanya atribut hidden) dan tidak menutupi arena; toast yang tampil dan perpotongannya dengan bilah atas layar VS.
|
|
const menuGone = () => game(() => { const fm = document.querySelector('#front-menu'), hit = document.elementFromPoint(innerWidth / 2, innerHeight / 2); return { hidden: fm.hidden, display: getComputedStyle(fm).display, screen: fm.dataset.screen, coversArena: !!hit?.closest('#front-menu'), hit: hit?.id || hit?.className || hit?.tagName, vs: !!document.querySelector('#vs-screen') }; });
|
|
const arenaClear = (name, m) => ok(name, m.hidden && m.display === 'none' && !m.coversArena && !m.vs, m);
|
|
const toasts = () => game(() => { const top = document.querySelector('#vs-screen .vs-top')?.getBoundingClientRect(); return [...document.querySelectorAll('.meta-toast')].map(t => { const r = t.getBoundingClientRect(); return { text: t.textContent, opacity: getComputedStyle(t).opacity, coversVsTop: !!top && r.bottom > top.top && r.top < top.bottom && r.right > top.left && r.left < top.right }; }); });
|
|
|
|
// ---------- 2.6 data kutipan ----------
|
|
if (want('quotes')) {
|
|
const q = await game(() => JSON.parse(JSON.stringify(window.QUOTES || {})));
|
|
const bad = /allah|tuhan|bismillah|\bamin\b|islam|kristen|hindu|budha|gereja|masjid|ustaz|ustadz|kiai|alhamdulillah|insyaallah|korupsi|tersangka|penjara|ijazah|dinasti|nepotisme|\bsuap\b|skandal|\bayah\b|\bbapak\b|\bibu\b|keluarga|\banak\b|istri|suami/i;
|
|
for (const id of characters) {
|
|
const d = q[id];
|
|
ok(`kutipan ${id}: ada intro/win/lose`, d && ['intro', 'win', 'lose'].every(k => Array.isArray(d[k])), d);
|
|
for (const k of ['intro', 'win', 'lose']) {
|
|
const list = d?.[k] || [];
|
|
ok(`kutipan ${id}.${k}: 3-4 kalimat pendek, unik, tanpa kata terlarang`, list.length >= 3 && list.length <= 4 && new Set(list).size === list.length && list.every(t => typeof t === 'string' && t.length >= 8 && t.length <= 70 && !bad.test(t)), list);
|
|
}
|
|
}
|
|
// Gufron hanya meme ringan: semut / syududu / koloni
|
|
const gufron = Object.values(q.gufron || {}).flat();
|
|
ok('kutipan gufron: hanya meme ringan semut/syududu', gufron.length > 0 && gufron.every(t => /semut|syududu|koloni|remah|gula/i.test(t)), gufron);
|
|
// Pemilih: tidak memakai random() mesin (seed mesin tidak berubah) dan tidak mengulang kutipan yang sama berturut-turut
|
|
const pick = await game(() => { const g = window.__game, before = g.snapshot().stepCount; const seen = []; for (let i = 0; i < 40; i++) seen.push(VS.quote('browo', 'win')); return { seen, repeats: seen.filter((t, i) => i && t === seen[i - 1]).length, distinct: new Set(seen).size, stepCount: g.snapshot().stepCount - before }; });
|
|
ok('pemilih kutipan: variasi dan tanpa pengulangan berturut-turut', pick.repeats === 0 && pick.distinct >= 3, pick);
|
|
const unk = await game(() => VS.quote('tidakada', 'intro'));
|
|
ok('pemilih kutipan: karakter tak dikenal memakai kutipan umum', typeof unk === 'string' && unk.length > 3, unk);
|
|
}
|
|
|
|
// ---------- 2.2 layar VS ----------
|
|
if (want('vs')) {
|
|
// tanpa opsi vs: perilaku lama (cue langsung, tanpa overlay)
|
|
await startVS('browo', 'megachan', { vs: false });
|
|
let s = await vsState();
|
|
ok('tanpa vs: tidak ada overlay dan cue Round 1 langsung', !s.el && !s.active && s.cues.join() === 'round1', s);
|
|
// latihan: tanpa layar VS walau diminta
|
|
await startVS('browo', 'megachan', { mode: 'training', vs: true });
|
|
s = await vsState();
|
|
ok('latihan: tidak ada layar VS', !s.el && !s.active, s);
|
|
|
|
const pairs = [['browo', 'megachan'], ['gufron', 'soloman'], ['sonofsolo', 'anice'], ['ethanolman', 'browo']];
|
|
for (const [p, e] of pairs) {
|
|
await game(() => { window.VS.seed(7); });
|
|
const r = await startVS(p, e, { stage: 'harbor', level: 'hard' });
|
|
s = await vsState();
|
|
ok(`vs ${p} lawan ${e}: layar tampil, ronde ditahan`, r && s.el && s.active && s.phase === 'intro' && s.phaseTime === 0 && s.cues.length === 0 && !s.roundCard, s);
|
|
const names = await game(({ p, e }) => ({ p: window.FIGHTERS?.[p]?.name || 'BRO WO', e: window.FIGHTERS?.[e]?.name || 'BRO WO', q: window.QUOTES }), { p, e });
|
|
ok(`vs ${p} lawan ${e}: nama dan kutipan pembuka dari QUOTES`, s.names[0] === names.p && s.names[1] === names.e && names.q[p].intro.some(t => s.quotes[0].includes(t)) && names.q[e].intro.some(t => s.quotes[1].includes(t)), { s, names: [names.p, names.e] });
|
|
const adv = await game(() => { const g = window.__game; g.manual = true; g.advance(1.5); return { phaseTime: g.snapshot().match.phaseTime, cues: g.snapshot().roundCues.length }; });
|
|
ok(`vs ${p} lawan ${e}: simulasi tidak berjalan selama layar VS`, adv.phaseTime === 0 && adv.cues === 0, adv);
|
|
if (p === 'browo') { await game(() => { window.__game.manual = false; }); await wait(300); await shot('vs-1280x520-a-masuk'); await wait(700); await shot('vs-1280x520-b-slam'); await wait(600); await shot('vs-1280x520-c-akhir'); }
|
|
await game(() => { window.__game.manual = false; });
|
|
}
|
|
|
|
// waktu: kira-kira 2,2 detik lalu cue Round 1 muncul tepat sekali dan urutan intro normal
|
|
const t0 = Date.now(); await startVS('gufron', 'anice');
|
|
let first = null; while (Date.now() - t0 < 4000) { s = await vsState(); if (!s.active) { first = Date.now() - t0; break; } await wait(40); }
|
|
ok('vs: selesai sendiri dalam 1,8 - 2,6 detik', first !== null && first > 1800 && first < 2600, { first });
|
|
s = await vsState();
|
|
ok('vs: cue Round 1 keluar tepat sekali setelah layar selesai', s.cues.join() === 'round1', s);
|
|
await wait(500); s = await vsState();
|
|
ok('vs: overlay hilang dari DOM sesudah fade', !s.el, s);
|
|
// urutan intro sesudah VS sama seperti tanpa VS: round -> fight -> fase fight
|
|
const seq = await game(() => { const g = window.__game; g.manual = true; const out = []; for (let i = 0; i < 60; i++) { g.advance(.1); const s = g.snapshot(); const k = s.match.phase + ':' + s.roundCues.map(c => c.cue + c.round).join('+'); if (out[out.length - 1] !== k) out.push(k); } return out; });
|
|
ok('vs: urutan cue ronde dan fase intro utuh (round1 -> fight -> fight phase)', seq[0].startsWith('intro:round1') && seq.some(x => x === 'intro:round1+fight1') && seq[seq.length - 1] === 'fight:round1+fight1', seq);
|
|
await game(() => { window.__game.manual = false; });
|
|
}
|
|
|
|
// gerak dikurangi: tidak ada animasi, tetapi layar tetap selesai dan ronde jalan
|
|
if (want('vs')) {
|
|
await page.emulateMedia({ reducedMotion: 'reduce' });
|
|
await startVS('anice', 'soloman'); await wait(150);
|
|
const calm = await game(() => { const art = document.querySelector('#vs-screen .vs-art'), q = document.querySelector('#vs-screen .vs-quote'); return { anim: getComputedStyle(art).animationName, qop: getComputedStyle(q).opacity, active: window.__game.vsActive() }; });
|
|
ok('gerak dikurangi: tanpa animasi masuk, kutipan langsung terlihat', calm.anim === 'none' && calm.qop === '1' && calm.active, calm);
|
|
await wait(2200); const after = await vsState();
|
|
ok('gerak dikurangi: layar tetap selesai dan cue Round 1 keluar', !after.active && after.cues.join() === 'round1', after);
|
|
await page.emulateMedia({ reducedMotion: 'no-preference' });
|
|
}
|
|
|
|
// ---------- melewati / membatalkan ----------
|
|
if (want('skip')) {
|
|
for (const how of ['Space', 'Enter', 'click', 'tidak-awal']) {
|
|
await startVS('browo', 'anice'); await wait(how === 'tidak-awal' ? 60 : 450);
|
|
if (how === 'Space') await page.keyboard.press(' '); else if (how === 'Enter') await page.keyboard.press('Enter'); else if (how === 'click') await page.mouse.click(640, 260); else { await page.keyboard.press(' '); await page.mouse.click(640, 260); }
|
|
await wait(30); let s = await vsState();
|
|
if (how === 'tidak-awal') { ok('skip: tidak bisa dilewati dalam 0,38 detik pertama (cegah klik ganda dari menu)', s.active && s.cues.length === 0, s); await wait(450); await page.keyboard.press(' '); await wait(30); s = await vsState(); }
|
|
ok(`skip ${how}: layar selesai dan cue Round 1 sekali`, !s.active && s.cues.join() === 'round1' && s.phase === 'intro', s);
|
|
await page.keyboard.press(' '); await page.keyboard.press('Enter'); await wait(60); s = await vsState();
|
|
ok(`skip ${how}: menekan lagi tidak menggandakan cue`, s.cues.join() === 'round1', s);
|
|
await wait(350);
|
|
}
|
|
// Esc selama VS tidak membuka jeda; hanya melewati
|
|
await startVS('soloman', 'megachan'); await wait(450); await page.keyboard.press('Escape'); await wait(40);
|
|
let s = await vsState();
|
|
ok('Esc selama VS: melewati, tidak membuka jeda', !s.active && !s.paused && s.cues.join() === 'round1', s);
|
|
// tombol game tidak bocor ke bawah layar VS
|
|
await startVS('soloman', 'megachan'); await wait(100);
|
|
await page.keyboard.down('d'); await page.keyboard.press('i'); await page.keyboard.up('d');
|
|
const keys = await game(() => ({ x: Math.round(window.__game.hero.x), cd: window.__game.hero.cooldowns.skill1 }));
|
|
ok('tombol gerak/skill tidak bocor saat VS', keys.x === 350 && keys.cd === 0, keys);
|
|
await wait(2300);
|
|
}
|
|
if (want('abort')) {
|
|
// kembali ke menu saat VS: overlay hilang, tidak ada cue susulan
|
|
await startVS('browo', 'anice'); await wait(200);
|
|
await game(() => window.__game.setMenuOpen(true)); await wait(60);
|
|
let s = await vsState();
|
|
ok('abort: setMenuOpen(true) menutup layar VS tanpa cue', !s.el && !s.active && s.cues.length === 0, s);
|
|
await wait(2400); s = await vsState();
|
|
ok('abort: tidak ada cue susulan setelah layar dibatalkan', s.cues.length === 0, s);
|
|
// pertandingan baru saat VS masih jalan: tidak ada cue ganda dan hanya satu overlay
|
|
await startVS('browo', 'anice'); await wait(300); await startVS('gufron', 'megachan'); await wait(100);
|
|
s = await vsState(); const n = await game(() => document.querySelectorAll('#vs-screen').length);
|
|
ok('abort: pertandingan baru saat VS menggantikan layar lama', n === 1 && s.names[0] === 'GUFRON' && s.active, { s, n });
|
|
await wait(2300); s = await vsState();
|
|
ok('abort: hanya satu cue Round 1 untuk pertandingan terbaru', s.cues.join() === 'round1' && !s.el, s);
|
|
// announcer tidak ditahan: clip round_1 diminta setelah VS (bukan sebelum)
|
|
const ann = await game(async () => { const g = window.__game; g.manual = false; const log = []; g.onRoundCue(e => log.push(e.cue + e.round)); g.startMatch({ player: 'browo', enemy: 'anice', stage: 'bellora', level: 'easy', mode: 'versus', vs: true }); const t = performance.now(); await new Promise(r => setTimeout(r, 900)); const early = log.length; await new Promise(r => setTimeout(r, 1700)); const late = log.slice(); g.onRoundCue(null); return { early, late, t: Math.round(performance.now() - t) }; });
|
|
ok('onRoundCue: tidak terpanggil selama VS, sekali sesudahnya', ann.early === 0 && ann.late.join() === 'round1', ann);
|
|
}
|
|
|
|
// ---------- 2.3 layar hasil ----------
|
|
if (want('result')) {
|
|
// jumlah koin tidak berubah: bandingkan dengan rumus lama
|
|
const legacy = (r) => { const won = r.winner === 'player'; let g = won ? 120 : 30; if (won) { if (r.level && r.level !== 'easy') g += { medium: 40, hard: 80, excellent: 120 }[r.level] || 40; if (r.perfect) g += 60; } g += Math.min(60, (r.bestCombo || 0) * 4); return g; };
|
|
const cases = [];
|
|
for (const winner of ['player', 'enemy']) for (const level of ['easy', 'medium', 'hard', 'excellent']) for (const perfect of [false, true]) for (const bestCombo of [0, 3, 9, 20]) cases.push({ winner, level, perfect: perfect && winner === 'player', bestCombo, playerWins: 2, enemyWins: 0, player: 'browo', enemy: 'anice' });
|
|
const coin = await game(cases => { const M = window.Meta; M.save.ach = { first_win: 1, perfect: 1, hard_win: 1, combo5: 1, combo10: 1, wins10: 1, rich: 1 }; const rows = []; for (const c of cases) { const before = M.coins; M.award(c); const a = M.lastAward; rows.push({ c, delta: M.coins - before, gain: a.gain, sum: a.lines.reduce((x, l) => x + l.amount, 0), keys: a.lines.map(l => l.key).join() }); } return rows; }, cases);
|
|
const wrong = coin.filter(r => !(r.delta === legacy(r.c) && r.gain === r.delta && r.sum === r.delta));
|
|
ok(`koin: ${coin.length} kombinasi sama dengan rumus lama dan rincian berjumlah total`, wrong.length === 0, wrong.slice(0, 3));
|
|
ok('koin: rincian memuat baris menang/hadiah main, tingkat, bersih, kombo sesuai syarat', coin.every(r => r.keys.split(',')[0] === (r.c.winner === 'player' ? 'win' : 'play') && (r.c.winner === 'player' && r.c.level !== 'easy') === r.keys.includes('level') && r.c.perfect === r.keys.includes('perfect') && (r.c.bestCombo > 0) === r.keys.includes('combo')), coin.filter(r => false));
|
|
|
|
// rincian terpenuh (menang bersih, tingkat, kombo) lewat data buatan: harus tetap muat di 520 px
|
|
await game(() => { const g = window.__game; g.setMenuOpen(true); FrontEnd.showResult({ winner: 'player', playerWins: 2, enemyWins: 0, player: 'sonofsolo', enemy: 'ethanolman', stage: 'elderwood', level: 'excellent', mode: 'versus', bestCombo: 9, matchBest: 14, perfect: true, award: { won: true, gain: 120 + 120 + 60 + 36, lines: [{ key: 'win', label: 'Menang', amount: 120 }, { key: 'level', label: 'Tingkat kesulitan', amount: 120, note: 'Sangat sulit' }, { key: 'perfect', label: 'Bersih', amount: 60, note: 'Tanpa kehilangan ronde' }, { key: 'combo', label: 'Bonus kombo', amount: 36, note: '9 pukulan, ronde terakhir' }] } }); });
|
|
await wait(2300); await shot('hasil-penuh-sonofsolo-ethanolman');
|
|
const full = await game(() => { const b = [...document.querySelectorAll('.rs-actions button')].map(x => x.getBoundingClientRect()), q = document.querySelector('.rs-stats dd'), m = document.querySelector('.rs-main').getBoundingClientRect(); return { bottom: Math.max(...b.map(x => x.bottom)), h: innerHeight, scroll: document.querySelector('#front-menu').scrollHeight - document.querySelector('#front-menu').clientHeight, mainTop: m.top, mainBottom: m.bottom, rows: document.querySelectorAll('.rs-coins li').length, combo: q.textContent }; });
|
|
ok('hasil dengan empat baris koin dan kombo 14 muat di 520 px', full.bottom <= full.h - 8 && full.scroll <= 0 && full.rows === 4 && /14/.test(full.combo), full);
|
|
|
|
// kemenangan pemain (tanpa kalah ronde = bersih)
|
|
await game(() => { window.Meta.save.ach = { first_win: 1, perfect: 1, hard_win: 1, combo5: 1, combo10: 1, wins10: 1, rich: 1 }; window.__game.manual = false; });
|
|
for (const [p, e, side] of [['browo', 'megachan', 'player'], ['sonofsolo', 'ethanolman', 'enemy'], ['anice', 'gufron', 'player'], ['megachan', 'soloman', 'enemy']]) {
|
|
await startVS(p, e, { vs: false, level: 'hard', stage: 'moonrise' });
|
|
const before = await game(() => window.Meta.coins);
|
|
await finish(side);
|
|
await wait(120);
|
|
const r = await game(() => { const el = document.querySelector('#front-menu .rs'); const rows = [...document.querySelectorAll('.rs-coins li')].map(li => [li.querySelector('span').textContent, li.querySelector('b').textContent]); return { screen: document.querySelector('#front-menu').dataset.screen, tone: el?.dataset.tone, h1: el?.querySelector('h1')?.textContent, winName: el?.querySelector('.rs-fighter.win strong')?.textContent, loseName: el?.querySelector('.rs-fighter.lose .rs-plate small')?.textContent, winArt: el?.querySelector('.rs-fighter.win img')?.getAttribute('src'), quote: el?.querySelector('.rs-quote p')?.textContent, loseQuote: el?.querySelector('.rs-fighter.lose p')?.textContent, rows, total: el?.querySelector('.rs-total b')?.dataset.count, btn: [...document.querySelectorAll('.rs-actions [data-cmd]')].map(b => b.dataset.cmd + ':' + b.textContent.trim()), stats: [...document.querySelectorAll('.rs-stats dl div')].map(d => d.textContent), coins: window.Meta.coins, last: window.Meta.lastAward, bar: document.querySelector('#front-menu').scrollHeight, h: innerHeight }; });
|
|
const wn = side === 'player' ? p : e, ln = side === 'player' ? e : p;
|
|
const info = await game(({ wn, ln }) => ({ wn: window.FIGHTERS?.[wn]?.name || 'BRO WO', ln: window.FIGHTERS?.[ln]?.name || 'BRO WO', q: window.QUOTES }), { wn, ln });
|
|
ok(`hasil ${p} vs ${e} (${side} menang): layar baru tampil`, r.screen === 'result' && !!r.h1 && r.tone === (side === 'player' ? 'win' : 'lose') && r.h1 === (side === 'player' ? 'MENANG' : 'KALAH'), r);
|
|
ok(`hasil ${p} vs ${e}: pemenang besar dengan kutipan menang, yang kalah dengan kutipan kalah`, r.winName === info.wn && r.loseName === info.ln && r.winArt.includes(wn) && info.q[wn].win.some(t => r.quote.includes(t)) && info.q[ln].lose.some(t => r.loseQuote.includes(t)), { r, info: [info.wn, info.ln] });
|
|
ok(`hasil ${p} vs ${e}: toast koin tidak menumpuk di layar hasil`, await game(() => document.querySelectorAll('.meta-toast.coin').length === 0), 'toast koin masih tampil');
|
|
ok(`hasil ${p} vs ${e}: rincian koin berjumlah total dan sama dengan koin bertambah`, r.rows.length >= 1 && r.rows.reduce((x, [, a]) => x + Number(a.replace('+', '')), 0) === r.last.gain && Number(r.total) === r.last.gain && r.coins - before === r.last.gain, { rows: r.rows, total: r.total, delta: r.coins - before, gain: r.last.gain });
|
|
ok(`hasil ${p} vs ${e}: empat tombol dan empat statistik`, r.btn.join('|') === 'rematch:MAIN LAGI ↗|reselect:PILIH ULANG|shop:KE TOKO|home:MENU UTAMA' && r.stats.length === 4, r);
|
|
await wait(1900); await shot(`hasil-${side === 'player' ? 'menang' : 'kalah'}-${p}-${e}`);
|
|
const fit = await game(() => { const b = [...document.querySelectorAll('.rs-actions button')].map(x => x.getBoundingClientRect()), rs = document.querySelector('.rs').getBoundingClientRect(); return { bottom: Math.max(...b.map(x => x.bottom)), top: Math.min(...b.map(x => x.top)), rsBottom: rs.bottom, h: innerHeight, scroll: document.querySelector('#front-menu').scrollHeight - document.querySelector('#front-menu').clientHeight, clipped: [...document.querySelectorAll('.rs-main > *')].some(n => { const r = n.getBoundingClientRect(); return r.bottom > innerHeight + 1 || r.top < 0; }) }; });
|
|
ok(`hasil ${p} vs ${e}: semua tombol muat di jendela 520 px tanpa gulir`, fit.bottom <= fit.h && fit.scroll <= 0 && !fit.clipped, fit);
|
|
}
|
|
}
|
|
|
|
// ---------- Arcade/Survival ----------
|
|
if (want('run')) {
|
|
await game(() => { window.Meta.save.ach = { first_win: 1, perfect: 1, hard_win: 1, combo5: 1, combo10: 1, wins10: 1, rich: 1, arcade_clear: 1, survival5: 1 }; window.__game.manual = false; });
|
|
// Arcade: lawan pertama memakai layar VS berlabel, menang, lalu lawan kedua, lalu kalah -> layar akhir bergaya hasil
|
|
await game(() => { window.Meta.startRun('arcade', 'browo'); });
|
|
await wait(150); let s = await vsState();
|
|
const label = await game(() => document.querySelector('#vs-screen .vs-top')?.textContent || '');
|
|
ok('arcade: lawan 1 memakai layar VS berlabel', s.el && s.active && /ARCADE · LAWAN 1 \/ 6/.test(label), { s, label });
|
|
const tArc = await toasts();
|
|
ok('arcade: tidak ada toast di atas bilah layar VS (info lawan sudah ada di bilah)', tArc.length === 0, tArc);
|
|
await wait(450); await shot('vs-arcade'); await game(() => window.__game.vsSkip());
|
|
await finish('player', 2).then(() => null).catch(() => null);
|
|
await wait(100);
|
|
// setelah menang jeda 2,6 detik lalu lawan berikut (VS lagi); jam jeda mulai saat K.O. di dalam finish, jadi tunggu layarnya muncul (bukan tidur tetap)
|
|
await page.waitForSelector('#vs-screen', { timeout: 6000 }).catch(() => null); s = await vsState();
|
|
ok('arcade: menang -> lawan 2 muncul dengan layar VS', s.el && /LAWAN 2 \/ 6/.test(await game(() => document.querySelector('#vs-screen .vs-top')?.textContent || '')), s);
|
|
await game(() => window.__game.vsSkip());
|
|
await finish('enemy');
|
|
await wait(200);
|
|
const end = await game(() => { const el = document.querySelector('#front-menu .rs'); return { screen: document.querySelector('#front-menu').dataset.screen, tone: el?.dataset.tone, h1: el?.querySelector('h1')?.textContent, kicker: el?.querySelector('.rs-kicker')?.textContent, rows: [...document.querySelectorAll('.rs-coins li')].map(li => li.textContent.replace(/\s+/g, ' ').trim()), total: el?.querySelector('.rs-total b')?.dataset.count, stats: [...document.querySelectorAll('.rs-stats dl div')].map(d => d.textContent), oldPanel: !!document.querySelector('.run-end'), metaHidden: document.querySelector('#meta-overlay')?.hidden ?? true }; });
|
|
ok('arcade: kalah di lawan 2 -> layar akhir memakai tampilan hasil baru', end.screen === 'result' && end.tone === 'lose' && end.h1 === 'BERAKHIR' && /KALAH DI LAWAN KE-2 DARI 6/.test(end.kicker) && !end.oldPanel, end);
|
|
ok('arcade: rincian koin memuat lawan sebelumnya dan pertandingan terakhir', end.rows.length >= 2 && /sebelumnya/i.test(end.rows[0]) && /terakhir/i.test(end.rows[end.rows.length - 1]), end);
|
|
await wait(1900); await shot('hasil-arcade-berakhir');
|
|
// Main lagi memulai rangkaian baru; Pilih ulang kembali ke pilih karakter dengan mode run
|
|
await page.click('[data-cmd=rematch]'); await wait(200); s = await vsState();
|
|
const lab2 = await game(() => document.querySelector('#vs-screen .vs-top')?.textContent || '');
|
|
ok('arcade: Main lagi memulai rangkaian baru (lawan 1)', s.el && /LAWAN 1 \/ 6/.test(lab2), { s, lab2 });
|
|
await game(() => window.__game.vsSkip()); await wait(350);
|
|
arenaClear('arcade: Main lagi, setelah VS layar hasil tidak lagi menutupi arena', await menuGone());
|
|
await finish('enemy'); await wait(200);
|
|
await page.click('[data-cmd=reselect]'); await wait(100);
|
|
const sel = await game(() => ({ screen: document.querySelector('#front-menu').dataset.screen, step: document.querySelector('#front-menu').dataset.step, eyebrow: document.querySelector('.select-header .eyebrow')?.textContent }));
|
|
ok('arcade: Pilih ulang kembali ke pilih karakter (mode ARCADE)', sel.screen === 'select' && sel.step === '0' && /ARCADE/.test(sel.eyebrow), sel);
|
|
|
|
// Survival berakhir pada kekalahan pertama
|
|
await game(() => { window.Meta.startRun('survival', 'megachan'); }); await wait(100); await game(() => window.__game.vsSkip());
|
|
await finish('enemy'); await wait(200);
|
|
const sv = await game(() => { const el = document.querySelector('#front-menu .rs'); return { tone: el?.dataset.tone, h1: el?.querySelector('h1')?.textContent, kicker: el?.querySelector('.rs-kicker')?.textContent, score: el?.querySelector('.rs-score')?.textContent.replace(/\s+/g, ' ') }; });
|
|
ok('survival: layar akhir tampil dengan rekor', sv.h1 === 'BERAKHIR' && /SURVIVAL/.test(sv.kicker), sv);
|
|
await wait(1900); await shot('hasil-survival');
|
|
// Arcade tamat: menang semua lawan (6)
|
|
await game(() => { window.Meta.startRun('arcade', 'anice'); });
|
|
for (let i = 0; i < 6; i++) { await wait(80); await game(() => window.__game.vsSkip()); await finish('player'); if (i < 5) await wait(2800); }
|
|
await wait(300);
|
|
const clr = await game(() => { const el = document.querySelector('#front-menu .rs'); return { tone: el?.dataset.tone, h1: el?.querySelector('h1')?.textContent, kicker: el?.querySelector('.rs-kicker')?.textContent, rows: [...document.querySelectorAll('.rs-coins li')].map(li => li.textContent.replace(/\s+/g, ' ').trim()), total: el?.querySelector('.rs-total b')?.dataset.count, confetti: document.querySelectorAll('.rs-confetti i').length }; });
|
|
ok('arcade tamat: layar TAMAT! dengan bonus 500 dan konfeti', clr.h1 === 'TAMAT!' && clr.tone === 'win' && clr.rows.some(r => /Bonus Arcade tamat.*\+500/.test(r)) && clr.confetti > 0, clr);
|
|
await wait(1900); await shot('hasil-arcade-tamat');
|
|
}
|
|
|
|
// ---------- jalur menu: dua pemain, tombol hasil ----------
|
|
if (want('ui')) {
|
|
await game(() => { window.Meta.save.ach = { first_win: 1, perfect: 1, hard_win: 1, combo5: 1, combo10: 1, wins10: 1, rich: 1 }; window.Meta.cancelRun(); window.__game.setMenuOpen(true); window.FrontEnd.home(); });
|
|
await game(() => { delete window.Meta.save.ach.twoplayer; });
|
|
await page.click('[data-cmd="2p"]'); await wait(80);
|
|
await page.click('[data-fighter=browo]'); await wait(1100);
|
|
await page.click('[data-fighter=megachan]'); await wait(1100);
|
|
await page.click('[data-cmd=start]'); await wait(200);
|
|
let s = await vsState();
|
|
const meta = await game(() => ({ who: [...document.querySelectorAll('#vs-screen .vs-who')].map(n => n.textContent), top: document.querySelector('#vs-screen .vs-top')?.textContent }));
|
|
ok('dua pemain lewat menu: layar VS berlabel PEMAIN 1 / PEMAIN 2', s.active && meta.who.join() === 'PEMAIN 1,PEMAIN 2' && /DUA PEMAIN/.test(meta.top), { s, meta });
|
|
const tDuring = await toasts();
|
|
ok('dua pemain: toast petunjuk P2 dan pencapaian ditunda, tidak menutupi bilah VS', tDuring.length === 0, tDuring);
|
|
await wait(2200); s = await vsState();
|
|
const tAfter = await toasts();
|
|
ok('dua pemain: petunjuk P2 dan pencapaian TEMAN BERMAIN muncul tepat setelah VS selesai', tAfter.some(t => /^P2:/.test(t.text)) && tAfter.some(t => /TEMAN BERMAIN/.test(t.text)) && tAfter.every(t => !t.coversVsTop), tAfter);
|
|
ok('dua pemain: ronde mulai normal setelah VS', !s.active && s.cues.join() === 'round1', s);
|
|
await finish('enemy'); await wait(150);
|
|
const two = await game(() => ({ screen: document.querySelector('#front-menu').dataset.screen, kicker: document.querySelector('.rs-kicker')?.textContent, tone: document.querySelector('.rs')?.dataset.tone }));
|
|
ok('dua pemain: hasil menyebut PEMAIN 2 MENANG', two.screen === 'result' && /PEMAIN 2 MENANG/.test(two.kicker), two);
|
|
// tombol: Ke toko membuka toko; tutup kembali ke layar hasil; Main lagi memulai pertandingan lagi (2P)
|
|
await page.click('[data-cmd=shop]'); await wait(150);
|
|
let m = await game(() => ({ hidden: document.querySelector('#meta-overlay')?.hidden, screen: document.querySelector('#front-menu').dataset.screen }));
|
|
ok('tombol KE TOKO membuka toko di atas layar hasil', m.hidden === false && m.screen === 'result', m);
|
|
await page.click('#meta-overlay [data-close]'); await wait(100);
|
|
m = await game(() => ({ hidden: document.querySelector('#meta-overlay')?.hidden, screen: document.querySelector('#front-menu').dataset.screen, rs: !!document.querySelector('.rs') }));
|
|
ok('menutup toko kembali ke layar hasil', m.hidden === true && m.screen === 'result' && m.rs, m);
|
|
await page.click('[data-cmd=rematch]'); await wait(200); s = await vsState();
|
|
const again = await game(() => ({ who: [...document.querySelectorAll('#vs-screen .vs-who')].map(n => n.textContent) }));
|
|
ok('tombol MAIN LAGI memulai ulang (VS tampil, tetap dua pemain)', s.active && again.who.join() === 'PEMAIN 1,PEMAIN 2', { s, again });
|
|
await game(() => window.__game.vsSkip()); await wait(350);
|
|
arenaClear('MAIN LAGI (dua pemain): setelah VS layar hasil tidak lagi menutupi arena', await menuGone());
|
|
await finish('player'); await wait(150);
|
|
await page.click('[data-cmd=home]'); await wait(100);
|
|
m = await game(() => ({ screen: document.querySelector('#front-menu').dataset.screen }));
|
|
ok('tombol MENU UTAMA kembali ke menu utama', m.screen === 'home', m);
|
|
// lawan CPU lewat menu: VS menampilkan KOMPUTER dan tingkat kesulitan
|
|
await page.click('[data-cmd=versus]'); await wait(80);
|
|
await page.click('[data-fighter=sonofsolo]'); await wait(1100);
|
|
await page.click('[data-fighter=ethanolman]'); await wait(1100);
|
|
await page.click('[data-level=hard]'); await page.click('[data-cmd=start]'); await wait(450);
|
|
const cpu = await game(() => ({ who: [...document.querySelectorAll('#vs-screen .vs-who')].map(n => n.textContent), top: document.querySelector('#vs-screen .vs-top')?.textContent }));
|
|
ok('versus CPU lewat menu: label KOMPUTER, arena, tingkat SULIT', cpu.who.join() === 'PEMAIN 1,KOMPUTER' && /VERSUS KOMPUTER/.test(cpu.top) && /SULIT/.test(cpu.top), cpu);
|
|
await shot('vs-versus-menu'); await game(() => window.__game.vsSkip());
|
|
await finish('player'); await wait(150);
|
|
await page.click('[data-cmd=rematch]'); await wait(200); await game(() => window.__game.vsSkip()); await wait(350);
|
|
arenaClear('MAIN LAGI (versus CPU): setelah VS layar hasil tidak lagi menutupi arena', await menuGone());
|
|
}
|
|
|
|
// ---------- fokus awal layar hasil: Spasi/Enter (tombol serang) tidak boleh langsung memulai ulang ----------
|
|
if (want('focus')) {
|
|
await game(() => { window.Meta.cancelRun(); window.Meta.save.ach = { first_win: 1, perfect: 1, hard_win: 1, combo5: 1, combo10: 1, wins10: 1, rich: 1, arcade_clear: 1, survival5: 1 }; window.__game.manual = false; });
|
|
const state = () => game(() => { const g = window.__game, a = document.activeElement; return { menuOpen: g.snapshot().menuOpen, screen: document.querySelector('#front-menu').dataset.screen, rs: !!document.querySelector('.rs'), vs: !!document.querySelector('#vs-screen'), activeCmd: a?.dataset?.cmd || null, activeTag: a?.tagName }; });
|
|
const stays = (s) => s.menuOpen && s.screen === 'result' && s.rs && !s.vs;
|
|
await startVS('browo', 'megachan', { vs: false, level: 'easy' });
|
|
await finish('player'); await wait(150);
|
|
let s = await state();
|
|
ok('hasil: fokus awal bukan tombol aksi (bukan MAIN LAGI)', stays(s) && s.activeCmd === null, s);
|
|
await page.keyboard.press(' '); await page.keyboard.press('Enter'); await wait(450); s = await state();
|
|
ok('hasil: Spasi lalu Enter tepat setelah layar muncul tidak memulai ulang', stays(s), s);
|
|
// tombol serang ditahan menembus K.O.: auto-repeat dan keyup tidak boleh mengaktifkan tombol apa pun
|
|
await page.keyboard.down(' '); await page.keyboard.down(' '); await page.keyboard.down(' '); await wait(60); await page.keyboard.up(' '); await wait(450); s = await state();
|
|
ok('hasil: Spasi ditahan (auto-repeat) lalu dilepas tidak memulai ulang', stays(s), s);
|
|
await page.keyboard.press('Tab'); s = await state();
|
|
ok('hasil: Tab menjangkau MAIN LAGI (papan ketik tetap bisa dipakai)', s.activeCmd === 'rematch', s);
|
|
await page.keyboard.press('Enter'); await wait(250); s = await state();
|
|
ok('hasil: Tab lalu Enter (sengaja) memulai ulang dengan layar VS', !s.menuOpen && s.vs, s);
|
|
await game(() => window.__game.vsSkip());
|
|
// akhir Arcade: Spasi tidak boleh memulai rangkaian baru
|
|
await game(() => { window.Meta.startRun('arcade', 'browo'); }); await wait(100); await game(() => window.__game.vsSkip());
|
|
await finish('enemy'); await wait(200);
|
|
await page.keyboard.press(' '); await page.keyboard.press('Enter'); await wait(450); s = await state();
|
|
ok('akhir Arcade: Spasi/Enter tidak memulai rangkaian baru', stays(s) && await game(() => document.querySelector('.rs-kicker')?.textContent.includes('KALAH DI LAWAN')), s);
|
|
await game(() => { window.Meta.cancelRun(); window.__game.setMenuOpen(true); window.FrontEnd.home(); });
|
|
}
|
|
|
|
// ---------- ponsel lanskap ----------
|
|
if (want('phone')) {
|
|
await page.setViewportSize({ width: 667, height: 375 });
|
|
await game(() => { window.Meta.save.ach = { first_win: 1, perfect: 1, hard_win: 1, combo5: 1, combo10: 1, wins10: 1, rich: 1 }; window.__game.setMenuOpen(true); });
|
|
await startVS('sonofsolo', 'ethanolman'); await wait(1300); await shot('vs-667x375');
|
|
await game(() => window.__game.vsSkip());
|
|
await finish('player'); await wait(2300); await shot('hasil-667x375');
|
|
const fit = await game(() => { const b = [...document.querySelectorAll('.rs-actions button')].map(x => x.getBoundingClientRect()); return { bottom: Math.max(...b.map(x => x.bottom)), h: innerHeight, scroll: document.querySelector('#front-menu').scrollHeight - document.querySelector('#front-menu').clientHeight }; });
|
|
ok('hasil di ponsel lanskap 667x375: tombol muat tanpa gulir', fit.bottom <= fit.h && fit.scroll <= 0, fit);
|
|
// Kolom Statistik tidak boleh meluap atau terpotong di layar pendek maupun potret (gerak dikurangi: tata letak langsung jadi, tanpa menunggu animasi).
|
|
await page.emulateMedia({ reducedMotion: 'reduce' });
|
|
const stats = () => game(() => { const p = document.querySelector('.rs-stats'), pr = p.getBoundingClientRect(), bad = []; for (const n of p.querySelectorAll('dt,dd')) { const r = n.getBoundingClientRect(); if (r.right > pr.right + .5 || r.right > innerWidth + .5 || n.scrollWidth > n.clientWidth + 1) bad.push(`${n.textContent} r=${Math.round(r.right)} sw=${n.scrollWidth} cw=${n.clientWidth}`); } const ab = Math.max(...[...document.querySelectorAll('.rs-actions button')].map(x => x.getBoundingClientRect().bottom)); return { bad, panelRight: Math.round(pr.right), panelOverflow: p.scrollWidth - p.clientWidth, vw: innerWidth, actionsBottom: Math.round(ab), vh: innerHeight }; });
|
|
const base = { winner: 'player', playerWins: 2, enemyWins: 0, player: 'sonofsolo', enemy: 'ethanolman', level: 'excellent', mode: 'versus', bestCombo: 14, matchBest: 14, award: { won: true, gain: 120, lines: [{ key: 'win', label: 'Menang', amount: 120 }] } };
|
|
const variants = [
|
|
['versus elderwood', { stage: 'elderwood' }], ['versus sunspire', { stage: 'sunspire' }],
|
|
['arcade kalah', { winner: 'enemy', playerWins: 0, enemyWins: 2, stage: 'elderwood', run: { kind: 'arcade', wins: 1, total: 6, cleared: false, best: 3 } }],
|
|
['survival kalah', { winner: 'enemy', playerWins: 0, enemyWins: 2, stage: 'sunspire', run: { kind: 'survival', wins: 12, total: 0, cleared: false, best: 14 } }]
|
|
];
|
|
const bads = [], lows = [];
|
|
for (const [w, h] of [[568, 320], [667, 375], [740, 360], [812, 375], [844, 390], [896, 414], [1024, 520], [768, 1024]]) {
|
|
await page.setViewportSize({ width: w, height: h });
|
|
for (const [name, extra] of variants) {
|
|
await game(d => { window.__game.setMenuOpen(true); window.FrontEnd.showResult(d); }, { ...base, ...extra });
|
|
await wait(120);
|
|
const st = await stats(); if (st.bad.length || st.panelOverflow > 0) bads.push({ viewport: `${w}x${h}`, variant: name, ...st }); if (st.actionsBottom > st.vh) lows.push(`${w}x${h} ${name} bottom=${st.actionsBottom}`);
|
|
if (w === 667 && name === 'arcade kalah') await shot('hasil-arcade-667x375');
|
|
}
|
|
}
|
|
ok('hasil: kolom Statistik tidak meluap atau terpotong di 568x320 sampai 1024x520 dan potret 768x1024 (versus, Arcade, Survival)', bads.length === 0, bads.slice(0, 4));
|
|
ok('hasil: tombol aksi tetap muat di semua ukuran uji (Statistik tidak menambah tinggi berlebih)', lows.length === 0, lows);
|
|
await page.emulateMedia({ reducedMotion: 'no-preference' });
|
|
await page.setViewportSize({ width: 1280, height: 520 });
|
|
}
|
|
|
|
const failed = checks.filter(c => !c.pass);
|
|
return { total: checks.length, failed: failed.length, shots: dir, ...(failed.length ? { failures: failed } : {}), passed: checks.filter(c => c.pass).map(c => c.name) };
|
|
};
|