test: reset pointer-events before each Storybook story (#26081)

fixes DEVEX-402

Alternative to #26077, which aimed to address this flake in
UsersPage.stories.tsx only:

>Unable to perform pointer interaction as the element has
`pointer-events: none`

In the original PR I was going to "fix" the flake by adding
`pointerEventsCheck: Never` like we have in 2 other tests. However,
resetting `pointer-events` during `beforeEach` in our global Vitest
setup removes the need for that `pointerEventsCheck` workaround. 🙂

tl;dr: Each of these 3 tests were failing because of an immediately
preceding test which opens a Radix Dialog, setting `pointer-events:
none` on the body element.

Co-written with Coder Agents. Relevant chat responses:

>**The actual root cause is story isolation.** The previous story
(`UpdateUserRoleSuccess`) opens a Radix Dialog, which triggers Radix's
`DismissableLayer` to set `document.body.style.pointerEvents = "none"`.
When that story completes, Storybook unmounts the React tree, but the
cleanup of `body.style.pointerEvents` is tied to a `useEffect` cleanup
in `DismissableLayer`. If Storybook begins the next story's play
function before the browser has fully flushed the previous unmount
cycle, `document.body.style.pointerEvents` is still `"none"`. The "Open
menu" button, which is a plain table button with no Radix modal layer
above it providing `pointer-events: auto`, inherits `"none"` from body,
and the first click fails.
>
>**Why it's flaky:** The Storybook Vitest plugin
(`@storybook/addon-vitest`) runs stories sequentially in the same
browser page. When `UpdateUserRoleSuccess` finishes, React unmounts the
tree, and `DismissableLayer`'s `useEffect` cleanup should restore
`body.pointerEvents`. But there's a race between unmount cleanup
completing and the next story's play function starting. Sometimes
cleanup wins (test passes), sometimes the play function wins (test
fails).
>
>**[#26077]'s `pointerEventsCheck: Never`** works but is a workaround.
It doesn't address the leaked state, and `UpdateUserRoleSuccess` itself
could be flaky too (if whatever story precedes it leaves the same stale
state). The other stories in the same file (Suspend, Delete, Activate,
ResetPassword) all open Radix Dialogs too, so any of them could leak
this state to the next story.
This commit is contained in:
Andrew Aquino
2026-06-05 12:05:28 -07:00
committed by GitHub
parent e21e3e5826
commit 2ef468ef76
3 changed files with 13 additions and 33 deletions
@@ -1,5 +1,4 @@
import type { Meta, StoryObj } from "@storybook/react-vite";
import { PointerEventsCheckLevel } from "@testing-library/user-event";
import type { FC } from "react";
import { fn, userEvent, within } from "storybook/test";
import {
@@ -96,7 +95,7 @@ export const Member: Story = {
export const ProxySettings: Story = {
play: async ({ canvasElement }) => {
const user = setupUser();
const user = userEvent.setup();
const body = within(canvasElement.ownerDocument.body);
const menuItem = await body.findByRole("menuitem", {
name: /workspace proxy settings/i,
@@ -107,7 +106,7 @@ export const ProxySettings: Story = {
export const UserSettings: Story = {
play: async ({ canvasElement }) => {
const user = setupUser();
const user = userEvent.setup();
const body = within(canvasElement.ownerDocument.body);
const menuItem = await body.findByRole("menuitem", {
name: /user settings/i,
@@ -124,21 +123,12 @@ function withNavbarMock(Story: FC) {
);
}
function setupUser() {
// It seems the dropdown component is disabling pointer events, which is
// causing Testing Library to throw an error. As a workaround, we can
// disable the pointer events check.
return userEvent.setup({
pointerEventsCheck: PointerEventsCheckLevel.Never,
});
}
async function openAdminSettings({
canvasElement,
}: {
canvasElement: HTMLElement;
}) {
const user = setupUser();
const user = userEvent.setup();
const body = within(canvasElement.ownerDocument.body);
const menuItem = await body.findByRole("menuitem", {
name: /admin settings/i,