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-/);
});
/**