423 lines
12 KiB
Go
423 lines
12 KiB
Go
package gio
|
|
|
|
import (
|
|
"context"
|
|
"errors"
|
|
"image"
|
|
"strings"
|
|
"testing"
|
|
"time"
|
|
|
|
"softbox.local/core/application"
|
|
)
|
|
|
|
func TestIconEventRelayAppliesOnlyDuringUIDrain(t *testing.T) {
|
|
reference := testIconReference("11")
|
|
shell := NewAppShell("Test", application.CatalogListItem{
|
|
ID: "app-one",
|
|
Name: "One",
|
|
IconRef: reference,
|
|
})
|
|
identity := testIconIdentity(t, "request-one", "app-one", reference, 96)
|
|
if err := shell.ExpectIcon(identity); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
ready, err := application.NewIconReadyEvent(
|
|
identity,
|
|
image.NewNRGBA(image.Rect(0, 0, 24, 24)),
|
|
)
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
relay, err := application.NewEventRelay(1)
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
submitted := make(chan error, 1)
|
|
go func() {
|
|
submitted <- relay.Submit(context.Background(), ready)
|
|
}()
|
|
if err := waitIconSubmit(submitted); err != nil {
|
|
t.Fatalf("Submit() error = %v", err)
|
|
}
|
|
if _, exists := shell.icons["app-one"]; exists {
|
|
t.Fatal("background relay changed shell before UI drain")
|
|
}
|
|
if err := relay.Drain(shell.ApplyEvent); err != nil {
|
|
t.Fatalf("Drain() error = %v", err)
|
|
}
|
|
icon, exists := shell.icons["app-one"]
|
|
if !exists || icon.Size() != image.Pt(24, 24) {
|
|
t.Fatalf("applied icon = (%t, %v)", exists, icon.Size())
|
|
}
|
|
}
|
|
|
|
func TestAppShellAcceptsOnlyLatestIconRequest(t *testing.T) {
|
|
reference := testIconReference("22")
|
|
shell := NewAppShell("Test", application.CatalogListItem{
|
|
ID: "app-one",
|
|
Name: "One",
|
|
IconRef: reference,
|
|
})
|
|
oldIdentity := testIconIdentity(t, "request-old", "app-one", reference, 96)
|
|
newIdentity := testIconIdentity(t, "request-new", "app-one", reference, 96)
|
|
if err := shell.ExpectIcon(oldIdentity); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if err := shell.ExpectIcon(newIdentity); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
oldReady, err := application.NewIconReadyEvent(
|
|
oldIdentity,
|
|
image.NewNRGBA(image.Rect(0, 0, 12, 12)),
|
|
)
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if err := shell.ApplyEvent(oldReady); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if _, exists := shell.icons["app-one"]; exists {
|
|
t.Fatal("stale request inserted an icon")
|
|
}
|
|
|
|
newReady, err := application.NewIconReadyEvent(
|
|
newIdentity,
|
|
image.NewNRGBA(image.Rect(0, 0, 30, 30)),
|
|
)
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if err := shell.ApplyEvent(newReady); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if got := shell.icons["app-one"].Size(); got != image.Pt(30, 30) {
|
|
t.Fatalf("latest icon size = %v", got)
|
|
}
|
|
|
|
retryIdentity := testIconIdentity(t, "request-retry", "app-one", reference, 96)
|
|
if err := shell.ExpectIcon(retryIdentity); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if _, exists := shell.icons["app-one"]; !exists {
|
|
t.Fatal("same-resource retry discarded an already valid icon")
|
|
}
|
|
failed, err := application.NewIconFailedEvent(
|
|
retryIdentity,
|
|
application.IconFailureUnavailable,
|
|
)
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if err := shell.ApplyEvent(failed); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if _, exists := shell.icons["app-one"]; !exists {
|
|
t.Fatal("matching failure discarded an already valid icon")
|
|
}
|
|
if failure, exists := shell.IconFailure("app-one"); !exists || failure != application.IconFailureUnavailable {
|
|
t.Fatalf("IconFailure() = (%q, %t)", failure, exists)
|
|
}
|
|
|
|
dpiIdentity := testIconIdentity(t, "request-dpi", "app-one", reference, 144)
|
|
if err := shell.ExpectIcon(dpiIdentity); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if _, exists := shell.icons["app-one"]; exists {
|
|
t.Fatal("different-DPI request retained an unmatching image")
|
|
}
|
|
dpiFailed, err := application.NewIconFailedEvent(
|
|
dpiIdentity,
|
|
application.IconFailureUnavailable,
|
|
)
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if err := shell.ApplyEvent(dpiFailed); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if _, exists := shell.icons["app-one"]; exists {
|
|
t.Fatal("different-DPI failure restored an unmatching image")
|
|
}
|
|
}
|
|
|
|
func TestAppShellDropsChangedRemovedAndCanceledIconResults(t *testing.T) {
|
|
oldReference := testIconReference("33")
|
|
newReference := testIconReference("44")
|
|
shell := NewAppShell("Test", application.CatalogListItem{
|
|
ID: "app-one",
|
|
Name: "One",
|
|
IconRef: oldReference,
|
|
})
|
|
oldIdentity := testIconIdentity(t, "request-old", "app-one", oldReference, 96)
|
|
if err := shell.ExpectIcon(oldIdentity); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
oldReady, err := application.NewIconReadyEvent(
|
|
oldIdentity,
|
|
image.NewNRGBA(image.Rect(0, 0, 20, 20)),
|
|
)
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if err := shell.ApplyEvent(oldReady); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
shell.SetItems([]application.CatalogListItem{{
|
|
ID: "app-one",
|
|
Name: "One",
|
|
IconRef: newReference,
|
|
}})
|
|
if _, exists := shell.icons["app-one"]; exists {
|
|
t.Fatal("IconRef change retained the previous image")
|
|
}
|
|
if err := shell.ApplyEvent(oldReady); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if _, exists := shell.icons["app-one"]; exists {
|
|
t.Fatal("old IconRef result was reinserted")
|
|
}
|
|
|
|
newIdentity := testIconIdentity(t, "request-new", "app-one", newReference, 96)
|
|
if err := shell.ExpectIcon(newIdentity); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if !shell.CancelIconRequest("app-one", newIdentity.RequestID) {
|
|
t.Fatal("CancelIconRequest() did not cancel the latest request")
|
|
}
|
|
newReady, err := application.NewIconReadyEvent(
|
|
newIdentity,
|
|
image.NewNRGBA(image.Rect(0, 0, 22, 22)),
|
|
)
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if err := shell.ApplyEvent(newReady); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if _, exists := shell.icons["app-one"]; exists {
|
|
t.Fatal("canceled result was applied")
|
|
}
|
|
|
|
shell.SetItems(nil)
|
|
if err := shell.ExpectIcon(newIdentity); !errors.Is(err, ErrIconRequestStale) {
|
|
t.Fatalf("ExpectIcon(removed app) error = %v", err)
|
|
}
|
|
if err := shell.ApplyEvent(newReady); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if _, exists := shell.icons["app-one"]; exists {
|
|
t.Fatal("removed app was reinserted by a late result")
|
|
}
|
|
}
|
|
|
|
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)
|
|
}
|
|
err := shell.ApplyEvent(application.Event{
|
|
Type: application.EventIconReady,
|
|
RequestID: "request",
|
|
AppID: "app-one",
|
|
Payload: "wrong",
|
|
})
|
|
if !errors.Is(err, application.ErrInvalidIconEvent) {
|
|
t.Fatalf("ApplyEvent(invalid payload) error = %v", err)
|
|
}
|
|
if len(shell.icons) != 0 {
|
|
t.Fatal("invalid payload polluted icon state")
|
|
}
|
|
}
|
|
|
|
func TestAppShellRendersOnlyUnsafeIconCacheDiagnostic(t *testing.T) {
|
|
reference := testIconReference("55")
|
|
item := application.CatalogListItem{
|
|
ID: "app-one",
|
|
Name: "One",
|
|
Version: "1.0.0",
|
|
IconRef: reference,
|
|
}
|
|
shell := NewAppShell("Test", item)
|
|
identity := testIconIdentity(t, "request-unsafe", item.ID, reference, 144)
|
|
if err := shell.ExpectIcon(identity); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
applyTestIconFailure(t, shell, identity, application.IconFailureUnsafe)
|
|
shell.model.Select(item.ID)
|
|
|
|
nodes := adapterContractLayout(shell, adapterContractViewport)
|
|
failure := shell.iconFailures[item.ID]
|
|
for _, want := range []string{
|
|
"图标缓存安全警告",
|
|
unsafeIconCacheMessage,
|
|
unsafeIconCacheDiagnostic(failure),
|
|
} {
|
|
if !adapterContractHasSemantic(nodes, want) {
|
|
t.Fatalf("unsafe cache detail is missing semantic text %q", want)
|
|
}
|
|
}
|
|
if diagnostic := unsafeIconCacheDiagnostic(failure); strings.Contains(diagnostic, "sha256:") {
|
|
t.Fatalf("unsafe cache diagnostic exposed the reference scheme: %q", diagnostic)
|
|
}
|
|
|
|
unavailable := testIconIdentity(t, "request-unavailable", item.ID, reference, 144)
|
|
if err := shell.ExpectIcon(unavailable); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
applyTestIconFailure(t, shell, unavailable, application.IconFailureUnavailable)
|
|
nodes = adapterContractLayout(shell, adapterContractViewport)
|
|
if adapterContractHasSemantic(nodes, "图标缓存安全警告") {
|
|
t.Fatal("ordinary icon failure rendered an unsafe-cache warning")
|
|
}
|
|
if code, exists := shell.IconFailure(item.ID); !exists ||
|
|
code != application.IconFailureUnavailable {
|
|
t.Fatalf("IconFailure() = (%q, %t)", code, exists)
|
|
}
|
|
}
|
|
|
|
func TestAppShellRetainsUnsafeDiagnosticOnlyForCurrentResource(t *testing.T) {
|
|
reference := testIconReference("66")
|
|
item := application.CatalogListItem{
|
|
ID: "app-one",
|
|
Name: "One",
|
|
Version: "1.0.0",
|
|
IconRef: reference,
|
|
}
|
|
shell := NewAppShell("Test", item)
|
|
first := testIconIdentity(t, "request-first", item.ID, reference, 96)
|
|
if err := shell.ExpectIcon(first); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
firstFailure := applyTestIconFailure(
|
|
t,
|
|
shell,
|
|
first,
|
|
application.IconFailureUnsafe,
|
|
)
|
|
if got := shell.iconFailures[item.ID].Identity; got != first {
|
|
t.Fatalf("stored failure identity = %+v, want %+v", got, first)
|
|
}
|
|
|
|
shell.SetItems([]application.CatalogListItem{item})
|
|
if code, exists := shell.IconFailure(item.ID); !exists ||
|
|
code != application.IconFailureUnsafe {
|
|
t.Fatal("same-reference snapshot discarded the unsafe diagnostic")
|
|
}
|
|
|
|
latest := testIconIdentity(t, "request-latest", item.ID, reference, 96)
|
|
if err := shell.ExpectIcon(latest); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if _, exists := shell.IconFailure(item.ID); exists {
|
|
t.Fatal("new request retained the previous unsafe diagnostic")
|
|
}
|
|
if err := shell.ApplyEvent(firstFailure); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if _, exists := shell.IconFailure(item.ID); exists {
|
|
t.Fatal("late failure restored a stale diagnostic")
|
|
}
|
|
ready, err := application.NewIconReadyEvent(
|
|
latest,
|
|
image.NewNRGBA(image.Rect(0, 0, 16, 16)),
|
|
)
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if err := shell.ApplyEvent(ready); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if _, exists := shell.IconFailure(item.ID); exists {
|
|
t.Fatal("ready event retained an unsafe diagnostic")
|
|
}
|
|
|
|
dpiRequest := testIconIdentity(t, "request-dpi", item.ID, reference, 144)
|
|
if err := shell.ExpectIcon(dpiRequest); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
applyTestIconFailure(t, shell, dpiRequest, application.IconFailureUnsafe)
|
|
if got := shell.iconFailures[item.ID].Identity.DPI; got != 144 {
|
|
t.Fatalf("stored failure DPI = %d, want 144", got)
|
|
}
|
|
|
|
newReference := testIconReference("77")
|
|
changed := item
|
|
changed.IconRef = newReference
|
|
shell.SetItems([]application.CatalogListItem{changed})
|
|
if _, exists := shell.IconFailure(item.ID); exists {
|
|
t.Fatal("IconRef change retained the unsafe diagnostic")
|
|
}
|
|
canceled := testIconIdentity(t, "request-canceled", item.ID, newReference, 96)
|
|
if err := shell.ExpectIcon(canceled); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if !shell.CancelIconRequest(item.ID, canceled.RequestID) {
|
|
t.Fatal("CancelIconRequest() did not cancel the current request")
|
|
}
|
|
applyTestIconFailure(t, shell, canceled, application.IconFailureUnsafe)
|
|
if _, exists := shell.IconFailure(item.ID); exists {
|
|
t.Fatal("canceled failure created an unsafe diagnostic")
|
|
}
|
|
|
|
final := testIconIdentity(t, "request-final", item.ID, newReference, 96)
|
|
if err := shell.ExpectIcon(final); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
applyTestIconFailure(t, shell, final, application.IconFailureUnsafe)
|
|
shell.SetItems(nil)
|
|
if _, exists := shell.IconFailure(item.ID); exists {
|
|
t.Fatal("removed app retained the unsafe diagnostic")
|
|
}
|
|
}
|
|
|
|
func applyTestIconFailure(
|
|
t *testing.T,
|
|
shell *AppShell,
|
|
identity application.IconEventIdentity,
|
|
code application.IconFailureCode,
|
|
) application.Event {
|
|
t.Helper()
|
|
event, err := application.NewIconFailedEvent(identity, code)
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if err := shell.ApplyEvent(event); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
return event
|
|
}
|
|
|
|
func testIconReference(pair string) string {
|
|
return "sha256:" + strings.Repeat(pair, 32)
|
|
}
|
|
|
|
func testIconIdentity(
|
|
t *testing.T,
|
|
requestID string,
|
|
appID string,
|
|
reference string,
|
|
dpi int,
|
|
) application.IconEventIdentity {
|
|
t.Helper()
|
|
identity, err := application.NewIconEventIdentity(
|
|
requestID,
|
|
appID,
|
|
reference,
|
|
dpi,
|
|
)
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
return identity
|
|
}
|
|
|
|
func waitIconSubmit(result <-chan error) error {
|
|
select {
|
|
case err := <-result:
|
|
return err
|
|
case <-time.After(2 * time.Second):
|
|
return errors.New("timed out waiting for icon relay")
|
|
}
|
|
}
|