-
Notifications
You must be signed in to change notification settings - Fork 4.3k
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Block Editor: Improve act()
usage in BlockSwitcher
tests
#46227
Conversation
Size Change: 0 B Total Size: 1.32 MB ℹ️ View Unchanged
|
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Looks good 👍 One day we should solve the animation, too.
@@ -15,13 +15,22 @@ import { copy } from '@wordpress/icons'; | |||
* Internal dependencies | |||
*/ | |||
import { BlockSwitcher, BlockSwitcherDropdownMenu } from '../'; | |||
import { act } from 'react-test-renderer'; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Oh, we didn't remove all of them yet? 🙂
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I think some of those fixes got lost in the thousand rebases 😅 We'll clean them up, don't worry.
name: 'Block Name', | ||
} ); | ||
|
||
expect( menu ).not.toBeVisible(); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This .not.toBeVisible()
check points at another thing a reliable test should watch for. The menu
is invisible not because the popover is not yet positioned, but because its appearance is animated with framer-motion
. At the very beginning the popover has opacity: 0
.
The animation and positioning are completely independent from each other.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Exactly.
expect( menu ).toBeInTheDocument(); | ||
expect( menu ).not.toBeVisible(); | ||
expect( menu ).toBeVisible(); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
At this moment, if you check it with a snapshot, the opacity is not yet 1, but "only" 0.9994. There is an animation frame loop (in framer-motion
) that gradually changes it from 0 to 1. At the moment of this check, i.e., after we're finished waiting for popover positioning, the animation hasn't finished yet.
We should do another wait with:
await waitFor( () => expect(popover).toHaveStyle( { opacity: 1 } );
Or integrate it into toBePositionedPopover
? Making it do a toBePositionedAndFullDisplayedPopover
?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Yep, I was thinking about that. I think this is a worthy improvement, let me work on it.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Okay, this is interesting. I haven't debugged thoroughly yet, but the moment I add the assertion you mentioned, or extend .toBePositionedPopover()
to check for opacity: 1
, I get the act
warnings again!
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I debugged this and the act()
warning is caused by this setHasAnimatedOnce
state update:
gutenberg/packages/components/src/popover/index.tsx
Lines 120 to 123 in 5fcf00d
const onAnimationComplete = useCallback( | |
() => setHasAnimatedOnce( true ), | |
[] | |
); |
When framer-motion
is finishing an animation, it will set the final opacity: 1
style, then will resolve a promise, and then there's a .then( () => props.onAnimationComplete() )
handler. The state update runs one promise tick after the waitFor
for opacity: 1
has finished.
@ciampo I'm curious what are we preventing with this hasAnimatedOnce
guard? My understanding is that with a <motion.div animate={ { opacity: 1 } }>
, the animate
animation runs only once, on mount, and then never again. But a code comment suggests that it runs also on "subsequent prop changes"? What kind of change exactly can re-trigger the animate
animation to run again?
If Popover
didn't have the hasAnimatedOnce
state, waiting for .toHaveStyle( { opacity: 1 } )
would work flawlessly.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Hey @jsnajdr , if I recall correctly that state variable is in place to avoid the animation from re-running when the placement
changes, as this inline comment says:
gutenberg/packages/components/src/popover/index.tsx
Lines 109 to 112 in b120213
// When animating, animate only once (i.e. when the popover is opened), and | |
// do not animate on subsequent prop changes (as it conflicts with | |
// floating-ui's positioning updates). | |
const [ hasAnimatedOnce, setHasAnimatedOnce ] = useState( false ); |
If I remember correctly, without this flag, the animation would run again if the value of the placement
prop (and probably other props too) change.
Feel free to play around with it and remove it if we find a better solution to the problem!
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
without this flag, the animation would run again if the value of the
placement
prop (and probably other props too) change.
This is however not true, at least with the version of framer-motion
that we're using now. The animate
animation runs only on mount, never anywhere else, like on a prop change. When placement
changes, then via placementToMotionAnimationProps
the value of the animate
prop changes, too, but that doesn't restart the animation either.
I verified that when debugging framer-motion
back in December, and also experimentally. On this video, the block inserter popover appears with animation (I made it run slower to make it more visible), but then the switches between top
and bottom
placements are not animated any more:
I think we can either remove hasAnimatedOnce
completely, or use it just to set the is-positioned
class (meaning that the popover has finished positioning and animating).
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Sounds good to me, then! Please do ping me on the PR if you're going to work on it, happy to help with reviewing and testing.
As recognized, there are some flaws with this approach, it's not reliable enough to check if a popover is positioned the way we do. I'll reconsider the approach in another PR. |
What?
This PR improves the
act()
usage inBlockSwitcher
to make the tests more precise and more expressive.Why?
As part of the React 18 upgrade in #45235, we added a bunch of
act()
calls that could be improved and made more specific.How?
Instead of going with
await act( () => Promise.resolve() )
, we change the test to wait for the popover to be positioned before continuing.Testing Instructions
Verify tests still pass:
npm run test:unit block-editor/src/components/block-switcher/test/index.js
Testing Instructions for Keyboard
None
Screenshots or screencast
None