Skip to content

Commit

Permalink
feat: add teatest/v2 to use bubbletea/v2 (#197)
Browse files Browse the repository at this point in the history
* feat: add teatest/v2 to use bubbletea@v2-exp

Will keep this open until we push a bubbletea/v2 release

* feat(ci): add teatest-v2 workflow

* fix: use bubbletea/v2
  • Loading branch information
aymanbagabas authored Sep 18, 2024
1 parent 9b0f832 commit 9350707
Show file tree
Hide file tree
Showing 13 changed files with 689 additions and 1 deletion.
9 changes: 9 additions & 0 deletions .github/dependabot.yml
Original file line number Diff line number Diff line change
Expand Up @@ -146,6 +146,15 @@ updates:
commit-message:
prefix: "chore"
include: "scope"
- package-ecosystem: "gomod"
directory: "/exp/teatest/v2"
schedule:
interval: "daily"
labels:
- "dependencies"
commit-message:
prefix: "chore"
include: "scope"
- package-ecosystem: "gomod"
directory: "/input"
schedule:
Expand Down
30 changes: 30 additions & 0 deletions .github/workflows/teatest-v2.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
# auto-generated by scripts/builds. DO NOT EDIT.
name: teatest-v2

on:
push:
branches:
- main
pull_request:
paths:
- exp/teatest/v2/**
- .github/workflows/teatest-v2.yml

jobs:
build:
strategy:
matrix:
os: [ubuntu-latest, macos-latest, windows-latest]
runs-on: ${{ matrix.os }}
defaults:
run:
working-directory: ./exp/teatest/v2
steps:
- uses: actions/checkout@v4
- uses: actions/setup-go@v5
with:
go-version-file: ./exp/teatest/v2/go.mod
cache: true
cache-dependency-path: ./exp/teatest/go.sum
- run: go build -v ./...
- run: go test -race -v ./...
130 changes: 130 additions & 0 deletions exp/teatest/v2/app_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,130 @@
package teatest_test

import (
"bytes"
"fmt"
"io"
"regexp"
"testing"
"time"

tea "github.com/charmbracelet/bubbletea/v2"
"github.com/charmbracelet/x/exp/teatest/v2"
)

func TestApp(t *testing.T) {
m := model(10)
tm := teatest.NewTestModel(
t, m,
teatest.WithInitialTermSize(70, 30),
)
t.Cleanup(func() {
if err := tm.Quit(); err != nil {
t.Fatal(err)
}
})

time.Sleep(time.Second + time.Millisecond*200)
tm.Type("I'm typing things, but it'll be ignored by my program")
tm.Send("ignored msg")
tm.Send(tea.KeyPressMsg{
Code: tea.KeyEnter,
})

if err := tm.Quit(); err != nil {
t.Fatal(err)
}

out := readBts(t, tm.FinalOutput(t, teatest.WithFinalTimeout(time.Second)))
if !regexp.MustCompile(`This program will exit in \d+ seconds`).Match(out) {
t.Fatalf("output does not match the given regular expression: %s", string(out))
}
teatest.RequireEqualOutput(t, out)

if tm.FinalModel(t).(model) != 9 {
t.Errorf("expected model to be 10, was %d", m)
}
}

func TestAppInteractive(t *testing.T) {
m := model(10)
tm := teatest.NewTestModel(
t, m,
teatest.WithInitialTermSize(70, 30),
)

time.Sleep(time.Second + time.Millisecond*200)
tm.Send("ignored msg")

if bts := readBts(t, tm.Output()); !bytes.Contains(bts, []byte("This program will exit in 9 seconds")) {
t.Fatalf("output does not match: expected %q", string(bts))
}

teatest.WaitFor(t, tm.Output(), func(out []byte) bool {
return bytes.Contains(out, []byte("This program will exit in 7 seconds"))
}, teatest.WithDuration(5*time.Second), teatest.WithCheckInterval(time.Millisecond*10))

tm.Send(tea.KeyPressMsg{
Code: tea.KeyEnter,
})

if err := tm.Quit(); err != nil {
t.Fatal(err)
}

if tm.FinalModel(t).(model) != 7 {
t.Errorf("expected model to be 7, was %d", m)
}
}

func readBts(tb testing.TB, r io.Reader) []byte {
tb.Helper()
bts, err := io.ReadAll(r)
if err != nil {
tb.Fatal(err)
}
return bts
}

// A model can be more or less any type of data. It holds all the data for a
// program, so often it's a struct. For this simple example, however, all
// we'll need is a simple integer.
type model int

// Init optionally returns an initial command we should run. In this case we
// want to start the timer.
func (m model) Init() (tea.Model, tea.Cmd) {
return m, tick
}

// Update is called when messages are received. The idea is that you inspect the
// message and send back an updated model accordingly. You can also return
// a command, which is a function that performs I/O and returns a message.
func (m model) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
switch msg.(type) {
case tea.KeyMsg:
return m, tea.Quit
case tickMsg:
m--
if m <= 0 {
return m, tea.Quit
}
return m, tick
}
return m, nil
}

// View returns a string based on data in the model. That string which will be
// rendered to the terminal.
func (m model) View() string {
return fmt.Sprintf("Hi. This program will exit in %d seconds. To quit sooner press any key.\n", m)
}

// Messages are events that we respond to in our Update function. This
// particular one indicates that the timer has ticked.
type tickMsg time.Time

func tick() tea.Msg {
time.Sleep(time.Second)
return tickMsg{}
}
27 changes: 27 additions & 0 deletions exp/teatest/v2/go.mod
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
module github.com/charmbracelet/x/exp/teatest/v2

go 1.19

require (
github.com/charmbracelet/bubbletea/v2 v2.0.0-20240918154035-3313a4cfa033
github.com/charmbracelet/x/exp/golden v0.0.0-20240806155701-69247e0abc2a
)

require (
github.com/aymanbagabas/go-osc52/v2 v2.0.1 // indirect
github.com/aymanbagabas/go-udiff v0.2.0 // indirect
github.com/charmbracelet/lipgloss v0.13.0 // indirect
github.com/charmbracelet/x/ansi v0.3.2 // indirect
github.com/charmbracelet/x/term v0.2.0 // indirect
github.com/charmbracelet/x/windows v0.2.0 // indirect
github.com/lucasb-eyer/go-colorful v1.2.0 // indirect
github.com/mattn/go-isatty v0.0.20 // indirect
github.com/mattn/go-runewidth v0.0.16 // indirect
github.com/muesli/ansi v0.0.0-20230316100256-276c6243b2f6 // indirect
github.com/muesli/cancelreader v0.2.2 // indirect
github.com/muesli/termenv v0.15.2 // indirect
github.com/rivo/uniseg v0.4.7 // indirect
github.com/xo/terminfo v0.0.0-20220910002029-abceb7e1c41e // indirect
golang.org/x/sync v0.8.0 // indirect
golang.org/x/sys v0.25.0 // indirect
)
39 changes: 39 additions & 0 deletions exp/teatest/v2/go.sum
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
github.com/aymanbagabas/go-osc52/v2 v2.0.1 h1:HwpRHbFMcZLEVr42D4p7XBqjyuxQH5SMiErDT4WkJ2k=
github.com/aymanbagabas/go-osc52/v2 v2.0.1/go.mod h1:uYgXzlJ7ZpABp8OJ+exZzJJhRNQ2ASbcXHWsFqH8hp8=
github.com/aymanbagabas/go-udiff v0.2.0 h1:TK0fH4MteXUDspT88n8CKzvK0X9O2xu9yQjWpi6yML8=
github.com/aymanbagabas/go-udiff v0.2.0/go.mod h1:RE4Ex0qsGkTAJoQdQQCA0uG+nAzJO/pI/QwceO5fgrA=
github.com/charmbracelet/bubbletea/v2 v2.0.0-20240918154035-3313a4cfa033 h1:eEQ1R5wHOfU53qifp4eNlRPfDwRcVGXmJVsBTpbr92Y=
github.com/charmbracelet/bubbletea/v2 v2.0.0-20240918154035-3313a4cfa033/go.mod h1:j0gn4ft5CE7NDYNZjAA3hBM8t2OPjI8urxuAD0oR4w8=
github.com/charmbracelet/lipgloss v0.13.0 h1:4X3PPeoWEDCMvzDvGmTajSyYPcZM4+y8sCA/SsA3cjw=
github.com/charmbracelet/lipgloss v0.13.0/go.mod h1:nw4zy0SBX/F/eAO1cWdcvy6qnkDUxr8Lw7dvFrAIbbY=
github.com/charmbracelet/x/ansi v0.3.2 h1:wsEwgAN+C9U06l9dCVMX0/L3x7ptvY1qmjMwyfE6USY=
github.com/charmbracelet/x/ansi v0.3.2/go.mod h1:dk73KoMTT5AX5BsX0KrqhsTqAnhZZoCBjs7dGWp4Ktw=
github.com/charmbracelet/x/exp/golden v0.0.0-20240806155701-69247e0abc2a h1:G99klV19u0QnhiizODirwVksQB91TJKV/UaTnACcG30=
github.com/charmbracelet/x/exp/golden v0.0.0-20240806155701-69247e0abc2a/go.mod h1:wDlXFlCrmJ8J+swcL/MnGUuYnqgQdW9rhSD61oNMb6U=
github.com/charmbracelet/x/term v0.2.0 h1:cNB9Ot9q8I711MyZ7myUR5HFWL/lc3OpU8jZ4hwm0x0=
github.com/charmbracelet/x/term v0.2.0/go.mod h1:GVxgxAbjUrmpvIINHIQnJJKpMlHiZ4cktEQCN6GWyF0=
github.com/charmbracelet/x/windows v0.2.0 h1:ilXA1GJjTNkgOm94CLPeSz7rar54jtFatdmoiONPuEw=
github.com/charmbracelet/x/windows v0.2.0/go.mod h1:ZibNFR49ZFqCXgP76sYanisxRyC+EYrBE7TTknD8s1s=
github.com/lucasb-eyer/go-colorful v1.2.0 h1:1nnpGOrhyZZuNyfu1QjKiUICQ74+3FNCN69Aj6K7nkY=
github.com/lucasb-eyer/go-colorful v1.2.0/go.mod h1:R4dSotOR9KMtayYi1e77YzuveK+i7ruzyGqttikkLy0=
github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWEY=
github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y=
github.com/mattn/go-runewidth v0.0.16 h1:E5ScNMtiwvlvB5paMFdw9p4kSQzbXFikJ5SQO6TULQc=
github.com/mattn/go-runewidth v0.0.16/go.mod h1:Jdepj2loyihRzMpdS35Xk/zdY8IAYHsh153qUoGf23w=
github.com/muesli/ansi v0.0.0-20230316100256-276c6243b2f6 h1:ZK8zHtRHOkbHy6Mmr5D264iyp3TiX5OmNcI5cIARiQI=
github.com/muesli/ansi v0.0.0-20230316100256-276c6243b2f6/go.mod h1:CJlz5H+gyd6CUWT45Oy4q24RdLyn7Md9Vj2/ldJBSIo=
github.com/muesli/cancelreader v0.2.2 h1:3I4Kt4BQjOR54NavqnDogx/MIoWBFa0StPA8ELUXHmA=
github.com/muesli/cancelreader v0.2.2/go.mod h1:3XuTXfFS2VjM+HTLZY9Ak0l6eUKfijIfMUZ4EgX0QYo=
github.com/muesli/termenv v0.15.2 h1:GohcuySI0QmI3wN8Ok9PtKGkgkFIk7y6Vpb5PvrY+Wo=
github.com/muesli/termenv v0.15.2/go.mod h1:Epx+iuz8sNs7mNKhxzH4fWXGNpZwUaJKRS1noLXviQ8=
github.com/rivo/uniseg v0.2.0/go.mod h1:J6wj4VEh+S6ZtnVlnTBMWIodfgj8LQOQFoIToxlJtxc=
github.com/rivo/uniseg v0.4.7 h1:WUdvkW8uEhrYfLC4ZzdpI2ztxP1I582+49Oc5Mq64VQ=
github.com/rivo/uniseg v0.4.7/go.mod h1:FN3SvrM+Zdj16jyLfmOkMNblXMcoc8DfTHruCPUcx88=
github.com/xo/terminfo v0.0.0-20220910002029-abceb7e1c41e h1:JVG44RsyaB9T2KIHavMF/ppJZNG9ZpyihvCd0w101no=
github.com/xo/terminfo v0.0.0-20220910002029-abceb7e1c41e/go.mod h1:RbqR21r5mrJuqunuUZ/Dhy/avygyECGrLceyNeo4LiM=
golang.org/x/exp v0.0.0-20220909182711-5c715a9e8561 h1:MDc5xs78ZrZr3HMQugiXOAkSZtfTpbJLDr/lwfgO53E=
golang.org/x/sync v0.8.0 h1:3NFvSEYkUoMifnESzZl15y791HH1qU2xm6eCJU5ZPXQ=
golang.org/x/sync v0.8.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk=
golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.25.0 h1:r+8e+loiHxRqhXVl6ML1nO3l1+oFoWbnlu2Ehimmi34=
golang.org/x/sys v0.25.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
87 changes: 87 additions & 0 deletions exp/teatest/v2/send_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,87 @@
package teatest_test

import (
"fmt"
"strings"
"testing"
"time"

tea "github.com/charmbracelet/bubbletea/v2"
"github.com/charmbracelet/x/exp/teatest/v2"
)

func TestAppSendToOtherProgram(t *testing.T) {
m1 := &connectedModel{
name: "m1",
}
m2 := &connectedModel{
name: "m2",
}

tm1 := teatest.NewTestModel(t, m1, teatest.WithInitialTermSize(70, 30))
t.Cleanup(func() {
if err := tm1.Quit(); err != nil {
t.Fatal(err)
}
})
tm2 := teatest.NewTestModel(t, m2, teatest.WithInitialTermSize(70, 30))
t.Cleanup(func() {
if err := tm2.Quit(); err != nil {
t.Fatal(err)
}
})
m1.programs = append(m1.programs, tm2)
m2.programs = append(m2.programs, tm1)

tm1.Type("pp")
tm2.Type("pppp")

tm1.Type("q")
tm2.Type("q")

out1 := readBts(t, tm1.FinalOutput(t, teatest.WithFinalTimeout(time.Second)))
out2 := readBts(t, tm2.FinalOutput(t, teatest.WithFinalTimeout(time.Second)))

if string(out1) != string(out2) {
t.Errorf("output of both models should be the same, got:\n%v\nand:\n%v\n", string(out1), string(out2))
}

teatest.RequireEqualOutput(t, out1)
}

type connectedModel struct {
name string
programs []interface{ Send(tea.Msg) }
msgs []string
}

type ping string

func (m *connectedModel) Init() (tea.Model, tea.Cmd) {
return m, nil
}

func (m *connectedModel) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
switch msg := msg.(type) {
case tea.KeyMsg:
switch msg.String() {
case "p":
send := ping("from " + m.name)
m.msgs = append(m.msgs, string(send))
for _, p := range m.programs {
p.Send(send)
}
fmt.Printf("sent ping %q to others\n", send)
case "q":
return m, tea.Quit
}
case ping:
fmt.Printf("rcvd ping %q on %s\n", msg, m.name)
m.msgs = append(m.msgs, string(msg))
}
return m, nil
}

func (m *connectedModel) View() string {
return "All pings:\n" + strings.Join(m.msgs, "\n")
}
Loading

0 comments on commit 9350707

Please sign in to comment.