mirror of
https://github.com/lightpanda-io/browser.git
synced 2026-10-08 20:32:00 -04:00
Merge pull request #3542 from lightpanda-io/cdp-device-pixel-ratio-and-char-input
cdp: window.devicePixelRatio from viewport scale and Input char text insertion
This commit is contained in:
13 files changed
+411
-163
No files matched your search
@@ -20,7 +20,7 @@ const Viewport = @This();
|
||||
|
||||
width: u32,
|
||||
height: u32,
|
||||
scale: f32 = 1.0, // for screenshot raster
|
||||
scale: f32 = 1.0, // CSS px to device px
|
||||
// window.screen dimensions; null means the same as the viewport.
|
||||
screen_width: ?u32 = null,
|
||||
screen_height: ?u32 = null,
|
||||
|
||||
+4
-35
@@ -78,11 +78,10 @@ pub fn hover(node: *DOMNode, frame: *Frame) !void {
|
||||
}
|
||||
|
||||
pub fn press(node: ?*DOMNode, key: []const u8, frame: *Frame) !void {
|
||||
const target_el: ?*Element = if (node) |n|
|
||||
const target: *Element = if (node) |n|
|
||||
(n.is(Element) orelse return error.InvalidNodeType)
|
||||
else
|
||||
null;
|
||||
const target = if (target_el) |el| el.asEventTarget() else frame.document.asNode().asEventTarget();
|
||||
Frame.user_input.focusedElement(frame) orelse return error.ActionFailed;
|
||||
const canonical = canonicalKey(key);
|
||||
|
||||
const keydown_event: *KeyboardEvent = try .initTrusted(comptime .wrap("keydown"), .{
|
||||
@@ -92,19 +91,11 @@ pub fn press(node: ?*DOMNode, key: []const u8, frame: *Frame) !void {
|
||||
.key = canonical,
|
||||
}, frame);
|
||||
|
||||
const prevented = frame._event_manager.dispatchCancelable(target, keydown_event.asEvent()) catch |err| {
|
||||
_ = Frame.user_input.pressKey(frame, target, keydown_event, Frame.user_input.textForKey(keydown_event)) catch |err| {
|
||||
lp.log.err(.app, "press keydown failed", .{ .err = err });
|
||||
return error.ActionFailed;
|
||||
};
|
||||
|
||||
if (std.mem.eql(u8, canonical, "Enter") and !prevented) {
|
||||
if (target_el) |el| implicitFormSubmit(el, frame) catch |err| {
|
||||
// Don't skip keyup on a submit-listener throw — UIs that gate
|
||||
// state on keyup (e.g. clearing a "submitting" flag) would hang.
|
||||
lp.log.warn(.app, "implicit form submit failed", .{ .err = err });
|
||||
};
|
||||
}
|
||||
|
||||
const keyup_event: *KeyboardEvent = try .initTrusted(comptime .wrap("keyup"), .{
|
||||
.bubbles = true,
|
||||
.cancelable = true,
|
||||
@@ -112,7 +103,7 @@ pub fn press(node: ?*DOMNode, key: []const u8, frame: *Frame) !void {
|
||||
.key = canonical,
|
||||
}, frame);
|
||||
|
||||
frame._event_manager.dispatch(target, keyup_event.asEvent()) catch |err| {
|
||||
frame._event_manager.dispatch(target.asEventTarget(), keyup_event.asEvent()) catch |err| {
|
||||
lp.log.err(.app, "press keyup failed", .{ .err = err });
|
||||
return error.ActionFailed;
|
||||
};
|
||||
@@ -145,28 +136,6 @@ fn canonicalKey(key: []const u8) []const u8 {
|
||||
return key;
|
||||
}
|
||||
|
||||
fn implicitFormSubmit(el: *Element, frame: *Frame) !void {
|
||||
const Input = Element.Html.Input;
|
||||
const Button = Element.Html.Button;
|
||||
|
||||
if (el.is(Input)) |input| {
|
||||
const form = input.getForm(frame) orelse return;
|
||||
const submitter: ?*Element = switch (input._input_type) {
|
||||
.submit, .image => el,
|
||||
// Non-text controls (checkbox, radio, file, ...) don't trigger
|
||||
// implicit submission; only the text-like family does.
|
||||
.text, .password, .email, .url, .tel, .search, .number, .date, .time, .@"datetime-local", .month, .week => null,
|
||||
else => return,
|
||||
};
|
||||
return form.requestSubmit(submitter, frame);
|
||||
}
|
||||
if (el.is(Button)) |button| {
|
||||
if (!std.ascii.eqlIgnoreCase(button.getType(), "submit")) return;
|
||||
const form = button.getForm(frame) orelse return;
|
||||
return form.requestSubmit(el, frame);
|
||||
}
|
||||
}
|
||||
|
||||
pub fn selectOption(node: *DOMNode, value: []const u8, frame: *Frame) !void {
|
||||
const el = node.is(Element) orelse return error.InvalidNodeType;
|
||||
const select = el.is(Element.Html.Select) orelse return error.InvalidNodeType;
|
||||
|
||||
@@ -798,26 +798,109 @@ fn followLink(frame: *Frame, target: *Node, element: *Element, href: []const u8,
|
||||
}, .{ .anchor = target_frame });
|
||||
}
|
||||
|
||||
pub fn triggerKeyboard(frame: *Frame, keyboard_event: *KeyboardEvent) !void {
|
||||
const event = keyboard_event.asEvent();
|
||||
// Dispatch to the effective active element. When nothing is explicitly
|
||||
// focused this resolves to <body> (matching `document.activeElement`), so
|
||||
// the keydown still fires and its default action — e.g. sequential focus
|
||||
// navigation on Tab — can run.
|
||||
const element = frame.window._document.getActiveElement() orelse {
|
||||
event.deinit(frame.page);
|
||||
pub fn triggerKeyDown(frame: *Frame, keydown: *KeyboardEvent, text: ?[]const u8) !bool {
|
||||
const element = focusedElement(frame) orelse {
|
||||
keydown.asEvent().deinit(frame.page);
|
||||
return false;
|
||||
};
|
||||
return pressKey(frame, element, keydown, text);
|
||||
}
|
||||
|
||||
pub fn triggerKeyUp(frame: *Frame, keyup: *KeyboardEvent) !void {
|
||||
const element = focusedElement(frame) orelse {
|
||||
keyup.asEvent().deinit(frame.page);
|
||||
return;
|
||||
};
|
||||
try frame._event_manager.dispatch(element.asEventTarget(), keyup.asEvent());
|
||||
}
|
||||
|
||||
/// Where a key event goes: `document.activeElement`, so with nothing focused
|
||||
/// a key still fires on <body> and Tab's focus navigation can run.
|
||||
pub fn focusedElement(frame: *Frame) ?*Element {
|
||||
return frame.window._document.getActiveElement();
|
||||
}
|
||||
|
||||
/// Dispatches a trusted keydown on `target` then, unless cancelled, types
|
||||
/// `text` (Chrome's WebKeyboardEvent.text; null when the client sends the
|
||||
/// char as its own event, as chromedp does). Returns whether the keydown was
|
||||
/// cancelled.
|
||||
pub fn pressKey(frame: *Frame, target: *Element, keydown: *KeyboardEvent, text: ?[]const u8) !bool {
|
||||
if (comptime lp.IS_DEBUG) {
|
||||
log.debug(.frame, "frame keydown", .{
|
||||
.url = frame.url,
|
||||
.node = element,
|
||||
.key = keyboard_event._key,
|
||||
.node = target,
|
||||
.key = keydown._key,
|
||||
.type = frame._type,
|
||||
});
|
||||
}
|
||||
try frame._event_manager.dispatch(element.asEventTarget(), event);
|
||||
const event = keydown.asEvent();
|
||||
const t = text orelse return frame._event_manager.dispatchCancelable(target.asEventTarget(), event);
|
||||
|
||||
// dispatch drops the event; keypressFor still needs it.
|
||||
event.acquireRef();
|
||||
defer event.releaseRef(frame.page);
|
||||
if (try frame._event_manager.dispatchCancelable(target.asEventTarget(), event)) {
|
||||
return true;
|
||||
}
|
||||
// logged like a default action's failure, not the key event's
|
||||
typeChar(frame, target, try keypressFor(frame, keydown), t) catch |err| {
|
||||
log.warn(.frame, "frame.keypress", .{ .err = err });
|
||||
};
|
||||
return false;
|
||||
}
|
||||
|
||||
/// The text a key press produces, following Chrome's WebKeyboardEvent.text: the
|
||||
/// key itself when printable, "\r" for Enter, nothing when ctrl/meta turn the
|
||||
/// press into a shortcut.
|
||||
pub fn textForKey(keyboard_event: *const KeyboardEvent) ?[]const u8 {
|
||||
if (keyboard_event.getCtrlKey() or keyboard_event.getMetaKey()) {
|
||||
return null;
|
||||
}
|
||||
const key = keyboard_event.getKey();
|
||||
if (key == .Enter) {
|
||||
return "\r";
|
||||
}
|
||||
return if (key.isPrintable()) key.asString() else null;
|
||||
}
|
||||
|
||||
/// The char half of a key press (Chrome's WebInputEvent::kChar): fires
|
||||
/// `keypress` on `target` and, unless a listener cancels it, performs the
|
||||
/// text edit it stands for.
|
||||
pub fn typeChar(frame: *Frame, target: *Element, keypress: *KeyboardEvent, text: []const u8) !void {
|
||||
if (try frame._event_manager.dispatchCancelable(target.asEventTarget(), keypress.asEvent())) {
|
||||
return;
|
||||
}
|
||||
const is_enter = text.len == 1 and (text[0] == '\r' or text[0] == '\n');
|
||||
if (is_enter and isButton(target)) {
|
||||
return dispatchKeyboardClick(frame, target);
|
||||
}
|
||||
|
||||
if (target.is(Element.Html.Input)) |input| {
|
||||
if (is_enter) {
|
||||
return frame.submitForm(input.asElement(), input.getForm(frame), .{});
|
||||
}
|
||||
return insertInto(frame, input, text);
|
||||
}
|
||||
|
||||
if (target.is(Element.Html.TextArea)) |textarea| {
|
||||
if (is_enter) {
|
||||
if (try allowEdit(frame, textarea.asElement(), null, "\n", "insertLineBreak")) {
|
||||
try textarea.innerInsert("\n", frame);
|
||||
}
|
||||
return;
|
||||
}
|
||||
return insertInto(frame, textarea, text);
|
||||
}
|
||||
}
|
||||
|
||||
fn keypressFor(frame: *Frame, keydown: *const KeyboardEvent) !*KeyboardEvent {
|
||||
return KeyboardEvent.initTrusted(comptime .wrap("keypress"), .{
|
||||
.key = keydown.getKey().asString(),
|
||||
.ctrlKey = keydown.getCtrlKey(),
|
||||
.shiftKey = keydown.getShiftKey(),
|
||||
.altKey = keydown.getAltKey(),
|
||||
.metaKey = keydown.getMetaKey(),
|
||||
}, frame);
|
||||
}
|
||||
|
||||
pub fn handleKeydown(frame: *Frame, target: *Node, event: *Event) !void {
|
||||
@@ -833,55 +916,29 @@ pub fn handleKeydown(frame: *Frame, target: *Node, event: *Event) !void {
|
||||
return moveFocus(frame, keyboard_event.getShiftKey() == false);
|
||||
}
|
||||
|
||||
if (event.getIsTrusted()) {
|
||||
if ((key.isPrintable() or key == .Enter) and keyboard_event.getCtrlKey() == false and keyboard_event.getMetaKey() == false) {
|
||||
// Fire a keypress for a printable (or Enter) keydown when ctrl/meta
|
||||
// aren't pressed
|
||||
if (try dispatchKeypress(frame, target, keyboard_event)) {
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
if (key == .Enter) {
|
||||
if (target.is(Element)) |element| {
|
||||
if (enterActivates(element)) {
|
||||
// Enter generates a button-like "click" for some elements
|
||||
return dispatchKeyboardClick(frame, element);
|
||||
}
|
||||
if (key == .Enter and event.getIsTrusted()) {
|
||||
if (target.is(Element)) |element| {
|
||||
if (enterFollowsLink(element)) {
|
||||
return dispatchKeyboardClick(frame, element);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (target.is(Element.Html.Input)) |input| {
|
||||
if (key == .Enter) {
|
||||
return frame.submitForm(input.asElement(), input.getForm(frame), .{});
|
||||
}
|
||||
|
||||
// Don't handle text input for radio/checkbox
|
||||
const input_type = input._input_type;
|
||||
if (input_type == .radio or input_type == .checkbox) {
|
||||
if (!input.acceptsTextEntry()) {
|
||||
return;
|
||||
}
|
||||
|
||||
return editKey(frame, keyboard_event, input, key);
|
||||
}
|
||||
|
||||
if (target.is(Element.Html.TextArea)) |textarea| {
|
||||
if (key == .Enter) {
|
||||
if (try allowEdit(frame, event, textarea.asElement(), null, "\n", "insertLineBreak")) {
|
||||
try textarea.innerInsert("\n", frame);
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
return editKey(frame, keyboard_event, textarea, key);
|
||||
}
|
||||
}
|
||||
|
||||
// edit keys are handled by Input and TextArea the same
|
||||
// edit keys other than text insertion (typeChar's) are handled by Input and
|
||||
// TextArea the same
|
||||
fn editKey(frame: *Frame, keyboard_event: *KeyboardEvent, ctl: anytype, key: KeyboardEvent.Key) !void {
|
||||
const event = keyboard_event.asEvent();
|
||||
|
||||
if (caretMove(key, ctl)) |move| {
|
||||
// Word/paragraph motions (ctrl/alt/meta variants) aren't modeled.
|
||||
if (keyboard_event.getCtrlKey() or keyboard_event.getAltKey() or keyboard_event.getMetaKey()) {
|
||||
@@ -892,16 +949,18 @@ fn editKey(frame: *Frame, keyboard_event: *KeyboardEvent, ctl: anytype, key: Key
|
||||
|
||||
if (key == .Backspace or key == .Delete) {
|
||||
const forward = key == .Delete;
|
||||
if (try allowEdit(frame, event, ctl.asElement(), null, null, deleteInputType(forward))) {
|
||||
if (!keyboard_event.asEvent().getIsTrusted() or try allowEdit(frame, ctl.asElement(), null, null, deleteInputType(forward))) {
|
||||
try ctl.innerDelete(forward, frame);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn insertInto(frame: *Frame, ctl: anytype, text: []const u8) !void {
|
||||
if (!ctl.acceptsTextEntry()) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (key.isPrintable()) {
|
||||
if (try allowEdit(frame, event, ctl.asElement(), key.asString(), key.asString(), "insertText")) {
|
||||
try ctl.innerInsert(key.asString(), frame);
|
||||
}
|
||||
if (try allowEdit(frame, ctl.asElement(), text, text, "insertText")) {
|
||||
try ctl.innerInsert(text, frame);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -924,15 +983,9 @@ fn deleteInputType(forward: bool) []const u8 {
|
||||
return if (forward) "deleteContentForward" else "deleteContentBackward";
|
||||
}
|
||||
|
||||
// pre-edit events for a key's default action, can cancel the edit (i.e. by
|
||||
// returning false)
|
||||
fn allowEdit(frame: *Frame, keydown: *Event, target: *Element, before_data: ?[]const u8, text_data: ?[]const u8, input_type: []const u8) !bool {
|
||||
if (keydown.getIsTrusted() == false) {
|
||||
// only trusted events fire these events, so for a untrusted event, the
|
||||
// edit isn't cancelled.
|
||||
return true;
|
||||
}
|
||||
|
||||
// pre-edit events for a trusted key's default action, can cancel the edit
|
||||
// (i.e. by returning false)
|
||||
fn allowEdit(frame: *Frame, target: *Element, before_data: ?[]const u8, text_data: ?[]const u8, input_type: []const u8) !bool {
|
||||
{
|
||||
const before = (try InputEvent.initTrusted(comptime .wrap("beforeinput"), .{
|
||||
.bubbles = true,
|
||||
@@ -982,23 +1035,6 @@ pub fn handleKeyup(frame: *Frame, target: *Node, event: *Event) !void {
|
||||
}
|
||||
}
|
||||
|
||||
// Dispatch keypress mirroring `keydown`'s key and modifiers; returns true when
|
||||
// a listener canceled it.
|
||||
fn dispatchKeypress(frame: *Frame, target: *Node, keydown: *KeyboardEvent) !bool {
|
||||
const event = (try KeyboardEvent.initTrusted(comptime .wrap("keypress"), .{
|
||||
.bubbles = true,
|
||||
.cancelable = true,
|
||||
.composed = true,
|
||||
.key = keydown.getKey().asString(),
|
||||
.ctrlKey = keydown.getCtrlKey(),
|
||||
.shiftKey = keydown.getShiftKey(),
|
||||
.altKey = keydown.getAltKey(),
|
||||
.metaKey = keydown.getMetaKey(),
|
||||
}, frame)).asEvent();
|
||||
|
||||
return frame._event_manager.dispatchCancelable(target.asEventTarget(), event);
|
||||
}
|
||||
|
||||
// keydown+enter or keyup+space trigger this syntthetic pointer event (under
|
||||
// specific conditions, see handleKeydown and handleKeyup).
|
||||
fn dispatchKeyboardClick(frame: *Frame, element: *Element) !void {
|
||||
@@ -1011,15 +1047,19 @@ fn dispatchKeyboardClick(frame: *Frame, element: *Element) !void {
|
||||
try frame._event_manager.dispatch(element.asEventTarget(), event.asEvent());
|
||||
}
|
||||
|
||||
// elements where enter on a keydown should dispatch a click-click event
|
||||
fn enterActivates(element: *Element) bool {
|
||||
// Which elements act on which key, and at which step: a link follows Enter on
|
||||
// the keydown, a button clicks on Enter's keypress (typeChar) and on Space's
|
||||
// keyup, as do checkboxes and radios for Space.
|
||||
fn enterFollowsLink(element: *Element) bool {
|
||||
const html_element = element.is(Element.Html) orelse return false;
|
||||
return html_element._type == .anchor and element.getAttributeInterned("href") != null;
|
||||
}
|
||||
|
||||
fn isButton(element: *Element) bool {
|
||||
const html_element = element.is(Element.Html) orelse return false;
|
||||
if (html_element._type == .button) {
|
||||
return true;
|
||||
}
|
||||
if (html_element._type == .anchor) {
|
||||
return element.getAttributeInterned("href") != null;
|
||||
}
|
||||
if (element.is(Element.Html.Input)) |input| {
|
||||
return switch (input._input_type) {
|
||||
.button, .submit, .reset, .image => true,
|
||||
@@ -1029,19 +1069,12 @@ fn enterActivates(element: *Element) bool {
|
||||
return false;
|
||||
}
|
||||
|
||||
// elements where space on a keyup should dispatch a click-click event
|
||||
fn spaceActivates(element: *Element) bool {
|
||||
const html_element = element.is(Element.Html) orelse return false;
|
||||
if (html_element._type == .button) {
|
||||
if (isButton(element)) {
|
||||
return true;
|
||||
}
|
||||
if (element.is(Element.Html.Input)) |input| {
|
||||
return switch (input._input_type) {
|
||||
.button, .submit, .reset, .image, .checkbox, .radio => true,
|
||||
else => false,
|
||||
};
|
||||
}
|
||||
return false;
|
||||
const input = element.is(Element.Html.Input) orelse return false;
|
||||
return input._input_type == .checkbox or input._input_type == .radio;
|
||||
}
|
||||
|
||||
// Sequential focus navigation: move `document.activeElement` to the next (Tab)
|
||||
@@ -1125,21 +1158,16 @@ fn focusOrderBefore(a: *Element, a_tab_index: i32, b: *Element, b_tab_index: i32
|
||||
return a_tab_index < b_tab_index;
|
||||
}
|
||||
|
||||
// insertText is a shortcut to insert text into the active element.
|
||||
/// Text input without a key press (IME, paste): beforeinput but no keypress.
|
||||
pub fn insertText(frame: *Frame, v: []const u8) !void {
|
||||
const html_element = frame.document._active_element orelse return;
|
||||
|
||||
if (html_element.is(Element.Html.Input)) |input| {
|
||||
const input_type = input._input_type;
|
||||
if (input_type == .radio or input_type == .checkbox) {
|
||||
return;
|
||||
}
|
||||
|
||||
return input.innerInsert(v, frame);
|
||||
return insertInto(frame, input, v);
|
||||
}
|
||||
|
||||
if (html_element.is(Element.Html.TextArea)) |textarea| {
|
||||
return textarea.innerInsert(v, frame);
|
||||
return insertInto(frame, textarea, v);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -409,15 +409,7 @@ fn performKeySource(source: js.Object, frame: *Frame) !void {
|
||||
// longer does.
|
||||
setModifier(&frame.page.input_modifiers, key, is_down);
|
||||
|
||||
// Key actions have no explicit target; they go to the focused element,
|
||||
// or the document if nothing is focused. Resolved per action since a
|
||||
// key's default action can move focus.
|
||||
const target = if (frame.document._active_element) |el|
|
||||
el.asEventTarget()
|
||||
else
|
||||
frame.document.asNode().asEventTarget();
|
||||
|
||||
dispatchKey(target, if (is_down) comptime .wrap("keydown") else comptime .wrap("keyup"), key, frame);
|
||||
dispatchKey(is_down, key, frame);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -502,7 +494,10 @@ fn setModifier(modifiers: *Modifiers, key: []const u8, pressed: bool) void {
|
||||
}
|
||||
}
|
||||
|
||||
fn dispatchKey(target: *EventTarget, typ: lp.String, key: []const u8, frame: *Frame) void {
|
||||
// Key actions have no explicit target; they go to the focused element,
|
||||
// resolved per action since a key's default action can move focus.
|
||||
fn dispatchKey(is_down: bool, key: []const u8, frame: *Frame) void {
|
||||
const typ: lp.String = if (is_down) comptime .wrap("keydown") else comptime .wrap("keyup");
|
||||
const modifiers = frame.page.input_modifiers;
|
||||
const event = KeyboardEvent.initTrusted(typ, .{
|
||||
.bubbles = true,
|
||||
@@ -517,7 +512,12 @@ fn dispatchKey(target: *EventTarget, typ: lp.String, key: []const u8, frame: *Fr
|
||||
log.warn(.app, "webdriver key event", .{ .err = err });
|
||||
return;
|
||||
};
|
||||
dispatch(target, event.asEvent(), frame, typ.str());
|
||||
(if (is_down)
|
||||
Frame.user_input.triggerKeyDown(frame, event, Frame.user_input.textForKey(event))
|
||||
else
|
||||
Frame.user_input.triggerKeyUp(frame, event)) catch |err| {
|
||||
log.warn(.app, "webdriver dispatch", .{ .err = err, .type = typ.str() });
|
||||
};
|
||||
}
|
||||
|
||||
fn readI32(obj: js.Object, key: []const u8, default: i32) i32 {
|
||||
|
||||
@@ -222,6 +222,10 @@ fn setInnerHeight(self: *Window, value: js.Value) void {
|
||||
self.replaceGlobalProperty(value, "innerHeight");
|
||||
}
|
||||
|
||||
fn setDevicePixelRatio(self: *Window, value: js.Value) void {
|
||||
self.replaceGlobalProperty(value, "devicePixelRatio");
|
||||
}
|
||||
|
||||
fn setScrollX(self: *Window, value: js.Value) void {
|
||||
self.replaceGlobalProperty(value, "scrollX");
|
||||
}
|
||||
@@ -926,6 +930,10 @@ pub fn getInnerHeight(_: *const Window, frame: *Frame) u32 {
|
||||
return frame.page.getViewport().height;
|
||||
}
|
||||
|
||||
fn getDevicePixelRatio(_: *const Window, frame: *Frame) f32 {
|
||||
return frame.page.getViewport().scale;
|
||||
}
|
||||
|
||||
pub fn scrollTo(self: *Window, opts: Element.ScrollToOpts, y: ?i32, frame: *Frame) !void {
|
||||
const o = opts.offsets(y);
|
||||
const new_x: u32 = if (o.left) |left| @intCast(@max(0, left)) else self._scroll_pos.x;
|
||||
@@ -1230,7 +1238,7 @@ pub const JsApi = struct {
|
||||
// the attribute rather than throwing.
|
||||
pub const innerWidth = bridge.accessor(Window.getInnerWidth, Window.setInnerWidth, .{});
|
||||
pub const innerHeight = bridge.accessor(Window.getInnerHeight, Window.setInnerHeight, .{});
|
||||
pub const devicePixelRatio = bridge.property(1, .{ .template = false, .readonly = false });
|
||||
pub const devicePixelRatio = bridge.accessor(Window.getDevicePixelRatio, Window.setDevicePixelRatio, .{});
|
||||
|
||||
pub const opener = bridge.accessor(Window.getOpener, Window.setOpener, .{});
|
||||
pub const closed = bridge.accessor(Window.getClosed, null, .{});
|
||||
|
||||
@@ -627,6 +627,7 @@ const entry = text_entry.TextEntry(Input);
|
||||
|
||||
pub const select = entry.select;
|
||||
pub const innerInsert = entry.innerInsert;
|
||||
pub const acceptsTextEntry = entry.acceptsTextEntry;
|
||||
pub const innerDelete = entry.innerDelete;
|
||||
pub const moveCaret = entry.moveCaret;
|
||||
pub const CaretMove = entry.CaretMove;
|
||||
|
||||
@@ -140,6 +140,7 @@ const entry = text_entry.TextEntry(TextArea);
|
||||
|
||||
pub const select = entry.select;
|
||||
pub const innerInsert = entry.innerInsert;
|
||||
pub const acceptsTextEntry = entry.acceptsTextEntry;
|
||||
pub const innerDelete = entry.innerDelete;
|
||||
pub const moveCaret = entry.moveCaret;
|
||||
pub const CaretMove = entry.CaretMove;
|
||||
|
||||
@@ -29,6 +29,18 @@ const InputEvent = @import("../event/InputEvent.zig");
|
||||
|
||||
pub fn TextEntry(comptime T: type) type {
|
||||
return struct {
|
||||
/// Whether typing edits the control's value. Checkbox and radio share
|
||||
/// Input's value machinery but no text goes into them.
|
||||
pub fn acceptsTextEntry(self: *const T) bool {
|
||||
if (!@hasField(T, "_input_type")) {
|
||||
return true;
|
||||
}
|
||||
return switch (self._input_type) {
|
||||
.checkbox, .radio => false,
|
||||
else => true,
|
||||
};
|
||||
}
|
||||
|
||||
pub fn select(self: *T, frame: *Frame) !void {
|
||||
const len: u32 = @intCast(self.getValue().len);
|
||||
try setSelectionRange(self, 0, len, null, frame);
|
||||
|
||||
@@ -289,7 +289,7 @@ const KeyboardEventOptions = struct {
|
||||
shiftKey: bool = false,
|
||||
};
|
||||
|
||||
const Options = Event.inheritOptions(
|
||||
pub const Options = Event.inheritOptions(
|
||||
KeyboardEvent,
|
||||
KeyboardEventOptions,
|
||||
);
|
||||
|
||||
@@ -642,7 +642,11 @@ fn dispatchKey(frame: *Frame, comptime typ: []const u8, info: *const KeyInfo, mo
|
||||
.metaKey = modifiers.meta,
|
||||
.shiftKey = modifiers.shift,
|
||||
}, frame);
|
||||
try user_input.triggerKeyboard(frame, event);
|
||||
if (comptime std.mem.eql(u8, typ, "keydown")) {
|
||||
_ = try user_input.triggerKeyDown(frame, event, user_input.textForKey(event));
|
||||
} else {
|
||||
try user_input.triggerKeyUp(frame, event);
|
||||
}
|
||||
}
|
||||
|
||||
fn setModifier(modifiers: *Modifiers, which: ?Modifier, down: bool) void {
|
||||
|
||||
@@ -459,6 +459,10 @@ pub const BrowserContext = struct {
|
||||
session_id: ?[]const u8,
|
||||
attached_sessions: std.ArrayList(AttachedSession) = .empty,
|
||||
|
||||
// A cancelled text-less keyDown drops the char message that follows it
|
||||
// (chromedp's keyDown/char/keyUp split), as Chrome does.
|
||||
suppress_next_char: bool = false,
|
||||
|
||||
security_origin: []const u8,
|
||||
page_life_cycle_events: bool,
|
||||
secure_context_type: []const u8,
|
||||
|
||||
@@ -310,10 +310,10 @@ test "cdp.Emulation: setDeviceMetricsOverride screenWidth/screenHeight reach win
|
||||
try ctx.processMessage(.{
|
||||
.id = 1,
|
||||
.method = "Emulation.setDeviceMetricsOverride",
|
||||
.params = .{ .width = 1280, .height = 720, .deviceScaleFactor = 1, .mobile = false, .screenWidth = 2560, .screenHeight = 1440 },
|
||||
.params = .{ .width = 1280, .height = 720, .deviceScaleFactor = 2, .mobile = false, .screenWidth = 2560, .screenHeight = 1440 },
|
||||
});
|
||||
try ctx.expectSentResult(null, .{ .id = 1 });
|
||||
var v = try ls.local.exec("screen.width === 2560 && screen.height === 1440 && innerWidth === 1280 && innerHeight === 720", null);
|
||||
var v = try ls.local.exec("screen.width === 2560 && screen.height === 1440 && innerWidth === 1280 && innerHeight === 720 && devicePixelRatio === 2", null);
|
||||
try testing.expect(v.toBool());
|
||||
|
||||
// 0 keeps the current value, as for width/height.
|
||||
@@ -322,11 +322,11 @@ test "cdp.Emulation: setDeviceMetricsOverride screenWidth/screenHeight reach win
|
||||
.method = "Emulation.setDeviceMetricsOverride",
|
||||
.params = .{ .width = 1024, .height = 0, .deviceScaleFactor = 0, .mobile = false, .screenWidth = 0, .screenHeight = 0 },
|
||||
});
|
||||
v = try ls.local.exec("screen.width === 2560 && screen.height === 1440 && innerWidth === 1024 && innerHeight === 720", null);
|
||||
v = try ls.local.exec("screen.width === 2560 && screen.height === 1440 && innerWidth === 1024 && innerHeight === 720 && devicePixelRatio === 2", null);
|
||||
try testing.expect(v.toBool());
|
||||
|
||||
try ctx.processMessage(.{ .id = 3, .method = "Emulation.clearDeviceMetricsOverride" });
|
||||
v = try ls.local.exec("screen.width === innerWidth && screen.height === innerHeight", null);
|
||||
v = try ls.local.exec("screen.width === innerWidth && screen.height === innerHeight && devicePixelRatio === 1", null);
|
||||
try testing.expect(v.toBool());
|
||||
}
|
||||
|
||||
|
||||
@@ -43,6 +43,7 @@ fn dispatchKeyEvent(cmd: *CDP.Command) !void {
|
||||
key: []const u8 = "",
|
||||
code: ?[]const u8 = null,
|
||||
modifiers: u4 = 0,
|
||||
text: []const u8 = "",
|
||||
// Many optional parameters are not implemented yet, see documentation url.
|
||||
|
||||
const Type = enum {
|
||||
@@ -61,22 +62,41 @@ fn dispatchKeyEvent(cmd: *CDP.Command) !void {
|
||||
const bc = cmd.browser_context orelse return;
|
||||
const frame = bc.mainFrame() orelse return;
|
||||
|
||||
// Chrome types text only for an event carrying it: a keyDown with `text`
|
||||
// (Puppeteer, Playwright) or a `char` (chromedp, after a text-less keyDown).
|
||||
const text: ?[]const u8 = if (params.text.len == 0) null else params.text;
|
||||
const KeyboardEvent = @import("../../../browser/webapi/event/KeyboardEvent.zig");
|
||||
const keyboard_event = try KeyboardEvent.initTrusted(switch (params.type) {
|
||||
.keyDown => comptime .wrap("keydown"),
|
||||
.keyUp => comptime .wrap("keyup"),
|
||||
.char => comptime .wrap("keypress"),
|
||||
.rawKeyDown => unreachable,
|
||||
}, .{
|
||||
.key = params.key,
|
||||
const opts: KeyboardEvent.Options = .{
|
||||
.key = if (params.key.len > 0) params.key else params.text,
|
||||
.code = params.code,
|
||||
.altKey = params.modifiers & 1 == 1,
|
||||
.ctrlKey = params.modifiers & 2 == 2,
|
||||
.metaKey = params.modifiers & 4 == 4,
|
||||
.shiftKey = params.modifiers & 8 == 8,
|
||||
}, frame);
|
||||
try Frame.user_input.triggerKeyboard(frame, keyboard_event);
|
||||
// result already sent
|
||||
};
|
||||
|
||||
switch (params.type) {
|
||||
.rawKeyDown => unreachable,
|
||||
.keyDown => {
|
||||
const event = try KeyboardEvent.initTrusted(comptime .wrap("keydown"), opts, frame);
|
||||
const prevented = try Frame.user_input.triggerKeyDown(frame, event, text);
|
||||
bc.suppress_next_char = prevented and text == null;
|
||||
},
|
||||
.keyUp => {
|
||||
const event = try KeyboardEvent.initTrusted(comptime .wrap("keyup"), opts, frame);
|
||||
try Frame.user_input.triggerKeyUp(frame, event);
|
||||
},
|
||||
.char => {
|
||||
const t = text orelse return;
|
||||
if (bc.suppress_next_char) {
|
||||
bc.suppress_next_char = false;
|
||||
return;
|
||||
}
|
||||
const target = Frame.user_input.focusedElement(frame) orelse return;
|
||||
const event = try KeyboardEvent.initTrusted(comptime .wrap("keypress"), opts, frame);
|
||||
try Frame.user_input.typeChar(frame, target, event, t);
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
// https://chromedevtools.github.io/devtools-protocol/tot/Input/#method-dispatchMouseEvent
|
||||
@@ -1270,3 +1290,204 @@ test "cdp.input: dispatchKeyEvent caret movement keys move the text entry cursor
|
||||
try testing.expectEqualSlices(u8, step.expect, got);
|
||||
}
|
||||
}
|
||||
|
||||
// chromedp's SendKeys shape: a text-less keyDown, the char with the text, keyUp.
|
||||
test "cdp.input: dispatchKeyEvent text-less keyDown then char types once" {
|
||||
var ctx = try testing.context();
|
||||
defer ctx.deinit();
|
||||
|
||||
const bc = try ctx.loadBrowserContext(.{ .url = "mcp_actions.html" });
|
||||
const frame = bc.mainFrame().?;
|
||||
|
||||
var ls: lp.js.Local.Scope = undefined;
|
||||
frame.js.localScope(&ls);
|
||||
defer ls.deinit();
|
||||
|
||||
_ = try ls.local.compileAndRun(
|
||||
\\const inp = document.getElementById('inp');
|
||||
\\inp.value = '';
|
||||
\\inp.focus();
|
||||
\\window.events = [];
|
||||
\\for (const t of ['keydown', 'keypress', 'input', 'keyup']) {
|
||||
\\ inp.addEventListener(t, (e) => window.events.push(t + ':' + (e.key ?? inp.value)));
|
||||
\\}
|
||||
, null);
|
||||
|
||||
try ctx.processMessage(.{ .id = 1, .method = "Input.dispatchKeyEvent", .params = .{ .type = "keyDown", .key = "h", .code = "KeyH" } });
|
||||
try ctx.expectSentResult(null, .{ .id = 1 });
|
||||
try testing.expect((try ls.local.compileAndRun("inp.value === '' && window.events.join(',') === 'keydown:h'", null)).isTrue());
|
||||
|
||||
try ctx.processMessage(.{ .id = 2, .method = "Input.dispatchKeyEvent", .params = .{ .type = "char", .key = "h", .text = "h" } });
|
||||
try ctx.expectSentResult(null, .{ .id = 2 });
|
||||
try ctx.processMessage(.{ .id = 3, .method = "Input.dispatchKeyEvent", .params = .{ .type = "keyUp", .key = "h", .code = "KeyH" } });
|
||||
try ctx.expectSentResult(null, .{ .id = 3 });
|
||||
try testing.expect((try ls.local.compileAndRun(
|
||||
\\inp.value === 'h' && window.events.join(',') === 'keydown:h,keypress:h,input:h,keyup:h'
|
||||
, null)).isTrue());
|
||||
|
||||
// A char without text has nothing to type.
|
||||
try ctx.processMessage(.{ .id = 4, .method = "Input.dispatchKeyEvent", .params = .{ .type = "char", .key = "Enter" } });
|
||||
try ctx.expectSentResult(null, .{ .id = 4 });
|
||||
try testing.expect((try ls.local.compileAndRun("inp.value === 'h' && window.events.length === 4", null)).isTrue());
|
||||
|
||||
// A keyDown with text but no key (Puppeteer's shape for some keys) types it.
|
||||
try ctx.processMessage(.{ .id = 5, .method = "Input.dispatchKeyEvent", .params = .{ .type = "keyDown", .text = "z" } });
|
||||
try ctx.expectSentResult(null, .{ .id = 5 });
|
||||
try testing.expect((try ls.local.compileAndRun("inp.value === 'hz'", null)).isTrue());
|
||||
|
||||
// Enter's char is "\r": a line break in a <textarea>, never a literal "\r".
|
||||
_ = try ls.local.compileAndRun(
|
||||
\\const ta = document.createElement('textarea');
|
||||
\\document.body.appendChild(ta);
|
||||
\\ta.value = 'one';
|
||||
\\ta.focus();
|
||||
, null);
|
||||
try ctx.processMessage(.{ .id = 6, .method = "Input.dispatchKeyEvent", .params = .{ .type = "keyDown", .key = "Enter", .code = "Enter" } });
|
||||
try ctx.expectSentResult(null, .{ .id = 6 });
|
||||
try ctx.processMessage(.{ .id = 7, .method = "Input.dispatchKeyEvent", .params = .{ .type = "char", .key = "Enter", .text = "\r" } });
|
||||
try ctx.expectSentResult(null, .{ .id = 7 });
|
||||
try ctx.processMessage(.{ .id = 8, .method = "Input.dispatchKeyEvent", .params = .{ .type = "keyUp", .key = "Enter", .code = "Enter" } });
|
||||
try ctx.expectSentResult(null, .{ .id = 8 });
|
||||
try testing.expect((try ls.local.compileAndRun("ta.value === 'one\\n'", null)).isTrue());
|
||||
}
|
||||
|
||||
test "cdp.input: dispatchKeyEvent char honors keypress and beforeinput vetoes" {
|
||||
var ctx = try testing.context();
|
||||
defer ctx.deinit();
|
||||
|
||||
const bc = try ctx.loadBrowserContext(.{ .url = "mcp_actions.html" });
|
||||
const frame = bc.mainFrame().?;
|
||||
|
||||
var ls: lp.js.Local.Scope = undefined;
|
||||
frame.js.localScope(&ls);
|
||||
defer ls.deinit();
|
||||
|
||||
// keypress fires before beforeinput, so it sees the digits the latter vetoes.
|
||||
_ = try ls.local.compileAndRun(
|
||||
\\const inp = document.getElementById('inp');
|
||||
\\inp.value = '';
|
||||
\\inp.focus();
|
||||
\\window.keypresses = [];
|
||||
\\inp.addEventListener('keypress', (e) => {
|
||||
\\ window.keypresses.push(e.key);
|
||||
\\ if (e.key === 'x') e.preventDefault();
|
||||
\\});
|
||||
\\inp.addEventListener('beforeinput', (e) => {
|
||||
\\ if (/[0-9]/.test(e.data)) e.preventDefault();
|
||||
\\});
|
||||
, null);
|
||||
|
||||
var id: u32 = 1;
|
||||
for ("a1x2b") |c| {
|
||||
const key: []const u8 = &.{c};
|
||||
try ctx.processMessage(.{ .id = id, .method = "Input.dispatchKeyEvent", .params = .{ .type = "keyDown", .key = key } });
|
||||
try ctx.expectSentResult(null, .{ .id = id });
|
||||
id += 1;
|
||||
try ctx.processMessage(.{ .id = id, .method = "Input.dispatchKeyEvent", .params = .{ .type = "char", .key = key, .text = key } });
|
||||
try ctx.expectSentResult(null, .{ .id = id });
|
||||
id += 1;
|
||||
}
|
||||
try testing.expect((try ls.local.compileAndRun(
|
||||
\\inp.value === 'ab' && window.keypresses.join('') === 'a1x2b'
|
||||
, null)).isTrue());
|
||||
}
|
||||
|
||||
test "cdp.input: dispatchKeyEvent cancelled keyDown suppresses its char" {
|
||||
var ctx = try testing.context();
|
||||
defer ctx.deinit();
|
||||
|
||||
const bc = try ctx.loadBrowserContext(.{ .url = "mcp_actions.html" });
|
||||
const frame = bc.mainFrame().?;
|
||||
|
||||
var ls: lp.js.Local.Scope = undefined;
|
||||
frame.js.localScope(&ls);
|
||||
defer ls.deinit();
|
||||
|
||||
_ = try ls.local.compileAndRun(
|
||||
\\const inp = document.getElementById('inp');
|
||||
\\inp.value = '';
|
||||
\\inp.focus();
|
||||
\\window.keypresses = [];
|
||||
\\inp.addEventListener('keydown', (e) => { if (e.key === 'x') e.preventDefault(); });
|
||||
\\inp.addEventListener('keypress', (e) => window.keypresses.push(e.key));
|
||||
, null);
|
||||
|
||||
// The char in the same message (keyDown with text) and in the next one
|
||||
// (chromedp) are both dropped.
|
||||
try ctx.processMessage(.{ .id = 1, .method = "Input.dispatchKeyEvent", .params = .{ .type = "keyDown", .key = "x", .text = "x" } });
|
||||
try ctx.expectSentResult(null, .{ .id = 1 });
|
||||
try ctx.processMessage(.{ .id = 2, .method = "Input.dispatchKeyEvent", .params = .{ .type = "keyDown", .key = "x" } });
|
||||
try ctx.expectSentResult(null, .{ .id = 2 });
|
||||
try ctx.processMessage(.{ .id = 3, .method = "Input.dispatchKeyEvent", .params = .{ .type = "char", .key = "x", .text = "x" } });
|
||||
try ctx.expectSentResult(null, .{ .id = 3 });
|
||||
try testing.expect((try ls.local.compileAndRun("inp.value === '' && window.keypresses.length === 0", null)).isTrue());
|
||||
|
||||
try ctx.processMessage(.{ .id = 4, .method = "Input.dispatchKeyEvent", .params = .{ .type = "keyDown", .key = "y" } });
|
||||
try ctx.expectSentResult(null, .{ .id = 4 });
|
||||
try ctx.processMessage(.{ .id = 5, .method = "Input.dispatchKeyEvent", .params = .{ .type = "char", .key = "y", .text = "y" } });
|
||||
try ctx.expectSentResult(null, .{ .id = 5 });
|
||||
try testing.expect((try ls.local.compileAndRun("inp.value === 'y' && window.keypresses.join('') === 'y'", null)).isTrue());
|
||||
}
|
||||
|
||||
// Enter, as chromedp sends it: keyDown, a "\r" char, keyUp. Buttons click
|
||||
// after their keypress; the form submits at most once.
|
||||
test "cdp.input: dispatchKeyEvent Enter clicks buttons and submits once" {
|
||||
var ctx = try testing.context();
|
||||
defer ctx.deinit();
|
||||
|
||||
const bc = try ctx.loadBrowserContext(.{ .url = "mcp_actions.html" });
|
||||
const frame = bc.mainFrame().?;
|
||||
|
||||
var ls: lp.js.Local.Scope = undefined;
|
||||
frame.js.localScope(&ls);
|
||||
defer ls.deinit();
|
||||
|
||||
_ = try ls.local.compileAndRun(
|
||||
\\document.body.insertAdjacentHTML('beforeend', '<form id=f>' +
|
||||
\\ '<input id=text><input id=check type=checkbox><input id=submit type=submit>' +
|
||||
\\ '<button id=button>go</button><input id=ibutton type=button><input id=reset type=reset></form>');
|
||||
\\const form = document.getElementById('f');
|
||||
\\form.addEventListener('submit', (e) => {
|
||||
\\ e.preventDefault();
|
||||
\\ window.events.push('submit');
|
||||
\\});
|
||||
\\// Only the focused control's keypress and click are recorded.
|
||||
\\for (const t of ['keypress', 'click']) {
|
||||
\\ form.addEventListener(t, (e) => {
|
||||
\\ if (e.target === document.activeElement) window.events.push(t);
|
||||
\\ }, true);
|
||||
\\}
|
||||
\\window.arm = (id) => {
|
||||
\\ window.events = [];
|
||||
\\ document.getElementById(id).focus();
|
||||
\\};
|
||||
, null);
|
||||
|
||||
const cases = [_]struct { id: []const u8, expect: []const u8 }{
|
||||
.{ .id = "text", .expect = "keypress submit" },
|
||||
.{ .id = "check", .expect = "keypress submit" },
|
||||
.{ .id = "submit", .expect = "keypress click submit" },
|
||||
.{ .id = "button", .expect = "keypress click submit" },
|
||||
.{ .id = "ibutton", .expect = "keypress click" },
|
||||
.{ .id = "reset", .expect = "keypress click" },
|
||||
};
|
||||
|
||||
var id: u32 = 1;
|
||||
for (cases) |c| {
|
||||
var buf: [32]u8 = undefined;
|
||||
_ = try ls.local.compileAndRun(try std.fmt.bufPrint(&buf, "arm('{s}')", .{c.id}), null);
|
||||
|
||||
try ctx.processMessage(.{ .id = id, .method = "Input.dispatchKeyEvent", .params = .{ .type = "keyDown", .key = "Enter", .code = "Enter" } });
|
||||
try ctx.expectSentResult(null, .{ .id = id });
|
||||
id += 1;
|
||||
try ctx.processMessage(.{ .id = id, .method = "Input.dispatchKeyEvent", .params = .{ .type = "char", .key = "Enter", .text = "\r" } });
|
||||
try ctx.expectSentResult(null, .{ .id = id });
|
||||
id += 1;
|
||||
try ctx.processMessage(.{ .id = id, .method = "Input.dispatchKeyEvent", .params = .{ .type = "keyUp", .key = "Enter", .code = "Enter" } });
|
||||
try ctx.expectSentResult(null, .{ .id = id });
|
||||
id += 1;
|
||||
|
||||
const got = try (try ls.local.compileAndRun("window.events.join(' ')", null)).toStringSlice();
|
||||
try testing.expectEqualSlices(u8, c.expect, got);
|
||||
}
|
||||
}
|
||||
Reference in new issue
Block a user