Pad/internal/test/e2e/edit_lifecycle_test.go
Greg Pomerantz 58725a5e6c Make state single-owner and stabilize race detector
Frame now carries view state (scale/focus/query) to the main goroutine,
which reads only the frame-receiver-stored snapshot. Renderer owns Gio
widget editors (registered by ID) and the draw scale. Autosave timer
sends a token to the logic goroutine instead of touching state;
Shutdown waits for the owner to exit before FlushAll.

Tests access state only through owner-side Inspect/WithState helpers
(harness + in-package). Fixed double-Harness.Run race in two e2e tests,
made TestWorkerPool_PriorityPreemption deterministic, and raised the
lazy-loading test timeout that was too short under -race.

go test -race ./... is now green.
2026-08-16 01:32:27 -04:00

102 lines
2.9 KiB
Go

package e2e_test
import (
"strings"
"testing"
"time"
"pad/internal/editor"
"pad/internal/test/e2e"
"pad/internal/ui"
)
// TestEditLifecycle verifies that edits made through the editor are persisted
// when the file is closed (via GoToBrowser -> FlushAll).
//
// All state access goes through the harness's owner-side helpers: the logic
// goroutine is the sole owner of State (architecture.md §1).
func TestEditLifecycle(t *testing.T) {
h := e2e.NewHarnessWithDefaults()
defer h.Cleanup()
// 1. Go to browser page
if err := h.WithState(func(st *editor.State) { editor.GoToBrowser(nil) }); err != nil {
t.Fatalf("GoToBrowser: %v", err)
}
time.Sleep(200 * time.Millisecond)
// 2. Open File
filename := "/notes.txt"
if err := h.WithState(func(st *editor.State) { editor.OpenFile(filename) }); err != nil {
t.Fatalf("OpenFile: %v", err)
}
// Wait for file to load
success := false
for i := 0; i < 20; i++ {
loaded, err := h.FileLoaded()
if err != nil {
t.Fatalf("FileLoaded: %v", err)
}
if loaded {
success = true
break
}
time.Sleep(100 * time.Millisecond)
}
if !success {
t.Fatal("Timed out waiting for file to load")
}
// 3. Edit File
// Original content of notes.txt is 198 bytes, let's append " UPDATED"
if err := h.WithState(func(st *editor.State) { editor.HandleInsert(" UPDATED") }); err != nil {
t.Fatalf("HandleInsert: %v", err)
}
time.Sleep(100 * time.Millisecond)
// 4. Close File (Go back to browser)
// This will trigger FlushAll()
if err := h.WithState(func(st *editor.State) { editor.GoToBrowser(nil) }); err != nil {
t.Fatalf("GoToBrowser: %v", err)
}
time.Sleep(500 * time.Millisecond) // Ensure it had time to process close
// 5. Re-open File
if err := h.WithState(func(st *editor.State) { editor.OpenFile(filename) }); err != nil {
t.Fatalf("OpenFile: %v", err)
}
time.Sleep(1000 * time.Millisecond) // Wait for re-open and load
// 6. Verify Edits
fullContent, err := h.FullContent()
if err != nil {
t.Fatalf("Failed to reconstruct content: %v", err)
}
if len(fullContent) <= 198 {
t.Errorf("Content did not persist. Length: %d, expected > 198", len(fullContent))
}
// 7. Verify Size on Screen (check bottom bar label)
frame := e2e.GetLastFrame(h)
var sizeText string
for _, elem := range frame {
if container, ok := elem.(ui.Container); ok {
// The bottom bar is a container, look for labels inside
for _, child := range container.Children {
if label, ok := child.(ui.Label); ok {
// The cursor position text is in the middle, containing "/"
if len(label.Text) > 0 && label.Text[0] != '/' && strings.Contains(label.Text, "/") {
sizeText = label.Text
}
}
}
}
}
expectedText := "0 / 206" // 198 original + " UPDATED" (8) = 206, cursor at top
if sizeText != expectedText {
t.Errorf("Bottom bar size label mismatch: got %q, expected %q", sizeText, expectedText)
}
}