删除资源画布左侧的栏目大纲导航:切栏目改由总览卡片、所有资源入口与下一页承担
- index.tsx:整条删除 `ResourceBookOutlineNav` 组件(`.game-resource-outline` / `aria-label="资源栏目大纲"` / 栏目图标 + 栏目文字 + 未读红点)以及栏目页与「所有资源」页两处挂载点;`ResourceBookAllResourcesPage` 不再接收只服务导航的 `unreadCategories` / `onOpen`。 - index.tsx:一并清掉只服务这条导航的「未读红点」状态链(`unreadResourceCategoryState`、`resourceCategorySnapshotRef`、`resourceIdsByCategory`、`viewedResourceCategory` 与对应 effect),不留死代码、不留墓碑注释。 - styles.css:删除全部 `.game-resource-outline*` 规则(悬浮 Dock、常态/悬停态、图标列宽、`.game-resource-outline-label`、`.game-resource-outline-unread`,共 106 行)。 - 保留未动:分区栏目本身、栏目标题栏、资源卡、总览缩略卡片、第 0 张「所有资源」入口、「所有资源」汇总页、搜索、总览、快速编辑与选中工具条。 - 测试:切栏目 helper(appSurface/project-development、appSurface/home、resourceRename、resourceVersionSwitch)改为走「资源总览」缩略卡片(需要时先点「收起资源」回总览),不再点导航;栏目顺序断言改钉在总览缩略卡片的 `aria-label` 序列上(第 0 张「所有资源」+ 固定七栏目),断言内容未放宽。 - 测试:按「四不写」删除只守这条导航的断言——大纲常态可读性的 CSS 声明用例、大纲条目图标/文字的 DOM 用例(该用例保留其中与搜索浮层相关的断言并改名)、未读红点用例(该用例保留"只登记游戏代码的项目照常分页并落在待归类"的部分并改名)、空项目与「所有资源」页里的导航存在性断言。 - 测试:新增反向守卫 + 切栏目链路用例(`has no outline nav and keeps every section reachable through the remaining entries`):断言 `资源栏目大纲` 标签、`.game-resource-outline` 类名与对应 `<nav>` 都不再渲染,并逐条走通总览缩略卡片(含空栏目)→ 下一页 → 标题栏「资源总览」→ 「所有资源」入口往返。 - 测试:`preserves independent art viewports across sort and workbench mode switches` 保留全部原断言,只去掉排序模式切换后那些"点当前栏目"的空操作往返(旧导航点在同栏目上是提前返回,改 helper 后变成真实往返,会在 jsdom 里触发重新 fit)。
This commit is contained in:
@@ -6416,112 +6416,6 @@ iframe.preview-frame {
|
||||
height: 100%;
|
||||
}
|
||||
|
||||
.game-resource-outline {
|
||||
position: absolute;
|
||||
top: 50%;
|
||||
left: 12px;
|
||||
z-index: 4;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 4px;
|
||||
width: max-content;
|
||||
max-width: 180px;
|
||||
max-height: calc(100% - 24px);
|
||||
padding: 6px;
|
||||
overflow: auto;
|
||||
/* 常态就给完整尺寸和 Dock 底板:只留 11px 文字、去掉图标和底色时,用户认不出
|
||||
这是栏目入口。悬停/键盘聚焦仍在同一套 token 上加强(见下方 :hover 规则)。 */
|
||||
border: 1px solid var(--platform-subpanel-border);
|
||||
border-radius: 14px;
|
||||
background: var(--platform-nav-fill);
|
||||
box-shadow: 0 6px 18px rgb(112 70 52 / 12%);
|
||||
opacity: 1;
|
||||
transform: translateY(-50%);
|
||||
transform-origin: left center;
|
||||
transition:
|
||||
border-color 160ms ease,
|
||||
opacity 160ms ease,
|
||||
box-shadow 160ms ease;
|
||||
backdrop-filter: blur(6px);
|
||||
}
|
||||
|
||||
.game-resource-outline:hover,
|
||||
.game-resource-outline:has(> button:focus-visible) {
|
||||
border-color: var(--platform-nav-active-border);
|
||||
background: var(--platform-nav-fill);
|
||||
box-shadow: var(--platform-nav-active-shadow);
|
||||
opacity: 1;
|
||||
transform: translateY(-50%);
|
||||
backdrop-filter: blur(10px);
|
||||
}
|
||||
|
||||
.game-resource-outline button {
|
||||
display: grid;
|
||||
grid-template-columns: 18px minmax(0, 1fr);
|
||||
align-items: center;
|
||||
gap: 6px;
|
||||
min-height: 34px;
|
||||
padding: 0 9px;
|
||||
border: 1px solid transparent;
|
||||
border-radius: 8px;
|
||||
background: transparent;
|
||||
color: var(--platform-nav-item-text);
|
||||
cursor: pointer;
|
||||
text-align: left;
|
||||
transition:
|
||||
background-color 160ms ease,
|
||||
border-color 160ms ease;
|
||||
}
|
||||
|
||||
/* 图标与栏目文字常态就在位(上面两条规则),悬停/键盘聚焦只加强 Dock 底板与当前项高亮。 */
|
||||
.game-resource-outline button svg {
|
||||
width: 14px;
|
||||
min-width: 0;
|
||||
opacity: 1;
|
||||
transform: scale(1);
|
||||
transition:
|
||||
opacity 120ms ease,
|
||||
transform 160ms ease;
|
||||
}
|
||||
|
||||
.game-resource-outline button:hover,
|
||||
.game-resource-outline button:focus-visible {
|
||||
background: var(--platform-nav-item-hover-fill);
|
||||
outline: 0;
|
||||
}
|
||||
|
||||
.game-resource-outline button.is-active {
|
||||
border-color: var(--platform-nav-active-border);
|
||||
background: var(--platform-nav-active-fill);
|
||||
color: var(--platform-nav-item-text-active);
|
||||
box-shadow: var(--platform-nav-active-shadow);
|
||||
}
|
||||
|
||||
.game-resource-outline button strong {
|
||||
overflow: hidden;
|
||||
font-size: 12px;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.game-resource-outline button .game-resource-outline-label {
|
||||
position: relative;
|
||||
overflow: visible;
|
||||
}
|
||||
|
||||
.game-resource-outline-unread {
|
||||
position: absolute;
|
||||
top: -2px;
|
||||
right: -6px;
|
||||
width: 6px;
|
||||
height: 6px;
|
||||
border: 1px solid var(--platform-nav-fill);
|
||||
border-radius: 50%;
|
||||
background: #e5484d;
|
||||
box-shadow: 0 0 0 1px rgb(229 72 77 / 18%);
|
||||
pointer-events: none;
|
||||
}
|
||||
|
||||
.game-resource-page {
|
||||
display: grid;
|
||||
grid-template-rows: auto minmax(0, 1fr) auto;
|
||||
|
||||
@@ -165,7 +165,6 @@ import {
|
||||
initialResourceBookState,
|
||||
isResourceBookAllTarget,
|
||||
RESOURCE_BOOK_ALL_TARGET,
|
||||
type ResourceBookCategory,
|
||||
resourceBookReducer,
|
||||
type ResourceBookState,
|
||||
type ResourceBookTarget,
|
||||
@@ -1042,81 +1041,21 @@ function ResourceBookTitleBar({
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* 资源栏目大纲。栏目页与「所有资源」页共用同一条入口带:两页都能横向切到任一栏目,
|
||||
* 只是「所有资源」页在最前面多一颗指向自己的入口。
|
||||
*/
|
||||
function ResourceBookOutlineNav({
|
||||
categories,
|
||||
activeCategory,
|
||||
unreadCategories,
|
||||
onOpen,
|
||||
includeAllTarget = false,
|
||||
}: {
|
||||
categories: readonly ResourceBookCategory[];
|
||||
activeCategory: ResourceBookTarget | null;
|
||||
unreadCategories: ReadonlySet<ResourceBookCategory>;
|
||||
onOpen: (category: ResourceBookTarget) => void;
|
||||
includeAllTarget?: boolean;
|
||||
}) {
|
||||
const items: ResourceBookTarget[] = includeAllTarget
|
||||
? [RESOURCE_BOOK_ALL_TARGET, ...categories]
|
||||
: [...categories];
|
||||
return (
|
||||
<nav className="game-resource-outline" aria-label="资源栏目大纲">
|
||||
{items.map((category) => {
|
||||
const Icon = categoryIcons[category];
|
||||
// 「所有资源」是汇总入口,不属于任何栏目,因此没有「有新资源」一说。
|
||||
const hasUnreadResources =
|
||||
!isResourceBookAllTarget(category) && unreadCategories.has(category);
|
||||
return (
|
||||
<button
|
||||
key={category}
|
||||
type="button"
|
||||
aria-label={`${categoryLabels[category]}${
|
||||
hasUnreadResources ? ',有新资源' : ''
|
||||
}`}
|
||||
aria-current={category === activeCategory ? 'page' : undefined}
|
||||
className={category === activeCategory ? 'is-active' : undefined}
|
||||
onClick={() => onOpen(category)}
|
||||
>
|
||||
<Icon size={14} aria-hidden="true" />
|
||||
<strong className="game-resource-outline-label">
|
||||
{categoryLabels[category]}
|
||||
{hasUnreadResources ? (
|
||||
<span
|
||||
className="game-resource-outline-unread"
|
||||
aria-hidden="true"
|
||||
/>
|
||||
) : null}
|
||||
</strong>
|
||||
</button>
|
||||
);
|
||||
})}
|
||||
</nav>
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* 「所有资源」页:把每个非空栏目的资源卡摊在同一张滚动网格里。
|
||||
*
|
||||
* 复用既有分区机制的三件事:栏目大纲(可横向切栏目)、栏目标题栏(图标 + 文案 + N 项
|
||||
* 计数徽标)、以及栏目页同一颗资源卡片渲染器;差别只是这里一次铺开全部栏目,
|
||||
* 卡片按格子排布而不是画布坐标。网格里的卡片是只读的(不接拖拽),
|
||||
* 因为它们的坐标不属于任何栏目画布。
|
||||
* 复用既有分区机制的两件事:栏目标题栏(图标 + 文案 + N 项计数徽标)、以及栏目页同一颗
|
||||
* 资源卡片渲染器;差别只是这里一次铺开全部栏目,卡片按格子排布而不是画布坐标。网格里的
|
||||
* 卡片是只读的(不接拖拽),因为它们的坐标不属于任何栏目画布。
|
||||
*/
|
||||
function ResourceBookAllResourcesPage({
|
||||
sections,
|
||||
totalCount,
|
||||
unreadCategories,
|
||||
onOpen,
|
||||
onCollapse,
|
||||
renderCard,
|
||||
}: {
|
||||
sections: ResourceBookAllResourcesSection[];
|
||||
totalCount: number;
|
||||
unreadCategories: ReadonlySet<ResourceBookCategory>;
|
||||
onOpen: (category: ResourceBookTarget) => void;
|
||||
onCollapse: () => void;
|
||||
renderCard: (
|
||||
resource: ProjectResource,
|
||||
@@ -1132,19 +1071,11 @@ function ResourceBookAllResourcesPage({
|
||||
options?: { draggable?: boolean },
|
||||
) => ReactNode;
|
||||
}) {
|
||||
const outlineCategories = sections.map((section) => section.category);
|
||||
return (
|
||||
<div
|
||||
className="game-resource-page-shell"
|
||||
data-resource-book-all-page="true"
|
||||
>
|
||||
<ResourceBookOutlineNav
|
||||
categories={outlineCategories}
|
||||
activeCategory={RESOURCE_BOOK_ALL_TARGET}
|
||||
unreadCategories={unreadCategories}
|
||||
onOpen={onOpen}
|
||||
includeAllTarget
|
||||
/>
|
||||
<section
|
||||
className="game-resource-page game-resource-all-page"
|
||||
aria-label={categoryLabels[RESOURCE_BOOK_ALL_TARGET]}
|
||||
@@ -1431,15 +1362,6 @@ export default function ProjectDevelopmentView({
|
||||
);
|
||||
const [activeResourceCategory, setActiveResourceCategory] =
|
||||
useState<ResourceCategory | null>(null);
|
||||
const [unreadResourceCategoryState, setUnreadResourceCategoryState] =
|
||||
useState<{
|
||||
scopeKey: string;
|
||||
categories: Set<ResourceCategory>;
|
||||
}>(() => ({ scopeKey: '', categories: new Set() }));
|
||||
const resourceCategorySnapshotRef = useRef<{
|
||||
scopeKey: string;
|
||||
resourceIdsByCategory: Map<ResourceCategory, Set<string>>;
|
||||
} | null>(null);
|
||||
const [selectedResourceIds, setSelectedResourceIds] = useState<string[]>([]);
|
||||
const [quickEditSourceLayer, setQuickEditSourceLayer] =
|
||||
useState<CanvasLayer | null>(null);
|
||||
@@ -2173,8 +2095,8 @@ export default function ProjectDevelopmentView({
|
||||
}, [resources, selectedResourceId]);
|
||||
const normalizedSearch = searchText.trim().toLowerCase();
|
||||
/**
|
||||
* 画布可见资源只由搜索框收窄:功能分类筛选条与画布标签筛选已按用户要求移除,
|
||||
* 分区栏目本身(`categoryOrder` / 栏目大纲 / 栏目标题栏)不受影响。
|
||||
* 画布可见资源只由搜索框收窄:功能分类筛选条、画布标签筛选与左侧栏目大纲导航
|
||||
* 已按用户要求移除,分区栏目本身(`categoryOrder` / 栏目标题栏 / 总览缩略卡片)不受影响。
|
||||
*/
|
||||
const visibleResources = useMemo(
|
||||
() =>
|
||||
@@ -2220,20 +2142,6 @@ export default function ProjectDevelopmentView({
|
||||
}),
|
||||
[visibleResourcesByCategory],
|
||||
);
|
||||
const resourceIdsByCategory = useMemo(
|
||||
() =>
|
||||
new Map(
|
||||
categoryOrder.map((category) => [
|
||||
category,
|
||||
new Set(
|
||||
(projectResourcesByCategory.get(category) ?? []).map(
|
||||
(resource) => resource.id,
|
||||
),
|
||||
),
|
||||
]),
|
||||
),
|
||||
[projectResourcesByCategory],
|
||||
);
|
||||
const resourceCategoryScopeKey = useMemo(
|
||||
() => JSON.stringify([projectPath, manifest.projectId]),
|
||||
[manifest.projectId, projectPath],
|
||||
@@ -2387,63 +2295,6 @@ export default function ProjectDevelopmentView({
|
||||
resourceSearchRef.current?.focus();
|
||||
}, [resourceSearchOpen]);
|
||||
|
||||
const viewedResourceCategory =
|
||||
mode === 'resources' ? activePageCategory : null;
|
||||
useEffect(() => {
|
||||
const previousSnapshot = resourceCategorySnapshotRef.current;
|
||||
if (
|
||||
!previousSnapshot ||
|
||||
previousSnapshot.scopeKey !== resourceCategoryScopeKey
|
||||
) {
|
||||
resourceCategorySnapshotRef.current = {
|
||||
scopeKey: resourceCategoryScopeKey,
|
||||
resourceIdsByCategory,
|
||||
};
|
||||
setUnreadResourceCategoryState({
|
||||
scopeKey: resourceCategoryScopeKey,
|
||||
categories: new Set(),
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
const categoriesWithNewResources = categoryOrder.filter((category) => {
|
||||
if (category === viewedResourceCategory) {
|
||||
return false;
|
||||
}
|
||||
const previousIds =
|
||||
previousSnapshot.resourceIdsByCategory.get(category) ?? new Set();
|
||||
return Array.from(resourceIdsByCategory.get(category) ?? []).some(
|
||||
(resourceId) => !previousIds.has(resourceId),
|
||||
);
|
||||
});
|
||||
resourceCategorySnapshotRef.current = {
|
||||
scopeKey: resourceCategoryScopeKey,
|
||||
resourceIdsByCategory,
|
||||
};
|
||||
setUnreadResourceCategoryState((current) => {
|
||||
const categories =
|
||||
current.scopeKey === resourceCategoryScopeKey
|
||||
? new Set(current.categories)
|
||||
: new Set<ResourceCategory>();
|
||||
let changed = current.scopeKey !== resourceCategoryScopeKey;
|
||||
if (viewedResourceCategory && categories.delete(viewedResourceCategory)) {
|
||||
changed = true;
|
||||
}
|
||||
categoriesWithNewResources.forEach((category) => {
|
||||
if (!categories.has(category)) {
|
||||
categories.add(category);
|
||||
changed = true;
|
||||
}
|
||||
});
|
||||
return changed
|
||||
? { scopeKey: resourceCategoryScopeKey, categories }
|
||||
: current;
|
||||
});
|
||||
}, [resourceCategoryScopeKey, resourceIdsByCategory, viewedResourceCategory]);
|
||||
const unreadResourceCategories =
|
||||
unreadResourceCategoryState.scopeKey === resourceCategoryScopeKey
|
||||
? unreadResourceCategoryState.categories
|
||||
: new Set<ResourceCategory>();
|
||||
const nextPageCategory =
|
||||
activePageCategory && resourcePageCategories.length > 1
|
||||
? resourcePageCategories[
|
||||
@@ -6190,12 +6041,6 @@ export default function ProjectDevelopmentView({
|
||||
栏目页的分页画布。 */}
|
||||
{resourceBookOpensAllResources ? null : activePageCategory ? (
|
||||
<div className="game-resource-page-shell">
|
||||
<ResourceBookOutlineNav
|
||||
categories={resourcePageCategories}
|
||||
activeCategory={activePageCategory}
|
||||
unreadCategories={unreadResourceCategories}
|
||||
onOpen={openResourceBookChild}
|
||||
/>
|
||||
<section
|
||||
className="game-resource-page"
|
||||
aria-label={categoryLabels[activePageCategory]}
|
||||
@@ -6328,8 +6173,6 @@ export default function ProjectDevelopmentView({
|
||||
<ResourceBookAllResourcesPage
|
||||
sections={resourceBookAllResourcesSections}
|
||||
totalCount={canvasResources.length}
|
||||
unreadCategories={unreadResourceCategories}
|
||||
onOpen={openResourceBookChild}
|
||||
onCollapse={returnToResourceBookMain}
|
||||
renderCard={renderResourceBookCard}
|
||||
/>
|
||||
|
||||
@@ -101,10 +101,16 @@ async function openResourceBookCategory(label: string) {
|
||||
项目版本: 'version',
|
||||
};
|
||||
const category = categoryByLabel[label];
|
||||
const outline = await screen.findByLabelText('资源栏目大纲');
|
||||
fireEvent.click(
|
||||
within(outline).getByRole('button', { name: new RegExp(label) }),
|
||||
);
|
||||
// 左侧栏目大纲导航已按用户要求删除:切栏目走「资源总览」的栏目缩略卡片。
|
||||
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(() => {
|
||||
const manager = document.querySelector('[data-resource-book-view="child"]');
|
||||
expect(manager).not.toBeNull();
|
||||
|
||||
@@ -168,10 +168,16 @@ async function openResourceBookCategory(label: string) {
|
||||
项目版本: 'version',
|
||||
};
|
||||
const category = categoryByLabel[label];
|
||||
const outline = await screen.findByLabelText('资源栏目大纲');
|
||||
fireEvent.click(
|
||||
within(outline).getByRole('button', { name: new RegExp(label) }),
|
||||
);
|
||||
// 左侧栏目大纲导航已按用户要求删除:切栏目走「资源总览」的栏目缩略卡片。
|
||||
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(() => {
|
||||
const manager = document.querySelector('[data-resource-book-view="child"]');
|
||||
expect(manager).not.toBeNull();
|
||||
@@ -419,10 +425,9 @@ export function registerProjectWorkbenchFoundationTests() {
|
||||
const dependencyViewport = readViewport(artCanvas);
|
||||
expect(dependencyViewport).toBeTruthy();
|
||||
|
||||
// 切换排序模式时栏目页保持挂载:这里换的是「按依赖 / 按类型」各自的 viewport,
|
||||
// 不需要(也不再有)切栏目动作——旧的栏目大纲入口点在同栏目上是提前返回的空操作。
|
||||
fireEvent.click(screen.getByRole('button', { name: '按类型' }));
|
||||
const outline = screen.getByLabelText('资源栏目大纲');
|
||||
fireEvent.click(within(outline).getByRole('button', { name: /文档/ }));
|
||||
await openResourceBookCategory('角色与对象');
|
||||
const typeCanvas = await screen.findByRole('region', {
|
||||
name: '角色与对象',
|
||||
});
|
||||
@@ -436,7 +441,6 @@ export function registerProjectWorkbenchFoundationTests() {
|
||||
expect(typeViewport).not.toBe(dependencyViewport);
|
||||
|
||||
fireEvent.click(screen.getByRole('button', { name: '按依赖' }));
|
||||
await openResourceBookCategory('角色与对象');
|
||||
const restoredDependencyCanvas = await screen.findByRole('region', {
|
||||
name: '角色与对象',
|
||||
});
|
||||
@@ -445,7 +449,6 @@ export function registerProjectWorkbenchFoundationTests() {
|
||||
);
|
||||
|
||||
fireEvent.click(screen.getByRole('button', { name: '按类型' }));
|
||||
await openResourceBookCategory('角色与对象');
|
||||
const restoredTypeCanvas = await screen.findByRole('region', {
|
||||
name: '角色与对象',
|
||||
});
|
||||
@@ -456,7 +459,6 @@ export function registerProjectWorkbenchFoundationTests() {
|
||||
fireEvent.click(screen.getByRole('tab', { name: '运行' }));
|
||||
expect(screen.getByRole('region', { name: '运行表现层' })).not.toBeNull();
|
||||
fireEvent.click(screen.getByRole('tab', { name: '资源管理' }));
|
||||
await openResourceBookCategory('角色与对象');
|
||||
const restoredArtCanvas = await screen.findByRole('region', {
|
||||
name: '角色与对象',
|
||||
});
|
||||
@@ -734,24 +736,25 @@ export function registerProjectWorkbenchFoundationTests() {
|
||||
}
|
||||
window.dispatchEvent(new Event('resize'));
|
||||
|
||||
const outline = screen.getByLabelText('资源栏目大纲');
|
||||
// 左侧栏目大纲导航已删除:栏目顺序改由「资源总览」缩略卡片承载(第 0 张是「所有资源」)。
|
||||
expect(
|
||||
Array.from(outline.querySelectorAll('strong')).map(
|
||||
(item) => item.textContent,
|
||||
Array.from(
|
||||
document.querySelectorAll('.game-resource-book-thumbnail'),
|
||||
(thumbnail) => thumbnail.getAttribute('aria-label'),
|
||||
),
|
||||
).toEqual([
|
||||
'UI 交互',
|
||||
'角色与对象',
|
||||
'场景与环境',
|
||||
'音频',
|
||||
'文档',
|
||||
'待归类',
|
||||
'项目版本',
|
||||
'打开所有资源',
|
||||
'打开UI 交互',
|
||||
'打开角色与对象',
|
||||
'打开场景与环境',
|
||||
'打开音频',
|
||||
'打开文档',
|
||||
'打开待归类',
|
||||
'打开项目版本',
|
||||
]);
|
||||
expect(graphResourceIds).toContain('asset:section-code');
|
||||
expect(queryResourceSelectButton('section.js')).not.toBeNull();
|
||||
expect(screen.queryByRole('region', { name: '游戏代码' })).toBeNull();
|
||||
expect(outline.querySelectorAll('small')).toHaveLength(0);
|
||||
expect(screen.getByRole('region', { name: '角色与对象' })).not.toBeNull();
|
||||
expect(
|
||||
screen.getByTestId('resource-dependency-overlay-character'),
|
||||
@@ -780,9 +783,6 @@ export function registerProjectWorkbenchFoundationTests() {
|
||||
document.querySelector('.game-resource-book-transition-layer'),
|
||||
).toBeNull();
|
||||
|
||||
expect(
|
||||
within(outline).queryByRole('button', { name: /游戏代码/ }),
|
||||
).toBeNull();
|
||||
expect(screen.queryByRole('region', { name: '游戏代码' })).toBeNull();
|
||||
const dispatchViewWheel = (deltaY = 160) => {
|
||||
const scene = document.querySelector<HTMLElement>(
|
||||
@@ -968,9 +968,7 @@ export function registerProjectWorkbenchFoundationTests() {
|
||||
clientX: 100,
|
||||
clientY: 50,
|
||||
});
|
||||
fireEvent.click(
|
||||
within(outline).getByRole('button', { name: /角色与对象/ }),
|
||||
);
|
||||
fireEvent.click(screen.getByRole('button', { name: /^下一页/ }));
|
||||
expect(cardReleasePointerCapture).toHaveBeenCalledWith(91);
|
||||
fireEvent.pointerUp(dragCard!, {
|
||||
pointerId: 91,
|
||||
@@ -1030,9 +1028,7 @@ export function registerProjectWorkbenchFoundationTests() {
|
||||
screen.queryByTestId('resource-dependency-overlay-character'),
|
||||
).toBeNull();
|
||||
|
||||
fireEvent.click(
|
||||
within(outline).getByRole('button', { name: /角色与对象/ }),
|
||||
);
|
||||
await openResourceBookCategory('角色与对象');
|
||||
fireEvent.click(screen.getByRole('button', { name: '按类型' }));
|
||||
const typeCanvas = screen.getByLabelText('资源类型视图') as HTMLDivElement;
|
||||
expect(
|
||||
@@ -1250,46 +1246,17 @@ export function registerProjectWorkbenchFoundationTests() {
|
||||
expect(styleNumber(notices, 'top')).toBeGreaterThanOrEqual(titlebarHeight);
|
||||
});
|
||||
|
||||
it('keeps the outline dock readable without hover', () => {
|
||||
const styles = readFileSync(
|
||||
resolve(process.cwd(), 'apps/ai-game-creator-shell/src/styles.css'),
|
||||
'utf8',
|
||||
);
|
||||
const outline = styleRuleBody(styles, '\\.game-resource-outline');
|
||||
// 常态必须有 Dock 底板且不再整体降透明度/缩小,否则用户认不出这是栏目入口。
|
||||
expect(styleNumber(outline, 'opacity')).toBe(1);
|
||||
expect(outline).not.toMatch(/scale\(/u);
|
||||
expect(outline).toMatch(/background:\s*var\(--platform-nav-fill\)/u);
|
||||
expect(outline).toMatch(
|
||||
/border:\s*1px solid var\(--platform-subpanel-border\)/u,
|
||||
);
|
||||
|
||||
// 栏目图标常态就展开:常态把 svg 收成 width: 0、只剩 11px 文字是"认不出"的根因。
|
||||
expect(styleRuleBody(styles, '\\.game-resource-outline button')).toMatch(
|
||||
/grid-template-columns:\s*18px minmax\(0, 1fr\)/u,
|
||||
);
|
||||
const icon = styleRuleBody(styles, '\\.game-resource-outline button svg');
|
||||
expect(styleNumber(icon, 'width')).toBe(14);
|
||||
expect(styleNumber(icon, 'opacity')).toBe(1);
|
||||
expect(
|
||||
styleNumber(
|
||||
styleRuleBody(styles, '\\.game-resource-outline button strong'),
|
||||
'font-size',
|
||||
),
|
||||
).toBe(12);
|
||||
});
|
||||
|
||||
it('renders every outline entry with its icon and section label', async () => {
|
||||
it('renders the resource search overlay as a sibling of the book scene', async () => {
|
||||
const manifest = createGameCreationAppManifest(
|
||||
'workbench-outline-dock',
|
||||
'栏目大纲可读性测试',
|
||||
'workbench-search-overlay',
|
||||
'资源搜索浮层测试',
|
||||
);
|
||||
manifest.assets = [
|
||||
{
|
||||
id: 'outline-art',
|
||||
id: 'search-overlay-art',
|
||||
kind: 'character',
|
||||
mediaType: 'image/png',
|
||||
localPath: 'assets/outline-art.png',
|
||||
localPath: 'assets/search-overlay-art.png',
|
||||
source: { kind: 'generated', taskId: 'art-asset-plan' },
|
||||
},
|
||||
];
|
||||
@@ -1316,7 +1283,7 @@ export function registerProjectWorkbenchFoundationTests() {
|
||||
render(
|
||||
React.createElement(ProjectDevelopmentView, {
|
||||
projectName: manifest.name,
|
||||
projectPath: '/tmp/workbench-outline-dock',
|
||||
projectPath: '/tmp/workbench-search-overlay',
|
||||
manifest,
|
||||
attachments: [],
|
||||
recentRunStatus: null,
|
||||
@@ -1327,36 +1294,6 @@ export function registerProjectWorkbenchFoundationTests() {
|
||||
}),
|
||||
);
|
||||
|
||||
const outline = await screen.findByLabelText('资源栏目大纲');
|
||||
const entries = Array.from(outline.querySelectorAll('button'));
|
||||
expect(entries.length).toBeGreaterThan(0);
|
||||
// 分区轴固定 7 栏:6 类资产功能分类 + 末尾独立的「项目版本」。
|
||||
expect(
|
||||
entries.map((button) => button.querySelector('strong')?.textContent),
|
||||
).toEqual([
|
||||
'UI 交互',
|
||||
'角色与对象',
|
||||
'场景与环境',
|
||||
'音频',
|
||||
'文档',
|
||||
'待归类',
|
||||
'项目版本',
|
||||
]);
|
||||
// 每条栏目都是"图标 + 栏目文字"的完整入口,不是只有一行小字。
|
||||
expect(
|
||||
entries.every(
|
||||
(button) =>
|
||||
button.querySelector('svg') !== null &&
|
||||
(button.querySelector('strong')?.textContent ?? '').trim().length > 0,
|
||||
),
|
||||
).toBe(true);
|
||||
await openResourceBookCategory('角色与对象');
|
||||
expect(
|
||||
within(outline)
|
||||
.getByRole('button', { name: '角色与对象' })
|
||||
.getAttribute('aria-current'),
|
||||
).toBe('page');
|
||||
|
||||
// 搜索浮层与栏目标题栏分属两条排布带,这不是 CSS 声明能表达的:画本场景是管理区里
|
||||
// 铺满整块的绝对定位层,搜索框只要落在场景内部,就会跟着场景走、和栏目标题栏共享
|
||||
// 同一条水平带。所以这里钉住渲染出来的 DOM 归属关系:浮层是管理区的直接子节点,
|
||||
@@ -1947,7 +1884,7 @@ export function registerProjectWorkbenchFoundationTests() {
|
||||
);
|
||||
});
|
||||
|
||||
it('marks newly added resources on inactive section tabs until the user opens them', async () => {
|
||||
it('keeps a code-only project paged into the catch-all section', async () => {
|
||||
const manifest = createGameCreationAppManifest(
|
||||
'workbench-resource-unread',
|
||||
'资源未读标识测试',
|
||||
@@ -2009,83 +1946,10 @@ export function registerProjectWorkbenchFoundationTests() {
|
||||
const rendered = render(
|
||||
React.createElement(ProjectDevelopmentView, viewProps),
|
||||
);
|
||||
const outline = await screen.findByLabelText('资源栏目大纲');
|
||||
|
||||
expect(
|
||||
within(outline).queryByRole('button', { name: /有新资源/ }),
|
||||
).toBeNull();
|
||||
expect(
|
||||
within(outline)
|
||||
.getByRole('button', { name: '角色与对象' })
|
||||
.getAttribute('aria-current'),
|
||||
).toBe('page');
|
||||
|
||||
const manifestWithCurrentArt = {
|
||||
...manifest,
|
||||
assets: [
|
||||
...manifest.assets,
|
||||
{
|
||||
id: 'unread-art-current',
|
||||
kind: 'character' as const,
|
||||
mediaType: 'image/png',
|
||||
localPath: 'assets/current.png',
|
||||
source: { kind: 'generated' as const },
|
||||
},
|
||||
],
|
||||
};
|
||||
rendered.rerender(
|
||||
React.createElement(ProjectDevelopmentView, {
|
||||
...viewProps,
|
||||
manifest: manifestWithCurrentArt,
|
||||
}),
|
||||
// 默认停留在顺序里第一个非空栏目,也就是首个栏目资产所在的「角色与对象」。
|
||||
await waitFor(() =>
|
||||
expect(screen.getByRole('region', { name: '角色与对象' })).not.toBeNull(),
|
||||
);
|
||||
expect(
|
||||
within(outline).queryByRole('button', { name: /有新资源/ }),
|
||||
).toBeNull();
|
||||
|
||||
const manifestWithAudio = {
|
||||
...manifestWithCurrentArt,
|
||||
assets: [
|
||||
...manifestWithCurrentArt.assets,
|
||||
{
|
||||
id: 'unread-audio-new',
|
||||
kind: 'background-music' as const,
|
||||
mediaType: 'audio/mpeg',
|
||||
localPath: 'assets/new.mp3',
|
||||
source: { kind: 'generated' as const },
|
||||
},
|
||||
],
|
||||
};
|
||||
rendered.rerender(
|
||||
React.createElement(ProjectDevelopmentView, {
|
||||
...viewProps,
|
||||
manifest: manifestWithAudio,
|
||||
}),
|
||||
);
|
||||
|
||||
const unreadAudioTab = await within(outline).findByRole('button', {
|
||||
name: '音频,有新资源',
|
||||
});
|
||||
expect(
|
||||
unreadAudioTab.querySelector('.game-resource-outline-unread'),
|
||||
).not.toBeNull();
|
||||
expect(
|
||||
within(outline).queryByRole('button', {
|
||||
name: '角色与对象,有新资源',
|
||||
}),
|
||||
).toBeNull();
|
||||
|
||||
fireEvent.click(unreadAudioTab);
|
||||
await waitFor(() => {
|
||||
expect(
|
||||
within(outline)
|
||||
.getByRole('button', { name: '音频' })
|
||||
.getAttribute('aria-current'),
|
||||
).toBe('page');
|
||||
expect(
|
||||
within(outline).queryByRole('button', { name: /有新资源/ }),
|
||||
).toBeNull();
|
||||
});
|
||||
|
||||
const nextProjectManifest = createGameCreationAppManifest(
|
||||
'workbench-resource-unread-next',
|
||||
@@ -2110,7 +1974,6 @@ export function registerProjectWorkbenchFoundationTests() {
|
||||
);
|
||||
await waitFor(() => {
|
||||
// 只登记游戏代码的项目不再被分区轴排除:画布照常分页,卡片落在「待归类」栏目。
|
||||
expect(screen.queryByLabelText('资源栏目大纲')).not.toBeNull();
|
||||
expect(screen.queryAllByText('暂无已登记资源')).toHaveLength(0);
|
||||
expect(
|
||||
screen
|
||||
@@ -2123,6 +1986,121 @@ export function registerProjectWorkbenchFoundationTests() {
|
||||
});
|
||||
});
|
||||
|
||||
it('has no outline nav and keeps every section reachable through the remaining entries', async () => {
|
||||
const manifest = createGameCreationAppManifest(
|
||||
'workbench-no-outline-nav',
|
||||
'无栏目大纲导航测试',
|
||||
);
|
||||
manifest.assets = [
|
||||
{
|
||||
id: 'nav-document',
|
||||
kind: 'design-document',
|
||||
mediaType: 'text/markdown',
|
||||
localPath: 'memory/nav-document.md',
|
||||
source: { kind: 'generated', taskId: 'design-foundation' },
|
||||
},
|
||||
{
|
||||
id: 'nav-art',
|
||||
kind: 'character',
|
||||
mediaType: 'image/png',
|
||||
localPath: 'assets/nav-art.png',
|
||||
source: { kind: 'generated', taskId: 'art-asset-plan' },
|
||||
},
|
||||
];
|
||||
const invoke = vi.fn(
|
||||
async (command: string, args?: Record<string, unknown>) => {
|
||||
if (command === 'read_local_project_resource_graph') {
|
||||
return resourceGraphForInputs(args);
|
||||
}
|
||||
if (command === 'read_local_project_resource_canvas_layout') {
|
||||
return {
|
||||
schemaVersion: 'game-creator-resource-layout.v1',
|
||||
projectId: args?.expectedProjectId,
|
||||
mode: args?.mode,
|
||||
revision: 0,
|
||||
positions: [],
|
||||
updatedAt: 0,
|
||||
};
|
||||
}
|
||||
throw new Error(`unexpected invoke ${command}`);
|
||||
},
|
||||
);
|
||||
window.__TAURI__ = { core: { invoke } };
|
||||
|
||||
render(
|
||||
React.createElement(ProjectDevelopmentView, {
|
||||
projectName: manifest.name,
|
||||
projectPath: '/tmp/workbench-no-outline-nav',
|
||||
manifest,
|
||||
attachments: [],
|
||||
recentRunStatus: null,
|
||||
recentRunStopReason: null,
|
||||
supervisor: React.createElement('div', null, '项目总控'),
|
||||
onHomeOpen: vi.fn(),
|
||||
onProjectsOpen: vi.fn(),
|
||||
}),
|
||||
);
|
||||
|
||||
await waitFor(() =>
|
||||
expect(
|
||||
document.querySelector('.game-resource-book-thumbnail'),
|
||||
).not.toBeNull(),
|
||||
);
|
||||
// 反向守卫:左侧栏目大纲导航整条不再渲染(标签、类名、nav 角色都不出现)。
|
||||
expect(screen.queryByLabelText('资源栏目大纲')).toBeNull();
|
||||
expect(document.querySelector('.game-resource-outline')).toBeNull();
|
||||
expect(document.querySelector('nav[aria-label="资源栏目大纲"]')).toBeNull();
|
||||
|
||||
// 链路一:总览缩略卡片 → 任意栏目(含空栏目)。
|
||||
fireEvent.click(screen.getByRole('button', { name: '打开项目版本' }));
|
||||
await waitFor(() =>
|
||||
expect(
|
||||
document.querySelector(
|
||||
'.game-resource-book-scene-titlebar.is-active[data-resource-book-category="version"]',
|
||||
),
|
||||
).not.toBeNull(),
|
||||
);
|
||||
expect(screen.getByRole('region', { name: '项目版本' })).not.toBeNull();
|
||||
|
||||
// 链路二:页内「下一页」→ 顺序里的下一个栏目(项目版本之后回到 UI 交互)。
|
||||
expect(
|
||||
screen.getByRole('button', { name: /下一页\s*UI 交互/ }),
|
||||
).not.toBeNull();
|
||||
fireEvent.click(screen.getByRole('button', { name: /^下一页/ }));
|
||||
await waitFor(() =>
|
||||
expect(
|
||||
document.querySelector(
|
||||
'.game-resource-book-scene-titlebar.is-active[data-resource-book-category="ui-interaction"]',
|
||||
),
|
||||
).not.toBeNull(),
|
||||
);
|
||||
expect(screen.getByRole('region', { name: 'UI 交互' })).not.toBeNull();
|
||||
|
||||
// 链路三:栏目标题栏的「资源总览」→ 回到总览。
|
||||
fireEvent.click(screen.getByRole('button', { name: '收起资源' }));
|
||||
await waitFor(() =>
|
||||
expect(
|
||||
document.querySelector('[data-resource-book-view="main"]'),
|
||||
).not.toBeNull(),
|
||||
);
|
||||
|
||||
// 链路四:「所有资源」入口 → 汇总页 → 再回总览,全程没有栏目大纲导航。
|
||||
fireEvent.click(screen.getByRole('button', { name: '打开所有资源' }));
|
||||
await waitFor(() =>
|
||||
expect(
|
||||
document.querySelector('[data-resource-book-all-host="true"]'),
|
||||
).not.toBeNull(),
|
||||
);
|
||||
expect(screen.queryByLabelText('资源栏目大纲')).toBeNull();
|
||||
fireEvent.click(screen.getByRole('button', { name: '收起资源' }));
|
||||
await waitFor(() =>
|
||||
expect(
|
||||
document.querySelector('[data-resource-book-view="main"]'),
|
||||
).not.toBeNull(),
|
||||
);
|
||||
expect(screen.queryByLabelText('资源栏目大纲')).toBeNull();
|
||||
});
|
||||
|
||||
it('refreshes resource pages when a same-count manifest update changes category', async () => {
|
||||
const manifest = createGameCreationAppManifest(
|
||||
'workbench-imported-image-canvas',
|
||||
@@ -2185,10 +2163,6 @@ export function registerProjectWorkbenchFoundationTests() {
|
||||
const rendered = render(
|
||||
React.createElement(ProjectDevelopmentView, viewProps),
|
||||
);
|
||||
const outline = await screen.findByLabelText('资源栏目大纲');
|
||||
expect(
|
||||
within(outline).getByRole('button', { name: '待归类' }),
|
||||
).not.toBeNull();
|
||||
|
||||
const refreshedManifest = {
|
||||
...manifest,
|
||||
@@ -2213,7 +2187,7 @@ export function registerProjectWorkbenchFoundationTests() {
|
||||
}),
|
||||
);
|
||||
|
||||
fireEvent.click(within(outline).getByRole('button', { name: /UI 交互/ }));
|
||||
await openResourceBookCategory('UI 交互');
|
||||
expect(
|
||||
await screen.findByRole('button', {
|
||||
name: '选中资源:UI 交互 local-imported-image.png',
|
||||
@@ -2277,7 +2251,6 @@ export function registerProjectWorkbenchFoundationTests() {
|
||||
);
|
||||
|
||||
const assertOverview = () => {
|
||||
expect(screen.queryByLabelText('资源栏目大纲')).toBeNull();
|
||||
expect(
|
||||
screen
|
||||
.getByLabelText(/资源(?:依赖|类型)视图/)
|
||||
@@ -2873,11 +2846,7 @@ export function registerProjectWorkbenchFoundationTests() {
|
||||
);
|
||||
|
||||
fireEvent.click(screen.getByRole('button', { name: '按类型' }));
|
||||
fireEvent.click(
|
||||
within(screen.getByLabelText('资源栏目大纲')).getByRole('button', {
|
||||
name: /项目版本/,
|
||||
}),
|
||||
);
|
||||
await openResourceBookCategory('项目版本');
|
||||
const rootVersionButton = screen.getByRole('button', {
|
||||
name: /选中资源:项目版本 版本 1/,
|
||||
});
|
||||
@@ -2895,11 +2864,7 @@ export function registerProjectWorkbenchFoundationTests() {
|
||||
expect(childVersionButton.getAttribute('aria-pressed')).toBe('true');
|
||||
expect(screen.queryByRole('toolbar')).toBeNull();
|
||||
expect(screen.queryByText('asset:asset-removed')).toBeNull();
|
||||
fireEvent.click(
|
||||
within(screen.getByLabelText('资源栏目大纲')).getByRole('button', {
|
||||
name: /角色与对象/,
|
||||
}),
|
||||
);
|
||||
await openResourceBookCategory('角色与对象');
|
||||
const playerCard = getResourceSelectButton('player.png').closest(
|
||||
'.game-resource-card',
|
||||
);
|
||||
@@ -8924,11 +8889,6 @@ export function registerProjectAgentStatusTests() {
|
||||
}
|
||||
// 汇总页自己的标题栏也用同一份全量计数。
|
||||
expect(readCountBadge('.game-resource-all-page')).toBe(allResourcesCount);
|
||||
// 汇总页复用栏目大纲:第 0 项是「所有资源」,并标成当前页。
|
||||
const allOutline = screen.getByLabelText('资源栏目大纲');
|
||||
expect(
|
||||
allOutline.querySelector('[aria-current="page"]')?.textContent?.trim(),
|
||||
).toBe('所有资源');
|
||||
});
|
||||
|
||||
/**
|
||||
|
||||
@@ -10,7 +10,7 @@ import {
|
||||
React,
|
||||
render,
|
||||
screen,
|
||||
within,
|
||||
waitFor,
|
||||
} from './appSurface/harness';
|
||||
|
||||
const PROJECT_PATH = '/tmp/workbench-code-only-project';
|
||||
@@ -100,14 +100,22 @@ describe('只登记游戏代码的项目分区', () => {
|
||||
installResourceCardIntersectionObserver();
|
||||
renderWorkbench(createCodeOnlyManifest());
|
||||
|
||||
const outline = await screen.findByLabelText('资源栏目大纲');
|
||||
expect(
|
||||
within(outline).getByRole('button', { name: /待归类/ }),
|
||||
).not.toBeNull();
|
||||
// 左侧栏目大纲导航已删除:栏目清单改由「资源总览」缩略卡片承载。
|
||||
await waitFor(() =>
|
||||
expect(
|
||||
document.querySelector('.game-resource-book-thumbnail'),
|
||||
).not.toBeNull(),
|
||||
);
|
||||
const thumbnailLabels = Array.from(
|
||||
document.querySelectorAll('.game-resource-book-thumbnail'),
|
||||
(thumbnail) => thumbnail.getAttribute('aria-label'),
|
||||
);
|
||||
expect(thumbnailLabels).toContain('打开待归类');
|
||||
// 项目版本仍然是末尾独立栏目,代码栏目不再存在。
|
||||
expect(
|
||||
within(outline).getByRole('button', { name: /项目版本/ }),
|
||||
).not.toBeNull();
|
||||
expect(thumbnailLabels).toContain('打开项目版本');
|
||||
expect(thumbnailLabels.some((label) => label?.includes('游戏代码'))).toBe(
|
||||
false,
|
||||
);
|
||||
|
||||
// 默认停留在顺序里第一个非空栏目,也就是代码资产所在的「待归类」。
|
||||
expect(
|
||||
|
||||
@@ -80,8 +80,18 @@ function createManifestWithHero(localPath: string): GameCreationAppManifest {
|
||||
|
||||
async function openHeroCard(name = 'hero.png') {
|
||||
fireEvent.click(screen.getByRole('button', { name: '按类型' }));
|
||||
const outline = await screen.findByLabelText('资源栏目大纲');
|
||||
fireEvent.click(within(outline).getByRole('button', { name: /角色与对象/ }));
|
||||
// 左侧栏目大纲导航已按用户要求删除:切栏目走「资源总览」的栏目缩略卡片。
|
||||
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: '打开角色与对象' }),
|
||||
);
|
||||
// 资源画本先切到子画布再渲染卡片,等它落到 character 栏目再找卡。
|
||||
await waitFor(() => {
|
||||
const manager = document.querySelector('[data-resource-book-view="child"]');
|
||||
|
||||
@@ -149,8 +149,18 @@ function renderWorkbench(
|
||||
|
||||
async function openArtCategory() {
|
||||
fireEvent.click(screen.getByRole('button', { name: '按类型' }));
|
||||
const outline = await screen.findByLabelText('资源栏目大纲');
|
||||
fireEvent.click(within(outline).getByRole('button', { name: /角色与对象/ }));
|
||||
// 左侧栏目大纲导航已按用户要求删除:切栏目走「资源总览」的栏目缩略卡片。
|
||||
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: '打开角色与对象' }),
|
||||
);
|
||||
await waitFor(() => {
|
||||
expect(
|
||||
document.querySelector('[data-resource-book-view="child"]'),
|
||||
|
||||
Reference in New Issue
Block a user