Files
youle_cocos/cocoscreator_projects/framework-tests/ui/frame-set.test.ts
T
joywayerandClaude Opus 5 ff346a759f feat(framework): FrameSet 多帧切帧组件(B §3.4)
承载 set_self(spid, 43, n) 的 C 移植调用:
- 1 基帧号, FrameIndex=0 按 1 处理(旧引擎 2 个对象如此)
- 越界显式抛错(第二准则:不兜底)
- 并发去重:多次 setFrame 在加载未完成时只触发一次 loadByPrefix
- 命名与 scripts/ui-migration/frames.mjs:frameFileName 公式逐字对齐

分层:
- core/frame-file-name.ts 纯公式,无 Cocos 依赖,直接单测
- ui/frame-set.ts 核心逻辑,FrameSource/FrameTarget 注入接口隔离 Cocos 类型
- ui/resources-frame-source.ts 生产实现,Resources.loadDir + Auto Atlas 合图过滤

tsconfig.framework.json 排除 resources-frame-source.ts(其含 cc 模块,走 Cocos 编辑器编译)。

103/103 framework tests pass,typecheck exit 0。

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-31 17:50:22 +08:00

128 lines
5.0 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 { FrameSet, type FrameSource, type FrameTarget } from '../../YouleNexus/assets/framework/ui/frame-set.ts';
/**
* Mock FrameSource:按 prefix 返回 SpriteFrame 数组。
* SpriteFrame 是 opaque(任何非 null 对象即可,FrameSet 只引用不深查)。
*/
function makeFrames(prefix: string, count: number): any[] {
return Array.from({ length: count }, (_, i) => ({ __mock: true, name: frameName(prefix, i + 1, count) }));
}
function frameName(prefix: string, i: number, count: number): string {
return count <= 1 ? `${prefix}.png` : `${prefix}_${String(i).padStart(String(count).length, '0')}.png`;
}
/** Mock 加载器:同步、按 prefix 全量返回该 prefix 的所有帧。 */
function mockSource(frames: Map<string, any[]>): FrameSource {
return {
async loadByPrefix(prefix: string) {
return frames.get(prefix) ?? [];
},
};
}
/** Mock 目标:仅记录最后一次 setFrame 调用。 */
function mockTarget(): FrameTarget & { lastFrame: any | null; lastError: Error | null } {
return { lastFrame: null, lastError: null, setSpriteFrame(f) { this.lastFrame = f; } };
}
test('FrameSet 首次 setFrame 触发按 prefix 加载', async () => {
const frames = makeFrames('atlas-login/00014', 12);
const source = mockSource(new Map([['atlas-login/00014', frames]]));
const set = new FrameSet({ prefix: 'atlas-login/00014', source, target: mockTarget() });
const before = (set as any).state;
assert.equal(before, 'idle');
await set.setFrame(1);
assert.equal((set as any).state, 'ready');
});
test('FrameSet setFrame(1) 返回第一帧(与 frameFileName 公式对齐)', async () => {
const frames = makeFrames('atlas-login/00014', 12);
const source = mockSource(new Map([['atlas-login/00014', frames]]));
const target = mockTarget();
const set = new FrameSet({ prefix: 'atlas-login/00014', source, target });
await set.setFrame(1);
assert.equal(target.lastFrame?.name, 'atlas-login/00014_01.png');
});
test('FrameSet setFrame(10) 返回第 10 帧(旧引擎实证:通知=FrameIndex=10)', async () => {
const frames = makeFrames('atlas-login/00014', 12);
const source = mockSource(new Map([['atlas-login/00014', frames]]));
const target = mockTarget();
const set = new FrameSet({ prefix: 'atlas-login/00014', source, target });
await set.setFrame(10);
assert.equal(target.lastFrame?.name, 'atlas-login/00014_10.png');
});
test('FrameSet setFrame(0) 按 1 处理(旧引擎 2 个对象 FrameIndex=0)', async () => {
const frames = makeFrames('atlas-login/00014', 12);
const source = mockSource(new Map([['atlas-login/00014', frames]]));
const target = mockTarget();
const set = new FrameSet({ prefix: 'atlas-login/00014', source, target });
await set.setFrame(0);
assert.equal(target.lastFrame?.name, 'atlas-login/00014_01.png');
});
test('FrameSet 越界帧号显式抛错(第二准则:不兜底)', async () => {
const frames = makeFrames('atlas-login/00014', 12);
const source = mockSource(new Map([['atlas-login/00014', frames]]));
const set = new FrameSet({ prefix: 'atlas-login/00014', source, target: mockTarget() });
await set.setFrame(1); // 先把 state 推到 ready
await assert.rejects(() => set.setFrame(13), (e: Error) => {
return e.message.includes('atlas-login/00014') && e.message.includes('13');
});
});
test('FrameSet 源返回空数组显式抛错(prefix 错配要早暴露)', async () => {
const source = mockSource(new Map()); // 空
const set = new FrameSet({ prefix: 'atlas-login/wrong', source, target: mockTarget() });
await assert.rejects(() => set.setFrame(1), (e: Error) => e.message.includes('atlas-login/wrong'));
});
test('FrameSet 不重复 IO:连续 setFrame 只 loadByPrefix 一次', async () => {
const frames = makeFrames('atlas-login/00014', 12);
let calls = 0;
const source: FrameSource = {
async loadByPrefix(prefix: string) {
calls++;
return frames;
},
};
const target = mockTarget();
const set = new FrameSet({ prefix: 'atlas-login/00014', source, target });
await set.setFrame(1);
await set.setFrame(5);
await set.setFrame(10);
assert.equal(calls, 1, '只首次触发加载');
});
test('FrameSet 并发去重:两次 setFrame(1) 同时调用也只 loadByPrefix 一次', async () => {
const frames = makeFrames('atlas-login/00014', 12);
let calls = 0;
const source: FrameSource = {
async loadByPrefix(prefix: string) {
calls++;
// 模拟异步:制造竞态
await new Promise((r) => setTimeout(r, 5));
return frames;
},
};
const target = mockTarget();
const set = new FrameSet({ prefix: 'atlas-login/00014', source, target });
await Promise.all([set.setFrame(1), set.setFrame(1), set.setFrame(1)]);
assert.equal(calls, 1);
});
test('FrameSet currentFrame 默认 0(与 B §3 cpid=43 初始一致)', () => {
const set = new FrameSet({ prefix: 'atlas-login/00014', source: mockSource(new Map()), target: mockTarget() });
assert.equal(set.currentFrame, 0);
});