fix interactive error

This commit is contained in:
2026-07-20 19:52:49 +08:00
parent e6fd4a1fe1
commit e8d8dee767
4 changed files with 98 additions and 13 deletions
@@ -95,9 +95,74 @@ describe('MessageBubble', () => {
await waitFor(() =>
expect(copyTextToClipboard).toHaveBeenCalledWith('第一行\n第二行'),
);
expect(
await screen.findByRole('menuitem', { name: '已复制' }),
).toBeTruthy();
await waitFor(() =>
expect(
screen.queryByRole('menu', { name: '消息右键菜单' }),
).toBeNull(),
);
});
it('closes the previous menu before another right click opens a new one', () => {
render(
<>
<MessageBubble
message={{
id: 40,
role: 'assistant',
text: '第一条消息',
attachments: [],
toolCall: null,
createdAt: '2026-07-20T00:00:00Z',
}}
busyAction={null}
onConfirmToolCall={vi.fn()}
onCancelToolCall={vi.fn()}
/>
<MessageBubble
message={{
id: 41,
role: 'assistant',
text: '第二条消息',
attachments: [],
toolCall: null,
createdAt: '2026-07-20T00:00:00Z',
}}
busyAction={null}
onConfirmToolCall={vi.fn()}
onCancelToolCall={vi.fn()}
/>
</>,
);
const messages = screen.getAllByLabelText('Agent消息');
fireEvent.contextMenu(messages[0]!, { clientX: 30, clientY: 40 });
fireEvent.contextMenu(messages[1]!, { clientX: 70, clientY: 80 });
const menus = screen.getAllByRole('menu', { name: '消息右键菜单' });
expect(menus).toHaveLength(1);
expect(menus[0]?.style.left).toBe('70px');
expect(menus[0]?.style.top).toBe('80px');
});
it('closes an open menu when the next right click has no menu target', () => {
renderMessage({
id: 42,
role: 'assistant',
text: '右键后应可收起',
attachments: [],
toolCall: null,
createdAt: '2026-07-20T00:00:00Z',
});
fireEvent.contextMenu(screen.getByLabelText('Agent消息'), {
clientX: 30,
clientY: 40,
});
expect(screen.getByRole('menu', { name: '消息右键菜单' })).toBeTruthy();
fireEvent.contextMenu(document.body, { clientX: 70, clientY: 80 });
expect(screen.queryByRole('menu', { name: '消息右键菜单' })).toBeNull();
});
it('copies an attached WebP as PNG without opening the message text menu', async () => {
@@ -182,7 +247,11 @@ describe('MessageBubble', () => {
.fn()
.mockResolvedValue(new Blob(['webp'], { type: 'image/webp' })),
} as unknown as Response);
await screen.findByRole('menuitem', { name: '已复制' });
await waitFor(() =>
expect(
screen.queryByRole('menu', { name: '消息素材右键菜单' }),
).toBeNull(),
);
expect(bitmapClose).toHaveBeenCalledTimes(1);
});
@@ -219,9 +288,11 @@ describe('MessageBubble', () => {
objectKey: 'editor/reference.png',
}),
);
expect(
await screen.findByRole('menuitem', { name: '已引用' }),
).toBeTruthy();
await waitFor(() =>
expect(
screen.queryByRole('menu', { name: '消息素材右键菜单' }),
).toBeNull(),
);
});
it('offers reference for generated images using their media lookup fields', async () => {
@@ -335,6 +406,9 @@ describe('MessageBubble', () => {
fireEvent.click(screen.getByRole('menuitem', { name: '下载视频' }));
await waitFor(() => expect(anchorClick).toHaveBeenCalledTimes(1));
expect(
screen.queryByRole('menu', { name: '消息素材右键菜单' }),
).toBeNull();
expect(createObjectURL).toHaveBeenCalledWith(expect.any(Blob));
expect(revokeObjectURL).toHaveBeenCalledWith('blob:agent-video');
});
@@ -115,17 +115,25 @@ export function MessageBubbleRightClickMenu({
onClose();
}
};
const handleContextMenu = (event: MouseEvent) => {
if (menuRef.current?.contains(event.target as Node)) {
event.preventDefault();
}
onClose();
};
const handleKeyDown = (event: KeyboardEvent) => {
if (event.key === 'Escape') {
onClose();
}
};
window.addEventListener('pointerdown', handlePointerDown);
window.addEventListener('contextmenu', handleContextMenu, true);
window.addEventListener('keydown', handleKeyDown);
window.addEventListener('scroll', onClose, true);
window.addEventListener('resize', onClose);
return () => {
window.removeEventListener('pointerdown', handlePointerDown);
window.removeEventListener('contextmenu', handleContextMenu, true);
window.removeEventListener('keydown', handleKeyDown);
window.removeEventListener('scroll', onClose, true);
window.removeEventListener('resize', onClose);
@@ -197,12 +197,14 @@ export function useRightClickMenu({
: false;
setRightClickMenu((current) =>
current?.target === target && current.pendingAction === action
? {
...current,
pendingAction: null,
resultAction: action,
result: succeeded ? 'success' : 'error',
}
? succeeded
? null
: {
...current,
pendingAction: null,
resultAction: action,
result: 'error',
}
: current,
);
},