This commit is contained in:
@@ -4,6 +4,7 @@ import (
|
||||
"errors"
|
||||
"strings"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"github.com/charmbracelet/bubbles/spinner"
|
||||
tea "github.com/charmbracelet/bubbletea"
|
||||
@@ -42,15 +43,53 @@ func operationSnapshot() app.Snapshot {
|
||||
}
|
||||
}
|
||||
|
||||
// applyOperationCommand drives the same command shape Bubble Tea drives in
|
||||
// production. An operation starts the stream wait and spinner tick as an
|
||||
// independent Batch; tests select the stream result without coupling its
|
||||
// completion to animation timing.
|
||||
func applyOperationCommand(t *testing.T, model Model, command tea.Cmd) (Model, tea.Cmd) {
|
||||
t.Helper()
|
||||
if command == nil {
|
||||
t.Fatal("operation command is nil")
|
||||
}
|
||||
updated, next := model.Update(command())
|
||||
message, ok := operationStreamMessage(command())
|
||||
if !ok {
|
||||
t.Fatal("operation command produced no operation-stream message")
|
||||
}
|
||||
updated, next := model.Update(message)
|
||||
return updated.(Model), next
|
||||
}
|
||||
|
||||
func operationStreamMessage(message tea.Msg) (tea.Msg, bool) {
|
||||
switch typed := message.(type) {
|
||||
case operationProgress:
|
||||
return typed, true
|
||||
case tea.BatchMsg:
|
||||
for _, command := range typed {
|
||||
if command == nil {
|
||||
continue
|
||||
}
|
||||
if message, ok := operationStreamMessage(command()); ok {
|
||||
return message, true
|
||||
}
|
||||
}
|
||||
case spinner.TickMsg:
|
||||
// Spinner ticks have their own production command chain. Applying one in
|
||||
// this helper would schedule animation work unrelated to stream progress.
|
||||
return nil, false
|
||||
}
|
||||
return nil, false
|
||||
}
|
||||
|
||||
func TestCompleteModelSpinnerAnimationHasDeterministicCadence(t *testing.T) {
|
||||
if got, want := applyingSpinner.FPS, 90*time.Millisecond; got != want {
|
||||
t.Fatalf("spinner cadence = %s, want %s", got, want)
|
||||
}
|
||||
if got, want := strings.Join(applyingSpinner.Frames, ""), "|/-\\"; got != want {
|
||||
t.Fatalf("spinner frames = %q, want %q", got, want)
|
||||
}
|
||||
}
|
||||
|
||||
func TestCompleteModelUsesAnimatedBubblesSpinnerOnlyWhileOperationRuns(t *testing.T) {
|
||||
updates := make(chan app.OperationUpdate)
|
||||
module := &interactiveScriptModule{snapshot: operationSnapshot(), streams: []chan app.OperationUpdate{updates}}
|
||||
@@ -61,6 +100,11 @@ func TestCompleteModelUsesAnimatedBubblesSpinnerOnlyWhileOperationRuns(t *testin
|
||||
if command == nil || !strings.Contains(model.View(), "| Applying") {
|
||||
t.Fatalf("operation did not start the Bubbles spinner:\n%s", model.View())
|
||||
}
|
||||
message := command()
|
||||
batch, ok := message.(tea.BatchMsg)
|
||||
if !ok || len(batch) != 2 {
|
||||
t.Fatalf("operation command = %T with %d children, want independent stream and spinner Batch", message, len(batch))
|
||||
}
|
||||
updated, next := model.Update(spinner.TickMsg{ID: model.operation.spinner.ID()})
|
||||
model = updated.(Model)
|
||||
if next == nil || !strings.Contains(model.View(), "/ Applying") {
|
||||
@@ -184,6 +228,70 @@ func TestCompleteModelReadOnlyRefreshDoesNotClearTargetMutationErrorForSameTarge
|
||||
}
|
||||
}
|
||||
|
||||
func TestCompleteModelFailedRefreshDoesNotReplaceTargetMutationError(t *testing.T) {
|
||||
mutationFailure := make(chan app.OperationUpdate, 1)
|
||||
refreshFailure := make(chan app.OperationUpdate, 1)
|
||||
module := &interactiveScriptModule{snapshot: operationSnapshot(), streams: []chan app.OperationUpdate{mutationFailure, refreshFailure}}
|
||||
model := resizedModel(t, module, themeModeNoColor, 120, 36)
|
||||
|
||||
updated, command := model.Update(tea.KeyMsg{Type: tea.KeySpace})
|
||||
model = updated.(Model)
|
||||
mutationFailure <- app.FailedOperationUpdate(module.snapshot,
|
||||
app.OperationStatus{Summary: "Shell Target update failed", TargetID: "shell"}, nil, "shell", errors.New("mutation permission denied"))
|
||||
close(mutationFailure)
|
||||
model, _ = applyOperationCommand(t, model, command)
|
||||
|
||||
updated, command = model.Update(keyMessage("r"))
|
||||
model = updated.(Model)
|
||||
refreshFailure <- app.FailedOperationUpdate(module.snapshot,
|
||||
app.OperationStatus{Summary: "Some Target checks failed", TargetID: "apt"}, nil, "apt", errors.New("refresh inspection failed"))
|
||||
close(refreshFailure)
|
||||
model, _ = applyOperationCommand(t, model, command)
|
||||
|
||||
view := model.View()
|
||||
if !strings.Contains(view, "Current error: Shell Target update failed") {
|
||||
t.Fatalf("failed Refresh Targets replaced the current mutation error:\n%s", view)
|
||||
}
|
||||
if !strings.Contains(view, "Status: Some Target checks failed") {
|
||||
t.Fatalf("failed Refresh Targets status was not shown separately:\n%s", view)
|
||||
}
|
||||
model = updateKey(model, "e")
|
||||
view = model.View()
|
||||
if !strings.Contains(view, "mutation permission denied") || strings.Contains(view, "refresh inspection failed") {
|
||||
t.Fatalf("expanded current error did not preserve the mutation detail:\n%s", view)
|
||||
}
|
||||
}
|
||||
|
||||
func TestCompleteModelRelatedTargetMutationFailureReplacesErrorForSameTarget(t *testing.T) {
|
||||
firstFailure := make(chan app.OperationUpdate, 1)
|
||||
relatedFailure := make(chan app.OperationUpdate, 1)
|
||||
module := &interactiveScriptModule{snapshot: operationSnapshot(), streams: []chan app.OperationUpdate{firstFailure, relatedFailure}}
|
||||
model := resizedModel(t, module, themeModeNoColor, 120, 36)
|
||||
|
||||
updated, command := model.Update(tea.KeyMsg{Type: tea.KeySpace})
|
||||
model = updated.(Model)
|
||||
firstFailure <- app.FailedOperationUpdate(module.snapshot,
|
||||
app.OperationStatus{Summary: "Initial Shell Target failure", TargetID: "shell"}, nil, "shell", errors.New("initial detail"))
|
||||
close(firstFailure)
|
||||
model, _ = applyOperationCommand(t, model, command)
|
||||
|
||||
updated, command = model.Update(tea.KeyMsg{Type: tea.KeySpace})
|
||||
model = updated.(Model)
|
||||
relatedFailure <- app.FailedOperationUpdate(module.snapshot,
|
||||
app.OperationStatus{Summary: "Latest Shell Target failure", TargetID: "shell"}, nil, "shell", errors.New("latest detail"))
|
||||
close(relatedFailure)
|
||||
model, _ = applyOperationCommand(t, model, command)
|
||||
|
||||
view := model.View()
|
||||
if !strings.Contains(view, "Current error: Latest Shell Target failure") || strings.Contains(view, "Initial Shell Target failure") {
|
||||
t.Fatalf("related Target mutation failure did not replace the prior error:\n%s", view)
|
||||
}
|
||||
model = updateKey(model, "e")
|
||||
if view := model.View(); !strings.Contains(view, "latest detail") || strings.Contains(view, "initial detail") {
|
||||
t.Fatalf("related Target mutation failure did not replace the prior detail:\n%s", view)
|
||||
}
|
||||
}
|
||||
|
||||
func TestCompleteModelRelatedTargetMutationClearsErrorForSameTarget(t *testing.T) {
|
||||
failure := make(chan app.OperationUpdate, 1)
|
||||
resolution := make(chan app.OperationUpdate, 1)
|
||||
|
||||
Reference in New Issue
Block a user