mirror of
https://github.com/Syngnat/GoNavi.git
synced 2026-06-16 19:49:51 +08:00
🐛 fix(sql-editor): 修复对象元数据与跳转交互异常
- 修复 SQL 元数据 hover provider 多实例重复注册导致内容重复显示 - 修复侧栏对象拖入编辑器后 Monaco 原生拖拽虚线残留 - 修复跨库拖拽对象丢失来源库导致后续无法跳转 - 兼容 macOS Cmd 点击时 Monaco 未提供 leftButton 的事件结构 - 补充 hover 去重、拖拽插入、对象跳转和 Cmd 点击回归测试
This commit is contained in:
@@ -675,6 +675,7 @@ describe('QueryEditor external SQL save', () => {
|
||||
connectionId: 'conn-1',
|
||||
dbName: 'analytics',
|
||||
tableName: 'events',
|
||||
objectType: 'table',
|
||||
});
|
||||
expect((window as any).dispatchEvent).toHaveBeenCalledWith(expect.objectContaining({
|
||||
type: 'gonavi:locate-sidebar-object',
|
||||
@@ -683,6 +684,48 @@ describe('QueryEditor external SQL save', () => {
|
||||
expect(stopPropagation).toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('opens a table tab on macOS cmd click when Monaco omits leftButton', async () => {
|
||||
editorState.value = 'select * from fs_mkefu_regist_record;';
|
||||
autoFetchState.visible = true;
|
||||
backendApp.DBGetDatabases.mockResolvedValueOnce({ success: true, data: [{ Database: 'mkefu_location_dev_local' }] });
|
||||
backendApp.DBGetTables.mockResolvedValueOnce({ success: true, data: [{ Tables_in_mkefu_location_dev_local: 'fs_mkefu_regist_record' }] });
|
||||
backendApp.DBGetAllColumns.mockResolvedValueOnce({ success: true, data: [] });
|
||||
|
||||
await act(async () => {
|
||||
create(<QueryEditor tab={createTab({ query: editorState.value, dbName: 'mkefu_location_dev_local' })} />);
|
||||
});
|
||||
await act(async () => {
|
||||
await Promise.resolve();
|
||||
await Promise.resolve();
|
||||
});
|
||||
|
||||
const preventDefault = vi.fn();
|
||||
const stopPropagation = vi.fn();
|
||||
await act(async () => {
|
||||
editorState.mouseDownListeners[0]?.({
|
||||
target: { position: { lineNumber: 1, column: 'select * from fs_mkefu_regist_record'.length } },
|
||||
event: {
|
||||
browserEvent: { button: 0, buttons: 1 },
|
||||
ctrlKey: false,
|
||||
metaKey: true,
|
||||
preventDefault,
|
||||
stopPropagation,
|
||||
},
|
||||
});
|
||||
});
|
||||
|
||||
expect(storeState.setActiveContext).toHaveBeenCalledWith({ connectionId: 'conn-1', dbName: 'mkefu_location_dev_local' });
|
||||
expect(storeState.addTab).toHaveBeenCalledWith(expect.objectContaining({
|
||||
type: 'table',
|
||||
connectionId: 'conn-1',
|
||||
dbName: 'mkefu_location_dev_local',
|
||||
tableName: 'fs_mkefu_regist_record',
|
||||
objectType: 'table',
|
||||
}));
|
||||
expect(preventDefault).toHaveBeenCalled();
|
||||
expect(stopPropagation).toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('shows link-style hover feedback when ctrl/cmd is pressed over a navigable identifier', async () => {
|
||||
editorState.value = 'select * from analytics.events where id = 1';
|
||||
autoFetchState.visible = true;
|
||||
@@ -912,6 +955,39 @@ describe('QueryEditor external SQL save', () => {
|
||||
expect(hover?.contents?.[0]?.value).toContain('表:`users`');
|
||||
});
|
||||
|
||||
it('registers SQL metadata hover provider only once across query editor instances', async () => {
|
||||
editorState.value = 'select * from H2.S_BUSI';
|
||||
autoFetchState.visible = true;
|
||||
backendApp.DBGetDatabases.mockResolvedValue({ success: true, data: [{ Database: 'H2' }] });
|
||||
backendApp.DBGetTables.mockResolvedValue({ success: true, data: [{ Tables_in_H2: 'H2.S_BUSI' }] });
|
||||
backendApp.DBGetAllColumns.mockResolvedValue({ success: true, data: [] });
|
||||
|
||||
let firstRenderer: ReactTestRenderer;
|
||||
let secondRenderer: ReactTestRenderer;
|
||||
await act(async () => {
|
||||
firstRenderer = create(<QueryEditor tab={createTab({ id: 'tab-1', query: editorState.value, dbName: 'H2' })} isActive={false} />);
|
||||
});
|
||||
await act(async () => {
|
||||
secondRenderer = create(<QueryEditor tab={createTab({ id: 'tab-2', query: editorState.value, dbName: 'H2' })} isActive />);
|
||||
});
|
||||
await act(async () => {
|
||||
await Promise.resolve();
|
||||
await Promise.resolve();
|
||||
});
|
||||
|
||||
expect(editorState.hoverProviders).toHaveLength(1);
|
||||
const hover = editorState.hoverProviders[0].provideHover(
|
||||
editorState.editor.getModel(),
|
||||
{ lineNumber: 1, column: 18 },
|
||||
);
|
||||
const hoverText = String(hover?.contents?.[0]?.value || '');
|
||||
expect(hoverText.match(/\*\*表\*\*/g)).toHaveLength(1);
|
||||
expect(hoverText).toContain('`H2.S_BUSI`');
|
||||
|
||||
firstRenderer!.unmount();
|
||||
secondRenderer!.unmount();
|
||||
});
|
||||
|
||||
it('keeps hover underline active when ctrl/cmd is pressed repeatedly without moving the mouse', async () => {
|
||||
const windowListeners: Record<string, ((event?: any) => void)[]> = {};
|
||||
vi.stubGlobal('window', {
|
||||
@@ -2550,6 +2626,7 @@ describe('QueryEditor external SQL save', () => {
|
||||
preventDefault: vi.fn(),
|
||||
stopPropagation: vi.fn(),
|
||||
dataTransfer: {
|
||||
types: ['application/x-gonavi-sql-object', 'text/plain'],
|
||||
getData: (type: string) => {
|
||||
if (type === 'application/x-gonavi-sql-object') {
|
||||
return JSON.stringify({ text: 'reporting.active_users' });
|
||||
@@ -2570,6 +2647,184 @@ describe('QueryEditor external SQL save', () => {
|
||||
expect(editorState.value).toContain('reporting.active_users');
|
||||
});
|
||||
|
||||
it('prevents Monaco native drag marker and keeps metadata hover after sidebar object drops', async () => {
|
||||
const domListeners: Record<string, ((event?: any) => void)[]> = {};
|
||||
editorState.domNode = {
|
||||
style: { cursor: '' },
|
||||
addEventListener: vi.fn((type: string, listener: (event?: any) => void) => {
|
||||
domListeners[type] ||= [];
|
||||
domListeners[type].push(listener);
|
||||
}),
|
||||
removeEventListener: vi.fn(),
|
||||
} as any;
|
||||
editorState.editor.getTargetAtClientPoint = vi.fn(() => ({
|
||||
position: { lineNumber: 1, column: 'SELECT * FROM '.length + 1 },
|
||||
}));
|
||||
editorState.value = 'SELECT * FROM ';
|
||||
autoFetchState.visible = true;
|
||||
backendApp.DBGetDatabases.mockResolvedValueOnce({ success: true, data: [{ Database: 'front_end_sys' }] });
|
||||
backendApp.DBGetTables.mockResolvedValueOnce({ success: true, data: [{ Tables_in_front_end_sys: 'fs_mkefu_regist_record' }] });
|
||||
backendApp.DBGetAllColumns.mockResolvedValueOnce({ success: true, data: [] });
|
||||
|
||||
await act(async () => {
|
||||
create(<QueryEditor tab={createTab({ query: editorState.value, dbName: 'front_end_sys' })} />);
|
||||
});
|
||||
await act(async () => {
|
||||
await Promise.resolve();
|
||||
await Promise.resolve();
|
||||
});
|
||||
|
||||
const dragOverEvent = {
|
||||
preventDefault: vi.fn(),
|
||||
stopPropagation: vi.fn(),
|
||||
dataTransfer: {
|
||||
types: ['application/x-gonavi-sql-object', 'text/plain'],
|
||||
dropEffect: 'none',
|
||||
getData: vi.fn(() => ''),
|
||||
},
|
||||
};
|
||||
await act(async () => {
|
||||
domListeners.dragover?.forEach((listener) => listener(dragOverEvent));
|
||||
});
|
||||
|
||||
expect(dragOverEvent.preventDefault).toHaveBeenCalled();
|
||||
expect(dragOverEvent.stopPropagation).toHaveBeenCalled();
|
||||
expect(dragOverEvent.dataTransfer.dropEffect).toBe('copy');
|
||||
expect(dragOverEvent.dataTransfer.getData).not.toHaveBeenCalled();
|
||||
|
||||
await act(async () => {
|
||||
domListeners.drop?.forEach((listener) => listener({
|
||||
clientX: 10,
|
||||
clientY: 10,
|
||||
preventDefault: vi.fn(),
|
||||
stopPropagation: vi.fn(),
|
||||
dataTransfer: {
|
||||
types: ['application/x-gonavi-sql-object', 'text/plain'],
|
||||
getData: (type: string) => {
|
||||
if (type === 'application/x-gonavi-sql-object') {
|
||||
return JSON.stringify({ text: 'fs_mkefu_regist_record' });
|
||||
}
|
||||
if (type === 'text/plain') {
|
||||
return 'fs_mkefu_regist_record';
|
||||
}
|
||||
return '';
|
||||
},
|
||||
},
|
||||
}));
|
||||
});
|
||||
|
||||
const hover = editorState.hoverProviders[0]?.provideHover(
|
||||
editorState.editor.getModel(),
|
||||
{ lineNumber: 1, column: 'SELECT * FROM fs_mkefu_regist_record'.length },
|
||||
);
|
||||
expect(editorState.value).toContain('fs_mkefu_regist_record');
|
||||
expect(hover?.contents?.[0]?.value).toContain('**表** `fs_mkefu_regist_record`');
|
||||
|
||||
await act(async () => {
|
||||
editorState.mouseDownListeners[0]?.({
|
||||
target: { position: { lineNumber: 1, column: 'SELECT * FROM fs_mkefu_regist_record'.length } },
|
||||
event: {
|
||||
leftButton: true,
|
||||
ctrlKey: true,
|
||||
metaKey: false,
|
||||
preventDefault: vi.fn(),
|
||||
stopPropagation: vi.fn(),
|
||||
},
|
||||
});
|
||||
});
|
||||
|
||||
expect(storeState.setActiveContext).toHaveBeenCalledWith({ connectionId: 'conn-1', dbName: 'front_end_sys' });
|
||||
expect(storeState.addTab).toHaveBeenCalledWith(expect.objectContaining({
|
||||
type: 'table',
|
||||
connectionId: 'conn-1',
|
||||
dbName: 'front_end_sys',
|
||||
tableName: 'fs_mkefu_regist_record',
|
||||
objectType: 'table',
|
||||
}));
|
||||
});
|
||||
|
||||
it('keeps sidebar object navigation tied to the dragged database after drop', async () => {
|
||||
const domListeners: Record<string, ((event?: any) => void)[]> = {};
|
||||
editorState.domNode = {
|
||||
style: { cursor: '' },
|
||||
addEventListener: vi.fn((type: string, listener: (event?: any) => void) => {
|
||||
domListeners[type] ||= [];
|
||||
domListeners[type].push(listener);
|
||||
}),
|
||||
removeEventListener: vi.fn(),
|
||||
} as any;
|
||||
editorState.editor.getTargetAtClientPoint = vi.fn(() => ({
|
||||
position: { lineNumber: 1, column: 'SELECT * FROM '.length + 1 },
|
||||
}));
|
||||
editorState.value = 'SELECT * FROM ';
|
||||
autoFetchState.visible = true;
|
||||
backendApp.DBGetDatabases.mockResolvedValueOnce({ success: true, data: [{ Database: 'main' }, { Database: 'front_end_sys' }] });
|
||||
backendApp.DBGetTables
|
||||
.mockResolvedValueOnce({ success: true, data: [{ Tables_in_main: 'users' }] })
|
||||
.mockResolvedValueOnce({ success: true, data: [{ Tables_in_front_end_sys: 'fs_mkefu_regist_record' }] });
|
||||
backendApp.DBGetAllColumns
|
||||
.mockResolvedValueOnce({ success: true, data: [] })
|
||||
.mockResolvedValueOnce({ success: true, data: [] });
|
||||
|
||||
await act(async () => {
|
||||
create(<QueryEditor tab={createTab({ query: editorState.value, dbName: 'main' })} />);
|
||||
});
|
||||
await act(async () => {
|
||||
await Promise.resolve();
|
||||
await Promise.resolve();
|
||||
});
|
||||
|
||||
await act(async () => {
|
||||
domListeners.drop?.forEach((listener) => listener({
|
||||
clientX: 10,
|
||||
clientY: 10,
|
||||
preventDefault: vi.fn(),
|
||||
stopPropagation: vi.fn(),
|
||||
dataTransfer: {
|
||||
types: ['application/x-gonavi-sql-object', 'text/plain'],
|
||||
getData: (type: string) => {
|
||||
if (type === 'application/x-gonavi-sql-object') {
|
||||
return JSON.stringify({
|
||||
text: 'fs_mkefu_regist_record',
|
||||
nodeType: 'table',
|
||||
connectionId: 'conn-1',
|
||||
dbName: 'front_end_sys',
|
||||
});
|
||||
}
|
||||
if (type === 'text/plain') {
|
||||
return 'fs_mkefu_regist_record';
|
||||
}
|
||||
return '';
|
||||
},
|
||||
},
|
||||
}));
|
||||
});
|
||||
|
||||
expect(editorState.value).toContain('front_end_sys.fs_mkefu_regist_record');
|
||||
|
||||
await act(async () => {
|
||||
editorState.mouseDownListeners[0]?.({
|
||||
target: { position: { lineNumber: 1, column: 'SELECT * FROM front_end_sys.fs_mkefu_regist_record'.length } },
|
||||
event: {
|
||||
leftButton: true,
|
||||
ctrlKey: true,
|
||||
metaKey: false,
|
||||
preventDefault: vi.fn(),
|
||||
stopPropagation: vi.fn(),
|
||||
},
|
||||
});
|
||||
});
|
||||
|
||||
expect(storeState.setActiveContext).toHaveBeenCalledWith({ connectionId: 'conn-1', dbName: 'front_end_sys' });
|
||||
expect(storeState.addTab).toHaveBeenCalledWith(expect.objectContaining({
|
||||
type: 'table',
|
||||
connectionId: 'conn-1',
|
||||
dbName: 'front_end_sys',
|
||||
tableName: 'fs_mkefu_regist_record',
|
||||
objectType: 'table',
|
||||
}));
|
||||
});
|
||||
|
||||
it('runs selected SQL before cursor SQL', async () => {
|
||||
backendApp.DBQueryMulti.mockResolvedValueOnce({
|
||||
success: true,
|
||||
|
||||
Reference in New Issue
Block a user