066d0fe6f9
Project CI / AI game creator shell Rust shard 1/4 (pull_request) Successful in 6m16s
Project CI / AI game creator shell Rust smoke (pull_request) Successful in 1m56s
Project CI / AI game creator shell Rust shard 2/4 (pull_request) Successful in 5m17s
Project CI / Backend tests (pull_request) Failing after 9s
Project CI / AI game creator shell Rust shard 3/4 (pull_request) Failing after 5m33s
Project CI / AI game creator shell Rust shard 4/4 (pull_request) Successful in 5m29s
Project CI / Repository checks (pull_request) Failing after 12s
Project CI / AI game creator shell Rust crates (pull_request) Successful in 3m40s
Project CI / AI game creator shell web tests (pull_request) Failing after 4m2s
Project CI / Frontend tests (pull_request) Failing after 4m19s
Project CI / Native shell tests (pull_request) Successful in 8m46s
- 用 workspace 已有的 react-window@1.8.11 的 FixedSizeGrid:只渲染可视行 + 2 行 overscan,1000 条不再一次性挂 1000 个卡片节点 - 新增 templateLibraryGrid.ts 纯函数布局契约(列数/列宽/行高/行数/按行切分),单测覆盖 - 卡片抽成 memo 化的 TemplateCard;筛选条件变化时把滚动位置复位到顶部 - 筛选区(运行时/标签)改为可独立滚动区块,标签膨胀不再挤压卡片区 - 页面回归改用固定视口:断言 1000 条只渲染 ≤40 张卡、滚动高度按 250 行计算 - 技术方案补虚拟滚动契约说明
109 lines
3.5 KiB
TypeScript
109 lines
3.5 KiB
TypeScript
import { describe, expect, it } from 'vitest';
|
||
|
||
import {
|
||
buildTemplateRows,
|
||
computeTemplateGridColumns,
|
||
computeTemplateGridLayout,
|
||
computeTemplateRowHeight,
|
||
TEMPLATE_CARD_GAP,
|
||
TEMPLATE_CARD_MIN_WIDTH,
|
||
TEMPLATE_CARD_TEXT_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('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('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([]);
|
||
});
|
||
});
|