// @vitest-environment jsdom
import React from 'react';
import { cleanup, render, screen } from '@testing-library/react';
import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest';
import '@testing-library/jest-dom';
// Mock i18n formatters (used by InvoiceSyncBadge via useFormatters)
vi.mock('@alga-psa/ui/lib/i18n/client', () => ({
useFormatters: () => ({
formatDate: (value: string) => new Date(value).toLocaleDateString('en-US', { year: 'numeric', month: 'short', day: 'numeric' }),
formatCurrency: (value: number) => `$${value}`,
}),
useTranslation: () => ({ t: (key: string, opts?: { defaultValue?: string }) => opts?.defaultValue ?? key }),
}));
// Minimal Badge stub
vi.mock('@alga-psa/ui/components/Badge', () => ({
Badge: ({ children, id, variant }: { children: React.ReactNode; id?: string; variant?: string }) => (
{children}
),
}));
// Minimal Tooltip stub — renders content inline so we can assert on tooltip text
vi.mock('@alga-psa/ui/components/Tooltip', () => ({
Tooltip: ({ children, content }: { children: React.ReactNode; content: React.ReactNode }) => (
),
}));
import { InvoiceSyncBadge, qboInvoiceDeepLink } from '../src/components/invoices/InvoiceSyncBadge';
describe('InvoiceSyncBadge', () => {
beforeEach(() => {
cleanup();
});
afterEach(() => {
cleanup();
});
it('renders "Not synced" for not_synced state', () => {
render();
// getAllByText because the Tooltip mock also echoes label text in the tooltip-content div
expect(screen.getAllByText('Not synced').length).toBeGreaterThan(0);
expect(screen.getByTestId('badge')).toHaveAttribute('data-variant', 'secondary');
});
it('renders "Queued" for queued state', () => {
render();
expect(screen.getAllByText('Queued').length).toBeGreaterThan(0);
expect(screen.getByTestId('badge')).toHaveAttribute('data-variant', 'secondary');
});
it('renders "Synced" for synced state with success variant', () => {
render();
expect(screen.getAllByText('Synced').length).toBeGreaterThan(0);
expect(screen.getByTestId('badge')).toHaveAttribute('data-variant', 'success');
});
it('renders "Drift" for drift state with warning variant', () => {
render();
expect(screen.getAllByText('Drift').length).toBeGreaterThan(0);
expect(screen.getByTestId('badge')).toHaveAttribute('data-variant', 'warning');
});
it('renders "Sync error" for error state with error variant', () => {
render();
expect(screen.getAllByText('Sync error').length).toBeGreaterThan(0);
expect(screen.getByTestId('badge')).toHaveAttribute('data-variant', 'error');
});
it('renders "Voided" for voided state', () => {
render();
expect(screen.getAllByText('Voided').length).toBeGreaterThan(0);
expect(screen.getByTestId('badge')).toHaveAttribute('data-variant', 'secondary');
});
it('tooltip contains doc number for synced state', () => {
render(
,
);
const tooltip = screen.getByTestId('tooltip-content');
expect(tooltip).toHaveTextContent('INV-1234');
});
it('tooltip contains "View in QuickBooks" link when externalId is present for synced state', () => {
render(
,
);
const link = screen.getByRole('link', { name: 'View in QuickBooks' });
expect(link).toBeInTheDocument();
expect(link).toHaveAttribute('target', '_blank');
expect(link).toHaveAttribute('href', expect.stringContaining('ext-abc-123'));
});
it('sandbox deep link points to sandbox domain', () => {
const url = qboInvoiceDeepLink('txn-123', 'sandbox');
expect(url).toBe('https://app.sandbox.qbo.intuit.com/app/invoice?txnId=txn-123');
});
it('production deep link points to production domain', () => {
const url = qboInvoiceDeepLink('txn-456', 'production');
expect(url).toBe('https://app.qbo.intuit.com/app/invoice?txnId=txn-456');
});
it('default deep link (no environment) points to sandbox domain', () => {
const url = qboInvoiceDeepLink('txn-789');
expect(url).toBe('https://app.sandbox.qbo.intuit.com/app/invoice?txnId=txn-789');
});
it('badge has correct element id with state suffix', () => {
render();
expect(document.getElementById('invoice-sync-badge-drift')).toBeInTheDocument();
});
it('renders error text in tooltip when error is present', () => {
render(
,
);
expect(screen.getByTestId('tooltip-content')).toHaveTextContent('Connection refused');
});
});