- 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
358 lines
8.7 KiB
Go
358 lines
8.7 KiB
Go
package e2e
|
|
|
|
import (
|
|
"fmt"
|
|
"reflect"
|
|
"testing"
|
|
|
|
"pad/internal/ui"
|
|
)
|
|
|
|
// ElementAssertions provides fluent assertions on element slices.
|
|
type ElementAssertions struct {
|
|
elements []ui.Element
|
|
t testing.TB
|
|
failed bool
|
|
}
|
|
|
|
// NewElementAssertions creates assertions for a frame.
|
|
func NewElementAssertions(t testing.TB, elements []ui.Element) *ElementAssertions {
|
|
return &ElementAssertions{
|
|
elements: elements,
|
|
t: t,
|
|
failed: false,
|
|
}
|
|
}
|
|
|
|
// HasElementCount asserts the number of elements.
|
|
func (ea *ElementAssertions) HasElementCount(count int) *ElementAssertions {
|
|
if len(ea.elements) != count {
|
|
ea.t.Errorf("expected %d elements, got %d", count, len(ea.elements))
|
|
ea.failed = true
|
|
}
|
|
return ea
|
|
}
|
|
|
|
// HasElementWithID asserts an element exists with the given ID.
|
|
func (ea *ElementAssertions) HasElementWithID(id string) *ElementAssertions {
|
|
found := false
|
|
for _, elem := range ea.elements {
|
|
if idElem, ok := elem.(interface{ ID() string }); ok {
|
|
if idElem.ID() == id {
|
|
found = true
|
|
break
|
|
}
|
|
}
|
|
}
|
|
if !found {
|
|
ea.t.Errorf("expected element with ID %q, not found", id)
|
|
ea.failed = true
|
|
}
|
|
return ea
|
|
}
|
|
|
|
// HasElementOfType asserts an element of the given type exists.
|
|
func (ea *ElementAssertions) HasElementOfType(t reflect.Type) *ElementAssertions {
|
|
found := false
|
|
for _, elem := range ea.elements {
|
|
if reflect.TypeOf(elem) == t {
|
|
found = true
|
|
break
|
|
}
|
|
}
|
|
if !found {
|
|
ea.t.Errorf("expected element of type %v, not found", t)
|
|
ea.failed = true
|
|
}
|
|
return ea
|
|
}
|
|
|
|
// HasLabelWithText asserts a Label element contains the given text.
|
|
func (ea *ElementAssertions) HasLabelWithText(text string) *ElementAssertions {
|
|
found := false
|
|
for _, elem := range ea.elements {
|
|
if label, ok := elem.(ui.Label); ok {
|
|
if label.Text == text {
|
|
found = true
|
|
break
|
|
}
|
|
}
|
|
}
|
|
if !found {
|
|
ea.t.Errorf("expected Label with text %q, not found", text)
|
|
ea.failed = true
|
|
}
|
|
return ea
|
|
}
|
|
|
|
// HasListViewWithItemCount asserts a ListView has the expected number of items.
|
|
func (ea *ElementAssertions) HasListViewWithItemCount(count int) *ElementAssertions {
|
|
var listView *ui.ListView
|
|
for _, elem := range ea.elements {
|
|
if lv, ok := elem.(ui.ListView); ok {
|
|
listView = &lv
|
|
break
|
|
}
|
|
}
|
|
|
|
if listView == nil {
|
|
ea.t.Error("expected ListView element, not found")
|
|
ea.failed = true
|
|
return ea
|
|
}
|
|
|
|
if len(listView.Items) != count {
|
|
ea.t.Errorf("expected ListView with %d items, got %d", count, len(listView.Items))
|
|
ea.failed = true
|
|
}
|
|
return ea
|
|
}
|
|
|
|
// HasListViewWithItems asserts a ListView has the expected items.
|
|
func (ea *ElementAssertions) HasListViewWithItems(expected []string) *ElementAssertions {
|
|
var listView *ui.ListView
|
|
for _, elem := range ea.elements {
|
|
if lv, ok := elem.(ui.ListView); ok {
|
|
listView = &lv
|
|
break
|
|
}
|
|
}
|
|
|
|
if listView == nil {
|
|
ea.t.Error("expected ListView element, not found")
|
|
ea.failed = true
|
|
return ea
|
|
}
|
|
|
|
for _, expItem := range expected {
|
|
found := false
|
|
for _, item := range listView.Items {
|
|
if item.Text == expItem {
|
|
found = true
|
|
break
|
|
}
|
|
}
|
|
if !found {
|
|
ea.t.Errorf("expected ListView item %q, not found", expItem)
|
|
ea.failed = true
|
|
}
|
|
}
|
|
return ea
|
|
}
|
|
|
|
// HasElementInRegion asserts an element exists within the given region bounds.
|
|
func (ea *ElementAssertions) HasElementInRegion(region ui.Region) *ElementAssertions {
|
|
found := false
|
|
for _, elem := range ea.elements {
|
|
elemRegion := elem.Region()
|
|
if elemRegion.X >= region.X &&
|
|
elemRegion.Y >= region.Y &&
|
|
elemRegion.X+elemRegion.W <= region.X+region.W &&
|
|
elemRegion.Y+elemRegion.H <= region.Y+region.H {
|
|
found = true
|
|
break
|
|
}
|
|
}
|
|
if !found {
|
|
ea.t.Errorf("expected element in region %+v, not found", region)
|
|
ea.failed = true
|
|
}
|
|
return ea
|
|
}
|
|
|
|
// HasVisibleElement asserts at least one visible element exists.
|
|
func (ea *ElementAssertions) HasVisibleElement() *ElementAssertions {
|
|
found := false
|
|
for _, elem := range ea.elements {
|
|
if elem.Visible() {
|
|
found = true
|
|
break
|
|
}
|
|
}
|
|
if !found {
|
|
ea.t.Error("expected at least one visible element, none found")
|
|
ea.failed = true
|
|
}
|
|
return ea
|
|
}
|
|
|
|
// HasButtonWithText asserts a Button element with the given text exists.
|
|
func (ea *ElementAssertions) HasButtonWithText(text string) *ElementAssertions {
|
|
found := false
|
|
for _, elem := range ea.elements {
|
|
if btn, ok := elem.(ui.Button); ok {
|
|
if btn.Text == text {
|
|
found = true
|
|
break
|
|
}
|
|
}
|
|
}
|
|
if !found {
|
|
ea.t.Errorf("expected Button with text %q, not found", text)
|
|
ea.failed = true
|
|
}
|
|
return ea
|
|
}
|
|
|
|
// HasTextFieldWithID asserts a TextField with the given ID exists.
|
|
func (ea *ElementAssertions) HasTextFieldWithID(id string) *ElementAssertions {
|
|
found := false
|
|
for _, elem := range ea.elements {
|
|
if tf, ok := elem.(ui.TextField); ok {
|
|
if tf.ID() == id {
|
|
found = true
|
|
break
|
|
}
|
|
}
|
|
}
|
|
if !found {
|
|
ea.t.Errorf("expected TextField with ID %q, not found", id)
|
|
ea.failed = true
|
|
}
|
|
return ea
|
|
}
|
|
|
|
// HasIconWithName asserts an Icon with the given name exists.
|
|
func (ea *ElementAssertions) HasIconWithName(name string) *ElementAssertions {
|
|
found := false
|
|
for _, elem := range ea.elements {
|
|
if icon, ok := elem.(ui.Icon); ok {
|
|
if icon.Name == name {
|
|
found = true
|
|
break
|
|
}
|
|
}
|
|
}
|
|
if !found {
|
|
ea.t.Errorf("expected Icon with name %q, not found", name)
|
|
ea.failed = true
|
|
}
|
|
return ea
|
|
}
|
|
|
|
// GetElementByID returns the element with the given ID, or nil if not found.
|
|
func (ea *ElementAssertions) GetElementByID(id string) ui.Element {
|
|
for _, elem := range ea.elements {
|
|
if idElem, ok := elem.(interface{ ID() string }); ok {
|
|
if idElem.ID() == id {
|
|
return elem
|
|
}
|
|
}
|
|
}
|
|
return nil
|
|
}
|
|
|
|
// GetElementByType returns the first element of the given type, or nil if not found.
|
|
func (ea *ElementAssertions) GetElementByType(t reflect.Type) ui.Element {
|
|
for _, elem := range ea.elements {
|
|
if reflect.TypeOf(elem) == t {
|
|
return elem
|
|
}
|
|
}
|
|
return nil
|
|
}
|
|
|
|
// GetListItems returns all list item texts from the first ListView found.
|
|
func (ea *ElementAssertions) GetListItems() []string {
|
|
for _, elem := range ea.elements {
|
|
if lv, ok := elem.(ui.ListView); ok {
|
|
items := make([]string, len(lv.Items))
|
|
for i, item := range lv.Items {
|
|
items[i] = item.Text
|
|
}
|
|
return items
|
|
}
|
|
}
|
|
return nil
|
|
}
|
|
|
|
// GetLabelTexts returns all label texts from the frame.
|
|
func (ea *ElementAssertions) GetLabelTexts() []string {
|
|
var texts []string
|
|
for _, elem := range ea.elements {
|
|
if label, ok := elem.(ui.Label); ok {
|
|
texts = append(texts, label.Text)
|
|
}
|
|
}
|
|
return texts
|
|
}
|
|
|
|
// GetButtonTexts returns all button texts from the frame.
|
|
func (ea *ElementAssertions) GetButtonTexts() []string {
|
|
var texts []string
|
|
for _, elem := range ea.elements {
|
|
if btn, ok := elem.(ui.Button); ok {
|
|
texts = append(texts, btn.Text)
|
|
}
|
|
}
|
|
return texts
|
|
}
|
|
|
|
// GetElementRegions returns all element regions from the frame.
|
|
func (ea *ElementAssertions) GetElementRegions() []ui.Region {
|
|
regions := make([]ui.Region, len(ea.elements))
|
|
for i, elem := range ea.elements {
|
|
regions[i] = elem.Region()
|
|
}
|
|
return regions
|
|
}
|
|
|
|
// Failed returns true if any assertion has failed.
|
|
func (ea *ElementAssertions) Failed() bool {
|
|
return ea.failed
|
|
}
|
|
|
|
// --- Helper functions ---
|
|
|
|
// regionsOverlap checks if two regions overlap.
|
|
func regionsOverlap(a, b ui.Region) bool {
|
|
return !(a.X+a.W <= b.X || b.X+b.W <= a.X ||
|
|
a.Y+a.H <= b.Y || b.Y+b.H <= a.Y)
|
|
}
|
|
|
|
// AssertNoOverlappingElements asserts that no elements in the frame overlap.
|
|
func AssertNoOverlappingElements(t testing.TB, frame []ui.Element) {
|
|
for i := 0; i < len(frame); i++ {
|
|
for j := i + 1; j < len(frame); j++ {
|
|
regionA := frame[i].Region()
|
|
regionB := frame[j].Region()
|
|
|
|
if regionsOverlap(regionA, regionB) {
|
|
t.Errorf("elements %d (%T) and %d (%T) overlap: %+v vs %+v",
|
|
i, frame[i], j, frame[j], regionA, regionB)
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
// AssertElementPositions asserts that elements are positioned within expected bounds.
|
|
func AssertElementPositions(t testing.TB, 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 outside expected bounds: %+v vs %+v",
|
|
i, elem, region, bounds)
|
|
}
|
|
}
|
|
}
|
|
|
|
// AssertFrameStructure asserts the basic structure of a frame.
|
|
func AssertFrameStructure(t testing.TB, frame []ui.Element, expectedCount int) {
|
|
if len(frame) != expectedCount {
|
|
t.Errorf("expected %d elements, got %d", expectedCount, len(frame))
|
|
}
|
|
}
|
|
|
|
// PrintFrame prints all elements in a frame for debugging.
|
|
func PrintFrame(frame []ui.Element) string {
|
|
var result string
|
|
for i, elem := range frame {
|
|
region := elem.Region()
|
|
result += fmt.Sprintf(" [%d] %T: region=%+v visible=%v\n",
|
|
i, elem, region, elem.Visible())
|
|
}
|
|
return result
|
|
}
|