feat: add animation to Settings menu (#6617)

* feat: add price impact back

* chore: update tes tname

* chore: update snapshot for price impact

* fix

* fix

* update snapshot after rebase

* update snapshot

* chore: finish

* chore: remove snapshot

* feat: add test matcher

* cleanup

* chore: add animation test

* add comment

* update comment
This commit is contained in:
Mike Grabowski 2023-05-24 22:02:59 +04:00 committed by GitHub
parent 65d91eb363
commit c07c401189
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
9 changed files with 122 additions and 43 deletions

@ -0,0 +1,31 @@
import { render, screen, waitFor } from 'test-utils/render'
import AnimatedDropdown from './index'
describe('AnimatedDropdown', () => {
it('does not render children when closed', () => {
render(<AnimatedDropdown open={false}>Body</AnimatedDropdown>)
expect(screen.getByText('Body')).not.toBeVisible()
})
it('renders children when open', () => {
render(<AnimatedDropdown open={true}>Body</AnimatedDropdown>)
expect(screen.getByText('Body')).toBeVisible()
})
it('animates when open changes', async () => {
const { rerender } = render(<AnimatedDropdown open={false}>Body</AnimatedDropdown>)
const body = screen.getByText('Body')
expect(body).not.toBeVisible()
rerender(<AnimatedDropdown open={true}>Body</AnimatedDropdown>)
expect(body).not.toBeVisible()
// wait for React Spring animation to finish
await waitFor(() => {
expect(body).toBeVisible()
})
})
})

@ -9,7 +9,10 @@ export default function AnimatedDropdown({ open, children }: React.PropsWithChil
const { ref, height } = useResizeObserver()
const props = useSpring({
height: open ? height ?? 0 : 0,
// On initial render, `height` will be undefined as ref has not been set yet.
// If the dropdown should be open, we fallback to `auto` to avoid flickering.
// Otherwise, we just animate between actual height (when open) and 0 (when closed).
height: open ? height ?? 'auto' : 0,
config: {
mass: 1.2,
tension: 300,
@ -20,14 +23,7 @@ export default function AnimatedDropdown({ open, children }: React.PropsWithChil
})
return (
<animated.div
style={{
...props,
overflow: 'hidden',
width: '100%',
willChange: 'height',
}}
>
<animated.div style={{ ...props, overflow: 'hidden', width: '100%', willChange: 'height' }}>
<div ref={ref}>{children}</div>
</animated.div>
)

@ -10,7 +10,7 @@ describe('Expand', () => {
Body
</Expand>
)
expect(screen.queryByText('Body')).not.toBeInTheDocument()
expect(screen.queryByText('Body')).not.toBeVisible()
})
it('renders children when open', () => {
@ -19,7 +19,7 @@ describe('Expand', () => {
Body
</Expand>
)
expect(screen.queryByText('Body')).toBeInTheDocument()
expect(screen.queryByText('Body')).toBeVisible()
})
it('calls `onToggle` when button is pressed', () => {

@ -1,3 +1,4 @@
import AnimatedDropdown from 'components/AnimatedDropdown'
import Column from 'components/Column'
import React, { PropsWithChildren, ReactElement } from 'react'
import { ChevronDown } from 'react-feather'
@ -17,6 +18,10 @@ const ExpandIcon = styled(ChevronDown)<{ $isOpen: boolean }>`
transition: transform ${({ theme }) => theme.transition.duration.medium};
`
const Content = styled(Column)`
padding-top: ${({ theme }) => theme.grids.md};
`
export default function Expand({
header,
button,
@ -32,7 +37,7 @@ export default function Expand({
onToggle: () => void
}>) {
return (
<Column gap="md">
<Column>
<RowBetween>
{header}
<ButtonContainer data-testid={testId} onClick={onToggle} aria-expanded={isOpen}>
@ -40,7 +45,9 @@ export default function Expand({
<ExpandIcon $isOpen={isOpen} />
</ButtonContainer>
</RowBetween>
{isOpen && children}
<AnimatedDropdown open={isOpen}>
<Content gap="md">{children}</Content>
</AnimatedDropdown>
</Column>
)
}

@ -12,13 +12,6 @@ const renderSlippageSettings = () => {
render(<MaxSlippageSettings autoSlippage={AUTO_SLIPPAGE} />)
}
const renderAndExpandSlippageSettings = () => {
renderSlippageSettings()
// By default, the button to expand Slippage component and show `input` will have `Auto` label
fireEvent.click(screen.getByText('Auto'))
}
// Switch to custom mode by tapping on `Custom` label
const switchToCustomSlippage = () => {
fireEvent.click(screen.getByText('Custom'))
@ -34,21 +27,21 @@ describe('MaxSlippageSettings', () => {
})
it('is not expanded by default', () => {
renderSlippageSettings()
expect(getSlippageInput()).not.toBeInTheDocument()
expect(getSlippageInput()).not.toBeVisible()
})
it('is expanded by default when custom slippage is set', () => {
store.dispatch(updateUserSlippageTolerance({ userSlippageTolerance: 10 }))
renderSlippageSettings()
expect(getSlippageInput()).toBeInTheDocument()
expect(getSlippageInput()).toBeVisible()
})
it('does not render auto slippage as a value, but a placeholder', () => {
renderAndExpandSlippageSettings()
renderSlippageSettings()
switchToCustomSlippage()
expect(getSlippageInput().value).toBe('')
})
it('renders custom slippage above the input', () => {
renderAndExpandSlippageSettings()
renderSlippageSettings()
switchToCustomSlippage()
fireEvent.change(getSlippageInput(), { target: { value: '0.5' } })
@ -56,7 +49,7 @@ describe('MaxSlippageSettings', () => {
expect(screen.queryAllByText('0.50%').length).toEqual(1)
})
it('updates input value on blur with the slippage in store', () => {
renderAndExpandSlippageSettings()
renderSlippageSettings()
switchToCustomSlippage()
const input = getSlippageInput()
@ -66,7 +59,7 @@ describe('MaxSlippageSettings', () => {
expect(input.value).toBe('0.50')
})
it('clears errors on blur and overwrites incorrect value with the latest correct value', () => {
renderAndExpandSlippageSettings()
renderSlippageSettings()
switchToCustomSlippage()
const input = getSlippageInput()
@ -78,7 +71,7 @@ describe('MaxSlippageSettings', () => {
expect(input.value).toBe('50.00')
})
it('does not allow to enter more than 2 digits after the decimal point', () => {
renderAndExpandSlippageSettings()
renderSlippageSettings()
switchToCustomSlippage()
const input = getSlippageInput()
@ -88,7 +81,7 @@ describe('MaxSlippageSettings', () => {
expect(input.value).toBe('0.01')
})
it('does not accept non-numerical values', () => {
renderAndExpandSlippageSettings()
renderSlippageSettings()
switchToCustomSlippage()
const input = getSlippageInput()
@ -97,7 +90,7 @@ describe('MaxSlippageSettings', () => {
expect(input.value).toBe('')
})
it('does not set slippage when user enters `.` value', () => {
renderAndExpandSlippageSettings()
renderSlippageSettings()
switchToCustomSlippage()
const input = getSlippageInput()

@ -9,13 +9,6 @@ const renderTransactionDeadlineSettings = () => {
render(<TransactionDeadlineSettings />)
}
const renderAndExpandTransactionDeadlineSettings = () => {
renderTransactionDeadlineSettings()
// By default, the button to expand Slippage component and show `input` will have `<deadline>m` label
fireEvent.click(screen.getByText(`${DEFAULT_DEADLINE_FROM_NOW / 60}m`))
}
const getDeadlineInput = () => screen.queryByTestId('deadline-input') as HTMLInputElement
describe('TransactionDeadlineSettings', () => {
@ -26,26 +19,26 @@ describe('TransactionDeadlineSettings', () => {
})
it('is not expanded by default', () => {
renderTransactionDeadlineSettings()
expect(getDeadlineInput()).not.toBeInTheDocument()
expect(getDeadlineInput()).not.toBeVisible()
})
it('is expanded by default when custom deadline is set', () => {
store.dispatch(updateUserDeadline({ userDeadline: DEFAULT_DEADLINE_FROM_NOW * 2 }))
renderTransactionDeadlineSettings()
expect(getDeadlineInput()).toBeInTheDocument()
expect(getDeadlineInput()).toBeVisible()
})
it('does not render default deadline as a value, but a placeholder', () => {
renderAndExpandTransactionDeadlineSettings()
renderTransactionDeadlineSettings()
expect(getDeadlineInput().value).toBe('')
})
it('renders custom deadline above the input', () => {
renderAndExpandTransactionDeadlineSettings()
renderTransactionDeadlineSettings()
fireEvent.change(getDeadlineInput(), { target: { value: '50' } })
expect(screen.queryAllByText('50m').length).toEqual(1)
})
it('marks deadline as invalid if it is greater than 4320m (3 days) or 0m', () => {
renderAndExpandTransactionDeadlineSettings()
renderTransactionDeadlineSettings()
const input = getDeadlineInput()
fireEvent.change(input, { target: { value: '4321' } })
@ -55,7 +48,7 @@ describe('TransactionDeadlineSettings', () => {
expect(input.value).toBe('')
})
it('clears errors on blur and overwrites incorrect value with the latest correct value', () => {
renderAndExpandTransactionDeadlineSettings()
renderTransactionDeadlineSettings()
const input = getDeadlineInput()
fireEvent.change(input, { target: { value: '5' } })
@ -69,7 +62,7 @@ describe('TransactionDeadlineSettings', () => {
expect(input.value).toBe('5')
})
it('does not accept non-numerical values', () => {
renderAndExpandTransactionDeadlineSettings()
renderTransactionDeadlineSettings()
const input = getDeadlineInput()
fireEvent.change(input, { target: { value: 'c' } })

@ -7,6 +7,7 @@ import type { createPopper } from '@popperjs/core'
import { useWeb3React } from '@web3-react/core'
import failOnConsole from 'jest-fail-on-console'
import { Readable } from 'stream'
import { toBeVisible } from 'test-utils/matchers'
import { mocked } from 'test-utils/mocked'
import { TextDecoder, TextEncoder } from 'util'
@ -100,3 +101,7 @@ failOnConsole({
shouldFailOnLog: true,
shouldFailOnWarn: true,
})
expect.extend({
toBeVisible,
})

@ -0,0 +1,22 @@
import { render, screen } from './render'
describe('matchers', () => {
describe('toBeVisible', () => {
it('should return true if element is visible', () => {
render(<div>test</div>)
expect(screen.getByText('test')).toBeVisible()
})
it('should return false if element is hidden', () => {
render(<div style={{ height: 0 }}>test</div>)
expect(screen.getByText('test')).not.toBeVisible()
})
it('should return false if parent element is hidden', () => {
render(
<div style={{ height: 0 }}>
<div>test</div>
</div>
)
expect(screen.getByText('test')).not.toBeVisible()
})
})
})

@ -0,0 +1,32 @@
// This type is not exported from Jest, so we need to infer it from the expect.extend function.
type MatcherFunction = Parameters<typeof expect.extend>[0] extends { [key: string]: infer I } ? I : never
const isElementVisible = (element: HTMLElement): boolean => {
return element.style.height !== '0px' && (!element.parentElement || isElementVisible(element.parentElement))
}
// Overrides the Testing Library matcher to check for height when determining whether an element is visible.
// We are doing this because:
// - original `toBeVisible()` does not take `height` into account
// https://github.com/testing-library/jest-dom/issues/450
// - original `toBeVisible()` and `toHaveStyle()` does not work at all in some cases
// https://github.com/testing-library/jest-dom/issues/209
// - `getComputedStyles()` returns empty object, making it impossible to check for Styled Components styles
// https://github.com/styled-components/styled-components/issues/3262
// https://github.com/jsdom/jsdom/issues/2986
// For the reasons above, this matcher only works for inline styles.
export const toBeVisible: MatcherFunction = function (element: HTMLElement) {
const isVisible = isElementVisible(element)
return {
pass: isVisible,
message: () => {
const is = isVisible ? 'is' : 'is not'
return [
this.utils.matcherHint(`${this.isNot ? '.not' : ''}.toBeVisible`, 'element', ''),
'',
`Received element ${is} visible:`,
` ${this.utils.printReceived(element.cloneNode(false))}`,
].join('\n')
},
}
}