Files
esengine/packages/core/tests/Plugins/DebugPlugin.test.ts
YHH a716d8006c fix(build): 修复 Web 构建组件注册和用户脚本打包问题 (#302)
* refactor(build): 重构 Web 构建管线,支持配置驱动的 Import Maps

- 重构 WebBuildPipeline 支持 split-bundles 和 single-bundle 两种构建模式
- 使用 module.json 的 isCore 字段识别核心模块,消除硬编码列表
- 动态生成 Import Map,从模块清单的 name 字段获取包名映射
- 动态扫描 module.json 文件,不再依赖固定模块列表
- 添加 HTTP 服务器启动脚本 (start-server.bat/sh) 支持 ESM 模块
- 更新 BuildSettingsPanel UI 支持新的构建模式选项
- 添加多语言支持 (zh/en/es)

* fix(build): 修复 Web 构建组件注册和用户脚本打包问题

主要修复:
- 修复组件反序列化时找不到类型的问题
- @ECSComponent 装饰器现在自动注册到 ComponentRegistry
- 添加未使用装饰器的组件警告
- 构建管线自动扫描用户脚本(无需入口文件)

架构改进:
- 解决 Decorators ↔ ComponentRegistry 循环依赖
- 新建 ComponentTypeUtils.ts 作为底层无依赖模块
- 移除冗余的防御性 register 调用
- 统一 ComponentType 定义位置

* refactor(build): 统一 WASM 配置架构,移除硬编码

- 新增 wasmConfig 统一配置替代 wasmPaths/wasmBindings
- wasmConfig.files 支持多候选源路径和明确目标路径
- wasmConfig.runtimePath 指定运行时加载路径
- 重构 _copyWasmFiles 使用统一配置
- HTML 生成使用配置中的 runtimePath
- 移除 physics-rapier2d 的冗余 WASM 配置(由 rapier2d 负责)
- IBuildFileSystem 新增 deleteFile 方法

* feat(build): 单文件构建模式完善和场景配置驱动

## 主要改动

### 单文件构建(single-file mode)
- 修复 WASM 初始化问题,支持 initSync 同步初始化
- 配置驱动的 WASM 识别,通过 wasmConfig.isEngineCore 标识核心引擎模块
- 从 wasmConfig.files 动态获取 JS 绑定路径,消除硬编码

### 场景配置
- 构建验证:必须选择至少一个场景才能构建
- 自动扫描:项目加载时扫描 scenes 目录
- 抽取 _filterScenesByWhitelist 公共方法统一过滤逻辑

### 构建面板优化
- availableScenes prop 传递场景列表
- 场景复选框可点击切换启用状态
- 移除动态 import,使用 prop 传入数据

* chore(build): 补充构建相关的辅助改动

- 添加 BuildFileSystemService 的 listFilesByExtension 优化
- 更新 module.json 添加 externalDependencies 配置
- BrowserRuntime 支持 wasmModule 参数传递
- GameRuntime 添加 loadSceneFromData 方法
- Rust 构建命令更新
- 国际化文案更新

* feat(build): 持久化构建设置到项目配置

## 设计架构

### ProjectService 扩展
- 新增 BuildSettingsConfig 接口定义构建配置字段
- ProjectConfig 添加 buildSettings 字段
- 新增 getBuildSettings / updateBuildSettings 方法

### BuildSettingsPanel
- 组件挂载时从 projectService 加载已保存配置
- 设置变化时自动保存(500ms 防抖)
- 场景选择状态与项目配置同步

### 配置保存位置
保存在项目的 ecs-editor.config.json 中:
- scenes: 选中的场景列表
- buildMode: 构建模式
- companyName/productName/version: 产品信息
- developmentBuild/sourceMap: 构建选项

* fix(editor): Ctrl+S 仅在主编辑区域触发保存场景

- 模态窗口打开时跳过(构建设置、设置、关于等)
- 焦点在 input/textarea/contenteditable 时跳过

* fix(tests): 修复 ECS 测试中 Component 注册问题

- 为所有测试 Component 类添加 @ECSComponent 装饰器
- 移除 beforeEach 中的 ComponentRegistry.reset() 调用
- 将内联 Component 类移到文件顶层以支持装饰器
- 更新测试预期值匹配新的组件类型名称
- 添加缺失的 HierarchyComponent 导入

所有 1388 个测试现已通过。
2025-12-10 18:23:29 +08:00

363 lines
12 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 { Core } from '../../src/Core';
import { World } from '../../src/ECS/World';
import { Scene } from '../../src/ECS/Scene';
import { Component } from '../../src/ECS/Component';
import { Matcher } from '../../src/ECS/Utils/Matcher';
import { DebugPlugin } from '../../src/Plugins/DebugPlugin';
import { Injectable } from '../../src/Core/DI';
import { ECSSystem, ECSComponent } from '../../src/ECS/Decorators';
import { EntitySystem } from '../../src/ECS/Systems/EntitySystem';
@ECSComponent('Debug_HealthComponent')
class HealthComponent extends Component {
public health: number = 100;
public maxHealth: number = 100;
}
@ECSComponent('Debug_PositionComponent')
class PositionComponent extends Component {
public x: number = 0;
public y: number = 0;
}
@Injectable()
@ECSSystem('TestSystem', { updateOrder: 10 })
class TestSystem extends EntitySystem {
constructor() {
super(Matcher.empty().all(PositionComponent));
}
protected override process(entities: readonly import('../../src/ECS/Entity').Entity[]): void {
// 模拟处理逻辑
}
}
describe('DebugPlugin', () => {
let core: Core;
let world: World;
let scene: Scene;
let debugPlugin: DebugPlugin;
beforeEach(() => {
core = Core.create({ debug: false });
world = Core.worldManager.createWorld('test-world', { name: 'test-world' });
scene = world.createScene('test-scene');
world.setSceneActive('test-scene', true);
world.start();
debugPlugin = new DebugPlugin({ autoStart: false, updateInterval: 1000 });
});
afterEach(() => {
debugPlugin.stop();
Core.destroy();
});
describe('基本功能', () => {
it('应该能够安装插件', async () => {
await Core.installPlugin(debugPlugin);
expect(Core.isPluginInstalled('@esengine/debug-plugin')).toBe(true);
});
it('应该能够卸载插件', async () => {
await Core.installPlugin(debugPlugin);
await Core.uninstallPlugin('@esengine/debug-plugin');
expect(Core.isPluginInstalled('@esengine/debug-plugin')).toBe(false);
});
it('应该能够获取插件信息', async () => {
await Core.installPlugin(debugPlugin);
const plugin = Core.getPlugin('@esengine/debug-plugin');
expect(plugin).toBeDefined();
expect(plugin?.name).toBe('@esengine/debug-plugin');
expect(plugin?.version).toBe('1.0.0');
});
});
describe('统计信息', () => {
beforeEach(async () => {
await Core.installPlugin(debugPlugin);
});
it('应该能够获取 ECS 统计信息', () => {
const entity1 = scene.createEntity('Entity1');
entity1.addComponent(new PositionComponent());
const entity2 = scene.createEntity('Entity2');
entity2.addComponent(new HealthComponent());
const stats = debugPlugin.getStats();
expect(stats).toBeDefined();
expect(stats.totalEntities).toBe(2);
expect(stats.scenes.length).toBe(1);
expect(stats.scenes[0].name).toBe('test-scene');
expect(stats.scenes[0].entityCount).toBe(2);
});
it('应该能够获取场景信息', () => {
const entity = scene.createEntity('TestEntity');
entity.addComponent(new PositionComponent());
entity.addComponent(new HealthComponent());
scene.registerSystems([TestSystem]);
const sceneInfo = debugPlugin.getSceneInfo(scene);
expect(sceneInfo.name).toBe('test-scene');
expect(sceneInfo.entityCount).toBe(1);
expect(sceneInfo.systems.length).toBeGreaterThan(0);
expect(sceneInfo.entities.length).toBe(1);
});
it('应该能够获取实体详细信息', () => {
const entity = scene.createEntity('PlayerEntity');
entity.tag = 1;
entity.addComponent(new PositionComponent());
entity.addComponent(new HealthComponent());
const entityInfo = debugPlugin.getEntityInfo(entity);
expect(entityInfo.name).toBe('PlayerEntity');
expect(entityInfo.tag).toBe(1);
expect(entityInfo.enabled).toBe(true);
expect(entityInfo.componentCount).toBe(2);
expect(entityInfo.components.length).toBe(2);
const componentTypes = entityInfo.components.map(c => c.type);
expect(componentTypes).toContain('PositionComponent');
expect(componentTypes).toContain('HealthComponent');
});
it('应该能够获取组件数据', () => {
const entity = scene.createEntity('TestEntity');
const position = new PositionComponent();
position.x = 100;
position.y = 200;
entity.addComponent(position);
const entityInfo = debugPlugin.getEntityInfo(entity);
const positionInfo = entityInfo.components.find(c => c.type === 'PositionComponent');
expect(positionInfo).toBeDefined();
expect(positionInfo?.data.x).toBe(100);
expect(positionInfo?.data.y).toBe(200);
});
});
describe('实体查询', () => {
beforeEach(async () => {
await Core.installPlugin(debugPlugin);
const entity1 = scene.createEntity('Player');
entity1.tag = 1;
entity1.addComponent(new PositionComponent());
entity1.addComponent(new HealthComponent());
const entity2 = scene.createEntity('Enemy');
entity2.tag = 2;
entity2.addComponent(new PositionComponent());
const entity3 = scene.createEntity('Item');
entity3.tag = 3;
entity3.addComponent(new HealthComponent());
});
it('应该能够按 tag 查询实体', () => {
const results = debugPlugin.queryEntities({ tag: 1 });
expect(results.length).toBe(1);
expect(results[0].name).toBe('Player');
expect(results[0].tag).toBe(1);
});
it('应该能够按名称查询实体', () => {
const results = debugPlugin.queryEntities({ name: 'Player' });
expect(results.length).toBe(1);
expect(results[0].name).toBe('Player');
});
it('应该能够按组件查询实体', () => {
const results = debugPlugin.queryEntities({ hasComponent: 'PositionComponent' });
expect(results.length).toBe(2);
expect(results.map(r => r.name)).toContain('Player');
expect(results.map(r => r.name)).toContain('Enemy');
});
it('应该能够组合多个过滤条件', () => {
const results = debugPlugin.queryEntities({
tag: 1,
hasComponent: 'HealthComponent'
});
expect(results.length).toBe(1);
expect(results[0].name).toBe('Player');
});
it('应该在没有匹配时返回空数组', () => {
const results = debugPlugin.queryEntities({ tag: 999 });
expect(results.length).toBe(0);
});
it('应该能够按 sceneName 过滤实体', () => {
// 查询特定场景的实体
const results = debugPlugin.queryEntities({ sceneName: 'test-scene' });
// 应该返回 test-scene 中的所有实体Player, Enemy, Item
expect(results.length).toBe(3);
});
});
describe('监控功能', () => {
beforeEach(async () => {
await Core.installPlugin(debugPlugin);
});
it('应该能够启动监控', () => {
debugPlugin.start();
expect(debugPlugin['updateTimer']).not.toBeNull();
});
it('应该能够停止监控', () => {
debugPlugin.start();
debugPlugin.stop();
expect(debugPlugin['updateTimer']).toBeNull();
});
it('应该防止重复启动', () => {
debugPlugin.start();
const timer1 = debugPlugin['updateTimer'];
debugPlugin.start();
const timer2 = debugPlugin['updateTimer'];
expect(timer1).toBe(timer2);
debugPlugin.stop();
});
it('应该支持自动启动', async () => {
await Core.uninstallPlugin('@esengine/debug-plugin');
const autoPlugin = new DebugPlugin({ autoStart: true, updateInterval: 100 });
await Core.installPlugin(autoPlugin);
expect(autoPlugin['updateTimer']).not.toBeNull();
autoPlugin.stop();
});
});
describe('数据导出', () => {
beforeEach(async () => {
await Core.installPlugin(debugPlugin);
});
it('应该能够导出 JSON 格式数据', () => {
const entity = scene.createEntity('TestEntity');
entity.addComponent(new PositionComponent());
const json = debugPlugin.exportJSON();
expect(json).toBeDefined();
expect(typeof json).toBe('string');
const data = JSON.parse(json);
expect(data.totalEntities).toBe(1);
expect(data.scenes).toBeDefined();
expect(data.timestamp).toBeDefined();
});
it('导出的 JSON 应该包含完整的实体信息', () => {
const entity = scene.createEntity('ComplexEntity');
const position = new PositionComponent();
position.x = 50;
position.y = 75;
entity.addComponent(position);
const json = debugPlugin.exportJSON();
const data = JSON.parse(json);
const entityData = data.scenes[0].entities[0];
expect(entityData.name).toBe('ComplexEntity');
expect(entityData.components[0].data.x).toBe(50);
expect(entityData.components[0].data.y).toBe(75);
});
});
describe('性能监控', () => {
beforeEach(async () => {
await Core.installPlugin(debugPlugin);
scene.registerSystems([TestSystem]);
});
it('应该能够获取 System 性能数据', () => {
scene.createEntity('E1').addComponent(new PositionComponent());
scene.createEntity('E2').addComponent(new PositionComponent());
scene.update();
scene.update();
scene.update();
const sceneInfo = debugPlugin.getSceneInfo(scene);
const systemInfo = sceneInfo.systems.find(s => s.name === 'TestSystem');
expect(systemInfo).toBeDefined();
if (systemInfo?.performance) {
expect(systemInfo.performance.totalCalls).toBeGreaterThan(0);
expect(systemInfo.performance.avgExecutionTime).toBeGreaterThanOrEqual(0);
}
});
it('应该记录 System 的实体数量', () => {
scene.createEntity('E1').addComponent(new PositionComponent());
scene.createEntity('E2').addComponent(new PositionComponent());
scene.createEntity('E3').addComponent(new HealthComponent());
const sceneInfo = debugPlugin.getSceneInfo(scene);
const systemInfo = sceneInfo.systems.find(s => s.name === 'TestSystem');
expect(systemInfo).toBeDefined();
expect(systemInfo?.entityCount).toBe(2);
});
});
describe('错误处理', () => {
it('应该在未安装时抛出错误', () => {
expect(() => {
debugPlugin.getStats();
}).toThrow('Plugin not installed');
});
it('应该在未安装时查询实体抛出错误', () => {
expect(() => {
debugPlugin.queryEntities({ tag: 1 });
}).toThrow('Plugin not installed');
});
it('应该处理空场景', async () => {
await Core.installPlugin(debugPlugin);
const stats = debugPlugin.getStats();
expect(stats.totalEntities).toBe(0);
expect(stats.totalSystems).toBe(0);
});
it('应该处理没有 World 的情况', async () => {
Core.destroy();
Core.create({ debug: false });
const tempPlugin = new DebugPlugin();
await Core.installPlugin(tempPlugin);
const stats = tempPlugin.getStats();
expect(stats.totalEntities).toBe(0);
expect(stats.scenes.length).toBe(0);
});
});
});