mirror of
https://github.com/taigrr/bubbletea.git
synced 2026-04-02 11:09:17 -07:00
`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.
266 lines
4.6 KiB
Go
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))
|
|
}
|