137 lines
6.6 KiB
JavaScript
137 lines
6.6 KiB
JavaScript
// 编辑器交互回归:调色板无「查看属性」工具;无论手上拿什么方块(橡皮除外),
|
||
// 点击画布上任意方块都选中该格并在属性面板显示其属性(不覆盖已有方块)。
|
||
const noop = () => { };
|
||
let fail = 0;
|
||
const ok = (c, m) => { console.log((c ? ' ✔ ' : ' ✗ ') + m); if (!c) fail++; };
|
||
|
||
// ---------- 最小假 DOM(addEventListener 收句柄,children 真实入列)----------
|
||
const els = new Map();
|
||
function mkEl(id = '') {
|
||
const e = {
|
||
id, tagName: 'DIV', value: '', _html: '', textContent: '',
|
||
style: {}, dataset: {}, children: [],
|
||
width: 800, height: 608,
|
||
classList: {
|
||
_s: new Set(),
|
||
add(...c) { c.forEach(x => this._s.add(x)); },
|
||
remove(...c) { c.forEach(x => this._s.delete(x)); },
|
||
toggle(c, on) { on ? this._s.add(c) : this._s.delete(c); },
|
||
contains(c) { return this._s.has(c); },
|
||
},
|
||
_h: {},
|
||
addEventListener(t, fn) { (this._h[t] = this._h[t] || []).push(fn); },
|
||
removeEventListener: noop,
|
||
appendChild(c) { this.children.push(c); return c; },
|
||
querySelector: () => mkEl(), querySelectorAll: () => [],
|
||
getBoundingClientRect: () => ({ left: 0, top: 0, width: 800, height: 608 }),
|
||
getContext: () => ctx2d, focus: noop, click: noop,
|
||
preventDefault: noop,
|
||
};
|
||
Object.defineProperty(e, 'innerHTML', {
|
||
get() { return this._html; },
|
||
set(v) { this._html = String(v); this.children = []; },
|
||
});
|
||
return e;
|
||
}
|
||
const ctx2d = new Proxy({
|
||
canvas: { width: 800, height: 608 },
|
||
measureText: () => ({ width: 12 }),
|
||
createLinearGradient: () => ({ addColorStop: noop }),
|
||
createRadialGradient: () => ({ addColorStop: noop }),
|
||
getImageData: () => ({ data: [] }),
|
||
}, {
|
||
get(t, k) { return k in t ? t[k] : noop; },
|
||
set(t, k, v) { t[k] = v; return true; },
|
||
});
|
||
const getEl = s => { if (!els.has(s)) els.set(s, mkEl(s)); return els.get(s); };
|
||
|
||
globalThis.location = { search: '', href: 'http://localhost/', pathname: '/', reload: noop };
|
||
Object.defineProperty(globalThis, 'navigator', { value: { maxTouchPoints: 0, userAgent: 'node' }, configurable: true });
|
||
globalThis.matchMedia = q => ({ matches: false, media: q, addEventListener: noop, removeEventListener: noop });
|
||
globalThis.window = {
|
||
addEventListener: noop, removeEventListener: noop, AudioContext: null, webkitAudioContext: null,
|
||
innerWidth: 1280, innerHeight: 800, location: globalThis.location, navigator: globalThis.navigator, matchMedia: globalThis.matchMedia,
|
||
};
|
||
globalThis.addEventListener = noop;
|
||
globalThis.removeEventListener = noop;
|
||
globalThis.confirm = () => true;
|
||
globalThis.alert = noop;
|
||
globalThis.localStorage = { _d: {}, getItem(k) { return this._d[k] ?? null; }, setItem(k, v) { this._d[k] = String(v); }, removeItem(k) { delete this._d[k]; } };
|
||
globalThis.document = {
|
||
getElementById: getEl, querySelector: getEl, querySelectorAll: () => [],
|
||
createElement(t) { const e = mkEl(); e.tagName = t.toUpperCase(); return e; },
|
||
addEventListener: noop, body: mkEl('body'),
|
||
};
|
||
|
||
const { Editor, PALETTE } = await import('../js/editor.js');
|
||
|
||
// 画布坐标 → 格子:pos() 用 clientX/Y(scroll=0,缩放 1:1),格心 (tx*32+16, ty*32+16)
|
||
const ev = (tx, ty, button = 0) => ({ clientX: tx * 32 + 16, clientY: ty * 32 + 16, button, preventDefault: noop });
|
||
const clickTile = (tx, ty, button = 0) => {
|
||
for (const fn of getEl('#ed-canvas')._h.mousedown) fn(ev(tx, ty, button));
|
||
};
|
||
// renderProps 先写面板 innerHTML(标题),再把每个属性字段 appendChild 成子行
|
||
const propsHtml = () => {
|
||
const box = getEl('#ed-props');
|
||
return box.innerHTML + box.children.map(c => c.innerHTML || '').join('');
|
||
};
|
||
|
||
console.log('=== 调色板不再有「查看属性」工具 ===');
|
||
ok(!PALETTE.some(p => p[0] === '?'), "PALETTE 中已无 '?'(查看属性)");
|
||
ok(!PALETTE.some(p => p[1] === '查看属性'), 'PALETTE 中已无「查看属性」条目');
|
||
Editor.init({ startLevel: noop });
|
||
const palLabels = getEl('#palette').children.map(c => c.textContent || '');
|
||
ok(palLabels.length === PALETTE.length, `调色板渲染 ${PALETTE.length} 项(无工具项),实际 ${palLabels.length}`);
|
||
ok(!palLabels.some(t => t.includes('查看属性')), '调色板 UI 无「查看属性」按钮');
|
||
|
||
console.log('=== 打开地图,构造一块带属性的摄像头 C ===');
|
||
Editor.open();
|
||
const f = Editor.curFace();
|
||
f.grid[5][7] = 'C'; // 摄像头:SCHEMA 里有 4 个可编辑属性
|
||
f.props[5][7] = { w: 4, h: 4, mode: 'zoom', zoom: 1.5 };
|
||
|
||
console.log('=== 手上拿刺(^),点击已有摄像头 → 只看属性,不覆盖 ===');
|
||
Editor.sel = '^';
|
||
clickTile(7, 5);
|
||
ok(f.grid[5][7] === 'C', '格子内容未被覆盖(仍是 C)');
|
||
ok(Editor.selTile && Editor.selTile.tx === 7 && Editor.selTile.ty === 5, 'selTile 指向被点方块 (7,5)');
|
||
const html1 = propsHtml();
|
||
ok(html1.includes('区域宽') && html1.includes('镜头模式'), '属性面板显示该方块的属性(区域宽/镜头模式)');
|
||
|
||
console.log('=== 手上拿方块(#),点击同一格 → 同样只查看,不覆盖 ===');
|
||
Editor.sel = '#';
|
||
clickTile(7, 5);
|
||
ok(f.grid[5][7] === 'C', '拿 # 点击也不覆盖已有方块');
|
||
ok(propsHtml().includes('区域宽'), '属性面板仍显示 C 的属性');
|
||
|
||
console.log('=== 手上拿炮台(O),点击空格子 → 放置并显示新方块属性 ===');
|
||
Editor.sel = 'O';
|
||
clickTile(3, 9);
|
||
ok(f.grid[9][3] === 'O', '空格子正常放置 O');
|
||
ok(propsHtml().includes('发射间隔'), '放置后属性面板显示 O 的属性');
|
||
|
||
console.log('=== 提示牌 T 的文本属性可随点击查看 ===');
|
||
f.grid[9][4] = 'T'; f.props[9][4] = { msg: '小心刺' };
|
||
Editor.sel = '%'; // 拿着冰面点提示牌
|
||
clickTile(4, 9);
|
||
ok(f.grid[9][4] === 'T' && propsHtml().includes('提示文字'), '非相关工具点击 T 也能查看文字属性');
|
||
|
||
console.log('=== 橡皮擦例外:选中橡皮点击 → 擦除 ===');
|
||
Editor.sel = '.';
|
||
clickTile(4, 9);
|
||
ok(f.grid[9][4] === '.', '橡皮擦点击后 T 被擦除');
|
||
Editor.sel = '^'; // 恢复非橡皮工具
|
||
clickTile(7, 5); // 顺手确认 C 还在
|
||
|
||
console.log('=== 右键擦除与工具无关 ===');
|
||
Editor.sel = '#';
|
||
clickTile(3, 9, 2); // button=2 右键
|
||
ok(f.grid[9][3] === '.', '右键擦除不受手上工具影响');
|
||
|
||
console.log('=== 属性面板空态提示仍存在 ===');
|
||
Editor.show(); // 重置 selTile
|
||
ok(propsHtml().includes('点击画布上的方块'), '未选中时显示引导文案');
|
||
|
||
console.log(fail === 0 ? '\n✅ 编辑器点击查看属性 全部断言通过' : `\n❌ 失败 ${fail} 项`);
|
||
process.exit(fail === 0 ? 0 : 1);
|