1package atspi
2
3import (
4 "testing"
5
6 "github.com/stretchr/testify/require"
7
8 "github.com/bnema/neferwl/internal/ports"
9)
10
11func TestToTargetConvertsWindowCoordinates(t *testing.T) {
12 win := ports.Rect{X: 100, Y: 50, W: 800, H: 600}
13 // The frame origin (fx, fy) is subtracted: toolkit offsets cancel out.
14 got, ok := toTarget(win, 10, 20, element{role: roleLink, x: 110, y: 120, w: 40, h: 16})
15 require.True(t, ok)
16 require.Equal(t, ports.HintTarget{Rect: ports.Rect{X: 200, Y: 150, W: 40, H: 16}, Kind: ports.HintLink}, got)
17}
18
19func TestToTargetClipsToWindow(t *testing.T) {
20 win := ports.Rect{X: 100, Y: 50, W: 200, H: 100}
21 got, ok := toTarget(win, 0, 0, element{role: rolePushButton, x: 180, y: 90, w: 60, h: 30})
22 require.True(t, ok)
23 require.Equal(t, ports.Rect{X: 280, Y: 140, W: 20, H: 10}, got.Rect)
24 require.Equal(t, ports.HintButton, got.Kind)
25}
26
27func TestToTargetDropsEmptyAndOutside(t *testing.T) {
28 win := ports.Rect{X: 0, Y: 0, W: 100, H: 100}
29 for name, e := range map[string]element{
30 "zero width": {role: roleLink, x: 1, y: 1, w: 0, h: 10},
31 "neg height": {role: roleLink, x: 1, y: 1, w: 10, h: -1},
32 "outside": {role: roleLink, x: 500, y: 1, w: 10, h: 10},
33 "above": {role: roleLink, x: 1, y: -50, w: 10, h: 10},
34 "left of win": {role: roleLink, x: -30, y: 1, w: 10, h: 10},
35 } {
36 _, ok := toTarget(win, 0, 0, e)
37 require.False(t, ok, name)
38 }
39}
40
41func TestRoleKindMapping(t *testing.T) {
42 require.Equal(t, ports.HintLink, kindOf(roleLink))
43 for _, r := range []uint32{rolePushButton, roleToggleButton, roleCheckBox, roleRadioButton, roleMenuItem, rolePageTab, roleComboBox} {
44 require.Equal(t, ports.HintButton, kindOf(r), r)
45 require.True(t, isClickable(r, false), r)
46 require.False(t, isClickable(r, true), r)
47 }
48 require.True(t, isClickable(roleLink, true))
49 require.False(t, isClickable(roleFrame, false))
50 require.True(t, isWindowRole(roleFrame) && isWindowRole(roleWindow) && isWindowRole(roleDialog))
51 require.False(t, isWindowRole(roleLink))
52}
53
54func TestRoleAndStateBits(t *testing.T) {
55 require.Equal(t, []int32{0, 0, 0, 0, 0}, roleBits(nil))
56 // link is role 88: word 2, bit 24; push button is 43: word 1, bit 11.
57 require.Equal(t, []int32{0, 1 << 11, 1 << 24, 0, 0}, roleBits([]uint32{roleLink, rolePushButton}))
58 // showing is state 25, visible 30: both in word 0.
59 require.Equal(t, []int32{1<<25 | 1<<30, 0}, stateBits(stateShowing, stateVisible))
60}
61
62func wins(ws ...ports.HintWindow) []ports.HintWindow { return ws }
63
64func win(id ports.WindowID, w, h int, focused bool) ports.HintWindow {
65 return ports.HintWindow{ID: id, PID: 1, Rect: ports.Rect{W: w, H: h}, Focused: focused}
66}
67
68func TestMatchFramesSingle(t *testing.T) {
69 got := matchFrames(wins(win(1, 800, 600, false)), []frameInfo{{w: 100, h: 100}})
70 require.Equal(t, map[int]int{0: 0}, got)
71}
72
73func TestMatchFramesActive(t *testing.T) {
74 frames := []frameInfo{{w: 800, h: 600}, {w: 800, h: 600, active: true}}
75 got := matchFrames(wins(win(1, 800, 600, true), win(2, 800, 600, false)), frames)
76 // The focused window takes the ACTIVE frame and reserves it; the other
77 // window then only has the remaining frame to match.
78 require.Equal(t, map[int]int{0: 1, 1: 0}, got)
79}
80
81func TestMatchFramesSize(t *testing.T) {
82 frames := []frameInfo{{w: 1000, h: 700}, {w: 400, h: 300}}
83 got := matchFrames(wins(win(1, 1000, 700, false), win(2, 420, 290, false)), frames)
84 require.Equal(t, map[int]int{0: 0, 1: 1}, got)
85}
86
87func TestMatchFramesSizeTolerance(t *testing.T) {
88 frames := []frameInfo{{w: 840, h: 640}, {w: 100, h: 100}}
89 other := win(2, 90, 90, false)
90 // 40 px off still matches; 41 px does not.
91 require.Equal(t, map[int]int{0: 0, 1: 1}, matchFrames(wins(win(1, 800, 600, false), other), frames))
92 require.Equal(t, map[int]int{1: 1}, matchFrames(wins(win(1, 799, 600, false), other), frames))
93}
94
95func TestMatchFramesAmbiguous(t *testing.T) {
96 // Two windows and two frames of the same size, none active: skipped.
97 frames := []frameInfo{{w: 800, h: 600}, {w: 810, h: 600}}
98 got := matchFrames(wins(win(1, 800, 600, false), win(2, 800, 600, false)), frames)
99 require.Empty(t, got)
100 // One window, two same-size frames, not focused: skipped.
101 require.Empty(t, matchFrames(wins(win(1, 800, 600, false)), frames))
102 // No frame close enough.
103 require.Empty(t, matchFrames(wins(win(1, 200, 200, false), win(2, 300, 300, false)), frames))
104}
105
106// The active frame is reserved by the focused window: an unfocused window of
107// the same size as that frame must not be paired with it as well.
108func TestMatchFramesOneToOne(t *testing.T) {
109 frames := []frameInfo{{ref: ref{"a", "/active"}, active: true, w: 400, h: 300}, {ref: ref{"a", "/idle"}, w: 800, h: 600}}
110 got := matchFrames(wins(win(1, 800, 600, true), win(2, 400, 300, false)), frames)
111 seen := map[int]bool{}
112 for _, f := range got {
113 require.False(t, seen[f], "frame %d assigned twice: %v", f, got)
114 seen[f] = true
115 }
116 require.Equal(t, map[int]int{0: 0}, got)
117}