import { render, screen, waitFor } from '@testing-library/react' import userEvent from '@testing-library/user-event' import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest' import '../i18n' import { SigilDesigner } from './SigilDesigner' import { RUNES, SIGIL_MAX_POINTS, SIGIL_SLOT_COUNT } from '../lib/sigil' function anchorButtons() { return screen.getAllByRole('button', { name: /anchor point/i }) } describe('SigilDesigner', () => { afterEach(() => { vi.unstubAllGlobals() }) it('renders all 24 clock-face anchor slots and starts with zero points', () => { render() expect(anchorButtons()).toHaveLength(SIGIL_SLOT_COUNT) expect(screen.getByTestId('sigil-point-count').textContent).toBe(`0/${SIGIL_MAX_POINTS}`) }) it('adds a point on click, up to the 12-point cap, and refuses a 13th', async () => { const user = userEvent.setup() render() const anchors = anchorButtons() for (let i = 0; i < SIGIL_MAX_POINTS; i++) { await user.click(anchors[i]) } expect(screen.getByTestId('sigil-point-count').textContent).toBe( `${SIGIL_MAX_POINTS}/${SIGIL_MAX_POINTS}`, ) // clicking a 13th, distinct, previously-unselected slot must be a no-op. await user.click(anchors[SIGIL_MAX_POINTS]) expect(screen.getByTestId('sigil-point-count').textContent).toBe( `${SIGIL_MAX_POINTS}/${SIGIL_MAX_POINTS}`, ) expect(anchors[SIGIL_MAX_POINTS]).toHaveAttribute('aria-pressed', 'false') }) it('toggling a placed point removes it, freeing a slot even while capped', async () => { const user = userEvent.setup() render() const anchors = anchorButtons() for (let i = 0; i < SIGIL_MAX_POINTS; i++) { await user.click(anchors[i]) } await user.click(anchors[0]) // remove the first point expect(screen.getByTestId('sigil-point-count').textContent).toBe( `${SIGIL_MAX_POINTS - 1}/${SIGIL_MAX_POINTS}`, ) expect(anchors[0]).toHaveAttribute('aria-pressed', 'false') await user.click(anchors[SIGIL_MAX_POINTS]) // now there's room expect(screen.getByTestId('sigil-point-count').textContent).toBe( `${SIGIL_MAX_POINTS}/${SIGIL_MAX_POINTS}`, ) }) it('offers the fixed rune set and lets the seeker pick one', async () => { const user = userEvent.setup() render() // First rune defaults active. const firstBtn = screen.getByRole('button', { name: RUNES[0].label }) expect(firstBtn).toHaveAttribute('aria-pressed', 'true') const secondBtn = screen.getByRole('button', { name: RUNES[1].label }) expect(secondBtn).toHaveAttribute('aria-pressed', 'false') await user.click(secondBtn) expect(secondBtn).toHaveAttribute('aria-pressed', 'true') expect(firstBtn).toHaveAttribute('aria-pressed', 'false') }) it('save is disabled with zero points, and posts the {points, rune} payload shape', async () => { const fetchMock = vi.fn(async (_input: RequestInfo | URL, _init?: RequestInit) => new Response( JSON.stringify({ id: 's1', name: 'untitled sigil', design: { points: [], rune: 'eye' }, created_at: '2026-01-01T00:00:00Z', }), { status: 200, headers: { 'Content-Type': 'application/json' } }, ), ) vi.stubGlobal('fetch', fetchMock) const user = userEvent.setup() render() const saveBtn = screen.getByRole('button', { name: /inscribe sigil/i }) expect(saveBtn).toHaveProperty('disabled', true) const anchors = anchorButtons() await user.click(anchors[0]) await user.click(anchors[6]) expect(saveBtn).toHaveProperty('disabled', false) await user.click(saveBtn) await waitFor(() => expect(fetchMock).toHaveBeenCalledTimes(1)) const [url, init] = fetchMock.mock.calls[0] expect(String(url)).toBe('/api/inventory/sigils') expect(init?.method).toBe('POST') const body = JSON.parse(String(init?.body)) expect(body.design.rune).toBe(RUNES[0].id) expect(Array.isArray(body.design.points)).toBe(true) expect(body.design.points).toHaveLength(2) for (const p of body.design.points) { expect(p).toHaveLength(2) expect(typeof p[0]).toBe('number') expect(typeof p[1]).toBe('number') } await screen.findByText(/bound/i) }) it('shows an error state when saving fails', async () => { vi.stubGlobal( 'fetch', vi.fn(async () => new Response('nope', { status: 500 })), ) const user = userEvent.setup() render() await user.click(anchorButtons()[0]) await user.click(screen.getByRole('button', { name: /inscribe sigil/i })) await screen.findByRole('alert') }) })