import { vi } from 'vitest'
import * as React from 'react'
import * as Redux from 'react-redux'
import { useRefreshOnEvents } from './useRefreshOnEvents'
vi.mock('react', async () => {
const actual = await vi.importActual('react')
return {
...actual,
useState: vi.fn(),
useEffect: vi.fn(),
}
})
vi.mock('react-redux', async () => {
const actual = await vi.importActual('react-redux')
return {
...actual,
useSelector: vi.fn(),
}
})
describe('useRefreshOnEvents', () => {
const setState = vi.fn()
const useStateMock = (initState) => [initState, setState]
const onRefresh = vi.fn().mockResolvedValue()
let lastTime
let mockUseEffect
beforeEach(() => {
vi.spyOn(React, 'useState').mockImplementation(useStateMock)
mockUseEffect = vi.spyOn(React, 'useEffect')
lastTime = new Date(new Date().valueOf() + 1000)
onRefresh.mockClear()
setState.mockClear()
})
afterEach(() => {
vi.clearAllMocks()
})
it('stores last time checked, to avoid redundant runs', () => {
const useSelectorMock = () => ({
lastReceived: lastTime,
resources: { library: ['lib-1'] },
})
vi.spyOn(Redux, 'useSelector').mockImplementation(useSelectorMock)
mockUseEffect.mockImplementation((callback) => callback())
useRefreshOnEvents({
events: ['library'],
onRefresh,
})
expect(setState).toHaveBeenCalledWith(lastTime)
})
it("does not run again if lastTime didn't change", () => {
vi.spyOn(React, 'useState').mockImplementation(() => [lastTime, setState])
const useSelectorMock = () => ({ lastReceived: lastTime })
vi.spyOn(Redux, 'useSelector').mockImplementation(useSelectorMock)
mockUseEffect.mockImplementation((callback) => callback())
useRefreshOnEvents({
events: ['library'],
onRefresh,
})
expect(setState).not.toHaveBeenCalled()
expect(onRefresh).not.toHaveBeenCalled()
})
describe('Event listening and refresh triggering', () => {
beforeEach(() => {
mockUseEffect.mockImplementation((callback) => callback())
})
it('triggers refresh when a watched event occurs', () => {
const useSelectorMock = () => ({
lastReceived: lastTime,
resources: { library: ['lib-1', 'lib-2'] },
})
vi.spyOn(Redux, 'useSelector').mockImplementation(useSelectorMock)
useRefreshOnEvents({
events: ['library'],
onRefresh,
})
expect(onRefresh).toHaveBeenCalledTimes(1)
expect(setState).toHaveBeenCalledWith(lastTime)
})
it('triggers refresh when multiple watched events occur', () => {
const useSelectorMock = () => ({
lastReceived: lastTime,
resources: {
library: ['lib-1'],
user: ['user-1'],
album: ['album-1'],
},
})
vi.spyOn(Redux, 'useSelector').mockImplementation(useSelectorMock)
useRefreshOnEvents({
events: ['library', 'user'],
onRefresh,
})
expect(onRefresh).toHaveBeenCalledTimes(1)
expect(setState).toHaveBeenCalledWith(lastTime)
})
it('does not trigger refresh when unwatched events occur', () => {
const useSelectorMock = () => ({
lastReceived: lastTime,
resources: { album: ['album-1'], song: ['song-1'] },
})
vi.spyOn(Redux, 'useSelector').mockImplementation(useSelectorMock)
useRefreshOnEvents({
events: ['library', 'user'],
onRefresh,
})
expect(onRefresh).not.toHaveBeenCalled()
expect(setState).not.toHaveBeenCalled()
})
it('triggers refresh on global refresh event', () => {
const useSelectorMock = () => ({
lastReceived: lastTime,
resources: { '*': '*' },
})
vi.spyOn(Redux, 'useSelector').mockImplementation(useSelectorMock)
useRefreshOnEvents({
events: ['library'],
onRefresh,
})
expect(onRefresh).toHaveBeenCalledTimes(1)
expect(setState).toHaveBeenCalledWith(lastTime)
})
it('triggers refresh when listening to all events with "*"', () => {
const useSelectorMock = () => ({
lastReceived: lastTime,
resources: { song: ['song-1'] },
})
vi.spyOn(Redux, 'useSelector').mockImplementation(useSelectorMock)
useRefreshOnEvents({
events: ['*'],
onRefresh,
})
expect(onRefresh).toHaveBeenCalledTimes(1)
expect(setState).toHaveBeenCalledWith(lastTime)
})
it('handles empty events array gracefully', () => {
const useSelectorMock = () => ({
lastReceived: lastTime,
resources: { library: ['lib-1'] },
})
vi.spyOn(Redux, 'useSelector').mockImplementation(useSelectorMock)
useRefreshOnEvents({
events: [],
onRefresh,
})
expect(onRefresh).not.toHaveBeenCalled()
expect(setState).not.toHaveBeenCalled()
})
it('handles missing onRefresh function gracefully', () => {
const useSelectorMock = () => ({
lastReceived: lastTime,
resources: { library: ['lib-1'] },
})
vi.spyOn(Redux, 'useSelector').mockImplementation(useSelectorMock)
expect(() => {
useRefreshOnEvents({
events: ['library'],
})
}).not.toThrow()
expect(setState).toHaveBeenCalledWith(lastTime)
})
it('handles onRefresh errors gracefully', async () => {
const consoleWarnSpy = vi
.spyOn(console, 'warn')
.mockImplementation(() => {})
const failingRefresh = vi
.fn()
.mockRejectedValue(new Error('Refresh failed'))
const useSelectorMock = () => ({
lastReceived: lastTime,
resources: { library: ['lib-1'] },
})
vi.spyOn(Redux, 'useSelector').mockImplementation(useSelectorMock)
useRefreshOnEvents({
events: ['library'],
onRefresh: failingRefresh,
})
expect(failingRefresh).toHaveBeenCalledTimes(1)
expect(setState).toHaveBeenCalledWith(lastTime)
await new Promise((resolve) => setTimeout(resolve, 10))
expect(consoleWarnSpy).toHaveBeenCalledWith(
'Error in useRefreshOnEvents onRefresh callback:',
expect.any(Error),
)
consoleWarnSpy.mockRestore()
})
})
})