Commit c07c4011 authored by Mike Grabowski's avatar Mike Grabowski Committed by GitHub

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
parent 65d91eb3
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 ...@@ -9,7 +9,10 @@ export default function AnimatedDropdown({ open, children }: React.PropsWithChil
const { ref, height } = useResizeObserver() const { ref, height } = useResizeObserver()
const props = useSpring({ 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: { config: {
mass: 1.2, mass: 1.2,
tension: 300, tension: 300,
...@@ -20,14 +23,7 @@ export default function AnimatedDropdown({ open, children }: React.PropsWithChil ...@@ -20,14 +23,7 @@ export default function AnimatedDropdown({ open, children }: React.PropsWithChil
}) })
return ( return (
<animated.div <animated.div style={{ ...props, overflow: 'hidden', width: '100%', willChange: 'height' }}>
style={{
...props,
overflow: 'hidden',
width: '100%',
willChange: 'height',
}}
>
<div ref={ref}>{children}</div> <div ref={ref}>{children}</div>
</animated.div> </animated.div>
) )
......
...@@ -10,7 +10,7 @@ describe('Expand', () => { ...@@ -10,7 +10,7 @@ describe('Expand', () => {
Body Body
</Expand> </Expand>
) )
expect(screen.queryByText('Body')).not.toBeInTheDocument() expect(screen.queryByText('Body')).not.toBeVisible()
}) })
it('renders children when open', () => { it('renders children when open', () => {
...@@ -19,7 +19,7 @@ describe('Expand', () => { ...@@ -19,7 +19,7 @@ describe('Expand', () => {
Body Body
</Expand> </Expand>
) )
expect(screen.queryByText('Body')).toBeInTheDocument() expect(screen.queryByText('Body')).toBeVisible()
}) })
it('calls `onToggle` when button is pressed', () => { it('calls `onToggle` when button is pressed', () => {
......
import AnimatedDropdown from 'components/AnimatedDropdown'
import Column from 'components/Column' import Column from 'components/Column'
import React, { PropsWithChildren, ReactElement } from 'react' import React, { PropsWithChildren, ReactElement } from 'react'
import { ChevronDown } from 'react-feather' import { ChevronDown } from 'react-feather'
...@@ -17,6 +18,10 @@ const ExpandIcon = styled(ChevronDown)<{ $isOpen: boolean }>` ...@@ -17,6 +18,10 @@ const ExpandIcon = styled(ChevronDown)<{ $isOpen: boolean }>`
transition: transform ${({ theme }) => theme.transition.duration.medium}; transition: transform ${({ theme }) => theme.transition.duration.medium};
` `
const Content = styled(Column)`
padding-top: ${({ theme }) => theme.grids.md};
`
export default function Expand({ export default function Expand({
header, header,
button, button,
...@@ -32,7 +37,7 @@ export default function Expand({ ...@@ -32,7 +37,7 @@ export default function Expand({
onToggle: () => void onToggle: () => void
}>) { }>) {
return ( return (
<Column gap="md"> <Column>
<RowBetween> <RowBetween>
{header} {header}
<ButtonContainer data-testid={testId} onClick={onToggle} aria-expanded={isOpen}> <ButtonContainer data-testid={testId} onClick={onToggle} aria-expanded={isOpen}>
...@@ -40,7 +45,9 @@ export default function Expand({ ...@@ -40,7 +45,9 @@ export default function Expand({
<ExpandIcon $isOpen={isOpen} /> <ExpandIcon $isOpen={isOpen} />
</ButtonContainer> </ButtonContainer>
</RowBetween> </RowBetween>
{isOpen && children} <AnimatedDropdown open={isOpen}>
<Content gap="md">{children}</Content>
</AnimatedDropdown>
</Column> </Column>
) )
} }
...@@ -12,13 +12,6 @@ const renderSlippageSettings = () => { ...@@ -12,13 +12,6 @@ const renderSlippageSettings = () => {
render(<MaxSlippageSettings autoSlippage={AUTO_SLIPPAGE} />) 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 // Switch to custom mode by tapping on `Custom` label
const switchToCustomSlippage = () => { const switchToCustomSlippage = () => {
fireEvent.click(screen.getByText('Custom')) fireEvent.click(screen.getByText('Custom'))
...@@ -34,21 +27,21 @@ describe('MaxSlippageSettings', () => { ...@@ -34,21 +27,21 @@ describe('MaxSlippageSettings', () => {
}) })
it('is not expanded by default', () => { it('is not expanded by default', () => {
renderSlippageSettings() renderSlippageSettings()
expect(getSlippageInput()).not.toBeInTheDocument() expect(getSlippageInput()).not.toBeVisible()
}) })
it('is expanded by default when custom slippage is set', () => { it('is expanded by default when custom slippage is set', () => {
store.dispatch(updateUserSlippageTolerance({ userSlippageTolerance: 10 })) store.dispatch(updateUserSlippageTolerance({ userSlippageTolerance: 10 }))
renderSlippageSettings() renderSlippageSettings()
expect(getSlippageInput()).toBeInTheDocument() expect(getSlippageInput()).toBeVisible()
}) })
it('does not render auto slippage as a value, but a placeholder', () => { it('does not render auto slippage as a value, but a placeholder', () => {
renderAndExpandSlippageSettings() renderSlippageSettings()
switchToCustomSlippage() switchToCustomSlippage()
expect(getSlippageInput().value).toBe('') expect(getSlippageInput().value).toBe('')
}) })
it('renders custom slippage above the input', () => { it('renders custom slippage above the input', () => {
renderAndExpandSlippageSettings() renderSlippageSettings()
switchToCustomSlippage() switchToCustomSlippage()
fireEvent.change(getSlippageInput(), { target: { value: '0.5' } }) fireEvent.change(getSlippageInput(), { target: { value: '0.5' } })
...@@ -56,7 +49,7 @@ describe('MaxSlippageSettings', () => { ...@@ -56,7 +49,7 @@ describe('MaxSlippageSettings', () => {
expect(screen.queryAllByText('0.50%').length).toEqual(1) expect(screen.queryAllByText('0.50%').length).toEqual(1)
}) })
it('updates input value on blur with the slippage in store', () => { it('updates input value on blur with the slippage in store', () => {
renderAndExpandSlippageSettings() renderSlippageSettings()
switchToCustomSlippage() switchToCustomSlippage()
const input = getSlippageInput() const input = getSlippageInput()
...@@ -66,7 +59,7 @@ describe('MaxSlippageSettings', () => { ...@@ -66,7 +59,7 @@ describe('MaxSlippageSettings', () => {
expect(input.value).toBe('0.50') expect(input.value).toBe('0.50')
}) })
it('clears errors on blur and overwrites incorrect value with the latest correct value', () => { it('clears errors on blur and overwrites incorrect value with the latest correct value', () => {
renderAndExpandSlippageSettings() renderSlippageSettings()
switchToCustomSlippage() switchToCustomSlippage()
const input = getSlippageInput() const input = getSlippageInput()
...@@ -78,7 +71,7 @@ describe('MaxSlippageSettings', () => { ...@@ -78,7 +71,7 @@ describe('MaxSlippageSettings', () => {
expect(input.value).toBe('50.00') expect(input.value).toBe('50.00')
}) })
it('does not allow to enter more than 2 digits after the decimal point', () => { it('does not allow to enter more than 2 digits after the decimal point', () => {
renderAndExpandSlippageSettings() renderSlippageSettings()
switchToCustomSlippage() switchToCustomSlippage()
const input = getSlippageInput() const input = getSlippageInput()
...@@ -88,7 +81,7 @@ describe('MaxSlippageSettings', () => { ...@@ -88,7 +81,7 @@ describe('MaxSlippageSettings', () => {
expect(input.value).toBe('0.01') expect(input.value).toBe('0.01')
}) })
it('does not accept non-numerical values', () => { it('does not accept non-numerical values', () => {
renderAndExpandSlippageSettings() renderSlippageSettings()
switchToCustomSlippage() switchToCustomSlippage()
const input = getSlippageInput() const input = getSlippageInput()
...@@ -97,7 +90,7 @@ describe('MaxSlippageSettings', () => { ...@@ -97,7 +90,7 @@ describe('MaxSlippageSettings', () => {
expect(input.value).toBe('') expect(input.value).toBe('')
}) })
it('does not set slippage when user enters `.` value', () => { it('does not set slippage when user enters `.` value', () => {
renderAndExpandSlippageSettings() renderSlippageSettings()
switchToCustomSlippage() switchToCustomSlippage()
const input = getSlippageInput() const input = getSlippageInput()
......
...@@ -9,13 +9,6 @@ const renderTransactionDeadlineSettings = () => { ...@@ -9,13 +9,6 @@ const renderTransactionDeadlineSettings = () => {
render(<TransactionDeadlineSettings />) 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 const getDeadlineInput = () => screen.queryByTestId('deadline-input') as HTMLInputElement
describe('TransactionDeadlineSettings', () => { describe('TransactionDeadlineSettings', () => {
...@@ -26,26 +19,26 @@ describe('TransactionDeadlineSettings', () => { ...@@ -26,26 +19,26 @@ describe('TransactionDeadlineSettings', () => {
}) })
it('is not expanded by default', () => { it('is not expanded by default', () => {
renderTransactionDeadlineSettings() renderTransactionDeadlineSettings()
expect(getDeadlineInput()).not.toBeInTheDocument() expect(getDeadlineInput()).not.toBeVisible()
}) })
it('is expanded by default when custom deadline is set', () => { it('is expanded by default when custom deadline is set', () => {
store.dispatch(updateUserDeadline({ userDeadline: DEFAULT_DEADLINE_FROM_NOW * 2 })) store.dispatch(updateUserDeadline({ userDeadline: DEFAULT_DEADLINE_FROM_NOW * 2 }))
renderTransactionDeadlineSettings() renderTransactionDeadlineSettings()
expect(getDeadlineInput()).toBeInTheDocument() expect(getDeadlineInput()).toBeVisible()
}) })
it('does not render default deadline as a value, but a placeholder', () => { it('does not render default deadline as a value, but a placeholder', () => {
renderAndExpandTransactionDeadlineSettings() renderTransactionDeadlineSettings()
expect(getDeadlineInput().value).toBe('') expect(getDeadlineInput().value).toBe('')
}) })
it('renders custom deadline above the input', () => { it('renders custom deadline above the input', () => {
renderAndExpandTransactionDeadlineSettings() renderTransactionDeadlineSettings()
fireEvent.change(getDeadlineInput(), { target: { value: '50' } }) fireEvent.change(getDeadlineInput(), { target: { value: '50' } })
expect(screen.queryAllByText('50m').length).toEqual(1) expect(screen.queryAllByText('50m').length).toEqual(1)
}) })
it('marks deadline as invalid if it is greater than 4320m (3 days) or 0m', () => { it('marks deadline as invalid if it is greater than 4320m (3 days) or 0m', () => {
renderAndExpandTransactionDeadlineSettings() renderTransactionDeadlineSettings()
const input = getDeadlineInput() const input = getDeadlineInput()
fireEvent.change(input, { target: { value: '4321' } }) fireEvent.change(input, { target: { value: '4321' } })
...@@ -55,7 +48,7 @@ describe('TransactionDeadlineSettings', () => { ...@@ -55,7 +48,7 @@ describe('TransactionDeadlineSettings', () => {
expect(input.value).toBe('') expect(input.value).toBe('')
}) })
it('clears errors on blur and overwrites incorrect value with the latest correct value', () => { it('clears errors on blur and overwrites incorrect value with the latest correct value', () => {
renderAndExpandTransactionDeadlineSettings() renderTransactionDeadlineSettings()
const input = getDeadlineInput() const input = getDeadlineInput()
fireEvent.change(input, { target: { value: '5' } }) fireEvent.change(input, { target: { value: '5' } })
...@@ -69,7 +62,7 @@ describe('TransactionDeadlineSettings', () => { ...@@ -69,7 +62,7 @@ describe('TransactionDeadlineSettings', () => {
expect(input.value).toBe('5') expect(input.value).toBe('5')
}) })
it('does not accept non-numerical values', () => { it('does not accept non-numerical values', () => {
renderAndExpandTransactionDeadlineSettings() renderTransactionDeadlineSettings()
const input = getDeadlineInput() const input = getDeadlineInput()
fireEvent.change(input, { target: { value: 'c' } }) fireEvent.change(input, { target: { value: 'c' } })
......
...@@ -7,6 +7,7 @@ import type { createPopper } from '@popperjs/core' ...@@ -7,6 +7,7 @@ import type { createPopper } from '@popperjs/core'
import { useWeb3React } from '@web3-react/core' import { useWeb3React } from '@web3-react/core'
import failOnConsole from 'jest-fail-on-console' import failOnConsole from 'jest-fail-on-console'
import { Readable } from 'stream' import { Readable } from 'stream'
import { toBeVisible } from 'test-utils/matchers'
import { mocked } from 'test-utils/mocked' import { mocked } from 'test-utils/mocked'
import { TextDecoder, TextEncoder } from 'util' import { TextDecoder, TextEncoder } from 'util'
...@@ -100,3 +101,7 @@ failOnConsole({ ...@@ -100,3 +101,7 @@ failOnConsole({
shouldFailOnLog: true, shouldFailOnLog: true,
shouldFailOnWarn: true, shouldFailOnWarn: true,
}) })
expect.extend({
toBeVisible,
})
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()
})
})
})
// 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')
},
}
}
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment