Files
Genarrative/apps/ai-game-creator-shell/tests/templateLibraryGrid.test.ts
T
kdletters e072c66ce9
Project CI / AI game creator shell Rust lane 1/2 (push) Has been cancelled
Project CI / AI game creator shell Rust lane 2/2 (push) Has been cancelled
Project CI / Backend tests (push) Has been cancelled
Project CI / Native shell tests (push) Has been cancelled
Project CI / Frontend tests (push) Has been cancelled
Project CI / Repository checks (push) Has been cancelled
Project CI / AI game creator shell Rust crates (push) Has been cancelled
Project CI / AI game creator shell web tests (push) Has been cancelled
Project CI / AI game creator shell Rust smoke (push) Has been cancelled
修复 AGC 模板库筛选与卡片状态的表现缺陷
- 模板库筛选区改用共享筛选条与标签 chip:选中为实心品牌填充 + 反白文字,未选为浅底描边
- 未选中悬停不再借用品牌色(品牌色专属已选中),状态阶梯固定为静止 / 悬停 / 按下 / 选中
- 新增 getPlatformCategoryChipClassName 收敛筛选 chip 类名口径,模板库、资源画布、参考图弹窗与平台 Web 端共用
- PlatformSegmentedTabs 增加 accent 选中口径,与筛选 chip 共用 --platform-chip-* 语义色
- 修复卡片文字区按 grid auto 行排版导致的标题 / 简介 / 标签被裁:行高契约拆为分项常量,文字区固定 174
- 修复忙状态下动作行塞入第三个元素导致的按钮文案换行顶出卡片:忙状态写在触发它的按钮上
- 修复经典滚动条占宽导致的卡片区横向滚动条:按竖滚动条预留列宽后再算列宽行高
- 焦点环由 15% 透明度改为实心色并用 outline 绘制,选中项聚焦不再被状态投影盖掉
- 封面加载失败改为隐藏图片留中性底色,不再出现浏览器裂图图标
- 补充状态对比度、行高契约、滚动条预留、封面兜底与忙状态文案的回归测试
- 同步技术方案、踩坑记录与决策记录的口径
2026-09-23 16:51:15 +08:00

183 lines
6.3 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
import { describe, expect, it } from 'vitest';
import {
buildTemplateRows,
computeTemplateGridColumns,
computeTemplateGridLayout,
computeTemplateGridLayoutWithScrollbar,
computeTemplateRowHeight,
TEMPLATE_CARD_ACTIONS_HEIGHT,
TEMPLATE_CARD_GAP,
TEMPLATE_CARD_META_HEIGHT,
TEMPLATE_CARD_MIN_WIDTH,
TEMPLATE_CARD_SUMMARY_HEIGHT,
TEMPLATE_CARD_TAGS_HEIGHT,
TEMPLATE_CARD_TEXT_GAP,
TEMPLATE_CARD_TEXT_HEIGHT,
TEMPLATE_CARD_TEXT_PADDING,
TEMPLATE_CARD_TITLE_HEIGHT,
} from '../src/features/template-library/templateLibraryGrid';
import type { GameTemplateEntry } from '../src/features/template-library/templateLibraryModel';
function entry(id: string): GameTemplateEntry {
return {
id,
title: id,
summary: '',
tags: [],
runtime: 'html',
engine: 'phaser',
engineVersion: '4.2.1',
templateVersion: '0.1.0',
updatedAt: '2026-09-17T00:00:00Z',
entry: 'game/index.html',
zipUrl: `https://oss.example/templates/v1/${id}/template.zip`,
zipSizeBytes: 1024,
zipSha256: 'a'.repeat(64),
coverUrl: `https://oss.example/templates/v1/${id}/cover.svg`,
coverWidth: 960,
coverHeight: 540,
installed: false,
installedVersion: null,
installedAtMillis: null,
};
}
describe('computeTemplateGridColumns', () => {
it('fits as many min-width columns as the container allows', () => {
expect(computeTemplateGridColumns(0)).toBe(1);
expect(computeTemplateGridColumns(200)).toBe(1);
expect(computeTemplateGridColumns(TEMPLATE_CARD_MIN_WIDTH)).toBe(1);
// 两列边界:2*250 + 14 = 514
const twoColumnWidth = 2 * TEMPLATE_CARD_MIN_WIDTH + TEMPLATE_CARD_GAP;
expect(computeTemplateGridColumns(twoColumnWidth)).toBe(2);
expect(computeTemplateGridColumns(twoColumnWidth - 1)).toBe(1);
// 1200px:4 列((1200+14)/(250+14) = 4.59)
expect(computeTemplateGridColumns(1200)).toBe(4);
});
});
describe('computeTemplateRowHeight', () => {
it('budgets the same height the card rows actually need', () => {
// 这些数字对应 TemplateCard 的 `h-5`/`h-4`/`h-8`/`h-5.5`/`h-7` 与 `p-3`/`gap-2`:
// 行高契约比真实内容小,被截断的就是卡片里的标题与简介。
expect(TEMPLATE_CARD_TITLE_HEIGHT).toBe(20);
expect(TEMPLATE_CARD_META_HEIGHT).toBe(16);
expect(TEMPLATE_CARD_SUMMARY_HEIGHT).toBe(32);
expect(TEMPLATE_CARD_TAGS_HEIGHT).toBe(22);
expect(TEMPLATE_CARD_ACTIONS_HEIGHT).toBe(28);
expect(TEMPLATE_CARD_TEXT_PADDING).toBe(12);
expect(TEMPLATE_CARD_TEXT_GAP).toBe(8);
expect(TEMPLATE_CARD_TEXT_HEIGHT).toBe(174);
});
it('keeps cover ratio + fixed text block', () => {
// 列宽 300 → 卡片 286 → 封面 286*9/16 = 160.875 → 161
expect(computeTemplateRowHeight(300)).toBe(
161 + TEMPLATE_CARD_TEXT_HEIGHT + TEMPLATE_CARD_GAP,
);
// 极窄时按最小卡片宽度兜底,避免行高被压成 0
expect(computeTemplateRowHeight(10)).toBeGreaterThan(
TEMPLATE_CARD_TEXT_HEIGHT,
);
});
});
describe('computeTemplateGridLayout', () => {
it('derives columns, row height and row count for a big library', () => {
const layout = computeTemplateGridLayout({
containerWidth: 1200,
itemCount: 1000,
});
expect(layout.columnCount).toBe(4);
expect(layout.columnWidth).toBe(300);
expect(layout.rowHeight).toBe(
161 + TEMPLATE_CARD_TEXT_HEIGHT + TEMPLATE_CARD_GAP,
);
expect(layout.rowCount).toBe(250);
// 虚拟列表只渲染可视行,滚动高度仍由总行数决定
expect(layout.rowCount * layout.rowHeight).toBeGreaterThan(80000);
});
it('handles inline and empty libraries', () => {
expect(
computeTemplateGridLayout({ containerWidth: 0, itemCount: 5 }),
).toEqual({
columnCount: 1,
columnWidth: 1,
rowHeight: computeTemplateRowHeight(1),
rowCount: 5,
});
expect(
computeTemplateGridLayout({ containerWidth: 1200, itemCount: 0 })
.rowCount,
).toBe(0);
});
});
describe('computeTemplateGridLayoutWithScrollbar', () => {
const innerWidth = (layout: { columnCount: number; columnWidth: number }) =>
layout.columnCount * layout.columnWidth;
it('reserves the classic scrollbar width once the grid scrolls vertically', () => {
// 经典滚动条(Windows/WebView2 ≈ 17px)不在包裹层宽度里,不预留就会多出一条横向滚动条。
const plain = computeTemplateGridLayout({
containerWidth: 1184,
itemCount: 13,
});
const layout = computeTemplateGridLayoutWithScrollbar({
containerWidth: 1184,
itemCount: 13,
viewportHeight: 500,
scrollbarWidth: 17,
});
expect(layout.columnCount).toBe(4);
// 内层宽度必须落在竖滚动条左侧的可用宽度里,否则又会出现横向滚动条。
expect(innerWidth(layout)).toBeLessThanOrEqual(1184 - 17);
expect(innerWidth(layout)).toBeLessThan(innerWidth(plain));
// 行高仍按预留后的列宽算,卡片内容不会被压。
expect(layout.rowHeight).toBe(computeTemplateRowHeight(layout.columnWidth));
});
it('keeps the plain layout when nothing overflows or the scrollbar is an overlay', () => {
const plain = computeTemplateGridLayout({
containerWidth: 1184,
itemCount: 4,
});
// 只有一行:不会竖向滚动,不需要预留,右侧不留白边。
expect(
computeTemplateGridLayoutWithScrollbar({
containerWidth: 1184,
itemCount: 4,
viewportHeight: 900,
scrollbarWidth: 17,
}),
).toEqual(plain);
// overlay 滚动条占宽 0:与不预留完全一致。
expect(
computeTemplateGridLayoutWithScrollbar({
containerWidth: 1184,
itemCount: 13,
viewportHeight: 500,
scrollbarWidth: 0,
}),
).toEqual(
computeTemplateGridLayout({ containerWidth: 1184, itemCount: 13 }),
);
});
});
describe('buildTemplateRows', () => {
it('chunks entries per row and pads the tail with nulls', () => {
const rows = buildTemplateRows([entry('a'), entry('b'), entry('c')], 2);
expect(rows).toHaveLength(2);
expect(rows[0]?.map((item) => item?.id)).toEqual(['a', 'b']);
expect(rows[1]?.map((item) => item?.id ?? null)).toEqual(['c', null]);
});
it('returns no rows for an invalid column count', () => {
expect(buildTemplateRows([entry('a')], 0)).toEqual([]);
});
});