@@ -104,10 +104,17 @@ describe('package versions page', () => {
104104 beta : '1.0.0-beta.1' ,
105105 } )
106106 const component = await mountPage ( )
107- await vi . waitFor ( ( ) => {
108- expect ( component . text ( ) ) . toContain ( 'stable' )
109- expect ( component . text ( ) ) . toContain ( 'beta' )
110- } )
107+
108+ // stable is a non-prerelease tag — visible by default
109+ await vi . waitFor ( ( ) => expect ( component . text ( ) ) . toContain ( 'stable' ) )
110+
111+ // beta points to a prerelease version — hidden by default, revealed via "Show all"
112+ expect ( component . text ( ) ) . not . toContain ( 'beta' )
113+ const showAllButton = component . findAll ( 'button' ) . find ( b => b . text ( ) . includes ( 'Show all' ) )
114+ expect ( showAllButton ) . toBeDefined ( )
115+ await showAllButton ! . trigger ( 'click' )
116+
117+ await vi . waitFor ( ( ) => expect ( component . text ( ) ) . toContain ( 'beta' ) )
111118 } )
112119 } )
113120
@@ -164,7 +171,6 @@ describe('package versions page', () => {
164171
165172 describe ( 'version filter' , ( ) => {
166173 it ( 'filters groups by substring match' , async ( ) => {
167- // Use versions where the filter string "1.0" is unique to the 1.x group
168174 nextFetchResponse = makeVersionData ( [ '3.0.0' , '2.0.0' , '1.0.0' ] , { latest : '3.0.0' } )
169175 const component = await mountPage ( )
170176 await vi . waitFor ( ( ) => {
@@ -182,5 +188,287 @@ describe('package versions page', () => {
182188 expect ( component . text ( ) ) . not . toContain ( '3.x' )
183189 } )
184190 } )
191+
192+ it ( 'filters groups by semver range' , async ( ) => {
193+ nextFetchResponse = makeVersionData ( [ '3.0.0' , '2.1.0' , '2.0.0' , '1.0.0' ] , {
194+ latest : '3.0.0' ,
195+ } )
196+ const component = await mountPage ( )
197+ await vi . waitFor ( ( ) => expect ( component . text ( ) ) . toContain ( '3.x' ) )
198+
199+ const input = component . find ( 'input[autocomplete="off"]' )
200+ await input . setValue ( '>=2.0.0 <3.0.0' )
201+
202+ await vi . waitFor ( ( ) => {
203+ expect ( component . text ( ) ) . toContain ( '2.x' )
204+ expect ( component . text ( ) ) . not . toContain ( '1.x' )
205+ expect ( component . text ( ) ) . not . toContain ( '3.x' )
206+ } )
207+ } )
208+
209+ it ( 'shows no-match message when filter matches nothing' , async ( ) => {
210+ nextFetchResponse = makeVersionData ( [ '2.0.0' , '1.0.0' ] , { latest : '2.0.0' } )
211+ const component = await mountPage ( )
212+ await vi . waitFor ( ( ) => expect ( component . text ( ) ) . toContain ( '2.x' ) )
213+
214+ const input = component . find ( 'input[autocomplete="off"]' )
215+ await input . setValue ( '9.9.9' )
216+
217+ await vi . waitFor ( ( ) => {
218+ expect ( component . text ( ) ) . not . toContain ( '1.x' )
219+ expect ( component . text ( ) ) . not . toContain ( '2.x' )
220+ // no-match status message rendered
221+ expect ( component . find ( '[role="status"]' ) . exists ( ) ) . toBe ( true )
222+ } )
223+ } )
224+
225+ it ( 'shows error indicator for an invalid semver range' , async ( ) => {
226+ nextFetchResponse = makeVersionData ( [ '1.0.0' ] , { latest : '1.0.0' } )
227+ const component = await mountPage ( )
228+ await vi . waitFor ( ( ) => expect ( component . text ( ) ) . toContain ( '1.x' ) )
229+
230+ const input = component . find ( 'input[autocomplete="off"]' )
231+ await input . setValue ( 'not-a-range!!!' )
232+
233+ await vi . waitFor ( ( ) => {
234+ expect ( input . attributes ( 'aria-invalid' ) ) . toBe ( 'true' )
235+ } )
236+ } )
237+
238+ it ( 'clearing the filter restores all groups' , async ( ) => {
239+ nextFetchResponse = makeVersionData ( [ '2.0.0' , '1.0.0' ] , { latest : '2.0.0' } )
240+ const component = await mountPage ( )
241+ await vi . waitFor ( ( ) => {
242+ expect ( component . text ( ) ) . toContain ( '1.x' )
243+ expect ( component . text ( ) ) . toContain ( '2.x' )
244+ } )
245+
246+ const input = component . find ( 'input[autocomplete="off"]' )
247+ await input . setValue ( '1.0' )
248+ await vi . waitFor ( ( ) => expect ( component . text ( ) ) . not . toContain ( '2.x' ) )
249+
250+ await input . setValue ( '' )
251+ await vi . waitFor ( ( ) => {
252+ expect ( component . text ( ) ) . toContain ( '1.x' )
253+ expect ( component . text ( ) ) . toContain ( '2.x' )
254+ } )
255+ } )
256+ } )
257+
258+ describe ( 'filter popover' , ( ) => {
259+ it ( 'opens and closes on toggle button click' , async ( ) => {
260+ nextFetchResponse = makeVersionData ( [ '1.0.0' ] , { latest : '1.0.0' } )
261+ const component = await mountPage ( )
262+ await vi . waitFor ( ( ) => expect ( component . text ( ) ) . toContain ( 'test-package' ) )
263+
264+ const toggleBtn = component . find ( 'button[aria-haspopup="dialog"]' )
265+ expect ( toggleBtn . attributes ( 'aria-expanded' ) ) . toBe ( 'false' )
266+
267+ await toggleBtn . trigger ( 'click' )
268+ expect ( toggleBtn . attributes ( 'aria-expanded' ) ) . toBe ( 'true' )
269+ expect ( component . find ( '[role="dialog"]' ) . exists ( ) ) . toBe ( true )
270+
271+ await toggleBtn . trigger ( 'click' )
272+ expect ( toggleBtn . attributes ( 'aria-expanded' ) ) . toBe ( 'false' )
273+ expect ( component . find ( '[role="dialog"]' ) . exists ( ) ) . toBe ( false )
274+ } )
275+
276+ it ( 'closes when Escape is pressed' , async ( ) => {
277+ nextFetchResponse = makeVersionData ( [ '1.0.0' ] , { latest : '1.0.0' } )
278+ const component = await mountPage ( )
279+ await vi . waitFor ( ( ) => expect ( component . text ( ) ) . toContain ( 'test-package' ) )
280+
281+ const toggleBtn = component . find ( 'button[aria-haspopup="dialog"]' )
282+ await toggleBtn . trigger ( 'click' )
283+ expect ( toggleBtn . attributes ( 'aria-expanded' ) ) . toBe ( 'true' )
284+
285+ window . dispatchEvent ( new KeyboardEvent ( 'keydown' , { key : 'Escape' } ) )
286+ await vi . waitFor ( ( ) => expect ( toggleBtn . attributes ( 'aria-expanded' ) ) . toBe ( 'false' ) )
287+ } )
288+
289+ it ( 'shows a badge counting active filters' , async ( ) => {
290+ nextFetchResponse = makeVersionData ( [ '1.0.0' ] , { latest : '1.0.0' } )
291+ const component = await mountPage ( )
292+ await vi . waitFor ( ( ) => expect ( component . text ( ) ) . toContain ( 'test-package' ) )
293+
294+ const toggleBtn = component . find ( 'button[aria-haspopup="dialog"]' )
295+ expect ( toggleBtn . text ( ) ) . toBe ( '' ) // no badge when no filters active
296+
297+ await toggleBtn . trigger ( 'click' )
298+ const checkboxes = component . find ( '[role="dialog"]' ) . findAll ( 'input[type="checkbox"]' )
299+
300+ await checkboxes [ 0 ] . setValue ( true ) // enable show prereleases
301+ await vi . waitFor ( ( ) => expect ( toggleBtn . text ( ) ) . toBe ( '1' ) )
302+
303+ await checkboxes [ 1 ] . setValue ( true ) // enable show deprecated
304+ await vi . waitFor ( ( ) => expect ( toggleBtn . text ( ) ) . toBe ( '2' ) )
305+
306+ await checkboxes [ 0 ] . setValue ( false ) // disable show prereleases
307+ await vi . waitFor ( ( ) => expect ( toggleBtn . text ( ) ) . toBe ( '1' ) )
308+ } )
309+ } )
310+
311+ describe ( 'show prereleases toggle' , ( ) => {
312+ it ( 'hides prerelease-only version groups by default' , async ( ) => {
313+ // 1.0.0-alpha.1 is the only version in 1.x — group is invisible until toggled
314+ nextFetchResponse = makeVersionData ( [ '2.0.0' , '1.0.0-alpha.1' ] , { latest : '2.0.0' } )
315+ const component = await mountPage ( )
316+ await vi . waitFor ( ( ) => {
317+ expect ( component . text ( ) ) . toContain ( '2.x' )
318+ expect ( component . text ( ) ) . not . toContain ( '1.x' )
319+ } )
320+ } )
321+
322+ it ( 'reveals prerelease version groups when the toggle is enabled' , async ( ) => {
323+ nextFetchResponse = makeVersionData ( [ '2.0.0' , '1.0.0-alpha.1' ] , { latest : '2.0.0' } )
324+ const component = await mountPage ( )
325+ await vi . waitFor ( ( ) => expect ( component . text ( ) ) . toContain ( '2.x' ) )
326+
327+ const toggleBtn = component . find ( 'button[aria-haspopup="dialog"]' )
328+ await toggleBtn . trigger ( 'click' )
329+ const checkboxes = component . find ( '[role="dialog"]' ) . findAll ( 'input[type="checkbox"]' )
330+ await checkboxes [ 0 ] . setValue ( true ) // showPrereleases
331+
332+ await vi . waitFor ( ( ) => {
333+ expect ( component . text ( ) ) . toContain ( '2.x' )
334+ expect ( component . text ( ) ) . toContain ( '1.x' )
335+ } )
336+ } )
337+ } )
338+
339+ describe ( 'show deprecated toggle' , ( ) => {
340+ it ( 'hides deprecated-only version groups by default once metadata loads' , async ( ) => {
341+ nextFetchResponse = makeVersionData ( [ '2.0.0' , '1.0.0' ] , { latest : '2.0.0' } )
342+ mockFetchAllPackageVersions . mockResolvedValue ( [
343+ { version : '2.0.0' , hasProvenance : false } ,
344+ { version : '1.0.0' , deprecated : 'Use 2.x instead' , hasProvenance : false } ,
345+ ] )
346+ const component = await mountPage ( )
347+ // fullVersionMap populates async; wait for the 1.x group to disappear
348+ await vi . waitFor ( ( ) => {
349+ expect ( component . text ( ) ) . toContain ( '2.x' )
350+ expect ( component . text ( ) ) . not . toContain ( '1.x' )
351+ } )
352+ } )
353+
354+ it ( 'reveals deprecated version groups when the toggle is enabled' , async ( ) => {
355+ nextFetchResponse = makeVersionData ( [ '2.0.0' , '1.0.0' ] , { latest : '2.0.0' } )
356+ mockFetchAllPackageVersions . mockResolvedValue ( [
357+ { version : '2.0.0' , hasProvenance : false } ,
358+ { version : '1.0.0' , deprecated : 'Use 2.x instead' , hasProvenance : false } ,
359+ ] )
360+ const component = await mountPage ( )
361+ await vi . waitFor ( ( ) => expect ( component . text ( ) ) . not . toContain ( '1.x' ) )
362+
363+ const toggleBtn = component . find ( 'button[aria-haspopup="dialog"]' )
364+ await toggleBtn . trigger ( 'click' )
365+ const checkboxes = component . find ( '[role="dialog"]' ) . findAll ( 'input[type="checkbox"]' )
366+ await checkboxes [ 1 ] . setValue ( true ) // showDeprecated
367+
368+ await vi . waitFor ( ( ) => expect ( component . text ( ) ) . toContain ( '1.x' ) )
369+ } )
370+
371+ it ( 'marks a group header with a deprecated badge when all its versions are deprecated' , async ( ) => {
372+ nextFetchResponse = makeVersionData ( [ '2.0.0' , '1.1.0' , '1.0.0' ] , { latest : '2.0.0' } )
373+ mockFetchAllPackageVersions . mockResolvedValue ( [
374+ { version : '2.0.0' , hasProvenance : false } ,
375+ { version : '1.1.0' , deprecated : 'Use 2.x instead' , hasProvenance : false } ,
376+ { version : '1.0.0' , deprecated : 'Use 2.x instead' , hasProvenance : false } ,
377+ ] )
378+ const component = await mountPage ( )
379+ await vi . waitFor ( ( ) => expect ( component . text ( ) ) . not . toContain ( '1.x' ) )
380+
381+ // Enable show deprecated to reveal the all-deprecated 1.x group
382+ const toggleBtn = component . find ( 'button[aria-haspopup="dialog"]' )
383+ await toggleBtn . trigger ( 'click' )
384+ const checkboxes = component . find ( '[role="dialog"]' ) . findAll ( 'input[type="checkbox"]' )
385+ await checkboxes [ 1 ] . setValue ( true )
386+
387+ await vi . waitFor ( ( ) => {
388+ expect ( component . text ( ) ) . toContain ( '1.x' )
389+ expect ( component . text ( ) ) . toContain ( 'deprecated' )
390+ } )
391+ } )
392+ } )
393+
394+ describe ( 'sort tags buttons' , ( ) => {
395+ it ( 'does not render sort controls with only one non-latest tag' , async ( ) => {
396+ nextFetchResponse = makeVersionData ( [ '2.0.0' , '1.0.0' ] , {
397+ latest : '2.0.0' ,
398+ stable : '1.0.0' ,
399+ } )
400+ const component = await mountPage ( )
401+ await vi . waitFor ( ( ) => expect ( component . text ( ) ) . toContain ( 'stable' ) )
402+
403+ expect ( component . findAll ( 'button[aria-pressed]' ) ) . toHaveLength ( 0 )
404+ } )
405+
406+ it ( 'renders sort controls when there are two or more non-latest tags' , async ( ) => {
407+ nextFetchResponse = makeVersionData ( [ '3.0.0' , '2.0.0' , '1.0.0' ] , {
408+ latest : '3.0.0' ,
409+ next : '2.0.0' ,
410+ legacy : '1.0.0' ,
411+ } )
412+ const component = await mountPage ( )
413+ await vi . waitFor ( ( ) => expect ( component . text ( ) ) . toContain ( 'next' ) )
414+
415+ expect ( component . findAll ( 'button[aria-pressed]' ) ) . toHaveLength ( 2 )
416+ } )
417+
418+ it ( '"Sort by tag" is active (aria-pressed) by default' , async ( ) => {
419+ nextFetchResponse = makeVersionData ( [ '3.0.0' , '2.0.0' , '1.0.0' ] , {
420+ latest : '3.0.0' ,
421+ next : '2.0.0' ,
422+ legacy : '1.0.0' ,
423+ } )
424+ const component = await mountPage ( )
425+ await vi . waitFor ( ( ) => expect ( component . text ( ) ) . toContain ( 'next' ) )
426+
427+ const [ sortByTagBtn , sortByDateBtn ] = component . findAll ( 'button[aria-pressed]' )
428+ expect ( sortByTagBtn . attributes ( 'aria-pressed' ) ) . toBe ( 'true' )
429+ expect ( sortByDateBtn . attributes ( 'aria-pressed' ) ) . toBe ( 'false' )
430+ } )
431+
432+ it ( 'clicking "Sort by date" activates date sort mode' , async ( ) => {
433+ nextFetchResponse = makeVersionData ( [ '3.0.0' , '2.0.0' , '1.0.0' ] , {
434+ latest : '3.0.0' ,
435+ next : '2.0.0' ,
436+ legacy : '1.0.0' ,
437+ } )
438+ const component = await mountPage ( )
439+ await vi . waitFor ( ( ) => expect ( component . text ( ) ) . toContain ( 'next' ) )
440+
441+ const [ sortByTagBtn , sortByDateBtn ] = component . findAll ( 'button[aria-pressed]' )
442+ await sortByDateBtn . trigger ( 'click' )
443+
444+ await vi . waitFor ( ( ) => {
445+ expect ( sortByDateBtn . attributes ( 'aria-pressed' ) ) . toBe ( 'true' )
446+ expect ( sortByTagBtn . attributes ( 'aria-pressed' ) ) . toBe ( 'false' )
447+ } )
448+ } )
449+
450+ it ( 'clicking "Sort by date" twice toggles the sort direction' , async ( ) => {
451+ nextFetchResponse = makeVersionData ( [ '3.0.0' , '2.0.0' , '1.0.0' ] , {
452+ latest : '3.0.0' ,
453+ next : '2.0.0' ,
454+ legacy : '1.0.0' ,
455+ } )
456+ const component = await mountPage ( )
457+ await vi . waitFor ( ( ) => expect ( component . text ( ) ) . toContain ( 'next' ) )
458+
459+ const [ , sortByDateBtn ] = component . findAll ( 'button[aria-pressed]' )
460+
461+ // First click: date sort, defaults to newest-first
462+ await sortByDateBtn . trigger ( 'click' )
463+ await vi . waitFor ( ( ) =>
464+ expect ( sortByDateBtn . attributes ( 'aria-label' ) ) . toContain ( 'newest first' ) ,
465+ )
466+
467+ // Second click on the same active button: flips to oldest-first
468+ await sortByDateBtn . trigger ( 'click' )
469+ await vi . waitFor ( ( ) =>
470+ expect ( sortByDateBtn . attributes ( 'aria-label' ) ) . toContain ( 'oldest first' ) ,
471+ )
472+ } )
185473 } )
186474} )
0 commit comments