Implement catalog startup diagnostics (T-616)
This commit is contained in:
@@ -174,8 +174,8 @@ func TestAdapterContractVirtualizationAndControlLifecycle(t *testing.T) {
|
||||
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 did not render the catalog-loading state")
|
||||
}
|
||||
if adapterContractHasSemantic(emptyNodes, "显示全部软件") {
|
||||
t.Fatal("empty catalog rendered a filter recovery action")
|
||||
|
||||
@@ -0,0 +1,46 @@
|
||||
package gio
|
||||
|
||||
import "softbox.local/core/application"
|
||||
|
||||
type catalogPresentationState string
|
||||
|
||||
const (
|
||||
catalogStateLoading catalogPresentationState = "loading"
|
||||
catalogStateReady catalogPresentationState = "ready"
|
||||
catalogStateUnconfigured catalogPresentationState = "unconfigured"
|
||||
catalogStateLoadFailed catalogPresentationState = "load_failed"
|
||||
)
|
||||
|
||||
func (shell *AppShell) applyCatalogEvent(event application.Event) (bool, error) {
|
||||
payload, handled, err := application.ParseCatalogEvent(event)
|
||||
if err != nil || !handled {
|
||||
return handled, err
|
||||
}
|
||||
switch payload.Type {
|
||||
case application.EventCatalogRefreshed:
|
||||
shell.SetItems(payload.Items)
|
||||
case application.EventCatalogRejected:
|
||||
switch payload.FailureCode {
|
||||
case application.CatalogFailureSourceUnconfigured:
|
||||
shell.catalogState = catalogStateUnconfigured
|
||||
case application.CatalogFailureLoadFailed:
|
||||
shell.catalogState = catalogStateLoadFailed
|
||||
}
|
||||
}
|
||||
return true, nil
|
||||
}
|
||||
|
||||
func (shell *AppShell) catalogStatusText() string {
|
||||
switch shell.catalogState {
|
||||
case catalogStateLoading:
|
||||
return "目录状态:正在加载已验证 Catalog"
|
||||
case catalogStateUnconfigured:
|
||||
return "目录状态:Catalog 来源尚未配置"
|
||||
case catalogStateLoadFailed:
|
||||
return "目录状态:Catalog 加载失败"
|
||||
case catalogStateReady:
|
||||
return "目录状态:已加载已验证 Catalog"
|
||||
default:
|
||||
return "目录状态:未知"
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,72 @@
|
||||
package gio
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"testing"
|
||||
|
||||
"softbox.local/core/application"
|
||||
)
|
||||
|
||||
func TestCatalogEventsUpdateSnapshotAndRetainItOnFailure(t *testing.T) {
|
||||
shell := NewAppShell("Modern")
|
||||
items := []application.CatalogListItem{{ID: "json-tool", Name: "JSON Tool", Version: "1.0.0", Category: "工具"}}
|
||||
if err := shell.ApplyEvent(application.Event{
|
||||
Type: application.EventCatalogRefreshed,
|
||||
Payload: application.CatalogEvent{Type: application.EventCatalogRefreshed, Source: application.CatalogSourceCache, Items: items},
|
||||
}); err != nil {
|
||||
t.Fatalf("ApplyEvent(refresh) error = %v", err)
|
||||
}
|
||||
if shell.model.TotalCount() != 1 || shell.catalogState != catalogStateReady {
|
||||
t.Fatalf("snapshot count/state = %d/%q", shell.model.TotalCount(), shell.catalogState)
|
||||
}
|
||||
if err := shell.ApplyEvent(application.Event{
|
||||
Type: application.EventCatalogRejected,
|
||||
Payload: application.CatalogEvent{Type: application.EventCatalogRejected, FailureCode: application.CatalogFailureLoadFailed},
|
||||
}); err != nil {
|
||||
t.Fatalf("ApplyEvent(reject) error = %v", err)
|
||||
}
|
||||
if shell.model.TotalCount() != 1 || shell.catalogState != catalogStateLoadFailed {
|
||||
t.Fatalf("failure cleared snapshot or state = %d/%q", shell.model.TotalCount(), shell.catalogState)
|
||||
}
|
||||
}
|
||||
|
||||
func TestCatalogEventsExposeStableEmptyStatesAndRejectBadPayload(t *testing.T) {
|
||||
shell := NewAppShell("Modern")
|
||||
if shell.catalogState != catalogStateLoading {
|
||||
t.Fatalf("initial state = %q, want loading", shell.catalogState)
|
||||
}
|
||||
if err := shell.ApplyEvent(application.Event{
|
||||
Type: application.EventCatalogRejected,
|
||||
Payload: application.CatalogEvent{Type: application.EventCatalogRejected, FailureCode: application.CatalogFailureSourceUnconfigured},
|
||||
}); err != nil {
|
||||
t.Fatalf("ApplyEvent(unconfigured) error = %v", err)
|
||||
}
|
||||
if shell.catalogState != catalogStateUnconfigured || shell.catalogStatusText() != "目录状态:Catalog 来源尚未配置" {
|
||||
t.Fatalf("unconfigured state/status = %q/%q", shell.catalogState, shell.catalogStatusText())
|
||||
}
|
||||
if nodes := adapterContractLayout(shell, adapterContractViewport); !adapterContractHasSemantic(nodes, "Catalog 来源尚未配置") {
|
||||
t.Fatal("unconfigured state was not visible")
|
||||
}
|
||||
if err := shell.ApplyEvent(application.Event{
|
||||
Type: application.EventCatalogRejected,
|
||||
Payload: application.CatalogEvent{Type: application.EventCatalogRejected, FailureCode: application.CatalogFailureLoadFailed},
|
||||
}); err != nil {
|
||||
t.Fatalf("ApplyEvent(load failed) error = %v", err)
|
||||
}
|
||||
if nodes := adapterContractLayout(shell, adapterContractViewport); !adapterContractHasSemantic(nodes, "Catalog 加载失败") {
|
||||
t.Fatal("load-failed state was not visible")
|
||||
}
|
||||
if err := shell.ApplyEvent(application.Event{
|
||||
Type: application.EventCatalogRefreshed,
|
||||
Payload: application.CatalogEvent{Type: application.EventCatalogRefreshed, Source: application.CatalogSourceRemote},
|
||||
}); err != nil {
|
||||
t.Fatalf("ApplyEvent(empty refreshed) error = %v", err)
|
||||
}
|
||||
if nodes := adapterContractLayout(shell, adapterContractViewport); !adapterContractHasSemantic(nodes, "Catalog 暂无可显示软件") {
|
||||
t.Fatal("loaded-empty state was not visible")
|
||||
}
|
||||
err := shell.ApplyEvent(application.Event{Type: application.EventCatalogRefreshed, Payload: "raw error"})
|
||||
if !errors.Is(err, application.ErrCatalogEventPayload) {
|
||||
t.Fatalf("bad payload error = %v", err)
|
||||
}
|
||||
}
|
||||
@@ -56,6 +56,9 @@ func (shell *AppShell) CancelIconRequest(appID, requestID string) bool {
|
||||
|
||||
// ApplyEvent validates and applies an application event on the UI goroutine.
|
||||
func (shell *AppShell) ApplyEvent(event application.Event) error {
|
||||
if handled, err := shell.applyCatalogEvent(event); err != nil || handled {
|
||||
return err
|
||||
}
|
||||
iconEvent, handled, err := application.ParseIconEvent(event)
|
||||
if err != nil || !handled {
|
||||
return err
|
||||
|
||||
@@ -213,8 +213,8 @@ func TestAppShellDropsChangedRemovedAndCanceledIconResults(t *testing.T) {
|
||||
|
||||
func TestAppShellRejectsMalformedIconEventAndIgnoresOtherEvents(t *testing.T) {
|
||||
shell := NewAppShell("Test")
|
||||
if err := shell.ApplyEvent(application.Event{Type: application.EventCatalogRefreshed}); err != nil {
|
||||
t.Fatalf("ApplyEvent(non-icon) error = %v", err)
|
||||
if err := shell.ApplyEvent(application.Event{Type: application.EventCatalogRefreshed}); !errors.Is(err, application.ErrCatalogEventPayload) {
|
||||
t.Fatalf("ApplyEvent(malformed catalog payload) error = %v", err)
|
||||
}
|
||||
err := shell.ApplyEvent(application.Event{
|
||||
Type: application.EventIconReady,
|
||||
|
||||
@@ -34,6 +34,7 @@ type AppShell struct {
|
||||
iconRequests map[string]application.IconEventIdentity
|
||||
iconApplied map[string]application.IconEventIdentity
|
||||
iconFailures map[string]iconFailureState
|
||||
catalogState catalogPresentationState
|
||||
lastRendered int
|
||||
detailRendered bool
|
||||
}
|
||||
@@ -58,6 +59,9 @@ func NewAppShell(
|
||||
}
|
||||
shell.search.SingleLine = true
|
||||
shell.SetItems(items)
|
||||
if len(items) == 0 {
|
||||
shell.catalogState = catalogStateLoading
|
||||
}
|
||||
return shell
|
||||
}
|
||||
|
||||
@@ -76,6 +80,7 @@ func (shell *AppShell) ApplyIcon(appID string, icon image.Image) {
|
||||
// SetItems applies a prepared, IO-free catalog/status snapshot.
|
||||
func (shell *AppShell) SetItems(items []application.CatalogListItem) {
|
||||
shell.model.SetItems(items)
|
||||
shell.catalogState = catalogStateReady
|
||||
|
||||
nextRows := make(map[string]*rowControls, len(items))
|
||||
nextIcons := make(map[string]paint.ImageOp, len(items))
|
||||
|
||||
@@ -297,6 +297,20 @@ func (shell *AppShell) layoutEmptyState(
|
||||
if shell.model.TotalCount() == 0 {
|
||||
title = "软件目录尚未加载"
|
||||
body = "联网刷新或存在已验证缓存后,软件会显示在这里。"
|
||||
switch shell.catalogState {
|
||||
case catalogStateLoading:
|
||||
title = "正在加载软件目录"
|
||||
body = "正在等待已验证 Catalog 快照。"
|
||||
case catalogStateUnconfigured:
|
||||
title = "Catalog 来源尚未配置"
|
||||
body = "此构建未装配可信发布配置,因此未显示任何软件。"
|
||||
case catalogStateLoadFailed:
|
||||
title = "Catalog 加载失败"
|
||||
body = "未收到可验证的 Catalog;已显示的目录不会被清除。"
|
||||
case catalogStateReady:
|
||||
title = "Catalog 暂无可显示软件"
|
||||
body = "已验证 Catalog 没有适用于当前目标的软件。"
|
||||
}
|
||||
}
|
||||
return layout.Flex{Axis: layout.Vertical, Alignment: layout.Middle}.Layout(
|
||||
gtx,
|
||||
|
||||
@@ -138,7 +138,7 @@ func (shell *AppShell) layoutFooter(
|
||||
return layout.Flex{Alignment: layout.Middle}.Layout(
|
||||
gtx,
|
||||
layout.Rigid(func(gtx layout.Context) layout.Dimensions {
|
||||
label := material.Caption(theme, "目录状态:等待已验证 Catalog")
|
||||
label := material.Caption(theme, shell.catalogStatusText())
|
||||
label.Color = shellColors.secondary
|
||||
return label.Layout(gtx)
|
||||
}),
|
||||
|
||||
Reference in New Issue
Block a user