import { render, screen, fireEvent, act } from '@testing-library/react' import { MembersTable, RoleSelector, InviteMemberDialog, PendingInvitationCallout, DomainClaimCard, LeaveTeamDialog, OrgSwitcher, } from '../index' import type { MemberRow, InvitationLite, DomainClaimLite, TeamLite, CompanyLite } from '../types' // ---------- MembersTable --------------------------------------------------- describe('MembersTable', () => { const baseMembers: MemberRow[] = [ { id: 'm1', userId: 'u1', teamId: 't1', role: 'owner', joinedAt: '2025-01-01', user: { id: 'u1', email: 'owner@x.com', firstName: 'Olivia', lastName: 'Owner' }, }, { id: 'm2', userId: 'u2', teamId: 't1', role: 'member', joinedAt: '2025-01-02', user: { id: 'u2', email: 'mem@x.com', firstName: 'Mike', lastName: 'Member' }, }, ] it('renders a row per member with name + email + role', () => { render() expect(screen.getByText('Olivia Owner')).toBeInTheDocument() expect(screen.getByText('owner@x.com')).toBeInTheDocument() expect(screen.getAllByText('Owner').length).toBeGreaterThan(0) expect(screen.getByText('Mike Member')).toBeInTheDocument() }) it('hides the actions column when canManage is false', () => { render() expect(screen.queryByText('Remove')).not.toBeInTheDocument() }) it('shows RoleSelector + Remove when canManage is true', () => { const onChangeRole = jest.fn() const onRemove = jest.fn() render( ) const removes = screen.getAllByText('Remove') expect(removes).toHaveLength(2) fireEvent.click(removes[1]) expect(onRemove).toHaveBeenCalledWith('u2') const selects = screen.getAllByRole('combobox') as HTMLSelectElement[] fireEvent.change(selects[1], { target: { value: 'admin' } }) expect(onChangeRole).toHaveBeenCalledWith('u2', 'admin') }) it('respects classNames overrides', () => { const { container } = render( ) expect(container.firstChild).toHaveClass('my-root-class') }) it('does not print the email twice when it is also the display name', () => { // Foundry maps control-plane members with an email and no name, so // userDisplayName() falls back to the email — the row used to stack the // same string on itself. startsim-1f3n.4. render( ) expect(screen.getAllByText('qosha@debugg.ai')).toHaveLength(1) }) it('marks the signed-in user so they can tell which row is them', () => { // startsim-1f3n.7 — the owner appeared as an anonymous row next to a live // Remove button with nothing saying "this one is you". render() expect(screen.getByText('You')).toBeInTheDocument() const selfRow = screen.getByText('Mike Member').closest('tr')! expect(selfRow).toContainElement(screen.getByText('You')) }) it('renders no You badge when currentUserId matches nobody', () => { render() expect(screen.queryByText('You')).not.toBeInTheDocument() }) it('keeps the Role and actions columns to their content width', () => { // startsim-1f3n.6 — one shared cell class meant Member ate ~55% of the // table and Role/Remove floated in a gap. render() const roleHeader = screen.getByRole('columnheader', { name: 'Role' }) expect(roleHeader.className).toContain('w-px') expect(roleHeader.className).toContain('whitespace-nowrap') }) it('shows the empty state when no members', () => { render() expect(screen.getByText('No members yet.')).toBeInTheDocument() }) }) // ---------- RoleSelector --------------------------------------------------- describe('RoleSelector', () => { it('renders the supplied roles and fires onChange', () => { const onChange = jest.fn() render( ) const select = screen.getByRole('combobox') as HTMLSelectElement expect(select.value).toBe('member') // Only the three supplied roles should be rendered. const options = Array.from(select.querySelectorAll('option')).map((o) => o.value) expect(options).toEqual(['admin', 'member', 'viewer']) fireEvent.change(select, { target: { value: 'admin' } }) expect(onChange).toHaveBeenCalledWith('admin') }) it('honors disabled', () => { render( {}} disabled />) expect(screen.getByRole('combobox')).toBeDisabled() }) }) // ---------- InviteMemberDialog -------------------------------------------- describe('InviteMemberDialog', () => { it('does not render the panel until the trigger is clicked', () => { render() expect(screen.queryByRole('dialog')).not.toBeInTheDocument() fireEvent.click(screen.getByRole('button', { name: /invite member/i })) expect(screen.getByRole('dialog')).toBeInTheDocument() }) it('submits email + role + teamId via onSubmit and shows a success line', async () => { const onSubmit = jest.fn().mockResolvedValue({ id: 'i1' } as InvitationLite) const onInvited = jest.fn() render( ) fireEvent.click(screen.getByRole('button', { name: /invite member/i })) fireEvent.change(screen.getByLabelText('Email'), { target: { value: 'newhire@x.com' }, }) fireEvent.change(screen.getByLabelText('Invite role'), { target: { value: 'admin' }, }) await act(async () => { fireEvent.submit(screen.getByRole('dialog').querySelector('form')!) }) expect(onSubmit).toHaveBeenCalledWith({ email: 'newhire@x.com', teamId: 't1', role: 'admin', }) expect(onInvited).toHaveBeenCalled() expect(screen.getByText(/Invitation sent to newhire@x.com/)).toBeInTheDocument() }) it('surfaces submit errors inline', async () => { const onSubmit = jest.fn().mockRejectedValue(new Error('domain not allowed')) render() fireEvent.click(screen.getByRole('button', { name: /invite member/i })) fireEvent.change(screen.getByLabelText('Email'), { target: { value: 'a@x.com' } }) await act(async () => { fireEvent.submit(screen.getByRole('dialog').querySelector('form')!) }) expect(screen.getByRole('alert')).toHaveTextContent('domain not allowed') }) it('honors classNames slots', () => { render( ) expect(screen.getByRole('button', { name: /invite member/i })).toHaveClass( 'my-trigger' ) }) }) // ---------- PendingInvitationCallout -------------------------------------- describe('PendingInvitationCallout', () => { const invitation: InvitationLite = { id: 'i1', email: 'a@x.com', teamId: 't1', role: 'admin', expiresAt: '2099-01-01', isExpired: false, isAccepted: false, } it('renders the message + team name + role', () => { render( ) expect(screen.getByText(/You.*invited/i)).toBeInTheDocument() expect(screen.getByText('Acme Eng')).toBeInTheDocument() expect(screen.getByText('admin')).toBeInTheDocument() }) it('falls back to "team #" when no team name is supplied', () => { render() expect(screen.getByText('team #t1')).toBeInTheDocument() }) it('calls onAccept + onAccepted', async () => { const onAccept = jest.fn().mockResolvedValue(undefined) const onAccepted = jest.fn() render( ) await act(async () => { fireEvent.click(screen.getByRole('button', { name: /accept/i })) }) expect(onAccept).toHaveBeenCalledWith(invitation) expect(onAccepted).toHaveBeenCalledWith(invitation) }) it('only renders Decline when an onDecline is provided', () => { const { rerender } = render( ) expect(screen.queryByRole('button', { name: /decline/i })).not.toBeInTheDocument() rerender( ) expect(screen.getByRole('button', { name: /decline/i })).toBeInTheDocument() }) }) // ---------- DomainClaimCard ------------------------------------------------ describe('DomainClaimCard', () => { const baseClaim: DomainClaimLite = { id: 'd1', companyId: 'c1', domain: 'acme.com', verified: false, verificationToken: 'startsim-verify=xyz', createdAt: '2025-01-01', } it('renders the DNS TXT block when a verification token is present', () => { render() expect(screen.getByText('acme.com')).toBeInTheDocument() expect(screen.getByText('startsim-verify=xyz')).toBeInTheDocument() expect(screen.getByRole('button', { name: /verify dns/i })).toBeInTheDocument() }) it('hides verification UI once verified=true', () => { render() expect(screen.getByText('Verified')).toBeInTheDocument() expect(screen.queryByRole('button', { name: /verify dns/i })).not.toBeInTheDocument() }) it('initiate-email then submit-code calls the verify handler', async () => { const onInitiateEmail = jest.fn().mockResolvedValue(undefined) const onVerifyEmail = jest.fn().mockResolvedValue(undefined) render( ) await act(async () => { fireEvent.click(screen.getByRole('button', { name: /email attestation/i })) }) expect(onInitiateEmail).toHaveBeenCalledWith(baseClaim) const code = screen.getByLabelText('Attestation code') fireEvent.change(code, { target: { value: '123456' } }) await act(async () => { fireEvent.click(screen.getByRole('button', { name: /submit code/i })) }) expect(onVerifyEmail).toHaveBeenCalledWith(baseClaim, '123456') }) it('renders Revoke when onRevoke is provided', () => { render() expect(screen.getByRole('button', { name: /revoke/i })).toBeInTheDocument() }) }) // ---------- LeaveTeamDialog ------------------------------------------------ describe('LeaveTeamDialog', () => { const team: TeamLite = { id: 't1', slug: 'eng', name: 'Engineering', companyId: 'c1' } it('opens on trigger click, fires onSubmit + onLeft on confirm', async () => { const onSubmit = jest.fn().mockResolvedValue(undefined) const onLeft = jest.fn() render() fireEvent.click(screen.getByRole('button', { name: /leave team/i })) expect(screen.getByRole('dialog')).toBeInTheDocument() await act(async () => { // Click the confirm button (red); it's the second 'Leave team' on screen. fireEvent.click(screen.getAllByRole('button', { name: /leave team/i })[1]) }) expect(onSubmit).toHaveBeenCalledWith(team) expect(onLeft).toHaveBeenCalledWith(team) }) it('surfaces backend "last owner" errors inline', async () => { const onSubmit = jest.fn().mockRejectedValue(new Error('Cannot remove the last owner')) render() fireEvent.click(screen.getByRole('button', { name: /leave team/i })) await act(async () => { fireEvent.click(screen.getAllByRole('button', { name: /leave team/i })[1]) }) expect(screen.getByRole('alert')).toHaveTextContent( /cannot remove the last owner/i ) }) }) // ---------- OrgSwitcher ---------------------------------------------------- describe('OrgSwitcher', () => { const companies: CompanyLite[] = [ { id: 'c1', slug: 'acme', name: 'Acme' }, { id: 'c2', slug: 'beta', name: 'Beta' }, ] it('renders an option per company + fires onSwitch on change', () => { const onSwitch = jest.fn() render( ) const select = screen.getByRole('combobox') as HTMLSelectElement expect(select.value).toBe('c1') fireEvent.change(select, { target: { value: 'c2' } }) expect(onSwitch).toHaveBeenCalledWith('c2') }) it('renders nothing when there are no companies', () => { const { container } = render( {}} /> ) expect(container.firstChild).toBeNull() }) })