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,避免同名按钮歧义 - 真连检查扩展为「读线上清单 → 下载安装线上模板 → 据此建项目」的完整链路 - 同步技术方案、里程碑与实施计划的验证命令与验收口径
301 lines
9.2 KiB
TypeScript
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();
|
|
});
|
|
});
|