271 lines
9.4 KiB
Go
271 lines
9.4 KiB
Go
package gio
|
|
|
|
import (
|
|
"fmt"
|
|
"image"
|
|
"testing"
|
|
|
|
"gioui.org/io/input"
|
|
"gioui.org/layout"
|
|
"gioui.org/op"
|
|
"gioui.org/unit"
|
|
|
|
"softbox.local/core/application"
|
|
"softbox.local/core/domain"
|
|
)
|
|
|
|
var (
|
|
adapterContractViewport = image.Pt(1024, 680)
|
|
adapterContractCompactViewport = image.Pt(1024, 380)
|
|
)
|
|
|
|
const adapterContractEdition = "Legacy"
|
|
|
|
func TestAdapterContractInputEventsUpdateModel(t *testing.T) {
|
|
shell := NewAppShell(adapterContractEdition, adapterContractItems()...)
|
|
|
|
shell.search.SetText("APP-TWO")
|
|
adapterContractLayout(shell, adapterContractViewport)
|
|
if got := shell.model.Query(); got != "app-two" {
|
|
t.Fatalf("query after editor update = %q, want app-two", got)
|
|
}
|
|
visible := shell.model.VisibleItems()
|
|
if len(visible) != 1 || visible[0].ID != "app-two" {
|
|
t.Fatalf("visible IDs after editor update = %v, want [app-two]", adapterContractIDs(visible))
|
|
}
|
|
|
|
shell.search.SetText("")
|
|
adapterContractLayout(shell, adapterContractViewport)
|
|
shell.categoryControls["图像"].Click()
|
|
adapterContractLayout(shell, adapterContractViewport)
|
|
if got := shell.model.Category(); got != "图像" {
|
|
t.Fatalf("category after click = %q, want 图像", got)
|
|
}
|
|
|
|
shell.viewUpdates.Click()
|
|
adapterContractLayout(shell, adapterContractViewport)
|
|
if got := shell.model.View(); got != application.CatalogViewUpdates {
|
|
t.Fatalf("view after updates click = %q", got)
|
|
}
|
|
shell.viewInstalled.Click()
|
|
adapterContractLayout(shell, adapterContractViewport)
|
|
if got := shell.model.View(); got != application.CatalogViewInstalled {
|
|
t.Fatalf("view after installed click = %q", got)
|
|
}
|
|
shell.viewAll.Click()
|
|
adapterContractLayout(shell, adapterContractViewport)
|
|
if got := shell.model.View(); got != application.CatalogViewAll {
|
|
t.Fatalf("view after all click = %q", got)
|
|
}
|
|
|
|
shell.search.SetText("missing-app")
|
|
nodes := adapterContractLayout(shell, adapterContractViewport)
|
|
if !adapterContractHasSemantic(nodes, "没有匹配的软件") ||
|
|
!adapterContractHasSemantic(nodes, "显示全部软件") {
|
|
t.Fatal("filtered empty state did not expose its recovery action")
|
|
}
|
|
shell.resetFilters.Click()
|
|
adapterContractLayout(shell, adapterContractViewport)
|
|
if shell.search.Text() != "" || shell.model.Query() != "" ||
|
|
shell.model.Category() != "" || shell.model.View() != application.CatalogViewAll {
|
|
t.Fatalf(
|
|
"reset state = editor %q, query %q, category %q, view %q",
|
|
shell.search.Text(), shell.model.Query(), shell.model.Category(), shell.model.View(),
|
|
)
|
|
}
|
|
if got := len(shell.model.VisibleItems()); got != len(adapterContractItems()) {
|
|
t.Fatalf("visible count after reset = %d, want %d", got, len(adapterContractItems()))
|
|
}
|
|
}
|
|
|
|
func TestAdapterContractRowIdentityAndDetailContext(t *testing.T) {
|
|
items := adapterContractLargeCatalog(80)
|
|
shell := NewAppShell(adapterContractEdition, items...)
|
|
targetID := "app-037"
|
|
targetControl := shell.rows[targetID]
|
|
|
|
reordered := append([]application.CatalogListItem(nil), items...)
|
|
for left, right := 0, len(reordered)-1; left < right; left, right = left+1, right-1 {
|
|
reordered[left], reordered[right] = reordered[right], reordered[left]
|
|
}
|
|
shell.SetItems(reordered)
|
|
if shell.rows[targetID] != targetControl {
|
|
t.Fatal("row control was recreated after catalog reorder")
|
|
}
|
|
|
|
shell.search.SetText("app-")
|
|
adapterContractLayout(shell, adapterContractViewport)
|
|
shell.categoryControls["图像"].Click()
|
|
adapterContractLayout(shell, adapterContractViewport)
|
|
shell.viewInstalled.Click()
|
|
adapterContractLayout(shell, adapterContractViewport)
|
|
shell.appList.ScrollTo(12)
|
|
adapterContractLayout(shell, adapterContractViewport)
|
|
|
|
targetControl.open.Click()
|
|
adapterContractLayout(shell, adapterContractViewport)
|
|
if got := shell.model.SelectedID(); got != targetID {
|
|
t.Fatalf("selected ID after reordered row click = %q, want %q", got, targetID)
|
|
}
|
|
if !shell.detailRendered {
|
|
t.Fatal("row click selected the model but did not render detail")
|
|
}
|
|
queryBefore := shell.model.Query()
|
|
categoryBefore := shell.model.Category()
|
|
viewBefore := shell.model.View()
|
|
positionBefore := shell.appList.Position
|
|
|
|
shell.closeDetail.Click()
|
|
adapterContractLayout(shell, adapterContractViewport)
|
|
if got := shell.model.SelectedID(); got != "" {
|
|
t.Fatalf("selected ID after close = %q, want empty", got)
|
|
}
|
|
if shell.detailRendered {
|
|
t.Fatal("detail remained rendered after close click")
|
|
}
|
|
if shell.model.Query() != queryBefore || shell.model.Category() != categoryBefore ||
|
|
shell.model.View() != viewBefore {
|
|
t.Fatal("closing detail changed the active list filters")
|
|
}
|
|
positionAfter := shell.appList.Position
|
|
if positionAfter.First != positionBefore.First || positionAfter.Offset != positionBefore.Offset {
|
|
t.Fatalf(
|
|
"list position after close = first %d offset %d, want first %d offset %d",
|
|
positionAfter.First, positionAfter.Offset, positionBefore.First, positionBefore.Offset,
|
|
)
|
|
}
|
|
}
|
|
|
|
func TestAdapterContractVirtualizationAndControlLifecycle(t *testing.T) {
|
|
items := adapterContractLargeCatalog(500)
|
|
shell := NewAppShell(adapterContractEdition, items...)
|
|
retainedRow := shell.rows["app-001"]
|
|
retainedCategory := shell.categoryControls["图像"]
|
|
|
|
adapterContractLayout(shell, adapterContractCompactViewport)
|
|
if shell.lastRendered <= 0 || shell.lastRendered >= len(items) {
|
|
t.Fatalf("lastRendered = %d, want a non-zero subset of %d", shell.lastRendered, len(items))
|
|
}
|
|
if count := shell.appList.Position.Count; count <= 0 || count >= len(items) {
|
|
t.Fatalf("layout.List visible count = %d, want a non-zero subset of %d", count, len(items))
|
|
}
|
|
|
|
shell.categoryControls["图像"].Click()
|
|
adapterContractLayout(shell, adapterContractCompactViewport)
|
|
if shell.rows["app-001"] != retainedRow || shell.categoryControls["图像"] != retainedCategory {
|
|
t.Fatal("filtering recreated stable app or category controls")
|
|
}
|
|
|
|
shell.SetItems([]application.CatalogListItem{items[3], items[1]})
|
|
if shell.rows["app-001"] != retainedRow {
|
|
t.Fatal("retained app lost its row control after snapshot update")
|
|
}
|
|
if shell.categoryControls["图像"] != retainedCategory {
|
|
t.Fatal("retained category lost its control after snapshot update")
|
|
}
|
|
if _, exists := shell.rows["app-000"]; exists {
|
|
t.Fatal("removed app retained its row control")
|
|
}
|
|
if _, exists := shell.categoryControls["工具"]; exists {
|
|
t.Fatal("removed category retained its control")
|
|
}
|
|
}
|
|
|
|
func TestAdapterContractDistinguishesEmptyCatalogAndNoMatches(t *testing.T) {
|
|
emptyShell := NewAppShell(adapterContractEdition)
|
|
emptyNodes := adapterContractLayout(emptyShell, adapterContractViewport)
|
|
if !adapterContractHasSemantic(emptyNodes, "软件目录尚未加载") {
|
|
t.Fatal("empty catalog did not render the catalog-unavailable state")
|
|
}
|
|
if adapterContractHasSemantic(emptyNodes, "显示全部软件") {
|
|
t.Fatal("empty catalog rendered a filter recovery action")
|
|
}
|
|
|
|
filteredShell := NewAppShell(adapterContractEdition, adapterContractItems()...)
|
|
filteredShell.search.SetText("missing-app")
|
|
filteredNodes := adapterContractLayout(filteredShell, adapterContractViewport)
|
|
if !adapterContractHasSemantic(filteredNodes, "没有匹配的软件") {
|
|
t.Fatal("filtered catalog did not render the no-matches state")
|
|
}
|
|
if !adapterContractHasSemantic(filteredNodes, "显示全部软件") {
|
|
t.Fatal("filtered catalog did not render its recovery action")
|
|
}
|
|
filteredShell.resetFilters.Click()
|
|
adapterContractLayout(filteredShell, adapterContractViewport)
|
|
if filteredShell.search.Text() != "" || filteredShell.model.Query() != "" {
|
|
t.Fatal("filter recovery did not clear editor and model query")
|
|
}
|
|
if len(filteredShell.model.VisibleItems()) == 0 {
|
|
t.Fatal("filter recovery did not restore catalog rows")
|
|
}
|
|
}
|
|
|
|
func adapterContractItems() []application.CatalogListItem {
|
|
return []application.CatalogListItem{
|
|
{
|
|
ID: "app-one", Name: "One", Version: "1.0.0", Category: "工具",
|
|
Status: domain.StatusNotInstalled, Installable: true,
|
|
},
|
|
{
|
|
ID: "app-two", Name: "Two", Version: "1.0.0", Category: "图像",
|
|
Status: domain.StatusInstalled, Installed: true, Installable: true,
|
|
},
|
|
{
|
|
ID: "app-three", Name: "Three", Version: "2.0.0", Category: "图像",
|
|
Status: domain.StatusUpdateAvailable, Installed: true, Installable: true,
|
|
},
|
|
}
|
|
}
|
|
|
|
func adapterContractLargeCatalog(count int) []application.CatalogListItem {
|
|
items := make([]application.CatalogListItem, count)
|
|
for index := range items {
|
|
category := "工具"
|
|
if index%2 == 1 {
|
|
category = "图像"
|
|
}
|
|
items[index] = application.CatalogListItem{
|
|
ID: fmt.Sprintf("app-%03d", index),
|
|
Name: fmt.Sprintf("App %03d", index),
|
|
Version: "1.0.0",
|
|
Category: category,
|
|
Status: domain.StatusInstalled,
|
|
Installed: true,
|
|
Installable: true,
|
|
}
|
|
}
|
|
return items
|
|
}
|
|
|
|
func adapterContractLayout(shell *AppShell, size image.Point) []input.SemanticNode {
|
|
var operations op.Ops
|
|
var router input.Router
|
|
context := layout.Context{
|
|
Ops: &operations,
|
|
Source: router.Source(),
|
|
Metric: unit.Metric{PxPerDp: 1, PxPerSp: 1},
|
|
Constraints: layout.Exact(size),
|
|
}
|
|
shell.Layout(context, NewTheme())
|
|
router.Frame(&operations)
|
|
return router.AppendSemantics(nil)
|
|
}
|
|
|
|
func adapterContractHasSemantic(nodes []input.SemanticNode, want string) bool {
|
|
for _, node := range nodes {
|
|
if node.Desc.Label == want || node.Desc.Description == want ||
|
|
adapterContractHasSemantic(node.Children, want) {
|
|
return true
|
|
}
|
|
}
|
|
return false
|
|
}
|
|
|
|
func adapterContractIDs(items []application.CatalogListItem) []string {
|
|
ids := make([]string, len(items))
|
|
for index, item := range items {
|
|
ids[index] = item.ID
|
|
}
|
|
return ids
|
|
}
|