import { describe, it, expect, beforeEach, afterEach, vi } from 'vitest' import { render, screen, cleanup } from '@testing-library/react' import { I18nProvider } from '@/lib/i18n-context' import { CursorMigrationBanner, isCursorMigrationAmbiguous, isCursorMigrationInProgress } from './CursorMigrationBanner' import type { Metadata } from '@/types/api' function renderWithProviders(ui: React.ReactElement) { return render({ui}) } afterEach(() => { cleanup() }) function metadata(partial: Partial = {}): Metadata { return { path: '/tmp/x', host: 'localhost', ...partial } as Metadata } describe('isCursorMigrationInProgress', () => { it('returns true when flag is in_progress', () => { expect(isCursorMigrationInProgress(metadata({ cursorMigrationState: 'in_progress' }))).toBe(true) }) it('returns false when flag is undefined', () => { expect(isCursorMigrationInProgress(metadata())).toBe(false) }) it('returns false when metadata is null', () => { expect(isCursorMigrationInProgress(null)).toBe(false) }) it('returns false when metadata is undefined', () => { expect(isCursorMigrationInProgress(undefined)).toBe(false) }) }) describe('CursorMigrationBanner', () => { beforeEach(() => { vi.clearAllMocks() Object.defineProperty(window, 'localStorage', { value: { getItem: vi.fn(() => 'en'), setItem: vi.fn(), removeItem: vi.fn(), clear: vi.fn(), key: vi.fn(() => null), length: 0 }, configurable: true }) }) it('renders the banner when cursorMigrationState is in_progress', () => { renderWithProviders() expect(screen.getByTestId('cursor-migration-banner')).toBeInTheDocument() expect(screen.getByText('Upgrading Cursor session')).toBeInTheDocument() expect(screen.getByText(/safer ACP protocol/)).toBeInTheDocument() }) it('does not render when cursorMigrationState is undefined', () => { renderWithProviders() expect(screen.queryByTestId('cursor-migration-banner')).not.toBeInTheDocument() }) it('does not render when metadata is null', () => { renderWithProviders() expect(screen.queryByTestId('cursor-migration-banner')).not.toBeInTheDocument() }) it('does not render when metadata is undefined', () => { renderWithProviders() expect(screen.queryByTestId('cursor-migration-banner')).not.toBeInTheDocument() }) it('does not render when migration is complete (cursorSessionProtocol is acp but no in_progress flag)', () => { renderWithProviders() expect(screen.queryByTestId('cursor-migration-banner')).not.toBeInTheDocument() }) it('uses role=status and aria-live=polite for screen-reader accessibility', () => { renderWithProviders() const status = screen.getByRole('status') expect(status).toHaveAttribute('aria-live', 'polite') }) /** * tiann/hapi#873: when the migrator refuses to transplant a legacy * store (ambiguous source or size mismatch), the hub promotes * cursorMigrationState to 'ambiguous'. The banner must switch to a * "manual review needed" surface rather than disappear. */ it('renders the ambiguous banner when cursorMigrationState is ambiguous', () => { renderWithProviders() expect(screen.getByTestId('cursor-migration-banner-ambiguous')).toBeInTheDocument() expect(screen.getByText('Cursor session upgrade needs manual review')).toBeInTheDocument() expect(screen.queryByTestId('cursor-migration-banner')).not.toBeInTheDocument() }) it('uses role=alert on the ambiguous banner so it surfaces over the in-progress styling', () => { renderWithProviders() const alert = screen.getByRole('alert') expect(alert).toHaveAttribute('aria-live', 'polite') }) it('isCursorMigrationAmbiguous returns true only for the ambiguous state', () => { expect(isCursorMigrationAmbiguous(metadata({ cursorMigrationState: 'ambiguous' }))).toBe(true) expect(isCursorMigrationAmbiguous(metadata({ cursorMigrationState: 'in_progress' }))).toBe(false) expect(isCursorMigrationAmbiguous(metadata())).toBe(false) expect(isCursorMigrationAmbiguous(null)).toBe(false) expect(isCursorMigrationAmbiguous(undefined)).toBe(false) }) })