import React, { act } from 'react'; import { createRoot } from 'react-dom/client'; import { afterEach, describe, expect, it, vi } from 'vitest'; import type { ClaudeLogsController } from '@renderer/components/team/useClaudeLogsController'; const cliLogsRichViewState = vi.hoisted(() => ({ calls: [] as Record[], })); vi.mock('@renderer/components/team/CliLogsRichView', () => ({ CliLogsRichView: (props: Record) => { cliLogsRichViewState.calls.push(props); const cliLogsTail = typeof props.cliLogsTail === 'string' ? props.cliLogsTail : ''; return React.createElement('div', { 'data-testid': 'cli-logs-rich-view' }, cliLogsTail); }, })); vi.mock('@renderer/components/team/ClaudeLogsFilterPopover', () => ({ ClaudeLogsFilterPopover: () => React.createElement('div', { 'data-testid': 'logs-filter' }), })); vi.mock('@renderer/components/ui/button', () => ({ Button: ({ children, onClick, disabled, }: { children: React.ReactNode; onClick?: () => void; disabled?: boolean; }) => React.createElement('button', { type: 'button', onClick, disabled }, children), })); import { ClaudeLogsPanel } from '@renderer/components/team/ClaudeLogsPanel'; function createController(overrides: Partial = {}): ClaudeLogsController { return { data: { lines: [], total: 0, hasMore: false }, loading: false, loadingMore: false, error: null, pendingNewCount: 0, isAlive: false, filteredText: '', online: false, badge: undefined, showMoreVisible: false, lastLogPreview: null, searchQuery: '', setSearchQuery: vi.fn(), filter: { streams: new Set(), kinds: new Set() } as ClaudeLogsController['filter'], setFilter: vi.fn(), filterOpen: false, setFilterOpen: vi.fn(), viewerState: {} as ClaudeLogsController['viewerState'], onViewerStateChange: vi.fn(), applyPending: vi.fn(() => Promise.resolve()), loadOlderLogs: vi.fn(() => Promise.resolve()), containerRefCallback: vi.fn(), handleScroll: vi.fn(), ...overrides, }; } describe('ClaudeLogsPanel', () => { afterEach(() => { document.body.innerHTML = ''; cliLogsRichViewState.calls = []; vi.unstubAllGlobals(); }); it('renders logs even when the team is offline if log lines are available', async () => { vi.stubGlobal('IS_REACT_ACT_ENVIRONMENT', true); const host = document.createElement('div'); document.body.appendChild(host); const root = createRoot(host); const ctrl = createController({ isAlive: false, data: { lines: ['second line', 'first line'], total: 2, hasMore: false, updatedAt: '2026-04-19T10:00:01.000Z', }, filteredText: '[stdout]\nfirst line\nsecond line', badge: '2 raw', }); await act(async () => { root.render(React.createElement(ClaudeLogsPanel, { ctrl })); await Promise.resolve(); }); expect(host.textContent).toContain('2 raw lines'); expect(host.textContent).toContain('first line'); expect(host.textContent).not.toContain('Team is not running.'); expect(host.querySelector('[data-testid="cli-logs-rich-view"]')).not.toBeNull(); expect(cliLogsRichViewState.calls.at(-1)?.cliLogsTail).toBe( '[stdout]\nfirst line\nsecond line' ); await act(async () => { root.unmount(); await Promise.resolve(); }); }); it('renders leading toolbar controls before the search field', async () => { vi.stubGlobal('IS_REACT_ACT_ENVIRONMENT', true); const host = document.createElement('div'); document.body.appendChild(host); const root = createRoot(host); const ctrl = createController({ isAlive: true, data: { lines: ['lead output'], total: 1, hasMore: false, }, filteredText: 'lead output', }); await act(async () => { root.render( React.createElement(ClaudeLogsPanel, { ctrl, toolbarControlsStart: React.createElement( 'div', { 'data-testid': 'toolbar-source' }, 'Lead' ), }) ); await Promise.resolve(); }); const source = host.querySelector('[data-testid="toolbar-source"]'); const search = host.querySelector('input[placeholder="Search logs..."]'); expect(source).not.toBeNull(); expect(search).not.toBeNull(); expect(source?.compareDocumentPosition(search as Node)).toBe( Node.DOCUMENT_POSITION_FOLLOWING ); await act(async () => { root.unmount(); await Promise.resolve(); }); }); it('shows the offline empty state only when no logs exist', async () => { vi.stubGlobal('IS_REACT_ACT_ENVIRONMENT', true); const host = document.createElement('div'); document.body.appendChild(host); const root = createRoot(host); const ctrl = createController({ isAlive: false, data: { lines: [], total: 0, hasMore: false }, filteredText: '', }); await act(async () => { root.render(React.createElement(ClaudeLogsPanel, { ctrl })); await Promise.resolve(); }); expect(host.textContent).toContain('Team is not running.'); expect(host.querySelector('[data-testid="cli-logs-rich-view"]')).toBeNull(); await act(async () => { root.unmount(); await Promise.resolve(); }); }); it('explains raw-only logs instead of showing an empty displayable-log message', async () => { vi.stubGlobal('IS_REACT_ACT_ENVIRONMENT', true); const host = document.createElement('div'); document.body.appendChild(host); const root = createRoot(host); const ctrl = createController({ isAlive: true, data: { lines: [ '[stdout] {"type":"system","subtype":"init"}', '[stdout] {"type":"thread.started","thread_id":"thread-1"}', ], total: 16, hasMore: false, }, filteredText: '[stdout]\n{"type":"thread.started","thread_id":"thread-1"}', badge: '16 raw', }); await act(async () => { root.render(React.createElement(ClaudeLogsPanel, { ctrl })); await Promise.resolve(); }); expect(host.textContent).toContain('16 raw lines captured'); expect(cliLogsRichViewState.calls.at(-1)?.emptyMessageOverride).toBe( '16 raw lines captured; none are assistant/tool output yet.' ); await act(async () => { root.unmount(); await Promise.resolve(); }); }); it('renders toolbar accessory beside log search and filters', async () => { vi.stubGlobal('IS_REACT_ACT_ENVIRONMENT', true); const host = document.createElement('div'); document.body.appendChild(host); const root = createRoot(host); const ctrl = createController({ isAlive: true, data: { lines: ['[stdout] ready'], total: 1, hasMore: false, }, filteredText: '[stdout]\nready', }); await act(async () => { root.render( React.createElement(ClaudeLogsPanel, { ctrl, compactMetaInTooltip: true, toolbarAccessory: React.createElement( 'button', { type: 'button', 'data-testid': 'log-member-selector' }, 'Lead' ), }) ); await Promise.resolve(); }); const search = host.querySelector('input[placeholder="Search logs..."]'); const accessory = host.querySelector('[data-testid="log-member-selector"]'); const filter = host.querySelector('[data-testid="logs-filter"]'); expect(search).not.toBeNull(); expect(accessory).not.toBeNull(); expect(filter).not.toBeNull(); expect(search?.parentElement?.className).toContain('flex-1'); expect(search?.compareDocumentPosition(accessory as Node) ?? 0).toBe( Node.DOCUMENT_POSITION_FOLLOWING ); expect(accessory?.compareDocumentPosition(filter as Node) ?? 0).toBe( Node.DOCUMENT_POSITION_FOLLOWING ); await act(async () => { root.unmount(); await Promise.resolve(); }); }); });