225 lines
21 KiB
JavaScript
225 lines
21 KiB
JavaScript
// Tahap 2.1 + 2.4: meter ultimate di HUD, indikator mode, daftar gerakan di menu jeda.
|
|
// node tests/harness.mjs tests/t2-hud.mjs
|
|
// HUD_SHOTS=folder simpan screenshot (default: tmp/t2-hud di folder sementara sistem)
|
|
// HUD_TOUCH=1 jalankan di layar sentuh 844x390 (html.touch); tanpa itu 1280x520
|
|
// HUD_ONLY=regex hanya jalankan bagian yang namanya cocok (meter|enemy|cpu|pause|layout)
|
|
// Jendela 520 px dipakai sengaja: tampilan harus muat di tinggi 520 px.
|
|
import fs from 'node:fs';
|
|
import os from 'node:os';
|
|
import path from 'node:path';
|
|
|
|
const touch = !!process.env.HUD_TOUCH;
|
|
export const options = touch ? { query: '?touch=1&embed=1', viewport: { width: 844, height: 390 } } : { viewport: { width: 1280, height: 520 } };
|
|
|
|
export default async ({ page, game, characters, errors }) => {
|
|
const checks = [], ok = (name, pass, info) => checks.push({ name, pass: !!pass, info: pass ? undefined : info });
|
|
const only = process.env.HUD_ONLY ? new RegExp(process.env.HUD_ONLY) : null, want = n => !only || only.test(n);
|
|
const dir = process.env.HUD_SHOTS || path.join(os.tmpdir(), 't2-hud'); fs.mkdirSync(dir, { recursive: true });
|
|
const tag = touch ? 'touch' : 'desk';
|
|
const shot = async name => { await game(() => window.__game.draw()); await page.screenshot({ path: path.join(dir, `${tag}-${name}.png`) }); };
|
|
const MODES = { browo: 'MENGAMUK', gufron: 'KOLONI', soloman: 'BLUSUKAN', anice: 'SENATOR', ethanolman: 'LEDAKAN', sonofsolo: 'ASAM', megachan: 'MURKA' };
|
|
const COLORS = { browo: '#ff6e28', gufron: '#c9a05a', soloman: '#ffd24a', anice: '#ffd24a', ethanolman: '#ff9a2a', sonofsolo: '#9dff3c', megachan: '#ff2a2a' };
|
|
const BASE = { browo: '#d9b44a', gufron: '#c9a05a', soloman: '#e0b04a', anice: '#5aa9f0', ethanolman: '#2fbf7a', sonofsolo: '#7bd23c', megachan: '#d62828' };
|
|
// Membaca keadaan meter dari DOM (apa yang benar-benar dilihat pemain).
|
|
const read = side => game(side => {
|
|
const r = document.querySelector('#ult-' + side), f = r.querySelector('.ult-fill'), t = f.style.transform.match(/scaleX\(([\d.]+)\)/);
|
|
const box = r.getBoundingClientRect(), tr = r.querySelector('.ult-track').getBoundingClientRect();
|
|
return { state: r.dataset.state, name: r.querySelector('.ult-name').textContent, val: r.querySelector('.ult-val').textContent, p: t ? +t[1] : null, hidden: r.hidden, low: r.classList.contains('low'), again: r.classList.contains('again'),
|
|
color: r.style.getPropertyValue('--ult'), kbd: r.querySelector('.ult-kbd').hidden ? '' : r.querySelector('.ult-kbd').textContent, label: r.getAttribute('aria-label'),
|
|
box: { x: Math.round(box.x), y: Math.round(box.y), w: Math.round(box.width), h: Math.round(box.height) }, trackW: Math.round(tr.width), vis: getComputedStyle(r).visibility };
|
|
}, side);
|
|
const start = (p, e, extra = {}) => game(({ p, e, extra }) => {
|
|
const g = window.__game; g.manual = true; g.startMatch({ player: p, enemy: e, stage: 'bellora', level: 'easy', mode: extra.mode || 'versus' });
|
|
g.setAI(!!extra.ai); if (extra.p2) g.setP2(true); g.advance(extra.wait ?? 3); return g.snapshot().hero.cooldowns;
|
|
}, { p, e, extra });
|
|
const press = (key, hold = 1 / 30) => game(({ key, hold }) => { const g = window.__game; g.press(key); g.advance(hold); g.release(key); g.advance(1 / 60); }, { key, hold });
|
|
const adv = s => game(s => { window.__game.advance(s); }, s);
|
|
const near = (a, b, t = .02) => Math.abs(a - b) <= t;
|
|
|
|
// ---------- 1. meter pemain: SIAP -> mengisi -> mode (7 karakter) ----------
|
|
if (want('meter')) for (const [i, ch] of characters.entries()) {
|
|
const rival = characters[(i + 3) % characters.length];
|
|
await start(ch, rival, { ai: false });
|
|
const a = await read('player');
|
|
ok(`${ch}: awal ronde SIAP berdenyut`, a.state === 'ready' && a.val === 'SIAP' && a.name === 'ULTIMATE' && near(a.p, 1, .001) && !a.hidden && (touch ? true : a.kbd === 'P'), a);
|
|
ok(`${ch}: warna meter = warna karakter`, a.color.toLowerCase() === BASE[ch], { got: a.color, want: BASE[ch] });
|
|
if (ch === characters[0]) await shot('ready');
|
|
await press('p');
|
|
// lewati sinematik ultimate sampai mode atau cooldown berjalan
|
|
let seen = null, mid = null, prevP = null, dec = true, steps = 0, charging = null;
|
|
for (; steps < 200 && !seen; steps++) {
|
|
await adv(.1); const r = await read('player');
|
|
if (r.state === 'mode') seen = r; else if (r.state === 'charging' && !charging) charging = r;
|
|
}
|
|
ok(`${ch}: setelah ultimate masuk mode ${MODES[ch]}`, seen && seen.name === MODES[ch] && seen.color.toLowerCase() === COLORS[ch], { seen, steps });
|
|
if (seen) {
|
|
ok(`${ch}: bar mode penuh di awal lalu menyusut`, seen.p > .85, seen);
|
|
const vals = [seen.p];
|
|
for (let k = 0; k < 4; k++) { await adv(1.5); const r = await read('player'); if (r.state !== 'mode') break; vals.push(r.p); mid = r; }
|
|
for (let k = 1; k < vals.length; k++) if (!(vals[k] < vals[k - 1])) dec = false;
|
|
ok(`${ch}: bar mode menyusut monoton`, dec && vals.length >= 4, vals);
|
|
ok(`${ch}: teks sisa waktu mengikuti bar`, mid && /^\d+,\d dtk$/.test(mid.val) && near(parseFloat(mid.val.replace(',', '.')) / 10, mid.p, .03), mid);
|
|
if (ch === 'browo' || ch === 'anice') await shot('mode-' + ch);
|
|
for (let k = 0; k < 12; k++) { await adv(1); const r = await read('player'); if (r.state !== 'mode') { prevP = r; break; } }
|
|
ok(`${ch}: mode berakhir kembali ke bar cooldown`, prevP && prevP.state === 'charging' && prevP.p < 1 && /^\d+ dtk$/.test(prevP.val), prevP);
|
|
}
|
|
// cooldown menurun: bar naik, teks sesuai cooldown asli
|
|
const before = await read('player'); await adv(5); const after = await read('player');
|
|
const cdNow = await game(() => window.__game.hero.cooldowns.ultimate);
|
|
ok(`${ch}: bar cooldown naik dan teks = ceil(cooldown)`, after.state === 'charging' && after.p > before.p && after.val === Math.ceil(cdNow) + ' dtk', { before, after, cdNow });
|
|
if (ch === characters[0]) await shot('charging');
|
|
// pulih penuh -> SIAP lagi
|
|
await adv(40); const done = await read('player');
|
|
ok(`${ch}: cooldown habis kembali SIAP`, done.state === 'ready' && near(done.p, 1, .001), done);
|
|
}
|
|
|
|
// ---------- 2. meter lawan (sisi P2 dan CPU) memakai data yang sama ----------
|
|
if (want('enemy')) for (const [i, ch] of characters.entries()) {
|
|
const me = characters[(i + 1) % characters.length];
|
|
await start(me, ch, { ai: false, p2: true });
|
|
const a = await read('enemy');
|
|
ok(`lawan ${ch}: terlihat dan SIAP di awal (ringkas, tanpa tombol)`, !a.hidden && a.vis === 'visible' && a.state === 'ready' && a.kbd === '', a);
|
|
ok(`lawan ${ch}: warna meter = warna karakter`, a.color.toLowerCase() === BASE[ch], { got: a.color, want: BASE[ch] });
|
|
await press('.');
|
|
let seen = null, steps = 0;
|
|
for (; steps < 200 && !seen; steps++) { await adv(.1); const r = await read('enemy'); if (r.state === 'mode') seen = r; }
|
|
ok(`lawan ${ch}: mode ${MODES[ch]} tampil di sisi lawan`, seen && seen.name === MODES[ch] && seen.color.toLowerCase() === COLORS[ch] && seen.p > .8, { seen, steps });
|
|
// efek mode sungguhan terjadi di sisi lawan (cooldown skill diganti) sehingga indikator tidak sekadar kosmetik
|
|
const rage = await game(() => window.__game.dummy.bwRage);
|
|
ok(`lawan ${ch}: mode aktif juga di mesin (dummy.bwRage)`, rage === true, rage);
|
|
if (ch === 'anice' || ch === 'browo') await shot('enemy-mode-' + ch);
|
|
await adv(12); const back = await read('enemy'); const cdNow = await game(() => window.__game.dummy.cooldowns.ultimate);
|
|
ok(`lawan ${ch}: setelah mode bar cooldown sesuai`, back.state === 'charging' && back.val === Math.ceil(cdNow) + ' dtk', { back, cdNow });
|
|
}
|
|
|
|
// ---------- 3. lawan CPU memakai ultimate sendiri; latihan tanpa AI menyembunyikan meter lawan ----------
|
|
if (want('cpu')) {
|
|
await start('gufron', 'megachan', { ai: true, wait: .2 });
|
|
const states = new Set(); let modeSeen = null;
|
|
for (let k = 0; k < 400 && !modeSeen; k++) { await adv(.25); const r = await read('enemy'); states.add(r.state); if (r.state === 'mode') modeSeen = r; }
|
|
ok('CPU: meter lawan melewati SIAP lalu mode saat CPU memakai ultimate', states.has('ready') && modeSeen && modeSeen.name === 'MURKA', { states: [...states], modeSeen });
|
|
await start('gufron', 'megachan', { mode: 'training', ai: false });
|
|
const off = await read('enemy'); ok('latihan tanpa AI: meter lawan tersembunyi', off.hidden && off.vis === 'hidden', off);
|
|
await game(() => window.__game.setAI(true)); await adv(.1);
|
|
const on = await read('enemy'); ok('latihan dengan AI: meter lawan tampil', !on.hidden && on.vis === 'visible', on);
|
|
const pl = await read('player'); ok('latihan: meter pemain tetap tampil', !pl.hidden, pl);
|
|
}
|
|
|
|
// ---------- 4. karakter lama (ARCO, FENR) tidak merusak meter ----------
|
|
if (want('legacy')) {
|
|
for (const [p, e] of [['arco', 'fenr'], ['fenr', 'arco']]) {
|
|
await start(p, e, { ai: true, wait: 3 });
|
|
const a = await read('player'), b = await read('enemy');
|
|
ok(`${p} vs ${e}: meter terisi`, a.state === 'ready' && b.state === 'ready', { a, b });
|
|
await press('p'); await adv(4);
|
|
const c = await read('player'); ok(`${p}: setelah ultimate meter berubah (mode/cooldown)`, ['mode', 'charging'].includes(c.state), c);
|
|
}
|
|
}
|
|
|
|
// ---------- 4b. menu jeda untuk karakter lama dan jendela sempit ----------
|
|
if (want('pause')) {
|
|
await game(() => { for (const s of [...document.querySelectorAll('style')]) if (s.textContent.includes('#pause-panel{display:none!important}')) s.remove(); try { localStorage.removeItem('konoha.moveList'); } catch (_) {} });
|
|
for (const ch of ['arco', 'fenr', 'mira']) {
|
|
await start(ch, 'gufron', { ai: true, wait: .5 });
|
|
const r = await game(() => { window.__game.setPaused(true); const d = document.querySelector('#pause-panel'), rows = d.querySelectorAll('.mv-row').length, keys = [...d.querySelectorAll('.mv-key')].map(k => k.textContent); window.__game.setPaused(false); return { open: true, rows, keys }; });
|
|
ok(`jeda ${ch} (karakter lama): daftar terisi tanpa galat`, r.rows >= 12 && ['Spasi', 'I', 'O', 'P', 'K', 'L', 'J'].every(k => r.keys.includes(k)), r);
|
|
}
|
|
if (!touch) {
|
|
await page.setViewportSize({ width: 600, height: 420 }); await page.waitForTimeout(100);
|
|
await start('browo', 'anice', { ai: true, wait: .5 });
|
|
await game(() => window.__game.setPaused(true));
|
|
const n = await game(() => { const d = document.querySelector('#pause-panel'), r = d.getBoundingClientRect(), b = document.querySelector('#resume-btn').getBoundingClientRect(), det = document.querySelector('#move-list'); const was = det.open; det.open = true;
|
|
return new Promise(res => setTimeout(() => { d.scrollTop = d.scrollHeight; const rows = [...d.querySelectorAll('.mv-row')], last = rows[rows.length - 1].getBoundingClientRect(), dr = d.getBoundingClientRect(); res({ was, fits: r.top >= 0 && r.bottom <= innerHeight && r.left >= 0 && r.right <= innerWidth, resume: b.top >= 0 && b.bottom <= innerHeight, lastVisible: last.bottom <= dr.bottom + 1, canScroll: d.scrollHeight > d.clientHeight, oy: getComputedStyle(d).overflowY, hs: d.scrollWidth > d.clientWidth }); }, 60)); });
|
|
ok('jeda 600x420: terlipat di jendela sempit, dialog muat, dapat digulir saat dibuka, tanpa gulir samping', n.was === false && n.fits && n.resume && n.lastVisible && n.oy === 'auto' && !n.hs, n);
|
|
await shot('pause-600x420'); await game(() => { document.querySelector('#pause-panel').scrollTop = 0; window.__game.setPaused(false); });
|
|
await page.setViewportSize({ width: 1280, height: 520 });
|
|
}
|
|
}
|
|
|
|
// ---------- 5. menu jeda: daftar gerakan ----------
|
|
if (want('pause')) {
|
|
await game(() => { for (const s of [...document.querySelectorAll('style')]) if (s.textContent.includes('#pause-panel{display:none!important}')) s.remove(); try { localStorage.removeItem('konoha.moveList'); } catch (_) {} });
|
|
const dump = () => game(() => {
|
|
const d = document.querySelector('#pause-panel'), det = document.querySelector('#move-list'), body = document.querySelector('#move-list-body');
|
|
const r = d.getBoundingClientRect(), rb = document.querySelector('#resume-btn').getBoundingClientRect();
|
|
return { open: d.open, detOpen: det.open, text: d.innerText, keys: [...body.querySelectorAll('.mv-key')].map(k => k.textContent), names: [...body.querySelectorAll('.mv-row > span:last-child > span')].map(n => n.textContent),
|
|
titles: [...body.querySelectorAll('h3')].map(h => h.childNodes[0].textContent), rect: { x: Math.round(r.x), y: Math.round(r.y), w: Math.round(r.width), h: Math.round(r.height), b: Math.round(r.bottom) }, resume: { y: Math.round(rb.y), b: Math.round(rb.bottom) },
|
|
scroll: { sh: d.scrollHeight, ch: d.clientHeight, oy: getComputedStyle(d).overflowY }, vw: innerWidth, vh: innerHeight, paused: window.__game.snapshot().paused };
|
|
});
|
|
const FIGHT = await game(() => Object.fromEntries(Object.entries(window.FIGHTERS).map(([id, f]) => [id, { names: f.names, mode: f.mode && { label: f.mode.label, names: f.mode.names } }])));
|
|
const BW = await game(() => ({ names: window.Browo.names, rage: window.Browo.rageNames }));
|
|
for (const ch of characters) {
|
|
await start(ch, characters[(characters.indexOf(ch) + 2) % characters.length], { ai: true, wait: .5 });
|
|
await game(() => window.__game.setPaused(true));
|
|
const d = await dump();
|
|
const want4 = ch === 'browo' ? BW.names : FIGHT[ch].names, wantMode = ch === 'browo' ? BW.rage : FIGHT[ch].mode.names;
|
|
ok(`jeda ${ch}: dialog terbuka dan game dijeda`, d.open && d.paused, d);
|
|
ok(`jeda ${ch}: berisi 11 tombol dasar`, ['A', 'D', 'W', 'S', 'Spasi', 'I', 'O', 'P', 'K', 'L', 'J'].every(k => d.keys.includes(k)), d.keys);
|
|
ok(`jeda ${ch}: gerakan perintah`, d.text.includes('Uppercut') && d.text.includes('Tinju maju') && d.keys.filter(k => k === '↓').length === 3 && d.keys.includes('→'), d.keys);
|
|
ok(`jeda ${ch}: nama skill karakter`, want4.every(n => d.names.includes(n)), { want4, names: d.names });
|
|
ok(`jeda ${ch}: nama skill mode ${MODES[ch]}`, wantMode.every(n => d.names.includes(n)) && d.titles.some(t => t === 'Mode ' + MODES[ch]), { wantMode, titles: d.titles });
|
|
ok(`jeda ${ch}: tanpa bagian Pemain 2 di mode lawan CPU`, !d.titles.includes('Pemain 2') && !d.keys.includes('N'), d.titles);
|
|
ok(`jeda ${ch}: teks Indonesia, tidak ada "PAUSED"`, !/PAUSED/.test(d.text) && d.text.includes('JEDA'), d.text.slice(0, 80));
|
|
ok(`jeda ${ch}: panel muat di jendela (atau dapat digulir)`, d.rect.y >= 0 && d.rect.b <= d.vh && d.resume.y >= 0 && d.resume.b <= d.vh, d);
|
|
ok(`jeda ${ch}: seluruh daftar terlihat tanpa menggulir`, d.scroll.sh <= d.scroll.ch + 1, d.scroll);
|
|
if (ch === 'browo' || ch === 'anice') await shot('pause-' + ch);
|
|
await game(() => window.__game.setPaused(false));
|
|
}
|
|
// dua pemain: kontrol P2 + nama skill lawan
|
|
await start('gufron', 'anice', { ai: false, p2: true, wait: .5 });
|
|
await game(() => window.__game.setPaused(true));
|
|
let d = await dump();
|
|
ok('jeda 2P: bagian Pemain 2 dengan tombol P2', d.titles.includes('Pemain 2') && ['←', '→', '↑', 'N', 'M', ',', '.'].every(k => d.keys.includes(k)), d);
|
|
ok('jeda 2P: nama skill lawan (ANICE) di bagian P2', FIGHT.anice.names.every(n => d.names.includes(n)), d.names);
|
|
ok('jeda 2P: panel muat atau dapat digulir', d.rect.y >= 0 && d.rect.b <= d.vh && d.resume.b <= d.vh, d);
|
|
await shot('pause-2p');
|
|
const reach = await game(() => { const dlg = document.querySelector('#pause-panel'); dlg.scrollTop = dlg.scrollHeight; const rows = [...dlg.querySelectorAll('.mv-row')], last = rows[rows.length - 1].getBoundingClientRect(), dr = dlg.getBoundingClientRect(); const r = { canScroll: dlg.scrollHeight > dlg.clientHeight, lastVisible: last.bottom <= dr.bottom + 1 && last.top >= dr.top, oy: getComputedStyle(dlg).overflowY }; dlg.scrollTop = 0; return r; });
|
|
ok('jeda 2P: baris terakhir dapat dijangkau (muat atau digulir)', reach.lastVisible && (!reach.canScroll || reach.oy === 'auto'), reach);
|
|
// tombol lipat + preferensi diingat
|
|
const toggled = await game(() => { const det = document.querySelector('#move-list'), was = det.open; det.querySelector('summary').click(); return new Promise(r => setTimeout(() => r({ was, now: det.open, pref: localStorage.getItem('konoha.moveList') }), 60)); });
|
|
ok('jeda: ringkasan melipat/membuka daftar dan menyimpan pilihan', toggled.was !== toggled.now && toggled.pref === (toggled.now ? '1' : '0'), toggled);
|
|
d = await dump(); await shot('pause-' + (toggled.now ? 'open' : 'folded'));
|
|
ok('jeda: terlipat tetap muat di jendela', d.rect.y >= 0 && d.rect.b <= d.vh, d);
|
|
await game(() => window.__game.setPaused(false)); await game(() => window.__game.setPaused(true));
|
|
d = await dump(); ok('jeda: pilihan lipat diingat saat dibuka lagi', d.detOpen === toggled.now, { d: d.detOpen, want: toggled.now });
|
|
// Esc melanjutkan
|
|
await page.keyboard.press('Escape'); await page.waitForTimeout(80);
|
|
d = await dump(); ok('jeda: Esc melanjutkan permainan', !d.open && !d.paused, d);
|
|
// jeda -> lanjut tidak merusak meter
|
|
const m = await read('player'); ok('jeda: meter pemain tetap berfungsi setelah lanjut', ['ready', 'charging', 'mode'].includes(m.state), m);
|
|
}
|
|
|
|
// ---------- 6. tata letak: meter muat, tidak menimpa HP, tombol sentuh, dan kartu skill ----------
|
|
if (want('layout')) {
|
|
await start('anice', 'browo', { ai: true, wait: .5 });
|
|
const lay = await game(() => {
|
|
const R = s => { const e = document.querySelector(s); if (!e || e.offsetParent === null && getComputedStyle(e).position !== 'fixed') return null; const r = e.getBoundingClientRect(); return { x: r.x, y: r.y, r: r.right, b: r.bottom, w: r.width, h: r.height }; };
|
|
const hit = (a, b) => a && b && a.x < b.r && b.x < a.r && a.y < b.b && b.y < a.b;
|
|
const pm = R('#ult-player'), em = R('#ult-enemy'), pl = R('.fighter-one .life-frame'), el = R('.fighter-two .life-frame'), pc = R('.fighter-one .bar-caption'), ec = R('.fighter-two .bar-caption');
|
|
const pp = R('.fighter-one .portrait-frame'), ep = R('.fighter-two .portrait-frame'), clock = R('.match-center'), acts = R('.match-actions'), ft = R('#player-form-timer');
|
|
const touchBtns = [...document.querySelectorAll('#touch-controls .dbtn, #touch-controls .tbtn')].map(b => b.getBoundingClientRect()).filter(r => r.width > 0);
|
|
const meters = [pm, em].filter(Boolean);
|
|
return { vw: innerWidth, vh: innerHeight, pm, em, overlapHp: hit(pm, pl) || hit(em, el), overlapCaption: hit(pm, pc) || hit(em, ec), overlapPortrait: hit(pm, pp) || hit(em, ep), overlapClock: hit(pm, clock) || hit(em, clock), overlapActions: hit(pm, acts) || hit(em, acts),
|
|
overlapTouch: touchBtns.some(t => meters.some(m => m.x < t.right && t.x < m.r && m.y < t.bottom && t.y < m.b)), touchCount: touchBtns.length, inView: meters.every(m => m.x >= 0 && m.r <= innerWidth && m.y >= 0 && m.b <= innerHeight),
|
|
hscroll: document.documentElement.scrollWidth > innerWidth, vscroll: document.documentElement.scrollHeight > innerHeight, cards: R('.skill-deck') };
|
|
});
|
|
ok(`layout ${tag}: meter pemain dan lawan terlihat di dalam jendela`, lay.pm && lay.em && lay.inView, lay);
|
|
ok(`layout ${tag}: tidak menimpa bar HP, keterangan, potret, jam, atau tombol jeda`, !lay.overlapHp && !lay.overlapCaption && !lay.overlapPortrait && !lay.overlapClock && !lay.overlapActions, lay);
|
|
ok(`layout ${tag}: tidak menimpa tombol sentuh`, !lay.overlapTouch && (!touch || lay.touchCount >= 8), lay);
|
|
ok(`layout ${tag}: tidak ada gulir halaman`, !lay.hscroll && !lay.vscroll, lay);
|
|
await shot('layout');
|
|
// jendela lain pada pengujian desktop: sempit (950 / 620) dan tinggi (720)
|
|
if (!touch) for (const [w, h] of [[1280, 720], [960, 520], [844, 390], [600, 420]]) {
|
|
await page.setViewportSize({ width: w, height: h }); await page.waitForTimeout(120);
|
|
const l = await game(() => { const R = s => { const r = document.querySelector(s).getBoundingClientRect(); return { x: r.x, y: r.y, r: r.right, b: r.bottom }; }, hit = (a, b) => a.x < b.r && b.x < a.r && a.y < b.b && b.y < a.b;
|
|
const pm = R('#ult-player'), em = R('#ult-enemy'); return { pm, em, hp: hit(pm, R('.fighter-one .life-frame')) || hit(em, R('.fighter-two .life-frame')), cap: hit(pm, R('.fighter-one .bar-caption')) || hit(em, R('.fighter-two .bar-caption')), por: hit(pm, R('.fighter-one .portrait-frame')) || hit(em, R('.fighter-two .portrait-frame')), mid: hit(pm, R('.match-center')) || hit(em, R('.match-center')), inView: pm.x >= 0 && em.r <= innerWidth && em.b <= innerHeight, vs: document.documentElement.scrollHeight > innerHeight }; });
|
|
ok(`layout ${w}x${h}: meter tidak bertabrakan dan muat`, !l.hp && !l.cap && !l.por && !l.mid && l.inView && !l.vs, l);
|
|
await shot(`layout-${w}x${h}`);
|
|
}
|
|
if (!touch) await page.setViewportSize({ width: 1280, height: 520 });
|
|
}
|
|
|
|
const failed = checks.filter(c => !c.pass);
|
|
return { passed: checks.length - failed.length, total: checks.length, failed, pageErrors: errors.length };
|
|
};
|