// @vitest-environment jsdom import { afterEach, describe, expect, test, vi } from 'vitest'; import type { GameCreationAppManifest } from '../../../packages/shared/src/contracts/gameCreationApp'; import { cleanup, createGameCreationAppManifest, createGameCreationAppSeedTasks, ProjectDevelopmentView, React, render, screen, waitFor, } from './appSurface/harness'; const PROJECT_PATH = '/tmp/workbench-code-only-project'; function installResourceCardIntersectionObserver() { class ResourceCardIntersectionObserver { readonly root = null; readonly rootMargin = '160px'; readonly thresholds = [0]; readonly observed = new Set(); 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, }); } /** * 只登记游戏代码的项目:分区轴上没有代码栏目,代码资产(manifest 资产与已完成任务登记 * 的产物)按资产功能分类落入「待归类」。旧四栏目口径会把它们整体排除在可见栏目之外, * 于是资源签名非空、画布已进入分页态,却四栏全空、一张卡都不显示。 */ function createCodeOnlyManifest(): GameCreationAppManifest { const manifest = createGameCreationAppManifest( 'workbench-code-only', '只登记游戏代码的项目', ); manifest.assets = [ { id: 'asset-game-entry', kind: 'code', mediaType: 'text/html', localPath: 'game/index.html', source: { kind: 'generated' }, }, ]; manifest.tasks = createGameCreationAppSeedTasks().map((task) => task.id === 'code-prototype' ? { ...task, status: 'completed' as const, artifacts: ['game/main.js'] } : task, ); return manifest; } function renderWorkbench(manifest: GameCreationAppManifest) { return 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(), }), ); } describe('只登记游戏代码的项目分区', () => { afterEach(() => { cleanup(); }); test('代码资产落在待归类栏目并渲染出卡片,而不是四栏全空', async () => { installResourceCardIntersectionObserver(); renderWorkbench(createCodeOnlyManifest()); // 左侧栏目大纲导航已删除:栏目清单改由「资源总览」缩略卡片承载。 await waitFor(() => expect( document.querySelector('.game-resource-book-thumbnail'), ).not.toBeNull(), ); const thumbnailLabels = Array.from( document.querySelectorAll('.game-resource-book-thumbnail'), (thumbnail) => thumbnail.getAttribute('aria-label'), ); expect(thumbnailLabels).toContain('打开待归类'); // 项目版本仍然是末尾独立栏目,代码栏目不再存在。 expect(thumbnailLabels).toContain('打开项目版本'); expect(thumbnailLabels.some((label) => label?.includes('游戏代码'))).toBe( false, ); // 默认停留在顺序里第一个非空栏目,也就是代码资产所在的「待归类」。 expect( await screen.findByRole('region', { name: '待归类' }), ).not.toBeNull(); // manifest 资产与已完成任务的代码产物都必须真的渲染成卡片。 await screen.findByRole('button', { name: /选中资源:待归类 index\.html/, }); await screen.findByRole('button', { name: /选中资源:待归类 main\.js/, }); }); });