Pad/internal/test/e2e/scroll_cursor_test.go
Greg Pomerantz 58725a5e6c Make state single-owner and stabilize race detector
Frame now carries view state (scale/focus/query) to the main goroutine,
which reads only the frame-receiver-stored snapshot. Renderer owns Gio
widget editors (registered by ID) and the draw scale. Autosave timer
sends a token to the logic goroutine instead of touching state;
Shutdown waits for the owner to exit before FlushAll.

Tests access state only through owner-side Inspect/WithState helpers
(harness + in-package). Fixed double-Harness.Run race in two e2e tests,
made TestWorkerPool_PriorityPreemption deterministic, and raised the
lazy-loading test timeout that was too short under -race.

go test -race ./... is now green.
2026-08-16 01:32:27 -04:00

109 lines
3.5 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 margin(10) + statusBarH(52) = 62.
// Let's set pt.Y to 10 + 62 = 72.
localY := float64(pt.Y) - 62.0 + float64(editor.TheState.ScrollOffset)
editor.SetCursorFromPoint(float64(pt.X), localY)
}
},
Data: ui.Point{X: 10, Y: 72},
},
})
// 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)
}
}