package e2e_test import ( "testing" "time" "pad/internal/editor" "pad/internal/test/e2e" "pad/internal/ui" ) // editorTapHandler mimics the production tap/long-press/double-tap closure // that layout attaches to the editor's TextField (a type switch over the // gesture data). e2e injects it because the harness has no renderer to derive // the gestures. func editorTapHandler(data any) { switch pt := data.(type) { case ui.Point: editor.HandleTapAt(pt.X, pt.Y) case ui.DoubleTapPoint: editor.HandleDoubleTapAt(pt.X, pt.Y) case ui.LongPressPoint: editor.HandleLongPressAt(pt.X, pt.Y) } } // setupTouchE2E opens a real file, forces the just-opened tap guard into the // past, installs a single-line synthetic GlyphLayout (11 glyphs at x=10*i) and // returns the harness plus the editor region (app-local Dp). func setupTouchE2E(t *testing.T) (*e2e.Harness, ui.Region) { t.Helper() content := "hello world\nsecond line\n" h, _ := realFileHarness(t, "notes.txt", content) h.SendConfig(780, 1688) // Let the just-opened guard (300 ms) lapse so test taps are not swallowed. time.Sleep(350 * time.Millisecond) // Synthetic single-line layout for "hello world" (the visible window of a // small file is the whole buffer, so window-relative == file-relative). if err := h.WithState(func(st *editor.State) { n := 11 bo := make([]int, n) xs := make([]ui.Dp, n) ys := make([]ui.Dp, n) ad := make([]ui.Dp, n) for i := 0; i < n; i++ { bo[i] = i xs[i] = ui.Dp(10 * i) ys[i] = 0 ad[i] = 10 } st.Editor.GlyphLayout = ui.GlyphLayout{ByteOffsets: bo, X: xs, Y: ys, Advance: ad} }); err != nil { t.Fatalf("set GlyphLayout: %v", err) } // Wait for a frame so EditorRegion is set, then read it. if _, err := h.WaitForFrame(e2e.DefaultTimeout); err != nil { t.Fatalf("wait for frame: %v", err) } regAny, err := h.Inspect(func(st *editor.State) any { return st.EditorRegion }) if err != nil { t.Fatalf("inspect region: %v", err) } reg := regAny.(ui.Region) if reg.W <= 0 { t.Fatalf("editor region not laid out: %+v", reg) } return h, reg } // frameHasMenu reports whether any captured frame carries a Menu element. func frameHasMenu(frames [][]ui.Element) bool { for _, f := range frames { for _, el := range f { if el.Type() == "menu" { return true } } } return false } func selectionOf(t *testing.T, h *e2e.Harness) (int, int) { t.Helper() v, err := h.Inspect(func(st *editor.State) any { return [2]int{st.Editor.SelectionStart, st.Editor.SelectionEnd} }) if err != nil { t.Fatalf("inspect selection: %v", err) } s := v.([2]int) return s[0], s[1] } func TestRealFile_TouchSelectionLongPressWord(t *testing.T) { h, reg := setupTouchE2E(t) defer h.Cleanup() before := h.FrameCount() h.SendInput([]ui.InputEvent{{ Handler: editorTapHandler, Data: ui.LongPressPoint{X: reg.X + 2, Y: reg.Y + 8}, // glyph 0 of "hello" }}) frames, err := h.WaitForFrameCount(before+1, e2e.DefaultTimeout) if err != nil { t.Fatalf("wait frame: %v", err) } s, e := selectionOf(t, h) if s != 0 || e != 5 { t.Errorf("selection = [%d,%d), want [0,5) (\"hello\")", s, e) } if !frameHasMenu(frames) { t.Error("no Menu element in frames after long press") } } func TestRealFile_TouchSelectionDragEndHandle(t *testing.T) { h, reg := setupTouchE2E(t) defer h.Cleanup() // Long press selects "hello". before := h.FrameCount() h.SendInput([]ui.InputEvent{{ Handler: editorTapHandler, Data: ui.LongPressPoint{X: reg.X + 2, Y: reg.Y + 8}, }}) if _, err := h.WaitForFrameCount(before+1, e2e.DefaultTimeout); err != nil { t.Fatalf("wait frame: %v", err) } // Drag the end handle (which=1) past the last glyph: selects the line. before = h.FrameCount() h.SendInput([]ui.InputEvent{ {Handler: editor.HandleSelDragEvt, Data: ui.SelectionDragEvent{Which: 1, X: reg.X + 115, Y: reg.Y + 8}}, {Handler: editor.HandleSelDragEvt, Data: ui.SelectionDragEvent{Which: 1, X: reg.X + 115, Y: reg.Y + 8}}, {Handler: editor.HandleSelDragEvt, Data: ui.SelectionDragEnd{}}, }) if _, err := h.WaitForFrameCount(before+1, e2e.DefaultTimeout); err != nil { t.Fatalf("wait frame: %v", err) } s, e := selectionOf(t, h) if s != 0 || e != 11 { t.Errorf("selection = [%d,%d), want [0,11) after end-handle drag", s, e) } } func TestRealFile_TouchSelectionMenuCopyPaste(t *testing.T) { h, reg := setupTouchE2E(t) defer h.Cleanup() // Long press selects "hello" and shows the menu. before := h.FrameCount() h.SendInput([]ui.InputEvent{{ Handler: editorTapHandler, Data: ui.LongPressPoint{X: reg.X + 2, Y: reg.Y + 8}, }}) frames, err := h.WaitForFrameCount(before+1, e2e.DefaultTimeout) if err != nil { t.Fatalf("wait frame: %v", err) } if !frameHasMenu(frames) { t.Fatal("no Menu element after long press") } // Read the menu geometry from the owner and tap the first item (Copy) // at its center. menuAny, err := h.Inspect(func(st *editor.State) any { return struct { R ui.Region Items []ui.MenuItem }{st.Editor.MenuRect, st.Editor.MenuItems} }) if err != nil { t.Fatalf("inspect menu: %v", err) } menu := menuAny.(struct { R ui.Region Items []ui.MenuItem }) if len(menu.Items) == 0 || menu.R.W <= 0 { t.Fatal("menu not visible with items") } tapX := menu.R.X + menu.Items[0].W/2 tapY := menu.R.Y + menu.R.H/2 before = h.FrameCount() h.SendInput([]ui.InputEvent{ {Handler: func(d any) { p := d.(ui.Point) editor.HandleMenuTap(p.X, p.Y) }, Data: ui.Point{X: tapX, Y: tapY}}, }) if _, err := h.WaitForFrameCount(before+1, e2e.DefaultTimeout); err != nil { t.Fatalf("wait frame: %v", err) } // The copy went to the clipboard channel; the harness has no main loop, so // feed it back through PasteChan and verify the selection is replaced. h.Logic().PasteChan() <- "XY" got, err := h.FullContent() if err != nil { t.Fatalf("full content: %v", err) } // "hello" (the selection) is replaced by "XY". want := "XY world\nsecond line\n" if got != want { t.Errorf("content after paste = %q, want %q", got, want) } }