补齐模板库页面单测与真连建项目检查
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
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,避免同名按钮歧义 - 真连检查扩展为「读线上清单 → 下载安装线上模板 → 据此建项目」的完整链路 - 同步技术方案、里程碑与实施计划的验证命令与验收口径
This commit is contained in:
@@ -1072,5 +1072,21 @@ mod tests {
|
||||
assert!(project_root.join("game/index.html").is_file());
|
||||
assert!(project_root.join("game/main.js").is_file());
|
||||
assert!(project_root.join(TEMPLATE_INSTALLED_MARKER_FILE).is_file());
|
||||
|
||||
// 同一条链路继续建项目:线上模板 → 本机安装 → 新项目目录。
|
||||
let projects_root = unique_projects_root();
|
||||
let created = create_project_from_installed_template_at(
|
||||
&projects_root,
|
||||
project_root,
|
||||
Some("线上模板项目"),
|
||||
false,
|
||||
)
|
||||
.expect("create project from live template");
|
||||
assert_eq!(created.manifest.name, "线上模板项目");
|
||||
let created_root = Path::new(&created.project_path);
|
||||
assert!(created_root.join("game/index.html").is_file());
|
||||
assert!(created_root.join("game/main.js").is_file());
|
||||
assert!(created_root.join(".agent/manifest.json").is_file());
|
||||
fs::remove_dir_all(&projects_root).ok();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -255,6 +255,7 @@ export default function TemplateLibraryView({
|
||||
filters.runtime === runtime ? activeChipClass : chipClass
|
||||
}
|
||||
aria-pressed={filters.runtime === runtime}
|
||||
aria-label={`运行时筛选 ${templateRuntimeLabel(runtime)}`}
|
||||
onClick={() => selectRuntime(runtime)}
|
||||
>
|
||||
{templateRuntimeLabel(runtime)}
|
||||
@@ -275,6 +276,7 @@ export default function TemplateLibraryView({
|
||||
filters.tags.includes(tag) ? activeChipClass : chipClass
|
||||
}
|
||||
aria-pressed={filters.tags.includes(tag)}
|
||||
aria-label={`标签筛选 ${tag}`}
|
||||
onClick={() => toggleTag(tag)}
|
||||
>
|
||||
{tag}
|
||||
|
||||
+1
-1
@@ -12,7 +12,7 @@ import {
|
||||
templateMatchesQuery,
|
||||
templateRuntimeLabel,
|
||||
toggleGameTemplateTag,
|
||||
} from './templateLibraryModel';
|
||||
} from '../src/features/template-library/templateLibraryModel';
|
||||
|
||||
function template(
|
||||
overrides: Partial<GameTemplateEntry> & Pick<GameTemplateEntry, 'id'>,
|
||||
@@ -0,0 +1,300 @@
|
||||
// @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();
|
||||
});
|
||||
});
|
||||
@@ -42,8 +42,9 @@ Milestone Spec: `docs/project-memory/plans/【里程碑】AGC模板库客户端
|
||||
```bash
|
||||
cargo check --manifest-path apps/ai-game-creator-shell/src-tauri/Cargo.toml
|
||||
cargo test --manifest-path apps/ai-game-creator-shell/src-tauri/Cargo.toml --bin genarrative-ai-game-creator-shell template_library
|
||||
cargo test --manifest-path apps/ai-game-creator-shell/src-tauri/Cargo.toml --bin genarrative-ai-game-creator-shell template_library -- --ignored
|
||||
cd apps/ai-game-creator-shell && npx tsc -p tsconfig.json --noEmit
|
||||
cd apps/ai-game-creator-shell && npx vitest run src/features/template-library
|
||||
npx vitest run apps/ai-game-creator-shell/tests/templateLibraryModel.test.ts apps/ai-game-creator-shell/tests/templateLibraryView.test.tsx
|
||||
node scripts/agc-template-library-publish.mjs --source <dir> --dry-run
|
||||
npm run check:encoding
|
||||
node scripts/check-doc-index.mjs
|
||||
|
||||
@@ -31,7 +31,7 @@ AGC 客户端能读取公共 OSS 上的游戏模板库,并把「浏览 → 筛
|
||||
4. `create_automatic_local_game_project_from_template` 建出的项目同时具备模板文件、`.agent` 清单与标准目录;失败时不留项目目录。
|
||||
5. 模板库页可按关键词、标签、运行时与「仅看已下载」筛选;卡片显示封面与已下载徽标;「使用模板」在版本落后时先重下再建项。
|
||||
6. 首页推荐位展示模板库内容并可进入模板库页;左侧导航有模板库入口且为独立全屏页。
|
||||
7. 定向 Rust 单测 8 项、前端模型单测 9 项、`tsc` 类型检查、`npm run check:encoding`、`git diff --check` 全部通过。
|
||||
7. 定向 Rust 单测 9 项(含线上清单 fixture)、前端模型 9 项 + 页面/推荐位 8 项、`tsc` 类型检查、`npm run check:encoding`、`git diff --check` 全部通过;可选真连检查能读线上清单、下载安装线上模板并据此建项目。
|
||||
|
||||
## 依赖
|
||||
|
||||
|
||||
@@ -76,8 +76,10 @@ templates/
|
||||
```bash
|
||||
# 模板库单测(清单校验、键安全、解压路径逃逸、安装与建项目)
|
||||
cargo test --manifest-path apps/ai-game-creator-shell/src-tauri/Cargo.toml --bin genarrative-ai-game-creator-shell template_library
|
||||
# 前端模型单测
|
||||
cd apps/ai-game-creator-shell && npx vitest run src/features/template-library
|
||||
# 模板库真连检查(可选,需要网络):读线上清单、下载安装线上模板包并据此建项目
|
||||
cargo test --manifest-path apps/ai-game-creator-shell/src-tauri/Cargo.toml --bin genarrative-ai-game-creator-shell template_library -- --ignored
|
||||
# 前端模型与页面单测(AGC 测试统一在 apps/ai-game-creator-shell/tests/)
|
||||
npx vitest run apps/ai-game-creator-shell/tests/templateLibraryModel.test.ts apps/ai-game-creator-shell/tests/templateLibraryView.test.tsx
|
||||
# 类型检查 / 编码 / 空白
|
||||
cd apps/ai-game-creator-shell && npm run typecheck
|
||||
npm run check:encoding
|
||||
@@ -88,6 +90,8 @@ curl -s https://agc-dev.oss-rg-china-mainland.aliyuncs.com/templates/index.json
|
||||
|
||||
手工验收:打开模板库 → 搜索与筛选 → 下载(出现「已下载」徽标)→ 「使用模板」→ 进入项目工作台且项目里已有模板文件。
|
||||
|
||||
运行态核验(客户端真的拉过清单时):本机缓存 `<app_data>/templates/index.json` 与线上 `templates/index.json` 逐字节一致;`<app_data>/templates/installed/<id>/<version>/installed.json` 出现即表示该模板已下载完成。
|
||||
|
||||
## 失败与回退
|
||||
|
||||
- 清单读不到且没有本机缓存:模板库页显示错误与重试,首页推荐位显示「模板库暂时没有可用的模板」。
|
||||
|
||||
Reference in New Issue
Block a user