Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
26 changes: 18 additions & 8 deletions superset-frontend/package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 1 addition & 1 deletion superset-frontend/packages/superset-ui-core/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -77,7 +77,7 @@
"math-expression-evaluator": "^2.0.7",
"parse-ms": "^4.0.0",
"re-resizable": "^6.11.2",
"react-ace": "^14.0.1",
"react-ace": "^15.0.0",
"react-draggable": "^4.7.1",
"react-error-boundary": "^6.1.2",
"react-js-cron": "^6.0.2",
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -604,6 +604,61 @@ test('cleans up event listeners on unmount', async () => {
offSpy.mockRestore();
});

test('re-applies annotations only when their content actually changes across renders (react-ace 15 fast-equals regression guard)', async () => {
// react-ace's componentDidUpdate decides whether to call
// session.setAnnotations() by deep-comparing the new/old `annotations`
// prop (lib/ace.js, using an internal deep-equality helper -- lodash's
// isEqual through react-ace 14.x, fast-equals's deepEqual from 15.0.0
// onward). Superset's own AceEditorProvider/EditorWrapper always pass a
// freshly `.map()`-derived annotations array on every render, so this
// guards the actual behavior Superset relies on: a same-content-but-
// different-reference array must NOT re-trigger setAnnotations (or the
// editor would thrash on every keystroke-driven re-render), while a
// genuinely different array must still update the editor.
const ref = createRef<AceEditor>();
const annotationsV1 = [{ row: 0, column: 0, type: 'error', text: 'oops' }];

const { rerender, container } = render(
<SQLEditor ref={ref as React.Ref<never>} annotations={annotationsV1} />,
);

await waitFor(() => {
expect(container.querySelector(selector)).toBeInTheDocument();
});

const session = ref.current?.editor?.getSession();
expect(session).toBeDefined();
if (!session) return;

// The initial mount already applies annotations via componentDidMount,
// not componentDidUpdate, so start observing only from the first update.
const setAnnotationsSpy = jest.spyOn(session, 'setAnnotations');

// Same content, new array/object references -- must be a no-op.
const annotationsV1SameContent = [
{ row: 0, column: 0, type: 'error', text: 'oops' },
];
rerender(
<SQLEditor
ref={ref as React.Ref<never>}
annotations={annotationsV1SameContent}
/>,
);
expect(setAnnotationsSpy).not.toHaveBeenCalled();

// Genuinely different content -- must update, with the new value.
const annotationsV2 = [
{ row: 1, column: 2, type: 'warning', text: 'different' },
];
rerender(
<SQLEditor ref={ref as React.Ref<never>} annotations={annotationsV2} />,
);
expect(setAnnotationsSpy).toHaveBeenCalledTimes(1);
expect(setAnnotationsSpy).toHaveBeenCalledWith(annotationsV2);

setAnnotationsSpy.mockRestore();
});

test('does not move autocomplete popup if target container is document.body', async () => {
const ref = createRef<AceEditor>();
const { container } = render(<SQLEditor ref={ref as React.Ref<never>} />);
Expand Down
Loading