mirror of
https://github.com/fsolla/teqo.git
synced 2026-11-13 15:13:07 +00:00
262 lines
8.9 KiB
TypeScript
262 lines
8.9 KiB
TypeScript
import { cleanup, fireEvent, render, screen, waitFor } from '@testing-library/react'
|
|
import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest'
|
|
|
|
import { SpeechCutDialog } from '@/components/campaign/speech/SpeechCutDialog'
|
|
import type { SpeechCutViewModel } from '@/lib/speechCut'
|
|
|
|
const SAVE_ENDPOINT = '/campanha/comunicacao/acervo/cortar'
|
|
const STATUS_ENDPOINT = `${SAVE_ENDPOINT}/status`
|
|
const SUGGESTION_ENDPOINT = `${SAVE_ENDPOINT}/sugestao`
|
|
|
|
const RANGE = { startSeconds: 43, endSeconds: 118 }
|
|
|
|
const cutView = (overrides: Partial<SpeechCutViewModel> = {}): SpeechCutViewModel => ({
|
|
id: 123,
|
|
status: 'processing',
|
|
step: 'resolving',
|
|
title: 'Acesso a medicamentos',
|
|
description: 'Descrição do trecho',
|
|
startSeconds: 43,
|
|
endSeconds: 118,
|
|
durationSeconds: 75,
|
|
durationLabel: '1min15s',
|
|
publicPath: '/corte/123',
|
|
mediaUrl: null,
|
|
mediaFilename: null,
|
|
youtubeVideoId: null,
|
|
publishedAt: null,
|
|
failureMessage: null,
|
|
...overrides,
|
|
})
|
|
|
|
const jsonResponse = (payload: unknown, ok = true) => ({ ok, json: async () => payload })
|
|
|
|
const fetchMock = vi.fn()
|
|
|
|
const renderDialog = (overrides: Partial<Parameters<typeof SpeechCutDialog>[0]> = {}) => {
|
|
const onPublished = vi.fn()
|
|
const onOpenChange = vi.fn()
|
|
render(
|
|
<SpeechCutDialog
|
|
open
|
|
onOpenChange={onOpenChange}
|
|
speechId={42}
|
|
speechType="BREVES COMUNICAÇÕES"
|
|
dateLabel="11/08/2026"
|
|
summary="Saúde e educação em Feira de Santana."
|
|
range={RANGE}
|
|
onPublished={onPublished}
|
|
{...overrides}
|
|
/>,
|
|
)
|
|
return { onPublished, onOpenChange }
|
|
}
|
|
|
|
beforeEach(() => {
|
|
vi.stubGlobal(
|
|
'ResizeObserver',
|
|
class {
|
|
observe() {}
|
|
unobserve() {}
|
|
disconnect() {}
|
|
},
|
|
)
|
|
Element.prototype.scrollIntoView = () => {}
|
|
vi.stubGlobal('fetch', fetchMock)
|
|
})
|
|
|
|
afterEach(() => {
|
|
cleanup()
|
|
fetchMock.mockReset()
|
|
vi.unstubAllGlobals()
|
|
Reflect.deleteProperty(Element.prototype, 'scrollIntoView')
|
|
})
|
|
|
|
describe('SpeechCutDialog (C167)', () => {
|
|
it('pre-fills the deterministic fallback, then swaps in the AI suggestion on untouched fields', async () => {
|
|
fetchMock.mockResolvedValue(
|
|
jsonResponse({
|
|
status: 'success',
|
|
suggestion: { title: 'Acesso a medicamentos', description: 'Sugestão IA', source: 'ai' },
|
|
}),
|
|
)
|
|
|
|
renderDialog()
|
|
|
|
await screen.findByDisplayValue('Sugestão IA')
|
|
expect(screen.getByDisplayValue('Acesso a medicamentos')).toBeTruthy()
|
|
expect(screen.getByText(/Sugerido por IA/)).toBeTruthy()
|
|
})
|
|
|
|
it('never overwrites a field the user has already typed in', async () => {
|
|
let resolveSuggestion!: (response: unknown) => void
|
|
const suggestion = new Promise((resolve) => {
|
|
resolveSuggestion = resolve
|
|
})
|
|
fetchMock.mockImplementation(async (input: RequestInfo | URL) => {
|
|
if (String(input).endsWith(SUGGESTION_ENDPOINT)) return suggestion
|
|
throw new Error(`unexpected fetch: ${String(input)}`)
|
|
})
|
|
|
|
renderDialog()
|
|
fireEvent.change(screen.getByLabelText('Título'), { target: { value: 'Meu título' } })
|
|
|
|
resolveSuggestion(
|
|
jsonResponse({
|
|
status: 'success',
|
|
suggestion: { title: 'Título da IA', description: 'Descrição da IA', source: 'ai' },
|
|
}),
|
|
)
|
|
|
|
await screen.findByDisplayValue('Descrição da IA')
|
|
expect(screen.getByDisplayValue('Meu título')).toBeTruthy()
|
|
expect(screen.queryByDisplayValue('Título da IA')).toBeNull()
|
|
})
|
|
|
|
it('creates, follows the steps and hands the published cut to the caller', async () => {
|
|
fetchMock.mockImplementation(async (input: RequestInfo | URL, init?: RequestInit) => {
|
|
const url = String(input)
|
|
if (url.endsWith(SUGGESTION_ENDPOINT)) {
|
|
return jsonResponse({
|
|
status: 'success',
|
|
suggestion: { title: 'Fallback', description: 'Fallback', source: 'fallback' },
|
|
})
|
|
}
|
|
if (url.endsWith(SAVE_ENDPOINT)) {
|
|
expect(JSON.parse(String(init?.body))).toMatchObject({
|
|
speechId: 42,
|
|
startSeconds: 43,
|
|
endSeconds: 118,
|
|
})
|
|
return jsonResponse({ status: 'success', cut: cutView() })
|
|
}
|
|
if (url.endsWith(STATUS_ENDPOINT)) {
|
|
return jsonResponse({
|
|
status: 'success',
|
|
cut: cutView({
|
|
status: 'published',
|
|
step: null,
|
|
mediaUrl: '/api/media/file/corte-123-43-118.mp4',
|
|
mediaFilename: 'corte-123-43-118.mp4',
|
|
}),
|
|
})
|
|
}
|
|
throw new Error(`unexpected fetch: ${url}`)
|
|
})
|
|
|
|
const { onPublished, onOpenChange } = renderDialog()
|
|
await screen.findByDisplayValue(/Trecho de BREVES COMUNICAÇÕES/)
|
|
|
|
fireEvent.click(screen.getByRole('button', { name: /Cortar e publicar/ }))
|
|
|
|
await screen.findByText('Preparando o corte…')
|
|
await waitFor(() => expect(onPublished).toHaveBeenCalledTimes(1), { timeout: 4000 })
|
|
expect(onPublished.mock.calls[0][0].publicPath).toBe('/corte/123')
|
|
expect(onOpenChange).toHaveBeenCalledWith(false)
|
|
})
|
|
|
|
it('shows the cause the row mapped instead of the generic failure line', async () => {
|
|
fetchMock.mockImplementation(async (input: RequestInfo | URL) => {
|
|
const url = String(input)
|
|
if (url.endsWith(SUGGESTION_ENDPOINT)) {
|
|
return jsonResponse({
|
|
status: 'success',
|
|
suggestion: { title: 'Fallback', description: 'Fallback', source: 'fallback' },
|
|
})
|
|
}
|
|
if (url.endsWith(SAVE_ENDPOINT)) {
|
|
return jsonResponse({ status: 'success', cut: cutView() })
|
|
}
|
|
if (url.endsWith(STATUS_ENDPOINT)) {
|
|
return jsonResponse({
|
|
status: 'success',
|
|
cut: cutView({
|
|
status: 'failed',
|
|
step: null,
|
|
failureMessage: 'A Câmara ainda está gerando o vídeo deste trecho.',
|
|
}),
|
|
})
|
|
}
|
|
throw new Error(`unexpected fetch: ${url}`)
|
|
})
|
|
|
|
renderDialog()
|
|
await screen.findByDisplayValue(/Trecho de BREVES COMUNICAÇÕES/)
|
|
fireEvent.click(screen.getByRole('button', { name: /Cortar e publicar/ }))
|
|
|
|
await screen.findByText(
|
|
'A Câmara ainda está gerando o vídeo deste trecho.',
|
|
{},
|
|
{ timeout: 4000 },
|
|
)
|
|
expect(screen.queryByText(/A Câmara não está entregando o vídeo desta fala agora/)).toBeNull()
|
|
})
|
|
|
|
it('drops the Câmara copy when the source is a web speech (C217)', async () => {
|
|
fetchMock.mockImplementation(async (input: RequestInfo | URL) => {
|
|
const url = String(input)
|
|
if (url.endsWith(SUGGESTION_ENDPOINT)) {
|
|
return jsonResponse({
|
|
status: 'success',
|
|
suggestion: { title: '', description: '', source: 'fallback' },
|
|
})
|
|
}
|
|
if (url.endsWith(SAVE_ENDPOINT)) {
|
|
return jsonResponse({ status: 'success', cut: cutView({ step: 'resolving' }) })
|
|
}
|
|
if (url.endsWith(STATUS_ENDPOINT)) {
|
|
return jsonResponse({
|
|
status: 'success',
|
|
cut: cutView({ status: 'failed', step: 'resolving', failureMessage: null }),
|
|
})
|
|
}
|
|
throw new Error(`unexpected fetch: ${url}`)
|
|
})
|
|
|
|
renderDialog({ speechSource: 'web', speechType: null, summary: null, dateLabel: '24/09/2026' })
|
|
|
|
// The deterministic fallback never claims the Câmara.
|
|
await screen.findByDisplayValue('Trecho de fala de 24/09/2026, publicado na internet.')
|
|
expect(screen.queryByDisplayValue(/na Câmara dos Deputados/)).toBeNull()
|
|
|
|
fireEvent.click(screen.getByRole('button', { name: /Cortar e publicar/ }))
|
|
|
|
await screen.findByText(
|
|
'Não foi possível preparar o corte desta fala agora. Tente novamente em alguns minutos.',
|
|
{},
|
|
{ timeout: 4000 },
|
|
)
|
|
expect(screen.queryByText(/A Câmara não está entregando o vídeo desta fala agora/)).toBeNull()
|
|
})
|
|
|
|
it('shows the honest failure and retries the same row (no duplicate create)', async () => {
|
|
const retryBodies: unknown[] = []
|
|
fetchMock.mockImplementation(async (input: RequestInfo | URL, init?: RequestInit) => {
|
|
const url = String(input)
|
|
if (url.endsWith(SUGGESTION_ENDPOINT)) {
|
|
return jsonResponse({
|
|
status: 'success',
|
|
suggestion: { title: 'Fallback', description: 'Fallback', source: 'fallback' },
|
|
})
|
|
}
|
|
if (url.endsWith(SAVE_ENDPOINT)) {
|
|
retryBodies.push(JSON.parse(String(init?.body)))
|
|
return jsonResponse({ status: 'success', cut: cutView() })
|
|
}
|
|
if (url.endsWith(STATUS_ENDPOINT)) {
|
|
return jsonResponse({ status: 'success', cut: cutView({ status: 'failed', step: null }) })
|
|
}
|
|
throw new Error(`unexpected fetch: ${url}`)
|
|
})
|
|
|
|
renderDialog()
|
|
await screen.findByDisplayValue(/Trecho de BREVES COMUNICAÇÕES/)
|
|
fireEvent.click(screen.getByRole('button', { name: /Cortar e publicar/ }))
|
|
|
|
const retry = await screen.findByRole('button', { name: 'Tentar novamente' }, { timeout: 4000 })
|
|
fireEvent.click(retry)
|
|
|
|
await waitFor(() => expect(retryBodies).toHaveLength(2))
|
|
expect(retryBodies[1]).toEqual({ retryOf: 123 })
|
|
})
|
|
})
|