MatchMaster/tools/test-home-button-vibration.cjs

146 lines
6.9 KiB
JavaScript

const test = require('node:test');
const assert = require('node:assert/strict');
const fs = require('node:fs');
const vm = require('node:vm');
const ts = require('typescript');
function evaluate(source, globals) {
return vm.runInNewContext(ts.transpileModule(source, {
compilerOptions: { target: ts.ScriptTarget.ES2017, module: ts.ModuleKind.CommonJS },
}).outputText, globals);
}
function method(file, name, globals) {
const source = ts.createSourceFile(file, fs.readFileSync(file, 'utf8'), ts.ScriptTarget.Latest, true);
let found;
function visit(node) {
if (ts.isMethodDeclaration(node) && node.name.getText(source) === name) found = node;
else ts.forEachChild(node, visit);
}
visit(source);
assert.ok(found, `${file}: ${name}`);
return evaluate(`({ ${found.getText(source).replace(/^private\s+/, '')} }).${name};`, globals);
}
function fixture() {
const state = { scene: { name: 'HomeScene' }, pulses: [], warnings: [] };
const cc = { Button: 'Button', Node: { EventType: { TOUCH_END: 'touchend' } },
director: { getScene: () => state.scene }, fx: { GameConfig: { GM_INFO: { vibrateOpen: true } } } };
const wx = { vibrateShort: options => state.pulses.push(options.type) };
cc.fx.GameTool = { setVibrate: method('assets/Script/module/Tool/GameTool.ts', 'setVibrate', { cc, wx }) };
const globals = { cc, exports: {}, console: { warn: (...args) => state.warnings.push(args) } };
evaluate(fs.readFileSync('assets/Script/module/Tool/HomeButtonVibration.ts', 'utf8'), globals);
Object.assign(globals, globals.exports);
const node = (parent = null, button = null) => ({ parent, getComponent: () => button });
const root = node();
const button = { interactable: true, enabledInHierarchy: true, _pressed: true };
const target = node(root, button);
const event = { target, currentTarget: root };
return { state, cc, wx, globals, node, root, button, event, ...globals.exports };
}
test('existing and late-mounted bundle buttons vibrate once, including child hit targets', () => {
const f = fixture();
f.onHomeButtonTouchEnd(f.event);
f.vibrateHomeButton(f.event);
assert.deepEqual(f.state.pulses, ['light']);
const popup = f.node(f.root);
const dynamicButton = f.node(popup, { ...f.button });
f.onHomeButtonTouchEnd({ target: f.node(dynamicButton), currentTarget: f.root });
assert.deepEqual(f.state.pulses, ['light', 'light']);
});
test('disabled buttons, cancelled presses and empty backgrounds do not vibrate', () => {
for (const property of ['interactable', 'enabledInHierarchy', '_pressed']) {
const f = fixture(); f.button[property] = false;
f.onHomeButtonTouchEnd(f.event);
assert.equal(f.state.pulses.length, 0, property);
}
const f = fixture();
f.onHomeButtonTouchEnd({ target: f.node(f.root), currentTarget: f.root });
assert.equal(f.state.pulses.length, 0);
});
test('reused event objects vibrate on each new dispatch, but never twice per dispatch', () => {
const f = fixture();
for (let i = 0; i < 2; i++) {
f.onHomeButtonTouchEnd(f.event);
f.vibrateHomeButton(f.event);
assert.equal(f.state.pulses.length, i + 1);
}
});
test('respects the vibration switch and leaves GameScene and other scenes untouched', () => {
const f = fixture();
f.cc.fx.GameConfig.GM_INFO.vibrateOpen = false;
f.onHomeButtonTouchEnd(f.event);
assert.equal(f.state.pulses.length, 0);
f.cc.fx.GameConfig.GM_INFO.vibrateOpen = true;
for (const scene of [{ name: 'GameScene' }, { name: 'LoadScene' }, null]) {
f.state.scene = scene;
f.onHomeButtonTouchEnd(f.event);
assert.equal(f.vibrateHomeButton({ target: f.event.target }), false);
assert.equal(f.state.pulses.length, 0);
}
});
test('legacy Home and Career callbacks do not double vibrate or alter popup request cancellation', () => {
const f = fixture();
const homeClick = method('assets/Script/JiaZai.ts', 'vibrateButtonClick', f.globals);
const careerClick = method('assets/career/script/CareerManager2.ts', 'vibrateButtonClick', f.globals);
const home = { homePopupOpenRequest: 7 };
f.onHomeButtonTouchEnd(f.event);
homeClick.call(home, f.event);
careerClick(f.event);
assert.equal(home.homePopupOpenRequest, 8);
assert.deepEqual(f.state.pulses, ['light']);
homeClick.call(home);
assert.equal(home.homePopupOpenRequest, 8);
f.state.scene = { name: 'GameScene' };
careerClick({});
assert.deepEqual(f.state.pulses, ['light', 'light'], 'preserves the legacy callback outside Home');
});
test('vibration API errors do not interrupt click handling or cause a duplicate attempt', () => {
const f = fixture(); let attempts = 0;
f.wx.vibrateShort = () => { attempts++; throw Error('unavailable'); };
assert.doesNotThrow(() => f.onHomeButtonTouchEnd(f.event));
assert.doesNotThrow(() => f.vibrateHomeButton(f.event));
assert.equal(attempts, 1);
assert.equal(f.state.warnings.length, 1);
});
test('Home component registers capture on enable and removes the same listener on disable', () => {
const f = fixture(); const calls = [];
const home = { node: { on: (...args) => calls.push(['on', ...args]), off: (...args) => calls.push(['off', ...args]) } };
method('assets/Script/JiaZai.ts', 'onEnable', f.globals).call(home);
method('assets/Script/JiaZai.ts', 'onDisable', f.globals).call(home);
assert.equal(calls[0][0], 'on'); assert.equal(calls[1][0], 'off');
assert.deepEqual(calls[0].slice(1), ['touchend', f.onHomeButtonTouchEnd, home, true]);
assert.deepEqual(calls[0].slice(1), calls[1].slice(1));
});
test('touch-only Jungle close vibrates once and preserves confirmation guarding', () => {
const f = fixture(); let closes = 0;
const close = method('assets/jungle_treasure/script/JungleOver.ts', 'onClose', f.globals);
const panel = { confirming: false, node: { emit: () => closes++ } };
const event = { target: f.node(f.root), currentTarget: f.root };
f.onHomeButtonTouchEnd(event);
assert.equal(f.state.pulses.length, 0);
close.call(panel, event); close.call(panel, event);
assert.equal(closes, 1); assert.deepEqual(f.state.pulses, ['light']);
});
test('seven-day cards only vibrate when the reward can be claimed', () => {
const f = fixture(); let claims = 0;
const click = method('assets/seven_day_gift/SevenDayGift.ts', 'onCardClicked', f.globals);
const panel = { findReward: () => ({ claimed: false }), info: { canClaim: true, nextRewardDay: 2 },
claiming: false, claimHandler() {}, requestCurrentReward: () => claims++ };
click.call(panel, 1, {});
assert.equal(f.state.pulses.length, 0);
click.call(panel, 2, {});
assert.equal(claims, 1); assert.deepEqual(f.state.pulses, ['light']);
panel.claiming = true; click.call(panel, 2, {});
assert.equal(claims, 1); assert.equal(f.state.pulses.length, 1);
});