@neo4j-cypher/react-codemirror 2.0.0-canary-9fcb804 → 2.0.0-canary-bb7e9d3
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/CypherEditor.d.ts +0 -1
- package/dist/CypherEditor.js +8 -9
- package/dist/CypherEditor.js.map +1 -1
- package/dist/CypherEditor.test.d.ts +1 -0
- package/dist/CypherEditor.test.js +150 -0
- package/dist/CypherEditor.test.js.map +1 -0
- package/dist/constants.d.ts +1 -0
- package/dist/constants.js +2 -0
- package/dist/constants.js.map +1 -0
- package/dist/e2e_tests/autoCompletion.spec.js +18 -0
- package/dist/e2e_tests/autoCompletion.spec.js.map +1 -1
- package/dist/e2e_tests/debounce.spec.js +7 -6
- package/dist/e2e_tests/debounce.spec.js.map +1 -1
- package/dist/lang-cypher/contants.test.js +1 -0
- package/dist/lang-cypher/contants.test.js.map +1 -1
- package/dist/lang-cypher/syntaxValidation.js +1 -3
- package/dist/lang-cypher/syntaxValidation.js.map +1 -1
- package/dist/ndlTokensCopy.test.js +1 -0
- package/dist/ndlTokensCopy.test.js.map +1 -1
- package/dist/tsconfig.tsbuildinfo +1 -1
- package/package.json +11 -9
- package/src/CypherEditor.test.tsx +197 -0
- package/src/CypherEditor.tsx +7 -11
- package/src/constants.ts +1 -0
- package/src/e2e_tests/autoCompletion.spec.tsx +32 -0
- package/src/e2e_tests/debounce.spec.tsx +23 -23
- package/src/lang-cypher/contants.test.ts +1 -0
- package/src/lang-cypher/syntaxValidation.ts +2 -5
- package/src/ndlTokensCopy.test.ts +1 -0
- package/src/viteEnv.d.ts +1 -0
|
@@ -0,0 +1,197 @@
|
|
|
1
|
+
// @vitest-environment jsdom
|
|
2
|
+
|
|
3
|
+
import { EditorView } from '@codemirror/view';
|
|
4
|
+
import { createRef } from 'react';
|
|
5
|
+
import { createRoot } from 'react-dom/client';
|
|
6
|
+
import { act } from 'react-dom/test-utils';
|
|
7
|
+
import { afterEach, beforeEach, expect, test, vi } from 'vitest';
|
|
8
|
+
import { DEBOUNCE_TIME } from './constants';
|
|
9
|
+
import { CypherEditor } from './CypherEditor';
|
|
10
|
+
|
|
11
|
+
const container = document.createElement('div');
|
|
12
|
+
let root: ReturnType<typeof createRoot>;
|
|
13
|
+
|
|
14
|
+
const ref = createRef<CypherEditor>();
|
|
15
|
+
let value = '';
|
|
16
|
+
const onChange = vi.fn((v: string) => {
|
|
17
|
+
value = v;
|
|
18
|
+
rerender();
|
|
19
|
+
});
|
|
20
|
+
|
|
21
|
+
global.IS_REACT_ACT_ENVIRONMENT = true;
|
|
22
|
+
|
|
23
|
+
/** Avoids crash in test environment */
|
|
24
|
+
function mockEditorView(editorView: EditorView) {
|
|
25
|
+
editorView.coordsAtPos = vi.fn().mockReturnValue({
|
|
26
|
+
left: 0,
|
|
27
|
+
top: 0,
|
|
28
|
+
right: 0,
|
|
29
|
+
bottom: 0,
|
|
30
|
+
});
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
async function debounce() {
|
|
34
|
+
await new Promise((resolve) => setTimeout(resolve, DEBOUNCE_TIME));
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
function getEditorValue() {
|
|
38
|
+
return ref.current.editorView.current.state.doc.toString();
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
function rerender() {
|
|
42
|
+
act(() => {
|
|
43
|
+
root.render(<CypherEditor value={value} onChange={onChange} ref={ref} />);
|
|
44
|
+
});
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
beforeEach(() => {
|
|
48
|
+
root = createRoot(container);
|
|
49
|
+
act(() => {
|
|
50
|
+
root.render(<CypherEditor value={value} onChange={onChange} ref={ref} />);
|
|
51
|
+
});
|
|
52
|
+
mockEditorView(ref.current.editorView.current);
|
|
53
|
+
});
|
|
54
|
+
|
|
55
|
+
afterEach(() => {
|
|
56
|
+
act(() => {
|
|
57
|
+
root.unmount();
|
|
58
|
+
});
|
|
59
|
+
value = '';
|
|
60
|
+
vi.clearAllMocks();
|
|
61
|
+
});
|
|
62
|
+
|
|
63
|
+
test('editorValue updates props.value after debounce', async () => {
|
|
64
|
+
ref.current.setValueAndFocus('new value');
|
|
65
|
+
expect(onChange).not.toHaveBeenCalled();
|
|
66
|
+
expect(getEditorValue()).toBe('new value');
|
|
67
|
+
expect(value).toBe('');
|
|
68
|
+
await debounce();
|
|
69
|
+
|
|
70
|
+
expect(onChange).toHaveBeenCalledOnce();
|
|
71
|
+
expect(getEditorValue()).toBe('new value');
|
|
72
|
+
expect(value).toBe('new value');
|
|
73
|
+
});
|
|
74
|
+
|
|
75
|
+
test('editorValue updates should not be applied twice', async () => {
|
|
76
|
+
const dispatch = vi.spyOn(ref.current.editorView.current, 'dispatch');
|
|
77
|
+
|
|
78
|
+
ref.current.setValueAndFocus('new value');
|
|
79
|
+
await debounce();
|
|
80
|
+
|
|
81
|
+
expect(onChange).toHaveBeenCalledOnce();
|
|
82
|
+
expect(dispatch).toHaveBeenCalledOnce(); // it gets called once for the initial setValueAndFocus
|
|
83
|
+
});
|
|
84
|
+
|
|
85
|
+
test('props.value updates editorValue', () => {
|
|
86
|
+
value = 'external value';
|
|
87
|
+
rerender();
|
|
88
|
+
|
|
89
|
+
expect(getEditorValue()).toBe('external value');
|
|
90
|
+
expect(value).toBe('external value');
|
|
91
|
+
});
|
|
92
|
+
|
|
93
|
+
test('props.value set to undefined preserves editorValue', () => {
|
|
94
|
+
// 1. value is set initially
|
|
95
|
+
value = 'initial';
|
|
96
|
+
rerender();
|
|
97
|
+
|
|
98
|
+
// 2. value is set to undefined
|
|
99
|
+
value = undefined;
|
|
100
|
+
rerender();
|
|
101
|
+
|
|
102
|
+
expect(onChange).not.toHaveBeenCalled();
|
|
103
|
+
expect(value).toBeUndefined();
|
|
104
|
+
expect(getEditorValue()).toBe('initial');
|
|
105
|
+
});
|
|
106
|
+
|
|
107
|
+
// value updates from outside onExecute are overwritten by pending updates
|
|
108
|
+
test.fails('new props.value should cancel onChange', async () => {
|
|
109
|
+
// 1. value is updated internally
|
|
110
|
+
ref.current.setValueAndFocus('update');
|
|
111
|
+
|
|
112
|
+
// 2. editor is rerendered with a new value while a value update is still pending
|
|
113
|
+
value = 'new external value';
|
|
114
|
+
rerender();
|
|
115
|
+
|
|
116
|
+
await debounce();
|
|
117
|
+
|
|
118
|
+
// expect(onChange).not.toHaveBeenCalled();
|
|
119
|
+
expect(getEditorValue()).toBe('new external value');
|
|
120
|
+
expect(value).toBe('new external value');
|
|
121
|
+
});
|
|
122
|
+
|
|
123
|
+
// value updates from outside onExecute are overwritten by pending updates
|
|
124
|
+
test.fails(
|
|
125
|
+
'new props.value set to same value should cancel onChange',
|
|
126
|
+
async () => {
|
|
127
|
+
// 1. value is set initially
|
|
128
|
+
value = 'same value';
|
|
129
|
+
rerender();
|
|
130
|
+
|
|
131
|
+
// 2. value is updated internally
|
|
132
|
+
ref.current.setValueAndFocus('update');
|
|
133
|
+
|
|
134
|
+
// 3. editor is rerendered with a new value while a value update is still pending
|
|
135
|
+
value = 'same value';
|
|
136
|
+
rerender();
|
|
137
|
+
|
|
138
|
+
await debounce();
|
|
139
|
+
|
|
140
|
+
// expect(onChange).not.toHaveBeenCalled();
|
|
141
|
+
expect(getEditorValue()).toBe('same value');
|
|
142
|
+
expect(value).toBe('same value');
|
|
143
|
+
},
|
|
144
|
+
);
|
|
145
|
+
|
|
146
|
+
test('rerender should not cancel onChange', async () => {
|
|
147
|
+
// 1. value is updated ínternally
|
|
148
|
+
ref.current.setValueAndFocus('changed');
|
|
149
|
+
|
|
150
|
+
// 2. editor is rerendered while a value update is still pending
|
|
151
|
+
rerender();
|
|
152
|
+
|
|
153
|
+
await debounce();
|
|
154
|
+
|
|
155
|
+
expect(onChange).toHaveBeenCalledOnce();
|
|
156
|
+
expect(getEditorValue()).toBe('changed');
|
|
157
|
+
expect(value).toBe('changed');
|
|
158
|
+
});
|
|
159
|
+
|
|
160
|
+
test('rerender with a previous update should not cancel onChange', async () => {
|
|
161
|
+
// 1. value is updated ínternally
|
|
162
|
+
ref.current.setValueAndFocus('changed');
|
|
163
|
+
await debounce();
|
|
164
|
+
|
|
165
|
+
// 2. value is updated ínternally again
|
|
166
|
+
ref.current.setValueAndFocus('new change');
|
|
167
|
+
|
|
168
|
+
// 3. editor is rerendered while a value update is still pending
|
|
169
|
+
rerender();
|
|
170
|
+
|
|
171
|
+
await debounce();
|
|
172
|
+
|
|
173
|
+
expect(onChange).toHaveBeenCalledTimes(2);
|
|
174
|
+
expect(getEditorValue()).toBe('new change');
|
|
175
|
+
expect(value).toBe('new change');
|
|
176
|
+
});
|
|
177
|
+
|
|
178
|
+
test('rerender with prior external update should not cancel onChange', async () => {
|
|
179
|
+
// 1. value is set initially
|
|
180
|
+
ref.current.setValueAndFocus('initial');
|
|
181
|
+
await debounce();
|
|
182
|
+
|
|
183
|
+
// 2. value is updated externally
|
|
184
|
+
value = 'external update';
|
|
185
|
+
rerender();
|
|
186
|
+
|
|
187
|
+
// 3. value is updated internally
|
|
188
|
+
ref.current.setValueAndFocus('internal update');
|
|
189
|
+
|
|
190
|
+
// 4. editor is rerendered while a value update is still pending
|
|
191
|
+
rerender();
|
|
192
|
+
|
|
193
|
+
await debounce();
|
|
194
|
+
|
|
195
|
+
expect(getEditorValue()).toBe('internal update');
|
|
196
|
+
expect(value).toBe('internal update');
|
|
197
|
+
});
|
package/src/CypherEditor.tsx
CHANGED
|
@@ -16,6 +16,7 @@ import {
|
|
|
16
16
|
import { type DbSchema } from '@neo4j-cypher/language-support';
|
|
17
17
|
import debounce from 'lodash.debounce';
|
|
18
18
|
import { Component, createRef } from 'react';
|
|
19
|
+
import { DEBOUNCE_TIME } from './constants';
|
|
19
20
|
import {
|
|
20
21
|
replaceHistory,
|
|
21
22
|
replMode as historyNavigation,
|
|
@@ -265,8 +266,6 @@ export class CypherEditor extends Component<
|
|
|
265
266
|
editorView: React.MutableRefObject<EditorView> = createRef();
|
|
266
267
|
private schemaRef: React.MutableRefObject<CypherConfig> = createRef();
|
|
267
268
|
|
|
268
|
-
private latestDispatchedValue: string | undefined;
|
|
269
|
-
|
|
270
269
|
/**
|
|
271
270
|
* Focus the editor
|
|
272
271
|
*/
|
|
@@ -316,10 +315,9 @@ export class CypherEditor extends Component<
|
|
|
316
315
|
private debouncedOnChange = this.props.onChange
|
|
317
316
|
? debounce(
|
|
318
317
|
((value, viewUpdate) => {
|
|
319
|
-
this.latestDispatchedValue = value;
|
|
320
318
|
this.props.onChange(value, viewUpdate);
|
|
321
319
|
}) satisfies CypherEditorProps['onChange'],
|
|
322
|
-
|
|
320
|
+
DEBOUNCE_TIME,
|
|
323
321
|
)
|
|
324
322
|
: undefined;
|
|
325
323
|
|
|
@@ -374,14 +372,12 @@ export class CypherEditor extends Component<
|
|
|
374
372
|
]
|
|
375
373
|
: [];
|
|
376
374
|
|
|
377
|
-
this.latestDispatchedValue = this.props.value;
|
|
378
|
-
|
|
379
375
|
this.editorState.current = EditorState.create({
|
|
380
376
|
extensions: [
|
|
381
377
|
keyBindingCompartment.of(
|
|
382
378
|
keymap.of([
|
|
383
379
|
...executeKeybinding(onExecute, newLineOnEnter, () =>
|
|
384
|
-
this.debouncedOnChange
|
|
380
|
+
this.debouncedOnChange?.flush(),
|
|
385
381
|
),
|
|
386
382
|
...extraKeybindings,
|
|
387
383
|
]),
|
|
@@ -440,10 +436,10 @@ export class CypherEditor extends Component<
|
|
|
440
436
|
const currentCmValue = this.editorView.current.state?.doc.toString() ?? '';
|
|
441
437
|
|
|
442
438
|
if (
|
|
443
|
-
this.props.value !== undefined &&
|
|
444
|
-
this.props.value !==
|
|
439
|
+
this.props.value !== undefined && // If the component becomes uncontolled, we just leave the value as is
|
|
440
|
+
this.props.value !== prevProps.value && // The value prop has changed, we need to update the editor
|
|
441
|
+
this.props.value !== currentCmValue // No need to dispatch an update if the value is the same
|
|
445
442
|
) {
|
|
446
|
-
this.debouncedOnChange?.cancel();
|
|
447
443
|
this.editorView.current.dispatch({
|
|
448
444
|
changes: {
|
|
449
445
|
from: 0,
|
|
@@ -510,7 +506,7 @@ export class CypherEditor extends Component<
|
|
|
510
506
|
...executeKeybinding(
|
|
511
507
|
this.props.onExecute,
|
|
512
508
|
this.props.newLineOnEnter,
|
|
513
|
-
() => this.debouncedOnChange
|
|
509
|
+
() => this.debouncedOnChange?.flush(),
|
|
514
510
|
),
|
|
515
511
|
...this.props.extraKeybindings,
|
|
516
512
|
]),
|
package/src/constants.ts
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export const DEBOUNCE_TIME = 200;
|
|
@@ -62,6 +62,38 @@ test('get completions when typing and can accept completions with tab', async ({
|
|
|
62
62
|
await expect(component).toContainText('RETURN');
|
|
63
63
|
});
|
|
64
64
|
|
|
65
|
+
test('get completions when typing in controlled component', async ({
|
|
66
|
+
mount,
|
|
67
|
+
page,
|
|
68
|
+
}) => {
|
|
69
|
+
let value = '';
|
|
70
|
+
const onChange = (val: string) => {
|
|
71
|
+
value = val;
|
|
72
|
+
void component.update(<CypherEditor value={val} onChange={onChange} />);
|
|
73
|
+
};
|
|
74
|
+
|
|
75
|
+
const component = await mount(
|
|
76
|
+
<CypherEditor value={value} onChange={onChange} />,
|
|
77
|
+
);
|
|
78
|
+
const textField = page.getByRole('textbox');
|
|
79
|
+
|
|
80
|
+
await textField.fill('RETU');
|
|
81
|
+
await page.waitForTimeout(500); // wait for debounce
|
|
82
|
+
|
|
83
|
+
await expect(
|
|
84
|
+
page.locator('.cm-tooltip-autocomplete').getByText('RETURN'),
|
|
85
|
+
).toBeVisible();
|
|
86
|
+
|
|
87
|
+
// We need to wait for the editor to realise there is a completion open
|
|
88
|
+
// so that it does not just indent with tab key
|
|
89
|
+
await page.waitForTimeout(500);
|
|
90
|
+
await textField.press('Tab');
|
|
91
|
+
|
|
92
|
+
await expect(page.locator('.cm-tooltip-autocomplete')).not.toBeVisible();
|
|
93
|
+
|
|
94
|
+
await expect(component).toContainText('RETURN');
|
|
95
|
+
});
|
|
96
|
+
|
|
65
97
|
test('can complete labels', async ({ mount, page }) => {
|
|
66
98
|
const component = await mount(
|
|
67
99
|
<CypherEditor
|
|
@@ -1,30 +1,30 @@
|
|
|
1
1
|
import { expect, test } from '@playwright/experimental-ct-react';
|
|
2
|
+
import { DEBOUNCE_TIME } from '../constants';
|
|
2
3
|
import { CypherEditor } from '../CypherEditor';
|
|
3
4
|
import { CypherEditorPage } from './e2eUtils';
|
|
4
5
|
|
|
5
|
-
|
|
6
|
+
// value updates from outside onExecute are overwritten by pending updates
|
|
7
|
+
test.fail(
|
|
8
|
+
'external updates should override debounced updates',
|
|
9
|
+
async ({ mount, page }) => {
|
|
10
|
+
const editorPage = new CypherEditorPage(page);
|
|
11
|
+
let value = '';
|
|
6
12
|
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
}
|
|
11
|
-
const editorPage = new CypherEditorPage(page);
|
|
12
|
-
let value = '';
|
|
13
|
-
|
|
14
|
-
const onChange = (val: string) => {
|
|
15
|
-
value = val;
|
|
16
|
-
void component.update(<CypherEditor value={val} onChange={onChange} />);
|
|
17
|
-
};
|
|
13
|
+
const onChange = (val: string) => {
|
|
14
|
+
value = val;
|
|
15
|
+
void component.update(<CypherEditor value={val} onChange={onChange} />);
|
|
16
|
+
};
|
|
18
17
|
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
18
|
+
const component = await mount(
|
|
19
|
+
<CypherEditor value={value} onChange={onChange} />,
|
|
20
|
+
);
|
|
22
21
|
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
}
|
|
22
|
+
await editorPage.getEditor().pressSequentially('RETURN 1');
|
|
23
|
+
onChange('foo');
|
|
24
|
+
await page.waitForTimeout(DEBOUNCE_TIME);
|
|
25
|
+
await expect(component).toContainText('foo');
|
|
26
|
+
},
|
|
27
|
+
);
|
|
28
28
|
|
|
29
29
|
test('onExecute updates should override debounce updates', async ({
|
|
30
30
|
mount,
|
|
@@ -53,14 +53,14 @@ test('onExecute updates should override debounce updates', async ({
|
|
|
53
53
|
|
|
54
54
|
await editorPage.getEditor().pressSequentially('RETURN 1');
|
|
55
55
|
await editorPage.getEditor().press('Control+Enter');
|
|
56
|
-
await page.waitForTimeout(
|
|
56
|
+
await page.waitForTimeout(DEBOUNCE_TIME);
|
|
57
57
|
await expect(component).not.toContainText('RETURN 1');
|
|
58
58
|
|
|
59
59
|
await editorPage.getEditor().pressSequentially('RETURN 1');
|
|
60
60
|
await editorPage.getEditor().pressSequentially('');
|
|
61
61
|
await editorPage.getEditor().pressSequentially('RETURN 1');
|
|
62
62
|
await editorPage.getEditor().press('Control+Enter');
|
|
63
|
-
await page.waitForTimeout(
|
|
63
|
+
await page.waitForTimeout(DEBOUNCE_TIME);
|
|
64
64
|
await expect(component).not.toContainText('RETURN 1');
|
|
65
65
|
});
|
|
66
66
|
|
|
@@ -94,7 +94,7 @@ test('onExecute should fire after debounced updates', async ({
|
|
|
94
94
|
await editorPage.getEditor().press('Control+Enter');
|
|
95
95
|
await editorPage.getEditor().fill('RETURN 2');
|
|
96
96
|
await editorPage.getEditor().press('Control+Enter');
|
|
97
|
-
await page.waitForTimeout(
|
|
97
|
+
await page.waitForTimeout(DEBOUNCE_TIME);
|
|
98
98
|
await expect(component).toContainText('RETURN 2');
|
|
99
99
|
expect(executedCommand).toBe('RETURN 2');
|
|
100
100
|
});
|
|
@@ -3,6 +3,7 @@ import {
|
|
|
3
3
|
applySyntaxColouring,
|
|
4
4
|
CypherTokenType,
|
|
5
5
|
} from '@neo4j-cypher/language-support';
|
|
6
|
+
import { expect, test } from 'vitest';
|
|
6
7
|
import { tokenTypeToStyleTag } from './constants';
|
|
7
8
|
|
|
8
9
|
const cypherQueryWithAllTokenTypes = `MATCH (variable :Label)-[:REL_TYPE]->()
|
|
@@ -5,12 +5,9 @@ import { DiagnosticSeverity } from 'vscode-languageserver-types';
|
|
|
5
5
|
import workerpool from 'workerpool';
|
|
6
6
|
import type { CypherConfig } from './langCypher';
|
|
7
7
|
import type { LinterTask, LintWorker } from './lintWorker';
|
|
8
|
+
import WorkerURL from './lintWorker?worker&url';
|
|
8
9
|
|
|
9
|
-
|
|
10
|
-
// @ts-ignore ignore: https://v3.vitejs.dev/guide/features.html#import-with-query-suffixes
|
|
11
|
-
import WorkerURL from './lintWorker?url&worker';
|
|
12
|
-
|
|
13
|
-
const pool = workerpool.pool(WorkerURL as string, {
|
|
10
|
+
const pool = workerpool.pool(WorkerURL, {
|
|
14
11
|
minWorkers: 2,
|
|
15
12
|
workerOpts: { type: 'module' },
|
|
16
13
|
workerTerminateTimeout: 2000,
|
package/src/viteEnv.d.ts
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
/// <reference types="vite/client" />
|