画布资源卡统一显示正式资源名并简化文档卡卡面

- 资源卡新增卡面名称条,所有类型卡统一显示正式资源 label,长名单行省略、完整名进 title

- 文档卡卡面改为居中图标,不再把正文前几行铺在卡面上,正文只在独立详情浮层读取

- 删除卡面正文摘要的取文本 helper 与行数/长度常量,移除对应 CSS 规则不留墓碑

- 名称条按媒体卡让出右下播放钮位置,替换血缘角标上移避免遮住名称

- 新增资源卡名称与文档卡卡面用例,并同步重命名链路与卡面正文的既有断言
This commit is contained in:
2026-09-17 17:06:37 +08:00
parent a7b2b0e23b
commit 2628b83d4b
6 changed files with 480 additions and 114 deletions
+51 -22
View File
@@ -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%;
@@ -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({
</span>
);
}
if (previewVariant !== null && documentPreview) {
/**
* **** 3
* "卡面不显示无效正文片段" UI JSON
*
*/
if (isDocumentCard) {
return (
<span
className="game-resource-card-document-summary"
data-document-preview-variant={previewVariant}
>
{documentPreview}
<span className="game-resource-card-document-visual">
<FileText size={30} aria-hidden="true" />
</span>
);
}
@@ -1073,6 +1073,22 @@ const ResourceCard = memo(function ResourceCard({
<span className="game-resource-card-visual" aria-hidden="true">
{visual}
</span>
{/*
****`resource.label`
manifest `localPath`
`assetName` `localPath`
label
CSS `title` `data-resource-name`
*/}
<span
className="game-resource-card-name"
data-resource-name={resource.label}
title={resource.label}
>
{resource.label}
</span>
<span
className="game-resource-card-type-badge"
data-resource-type={cardTypeLabel}
@@ -330,9 +330,12 @@ export function projectResourceCardPreviewKind(
* 文档类卡片(`document` / `code`)的卡面分流结论。
*
* 三种落点:
* - `markdown`显示前几行,预览前做轻量标记清理
* - `markdown`卡面只画居中文档图标,正文只在独立详情浮层按 Markdown 渲染
* - `code`:**完全不读内容**,卡面改画代码图标 + 类型标签;
* - `plain-text`显示前几行,不做标记清理
* - `plain-text`卡面只画居中文档图标,正文只在独立详情浮层按原文展示
*
* 三者都判**路径**,不依赖是否读到内容:卡面本来就不铺正文,等读完再决定画什么
* 只会把"半截正文"和多余的重排一起留在链路上。
*/
export type ProjectResourceCardPreviewVariant =
| 'markdown'
@@ -400,64 +403,3 @@ export function projectResourceCardPreviewIdentity(input: {
input.previewVersion ?? '',
]);
}
/** 卡面文档预览的最大行数(超出由 CSS 省略号截断)。 */
export const PROJECT_RESOURCE_CARD_PREVIEW_LINE_LIMIT = 3;
/** 单行预览的最大字符数,超出截断成省略号。 */
export const PROJECT_RESOURCE_CARD_PREVIEW_LINE_LENGTH = 72;
/**
* Markdown 的**轻量**清理:只去掉标记符号,不是渲染器。
*
* - 去掉围栏标记行(``` / ~~~),保留代码内容本身;
* - 去掉行首标题 `#`、引用 `>` 与列表符号(保留缩进层级);
* - 去掉强调 / 行内代码 / 图片 / 链接的标记符号,保留可见文字;
* - **不折叠换行** —— 卡面要的是"前几行",压成一行就看不出结构了。
*/
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');
}
@@ -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<HTMLElement>('.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(
@@ -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<typeof import('@tauri-apps/api/core')>(
'@tauri-apps/api/core',
)),
invoke: (command: string, args?: Record<string, unknown>) =>
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<string, unknown>,
) => Promise<unknown>,
) {
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<HTMLElement>(
'.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);
});
});
@@ -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');
});
});