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 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") } }