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 18737bd92..22c0c5c0f 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 @@ -1260,6 +1260,51 @@ export function registerProjectWorkbenchFoundationTests() { expect(styleNumber(notices, 'top')).toBeGreaterThanOrEqual(titlebarHeight); }); + it('keeps the resource overview grid off fit-content so its columns stay responsive', () => { + const styles = readFileSync( + resolve(process.cwd(), 'apps/ai-game-creator-shell/src/styles.css'), + 'utf8', + ); + // 先剥掉 CSS 注释再取声明体:这条修复的注释里就写着 `width: 100%` 等字样, + // 不剥掉的话断言可能匹配到注释而"假绿"——删除真正的声明也照样通过。 + const declarationsOnly = styles.replace(/\/\*[\s\S]*?\*\//gu, ''); + /** + * 声明级断言:只能钉住「CSS 源文件里写了什么」,**验不到布局**。 + * + * AGC 的 vitest 没开 `css: true`、`styles.css` 不会被加载,jsdom 也没有布局引擎; + * `gridTemplateColumns` 的解析结果与缩略卡的实际列数只能在真机上量。真机判据见下。 + */ + const grid = styleRuleBody( + declarationsOnly, + '\\.game-resource-book-main-grid', + ); + // 父级 `.game-resource-book-main-world` 是列向 flex 容器,交叉轴 auto 外边距会取消 stretch + // 并让本网格按 fit-content 定宽;fit-content 定宽时 auto-fit 的重复次数恒为 1 + // ⇒ grid 只有一张缩略卡那么宽、只出 1 列。`width: 100%` 把 inline size 定下来, + // auto-fit 才会按下面的 max-width 解析重复次数。删掉它 = 资源总览退回纵向长条。 + expect(grid).toMatch(/width:\s*100%/u); + expect(grid).toMatch(/max-width:\s*1120px/u); + expect(grid).toMatch(/margin:\s*0\s*auto/u); + expect(grid).toMatch(/grid-template-columns:\s*repeat\(auto-fit/u); + + // 同一个 flex 列里的标题栏带同款退化(只占

宽度、space-between 无从展开),一并钉住。 + const heading = styleRuleBody( + declarationsOnly, + '\\.game-resource-book-main-heading', + ); + expect(heading).toMatch(/width:\s*100%/u); + expect(heading).toMatch(/max-width:\s*1120px/u); + + /* + * 真机判据(修复后必须同时成立,供人工核对): + * 1) `getComputedStyle(grid).gridTemplateColumns` 给出 3 个非 0 轨道(约 361.33px), + * 而不是单轨道 `432px`; + * 2) grid 的 `getBoundingClientRect().width` 由 432 变成 1120; + * 3) 8 张 `.game-resource-book-thumbnail` 的 `top` 只有 3 个不同取值。 + * 取样要在**总览态**做:栏目态该层带 `is-background`(opacity 为 0),是背景层。 + */ + }); + it('renders the resource search overlay as a sibling of the book scene', async () => { const manifest = createGameCreationAppManifest( 'workbench-search-overlay', @@ -9562,37 +9607,83 @@ export function registerProjectAgentStatusTests() { expect(previewHostRule).toMatch(/position:\s*absolute/); expect(previewHostRule).toMatch(/transform-origin:\s*0 0/); - // 3) 点它进入的视图包含来自多个栏目的资源,而不是某一个栏目。 + // 3) 点它进入的是**同一套画本场景**,不是另一条渲染分支:每个真实栏目铺在自己的分带上, + // 卡片宿主是栏目页那一个 `.game-resource-book-scene-card`、`data-resource-book-category` + // 仍是真实栏目;`all` 自己只留钉住的标题栏与全量计数。 fireEvent.click(screen.getByRole('button', { name: '打开所有资源' })); await waitFor(() => expect( - document.querySelector('[data-resource-book-all-host="true"]'), + document.querySelector( + '.game-resource-book-scene-titlebar[data-resource-book-category="all"].is-active', + ), ).not.toBeNull(), ); + // 旧的分组网格宿主与网格容器一个都不该再存在;改回旧分支时这三条会立刻红。 + expect( + document.querySelector('[data-resource-book-all-host="true"]'), + ).toBeNull(); + expect( + document.querySelector('[data-resource-book-all-page="true"]'), + ).toBeNull(); + expect(document.querySelector('.game-resource-all-grid')).toBeNull(); + expect(document.querySelector('.game-resource-all-section')).toBeNull(); const expectSortedCategories = (categories: string[]) => [...categories].sort(); - const allSections = Array.from( + const expectSortedIds = (ids: string[]) => [...ids].sort(); + const allCardHosts = Array.from( document.querySelectorAll( - '.game-resource-all-section[data-resource-book-category]', + '.game-resource-book-scene-card[data-resource-book-category]', ), ); - const expectedSections = expectSortedCategories( - Array.from(new Set(allResources.map((resource) => resource.category))), + // 宿主按真实栏目分组,栏目集合与总览里的那批一致;`all` 自己不出宿主。 + const hostCategories = expectSortedCategories( + Array.from( + new Set(allCardHosts.map((host) => host.dataset.resourceBookCategory!)), + ), ); - expect( + expect(hostCategories).toEqual( expectSortedCategories( - allSections.map((section) => section.dataset.resourceBookCategory!), + Array.from(new Set(allResources.map((resource) => resource.category))), ), - ).toEqual(expectedSections); - expect(allSections.length).toBeGreaterThan(1); - for (const section of allSections) { - expect( - section.querySelectorAll('.game-resource-card').length, - ).toBeGreaterThan(0); + ); + expect(hostCategories.length).toBeGreaterThan(1); + expect(hostCategories).not.toContain('all'); + // 每张资源恰好一个宿主(展开态没有"第二份宿主"):条数与全量计数同源, + // 因此不会出现「标题说 N 项、画布少几张」。 + expect( + expectSortedIds( + allCardHosts.map( + (host) => + host.querySelector('.game-resource-card')?.dataset + .resourceCardId ?? '', + ), + ), + ).toEqual(expectSortedIds(allResources.map((resource) => resource.id))); + // 卡片本体仍是栏目页同一颗渲染器(视觉体 + 选中按钮),不是另一套占位。 + for (const host of allCardHosts) { + expect(host.querySelectorAll('.game-resource-card')).toHaveLength(1); + expect(host.querySelector('.game-resource-card-visual')).not.toBeNull(); } - // 汇总页自己的标题栏也用同一份全量计数。 - expect(readCountBadge('.game-resource-all-page')).toBe(allResourcesCount); + // 展开态的标题栏也是同一颗标题栏组件:`all` 那条钉在视口上并带「资源总览」收起入口, + // 计数用同一份全量计数。 + expect( + readCountBadge( + '.game-resource-book-scene-titlebar[data-resource-book-category="all"]', + ), + ).toBe(allResourcesCount); + expect( + document.querySelector( + '.game-resource-book-scene-titlebar[data-resource-book-category="all"] .game-resource-page-collapse', + ), + ).not.toBeNull(); + // 视觉不是另写一套:那一页没有任何专属选择器,卡与宿主都只用共享规则 + // (声明级断言——它验的是"没有平行样式",验不到布局本身)。 + const allPageStyles = readFileSync( + resolve(process.cwd(), 'apps/ai-game-creator-shell/src/styles.css'), + 'utf8', + ); + expect(allPageStyles).not.toMatch(/\.game-resource-all-/); }); /**