import { afterEach, describe, expect, test, vi } from 'vitest'; import { render, screen, fireEvent } from '@testing-library/svelte'; import { mockQuery } from '../../test-utils/query'; const invalidateMock = vi.fn(); vi.mock('@tanstack/svelte-query', async (orig) => { const actual = (await orig()) as Record; return { ...actual, useQueryClient: () => ({ invalidateQueries: invalidateMock }) }; }); vi.mock('$lib/api/suggestions', () => ({ createSuggestionsQuery: vi.fn() })); vi.mock('$lib/api/requests', () => ({ createRequest: vi.fn().mockResolvedValue({}) })); import SuggestionFeed from './SuggestionFeed.svelte'; import { createSuggestionsQuery } from '$lib/api/suggestions'; import { createRequest } from '$lib/api/requests'; import type { ArtistSuggestion } from '$lib/api/types'; const oneSeed: ArtistSuggestion = { mbid: 'mb1', name: 'Outsider', score: 1.0, attribution: [ { artist_id: 'a1', name: 'Seed', contribution: 0.9, is_liked: true, play_count: 0 } ] }; const twoSeeds: ArtistSuggestion = { mbid: 'mb2', name: 'Outsider Two', score: 2.0, attribution: [ { artist_id: 'a1', name: 'A', contribution: 0.8, is_liked: true, play_count: 0 }, { artist_id: 'a2', name: 'B', contribution: 0.5, is_liked: false, play_count: 3 } ] }; const threeSeeds: ArtistSuggestion = { mbid: 'mb3', name: 'Outsider Three', score: 3.0, attribution: [ { artist_id: 'a1', name: 'X', contribution: 0.9, is_liked: true, play_count: 0 }, { artist_id: 'a2', name: 'Y', contribution: 0.6, is_liked: false, play_count: 5 }, { artist_id: 'a3', name: 'Z', contribution: 0.3, is_liked: false, play_count: 1 } ] }; afterEach(() => vi.clearAllMocks()); describe('SuggestionFeed', () => { test('renders one card per suggestion', () => { (createSuggestionsQuery as ReturnType).mockReturnValue( mockQuery({ data: [oneSeed, twoSeeds] }) ); render(SuggestionFeed); expect(screen.getByText('Outsider')).toBeInTheDocument(); expect(screen.getByText('Outsider Two')).toBeInTheDocument(); }); test('attribution copy: 1 seed → "Because you liked X."', () => { (createSuggestionsQuery as ReturnType).mockReturnValue( mockQuery({ data: [oneSeed] }) ); render(SuggestionFeed); expect(screen.getByText(/because you liked seed\./i)).toBeInTheDocument(); }); test('attribution copy: 2 seeds → "Because you liked A and played B."', () => { (createSuggestionsQuery as ReturnType).mockReturnValue( mockQuery({ data: [twoSeeds] }) ); render(SuggestionFeed); expect(screen.getByText(/because you liked a and played b\./i)).toBeInTheDocument(); }); test('attribution copy: 3 seeds → Oxford comma', () => { (createSuggestionsQuery as ReturnType).mockReturnValue( mockQuery({ data: [threeSeeds] }) ); render(SuggestionFeed); expect(screen.getByText(/because you liked x, played y, and played z\./i)).toBeInTheDocument(); }); test('Request button calls createRequest with artist-kind body', async () => { (createSuggestionsQuery as ReturnType).mockReturnValue( mockQuery({ data: [oneSeed] }) ); render(SuggestionFeed); await fireEvent.click(screen.getByRole('button', { name: /request outsider/i })); expect(createRequest).toHaveBeenCalledWith({ kind: 'artist', lidarr_artist_mbid: 'mb1', artist_name: 'Outsider' }); expect(invalidateMock).toHaveBeenCalled(); }); test('empty state when data is []', () => { (createSuggestionsQuery as ReturnType).mockReturnValue(mockQuery({ data: [] })); render(SuggestionFeed); expect(screen.getByText(/listen to something or like an artist/i)).toBeInTheDocument(); }); });