Skip to content

script: Fire input events when executing commands#43087

Merged
TimvdLippe merged 1 commit intoservo:mainfrom
TimvdLippe:fire-events-for-commands
Mar 26, 2026
Merged

script: Fire input events when executing commands#43087
TimvdLippe merged 1 commit intoservo:mainfrom
TimvdLippe:fire-events-for-commands

Conversation

@TimvdLippe
Copy link
Copy Markdown
Contributor

@TimvdLippe TimvdLippe commented Mar 8, 2026

For any enabled command, we should fire corresponding
beforeinput and input events.

Also update a WPT test to make it clearer what the
expected behavior is. None of the test expect a
beforeinput event. Before, it would fail with
"expected undefined, but got", which is not descriptive
as a failure to what's happening.

We are failing these tests, since this behavior is
currently unspecced and also requires changes in
the input element to work with text edits.

Part of #25005

@TimvdLippe TimvdLippe added the T-linux-wpt Do a try run of the WPT label Mar 8, 2026
@github-actions github-actions Bot removed the T-linux-wpt Do a try run of the WPT label Mar 8, 2026
@github-actions
Copy link
Copy Markdown

github-actions Bot commented Mar 8, 2026

🔨 Triggering try run (#22817924525) for Linux (WPT)

@github-actions
Copy link
Copy Markdown

github-actions Bot commented Mar 8, 2026

Test results for linux-wpt from try job (#22817924525):

Flaky unexpected result (22)
  • OK /_mozilla/mozilla/getBoundingClientRect.html (#39668)
    • FAIL [expected PASS] subtest: getBoundingClientRect 1

      assert_equals: expected 62 but got 60.35
      

  • CRASH [expected OK] /_webgl/conformance/glsl/variables/glsl-built-ins.html
  • CRASH [expected OK] /_webgl/conformance/textures/image_bitmap_from_image_data/tex-2d-rgba-rgba-unsigned_byte.html
  • FAIL [expected PASS] /css/css-backgrounds/background-size-041.html
  • OK /css/css-fonts/generic-family-keywords-002.html (#40929)
    • FAIL [expected PASS] subtest: font-family: -webkit-serif treated as <font-family>, not <generic-name>

      assert_equals: expected 30 but got 50
      

    • FAIL [expected PASS] subtest: font-family: -webkit-sans-serif treated as <font-family>, not <generic-name>

      assert_equals: expected 30 but got 50
      

    • FAIL [expected PASS] subtest: font-family: -webkit-cursive treated as <font-family>, not <generic-name>

      assert_equals: expected 30 but got 50
      

    • FAIL [expected PASS] subtest: font-family: -webkit-fantasy treated as <font-family>, not <generic-name>

      assert_equals: expected 30 but got 50
      

    • FAIL [expected PASS] subtest: font-family: -webkit-monospace treated as <font-family>, not <generic-name>

      assert_equals: expected 30 but got 50
      

    • FAIL [expected PASS] subtest: font-family: -webkit-system-ui treated as <font-family>, not <generic-name>

      assert_equals: expected 30 but got 50
      

    • FAIL [expected PASS] subtest: font-family: -webkit-math treated as <font-family>, not <generic-name>

      assert_equals: expected 30 but got 50
      

    • PASS [expected FAIL] subtest: font-family: -webkit-generic(fangsong) treated as <font-family>, not <generic-name>
    • PASS [expected FAIL] subtest: font-family: -webkit-generic(kai) treated as <font-family>, not <generic-name>
    • PASS [expected FAIL] subtest: font-family: -webkit-generic(khmer-mul) treated as <font-family>, not <generic-name>
    • And 12 more unexpected results...
  • ERROR [expected OK] /fetch/fetch-later/quota/same-origin-iframe/accumulated-oversized-payload.https.window.html (#41705)
  • OK /fetch/metadata/generated/css-font-face.https.sub.tentative.html (#32732)
    • PASS [expected FAIL] subtest: sec-fetch-mode
  • CRASH [expected OK] /html/browsers/sandboxing/sandbox-initial-empty-document-toward-same-origin.html (#35948)
  • CRASH [expected OK] /html/canvas/element/color-type/2d.color.type.u8srgb.to.f16p3.to.u8srgb.html
  • CRASH [expected OK] /html/canvas/element/compositing/2d.composite.transparent.destination-out.html
  • OK /html/semantics/document-metadata/the-meta-element/pragma-directives/attr-meta-http-equiv-refresh/allow-scripts-flag-changing-1.html (#39694)
    • PASS [expected FAIL] subtest: Meta refresh is blocked by the allow-scripts sandbox flag at its creation time, not when refresh comes due
  • TIMEOUT [expected OK] /html/semantics/embedded-content/the-iframe-element/iframe_sandbox_navigate_other_frame_popup.sub.html (#39702)
    • TIMEOUT [expected FAIL] subtest: Sandboxed iframe can not navigate other frame's popup

      Test timed out
      

  • OK /html/semantics/forms/form-submission-0/jsurl-form-submit.tentative.html (#36489)
    • PASS [expected FAIL] subtest: Verifies that form submissions scheduled inside javascript: urls take precedence over the javascript: url's return value.
  • PASS [expected FAIL] /png/apng/acTL-plays-one.html (#41218)
  • OK [expected CRASH] /resource-timing/render-blocking-status-link.html (#41664)
  • OK /resource-timing/test_resource_timing.html (#25720)
    • PASS [expected FAIL] subtest: PerformanceEntry has correct name, initiatorType, startTime, and duration (img)
    • FAIL [expected PASS] subtest: PerformanceEntry has correct name, initiatorType, startTime, and duration (link)

      assert_equals: expected 16.61 but got 16.6
      

  • OK /resource-timing/test_resource_timing.https.html (#25216)
    • PASS [expected FAIL] subtest: PerformanceEntry has correct name, initiatorType, startTime, and duration (xmlhttprequest)
  • TIMEOUT /trusted-types/trusted-types-navigation.html?31-35 (#38034)
    • TIMEOUT [expected PASS] subtest: Navigate a frame via form-submission with javascript:-urls in report-only mode.

      Test timed out
      

    • NOTRUN [expected TIMEOUT] subtest: Navigate a frame via form-submission with javascript:-urls w/ default policy in report-only mode.
  • OK /visual-viewport/resize-event-order.html (#41981)
    • PASS [expected FAIL] subtest: Popup: DOMWindow resize fired before VisualViewport.
  • ERROR [expected OK] /webxr/render_state_update.https.html (#27535)
  • CRASH [expected ERROR] /workers/Worker-constructor-proto.any.serviceworker.html
  • ERROR [expected OK] /workers/baseurl/alpha/sharedworker-in-worker.html (#21315)
Stable unexpected results that are known to be intermittent (11)
  • FAIL [expected PASS] /_mozilla/mozilla/sslfail.html (#10760)
  • TIMEOUT [expected OK] /_mozilla/mozilla/window_resize_event.html (#36741)
    • TIMEOUT [expected PASS] subtest: Popup onresize event fires after resizeTo

      Test timed out
      

  • OK /_webgl/conformance/textures/misc/texture-upload-size.html (#21770)
    • PASS [expected FAIL] subtest: WebGL test #85
    • PASS [expected FAIL] subtest: WebGL test #87
    • PASS [expected FAIL] subtest: WebGL test #89
    • PASS [expected FAIL] subtest: WebGL test #91
  • OK /css/css-cascade/layer-cssom-order-reverse.html (#36094)
    • PASS [expected FAIL] subtest: Delete layer invalidates @font-face
  • OK /css/css-fonts/generic-family-keywords-003.html (#38994)
    • PASS [expected FAIL] subtest: @font-face matching for quoted and unquoted fantasy (drawing text in a canvas)
    • PASS [expected FAIL] subtest: @font-face matching for quoted and unquoted monospace (drawing text in a canvas)
    • PASS [expected FAIL] subtest: @font-face matching for quoted and unquoted system-ui (drawing text in a canvas)
    • PASS [expected FAIL] subtest: @font-face matching for quoted and unquoted math (drawing text in a canvas)
    • PASS [expected FAIL] subtest: @font-face matching for quoted and unquoted generic(fangsong) (drawing text in a canvas)
    • PASS [expected FAIL] subtest: @font-face matching for quoted and unquoted generic(khmer-mul) (drawing text in a canvas)
  • TIMEOUT /fetch/metadata/generated/css-images.https.sub.tentative.html (#42229)
    • FAIL [expected PASS] subtest: content sec-fetch-site - Cross-Site -> Same-Site

      assert_unreached: Reached unreachable code
      

  • OK [expected TIMEOUT] /html/anonymous-iframe/indexeddb.tentative.https.window.html (#39254)
    • FAIL [expected TIMEOUT] subtest: indexeddb

      assert_equals: expected (undefined) undefined but got (string) "7fbee15e-a0e8-4e46-bcb3-a24b5a3cfbf5"
      

  • OK /html/browsers/browsing-the-web/navigating-across-documents/navigation-unload-same-origin-fragment.html (#20768)
    • FAIL [expected PASS] subtest: Tests that a fragment navigation in the unload handler will not block the initial navigation

      assert_equals: expected "" but got "#fragment"
      

  • OK /html/browsers/history/the-history-interface/traverse_the_history_2.html (#21383)
    • FAIL [expected PASS] subtest: Multiple history traversals, last would be aborted

      assert_array_equals: Pages opened during history navigation expected property 1 to be 3 but got 1 (expected array [6, 3] got [6, 1])
      

  • TIMEOUT /html/interaction/focus/the-autofocus-attribute/supported-elements.html (#24145)
    • FAIL [expected TIMEOUT] subtest: Host element with delegatesFocus should support autofocus

      assert_equals: expected Element node <div autofocus=""></div> but got Element node <body><div autofocus=""></div></body>
      

    • TIMEOUT [expected NOTRUN] subtest: Host element with delegatesFocus including no focusable descendants should be skipped

      Test timed out
      

  • OK /navigation-timing/test-navigation-type-reload.html (#33334)
    • FAIL [expected PASS] subtest: Reload domComplete > Original domComplete

      assert_true: Reload domComplete > Original domComplete expected true got false
      

    • FAIL [expected PASS] subtest: Reload domContentLoadedEventEnd > Original domContentLoadedEventEnd

      assert_true: Reload domContentLoadedEventEnd > Original domContentLoadedEventEnd expected true got false
      

    • FAIL [expected PASS] subtest: Reload domContentLoadedEventStart > Original domContentLoadedEventStart

      assert_true: Reload domContentLoadedEventStart > Original domContentLoadedEventStart expected true got false
      

    • FAIL [expected PASS] subtest: Reload domInteractive > Original domInteractive

      assert_true: Reload domInteractive > Original domInteractive expected true got false
      

    • FAIL [expected PASS] subtest: Reload fetchStart > Original fetchStart

      assert_true: Reload fetchStart > Original fetchStart expected true got false
      

    • FAIL [expected PASS] subtest: Reload loadEventEnd > Original loadEventEnd

      assert_true: Reload loadEventEnd > Original loadEventEnd expected true got false
      

    • FAIL [expected PASS] subtest: Reload loadEventStart > Original loadEventStart

      assert_true: Reload loadEventStart > Original loadEventStart expected true got false
      

Stable unexpected results (3)
  • OK /editing/other/exec-command-with-text-editor.tentative.html?type=password
    • FAIL [expected PASS] subtest: In <input type="password"> in contenteditable, execCommand("delete", false, null), ab[]c): beforeinput.inputType should be undefined

      assert_equals: expected (undefined) undefined but got (string) ""
      

    • FAIL [expected PASS] subtest: In <input type="password"> in contenteditable, execCommand("delete", false, null), ab[]c): beforeinput.target should be undefined

      assert_equals: expected (undefined) undefined but got (object) Element node <div id="container" contenteditable="true">Here <b>is</b>...
      

    • PASS [expected FAIL] subtest: In <input type="password"> in contenteditable, execCommand("delete", false, null), ab[]c): input.inputType should be deleteContentBackward
    • FAIL [expected PASS] subtest: In <input type="password"> in contenteditable, execCommand("delete", false, null), a[b]c): beforeinput.inputType should be undefined

      assert_equals: expected (undefined) undefined but got (string) ""
      

    • FAIL [expected PASS] subtest: In <input type="password"> in contenteditable, execCommand("delete", false, null), a[b]c): beforeinput.target should be undefined

      assert_equals: expected (undefined) undefined but got (object) Element node <div id="container" contenteditable="true">Here <b>is</b>...
      

    • PASS [expected FAIL] subtest: In <input type="password"> in contenteditable, execCommand("delete", false, null), a[b]c): input.inputType should be deleteContentBackward
  • OK /editing/other/exec-command-with-text-editor.tentative.html?type=text
    • FAIL [expected PASS] subtest: In <input type="text"> in contenteditable, execCommand("delete", false, null), ab[]c): beforeinput.inputType should be undefined

      assert_equals: expected (undefined) undefined but got (string) ""
      

    • FAIL [expected PASS] subtest: In <input type="text"> in contenteditable, execCommand("delete", false, null), ab[]c): beforeinput.target should be undefined

      assert_equals: expected (undefined) undefined but got (object) Element node <div id="container" contenteditable="true">Here <b>is</b>...
      

    • PASS [expected FAIL] subtest: In <input type="text"> in contenteditable, execCommand("delete", false, null), ab[]c): input.inputType should be deleteContentBackward
    • FAIL [expected PASS] subtest: In <input type="text"> in contenteditable, execCommand("delete", false, null), a[b]c): beforeinput.inputType should be undefined

      assert_equals: expected (undefined) undefined but got (string) ""
      

    • FAIL [expected PASS] subtest: In <input type="text"> in contenteditable, execCommand("delete", false, null), a[b]c): beforeinput.target should be undefined

      assert_equals: expected (undefined) undefined but got (object) Element node <div id="container" contenteditable="true">Here <b>is</b>...
      

    • PASS [expected FAIL] subtest: In <input type="text"> in contenteditable, execCommand("delete", false, null), a[b]c): input.inputType should be deleteContentBackward
  • OK /editing/other/exec-command-with-text-editor.tentative.html?type=textarea
    • FAIL [expected PASS] subtest: In <textarea> in contenteditable, execCommand("delete", false, null), ab[]c): beforeinput.inputType should be undefined

      assert_equals: expected (undefined) undefined but got (string) ""
      

    • FAIL [expected PASS] subtest: In <textarea> in contenteditable, execCommand("delete", false, null), ab[]c): beforeinput.target should be undefined

      assert_equals: expected (undefined) undefined but got (object) Element node <div id="container" contenteditable="true">Here <b>is</b>...
      

    • PASS [expected FAIL] subtest: In <textarea> in contenteditable, execCommand("delete", false, null), ab[]c): input.inputType should be deleteContentBackward
    • FAIL [expected PASS] subtest: In <textarea> in contenteditable, execCommand("delete", false, null), a[b]c): beforeinput.inputType should be undefined

      assert_equals: expected (undefined) undefined but got (string) ""
      

    • FAIL [expected PASS] subtest: In <textarea> in contenteditable, execCommand("delete", false, null), a[b]c): beforeinput.target should be undefined

      assert_equals: expected (undefined) undefined but got (object) Element node <div id="container" contenteditable="true">Here <b>is</b>...
      

    • PASS [expected FAIL] subtest: In <textarea> in contenteditable, execCommand("delete", false, null), a[b]c): input.inputType should be deleteContentBackward

@github-actions
Copy link
Copy Markdown

github-actions Bot commented Mar 8, 2026

⚠️ Try run (#22817924525) failed!

@TimvdLippe TimvdLippe force-pushed the fire-events-for-commands branch from 1c5b6f7 to 1620072 Compare March 11, 2026 09:38
* expectedExecCommandResult: expected bool result of execCommand().
* expectedCommandSupported: expected bool result of queryCommandSupported().
* expectedCommandEnabled: expected bool result of queryCommandEnabled().
* beforeinputExpected: if "beforeinput" event shouldn't be fired, set
Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This was always set to null, essentially meaning it shouldn't fire.

@TimvdLippe TimvdLippe marked this pull request as ready for review March 11, 2026 09:39
@TimvdLippe TimvdLippe requested a review from gterzian as a code owner March 11, 2026 09:39
@servo-highfive servo-highfive added the S-awaiting-review There is new code that needs to be reviewed. label Mar 11, 2026
@servo-wpt-sync
Copy link
Copy Markdown
Collaborator

🤖 Opened new upstream WPT pull request (web-platform-tests/wpt#58422) with upstreamable changes.

@servo-wpt-sync
Copy link
Copy Markdown
Collaborator

✍ Updated existing upstream WPT pull request (web-platform-tests/wpt#58422) title and body.

For any enabled command, we should fire corresponding
beforeinput and input events.

Also update a WPT test to make it clearer what the
expected behavior is. None of the test expect a
beforeinput event. Before, it would fail with
"expected undefined, but got", which is not descriptive
as a failure to what's happening.

We are failing these tests, since this behavior is
currently unspecced and also requires changes in
the input element to work with text edits.

Part of servo#25005

Signed-off-by: Tim van der Lippe <tvanderlippe@gmail.com>
@TimvdLippe TimvdLippe force-pushed the fire-events-for-commands branch from 1620072 to be2f62a Compare March 26, 2026 17:56
@servo-wpt-sync
Copy link
Copy Markdown
Collaborator

📝 Transplanted new upstreamable changes to existing upstream WPT pull request (web-platform-tests/wpt#58422).

[In <input type="password">, execCommand("defaultParagraphSeparator", false, div), a[b\]c): execCommand() should return false]
expected: FAIL

[In <input type="password"> in contenteditable, execCommand("delete", false, null), ab[\]c): should not fire beforeinput event]
Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I haven't dug into why this shouldn't happen and why browsers skip it. For now, I prefer to stick to the spec and in the end figure out what's what.

Comment on lines +262 to +278
if let Some(affected_editing_host) = affected_editing_host {
let event = InputEvent::new(
window,
None,
atom!("input"),
true,
false,
Some(window),
0,
None,
false,
mapped_value_of_command(command),
CanGc::from_cx(cx),
);
let event = event.upcast::<Event>();
event.set_trusted(true);
event.fire(affected_editing_host.upcast(), CanGc::from_cx(cx));
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

How do you we know if the command actually modified the tree here such as if the paste contents were empty? It seems we will fire too many input events here. I guess this can be handled in a followup.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Well, we don't. The spec doesn't tell us anything about it.

The thing is, I think it meant the logic that we have here: if the editing host was selected before, we should fire again on it.

Still, based on the test results there is a lot more software archaeology left to do to figure out what's the correct behavior here.

@servo-highfive servo-highfive removed the S-awaiting-review There is new code that needs to be reviewed. label Mar 26, 2026
@TimvdLippe TimvdLippe added this pull request to the merge queue Mar 26, 2026
@servo-highfive servo-highfive added the S-awaiting-merge The PR is in the process of compiling and running tests on the automated CI. label Mar 26, 2026
Merged via the queue into servo:main with commit dd89609 Mar 26, 2026
30 checks passed
@TimvdLippe TimvdLippe deleted the fire-events-for-commands branch March 26, 2026 20:01
@servo-highfive servo-highfive removed the S-awaiting-merge The PR is in the process of compiling and running tests on the automated CI. label Mar 26, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

4 participants