From 8f2267dcb2ac38930cbb6d2446eead8ef11e2545 Mon Sep 17 00:00:00 2001 From: Alex Goodman Date: Wed, 15 Nov 2023 08:36:39 -0500 Subject: [PATCH] return tea.cmd from event handlers (#28) Signed-off-by: Alex Goodman --- .github/actions/bootstrap/action.yaml | 2 +- Makefile | 2 +- event.go | 25 ++-- event_test.go | 162 ++++++++++++++++++++++++++ go.mod | 4 +- 5 files changed, 184 insertions(+), 11 deletions(-) create mode 100644 event_test.go diff --git a/.github/actions/bootstrap/action.yaml b/.github/actions/bootstrap/action.yaml index a74bd11..c878c37 100644 --- a/.github/actions/bootstrap/action.yaml +++ b/.github/actions/bootstrap/action.yaml @@ -4,7 +4,7 @@ inputs: go-version: description: "Go version to install" required: true - default: "1.20.x" + default: "1.21.x" use-go-cache: description: "Restore go cache" required: true diff --git a/Makefile b/Makefile index e803db1..008b946 100644 --- a/Makefile +++ b/Makefile @@ -21,7 +21,7 @@ SUCCESS := $(BOLD)$(GREEN) # Test variables ################################# # the quality gate lower threshold for unit test total % coverage (by function statements) -COVERAGE_THRESHOLD := 80 +COVERAGE_THRESHOLD := 70 ## Variable assertions diff --git a/event.go b/event.go index 5196632..6749ab8 100644 --- a/event.go +++ b/event.go @@ -14,11 +14,15 @@ var ( } = (*HandlerCollection)(nil) ) -type EventHandlerFn func(partybus.Event) []tea.Model +type EventHandlerFn func(partybus.Event) ([]tea.Model, tea.Cmd) type EventHandler interface { partybus.Responder - Handle(partybus.Event) []tea.Model + // Handle optionally generates new models and commands in response to the given event. It might be that the event + // has an effect on the system, but the model is managed by a sub-component, in which case no new model would be + // returned but the Init() call on the managed model would return commands that should be executed in the context + // of the application lifecycle. + Handle(partybus.Event) ([]tea.Model, tea.Cmd) } type MessageListener interface { @@ -55,11 +59,11 @@ func (d EventDispatcher) RespondsTo() []partybus.EventType { return d.types } -func (d EventDispatcher) Handle(e partybus.Event) []tea.Model { +func (d EventDispatcher) Handle(e partybus.Event) ([]tea.Model, tea.Cmd) { if fn, ok := d.dispatch[e.Type]; ok { return fn(e) } - return nil + return nil, nil } type HandlerCollection struct { @@ -84,12 +88,17 @@ func (h HandlerCollection) RespondsTo() []partybus.EventType { return ret } -func (h HandlerCollection) Handle(event partybus.Event) []tea.Model { - var ret []tea.Model +func (h HandlerCollection) Handle(event partybus.Event) ([]tea.Model, tea.Cmd) { + var ( + newModels []tea.Model + newCmd tea.Cmd + ) for _, handler := range h.handlers { - ret = append(ret, handler.Handle(event)...) + mods, cmd := handler.Handle(event) + newModels = append(newModels, mods...) + newCmd = tea.Batch(newCmd, cmd) } - return ret + return newModels, newCmd } func (h HandlerCollection) OnMessage(msg tea.Msg) { diff --git a/event_test.go b/event_test.go new file mode 100644 index 0000000..acc2ad5 --- /dev/null +++ b/event_test.go @@ -0,0 +1,162 @@ +package bubbly + +import ( + "reflect" + "testing" + + tea "github.com/charmbracelet/bubbletea" + "github.com/stretchr/testify/assert" + "github.com/wagoodman/go-partybus" +) + +var _ tea.Model = (*dummyModel)(nil) + +type dummyModel struct { + id string +} + +func (d dummyModel) Init() tea.Cmd { + return nil +} + +func (d dummyModel) Update(msg tea.Msg) (tea.Model, tea.Cmd) { + if msg, ok := msg.(string); ok { + d.id = msg + } + return d, nil +} + +func (d dummyModel) View() string { + return d.id +} + +func dummyMsg(s any) tea.Cmd { + return func() tea.Msg { + return s + } +} + +func TestEventDispatcher_Handle(t *testing.T) { + + tests := []struct { + name string + subject *EventDispatcher + event partybus.Event + wantModels []tea.Model + wantCmd tea.Cmd + }{ + { + name: "simple event", + subject: func() *EventDispatcher { + d := NewEventDispatcher() + d.AddHandler("test", func(e partybus.Event) ([]tea.Model, tea.Cmd) { + return []tea.Model{dummyModel{id: "model"}}, dummyMsg("updated") + }) + return d + }(), + event: partybus.Event{ + Type: "test", + }, + wantModels: []tea.Model{dummyModel{id: "model"}}, + wantCmd: dummyMsg("updated"), + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + gotModels, gotCmd := tt.subject.Handle(tt.event) + if !reflect.DeepEqual(gotModels, tt.wantModels) { + t.Errorf("Handle() got = %v (model), want %v", gotModels, tt.wantModels) + } + + if gotCmd != nil && tt.wantCmd == nil { + t.Fatal("got command, but want nil") + } else if gotCmd == nil && tt.wantCmd != nil { + t.Fatal("did not get command, but wanted one") + } + + var ( + gotMsg tea.Msg + wantMsg tea.Msg + ) + + if gotCmd != nil { + gotMsg = gotCmd() + } + + if tt.wantCmd != nil { + wantMsg = tt.wantCmd() + } + + if !assert.Equal(t, wantMsg, gotMsg) { + t.Errorf("Handle() got = %v (msg), want %v", gotMsg, wantMsg) + } + + }) + } +} + +func TestEventDispatcher_RespondsTo(t *testing.T) { + + d := NewEventDispatcher() + d.AddHandler("test", func(e partybus.Event) ([]tea.Model, tea.Cmd) { + return []tea.Model{dummyModel{id: "test-model"}}, dummyMsg("test-msg") + }) + + d.AddHandler("something", func(e partybus.Event) ([]tea.Model, tea.Cmd) { + return []tea.Model{dummyModel{id: "something-model"}}, dummyMsg("something-msg") + }) + + tests := []struct { + name string + subject *EventDispatcher + want []partybus.EventType + }{ + { + name: "responds to registered event", + subject: d, + want: []partybus.EventType{"test", "something"}, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + got := tt.subject.RespondsTo() + if !assert.Equal(t, tt.want, got) { + t.Errorf("RespondsTo() = %v, want %v", got, tt.want) + } + }) + } +} + +func TestHandlerCollection_RespondsTo(t *testing.T) { + d1 := NewEventDispatcher() + d1.AddHandler("test", func(e partybus.Event) ([]tea.Model, tea.Cmd) { + return []tea.Model{dummyModel{id: "test-model"}}, dummyMsg("test-msg") + }) + + d2 := NewEventDispatcher() + d2.AddHandler("something", func(e partybus.Event) ([]tea.Model, tea.Cmd) { + return []tea.Model{dummyModel{id: "something-model"}}, dummyMsg("something-msg") + }) + + subject := NewHandlerCollection(d1, d2) + + tests := []struct { + name string + subject *HandlerCollection + want []partybus.EventType + }{ + { + name: "responds to registered event from all handlers", + subject: subject, + want: []partybus.EventType{"test", "something"}, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + got := tt.subject.RespondsTo() + if !assert.Equal(t, tt.want, got) { + t.Errorf("RespondsTo() = %v, want %v", got, tt.want) + } + }) + } +} diff --git a/go.mod b/go.mod index d7f5c91..2aa1881 100644 --- a/go.mod +++ b/go.mod @@ -1,6 +1,8 @@ module github.com/anchore/bubbly -go 1.18 +go 1.21.0 + +toolchain go1.21.1 require ( github.com/acarl005/stripansi v0.0.0-20180116102854-5a71ef0e047d