@@ -3,11 +3,18 @@ import "@testing-library/jest-dom";
33import { act , cleanup , fireEvent , render , screen , waitFor , within } from "@testing-library/react" ;
44import userEvent from "@testing-library/user-event" ;
55import { StrictMode } from "react" ;
6+ import { toast } from "sonner" ;
67import { afterEach , beforeEach , describe , expect , it , vi } from "vitest" ;
78import { nativeBridgeClient } from "@/native" ;
89import type { NativePlatform } from "@/native/contracts" ;
910import { TooltipProvider } from "../ui/tooltip" ;
10- import { HUD_BAR_BOTTOM , HUD_POPOVER_GAP , HUD_POPOVER_MAX_HEIGHT } from "./hudGeometry" ;
11+ import {
12+ HUD_BAR_BOTTOM ,
13+ HUD_POPOVER_GAP ,
14+ HUD_POPOVER_MAX_HEIGHT ,
15+ HUD_STACK_GAP ,
16+ HUD_TOAST_WIDTH ,
17+ } from "./hudGeometry" ;
1118import { LaunchWindow } from "./LaunchWindow" ;
1219
1320type SelectedSourceChangedListener = Parameters <
@@ -2206,3 +2213,137 @@ describe("LaunchWindow software encoder fallback notice", () => {
22062213 expect ( recorderState . value . dismissSoftwareEncoderFallbackNotice ) . toHaveBeenCalledWith ( true ) ;
22072214 } ) ;
22082215} ) ;
2216+
2217+ // #1149: sonner's default corner of the HUD window is where the bar is, so a toast covered
2218+ // Record, Pause and Stop, and the window being click-through there passed a click on its
2219+ // close button to the app behind.
2220+ describe ( "LaunchWindow toasts" , ( ) => {
2221+ const DISK_SPACE = "Not enough disk space to start recording (only 298 MB free)." ;
2222+
2223+ beforeEach ( ( ) => {
2224+ platformState . value = "darwin" ;
2225+ resetLaunchMocks ( ) ;
2226+ resizeCallbacks . length = 0 ;
2227+ vi . stubGlobal ( "ResizeObserver" , CapturingResizeObserver ) ;
2228+ } ) ;
2229+
2230+ afterEach ( ( ) => {
2231+ act ( ( ) => {
2232+ toast . dismiss ( ) ;
2233+ } ) ;
2234+ cleanup ( ) ;
2235+ vi . unstubAllGlobals ( ) ;
2236+ } ) ;
2237+
2238+ async function showToast ( ) {
2239+ act ( ( ) => {
2240+ toast . error ( DISK_SPACE ) ;
2241+ } ) ;
2242+ const text = await screen . findByText ( DISK_SPACE ) ;
2243+ const item = text . closest ( "[data-sonner-toast]" ) as HTMLElement ;
2244+ const list = text . closest ( "[data-sonner-toaster]" ) as HTMLElement ;
2245+ return { item, list } ;
2246+ }
2247+
2248+ async function stubBar ( width : number , height : number ) {
2249+ const bar = ( await screen . findByTestId ( "hud-drag-handle" ) ) . closest (
2250+ "[data-tray-layout]" ,
2251+ ) as HTMLElement ;
2252+ stubBox ( bar , width , height ) ;
2253+ return bar ;
2254+ }
2255+
2256+ it ( "centres the toasts above the bar instead of in the window's corner" , async ( ) => {
2257+ renderLaunchWindow ( ) ;
2258+ await stubBar ( 400 , 56 ) ;
2259+ await flushResizeObservers ( ) ;
2260+
2261+ const { list } = await showToast ( ) ;
2262+
2263+ expect ( list ) . toHaveAttribute ( "data-y-position" , "bottom" ) ;
2264+ expect ( list ) . toHaveAttribute ( "data-x-position" , "center" ) ;
2265+ expect ( list . style . getPropertyValue ( "--offset-bottom" ) ) . toBe (
2266+ `${ HUD_BAR_BOTTOM + 56 + HUD_STACK_GAP } px` ,
2267+ ) ;
2268+ } ) ;
2269+
2270+ it ( "moves the toasts above a popover opened over the bar" , async ( ) => {
2271+ renderLaunchWindow ( ) ;
2272+ const bar = await stubBar ( 400 , 56 ) ;
2273+ await flushResizeObservers ( ) ;
2274+ const { list } = await showToast ( ) ;
2275+
2276+ fireEvent . click ( screen . getByRole ( "button" , { name : "Language: English" } ) ) ;
2277+ await screen . findByTestId ( "hud-language-menu" ) ;
2278+ const stackHeight = 56 + HUD_POPOVER_GAP + 300 ;
2279+ stubBox ( bar . parentElement as HTMLElement , 400 , stackHeight ) ;
2280+ await flushResizeObservers ( ) ;
2281+
2282+ expect ( list . style . getPropertyValue ( "--offset-bottom" ) ) . toBe (
2283+ `${ HUD_BAR_BOTTOM + stackHeight + HUD_STACK_GAP } px` ,
2284+ ) ;
2285+ } ) ;
2286+
2287+ it ( "marks a toast interactive, so the pushed cursor over it lifts click-through" , async ( ) => {
2288+ renderLaunchWindow ( ) ;
2289+ await waitFor ( ( ) => {
2290+ expect ( window . electronAPI . setHudOverlayIgnoreMouseEvents ) . toHaveBeenLastCalledWith ( true ) ;
2291+ } ) ;
2292+ const { item } = await showToast ( ) ;
2293+
2294+ expect ( item . closest ( "[data-hud-interactive='true']" ) ) . toBe ( screen . getByTestId ( "hud-toaster" ) ) ;
2295+
2296+ // jsdom has no hit test: the point resolves to the toast's close button, as it
2297+ // would in the browser.
2298+ const closeButton = item . querySelector ( "[data-close-button]" ) ;
2299+ expect ( closeButton ) . not . toBeNull ( ) ;
2300+ Object . defineProperty ( document , "elementFromPoint" , {
2301+ value : vi . fn ( ( ) => closeButton ) ,
2302+ configurable : true ,
2303+ } ) ;
2304+ const setIgnore = vi . mocked ( window . electronAPI . setHudOverlayIgnoreMouseEvents ) ;
2305+ try {
2306+ setIgnore . mockClear ( ) ;
2307+ for ( const listener of hudCursorListeners ) listener ( 450 , 300 ) ;
2308+ expect ( setIgnore ) . toHaveBeenCalledWith ( false ) ;
2309+ } finally {
2310+ Reflect . deleteProperty ( document , "elementFromPoint" ) ;
2311+ }
2312+ } ) ;
2313+
2314+ it ( "keeps the toasts in the rect the main process holds on screen" , async ( ) => {
2315+ renderLaunchWindow ( ) ;
2316+ // A vertical bar, narrower than a toast.
2317+ await stubBar ( 48 , 480 ) ;
2318+ await flushResizeObservers ( ) ;
2319+ const { list } = await showToast ( ) ;
2320+ // Sonner writes the front toast's height on its list once it has measured it; jsdom
2321+ // has no layout, so the measurement is written here.
2322+ list . style . setProperty ( "--front-toast-height" , "64px" ) ;
2323+
2324+ const sizeCalls = vi . mocked ( window . electronAPI . setHudOverlaySize ) . mock . calls ;
2325+ const [ allocatedWidth , allocatedHeight ] = sizeCalls [ sizeCalls . length - 1 ] ;
2326+ const height = 480 + HUD_STACK_GAP + 64 ;
2327+ await waitFor ( ( ) => {
2328+ expect ( window . electronAPI . setHudOverlayContent ) . toHaveBeenLastCalledWith ( {
2329+ x : ( allocatedWidth - HUD_TOAST_WIDTH ) / 2 ,
2330+ y : allocatedHeight - HUD_BAR_BOTTOM - height ,
2331+ width : HUD_TOAST_WIDTH ,
2332+ height,
2333+ } ) ;
2334+ } ) ;
2335+
2336+ // And out of it once the toast is gone.
2337+ act ( ( ) => {
2338+ toast . dismiss ( ) ;
2339+ } ) ;
2340+ await waitFor ( ( ) => {
2341+ expect ( window . electronAPI . setHudOverlayContent ) . toHaveBeenLastCalledWith ( {
2342+ x : ( allocatedWidth - 48 ) / 2 ,
2343+ y : allocatedHeight - HUD_BAR_BOTTOM - 480 ,
2344+ width : 48 ,
2345+ height : 480 ,
2346+ } ) ;
2347+ } ) ;
2348+ } ) ;
2349+ } ) ;
0 commit comments