Skip to the code
cf.

Dependent selects

MediumForms

Build App({ places }): a tea shop asks which tea region to ship a free sample to. places maps each country to its cities, both in the order to show them. The starter gives places a default so the Preview has something to show; the tests pass their own.

<App places={{ India: ["Darjeeling", "Munnar"], Japan: ["Uji", "Shizuoka"] }} />

There are two selects, labelled Country and City. Country's first option is Choose a country, with the value "", and it's chosen at first. Then comes one option per country, whose text and value are the country's name.

City's first option is Choose a city, with the value "". After it comes one option per city of the chosen country, and nothing else. City is disabled until a country is chosen, and disabled again if Country goes back to Choose a country.

Changing the country always puts City back on Choose a city.

Once both are chosen, a line reads Shipping to Uji, Japan. (city, then country). Until then, that line isn't on the page.

What the tests check

  1. Country starts on its placeholder and lists every country in order
    const places = { India: ["Darjeeling", "Munnar"], Japan: ["Uji", "Shizuoka", "Kagoshima"], Kenya: ["Kericho"] };
    render(<App places={places} />);
    const country = screen.getByRole("combobox", { name: "Country" });
    expect(country).toHaveValue("");
    expect(within(country).getAllByRole("option").map((option) => option.textContent)).toEqual(["Choose a country", "India", "Japan", "Kenya"]);
    
  2. City is disabled, with only its placeholder, until a country is chosen
    const places = { India: ["Darjeeling", "Munnar"], Japan: ["Uji", "Shizuoka", "Kagoshima"], Kenya: ["Kericho"] };
    render(<App places={places} />);
    const city = screen.getByRole("combobox", { name: "City" });
    expect(city).toBeDisabled();
    expect(within(city).getAllByRole("option").map((option) => option.textContent)).toEqual(["Choose a city"]);
    
  3. choosing a country enables City and lists only that country's cities
    const places = { India: ["Darjeeling", "Munnar"], Japan: ["Uji", "Shizuoka", "Kagoshima"], Kenya: ["Kericho"] };
    render(<App places={places} />);
    await user.selectOptions(screen.getByRole("combobox", { name: "Country" }), "Japan");
    const city = screen.getByRole("combobox", { name: "City" });
    expect(city).toBeEnabled();
    expect(city).toHaveValue("");
    expect(within(city).getAllByRole("option").map((option) => option.textContent)).toEqual(["Choose a city", "Uji", "Shizuoka", "Kagoshima"]);
    
  4. says nothing about shipping until a city is chosen too
    const places = { India: ["Darjeeling", "Munnar"], Japan: ["Uji", "Shizuoka", "Kagoshima"], Kenya: ["Kericho"] };
    render(<App places={places} />);
    expect(screen.queryByText(/Shipping to/)).not.toBeInTheDocument();
    await user.selectOptions(screen.getByRole("combobox", { name: "Country" }), "Kenya");
    expect(screen.queryByText(/Shipping to/)).not.toBeInTheDocument();
    
  5. choosing a city shows where the sample ships
    const places = { India: ["Darjeeling", "Munnar"], Japan: ["Uji", "Shizuoka", "Kagoshima"], Kenya: ["Kericho"] };
    render(<App places={places} />);
    await user.selectOptions(screen.getByRole("combobox", { name: "Country" }), "Japan");
    await user.selectOptions(screen.getByRole("combobox", { name: "City" }), "Shizuoka");
    expect(screen.getByRole("combobox", { name: "City" })).toHaveValue("Shizuoka");
    expect(screen.getByText("Shipping to Shizuoka, Japan.")).toBeInTheDocument();
    
  6. changing the country puts City back on its placeholder
    const places = { India: ["Darjeeling", "Munnar"], Japan: ["Uji", "Shizuoka", "Kagoshima"], Kenya: ["Kericho"] };
    render(<App places={places} />);
    const country = screen.getByRole("combobox", { name: "Country" });
    await user.selectOptions(country, "Japan");
    await user.selectOptions(screen.getByRole("combobox", { name: "City" }), "Uji");
    await user.selectOptions(country, "India");
    const city = screen.getByRole("combobox", { name: "City" });
    expect(city).toHaveValue("");
    expect(within(city).getAllByRole("option").map((option) => option.textContent)).toEqual(["Choose a city", "Darjeeling", "Munnar"]);
    expect(screen.queryByText(/Shipping to/)).not.toBeInTheDocument();
    
  7. going back to Choose a country disables City again
    const places = { India: ["Darjeeling", "Munnar"], Japan: ["Uji", "Shizuoka", "Kagoshima"], Kenya: ["Kericho"] };
    render(<App places={places} />);
    const country = screen.getByRole("combobox", { name: "Country" });
    await user.selectOptions(country, "India");
    await user.selectOptions(screen.getByRole("combobox", { name: "City" }), "Munnar");
    await user.selectOptions(country, "");
    const city = screen.getByRole("combobox", { name: "City" });
    expect(city).toBeDisabled();
    expect(city).toHaveValue("");
    expect(screen.queryByText(/Shipping to/)).not.toBeInTheDocument();
    

Learn more

App.jsxSaved in this browser

Run tests runs every test on your code, here in your browser. The problem is solved when they all pass.

Ctrl + Enter runs the tests (⌘ + Enter on a Mac).