Skip to content

Commit 1c1e4ef

Browse files
committed
chore: improve AsyncAceEditor tests
1 parent 5fae824 commit 1c1e4ef

1 file changed

Lines changed: 251 additions & 0 deletions

File tree

superset-frontend/packages/superset-ui-core/src/components/AsyncAceEditor/AsyncAceEditor.test.tsx

Lines changed: 251 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -16,7 +16,9 @@
1616
* specific language governing permissions and limitations
1717
* under the License.
1818
*/
19+
import { createRef } from 'react';
1920
import { render, screen, waitFor } from '@superset-ui/core/spec';
21+
import type AceEditor from 'react-ace';
2022
import {
2123
AsyncAceEditor,
2224
SQLEditor,
@@ -99,3 +101,252 @@ test('renders a custom placeholder', () => {
99101

100102
expect(screen.getByRole('paragraph')).toBeInTheDocument();
101103
});
104+
105+
test('registers afterExec event listener for command handling', async () => {
106+
const ref = createRef<AceEditor>();
107+
const { container } = render(<SQLEditor ref={ref as React.Ref<never>} />);
108+
109+
await waitFor(() => {
110+
expect(container.querySelector(selector)).toBeInTheDocument();
111+
});
112+
113+
const editorInstance = ref.current?.editor;
114+
expect(editorInstance).toBeDefined();
115+
116+
if (!editorInstance) return;
117+
118+
// Verify the commands object has the 'on' method (confirms event listener capability)
119+
expect(editorInstance.commands).toHaveProperty('on');
120+
expect(typeof editorInstance.commands.on).toBe('function');
121+
});
122+
123+
test('moves autocomplete popup to parent container when triggered', async () => {
124+
const ref = createRef<AceEditor>();
125+
const { container } = render(<SQLEditor ref={ref as React.Ref<never>} />);
126+
127+
await waitFor(() => {
128+
expect(container.querySelector(selector)).toBeInTheDocument();
129+
});
130+
131+
const editorInstance = ref.current?.editor;
132+
expect(editorInstance).toBeDefined();
133+
134+
if (!editorInstance) return;
135+
136+
// Create a mock autocomplete popup in the editor container
137+
const mockAutocompletePopup = document.createElement('div');
138+
mockAutocompletePopup.className = 'ace_autocomplete';
139+
editorInstance.container?.appendChild(mockAutocompletePopup);
140+
141+
const parentContainer =
142+
editorInstance.container?.closest('#ace-editor') ??
143+
editorInstance.container?.parentElement;
144+
145+
// Manually trigger the afterExec event with insertstring command using _emit
146+
type CommandManagerWithEmit = typeof editorInstance.commands & {
147+
_emit: (event: string, data: unknown) => void;
148+
};
149+
(editorInstance.commands as CommandManagerWithEmit)._emit('afterExec', {
150+
command: { name: 'insertstring' },
151+
args: ['SELECT'],
152+
});
153+
154+
await waitFor(() => {
155+
// Check that the popup has the data attribute set
156+
expect(mockAutocompletePopup.dataset.aceAutocomplete).toBe('true');
157+
// Check that the popup is in the parent container
158+
expect(parentContainer?.contains(mockAutocompletePopup)).toBe(true);
159+
});
160+
});
161+
162+
test('moves autocomplete popup on startAutocomplete command event', async () => {
163+
const ref = createRef<AceEditor>();
164+
const { container } = render(<SQLEditor ref={ref as React.Ref<never>} />);
165+
166+
await waitFor(() => {
167+
expect(container.querySelector(selector)).toBeInTheDocument();
168+
});
169+
170+
const editorInstance = ref.current?.editor;
171+
expect(editorInstance).toBeDefined();
172+
173+
if (!editorInstance) return;
174+
175+
// Create a mock autocomplete popup
176+
const mockAutocompletePopup = document.createElement('div');
177+
mockAutocompletePopup.className = 'ace_autocomplete';
178+
editorInstance.container?.appendChild(mockAutocompletePopup);
179+
180+
const parentContainer =
181+
editorInstance.container?.closest('#ace-editor') ??
182+
editorInstance.container?.parentElement;
183+
184+
// Manually trigger the afterExec event with startAutocomplete command
185+
type CommandManagerWithEmit = typeof editorInstance.commands & {
186+
_emit: (event: string, data: unknown) => void;
187+
};
188+
(editorInstance.commands as CommandManagerWithEmit)._emit('afterExec', {
189+
command: { name: 'startAutocomplete' },
190+
});
191+
192+
await waitFor(() => {
193+
// Check that the popup has the data attribute set
194+
expect(mockAutocompletePopup.dataset.aceAutocomplete).toBe('true');
195+
// Check that the popup is in the parent container
196+
expect(parentContainer?.contains(mockAutocompletePopup)).toBe(true);
197+
});
198+
});
199+
200+
test('does not move autocomplete popup on unrelated commands', async () => {
201+
const ref = createRef<AceEditor>();
202+
const { container } = render(<SQLEditor ref={ref as React.Ref<never>} />);
203+
204+
await waitFor(() => {
205+
expect(container.querySelector(selector)).toBeInTheDocument();
206+
});
207+
208+
const editorInstance = ref.current?.editor;
209+
expect(editorInstance).toBeDefined();
210+
211+
if (!editorInstance) return;
212+
213+
// Create a mock autocomplete popup in the body
214+
const mockAutocompletePopup = document.createElement('div');
215+
mockAutocompletePopup.className = 'ace_autocomplete';
216+
document.body.appendChild(mockAutocompletePopup);
217+
218+
const originalParent = mockAutocompletePopup.parentElement;
219+
220+
// Simulate an unrelated command (e.g., 'selectall')
221+
editorInstance.commands.exec('selectall', editorInstance, {});
222+
223+
// Wait a bit to ensure no movement happens
224+
await new Promise(resolve => {
225+
setTimeout(resolve, 100);
226+
});
227+
228+
// The popup should remain in its original location
229+
expect(mockAutocompletePopup.parentElement).toBe(originalParent);
230+
231+
// Cleanup
232+
document.body.removeChild(mockAutocompletePopup);
233+
});
234+
235+
test('revalidates cached autocomplete popup when detached from DOM', async () => {
236+
const ref = createRef<AceEditor>();
237+
const { container } = render(<SQLEditor ref={ref as React.Ref<never>} />);
238+
239+
await waitFor(() => {
240+
expect(container.querySelector(selector)).toBeInTheDocument();
241+
});
242+
243+
const editorInstance = ref.current?.editor;
244+
expect(editorInstance).toBeDefined();
245+
246+
if (!editorInstance) return;
247+
248+
// Create first autocomplete popup
249+
const firstPopup = document.createElement('div');
250+
firstPopup.className = 'ace_autocomplete';
251+
editorInstance.container?.appendChild(firstPopup);
252+
253+
// Trigger command to cache the first popup
254+
editorInstance.commands.exec('insertstring', editorInstance, 'SELECT');
255+
256+
await waitFor(() => {
257+
expect(firstPopup.dataset.aceAutocomplete).toBe('true');
258+
});
259+
260+
// Remove the first popup from DOM (simulating ACE editor replacing it)
261+
firstPopup.remove();
262+
263+
// Create a new autocomplete popup
264+
const secondPopup = document.createElement('div');
265+
secondPopup.className = 'ace_autocomplete';
266+
editorInstance.container?.appendChild(secondPopup);
267+
268+
// Trigger command again - should find and move the new popup
269+
editorInstance.commands.exec('insertstring', editorInstance, ' ');
270+
271+
await waitFor(() => {
272+
expect(secondPopup.dataset.aceAutocomplete).toBe('true');
273+
const parentContainer =
274+
editorInstance.container?.closest('#ace-editor') ??
275+
editorInstance.container?.parentElement;
276+
expect(parentContainer?.contains(secondPopup)).toBe(true);
277+
});
278+
});
279+
280+
test('cleans up event listeners on unmount', async () => {
281+
const ref = createRef<AceEditor>();
282+
const { container, unmount } = render(<SQLEditor ref={ref as React.Ref<never>} />);
283+
284+
await waitFor(() => {
285+
expect(container.querySelector(selector)).toBeInTheDocument();
286+
});
287+
288+
const editorInstance = ref.current?.editor;
289+
expect(editorInstance).toBeDefined();
290+
291+
if (!editorInstance) return;
292+
293+
// Spy on the commands.off method
294+
const offSpy = jest.spyOn(editorInstance.commands, 'off');
295+
296+
// Unmount the component
297+
unmount();
298+
299+
// Verify that the event listener was removed
300+
expect(offSpy).toHaveBeenCalledWith('afterExec', expect.any(Function));
301+
302+
offSpy.mockRestore();
303+
});
304+
305+
test('does not move autocomplete popup if target container is document.body', async () => {
306+
const ref = createRef<AceEditor>();
307+
const { container } = render(<SQLEditor ref={ref as React.Ref<never>} />);
308+
309+
await waitFor(() => {
310+
expect(container.querySelector(selector)).toBeInTheDocument();
311+
});
312+
313+
const editorInstance = ref.current?.editor;
314+
expect(editorInstance).toBeDefined();
315+
316+
if (!editorInstance) return;
317+
318+
// Create a mock autocomplete popup
319+
const mockAutocompletePopup = document.createElement('div');
320+
mockAutocompletePopup.className = 'ace_autocomplete';
321+
document.body.appendChild(mockAutocompletePopup);
322+
323+
// Mock the closest method to return null (simulating no #ace-editor parent)
324+
const originalClosest = editorInstance.container?.closest;
325+
if (editorInstance.container) {
326+
editorInstance.container.closest = jest.fn(() => null);
327+
}
328+
329+
// Mock parentElement to be document.body
330+
Object.defineProperty(editorInstance.container, 'parentElement', {
331+
value: document.body,
332+
configurable: true,
333+
});
334+
335+
const initialParent = mockAutocompletePopup.parentElement;
336+
337+
// Trigger command
338+
editorInstance.commands.exec('insertstring', editorInstance, 'SELECT');
339+
340+
await new Promise(resolve => {
341+
setTimeout(resolve, 100);
342+
});
343+
344+
// The popup should NOT be moved because target container is document.body
345+
expect(mockAutocompletePopup.parentElement).toBe(initialParent);
346+
347+
// Cleanup
348+
if (editorInstance.container && originalClosest) {
349+
editorInstance.container.closest = originalClosest;
350+
}
351+
document.body.removeChild(mockAutocompletePopup);
352+
});

0 commit comments

Comments
 (0)