-
Notifications
You must be signed in to change notification settings - Fork 3.4k
add radio unit tests #13212
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Open
hannahblair
wants to merge
7
commits into
main
Choose a base branch
from
radio-tests
base: main
Could not load branches
Branch not found: {{ refName }}
Loading
Could not load tags
Nothing to show
Loading
Are you sure you want to change the base?
Some commits from the old base branch may be removed from the timeline,
and old review comments may become outdated.
+264
−74
Open
add radio unit tests #13212
Changes from all commits
Commits
Show all changes
7 commits
Select commit
Hold shift + click to select a range
3ca42e7
add radio tests
hannahblair e9d18c3
undefined test
hannahblair 8bd33ec
add two more edge cases
hannahblair 6cbd195
truthy -> visible
hannahblair 49585e8
set retrospective
hannahblair 9fc1f1d
check for input
hannahblair c35e6af
Merge branch 'main' into radio-tests
hannahblair File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -1,96 +1,231 @@ | ||
| import { test, describe, assert, afterEach, expect } from "vitest"; | ||
|
|
||
| import { cleanup, render } from "@self/tootils/render"; | ||
| import { tick } from "svelte"; | ||
| import { test, describe, expect, afterEach } from "vitest"; | ||
| import { cleanup, render, fireEvent, waitFor } from "@self/tootils/render"; | ||
| import { run_shared_prop_tests } from "@self/tootils/shared-prop-tests"; | ||
| import event from "@testing-library/user-event"; | ||
|
|
||
| import Radio from "./Index.svelte"; | ||
|
|
||
| describe("Radio", () => { | ||
| afterEach(() => cleanup()); | ||
| const choices = [ | ||
| ["dog", "dog"], | ||
| ["cat", "cat"], | ||
| ["turtle", "turtle"] | ||
| ] as [string, string][]; | ||
| afterEach(cleanup); | ||
|
|
||
| const choices: [string, string][] = [ | ||
| ["dog", "dog"], | ||
| ["cat", "cat"], | ||
| ["turtle", "turtle"] | ||
| ]; | ||
|
|
||
| const default_props = { | ||
| show_label: true, | ||
| choices, | ||
| value: "cat", | ||
| label: "Radio", | ||
| interactive: true | ||
| }; | ||
|
|
||
| run_shared_prop_tests({ | ||
| component: Radio, | ||
| name: "Radio", | ||
| base_props: { | ||
| choices, | ||
| value: "dog", | ||
| interactive: true | ||
| }, | ||
| has_validation_error: false | ||
| }); | ||
|
|
||
| describe("Props: value", () => { | ||
| test("renders provided value as checked", async () => { | ||
| const { getAllByRole } = await render(Radio, default_props); | ||
|
|
||
| const radios = getAllByRole("radio") as HTMLInputElement[]; | ||
| expect(radios).toHaveLength(3); | ||
| expect(radios[0]).not.toBeChecked(); | ||
| expect(radios[1]).toBeChecked(); | ||
| expect(radios[2]).not.toBeChecked(); | ||
| }); | ||
|
|
||
| test("null value renders no radio checked", async () => { | ||
| const { getAllByRole } = await render(Radio, { | ||
| ...default_props, | ||
| value: null | ||
| }); | ||
|
|
||
| const radios = getAllByRole("radio") as HTMLInputElement[]; | ||
| radios.forEach((radio) => { | ||
| expect(radio).not.toBeChecked(); | ||
| }); | ||
| }); | ||
|
|
||
| test("undefined value renders no radio checked", async () => { | ||
| const { getAllByRole } = await render(Radio, { | ||
| ...default_props, | ||
| value: undefined | ||
| }); | ||
|
|
||
| const radios = getAllByRole("radio") as HTMLInputElement[]; | ||
| radios.forEach((radio) => { | ||
| expect(radio).not.toBeChecked(); | ||
| }); | ||
| }); | ||
|
|
||
| test("value not in choices renders no radio checked", async () => { | ||
| const { getAllByRole } = await render(Radio, { | ||
| ...default_props, | ||
| value: "fish" | ||
| }); | ||
|
|
||
| const radios = getAllByRole("radio") as HTMLInputElement[]; | ||
| radios.forEach((radio) => { | ||
| expect(radio).not.toBeChecked(); | ||
| }); | ||
| }); | ||
|
|
||
| test("numeric values are supported in choices", async () => { | ||
| const numeric_choices: [string, number][] = [ | ||
| ["one", 1], | ||
| ["two", 2], | ||
| ["three", 3] | ||
| ]; | ||
|
|
||
| test("renders provided value", async () => { | ||
| const { getAllByRole, getByTestId } = await render(Radio, { | ||
| choices: choices, | ||
| value: "cat", | ||
| label: "Radio" | ||
| const { getAllByRole } = await render(Radio, { | ||
| ...default_props, | ||
| choices: numeric_choices, | ||
| value: 2 | ||
| }); | ||
|
|
||
| const cat_radio = getAllByRole("radio")[1]; | ||
| const radios = getAllByRole("radio") as HTMLInputElement[]; | ||
| expect(radios[1]).toBeChecked(); | ||
| }); | ||
| }); | ||
|
|
||
| expect(cat_radio).toBeChecked(); | ||
| describe("Props: choices", () => { | ||
| test("renders display values as labels", async () => { | ||
| const { getByText } = await render(Radio, default_props); | ||
|
|
||
| const radioButtons: HTMLOptionElement[] = getAllByRole( | ||
| "radio" | ||
| ) as HTMLOptionElement[]; | ||
| assert.equal(radioButtons.length, 3); | ||
| expect(getByText("dog")).toBeVisible(); | ||
| expect(getByText("cat")).toBeVisible(); | ||
| expect(getByText("turtle")).toBeVisible(); | ||
| }); | ||
|
|
||
| radioButtons.forEach((radioButton: HTMLOptionElement, index) => { | ||
| assert.equal(radioButton.value === choices[index][1], true); | ||
| test("display value and internal value can differ", async () => { | ||
| const custom_choices: [string, string][] = [ | ||
| ["dog label", "dog_val"], | ||
| ["cat label", "cat_val"] | ||
| ]; | ||
|
|
||
| const { getByText, getAllByRole, get_data } = await render(Radio, { | ||
| ...default_props, | ||
| choices: custom_choices, | ||
| value: "cat_val" | ||
| }); | ||
|
|
||
| expect(getByText("dog label")).toBeVisible(); | ||
| expect(getByText("cat label")).toBeVisible(); | ||
|
|
||
| const radios = getAllByRole("radio") as HTMLInputElement[]; | ||
| expect(radios[1]).toBeChecked(); | ||
|
|
||
| const data = await get_data(); | ||
| expect(data.value).toBe("cat_val"); | ||
| }); | ||
|
|
||
| test("should update the value when a radio is clicked", async () => { | ||
| const { getByDisplayValue, getAllByRole } = await render(Radio, { | ||
| choices: choices, | ||
| value: "cat", | ||
| label: "Radio", | ||
| interactive: true | ||
| test("empty choices renders no radios", async () => { | ||
| const { queryAllByRole } = await render(Radio, { | ||
| ...default_props, | ||
| choices: [], | ||
| value: null | ||
| }); | ||
|
|
||
| const dog_radio = getAllByRole("radio")[0]; | ||
| expect(queryAllByRole("radio")).toHaveLength(0); | ||
| }); | ||
| }); | ||
|
|
||
| await event.click(dog_radio); | ||
| describe("Props: interactive", () => { | ||
| test("interactive=false disables all radios", async () => { | ||
| const { getAllByRole } = await render(Radio, { | ||
| ...default_props, | ||
| interactive: false | ||
| }); | ||
|
|
||
| const radios = getAllByRole("radio") as HTMLInputElement[]; | ||
| radios.forEach((radio) => { | ||
| expect(radio).toBeDisabled(); | ||
| }); | ||
| }); | ||
| }); | ||
|
|
||
| expect(dog_radio).toBeChecked(); | ||
| describe("Props: info", () => { | ||
| test("info renders descriptive text", async () => { | ||
| const { getByText } = await render(Radio, { | ||
| ...default_props, | ||
| info: "Pick your favorite animal" | ||
| }); | ||
|
|
||
| const cat_radio = getAllByRole("radio")[1]; | ||
| expect(getByText("Pick your favorite animal")).toBeVisible(); | ||
| }); | ||
|
|
||
| expect(cat_radio).not.toBeChecked(); | ||
| test("no info does not render info text", async () => { | ||
| const { queryByText } = await render(Radio, { | ||
| ...default_props, | ||
| info: undefined | ||
| }); | ||
|
|
||
| await event.click(getByDisplayValue("turtle")); | ||
| expect(queryByText("Pick your favorite animal")).toBeNull(); | ||
| }); | ||
| }); | ||
|
|
||
| await event.click(cat_radio); | ||
| describe("Props: buttons", () => { | ||
| test("custom buttons are rendered when provided", async () => { | ||
| const { getByLabelText } = await render(Radio, { | ||
| ...default_props, | ||
| buttons: [{ value: "Shuffle", id: 1, icon: null }] | ||
| }); | ||
|
|
||
| expect(cat_radio).toBeChecked(); | ||
| getByLabelText("Shuffle"); | ||
| }); | ||
|
|
||
| test.skip("should dispatch the select event when clicks", async () => { | ||
| const { listen, getAllByTestId } = await render(Radio, { | ||
| choices: choices, | ||
| value: "cat", | ||
| label: "Radio", | ||
| interactive: true | ||
| test("no buttons rendered when null", async () => { | ||
| const { queryByRole } = await render(Radio, { | ||
| ...default_props, | ||
| buttons: null | ||
| }); | ||
|
|
||
| const mock = listen("select"); | ||
| await event.click(getAllByTestId("dog-radio-label")[0]); | ||
| expect(mock.callCount).toBe(1); | ||
| expect(mock.calls[0][0].detail.data.value).toEqual("dog"); | ||
| expect(queryByRole("button")).toBeNull(); | ||
| }); | ||
| }); | ||
|
|
||
| describe("Interactive behavior", () => { | ||
| test("clicking through options selects and deselects correctly", async () => { | ||
| const { getAllByRole } = await render(Radio, default_props); | ||
|
|
||
| const radios = getAllByRole("radio") as HTMLInputElement[]; | ||
| expect(radios[1]).toBeChecked(); | ||
|
|
||
| await event.click(radios[0]); | ||
| expect(radios[0]).toBeChecked(); | ||
| expect(radios[1]).not.toBeChecked(); | ||
|
|
||
| test("when multiple radios are on the screen, they should not conflict", async () => { | ||
| await event.click(radios[2]); | ||
| expect(radios[2]).toBeChecked(); | ||
| expect(radios[0]).not.toBeChecked(); | ||
|
|
||
| await event.click(radios[1]); | ||
| expect(radios[1]).toBeChecked(); | ||
| expect(radios[2]).not.toBeChecked(); | ||
| }); | ||
|
|
||
| test("multiple radio instances on the same page do not conflict", async () => { | ||
| const { container } = await render(Radio, { | ||
| choices: choices, | ||
| value: "cat", | ||
| label: "Radio", | ||
| interactive: true | ||
| ...default_props, | ||
| value: "cat" | ||
| }); | ||
|
|
||
| const { getAllByLabelText } = await render( | ||
| Radio, | ||
| { | ||
| choices: choices, | ||
| value: "dog", | ||
| label: "Radio", | ||
| interactive: true | ||
| ...default_props, | ||
| value: "dog" | ||
| }, | ||
| container | ||
| { container: container as HTMLElement } | ||
| ); | ||
|
|
||
| const items = getAllByLabelText("dog") as HTMLInputElement[]; | ||
|
|
@@ -99,28 +234,83 @@ describe("Radio", () => { | |
| await event.click(items[0]); | ||
|
|
||
| expect([items[0].checked, items[1].checked]).toEqual([true, true]); | ||
| cleanup(); | ||
| }); | ||
| }); | ||
|
|
||
| describe("Events", () => { | ||
|
Collaborator
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I would test the select and input events |
||
| test("change emitted when value changes via set_data", async () => { | ||
| const { listen, set_data } = await render(Radio, default_props); | ||
|
|
||
| const change = listen("change"); | ||
| const input = listen("input"); | ||
| await set_data({ value: "dog" }); | ||
hannahblair marked this conversation as resolved.
Show resolved
Hide resolved
|
||
|
|
||
| expect(change).toHaveBeenCalledTimes(1); | ||
| expect(input).not.toHaveBeenCalled(); | ||
| }); | ||
|
|
||
| test("change event does not fire on mount", async () => { | ||
| const { listen } = await render(Radio, default_props); | ||
|
|
||
| const change = listen("change", { retrospective: true }); | ||
|
|
||
| expect(change).not.toHaveBeenCalled(); | ||
| }); | ||
|
|
||
| test.skip("dispatches change and should not dispatch select/input on programmatic value update", async () => { | ||
| const { unmount, listen } = await render(Radio, { | ||
| choices: choices, | ||
| value: "cat", | ||
| label: "Radio" | ||
| test("change deduplication: same value does not re-fire", async () => { | ||
| const { listen, set_data } = await render(Radio, { | ||
| ...default_props, | ||
| value: "dog" | ||
| }); | ||
|
|
||
| const select_mock = listen("select" as never); | ||
| const input_mock = listen("input" as never); | ||
| const change = listen("change"); | ||
| await set_data({ value: "cat" }); | ||
| await set_data({ value: "cat" }); | ||
|
|
||
| expect(change).toHaveBeenCalledTimes(1); | ||
| }); | ||
|
|
||
| unmount(); | ||
| await render(Radio, { | ||
| choices: choices, | ||
| value: "dog", | ||
| label: "Radio" | ||
| test("custom_button_click emitted when custom button is clicked", async () => { | ||
| const { listen, getByLabelText } = await render(Radio, { | ||
| ...default_props, | ||
| buttons: [{ value: "Shuffle", id: 5, icon: null }] | ||
| }); | ||
| await tick(); | ||
|
|
||
| expect(select_mock.callCount).toBe(0); | ||
| expect(input_mock.callCount).toBe(0); | ||
| const custom = listen("custom_button_click"); | ||
| const btn = getByLabelText("Shuffle"); | ||
| await fireEvent.click(btn); | ||
|
|
||
| expect(custom).toHaveBeenCalledTimes(1); | ||
| expect(custom).toHaveBeenCalledWith({ id: 5 }); | ||
| }); | ||
| }); | ||
|
|
||
| describe("get_data / set_data", () => { | ||
| test("get_data returns the current value", async () => { | ||
| const { get_data } = await render(Radio, { | ||
| ...default_props, | ||
| value: "turtle" | ||
| }); | ||
|
|
||
| const data = await get_data(); | ||
| expect(data.value).toBe("turtle"); | ||
| }); | ||
|
|
||
| test("set_data updates the value", async () => { | ||
| const { set_data, get_data } = await render(Radio, default_props); | ||
|
|
||
| await set_data({ value: "dog" }); | ||
|
|
||
| const data = await get_data(); | ||
| expect(data.value).toBe("dog"); | ||
| }); | ||
|
|
||
| test("set_data to null clears the value", async () => { | ||
| const { set_data, get_data } = await render(Radio, default_props); | ||
|
|
||
| await set_data({ value: null }); | ||
|
|
||
| const data = await get_data(); | ||
| expect(data.value).toBeNull(); | ||
| }); | ||
| }); | ||
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
need this for tests to pass despite it being in run_shared_prop_tests