Unified · Split
| @@ -34,6 +34,18 @@ scenario( · 33 unmodified lines | |||
| 34 | // The overlay covers the whole viewport and the dialog panel is centered, | 34 | // The overlay covers the whole viewport and the dialog panel is centered, |
| 35 | // so the top-left corner is always outside the panel. | 35 | // so the top-left corner is always outside the panel. |
| 36 | const clickOutside = async () => { | 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 | await page.mouse.click(8, 8); | 49 | await page.mouse.click(8, 8); |
| 38 | // Dismissal is synchronous with the pointer event; this beat only gives | 50 | // Dismissal is synchronous with the pointer event; this beat only gives |
| 39 | // a wrongly-dismissed dialog time to actually unmount before the | 51 | // a wrongly-dismissed dialog time to actually unmount before the |