feat(ui): wire Logo native loading progress

This commit is contained in:
2026-09-06 00:00:19 +08:00
parent 41fcd65464
commit 09e80e5052
6 changed files with 421 additions and 86 deletions
@@ -7,6 +7,7 @@ import { EventEmitter } from 'node:events';
class Sprite {}
class UITransform {}
class ProgressBar { progress = 0; }
class Label { string = ''; }
class Node extends EventEmitter {
static EventType = { TOUCH_END: 'touch-end' };
@@ -84,6 +85,53 @@ test('frame request is one shot and real cancellation removes pending draw callb
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);
@@ -116,14 +164,14 @@ test('component rejects non-debug/native before creating host or writing browser
}
});
test('component stop cancels pending draw and timer work and disables spinner updates', async () => {
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 {}, sys: { isNative: false },
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();
@@ -136,10 +184,7 @@ test('component stop cancels pending draw and timer work and disables spinner up
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);
});
@@ -152,7 +197,7 @@ test('minimum display ignores an early timer wake and cancellation clears its re
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 },
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: () => ({}) };
@@ -168,10 +213,10 @@ test('minimum display ignores an early timer wake and cancellation clears its re
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);
const logo = new Node('Layer1_Logo'); const group = new Node('group-1');
const bar = new Node('Spirit254'); bar.components.set(ProgressBar, new ProgressBar());
group.children.push(bar, new Node('Spirit3103'), new Node('Spirit3104'));
logo.children.push(group, new Node('group-32')); component.loadingRoot.children.push(logo);
component.loadingRoot.getComponentsInChildren = () => [];
await component.start(); director.emit('draw'); await Promise.resolve();
return component;