Files
readest/apps/readest-app/src/__tests__/components/BookCover.test.tsx
T
Huang Xin 4e01e13ee7 fix(library): make bookitem-main shrink to match cover in fit mode (#4331)
* fix(library): make bookitem-main shrink to match cover in fit mode

Closes #4234. In fit mode the bookitem-main kept its 28/41 aspect
regardless of the cover image's natural aspect, leaving extra padding
beside (portrait covers) or above (landscape covers) the cover. The
select-mode overlay and icons drifted away from the cover edge.

BookCover now reports the loaded image's natural aspect ratio. BookItem
overrides the bookitem-main's aspect-ratio with the cover's aspect so
the box hugs the cover exactly, and proportionally shrinks book-item
width for portrait covers so the info row icons align with the cover's
right edge.

Also wrap the TTS "Back to TTS Location" pill with whitespace-nowrap so
long translations (e.g. German "Zurück zur TTS-Position") expand the
button width instead of overflowing the fixed height.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* fix(library): scope cover shrink to bookitem-main, leave info row at cell width

Per review, the width shrink should only apply to the cover row so the
title and info icons keep their original cell-wide layout. Move the
width style from .book-item to .bookitem-main alongside its aspectRatio
override.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-27 20:25:18 +02:00

68 lines
2.4 KiB
TypeScript

import { describe, it, expect, vi, afterEach } from 'vitest';
import { render, cleanup, fireEvent } from '@testing-library/react';
import BookCover from '@/components/BookCover';
import { Book } from '@/types/book';
vi.mock('next/image', () => ({
__esModule: true,
default: (props: Record<string, unknown>) => {
// biome-ignore lint/a11y/useAltText: test mock; alt comes from spread props
return <img {...props} />;
},
}));
afterEach(cleanup);
const makeBook = (overrides?: Partial<Book>): Book =>
({
hash: 'abc123',
title: 'Test Book',
author: 'Test Author',
format: 'epub',
coverImageUrl: 'https://example.com/cover.jpg',
...overrides,
}) as Book;
describe('BookCover', () => {
it('passes loading="lazy" to crop-mode Image', () => {
const { container } = render(<BookCover book={makeBook()} coverFit='crop' />);
const img = container.querySelector('img.cover-image');
expect(img).toBeTruthy();
expect(img?.getAttribute('loading')).toBe('lazy');
});
it('passes loading="lazy" to fit-mode Image', () => {
const { container } = render(<BookCover book={makeBook()} coverFit='fit' />);
const img = container.querySelector('img.cover-image');
expect(img).toBeTruthy();
expect(img?.getAttribute('loading')).toBe('lazy');
});
it('reports natural aspect ratio via onAspectRatioChange when fit-mode image loads', () => {
const onAspectRatioChange = vi.fn();
const { container } = render(
<BookCover book={makeBook()} coverFit='fit' onAspectRatioChange={onAspectRatioChange} />,
);
const img = container.querySelector('img.cover-image') as HTMLImageElement;
expect(img).toBeTruthy();
Object.defineProperty(img, 'naturalWidth', { value: 600, configurable: true });
Object.defineProperty(img, 'naturalHeight', { value: 900, configurable: true });
fireEvent.load(img);
expect(onAspectRatioChange).toHaveBeenCalledWith(600 / 900);
});
it('falls back to metadata.author on the fallback cover when book.author is empty', () => {
const book = makeBook({
author: '',
coverImageUrl: undefined,
metadata: { author: 'Edited Author' } as Book['metadata'],
});
const { container } = render(<BookCover book={book} coverFit='crop' />);
const fallback = container.querySelector('.fallback-cover');
expect(fallback?.textContent).toContain('Edited Author');
});
});