Move LinkChips and add unit + a11y tests for chips components
This commit is contained in:
@@ -0,0 +1,105 @@
|
||||
import { describe, expect, it, afterEach } from "vitest"
|
||||
import { render, screen, cleanup } from "@testing-library/react"
|
||||
import userEvent from "@testing-library/user-event"
|
||||
|
||||
import { LinkChips } from "./LinkChips"
|
||||
import type { LinkChipsProps } from "./types"
|
||||
|
||||
afterEach(() => {
|
||||
cleanup()
|
||||
})
|
||||
|
||||
const defaultChips: LinkChipsProps["chips"] = [
|
||||
{ title: "Hotels in Stockholm", url: "/hotels/stockholm" },
|
||||
{ title: "Hotels in Copenhagen", url: "/hotels/copenhagen" },
|
||||
{ title: "Hotels in Oslo", url: "/hotels/oslo" },
|
||||
]
|
||||
|
||||
describe("LinkChips accessibility", () => {
|
||||
describe("semantic HTML", () => {
|
||||
it("uses proper link elements for navigation", () => {
|
||||
render(<LinkChips chips={defaultChips} />)
|
||||
const links = screen.getAllByRole("link")
|
||||
expect(links.length).toBe(3)
|
||||
links.forEach((link) => {
|
||||
expect(link.tagName).toBe("A")
|
||||
})
|
||||
})
|
||||
|
||||
it("has accessible link text", () => {
|
||||
render(<LinkChips chips={defaultChips} />)
|
||||
expect(
|
||||
screen.getByRole("link", { name: "Hotels in Stockholm" })
|
||||
).toBeTruthy()
|
||||
expect(
|
||||
screen.getByRole("link", { name: "Hotels in Copenhagen" })
|
||||
).toBeTruthy()
|
||||
expect(screen.getByRole("link", { name: "Hotels in Oslo" })).toBeTruthy()
|
||||
})
|
||||
|
||||
it("links have descriptive href attributes", () => {
|
||||
render(<LinkChips chips={defaultChips} />)
|
||||
const stockholmLink = screen.getByRole("link", {
|
||||
name: "Hotels in Stockholm",
|
||||
})
|
||||
expect(stockholmLink.getAttribute("href")).toBe("/hotels/stockholm")
|
||||
})
|
||||
})
|
||||
|
||||
describe("keyboard navigation", () => {
|
||||
it("all links are keyboard accessible", async () => {
|
||||
const user = userEvent.setup()
|
||||
render(<LinkChips chips={defaultChips} />)
|
||||
|
||||
const links = screen.getAllByRole("link")
|
||||
expect(links.length).toBeGreaterThan(0)
|
||||
|
||||
// Tab through all links
|
||||
for (const link of links) {
|
||||
await user.tab()
|
||||
expect(document.activeElement).toBe(link)
|
||||
}
|
||||
})
|
||||
|
||||
it("maintains logical focus order", async () => {
|
||||
const user = userEvent.setup()
|
||||
render(<LinkChips chips={defaultChips} />)
|
||||
|
||||
const firstLink = screen.getByRole("link", {
|
||||
name: "Hotels in Stockholm",
|
||||
})
|
||||
const secondLink = screen.getByRole("link", {
|
||||
name: "Hotels in Copenhagen",
|
||||
})
|
||||
const thirdLink = screen.getByRole("link", { name: "Hotels in Oslo" })
|
||||
|
||||
await user.tab()
|
||||
expect(document.activeElement).toBe(firstLink)
|
||||
|
||||
await user.tab()
|
||||
expect(document.activeElement).toBe(secondLink)
|
||||
|
||||
await user.tab()
|
||||
expect(document.activeElement).toBe(thirdLink)
|
||||
})
|
||||
})
|
||||
|
||||
describe("screen reader support", () => {
|
||||
it("links have accessible names", () => {
|
||||
render(<LinkChips chips={defaultChips} />)
|
||||
const links = screen.getAllByRole("link")
|
||||
links.forEach((link) => {
|
||||
// Check that link has text content (accessible name)
|
||||
expect(link.textContent?.trim().length).toBeGreaterThan(0)
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
describe("empty state", () => {
|
||||
it("does not render anything when chips array is empty", () => {
|
||||
const { container } = render(<LinkChips chips={[]} />)
|
||||
expect(container.firstChild).toBeNull()
|
||||
expect(screen.queryAllByRole("link")).toHaveLength(0)
|
||||
})
|
||||
})
|
||||
})
|
||||
@@ -0,0 +1,43 @@
|
||||
import type { Meta, StoryObj } from "@storybook/nextjs-vite"
|
||||
|
||||
import { LinkChips } from "./LinkChips"
|
||||
|
||||
const meta: Meta<typeof LinkChips> = {
|
||||
title: "Product Components/LinkChips",
|
||||
component: LinkChips,
|
||||
}
|
||||
|
||||
export default meta
|
||||
|
||||
type Story = StoryObj<typeof LinkChips>
|
||||
|
||||
export const Default: Story = {
|
||||
args: {
|
||||
chips: [
|
||||
{ title: "Hotels in Stockholm", url: "/hotels/stockholm" },
|
||||
{ title: "Hotels in Copenhagen", url: "/hotels/copenhagen" },
|
||||
{ title: "Hotels in Oslo", url: "/hotels/oslo" },
|
||||
],
|
||||
},
|
||||
}
|
||||
|
||||
export const SingleChip: Story = {
|
||||
args: {
|
||||
chips: [{ title: "View all hotels", url: "/hotels" }],
|
||||
},
|
||||
}
|
||||
|
||||
export const ManyChips: Story = {
|
||||
args: {
|
||||
chips: [
|
||||
{ title: "Stockholm", url: "/hotels/stockholm" },
|
||||
{ title: "Copenhagen", url: "/hotels/copenhagen" },
|
||||
{ title: "Oslo", url: "/hotels/oslo" },
|
||||
{ title: "Helsinki", url: "/hotels/helsinki" },
|
||||
{ title: "Gothenburg", url: "/hotels/gothenburg" },
|
||||
{ title: "Malmö", url: "/hotels/malmo" },
|
||||
{ title: "Bergen", url: "/hotels/bergen" },
|
||||
{ title: "Tampere", url: "/hotels/tampere" },
|
||||
],
|
||||
},
|
||||
}
|
||||
@@ -0,0 +1,92 @@
|
||||
import { describe, expect, it, afterEach } from "vitest"
|
||||
import { render, screen, cleanup } from "@testing-library/react"
|
||||
import userEvent from "@testing-library/user-event"
|
||||
|
||||
import { LinkChips } from "./LinkChips"
|
||||
import type { LinkChipsProps } from "./types"
|
||||
|
||||
afterEach(() => {
|
||||
cleanup()
|
||||
})
|
||||
|
||||
const defaultChips: LinkChipsProps["chips"] = [
|
||||
{ title: "Hotels in Stockholm", url: "/hotels/stockholm" },
|
||||
{ title: "Hotels in Copenhagen", url: "/hotels/copenhagen" },
|
||||
{ title: "Hotels in Oslo", url: "/hotels/oslo" },
|
||||
]
|
||||
|
||||
describe("LinkChips", () => {
|
||||
describe("rendering", () => {
|
||||
it("renders all chip links", () => {
|
||||
render(<LinkChips chips={defaultChips} />)
|
||||
expect(
|
||||
screen.getByRole("link", { name: "Hotels in Stockholm" })
|
||||
).toBeTruthy()
|
||||
expect(
|
||||
screen.getByRole("link", { name: "Hotels in Copenhagen" })
|
||||
).toBeTruthy()
|
||||
expect(screen.getByRole("link", { name: "Hotels in Oslo" })).toBeTruthy()
|
||||
})
|
||||
|
||||
it("renders chip links with correct href attributes", () => {
|
||||
render(<LinkChips chips={defaultChips} />)
|
||||
expect(
|
||||
screen.getByRole("link", { name: "Hotels in Stockholm" }).getAttribute("href")
|
||||
).toBe("/hotels/stockholm")
|
||||
expect(
|
||||
screen.getByRole("link", { name: "Hotels in Copenhagen" }).getAttribute("href")
|
||||
).toBe("/hotels/copenhagen")
|
||||
expect(
|
||||
screen.getByRole("link", { name: "Hotels in Oslo" }).getAttribute("href")
|
||||
).toBe("/hotels/oslo")
|
||||
})
|
||||
|
||||
it("returns null when chips array is empty", () => {
|
||||
const { container } = render(<LinkChips chips={[]} />)
|
||||
expect(container.firstChild).toBeNull()
|
||||
})
|
||||
|
||||
it("handles single chip", () => {
|
||||
const singleChip = [{ title: "View all hotels", url: "/hotels" }]
|
||||
render(<LinkChips chips={singleChip} />)
|
||||
expect(screen.getByRole("link", { name: "View all hotels" })).toBeTruthy()
|
||||
expect(
|
||||
screen.getByRole("link", { name: "View all hotels" }).getAttribute("href")
|
||||
).toBe("/hotels")
|
||||
})
|
||||
})
|
||||
|
||||
describe("keyboard navigation", () => {
|
||||
it("allows keyboard navigation between links", async () => {
|
||||
const user = userEvent.setup()
|
||||
render(<LinkChips chips={defaultChips} />)
|
||||
|
||||
const firstLink = screen.getByRole("link", {
|
||||
name: "Hotels in Stockholm",
|
||||
})
|
||||
const secondLink = screen.getByRole("link", {
|
||||
name: "Hotels in Copenhagen",
|
||||
})
|
||||
|
||||
await user.tab()
|
||||
expect(document.activeElement).toBe(firstLink)
|
||||
|
||||
await user.tab()
|
||||
expect(document.activeElement).toBe(secondLink)
|
||||
})
|
||||
})
|
||||
|
||||
describe("edge cases", () => {
|
||||
it("handles chips with duplicate titles but different URLs", () => {
|
||||
const duplicateTitles: LinkChipsProps["chips"] = [
|
||||
{ title: "Hotels", url: "/hotels/stockholm" },
|
||||
{ title: "Hotels", url: "/hotels/copenhagen" },
|
||||
]
|
||||
render(<LinkChips chips={duplicateTitles} />)
|
||||
const links = screen.getAllByRole("link", { name: "Hotels" })
|
||||
expect(links).toHaveLength(2)
|
||||
expect(links[0].getAttribute("href")).toBe("/hotels/stockholm")
|
||||
expect(links[1].getAttribute("href")).toBe("/hotels/copenhagen")
|
||||
})
|
||||
})
|
||||
})
|
||||
@@ -0,0 +1,23 @@
|
||||
"use client"
|
||||
|
||||
import { ChipLink } from "../ChipLink"
|
||||
import { Chips } from "../Chips"
|
||||
import { MaterialIcon } from "../Icons/MaterialIcon"
|
||||
import type { LinkChipsProps } from "./types"
|
||||
|
||||
export function LinkChips({ chips }: LinkChipsProps) {
|
||||
if (!chips.length) {
|
||||
return null
|
||||
}
|
||||
|
||||
return (
|
||||
<Chips>
|
||||
{chips.map(({ title, url }) => (
|
||||
<ChipLink key={`${title}-${url}`} href={url}>
|
||||
{title}
|
||||
<MaterialIcon icon="chevron_right" size={20} color="CurrentColor" />
|
||||
</ChipLink>
|
||||
))}
|
||||
</Chips>
|
||||
)
|
||||
}
|
||||
@@ -0,0 +1,2 @@
|
||||
export { LinkChips } from "./LinkChips"
|
||||
export type { LinkChipsProps } from "./types"
|
||||
@@ -0,0 +1,6 @@
|
||||
export interface LinkChipsProps {
|
||||
chips: {
|
||||
url: string
|
||||
title: string
|
||||
}[]
|
||||
}
|
||||
Reference in New Issue
Block a user