mirror of
https://github.com/Syngnat/GoNavi.git
synced 2026-05-22 17:00:21 +08:00
🐛 fix(redis): 修复命名空间行点击误选中
- 命名空间行点击改为展开或折叠分组 - 阻止行点击冒泡触发 Tree 复选/选中逻辑 - 增加 Redis 树行点击交互回归测试 Refs #457
This commit is contained in:
193
frontend/src/components/RedisViewer.interaction.test.tsx
Normal file
193
frontend/src/components/RedisViewer.interaction.test.tsx
Normal file
@@ -0,0 +1,193 @@
|
||||
import React from 'react';
|
||||
import { act, create, type ReactTestRenderer } from 'react-test-renderer';
|
||||
import { beforeEach, describe, expect, it, vi } from 'vitest';
|
||||
|
||||
import RedisViewer from './RedisViewer';
|
||||
|
||||
const storeState = vi.hoisted(() => ({
|
||||
connections: [
|
||||
{
|
||||
id: 'redis-1',
|
||||
name: 'redis',
|
||||
config: {
|
||||
type: 'redis',
|
||||
host: '127.0.0.1',
|
||||
port: 6379,
|
||||
password: '',
|
||||
database: '',
|
||||
},
|
||||
},
|
||||
],
|
||||
theme: 'light',
|
||||
appearance: {
|
||||
enabled: true,
|
||||
opacity: 1,
|
||||
blur: 0,
|
||||
useNativeMacWindowControls: false,
|
||||
},
|
||||
}));
|
||||
|
||||
const redisBackend = vi.hoisted(() => ({
|
||||
RedisScanKeys: vi.fn(),
|
||||
RedisGetValue: vi.fn(),
|
||||
}));
|
||||
|
||||
const antdState = vi.hoisted(() => ({
|
||||
treeProps: null as any,
|
||||
message: {
|
||||
error: vi.fn(),
|
||||
success: vi.fn(),
|
||||
warning: vi.fn(),
|
||||
},
|
||||
}));
|
||||
|
||||
vi.mock('../store', () => {
|
||||
const useStore = Object.assign(
|
||||
(selector: (state: typeof storeState) => any) => selector(storeState),
|
||||
{ getState: () => storeState },
|
||||
);
|
||||
return { useStore };
|
||||
});
|
||||
|
||||
vi.mock('@monaco-editor/react', async () => {
|
||||
const React = await import('react');
|
||||
return {
|
||||
default: () => React.createElement('div', { 'data-monaco-editor': true }),
|
||||
};
|
||||
});
|
||||
|
||||
vi.mock('@ant-design/icons', async () => {
|
||||
const React = await import('react');
|
||||
const Icon = () => React.createElement('span', { 'data-icon': true });
|
||||
return {
|
||||
ReloadOutlined: Icon,
|
||||
DeleteOutlined: Icon,
|
||||
PlusOutlined: Icon,
|
||||
EditOutlined: Icon,
|
||||
SearchOutlined: Icon,
|
||||
ClockCircleOutlined: Icon,
|
||||
CopyOutlined: Icon,
|
||||
FolderOpenOutlined: Icon,
|
||||
KeyOutlined: Icon,
|
||||
RightOutlined: Icon,
|
||||
DownOutlined: Icon,
|
||||
};
|
||||
});
|
||||
|
||||
vi.mock('antd', async () => {
|
||||
const React = await import('react');
|
||||
const passthrough = (tag: string) => ({ children, ...props }: any) => React.createElement(tag, props, children);
|
||||
const Button = ({ children, ...props }: any) => React.createElement('button', props, children);
|
||||
const Input = Object.assign(
|
||||
({ ...props }: any) => React.createElement('input', props),
|
||||
{
|
||||
Search: ({ ...props }: any) => React.createElement('input', props),
|
||||
TextArea: ({ ...props }: any) => React.createElement('textarea', props),
|
||||
},
|
||||
);
|
||||
const FormComponent = Object.assign(
|
||||
({ children, ...props }: any) => React.createElement('form', props, children),
|
||||
{
|
||||
Item: passthrough('div'),
|
||||
useForm: () => [{
|
||||
validateFields: vi.fn(),
|
||||
resetFields: vi.fn(),
|
||||
setFieldsValue: vi.fn(),
|
||||
}],
|
||||
},
|
||||
);
|
||||
|
||||
return {
|
||||
Table: passthrough('div'),
|
||||
Input,
|
||||
Button,
|
||||
Space: Object.assign(passthrough('div'), { Compact: passthrough('div') }),
|
||||
Tag: passthrough('span'),
|
||||
Tree: (props: any) => {
|
||||
antdState.treeProps = props;
|
||||
return React.createElement('redis-tree');
|
||||
},
|
||||
Spin: ({ children }: any) => React.createElement(React.Fragment, null, children),
|
||||
message: antdState.message,
|
||||
Modal: Object.assign(passthrough('div'), { confirm: vi.fn() }),
|
||||
Form: FormComponent,
|
||||
InputNumber: ({ ...props }: any) => React.createElement('input', props),
|
||||
Popconfirm: passthrough('span'),
|
||||
Tooltip: ({ children }: any) => React.createElement(React.Fragment, null, children),
|
||||
Radio: {
|
||||
Group: passthrough('div'),
|
||||
Button,
|
||||
},
|
||||
};
|
||||
});
|
||||
|
||||
const flushEffects = async () => {
|
||||
await act(async () => {
|
||||
await Promise.resolve();
|
||||
await Promise.resolve();
|
||||
await Promise.resolve();
|
||||
});
|
||||
};
|
||||
|
||||
describe('RedisViewer tree interactions', () => {
|
||||
beforeEach(() => {
|
||||
vi.clearAllMocks();
|
||||
antdState.treeProps = null;
|
||||
redisBackend.RedisScanKeys.mockResolvedValue({
|
||||
success: true,
|
||||
data: {
|
||||
cursor: '0',
|
||||
keys: [
|
||||
{ key: 'app:user:1', type: 'string', ttl: -1 },
|
||||
{ key: 'app:user:2', type: 'string', ttl: -1 },
|
||||
],
|
||||
},
|
||||
});
|
||||
redisBackend.RedisGetValue.mockResolvedValue({
|
||||
success: true,
|
||||
data: { key: 'app:user:1', type: 'string', ttl: -1, value: 'demo' },
|
||||
});
|
||||
vi.stubGlobal('window', {
|
||||
innerWidth: 1280,
|
||||
innerHeight: 800,
|
||||
addEventListener: vi.fn(),
|
||||
removeEventListener: vi.fn(),
|
||||
go: {
|
||||
app: {
|
||||
App: redisBackend,
|
||||
},
|
||||
},
|
||||
});
|
||||
vi.stubGlobal('ResizeObserver', undefined);
|
||||
});
|
||||
|
||||
it('toggles namespace expansion from row clicks without checking the group', async () => {
|
||||
let renderer: ReactTestRenderer;
|
||||
await act(async () => {
|
||||
renderer = create(<RedisViewer connectionId="redis-1" redisDB={0} />);
|
||||
});
|
||||
await flushEffects();
|
||||
|
||||
const appGroup = antdState.treeProps.treeData.find((node: any) => node.key === 'group:app');
|
||||
expect(appGroup).toBeTruthy();
|
||||
expect(antdState.treeProps.expandedKeys).not.toContain('group:app');
|
||||
|
||||
const groupTitle = antdState.treeProps.titleRender(appGroup);
|
||||
expect(typeof groupTitle.props.onClick).toBe('function');
|
||||
|
||||
const event = {
|
||||
preventDefault: vi.fn(),
|
||||
stopPropagation: vi.fn(),
|
||||
};
|
||||
await act(async () => {
|
||||
groupTitle.props.onClick(event);
|
||||
});
|
||||
|
||||
expect(event.preventDefault).toHaveBeenCalled();
|
||||
expect(event.stopPropagation).toHaveBeenCalled();
|
||||
expect(antdState.treeProps.expandedKeys).toContain('group:app');
|
||||
expect(antdState.treeProps.checkedKeys.checked).toEqual([]);
|
||||
|
||||
renderer!.unmount();
|
||||
});
|
||||
});
|
||||
@@ -762,7 +762,7 @@ const RedisViewer: React.FC<RedisViewerProps> = ({ connectionId, redisDB }) => {
|
||||
});
|
||||
}, [isLargeKeyspace]);
|
||||
|
||||
const stopTreeTitleEvent = (event: React.MouseEvent<HTMLElement>) => {
|
||||
const stopTreeTitleEvent = (event: React.SyntheticEvent<HTMLElement>) => {
|
||||
event.preventDefault();
|
||||
event.stopPropagation();
|
||||
};
|
||||
@@ -776,6 +776,20 @@ const RedisViewer: React.FC<RedisViewerProps> = ({ connectionId, redisDB }) => {
|
||||
const isExpanded = expandedGroupKeys.includes(groupNodeKey);
|
||||
return (
|
||||
<div
|
||||
role="button"
|
||||
tabIndex={0}
|
||||
onMouseDown={stopTreeTitleEvent}
|
||||
onClick={(event) => {
|
||||
stopTreeTitleEvent(event);
|
||||
handleToggleGroupExpand(groupNodeKey);
|
||||
}}
|
||||
onKeyDown={(event) => {
|
||||
if (event.key !== 'Enter' && event.key !== ' ') {
|
||||
return;
|
||||
}
|
||||
stopTreeTitleEvent(event);
|
||||
handleToggleGroupExpand(groupNodeKey);
|
||||
}}
|
||||
style={{
|
||||
display: 'flex',
|
||||
alignItems: 'center',
|
||||
@@ -784,6 +798,7 @@ const RedisViewer: React.FC<RedisViewerProps> = ({ connectionId, redisDB }) => {
|
||||
width: '100%',
|
||||
minWidth: 0,
|
||||
padding: '2px 0',
|
||||
cursor: 'pointer',
|
||||
}}
|
||||
>
|
||||
<Space size={6} style={{ minWidth: 0, overflow: 'hidden' }}>
|
||||
|
||||
Reference in New Issue
Block a user