package e2e import ( "fmt" "testing" "time" "pad/internal/ui" ) // TestScenario represents a complete test scenario with setup, actions, and assertions. type TestScenario struct { Name string Setup func(*Harness) Actions []HarnessAction Assertions []FrameAssertion FrameIndex int // which frame to assert on (-1 for last) Timeout time.Duration } // HarnessAction represents an action to perform on the harness. type HarnessAction func(*Harness) // FrameAssertion represents an assertion to make on a frame. type FrameAssertion func(*testing.T, []ui.Element) // --- Common actions --- // ActionSendConfig creates an action to send a config event. func ActionSendConfig(width, height int) HarnessAction { return func(h *Harness) { h.SendConfig(width, height) } } // ActionSendScale creates an action to send a scale event. func ActionSendScale(scale float32) HarnessAction { return func(h *Harness) { h.SendScale(scale) } } // ActionSendInput creates an action to send input events. func ActionSendInput(events []ui.InputEvent) HarnessAction { return func(h *Harness) { h.SendInput(events) } } // ActionSendSearchQuery creates an action to send a search query. func ActionSendSearchQuery(query string) HarnessAction { return func(h *Harness) { h.SendSearchQuery(query) } } // ActionWait creates an action to wait for frames. func ActionWait(count int, timeout time.Duration) HarnessAction { return func(h *Harness) { h.WaitForFrameCount(count, timeout) } } // --- Common assertions --- // AssertionHasElementCount creates an assertion for element count. func AssertionHasElementCount(count int) FrameAssertion { return func(t *testing.T, frame []ui.Element) { NewElementAssertions(t, frame).HasElementCount(count) } } // AssertionHasElementWithID creates an assertion for element ID. func AssertionHasElementWithID(id string) FrameAssertion { return func(t *testing.T, frame []ui.Element) { NewElementAssertions(t, frame).HasElementWithID(id) } } // AssertionHasLabelWithText creates an assertion for label text. func AssertionHasLabelWithText(text string) FrameAssertion { return func(t *testing.T, frame []ui.Element) { NewElementAssertions(t, frame).HasLabelWithText(text) } } // AssertionHasListViewWithItems creates an assertion for list view items. func AssertionHasListViewWithItems(items []string) FrameAssertion { return func(t *testing.T, frame []ui.Element) { NewElementAssertions(t, frame).HasListViewWithItems(items) } } // AssertionHasListViewWithItemCount creates an assertion for list view item count. func AssertionHasListViewWithItemCount(count int) FrameAssertion { return func(t *testing.T, frame []ui.Element) { NewElementAssertions(t, frame).HasListViewWithItemCount(count) } } // AssertionHasButtonWithText creates an assertion for button text. func AssertionHasButtonWithText(text string) FrameAssertion { return func(t *testing.T, frame []ui.Element) { NewElementAssertions(t, frame).HasButtonWithText(text) } } // AssertionHasIconWithName creates an assertion for icon name. func AssertionHasIconWithName(name string) FrameAssertion { return func(t *testing.T, frame []ui.Element) { NewElementAssertions(t, frame).HasIconWithName(name) } } // AssertionHasSearchBarWithQuery creates an assertion for search bar query. // NOTE: ui.SearchBar does not implement ui.Element (no Draw method), // so this assertion is currently a no-op placeholder. func AssertionHasSearchBarWithQuery(query string) FrameAssertion { return func(t *testing.T, frame []ui.Element) { // TODO: implement when SearchBar implements ui.Element } } // AssertionHasToastWithText creates an assertion for toast text. // NOTE: ui.Toast does not implement ui.Element (no Draw method), // so this assertion is currently a no-op placeholder. func AssertionHasToastWithText(text string) FrameAssertion { return func(t *testing.T, frame []ui.Element) { // TODO: implement when Toast implements ui.Element } } // AssertionHasCursorAt creates an assertion for cursor position. // NOTE: ui.Cursor does not implement ui.Element (no Draw method), // so this assertion is currently a no-op placeholder. func AssertionHasCursorAt(line, col int) FrameAssertion { return func(t *testing.T, frame []ui.Element) { // TODO: implement when Cursor implements ui.Element } } // AssertionNoOverlappingElements creates an assertion for no overlapping elements. func AssertionNoOverlappingElements() FrameAssertion { return func(t *testing.T, frame []ui.Element) { AssertNoOverlappingElements(t, frame) } } // AssertionElementPositions creates an assertion for element positions. func AssertionElementPositions(bounds ui.Region) FrameAssertion { return func(t *testing.T, frame []ui.Element) { AssertElementPositions(t, frame, bounds) } } // --- Scenario runner --- // RunScenario runs a test scenario. func RunScenario(t *testing.T, scenario TestScenario) { h := NewHarness() h.Run() defer h.Cleanup() // Run setup if scenario.Setup != nil { scenario.Setup(h) } // Run actions for _, action := range scenario.Actions { action(h) } // Wait for frames timeout := scenario.Timeout if timeout == 0 { timeout = DefaultTimeout } frames, err := h.WaitForFrameCount(1, timeout) if err != nil { t.Fatalf("timeout waiting for frames: %v", err) } // Determine which frame to assert on frameIndex := scenario.FrameIndex if frameIndex == -1 || frameIndex >= len(frames) { frameIndex = len(frames) - 1 } // Run assertions for _, assertion := range scenario.Assertions { assertion(t, frames[frameIndex]) } } // --- Helper functions --- // WaitForStableFrames waits until no new frames are captured for a period. func WaitForStableFrames(h *Harness, stabilityPeriod time.Duration) error { deadline := time.Now().Add(stabilityPeriod) for time.Now().Before(deadline) { count := h.FrameCount() time.Sleep(50 * time.Millisecond) if h.FrameCount() == count { return nil } } return fmt.Errorf("frames not stable after %v", stabilityPeriod) } // GetLastFrame returns the last captured frame. func GetLastFrame(h *Harness) []ui.Element { frames := h.GetFrames() if len(frames) == 0 { return nil } return frames[len(frames)-1] } // GetFrameByIndex returns a frame by index. func GetFrameByIndex(h *Harness, index int) []ui.Element { frames := h.GetFrames() if index < 0 || index >= len(frames) { return nil } return frames[index] } // DebugPrintFrames prints all captured frames for debugging. func DebugPrintFrames(h *Harness) { frames := h.GetFrames() for i, frame := range frames { fmt.Printf("Frame %d (%d elements):\n", i, len(frame)) fmt.Print(PrintFrame(frame)) } } // NewHarnessWithDefaults creates a harness with default configuration. func NewHarnessWithDefaults() *Harness { h := NewHarness() h.Run() h.SendConfig(780, 1688) // 390x844 @ 2x scale h.SendScale(2.0) return h } // NewHarnessWithCustomConfig creates a harness with custom configuration. func NewHarnessWithCustomConfig(width, height int, scale float32) *Harness { h := NewHarness() h.Run() h.SendConfig(width, height) h.SendScale(scale) return h } // WaitForInitialFrame waits for the initial frame after setup. func WaitForInitialFrame(h *Harness, timeout time.Duration) ([]ui.Element, error) { _, err := h.WaitForFrameCount(1, timeout) if err != nil { return nil, err } return GetLastFrame(h), nil } // WaitForNewFrame waits for a new frame after an action. func WaitForNewFrame(h *Harness, beforeCount int, timeout time.Duration) (int, error) { deadline := time.Now().Add(timeout) for time.Now().Before(deadline) { count := h.FrameCount() if count > beforeCount { return count, nil } time.Sleep(10 * time.Millisecond) } return beforeCount, fmt.Errorf("no new frames after %v", timeout) } // CompareFrameElements compares two frames element by element. func CompareFrameElements(frame1, frame2 []ui.Element) []string { var differences []string if len(frame1) != len(frame2) { differences = append(differences, fmt.Sprintf("element count: %d vs %d", len(frame1), len(frame2))) return differences } for i := range frame1 { region1 := frame1[i].Region() region2 := frame2[i].Region() if region1 != region2 { differences = append(differences, fmt.Sprintf("element %d region: %+v vs %+v", i, region1, region2)) } if frame1[i].Visible() != frame2[i].Visible() { differences = append(differences, fmt.Sprintf("element %d visible: %v vs %v", i, frame1[i].Visible(), frame2[i].Visible())) } } return differences } // --- Input event helpers --- // CreateTapEvent creates a tap input event. func CreateTapEvent(handler func(any)) ui.InputEvent { return ui.InputEvent{ Handler: handler, Data: ui.Interaction{Gesture: ui.Tap}, } } // CreateScrollEvent creates a scroll input event. func CreateScrollEvent(handler func(any)) ui.InputEvent { return ui.InputEvent{ Handler: handler, Data: ui.Interaction{Gesture: ui.Scroll}, } } // CreateKeyEvent creates a key input event. func CreateKeyEvent(handler func(any)) ui.InputEvent { return ui.InputEvent{ Handler: handler, Data: ui.Interaction{Gesture: ui.KeyDown}, } } // --- Structural assertions --- // AssertFrameHasNoInvisibleElements asserts that all elements in the frame are visible. func AssertFrameHasNoInvisibleElements(t *testing.T, frame []ui.Element) { for i, elem := range frame { if !elem.Visible() { t.Errorf("element %d (%T) is invisible", i, elem) } } } // AssertFrameHasNoEmptyRegions asserts that no elements have empty regions. func AssertFrameHasNoEmptyRegions(t *testing.T, frame []ui.Element) { for i, elem := range frame { region := elem.Region() if region.W == 0 || region.H == 0 { t.Errorf("element %d (%T) has empty region: %+v", i, elem, region) } } } // AssertFrameHasNoNegativeRegions asserts that no elements have negative regions. func AssertFrameHasNoNegativeRegions(t *testing.T, frame []ui.Element) { for i, elem := range frame { region := elem.Region() if region.X < 0 || region.Y < 0 || region.W < 0 || region.H < 0 { t.Errorf("element %d (%T) has negative region: %+v", i, elem, region) } } } // AssertFrameHasNoOutOfBoundsElements asserts that no elements are outside the expected bounds. func AssertFrameHasNoOutOfBoundsElements(t *testing.T, frame []ui.Element, bounds ui.Region) { for i, elem := range frame { region := elem.Region() if region.X < bounds.X || region.Y < bounds.Y || region.X+region.W > bounds.X+bounds.W || region.Y+region.H > bounds.Y+bounds.H { t.Errorf("element %d (%T) is out of bounds: %+v vs %+v", i, elem, region, bounds) } } } // AssertFrameHasNoDuplicateIDs asserts that no elements have duplicate IDs. func AssertFrameHasNoDuplicateIDs(t *testing.T, frame []ui.Element) { seen := make(map[string]bool) for i, elem := range frame { if idElem, ok := elem.(interface{ ID() string }); ok { id := idElem.ID() if id == "" { continue // skip elements without IDs } if seen[id] { t.Errorf("element %d has duplicate ID %q", i, id) } seen[id] = true } } }