@@ -8,9 +8,9 @@ import 'codemirror/addon/fold/xml-fold';
8
8
import 'codemirror/addon/scroll/simplescrollbars' ;
9
9
import 'codemirror/addon/hint/show-hint' ;
10
10
import { queries } from '@testing-library/dom' ;
11
+ import userEvent from '@testing-library/user-event' ;
11
12
12
13
import CodeMirror from 'codemirror' ;
13
- import debounce from 'lodash/debounce' ;
14
14
import beautify from '../lib/beautify' ;
15
15
16
16
const baseOptions = {
@@ -26,17 +26,14 @@ const baseOptions = {
26
26
27
27
const options = {
28
28
html : {
29
- ...baseOptions ,
30
29
mode : { name : 'text/html' , multilineTagIndentPastTag : false } ,
31
30
} ,
32
31
33
32
htmlmixed : {
34
- ...baseOptions ,
35
33
mode : { name : 'htmlmixed' , multilineTagIndentPastTag : false } ,
36
34
} ,
37
35
38
36
javascript : {
39
- ...baseOptions ,
40
37
mode : 'javascript' ,
41
38
extraKeys : { 'Ctrl-Space' : 'autocomplete' } ,
42
39
hintOptions : { hint : getQueryHints } ,
@@ -48,6 +45,7 @@ const suggestions = {
48
45
. filter ( ( x ) => x . startsWith ( 'getBy' ) )
49
46
. sort ( ) ,
50
47
container : [ 'querySelector' , 'querySelectorAll' ] ,
48
+ userEvent : Object . keys ( userEvent ) . sort ( ) ,
51
49
} ;
52
50
53
51
function getQueryHints ( cm ) {
@@ -66,7 +64,7 @@ function getQueryHints(cm) {
66
64
++ end ;
67
65
}
68
66
69
- const word = line . slice ( start , end ) . toLowerCase ( ) ;
67
+ const word = line . slice ( start , end ) ; // .toLowerCase();
70
68
const offset = word . lastIndexOf ( '.' ) + 1 ;
71
69
const list = [ ] ;
72
70
@@ -83,8 +81,8 @@ function getQueryHints(cm) {
83
81
} else if ( word . includes ( '.' ) ) {
84
82
// user is already one level deeper, entered `screen.get...`
85
83
const [ obj , method ] = word . split ( '.' ) ;
86
- const values = ( suggestions [ obj ] || [ ] ) . filter ( ( x ) =>
87
- x . toLowerCase ( ) . includes ( method ) ,
84
+ const values = ( suggestions [ obj ] || [ ] ) . filter (
85
+ ( x ) => x . includes ( method ) , //x. toLowerCase().includes(method.toLowerCase() ),
88
86
) ;
89
87
list . push ( ...values ) ;
90
88
} else {
@@ -154,71 +152,143 @@ const NON_TRIGGER_KEYS = {
154
152
'222' : 'quote' ,
155
153
} ;
156
154
157
- function Editor ( { onLoad, onChange, mode, initialValue } ) {
158
- const elem = useRef ( ) ;
159
- const editor = useRef ( ) ;
155
+ function formatValue ( cm ) {
156
+ const mode = cm . options . mode . name ;
157
+ const value = cm . getValue ( ) ;
158
+ const formatted = beautify . format ( mode , value ) ;
159
+ cm . setValue ( formatted ) ;
160
+ }
160
161
161
- useEffect ( ( ) => {
162
- editor . current = CodeMirror . fromTextArea (
163
- elem . current ,
164
- options [ mode ] || baseOptions ,
165
- ) ;
166
- editor . current . setValue ( initialValue || '' ) ;
162
+ function autoComplete ( cm , event ) {
163
+ const cursor = cm . getDoc ( ) . getCursor ( ) ;
164
+ const token = cm . getTokenAt ( cursor ) ;
167
165
168
- // in some cases, CM loads with a scrollbar visible
169
- // while it shouldn't be required. Requesting a refresh
170
- // fixes this
171
- requestAnimationFrame ( ( ) => {
172
- editor . current . refresh ( ) ;
166
+ const shouldComplete =
167
+ ! cm . state . completionActive &&
168
+ ! NON_TRIGGER_KEYS [ ( event . keyCode || event . which ) . toString ( ) ] &&
169
+ ! ( token . string === '(' || token . string === ')' ) ;
170
+
171
+ if ( shouldComplete ) {
172
+ CodeMirror . commands . autocomplete ( cm , null , {
173
+ completeSingle : false ,
173
174
} ) ;
174
- } , [ mode ] ) ;
175
+ }
176
+ }
175
177
176
- useEffect ( ( ) => {
177
- if ( ! editor . current || typeof onChange !== 'function' ) {
178
+ function handleChange ( cm , change ) {
179
+ switch ( change . origin ) {
180
+ case 'setValue' :
178
181
return ;
182
+
183
+ case 'paste' : {
184
+ formatValue ( cm ) ;
185
+ break ;
179
186
}
180
187
181
- editor . current . on (
182
- 'changes' ,
183
- debounce ( ( ) => {
184
- onChange ( editor . current . getValue ( ) ) ;
185
- } , 25 ) ,
186
- ) ;
188
+ default : {
189
+ cm . onChange ( cm . getValue ( ) , { origin : change . origin } ) ;
190
+ break ;
191
+ }
192
+ }
193
+ }
187
194
188
- editor . current . on ( 'keyup' , ( editor , event ) => {
189
- const cursor = editor . getDoc ( ) . getCursor ( ) ;
190
- const token = editor . getTokenAt ( cursor ) ;
195
+ // with devtools open, the roundtrip between blur and result is about 200 ms.
196
+ // Close devtools, and it drops down to ~ 5 ms. If you're still getting weird
197
+ // user-event / fireEvent issues, it might be that either 25 or 500 ms is to low for
198
+ // your machine. Pumping up production timeouts can cause more side-effects, but
199
+ // I think we can quite safely increase to ~ 100 ms when neccasary. For the dev
200
+ // environment, I hope you can life with it. The worst thing that happens, is
201
+ // that the query editor loses focus while you're typing your userEvents.
202
+ const threshold = process . env . NODE_ENV === 'production' ? 25 : 500 ;
191
203
192
- const shouldComplete =
193
- ! editor . state . completionActive &&
194
- ! NON_TRIGGER_KEYS [ ( event . keyCode || event . which ) . toString ( ) ] &&
195
- ! ( token . string === '(' || token . string === ')' ) ;
204
+ // There are two ways that the query editor can use blur. One is if the user
205
+ // decides to leave the editor. The other is caused by evaluating the users
206
+ // script. For example to focus one of the inputs in the sandbox, to enter
207
+ // some text, or click an element. If this happens, we need to restore focus
208
+ // as soon as possible. This is, when the SANDBOX_READY event occurs before
209
+ // the `threshold`ms timeout has passed.
210
+ function handleBlur ( cm ) {
211
+ const cursor = cm . getCursor ( ) ;
212
+ let timeout ;
196
213
197
- if ( shouldComplete ) {
198
- CodeMirror . commands . autocomplete ( editor , null , {
199
- completeSingle : false ,
200
- } ) ;
201
- }
202
- } ) ;
214
+ function listener ( event ) {
215
+ const {
216
+ data : { source, type } ,
217
+ } = event ;
203
218
204
- const format = ( ) => {
205
- const value = editor . current . getValue ( ) ;
206
- const formatted = beautify . format ( mode , value ) ;
207
- editor . current . setValue ( formatted ) ;
208
- } ;
219
+ if ( source !== 'testing-playground-sandbox' || type !== 'SANDBOX_READY' ) {
220
+ return ;
221
+ }
222
+
223
+ // if we came to here, it means that the time between the `blur` event and
224
+ // SANDBOX_READY msg was within `threshold` ms. Otherwise, the timeout would
225
+ // already have removed this listener.
226
+ clearTimeout ( timeout ) ;
227
+ window . removeEventListener ( 'message' , listener ) ;
228
+ cm . focus ( ) ;
229
+ cm . setCursor ( cursor ) ;
230
+ }
231
+
232
+ // note, { once: true } doesn't work here! there can be multiple messages, while
233
+ // we need one with a specific event attribute
234
+ window . addEventListener ( 'message' , listener ) ;
235
+
236
+ // we wait a couple of ms for the SANDBOX_READY event, if that doesn't come
237
+ // before given treshold, we assume the user left the editor, and allow it
238
+ // lose focus.
239
+ timeout = setTimeout ( ( ) => {
240
+ window . removeEventListener ( 'message' , listener ) ;
241
+ formatValue ( cm ) ;
242
+ } , threshold ) ;
243
+ }
209
244
210
- editor . current . on ( 'change' , ( _ , change ) => {
211
- if ( change . origin !== 'paste' ) {
212
- return ;
213
- }
245
+ function Editor ( { onLoad, onChange, mode, initialValue } ) {
246
+ const elem = useRef ( ) ;
247
+ const editor = useRef ( ) ;
214
248
215
- format ( ) ;
249
+ useEffect ( ( ) => {
250
+ editor . current = CodeMirror . fromTextArea ( elem . current , {
251
+ ...baseOptions ,
252
+ ...options [ mode ] ,
253
+ extraKeys :
254
+ typeof onChange === 'function'
255
+ ? {
256
+ 'Ctrl-Enter' : ( ) => {
257
+ onChange ( editor . current . getValue ( ) , { origin : 'user' } ) ;
258
+ } ,
259
+ 'Cmd-Enter' : ( ) => {
260
+ onChange ( editor . current . getValue ( ) , { origin : 'user' } ) ;
261
+ } ,
262
+ ...( options [ mode ] . extraKeys || { } ) ,
263
+ }
264
+ : options [ mode ] . extraKeys ,
216
265
} ) ;
217
266
218
- editor . current . on ( 'blur' , format ) ;
267
+ editor . current . setValue ( initialValue || '' ) ;
268
+
269
+ editor . current . on ( 'change' , handleChange ) ;
270
+ editor . current . on ( 'keyup' , autoComplete ) ;
271
+ editor . current . on ( 'blur' , handleBlur ) ;
219
272
220
273
onLoad ( editor . current ) ;
221
- } , [ editor . current , onChange ] ) ;
274
+
275
+ // in some cases, CM loads with a scrollbar visible
276
+ // while it shouldn't be required. Requesting a refresh
277
+ // fixes this
278
+ requestAnimationFrame ( ( ) => {
279
+ editor . current . refresh ( ) ;
280
+ } ) ;
281
+
282
+ return ( ) => {
283
+ editor . current . off ( 'change' , handleChange ) ;
284
+ editor . current . off ( 'keyup' , autoComplete ) ;
285
+ editor . current . off ( 'blur' , handleBlur ) ;
286
+ } ;
287
+ } , [ mode , onChange , onLoad , initialValue ] ) ;
288
+
289
+ useEffect ( ( ) => {
290
+ editor . current . onChange = onChange ;
291
+ } , [ onChange ] ) ;
222
292
223
293
return (
224
294
< div className = "w-full h-full" >
0 commit comments