4e01e13ee7
* 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>
68 lines
2.4 KiB
TypeScript
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');
|
|
});
|
|
});
|