Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
23 changes: 9 additions & 14 deletions package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -126,7 +126,7 @@
"@testing-library/jest-dom": "5.16.4",
"@testing-library/react": "12.1.5",
"@testing-library/react-hooks": "7.0.2",
"@testing-library/user-event": "13.1.9",
"@testing-library/user-event": "^14.3.0",
"@types/chroma-js": "2.1.3",
"@types/enzyme": "3.10.9",
"@types/jest": "27.0.2",
Expand Down
17 changes: 10 additions & 7 deletions src/SegmentedControl/SegmentedControl.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -153,7 +153,8 @@ describe('SegmentedControl', () => {
}
})

it('calls onChange with index of clicked segment button', () => {
it('calls onChange with index of clicked segment button', async () => {
const user = userEvent.setup()
const handleChange = jest.fn()
const {getByText} = render(
<SegmentedControl aria-label="File view" onChange={handleChange}>
Expand All @@ -169,12 +170,13 @@ describe('SegmentedControl', () => {

expect(handleChange).not.toHaveBeenCalled()
if (buttonToClick) {
userEvent.click(buttonToClick)
await user.click(buttonToClick)
}
expect(handleChange).toHaveBeenCalledWith(1)
})

it('calls segment button onClick if it is passed', () => {
it('calls segment button onClick if it is passed', async () => {
const user = userEvent.setup()
const handleClick = jest.fn()
const {getByText} = render(
<SegmentedControl aria-label="File view">
Expand All @@ -190,12 +192,13 @@ describe('SegmentedControl', () => {

expect(handleClick).not.toHaveBeenCalled()
if (buttonToClick) {
userEvent.click(buttonToClick)
await user.click(buttonToClick)
}
expect(handleClick).toHaveBeenCalled()
})

it('focuses the selected button first', () => {
it('focuses the selected button first', async () => {
const user = userEvent.setup()
const {getByRole} = render(
<>
<button>Before</button>
Expand All @@ -212,8 +215,8 @@ describe('SegmentedControl', () => {

expect(document.activeElement?.id).not.toEqual(initialFocusButtonNode.id)

userEvent.tab() // focus the button before the segmented control
userEvent.tab() // move focus into the segmented control
await user.tab() // focus the button before the segmented control
await user.tab() // move focus into the segmented control

expect(document.activeElement?.id).toEqual(initialFocusButtonNode.id)
})
Expand Down
68 changes: 37 additions & 31 deletions src/__tests__/Autocomplete.test.tsx
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import React from 'react'
import {render} from '../utils/testing'
import {render as HTMLRender, fireEvent} from '@testing-library/react'
import {render as HTMLRender, fireEvent, waitFor} from '@testing-library/react'
import {toHaveNoViolations} from 'jest-axe'
import 'babel-polyfill'
import Autocomplete, {AutocompleteInputProps} from '../Autocomplete'
Expand Down Expand Up @@ -193,7 +193,8 @@ describe('Autocomplete', () => {
})

describe('Autocomplete.Input', () => {
it('calls onChange', () => {
it('calls onChange', async () => {
const user = userEvent.setup()
const onChangeMock = jest.fn()
const {container} = HTMLRender(
<LabelledAutocomplete
Expand All @@ -204,7 +205,7 @@ describe('Autocomplete', () => {
const inputNode = container.querySelector('#autocompleteInput')

expect(onChangeMock).not.toHaveBeenCalled()
inputNode && userEvent.type(inputNode, 'z')
inputNode && (await user.type(inputNode, 'z'))
expect(onChangeMock).toHaveBeenCalled()
})

Expand Down Expand Up @@ -244,15 +245,16 @@ describe('Autocomplete', () => {
expect(onKeyUpMock).toHaveBeenCalled()
})

it('calls onKeyPress', () => {
it('calls onKeyPress', async () => {
const user = userEvent.setup()
const onKeyPressMock = jest.fn()
const {getByLabelText} = HTMLRender(
<LabelledAutocomplete inputProps={{onKeyPress: onKeyPressMock}} menuProps={{items: [], selectedItemIds: []}} />
)
const inputNode = getByLabelText(AUTOCOMPLETE_LABEL)

expect(onKeyPressMock).not.toHaveBeenCalled()
userEvent.type(inputNode, '{enter}')
await user.type(inputNode, '{enter}')
expect(onKeyPressMock).toHaveBeenCalled()
})

Expand All @@ -265,7 +267,7 @@ describe('Autocomplete', () => {
expect(inputNode.getAttribute('aria-expanded')).toBe('true')
})

it('closes the menu when the input is blurred', () => {
it('closes the menu when the input is blurred', async () => {
const {getByLabelText} = HTMLRender(<LabelledAutocomplete menuProps={{items: [], selectedItemIds: []}} />)
const inputNode = getByLabelText(AUTOCOMPLETE_LABEL)

Expand All @@ -276,49 +278,50 @@ describe('Autocomplete', () => {
fireEvent.blur(inputNode)

// wait a tick for blur to finish
setTimeout(() => {
expect(inputNode.getAttribute('aria-expanded')).not.toBe('true')
}, 0)
await waitFor(() => expect(inputNode.getAttribute('aria-expanded')).not.toBe('true'))
Comment on lines -279 to +281
Copy link
Contributor Author

@iansan5653 iansan5653 Jul 26, 2022

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This strategy of using setTimeout to wait for an action to resolve was actually failing silently because the assertion would fail after the test completes. Introducing async tests exposed this problem because the failure would actually happen during the next test, because async calls get put at the end of the stack. await waitFor is what we want here anyway.

Before, this looked like:

  1. suite start
  2. test 1 start
  3. test 1 end
  4. test 2 start
  5. test 2 end
  6. suite end
  7. ⚠️ test 1 assertion completes silently

With async tests, it looked like:

  1. suite start
  2. test 1 start
  3. test 1 end
  4. test 2 start
  5. ⚠️ test 1 assertion fails, causing test 2 to fail
  6. test 2 end
  7. suite end

})

it('sets the input value to the suggested item text and highlights the untyped part of the word', () => {
it('sets the input value to the suggested item text and highlights the untyped part of the word', async () => {
const user = userEvent.setup()
const {container, getByDisplayValue} = HTMLRender(
<LabelledAutocomplete menuProps={{items: mockItems, selectedItemIds: []}} />
)
const inputNode = container.querySelector('#autocompleteInput')

inputNode && userEvent.type(inputNode, 'ze')
inputNode && (await user.type(inputNode, 'ze'))
expect(getByDisplayValue('zero')).toBeDefined()
})

it('does not show or highlight suggestion text after the user hits Backspace until they hit another key', () => {
it('does not show or highlight suggestion text after the user hits Backspace until they hit another key', async () => {
const user = userEvent.setup()
const {container, getByDisplayValue} = HTMLRender(
<LabelledAutocomplete menuProps={{items: mockItems, selectedItemIds: []}} />
)
const inputNode = container.querySelector('#autocompleteInput')

expect((inputNode as HTMLInputElement).selectionStart).toBe(0)
inputNode && userEvent.type(inputNode, 'ze')
inputNode && (await user.type(inputNode, 'ze'))
expect(getByDisplayValue('zero')).toBeDefined()
expect((inputNode as HTMLInputElement).selectionStart).toBe(2)
expect((inputNode as HTMLInputElement).selectionEnd).toBe(4)
inputNode && userEvent.type(inputNode, '{backspace}')
inputNode && (await user.keyboard('{backspace}'))
expect((inputNode as HTMLInputElement).selectionStart).toBe(2)
expect(getByDisplayValue('ze')).toBeDefined()
inputNode && userEvent.type(inputNode, 'r')
inputNode && (await user.keyboard('r'))
expect((inputNode as HTMLInputElement).selectionStart).toBe(3)
expect((inputNode as HTMLInputElement).selectionEnd).toBe(4)
expect(getByDisplayValue('zero')).toBeDefined()
})

it('clears the input value when the user hits Escape', () => {
it('clears the input value when the user hits Escape', async () => {
const user = userEvent.setup()
const {container} = HTMLRender(<LabelledAutocomplete menuProps={{items: mockItems, selectedItemIds: []}} />)
const inputNode = container.querySelector('#autocompleteInput')

expect(inputNode?.getAttribute('aria-expanded')).not.toBe('true')
inputNode && userEvent.type(inputNode, 'ze')
inputNode && (await user.type(inputNode, 'ze'))
expect(inputNode?.getAttribute('aria-expanded')).toBe('true')
inputNode && userEvent.type(inputNode, '{esc}')
inputNode && (await user.keyboard('{escape}'))
expect(inputNode?.getAttribute('aria-expanded')).not.toBe('true')
})

Expand All @@ -332,18 +335,20 @@ describe('Autocomplete', () => {
})

describe('Autocomplete.Menu', () => {
it('calls a custom filter function', () => {
it('calls a custom filter function', async () => {
const user = userEvent.setup()
const filterFnMock = jest.fn()
const {container} = HTMLRender(
<LabelledAutocomplete menuProps={{items: mockItems, selectedItemIds: [], filterFn: filterFnMock}} />
)
const inputNode = container.querySelector('#autocompleteInput')

inputNode && userEvent.type(inputNode, 'ze')
inputNode && (await user.type(inputNode, 'ze'))
expect(filterFnMock).toHaveBeenCalled()
})

it('calls a custom sort function when the menu closes', () => {
it('calls a custom sort function when the menu closes', async () => {
const user = userEvent.setup()
const sortOnCloseFnMock = jest.fn()
const {container} = HTMLRender(
<LabelledAutocomplete menuProps={{items: mockItems, selectedItemIds: [], sortOnCloseFn: sortOnCloseFnMock}} />
Expand All @@ -354,29 +359,29 @@ describe('Autocomplete', () => {
// current sort order matches the result of `sortOnCloseFnMock`
expect(sortOnCloseFnMock).toHaveBeenCalledTimes(mockItems.length - 1)
if (inputNode) {
userEvent.type(inputNode, 'ze')
await user.type(inputNode, 'ze')
// eslint-disable-next-line github/no-blur
fireEvent.blur(inputNode)
}

// wait a tick for blur to finish
setTimeout(() => {
expect(sortOnCloseFnMock).toHaveBeenCalledTimes(mockItems.length)
}, 0)
await waitFor(() => expect(sortOnCloseFnMock).toHaveBeenCalled())
Comment on lines -363 to +368
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Here fixing the setTimeout actually exposed an assertion failure that was previously hidden. Array.sort does not call the sort function once per element like filter or map would; a sorting algorithm has to run more than one single pass over the array to fully sort it. In this case, it was calling the sort function 18 times on an array of 10 elements. Rather than test the number of calls, which depends on the JS engine's sort implementation, I changed it to just test that at least one call was made.

})

it("calls onOpenChange with the menu's open state", () => {
it("calls onOpenChange with the menu's open state", async () => {
const user = userEvent.setup()
const onOpenChangeMock = jest.fn()
const {container} = HTMLRender(
<LabelledAutocomplete menuProps={{items: mockItems, selectedItemIds: [], onOpenChange: onOpenChangeMock}} />
)
const inputNode = container.querySelector('#autocompleteInput')

inputNode && userEvent.type(inputNode, 'ze')
inputNode && (await user.type(inputNode, 'ze'))
expect(onOpenChangeMock).toHaveBeenCalled()
})

it('calls onSelectedChange with the data for the selected items', () => {
it('calls onSelectedChange with the data for the selected items', async () => {
const user = userEvent.setup()
const onSelectedChangeMock = jest.fn()
const {container} = HTMLRender(
<LabelledAutocomplete
Expand All @@ -388,7 +393,7 @@ describe('Autocomplete', () => {
expect(onSelectedChangeMock).not.toHaveBeenCalled()
if (inputNode) {
fireEvent.focus(inputNode)
userEvent.type(inputNode, '{enter}')
await user.type(inputNode, '{enter}')
}

// wait a tick for the keyboard event to be dispatched to the menu item
Expand All @@ -397,7 +402,8 @@ describe('Autocomplete', () => {
}, 0)
})

it('does not close the menu when clicking an item in the menu if selectionVariant=multiple', () => {
it('does not close the menu when clicking an item in the menu if selectionVariant=multiple', async () => {
const user = userEvent.setup()
const {getByText, container} = HTMLRender(
<LabelledAutocomplete menuProps={{items: mockItems, selectedItemIds: [], selectionVariant: 'multiple'}} />
)
Expand All @@ -408,7 +414,7 @@ describe('Autocomplete', () => {
inputNode && fireEvent.focus(inputNode)
expect(inputNode?.getAttribute('aria-expanded')).toBe('true')
fireEvent.click(itemToClickNode)
inputNode && userEvent.type(inputNode, '{enter}')
inputNode && (await user.type(inputNode, '{enter}'))
expect(inputNode?.getAttribute('aria-expanded')).toBe('true')
})

Expand Down
17 changes: 10 additions & 7 deletions src/__tests__/Checkbox.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -45,19 +45,20 @@ describe('Checkbox', () => {
expect(checkbox.checked).toEqual(true)
})

it('accepts a change handler that can alter the checkbox state', () => {
it('accepts a change handler that can alter the checkbox state', async () => {
const user = userEvent.setup()
const handleChange = jest.fn()
const {getByRole} = render(<Checkbox onChange={handleChange} />)

const checkbox = getByRole('checkbox') as HTMLInputElement

expect(checkbox.checked).toEqual(false)

userEvent.click(checkbox)
await user.click(checkbox)
expect(handleChange).toHaveBeenCalled()
expect(checkbox.checked).toEqual(true)

userEvent.click(checkbox)
await user.click(checkbox)
expect(handleChange).toHaveBeenCalled()
expect(checkbox.checked).toEqual(false)
})
Expand All @@ -72,7 +73,8 @@ describe('Checkbox', () => {
expect(checkbox.checked).toEqual(false)
})

it('renders an inactive checkbox state correctly', () => {
it('renders an inactive checkbox state correctly', async () => {
const user = userEvent.setup()
const handleChange = jest.fn()
const {getByRole, rerender} = render(<Checkbox disabled onChange={handleChange} />)

Expand All @@ -82,7 +84,7 @@ describe('Checkbox', () => {
expect(checkbox.checked).toEqual(false)
expect(checkbox).toHaveAttribute('aria-disabled', 'true')

userEvent.click(checkbox)
await user.click(checkbox)

expect(checkbox.disabled).toEqual(true)
expect(checkbox.checked).toEqual(false)
Expand All @@ -94,14 +96,15 @@ describe('Checkbox', () => {
expect(checkbox).toHaveAttribute('aria-disabled', 'false')
})

it('renders an uncontrolled component correctly', () => {
it('renders an uncontrolled component correctly', async () => {
const user = userEvent.setup()
const {getByRole} = render(<Checkbox defaultChecked />)

const checkbox = getByRole('checkbox') as HTMLInputElement

expect(checkbox.checked).toEqual(true)

userEvent.click(checkbox)
await user.click(checkbox)

expect(checkbox.checked).toEqual(false)
})
Expand Down
Loading