charmbracelet / bubbles

TUI components for Bubble Tea 🫧
MIT License
5.52k stars 265 forks source link

fix(textinput): make textinput.Model satisfy tea.Model interface #492

Open twpayne opened 7 months ago

twpayne commented 7 months ago

An early step towards #483.

twpayne commented 7 months ago

The CI failures are in a different package, so probably unrelated to this PR 😕

bashbunni commented 7 months ago

Hey @twpayne! Heads up, we're figuring out a solution to this right now. When we try to make the bubbles implement the tea.Model interface, we end up having to do a lot of type assertions. For example, here's what it would look like to update a viewport bubble that implements tea.Model:

// Pretend viewport.Model implements
// tea.Model here
type model {
    vp viewport.Model
}

// Some time later...
newModel, cmd := m.vp.Update(msg)
if newVPModel, ok := newModel(viewport.Model); ok {
    m.vp = newCVPModel
}

by comparison, this is the solution with the way to update the viewport model right now:

m.vp, cmd := m.vp.Update(msg) // DONE

Just wanted to be transparent on this one as any activity on this PR is likely going to be on hold until these refactors are ironed out :)

twpayne commented 7 months ago

Ah, thanks for the info. I wanted to add this so I could easily add tests in a follow-up PR. I've been using the following generic function in my tests for my small collection of bubbles:

func testRunModelWithInput[M tea.Model](t *testing.T, model M, input string) M {
    t.Helper()
    for _, msg := range makeKeyMsgs(input) {
        m, _ := model.Update(msg)
        var ok bool
        model, ok = m.(M)
        assert.True(t, ok)
    }
    return model
}

This allows me to write generic tests without type assertions, like this:

        t.Run(tc.name, func(t *testing.T) {
            actualModel := testRunModelWithInput(t, NewBoolInputModel("prompt", tc.defaultValue), tc.input)
            assert.Equal(t, tc.expectedCanceled, actualModel.Canceled())
            assert.Equal(t, tc.expectedValue, actualModel.Value())
        })

I'm sure you've already considering this and similar approaches.