Skip to content

Commit 2d4f608

Browse files
committed
tests for field.py
1 parent 38f2400 commit 2d4f608

File tree

1 file changed

+320
-0
lines changed

1 file changed

+320
-0
lines changed

tests/cli/forms/test_field.py

Lines changed: 320 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,320 @@
1+
# SPDX-FileCopyrightText: Copyright (c) 2025 NVIDIA CORPORATION & AFFILIATES. All rights reserved.
2+
# SPDX-License-Identifier: Apache-2.0
3+
4+
from unittest.mock import Mock, patch
5+
6+
import pytest
7+
8+
from data_designer.cli.forms.field import Field, NumericField, SelectField, TextField, ValidationError
9+
10+
11+
# ValidationError tests
12+
def test_validation_error_is_exception() -> None:
13+
"""Test that ValidationError is an exception."""
14+
error = ValidationError("Test error")
15+
assert isinstance(error, Exception)
16+
assert str(error) == "Test error"
17+
18+
19+
# TextField tests - focus on validation behavior
20+
def test_text_field_value_setter_without_validator() -> None:
21+
"""Test setting TextField value without validator succeeds."""
22+
field = TextField(name="name", prompt="Enter name")
23+
field.value = "John Doe"
24+
assert field.value == "John Doe"
25+
26+
27+
def test_text_field_value_setter_with_valid_value() -> None:
28+
"""Test setting TextField value with validator that passes."""
29+
validator = Mock(return_value=(True, None))
30+
field = TextField(name="email", prompt="Enter email", validator=validator)
31+
32+
field.value = "[email protected]"
33+
34+
assert field.value == "[email protected]"
35+
validator.assert_called_once_with("[email protected]")
36+
37+
38+
def test_text_field_value_setter_with_invalid_value() -> None:
39+
"""Test setting TextField value with validator that fails raises ValidationError."""
40+
validator = Mock(return_value=(False, "Invalid format"))
41+
field = TextField(name="email", prompt="Enter email", validator=validator)
42+
43+
with pytest.raises(ValidationError, match="Invalid format"):
44+
field.value = "not-an-email"
45+
46+
assert field.value is None
47+
48+
49+
def test_text_field_validator_receives_string() -> None:
50+
"""Test that validator always receives string values."""
51+
validator = Mock(return_value=(True, None))
52+
field = TextField(name="field", prompt="Enter", validator=validator)
53+
54+
field.value = "text"
55+
validator.assert_called_with("text")
56+
57+
58+
@patch("data_designer.cli.ui.prompt_text_input")
59+
def test_text_field_prompt_user_returns_input(mock_prompt: Mock) -> None:
60+
"""Test TextField prompt_user returns user input."""
61+
mock_prompt.return_value = "user input"
62+
field = TextField(name="name", prompt="Enter name")
63+
64+
assert field.prompt_user() == "user input"
65+
66+
67+
@patch("data_designer.cli.ui.BACK", "BACK_SENTINEL")
68+
@patch("data_designer.cli.ui.prompt_text_input")
69+
def test_text_field_prompt_user_handles_back_navigation(mock_prompt: Mock) -> None:
70+
"""Test TextField prompt_user properly returns BACK sentinel."""
71+
mock_prompt.return_value = "BACK_SENTINEL"
72+
field = TextField(name="name", prompt="Enter name")
73+
74+
result = field.prompt_user(allow_back=True)
75+
76+
assert result == "BACK_SENTINEL"
77+
78+
79+
# SelectField tests
80+
def test_select_field_value_setter() -> None:
81+
"""Test setting SelectField value."""
82+
options = {"1": "One", "2": "Two"}
83+
field = SelectField(name="number", prompt="Select number", options=options)
84+
85+
field.value = "1"
86+
87+
assert field.value == "1"
88+
89+
90+
@patch("data_designer.cli.ui.select_with_arrows")
91+
def test_select_field_prompt_user_returns_selection(mock_select: Mock) -> None:
92+
"""Test SelectField prompt_user returns user selection."""
93+
mock_select.return_value = "opt1"
94+
options = {"opt1": "Option 1", "opt2": "Option 2"}
95+
field = SelectField(name="choice", prompt="Select", options=options)
96+
97+
assert field.prompt_user() == "opt1"
98+
99+
100+
@patch("data_designer.cli.ui.BACK", "BACK_SENTINEL")
101+
@patch("data_designer.cli.ui.select_with_arrows")
102+
def test_select_field_prompt_user_handles_back_navigation(mock_select: Mock) -> None:
103+
"""Test SelectField prompt_user properly returns BACK sentinel."""
104+
mock_select.return_value = "BACK_SENTINEL"
105+
options = {"1": "One", "2": "Two"}
106+
field = SelectField(name="num", prompt="Select", options=options)
107+
108+
result = field.prompt_user(allow_back=True)
109+
110+
assert result == "BACK_SENTINEL"
111+
112+
113+
# NumericField validator tests - core business logic
114+
def test_numeric_field_validator_valid_value() -> None:
115+
"""Test NumericField validator accepts valid value within range."""
116+
field = NumericField(name="age", prompt="Enter age", min_value=0.0, max_value=150.0)
117+
118+
is_valid, error = field.validator("25")
119+
120+
assert is_valid is True
121+
assert error is None
122+
123+
124+
def test_numeric_field_validator_rejects_below_min() -> None:
125+
"""Test NumericField validator rejects value below minimum."""
126+
field = NumericField(name="age", prompt="Enter age", min_value=0.0, max_value=150.0)
127+
128+
is_valid, error = field.validator("-5")
129+
130+
assert is_valid is False
131+
assert error == "Value must be between 0.0 and 150.0"
132+
133+
134+
def test_numeric_field_validator_rejects_above_max() -> None:
135+
"""Test NumericField validator rejects value above maximum."""
136+
field = NumericField(name="age", prompt="Enter age", min_value=0.0, max_value=150.0)
137+
138+
is_valid, error = field.validator("200")
139+
140+
assert is_valid is False
141+
assert error == "Value must be between 0.0 and 150.0"
142+
143+
144+
def test_numeric_field_validator_only_min_accepts_valid() -> None:
145+
"""Test NumericField validator with only min value accepts valid input."""
146+
field = NumericField(name="count", prompt="Enter count", min_value=10.0)
147+
148+
is_valid, error = field.validator("15")
149+
150+
assert is_valid is True
151+
assert error is None
152+
153+
154+
def test_numeric_field_validator_only_min_rejects_invalid() -> None:
155+
"""Test NumericField validator with only min value rejects invalid input."""
156+
field = NumericField(name="count", prompt="Enter count", min_value=10.0)
157+
158+
is_valid, error = field.validator("5")
159+
160+
assert is_valid is False
161+
assert error == "Value must be >= 10.0"
162+
163+
164+
def test_numeric_field_validator_only_max_accepts_valid() -> None:
165+
"""Test NumericField validator with only max value accepts valid input."""
166+
field = NumericField(name="score", prompt="Enter score", max_value=100.0)
167+
168+
is_valid, error = field.validator("85")
169+
170+
assert is_valid is True
171+
assert error is None
172+
173+
174+
def test_numeric_field_validator_only_max_rejects_invalid() -> None:
175+
"""Test NumericField validator with only max value rejects invalid input."""
176+
field = NumericField(name="score", prompt="Enter score", max_value=100.0)
177+
178+
is_valid, error = field.validator("150")
179+
180+
assert is_valid is False
181+
assert error == "Value must be <= 100.0"
182+
183+
184+
def test_numeric_field_validator_rejects_non_numeric_with_range() -> None:
185+
"""Test NumericField validator rejects non-numeric input when range is set."""
186+
field = NumericField(name="age", prompt="Enter age", min_value=0.0, max_value=150.0)
187+
188+
is_valid, error = field.validator("not-a-number")
189+
190+
assert is_valid is False
191+
assert error == "Value must be between 0.0 and 150.0"
192+
193+
194+
def test_numeric_field_validator_rejects_non_numeric_without_range() -> None:
195+
"""Test NumericField validator rejects non-numeric input when no range is set."""
196+
field = NumericField(name="count", prompt="Enter count")
197+
198+
is_valid, error = field.validator("not-a-number")
199+
200+
assert is_valid is False
201+
assert error == "Must be a valid number"
202+
203+
204+
def test_numeric_field_validator_accepts_empty_when_not_required() -> None:
205+
"""Test NumericField validator accepts empty value when field is not required."""
206+
field = NumericField(
207+
name="optional_value",
208+
prompt="Enter value",
209+
required=False,
210+
min_value=0.0,
211+
max_value=100.0,
212+
)
213+
214+
is_valid, error = field.validator("")
215+
216+
assert is_valid is True
217+
assert error is None
218+
219+
220+
def test_numeric_field_validator_handles_boundary_values() -> None:
221+
"""Test NumericField validator accepts boundary values."""
222+
field = NumericField(name="score", prompt="Enter score", min_value=0.0, max_value=100.0)
223+
224+
# Test min boundary
225+
is_valid_min, error_min = field.validator("0.0")
226+
assert is_valid_min is True
227+
assert error_min is None
228+
229+
# Test max boundary
230+
is_valid_max, error_max = field.validator("100.0")
231+
assert is_valid_max is True
232+
assert error_max is None
233+
234+
235+
# NumericField value setter tests with validation
236+
def test_numeric_field_value_setter_accepts_valid() -> None:
237+
"""Test setting NumericField value with valid number succeeds."""
238+
field = NumericField(name="age", prompt="Enter age", min_value=0.0, max_value=150.0)
239+
240+
field.value = 25.5
241+
242+
assert field.value == 25.5
243+
244+
245+
def test_numeric_field_value_setter_rejects_invalid() -> None:
246+
"""Test setting NumericField value with invalid number raises ValidationError."""
247+
field = NumericField(name="age", prompt="Enter age", min_value=0.0, max_value=150.0)
248+
249+
with pytest.raises(ValidationError):
250+
field.value = 200.0
251+
252+
assert field.value is None
253+
254+
255+
# NumericField prompt_user tests
256+
@patch("data_designer.cli.ui.prompt_text_input")
257+
def test_numeric_field_prompt_user_returns_float(mock_prompt: Mock) -> None:
258+
"""Test NumericField prompt_user converts string to float."""
259+
mock_prompt.return_value = "42"
260+
field = NumericField(name="age", prompt="Enter age")
261+
262+
result = field.prompt_user()
263+
264+
assert result == 42.0
265+
assert isinstance(result, float)
266+
267+
268+
@patch("data_designer.cli.ui.prompt_text_input")
269+
def test_numeric_field_prompt_user_returns_none_for_empty(mock_prompt: Mock) -> None:
270+
"""Test NumericField prompt_user returns None for empty input."""
271+
mock_prompt.return_value = ""
272+
field = NumericField(name="optional", prompt="Enter value", required=False)
273+
274+
result = field.prompt_user()
275+
276+
assert result is None
277+
278+
279+
@patch("data_designer.cli.ui.BACK", "BACK_SENTINEL")
280+
@patch("data_designer.cli.ui.prompt_text_input")
281+
def test_numeric_field_prompt_user_handles_back_navigation(mock_prompt: Mock) -> None:
282+
"""Test NumericField prompt_user properly returns BACK sentinel."""
283+
mock_prompt.return_value = "BACK_SENTINEL"
284+
field = NumericField(name="value", prompt="Enter value")
285+
286+
result = field.prompt_user(allow_back=True)
287+
288+
assert result == "BACK_SENTINEL"
289+
290+
291+
# Field base class tests - design constraints
292+
def test_field_is_abstract() -> None:
293+
"""Test that Field cannot be instantiated directly."""
294+
with pytest.raises(TypeError):
295+
Field(name="test", prompt="Test prompt") # type: ignore
296+
297+
298+
def test_field_generic_type_preserved() -> None:
299+
"""Test that Field generic type is preserved in subclasses."""
300+
text_field = TextField(name="text", prompt="Enter text")
301+
numeric_field = NumericField(name="num", prompt="Enter number")
302+
303+
text_field.value = "string value"
304+
numeric_field.value = 42.0
305+
306+
assert isinstance(text_field.value, str)
307+
assert isinstance(numeric_field.value, float)
308+
309+
310+
def test_validator_converts_non_string_values() -> None:
311+
"""Test that validator converts non-string values to strings before validation."""
312+
validator = Mock(return_value=(True, None))
313+
field = NumericField(name="num", prompt="Enter number", min_value=0.0, max_value=100.0)
314+
315+
# Override validator to test conversion
316+
field.validator = validator
317+
field.value = 42.5
318+
319+
# Validator should be called with string representation
320+
validator.assert_called_once_with("42.5")

0 commit comments

Comments
 (0)