Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

teatest: Obtaining final state of terminal #212

Open
crab-apple opened this issue Oct 6, 2024 · 4 comments
Open

teatest: Obtaining final state of terminal #212

crab-apple opened this issue Oct 6, 2024 · 4 comments
Labels
enhancement New feature or request

Comments

@crab-apple
Copy link

I'm not sure if I'm looking at this the right way. Here's what I'm trying to achieve:

Say I have a program that starts by making some asynchronous request to an external system (with a tea.Cmd).

In my tests I use a fake instead of the real system. My fake returns a result with a delay of a few milliseconds.

Now I want to test that, when the command is complete, my program displays "foo" and it doesn't display "bar". Testing that it displays "foo" is easy with WaitFor.

teatest.WaitFor(
    t, tm.Output(),
    func(bts []byte) bool {
        return bytes.Contains(bts, []byte("foo"))
    }
    ...
)

But how do I test that the program doesn't display "bar"?

One approach would be to wait until the program displays "foo", and then get the FinalOutput and assert that it doesn't contain "bar". This approach has two issues:

  1. With the current implementation, as soon as I've used WaitFor I've consumed part of the output, so FinalOutput will not actually give me the final "display".
  2. Even if I could use WaitFor without consuming the output, the condition "contains 'foo'" still doesn't give me any guarantee that I'm looking at the final state. The program could be in the middle of writing out the output.

Before switching to teatest, I was using a homemade solution for testing where I didn't have problem 1 above but I still had problem 2. Essentially I haven't found a way to wait until the program has reached a "stable" state, for lack of a better term. I suppose that would be a state where there are no commands still pending to return a result, and the program has completely processed all the pending messages.

Am I missing something?

@crab-apple crab-apple added the enhancement New feature or request label Oct 6, 2024
@caarlos0
Copy link
Member

caarlos0 commented Oct 7, 2024

you can check tm.FinalOutput() after the wait, i believe.

@crab-apple
Copy link
Author

you can check tm.FinalOutput() after the wait, i believe.

But it doesn't return the full output, as it has already been partially or totally consumed by the wait, right? Unless I'm doing something wrong. Here is an example:

Given this model, which always returns "Hello" as the view:

type Model struct {
}

func InitialModel() Model {
	return Model{}
}

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

func (m Model) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
	return m, nil
}

func (m Model) View() string {
	return "Hello"
}

I can assert the output with WaitFor:

func TestCheckIntermediate(t *testing.T) {

	tm := teatest.NewTestModel(t, InitialModel())

	teatest.WaitFor(t, tm.Output(), func(bts []byte) bool {
		return bytes.Contains(bts, []byte("Hello"))
	})
}

And I can assert the final output:

func TestCheckFinal(t *testing.T) {

	tm := teatest.NewTestModel(t, InitialModel())

	assert.NoError(t, tm.Quit())

	final, err := io.ReadAll(tm.FinalOutput(t))
	assert.NoError(t, err)

	assert.True(t, bytes.Contains(final, []byte("Hello")), "Final output should contain 'Hello'")
}

But if I wait and then also check the final output, then the test fails:

func TestCheckIntermediateAndFinal(t *testing.T) {

	tm := teatest.NewTestModel(t, InitialModel())

	teatest.WaitFor(t, tm.Output(), func(bts []byte) bool {
		return bytes.Contains(bts, []byte("Hello"))
	})

	assert.NoError(t, tm.Quit())

	final, err := io.ReadAll(tm.FinalOutput(t))
	assert.NoError(t, err)

	assert.True(t, bytes.Contains(final, []byte("Hello")), "Final output should contain 'Hello'.")
}

@caarlos0
Copy link
Member

You want the final render of the model, right?

If so, you can do something like:

func TestCheckIntermediateAndFinal(t *testing.T) {
	tm := teatest.NewTestModel(t, Model{})

	teatest.WaitFor(t, tm.Output(), func(bts []byte) bool {
		return bytes.Contains(bts, []byte("Hello"))
	})

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

	final := tm.FinalModel(t).View()
	if final != "Hello" {
		t.Errorf("expected model to be 'Hello', was '%s'", final)
	}
}

the View() method should not have any side effect (i.e. change the model), so you can do it like this :)

@crab-apple
Copy link
Author

Right, that makes sense.

I feel it makes more sense even, as my tests are interested in the view itself, rather than the output stream. The output stream could contain content of previous views that have been since painted over (in my limited understanding of how this works).

Which kind of begs the question: when waiting for a given state, wouldn't I be interested in the view as well, and not in the raw output? Would it make sense to have a method like this?

	teatest.WaitForView(t, tm, func(view string) bool {
		return strings.Contains(view, "Hello")
	})

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
enhancement New feature or request
Projects
None yet
Development

No branches or pull requests

2 participants