cflx 0.6.11

Conflux – a spec-driven parallel coding orchestrator that runs AI agents on git worktrees
// @vitest-environment jsdom

import React from 'react';
import { afterEach, describe, expect, it, vi } from 'vitest';
import { cleanup, render, screen, within } from '@testing-library/react';
import userEvent from '@testing-library/user-event';

import { ElicitationDialog } from './ElicitationDialog';
import { ElicitationRequest } from '../api/types';

afterEach(() => {
  cleanup();
});

const elicitation: ElicitationRequest = {
  id: 'elicitation-1',
  message: 'Provide the missing configuration.',
  properties: {
    mode: {
      type: 'string',
      title: 'Mode',
      oneOf: [
        { const: 'fast', title: 'Fast' },
        { const: 'safe', title: 'Safe' },
      ],
    },
    note: {
      type: 'string',
      title: 'Note',
      description: 'Optional note for the agent',
    },
    confirmed: {
      type: 'boolean',
      title: 'Confirmed',
    },
    retries: {
      type: 'integer',
      title: 'Retries',
      default: 2,
    },
  },
  required: ['mode', 'note'],
};

describe('ElicitationDialog', () => {
  it('renders schema-driven controls and submits collected values', async () => {
    const onSubmit = vi.fn();
    const onDecline = vi.fn();
    const onCancel = vi.fn();
    const user = userEvent.setup();

    render(
      <ElicitationDialog
        elicitation={elicitation}
        onSubmit={onSubmit}
        onDecline={onDecline}
        onCancel={onCancel}
      />,
    );

    expect(screen.getByText('Provide the missing configuration.')).toBeTruthy();

    await user.selectOptions(screen.getByLabelText('Mode*'), 'safe');
    await user.type(screen.getByLabelText('Note*'), 'Use conservative defaults');
    await user.click(screen.getByLabelText('Confirmed'));

    const retriesInput = screen.getByLabelText('Retries') as HTMLInputElement;
    await user.clear(retriesInput);
    await user.type(retriesInput, '5');

    await user.click(screen.getByRole('button', { name: 'Submit' }));

    expect(onSubmit).toHaveBeenCalledWith({
      mode: 'safe',
      note: 'Use conservative defaults',
      confirmed: true,
      retries: 5,
    });
    expect(onDecline).not.toHaveBeenCalled();
    expect(onCancel).not.toHaveBeenCalled();
  });

  it('invokes decline and cancel actions', async () => {
    const onSubmit = vi.fn();
    const onDecline = vi.fn();
    const onCancel = vi.fn();
    const user = userEvent.setup();

    const { unmount } = render(
      <ElicitationDialog
        elicitation={elicitation}
        onSubmit={onSubmit}
        onDecline={onDecline}
        onCancel={onCancel}
      />,
    );

    await user.click(screen.getByRole('button', { name: 'Decline' }));
    expect(onDecline).toHaveBeenCalledTimes(1);
    expect(onCancel).not.toHaveBeenCalled();

    unmount();

    render(
      <ElicitationDialog
        elicitation={elicitation}
        onSubmit={onSubmit}
        onDecline={onDecline}
        onCancel={onCancel}
      />,
    );

    await user.click(screen.getByRole('button', { name: 'Cancel' }));

    expect(onCancel).toHaveBeenCalledTimes(1);
    expect(onSubmit).not.toHaveBeenCalled();
  });

  it('applies dialog accessibility semantics and closes on Escape', async () => {
    const onSubmit = vi.fn();
    const onDecline = vi.fn();
    const onCancel = vi.fn();
    const user = userEvent.setup();

    render(
      <ElicitationDialog
        elicitation={elicitation}
        onSubmit={onSubmit}
        onDecline={onDecline}
        onCancel={onCancel}
      />,
    );

    const dialog = screen.getByRole('dialog', { name: 'Agent Request' });
    expect(dialog.getAttribute('aria-modal')).toBe('true');
    expect(dialog.getAttribute('aria-labelledby')).toBeTruthy();

    await user.keyboard('{Escape}');
    expect(onCancel).toHaveBeenCalledTimes(1);
  });

  it('traps focus within dialog when tabbing', async () => {
    const onSubmit = vi.fn();
    const onDecline = vi.fn();
    const onCancel = vi.fn();
    const user = userEvent.setup();

    render(
      <ElicitationDialog
        elicitation={elicitation}
        onSubmit={onSubmit}
        onDecline={onDecline}
        onCancel={onCancel}
      />,
    );

    const dialog = screen.getByRole('dialog', { name: 'Agent Request' });
    const submitButton = within(dialog).getByRole('button', { name: 'Submit' });
    const closeButton = within(dialog).getByRole('button', { name: 'Close' });

    submitButton.focus();
    expect(document.activeElement).toBe(submitButton);

    await user.tab();
    expect(document.activeElement).toBe(closeButton);

    closeButton.focus();
    expect(document.activeElement).toBe(closeButton);

    await user.tab({ shift: true });
    expect(document.activeElement).toBe(submitButton);
  });

  it('closes when clicking the backdrop', async () => {
    const onSubmit = vi.fn();
    const onDecline = vi.fn();
    const onCancel = vi.fn();
    const user = userEvent.setup();

    const { container } = render(
      <ElicitationDialog
        elicitation={elicitation}
        onSubmit={onSubmit}
        onDecline={onDecline}
        onCancel={onCancel}
      />,
    );

    const backdrop = container.firstElementChild as HTMLElement;
    await user.click(backdrop);

    expect(onCancel).toHaveBeenCalledTimes(1);
    expect(onSubmit).not.toHaveBeenCalled();
    expect(onDecline).not.toHaveBeenCalled();
  });
});