mirror of
https://github.com/fsolla/teqo.git
synced 2026-11-12 22:13:05 +00:00
218 lines
7.5 KiB
TypeScript
218 lines
7.5 KiB
TypeScript
import { cleanup, fireEvent, render, screen, waitFor, within } from '@testing-library/react'
|
|
import { afterAll, afterEach, beforeAll, beforeEach, describe, expect, it, vi } from 'vitest'
|
|
|
|
import type {
|
|
GoogleCalendarListActionResult,
|
|
GoogleCalendarSyncActionResult,
|
|
} from '@/app/(campaign)/campanha/actions/googleCalendarSync'
|
|
import { GoogleCalendarPickerDialog } from '@/components/campaign/activity/GoogleCalendarPickerDialog'
|
|
|
|
/**
|
|
* C150 — the primary-calendar picker: live list states (loading/error/empty),
|
|
* the "em uso" mark, the confirm gating and the choose callback. Desktop
|
|
* layout (matchMedia false); the mobile drawer shares the same content.
|
|
*/
|
|
|
|
const CALENDAR_A = 'c_campanha@group.calendar.google.com'
|
|
const CALENDAR_B = 'c_pessoal@group.calendar.google.com'
|
|
|
|
const listResult: GoogleCalendarListActionResult = {
|
|
ok: true,
|
|
calendars: [
|
|
{ id: CALENDAR_A, summary: 'Agenda da Campanha', primary: false },
|
|
{ id: CALENDAR_B, summary: 'Meu calendário', primary: true },
|
|
],
|
|
}
|
|
|
|
const syncResult = (
|
|
overrides: Partial<GoogleCalendarSyncActionResult> = {},
|
|
): GoogleCalendarSyncActionResult => ({
|
|
ok: true,
|
|
canManageConnection: true,
|
|
connection: 'connected',
|
|
oauthAvailable: true,
|
|
oauthConnectedAt: '2026-08-01T12:00:00.000Z',
|
|
oauthError: null,
|
|
status: 'synced',
|
|
calendarId: CALENDAR_B,
|
|
lastSyncedAt: null,
|
|
lastSuccessAt: null,
|
|
lastErrorAt: null,
|
|
lastError: null,
|
|
pushChannelExpiresAt: null,
|
|
pushChannelError: null,
|
|
addLink: null,
|
|
...overrides,
|
|
})
|
|
|
|
const renderPicker = (
|
|
overrides: {
|
|
currentCalendarId?: string | null
|
|
onOpenChange?: (open: boolean) => void
|
|
onListCalendars?: () => Promise<GoogleCalendarListActionResult>
|
|
onChooseCalendar?: (calendarId: string) => Promise<GoogleCalendarSyncActionResult>
|
|
} = {},
|
|
) =>
|
|
render(
|
|
<GoogleCalendarPickerDialog
|
|
open
|
|
onOpenChange={overrides.onOpenChange ?? vi.fn()}
|
|
currentCalendarId={overrides.currentCalendarId ?? CALENDAR_A}
|
|
onListCalendars={overrides.onListCalendars ?? vi.fn(async () => listResult)}
|
|
onChooseCalendar={overrides.onChooseCalendar ?? vi.fn(async () => syncResult())}
|
|
/>,
|
|
)
|
|
|
|
const matchMediaMock = vi.fn()
|
|
|
|
beforeAll(() => {
|
|
vi.stubGlobal(
|
|
'ResizeObserver',
|
|
class {
|
|
observe() {}
|
|
unobserve() {}
|
|
disconnect() {}
|
|
},
|
|
)
|
|
Element.prototype.scrollIntoView = () => {}
|
|
})
|
|
|
|
beforeEach(() => {
|
|
matchMediaMock.mockImplementation((query: string) => ({
|
|
matches: false,
|
|
media: query,
|
|
addEventListener: vi.fn(),
|
|
removeEventListener: vi.fn(),
|
|
addListener: vi.fn(),
|
|
removeListener: vi.fn(),
|
|
dispatchEvent: vi.fn(),
|
|
}))
|
|
vi.stubGlobal('matchMedia', matchMediaMock)
|
|
})
|
|
|
|
afterEach(() => {
|
|
cleanup()
|
|
vi.clearAllMocks()
|
|
})
|
|
|
|
afterAll(() => {
|
|
vi.unstubAllGlobals()
|
|
})
|
|
|
|
describe('GoogleCalendarPickerDialog (C150)', () => {
|
|
it('shows the loading state while the live list is fetched', () => {
|
|
renderPicker({
|
|
onListCalendars: vi.fn((): Promise<GoogleCalendarListActionResult> => new Promise(() => {})),
|
|
})
|
|
|
|
expect(screen.getByText('Carregando calendários...')).toBeTruthy()
|
|
})
|
|
|
|
it('lists the calendars, marks the current one and disables the no-op confirm', async () => {
|
|
renderPicker()
|
|
|
|
const picker = await screen.findByRole('dialog', {
|
|
name: /Calendário principal da campanha/,
|
|
})
|
|
const current = within(picker).getByRole('button', {
|
|
name: /Agenda da Campanha/,
|
|
pressed: true,
|
|
})
|
|
expect(current.getAttribute('aria-pressed')).toBe('true')
|
|
expect(within(picker).getByText('em uso')).toBeTruthy()
|
|
expect(within(picker).getByText('Calendário principal da conta')).toBeTruthy()
|
|
|
|
const confirm = within(picker).getByRole('button', { name: 'Escolher calendário' })
|
|
expect(confirm.hasAttribute('disabled')).toBe(true)
|
|
})
|
|
|
|
it('chooses another calendar and closes on success', async () => {
|
|
const onOpenChange = vi.fn()
|
|
const onChooseCalendar = vi.fn(async () => syncResult())
|
|
renderPicker({ onOpenChange, onChooseCalendar })
|
|
|
|
const picker = await screen.findByRole('dialog', {
|
|
name: /Calendário principal da campanha/,
|
|
})
|
|
fireEvent.click(within(picker).getByRole('button', { name: /Meu calendário/ }))
|
|
fireEvent.click(within(picker).getByRole('button', { name: 'Escolher calendário' }))
|
|
|
|
await waitFor(() => expect(onChooseCalendar).toHaveBeenCalledWith(CALENDAR_B))
|
|
await waitFor(() => expect(onOpenChange).toHaveBeenCalledWith(false))
|
|
})
|
|
|
|
it('keeps the picker open and shows the failure message', async () => {
|
|
const onOpenChange = vi.fn()
|
|
const onChooseCalendar = vi.fn(async () =>
|
|
syncResult({ ok: false, message: 'O calendário escolhido não está mais disponível.' }),
|
|
)
|
|
renderPicker({ onOpenChange, onChooseCalendar })
|
|
|
|
const picker = await screen.findByRole('dialog', {
|
|
name: /Calendário principal da campanha/,
|
|
})
|
|
fireEvent.click(within(picker).getByRole('button', { name: /Meu calendário/ }))
|
|
fireEvent.click(within(picker).getByRole('button', { name: 'Escolher calendário' }))
|
|
|
|
expect(await within(picker).findByText(/não está mais disponível/)).toBeTruthy()
|
|
expect(onOpenChange).not.toHaveBeenCalled()
|
|
})
|
|
|
|
it('offers a retry when the list fails', async () => {
|
|
const onListCalendars = vi
|
|
.fn<() => Promise<GoogleCalendarListActionResult>>()
|
|
.mockResolvedValueOnce({ ok: false, message: 'Não foi possível listar os calendários.' })
|
|
.mockResolvedValueOnce(listResult)
|
|
renderPicker({ onListCalendars })
|
|
|
|
expect(await screen.findByText('Não foi possível listar os calendários.')).toBeTruthy()
|
|
fireEvent.click(screen.getByRole('button', { name: 'Tentar de novo' }))
|
|
|
|
expect(await screen.findByText('Agenda da Campanha')).toBeTruthy()
|
|
expect(onListCalendars).toHaveBeenCalledTimes(2)
|
|
})
|
|
|
|
it('shows the empty state when the account has no writable calendar', async () => {
|
|
renderPicker({
|
|
onListCalendars: vi.fn(
|
|
async (): Promise<GoogleCalendarListActionResult> => ({ ok: true, calendars: [] }),
|
|
),
|
|
})
|
|
|
|
expect(
|
|
await screen.findByText('Nenhum calendário com permissão de edição nesta conta Google.'),
|
|
).toBeTruthy()
|
|
expect(screen.queryByRole('button', { name: 'Escolher calendário' })).toBeNull()
|
|
})
|
|
|
|
it('maps a rejected list action to the error state with retry', async () => {
|
|
const onListCalendars = vi
|
|
.fn<() => Promise<GoogleCalendarListActionResult>>()
|
|
.mockRejectedValueOnce(new Error('transport down'))
|
|
.mockResolvedValueOnce(listResult)
|
|
renderPicker({ onListCalendars })
|
|
|
|
expect(await screen.findByText('Não foi possível listar os calendários agora.')).toBeTruthy()
|
|
fireEvent.click(screen.getByRole('button', { name: 'Tentar de novo' }))
|
|
expect(await screen.findByText('Agenda da Campanha')).toBeTruthy()
|
|
})
|
|
|
|
it('maps a rejected choose action to an inline error without closing', async () => {
|
|
const onOpenChange = vi.fn()
|
|
const onChooseCalendar = vi
|
|
.fn<() => Promise<GoogleCalendarSyncActionResult>>()
|
|
.mockRejectedValueOnce(new Error('transport down'))
|
|
renderPicker({ onOpenChange, onChooseCalendar })
|
|
|
|
const picker = await screen.findByRole('dialog', {
|
|
name: /Calendário principal da campanha/,
|
|
})
|
|
fireEvent.click(within(picker).getByRole('button', { name: /Meu calendário/ }))
|
|
fireEvent.click(within(picker).getByRole('button', { name: 'Escolher calendário' }))
|
|
|
|
expect(
|
|
await within(picker).findByText(/Não foi possível escolher o calendário agora/),
|
|
).toBeTruthy()
|
|
expect(onOpenChange).not.toHaveBeenCalled()
|
|
})
|
|
})
|