234 lines
6.5 KiB
JavaScript
234 lines
6.5 KiB
JavaScript
|
|
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'] }, // Need some resources to trigger the update
|
||
|
|
})
|
||
|
|
vi.spyOn(Redux, 'useSelector').mockImplementation(useSelectorMock)
|
||
|
|
|
||
|
|
// Mock useEffect to immediately call the effect callback
|
||
|
|
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)
|
||
|
|
|
||
|
|
// Mock useEffect to immediately call the effect callback
|
||
|
|
mockUseEffect.mockImplementation((callback) => callback())
|
||
|
|
|
||
|
|
useRefreshOnEvents({
|
||
|
|
events: ['library'],
|
||
|
|
onRefresh,
|
||
|
|
})
|
||
|
|
|
||
|
|
expect(setState).not.toHaveBeenCalled()
|
||
|
|
expect(onRefresh).not.toHaveBeenCalled()
|
||
|
|
})
|
||
|
|
|
||
|
|
describe('Event listening and refresh triggering', () => {
|
||
|
|
beforeEach(() => {
|
||
|
|
// Mock useEffect to immediately call the effect callback
|
||
|
|
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'], // This shouldn't trigger since it's not watched
|
||
|
|
},
|
||
|
|
})
|
||
|
|
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'],
|
||
|
|
// onRefresh is undefined
|
||
|
|
})
|
||
|
|
}).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)
|
||
|
|
|
||
|
|
// Wait for the promise to be rejected and handled
|
||
|
|
await new Promise((resolve) => setTimeout(resolve, 10))
|
||
|
|
|
||
|
|
expect(consoleWarnSpy).toHaveBeenCalledWith(
|
||
|
|
'Error in useRefreshOnEvents onRefresh callback:',
|
||
|
|
expect.any(Error),
|
||
|
|
)
|
||
|
|
|
||
|
|
consoleWarnSpy.mockRestore()
|
||
|
|
})
|
||
|
|
})
|
||
|
|
})
|