input: fix cursor jumping to position 0 after typing first character

When composing a new note, the cursor would jump in front of the first
letter after typing it. This occurred because multiple SwiftUI view
updates (text change, placeholder removal, height change) could cause
the cursor position to be incorrectly restored.

The fix explicitly tracks the cursor position after each text change
by calling updateCursorPosition, ensuring the correct position is
always used regardless of view update timing.

Refactored textViewDidChange to use early return pattern for clarity.

Added UI test to guard against cursor position regressions in the
post composer.

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Changelog-Fixed: Fixed cursor jumping behind first letter when typing a new note
Closes: https://github.com/damus-io/damus/pull/3473
Closes: https://github.com/damus-io/damus/issues/3461
Co-Authored-By: Claude Opus 4.5
Tested-by: William Casarin <jb55@jb55.com>
Signed-off-by: alltheseas
Signed-off-by: William Casarin <jb55@jb55.com>
This commit is contained in:
alltheseas
2025-12-26 15:04:01 -06:00
committed by Daniel D’Aquino
parent f506f9cfe8
commit 4941b502d5
5 changed files with 63 additions and 5 deletions

View File

@@ -188,6 +188,43 @@ class damusUITests: XCTestCase {
guard app.buttons[AID.sign_in_confirm_button.rawValue].tapIfExists(timeout: 5) else { throw DamusUITestError.timeout_waiting_for_element }
}
/// Tests that typing in the post composer works correctly, specifically that
/// the cursor position is maintained after typing each character.
/// This guards against regressions like https://github.com/damus-io/damus/issues/3461
/// where the cursor would jump to position 0 after typing the first character.
func testPostComposerCursorPosition() throws {
try self.loginIfNotAlready()
// Wait for main interface to load, then tap the post button (FAB)
guard app.buttons[AID.post_button.rawValue].waitForExistence(timeout: 10) else {
throw DamusUITestError.timeout_waiting_for_element
}
app.buttons[AID.post_button.rawValue].tap()
// Wait for the post composer text view to appear
guard app.textViews[AID.post_composer_text_view.rawValue].waitForExistence(timeout: 5) else {
throw DamusUITestError.timeout_waiting_for_element
}
let textView = app.textViews[AID.post_composer_text_view.rawValue]
textView.tap()
// Type a test string character by character
// If the cursor jumps to position 0 after the first character,
// the resulting text would be scrambled (e.g., "olleH" instead of "Hello")
let testString = "Hello"
textView.typeText(testString)
// Verify the text was typed correctly (not scrambled)
let actualText = textView.value as? String ?? ""
XCTAssertEqual(actualText, testString,
"Text should be '\(testString)' but was '\(actualText)'. " +
"This may indicate a cursor position bug.")
// Cancel the post to clean up
app.buttons[AID.post_composer_cancel_button.rawValue].tap()
}
enum DamusUITestError: Error {
case timeout_waiting_for_element
}