Unified · Split
| @@ -34,6 +34,18 @@ scenario( · 33 unmodified lines | ||
| 34 | 34 | // The overlay covers the whole viewport and the dialog panel is centered, |
| 35 | 35 | // so the top-left corner is always outside the panel. |
| 36 | 36 | const clickOutside = async () => { |
| 37 | + // "Visible" is not "listening": Radix arms outside-pointerdown | |
| 38 | + // dismissal via a document listener it attaches in a passive effect | |
| 39 | + // plus a 0ms timer after the content mounts (DismissableLayer). A | |
| 40 | + // busy renderer can service the injected click ahead of that pending | |
| 41 | + // timer, and a pre-arming pointerdown is never seen at all — the | |
| 42 | + // field-free confirm then stays open and the detach wait times out. | |
| 43 | + // Synchronize on the page's own task queue instead of sleeping: the | |
| 44 | + // modal scroll lock proves the effect flush ran (the arming timer is | |
| 45 | + // queued by then), and one 0ms timer round-trip proves that timer | |
| 46 | + // fired, because same-source timers run in FIFO order. | |
| 47 | + await page.waitForFunction(() => document.body.hasAttribute("data-scroll-locked")); | |
| 48 | + await page.evaluate(() => new Promise((resolve) => setTimeout(resolve, 0))); | |
| 37 | 49 | await page.mouse.click(8, 8); |
| 38 | 50 | // Dismissal is synchronous with the pointer event; this beat only gives |
| 39 | 51 | // a wrongly-dismissed dialog time to actually unmount before the |