//go:build windows package main import ( "context" "sync" "testing" "time" "unsafe" ) type guiThreadInfo struct { Size, Flags uint32 Active, Focus, Capture, MenuOwner, MoveSize, Caret uintptr CaretRect winRect } func waitNative(t *testing.T, condition func() bool, what string) { t.Helper() deadline := time.Now().Add(3 * time.Second) for time.Now().Before(deadline) { if condition() { return } time.Sleep(10 * time.Millisecond) } t.Fatal("timeout: " + what) } func postOwned(t *testing.T, h uintptr, msg uint32, wp, lp uintptr) { t.Helper() ok, _, err := pPostMessage.Call(h, uintptr(msg), wp, lp) if ok == 0 { t.Fatal(err) } } func ownIconRect(h uintptr) (winRect, bool) { id := notifyIconIdentifier{Size: uint32(unsafe.Sizeof(notifyIconIdentifier{})), Window: h, ID: trayIconID} var rect winRect hr, _, _ := pNotifyRect.Call(uintptr(unsafe.Pointer(&id)), uintptr(unsafe.Pointer(&rect))) return rect, hr == 0 } func ownMenuActive(thread uintptr) bool { info := guiThreadInfo{Size: uint32(unsafe.Sizeof(guiThreadInfo{}))} ok, _, _ := u32.NewProc("GetGUIThreadInfo").Call(thread, uintptr(unsafe.Pointer(&info))) return ok != 0 && info.Flags&(0x4|0x10) != 0 } func TestNativeTrayMouseKeyboardRestartAndControllerExit(t *testing.T) { requireNativeGUI(t) ctx, cancel := context.WithCancel(context.Background()) defer cancel() restored := make(chan struct{}) tr, err := newTray(cancel, restored) if err != nil { t.Fatal(err) } defer tr.Close() h := tr.hwnd.Load() thread, _, _ := u32.NewProc("GetWindowThreadProcessId").Call(h, 0) f := newFake() p := f.add(10, 150, PriorityClassNormal) changed := make(chan struct{}) var once sync.Once finished := make(chan error, 1) go func() { err := runController(ctx, options{mem: 100, interval: time.Minute}, f, func(r scanReport) error { if len(r.Rows) > 0 { once.Do(func() { close(changed) }) } return nil }) close(restored) finished <- err }() select { case <-changed: case <-time.After(3 * time.Second): t.Fatal("controller did not start") } waitNative(t, func() bool { _, ok := ownIconRect(h); return ok }, "notification icon registration") // Post only to OUR window. No mouse injection, no interaction with other applications. for _, event := range []uint32{0x400, 0x7b, 0x208, 0x20c} { rect, _ := ownIconRect(h) wp := uintptr(uint16(rect.Left)) | uintptr(uint16(rect.Top))<<16 postOwned(t, h, trayCallbackMessage, wp, uintptr(event)|trayIconID<<16) waitNative(t, func() bool { return ownMenuActive(thread) }, "popup for a mouse button") postOwned(t, h, 0x1f, 0, 0) waitNative(t, func() bool { return !tr.menuOpen.Load() }, "menu cancellation") select { case <-ctx.Done(): t.Fatal("cancelling menu unexpectedly exited application") default: } } // Simulate loss of THIS icon, not an actual restart of the user's Explorer. data := notifyIconData{Size: uint32(unsafe.Sizeof(notifyIconData{})), Window: h, ID: trayIconID} removed, _, _ := pShellNotify.Call(2, uintptr(unsafe.Pointer(&data))) if removed == 0 { t.Fatal("could not remove own test icon") } postOwned(t, h, tr.taskbar, 0, 0) waitNative(t, func() bool { _, ok := ownIconRect(h); return ok }, "icon recreation after TaskbarCreated") // The keyboard notification opens the same real menu; select its only item. postOwned(t, h, trayCallbackMessage, 0, 0x401|trayIconID<<16) waitNative(t, func() bool { return ownMenuActive(thread) }, "keyboard menu") postOwned(t, h, 0x100, 0x28, 0) // VK_DOWN postOwned(t, h, 0x100, 0x0d, 0) // VK_RETURN select { case err = <-finished: if err != nil { t.Fatal(err) } case <-time.After(3 * time.Second): t.Fatal("menu selection did not finish restoration") } assertPriority(t, p, PriorityClassNormal) assertClosed(t, f) if err = tr.Close(); err != nil { t.Fatal(err) } if _, ok := ownIconRect(h); ok { t.Fatal("notification icon was not removed") } } func TestTrayCoordinatesAndNativeLayout(t *testing.T) { x, y := int16(-1200), int16(1400) wp := uintptr(uint16(x)) | uintptr(uint16(y))<<16 event, id, point, anchored := trayEvent(true, wp, 0x400|trayIconID<<16) if event != 0x400 || id != trayIconID || point.X != -1200 || point.Y != 1400 || !anchored { t.Fatal("signed multi-monitor anchor lost") } _, _, _, anchored = trayEvent(true, wp, 0x7b|trayIconID<<16) if anchored { t.Fatal("WM_CONTEXTMENU wParam is undefined in the documented v4 contract") } size := unsafe.Sizeof(uintptr(0)) want := map[uintptr]uintptr{4: 956, 8: 976}[size] if unsafe.Sizeof(notifyIconData{}) != want { t.Fatal("NOTIFYICONDATAW layout mismatch") } if unsafe.Sizeof(bitmapHeader{}) != 40 { t.Fatal("BITMAPINFOHEADER layout mismatch") } }