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')
})
})