From 2628b83d4bc4b74031d902fb2778b7f3e0089852 Mon Sep 17 00:00:00 2001 From: Suzumiya Date: Thu, 17 Sep 2026 17:06:37 +0800 Subject: [PATCH] =?UTF-8?q?=E7=94=BB=E5=B8=83=E8=B5=84=E6=BA=90=E5=8D=A1?= =?UTF-8?q?=E7=BB=9F=E4=B8=80=E6=98=BE=E7=A4=BA=E6=AD=A3=E5=BC=8F=E8=B5=84?= =?UTF-8?q?=E6=BA=90=E5=90=8D=E5=B9=B6=E7=AE=80=E5=8C=96=E6=96=87=E6=A1=A3?= =?UTF-8?q?=E5=8D=A1=E5=8D=A1=E9=9D=A2?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 资源卡新增卡面名称条,所有类型卡统一显示正式资源 label,长名单行省略、完整名进 title - 文档卡卡面改为居中图标,不再把正文前几行铺在卡面上,正文只在独立详情浮层读取 - 删除卡面正文摘要的取文本 helper 与行数/长度常量,移除对应 CSS 规则不留墓碑 - 名称条按媒体卡让出右下播放钮位置,替换血缘角标上移避免遮住名称 - 新增资源卡名称与文档卡卡面用例,并同步重命名链路与卡面正文的既有断言 --- apps/ai-game-creator-shell/src/styles.css | 73 ++-- .../src/view/project-development/index.tsx | 52 ++- .../resourceCardPreviewModel.ts | 68 +--- .../appSurface/project-development.suite.ts | 28 +- .../tests/resourceCanvasCardName.test.tsx | 360 ++++++++++++++++++ .../tests/resourceRename.test.tsx | 13 +- 6 files changed, 480 insertions(+), 114 deletions(-) create mode 100644 apps/ai-game-creator-shell/tests/resourceCanvasCardName.test.tsx diff --git a/apps/ai-game-creator-shell/src/styles.css b/apps/ai-game-creator-shell/src/styles.css index 5b0352492..b525ce3ac 100644 --- a/apps/ai-game-creator-shell/src/styles.css +++ b/apps/ai-game-creator-shell/src/styles.css @@ -7494,6 +7494,48 @@ iframe.preview-frame { visibility: visible; } +/* + * 卡面名称:所有类型卡统一在底部显示正式资源名(`resource.label`)。 + * + * 名称来自现有正式命名链路——生成时 `assetName` 参与落盘名、用户重命名改写同一份 + * `localPath`,所以这里只消费投影 label,不另建第二份显示名。名称长了由省略号截断, + * 完整名称挂在 `title` 上;`pointer-events: none` 保证点击仍然落到整卡的选中按钮。 + * + * 圆角用 `inherit` 拿卡片自己的圆角(总览态与栏目态卡片圆角不同),再把上沿两角归零: + * 卡片本体是 `overflow: visible`,名称条不跟着圆角就会在卡片圆角外露出直角。 + */ +.game-resource-card-name { + position: absolute; + right: 0; + bottom: 0; + left: 0; + z-index: 2; + padding: 10px 8px 4px; + overflow: hidden; + border-radius: inherit; + border-top-left-radius: 0; + border-top-right-radius: 0; + background: linear-gradient( + to top, + rgb(255 253 250 / 96%) 52%, + rgb(255 253 250 / 0%) + ); + color: #4e382f; + font-size: 10px; + font-weight: 700; + line-height: 1.3; + letter-spacing: 0.01em; + pointer-events: none; + text-overflow: ellipsis; + white-space: nowrap; +} + +/* 视频 / 音频卡右下角是 34px 播放钮(+9px 内边距),名称的右侧不钻到按钮下面。 */ +.game-resource-card[data-preview-kind='video'] .game-resource-card-name, +.game-resource-card[data-preview-kind='audio'] .game-resource-card-name { + padding-right: 50px; +} + .game-resource-card-type-badge { position: absolute; top: 8px; @@ -7522,11 +7564,12 @@ iframe.preview-frame { } /* 替换血缘标注(本次会话内有效):源素材卡「已被 … 替换」/ 替换素材卡「替换自 …」。 - 左下角是卡片上唯一空闲的角(右上角标是类型、右下是媒体播放钮),用「当前版本」同一支橙色 - 把这条关系与光环联系起来。 */ + 卡片底部整条是卡面名称(`.game-resource-card-name`),所以血缘角标压在名称条之上; + 右下角仍是媒体播放钮,最大宽度按右侧让出播放钮的宽度。 + 用「当前版本」同一支橙色把这条关系与光环联系起来。 */ .game-resource-card-lineage-badge { position: absolute; - bottom: 8px; + bottom: 32px; left: 8px; z-index: 2; display: inline-flex; @@ -7638,27 +7681,13 @@ iframe.preview-frame { display: none; } -.game-resource-card-document-summary { - display: -webkit-box; - max-height: 100%; - padding: 16px; - overflow: hidden; - color: #674c41; - font-size: 11px; - line-height: 1.55; - text-align: left; - /* 文档卡预览改为"前几行"(最多 3 行),必须保留换行:折成一行就看不出结构了。 */ - white-space: pre-line; - word-break: break-word; - -webkit-box-orient: vertical; - -webkit-line-clamp: 3; -} - /* - * 代码文件卡:不显示文件内容,只给代码图标 + 扩展名标签。 - * 与 `.game-resource-card-version-visual` 共用同一套居中排布口径,视觉上仍是同一族卡片。 + * 代码文件卡与文档卡:卡面都不显示正文 —— 代码卡给代码图标 + 扩展名标签, + * 文档卡只给居中的文档图标(正文只在独立的文档详情浮层里按原文 / 代码 / UI JSON 渲染)。 + * 两者与 `.game-resource-card-version-visual` 共用同一套居中排布口径,视觉上仍是同一族卡片。 */ -.game-resource-card-code-visual { +.game-resource-card-code-visual, +.game-resource-card-document-visual { display: grid; gap: 8px; width: 100%; diff --git a/apps/ai-game-creator-shell/src/view/project-development/index.tsx b/apps/ai-game-creator-shell/src/view/project-development/index.tsx index d713a603a..12ac2247a 100644 --- a/apps/ai-game-creator-shell/src/view/project-development/index.tsx +++ b/apps/ai-game-creator-shell/src/view/project-development/index.tsx @@ -298,7 +298,6 @@ import { type ProjectResourceCardPreviewState, projectResourceCardPreviewVariant, projectResourceCodeTypeLabel, - projectResourceDocumentPreviewText, projectResourceJsonPresentation, } from './resourceCardPreviewModel'; import { ResourceClassificationPanel } from './ResourceClassificationPanel'; @@ -793,17 +792,16 @@ const ResourceCard = memo(function ResourceCard({ : resourceCategoryLabel; /** 代码卡的类型标签(`.tsx` → `TSX`);不是代码卡时为 `null`。 */ const codeTypeLabel = projectResourceCodeTypeLabel(resource.path); - const documentPreview = - !jsonPresentation && - preview.status === 'loaded' && - preview.preview.content !== undefined && - previewVariant !== null && - previewVariant !== 'code' - ? projectResourceDocumentPreviewText( - preview.preview.content, - previewVariant === 'markdown', - ) - : ''; + /** + * 文档卡(markdown / 纯文本,且不是 JSON 或 UI 设计):**卡面不显示正文**, + * 只给居中的文档图标。 + * + * 判据与代码卡同源,只看路径分流(`previewVariant`),**不依赖是否读到内容**: + * 等读完再决定画什么,只会把"半截正文"和多余的重排一起留在链路上。 + * 正文仍在独立详情浮层里按原文 / 代码 / UI JSON 口径读取,读取预算与权限不变。 + */ + const isDocumentCard = + !jsonPresentation && previewVariant !== null && previewVariant !== 'code'; useEffect(() => { const element = cardRef.current; @@ -983,13 +981,15 @@ const ResourceCard = memo(function ResourceCard({ ); } - if (previewVariant !== null && documentPreview) { + /** + * 文档卡:居中图标,**不显示正文摘要**(旧实现把前 3 行正文铺在卡面上, + * 与"卡面不显示无效正文片段"冲突)。原文预览、代码块与 UI JSON 识别都留在 + * 独立详情浮层,卡面这条链路不再读内容做展示。 + */ + if (isDocumentCard) { return ( - - {documentPreview} + + ); } @@ -1073,6 +1073,22 @@ const ResourceCard = memo(function ResourceCard({ + {/* + 卡面名称:**所有类型卡统一显示正式资源名**(`resource.label`)。 + + 它来自现有正式命名链路——manifest 资产的 `localPath` 就是生成时的 + `assetName` 落盘名,用户重命名也会改写同一份 `localPath`,因此这里消费 + 投影 label 即可,不从临时输入或历史任务名另取一份显示名,也不持久化第二份 + 名称。名称长了由 CSS 单行省略,完整名称在 `title` 上;`data-resource-name` + 是给端到端验收的稳定判据。 + */} + + {resource.label} + ` 与列表符号(保留缩进层级); - * - 去掉强调 / 行内代码 / 图片 / 链接的标记符号,保留可见文字; - * - **不折叠换行** —— 卡面要的是"前几行",压成一行就看不出结构了。 - */ -function stripMarkdownMarkers(content: string): string { - return content - .replace(/^\s*(?:```|~~~).*$/gmu, '') - .replace(/^\s{0,3}#{1,6}\s*/gmu, '') - .replace(/^\s{0,3}>\s?/gmu, '') - .replace(/^\s*[-+*]\s+/gmu, '') - .replace(/!\[([^\]]*)\]\([^)]*\)/gu, '$1') - .replace(/\[([^\]]+)\]\([^)]*\)/gu, '$1') - .replace(/`{1,3}([^`]*)`{1,3}/gu, '$1') - .replace(/\*\*([^*]+)\*\*/gu, '$1') - .replace(/__([^_]+)__/gu, '$1') - .replace(/(^|[^*])\*([^*\n]+)\*/gu, '$1$2') - .replace(/(^|[^_])_([^_\n]+)_/gu, '$1$2'); -} - -/** - * 文档卡面的前几行预览文本。 - * - * `isMarkdown` 为 `true` 时先做轻量标记清理;纯文本原样输出。 - * 逐行去掉多余空白(缩进保留最多 2 个空格)并按 `PROJECT_RESOURCE_CARD_PREVIEW_LINE_LENGTH` - * 单行截断,最多取 `PROJECT_RESOURCE_CARD_PREVIEW_LINE_LIMIT` 行。 - */ -export function projectResourceDocumentPreviewText( - content: string, - isMarkdown: boolean, -): string { - const normalized = isMarkdown ? stripMarkdownMarkers(content) : content; - const lines: string[] = []; - for (const rawLine of normalized.split(/\r?\n/u)) { - const line = rawLine.replace(/\t/gu, ' ').replace(/\s+$/u, ''); - const trimmed = line.trimStart(); - if (!trimmed) { - continue; - } - const indent = line.slice(0, line.length - trimmed.length).slice(0, 2); - const text = `${indent}${trimmed}`; - lines.push( - text.length > PROJECT_RESOURCE_CARD_PREVIEW_LINE_LENGTH - ? `${text.slice(0, PROJECT_RESOURCE_CARD_PREVIEW_LINE_LENGTH)}…` - : text, - ); - if (lines.length >= PROJECT_RESOURCE_CARD_PREVIEW_LINE_LIMIT) { - break; - } - } - return lines.join('\n'); -} diff --git a/apps/ai-game-creator-shell/tests/appSurface/project-development.suite.ts b/apps/ai-game-creator-shell/tests/appSurface/project-development.suite.ts index 21c3f481c..02c8f889a 100644 --- a/apps/ai-game-creator-shell/tests/appSurface/project-development.suite.ts +++ b/apps/ai-game-creator-shell/tests/appSurface/project-development.suite.ts @@ -2979,7 +2979,13 @@ export function registerProjectWorkbenchFoundationTests() { await showResourcePage('角色与对象'); let heroDetailButton = await findResourceSelectButton('hero.png'); const heroCard = heroDetailButton.closest('.game-resource-card'); - expect(heroCard?.textContent).not.toContain('hero.png'); + // 卡面名称就是正式资源名(manifest `localPath` 的文件名,生成时来自 assetName、 + // 重命名时同步改写),长名由 CSS 省略、完整名在 `title` 上。 + const heroName = heroCard?.querySelector('.game-resource-card-name'); + expect(heroName?.textContent).toBe('hero.png'); + expect(heroName?.getAttribute('title')).toBe('hero.png'); + expect(heroName?.getAttribute('data-resource-name')).toBe('hero.png'); + // 卡面仍然只给"名称 + 类型",不铺完整路径与来源这类详细文本。 expect(heroCard?.textContent).not.toContain('assets/hero.png'); expect(heroCard?.textContent).not.toContain('Agent 生成'); // 角标显示的是**资源类型**(功能分类),等于它所在的画布栏目;不再是"图片"这类媒体类型。 @@ -3003,13 +3009,21 @@ export function registerProjectWorkbenchFoundationTests() { }); await showResourcePage('待归类'); act(() => observer.triggerVisible()); - const documentSummary = await screen.findByText(/这是安全的卡片正文摘要。/); - // 卡面正文按扩展名走文档分支(媒体类型),但角标是资源类型:`design-document` 不在 - // canonical 目录里 → 落「待归类」,与它所在的栏目一致(这正是改前"标着文档、却在待归类栏"的分叉)。 + // 文档卡卡面**不铺正文**:只显示居中图标 + 正式资源名,正文只在独立的文档详情浮层里读。 + const documentCard = ( + await findResourceSelectButton('design.md') + ).closest('.game-resource-card'); expect( - documentSummary - .closest('.game-resource-card') - ?.querySelector('[data-resource-type="待归类"]'), + documentCard?.querySelector('.game-resource-card-name')?.textContent, + ).toBe('design.md'); + expect( + documentCard?.querySelector('.game-resource-card-document-visual'), + ).not.toBeNull(); + expect(documentCard?.textContent).not.toContain('这是安全的卡片正文摘要。'); + // 角标仍是资源类型:`design-document` 不在 canonical 目录里 → 落「待归类」, + // 与它所在的栏目一致(这正是改前"标着文档、却在待归类栏"的分叉)。 + expect( + documentCard?.querySelector('[data-resource-type="待归类"]'), ).not.toBeNull(); await showResourcePage('角色与对象'); expect( diff --git a/apps/ai-game-creator-shell/tests/resourceCanvasCardName.test.tsx b/apps/ai-game-creator-shell/tests/resourceCanvasCardName.test.tsx new file mode 100644 index 000000000..60eb78499 --- /dev/null +++ b/apps/ai-game-creator-shell/tests/resourceCanvasCardName.test.tsx @@ -0,0 +1,360 @@ +/** @vitest-environment jsdom */ +import { readFileSync } from 'node:fs'; +import { resolve } from 'node:path'; + +import { afterEach, describe, expect, it, vi } from 'vitest'; + +import type { GameCreationAppManifest } from '../../../packages/shared/src/contracts/gameCreationApp'; +import { + createGameCreationAppManifest, + findResourceSelectButton, + fireEvent, + installResizeObserverStub, + ProjectDevelopmentView, + React, + render, + screen, + waitFor, + within, +} from './appSurface/harness'; + +vi.mock('@tauri-apps/api/core', async () => ({ + ...(await vi.importActual( + '@tauri-apps/api/core', + )), + invoke: (command: string, args?: Record) => + window.__TAURI__!.core.invoke(command, args), +})); + +const PROJECT_PATH = '/tmp/workbench-card-name'; +/** + * 生成落盘名的真实形状:`assets/canvas-generated/{毫秒}-{assetName}.{ext}`。 + * 卡面名称必须消费这条正式链路的结果,而不是另造一份显示名。 + */ +const GENERATED_IMAGE_NAME = + '1758000000000-夜行侠主角三视图与战斗姿态设定稿.png'; +const GENERATED_IMAGE_PATH = `assets/canvas-generated/${GENERATED_IMAGE_NAME}`; +const DOCUMENT_BODY = '# 玩法摘要\n\n这段正文只应出现在独立详情浮层里。'; + +function createCardNameManifest(): GameCreationAppManifest { + const manifest = createGameCreationAppManifest( + 'workbench-card-name', + '卡片名称测试', + ); + manifest.assets = [ + { + id: 'asset-hero', + kind: 'character', + mediaType: 'image/png', + localPath: 'assets/hero.png', + source: { kind: 'generated' }, + }, + { + id: 'asset-scene', + kind: 'scene', + mediaType: 'image/png', + localPath: GENERATED_IMAGE_PATH, + source: { kind: 'generated' }, + }, + { + id: 'asset-notes', + kind: 'design-document', + mediaType: 'text/markdown', + localPath: 'memory/设计文档.md', + source: { kind: 'generated' }, + }, + { + id: 'asset-code', + kind: 'code', + mediaType: 'text/html', + localPath: 'game/index.html', + source: { kind: 'generated' }, + }, + { + id: 'asset-bgm', + kind: 'background-music', + mediaType: 'audio/mpeg', + localPath: 'assets/theme.mp3', + source: { kind: 'generated' }, + }, + ]; + manifest.versions = [ + { + versionId: 'version-initial', + parentVersionId: null, + projectRevision: 1, + resourceBindings: [], + createdReason: 'initial', + createdAt: 1, + }, + ]; + return manifest; +} + +function installInvoke( + implementation: ( + command: string, + args?: Record, + ) => Promise, +) { + const invoke = vi.fn(implementation); + ( + window as unknown as { + __TAURI__?: { core?: { invoke?: typeof invoke } }; + } + ).__TAURI__ = { core: { invoke } }; + return invoke; +} + +function installManifestInvoke(projectId: string) { + return installInvoke(async (command, args) => { + if (command === 'read_local_project_resource_graph') { + const ids = (args?.resources as Array<{ resourceId: string }>).map( + (item) => item.resourceId, + ); + return { + resourceIds: ids, + referenceEdges: [], + taskFlows: [], + producerAssignments: [], + dependencyDepths: ids.map((resourceId) => ({ + resourceId, + dependencyDepth: 0, + })), + connectionIndex: ids.map((resourceId) => ({ + resourceId, + upstreamReferenceResourceIds: [], + downstreamReferenceResourceIds: [], + referenceEdgeIds: [], + taskFlowIds: [], + })), + unresolvedReferenceResourceIds: [], + cyclicResourceIds: [], + cyclicTaskIds: [], + producerMappingTruncated: false, + }; + } + if ( + command === 'read_local_project_resource_canvas_layout' || + command === 'update_local_project_resource_canvas_layout' + ) { + const layout = { + schemaVersion: 'game-creator-resource-layout.v1', + projectId, + mode: args?.mode, + revision: Number(args?.expectedRevision ?? 0) + 1, + positions: args?.positions ?? [], + updatedAt: 1, + }; + return command.startsWith('update_') + ? { status: 'updated', layout } + : layout; + } + if (command === 'read_local_project_image_preview') { + return { + path: String(args?.relativePath ?? ''), + mediaType: 'image/png', + byteLen: 12, + dataUrl: 'data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAEAAAAB', + }; + } + if (command === 'read_local_project_text_preview') { + return { + path: String(args?.relativePath ?? ''), + mediaType: 'text/markdown', + byteLen: DOCUMENT_BODY.length, + content: DOCUMENT_BODY, + }; + } + if (command === 'read_local_project_media_preview') { + return { + path: String(args?.relativePath ?? ''), + mediaType: 'audio/mpeg', + byteLen: 32, + dataUrl: 'data:audio/mpeg;base64,SUQz', + }; + } + if ( + command === 'list_pending_local_project_resource_edits' || + command === 'list_local_project_asset_generations' + ) { + return []; + } + throw new Error(`unexpected command: ${command}`); + }); +} + +/** 切栏目:左侧大纲导航已删除,一律走「资源总览」的栏目缩略卡片。 */ +async function openResourceBookCategory(label: string) { + if (document.querySelector('[data-resource-book-view="child"]')) { + fireEvent.click(await screen.findByRole('button', { name: '收起资源' })); + await waitFor(() => + expect( + document.querySelector('[data-resource-book-view="main"]'), + ).not.toBeNull(), + ); + } + fireEvent.click(await screen.findByRole('button', { name: `打开${label}` })); + await waitFor(() => + expect( + document.querySelector('[data-resource-book-view="child"]'), + ).not.toBeNull(), + ); +} + +/** 卡面名称节点断言:文本、`title` 与稳定 DOM 判据都是同一个正式资源名。 */ +async function expectCardName(name: string) { + const card = (await findResourceSelectButton(name)).closest( + '.game-resource-card', + )!; + const nameNode = card.querySelector('.game-resource-card-name'); + expect(nameNode?.textContent).toBe(name); + expect(nameNode?.getAttribute('title')).toBe(name); + expect(nameNode?.getAttribute('data-resource-name')).toBe(name); + return card; +} + +function renderWorkbench(manifest: GameCreationAppManifest) { + return render( + React.createElement(ProjectDevelopmentView, { + projectName: manifest.name, + projectPath: PROJECT_PATH, + manifest, + attachments: [], + recentRunStatus: null, + recentRunStopReason: null, + supervisor: React.createElement('div', null, '项目总控'), + onHomeOpen: vi.fn(), + onProjectsOpen: vi.fn(), + }), + ); +} + +function stylesSource() { + return readFileSync( + resolve(process.cwd(), 'apps/ai-game-creator-shell/src/styles.css'), + 'utf8', + ); +} + +/** 取 CSS 源文件里某条规则的声明体。 */ +function ruleBody(styles: string, selector: string) { + const match = new RegExp(`${selector}\\s*\\{([^}]*)\\}`, 'su').exec(styles); + expect(match, `${selector} 规则缺失`).not.toBeNull(); + return match![1]!; +} + +afterEach(() => { + delete (window as unknown as { __TAURI__?: unknown }).__TAURI__; +}); + +describe('资源卡名称与文档卡卡面', () => { + /** + * 名称口径:所有类型卡都显示**正式资源 label**——manifest `localPath` 的文件名。 + * 生成时它是 `assetName` 的落盘名,用户重命名会改写同一份 `localPath`, + * 卡片不另取临时输入或历史任务名,也不持久化第二份显示名。 + */ + it('所有类型卡都显示正式资源名,且不把目录路径铺到卡面上', async () => { + installResizeObserverStub(); + installManifestInvoke('workbench-card-name'); + renderWorkbench(createCardNameManifest()); + + await openResourceBookCategory('角色与对象'); + const heroCard = await expectCardName('hero.png'); + expect(heroCard.textContent).not.toContain('assets/'); + expect(heroCard.textContent).not.toContain('Agent 生成'); + + await openResourceBookCategory('场景与环境'); + // 长名不被截断在数据层:这里拿到的是完整名,单行省略只由 CSS 负责, + // 用户悬停 `title` 仍能读到完整名称。 + const sceneCard = await expectCardName(GENERATED_IMAGE_NAME); + expect(sceneCard.textContent).not.toContain('assets/'); + + await openResourceBookCategory('音频'); + await expectCardName('theme.mp3'); + + await openResourceBookCategory('待归类'); + await expectCardName('设计文档.md'); + await expectCardName('index.html'); + + await openResourceBookCategory('项目版本'); + await expectCardName('版本 1'); + }); + + it('文档卡以居中图标呈现,正文只在独立详情里读', async () => { + installResizeObserverStub(); + const invoke = installManifestInvoke('workbench-card-name'); + renderWorkbench(createCardNameManifest()); + + await openResourceBookCategory('待归类'); + const documentCard = await expectCardName('设计文档.md'); + // 先证明正文**确实读到过**:读取链路没有被这次改动掐掉,卡面仍旧不铺摘要。 + await waitFor(() => + expect(documentCard.getAttribute('data-preview-status')).toBe('loaded'), + ); + expect( + invoke.mock.calls.some( + ([command, args]) => + command === 'read_local_project_text_preview' && + args?.relativePath === 'memory/设计文档.md', + ), + ).toBe(true); + expect( + documentCard.querySelector('.game-resource-card-document-visual'), + ).not.toBeNull(); + expect(documentCard.textContent).not.toContain(DOCUMENT_BODY); + expect(documentCard.textContent).not.toContain( + '这段正文只应出现在独立详情浮层里', + ); + expect( + documentCard.querySelector('.game-resource-card-document-summary'), + ).toBeNull(); + // 代码卡同样只在卡面给图标 + 类型标签,名称照常显示。 + const codeCard = await expectCardName('index.html'); + expect( + codeCard.querySelector('.game-resource-card-code-visual'), + ).not.toBeNull(); + + // 独立详情不回归:选中文档卡 → 工具条「预览」→ 正文按原文读到浮层里。 + fireEvent.click( + within(documentCard).getByRole('button', { + name: '选中资源:待归类 设计文档.md', + }), + ); + fireEvent.click(await screen.findByRole('button', { name: '预览' })); + const dialog = await screen.findByRole('dialog', { name: '文档预览' }); + await waitFor(() => + expect(dialog.textContent).toContain('这段正文只应出现在独立详情浮层里'), + ); + }); + + /** + * jsdom 不加载样式表:长名单行省略、名称条不挡点击这些声明本身在源码层钉住; + * 真机像素表现仍需人工核验。 + */ + it('卡面名称的省略与层级声明固定在样式表里', () => { + const styles = stylesSource(); + const nameRule = ruleBody(styles, '\\.game-resource-card-name'); + expect(nameRule).toMatch(/overflow:\s*hidden/u); + expect(nameRule).toMatch(/text-overflow:\s*ellipsis/u); + expect(nameRule).toMatch(/white-space:\s*nowrap/u); + expect(nameRule).toMatch(/pointer-events:\s*none/u); + // 名称条铺在整卡选中按钮之上、右下角播放钮之下,两边都不能被盖住。 + expect(nameRule).toMatch(/z-index:\s*2/u); + expect( + ruleBody( + styles, + "\\.game-resource-card\\[data-preview-kind='audio'\\] \\.game-resource-card-name", + ), + ).toMatch(/padding-right:\s*50px/u); + // 旧的卡面正文摘要样式必须随正文一起消失,不留墓碑规则。 + expect(styles).not.toContain('.game-resource-card-document-summary'); + // 文档卡与代码卡共用同一套居中排布。 + const documentVisualRule = ruleBody( + styles, + '\\.game-resource-card-code-visual,\\s*\\.game-resource-card-document-visual', + ); + expect(documentVisualRule).toMatch(/place-items:\s*center/u); + expect(documentVisualRule).toMatch(/align-content:\s*center/u); + }); +}); diff --git a/apps/ai-game-creator-shell/tests/resourceRename.test.tsx b/apps/ai-game-creator-shell/tests/resourceRename.test.tsx index 3846ae0bf..df0ba4b55 100644 --- a/apps/ai-game-creator-shell/tests/resourceRename.test.tsx +++ b/apps/ai-game-creator-shell/tests/resourceRename.test.tsx @@ -231,11 +231,16 @@ describe('素材重命名前端链路', () => { rendered.rerenderWith(renamedManifest); await waitFor(() => { - expect( - screen.getByRole('button', { + const renamedCard = screen + .getByRole('button', { name: '选中资源:角色与对象 hero-v2.png', - }), - ).not.toBeNull(); + }) + .closest('.game-resource-card'); + // 卡面名称与重命名后的 manifest `localPath` 同步:名称就是正式文件名的投影, + // 不存在第二份需要一起改的显示名。 + const nameNode = renamedCard?.querySelector('.game-resource-card-name'); + expect(nameNode?.textContent).toBe('hero-v2.png'); + expect(nameNode?.getAttribute('title')).toBe('hero-v2.png'); }); });