feat(ui): restore Logo countdown and gated skip

This commit is contained in:
2026-09-06 00:32:37 +08:00
parent 09e80e5052
commit 84fa739ddd
9 changed files with 415 additions and 27 deletions
@@ -3,7 +3,7 @@ import assert from 'node:assert/strict';
import { readFileSync } from 'node:fs';
import { test } from 'node:test';
import type { PageModel, OverlayModel, PageId } from '../../YouleNexus/assets/framework/presentation/ui-contracts.ts';
import type { ResolveRuntimeConfigOptions } from '../../YouleNexus/assets/framework/config/runtime-config.ts';
import type { ResolveRuntimeConfigOptions, RuntimeConfig } from '../../YouleNexus/assets/framework/config/runtime-config.ts';
function deferred() {
let resolve!: () => void;
@@ -31,7 +31,9 @@ class Socket {
}
async function setup(overrides: { config?: Partial<ResolveRuntimeConfigOptions>; render?: (model: PageModel) => void;
cancelFrame?: () => void; showOverlay?: (model: OverlayModel) => void;
record?: (event: { kind: string; data: unknown }) => void; onBusy?: (busy: boolean) => void } = {}) {
record?: (event: { kind: string; data: unknown }) => void; onBusy?: (busy: boolean) => void;
onConfigResolved?: (config: RuntimeConfig) => void; onResourcesReady?: () => void;
onSocketReady?: (ready: boolean) => void; onStopped?: () => void } = {}) {
const api = await import('../../YouleNexus/assets/scripts/local-platform-login/local-login-host.ts')
.catch(error => assert.fail(`Local login host implementation must exist: ${String(error)}`));
const resources = deferred(), minimum = deferred();
@@ -40,6 +42,8 @@ async function setup(overrides: { config?: Partial<ResolveRuntimeConfigOptions>;
const values = new Map<string, string>(), frames = new Set<() => void>();
let fetches = 0;
const host = api.createLocalLoginHost({
onConfigResolved: overrides.onConfigResolved, onResourcesReady: overrides.onResourcesReady,
onSocketReady: overrides.onSocketReady, onStopped: overrides.onStopped,
configOptions: { hostKind: 'h5', win: {}, search: '?profile=local&version=731',
fetcher: { async fetch() { fetches++; return { player_server_tcp: 'remote.example:3088' }; } }, ...overrides.config },
storage: { getItem: key => values.get(key) ?? null, setItem: (key, value) => { values.set(key, value); } },
@@ -57,6 +61,57 @@ async function setup(overrides: { config?: Partial<ResolveRuntimeConfigOptions>;
return { host, resources, minimum, sockets, pages, overlays, activations, failures, busy, evidence, values, frames, draw, ready, fetches: () => fetches };
}
test('host exposes resolved config and independent socket/resources readiness before display, and stops late events', async () => {
const configs: RuntimeConfig[] = [], socket: boolean[] = []; let resources = 0, stopped = 0;
const h = await setup({ onConfigResolved: config => configs.push(config), onResourcesReady: () => resources++,
onSocketReady: ready => socket.push(ready), onStopped: () => stopped++ });
const starting = h.host.start(); await flush();
assert.equal(configs.length, 1); assert.equal(configs[0].identity.version, 731);
assert.equal(configs[0].remoteConfig.getValue('player_server_tcp'), 'remote.example:3088');
h.sockets[0].open(); await flush(); assert.equal(socket[socket.length - 1], true);
assert.equal(h.host.getSnapshot().ready, false);
h.resources.resolve(); await flush(); assert.equal(resources, 1);
assert.equal(h.host.getSnapshot().ready, false);
h.minimum.resolve(); await starting; h.host.stop(); assert.equal(stopped, 1);
const events = socket.length; h.sockets[0].open(); await flush(); assert.equal(socket.length, events);
});
test('host stop cancels presentation and ignores a resource completion still pending', async () => {
let resources = 0, stopped = 0;
const h = await setup({ onResourcesReady: () => resources++, onStopped: () => stopped++ });
const starting = h.host.start(); const rejection = assert.rejects(starting, /stop|terminal/);
await flush(); h.host.stop(); h.resources.resolve(); h.minimum.resolve();
await rejection; assert.equal(resources, 0); assert.equal(stopped, 1);
});
for (const order of [
['resources', 'socket', 'minimum'], ['resources', 'minimum', 'socket'],
['socket', 'resources', 'minimum'], ['socket', 'minimum', 'resources'],
['minimum', 'resources', 'socket'], ['minimum', 'socket', 'resources'],
] as const) test(`all startup gates remain independent in ${order.join(' -> ')} order`, async t => {
const h = await setup(); t.after(() => h.host.stop());
const starting = h.host.start(); await flush();
for (let index = 0; index < order.length; index++) {
const gate = order[index];
if (gate === 'socket') h.sockets[0].open(); else h[gate].resolve();
await flush();
assert.equal(h.host.getSnapshot().ready, index === order.length - 1);
assert.equal(h.activations.includes('login'), index === order.length - 1);
}
await starting; h.draw(); assert.equal(h.activations.includes('login'), true);
});
test('pending config cannot be bypassed by previously completed resource/display tasks', async t => {
const config = deferred(); let resolved = 0;
const h = await setup({ config: { fetcher: { async fetch() { await config.promise; return {}; } } },
onConfigResolved: () => resolved++ });
t.after(() => h.host.stop()); const starting = h.host.start();
h.resources.resolve(); h.minimum.resolve(); await flush();
assert.equal(resolved, 0); assert.equal(h.sockets.length, 0); assert.equal(h.host.getSnapshot().ready, false);
config.resolve(); await flush(); assert.equal(resolved, 1); assert.equal(h.host.getSnapshot().ready, false);
h.sockets[0].open(); await starting; h.draw(); assert.equal(h.host.getSnapshot().ready, true);
});
for (const missing of ['resources', 'minimum', 'socket'] as const) test(`login waits for ${missing}, and start composes only once`, async t => {
const h = await setup(); t.after(() => h.host.stop());
const starting = h.host.start(); const repeated = h.host.start();