Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
@@ -0,0 +1,46 @@
import React from 'react'
import { render, screen } from '@testing-library/react'
import { FluentProvider, webLightTheme } from '@fluentui/react-components'
import type { ConverterCatalogEntry } from '../../../types'
import ConverterParams from './ConverterParams'

const converter: ConverterCatalogEntry = {
converter_type: 'TestConverter',
supported_input_types: ['text'],
supported_output_types: ['text'],
is_llm_based: false,
parameters: [
{ name: 'key', type_name: 'str', required: true, description: 'Encryption key' },
{ name: 'append_description', type_name: 'bool', required: false, default: 'false' },
{ name: 'mode', type_name: 'str', required: false, choices: ['fast', 'safe'] },
{ name: 'file_path', type_name: 'str', required: false, description: 'Input file path' },
],
}

function renderParams() {
return render(
<FluentProvider theme={webLightTheme}>
<ConverterParams
converter={converter}
paramValues={{}}
paramsExpanded
showValidation={false}
onParamChange={jest.fn()}
onFileBrowse={jest.fn()}
onToggleExpanded={jest.fn()}
/>
</FluentProvider>,
)
}

describe('ConverterParams accessibility', () => {
it('associates visible parameter names with every control type', () => {
renderParams()

expect(screen.getByRole('textbox', { name: /key/i })).toBeInTheDocument()
expect(screen.getByRole('switch', { name: /append_description/i })).toBeInTheDocument()
expect(screen.getByRole('combobox', { name: /mode/i })).toBeInTheDocument()
expect(screen.getByRole('textbox', { name: /file_path/i })).toBeInTheDocument()
expect(screen.getByRole('button', { name: /browse for file_path/i })).toBeInTheDocument()
})
})
29 changes: 21 additions & 8 deletions frontend/src/components/Chat/ConverterPanel/ConverterParams.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -7,14 +7,18 @@ interface ParamInputProps {
param: Parameter
value: string
isMissing: boolean
labelId: string
descriptionId?: string
onChange: (name: string, value: string) => void
}

function ConverterParameterChoiceViewer({ param, value, onChange }: ParamInputProps) {
function ConverterParameterChoiceViewer({ param, value, labelId, descriptionId, onChange }: ParamInputProps) {
const stringDefault = typeof param.default === 'string' ? param.default : ''
return (
<Select
value={value ?? stringDefault}
aria-labelledby={labelId}
aria-describedby={descriptionId}
onChange={(_, data) => onChange(param.name, data.value)}
data-testid={`param-${param.name}`}
>
Expand All @@ -27,7 +31,7 @@ function ConverterParameterChoiceViewer({ param, value, onChange }: ParamInputPr
)
}

function ParameterFileViewer({ param, value, isMissing, onChange, onBrowse }: ParamInputProps & { onBrowse: (name: string) => void }) {
function ParameterFileViewer({ param, value, isMissing, labelId, descriptionId, onChange, onBrowse }: ParamInputProps & { onBrowse: (name: string) => void }) {
const styles = useConverterPanelStyles()
const stringDefault = typeof param.default === 'string' ? param.default : 'Select a file...'

Expand All @@ -36,13 +40,16 @@ function ParameterFileViewer({ param, value, isMissing, onChange, onBrowse }: Pa
<Input
value={value ?? ''}
placeholder={stringDefault}
aria-labelledby={labelId}
aria-describedby={descriptionId}
onChange={(_, data) => onChange(param.name, data.value)}
className={isMissing ? styles.paramInputError : undefined}
data-testid={`param-${param.name}`}
/>
<Button
appearance="subtle"
size="small"
aria-label={`Browse for ${param.name}`}
onClick={() => onBrowse(param.name)}
className={styles.touchTarget}
data-testid={`param-${param.name}-browse`}
Expand All @@ -53,14 +60,16 @@ function ParameterFileViewer({ param, value, isMissing, onChange, onBrowse }: Pa
)
}

function ConverterParameterViewer({ param, value, isMissing, onChange }: ParamInputProps) {
function ConverterParameterViewer({ param, value, isMissing, labelId, descriptionId, onChange }: ParamInputProps) {
const styles = useConverterPanelStyles()
const stringDefault = typeof param.default === 'string' ? param.default : undefined

return (
<Input
value={value ?? ''}
placeholder={stringDefault}
aria-labelledby={labelId}
aria-describedby={descriptionId}
onChange={(_, data) => onChange(param.name, data.value)}
className={isMissing ? styles.paramInputError : undefined}
data-testid={`param-${param.name}`}
Expand Down Expand Up @@ -97,29 +106,33 @@ export default function ConverterParams({ converter, paramValues, paramsExpanded
</Button>
{paramsExpanded && (converter.parameters ?? []).map((param) => {
const isMissing = showValidation && param.required && !paramValues[param.name]?.trim()
const labelId = `converter-param-${param.name}-label`
const descriptionId = param.description ? `converter-param-${param.name}-description` : undefined
return (
<div key={param.name} className={styles.paramBlock}>
<span className={styles.paramLabel}>
<span id={labelId} className={styles.paramLabel}>
<Text size={200} weight="semibold">{param.name}{param.required ? ' *' : ''}</Text>
{param.description && (
<Tooltip content={param.description} relationship="description">
<span className={styles.paramInfo}><InfoRegular fontSize={12} /></span>
<span id={descriptionId} className={styles.paramInfo} aria-label={param.description}><InfoRegular fontSize={12} /></span>
</Tooltip>
)}
</span>
{param.type_name === 'bool' ? (
<Switch
checked={(paramValues[param.name] ?? (typeof param.default === 'string' ? param.default : 'false')).toLowerCase() === 'true'}
aria-labelledby={labelId}
aria-describedby={descriptionId}
onChange={(_, data) => onParamChange(param.name, data.checked ? 'true' : 'false')}
label={(paramValues[param.name] ?? (typeof param.default === 'string' ? param.default : 'false')).toLowerCase() === 'true' ? 'True' : 'False'}
data-testid={`param-${param.name}`}
/>
) : param.choices ? (
<ConverterParameterChoiceViewer param={param} value={paramValues[param.name]} isMissing={isMissing} onChange={onParamChange} />
<ConverterParameterChoiceViewer param={param} value={paramValues[param.name]} isMissing={isMissing} labelId={labelId} descriptionId={descriptionId} onChange={onParamChange} />
) : /path|file/i.test(param.name) || /path|file/i.test(param.description ?? '') ? (
<ParameterFileViewer param={param} value={paramValues[param.name]} isMissing={isMissing} onChange={onParamChange} onBrowse={onFileBrowse} />
<ParameterFileViewer param={param} value={paramValues[param.name]} isMissing={isMissing} labelId={labelId} descriptionId={descriptionId} onChange={onParamChange} onBrowse={onFileBrowse} />
) : (
<ConverterParameterViewer param={param} value={paramValues[param.name]} isMissing={isMissing} onChange={onParamChange} />
<ConverterParameterViewer param={param} value={paramValues[param.name]} isMissing={isMissing} labelId={labelId} descriptionId={descriptionId} onChange={onParamChange} />
)}
{isMissing && (
<Text size={100} className={styles.paramErrorText}>Required</Text>
Expand Down