Merge pull request #3414 from navidemad/fix-a54-beforeinput-cancelable

webapi: make trusted beforeinput cancelable
This commit is contained in:
Karl Seguin authored and GitHub committed 2026-09-06 18:14:11 +08:00
commit cdf4f936dc
3 files changed
+155 -5

No files matched your search

+66
View File
@@ -0,0 +1,66 @@
<!DOCTYPE html>
<script src="../testing.js"></script>
<script id=constructorDefaults>
{
// Constructed events follow the EventInit dictionary defaults.
let evt = new InputEvent('beforeinput');
testing.expectEqual(true, evt instanceof InputEvent);
testing.expectEqual(true, evt instanceof UIEvent);
testing.expectEqual(false, evt.bubbles);
testing.expectEqual(false, evt.cancelable);
testing.expectEqual(false, evt.composed);
testing.expectEqual(null, evt.data);
testing.expectEqual('', evt.inputType);
testing.expectEqual(false, evt.isComposing);
}
</script>
<script id=constructorOptions>
{
let evt = new InputEvent('beforeinput', {
bubbles: true,
cancelable: true,
composed: true,
data: 'x',
inputType: 'insertText',
isComposing: true,
});
testing.expectEqual(true, evt.bubbles);
testing.expectEqual(true, evt.cancelable);
testing.expectEqual(true, evt.composed);
testing.expectEqual('x', evt.data);
testing.expectEqual('insertText', evt.inputType);
testing.expectEqual(true, evt.isComposing);
}
</script>
<script id=constructorCancelable>
{
// preventDefault() only takes effect on a cancelable event.
let cancelable = new InputEvent('beforeinput', {cancelable: true});
cancelable.preventDefault();
testing.expectEqual(true, cancelable.defaultPrevented);
let plain = new InputEvent('beforeinput');
plain.preventDefault();
testing.expectEqual(false, plain.defaultPrevented);
}
</script>
<script id=createEvent>
{
// document.createEvent leaves the flags unset until initEvent runs.
let evt = document.createEvent('InputEvent');
testing.expectEqual(true, evt instanceof InputEvent);
testing.expectEqual(false, evt.bubbles);
testing.expectEqual(false, evt.cancelable);
}
</script>
+17 -5
View File
@@ -80,11 +80,18 @@ fn initWithTrusted(arena: *lp.Arena, typ: String, _opts: ?Options, trusted: bool
Event.populatePrototypes(event, opts, trusted);
// https://developer.mozilla.org/en-US/docs/Web/API/Element/input_event
const rootevt = event._proto._proto;
rootevt._bubbles = true;
rootevt._cancelable = false;
rootevt._composed = true;
if (trusted) {
// Browser-generated input events bubble and are composed. `beforeinput`
// is cancelable — cancelling it vetoes the edit — while `input` is
// dispatched after the fact and is not:
// https://w3c.github.io/uievents/#event-type-beforeinput
// https://w3c.github.io/uievents/#event-type-input
// Synthetic ones follow the EventInit dictionary defaults.
const rootevt = event._proto._proto;
rootevt._bubbles = true;
rootevt._cancelable = typ.eqlSlice("beforeinput");
rootevt._composed = true;
}
// Hold a ref on the DataTransfer (when present) for this event's lifetime;
// released in deinit. Almost always null for input events, but keeps the
@@ -146,3 +153,8 @@ pub const JsApi = struct {
pub const inputType = bridge.accessor(InputEvent.getInputType, null, .{});
pub const isComposing = bridge.accessor(InputEvent.getIsComposing, null, .{});
};
const testing = @import("../../../testing.zig");
test "WebApi: InputEvent" {
try testing.htmlRunner("event/input.html", .{});
}
+72
View File
@@ -406,3 +406,75 @@ test "cdp.input: dispatchKeyEvent Tab runs sequential focus navigation" {
});
try testing.expect((try ls.local.compileAndRun("document.activeElement.id === 'b1'", null)).isTrue());
}
test "cdp.input: dispatchKeyEvent beforeinput can veto the edit" {
var ctx = try testing.context();
defer ctx.deinit();
const bc = try ctx.loadBrowserContext(.{});
const page = try bc.session.createPage();
const frame = page.frame().?;
try frame.navigate("http://localhost:9582/src/browser/tests/mcp_actions.html", .{
.reason = .address_bar,
.kind = .{ .push = null },
});
try testing.waitForPage(bc);
var ls: lp.js.Local.Scope = undefined;
frame.js.localScope(&ls);
defer ls.deinit();
// A listener that rejects every keystroke, the way masked-input libraries
// do. Records what it saw so we can assert the event was cancelable.
_ = try ls.local.compileAndRun(
\\const inp = document.getElementById('inp');
\\inp.value = 'ab';
\\inp.focus();
\\window.seen = [];
\\inp.addEventListener('beforeinput', (e) => {
\\ window.seen.push(['beforeinput', e.cancelable].join(':'));
\\ e.preventDefault();
\\ window.seen.push(['defaultPrevented', e.defaultPrevented].join(':'));
\\});
\\inp.addEventListener('input', () => window.seen.push('input'));
, null);
try ctx.processMessage(.{
.id = 1,
.method = "Input.dispatchKeyEvent",
.params = .{ .type = "keyDown", .key = "c", .text = "c" },
});
// The veto held: no character inserted and no `input` event followed.
try testing.expect((try ls.local.compileAndRun(
\\inp.value === 'ab' &&
\\window.seen.join(',') === 'beforeinput:true,defaultPrevented:true'
, null)).isTrue());
// Backspace goes through the same pre-edit gate.
try ctx.processMessage(.{
.id = 2,
.method = "Input.dispatchKeyEvent",
.params = .{ .type = "keyDown", .key = "Backspace", .code = "Backspace" },
});
try testing.expect((try ls.local.compileAndRun("inp.value === 'ab'", null)).isTrue());
// Without the veto, the edit goes through and `input` is dispatched — and
// `input` itself is not cancelable.
_ = try ls.local.compileAndRun(
\\const clone = inp.cloneNode(true);
\\inp.replaceWith(clone);
\\clone.focus();
\\window.seen = [];
\\clone.addEventListener('input', (e) => window.seen.push(['input', e.cancelable].join(':')));
, null);
try ctx.processMessage(.{
.id = 3,
.method = "Input.dispatchKeyEvent",
.params = .{ .type = "keyDown", .key = "c", .text = "c" },
});
try testing.expect((try ls.local.compileAndRun(
\\clone.value === 'abc' && window.seen.join(',') === 'input:false'
, null)).isTrue());
}