299 lines
9.4 KiB
Go
299 lines
9.4 KiB
Go
package ui
|
|
|
|
import (
|
|
"context"
|
|
"sync/atomic"
|
|
"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 TestShellDeletesOnlyAfterConfirmation(t *testing.T) {
|
|
shell := NewShell(material.NewTheme())
|
|
target := shell.rows[1]
|
|
changes := 0
|
|
shell.OnInstancesChanged(func([]InstanceRow) { changes++ })
|
|
|
|
shell.requestDelete(target.ID)
|
|
if shell.pendingDeleteID != target.ID {
|
|
t.Fatalf("pending delete = %q, want %q", shell.pendingDeleteID, target.ID)
|
|
}
|
|
if len(shell.rows) != 4 {
|
|
t.Fatalf("instance count changed before confirmation: %d", len(shell.rows))
|
|
}
|
|
shell.cancelDelete()
|
|
if shell.pendingDeleteID != "" || len(shell.rows) != 4 || changes != 0 {
|
|
t.Fatalf("cancel delete state = pending %q, rows %d, changes %d", shell.pendingDeleteID, len(shell.rows), changes)
|
|
}
|
|
|
|
shell.requestDelete(target.ID)
|
|
shell.confirmDelete()
|
|
if shell.pendingDeleteID != "" || len(shell.rows) != 3 || changes != 1 {
|
|
t.Fatalf("confirm delete state = pending %q, rows %d, changes %d", shell.pendingDeleteID, len(shell.rows), changes)
|
|
}
|
|
}
|
|
|
|
func TestShellStartsInstanceAsynchronouslyAndAppliesResult(t *testing.T) {
|
|
shell := NewShell(material.NewTheme())
|
|
target := shell.rows[0]
|
|
started := make(chan InstanceRow, 1)
|
|
shell.OnStartInstance(func(_ context.Context, row InstanceRow, _ SettingsState) (int, error) {
|
|
started <- row
|
|
return 4242, nil
|
|
}, nil)
|
|
|
|
shell.requestStart(target.ID)
|
|
if row, ok := shell.instanceRow(target.ID); !ok || row.Status != "启动中" {
|
|
t.Fatalf("start status = %#v, want 启动中", row)
|
|
}
|
|
select {
|
|
case got := <-started:
|
|
if got.ID != target.ID {
|
|
t.Fatalf("started instance = %q, want %q", got.ID, target.ID)
|
|
}
|
|
case <-time.After(time.Second):
|
|
t.Fatal("start callback was not invoked")
|
|
}
|
|
var result instanceStartResult
|
|
select {
|
|
case result = <-shell.startResults:
|
|
case <-time.After(time.Second):
|
|
t.Fatal("start result was not produced")
|
|
}
|
|
shell.startResults <- result
|
|
shell.consumeStartResults()
|
|
if row, ok := shell.instanceRow(target.ID); !ok || row.Status != "运行中" {
|
|
t.Fatalf("completed start status = %#v, want 运行中", row)
|
|
}
|
|
}
|
|
|
|
func TestShellDoesNotStartTheSameInstanceTwiceWhilePending(t *testing.T) {
|
|
shell := NewShell(material.NewTheme())
|
|
target := shell.rows[0]
|
|
entered := make(chan struct{}, 1)
|
|
release := make(chan struct{})
|
|
var calls atomic.Int32
|
|
shell.OnStartInstance(func(_ context.Context, _ InstanceRow, _ SettingsState) (int, error) {
|
|
calls.Add(1)
|
|
entered <- struct{}{}
|
|
<-release
|
|
return 4242, nil
|
|
}, nil)
|
|
|
|
shell.requestStart(target.ID)
|
|
shell.requestStart(target.ID)
|
|
select {
|
|
case <-entered:
|
|
case <-time.After(time.Second):
|
|
t.Fatal("start callback was not invoked")
|
|
}
|
|
if calls.Load() != 1 {
|
|
t.Fatalf("start calls = %d, want 1", calls.Load())
|
|
}
|
|
close(release)
|
|
select {
|
|
case result := <-shell.startResults:
|
|
shell.startResults <- result
|
|
shell.consumeStartResults()
|
|
case <-time.After(time.Second):
|
|
t.Fatal("start result was not produced")
|
|
}
|
|
}
|
|
|
|
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)
|
|
}
|
|
}
|