Files
Genarrative/apps/ai-game-creator-shell/tests/templateLibraryView.test.tsx
T
kdletters 46d395763d
Project CI / AI game creator shell Rust shard 1/4 (pull_request) Successful in 6m50s
Project CI / AI game creator shell Rust shard 2/4 (pull_request) Successful in 6m2s
Project CI / AI game creator shell Rust smoke (pull_request) Successful in 2m36s
Project CI / Backend tests (pull_request) Failing after 11s
Project CI / AI game creator shell Rust shard 3/4 (pull_request) Successful in 6m7s
Project CI / AI game creator shell Rust shard 4/4 (pull_request) Successful in 4m56s
Project CI / AI game creator shell Rust crates (pull_request) Successful in 2m46s
Project CI / Repository checks (pull_request) Failing after 15s
Project CI / Frontend tests (pull_request) Failing after 4m41s
Project CI / AI game creator shell web tests (pull_request) Failing after 3m48s
Project CI / Native shell tests (pull_request) Successful in 9m39s
补齐模板库页面单测与真连建项目检查
- 前端单测迁到 apps/ai-game-creator-shell/tests/,与根 vitest include 一致;新增模板库页面与首页推荐位 8 项渲染/交互测试(卡片封面、标签、已下载徽标、搜索、筛选、下载与使用模板、空态与错误态)
- 模板库的运行时与标签筛选补 aria-label,避免同名按钮歧义
- 真连检查扩展为「读线上清单 → 下载安装线上模板 → 据此建项目」的完整链路
- 同步技术方案、里程碑与实施计划的验证命令与验收口径
2026-09-17 11:37:29 +08:00

301 lines
9.2 KiB
TypeScript

// @vitest-environment jsdom
import { fireEvent, render, screen, within } from '@testing-library/react';
import { describe, expect, it, vi } from 'vitest';
import type {
GameTemplateEntry,
TemplateLibraryFilters,
} from '../src/features/template-library/templateLibraryModel';
import type { TemplateLibraryController } from '../src/features/template-library/useTemplateLibrary';
import TemplateRecommendations from '../src/view/home/TemplateRecommendations';
import TemplateLibraryView from '../src/view/template-library';
const OSS_BASE = 'https://agc-dev.oss-rg-china-mainland.aliyuncs.com';
function template(
overrides: Partial<GameTemplateEntry> & Pick<GameTemplateEntry, 'id'>,
): GameTemplateEntry {
return {
title: '未命名模板',
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: `${OSS_BASE}/templates/v1/${overrides.id}/template.zip`,
zipSizeBytes: 2048,
zipSha256: 'a'.repeat(64),
coverUrl: `${OSS_BASE}/templates/v1/${overrides.id}/cover.svg`,
coverWidth: 960,
coverHeight: 540,
installed: false,
installedVersion: null,
installedAtMillis: null,
...overrides,
};
}
const blankWeb = template({
id: 'blank-web',
title: '空白网页工程',
summary: '零依赖最小网页工程',
tags: ['空白', '网页'],
engine: 'none',
engineVersion: '',
installed: true,
installedVersion: '0.1.0',
installedAtMillis: 1789000000000,
});
const blankCanvas = template({
id: 'blank-2d-canvas',
title: '空白二维画布工程',
tags: ['空白', '2d', 'canvas'],
engine: 'canvas',
engineVersion: '',
});
const templates = [blankCanvas, blankWeb];
function controller(
overrides: Partial<TemplateLibraryController> = {},
): TemplateLibraryController {
const filters: TemplateLibraryFilters = {
query: '',
tags: [],
runtime: '',
installedOnly: false,
};
return {
snapshot: null,
status: 'ready',
error: '',
notice: '',
templates,
visibleTemplates: templates,
tagOptions: ['空白', '2d', 'canvas', '网页'],
runtimeOptions: ['html'],
installedCount: 1,
filters,
filtersActive: false,
setQuery: vi.fn(),
selectRuntime: vi.fn(),
toggleTag: vi.fn(),
setInstalledOnly: vi.fn(),
clearFilters: vi.fn(),
busyTemplateId: null,
busyKind: null,
refresh: vi.fn(),
downloadTemplate: vi.fn(async () => undefined),
createProjectFromTemplate: vi.fn(async () => undefined),
clearNotice: vi.fn(),
...overrides,
} as unknown as TemplateLibraryController;
}
function cardFor(title: string): HTMLElement {
const heading = screen.getByText(title);
const card = heading.closest('article');
if (!card) throw new Error(`找不到卡片:${title}`);
return card;
}
describe('TemplateLibraryView', () => {
it('renders a card per template with cover, meta, tags and installed badge', () => {
render(<TemplateLibraryView controller={controller()} onBack={() => {}} />);
expect(screen.getByRole('heading', { name: '模板库' })).toBeTruthy();
expect(screen.getByText('共 2 个模板 · 已下载 1 个')).toBeTruthy();
const blankWebCard = cardFor('空白网页工程');
const cover = blankWebCard.querySelector('img');
expect(cover?.getAttribute('src')).toBe(
`${OSS_BASE}/templates/v1/blank-web/cover.svg`,
);
expect(blankWebCard.textContent).toContain('已下载');
expect(blankWebCard.textContent).toContain('网页 · none · v0.1.0 · 2.0 KB');
expect(blankWebCard.textContent).toContain('空白');
expect(blankWebCard.textContent).toContain('零依赖最小网页工程');
const blankCanvasCard = cardFor('空白二维画布工程');
expect(blankCanvasCard.textContent).not.toContain('已下载');
expect(blankCanvasCard.querySelector('img')?.getAttribute('src')).toBe(
`${OSS_BASE}/templates/v1/blank-2d-canvas/cover.svg`,
);
});
it('routes search, tag, runtime and installed-only controls through the controller', () => {
const setQuery = vi.fn();
const toggleTag = vi.fn();
const selectRuntime = vi.fn();
const setInstalledOnly = vi.fn();
const clearFilters = vi.fn();
render(
<TemplateLibraryView
controller={controller({
setQuery,
toggleTag,
selectRuntime,
setInstalledOnly,
clearFilters,
filtersActive: true,
})}
onBack={() => {}}
/>,
);
fireEvent.change(screen.getByLabelText('搜索模板'), {
target: { value: '空白 网页' },
});
expect(setQuery).toHaveBeenCalledWith('空白 网页');
fireEvent.click(screen.getByRole('button', { name: '标签筛选 canvas' }));
expect(toggleTag).toHaveBeenCalledWith('canvas');
fireEvent.click(screen.getByRole('button', { name: '运行时筛选 网页' }));
expect(selectRuntime).toHaveBeenCalledWith('html');
fireEvent.click(screen.getByRole('button', { name: '仅看已下载' }));
expect(setInstalledOnly).toHaveBeenCalledWith(true);
fireEvent.click(screen.getByRole('button', { name: '清除筛选' }));
expect(clearFilters).toHaveBeenCalled();
});
it('starts a download and a template project from the card actions', () => {
const downloadTemplate = vi.fn(async () => undefined);
const createProjectFromTemplate = vi.fn(async () => undefined);
render(
<TemplateLibraryView
controller={controller({ downloadTemplate, createProjectFromTemplate })}
onBack={() => {}}
/>,
);
fireEvent.click(
within(cardFor('空白二维画布工程')).getByRole('button', { name: /下载/ }),
);
expect(downloadTemplate).toHaveBeenCalledWith(blankCanvas);
fireEvent.click(
within(cardFor('空白二维画布工程')).getByRole('button', {
name: /使用模板/,
}),
);
expect(createProjectFromTemplate).toHaveBeenCalledWith(blankCanvas);
});
it('disables card actions while a template is busy', () => {
render(
<TemplateLibraryView
controller={controller({
busyTemplateId: 'blank-2d-canvas',
busyKind: 'create',
})}
onBack={() => {}}
/>,
);
const busyCard = cardFor('空白二维画布工程');
const buttons = Array.from(busyCard.querySelectorAll('button'));
expect(buttons.every((button) => button.hasAttribute('disabled'))).toBe(
true,
);
expect(busyCard.textContent).toContain('正在创建项目');
expect(cardFor('空白网页工程').textContent).not.toContain('正在创建项目');
});
it('shows empty, no-match, error and notice states', () => {
const { unmount } = render(
<TemplateLibraryView
controller={controller({
templates: [],
visibleTemplates: [],
installedCount: 0,
})}
onBack={() => {}}
/>,
);
expect(screen.getByText('模板库暂时还没有可用的模板。')).toBeTruthy();
unmount();
const { unmount: unmountNoMatch } = render(
<TemplateLibraryView
controller={controller({ visibleTemplates: [], filtersActive: true })}
onBack={() => {}}
/>,
);
expect(screen.getByText('没有符合当前筛选的模板')).toBeTruthy();
unmountNoMatch();
render(
<TemplateLibraryView
controller={controller({
error: '模板库返回 HTTP 503',
notice: '远端清单暂时读不到,当前展示本机缓存',
})}
onBack={() => {}}
/>,
);
expect(screen.getByRole('alert').textContent).toContain(
'模板库返回 HTTP 503',
);
expect(
screen.getByText('远端清单暂时读不到,当前展示本机缓存'),
).toBeTruthy();
});
});
describe('TemplateRecommendations', () => {
it('renders the recommended templates and opens the library', () => {
const onOpenLibrary = vi.fn();
render(
<TemplateRecommendations
templates={templates}
loading={false}
error=""
onOpenLibrary={onOpenLibrary}
/>,
);
const recommendation = screen.getByRole('button', {
name: '查看模板 空白网页工程',
});
expect(recommendation.querySelector('img')?.getAttribute('src')).toBe(
`${OSS_BASE}/templates/v1/blank-web/cover.svg`,
);
expect(recommendation.textContent).toContain('已下载');
fireEvent.click(recommendation);
expect(onOpenLibrary).toHaveBeenCalled();
});
it('falls back to an empty state with a library entry', () => {
const onOpenLibrary = vi.fn();
render(
<TemplateRecommendations
templates={[]}
loading={false}
error="需要在陶泥儿客户端内运行"
onOpenLibrary={onOpenLibrary}
/>,
);
expect(screen.getByText('需要在陶泥儿客户端内运行')).toBeTruthy();
fireEvent.click(screen.getByRole('button', { name: '打开模板库' }));
expect(onOpenLibrary).toHaveBeenCalled();
});
it('renders a loading state before the first snapshot arrives', () => {
render(
<TemplateRecommendations
templates={[]}
loading
error=""
onOpenLibrary={() => {}}
/>,
);
expect(screen.getByText('正在读取模板库…')).toBeTruthy();
});
});