This commit is contained in:
Daniel Banariba 2026-10-08 01:59:07 +08:00 • committed by GitHub
commit 24361722e2
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
2 changed files with 122 additions and 2 deletions

View file

@ -49,6 +49,7 @@ const Player = () => {
const stoppedRef = useRef(false)
const [audioInstance, setAudioInstance] = useState(null)
const isDesktop = useMediaQuery('(min-width:810px)')
const [playerMode, setPlayerMode] = useState('full')
const isMobilePlayer =
/Android|webOS|iPhone|iPad|iPod|BlackBerry|IEMobile|Opera Mini/i.test(
navigator.userAgent,
@ -56,6 +57,15 @@ const Player = () => {
const { authenticated } = useAuthState()
// On desktop the player is the footer panel and `toggleMode` is disabled, so
// a player left collapsed while the window was narrow can never be reopened.
// Driving `mode` back to full on the transition restores the footer layout.
useEffect(() => {
if (isDesktop) {
setPlayerMode('full')
}
}, [isDesktop])
// Keep a ref to playerState so the mount effect can read the latest value
// without re-triggering on every queue/position change
const playerStateRef = useRef(playerState)
@ -207,7 +217,8 @@ const Player = () => {
theme: playerTheme,
bounds: 'body',
playMode: playerState.mode,
mode: 'full',
mode: playerMode,
onModeChange: setPlayerMode,
loadAudioErrorPlayNext: false,
autoPlayInitLoadPlayList: true,
clearPriorAudioLists: false,
@ -236,7 +247,7 @@ const Player = () => {
locale: locale(translate),
sortableOptions: { delay: 200, delayOnTouchOnly: true },
}),
[gainInfo, isDesktop, playerTheme, translate, playerState.mode],
[gainInfo, isDesktop, playerMode, playerTheme, translate, playerState.mode],
)
const options = useMemo(() => {

View file

@ -0,0 +1,109 @@
import React from 'react'
import { render, act, cleanup } from '@testing-library/react'
import { useMediaQuery } from '@material-ui/core'
import { useAuthState } from 'react-admin'
import { useSelector } from 'react-redux'
import { Player } from './Player'
// Captures the props the music player is rendered with, and exposes the
// onModeChange callback so a test can simulate the user collapsing the player.
const playerProps = { current: null }
vi.mock('navidrome-music-player', () => ({
default: (props) => {
playerProps.current = props
return <div data-testid="music-player" />
},
}))
vi.mock('navidrome-music-player/assets/index.css', () => ({}))
vi.mock('@material-ui/core', async () => {
const actual = await import('@material-ui/core')
return { ...actual, useMediaQuery: vi.fn() }
})
vi.mock('react-admin', async () => {
const actual = await import('react-admin')
return {
...actual,
useAuthState: vi.fn(),
useDataProvider: () => ({ getOne: vi.fn().mockResolvedValue({}) }),
useTranslate: () => (key) => key,
}
})
vi.mock('react-redux', () => ({
useDispatch: () => vi.fn(),
useSelector: vi.fn(),
}))
vi.mock('../transcode', () => ({
detectBrowserProfile: () => ({}),
decisionService: {
setProfile: vi.fn(),
resolveStreamUrl: vi.fn().mockResolvedValue(''),
prefetchDecisions: vi.fn(),
invalidateAll: vi.fn(),
},
}))
vi.mock('./PlayerToolbar', () => ({ default: () => <div /> }))
vi.mock('../subsonic', () => ({ default: { reportPlayback: vi.fn() } }))
const state = {
player: {
queue: [{ trackId: 't1', uuid: 'u1', isRadio: false }],
playIndex: 0,
volume: 1,
mode: 'order',
current: { trackId: 't1' },
clear: false,
savedPlayIndex: 0,
},
settings: { notifications: false },
replayGain: { gainMode: 'none' },
theme: 'dark',
}
describe('<Player />', () => {
beforeEach(() => {
playerProps.current = null
useAuthState.mockReturnValue({ authenticated: true })
useSelector.mockImplementation((selector) => selector(state))
})
afterEach(cleanup)
// Regression test for #5917: a player collapsed while the window was narrow
// could never be reopened once the window grew past the desktop breakpoint,
// because toggleMode is disabled on desktop.
it('returns to full mode when the viewport grows into desktop', () => {
useMediaQuery.mockReturnValue(false)
const { rerender } = render(<Player />)
expect(playerProps.current.mode).toBe('full')
// The user collapses the player into the mini circle.
act(() => playerProps.current.onModeChange('mini'))
expect(playerProps.current.mode).toBe('mini')
// The window is widened past the desktop breakpoint.
useMediaQuery.mockReturnValue(true)
act(() => {
rerender(<Player />)
})
expect(playerProps.current.mode).toBe('full')
})
it('keeps the collapsed mode while the viewport stays narrow', () => {
useMediaQuery.mockReturnValue(false)
const { rerender } = render(<Player />)
act(() => playerProps.current.onModeChange('mini'))
act(() => {
rerender(<Player />)
})
expect(playerProps.current.mode).toBe('mini')
})
})