392 lines
24 KiB
TypeScript
392 lines
24 KiB
TypeScript
import test from 'node:test';
|
||
import assert from 'node:assert/strict';
|
||
import { readFileSync, existsSync } from 'node:fs';
|
||
import { runInNewContext } from 'node:vm';
|
||
import ts from 'typescript';
|
||
import { EventEmitter } from 'node:events';
|
||
import { resolveLaunchWaitTime } from '../../YouleNexus/assets/framework/config/remote-config.ts';
|
||
|
||
class Sprite {}
|
||
class Vec3 { constructor(public x = 0, public y = 0, public z = 0) {} }
|
||
class Widget { enabled = true; updateAlignment() {} }
|
||
class UITransform {
|
||
width = 100; height = 30; anchorX = 0.5; anchorY = 0.5;
|
||
convertToWorldSpaceAR(value: Vec3) { return new Vec3(value.x + 250, value.y + 70, value.z); }
|
||
convertToNodeSpaceAR(value: Vec3) { return new Vec3(value.x - 20, value.y - 10, value.z); }
|
||
}
|
||
class ProgressBar { progress = 0; }
|
||
class Label {
|
||
static Overflow = { NONE: 0 }; overflow = 1;
|
||
string = ''; updateRenderData() {}
|
||
}
|
||
class Node extends EventEmitter {
|
||
static EventType = { TOUCH_END: 'touch-end' };
|
||
active = true;
|
||
children: Node[] = [];
|
||
parent: Node | null = null;
|
||
position = new Vec3();
|
||
setPosition(position: Vec3) { this.position = position; }
|
||
components = new Map<unknown, unknown>();
|
||
constructor(public name = '') { super(); }
|
||
getComponent(type: unknown) { return this.components.get(type) ?? null; }
|
||
getComponentsInChildren(type: unknown): any[] {
|
||
const own = this.getComponent(type);
|
||
return [...(own ? [own] : []), ...this.children.flatMap(child => child.getComponentsInChildren(type))];
|
||
}
|
||
getChildByPath(path: string): Node | null {
|
||
const [head, ...tail] = path.split('/');
|
||
const child = this.children.find(node => node.name === head);
|
||
return child ? (tail.length ? child.getChildByPath(tail.join('/')) : child) : null;
|
||
}
|
||
}
|
||
const director = new EventEmitter();
|
||
function countdownFixture() {
|
||
const { createLogoCountdown } = load();
|
||
assert.equal(typeof createLogoCountdown, 'function');
|
||
let now = 0; let id = 0;
|
||
const timers = new Map<number, { at: number; callback: () => void }>();
|
||
const states: { visible: boolean; text: string }[] = [];
|
||
const gate = createLogoCountdown({ now: () => now,
|
||
schedule: (callback: () => void, ms: number) => {
|
||
const key = ++id; timers.set(key, { at: now + ms, callback }); return () => timers.delete(key);
|
||
}, render: (state: { visible: boolean; text: string }) => states.push(state) });
|
||
const advance = (ms: number) => {
|
||
const end = now + ms;
|
||
while (true) {
|
||
const next = [...timers].filter(([, timer]) => timer.at <= end).sort((a, b) => a[1].at - b[1].at)[0];
|
||
if (!next) break;
|
||
now = next[1].at; timers.delete(next[0]); next[1].callback();
|
||
}
|
||
now = end;
|
||
};
|
||
return { gate, timers, states, advance, last: () => states[states.length - 1]! };
|
||
}
|
||
|
||
test('Logo countdown ticks legacy milliseconds and skip requires config, resource and socket readiness', async () => {
|
||
const f = countdownFixture(); let complete = false; void f.gate.promise.then(() => { complete = true; });
|
||
assert.equal(f.last().visible, false);
|
||
f.gate.skip(); f.gate.start(2500);
|
||
assert.equal(f.last().text, '倒计时3s');
|
||
f.advance(1000); assert.equal(f.last().text, '倒计时2s');
|
||
f.gate.resourcesReady(); assert.equal(f.last().text, '倒计时2s 跳过');
|
||
f.gate.skip(); await Promise.resolve(); assert.equal(complete, false);
|
||
f.gate.socketReady(true); await Promise.resolve(); assert.equal(complete, false, 'premature click must not queue');
|
||
f.gate.skip(); await f.gate.promise;
|
||
assert.equal(f.last().visible, false); assert.equal(f.timers.size, 0);
|
||
});
|
||
|
||
test('Logo exact deadline is independent from resources and socket; terminal cancellation ignores late events', async () => {
|
||
const f = countdownFixture(); f.gate.start(10); f.advance(9);
|
||
assert.equal(f.last().visible, true); f.advance(1); await f.gate.promise;
|
||
assert.equal(f.last().visible, false); assert.equal(f.timers.size, 0);
|
||
const cancelled = countdownFixture(); cancelled.gate.start(5000);
|
||
const rejection = assert.rejects(cancelled.gate.promise, /cancelled/);
|
||
cancelled.gate.cancel(); await rejection;
|
||
const count = cancelled.states.length;
|
||
cancelled.gate.start(9999); cancelled.gate.resourcesReady(); cancelled.gate.socketReady(true); cancelled.gate.skip(); cancelled.advance(10000);
|
||
assert.equal(cancelled.states.length, count); assert.equal(cancelled.timers.size, 0);
|
||
});
|
||
|
||
test('Logo render failure at completion rejects the gate and clears all timers', async () => {
|
||
const { createLogoCountdown } = load(); let now = 0; let failRender = false;
|
||
const callbacks = new Set<() => void>();
|
||
const gate = createLogoCountdown({ now: () => now,
|
||
schedule: (callback: () => void) => { callbacks.add(callback); return () => callbacks.delete(callback); },
|
||
render: () => { if (failRender) throw new Error('label rendering failed'); } });
|
||
gate.start(2500); failRender = true; now = 2500;
|
||
const rejection = assert.rejects(gate.promise, /label rendering failed/);
|
||
assert.doesNotThrow(() => { const callback = [...callbacks][0]; callbacks.delete(callback); callback(); });
|
||
await rejection; assert.equal(callbacks.size, 0);
|
||
});
|
||
|
||
test('Logo scheduling and tick render failures settle pending display promises', async () => {
|
||
const { createLogoCountdown } = load();
|
||
const failed = createLogoCountdown({ now: () => 0, schedule: () => { throw new Error('timer failed'); }, render() {} });
|
||
const scheduling = assert.rejects(failed.promise, /timer failed/);
|
||
assert.doesNotThrow(() => failed.start(2500)); await scheduling;
|
||
let failRender = false; const callbacks = new Set<() => void>();
|
||
const gate = createLogoCountdown({ now: () => 0,
|
||
schedule: (callback: () => void) => { callbacks.add(callback); return () => callbacks.delete(callback); },
|
||
render: () => { if (failRender) throw new Error('tick rendering failed'); } });
|
||
gate.start(2500); failRender = true;
|
||
const rendering = assert.rejects(gate.promise, /tick rendering failed/);
|
||
assert.doesNotThrow(() => { const callback = [...callbacks][1]; callbacks.delete(callback); callback(); });
|
||
await rendering; assert.equal(callbacks.size, 0);
|
||
});
|
||
const source = new URL('../../YouleNexus/assets/scripts/local-platform-login/local-login-view.ts', import.meta.url);
|
||
function load() {
|
||
assert.ok(existsSync(source), 'Cocos local login view implementation must exist');
|
||
const exports = {};
|
||
runInNewContext(ts.transpileModule(readFileSync(source, 'utf8'), {
|
||
compilerOptions: { module: ts.ModuleKind.CommonJS, target: ts.ScriptTarget.ES2020 },
|
||
}).outputText, { exports, require: (name: string) => {
|
||
assert.equal(name, 'cc');
|
||
return { Node, Sprite, UITransform, Label, Widget, Vec3, director, Director: { EVENT_AFTER_DRAW: 'draw' } };
|
||
}, Error });
|
||
return exports as any;
|
||
}
|
||
|
||
test('Logo native binding measures label around button center and releases its touch listener', () => {
|
||
const { CocosLogoCountdownView } = load(); assert.equal(typeof CocosLogoCountdownView, 'function');
|
||
const button = new Node('Spirit3103'), text = new Node('Spirit3104'), parent = new Node('group');
|
||
text.parent = parent;
|
||
for (const node of [button, text, parent]) node.components.set(UITransform, new UITransform());
|
||
const label = new Label(); text.components.set(Label, label);
|
||
const widget = new Widget(); text.components.set(Widget, widget);
|
||
let clicks = 0; const view = new CocosLogoCountdownView(button, text, () => clicks++);
|
||
assert.equal(button.active, false); assert.equal(text.active, false);
|
||
view.render({ visible: true, text: '倒计时3s 跳过' });
|
||
assert.equal(label.string, '倒计时3s 跳过'); assert.equal(label.overflow, Label.Overflow.NONE);
|
||
assert.equal(widget.enabled, false); assert.equal(text.position.x, 230); assert.equal(text.position.y, 60);
|
||
button.emit('touch-end'); assert.equal(clicks, 1);
|
||
view.dispose(); assert.equal(button.listenerCount('touch-end'), 0); assert.equal(button.active, false);
|
||
});
|
||
|
||
test('diagnostic stop button is placed below Logo across different parent coordinates without changing its right-side x', () => {
|
||
const { positionDiagnosticStopButtonBelow } = load();
|
||
assert.equal(typeof positionDiagnosticStopButtonBelow, 'function');
|
||
const logo = new Node('Spirit3103'), logoParent = new Node('logo-parent');
|
||
const stop = new Node('StopButton'), canvas = new Node('Canvas');
|
||
logo.parent = logoParent; stop.parent = canvas; stop.position = new Vec3(665, 310, 7);
|
||
const logoTransform = new UITransform();
|
||
Object.assign(logoTransform, { width: 208, height: 59, anchorX: 0.5, anchorY: 0.5 });
|
||
logoTransform.convertToWorldSpaceAR = value => new Vec3(value.x + 671.37, value.y + 311.5, value.z);
|
||
const stopTransform = new UITransform();
|
||
Object.assign(stopTransform, { width: 220, height: 64, anchorX: 0.5, anchorY: 0.5 });
|
||
const canvasTransform = new UITransform();
|
||
canvasTransform.convertToNodeSpaceAR = value => new Vec3(value.x - 100, value.y - 40, value.z);
|
||
logo.components.set(UITransform, logoTransform); stop.components.set(UITransform, stopTransform);
|
||
canvas.components.set(UITransform, canvasTransform);
|
||
const widget = new Widget(); stop.components.set(Widget, widget);
|
||
|
||
positionDiagnosticStopButtonBelow(stop, logo);
|
||
|
||
assert.equal(widget.enabled, false, 'runtime position must not be overwritten by the native Widget');
|
||
assert.equal(stop.position.x, 665); assert.equal(stop.position.z, 7);
|
||
assert.equal(stop.position.y, 198, 'button top is 12 px below the logo bottom in Canvas coordinates');
|
||
});
|
||
function fixture() {
|
||
const refs = { loadingRoot: new Node(), loginRoot: new Node(), resultRoot: new Node(),
|
||
statusOverlay: new Node(), statusLabel: new Label(), resultLabel: new Label() };
|
||
const instance = new Node(); const group = new Node('group-2'); instance.children.push(group);
|
||
for (const name of ['游客登录', '版本显示', '微信登录', '手机登录', 'qq登录', '快速登录',
|
||
'隐藏快速登录-1', '隐藏快速登录-2', '登录中遮罩', '登录头像背景框', '登录中头像',
|
||
'登录中昵称', '登录中动画', '登录中头像框']) {
|
||
const node = new Node(name); group.children.push(node);
|
||
node.components.set(Sprite, new Sprite()); node.components.set(UITransform, new UITransform());
|
||
node.components.set(Label, new Label());
|
||
}
|
||
return { refs, instance, group, visitor: instance.getChildByPath('group-2/游客登录')! };
|
||
}
|
||
test('required binding fails explicitly, including missing Sprite', () => {
|
||
const { CocosLocalLoginView } = load(); const f = fixture();
|
||
const view = new CocosLocalLoginView(f.refs);
|
||
assert.throws(() => view.bindLogin(new Node(), () => {}), /游客登录/);
|
||
f.visitor.components.delete(Sprite);
|
||
assert.throws(() => view.bindLogin(f.instance, () => {}), /Sprite/);
|
||
});
|
||
test('PageModel drives result labels and blocks room presentation', () => {
|
||
const { CocosLocalLoginView } = load(); const f = fixture(); const view = new CocosLocalLoginView(f.refs);
|
||
view.render({ page: 'lobby', self: { playerid: 19, nickname: '测试玩家', bean: 72, avatar: 'unused' } });
|
||
view.activate('lobby');
|
||
assert.equal(f.refs.resultLabel.string, '登录成功\n玩家 ID:19\n昵称:测试玩家\n豆豆:72');
|
||
assert.equal(f.refs.resultRoot.active, true); assert.equal(f.refs.loginRoot.active, false);
|
||
assert.throws(() => view.activate('room'), /room/);
|
||
});
|
||
test('busy mask and readiness gate touch, terminal disposal removes ownership', () => {
|
||
const { CocosLocalLoginView } = load(); const f = fixture(); const view = new CocosLocalLoginView(f.refs);
|
||
let clicks = 0; view.bindLogin(f.instance, () => clicks++); view.setVersion(123);
|
||
assert.equal((f.instance.getChildByPath('group-2/版本显示')!.getComponent(Label) as Label).string, '123');
|
||
assert.equal(f.instance.getChildByPath('group-2/微信登录')!.active, false);
|
||
view.render({ page: 'login', canLogin: false }); view.activate('login'); f.visitor.emit('touch-end');
|
||
assert.equal(clicks, 0);
|
||
view.render({ page: 'login', canLogin: true }); view.setBusy(true); f.visitor.emit('touch-end');
|
||
assert.equal(clicks, 0); assert.equal(f.instance.getChildByPath('group-2/登录中遮罩')!.active, true);
|
||
view.setBusy(false); f.visitor.emit('touch-end'); assert.equal(clicks, 1);
|
||
view.showFailure({ state: 7, msg: '服务器拒绝' }); view.showOverlay({ kind: 'none' });
|
||
assert.match(f.refs.statusLabel.string, /服务器拒绝/);
|
||
f.visitor.emit('touch-end'); assert.equal(clicks, 1);
|
||
view.dispose(); assert.equal(f.visitor.listenerCount('touch-end'), 0);
|
||
});
|
||
test('frame request is one shot and real cancellation removes pending draw callback', () => {
|
||
const { createCocosFrameScheduler } = load(); const scheduler = createCocosFrameScheduler(); let calls = 0;
|
||
const cancel = scheduler.request(() => calls++); cancel(); director.emit('draw'); assert.equal(calls, 0);
|
||
scheduler.request(() => calls++); director.emit('draw'); director.emit('draw'); assert.equal(calls, 1);
|
||
assert.equal(director.listenerCount('draw'), 0);
|
||
});
|
||
|
||
test('asset load task reports validated native loader progress from zero through completion', async () => {
|
||
const { createAssetLoadTask } = load();
|
||
const progress: number[] = [];
|
||
let onProgress!: (completed: number, total: number) => void;
|
||
let onComplete!: (error: unknown, asset?: unknown) => void;
|
||
const task = createAssetLoadTask((progressCallback: typeof onProgress, completeCallback: typeof onComplete) => {
|
||
onProgress = progressCallback; onComplete = completeCallback;
|
||
}, (value: number) => progress.push(value));
|
||
|
||
assert.deepEqual(progress, [0]);
|
||
onProgress(1, 2); onProgress(2, 2); onComplete(null, { uuid: 'login' });
|
||
assert.deepEqual(progress, [0, 0.5, 1]);
|
||
assert.deepEqual(await task.promise, { uuid: 'login' });
|
||
});
|
||
|
||
test('asset load task rejects invalid counts and loader failures without reporting guessed progress', async () => {
|
||
const { createAssetLoadTask } = load();
|
||
for (const invalid of [[0, 0], [-1, 2], [3, 2], [0.5, 2], [1, Number.NaN]]) {
|
||
const progress: number[] = [];
|
||
let onProgress!: (completed: number, total: number) => void;
|
||
const task = createAssetLoadTask((progressCallback: typeof onProgress) => { onProgress = progressCallback; },
|
||
(value: number) => progress.push(value));
|
||
onProgress(invalid[0], invalid[1]);
|
||
await assert.rejects(task.promise, /asset load progress/i);
|
||
assert.deepEqual(progress, [0]);
|
||
}
|
||
|
||
let complete!: (error: unknown, asset?: unknown) => void;
|
||
const failed = createAssetLoadTask((_progress: unknown, callback: typeof complete) => { complete = callback; }, () => {});
|
||
complete(new Error('decoder failed'));
|
||
await assert.rejects(failed.promise, /decoder failed/);
|
||
});
|
||
|
||
test('asset load task cancellation settles pending work and ignores late loader callbacks', async () => {
|
||
const { createAssetLoadTask } = load();
|
||
const progress: number[] = [];
|
||
let onProgress!: (completed: number, total: number) => void;
|
||
let onComplete!: (error: unknown, asset?: unknown) => void;
|
||
const task = createAssetLoadTask((progressCallback: typeof onProgress, completeCallback: typeof onComplete) => {
|
||
onProgress = progressCallback; onComplete = completeCallback;
|
||
}, (value: number) => progress.push(value));
|
||
task.cancel();
|
||
await assert.rejects(task.promise, /cancelled/i);
|
||
onProgress(1, 2); onComplete(null, { uuid: 'late' });
|
||
assert.deepEqual(progress, [0]);
|
||
});
|
||
|
||
test('failure display preserves nested server and cleanup errors', () => {
|
||
const { CocosLocalLoginView } = load(); const f = fixture();
|
||
const view = new CocosLocalLoginView(f.refs);
|
||
view.showFailure({ error: { state: 8, msg: '版本拒绝' }, cleanup: new Error('socket close failed') });
|
||
assert.match(f.refs.statusLabel.string, /版本拒绝/);
|
||
assert.match(f.refs.statusLabel.string, /socket close failed/);
|
||
});
|
||
|
||
test('component rejects non-debug/native before creating host or writing browser storage', async () => {
|
||
const path = new URL('../../YouleNexus/assets/scripts/local-platform-login/LocalPlatformLogin.ts', import.meta.url);
|
||
assert.ok(existsSync(path), 'LocalPlatformLogin component must exist');
|
||
for (const [debug, native] of [[false, false], [true, true]]) {
|
||
let hosts = 0; const exports: any = {};
|
||
class Component {}
|
||
const decorator = (..._args: unknown[]) => (target: unknown) => target;
|
||
runInNewContext(ts.transpileModule(readFileSync(path, 'utf8'), {
|
||
compilerOptions: { module: ts.ModuleKind.CommonJS, target: ts.ScriptTarget.ES2020, experimentalDecorators: true },
|
||
}).outputText, { exports, require: (name: string) => {
|
||
if (name === 'cc/env') return { DEBUG: debug };
|
||
if (name === 'cc') return { Component, Node, Label, Prefab: class {}, sys: { isNative: native },
|
||
_decorator: { ccclass: decorator, property: () => () => {} } };
|
||
if (name.endsWith('local-login-host.ts')) return { createLocalLoginHost: () => { hosts++; } };
|
||
if (name.endsWith('local-login-view.ts')) return load();
|
||
if (name.endsWith('remote-config.ts')) return { resolveLaunchWaitTime };
|
||
if (name.endsWith('remote-config-fetcher.ts')) return { HttpConfigFetcher: class {} };
|
||
throw new Error(`Unexpected dependency ${name}`);
|
||
}, Error });
|
||
const component = new exports.LocalPlatformLogin();
|
||
await assert.rejects(async () => component.start(), /DEBUG.*browser/);
|
||
assert.equal(hosts, 0);
|
||
}
|
||
});
|
||
|
||
test('component stop cancels pending draw and timer work', async () => {
|
||
const path = new URL('../../YouleNexus/assets/scripts/local-platform-login/LocalPlatformLogin.ts', import.meta.url);
|
||
const exports: any = {}; const timers = new Map<number, () => void>(); let timerId = 0;
|
||
runInNewContext(ts.transpileModule(readFileSync(path, 'utf8'), {
|
||
compilerOptions: { module: ts.ModuleKind.CommonJS, target: ts.ScriptTarget.ES2020, experimentalDecorators: true },
|
||
}).outputText, { exports, require: (name: string) => {
|
||
if (name === 'cc/env') return { DEBUG: true };
|
||
if (name === 'cc') return { Component: class {}, Node, Label, Prefab: class {}, ProgressBar, sys: { isNative: false },
|
||
_decorator: { ccclass: () => (target: unknown) => target, property: () => () => {} } };
|
||
if (name.endsWith('local-login-host.ts')) return {};
|
||
if (name.endsWith('local-login-view.ts')) return load();
|
||
if (name.endsWith('remote-config.ts')) return { resolveLaunchWaitTime };
|
||
if (name.endsWith('remote-config-fetcher.ts')) return {};
|
||
throw new Error(`Unexpected dependency ${name}`);
|
||
}, Error, performance: { now: () => 123 }, window: {
|
||
setTimeout: (callback: () => void) => { timers.set(++timerId, callback); return timerId; },
|
||
clearTimeout: (id: number) => timers.delete(id),
|
||
} });
|
||
const component = new exports.LocalPlatformLogin();
|
||
const draw = component.nextDraw(); const timer = component.delay(900);
|
||
const drawRejection = assert.rejects(draw, /cancelled/); const timerRejection = assert.rejects(timer, /cancelled/);
|
||
component.stop(); await Promise.all([drawRejection, timerRejection]);
|
||
assert.equal(timers.size, 0); assert.equal(director.listenerCount('draw'), 0);
|
||
assert.equal(component.getDiagnostics().pendingCount, 0);
|
||
});
|
||
|
||
test('minimum display ignores an early timer wake and cancellation clears its remaining wait', async () => {
|
||
const path = new URL('../../YouleNexus/assets/scripts/local-platform-login/LocalPlatformLogin.ts', import.meta.url);
|
||
const exports: any = {}; const timers = new Map<number, () => void>(); let timerId = 0; let clock = 100;
|
||
let hostOptions: any;
|
||
runInNewContext(ts.transpileModule(readFileSync(path, 'utf8'), {
|
||
compilerOptions: { module: ts.ModuleKind.CommonJS, target: ts.ScriptTarget.ES2020, experimentalDecorators: true },
|
||
}).outputText, { exports, require: (name: string) => {
|
||
if (name === 'cc/env') return { DEBUG: true };
|
||
if (name === 'cc') return { Component: class {}, Node, Label, Sprite, UITransform, ProgressBar, Prefab: class {}, sys: { isNative: false },
|
||
_decorator: { ccclass: () => (target: unknown) => target, property: () => () => {} } };
|
||
if (name.endsWith('local-login-host.ts')) return { createLocalLoginHost: (options: unknown) => {
|
||
hostOptions = options; return { start: async () => {}, stop() {}, getSnapshot: () => ({}) };
|
||
} };
|
||
if (name.endsWith('local-login-view.ts')) return load();
|
||
if (name.endsWith('remote-config.ts')) return { resolveLaunchWaitTime };
|
||
if (name.endsWith('remote-config-fetcher.ts')) return { HttpConfigFetcher: class {} };
|
||
throw new Error(`Unexpected dependency ${name}`);
|
||
}, Error, performance: { now: () => clock }, window: {
|
||
location: { search: '?profile=local' }, localStorage: {},
|
||
setTimeout: (callback: () => void) => { timers.set(++timerId, callback); return timerId; },
|
||
clearTimeout: (id: number) => timers.delete(id),
|
||
} });
|
||
async function started(emitDraw = true) {
|
||
const component = new exports.LocalPlatformLogin(); const f = fixture();
|
||
const stopButton = new Node('StopButton'), canvas = new Node('Canvas');
|
||
stopButton.parent = canvas; stopButton.components.set(UITransform, new UITransform());
|
||
canvas.components.set(UITransform, new UITransform());
|
||
Object.assign(component, f.refs, { stopButton, loadingPrefab: {}, loginPrefab: {} });
|
||
const logo = new Node('Layer1_Logo'); const group = new Node('group-1');
|
||
const bar = new Node('Spirit254'); bar.components.set(ProgressBar, new ProgressBar());
|
||
const button = new Node('Spirit3103'), text = new Node('Spirit3104');
|
||
for (const node of [button, text, group]) node.components.set(UITransform, new UITransform());
|
||
const label = new Label() as Label & { node: Node }; label.node = text; text.components.set(Label, label); text.parent = group;
|
||
group.children.push(bar, button, text);
|
||
logo.children.push(group, new Node('group-32')); component.loadingRoot.children.push(logo);
|
||
component.loadingRoot.getComponentsInChildren = () => [];
|
||
await component.start(); if (emitDraw) director.emit('draw'); await Promise.resolve();
|
||
return component;
|
||
}
|
||
const drain = async () => { for (let i = 0; i < 5; i++) await Promise.resolve(); };
|
||
function wake(at: number) {
|
||
clock = at; const [id, callback] = timers.entries().next().value!;
|
||
timers.delete(id); callback();
|
||
}
|
||
const component = await started(); let completed = false;
|
||
hostOptions.onConfigResolved({ identity: { version: 731 }, remoteConfig: { getValue: (name: string) => name === 'launchWaitTime' ? 500 : null } });
|
||
const minimum = hostOptions.waitForMinimumDisplay().then(() => { completed = true; });
|
||
await drain(); wake(599.8); await drain();
|
||
assert.equal(completed, false, '499.8 ms elapsed must not open minimum display gate');
|
||
assert.equal(component.getDiagnostics().events.some((event: any) => event.kind === 'minimum-display-ready'), false);
|
||
assert.equal(timers.size, 1);
|
||
wake(600); await minimum; assert.equal(completed, true); assert.equal(timers.size, 0);
|
||
component.stop();
|
||
|
||
clock = 1000; const cancelled = await started();
|
||
hostOptions.onConfigResolved({ identity: { version: 731 }, remoteConfig: { getValue: (name: string) => name === 'launchWaitTime' ? 500 : null } });
|
||
const cancelledMinimum = hostOptions.waitForMinimumDisplay();
|
||
const rejection = assert.rejects(cancelledMinimum, /cancelled/);
|
||
await drain(); wake(1499.8); await drain();
|
||
assert.equal(timers.size, 1); cancelled.stop(); await rejection;
|
||
assert.equal(timers.size, 0); assert.equal(cancelled.getDiagnostics().pendingCount, 0);
|
||
|
||
const undrawn = await started(false); let zeroComplete = false;
|
||
hostOptions.onConfigResolved({ identity: { version: 731 }, remoteConfig: { getValue: (name: string) => name === 'hall_config' ? { launchWaitTime: 0 } : null } });
|
||
const zeroMinimum = hostOptions.waitForMinimumDisplay().then(() => { zeroComplete = true; });
|
||
await drain(); assert.equal(zeroComplete, false, 'zero display deadline cannot bypass first native frame');
|
||
director.emit('draw'); await zeroMinimum; assert.equal(zeroComplete, true); undrawn.stop();
|
||
});
|