🏡 index : quine-global/quintodrome.git

import { vi } from 'vitest'
import * as React from 'react'
import * as Redux from 'react-redux'
import * as RA from 'react-admin'
import { useResourceRefresh } from './useResourceRefresh'

vi.mock('react', async () => {
  const actual = await vi.importActual('react')
  return {
    ...actual,
    useState: vi.fn(),
  }
})

vi.mock('react-redux', async () => {
  const actual = await vi.importActual('react-redux')
  return {
    ...actual,
    useSelector: vi.fn(),
  }
})

vi.mock('react-admin', async () => {
  const actual = await vi.importActual('react-admin')
  return {
    ...actual,
    useRefresh: vi.fn(),
    useDataProvider: vi.fn(),
  }
})

describe('useResourceRefresh', () => {
  const setState = vi.fn()
  const useStateMock = (initState) => [initState, setState]
  const refresh = vi.fn()
  const useRefreshMock = () => refresh
  const getMany = vi.fn()
  const useDataProviderMock = () => ({ getMany })
  let lastTime

  // Applies each real selector against a constructed store, so both useSelector
  // calls in the hook (refresh payload + loaded records) resolve correctly.
  const mockStore = ({ refresh: refreshPayload, loaded = {} }) => {
    const state = {
      activity: { refresh: refreshPayload },
      admin: { resources: loaded },
    }
    vi.spyOn(Redux, 'useSelector').mockImplementation((selector) =>
      selector(state),
    )
  }

  // Turns id lists into the react-admin store shape: { album: { data: { 'al-1': {...} } } }
  const asStore = (byResource) =>
    Object.fromEntries(
      Object.entries(byResource).map(([r, ids]) => [
        r,
        { data: Object.fromEntries(ids.map((id) => [id, { id }])) },
      ]),
    )

  beforeEach(() => {
    vi.spyOn(React, 'useState').mockImplementation(useStateMock)
    vi.spyOn(RA, 'useRefresh').mockImplementation(useRefreshMock)
    vi.spyOn(RA, 'useDataProvider').mockImplementation(useDataProviderMock)
    lastTime = new Date(new Date().valueOf() + 1000)
  })

  afterEach(() => {
    vi.clearAllMocks()
  })

  it('stores last time checked, to avoid redundant runs', () => {
    mockStore({ refresh: { lastReceived: lastTime } })

    useResourceRefresh()

    expect(setState).toHaveBeenCalledWith(lastTime)
  })

  it("does not run again if lastTime didn't change", () => {
    vi.spyOn(React, 'useState').mockImplementation(() => [lastTime, setState])
    mockStore({ refresh: { lastReceived: lastTime } })

    useResourceRefresh()

    expect(setState).not.toHaveBeenCalled()
  })

  describe('No visible resources specified', () => {
    it('triggers a UI refresh when received a "any" resource refresh', () => {
      mockStore({
        refresh: { lastReceived: lastTime, resources: { '*': '*' } },
      })

      useResourceRefresh()

      expect(refresh).toHaveBeenCalledTimes(1)
      expect(getMany).not.toHaveBeenCalled()
    })

    it('triggers a UI refresh when received an "any" id', () => {
      mockStore({
        refresh: { lastReceived: lastTime, resources: { album: ['*'] } },
      })

      useResourceRefresh()

      expect(refresh).toHaveBeenCalledTimes(1)
      expect(getMany).not.toHaveBeenCalled()
    })

    it('refetches only the received resources already loaded in the store', () => {
      mockStore({
        refresh: {
          lastReceived: lastTime,
          resources: { album: ['al-1', 'al-2'], song: ['sg-1', 'sg-2'] },
        },
        loaded: asStore({ album: ['al-1', 'al-2'], song: ['sg-1', 'sg-2'] }),
      })

      useResourceRefresh()

      expect(refresh).not.toHaveBeenCalled()
      expect(getMany).toHaveBeenCalledTimes(2)
      expect(getMany).toHaveBeenCalledWith('album', { ids: ['al-1', 'al-2'] })
      expect(getMany).toHaveBeenCalledWith('song', { ids: ['sg-1', 'sg-2'] })
    })

    it('skips ids that are not loaded in the store', () => {
      mockStore({
        refresh: {
          lastReceived: lastTime,
          resources: { album: ['al-1', 'al-2', 'al-3'] },
        },
        loaded: asStore({ album: ['al-2'] }),
      })

      useResourceRefresh()

      expect(getMany).toHaveBeenCalledTimes(1)
      expect(getMany).toHaveBeenCalledWith('album', { ids: ['al-2'] })
    })

    it('does not fetch when none of the received ids are loaded', () => {
      mockStore({
        refresh: {
          lastReceived: lastTime,
          resources: { artist: ['ar-1', 'ar-2'] },
        },
        loaded: asStore({ artist: ['ar-9'] }),
      })

      useResourceRefresh()

      expect(getMany).not.toHaveBeenCalled()
    })
  })

  describe('Visible resources specified', () => {
    it('triggers a UI refresh when received a "any" resource refresh', () => {
      mockStore({
        refresh: { lastReceived: lastTime, resources: { '*': '*' } },
      })

      useResourceRefresh('album')

      expect(refresh).toHaveBeenCalledTimes(1)
      expect(getMany).not.toHaveBeenCalled()
    })

    it('does not refresh the page when the wildcard is on a resource it does not watch', () => {
      // Guards other senders: a wildcard on an unwatched resource must not reload this list.
      mockStore({
        refresh: {
          lastReceived: lastTime,
          resources: { album: ['al-1', 'al-2'], song: ['*'] },
        },
        loaded: asStore({ album: ['al-1', 'al-2'] }),
      })

      useResourceRefresh('album')

      expect(refresh).not.toHaveBeenCalled()
      expect(getMany).toHaveBeenCalledWith('album', { ids: ['al-1', 'al-2'] })
    })

    it('refetches the loaded songs of a refreshed album', () => {
      // A track with no art of its own is served its album's, so its coverArt id moves with the
      // album's. The backend cannot know which tracks are loaded; the store can.
      mockStore({
        refresh: { lastReceived: lastTime, resources: { album: ['al-1'] } },
        loaded: {
          album: { data: { 'al-1': { id: 'al-1' } } },
          song: {
            data: {
              'sg-1': { id: 'sg-1', albumId: 'al-1' },
              'sg-2': { id: 'sg-2', albumId: 'al-2' },
            },
          },
        },
      })

      useResourceRefresh('song')

      expect(refresh).not.toHaveBeenCalled()
      expect(getMany).toHaveBeenCalledWith('song', { ids: ['sg-1'] })
    })

    it('fans an album refresh out to loaded playlist tracks, which have their own ids', () => {
      mockStore({
        refresh: { lastReceived: lastTime, resources: { album: ['al-1'] } },
        loaded: {
          playlistTrack: {
            data: {
              'pt-1': { id: 'pt-1', albumId: 'al-1' },
              'pt-2': { id: 'pt-2', albumId: 'al-2' },
            },
          },
        },
      })

      useResourceRefresh('playlistTrack', 'song', 'playlist')

      expect(refresh).not.toHaveBeenCalled()
      expect(getMany).toHaveBeenCalledWith('playlistTrack', { ids: ['pt-1'] })
    })

    it('does not fan out to track resources the component does not show', () => {
      mockStore({
        refresh: { lastReceived: lastTime, resources: { album: ['al-1'] } },
        loaded: {
          album: { data: { 'al-1': { id: 'al-1' } } },
          song: { data: { 'sg-1': { id: 'sg-1', albumId: 'al-1' } } },
        },
      })

      useResourceRefresh('album')

      expect(refresh).not.toHaveBeenCalled()
      expect(getMany).toHaveBeenCalledTimes(1)
      expect(getMany).toHaveBeenCalledWith('album', { ids: ['al-1'] })
    })

    it('does not refetch songs when no loaded song belongs to the refreshed album', () => {
      mockStore({
        refresh: { lastReceived: lastTime, resources: { album: ['al-9'] } },
        loaded: {
          song: { data: { 'sg-1': { id: 'sg-1', albumId: 'al-1' } } },
        },
      })

      useResourceRefresh('song')

      expect(refresh).not.toHaveBeenCalled()
      expect(getMany).not.toHaveBeenCalled()
    })

    it('refetches the received resources if they are visible and loaded', () => {
      mockStore({
        refresh: {
          lastReceived: lastTime,
          resources: { album: ['al-1', 'al-2'], song: ['sg-1', 'sg-2'] },
        },
        loaded: asStore({ album: ['al-1', 'al-2'], song: ['sg-1', 'sg-2'] }),
      })

      useResourceRefresh('song')

      expect(refresh).not.toHaveBeenCalled()
      expect(getMany).toHaveBeenCalledTimes(1)
      expect(getMany).toHaveBeenCalledWith('song', { ids: ['sg-1', 'sg-2'] })
    })
  })
})