package ui import ( "context" "testing" "time" "gioui.org/layout" "gioui.org/widget/material" ) func TestShellStartsWithSemanticInstanceStatuses(t *testing.T) { shell := NewShell(material.NewTheme()) seen := map[string]bool{} for _, row := range shell.rows { seen[row.Status] = true } for _, status := range []string{"运行中", "启动中", "外部占用", "已退出"} { if !seen[status] { t.Fatalf("status %q is missing", status) } } } func TestShellInstanceListScrollsVertically(t *testing.T) { shell := NewShell(material.NewTheme()) if shell.list.Axis != layout.Vertical { t.Fatalf("instance list axis = %v, want vertical", shell.list.Axis) } } func TestShellInstanceRowsContainRequiredColumnsAndIndependentActionControls(t *testing.T) { shell := NewShell(material.NewTheme()) if len(shell.rows) < 2 { t.Fatal("expected fixture rows") } for _, row := range shell.rows { if row.ID == "" || row.Name == "" || row.Browser == "" || row.UserDataDir == "" || row.Status == "" { t.Fatalf("incomplete instance row: %#v", row) } } first := shell.startClickFor(shell.rows[0].ID) second := shell.startClickFor(shell.rows[1].ID) if first == second { t.Fatal("rows share a start button state") } if shell.deleteClickFor(shell.rows[0].ID) == shell.deleteClickFor(shell.rows[1].ID) { t.Fatal("rows share a delete button state") } } func TestInstanceListColumnWeightsPrioritizeDirectoryAndCompactAction(t *testing.T) { got := instanceNameColumnWeight + instanceBrowserColumnWeight + instanceDirectoryColumnWeight + instanceStatusColumnWeight + instanceActionColumnWeight if got != 100 { t.Fatalf("instance column weights = %d, want 100", got) } if instanceDirectoryColumnWeight <= instanceNameColumnWeight { t.Fatal("user data directory must have the widest instance-list column") } if instanceActionColumnWeight <= instanceStatusColumnWeight { t.Fatal("actions column must fit both compact icon commands") } } func TestShellDeletesOnlyTheRequestedInstance(t *testing.T) { shell := NewShell(material.NewTheme()) target := shell.rows[1] remaining := shell.rows[2].ID shell.startClickFor(target.ID) shell.deleteClickFor(target.ID) shell.deleteInstance(target.ID) if len(shell.rows) != 3 { t.Fatalf("instance count = %d, want 3", len(shell.rows)) } for _, row := range shell.rows { if row.ID == target.ID { t.Fatalf("deleted instance %q remains in the list", target.ID) } } if _, exists := shell.startClicks[target.ID]; exists { t.Fatal("deleted instance start control was not released") } if _, exists := shell.deleteClicks[target.ID]; exists { t.Fatal("deleted instance delete control was not released") } if shell.rows[1].ID != remaining { t.Fatalf("remaining instance id = %q, want %q", shell.rows[1].ID, remaining) } } func TestShellCancelsOnlyTheActivePathSearch(t *testing.T) { shell := NewShell(material.NewTheme()) started := make(chan struct{}, 1) shell.OnPathSearch(func(ctx context.Context, field PathField, current string) (string, error) { started <- struct{}{} <-ctx.Done() return "", ctx.Err() }, nil) shell.togglePathSearch(PathChromeExecutable) if got := shell.searchButtonLabel(PathChromeExecutable); got != "取消" { t.Fatalf("chrome search label = %q, want 取消", got) } if got := shell.searchButtonLabel(PathEdgeExecutable); got != "搜索" { t.Fatalf("edge search label = %q, want 搜索", got) } select { case <-started: case <-time.After(time.Second): t.Fatal("search did not start") } shell.togglePathSearch(PathChromeExecutable) if got := shell.searchButtonLabel(PathChromeExecutable); got != "搜索" { t.Fatalf("chrome search label = %q after cancel, want 搜索", got) } } func TestShellAppliesLatestPathSearchResult(t *testing.T) { shell := NewShell(material.NewTheme()) shell.OnPathSearch(func(context.Context, PathField, string) (string, error) { return `C:\Browser\chrome.exe`, nil }, nil) shell.togglePathSearch(PathChromeExecutable) var result pathSearchResult select { case result = <-shell.searchResults: case <-time.After(time.Second): t.Fatal("search did not produce a result") } shell.searchResults <- result shell.consumeSearchResults() if got := shell.chromePath.Text(); got != `C:\Browser\chrome.exe` { t.Fatalf("chrome path = %q", got) } } func TestShellSettingsCanBeRestored(t *testing.T) { shell := NewShell(material.NewTheme()) shell.SetSettings(SettingsState{ChromePath: "chrome", EdgePath: "edge", DefaultDir: "profiles", LogDir: "logs", CloseOnExit: false}) if shell.chromePath.Text() != "chrome" || shell.edgePath.Text() != "edge" || shell.dataDir.Text() != "profiles" || shell.logDir.Text() != "logs" || shell.closeOnExit.Value { t.Fatalf("settings were not restored") } } func TestShellKeepsSettingsStateAcrossPageSwitch(t *testing.T) { shell := NewShell(material.NewTheme()) shell.page = pageSettings shell.dataDir.SetText(`D:\profiles\current`) shell.pathFeedback = "正在搜索默认 User Data Dir…" shell.page = pageInstances shell.page = pageSettings if got := shell.dataDir.Text(); got != `D:\profiles\current` { t.Fatalf("data dir = %q", got) } if got := shell.pathFeedback; got != "正在搜索默认 User Data Dir…" { t.Fatalf("path feedback = %q", got) } } func TestShellIgnoresCancelledPathSearchResult(t *testing.T) { shell := NewShell(material.NewTheme()) started := make(chan struct{}, 1) shell.OnPathSearch(func(ctx context.Context, field PathField, current string) (string, error) { started <- struct{}{} <-ctx.Done() return `C:\stale\chrome.exe`, nil }, nil) shell.togglePathSearch(PathChromeExecutable) select { case <-started: case <-time.After(time.Second): t.Fatal("search did not start") } shell.togglePathSearch(PathChromeExecutable) select { case result := <-shell.searchResults: shell.searchResults <- result case <-time.After(time.Second): t.Fatal("cancelled search did not return") } shell.consumeSearchResults() if got := shell.chromePath.Text(); got == `C:\stale\chrome.exe` { t.Fatal("cancelled result overwrote the editor") } } func TestShellAppliesSelectedInstanceDirectory(t *testing.T) { shell := NewShell(material.NewTheme()) shell.OnChooseDirectory(func(context.Context) (string, error) { return `C:\profiles\new-instance`, nil }, nil) shell.chooseInstanceDirectory() var result directoryPickResult select { case result = <-shell.directoryResults: case <-time.After(time.Second): t.Fatal("directory picker did not produce a result") } shell.directoryResults <- result shell.consumeDirectoryResults() if got := shell.instanceDir.Text(); got != `C:\profiles\new-instance` { t.Fatalf("instance dir = %q", got) } }