Phase 2 of the mobile redesign: the catalog at `/` adapts to phone viewports without disrupting the desktop layout. The same filter form renders inline on desktop and inside a bottom sheet on mobile, and a dedicated sort sheet replaces the inline `<select>` below 640px. - MangaCard gains optional `unreadCount` and `progress` props that render a top-right pill badge and a bottom progress overlay on the cover. Both are no-ops when omitted, so existing callers don't change. Counts past 99 cap at "99+". - The filter form body is extracted into a Svelte 5 snippet and rendered conditionally — inline desktop panel OR mobile sheet, never both — so no testid is duplicated and the focus trap can't double- fire. A matchMedia listener tracks the 640px breakpoint and drives the snippet target. - Mobile catalog adds: Filter / Sort chip buttons, an always-visible active-filter chip row with per-facet remove buttons, full-width search, and a 2-column grid. - Auto-expanding the filter panel from URL params is now desktop-only — on mobile the active-filter chips signal applied facets without hiding the catalog under a scrim on first paint. - Vitest suite covers MangaCard badge / overlay behavior including clamp / hide edge cases. Playwright spec covers the mobile filter + sort sheet flow at 390px viewport, with a hydration gate to keep click dispatch from racing the SSR'd-but-not-yet-reactive button. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
66 lines
2.5 KiB
TypeScript
66 lines
2.5 KiB
TypeScript
import { describe, it, expect, afterEach } from 'vitest';
|
|
import { render, screen, cleanup } from '@testing-library/svelte';
|
|
import MangaCard from './MangaCard.svelte';
|
|
import type { Manga } from '$lib/api/client';
|
|
|
|
afterEach(() => cleanup());
|
|
|
|
const baseManga: Manga = {
|
|
id: 'm1',
|
|
title: 'Berserk',
|
|
status: 'ongoing',
|
|
alt_titles: [],
|
|
description: null,
|
|
cover_image_path: null,
|
|
created_at: '2026-01-01T00:00:00Z',
|
|
updated_at: '2026-01-01T00:00:00Z'
|
|
};
|
|
|
|
describe('MangaCard badges & overlays', () => {
|
|
it('does not render an unread badge when unreadCount is omitted', () => {
|
|
render(MangaCard, { props: { manga: baseManga } });
|
|
expect(screen.queryByTestId('unread-badge')).toBeNull();
|
|
});
|
|
|
|
it('does not render an unread badge when unreadCount is 0', () => {
|
|
render(MangaCard, { props: { manga: baseManga, unreadCount: 0 } });
|
|
expect(screen.queryByTestId('unread-badge')).toBeNull();
|
|
});
|
|
|
|
it('renders the unread badge with the count when unreadCount > 0', () => {
|
|
render(MangaCard, { props: { manga: baseManga, unreadCount: 7 } });
|
|
const badge = screen.getByTestId('unread-badge');
|
|
expect(badge.textContent?.trim()).toBe('7');
|
|
});
|
|
|
|
it('caps the unread badge at 99+ for very large counts', () => {
|
|
render(MangaCard, { props: { manga: baseManga, unreadCount: 250 } });
|
|
const badge = screen.getByTestId('unread-badge');
|
|
expect(badge.textContent?.trim()).toBe('99+');
|
|
});
|
|
|
|
it('does not render a progress bar when progress is omitted', () => {
|
|
render(MangaCard, { props: { manga: baseManga } });
|
|
expect(screen.queryByTestId('cover-progress')).toBeNull();
|
|
});
|
|
|
|
it('does not render a progress bar at exactly 0 (no reads yet)', () => {
|
|
render(MangaCard, { props: { manga: baseManga, progress: 0 } });
|
|
expect(screen.queryByTestId('cover-progress')).toBeNull();
|
|
});
|
|
|
|
it('renders the progress overlay clamped to 0..1 and reflects the value as a width style', () => {
|
|
const { rerender } = render(MangaCard, {
|
|
props: { manga: baseManga, progress: 0.4 }
|
|
});
|
|
const bar = screen.getByTestId('cover-progress-fill');
|
|
expect((bar as HTMLElement).style.width).toBe('40%');
|
|
|
|
rerender({ manga: baseManga, progress: 1.5 });
|
|
expect((screen.getByTestId('cover-progress-fill') as HTMLElement).style.width).toBe('100%');
|
|
|
|
rerender({ manga: baseManga, progress: -0.2 });
|
|
expect(screen.queryByTestId('cover-progress')).toBeNull();
|
|
});
|
|
});
|