import React, { act } from 'react'; import { createRoot } from 'react-dom/client'; import { MemberSelect } from '@renderer/components/ui/MemberSelect'; import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest'; import type { ResolvedTeamMember } from '@shared/types'; vi.mock('@renderer/hooks/useTheme', () => ({ useTheme: () => ({ theme: 'dark', resolvedTheme: 'dark', isDark: true, isLight: false, }), })); function member(name: string, overrides: Partial = {}): ResolvedTeamMember { return { name, status: 'active', currentTaskId: null, taskCount: 0, lastActiveAt: null, messageCount: 0, ...overrides, }; } async function flush(): Promise { await Promise.resolve(); await Promise.resolve(); } describe('MemberSelect', () => { let host: HTMLDivElement; let root: ReturnType; let originalScrollIntoView: typeof HTMLElement.prototype.scrollIntoView; beforeEach(() => { vi.stubGlobal('IS_REACT_ACT_ENVIRONMENT', true); originalScrollIntoView = HTMLElement.prototype.scrollIntoView; HTMLElement.prototype.scrollIntoView = vi.fn(); host = document.createElement('div'); document.body.appendChild(host); root = createRoot(host); }); afterEach(async () => { await act(async () => { root.unmount(); await flush(); }); document.body.innerHTML = ''; HTMLElement.prototype.scrollIntoView = originalScrollIntoView; vi.unstubAllGlobals(); vi.clearAllMocks(); }); it('preserves Create Task defaults for unassigned and lead display', async () => { const onChange = vi.fn(); await act(async () => { root.render( ); await flush(); }); const trigger = host.querySelector('button[role="combobox"]') as HTMLButtonElement; expect(trigger.textContent).toContain('Unassigned'); expect(trigger.getAttribute('aria-label')).toBeNull(); await act(async () => { trigger.click(); await flush(); }); const list = document.body.querySelector('[cmdk-list]') as HTMLElement | null; expect(list?.textContent).toContain('Unassigned'); expect(list?.textContent).toContain('lead'); expect(list?.textContent).toContain('alice'); expect(document.body.querySelector('input')?.getAttribute('placeholder')).toBe( 'Search members...' ); }); it('supports custom log-source labels, descriptions, search text, and selection', async () => { const onChange = vi.fn(); await act(async () => { root.render( { if (candidate.name === 'team-lead') return 'Lead'; if (candidate.removedAt) return `${candidate.name} (removed)`; return candidate.name; }} getMemberDescription={(candidate) => { if (candidate.name === 'team-lead') return 'Team Lead'; if (candidate.removedAt) return 'Removed'; return 'Reviewer'; }} /> ); await flush(); }); const trigger = host.querySelector('button[role="combobox"]') as HTMLButtonElement; expect(trigger.textContent).toContain('Lead'); expect(trigger.getAttribute('aria-label')).toBe('Log source'); await act(async () => { trigger.click(); await flush(); }); const input = document.body.querySelector('input') as HTMLInputElement; const list = document.body.querySelector('[cmdk-list]') as HTMLElement | null; expect(input.getAttribute('placeholder')).toBe('Search log sources...'); expect(list?.textContent).toContain('Lead'); expect(list?.textContent).toContain('Team Lead'); expect(list?.textContent).toContain('Builder (removed)'); expect(list?.textContent).toContain('Removed'); await act(async () => { const valueSetter = Object.getOwnPropertyDescriptor(HTMLInputElement.prototype, 'value')?.set; valueSetter?.call(input, 'removed'); input.dispatchEvent(new Event('input', { bubbles: true })); await flush(); }); expect(list?.textContent).toContain('Builder (removed)'); expect(list?.textContent).not.toContain('Reviewer'); expect(list?.textContent).not.toContain('Team Lead'); const builderItem = Array.from(list?.querySelectorAll('[cmdk-item]') ?? []).find((item) => item.textContent?.includes('Builder (removed)') ) as HTMLElement | undefined; expect(builderItem).toBeDefined(); await act(async () => { builderItem?.click(); await flush(); }); expect(onChange).toHaveBeenCalledWith('Builder'); }); it('uses an avatar trigger for dense surfaces while keeping the full member list popover', async () => { const onChange = vi.fn(); await act(async () => { root.render( ); await flush(); }); const trigger = host.querySelector('button[role="combobox"]') as HTMLButtonElement | null; expect(trigger).not.toBeNull(); expect(trigger?.getAttribute('aria-label')).toBe('Select member: Lead'); expect(trigger?.getAttribute('title')).toBe('Lead'); expect(host.textContent).not.toContain('Lead'); await act(async () => { trigger?.click(); await flush(); }); expect(document.body.textContent).toContain('Lead'); expect(document.body.textContent).toContain('Alice'); }); });