import React from 'react' import { render, screen, fireEvent, act } from '@testing-library/react' import { describe, it, expect, beforeEach, vi } from 'vitest' import { Provider } from 'react-redux' import { createStore, combineReducers } from 'redux' import { ThemeProvider, createTheme } from '@material-ui/core/styles' import { settingsReducer, activityReducer } from '../reducers' import { processEvent, EVENT_REFRESH_RESOURCE } from '../actions' import PlaylistsSubMenu from './PlaylistsSubMenu' const mockUseQueryWithStore = vi.fn() vi.mock('../config', () => ({ // losslessFormats is read at module-load time by common/QualityInfo.jsx, // pulled in transitively via the '../common' barrel file default: { enableFavourites: true, maxSidebarPlaylists: 100, losslessFormats: '', }, })) vi.mock('react-dnd', () => ({ useDrop: () => [{}, () => {}], })) vi.mock('react-router-dom', () => ({ useHistory: () => ({ push: vi.fn() }), })) vi.mock('react-admin', async (importOriginal) => { const actual = await importOriginal() return { ...actual, useTranslate: () => (x) => x, useDataProvider: () => ({ addToPlaylist: vi.fn() }), useNotify: () => vi.fn(), useQueryWithStore: (query) => mockUseQueryWithStore(query), MenuItemLink: ({ primaryText }) =>
{primaryText}
, } }) const playlists = { 'pl-1': { id: 'pl-1', name: 'Mine', ownerId: 'user-1' }, 'pl-2': { id: 'pl-2', name: 'Theirs', ownerId: 'user-2' }, } const SET_PLAYLIST_DATA = 'TEST/SET_PLAYLIST_DATA' const adminReducer = (state = { resources: {} }, action) => action.type === SET_PLAYLIST_DATA ? { resources: { playlist: { data: action.data } } } : state const renderMenu = (preloadedSettings = {}, preloadedPlaylistData) => { const store = createStore( combineReducers({ settings: settingsReducer, activity: activityReducer, admin: adminReducer, }), { settings: preloadedSettings, activity: {}, admin: { resources: preloadedPlaylistData ? { playlist: { data: preloadedPlaylistData } } : {}, }, }, ) const theme = createTheme() render( , ) return store } const lastQuery = () => mockUseQueryWithStore.mock.calls[ mockUseQueryWithStore.mock.calls.length - 1 ][0] describe('', () => { beforeEach(() => { vi.clearAllMocks() localStorage.setItem('userId', 'user-1') mockUseQueryWithStore.mockReturnValue({ data: playlists, loaded: true }) // SubMenu uses MUI's useMediaQuery, which needs window.matchMedia in jsdom window.matchMedia = (query) => ({ matches: false, media: query, addListener: () => {}, removeListener: () => {}, }) // OverflowTooltip (via MenuItemLink) needs ResizeObserver, unavailable in jsdom window.ResizeObserver = class { observe() {} unobserve() {} disconnect() {} } }) it('queries without a starred filter by default', () => { renderMenu() expect(lastQuery().payload.filter).toBeUndefined() expect(screen.getByText('Mine')).not.toBeNull() expect(screen.getByText('Theirs')).not.toBeNull() }) it('adds the starred filter when favourites-only is enabled', () => { renderMenu({ sidebarPlaylistsOnlyFavourites: true }) expect(lastQuery().payload.filter).toEqual({ starred: true }) }) it('toggles the setting when the heart action is clicked', () => { const store = renderMenu() fireEvent.click(screen.getByTitle('menu.onlyFavourites')) expect(store.getState().settings.sidebarPlaylistsOnlyFavourites).toBe(true) expect(lastQuery().payload.filter).toEqual({ starred: true }) }) it('refetches on a playlist SSE event when favourites-only is on', async () => { const store = renderMenu({ sidebarPlaylistsOnlyFavourites: true }) const before = lastQuery().payload.refresh // useRefreshOnEvents compares Date.now() timestamps; make sure it advances await act(() => new Promise((resolve) => setTimeout(resolve, 5))) act(() => { store.dispatch( processEvent(EVENT_REFRESH_RESOURCE, { playlist: ['pl-1'] }), ) }) expect(lastQuery().payload.refresh).toBe(before + 1) }) it('does not change the query signature on an SSE event when favourites-only is off', async () => { const store = renderMenu() const before = JSON.stringify(lastQuery().payload) await act(() => new Promise((resolve) => setTimeout(resolve, 5))) act(() => { store.dispatch( processEvent(EVENT_REFRESH_RESOURCE, { playlist: ['pl-1'] }), ) }) // Signature unchanged → useQueryWithStore dedupes, no wasted refetch expect(lastQuery().payload.refresh).toBeUndefined() expect(JSON.stringify(lastQuery().payload)).toBe(before) }) it('refetches when a playlist is starred locally (no SSE echo)', () => { const store = renderMenu( { sidebarPlaylistsOnlyFavourites: true }, { 'pl-1': { id: 'pl-1', name: 'Mine', ownerId: 'user-1' } }, ) const before = lastQuery().payload.starFingerprint act(() => { store.dispatch({ type: SET_PLAYLIST_DATA, data: { 'pl-1': { id: 'pl-1', name: 'Mine', ownerId: 'user-1', starred: true, }, }, }) }) expect(lastQuery().payload.starFingerprint).not.toBe(before) expect(lastQuery().payload.starFingerprint).toContain('pl-1') }) })