6264fed6b4
- 新增匹配纯函数:显式 id、精确路径后缀、文件名、序列帧地址四档,未知值一律丢弃 - 恰好命中一个素材且标签由文件名派生时,芯片名归一化为资源显示名 - 点选处理改为调用该纯函数,派发载荷字段与顺序不变 - 新增匹配与标签归一化的单元用例
279 lines
8.3 KiB
TypeScript
279 lines
8.3 KiB
TypeScript
import { describe, expect, it } from 'vitest';
|
||
|
||
import type { GameCreationAppAssetManifestEntry } from '../../../packages/shared/src/contracts/gameCreationApp';
|
||
import {
|
||
matchRuntimeInspectResources,
|
||
resolveRuntimeInspectLabel,
|
||
} from '../src/features/project-workspace/runtimeInspectResourceMatch';
|
||
|
||
function asset(
|
||
id: string,
|
||
localPath: string,
|
||
frameSrcs: string[] = [],
|
||
): GameCreationAppAssetManifestEntry {
|
||
return {
|
||
id,
|
||
kind: 'character',
|
||
mediaType: 'image/png',
|
||
localPath,
|
||
source: { kind: 'uploaded' },
|
||
imageSequenceFrames: frameSrcs.map((imageSrc) => ({
|
||
imageSrc,
|
||
width: 32,
|
||
height: 32,
|
||
})),
|
||
};
|
||
}
|
||
|
||
const hero = asset('hero', 'assets/hero.png');
|
||
const enemy = asset('enemy', 'assets/enemy.png');
|
||
const manifest = [hero, enemy];
|
||
|
||
describe('运行画面点选 → manifest 素材 id', () => {
|
||
it('显式 id 优先:登记在 manifest 的按输入顺序保留,未知 id 一律丢弃', () => {
|
||
expect(
|
||
matchRuntimeInspectResources({
|
||
assets: manifest,
|
||
resourceIds: ['ghost', 'hero'],
|
||
sourcePath: '/assets/enemy.png',
|
||
}),
|
||
).toEqual({ resourceIds: ['hero', 'enemy'] });
|
||
// 全是未知 id:宁可给空,也不能把值透传给 Rust(未知 id 会让整条引用被拒)。
|
||
expect(
|
||
matchRuntimeInspectResources({
|
||
assets: manifest,
|
||
resourceIds: ['ghost'],
|
||
}),
|
||
).toEqual({ resourceIds: [] });
|
||
});
|
||
|
||
it('精确后缀匹配:深层路径以段边界命中短 localPath', () => {
|
||
expect(
|
||
matchRuntimeInspectResources({
|
||
assets: [asset('hero', 'game/public/assets/hero.png')],
|
||
sourcePath: '/assets/hero.png',
|
||
}),
|
||
).toEqual({ resourceIds: ['hero'] });
|
||
// `/` 与 `\` 都是分隔符,两侧混用同样按段比较(大小写不敏感)。
|
||
expect(
|
||
matchRuntimeInspectResources({
|
||
assets: [asset('hero', 'game\\public\\assets\\hero.png')],
|
||
sourcePath: '/ASSETS/HERO.PNG',
|
||
}),
|
||
).toEqual({ resourceIds: ['hero'] });
|
||
});
|
||
|
||
it('段边界不误配:后缀档先于文件名档,`minigame/hero.png` 不当作 `game/hero.png` 的后缀', () => {
|
||
expect(
|
||
matchRuntimeInspectResources({
|
||
assets: [
|
||
asset('mini', 'minigame/hero.png'),
|
||
asset('game', 'game/hero.png'),
|
||
],
|
||
sourcePath: '/game/hero.png',
|
||
}),
|
||
).toEqual({ resourceIds: ['game', 'mini'] });
|
||
expect(
|
||
matchRuntimeInspectResources({
|
||
assets: [asset('a', 'a/hero.png'), asset('ab', 'ab/hero.png')],
|
||
sourcePath: '/ab/hero.png',
|
||
}),
|
||
).toEqual({ resourceIds: ['ab', 'a'] });
|
||
});
|
||
|
||
it('重名素材命中多条时全部返回,不静默只取一条', () => {
|
||
expect(
|
||
matchRuntimeInspectResources({
|
||
assets: [
|
||
asset('first', 'art/a/hero.png'),
|
||
asset('second', 'art/b/hero.png'),
|
||
],
|
||
sourcePath: '/other/hero.png',
|
||
}),
|
||
).toEqual({ resourceIds: ['first', 'second'] });
|
||
});
|
||
|
||
it('序列帧 `imageSrc` 与 `localPath` 同权参与后缀与文件名匹配', () => {
|
||
const walk = asset('walk', 'assets/hero.png', ['assets/walk/frame_01.png']);
|
||
expect(
|
||
matchRuntimeInspectResources({
|
||
assets: [walk],
|
||
sourcePath: '/game/assets/walk/frame_01.png',
|
||
}),
|
||
).toEqual({ resourceIds: ['walk'] });
|
||
expect(
|
||
matchRuntimeInspectResources({
|
||
assets: [walk],
|
||
sourcePath: '/elsewhere/frame_01.png',
|
||
}),
|
||
).toEqual({ resourceIds: ['walk'] });
|
||
// 序列帧不命中时不能靠 localPath 蹭上:这里文件名与帧地址都不同。
|
||
expect(
|
||
matchRuntimeInspectResources({
|
||
assets: [walk],
|
||
sourcePath: '/assets/other.png',
|
||
}),
|
||
).toEqual({ resourceIds: [] });
|
||
});
|
||
|
||
it('无匹配时返回空数组', () => {
|
||
expect(
|
||
matchRuntimeInspectResources({
|
||
assets: manifest,
|
||
sourcePath: '/assets/missing.png',
|
||
}),
|
||
).toEqual({ resourceIds: [] });
|
||
expect(matchRuntimeInspectResources({ assets: manifest })).toEqual({
|
||
resourceIds: [],
|
||
});
|
||
expect(
|
||
matchRuntimeInspectResources({
|
||
assets: [],
|
||
sourcePath: '/assets/hero.png',
|
||
}),
|
||
).toEqual({ resourceIds: [] });
|
||
});
|
||
|
||
it('显式 id 与路径命中撞上同一素材时去重', () => {
|
||
expect(
|
||
matchRuntimeInspectResources({
|
||
assets: manifest,
|
||
resourceIds: ['hero', 'hero'],
|
||
sourcePath: '/assets/hero.png',
|
||
}),
|
||
).toEqual({ resourceIds: ['hero'] });
|
||
});
|
||
});
|
||
|
||
describe('运行画面点选载荷的 label', () => {
|
||
// 引擎档证据:Phaser 4 loader 走 XHR + blob URL,贴图 URL 读不回来,桥的 label 会落到
|
||
// `texture.key`(`decoy`)或常量 `'canvas'`。唯一命中素材时这两类都要按 manifest 显示名定名。
|
||
it('引擎档(elementTag=canvas)唯一命中素材时,无论 label 是不是文件名派生,都归一化成资源显示名', () => {
|
||
expect(
|
||
resolveRuntimeInspectLabel({
|
||
assets: manifest,
|
||
resourceIds: ['enemy'],
|
||
sourcePath: '/assets/decoy.png',
|
||
label: 'decoy',
|
||
elementTag: 'canvas',
|
||
}),
|
||
).toBe('enemy');
|
||
// 贴图 URL 读不回来(sourcePath 缺失)时同样定名。
|
||
expect(
|
||
resolveRuntimeInspectLabel({
|
||
assets: manifest,
|
||
resourceIds: ['enemy'],
|
||
label: 'decoy',
|
||
elementTag: 'canvas',
|
||
}),
|
||
).toBe('enemy');
|
||
expect(
|
||
resolveRuntimeInspectLabel({
|
||
assets: manifest,
|
||
resourceIds: ['hero'],
|
||
label: 'canvas',
|
||
elementTag: 'canvas',
|
||
}),
|
||
).toBe('hero');
|
||
});
|
||
|
||
it('label 是文件名派生的词干时归一化(含空白折 `-`、大小写取 manifest 的 localPath)', () => {
|
||
expect(
|
||
resolveRuntimeInspectLabel({
|
||
assets: manifest,
|
||
resourceIds: ['hero'],
|
||
sourcePath: '/assets/hero.png',
|
||
label: 'hero',
|
||
elementTag: 'canvas',
|
||
}),
|
||
).toBe('hero');
|
||
expect(
|
||
resolveRuntimeInspectLabel({
|
||
assets: [asset('hero-v2', 'assets/hero v2.png')],
|
||
resourceIds: ['hero-v2'],
|
||
sourcePath: '/assets/hero v2.png',
|
||
label: 'hero v2',
|
||
elementTag: 'canvas',
|
||
}),
|
||
).toBe('hero-v2');
|
||
expect(
|
||
resolveRuntimeInspectLabel({
|
||
assets: manifest,
|
||
resourceIds: ['hero'],
|
||
sourcePath: '/assets/Hero.PNG',
|
||
label: 'Hero',
|
||
elementTag: 'canvas',
|
||
}),
|
||
).toBe('hero');
|
||
// 非 canvas 的选点(DOM 档)恰好在媒体 `src` 上、且 label 逐字等于文件名时,同样归一化。
|
||
expect(
|
||
resolveRuntimeInspectLabel({
|
||
assets: manifest,
|
||
resourceIds: ['hero'],
|
||
sourcePath: '/assets/Hero.PNG',
|
||
label: 'Hero',
|
||
elementTag: 'img',
|
||
}),
|
||
).toBe('hero');
|
||
});
|
||
|
||
it('DOM 档的元素文本 / 标签不是文件名派生时原样保留(DOM 点选行为逐字不变)', () => {
|
||
expect(
|
||
resolveRuntimeInspectLabel({
|
||
assets: manifest,
|
||
resourceIds: ['hero'],
|
||
sourcePath: '/assets/hero.png',
|
||
label: '开始按钮',
|
||
elementTag: 'button',
|
||
}),
|
||
).toBe('开始按钮');
|
||
expect(
|
||
resolveRuntimeInspectLabel({
|
||
assets: manifest,
|
||
resourceIds: ['hero'],
|
||
sourcePath: '/assets/hero.png',
|
||
label: 'img',
|
||
elementTag: 'img',
|
||
}),
|
||
).toBe('img');
|
||
expect(
|
||
resolveRuntimeInspectLabel({
|
||
assets: manifest,
|
||
resourceIds: ['hero'],
|
||
label: 'HUD 区域',
|
||
elementTag: 'div',
|
||
}),
|
||
).toBe('HUD 区域');
|
||
});
|
||
|
||
it('匹配不到唯一素材时不改 label', () => {
|
||
expect(
|
||
resolveRuntimeInspectLabel({
|
||
assets: [asset('first', 'a/hero.png'), asset('second', 'b/hero.png')],
|
||
resourceIds: ['first', 'second'],
|
||
sourcePath: '/other/hero.png',
|
||
label: 'Hero',
|
||
elementTag: 'canvas',
|
||
}),
|
||
).toBe('Hero');
|
||
expect(
|
||
resolveRuntimeInspectLabel({
|
||
assets: manifest,
|
||
resourceIds: [],
|
||
sourcePath: '/assets/hero.png',
|
||
label: 'hero',
|
||
elementTag: 'canvas',
|
||
}),
|
||
).toBe('hero');
|
||
expect(
|
||
resolveRuntimeInspectLabel({
|
||
assets: manifest,
|
||
resourceIds: ['ghost'],
|
||
sourcePath: '/assets/hero.png',
|
||
label: 'hero',
|
||
elementTag: 'canvas',
|
||
}),
|
||
).toBe('hero');
|
||
});
|
||
});
|