From 46d395763dc9d82ba87e8cea42cf8703eaa5f36d Mon Sep 17 00:00:00 2001 From: kdletters <61648117+kdletters@users.noreply.github.com> Date: Thu, 17 Sep 2026 11:37:29 +0800 Subject: [PATCH] =?UTF-8?q?=E8=A1=A5=E9=BD=90=E6=A8=A1=E6=9D=BF=E5=BA=93?= =?UTF-8?q?=E9=A1=B5=E9=9D=A2=E5=8D=95=E6=B5=8B=E4=B8=8E=E7=9C=9F=E8=BF=9E?= =?UTF-8?q?=E5=BB=BA=E9=A1=B9=E7=9B=AE=E6=A3=80=E6=9F=A5?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 前端单测迁到 apps/ai-game-creator-shell/tests/,与根 vitest include 一致;新增模板库页面与首页推荐位 8 项渲染/交互测试(卡片封面、标签、已下载徽标、搜索、筛选、下载与使用模板、空态与错误态) - 模板库的运行时与标签筛选补 aria-label,避免同名按钮歧义 - 真连检查扩展为「读线上清单 → 下载安装线上模板 → 据此建项目」的完整链路 - 同步技术方案、里程碑与实施计划的验证命令与验收口径 --- .../src-tauri/src/template_library.rs | 16 + .../src/view/template-library/index.tsx | 2 + .../templateLibraryModel.test.ts | 2 +- .../tests/templateLibraryView.test.tsx | 300 ++++++++++++++++++ ...实施计划】AGC模板库客户端接入-2026-09-17.md | 3 +- ...里程碑】AGC模板库客户端接入-2026-09-17.md | 2 +- ...技术方案】AGC模板库与模板建项-2026-09-17.md | 8 +- 7 files changed, 328 insertions(+), 5 deletions(-) rename apps/ai-game-creator-shell/{src/features/template-library => tests}/templateLibraryModel.test.ts (98%) create mode 100644 apps/ai-game-creator-shell/tests/templateLibraryView.test.tsx diff --git a/apps/ai-game-creator-shell/src-tauri/src/template_library.rs b/apps/ai-game-creator-shell/src-tauri/src/template_library.rs index 56e76c3af..11ad74279 100644 --- a/apps/ai-game-creator-shell/src-tauri/src/template_library.rs +++ b/apps/ai-game-creator-shell/src-tauri/src/template_library.rs @@ -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(); } } diff --git a/apps/ai-game-creator-shell/src/view/template-library/index.tsx b/apps/ai-game-creator-shell/src/view/template-library/index.tsx index d20654325..154e2ddc0 100644 --- a/apps/ai-game-creator-shell/src/view/template-library/index.tsx +++ b/apps/ai-game-creator-shell/src/view/template-library/index.tsx @@ -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} diff --git a/apps/ai-game-creator-shell/src/features/template-library/templateLibraryModel.test.ts b/apps/ai-game-creator-shell/tests/templateLibraryModel.test.ts similarity index 98% rename from apps/ai-game-creator-shell/src/features/template-library/templateLibraryModel.test.ts rename to apps/ai-game-creator-shell/tests/templateLibraryModel.test.ts index cb71a5363..5eb52ca01 100644 --- a/apps/ai-game-creator-shell/src/features/template-library/templateLibraryModel.test.ts +++ b/apps/ai-game-creator-shell/tests/templateLibraryModel.test.ts @@ -12,7 +12,7 @@ import { templateMatchesQuery, templateRuntimeLabel, toggleGameTemplateTag, -} from './templateLibraryModel'; +} from '../src/features/template-library/templateLibraryModel'; function template( overrides: Partial & Pick, diff --git a/apps/ai-game-creator-shell/tests/templateLibraryView.test.tsx b/apps/ai-game-creator-shell/tests/templateLibraryView.test.tsx new file mode 100644 index 000000000..a17c035e9 --- /dev/null +++ b/apps/ai-game-creator-shell/tests/templateLibraryView.test.tsx @@ -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 & Pick, +): 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 { + 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( {}} />); + + 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( + {}} + />, + ); + + 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( + {}} + />, + ); + + 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( + {}} + />, + ); + + 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( + {}} + />, + ); + expect(screen.getByText('模板库暂时还没有可用的模板。')).toBeTruthy(); + unmount(); + + const { unmount: unmountNoMatch } = render( + {}} + />, + ); + expect(screen.getByText('没有符合当前筛选的模板')).toBeTruthy(); + unmountNoMatch(); + + render( + {}} + />, + ); + 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( + , + ); + + 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( + , + ); + + expect(screen.getByText('需要在陶泥儿客户端内运行')).toBeTruthy(); + fireEvent.click(screen.getByRole('button', { name: '打开模板库' })); + expect(onOpenLibrary).toHaveBeenCalled(); + }); + + it('renders a loading state before the first snapshot arrives', () => { + render( + {}} + />, + ); + expect(screen.getByText('正在读取模板库…')).toBeTruthy(); + }); +}); diff --git a/docs/project-memory/plans/【实施计划】AGC模板库客户端接入-2026-09-17.md b/docs/project-memory/plans/【实施计划】AGC模板库客户端接入-2026-09-17.md index 97e12c415..1b7315703 100644 --- a/docs/project-memory/plans/【实施计划】AGC模板库客户端接入-2026-09-17.md +++ b/docs/project-memory/plans/【实施计划】AGC模板库客户端接入-2026-09-17.md @@ -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 --dry-run npm run check:encoding node scripts/check-doc-index.mjs diff --git a/docs/project-memory/plans/【里程碑】AGC模板库客户端接入-2026-09-17.md b/docs/project-memory/plans/【里程碑】AGC模板库客户端接入-2026-09-17.md index f35aec28b..9059a5348 100644 --- a/docs/project-memory/plans/【里程碑】AGC模板库客户端接入-2026-09-17.md +++ b/docs/project-memory/plans/【里程碑】AGC模板库客户端接入-2026-09-17.md @@ -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` 全部通过;可选真连检查能读线上清单、下载安装线上模板并据此建项目。 ## 依赖 diff --git a/docs/technical/【技术方案】AGC模板库与模板建项-2026-09-17.md b/docs/technical/【技术方案】AGC模板库与模板建项-2026-09-17.md index 3956604f8..a22a96145 100644 --- a/docs/technical/【技术方案】AGC模板库与模板建项-2026-09-17.md +++ b/docs/technical/【技术方案】AGC模板库与模板建项-2026-09-17.md @@ -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 手工验收:打开模板库 → 搜索与筛选 → 下载(出现「已下载」徽标)→ 「使用模板」→ 进入项目工作台且项目里已有模板文件。 +运行态核验(客户端真的拉过清单时):本机缓存 `/templates/index.json` 与线上 `templates/index.json` 逐字节一致;`/templates/installed///installed.json` 出现即表示该模板已下载完成。 + ## 失败与回退 - 清单读不到且没有本机缓存:模板库页显示错误与重试,首页推荐位显示「模板库暂时没有可用的模板」。