Move LinkChips and add unit + a11y tests for chips components
This commit is contained in:
@@ -0,0 +1,144 @@
|
||||
/* eslint-disable formatjs/no-literal-string-in-jsx */
|
||||
import { describe, expect, it, vi, afterEach } from "vitest"
|
||||
import { render, screen, cleanup } from "@testing-library/react"
|
||||
import userEvent from "@testing-library/user-event"
|
||||
|
||||
import { ChipButton } from "./ChipButton"
|
||||
|
||||
afterEach(() => {
|
||||
cleanup()
|
||||
})
|
||||
|
||||
describe("ChipButton accessibility", () => {
|
||||
describe("semantic HTML", () => {
|
||||
it("uses proper button element", () => {
|
||||
render(<ChipButton>Button</ChipButton>)
|
||||
const button = screen.getByRole("button")
|
||||
expect(button.tagName).toBe("BUTTON")
|
||||
})
|
||||
|
||||
it("has accessible button text", () => {
|
||||
render(<ChipButton>Filter by price</ChipButton>)
|
||||
expect(
|
||||
screen.getByRole("button", { name: "Filter by price" })
|
||||
).toBeTruthy()
|
||||
})
|
||||
|
||||
it("button type defaults to button (not submit)", () => {
|
||||
render(<ChipButton>Not Submit</ChipButton>)
|
||||
const button = screen.getByRole("button")
|
||||
// React Aria Components Button defaults to type="button"
|
||||
expect(button.getAttribute("type")).toBe("button")
|
||||
})
|
||||
})
|
||||
|
||||
describe("disabled state", () => {
|
||||
it("disabled button has disabled attribute", () => {
|
||||
render(<ChipButton isDisabled>Disabled</ChipButton>)
|
||||
const button = screen.getByRole("button", { name: "Disabled" })
|
||||
expect(button).toHaveProperty("disabled", true)
|
||||
})
|
||||
|
||||
it("disabled button is not focusable", async () => {
|
||||
const user = userEvent.setup()
|
||||
render(
|
||||
<div>
|
||||
<ChipButton isDisabled>Disabled</ChipButton>
|
||||
<ChipButton>Enabled</ChipButton>
|
||||
</div>
|
||||
)
|
||||
|
||||
await user.tab()
|
||||
// Focus should skip disabled button and go to enabled one
|
||||
expect(document.activeElement).toBe(
|
||||
screen.getByRole("button", { name: "Enabled" })
|
||||
)
|
||||
})
|
||||
})
|
||||
|
||||
describe("keyboard navigation", () => {
|
||||
it("button is keyboard accessible", async () => {
|
||||
const user = userEvent.setup()
|
||||
render(<ChipButton>Accessible Button</ChipButton>)
|
||||
|
||||
const button = screen.getByRole("button")
|
||||
await user.tab()
|
||||
expect(document.activeElement).toBe(button)
|
||||
})
|
||||
|
||||
it("multiple buttons maintain logical focus order", async () => {
|
||||
const user = userEvent.setup()
|
||||
render(
|
||||
<div>
|
||||
<ChipButton>First</ChipButton>
|
||||
<ChipButton>Second</ChipButton>
|
||||
<ChipButton>Third</ChipButton>
|
||||
</div>
|
||||
)
|
||||
|
||||
const firstButton = screen.getByRole("button", { name: "First" })
|
||||
const secondButton = screen.getByRole("button", { name: "Second" })
|
||||
const thirdButton = screen.getByRole("button", { name: "Third" })
|
||||
|
||||
await user.tab()
|
||||
expect(document.activeElement).toBe(firstButton)
|
||||
|
||||
await user.tab()
|
||||
expect(document.activeElement).toBe(secondButton)
|
||||
|
||||
await user.tab()
|
||||
expect(document.activeElement).toBe(thirdButton)
|
||||
})
|
||||
|
||||
it("Enter key activates button", async () => {
|
||||
const onPress = vi.fn()
|
||||
const user = userEvent.setup()
|
||||
render(<ChipButton onPress={onPress}>Activate</ChipButton>)
|
||||
|
||||
await user.tab()
|
||||
await user.keyboard("{Enter}")
|
||||
expect(onPress).toHaveBeenCalledTimes(1)
|
||||
})
|
||||
|
||||
it("Space key activates button", async () => {
|
||||
const onPress = vi.fn()
|
||||
const user = userEvent.setup()
|
||||
render(<ChipButton onPress={onPress}>Activate</ChipButton>)
|
||||
|
||||
await user.tab()
|
||||
await user.keyboard(" ")
|
||||
expect(onPress).toHaveBeenCalledTimes(1)
|
||||
})
|
||||
})
|
||||
|
||||
describe("screen reader support", () => {
|
||||
it("button has accessible name from content", () => {
|
||||
render(<ChipButton>Descriptive Button Text</ChipButton>)
|
||||
const button = screen.getByRole("button")
|
||||
expect(button.textContent?.trim().length).toBeGreaterThan(0)
|
||||
})
|
||||
|
||||
it("button with icon and text has accessible name", () => {
|
||||
render(
|
||||
<ChipButton>
|
||||
<span aria-hidden="true">✓</span>
|
||||
Selected Filter
|
||||
</ChipButton>
|
||||
)
|
||||
const button = screen.getByRole("button")
|
||||
expect(button.textContent).toContain("Selected Filter")
|
||||
})
|
||||
})
|
||||
|
||||
describe("selected state accessibility", () => {
|
||||
it("selected state is indicated via aria-pressed", () => {
|
||||
render(
|
||||
<ChipButton selected aria-pressed={true}>
|
||||
Selected
|
||||
</ChipButton>
|
||||
)
|
||||
const button = screen.getByRole("button", { name: "Selected" })
|
||||
expect(button.getAttribute("aria-pressed")).toBe("true")
|
||||
})
|
||||
})
|
||||
})
|
||||
@@ -0,0 +1,148 @@
|
||||
/* eslint-disable formatjs/no-literal-string-in-jsx */
|
||||
import { describe, expect, it, vi, afterEach } from "vitest"
|
||||
import { render, screen, cleanup, fireEvent } from "@testing-library/react"
|
||||
import userEvent from "@testing-library/user-event"
|
||||
|
||||
import { ChipButton } from "./ChipButton"
|
||||
|
||||
afterEach(() => {
|
||||
cleanup()
|
||||
})
|
||||
|
||||
describe("ChipButton", () => {
|
||||
describe("rendering", () => {
|
||||
it("renders as a button element", () => {
|
||||
render(<ChipButton>Click me</ChipButton>)
|
||||
const button = screen.getByRole("button", { name: "Click me" })
|
||||
expect(button).toBeTruthy()
|
||||
expect(button.tagName).toBe("BUTTON")
|
||||
})
|
||||
|
||||
it("renders children content", () => {
|
||||
render(<ChipButton>Button Content</ChipButton>)
|
||||
expect(screen.getByText("Button Content")).toBeTruthy()
|
||||
})
|
||||
|
||||
it("renders with multiple children", () => {
|
||||
render(
|
||||
<ChipButton>
|
||||
<span>Icon</span>
|
||||
Label
|
||||
</ChipButton>
|
||||
)
|
||||
expect(screen.getByText("Icon")).toBeTruthy()
|
||||
expect(screen.getByText("Label")).toBeTruthy()
|
||||
})
|
||||
})
|
||||
|
||||
describe("variants", () => {
|
||||
it("renders Default variant", () => {
|
||||
render(<ChipButton variant="Default">Default</ChipButton>)
|
||||
expect(screen.getByRole("button", { name: "Default" })).toBeTruthy()
|
||||
})
|
||||
|
||||
it("renders Outlined variant", () => {
|
||||
render(<ChipButton variant="Outlined">Outlined</ChipButton>)
|
||||
expect(screen.getByRole("button", { name: "Outlined" })).toBeTruthy()
|
||||
})
|
||||
|
||||
it("renders FilterRounded variant", () => {
|
||||
render(<ChipButton variant="FilterRounded">Filter</ChipButton>)
|
||||
expect(screen.getByRole("button", { name: "Filter" })).toBeTruthy()
|
||||
})
|
||||
})
|
||||
|
||||
describe("selected state", () => {
|
||||
it("renders with selected=false by default", () => {
|
||||
render(<ChipButton>Not Selected</ChipButton>)
|
||||
expect(screen.getByRole("button", { name: "Not Selected" })).toBeTruthy()
|
||||
})
|
||||
|
||||
it("renders with selected=true", () => {
|
||||
render(<ChipButton selected>Selected</ChipButton>)
|
||||
expect(screen.getByRole("button", { name: "Selected" })).toBeTruthy()
|
||||
})
|
||||
})
|
||||
|
||||
describe("sizes", () => {
|
||||
it("renders Medium size", () => {
|
||||
render(<ChipButton size="Medium">Medium</ChipButton>)
|
||||
expect(screen.getByRole("button", { name: "Medium" })).toBeTruthy()
|
||||
})
|
||||
|
||||
it("renders Large size (default)", () => {
|
||||
render(<ChipButton size="Large">Large</ChipButton>)
|
||||
expect(screen.getByRole("button", { name: "Large" })).toBeTruthy()
|
||||
})
|
||||
})
|
||||
|
||||
describe("props", () => {
|
||||
it("applies custom className", () => {
|
||||
render(<ChipButton className="custom-class">Button</ChipButton>)
|
||||
const button = screen.getByRole("button", { name: "Button" })
|
||||
expect(button.className).toContain("custom-class")
|
||||
})
|
||||
|
||||
it("can be disabled", () => {
|
||||
render(<ChipButton isDisabled>Disabled</ChipButton>)
|
||||
const button = screen.getByRole("button", { name: "Disabled" })
|
||||
expect(button).toHaveProperty("disabled", true)
|
||||
})
|
||||
})
|
||||
|
||||
describe("interactions", () => {
|
||||
it("calls onPress when clicked", () => {
|
||||
const onPress = vi.fn()
|
||||
render(<ChipButton onPress={onPress}>Click me</ChipButton>)
|
||||
|
||||
// React Aria Components Button uses onPress which listens to click events
|
||||
fireEvent.click(screen.getByRole("button", { name: "Click me" }))
|
||||
expect(onPress).toHaveBeenCalledTimes(1)
|
||||
})
|
||||
|
||||
it("does not call onPress when disabled", () => {
|
||||
const onPress = vi.fn()
|
||||
render(
|
||||
<ChipButton isDisabled onPress={onPress}>
|
||||
Disabled
|
||||
</ChipButton>
|
||||
)
|
||||
|
||||
fireEvent.click(screen.getByRole("button", { name: "Disabled" }))
|
||||
expect(onPress).not.toHaveBeenCalled()
|
||||
})
|
||||
})
|
||||
|
||||
describe("keyboard navigation", () => {
|
||||
it("is focusable via keyboard", async () => {
|
||||
const user = userEvent.setup()
|
||||
render(<ChipButton>Focusable</ChipButton>)
|
||||
|
||||
const button = screen.getByRole("button", { name: "Focusable" })
|
||||
await user.tab()
|
||||
expect(document.activeElement).toBe(button)
|
||||
})
|
||||
|
||||
it("can be activated with Enter key", async () => {
|
||||
const onPress = vi.fn()
|
||||
const user = userEvent.setup()
|
||||
render(<ChipButton onPress={onPress}>Press Enter</ChipButton>)
|
||||
|
||||
screen.getByRole("button", { name: "Press Enter" })
|
||||
await user.tab()
|
||||
await user.keyboard("{Enter}")
|
||||
expect(onPress).toHaveBeenCalledTimes(1)
|
||||
})
|
||||
|
||||
it("can be activated with Space key", async () => {
|
||||
const onPress = vi.fn()
|
||||
const user = userEvent.setup()
|
||||
render(<ChipButton onPress={onPress}>Press Space</ChipButton>)
|
||||
|
||||
screen.getByRole("button", { name: "Press Space" })
|
||||
await user.tab()
|
||||
await user.keyboard(" ")
|
||||
expect(onPress).toHaveBeenCalledTimes(1)
|
||||
})
|
||||
})
|
||||
})
|
||||
@@ -0,0 +1,85 @@
|
||||
/* eslint-disable formatjs/no-literal-string-in-jsx */
|
||||
import { describe, expect, it, afterEach } from "vitest"
|
||||
import { render, screen, cleanup } from "@testing-library/react"
|
||||
import userEvent from "@testing-library/user-event"
|
||||
|
||||
import { ChipLink } from "./ChipLink"
|
||||
|
||||
afterEach(() => {
|
||||
cleanup()
|
||||
})
|
||||
|
||||
describe("ChipLink accessibility", () => {
|
||||
describe("semantic HTML", () => {
|
||||
it("uses proper link element for navigation", () => {
|
||||
render(<ChipLink href="/test">Test Link</ChipLink>)
|
||||
const link = screen.getByRole("link")
|
||||
expect(link.tagName).toBe("A")
|
||||
})
|
||||
|
||||
it("has accessible link text", () => {
|
||||
render(<ChipLink href="/hotels">View Hotels</ChipLink>)
|
||||
expect(screen.getByRole("link", { name: "View Hotels" })).toBeTruthy()
|
||||
})
|
||||
|
||||
it("link has descriptive href attribute", () => {
|
||||
render(<ChipLink href="/hotels/stockholm">Stockholm Hotels</ChipLink>)
|
||||
const link = screen.getByRole("link", { name: "Stockholm Hotels" })
|
||||
expect(link.getAttribute("href")).toBe("/hotels/stockholm")
|
||||
})
|
||||
})
|
||||
|
||||
describe("keyboard navigation", () => {
|
||||
it("link is keyboard accessible", async () => {
|
||||
const user = userEvent.setup()
|
||||
render(<ChipLink href="/test">Accessible Link</ChipLink>)
|
||||
|
||||
const link = screen.getByRole("link")
|
||||
await user.tab()
|
||||
expect(document.activeElement).toBe(link)
|
||||
})
|
||||
|
||||
it("multiple links maintain logical focus order", async () => {
|
||||
const user = userEvent.setup()
|
||||
render(
|
||||
<div>
|
||||
<ChipLink href="/first">First Link</ChipLink>
|
||||
<ChipLink href="/second">Second Link</ChipLink>
|
||||
<ChipLink href="/third">Third Link</ChipLink>
|
||||
</div>
|
||||
)
|
||||
|
||||
const firstLink = screen.getByRole("link", { name: "First Link" })
|
||||
const secondLink = screen.getByRole("link", { name: "Second Link" })
|
||||
const thirdLink = screen.getByRole("link", { name: "Third Link" })
|
||||
|
||||
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("link has accessible name from content", () => {
|
||||
render(<ChipLink href="/test">Descriptive Link Text</ChipLink>)
|
||||
const link = screen.getByRole("link")
|
||||
expect(link.textContent?.trim().length).toBeGreaterThan(0)
|
||||
})
|
||||
|
||||
it("link with icon and text has accessible name", () => {
|
||||
render(
|
||||
<ChipLink href="/test">
|
||||
<span aria-hidden="true">→</span>
|
||||
Next Page
|
||||
</ChipLink>
|
||||
)
|
||||
const link = screen.getByRole("link")
|
||||
expect(link.textContent).toContain("Next Page")
|
||||
})
|
||||
})
|
||||
})
|
||||
@@ -0,0 +1,86 @@
|
||||
/* eslint-disable formatjs/no-literal-string-in-jsx */
|
||||
import { describe, expect, it, afterEach } from "vitest"
|
||||
import { render, screen, cleanup } from "@testing-library/react"
|
||||
import userEvent from "@testing-library/user-event"
|
||||
|
||||
import { ChipLink } from "./ChipLink"
|
||||
|
||||
afterEach(() => {
|
||||
cleanup()
|
||||
})
|
||||
|
||||
describe("ChipLink", () => {
|
||||
describe("rendering", () => {
|
||||
it("renders as a link element", () => {
|
||||
render(<ChipLink href="/test">Test Link</ChipLink>)
|
||||
const link = screen.getByRole("link", { name: "Test Link" })
|
||||
expect(link).toBeTruthy()
|
||||
expect(link.tagName).toBe("A")
|
||||
})
|
||||
|
||||
it("renders children content", () => {
|
||||
render(<ChipLink href="/test">Link Content</ChipLink>)
|
||||
expect(screen.getByText("Link Content")).toBeTruthy()
|
||||
})
|
||||
|
||||
it("applies correct href attribute", () => {
|
||||
render(<ChipLink href="/destination">Go somewhere</ChipLink>)
|
||||
const link = screen.getByRole("link", { name: "Go somewhere" })
|
||||
expect(link.getAttribute("href")).toBe("/destination")
|
||||
})
|
||||
|
||||
it("renders with multiple children", () => {
|
||||
render(
|
||||
<ChipLink href="/test">
|
||||
<span>Icon</span>
|
||||
Text
|
||||
</ChipLink>
|
||||
)
|
||||
expect(screen.getByText("Icon")).toBeTruthy()
|
||||
expect(screen.getByText("Text")).toBeTruthy()
|
||||
})
|
||||
})
|
||||
|
||||
describe("props", () => {
|
||||
it("applies custom className", () => {
|
||||
render(
|
||||
<ChipLink href="/test" className="custom-class">
|
||||
Link
|
||||
</ChipLink>
|
||||
)
|
||||
const link = screen.getByRole("link", { name: "Link" })
|
||||
expect(link.className).toContain("custom-class")
|
||||
})
|
||||
|
||||
it("supports target attribute", () => {
|
||||
render(
|
||||
<ChipLink href="/external" target="_blank">
|
||||
External Link
|
||||
</ChipLink>
|
||||
)
|
||||
const link = screen.getByRole("link", { name: "External Link" })
|
||||
expect(link.getAttribute("target")).toBe("_blank")
|
||||
})
|
||||
|
||||
it("supports rel attribute", () => {
|
||||
render(
|
||||
<ChipLink href="/external" rel="noopener noreferrer">
|
||||
External Link
|
||||
</ChipLink>
|
||||
)
|
||||
const link = screen.getByRole("link", { name: "External Link" })
|
||||
expect(link.getAttribute("rel")).toBe("noopener noreferrer")
|
||||
})
|
||||
})
|
||||
|
||||
describe("keyboard navigation", () => {
|
||||
it("is focusable via keyboard", async () => {
|
||||
const user = userEvent.setup()
|
||||
render(<ChipLink href="/test">Focusable Link</ChipLink>)
|
||||
|
||||
const link = screen.getByRole("link", { name: "Focusable Link" })
|
||||
await user.tab()
|
||||
expect(document.activeElement).toBe(link)
|
||||
})
|
||||
})
|
||||
})
|
||||
@@ -0,0 +1,116 @@
|
||||
/* eslint-disable formatjs/no-literal-string-in-jsx */
|
||||
import { describe, expect, it, afterEach } from "vitest"
|
||||
import { render, screen, cleanup } from "@testing-library/react"
|
||||
import userEvent from "@testing-library/user-event"
|
||||
|
||||
import { ChipStatic } from "./ChipStatic"
|
||||
|
||||
afterEach(() => {
|
||||
cleanup()
|
||||
})
|
||||
|
||||
describe("ChipStatic accessibility", () => {
|
||||
describe("semantic HTML", () => {
|
||||
it("uses span element (non-interactive)", () => {
|
||||
render(<ChipStatic>Static Label</ChipStatic>)
|
||||
const chip = screen.getByText("Static Label")
|
||||
expect(chip.tagName).toBe("SPAN")
|
||||
})
|
||||
|
||||
it("is not a button or link", () => {
|
||||
render(<ChipStatic>Not Interactive</ChipStatic>)
|
||||
expect(screen.queryByRole("button")).toBeNull()
|
||||
expect(screen.queryByRole("link")).toBeNull()
|
||||
})
|
||||
|
||||
it("content is visible and readable", () => {
|
||||
render(<ChipStatic>Readable Content</ChipStatic>)
|
||||
expect(screen.getByText("Readable Content")).toBeTruthy()
|
||||
})
|
||||
})
|
||||
|
||||
describe("non-interactive behavior", () => {
|
||||
it("is not focusable by default", async () => {
|
||||
const user = userEvent.setup()
|
||||
render(
|
||||
<div>
|
||||
<ChipStatic>Static Chip</ChipStatic>
|
||||
<button>Focusable Button</button>
|
||||
</div>
|
||||
)
|
||||
|
||||
await user.tab()
|
||||
// Focus should skip the static chip and go directly to the button
|
||||
expect(document.activeElement).toBe(
|
||||
screen.getByRole("button", { name: "Focusable Button" })
|
||||
)
|
||||
})
|
||||
|
||||
it("does not receive focus when tabbing through page", async () => {
|
||||
const user = userEvent.setup()
|
||||
render(
|
||||
<div>
|
||||
<button>First</button>
|
||||
<ChipStatic>Static</ChipStatic>
|
||||
<button>Second</button>
|
||||
</div>
|
||||
)
|
||||
|
||||
const firstButton = screen.getByRole("button", { name: "First" })
|
||||
const secondButton = screen.getByRole("button", { name: "Second" })
|
||||
|
||||
await user.tab()
|
||||
expect(document.activeElement).toBe(firstButton)
|
||||
|
||||
await user.tab()
|
||||
expect(document.activeElement).toBe(secondButton)
|
||||
})
|
||||
})
|
||||
|
||||
describe("screen reader support", () => {
|
||||
it("has visible text content", () => {
|
||||
render(<ChipStatic>Screen Reader Text</ChipStatic>)
|
||||
const chip = screen.getByText("Screen Reader Text")
|
||||
expect(chip.textContent?.trim().length).toBeGreaterThan(0)
|
||||
})
|
||||
|
||||
it("text content is accessible in the DOM", () => {
|
||||
render(<ChipStatic>Status: Active</ChipStatic>)
|
||||
expect(screen.getByText("Status: Active")).toBeTruthy()
|
||||
})
|
||||
})
|
||||
|
||||
describe("color contrast considerations", () => {
|
||||
it("Neutral color variant renders", () => {
|
||||
render(<ChipStatic color="Neutral">Neutral</ChipStatic>)
|
||||
expect(screen.getByText("Neutral")).toBeTruthy()
|
||||
})
|
||||
|
||||
it("Subtle color variant renders", () => {
|
||||
render(<ChipStatic color="Subtle">Subtle</ChipStatic>)
|
||||
expect(screen.getByText("Subtle")).toBeTruthy()
|
||||
})
|
||||
|
||||
it("Disabled color variant renders", () => {
|
||||
render(<ChipStatic color="Disabled">Disabled</ChipStatic>)
|
||||
expect(screen.getByText("Disabled")).toBeTruthy()
|
||||
})
|
||||
})
|
||||
|
||||
describe("text sizing", () => {
|
||||
it("xs size renders readable text", () => {
|
||||
render(<ChipStatic size="xs">Extra Small</ChipStatic>)
|
||||
expect(screen.getByText("Extra Small")).toBeTruthy()
|
||||
})
|
||||
|
||||
it("sm size renders readable text", () => {
|
||||
render(<ChipStatic size="sm">Small</ChipStatic>)
|
||||
expect(screen.getByText("Small")).toBeTruthy()
|
||||
})
|
||||
|
||||
it("lg size renders readable text", () => {
|
||||
render(<ChipStatic size="lg">Large</ChipStatic>)
|
||||
expect(screen.getByText("Large")).toBeTruthy()
|
||||
})
|
||||
})
|
||||
})
|
||||
@@ -0,0 +1,104 @@
|
||||
/* eslint-disable formatjs/no-literal-string-in-jsx */
|
||||
import { describe, expect, it, afterEach } from "vitest"
|
||||
import { render, screen, cleanup } from "@testing-library/react"
|
||||
|
||||
import { ChipStatic } from "./ChipStatic"
|
||||
|
||||
afterEach(() => {
|
||||
cleanup()
|
||||
})
|
||||
|
||||
describe("ChipStatic", () => {
|
||||
describe("rendering", () => {
|
||||
it("renders as a span element", () => {
|
||||
render(<ChipStatic>Static Chip</ChipStatic>)
|
||||
const chip = screen.getByText("Static Chip")
|
||||
expect(chip).toBeTruthy()
|
||||
expect(chip.tagName).toBe("SPAN")
|
||||
})
|
||||
|
||||
it("renders children content", () => {
|
||||
render(<ChipStatic>Chip Content</ChipStatic>)
|
||||
expect(screen.getByText("Chip Content")).toBeTruthy()
|
||||
})
|
||||
|
||||
it("renders with multiple children", () => {
|
||||
render(
|
||||
<ChipStatic>
|
||||
<span>Icon</span>
|
||||
Label
|
||||
</ChipStatic>
|
||||
)
|
||||
expect(screen.getByText("Icon")).toBeTruthy()
|
||||
expect(screen.getByText("Label")).toBeTruthy()
|
||||
})
|
||||
})
|
||||
|
||||
describe("color variants", () => {
|
||||
it("renders Neutral color (default)", () => {
|
||||
render(<ChipStatic color="Neutral">Neutral</ChipStatic>)
|
||||
expect(screen.getByText("Neutral")).toBeTruthy()
|
||||
})
|
||||
|
||||
it("renders Subtle color", () => {
|
||||
render(<ChipStatic color="Subtle">Subtle</ChipStatic>)
|
||||
expect(screen.getByText("Subtle")).toBeTruthy()
|
||||
})
|
||||
|
||||
it("renders Disabled color", () => {
|
||||
render(<ChipStatic color="Disabled">Disabled</ChipStatic>)
|
||||
expect(screen.getByText("Disabled")).toBeTruthy()
|
||||
})
|
||||
})
|
||||
|
||||
describe("sizes", () => {
|
||||
it("renders xs size", () => {
|
||||
render(<ChipStatic size="xs">Extra Small</ChipStatic>)
|
||||
expect(screen.getByText("Extra Small")).toBeTruthy()
|
||||
})
|
||||
|
||||
it("renders sm size (default)", () => {
|
||||
render(<ChipStatic size="sm">Small</ChipStatic>)
|
||||
expect(screen.getByText("Small")).toBeTruthy()
|
||||
})
|
||||
|
||||
it("renders lg size", () => {
|
||||
render(<ChipStatic size="lg">Large</ChipStatic>)
|
||||
expect(screen.getByText("Large")).toBeTruthy()
|
||||
})
|
||||
})
|
||||
|
||||
describe("typography", () => {
|
||||
it("uses uppercase typography by default", () => {
|
||||
render(<ChipStatic>Default Case</ChipStatic>)
|
||||
expect(screen.getByText("Default Case")).toBeTruthy()
|
||||
})
|
||||
|
||||
it("uses lowercase typography when lowerCase is true", () => {
|
||||
render(<ChipStatic lowerCase>Lower Case</ChipStatic>)
|
||||
expect(screen.getByText("Lower Case")).toBeTruthy()
|
||||
})
|
||||
})
|
||||
|
||||
describe("props", () => {
|
||||
it("applies custom className", () => {
|
||||
render(<ChipStatic className="custom-class">Styled</ChipStatic>)
|
||||
const chip = screen.getByText("Styled")
|
||||
expect(chip.className).toContain("custom-class")
|
||||
})
|
||||
})
|
||||
|
||||
describe("edge cases", () => {
|
||||
it("handles empty string children", () => {
|
||||
const emptyString = ""
|
||||
const { container } = render(<ChipStatic>{emptyString}</ChipStatic>)
|
||||
const span = container.querySelector("span")
|
||||
expect(span).toBeTruthy()
|
||||
})
|
||||
|
||||
it("handles numeric children", () => {
|
||||
render(<ChipStatic>{42}</ChipStatic>)
|
||||
expect(screen.getByText("42")).toBeTruthy()
|
||||
})
|
||||
})
|
||||
})
|
||||
@@ -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"
|
||||
6
packages/design-system/lib/components/LinkChips/types.ts
Normal file
6
packages/design-system/lib/components/LinkChips/types.ts
Normal file
@@ -0,0 +1,6 @@
|
||||
export interface LinkChipsProps {
|
||||
chips: {
|
||||
url: string
|
||||
title: string
|
||||
}[]
|
||||
}
|
||||
@@ -20,6 +20,7 @@
|
||||
"./ChipLink": "./lib/components/ChipLink/index.tsx",
|
||||
"./Chips": "./lib/components/Chips/index.tsx",
|
||||
"./ChipStatic": "./lib/components/ChipStatic/index.tsx",
|
||||
"./LinkChips": "./lib/components/LinkChips/index.tsx",
|
||||
"./CodeRateCard": "./lib/components/RateCard/Code/index.tsx",
|
||||
"./ContentCard": "./lib/components/ContentCard/index.tsx",
|
||||
"./DeprecatedSelect": "./lib/components/DeprecatedSelect/index.tsx",
|
||||
|
||||
Reference in New Issue
Block a user