Files
kidgo/tools/settings-test.mjs
T
2026-10-23 17:49:29 +08:00

135 lines
6.6 KiB
JavaScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
// 设置模块单元测试:默认布局 / 应用定位 / 进入 / 确定保存 / 恢复默认
const noop = () => { };
class El {
constructor(tag = 'DIV') {
this.tagName = tag.toUpperCase();
this.value = '';
this.textContent = '';
this._html = '';
this._classes = new Set();
this._style = {};
this.dataset = {};
this.children = [];
this._ev = {};
this.parent = null;
const self = this;
this.classList = {
add: c => self._classes.add(c),
remove: c => self._classes.delete(c),
contains: c => self._classes.has(c),
toggle: (c, f) => { const on = f === undefined ? !self._classes.has(c) : f; on ? self._classes.add(c) : self._classes.delete(c); return on; },
};
}
get style() { return this._style; }
set className(v) { this._classes = new Set(String(v).split(/\s+/).filter(Boolean)); }
get className() { return [...this._classes].join(' '); }
set innerHTML(v) { this._html = v; if (v === '') this.children = []; }
get innerHTML() { return this._html; }
appendChild(c) { c.parent = this; this.children.push(c); return c; }
remove() { if (this.parent) { const i = this.parent.children.indexOf(this); if (i >= 0) this.parent.children.splice(i, 1); } }
querySelector() { return new El(); }
querySelectorAll(sel) { return sel === '.tc-btn' ? this.children.filter(c => c._classes.has('tc-btn')) : []; }
addEventListener(type, fn) { (this._ev[type] ||= []).push(fn); }
removeEventListener() { }
dispatch(type, ev) { (this._ev[type] || []).forEach(fn => fn(ev)); }
setPointerCapture() { }
releasePointerCapture() { }
getBoundingClientRect() { return { left: 0, top: 0, width: 800, height: 608 }; }
}
const byId = new Map();
const touchMap = new Map();
globalThis.localStorage = {
_d: {},
getItem(k) { return k in this._d ? this._d[k] : null; },
setItem(k, v) { this._d[k] = String(v); },
removeItem(k) { delete this._d[k]; },
};
globalThis.document = {
getElementById(id) { if (!byId.has(id)) byId.set(id, new El()); return byId.get(id); },
createElement(t) { return new El(t); },
querySelector(sel) {
const m = sel.match(/data-touch="([^"]+)"/);
if (m) { const k = m[1]; if (!touchMap.has(k)) touchMap.set(k, new El()); return touchMap.get(k); }
return new El();
},
querySelectorAll() { return []; },
};
globalThis.window = { addEventListener: noop, removeEventListener: noop };
globalThis.confirm = () => true;
let fail = 0;
const ok = (c, m) => { console.log((c ? ' ✔ ' : ' ✗ ') + m); if (!c) fail++; };
const { Save } = await import('../js/save.js');
const { Settings } = await import('../js/settings.js');
console.log('=== 初始化 / 默认布局 ===');
Settings.init();
const s0 = Save.data.settings;
ok(!!s0 && !!s0.touch, '存档含 settings.touch');
const keys = ['left', 'right', 'jump', 'shoot', 'down', 'pause', 'suicide'];
ok(keys.every(k => s0.touch[k]), '7 个手柄键都有默认布局');
ok(s0.touch.left.cx === 12 && s0.touch.jump.size === 94, '默认坐标/尺寸正确');
ok(byId.get('title-user').textContent === '玩家:无名氏', '标题默认显示「无名氏」');
console.log('=== apply 把布局写到真实手柄 ===');
const L = touchMap.get('left'), J = touchMap.get('jump');
ok(L._style.left === '12%' && L._style.top === '80%' && L._style.width === '62px', 'left 按钮定位/尺寸已应用');
ok(J._style.width === '94px' && J._style.left === '88%', 'jump 按钮定位/尺寸已应用');
console.log('=== 进入 / 确定 保存用户名与布局 ===');
Settings.enter();
byId.get('set-username').value = '阿测玩家';
Settings.confirm();
ok(Save.data.settings.username === '阿测玩家', '用户名已保存到存档');
ok(byId.get('title-user').textContent === '玩家:阿测玩家', '标题用户名已更新');
ok(localStorage.getItem('iwbtc_fan_v1').includes('阿测玩家'), '已写入 localStorage');
console.log('=== 恢复默认 ===');
byId.get('set-username').value = '随便';
Settings.reset();
ok(Settings._state().username === '', '恢复默认后用户名为空');
ok(Settings._state().touch.jump.size === 94 && Settings._state().touch.left.cx === 12, '恢复默认后布局回到出厂值');
// 恢复默认后必须再点确定才落盘
ok(Save.data.settings.username === '阿测玩家', '恢复默认未自动落盘(需点确定)');
Settings.confirm();
ok(Save.data.settings.username === '', '点确定后空用户名落盘');
console.log('=== 布局越长越界会被限制(place 约束在交互层,这里校验默认在合法区间)===');
ok(keys.every(k => { const p = Save.data.settings.touch[k]; return p.cx >= 4 && p.cx <= 96 && p.size >= 24 && p.size <= 160; }), '默认布局均在合法范围');
console.log('=== UI SCREENS 注册(防止设置屏永远不显示)===');
const uiSrc = (await import('node:fs')).readFileSync(new URL('../js/ui.js', import.meta.url), 'utf8');
ok(/SCREENS\s*=\s*\[[^\]]*'scr-settings'/.test(uiSrc), 'scr-settings 已加入 SCREENS 列表');
ok(/data-act="settings"/.test((await import('node:fs')).readFileSync(new URL('../index.html', import.meta.url), 'utf8')), '标题菜单有「设置」入口');
console.log('=== 模拟拖动 / 缩放(防「按下即跳角」)===');
Settings.enter(); // 重新同步工作副本
const ev = (t, x, y) => ({ target: t, clientX: x, clientY: y, pointerId: 1, preventDefault: noop, stopPropagation: noop });
const left = Settings._els().left;
const handle = left.children.find(c => c._classes.has('tc-rsz')) || left.children[0];
const wt = Settings._state().touch;
const cx0 = wt.left.cx, cy0 = wt.left.cy;
// 在按钮中心 (96,486) 按下,向右拖 40px(预览宽 800 → +5%)
left.dispatch('pointerdown', ev(left, 96, 486));
left.dispatch('pointermove', ev(left, 136, 486));
ok(Math.abs(wt.left.cx - (cx0 + 5)) < 0.01 && wt.left.cy === cy0, '拖动:cx 按位移增量变化,无跳角');
// 再拖回原处
left.dispatch('pointermove', ev(left, 96, 486));
ok(Math.abs(wt.left.cx - cx0) < 0.01, '拖动:逆向位移可精确归位');
left.dispatch('pointerup', ev(left, 96, 486));
// 缩放手柄:中心 (96,486),起点距中心 ~56,拉远到 ~113 → 尺寸翻倍
const size0 = wt.left.size;
handle.dispatch('pointerdown', ev(handle, 136, 526));
handle.dispatch('pointermove', ev(handle, 176, 566));
ok(wt.left.size > size0 && wt.left.size <= 160, `缩放:尺寸随距离放大(${size0}→${wt.left.size.toFixed(0)})`);
handle.dispatch('pointerup', ev(handle, 176, 566));
ok(wt.left.cx === cx0 && wt.left.cy === cy0, '缩放:不改变按钮位置');
console.log(fail === 0 ? '\n✅ settings 全部断言通过' : `\n❌ 失败 ${fail} 项`);
process.exit(fail === 0 ? 0 : 1);