Files
Genarrative/apps/ai-game-creator-shell/tests/resourceCanvasFloatingDismiss.test.tsx
suzmii 973e965f4f 卡片浮层改为提交即关:快速编辑 / 生成动画只负责交任务
- 快速编辑与生成动画点提交当帧即关面板(不等 IPC、不留等待态),进度、阶段与结果只由「生成任务」侧栏承载,与图片类生成面板同一口径
- 受理之后的失败不再重开面板:侧栏那条收口为失败 + 提示条,草稿回落成「未完成编辑」等用户重开;只有点击瞬间就失败(名称预检、源素材不在画布、无客户端)留在面板里
- 派生请求身份改为按「入口 + 资源路径」记忆:重开面板再提交时提示词没变就沿用同一 operationId / 幂等键,命中同一 operation 账本;派生成成功后清除
- 删除 canDismissResourceCanvasQuickEdit 与宿主里只为它服务的两块面板状态 ref,clearResourceCanvasFocus 回到「清选中 + 关两块浮层」的直线逻辑
- 三条既有用例按新口径改写为「重开面板再重试,身份不变」;PRD §3.10 与 decision-log 同步
2026-09-21 00:19:42 +08:00

749 lines
25 KiB
TypeScript
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
// @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 { createPortal } from 'react-dom';
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 {
isFloatingOverlayWheelEvent,
useImageCanvasFloatingOptionDismiss,
} from '../../../src/components/image-editor/useImageCanvasFloatingOptionDismiss';
import {
isResourceCanvasHostOverlayOpen,
isResourceCanvasInteractionTarget,
isResourceCanvasPanTarget,
isResourceCanvasWheelOverlayTarget,
resolveResourceCanvasFloatingPanelDismissOpen,
resolveResourceCanvasFloatingPanelOpen,
resolveResourceCanvasFocusEscapeActive,
} 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 id="blank"></div>
<div class="game-resource-card" id="card">
<button class="game-resource-card-select" id="select"></button>
<button class="game-resource-card-media-control" id="play"></button>
<span role="button" id="corner">信息</span>
<video id="video"></video><input id="input" />
</div>
<div contenteditable="true" id="editor"></div>
<div class="game-resource-filter-panel"><span id="filter">筛选</span></div>
<div class="image-canvas-editor__floating-toolbar"><span id="toolbar">操作</span></div>
<div class="game-resource-book-scene-titlebar" id="title"></div>
`;
for (const id of ['blank', 'card', 'select']) {
expect(isResourceCanvasPanTarget(document.getElementById(id))).toBe(true);
}
for (const id of [
'play',
'corner',
'video',
'input',
'editor',
'filter',
'toolbar',
'title',
]) {
expect(isResourceCanvasPanTarget(document.getElementById(id))).toBe(
false,
);
}
expect(isResourceCanvasPanTarget(null)).toBe(false);
});
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 class="game-resource-info-panel"><div id="info-value">封面图</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(query('.game-resource-info-panel')),
).toBe(true);
expect(isResourceCanvasInteractionTarget(query('#info-value'))).toBe(true);
expect(isResourceCanvasInteractionTarget(null)).toBe(false);
document.body.innerHTML = '';
});
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('快速编辑 / 资源信息 / 生成动画任一浮层开着都算画布浮层打开', () => {
// 三种浮层共用一条开关判据:少算「生成动画」时,只有它开着的那一帧
// `resolveResourceCanvasFloatingPanelDismissOpen` 会判 false,点外部就收不掉面板。
expect(
resolveResourceCanvasFloatingPanelOpen({
isQuickEditPanelOpen: false,
isResourceInfoPanelOpen: false,
isCharacterAnimationPanelOpen: false,
}),
).toBe(false);
for (const panel of [
{ isQuickEditPanelOpen: true },
{ isResourceInfoPanelOpen: true },
{ isCharacterAnimationPanelOpen: true },
] as const) {
const isFloatingPanelOpen = resolveResourceCanvasFloatingPanelOpen({
isQuickEditPanelOpen: false,
isResourceInfoPanelOpen: false,
isCharacterAnimationPanelOpen: false,
...panel,
});
expect(isFloatingPanelOpen).toBe(true);
expect(
resolveResourceCanvasFloatingPanelDismissOpen({
isCanvasVisible: true,
isFloatingPanelOpen,
hostOverlay: {
isResourcePanelOpen: false,
isGenerationPanelOpen: false,
isClassificationPanelOpen: false,
isRenameDialogOpen: false,
isRecoveryPanelOpen: false,
},
}),
).toBe(true);
}
});
test('画布可见且画布浮层打开时才接管点外部关闭', () => {
const closed = {
isResourcePanelOpen: false,
isGenerationPanelOpen: false,
isClassificationPanelOpen: false,
isRenameDialogOpen: false,
isRecoveryPanelOpen: false,
};
const base = {
isCanvasVisible: true,
isFloatingPanelOpen: true,
hostOverlay: closed,
};
// 判据对浮层种类无差别:快速编辑与信息浮层共用这一条(宿主把两者合成一个布尔)。
expect(resolveResourceCanvasFloatingPanelDismissOpen(base)).toBe(true);
expect(
resolveResourceCanvasFloatingPanelDismissOpen({
...base,
isFloatingPanelOpen: false,
}),
).toBe(false);
expect(
resolveResourceCanvasFloatingPanelDismissOpen({
...base,
isCanvasVisible: false,
}),
).toBe(false);
expect(
resolveResourceCanvasFloatingPanelDismissOpen({
...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,
isFloatingPanelOpen: true,
hostOverlay: closed,
};
expect(resolveResourceCanvasFocusEscapeActive(base)).toBe(true);
// 选中工具条没有独立开关:只有选中、没有浮层时 Escape 同样要清选中。
expect(
resolveResourceCanvasFocusEscapeActive({
...base,
isFloatingPanelOpen: false,
hasSelectedResource: true,
}),
).toBe(true);
expect(
resolveResourceCanvasFocusEscapeActive({
...base,
isFloatingPanelOpen: false,
hasSelectedResource: false,
}),
).toBe(false);
expect(
resolveResourceCanvasFocusEscapeActive({
...base,
isCanvasVisible: false,
}),
).toBe(false);
expect(
resolveResourceCanvasFocusEscapeActive({
...base,
hostOverlay: { ...closed, isRenameDialogOpen: true },
}),
).toBe(false);
const documentPreviewOverlay = { ...closed, isDocumentPreviewOpen: true };
expect(
resolveResourceCanvasFocusEscapeActive({
...base,
hostOverlay: documentPreviewOverlay,
}),
).toBe(false);
expect(
resolveResourceCanvasFloatingPanelDismissOpen({
isCanvasVisible: true,
isFloatingPanelOpen: true,
hostOverlay: documentPreviewOverlay,
}),
).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-005 的验收视频里,这一下把整个快速编辑面板
* 连同画布选中一起收掉了。
*
* 这里用 React 的 `onClick`(应用里的真实形状)复现。
*/
function SelfUnmountingOverlayHarness({
onDismiss,
}: {
onDismiss: () => void;
}) {
const canvasRef = useRef<HTMLDivElement | null>(null);
const [restoreVisible, setRestoreVisible] = useState(true);
const [panelOpen, setPanelOpen] = useState(true);
useImageCanvasFloatingOptionDismiss({
isOpen: panelOpen,
boundaryRefs: [canvasRef],
onDismiss: () => {
onDismiss();
setPanelOpen(false);
},
});
return (
<div ref={canvasRef} data-testid="self-unmount-canvas">
{panelOpen ? (
<div role="dialog" aria-label="快速编辑浮层面板">
{restoreVisible ? (
<button type="button" onClick={() => setRestoreVisible(false)}>
恢复原文
</button>
) : null}
</div>
) : null}
</div>
);
}
/**
* 同一种形状的**同步**版本:目标节点在自己的原生 click 监听里被 `remove()`。
*
* 边界判定必须在事件派发最早期完成——等冒泡到 `document` 再拿 `event.target` 现算,
* 节点已经脱离 DOM,`element.contains(target)` 恒为 false,「点浮层内部」就被误判成
* 「点外部」,面板被自己的按钮关掉。这条用例是 AGC-005 的回归判据:
* 冻结判定接上之前它是红的。
*/
function NativeSelfRemovingOverlayHarness({
onDismiss,
}: {
onDismiss: () => void;
}) {
const canvasRef = useRef<HTMLDivElement | null>(null);
const buttonRef = useRef<HTMLButtonElement | null>(null);
const [panelOpen, setPanelOpen] = useState(true);
useImageCanvasFloatingOptionDismiss({
isOpen: panelOpen,
boundaryRefs: [canvasRef],
onDismiss: () => {
onDismiss();
setPanelOpen(false);
},
});
useEffect(() => {
const button = buttonRef.current;
if (!button) {
return undefined;
}
const handleClick = () => {
button.remove();
};
button.addEventListener('click', handleClick);
return () => button.removeEventListener('click', handleClick);
}, [panelOpen]);
return (
<div ref={canvasRef} data-testid="native-self-unmount-canvas">
{panelOpen ? (
<div role="dialog" aria-label="快速编辑浮层面板">
<button ref={buttonRef} type="button">
恢复原文
</button>
</div>
) : null}
</div>
);
}
/**
* 宿主自己 portal 到 `body` 的浮层(`@` 资源选择器 / 候选菜单)由 `isInsideExtraOverlay`
* 登记;菜单项点完即卸载时同样不能被判成点外部——portal 语义必须与边界 DOM 一视同仁。
*/
function ExtraOverlaySelfRemovalHarness({
onDismiss,
}: {
onDismiss: () => void;
}) {
const canvasRef = useRef<HTMLDivElement | null>(null);
const itemRef = useRef<HTMLButtonElement | null>(null);
const [panelOpen, setPanelOpen] = useState(true);
useImageCanvasFloatingOptionDismiss({
isOpen: panelOpen,
boundaryRefs: [canvasRef],
onDismiss: () => {
onDismiss();
setPanelOpen(false);
},
isInsideExtraOverlay: (target) =>
target instanceof Element &&
target.closest('.resource-reference-picker') !== null,
});
useEffect(() => {
const item = itemRef.current;
if (!item) {
return undefined;
}
const handleClick = () => {
item.remove();
};
item.addEventListener('click', handleClick);
return () => item.removeEventListener('click', handleClick);
}, [panelOpen]);
if (!panelOpen) {
return null;
}
return (
<>
<div ref={canvasRef} data-testid="registered-overlay-canvas">
<div role="dialog" aria-label="快速编辑浮层面板" />
</div>
{createPortal(
<div className="resource-reference-picker">
<button ref={itemRef} type="button">
引用素材
</button>
</div>,
document.body,
)}
</>
);
}
describe('点外部关闭的边界判据', () => {
test('点会自卸载的按钮不算点外部,面板不退出', () => {
const onDismiss = vi.fn();
render(<SelfUnmountingOverlayHarness onDismiss={onDismiss} />);
fireEvent.click(screen.getByRole('button', { name: '恢复原文' }));
expect(screen.queryByRole('button', { name: '恢复原文' })).toBeNull();
expect(onDismiss).not.toHaveBeenCalled();
expect(
screen.getByRole('dialog', { name: '快速编辑浮层面板' }),
).toBeTruthy();
});
test('按钮在自己这次点击里被同步卸载,也仍算点在浮层内部', () => {
const onDismiss = vi.fn();
render(<NativeSelfRemovingOverlayHarness onDismiss={onDismiss} />);
const button = screen.getByRole('button', { name: '恢复原文' });
fireEvent.click(button);
// 目标确实是在这次点击里卸载的——判定不能因此翻成「点外部」。
expect(button.isConnected).toBe(false);
expect(onDismiss).not.toHaveBeenCalled();
expect(
screen.getByRole('dialog', { name: '快速编辑浮层面板' }),
).toBeTruthy();
});
test('登记的 portal 浮层项自己卸载也算内部,宿主面板不被误关', () => {
const onDismiss = vi.fn();
render(<ExtraOverlaySelfRemovalHarness onDismiss={onDismiss} />);
const item = screen.getByRole('button', { name: '引用素材' });
fireEvent.click(item);
expect(item.isConnected).toBe(false);
expect(onDismiss).not.toHaveBeenCalled();
expect(
screen.getByRole('dialog', { name: '快速编辑浮层面板' }),
).toBeTruthy();
});
test('点边界外照旧立即关闭(对照)', () => {
const onDismiss = vi.fn();
render(<NativeSelfRemovingOverlayHarness onDismiss={onDismiss} />);
fireEvent.click(document.body);
expect(onDismiss).toHaveBeenCalledTimes(1);
expect(
screen.queryByRole('dialog', { name: '快速编辑浮层面板' }),
).toBeNull();
});
});
describe('画布滚轮归属判据', () => {
/**
* 与「点外部关闭」同一份口径:portal 出去的东西 DOM 上不在边界里,但 React 的事件会沿
* React 树冒泡到画布宿主的 `onWheel`。滚轮这类事件必须按 DOM 判归属,否则用户在
* `@` 选择器列表上滚动时,背后的画布会跟着平移 / 缩放。
*/
test('portal 出去的浮层与已登记浮层归浮层,边界里的画布元素归画布', () => {
document.body.innerHTML = `
<div id="owner">
<div id="card-blank">画布空白</div>
<div class="image-canvas-editor__portal-menu"><span id="menu-item">共享弹出层选项</span></div>
<div class="resource-reference-picker"><span id="picker-item">选择器条目</span></div>
</div>
<div id="portal"><span id="portal-item">portal 到 body 的候选项</span></div>
`;
const ownerRef = { current: document.querySelector<HTMLElement>('#owner') };
const options = {
boundaryRefs: [ownerRef],
isInsideExtraOverlay: (target: EventTarget | null) =>
target instanceof Element &&
target.closest('.resource-reference-picker') !== null,
};
const at = (selector: string) =>
document.querySelector<HTMLElement>(selector);
// 画布自己的:DOM 在边界里,也不是浮层。
expect(isFloatingOverlayWheelEvent(at('#card-blank'), options)).toBe(false);
// portal 到 body 的浮层:DOM 根本不在边界里 ⇒ 归浮层。
expect(isFloatingOverlayWheelEvent(at('#portal-item'), options)).toBe(true);
// 边界里但落在共享弹出层上:与「点外部关闭」同口径 ⇒ 归浮层。
expect(isFloatingOverlayWheelEvent(at('#menu-item'), options)).toBe(true);
// 边界里但由宿主登记成浮层(`@` 选择器 / 候选菜单)⇒ 归浮层。
expect(isFloatingOverlayWheelEvent(at('#picker-item'), options)).toBe(true);
// 判不出归属(没有事件目标)时不抢:归浮层。
expect(isFloatingOverlayWheelEvent(null, options)).toBe(true);
document.body.innerHTML = '';
});
test('自带滚动区的画布浮层归浮层,资源卡与画布空白仍归画布', () => {
document.body.innerHTML = `
<div class="game-resource-book-manager">
<div class="game-resource-card"><span id="card">卡片</span></div>
<div id="blank">画布空白</div>
<div class="image-canvas-editor__generation-composer"><span id="prompt">快速编辑提示词</span></div>
<div class="game-resource-info-panel"><span id="info">信息浮层</span></div>
<div class="game-resource-filter-panel"><span id="filter">筛选面板</span></div>
</div>
`;
const at = (selector: string) =>
document.querySelector<HTMLElement>(selector);
expect(isResourceCanvasWheelOverlayTarget(at('#card'))).toBe(false);
expect(isResourceCanvasWheelOverlayTarget(at('#blank'))).toBe(false);
expect(isResourceCanvasWheelOverlayTarget(at('#prompt'))).toBe(true);
expect(isResourceCanvasWheelOverlayTarget(at('#info'))).toBe(true);
expect(isResourceCanvasWheelOverlayTarget(at('#filter'))).toBe(true);
expect(isResourceCanvasWheelOverlayTarget(null)).toBe(false);
document.body.innerHTML = '';
});
});
/** 与 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();
});
});