2026-07-16 15:43:03 +08:00
|
|
|
package gio
|
|
|
|
|
|
|
|
|
|
import (
|
2026-07-16 17:10:42 +08:00
|
|
|
"fmt"
|
2026-07-16 15:43:03 +08:00
|
|
|
"image"
|
|
|
|
|
"testing"
|
|
|
|
|
|
|
|
|
|
"gioui.org/layout"
|
|
|
|
|
"gioui.org/op"
|
|
|
|
|
"gioui.org/unit"
|
2026-07-16 17:10:42 +08:00
|
|
|
|
|
|
|
|
"softbox.local/core/application"
|
|
|
|
|
"softbox.local/core/domain"
|
2026-07-16 15:43:03 +08:00
|
|
|
)
|
|
|
|
|
|
|
|
|
|
func TestAppShellFillsWindow(t *testing.T) {
|
|
|
|
|
var operations op.Ops
|
|
|
|
|
size := image.Pt(1080, 720)
|
|
|
|
|
context := layout.Context{
|
|
|
|
|
Ops: &operations,
|
|
|
|
|
Metric: unit.Metric{PxPerDp: 1, PxPerSp: 1},
|
|
|
|
|
Constraints: layout.Exact(size),
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
dimensions := NewAppShell("Modern").Layout(context, NewTheme())
|
|
|
|
|
if dimensions.Size != size {
|
|
|
|
|
t.Fatalf("Layout() size = %v, want %v", dimensions.Size, size)
|
|
|
|
|
}
|
|
|
|
|
}
|
2026-07-16 17:10:42 +08:00
|
|
|
|
|
|
|
|
func TestAppShellVirtualizesLargeCatalog(t *testing.T) {
|
|
|
|
|
items := make([]application.CatalogListItem, 500)
|
|
|
|
|
for index := range items {
|
|
|
|
|
items[index] = application.CatalogListItem{
|
|
|
|
|
ID: fmt.Sprintf("app-%03d", index),
|
|
|
|
|
Name: fmt.Sprintf("软件 %03d", index),
|
|
|
|
|
Version: "1.0.0",
|
|
|
|
|
Category: "工具",
|
|
|
|
|
Tags: []string{"工具"},
|
|
|
|
|
Status: domain.StatusNotInstalled,
|
|
|
|
|
Installable: true,
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
shell := NewAppShell("Modern", items...)
|
|
|
|
|
context := testContext(image.Pt(1080, 420))
|
|
|
|
|
|
|
|
|
|
shell.Layout(context, NewTheme())
|
|
|
|
|
if shell.lastRendered <= 0 || shell.lastRendered >= len(items) {
|
|
|
|
|
t.Fatalf(
|
|
|
|
|
"lastRendered = %d, want visible subset of %d",
|
|
|
|
|
shell.lastRendered,
|
|
|
|
|
len(items),
|
|
|
|
|
)
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
func TestAppShellKeepsRowControlsByAppID(t *testing.T) {
|
|
|
|
|
items := []application.CatalogListItem{
|
|
|
|
|
{ID: "app-one", Name: "One", Version: "1.0.0", Category: "工具"},
|
|
|
|
|
{ID: "app-two", Name: "Two", Version: "1.0.0", Category: "图像"},
|
|
|
|
|
}
|
|
|
|
|
shell := NewAppShell("Modern", items...)
|
|
|
|
|
original := shell.rows["app-two"]
|
2026-07-17 09:38:25 +08:00
|
|
|
shell.ApplyIcon("app-one", image.NewNRGBA(image.Rect(0, 0, 16, 16)))
|
|
|
|
|
shell.ApplyIcon("app-two", image.NewNRGBA(image.Rect(0, 0, 24, 24)))
|
2026-07-16 17:10:42 +08:00
|
|
|
|
|
|
|
|
shell.model.SetCategory("图像")
|
|
|
|
|
shell.Layout(testContext(image.Pt(1080, 720)), NewTheme())
|
|
|
|
|
if shell.rows["app-two"] != original {
|
|
|
|
|
t.Fatal("row controls were recreated after filtering")
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
shell.SetItems([]application.CatalogListItem{
|
|
|
|
|
{ID: "app-two", Name: "Two", Version: "1.1.0", Category: "图像"},
|
|
|
|
|
})
|
|
|
|
|
if shell.rows["app-two"] != original {
|
|
|
|
|
t.Fatal("row controls were recreated after snapshot update")
|
|
|
|
|
}
|
|
|
|
|
if _, exists := shell.rows["app-one"]; exists {
|
|
|
|
|
t.Fatal("removed app retained row controls")
|
|
|
|
|
}
|
2026-07-17 09:38:25 +08:00
|
|
|
if _, exists := shell.icons["app-one"]; exists {
|
|
|
|
|
t.Fatal("removed app retained prepared icon")
|
|
|
|
|
}
|
|
|
|
|
icon, exists := shell.icons["app-two"]
|
|
|
|
|
if !exists {
|
|
|
|
|
t.Fatal("retained app lost its prepared icon")
|
|
|
|
|
}
|
|
|
|
|
if icon.Size() != image.Pt(24, 24) {
|
|
|
|
|
t.Fatalf("retained app icon size = %v", icon.Size())
|
|
|
|
|
}
|
2026-07-16 17:10:42 +08:00
|
|
|
}
|
|
|
|
|
|
2026-07-16 17:22:37 +08:00
|
|
|
func TestAppShellRendersSelectedDetailAndAppliedIcon(t *testing.T) {
|
|
|
|
|
item := application.CatalogListItem{
|
|
|
|
|
ID: "json-parser",
|
|
|
|
|
Name: "JSON解析工具",
|
|
|
|
|
Description: "格式化并检查 JSON",
|
|
|
|
|
Version: "1.2.0",
|
|
|
|
|
Category: "开发工具",
|
|
|
|
|
Tags: []string{"JSON", "格式化"},
|
|
|
|
|
Homepage: "https://example.invalid/json-parser",
|
|
|
|
|
Tutorial: "https://example.invalid/json-parser/tutorial",
|
|
|
|
|
Status: domain.StatusInstalled,
|
|
|
|
|
Installed: true,
|
|
|
|
|
}
|
|
|
|
|
shell := NewAppShell("Modern", item)
|
|
|
|
|
icon := image.NewNRGBA(image.Rect(0, 0, 32, 32))
|
|
|
|
|
shell.ApplyIcon(item.ID, icon)
|
|
|
|
|
shell.model.Select(item.ID)
|
|
|
|
|
|
|
|
|
|
shell.Layout(testContext(image.Pt(1280, 800)), NewTheme())
|
|
|
|
|
if !shell.detailRendered {
|
|
|
|
|
t.Fatal("selected app detail was not rendered")
|
|
|
|
|
}
|
|
|
|
|
if _, exists := shell.icons[item.ID]; !exists {
|
|
|
|
|
t.Fatal("ApplyIcon did not retain the prepared image operation")
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
shell.ApplyIcon(item.ID, nil)
|
|
|
|
|
if _, exists := shell.icons[item.ID]; exists {
|
|
|
|
|
t.Fatal("ApplyIcon(nil) did not remove the image")
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2026-07-16 17:10:42 +08:00
|
|
|
func testContext(size image.Point) layout.Context {
|
|
|
|
|
var operations op.Ops
|
|
|
|
|
return layout.Context{
|
|
|
|
|
Ops: &operations,
|
|
|
|
|
Metric: unit.Metric{PxPerDp: 1, PxPerSp: 1},
|
|
|
|
|
Constraints: layout.Exact(size),
|
|
|
|
|
}
|
|
|
|
|
}
|