@neo4j-cypher/react-codemirror 1.0.3 → 2.0.0-next.0

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.
Files changed (65) hide show
  1. package/CHANGELOG.md +7 -0
  2. package/LICENSE.md +201 -0
  3. package/README.md +26 -27
  4. package/dist/cjs/index.cjs +1455 -0
  5. package/dist/cjs/index.cjs.map +7 -0
  6. package/dist/esm/index.mjs +1468 -0
  7. package/dist/esm/index.mjs.map +7 -0
  8. package/dist/types/CypherEditor.d.ts +118 -0
  9. package/dist/types/e2e_tests/auto-completion.spec.d.ts +1 -0
  10. package/dist/types/e2e_tests/e2e-utils.d.ts +12 -0
  11. package/dist/types/e2e_tests/extra-keybindings.spec.d.ts +1 -0
  12. package/dist/types/e2e_tests/history-navigation.spec.d.ts +1 -0
  13. package/dist/types/e2e_tests/mock-data.d.ts +3779 -0
  14. package/dist/types/e2e_tests/performance-test.spec.d.ts +1 -0
  15. package/dist/types/e2e_tests/sanity-checks.spec.d.ts +1 -0
  16. package/dist/types/e2e_tests/syntax-highlighting.spec.d.ts +1 -0
  17. package/dist/types/e2e_tests/syntax-validation.spec.d.ts +1 -0
  18. package/dist/types/icons.d.ts +2 -0
  19. package/dist/types/index.d.ts +5 -0
  20. package/dist/types/lang-cypher/ParserAdapter.d.ts +14 -0
  21. package/dist/types/lang-cypher/autocomplete.d.ts +3 -0
  22. package/dist/types/lang-cypher/constants.d.ts +31 -0
  23. package/dist/types/lang-cypher/contants.test.d.ts +1 -0
  24. package/dist/types/lang-cypher/create-cypher-theme.d.ts +26 -0
  25. package/dist/types/lang-cypher/lang-cypher.d.ts +7 -0
  26. package/dist/types/lang-cypher/syntax-validation.d.ts +3 -0
  27. package/dist/types/lang-cypher/theme-icons.d.ts +7 -0
  28. package/dist/types/ndl-tokens-copy.d.ts +379 -0
  29. package/dist/types/ndl-tokens-copy.test.d.ts +1 -0
  30. package/dist/types/neo4j-setup.d.ts +2 -0
  31. package/dist/types/repl-mode.d.ts +8 -0
  32. package/dist/types/themes.d.ts +11 -0
  33. package/dist/types/tsconfig.tsbuildinfo +1 -0
  34. package/package.json +60 -34
  35. package/src/CypherEditor.tsx +316 -0
  36. package/src/e2e_tests/auto-completion.spec.tsx +232 -0
  37. package/src/e2e_tests/e2e-utils.ts +75 -0
  38. package/src/e2e_tests/extra-keybindings.spec.tsx +57 -0
  39. package/src/e2e_tests/history-navigation.spec.tsx +144 -0
  40. package/src/e2e_tests/mock-data.ts +4310 -0
  41. package/src/e2e_tests/performance-test.spec.tsx +71 -0
  42. package/src/e2e_tests/sanity-checks.spec.tsx +87 -0
  43. package/src/e2e_tests/syntax-highlighting.spec.tsx +198 -0
  44. package/src/e2e_tests/syntax-validation.spec.tsx +157 -0
  45. package/src/icons.ts +87 -0
  46. package/src/index.ts +5 -0
  47. package/src/lang-cypher/ParserAdapter.ts +92 -0
  48. package/src/lang-cypher/autocomplete.ts +65 -0
  49. package/src/lang-cypher/constants.ts +61 -0
  50. package/src/lang-cypher/contants.test.ts +104 -0
  51. package/src/lang-cypher/create-cypher-theme.ts +207 -0
  52. package/src/lang-cypher/lang-cypher.ts +32 -0
  53. package/src/lang-cypher/syntax-validation.ts +24 -0
  54. package/src/lang-cypher/theme-icons.ts +27 -0
  55. package/src/ndl-tokens-copy.test.ts +11 -0
  56. package/src/ndl-tokens-copy.ts +379 -0
  57. package/src/neo4j-setup.tsx +129 -0
  58. package/src/repl-mode.ts +214 -0
  59. package/src/themes.ts +130 -0
  60. package/es/CypherEditor.js +0 -262
  61. package/es/react-codemirror.js +0 -1
  62. package/lib/CypherEditor.js +0 -272
  63. package/lib/react-codemirror.js +0 -13
  64. package/src/CypherEditor.d.ts +0 -310
  65. package/src/react-codemirror.d.ts +0 -18
@@ -0,0 +1,232 @@
1
+ import { expect, test } from '@playwright/experimental-ct-react';
2
+ import { CypherEditor } from '../CypherEditor';
3
+
4
+ test.use({ viewport: { width: 500, height: 500 } });
5
+
6
+ test('hello world end 2 end test', async ({ mount }) => {
7
+ const component = await mount(<CypherEditor value="hello world" />);
8
+ await expect(component).toContainText('hello world');
9
+ await component.update(<CypherEditor value="RETURN 123" />);
10
+ await expect(component).toContainText('RETURN 123');
11
+ });
12
+
13
+ test('can complete in the middle of statement', async ({ mount, page }) => {
14
+ const component = await mount(
15
+ <CypherEditor
16
+ value={`MATCH ()
17
+ WHER true
18
+ RETURN n;`}
19
+ />,
20
+ );
21
+
22
+ // Move into the statement and trigger autocompletion
23
+ const textField = page.getByRole('textbox');
24
+
25
+ await textField.focus();
26
+ await textField.press('ArrowDown');
27
+ await textField.press('ArrowRight');
28
+ await textField.press('ArrowRight');
29
+ await textField.press('ArrowRight');
30
+ await textField.press('ArrowRight');
31
+
32
+ await textField.press('Control+ ');
33
+
34
+ await expect(page.locator('.cm-tooltip-autocomplete')).toBeVisible();
35
+ await page.locator('.cm-tooltip-autocomplete').getByText('WHERE').click();
36
+
37
+ await expect(page.locator('.cm-tooltip-autocomplete')).not.toBeVisible();
38
+
39
+ await expect(component).toContainText('WHERE true');
40
+ });
41
+
42
+ test('get completions when typing and can accept completions with tab', async ({
43
+ mount,
44
+ page,
45
+ }) => {
46
+ const component = await mount(<CypherEditor />);
47
+ const textField = page.getByRole('textbox');
48
+
49
+ await textField.fill('RETU');
50
+
51
+ await expect(
52
+ page.locator('.cm-tooltip-autocomplete').getByText('RETURN'),
53
+ ).toBeVisible();
54
+
55
+ // We need to wait for the editor to realise there is a completion open
56
+ // so that it does not just indent with tab key
57
+ await page.waitForTimeout(500);
58
+ await textField.press('Tab');
59
+
60
+ await expect(page.locator('.cm-tooltip-autocomplete')).not.toBeVisible();
61
+
62
+ await expect(component).toContainText('RETURN');
63
+ });
64
+
65
+ test('can complete labels', async ({ mount, page }) => {
66
+ const component = await mount(
67
+ <CypherEditor
68
+ schema={{
69
+ labels: ['Pokemon'],
70
+ }}
71
+ />,
72
+ );
73
+
74
+ const textField = page.getByRole('textbox');
75
+
76
+ await textField.fill('MATCH (n :P');
77
+
78
+ await page.locator('.cm-tooltip-autocomplete').getByText('Pokemon').click();
79
+ await expect(page.locator('.cm-tooltip-autocomplete')).not.toBeVisible();
80
+
81
+ await expect(component).toContainText('MATCH (n :Pokemon');
82
+ });
83
+
84
+ test('can update dbschema', async ({ mount, page }) => {
85
+ const component = await mount(
86
+ <CypherEditor
87
+ schema={{
88
+ labels: ['Pokemon'],
89
+ }}
90
+ />,
91
+ );
92
+
93
+ const textField = page.getByRole('textbox');
94
+
95
+ await textField.fill('MATCH (n :');
96
+
97
+ await expect(
98
+ page.locator('.cm-tooltip-autocomplete').getByText('Pokemon'),
99
+ ).toBeVisible();
100
+
101
+ await textField.press('Escape');
102
+
103
+ await expect(page.locator('.cm-tooltip-autocomplete')).not.toBeVisible();
104
+
105
+ await component.update(
106
+ <CypherEditor
107
+ schema={{
108
+ labels: ['Pokemon', 'Digimon'],
109
+ }}
110
+ />,
111
+ );
112
+
113
+ await textField.press('Control+ ');
114
+
115
+ await expect(
116
+ page.locator('.cm-tooltip-autocomplete').getByText('Pokemon'),
117
+ ).toBeVisible();
118
+
119
+ await expect(
120
+ page.locator('.cm-tooltip-autocomplete').getByText('Digimon'),
121
+ ).toBeVisible();
122
+ });
123
+
124
+ test('can complete rel types', async ({ page, mount }) => {
125
+ const component = await mount(
126
+ <CypherEditor
127
+ schema={{
128
+ relationshipTypes: ['KNOWS'],
129
+ }}
130
+ />,
131
+ );
132
+
133
+ const textField = page.getByRole('textbox');
134
+
135
+ await textField.fill('MATCH (n)-[:');
136
+
137
+ await page.locator('.cm-tooltip-autocomplete').getByText('KNOWS').click();
138
+ await expect(page.locator('.cm-tooltip-autocomplete')).not.toBeVisible();
139
+
140
+ await expect(component).toContainText('MATCH (n)-[:KNOWS');
141
+ });
142
+
143
+ test('can complete functions', async ({ page, mount }) => {
144
+ const component = await mount(
145
+ <CypherEditor
146
+ schema={{
147
+ functionSignatures: {
148
+ function123: { label: 'function123', documentation: 'no docs' },
149
+ },
150
+ }}
151
+ />,
152
+ );
153
+
154
+ const textField = page.getByRole('textbox');
155
+
156
+ await textField.fill('RETURN func');
157
+
158
+ await page
159
+ .locator('.cm-tooltip-autocomplete')
160
+ .getByText('function123')
161
+ .click();
162
+
163
+ await expect(page.locator('.cm-tooltip-autocomplete')).not.toBeVisible();
164
+
165
+ await expect(component).toContainText('RETURN function123');
166
+ });
167
+
168
+ test('can complete procedures', async ({ page, mount }) => {
169
+ const component = await mount(
170
+ <CypherEditor
171
+ schema={{
172
+ procedureSignatures: {
173
+ 'db.ping': { label: 'db.ping', documentation: 'no docs' },
174
+ },
175
+ }}
176
+ />,
177
+ );
178
+
179
+ const textField = page.getByRole('textbox');
180
+
181
+ await textField.fill('CALL d');
182
+
183
+ await page.locator('.cm-tooltip-autocomplete').getByText('db.ping').click();
184
+ await expect(page.locator('.cm-tooltip-autocomplete')).not.toBeVisible();
185
+
186
+ await expect(component).toContainText('CALL db.ping');
187
+ });
188
+
189
+ test('can complete parameters', async ({ page, mount }) => {
190
+ const component = await mount(
191
+ <CypherEditor
192
+ schema={{
193
+ parameters: { parameter: { type: 'string' } },
194
+ }}
195
+ />,
196
+ );
197
+
198
+ const textField = page.getByRole('textbox');
199
+
200
+ await textField.fill('RETURN $p');
201
+
202
+ await page.locator('.cm-tooltip-autocomplete').getByText('parameter').click();
203
+ await expect(page.locator('.cm-tooltip-autocomplete')).not.toBeVisible();
204
+
205
+ await expect(component).toContainText('RETURN $parameter');
206
+ });
207
+
208
+ test('completes allShortestPaths correctly', async ({ page, mount }) => {
209
+ await mount(
210
+ <CypherEditor
211
+ schema={{
212
+ parameters: { parameter: { type: 'string' } },
213
+ }}
214
+ />,
215
+ );
216
+
217
+ const textField = page.getByRole('textbox');
218
+
219
+ // The first query contains errors on purpose so the
220
+ // syntax errors get triggered before the auto-completion
221
+ await textField.fill('MATCH (n) REURN n; MATCH a');
222
+
223
+ await page
224
+ .locator('.cm-tooltip-autocomplete')
225
+ .getByText('allShortestPaths')
226
+ .click();
227
+ await expect(page.locator('.cm-tooltip-autocomplete')).not.toBeVisible();
228
+
229
+ expect(await textField.textContent()).toEqual(
230
+ 'MATCH (n) REURN n; MATCH allShortestPaths',
231
+ );
232
+ });
@@ -0,0 +1,75 @@
1
+ import { expect } from '@playwright/experimental-ct-react';
2
+ import type { Locator, Page } from 'playwright/test';
3
+
4
+ export class CypherEditorPage {
5
+ readonly page: Page;
6
+
7
+ constructor(page: Page) {
8
+ this.page = page;
9
+ }
10
+
11
+ getEditor() {
12
+ return this.page.getByRole('textbox');
13
+ }
14
+
15
+ async focusEditor() {
16
+ await this.getEditor().focus();
17
+ }
18
+
19
+ editorBackgroundIsUnset() {
20
+ return this.page.locator('.cm-editor').evaluate((e: Element) => {
21
+ const browserDefaultBackgroundColor = 'rgba(0, 0, 0, 0)';
22
+ return (
23
+ window.getComputedStyle(e).getPropertyValue('background-color') ===
24
+ browserDefaultBackgroundColor
25
+ );
26
+ });
27
+ }
28
+
29
+ getHexColorOfLocator(locator: Locator) {
30
+ return locator.evaluate((e: Element) => {
31
+ // https://stackoverflow.com/questions/49974145/how-to-convert-rgba-to-hex-color-code-using-javascript
32
+ function RGBAToHexA(rgba: string, forceRemoveAlpha = false) {
33
+ return (
34
+ '#' +
35
+ rgba
36
+ .replace(/^rgba?\(|\s+|\)$/g, '')
37
+ .split(',')
38
+ .filter((string, index) => !forceRemoveAlpha || index !== 3)
39
+ .map((string) => parseFloat(string))
40
+ .map((number, index) =>
41
+ index === 3 ? Math.round(number * 255) : number,
42
+ )
43
+ .map((number) => number.toString(16))
44
+ .map((string) => (string.length === 1 ? '0' + string : string))
45
+ .join('')
46
+ );
47
+ }
48
+
49
+ const color = window.getComputedStyle(e).getPropertyValue('color');
50
+ return RGBAToHexA(color);
51
+ });
52
+ }
53
+
54
+ async checkErrorMessage(queryChunk: string, expectedMsg: string) {
55
+ return this.checkNotificationMessage('error', queryChunk, expectedMsg);
56
+ }
57
+
58
+ async checkWarningMessage(queryChunk: string, expectedMsg: string) {
59
+ return this.checkNotificationMessage('warning', queryChunk, expectedMsg);
60
+ }
61
+
62
+ private async checkNotificationMessage(
63
+ type: 'error' | 'warning',
64
+ queryChunk: string,
65
+ expectedMsg: string,
66
+ ) {
67
+ await expect(this.page.locator('.cm-lintRange-' + type).last()).toBeVisible(
68
+ { timeout: 2000 },
69
+ );
70
+
71
+ await this.page.getByText(queryChunk, { exact: true }).hover();
72
+ await expect(this.page.locator('.cm-tooltip-hover').last()).toBeVisible();
73
+ await expect(this.page.getByText(expectedMsg)).toBeVisible();
74
+ }
75
+ }
@@ -0,0 +1,57 @@
1
+ import { expect, test } from '@playwright/experimental-ct-react';
2
+ import { CypherEditor } from '../CypherEditor';
3
+ import { CypherEditorPage } from './e2e-utils';
4
+
5
+ test.use({ viewport: { width: 500, height: 500 } });
6
+
7
+ test('can add extra keybinding statically', async ({ mount, page }) => {
8
+ const editorPage = new CypherEditorPage(page);
9
+ let hasRun = false;
10
+ const component = await mount(
11
+ <CypherEditor
12
+ extraKeybindings={[
13
+ {
14
+ key: 'a',
15
+ preventDefault: true,
16
+ run: () => {
17
+ hasRun = true;
18
+ return true;
19
+ },
20
+ },
21
+ ]}
22
+ />,
23
+ );
24
+
25
+ await editorPage.getEditor().press('a');
26
+
27
+ await expect(component).not.toHaveText('a');
28
+ expect(hasRun).toBe(true);
29
+ });
30
+
31
+ test('can add extra keybinding dymanically', async ({ mount, page }) => {
32
+ const editorPage = new CypherEditorPage(page);
33
+ let hasRun = false;
34
+ const component = await mount(<CypherEditor />);
35
+ await editorPage.getEditor().press('a');
36
+ await editorPage.getEditor().press('Escape');
37
+ await expect(component).toContainText('a');
38
+
39
+ await component.update(
40
+ <CypherEditor
41
+ extraKeybindings={[
42
+ {
43
+ key: 'a',
44
+ preventDefault: true,
45
+ run: () => {
46
+ hasRun = true;
47
+ return true;
48
+ },
49
+ },
50
+ ]}
51
+ />,
52
+ );
53
+
54
+ await editorPage.getEditor().press('a');
55
+ await expect(component).not.toContainText('aa');
56
+ expect(hasRun).toBe(true);
57
+ });
@@ -0,0 +1,144 @@
1
+ import { expect, test } from '@playwright/experimental-ct-react';
2
+ import { CypherEditor } from '../CypherEditor';
3
+ import { CypherEditorPage } from './e2e-utils';
4
+
5
+ test.use({ viewport: { width: 500, height: 500 } });
6
+
7
+ test('respects preloaded history', async ({ page, mount }) => {
8
+ const editorPage = new CypherEditorPage(page);
9
+
10
+ const initialValue = 'MATCH (n) RETURN n;';
11
+
12
+ await mount(
13
+ <CypherEditor
14
+ value={initialValue}
15
+ initialHistory={['first', 'second']}
16
+ onExecute={() => {
17
+ /* needed to turn on history movements */
18
+ }}
19
+ />,
20
+ );
21
+
22
+ await editorPage.getEditor().press('ArrowUp');
23
+ await expect(page.getByText('first')).toBeVisible();
24
+
25
+ // First arrow up is to get to start of line
26
+ await editorPage.getEditor().press('ArrowUp');
27
+ await editorPage.getEditor().press('ArrowUp');
28
+ await expect(page.getByText('second')).toBeVisible();
29
+
30
+ await editorPage.getEditor().press('ArrowDown');
31
+ await expect(page.getByText('first')).toBeVisible();
32
+ await editorPage.getEditor().press('ArrowDown');
33
+ await expect(page.getByText(initialValue)).toBeVisible();
34
+ });
35
+
36
+ test('can execute queries and see them in history', async ({ page, mount }) => {
37
+ const editorPage = new CypherEditorPage(page);
38
+
39
+ const initialValue = `MATCH (n)
40
+ RETURN n;`;
41
+
42
+ let exectutedQueries = 0;
43
+ const onExecute = () => {
44
+ exectutedQueries++;
45
+ };
46
+
47
+ await mount(<CypherEditor value={initialValue} onExecute={onExecute} />);
48
+
49
+ // Execute initial query
50
+ await editorPage.getEditor().press('Control+Enter');
51
+ await editorPage.getEditor().press('Meta+Enter');
52
+ expect(exectutedQueries).toBe(1);
53
+
54
+ // Ensure query execution doesn't fire if the query is only whitespace
55
+ await editorPage.getEditor().fill(' ');
56
+ await editorPage.getEditor().press('Control+Enter');
57
+ await editorPage.getEditor().press('Meta+Enter');
58
+ expect(exectutedQueries).toBe(1);
59
+
60
+ // Ensure only enter doesn't execute query
61
+ await editorPage.getEditor().fill('multiline');
62
+ await editorPage.getEditor().press('Enter');
63
+ await editorPage.getEditor().press('Enter');
64
+ await editorPage.getEditor().press('Enter');
65
+ await editorPage.getEditor().press('Enter');
66
+ await page.keyboard.type('entry');
67
+ expect(exectutedQueries).toBe(1);
68
+ await editorPage.getEditor().press('Control+Enter');
69
+ await editorPage.getEditor().press('Meta+Enter');
70
+ expect(exectutedQueries).toBe(2);
71
+
72
+ // type a new query and make sure it's not lost when navigating history
73
+ await editorPage.getEditor().fill('draft');
74
+ await expect(page.getByText('draft')).toBeVisible();
75
+ expect(exectutedQueries).toBe(2);
76
+
77
+ // Navigate to the top of the editor before navigating history
78
+ await editorPage.getEditor().press('ArrowLeft');
79
+ await editorPage.getEditor().press('ArrowLeft');
80
+ await editorPage.getEditor().press('ArrowLeft');
81
+ await editorPage.getEditor().press('ArrowLeft');
82
+ await editorPage.getEditor().press('ArrowLeft');
83
+ await editorPage.getEditor().press('ArrowUp');
84
+
85
+ // Ensure moving down in the editor doesn't navigate history
86
+ await expect(page.getByText('multiline')).toBeVisible();
87
+
88
+ // arrow movements don't matter until bottom is hit
89
+ await editorPage.getEditor().press('ArrowUp');
90
+ await editorPage.getEditor().press('ArrowUp');
91
+ await editorPage.getEditor().press('ArrowDown');
92
+ await editorPage.getEditor().press('ArrowDown');
93
+
94
+ // editor still multiline
95
+ await expect(page.getByText('multiline')).toBeVisible();
96
+
97
+ // until you hit the end where have the draft we created earlier
98
+ await editorPage.getEditor().press('ArrowDown');
99
+ await expect(page.getByText('draft')).toBeVisible();
100
+ });
101
+
102
+ test('can navigate with cmd+up as well', async ({ page, mount }) => {
103
+ const editorPage = new CypherEditorPage(page);
104
+ const isMac = process.platform === 'darwin';
105
+ const metaUp = isMac ? 'Meta+ArrowUp' : 'Control+ArrowUp';
106
+ const metaDown = isMac ? 'Meta+ArrowDown' : 'Control+ArrowDown';
107
+
108
+ const initialValue = 'MATCH (n) RETURN n;';
109
+
110
+ await mount(
111
+ <CypherEditor
112
+ value={initialValue}
113
+ initialHistory={[
114
+ `one
115
+ multiline
116
+ entry
117
+ .`,
118
+ 'second',
119
+ ]}
120
+ onExecute={() => {
121
+ /* needed to turn on history movements */
122
+ }}
123
+ />,
124
+ );
125
+
126
+ await editorPage.getEditor().press(metaUp);
127
+ await expect(page.getByText('multiline')).toBeVisible();
128
+
129
+ // Single meta up moves all the way to top of editor on mac
130
+ if (isMac) {
131
+ await editorPage.getEditor().press(metaUp);
132
+ } else {
133
+ await editorPage.getEditor().press('ArrowUp');
134
+ await editorPage.getEditor().press('ArrowUp');
135
+ await editorPage.getEditor().press('ArrowUp');
136
+ await editorPage.getEditor().press('ArrowUp');
137
+ }
138
+ // move in history
139
+ await editorPage.getEditor().press(metaUp);
140
+ await expect(page.getByText('second')).toBeVisible();
141
+
142
+ await editorPage.getEditor().press(metaDown);
143
+ await expect(page.getByText('multiline')).toBeVisible();
144
+ });