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'); }); });