import { cleanup, fireEvent, render, screen, waitFor, within } from '@testing-library/react' import { afterAll, afterEach, beforeAll, describe, expect, it, vi } from 'vitest' const mocks = vi.hoisted(() => ({ createInline: vi.fn(), searchResponsibles: vi.fn(), toastError: vi.fn(), toastSuccess: vi.fn(), })) vi.mock('@/app/(campaign)/campanha/actions/activity', () => ({ createActivityInline: (...args: unknown[]) => mocks.createInline(...args), })) vi.mock('@/app/(campaign)/campanha/(app)/atividades/contactSearchActions', () => ({ searchActivityResponsibleOptionsAction: (query: string) => mocks.searchResponsibles(query), })) vi.mock('sonner', () => ({ toast: { error: mocks.toastError, success: mocks.toastSuccess }, })) import { ActivityInlineCreate, type ActivityInlineCreateDraft, } from '@/components/campaign/activity/ActivityInlineCreate' import type { RelationOption } from '@/components/campaign/shared/RelationMultiSelect' import type { ActivityAgendaState } from '@/utilities/activityUi' const DRAFT: ActivityInlineCreateDraft = { startAt: '2026-08-07T16:00:00.000Z', endAt: '2026-08-07T16:30:00.000Z', anchor: { x: 100, y: 200 }, } const MUNICIPALITIES: RelationOption[] = [ { id: 12, name: 'Camaçari' }, { id: 13, name: 'Ilhéus' }, ] const renderOverlay = ({ draft = DRAFT, agendaState = {}, isNarrow = false, knownTags = [], onCreated = vi.fn(), onClose = vi.fn(), }: { draft?: ActivityInlineCreateDraft | null agendaState?: ActivityAgendaState isNarrow?: boolean knownTags?: string[] onCreated?: () => void onClose?: () => void } = {}) => render( , ) beforeAll(() => { vi.stubGlobal( 'ResizeObserver', class { observe() {} unobserve() {} disconnect() {} }, ) }) afterAll(() => { vi.unstubAllGlobals() }) describe('ActivityInlineCreate — overlay de criação inline', () => { afterEach(() => { cleanup() vi.clearAllMocks() }) it('preenche horários do slot (24h) e usa o município do filtro ativo', () => { renderOverlay({ agendaState: { municipality: 12 } }) // C97 — the native datetime-local is gone: the trigger button shows the // civil label, 24h by construction (no browser locale can inject AM/PM). expect(screen.getByLabelText('Início *').textContent).toBe('07/08/2026 às 13:00') expect(screen.getByLabelText('Término').textContent).toBe('07/08/2026 às 13:30') expect((screen.getByLabelText('Município *') as HTMLInputElement).value).toBe('Camaçari') }) it('troca dia no calendário e hora/minuto nos passos, sem perder o outro eixo', () => { renderOverlay({ agendaState: { municipality: 12 } }) fireEvent.click(screen.getByLabelText('Início *')) const dialogs = screen.getAllByRole('dialog') const picker = within(dialogs[dialogs.length - 1]) fireEvent.click(picker.getByRole('button', { name: /15 de agosto de 2026/ })) expect(screen.getByLabelText('Início *').textContent).toBe('15/08/2026 às 13:00') fireEvent.change(picker.getByRole('combobox', { name: 'Hora' }), { target: { value: '09' }, }) expect(screen.getByLabelText('Início *').textContent).toBe('15/08/2026 às 09:00') fireEvent.change(picker.getByRole('combobox', { name: 'Minuto' }), { target: { value: '45' }, }) expect(screen.getByLabelText('Início *').textContent).toBe('15/08/2026 às 09:45') }) it('cria sem navegar e avisa o pai para recarregar a janela', async () => { const onCreated = vi.fn() mocks.createInline.mockResolvedValue({ ok: true }) renderOverlay({ agendaState: { municipality: 12 }, onCreated }) fireEvent.change(screen.getByLabelText('Título *'), { target: { value: 'Café com apoiadores' }, }) fireEvent.change(screen.getByLabelText('Local (opcional)'), { target: { value: 'Centro histórico' }, }) fireEvent.click(screen.getByLabelText('Término')) const dialogs = screen.getAllByRole('dialog') const endPicker = within(dialogs[dialogs.length - 1]) fireEvent.change(endPicker.getByRole('combobox', { name: 'Minuto' }), { target: { value: '45' }, }) fireEvent.click(screen.getByRole('button', { name: 'Salvar' })) await waitFor(() => expect(mocks.createInline).toHaveBeenCalledTimes(1)) expect(mocks.createInline).toHaveBeenCalledWith({ title: 'Café com apoiadores', municipality: 12, startAt: '2026-08-07T16:00:00.000Z', endAt: '2026-08-07T16:45:00.000Z', locality: 'Centro histórico', }) expect(onCreated).toHaveBeenCalledTimes(1) }) it('bloqueia salvar sem município e não invoca a ação', async () => { const onCreated = vi.fn() mocks.createInline.mockResolvedValue({ ok: true }) renderOverlay({ agendaState: {}, onCreated }) fireEvent.change(screen.getByLabelText('Título *'), { target: { value: 'Café com apoiadores' }, }) fireEvent.click(screen.getByRole('button', { name: 'Salvar' })) await waitFor(() => expect(screen.getByText('Informe o município.')).toBeTruthy()) expect(mocks.createInline).not.toHaveBeenCalled() expect(onCreated).not.toHaveBeenCalled() }) it('mantém o overlay aberto com erro inline quando a criação falha', async () => { const onCreated = vi.fn() const message = 'Já existe uma atividade com este título.' mocks.createInline.mockResolvedValue({ ok: false, message, fieldErrors: { title: [message] }, }) renderOverlay({ agendaState: { municipality: 12 }, onCreated }) fireEvent.change(screen.getByLabelText('Título *'), { target: { value: 'Café com apoiadores' }, }) fireEvent.click(screen.getByRole('button', { name: 'Salvar' })) await waitFor(() => expect(mocks.toastError).toHaveBeenCalledWith(message)) await waitFor(() => expect(screen.getAllByText(message).length).toBeGreaterThan(0)) expect(onCreated).not.toHaveBeenCalled() }) it('monta o href de "Mais detalhes" com município e título digitados', () => { renderOverlay({ agendaState: { municipality: 12 } }) fireEvent.change(screen.getByLabelText('Título *'), { target: { value: 'Café com apoiadores' }, }) const link = screen.getByRole('link', { name: 'Mais detalhes' }) expect(link.getAttribute('href')).toBe( '/campanha/atividades/nova?startAt=2026-08-07T16%3A00%3A00.000Z&endAt=2026-08-07T16%3A30%3A00.000Z&municipality=12&title=Caf%C3%A9+com+apoiadores&returnTo=%2Fcampanha%2Fagenda%3Fmunicipality%3D12', ) }) it('adiciona tags por Enter e as envia ao salvar (C105)', async () => { const onCreated = vi.fn() mocks.createInline.mockResolvedValue({ ok: true }) renderOverlay({ agendaState: { municipality: 12 }, onCreated }) const tagInput = screen.getByPlaceholderText('Ex.: comício, imprensa…') fireEvent.change(tagInput, { target: { value: 'Panfletagem' } }) fireEvent.keyDown(tagInput, { key: 'Enter' }) expect(screen.getByLabelText('Remover tag Panfletagem')).toBeTruthy() fireEvent.change(screen.getByLabelText('Título *'), { target: { value: 'Panfletagem no centro' }, }) fireEvent.click(screen.getByRole('button', { name: 'Salvar' })) await waitFor(() => expect(mocks.createInline).toHaveBeenCalledTimes(1)) expect(mocks.createInline).toHaveBeenCalledWith({ title: 'Panfletagem no centro', municipality: 12, startAt: '2026-08-07T16:00:00.000Z', endAt: '2026-08-07T16:30:00.000Z', tags: ['Panfletagem'], }) expect(onCreated).toHaveBeenCalledTimes(1) }) it('deduplica tags e permite remover um chip antes de salvar (C105)', async () => { const onCreated = vi.fn() mocks.createInline.mockResolvedValue({ ok: true }) renderOverlay({ agendaState: { municipality: 12 }, onCreated }) const tagInput = screen.getByPlaceholderText('Ex.: comício, imprensa…') for (const tag of ['Comício', 'Comício', 'Caminhada']) { fireEvent.change(tagInput, { target: { value: tag } }) fireEvent.keyDown(tagInput, { key: 'Enter' }) } expect(screen.getByLabelText('Remover tag Comício')).toBeTruthy() expect(screen.getByLabelText('Remover tag Caminhada')).toBeTruthy() fireEvent.click(screen.getByLabelText('Remover tag Comício')) fireEvent.change(screen.getByLabelText('Título *'), { target: { value: 'Comício na feira' }, }) fireEvent.click(screen.getByRole('button', { name: 'Salvar' })) await waitFor(() => expect(mocks.createInline).toHaveBeenCalledTimes(1)) expect(mocks.createInline).toHaveBeenCalledWith({ title: 'Comício na feira', municipality: 12, startAt: '2026-08-07T16:00:00.000Z', endAt: '2026-08-07T16:30:00.000Z', tags: ['Caminhada'], }) }) it('oferece as tags já usadas como sugestões e as carrega no href de detalhes (C105)', () => { renderOverlay({ agendaState: { municipality: 12 }, knownTags: ['Comício', 'Imprensa'], }) // The overlay renders inside a popover portal, so the datalist lives on // document.body, not in the container returned by render; the suggestion // values are `value` attributes, not option text. const datalist = document.querySelector('datalist') const suggestionValues = [...(datalist?.querySelectorAll('option') ?? [])].map((option) => option.getAttribute('value'), ) expect(suggestionValues).toContain('Comício') expect(suggestionValues).toContain('Imprensa') const tagInput = screen.getByPlaceholderText('Ex.: comício, imprensa…') fireEvent.change(tagInput, { target: { value: 'Panfletagem' } }) fireEvent.keyDown(tagInput, { key: 'Enter' }) const link = screen.getByRole('link', { name: 'Mais detalhes' }) expect(link.getAttribute('href')).toContain('&tags=Panfletagem') }) it('não renderiza nada sem um rascunho de slot', () => { const { container } = renderOverlay({ draft: null }) expect(container.firstChild).toBeNull() }) it('renderiza o sheet do topo mobile com labels ocultos e rodapé fixo', () => { renderOverlay({ agendaState: { municipality: 12 }, isNarrow: true }) // C103 — labels stay in the a11y tree (sr-only), placeholders give context // and required fields keep the asterisk. expect(screen.getByLabelText('Título *')).toBeTruthy() expect((screen.getByLabelText('Título *') as HTMLInputElement).placeholder).toBe( 'Adicionar título *', ) expect(screen.getByLabelText('Início *').textContent).toMatch(/07\/08\/2026 às 13:00/) expect(screen.getByLabelText('Início *').textContent).toContain('*') expect((screen.getByLabelText('Município *') as HTMLInputElement).placeholder).toBe( 'Município *', ) expect((screen.getByLabelText('Local (opcional)') as HTMLInputElement).placeholder).toBe( 'Local (opcional)', ) expect( screen.getByRole('button', { name: 'Responsáveis: Adicionar responsáveis' }), ).toBeTruthy() expect(screen.getByRole('button', { name: 'Salvar' })).toBeTruthy() }) it('abre o seletor de data/hora como bottom sheet no mobile, com Pronto', () => { renderOverlay({ agendaState: { municipality: 12 }, isNarrow: true }) fireEvent.click(screen.getByLabelText('Início *')) const dialogs = screen.getAllByRole('dialog') const picker = within(dialogs[dialogs.length - 1]) expect(picker.getByRole('combobox', { name: 'Hora' })).toBeTruthy() expect(picker.getByRole('button', { name: 'Pronto' })).toBeTruthy() fireEvent.click(picker.getByRole('button', { name: 'Pronto' })) expect(screen.queryByRole('button', { name: 'Pronto' })).toBeNull() }) })