🏡 index : ironmagma/quintodrome.git

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()
    })
  })
})