import { composeStories } from "@storybook/react" import { expect, userEvent, waitFor } from "storybook/test" import { test } from "vitest" import { expectNoAxeViolations } from "../expectNoAxeViolations.testHelpers.ts" import { mountStory } from "../mountStory.testHelpers.ts" import { expectAgentDrivable } from "../testing/index.ts" import * as stories from "./ActionTiles.stories.tsx" const { AllStates, AutoHues, BesideTheRadioTile, Playground, Interactive, Links, NamedHues, Neutral, Responsive, WithIcons, } = composeStories(stories) /** * The accent-edge layer out of a computed `box-shadow`. * * Reading the whole string does not work: Tailwind's shadow chain * always emits four EMPTY `rgba(0, 0, 0, 0) 0px 0px 0px 0px` layers * for the ring and shadow slots nothing filled, so a naive * "contains no transparent colour" assertion fails on a bar that is * painted perfectly. The bar is the `inset` layer, and it is the * only one. * * Returns `"none"` when there is no inset layer at all, which is * what `accent="none"` must produce. */ const getAccentEdgeLayer = (boxShadow: string) => boxShadow .split(/,(?![^(]*\))/) .map((layer) => layer.trim()) .find((layer) => layer.endsWith("inset")) ?? "none" test("it is a named group of pressable tiles", async () => { const { canvas, canvasElement } = await mountStory(Playground) const group = expectAgentDrivable(canvas, { name: "Queue type", role: "group", }) await expect( group.querySelectorAll("button"), ).toHaveLength(2) // Nothing here is selected, and nothing claims to be. The whole // reason this is not a `RadioGroup` in a costume. await expect( group.querySelector( "[aria-checked], [aria-pressed], [role='radio']", ), ).toBeNull() await expectNoAxeViolations(canvasElement) }) test("a tile's hint is part of its accessible name", async () => { const { canvas } = await mountStory(Playground) // The reason the hint is rendered inside the tile rather than // beside it: a hint a screen reader never reads is a hint half the // audience does not have. `getByRole` computes the name from // contents, so this query only passes if it is in there. expectAgentDrivable(canvas, { name: "Picks Choose titles yourself, then arrange them in priority and random lanes.", role: "button", }) }) test("a tile icon is decoration and stays out of the name", async () => { const { canvas, canvasElement } = await mountStory(WithIcons) // Named by its words alone. An icon that joined the name would // make this query fail, which is the whole reason it is // `aria-hidden`. expectAgentDrivable(canvas, { name: "New arrivals Everything that arrived since the last run.", role: "button", }) await expectNoAxeViolations(canvasElement) }) test("every tile is its own tab stop, and Enter and Space press it", async () => { const { canvas } = await mountStory(Interactive) const group = expectAgentDrivable(canvas, { name: "Queue type", role: "group", }) const tiles = Array.from( group.querySelectorAll("button"), ) // The opposite of the roving-tabindex rule, on purpose: a radio // group borrows one tab stop because exactly one option is true at // a time, and nothing here is true. Every tile must be reachable // by Tab. await expect( tiles.filter((one) => one.tabIndex === 0), ).toHaveLength(2) tiles[0]?.focus() await userEvent.tab() await waitFor(() => { expect(tiles[1]).toHaveFocus() }) // Enter and Space are the platform's, because these are real // `