Files
autoPriority/tray_integration_test.go
T

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")
}
}