diff --git a/README.md b/README.md index c7f7995..164c6e4 100644 --- a/README.md +++ b/README.md @@ -50,6 +50,22 @@ User friendly audible-backed searches, multi-file chapter merging, e-book sideca **Prerequisites:** Docker, Plex or Audiobookshelf, qBittorrent or SABnzbd, Prowlarr +### Quick Start + +```bash +# Download docker-compose.yml +curl -fsSL https://raw.githubusercontent.com/kikootwo/readmeabook/main/docker-compose.yml -o docker-compose.yml + +# Start the container +docker compose up -d +``` + +Open http://localhost:3030 and follow the setup wizard. + +### Manual Setup + +If you prefer to customize the compose file: + ```yaml services: readmeabook: @@ -71,11 +87,7 @@ services: PUBLIC_URL: "https://audiobooks.example.com" # Required for OAuth ``` -```bash -docker compose up -d -``` - -Open http://localhost:3030 and follow the setup wizard. +Then run `docker compose up -d` to start. **Important:** Your download client (qBittorrent/SABnzbd) and RMAB must see files at the same path. See the [Volume Mapping Guide](documentation/deployment/volume-mapping.md) if downloads aren't being detected. diff --git a/screenshots/ADMIN.png b/screenshots/ADMIN.png index 89e3cfa..425623f 100644 Binary files a/screenshots/ADMIN.png and b/screenshots/ADMIN.png differ diff --git a/screenshots/BOOKDATE.png b/screenshots/BOOKDATE.png index 8dc9a33..e129e81 100644 Binary files a/screenshots/BOOKDATE.png and b/screenshots/BOOKDATE.png differ diff --git a/screenshots/HOMEPAGE.png b/screenshots/HOMEPAGE.png index 1365b38..8ed0add 100644 Binary files a/screenshots/HOMEPAGE.png and b/screenshots/HOMEPAGE.png differ diff --git a/tests/components/audiobooks/AudiobookCard.test.tsx b/tests/components/audiobooks/AudiobookCard.test.tsx index be881fe..52cb4e7 100644 --- a/tests/components/audiobooks/AudiobookCard.test.tsx +++ b/tests/components/audiobooks/AudiobookCard.test.tsx @@ -55,7 +55,7 @@ describe('AudiobookCard', () => { render(); - const requestButton = screen.getByRole('button', { name: 'Login to Request' }); + const requestButton = screen.getByRole('button', { name: 'Sign in to Request' }); expect(requestButton).toBeDisabled(); expect(createRequestMock).not.toHaveBeenCalled(); }); @@ -79,12 +79,12 @@ describe('AudiobookCard', () => { expect(createRequestMock).toHaveBeenCalledWith(baseAudiobook); expect(onRequestSuccess).toHaveBeenCalled(); - expect(screen.getByText(/Request created successfully/)).toBeInTheDocument(); + expect(screen.getByText(/Request created!/)).toBeInTheDocument(); await act(async () => { vi.advanceTimersByTime(3000); }); - expect(screen.queryByText(/Request created successfully/)).toBeNull(); + expect(screen.queryByText(/Request created!/)).toBeNull(); }); it('shows in-library state when available', async () => { @@ -116,11 +116,11 @@ describe('AudiobookCard', () => { /> ); - const button = screen.getByRole('button', { name: 'Processing...' }); - expect(button).toBeDisabled(); + // Processing status is shown as a div overlay, not a button + expect(screen.getByText('Processing')).toBeInTheDocument(); }); - it('shows pending approval status with requester name', async () => { + it('shows pending status for awaiting_approval requests', async () => { const { AudiobookCard } = await import('@/components/audiobooks/AudiobookCard'); render( @@ -134,10 +134,12 @@ describe('AudiobookCard', () => { /> ); - expect(screen.getByRole('button', { name: /Pending Approval \(alice\)/ })).toBeDisabled(); + // Card shows "Requested" for all pending statuses + expect(screen.getByText('Requested')).toBeInTheDocument(); }); - it('shows a denied request state', async () => { + it('allows re-requesting for denied status', async () => { + authState.user = { id: 'user-1', username: 'user' }; const { AudiobookCard } = await import('@/components/audiobooks/AudiobookCard'); render( @@ -146,7 +148,8 @@ describe('AudiobookCard', () => { /> ); - expect(screen.getByRole('button', { name: 'Request Denied' })).toBeDisabled(); + // 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 () => { diff --git a/tests/components/audiobooks/AudiobookDetailsModal.test.tsx b/tests/components/audiobooks/AudiobookDetailsModal.test.tsx index 5ea9adf..183a1d5 100644 --- a/tests/components/audiobooks/AudiobookDetailsModal.test.tsx +++ b/tests/components/audiobooks/AudiobookDetailsModal.test.tsx @@ -19,6 +19,10 @@ vi.mock('@/contexts/AuthContext', () => ({ useAuth: () => useAuthMock(), })); +vi.mock('@/contexts/PreferencesContext', () => ({ + usePreferences: () => ({ squareCovers: false, setSquareCovers: vi.fn(), cardSize: 5, setCardSize: vi.fn() }), +})); + vi.mock('@/lib/hooks/useAudiobooks', () => ({ useAudiobookDetails: (asin: string | null) => useAudiobookDetailsMock(asin), })); @@ -90,7 +94,9 @@ describe('AudiobookDetailsModal', () => { expect(screen.getByText('Detail Book')).toBeInTheDocument(); expect(document.body.style.overflow).toBe('hidden'); - fireEvent.click(screen.getByRole('button', { name: 'Close modal' })); + // Both mobile and desktop close buttons exist, click the first one + const closeButtons = screen.getAllByRole('button', { name: 'Close' }); + fireEvent.click(closeButtons[0]); expect(onClose).toHaveBeenCalled(); }); @@ -120,7 +126,7 @@ describe('AudiobookDetailsModal', () => { }); expect(onRequestSuccess).toHaveBeenCalled(); - expect(screen.getByText(/Request created successfully/)).toBeInTheDocument(); + expect(screen.getByText(/Request created!/)).toBeInTheDocument(); await act(async () => { vi.advanceTimersByTime(2000); @@ -167,7 +173,7 @@ describe('AudiobookDetailsModal', () => { ); await act(async () => {}); - expect(screen.getByText('Failed to load audiobook details')).toBeInTheDocument(); + expect(screen.getByText('Failed to load details')).toBeInTheDocument(); }); it('shows availability state and hides interactive search when available', async () => { @@ -183,8 +189,9 @@ describe('AudiobookDetailsModal', () => { ); await act(async () => {}); - expect(screen.getByText('Available in Your Library')).toBeInTheDocument(); - expect(screen.queryByLabelText('Interactive Search')).toBeNull(); + // Status badge and button both show "In Your Library" + expect(screen.getAllByText('In Your Library').length).toBeGreaterThan(0); + expect(screen.queryByTitle('Interactive Search')).toBeNull(); }); it('shows pending approval status with requester name', async () => { @@ -205,7 +212,7 @@ describe('AudiobookDetailsModal', () => { expect(screen.getByRole('button', { name: /Pending Approval \(alice\)/ })).toBeDisabled(); }); - it('shows a denied request state', async () => { + it('shows request button for denied status (allows re-request)', async () => { const { AudiobookDetailsModal } = await import('@/components/audiobooks/AudiobookDetailsModal'); render( @@ -219,10 +226,11 @@ describe('AudiobookDetailsModal', () => { ); await act(async () => {}); - expect(screen.getByRole('button', { name: 'Request Denied' })).toBeDisabled(); + // Denied status allows re-requesting, shows Request Audiobook button + expect(screen.getByRole('button', { name: 'Request Audiobook' })).toBeInTheDocument(); }); - it('shows Not Found when rating is missing', async () => { + it('does not show rating badge when rating is zero', async () => { useAudiobookDetailsMock.mockReturnValue({ audiobook: { ...audiobookDetails, rating: 0 }, isLoading: false, @@ -239,7 +247,8 @@ describe('AudiobookDetailsModal', () => { ); await act(async () => {}); - expect(screen.getByText('Not Found')).toBeInTheDocument(); + // Rating badge is not shown when rating is 0 + expect(screen.queryByText('0.0')).toBeNull(); }); it('opens interactive search when requested', async () => { @@ -257,7 +266,7 @@ describe('AudiobookDetailsModal', () => { expect(screen.queryByTestId('interactive-modal')).toBeNull(); - fireEvent.click(screen.getByLabelText('Interactive Search')); + fireEvent.click(screen.getByTitle('Interactive Search')); expect(screen.getByTestId('interactive-modal')).toHaveAttribute('data-open', 'true'); }); diff --git a/tests/components/audiobooks/AudiobookGrid.test.tsx b/tests/components/audiobooks/AudiobookGrid.test.tsx index 217ff01..01de264 100644 --- a/tests/components/audiobooks/AudiobookGrid.test.tsx +++ b/tests/components/audiobooks/AudiobookGrid.test.tsx @@ -29,7 +29,7 @@ describe('AudiobookGrid', () => { const { container } = render(); - expect(container.querySelectorAll('.animate-pulse')).toHaveLength(8); + expect(container.querySelectorAll('.animate-pulse')).toHaveLength(10); }); it('shows the empty message when there are no results', async () => {