diff --git a/client/src/components/SidePanel/Agents/__tests__/Instructions.spec.tsx b/client/src/components/SidePanel/Agents/__tests__/Instructions.spec.tsx new file mode 100644 index 0000000000..b2ff3fab9b --- /dev/null +++ b/client/src/components/SidePanel/Agents/__tests__/Instructions.spec.tsx @@ -0,0 +1,33 @@ +import { ToastProvider } from '@librechat/client'; +import userEvent from '@testing-library/user-event'; +import { render, screen } from '@testing-library/react'; +import { FormProvider, useForm } from 'react-hook-form'; +import type { AgentForm } from '~/common'; +import Instructions from '../Instructions'; + +jest.mock('~/hooks', () => ({ + useLocalize: () => (key: string) => key, +})); + +function InstructionsHarness() { + const methods = useForm({ defaultValues: { instructions: '' } }); + return ( + + + + + + ); +} + +describe('Agent Instructions', () => { + it('offers special-variable insertion by default', async () => { + const user = userEvent.setup(); + render(); + + await user.click(screen.getByRole('button', { name: 'com_ui_variables' })); + expect( + await screen.findByRole('menuitem', { name: 'com_ui_special_var_current_date' }), + ).toBeInTheDocument(); + }); +}); diff --git a/client/src/components/SidePanel/Schedules/ScheduleDialog.tsx b/client/src/components/SidePanel/Schedules/ScheduleDialog.tsx index 16d1356e18..4a5fc8289a 100644 --- a/client/src/components/SidePanel/Schedules/ScheduleDialog.tsx +++ b/client/src/components/SidePanel/Schedules/ScheduleDialog.tsx @@ -418,6 +418,7 @@ export default function ScheduleDialog({ required={true} invalid={errors.prompt != null} describedBy="schedule-prompt-message" + showVariables={false} portal={false} /> diff --git a/client/src/components/SidePanel/Schedules/__tests__/ScheduleDialog.spec.tsx b/client/src/components/SidePanel/Schedules/__tests__/ScheduleDialog.spec.tsx index 1facc9e110..fd4bf0cc32 100644 --- a/client/src/components/SidePanel/Schedules/__tests__/ScheduleDialog.spec.tsx +++ b/client/src/components/SidePanel/Schedules/__tests__/ScheduleDialog.spec.tsx @@ -1,7 +1,7 @@ import { createElement } from 'react'; import { ToastProvider } from '@librechat/client'; import userEvent from '@testing-library/user-event'; -import { render, screen, within } from '@testing-library/react'; +import { render, screen, waitFor, within } from '@testing-library/react'; import { QueryClient, QueryClientProvider } from '@tanstack/react-query'; import type { ReactNode } from 'react'; import ScheduleDialog from '../ScheduleDialog'; @@ -97,13 +97,54 @@ describe('ScheduleDialog', () => { expect(screen.getByPlaceholderText('com_ui_schedule_prompt_placeholder')).toBeInTheDocument(); }); - it('offers the special-variable menu on the prompt field', async () => { + it('does not offer special-variable insertion in either prompt editor', async () => { const user = userEvent.setup(); renderDialog(); - await user.click(screen.getByRole('button', { name: 'com_ui_variables' })); + expect(screen.queryByRole('button', { name: 'com_ui_variables' })).not.toBeInTheDocument(); + + await user.click(screen.getByRole('button', { name: 'com_ui_expand_editor' })); + const expandedDialog = screen.getAllByRole('dialog').at(-1); + expect(expandedDialog).toBeDefined(); expect( - await screen.findByRole('menuitem', { name: 'com_ui_special_var_current_date' }), - ).toBeInTheDocument(); + within(expandedDialog as HTMLElement).queryByRole('button', { name: 'com_ui_variables' }), + ).not.toBeInTheDocument(); + }); + + it('keeps the expanded prompt editor available and synchronized', async () => { + const user = userEvent.setup(); + renderDialog(); + + const prompt = screen.getByPlaceholderText('com_ui_schedule_prompt_placeholder'); + await user.type(prompt, 'Initial prompt'); + await user.click(screen.getByRole('button', { name: 'com_ui_expand_editor' })); + + const expandedDialog = screen.getAllByRole('dialog').at(-1); + expect(expandedDialog).toBeDefined(); + const expandedPrompt = within(expandedDialog as HTMLElement).getByRole('textbox', { + name: 'com_ui_prompt', + }); + expect(expandedPrompt).toHaveValue('Initial prompt'); + + await user.type(expandedPrompt, ' with details'); + expect(prompt).toHaveValue('Initial prompt with details'); + }); + + it('preserves prompt validation and accessibility relationships', async () => { + const user = userEvent.setup(); + renderDialog(); + + const prompt = screen.getByPlaceholderText('com_ui_schedule_prompt_placeholder'); + expect(prompt).toHaveAccessibleName('com_ui_prompt'); + expect(prompt).toHaveAttribute('aria-required', 'true'); + expect(prompt).toHaveAttribute('aria-describedby', 'schedule-prompt-message'); + expect(prompt).toHaveAttribute('aria-invalid', 'false'); + + await user.click(screen.getByRole('button', { name: 'com_ui_create' })); + + await waitFor(() => expect(prompt).toHaveAttribute('aria-invalid', 'true')); + expect(document.getElementById('schedule-prompt-message')).toHaveTextContent( + 'com_ui_field_required', + ); }); }); diff --git a/client/src/components/Variables/Editor.tsx b/client/src/components/Variables/Editor.tsx index 27b5820bcb..0d9dddbfbd 100644 --- a/client/src/components/Variables/Editor.tsx +++ b/client/src/components/Variables/Editor.tsx @@ -44,6 +44,8 @@ interface VariableEditorProps { className?: string; labelClassName?: string; containerClassName?: string; + /** Whether to expose special-variable insertion controls. Defaults to true. */ + showVariables?: boolean; /** * Ariakit popovers portal to the body by default, which puts them outside a Radix * dialog's focus trap. Pass `false` from inside a dialog, and give that dialog @@ -73,6 +75,7 @@ export default function VariableEditor({ className, labelClassName, containerClassName, + showVariables = true, portal = true, }: VariableEditorProps) { const menuId = useId(); @@ -102,27 +105,29 @@ export default function VariableEditor({ {label}
- - - - } - items={variableItems} - menuId={menuId} - className="pointer-events-auto z-30" - /> + {showVariables && ( + + + + } + items={variableItems} + menuId={menuId} + className="pointer-events-auto z-30" + /> + )} -
- - - {localize('com_ui_variables')} - - } - /> - } - items={variableItems} - menuId={dialogMenuId} - className="pointer-events-auto z-[200]" - /> +
+ {showVariables && ( + + + {localize('com_ui_variables')} + + } + /> + } + items={variableItems} + menuId={dialogMenuId} + className="pointer-events-auto z-[200]" + /> + )}