navidrome/ui/src/common/Pagination.test.jsx
Deluan Quintão 6c2644d208
feat(ui): remember 'items per page' selection across sessions (#5819)
* feat(ui): add perPageStore helper for items-per-page persistence

* feat(ui): persist items-per-page selection in localStorage

* feat(ui): enable per-page persistence on album grid, missing files and playlist tracks

* feat(ui): restore saved album grid page size on fresh load

* feat(ui): restore saved items-per-page on list load

* fix(ui): move defaultRowsPerPageOptions to perPageStore to satisfy react-refresh lint

* fix(ui): correct defaultRowsPerPageOptions import in List and add render test

* refactor(ui): default getStoredPerPage fallback to the first option

The fallback equalled options[0] at three of four call sites; default it so
those sites stop restating it. SongList and useAlbumsPerPage still pass an
explicit fallback where it legitimately differs from the first option.

* fix(ui): persist only user-selected page sizes, validate album size against width

Persisting on every pagination context value let a URL-injected perPage (e.g.
the perPage=15 album link in NowPlayingPanel) or a forced single-option mobile
grid overwrite the saved preference. Persist only a value that is an actual
option in a multi-option selector. Also validate the album grid's redux session
value against the current width so a size chosen at a wider breakpoint can't
leave an out-of-range selector.

* fix(ui): restore saved items-per-page on the radio list

RadioList passed a hard-coded perPage that overrode List's stored seed via the
props spread, so a radio-list page size was persisted but never restored. Seed
it from storage like the other list views.

* fix(ui): persist page size only on an actual selector change

Watching the pagination context value meant any valid value persisted itself:
loading a list at a breakpoint where the saved size is invalid stored the
responsive fallback, and opening a URL with a valid ?perPage= stored that too,
either way discarding the user's real preference. Inject a wrapped setPerPage
instead, so only the rows-per-page selector writes. This also drops the
option-validation heuristics, which the new trigger makes unnecessary.
2026-07-19 18:59:37 -04:00

62 lines
2 KiB
JavaScript

import React from 'react'
import { render, screen, fireEvent } from '@testing-library/react'
import { describe, it, expect, beforeEach, vi } from 'vitest'
import { Pagination } from './Pagination'
// stub RA's Pagination so a test can invoke the injected setPerPage, i.e.
// simulate an actual rows-per-page selection
vi.mock('react-admin', async () => {
const React = await vi.importActual('react')
return {
Pagination: ({ setPerPage }) =>
React.createElement(
'button',
{ onClick: () => setPerPage(50) },
'select 50',
),
useListPaginationContext: vi.fn(),
}
})
describe('Pagination', () => {
let mockContext
let setPerPage
beforeEach(async () => {
vi.clearAllMocks()
localStorage.clear()
setPerPage = vi.fn()
const { useListPaginationContext } = await import('react-admin')
mockContext = vi.mocked(useListPaginationContext)
})
const selectPerPage = () => fireEvent.click(screen.getByText('select 50'))
it('persists the page size chosen in the selector', () => {
mockContext.mockReturnValue({ resource: 'song', perPage: 15, setPerPage })
render(<Pagination />)
selectPerPage()
expect(localStorage.getItem('perPage.song')).toEqual('50')
})
it('still applies the change to the list', () => {
mockContext.mockReturnValue({ resource: 'song', perPage: 15, setPerPage })
render(<Pagination />)
selectPerPage()
expect(setPerPage).toHaveBeenCalledWith(50)
})
it('does not persist a page size the user did not select', () => {
mockContext.mockReturnValue({ resource: 'song', perPage: 15, setPerPage })
render(<Pagination />)
expect(localStorage.getItem('perPage.song')).toBeNull()
})
it('does not persist without a resource in context', () => {
mockContext.mockReturnValue({ perPage: 15, setPerPage })
render(<Pagination />)
selectPerPage()
expect(localStorage.getItem('perPage.undefined')).toBeNull()
expect(setPerPage).toHaveBeenCalledWith(50)
})
})