搜索栏改成「临时叫出」的浮层,并撤掉为它引入的工具条带
- index.tsx:搜索框不再常驻。常态只在右下角缩放 Dock 末尾留一颗搜索按钮(复用 `game-resource-book-zoom-button`),`Ctrl/Cmd+F` 或点它把浮层叫出来;关闭走本仓既有浮层口径——直接复用美术画布的 `useImageCanvasFloatingOptionDismiss`(Esc / 点外部,Escape 在 document 上截断,不会连带触发画布清选中)。 - index.tsx:浮层打开后自动聚焦搜索框,`resourceSearchRef` 不再是死 ref(PRD「焦点落到资源搜索框」的入口接上了)。 - index.tsx:搜索语义零改动——`setSearchText`、`visibleResources` 过滤、「清除搜索并定位」原样保留;浮层收起不清条件(`type=search` 的原生 Esc 清空被 preventDefault 挡掉),条件生效时 Dock 搜索按钮带 `is-active` 高亮,用户看得出"当前有搜索"。 - index.tsx:撤掉常驻搜索条连带的那条排布带——删除 `resourceBookToolsRef`、`resourceBookToolsHeight`、`resourceCanvasSceneSize()`、ResizeObserver 测高 effect,以及缩放锚点 / 滚轮锚点里减掉工具条带的两处算式;`resourceBookSceneSize` 重新等于管理区盒子。 - index.tsx:状态提示与附件导入失败条移出排布带,落到新的浮层容器 `game-resource-book-notices`(顶部绝对定位,不占流内高度)。 - styles.css:`.game-resource-manager` 去掉 `padding-top: var(--game-resource-book-tools-height)`,`.game-resource-book-scene` 恢复 `inset: 0`,删除 `.game-resource-book-tools` 规则与窄屏 560px 那条只为常驻搜索框写的宽度规则;搜索框改为贴右下角 Dock 向上展开的浮层(`bottom: 58px` = Dock 的 14px + 34px 高 + 10px 间隙,`z-index: 40`)。 - styles.css:新增 `.game-resource-book-notices` 浮层与 `.game-resource-book-zoom-button.is-active`(复用 Dock 按钮的 hover 色,不新增配色 token)。 - tests/appSurface/harness.ts:新增 `openResourceSearch()`——按产品入口打开浮层再返回输入框,`getByLabelText` 在浮层未打开时直接抛错,堵掉"元素在但点不到"那类假绿。 - tests/appSurface/project-development.suite.ts:重写搜索可见性守卫为「浮层不落在标题栏那条带上」(管理区不再有 padding-top、场景 `inset: 0`、浮层底边 ≥ Dock 底边 + Dock 高度、浮层不带 top、提示层顶边 ≥ 标题栏高度),新增浮层开合用例(按钮 / Ctrl+F 打开、焦点落在输入框、Esc 与点外部收起都不清条件),并把既有 5 处搜索用例改为先叫出浮层。 - tests/projectResourceLiveIntegration.test.tsx:「清除搜索并定位」用例改为重新叫出浮层读条件与清空后的值。 - 验证:新增的两条断言已用变异实测不是恒真守卫——去掉自动聚焦、或让关闭顺手清条件,对应用例都会红灯。
This commit is contained in:
@@ -5640,13 +5640,6 @@ iframe.preview-frame {
|
||||
flex-direction: column;
|
||||
flex: 1 1 auto;
|
||||
min-height: 0;
|
||||
/*
|
||||
* 顶部工具条带(搜索框、筛选条、状态提示)在流内占一条独立排布带,
|
||||
* `--game-resource-book-tools-height` 由实测高度写入。铺满管理区的画本场景按同一
|
||||
* 变量把自己缩到这条带下面,栏目标题栏与栏目大纲就不会和搜索框落在同一条排布带上
|
||||
* (分页态与总览态都适用)。
|
||||
*/
|
||||
padding-top: var(--game-resource-book-tools-height, 0px);
|
||||
}
|
||||
|
||||
.game-resource-book-manager {
|
||||
@@ -5658,8 +5651,8 @@ iframe.preview-frame {
|
||||
.game-resource-book-scene {
|
||||
position: absolute;
|
||||
z-index: 20;
|
||||
/* 顶边让出工具条带:栏目标题栏与栏目大纲整体下移,不与搜索框争同一条排布带。 */
|
||||
inset: var(--game-resource-book-tools-height, 0px) 0 0;
|
||||
/* 铺满管理区:栏目标题栏、栏目大纲与资源卡都从管理区顶边开始正常占位。 */
|
||||
inset: 0;
|
||||
overflow: hidden;
|
||||
pointer-events: none;
|
||||
transition: none;
|
||||
@@ -6166,35 +6159,26 @@ iframe.preview-frame {
|
||||
}
|
||||
|
||||
/*
|
||||
* 工具条带:搜索框、筛选条与状态提示在这里按独立排布行排列,画本场景从这条带下面
|
||||
* 开始(见 `.game-resource-manager` 的 padding-top)。工具条里的浮层元素仍高于场景
|
||||
* (z-index 40 > 场景的 20),工具条内容溢出时不会被画本盖住。
|
||||
* 资源搜索浮层。
|
||||
*
|
||||
* 搜索条不再常驻:原来它和筛选条一起在管理区顶部占一条独立排布带(管理区 padding-top
|
||||
* 加画本场景下移),栏目标题栏被挤到那条带下面仍然和搜索框叠在同一条水平带上。现在常态
|
||||
* 只有右下角 Dock 里的搜索按钮,Ctrl/Cmd+F(macOS 为 Cmd+F)或点按钮才把浮层叫出来,
|
||||
* Esc / 点外部收起(口径见 `useImageCanvasFloatingOptionDismiss`)。
|
||||
*
|
||||
* 浮层贴着右下角 Dock 向上展开:`bottom: 58px` = Dock 的 `bottom: 14px` + Dock 高度
|
||||
* `34px` + 10px 间隙,所以它永远不会落到栏目标题栏(管理区顶边 min-height 42px)那条带上。
|
||||
* `z-index: 40` 高于画本场景的 20,浮在资源卡与栏目标题栏之上。
|
||||
*/
|
||||
.game-resource-book-tools {
|
||||
position: absolute;
|
||||
top: 0;
|
||||
right: 0;
|
||||
left: 0;
|
||||
z-index: 40;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 8px;
|
||||
padding: 10px 12px 4px;
|
||||
}
|
||||
|
||||
.game-resource-book-tools > * {
|
||||
margin: 0;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.game-resource-search {
|
||||
position: relative;
|
||||
position: absolute;
|
||||
right: 14px;
|
||||
bottom: 58px;
|
||||
z-index: 40;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
align-self: center;
|
||||
width: min(300px, 100%);
|
||||
width: min(320px, calc(100% - 28px));
|
||||
padding: 0 10px;
|
||||
border: 1px solid #ecdcd4;
|
||||
border-radius: 10px;
|
||||
@@ -6203,6 +6187,15 @@ iframe.preview-frame {
|
||||
color: #a08073;
|
||||
}
|
||||
|
||||
/*
|
||||
* 搜索条件生效时(浮层收起也生效)Dock 上的搜索按钮保持高亮:筛选条件不会随浮层一起
|
||||
* 消失,按钮必须能看出"当前有搜索",否则用户会以为资源丢了。复用 Dock 按钮的 hover 色。
|
||||
*/
|
||||
.game-resource-book-zoom-button.is-active {
|
||||
background: #f7ebe4;
|
||||
color: #b5582f;
|
||||
}
|
||||
|
||||
.game-resource-search input {
|
||||
width: 100%;
|
||||
min-width: 0;
|
||||
@@ -6215,6 +6208,35 @@ iframe.preview-frame {
|
||||
font-size: 12px;
|
||||
}
|
||||
|
||||
/*
|
||||
* 浮层提示(工作台状态提示、附件导入失败):不再占管理区顶部的排布带高度,改成绝对
|
||||
* 定位的浮层,落在栏目标题栏(管理区顶边 min-height 42px)下面 16px 处居中。
|
||||
*
|
||||
* 容器整体 `pointer-events: none`,只有提示条自己恢复 `pointer-events: auto`:这条横跨
|
||||
* 管理区的空盒子如果接收点击,就会把「资源总览」这类按钮的命中区域吃掉——那正是
|
||||
* "回到资源总览点不动"的成因,所以命中的门槛只留在提示条自己的盒子上。
|
||||
*/
|
||||
.game-resource-book-notices {
|
||||
position: absolute;
|
||||
top: 58px;
|
||||
left: 50%;
|
||||
z-index: 40;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
width: max-content;
|
||||
max-width: min(420px, calc(100% - 28px));
|
||||
transform: translateX(-50%);
|
||||
pointer-events: none;
|
||||
}
|
||||
|
||||
.game-resource-book-notices > * {
|
||||
width: auto;
|
||||
max-width: 100%;
|
||||
pointer-events: auto;
|
||||
}
|
||||
|
||||
.game-resource-classification-dialog {
|
||||
width: min(480px, 100%);
|
||||
}
|
||||
@@ -6294,7 +6316,7 @@ iframe.preview-frame {
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
gap: 10px;
|
||||
margin: 8px 12px 0;
|
||||
margin: 0;
|
||||
padding: 8px 10px;
|
||||
border: 1px solid #edc7b5;
|
||||
border-radius: 10px;
|
||||
@@ -6314,7 +6336,7 @@ iframe.preview-frame {
|
||||
.game-attachment-errors {
|
||||
display: grid;
|
||||
gap: 4px;
|
||||
margin: 8px 12px 0;
|
||||
margin: 0;
|
||||
padding: 8px 10px;
|
||||
border: 1px solid #edc1ad;
|
||||
border-radius: 10px;
|
||||
@@ -8878,16 +8900,6 @@ iframe.preview-frame {
|
||||
font-size: 12px;
|
||||
}
|
||||
|
||||
/*
|
||||
* 窄屏(移动端优先):筛选条与标签开始换行时,搜索框改为整行独占,
|
||||
* 工具条带变高后画本场景按实测高度继续下移,任何宽度下都不与标题栏重叠。
|
||||
*/
|
||||
@media (max-width: 560px) {
|
||||
.game-resource-search {
|
||||
width: 100%;
|
||||
}
|
||||
}
|
||||
|
||||
@media (max-width: 620px) {
|
||||
.game-resource-recovery-item,
|
||||
.game-resource-recovery-error {
|
||||
|
||||
@@ -406,21 +406,6 @@ function resourceCanvasElementSize(element: HTMLElement | null) {
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* 资源工具条(搜索框、筛选条、状态提示)在管理区顶部占一条独立排布带,画本场景的盒子
|
||||
* 从这条带下面开始。按视口排布资源时要减掉这条带,否则"适应内容"仍按旧的
|
||||
* "场景一直顶到管理区顶端"居中,把卡片排到工具条底下。
|
||||
*/
|
||||
function resourceCanvasSceneSize(
|
||||
size: { width: number; height: number },
|
||||
chromeHeight: number,
|
||||
) {
|
||||
return {
|
||||
width: size.width,
|
||||
height: Math.max(0, size.height - Math.max(0, chromeHeight)),
|
||||
};
|
||||
}
|
||||
|
||||
function resourceCanvasViewportsEqual(
|
||||
left: CanvasViewport,
|
||||
right: CanvasViewport,
|
||||
@@ -1222,11 +1207,6 @@ export default function ProjectDevelopmentView({
|
||||
createResourceBookTransitionController(),
|
||||
);
|
||||
const resourceBookManagerRef = useRef<HTMLDivElement>(null);
|
||||
/**
|
||||
* 工具条带的第一条流式元素(搜索框)。工具条带本身没有独立包裹节点,命中它就能把
|
||||
* "第一条流式元素顶边到管理区顶边"的高度算出来,正好是画本场景要让出的那条带。
|
||||
*/
|
||||
const resourceBookToolsRef = useRef<HTMLLabelElement>(null);
|
||||
const resourceBookPlanKeysRef = useRef(
|
||||
new Map<string, ResourceBookSceneCardPlan>(),
|
||||
);
|
||||
@@ -1240,17 +1220,14 @@ export default function ProjectDevelopmentView({
|
||||
width: 900,
|
||||
height: 640,
|
||||
});
|
||||
const [resourceBookToolsHeight, setResourceBookToolsHeight] = useState(0);
|
||||
/**
|
||||
* 场景局部的可排布尺寸:管理区盒子减掉顶部工具条带。视口居中与"适应内容"都按它算。
|
||||
*/
|
||||
const sceneFitCanvasSize = useMemo(
|
||||
() =>
|
||||
resourceCanvasSceneSize(resourceBookSceneSize, resourceBookToolsHeight),
|
||||
[resourceBookSceneSize, resourceBookToolsHeight],
|
||||
);
|
||||
const [sortMode, setSortMode] = useState<ResourceSortMode>('dependency');
|
||||
const [searchText, setSearchText] = useState('');
|
||||
/**
|
||||
* 搜索浮层的开合。搜索条不再常驻:常态只留右下角 Dock 里的搜索按钮与
|
||||
* Ctrl/Cmd+F(见下方两处效果),浮层收起不清 `searchText`,筛选条件继续生效。
|
||||
*/
|
||||
const [resourceSearchOpen, setResourceSearchOpen] = useState(false);
|
||||
const resourceSearchId = useId();
|
||||
const [resourceCanvasViewports, setResourceCanvasViewports] =
|
||||
useState<ResourceCanvasViewportBySortMode>(defaultResourceCanvasViewports);
|
||||
const resourceCanvasViewportTargetsRef = useRef(
|
||||
@@ -1374,6 +1351,9 @@ export default function ProjectDevelopmentView({
|
||||
const skipNextResourceCardClickRef = useRef<string | null>(null);
|
||||
const skipNextResourceCardClickTimerRef = useRef<number | null>(null);
|
||||
const resourceSearchRef = useRef<HTMLInputElement>(null);
|
||||
/** 右下角 Dock 里的搜索按钮:开合搜索浮层,也是浮层「点外部 / Esc」边界的一半。 */
|
||||
const resourceSearchTriggerRef = useRef<HTMLButtonElement>(null);
|
||||
const resourceSearchPopoverRef = useRef<HTMLLabelElement>(null);
|
||||
const resourceFocusProjectPathRef = useRef(projectPath);
|
||||
/** 框选命中测试用的资源卡图层与当前画本视口,渲染期同步赋值。 */
|
||||
const marqueeHitTestLayersRef = useRef<CanvasLayer[]>([]);
|
||||
@@ -2041,16 +2021,13 @@ export default function ProjectDevelopmentView({
|
||||
return;
|
||||
}
|
||||
const managerRect = manager.getBoundingClientRect();
|
||||
// 画本场景是管理区内缩掉顶部工具条带之后的盒子;总览栏目矩形与场景世界都按它定位。
|
||||
const sceneRect =
|
||||
manager
|
||||
.querySelector<HTMLElement>('.game-resource-book-scene')
|
||||
?.getBoundingClientRect() ?? managerRect;
|
||||
// 画本场景铺满管理区(`inset: 0`,顶部不再有工具条带):管理区盒子就是场景盒子,
|
||||
// 总览栏目矩形与场景世界都按同一个原点定位。
|
||||
setResourceBookSceneSize((current) =>
|
||||
Math.abs(current.width - sceneRect.width) < 0.5 &&
|
||||
Math.abs(current.height - sceneRect.height) < 0.5
|
||||
Math.abs(current.width - managerRect.width) < 0.5 &&
|
||||
Math.abs(current.height - managerRect.height) < 0.5
|
||||
? current
|
||||
: { width: sceneRect.width, height: sceneRect.height },
|
||||
: { width: managerRect.width, height: managerRect.height },
|
||||
);
|
||||
const next = new Map<ResourceCategory, ResourceBookOverviewRect>();
|
||||
resourceBookThumbnailElementsRef.current.forEach((element, category) => {
|
||||
@@ -2112,28 +2089,59 @@ export default function ProjectDevelopmentView({
|
||||
};
|
||||
}, [measureResourceBookOverview, resourceBookView, canvasResources.length]);
|
||||
|
||||
useLayoutEffect(() => {
|
||||
const tools = resourceBookToolsRef.current;
|
||||
if (!tools) {
|
||||
/**
|
||||
* 搜索浮层的「点外部 / Esc 关闭」。
|
||||
*
|
||||
* 直接复用美术画布的 `useImageCanvasFloatingOptionDismiss`,不另写判据:它在
|
||||
* document 上同时接管 click 与 Escape,点在边界外即关、点在边界内保留;Escape 走
|
||||
* 同一口径在 document 上截断,画布全局的 Esc(清选中)不会跟着一起触发。
|
||||
* 边界是浮层自己与右下角那颗搜索按钮:点按钮只走按钮自己的开合,不会先被判定成外部。
|
||||
*/
|
||||
useImageCanvasFloatingOptionDismiss({
|
||||
isOpen: resourceSearchOpen && mode === 'resources' && !uiEditorRoute,
|
||||
boundaryRefs: [resourceSearchPopoverRef, resourceSearchTriggerRef],
|
||||
onDismiss: () => setResourceSearchOpen(false),
|
||||
});
|
||||
|
||||
/**
|
||||
* Ctrl/Cmd+F 叫出搜索浮层。
|
||||
*
|
||||
* 搜索条不再常驻占管理区顶部那条排布带(那条带与栏目标题栏争同一条水平带),改成
|
||||
* 快捷键 + 右下角小按钮临时叫出。关闭不清 `searchText`:筛选条件与「清除搜索并定位」
|
||||
* 的语义与常驻搜索条时期一致。
|
||||
*/
|
||||
useEffect(() => {
|
||||
if (mode !== 'resources' || uiEditorRoute) {
|
||||
return undefined;
|
||||
}
|
||||
const measureTools = () => {
|
||||
const height = tools.offsetTop + tools.offsetHeight;
|
||||
setResourceBookToolsHeight((current) =>
|
||||
Math.abs(current - height) < 0.5 ? current : height,
|
||||
);
|
||||
const handleResourceSearchShortcut = (event: KeyboardEvent) => {
|
||||
if (
|
||||
event.key.toLowerCase() !== 'f' ||
|
||||
!(event.ctrlKey || event.metaKey) ||
|
||||
event.altKey ||
|
||||
event.shiftKey
|
||||
) {
|
||||
return;
|
||||
}
|
||||
event.preventDefault();
|
||||
setResourceSearchOpen(true);
|
||||
};
|
||||
measureTools();
|
||||
if (typeof ResizeObserver === 'undefined') {
|
||||
window.addEventListener('resize', measureTools);
|
||||
return () => window.removeEventListener('resize', measureTools);
|
||||
window.addEventListener('keydown', handleResourceSearchShortcut);
|
||||
return () =>
|
||||
window.removeEventListener('keydown', handleResourceSearchShortcut);
|
||||
}, [mode, uiEditorRoute]);
|
||||
|
||||
/**
|
||||
* 浮层打开后焦点落到搜索框:PRD「聚焦资源被删除时焦点落到资源搜索框」的落点入口,
|
||||
* `resourceSearchRef` 由此不再是死 ref。
|
||||
*/
|
||||
useEffect(() => {
|
||||
if (!resourceSearchOpen) {
|
||||
return;
|
||||
}
|
||||
// 工具条会随窗口宽度换行(窄屏下搜索框与筛选条各占一行),高度必须实测:
|
||||
// 场景盒子按它下移,两者才不会落在同一条排布带上。
|
||||
const observer = new ResizeObserver(measureTools);
|
||||
observer.observe(tools);
|
||||
return () => observer.disconnect();
|
||||
}, [mode, resourceBookView, searchText]);
|
||||
resourceSearchRef.current?.focus();
|
||||
}, [resourceSearchOpen]);
|
||||
|
||||
const viewedResourceCategory =
|
||||
mode === 'resources' ? activePageCategory : null;
|
||||
useEffect(() => {
|
||||
@@ -2310,8 +2318,8 @@ export default function ProjectDevelopmentView({
|
||||
// 返回总览时也不会跳成另一张图。
|
||||
const resourceBookCategoryViewCenters = useMemo(() => {
|
||||
const canvasSize =
|
||||
sceneFitCanvasSize.width > 0 && sceneFitCanvasSize.height > 0
|
||||
? sceneFitCanvasSize
|
||||
resourceBookSceneSize.width > 0 && resourceBookSceneSize.height > 0
|
||||
? resourceBookSceneSize
|
||||
: {
|
||||
width: RESOURCE_CANVAS_FALLBACK_WIDTH,
|
||||
height: RESOURCE_CANVAS_FALLBACK_HEIGHT,
|
||||
@@ -2330,7 +2338,7 @@ export default function ProjectDevelopmentView({
|
||||
] as const;
|
||||
}),
|
||||
);
|
||||
}, [sceneFitCanvasSize, resourceCanvasViewports, sortMode]);
|
||||
}, [resourceBookSceneSize, resourceCanvasViewports, sortMode]);
|
||||
const setResourceCanvasViewport = useCallback(
|
||||
(
|
||||
category: ResourceCategory,
|
||||
@@ -2823,14 +2831,14 @@ export default function ProjectDevelopmentView({
|
||||
|
||||
const categoryFitKey = `${projectPath}\n${manifest.projectId}\n${sortMode}\n${category}`;
|
||||
const fitCanvasSize =
|
||||
sceneFitCanvasSize.width > 0 && sceneFitCanvasSize.height > 0
|
||||
? sceneFitCanvasSize
|
||||
resourceBookSceneSize.width > 0 && resourceBookSceneSize.height > 0
|
||||
? resourceBookSceneSize
|
||||
: {
|
||||
width: RESOURCE_CANVAS_FALLBACK_WIDTH,
|
||||
height: RESOURCE_CANVAS_FALLBACK_HEIGHT,
|
||||
};
|
||||
const fitMaxScale =
|
||||
sceneFitCanvasSize.width > 0 && sceneFitCanvasSize.height > 0
|
||||
resourceBookSceneSize.width > 0 && resourceBookSceneSize.height > 0
|
||||
? RESOURCE_CANVAS_BOOK_FIT_MAX_SCALE
|
||||
: 1;
|
||||
const categoryBounds = resourceCanvasContentBounds(
|
||||
@@ -2875,7 +2883,7 @@ export default function ProjectDevelopmentView({
|
||||
// sort/category pair. Mark that fit as settled so the paged-canvas
|
||||
// layout effect does not immediately recalculate it from a shared DOM
|
||||
// measurement and overwrite the stored per-category viewport.
|
||||
if (sceneFitCanvasSize.width > 0 && sceneFitCanvasSize.height > 0) {
|
||||
if (resourceBookSceneSize.width > 0 && resourceBookSceneSize.height > 0) {
|
||||
resourceCanvasFitKeysRef.current.add(categoryFitKey);
|
||||
}
|
||||
|
||||
@@ -2888,7 +2896,7 @@ export default function ProjectDevelopmentView({
|
||||
manifest.projectId,
|
||||
projectPath,
|
||||
resourceBookState,
|
||||
sceneFitCanvasSize,
|
||||
resourceBookSceneSize,
|
||||
resourceCardSizeByResourceId,
|
||||
resourcePositionsByCategory,
|
||||
setResourceCanvasViewport,
|
||||
@@ -3833,14 +3841,14 @@ export default function ProjectDevelopmentView({
|
||||
activePageCategory,
|
||||
fitResourceCanvasViewportToContent({
|
||||
bounds: resourceCanvasFitBounds,
|
||||
canvasSize: sceneFitCanvasSize,
|
||||
canvasSize: resourceBookSceneSize,
|
||||
padding: RESOURCE_CANVAS_FIT_PADDING,
|
||||
maxScale: RESOURCE_CANVAS_BOOK_FIT_MAX_SCALE,
|
||||
}),
|
||||
);
|
||||
}, [
|
||||
activePageCategory,
|
||||
sceneFitCanvasSize,
|
||||
resourceBookSceneSize,
|
||||
resourceBookView,
|
||||
resourceCanvasFitBounds,
|
||||
setResourceCanvasViewport,
|
||||
@@ -3852,8 +3860,8 @@ export default function ProjectDevelopmentView({
|
||||
const sceneElement = resourceBookManagerRef.current;
|
||||
const rect = sceneElement?.getBoundingClientRect();
|
||||
const anchorX = rect ? rect.width / 2 : 0;
|
||||
// 锚点取真正看得见的画本区域中心:顶部工具条带不算画布。
|
||||
const anchorY = rect ? (rect.height - resourceBookToolsHeight) / 2 : 0;
|
||||
// 画本场景就是管理区盒子(顶部不再有工具条带),锚点取盒子中心即可。
|
||||
const anchorY = rect ? rect.height / 2 : 0;
|
||||
if (resourceBookView === 'main') {
|
||||
const viewport = resourceBookMainViewportRef.current;
|
||||
const next = zoomResourceBookViewport(viewport, factor, {
|
||||
@@ -3877,12 +3885,7 @@ export default function ProjectDevelopmentView({
|
||||
if (next.scale === viewport.scale) return;
|
||||
setResourceCanvasViewport(category, next);
|
||||
},
|
||||
[
|
||||
activePageCategory,
|
||||
resourceBookToolsHeight,
|
||||
resourceBookView,
|
||||
setResourceCanvasViewport,
|
||||
],
|
||||
[activePageCategory, resourceBookView, setResourceCanvasViewport],
|
||||
);
|
||||
|
||||
useLayoutEffect(() => {
|
||||
@@ -3918,10 +3921,9 @@ export default function ProjectDevelopmentView({
|
||||
}
|
||||
event.preventDefault();
|
||||
const rect = sceneElement.getBoundingClientRect();
|
||||
// 工具条带里没有画布,滚轮锚点先减掉它,缩放才不会以被盖住的区域为中心。
|
||||
const screenPoint = {
|
||||
x: event.clientX - rect.left,
|
||||
y: event.clientY - rect.top - resourceBookToolsHeight,
|
||||
y: event.clientY - rect.top,
|
||||
};
|
||||
if (resourceBookView === 'main') {
|
||||
const current = resourceBookMainViewportRef.current;
|
||||
@@ -3953,12 +3955,7 @@ export default function ProjectDevelopmentView({
|
||||
});
|
||||
setResourceCanvasViewport(category, result.viewport, true);
|
||||
},
|
||||
[
|
||||
activePageCategory,
|
||||
resourceBookToolsHeight,
|
||||
resourceBookView,
|
||||
setResourceCanvasViewport,
|
||||
],
|
||||
[activePageCategory, resourceBookView, setResourceCanvasViewport],
|
||||
);
|
||||
|
||||
useEffect(() => {
|
||||
@@ -5742,19 +5739,39 @@ export default function ProjectDevelopmentView({
|
||||
>
|
||||
<Maximize2 size={16} aria-hidden="true" />
|
||||
</button>
|
||||
<button
|
||||
ref={resourceSearchTriggerRef}
|
||||
type="button"
|
||||
className={`game-resource-book-zoom-button${searchText ? ' is-active' : ''}`}
|
||||
aria-label="搜索资源"
|
||||
title="搜索资源(Ctrl/Cmd+F)"
|
||||
aria-expanded={resourceSearchOpen}
|
||||
aria-controls={resourceSearchId}
|
||||
onClick={() => setResourceSearchOpen((current) => !current)}
|
||||
>
|
||||
<Search size={16} aria-hidden="true" />
|
||||
</button>
|
||||
</div>
|
||||
<div className="game-resource-book-tools">
|
||||
{resourceSearchOpen ? (
|
||||
<label
|
||||
ref={resourceBookToolsRef}
|
||||
ref={resourceSearchPopoverRef}
|
||||
className="game-resource-search"
|
||||
>
|
||||
<Search size={16} aria-hidden="true" />
|
||||
<input
|
||||
ref={resourceSearchRef}
|
||||
id={resourceSearchId}
|
||||
type="search"
|
||||
aria-label="搜索项目资源"
|
||||
value={searchText}
|
||||
placeholder="搜索资源名称、路径或类型"
|
||||
onKeyDown={(event) => {
|
||||
// type=search 的原生行为是 Esc 清空输入框;这里只让浮层收起,
|
||||
// 搜索条件本身不因为按了 Esc 被清掉(关闭不等于清除搜索)。
|
||||
if (event.key === 'Escape') {
|
||||
event.preventDefault();
|
||||
}
|
||||
}}
|
||||
onChange={(event) => {
|
||||
advanceFocusGeneration();
|
||||
setSearchText(event.currentTarget.value);
|
||||
@@ -5762,6 +5779,8 @@ export default function ProjectDevelopmentView({
|
||||
}}
|
||||
/>
|
||||
</label>
|
||||
) : null}
|
||||
<div className="game-resource-book-notices">
|
||||
{resourceWorkbenchNotice ? (
|
||||
<div className="game-resource-live-notice" role="status">
|
||||
<span>{resourceWorkbenchNotice}</span>
|
||||
|
||||
@@ -365,6 +365,18 @@ function queryResourceSelectButton(label: string) {
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* 叫出资源搜索浮层并返回搜索框。
|
||||
*
|
||||
* 搜索条改成「临时叫出」的浮层之后,输入框只在浮层打开时存在于 DOM:用例必须走产品
|
||||
* 入口(右下角 Dock 的搜索按钮)打开它。`getByLabelText` 在浮层没打开时会直接抛错,
|
||||
* 所以「搜索框在真实 UI 里点不到」那类假绿不会再出现。
|
||||
*/
|
||||
function openResourceSearch() {
|
||||
fireEvent.click(screen.getByRole('button', { name: '搜索资源' }));
|
||||
return screen.getByLabelText('搜索项目资源') as HTMLInputElement;
|
||||
}
|
||||
|
||||
function emptyProjectPolicy() {
|
||||
return {
|
||||
path: '.agent/policy.json',
|
||||
@@ -1302,6 +1314,7 @@ export {
|
||||
mockRoleAgentReply,
|
||||
nativeClipboardMock,
|
||||
openMainProject,
|
||||
openResourceSearch,
|
||||
pickProjectFromLauncher,
|
||||
ProjectDevelopmentView,
|
||||
projectSupervisorResponseStream,
|
||||
|
||||
@@ -32,6 +32,7 @@ import {
|
||||
getResourceSelectButton,
|
||||
it,
|
||||
mockRoleAgentReply,
|
||||
openResourceSearch,
|
||||
pickProjectFromLauncher,
|
||||
ProjectDevelopmentView,
|
||||
projectSupervisorResponseStream,
|
||||
@@ -1145,7 +1146,7 @@ export function registerProjectWorkbenchFoundationTests() {
|
||||
).toHaveLength(5),
|
||||
);
|
||||
|
||||
fireEvent.change(screen.getByLabelText('搜索项目资源'), {
|
||||
fireEvent.change(openResourceSearch(), {
|
||||
target: { value: 'overview-art-4' },
|
||||
});
|
||||
fireEvent.click(screen.getByRole('button', { name: '收起资源' }));
|
||||
@@ -1158,7 +1159,7 @@ export function registerProjectWorkbenchFoundationTests() {
|
||||
);
|
||||
});
|
||||
|
||||
it('keeps the resource search box above the resource book scene in both views', () => {
|
||||
it('keeps the resource search as a transient overlay that never lands on the titlebar band', () => {
|
||||
const styles = readFileSync(
|
||||
resolve(process.cwd(), 'apps/ai-game-creator-shell/src/styles.css'),
|
||||
'utf8',
|
||||
@@ -1170,7 +1171,7 @@ export function registerProjectWorkbenchFoundationTests() {
|
||||
),
|
||||
'utf8',
|
||||
);
|
||||
// 卡片(z-index 25)与栏目标题栏(30)都画在铺满资源区的场景里:工具条必须比场景更高,
|
||||
// 卡片(z-index 25)与栏目标题栏(30)都画在铺满资源区的场景里:搜索浮层必须比场景更高,
|
||||
// 否则放开 display 也只是"画了但看不见、点不到"。
|
||||
const sceneZIndex = styleNumber(
|
||||
styleRuleBody(styles, '\\.game-resource-book-scene'),
|
||||
@@ -1181,7 +1182,7 @@ export function registerProjectWorkbenchFoundationTests() {
|
||||
const search = styleRuleBody(styles, '\\.game-resource-search');
|
||||
expect(search).not.toMatch(/display:\s*none/u);
|
||||
expect(search).toMatch(/display:\s*flex/u);
|
||||
expect(search).toMatch(/position:\s*relative/u);
|
||||
expect(search).toMatch(/position:\s*absolute/u);
|
||||
expect(styleNumber(search, 'z-index')).toBeGreaterThan(sceneZIndex);
|
||||
|
||||
// 「清除搜索并定位」只在搜索把刚提交的资源挡掉时出现,它所在的提示条同样要在场景之上。
|
||||
@@ -1189,47 +1190,57 @@ export function registerProjectWorkbenchFoundationTests() {
|
||||
expect(notice).toMatch(/position:\s*relative/u);
|
||||
expect(styleNumber(notice, 'z-index')).toBeGreaterThan(sceneZIndex);
|
||||
|
||||
// 但"在场景之上"只是不被盖住,不等于两者不重叠:上一版把搜索框常态居中浮在画本顶上,
|
||||
// 分页态栏目标题栏正好被压在同一水平带上。所以还要钉住"工具条带独占顶部那条带"。
|
||||
//
|
||||
// 不重叠由两条声明级约定保证,缺一条就会退回重叠:
|
||||
// 1. 管理区按实测的工具条带高度留出 padding-top;
|
||||
// 2. 画本场景把自己的顶边按同一个变量下移。
|
||||
// 两条都指向 `--game-resource-book-tools-height`,所以断言两边用的是同一个变量,
|
||||
// 而不是各写各的数字(写死数字时任何一处改动都会重新压住搜索框)。
|
||||
const toolsHeightVariable = '--game-resource-book-tools-height';
|
||||
// 选择器前缀加行首断言:`.game-workbench-stage > .game-resource-manager` 这种
|
||||
// 后代选择器里也含同名类,不加断言会取到那条只有 grid-row 的规则。
|
||||
const manager = styleRuleBody(styles, '(?:^|\\n)\\.game-resource-manager');
|
||||
expect(manager).toMatch(
|
||||
new RegExp(`padding-top:\\s*var\\(${toolsHeightVariable}`, 'u'),
|
||||
// 常驻搜索条连同它占的那条排布带一起撤掉:管理区不再为工具条留 padding-top,
|
||||
// 画本场景重新铺满管理区盒子——栏目标题栏回到管理区顶边正常占位。
|
||||
expect(styles).not.toContain('--game-resource-book-tools-height');
|
||||
expect(styles).not.toContain('game-resource-book-tools');
|
||||
expect(tsxSource).not.toContain('game-resource-book-tools');
|
||||
expect(
|
||||
styleRuleBody(styles, '(?:^|\\n)\\.game-resource-manager'),
|
||||
).not.toMatch(/padding-top/u);
|
||||
expect(styleRuleBody(styles, '\\.game-resource-book-scene')).toMatch(
|
||||
/inset:\s*0;/u,
|
||||
);
|
||||
|
||||
const scene = styleRuleBody(styles, '\\.game-resource-book-scene');
|
||||
expect(scene).toMatch(
|
||||
new RegExp(`inset:\\s*var\\(${toolsHeightVariable}`, 'u'),
|
||||
// 搜索浮层贴着右下角 Dock 向上展开:浮层底边 = Dock 底边 + Dock 高度 + 间隙。
|
||||
// 两个数字都读自声明,任何一处改小都会让浮层压到 Dock 或下滑到标题栏那条带上。
|
||||
const dock = styleRuleBody(styles, '\\.game-resource-book-zoom');
|
||||
const dockBottom = styleNumber(dock, 'bottom');
|
||||
const dockHeight = styleNumber(dock, 'height');
|
||||
expect(dockBottom).toBeGreaterThan(0);
|
||||
expect(dockHeight).toBeGreaterThan(0);
|
||||
expect(styleNumber(search, 'bottom')).toBeGreaterThanOrEqual(
|
||||
dockBottom + dockHeight,
|
||||
);
|
||||
// 场景只让出顶边:宽度与底边必须仍是 0,否则画本区域会缩成一条缝。
|
||||
expect(scene).toMatch(/inset:\s*var\([^)]*\)\s+0\s+0;/u);
|
||||
|
||||
// 工具条带是绝对定位的独立排布行,不是流内浮层;搜索框按整行宽度算,
|
||||
// 不与任何栏目标题栏共享同一水平带(分页态标题栏 z-index 30 < 工具条带 40)。
|
||||
const tools = styleRuleBody(styles, '\\.game-resource-book-tools');
|
||||
expect(tools).toMatch(/position:\s*absolute/u);
|
||||
expect(styleNumber(tools, 'top')).toBe(0);
|
||||
// 浮层右边缘与 Dock 右边缘对齐。
|
||||
expect(styleNumber(search, 'right')).toBe(styleNumber(dock, 'right'));
|
||||
// 浮层只按底边 + 右边锚定:一旦补上 top,它会重新浮回管理区顶部那条标题栏带上。
|
||||
expect(search).not.toMatch(/(?:^|[;\s])top:/u);
|
||||
const titlebar = styleRuleBody(
|
||||
styles,
|
||||
'\\.game-resource-book-scene-titlebar',
|
||||
);
|
||||
expect(styleNumber(titlebar, 'z-index')).toBeLessThan(
|
||||
styleNumber(tools, 'z-index'),
|
||||
const titlebarHeight = styleNumber(titlebar, 'min-height');
|
||||
expect(titlebarHeight).toBeGreaterThan(0);
|
||||
|
||||
// 结构上搜索框只在浮层打开时渲染,而且挂在管理区下(与画本场景并列),不是常驻节点。
|
||||
const searchTag = tsxSource.indexOf('className="game-resource-search"');
|
||||
expect(searchTag).toBeGreaterThan(-1);
|
||||
expect(tsxSource).toMatch(
|
||||
/\{resourceSearchOpen \? \(\s*<label\s+ref=\{resourceSearchPopoverRef\}\s+className="game-resource-search"/s,
|
||||
);
|
||||
|
||||
// 结构上搜索框必须待在工具条带里:离开这条带就又会浮回画本顶部。
|
||||
const toolsTag = tsxSource.indexOf('game-resource-book-tools');
|
||||
const searchTag = tsxSource.indexOf('game-resource-search');
|
||||
expect(toolsTag).toBeGreaterThan(-1);
|
||||
expect(searchTag).toBeGreaterThan(toolsTag);
|
||||
// 浮层提示层是绝对定位的浮层,容器不吃点击:否则这条横跨管理区的空盒子会把
|
||||
// 「资源总览」(回到资源总览)这类按钮的命中区域整段吃掉。
|
||||
const notices = styleRuleBody(styles, '\\.game-resource-book-notices');
|
||||
expect(notices).toMatch(/position:\s*absolute/u);
|
||||
expect(notices).toMatch(/pointer-events:\s*none/u);
|
||||
expect(styleNumber(notices, 'z-index')).toBeGreaterThan(sceneZIndex);
|
||||
expect(
|
||||
styleRuleBody(styles, '\\.game-resource-book-notices > \\*'),
|
||||
).toMatch(/pointer-events:\s*auto/u);
|
||||
// 提示条必须落在栏目标题栏下面:顶边 ≥ 标题栏高度,两者不共享同一条水平带。
|
||||
expect(styleNumber(notices, 'top')).toBeGreaterThanOrEqual(titlebarHeight);
|
||||
});
|
||||
|
||||
it('keeps the outline dock readable without hover', () => {
|
||||
@@ -1339,30 +1350,32 @@ export function registerProjectWorkbenchFoundationTests() {
|
||||
.getAttribute('aria-current'),
|
||||
).toBe('page');
|
||||
|
||||
// 搜索框与栏目标题栏分属两条排布带,这不是 CSS 声明能表达的:画本场景是管理区里
|
||||
// 铺满整块的绝对定位层,搜索框只要还在场景内部,就一定会被栏目标题栏压在同一个
|
||||
// 水平带上。所以这里钉住渲染出来的 DOM 归属关系。
|
||||
const searchLabel = screen.getByLabelText('搜索项目资源').closest('label');
|
||||
// 搜索浮层与栏目标题栏分属两条排布带,这不是 CSS 声明能表达的:画本场景是管理区里
|
||||
// 铺满整块的绝对定位层,搜索框只要落在场景内部,就会跟着场景走、和栏目标题栏共享
|
||||
// 同一条水平带。所以这里钉住渲染出来的 DOM 归属关系:浮层是管理区的直接子节点,
|
||||
// 与画本场景并列,而且只在打开时存在。
|
||||
expect(screen.queryByLabelText('搜索项目资源')).toBeNull();
|
||||
const searchLabel = openResourceSearch().closest('label');
|
||||
expect(searchLabel?.className).toContain('game-resource-search');
|
||||
|
||||
const toolsBand = searchLabel?.parentElement ?? null;
|
||||
expect(toolsBand?.className).toContain('game-resource-book-tools');
|
||||
|
||||
const manager = toolsBand?.parentElement ?? null;
|
||||
const manager = searchLabel?.parentElement ?? null;
|
||||
expect(manager?.className).toContain('game-resource-manager');
|
||||
|
||||
// 画本场景是管理区里铺满整块的绝对定位层:搜索框只要落在它内部,就会被栏目标题栏
|
||||
// 压在同一个水平带上。入场动画会替换场景节点,所以这里用祖先链判断归属关系,
|
||||
// 不依赖某一个具体节点的身份,也不受节点替换影响。
|
||||
// 从父节点起算:label 自己就带 `game-resource-search`,closest 要排除自身。
|
||||
// 入场动画会替换场景节点,所以这里用祖先链判断归属关系,不依赖某一个具体节点的
|
||||
// 身份,也不受节点替换影响。从父节点起算:label 自己就带 `game-resource-search`。
|
||||
const searchAncestors = searchLabel?.parentElement ?? null;
|
||||
expect(searchAncestors?.closest('.game-resource-book-scene')).toBeNull();
|
||||
expect(searchAncestors?.closest('.game-resource-book-main')).toBeNull();
|
||||
expect(searchAncestors?.closest('.game-resource-book-tools')).toBe(
|
||||
toolsBand,
|
||||
expect(searchAncestors).toBe(manager);
|
||||
|
||||
// 浮层提示层同样挂在管理区上,与画本场景并列,而不是场景内部的流式内容。
|
||||
const notices = manager?.querySelector(
|
||||
':scope > .game-resource-book-notices',
|
||||
);
|
||||
// 两条排布带在管理区里是并列的:搜索框所在的工具条带与画本场景各占一条。
|
||||
expect(toolsBand?.parentElement).toBe(manager);
|
||||
expect(notices).not.toBeNull();
|
||||
expect(notices?.closest('.game-resource-book-scene')).toBeNull();
|
||||
|
||||
// 画本场景是管理区里铺满整块的绝对定位层,两个浮层与它并列。
|
||||
expect(
|
||||
manager?.querySelector(':scope > .game-resource-book-scene'),
|
||||
).not.toBeNull();
|
||||
@@ -1453,11 +1466,11 @@ export function registerProjectWorkbenchFoundationTests() {
|
||||
}),
|
||||
);
|
||||
|
||||
// 搜索框是常驻的真输入框(图标 + input 都在同一个 label 里),不是悬停才展开的浮层。
|
||||
const searchInput = () =>
|
||||
screen.getByLabelText('搜索项目资源') as HTMLInputElement;
|
||||
expect(searchInput().type).toBe('search');
|
||||
expect(searchInput().closest('label')?.className).toContain(
|
||||
// 搜索框不再常驻:常态只有右下角 Dock 里的搜索按钮,浮层由按钮或 Ctrl/Cmd+F 叫出。
|
||||
expect(screen.queryByLabelText('搜索项目资源')).toBeNull();
|
||||
const searchInput = openResourceSearch();
|
||||
expect(searchInput.type).toBe('search');
|
||||
expect(searchInput.closest('label')?.className).toContain(
|
||||
'game-resource-search',
|
||||
);
|
||||
|
||||
@@ -1468,24 +1481,130 @@ export function registerProjectWorkbenchFoundationTests() {
|
||||
await waitFor(() => expect(overviewCards()).toHaveLength(2));
|
||||
|
||||
// 总览态:输入即过滤总览摞上的卡片。
|
||||
fireEvent.change(searchInput(), { target: { value: 'alpha-hero' } });
|
||||
fireEvent.change(searchInput, { target: { value: 'alpha-hero' } });
|
||||
await waitFor(() => expect(overviewCards()).toHaveLength(1));
|
||||
|
||||
// 分页画布态:同一条件继续生效,输入框仍可读写。
|
||||
// 分页画布态:同一条件继续生效。点栏目大纲属于「点外部」,浮层按既有口径收起,
|
||||
// 但搜索条件跟着状态走,不随浮层消失。
|
||||
await openResourceBookCategory('角色与对象');
|
||||
expect(searchInput().value).toBe('alpha-hero');
|
||||
expect(screen.queryByLabelText('搜索项目资源')).toBeNull();
|
||||
await waitFor(() =>
|
||||
expect(getResourceSelectButton('alpha-hero.png')).not.toBeNull(),
|
||||
);
|
||||
expect(queryResourceSelectButton('beta-enemy.png')).toBeNull();
|
||||
|
||||
// 清空后恢复:证明这条链路确实由搜索框驱动。
|
||||
fireEvent.change(searchInput(), { target: { value: '' } });
|
||||
fireEvent.change(openResourceSearch(), { target: { value: '' } });
|
||||
await waitFor(() =>
|
||||
expect(queryResourceSelectButton('beta-enemy.png')).not.toBeNull(),
|
||||
);
|
||||
});
|
||||
|
||||
it('calls out the resource search overlay by shortcut and closes it without clearing the condition', async () => {
|
||||
const manifest = createGameCreationAppManifest(
|
||||
'workbench-search-overlay',
|
||||
'资源搜索浮层开合测试',
|
||||
);
|
||||
manifest.assets = [
|
||||
{
|
||||
id: 'overlay-alpha',
|
||||
kind: 'character',
|
||||
mediaType: 'image/png',
|
||||
localPath: 'assets/alpha-hero.png',
|
||||
source: { kind: 'generated', taskId: 'art-asset-plan' },
|
||||
},
|
||||
{
|
||||
id: 'overlay-beta',
|
||||
kind: 'character',
|
||||
mediaType: 'image/png',
|
||||
localPath: 'assets/beta-enemy.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-search-overlay',
|
||||
manifest,
|
||||
attachments: [],
|
||||
recentRunStatus: null,
|
||||
recentRunStopReason: null,
|
||||
supervisor: React.createElement('div', null, '项目总控'),
|
||||
onHomeOpen: vi.fn(),
|
||||
onProjectsOpen: vi.fn(),
|
||||
}),
|
||||
);
|
||||
|
||||
const overviewCards = () =>
|
||||
document.querySelectorAll(
|
||||
'.game-resource-book-scene-card[data-resource-book-category="character"]',
|
||||
);
|
||||
await waitFor(() => expect(overviewCards()).toHaveLength(2));
|
||||
const searchTrigger = () =>
|
||||
screen.getByRole('button', { name: '搜索资源' });
|
||||
expect(searchTrigger().getAttribute('aria-expanded')).toBe('false');
|
||||
|
||||
// Ctrl/Cmd+F 叫出浮层,焦点自动落到搜索框(PRD「焦点落到资源搜索框」的入口)。
|
||||
fireEvent.keyDown(window, { key: 'f', ctrlKey: true });
|
||||
const searchInput = screen.getByLabelText(
|
||||
'搜索项目资源',
|
||||
) as HTMLInputElement;
|
||||
expect(searchTrigger().getAttribute('aria-expanded')).toBe('true');
|
||||
expect(document.activeElement).toBe(searchInput);
|
||||
|
||||
fireEvent.change(searchInput, { target: { value: 'alpha-hero' } });
|
||||
await waitFor(() => expect(overviewCards()).toHaveLength(1));
|
||||
// 有搜索条件时 Dock 上的搜索按钮保持高亮:浮层收起后条件仍在,按钮必须看得出来。
|
||||
expect(searchTrigger().className).toContain('is-active');
|
||||
|
||||
// Esc 只收起浮层:搜索条件不跟着被清掉(关闭不等于清除搜索)。
|
||||
fireEvent.keyDown(document, { key: 'Escape' });
|
||||
expect(screen.queryByLabelText('搜索项目资源')).toBeNull();
|
||||
expect(overviewCards()).toHaveLength(1);
|
||||
expect(searchTrigger().getAttribute('aria-expanded')).toBe('false');
|
||||
|
||||
// 点搜索按钮同样能开合,且不会被「点外部关闭」抢在前面。
|
||||
fireEvent.click(searchTrigger());
|
||||
expect(screen.getByLabelText('搜索项目资源')).not.toBeNull();
|
||||
fireEvent.click(searchTrigger());
|
||||
expect(screen.queryByLabelText('搜索项目资源')).toBeNull();
|
||||
|
||||
// 点浮层外部关闭,条件继续保留。
|
||||
fireEvent.click(searchTrigger());
|
||||
expect(screen.getByLabelText('搜索项目资源')).not.toBeNull();
|
||||
fireEvent.click(document.body);
|
||||
expect(screen.queryByLabelText('搜索项目资源')).toBeNull();
|
||||
expect(overviewCards()).toHaveLength(1);
|
||||
|
||||
// 「清除搜索并定位」那条链路的入口还在:清空搜索后过滤恢复。
|
||||
fireEvent.click(searchTrigger());
|
||||
fireEvent.change(screen.getByLabelText('搜索项目资源'), {
|
||||
target: { value: '' },
|
||||
});
|
||||
await waitFor(() => expect(overviewCards()).toHaveLength(2));
|
||||
expect(searchTrigger().className).not.toContain('is-active');
|
||||
});
|
||||
|
||||
it('paints the marquee selection box with the scene token root and non-empty geometry', async () => {
|
||||
const manifest = createGameCreationAppManifest(
|
||||
'workbench-marquee-colour',
|
||||
@@ -2224,7 +2343,7 @@ export function registerProjectWorkbenchFoundationTests() {
|
||||
).not.toBeNull(),
|
||||
);
|
||||
pause.mockClear();
|
||||
const search = screen.getByLabelText('搜索项目资源');
|
||||
const search = openResourceSearch();
|
||||
fireEvent.change(search, { target: { value: 'assets/hero.png' } });
|
||||
await waitFor(() => expect(pause.mock.instances).toContain(typeVideo));
|
||||
expect(getResourceSelectButton('hero.png')).not.toBeNull();
|
||||
@@ -3152,7 +3271,7 @@ export function registerProjectWorkbenchFoundationTests() {
|
||||
'.game-resource-canvas-content',
|
||||
);
|
||||
const boundaryBeforeSearch = dependencyWorld?.dataset.resourceBoundary;
|
||||
const search = screen.getByLabelText('搜索项目资源');
|
||||
const search = openResourceSearch();
|
||||
fireEvent.change(search, { target: { value: 'ui-dependency' } });
|
||||
await waitFor(() => {
|
||||
expect(overlay.querySelector('[data-edge-kind]')).toBeNull();
|
||||
@@ -4776,7 +4895,7 @@ export function registerProjectWorkbenchFoundationTests() {
|
||||
fireEvent.click(screen.getByRole('button', { name: '按类型' }));
|
||||
await openResourceBookCategory('角色与对象');
|
||||
expect(screen.getByLabelText('资源类型视图')).not.toBeNull();
|
||||
fireEvent.change(screen.getByLabelText('搜索项目资源'), {
|
||||
fireEvent.change(openResourceSearch(), {
|
||||
target: { value: 'hero.png' },
|
||||
});
|
||||
fireEvent.click(await findResourceSelectButton('hero.png'));
|
||||
|
||||
@@ -15,6 +15,7 @@ import {
|
||||
createGameCreationAppManifest,
|
||||
findResourceSelectButton,
|
||||
fireEvent,
|
||||
openResourceSearch,
|
||||
render,
|
||||
screen,
|
||||
setComposerText,
|
||||
@@ -580,7 +581,7 @@ describe('project resource live canvas integration', () => {
|
||||
'把角色头发设定改为红色',
|
||||
);
|
||||
// 搜索条件在提交前就存在,新素材(文档分类)不会命中它:走"被当前搜索隐藏"分支。
|
||||
const search = screen.getByLabelText('搜索项目资源') as HTMLInputElement;
|
||||
const search = openResourceSearch();
|
||||
fireEvent.change(search, { target: { value: 'source-art' } });
|
||||
fireEvent.click(within(panel).getByRole('button', { name: '修改' }));
|
||||
await waitFor(() => expect(deriveCalls).toHaveLength(1));
|
||||
@@ -589,10 +590,12 @@ describe('project resource live canvas integration', () => {
|
||||
expect(
|
||||
await screen.findByText('新资源已保存,但被当前搜索条件隐藏'),
|
||||
).not.toBeNull();
|
||||
expect(search.value).toBe('source-art');
|
||||
// 提交时点了快速编辑面板,搜索浮层按「点外部」收起;重新叫出来读到的仍是被保留的条件。
|
||||
expect(openResourceSearch().value).toBe('source-art');
|
||||
fireEvent.click(screen.getByRole('button', { name: '清除搜索并定位' }));
|
||||
|
||||
expect(search.value).toBe('');
|
||||
// 清空只由这个显式动作发起:再叫出浮层读到的已经是空值。
|
||||
expect(openResourceSearch().value).toBe('');
|
||||
const operationId = String(deriveCalls[0]?.operationId);
|
||||
expect(
|
||||
(await findResourceSelectButton(`${operationId}-rules.md`)).getAttribute(
|
||||
|
||||
Reference in New Issue
Block a user