Files
youle_cocos/cocoscreator_projects/framework-tests/local-platform-login/view.test.ts
T

200 lines
12 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
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';
class Sprite {}
class UITransform {}
class Label { string = ''; }
class Node extends EventEmitter {
static EventType = { TOUCH_END: 'touch-end' };
active = true;
children: Node[] = [];
components = new Map<unknown, unknown>();
constructor(public name = '') { super(); }
getComponent(type: unknown) { return this.components.get(type) ?? null; }
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();
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, director, Director: { EVENT_AFTER_DRAW: 'draw' } };
}, Error });
return exports as any;
}
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('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-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 and disables spinner updates', 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 {}, 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-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.spinner = { angle: 50, activeInHierarchy: true };
component.update(0.5); assert.equal(component.spinner.angle, -40);
component.stop(); await Promise.all([drawRejection, timerRejection]);
component.update(0.5); assert.equal(component.spinner.angle, -40);
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, 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-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() {
const component = new exports.LocalPlatformLogin(); const f = fixture();
Object.assign(component, f.refs, { stopButton: new Node(), loadingPrefab: {}, loginPrefab: {} });
const loading = new Node('Layer614_Loading'); const group = new Node('group-40');
const spinner = new Node('载入动画'); spinner.components.set(Sprite, new Sprite());
spinner.components.set(UITransform, new UITransform()); group.children.push(spinner);
loading.children.push(group); component.loadingRoot.children.push(loading);
component.loadingRoot.getComponentsInChildren = () => [];
await component.start(); 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;
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();
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);
});