From a844e38826cd33568952c4138fc50e00fe71bdd0 Mon Sep 17 00:00:00 2001 From: Karl Seguin Date: Mon, 28 Sep 2026 09:49:13 +0800 Subject: [PATCH] preserve correctsendKeys() behavior for webdriver --- src/browser/tests/element/html/input.html | 8 +++++++ src/browser/webapi/element/html/Input.zig | 3 ++- src/browser/webapi/element/html/TextArea.zig | 1 + src/browser/webapi/element/text_entry.zig | 22 +++++++++++++++----- src/server/bidi/http_command.zig | 12 ++--------- src/server/cdp/domains/input.zig | 5 +++++ 6 files changed, 35 insertions(+), 16 deletions(-) diff --git a/src/browser/tests/element/html/input.html b/src/browser/tests/element/html/input.html index a56d553ca..b3b70dc95 100644 --- a/src/browser/tests/element/html/input.html +++ b/src/browser/tests/element/html/input.html @@ -609,5 +609,13 @@ email.select(); testing.expectEqual(1, selects); testing.expectError('InvalidStateError', () => email.setSelectionRange(0, 1)); + + const date = document.createElement('input'); + date.type = 'date'; + date.value = '2026-09-28'; + let date_selects = 0; + date.addEventListener('select', () => date_selects++); + date.select(); + testing.expectEqual(0, date_selects); } diff --git a/src/browser/webapi/element/html/Input.zig b/src/browser/webapi/element/html/Input.zig index cb955f367..418848f5b 100644 --- a/src/browser/webapi/element/html/Input.zig +++ b/src/browser/webapi/element/html/Input.zig @@ -628,9 +628,10 @@ const entry = text_entry.TextEntry(Input); pub const select = entry.select; pub const innerInsert = entry.innerInsert; pub const acceptsTextEntry = entry.acceptsTextEntry; -pub const tracksSelection = entry.tracksSelection; +const tracksSelection = entry.tracksSelection; pub const innerDelete = entry.innerDelete; pub const moveCaret = entry.moveCaret; +pub const caretToEnd = entry.caretToEnd; pub const CaretMove = entry.CaretMove; pub const getSelectionDirection = entry.getSelectionDirection; pub const setSelectionStart = entry.setSelectionStart; diff --git a/src/browser/webapi/element/html/TextArea.zig b/src/browser/webapi/element/html/TextArea.zig index a4aa87934..42431e682 100644 --- a/src/browser/webapi/element/html/TextArea.zig +++ b/src/browser/webapi/element/html/TextArea.zig @@ -143,6 +143,7 @@ pub const innerInsert = entry.innerInsert; pub const acceptsTextEntry = entry.acceptsTextEntry; pub const innerDelete = entry.innerDelete; pub const moveCaret = entry.moveCaret; +pub const caretToEnd = entry.caretToEnd; pub const CaretMove = entry.CaretMove; pub const getSelectionDirection = entry.getSelectionDirection; pub const setSelectionStart = entry.setSelectionStart; diff --git a/src/browser/webapi/element/text_entry.zig b/src/browser/webapi/element/text_entry.zig index f1458a391..72f466880 100644 --- a/src/browser/webapi/element/text_entry.zig +++ b/src/browser/webapi/element/text_entry.zig @@ -77,8 +77,10 @@ pub fn TextEntry(comptime T: type) type { // fully selected, replace the content. const new_value = try arena.dupe(u8, str); try self.setUserValue(new_value, frame); - self._selection_start = @intCast(new_value.len); - self._selection_end = @intCast(new_value.len); + // the sanitized value can be shorter than what was inserted + const new_len: u32 = @intCast(self.getValue().len); + self._selection_start = new_len; + self._selection_end = new_len; self._selection_direction = .none; try dispatchSelectionChangeEvent(self, frame); }, @@ -95,9 +97,9 @@ pub fn TextEntry(comptime T: type) type { ); try self.setUserValue(new_value, frame); - const new_pos = range[0] + str.len; - self._selection_start = @intCast(new_pos); - self._selection_end = @intCast(new_pos); + const new_pos: u32 = @intCast(@min(range[0] + str.len, self.getValue().len)); + self._selection_start = new_pos; + self._selection_end = new_pos; self._selection_direction = .none; try dispatchSelectionChangeEvent(self, frame); }, @@ -171,6 +173,16 @@ pub fn TextEntry(comptime T: type) type { try dispatchInputEvent(self, null, if (forward) "deleteContentForward" else "deleteContentBackward", frame); } + // Collapses the selection to the end of the value. Unlike + // setSelectionRange(), applies to email and number too. + pub fn caretToEnd(self: *T, frame: *Frame) !void { + if (tracksSelection(self) == false) { + return; + } + const len: u32 = @intCast(self.getValue().len); + try applySelectionRange(self, len, len, .none, frame); + } + pub const CaretMove = enum { backward, forward, line_start, line_end }; // Default action of the caret movement keys (ArrowLeft, ArrowRight, diff --git a/src/server/bidi/http_command.zig b/src/server/bidi/http_command.zig index 9a00fe4ae..a59e80921 100644 --- a/src/server/bidi/http_command.zig +++ b/src/server/bidi/http_command.zig @@ -689,9 +689,9 @@ fn elementSendKeys(cmd: *BiDi.Command, p: SendKeys) !void { return cmd.sendError("element not interactable", "element can't be focused"); } if (element.is(Html.Input)) |input_element| { - try caretToEnd(input_element, frame); + try input_element.caretToEnd(frame); } else if (element.is(Html.TextArea)) |textarea| { - try caretToEnd(textarea, frame); + try textarea.caretToEnd(frame); } } @@ -702,14 +702,6 @@ fn elementSendKeys(cmd: *BiDi.Command, p: SendKeys) !void { return browsing_context.answerAfterNavigation(cmd, ctx, frame); } -fn caretToEnd(ctl: anytype, frame: *Frame) !void { - if (ctl.selectionAvailable() == false) { - return; - } - const len: u32 = @intCast(ctl.getValue().len); - try ctl.setSelectionRange(len, len, null, frame); -} - fn requireInteractable(cmd: *BiDi.Command, element: *Node.Element, frame: *Frame) !bool { element.scrollIntoView(null, frame); if (element.checkVisibility(.{}, frame)) { diff --git a/src/server/cdp/domains/input.zig b/src/server/cdp/domains/input.zig index e2d3e3ccb..c590d7649 100644 --- a/src/server/cdp/domains/input.zig +++ b/src/server/cdp/domains/input.zig @@ -229,6 +229,11 @@ test "cdp.input: insertText replaces select()ed value of email and number inputs _ = try ls.local.compileAndRun("inp.type = 'number'; inp.value = '12'; inp.select();", null); try ctx.processMessage(.{ .id = 2, .method = "Input.insertText", .params = .{ .text = "345" } }); try testing.expect((try ls.local.compileAndRun("inp.value === '345'", null)).isTrue()); + + // The caret lands at the end of the sanitized value, not of the inserted text. + _ = try ls.local.compileAndRun("inp.type = 'text'; inp.value = 'ab'; inp.select();", null); + try ctx.processMessage(.{ .id = 3, .method = "Input.insertText", .params = .{ .text = "c\nd" } }); + try testing.expect((try ls.local.compileAndRun("inp.value === 'cd' && inp.selectionStart === 2", null)).isTrue()); } test "cdp.input: dispatchMouseEvent mouseMoved fires hover events" {