Skip to content

Commit 9ebeefe

Browse files
committed
tests for form.py
1 parent 2d4f608 commit 9ebeefe

File tree

1 file changed

+309
-0
lines changed

1 file changed

+309
-0
lines changed

tests/cli/forms/test_form.py

Lines changed: 309 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,309 @@
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 TextField, ValidationError
9+
from data_designer.cli.forms.form import Form
10+
11+
12+
# Helper to create a simple field for testing
13+
def create_field(name: str, prompt: str = "Enter value") -> TextField:
14+
"""Create a TextField for testing."""
15+
return TextField(name=name, prompt=prompt)
16+
17+
18+
# get_field tests - basic lookup behavior
19+
def test_get_field_returns_existing_field() -> None:
20+
"""Test get_field returns field when it exists."""
21+
field1 = create_field("name")
22+
field2 = create_field("email")
23+
form = Form(name="test_form", fields=[field1, field2])
24+
25+
assert form.get_field("name") is field1
26+
assert form.get_field("email") is field2
27+
28+
29+
def test_get_field_returns_none_for_nonexistent_field() -> None:
30+
"""Test get_field returns None when field doesn't exist."""
31+
field = create_field("name")
32+
form = Form(name="test_form", fields=[field])
33+
34+
result = form.get_field("nonexistent")
35+
36+
assert result is None
37+
38+
39+
def test_field_map_handles_duplicate_names() -> None:
40+
"""Test field map with duplicate names - last one wins."""
41+
field1 = create_field("name")
42+
field2 = create_field("name")
43+
44+
form = Form(name="test_form", fields=[field1, field2])
45+
46+
# Last field with same name should be accessible
47+
assert form.get_field("name") is field2
48+
49+
50+
# get_values tests - filtering and collection behavior
51+
def test_get_values_returns_only_non_none_values() -> None:
52+
"""Test get_values filters out fields with None values."""
53+
field1 = create_field("name")
54+
field2 = create_field("email")
55+
field3 = create_field("optional")
56+
57+
field1.value = "John"
58+
field2.value = "[email protected]"
59+
field3.value = None
60+
61+
form = Form(name="test_form", fields=[field1, field2, field3])
62+
63+
result = form.get_values()
64+
65+
assert result == {"name": "John", "email": "[email protected]"}
66+
assert "optional" not in result
67+
68+
69+
def test_get_values_returns_empty_dict_when_no_values_set() -> None:
70+
"""Test get_values returns empty dict when all fields are None."""
71+
field1 = create_field("name")
72+
field2 = create_field("email")
73+
form = Form(name="test_form", fields=[field1, field2])
74+
75+
result = form.get_values()
76+
77+
assert result == {}
78+
79+
80+
# set_values tests - batch setting behavior
81+
def test_set_values_sets_matching_fields() -> None:
82+
"""Test set_values updates all matching fields."""
83+
field1 = create_field("name")
84+
field2 = create_field("email")
85+
form = Form(name="test_form", fields=[field1, field2])
86+
87+
form.set_values({"name": "Alice", "email": "[email protected]"})
88+
89+
assert field1.value == "Alice"
90+
assert field2.value == "[email protected]"
91+
92+
93+
def test_set_values_ignores_unknown_fields() -> None:
94+
"""Test set_values silently ignores values for non-existent fields."""
95+
field = create_field("name")
96+
form = Form(name="test_form", fields=[field])
97+
98+
# Should not raise error
99+
form.set_values({"name": "Bob", "unknown": "value"})
100+
101+
assert field.value == "Bob"
102+
103+
104+
def test_set_values_triggers_field_validation() -> None:
105+
"""Test set_values enforces field validators."""
106+
validator = Mock(return_value=(False, "Invalid value"))
107+
field = TextField(name="email", prompt="Enter email", validator=validator)
108+
form = Form(name="test_form", fields=[field])
109+
110+
with pytest.raises(ValidationError):
111+
form.set_values({"email": "invalid"})
112+
113+
114+
# prompt_all tests - focus on navigation logic and edge cases
115+
@patch("data_designer.cli.ui.BACK", "BACK_SENTINEL")
116+
def test_prompt_all_collects_all_values_sequentially() -> None:
117+
"""Test prompt_all prompts each field in order and returns all values."""
118+
field1 = create_field("name")
119+
field2 = create_field("email")
120+
field3 = create_field("phone")
121+
122+
field1.prompt_user = Mock(return_value="John")
123+
field2.prompt_user = Mock(return_value="[email protected]")
124+
field3.prompt_user = Mock(return_value="123-456-7890")
125+
126+
form = Form(name="test_form", fields=[field1, field2, field3])
127+
128+
result = form.prompt_all()
129+
130+
assert result == {
131+
"name": "John",
132+
"email": "[email protected]",
133+
"phone": "123-456-7890",
134+
}
135+
136+
137+
@patch("data_designer.cli.ui.BACK", "BACK_SENTINEL")
138+
def test_prompt_all_returns_none_when_user_cancels() -> None:
139+
"""Test prompt_all returns None when any field returns None (cancel)."""
140+
field1 = create_field("name")
141+
field2 = create_field("email")
142+
143+
field1.prompt_user = Mock(return_value="John")
144+
field2.prompt_user = Mock(return_value=None)
145+
146+
form = Form(name="test_form", fields=[field1, field2])
147+
148+
result = form.prompt_all()
149+
150+
assert result is None
151+
152+
153+
@patch("data_designer.cli.ui.BACK", "BACK_SENTINEL")
154+
def test_prompt_all_navigates_back_to_previous_field() -> None:
155+
"""Test prompt_all allows user to go back and change previous answers."""
156+
field1 = create_field("name")
157+
field2 = create_field("email")
158+
159+
# User enters "John", then backs up from email, re-enters "Jane", proceeds with email
160+
field1.prompt_user = Mock(side_effect=["John", "Jane"])
161+
field2.prompt_user = Mock(side_effect=["BACK_SENTINEL", "[email protected]"])
162+
163+
form = Form(name="test_form", fields=[field1, field2])
164+
165+
result = form.prompt_all()
166+
167+
# Changed value should be preserved
168+
assert result == {"name": "Jane", "email": "[email protected]"}
169+
assert field1.prompt_user.call_count == 2
170+
assert field2.prompt_user.call_count == 2
171+
172+
173+
@patch("data_designer.cli.ui.BACK", "BACK_SENTINEL")
174+
def test_prompt_all_handles_back_from_first_field() -> None:
175+
"""Test prompt_all stays at first field if BACK is returned (edge case)."""
176+
field1 = create_field("name")
177+
field2 = create_field("email")
178+
179+
# Simulate first field somehow returning BACK, then valid input
180+
field1.prompt_user = Mock(side_effect=["BACK_SENTINEL", "John"])
181+
field2.prompt_user = Mock(return_value="[email protected]")
182+
183+
form = Form(name="test_form", fields=[field1, field2])
184+
185+
result = form.prompt_all()
186+
187+
# Should eventually complete successfully
188+
assert result == {"name": "John", "email": "[email protected]"}
189+
assert field1.prompt_user.call_count == 2
190+
191+
192+
@patch("data_designer.cli.forms.form.print_error")
193+
@patch("data_designer.cli.ui.BACK", "BACK_SENTINEL")
194+
def test_prompt_all_retries_on_validation_error(mock_print_error: Mock) -> None:
195+
"""Test prompt_all catches validation errors and re-prompts user."""
196+
validator = Mock(
197+
side_effect=[
198+
(False, "Invalid format"),
199+
(True, None),
200+
]
201+
)
202+
field = TextField(name="email", prompt="Enter email", validator=validator)
203+
field.prompt_user = Mock(side_effect=["invalid", "[email protected]"])
204+
205+
form = Form(name="test_form", fields=[field])
206+
207+
result = form.prompt_all()
208+
209+
# Should succeed after retry
210+
assert result == {"email": "[email protected]"}
211+
# Should have printed error message
212+
assert mock_print_error.called
213+
# Should have prompted twice
214+
assert field.prompt_user.call_count == 2
215+
216+
217+
@patch("data_designer.cli.ui.BACK", "BACK_SENTINEL")
218+
def test_prompt_all_handles_multiple_back_steps() -> None:
219+
"""Test prompt_all handles navigating back multiple fields."""
220+
field1 = create_field("name")
221+
field2 = create_field("email")
222+
field3 = create_field("phone")
223+
224+
# User goes forward to field3, backs to field2, backs to field1, then completes
225+
field1.prompt_user = Mock(side_effect=["John", "Jane"])
226+
field2.prompt_user = Mock(side_effect=["[email protected]", "BACK_SENTINEL", "[email protected]"])
227+
field3.prompt_user = Mock(side_effect=["BACK_SENTINEL", "123-456-7890"])
228+
229+
form = Form(name="test_form", fields=[field1, field2, field3])
230+
231+
result = form.prompt_all()
232+
233+
assert result == {
234+
"name": "Jane",
235+
"email": "[email protected]",
236+
"phone": "123-456-7890",
237+
}
238+
239+
240+
@patch("data_designer.cli.ui.BACK", "BACK_SENTINEL")
241+
def test_prompt_all_handles_validation_error_after_back_navigation() -> None:
242+
"""Test validation errors work correctly after navigating back."""
243+
validator = Mock(
244+
side_effect=[
245+
(True, None), # First entry valid
246+
(False, "Invalid"), # After back, new entry invalid
247+
(True, None), # Third try valid
248+
]
249+
)
250+
field1 = TextField(name="email", prompt="Enter email", validator=validator)
251+
field2 = create_field("name")
252+
253+
# Enter valid email, enter name, go back, enter invalid email, enter valid email, complete
254+
field1.prompt_user = Mock(side_effect=["[email protected]", "invalid", "[email protected]"])
255+
field2.prompt_user = Mock(side_effect=["John", "BACK_SENTINEL", "Jane"])
256+
257+
form = Form(name="test_form", fields=[field1, field2])
258+
259+
result = form.prompt_all()
260+
261+
# Should eventually succeed with final values
262+
assert result == {"email": "[email protected]", "name": "Jane"}
263+
264+
265+
# Edge cases
266+
@patch("data_designer.cli.ui.BACK", "BACK_SENTINEL")
267+
def test_prompt_all_with_single_field() -> None:
268+
"""Test prompt_all works with single field form."""
269+
field = create_field("name")
270+
field.prompt_user = Mock(return_value="John")
271+
272+
form = Form(name="test_form", fields=[field])
273+
274+
result = form.prompt_all()
275+
276+
assert result == {"name": "John"}
277+
278+
279+
@patch("data_designer.cli.ui.BACK", "BACK_SENTINEL")
280+
def test_prompt_all_with_empty_fields_list() -> None:
281+
"""Test prompt_all with no fields returns empty dict."""
282+
form = Form(name="test_form", fields=[])
283+
284+
result = form.prompt_all()
285+
286+
assert result == {}
287+
288+
289+
@patch("data_designer.cli.ui.BACK", "BACK_SENTINEL")
290+
def test_prompt_all_preserves_values_from_interrupted_session() -> None:
291+
"""Test that values are stored even if user cancels later."""
292+
field1 = create_field("name")
293+
field2 = create_field("email")
294+
field3 = create_field("phone")
295+
296+
field1.prompt_user = Mock(return_value="John")
297+
field2.prompt_user = Mock(return_value="[email protected]")
298+
field3.prompt_user = Mock(return_value=None) # User cancels
299+
300+
form = Form(name="test_form", fields=[field1, field2, field3])
301+
302+
result = form.prompt_all()
303+
304+
# Should return None on cancel
305+
assert result is None
306+
# But fields should still have their values
307+
assert field1.value == "John"
308+
assert field2.value == "[email protected]"
309+
assert field3.value is None

0 commit comments

Comments
 (0)