import { useState } from "react"; import { RadioOption } from "../../src/components/ui/Radio"; // The `type="radio"` sibling of CheckboxOption.cy.tsx — same "selectable // card" markup, but exercised for what actually differs about a radio // input: the mandatory `name`/`value` pair and the mutually-exclusive // group behavior that's the whole reason to reach for radio over checkbox. describe("RadioOption", () => { it("renders its label content", () => { cy.mount( {}}> Sombre , ); cy.contains("label", "Sombre").should("be.visible"); }); it("sets the native input's name and value", () => { cy.mount( {}}> Sombre , ); cy.get("input[type=radio]") .should("have.attr", "name", "theme") .and("have.attr", "value", "dark"); }); it("reflects the checked prop on the native input, and the is-selected class", () => { cy.mount( {}}> Sombre , ); cy.get("input[type=radio]").should("not.be.checked"); cy.get("label").should("not.have.class", "is-selected"); cy.mount( {}}> Sombre , ); cy.get("input[type=radio]").should("be.checked"); cy.get("label").should("have.class", "is-selected"); }); it("calls onChange with its own value when clicked while unchecked", () => { const onChange = cy.stub().as("onChange"); cy.mount( Sombre , ); cy.get("input[type=radio]").click(); cy.get("@onChange").should("have.been.calledOnceWith", "dark"); }); it("does not fire onChange again when clicking a radio that's already checked", () => { // Native radio inputs only emit a `change` event when their checked // state actually flips — clicking an already-selected option in a // group is a no-op, unlike a checkbox which always toggles. const onChange = cy.stub().as("onChange"); cy.mount( Sombre , ); cy.get("input[type=radio]").click(); cy.get("@onChange").should("not.have.been.called"); }); it("toggles when the click lands on the label text, not just the input itself", () => { const onChange = cy.stub().as("onChange"); cy.mount( Sombre , ); cy.contains("label", "Sombre").click(); cy.get("@onChange").should("have.been.calledOnceWith", "dark"); }); it("merges the caller's className with the container layout, alongside is-selected", () => { cy.mount( {}} className="theme-select__option" > Sombre , ); cy.get("label").should("have.class", "theme-select__option").and("have.class", "is-selected"); }); it("marks the check-mark decoration as aria-hidden, so screen readers only announce the radio itself", () => { cy.mount( {}}> Sombre , ); cy.get("span.check-mark").should("have.attr", "aria-hidden", "true"); }); it("behaves as a mutually-exclusive group when several options share the same name", () => { // A stateful wrapper mirroring UserPreferencesPage's theme picker — the // one real caller — mounting 3 RadioOptions that share `name="theme"` // and one `value` of state between them. function ThemeGroup() { const [theme, setTheme] = useState<"system" | "light" | "dark">("system"); return ( <> Système Clair Sombre ); } cy.mount(); cy.contains("label", "Système").should("have.class", "is-selected"); cy.contains("label", "Clair").should("not.have.class", "is-selected"); cy.contains("label", "Sombre").should("not.have.class", "is-selected"); cy.contains("label", "Sombre").click(); cy.contains("label", "Sombre").should("have.class", "is-selected"); cy.contains("label", "Système").should("not.have.class", "is-selected"); cy.contains("label", "Clair").should("not.have.class", "is-selected"); // The native `name` grouping also keeps the browser's own radio // semantics honest — only one input in the group can be `:checked`. cy.get("input[type=radio]:checked").should("have.length", 1); }); });