Skip to content

Commit

Permalink
Add more tests
Browse files Browse the repository at this point in the history
  • Loading branch information
lariciamota committed Oct 12, 2021
1 parent bc339eb commit 82c5b3c
Show file tree
Hide file tree
Showing 5 changed files with 191 additions and 18 deletions.
138 changes: 126 additions & 12 deletions packages/store-ui/src/molecules/Accordion/Accordion.test.tsx
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { render, fireEvent } from '@testing-library/react'
import { render, fireEvent, cleanup } from '@testing-library/react'
import { axe } from 'jest-axe'
import React, { useState } from 'react'

Expand Down Expand Up @@ -56,23 +56,137 @@ const TestAccordion = () => {
}

describe('Accordion', () => {
it('should have `data-store-accordion` attribute', () => {
const { getByTestId } = render(<TestAccordion />)
let accordion: HTMLElement
let items: HTMLElement[]
let buttons: HTMLElement[]
let panels: HTMLElement[]

expect(getByTestId('store-accordion')).toHaveAttribute(
'data-store-accordion'
beforeEach(() => {
const { getByTestId, getAllByTestId } = render(<TestAccordion />)

accordion = getByTestId('store-accordion')
items = getAllByTestId('store-accordion-item')
buttons = getAllByTestId('store-accordion-button')
panels = getAllByTestId('store-accordion-panel')
})

afterEach(cleanup)

it('should show panel specified by `indices`', () => {
const { getAllByTestId } = render(
<Accordion indices={[1]} onChange={() => {}}>
<AccordionItem>
<AccordionButton />
<AccordionPanel testId="store-accordion-panel-mock" />
</AccordionItem>
<AccordionItem>
<AccordionButton />
<AccordionPanel testId="store-accordion-panel-mock" />
</AccordionItem>
</Accordion>
)

const panelsMock = getAllByTestId('store-accordion-panel-mock')

expect(panelsMock[0]).not.toBeVisible()
expect(panelsMock[1]).toBeVisible()
})

it('should not have ARIA violations', async () => {
const { queryAllByTestId } = render(<TestAccordion />)
describe('Data attributes', () => {
it('`Accordion` component should have `data-store-accordion` attribute', () => {
expect(accordion).toHaveAttribute('data-store-accordion')
})

it('`AccordionItem` component should have `data-store-accordion-item` attribute', () => {
for (const item of items) {
expect(item).toHaveAttribute('data-store-accordion-item')
}
})

it('`AccordionButton` component should have `data-store-accordion-button` attribute', () => {
for (const button of buttons) {
expect(button).toHaveAttribute('data-store-accordion-button')
}
})

it('`AccordionPanel` component should have `data-store-accordion-panel` attribute', () => {
for (const panel of panels) {
expect(panel).toHaveAttribute('data-store-accordion-panel')
}
})
})

describe('User actions', () => {
it('clicking item should call `onChange` function', () => {
const mockOnChange = jest.fn()
const { getByTestId } = render(
<Accordion onChange={mockOnChange} indices={[]}>
<AccordionItem>
<AccordionButton testId="store-accordion-button-mock" />
</AccordionItem>
</Accordion>
)

const button = getByTestId('store-accordion-button-mock')

fireEvent.click(button)
expect(mockOnChange).toHaveBeenCalledTimes(1)
})

it('should move focus to the next focusable button on `ArrowDown` press', () => {
buttons[1].focus()
expect(buttons[1]).toHaveFocus()
fireEvent.keyDown(document.activeElement!, { key: 'ArrowDown' })
expect(buttons[0]).toHaveFocus()
})

it('should move focus to the previous focusable button on `ArrowUp` press', () => {
buttons[1].focus()
expect(buttons[1]).toHaveFocus()
fireEvent.keyDown(document.activeElement!, { key: 'ArrowUp' })
expect(buttons[0]).toHaveFocus()
})
})

describe('Acessibility', () => {
// WAI-ARIA tests
// https://www.w3.org/TR/wai-aria-practices-1.2/#accordion
it('should not have violations', async () => {
expect(await axe(document.body)).toHaveNoViolations()

// Open a panel and check again
fireEvent.click(buttons[0])
expect(await axe(document.body)).toHaveNoViolations()
})

it('`role` should be set to `region` for panel elements', () => {
for (const panel of panels) {
expect(panel).toHaveAttribute('role', 'region')
}
})

expect(await axe(document.body)).toHaveNoViolations()
it('`aria-labelledby` for panel elements should point to the corresponding button', () => {
panels.forEach((panel, index) => {
expect(panel).toHaveAttribute(
'aria-labelledby',
buttons[index].getAttribute('id')
)
})
})

// Open a panel and check again
const buttons = queryAllByTestId('store-accordion-button')
it('`aria-controls` for button elements should point to the corresponding panel', () => {
buttons.forEach((button, index) => {
expect(button).toHaveAttribute(
'aria-controls',
panels[index].getAttribute('id')
)
})
})

fireEvent.click(buttons[1])
expect(await axe(document.body)).toHaveNoViolations()
it('`aria-expanded` should be true only for active button', () => {
expect(buttons[0]).toHaveAttribute('aria-expanded', 'false')
fireEvent.click(buttons[0])
expect(buttons[0]).toHaveAttribute('aria-expanded', 'true')
})
})
})
13 changes: 9 additions & 4 deletions packages/store-ui/src/molecules/Accordion/Accordion.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -9,18 +9,19 @@ export interface AccordionProps
*/
testId?: string
/**
* Indices that indicate which accordion items are opened
* Indices that indicate which accordion items are opened.
*/
indices: Iterable<number>
/**
* Function that is triggered when an accordion item is opened/closed
* Function that is triggered when an accordion item is opened/closed.
*/
onChange: (index: number) => void
}

interface AccordionContext {
indices: Set<number>
onChange: (index: number) => void
numberOfItems: number
}

const AccordionContext = createContext<AccordionContext | undefined>(undefined)
Expand All @@ -29,13 +30,17 @@ const Accordion = forwardRef<HTMLDivElement, AccordionProps>(function Accordion(
{ testId = 'store-accordion', indices, onChange, children, ...props },
ref
) {
const context = { indices: new Set(indices), onChange }

const childrenWithIndex = React.Children.map(
children as ReactElement,
(child, index) => cloneElement(child, { index: child.props.index ?? index })
)

const context = {
indices: new Set(indices),
onChange,
numberOfItems: childrenWithIndex.length,
}

return (
<AccordionContext.Provider value={context}>
<div
Expand Down
36 changes: 35 additions & 1 deletion packages/store-ui/src/molecules/Accordion/AccordionButton.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -20,9 +20,42 @@ export const AccordionButton = forwardRef<
{ testId = 'store-accordion-button', children, ...props },
ref
) {
const { indices, onChange } = useAccordion()
const { indices, onChange, numberOfItems } = useAccordion()
const { index, panel, button } = useAccordionItem()

const onKeyDown = (event: React.KeyboardEvent) => {
if (!['ArrowDown', 'ArrowUp'].includes(event.key)) {
return
}

const getNext = () => {
const next = Number(index) + 1 === numberOfItems ? 0 : Number(index) + 1

return document.getElementById(`button--${next}`)
}

const getPrevious = () => {
const previous =
Number(index) - 1 < 0 ? numberOfItems - 1 : Number(index) - 1

return document.getElementById(`button--${previous}`)
}

switch (event.key) {
case 'ArrowDown':
event.preventDefault()
getNext()?.focus()
break

case 'ArrowUp':
event.preventDefault()
getPrevious()?.focus()
break

default:
}
}

return (
<Button
ref={ref}
Expand All @@ -31,6 +64,7 @@ export const AccordionButton = forwardRef<
aria-controls={panel}
data-store-accordion-button
data-testid={testId}
onKeyDown={props.onKeyDown ?? onKeyDown}
onClick={() => {
onChange(index)
}}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@ export interface AccordionItemProps extends HTMLAttributes<HTMLDivElement> {
*/
testId?: string
/**
* Index of the current accordion item within the accordion
* Index of the current accordion item within the accordion.
*/
index?: number
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -45,6 +45,24 @@ const Clothing = ({ icon, ...props }: { icon?: boolean }) => (
</AccordionItem>
)

const Bags = ({ icon, ...props }: { icon?: boolean }) => (
<AccordionItem {...props}>
<AccordionButton>
Bags {icon ? <Icon component={<Caret />} /> : null}
</AccordionButton>
<AccordionPanel>
<ul>
<li>
<a href="/">Backpacks</a>
</li>
<li>
<a href="/">Necessaire</a>
</li>
</ul>
</AccordionPanel>
</AccordionItem>
)

const Sale = ({ icon, ...props }: { icon?: boolean }) => (
<AccordionItem {...props}>
<AccordionButton>
Expand Down Expand Up @@ -77,6 +95,7 @@ const AccordionTemplate: Story<AccordionProps> = ({ testId }) => {
return (
<Component testId={testId} indices={indices} onChange={onChange}>
<Clothing />
<Bags />
<Sale />
</Component>
)
Expand All @@ -93,6 +112,7 @@ const ToggleWithIconTemplate: Story<AccordionProps> = ({ testId }) => {
return (
<Component testId={testId} indices={indices} onChange={onChange}>
<Clothing icon />
<Bags icon />
<Sale icon />
</Component>
)
Expand Down

0 comments on commit 82c5b3c

Please sign in to comment.