test(web): verify parameter controls in Browser Mode (#45485)

This commit is contained in:
Stephen Zhou authored and GitHub committed 2026-11-04 00:50:22 +00:00
1 parent 6f0ee4bd1e
commit b112b28585
9 files changed
+298 -327

No files matched your search

@@ -0,0 +1,30 @@
import { page, userEvent } from 'vite-plus/test/browser'
import { render } from 'vitest-browser-react'
import { ParamItemExample } from './param-item-example'
beforeEach(async () => {
await page.viewport(800, 600)
})
// Native Tab/arrow input must update the caller and numeric field, not just emit a callback.
it.each([
{ max: 1, min: 0, value: 0.5, sliderValue: '50', next: 0.51, nextSliderValue: '51' },
{ max: 10, min: 1, value: 5, sliderValue: '5', next: 6, nextSliderValue: '6' },
])(
'keeps keyboard slider edits in parameter units with max=$max',
async ({ max, min, value, sliderValue, next, nextSliderValue }) => {
await render(<ParamItemExample value={value} min={min} max={max} />)
const slider = page.getByRole('slider', { name: 'Test Param' })
const input = page.getByRole('textbox', { name: 'Test Param' })
await expect.element(slider).toHaveValue(sliderValue)
await input.click()
await userEvent.tab()
await expect.element(slider).toHaveFocus()
await userEvent.keyboard('{ArrowRight}')
await expect.element(input).toHaveValue(String(next))
await expect.element(slider).toHaveValue(nextSliderValue)
await expect
.element(page.getByRole('status', { name: 'Saved parameters' }))
.toHaveTextContent(JSON.stringify({ test_param: next }))
},
)
@@ -1,41 +0,0 @@
import { render, screen } from '@testing-library/react'
import userEvent from '@testing-library/user-event'
import ParamItem from '..'
describe('ParamItem Slider onChange', () => {
const defaultProps = {
id: 'test_param',
name: 'Test Param',
enable: true,
onChange: vi.fn(),
}
const getSlider = () => screen.getByRole('slider', { name: 'Test Param' })
it('should divide slider value by 100 when max < 5', async () => {
const user = userEvent.setup()
render(<ParamItem {...defaultProps} value={0.5} min={0} max={1} />)
const slider = getSlider()
expect(slider).toHaveAttribute('aria-valuenow', '50')
expect(slider).toHaveAttribute('aria-valuetext', '0.5')
slider.focus()
await user.keyboard('{ArrowRight}')
// max=1 < 5, so slider value change (50->51) becomes 0.51
expect(defaultProps.onChange).toHaveBeenLastCalledWith('test_param', 0.51)
})
it('should not divide slider value when max >= 5', async () => {
const user = userEvent.setup()
render(<ParamItem {...defaultProps} value={5} min={1} max={10} />)
const slider = getSlider()
slider.focus()
await user.keyboard('{ArrowRight}')
// max=10 >= 5, so value remains raw (5->6)
expect(defaultProps.onChange).toHaveBeenLastCalledWith('test_param', 6)
})
})
@@ -0,0 +1,87 @@
import { page, userEvent } from 'vite-plus/test/browser'
import { render } from 'vitest-browser-react'
import { ParamItemExample } from './param-item-example'
beforeEach(async () => {
await page.viewport(800, 600)
})
describe('ParamItem', () => {
it('omits the tooltip trigger when tooltips are disabled', async () => {
await render(<ParamItemExample noTooltip tip="Hidden tip" />)
await expect.element(page.getByRole('button', { name: 'Test Param' })).not.toBeInTheDocument()
})
it('disables both numeric and slider edits when the parameter is off', async () => {
await render(<ParamItemExample enable={false} />)
await expect.element(page.getByRole('textbox', { name: 'Test Param' })).toBeDisabled()
await expect.element(page.getByRole('slider', { name: 'Test Param' })).toBeDisabled()
})
it('applies text edits to the named parameter and its slider', async () => {
await render(<ParamItemExample />)
const input = page.getByRole('textbox', { name: 'Test Param' })
await input.clear()
await userEvent.keyboard('0.8')
await expect.element(input).toHaveValue('0.8')
await expect
.element(page.getByRole('status', { name: 'Saved parameters' }))
.toHaveTextContent('{"test_param":0.8}')
await expect
.element(page.getByRole('slider', { name: 'Test Param' }))
.toHaveAttribute('aria-valuetext', '0.8')
})
it('allows replacing the minimum with a multi-digit value without saving the empty draft', async () => {
await render(<ParamItemExample value={1} min={1} max={100} />)
const input = page.getByRole('textbox', { name: 'Test Param' })
await input.clear()
await expect.element(input).toHaveValue('')
await expect
.element(page.getByRole('status', { name: 'Saved parameters' }))
.toHaveTextContent('{"test_param":1}')
await userEvent.keyboard('20')
await expect.element(input).toHaveValue('20')
await expect
.element(page.getByRole('status', { name: 'Saved parameters' }))
.toHaveTextContent('{"test_param":20}')
await expect.element(page.getByRole('slider', { name: 'Test Param' })).toHaveValue('20')
})
it('clamps text edits before saving the parameter and updating the slider', async () => {
await render(<ParamItemExample />)
const input = page.getByRole('textbox', { name: 'Test Param' })
await input.clear()
await userEvent.keyboard('1.5')
await expect
.element(page.getByRole('status', { name: 'Saved parameters' }))
.toHaveTextContent('{"test_param":1}')
await expect
.element(page.getByRole('slider', { name: 'Test Param' }))
.toHaveAttribute('aria-valuetext', '1')
})
it('applies the named switch change and disables the parameter controls', async () => {
await render(<ParamItemExample hasSwitch />)
const toggle = page.getByRole('switch', { name: 'Test Param' })
await toggle.click()
await expect.element(toggle).not.toBeChecked()
await expect
.element(page.getByRole('status', { name: 'Enabled parameters' }))
.toHaveTextContent('{"test_param":false}')
await expect.element(page.getByRole('textbox', { name: 'Test Param' })).toBeDisabled()
await expect.element(page.getByRole('slider', { name: 'Test Param' })).toBeDisabled()
})
it('applies the increment step to the saved parameter and both controls', async () => {
await render(<ParamItemExample step={0.1} />)
await page.getByRole('button', { name: 'Increment value' }).click()
await expect.element(page.getByRole('textbox', { name: 'Test Param' })).toHaveValue('0.6')
await expect
.element(page.getByRole('slider', { name: 'Test Param' }))
.toHaveAttribute('aria-valuetext', '0.6')
await expect
.element(page.getByRole('status', { name: 'Saved parameters' }))
.toHaveTextContent('{"test_param":0.6}')
})
})
@@ -1,169 +0,0 @@
import { render, screen } from '@testing-library/react'
import userEvent from '@testing-library/user-event'
import { useState } from 'react'
import ParamItem from '..'
describe('ParamItem', () => {
const defaultProps = {
id: 'test_param',
name: 'Test Param',
value: 0.5,
enable: true,
max: 1,
onChange: vi.fn(),
}
beforeEach(() => {
vi.clearAllMocks()
})
const getSlider = () =>
screen.getByLabelText('Test Param', {
selector: 'input[type="range"]',
})
describe('Rendering', () => {
it('should not render tooltip trigger when noTooltip is true', () => {
render(<ParamItem {...defaultProps} noTooltip tip="Hidden tip" />)
expect(screen.queryByRole('button', { name: 'Test Param' })).not.toBeInTheDocument()
})
})
describe('Props', () => {
it('should disable InputNumber when enable is false', () => {
render(<ParamItem {...defaultProps} enable={false} />)
expect(screen.getByRole('textbox')).toBeDisabled()
expect(getSlider()).toBeDisabled()
})
})
describe('User Interactions', () => {
it('should call onChange with id and value when InputNumber changes', async () => {
const user = userEvent.setup()
const StatefulParamItem = () => {
const [value, setValue] = useState(defaultProps.value)
return (
<ParamItem
{...defaultProps}
value={value}
onChange={(key: string, nextValue: number) => {
defaultProps.onChange(key, nextValue)
setValue(nextValue)
}}
/>
)
}
render(<StatefulParamItem />)
const input = screen.getByRole('textbox')
await user.clear(input)
await user.type(input, '0.8')
expect(defaultProps.onChange).toHaveBeenLastCalledWith('test_param', 0.8)
})
it('should allow users to replace the minimum value with a multi-digit value', async () => {
const user = userEvent.setup()
const StatefulParamItem = () => {
const [value, setValue] = useState(1)
return (
<ParamItem
{...defaultProps}
value={value}
min={1}
max={100}
onChange={(key: string, nextValue: number) => {
defaultProps.onChange(key, nextValue)
setValue(nextValue)
}}
/>
)
}
render(<StatefulParamItem />)
const input = screen.getByRole('textbox')
await user.clear(input)
expect(input).toHaveValue('')
expect(defaultProps.onChange).not.toHaveBeenCalled()
await user.type(input, '20')
expect(input).toHaveValue('20')
expect(defaultProps.onChange).toHaveBeenLastCalledWith('test_param', 20)
expect(getSlider()).toHaveAttribute('aria-valuenow', '20')
})
it('should clamp out-of-range text edits before updating state', async () => {
const user = userEvent.setup()
const StatefulParamItem = () => {
const [value, setValue] = useState(defaultProps.value)
return (
<ParamItem
{...defaultProps}
value={value}
onChange={(key: string, nextValue: number) => {
defaultProps.onChange(key, nextValue)
setValue(nextValue)
}}
/>
)
}
render(<StatefulParamItem />)
const input = screen.getByRole('textbox')
await user.clear(input)
await user.type(input, '1.5')
expect(defaultProps.onChange).toHaveBeenLastCalledWith('test_param', 1)
expect(getSlider()).toHaveAttribute('aria-valuenow', '100')
})
it('should pass scaled value to slider when max < 5', () => {
render(<ParamItem {...defaultProps} value={0.5} />)
const slider = getSlider()
// When max < 5, slider value = value * 100 = 50
expect(slider).toHaveAttribute('aria-valuenow', '50')
expect(slider).toHaveAttribute('max', '100')
})
it('should pass raw value to slider when max >= 5', () => {
render(<ParamItem {...defaultProps} value={5} max={10} />)
const slider = getSlider()
// When max >= 5, slider value = value = 5
expect(slider).toHaveAttribute('aria-valuenow', '5')
expect(slider).toHaveAttribute('max', '10')
})
it('should call onSwitchChange with id and value when switch is toggled', async () => {
const user = userEvent.setup()
const onSwitchChange = vi.fn()
render(<ParamItem {...defaultProps} hasSwitch onSwitchChange={onSwitchChange} />)
await user.click(screen.getByRole('switch'))
expect(onSwitchChange).toHaveBeenCalledWith('test_param', false)
})
it('should call onChange with id when increment button is clicked', async () => {
const user = userEvent.setup()
render(<ParamItem {...defaultProps} value={0.5} step={0.1} />)
const incrementBtn = screen.getByRole('button', { name: /increment/i })
await user.click(incrementBtn)
// step=0.1, so 0.5 + 0.1 = 0.6, clamped to [0,1] → 0.6
expect(defaultProps.onChange).toHaveBeenCalledWith('test_param', 0.6)
})
})
})
@@ -0,0 +1,30 @@
import type { ComponentProps } from 'react'
import { useState } from 'react'
import ParamItem from '..'
type Props = Omit<
Partial<ComponentProps<typeof ParamItem>>,
'id' | 'name' | 'onChange' | 'onSwitchChange'
>
// A caller applies the public value/key payloads; no owner transformation is copied here.
export function ParamItemExample({ value = 0.5, enable = true, max = 1, ...props }: Props) {
const [values, setValues] = useState({ test_param: value })
const [enabled, setEnabled] = useState({ test_param: enable })
return (
<>
<ParamItem
{...props}
id="test_param"
name="Test Param"
max={max}
value={values.test_param}
enable={enabled.test_param}
onChange={(key, next) => setValues((previous) => ({ ...previous, [key]: next }))}
onSwitchChange={(key, next) => setEnabled((previous) => ({ ...previous, [key]: next }))}
/>
<output aria-label="Saved parameters">{JSON.stringify(values)}</output>
<output aria-label="Enabled parameters">{JSON.stringify(enabled)}</output>
</>
)
}
@@ -0,0 +1,71 @@
import { useState } from 'react'
import { page, userEvent } from 'vite-plus/test/browser'
import { render } from 'vitest-browser-react'
import ScoreThresholdItem from '../score-threshold-item'
beforeEach(async () => {
await page.viewport(800, 600)
})
it.each([
[undefined, '0.7'],
[-0.1, '0'],
[1.5, '1'],
[0.556, '0.56'],
])('normalizes configured threshold %s to %s in both editors', async (value, expected) => {
await render(<ScoreThresholdItem value={value} enable onChange={vi.fn()} />)
await expect
.element(page.getByRole('textbox', { name: 'appDebug.datasetConfig.score_threshold' }))
.toHaveValue(expected)
await expect
.element(page.getByRole('slider', { name: 'appDebug.datasetConfig.score_threshold' }))
.toHaveAttribute('aria-valuetext', expected)
})
it('saves threshold edits in hundredths and makes both editors unavailable when switched off', async () => {
function Settings() {
const [values, setValues] = useState({ score_threshold: 0.5 })
const [enabled, setEnabled] = useState({ score_threshold: true })
return (
<>
<ScoreThresholdItem
value={values.score_threshold}
enable={enabled.score_threshold}
hasSwitch
onChange={(key, next) => setValues((previous) => ({ ...previous, [key]: next }))}
onSwitchChange={(key, next) => setEnabled((previous) => ({ ...previous, [key]: next }))}
/>
<output aria-label="Saved parameters">{JSON.stringify(values)}</output>
<output aria-label="Enabled parameters">{JSON.stringify(enabled)}</output>
</>
)
}
await render(<Settings />)
const name = 'appDebug.datasetConfig.score_threshold'
const input = page.getByRole('textbox', { name })
const slider = page.getByRole('slider', { name })
const saved = page.getByRole('status', { name: 'Saved parameters' })
await page.getByRole('button', { name: 'Increment value' }).click()
await expect.element(input).toHaveValue('0.51')
await expect.element(slider).toHaveAttribute('aria-valuetext', '0.51')
await expect.element(saved).toHaveTextContent('{"score_threshold":0.51}')
await input.clear()
await userEvent.keyboard('0.55')
await expect.element(input).toHaveValue('0.55')
await expect.element(slider).toHaveAttribute('aria-valuetext', '0.55')
await expect.element(saved).toHaveTextContent('{"score_threshold":0.55}')
// Return through the real tab order rather than imperatively focusing the switch.
await userEvent.tab({ shift: true })
await expect.element(page.getByRole('button', { name })).toHaveFocus()
await userEvent.tab({ shift: true })
const toggle = page.getByRole('switch', { name })
await expect.element(toggle).toHaveFocus()
await userEvent.keyboard(' ')
await expect.element(toggle).not.toBeChecked()
await expect
.element(page.getByRole('status', { name: 'Enabled parameters' }))
.toHaveTextContent('{"score_threshold":false}')
await expect.element(input).toBeDisabled()
await expect.element(slider).toBeDisabled()
await expect.element(saved).toHaveTextContent('{"score_threshold":0.55}')
})
@@ -1,55 +0,0 @@
import { render, screen } from '@testing-library/react'
import userEvent from '@testing-library/user-event'
import { useState } from 'react'
import ScoreThresholdItem from '../score-threshold-item'
it.each([
[undefined, '0.7'],
[-0.1, '0'],
[1.5, '1'],
[0.556, '0.56'],
])('normalizes configured threshold %s to %s', (value, expected) => {
render(<ScoreThresholdItem value={value} enable onChange={vi.fn()} />)
expect(screen.getByRole('textbox')).toHaveValue(expected)
})
it('edits the score threshold in hundredths and disables its controls when switched off', async () => {
const user = userEvent.setup()
const onChange = vi.fn()
const onSwitchChange = vi.fn()
function Settings() {
const [value, setValue] = useState(0.5)
const [enable, setEnable] = useState(true)
return (
<ScoreThresholdItem
value={value}
enable={enable}
hasSwitch
onChange={(key, next) => {
onChange(key, next)
setValue(next)
}}
onSwitchChange={(key, next) => {
onSwitchChange(key, next)
setEnable(next)
}}
/>
)
}
render(<Settings />)
const input = screen.getByRole('textbox')
await user.click(screen.getByRole('button', { name: /increment/i }))
expect(input).toHaveValue('0.51')
expect(onChange).toHaveBeenLastCalledWith('score_threshold', 0.51)
await user.clear(input)
await user.type(input, '0.55')
expect(onChange).toHaveBeenLastCalledWith('score_threshold', 0.55)
await user.click(screen.getByRole('switch'))
expect(onSwitchChange).toHaveBeenLastCalledWith('score_threshold', false)
expect(input).toBeDisabled()
expect(
screen.getByLabelText('appDebug.datasetConfig.score_threshold', {
selector: 'input[type="range"]',
}),
).toBeDisabled()
})
@@ -0,0 +1,80 @@
import { useState } from 'react'
import { page, userEvent } from 'vite-plus/test/browser'
import { render } from 'vitest-browser-react'
import TopKItem from '../top-k-item'
vi.mock('@/env', () => ({ env: { NEXT_PUBLIC_TOP_K_MAX_VALUE: 10 } }))
beforeEach(async () => {
await page.viewport(800, 600)
})
function Settings({ max }: { max?: number }) {
const [values, setValues] = useState({ top_k: 2 })
return (
<>
<TopKItem
enable
value={values.top_k}
max={max}
onChange={(key, next) => setValues((previous) => ({ ...previous, [key]: next }))}
/>
<output aria-label="Saved parameters">{JSON.stringify(values)}</output>
</>
)
}
it('keeps numeric edits and native slider keys synchronized within configured top-k bounds', async () => {
await render(<Settings />)
const input = page.getByRole('textbox', { name: 'appDebug.datasetConfig.top_k' })
const slider = page.getByRole('slider', { name: 'appDebug.datasetConfig.top_k' })
const saved = page.getByRole('status', { name: 'Saved parameters' })
await page.getByRole('button', { name: 'Increment value' }).click()
await expect.element(input).toHaveValue('3')
await expect.element(slider).toHaveValue('3')
await expect.element(saved).toHaveTextContent('{"top_k":3}')
await input.click()
await userEvent.tab()
await expect.element(slider).toHaveFocus()
await userEvent.keyboard('{End}')
await expect.element(input).toHaveValue('10')
await expect.element(saved).toHaveTextContent('{"top_k":10}')
await userEvent.keyboard('{ArrowRight}')
await expect.element(input).toHaveValue('10')
await expect.element(saved).toHaveTextContent('{"top_k":10}')
await userEvent.keyboard('{Home}')
await expect.element(input).toHaveValue('1')
await expect.element(saved).toHaveTextContent('{"top_k":1}')
await userEvent.keyboard('{ArrowLeft}')
await expect.element(input).toHaveValue('1')
await expect.element(saved).toHaveTextContent('{"top_k":1}')
})
it.each([
{ enable: false, disabled: false },
{ enable: true, disabled: true },
])('makes both top-k editors unavailable with %j', async (props) => {
await render(<TopKItem {...props} value={2} onChange={vi.fn()} />)
await expect
.element(page.getByRole('textbox', { name: 'appDebug.datasetConfig.top_k' }))
.toBeDisabled()
await expect
.element(page.getByRole('slider', { name: 'appDebug.datasetConfig.top_k' }))
.toBeDisabled()
})
it('applies a feature-specific maximum to native slider edits and the saved top-k', async () => {
await render(<Settings max={100} />)
const slider = page.getByRole('slider', { name: 'appDebug.datasetConfig.top_k' })
await page.getByRole('textbox', { name: 'appDebug.datasetConfig.top_k' }).click()
await userEvent.tab()
await expect.element(slider).toHaveFocus()
await userEvent.keyboard('{End}')
await expect.element(slider).toHaveValue('100')
await expect
.element(page.getByRole('textbox', { name: 'appDebug.datasetConfig.top_k' }))
.toHaveValue('100')
await expect
.element(page.getByRole('status', { name: 'Saved parameters' }))
.toHaveTextContent('{"top_k":100}')
})
@@ -1,62 +0,0 @@
import { render, screen } from '@testing-library/react'
import userEvent from '@testing-library/user-event'
import { useState } from 'react'
import TopKItem from '../top-k-item'
vi.mock('@/env', () => ({ env: { NEXT_PUBLIC_TOP_K_MAX_VALUE: 10 } }))
it('keeps the numeric field and slider synchronized within the configured top-k bounds', async () => {
const user = userEvent.setup()
const onChange = vi.fn()
function Harness() {
const [value, setValue] = useState(2)
return (
<TopKItem
enable
value={value}
onChange={(key, next) => {
onChange(key, next)
setValue(next)
}}
/>
)
}
render(<Harness />)
const input = screen.getByRole('textbox', { name: 'appDebug.datasetConfig.top_k' })
const slider = screen.getByLabelText('appDebug.datasetConfig.top_k', {
selector: 'input[type="range"]',
})
await user.click(screen.getByRole('button', { name: /increment/i }))
expect(input).toHaveValue('3')
expect(slider).toHaveValue('3')
slider.focus()
await user.keyboard('{End}{ArrowRight}')
expect(input).toHaveValue('10')
expect(onChange).toHaveBeenLastCalledWith('top_k', 10)
await user.keyboard('{Home}{ArrowLeft}')
expect(input).toHaveValue('1')
expect(onChange).toHaveBeenLastCalledWith('top_k', 1)
})
it.each([
{ enable: false, disabled: false },
{ enable: true, disabled: true },
])('prevents edits with %j', async (props) => {
const user = userEvent.setup()
const onChange = vi.fn()
render(<TopKItem {...props} value={2} onChange={onChange} />)
const input = screen.getByRole('textbox', { name: 'appDebug.datasetConfig.top_k' })
expect(input).toBeDisabled()
expect(
screen.getByLabelText('appDebug.datasetConfig.top_k', { selector: 'input[type="range"]' }),
).toBeDisabled()
await user.type(input, '9')
expect(onChange).not.toHaveBeenCalled()
})
it('allows a feature-specific maximum', () => {
render(<TopKItem enable value={2} max={100} onChange={vi.fn()} />)
expect(
screen.getByLabelText('appDebug.datasetConfig.top_k', { selector: 'input[type="range"]' }),
).toHaveAttribute('max', '100')
})