Pad/internal/test/e2e/harness.go

129 lines
2.9 KiB
Go

package e2e
import (
"sync"
"time"
"pad/internal/editor"
"pad/internal/ui"
)
// Harness orchestrates the test environment for e2e tests.
type Harness struct {
logic *editor.Logic
capture *FrameCapture
logicWg sync.WaitGroup
frameReceiverDone chan struct{}
wg sync.WaitGroup
}
// HarnessOption configures the test harness.
type HarnessOption func(*Harness)
// NewHarness creates a test harness with the given options.
func NewHarness(opts ...HarnessOption) *Harness {
h := &Harness{
logic: editor.NewLogic(nil),
capture: NewFrameCapture(),
frameReceiverDone: make(chan struct{}),
}
for _, opt := range opts {
opt(h)
}
return h
}
// Run starts the logic goroutine and frame capture.
func (h *Harness) Run() {
h.logicWg.Add(1)
h.wg.Add(1)
go func() {
defer h.wg.Done()
defer h.logicWg.Done()
h.logic.Run()
}()
h.wg.Add(1)
go func() {
defer h.wg.Done()
for {
select {
case frame := <-h.logic.FrameChan():
h.capture.CaptureFrame(frame)
case <-h.frameReceiverDone:
return
}
}
}()
}
// SendConfig simulates a window config event (resize).
func (h *Harness) SendConfig(width, height int) {
h.logic.ConfigChan() <- editor.ConfigEvent{
PixelWidth: width,
PixelHeight: height,
}
}
// SendScale simulates a scale factor change.
func (h *Harness) SendScale(scale float32) {
h.logic.ConfigChan() <- editor.ScaleEvent{Scale: scale}
}
// SendInput simulates user input events.
func (h *Harness) SendInput(events []ui.InputEvent) {
h.logic.InputChan() <- events
}
// SendSearchQuery simulates a search query update.
func (h *Harness) SendSearchQuery(query string) {
h.logic.SearchQueryChan() <- query
}
// GetFrames returns all captured frames.
func (h *Harness) GetFrames() [][]ui.Element {
return h.capture.GetFrames()
}
// FrameCount returns the number of frames captured so far.
func (h *Harness) FrameCount() int {
return h.capture.FrameCount()
}
// State returns the editor state for inspection.
func (h *Harness) State() *editor.State {
return h.logic.State()
}
// WaitForFrameCount blocks until at least N frames are captured.
func (h *Harness) WaitForFrameCount(count int, timeout time.Duration) ([][]ui.Element, error) {
return h.capture.WaitForFrameCount(count, timeout)
}
// WaitForFrame blocks until at least one frame is captured.
func (h *Harness) WaitForFrame(timeout time.Duration) ([][]ui.Element, error) {
return h.capture.WaitForFrame(timeout)
}
// Cleanup stops all goroutines.
func (h *Harness) Cleanup() {
// 1. Signal logic to stop sending frames
h.logic.Done()
// 2. Wait for logic goroutine to fully exit (no more frameChan sends)
h.logicWg.Wait()
// 3. Now safe to stop the frame receiver
close(h.frameReceiverDone)
// 4. Wait for frame receiver to finish
h.wg.Wait()
h.capture.Close()
}
// DefaultTimeout is the default timeout for waiting operations.
const DefaultTimeout = 5 * time.Second