Files
youle_cocos/cocoscreator_projects/framework-tests/platform-login/logo-countdown.test.ts
T

145 lines
7.8 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 ScrollView { scrollToTop() {} stopAutoScroll() {} }
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/platform-login/logo-countdown.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, ScrollView, 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);
});