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 "./SortableTableHeader.stories.tsx"
const { AllVariants, Playground, Interactive } =
composeStories(stories)
/**
* `aria-sort` exists **nowhere in the fleet** — not in mux-magic,
* not in rip-deck, not in castkit. The one sortable table renders
* the state as `{sortDirection === "asc" ? "▲" : "▼"}`, which a
* screen reader announces as "black up-pointing triangle" if the
* font has it at all, and this sandbox's headless Chromium does not.
*/
test("the cell announces the sort, not the glyph", async () => {
const { canvas, canvasElement } =
await mountStory(Playground)
const title = expectAgentDrivable(canvas, {
name: "Title",
role: "columnheader",
})
await expect(title).toHaveAttribute(
"aria-sort",
"ascending",
)
await expectNoAxeViolations(canvasElement)
})
/**
* `none`, not an omitted attribute. Absent means "this column is not
* sortable"; `none` means "sortable, not currently sorted" — and a
* table mixing the two tells a screen-reader user the other columns
* cannot be sorted at all.
*/
test("an unsorted sortable column says `none`", async () => {
const { canvas, canvasElement } =
await mountStory(AllVariants)
await expect(
expectAgentDrivable(canvas, {
name: "Size",
role: "columnheader",
}),
).toHaveAttribute("aria-sort", "none")
await expect(
expectAgentDrivable(canvas, {
name: "Year",
role: "columnheader",
}),
).toHaveAttribute("aria-sort", "descending")
await expectNoAxeViolations(canvasElement)
})
/**
* The cell announces and the button acts. Putting the role on the
* button instead is the common mistake, and it breaks the grid
* semantics — a `
` that is a button is no longer a column header
* to anything reading the table's structure.
*/
test("the header is a columnheader containing a button", async () => {
const { canvas } = await mountStory(Playground)
const header = expectAgentDrivable(canvas, {
name: "Title",
role: "columnheader",
})
await expect(header.tagName).toBe("TH")
await expect(header).toHaveAttribute("scope", "col")
const button = expectAgentDrivable(canvas, {
name: "Title",
role: "button",
})
await expect(header).toContainElement(button)
})
test("clicking an unsorted column asks for ascending", async () => {
const { canvas } = await mountStory(Interactive)
await userEvent.click(
expectAgentDrivable(canvas, {
name: "Year",
role: "button",
}),
)
await waitFor(() => {
expect(
expectAgentDrivable(canvas, {
name: "Year",
role: "columnheader",
}),
).toHaveAttribute("aria-sort", "ascending")
})
// Exactly one column ever holds a direction — the invariant a
// hand-rolled `▲`/`▼` cannot express at all.
await expect(
expectAgentDrivable(canvas, {
name: "Title",
role: "columnheader",
}),
).toHaveAttribute("aria-sort", "none")
})
test("clicking the sorted column reverses it", async () => {
const { canvas } = await mountStory(Interactive)
const title = expectAgentDrivable(canvas, {
name: "Title",
role: "button",
})
await userEvent.click(title)
await waitFor(() => {
expect(
expectAgentDrivable(canvas, {
name: "Title",
role: "columnheader",
}),
).toHaveAttribute("aria-sort", "descending")
})
await userEvent.click(title)
await waitFor(() => {
expect(
expectAgentDrivable(canvas, {
name: "Title",
role: "columnheader",
}),
).toHaveAttribute("aria-sort", "ascending")
})
})
/**
* The name stays the column's name. Appending the state —
* "Title, sorted ascending" — duplicates what `aria-sort` already
* says *and* breaks `getByRole("columnheader", { name: "Title" })`
* the moment somebody sorts it.
*/
test("sorting does not rename the column", async () => {
const { canvas } = await mountStory(Interactive)
await userEvent.click(
expectAgentDrivable(canvas, {
name: "Title",
role: "button",
}),
)
await waitFor(() => {
expect(
expectAgentDrivable(canvas, {
name: "Title",
role: "columnheader",
}),
).toHaveAttribute("aria-sort", "descending")
})
// Still exactly one match, and still under the same name.
expectAgentDrivable(canvas, {
name: "Title",
role: "columnheader",
})
})
|