The 10dp margin used to surround the top and bottom bars, leaving a gap around them. The bars now span the full screen width and sit flush with the top/bottom edges (merging with the system UI); their inner content (icons, labels) keeps the margin so it still aligns with the text area. The editor text area and the browser file list keep the margin, as before, and the editor gains the 20dp of height the bar margins used to occupy. Tests: menu-geometry expectations shifted with editorRegion.Y 42 -> 32; the below-placement clamp test now expects the window-top clamp; the stable-end test anchors at line 3 (less headroom above line 2 now).
202 lines
7.0 KiB
Go
202 lines
7.0 KiB
Go
package e2e_test
|
|
|
|
import (
|
|
"testing"
|
|
"time"
|
|
|
|
"pad/internal/editor"
|
|
"pad/internal/test/e2e"
|
|
"pad/internal/ui"
|
|
)
|
|
|
|
// TestEditorClickToMoveCursorWithScroll verifies that a tap in the editor
|
|
// accounts for the current scroll offset and lands on the right line.
|
|
//
|
|
// State setup/assertions use the harness's owner-side helpers (WithState /
|
|
// CursorPosition / Inspect); the tap handler closure runs on the logic
|
|
// goroutine via SendInput, so its reads of editor.TheState are safe.
|
|
func TestEditorClickToMoveCursorWithScroll(t *testing.T) {
|
|
// NewHarnessWithDefaults already starts the logic goroutine.
|
|
h := e2e.NewHarnessWithDefaults()
|
|
defer h.Cleanup()
|
|
|
|
// Switch to editor page (owner-side)
|
|
if err := h.WithState(func(st *editor.State) { editor.GoToEditor(nil) }); err != nil {
|
|
t.Fatalf("GoToEditor: %v", err)
|
|
}
|
|
h.SendConfig(780, 1688)
|
|
// Give it a moment to initialize
|
|
time.Sleep(200 * time.Millisecond)
|
|
|
|
// Wait for frame
|
|
_, err := h.WaitForFrameCount(1, 5*time.Second)
|
|
if err != nil {
|
|
t.Fatalf("timeout waiting for frame: %v", err)
|
|
}
|
|
|
|
// Setup:
|
|
// - 2 lines, each 10 chars + newline.
|
|
// - Line height = 14 * 1.2 = 16.8 (rounded?) Let's check EditorLineHeight()
|
|
// EditorLineHeight is 14 * 1.2 = 16.8.
|
|
lineHeight := 16.8
|
|
if err := h.WithState(func(st *editor.State) {
|
|
st.Editor.Buffer = "Line 1\nLine 2\nLine 3"
|
|
|
|
// Scroll to start of Line 2 (skip Line 1)
|
|
st.ScrollOffset = ui.Dp(lineHeight)
|
|
|
|
// GlyphLayout:
|
|
// Line 1: y = 0
|
|
// Line 2: y = 16.8
|
|
// Line 3: y = 33.6
|
|
st.Editor.GlyphLayout = ui.GlyphLayout{
|
|
ByteOffsets: []int{0, 7, 14}, // Start of each line
|
|
X: []ui.Dp{10, 10, 10},
|
|
Y: []ui.Dp{ui.Dp(0), ui.Dp(lineHeight), ui.Dp(lineHeight * 2)},
|
|
Advance: []ui.Dp{10, 10, 10},
|
|
}
|
|
}); err != nil {
|
|
t.Fatalf("WithState: %v", err)
|
|
}
|
|
|
|
// Tap at y=10 (within the text area).
|
|
// Because of scroll=16.8, this should correspond to:
|
|
// visualLine = (y + scroll) / lineHeight = (10 + 16.8) / 16.8 = 26.8 / 16.8 = 1.59 -> 1
|
|
// Line 1 is index 0. Line 2 is index 1.
|
|
// So visualLine 1 should be Line 2.
|
|
|
|
// The problem is that SetCursorFromPoint expects y in DP, but receives it as raw pixels
|
|
// if we're not careful. Let's pass the y value properly.
|
|
// The test harness sends raw pixel coordinates to handler.
|
|
// EditorLayout converts tap point:
|
|
// localY := float64(pt.Y - editorRegion.Y + TheState.ScrollOffset)
|
|
// So the handler receives Y as pt.Y, where pt.Y is relative to the screen.
|
|
// The test harness doesn't seem to account for region offset.
|
|
|
|
// Let's debug by printing in the test (owner-side read).
|
|
v, err := h.Inspect(func(st *editor.State) any { return float64(st.ScrollOffset) })
|
|
if err != nil {
|
|
t.Fatalf("Inspect: %v", err)
|
|
}
|
|
t.Logf("ScrollOffset: %v", v)
|
|
|
|
h.SendInput([]ui.InputEvent{
|
|
{
|
|
Handler: func(data any) {
|
|
if pt, ok := data.(ui.Point); ok {
|
|
// Manually simulate the offset correction that EditorLayout does
|
|
// editorRegion.Y is the (margin-free) statusBarH(32) = 32.
|
|
// Let's set pt.Y to 10 + 32 = 42.
|
|
localY := float64(pt.Y) - 32.0 + float64(editor.TheState.ScrollOffset)
|
|
editor.SetCursorFromPoint(float64(pt.X), localY)
|
|
}
|
|
},
|
|
Data: ui.Point{X: 10, Y: 42},
|
|
},
|
|
})
|
|
|
|
// Assert cursor moved to start of Line 2 (offset 7)
|
|
time.Sleep(100 * time.Millisecond) // Give logic goroutine a moment to process input
|
|
pos, err := h.CursorPosition()
|
|
if err != nil {
|
|
t.Fatalf("CursorPosition: %v", err)
|
|
}
|
|
t.Logf("Cursor position: %d", pos)
|
|
if pos != 7 {
|
|
t.Errorf("expected cursor to move to 7 (Line 2), but got %d", pos)
|
|
}
|
|
}
|
|
|
|
// TestRealFile_CaretCursorOutsideWindow pins the off-screen caret contract:
|
|
// when the user scrolls the caret's line out of the viewport, the shaped
|
|
// window no longer contains the cursor's byte, and the frame must carry the
|
|
// TRUE window-relative cursor position (negative when the cursor is above
|
|
// the window, past len(Value) when below) so the renderer can skip the caret
|
|
// entirely.
|
|
//
|
|
// Pre-fix, EditorLayout clamped a negative window-relative cursor to 0 and
|
|
// drawWrappedText drew the caret unconditionally: scrolling the caret's line
|
|
// just past the top of the viewport made the caret "jump" onto the first
|
|
// visible line, then snap back to its real line when the user scrolled back
|
|
// (the reported cursor-jumps-to-adjacent-line bug). Native Android simply
|
|
// does not draw a caret that is off-screen.
|
|
func TestRealFile_CaretCursorOutsideWindow(t *testing.T) {
|
|
const (
|
|
lines = 60
|
|
lineLen = 21
|
|
scrollLn = 10
|
|
)
|
|
h, _ := realFileHarness(t, "caret_window.txt", uniformContent(lines))
|
|
defer h.Cleanup()
|
|
|
|
h.SendConfig(780, 400)
|
|
if _, err := h.WaitForFrame(5 * time.Second); err != nil {
|
|
t.Fatalf("wait for frame: %v", err)
|
|
}
|
|
|
|
// Scroll so the window starts at line 10 (byte 210); put the caret on
|
|
// line 5 (byte 105), ABOVE the window.
|
|
caretAbove := 5 * lineLen
|
|
if err := h.WithState(func(st *editor.State) {
|
|
st.ScrollOffset = ui.Dp(16.8*scrollLn + 5)
|
|
st.Editor.CursorPosition = caretAbove
|
|
}); err != nil {
|
|
t.Fatalf("WithState: %v", err)
|
|
}
|
|
prev := h.FrameCount()
|
|
h.SendConfig(780, 400) // state changes alone do not emit frames
|
|
if _, err := h.WaitForFrameCount(prev+1, 5*time.Second); err != nil {
|
|
t.Fatalf("wait for scrolled frame: %v", err)
|
|
}
|
|
tf, ok := lastEditorTextField(t, h)
|
|
if !ok {
|
|
t.Fatal("no editor_text TextField in latest frame")
|
|
}
|
|
if want := caretAbove - scrollLn*lineLen; tf.CursorPosition != want {
|
|
t.Fatalf("window cursor above = %d, want %d (negative: cursor above window, caret must be hidden)", tf.CursorPosition, want)
|
|
}
|
|
|
|
// Mirror case: caret on line 50 (byte 1050), BELOW the window (the
|
|
// ~18-line window at line 10 ends far short of it).
|
|
caretBelow := 50 * lineLen
|
|
if err := h.WithState(func(st *editor.State) {
|
|
st.Editor.CursorPosition = caretBelow
|
|
}); err != nil {
|
|
t.Fatalf("WithState: %v", err)
|
|
}
|
|
prev = h.FrameCount()
|
|
h.SendConfig(780, 400)
|
|
if _, err := h.WaitForFrameCount(prev+1, 5*time.Second); err != nil {
|
|
t.Fatalf("wait for scrolled frame (below): %v", err)
|
|
}
|
|
tf, ok = lastEditorTextField(t, h)
|
|
if !ok {
|
|
t.Fatal("no editor_text TextField in latest frame")
|
|
}
|
|
if want := caretBelow - scrollLn*lineLen; tf.CursorPosition != want {
|
|
t.Fatalf("window cursor below = %d, want %d (past len(Value): cursor below window, caret must be hidden)", tf.CursorPosition, want)
|
|
}
|
|
if tf.CursorPosition <= len(tf.Value) {
|
|
t.Fatalf("window cursor %d should be past window end %d", tf.CursorPosition, len(tf.Value))
|
|
}
|
|
|
|
// And the in-window case must still carry the plain relative offset.
|
|
if err := h.WithState(func(st *editor.State) {
|
|
st.Editor.CursorPosition = 12*lineLen + 9
|
|
}); err != nil {
|
|
t.Fatalf("WithState: %v", err)
|
|
}
|
|
prev = h.FrameCount()
|
|
h.SendConfig(780, 400)
|
|
if _, err := h.WaitForFrameCount(prev+1, 5*time.Second); err != nil {
|
|
t.Fatalf("wait for scrolled frame (inside): %v", err)
|
|
}
|
|
tf, ok = lastEditorTextField(t, h)
|
|
if !ok {
|
|
t.Fatal("no editor_text TextField in latest frame")
|
|
}
|
|
if want := 12*lineLen + 9 - scrollLn*lineLen; tf.CursorPosition != want {
|
|
t.Fatalf("window cursor inside = %d, want %d", tf.CursorPosition, want)
|
|
}
|
|
}
|