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