Skip to content

Commit 11a9985

Browse files
authored
add radio unit tests (#13212)
* add radio tests * undefined test * add two more edge cases * truthy -> visible * set retrospective * check for input
1 parent 457f130 commit 11a9985

File tree

1 file changed

+264
-74
lines changed

1 file changed

+264
-74
lines changed

js/radio/Radio.test.ts

Lines changed: 264 additions & 74 deletions
Original file line numberDiff line numberDiff line change
@@ -1,96 +1,231 @@
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";
54
import event from "@testing-library/user-event";
65

76
import 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

Comments
 (0)