mirror of
https://github.com/kikootwo/ReadMeABook.git
synced 2026-06-02 20:30:10 +00:00
Update README quick-start and adjust tests
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.
This commit is contained in:
@@ -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
|
**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
|
```yaml
|
||||||
services:
|
services:
|
||||||
readmeabook:
|
readmeabook:
|
||||||
@@ -71,11 +87,7 @@ services:
|
|||||||
PUBLIC_URL: "https://audiobooks.example.com" # Required for OAuth
|
PUBLIC_URL: "https://audiobooks.example.com" # Required for OAuth
|
||||||
```
|
```
|
||||||
|
|
||||||
```bash
|
Then run `docker compose up -d` to start.
|
||||||
docker compose up -d
|
|
||||||
```
|
|
||||||
|
|
||||||
Open http://localhost:3030 and follow the setup wizard.
|
|
||||||
|
|
||||||
**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.
|
**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.
|
||||||
|
|
||||||
|
|||||||
Binary file not shown.
|
Before Width: | Height: | Size: 82 KiB After Width: | Height: | Size: 83 KiB |
Binary file not shown.
|
Before Width: | Height: | Size: 111 KiB After Width: | Height: | Size: 211 KiB |
Binary file not shown.
|
Before Width: | Height: | Size: 1.4 MiB After Width: | Height: | Size: 1.7 MiB |
@@ -55,7 +55,7 @@ describe('AudiobookCard', () => {
|
|||||||
|
|
||||||
render(<AudiobookCard audiobook={baseAudiobook} />);
|
render(<AudiobookCard audiobook={baseAudiobook} />);
|
||||||
|
|
||||||
const requestButton = screen.getByRole('button', { name: 'Login to Request' });
|
const requestButton = screen.getByRole('button', { name: 'Sign in to Request' });
|
||||||
expect(requestButton).toBeDisabled();
|
expect(requestButton).toBeDisabled();
|
||||||
expect(createRequestMock).not.toHaveBeenCalled();
|
expect(createRequestMock).not.toHaveBeenCalled();
|
||||||
});
|
});
|
||||||
@@ -79,12 +79,12 @@ describe('AudiobookCard', () => {
|
|||||||
expect(createRequestMock).toHaveBeenCalledWith(baseAudiobook);
|
expect(createRequestMock).toHaveBeenCalledWith(baseAudiobook);
|
||||||
expect(onRequestSuccess).toHaveBeenCalled();
|
expect(onRequestSuccess).toHaveBeenCalled();
|
||||||
|
|
||||||
expect(screen.getByText(/Request created successfully/)).toBeInTheDocument();
|
expect(screen.getByText(/Request created!/)).toBeInTheDocument();
|
||||||
|
|
||||||
await act(async () => {
|
await act(async () => {
|
||||||
vi.advanceTimersByTime(3000);
|
vi.advanceTimersByTime(3000);
|
||||||
});
|
});
|
||||||
expect(screen.queryByText(/Request created successfully/)).toBeNull();
|
expect(screen.queryByText(/Request created!/)).toBeNull();
|
||||||
});
|
});
|
||||||
|
|
||||||
it('shows in-library state when available', async () => {
|
it('shows in-library state when available', async () => {
|
||||||
@@ -116,11 +116,11 @@ describe('AudiobookCard', () => {
|
|||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
|
|
||||||
const button = screen.getByRole('button', { name: 'Processing...' });
|
// Processing status is shown as a div overlay, not a button
|
||||||
expect(button).toBeDisabled();
|
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');
|
const { AudiobookCard } = await import('@/components/audiobooks/AudiobookCard');
|
||||||
|
|
||||||
render(
|
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');
|
const { AudiobookCard } = await import('@/components/audiobooks/AudiobookCard');
|
||||||
|
|
||||||
render(
|
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 () => {
|
it('shows an error when a request fails', async () => {
|
||||||
|
|||||||
@@ -19,6 +19,10 @@ vi.mock('@/contexts/AuthContext', () => ({
|
|||||||
useAuth: () => useAuthMock(),
|
useAuth: () => useAuthMock(),
|
||||||
}));
|
}));
|
||||||
|
|
||||||
|
vi.mock('@/contexts/PreferencesContext', () => ({
|
||||||
|
usePreferences: () => ({ squareCovers: false, setSquareCovers: vi.fn(), cardSize: 5, setCardSize: vi.fn() }),
|
||||||
|
}));
|
||||||
|
|
||||||
vi.mock('@/lib/hooks/useAudiobooks', () => ({
|
vi.mock('@/lib/hooks/useAudiobooks', () => ({
|
||||||
useAudiobookDetails: (asin: string | null) => useAudiobookDetailsMock(asin),
|
useAudiobookDetails: (asin: string | null) => useAudiobookDetailsMock(asin),
|
||||||
}));
|
}));
|
||||||
@@ -90,7 +94,9 @@ describe('AudiobookDetailsModal', () => {
|
|||||||
expect(screen.getByText('Detail Book')).toBeInTheDocument();
|
expect(screen.getByText('Detail Book')).toBeInTheDocument();
|
||||||
expect(document.body.style.overflow).toBe('hidden');
|
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();
|
expect(onClose).toHaveBeenCalled();
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -120,7 +126,7 @@ describe('AudiobookDetailsModal', () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
expect(onRequestSuccess).toHaveBeenCalled();
|
expect(onRequestSuccess).toHaveBeenCalled();
|
||||||
expect(screen.getByText(/Request created successfully/)).toBeInTheDocument();
|
expect(screen.getByText(/Request created!/)).toBeInTheDocument();
|
||||||
|
|
||||||
await act(async () => {
|
await act(async () => {
|
||||||
vi.advanceTimersByTime(2000);
|
vi.advanceTimersByTime(2000);
|
||||||
@@ -167,7 +173,7 @@ describe('AudiobookDetailsModal', () => {
|
|||||||
);
|
);
|
||||||
|
|
||||||
await act(async () => {});
|
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 () => {
|
it('shows availability state and hides interactive search when available', async () => {
|
||||||
@@ -183,8 +189,9 @@ describe('AudiobookDetailsModal', () => {
|
|||||||
);
|
);
|
||||||
|
|
||||||
await act(async () => {});
|
await act(async () => {});
|
||||||
expect(screen.getByText('Available in Your Library')).toBeInTheDocument();
|
// Status badge and button both show "In Your Library"
|
||||||
expect(screen.queryByLabelText('Interactive Search')).toBeNull();
|
expect(screen.getAllByText('In Your Library').length).toBeGreaterThan(0);
|
||||||
|
expect(screen.queryByTitle('Interactive Search')).toBeNull();
|
||||||
});
|
});
|
||||||
|
|
||||||
it('shows pending approval status with requester name', async () => {
|
it('shows pending approval status with requester name', async () => {
|
||||||
@@ -205,7 +212,7 @@ describe('AudiobookDetailsModal', () => {
|
|||||||
expect(screen.getByRole('button', { name: /Pending Approval \(alice\)/ })).toBeDisabled();
|
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');
|
const { AudiobookDetailsModal } = await import('@/components/audiobooks/AudiobookDetailsModal');
|
||||||
|
|
||||||
render(
|
render(
|
||||||
@@ -219,10 +226,11 @@ describe('AudiobookDetailsModal', () => {
|
|||||||
);
|
);
|
||||||
|
|
||||||
await act(async () => {});
|
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({
|
useAudiobookDetailsMock.mockReturnValue({
|
||||||
audiobook: { ...audiobookDetails, rating: 0 },
|
audiobook: { ...audiobookDetails, rating: 0 },
|
||||||
isLoading: false,
|
isLoading: false,
|
||||||
@@ -239,7 +247,8 @@ describe('AudiobookDetailsModal', () => {
|
|||||||
);
|
);
|
||||||
|
|
||||||
await act(async () => {});
|
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 () => {
|
it('opens interactive search when requested', async () => {
|
||||||
@@ -257,7 +266,7 @@ describe('AudiobookDetailsModal', () => {
|
|||||||
|
|
||||||
expect(screen.queryByTestId('interactive-modal')).toBeNull();
|
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');
|
expect(screen.getByTestId('interactive-modal')).toHaveAttribute('data-open', 'true');
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -29,7 +29,7 @@ describe('AudiobookGrid', () => {
|
|||||||
|
|
||||||
const { container } = render(<AudiobookGrid audiobooks={[]} isLoading={true} />);
|
const { container } = render(<AudiobookGrid audiobooks={[]} isLoading={true} />);
|
||||||
|
|
||||||
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 () => {
|
it('shows the empty message when there are no results', async () => {
|
||||||
|
|||||||
Reference in New Issue
Block a user