381 lines
26 KiB
JavaScript
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();'));
|
|
});
|