WIP: AGC 资源画布与替换改造 V3.0 #316

Draft
suzmii wants to merge 131 commits from feat/agc-canvas-resource-workbench-v3 into master
3 changed files with 618 additions and 26 deletions
Showing only changes of commit f49ff61ddc - Show all commits
@@ -0,0 +1,119 @@
/**
* 资源画布浮层的关闭判据。
*
* 判据全部对齐网页端美术画布(`packages/image-canvas-react/src/useImageCanvasStageController.ts`
* 的 `clearCanvasFocus()` 与 `src/components/image-editor/useImageCanvasKeyboardShortcuts.ts`
* 的 Escape 分支),AGC 不再自带第二套口径:
*
* - 画布空白处左键 pointerdown → 清焦点(清空选中 + 关闭快速编辑浮层);
* - 点在资源卡、交互控件或画布浮层里 → 不清焦点;
* - 选中变化 / 切项目 / 切模式 → 由宿主既有的重置逻辑负责。
*/
/**
* 落在这些元素里的 pointerdown 不清画布焦点。
*
* 与美术画布「点在图层上不清焦点、点在空白/world 上才清」同口径:AGC 的资源卡
* 等价于图层,工具条与快速编辑面板等价于浮层,其余交互控件(按钮、输入、缩放、
* 页签)都不该顺手把选中清掉。
*/
export const RESOURCE_CANVAS_INTERACTION_SELECTOR = [
'button',
'input',
'textarea',
'select',
'a',
'.game-resource-card',
'.game-resource-book-scene-titlebar',
'.game-resource-book-zoom',
'.image-canvas-editor__floating-toolbar',
'.image-canvas-editor__generation-composer',
].join(', ');
/** 这一次画布 pointerdown 要不要清焦点。 */
export function isResourceCanvasInteractionTarget(
target: Element | null | undefined,
): boolean {
return Boolean(target?.closest(RESOURCE_CANVAS_INTERACTION_SELECTOR));
}
/**
* 快速编辑面板是否可以被清焦点顺手关掉。
*
* 对齐美术画布 `closeTransientEditorPanels()`:生成中的面板不关,否则用户看不到
* 「修改中」状态,也拿不到失败后的重试入口。
*/
export function canDismissResourceCanvasQuickEdit(
panel: { readonly status: string } | null | undefined,
): boolean {
return panel?.status !== 'generating';
}
/** 资源画布上的 AGC 自有弹层:它们开着时画布浮层不参与「点外部关闭」。 */
export type ResourceCanvasHostOverlayState = {
readonly isResourcePanelOpen: boolean;
readonly isGenerationPanelOpen: boolean;
readonly isClassificationPanelOpen: boolean;
readonly isRenameDialogOpen: boolean;
readonly isRecoveryPanelOpen: boolean;
};
export function isResourceCanvasHostOverlayOpen(
overlay: ResourceCanvasHostOverlayState,
): boolean {
return (
overlay.isResourcePanelOpen ||
overlay.isGenerationPanelOpen ||
overlay.isClassificationPanelOpen ||
overlay.isRenameDialogOpen ||
overlay.isRecoveryPanelOpen
);
}
/**
* 快速编辑浮层是否接管「点外部 / Esc 关闭」。
*
* 只有它自己开着时才接管,而且是唯一的取消条件:AGC 弹层开着时不接管——资源面板的
* 多选直接读写画布那一份 `selectedResourceIds`,此刻清选中会把用户刚勾的资源清空。
*
* 选中工具条不在这里,它跟美术画布一样只随选中存在:单独选中一个资源卡不会因为
* 点了一下对话区就丢选中(资源面板下载、版本绑定高亮都挂在同一份选中上)。
*/
export function resolveResourceCanvasQuickEditDismissOpen({
isCanvasVisible,
isQuickEditPanelOpen,
hostOverlay,
}: {
isCanvasVisible: boolean;
isQuickEditPanelOpen: boolean;
hostOverlay: ResourceCanvasHostOverlayState;
}): boolean {
return (
isCanvasVisible &&
isQuickEditPanelOpen &&
!isResourceCanvasHostOverlayOpen(hostOverlay)
);
}
/**
* 宿主自己的 Escape 是否归资源画布。
*
* 对齐美术画布:Escape 走 `closeTransientEditorPanels()` + `clearCanvasSelection()`
* 既收浮层也清选中。AGC 自带弹窗开着时让位给弹窗自己(Esc 只关弹窗)。
*/
export function resolveResourceCanvasFocusEscapeActive({
isCanvasVisible,
hasSelectedResource,
isQuickEditPanelOpen,
hostOverlay,
}: {
isCanvasVisible: boolean;
hasSelectedResource: boolean;
isQuickEditPanelOpen: boolean;
hostOverlay: ResourceCanvasHostOverlayState;
}): boolean {
if (!isCanvasVisible || isResourceCanvasHostOverlayOpen(hostOverlay)) {
return false;
}
return hasSelectedResource || isQuickEditPanelOpen;
}
@@ -78,6 +78,7 @@ import type {
} from '../../../../../src/components/image-editor/ImageCanvasEditorTypes';
import { ImageCanvasQuickEditPanelView } from '../../../../../src/components/image-editor/ImageCanvasQuickEditPanelView';
import { ImageCanvasSelectedLayerToolbarView } from '../../../../../src/components/image-editor/ImageCanvasSelectedLayerToolbarView';
import { useImageCanvasFloatingOptionDismiss } from '../../../../../src/components/image-editor/useImageCanvasFloatingOptionDismiss';
import {
LocalGamePreviewFrame,
type LocalGamePreviewInspectSelection,
@@ -96,6 +97,12 @@ import {
resolveResourceCanvasPanelEntries,
resolveSelectedPanelEntries,
} from '../../features/resource-canvas/resourceCanvasAssetTransferModel';
import {
canDismissResourceCanvasQuickEdit,
isResourceCanvasInteractionTarget,
resolveResourceCanvasFocusEscapeActive,
resolveResourceCanvasQuickEditDismissOpen,
} from '../../features/resource-canvas/resourceCanvasFocusModel';
import {
isResourceCanvasGenerationAvailable,
resourceCanvasGenerationOption,
@@ -1373,6 +1380,102 @@ export default function ProjectDevelopmentView({
normalizedAssetId: string | null;
expectedProjectRevision: number | null;
} | null>(null);
/** 快速编辑面板的最新状态:清焦点回调只在事件里读,不随面板状态反复重建。 */
const quickEditPanelRef = useRef<QuickEditPanelState | null>(null);
quickEditPanelRef.current = quickEditPanel;
const closeResourceQuickEditPanel = useCallback(() => {
setQuickEditPanel(null);
setQuickEditSourceLayer(null);
resourceQuickEditRequestRef.current = null;
}, []);
/**
* 清资源画布焦点:对齐美术画布 `clearCanvasFocus()`。
*
* 美术画布在画布空白 pointerdown 与 Escape 两处都会走这一步——清空选中让选中工具条
* 收起,并关掉快速编辑浮层。资源画布保持同一套时机:点在资源卡或浮层里不清,
* 点画布空白、按 Esc 才清。生成中的面板不关,与美术画布
* `closeTransientEditorPanels()` 的 `status === 'generating'` 判断一致。
*/
const clearResourceCanvasFocus = useCallback(() => {
setSelectedResourceIds([]);
if (!canDismissResourceCanvasQuickEdit(quickEditPanelRef.current)) {
return;
}
closeResourceQuickEditPanel();
}, [closeResourceQuickEditPanel]);
/**
* 快速编辑浮层的「点外部 / Esc 关闭」。
*
* 直接复用美术画布的 `useImageCanvasFloatingOptionDismiss`,不另写判据:它在
* document 上同时接管 click 与 Escape,点在边界外即关、点在边界内(含
* `.image-canvas-editor__portal-menu` 弹出层,例如模型 / 比例下拉)保留。
*
* 边界取整个资源画布管理器:面板内部的点(改提示词、切选项)不算外部;点到画布
* 以外(顶部工具条、对话区、左侧导航)立即收起。
*/
useImageCanvasFloatingOptionDismiss({
isOpen: resolveResourceCanvasQuickEditDismissOpen({
isCanvasVisible: mode === 'resources' && !uiEditorRoute,
isQuickEditPanelOpen: quickEditPanel !== null,
hostOverlay: {
isResourcePanelOpen: resourcePanelOpen,
isGenerationPanelOpen: resourceGenerationOpen,
isClassificationPanelOpen: resourceClassificationAssetId !== null,
isRenameDialogOpen: resourceRenameAssetId !== null,
isRecoveryPanelOpen: resourceRecoveryPanelOpen,
},
}),
boundaryRefs: [resourceBookManagerRef],
onDismiss: clearResourceCanvasFocus,
});
/**
* 选中工具条的「Esc 关闭」。
*
* 选中工具条跟随选中存在,所以它没有独立的开关;Esc 与美术画布一样走
* `clearCanvasSelection()` 那一半:清空选中,工具条随之收起。挂在 window 上,
* 快速编辑里的下拉弹层打开时会被共享 hook 在 document 上截断,符合
* 「浮层打开时 Escape 归浮层所有」的既有口径。
*/
useEffect(() => {
if (
!resolveResourceCanvasFocusEscapeActive({
isCanvasVisible: mode === 'resources' && !uiEditorRoute,
hasSelectedResource: selectedResourceIds.length > 0,
isQuickEditPanelOpen: quickEditPanel !== null,
hostOverlay: {
isResourcePanelOpen: resourcePanelOpen,
isGenerationPanelOpen: resourceGenerationOpen,
isClassificationPanelOpen: resourceClassificationAssetId !== null,
isRenameDialogOpen: resourceRenameAssetId !== null,
isRecoveryPanelOpen: resourceRecoveryPanelOpen,
},
})
) {
return undefined;
}
const handleKeyDown = (event: KeyboardEvent) => {
if (event.key === 'Escape') {
clearResourceCanvasFocus();
}
};
window.addEventListener('keydown', handleKeyDown);
return () => window.removeEventListener('keydown', handleKeyDown);
}, [
clearResourceCanvasFocus,
mode,
quickEditPanel,
resourceClassificationAssetId,
resourceGenerationOpen,
resourcePanelOpen,
resourceRecoveryPanelOpen,
resourceRenameAssetId,
selectedResourceIds,
uiEditorRoute,
]);
const resourceSectionScrollPositionsRef = useRef(
new Map<string, { left: number; top: number }>(),
);
@@ -4038,11 +4141,7 @@ export default function ProjectDevelopmentView({
return;
}
const target = event.target as HTMLElement;
if (
target.closest(
'button, input, textarea, select, a, .game-resource-card, .game-resource-book-scene-titlebar, .game-resource-book-zoom, .image-canvas-editor__floating-toolbar, .image-canvas-editor__generation-composer',
)
) {
if (isResourceCanvasInteractionTarget(target)) {
return;
}
resourceBookTransitionControllerRef.current.settle();
@@ -4059,6 +4158,10 @@ export default function ProjectDevelopmentView({
};
return;
}
// 与美术画布一致:空白处的左键 pointerdown 先清画布焦点,选中工具条与快速编辑
// 浮层随之收起。这里不能等 click——下面的 preventDefault() 会掐掉指针的兼容鼠标
// 事件,document 上的点外部关闭收不到这次点击。
clearResourceCanvasFocus();
event.preventDefault();
event.currentTarget.setPointerCapture(event.pointerId);
setResourceCanvasMarquee(
@@ -4069,7 +4172,7 @@ export default function ProjectDevelopmentView({
}),
);
},
[activePageCategory],
[activePageCategory, clearResourceCanvasFocus],
);
const handleResourceCanvasPointerMove = useCallback(
@@ -4913,12 +5016,6 @@ export default function ProjectDevelopmentView({
[canvasResources],
);
const closeResourceQuickEditPanel = useCallback(() => {
setQuickEditPanel(null);
setQuickEditSourceLayer(null);
resourceQuickEditRequestRef.current = null;
}, []);
/**
* 快速编辑:先按后端真实门禁把任务产物正规化成正式素材,再走资源派生产出一张新素材。
* 源素材(文件与 manifest 条目)保持不变。
@@ -5202,20 +5299,6 @@ export default function ProjectDevelopmentView({
selectedResource?.subtype === 'UI' ||
selectedResource?.subtype === 'ui-prototype';
useEffect(() => {
if (!quickEditPanel) {
return undefined;
}
const handleKeyDown = (event: KeyboardEvent) => {
if (event.key === 'Escape') {
event.preventDefault();
closeResourceQuickEditPanel();
}
};
window.addEventListener('keydown', handleKeyDown);
return () => window.removeEventListener('keydown', handleKeyDown);
}, [closeResourceQuickEditPanel, quickEditPanel]);
const selectedToolbarStyle = selectedResourceLayer
? resolveSelectedToolbarStyle({
selectedLayer: selectedResourceLayer,
@@ -0,0 +1,390 @@
// @vitest-environment jsdom
import type { CanvasLayer } from '@genarrative/image-canvas-core';
import { cleanup, fireEvent, render, screen } from '@testing-library/react';
import { useCallback, useEffect, useRef, useState } from 'react';
import { afterEach, describe, expect, test, vi } from 'vitest';
import type { QuickEditPanelState } from '../../../src/components/image-editor/ImageCanvasEditorTypes';
import { ImageCanvasQuickEditPanelView } from '../../../src/components/image-editor/ImageCanvasQuickEditPanelView';
import { useImageCanvasFloatingOptionDismiss } from '../../../src/components/image-editor/useImageCanvasFloatingOptionDismiss';
import {
canDismissResourceCanvasQuickEdit,
isResourceCanvasHostOverlayOpen,
isResourceCanvasInteractionTarget,
resolveResourceCanvasFocusEscapeActive,
resolveResourceCanvasQuickEditDismissOpen,
} from '../src/features/resource-canvas/resourceCanvasFocusModel';
import { createResourceQuickEditPanelDraft } from '../src/features/resource-canvas/resourceCanvasQuickEditModel';
afterEach(() => {
cleanup();
});
const RESOURCE_FOCUS_SOURCE_LAYER: CanvasLayer = {
id: 'resource-1',
resourceId: 'resource-1',
title: '封面图',
src: '/cover.png',
x: 0,
y: 0,
width: 512,
height: 512,
originalWidth: 1024,
originalHeight: 1024,
zIndex: 1,
sourceType: 'upload',
};
describe('resourceCanvasFocusModel', () => {
test('点在资源卡、交互控件与画布浮层里时不清画布焦点', () => {
document.body.innerHTML = `
<div class="game-resource-canvas">
<div class="game-resource-card"><button type="button">卡片按钮</button></div>
<div class="game-resource-book-scene-titlebar"></div>
<div class="game-resource-book-zoom"><button type="button">放大</button></div>
<div class="game-resource-canvas-content"><span id="blank">空白</span></div>
<div class="image-canvas-editor__floating-toolbar"><button type="button">快速编辑</button></div>
<div class="image-canvas-editor__generation-composer"><div id="prompt">提示词</div></div>
</div>
`;
const query = (selector: string) =>
document.querySelector<HTMLElement>(selector);
expect(isResourceCanvasInteractionTarget(query('#blank'))).toBe(false);
expect(
isResourceCanvasInteractionTarget(query('.game-resource-card')),
).toBe(true);
expect(
isResourceCanvasInteractionTarget(query('.game-resource-card button')),
).toBe(true);
expect(
isResourceCanvasInteractionTarget(
query('.game-resource-book-scene-titlebar'),
),
).toBe(true);
expect(
isResourceCanvasInteractionTarget(query('.game-resource-book-zoom')),
).toBe(true);
expect(
isResourceCanvasInteractionTarget(
query('.image-canvas-editor__floating-toolbar'),
),
).toBe(true);
expect(
isResourceCanvasInteractionTarget(
query('.image-canvas-editor__floating-toolbar button'),
),
).toBe(true);
expect(
isResourceCanvasInteractionTarget(
query('.image-canvas-editor__generation-composer'),
),
).toBe(true);
expect(isResourceCanvasInteractionTarget(query('#prompt'))).toBe(true);
expect(isResourceCanvasInteractionTarget(null)).toBe(false);
document.body.innerHTML = '';
});
test('快速编辑生成中不参与清焦点', () => {
expect(canDismissResourceCanvasQuickEdit(null)).toBe(true);
expect(canDismissResourceCanvasQuickEdit({ status: 'idle' })).toBe(true);
expect(canDismissResourceCanvasQuickEdit({ status: 'failed' })).toBe(true);
expect(canDismissResourceCanvasQuickEdit({ status: 'generating' })).toBe(
false,
);
});
test('AGC 自有弹层开着时画布浮层不接管点外部关闭', () => {
const closed = {
isResourcePanelOpen: false,
isGenerationPanelOpen: false,
isClassificationPanelOpen: false,
isRenameDialogOpen: false,
isRecoveryPanelOpen: false,
};
expect(isResourceCanvasHostOverlayOpen(closed)).toBe(false);
for (const key of Object.keys(closed) as Array<keyof typeof closed>) {
expect(isResourceCanvasHostOverlayOpen({ ...closed, [key]: true })).toBe(
true,
);
}
});
test('画布可见且快速编辑浮层打开时才接管点外部关闭', () => {
const closed = {
isResourcePanelOpen: false,
isGenerationPanelOpen: false,
isClassificationPanelOpen: false,
isRenameDialogOpen: false,
isRecoveryPanelOpen: false,
};
const base = {
isCanvasVisible: true,
isQuickEditPanelOpen: true,
hostOverlay: closed,
};
expect(resolveResourceCanvasQuickEditDismissOpen(base)).toBe(true);
expect(
resolveResourceCanvasQuickEditDismissOpen({
...base,
isQuickEditPanelOpen: false,
}),
).toBe(false);
expect(
resolveResourceCanvasQuickEditDismissOpen({
...base,
isCanvasVisible: false,
}),
).toBe(false);
expect(
resolveResourceCanvasQuickEditDismissOpen({
...base,
hostOverlay: { ...closed, isResourcePanelOpen: true },
}),
).toBe(false);
});
test('宿主 Escape 只在画布有浮层且没有自带弹窗时归资源画布', () => {
const closed = {
isResourcePanelOpen: false,
isGenerationPanelOpen: false,
isClassificationPanelOpen: false,
isRenameDialogOpen: false,
isRecoveryPanelOpen: false,
};
const base = {
isCanvasVisible: true,
hasSelectedResource: false,
isQuickEditPanelOpen: true,
hostOverlay: closed,
};
expect(resolveResourceCanvasFocusEscapeActive(base)).toBe(true);
// 选中工具条没有独立开关:只有选中、没有浮层时 Escape 同样要清选中。
expect(
resolveResourceCanvasFocusEscapeActive({
...base,
isQuickEditPanelOpen: false,
hasSelectedResource: true,
}),
).toBe(true);
expect(
resolveResourceCanvasFocusEscapeActive({
...base,
isQuickEditPanelOpen: false,
hasSelectedResource: false,
}),
).toBe(false);
expect(
resolveResourceCanvasFocusEscapeActive({
...base,
isCanvasVisible: false,
}),
).toBe(false);
expect(
resolveResourceCanvasFocusEscapeActive({
...base,
hostOverlay: { ...closed, isRenameDialogOpen: true },
}),
).toBe(false);
});
});
/**
* 与 `project-development/index.tsx` 同一套接线:
* - 共享 hook 只管快速编辑浮层,边界是整个资源画布,点边界外即关;
* - 选中工具条跟随选中存在,Esc 由宿主自己的 window 监听清焦点。
*/
function ResourceCanvasFocusHarness({
onDismiss,
initialPanelOpen = true,
}: {
onDismiss: () => void;
initialPanelOpen?: boolean;
}) {
const canvasRef = useRef<HTMLDivElement | null>(null);
const [selected, setSelected] = useState(true);
const [panelOpen, setPanelOpen] = useState(initialPanelOpen);
const clearFocus = useCallback(() => {
onDismiss();
setSelected(false);
setPanelOpen(false);
}, [onDismiss]);
useImageCanvasFloatingOptionDismiss({
isOpen: panelOpen,
boundaryRefs: [canvasRef],
onDismiss: clearFocus,
});
useEffect(() => {
if (!selected && !panelOpen) {
return undefined;
}
const handleKeyDown = (event: KeyboardEvent) => {
if (event.key === 'Escape') {
clearFocus();
}
};
window.addEventListener('keydown', handleKeyDown);
return () => window.removeEventListener('keydown', handleKeyDown);
}, [clearFocus, panelOpen, selected]);
return (
<div>
<div ref={canvasRef} data-testid="resource-canvas">
{selected ? <div role="toolbar" aria-label="选中工具条" /> : null}
{panelOpen ? <div role="dialog" aria-label="快速编辑浮层面板" /> : null}
</div>
</div>
);
}
describe('资源画布浮层关闭时机', () => {
test('点画布浮层内部不关闭,点画布以外立即关闭', () => {
const onDismiss = vi.fn();
render(<ResourceCanvasFocusHarness onDismiss={onDismiss} />);
fireEvent.click(screen.getByRole('toolbar', { name: '选中工具条' }));
fireEvent.click(screen.getByRole('dialog', { name: '快速编辑浮层面板' }));
expect(onDismiss).not.toHaveBeenCalled();
expect(screen.getByRole('toolbar', { name: '选中工具条' })).toBeTruthy();
fireEvent.click(document.body);
expect(onDismiss).toHaveBeenCalledTimes(1);
expect(screen.queryByRole('toolbar', { name: '选中工具条' })).toBeNull();
expect(
screen.queryByRole('dialog', { name: '快速编辑浮层面板' }),
).toBeNull();
});
test('快速编辑浮层开着时 Esc 一并收起工具条', () => {
const onDismiss = vi.fn();
render(<ResourceCanvasFocusHarness onDismiss={onDismiss} />);
fireEvent.keyDown(document, { key: 'Escape' });
expect(onDismiss).toHaveBeenCalledTimes(1);
expect(screen.queryByRole('toolbar', { name: '选中工具条' })).toBeNull();
expect(
screen.queryByRole('dialog', { name: '快速编辑浮层面板' }),
).toBeNull();
});
test('只有选中工具条时 Esc 也收起来', () => {
const onDismiss = vi.fn();
render(
<ResourceCanvasFocusHarness
onDismiss={onDismiss}
initialPanelOpen={false}
/>,
);
fireEvent.keyDown(document, { key: 'Escape' });
expect(onDismiss).toHaveBeenCalledTimes(1);
expect(screen.queryByRole('toolbar', { name: '选中工具条' })).toBeNull();
});
test('只有选中工具条时点画布以外不动选中', () => {
const onDismiss = vi.fn();
render(
<ResourceCanvasFocusHarness
onDismiss={onDismiss}
initialPanelOpen={false}
/>,
);
fireEvent.click(document.body);
// 工具条跟随选中存在:清选中会连带丢资源面板多选与版本绑定高亮,所以点外部不代劳。
expect(onDismiss).not.toHaveBeenCalled();
expect(screen.getByRole('toolbar', { name: '选中工具条' })).toBeTruthy();
});
});
/** 与 AGC 一样只给必要 props:下拉弹层就地渲染,不走 portal。 */
function QuickEditPanelHarness({
initialPanel,
}: {
initialPanel: QuickEditPanelState;
}) {
const [panel, setPanel] = useState<QuickEditPanelState | null>(initialPanel);
if (!panel) {
return <output aria-label="面板状态">closed</output>;
}
return (
<div>
<ImageCanvasQuickEditPanelView
panel={panel}
style={{ left: 10, top: 20 }}
setQuickEditPanel={setPanel}
onSubmit={() => undefined}
/>
</div>
);
}
describe('快速编辑浮层的下拉弹层', () => {
test('模型与比例弹层都能点外部收起', () => {
render(
<QuickEditPanelHarness
initialPanel={createResourceQuickEditPanelDraft(
RESOURCE_FOCUS_SOURCE_LAYER,
)}
/>,
);
fireEvent.click(
screen.getByRole('button', { name: '快速编辑模型 nanobanana2' }),
);
expect(screen.getByRole('button', { name: 'gpt-image-2' })).toBeTruthy();
// 点面板内、弹层外的提示词区域:只收弹层,不动面板本身。
fireEvent.click(screen.getByLabelText('快速编辑提示词'));
expect(screen.queryByRole('button', { name: 'gpt-image-2' })).toBeNull();
expect(
screen.getByRole('button', { name: '快速编辑模型 nanobanana2' }),
).toBeTruthy();
fireEvent.click(
screen.getByRole('button', { name: '快速编辑尺寸 1:1·1K' }),
);
expect(screen.getByRole('button', { name: '比例 4:3' })).toBeTruthy();
// 点弹层里的选项只改选区,弹层留着继续选尺寸。
fireEvent.click(screen.getByRole('button', { name: '比例 4:3' }));
expect(
screen.getByRole('button', { name: '快速编辑尺寸 4:3·1K' }),
).toBeTruthy();
fireEvent.click(screen.getByLabelText('快速编辑提示词'));
expect(screen.queryByRole('button', { name: '比例 4:3' })).toBeNull();
});
test('Esc 收起下拉弹层', () => {
render(
<QuickEditPanelHarness
initialPanel={createResourceQuickEditPanelDraft(
RESOURCE_FOCUS_SOURCE_LAYER,
)}
/>,
);
fireEvent.click(
screen.getByRole('button', { name: '快速编辑模型 nanobanana2' }),
);
expect(screen.getByRole('button', { name: 'gpt-image-2' })).toBeTruthy();
fireEvent.keyDown(document, { key: 'Escape' });
expect(screen.queryByRole('button', { name: 'gpt-image-2' })).toBeNull();
});
});