mirror of
https://github.com/n8n-io/n8n.git
synced 2025-03-05 20:50:17 -08:00
fix(editor): Fix Code node bug erasing and overwriting code when switching between nodes (#12637)
Some checks are pending
Test Master / install-and-build (push) Waiting to run
Test Master / Unit tests (18.x) (push) Blocked by required conditions
Test Master / Unit tests (20.x) (push) Blocked by required conditions
Test Master / Unit tests (22.4) (push) Blocked by required conditions
Test Master / Lint (push) Blocked by required conditions
Test Master / Notify Slack on failure (push) Blocked by required conditions
Some checks are pending
Test Master / install-and-build (push) Waiting to run
Test Master / Unit tests (18.x) (push) Blocked by required conditions
Test Master / Unit tests (20.x) (push) Blocked by required conditions
Test Master / Unit tests (22.4) (push) Blocked by required conditions
Test Master / Lint (push) Blocked by required conditions
Test Master / Notify Slack on failure (push) Blocked by required conditions
Co-authored-by: Elias Meire <elias@meire.dev>
This commit is contained in:
parent
0b0f532367
commit
02d953db34
|
@ -61,3 +61,25 @@ Object.defineProperty(window, 'matchMedia', {
|
|||
dispatchEvent: vi.fn(),
|
||||
})),
|
||||
});
|
||||
|
||||
class Worker {
|
||||
onmessage: (message: string) => void;
|
||||
|
||||
url: string;
|
||||
|
||||
constructor(url: string) {
|
||||
this.url = url;
|
||||
this.onmessage = () => {};
|
||||
}
|
||||
|
||||
postMessage(message: string) {
|
||||
this.onmessage(message);
|
||||
}
|
||||
|
||||
addEventListener() {}
|
||||
}
|
||||
|
||||
Object.defineProperty(window, 'Worker', {
|
||||
writable: true,
|
||||
value: Worker,
|
||||
});
|
||||
|
|
103
packages/editor-ui/src/composables/useCodeEditor.test.ts
Normal file
103
packages/editor-ui/src/composables/useCodeEditor.test.ts
Normal file
|
@ -0,0 +1,103 @@
|
|||
import { renderComponent } from '@/__tests__/render';
|
||||
import { EditorView } from '@codemirror/view';
|
||||
import { createTestingPinia } from '@pinia/testing';
|
||||
import { waitFor } from '@testing-library/vue';
|
||||
import { setActivePinia } from 'pinia';
|
||||
import { beforeEach, describe, vi } from 'vitest';
|
||||
import { defineComponent, h, ref, toValue } from 'vue';
|
||||
import { useCodeEditor } from './useCodeEditor';
|
||||
import userEvent from '@testing-library/user-event';
|
||||
|
||||
describe('useCodeEditor', () => {
|
||||
const defaultOptions: Omit<Parameters<typeof useCodeEditor>[0], 'editorRef'> = {
|
||||
language: 'javaScript',
|
||||
};
|
||||
|
||||
const renderCodeEditor = async (options: Partial<typeof defaultOptions> = defaultOptions) => {
|
||||
let codeEditor!: ReturnType<typeof useCodeEditor>;
|
||||
const renderResult = renderComponent(
|
||||
defineComponent({
|
||||
setup() {
|
||||
const root = ref<HTMLElement>();
|
||||
codeEditor = useCodeEditor({ ...defaultOptions, ...options, editorRef: root });
|
||||
|
||||
return () => h('div', { ref: root, 'data-test-id': 'editor-root' });
|
||||
},
|
||||
}),
|
||||
{ props: { options } },
|
||||
);
|
||||
expect(renderResult.getByTestId('editor-root')).toBeInTheDocument();
|
||||
await waitFor(() => toValue(codeEditor.editor));
|
||||
return { renderResult, codeEditor };
|
||||
};
|
||||
|
||||
beforeEach(() => {
|
||||
setActivePinia(createTestingPinia());
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
vi.clearAllMocks();
|
||||
vi.useRealTimers();
|
||||
});
|
||||
|
||||
it('should create an editor', async () => {
|
||||
const { codeEditor } = await renderCodeEditor();
|
||||
|
||||
await waitFor(() => expect(toValue(codeEditor.editor)).toBeInstanceOf(EditorView));
|
||||
});
|
||||
|
||||
it('should focus editor', async () => {
|
||||
const { renderResult, codeEditor } = await renderCodeEditor({});
|
||||
|
||||
const root = renderResult.getByTestId('editor-root');
|
||||
const input = root.querySelector('.cm-line') as HTMLDivElement;
|
||||
|
||||
await userEvent.click(input);
|
||||
|
||||
expect(codeEditor.editor.value?.hasFocus).toBe(true);
|
||||
});
|
||||
|
||||
it('should emit changes', async () => {
|
||||
vi.useFakeTimers();
|
||||
|
||||
const user = userEvent.setup({
|
||||
advanceTimers: vi.advanceTimersByTime,
|
||||
});
|
||||
|
||||
const onChange = vi.fn();
|
||||
const { renderResult } = await renderCodeEditor({
|
||||
onChange,
|
||||
});
|
||||
|
||||
const root = renderResult.getByTestId('editor-root');
|
||||
const input = root.querySelector('.cm-line') as HTMLDivElement;
|
||||
|
||||
await user.type(input, 'test');
|
||||
|
||||
vi.advanceTimersByTime(300);
|
||||
|
||||
expect(onChange.mock.calls[0][0].state.doc.toString()).toEqual('test');
|
||||
});
|
||||
|
||||
it('should emit debounced changes before unmount', async () => {
|
||||
vi.useFakeTimers();
|
||||
|
||||
const user = userEvent.setup({
|
||||
advanceTimers: vi.advanceTimersByTime,
|
||||
});
|
||||
|
||||
const onChange = vi.fn();
|
||||
const { renderResult } = await renderCodeEditor({
|
||||
onChange,
|
||||
});
|
||||
|
||||
const root = renderResult.getByTestId('editor-root');
|
||||
const input = root.querySelector('.cm-line') as HTMLDivElement;
|
||||
|
||||
await user.type(input, 'test');
|
||||
|
||||
renderResult.unmount();
|
||||
|
||||
expect(onChange.mock.calls[0][0].state.doc.toString()).toEqual('test');
|
||||
});
|
||||
});
|
|
@ -30,7 +30,6 @@ import {
|
|||
} from '@codemirror/view';
|
||||
import { indentationMarkers } from '@replit/codemirror-indentation-markers';
|
||||
import { html } from 'codemirror-lang-html-n8n';
|
||||
import { debounce } from 'lodash-es';
|
||||
import { jsonParse, type CodeExecutionMode, type IDataObject } from 'n8n-workflow';
|
||||
import { v4 as uuid } from 'uuid';
|
||||
import {
|
||||
|
@ -47,6 +46,8 @@ import {
|
|||
import { useCompleter } from '../components/CodeNodeEditor/completer';
|
||||
import { mappingDropCursor } from '../plugins/codemirror/dragAndDrop';
|
||||
import { languageFacet, type CodeEditorLanguage } from '../plugins/codemirror/format';
|
||||
import { debounce } from 'lodash-es';
|
||||
import { ignoreUpdateAnnotation } from '../utils/forceParse';
|
||||
|
||||
export type CodeEditorLanguageParamsMap = {
|
||||
json: {};
|
||||
|
@ -85,7 +86,6 @@ export const useCodeEditor = <L extends CodeEditorLanguage>({
|
|||
const editor = ref<EditorView>();
|
||||
const hasFocus = ref(false);
|
||||
const hasChanges = ref(false);
|
||||
const lastChange = ref<ViewUpdate>();
|
||||
const selection = ref<SelectionRange>(EditorSelection.cursor(0)) as Ref<SelectionRange>;
|
||||
const customExtensions = ref<Compartment>(new Compartment());
|
||||
const readOnlyExtensions = ref<Compartment>(new Compartment());
|
||||
|
@ -157,14 +157,19 @@ export const useCodeEditor = <L extends CodeEditorLanguage>({
|
|||
const emitChanges = debounce((update: ViewUpdate) => {
|
||||
onChange(update);
|
||||
}, 300);
|
||||
const lastChange = ref<ViewUpdate>();
|
||||
|
||||
function onEditorUpdate(update: ViewUpdate) {
|
||||
autocompleteStatus.value = completionStatus(update.view.state);
|
||||
updateSelection(update);
|
||||
|
||||
if (update.docChanged) {
|
||||
hasChanges.value = true;
|
||||
const shouldIgnoreUpdate = update.transactions.some((tr) =>
|
||||
tr.annotation(ignoreUpdateAnnotation),
|
||||
);
|
||||
|
||||
if (update.docChanged && !shouldIgnoreUpdate) {
|
||||
lastChange.value = update;
|
||||
hasChanges.value = true;
|
||||
emitChanges(update);
|
||||
}
|
||||
}
|
||||
|
@ -369,7 +374,10 @@ export const useCodeEditor = <L extends CodeEditorLanguage>({
|
|||
// Code is too large, localStorage quota exceeded
|
||||
localStorage.removeItem(storedStateId.value);
|
||||
}
|
||||
if (lastChange.value) onChange(lastChange.value);
|
||||
|
||||
if (lastChange.value) {
|
||||
onChange(lastChange.value);
|
||||
}
|
||||
editor.value.destroy();
|
||||
}
|
||||
});
|
||||
|
|
|
@ -1,14 +1,19 @@
|
|||
import { Annotation } from '@codemirror/state';
|
||||
import type { EditorView } from '@codemirror/view';
|
||||
|
||||
export const ignoreUpdateAnnotation = Annotation.define<boolean>();
|
||||
|
||||
/**
|
||||
* Simulate user action to force parser to catch up during scroll.
|
||||
*/
|
||||
export function forceParse(view: EditorView) {
|
||||
view.dispatch({
|
||||
changes: { from: view.viewport.to, insert: '_' },
|
||||
annotations: [ignoreUpdateAnnotation.of(true)],
|
||||
});
|
||||
|
||||
view.dispatch({
|
||||
changes: { from: view.viewport.to - 1, to: view.viewport.to, insert: '' },
|
||||
annotations: [ignoreUpdateAnnotation.of(true)],
|
||||
});
|
||||
}
|
||||
|
|
Loading…
Reference in a new issue