diff --git a/src/components/onboarding/__tests__/ArtistProfileWizard.test.tsx b/src/components/onboarding/__tests__/ArtistProfileWizard.test.tsx index d058fce..63fb49c 100644 --- a/src/components/onboarding/__tests__/ArtistProfileWizard.test.tsx +++ b/src/components/onboarding/__tests__/ArtistProfileWizard.test.tsx @@ -12,10 +12,8 @@ jest.mock('@heroui/dom-animation', () => ({ jest.mock('@heroui/ripple', () => ({ useRipple: () => ({ ripples: [], - onMouseDown: jest.fn(), - onMouseUp: jest.fn(), - onTouchStart: jest.fn(), - onTouchEnd: jest.fn(), + onPress: jest.fn(), + onClear: jest.fn(), }), Ripple: () => null, })); diff --git a/src/components/onboarding/steps/__tests__/ClaimProfileStep.test.tsx b/src/components/onboarding/steps/__tests__/ClaimProfileStep.test.tsx index 91fdb76..dc3f0b7 100644 --- a/src/components/onboarding/steps/__tests__/ClaimProfileStep.test.tsx +++ b/src/components/onboarding/steps/__tests__/ClaimProfileStep.test.tsx @@ -2,7 +2,23 @@ import React from 'react'; import { render, screen, waitFor } from '@testing-library/react'; import userEvent from '@testing-library/user-event'; import '@testing-library/jest-dom'; -import ClaimProfileStep from '../ClaimProfileStep'; + +// Mock HeroUI ripple + dom-animation before any other imports so HeroUI +// Button click handlers don't try to dynamic-import framer-motion features +// (which jest's jsdom env can't resolve without --experimental-vm-modules). +jest.mock('@heroui/dom-animation', () => ({ + __esModule: true, + default: () => ({}), +})); + +jest.mock('@heroui/ripple', () => ({ + useRipple: () => ({ + ripples: [], + onPress: jest.fn(), + onClear: jest.fn(), + }), + Ripple: () => null, +})); // Mock Next.js Image component jest.mock('next/image', () => ({ @@ -13,6 +29,8 @@ jest.mock('next/image', () => ({ }, })); +import ClaimProfileStep from '../ClaimProfileStep'; + // Mock fetch global.fetch = jest.fn(); @@ -42,8 +60,9 @@ const defaultProps = { describe('ClaimProfileStep', () => { beforeEach(() => { - jest.clearAllMocks(); - (fetch as jest.Mock).mockClear(); + // mockReset (not just mockClear) so each test starts with a fresh + // mockResolvedValueOnce queue and no leftover responses from prior cases. + (fetch as jest.Mock).mockReset(); }); it('should render search input and description', () => { @@ -53,7 +72,7 @@ describe('ClaimProfileStep', () => { screen.getByPlaceholderText('Search by artist name...') ).toBeInTheDocument(); expect( - screen.getByText(/Already have music on Flemoji/) + screen.getByText(/Search for your existing artist profile/i) ).toBeInTheDocument(); }); diff --git a/src/components/upload/__tests__/FileUpload.test.tsx b/src/components/upload/__tests__/FileUpload.test.tsx index 95696c2..aae3fc6 100644 --- a/src/components/upload/__tests__/FileUpload.test.tsx +++ b/src/components/upload/__tests__/FileUpload.test.tsx @@ -2,6 +2,25 @@ import React from 'react'; import { render, screen, waitFor } from '@testing-library/react'; import userEvent from '@testing-library/user-event'; import '@testing-library/jest-dom'; + +// Mock HeroUI ripple + dom-animation before importing the component so HeroUI +// Buttons (used internally by FButton) don't try to dynamic-import framer +// motion features (which jest's jsdom env can't resolve without +// --experimental-vm-modules). +jest.mock('@heroui/dom-animation', () => ({ + __esModule: true, + default: () => ({}), +})); + +jest.mock('@heroui/ripple', () => ({ + useRipple: () => ({ + ripples: [], + onPress: jest.fn(), + onClear: jest.fn(), + }), + Ripple: () => null, +})); + import FileUpload from '../FileUpload'; // Mock Ably @@ -28,18 +47,28 @@ const renderFileUpload = (props = {}) => { ); }; +// The "Choose File" button in FileUpload triggers a hidden +// via a ref. For testing we drive the hidden input directly with userEvent.upload. +const getFileInput = () => + document.querySelector('input[type="file"]') as HTMLInputElement; + describe('FileUpload', () => { beforeEach(() => { jest.clearAllMocks(); - (fetch as jest.Mock).mockClear(); + // mockReset (not just mockClear) so each test starts with a fresh + // mockResolvedValueOnce queue and no leftover responses from prior cases. + (fetch as jest.Mock).mockReset(); }); it('should render file upload component', () => { renderFileUpload(); - expect(screen.getByText('Upload your music')).toBeInTheDocument(); + expect(screen.getByText('Drag & drop your music')).toBeInTheDocument(); + expect( + screen.getByText('or click below to browse your files') + ).toBeInTheDocument(); expect( - screen.getByText('Drag and drop files here, or click to select') + screen.getByRole('button', { name: /choose file/i }) ).toBeInTheDocument(); }); @@ -50,7 +79,7 @@ describe('FileUpload', () => { const file = new File(['test audio content'], 'test.mp3', { type: 'audio/mpeg', }); - const input = screen.getByRole('button', { name: /choose files/i }); + const input = getFileInput(); await user.upload(input, file); @@ -64,11 +93,13 @@ describe('FileUpload', () => { const file = new File(['test audio content'], 'test.mp3', { type: 'audio/mpeg', }); - const input = screen.getByRole('button', { name: /choose files/i }); + const input = getFileInput(); await user.upload(input, file); - expect(screen.getByText('Upload to Cloud Storage')).toBeInTheDocument(); + expect( + screen.getByRole('button', { name: /upload track/i }) + ).toBeInTheDocument(); }); it('should handle successful upload flow', async () => { @@ -99,11 +130,11 @@ describe('FileUpload', () => { const file = new File(['test audio content'], 'test.mp3', { type: 'audio/mpeg', }); - const input = screen.getByRole('button', { name: /choose files/i }); + const input = getFileInput(); await user.upload(input, file); - const uploadButton = screen.getByText('Upload to Cloud Storage'); + const uploadButton = screen.getByRole('button', { name: /upload track/i }); await user.click(uploadButton); await waitFor(() => { @@ -125,11 +156,11 @@ describe('FileUpload', () => { const file = new File(['test audio content'], 'test.mp3', { type: 'audio/mpeg', }); - const input = screen.getByRole('button', { name: /choose files/i }); + const input = getFileInput(); await user.upload(input, file); - const uploadButton = screen.getByText('Upload to Cloud Storage'); + const uploadButton = screen.getByRole('button', { name: /upload track/i }); await user.click(uploadButton); await waitFor(() => { @@ -137,34 +168,35 @@ describe('FileUpload', () => { }); }); - it('should validate file type', async () => { - const user = userEvent.setup(); + // NOTE: FileUpload performs no JS-level type/size validation in + // handleFileSelect (see src/components/upload/FileUpload.tsx). The only + // type filtering comes from the hidden input's `accept` attribute, and + // size enforcement happens server-side at /api/uploads/init. The chips + // ("Max 100 MB", format names) are display-only. + it('restricts file picker to audio formats via the accept attribute', () => { renderFileUpload(); - - const file = new File(['test content'], 'test.txt', { type: 'text/plain' }); - const input = screen.getByRole('button', { name: /choose files/i }); - - await user.upload(input, file); - - expect( - screen.getByText(/please select a valid audio file/i) - ).toBeInTheDocument(); + const input = getFileInput(); + expect(input).toHaveAttribute( + 'accept', + 'audio/mpeg,audio/wav,audio/flac,audio/mp4,audio/aac' + ); }); - it('should validate file size', async () => { + it('accepts large files (no client-side size validation)', async () => { const user = userEvent.setup(); renderFileUpload(); - // Create a large file (100MB) - const largeFile = new File(['x'.repeat(100 * 1024 * 1024)], 'large.mp3', { + // 2 MB is large enough to be meaningful but keeps the test fast. + const largeFile = new File(['x'.repeat(2 * 1024 * 1024)], 'large.mp3', { type: 'audio/mpeg', }); - const input = screen.getByRole('button', { name: /choose files/i }); + const input = getFileInput(); await user.upload(input, largeFile); + expect(screen.getByText('large.mp3')).toBeInTheDocument(); expect( - screen.getByText(/file size must be less than 50mb/i) - ).toBeInTheDocument(); + screen.queryByText(/file size must be less than/i) + ).not.toBeInTheDocument(); }); });