// ========= Copyright 2025-2026 @ Eigent.ai All Rights Reserved. ========= // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. // You may obtain a copy of the License at // // http://www.apache.org/licenses/LICENSE-2.0 // // Unless required by applicable law or agreed to in writing, software // distributed under the License is distributed on an "AS IS" BASIS, // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. // See the License for the specific language governing permissions and // limitations under the License. // ========= Copyright 2025-2026 @ Eigent.ai All Rights Reserved. ========= import BottomBox, { type BottomBoxProps } from '@/components/ChatBox/BottomBox'; import { fireEvent, render, screen, waitFor, within, } from '@testing-library/react'; import { describe, expect, it, vi } from 'vitest'; vi.mock('@/components/ChatBox/BottomBox/BoxFooter', () => ({ BoxFooter: ({ disabled }: { disabled?: boolean }) => (
), })); vi.mock('@/components/ChatBox/BottomBox/InputBox', () => ({ Inputbox: ({ value, files = [], header, onFilesChange, }: { value?: string; files?: { fileName: string; filePath: string }[]; header?: { eyebrow?: string; title?: string; description?: string; }; onFilesChange?: (files: { fileName: string; filePath: string }[]) => void; }) => (
{header && (header.eyebrow || header.title || header.description) ? (
{header.eyebrow} {header.title} {header.description}
) : null} {value} {files.map((file) => (
{file.fileName}
))}
), })); vi.mock('@/components/ChatBox/BottomBox/PickerPanel', () => ({ ConnectorPickerPanel: () =>
, SkillPickerPanel: () =>
, })); const footerProps = { sessionMode: 'single-agent' as const, }; describe('BottomBox structure', () => { it('keeps QueryBox above BoxMain and routes the legacy default to input', () => { const onFilesChange = vi.fn(); const { container } = render( ); const root = container.querySelector('[data-bottom-box]'); const query = container.querySelector('[data-bottom-box-query]'); const main = container.querySelector('[data-bottom-box-main]'); const input = container.querySelector('[data-bottom-box-input]'); const footer = container.querySelector('[data-bottom-box-footer]'); expect(root).toBeInTheDocument(); expect(query).toBeInTheDocument(); expect(main).toBeInTheDocument(); expect(root?.firstElementChild).toBe(query); expect(main).toContainElement(input); expect(main).toContainElement(footer); expect(input).toHaveAttribute('data-variant', 'input'); expect( main?.querySelector(':scope > [data-bottom-box-header]') ).not.toBeInTheDocument(); expect(screen.getByTestId('text-composer')).toHaveTextContent( 'Draft query' ); expect(screen.getByText('brief.pdf')).toBeInTheDocument(); expect(screen.getByTestId('project-setup-footer')).toBeInTheDocument(); fireEvent.click(screen.getByRole('button', { name: 'Remove brief.pdf' })); expect(onFilesChange).toHaveBeenCalledWith([]); }); it('renders the composer question inside InputBox instead of BoxHeader', () => { const { container } = render( ); const main = container.querySelector('[data-bottom-box-main]'); const input = container.querySelector('[data-bottom-box-input]'); const header = input?.querySelector('[data-bottom-box-header]'); const surface = input?.querySelector('[data-bottom-box-input-surface]'); const textInput = surface?.querySelector('[data-text-input]'); const inputActions = surface?.querySelector('[data-input-actions]'); expect( main?.querySelector(':scope > [data-bottom-box-header]') ).not.toBeInTheDocument(); expect(header).toBeInTheDocument(); expect(surface).toContainElement(header as HTMLElement); expect(surface).toContainElement(textInput as HTMLElement); expect(surface).toContainElement(inputActions as HTMLElement); expect(header).toHaveTextContent('Input required'); expect(header).toHaveTextContent('Which format should I use?'); }); it('routes a confirmation request and keeps the project footer mounted', () => { const onConfirm = vi.fn(); const onReject = vi.fn(); const { container } = render( ); expect(screen.getByText('Publish the report?')).toBeInTheDocument(); expect(container.querySelector('[data-bottom-box-input]')).toHaveAttribute( 'data-variant', 'confirmation' ); expect(screen.getByTestId('project-setup-footer')).toBeInTheDocument(); fireEvent.click(screen.getByRole('button', { name: 'Publish' })); expect(onConfirm).toHaveBeenCalledOnce(); }); it('renders only the approval scopes supplied by the owner', async () => { const onApprove = vi.fn(); const { container } = render( ); expect(container.querySelector('[data-bottom-box-input]')).toHaveAttribute( 'data-variant', 'approval' ); expect(container.querySelector('[data-bottom-box-footer]')).toBeNull(); expect(screen.queryByTestId('project-setup-footer')).toBeNull(); const approvalSurface = container.querySelector( '[data-approval-surface][data-bottom-box-input-surface]' ); const approvalHeader = approvalSurface?.querySelector( '[data-bottom-box-header]' ); const approvalActions = approvalSurface?.querySelector( '[data-approval-actions]' ); expect(approvalSurface).toBeInTheDocument(); expect(approvalHeader).toBeInTheDocument(); expect(approvalHeader).not.toHaveTextContent('Permission required'); expect(approvalHeader).toHaveTextContent('Allow todo_write?'); expect(approvalSurface).toContainElement(approvalActions as HTMLElement); expect(container.querySelector('[data-approval-actions]')).toHaveClass( 'justify-end' ); const approvalButtons = within(approvalActions as HTMLElement).getAllByRole( 'button' ); expect(approvalButtons).toHaveLength(2); approvalButtons.forEach((button) => expect(button).toHaveClass('!rounded-full') ); expect(container.querySelector('p, h1, h2, h3, h4, h5, h6')).toBeNull(); expect( screen.queryByRole('button', { name: /always allow/i }) ).not.toBeInTheDocument(); expect( screen.queryByText('Review arguments (secrets redacted)') ).not.toBeInTheDocument(); expect(screen.queryByText('single_agent')).not.toBeInTheDocument(); expect(screen.queryByText('mcp.tool.write')).not.toBeInTheDocument(); expect( screen.queryByText('brave_search.web_search') ).not.toBeInTheDocument(); await waitFor(() => expect(screen.getByRole('button', { name: 'Reject' })).not.toHaveFocus() ); fireEvent.click(screen.getByRole('button', { name: 'Approve once' })); expect(onApprove).toHaveBeenCalledWith('once'); }); it('renders a concise approval without eyebrow or scope descriptions', () => { render( ); expect(screen.queryByText('Input required')).not.toBeInTheDocument(); expect( screen.queryByText('Allow this action one time only.') ).not.toBeInTheDocument(); expect( screen.queryByText('Allow this action in this Space from now on.') ).not.toBeInTheDocument(); expect( screen.getByRole('button', { name: 'Approve once' }) ).toBeInTheDocument(); expect( screen.getByRole('button', { name: 'Always allow' }) ).toBeInTheDocument(); }); it('animates from the composer into a three-action approval variant', async () => { const { container, rerender } = render( ); expect( container.querySelector( '[data-bottom-box-variant-transition][data-variant="input"]' ) ).toBeInTheDocument(); expect(container.querySelector('[data-bottom-box-main]')).toHaveAttribute( 'data-layout-motion', 'smooth' ); rerender( ); expect( container.querySelector( '[data-bottom-box-variant-transition][data-variant="approval"]' ) ).toBeInTheDocument(); expect(container.querySelector('[data-bottom-box-footer]')).toBeNull(); expect(screen.queryByTestId('project-setup-footer')).toBeNull(); expect(container.querySelector('[data-bottom-box-main]')).toHaveAttribute( 'data-layout-motion', 'instant' ); expect(screen.queryByText('Input required')).not.toBeInTheDocument(); expect( screen.getByText('The agent wants to publish the report.') ).toBeInTheDocument(); expect( within( container.querySelector('[data-approval-actions]') as HTMLElement ).getAllByRole('button') ).toHaveLength(3); await waitFor(() => expect(screen.getByRole('button', { name: 'Reject' })).not.toHaveFocus() ); }); it('routes controlled selection changes without owning event state', () => { const onSelectionChange = vi.fn(); render( ); fireEvent.click(screen.getByRole('radio', { name: 'PDF' })); expect(onSelectionChange).toHaveBeenCalledWith(['pdf']); }); it('keeps standard textarea chrome for non-question feedback', () => { render( ); expect(screen.getByRole('textbox', { name: 'Feedback' })).toHaveClass( 'border', 'shadow-sm', 'focus-visible:ring-1' ); }); it('routes feedback and structured form callbacks', async () => { const onFeedbackChange = vi.fn(); const onFieldChange = vi.fn(); const { rerender } = render( ); const questionLabel = screen.getByText('Question'); expect(questionLabel.parentElement).toHaveClass( 'text-body-sm', 'font-bold' ); expect( questionLabel .closest('[data-bottom-box-header]') ?.querySelector('[data-bottom-box-question-icon]') ).toBeInTheDocument(); await waitFor(() => expect( document.querySelector('[data-bottom-box-question-markdown] h3') ).toBeInTheDocument() ); const questionPrompt = document.querySelector( '[data-bottom-box-question-markdown] h3' ) as HTMLElement; expect(questionPrompt).toHaveTextContent('What should I change?'); expect(questionPrompt.querySelector('strong')).toHaveTextContent('change'); expect( questionPrompt.closest('[data-bottom-box-question-markdown]') ).toHaveClass( 'bottom-box-question-markdown', 'text-body-sm', 'text-ds-text-neutral-default-default' ); expect( questionPrompt.closest('[data-bottom-box-header]') ).toBeInTheDocument(); const questionTextarea = screen.getByRole('textbox', { name: 'Feedback', }); expect(questionTextarea).toHaveClass( 'border-none', 'shadow-none', 'focus-visible:ring-0', 'focus-visible:ring-offset-0' ); fireEvent.change(questionTextarea, { target: { value: 'Use a shorter title' }, }); expect(onFeedbackChange).toHaveBeenCalledWith('Use a shorter title'); rerender( ); const inputRegion = document.querySelector( '[data-bottom-box-input][data-variant="form"]' ); expect(inputRegion).toBeInTheDocument(); fireEvent.change(within(inputRegion as HTMLElement).getByRole('textbox'), { target: { value: 'Executives' }, }); expect(onFieldChange).toHaveBeenCalledWith('audience', 'Executives'); }); it('accepts every controlled variant without requiring composer props', () => { const variants: BottomBoxProps['variant'][] = [ { kind: 'confirmation', header: {}, onConfirm: vi.fn(), onReject: vi.fn(), }, { kind: 'approval', header: {}, options: [], onApprove: vi.fn(), onReject: vi.fn(), }, { kind: 'selection', header: {}, options: [], selectedIds: [], onSelectionChange: vi.fn(), onSubmit: vi.fn(), }, { kind: 'feedback', header: {}, value: '', onChange: vi.fn(), onSubmit: vi.fn(), }, { kind: 'form', header: {}, fields: [], onFieldChange: vi.fn(), onSubmit: vi.fn(), }, { kind: 'blocked', header: { title: 'Unsupported request' }, message: 'Update the app before responding.', }, { kind: 'run_control', header: { title: 'Run controls' }, runId: 'run-1', state: 'read_only', }, ]; expect(variants.map((variant) => variant?.kind)).toEqual([ 'confirmation', 'approval', 'selection', 'feedback', 'form', 'blocked', 'run_control', ]); }); it('fails closed for a blocked mandatory interaction', () => { render( ); expect(screen.getByRole('alert')).toHaveTextContent( 'This decision cannot be submitted safely.' ); expect(screen.queryByRole('button')).not.toBeInTheDocument(); expect(screen.getByTestId('project-setup-footer')).toBeInTheDocument(); }); it('routes Resume and Cancel to the explicitly targeted interrupted Run', () => { const onResume = vi.fn(); const onCancel = vi.fn(); const { rerender } = render( ); fireEvent.click(screen.getByRole('button', { name: 'Resume' })); fireEvent.click(screen.getByRole('button', { name: 'Cancel Run' })); expect(onResume).toHaveBeenCalledWith('run-interrupted'); expect(onCancel).toHaveBeenCalledWith('run-interrupted'); rerender( ); expect(screen.getByRole('button', { name: 'Resume' })).toBeDisabled(); expect(screen.getByRole('button', { name: 'Cancel Run' })).toBeDisabled(); }); it('shows locked transition labels and hides actions for read-only Runs', () => { const common = { kind: 'run_control' as const, header: { title: 'Run lifecycle' }, runId: 'run-7', }; const { rerender } = render( ); expect(screen.getByRole('button', { name: 'Resuming…' })).toBeDisabled(); expect(screen.getByRole('button', { name: 'Cancel Run' })).toBeDisabled(); rerender( ); expect(screen.getByRole('button', { name: 'Cancelling…' })).toBeDisabled(); expect(screen.getByRole('button', { name: 'Resume' })).toBeDisabled(); rerender( ); expect(screen.getByRole('status')).toHaveTextContent( 'This Run has finished, so its controls are no longer available.' ); expect(screen.queryByRole('button')).not.toBeInTheDocument(); }); });