- 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
103 lines
2.3 KiB
Go
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
|
|
}
|