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(), 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() } // 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