`, which cannot take focus, so a tip that opens here can
// only have been wired to the `
`.
control.focus()
// The tip portals to the body now; the `aria-describedby` link still
// resolves across the boundary, asserted below.
await waitFor(() => {
expect(body.getByRole("tooltip")).toHaveTextContent(
"Where finished rips are moved.",
)
})
const [tipId, errorId] = (
control.getAttribute("aria-describedby") ?? ""
).split(" ")
// Outer first, which here is the tip: the merge order is
// structural — whoever wrapped whom — and the author of the nesting
// is the one who chose it.
await expect(
document.getElementById(tipId ?? ""),
).toHaveTextContent("Where finished rips are moved.")
await expect(
document.getElementById(errorId ?? ""),
).toHaveTextContent("Not writable.")
})
/**
* `aria-describedby` is the one slot prop that is a **list**, so the
* merge is not last-write-wins. A `Field` names its description and
* its error; a `Tooltip` names its tip. A plain spread keeps one and
* loses the other — the same silent drop by a shorter route.
*
* Outer first: the field's own explanation before the supplementary
* tip.
*/
test("a tip is described alongside the field's own text, not instead of it", async () => {
const { body, canvas } = await mountStory(Nested)
const control = expectAgentDrivable(canvas, {
name: "Rename pattern",
role: "textbox",
})
control.focus()
// `useFocus` is what opens it — the line mux-magic's hand-rolled
// tip is missing, and the reason this is testable without a
// pointer at all.
await waitFor(() => {
expect(body.getByRole("tooltip")).toHaveTextContent(
"A JavaScript regular expression.",
)
})
const [descriptionId, tipId] = (
control.getAttribute("aria-describedby") ?? ""
).split(" ")
await expect(
document.getElementById(descriptionId ?? ""),
).toHaveTextContent("Applied to every title.")
await expect(
document.getElementById(tipId ?? ""),
).toHaveTextContent("A JavaScript regular expression.")
})
/**
* Order inside `aria-describedby` is behaviour, not formatting. A
* screen reader reads the list in sequence, and "absolute paths only
* — that path is not writable" is a different sentence from its
* reverse. Nothing but the order of one array enforces it.
*/
test("description is described before error", async () => {
const { canvas } = await mountStory(AllStates)
const control = expectAgentDrivable(canvas, {
name: "Archive path",
role: "textbox",
})
const [descriptionId, errorId] = (
control.getAttribute("aria-describedby") ?? ""
).split(" ")
await expect(
document.getElementById(descriptionId ?? ""),
).toHaveTextContent("Absolute paths only.")
await expect(
document.getElementById(errorId ?? ""),
).toHaveTextContent("That path is not writable.")
})
/**
* `Field` cannot go here and the failure is quiet: an `id` names one
* element and a `
` points at one, so a label over three
* inputs names one of them and leaves the other two anonymous. Six of
* mux-magic's sixteen field components are in exactly that state.
*
* `` + `` is the platform's answer, and it is the
* one place in this library where the element is right — the content
* really is a form-control grouping.
*/
test("a group is named by its legend, and its controls keep their own names", async () => {
const { canvas, canvasElement } = await mountStory(Group)
const group = expectAgentDrivable(canvas, {
// "Rename pattern", not "Rename pattern *" — the asterisk is
// `aria-hidden`, so it is decoration and stays out of the name.
name: "Rename pattern",
role: "group",
})
// Every control inside is still individually findable. A group
// that swallows its members' names is the same defect one level up.
for (const name of ["Pattern", "Flags", "Sample"]) {
expectAgentDrivable(canvas, { name, role: "textbox" })
}
await expect(
document.getElementById(
group.getAttribute("aria-describedby") ?? "",
),
).toHaveTextContent("Applied to every title in the disc.")
await expectNoAxeViolations(canvasElement)
})
/**
* `aria-invalid` has no group form — it belongs on the control that
* is actually invalid, and a `FieldGroup` does not know which one
* that is. So the error is *described*, not *asserted*, and the
* alternative — cloning `aria-invalid` onto every child — would mark
* the valid ones invalid.
*/
test("a group describes its error rather than asserting invalidity", async () => {
const { canvas } = await mountStory(Group)
const group = expectAgentDrivable(canvas, {
name: "Chapter split",
role: "group",
})
await expect(group).not.toHaveAttribute("aria-invalid")
await expect(
document.getElementById(
group.getAttribute("aria-describedby") ?? "",
),
).toHaveTextContent(
"A pattern needs at least one capture group.",
)
})
/**
* The gap this closes, stated as the two things that were impossible.
*
* `FieldProps` and `FieldGroupProps` were closed types, so the only
* prop that reached an element was `className` — which is the escape
* hatch the "configured by props, not a borrowed class" rule exists to
* close. A component that accepts only `className` forces the very
* thing the rule forbids, and queuepilot paid for it twice: a
* `FieldGroup` that had to give up the `id` its screenshot suite
* drives by, and a box beside it that stayed a hand-rolled
* ` ` because it needed `hidden`.
*
* `Field` clones its child, so its props are the **control's**. The
* assertion is which element they landed on, because that is the half
* that could have gone silently wrong: routing them to the wrapper
* would have moved every consumer's `id` off the control the
* `` points at.
*/
test("a rest prop reaches the control, not the box around it", async () => {
const { canvas, canvasElement } = await mountStory(
ForwardsRestProps,
)
const control = expectAgentDrivable(canvas, {
name: "Scratch path",
role: "textbox",
})
await expect(control).toHaveAttribute(
"name",
"scratchPath",
)
await expect(control).toHaveAttribute(
"placeholder",
"/mnt/scratch",
)
// …and the `` did not take them. `Field`'s own `className` is
// the one prop that stays on the box, so the two destinations have
// to be distinguishable in the DOM rather than merely documented.
await expect(
canvasElement.querySelector("[name='scratchPath']"),
).toBe(control)
await expectNoAxeViolations(canvasElement)
})
/**
* `aria-describedby` is a **list**, and opening the type is what makes
* a caller able to write one at all. A plain spread would keep one
* side and drop the other — the same silent drop `mergeSlotProps`
* exists to prevent between two nested slots, arriving by a shorter
* route now that the outer writer can be the author.
*
* Order is behaviour, not formatting: what the caller passed is the
* outer declaration and reads first, then the field's own description,
* then its error.
*/
test("a caller's aria-describedby composes with the description and the error", async () => {
const { canvas } = await mountStory(ForwardsRestProps)
const control = expectAgentDrivable(canvas, {
name: "Scratch path",
role: "textbox",
})
const [policyId, descriptionId, errorId] = (
control.getAttribute("aria-describedby") ?? ""
).split(" ")
await expect(
document.getElementById(policyId ?? ""),
).toHaveTextContent("Cleared between runs.")
await expect(
document.getElementById(descriptionId ?? ""),
).toHaveTextContent("Absolute paths only.")
await expect(
document.getElementById(errorId ?? ""),
).toHaveTextContent("That path is not writable.")
// The error is still the only source of invalidity, and a caller's
// own description did not disturb it.
await expect(control).toHaveAttribute(
"aria-invalid",
"true",
)
})
/**
* The other reading of the same rule. `FieldGroup` renders its
* children as-is, so the only element it owns is the `
` and
* a prop has nowhere else to go — including `disabled`, which on this
* element is not decoration: the platform disables every control
* inside it.
*
* This is the case queuepilot's `#dyn-lineup` needed and could not
* have: an `id` on the group's own element.
*/
test("a rest prop reaches the fieldset, and the fieldset disables what is inside it", async () => {
const { canvas, canvasElement } = await mountStory(
ForwardsRestProps,
)
const group = expectAgentDrivable(canvas, {
name: "Playback",
role: "group",
})
await expect(group).toHaveAttribute(
"id",
"playback-lineup",
)
await expect(group.tagName).toBe("FIELDSET")
// The `` still names the group, and the group's own
// description still reaches it, so the rest props did not overwrite
// the wiring they were spread beside.
await expect(
document.getElementById(
group.getAttribute("aria-describedby") ?? "",
),
).toHaveTextContent(
"The whole group is turned off by the element itself.",
)
await expect(
canvasElement.querySelector("input[type='number']"),
).toBeDisabled()
await expectNoAxeViolations(canvasElement)
})