@neo4j-cypher/react-codemirror 2.0.0-alpha.0 → 2.0.0-canary-a1ed8f3

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 (122) hide show
  1. package/CHANGELOG.md +68 -0
  2. package/LICENSE.md +201 -0
  3. package/README.md +27 -4
  4. package/dist/CypherEditor.d.ts +153 -0
  5. package/dist/CypherEditor.js +242 -0
  6. package/dist/CypherEditor.js.map +1 -0
  7. package/dist/e2e_tests/autoCompletion.spec.d.ts +1 -0
  8. package/dist/e2e_tests/autoCompletion.spec.js +133 -0
  9. package/dist/e2e_tests/autoCompletion.spec.js.map +1 -0
  10. package/dist/e2e_tests/configuration.spec.d.ts +1 -0
  11. package/dist/e2e_tests/configuration.spec.js +73 -0
  12. package/dist/e2e_tests/configuration.spec.js.map +1 -0
  13. package/dist/e2e_tests/e2eUtils.d.ts +12 -0
  14. package/dist/e2e_tests/e2eUtils.js +60 -0
  15. package/dist/e2e_tests/e2eUtils.js.map +1 -0
  16. package/dist/e2e_tests/extraKeybindings.spec.d.ts +1 -0
  17. package/dist/e2e_tests/extraKeybindings.spec.js +44 -0
  18. package/dist/e2e_tests/extraKeybindings.spec.js.map +1 -0
  19. package/dist/e2e_tests/historyNavigation.spec.d.ts +1 -0
  20. package/dist/e2e_tests/historyNavigation.spec.js +136 -0
  21. package/dist/e2e_tests/historyNavigation.spec.js.map +1 -0
  22. package/dist/e2e_tests/performanceTest.spec.d.ts +6 -0
  23. package/dist/e2e_tests/performanceTest.spec.js +96 -0
  24. package/dist/e2e_tests/performanceTest.spec.js.map +1 -0
  25. package/dist/e2e_tests/sanityChecks.spec.d.ts +1 -0
  26. package/dist/e2e_tests/sanityChecks.spec.js +56 -0
  27. package/dist/e2e_tests/sanityChecks.spec.js.map +1 -0
  28. package/dist/e2e_tests/signatureHelp.spec.d.ts +1 -0
  29. package/dist/e2e_tests/signatureHelp.spec.js +152 -0
  30. package/dist/e2e_tests/signatureHelp.spec.js.map +1 -0
  31. package/dist/e2e_tests/snippets.spec.d.ts +1 -0
  32. package/dist/e2e_tests/snippets.spec.js +63 -0
  33. package/dist/e2e_tests/snippets.spec.js.map +1 -0
  34. package/dist/e2e_tests/syntaxHighlighting.spec.d.ts +1 -0
  35. package/dist/e2e_tests/syntaxHighlighting.spec.js +91 -0
  36. package/dist/e2e_tests/syntaxHighlighting.spec.js.map +1 -0
  37. package/dist/e2e_tests/syntaxValidation.spec.d.ts +1 -0
  38. package/dist/e2e_tests/syntaxValidation.spec.js +79 -0
  39. package/dist/e2e_tests/syntaxValidation.spec.js.map +1 -0
  40. package/dist/historyNavigation.d.ts +7 -0
  41. package/dist/historyNavigation.js +163 -0
  42. package/dist/historyNavigation.js.map +1 -0
  43. package/dist/icons.d.ts +2 -0
  44. package/dist/icons.js +62 -0
  45. package/dist/icons.js.map +1 -0
  46. package/dist/index.d.ts +4 -0
  47. package/dist/index.js +5 -0
  48. package/dist/index.js.map +1 -0
  49. package/dist/lang-cypher/autocomplete.d.ts +3 -0
  50. package/dist/lang-cypher/autocomplete.js +62 -0
  51. package/dist/lang-cypher/autocomplete.js.map +1 -0
  52. package/dist/lang-cypher/constants.d.ts +40 -0
  53. package/dist/lang-cypher/constants.js +65 -0
  54. package/dist/lang-cypher/constants.js.map +1 -0
  55. package/dist/lang-cypher/contants.test.d.ts +1 -0
  56. package/dist/lang-cypher/contants.test.js +102 -0
  57. package/dist/lang-cypher/contants.test.js.map +1 -0
  58. package/dist/lang-cypher/createCypherTheme.d.ts +26 -0
  59. package/dist/lang-cypher/createCypherTheme.js +172 -0
  60. package/dist/lang-cypher/createCypherTheme.js.map +1 -0
  61. package/dist/lang-cypher/langCypher.d.ts +9 -0
  62. package/dist/lang-cypher/langCypher.js +24 -0
  63. package/dist/lang-cypher/langCypher.js.map +1 -0
  64. package/dist/lang-cypher/lintWorker.d.ts +8 -0
  65. package/dist/lang-cypher/lintWorker.js +4 -0
  66. package/dist/lang-cypher/lintWorker.js.map +1 -0
  67. package/dist/lang-cypher/parser-adapter.d.ts +19 -0
  68. package/dist/lang-cypher/parser-adapter.js +113 -0
  69. package/dist/lang-cypher/parser-adapter.js.map +1 -0
  70. package/dist/lang-cypher/signatureHelp.d.ts +4 -0
  71. package/dist/lang-cypher/signatureHelp.js +93 -0
  72. package/dist/lang-cypher/signatureHelp.js.map +1 -0
  73. package/dist/lang-cypher/syntaxValidation.d.ts +5 -0
  74. package/dist/lang-cypher/syntaxValidation.js +71 -0
  75. package/dist/lang-cypher/syntaxValidation.js.map +1 -0
  76. package/dist/lang-cypher/themeIcons.d.ts +7 -0
  77. package/dist/lang-cypher/themeIcons.js +22 -0
  78. package/dist/lang-cypher/themeIcons.js.map +1 -0
  79. package/dist/ndlTokensCopy.d.ts +379 -0
  80. package/dist/ndlTokensCopy.js +380 -0
  81. package/dist/ndlTokensCopy.js.map +1 -0
  82. package/dist/ndlTokensCopy.test.d.ts +1 -0
  83. package/dist/ndlTokensCopy.test.js +11 -0
  84. package/dist/ndlTokensCopy.test.js.map +1 -0
  85. package/dist/neo4jSetup.d.ts +2 -0
  86. package/dist/neo4jSetup.js +120 -0
  87. package/dist/neo4jSetup.js.map +1 -0
  88. package/dist/themes.d.ts +11 -0
  89. package/dist/themes.js +114 -0
  90. package/dist/themes.js.map +1 -0
  91. package/dist/tsconfig.tsbuildinfo +1 -0
  92. package/package.json +46 -16
  93. package/src/CypherEditor.tsx +461 -0
  94. package/src/e2e_tests/autoCompletion.spec.tsx +236 -0
  95. package/src/e2e_tests/configuration.spec.tsx +97 -0
  96. package/src/e2e_tests/e2eUtils.ts +85 -0
  97. package/src/e2e_tests/extraKeybindings.spec.tsx +57 -0
  98. package/src/e2e_tests/historyNavigation.spec.tsx +196 -0
  99. package/src/e2e_tests/performanceTest.spec.tsx +158 -0
  100. package/src/e2e_tests/sanityChecks.spec.tsx +78 -0
  101. package/src/e2e_tests/signatureHelp.spec.tsx +309 -0
  102. package/src/e2e_tests/snippets.spec.tsx +94 -0
  103. package/src/e2e_tests/syntaxHighlighting.spec.tsx +198 -0
  104. package/src/e2e_tests/syntaxValidation.spec.tsx +156 -0
  105. package/src/historyNavigation.ts +191 -0
  106. package/{esm/index.mjs → src/icons.ts} +37 -1283
  107. package/src/index.ts +4 -0
  108. package/src/lang-cypher/autocomplete.ts +81 -0
  109. package/src/lang-cypher/constants.ts +84 -0
  110. package/src/lang-cypher/contants.test.ts +104 -0
  111. package/src/lang-cypher/createCypherTheme.ts +240 -0
  112. package/src/lang-cypher/langCypher.ts +41 -0
  113. package/src/lang-cypher/lintWorker.ts +14 -0
  114. package/src/lang-cypher/parser-adapter.ts +145 -0
  115. package/src/lang-cypher/signatureHelp.ts +131 -0
  116. package/src/lang-cypher/syntaxValidation.ts +99 -0
  117. package/src/lang-cypher/themeIcons.ts +27 -0
  118. package/src/ndlTokensCopy.test.ts +11 -0
  119. package/src/ndlTokensCopy.ts +379 -0
  120. package/src/neo4jSetup.tsx +179 -0
  121. package/src/themes.ts +132 -0
  122. package/dist/index.cjs +0 -1330
@@ -0,0 +1,97 @@
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('prompt shows up', async ({ mount, page }) => {
7
+ const component = await mount(<CypherEditor prompt="neo4j>" />);
8
+
9
+ await expect(component).toContainText('neo4j>');
10
+
11
+ await component.update(<CypherEditor prompt="test>" />);
12
+ await expect(component).toContainText('test>');
13
+
14
+ const textField = page.getByRole('textbox');
15
+ await textField.press('a');
16
+
17
+ await expect(textField).toHaveText('a');
18
+ });
19
+
20
+ test('line numbers can be turned on/off', async ({ mount }) => {
21
+ const component = await mount(<CypherEditor lineNumbers />);
22
+
23
+ await expect(component).toContainText('1');
24
+
25
+ await component.update(<CypherEditor lineNumbers={false} />);
26
+ await expect(component).not.toContainText('1');
27
+ });
28
+
29
+ test('can configure readonly', async ({ mount, page }) => {
30
+ const component = await mount(<CypherEditor readonly />);
31
+
32
+ const textField = page.getByRole('textbox');
33
+ await textField.press('a');
34
+ await expect(textField).not.toHaveText('a');
35
+
36
+ await component.update(<CypherEditor readonly={false} />);
37
+ await textField.press('b');
38
+ await expect(textField).toHaveText('b');
39
+ });
40
+
41
+ test('can set placeholder ', async ({ mount, page }) => {
42
+ const component = await mount(<CypherEditor placeholder="bulbasaur" />);
43
+
44
+ const textField = page.getByRole('textbox');
45
+ await expect(textField).toHaveText('bulbasaur');
46
+
47
+ await component.update(<CypherEditor placeholder="venusaur" />);
48
+ await expect(textField).not.toHaveText('bulbasaur');
49
+ await expect(textField).toHaveText('venusaur');
50
+
51
+ await textField.fill('abc');
52
+ await expect(textField).not.toHaveText('venusaur');
53
+ await expect(textField).toHaveText('abc');
54
+ });
55
+
56
+ test('can set/unset onFocus/onBlur', async ({ mount, page }) => {
57
+ const component = await mount(<CypherEditor />);
58
+
59
+ let focusFireCount = 0;
60
+ let blurFireCount = 0;
61
+
62
+ const focus = () => {
63
+ focusFireCount += 1;
64
+ };
65
+ const blur = () => {
66
+ blurFireCount += 1;
67
+ };
68
+
69
+ await component.update(<CypherEditor domEventHandlers={{ blur, focus }} />);
70
+
71
+ const textField = page.getByRole('textbox');
72
+ await textField.click();
73
+ await expect(textField).toBeFocused();
74
+
75
+ // this is to give the events time to fire
76
+ await expect(() => {
77
+ expect(focusFireCount).toBe(1);
78
+ expect(blurFireCount).toBe(0);
79
+ }).toPass();
80
+
81
+ await textField.blur();
82
+
83
+ await expect(() => {
84
+ expect(focusFireCount).toBe(1);
85
+ expect(blurFireCount).toBe(1);
86
+ }).toPass();
87
+
88
+ await component.update(<CypherEditor />);
89
+ await textField.click();
90
+ await expect(textField).toBeFocused();
91
+ await textField.blur();
92
+
93
+ await expect(() => {
94
+ expect(focusFireCount).toBe(1);
95
+ expect(blurFireCount).toBe(1);
96
+ }).toPass();
97
+ });
@@ -0,0 +1,85 @@
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: 3000 },
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
+ /* Return the mouse to the beginning of the query and
75
+ This is because if for example we have an overlay with a
76
+ first interaction that covers the element we want to perform
77
+ the second interaction on, we won't be able to see that second element
78
+ */
79
+ await this.page.mouse.move(0, 0);
80
+ // Make the sure the tooltip closed
81
+ await expect(
82
+ this.page.locator('.cm-tooltip-hover').last(),
83
+ ).not.toBeVisible();
84
+ }
85
+ }
@@ -0,0 +1,57 @@
1
+ import { expect, test } from '@playwright/experimental-ct-react';
2
+ import { CypherEditor } from '../CypherEditor';
3
+ import { CypherEditorPage } from './e2eUtils';
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,196 @@
1
+ import { expect, test } from '@playwright/experimental-ct-react';
2
+ import { CypherEditor } from '../CypherEditor';
3
+ import { CypherEditorPage } from './e2eUtils';
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
+ history={['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
+ const history: string[] = [];
43
+ const onExecute = (cmd: string) => {
44
+ history.unshift(cmd);
45
+ };
46
+
47
+ const editor = await mount(
48
+ <CypherEditor
49
+ value={initialValue}
50
+ history={history}
51
+ onExecute={onExecute}
52
+ />,
53
+ );
54
+
55
+ // Execute initial query
56
+ await editorPage.getEditor().press('Control+Enter');
57
+ await editorPage.getEditor().press('Meta+Enter');
58
+ expect(history.length).toBe(1);
59
+
60
+ // Ensure query execution doesn't fire if the query is only whitespace
61
+ await editorPage.getEditor().fill(' ');
62
+ await editorPage.getEditor().press('Control+Enter');
63
+ await editorPage.getEditor().press('Meta+Enter');
64
+ expect(history.length).toBe(1);
65
+
66
+ // Ensure only enter doesn't execute query
67
+ await editorPage.getEditor().fill('multiline');
68
+ await editorPage.getEditor().press('Enter');
69
+ await editorPage.getEditor().press('Enter');
70
+ await editorPage.getEditor().press('Enter');
71
+ await editorPage.getEditor().press('Enter');
72
+ await page.keyboard.type('entry');
73
+ expect(history.length).toBe(1);
74
+
75
+ await editorPage.getEditor().press('Control+Enter');
76
+ await editorPage.getEditor().press('Meta+Enter');
77
+ expect(history.length).toBe(2);
78
+
79
+ // rerender with new history
80
+ await editor.update(
81
+ <CypherEditor
82
+ value={initialValue}
83
+ history={history}
84
+ onExecute={onExecute}
85
+ />,
86
+ );
87
+
88
+ // type a new query and make sure it's not lost when navigating history
89
+ await editorPage.getEditor().fill('draft');
90
+ await expect(page.getByText('draft')).toBeVisible();
91
+ expect(history.length).toBe(2);
92
+
93
+ // Navigate to the top of the editor before navigating history
94
+ await editorPage.getEditor().press('ArrowLeft');
95
+ await editorPage.getEditor().press('ArrowLeft');
96
+ await editorPage.getEditor().press('ArrowLeft');
97
+ await editorPage.getEditor().press('ArrowLeft');
98
+ await editorPage.getEditor().press('ArrowLeft');
99
+ await editorPage.getEditor().press('ArrowUp');
100
+
101
+ // Ensure moving down in the editor doesn't navigate history
102
+ await expect(page.getByText('multiline')).toBeVisible();
103
+
104
+ // arrow movements don't matter until bottom is hit
105
+ await editorPage.getEditor().press('ArrowUp');
106
+ await editorPage.getEditor().press('ArrowUp');
107
+ await editorPage.getEditor().press('ArrowDown');
108
+ await editorPage.getEditor().press('ArrowDown');
109
+
110
+ // editor still multiline
111
+ await expect(page.getByText('multiline')).toBeVisible();
112
+
113
+ // until you hit the end where have the draft we created earlier
114
+ await editorPage.getEditor().press('ArrowDown');
115
+ await expect(page.getByText('draft')).toBeVisible();
116
+ });
117
+
118
+ test('can navigate with cmd+up as well', async ({ page, mount }) => {
119
+ const editorPage = new CypherEditorPage(page);
120
+ const isMac = process.platform === 'darwin';
121
+ const metaUp = isMac ? 'Meta+ArrowUp' : 'Control+ArrowUp';
122
+ const metaDown = isMac ? 'Meta+ArrowDown' : 'Control+ArrowDown';
123
+
124
+ const initialValue = 'MATCH (n) RETURN n;';
125
+
126
+ await mount(
127
+ <CypherEditor
128
+ value={initialValue}
129
+ history={[
130
+ `one
131
+ multiline
132
+ entry
133
+ .`,
134
+ 'second',
135
+ ]}
136
+ onExecute={() => {
137
+ /* needed to turn on history movements */
138
+ }}
139
+ />,
140
+ );
141
+
142
+ await editorPage.getEditor().press(metaUp);
143
+ await expect(page.getByText('multiline')).toBeVisible();
144
+
145
+ // Single meta up moves all the way to top of editor on mac
146
+ if (isMac) {
147
+ await editorPage.getEditor().press(metaUp);
148
+ } 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');
153
+ }
154
+ // move in history
155
+ await editorPage.getEditor().press(metaUp);
156
+ await expect(page.getByText('second')).toBeVisible();
157
+
158
+ await editorPage.getEditor().press(metaDown);
159
+ await expect(page.getByText('multiline')).toBeVisible();
160
+ });
161
+
162
+ test('test onExecute', async ({ page, mount }) => {
163
+ const editorPage = new CypherEditorPage(page);
164
+ const isMac = process.platform === 'darwin';
165
+ const execButton = isMac ? 'Meta+Enter' : 'Control+Enter';
166
+
167
+ const initialValue = 'MATCH (n) RETURN n;';
168
+ const history = [];
169
+
170
+ const onExecute = (cmd: string) => {
171
+ history.unshift(cmd);
172
+ };
173
+
174
+ const cypherEditor = await mount(
175
+ <CypherEditor value={initialValue} onExecute={onExecute} />,
176
+ );
177
+ await editorPage.getEditor().press(execButton);
178
+ expect(history).toEqual([initialValue]);
179
+
180
+ await editorPage.getEditor().press(execButton);
181
+ expect(history).toEqual([initialValue, initialValue]);
182
+
183
+ // can update onExecute
184
+ let newExecRan = false;
185
+ const newOnExecute = () => {
186
+ newExecRan = true;
187
+ };
188
+ await cypherEditor.update(
189
+ <CypherEditor value={initialValue} onExecute={newOnExecute} />,
190
+ );
191
+
192
+ await editorPage.getEditor().press(execButton);
193
+ expect(newExecRan).toEqual(true);
194
+ // old value should still be only 2
195
+ expect(history).toEqual([initialValue, initialValue]);
196
+ });
@@ -0,0 +1,158 @@
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 ({ mount, page }) => {
14
+ const client = await page.context().newCDPSession(page);
15
+ if (process.env.BENCHMARKING === 'true') {
16
+ test.setTimeout(1000000);
17
+ await client.send('Performance.enable');
18
+ await client.send('Emulation.setCPUThrottlingRate', { rate: 4 });
19
+ await client.send('Overlay.setShowFPSCounter', { show: true });
20
+
21
+ await page.evaluate(() => {
22
+ window.longtasks = [];
23
+ const observer = new PerformanceObserver((list) => {
24
+ window.longtasks.push(...list.getEntries().map((e) => e.duration));
25
+ });
26
+
27
+ observer.observe({ entryTypes: ['longtask'] });
28
+ });
29
+ } else {
30
+ test.setTimeout(30 * 1000);
31
+ }
32
+ const editorPage = new CypherEditorPage(page);
33
+ const component = await mount(
34
+ <CypherEditor
35
+ prompt="neo4j>"
36
+ theme="dark"
37
+ lint
38
+ schema={testData.mockSchema}
39
+ />,
40
+ );
41
+
42
+ // pressSequentially is less efficient -> we want to test the performance of the editor
43
+ await editorPage.getEditor().pressSequentially(`
44
+ MATCH (n:Person) RETURN m;`);
45
+
46
+ await editorPage.checkErrorMessage('m', 'Variable `m` not defined');
47
+
48
+ // set and unset large query a few times
49
+ await component.update(
50
+ <CypherEditor value={testData.largeQuery} schema={testData.mockSchema} />,
51
+ );
52
+ await component.update(
53
+ <CypherEditor value="" schema={testData.mockSchema} />,
54
+ );
55
+
56
+ await component.update(
57
+ <CypherEditor value={testData.largeQuery} schema={testData.mockSchema} />,
58
+ );
59
+ await component.update(<CypherEditor value="" />);
60
+
61
+ await component.update(
62
+ <CypherEditor value={testData.largeQuery} schema={testData.mockSchema} />,
63
+ );
64
+ await component.update(
65
+ <CypherEditor value="" schema={testData.mockSchema} />,
66
+ );
67
+
68
+ await component.update(
69
+ <CypherEditor value={testData.largeQuery} schema={testData.mockSchema} />,
70
+ );
71
+ await component.update(
72
+ <CypherEditor value="" schema={testData.mockSchema} />,
73
+ );
74
+
75
+ await component.update(
76
+ <CypherEditor value={testData.largeQuery} schema={testData.mockSchema} />,
77
+ );
78
+
79
+ await editorPage.getEditor().pressSequentially(`
80
+ MATCH (n:P`);
81
+
82
+ await expect(
83
+ page.locator('.cm-tooltip-autocomplete').getByText('Person'),
84
+ ).toBeVisible();
85
+
86
+ await page.locator('.cm-tooltip-autocomplete').getByText('Person').click();
87
+
88
+ await expect(page.locator('.cm-tooltip-autocomplete')).not.toBeVisible();
89
+
90
+ await expect(component).toContainText('MATCH (n:Person');
91
+
92
+ await editorPage.getEditor().pressSequentially(') RETRN my');
93
+
94
+ await expect(component).toContainText('MATCH (n:Person) RETRN m');
95
+
96
+ await editorPage.checkErrorMessage(
97
+ 'RETRN',
98
+ 'Unexpected token. Did you mean RETURN?',
99
+ );
100
+
101
+ await editorPage
102
+ .getEditor()
103
+ .pressSequentially('veryveryveryverylongvariable');
104
+
105
+ if (process.env.BENCHMARKING === 'true') {
106
+ const longtasks = await page.evaluate(() => window.longtasks);
107
+ const sortedLongTasks = longtasks.sort((a, b) => a - b);
108
+ const medianLongTask =
109
+ sortedLongTasks[Math.floor(sortedLongTasks.length / 2)];
110
+ const averageLongTask =
111
+ sortedLongTasks.reduce((a, b) => a + b, 0) / sortedLongTasks.length;
112
+ const over500 = sortedLongTasks.filter((t) => t > 500).length;
113
+ const nintyninethPercentile =
114
+ sortedLongTasks[Math.floor(sortedLongTasks.length * 0.99)];
115
+ const longTaskCount = longtasks.length;
116
+ const totalLongTaskTime = longtasks.reduce((a, b) => a + b, 0);
117
+
118
+ const USER_ID = 1226722;
119
+ const API_KEY = process.env.GRAFANA_API_KEY;
120
+ if (!API_KEY) {
121
+ throw new Error('Missing grafana api key');
122
+ }
123
+
124
+ const metrics = {
125
+ medianLongTask,
126
+ averageLongTask,
127
+ over500,
128
+ nintyninethPercentile,
129
+ longTaskCount,
130
+ totalLongTaskTime,
131
+ };
132
+ const body = Object.entries(metrics)
133
+ .map(
134
+ ([key, value]) =>
135
+ `benchmark,bar_label=${key},source=playwright metric=${value}`,
136
+ )
137
+ .join('\n');
138
+
139
+ await fetch(
140
+ 'https://influx-prod-39-prod-eu-north-0.grafana.net/api/v1/push/influx/write',
141
+ {
142
+ method: 'post',
143
+ body,
144
+ headers: {
145
+ Authorization: `Bearer ${USER_ID}:${API_KEY}`,
146
+ 'Content-Type': 'text/plain',
147
+ },
148
+ },
149
+ ).then((res) => {
150
+ if (res.ok) {
151
+ // eslint-disable-next-line no-console
152
+ console.log('Metrics pushed to grafana successfully');
153
+ } else {
154
+ throw new Error(`Failed to push metrics to grafana: ${res.statusText}`);
155
+ }
156
+ });
157
+ }
158
+ });
@@ -0,0 +1,78 @@
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('can mount the editor with text', async ({ mount }) => {
7
+ const component = await mount(<CypherEditor value="MATCH (n) RETURN n;" />);
8
+
9
+ await expect(component).toContainText('MATCH (n) RETURN n;');
10
+ });
11
+
12
+ test('the editors text can be externally controlled ', async ({ mount }) => {
13
+ const intitialValue = 'MATCH (n) RETURN n;';
14
+
15
+ const component = await mount(<CypherEditor value={intitialValue} />);
16
+
17
+ await expect(component).toContainText(intitialValue);
18
+
19
+ const newValue = 'RETURN 123';
20
+ await component.update(<CypherEditor value={newValue} />);
21
+
22
+ await expect(component).toContainText(newValue);
23
+ });
24
+
25
+ test('the editor can report changes to the text ', async ({ mount, page }) => {
26
+ const intitialValue = 'MATCH (n) ';
27
+
28
+ let editorValueCopy = intitialValue;
29
+ const onChange = (val: string) => {
30
+ editorValueCopy = val;
31
+ };
32
+
33
+ await mount(<CypherEditor value={intitialValue} onChange={onChange} />);
34
+
35
+ const textField = page.getByRole('textbox');
36
+
37
+ await textField.fill('RETURN 12');
38
+
39
+ // editor update is debounced, retry wait for debounced
40
+ await expect(() => {
41
+ expect(editorValueCopy).toBe('RETURN 12');
42
+ }).toPass({ intervals: [300, 300, 1000] });
43
+
44
+ await page.keyboard.type('34');
45
+
46
+ await expect(() => {
47
+ expect(editorValueCopy).toBe('RETURN 12');
48
+ }).toPass({ intervals: [300, 300, 1000] });
49
+ });
50
+
51
+ test('can complete RETURN', async ({ page, mount }) => {
52
+ await mount(<CypherEditor />);
53
+ const textField = page.getByRole('textbox');
54
+
55
+ await textField.fill('RETU');
56
+
57
+ await page.getByText('RETURN').click();
58
+ await expect(textField).toHaveText('RETURN');
59
+ });
60
+
61
+ test('can complete CALL/CREATE', async ({ page, mount }) => {
62
+ await mount(<CypherEditor />);
63
+ const textField = page.getByRole('textbox');
64
+
65
+ await textField.fill('C');
66
+ await expect(page.getByText('CALL')).toBeVisible();
67
+ await expect(page.getByText('CREATE')).toBeVisible();
68
+
69
+ await textField.fill('CA');
70
+ await expect(page.getByText('CALL')).toBeVisible();
71
+ await expect(page.getByText('CREATE')).not.toBeVisible();
72
+
73
+ // wait for the autocomplete interactivity
74
+ await page.waitForTimeout(500);
75
+ await textField.press('Enter');
76
+
77
+ await expect(textField).toHaveText('CALL');
78
+ });