补资源总览网格的声明级断言:钉住 width:100% 与 max-width 并存,防止退回单列

- 新增用例「keeps the resource overview grid off fit-content so its columns stay responsive」:断言 .game-resource-book-main-grid 同时声明 width: 100% / max-width: 1120px / margin: 0 auto / repeat(auto-fit …),并同款断言 .game-resource-book-main-heading 的 width: 100%
- 取声明体前先剥掉 CSS 注释:本次修复的注释里就写着 width: 100% 等字样,不剥掉会让断言匹配到注释而假绿(删掉真正的声明也照样通过)
- 注释里写明这条断言**验不到布局**:AGC 的 vitest 没开 css: true、styles.css 不会被加载、jsdom 也没有布局引擎,gridTemplateColumns 的解析结果与缩略卡列数只能在真机量;真机判据(3 个非 0 轨道 / grid 宽 432→1120 / 8 张缩略卡 top 只有 3 个取值,且需在总览态取样)写在用例注释里
- 变异验证:临时删掉 .game-resource-book-main-grid 的 width: 100% → 用例变红且失败信息打印的是真实声明体(证明注释剥离生效);还原后 styles.css 逐字节哈希与已提交版本一致,复跑绿灯
- 门禁:npm run typecheck exit 0;npm run check:encoding exit 0(4399 files);npx vitest run apps/ai-game-creator-shell/tests/appSurface.test.ts 本用例 1 passed
This commit is contained in:
2026-09-11 22:50:01 +08:00
parent 3db6afa052
commit 28781a420c
@@ -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 列里的标题栏带同款退化(只占 <h2> 宽度、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<HTMLElement>(
'.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<HTMLElement>('.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-/);
});
/**