146 lines
4.7 KiB
Go
146 lines
4.7 KiB
Go
//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")
|
|
}
|
|
}
|