1- import { test , describe , assert , afterEach , expect } from "vitest" ;
2-
3- import { cleanup , render } from "@self/tootils/render" ;
4- import { tick } from "svelte" ;
1+ import { test , describe , expect , afterEach } from "vitest" ;
2+ import { cleanup , render , fireEvent , waitFor } from "@self/tootils/render" ;
3+ import { run_shared_prop_tests } from "@self/tootils/shared-prop-tests" ;
54import event from "@testing-library/user-event" ;
65
76import Radio from "./Index.svelte" ;
87
9- describe ( "Radio" , ( ) => {
10- afterEach ( ( ) => cleanup ( ) ) ;
11- const choices = [
12- [ "dog" , "dog" ] ,
13- [ "cat" , "cat" ] ,
14- [ "turtle" , "turtle" ]
15- ] as [ string , string ] [ ] ;
8+ afterEach ( cleanup ) ;
9+
10+ const choices : [ string , string ] [ ] = [
11+ [ "dog" , "dog" ] ,
12+ [ "cat" , "cat" ] ,
13+ [ "turtle" , "turtle" ]
14+ ] ;
15+
16+ const default_props = {
17+ show_label : true ,
18+ choices,
19+ value : "cat" ,
20+ label : "Radio" ,
21+ interactive : true
22+ } ;
23+
24+ run_shared_prop_tests ( {
25+ component : Radio ,
26+ name : "Radio" ,
27+ base_props : {
28+ choices,
29+ value : "dog" ,
30+ interactive : true
31+ } ,
32+ has_validation_error : false
33+ } ) ;
34+
35+ describe ( "Props: value" , ( ) => {
36+ test ( "renders provided value as checked" , async ( ) => {
37+ const { getAllByRole } = await render ( Radio , default_props ) ;
38+
39+ const radios = getAllByRole ( "radio" ) as HTMLInputElement [ ] ;
40+ expect ( radios ) . toHaveLength ( 3 ) ;
41+ expect ( radios [ 0 ] ) . not . toBeChecked ( ) ;
42+ expect ( radios [ 1 ] ) . toBeChecked ( ) ;
43+ expect ( radios [ 2 ] ) . not . toBeChecked ( ) ;
44+ } ) ;
45+
46+ test ( "null value renders no radio checked" , async ( ) => {
47+ const { getAllByRole } = await render ( Radio , {
48+ ...default_props ,
49+ value : null
50+ } ) ;
51+
52+ const radios = getAllByRole ( "radio" ) as HTMLInputElement [ ] ;
53+ radios . forEach ( ( radio ) => {
54+ expect ( radio ) . not . toBeChecked ( ) ;
55+ } ) ;
56+ } ) ;
57+
58+ test ( "undefined value renders no radio checked" , async ( ) => {
59+ const { getAllByRole } = await render ( Radio , {
60+ ...default_props ,
61+ value : undefined
62+ } ) ;
63+
64+ const radios = getAllByRole ( "radio" ) as HTMLInputElement [ ] ;
65+ radios . forEach ( ( radio ) => {
66+ expect ( radio ) . not . toBeChecked ( ) ;
67+ } ) ;
68+ } ) ;
69+
70+ test ( "value not in choices renders no radio checked" , async ( ) => {
71+ const { getAllByRole } = await render ( Radio , {
72+ ...default_props ,
73+ value : "fish"
74+ } ) ;
75+
76+ const radios = getAllByRole ( "radio" ) as HTMLInputElement [ ] ;
77+ radios . forEach ( ( radio ) => {
78+ expect ( radio ) . not . toBeChecked ( ) ;
79+ } ) ;
80+ } ) ;
81+
82+ test ( "numeric values are supported in choices" , async ( ) => {
83+ const numeric_choices : [ string , number ] [ ] = [
84+ [ "one" , 1 ] ,
85+ [ "two" , 2 ] ,
86+ [ "three" , 3 ]
87+ ] ;
1688
17- test ( "renders provided value" , async ( ) => {
18- const { getAllByRole, getByTestId } = await render ( Radio , {
19- choices : choices ,
20- value : "cat" ,
21- label : "Radio"
89+ const { getAllByRole } = await render ( Radio , {
90+ ...default_props ,
91+ choices : numeric_choices ,
92+ value : 2
2293 } ) ;
2394
24- const cat_radio = getAllByRole ( "radio" ) [ 1 ] ;
95+ const radios = getAllByRole ( "radio" ) as HTMLInputElement [ ] ;
96+ expect ( radios [ 1 ] ) . toBeChecked ( ) ;
97+ } ) ;
98+ } ) ;
2599
26- expect ( cat_radio ) . toBeChecked ( ) ;
100+ describe ( "Props: choices" , ( ) => {
101+ test ( "renders display values as labels" , async ( ) => {
102+ const { getByText } = await render ( Radio , default_props ) ;
27103
28- const radioButtons : HTMLOptionElement [ ] = getAllByRole (
29- "radio"
30- ) as HTMLOptionElement [ ] ;
31- assert . equal ( radioButtons . length , 3 ) ;
104+ expect ( getByText ( "dog" ) ) . toBeVisible ( ) ;
105+ expect ( getByText ( "cat" ) ) . toBeVisible ( ) ;
106+ expect ( getByText ( "turtle" ) ) . toBeVisible ( ) ;
107+ } ) ;
32108
33- radioButtons . forEach ( ( radioButton : HTMLOptionElement , index ) => {
34- assert . equal ( radioButton . value === choices [ index ] [ 1 ] , true ) ;
109+ test ( "display value and internal value can differ" , async ( ) => {
110+ const custom_choices : [ string , string ] [ ] = [
111+ [ "dog label" , "dog_val" ] ,
112+ [ "cat label" , "cat_val" ]
113+ ] ;
114+
115+ const { getByText, getAllByRole, get_data } = await render ( Radio , {
116+ ...default_props ,
117+ choices : custom_choices ,
118+ value : "cat_val"
35119 } ) ;
120+
121+ expect ( getByText ( "dog label" ) ) . toBeVisible ( ) ;
122+ expect ( getByText ( "cat label" ) ) . toBeVisible ( ) ;
123+
124+ const radios = getAllByRole ( "radio" ) as HTMLInputElement [ ] ;
125+ expect ( radios [ 1 ] ) . toBeChecked ( ) ;
126+
127+ const data = await get_data ( ) ;
128+ expect ( data . value ) . toBe ( "cat_val" ) ;
36129 } ) ;
37130
38- test ( "should update the value when a radio is clicked" , async ( ) => {
39- const { getByDisplayValue, getAllByRole } = await render ( Radio , {
40- choices : choices ,
41- value : "cat" ,
42- label : "Radio" ,
43- interactive : true
131+ test ( "empty choices renders no radios" , async ( ) => {
132+ const { queryAllByRole } = await render ( Radio , {
133+ ...default_props ,
134+ choices : [ ] ,
135+ value : null
44136 } ) ;
45137
46- const dog_radio = getAllByRole ( "radio" ) [ 0 ] ;
138+ expect ( queryAllByRole ( "radio" ) ) . toHaveLength ( 0 ) ;
139+ } ) ;
140+ } ) ;
47141
48- await event . click ( dog_radio ) ;
142+ describe ( "Props: interactive" , ( ) => {
143+ test ( "interactive=false disables all radios" , async ( ) => {
144+ const { getAllByRole } = await render ( Radio , {
145+ ...default_props ,
146+ interactive : false
147+ } ) ;
148+
149+ const radios = getAllByRole ( "radio" ) as HTMLInputElement [ ] ;
150+ radios . forEach ( ( radio ) => {
151+ expect ( radio ) . toBeDisabled ( ) ;
152+ } ) ;
153+ } ) ;
154+ } ) ;
49155
50- expect ( dog_radio ) . toBeChecked ( ) ;
156+ describe ( "Props: info" , ( ) => {
157+ test ( "info renders descriptive text" , async ( ) => {
158+ const { getByText } = await render ( Radio , {
159+ ...default_props ,
160+ info : "Pick your favorite animal"
161+ } ) ;
51162
52- const cat_radio = getAllByRole ( "radio" ) [ 1 ] ;
163+ expect ( getByText ( "Pick your favorite animal" ) ) . toBeVisible ( ) ;
164+ } ) ;
53165
54- expect ( cat_radio ) . not . toBeChecked ( ) ;
166+ test ( "no info does not render info text" , async ( ) => {
167+ const { queryByText } = await render ( Radio , {
168+ ...default_props ,
169+ info : undefined
170+ } ) ;
55171
56- await event . click ( getByDisplayValue ( "turtle" ) ) ;
172+ expect ( queryByText ( "Pick your favorite animal" ) ) . toBeNull ( ) ;
173+ } ) ;
174+ } ) ;
57175
58- await event . click ( cat_radio ) ;
176+ describe ( "Props: buttons" , ( ) => {
177+ test ( "custom buttons are rendered when provided" , async ( ) => {
178+ const { getByLabelText } = await render ( Radio , {
179+ ...default_props ,
180+ buttons : [ { value : "Shuffle" , id : 1 , icon : null } ]
181+ } ) ;
59182
60- expect ( cat_radio ) . toBeChecked ( ) ;
183+ getByLabelText ( "Shuffle" ) ;
61184 } ) ;
62185
63- test . skip ( "should dispatch the select event when clicks" , async ( ) => {
64- const { listen, getAllByTestId } = await render ( Radio , {
65- choices : choices ,
66- value : "cat" ,
67- label : "Radio" ,
68- interactive : true
186+ test ( "no buttons rendered when null" , async ( ) => {
187+ const { queryByRole } = await render ( Radio , {
188+ ...default_props ,
189+ buttons : null
69190 } ) ;
70191
71- const mock = listen ( "select" ) ;
72- await event . click ( getAllByTestId ( "dog-radio-label" ) [ 0 ] ) ;
73- expect ( mock . callCount ) . toBe ( 1 ) ;
74- expect ( mock . calls [ 0 ] [ 0 ] . detail . data . value ) . toEqual ( "dog" ) ;
192+ expect ( queryByRole ( "button" ) ) . toBeNull ( ) ;
75193 } ) ;
194+ } ) ;
195+
196+ describe ( "Interactive behavior" , ( ) => {
197+ test ( "clicking through options selects and deselects correctly" , async ( ) => {
198+ const { getAllByRole } = await render ( Radio , default_props ) ;
199+
200+ const radios = getAllByRole ( "radio" ) as HTMLInputElement [ ] ;
201+ expect ( radios [ 1 ] ) . toBeChecked ( ) ;
202+
203+ await event . click ( radios [ 0 ] ) ;
204+ expect ( radios [ 0 ] ) . toBeChecked ( ) ;
205+ expect ( radios [ 1 ] ) . not . toBeChecked ( ) ;
76206
77- test ( "when multiple radios are on the screen, they should not conflict" , async ( ) => {
207+ await event . click ( radios [ 2 ] ) ;
208+ expect ( radios [ 2 ] ) . toBeChecked ( ) ;
209+ expect ( radios [ 0 ] ) . not . toBeChecked ( ) ;
210+
211+ await event . click ( radios [ 1 ] ) ;
212+ expect ( radios [ 1 ] ) . toBeChecked ( ) ;
213+ expect ( radios [ 2 ] ) . not . toBeChecked ( ) ;
214+ } ) ;
215+
216+ test ( "multiple radio instances on the same page do not conflict" , async ( ) => {
78217 const { container } = await render ( Radio , {
79- choices : choices ,
80- value : "cat" ,
81- label : "Radio" ,
82- interactive : true
218+ ...default_props ,
219+ value : "cat"
83220 } ) ;
84221
85222 const { getAllByLabelText } = await render (
86223 Radio ,
87224 {
88- choices : choices ,
89- value : "dog" ,
90- label : "Radio" ,
91- interactive : true
225+ ...default_props ,
226+ value : "dog"
92227 } ,
93- container
228+ { container : container as HTMLElement }
94229 ) ;
95230
96231 const items = getAllByLabelText ( "dog" ) as HTMLInputElement [ ] ;
@@ -99,28 +234,83 @@ describe("Radio", () => {
99234 await event . click ( items [ 0 ] ) ;
100235
101236 expect ( [ items [ 0 ] . checked , items [ 1 ] . checked ] ) . toEqual ( [ true , true ] ) ;
102- cleanup ( ) ;
237+ } ) ;
238+ } ) ;
239+
240+ describe ( "Events" , ( ) => {
241+ test ( "change emitted when value changes via set_data" , async ( ) => {
242+ const { listen, set_data } = await render ( Radio , default_props ) ;
243+
244+ const change = listen ( "change" ) ;
245+ const input = listen ( "input" ) ;
246+ await set_data ( { value : "dog" } ) ;
247+
248+ expect ( change ) . toHaveBeenCalledTimes ( 1 ) ;
249+ expect ( input ) . not . toHaveBeenCalled ( ) ;
250+ } ) ;
251+
252+ test ( "change event does not fire on mount" , async ( ) => {
253+ const { listen } = await render ( Radio , default_props ) ;
254+
255+ const change = listen ( "change" , { retrospective : true } ) ;
256+
257+ expect ( change ) . not . toHaveBeenCalled ( ) ;
103258 } ) ;
104259
105- test . skip ( "dispatches change and should not dispatch select/input on programmatic value update" , async ( ) => {
106- const { unmount, listen } = await render ( Radio , {
107- choices : choices ,
108- value : "cat" ,
109- label : "Radio"
260+ test ( "change deduplication: same value does not re-fire" , async ( ) => {
261+ const { listen, set_data } = await render ( Radio , {
262+ ...default_props ,
263+ value : "dog"
110264 } ) ;
111265
112- const select_mock = listen ( "select" as never ) ;
113- const input_mock = listen ( "input" as never ) ;
266+ const change = listen ( "change" ) ;
267+ await set_data ( { value : "cat" } ) ;
268+ await set_data ( { value : "cat" } ) ;
269+
270+ expect ( change ) . toHaveBeenCalledTimes ( 1 ) ;
271+ } ) ;
114272
115- unmount ( ) ;
116- await render ( Radio , {
117- choices : choices ,
118- value : "dog" ,
119- label : "Radio"
273+ test ( "custom_button_click emitted when custom button is clicked" , async ( ) => {
274+ const { listen, getByLabelText } = await render ( Radio , {
275+ ...default_props ,
276+ buttons : [ { value : "Shuffle" , id : 5 , icon : null } ]
120277 } ) ;
121- await tick ( ) ;
122278
123- expect ( select_mock . callCount ) . toBe ( 0 ) ;
124- expect ( input_mock . callCount ) . toBe ( 0 ) ;
279+ const custom = listen ( "custom_button_click" ) ;
280+ const btn = getByLabelText ( "Shuffle" ) ;
281+ await fireEvent . click ( btn ) ;
282+
283+ expect ( custom ) . toHaveBeenCalledTimes ( 1 ) ;
284+ expect ( custom ) . toHaveBeenCalledWith ( { id : 5 } ) ;
285+ } ) ;
286+ } ) ;
287+
288+ describe ( "get_data / set_data" , ( ) => {
289+ test ( "get_data returns the current value" , async ( ) => {
290+ const { get_data } = await render ( Radio , {
291+ ...default_props ,
292+ value : "turtle"
293+ } ) ;
294+
295+ const data = await get_data ( ) ;
296+ expect ( data . value ) . toBe ( "turtle" ) ;
297+ } ) ;
298+
299+ test ( "set_data updates the value" , async ( ) => {
300+ const { set_data, get_data } = await render ( Radio , default_props ) ;
301+
302+ await set_data ( { value : "dog" } ) ;
303+
304+ const data = await get_data ( ) ;
305+ expect ( data . value ) . toBe ( "dog" ) ;
306+ } ) ;
307+
308+ test ( "set_data to null clears the value" , async ( ) => {
309+ const { set_data, get_data } = await render ( Radio , default_props ) ;
310+
311+ await set_data ( { value : null } ) ;
312+
313+ const data = await get_data ( ) ;
314+ expect ( data . value ) . toBeNull ( ) ;
125315 } ) ;
126316} ) ;
0 commit comments