// Copyright (c) 2026 Storm Dragon
// Authors: Storm Dragon, Angus Kola
// SPDX-License-Identifier: GPL-3.0-or-later
package i3ipc
import (
"encoding/json"
"reflect"
"testing"
)
func ptr[T any](v T) *T { return &v }
func testWindow() node {
return node{ID: 10, Window: 100, Name: "Editor", Focused: true, Marks: []string{}, Urgent: ptr(false), Fullscreen: ptr(0), Floating: "auto_off"}
}
func testRoot(n node, ws uint64, name string) node {
return node{ID: 1, Type: "root", Nodes: []node{{ID: ws, Type: "workspace", Name: name, Nodes: []node{n}}}}
}
func applyTest(t *testing.T, s *state, kind uint32, change string, n node, root node) []Event {
t.Helper()
data := map[string]any{"change": change, "container": n, "current": n}
raw, _ := json.Marshal(data)
got, err := s.apply(message{kind, raw}, func() (node, error) { return root, nil })
if err != nil {
t.Fatal(err)
}
return got
}
func TestConfirmedStateChanges(t *testing.T) {
n := testWindow()
var s state
root := testRoot(n, 2, "A")
s.baseline(root, []workspace{{ID: 2, Name: "A", Focused: true}})
if got := applyTest(t, &s, windowEvent, "focus", n, root); len(got) != 0 {
t.Fatal(got)
}
n.Fullscreen = ptr(1)
if got := applyTest(t, &s, windowEvent, "fullscreen_mode", n, root); len(got) != 1 || got[0].Kind != Fullscreen || !got[0].Enabled {
t.Fatal(got)
}
n.Fullscreen = ptr(0)
n.Focused = false
if got := applyTest(t, &s, windowEvent, "fullscreen_mode", n, root); len(got) != 1 || got[0].Enabled || got[0].Window != "Editor" || got[0].Focused {
t.Fatal(got)
}
n.Floating = "user_on"
if got := applyTest(t, &s, windowEvent, "floating", n, root); len(got) != 1 || got[0].Kind != Floating || !got[0].Enabled {
t.Fatal(got)
}
n.Floating = "user_off"
if got := applyTest(t, &s, windowEvent, "floating", n, root); len(got) != 1 || got[0].Enabled {
t.Fatal(got)
}
n.Urgent = ptr(true)
root = testRoot(n, 2, "A")
if got := applyTest(t, &s, windowEvent, "urgent", n, root); len(got) != 1 || got[0].Kind != Urgent || got[0].Workspace != "A" {
t.Fatal(got)
}
if got := applyTest(t, &s, windowEvent, "urgent", n, root); len(got) != 0 {
t.Fatal(got)
}
if got := applyTest(t, &s, workspaceEvent, "urgent", root.Nodes[0], root); len(got) != 0 {
t.Fatal(got)
}
n.Urgent = ptr(false)
if got := applyTest(t, &s, windowEvent, "urgent", n, root); len(got) != 0 {
t.Fatal(got)
}
n.Marks = []string{"z", "a"}
got := applyTest(t, &s, windowEvent, "mark", n, root)
if len(got) != 2 || got[0].Name != "a" || got[1].Name != "z" || got[0].Kind != MarkAdded {
t.Fatal(got)
}
n.Marks = []string{"a", "z"}
if got := applyTest(t, &s, windowEvent, "mark", n, root); len(got) != 0 {
t.Fatal(got)
}
n.Marks = []string{}
if got := applyTest(t, &s, windowEvent, "mark", n, root); len(got) != 2 || got[0].Kind != MarkRemoved {
t.Fatal(got)
}
if got := applyTest(t, &s, windowEvent, "move", n, root); len(got) != 0 {
t.Fatal(got)
}
root = testRoot(n, 3, "résumé & 99")
if got := applyTest(t, &s, windowEvent, "move", n, root); len(got) != 1 || got[0].Kind != WorkspaceMove || got[0].Workspace != "résumé & 99" {
t.Fatal(got)
}
root = testRoot(n, 4, "__i3_scratch")
if got := applyTest(t, &s, windowEvent, "move", n, root); len(got) != 0 {
t.Fatal(got)
}
root = testRoot(n, 3, "B")
if got := applyTest(t, &s, windowEvent, "move", n, root); len(got) != 0 {
t.Fatal(got)
}
}
func TestWorkspacesAndModes(t *testing.T) {
n := testWindow()
var s state
s.baseline(testRoot(n, 2, "A"), []workspace{{ID: 2, Focused: true}})
for _, change := range []string{"focus", "rename"} {
ws := node{ID: 2, Name: "empty & arbitrary", Type: "workspace"}
if got := applyTest(t, &s, workspaceEvent, change, ws, node{}); len(got) != 1 || got[0].Kind != Workspace || got[0].Name != ws.Name {
t.Fatal(got)
}
}
if got := applyTest(t, &s, workspaceEvent, "rename", node{ID: 3, Name: "background"}, node{}); len(got) != 0 {
t.Fatal(got)
}
raw := []byte(`{"change":"personal & more","pango_markup":true}`)
got, err := s.apply(message{modeEvent, raw}, nil)
want := []Event{{Kind: Mode, Name: "personal & more", Markup: true}}
if err != nil || !reflect.DeepEqual(got, want) {
t.Fatalf("%+v %v", got, err)
}
}
func TestUnknownContextAndMissingEvidence(t *testing.T) {
n := testWindow()
var s state
s.baseline(node{}, nil)
if got := applyTest(t, &s, windowEvent, "mark", n, node{}); len(got) != 0 {
t.Fatal(got)
}
if got := applyTest(t, &s, windowEvent, "move", n, testRoot(n, 3, "B")); len(got) != 0 {
t.Fatal(got)
}
n.Fullscreen = nil
n.Floating = "unknown"
for _, change := range []string{"fullscreen_mode", "floating", "new", "close", "title"} {
if got := applyTest(t, &s, windowEvent, change, n, node{}); len(got) != 0 {
t.Fatal(got)
}
}
n = testWindow()
n.Urgent = ptr(true)
if got := applyTest(t, &s, windowEvent, "urgent", n, node{Type: "root"}); len(got) != 1 || got[0].Workspace != "" {
t.Fatal(got)
}
}
func TestScratchpadImplicitFloatingSuppressed(t *testing.T) {
n := testWindow()
var s state
s.baseline(testRoot(n, 2, "A"), nil)
n.Floating = "user_on" // i3 reports this before assigning the scratchpad.
if got := applyTest(t, &s, windowEvent, "floating", n, testRoot(n, 4, "__i3_scratch")); len(got) != 0 {
t.Fatal(got)
}
if got := applyTest(t, &s, windowEvent, "move", n, testRoot(n, 4, "__i3_scratch")); len(got) != 0 {
t.Fatal(got)
}
if got := applyTest(t, &s, windowEvent, "move", n, testRoot(n, 2, "A")); len(got) != 0 {
t.Fatal(got)
}
}