Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -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,
}));
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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', () => ({
Expand All @@ -13,6 +29,8 @@ jest.mock('next/image', () => ({
},
}));

import ClaimProfileStep from '../ClaimProfileStep';

// Mock fetch
global.fetch = jest.fn();

Expand Down Expand Up @@ -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', () => {
Expand All @@ -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();
});

Expand Down
86 changes: 59 additions & 27 deletions src/components/upload/__tests__/FileUpload.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -28,18 +47,28 @@ const renderFileUpload = (props = {}) => {
);
};

// The "Choose File" button in FileUpload triggers a hidden <input type="file" />
// 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();
});

Expand All @@ -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);

Expand All @@ -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 () => {
Expand Down Expand Up @@ -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(() => {
Expand All @@ -125,46 +156,47 @@ 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(() => {
expect(screen.getByText(/upload failed/i)).toBeInTheDocument();
});
});

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