@@ -10,13 +10,14 @@ import {
10
10
render ,
11
11
waitFor ,
12
12
screen ,
13
+ within ,
13
14
} from '@testing-library/react' ;
14
15
import fetchMock from 'fetch-mock-jest' ;
15
16
import initializeStore from '../store' ;
16
17
import { getContentSearchConfigUrl } from '../search-manager/data/api' ;
17
18
import mockResult from '../search-modal/__mocks__/search-result.json' ;
18
19
import mockEmptyResult from '../search-modal/__mocks__/empty-search-result.json' ;
19
- import { getContentLibraryApiUrl , type ContentLibrary } from './data/api' ;
20
+ import { getContentLibraryApiUrl , getXBlockFieldsApiUrl , type ContentLibrary } from './data/api' ;
20
21
import { LibraryLayout } from '.' ;
21
22
22
23
let store ;
@@ -61,16 +62,17 @@ const returnEmptyResult = (_url, req) => {
61
62
const returnLowNumberResults = ( _url , req ) => {
62
63
const requestData = JSON . parse ( req . body ?. toString ( ) ?? '' ) ;
63
64
const query = requestData ?. queries [ 0 ] ?. q ?? '' ;
65
+ const newMockResult = { ...mockResult } ;
64
66
// We have to replace the query (search keywords) in the mock results with the actual query,
65
67
// because otherwise we may have an inconsistent state that causes more queries and unexpected results.
66
- mockResult . results [ 0 ] . query = query ;
68
+ newMockResult . results [ 0 ] . query = query ;
67
69
// Limit number of results to just 2
68
- mockResult . results [ 0 ] . hits = mockResult . results [ 0 ] ?. hits . slice ( 0 , 2 ) ;
69
- mockResult . results [ 0 ] . estimatedTotalHits = 2 ;
70
+ newMockResult . results [ 0 ] . hits = mockResult . results [ 0 ] ?. hits . slice ( 0 , 2 ) ;
71
+ newMockResult . results [ 0 ] . estimatedTotalHits = 2 ;
70
72
// And fake the required '_formatted' fields; it contains the highlighting <mark>...</mark> around matched words
71
73
// eslint-disable-next-line no-underscore-dangle, no-param-reassign
72
- mockResult . results [ 0 ] ?. hits . forEach ( ( hit ) => { hit . _formatted = { ...hit } ; } ) ;
73
- return mockResult ;
74
+ newMockResult . results [ 0 ] ?. hits . forEach ( ( hit ) => { hit . _formatted = { ...hit } ; } ) ;
75
+ return newMockResult ;
74
76
} ;
75
77
76
78
const libraryData : ContentLibrary = {
@@ -97,6 +99,13 @@ const libraryData: ContentLibrary = {
97
99
updated : '2024-07-20' ,
98
100
} ;
99
101
102
+ const xBlockFields = {
103
+ display_name : 'Test HTML Block' ,
104
+ metadata : {
105
+ display_name : 'Test HTML Block' ,
106
+ } ,
107
+ } ;
108
+
100
109
const clipboardBroadcastChannelMock = {
101
110
postMessage : jest . fn ( ) ,
102
111
close : jest . fn ( ) ,
@@ -158,6 +167,19 @@ describe('<LibraryAuthoringPage />', () => {
158
167
queryClient . clear ( ) ;
159
168
} ) ;
160
169
170
+ const renderLibraryPage = async ( ) => {
171
+ mockUseParams . mockReturnValue ( { libraryId : libraryData . id } ) ;
172
+ axiosMock . onGet ( getContentLibraryApiUrl ( libraryData . id ) ) . reply ( 200 , libraryData ) ;
173
+
174
+ const result = render ( < RootWrapper /> ) ;
175
+
176
+ // Ensure the search endpoint is called:
177
+ // Call 1: To fetch searchable/filterable/sortable library data
178
+ await waitFor ( ( ) => { expect ( fetchMock ) . toHaveFetchedTimes ( 1 , searchEndpoint , 'post' ) ; } ) ;
179
+
180
+ return result ;
181
+ } ;
182
+
161
183
it ( 'shows the spinner before the query is complete' , ( ) => {
162
184
mockUseParams . mockReturnValue ( { libraryId : '1' } ) ;
163
185
// @ts -ignore Use unresolved promise to keep the Loading visible
@@ -185,12 +207,9 @@ describe('<LibraryAuthoringPage />', () => {
185
207
} ) ;
186
208
187
209
it ( 'show library data' , async ( ) => {
188
- mockUseParams . mockReturnValue ( { libraryId : libraryData . id } ) ;
189
- axiosMock . onGet ( getContentLibraryApiUrl ( libraryData . id ) ) . reply ( 200 , libraryData ) ;
190
-
191
210
const {
192
211
getByRole, getAllByText, getByText, queryByText, findByText, findAllByText,
193
- } = render ( < RootWrapper /> ) ;
212
+ } = await renderLibraryPage ( ) ;
194
213
195
214
await waitFor ( ( ) => { expect ( fetchMock ) . toHaveFetchedTimes ( 1 , searchEndpoint , 'post' ) ; } ) ;
196
215
@@ -263,10 +282,7 @@ describe('<LibraryAuthoringPage />', () => {
263
282
} ) ;
264
283
265
284
it ( 'show new content button' , async ( ) => {
266
- mockUseParams . mockReturnValue ( { libraryId : libraryData . id } ) ;
267
- axiosMock . onGet ( getContentLibraryApiUrl ( libraryData . id ) ) . reply ( 200 , libraryData ) ;
268
-
269
- render ( < RootWrapper /> ) ;
285
+ await renderLibraryPage ( ) ;
270
286
271
287
expect ( await screen . findByRole ( 'heading' ) ) . toBeInTheDocument ( ) ;
272
288
expect ( screen . getByRole ( 'button' , { name : / n e w / i } ) ) . toBeInTheDocument ( ) ;
@@ -322,10 +338,7 @@ describe('<LibraryAuthoringPage />', () => {
322
338
} ) ;
323
339
324
340
it ( 'should open and close new content sidebar' , async ( ) => {
325
- mockUseParams . mockReturnValue ( { libraryId : libraryData . id } ) ;
326
- axiosMock . onGet ( getContentLibraryApiUrl ( libraryData . id ) ) . reply ( 200 , libraryData ) ;
327
-
328
- render ( < RootWrapper /> ) ;
341
+ await renderLibraryPage ( ) ;
329
342
330
343
expect ( await screen . findByRole ( 'heading' ) ) . toBeInTheDocument ( ) ;
331
344
expect ( screen . queryByText ( / a d d c o n t e n t / i) ) . not . toBeInTheDocument ( ) ;
@@ -342,10 +355,7 @@ describe('<LibraryAuthoringPage />', () => {
342
355
} ) ;
343
356
344
357
it ( 'should open Library Info by default' , async ( ) => {
345
- mockUseParams . mockReturnValue ( { libraryId : libraryData . id } ) ;
346
- axiosMock . onGet ( getContentLibraryApiUrl ( libraryData . id ) ) . reply ( 200 , libraryData ) ;
347
-
348
- render ( < RootWrapper /> ) ;
358
+ await renderLibraryPage ( ) ;
349
359
350
360
expect ( await screen . findByText ( 'Content library' ) ) . toBeInTheDocument ( ) ;
351
361
expect ( ( await screen . findAllByText ( libraryData . title ) ) [ 0 ] ) . toBeInTheDocument ( ) ;
@@ -361,10 +371,7 @@ describe('<LibraryAuthoringPage />', () => {
361
371
} ) ;
362
372
363
373
it ( 'should close and open Library Info' , async ( ) => {
364
- mockUseParams . mockReturnValue ( { libraryId : libraryData . id } ) ;
365
- axiosMock . onGet ( getContentLibraryApiUrl ( libraryData . id ) ) . reply ( 200 , libraryData ) ;
366
-
367
- render ( < RootWrapper /> ) ;
374
+ await renderLibraryPage ( ) ;
368
375
369
376
expect ( await screen . findByText ( 'Content library' ) ) . toBeInTheDocument ( ) ;
370
377
expect ( ( await screen . findAllByText ( libraryData . title ) ) [ 0 ] ) . toBeInTheDocument ( ) ;
@@ -389,14 +396,9 @@ describe('<LibraryAuthoringPage />', () => {
389
396
} ) ;
390
397
391
398
it ( 'show the "View All" button when viewing library with many components' , async ( ) => {
392
- mockUseParams . mockReturnValue ( { libraryId : libraryData . id } ) ;
393
- axiosMock . onGet ( getContentLibraryApiUrl ( libraryData . id ) ) . reply ( 200 , libraryData ) ;
394
-
395
399
const {
396
400
getByRole, getByText, queryByText, getAllByText, findAllByText,
397
- } = render ( < RootWrapper /> ) ;
398
-
399
- await waitFor ( ( ) => { expect ( fetchMock ) . toHaveFetchedTimes ( 1 , searchEndpoint , 'post' ) ; } ) ;
401
+ } = await renderLibraryPage ( ) ;
400
402
401
403
expect ( getByText ( 'Content library' ) ) . toBeInTheDocument ( ) ;
402
404
expect ( ( await findAllByText ( libraryData . title ) ) [ 0 ] ) . toBeInTheDocument ( ) ;
@@ -456,13 +458,9 @@ describe('<LibraryAuthoringPage />', () => {
456
458
} ) ;
457
459
458
460
it ( 'sort library components' , async ( ) => {
459
- mockUseParams . mockReturnValue ( { libraryId : libraryData . id } ) ;
460
- axiosMock . onGet ( getContentLibraryApiUrl ( libraryData . id ) ) . reply ( 200 , libraryData ) ;
461
- fetchMock . post ( searchEndpoint , returnEmptyResult , { overwriteRoutes : true } ) ;
462
-
463
461
const {
464
462
findByTitle, getAllByText, getByRole, getByTitle,
465
- } = render ( < RootWrapper /> ) ;
463
+ } = await renderLibraryPage ( ) ;
466
464
467
465
expect ( await findByTitle ( 'Sort search results' ) ) . toBeInTheDocument ( ) ;
468
466
@@ -514,7 +512,7 @@ describe('<LibraryAuthoringPage />', () => {
514
512
515
513
// Re-selecting the previous sort option resets sort to default "Recently Modified"
516
514
await testSortOption ( 'Recently Published' , 'modified:desc' , true ) ;
517
- expect ( getAllByText ( 'Recently Modified' ) . length ) . toEqual ( 2 ) ;
515
+ expect ( getAllByText ( 'Recently Modified' ) . length ) . toEqual ( 3 ) ;
518
516
519
517
// Enter a keyword into the search box
520
518
const searchBox = getByRole ( 'searchbox' ) ;
@@ -530,4 +528,129 @@ describe('<LibraryAuthoringPage />', () => {
530
528
} ) ;
531
529
} ) ;
532
530
} ) ;
531
+
532
+ it ( 'should open and close the component sidebar' , async ( ) => {
533
+ const usageKey = mockResult . results [ 0 ] . hits [ 0 ] . usage_key ;
534
+ const { getAllByText, queryByTestId, queryByText } = await renderLibraryPage ( ) ;
535
+ axiosMock . onGet ( getXBlockFieldsApiUrl ( usageKey ) ) . reply ( 200 , xBlockFields ) ;
536
+
537
+ // Click on the first component
538
+ waitFor ( ( ) => expect ( queryByText ( 'Test HTML Block' ) ) . toBeInTheDocument ( ) ) ;
539
+ fireEvent . click ( getAllByText ( 'Test HTML Block' ) [ 0 ] ) ;
540
+
541
+ const sidebar = screen . getByTestId ( 'library-sidebar' ) ;
542
+
543
+ const { getByRole, getByText } = within ( sidebar ) ;
544
+
545
+ await waitFor ( ( ) => expect ( getByText ( 'Test HTML Block' ) ) . toBeInTheDocument ( ) ) ;
546
+
547
+ const closeButton = getByRole ( 'button' , { name : / c l o s e / i } ) ;
548
+ fireEvent . click ( closeButton ) ;
549
+
550
+ await waitFor ( ( ) => expect ( queryByTestId ( 'library-sidebar' ) ) . not . toBeInTheDocument ( ) ) ;
551
+ } ) ;
552
+
553
+ it ( 'filter by capa problem type' , async ( ) => {
554
+ mockUseParams . mockReturnValue ( { libraryId : libraryData . id } ) ;
555
+ axiosMock . onGet ( getContentLibraryApiUrl ( libraryData . id ) ) . reply ( 200 , libraryData ) ;
556
+
557
+ const problemTypes = {
558
+ 'Multiple Choice' : 'choiceresponse' ,
559
+ Checkboxes : 'multiplechoiceresponse' ,
560
+ 'Numerical Input' : 'numericalresponse' ,
561
+ Dropdown : 'optionresponse' ,
562
+ 'Text Input' : 'stringresponse' ,
563
+ } ;
564
+
565
+ render ( < RootWrapper /> ) ;
566
+
567
+ // Ensure the search endpoint is called
568
+ await waitFor ( ( ) => { expect ( fetchMock ) . toHaveFetchedTimes ( 1 , searchEndpoint , 'post' ) ; } ) ;
569
+ const filterButton = screen . getByRole ( 'button' , { name : / t y p e / i } ) ;
570
+ fireEvent . click ( filterButton ) ;
571
+
572
+ const openProblemItem = screen . getByTestId ( 'open-problem-item-button' ) ;
573
+ fireEvent . click ( openProblemItem ) ;
574
+
575
+ const validateSubmenu = async ( submenuText : string ) => {
576
+ const submenu = screen . getByText ( submenuText ) ;
577
+ expect ( submenu ) . toBeInTheDocument ( ) ;
578
+ fireEvent . click ( submenu ) ;
579
+
580
+ await waitFor ( ( ) => {
581
+ expect ( fetchMock ) . toHaveBeenLastCalledWith ( searchEndpoint , {
582
+ body : expect . stringContaining ( `content.problem_types = ${ problemTypes [ submenuText ] } ` ) ,
583
+ method : 'POST' ,
584
+ headers : expect . anything ( ) ,
585
+ } ) ;
586
+ } ) ;
587
+
588
+ fireEvent . click ( submenu ) ;
589
+ await waitFor ( ( ) => {
590
+ expect ( fetchMock ) . toHaveBeenLastCalledWith ( searchEndpoint , {
591
+ body : expect . not . stringContaining ( `content.problem_types = ${ problemTypes [ submenuText ] } ` ) ,
592
+ method : 'POST' ,
593
+ headers : expect . anything ( ) ,
594
+ } ) ;
595
+ } ) ;
596
+ } ;
597
+
598
+ // Validate per submenu
599
+ // eslint-disable-next-line no-restricted-syntax
600
+ for ( const key of Object . keys ( problemTypes ) ) {
601
+ // eslint-disable-next-line no-await-in-loop
602
+ await validateSubmenu ( key ) ;
603
+ }
604
+
605
+ // Validate click on Problem type
606
+ const problemMenu = screen . getByText ( 'Problem' ) ;
607
+ expect ( problemMenu ) . toBeInTheDocument ( ) ;
608
+ fireEvent . click ( problemMenu ) ;
609
+ await waitFor ( ( ) => {
610
+ expect ( fetchMock ) . toHaveBeenLastCalledWith ( searchEndpoint , {
611
+ body : expect . stringContaining ( 'block_type = problem' ) ,
612
+ method : 'POST' ,
613
+ headers : expect . anything ( ) ,
614
+ } ) ;
615
+ } ) ;
616
+
617
+ fireEvent . click ( problemMenu ) ;
618
+ await waitFor ( ( ) => {
619
+ expect ( fetchMock ) . toHaveBeenLastCalledWith ( searchEndpoint , {
620
+ body : expect . not . stringContaining ( 'block_type = problem' ) ,
621
+ method : 'POST' ,
622
+ headers : expect . anything ( ) ,
623
+ } ) ;
624
+ } ) ;
625
+
626
+ // Validate clear filters
627
+ const submenu = screen . getByText ( 'Checkboxes' ) ;
628
+ expect ( submenu ) . toBeInTheDocument ( ) ;
629
+ fireEvent . click ( submenu ) ;
630
+
631
+ const clearFitlersButton = screen . getByRole ( 'button' , { name : / c l e a r f i l t e r s / i } ) ;
632
+ fireEvent . click ( clearFitlersButton ) ;
633
+ await waitFor ( ( ) => {
634
+ expect ( fetchMock ) . toHaveBeenLastCalledWith ( searchEndpoint , {
635
+ body : expect . not . stringContaining ( `content.problem_types = ${ problemTypes . Checkboxes } ` ) ,
636
+ method : 'POST' ,
637
+ headers : expect . anything ( ) ,
638
+ } ) ;
639
+ } ) ;
640
+ } ) ;
641
+
642
+ it ( 'empty type filter' , async ( ) => {
643
+ mockUseParams . mockReturnValue ( { libraryId : libraryData . id } ) ;
644
+ axiosMock . onGet ( getContentLibraryApiUrl ( libraryData . id ) ) . reply ( 200 , libraryData ) ;
645
+ fetchMock . post ( searchEndpoint , returnEmptyResult , { overwriteRoutes : true } ) ;
646
+
647
+ render ( < RootWrapper /> ) ;
648
+
649
+ await waitFor ( ( ) => { expect ( fetchMock ) . toHaveFetchedTimes ( 1 , searchEndpoint , 'post' ) ; } ) ;
650
+
651
+ const filterButton = screen . getByRole ( 'button' , { name : / t y p e / i } ) ;
652
+ fireEvent . click ( filterButton ) ;
653
+
654
+ expect ( screen . getByText ( / n o m a t c h i n g c o m p o n e n t s / i) ) . toBeInTheDocument ( ) ;
655
+ } ) ;
533
656
} ) ;
0 commit comments