3d709ea1db
- 合并 feat/agc-canvas-resource-workbench-v3:资源卡改为选中+浮出工具条、支持多选、删除资源详情面板与 resourceEditorRoute - index.tsx 冲突以 C3 新结构为底:保留本包的素材重命名回调,丢弃被 C3 取消的 focusedResource 详情面板派生状态与浮层 JSX - 重命名入口从已删除的详情面板搬到选中工具条 extraActions,与「分类与标签」并列 - check-config.mjs 的 allowlist 保持主干版本(normalize_local_project_raster_resource 一行已删) - 还原 normalize_local_project_raster_resource Tauri 命令与注册:C3 已在前端接回调用方 - C7:新增 GameRunVersionPicker 运行模块右上角版本入口,无版本不渲染 - C7:版本状态收敛到 WorkspaceLauncherShell 一份,透传给资源画布与 @ 面板;换项目或版本消失时清回最新版本 - C7:currentVersionResourceBindingIds 接入 activeVersionId,复用 resolveActiveIterationVersion 口径,资源卡「当前使用」高亮随版本切换 - C7:切换版本时复用既有 onPlay 预览入口重载画面,不新增运行时资源重映射 - 新增 10 条测试覆盖版本判定口径、版本入口与切换、当前使用高亮、素材重命名链路
244 lines
7.2 KiB
TypeScript
244 lines
7.2 KiB
TypeScript
// @vitest-environment jsdom
|
|
import { afterEach, describe, expect, test, vi } from 'vitest';
|
|
|
|
import type { GameCreationAppManifest } from '../../../packages/shared/src/contracts/gameCreationApp';
|
|
import {
|
|
cleanup,
|
|
createGameCreationAppManifest,
|
|
createGameCreationAppSeedTasks,
|
|
fireEvent,
|
|
ProjectDevelopmentView,
|
|
React,
|
|
render,
|
|
screen,
|
|
waitFor,
|
|
within,
|
|
} from './appSurface/harness';
|
|
|
|
const PROJECT_PATH = '/tmp/workbench-version-switch';
|
|
|
|
function installResourceCardIntersectionObserver() {
|
|
class ResourceCardIntersectionObserver {
|
|
readonly root = null;
|
|
readonly rootMargin = '160px';
|
|
readonly thresholds = [0];
|
|
readonly observed = new Set<Element>();
|
|
|
|
constructor(readonly callback: IntersectionObserverCallback) {}
|
|
|
|
observe(element: Element) {
|
|
this.observed.add(element);
|
|
}
|
|
|
|
unobserve(element: Element) {
|
|
this.observed.delete(element);
|
|
}
|
|
|
|
disconnect() {
|
|
this.observed.clear();
|
|
}
|
|
|
|
takeRecords() {
|
|
return [];
|
|
}
|
|
}
|
|
|
|
Object.defineProperty(window, 'IntersectionObserver', {
|
|
configurable: true,
|
|
value: ResourceCardIntersectionObserver,
|
|
});
|
|
}
|
|
|
|
function createVersionedManifest(): GameCreationAppManifest {
|
|
const manifest = createGameCreationAppManifest(
|
|
'workbench-version-switch',
|
|
'版本切换测试',
|
|
);
|
|
// 运行模块的可用性与版本入口无关,这里用一条已完成原型任务把它打开。
|
|
manifest.tasks = createGameCreationAppSeedTasks().map((task) =>
|
|
task.id === 'code-prototype'
|
|
? { ...task, status: 'completed' as const }
|
|
: task,
|
|
);
|
|
manifest.assets = [
|
|
{
|
|
id: 'asset-player',
|
|
kind: 'character',
|
|
mediaType: 'image/png',
|
|
localPath: 'assets/player.png',
|
|
source: { kind: 'generated' },
|
|
},
|
|
{
|
|
id: 'asset-town',
|
|
kind: 'scene',
|
|
mediaType: 'image/png',
|
|
localPath: 'assets/town.png',
|
|
source: { kind: 'generated' },
|
|
},
|
|
];
|
|
manifest.versions = [
|
|
{
|
|
versionId: 'version-root',
|
|
parentVersionId: null,
|
|
projectRevision: 3,
|
|
resourceBindings: [
|
|
{ slotId: 'asset:asset-player', resourceId: 'asset-player' },
|
|
],
|
|
createdReason: 'initial',
|
|
createdAt: 1_760_000_000_000,
|
|
},
|
|
{
|
|
versionId: 'version-child',
|
|
parentVersionId: 'version-root',
|
|
projectRevision: 4,
|
|
resourceBindings: [
|
|
{ slotId: 'asset:asset-town', resourceId: 'asset-town' },
|
|
],
|
|
createdReason: 'agent-revision',
|
|
createdAt: 1_760_003_600_000,
|
|
},
|
|
];
|
|
return manifest;
|
|
}
|
|
|
|
function renderWorkbench(
|
|
manifest: GameCreationAppManifest,
|
|
props: {
|
|
activeVersionId?: string | null;
|
|
onActiveVersionChange?: (versionId: string) => void;
|
|
} = {},
|
|
) {
|
|
const { rerender } = render(
|
|
React.createElement(ProjectDevelopmentView, {
|
|
projectName: manifest.name,
|
|
projectPath: PROJECT_PATH,
|
|
manifest,
|
|
attachments: [],
|
|
recentRunStatus: null,
|
|
recentRunStopReason: null,
|
|
supervisor: React.createElement('div', null, '项目总控'),
|
|
onHomeOpen: vi.fn(),
|
|
onProjectsOpen: vi.fn(),
|
|
onPlay: vi.fn(),
|
|
...props,
|
|
}),
|
|
);
|
|
return {
|
|
rerenderWith(next: Partial<Record<string, unknown>>) {
|
|
rerender(
|
|
React.createElement(ProjectDevelopmentView, {
|
|
projectName: manifest.name,
|
|
projectPath: PROJECT_PATH,
|
|
manifest,
|
|
attachments: [],
|
|
recentRunStatus: null,
|
|
recentRunStopReason: null,
|
|
supervisor: React.createElement('div', null, '项目总控'),
|
|
onHomeOpen: vi.fn(),
|
|
onProjectsOpen: vi.fn(),
|
|
onPlay: vi.fn(),
|
|
...props,
|
|
...next,
|
|
}),
|
|
);
|
|
},
|
|
};
|
|
}
|
|
|
|
async function openArtCategory() {
|
|
fireEvent.click(screen.getByRole('button', { name: '按类型' }));
|
|
const outline = await screen.findByLabelText('资源栏目大纲');
|
|
fireEvent.click(within(outline).getByRole('button', { name: /美术资源/ }));
|
|
await waitFor(() => {
|
|
expect(
|
|
document.querySelector('[data-resource-book-view="child"]'),
|
|
).not.toBeNull();
|
|
});
|
|
}
|
|
|
|
function cardFor(label: string) {
|
|
return screen
|
|
.getByRole('button', { name: `选中资源:美术资源 ${label}` })
|
|
.closest('.game-resource-card');
|
|
}
|
|
|
|
afterEach(() => {
|
|
cleanup();
|
|
});
|
|
|
|
describe('C7 运行模块版本切换', () => {
|
|
test('keeps the version entry hidden without versions and shows the latest one by default', async () => {
|
|
installResourceCardIntersectionObserver();
|
|
const manifest = createVersionedManifest();
|
|
manifest.versions = [];
|
|
const withoutVersions = renderWorkbench(manifest);
|
|
fireEvent.click(screen.getByRole('tab', { name: '运行' }));
|
|
expect(screen.queryByLabelText(/^当前版本:/)).toBeNull();
|
|
|
|
withoutVersions.rerenderWith({ manifest: createVersionedManifest() });
|
|
const trigger = await screen.findByLabelText(/^当前版本:/);
|
|
expect(trigger.getAttribute('aria-label')).toContain('智能体修订');
|
|
});
|
|
|
|
test('switches the current version through the entry and reports it to the host', async () => {
|
|
installResourceCardIntersectionObserver();
|
|
const manifest = createVersionedManifest();
|
|
const onActiveVersionChange = vi.fn();
|
|
renderWorkbench(manifest, { onActiveVersionChange });
|
|
|
|
fireEvent.click(screen.getByRole('tab', { name: '运行' }));
|
|
fireEvent.click(await screen.findByLabelText(/^当前版本:/));
|
|
|
|
const menu = await screen.findByRole('listbox', {
|
|
name: '切换游戏版本',
|
|
});
|
|
expect(within(menu).getAllByRole('option')).toHaveLength(2);
|
|
// 当前版本(最新的那个)已在菜单里标记为选中。
|
|
expect(
|
|
within(menu)
|
|
.getByRole('option', { name: /智能体修订/ })
|
|
.getAttribute('aria-selected'),
|
|
).toBe('true');
|
|
|
|
fireEvent.click(within(menu).getByRole('option', { name: /初始版本/ }));
|
|
expect(onActiveVersionChange).toHaveBeenCalledWith('version-root');
|
|
});
|
|
|
|
test('drives the "current use" card highlight from the active version', async () => {
|
|
installResourceCardIntersectionObserver();
|
|
const manifest = createVersionedManifest();
|
|
const rendered = renderWorkbench(manifest);
|
|
await openArtCategory();
|
|
|
|
// 默认当前版本是 manifest 里最新的那个:只有它绑定的资源算「当前使用」。
|
|
expect(cardFor('town.png')?.classList.contains('is-current-version')).toBe(
|
|
true,
|
|
);
|
|
expect(
|
|
cardFor('player.png')?.classList.contains('is-current-version'),
|
|
).toBe(false);
|
|
|
|
rendered.rerenderWith({ activeVersionId: 'version-root' });
|
|
await waitFor(() => {
|
|
expect(
|
|
cardFor('player.png')?.classList.contains('is-current-version'),
|
|
).toBe(true);
|
|
});
|
|
expect(cardFor('town.png')?.classList.contains('is-current-version')).toBe(
|
|
false,
|
|
);
|
|
|
|
// 选中的版本已经不存在时按空态处理(与 `@` 面板同一口径),不残留旧高亮;
|
|
// 工作台壳会在版本消失时把选择清回「最新版本」。
|
|
rendered.rerenderWith({ activeVersionId: 'version-removed' });
|
|
await waitFor(() => {
|
|
expect(
|
|
cardFor('town.png')?.classList.contains('is-current-version'),
|
|
).toBe(false);
|
|
});
|
|
expect(
|
|
cardFor('player.png')?.classList.contains('is-current-version'),
|
|
).toBe(false);
|
|
});
|
|
});
|