From 4229f103cc7d579ce5250c2b292aaa4d64df74a8 Mon Sep 17 00:00:00 2001 From: Francis Bouvier Date: Sat, 11 Jul 2026 22:59:17 +0200 Subject: [PATCH] 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 --- src/browser/webapi/WebDriver.zig | 14 +++++++++++++- 1 file changed, 13 insertions(+), 1 deletion(-) diff --git a/src/browser/webapi/WebDriver.zig b/src/browser/webapi/WebDriver.zig index d712370de..976fede26 100644 --- a/src/browser/webapi/WebDriver.zig +++ b/src/browser/webapi/WebDriver.zig @@ -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); } };