LibWeb: Implement WebDriver pointer action execution

This implements execution of the pointer up, pointer down, and pointer
move actions.

This isn't 100% complete. Pointer move actions are supposed to break
the move into iterations over the specified duration, which we currently
do not do.
This commit is contained in:
Timothy Flynn 2024-09-29 09:30:45 -04:00 committed by Andreas Kling
parent 6cf7d07a98
commit 8000837f78
Notes: github-actions[bot] 2024-10-01 09:03:25 +00:00
4 changed files with 509 additions and 0 deletions

View file

@ -11,6 +11,11 @@
#include <AK/JsonValue.h>
#include <AK/Math.h>
#include <AK/Utf8View.h>
#include <LibWeb/Crypto/Crypto.h>
#include <LibWeb/HTML/BrowsingContext.h>
#include <LibWeb/HTML/EventLoop/EventLoop.h>
#include <LibWeb/HTML/TraversableNavigable.h>
#include <LibWeb/Page/Page.h>
#include <LibWeb/WebDriver/Actions.h>
#include <LibWeb/WebDriver/ElementReference.h>
#include <LibWeb/WebDriver/InputState.h>
@ -103,6 +108,46 @@ static Optional<ActionObject::Origin> determine_origin(ActionsOptions const& act
return {};
}
// https://w3c.github.io/webdriver/#dfn-get-coordinates-relative-to-an-origin
static ErrorOr<CSSPixelPoint, WebDriver::Error> get_coordinates_relative_to_origin(PointerInputSource const& source, CSSPixelPoint offset, CSSPixelRect viewport, ActionObject::Origin const& origin, ActionsOptions const& actions_options)
{
// 1. Run the substeps of the first matching value of origin
auto coordinates = TRY(origin.visit(
[&](ActionObject::OriginType origin) -> ErrorOr<CSSPixelPoint, WebDriver::Error> {
switch (origin) {
// "viewport"
case ActionObject::OriginType::Viewport:
// 1. Let x equal x offset and y equal y offset.
return offset;
// "pointer"
case ActionObject::OriginType::Pointer:
// 1. Let start x be equal to the x property of source.
// 2. Let start y be equal to the y property of source.
// 3. Let x equal start x + x offset and y equal start y + y offset.
return source.position.translated(offset);
}
VERIFY_NOT_REACHED();
},
[&](String const& origin) -> ErrorOr<CSSPixelPoint, WebDriver::Error> {
// Otherwise
// 1. Let element be the result of trying to run actions options' get element origin steps with origin and
// browsing context.
// 2. If element is null, return error with error code no such element.
auto element = TRY(actions_options.get_element_origin(origin));
// 3. Let x element and y element be the result of calculating the in-view center point of element.
auto position = in_view_center_point(element, viewport);
// 4. Let x equal x element + x offset, and y equal y element + y offset.
return position.translated(offset);
}));
// 2. Return (x, y)
return coordinates;
}
// https://w3c.github.io/webdriver/#dfn-process-pointer-parameters
struct PointerParameters {
PointerInputSource::Subtype pointer_type { PointerInputSource::Subtype::Mouse };
@ -546,4 +591,427 @@ ErrorOr<Vector<Vector<ActionObject>>, WebDriver::Error> extract_an_action_sequen
return actions_by_tick;
}
// https://w3c.github.io/webdriver/#dfn-computing-the-tick-duration
static AK::Duration compute_tick_duration(ReadonlySpan<ActionObject> tick_actions)
{
// 1. Let max duration be 0.
auto max_duration = AK::Duration::zero();
// 2. For each action object in tick actions:
for (auto const& action_object : tick_actions) {
// 1. let duration be undefined.
Optional<AK::Duration> duration;
// 2. If action object has subtype property set to "pause" or action object has type property set to "pointer"
// and subtype property set to "pointerMove", or action object has type property set to "wheel" and subtype
// property set to "scroll", let duration be equal to the duration property of action object.
action_object.fields.visit(
[&](OneOf<ActionObject::PauseFields, ActionObject::PointerMoveFields, ActionObject::ScrollFields> auto const& fields) {
duration = fields.duration;
},
[](auto const&) {});
// 3. If duration is not undefined, and duration is greater than max duration, let max duration be equal to duration.
if (duration.has_value())
max_duration = max(max_duration, *duration);
}
// 3. Return max duration.
return max_duration;
}
// https://w3c.github.io/webdriver/#dfn-dispatch-a-pause-action
static void dispatch_pause_action()
{
// 1. Return success with data null.
}
// https://w3c.github.io/webdriver/#dfn-dispatch-a-pointerdown-action
static ErrorOr<void, WebDriver::Error> dispatch_pointer_down_action(ActionObject::PointerUpDownFields const& action_object, PointerInputSource& source, GlobalKeyState const& global_key_state, HTML::BrowsingContext& browsing_context)
{
// 1. Let pointerType be equal to action object's pointerType property.
auto pointer_type = action_object.pointer_type;
// 2. Let button be equal to action object's button property.
auto button = action_object.button;
// 3. If the source's pressed property contains button return success with data null.
if (has_flag(source.pressed, button))
return {};
// 4. Let x be equal to source's x property.
// 5. Let y be equal to source's y property.
auto position = browsing_context.page().css_to_device_point(source.position);
// 6. Add button to the set corresponding to source's pressed property, and let buttons be the resulting value of
// that property.
auto buttons = (source.pressed |= button);
// 7. Let width be equal to action object's width property.
// 8. Let height be equal to action object's height property.
// 9. Let pressure be equal to action object's pressure property.
// 10. Let tangentialPressure be equal to action object's tangentialPressure property.
// 11. Let tiltX be equal to action object's tiltX property.
// 12. Let tiltY be equal to action object's tiltY property.
// 13. Let twist be equal to action object's twist property.
// 14. Let altitudeAngle be equal to action object's altitudeAngle property.
// 15. Let azimuthAngle be equal to action object's azimuthAngle property.
// 16. Perform implementation-specific action dispatch steps on browsing context equivalent to pressing the button
// numbered button on the pointer with pointerId equal to source's pointerId, having type pointerType at viewport
// x coordinate x, viewport y coordinate y, width, height, pressure, tangentialPressure, tiltX, tiltY, twist,
// altitudeAngle, azimuthAngle, with buttons buttons depressed in accordance with the requirements of [UI-EVENTS]
// and [POINTER-EVENTS]. set ctrlKey, shiftKey, altKey, and metaKey equal to the corresponding items in global
// key state. Type specific properties for the pointer that are not exposed through the webdriver API must be
// set to the default value specified for hardware that doesn't support that property.
switch (pointer_type) {
case PointerInputSource::Subtype::Mouse:
browsing_context.page().handle_mousedown(position, position, button, buttons, global_key_state.modifiers());
break;
case PointerInputSource::Subtype::Pen:
return WebDriver::Error::from_code(WebDriver::ErrorCode::UnsupportedOperation, "Pen events not implemented"sv);
case PointerInputSource::Subtype::Touch:
return WebDriver::Error::from_code(WebDriver::ErrorCode::UnsupportedOperation, "Touch events not implemented"sv);
}
// 17. Return success with data null.
return {};
}
// https://w3c.github.io/webdriver/#dfn-dispatch-a-pointerup-action
static ErrorOr<void, WebDriver::Error> dispatch_pointer_up_action(ActionObject::PointerUpDownFields const& action_object, PointerInputSource& source, GlobalKeyState const& global_key_state, HTML::BrowsingContext& browsing_context)
{
// 1. Let pointerType be equal to action object's pointerType property.
auto pointer_type = action_object.pointer_type;
// 2. Let button be equal to action object's button property.
auto button = action_object.button;
// 3. If the source's pressed property does not contain button, return success with data null.
if (!has_flag(source.pressed, button))
return {};
// 4. Let x be equal to source's x property.
// 5. Let y be equal to source's y property.
auto position = browsing_context.page().css_to_device_point(source.position);
// 6. Remove button from the set corresponding to source's pressed property, and let buttons be the resulting value
// of that property.
auto buttons = (source.pressed &= ~button);
// 7. Perform implementation-specific action dispatch steps on browsing context equivalent to releasing the button
// numbered button on the pointer with pointerId equal to input source's pointerId, having type pointerType at
// viewport x coordinate x, viewport y coordinate y, with buttons buttons depressed, in accordance with the
// requirements of [UI-EVENTS] and [POINTER-EVENTS]. The generated events must set ctrlKey, shiftKey, altKey,
// and metaKey equal to the corresponding items in global key state. Type specific properties for the pointer
// that are not exposed through the webdriver API must be set to the default value specified for hardware that
// doesn't support that property.
switch (pointer_type) {
case PointerInputSource::Subtype::Mouse:
browsing_context.page().handle_mouseup(position, position, button, buttons, global_key_state.modifiers());
break;
case PointerInputSource::Subtype::Pen:
return WebDriver::Error::from_code(WebDriver::ErrorCode::UnsupportedOperation, "Pen events not implemented"sv);
case PointerInputSource::Subtype::Touch:
return WebDriver::Error::from_code(WebDriver::ErrorCode::UnsupportedOperation, "Touch events not implemented"sv);
}
// 8. Return success with data null.
return {};
}
// https://w3c.github.io/webdriver/#dfn-perform-a-pointer-move
static ErrorOr<void, WebDriver::Error> perform_pointer_move(ActionObject::PointerMoveFields const& action_object, PointerInputSource& source, GlobalKeyState const& global_key_state, HTML::BrowsingContext& browsing_context, AK::Duration, CSSPixelPoint coordinates)
{
// FIXME: 1. Let time delta be the time since the beginning of the current tick, measured in milliseconds on a monotonic clock.
// FIXME: 2. Let duration ratio be the ratio of time delta and duration, if duration is greater than 0, or 1 otherwise.
// FIXME: 3. If duration ratio is 1, or close enough to 1 that the implementation will not further subdivide the move action,
// let last be true. Otherwise let last be false.
// FIXME: 4. If last is true, let x equal target x and y equal target y.
// FIXME: 5. Otherwise let x equal an approximation to duration ratio × (target x - start x) + start x, and y equal an
// approximation to duration ratio × (target y - start y) + start y.
// 6. Let current x equal the x property of input state.
// 7. Let current y equal the y property of input state.
auto current = source.position;
// 8. If x is not equal to current x or y is not equal to current y, run the following steps:
if (current != coordinates) {
// 1. Let buttons be equal to input state's buttons property.
auto buttons = source.pressed;
// 2. Perform implementation-specific action dispatch steps on browsing context equivalent to moving the pointer
// with pointerId equal to input source's pointerId, having type pointerType from viewport x coordinate current
// x, viewport y coordinate current y to viewport x coordinate x and viewport y coordinate y, width, height,
// pressure, tangentialPressure, tiltX, tiltY, twist, altitudeAngle, azimuthAngle, with buttons buttons
// depressed, in accordance with the requirements of [UI-EVENTS] and [POINTER-EVENTS]. The generated events
// must set ctrlKey, shiftKey, altKey, and metaKey equal to the corresponding items in global key state. Type
// specific properties for the pointer that are not exposed through the WebDriver API must be set to the
// default value specified for hardware that doesn't support that property. In the case where the pointerType
// is "pen" or "touch", and buttons is empty, this may be a no-op. For a pointer of type "mouse" this will
// always produce events including at least a pointerMove event.
auto position = browsing_context.page().css_to_device_point(coordinates);
switch (action_object.pointer_type) {
case PointerInputSource::Subtype::Mouse:
browsing_context.page().handle_mousemove(position, position, buttons, global_key_state.modifiers());
break;
case PointerInputSource::Subtype::Pen:
return WebDriver::Error::from_code(WebDriver::ErrorCode::UnsupportedOperation, "Pen events not implemented"sv);
case PointerInputSource::Subtype::Touch:
return WebDriver::Error::from_code(WebDriver::ErrorCode::UnsupportedOperation, "Touch events not implemented"sv);
}
// 3. Let input state's x property equal x and y property equal y.
source.position = coordinates;
}
// FIXME: 9. If last is true, return.
// FIXME: 10. Run the following substeps in parallel:
{
// FIXME: 1. Asynchronously wait for an implementation defined amount of time to pass.
// FIXME: 2. Perform a pointer move with arguments input state, duration, start x, start y, target x, target y.
}
return {};
}
// https://w3c.github.io/webdriver/#dfn-dispatch-a-pointermove-action
static ErrorOr<void, WebDriver::Error> dispatch_pointer_move_action(ActionObject::PointerMoveFields const& action_object, PointerInputSource& source, GlobalKeyState const& global_key_state, AK::Duration tick_duration, HTML::BrowsingContext& browsing_context, ActionsOptions const& actions_options)
{
auto viewport = browsing_context.page().top_level_traversable()->viewport_rect();
// 1. Let x offset be equal to the x property of action object.
// 2. Let y offset be equal to the y property of action object.
// 3. Let origin be equal to the origin property of action object.
// 4. Let (x, y) be the result of trying to get coordinates relative to an origin with source, x offset, y offset,
// origin, browsing context, and actions options.
auto coordinates = TRY(get_coordinates_relative_to_origin(source, action_object.position, viewport, action_object.origin, actions_options));
// 5. If x is less than 0 or greater than the width of the viewport in CSS pixels, then return error with error code move target out of bounds.
if (coordinates.x() < 0 || coordinates.x() > viewport.width())
return WebDriver::Error::from_code(WebDriver::ErrorCode::MoveTargetOutOfBounds, ByteString::formatted("Coordinates {} are out of bounds", coordinates));
// 6. If y is less than 0 or greater than the height of the viewport in CSS pixels, then return error with error code move target out of bounds.
if (coordinates.y() < 0 || coordinates.y() > viewport.height())
return WebDriver::Error::from_code(WebDriver::ErrorCode::MoveTargetOutOfBounds, ByteString::formatted("Coordinates {} are out of bounds", coordinates));
// 7. Let duration be equal to action object's duration property if it is not undefined, or tick duration otherwise.
[[maybe_unused]] auto duration = action_object.duration.value_or(tick_duration);
// FIXME: 8. If duration is greater than 0 and inside any implementation-defined bounds, asynchronously wait for an
// implementation defined amount of time to pass.
// 9. Let width be equal to action object's width property.
// 10. Let height be equal to action object's height property.
// 11. Let pressure be equal to action object's pressure property.
// 12. Let tangentialPressure be equal to action object's tangentialPressure property.
// 13. Let tiltX be equal to action object's tiltX property.
// 14. Let tiltY be equal to action object's tiltY property.
// 15. Let twist be equal to action object's twist property.
// 16. Let altitudeAngle be equal to action object's altitudeAngle property.
// 17. Let azimuthAngle be equal to action object's azimuthAngle property.
// 18. Perform a pointer move with arguments source, global key state, duration, start x, start y, x, y, width,
// height, pressure, tangentialPressure, tiltX, tiltY, twist, altitudeAngle, azimuthAngle.
TRY(perform_pointer_move(action_object, source, global_key_state, browsing_context, duration, coordinates));
// 19. Return success with data null.
return {};
}
// https://w3c.github.io/webdriver/#dfn-dispatch-actions-inner
class ActionExecutor final : public JS::Cell {
JS_CELL(ActionExecutor, JS::Cell);
JS_DECLARE_ALLOCATOR(ActionExecutor);
public:
ActionExecutor(InputState& input_state, Vector<Vector<ActionObject>> actions_by_tick, HTML::BrowsingContext& browsing_context, ActionsOptions actions_options, OnActionsComplete on_complete)
: m_browsing_context(browsing_context)
, m_input_state(input_state)
, m_actions_options(move(actions_options))
, m_actions_by_tick(move(actions_by_tick))
, m_on_complete(on_complete)
{
}
// 1. For each item tick actions in actions by tick:
void process_next_tick()
{
if (m_current_tick >= m_actions_by_tick.size()) {
m_on_complete->function()(JsonValue {});
return;
}
auto const& tick_actions = m_actions_by_tick[m_current_tick++];
// 1. Let tick duration be the result of computing the tick duration with argument tick actions.
auto tick_duration = compute_tick_duration(tick_actions);
// 2. Try to dispatch tick actions with input state, tick actions, tick duration, browsing context, and actions options.
if (auto result = dispatch_tick_actions(m_input_state, tick_actions, tick_duration, m_browsing_context, m_actions_options); result.is_error()) {
m_on_complete->function()(result.release_error());
return;
}
// 3. Wait until the following conditions are all met:
// * There are no pending asynchronous waits arising from the last invocation of the dispatch tick actions
// steps.
// * The user agent event loop has spun enough times to process the DOM events generated by the last
// invocation of the dispatch tick actions steps.
// * At least tick duration milliseconds have passed.
// FIXME: We currently do not implement any asynchronous waits. And we assume that Page will generally fire the
// events of interest synchronously. So we simply wait for the tick duration to pass, and then let the
// event loop spin a single time.
m_timer = Core::Timer::create_single_shot(static_cast<int>(tick_duration.to_milliseconds()), [this]() {
m_timer = nullptr;
HTML::queue_a_task(HTML::Task::Source::Unspecified, nullptr, nullptr, JS::create_heap_function(heap(), [this]() {
process_next_tick();
}));
});
m_timer->start();
}
private:
virtual void visit_edges(Cell::Visitor& visitor) override
{
Base::visit_edges(visitor);
visitor.visit(m_browsing_context);
visitor.visit(m_on_complete);
}
JS::NonnullGCPtr<HTML::BrowsingContext> m_browsing_context;
InputState& m_input_state;
ActionsOptions m_actions_options;
Vector<Vector<ActionObject>> m_actions_by_tick;
size_t m_current_tick { 0 };
OnActionsComplete m_on_complete;
RefPtr<Core::Timer> m_timer;
};
JS_DEFINE_ALLOCATOR(ActionExecutor);
// https://w3c.github.io/webdriver/#dfn-dispatch-actions
JS::NonnullGCPtr<JS::Cell> dispatch_actions(InputState& input_state, Vector<Vector<ActionObject>> actions_by_tick, HTML::BrowsingContext& browsing_context, ActionsOptions actions_options, OnActionsComplete on_complete)
{
// 1. Let token be a new unique identifier.
auto token = MUST(Crypto::generate_random_uuid());
// 2. Enqueue token in input state's actions queue.
input_state.actions_queue.append(token);
// 3. Wait for token to be the first item in input state's actions queue.
// FIXME: We should probably do this, but our WebDriver currently blocks until a given action is complete anyways,
// so we should never arrive here with an ongoing action (which we verify for now).
VERIFY(input_state.actions_queue.size() == 1);
// 4. Let actions result be the result of dispatch actions inner with input state, actions by tick, browsing
// context, and actions options.
auto action_executor = browsing_context.heap().allocate_without_realm<ActionExecutor>(input_state, move(actions_by_tick), browsing_context, move(actions_options), on_complete);
action_executor->process_next_tick();
// 5. Dequeue input state's actions queue.
auto executed_token = input_state.actions_queue.take_first();
// 6. Assert: this returns token
VERIFY(executed_token == token);
// 7. Return actions result.
return action_executor;
}
// https://w3c.github.io/webdriver/#dfn-dispatch-tick-actions
ErrorOr<void, WebDriver::Error> dispatch_tick_actions(InputState& input_state, ReadonlySpan<ActionObject> tick_actions, AK::Duration tick_duration, HTML::BrowsingContext& browsing_context, ActionsOptions const& actions_options)
{
// 1. For each action object in tick actions:
for (auto const& action_object : tick_actions) {
// 1. Let input id be equal to the value of action object's id property.
auto const& input_id = action_object.id;
// 2. Let source type be equal to the value of action object's type property.
// NOTE: We don't actually need this, we can determine the event to fire based on the subtype.
// 3. Let source be the result of get an input source given input state and input id.
auto source = get_input_source(input_state, input_id);
// 4. Assert: source is not undefined.
VERIFY(source.has_value());
// 5. Let global key state be the result of get the global key state with input state.
auto global_key_state = get_global_key_state(input_state);
// 6. Let subtype be action object's subtype.
auto subtype = action_object.subtype;
// 7. Let algorithm be the value of the column dispatch action algorithm from the following table where the
// source type column is source type and the subtype column is equal to subtype.
//
// source type | subtype | Dispatch action algorithm
// ---------------------------------------------------------------
// "none" | "pause" | Dispatch a pause action
// "key" | "pause" | Dispatch a pause action
// "key" | "keyDown" | Dispatch a keyDown action
// "key" | "keyUp" | Dispatch a keyUp action
// "pointer" | "pause" | Dispatch a pause action
// "pointer" | "pointerDown" | Dispatch a pointerDown action
// "pointer" | "pointerUp" | Dispatch a pointerUp action
// "pointer" | "pointerMove" | Dispatch a pointerMove action
// "pointer" | "pointerCancel" | Dispatch a pointerCancel action
// "wheel" | "pause" | Dispatch a pause action
// "wheel" | "scroll" | Dispatch a scroll action
// 8. Try to run algorithm with arguments action object, source, global key state, tick duration, browsing
// context, and actions options.
switch (subtype) {
case ActionObject::Subtype::Pause:
dispatch_pause_action();
break;
case ActionObject::Subtype::KeyDown:
return WebDriver::Error::from_code(WebDriver::ErrorCode::UnsupportedOperation, "Key down events not implemented"sv);
case ActionObject::Subtype::KeyUp:
return WebDriver::Error::from_code(WebDriver::ErrorCode::UnsupportedOperation, "Key up events not implemented"sv);
case ActionObject::Subtype::PointerDown:
TRY(dispatch_pointer_down_action(action_object.pointer_up_down_fields(), source->get<PointerInputSource>(), global_key_state, browsing_context));
break;
case ActionObject::Subtype::PointerUp:
TRY(dispatch_pointer_up_action(action_object.pointer_up_down_fields(), source->get<PointerInputSource>(), global_key_state, browsing_context));
break;
case ActionObject::Subtype::PointerMove:
TRY(dispatch_pointer_move_action(action_object.pointer_move_fields(), source->get<PointerInputSource>(), global_key_state, tick_duration, browsing_context, actions_options));
break;
case ActionObject::Subtype::PointerCancel:
return WebDriver::Error::from_code(WebDriver::ErrorCode::UnsupportedOperation, "Pointer cancel events not implemented"sv);
case ActionObject::Subtype::Scroll:
return WebDriver::Error::from_code(WebDriver::ErrorCode::UnsupportedOperation, "Scroll events not implemented"sv);
}
// 9. If subtype is "keyDown", append a copy of action object with the subtype property changed to "keyUp" to
// input state's input cancel list.
if (subtype == ActionObject::Subtype::KeyDown) {
auto action_copy = action_object;
action_copy.subtype = ActionObject::Subtype::KeyUp;
input_state.input_cancel_list.append(move(action_copy));
}
// 10. If subtype is "pointerDown", append a copy of action object with the subtype property changed to
// "pointerUp" to input state's input cancel list.
if (subtype == ActionObject::Subtype::PointerDown) {
auto action_copy = action_object;
action_copy.subtype = ActionObject::Subtype::PointerUp;
input_state.input_cancel_list.append(move(action_copy));
}
}
// 2. Return success with data null.
return {};
}
}

View file

@ -13,11 +13,13 @@
#include <AK/Variant.h>
#include <AK/Vector.h>
#include <LibJS/Heap/GCPtr.h>
#include <LibJS/Heap/HeapFunction.h>
#include <LibWeb/Forward.h>
#include <LibWeb/PixelUnits.h>
#include <LibWeb/UIEvents/MouseButton.h>
#include <LibWeb/WebDriver/Error.h>
#include <LibWeb/WebDriver/InputSource.h>
#include <LibWeb/WebDriver/Response.h>
namespace Web::WebDriver {
@ -120,6 +122,11 @@ struct ActionsOptions {
Function<ErrorOr<JS::NonnullGCPtr<DOM::Element>, WebDriver::Error>(StringView)> get_element_origin;
};
using OnActionsComplete = JS::NonnullGCPtr<JS::HeapFunction<void(Web::WebDriver::Response)>>;
ErrorOr<Vector<Vector<ActionObject>>, WebDriver::Error> extract_an_action_sequence(InputState&, JsonValue const&, ActionsOptions const&);
JS::NonnullGCPtr<JS::Cell> dispatch_actions(InputState&, Vector<Vector<ActionObject>>, HTML::BrowsingContext&, ActionsOptions, OnActionsComplete);
ErrorOr<void, WebDriver::Error> dispatch_tick_actions(InputState&, ReadonlySpan<ActionObject>, AK::Duration, HTML::BrowsingContext&, ActionsOptions const&);
}

View file

@ -8,6 +8,8 @@
#include <LibWeb/DOM/Element.h>
#include <LibWeb/DOM/Node.h>
#include <LibWeb/DOM/ShadowRoot.h>
#include <LibWeb/Geometry/DOMRect.h>
#include <LibWeb/Geometry/DOMRectList.h>
#include <LibWeb/WebDriver/ElementReference.h>
namespace Web::WebDriver {
@ -152,4 +154,33 @@ ErrorOr<Web::DOM::ShadowRoot*, Web::WebDriver::Error> get_known_shadow_root(Stri
return static_cast<Web::DOM::ShadowRoot*>(node);
}
// https://w3c.github.io/webdriver/#dfn-center-point
CSSPixelPoint in_view_center_point(DOM::Element const& element, CSSPixelRect viewport)
{
// 1. Let rectangle be the first element of the DOMRect sequence returned by calling getClientRects() on element.
auto const* rectangle = element.get_client_rects()->item(0);
VERIFY(rectangle);
// 2. Let left be max(0, min(x coordinate, x coordinate + width dimension)).
auto left = max(0.0, min(rectangle->x(), rectangle->x() + rectangle->width()));
// 3. Let right be min(innerWidth, max(x coordinate, x coordinate + width dimension)).
auto right = min(viewport.width().to_double(), max(rectangle->x(), rectangle->x() + rectangle->width()));
// 4. Let top be max(0, min(y coordinate, y coordinate + height dimension)).
auto top = max(0.0, min(rectangle->y(), rectangle->y() + rectangle->height()));
// 5. Let bottom be min(innerHeight, max(y coordinate, y coordinate + height dimension)).
auto bottom = min(viewport.height().to_double(), max(rectangle->y(), rectangle->y() + rectangle->height()));
// 6. Let x be floor((left + right) ÷ 2.0).
auto x = floor((left + right) / 2.0);
// 7. Let y be floor((top + bottom) ÷ 2.0).
auto y = floor((top + bottom) / 2.0);
// 8. Return the pair of (x, y).
return { x, y };
}
}

View file

@ -11,6 +11,7 @@
#include <AK/JsonObject.h>
#include <LibJS/Heap/GCPtr.h>
#include <LibWeb/Forward.h>
#include <LibWeb/PixelUnits.h>
#include <LibWeb/WebDriver/Error.h>
namespace Web::WebDriver {
@ -27,4 +28,6 @@ ByteString get_or_create_a_shadow_root_reference(Web::DOM::ShadowRoot const& sha
JsonObject shadow_root_reference_object(Web::DOM::ShadowRoot const& shadow_root);
ErrorOr<Web::DOM::ShadowRoot*, Web::WebDriver::Error> get_known_shadow_root(StringView shadow_id);
CSSPixelPoint in_view_center_point(DOM::Element const& element, CSSPixelRect viewport);
}