Files
bubbletea/tea_test.go
Christian Muehlhaeuser c56884c0e2 feat: add generic event filter (#536)
`WithFilter` lets you supply an event filter that will be invoked
before Bubble Tea processes a `tea.Msg`. The event filter can return
any `tea.Msg` which will then get handled by Bubble Tea instead of
the original event. If the event filter returns nil, the event
will be ignored and Bubble Tea will not process it.

As an example, this could be used to prevent a program from
shutting down if there are unsaved changes.

Based on the fantastic work by @aschey and supersedes #521.

Resolves #472.
2023-04-17 22:02:55 +02:00

266 lines
4.6 KiB
Go

package tea
import (
"bytes"
"context"
"sync/atomic"
"testing"
"time"
)
type incrementMsg struct{}
type testModel struct {
executed atomic.Value
counter atomic.Value
}
func (m testModel) Init() Cmd {
return nil
}
func (m *testModel) Update(msg Msg) (Model, Cmd) {
switch msg.(type) {
case incrementMsg:
i := m.counter.Load()
if i == nil {
m.counter.Store(1)
} else {
m.counter.Store(i.(int) + 1)
}
case KeyMsg:
return m, Quit
}
return m, nil
}
func (m *testModel) View() string {
m.executed.Store(true)
return "success\n"
}
func TestTeaModel(t *testing.T) {
var buf bytes.Buffer
var in bytes.Buffer
in.Write([]byte("q"))
p := NewProgram(&testModel{}, WithInput(&in), WithOutput(&buf))
if _, err := p.Run(); err != nil {
t.Fatal(err)
}
if buf.Len() == 0 {
t.Fatal("no output")
}
}
func TestTeaQuit(t *testing.T) {
var buf bytes.Buffer
var in bytes.Buffer
m := &testModel{}
p := NewProgram(m, WithInput(&in), WithOutput(&buf))
go func() {
for {
time.Sleep(time.Millisecond)
if m.executed.Load() != nil {
p.Quit()
return
}
}
}()
if _, err := p.Run(); err != nil {
t.Fatal(err)
}
}
func TestTeaWithFilter(t *testing.T) {
testTeaWithFilter(t, 0)
testTeaWithFilter(t, 1)
testTeaWithFilter(t, 2)
}
func testTeaWithFilter(t *testing.T, preventCount uint32) {
var buf bytes.Buffer
var in bytes.Buffer
m := &testModel{}
shutdowns := uint32(0)
p := NewProgram(m,
WithInput(&in),
WithOutput(&buf),
WithFilter(func(_ Model, msg Msg) Msg {
if _, ok := msg.(QuitMsg); !ok {
return msg
}
if shutdowns < preventCount {
atomic.AddUint32(&shutdowns, 1)
return nil
}
return msg
}))
go func() {
for atomic.LoadUint32(&shutdowns) <= preventCount {
time.Sleep(time.Millisecond)
p.Quit()
}
}()
if err := p.Start(); err != nil {
t.Fatal(err)
}
if shutdowns != preventCount {
t.Errorf("Expected %d prevented shutdowns, got %d", preventCount, shutdowns)
}
}
func TestTeaKill(t *testing.T) {
var buf bytes.Buffer
var in bytes.Buffer
m := &testModel{}
p := NewProgram(m, WithInput(&in), WithOutput(&buf))
go func() {
for {
time.Sleep(time.Millisecond)
if m.executed.Load() != nil {
p.Kill()
return
}
}
}()
if _, err := p.Run(); err != ErrProgramKilled {
t.Fatalf("Expected %v, got %v", ErrProgramKilled, err)
}
}
func TestTeaContext(t *testing.T) {
ctx, cancel := context.WithCancel(context.Background())
var buf bytes.Buffer
var in bytes.Buffer
m := &testModel{}
p := NewProgram(m, WithContext(ctx), WithInput(&in), WithOutput(&buf))
go func() {
for {
time.Sleep(time.Millisecond)
if m.executed.Load() != nil {
cancel()
return
}
}
}()
if _, err := p.Run(); err != ErrProgramKilled {
t.Fatalf("Expected %v, got %v", ErrProgramKilled, err)
}
}
func TestTeaBatchMsg(t *testing.T) {
var buf bytes.Buffer
var in bytes.Buffer
inc := func() Msg {
return incrementMsg{}
}
m := &testModel{}
p := NewProgram(m, WithInput(&in), WithOutput(&buf))
go func() {
p.Send(BatchMsg{inc, inc})
for {
time.Sleep(time.Millisecond)
i := m.counter.Load()
if i != nil && i.(int) >= 2 {
p.Quit()
return
}
}
}()
if _, err := p.Run(); err != nil {
t.Fatal(err)
}
if m.counter.Load() != 2 {
t.Fatalf("counter should be 2, got %d", m.counter.Load())
}
}
func TestTeaSequenceMsg(t *testing.T) {
var buf bytes.Buffer
var in bytes.Buffer
inc := func() Msg {
return incrementMsg{}
}
m := &testModel{}
p := NewProgram(m, WithInput(&in), WithOutput(&buf))
go p.Send(sequenceMsg{inc, inc, Quit})
if _, err := p.Run(); err != nil {
t.Fatal(err)
}
if m.counter.Load() != 2 {
t.Fatalf("counter should be 2, got %d", m.counter.Load())
}
}
func TestTeaSequenceMsgWithBatchMsg(t *testing.T) {
var buf bytes.Buffer
var in bytes.Buffer
inc := func() Msg {
return incrementMsg{}
}
batch := func() Msg {
return BatchMsg{inc, inc}
}
m := &testModel{}
p := NewProgram(m, WithInput(&in), WithOutput(&buf))
go p.Send(sequenceMsg{batch, inc, Quit})
if _, err := p.Run(); err != nil {
t.Fatal(err)
}
if m.counter.Load() != 3 {
t.Fatalf("counter should be 3, got %d", m.counter.Load())
}
}
func TestTeaSend(t *testing.T) {
var buf bytes.Buffer
var in bytes.Buffer
m := &testModel{}
p := NewProgram(m, WithInput(&in), WithOutput(&buf))
// sending before the program is started is a blocking operation
go p.Send(Quit())
if _, err := p.Run(); err != nil {
t.Fatal(err)
}
// sending a message after program has quit is a no-op
p.Send(Quit())
}
func TestTeaNoRun(t *testing.T) {
var buf bytes.Buffer
var in bytes.Buffer
m := &testModel{}
NewProgram(m, WithInput(&in), WithOutput(&buf))
}