Files
MatchMaster/tools/test-memory-diagnostics.cjs

381 lines
26 KiB
JavaScript

const test = require('node:test'), assert = require('node:assert/strict');
const fs = require('node:fs'), vm = require('node:vm');
const source = fs.readFileSync('build-templates/wechatgame/memory-diagnostics.js', 'utf8');
const ROOT = '/user/memory-diagnostics/';
function fixture(files = new Map(), envVersion = 'trial', now = 1800000000000, initiallyFull = false, enabled = true) {
const handlers = {}, timers = [], modals = [], sheets = [], calls = [], logins = [], requests = [], timeouts = new Map();
let timeoutId = 0;
let failWrite = initiallyFull;
const fileSystem = {
accessSync() {}, mkdirSync() {},
readdirSync() { return [...files.keys()].filter(k => k.startsWith(ROOT)).map(k => k.slice(ROOT.length)); },
readFileSync(p) { if (!files.has(p)) throw Error('missing'); return files.get(p); },
writeFileSync(p, data) { if (failWrite) throw Error('full'); calls.push(['write', p]); files.set(p, data); },
appendFileSync(p, data) { if (failWrite) throw Error('full'); calls.push(['append', p]); files.set(p, (files.get(p) || '') + data); },
unlinkSync(p) { calls.push(['unlink', p]); files.delete(p); }
};
const wx = { env: { USER_DATA_PATH: '/user' },
getAccountInfoSync() { if (envVersion === 'throws') throw Error(); return { miniProgram: { envVersion, version: '1.2.3' } }; },
getFileSystemManager() { calls.push(['fs']); return fileSystem; },
getDeviceInfo: () => ({ model: 'iPhone 8 Plus', platform: 'ios', system: 'iOS 16.7.10', memorySize: 3072, deviceId: 'secret' }),
getAppBaseInfo: () => ({ version: '8.0', SDKVersion: '3.0' }),
showModal(o) { modals.push(o); }, showActionSheet(o) { sheets.push(o); }, showToast() {},
showLoading() {}, hideLoading() {},
login(o) { logins.push(o); },
request(o) { requests.push(o); return { abort() { calls.push(['abort']); } }; },
setClipboardData() { calls.push(['clipboard']); throw Error('clipboard must not be used'); }
};
for (const name of ['onMemoryWarning', 'onHide', 'onShow', 'onError', 'onUnhandledRejection']) wx[name] = fn => { handlers[name] = fn; };
class Clock extends Date { static now() { return now; } }
const module = { exports: {} };
vm.runInNewContext(source, { module, Date: Clock, setInterval: fn => { timers.push(fn); return timers.length; }, clearInterval: id => { timers[id - 1] = null; },
setTimeout: fn => { timeouts.set(++timeoutId, fn); return timeoutId; }, clearTimeout: id => timeouts.delete(id) });
const api = module.exports.create(wx, { id: 'build-123', builtAt: '2026-10-26', flags: { iOSHighPerformance: true, 'iOSHighPerformance+': true } }, enabled);
const rows = () => [...files.values()].flatMap(s => s.split('\n').filter(Boolean).flatMap(line => { try { return [JSON.parse(line)]; } catch (_) { return []; } }));
return { api, wx, files, handlers, timers, modals, sheets, calls, rows, logins, requests, timeouts,
full() { failWrite = true; }, advance(ms) { now += ms; }, tick() { now += 5000; timers.filter(Boolean).forEach(fn => fn()); } };
}
function startUpload(f, code = 'one-use-code') {
assert.equal(f.modals[0].title, '上传诊断');
f.modals.shift().success({ confirm: true });
f.logins.shift().success({ code });
const request = f.requests.shift();
assert.equal(request.url, 'https://sor779u2w8.sealoshzh.site/memoryDiagnostics');
assert.equal(request.data.action, 'upload'); assert.equal(request.data.code, code);
return request;
}
function completeUpload(f, request) {
request.success({ statusCode: 200, data: { code: 1, data: {
id: 'a'.repeat(24), session: request.data.session, bytes: Buffer.byteLength(request.data.content),
} } });
assert.equal(f.modals[0].title, '上传成功'); f.modals.shift().success({ confirm: true });
assert(!f.calls.some(call => call[0] === 'clipboard'));
assert.equal(f.timeouts.size, 0);
}
function fakeEngine() {
const events = {}, calls = [];
class Asset {} class Texture2D extends Asset {} class Prefab extends Asset {}
class Bundle {
constructor() { this.name = 'stage1'; }
load(...args) { calls.push({ receiver: this, args }); return 42; }
releaseAll() { calls.push({ release: this }); return 43; }
}
const cc = { Asset, Texture2D, Prefab, AssetManager: { Bundle }, ENGINE_VERSION: '2.4.15', game: { renderType: 1 },
assetManager: { assets: new Map([['texture', new Texture2D()], ['prefab', new Prefab()]]), loadBundle() {} },
director: { on: (event, fn) => { events[event] = fn; }, getScene: () => ({ name: 'HomeScene', children: [{ children: [] }] }) },
Director: { EVENT_BEFORE_SCENE_LOADING: 'load', EVENT_BEFORE_SCENE_LAUNCH: 'before', EVENT_AFTER_SCENE_LAUNCH: 'after', EVENT_AFTER_UPDATE: 'frame' }, find: () => null };
return { cc, events, calls, Bundle };
}
test('diagnostics default to off before accessing WeChat APIs', () => {
const module = { exports: {} };
vm.runInNewContext(source, { module });
const wx = new Proxy({}, { get() { throw Error('WeChat API accessed while disabled'); } });
for (const enabled of [undefined, false, null, 0, 1, 'true']) {
assert.equal(module.exports.create(wx, {}, enabled).enabled, false);
}
assert.equal(module.exports.create(wx).enabled, false);
});
test('master switch disables diagnostics in every channel without side effects', () => {
for (const env of ['develop', 'trial', 'release']) {
const files = new Map([[ROOT + '1800000000000-abcdefgh.start.jsonl', '{"event":"session.start"}\n']]);
const before = [...files];
const f = fixture(files, env, 1800000010000, false, false), e = fakeEngine(), original = e.Bundle.prototype.load;
let launched = 0, exported = 0;
f.api.attachEngine(e.cc); f.api.record('x'); f.api.beforeBoot(() => launched++); f.api.exportPrevious(() => exported++);
assert.equal(f.api.enabled, false); assert.equal(launched, 1); assert.equal(exported, 1);
assert.equal(e.Bundle.prototype.load, original); assert.deepEqual([...files], before);
for (const entries of [f.calls, f.timers, f.modals, f.sheets, f.logins, f.requests, Object.keys(f.handlers), Object.keys(e.events)]) assert.equal(entries.length, 0);
assert.equal(f.timeouts.size, 0);
}
});
test('release/unknown/unavailable channel performs no persistence, listeners, timers or engine patching', () => {
for (const env of ['release', null, 'unknown', 'throws']) {
const f = fixture(new Map(), env), e = fakeEngine(), original = e.Bundle.prototype.load;
f.api.attachEngine(e.cc); f.api.record('x'); let launched = 0; f.api.beforeBoot(() => launched++);
assert.equal(f.api.enabled, false); assert.equal(launched, 1); assert.equal(e.Bundle.prototype.load, original);
assert.equal(f.calls.length, 0); assert.equal(f.timers.length, 0); assert.equal(Object.keys(f.handlers).length, 0);
}
});
test('test sessions persist startup metadata and exact warning counters before restarting', () => {
const f = fixture(), e = fakeEngine(); f.api.attachEngine(e.cc);
f.handlers.onMemoryWarning({ level: 10 });
const start = f.rows().find(r => r.event === 'session.start'), warning = f.rows().find(r => r.event === 'memory.warning');
assert.equal(start.packageVersion, '1.2.3'); assert.equal(start.model, 'iPhone 8 Plus'); assert.equal(start.buildId, 'build-123');
assert.equal(start.requestedHighPerformancePlus, true); assert.match(start.actualRuntimeMode, /unknown/);
assert.equal(warning.detail.count, 1); assert.equal(warning.counts.textures, 1); assert.equal(warning.counts.prefabs, 1);
assert.equal(warning.counts.cacheEntries, 2); assert.equal(warning.counts.sceneNodes, 2);
assert(!JSON.stringify(f.rows()).includes('secret'));
const next = fixture(f.files, 'develop', 1800000010000);
let boot = 0; next.api.beforeBoot(() => boot++);
assert.equal(boot, 0); assert.equal(next.modals.length, 1);
next.modals.shift().success({ confirm: false }); assert.equal(boot, 1);
assert.equal(next.rows().filter(r => r.event === 'memory.warning').length, 1);
});
test('logs stay bounded, preserve startup and newest events, and prune only their own old sessions', () => {
const f = fixture();
f.files.set(ROOT + 'unrelated.txt', 'keep'); f.files.set('/user/activity-bundles/cache.zip', 'keep');
for (let i = 0; i < 4000; i++) f.api.record('activity.page', { index: i, name: '金字塔'.repeat(50), token: 'secret', object: { huge: true } });
const segments = [...f.files].filter(([k]) => /\.[01]\.jsonl$/.test(k));
assert.equal(segments.length, 2); segments.forEach(([, v]) => assert(Buffer.byteLength(v) <= 65536));
assert.equal(f.rows().filter(r => r.event === 'session.start').length, 1);
assert(f.rows().some(r => r.detail?.index === 3999)); assert(!f.rows().some(r => r.detail?.index === 0));
assert(!JSON.stringify(f.rows()).includes('secret'));
for (let i = 1; i <= 7; i++) fixture(f.files, 'trial', 1800000000000 + i * 10000);
assert.equal([...f.files.keys()].filter(k => k.endsWith('.start.jsonl')).length, 5);
assert.equal(f.files.get(ROOT + 'unrelated.txt'), 'keep'); assert.equal(f.files.get('/user/activity-bundles/cache.zip'), 'keep');
});
test('batched asset callbacks flush on warning; disk failure cannot break warning or launch', () => {
const f = fixture(); f.api.record('resource.load.start', { operation: 1 });
assert(!f.rows().some(r => r.event === 'resource.load.start'));
f.handlers.onMemoryWarning({}); assert(f.rows().some(r => r.event === 'resource.load.start'));
f.full(); assert.doesNotThrow(() => f.handlers.onMemoryWarning({}));
assert.doesNotThrow(() => f.tick()); let boot = 0; f.api.beforeBoot(() => boot++); assert.equal(boot, 1);
});
test('initial storage failure leaves the game launchable with no hooks or timer', () => {
const f = fixture(new Map(), 'trial', 1800000000000, true); let boot = 0;
f.api.beforeBoot(() => boot++); assert.equal(boot, 1); assert.equal(f.api.enabled, false);
assert.equal(f.timers.length, 0); assert.equal(Object.keys(f.handlers).length, 0);
});
test('rotated export is chronological and preserves the warning count even after its original event rotates out', () => {
const f = fixture(); f.handlers.onMemoryWarning({});
for (let i = 0; i < 2000; i++) f.api.record('activity.page', { index: i, name: 'x'.repeat(100) });
const next = fixture(f.files, 'trial', 1800000010000); next.api.exportPrevious(); next.sheets.shift().success({ tapIndex: 0 });
const request = startUpload(next), rows = request.data.content.trim().split('\n').map(JSON.parse);
completeUpload(next, request);
assert.equal(rows[0].event, 'session.start'); assert.equal(rows.at(-1).detail.index, 1999);
assert.equal(rows.at(-1).memoryWarnings, 1);
assert(rows.every((row, i) => !i || row.seq > rows[i - 1].seq));
});
test('upload survives cancellation and sends every retained character without using the clipboard', () => {
const old = fixture(); for (let i = 0; i < 80; i++) old.api.record('activity.page', { index: i, name: 'a'.repeat(100) });
const before = new Map(old.files), f = fixture(old.files, 'trial', 1800000010000);
let done = 0; f.api.exportPrevious(() => done++); f.sheets.shift().fail(); assert.equal(done, 1);
f.api.exportPrevious(() => done++); f.sheets.shift().success({ tapIndex: 0 });
f.modals.shift().success({ confirm: false }); assert.equal(done, 2);
f.api.exportPrevious(() => done++); f.sheets.shift().success({ tapIndex: 0 });
assert.equal(done, 2);
assert.equal(f.logins.length, 0); assert.equal(f.requests.length, 0, 'no automatic upload');
const request = startUpload(f), exported = request.data.content;
completeUpload(f, request);
assert.equal(done, 3); assert.equal(exported, [...before.values()].join(''));
const rows = exported.trim().split('\n').map(JSON.parse);
assert.equal(rows[0].session, JSON.parse([...before.values()][0].split('\n')[0]).session);
assert.equal(rows.filter(r => r.event === 'activity.page').length, 80);
assert(rows.every((r, i) => !i || r.seq > rows[i - 1].seq));
for (const [k, v] of before) assert.equal(f.files.get(k), v, 'export does not delete or rewrite the old session');
});
test('upload preserves warning details and a damaged tail, and waits for its receipt before booting', () => {
const old = fixture(), engine = fakeEngine(); old.api.attachEngine(engine.cc);
old.handlers.onMemoryWarning({ level: 10 }); old.tick();
old.api.record('activity.close', { activity: 'pyramid' }, true);
const segment = [...old.files.keys()].find(k => k.endsWith('.0.jsonl'));
old.files.set(segment, old.files.get(segment) + '{partial');
const before = new Map(old.files), f = fixture(old.files, 'trial', 1800000010000);
let boot = 0; f.api.beforeBoot(() => boot++); f.modals.shift().success({ confirm: true });
f.sheets.shift().success({ tapIndex: 0 });
assert.equal(boot, 0);
const queued = f.modals[0]; f.tick(); assert.equal(f.modals[0], queued); assert.equal(boot, 0);
const request = startUpload(f), content = request.data.content;
assert.equal(boot, 0); completeUpload(f, request);
assert.equal(content, [...before.values()].join(''));
assert.match(content, /iPhone 8 Plus/); assert.match(content, /build-123/);
assert.match(content, /"memoryWarnings":1/); assert.match(content, /memory.warning/);
assert.match(content, /"textures":1/); assert.match(content, /"sceneNodes":2/);
assert.match(content, /activity.close/); assert.match(content, /pyramid/); assert(content.endsWith('{partial'));
assert.equal(boot, 1);
for (const [key, value] of before) assert.equal(f.files.get(key), value);
});
test('modal failure or cancellation releases the export lock and continues the game once', () => {
for (const mode of ['fail', 'throw', 'cancel']) {
const old = fixture(), f = fixture(old.files, 'trial', 1800000010000);
const showModal = f.wx.showModal;
if (mode === 'throw') f.wx.showModal = () => { throw Error('native modal failed'); };
let done = 0; f.api.exportPrevious(() => done++); f.sheets.shift().success({ tapIndex: 0 });
if (mode !== 'throw') {
const page = f.modals.shift();
if (mode === 'cancel') page.success({ confirm: false });
page.fail();
}
assert.equal(done, 1);
f.wx.showModal = showModal;
f.api.exportPrevious(() => done++); assert.equal(f.sheets.length, 1); f.sheets.shift().fail(); assert.equal(done, 2);
}
});
test('network failure keeps local bytes and retry gets a fresh login code', () => {
const old = fixture(), before = new Map(old.files), f = fixture(old.files, 'trial', 1800000010000);
let done = 0; f.api.exportPrevious(() => done++); f.sheets.shift().success({ tapIndex: 0 });
const first = startUpload(f); first.fail({ errMsg: 'request:fail' });
assert.equal(f.modals[0].title, '上传未完成'); assert.equal(done, 0);
f.modals.shift().success({ confirm: true });
f.logins.shift().success({ code: 'fresh-code' });
const retry = f.requests.shift(); assert.equal(retry.data.code, 'fresh-code');
assert.equal(retry.data.content, first.data.content);
first.success(undefined); assert.equal(f.modals.length, 0, 'late first response ignored');
completeUpload(f, retry); assert.equal(done, 1);
for (const [k, v] of before) assert.equal(f.files.get(k), v);
});
test('HTTP/business errors and malformed receipts cannot report upload success', () => {
for (const response of [undefined, { statusCode: 503 }, { statusCode: 200, data: { code: 0, errorCode: 'DIAGNOSTICS_DISABLED' } },
{ statusCode: 200, data: { code: 1, data: {} } },
{ statusCode: 200, data: { code: 1, data: { id: 'a'.repeat(24), session: 'wrong', bytes: 1 } } }]) {
const old = fixture(), f = fixture(old.files, 'trial', 1800000010000);
let done = 0; f.api.exportPrevious(() => done++); f.sheets.shift().success({ tapIndex: 0 });
startUpload(f).success(response);
assert.equal(f.modals[0].title, '上传未完成'); f.modals.shift().success({ confirm: false });
assert.equal(done, 1); assert.equal(f.timeouts.size, 0);
}
});
test('login and request timeouts release the game and ignore late callbacks', () => {
for (const stage of ['login', 'request']) {
const old = fixture(), f = fixture(old.files, 'trial', 1800000010000);
let done = 0; f.api.exportPrevious(() => done++); f.sheets.shift().success({ tapIndex: 0 });
f.modals.shift().success({ confirm: true }); const login = f.logins.shift();
if (stage === 'request') login.success({ code: 'test' });
[...f.timeouts.values()][0](); assert.equal(f.modals[0].title, '上传未完成');
if (stage === 'request') assert(f.calls.some(c => c[0] === 'abort'));
f.modals.shift().success({ confirm: false }); assert.equal(done, 1);
if (stage === 'login') { login.success({ code: 'late' }); assert.equal(f.requests.length, 0); }
else f.requests.shift().success({ statusCode: 200, data: { code: 1 } });
assert.equal(f.modals.length, 0); assert.equal(done, 1); assert.equal(f.timeouts.size, 0);
}
});
test('unavailable login/request and unreadable source files leave recovery usable', () => {
for (const stage of ['login', 'request', 'read']) {
const old = fixture(), f = fixture(old.files, 'trial', 1800000010000);
if (stage === 'read') f.wx.getFileSystemManager().readFileSync = () => { throw Error('disk'); };
else f.wx[stage] = () => { throw Error('native unavailable'); };
let done = 0; f.api.exportPrevious(() => done++); f.sheets.shift().success({ tapIndex: 0 });
if (stage !== 'read') {
f.modals.shift().success({ confirm: true });
if (stage === 'request') f.logins.shift().success({ code: 'test' });
assert.equal(f.modals[0].title, '上传未完成'); f.modals.shift().fail();
}
assert.equal(done, 1); assert.equal(f.requests.length, 0);
f.api.exportPrevious(() => done++); f.sheets.shift().fail(); assert.equal(done, 2);
}
});
test('pre-engine export does not launch the game until the user finishes; native UI failure fails open once', () => {
const first = fixture(), f = fixture(first.files, 'trial', 1800000010000); let boot = 0;
f.api.beforeBoot(() => boot++); const prompt = f.modals.shift(); prompt.success({ confirm: true });
assert.equal(boot, 0); f.sheets.shift().fail(); assert.equal(boot, 1); prompt.fail(); assert.equal(boot, 1);
});
test('load observers preserve overloads, callback receiver/results/exceptions, and release semantics', () => {
const f = fixture(), e = fakeEngine(); f.api.attachEngine(e.cc); const b = new e.Bundle();
let received; function complete(...args) { received = { self: this, args }; return 99; }
assert.equal(b.load('prefab/stage', e.cc.Prefab, complete), 42);
assert.equal(e.calls[0].receiver, b); assert.equal(e.calls[0].args[1], e.cc.Prefab);
const asset = {}; const receiver = {}; assert.equal(e.calls[0].args[2].call(receiver, null, asset), 99);
assert.equal(received.self, receiver); assert.equal(received.args[1], asset);
b.load('prefab/stage', e.cc.Prefab); assert.equal(e.calls[1].args[1], e.cc.Prefab, 'asset constructor is not a completion callback');
b.load('prefab/stage', () => { throw Error('user callback'); });
assert.throws(() => e.calls[2].args[1](null, asset), /user callback/);
assert.equal(b.releaseAll(), 43); assert.equal(e.calls[3].release, b);
f.tick(); assert(f.rows().some(r => r.event === 'resource.load.complete' && r.detail.ok));
assert(f.rows().some(r => r.event === 'resource.release.complete'));
});
test('scene hooks and background sampling record meaningful boundaries without counting hidden periodic samples', () => {
const f = fixture(), e = fakeEngine(); f.api.attachEngine(e.cc);
e.events.load('GameScene'); e.events.before({ name: 'GameScene' }); e.events.after();
f.handlers.onHide(); const count = f.rows().length; f.tick(); assert.equal(f.rows().length, count);
f.handlers.onShow(); f.tick(); assert(f.rows().some(r => r.event === 'sample'));
assert(f.rows().some(r => r.event === 'scene.load.start' && r.detail.scene === 'GameScene'));
});
test('frames keep sampling and flushing resources when the interval stops, with one shared cadence', () => {
const f = fixture(), e = fakeEngine(); f.api.attachEngine(e.cc);
assert.equal(f.timers.filter(Boolean).length, 1, 'engine attachment replaces the bootstrap timer');
f.tick(); e.events.frame(); assert.equal(f.rows().filter(r => r.event === 'sample').length, 1);
f.api.record('resource.load.start', { operation: 999 });
f.advance(5000); e.events.frame();
const samples = f.rows().filter(r => r.event === 'sample');
assert.equal(samples.length, 2); assert.equal(samples[1].detail.source, 'frame');
assert.equal(samples[1].detail.timerAgeMs, 5000); assert.equal(samples[1].detail.sampleAgeMs, 5000);
assert(f.rows().some(r => r.detail?.operation === 999));
for (let i = 0; i < 300; i++) e.events.frame();
assert.equal(f.rows().filter(r => r.event === 'sample').length, 2, 'no per-frame cache scans/log writes');
f.advance(20000); e.events.frame();
assert.equal(f.rows().filter(r => r.event === 'sample').at(-1).detail.sampleAgeMs, 20000);
f.handlers.onHide(); const count = f.rows().length;
f.tick(); e.events.frame(); assert.equal(f.rows().length, count);
f.handlers.onShow(); f.handlers.onShow(); assert.equal(f.timers.filter(Boolean).length, 1);
f.tick(); assert.equal(f.rows().at(-1).event, 'sample');
});
test('export dialogs are recorded and do not suppress live samples or mutate the previous log', () => {
const old = fixture(), before = new Map(old.files), f = fixture(old.files, 'trial', 1800000010000);
f.api.exportPrevious(); f.sheets.shift().success({ tapIndex: 0 });
for (let i = 0; i < 6; i++) f.tick();
const samples = f.rows().filter(r => r.event === 'sample');
assert.equal(samples.length, 6); assert(samples.every(r => r.detail.exporting === true));
f.modals.shift().success({ confirm: false }); f.tick();
assert.equal(f.rows().at(-1).detail.exporting, false);
assert(f.rows().some(r => r.event === 'diagnostic.export.start'));
assert(f.rows().some(r => r.event === 'diagnostic.export.finish'));
for (const [key, value] of before) assert.equal(f.files.get(key), value);
});
test('load sources survive asynchronous completion and nested scopes restore even after errors', () => {
const f = fixture(), e = fakeEngine(); f.api.attachEngine(e.cc); const bundle = new e.Bundle();
const receiver = {}; let received;
assert.equal(f.api.withSource('career.city.spine', () => {
bundle.load('spine/action03/skeleton', function (...args) { received = { receiver: this, args }; });
assert.throws(() => f.api.withSource('rank.item.remote', () => { bundle.load('head'); throw Error('user'); }), /user/);
return bundle.load('spine/action09/skeleton');
}), 42);
bundle.load('outside');
f.api.withSource('https://example.com/?token=secret', () => bundle.load('redacted'));
const asset = {}; e.calls[0].args[1].call(receiver, null, asset); f.tick();
const starts = f.rows().filter(r => r.event === 'resource.load.start');
assert.deepEqual(starts.map(r => r.detail.source), ['career.city.spine', 'rank.item.remote', 'career.city.spine', 'unknown', 'unknown']);
assert.equal(f.rows().find(r => r.event === 'resource.load.complete').detail.source, 'career.city.spine');
assert.equal(received.receiver, receiver); assert.equal(received.args[1], asset);
assert(!JSON.stringify(f.rows()).includes('secret'));
});
test('source helper preserves return values and exceptions with diagnostics absent, old or enabled', () => {
const ts = require('typescript');
const compiled = ts.transpileModule(fs.readFileSync('assets/Script/module/Tool/MemoryDiagnosticSource.ts', 'utf8'), {
compilerOptions: { module: ts.ModuleKind.CommonJS, target: ts.ScriptTarget.ES2017 },
}).outputText;
for (const diagnostics of [undefined, { enabled: true }, { enabled: false }, fixture().api]) {
const exports = {}; vm.runInNewContext(compiled, { exports, window: { __memoryDiagnostics: diagnostics } });
let calls = 0; const value = {};
assert.equal(exports.withDiagnosticSource('home.avatar.remote', () => { calls++; return value; }), value);
assert.throws(() => exports.withDiagnosticSource('home.avatar.remote', () => { calls++; throw Error('original'); }), /original/);
assert.equal(calls, 2);
}
});
test('the scene export button is created once and opens the previous-session picker', () => {
const old = fixture(), f = fixture(old.files, 'trial', 1800000010000), e = fakeEngine();
class Node {
constructor(name) { this.name = name; this.children = []; this.handlers = {}; this.width = 750; this.height = 1334; }
addChild(node) { this.children.push(node); }
getChildByName(name) { return this.children.find(n => n.name === name); }
setContentSize() {} setPosition() {} addComponent(Type) { return new Type(); }
on(event, callback) { this.handlers[event] = callback; }
}
Node.EventType = { TOUCH_END: 'touchend' }; const canvas = new Node('Canvas');
Object.assign(e.cc, { Node, Label: class {}, find: () => canvas });
f.api.attachEngine(e.cc); e.events.after(); e.events.after();
assert.equal(canvas.children.length, 1); let stopped = false;
canvas.children[0].handlers.touchend({ stopPropagation() { stopped = true; } });
assert(stopped); assert.equal(f.sheets.length, 1);
});
test('bootstrap keeps the recovery prompt before adapter/engine initialization and the build-hook insertion point', () => {
const boot = fs.readFileSync('build-templates/wechatgame/game.js', 'utf8');
const calls = [], context = { wx: {}, window: { boot: () => calls.push('boot') }, cc: { view: {}, sys: { platform: 1, WECHAT_GAME_SUB: 2 }, macro: {} },
requirePlugin: () => calls.push('engine'), __globalAdapter: { init() {}, adaptEngine() {} },
require: name => name === './memory-diagnostics' ? { create: (wx, build, enabled) => {
assert.equal(enabled, false);
return { enabled: false, beforeBoot: next => { context.resume = next; }, attachEngine() {} };
} } : {} };
vm.runInNewContext(boot, context); assert.deepEqual(calls, []); context.resume(); assert.deepEqual(calls, ['engine', 'boot']);
assert(boot.includes('window.boot();'));
});