webapi: WebDriver.actionSequence resolves when the actions have run

Fixes WPT /dom/events/focus-event-document-move.html: testdriver's
Actions().send() promise resolved immediately (the vendor glue returned
Promise.resolve()) while the action sequence runs on the next scheduler
tick, so tests asserting right after `await ...send()` observed the
pre-action state.

actionSequence now returns a promise that a persisted resolver settles
once the input sources have been performed; the testdriver vendor glue
returns it. The persisted resolver handle is page-managed (persist
tracks it on the context), so the task does not reset it itself - doing
so double-freed the v8 global at page teardown.

Coverage: /dom/events/focus-event-document-move.html 0/1 -> 1/1 (fully
green); /dom/events regression clean (201 files).

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
Francis Bouvier
2026-07-11 22:59:17 +02:00
parent 6c0788a922
commit 4229f103cc

View File

@@ -84,7 +84,7 @@ pub fn click(_: *const WebDriver, element: *Element, frame: *Frame) !void {
// { type: "pointer", actions: [{type: "pointerMove", x, y, origin}, ...] }
// { type: "key", actions: [{type: "keyDown", value}, ...] }
// { type: "wheel", actions: [{type: "scroll", deltaX, deltaY, origin}, ...] }
pub fn actionSequence(_: *const WebDriver, sources: js.Value, frame: *Frame) !void {
pub fn actionSequence(_: *const WebDriver, sources: js.Value, frame: *Frame) !js.Promise {
if (sources.isArray() == false) {
return error.InvalidArgument;
}
@@ -95,11 +95,16 @@ pub fn actionSequence(_: *const WebDriver, sources: js.Value, frame: *Frame) !vo
const persisted = try sources.persist();
errdefer persisted.release();
// Resolved once the actions have been performed, so testdriver's
// Actions().send() promise doesn't settle before the events fired.
const resolver = frame.js.local.?.createPromiseResolver();
const action_sequence = try arena.create(ActionSequence);
action_sequence.* = .{
.frame = frame,
.arena = arena,
.sources = persisted,
.resolver = try resolver.persist(),
};
errdefer action_sequence.sources.release();
@@ -108,12 +113,15 @@ pub fn actionSequence(_: *const WebDriver, sources: js.Value, frame: *Frame) !vo
.name = "WebDriver.actionSequence",
.finalizer = ActionSequence.finalize,
});
return resolver.promise();
}
const ActionSequence = struct {
frame: *Frame,
arena: Allocator,
sources: js.Value.Global,
resolver: js.PromiseResolver.Global,
fn run(ptr: *anyopaque) !?u32 {
const self: *ActionSequence = @ptrCast(@alignCast(ptr));
@@ -141,6 +149,8 @@ const ActionSequence = struct {
}
// "none" sources only carry pauses, which have no observable effect here.
}
ls.toLocal(self.resolver).resolve("WebDriver.actionSequence", {});
return null;
}
@@ -151,6 +161,8 @@ const ActionSequence = struct {
fn deinit(self: *ActionSequence) void {
self.sources.release();
// The persisted resolver handle is page-managed; resetting it here
// too would double-free the v8 global at page teardown.
self.frame.releaseArena(self.arena);
}
};