@wordpress/data 6.6.0 → 6.8.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.
@@ -1,7 +1,7 @@
1
1
  /**
2
2
  * External dependencies
3
3
  */
4
- import TestRenderer, { act } from 'react-test-renderer';
4
+ import { act, render } from '@testing-library/react';
5
5
 
6
6
  /**
7
7
  * WordPress dependencies
@@ -11,10 +11,15 @@ import { useState, useReducer } from '@wordpress/element';
11
11
  /**
12
12
  * Internal dependencies
13
13
  */
14
- import { createRegistry } from '../../../registry';
15
- import { createRegistrySelector } from '../../../factory';
16
- import { RegistryProvider } from '../../registry-provider';
17
- import useSelect from '../index';
14
+ import {
15
+ createRegistry,
16
+ createRegistrySelector,
17
+ RegistryProvider,
18
+ AsyncModeProvider,
19
+ } from '../../..';
20
+ import useSelect from '..';
21
+
22
+ jest.useRealTimers();
18
23
 
19
24
  describe( 'useSelect', () => {
20
25
  let registry;
@@ -22,15 +27,6 @@ describe( 'useSelect', () => {
22
27
  registry = createRegistry();
23
28
  } );
24
29
 
25
- const getTestComponent = ( mapSelectSpy, dependencyKey ) => ( props ) => {
26
- const dependencies = props[ dependencyKey ];
27
- mapSelectSpy.mockImplementation( ( select ) => ( {
28
- results: select( 'testStore' ).testSelector( props.keyName ),
29
- } ) );
30
- const data = useSelect( mapSelectSpy, [ dependencies ] );
31
- return <div>{ data.results }</div>;
32
- };
33
-
34
30
  it( 'passes the relevant data to the component', () => {
35
31
  registry.registerStore( 'testStore', {
36
32
  reducer: () => ( { foo: 'bar' } ),
@@ -38,19 +34,22 @@ describe( 'useSelect', () => {
38
34
  testSelector: ( state, key ) => state[ key ],
39
35
  },
40
36
  } );
37
+
41
38
  const selectSpy = jest.fn();
42
- const TestComponent = jest
43
- .fn()
44
- .mockImplementation( getTestComponent( selectSpy, 'keyName' ) );
45
- let renderer;
46
- act( () => {
47
- renderer = TestRenderer.create(
48
- <RegistryProvider value={ registry }>
49
- <TestComponent keyName="foo" />
50
- </RegistryProvider>
51
- );
39
+ const TestComponent = jest.fn( ( props ) => {
40
+ selectSpy.mockImplementation( ( select ) => ( {
41
+ results: select( 'testStore' ).testSelector( props.keyName ),
42
+ } ) );
43
+ const data = useSelect( selectSpy, [ props.keyName ] );
44
+ return <div role="status">{ data.results }</div>;
52
45
  } );
53
- const testInstance = renderer.root;
46
+
47
+ const rendered = render(
48
+ <RegistryProvider value={ registry }>
49
+ <TestComponent keyName="foo" />
50
+ </RegistryProvider>
51
+ );
52
+
54
53
  // 2 times expected
55
54
  // - 1 for initial mount
56
55
  // - 1 for after mount before subscription set.
@@ -58,9 +57,7 @@ describe( 'useSelect', () => {
58
57
  expect( TestComponent ).toHaveBeenCalledTimes( 1 );
59
58
 
60
59
  // Ensure expected state was rendered.
61
- expect( testInstance.findByType( 'div' ).props ).toEqual( {
62
- children: 'bar',
63
- } );
60
+ expect( rendered.getByRole( 'status' ) ).toHaveTextContent( 'bar' );
64
61
  } );
65
62
 
66
63
  it( 'uses memoized selector if dependencies do not change', () => {
@@ -71,73 +68,127 @@ describe( 'useSelect', () => {
71
68
  },
72
69
  } );
73
70
 
74
- const selectSpyFoo = jest.fn().mockImplementation( () => 'foo' );
75
- const selectSpyBar = jest.fn().mockImplementation( () => 'bar' );
76
- const TestComponent = jest.fn().mockImplementation( ( props ) => {
71
+ const selectSpyFoo = jest.fn( () => 'foo' );
72
+ const selectSpyBar = jest.fn( () => 'bar' );
73
+ const TestComponent = jest.fn( ( props ) => {
77
74
  const mapSelect = props.change ? selectSpyFoo : selectSpyBar;
78
75
  const data = useSelect( mapSelect, [ props.keyName ] );
79
- return <div>{ data }</div>;
80
- } );
81
- let renderer;
82
- act( () => {
83
- renderer = TestRenderer.create(
84
- <RegistryProvider value={ registry }>
85
- <TestComponent keyName="foo" change={ true } />
86
- </RegistryProvider>
87
- );
76
+ return <div role="status">{ data }</div>;
88
77
  } );
89
- const testInstance = renderer.root;
78
+
79
+ const rendered = render(
80
+ <RegistryProvider value={ registry }>
81
+ <TestComponent keyName="foo" change={ true } />
82
+ </RegistryProvider>
83
+ );
90
84
 
91
85
  expect( selectSpyFoo ).toHaveBeenCalledTimes( 2 );
92
86
  expect( selectSpyBar ).toHaveBeenCalledTimes( 0 );
93
87
  expect( TestComponent ).toHaveBeenCalledTimes( 1 );
94
88
 
95
89
  // Ensure expected state was rendered.
96
- expect( testInstance.findByType( 'div' ).props ).toEqual( {
97
- children: 'foo',
98
- } );
90
+ expect( rendered.getByRole( 'status' ) ).toHaveTextContent( 'foo' );
99
91
 
100
92
  // Rerender with non dependency changed.
101
- act( () => {
102
- renderer.update(
103
- <RegistryProvider value={ registry }>
104
- <TestComponent keyName="foo" change={ false } />
105
- </RegistryProvider>
106
- );
107
- } );
93
+ rendered.rerender(
94
+ <RegistryProvider value={ registry }>
95
+ <TestComponent keyName="foo" change={ false } />
96
+ </RegistryProvider>
97
+ );
108
98
 
109
99
  expect( selectSpyFoo ).toHaveBeenCalledTimes( 2 );
110
100
  expect( selectSpyBar ).toHaveBeenCalledTimes( 0 );
111
101
  expect( TestComponent ).toHaveBeenCalledTimes( 2 );
112
102
 
113
103
  // Ensure expected state was rendered.
114
- expect( testInstance.findByType( 'div' ).props ).toEqual( {
115
- children: 'foo',
116
- } );
104
+ expect( rendered.getByRole( 'status' ) ).toHaveTextContent( 'foo' );
117
105
 
118
106
  // Rerender with dependency changed.
119
- act( () => {
120
- renderer.update(
121
- <RegistryProvider value={ registry }>
122
- <TestComponent keyName="bar" change={ false } />
123
- </RegistryProvider>
124
- );
125
- } );
107
+ rendered.rerender(
108
+ <RegistryProvider value={ registry }>
109
+ <TestComponent keyName="bar" change={ false } />
110
+ </RegistryProvider>
111
+ );
126
112
 
127
113
  expect( selectSpyFoo ).toHaveBeenCalledTimes( 2 );
128
114
  expect( selectSpyBar ).toHaveBeenCalledTimes( 2 );
129
115
  expect( TestComponent ).toHaveBeenCalledTimes( 3 );
130
116
 
131
117
  // Ensure expected state was rendered.
132
- expect( testInstance.findByType( 'div' ).props ).toEqual( {
133
- children: 'bar',
118
+ expect( rendered.getByRole( 'status' ) ).toHaveTextContent( 'bar' );
119
+ } );
120
+
121
+ it( 'avoid calling nested listener after unmounted', async () => {
122
+ registry.registerStore( 'toggler', {
123
+ reducer: ( state = false, action ) =>
124
+ action.type === 'TOGGLE' ? ! state : state,
125
+ actions: {
126
+ toggle: () => ( { type: 'TOGGLE' } ),
127
+ },
128
+ selectors: {
129
+ get: ( state ) => state,
130
+ },
131
+ } );
132
+
133
+ const mapSelect = ( select ) => select( 'toggler' ).get();
134
+
135
+ const mapSelectChild = jest.fn( mapSelect );
136
+ function Child() {
137
+ const show = useSelect( mapSelectChild, [] );
138
+ return show ? 'yes' : 'no';
139
+ }
140
+
141
+ const mapSelectParent = jest.fn( mapSelect );
142
+ function Parent() {
143
+ const show = useSelect( mapSelectParent, [] );
144
+ return show ? <Child /> : 'none';
145
+ }
146
+
147
+ const rendered = render(
148
+ <RegistryProvider value={ registry }>
149
+ <Parent />
150
+ </RegistryProvider>
151
+ );
152
+
153
+ // Initial render renders only parent and subscribes the parent to store.
154
+ expect( rendered.getByText( 'none' ) ).toBeInTheDocument();
155
+ expect( mapSelectParent ).toHaveBeenCalledTimes( 2 );
156
+ expect( mapSelectChild ).toHaveBeenCalledTimes( 0 );
157
+
158
+ // act() does batched updates internally, i.e., any scheduled setStates or effects
159
+ // will be executed only after the dispatch finishes. But we want to opt out of
160
+ // batched updates here. We want all the setStates to be done synchronously, as the
161
+ // store listeners are called. The async/await code is a trick to do it: do the
162
+ // dispatch in a different event loop tick, where the batched updates are no longer active.
163
+ await act( async () => {
164
+ await Promise.resolve();
165
+ registry.dispatch( 'toggler' ).toggle();
134
166
  } );
167
+
168
+ // Child was rendered and subscribed to the store, as the _second_ subscription.
169
+ expect( rendered.getByText( 'yes' ) ).toBeInTheDocument();
170
+ expect( mapSelectParent ).toHaveBeenCalledTimes( 3 );
171
+ expect( mapSelectChild ).toHaveBeenCalledTimes( 2 );
172
+
173
+ await act( async () => {
174
+ await Promise.resolve();
175
+ registry.dispatch( 'toggler' ).toggle();
176
+ } );
177
+
178
+ // Check that child was unmounted without any extra state update being performed on it.
179
+ // I.e., `mapSelectChild` was never called again, and no "state update on an unmounted
180
+ // component" warning was triggered.
181
+ expect( rendered.getByText( 'none' ) ).toBeInTheDocument();
182
+ expect( mapSelectParent ).toHaveBeenCalledTimes( 4 );
183
+ expect( mapSelectChild ).toHaveBeenCalledTimes( 2 );
135
184
  } );
185
+
136
186
  describe( 'rerenders as expected with various mapSelect return types', () => {
137
187
  const getComponent = ( mapSelectSpy ) => () => {
138
188
  const data = useSelect( mapSelectSpy, [] );
139
- return <div data={ data } />;
189
+ return <div role="status" data-d={ JSON.stringify( data ) } />;
140
190
  };
191
+
141
192
  let TestComponent;
142
193
  const mapSelectSpy = jest.fn( ( select ) =>
143
194
  select( 'testStore' ).testSelector()
@@ -156,6 +207,7 @@ describe( 'useSelect', () => {
156
207
  } );
157
208
  TestComponent = getComponent( mapSelectSpy );
158
209
  } );
210
+
159
211
  afterEach( () => {
160
212
  selectorSpy.mockClear();
161
213
  mapSelectSpy.mockClear();
@@ -180,18 +232,14 @@ describe( 'useSelect', () => {
180
232
  ( type, testValues ) => {
181
233
  const [ valueA, valueB ] = testValues;
182
234
  selectorSpy.mockReturnValue( valueA );
183
- let renderer;
184
- act( () => {
185
- renderer = TestRenderer.create(
186
- <RegistryProvider value={ registry }>
187
- <TestComponent />
188
- </RegistryProvider>
189
- );
190
- } );
191
- const testInstance = renderer.root;
235
+ const rendered = render(
236
+ <RegistryProvider value={ registry }>
237
+ <TestComponent />
238
+ </RegistryProvider>
239
+ );
192
240
  // Ensure expected state was rendered.
193
- expect( testInstance.findByType( 'div' ).props.data ).toEqual(
194
- valueA
241
+ expect( rendered.getByRole( 'status' ).dataset.d ).toBe(
242
+ JSON.stringify( valueA )
195
243
  );
196
244
 
197
245
  // Update the returned value from the selector and trigger the
@@ -200,15 +248,15 @@ describe( 'useSelect', () => {
200
248
  selectorSpy.mockReturnValue( valueB );
201
249
  registry.dispatch( 'testStore' ).forceUpdate();
202
250
  } );
203
- expect( testInstance.findByType( 'div' ).props.data ).toEqual(
204
- valueB
251
+ expect( rendered.getByRole( 'status' ).dataset.d ).toBe(
252
+ JSON.stringify( valueB )
205
253
  );
206
254
  expect( mapSelectSpy ).toHaveBeenCalledTimes( 3 );
207
255
  }
208
256
  );
209
257
  } );
210
258
 
211
- describe( 're-calls the selector as minimal times as possible', () => {
259
+ describe( 're-calls the selector as few times as possible', () => {
212
260
  const counterStore = {
213
261
  actions: {
214
262
  increment: () => ( { type: 'INCREMENT' } ),
@@ -231,8 +279,6 @@ describe( 'useSelect', () => {
231
279
  registry.registerStore( 'store-1', counterStore );
232
280
  registry.registerStore( 'store-2', counterStore );
233
281
 
234
- let renderer;
235
-
236
282
  const selectCount1 = jest.fn();
237
283
  const selectCount2 = jest.fn();
238
284
 
@@ -248,23 +294,19 @@ describe( 'useSelect', () => {
248
294
  []
249
295
  );
250
296
 
251
- return <div data={ count1 } />;
297
+ return <div role="status">{ count1 }</div>;
252
298
  } );
253
299
 
254
- act( () => {
255
- renderer = TestRenderer.create(
256
- <RegistryProvider value={ registry }>
257
- <TestComponent />
258
- </RegistryProvider>
259
- );
260
- } );
261
-
262
- const testInstance = renderer.root;
300
+ const rendered = render(
301
+ <RegistryProvider value={ registry }>
302
+ <TestComponent />
303
+ </RegistryProvider>
304
+ );
263
305
 
264
306
  expect( selectCount1 ).toHaveBeenCalledTimes( 2 );
265
307
  expect( selectCount2 ).toHaveBeenCalledTimes( 2 );
266
308
  expect( TestComponent ).toHaveBeenCalledTimes( 1 );
267
- expect( testInstance.findByType( 'div' ).props.data ).toBe( 0 );
309
+ expect( rendered.getByRole( 'status' ) ).toHaveTextContent( 0 );
268
310
 
269
311
  act( () => {
270
312
  registry.dispatch( 'store-2' ).increment();
@@ -273,7 +315,7 @@ describe( 'useSelect', () => {
273
315
  expect( selectCount1 ).toHaveBeenCalledTimes( 2 );
274
316
  expect( selectCount2 ).toHaveBeenCalledTimes( 3 );
275
317
  expect( TestComponent ).toHaveBeenCalledTimes( 2 );
276
- expect( testInstance.findByType( 'div' ).props.data ).toBe( 0 );
318
+ expect( rendered.getByRole( 'status' ) ).toHaveTextContent( 0 );
277
319
 
278
320
  act( () => {
279
321
  registry.dispatch( 'store-1' ).increment();
@@ -282,10 +324,10 @@ describe( 'useSelect', () => {
282
324
  expect( selectCount1 ).toHaveBeenCalledTimes( 3 );
283
325
  expect( selectCount2 ).toHaveBeenCalledTimes( 3 );
284
326
  expect( TestComponent ).toHaveBeenCalledTimes( 3 );
285
- expect( testInstance.findByType( 'div' ).props.data ).toBe( 1 );
327
+ expect( rendered.getByRole( 'status' ) ).toHaveTextContent( 1 );
286
328
 
287
329
  // Test if the unsubscribers get called correctly.
288
- renderer.unmount();
330
+ rendered.unmount();
289
331
  } );
290
332
 
291
333
  it( 'can subscribe to multiple stores at once', () => {
@@ -293,8 +335,6 @@ describe( 'useSelect', () => {
293
335
  registry.registerStore( 'store-2', counterStore );
294
336
  registry.registerStore( 'store-3', counterStore );
295
337
 
296
- let renderer;
297
-
298
338
  const selectCount1And2 = jest.fn();
299
339
 
300
340
  const TestComponent = jest.fn( () => {
@@ -307,44 +347,35 @@ describe( 'useSelect', () => {
307
347
  []
308
348
  );
309
349
 
310
- return <div data={ { count1, count2 } } />;
311
- } );
312
-
313
- act( () => {
314
- renderer = TestRenderer.create(
315
- <RegistryProvider value={ registry }>
316
- <TestComponent />
317
- </RegistryProvider>
350
+ return (
351
+ <div role="status">
352
+ { count1 },{ count2 }
353
+ </div>
318
354
  );
319
355
  } );
320
356
 
321
- const testInstance = renderer.root;
357
+ const rendered = render(
358
+ <RegistryProvider value={ registry }>
359
+ <TestComponent />
360
+ </RegistryProvider>
361
+ );
322
362
 
323
363
  expect( selectCount1And2 ).toHaveBeenCalledTimes( 2 );
324
- expect( testInstance.findByType( 'div' ).props.data ).toEqual( {
325
- count1: 0,
326
- count2: 0,
327
- } );
364
+ expect( rendered.getByRole( 'status' ) ).toHaveTextContent( '0,0' );
328
365
 
329
366
  act( () => {
330
367
  registry.dispatch( 'store-2' ).increment();
331
368
  } );
332
369
 
333
370
  expect( selectCount1And2 ).toHaveBeenCalledTimes( 3 );
334
- expect( testInstance.findByType( 'div' ).props.data ).toEqual( {
335
- count1: 0,
336
- count2: 1,
337
- } );
371
+ expect( rendered.getByRole( 'status' ) ).toHaveTextContent( '0,1' );
338
372
 
339
373
  act( () => {
340
374
  registry.dispatch( 'store-3' ).increment();
341
375
  } );
342
376
 
343
377
  expect( selectCount1And2 ).toHaveBeenCalledTimes( 3 );
344
- expect( testInstance.findByType( 'div' ).props.data ).toEqual( {
345
- count1: 0,
346
- count2: 1,
347
- } );
378
+ expect( rendered.getByRole( 'status' ) ).toHaveTextContent( '0,1' );
348
379
  } );
349
380
 
350
381
  it( 're-calls the selector when deps changed', () => {
@@ -352,7 +383,7 @@ describe( 'useSelect', () => {
352
383
  registry.registerStore( 'store-2', counterStore );
353
384
  registry.registerStore( 'store-3', counterStore );
354
385
 
355
- let renderer, dep, setDep;
386
+ let dep, setDep;
356
387
  const selectCount1AndDep = jest.fn();
357
388
 
358
389
  const TestComponent = jest.fn( () => {
@@ -366,44 +397,41 @@ describe( 'useSelect', () => {
366
397
  [ dep ]
367
398
  );
368
399
 
369
- return <div data={ state } />;
370
- } );
371
-
372
- act( () => {
373
- renderer = TestRenderer.create(
374
- <RegistryProvider value={ registry }>
375
- <TestComponent />
376
- </RegistryProvider>
400
+ return (
401
+ <div role="status">
402
+ count:{ state.count1 },dep:{ state.dep }
403
+ </div>
377
404
  );
378
405
  } );
379
406
 
380
- const testInstance = renderer.root;
407
+ const rendered = render(
408
+ <RegistryProvider value={ registry }>
409
+ <TestComponent />
410
+ </RegistryProvider>
411
+ );
381
412
 
382
413
  expect( selectCount1AndDep ).toHaveBeenCalledTimes( 2 );
383
- expect( testInstance.findByType( 'div' ).props.data ).toEqual( {
384
- count1: 0,
385
- dep: 0,
386
- } );
414
+ expect( rendered.getByRole( 'status' ) ).toHaveTextContent(
415
+ 'count:0,dep:0'
416
+ );
387
417
 
388
418
  act( () => {
389
419
  setDep( 1 );
390
420
  } );
391
421
 
392
422
  expect( selectCount1AndDep ).toHaveBeenCalledTimes( 4 );
393
- expect( testInstance.findByType( 'div' ).props.data ).toEqual( {
394
- count1: 0,
395
- dep: 1,
396
- } );
423
+ expect( rendered.getByRole( 'status' ) ).toHaveTextContent(
424
+ 'count:0,dep:1'
425
+ );
397
426
 
398
427
  act( () => {
399
428
  registry.dispatch( 'store-1' ).increment();
400
429
  } );
401
430
 
402
431
  expect( selectCount1AndDep ).toHaveBeenCalledTimes( 5 );
403
- expect( testInstance.findByType( 'div' ).props.data ).toEqual( {
404
- count1: 1,
405
- dep: 1,
406
- } );
432
+ expect( rendered.getByRole( 'status' ) ).toHaveTextContent(
433
+ 'count:1,dep:1'
434
+ );
407
435
  } );
408
436
 
409
437
  it( 'handles registry selectors', () => {
@@ -423,7 +451,6 @@ describe( 'useSelect', () => {
423
451
  } );
424
452
  registry.registerStore( 'store-2', counterStore );
425
453
 
426
- let renderer;
427
454
  const selectCount1And2 = jest.fn();
428
455
 
429
456
  const TestComponent = jest.fn( () => {
@@ -434,46 +461,43 @@ describe( 'useSelect', () => {
434
461
  []
435
462
  );
436
463
 
437
- return <div data={ state } />;
438
- } );
439
-
440
- act( () => {
441
- renderer = TestRenderer.create(
442
- <RegistryProvider value={ registry }>
443
- <TestComponent />
444
- </RegistryProvider>
464
+ return (
465
+ <div role="status">
466
+ count1:{ state.count1 },count2:{ state.count2 }
467
+ </div>
445
468
  );
446
469
  } );
447
470
 
448
- const testInstance = renderer.root;
471
+ const rendered = render(
472
+ <RegistryProvider value={ registry }>
473
+ <TestComponent />
474
+ </RegistryProvider>
475
+ );
449
476
 
450
477
  expect( selectCount1And2 ).toHaveBeenCalledTimes( 2 );
451
- expect( testInstance.findByType( 'div' ).props.data ).toEqual( {
452
- count1: 0,
453
- count2: 0,
454
- } );
478
+ expect( rendered.getByRole( 'status' ) ).toHaveTextContent(
479
+ 'count1:0,count2:0'
480
+ );
455
481
 
456
482
  act( () => {
457
483
  registry.dispatch( 'store-2' ).increment();
458
484
  } );
459
485
 
460
486
  expect( selectCount1And2 ).toHaveBeenCalledTimes( 3 );
461
- expect( testInstance.findByType( 'div' ).props.data ).toEqual( {
462
- count1: 0,
463
- count2: 1,
464
- } );
487
+ expect( rendered.getByRole( 'status' ) ).toHaveTextContent(
488
+ 'count1:0,count2:1'
489
+ );
465
490
  } );
466
491
 
467
492
  it( 'handles conditional statements in selectors', () => {
468
493
  registry.registerStore( 'store-1', counterStore );
469
494
  registry.registerStore( 'store-2', counterStore );
470
495
 
471
- let renderer, shouldSelectCount1, toggle;
472
496
  const selectCount1 = jest.fn();
473
497
  const selectCount2 = jest.fn();
474
498
 
475
499
  const TestComponent = jest.fn( () => {
476
- [ shouldSelectCount1, toggle ] = useReducer(
500
+ const [ shouldSelectCount1, toggle ] = useReducer(
477
501
  ( should ) => ! should,
478
502
  false
479
503
  );
@@ -492,32 +516,31 @@ describe( 'useSelect', () => {
492
516
  [ shouldSelectCount1 ]
493
517
  );
494
518
 
495
- return <div data={ state } />;
496
- } );
497
-
498
- act( () => {
499
- renderer = TestRenderer.create(
500
- <RegistryProvider value={ registry }>
501
- <TestComponent />
502
- </RegistryProvider>
519
+ return (
520
+ <>
521
+ <div role="status">{ state }</div>
522
+ <button onClick={ toggle }>Toggle</button>
523
+ </>
503
524
  );
504
525
  } );
505
526
 
506
- const testInstance = renderer.root;
527
+ const rendered = render(
528
+ <RegistryProvider value={ registry }>
529
+ <TestComponent />
530
+ </RegistryProvider>
531
+ );
507
532
 
508
533
  expect( selectCount1 ).toHaveBeenCalledTimes( 0 );
509
534
  expect( selectCount2 ).toHaveBeenCalledTimes( 2 );
510
- expect( testInstance.findByType( 'div' ).props.data ).toBe(
535
+ expect( rendered.getByRole( 'status' ) ).toHaveTextContent(
511
536
  'count2'
512
537
  );
513
538
 
514
- act( () => {
515
- toggle();
516
- } );
539
+ rendered.getByText( 'Toggle' ).click();
517
540
 
518
541
  expect( selectCount1 ).toHaveBeenCalledTimes( 2 );
519
542
  expect( selectCount2 ).toHaveBeenCalledTimes( 2 );
520
- expect( testInstance.findByType( 'div' ).props.data ).toBe(
543
+ expect( rendered.getByRole( 'status' ) ).toHaveTextContent(
521
544
  'count1'
522
545
  );
523
546
  } );
@@ -528,8 +551,6 @@ describe( 'useSelect', () => {
528
551
  const subRegistry = createRegistry( {}, registry );
529
552
  subRegistry.registerStore( 'child-store', counterStore );
530
553
 
531
- let renderer;
532
-
533
554
  const TestComponent = jest.fn( () => {
534
555
  const state = useSelect(
535
556
  ( select ) => ( {
@@ -539,106 +560,95 @@ describe( 'useSelect', () => {
539
560
  []
540
561
  );
541
562
 
542
- return <div data={ state } />;
543
- } );
544
-
545
- act( () => {
546
- renderer = TestRenderer.create(
547
- <RegistryProvider value={ registry }>
548
- <RegistryProvider value={ subRegistry }>
549
- <TestComponent />
550
- </RegistryProvider>
551
- </RegistryProvider>
563
+ return (
564
+ <div role="status">
565
+ parent:{ state.parentCount },child:{ state.childCount }
566
+ </div>
552
567
  );
553
568
  } );
554
569
 
555
- const testInstance = renderer.root;
570
+ const rendered = render(
571
+ <RegistryProvider value={ registry }>
572
+ <RegistryProvider value={ subRegistry }>
573
+ <TestComponent />
574
+ </RegistryProvider>
575
+ </RegistryProvider>
576
+ );
556
577
 
557
- expect( testInstance.findByType( 'div' ).props.data ).toEqual( {
558
- parentCount: 0,
559
- childCount: 0,
560
- } );
578
+ expect( rendered.getByRole( 'status' ) ).toHaveTextContent(
579
+ 'parent:0,child:0'
580
+ );
561
581
 
562
582
  act( () => {
563
583
  registry.dispatch( 'parent-store' ).increment();
564
584
  } );
565
585
 
566
- expect( testInstance.findByType( 'div' ).props.data ).toEqual( {
567
- parentCount: 1,
568
- childCount: 0,
569
- } );
586
+ expect( rendered.getByRole( 'status' ) ).toHaveTextContent(
587
+ 'parent:1,child:0'
588
+ );
570
589
  } );
571
590
 
572
591
  it( 'handles non-existing stores', () => {
573
592
  registry.registerStore( 'store-1', counterStore );
574
593
 
575
- let renderer;
576
-
577
594
  const TestComponent = jest.fn( () => {
578
595
  const state = useSelect(
579
596
  ( select ) => ( {
580
597
  count1: select( 'store-1' ).getCounter(),
581
- blank: select( 'non-existing-store' )?.getCounter(),
598
+ count2: select( 'store-2' )?.getCounter() ?? 'blank',
582
599
  } ),
583
600
  []
584
601
  );
585
602
 
586
- return <div data={ state } />;
587
- } );
588
-
589
- act( () => {
590
- renderer = TestRenderer.create(
591
- <RegistryProvider value={ registry }>
592
- <TestComponent />
593
- </RegistryProvider>
603
+ return (
604
+ <div role="status">
605
+ count1:{ state.count1 },count2:{ state.count2 }
606
+ </div>
594
607
  );
595
608
  } );
596
609
 
597
- const testInstance = renderer.root;
610
+ const rendered = render(
611
+ <RegistryProvider value={ registry }>
612
+ <TestComponent />
613
+ </RegistryProvider>
614
+ );
598
615
 
599
- expect( testInstance.findByType( 'div' ).props.data ).toEqual( {
600
- count1: 0,
601
- blank: undefined,
602
- } );
616
+ expect( rendered.getByRole( 'status' ) ).toHaveTextContent(
617
+ 'count1:0,count2:blank'
618
+ );
603
619
 
604
620
  act( () => {
605
621
  registry.dispatch( 'store-1' ).increment();
606
622
  } );
607
623
 
608
- expect( testInstance.findByType( 'div' ).props.data ).toEqual( {
609
- count1: 1,
610
- blank: undefined,
611
- } );
624
+ expect( rendered.getByRole( 'status' ) ).toHaveTextContent(
625
+ 'count1:1,count2:blank'
626
+ );
612
627
 
613
628
  // Test if the unsubscribers get called correctly.
614
- renderer.unmount();
629
+ rendered.unmount();
615
630
  } );
616
631
 
617
632
  it( 'handles registration of a non-existing store during rendering', () => {
618
- let renderer;
619
-
620
633
  const TestComponent = jest.fn( () => {
621
634
  const state = useSelect(
622
635
  ( select ) =>
623
- select( 'not-yet-registered-store' )?.getCounter(),
636
+ select( 'not-yet-registered-store' )?.getCounter() ??
637
+ 'blank',
624
638
  []
625
639
  );
626
640
 
627
- return <div data={ state } />;
628
- } );
629
-
630
- act( () => {
631
- renderer = TestRenderer.create(
632
- <RegistryProvider value={ registry }>
633
- <TestComponent />
634
- </RegistryProvider>
635
- );
641
+ return <div role="status">{ state }</div>;
636
642
  } );
637
643
 
638
- const testInstance = renderer.root;
644
+ const rendered = render(
645
+ <RegistryProvider value={ registry }>
646
+ <TestComponent />
647
+ </RegistryProvider>
648
+ );
639
649
 
640
- expect( testInstance.findByType( 'div' ).props.data ).toBe(
641
- undefined
650
+ expect( rendered.getByRole( 'status' ) ).toHaveTextContent(
651
+ 'blank'
642
652
  );
643
653
 
644
654
  act( () => {
@@ -649,23 +659,21 @@ describe( 'useSelect', () => {
649
659
  } );
650
660
 
651
661
  // This is not ideal, but is the way it's working before and we want to prevent breaking changes.
652
- expect( testInstance.findByType( 'div' ).props.data ).toBe(
653
- undefined
662
+ expect( rendered.getByRole( 'status' ) ).toHaveTextContent(
663
+ 'blank'
654
664
  );
655
665
 
656
666
  act( () => {
657
667
  registry.dispatch( 'not-yet-registered-store' ).increment();
658
668
  } );
659
669
 
660
- expect( testInstance.findByType( 'div' ).props.data ).toBe( 1 );
670
+ expect( rendered.getByRole( 'status' ) ).toHaveTextContent( 1 );
661
671
 
662
672
  // Test if the unsubscribers get called correctly.
663
- renderer.unmount();
673
+ rendered.unmount();
664
674
  } );
665
675
 
666
676
  it( 'handles registration of a non-existing store of sub-registry during rendering', () => {
667
- let renderer;
668
-
669
677
  const subRegistry = createRegistry( {}, registry );
670
678
 
671
679
  const TestComponent = jest.fn( () => {
@@ -673,27 +681,23 @@ describe( 'useSelect', () => {
673
681
  ( select ) =>
674
682
  select(
675
683
  'not-yet-registered-child-store'
676
- )?.getCounter(),
684
+ )?.getCounter() ?? 'blank',
677
685
  []
678
686
  );
679
687
 
680
- return <div data={ state } />;
688
+ return <div role="status">{ state }</div>;
681
689
  } );
682
690
 
683
- act( () => {
684
- renderer = TestRenderer.create(
685
- <RegistryProvider value={ registry }>
686
- <RegistryProvider value={ subRegistry }>
687
- <TestComponent />
688
- </RegistryProvider>
691
+ const rendered = render(
692
+ <RegistryProvider value={ registry }>
693
+ <RegistryProvider value={ subRegistry }>
694
+ <TestComponent />
689
695
  </RegistryProvider>
690
- );
691
- } );
692
-
693
- const testInstance = renderer.root;
696
+ </RegistryProvider>
697
+ );
694
698
 
695
- expect( testInstance.findByType( 'div' ).props.data ).toBe(
696
- undefined
699
+ expect( rendered.getByRole( 'status' ) ).toHaveTextContent(
700
+ 'blank'
697
701
  );
698
702
 
699
703
  act( () => {
@@ -704,8 +708,8 @@ describe( 'useSelect', () => {
704
708
  } );
705
709
 
706
710
  // This is not ideal, but is the way it's working before and we want to prevent breaking changes.
707
- expect( testInstance.findByType( 'div' ).props.data ).toBe(
708
- undefined
711
+ expect( rendered.getByRole( 'status' ) ).toHaveTextContent(
712
+ 'blank'
709
713
  );
710
714
 
711
715
  act( () => {
@@ -714,15 +718,13 @@ describe( 'useSelect', () => {
714
718
  .increment();
715
719
  } );
716
720
 
717
- expect( testInstance.findByType( 'div' ).props.data ).toBe( 1 );
721
+ expect( rendered.getByRole( 'status' ) ).toHaveTextContent( '1' );
718
722
 
719
723
  // Test if the unsubscribers get called correctly.
720
- renderer.unmount();
724
+ rendered.unmount();
721
725
  } );
722
726
 
723
727
  it( 'handles custom generic stores without a unsubscribe function', () => {
724
- let renderer;
725
-
726
728
  const customStore = {
727
729
  name: 'generic-store',
728
730
  instantiate() {
@@ -762,28 +764,265 @@ describe( 'useSelect', () => {
762
764
  []
763
765
  );
764
766
 
765
- return <div data={ state } />;
767
+ return <div role="status">{ state }</div>;
766
768
  } );
767
769
 
770
+ const rendered = render(
771
+ <RegistryProvider value={ registry }>
772
+ <TestComponent />
773
+ </RegistryProvider>
774
+ );
775
+
776
+ expect( rendered.getByRole( 'status' ) ).toHaveTextContent( 0 );
777
+
768
778
  act( () => {
769
- renderer = TestRenderer.create(
779
+ registry.dispatch( customStore ).increment();
780
+ } );
781
+
782
+ expect( rendered.getByRole( 'status' ) ).toHaveTextContent( 1 );
783
+
784
+ expect( () => rendered.unmount() ).not.toThrow();
785
+ } );
786
+ } );
787
+
788
+ describe( 'async mode', () => {
789
+ function registerCounterStore( reg, initialCount = 0 ) {
790
+ reg.registerStore( 'counter', {
791
+ reducer: ( state = initialCount, action ) => {
792
+ if ( action.type === 'INCREMENT' ) {
793
+ return state + 1;
794
+ }
795
+ return state;
796
+ },
797
+ actions: {
798
+ inc: () => ( { type: 'INCREMENT' } ),
799
+ },
800
+ selectors: {
801
+ get: ( state ) => state,
802
+ },
803
+ } );
804
+ }
805
+
806
+ beforeEach( () => {
807
+ registerCounterStore( registry );
808
+ } );
809
+
810
+ it( 'renders with async mode', async () => {
811
+ const selectSpy = jest.fn( ( select ) =>
812
+ select( 'counter' ).get()
813
+ );
814
+
815
+ const TestComponent = jest.fn( () => {
816
+ const count = useSelect( selectSpy, [] );
817
+ return <div role="status">{ count }</div>;
818
+ } );
819
+
820
+ const rendered = render(
821
+ <AsyncModeProvider value={ true }>
770
822
  <RegistryProvider value={ registry }>
771
823
  <TestComponent />
772
824
  </RegistryProvider>
773
- );
825
+ </AsyncModeProvider>
826
+ );
827
+
828
+ // initial render + missed update catcher in subscribing effect
829
+ expect( selectSpy ).toHaveBeenCalledTimes( 2 );
830
+ expect( TestComponent ).toHaveBeenCalledTimes( 1 );
831
+
832
+ // Ensure expected state was rendered.
833
+ expect( rendered.getByRole( 'status' ) ).toHaveTextContent( 0 );
834
+
835
+ act( () => {
836
+ registry.dispatch( 'counter' ).inc();
837
+ } );
838
+
839
+ // still not called right after increment
840
+ expect( selectSpy ).toHaveBeenCalledTimes( 2 );
841
+ expect( rendered.getByRole( 'status' ) ).toHaveTextContent( 0 );
842
+
843
+ expect( await rendered.findByText( 1 ) ).toBeInTheDocument();
844
+
845
+ expect( selectSpy ).toHaveBeenCalledTimes( 3 );
846
+ expect( TestComponent ).toHaveBeenCalledTimes( 2 );
847
+ } );
848
+
849
+ // Tests render queue fixes done in https://github.com/WordPress/gutenberg/pull/19286
850
+ it( 'catches updates while switching from async to sync', () => {
851
+ const selectSpy = jest.fn( ( select ) =>
852
+ select( 'counter' ).get()
853
+ );
854
+
855
+ const TestComponent = jest.fn( () => {
856
+ const count = useSelect( selectSpy, [] );
857
+ return <div role="status">{ count }</div>;
858
+ } );
859
+
860
+ const App = ( { async } ) => (
861
+ <AsyncModeProvider value={ async }>
862
+ <RegistryProvider value={ registry }>
863
+ <TestComponent />
864
+ </RegistryProvider>
865
+ </AsyncModeProvider>
866
+ );
867
+
868
+ const rendered = render( <App async={ true } /> );
869
+
870
+ // Ensure expected state was rendered.
871
+ expect( rendered.getByRole( 'status' ) ).toHaveTextContent( 0 );
872
+
873
+ // Schedules an async update of the component.
874
+ act( () => {
875
+ registry.dispatch( 'counter' ).inc();
876
+ } );
877
+
878
+ // Ensure the async update wasn't processed yet.
879
+ expect( rendered.getByRole( 'status' ) ).toHaveTextContent( 0 );
880
+
881
+ // Switch from async mode to sync.
882
+ rendered.rerender( <App async={ false } /> );
883
+
884
+ // Ensure the async update was flushed during the rerender.
885
+ expect( rendered.getByRole( 'status' ) ).toHaveTextContent( 1 );
886
+
887
+ // initial render + subscription check + rerender with isAsync=false
888
+ expect( selectSpy ).toHaveBeenCalledTimes( 3 );
889
+ // initial render + rerender with isAsync=false
890
+ expect( TestComponent ).toHaveBeenCalledTimes( 2 );
891
+ } );
892
+
893
+ it( 'cancels scheduled updates when mapSelect function changes', async () => {
894
+ const selectA = jest.fn(
895
+ ( select ) => 'a:' + select( 'counter' ).get()
896
+ );
897
+ const selectB = jest.fn(
898
+ ( select ) => 'b:' + select( 'counter' ).get()
899
+ );
900
+
901
+ const TestComponent = jest.fn( ( { variant } ) => {
902
+ const count = useSelect( variant === 'a' ? selectA : selectB, [
903
+ variant,
904
+ ] );
905
+ return <div role="status">{ count }</div>;
774
906
  } );
775
907
 
776
- const testInstance = renderer.root;
908
+ const App = ( { variant } ) => (
909
+ <AsyncModeProvider value={ true }>
910
+ <RegistryProvider value={ registry }>
911
+ <TestComponent variant={ variant } />
912
+ </RegistryProvider>
913
+ </AsyncModeProvider>
914
+ );
915
+
916
+ const rendered = render( <App variant="a" /> );
777
917
 
778
- expect( testInstance.findByType( 'div' ).props.data ).toBe( 0 );
918
+ // Ensure expected state was rendered.
919
+ expect( rendered.getByRole( 'status' ) ).toHaveTextContent( 'a:0' );
779
920
 
921
+ // Schedules an async update of the component.
780
922
  act( () => {
781
- registry.dispatch( customStore ).increment();
923
+ registry.dispatch( 'counter' ).inc();
782
924
  } );
783
925
 
784
- expect( testInstance.findByType( 'div' ).props.data ).toBe( 1 );
926
+ // Ensure the async update wasn't processed yet.
927
+ expect( rendered.getByRole( 'status' ) ).toHaveTextContent( 'a:0' );
928
+
929
+ // Rerender with a prop change that causes dependency change.
930
+ rendered.rerender( <App variant="b" /> );
931
+
932
+ // Ensure the async update was flushed (cancelled) during the rerender.
933
+ expect( rendered.getByRole( 'status' ) ).toHaveTextContent( 'b:1' );
934
+
935
+ // Give the async update time to run in case it wasn't cancelled
936
+ await new Promise( setImmediate );
785
937
 
786
- expect( () => renderer.unmount() ).not.toThrow();
938
+ expect( selectA ).toHaveBeenCalledTimes( 2 );
939
+ expect( selectB ).toHaveBeenCalledTimes( 2 );
940
+ expect( TestComponent ).toHaveBeenCalledTimes( 2 );
941
+ } );
942
+
943
+ it( 'cancels scheduled updates when unmounting', async () => {
944
+ const selectSpy = jest.fn( ( select ) =>
945
+ select( 'counter' ).get()
946
+ );
947
+
948
+ const TestComponent = jest.fn( () => {
949
+ const count = useSelect( selectSpy, [] );
950
+ return <div role="status">{ count }</div>;
951
+ } );
952
+
953
+ const App = () => (
954
+ <AsyncModeProvider value={ true }>
955
+ <RegistryProvider value={ registry }>
956
+ <TestComponent />
957
+ </RegistryProvider>
958
+ </AsyncModeProvider>
959
+ );
960
+
961
+ const rendered = render( <App /> );
962
+
963
+ // Ensure expected state was rendered.
964
+ expect( rendered.getByRole( 'status' ) ).toHaveTextContent( 0 );
965
+
966
+ // Schedules an async update of the component.
967
+ act( () => {
968
+ registry.dispatch( 'counter' ).inc();
969
+ } );
970
+
971
+ // Ensure the async update wasn't processed yet.
972
+ expect( rendered.getByRole( 'status' ) ).toHaveTextContent( 0 );
973
+
974
+ // Unmount
975
+ rendered.unmount();
976
+
977
+ // Give the async update time to run in case it wasn't cancelled
978
+ await new Promise( setImmediate );
979
+
980
+ // only the initial render, no state updates
981
+ expect( selectSpy ).toHaveBeenCalledTimes( 2 );
982
+ expect( TestComponent ).toHaveBeenCalledTimes( 1 );
983
+ } );
984
+
985
+ it( 'cancels scheduled updates when registry changes', async () => {
986
+ const registry2 = createRegistry();
987
+ registerCounterStore( registry2, 100 );
988
+
989
+ const selectSpy = jest.fn( ( select ) =>
990
+ select( 'counter' ).get()
991
+ );
992
+
993
+ const TestComponent = jest.fn( () => {
994
+ const count = useSelect( selectSpy, [] );
995
+ return <div role="status">{ count }</div>;
996
+ } );
997
+
998
+ const App = ( { reg } ) => (
999
+ <AsyncModeProvider value={ true }>
1000
+ <RegistryProvider value={ reg }>
1001
+ <TestComponent />
1002
+ </RegistryProvider>
1003
+ </AsyncModeProvider>
1004
+ );
1005
+
1006
+ const rendered = render( <App reg={ registry } /> );
1007
+
1008
+ expect( rendered.getByRole( 'status' ) ).toHaveTextContent( 0 );
1009
+
1010
+ act( () => {
1011
+ registry.dispatch( 'counter' ).inc();
1012
+ } );
1013
+
1014
+ expect( rendered.getByRole( 'status' ) ).toHaveTextContent( 0 );
1015
+
1016
+ rendered.rerender( <App reg={ registry2 } /> );
1017
+
1018
+ expect( rendered.getByRole( 'status' ) ).toHaveTextContent( 100 );
1019
+
1020
+ // Give the async update time to run in case it wasn't cancelled
1021
+ await new Promise( setImmediate );
1022
+
1023
+ // initial render + registry change rerender, no state updates
1024
+ expect( selectSpy ).toHaveBeenCalledTimes( 4 );
1025
+ expect( TestComponent ).toHaveBeenCalledTimes( 2 );
787
1026
  } );
788
1027
  } );
789
1028
  } );