@@ -28,8 +28,12 @@ function createOerItem(name: string): OerItem {
2828 } ;
2929}
3030
31- function createSearchResult ( items : OerItem [ ] , page : number , total : number ) : SearchResult {
32- const pageSize = 20 ;
31+ function createSearchResult (
32+ items : OerItem [ ] ,
33+ page : number ,
34+ total : number ,
35+ pageSize = 20 ,
36+ ) : SearchResult {
3337 return {
3438 data : items ,
3539 meta : {
@@ -41,6 +45,10 @@ function createSearchResult(items: OerItem[], page: number, total: number): Sear
4145 } ;
4246}
4347
48+ function createOerItems ( prefix : string , count : number ) : OerItem [ ] {
49+ return Array . from ( { length : count } , ( _ , i ) => createOerItem ( `${ prefix } -${ i + 1 } ` ) ) ;
50+ }
51+
4452function createMockClient (
4553 searchFn ?: ( params : SearchParams ) => Promise < SearchResult > ,
4654 availableSources ?: { id : string ; label : string ; checked ?: boolean } [ ] ,
@@ -144,6 +152,81 @@ describe('OerSearch', () => {
144152 expect ( loadingFired ) . toBe ( true ) ;
145153 } ) ;
146154
155+ it ( 'caps multi-source results at pageSize' , async ( ) => {
156+ const pageSize = 10 ;
157+ const sourceAItems = createOerItems ( 'SourceA' , pageSize ) ;
158+ const sourceBItems = createOerItems ( 'SourceB' , pageSize ) ;
159+
160+ const mockClient = createMockClient (
161+ ( params : SearchParams ) => {
162+ if ( params . source === 'source-a' ) {
163+ return Promise . resolve ( createSearchResult ( sourceAItems , 1 , 40 , pageSize ) ) ;
164+ }
165+ return Promise . resolve ( createSearchResult ( sourceBItems , 1 , 40 , pageSize ) ) ;
166+ } ,
167+ [
168+ { id : 'source-a' , label : 'Source A' } ,
169+ { id : 'source-b' , label : 'Source B' } ,
170+ ] ,
171+ ) ;
172+
173+ createSpy . mockReturnValue ( mockClient ) ;
174+
175+ search = document . createElement ( 'oer-search' ) as OerSearchElement ;
176+ search . language = 'en' ;
177+ search . setAttribute ( 'page-size' , String ( pageSize ) ) ;
178+ document . body . appendChild ( search ) ;
179+ await new Promise ( ( resolve ) => setTimeout ( resolve , 0 ) ) ;
180+
181+ const resultPromise = awaitSearchResult ( search ) ;
182+ triggerSearch ( search , 'test' ) ;
183+
184+ const result = await resultPromise ;
185+ // 2 sources each return 10 items = 20 total, but should be capped at pageSize (10)
186+ expect ( result . data ) . toHaveLength ( pageSize ) ;
187+ } ) ;
188+
189+ it ( 'interleaves multi-source results in round-robin order within pageSize' , async ( ) => {
190+ const pageSize = 6 ;
191+ const sourceAItems = createOerItems ( 'A' , pageSize ) ;
192+ const sourceBItems = createOerItems ( 'B' , pageSize ) ;
193+
194+ const mockClient = createMockClient (
195+ ( params : SearchParams ) => {
196+ if ( params . source === 'source-a' ) {
197+ return Promise . resolve ( createSearchResult ( sourceAItems , 1 , 40 , pageSize ) ) ;
198+ }
199+ return Promise . resolve ( createSearchResult ( sourceBItems , 1 , 40 , pageSize ) ) ;
200+ } ,
201+ [
202+ { id : 'source-a' , label : 'Source A' } ,
203+ { id : 'source-b' , label : 'Source B' } ,
204+ ] ,
205+ ) ;
206+
207+ createSpy . mockReturnValue ( mockClient ) ;
208+
209+ search = document . createElement ( 'oer-search' ) as OerSearchElement ;
210+ search . language = 'en' ;
211+ search . setAttribute ( 'page-size' , String ( pageSize ) ) ;
212+ document . body . appendChild ( search ) ;
213+ await new Promise ( ( resolve ) => setTimeout ( resolve , 0 ) ) ;
214+
215+ const resultPromise = awaitSearchResult ( search ) ;
216+ triggerSearch ( search , 'test' ) ;
217+
218+ const result = await resultPromise ;
219+ // Round-robin: A-1, B-1, A-2, B-2, A-3, B-3
220+ expect ( result . data . map ( ( d ) => d . amb . name ) ) . toEqual ( [
221+ 'A-1' ,
222+ 'B-1' ,
223+ 'A-2' ,
224+ 'B-2' ,
225+ 'A-3' ,
226+ 'B-3' ,
227+ ] ) ;
228+ } ) ;
229+
147230 it ( 'dispatches search-results with empty data when source fails' , async ( ) => {
148231 const mockClient = createMockClient ( ( ) => Promise . reject ( new Error ( 'Network failure' ) ) ) ;
149232
@@ -187,6 +270,95 @@ describe('OerSearch', () => {
187270 expect ( secondResult . data ) . toEqual ( [ ...page1Items , ...page2Items ] ) ;
188271 } ) ;
189272
273+ it ( 'caps multi-source load-more results at pageSize' , async ( ) => {
274+ const pageSize = 6 ;
275+
276+ const mockClient = createMockClient (
277+ ( params : SearchParams ) => {
278+ const sourcePrefix = params . source === 'source-a' ? 'A' : 'B' ;
279+ const items = createOerItems ( `${ sourcePrefix } -p${ params . page } ` , pageSize ) ;
280+ return Promise . resolve ( createSearchResult ( items , params . page ?? 1 , 60 , pageSize ) ) ;
281+ } ,
282+ [
283+ { id : 'source-a' , label : 'Source A' } ,
284+ { id : 'source-b' , label : 'Source B' } ,
285+ ] ,
286+ ) ;
287+
288+ createSpy . mockReturnValue ( mockClient ) ;
289+
290+ search = document . createElement ( 'oer-search' ) as OerSearchElement ;
291+ search . language = 'en' ;
292+ search . setAttribute ( 'page-size' , String ( pageSize ) ) ;
293+ document . body . appendChild ( search ) ;
294+ await new Promise ( ( resolve ) => setTimeout ( resolve , 0 ) ) ;
295+
296+ // Initial search
297+ const firstResultPromise = awaitSearchResult ( search ) ;
298+ triggerSearch ( search , 'test' ) ;
299+ const firstResult = await firstResultPromise ;
300+
301+ // First search should be capped at pageSize
302+ expect ( firstResult . data ) . toHaveLength ( pageSize ) ;
303+
304+ // Load more — should drain overflow buffer, no new fetch
305+ const secondResultPromise = awaitSearchResult ( search ) ;
306+ search . dispatchEvent ( new CustomEvent ( 'load-more' , { bubbles : true , composed : true } ) ) ;
307+ const secondResult = await secondResultPromise ;
308+
309+ // After load-more: pageSize (initial) + pageSize (from buffer) = 12 total
310+ expect ( secondResult . data ) . toHaveLength ( pageSize * 2 ) ;
311+ } ) ;
312+
313+ it ( 'drains overflow buffer before fetching new pages' , async ( ) => {
314+ const pageSize = 6 ;
315+ const searchCalls : SearchParams [ ] = [ ] ;
316+
317+ const mockClient = createMockClient (
318+ ( params : SearchParams ) => {
319+ searchCalls . push ( { ...params } ) ;
320+ const sourcePrefix = params . source === 'source-a' ? 'A' : 'B' ;
321+ const items = createOerItems ( `${ sourcePrefix } -p${ params . page } ` , pageSize ) ;
322+ return Promise . resolve ( createSearchResult ( items , params . page ?? 1 , 60 , pageSize ) ) ;
323+ } ,
324+ [
325+ { id : 'source-a' , label : 'Source A' } ,
326+ { id : 'source-b' , label : 'Source B' } ,
327+ ] ,
328+ ) ;
329+
330+ createSpy . mockReturnValue ( mockClient ) ;
331+
332+ search = document . createElement ( 'oer-search' ) as OerSearchElement ;
333+ search . language = 'en' ;
334+ search . setAttribute ( 'page-size' , String ( pageSize ) ) ;
335+ document . body . appendChild ( search ) ;
336+ await new Promise ( ( resolve ) => setTimeout ( resolve , 0 ) ) ;
337+
338+ // Initial search: 2 sources × 6 items = 12 fetched, 6 shown, 6 buffered
339+ const firstResultPromise = awaitSearchResult ( search ) ;
340+ triggerSearch ( search , 'test' ) ;
341+ await firstResultPromise ;
342+
343+ // Record call count after initial search
344+ const callsAfterSearch = searchCalls . length ;
345+
346+ // First load-more should drain buffer — no new fetch
347+ const secondResultPromise = awaitSearchResult ( search ) ;
348+ search . dispatchEvent ( new CustomEvent ( 'load-more' , { bubbles : true , composed : true } ) ) ;
349+ await secondResultPromise ;
350+
351+ expect ( searchCalls . length ) . toBe ( callsAfterSearch ) ;
352+
353+ // Second load-more: buffer is empty, should trigger fetch (page 2)
354+ const thirdResultPromise = awaitSearchResult ( search ) ;
355+ search . dispatchEvent ( new CustomEvent ( 'load-more' , { bubbles : true , composed : true } ) ) ;
356+ await thirdResultPromise ;
357+
358+ const page2Calls = searchCalls . slice ( callsAfterSearch ) ;
359+ expect ( page2Calls . every ( ( c ) => c . page === 2 ) ) . toBe ( true ) ;
360+ } ) ;
361+
190362 it ( 'sends correct page number to the client on load-more' , async ( ) => {
191363 const searchCalls : SearchParams [ ] = [ ] ;
192364 const mockClient = createMockClient ( ( params : SearchParams ) => {
@@ -737,7 +909,20 @@ describe('OerSearch', () => {
737909 } ) ;
738910
739911 describe ( 'lockedType' , ( ) => {
740- it ( 'sends the locked type in search params and hides type dropdown' , async ( ) => {
912+ it ( 'hides the type dropdown when locked-type is set' , async ( ) => {
913+ createSpy . mockReturnValue ( createMockClient ( ) ) ;
914+
915+ search = document . createElement ( 'oer-search' ) as OerSearchElement ;
916+ search . language = 'en' ;
917+ search . setAttribute ( 'locked-type' , 'image' ) ;
918+ document . body . appendChild ( search ) ;
919+ await new Promise ( ( resolve ) => setTimeout ( resolve , 0 ) ) ;
920+
921+ const typeSelect = search . shadowRoot ?. querySelector ( '#type' ) ;
922+ expect ( typeSelect ) . toBeNull ( ) ;
923+ } ) ;
924+
925+ it ( 'sends the locked type in search params' , async ( ) => {
741926 const capturedParams : SearchParams [ ] = [ ] ;
742927 const mockClient = createMockClient ( ( params : SearchParams ) => {
743928 capturedParams . push ( { ...params } ) ;
@@ -752,10 +937,6 @@ describe('OerSearch', () => {
752937 document . body . appendChild ( search ) ;
753938 await new Promise ( ( resolve ) => setTimeout ( resolve , 0 ) ) ;
754939
755- // Type dropdown should be hidden
756- const typeSelect = search . shadowRoot ?. querySelector ( '#type' ) ;
757- expect ( typeSelect ) . toBeNull ( ) ;
758-
759940 const resultPromise = awaitSearchResult ( search ) ;
760941 triggerSearch ( search , 'test' ) ;
761942 await resultPromise ;
0 commit comments