import { existsSync } from 'node:fs'; import path from 'node:path'; import { render, screen, waitFor } from '@testing-library/react'; import userEvent from '@testing-library/user-event'; import * as React from 'react'; import { describe, expect, it } from 'vitest'; /** * The one thing that only the *built* package can prove. * * `dependencies` is empty because tsup compiles Radix into `dist` instead of * leaving it to a consumer's install (`noExternal` in tsup.config.ts). That is * safe only while the build keeps Radix a **single instance**: the dismissable * layer stack, react-remove-scroll's sidecar and aria-hidden's counter are all * module-level state, so if code-splitting ever stopped hoisting them into one * shared chunk, each entry would get a private copy and layered overlays would * come apart — Escape closing the dialog underneath instead of the menu on top, * a scroll lock that never releases. * * Every other suite in this kit imports from `src`, where that failure cannot * appear: there is one module graph and Vite resolves it once. So this file * imports the two entries from `dist` and puts a menu inside a dialog, which is * the smallest arrangement that needs them to agree. * * It is skipped when `dist` is absent, so a fresh clone can run `test:run` * before `build`; CI builds first, and so does `prepublishOnly`. * * Both cases carry explicit timeouts. Unlike every other suite here they pull * the built bundles in through a dynamic `import()`, which makes Vite transform * a few hundred kB before the first assertion — enough to overrun Vitest's 5s * default on a loaded machine and turn a contract guard into a flake that * someone eventually mutes. */ const TEST_TIMEOUT = 20_000; const WAIT = { timeout: 5_000 }; const DIST = path.resolve(__dirname, '../dist'); const built = existsSync(path.join(DIST, 'components/dialog/index.js')); describe.skipIf(!built)('built dist — Radix module state is shared', () => { it('keeps a menu inside a dialog on its own layer', { timeout: TEST_TIMEOUT }, async () => { const dialog = await import(/* @vite-ignore */ `${DIST}/components/dialog/index.js`); const menu = await import(/* @vite-ignore */ `${DIST}/components/dropdown-menu/index.js`); const { Dialog, DialogContent, DialogTitle } = dialog; const { DropdownMenu, DropdownMenuTrigger, DropdownMenuContent, DropdownMenuItem } = menu; const user = userEvent.setup(); function Layered() { const [menuOpen, setMenuOpen] = React.useState(false); const [dialogOpen, setDialogOpen] = React.useState(true); return ( Layered Open menu Item ); } render(); expect(screen.getByText('Layered')).toBeInTheDocument(); await user.click(screen.getByText('Open menu')); await waitFor(() => expect(screen.getByText('Item')).toBeInTheDocument(), WAIT); /* The assertion. With two copies of the layer stack neither knows about the other, so this Escape reaches the dialog as well. */ await user.keyboard('{Escape}'); await waitFor(() => expect(screen.queryByText('Item')).not.toBeInTheDocument(), WAIT); expect(screen.getByText('Layered')).toBeInTheDocument(); }); it('keeps one scroll lock across two overlays and releases it', { timeout: TEST_TIMEOUT }, async () => { const dialog = await import(/* @vite-ignore */ `${DIST}/components/dialog/index.js`); const menu = await import(/* @vite-ignore */ `${DIST}/components/dropdown-menu/index.js`); const { Dialog, DialogContent, DialogTitle } = dialog; const { DropdownMenu, DropdownMenuTrigger, DropdownMenuContent, DropdownMenuItem } = menu; const user = userEvent.setup(); /* react-remove-scroll locks by putting `data-scroll-locked` on and injecting one stylesheet through react-style-singleton. Both are module state, so a second copy of the package shows up as a second