mirror of
https://github.com/kikootwo/ReadMeABook.git
synced 2026-06-02 20:30:10 +00:00
2c9097f6b0
Add a Quick Start docker-compose snippet and simplify the Manual Setup instruction in README; also replace three screenshot assets. Update multiple audiobook component tests to match recent UI changes: adjust expected button/notification text (e.g. 'Sign in to Request', 'Request created!'), change selectors for close/interactive controls, add PreferencesContext mock, reflect processing overlay and pending/denied status behavior, and update skeleton loader count (8 -> 10). These edits keep tests aligned with the current UI and improve getting-started docs.
182 lines
5.5 KiB
TypeScript
182 lines
5.5 KiB
TypeScript
/**
|
|
* Component: Audiobook Card Tests
|
|
* Documentation: documentation/frontend/components.md
|
|
*/
|
|
|
|
// @vitest-environment jsdom
|
|
|
|
import React from 'react';
|
|
import { act, fireEvent, render, screen } from '@testing-library/react';
|
|
import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest';
|
|
|
|
const createRequestMock = vi.hoisted(() => vi.fn());
|
|
const authState = {
|
|
user: null as null | { id: string; username: string },
|
|
};
|
|
|
|
vi.mock('@/contexts/AuthContext', () => ({
|
|
useAuth: () => authState,
|
|
}));
|
|
|
|
vi.mock('@/lib/hooks/useRequests', () => ({
|
|
useCreateRequest: () => ({ createRequest: createRequestMock, isLoading: false }),
|
|
}));
|
|
|
|
vi.mock('@/components/audiobooks/AudiobookDetailsModal', () => ({
|
|
AudiobookDetailsModal: ({ isOpen }: { isOpen: boolean }) => (
|
|
<div data-testid="details-modal" data-open={String(isOpen)} />
|
|
),
|
|
}));
|
|
|
|
vi.mock('next/image', () => ({
|
|
__esModule: true,
|
|
default: (props: any) => <img {...props} />,
|
|
}));
|
|
|
|
const baseAudiobook = {
|
|
asin: 'asin-1',
|
|
title: 'Test Book',
|
|
author: 'Author',
|
|
};
|
|
|
|
describe('AudiobookCard', () => {
|
|
beforeEach(() => {
|
|
authState.user = null;
|
|
createRequestMock.mockReset();
|
|
vi.useFakeTimers();
|
|
});
|
|
|
|
afterEach(() => {
|
|
vi.useRealTimers();
|
|
});
|
|
|
|
it('disables requests when no user is logged in', async () => {
|
|
const { AudiobookCard } = await import('@/components/audiobooks/AudiobookCard');
|
|
|
|
render(<AudiobookCard audiobook={baseAudiobook} />);
|
|
|
|
const requestButton = screen.getByRole('button', { name: 'Sign in to Request' });
|
|
expect(requestButton).toBeDisabled();
|
|
expect(createRequestMock).not.toHaveBeenCalled();
|
|
});
|
|
|
|
it('creates a request and shows a success toast', async () => {
|
|
authState.user = { id: 'user-1', username: 'user' };
|
|
createRequestMock.mockResolvedValueOnce(undefined);
|
|
|
|
const onRequestSuccess = vi.fn();
|
|
const { AudiobookCard } = await import('@/components/audiobooks/AudiobookCard');
|
|
|
|
render(<AudiobookCard audiobook={baseAudiobook} onRequestSuccess={onRequestSuccess} />);
|
|
|
|
fireEvent.click(screen.getByRole('button', { name: 'Request' }));
|
|
|
|
const requestPromise = createRequestMock.mock.results[0]?.value;
|
|
await act(async () => {
|
|
await requestPromise;
|
|
});
|
|
|
|
expect(createRequestMock).toHaveBeenCalledWith(baseAudiobook);
|
|
expect(onRequestSuccess).toHaveBeenCalled();
|
|
|
|
expect(screen.getByText(/Request created!/)).toBeInTheDocument();
|
|
|
|
await act(async () => {
|
|
vi.advanceTimersByTime(3000);
|
|
});
|
|
expect(screen.queryByText(/Request created!/)).toBeNull();
|
|
});
|
|
|
|
it('shows in-library state when available', async () => {
|
|
const { AudiobookCard } = await import('@/components/audiobooks/AudiobookCard');
|
|
|
|
render(<AudiobookCard audiobook={{ ...baseAudiobook, isAvailable: true }} />);
|
|
|
|
expect(screen.getByText('In Your Library')).toBeInTheDocument();
|
|
});
|
|
|
|
it('opens the details modal when the title is clicked', async () => {
|
|
const { AudiobookCard } = await import('@/components/audiobooks/AudiobookCard');
|
|
|
|
render(<AudiobookCard audiobook={baseAudiobook} />);
|
|
|
|
expect(screen.getByTestId('details-modal')).toHaveAttribute('data-open', 'false');
|
|
|
|
fireEvent.click(screen.getByText('Test Book'));
|
|
|
|
expect(screen.getByTestId('details-modal')).toHaveAttribute('data-open', 'true');
|
|
});
|
|
|
|
it('shows processing state for downloaded requests', async () => {
|
|
const { AudiobookCard } = await import('@/components/audiobooks/AudiobookCard');
|
|
|
|
render(
|
|
<AudiobookCard
|
|
audiobook={{ ...baseAudiobook, isRequested: true, requestStatus: 'downloaded' }}
|
|
/>
|
|
);
|
|
|
|
// Processing status is shown as a div overlay, not a button
|
|
expect(screen.getByText('Processing')).toBeInTheDocument();
|
|
});
|
|
|
|
it('shows pending status for awaiting_approval requests', async () => {
|
|
const { AudiobookCard } = await import('@/components/audiobooks/AudiobookCard');
|
|
|
|
render(
|
|
<AudiobookCard
|
|
audiobook={{
|
|
...baseAudiobook,
|
|
isRequested: true,
|
|
requestStatus: 'awaiting_approval',
|
|
requestedByUsername: 'alice',
|
|
}}
|
|
/>
|
|
);
|
|
|
|
// Card shows "Requested" for all pending statuses
|
|
expect(screen.getByText('Requested')).toBeInTheDocument();
|
|
});
|
|
|
|
it('allows re-requesting for denied status', async () => {
|
|
authState.user = { id: 'user-1', username: 'user' };
|
|
const { AudiobookCard } = await import('@/components/audiobooks/AudiobookCard');
|
|
|
|
render(
|
|
<AudiobookCard
|
|
audiobook={{ ...baseAudiobook, isRequested: true, requestStatus: 'denied' }}
|
|
/>
|
|
);
|
|
|
|
// Denied status allows re-requesting, so Request button is shown
|
|
expect(screen.getByRole('button', { name: 'Request' })).toBeInTheDocument();
|
|
});
|
|
|
|
it('shows an error when a request fails', async () => {
|
|
authState.user = { id: 'user-1', username: 'user' };
|
|
createRequestMock.mockRejectedValueOnce(new Error('Request failed'));
|
|
|
|
const { AudiobookCard } = await import('@/components/audiobooks/AudiobookCard');
|
|
|
|
render(<AudiobookCard audiobook={baseAudiobook} />);
|
|
|
|
fireEvent.click(screen.getByRole('button', { name: 'Request' }));
|
|
|
|
const requestPromise = createRequestMock.mock.results[0]?.value;
|
|
await act(async () => {
|
|
try {
|
|
await requestPromise;
|
|
} catch {
|
|
// Expected for this test.
|
|
}
|
|
});
|
|
|
|
expect(screen.getByText('Request failed')).toBeInTheDocument();
|
|
|
|
await act(async () => {
|
|
vi.advanceTimersByTime(5000);
|
|
});
|
|
expect(screen.queryByText('Request failed')).toBeNull();
|
|
});
|
|
});
|