Pad/internal/test/e2e/capture.go
Greg Pomerantz 6a43e3c0db Add e2e test harness for testing editor logic without Gio display
- internal/test/e2e/: FrameCapture, Harness, ElementAssertions, helpers
- internal/editor/logic.go: Add done channel and Done() method for graceful shutdown
- internal/editor/mock_setup.go: Mock filesystem for tests
- internal/browser/: Browser layout and search logic
- internal/io/: Worker pool for async tasks
- Update architecture docs and spec
2026-05-31 08:56:30 -04:00

103 lines
2.3 KiB
Go

// Package e2e provides end-to-end testing utilities for the Pad editor.
// It allows testing the logic layer without requiring a Gio display.
package e2e
import (
"sync"
"time"
"pad/internal/ui"
)
// FrameCapture receives frames from the logic layer without Gio.
type FrameCapture struct {
frames [][]ui.Element
mu sync.Mutex
closed chan struct{}
}
// NewFrameCapture creates a new frame capture instance.
func NewFrameCapture() *FrameCapture {
return &FrameCapture{
frames: make([][]ui.Element, 0),
closed: make(chan struct{}),
}
}
// CaptureFrame receives a frame and stores it for assertion.
func (fc *FrameCapture) CaptureFrame(elements []ui.Element) {
fc.mu.Lock()
defer fc.mu.Unlock()
// Copy to avoid mutation issues
cp := make([]ui.Element, len(elements))
copy(cp, elements)
fc.frames = append(fc.frames, cp)
}
// GetFrames returns all captured frames (thread-safe).
func (fc *FrameCapture) GetFrames() [][]ui.Element {
fc.mu.Lock()
defer fc.mu.Unlock()
result := make([][]ui.Element, len(fc.frames))
for i, frame := range fc.frames {
result[i] = make([]ui.Element, len(frame))
copy(result[i], frame)
}
return result
}
// FrameCount returns the number of frames captured so far.
func (fc *FrameCapture) FrameCount() int {
fc.mu.Lock()
defer fc.mu.Unlock()
return len(fc.frames)
}
// WaitForFrame blocks until at least one frame is captured or timeout.
func (fc *FrameCapture) WaitForFrame(timeout time.Duration) ([][]ui.Element, error) {
return fc.WaitForFrameCount(1, timeout)
}
// WaitForFrameCount blocks until at least N frames are captured or timeout.
func (fc *FrameCapture) WaitForFrameCount(count int, timeout time.Duration) ([][]ui.Element, error) {
deadline := time.Now().Add(timeout)
for {
fc.mu.Lock()
n := len(fc.frames)
fc.mu.Unlock()
if n >= count {
return fc.GetFrames(), nil
}
if time.Now().After(deadline) {
return nil, ErrTimeout
}
time.Sleep(10 * time.Millisecond)
}
}
// Close signals the capture to stop.
func (fc *FrameCapture) Close() {
close(fc.closed)
}
// ErrTimeout is returned when a wait operation times out.
var ErrTimeout = errTimeout{}
type errTimeout struct{}
func (errTimeout) Error() string {
return "timeout waiting for frames"
}
// IsTimeout returns true if the error is a timeout error.
func IsTimeout(err error) bool {
_, ok := err.(errTimeout)
return ok
}