Files
Mangalord/frontend/src/lib/components/ReactionButtons.svelte.test.ts
MechaCat02 bb833c7e71 feat(detail): like/dislike buttons on the manga page
Add a tri-state like/dislike toggle beside the bookmark action (signed-in
only): click a reaction to set it, the other to switch, or the active one to
clear — optimistic with rollback, mirroring the bookmark toggle. Seeds from
the new GET /me/reactions/:id in the detail loader (non-critical: any failure
degrades to an unset toggle). Unit tests cover the state machine; e2e drives
set → switch → clear against the real endpoints.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-05 17:20:24 +02:00

63 lines
2.5 KiB
TypeScript

import { describe, it, expect, vi, afterEach } from 'vitest';
import { render, screen, cleanup, fireEvent } from '@testing-library/svelte';
const setReaction = vi.fn();
const clearReaction = vi.fn();
vi.mock('$lib/api/reactions', () => ({
setReaction: (...a: unknown[]) => setReaction(...a),
clearReaction: (...a: unknown[]) => clearReaction(...a)
}));
import ReactionButtons from './ReactionButtons.svelte';
afterEach(() => {
cleanup();
setReaction.mockReset();
clearReaction.mockReset();
});
const pressed = (testid: string) =>
screen.getByTestId(testid).getAttribute('aria-pressed');
describe('ReactionButtons', () => {
it('reflects the initial reaction', () => {
render(ReactionButtons, { props: { mangaId: 'm1', initial: 'like' } });
expect(pressed('reaction-like')).toBe('true');
expect(pressed('reaction-dislike')).toBe('false');
});
it('likes when nothing is set', async () => {
setReaction.mockResolvedValue({ manga_id: 'm1', reaction: 'like' });
render(ReactionButtons, { props: { mangaId: 'm1', initial: null } });
await fireEvent.click(screen.getByTestId('reaction-like'));
expect(setReaction).toHaveBeenCalledWith('m1', 'like');
expect(pressed('reaction-like')).toBe('true');
});
it('clears when clicking the active reaction again', async () => {
clearReaction.mockResolvedValue(undefined);
render(ReactionButtons, { props: { mangaId: 'm1', initial: 'like' } });
await fireEvent.click(screen.getByTestId('reaction-like'));
expect(clearReaction).toHaveBeenCalledWith('m1');
expect(pressed('reaction-like')).toBe('false');
});
it('switches from like to dislike', async () => {
setReaction.mockResolvedValue({ manga_id: 'm1', reaction: 'dislike' });
render(ReactionButtons, { props: { mangaId: 'm1', initial: 'like' } });
await fireEvent.click(screen.getByTestId('reaction-dislike'));
expect(setReaction).toHaveBeenCalledWith('m1', 'dislike');
expect(pressed('reaction-dislike')).toBe('true');
expect(pressed('reaction-like')).toBe('false');
});
it('rolls back on failure', async () => {
setReaction.mockRejectedValue(new Error('boom'));
render(ReactionButtons, { props: { mangaId: 'm1', initial: null } });
await fireEvent.click(screen.getByTestId('reaction-like'));
// Optimistic update reverted after the rejection.
await Promise.resolve();
expect(pressed('reaction-like')).toBe('false');
});
});