diff --git a/README.md b/README.md
index d0e1860..f2ca002 100644
--- a/README.md
+++ b/README.md
@@ -110,6 +110,19 @@ want to change a few configuration items via method calls, you can `Pause()` the
 spinner first. After making the changes you can call `Unpause()`, and it will
 continue rendering like normal with the newly applied configuration.
 
+#### Supporting non-TTY Output Targets
+`yacspin` also has native support for non-TTY output targets. This is detected
+automatically within the constructor, or can be specified via the `NotTTY`
+`Config` struct field, and results in a different mode of operation.
+
+Specifically, when this is detected the spinner no longer uses colors, disables
+the automatic spinner animation, and instead only animates the spinner when updating the
+message. In addition, each animation is rendered on a new line instead of
+overwriting the current line.
+
+This should result in human-readable output without any changes needed by
+consumers, even when the system is writing to a non-TTY destination.
+
 ## Usage
 ```
 go get github.com/theckman/yacspin
diff --git a/go.mod b/go.mod
index 9e907b1..c4f7b22 100644
--- a/go.mod
+++ b/go.mod
@@ -6,11 +6,11 @@ require (
 	github.com/fatih/color v1.13.0
 	github.com/google/go-cmp v0.5.6
 	github.com/mattn/go-colorable v0.1.12
+	github.com/mattn/go-isatty v0.0.14
 	github.com/mattn/go-runewidth v0.0.13
 )
 
 require (
-	github.com/mattn/go-isatty v0.0.14 // indirect
 	github.com/rivo/uniseg v0.2.0 // indirect
 	golang.org/x/sys v0.0.0-20210927094055-39ccf1dd6fa6 // indirect
 )
diff --git a/go.sum b/go.sum
index 8085d2e..4b374fd 100644
--- a/go.sum
+++ b/go.sum
@@ -2,7 +2,6 @@ github.com/fatih/color v1.13.0 h1:8LOYc1KYPPmyKMuN8QV2DNRWNbLo6LZ0iLs8+mlH53w=
 github.com/fatih/color v1.13.0/go.mod h1:kLAiJbzzSOZDVNGyDpeOxJ47H46qBXwg5ILebYFFOfk=
 github.com/google/go-cmp v0.5.6 h1:BKbKCqvP6I+rmFHt06ZmyQtvB8xAkWdhFyr0ZUNZcxQ=
 github.com/google/go-cmp v0.5.6/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
-github.com/mattn/go-colorable v0.1.9 h1:sqDoxXbdeALODt0DAeJCVp38ps9ZogZEAXjus69YV3U=
 github.com/mattn/go-colorable v0.1.9/go.mod h1:u6P/XSegPjTcexA+o6vUJrdnUu04hMope9wVRipJSqc=
 github.com/mattn/go-colorable v0.1.12 h1:jF+Du6AlPIjs2BiUiQlKOX0rt3SujHxPnksPKZbaA40=
 github.com/mattn/go-colorable v0.1.12/go.mod h1:u5H1YNBxpqRaxsYJYSkiCWKzEfiAb1Gb520KVy5xxl4=
@@ -15,7 +14,6 @@ github.com/rivo/uniseg v0.2.0 h1:S1pD9weZBuJdFmowNwbpi7BJ8TNftyUImj/0WQi72jY=
 github.com/rivo/uniseg v0.2.0/go.mod h1:J6wj4VEh+S6ZtnVlnTBMWIodfgj8LQOQFoIToxlJtxc=
 golang.org/x/sys v0.0.0-20200116001909-b77594299b42/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
 golang.org/x/sys v0.0.0-20200223170610-d5e6a3e2c0ae/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
-golang.org/x/sys v0.0.0-20210630005230-0f9fa26af87c h1:F1jZWGFhYfh0Ci55sIpILtKKK8p3i2/krTr0H1rg74I=
 golang.org/x/sys v0.0.0-20210630005230-0f9fa26af87c/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
 golang.org/x/sys v0.0.0-20210927094055-39ccf1dd6fa6 h1:foEbQz/B0Oz6YIqu/69kfXPYeFQAuuMYFkjaqXzl5Wo=
 golang.org/x/sys v0.0.0-20210927094055-39ccf1dd6fa6/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
diff --git a/spinner.go b/spinner.go
index 4984f5b..ec48349 100644
--- a/spinner.go
+++ b/spinner.go
@@ -49,6 +49,7 @@ import (
 	"errors"
 	"fmt"
 	"io"
+	"math"
 	"os"
 	"strings"
 	"sync"
@@ -56,6 +57,7 @@ import (
 	"time"
 
 	"github.com/mattn/go-colorable"
+	"github.com/mattn/go-isatty"
 	"github.com/mattn/go-runewidth"
 )
 
@@ -182,6 +184,12 @@ type Config struct {
 	// StopFailColors are the colors used for the StopFail() printed line. This
 	// respects the ColorAll field.
 	StopFailColors []string
+
+	// NotTTY tells the spinner that the Writer should not be treated as a TTY.
+	// This results in the animation being disabled, with the animation only
+	// happening whenever the data is updated. This mode also renders each
+	// update on new line, versus reusing the current line.
+	NotTTY bool
 }
 
 // Spinner is a type representing an animated CLi terminal spinner. It's
@@ -196,6 +204,7 @@ type Spinner struct {
 	cursorHidden    bool
 	suffixAutoColon bool
 	isDumbTerm      bool
+	isNotTTY        bool
 	spinnerAtEnd    bool
 
 	status       *uint32
@@ -236,12 +245,17 @@ const (
 	statusUnpausing
 )
 
-// New creates a new unstarted spinner.
+// New creates a new unstarted spinner. If stdout does not appear to be a TTY,
+// this constructor implicitly sets cfg.NotTTY to true.
 func New(cfg Config) (*Spinner, error) {
 	if cfg.Frequency < 1 {
 		return nil, errors.New("cfg.Frequency must be greater than 0")
 	}
 
+	if !isatty.IsTerminal(os.Stdout.Fd()) && !isatty.IsCygwinTerminal(os.Stdout.Fd()) {
+		cfg.NotTTY = true
+	}
+
 	s := &Spinner{
 		buffer:            bytes.NewBuffer(make([]byte, 2048)),
 		mu:                &sync.Mutex{},
@@ -279,6 +293,11 @@ func New(cfg Config) (*Spinner, error) {
 	// can only error if the charset is empty, and we prevent that above
 	_ = s.CharSet(cfg.CharSet)
 
+	if cfg.NotTTY {
+		s.isNotTTY = true
+		s.isDumbTerm = true
+	}
+
 	if cfg.Writer == nil {
 		cfg.Writer = colorable.NewColorableStdout()
 	}
@@ -394,6 +413,11 @@ func (s *Spinner) Start() error {
 	s.frequencyUpdateCh = make(chan time.Duration, 4)
 	s.dataUpdateCh, s.cancelCh = make(chan struct{}, 1), make(chan struct{}, 1)
 
+	if s.isNotTTY {
+		// hack to prevent the animation from running if not a TTY
+		s.frequency = time.Duration(math.MaxInt64)
+	}
+
 	s.mu.Unlock()
 
 	// because of the atomic swap above, we know it's safe to mutate these
@@ -568,14 +592,15 @@ func (s *Spinner) painter(cancel, dataUpdate, pause <-chan struct{}, done chan<-
 		case <-timer.C:
 			lastTick = time.Now()
 
-			s.paintUpdate(timer, false)
+			s.paintUpdate(timer, true)
 
 		case <-pause:
 			<-s.unpauseCh
 			close(s.unpausedCh)
 
 		case <-dataUpdate:
-			s.paintUpdate(timer, true)
+			// if this is not a TTY: animate the spinner on the data update
+			s.paintUpdate(timer, s.isNotTTY)
 
 		case frequency := <-frequencyUpdate:
 			handleFrequencyUpdate(frequency, timer, lastTick)
@@ -592,7 +617,7 @@ func (s *Spinner) painter(cancel, dataUpdate, pause <-chan struct{}, done chan<-
 	}
 }
 
-func (s *Spinner) paintUpdate(timer *time.Timer, dataUpdate bool) {
+func (s *Spinner) paintUpdate(timer *time.Timer, animate bool) {
 	s.mu.Lock()
 
 	p := s.prefix
@@ -603,7 +628,7 @@ func (s *Spinner) paintUpdate(timer *time.Timer, dataUpdate bool) {
 	d := s.frequency
 	index := s.index
 
-	if !dataUpdate {
+	if animate {
 		s.index++
 
 		if s.index == len(s.chars) {
@@ -635,7 +660,7 @@ func (s *Spinner) paintUpdate(timer *time.Timer, dataUpdate bool) {
 			}
 		}
 
-		if _, err := paint(s.buffer, mw, c, p, m, suf, s.suffixAutoColon, s.colorAll, s.spinnerAtEnd, false, cFn); err != nil {
+		if _, err := paint(s.buffer, mw, c, p, m, suf, s.suffixAutoColon, s.colorAll, s.spinnerAtEnd, false, s.isNotTTY, cFn); err != nil {
 			panic(fmt.Sprintf("failed to paint line: %v", err))
 		}
 	} else {
@@ -643,7 +668,7 @@ func (s *Spinner) paintUpdate(timer *time.Timer, dataUpdate bool) {
 			panic(fmt.Sprintf("failed to erase line: %v", err))
 		}
 
-		n, err := paint(s.buffer, mw, c, p, m, suf, s.suffixAutoColon, false, s.spinnerAtEnd, false, fmt.Sprintf)
+		n, err := paint(s.buffer, mw, c, p, m, suf, s.suffixAutoColon, false, s.spinnerAtEnd, false, s.isNotTTY, fmt.Sprintf)
 		if err != nil {
 			panic(fmt.Sprintf("failed to paint line: %v", err))
 		}
@@ -657,7 +682,7 @@ func (s *Spinner) paintUpdate(timer *time.Timer, dataUpdate bool) {
 		}
 	}
 
-	if !dataUpdate {
+	if animate {
 		timer.Reset(d)
 	}
 }
@@ -700,7 +725,7 @@ func (s *Spinner) paintStop(chanOk bool) {
 
 		if c.Size > 0 || len(m) > 0 {
 			// paint the line with a newline as it's the final line
-			if _, err := paint(s.buffer, mw, c, p, m, suf, s.suffixAutoColon, s.colorAll, s.spinnerAtEnd, true, cFn); err != nil {
+			if _, err := paint(s.buffer, mw, c, p, m, suf, s.suffixAutoColon, s.colorAll, s.spinnerAtEnd, true, s.isNotTTY, cFn); err != nil {
 				panic(fmt.Sprintf("failed to paint line: %v", err))
 			}
 		}
@@ -710,7 +735,7 @@ func (s *Spinner) paintStop(chanOk bool) {
 		}
 
 		if c.Size > 0 || len(m) > 0 {
-			if _, err := paint(s.buffer, mw, c, p, m, suf, s.suffixAutoColon, false, s.spinnerAtEnd, true, fmt.Sprintf); err != nil {
+			if _, err := paint(s.buffer, mw, c, p, m, suf, s.suffixAutoColon, false, s.spinnerAtEnd, true, s.isNotTTY, fmt.Sprintf); err != nil {
 				panic(fmt.Sprintf("failed to paint line: %v", err))
 			}
 		}
@@ -733,6 +758,10 @@ func erase(w io.Writer) error {
 
 // eraseDumbTerm clears the line on dumb terminals
 func (s *Spinner) eraseDumbTerm(w io.Writer) error {
+	if s.isNotTTY {
+		return nil
+	}
+
 	clear := "\r" + strings.Repeat(" ", s.lastPrintLen) + "\r"
 
 	_, err := fmt.Fprint(w, clear)
@@ -758,7 +787,7 @@ func padChar(char character, maxWidth int) string {
 
 // paint writes a single line to the w, using the provided character, message,
 // and color function
-func paint(w io.Writer, maxWidth int, char character, prefix, message, suffix string, suffixAutoColon, colorAll, spinnerAtEnd, finalPaint bool, colorFn func(format string, a ...interface{}) string) (int, error) {
+func paint(w io.Writer, maxWidth int, char character, prefix, message, suffix string, suffixAutoColon, colorAll, spinnerAtEnd, finalPaint, notTTY bool, colorFn func(format string, a ...interface{}) string) (int, error) {
 	var output string
 
 	switch char.Size {
@@ -797,7 +826,7 @@ func paint(w io.Writer, maxWidth int, char character, prefix, message, suffix st
 		output = fmt.Sprintf("%s%s%s%s", prefix, colorFn(c), suffix, message)
 	}
 
-	if finalPaint {
+	if finalPaint || notTTY {
 		output += "\n"
 	}
 
@@ -810,6 +839,10 @@ func (s *Spinner) Frequency(d time.Duration) error {
 		return errors.New("duration must be greater than 0")
 	}
 
+	if s.isNotTTY {
+		return nil
+	}
+
 	s.mu.Lock()
 	defer s.mu.Unlock()
 
diff --git a/spinner_test.go b/spinner_test.go
index eab373a..ebb47b6 100644
--- a/spinner_test.go
+++ b/spinner_test.go
@@ -4,6 +4,7 @@ import (
 	"bytes"
 	"fmt"
 	"io"
+	"math"
 	"os"
 	"strings"
 	"sync"
@@ -407,10 +408,11 @@ func TestSpinner_notifyDataChange(t *testing.T) {
 
 func TestSpinner_Frequency(t *testing.T) {
 	tests := []struct {
-		name  string
-		input time.Duration
-		ch    chan time.Duration
-		err   string
+		name     string
+		input    time.Duration
+		isNotTTY bool
+		ch       chan time.Duration
+		err      string
 	}{
 		{
 			name: "invalid",
@@ -427,6 +429,12 @@ func TestSpinner_Frequency(t *testing.T) {
 			input: 42,
 			ch:    make(chan time.Duration, 1),
 		},
+		{
+			name:     "is_not_tty",
+			input:    42,
+			isNotTTY: true,
+			ch:       make(chan time.Duration, 1),
+		},
 	}
 
 	for _, tt := range tests {
@@ -437,6 +445,7 @@ func TestSpinner_Frequency(t *testing.T) {
 				mu:                &sync.Mutex{},
 				frequency:         0,
 				frequencyUpdateCh: tt.ch,
+				isNotTTY:          tt.isNotTTY,
 			}
 
 			tmr := time.NewTimer(2 * time.Second)
@@ -470,13 +479,17 @@ func TestSpinner_Frequency(t *testing.T) {
 						t.Errorf("channel receive got = %s, want %s", got, tt.input)
 					}
 				default:
-					t.Fatal("notification channel had no messages")
+					if !tt.isNotTTY {
+						t.Fatal("notification channel had no messages")
+					}
 				}
 			}
 
-			got := spinner.frequency
-			if got != tt.input {
-				t.Errorf("got = %s, want %s", got, tt.input)
+			if !tt.isNotTTY {
+				got := spinner.frequency
+				if got != tt.input {
+					t.Errorf("got = %s, want %s", got, tt.input)
+				}
 			}
 		})
 	}
@@ -760,6 +773,21 @@ func TestSpinner_Start(t *testing.T) {
 				stopFailMsg:     "stop fail msg",
 			},
 		},
+		{
+			name: "spinner_not_tty",
+			spinner: &Spinner{
+				buffer:          &bytes.Buffer{},
+				status:          uint32Ptr(statusStopped),
+				mu:              &sync.Mutex{},
+				frequency:       time.Millisecond,
+				colorFn:         fmt.Sprintf,
+				stopColorFn:     fmt.Sprintf,
+				stopFailColorFn: fmt.Sprintf,
+				stopMsg:         "stop msg",
+				stopFailMsg:     "stop fail msg",
+				isNotTTY:        true,
+			},
+		},
 	}
 
 	for _, tt := range tests {
@@ -789,6 +817,10 @@ func TestSpinner_Start(t *testing.T) {
 			if buf.Len() == 0 {
 				t.Fatal("painter did not write data")
 			}
+
+			if max := time.Duration(math.MaxInt64); tt.spinner.isNotTTY && tt.spinner.frequency != max {
+				t.Fatalf("tt.spinner.duration = %s, want %s", tt.spinner.frequency, max)
+			}
 		})
 	}
 }
@@ -1164,10 +1196,10 @@ func TestSpinner_paintUpdate(t *testing.T) {
 
 			tm := time.NewTimer(10 * time.Millisecond)
 
-			tt.spinner.paintUpdate(tm, false)
-			tt.spinner.paintUpdate(tm, false)
+			tt.spinner.paintUpdate(tm, true)
 			tt.spinner.paintUpdate(tm, true)
 			tt.spinner.paintUpdate(tm, false)
+			tt.spinner.paintUpdate(tm, true)
 			tm.Stop()
 
 			got := buf.String()
@@ -1494,82 +1526,169 @@ func Test_setToCharSlice(t *testing.T) {
 }
 
 func TestSpinner_painter(t *testing.T) {
-	if testing.Short() {
-		t.Skip("skipping test in short mode.")
-	}
+	t.Run("animated", func(t *testing.T) {
+		if testing.Short() {
+			t.Skip("skipping test in short mode.")
+		}
 
-	const want = "\r\033[K\ray msg\r\033[K\ray othermsg\r\033[K\raz msg\r\033[K\ray msg\r\x1b[K\rav stop\n"
+		const want = "\r\033[K\ray msg\r\033[K\ray othermsg\r\033[K\raz msg\r\033[K\ray msg\r\x1b[K\rav stop\n"
+
+		buf := &bytes.Buffer{}
+
+		cancel, done, dataUpdate, pause := make(chan struct{}), make(chan struct{}), make(chan struct{}), make(chan struct{})
+		frequencyUpdate := make(chan time.Duration, 1)
+
+		spinner := &Spinner{
+			buffer:            &bytes.Buffer{},
+			mu:                &sync.Mutex{},
+			writer:            buf,
+			prefix:            "a",
+			message:           "msg",
+			suffix:            " ",
+			maxWidth:          1,
+			colorFn:           fmt.Sprintf,
+			chars:             []character{{Value: "y", Size: 1}, {Value: "z", Size: 1}},
+			stopColorFn:       fmt.Sprintf,
+			stopMsg:           "stop",
+			stopChar:          character{Value: "v", Size: 1},
+			frequency:         2000 * time.Millisecond,
+			cancelCh:          cancel,
+			doneCh:            done,
+			dataUpdateCh:      dataUpdate,
+			frequencyUpdateCh: frequencyUpdate,
+		}
 
-	buf := &bytes.Buffer{}
+		go spinner.painter(cancel, dataUpdate, pause, done, frequencyUpdate)
 
-	cancel, done, dataUpdate, pause := make(chan struct{}), make(chan struct{}), make(chan struct{}), make(chan struct{})
-	frequencyUpdate := make(chan time.Duration, 1)
-
-	spinner := &Spinner{
-		buffer:            &bytes.Buffer{},
-		mu:                &sync.Mutex{},
-		writer:            buf,
-		prefix:            "a",
-		message:           "msg",
-		suffix:            " ",
-		maxWidth:          1,
-		colorFn:           fmt.Sprintf,
-		chars:             []character{{Value: "y", Size: 1}, {Value: "z", Size: 1}},
-		stopColorFn:       fmt.Sprintf,
-		stopMsg:           "stop",
-		stopChar:          character{Value: "v", Size: 1},
-		frequency:         2000 * time.Millisecond,
-		cancelCh:          cancel,
-		doneCh:            done,
-		dataUpdateCh:      dataUpdate,
-		frequencyUpdateCh: frequencyUpdate,
-	}
+		time.Sleep(500 * time.Millisecond)
+
+		spinner.mu.Lock()
 
-	go spinner.painter(cancel, dataUpdate, pause, done, frequencyUpdate)
+		spinner.message = "othermsg"
+		spinner.dataUpdateCh <- struct{}{}
 
-	time.Sleep(500 * time.Millisecond)
+		spinner.mu.Unlock()
 
-	spinner.mu.Lock()
+		time.Sleep(500 * time.Millisecond)
 
-	spinner.message = "othermsg"
-	spinner.dataUpdateCh <- struct{}{}
+		spinner.unpauseCh, spinner.unpausedCh = make(chan struct{}), make(chan struct{})
+		pause <- struct{}{}
 
-	spinner.mu.Unlock()
+		close(spinner.unpauseCh)
+		_, ok := <-spinner.unpausedCh
 
-	time.Sleep(500 * time.Millisecond)
+		if ok {
+			t.Fatal("unexpected successful channel receive")
+		}
 
-	spinner.unpauseCh, spinner.unpausedCh = make(chan struct{}), make(chan struct{})
-	pause <- struct{}{}
+		spinner.unpauseCh = nil
+		spinner.unpausedCh = nil
 
-	close(spinner.unpauseCh)
-	_, ok := <-spinner.unpausedCh
+		spinner.mu.Lock()
 
-	if ok {
-		t.Fatal("unexpected successful channel receive")
-	}
+		spinner.message = "msg"
+		spinner.frequency = 1000 * time.Millisecond
+		frequencyUpdate <- 1000 * time.Millisecond
 
-	spinner.unpauseCh = nil
-	spinner.unpausedCh = nil
+		spinner.mu.Unlock()
 
-	spinner.mu.Lock()
+		time.Sleep(1200 * time.Millisecond)
 
-	spinner.message = "msg"
-	spinner.frequency = 1000 * time.Millisecond
-	frequencyUpdate <- 1000 * time.Millisecond
+		cancel <- struct{}{}
 
-	spinner.mu.Unlock()
+		<-done
 
-	time.Sleep(1200 * time.Millisecond)
+		got := buf.String()
 
-	cancel <- struct{}{}
+		if diff := cmp.Diff(want, got); diff != "" {
+			t.Fatalf("output differs: (-want / +got)\n%s", diff)
+		}
+	})
+
+	t.Run("no_tty", func(t *testing.T) {
+		const want = "ay msg\naz othermsg\nay msg\naz msg\nav stop\n"
+
+		buf := &bytes.Buffer{}
+
+		cancel, done, dataUpdate, pause := make(chan struct{}), make(chan struct{}), make(chan struct{}), make(chan struct{})
+		frequencyUpdate := make(chan time.Duration, 1)
+
+		spinner := &Spinner{
+			buffer:            &bytes.Buffer{},
+			mu:                &sync.Mutex{},
+			writer:            buf,
+			prefix:            "a",
+			message:           "msg",
+			suffix:            " ",
+			maxWidth:          1,
+			colorFn:           fmt.Sprintf,
+			chars:             []character{{Value: "y", Size: 1}, {Value: "z", Size: 1}},
+			stopColorFn:       fmt.Sprintf,
+			stopMsg:           "stop",
+			stopChar:          character{Value: "v", Size: 1},
+			frequency:         time.Duration(math.MaxInt64),
+			cancelCh:          cancel,
+			doneCh:            done,
+			dataUpdateCh:      dataUpdate,
+			frequencyUpdateCh: frequencyUpdate,
+			isNotTTY:          true,
+			isDumbTerm:        true,
+		}
 
-	<-done
+		go spinner.painter(cancel, dataUpdate, pause, done, frequencyUpdate)
 
-	got := buf.String()
+		time.Sleep(100 * time.Millisecond)
 
-	if diff := cmp.Diff(want, got); diff != "" {
-		t.Fatalf("output differs: (-want / +got)\n%s", diff)
-	}
+		spinner.mu.Lock()
+
+		spinner.message = "othermsg"
+		spinner.dataUpdateCh <- struct{}{}
+
+		spinner.mu.Unlock()
+
+		time.Sleep(100 * time.Millisecond)
+
+		spinner.unpauseCh, spinner.unpausedCh = make(chan struct{}), make(chan struct{})
+		pause <- struct{}{}
+
+		close(spinner.unpauseCh)
+		_, ok := <-spinner.unpausedCh
+
+		if ok {
+			t.Fatal("unexpected successful channel receive")
+		}
+
+		spinner.unpauseCh = nil
+		spinner.unpausedCh = nil
+
+		spinner.mu.Lock()
+
+		spinner.message = "msg"
+		spinner.dataUpdateCh <- struct{}{}
+
+		spinner.mu.Unlock()
+
+		time.Sleep(100 * time.Millisecond)
+
+		spinner.mu.Lock()
+
+		spinner.message = "msg"
+		spinner.dataUpdateCh <- struct{}{}
+
+		spinner.mu.Unlock()
+
+		time.Sleep(100 * time.Millisecond)
+
+		cancel <- struct{}{}
+
+		<-done
+
+		got := buf.String()
+
+		if diff := cmp.Diff(want, got); diff != "" {
+			t.Fatalf("output differs: (-want / +got)\n%s", diff)
+		}
+	})
 }
 
 func TestSpinnerStatus_String(t *testing.T) {