diff --git a/apps/ai-game-creator-shell/src/styles.css b/apps/ai-game-creator-shell/src/styles.css index 76d00c806..084b95721 100644 --- a/apps/ai-game-creator-shell/src/styles.css +++ b/apps/ai-game-creator-shell/src/styles.css @@ -6416,112 +6416,6 @@ iframe.preview-frame { height: 100%; } -.game-resource-outline { - position: absolute; - top: 50%; - left: 12px; - z-index: 4; - display: flex; - flex-direction: column; - gap: 4px; - width: max-content; - max-width: 180px; - max-height: calc(100% - 24px); - padding: 6px; - overflow: auto; - /* 常态就给完整尺寸和 Dock 底板:只留 11px 文字、去掉图标和底色时,用户认不出 - 这是栏目入口。悬停/键盘聚焦仍在同一套 token 上加强(见下方 :hover 规则)。 */ - border: 1px solid var(--platform-subpanel-border); - border-radius: 14px; - background: var(--platform-nav-fill); - box-shadow: 0 6px 18px rgb(112 70 52 / 12%); - opacity: 1; - transform: translateY(-50%); - transform-origin: left center; - transition: - border-color 160ms ease, - opacity 160ms ease, - box-shadow 160ms ease; - backdrop-filter: blur(6px); -} - -.game-resource-outline:hover, -.game-resource-outline:has(> button:focus-visible) { - border-color: var(--platform-nav-active-border); - background: var(--platform-nav-fill); - box-shadow: var(--platform-nav-active-shadow); - opacity: 1; - transform: translateY(-50%); - backdrop-filter: blur(10px); -} - -.game-resource-outline button { - display: grid; - grid-template-columns: 18px minmax(0, 1fr); - align-items: center; - gap: 6px; - min-height: 34px; - padding: 0 9px; - border: 1px solid transparent; - border-radius: 8px; - background: transparent; - color: var(--platform-nav-item-text); - cursor: pointer; - text-align: left; - transition: - background-color 160ms ease, - border-color 160ms ease; -} - -/* 图标与栏目文字常态就在位(上面两条规则),悬停/键盘聚焦只加强 Dock 底板与当前项高亮。 */ -.game-resource-outline button svg { - width: 14px; - min-width: 0; - opacity: 1; - transform: scale(1); - transition: - opacity 120ms ease, - transform 160ms ease; -} - -.game-resource-outline button:hover, -.game-resource-outline button:focus-visible { - background: var(--platform-nav-item-hover-fill); - outline: 0; -} - -.game-resource-outline button.is-active { - border-color: var(--platform-nav-active-border); - background: var(--platform-nav-active-fill); - color: var(--platform-nav-item-text-active); - box-shadow: var(--platform-nav-active-shadow); -} - -.game-resource-outline button strong { - overflow: hidden; - font-size: 12px; - text-overflow: ellipsis; - white-space: nowrap; -} - -.game-resource-outline button .game-resource-outline-label { - position: relative; - overflow: visible; -} - -.game-resource-outline-unread { - position: absolute; - top: -2px; - right: -6px; - width: 6px; - height: 6px; - border: 1px solid var(--platform-nav-fill); - border-radius: 50%; - background: #e5484d; - box-shadow: 0 0 0 1px rgb(229 72 77 / 18%); - pointer-events: none; -} - .game-resource-page { display: grid; grid-template-rows: auto minmax(0, 1fr) auto; diff --git a/apps/ai-game-creator-shell/src/view/project-development/index.tsx b/apps/ai-game-creator-shell/src/view/project-development/index.tsx index 09b1daf1e..1524176a4 100644 --- a/apps/ai-game-creator-shell/src/view/project-development/index.tsx +++ b/apps/ai-game-creator-shell/src/view/project-development/index.tsx @@ -165,7 +165,6 @@ import { initialResourceBookState, isResourceBookAllTarget, RESOURCE_BOOK_ALL_TARGET, - type ResourceBookCategory, resourceBookReducer, type ResourceBookState, type ResourceBookTarget, @@ -1042,81 +1041,21 @@ function ResourceBookTitleBar({ ); } -/** - * 资源栏目大纲。栏目页与「所有资源」页共用同一条入口带:两页都能横向切到任一栏目, - * 只是「所有资源」页在最前面多一颗指向自己的入口。 - */ -function ResourceBookOutlineNav({ - categories, - activeCategory, - unreadCategories, - onOpen, - includeAllTarget = false, -}: { - categories: readonly ResourceBookCategory[]; - activeCategory: ResourceBookTarget | null; - unreadCategories: ReadonlySet; - onOpen: (category: ResourceBookTarget) => void; - includeAllTarget?: boolean; -}) { - const items: ResourceBookTarget[] = includeAllTarget - ? [RESOURCE_BOOK_ALL_TARGET, ...categories] - : [...categories]; - return ( - - ); -} - /** * 「所有资源」页:把每个非空栏目的资源卡摊在同一张滚动网格里。 * - * 复用既有分区机制的三件事:栏目大纲(可横向切栏目)、栏目标题栏(图标 + 文案 + N 项 - * 计数徽标)、以及栏目页同一颗资源卡片渲染器;差别只是这里一次铺开全部栏目, - * 卡片按格子排布而不是画布坐标。网格里的卡片是只读的(不接拖拽), - * 因为它们的坐标不属于任何栏目画布。 + * 复用既有分区机制的两件事:栏目标题栏(图标 + 文案 + N 项计数徽标)、以及栏目页同一颗 + * 资源卡片渲染器;差别只是这里一次铺开全部栏目,卡片按格子排布而不是画布坐标。网格里的 + * 卡片是只读的(不接拖拽),因为它们的坐标不属于任何栏目画布。 */ function ResourceBookAllResourcesPage({ sections, totalCount, - unreadCategories, - onOpen, onCollapse, renderCard, }: { sections: ResourceBookAllResourcesSection[]; totalCount: number; - unreadCategories: ReadonlySet; - onOpen: (category: ResourceBookTarget) => void; onCollapse: () => void; renderCard: ( resource: ProjectResource, @@ -1132,19 +1071,11 @@ function ResourceBookAllResourcesPage({ options?: { draggable?: boolean }, ) => ReactNode; }) { - const outlineCategories = sections.map((section) => section.category); return (
-
(null); - const [unreadResourceCategoryState, setUnreadResourceCategoryState] = - useState<{ - scopeKey: string; - categories: Set; - }>(() => ({ scopeKey: '', categories: new Set() })); - const resourceCategorySnapshotRef = useRef<{ - scopeKey: string; - resourceIdsByCategory: Map>; - } | null>(null); const [selectedResourceIds, setSelectedResourceIds] = useState([]); const [quickEditSourceLayer, setQuickEditSourceLayer] = useState(null); @@ -2173,8 +2095,8 @@ export default function ProjectDevelopmentView({ }, [resources, selectedResourceId]); const normalizedSearch = searchText.trim().toLowerCase(); /** - * 画布可见资源只由搜索框收窄:功能分类筛选条与画布标签筛选已按用户要求移除, - * 分区栏目本身(`categoryOrder` / 栏目大纲 / 栏目标题栏)不受影响。 + * 画布可见资源只由搜索框收窄:功能分类筛选条、画布标签筛选与左侧栏目大纲导航 + * 已按用户要求移除,分区栏目本身(`categoryOrder` / 栏目标题栏 / 总览缩略卡片)不受影响。 */ const visibleResources = useMemo( () => @@ -2220,20 +2142,6 @@ export default function ProjectDevelopmentView({ }), [visibleResourcesByCategory], ); - const resourceIdsByCategory = useMemo( - () => - new Map( - categoryOrder.map((category) => [ - category, - new Set( - (projectResourcesByCategory.get(category) ?? []).map( - (resource) => resource.id, - ), - ), - ]), - ), - [projectResourcesByCategory], - ); const resourceCategoryScopeKey = useMemo( () => JSON.stringify([projectPath, manifest.projectId]), [manifest.projectId, projectPath], @@ -2387,63 +2295,6 @@ export default function ProjectDevelopmentView({ resourceSearchRef.current?.focus(); }, [resourceSearchOpen]); - const viewedResourceCategory = - mode === 'resources' ? activePageCategory : null; - useEffect(() => { - const previousSnapshot = resourceCategorySnapshotRef.current; - if ( - !previousSnapshot || - previousSnapshot.scopeKey !== resourceCategoryScopeKey - ) { - resourceCategorySnapshotRef.current = { - scopeKey: resourceCategoryScopeKey, - resourceIdsByCategory, - }; - setUnreadResourceCategoryState({ - scopeKey: resourceCategoryScopeKey, - categories: new Set(), - }); - return; - } - - const categoriesWithNewResources = categoryOrder.filter((category) => { - if (category === viewedResourceCategory) { - return false; - } - const previousIds = - previousSnapshot.resourceIdsByCategory.get(category) ?? new Set(); - return Array.from(resourceIdsByCategory.get(category) ?? []).some( - (resourceId) => !previousIds.has(resourceId), - ); - }); - resourceCategorySnapshotRef.current = { - scopeKey: resourceCategoryScopeKey, - resourceIdsByCategory, - }; - setUnreadResourceCategoryState((current) => { - const categories = - current.scopeKey === resourceCategoryScopeKey - ? new Set(current.categories) - : new Set(); - let changed = current.scopeKey !== resourceCategoryScopeKey; - if (viewedResourceCategory && categories.delete(viewedResourceCategory)) { - changed = true; - } - categoriesWithNewResources.forEach((category) => { - if (!categories.has(category)) { - categories.add(category); - changed = true; - } - }); - return changed - ? { scopeKey: resourceCategoryScopeKey, categories } - : current; - }); - }, [resourceCategoryScopeKey, resourceIdsByCategory, viewedResourceCategory]); - const unreadResourceCategories = - unreadResourceCategoryState.scopeKey === resourceCategoryScopeKey - ? unreadResourceCategoryState.categories - : new Set(); const nextPageCategory = activePageCategory && resourcePageCategories.length > 1 ? resourcePageCategories[ @@ -6190,12 +6041,6 @@ export default function ProjectDevelopmentView({ 栏目页的分页画布。 */} {resourceBookOpensAllResources ? null : activePageCategory ? (
-
diff --git a/apps/ai-game-creator-shell/tests/appSurface/home.suite.ts b/apps/ai-game-creator-shell/tests/appSurface/home.suite.ts index 81281c4d0..d6ce74581 100644 --- a/apps/ai-game-creator-shell/tests/appSurface/home.suite.ts +++ b/apps/ai-game-creator-shell/tests/appSurface/home.suite.ts @@ -101,10 +101,16 @@ async function openResourceBookCategory(label: string) { 项目版本: 'version', }; const category = categoryByLabel[label]; - const outline = await screen.findByLabelText('资源栏目大纲'); - fireEvent.click( - within(outline).getByRole('button', { name: new RegExp(label) }), - ); + // 左侧栏目大纲导航已按用户要求删除:切栏目走「资源总览」的栏目缩略卡片。 + if (document.querySelector('[data-resource-book-view="child"]')) { + fireEvent.click(await screen.findByRole('button', { name: '收起资源' })); + await waitFor(() => + expect( + document.querySelector('[data-resource-book-view="main"]'), + ).not.toBeNull(), + ); + } + fireEvent.click(await screen.findByRole('button', { name: `打开${label}` })); await waitFor(() => { const manager = document.querySelector('[data-resource-book-view="child"]'); expect(manager).not.toBeNull(); diff --git a/apps/ai-game-creator-shell/tests/appSurface/project-development.suite.ts b/apps/ai-game-creator-shell/tests/appSurface/project-development.suite.ts index 4c291695d..534a29539 100644 --- a/apps/ai-game-creator-shell/tests/appSurface/project-development.suite.ts +++ b/apps/ai-game-creator-shell/tests/appSurface/project-development.suite.ts @@ -168,10 +168,16 @@ async function openResourceBookCategory(label: string) { 项目版本: 'version', }; const category = categoryByLabel[label]; - const outline = await screen.findByLabelText('资源栏目大纲'); - fireEvent.click( - within(outline).getByRole('button', { name: new RegExp(label) }), - ); + // 左侧栏目大纲导航已按用户要求删除:切栏目走「资源总览」的栏目缩略卡片。 + if (document.querySelector('[data-resource-book-view="child"]')) { + fireEvent.click(await screen.findByRole('button', { name: '收起资源' })); + await waitFor(() => + expect( + document.querySelector('[data-resource-book-view="main"]'), + ).not.toBeNull(), + ); + } + fireEvent.click(await screen.findByRole('button', { name: `打开${label}` })); await waitFor(() => { const manager = document.querySelector('[data-resource-book-view="child"]'); expect(manager).not.toBeNull(); @@ -419,10 +425,9 @@ export function registerProjectWorkbenchFoundationTests() { const dependencyViewport = readViewport(artCanvas); expect(dependencyViewport).toBeTruthy(); + // 切换排序模式时栏目页保持挂载:这里换的是「按依赖 / 按类型」各自的 viewport, + // 不需要(也不再有)切栏目动作——旧的栏目大纲入口点在同栏目上是提前返回的空操作。 fireEvent.click(screen.getByRole('button', { name: '按类型' })); - const outline = screen.getByLabelText('资源栏目大纲'); - fireEvent.click(within(outline).getByRole('button', { name: /文档/ })); - await openResourceBookCategory('角色与对象'); const typeCanvas = await screen.findByRole('region', { name: '角色与对象', }); @@ -436,7 +441,6 @@ export function registerProjectWorkbenchFoundationTests() { expect(typeViewport).not.toBe(dependencyViewport); fireEvent.click(screen.getByRole('button', { name: '按依赖' })); - await openResourceBookCategory('角色与对象'); const restoredDependencyCanvas = await screen.findByRole('region', { name: '角色与对象', }); @@ -445,7 +449,6 @@ export function registerProjectWorkbenchFoundationTests() { ); fireEvent.click(screen.getByRole('button', { name: '按类型' })); - await openResourceBookCategory('角色与对象'); const restoredTypeCanvas = await screen.findByRole('region', { name: '角色与对象', }); @@ -456,7 +459,6 @@ export function registerProjectWorkbenchFoundationTests() { fireEvent.click(screen.getByRole('tab', { name: '运行' })); expect(screen.getByRole('region', { name: '运行表现层' })).not.toBeNull(); fireEvent.click(screen.getByRole('tab', { name: '资源管理' })); - await openResourceBookCategory('角色与对象'); const restoredArtCanvas = await screen.findByRole('region', { name: '角色与对象', }); @@ -734,24 +736,25 @@ export function registerProjectWorkbenchFoundationTests() { } window.dispatchEvent(new Event('resize')); - const outline = screen.getByLabelText('资源栏目大纲'); + // 左侧栏目大纲导航已删除:栏目顺序改由「资源总览」缩略卡片承载(第 0 张是「所有资源」)。 expect( - Array.from(outline.querySelectorAll('strong')).map( - (item) => item.textContent, + Array.from( + document.querySelectorAll('.game-resource-book-thumbnail'), + (thumbnail) => thumbnail.getAttribute('aria-label'), ), ).toEqual([ - 'UI 交互', - '角色与对象', - '场景与环境', - '音频', - '文档', - '待归类', - '项目版本', + '打开所有资源', + '打开UI 交互', + '打开角色与对象', + '打开场景与环境', + '打开音频', + '打开文档', + '打开待归类', + '打开项目版本', ]); expect(graphResourceIds).toContain('asset:section-code'); expect(queryResourceSelectButton('section.js')).not.toBeNull(); expect(screen.queryByRole('region', { name: '游戏代码' })).toBeNull(); - expect(outline.querySelectorAll('small')).toHaveLength(0); expect(screen.getByRole('region', { name: '角色与对象' })).not.toBeNull(); expect( screen.getByTestId('resource-dependency-overlay-character'), @@ -780,9 +783,6 @@ export function registerProjectWorkbenchFoundationTests() { document.querySelector('.game-resource-book-transition-layer'), ).toBeNull(); - expect( - within(outline).queryByRole('button', { name: /游戏代码/ }), - ).toBeNull(); expect(screen.queryByRole('region', { name: '游戏代码' })).toBeNull(); const dispatchViewWheel = (deltaY = 160) => { const scene = document.querySelector( @@ -968,9 +968,7 @@ export function registerProjectWorkbenchFoundationTests() { clientX: 100, clientY: 50, }); - fireEvent.click( - within(outline).getByRole('button', { name: /角色与对象/ }), - ); + fireEvent.click(screen.getByRole('button', { name: /^下一页/ })); expect(cardReleasePointerCapture).toHaveBeenCalledWith(91); fireEvent.pointerUp(dragCard!, { pointerId: 91, @@ -1030,9 +1028,7 @@ export function registerProjectWorkbenchFoundationTests() { screen.queryByTestId('resource-dependency-overlay-character'), ).toBeNull(); - fireEvent.click( - within(outline).getByRole('button', { name: /角色与对象/ }), - ); + await openResourceBookCategory('角色与对象'); fireEvent.click(screen.getByRole('button', { name: '按类型' })); const typeCanvas = screen.getByLabelText('资源类型视图') as HTMLDivElement; expect( @@ -1250,46 +1246,17 @@ export function registerProjectWorkbenchFoundationTests() { expect(styleNumber(notices, 'top')).toBeGreaterThanOrEqual(titlebarHeight); }); - it('keeps the outline dock readable without hover', () => { - const styles = readFileSync( - resolve(process.cwd(), 'apps/ai-game-creator-shell/src/styles.css'), - 'utf8', - ); - const outline = styleRuleBody(styles, '\\.game-resource-outline'); - // 常态必须有 Dock 底板且不再整体降透明度/缩小,否则用户认不出这是栏目入口。 - expect(styleNumber(outline, 'opacity')).toBe(1); - expect(outline).not.toMatch(/scale\(/u); - expect(outline).toMatch(/background:\s*var\(--platform-nav-fill\)/u); - expect(outline).toMatch( - /border:\s*1px solid var\(--platform-subpanel-border\)/u, - ); - - // 栏目图标常态就展开:常态把 svg 收成 width: 0、只剩 11px 文字是"认不出"的根因。 - expect(styleRuleBody(styles, '\\.game-resource-outline button')).toMatch( - /grid-template-columns:\s*18px minmax\(0, 1fr\)/u, - ); - const icon = styleRuleBody(styles, '\\.game-resource-outline button svg'); - expect(styleNumber(icon, 'width')).toBe(14); - expect(styleNumber(icon, 'opacity')).toBe(1); - expect( - styleNumber( - styleRuleBody(styles, '\\.game-resource-outline button strong'), - 'font-size', - ), - ).toBe(12); - }); - - it('renders every outline entry with its icon and section label', async () => { + it('renders the resource search overlay as a sibling of the book scene', async () => { const manifest = createGameCreationAppManifest( - 'workbench-outline-dock', - '栏目大纲可读性测试', + 'workbench-search-overlay', + '资源搜索浮层测试', ); manifest.assets = [ { - id: 'outline-art', + id: 'search-overlay-art', kind: 'character', mediaType: 'image/png', - localPath: 'assets/outline-art.png', + localPath: 'assets/search-overlay-art.png', source: { kind: 'generated', taskId: 'art-asset-plan' }, }, ]; @@ -1316,7 +1283,7 @@ export function registerProjectWorkbenchFoundationTests() { render( React.createElement(ProjectDevelopmentView, { projectName: manifest.name, - projectPath: '/tmp/workbench-outline-dock', + projectPath: '/tmp/workbench-search-overlay', manifest, attachments: [], recentRunStatus: null, @@ -1327,36 +1294,6 @@ export function registerProjectWorkbenchFoundationTests() { }), ); - const outline = await screen.findByLabelText('资源栏目大纲'); - const entries = Array.from(outline.querySelectorAll('button')); - expect(entries.length).toBeGreaterThan(0); - // 分区轴固定 7 栏:6 类资产功能分类 + 末尾独立的「项目版本」。 - expect( - entries.map((button) => button.querySelector('strong')?.textContent), - ).toEqual([ - 'UI 交互', - '角色与对象', - '场景与环境', - '音频', - '文档', - '待归类', - '项目版本', - ]); - // 每条栏目都是"图标 + 栏目文字"的完整入口,不是只有一行小字。 - expect( - entries.every( - (button) => - button.querySelector('svg') !== null && - (button.querySelector('strong')?.textContent ?? '').trim().length > 0, - ), - ).toBe(true); - await openResourceBookCategory('角色与对象'); - expect( - within(outline) - .getByRole('button', { name: '角色与对象' }) - .getAttribute('aria-current'), - ).toBe('page'); - // 搜索浮层与栏目标题栏分属两条排布带,这不是 CSS 声明能表达的:画本场景是管理区里 // 铺满整块的绝对定位层,搜索框只要落在场景内部,就会跟着场景走、和栏目标题栏共享 // 同一条水平带。所以这里钉住渲染出来的 DOM 归属关系:浮层是管理区的直接子节点, @@ -1947,7 +1884,7 @@ export function registerProjectWorkbenchFoundationTests() { ); }); - it('marks newly added resources on inactive section tabs until the user opens them', async () => { + it('keeps a code-only project paged into the catch-all section', async () => { const manifest = createGameCreationAppManifest( 'workbench-resource-unread', '资源未读标识测试', @@ -2009,83 +1946,10 @@ export function registerProjectWorkbenchFoundationTests() { const rendered = render( React.createElement(ProjectDevelopmentView, viewProps), ); - const outline = await screen.findByLabelText('资源栏目大纲'); - - expect( - within(outline).queryByRole('button', { name: /有新资源/ }), - ).toBeNull(); - expect( - within(outline) - .getByRole('button', { name: '角色与对象' }) - .getAttribute('aria-current'), - ).toBe('page'); - - const manifestWithCurrentArt = { - ...manifest, - assets: [ - ...manifest.assets, - { - id: 'unread-art-current', - kind: 'character' as const, - mediaType: 'image/png', - localPath: 'assets/current.png', - source: { kind: 'generated' as const }, - }, - ], - }; - rendered.rerender( - React.createElement(ProjectDevelopmentView, { - ...viewProps, - manifest: manifestWithCurrentArt, - }), + // 默认停留在顺序里第一个非空栏目,也就是首个栏目资产所在的「角色与对象」。 + await waitFor(() => + expect(screen.getByRole('region', { name: '角色与对象' })).not.toBeNull(), ); - expect( - within(outline).queryByRole('button', { name: /有新资源/ }), - ).toBeNull(); - - const manifestWithAudio = { - ...manifestWithCurrentArt, - assets: [ - ...manifestWithCurrentArt.assets, - { - id: 'unread-audio-new', - kind: 'background-music' as const, - mediaType: 'audio/mpeg', - localPath: 'assets/new.mp3', - source: { kind: 'generated' as const }, - }, - ], - }; - rendered.rerender( - React.createElement(ProjectDevelopmentView, { - ...viewProps, - manifest: manifestWithAudio, - }), - ); - - const unreadAudioTab = await within(outline).findByRole('button', { - name: '音频,有新资源', - }); - expect( - unreadAudioTab.querySelector('.game-resource-outline-unread'), - ).not.toBeNull(); - expect( - within(outline).queryByRole('button', { - name: '角色与对象,有新资源', - }), - ).toBeNull(); - - fireEvent.click(unreadAudioTab); - await waitFor(() => { - expect( - within(outline) - .getByRole('button', { name: '音频' }) - .getAttribute('aria-current'), - ).toBe('page'); - expect( - within(outline).queryByRole('button', { name: /有新资源/ }), - ).toBeNull(); - }); const nextProjectManifest = createGameCreationAppManifest( 'workbench-resource-unread-next', @@ -2110,7 +1974,6 @@ export function registerProjectWorkbenchFoundationTests() { ); await waitFor(() => { // 只登记游戏代码的项目不再被分区轴排除:画布照常分页,卡片落在「待归类」栏目。 - expect(screen.queryByLabelText('资源栏目大纲')).not.toBeNull(); expect(screen.queryAllByText('暂无已登记资源')).toHaveLength(0); expect( screen @@ -2123,6 +1986,121 @@ export function registerProjectWorkbenchFoundationTests() { }); }); + it('has no outline nav and keeps every section reachable through the remaining entries', async () => { + const manifest = createGameCreationAppManifest( + 'workbench-no-outline-nav', + '无栏目大纲导航测试', + ); + manifest.assets = [ + { + id: 'nav-document', + kind: 'design-document', + mediaType: 'text/markdown', + localPath: 'memory/nav-document.md', + source: { kind: 'generated', taskId: 'design-foundation' }, + }, + { + id: 'nav-art', + kind: 'character', + mediaType: 'image/png', + localPath: 'assets/nav-art.png', + source: { kind: 'generated', taskId: 'art-asset-plan' }, + }, + ]; + const invoke = vi.fn( + async (command: string, args?: Record) => { + if (command === 'read_local_project_resource_graph') { + return resourceGraphForInputs(args); + } + if (command === 'read_local_project_resource_canvas_layout') { + return { + schemaVersion: 'game-creator-resource-layout.v1', + projectId: args?.expectedProjectId, + mode: args?.mode, + revision: 0, + positions: [], + updatedAt: 0, + }; + } + throw new Error(`unexpected invoke ${command}`); + }, + ); + window.__TAURI__ = { core: { invoke } }; + + render( + React.createElement(ProjectDevelopmentView, { + projectName: manifest.name, + projectPath: '/tmp/workbench-no-outline-nav', + manifest, + attachments: [], + recentRunStatus: null, + recentRunStopReason: null, + supervisor: React.createElement('div', null, '项目总控'), + onHomeOpen: vi.fn(), + onProjectsOpen: vi.fn(), + }), + ); + + await waitFor(() => + expect( + document.querySelector('.game-resource-book-thumbnail'), + ).not.toBeNull(), + ); + // 反向守卫:左侧栏目大纲导航整条不再渲染(标签、类名、nav 角色都不出现)。 + expect(screen.queryByLabelText('资源栏目大纲')).toBeNull(); + expect(document.querySelector('.game-resource-outline')).toBeNull(); + expect(document.querySelector('nav[aria-label="资源栏目大纲"]')).toBeNull(); + + // 链路一:总览缩略卡片 → 任意栏目(含空栏目)。 + fireEvent.click(screen.getByRole('button', { name: '打开项目版本' })); + await waitFor(() => + expect( + document.querySelector( + '.game-resource-book-scene-titlebar.is-active[data-resource-book-category="version"]', + ), + ).not.toBeNull(), + ); + expect(screen.getByRole('region', { name: '项目版本' })).not.toBeNull(); + + // 链路二:页内「下一页」→ 顺序里的下一个栏目(项目版本之后回到 UI 交互)。 + expect( + screen.getByRole('button', { name: /下一页\s*UI 交互/ }), + ).not.toBeNull(); + fireEvent.click(screen.getByRole('button', { name: /^下一页/ })); + await waitFor(() => + expect( + document.querySelector( + '.game-resource-book-scene-titlebar.is-active[data-resource-book-category="ui-interaction"]', + ), + ).not.toBeNull(), + ); + expect(screen.getByRole('region', { name: 'UI 交互' })).not.toBeNull(); + + // 链路三:栏目标题栏的「资源总览」→ 回到总览。 + fireEvent.click(screen.getByRole('button', { name: '收起资源' })); + await waitFor(() => + expect( + document.querySelector('[data-resource-book-view="main"]'), + ).not.toBeNull(), + ); + + // 链路四:「所有资源」入口 → 汇总页 → 再回总览,全程没有栏目大纲导航。 + fireEvent.click(screen.getByRole('button', { name: '打开所有资源' })); + await waitFor(() => + expect( + document.querySelector('[data-resource-book-all-host="true"]'), + ).not.toBeNull(), + ); + expect(screen.queryByLabelText('资源栏目大纲')).toBeNull(); + fireEvent.click(screen.getByRole('button', { name: '收起资源' })); + await waitFor(() => + expect( + document.querySelector('[data-resource-book-view="main"]'), + ).not.toBeNull(), + ); + expect(screen.queryByLabelText('资源栏目大纲')).toBeNull(); + }); + it('refreshes resource pages when a same-count manifest update changes category', async () => { const manifest = createGameCreationAppManifest( 'workbench-imported-image-canvas', @@ -2185,10 +2163,6 @@ export function registerProjectWorkbenchFoundationTests() { const rendered = render( React.createElement(ProjectDevelopmentView, viewProps), ); - const outline = await screen.findByLabelText('资源栏目大纲'); - expect( - within(outline).getByRole('button', { name: '待归类' }), - ).not.toBeNull(); const refreshedManifest = { ...manifest, @@ -2213,7 +2187,7 @@ export function registerProjectWorkbenchFoundationTests() { }), ); - fireEvent.click(within(outline).getByRole('button', { name: /UI 交互/ })); + await openResourceBookCategory('UI 交互'); expect( await screen.findByRole('button', { name: '选中资源:UI 交互 local-imported-image.png', @@ -2277,7 +2251,6 @@ export function registerProjectWorkbenchFoundationTests() { ); const assertOverview = () => { - expect(screen.queryByLabelText('资源栏目大纲')).toBeNull(); expect( screen .getByLabelText(/资源(?:依赖|类型)视图/) @@ -2873,11 +2846,7 @@ export function registerProjectWorkbenchFoundationTests() { ); fireEvent.click(screen.getByRole('button', { name: '按类型' })); - fireEvent.click( - within(screen.getByLabelText('资源栏目大纲')).getByRole('button', { - name: /项目版本/, - }), - ); + await openResourceBookCategory('项目版本'); const rootVersionButton = screen.getByRole('button', { name: /选中资源:项目版本 版本 1/, }); @@ -2895,11 +2864,7 @@ export function registerProjectWorkbenchFoundationTests() { expect(childVersionButton.getAttribute('aria-pressed')).toBe('true'); expect(screen.queryByRole('toolbar')).toBeNull(); expect(screen.queryByText('asset:asset-removed')).toBeNull(); - fireEvent.click( - within(screen.getByLabelText('资源栏目大纲')).getByRole('button', { - name: /角色与对象/, - }), - ); + await openResourceBookCategory('角色与对象'); const playerCard = getResourceSelectButton('player.png').closest( '.game-resource-card', ); @@ -8924,11 +8889,6 @@ export function registerProjectAgentStatusTests() { } // 汇总页自己的标题栏也用同一份全量计数。 expect(readCountBadge('.game-resource-all-page')).toBe(allResourcesCount); - // 汇总页复用栏目大纲:第 0 项是「所有资源」,并标成当前页。 - const allOutline = screen.getByLabelText('资源栏目大纲'); - expect( - allOutline.querySelector('[aria-current="page"]')?.textContent?.trim(), - ).toBe('所有资源'); }); /** diff --git a/apps/ai-game-creator-shell/tests/resourceCanvasCodeOnlyProject.test.tsx b/apps/ai-game-creator-shell/tests/resourceCanvasCodeOnlyProject.test.tsx index 0c7329d17..be26bd850 100644 --- a/apps/ai-game-creator-shell/tests/resourceCanvasCodeOnlyProject.test.tsx +++ b/apps/ai-game-creator-shell/tests/resourceCanvasCodeOnlyProject.test.tsx @@ -10,7 +10,7 @@ import { React, render, screen, - within, + waitFor, } from './appSurface/harness'; const PROJECT_PATH = '/tmp/workbench-code-only-project'; @@ -100,14 +100,22 @@ describe('只登记游戏代码的项目分区', () => { installResourceCardIntersectionObserver(); renderWorkbench(createCodeOnlyManifest()); - const outline = await screen.findByLabelText('资源栏目大纲'); - expect( - within(outline).getByRole('button', { name: /待归类/ }), - ).not.toBeNull(); + // 左侧栏目大纲导航已删除:栏目清单改由「资源总览」缩略卡片承载。 + 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( - within(outline).getByRole('button', { name: /项目版本/ }), - ).not.toBeNull(); + expect(thumbnailLabels).toContain('打开项目版本'); + expect(thumbnailLabels.some((label) => label?.includes('游戏代码'))).toBe( + false, + ); // 默认停留在顺序里第一个非空栏目,也就是代码资产所在的「待归类」。 expect( diff --git a/apps/ai-game-creator-shell/tests/resourceRename.test.tsx b/apps/ai-game-creator-shell/tests/resourceRename.test.tsx index 280066dd4..f3aa71ba6 100644 --- a/apps/ai-game-creator-shell/tests/resourceRename.test.tsx +++ b/apps/ai-game-creator-shell/tests/resourceRename.test.tsx @@ -80,8 +80,18 @@ function createManifestWithHero(localPath: string): GameCreationAppManifest { async function openHeroCard(name = 'hero.png') { fireEvent.click(screen.getByRole('button', { name: '按类型' })); - const outline = await screen.findByLabelText('资源栏目大纲'); - fireEvent.click(within(outline).getByRole('button', { name: /角色与对象/ })); + // 左侧栏目大纲导航已按用户要求删除:切栏目走「资源总览」的栏目缩略卡片。 + if (document.querySelector('[data-resource-book-view="child"]')) { + fireEvent.click(await screen.findByRole('button', { name: '收起资源' })); + await waitFor(() => + expect( + document.querySelector('[data-resource-book-view="main"]'), + ).not.toBeNull(), + ); + } + fireEvent.click( + await screen.findByRole('button', { name: '打开角色与对象' }), + ); // 资源画本先切到子画布再渲染卡片,等它落到 character 栏目再找卡。 await waitFor(() => { const manager = document.querySelector('[data-resource-book-view="child"]'); diff --git a/apps/ai-game-creator-shell/tests/resourceVersionSwitch.test.tsx b/apps/ai-game-creator-shell/tests/resourceVersionSwitch.test.tsx index 931034082..f6736c90f 100644 --- a/apps/ai-game-creator-shell/tests/resourceVersionSwitch.test.tsx +++ b/apps/ai-game-creator-shell/tests/resourceVersionSwitch.test.tsx @@ -149,8 +149,18 @@ function renderWorkbench( async function openArtCategory() { fireEvent.click(screen.getByRole('button', { name: '按类型' })); - const outline = await screen.findByLabelText('资源栏目大纲'); - fireEvent.click(within(outline).getByRole('button', { name: /角色与对象/ })); + // 左侧栏目大纲导航已按用户要求删除:切栏目走「资源总览」的栏目缩略卡片。 + if (document.querySelector('[data-resource-book-view="child"]')) { + fireEvent.click(await screen.findByRole('button', { name: '收起资源' })); + await waitFor(() => + expect( + document.querySelector('[data-resource-book-view="main"]'), + ).not.toBeNull(), + ); + } + fireEvent.click( + await screen.findByRole('button', { name: '打开角色与对象' }), + ); await waitFor(() => { expect( document.querySelector('[data-resource-book-view="child"]'),