@neo4j-cypher/react-codemirror 2.0.0-next.3 → 2.0.0-next.31

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 (163) hide show
  1. package/CHANGELOG.md +265 -0
  2. package/README.md +3 -2
  3. package/dist/{types → src}/CypherEditor.d.ts +81 -3
  4. package/dist/src/CypherEditor.js +336 -0
  5. package/dist/src/CypherEditor.js.map +1 -0
  6. package/dist/src/CypherEditor.test.js +154 -0
  7. package/dist/src/CypherEditor.test.js.map +1 -0
  8. package/dist/src/constants.d.ts +1 -0
  9. package/dist/src/constants.js +2 -0
  10. package/dist/src/constants.js.map +1 -0
  11. package/dist/src/e2e_tests/autoCompletion.spec.js +332 -0
  12. package/dist/src/e2e_tests/autoCompletion.spec.js.map +1 -0
  13. package/dist/src/e2e_tests/configuration.spec.js +83 -0
  14. package/dist/src/e2e_tests/configuration.spec.js.map +1 -0
  15. package/dist/src/e2e_tests/debounce.spec.js +66 -0
  16. package/dist/src/e2e_tests/debounce.spec.js.map +1 -0
  17. package/dist/{types/e2e_tests/e2e-utils.d.ts → src/e2e_tests/e2eUtils.d.ts} +2 -0
  18. package/dist/src/e2e_tests/e2eUtils.js +79 -0
  19. package/dist/src/e2e_tests/e2eUtils.js.map +1 -0
  20. package/dist/src/e2e_tests/extraKeybindings.spec.js +43 -0
  21. package/dist/src/e2e_tests/extraKeybindings.spec.js.map +1 -0
  22. package/dist/src/e2e_tests/historyNavigation.spec.js +227 -0
  23. package/dist/src/e2e_tests/historyNavigation.spec.js.map +1 -0
  24. package/dist/src/e2e_tests/performanceTest.spec.d.ts +6 -0
  25. package/dist/src/e2e_tests/performanceTest.spec.js +97 -0
  26. package/dist/src/e2e_tests/performanceTest.spec.js.map +1 -0
  27. package/dist/src/e2e_tests/sanityChecks.spec.js +53 -0
  28. package/dist/src/e2e_tests/sanityChecks.spec.js.map +1 -0
  29. package/dist/src/e2e_tests/signatureHelp.spec.js +228 -0
  30. package/dist/src/e2e_tests/signatureHelp.spec.js.map +1 -0
  31. package/dist/src/e2e_tests/snippets.spec.js +62 -0
  32. package/dist/src/e2e_tests/snippets.spec.js.map +1 -0
  33. package/dist/src/e2e_tests/syntaxHighlighting.spec.d.ts +1 -0
  34. package/dist/src/e2e_tests/syntaxHighlighting.spec.js +90 -0
  35. package/dist/src/e2e_tests/syntaxHighlighting.spec.js.map +1 -0
  36. package/dist/src/e2e_tests/syntaxValidation.spec.d.ts +1 -0
  37. package/dist/src/e2e_tests/syntaxValidation.spec.js +126 -0
  38. package/dist/src/e2e_tests/syntaxValidation.spec.js.map +1 -0
  39. package/dist/src/historyNavigation.js +163 -0
  40. package/dist/src/historyNavigation.js.map +1 -0
  41. package/dist/{types → src}/icons.d.ts +1 -1
  42. package/dist/src/icons.js +62 -0
  43. package/dist/src/icons.js.map +1 -0
  44. package/dist/src/index.d.ts +4 -0
  45. package/dist/src/index.js +5 -0
  46. package/dist/src/index.js.map +1 -0
  47. package/dist/src/lang-cypher/autocomplete.d.ts +6 -0
  48. package/dist/src/lang-cypher/autocomplete.js +113 -0
  49. package/dist/src/lang-cypher/autocomplete.js.map +1 -0
  50. package/dist/{types → src}/lang-cypher/constants.d.ts +11 -0
  51. package/dist/src/lang-cypher/constants.js +69 -0
  52. package/dist/src/lang-cypher/constants.js.map +1 -0
  53. package/dist/src/lang-cypher/contants.test.d.ts +1 -0
  54. package/dist/src/lang-cypher/contants.test.js +103 -0
  55. package/dist/src/lang-cypher/contants.test.js.map +1 -0
  56. package/dist/src/lang-cypher/createCypherTheme.js +183 -0
  57. package/dist/src/lang-cypher/createCypherTheme.js.map +1 -0
  58. package/dist/src/lang-cypher/langCypher.d.ts +13 -0
  59. package/dist/src/lang-cypher/langCypher.js +23 -0
  60. package/dist/src/lang-cypher/langCypher.js.map +1 -0
  61. package/dist/src/lang-cypher/lintWorker.mjs +2022 -0
  62. package/dist/src/lang-cypher/parser-adapter.d.ts +19 -0
  63. package/dist/src/lang-cypher/parser-adapter.js +113 -0
  64. package/dist/src/lang-cypher/parser-adapter.js.map +1 -0
  65. package/dist/src/lang-cypher/signatureHelp.d.ts +4 -0
  66. package/dist/src/lang-cypher/signatureHelp.js +109 -0
  67. package/dist/src/lang-cypher/signatureHelp.js.map +1 -0
  68. package/dist/{types/lang-cypher/syntax-validation.d.ts → src/lang-cypher/syntaxValidation.d.ts} +2 -1
  69. package/dist/src/lang-cypher/syntaxValidation.js +57 -0
  70. package/dist/src/lang-cypher/syntaxValidation.js.map +1 -0
  71. package/dist/src/lang-cypher/themeIcons.js +22 -0
  72. package/dist/src/lang-cypher/themeIcons.js.map +1 -0
  73. package/dist/src/lang-cypher/utils.d.ts +2 -0
  74. package/dist/src/lang-cypher/utils.js +10 -0
  75. package/dist/src/lang-cypher/utils.js.map +1 -0
  76. package/dist/src/ndlTokensCopy.d.ts +570 -0
  77. package/dist/src/ndlTokensCopy.js +571 -0
  78. package/dist/src/ndlTokensCopy.js.map +1 -0
  79. package/dist/src/ndlTokensCopy.test.d.ts +1 -0
  80. package/dist/src/ndlTokensCopy.test.js +12 -0
  81. package/dist/src/ndlTokensCopy.test.js.map +1 -0
  82. package/dist/src/neo4jSetup.d.ts +6 -0
  83. package/dist/src/neo4jSetup.js +120 -0
  84. package/dist/src/neo4jSetup.js.map +1 -0
  85. package/dist/src/richClipboardCopier.d.ts +4 -0
  86. package/dist/src/richClipboardCopier.js +78 -0
  87. package/dist/src/richClipboardCopier.js.map +1 -0
  88. package/dist/src/richClipboardCopier.test.d.ts +1 -0
  89. package/dist/src/richClipboardCopier.test.js +53 -0
  90. package/dist/src/richClipboardCopier.test.js.map +1 -0
  91. package/dist/{types → src}/themes.d.ts +1 -1
  92. package/dist/src/themes.js +93 -0
  93. package/dist/src/themes.js.map +1 -0
  94. package/dist/tsconfig.tsbuildinfo +1 -0
  95. package/package.json +43 -41
  96. package/src/CypherEditor.test.tsx +204 -0
  97. package/src/CypherEditor.tsx +316 -42
  98. package/src/constants.ts +1 -0
  99. package/src/e2e_tests/autoCompletion.spec.tsx +571 -0
  100. package/src/e2e_tests/configuration.spec.tsx +111 -0
  101. package/src/e2e_tests/debounce.spec.tsx +106 -0
  102. package/src/e2e_tests/{e2e-utils.ts → e2eUtils.ts} +41 -3
  103. package/src/e2e_tests/{extra-keybindings.spec.tsx → extraKeybindings.spec.tsx} +1 -3
  104. package/src/e2e_tests/{history-navigation.spec.tsx → historyNavigation.spec.tsx} +137 -18
  105. package/src/e2e_tests/performanceTest.spec.tsx +163 -0
  106. package/src/e2e_tests/{sanity-checks.spec.tsx → sanityChecks.spec.tsx} +7 -22
  107. package/src/e2e_tests/signatureHelp.spec.tsx +444 -0
  108. package/src/e2e_tests/snippets.spec.tsx +92 -0
  109. package/src/e2e_tests/{syntax-highlighting.spec.tsx → syntaxHighlighting.spec.tsx} +26 -24
  110. package/src/e2e_tests/syntaxValidation.spec.tsx +259 -0
  111. package/src/{history-navigation.ts → historyNavigation.ts} +1 -1
  112. package/src/icons.ts +3 -0
  113. package/src/index.ts +2 -2
  114. package/src/lang-cypher/autocomplete.ts +99 -18
  115. package/src/lang-cypher/constants.ts +27 -0
  116. package/src/lang-cypher/contants.test.ts +6 -2
  117. package/src/lang-cypher/{create-cypher-theme.ts → createCypherTheme.ts} +45 -2
  118. package/src/lang-cypher/langCypher.ts +42 -0
  119. package/src/lang-cypher/lintWorker.mjs +2022 -0
  120. package/src/lang-cypher/parser-adapter.ts +145 -0
  121. package/src/lang-cypher/signatureHelp.ts +151 -0
  122. package/src/lang-cypher/syntaxValidation.ts +72 -0
  123. package/src/lang-cypher/utils.ts +9 -0
  124. package/src/{ndl-tokens-copy.test.ts → ndlTokensCopy.test.ts} +2 -1
  125. package/src/ndlTokensCopy.ts +570 -0
  126. package/src/{neo4j-setup.tsx → neo4jSetup.tsx} +78 -17
  127. package/src/richClipboardCopier.test.ts +65 -0
  128. package/src/richClipboardCopier.ts +99 -0
  129. package/src/themes.ts +45 -70
  130. package/src/viteEnv.d.ts +1 -0
  131. package/dist/cjs/index.cjs +0 -1440
  132. package/dist/cjs/index.cjs.map +0 -7
  133. package/dist/esm/index.mjs +0 -1463
  134. package/dist/esm/index.mjs.map +0 -7
  135. package/dist/types/e2e_tests/mock-data.d.ts +0 -3779
  136. package/dist/types/index.d.ts +0 -4
  137. package/dist/types/lang-cypher/ParserAdapter.d.ts +0 -14
  138. package/dist/types/lang-cypher/autocomplete.d.ts +0 -3
  139. package/dist/types/lang-cypher/lang-cypher.d.ts +0 -7
  140. package/dist/types/ndl-tokens-copy.d.ts +0 -379
  141. package/dist/types/neo4j-setup.d.ts +0 -2
  142. package/dist/types/tsconfig.tsbuildinfo +0 -1
  143. package/src/e2e_tests/auto-completion.spec.tsx +0 -232
  144. package/src/e2e_tests/mock-data.ts +0 -4310
  145. package/src/e2e_tests/performance-test.spec.tsx +0 -71
  146. package/src/e2e_tests/syntax-validation.spec.tsx +0 -156
  147. package/src/lang-cypher/ParserAdapter.ts +0 -92
  148. package/src/lang-cypher/lang-cypher.ts +0 -32
  149. package/src/lang-cypher/syntax-validation.ts +0 -24
  150. package/src/ndl-tokens-copy.ts +0 -379
  151. /package/dist/{types/e2e_tests/auto-completion.spec.d.ts → src/CypherEditor.test.d.ts} +0 -0
  152. /package/dist/{types/e2e_tests/extra-keybindings.spec.d.ts → src/e2e_tests/autoCompletion.spec.d.ts} +0 -0
  153. /package/dist/{types/e2e_tests/history-navigation.spec.d.ts → src/e2e_tests/configuration.spec.d.ts} +0 -0
  154. /package/dist/{types/e2e_tests/performance-test.spec.d.ts → src/e2e_tests/debounce.spec.d.ts} +0 -0
  155. /package/dist/{types/e2e_tests/sanity-checks.spec.d.ts → src/e2e_tests/extraKeybindings.spec.d.ts} +0 -0
  156. /package/dist/{types/e2e_tests/syntax-highlighting.spec.d.ts → src/e2e_tests/historyNavigation.spec.d.ts} +0 -0
  157. /package/dist/{types/e2e_tests/syntax-validation.spec.d.ts → src/e2e_tests/sanityChecks.spec.d.ts} +0 -0
  158. /package/dist/{types/lang-cypher/contants.test.d.ts → src/e2e_tests/signatureHelp.spec.d.ts} +0 -0
  159. /package/dist/{types/ndl-tokens-copy.test.d.ts → src/e2e_tests/snippets.spec.d.ts} +0 -0
  160. /package/dist/{types/history-navigation.d.ts → src/historyNavigation.d.ts} +0 -0
  161. /package/dist/{types/lang-cypher/create-cypher-theme.d.ts → src/lang-cypher/createCypherTheme.d.ts} +0 -0
  162. /package/dist/{types/lang-cypher/theme-icons.d.ts → src/lang-cypher/themeIcons.d.ts} +0 -0
  163. /package/src/lang-cypher/{theme-icons.ts → themeIcons.ts} +0 -0
@@ -0,0 +1,106 @@
1
+ import { expect, test } from '@playwright/experimental-ct-react';
2
+ import { DEBOUNCE_TIME } from '../constants';
3
+ import { CypherEditor } from '../CypherEditor';
4
+ import { CypherEditorPage } from './e2eUtils';
5
+
6
+ const DEBOUNCE_TIME_WITH_MARGIN = DEBOUNCE_TIME + 100;
7
+ // value updates from outside onExecute are overwritten by pending updates
8
+ test.fail(
9
+ 'external updates should override debounced updates',
10
+ async ({ mount, page }) => {
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
+ };
18
+
19
+ const component = await mount(
20
+ <CypherEditor value={value} onChange={onChange} />,
21
+ );
22
+
23
+ await editorPage.getEditor().pressSequentially('RETURN 1');
24
+ onChange('foo');
25
+ await page.waitForTimeout(DEBOUNCE_TIME_WITH_MARGIN);
26
+ await expect(component).toContainText('foo');
27
+ },
28
+ );
29
+
30
+ // TODO Fix this test
31
+ test.fixme(
32
+ 'onExecute updates should override debounce updates',
33
+ async ({ mount, page }) => {
34
+ const editorPage = new CypherEditorPage(page);
35
+ let value = '';
36
+
37
+ const onExecute = () => {
38
+ value = '';
39
+ void component.update(
40
+ <CypherEditor
41
+ value={value}
42
+ onChange={onChange}
43
+ onExecute={onExecute}
44
+ />,
45
+ );
46
+ };
47
+
48
+ const onChange = (val: string) => {
49
+ value = val;
50
+ void component.update(
51
+ <CypherEditor value={val} onChange={onChange} onExecute={onExecute} />,
52
+ );
53
+ };
54
+
55
+ const component = await mount(
56
+ <CypherEditor value={value} onChange={onChange} onExecute={onExecute} />,
57
+ );
58
+
59
+ await editorPage.getEditor().pressSequentially('RETURN 1');
60
+ await editorPage.getEditor().press('Enter');
61
+ await page.waitForTimeout(DEBOUNCE_TIME_WITH_MARGIN);
62
+ await expect(component).not.toContainText('RETURN 1');
63
+
64
+ await editorPage.getEditor().pressSequentially('RETURN 1');
65
+ await editorPage.getEditor().pressSequentially('');
66
+ await editorPage.getEditor().pressSequentially('RETURN 1');
67
+ await editorPage.getEditor().press('Enter');
68
+ await page.waitForTimeout(DEBOUNCE_TIME_WITH_MARGIN);
69
+ await expect(component).not.toContainText('RETURN 1');
70
+ },
71
+ );
72
+
73
+ test('onExecute should fire after debounced updates', async ({
74
+ mount,
75
+ page,
76
+ }) => {
77
+ const editorPage = new CypherEditorPage(page);
78
+ let value = '';
79
+ let executedCommand = '';
80
+
81
+ const onExecute = (cmd: string) => {
82
+ executedCommand = cmd;
83
+ void component.update(
84
+ <CypherEditor value={value} onChange={onChange} onExecute={onExecute} />,
85
+ );
86
+ };
87
+
88
+ const onChange = (val: string) => {
89
+ value = val;
90
+ void component.update(
91
+ <CypherEditor value={val} onChange={onChange} onExecute={onExecute} />,
92
+ );
93
+ };
94
+
95
+ const component = await mount(
96
+ <CypherEditor value={value} onChange={onChange} onExecute={onExecute} />,
97
+ );
98
+
99
+ await editorPage.getEditor().fill('RETURN 1');
100
+ await editorPage.getEditor().press('Enter');
101
+ await editorPage.getEditor().fill('RETURN 2');
102
+ await editorPage.getEditor().press('Enter');
103
+ await page.waitForTimeout(DEBOUNCE_TIME_WITH_MARGIN);
104
+ await expect(component).toContainText('RETURN 2');
105
+ expect(executedCommand).toBe('RETURN 2');
106
+ });
@@ -59,17 +59,55 @@ export class CypherEditorPage {
59
59
  return this.checkNotificationMessage('warning', queryChunk, expectedMsg);
60
60
  }
61
61
 
62
+ async checkNoNotificationMessage(type: 'error' | 'warning') {
63
+ await this.page.waitForTimeout(10000);
64
+ await expect(this.page.locator('.cm-lintRange-' + type)).toHaveCount(0, {
65
+ timeout: 10000,
66
+ });
67
+ await expect(this.page.locator('.cm-lintPoint-' + type)).toHaveCount(0, {
68
+ timeout: 10000,
69
+ });
70
+ }
71
+
62
72
  private async checkNotificationMessage(
63
73
  type: 'error' | 'warning',
64
74
  queryChunk: string,
65
75
  expectedMsg: string,
66
76
  ) {
67
77
  await expect(this.page.locator('.cm-lintRange-' + type).last()).toBeVisible(
68
- { timeout: 2000 },
78
+ { timeout: 10000 },
69
79
  );
70
80
 
71
81
  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();
82
+ await expect(this.page.locator('.cm-tooltip-hover').last()).toBeVisible({
83
+ timeout: 10000,
84
+ });
85
+ await this.checkHoverMessage(expectedMsg, type);
86
+ /* Return the mouse to the beginning of the query and
87
+ This is because if for example we have an overlay with a
88
+ first interaction that covers the element we want to perform
89
+ the second interaction on, we won't be able to see that second element
90
+ */
91
+ await this.page.mouse.move(0, 0);
92
+ // Make sure the tooltip closed
93
+ await expect(
94
+ this.page.locator('.cm-tooltip-hover').last(),
95
+ ).not.toBeVisible();
96
+ }
97
+
98
+ private async checkHoverMessage(
99
+ expectedMsg: string,
100
+ type: 'error' | 'warning',
101
+ ) {
102
+ const locator =
103
+ type === 'error'
104
+ ? 'li.cm-diagnostic.cm-diagnostic-error'
105
+ : 'li.cm-diagnostic.cm-diagnostic-warning';
106
+ const tooltips = await this.page.locator(locator).all();
107
+
108
+ const tooltipTexts = await Promise.all(
109
+ tooltips.map((t) => t.textContent()),
110
+ );
111
+ expect(tooltipTexts).toContain(expectedMsg);
74
112
  }
75
113
  }
@@ -1,8 +1,6 @@
1
1
  import { expect, test } from '@playwright/experimental-ct-react';
2
2
  import { CypherEditor } from '../CypherEditor';
3
- import { CypherEditorPage } from './e2e-utils';
4
-
5
- test.use({ viewport: { width: 500, height: 500 } });
3
+ import { CypherEditorPage } from './e2eUtils';
6
4
 
7
5
  test('can add extra keybinding statically', async ({ mount, page }) => {
8
6
  const editorPage = new CypherEditorPage(page);
@@ -1,8 +1,6 @@
1
1
  import { expect, test } from '@playwright/experimental-ct-react';
2
2
  import { CypherEditor } from '../CypherEditor';
3
- import { CypherEditorPage } from './e2e-utils';
4
-
5
- test.use({ viewport: { width: 500, height: 500 } });
3
+ import { CypherEditorPage } from './e2eUtils';
6
4
 
7
5
  test('respects preloaded history', async ({ page, mount }) => {
8
6
  const editorPage = new CypherEditorPage(page);
@@ -27,13 +25,75 @@ test('respects preloaded history', async ({ page, mount }) => {
27
25
  await editorPage.getEditor().press('ArrowUp');
28
26
  await expect(page.getByText('second')).toBeVisible();
29
27
 
28
+ // First arrow down is to get to end of line
29
+ await editorPage.getEditor().press('ArrowDown');
30
30
  await editorPage.getEditor().press('ArrowDown');
31
31
  await expect(page.getByText('first')).toBeVisible();
32
32
  await editorPage.getEditor().press('ArrowDown');
33
33
  await expect(page.getByText(initialValue)).toBeVisible();
34
34
  });
35
35
 
36
- test('can execute queries and see them in history', async ({ page, mount }) => {
36
+ test('can add new lines without onExecute', async ({ page, mount }) => {
37
+ const editorPage = new CypherEditorPage(page);
38
+
39
+ const editorComponent = await mount(<CypherEditor />);
40
+
41
+ // Ctrl-Enter does nothing when onExecute is false
42
+ await editorPage.getEditor().press('Control+Enter');
43
+ await expect(editorComponent).toHaveText('1\n', {
44
+ useInnerText: true,
45
+ });
46
+
47
+ // Enter adds new lines
48
+ await editorPage.getEditor().fill('Brock');
49
+ await editorPage.getEditor().press('Enter');
50
+ await editorPage.getEditor().press('Enter');
51
+ await expect(editorComponent).toHaveText('1\n2\n3\nBrock', {
52
+ useInnerText: true,
53
+ });
54
+
55
+ // Shift-Enter adds new lines
56
+ await editorPage.getEditor().press('Shift+Enter');
57
+ await editorPage.getEditor().press('Shift+Enter');
58
+ await expect(editorComponent).toHaveText('1\n2\n3\n4\n5\nBrock', {
59
+ useInnerText: true,
60
+ });
61
+ });
62
+
63
+ test('can add new lines with newLineOnEnter and without onExecute', async ({
64
+ page,
65
+ mount,
66
+ }) => {
67
+ const editorPage = new CypherEditorPage(page);
68
+
69
+ const editorComponent = await mount(<CypherEditor newLineOnEnter />);
70
+
71
+ // Ctrl-Enter does nothing when onExecute is false
72
+ await editorPage.getEditor().press('Control+Enter');
73
+ await expect(editorComponent).toHaveText('1\n', {
74
+ useInnerText: true,
75
+ });
76
+
77
+ // Enter adds new lines
78
+ await editorPage.getEditor().fill('Brock');
79
+ await editorPage.getEditor().press('Enter');
80
+ await editorPage.getEditor().press('Enter');
81
+ await expect(editorComponent).toHaveText('1\n2\n3\nBrock', {
82
+ useInnerText: true,
83
+ });
84
+
85
+ // Shift-Enter adds new lines
86
+ await editorPage.getEditor().press('Shift+Enter');
87
+ await editorPage.getEditor().press('Shift+Enter');
88
+ await expect(editorComponent).toHaveText('1\n2\n3\n4\n5\nBrock', {
89
+ useInnerText: true,
90
+ });
91
+ });
92
+
93
+ test('can execute queries and see them in history with newLineOnEnter', async ({
94
+ page,
95
+ mount,
96
+ }) => {
37
97
  const editorPage = new CypherEditorPage(page);
38
98
 
39
99
  const initialValue = `MATCH (n)
@@ -49,6 +109,7 @@ RETURN n;`;
49
109
  value={initialValue}
50
110
  history={history}
51
111
  onExecute={onExecute}
112
+ newLineOnEnter
52
113
  />,
53
114
  );
54
115
 
@@ -63,12 +124,12 @@ RETURN n;`;
63
124
  await editorPage.getEditor().press('Meta+Enter');
64
125
  expect(history.length).toBe(1);
65
126
 
66
- // Ensure only enter doesn't execute query
127
+ // Ensure cmd+enter is required in multiline
67
128
  await editorPage.getEditor().fill('multiline');
68
129
  await editorPage.getEditor().press('Enter');
69
130
  await editorPage.getEditor().press('Enter');
70
131
  await editorPage.getEditor().press('Enter');
71
- await editorPage.getEditor().press('Enter');
132
+ await editorPage.getEditor().press('Shift+Enter');
72
133
  await page.keyboard.type('entry');
73
134
  expect(history.length).toBe(1);
74
135
 
@@ -102,10 +163,15 @@ RETURN n;`;
102
163
  await expect(page.getByText('multiline')).toBeVisible();
103
164
 
104
165
  // arrow movements don't matter until bottom is hit
166
+ await editorPage.getEditor().press('ArrowDown');
167
+ await editorPage.getEditor().press('ArrowDown');
105
168
  await editorPage.getEditor().press('ArrowUp');
106
169
  await editorPage.getEditor().press('ArrowUp');
107
170
  await editorPage.getEditor().press('ArrowDown');
108
171
  await editorPage.getEditor().press('ArrowDown');
172
+ await editorPage.getEditor().press('ArrowDown');
173
+ await editorPage.getEditor().press('ArrowDown');
174
+ await editorPage.getEditor().press('ArrowDown');
109
175
 
110
176
  // editor still multiline
111
177
  await expect(page.getByText('multiline')).toBeVisible();
@@ -115,6 +181,59 @@ RETURN n;`;
115
181
  await expect(page.getByText('draft')).toBeVisible();
116
182
  });
117
183
 
184
+ test('can execute queries without newLineOnEnter', async ({ page, mount }) => {
185
+ const editorPage = new CypherEditorPage(page);
186
+
187
+ const initialValue = 'Brock';
188
+
189
+ const history: string[] = [];
190
+ const onExecute = (cmd: string) => {
191
+ history.unshift(cmd);
192
+ };
193
+
194
+ const editorComponent = await mount(
195
+ <CypherEditor value={initialValue} onExecute={onExecute} />,
196
+ );
197
+
198
+ // Cmd/Control still executes initial query
199
+ await editorPage.getEditor().press('Control+Enter');
200
+ await editorPage.getEditor().press('Meta+Enter');
201
+ expect(history.length).toBe(1);
202
+
203
+ // Ensure query execution doesn't fire if the query is only whitespace
204
+ await editorPage.getEditor().fill(' ');
205
+ await editorPage.getEditor().press('Control+Enter');
206
+ await editorPage.getEditor().press('Meta+Enter');
207
+ await editorPage.getEditor().press('Enter');
208
+ expect(history.length).toBe(1);
209
+
210
+ // Ensure enter executes query when in single line
211
+ await editorPage.getEditor().fill('Misty');
212
+ await editorPage.getEditor().press('Enter');
213
+ expect(history.length).toBe(2);
214
+ expect(history).toEqual(['Misty', 'Brock']);
215
+
216
+ // Ensure cmd+enter is required in multiline
217
+ await editorPage.getEditor().fill('multiline');
218
+ await editorPage.getEditor().press('Shift+Enter');
219
+ await editorPage.getEditor().press('Enter');
220
+ await editorPage.getEditor().press('A');
221
+
222
+ // line numbers and the text
223
+ await expect(editorComponent).toHaveText('1\n2\n3\nmultiline\nA', {
224
+ useInnerText: true,
225
+ });
226
+ await editorPage.getEditor().press('Enter');
227
+ await editorPage.getEditor().press('Enter');
228
+ await editorPage.getEditor().press('Enter');
229
+ await editorPage.getEditor().press('Enter');
230
+ expect(history.length).toBe(2);
231
+
232
+ await editorPage.getEditor().press('Control+Enter');
233
+ await editorPage.getEditor().press('Meta+Enter');
234
+ expect(history.length).toBe(3);
235
+ });
236
+
118
237
  test('can navigate with cmd+up as well', async ({ page, mount }) => {
119
238
  const editorPage = new CypherEditorPage(page);
120
239
  const isMac = process.platform === 'darwin';
@@ -127,11 +246,11 @@ test('can navigate with cmd+up as well', async ({ page, mount }) => {
127
246
  <CypherEditor
128
247
  value={initialValue}
129
248
  history={[
249
+ 'first',
130
250
  `one
131
251
  multiline
132
252
  entry
133
253
  .`,
134
- 'second',
135
254
  ]}
136
255
  onExecute={() => {
137
256
  /* needed to turn on history movements */
@@ -139,24 +258,24 @@ entry
139
258
  />,
140
259
  );
141
260
 
261
+ await editorPage.getEditor().press(metaUp);
262
+ await expect(page.getByText('first')).toBeVisible();
263
+
264
+ // move in history
142
265
  await editorPage.getEditor().press(metaUp);
143
266
  await expect(page.getByText('multiline')).toBeVisible();
144
267
 
145
- // Single meta up moves all the way to top of editor on mac
268
+ // Single meta down moves all the way to top of editor on mac
146
269
  if (isMac) {
147
- await editorPage.getEditor().press(metaUp);
270
+ await editorPage.getEditor().press(metaDown);
148
271
  } else {
149
- await editorPage.getEditor().press('ArrowUp');
150
- await editorPage.getEditor().press('ArrowUp');
151
- await editorPage.getEditor().press('ArrowUp');
152
- await editorPage.getEditor().press('ArrowUp');
272
+ await editorPage.getEditor().press('ArrowDown');
273
+ await editorPage.getEditor().press('ArrowDown');
274
+ await editorPage.getEditor().press('ArrowDown');
275
+ await editorPage.getEditor().press('ArrowDown');
153
276
  }
154
- // move in history
155
- await editorPage.getEditor().press(metaUp);
156
- await expect(page.getByText('second')).toBeVisible();
157
-
158
277
  await editorPage.getEditor().press(metaDown);
159
- await expect(page.getByText('multiline')).toBeVisible();
278
+ await expect(page.getByText('first')).toBeVisible();
160
279
  });
161
280
 
162
281
  test('test onExecute', async ({ page, mount }) => {
@@ -0,0 +1,163 @@
1
+ import { testData } from '@neo4j-cypher/language-support';
2
+ import { expect, test } from '@playwright/experimental-ct-react';
3
+ import { CypherEditor } from '../CypherEditor';
4
+ import { CypherEditorPage } from './e2eUtils';
5
+
6
+ test.use({ viewport: { width: 1000, height: 500 } });
7
+ declare global {
8
+ interface Window {
9
+ longtasks: number[];
10
+ }
11
+ }
12
+
13
+ test('benchmarking & performance test session', async ({
14
+ browserName,
15
+ mount,
16
+ page,
17
+ }) => {
18
+ test.skip(browserName !== 'chromium');
19
+ const client = await page.context().newCDPSession(page);
20
+ if (process.env.BENCHMARKING === 'true') {
21
+ test.setTimeout(1000000);
22
+ await client.send('Performance.enable');
23
+ await client.send('Emulation.setCPUThrottlingRate', { rate: 4 });
24
+ await client.send('Overlay.setShowFPSCounter', { show: true });
25
+
26
+ await page.evaluate(() => {
27
+ window.longtasks = [];
28
+ const observer = new PerformanceObserver((list) => {
29
+ window.longtasks.push(...list.getEntries().map((e) => e.duration));
30
+ });
31
+
32
+ observer.observe({ entryTypes: ['longtask'] });
33
+ });
34
+ } else {
35
+ test.setTimeout(30 * 1000);
36
+ }
37
+ const editorPage = new CypherEditorPage(page);
38
+ const component = await mount(
39
+ <CypherEditor
40
+ prompt="neo4j>"
41
+ theme="dark"
42
+ lint
43
+ schema={testData.mockSchema}
44
+ />,
45
+ );
46
+
47
+ // pressSequentially is less efficient -> we want to test the performance of the editor
48
+ await editorPage.getEditor().pressSequentially(`
49
+ MATCH (n:Person) RETURN m;`);
50
+
51
+ await editorPage.checkErrorMessage('m', 'Variable `m` not defined');
52
+
53
+ // set and unset large query a few times
54
+ await component.update(
55
+ <CypherEditor value={testData.largeQuery} schema={testData.mockSchema} />,
56
+ );
57
+ await component.update(
58
+ <CypherEditor value="" schema={testData.mockSchema} />,
59
+ );
60
+
61
+ await component.update(
62
+ <CypherEditor value={testData.largeQuery} schema={testData.mockSchema} />,
63
+ );
64
+ await component.update(<CypherEditor value="" />);
65
+
66
+ await component.update(
67
+ <CypherEditor value={testData.largeQuery} schema={testData.mockSchema} />,
68
+ );
69
+ await component.update(
70
+ <CypherEditor value="" schema={testData.mockSchema} />,
71
+ );
72
+
73
+ await component.update(
74
+ <CypherEditor value={testData.largeQuery} schema={testData.mockSchema} />,
75
+ );
76
+ await component.update(
77
+ <CypherEditor value="" schema={testData.mockSchema} />,
78
+ );
79
+
80
+ await component.update(
81
+ <CypherEditor value={testData.largeQuery} schema={testData.mockSchema} />,
82
+ );
83
+
84
+ await editorPage.getEditor().pressSequentially(`
85
+ MATCH (n:P`);
86
+
87
+ await expect(
88
+ page.locator('.cm-tooltip-autocomplete').getByText('Person'),
89
+ ).toBeVisible();
90
+
91
+ await page.locator('.cm-tooltip-autocomplete').getByText('Person').click();
92
+
93
+ await expect(page.locator('.cm-tooltip-autocomplete')).not.toBeVisible();
94
+
95
+ await expect(component).toContainText('MATCH (n:Person');
96
+
97
+ await editorPage.getEditor().pressSequentially(') RETRN my');
98
+
99
+ await expect(component).toContainText('MATCH (n:Person) RETRN m');
100
+
101
+ await editorPage.checkErrorMessage(
102
+ 'RETRN',
103
+ `Invalid input 'RETRN': expected a graph pattern, ',', 'ORDER BY', 'CALL', 'CREATE', 'LOAD CSV', 'DELETE', 'DETACH', 'FINISH', 'FOREACH', 'INSERT', 'LIMIT', 'MATCH', 'MERGE', 'NODETACH', 'OFFSET', 'OPTIONAL', 'REMOVE', 'RETURN', 'SET', 'SKIP', 'UNION', 'UNWIND', 'USE', 'USING', 'WHERE', 'WITH' or <EOF>`,
104
+ );
105
+
106
+ await editorPage
107
+ .getEditor()
108
+ .pressSequentially('veryveryveryverylongvariable');
109
+
110
+ if (process.env.BENCHMARKING === 'true') {
111
+ const longtasks = await page.evaluate(() => window.longtasks);
112
+ const sortedLongTasks = longtasks.sort((a, b) => a - b);
113
+ const medianLongTask =
114
+ sortedLongTasks[Math.floor(sortedLongTasks.length / 2)];
115
+ const averageLongTask =
116
+ sortedLongTasks.reduce((a, b) => a + b, 0) / sortedLongTasks.length;
117
+ const over500 = sortedLongTasks.filter((t) => t > 500).length;
118
+ const nintyninethPercentile =
119
+ sortedLongTasks[Math.floor(sortedLongTasks.length * 0.99)];
120
+ const longTaskCount = longtasks.length;
121
+ const totalLongTaskTime = longtasks.reduce((a, b) => a + b, 0);
122
+
123
+ const USER_ID = 1226722;
124
+ const API_KEY = process.env.GRAFANA_API_KEY;
125
+ if (!API_KEY) {
126
+ throw new Error('Missing grafana api key');
127
+ }
128
+
129
+ const metrics = {
130
+ medianLongTask,
131
+ averageLongTask,
132
+ over500,
133
+ nintyninethPercentile,
134
+ longTaskCount,
135
+ totalLongTaskTime,
136
+ };
137
+ const body = Object.entries(metrics)
138
+ .map(
139
+ ([key, value]) =>
140
+ `benchmark,bar_label=${key},source=playwright metric=${value}`,
141
+ )
142
+ .join('\n');
143
+
144
+ await fetch(
145
+ 'https://influx-prod-39-prod-eu-north-0.grafana.net/api/v1/push/influx/write',
146
+ {
147
+ method: 'post',
148
+ body,
149
+ headers: {
150
+ Authorization: `Bearer ${USER_ID}:${API_KEY}`,
151
+ 'Content-Type': 'text/plain',
152
+ },
153
+ },
154
+ ).then((res) => {
155
+ if (res.ok) {
156
+ // eslint-disable-next-line no-console
157
+ console.log('Metrics pushed to grafana successfully');
158
+ } else {
159
+ throw new Error(`Failed to push metrics to grafana: ${res.statusText}`);
160
+ }
161
+ });
162
+ }
163
+ });
@@ -1,8 +1,6 @@
1
1
  import { expect, test } from '@playwright/experimental-ct-react';
2
2
  import { CypherEditor } from '../CypherEditor';
3
3
 
4
- test.use({ viewport: { width: 500, height: 500 } });
5
-
6
4
  test('can mount the editor with text', async ({ mount }) => {
7
5
  const component = await mount(<CypherEditor value="MATCH (n) RETURN n;" />);
8
6
 
@@ -22,7 +20,7 @@ test('the editors text can be externally controlled ', async ({ mount }) => {
22
20
  await expect(component).toContainText(newValue);
23
21
  });
24
22
 
25
- test('the editors can report changes to the text ', async ({ mount, page }) => {
23
+ test('the editor can report changes to the text ', async ({ mount, page }) => {
26
24
  const intitialValue = 'MATCH (n) ';
27
25
 
28
26
  let editorValueCopy = intitialValue;
@@ -34,13 +32,14 @@ test('the editors can report changes to the text ', async ({ mount, page }) => {
34
32
 
35
33
  const textField = page.getByRole('textbox');
36
34
 
35
+ await textField.fill('');
37
36
  await textField.fill('RETURN 12');
37
+ await expect(textField).toHaveText('RETURN 12');
38
38
 
39
- expect(editorValueCopy).toBe('RETURN 12');
40
-
41
- await page.keyboard.type('34');
42
-
43
- expect(editorValueCopy).toBe('RETURN 1234');
39
+ // editor update is debounced, retry wait for debounced
40
+ await expect(() => {
41
+ expect(editorValueCopy).toBe('RETURN 12');
42
+ }).toPass({ intervals: [300, 300, 1000] });
44
43
  });
45
44
 
46
45
  test('can complete RETURN', async ({ page, mount }) => {
@@ -71,17 +70,3 @@ test('can complete CALL/CREATE', async ({ page, mount }) => {
71
70
 
72
71
  await expect(textField).toHaveText('CALL');
73
72
  });
74
-
75
- test('prompt shows up', async ({ mount, page }) => {
76
- const component = await mount(<CypherEditor prompt="neo4j>" />);
77
-
78
- await expect(component).toContainText('neo4j>');
79
-
80
- await component.update(<CypherEditor prompt="test>" />);
81
- await expect(component).toContainText('test>');
82
-
83
- const textField = page.getByRole('textbox');
84
- await textField.press('a');
85
-
86
- await expect(textField).toHaveText('a');
87
- });