package display import ( "errors" "strings" "testing" "github.com/BurntSushi/xgb/shape" "github.com/BurntSushi/xgb/xproto" ) func TestX11FullscreenArgsOrderByRole(t *testing.T) { below := strings.Join(x11FullscreenArgs(0x1a2b, RoleBelow), " ") if !strings.Contains(below, "add,fullscreen,below") { t.Fatalf("args %q must request fullscreen with below stacking", below) } if !strings.Contains(below, "0x00001a2b") { t.Fatalf("args %q must reference the kiosk window id", below) } above := strings.Join(x11FullscreenArgs(0x1a2b, RoleAbove), " ") if strings.Contains(above, "below") { t.Fatalf("args %q must never stack the OSD overlay below video", above) } if !strings.Contains(above, "add,fullscreen,above") { t.Fatalf("args %q must request fullscreen with above stacking", above) } } func TestX11SessionRedrawsOnlyOnExpose(t *testing.T) { if !x11WantsRedraw(xproto.ExposeEvent{}) { t.Fatal("expose events must trigger a redraw") } if x11WantsRedraw(xproto.MapNotifyEvent{}) { t.Fatal("map notify events must not trigger a redraw") } } func TestX11OpenerAppliesFullscreenArgsThroughHook(t *testing.T) { var got [][]string opener := X11Opener{Fullscreen: func(args []string) error { got = append(got, args) return errors.New("wmctrl missing") }} if err := opener.applyFullscreen(x11FullscreenArgs(0x42, RoleBelow)); err == nil { t.Fatal("hook error must propagate for logging") } if len(got) != 1 || !strings.Contains(strings.Join(got[0], " "), "add,fullscreen,below") { t.Fatalf("hook args %v must request fullscreen with below stacking", got) } } func TestClassifyVideoChangeTracksVideoWindowLifecycle(t *testing.T) { const own = xproto.Window(0x10) const video = xproto.Window(0x20) const other = xproto.Window(0x30) change, w := classifyVideoChange(xproto.MapNotifyEvent{Window: video}, own) if change != videoAppeared || w != video { t.Fatalf("map of a foreign window must announce video, got %v %v", change, w) } change, _ = classifyVideoChange(xproto.MapNotifyEvent{Window: own}, own) if change != videoUnrelated { t.Fatalf("map of the kiosk window must be unrelated, got %v", change) } change, w = classifyVideoChange(xproto.UnmapNotifyEvent{Window: video}, own) if change != videoDisappeared || w != video { t.Fatalf("unmap must announce a video end candidate, got %v %v", change, w) } change, _ = classifyVideoChange(xproto.UnmapNotifyEvent{Window: own}, own) if change != videoUnrelated { t.Fatalf("unmap of the kiosk window must be unrelated, got %v", change) } change, w = classifyVideoChange(xproto.UnmapNotifyEvent{Window: other}, own) if change != videoDisappeared || w != other { t.Fatalf("unmap of a foreign window must be an end candidate, got %v %v", change, w) } change, w = classifyVideoChange(xproto.DestroyNotifyEvent{Window: video}, own) if change != videoDisappeared || w != video { t.Fatalf("destroy must announce a video end candidate, got %v %v", change, w) } change, _ = classifyVideoChange(xproto.ExposeEvent{}, own) if change != videoUnrelated { t.Fatalf("expose must not change video presence, got %v", change) } } func TestAddAndRemoveVideoWindowTrackPresenceTransitions(t *testing.T) { const video = xproto.Window(0x20) const second = xproto.Window(0x30) windows := make(map[xproto.Window]struct{}) if !addVideoWindow(windows, video) { t.Fatal("first video window must change presence") } if addVideoWindow(windows, video) { t.Fatal("re-adding the tracked window must not change presence") } if addVideoWindow(windows, second) { t.Fatal("a second video window must not change presence") } if removeVideoWindow(windows, second) { t.Fatal("removing one of two video windows must not change presence") } if !removeVideoWindow(windows, video) { t.Fatal("removing the last video window must change presence") } if removeVideoWindow(windows, video) { t.Fatal("removing an untracked window must not change presence") } if len(windows) != 0 { t.Fatalf("set %v must be empty after all video ended", windows) } } func TestEWMHNonVideoWindowTypesExcludeDesktopHelpers(t *testing.T) { for _, name := range []string{"DOCK", "DESKTOP", "TOOLBAR", "MENU", "UTILITY", "SPLASH", "DIALOG", "NOTIFICATION", "TOOLTIP"} { if !ewmhNonVideoTypes[name] { t.Fatalf("window type %q must not count as video", name) } } for _, name := range []string{"NORMAL", "", "VIDEO_PLAY"} { if ewmhNonVideoTypes[name] { t.Fatalf("window type %q must count as video", name) } } } func TestUTF16Char2bEncodesPolishFallbackText(t *testing.T) { chars := utf16Char2b(wantFallbackText, maxImageText16Units) want := []xproto.Char2b{ {Byte1: 0, Byte2: 'B'}, {Byte1: 0, Byte2: 'r'}, {Byte1: 0, Byte2: 'a'}, {Byte1: 0, Byte2: 'k'}, {Byte1: 0, Byte2: ' '}, {Byte1: 0, Byte2: 'p'}, {Byte1: 0, Byte2: 'o'}, {Byte1: 0x01, Byte2: 0x42}, // ł {Byte1: 0x01, Byte2: 0x05}, // ą {Byte1: 0, Byte2: 'c'}, {Byte1: 0, Byte2: 'z'}, {Byte1: 0, Byte2: 'e'}, {Byte1: 0, Byte2: 'n'}, {Byte1: 0, Byte2: 'i'}, {Byte1: 0, Byte2: 'a'}, {Byte1: 0, Byte2: ' '}, {Byte1: 0, Byte2: 'W'}, {Byte1: 0, Byte2: 'F'}, {Byte1: 0, Byte2: 'B'}, } if len(chars) != len(want) { t.Fatalf("units=%v want %v", chars, want) } for i := range want { if chars[i] != want[i] { t.Fatalf("unit %d=%v want %v", i, chars[i], want[i]) } } } func TestUTF16Char2bTruncatesToOneRequestLength(t *testing.T) { if chars := utf16Char2b(wantFallbackText, 10); len(chars) != 10 { t.Fatalf("units=%d want 10", len(chars)) } } func TestUTF16Char2bNeverSplitsSurrogatePairs(t *testing.T) { // U+1F3A5 encodes as a surrogate pair (0xD83C, 0xDFA5): a cut that would // land between the two units must back off instead of splitting it. if chars := utf16Char2b("a🎥", 2); len(chars) != 1 || chars[0].Byte2 != 'a' { t.Fatalf("units=%v want only the leading ASCII unit", chars) } if chars := utf16Char2b("🎥", 1); len(chars) != 0 { t.Fatalf("units=%v want none, the pair does not fit", chars) } if chars := utf16Char2b("🎥", 2); len(chars) != 2 { t.Fatalf("units=%v want the intact pair", chars) } if chars := utf16Char2b(wantFallbackText, 0); len(chars) != 0 { t.Fatalf("units=%v want none for a zero limit", chars) } } func TestFoldToASCIIStripsPolishDiacritics(t *testing.T) { if got := foldToASCII(wantFallbackText); got != "Brak polaczenia WFB" { t.Fatalf("foldToASCII=%q want %q", got, "Brak polaczenia WFB") } } func TestFoldToASCIIKeepsPlainASCIIUntouched(t *testing.T) { if got := foldToASCII("plain text"); got != "plain text" { t.Fatalf("foldToASCII=%q want %q", got, "plain text") } } func TestX11FontPatternRequestsUnicodeFixedFont(t *testing.T) { if !strings.Contains(iso10646FontPattern, "fixed") { t.Fatalf("font pattern %q must select a fixed font", iso10646FontPattern) } if !strings.Contains(iso10646FontPattern, "iso10646") { t.Fatalf("font pattern %q must select a Unicode encoding for the exact fallback text", iso10646FontPattern) } } func TestLayerMarkerIdentifiesOwnLayerWindows(t *testing.T) { if layerMarkerValue(RoleBelow) == layerMarkerValue(RoleAbove) { t.Fatal("roles must carry distinct marker payloads") } for _, role := range []LayerRole{RoleBelow, RoleAbove} { if !markerIsGroundstation(layerMarkerValue(role)) { t.Fatalf("marker %q must identify our own window", layerMarkerValue(role)) } } for _, foreign := range []string{"", "video", "desktop", "groundstation-fake"} { if markerIsGroundstation(foreign) { t.Fatalf("foreign marker payload %q must not identify our windows", foreign) } } } func TestMarkerWalkBoundsStayBoundedAndCoverFrameDepth(t *testing.T) { depth, nodes := markerWalkBounds() if depth <= 0 || nodes <= 0 { t.Fatalf("marker walk bounds depth=%d nodes=%d must be positive", depth, nodes) } if nodes < depth { t.Fatalf("node budget %d must cover at least a depth-%d ancestor chain", nodes, depth) } // A reparenting window manager puts each of our two layer windows inside // its own frame, so the walk must reach our window below one frame. if depth < 2 { t.Fatalf("marker walk depth %d must cover our window below a window-manager frame", depth) } } func TestEWMHWindowTypeAnnouncesOverlayRoleOnly(t *testing.T) { if name := ewmhWindowTypeName(RoleAbove); !strings.Contains(name, "DOCK") { t.Fatalf("OSD overlay window type %q must be the always-above helper role", name) } if name := ewmhWindowTypeName(RoleBelow); name != "" { t.Fatalf("fallback canvas must keep announcing no window type, got %q", name) } } func TestPresentedVideoDecisionExcludesOwnLayersAndHelpers(t *testing.T) { video := windowFacts{inputOutput: true, viewable: true} if !isPresentedVideoWindow(video) { t.Fatal("a presented output window must count as video") } for name, facts := range map[string]windowFacts{ "input-only": {viewable: true}, "override-redirect": {inputOutput: true, viewable: true, overrideRedirect: true}, "unmapped": {inputOutput: true}, "auxiliary": {inputOutput: true, viewable: true, auxiliary: true}, "groundstation-canvas": {inputOutput: true, viewable: true, groundstation: true}, "groundstation-osd": {inputOutput: true, viewable: true, groundstation: true}, } { if isPresentedVideoWindow(facts) { t.Fatalf("%s window must never count as video", name) } } // Window-manager frames around our layer windows carry no marker of // their own; groundstationLayerWindow finds the marker below the frame, // so framed Ground Station layers land in these same excluded facts. } func TestStackOrderByRoleKeepsLayersOnTheirSideOfVideo(t *testing.T) { if mode := stackModeForVideoAppear(RoleBelow); mode != xproto.StackModeBelow { t.Fatalf("canvas must park below appearing video, got %v", mode) } if mode := stackModeForVideoAppear(RoleAbove); mode != xproto.StackModeAbove { t.Fatalf("OSD overlay must rise above appearing video, got %v", mode) } if !raisesOnVideoEnd(RoleBelow) { t.Fatal("canvas must raise itself when video ends so the fallback returns") } if raisesOnVideoEnd(RoleAbove) { t.Fatal("OSD overlay is already above video and must not restack on video end") } } func TestOSDEventMaskNeverSelectsDeviceInput(t *testing.T) { if osdEventMask&deviceInputEventMask != 0 { t.Fatalf("OSD event mask must not select pointer or key input: %b", osdEventMask) } if osdEventMask&xproto.EventMaskExposure == 0 { t.Fatal("OSD event mask must still select exposure for redraws") } } func TestTransparentShapesEmptyInputAndOutput(t *testing.T) { var input, output bool for _, kind := range transparentShapeKinds() { switch kind { case shape.SkInput: input = true case shape.SkBounding: output = true } } if !input || !output { t.Fatalf("shape kinds %v must empty both the input and the bounding region", transparentShapeKinds()) } }