mirror of
https://github.com/LadybirdBrowser/ladybird.git
synced 2025-06-09 09:34:57 +09:00
Everywhere: Hoist the Libraries folder to the top-level
This commit is contained in:
parent
950e819ee7
commit
93712b24bf
Notes:
github-actions[bot]
2024-11-10 11:51:52 +00:00
Author: https://github.com/trflynn89
Commit: 93712b24bf
Pull-request: https://github.com/LadybirdBrowser/ladybird/pull/2256
Reviewed-by: https://github.com/sideshowbarker
4547 changed files with 104 additions and 113 deletions
1606
Libraries/LibWeb/WebDriver/Actions.cpp
Normal file
1606
Libraries/LibWeb/WebDriver/Actions.cpp
Normal file
File diff suppressed because it is too large
Load diff
136
Libraries/LibWeb/WebDriver/Actions.h
Normal file
136
Libraries/LibWeb/WebDriver/Actions.h
Normal file
|
@ -0,0 +1,136 @@
|
|||
/*
|
||||
* Copyright (c) 2024, Tim Flynn <trflynn89@ladybird.org>
|
||||
*
|
||||
* SPDX-License-Identifier: BSD-2-Clause
|
||||
*/
|
||||
|
||||
#pragma once
|
||||
|
||||
#include <AK/Optional.h>
|
||||
#include <AK/String.h>
|
||||
#include <AK/Time.h>
|
||||
#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 {
|
||||
|
||||
// https://w3c.github.io/webdriver/#dfn-action-object
|
||||
struct ActionObject {
|
||||
enum class Subtype {
|
||||
Pause,
|
||||
KeyUp,
|
||||
KeyDown,
|
||||
PointerUp,
|
||||
PointerDown,
|
||||
PointerMove,
|
||||
PointerCancel,
|
||||
Scroll,
|
||||
};
|
||||
|
||||
enum class OriginType {
|
||||
Viewport,
|
||||
Pointer,
|
||||
};
|
||||
using Origin = Variant<OriginType, String>;
|
||||
|
||||
struct PauseFields {
|
||||
Optional<AK::Duration> duration;
|
||||
};
|
||||
|
||||
struct KeyFields {
|
||||
u32 value { 0 };
|
||||
};
|
||||
|
||||
struct PointerFields {
|
||||
PointerInputSource::Subtype pointer_type { PointerInputSource::Subtype::Mouse };
|
||||
Optional<double> width;
|
||||
Optional<double> height;
|
||||
Optional<double> pressure;
|
||||
Optional<double> tangential_pressure;
|
||||
Optional<i32> tilt_x;
|
||||
Optional<i32> tilt_y;
|
||||
Optional<u32> twist;
|
||||
Optional<double> altitude_angle;
|
||||
Optional<double> azimuth_angle;
|
||||
};
|
||||
|
||||
struct PointerUpDownFields : public PointerFields {
|
||||
UIEvents::MouseButton button { UIEvents::MouseButton::None };
|
||||
};
|
||||
|
||||
struct PointerMoveFields : public PointerFields {
|
||||
Optional<AK::Duration> duration;
|
||||
Origin origin { OriginType::Viewport };
|
||||
CSSPixelPoint position;
|
||||
};
|
||||
|
||||
struct PointerCancelFields {
|
||||
PointerInputSource::Subtype pointer_type { PointerInputSource::Subtype::Mouse };
|
||||
};
|
||||
|
||||
struct ScrollFields {
|
||||
Origin origin { OriginType::Viewport };
|
||||
Optional<AK::Duration> duration;
|
||||
i64 x { 0 };
|
||||
i64 y { 0 };
|
||||
i64 delta_x { 0 };
|
||||
i64 delta_y { 0 };
|
||||
};
|
||||
|
||||
ActionObject(String id, InputSourceType type, Subtype subtype);
|
||||
|
||||
void set_pointer_type(PointerInputSource::Subtype);
|
||||
|
||||
PauseFields& pause_fields() { return fields.get<PauseFields>(); }
|
||||
PauseFields const& pause_fields() const { return fields.get<PauseFields>(); }
|
||||
|
||||
KeyFields& key_fields() { return fields.get<KeyFields>(); }
|
||||
KeyFields const& key_fields() const { return fields.get<KeyFields>(); }
|
||||
|
||||
PointerUpDownFields& pointer_up_down_fields() { return fields.get<PointerUpDownFields>(); }
|
||||
PointerUpDownFields const& pointer_up_down_fields() const { return fields.get<PointerUpDownFields>(); }
|
||||
|
||||
PointerMoveFields& pointer_move_fields() { return fields.get<PointerMoveFields>(); }
|
||||
PointerMoveFields const& pointer_move_fields() const { return fields.get<PointerMoveFields>(); }
|
||||
|
||||
PointerCancelFields& pointer_cancel_fields() { return fields.get<PointerCancelFields>(); }
|
||||
PointerCancelFields const& pointer_cancel_fields() const { return fields.get<PointerCancelFields>(); }
|
||||
|
||||
ScrollFields& scroll_fields() { return fields.get<ScrollFields>(); }
|
||||
ScrollFields const& scroll_fields() const { return fields.get<ScrollFields>(); }
|
||||
|
||||
String id;
|
||||
InputSourceType type;
|
||||
Subtype subtype;
|
||||
|
||||
using Fields = Variant<PauseFields, KeyFields, PointerUpDownFields, PointerMoveFields, PointerCancelFields, ScrollFields>;
|
||||
Fields fields;
|
||||
};
|
||||
|
||||
// https://w3c.github.io/webdriver/#dfn-actions-options
|
||||
struct ActionsOptions {
|
||||
using IsElementOrigin = bool (*)(JsonValue const&);
|
||||
using GetElementOrigin = ErrorOr<JS::NonnullGCPtr<DOM::Element>, WebDriver::Error> (*)(HTML::BrowsingContext const&, StringView);
|
||||
|
||||
IsElementOrigin is_element_origin { nullptr };
|
||||
GetElementOrigin get_element_origin { nullptr };
|
||||
};
|
||||
|
||||
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&);
|
||||
JS::NonnullGCPtr<JS::Cell> dispatch_list_of_actions(InputState&, Vector<ActionObject>, HTML::BrowsingContext&, ActionsOptions, OnActionsComplete);
|
||||
JS::NonnullGCPtr<JS::Cell> dispatch_actions_for_a_string(Web::WebDriver::InputState&, String const& input_id, Web::WebDriver::InputSource&, StringView text, Web::HTML::BrowsingContext&, Web::WebDriver::OnActionsComplete);
|
||||
|
||||
}
|
464
Libraries/LibWeb/WebDriver/Capabilities.cpp
Normal file
464
Libraries/LibWeb/WebDriver/Capabilities.cpp
Normal file
|
@ -0,0 +1,464 @@
|
|||
/*
|
||||
* Copyright (c) 2022, Tim Flynn <trflynn89@serenityos.org>
|
||||
*
|
||||
* SPDX-License-Identifier: BSD-2-Clause
|
||||
*/
|
||||
|
||||
#include <AK/Debug.h>
|
||||
#include <AK/JsonArray.h>
|
||||
#include <AK/JsonObject.h>
|
||||
#include <AK/JsonValue.h>
|
||||
#include <AK/Optional.h>
|
||||
#include <LibWeb/Loader/UserAgent.h>
|
||||
#include <LibWeb/WebDriver/Capabilities.h>
|
||||
#include <LibWeb/WebDriver/TimeoutsConfiguration.h>
|
||||
|
||||
namespace Web::WebDriver {
|
||||
|
||||
// https://w3c.github.io/webdriver/#dfn-deserialize-as-a-page-load-strategy
|
||||
static Response deserialize_as_a_page_load_strategy(JsonValue value)
|
||||
{
|
||||
// 1. If value is not a string return an error with error code invalid argument.
|
||||
if (!value.is_string())
|
||||
return Error::from_code(ErrorCode::InvalidArgument, "Capability pageLoadStrategy must be a string"sv);
|
||||
|
||||
// 2. If there is no entry in the table of page load strategies with keyword value return an error with error code invalid argument.
|
||||
if (!value.as_string().is_one_of("none"sv, "eager"sv, "normal"sv))
|
||||
return Error::from_code(ErrorCode::InvalidArgument, "Invalid pageLoadStrategy capability"sv);
|
||||
|
||||
// 3. Return success with data value.
|
||||
return value;
|
||||
}
|
||||
|
||||
// https://w3c.github.io/webdriver/#dfn-deserialize-as-an-unhandled-prompt-behavior
|
||||
static Response deserialize_as_an_unhandled_prompt_behavior(JsonValue value)
|
||||
{
|
||||
// 1. If value is not a string return an error with error code invalid argument.
|
||||
if (!value.is_string())
|
||||
return Error::from_code(ErrorCode::InvalidArgument, "Capability unhandledPromptBehavior must be a string"sv);
|
||||
|
||||
// 2. If value is not present as a keyword in the known prompt handling approaches table return an error with error code invalid argument.
|
||||
if (!value.as_string().is_one_of("dismiss"sv, "accept"sv, "dismiss and notify"sv, "accept and notify"sv, "ignore"sv))
|
||||
return Error::from_code(ErrorCode::InvalidArgument, "Invalid pageLoadStrategy capability"sv);
|
||||
|
||||
// 3. Return success with data value.
|
||||
return value;
|
||||
}
|
||||
|
||||
// https://w3c.github.io/webdriver/#dfn-deserialize-as-a-proxy
|
||||
static ErrorOr<JsonObject, Error> deserialize_as_a_proxy(JsonValue parameter)
|
||||
{
|
||||
// 1. If parameter is not a JSON Object return an error with error code invalid argument.
|
||||
if (!parameter.is_object())
|
||||
return Error::from_code(ErrorCode::InvalidArgument, "Capability proxy must be an object"sv);
|
||||
|
||||
// 2. Let proxy be a new, empty proxy configuration object.
|
||||
JsonObject proxy;
|
||||
|
||||
// 3. For each enumerable own property in parameter run the following substeps:
|
||||
TRY(parameter.as_object().try_for_each_member([&](auto const& key, JsonValue const& value) -> ErrorOr<void, Error> {
|
||||
// 1. Let key be the name of the property.
|
||||
// 2. Let value be the result of getting a property named name from capability.
|
||||
|
||||
// FIXME: 3. If there is no matching key for key in the proxy configuration table return an error with error code invalid argument.
|
||||
// FIXME: 4. If value is not one of the valid values for that key, return an error with error code invalid argument.
|
||||
|
||||
// 5. Set a property key to value on proxy.
|
||||
proxy.set(key, value);
|
||||
|
||||
return {};
|
||||
}));
|
||||
|
||||
return proxy;
|
||||
}
|
||||
|
||||
static InterfaceMode default_interface_mode { InterfaceMode::Graphical };
|
||||
|
||||
void set_default_interface_mode(InterfaceMode interface_mode)
|
||||
{
|
||||
default_interface_mode = interface_mode;
|
||||
}
|
||||
|
||||
static Response deserialize_as_ladybird_options(JsonValue value)
|
||||
{
|
||||
if (!value.is_object())
|
||||
return Error::from_code(ErrorCode::InvalidArgument, "Extension capability serenity:ladybird must be an object"sv);
|
||||
|
||||
auto const& object = value.as_object();
|
||||
|
||||
if (auto headless = object.get("headless"sv); headless.has_value() && !headless->is_bool())
|
||||
return Error::from_code(ErrorCode::InvalidArgument, "Extension capability serenity:ladybird/headless must be a boolean"sv);
|
||||
|
||||
return value;
|
||||
}
|
||||
|
||||
static JsonObject default_ladybird_options()
|
||||
{
|
||||
JsonObject options;
|
||||
options.set("headless"sv, default_interface_mode == InterfaceMode::Headless);
|
||||
|
||||
return options;
|
||||
}
|
||||
|
||||
// https://w3c.github.io/webdriver/#dfn-validate-capabilities
|
||||
static ErrorOr<JsonObject, Error> validate_capabilities(JsonValue const& capability)
|
||||
{
|
||||
// 1. If capability is not a JSON Object return an error with error code invalid argument.
|
||||
if (!capability.is_object())
|
||||
return Error::from_code(ErrorCode::InvalidArgument, "Capability is not an Object"sv);
|
||||
|
||||
// 2. Let result be an empty JSON Object.
|
||||
JsonObject result;
|
||||
|
||||
// 3. For each enumerable own property in capability, run the following substeps:
|
||||
TRY(capability.as_object().try_for_each_member([&](auto const& name, JsonValue const& value) -> ErrorOr<void, Error> {
|
||||
// a. Let name be the name of the property.
|
||||
// b. Let value be the result of getting a property named name from capability.
|
||||
|
||||
// c. Run the substeps of the first matching condition:
|
||||
JsonValue deserialized;
|
||||
|
||||
// -> value is null
|
||||
if (value.is_null()) {
|
||||
// Let deserialized be set to null.
|
||||
}
|
||||
|
||||
// -> name equals "acceptInsecureCerts"
|
||||
else if (name == "acceptInsecureCerts"sv) {
|
||||
// If value is not a boolean return an error with error code invalid argument. Otherwise, let deserialized be set to value
|
||||
if (!value.is_bool())
|
||||
return Error::from_code(ErrorCode::InvalidArgument, "Capability acceptInsecureCerts must be a boolean"sv);
|
||||
deserialized = value;
|
||||
}
|
||||
|
||||
// -> name equals "browserName"
|
||||
// -> name equals "browserVersion"
|
||||
// -> name equals "platformName"
|
||||
else if (name.is_one_of("browserName"sv, "browserVersion"sv, "platformName"sv)) {
|
||||
// If value is not a string return an error with error code invalid argument. Otherwise, let deserialized be set to value.
|
||||
if (!value.is_string())
|
||||
return Error::from_code(ErrorCode::InvalidArgument, ByteString::formatted("Capability {} must be a string", name));
|
||||
deserialized = value;
|
||||
}
|
||||
|
||||
// -> name equals "pageLoadStrategy"
|
||||
else if (name == "pageLoadStrategy"sv) {
|
||||
// Let deserialized be the result of trying to deserialize as a page load strategy with argument value.
|
||||
deserialized = TRY(deserialize_as_a_page_load_strategy(value));
|
||||
}
|
||||
|
||||
// -> name equals "proxy"
|
||||
else if (name == "proxy"sv) {
|
||||
// Let deserialized be the result of trying to deserialize as a proxy with argument value.
|
||||
deserialized = TRY(deserialize_as_a_proxy(value));
|
||||
}
|
||||
|
||||
// -> name equals "strictFileInteractability"
|
||||
else if (name == "strictFileInteractability"sv) {
|
||||
// If value is not a boolean return an error with error code invalid argument. Otherwise, let deserialized be set to value
|
||||
if (!value.is_bool())
|
||||
return Error::from_code(ErrorCode::InvalidArgument, "Capability strictFileInteractability must be a boolean"sv);
|
||||
deserialized = value;
|
||||
}
|
||||
|
||||
// -> name equals "timeouts"
|
||||
else if (name == "timeouts"sv) {
|
||||
// Let deserialized be the result of trying to JSON deserialize as a timeouts configuration the value.
|
||||
auto timeouts = TRY(json_deserialize_as_a_timeouts_configuration(value));
|
||||
deserialized = JsonValue { timeouts_object(timeouts) };
|
||||
}
|
||||
|
||||
// -> name equals "unhandledPromptBehavior"
|
||||
else if (name == "unhandledPromptBehavior"sv) {
|
||||
// Let deserialized be the result of trying to deserialize as an unhandled prompt behavior with argument value.
|
||||
deserialized = TRY(deserialize_as_an_unhandled_prompt_behavior(value));
|
||||
}
|
||||
|
||||
// FIXME: -> name is the name of an additional WebDriver capability
|
||||
// FIXME: Let deserialized be the result of trying to run the additional capability deserialization algorithm for the extension capability corresponding to name, with argument value.
|
||||
|
||||
// https://w3c.github.io/webdriver-bidi/#type-session-CapabilityRequest
|
||||
else if (name == "webSocketUrl"sv) {
|
||||
// 1. If value is not a boolean, return error with code invalid argument.
|
||||
if (!value.is_bool())
|
||||
return Error::from_code(ErrorCode::InvalidArgument, "Capability webSocketUrl must be a boolean"sv);
|
||||
|
||||
// 2. Return success with data value.
|
||||
deserialized = value;
|
||||
}
|
||||
|
||||
// -> name is the key of an extension capability
|
||||
// If name is known to the implementation, let deserialized be the result of trying to deserialize value in an implementation-specific way. Otherwise, let deserialized be set to value.
|
||||
else if (name == "serenity:ladybird"sv) {
|
||||
deserialized = TRY(deserialize_as_ladybird_options(value));
|
||||
}
|
||||
|
||||
// -> The remote end is an endpoint node
|
||||
else {
|
||||
// Return an error with error code invalid argument.
|
||||
return Error::from_code(ErrorCode::InvalidArgument, ByteString::formatted("Unrecognized capability: {}", name));
|
||||
}
|
||||
|
||||
// d. If deserialized is not null, set a property on result with name name and value deserialized.
|
||||
if (!deserialized.is_null())
|
||||
result.set(name, move(deserialized));
|
||||
|
||||
return {};
|
||||
}));
|
||||
|
||||
// 4. Return success with data result.
|
||||
return result;
|
||||
}
|
||||
|
||||
// https://w3c.github.io/webdriver/#dfn-merging-capabilities
|
||||
static ErrorOr<JsonObject, Error> merge_capabilities(JsonObject const& primary, Optional<JsonObject const&> const& secondary)
|
||||
{
|
||||
// 1. Let result be a new JSON Object.
|
||||
JsonObject result;
|
||||
|
||||
// 2. For each enumerable own property in primary, run the following substeps:
|
||||
primary.for_each_member([&](auto const& name, auto const& value) {
|
||||
// a. Let name be the name of the property.
|
||||
// b. Let value be the result of getting a property named name from primary.
|
||||
|
||||
// c. Set a property on result with name name and value value.
|
||||
result.set(name, value);
|
||||
});
|
||||
|
||||
// 3. If secondary is undefined, return result.
|
||||
if (!secondary.has_value())
|
||||
return result;
|
||||
|
||||
// 4. For each enumerable own property in secondary, run the following substeps:
|
||||
TRY(secondary->try_for_each_member([&](auto const& name, auto const& value) -> ErrorOr<void, Error> {
|
||||
// a. Let name be the name of the property.
|
||||
// b. Let value be the result of getting a property named name from secondary.
|
||||
|
||||
// c. Let primary value be the result of getting the property name from primary.
|
||||
auto primary_value = primary.get(name);
|
||||
|
||||
// d. If primary value is not undefined, return an error with error code invalid argument.
|
||||
if (primary_value.has_value())
|
||||
return Error::from_code(ErrorCode::InvalidArgument, ByteString::formatted("Unable to merge capability {}", name));
|
||||
|
||||
// e. Set a property on result with name name and value value.
|
||||
result.set(name, value);
|
||||
return {};
|
||||
}));
|
||||
|
||||
// 5. Return result.
|
||||
return result;
|
||||
}
|
||||
|
||||
static bool matches_browser_version(StringView requested_version, StringView required_version)
|
||||
{
|
||||
// FIXME: Handle relative (>, >=, <. <=) comparisons. For now, require an exact match.
|
||||
return requested_version == required_version;
|
||||
}
|
||||
|
||||
static bool matches_platform_name(StringView requested_platform_name, StringView required_platform_name)
|
||||
{
|
||||
if (requested_platform_name == required_platform_name)
|
||||
return true;
|
||||
|
||||
// The following platform names are in common usage with well-understood semantics and, when matching capabilities, greatest interoperability can be achieved by honoring them as valid synonyms for well-known Operating Systems:
|
||||
// "linux" Any server or desktop system based upon the Linux kernel.
|
||||
// "mac" Any version of Apple’s macOS.
|
||||
// "windows" Any version of Microsoft Windows, including desktop and mobile versions.
|
||||
// This list is not exhaustive.
|
||||
|
||||
// NOTE: Of the synonyms listed in the spec, the only one that differs for us is macOS.
|
||||
// Further, we are allowed to handle synonyms for SerenityOS.
|
||||
if (requested_platform_name == "mac"sv && required_platform_name == "macos"sv)
|
||||
return true;
|
||||
if (requested_platform_name == "serenity"sv && required_platform_name == "serenityos"sv)
|
||||
return true;
|
||||
return false;
|
||||
}
|
||||
|
||||
// https://w3c.github.io/webdriver/#dfn-matching-capabilities
|
||||
static JsonValue match_capabilities(JsonObject const& capabilities)
|
||||
{
|
||||
static auto browser_name = StringView { BROWSER_NAME, strlen(BROWSER_NAME) }.to_lowercase_string();
|
||||
static auto platform_name = StringView { OS_STRING, strlen(OS_STRING) }.to_lowercase_string();
|
||||
|
||||
// 1. Let matched capabilities be a JSON Object with the following entries:
|
||||
JsonObject matched_capabilities;
|
||||
// "browserName"
|
||||
// ASCII Lowercase name of the user agent as a string.
|
||||
matched_capabilities.set("browserName"sv, browser_name);
|
||||
// "browserVersion"
|
||||
// The user agent version, as a string.
|
||||
matched_capabilities.set("browserVersion"sv, BROWSER_VERSION);
|
||||
// "platformName"
|
||||
// ASCII Lowercase name of the current platform as a string.
|
||||
matched_capabilities.set("platformName"sv, platform_name);
|
||||
// "acceptInsecureCerts"
|
||||
// Boolean initially set to false, indicating the session will not implicitly trust untrusted or self-signed TLS certificates on navigation.
|
||||
matched_capabilities.set("acceptInsecureCerts"sv, false);
|
||||
// "strictFileInteractability"
|
||||
// Boolean initially set to false, indicating that interactability checks will be applied to <input type=file>.
|
||||
matched_capabilities.set("strictFileInteractability"sv, false);
|
||||
// "setWindowRect"
|
||||
// Boolean indicating whether the remote end supports all of the resizing and positioning commands.
|
||||
matched_capabilities.set("setWindowRect"sv, true);
|
||||
|
||||
// 2. Optionally add extension capabilities as entries to matched capabilities. The values of these may be elided, and there is no requirement that all extension capabilities be added.
|
||||
matched_capabilities.set("serenity:ladybird"sv, default_ladybird_options());
|
||||
|
||||
// 3. For each name and value corresponding to capability’s own properties:
|
||||
auto result = capabilities.try_for_each_member([&](auto const& name, auto const& value) -> ErrorOr<void> {
|
||||
// a. Let match value equal value.
|
||||
|
||||
// b. Run the substeps of the first matching name:
|
||||
// -> "browserName"
|
||||
if (name == "browserName"sv) {
|
||||
// If value is not a string equal to the "browserName" entry in matched capabilities, return success with data null.
|
||||
if (value.as_string() != matched_capabilities.get_byte_string(name).value())
|
||||
return AK::Error::from_string_literal("browserName");
|
||||
}
|
||||
// -> "browserVersion"
|
||||
else if (name == "browserVersion"sv) {
|
||||
// Compare value to the "browserVersion" entry in matched capabilities using an implementation-defined comparison algorithm. The comparison is to accept a value that places constraints on the version using the "<", "<=", ">", and ">=" operators.
|
||||
// If the two values do not match, return success with data null.
|
||||
if (!matches_browser_version(value.as_string(), matched_capabilities.get_byte_string(name).value()))
|
||||
return AK::Error::from_string_literal("browserVersion");
|
||||
}
|
||||
// -> "platformName"
|
||||
else if (name == "platformName"sv) {
|
||||
// If value is not a string equal to the "platformName" entry in matched capabilities, return success with data null.
|
||||
if (!matches_platform_name(value.as_string(), matched_capabilities.get_byte_string(name).value()))
|
||||
return AK::Error::from_string_literal("platformName");
|
||||
}
|
||||
// -> "acceptInsecureCerts"
|
||||
else if (name == "acceptInsecureCerts"sv) {
|
||||
// If value is true and the endpoint node does not support insecure TLS certificates, return success with data null.
|
||||
if (value.as_bool())
|
||||
return AK::Error::from_string_literal("acceptInsecureCerts");
|
||||
}
|
||||
// -> "proxy"
|
||||
else if (name == "proxy"sv) {
|
||||
// FIXME: If the endpoint node does not allow the proxy it uses to be configured, or if the proxy configuration defined in value is not one that passes the endpoint node’s implementation-specific validity checks, return success with data null.
|
||||
}
|
||||
// -> Otherwise
|
||||
else {
|
||||
// FIXME: If name is the name of an additional WebDriver capability which defines a matched capability serialization algorithm, let match value be the result of running the matched capability serialization algorithm for capability name with argument value.
|
||||
// FIXME: Otherwise, if name is the key of an extension capability, let match value be the result of trying implementation-specific steps to match on name with value. If the match is not successful, return success with data null.
|
||||
|
||||
// https://w3c.github.io/webdriver-bidi/#type-session-CapabilityRequest
|
||||
if (name == "webSocketUrl"sv) {
|
||||
// 1. If value is false, return success with data null.
|
||||
if (!value.as_bool())
|
||||
return AK::Error::from_string_literal("webSocketUrl");
|
||||
|
||||
// 2. Return success with data value.
|
||||
// FIXME: Remove this when we support BIDI communication.
|
||||
return AK::Error::from_string_literal("webSocketUrl");
|
||||
}
|
||||
}
|
||||
|
||||
// c. Set a property on matched capabilities with name name and value match value.
|
||||
matched_capabilities.set(name, value);
|
||||
return {};
|
||||
});
|
||||
|
||||
if (result.is_error()) {
|
||||
dbgln_if(WEBDRIVER_DEBUG, "Failed to match capability: {}", result.error());
|
||||
return JsonValue {};
|
||||
}
|
||||
|
||||
// 4. Return success with data matched capabilities.
|
||||
return matched_capabilities;
|
||||
}
|
||||
|
||||
// https://w3c.github.io/webdriver/#dfn-capabilities-processing
|
||||
Response process_capabilities(JsonValue const& parameters)
|
||||
{
|
||||
if (!parameters.is_object())
|
||||
return Error::from_code(ErrorCode::InvalidArgument, "Session parameters is not an object"sv);
|
||||
|
||||
// 1. Let capabilities request be the result of getting the property "capabilities" from parameters.
|
||||
// a. If capabilities request is not a JSON Object, return error with error code invalid argument.
|
||||
auto maybe_capabilities_request = parameters.as_object().get_object("capabilities"sv);
|
||||
if (!maybe_capabilities_request.has_value())
|
||||
return Error::from_code(ErrorCode::InvalidArgument, "Capabilities is not an object"sv);
|
||||
|
||||
auto const& capabilities_request = maybe_capabilities_request.value();
|
||||
|
||||
// 2. Let required capabilities be the result of getting the property "alwaysMatch" from capabilities request.
|
||||
// a. If required capabilities is undefined, set the value to an empty JSON Object.
|
||||
JsonObject required_capabilities;
|
||||
|
||||
if (auto capability = capabilities_request.get("alwaysMatch"sv); capability.has_value()) {
|
||||
// b. Let required capabilities be the result of trying to validate capabilities with argument required capabilities.
|
||||
required_capabilities = TRY(validate_capabilities(*capability));
|
||||
}
|
||||
|
||||
// 3. Let all first match capabilities be the result of getting the property "firstMatch" from capabilities request.
|
||||
JsonArray all_first_match_capabilities;
|
||||
|
||||
if (auto capabilities = capabilities_request.get("firstMatch"sv); capabilities.has_value()) {
|
||||
// b. If all first match capabilities is not a JSON List with one or more entries, return error with error code invalid argument.
|
||||
if (!capabilities->is_array() || capabilities->as_array().is_empty())
|
||||
return Error::from_code(ErrorCode::InvalidArgument, "Capability firstMatch must be an array with at least one entry"sv);
|
||||
|
||||
all_first_match_capabilities = capabilities->as_array();
|
||||
} else {
|
||||
// a. If all first match capabilities is undefined, set the value to a JSON List with a single entry of an empty JSON Object.
|
||||
all_first_match_capabilities.must_append(JsonObject {});
|
||||
}
|
||||
|
||||
// 4. Let validated first match capabilities be an empty JSON List.
|
||||
JsonArray validated_first_match_capabilities;
|
||||
validated_first_match_capabilities.ensure_capacity(all_first_match_capabilities.size());
|
||||
|
||||
// 5. For each first match capabilities corresponding to an indexed property in all first match capabilities:
|
||||
TRY(all_first_match_capabilities.try_for_each([&](auto const& first_match_capabilities) -> ErrorOr<void, Error> {
|
||||
// a. Let validated capabilities be the result of trying to validate capabilities with argument first match capabilities.
|
||||
auto validated_capabilities = TRY(validate_capabilities(first_match_capabilities));
|
||||
|
||||
// b. Append validated capabilities to validated first match capabilities.
|
||||
validated_first_match_capabilities.must_append(move(validated_capabilities));
|
||||
return {};
|
||||
}));
|
||||
|
||||
// 6. Let merged capabilities be an empty List.
|
||||
JsonArray merged_capabilities;
|
||||
merged_capabilities.ensure_capacity(validated_first_match_capabilities.size());
|
||||
|
||||
// 7. For each first match capabilities corresponding to an indexed property in validated first match capabilities:
|
||||
TRY(validated_first_match_capabilities.try_for_each([&](auto const& first_match_capabilities) -> ErrorOr<void, Error> {
|
||||
// a. Let merged be the result of trying to merge capabilities with required capabilities and first match capabilities as arguments.
|
||||
auto merged = TRY(merge_capabilities(required_capabilities, first_match_capabilities.as_object()));
|
||||
|
||||
// b. Append merged to merged capabilities.
|
||||
merged_capabilities.must_append(move(merged));
|
||||
return {};
|
||||
}));
|
||||
|
||||
// 8. For each capabilities corresponding to an indexed property in merged capabilities:
|
||||
for (auto const& capabilities : merged_capabilities.values()) {
|
||||
// a. Let matched capabilities be the result of trying to match capabilities with capabilities as an argument.
|
||||
auto matched_capabilities = match_capabilities(capabilities.as_object());
|
||||
|
||||
// b. If matched capabilities is not null, return success with data matched capabilities.
|
||||
if (!matched_capabilities.is_null())
|
||||
return matched_capabilities;
|
||||
}
|
||||
|
||||
// 9. Return success with data null.
|
||||
return JsonValue {};
|
||||
}
|
||||
|
||||
LadybirdOptions::LadybirdOptions(JsonObject const& capabilities)
|
||||
{
|
||||
auto options = capabilities.get_object("serenity:ladybird"sv);
|
||||
if (!options.has_value())
|
||||
return;
|
||||
|
||||
auto headless = options->get_bool("headless"sv);
|
||||
if (headless.has_value())
|
||||
this->headless = headless.value();
|
||||
}
|
||||
|
||||
}
|
71
Libraries/LibWeb/WebDriver/Capabilities.h
Normal file
71
Libraries/LibWeb/WebDriver/Capabilities.h
Normal file
|
@ -0,0 +1,71 @@
|
|||
/*
|
||||
* Copyright (c) 2022, Tim Flynn <trflynn89@serenityos.org>
|
||||
*
|
||||
* SPDX-License-Identifier: BSD-2-Clause
|
||||
*/
|
||||
|
||||
#pragma once
|
||||
|
||||
#include <AK/Forward.h>
|
||||
#include <AK/StringView.h>
|
||||
#include <LibWeb/WebDriver/Response.h>
|
||||
|
||||
namespace Web::WebDriver {
|
||||
|
||||
// https://w3c.github.io/webdriver/#dfn-page-load-strategy
|
||||
enum class PageLoadStrategy {
|
||||
None,
|
||||
Eager,
|
||||
Normal,
|
||||
};
|
||||
|
||||
constexpr PageLoadStrategy page_load_strategy_from_string(StringView strategy)
|
||||
{
|
||||
if (strategy == "none"sv)
|
||||
return PageLoadStrategy::None;
|
||||
if (strategy == "eager"sv)
|
||||
return PageLoadStrategy::Eager;
|
||||
if (strategy == "normal"sv)
|
||||
return PageLoadStrategy::Normal;
|
||||
VERIFY_NOT_REACHED();
|
||||
}
|
||||
|
||||
// https://w3c.github.io/webdriver/#dfn-unhandled-prompt-behavior
|
||||
enum class UnhandledPromptBehavior {
|
||||
Dismiss,
|
||||
Accept,
|
||||
DismissAndNotify,
|
||||
AcceptAndNotify,
|
||||
Ignore,
|
||||
};
|
||||
|
||||
constexpr UnhandledPromptBehavior unhandled_prompt_behavior_from_string(StringView behavior)
|
||||
{
|
||||
if (behavior == "dismiss"sv)
|
||||
return UnhandledPromptBehavior::Dismiss;
|
||||
if (behavior == "accept"sv)
|
||||
return UnhandledPromptBehavior::Accept;
|
||||
if (behavior == "dismiss and notify"sv)
|
||||
return UnhandledPromptBehavior::DismissAndNotify;
|
||||
if (behavior == "accept and notify"sv)
|
||||
return UnhandledPromptBehavior::AcceptAndNotify;
|
||||
if (behavior == "ignore"sv)
|
||||
return UnhandledPromptBehavior::Ignore;
|
||||
VERIFY_NOT_REACHED();
|
||||
}
|
||||
|
||||
enum class InterfaceMode {
|
||||
Graphical,
|
||||
Headless,
|
||||
};
|
||||
void set_default_interface_mode(InterfaceMode);
|
||||
|
||||
struct LadybirdOptions {
|
||||
explicit LadybirdOptions(JsonObject const& capabilities);
|
||||
|
||||
bool headless { false };
|
||||
};
|
||||
|
||||
Response process_capabilities(JsonValue const& parameters);
|
||||
|
||||
}
|
362
Libraries/LibWeb/WebDriver/Client.cpp
Normal file
362
Libraries/LibWeb/WebDriver/Client.cpp
Normal file
|
@ -0,0 +1,362 @@
|
|||
/*
|
||||
* Copyright (c) 2022, Florent Castelli <florent.castelli@gmail.com>
|
||||
* Copyright (c) 2022, Sam Atkins <atkinssj@serenityos.org>
|
||||
* Copyright (c) 2022, Tobias Christiansen <tobyase@serenityos.org>
|
||||
* Copyright (c) 2022, Linus Groh <linusg@serenityos.org>
|
||||
* Copyright (c) 2022-2024, Tim Flynn <trflynn89@ladybird.org>
|
||||
*
|
||||
* SPDX-License-Identifier: BSD-2-Clause
|
||||
*/
|
||||
|
||||
#include <AK/ByteBuffer.h>
|
||||
#include <AK/Debug.h>
|
||||
#include <AK/Format.h>
|
||||
#include <AK/JsonObject.h>
|
||||
#include <AK/JsonParser.h>
|
||||
#include <AK/JsonValue.h>
|
||||
#include <AK/Span.h>
|
||||
#include <AK/StringBuilder.h>
|
||||
#include <AK/StringView.h>
|
||||
#include <LibCore/DateTime.h>
|
||||
#include <LibHTTP/HttpResponse.h>
|
||||
#include <LibWeb/WebDriver/Client.h>
|
||||
|
||||
namespace Web::WebDriver {
|
||||
|
||||
using RouteHandler = Response (*)(Client&, Parameters, JsonValue);
|
||||
|
||||
struct Route {
|
||||
HTTP::HttpRequest::Method method {};
|
||||
StringView path;
|
||||
RouteHandler handler { nullptr };
|
||||
};
|
||||
|
||||
struct MatchedRoute {
|
||||
RouteHandler handler;
|
||||
Vector<String> parameters;
|
||||
};
|
||||
|
||||
#define ROUTE(method, path, handler) \
|
||||
Route \
|
||||
{ \
|
||||
HTTP::HttpRequest::method, \
|
||||
path, \
|
||||
[](auto& client, auto parameters, auto payload) { \
|
||||
return client.handler(parameters, move(payload)); \
|
||||
} \
|
||||
}
|
||||
|
||||
// https://w3c.github.io/webdriver/#dfn-endpoints
|
||||
static constexpr auto s_webdriver_endpoints = Array {
|
||||
ROUTE(POST, "/session"sv, new_session),
|
||||
ROUTE(DELETE, "/session/:session_id"sv, delete_session),
|
||||
ROUTE(GET, "/status"sv, get_status),
|
||||
ROUTE(GET, "/session/:session_id/timeouts"sv, get_timeouts),
|
||||
ROUTE(POST, "/session/:session_id/timeouts"sv, set_timeouts),
|
||||
ROUTE(POST, "/session/:session_id/url"sv, navigate_to),
|
||||
ROUTE(GET, "/session/:session_id/url"sv, get_current_url),
|
||||
ROUTE(POST, "/session/:session_id/back"sv, back),
|
||||
ROUTE(POST, "/session/:session_id/forward"sv, forward),
|
||||
ROUTE(POST, "/session/:session_id/refresh"sv, refresh),
|
||||
ROUTE(GET, "/session/:session_id/title"sv, get_title),
|
||||
ROUTE(GET, "/session/:session_id/window"sv, get_window_handle),
|
||||
ROUTE(DELETE, "/session/:session_id/window"sv, close_window),
|
||||
ROUTE(POST, "/session/:session_id/window"sv, switch_to_window),
|
||||
ROUTE(GET, "/session/:session_id/window/handles"sv, get_window_handles),
|
||||
ROUTE(POST, "/session/:session_id/window/new"sv, new_window),
|
||||
ROUTE(POST, "/session/:session_id/frame"sv, switch_to_frame),
|
||||
ROUTE(POST, "/session/:session_id/frame/parent"sv, switch_to_parent_frame),
|
||||
ROUTE(GET, "/session/:session_id/window/rect"sv, get_window_rect),
|
||||
ROUTE(POST, "/session/:session_id/window/rect"sv, set_window_rect),
|
||||
ROUTE(POST, "/session/:session_id/window/maximize"sv, maximize_window),
|
||||
ROUTE(POST, "/session/:session_id/window/minimize"sv, minimize_window),
|
||||
ROUTE(POST, "/session/:session_id/window/fullscreen"sv, fullscreen_window),
|
||||
ROUTE(POST, "/session/:session_id/window/consume-user-activation"sv, consume_user_activation),
|
||||
ROUTE(POST, "/session/:session_id/element"sv, find_element),
|
||||
ROUTE(POST, "/session/:session_id/elements"sv, find_elements),
|
||||
ROUTE(POST, "/session/:session_id/element/:element_id/element"sv, find_element_from_element),
|
||||
ROUTE(POST, "/session/:session_id/element/:element_id/elements"sv, find_elements_from_element),
|
||||
ROUTE(POST, "/session/:session_id/shadow/:shadow_id/element"sv, find_element_from_shadow_root),
|
||||
ROUTE(POST, "/session/:session_id/shadow/:shadow_id/elements"sv, find_elements_from_shadow_root),
|
||||
ROUTE(GET, "/session/:session_id/element/active"sv, get_active_element),
|
||||
ROUTE(GET, "/session/:session_id/element/:element_id/shadow"sv, get_element_shadow_root),
|
||||
ROUTE(GET, "/session/:session_id/element/:element_id/selected"sv, is_element_selected),
|
||||
ROUTE(GET, "/session/:session_id/element/:element_id/attribute/:name"sv, get_element_attribute),
|
||||
ROUTE(GET, "/session/:session_id/element/:element_id/property/:name"sv, get_element_property),
|
||||
ROUTE(GET, "/session/:session_id/element/:element_id/css/:name"sv, get_element_css_value),
|
||||
ROUTE(GET, "/session/:session_id/element/:element_id/text"sv, get_element_text),
|
||||
ROUTE(GET, "/session/:session_id/element/:element_id/name"sv, get_element_tag_name),
|
||||
ROUTE(GET, "/session/:session_id/element/:element_id/rect"sv, get_element_rect),
|
||||
ROUTE(GET, "/session/:session_id/element/:element_id/enabled"sv, is_element_enabled),
|
||||
ROUTE(GET, "/session/:session_id/element/:element_id/computedrole"sv, get_computed_role),
|
||||
ROUTE(GET, "/session/:session_id/element/:element_id/computedlabel"sv, get_computed_label),
|
||||
ROUTE(POST, "/session/:session_id/element/:element_id/click"sv, element_click),
|
||||
ROUTE(POST, "/session/:session_id/element/:element_id/clear"sv, element_clear),
|
||||
ROUTE(POST, "/session/:session_id/element/:element_id/value"sv, element_send_keys),
|
||||
ROUTE(GET, "/session/:session_id/source"sv, get_source),
|
||||
ROUTE(POST, "/session/:session_id/execute/sync"sv, execute_script),
|
||||
ROUTE(POST, "/session/:session_id/execute/async"sv, execute_async_script),
|
||||
ROUTE(GET, "/session/:session_id/cookie"sv, get_all_cookies),
|
||||
ROUTE(GET, "/session/:session_id/cookie/:name"sv, get_named_cookie),
|
||||
ROUTE(POST, "/session/:session_id/cookie"sv, add_cookie),
|
||||
ROUTE(DELETE, "/session/:session_id/cookie/:name"sv, delete_cookie),
|
||||
ROUTE(DELETE, "/session/:session_id/cookie"sv, delete_all_cookies),
|
||||
ROUTE(POST, "/session/:session_id/actions"sv, perform_actions),
|
||||
ROUTE(DELETE, "/session/:session_id/actions"sv, release_actions),
|
||||
ROUTE(POST, "/session/:session_id/alert/dismiss"sv, dismiss_alert),
|
||||
ROUTE(POST, "/session/:session_id/alert/accept"sv, accept_alert),
|
||||
ROUTE(GET, "/session/:session_id/alert/text"sv, get_alert_text),
|
||||
ROUTE(POST, "/session/:session_id/alert/text"sv, send_alert_text),
|
||||
ROUTE(GET, "/session/:session_id/screenshot"sv, take_screenshot),
|
||||
ROUTE(GET, "/session/:session_id/element/:element_id/screenshot"sv, take_element_screenshot),
|
||||
ROUTE(POST, "/session/:session_id/print"sv, print_page),
|
||||
};
|
||||
|
||||
// https://w3c.github.io/webdriver/#dfn-match-a-request
|
||||
static ErrorOr<MatchedRoute, Error> match_route(HTTP::HttpRequest const& request)
|
||||
{
|
||||
dbgln_if(WEBDRIVER_ROUTE_DEBUG, "match_route({}, {})", HTTP::to_string_view(request.method()), request.resource());
|
||||
|
||||
auto request_path = request.resource().view();
|
||||
Vector<String> parameters;
|
||||
|
||||
auto next_segment = [](auto& path) -> Optional<StringView> {
|
||||
if (auto index = path.find('/'); index.has_value() && (*index + 1) < path.length()) {
|
||||
path = path.substring_view(*index + 1);
|
||||
|
||||
if (index = path.find('/'); index.has_value())
|
||||
return path.substring_view(0, *index);
|
||||
return path;
|
||||
}
|
||||
|
||||
path = {};
|
||||
return {};
|
||||
};
|
||||
|
||||
for (auto const& route : s_webdriver_endpoints) {
|
||||
dbgln_if(WEBDRIVER_ROUTE_DEBUG, "- Checking {} {}", HTTP::to_string_view(route.method), route.path);
|
||||
if (route.method != request.method())
|
||||
continue;
|
||||
|
||||
auto route_path = route.path;
|
||||
Optional<bool> match;
|
||||
|
||||
auto on_failed_match = [&]() {
|
||||
request_path = request.resource();
|
||||
parameters.clear();
|
||||
match = false;
|
||||
};
|
||||
|
||||
while (!match.has_value()) {
|
||||
auto request_segment = next_segment(request_path);
|
||||
auto route_segment = next_segment(route_path);
|
||||
|
||||
if (!request_segment.has_value() && !route_segment.has_value())
|
||||
match = true;
|
||||
else if (request_segment.has_value() != route_segment.has_value())
|
||||
on_failed_match();
|
||||
else if (route_segment->starts_with(':'))
|
||||
TRY(parameters.try_append(TRY(String::from_utf8(*request_segment))));
|
||||
else if (request_segment != route_segment)
|
||||
on_failed_match();
|
||||
}
|
||||
|
||||
if (*match) {
|
||||
dbgln_if(WEBDRIVER_ROUTE_DEBUG, "- Found match with parameters={}", parameters);
|
||||
return MatchedRoute { route.handler, move(parameters) };
|
||||
}
|
||||
}
|
||||
|
||||
return Error::from_code(ErrorCode::UnknownCommand, "The command was not recognized.");
|
||||
}
|
||||
|
||||
static JsonValue make_success_response(JsonValue value)
|
||||
{
|
||||
JsonObject result;
|
||||
result.set("value", move(value));
|
||||
return result;
|
||||
}
|
||||
|
||||
Client::Client(NonnullOwnPtr<Core::BufferedTCPSocket> socket, Core::EventReceiver* parent)
|
||||
: Core::EventReceiver(parent)
|
||||
, m_socket(move(socket))
|
||||
{
|
||||
m_socket->on_ready_to_read = [this] {
|
||||
if (auto result = on_ready_to_read(); result.is_error())
|
||||
handle_error({}, result.release_error());
|
||||
};
|
||||
}
|
||||
|
||||
Client::~Client()
|
||||
{
|
||||
m_socket->close();
|
||||
}
|
||||
|
||||
void Client::die()
|
||||
{
|
||||
// We defer removing this connection to avoid closing its socket while we are inside the on_ready_to_read callback.
|
||||
deferred_invoke([this] { remove_from_parent(); });
|
||||
}
|
||||
|
||||
ErrorOr<void, Client::WrappedError> Client::on_ready_to_read()
|
||||
{
|
||||
// FIXME: All this should be moved to LibHTTP and be made spec compliant.
|
||||
auto buffer = TRY(ByteBuffer::create_uninitialized(m_socket->buffer_size()));
|
||||
|
||||
for (;;) {
|
||||
if (!TRY(m_socket->can_read_without_blocking()))
|
||||
break;
|
||||
|
||||
auto data = TRY(m_socket->read_some(buffer));
|
||||
TRY(m_remaining_request.try_append(StringView { data }));
|
||||
|
||||
if (m_socket->is_eof()) {
|
||||
die();
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
if (m_remaining_request.is_empty())
|
||||
return {};
|
||||
|
||||
auto parsed_request = HTTP::HttpRequest::from_raw_request(m_remaining_request.string_view().bytes());
|
||||
|
||||
// If the request is not complete, we need to wait for more data to arrive.
|
||||
if (parsed_request.is_error() && parsed_request.error() == HTTP::HttpRequest::ParseError::RequestIncomplete)
|
||||
return {};
|
||||
|
||||
m_remaining_request.clear();
|
||||
auto request = parsed_request.release_value();
|
||||
|
||||
deferred_invoke([this, request = move(request)]() {
|
||||
auto body = read_body_as_json(request);
|
||||
if (body.is_error()) {
|
||||
handle_error(request, body.release_error());
|
||||
return;
|
||||
}
|
||||
|
||||
if (auto result = handle_request(request, body.release_value()); result.is_error())
|
||||
handle_error(request, result.release_error());
|
||||
});
|
||||
|
||||
return {};
|
||||
}
|
||||
|
||||
ErrorOr<JsonValue, Client::WrappedError> Client::read_body_as_json(HTTP::HttpRequest const& request)
|
||||
{
|
||||
// FIXME: If we received a multipart body here, this would fail badly.
|
||||
// FIXME: Check the Content-Type is actually application/json.
|
||||
size_t content_length = 0;
|
||||
|
||||
for (auto const& header : request.headers().headers()) {
|
||||
if (header.name.equals_ignoring_ascii_case("Content-Length"sv)) {
|
||||
content_length = header.value.to_number<size_t>(TrimWhitespace::Yes).value_or(0);
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
if (content_length == 0)
|
||||
return JsonValue {};
|
||||
|
||||
JsonParser json_parser(request.body());
|
||||
return TRY(json_parser.parse());
|
||||
}
|
||||
|
||||
ErrorOr<void, Client::WrappedError> Client::handle_request(HTTP::HttpRequest const& request, JsonValue body)
|
||||
{
|
||||
if constexpr (WEBDRIVER_DEBUG) {
|
||||
dbgln("Got HTTP request: {} {}", request.method_name(), request.resource());
|
||||
dbgln("Body: {}", body);
|
||||
}
|
||||
|
||||
auto [handler, parameters] = TRY(match_route(request));
|
||||
auto result = TRY((*handler)(*this, move(parameters), move(body)));
|
||||
return send_success_response(request, move(result));
|
||||
}
|
||||
|
||||
void Client::handle_error(HTTP::HttpRequest const& request, WrappedError const& error)
|
||||
{
|
||||
error.visit(
|
||||
[](AK::Error const& error) {
|
||||
warnln("Internal error: {}", error);
|
||||
},
|
||||
[](HTTP::HttpRequest::ParseError const& error) {
|
||||
warnln("HTTP request parsing error: {}", HTTP::HttpRequest::parse_error_to_string(error));
|
||||
},
|
||||
[&](WebDriver::Error const& error) {
|
||||
if (send_error_response(request, error).is_error())
|
||||
warnln("Could not send error response");
|
||||
});
|
||||
|
||||
die();
|
||||
}
|
||||
|
||||
ErrorOr<void, Client::WrappedError> Client::send_success_response(HTTP::HttpRequest const& request, JsonValue result)
|
||||
{
|
||||
bool keep_alive = false;
|
||||
if (auto it = request.headers().headers().find_if([](auto& header) { return header.name.equals_ignoring_ascii_case("Connection"sv); }); !it.is_end())
|
||||
keep_alive = it->value.trim_whitespace().equals_ignoring_ascii_case("keep-alive"sv);
|
||||
|
||||
result = make_success_response(move(result));
|
||||
auto content = result.serialized<StringBuilder>();
|
||||
|
||||
StringBuilder builder;
|
||||
builder.append("HTTP/1.1 200 OK\r\n"sv);
|
||||
builder.append("Server: WebDriver (SerenityOS)\r\n"sv);
|
||||
builder.append("X-Frame-Options: SAMEORIGIN\r\n"sv);
|
||||
builder.append("X-Content-Type-Options: nosniff\r\n"sv);
|
||||
if (keep_alive)
|
||||
builder.append("Connection: keep-alive\r\n"sv);
|
||||
builder.append("Cache-Control: no-cache\r\n"sv);
|
||||
builder.append("Content-Type: application/json; charset=utf-8\r\n"sv);
|
||||
builder.appendff("Content-Length: {}\r\n", content.length());
|
||||
builder.append("\r\n"sv);
|
||||
builder.append(content);
|
||||
|
||||
TRY(m_socket->write_until_depleted(builder.string_view()));
|
||||
|
||||
if (!keep_alive)
|
||||
die();
|
||||
|
||||
log_response(request, 200);
|
||||
return {};
|
||||
}
|
||||
|
||||
ErrorOr<void, Client::WrappedError> Client::send_error_response(HTTP::HttpRequest const& request, Error const& error)
|
||||
{
|
||||
// FIXME: Implement to spec.
|
||||
dbgln_if(WEBDRIVER_DEBUG, "Sending error response: {} {}: {}", error.http_status, error.error, error.message);
|
||||
auto reason = HTTP::HttpResponse::reason_phrase_for_code(error.http_status);
|
||||
|
||||
JsonObject error_response;
|
||||
error_response.set("error", error.error);
|
||||
error_response.set("message", error.message);
|
||||
error_response.set("stacktrace", "");
|
||||
if (error.data.has_value())
|
||||
error_response.set("data", *error.data);
|
||||
|
||||
JsonObject result;
|
||||
result.set("value", move(error_response));
|
||||
|
||||
auto content = result.serialized<StringBuilder>();
|
||||
|
||||
StringBuilder builder;
|
||||
builder.appendff("HTTP/1.1 {} {}\r\n", error.http_status, reason);
|
||||
builder.append("Cache-Control: no-cache\r\n"sv);
|
||||
builder.append("Content-Type: application/json; charset=utf-8\r\n"sv);
|
||||
builder.appendff("Content-Length: {}\r\n", content.length());
|
||||
builder.append("\r\n"sv);
|
||||
builder.append(content);
|
||||
|
||||
TRY(m_socket->write_until_depleted(builder.string_view()));
|
||||
|
||||
log_response(request, error.http_status);
|
||||
return {};
|
||||
}
|
||||
|
||||
void Client::log_response(HTTP::HttpRequest const& request, unsigned code)
|
||||
{
|
||||
outln("{} :: {:03d} :: {} {}", Core::DateTime::now().to_byte_string(), code, request.method_name(), request.resource());
|
||||
}
|
||||
|
||||
}
|
141
Libraries/LibWeb/WebDriver/Client.h
Normal file
141
Libraries/LibWeb/WebDriver/Client.h
Normal file
|
@ -0,0 +1,141 @@
|
|||
/*
|
||||
* Copyright (c) 2022, Florent Castelli <florent.castelli@gmail.com>
|
||||
* Copyright (c) 2022, Linus Groh <linusg@serenityos.org>
|
||||
* Copyright (c) 2022-2024, Tim Flynn <trflynn89@ladybird.org>
|
||||
*
|
||||
* SPDX-License-Identifier: BSD-2-Clause
|
||||
*/
|
||||
|
||||
#pragma once
|
||||
|
||||
#include <AK/Error.h>
|
||||
#include <AK/NonnullOwnPtr.h>
|
||||
#include <AK/String.h>
|
||||
#include <AK/Variant.h>
|
||||
#include <AK/Vector.h>
|
||||
#include <LibCore/EventReceiver.h>
|
||||
#include <LibCore/Socket.h>
|
||||
#include <LibHTTP/Forward.h>
|
||||
#include <LibHTTP/HttpRequest.h>
|
||||
#include <LibWeb/WebDriver/Error.h>
|
||||
#include <LibWeb/WebDriver/Response.h>
|
||||
|
||||
namespace Web::WebDriver {
|
||||
|
||||
using Parameters = Vector<String>;
|
||||
|
||||
class Client : public Core::EventReceiver {
|
||||
C_OBJECT_ABSTRACT(Client);
|
||||
|
||||
public:
|
||||
virtual ~Client();
|
||||
|
||||
// 8. Sessions, https://w3c.github.io/webdriver/#sessions
|
||||
virtual Response new_session(Parameters parameters, JsonValue payload) = 0;
|
||||
virtual Response delete_session(Parameters parameters, JsonValue payload) = 0;
|
||||
virtual Response get_status(Parameters parameters, JsonValue payload) = 0;
|
||||
|
||||
// 9. Timeouts, https://w3c.github.io/webdriver/#timeouts
|
||||
virtual Response get_timeouts(Parameters parameters, JsonValue payload) = 0;
|
||||
virtual Response set_timeouts(Parameters parameters, JsonValue payload) = 0;
|
||||
|
||||
// 10. Navigation, https://w3c.github.io/webdriver/#navigation
|
||||
virtual Response navigate_to(Parameters parameters, JsonValue payload) = 0;
|
||||
virtual Response get_current_url(Parameters parameters, JsonValue payload) = 0;
|
||||
virtual Response back(Parameters parameters, JsonValue payload) = 0;
|
||||
virtual Response forward(Parameters parameters, JsonValue payload) = 0;
|
||||
virtual Response refresh(Parameters parameters, JsonValue payload) = 0;
|
||||
virtual Response get_title(Parameters parameters, JsonValue payload) = 0;
|
||||
|
||||
// 11. Contexts, https://w3c.github.io/webdriver/#contexts
|
||||
virtual Response get_window_handle(Parameters parameters, JsonValue payload) = 0;
|
||||
virtual Response close_window(Parameters parameters, JsonValue payload) = 0;
|
||||
virtual Response new_window(Parameters parameters, JsonValue payload) = 0;
|
||||
virtual Response switch_to_window(Parameters parameters, JsonValue payload) = 0;
|
||||
virtual Response get_window_handles(Parameters parameters, JsonValue payload) = 0;
|
||||
virtual Response get_window_rect(Parameters parameters, JsonValue payload) = 0;
|
||||
virtual Response set_window_rect(Parameters parameters, JsonValue payload) = 0;
|
||||
virtual Response maximize_window(Parameters parameters, JsonValue payload) = 0;
|
||||
virtual Response minimize_window(Parameters parameters, JsonValue payload) = 0;
|
||||
virtual Response fullscreen_window(Parameters parameters, JsonValue payload) = 0;
|
||||
virtual Response switch_to_frame(Parameters parameters, JsonValue payload) = 0;
|
||||
virtual Response switch_to_parent_frame(Parameters parameters, JsonValue payload) = 0;
|
||||
|
||||
// Extension: https://html.spec.whatwg.org/multipage/interaction.html#user-activation-user-agent-automation
|
||||
virtual Response consume_user_activation(Parameters parameters, JsonValue payload) = 0;
|
||||
|
||||
// 12. Elements, https://w3c.github.io/webdriver/#elements
|
||||
virtual Response find_element(Parameters parameters, JsonValue payload) = 0;
|
||||
virtual Response find_elements(Parameters parameters, JsonValue payload) = 0;
|
||||
virtual Response find_element_from_element(Parameters parameters, JsonValue payload) = 0;
|
||||
virtual Response find_elements_from_element(Parameters parameters, JsonValue payload) = 0;
|
||||
virtual Response find_element_from_shadow_root(Parameters parameters, JsonValue payload) = 0;
|
||||
virtual Response find_elements_from_shadow_root(Parameters parameters, JsonValue payload) = 0;
|
||||
virtual Response get_active_element(Parameters parameters, JsonValue payload) = 0;
|
||||
virtual Response get_element_shadow_root(Parameters parameters, JsonValue payload) = 0;
|
||||
virtual Response is_element_selected(Parameters parameters, JsonValue payload) = 0;
|
||||
virtual Response get_element_attribute(Parameters parameters, JsonValue payload) = 0;
|
||||
virtual Response get_element_property(Parameters parameters, JsonValue payload) = 0;
|
||||
virtual Response get_element_css_value(Parameters parameters, JsonValue payload) = 0;
|
||||
virtual Response get_element_text(Parameters parameters, JsonValue payload) = 0;
|
||||
virtual Response get_element_tag_name(Parameters parameters, JsonValue payload) = 0;
|
||||
virtual Response get_element_rect(Parameters parameters, JsonValue payload) = 0;
|
||||
virtual Response is_element_enabled(Parameters parameters, JsonValue payload) = 0;
|
||||
virtual Response get_computed_role(Parameters parameters, JsonValue payload) = 0;
|
||||
virtual Response get_computed_label(Parameters parameters, JsonValue payload) = 0;
|
||||
virtual Response element_click(Parameters parameters, JsonValue payload) = 0;
|
||||
virtual Response element_clear(Parameters parameters, JsonValue payload) = 0;
|
||||
virtual Response element_send_keys(Parameters parameters, JsonValue payload) = 0;
|
||||
|
||||
// 13. Document, https://w3c.github.io/webdriver/#document
|
||||
virtual Response get_source(Parameters parameters, JsonValue payload) = 0;
|
||||
virtual Response execute_script(Parameters parameters, JsonValue payload) = 0;
|
||||
virtual Response execute_async_script(Parameters parameters, JsonValue payload) = 0;
|
||||
|
||||
// 14. Cookies, https://w3c.github.io/webdriver/#cookies
|
||||
virtual Response get_all_cookies(Parameters parameters, JsonValue payload) = 0;
|
||||
virtual Response get_named_cookie(Parameters parameters, JsonValue payload) = 0;
|
||||
virtual Response add_cookie(Parameters parameters, JsonValue payload) = 0;
|
||||
virtual Response delete_cookie(Parameters parameters, JsonValue payload) = 0;
|
||||
virtual Response delete_all_cookies(Parameters parameters, JsonValue payload) = 0;
|
||||
|
||||
// 15. Actions, https://w3c.github.io/webdriver/#actions
|
||||
virtual Response perform_actions(Parameters parameters, JsonValue payload) = 0;
|
||||
virtual Response release_actions(Parameters parameters, JsonValue payload) = 0;
|
||||
|
||||
// 16. User prompts, https://w3c.github.io/webdriver/#user-prompts
|
||||
virtual Response dismiss_alert(Parameters parameters, JsonValue payload) = 0;
|
||||
virtual Response accept_alert(Parameters parameters, JsonValue payload) = 0;
|
||||
virtual Response get_alert_text(Parameters parameters, JsonValue payload) = 0;
|
||||
virtual Response send_alert_text(Parameters parameters, JsonValue payload) = 0;
|
||||
|
||||
// 17. Screen capture, https://w3c.github.io/webdriver/#screen-capture
|
||||
virtual Response take_screenshot(Parameters parameters, JsonValue payload) = 0;
|
||||
virtual Response take_element_screenshot(Parameters parameters, JsonValue payload) = 0;
|
||||
|
||||
// 18. Print, https://w3c.github.io/webdriver/#print
|
||||
virtual Response print_page(Parameters parameters, JsonValue payload) = 0;
|
||||
|
||||
protected:
|
||||
Client(NonnullOwnPtr<Core::BufferedTCPSocket>, Core::EventReceiver* parent);
|
||||
|
||||
private:
|
||||
using WrappedError = Variant<AK::Error, HTTP::HttpRequest::ParseError, WebDriver::Error>;
|
||||
|
||||
void die();
|
||||
|
||||
ErrorOr<void, WrappedError> on_ready_to_read();
|
||||
static ErrorOr<JsonValue, WrappedError> read_body_as_json(HTTP::HttpRequest const&);
|
||||
|
||||
ErrorOr<void, WrappedError> handle_request(HTTP::HttpRequest const&, JsonValue body);
|
||||
void handle_error(HTTP::HttpRequest const&, WrappedError const&);
|
||||
|
||||
ErrorOr<void, WrappedError> send_success_response(HTTP::HttpRequest const&, JsonValue result);
|
||||
ErrorOr<void, WrappedError> send_error_response(HTTP::HttpRequest const&, Error const& error);
|
||||
static void log_response(HTTP::HttpRequest const&, unsigned code);
|
||||
|
||||
NonnullOwnPtr<Core::BufferedTCPSocket> m_socket;
|
||||
StringBuilder m_remaining_request;
|
||||
};
|
||||
|
||||
}
|
130
Libraries/LibWeb/WebDriver/Contexts.cpp
Normal file
130
Libraries/LibWeb/WebDriver/Contexts.cpp
Normal file
|
@ -0,0 +1,130 @@
|
|||
/*
|
||||
* Copyright (c) 2023, Linus Groh <linusg@serenityos.org>
|
||||
*
|
||||
* SPDX-License-Identifier: BSD-2-Clause
|
||||
*/
|
||||
|
||||
#include <AK/JsonObject.h>
|
||||
#include <LibWeb/DOM/Document.h>
|
||||
#include <LibWeb/HTML/BrowsingContext.h>
|
||||
#include <LibWeb/HTML/TraversableNavigable.h>
|
||||
#include <LibWeb/HTML/WindowProxy.h>
|
||||
#include <LibWeb/WebDriver/Contexts.h>
|
||||
|
||||
namespace Web::WebDriver {
|
||||
|
||||
// https://w3c.github.io/webdriver/#dfn-web-window-identifier
|
||||
static ByteString const WEB_WINDOW_IDENTIFIER = "window-fcc6-11e5-b4f8-330a88ab9d7f"sv;
|
||||
|
||||
// https://w3c.github.io/webdriver/#dfn-web-frame-identifier
|
||||
static ByteString const WEB_FRAME_IDENTIFIER = "frame-075b-4da1-b6ba-e579c2d3230a"sv;
|
||||
|
||||
// https://w3c.github.io/webdriver/#dfn-windowproxy-reference-object
|
||||
JsonObject window_proxy_reference_object(HTML::WindowProxy const& window)
|
||||
{
|
||||
// 1. Let identifier be the web window identifier if the associated browsing context of window is a top-level browsing context.
|
||||
// Otherwise let it be the web frame identifier.
|
||||
|
||||
// NOTE: We look at the active browsing context's active document's node navigable instead.
|
||||
// Because a Browsing context's top-level traversable is this navigable's top level traversable.
|
||||
// Ref: https://html.spec.whatwg.org/multipage/document-sequences.html#bc-traversable
|
||||
auto navigable = window.associated_browsing_context()->active_document()->navigable();
|
||||
|
||||
auto identifier = navigable->is_top_level_traversable()
|
||||
? WEB_WINDOW_IDENTIFIER
|
||||
: WEB_FRAME_IDENTIFIER;
|
||||
|
||||
// 2. Return a JSON Object initialized with the following properties:
|
||||
JsonObject object;
|
||||
|
||||
// identifier
|
||||
// Associated window handle of the window’s browsing context.
|
||||
object.set(identifier, navigable->traversable_navigable()->window_handle().to_byte_string());
|
||||
|
||||
return object;
|
||||
}
|
||||
|
||||
static JS::GCPtr<HTML::Navigable> find_navigable_with_handle(StringView handle, bool should_be_top_level)
|
||||
{
|
||||
for (auto* navigable : Web::HTML::all_navigables()) {
|
||||
if (navigable->is_top_level_traversable() != should_be_top_level)
|
||||
continue;
|
||||
|
||||
if (navigable->traversable_navigable()->window_handle() == handle)
|
||||
return navigable;
|
||||
}
|
||||
|
||||
return {};
|
||||
}
|
||||
|
||||
// https://w3c.github.io/webdriver/#dfn-represents-a-web-frame
|
||||
bool represents_a_web_frame(JS::Value value)
|
||||
{
|
||||
// An ECMAScript Object represents a web frame if it has a web frame identifier own property.
|
||||
if (!value.is_object())
|
||||
return false;
|
||||
|
||||
auto result = value.as_object().has_own_property(WEB_FRAME_IDENTIFIER);
|
||||
return !result.is_error() && result.value();
|
||||
}
|
||||
|
||||
// https://w3c.github.io/webdriver/#dfn-deserialize-a-web-frame
|
||||
ErrorOr<JS::NonnullGCPtr<HTML::WindowProxy>, WebDriver::Error> deserialize_web_frame(JS::Object const& object)
|
||||
{
|
||||
// 1. If object has no own property web frame identifier, return error with error code invalid argument.
|
||||
auto property = object.get(WEB_FRAME_IDENTIFIER);
|
||||
if (property.is_error() || !property.value().is_string())
|
||||
return WebDriver::Error::from_code(WebDriver::ErrorCode::InvalidArgument, "Object is not a web frame"sv);
|
||||
|
||||
// 2. Let reference be the result of getting the web frame identifier property from object.
|
||||
auto reference = property.value().as_string().utf8_string();
|
||||
|
||||
// 3. Let browsing context be the browsing context whose window handle is reference, or null if no such browsing
|
||||
// context exists.
|
||||
auto navigable = find_navigable_with_handle(reference, false);
|
||||
|
||||
// 4. If browsing context is null or a top-level browsing context, return error with error code no such frame.
|
||||
// NOTE: We filtered on the top-level browsing context condition in the previous step.
|
||||
if (!navigable)
|
||||
return WebDriver::Error::from_code(WebDriver::ErrorCode::NoSuchFrame, "Could not locate frame"sv);
|
||||
|
||||
// 5. Return success with data browsing context's associated window.
|
||||
return *navigable->active_window_proxy();
|
||||
}
|
||||
|
||||
// https://w3c.github.io/webdriver/#dfn-represents-a-web-frame
|
||||
bool represents_a_web_window(JS::Value value)
|
||||
{
|
||||
// An ECMAScript Object represents a web window if it has a web window identifier own property.
|
||||
if (!value.is_object())
|
||||
return false;
|
||||
|
||||
auto result = value.as_object().has_own_property(WEB_WINDOW_IDENTIFIER);
|
||||
return !result.is_error() && result.value();
|
||||
}
|
||||
|
||||
// https://w3c.github.io/webdriver/#dfn-deserialize-a-web-frame
|
||||
ErrorOr<JS::NonnullGCPtr<HTML::WindowProxy>, WebDriver::Error> deserialize_web_window(JS::Object const& object)
|
||||
{
|
||||
// 1. If object has no own property web window identifier, return error with error code invalid argument.
|
||||
auto property = object.get(WEB_WINDOW_IDENTIFIER);
|
||||
if (property.is_error() || !property.value().is_string())
|
||||
return WebDriver::Error::from_code(WebDriver::ErrorCode::InvalidArgument, "Object is not a web window"sv);
|
||||
|
||||
// 2. Let reference be the result of getting the web window identifier property from object.
|
||||
auto reference = property.value().as_string().utf8_string();
|
||||
|
||||
// 3. Let browsing context be the browsing context whose window handle is reference, or null if no such browsing
|
||||
// context exists.
|
||||
auto navigable = find_navigable_with_handle(reference, true);
|
||||
|
||||
// 4. If browsing context is null or not a top-level browsing context, return error with error code no such window.
|
||||
// NOTE: We filtered on the top-level browsing context condition in the previous step.
|
||||
if (!navigable)
|
||||
return WebDriver::Error::from_code(WebDriver::ErrorCode::NoSuchWindow, "Could not locate window"sv);
|
||||
|
||||
// 5. Return success with data browsing context's associated window.
|
||||
return *navigable->active_window_proxy();
|
||||
}
|
||||
|
||||
}
|
25
Libraries/LibWeb/WebDriver/Contexts.h
Normal file
25
Libraries/LibWeb/WebDriver/Contexts.h
Normal file
|
@ -0,0 +1,25 @@
|
|||
/*
|
||||
* Copyright (c) 2023, Linus Groh <linusg@serenityos.org>
|
||||
*
|
||||
* SPDX-License-Identifier: BSD-2-Clause
|
||||
*/
|
||||
|
||||
#pragma once
|
||||
|
||||
#include <AK/Forward.h>
|
||||
#include <AK/StringView.h>
|
||||
#include <LibJS/Runtime/Value.h>
|
||||
#include <LibWeb/Forward.h>
|
||||
#include <LibWeb/WebDriver/Error.h>
|
||||
|
||||
namespace Web::WebDriver {
|
||||
|
||||
JsonObject window_proxy_reference_object(HTML::WindowProxy const&);
|
||||
|
||||
bool represents_a_web_frame(JS::Value);
|
||||
ErrorOr<JS::NonnullGCPtr<HTML::WindowProxy>, WebDriver::Error> deserialize_web_frame(JS::Object const&);
|
||||
|
||||
bool represents_a_web_window(JS::Value);
|
||||
ErrorOr<JS::NonnullGCPtr<HTML::WindowProxy>, WebDriver::Error> deserialize_web_window(JS::Object const&);
|
||||
|
||||
}
|
152
Libraries/LibWeb/WebDriver/ElementLocationStrategies.cpp
Normal file
152
Libraries/LibWeb/WebDriver/ElementLocationStrategies.cpp
Normal file
|
@ -0,0 +1,152 @@
|
|||
/*
|
||||
* Copyright (c) 2022, Tobias Christiansen <tobyase@serenityos.org>
|
||||
* Copyright (c) 2022, Tim Flynn <trflynn89@serenityos.org>
|
||||
*
|
||||
* SPDX-License-Identifier: BSD-2-Clause
|
||||
*/
|
||||
|
||||
#include <LibWeb/DOM/Element.h>
|
||||
#include <LibWeb/DOM/HTMLCollection.h>
|
||||
#include <LibWeb/DOM/ParentNode.h>
|
||||
#include <LibWeb/DOM/StaticNodeList.h>
|
||||
#include <LibWeb/WebDriver/ElementLocationStrategies.h>
|
||||
#include <LibWeb/WebDriver/ElementReference.h>
|
||||
|
||||
namespace Web::WebDriver {
|
||||
|
||||
// https://w3c.github.io/webdriver/#css-selectors
|
||||
static ErrorOr<JS::NonnullGCPtr<DOM::NodeList>, Error> locate_element_by_css_selector(DOM::ParentNode& start_node, StringView selector)
|
||||
{
|
||||
// 1. Let elements be the result of calling querySelectorAll() with start node as this and selector as the argument.
|
||||
// If this causes an exception to be thrown, return error with error code invalid selector.
|
||||
auto elements = start_node.query_selector_all(selector);
|
||||
if (elements.is_exception())
|
||||
return Error::from_code(ErrorCode::InvalidSelector, "querySelectorAll() failed"sv);
|
||||
|
||||
// 2.Return success with data elements.
|
||||
return elements.release_value();
|
||||
}
|
||||
|
||||
// https://w3c.github.io/webdriver/#link-text
|
||||
static ErrorOr<JS::NonnullGCPtr<DOM::NodeList>, Error> locate_element_by_link_text(DOM::ParentNode& start_node, StringView selector)
|
||||
{
|
||||
auto& realm = start_node.realm();
|
||||
|
||||
// 1. Let elements be the result of calling querySelectorAll() with start node as this and "a" as the argument. If
|
||||
// this throws an exception, return error with error code unknown error.
|
||||
auto elements = start_node.query_selector_all("a"sv);
|
||||
if (elements.is_exception())
|
||||
return Error::from_code(ErrorCode::UnknownError, "querySelectorAll() failed"sv);
|
||||
|
||||
// 2. Let result be an empty NodeList.
|
||||
Vector<JS::Handle<DOM::Node>> result;
|
||||
|
||||
// 3. For each element in elements:
|
||||
for (size_t i = 0; i < elements.value()->length(); ++i) {
|
||||
auto& element = const_cast<DOM::Node&>(*elements.value()->item(i));
|
||||
|
||||
// 1. Let rendered text be the value that would be returned via a call to Get Element Text for element.
|
||||
auto rendered_text = element_rendered_text(element);
|
||||
|
||||
// 2. Let trimmed text be the result of removing all whitespace from the start and end of the string rendered text.
|
||||
auto trimmed_text = MUST(rendered_text.trim_whitespace());
|
||||
|
||||
// 3. If trimmed text equals selector, append element to result.
|
||||
if (trimmed_text == selector)
|
||||
result.append(element);
|
||||
}
|
||||
|
||||
// 4. Return success with data result.
|
||||
return DOM::StaticNodeList::create(realm, move(result));
|
||||
}
|
||||
|
||||
// https://w3c.github.io/webdriver/#partial-link-text
|
||||
static ErrorOr<JS::NonnullGCPtr<DOM::NodeList>, Error> locate_element_by_partial_link_text(DOM::ParentNode& start_node, StringView selector)
|
||||
{
|
||||
auto& realm = start_node.realm();
|
||||
|
||||
// 1. Let elements be the result of calling querySelectorAll() with start node as this and "a" as the argument. If
|
||||
// this throws an exception, return error with error code unknown error.
|
||||
auto elements = start_node.query_selector_all("a"sv);
|
||||
if (elements.is_exception())
|
||||
return Error::from_code(ErrorCode::UnknownError, "querySelectorAll() failed"sv);
|
||||
|
||||
// 2. Let result be an empty NodeList.
|
||||
Vector<JS::Handle<DOM::Node>> result;
|
||||
|
||||
// 3. For each element in elements:
|
||||
for (size_t i = 0; i < elements.value()->length(); ++i) {
|
||||
auto& element = const_cast<DOM::Node&>(*elements.value()->item(i));
|
||||
|
||||
// 1. Let rendered text be the value that would be returned via a call to Get Element Text for element.
|
||||
auto rendered_text = element_rendered_text(element);
|
||||
|
||||
// 2. If rendered text contains selector, append element to result.
|
||||
if (rendered_text.contains(selector))
|
||||
result.append(element);
|
||||
}
|
||||
|
||||
// 4. Return success with data result.
|
||||
return DOM::StaticNodeList::create(realm, move(result));
|
||||
}
|
||||
|
||||
// https://w3c.github.io/webdriver/#tag-name
|
||||
static JS::NonnullGCPtr<DOM::NodeList> locate_element_by_tag_name(DOM::ParentNode& start_node, StringView selector)
|
||||
{
|
||||
auto& realm = start_node.realm();
|
||||
|
||||
// To find a web element with the Tag Name strategy return success with data set to the result of calling
|
||||
// getElementsByTagName() with start node as this and selector as the argument.
|
||||
auto elements = start_node.get_elements_by_tag_name(MUST(FlyString::from_utf8(selector)));
|
||||
|
||||
// FIXME: Having to convert this to a NodeList is a bit awkward.
|
||||
Vector<JS::Handle<DOM::Node>> result;
|
||||
|
||||
for (size_t i = 0; i < elements->length(); ++i) {
|
||||
auto* element = elements->item(i);
|
||||
result.append(*element);
|
||||
}
|
||||
|
||||
return DOM::StaticNodeList::create(realm, move(result));
|
||||
}
|
||||
|
||||
// https://w3c.github.io/webdriver/#xpath
|
||||
static ErrorOr<JS::NonnullGCPtr<DOM::NodeList>, Error> locate_element_by_x_path(DOM::ParentNode&, StringView)
|
||||
{
|
||||
return Error::from_code(ErrorCode::UnsupportedOperation, "Not implemented: locate element by XPath"sv);
|
||||
}
|
||||
|
||||
Optional<LocationStrategy> location_strategy_from_string(StringView type)
|
||||
{
|
||||
if (type == "css selector"sv)
|
||||
return LocationStrategy::CssSelector;
|
||||
if (type == "link text"sv)
|
||||
return LocationStrategy::LinkText;
|
||||
if (type == "partial link text"sv)
|
||||
return LocationStrategy::PartialLinkText;
|
||||
if (type == "tag name"sv)
|
||||
return LocationStrategy::TagName;
|
||||
if (type == "xpath"sv)
|
||||
return LocationStrategy::XPath;
|
||||
return {};
|
||||
}
|
||||
|
||||
ErrorOr<JS::NonnullGCPtr<DOM::NodeList>, Error> invoke_location_strategy(LocationStrategy type, DOM::ParentNode& start_node, StringView selector)
|
||||
{
|
||||
switch (type) {
|
||||
case LocationStrategy::CssSelector:
|
||||
return locate_element_by_css_selector(start_node, selector);
|
||||
case LocationStrategy::LinkText:
|
||||
return locate_element_by_link_text(start_node, selector);
|
||||
case LocationStrategy::PartialLinkText:
|
||||
return locate_element_by_partial_link_text(start_node, selector);
|
||||
case LocationStrategy::TagName:
|
||||
return locate_element_by_tag_name(start_node, selector);
|
||||
case LocationStrategy::XPath:
|
||||
return locate_element_by_x_path(start_node, selector);
|
||||
}
|
||||
|
||||
VERIFY_NOT_REACHED();
|
||||
}
|
||||
|
||||
}
|
29
Libraries/LibWeb/WebDriver/ElementLocationStrategies.h
Normal file
29
Libraries/LibWeb/WebDriver/ElementLocationStrategies.h
Normal file
|
@ -0,0 +1,29 @@
|
|||
/*
|
||||
* Copyright (c) 2022, Tobias Christiansen <tobyase@serenityos.org>
|
||||
* Copyright (c) 2022, Tim Flynn <trflynn89@serenityos.org>
|
||||
*
|
||||
* SPDX-License-Identifier: BSD-2-Clause
|
||||
*/
|
||||
|
||||
#pragma once
|
||||
|
||||
#include <LibJS/Heap/GCPtr.h>
|
||||
#include <LibWeb/DOM/NodeList.h>
|
||||
#include <LibWeb/Forward.h>
|
||||
#include <LibWeb/WebDriver/Error.h>
|
||||
|
||||
namespace Web::WebDriver {
|
||||
|
||||
// https://w3c.github.io/webdriver/#dfn-table-of-location-strategies
|
||||
enum class LocationStrategy {
|
||||
CssSelector,
|
||||
LinkText,
|
||||
PartialLinkText,
|
||||
TagName,
|
||||
XPath,
|
||||
};
|
||||
|
||||
Optional<LocationStrategy> location_strategy_from_string(StringView type);
|
||||
ErrorOr<JS::NonnullGCPtr<DOM::NodeList>, Error> invoke_location_strategy(LocationStrategy type, DOM::ParentNode& start_node, StringView selector);
|
||||
|
||||
}
|
560
Libraries/LibWeb/WebDriver/ElementReference.cpp
Normal file
560
Libraries/LibWeb/WebDriver/ElementReference.cpp
Normal file
|
@ -0,0 +1,560 @@
|
|||
/*
|
||||
* Copyright (c) 2024, Tim Flynn <trflynn89@ladybird.org>
|
||||
*
|
||||
* SPDX-License-Identifier: BSD-2-Clause
|
||||
*/
|
||||
|
||||
#include <AK/HashMap.h>
|
||||
#include <LibJS/Runtime/Object.h>
|
||||
#include <LibWeb/DOM/Document.h>
|
||||
#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/HTML/BrowsingContext.h>
|
||||
#include <LibWeb/HTML/BrowsingContextGroup.h>
|
||||
#include <LibWeb/HTML/HTMLBodyElement.h>
|
||||
#include <LibWeb/HTML/HTMLInputElement.h>
|
||||
#include <LibWeb/HTML/HTMLTextAreaElement.h>
|
||||
#include <LibWeb/HTML/TraversableNavigable.h>
|
||||
#include <LibWeb/Page/Page.h>
|
||||
#include <LibWeb/Painting/PaintableBox.h>
|
||||
#include <LibWeb/WebDriver/ElementReference.h>
|
||||
|
||||
namespace Web::WebDriver {
|
||||
|
||||
// https://w3c.github.io/webdriver/#dfn-web-element-identifier
|
||||
static ByteString const web_element_identifier = "element-6066-11e4-a52e-4f735466cecf"sv;
|
||||
|
||||
// https://w3c.github.io/webdriver/#dfn-shadow-root-identifier
|
||||
static ByteString const shadow_root_identifier = "shadow-6066-11e4-a52e-4f735466cecf"sv;
|
||||
|
||||
// https://w3c.github.io/webdriver/#dfn-browsing-context-group-node-map
|
||||
static HashMap<JS::RawGCPtr<HTML::BrowsingContextGroup const>, HashTable<ByteString>> browsing_context_group_node_map;
|
||||
|
||||
// https://w3c.github.io/webdriver/#dfn-navigable-seen-nodes-map
|
||||
static HashMap<JS::RawGCPtr<HTML::Navigable>, HashTable<ByteString>> navigable_seen_nodes_map;
|
||||
|
||||
// https://w3c.github.io/webdriver/#dfn-get-a-node
|
||||
JS::GCPtr<Web::DOM::Node> get_node(HTML::BrowsingContext const& browsing_context, StringView reference)
|
||||
{
|
||||
// 1. Let browsing context group node map be session's browsing context group node map.
|
||||
// 2. Let browsing context group be browsing context's browsing context group.
|
||||
auto const* browsing_context_group = browsing_context.group();
|
||||
|
||||
// 3. If browsing context group node map does not contain browsing context group, return null.
|
||||
// 4. Let node id map be browsing context group node map[browsing context group].
|
||||
auto node_id_map = browsing_context_group_node_map.get(browsing_context_group);
|
||||
if (!node_id_map.has_value())
|
||||
return nullptr;
|
||||
|
||||
// 5. Let node be the entry in node id map whose value is reference, if such an entry exists, or null otherwise.
|
||||
JS::GCPtr<Web::DOM::Node> node;
|
||||
|
||||
if (node_id_map->contains(reference)) {
|
||||
auto node_id = reference.to_number<i64>().value();
|
||||
node = Web::DOM::Node::from_unique_id(UniqueNodeID(node_id));
|
||||
}
|
||||
|
||||
// 6. Return node.
|
||||
return node;
|
||||
}
|
||||
|
||||
// https://w3c.github.io/webdriver/#dfn-get-or-create-a-node-reference
|
||||
ByteString get_or_create_a_node_reference(HTML::BrowsingContext const& browsing_context, Web::DOM::Node const& node)
|
||||
{
|
||||
// 1. Let browsing context group node map be session's browsing context group node map.
|
||||
// 2. Let browsing context group be browsing context's browsing context group.
|
||||
auto const* browsing_context_group = browsing_context.group();
|
||||
|
||||
// 3. If browsing context group node map does not contain browsing context group, set browsing context group node
|
||||
// map[browsing context group] to a new weak map.
|
||||
// 4. Let node id map be browsing context group node map[browsing context group].
|
||||
auto& node_id_map = browsing_context_group_node_map.ensure(browsing_context_group);
|
||||
|
||||
auto node_id = ByteString::number(node.unique_id().value());
|
||||
|
||||
// 5. If node id map does not contain node:
|
||||
if (!node_id_map.contains(node_id)) {
|
||||
// 1. Let node id be a new globally unique string.
|
||||
// 2. Set node id map[node] to node id.
|
||||
node_id_map.set(node_id);
|
||||
|
||||
// 3. Let navigable be browsing context's active document's node navigable.
|
||||
auto navigable = browsing_context.active_document()->navigable();
|
||||
|
||||
// 4. Let navigable seen nodes map be session's navigable seen nodes map.
|
||||
// 5. If navigable seen nodes map does not contain navigable, set navigable seen nodes map[navigable] to an empty set.
|
||||
// 6. Append node id to navigable seen nodes map[navigable].
|
||||
navigable_seen_nodes_map.ensure(navigable).set(node_id);
|
||||
}
|
||||
|
||||
// 6. Return node id map[node].
|
||||
return node_id;
|
||||
}
|
||||
|
||||
// https://w3c.github.io/webdriver/#dfn-node-reference-is-known
|
||||
bool node_reference_is_known(HTML::BrowsingContext const& browsing_context, StringView reference)
|
||||
{
|
||||
// 1. Let navigable be browsing context's active document's node navigable.
|
||||
auto navigable = browsing_context.active_document()->navigable();
|
||||
if (!navigable)
|
||||
return false;
|
||||
|
||||
// 2. Let navigable seen nodes map be session's navigable seen nodes map.
|
||||
// 3. If navigable seen nodes map contains navigable and navigable seen nodes map[navigable] contains reference,
|
||||
// return true, otherwise return false.
|
||||
if (auto map = navigable_seen_nodes_map.get(navigable); map.has_value())
|
||||
return map->contains(reference);
|
||||
return false;
|
||||
}
|
||||
|
||||
// https://w3c.github.io/webdriver/#dfn-get-or-create-a-web-element-reference
|
||||
ByteString get_or_create_a_web_element_reference(HTML::BrowsingContext const& browsing_context, Web::DOM::Node const& element)
|
||||
{
|
||||
// 1. Assert: element implements Element.
|
||||
VERIFY(element.is_element());
|
||||
|
||||
// 2. Return the result of trying to get or create a node reference given session, session's current browsing
|
||||
// context, and element.
|
||||
return get_or_create_a_node_reference(browsing_context, element);
|
||||
}
|
||||
|
||||
// https://w3c.github.io/webdriver/#dfn-web-element-reference-object
|
||||
JsonObject web_element_reference_object(HTML::BrowsingContext const& browsing_context, Web::DOM::Node const& element)
|
||||
{
|
||||
// 1. Let identifier be the web element identifier.
|
||||
auto identifier = web_element_identifier;
|
||||
|
||||
// 2. Let reference be the result of get or create a web element reference given element.
|
||||
auto reference = get_or_create_a_web_element_reference(browsing_context, element);
|
||||
|
||||
// 3. Return a JSON Object initialized with a property with name identifier and value reference.
|
||||
JsonObject object;
|
||||
object.set(identifier, reference);
|
||||
return object;
|
||||
}
|
||||
|
||||
// https://w3c.github.io/webdriver/#dfn-represents-a-web-element
|
||||
bool represents_a_web_element(JsonValue const& value)
|
||||
{
|
||||
// An ECMAScript Object represents a web element if it has a web element identifier own property.
|
||||
if (!value.is_object())
|
||||
return false;
|
||||
|
||||
return value.as_object().has(web_element_identifier);
|
||||
}
|
||||
|
||||
// https://w3c.github.io/webdriver/#dfn-represents-a-web-element
|
||||
bool represents_a_web_element(JS::Value value)
|
||||
{
|
||||
// An ECMAScript Object represents a web element if it has a web element identifier own property.
|
||||
if (!value.is_object())
|
||||
return false;
|
||||
|
||||
auto result = value.as_object().has_own_property(web_element_identifier);
|
||||
return !result.is_error() && result.value();
|
||||
}
|
||||
|
||||
// https://w3c.github.io/webdriver/#dfn-deserialize-a-web-element
|
||||
ErrorOr<JS::NonnullGCPtr<Web::DOM::Element>, WebDriver::Error> deserialize_web_element(Web::HTML::BrowsingContext const& browsing_context, JsonObject const& object)
|
||||
{
|
||||
// 1. If object has no own property web element identifier, return error with error code invalid argument.
|
||||
if (!object.has_string(web_element_identifier))
|
||||
return WebDriver::Error::from_code(WebDriver::ErrorCode::InvalidArgument, "Object is not a web element");
|
||||
|
||||
// 2. Let reference be the result of getting the web element identifier property from object.
|
||||
auto reference = extract_web_element_reference(object);
|
||||
|
||||
// 3. Let element be the result of trying to get a known element with session and reference.
|
||||
auto element = TRY(get_known_element(browsing_context, reference));
|
||||
|
||||
// 4. Return success with data element.
|
||||
return element;
|
||||
}
|
||||
|
||||
// https://w3c.github.io/webdriver/#dfn-deserialize-a-web-element
|
||||
ErrorOr<JS::NonnullGCPtr<Web::DOM::Element>, WebDriver::Error> deserialize_web_element(Web::HTML::BrowsingContext const& browsing_context, JS::Object const& object)
|
||||
{
|
||||
// 1. If object has no own property web element identifier, return error with error code invalid argument.
|
||||
auto property = object.get(web_element_identifier);
|
||||
if (property.is_error() || !property.value().is_string())
|
||||
return WebDriver::Error::from_code(WebDriver::ErrorCode::InvalidArgument, "Object is not a web element");
|
||||
|
||||
// 2. Let reference be the result of getting the web element identifier property from object.
|
||||
auto reference = property.value().as_string().utf8_string();
|
||||
|
||||
// 3. Let element be the result of trying to get a known element with session and reference.
|
||||
auto element = TRY(get_known_element(browsing_context, reference));
|
||||
|
||||
// 4. Return success with data element.
|
||||
return element;
|
||||
}
|
||||
|
||||
ByteString extract_web_element_reference(JsonObject const& object)
|
||||
{
|
||||
return object.get_byte_string(web_element_identifier).release_value();
|
||||
}
|
||||
|
||||
// https://w3c.github.io/webdriver/#dfn-get-a-webelement-origin
|
||||
ErrorOr<JS::NonnullGCPtr<Web::DOM::Element>, Web::WebDriver::Error> get_web_element_origin(Web::HTML::BrowsingContext const& browsing_context, StringView origin)
|
||||
{
|
||||
// 1. Assert: browsing context is the current browsing context.
|
||||
|
||||
// 2. Let element be equal to the result of trying to get a known element with session and origin.
|
||||
auto element = TRY(get_known_element(browsing_context, origin));
|
||||
|
||||
// 3. Return success with data element.
|
||||
return element;
|
||||
}
|
||||
|
||||
// https://w3c.github.io/webdriver/#dfn-get-a-known-element
|
||||
ErrorOr<JS::NonnullGCPtr<Web::DOM::Element>, Web::WebDriver::Error> get_known_element(Web::HTML::BrowsingContext const& browsing_context, StringView reference)
|
||||
{
|
||||
// 1. If not node reference is known with session, session's current browsing context, and reference return error
|
||||
// with error code no such element.
|
||||
if (!node_reference_is_known(browsing_context, reference))
|
||||
return Web::WebDriver::Error::from_code(Web::WebDriver::ErrorCode::NoSuchElement, ByteString::formatted("Element reference '{}' is not known", reference));
|
||||
|
||||
// 2. Let node be the result of get a node with session, session's current browsing context, and reference.
|
||||
auto node = get_node(browsing_context, reference);
|
||||
|
||||
// 3. If node is not null and node does not implement Element return error with error code no such element.
|
||||
if (node && !node->is_element())
|
||||
return Web::WebDriver::Error::from_code(Web::WebDriver::ErrorCode::NoSuchElement, ByteString::formatted("Could not find element with reference '{}'", reference));
|
||||
|
||||
// 4. If node is null or node is stale return error with error code stale element reference.
|
||||
if (!node || is_element_stale(*node))
|
||||
return Web::WebDriver::Error::from_code(Web::WebDriver::ErrorCode::StaleElementReference, ByteString::formatted("Element reference '{}' is stale", reference));
|
||||
|
||||
// 5. Return success with data node.
|
||||
return static_cast<Web::DOM::Element&>(*node);
|
||||
}
|
||||
|
||||
// https://w3c.github.io/webdriver/#dfn-is-stale
|
||||
bool is_element_stale(Web::DOM::Node const& element)
|
||||
{
|
||||
// An element is stale if its node document is not the active document or if it is not connected.
|
||||
return !element.document().is_active() || !element.is_connected();
|
||||
}
|
||||
|
||||
// https://w3c.github.io/webdriver/#dfn-interactable
|
||||
bool is_element_interactable(Web::HTML::BrowsingContext const& browsing_context, Web::DOM::Element const& element)
|
||||
{
|
||||
// An interactable element is an element which is either pointer-interactable or keyboard-interactable.
|
||||
return is_element_keyboard_interactable(element) || is_element_pointer_interactable(browsing_context, element);
|
||||
}
|
||||
|
||||
// https://w3c.github.io/webdriver/#dfn-pointer-interactable
|
||||
bool is_element_pointer_interactable(Web::HTML::BrowsingContext const& browsing_context, Web::DOM::Element const& element)
|
||||
{
|
||||
// A pointer-interactable element is defined to be the first element, defined by the paint order found at the center
|
||||
// point of its rectangle that is inside the viewport, excluding the size of any rendered scrollbars.
|
||||
auto const* document = browsing_context.active_document();
|
||||
if (!document)
|
||||
return false;
|
||||
|
||||
auto const* paint_root = document->paintable_box();
|
||||
if (!paint_root)
|
||||
return false;
|
||||
|
||||
auto viewport = browsing_context.page().top_level_traversable()->viewport_rect();
|
||||
auto center_point = in_view_center_point(element, viewport);
|
||||
|
||||
auto result = paint_root->hit_test(center_point, Painting::HitTestType::TextCursor);
|
||||
if (!result.has_value())
|
||||
return false;
|
||||
|
||||
return result->dom_node() == &element;
|
||||
}
|
||||
|
||||
// https://w3c.github.io/webdriver/#dfn-keyboard-interactable
|
||||
bool is_element_keyboard_interactable(Web::DOM::Element const& element)
|
||||
{
|
||||
// A keyboard-interactable element is any element that has a focusable area, is a body element, or is the document element.
|
||||
return element.is_focusable() || is<HTML::HTMLBodyElement>(element) || element.is_document_element();
|
||||
}
|
||||
|
||||
// https://w3c.github.io/webdriver/#dfn-editable
|
||||
bool is_element_editable(Web::DOM::Element const& element)
|
||||
{
|
||||
// Editable elements are those that can be used for typing and clearing, and they fall into two subcategories:
|
||||
// "Mutable form control elements" and "Mutable elements".
|
||||
return is_element_mutable_form_control(element) || is_element_mutable(element);
|
||||
}
|
||||
|
||||
// https://w3c.github.io/webdriver/#dfn-mutable-element
|
||||
bool is_element_mutable(Web::DOM::Element const& element)
|
||||
{
|
||||
// Denotes elements that are editing hosts or content editable.
|
||||
if (!is<HTML::HTMLElement>(element))
|
||||
return false;
|
||||
|
||||
auto const& html_element = static_cast<HTML::HTMLElement const&>(element);
|
||||
return html_element.is_editable();
|
||||
}
|
||||
|
||||
// https://w3c.github.io/webdriver/#dfn-mutable-form-control-element
|
||||
bool is_element_mutable_form_control(Web::DOM::Element const& element)
|
||||
{
|
||||
// Denotes input elements that are mutable (e.g. that are not read only or disabled) and whose type attribute is
|
||||
// in one of the following states:
|
||||
if (is<HTML::HTMLInputElement>(element)) {
|
||||
auto const& input_element = static_cast<HTML::HTMLInputElement const&>(element);
|
||||
if (!input_element.is_mutable() || !input_element.enabled())
|
||||
return false;
|
||||
|
||||
// Text and Search, URL, Telephone, Email, Password, Date, Month, Week, Time, Local Date and Time, Number,
|
||||
// Range, Color, File Upload
|
||||
switch (input_element.type_state()) {
|
||||
case HTML::HTMLInputElement::TypeAttributeState::Text:
|
||||
case HTML::HTMLInputElement::TypeAttributeState::Search:
|
||||
case HTML::HTMLInputElement::TypeAttributeState::URL:
|
||||
case HTML::HTMLInputElement::TypeAttributeState::Telephone:
|
||||
case HTML::HTMLInputElement::TypeAttributeState::Email:
|
||||
case HTML::HTMLInputElement::TypeAttributeState::Password:
|
||||
case HTML::HTMLInputElement::TypeAttributeState::Date:
|
||||
case HTML::HTMLInputElement::TypeAttributeState::Month:
|
||||
case HTML::HTMLInputElement::TypeAttributeState::Week:
|
||||
case HTML::HTMLInputElement::TypeAttributeState::Time:
|
||||
case HTML::HTMLInputElement::TypeAttributeState::LocalDateAndTime:
|
||||
case HTML::HTMLInputElement::TypeAttributeState::Number:
|
||||
case HTML::HTMLInputElement::TypeAttributeState::Range:
|
||||
case HTML::HTMLInputElement::TypeAttributeState::Color:
|
||||
case HTML::HTMLInputElement::TypeAttributeState::FileUpload:
|
||||
return true;
|
||||
default:
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
// And the textarea element.
|
||||
if (is<HTML::HTMLTextAreaElement>(element)) {
|
||||
auto const& text_area = static_cast<HTML::HTMLTextAreaElement const&>(element);
|
||||
return text_area.enabled();
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
// https://w3c.github.io/webdriver/#dfn-non-typeable-form-control
|
||||
bool is_element_non_typeable_form_control(Web::DOM::Element const& element)
|
||||
{
|
||||
// A non-typeable form control is an input element whose type attribute state causes the primary input mechanism not
|
||||
// to be through means of a keyboard, whether virtual or physical.
|
||||
if (!is<HTML::HTMLInputElement>(element))
|
||||
return false;
|
||||
|
||||
auto const& input_element = static_cast<HTML::HTMLInputElement const&>(element);
|
||||
|
||||
switch (input_element.type_state()) {
|
||||
case HTML::HTMLInputElement::TypeAttributeState::Hidden:
|
||||
case HTML::HTMLInputElement::TypeAttributeState::Range:
|
||||
case HTML::HTMLInputElement::TypeAttributeState::Color:
|
||||
case HTML::HTMLInputElement::TypeAttributeState::Checkbox:
|
||||
case HTML::HTMLInputElement::TypeAttributeState::RadioButton:
|
||||
case HTML::HTMLInputElement::TypeAttributeState::FileUpload:
|
||||
case HTML::HTMLInputElement::TypeAttributeState::SubmitButton:
|
||||
case HTML::HTMLInputElement::TypeAttributeState::ImageButton:
|
||||
case HTML::HTMLInputElement::TypeAttributeState::ResetButton:
|
||||
case HTML::HTMLInputElement::TypeAttributeState::Button:
|
||||
return true;
|
||||
default:
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
// https://w3c.github.io/webdriver/#dfn-in-view
|
||||
bool is_element_in_view(ReadonlySpan<JS::NonnullGCPtr<Web::DOM::Element>> paint_tree, Web::DOM::Element& element)
|
||||
{
|
||||
// An element is in view if it is a member of its own pointer-interactable paint tree, given the pretense that its
|
||||
// pointer events are not disabled.
|
||||
if (!element.paintable() || !element.paintable()->is_visible() || !element.paintable()->visible_for_hit_testing())
|
||||
return false;
|
||||
|
||||
return paint_tree.contains_slow(JS::NonnullGCPtr { element });
|
||||
}
|
||||
|
||||
// https://w3c.github.io/webdriver/#dfn-in-view
|
||||
bool is_element_obscured(ReadonlySpan<JS::NonnullGCPtr<Web::DOM::Element>> paint_tree, Web::DOM::Element& element)
|
||||
{
|
||||
// An element is obscured if the pointer-interactable paint tree at its center point is empty, or the first element
|
||||
// in this tree is not an inclusive descendant of itself.
|
||||
return paint_tree.is_empty() || !paint_tree.first()->is_shadow_including_inclusive_descendant_of(element);
|
||||
}
|
||||
|
||||
// https://w3c.github.io/webdriver/#dfn-pointer-interactable-paint-tree
|
||||
JS::MarkedVector<JS::NonnullGCPtr<Web::DOM::Element>> pointer_interactable_tree(Web::HTML::BrowsingContext& browsing_context, Web::DOM::Element& element)
|
||||
{
|
||||
// 1. If element is not in the same tree as session's current browsing context's active document, return an empty sequence.
|
||||
if (!browsing_context.active_document()->contains(element))
|
||||
return JS::MarkedVector<JS::NonnullGCPtr<Web::DOM::Element>>(browsing_context.heap());
|
||||
|
||||
// 2. Let rectangles be the DOMRect sequence returned by calling getClientRects().
|
||||
auto rectangles = element.get_client_rects();
|
||||
|
||||
// 3. If rectangles has the length of 0, return an empty sequence.
|
||||
if (rectangles->length() == 0)
|
||||
return JS::MarkedVector<JS::NonnullGCPtr<Web::DOM::Element>>(browsing_context.heap());
|
||||
|
||||
// 4. Let center point be the in-view center point of the first indexed element in rectangles.
|
||||
auto viewport = browsing_context.page().top_level_traversable()->viewport_rect();
|
||||
auto center_point = Web::WebDriver::in_view_center_point(element, viewport);
|
||||
|
||||
// 5. Return the elements from point given the coordinates center point.
|
||||
return browsing_context.active_document()->elements_from_point(center_point.x().to_double(), center_point.y().to_double());
|
||||
}
|
||||
|
||||
// https://w3c.github.io/webdriver/#dfn-get-or-create-a-shadow-root-reference
|
||||
ByteString get_or_create_a_shadow_root_reference(HTML::BrowsingContext const& browsing_context, Web::DOM::ShadowRoot const& shadow_root)
|
||||
{
|
||||
// 1. Assert: element implements ShadowRoot.
|
||||
// 2. Return the result of trying to get or create a node reference with session, session's current browsing context,
|
||||
// and element.
|
||||
return get_or_create_a_node_reference(browsing_context, shadow_root);
|
||||
}
|
||||
|
||||
// https://w3c.github.io/webdriver/#dfn-shadow-root-reference-object
|
||||
JsonObject shadow_root_reference_object(HTML::BrowsingContext const& browsing_context, Web::DOM::ShadowRoot const& shadow_root)
|
||||
{
|
||||
// 1. Let identifier be the shadow root identifier.
|
||||
auto identifier = shadow_root_identifier;
|
||||
|
||||
// 2. Let reference be the result of get or create a shadow root reference with session and shadow root.
|
||||
auto reference = get_or_create_a_shadow_root_reference(browsing_context, shadow_root);
|
||||
|
||||
// 3. Return a JSON Object initialized with a property with name identifier and value reference.
|
||||
JsonObject object;
|
||||
object.set(identifier, reference);
|
||||
return object;
|
||||
}
|
||||
|
||||
// https://w3c.github.io/webdriver/#dfn-represents-a-shadow-root
|
||||
bool represents_a_shadow_root(JsonValue const& value)
|
||||
{
|
||||
// An ECMAScript Object represents a shadow root if it has a shadow root identifier own property.
|
||||
if (!value.is_object())
|
||||
return false;
|
||||
|
||||
return value.as_object().has(shadow_root_identifier);
|
||||
}
|
||||
|
||||
// https://w3c.github.io/webdriver/#dfn-represents-a-shadow-root
|
||||
bool represents_a_shadow_root(JS::Value value)
|
||||
{
|
||||
// An ECMAScript Object represents a shadow root if it has a shadow root identifier own property.
|
||||
if (!value.is_object())
|
||||
return false;
|
||||
|
||||
auto result = value.as_object().has_own_property(shadow_root_identifier);
|
||||
return !result.is_error() && result.value();
|
||||
}
|
||||
|
||||
// https://w3c.github.io/webdriver/#dfn-deserialize-a-shadow-root
|
||||
ErrorOr<JS::NonnullGCPtr<Web::DOM::ShadowRoot>, WebDriver::Error> deserialize_shadow_root(Web::HTML::BrowsingContext const& browsing_context, JsonObject const& object)
|
||||
{
|
||||
// 1. If object has no own property shadow root identifier, return error with error code invalid argument.
|
||||
if (!object.has_string(shadow_root_identifier))
|
||||
return WebDriver::Error::from_code(WebDriver::ErrorCode::InvalidArgument, "Object is not a Shadow Root");
|
||||
|
||||
// 2. Let reference be the result of getting the shadow root identifier property from object.
|
||||
auto reference = object.get_byte_string(shadow_root_identifier).release_value();
|
||||
|
||||
// 3. Let shadow be the result of trying to get a known shadow root with session and reference.
|
||||
auto shadow = TRY(get_known_shadow_root(browsing_context, reference));
|
||||
|
||||
// 4. Return success with data shadow.
|
||||
return shadow;
|
||||
}
|
||||
|
||||
// https://w3c.github.io/webdriver/#dfn-deserialize-a-shadow-root
|
||||
ErrorOr<JS::NonnullGCPtr<Web::DOM::ShadowRoot>, WebDriver::Error> deserialize_shadow_root(Web::HTML::BrowsingContext const& browsing_context, JS::Object const& object)
|
||||
{
|
||||
// 1. If object has no own property shadow root identifier, return error with error code invalid argument.
|
||||
auto property = object.get(shadow_root_identifier);
|
||||
if (property.is_error() || !property.value().is_string())
|
||||
return WebDriver::Error::from_code(WebDriver::ErrorCode::InvalidArgument, "Object is not a Shadow Root");
|
||||
|
||||
// 2. Let reference be the result of getting the shadow root identifier property from object.
|
||||
auto reference = property.value().as_string().utf8_string();
|
||||
|
||||
// 3. Let shadow be the result of trying to get a known shadow root with session and reference.
|
||||
auto shadow = TRY(get_known_shadow_root(browsing_context, reference));
|
||||
|
||||
// 4. Return success with data shadow.
|
||||
return shadow;
|
||||
}
|
||||
|
||||
// https://w3c.github.io/webdriver/#dfn-get-a-known-shadow-root
|
||||
ErrorOr<JS::NonnullGCPtr<Web::DOM::ShadowRoot>, Web::WebDriver::Error> get_known_shadow_root(HTML::BrowsingContext const& browsing_context, StringView reference)
|
||||
{
|
||||
// 1. If not node reference is known with session, session's current browsing context, and reference return error with error code no such shadow root.
|
||||
if (!node_reference_is_known(browsing_context, reference))
|
||||
return Web::WebDriver::Error::from_code(Web::WebDriver::ErrorCode::NoSuchShadowRoot, ByteString::formatted("Shadow root reference '{}' is not known", reference));
|
||||
|
||||
// 2. Let node be the result of get a node with session, session's current browsing context, and reference.
|
||||
auto node = get_node(browsing_context, reference);
|
||||
|
||||
// 3. If node is not null and node does not implement ShadowRoot return error with error code no such shadow root.
|
||||
if (node && !node->is_shadow_root())
|
||||
return Web::WebDriver::Error::from_code(Web::WebDriver::ErrorCode::NoSuchShadowRoot, ByteString::formatted("Could not find shadow root with reference '{}'", reference));
|
||||
|
||||
// 4. If node is null or node is detached return error with error code detached shadow root.
|
||||
if (!node || is_shadow_root_detached(static_cast<Web::DOM::ShadowRoot&>(*node)))
|
||||
return Web::WebDriver::Error::from_code(Web::WebDriver::ErrorCode::DetachedShadowRoot, ByteString::formatted("Element reference '{}' is stale", reference));
|
||||
|
||||
// 5. Return success with data node.
|
||||
return static_cast<Web::DOM::ShadowRoot&>(*node);
|
||||
}
|
||||
|
||||
// https://w3c.github.io/webdriver/#dfn-is-detached
|
||||
bool is_shadow_root_detached(Web::DOM::ShadowRoot const& shadow_root)
|
||||
{
|
||||
// A shadow root is detached if its node document is not the active document or if the element node referred to as
|
||||
// its host is stale.
|
||||
return !shadow_root.document().is_active() || !shadow_root.host() || is_element_stale(*shadow_root.host());
|
||||
}
|
||||
|
||||
// https://w3c.github.io/webdriver/#dfn-bot-dom-getvisibletext
|
||||
String element_rendered_text(DOM::Node& node)
|
||||
{
|
||||
// FIXME: The spec does not define how to get the element's rendered text, other than to do exactly as Selenium does.
|
||||
// This implementation is not sufficient, as we must also at least consider the shadow DOM.
|
||||
if (!is<HTML::HTMLElement>(node))
|
||||
return node.text_content().value_or(String {});
|
||||
|
||||
auto& element = static_cast<HTML::HTMLElement&>(node);
|
||||
return element.inner_text();
|
||||
}
|
||||
|
||||
// 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 };
|
||||
}
|
||||
|
||||
}
|
62
Libraries/LibWeb/WebDriver/ElementReference.h
Normal file
62
Libraries/LibWeb/WebDriver/ElementReference.h
Normal file
|
@ -0,0 +1,62 @@
|
|||
/*
|
||||
* Copyright (c) 2024, Tim Flynn <trflynn89@ladybird.org>
|
||||
*
|
||||
* SPDX-License-Identifier: BSD-2-Clause
|
||||
*/
|
||||
|
||||
#pragma once
|
||||
|
||||
#include <AK/ByteString.h>
|
||||
#include <AK/Error.h>
|
||||
#include <AK/JsonObject.h>
|
||||
#include <LibJS/Forward.h>
|
||||
#include <LibJS/Heap/GCPtr.h>
|
||||
#include <LibJS/Runtime/Value.h>
|
||||
#include <LibWeb/Forward.h>
|
||||
#include <LibWeb/PixelUnits.h>
|
||||
#include <LibWeb/WebDriver/Error.h>
|
||||
|
||||
namespace Web::WebDriver {
|
||||
|
||||
JS::GCPtr<Web::DOM::Node> get_node(HTML::BrowsingContext const&, StringView reference);
|
||||
ByteString get_or_create_a_node_reference(HTML::BrowsingContext const&, Web::DOM::Node const&);
|
||||
bool node_reference_is_known(HTML::BrowsingContext const&, StringView reference);
|
||||
|
||||
ByteString get_or_create_a_web_element_reference(HTML::BrowsingContext const&, Web::DOM::Node const& element);
|
||||
JsonObject web_element_reference_object(HTML::BrowsingContext const&, Web::DOM::Node const& element);
|
||||
bool represents_a_web_element(JsonValue const&);
|
||||
bool represents_a_web_element(JS::Value);
|
||||
ErrorOr<JS::NonnullGCPtr<Web::DOM::Element>, WebDriver::Error> deserialize_web_element(Web::HTML::BrowsingContext const&, JsonObject const&);
|
||||
ErrorOr<JS::NonnullGCPtr<Web::DOM::Element>, WebDriver::Error> deserialize_web_element(Web::HTML::BrowsingContext const&, JS::Object const&);
|
||||
ByteString extract_web_element_reference(JsonObject const&);
|
||||
ErrorOr<JS::NonnullGCPtr<Web::DOM::Element>, Web::WebDriver::Error> get_web_element_origin(Web::HTML::BrowsingContext const&, StringView origin);
|
||||
ErrorOr<JS::NonnullGCPtr<Web::DOM::Element>, Web::WebDriver::Error> get_known_element(Web::HTML::BrowsingContext const&, StringView reference);
|
||||
|
||||
bool is_element_stale(Web::DOM::Node const& element);
|
||||
bool is_element_interactable(Web::HTML::BrowsingContext const&, Web::DOM::Element const&);
|
||||
bool is_element_pointer_interactable(Web::HTML::BrowsingContext const&, Web::DOM::Element const&);
|
||||
bool is_element_keyboard_interactable(Web::DOM::Element const&);
|
||||
|
||||
bool is_element_editable(Web::DOM::Element const&);
|
||||
bool is_element_mutable(Web::DOM::Element const&);
|
||||
bool is_element_mutable_form_control(Web::DOM::Element const&);
|
||||
bool is_element_non_typeable_form_control(Web::DOM::Element const&);
|
||||
|
||||
bool is_element_in_view(ReadonlySpan<JS::NonnullGCPtr<Web::DOM::Element>> paint_tree, Web::DOM::Element&);
|
||||
bool is_element_obscured(ReadonlySpan<JS::NonnullGCPtr<Web::DOM::Element>> paint_tree, Web::DOM::Element&);
|
||||
JS::MarkedVector<JS::NonnullGCPtr<Web::DOM::Element>> pointer_interactable_tree(Web::HTML::BrowsingContext&, Web::DOM::Element&);
|
||||
|
||||
ByteString get_or_create_a_shadow_root_reference(HTML::BrowsingContext const&, Web::DOM::ShadowRoot const&);
|
||||
JsonObject shadow_root_reference_object(HTML::BrowsingContext const&, Web::DOM::ShadowRoot const&);
|
||||
bool represents_a_shadow_root(JsonValue const&);
|
||||
bool represents_a_shadow_root(JS::Value);
|
||||
ErrorOr<JS::NonnullGCPtr<Web::DOM::ShadowRoot>, WebDriver::Error> deserialize_shadow_root(Web::HTML::BrowsingContext const&, JsonObject const&);
|
||||
ErrorOr<JS::NonnullGCPtr<Web::DOM::ShadowRoot>, WebDriver::Error> deserialize_shadow_root(Web::HTML::BrowsingContext const&, JS::Object const&);
|
||||
ErrorOr<JS::NonnullGCPtr<Web::DOM::ShadowRoot>, Web::WebDriver::Error> get_known_shadow_root(HTML::BrowsingContext const&, StringView reference);
|
||||
bool is_shadow_root_detached(Web::DOM::ShadowRoot const&);
|
||||
|
||||
String element_rendered_text(DOM::Node&);
|
||||
|
||||
CSSPixelPoint in_view_center_point(DOM::Element const& element, CSSPixelRect viewport);
|
||||
|
||||
}
|
78
Libraries/LibWeb/WebDriver/Error.cpp
Normal file
78
Libraries/LibWeb/WebDriver/Error.cpp
Normal file
|
@ -0,0 +1,78 @@
|
|||
/*
|
||||
* Copyright (c) 2022, Sam Atkins <atkinssj@serenityos.org>
|
||||
*
|
||||
* SPDX-License-Identifier: BSD-2-Clause
|
||||
*/
|
||||
|
||||
#include <AK/Error.h>
|
||||
#include <AK/Vector.h>
|
||||
#include <LibWeb/WebDriver/Error.h>
|
||||
|
||||
namespace Web::WebDriver {
|
||||
|
||||
struct ErrorCodeData {
|
||||
ErrorCode error_code;
|
||||
unsigned http_status;
|
||||
ByteString json_error_code;
|
||||
};
|
||||
|
||||
// https://w3c.github.io/webdriver/#dfn-error-code
|
||||
static Vector<ErrorCodeData> const s_error_code_data = {
|
||||
{ ErrorCode::ElementClickIntercepted, 400, "element click intercepted" },
|
||||
{ ErrorCode::ElementNotInteractable, 400, "element not interactable" },
|
||||
{ ErrorCode::InsecureCertificate, 400, "insecure certificate" },
|
||||
{ ErrorCode::InvalidArgument, 400, "invalid argument" },
|
||||
{ ErrorCode::InvalidCookieDomain, 400, "invalid cookie domain" },
|
||||
{ ErrorCode::InvalidElementState, 400, "invalid element state" },
|
||||
{ ErrorCode::InvalidSelector, 400, "invalid selector" },
|
||||
{ ErrorCode::InvalidSessionId, 404, "invalid session id" },
|
||||
{ ErrorCode::JavascriptError, 500, "javascript error" },
|
||||
{ ErrorCode::MoveTargetOutOfBounds, 500, "move target out of bounds" },
|
||||
{ ErrorCode::NoSuchAlert, 404, "no such alert" },
|
||||
{ ErrorCode::NoSuchCookie, 404, "no such cookie" },
|
||||
{ ErrorCode::NoSuchElement, 404, "no such element" },
|
||||
{ ErrorCode::NoSuchFrame, 404, "no such frame" },
|
||||
{ ErrorCode::NoSuchWindow, 404, "no such window" },
|
||||
{ ErrorCode::NoSuchShadowRoot, 404, "no such shadow root" },
|
||||
{ ErrorCode::ScriptTimeoutError, 500, "script timeout" },
|
||||
{ ErrorCode::SessionNotCreated, 500, "session not created" },
|
||||
{ ErrorCode::StaleElementReference, 404, "stale element reference" },
|
||||
{ ErrorCode::DetachedShadowRoot, 404, "detached shadow root" },
|
||||
{ ErrorCode::Timeout, 500, "timeout" },
|
||||
{ ErrorCode::UnableToSetCookie, 500, "unable to set cookie" },
|
||||
{ ErrorCode::UnableToCaptureScreen, 500, "unable to capture screen" },
|
||||
{ ErrorCode::UnexpectedAlertOpen, 500, "unexpected alert open" },
|
||||
{ ErrorCode::UnknownCommand, 404, "unknown command" },
|
||||
{ ErrorCode::UnknownError, 500, "unknown error" },
|
||||
{ ErrorCode::UnknownMethod, 405, "unknown method" },
|
||||
{ ErrorCode::UnsupportedOperation, 500, "unsupported operation" },
|
||||
{ ErrorCode::OutOfMemory, 500, "out of memory" },
|
||||
};
|
||||
|
||||
Error Error::from_code(ErrorCode code, ByteString message, Optional<JsonValue> data)
|
||||
{
|
||||
auto const& error_code_data = s_error_code_data[to_underlying(code)];
|
||||
|
||||
return {
|
||||
error_code_data.http_status,
|
||||
error_code_data.json_error_code,
|
||||
move(message),
|
||||
move(data)
|
||||
};
|
||||
}
|
||||
|
||||
Error::Error(AK::Error const& error)
|
||||
{
|
||||
VERIFY(error.code() == ENOMEM);
|
||||
*this = from_code(ErrorCode::OutOfMemory, {}, {});
|
||||
}
|
||||
|
||||
Error::Error(unsigned http_status_, ByteString error_, ByteString message_, Optional<JsonValue> data_)
|
||||
: http_status(http_status_)
|
||||
, error(move(error_))
|
||||
, message(move(message_))
|
||||
, data(move(data_))
|
||||
{
|
||||
}
|
||||
|
||||
}
|
71
Libraries/LibWeb/WebDriver/Error.h
Normal file
71
Libraries/LibWeb/WebDriver/Error.h
Normal file
|
@ -0,0 +1,71 @@
|
|||
/*
|
||||
* Copyright (c) 2022, Florent Castelli <florent.castelli@gmail.com>
|
||||
* Copyright (c) 2022, Sam Atkins <atkinssj@serenityos.org>
|
||||
*
|
||||
* SPDX-License-Identifier: BSD-2-Clause
|
||||
*/
|
||||
|
||||
#pragma once
|
||||
|
||||
#include <AK/ByteString.h>
|
||||
#include <AK/JsonValue.h>
|
||||
|
||||
namespace Web::WebDriver {
|
||||
|
||||
// https://w3c.github.io/webdriver/#dfn-error-code
|
||||
enum class ErrorCode {
|
||||
ElementClickIntercepted,
|
||||
ElementNotInteractable,
|
||||
InsecureCertificate,
|
||||
InvalidArgument,
|
||||
InvalidCookieDomain,
|
||||
InvalidElementState,
|
||||
InvalidSelector,
|
||||
InvalidSessionId,
|
||||
JavascriptError,
|
||||
MoveTargetOutOfBounds,
|
||||
NoSuchAlert,
|
||||
NoSuchCookie,
|
||||
NoSuchElement,
|
||||
NoSuchFrame,
|
||||
NoSuchWindow,
|
||||
NoSuchShadowRoot,
|
||||
ScriptTimeoutError,
|
||||
SessionNotCreated,
|
||||
StaleElementReference,
|
||||
DetachedShadowRoot,
|
||||
Timeout,
|
||||
UnableToSetCookie,
|
||||
UnableToCaptureScreen,
|
||||
UnexpectedAlertOpen,
|
||||
UnknownCommand,
|
||||
UnknownError,
|
||||
UnknownMethod,
|
||||
UnsupportedOperation,
|
||||
|
||||
// Non-standard error codes:
|
||||
OutOfMemory,
|
||||
};
|
||||
|
||||
// https://w3c.github.io/webdriver/#errors
|
||||
struct Error {
|
||||
unsigned http_status;
|
||||
ByteString error;
|
||||
ByteString message;
|
||||
Optional<JsonValue> data;
|
||||
|
||||
static Error from_code(ErrorCode, ByteString message, Optional<JsonValue> data = {});
|
||||
|
||||
Error(unsigned http_status, ByteString error, ByteString message, Optional<JsonValue> data);
|
||||
Error(AK::Error const&);
|
||||
};
|
||||
|
||||
}
|
||||
|
||||
template<>
|
||||
struct AK::Formatter<Web::WebDriver::Error> : Formatter<StringView> {
|
||||
ErrorOr<void> format(FormatBuilder& builder, Web::WebDriver::Error const& error)
|
||||
{
|
||||
return Formatter<StringView>::format(builder, ByteString::formatted("Error {}, {}: {}", error.http_status, error.error, error.message));
|
||||
}
|
||||
};
|
240
Libraries/LibWeb/WebDriver/ExecuteScript.cpp
Normal file
240
Libraries/LibWeb/WebDriver/ExecuteScript.cpp
Normal file
|
@ -0,0 +1,240 @@
|
|||
/*
|
||||
* Copyright (c) 2022-2023, Linus Groh <linusg@serenityos.org>
|
||||
* Copyright (c) 2024, Tim Flynn <trflynn89@ladybird.org>
|
||||
*
|
||||
* SPDX-License-Identifier: BSD-2-Clause
|
||||
*/
|
||||
|
||||
#include <LibJS/Parser.h>
|
||||
#include <LibJS/Runtime/ECMAScriptFunctionObject.h>
|
||||
#include <LibJS/Runtime/GlobalEnvironment.h>
|
||||
#include <LibJS/Runtime/ObjectEnvironment.h>
|
||||
#include <LibJS/Runtime/PromiseConstructor.h>
|
||||
#include <LibWeb/DOM/Document.h>
|
||||
#include <LibWeb/HTML/BrowsingContext.h>
|
||||
#include <LibWeb/HTML/Scripting/Environments.h>
|
||||
#include <LibWeb/HTML/Scripting/TemporaryExecutionContext.h>
|
||||
#include <LibWeb/HTML/Window.h>
|
||||
#include <LibWeb/Platform/EventLoopPlugin.h>
|
||||
#include <LibWeb/WebDriver/ExecuteScript.h>
|
||||
#include <LibWeb/WebDriver/HeapTimer.h>
|
||||
|
||||
namespace Web::WebDriver {
|
||||
|
||||
// https://w3c.github.io/webdriver/#dfn-execute-a-function-body
|
||||
static JS::ThrowCompletionOr<JS::Value> execute_a_function_body(HTML::BrowsingContext const& browsing_context, ByteString const& body, ReadonlySpan<JS::Value> parameters)
|
||||
{
|
||||
// 1. Let window be the associated window of the current browsing context’s active document.
|
||||
auto window = browsing_context.active_document()->window();
|
||||
|
||||
// 2. Let environment settings be the environment settings object for window.
|
||||
auto& environment_settings = Web::HTML::relevant_settings_object(*window);
|
||||
|
||||
// 3. Let global scope be environment settings realm’s global environment.
|
||||
auto& realm = environment_settings.realm();
|
||||
auto& global_scope = realm.global_environment();
|
||||
|
||||
auto source_text = ByteString::formatted(
|
||||
R"~~~(function() {{
|
||||
{}
|
||||
}})~~~",
|
||||
body);
|
||||
|
||||
auto parser = JS::Parser { JS::Lexer { source_text } };
|
||||
auto function_expression = parser.parse_function_node<JS::FunctionExpression>();
|
||||
|
||||
// 4. If body is not parsable as a FunctionBody or if parsing detects an early error, return Completion { [[Type]]: normal, [[Value]]: null, [[Target]]: empty }.
|
||||
if (parser.has_errors())
|
||||
return JS::js_null();
|
||||
|
||||
// 5. If body begins with a directive prologue that contains a use strict directive then let strict be true, otherwise let strict be false.
|
||||
// NOTE: Handled in step 8 below.
|
||||
|
||||
// 6. Prepare to run a script with realm.
|
||||
HTML::prepare_to_run_script(realm);
|
||||
|
||||
// 7. Prepare to run a callback with environment settings.
|
||||
HTML::prepare_to_run_callback(realm);
|
||||
|
||||
// 8. Let function be the result of calling FunctionCreate, with arguments:
|
||||
// kind
|
||||
// Normal.
|
||||
// list
|
||||
// An empty List.
|
||||
// body
|
||||
// The result of parsing body above.
|
||||
// global scope
|
||||
// The result of parsing global scope above.
|
||||
// strict
|
||||
// The result of parsing strict above.
|
||||
auto function = JS::ECMAScriptFunctionObject::create(realm, "", move(source_text), function_expression->body(), function_expression->parameters(), function_expression->function_length(), function_expression->local_variables_names(), &global_scope, nullptr, function_expression->kind(), function_expression->is_strict_mode(), function_expression->parsing_insights());
|
||||
|
||||
// 9. Let completion be Function.[[Call]](window, parameters) with function as the this value.
|
||||
// NOTE: This is not entirely clear, but I don't think they mean actually passing `function` as
|
||||
// the this value argument, but using it as the object [[Call]] is executed on.
|
||||
auto completion = function->internal_call(window, parameters);
|
||||
|
||||
// 10. Clean up after running a callback with environment settings.
|
||||
HTML::clean_up_after_running_callback(realm);
|
||||
|
||||
// 11. Clean up after running a script with realm.
|
||||
HTML::clean_up_after_running_script(realm);
|
||||
|
||||
// 12. Return completion.
|
||||
return completion;
|
||||
}
|
||||
|
||||
void execute_script(HTML::BrowsingContext const& browsing_context, ByteString body, JS::MarkedVector<JS::Value> arguments, Optional<u64> const& timeout_ms, JS::NonnullGCPtr<OnScriptComplete> on_complete)
|
||||
{
|
||||
auto const* document = browsing_context.active_document();
|
||||
auto& realm = document->realm();
|
||||
auto& vm = document->vm();
|
||||
|
||||
// 5. Let timer be a new timer.
|
||||
auto timer = vm.heap().allocate<HeapTimer>(realm);
|
||||
|
||||
// 6. If timeout is not null:
|
||||
if (timeout_ms.has_value()) {
|
||||
// 1. Start the timer with timer and timeout.
|
||||
timer->start(timeout_ms.value(), JS::create_heap_function(vm.heap(), [on_complete]() {
|
||||
on_complete->function()({ .state = JS::Promise::State::Pending });
|
||||
}));
|
||||
}
|
||||
|
||||
// AD-HOC: An execution context is required for Promise creation hooks.
|
||||
HTML::TemporaryExecutionContext execution_context { realm, HTML::TemporaryExecutionContext::CallbacksEnabled::Yes };
|
||||
|
||||
// 7. Let promise be a new Promise.
|
||||
auto promise = WebIDL::create_promise(realm);
|
||||
|
||||
// 8. Run the following substeps in parallel:
|
||||
Platform::EventLoopPlugin::the().deferred_invoke(JS::create_heap_function(realm.heap(), [&realm, &browsing_context, promise, body = move(body), arguments = move(arguments)]() mutable {
|
||||
HTML::TemporaryExecutionContext execution_context { realm };
|
||||
|
||||
// 1. Let scriptPromise be the result of promise-calling execute a function body, with arguments body and arguments.
|
||||
auto script_result = execute_a_function_body(browsing_context, body, move(arguments));
|
||||
|
||||
// FIXME: This isn't right, we should be reacting to this using WebIDL::react_to_promise()
|
||||
// 2. Upon fulfillment of scriptPromise with value v, resolve promise with value v.
|
||||
if (script_result.has_value()) {
|
||||
WebIDL::resolve_promise(realm, promise, script_result.release_value());
|
||||
}
|
||||
|
||||
// 3. Upon rejection of scriptPromise with value r, reject promise with value r.
|
||||
if (script_result.is_throw_completion()) {
|
||||
WebIDL::reject_promise(realm, promise, *script_result.throw_completion().value());
|
||||
}
|
||||
}));
|
||||
|
||||
// 9. Wait until promise is resolved, or timer's timeout fired flag is set, whichever occurs first.
|
||||
auto reaction_steps = JS::create_heap_function(vm.heap(), [promise, timer, on_complete](JS::Value) -> WebIDL::ExceptionOr<JS::Value> {
|
||||
if (timer->is_timed_out())
|
||||
return JS::js_undefined();
|
||||
timer->stop();
|
||||
|
||||
auto promise_promise = JS::NonnullGCPtr { verify_cast<JS::Promise>(*promise->promise()) };
|
||||
on_complete->function()({ promise_promise->state(), promise_promise->result() });
|
||||
|
||||
return JS::js_undefined();
|
||||
});
|
||||
|
||||
WebIDL::react_to_promise(promise, reaction_steps, reaction_steps);
|
||||
}
|
||||
|
||||
void execute_async_script(HTML::BrowsingContext const& browsing_context, ByteString body, JS::MarkedVector<JS::Value> arguments, Optional<u64> const& timeout_ms, JS::NonnullGCPtr<OnScriptComplete> on_complete)
|
||||
{
|
||||
auto const* document = browsing_context.active_document();
|
||||
auto& realm = document->realm();
|
||||
auto& vm = document->vm();
|
||||
|
||||
// 5. Let timer be a new timer.
|
||||
auto timer = vm.heap().allocate<HeapTimer>(realm);
|
||||
|
||||
// 6. If timeout is not null:
|
||||
if (timeout_ms.has_value()) {
|
||||
// 1. Start the timer with timer and timeout.
|
||||
timer->start(timeout_ms.value(), JS::create_heap_function(vm.heap(), [on_complete]() {
|
||||
on_complete->function()({ .state = JS::Promise::State::Pending });
|
||||
}));
|
||||
}
|
||||
|
||||
// AD-HOC: An execution context is required for Promise creation hooks.
|
||||
HTML::TemporaryExecutionContext execution_context { realm, HTML::TemporaryExecutionContext::CallbacksEnabled::Yes };
|
||||
|
||||
// 7. Let promise be a new Promise.
|
||||
auto promise_capability = WebIDL::create_promise(realm);
|
||||
JS::NonnullGCPtr promise { verify_cast<JS::Promise>(*promise_capability->promise()) };
|
||||
|
||||
// 8. Run the following substeps in parallel:
|
||||
Platform::EventLoopPlugin::the().deferred_invoke(JS::create_heap_function(realm.heap(), [&vm, &realm, &browsing_context, timer, promise_capability, promise, body = move(body), arguments = move(arguments)]() mutable {
|
||||
HTML::TemporaryExecutionContext execution_context { realm };
|
||||
|
||||
// 1. Let resolvingFunctions be CreateResolvingFunctions(promise).
|
||||
auto resolving_functions = promise->create_resolving_functions();
|
||||
|
||||
// 2. Append resolvingFunctions.[[Resolve]] to arguments.
|
||||
arguments.append(resolving_functions.resolve);
|
||||
|
||||
// 3. Let result be the result of calling execute a function body, with arguments body and arguments.
|
||||
// FIXME: 'result' -> 'scriptResult' (spec issue)
|
||||
auto script_result = execute_a_function_body(browsing_context, body, move(arguments));
|
||||
|
||||
// 4. If scriptResult.[[Type]] is not normal, then reject promise with value scriptResult.[[Value]], and abort these steps.
|
||||
// NOTE: Prior revisions of this specification did not recognize the return value of the provided script.
|
||||
// In order to preserve legacy behavior, the return value only influences the command if it is a
|
||||
// "thenable" object or if determining this produces an exception.
|
||||
if (script_result.is_throw_completion()) {
|
||||
promise->reject(*script_result.throw_completion().value());
|
||||
return;
|
||||
}
|
||||
|
||||
// 5. If Type(scriptResult.[[Value]]) is not Object, then abort these steps.
|
||||
if (!script_result.value().is_object())
|
||||
return;
|
||||
|
||||
// 6. Let then be Get(scriptResult.[[Value]], "then").
|
||||
auto then = script_result.value().as_object().get(vm.names.then);
|
||||
|
||||
// 7. If then.[[Type]] is not normal, then reject promise with value then.[[Value]], and abort these steps.
|
||||
if (then.is_throw_completion()) {
|
||||
promise->reject(*then.throw_completion().value());
|
||||
return;
|
||||
}
|
||||
|
||||
// 8. If IsCallable(then.[[Type]]) is false, then abort these steps.
|
||||
if (!then.value().is_function())
|
||||
return;
|
||||
|
||||
// 9. Let scriptPromise be PromiseResolve(Promise, scriptResult.[[Value]]).
|
||||
auto script_promise_or_error = JS::promise_resolve(vm, realm.intrinsics().promise_constructor(), script_result.value());
|
||||
if (script_promise_or_error.is_throw_completion())
|
||||
return;
|
||||
auto& script_promise = static_cast<JS::Promise&>(*script_promise_or_error.value());
|
||||
|
||||
vm.custom_data()->spin_event_loop_until(JS::create_heap_function(vm.heap(), [timer, &script_promise]() {
|
||||
return timer->is_timed_out() || script_promise.state() != JS::Promise::State::Pending;
|
||||
}));
|
||||
|
||||
// 10. Upon fulfillment of scriptPromise with value v, resolve promise with value v.
|
||||
if (script_promise.state() == JS::Promise::State::Fulfilled)
|
||||
WebIDL::resolve_promise(realm, promise_capability, script_promise.result());
|
||||
|
||||
// 11. Upon rejection of scriptPromise with value r, reject promise with value r.
|
||||
if (script_promise.state() == JS::Promise::State::Rejected)
|
||||
WebIDL::reject_promise(realm, promise_capability, script_promise.result());
|
||||
}));
|
||||
|
||||
// 9. Wait until promise is resolved, or timer's timeout fired flag is set, whichever occurs first.
|
||||
auto reaction_steps = JS::create_heap_function(vm.heap(), [promise, timer, on_complete](JS::Value) -> WebIDL::ExceptionOr<JS::Value> {
|
||||
if (timer->is_timed_out())
|
||||
return JS::js_undefined();
|
||||
timer->stop();
|
||||
|
||||
on_complete->function()({ promise->state(), promise->result() });
|
||||
return JS::js_undefined();
|
||||
});
|
||||
|
||||
WebIDL::react_to_promise(promise_capability, reaction_steps, reaction_steps);
|
||||
}
|
||||
|
||||
}
|
29
Libraries/LibWeb/WebDriver/ExecuteScript.h
Normal file
29
Libraries/LibWeb/WebDriver/ExecuteScript.h
Normal file
|
@ -0,0 +1,29 @@
|
|||
/*
|
||||
* Copyright (c) 2022-2023, Linus Groh <linusg@serenityos.org>
|
||||
* Copyright (c) 2024, Tim Flynn <trflynn89@ladybird.org>
|
||||
*
|
||||
* SPDX-License-Identifier: BSD-2-Clause
|
||||
*/
|
||||
|
||||
#pragma once
|
||||
|
||||
#include <AK/Forward.h>
|
||||
#include <LibJS/Forward.h>
|
||||
#include <LibJS/Heap/HeapFunction.h>
|
||||
#include <LibJS/Runtime/Promise.h>
|
||||
#include <LibJS/Runtime/Value.h>
|
||||
#include <LibWeb/Forward.h>
|
||||
|
||||
namespace Web::WebDriver {
|
||||
|
||||
struct ExecutionResult {
|
||||
JS::Promise::State state { JS::Promise::State::Pending };
|
||||
JS::Value value {};
|
||||
};
|
||||
|
||||
using OnScriptComplete = JS::HeapFunction<void(ExecutionResult)>;
|
||||
|
||||
void execute_script(HTML::BrowsingContext const&, ByteString body, JS::MarkedVector<JS::Value> arguments, Optional<u64> const& timeout_ms, JS::NonnullGCPtr<OnScriptComplete> on_complete);
|
||||
void execute_async_script(HTML::BrowsingContext const&, ByteString body, JS::MarkedVector<JS::Value> arguments, Optional<u64> const& timeout_ms, JS::NonnullGCPtr<OnScriptComplete> on_complete);
|
||||
|
||||
}
|
60
Libraries/LibWeb/WebDriver/HeapTimer.cpp
Normal file
60
Libraries/LibWeb/WebDriver/HeapTimer.cpp
Normal file
|
@ -0,0 +1,60 @@
|
|||
/*
|
||||
* Copyright (c) 2024, Tim Flynn <trflynn89@ladybird.org>
|
||||
*
|
||||
* SPDX-License-Identifier: BSD-2-Clause
|
||||
*/
|
||||
|
||||
#include <LibCore/Timer.h>
|
||||
#include <LibWeb/WebDriver/HeapTimer.h>
|
||||
|
||||
namespace Web::WebDriver {
|
||||
|
||||
JS_DEFINE_ALLOCATOR(HeapTimer);
|
||||
|
||||
HeapTimer::HeapTimer()
|
||||
: m_timer(Core::Timer::create())
|
||||
{
|
||||
}
|
||||
|
||||
HeapTimer::~HeapTimer() = default;
|
||||
|
||||
void HeapTimer::visit_edges(JS::Cell::Visitor& visitor)
|
||||
{
|
||||
Base::visit_edges(visitor);
|
||||
visitor.visit(m_on_timeout);
|
||||
}
|
||||
|
||||
void HeapTimer::start(u64 timeout_ms, JS::NonnullGCPtr<JS::HeapFunction<void()>> on_timeout)
|
||||
{
|
||||
m_on_timeout = on_timeout;
|
||||
|
||||
m_timer->on_timeout = [this]() {
|
||||
m_timed_out = true;
|
||||
|
||||
if (m_on_timeout) {
|
||||
m_on_timeout->function()();
|
||||
m_on_timeout = nullptr;
|
||||
}
|
||||
};
|
||||
|
||||
m_timer->set_interval(static_cast<int>(timeout_ms));
|
||||
m_timer->set_single_shot(true);
|
||||
m_timer->start();
|
||||
}
|
||||
|
||||
void HeapTimer::stop_and_fire_timeout_handler()
|
||||
{
|
||||
auto on_timeout = m_on_timeout;
|
||||
stop();
|
||||
|
||||
if (on_timeout)
|
||||
on_timeout->function()();
|
||||
}
|
||||
|
||||
void HeapTimer::stop()
|
||||
{
|
||||
m_on_timeout = nullptr;
|
||||
m_timer->stop();
|
||||
}
|
||||
|
||||
}
|
37
Libraries/LibWeb/WebDriver/HeapTimer.h
Normal file
37
Libraries/LibWeb/WebDriver/HeapTimer.h
Normal file
|
@ -0,0 +1,37 @@
|
|||
/*
|
||||
* Copyright (c) 2024, Tim Flynn <trflynn89@ladybird.org>
|
||||
*
|
||||
* SPDX-License-Identifier: BSD-2-Clause
|
||||
*/
|
||||
|
||||
#pragma once
|
||||
|
||||
#include <LibCore/Forward.h>
|
||||
#include <LibJS/Heap/GCPtr.h>
|
||||
#include <LibJS/Heap/HeapFunction.h>
|
||||
|
||||
namespace Web::WebDriver {
|
||||
|
||||
class HeapTimer : public JS::Cell {
|
||||
JS_CELL(HeapTimer, JS::Cell);
|
||||
JS_DECLARE_ALLOCATOR(HeapTimer);
|
||||
|
||||
public:
|
||||
explicit HeapTimer();
|
||||
virtual ~HeapTimer() override;
|
||||
|
||||
void start(u64 timeout_ms, JS::NonnullGCPtr<JS::HeapFunction<void()>> on_timeout);
|
||||
void stop_and_fire_timeout_handler();
|
||||
void stop();
|
||||
|
||||
bool is_timed_out() const { return m_timed_out; }
|
||||
|
||||
private:
|
||||
virtual void visit_edges(JS::Cell::Visitor& visitor) override;
|
||||
|
||||
NonnullRefPtr<Core::Timer> m_timer;
|
||||
JS::GCPtr<JS::HeapFunction<void()>> m_on_timeout;
|
||||
bool m_timed_out { false };
|
||||
};
|
||||
|
||||
}
|
235
Libraries/LibWeb/WebDriver/InputSource.cpp
Normal file
235
Libraries/LibWeb/WebDriver/InputSource.cpp
Normal file
|
@ -0,0 +1,235 @@
|
|||
/*
|
||||
* Copyright (c) 2024, Tim Flynn <trflynn89@ladybird.org>
|
||||
*
|
||||
* SPDX-License-Identifier: BSD-2-Clause
|
||||
*/
|
||||
|
||||
#include <LibWeb/WebDriver/Actions.h>
|
||||
#include <LibWeb/WebDriver/InputSource.h>
|
||||
#include <LibWeb/WebDriver/InputState.h>
|
||||
|
||||
namespace Web::WebDriver {
|
||||
|
||||
static InputSourceType input_source_type(InputSource const& input_source)
|
||||
{
|
||||
return input_source.visit(
|
||||
[](NullInputSource const&) { return InputSourceType::None; },
|
||||
[](KeyInputSource const&) { return InputSourceType::Key; },
|
||||
[](PointerInputSource const&) { return InputSourceType::Pointer; },
|
||||
[](WheelInputSource const&) { return InputSourceType::Wheel; });
|
||||
}
|
||||
|
||||
// https://w3c.github.io/webdriver/#dfn-get-a-pointer-id
|
||||
static u32 get_pointer_id(InputState const& input_state, PointerInputSource::Subtype subtype)
|
||||
{
|
||||
// 1. Let minimum id be 0 if subtype is "mouse", or 2 otherwise.
|
||||
auto minimum_id = subtype == PointerInputSource::Subtype::Mouse ? 0u : 2u;
|
||||
|
||||
// 2. Let pointer ids be an empty set.
|
||||
HashTable<u32> pointer_ids;
|
||||
|
||||
// 3. Let sources be the result of getting the values with input state's input state map.
|
||||
// 4. For each source in sources:
|
||||
for (auto const& source : input_state.input_state_map) {
|
||||
// 1. If source is a pointer input source, append source's pointerId to pointer ids.
|
||||
if (auto const* pointer_input_source = source.value.get_pointer<PointerInputSource>())
|
||||
pointer_ids.set(pointer_input_source->pointer_id);
|
||||
}
|
||||
|
||||
// 5. Return the smallest integer that is greater than or equal to minimum id and that is not contained in pointer ids.
|
||||
for (u32 integer = minimum_id; integer < NumericLimits<u32>::max(); ++integer) {
|
||||
if (!pointer_ids.contains(integer))
|
||||
return integer;
|
||||
}
|
||||
|
||||
VERIFY_NOT_REACHED();
|
||||
}
|
||||
|
||||
// https://w3c.github.io/webdriver/#dfn-create-a-pointer-input-source
|
||||
PointerInputSource::PointerInputSource(InputState const& input_state, PointerInputSource::Subtype subtype)
|
||||
: subtype(subtype)
|
||||
, pointer_id(get_pointer_id(input_state, subtype))
|
||||
{
|
||||
// To create a pointer input source object given input state, and subtype, return a new pointer input source with
|
||||
// subtype set to subtype, pointerId set to get a pointer id with input state and subtype, and the other items set
|
||||
// to their default values.
|
||||
}
|
||||
|
||||
UIEvents::KeyModifier GlobalKeyState::modifiers() const
|
||||
{
|
||||
auto modifiers = UIEvents::KeyModifier::Mod_None;
|
||||
|
||||
if (ctrl_key)
|
||||
modifiers |= UIEvents::KeyModifier::Mod_Ctrl;
|
||||
if (shift_key)
|
||||
modifiers |= UIEvents::KeyModifier::Mod_Shift;
|
||||
if (alt_key)
|
||||
modifiers |= UIEvents::KeyModifier::Mod_Alt;
|
||||
if (meta_key)
|
||||
modifiers |= UIEvents::KeyModifier::Mod_Super;
|
||||
|
||||
return modifiers;
|
||||
}
|
||||
|
||||
Optional<InputSourceType> input_source_type_from_string(StringView input_source_type)
|
||||
{
|
||||
if (input_source_type == "none"sv)
|
||||
return InputSourceType::None;
|
||||
if (input_source_type == "key"sv)
|
||||
return InputSourceType::Key;
|
||||
if (input_source_type == "pointer"sv)
|
||||
return InputSourceType::Pointer;
|
||||
if (input_source_type == "wheel"sv)
|
||||
return InputSourceType::Wheel;
|
||||
return {};
|
||||
}
|
||||
|
||||
Optional<PointerInputSource::Subtype> pointer_input_source_subtype_from_string(StringView pointer_type)
|
||||
{
|
||||
if (pointer_type == "mouse"sv)
|
||||
return PointerInputSource::Subtype::Mouse;
|
||||
if (pointer_type == "pen"sv)
|
||||
return PointerInputSource::Subtype::Pen;
|
||||
if (pointer_type == "touch"sv)
|
||||
return PointerInputSource::Subtype::Touch;
|
||||
return {};
|
||||
}
|
||||
|
||||
// https://w3c.github.io/webdriver/#dfn-create-an-input-source
|
||||
InputSource create_input_source(InputState const& input_state, InputSourceType type, Optional<PointerInputSource::Subtype> subtype)
|
||||
{
|
||||
// Run the substeps matching the first matching value of type:
|
||||
switch (type) {
|
||||
// "none"
|
||||
case InputSourceType::None:
|
||||
// Let source be the result of create a null input source.
|
||||
return NullInputSource {};
|
||||
|
||||
// "key"
|
||||
case InputSourceType::Key:
|
||||
// Let source be the result of create a key input source.
|
||||
return KeyInputSource {};
|
||||
|
||||
// "pointer"
|
||||
case InputSourceType::Pointer:
|
||||
// Let source be the result of create a pointer input source with input state and subtype.
|
||||
return PointerInputSource { input_state, *subtype };
|
||||
|
||||
// "wheel"
|
||||
case InputSourceType::Wheel:
|
||||
// Let source be the result of create a wheel input source.
|
||||
return WheelInputSource {};
|
||||
}
|
||||
|
||||
// Otherwise:
|
||||
// Return error with error code invalid argument.
|
||||
|
||||
// NOTE: We know this cannot be reached because the only caller will have already thrown an invalid argument error
|
||||
// if the `type` parameter was not valid.
|
||||
VERIFY_NOT_REACHED();
|
||||
}
|
||||
|
||||
// https://w3c.github.io/webdriver/#dfn-remove-an-input-source
|
||||
void add_input_source(InputState& input_state, String id, InputSource source)
|
||||
{
|
||||
// 1. Let input state map be input state's input state map.
|
||||
// 2. Set input state map[input id] to source.
|
||||
input_state.input_state_map.set(move(id), move(source));
|
||||
}
|
||||
|
||||
// https://w3c.github.io/webdriver/#dfn-remove-an-input-source
|
||||
void remove_input_source(InputState& input_state, StringView id)
|
||||
{
|
||||
// 1. Assert: None of the items in input state's input cancel list has id equal to input id.
|
||||
// FIXME: Spec issue: This assertion cannot be correct. For example, when Element Click is executed, the initial
|
||||
// pointer down action will append a pointer up action to the input cancel list, and the input cancel list
|
||||
// is never subsequently cleared. So instead of performing this assertion, we remove any action from the
|
||||
// input cancel list with the provided input ID.
|
||||
// https://github.com/w3c/webdriver/issues/1809
|
||||
input_state.input_cancel_list.remove_all_matching([&](ActionObject const& action) {
|
||||
return action.id == id;
|
||||
});
|
||||
|
||||
// 2. Let input state map be input state's input state map.
|
||||
// 3. Remove input state map[input id].
|
||||
input_state.input_state_map.remove(id);
|
||||
}
|
||||
|
||||
// https://w3c.github.io/webdriver/#dfn-get-an-input-source
|
||||
Optional<InputSource&> get_input_source(InputState& input_state, StringView id)
|
||||
{
|
||||
// 1. Let input state map be input state's input state map.
|
||||
// 2. If input state map[input id] exists, return input state map[input id].
|
||||
// 3. Return undefined.
|
||||
return input_state.input_state_map.get(id);
|
||||
}
|
||||
|
||||
// https://w3c.github.io/webdriver/#dfn-get-or-create-an-input-source
|
||||
ErrorOr<InputSource*, WebDriver::Error> get_or_create_input_source(InputState& input_state, InputSourceType type, StringView id, Optional<PointerInputSource::Subtype> subtype)
|
||||
{
|
||||
// 1. Let source be get an input source with input state and input id.
|
||||
auto source = get_input_source(input_state, id);
|
||||
|
||||
// 2. If source is not undefined and source's type is not equal to type, or source is a pointer input source,
|
||||
// return error with error code invalid argument.
|
||||
if (source.has_value() && input_source_type(*source) != type) {
|
||||
// FIXME: Spec issue: It does not make sense to check if "source is a pointer input source". This would errantly
|
||||
// prevent the ability to perform two pointer actions in a row.
|
||||
// https://github.com/w3c/webdriver/issues/1810
|
||||
return WebDriver::Error::from_code(WebDriver::ErrorCode::InvalidArgument, "Property 'type' does not match existing input source type");
|
||||
}
|
||||
|
||||
// 3. If source is undefined, set source to the result of trying to create an input source with input state and type.
|
||||
if (!source.has_value()) {
|
||||
// FIXME: Spec issue: The spec doesn't say to add the source to the input state map, but it is explicitly
|
||||
// expected when we reach the `dispatch tick actions` AO.
|
||||
// https://github.com/w3c/webdriver/issues/1810
|
||||
input_state.input_state_map.set(MUST(String::from_utf8(id)), create_input_source(input_state, type, subtype));
|
||||
source = get_input_source(input_state, id);
|
||||
}
|
||||
|
||||
// 4. Return success with data source.
|
||||
return &source.value();
|
||||
}
|
||||
|
||||
// https://w3c.github.io/webdriver/#dfn-get-the-global-key-state
|
||||
GlobalKeyState get_global_key_state(InputState const& input_state)
|
||||
{
|
||||
// 1. Let input state map be input state's input state map.
|
||||
auto const& input_state_map = input_state.input_state_map;
|
||||
|
||||
// 2. Let sources be the result of getting the values with input state map.
|
||||
|
||||
// 3. Let key state be a new global key state with pressed set to an empty set, altKey, ctrlKey, metaKey, and
|
||||
// shiftKey set to false.
|
||||
GlobalKeyState key_state {};
|
||||
|
||||
// 4. For each source in sources:
|
||||
for (auto const& source : input_state_map) {
|
||||
// 1. If source is not a key input source, continue to the first step of this loop.
|
||||
auto const* key_input_source = source.value.get_pointer<KeyInputSource>();
|
||||
if (!key_input_source)
|
||||
continue;
|
||||
|
||||
// 2. Set key state's pressed item to the union of its current value and source's pressed item.
|
||||
for (auto const& pressed : key_input_source->pressed)
|
||||
key_state.pressed.set(pressed);
|
||||
|
||||
// 3. If source's alt item is true, set key state's altKey item to true.
|
||||
key_state.alt_key |= key_input_source->alt;
|
||||
|
||||
// 4. If source's ctrl item is true, set key state's ctrlKey item to true.
|
||||
key_state.ctrl_key |= key_input_source->ctrl;
|
||||
|
||||
// 5. If source's meta item is true, set key state's metaKey item to true.
|
||||
key_state.meta_key |= key_input_source->meta;
|
||||
|
||||
// 6. If source's shift item is true, set key state's shiftKey item to true.
|
||||
key_state.shift_key |= key_input_source->shift;
|
||||
}
|
||||
|
||||
// 5. Return key state.
|
||||
return key_state;
|
||||
}
|
||||
|
||||
}
|
87
Libraries/LibWeb/WebDriver/InputSource.h
Normal file
87
Libraries/LibWeb/WebDriver/InputSource.h
Normal file
|
@ -0,0 +1,87 @@
|
|||
/*
|
||||
* Copyright (c) 2024, Tim Flynn <trflynn89@ladybird.org>
|
||||
*
|
||||
* SPDX-License-Identifier: BSD-2-Clause
|
||||
*/
|
||||
|
||||
#pragma once
|
||||
|
||||
#include <AK/HashTable.h>
|
||||
#include <AK/Optional.h>
|
||||
#include <AK/String.h>
|
||||
#include <AK/Variant.h>
|
||||
#include <AK/Vector.h>
|
||||
#include <LibWeb/Forward.h>
|
||||
#include <LibWeb/PixelUnits.h>
|
||||
#include <LibWeb/UIEvents/KeyCode.h>
|
||||
#include <LibWeb/UIEvents/MouseButton.h>
|
||||
#include <LibWeb/WebDriver/Error.h>
|
||||
|
||||
namespace Web::WebDriver {
|
||||
|
||||
enum class InputSourceType {
|
||||
None,
|
||||
Key,
|
||||
Pointer,
|
||||
Wheel,
|
||||
};
|
||||
|
||||
// https://w3c.github.io/webdriver/#dfn-null-input-source
|
||||
struct NullInputSource {
|
||||
};
|
||||
|
||||
// https://w3c.github.io/webdriver/#dfn-key-input-source
|
||||
struct KeyInputSource {
|
||||
HashTable<String> pressed;
|
||||
bool alt { false };
|
||||
bool ctrl { false };
|
||||
bool meta { false };
|
||||
bool shift { false };
|
||||
};
|
||||
|
||||
// https://w3c.github.io/webdriver/#dfn-pointer-input-source
|
||||
struct PointerInputSource {
|
||||
enum class Subtype {
|
||||
Mouse,
|
||||
Pen,
|
||||
Touch,
|
||||
};
|
||||
|
||||
PointerInputSource(InputState const&, Subtype);
|
||||
|
||||
Subtype subtype { Subtype::Mouse };
|
||||
u32 pointer_id { 0 };
|
||||
UIEvents::MouseButton pressed { UIEvents::MouseButton::None };
|
||||
CSSPixelPoint position;
|
||||
};
|
||||
|
||||
// https://w3c.github.io/webdriver/#dfn-wheel-input-source
|
||||
struct WheelInputSource {
|
||||
};
|
||||
|
||||
// https://w3c.github.io/webdriver/#dfn-input-source
|
||||
using InputSource = Variant<NullInputSource, KeyInputSource, PointerInputSource, WheelInputSource>;
|
||||
|
||||
// https://w3c.github.io/webdriver/#dfn-global-key-state
|
||||
struct GlobalKeyState {
|
||||
UIEvents::KeyModifier modifiers() const;
|
||||
|
||||
HashTable<String> pressed;
|
||||
bool alt_key { false };
|
||||
bool ctrl_key { false };
|
||||
bool meta_key { false };
|
||||
bool shift_key { false };
|
||||
};
|
||||
|
||||
Optional<InputSourceType> input_source_type_from_string(StringView);
|
||||
Optional<PointerInputSource::Subtype> pointer_input_source_subtype_from_string(StringView);
|
||||
|
||||
InputSource create_input_source(InputState const&, InputSourceType, Optional<PointerInputSource::Subtype>);
|
||||
void add_input_source(InputState&, String id, InputSource);
|
||||
void remove_input_source(InputState&, StringView id);
|
||||
Optional<InputSource&> get_input_source(InputState&, StringView id);
|
||||
ErrorOr<InputSource*, WebDriver::Error> get_or_create_input_source(InputState&, InputSourceType, StringView id, Optional<PointerInputSource::Subtype>);
|
||||
|
||||
GlobalKeyState get_global_key_state(InputState const&);
|
||||
|
||||
}
|
44
Libraries/LibWeb/WebDriver/InputState.cpp
Normal file
44
Libraries/LibWeb/WebDriver/InputState.cpp
Normal file
|
@ -0,0 +1,44 @@
|
|||
/*
|
||||
* Copyright (c) 2024, Tim Flynn <trflynn89@ladybird.org>
|
||||
*
|
||||
* SPDX-License-Identifier: BSD-2-Clause
|
||||
*/
|
||||
|
||||
#include <LibWeb/HTML/BrowsingContext.h>
|
||||
#include <LibWeb/WebDriver/Actions.h>
|
||||
#include <LibWeb/WebDriver/InputState.h>
|
||||
|
||||
namespace Web::WebDriver {
|
||||
|
||||
// https://w3c.github.io/webdriver/#dfn-browsing-context-input-state-map
|
||||
static HashMap<JS::RawGCPtr<HTML::BrowsingContext>, InputState> s_browsing_context_input_state_map;
|
||||
|
||||
InputState::InputState() = default;
|
||||
InputState::~InputState() = default;
|
||||
|
||||
// https://w3c.github.io/webdriver/#dfn-get-the-input-state
|
||||
InputState& get_input_state(HTML::BrowsingContext& browsing_context)
|
||||
{
|
||||
// 1. Assert: browsing context is a top-level browsing context.
|
||||
VERIFY(browsing_context.is_top_level());
|
||||
|
||||
// 2. Let input state map be session's browsing context input state map.
|
||||
// 3. If input state map does not contain browsing context, set input state map[browsing context] to create an input state.
|
||||
auto& input_state = s_browsing_context_input_state_map.ensure(browsing_context);
|
||||
|
||||
// 4. Return input state map[browsing context].
|
||||
return input_state;
|
||||
}
|
||||
|
||||
// https://w3c.github.io/webdriver/#dfn-reset-the-input-state
|
||||
void reset_input_state(HTML::BrowsingContext& browsing_context)
|
||||
{
|
||||
// 1. Assert: browsing context is a top-level browsing context.
|
||||
VERIFY(browsing_context.is_top_level());
|
||||
|
||||
// 2. Let input state map be session's browsing context input state map.
|
||||
// 3. If input state map[browsing context] exists, then remove input state map[browsing context].
|
||||
s_browsing_context_input_state_map.remove(browsing_context);
|
||||
}
|
||||
|
||||
}
|
35
Libraries/LibWeb/WebDriver/InputState.h
Normal file
35
Libraries/LibWeb/WebDriver/InputState.h
Normal file
|
@ -0,0 +1,35 @@
|
|||
/*
|
||||
* Copyright (c) 2024, Tim Flynn <trflynn89@ladybird.org>
|
||||
*
|
||||
* SPDX-License-Identifier: BSD-2-Clause
|
||||
*/
|
||||
|
||||
#pragma once
|
||||
|
||||
#include <AK/HashMap.h>
|
||||
#include <AK/String.h>
|
||||
#include <AK/Vector.h>
|
||||
#include <LibWeb/Forward.h>
|
||||
#include <LibWeb/WebDriver/InputSource.h>
|
||||
|
||||
namespace Web::WebDriver {
|
||||
|
||||
// https://w3c.github.io/webdriver/#dfn-input-state
|
||||
struct InputState {
|
||||
InputState();
|
||||
~InputState();
|
||||
|
||||
// https://w3c.github.io/webdriver/#dfn-input-state-map
|
||||
HashMap<String, InputSource> input_state_map;
|
||||
|
||||
// https://w3c.github.io/webdriver/#dfn-input-cancel-list
|
||||
Vector<ActionObject> input_cancel_list;
|
||||
|
||||
// https://w3c.github.io/webdriver/#dfn-actions-queue
|
||||
Vector<String> actions_queue;
|
||||
};
|
||||
|
||||
InputState& get_input_state(HTML::BrowsingContext&);
|
||||
void reset_input_state(HTML::BrowsingContext&);
|
||||
|
||||
}
|
340
Libraries/LibWeb/WebDriver/JSON.cpp
Normal file
340
Libraries/LibWeb/WebDriver/JSON.cpp
Normal file
|
@ -0,0 +1,340 @@
|
|||
/*
|
||||
* Copyright (c) 2022-2023, Linus Groh <linusg@serenityos.org>
|
||||
* Copyright (c) 2024, Tim Flynn <trflynn89@ladybird.org>
|
||||
*
|
||||
* SPDX-License-Identifier: BSD-2-Clause
|
||||
*/
|
||||
|
||||
#include <AK/HashTable.h>
|
||||
#include <AK/JsonArray.h>
|
||||
#include <AK/JsonObject.h>
|
||||
#include <AK/JsonValue.h>
|
||||
#include <AK/NumericLimits.h>
|
||||
#include <AK/Variant.h>
|
||||
#include <LibJS/Runtime/Array.h>
|
||||
#include <LibJS/Runtime/JSONObject.h>
|
||||
#include <LibWeb/DOM/DOMTokenList.h>
|
||||
#include <LibWeb/DOM/Document.h>
|
||||
#include <LibWeb/DOM/HTMLCollection.h>
|
||||
#include <LibWeb/DOM/NodeList.h>
|
||||
#include <LibWeb/DOM/ShadowRoot.h>
|
||||
#include <LibWeb/FileAPI/FileList.h>
|
||||
#include <LibWeb/HTML/BrowsingContext.h>
|
||||
#include <LibWeb/HTML/HTMLAllCollection.h>
|
||||
#include <LibWeb/HTML/HTMLFormControlsCollection.h>
|
||||
#include <LibWeb/HTML/HTMLOptionsCollection.h>
|
||||
#include <LibWeb/HTML/WindowProxy.h>
|
||||
#include <LibWeb/WebDriver/Contexts.h>
|
||||
#include <LibWeb/WebDriver/ElementReference.h>
|
||||
#include <LibWeb/WebDriver/JSON.h>
|
||||
|
||||
namespace Web::WebDriver {
|
||||
|
||||
#define TRY_OR_JS_ERROR(expression) \
|
||||
({ \
|
||||
auto&& _temporary_result = (expression); \
|
||||
if (_temporary_result.is_error()) [[unlikely]] \
|
||||
return WebDriver::Error::from_code(ErrorCode::JavascriptError, "Script returned an error"); \
|
||||
static_assert(!::AK::Detail::IsLvalueReference<decltype(_temporary_result.release_value())>, \
|
||||
"Do not return a reference from a fallible expression"); \
|
||||
_temporary_result.release_value(); \
|
||||
})
|
||||
|
||||
using SeenMap = HashTable<JS::RawGCPtr<JS::Object const>>;
|
||||
|
||||
// https://w3c.github.io/webdriver/#dfn-collection
|
||||
static bool is_collection(JS::Object const& value)
|
||||
{
|
||||
// A collection is an Object that implements the Iterable interface, and whose:
|
||||
return (
|
||||
// - initial value of the toString own property is "Arguments"
|
||||
value.has_parameter_map()
|
||||
// - instance of Array
|
||||
|| is<JS::Array>(value)
|
||||
// - instance of DOMTokenList
|
||||
|| is<DOM::DOMTokenList>(value)
|
||||
// - instance of FileList
|
||||
|| is<FileAPI::FileList>(value)
|
||||
// - instance of HTMLAllCollection
|
||||
|| is<HTML::HTMLAllCollection>(value)
|
||||
// - instance of HTMLCollection
|
||||
|| is<DOM::HTMLCollection>(value)
|
||||
// - instance of HTMLFormControlsCollection
|
||||
|| is<HTML::HTMLFormControlsCollection>(value)
|
||||
// - instance of HTMLOptionsCollection
|
||||
|| is<HTML::HTMLOptionsCollection>(value)
|
||||
// - instance of NodeList
|
||||
|| is<DOM::NodeList>(value));
|
||||
}
|
||||
|
||||
// https://w3c.github.io/webdriver/#dfn-clone-an-object
|
||||
template<typename ResultType, typename CloneAlgorithm>
|
||||
static ErrorOr<ResultType, WebDriver::Error> clone_an_object(HTML::BrowsingContext const& browsing_context, JS::Object const& value, SeenMap& seen, CloneAlgorithm const& clone_algorithm)
|
||||
{
|
||||
static constexpr bool is_json_value = IsSame<ResultType, JsonValue>;
|
||||
|
||||
auto& realm = browsing_context.active_document()->realm();
|
||||
auto& vm = realm.vm();
|
||||
|
||||
// 1. If value is in seen, return error with error code javascript error.
|
||||
if (seen.contains(value))
|
||||
return WebDriver::Error::from_code(ErrorCode::JavascriptError, "Attempted to recursively clone an Object"sv);
|
||||
|
||||
// 2. Append value to seen.
|
||||
seen.set(value);
|
||||
|
||||
// 3. Let result be the value of the first matching statement, matching on value:
|
||||
auto result = TRY(([&]() -> ErrorOr<ResultType, WebDriver::Error> {
|
||||
// -> a collection
|
||||
if (is_collection(value)) {
|
||||
// A new Array which length property is equal to the result of getting the property length of value.
|
||||
auto length_property = TRY_OR_JS_ERROR(value.get(vm.names.length));
|
||||
|
||||
auto length = TRY_OR_JS_ERROR(length_property.to_length(vm));
|
||||
if (length > NumericLimits<u32>::max())
|
||||
return WebDriver::Error::from_code(ErrorCode::JavascriptError, "Length of Object too large"sv);
|
||||
|
||||
if constexpr (is_json_value)
|
||||
return JsonArray { length };
|
||||
else
|
||||
return TRY_OR_JS_ERROR(JS::Array::create(realm, length));
|
||||
}
|
||||
// -> Otherwise
|
||||
else {
|
||||
// A new Object.
|
||||
if constexpr (is_json_value)
|
||||
return JsonObject {};
|
||||
else
|
||||
return JS::Object::create(realm, realm.intrinsics().object_prototype());
|
||||
}
|
||||
}()));
|
||||
|
||||
Optional<WebDriver::Error> error;
|
||||
|
||||
// 4. For each enumerable property in value, run the following substeps:
|
||||
(void)value.enumerate_object_properties([&](auto property) -> Optional<JS::Completion> {
|
||||
// 1. Let name be the name of the property.
|
||||
auto name = MUST(JS::PropertyKey::from_value(vm, property));
|
||||
|
||||
// 2. Let source property value be the result of getting a property named name from value. If doing so causes
|
||||
// script to be run and that script throws an error, return error with error code javascript error.
|
||||
auto source_property_value = value.get(name);
|
||||
if (source_property_value.is_error()) {
|
||||
error = WebDriver::Error::from_code(ErrorCode::JavascriptError, "Script returned an error");
|
||||
return JS::normal_completion({});
|
||||
}
|
||||
|
||||
// 3. Let cloned property result be the result of calling the clone algorithm with session, source property
|
||||
// value and seen.
|
||||
auto cloned_property_result = clone_algorithm(browsing_context, source_property_value.value(), seen);
|
||||
|
||||
// 4. If cloned property result is a success, set a property of result with name name and value equal to cloned
|
||||
// property result's data.
|
||||
if (!cloned_property_result.is_error()) {
|
||||
if constexpr (is_json_value) {
|
||||
if (result.is_array() && name.is_number())
|
||||
result.as_array().set(name.as_number(), cloned_property_result.value());
|
||||
else if (result.is_object())
|
||||
result.as_object().set(name.to_string(), cloned_property_result.value());
|
||||
} else {
|
||||
(void)result->set(name, cloned_property_result.value(), JS::Object::ShouldThrowExceptions::No);
|
||||
}
|
||||
}
|
||||
// 5. Otherwise, return cloned property result.
|
||||
else {
|
||||
error = cloned_property_result.release_error();
|
||||
return JS::normal_completion({});
|
||||
}
|
||||
|
||||
return {};
|
||||
});
|
||||
|
||||
if (error.has_value())
|
||||
return error.release_value();
|
||||
|
||||
// 5. Remove the last element of seen.
|
||||
seen.remove(value);
|
||||
|
||||
// 6. Return success with data result.
|
||||
return result;
|
||||
}
|
||||
|
||||
// https://w3c.github.io/webdriver/#dfn-internal-json-clone
|
||||
static Response internal_json_clone(HTML::BrowsingContext const& browsing_context, JS::Value value, SeenMap& seen)
|
||||
{
|
||||
auto& vm = browsing_context.vm();
|
||||
|
||||
// To internal JSON clone given session, value and seen, return the value of the first matching statement, matching
|
||||
// on value:
|
||||
|
||||
// -> undefined
|
||||
// -> null
|
||||
if (value.is_nullish()) {
|
||||
// Return success with data null.
|
||||
return JsonValue {};
|
||||
}
|
||||
|
||||
// -> type Boolean
|
||||
// -> type Number
|
||||
// -> type String
|
||||
// Return success with data value.
|
||||
if (value.is_boolean())
|
||||
return JsonValue { value.as_bool() };
|
||||
if (value.is_number())
|
||||
return JsonValue { value.as_double() };
|
||||
if (value.is_string())
|
||||
return JsonValue { value.as_string().byte_string() };
|
||||
|
||||
// AD-HOC: BigInt and Symbol not mentioned anywhere in the WebDriver spec, as it references ES5.
|
||||
// It assumes that all primitives are handled above, and the value is an object for the remaining steps.
|
||||
if (value.is_bigint())
|
||||
return WebDriver::Error::from_code(ErrorCode::JavascriptError, "Cannot clone a BigInt"sv);
|
||||
if (value.is_symbol())
|
||||
return WebDriver::Error::from_code(ErrorCode::JavascriptError, "Cannot clone a Symbol"sv);
|
||||
|
||||
VERIFY(value.is_object());
|
||||
auto const& object = static_cast<JS::Object const&>(value.as_object());
|
||||
|
||||
// -> instance of Element
|
||||
if (is<DOM::Element>(object)) {
|
||||
auto const& element = static_cast<DOM::Element const&>(object);
|
||||
|
||||
// If the element is stale, return error with error code stale element reference.
|
||||
if (is_element_stale(element)) {
|
||||
return WebDriver::Error::from_code(ErrorCode::StaleElementReference, "Referenced element has become stale"sv);
|
||||
}
|
||||
// Otherwise:
|
||||
else {
|
||||
// 1. Let reference be the web element reference object for session and value.
|
||||
auto reference = web_element_reference_object(browsing_context, element);
|
||||
|
||||
// 2. Return success with data reference.
|
||||
return JsonValue { move(reference) };
|
||||
}
|
||||
}
|
||||
|
||||
// -> instance of ShadowRoot
|
||||
if (is<DOM::ShadowRoot>(object)) {
|
||||
auto const& shadow_root = static_cast<DOM::ShadowRoot const&>(object);
|
||||
|
||||
// If the shadow root is detached, return error with error code detached shadow root.
|
||||
if (is_shadow_root_detached(shadow_root)) {
|
||||
return WebDriver::Error::from_code(ErrorCode::DetachedShadowRoot, "Referenced shadow root has become detached"sv);
|
||||
}
|
||||
// Otherwise:
|
||||
else {
|
||||
// 1. Let reference be the shadow root reference object for session and value.
|
||||
auto reference = shadow_root_reference_object(browsing_context, shadow_root);
|
||||
|
||||
// 2. Return success with data reference.
|
||||
return JsonValue { move(reference) };
|
||||
}
|
||||
}
|
||||
|
||||
// -> a WindowProxy object
|
||||
if (is<HTML::WindowProxy>(object)) {
|
||||
auto const& window_proxy = static_cast<HTML::WindowProxy const&>(object);
|
||||
|
||||
// If the associated browsing context of the WindowProxy object in value has been destroyed, return error
|
||||
// with error code stale element reference.
|
||||
if (window_proxy.associated_browsing_context()->has_navigable_been_destroyed()) {
|
||||
return WebDriver::Error::from_code(ErrorCode::StaleElementReference, "Browsing context has been discarded"sv);
|
||||
}
|
||||
// Otherwise:
|
||||
else {
|
||||
// 1. Let reference be the WindowProxy reference object for value.
|
||||
auto reference = window_proxy_reference_object(window_proxy);
|
||||
|
||||
// 2. Return success with data reference.
|
||||
return JsonValue { move(reference) };
|
||||
}
|
||||
}
|
||||
|
||||
// -> has an own property named "toJSON" that is a Function
|
||||
if (auto to_json = object.get_without_side_effects(vm.names.toJSON); to_json.is_function()) {
|
||||
// Return success with the value returned by Function.[[Call]](toJSON) with value as the this value.
|
||||
auto to_json_result = TRY_OR_JS_ERROR(to_json.as_function().internal_call(value, JS::MarkedVector<JS::Value> { vm.heap() }));
|
||||
if (!to_json_result.is_string())
|
||||
return WebDriver::Error::from_code(ErrorCode::JavascriptError, "toJSON did not return a String"sv);
|
||||
|
||||
return JsonValue { to_json_result.as_string().byte_string() };
|
||||
}
|
||||
|
||||
// -> Otherwise
|
||||
// 1. Let result be clone an object with session value and seen, and internal JSON clone as the clone algorithm.
|
||||
auto result = TRY(clone_an_object<JsonValue>(browsing_context, object, seen, internal_json_clone));
|
||||
|
||||
// 2. Return success with data result.
|
||||
return result;
|
||||
}
|
||||
|
||||
// https://w3c.github.io/webdriver/#dfn-json-clone
|
||||
Response json_clone(HTML::BrowsingContext const& browsing_context, JS::Value value)
|
||||
{
|
||||
SeenMap seen;
|
||||
|
||||
// To JSON clone given session and value, return the result of internal JSON clone with session, value and an empty List.
|
||||
return internal_json_clone(browsing_context, value, seen);
|
||||
}
|
||||
|
||||
// https://w3c.github.io/webdriver/#dfn-json-deserialize
|
||||
static ErrorOr<JS::Value, WebDriver::Error> internal_json_deserialize(HTML::BrowsingContext const& browsing_context, JS::Value value, SeenMap& seen)
|
||||
{
|
||||
// 1. If seen is not provided, let seen be an empty List.
|
||||
// 2. Jump to the first appropriate step below:
|
||||
// 3. Matching on value:
|
||||
// -> undefined
|
||||
// -> null
|
||||
// -> type Boolean
|
||||
// -> type Number
|
||||
// -> type String
|
||||
if (value.is_nullish() || value.is_boolean() || value.is_number() || value.is_string()) {
|
||||
// Return success with data value.
|
||||
return value;
|
||||
}
|
||||
|
||||
// -> Object that represents a web element
|
||||
if (represents_a_web_element(value)) {
|
||||
// Return the deserialized web element of value.
|
||||
return deserialize_web_element(browsing_context, value.as_object());
|
||||
}
|
||||
|
||||
// -> Object that represents a shadow root
|
||||
if (represents_a_shadow_root(value)) {
|
||||
// Return the deserialized shadow root of value.
|
||||
return deserialize_shadow_root(browsing_context, value.as_object());
|
||||
}
|
||||
|
||||
// -> Object that represents a web frame
|
||||
if (represents_a_web_frame(value)) {
|
||||
// Return the deserialized web frame of value.
|
||||
return deserialize_web_frame(value.as_object());
|
||||
}
|
||||
|
||||
// -> Object that represents a web window
|
||||
if (represents_a_web_window(value)) {
|
||||
// Return the deserialized web window of value.
|
||||
return deserialize_web_window(value.as_object());
|
||||
}
|
||||
|
||||
// -> instance of Array
|
||||
// -> instance of Object
|
||||
if (value.is_object()) {
|
||||
// Return clone an object algorithm with session, value and seen, and the JSON deserialize algorithm as the
|
||||
// clone algorithm.
|
||||
return clone_an_object<JS::NonnullGCPtr<JS::Object>>(browsing_context, value.as_object(), seen, internal_json_deserialize);
|
||||
}
|
||||
|
||||
return WebDriver::Error::from_code(ErrorCode::JavascriptError, "Unrecognized value type"sv);
|
||||
}
|
||||
|
||||
// https://w3c.github.io/webdriver/#dfn-json-deserialize
|
||||
ErrorOr<JS::Value, WebDriver::Error> json_deserialize(HTML::BrowsingContext const& browsing_context, JsonValue const& value)
|
||||
{
|
||||
auto& vm = browsing_context.vm();
|
||||
|
||||
SeenMap seen;
|
||||
return internal_json_deserialize(browsing_context, JS::JSONObject::parse_json_value(vm, value), seen);
|
||||
}
|
||||
|
||||
}
|
19
Libraries/LibWeb/WebDriver/JSON.h
Normal file
19
Libraries/LibWeb/WebDriver/JSON.h
Normal file
|
@ -0,0 +1,19 @@
|
|||
/*
|
||||
* Copyright (c) 2022-2023, Linus Groh <linusg@serenityos.org>
|
||||
* Copyright (c) 2024, Tim Flynn <trflynn89@ladybird.org>
|
||||
*
|
||||
* SPDX-License-Identifier: BSD-2-Clause
|
||||
*/
|
||||
|
||||
#pragma once
|
||||
|
||||
#include <LibJS/Runtime/Value.h>
|
||||
#include <LibWeb/Forward.h>
|
||||
#include <LibWeb/WebDriver/Response.h>
|
||||
|
||||
namespace Web::WebDriver {
|
||||
|
||||
Response json_clone(HTML::BrowsingContext const&, JS::Value);
|
||||
ErrorOr<JS::Value, WebDriver::Error> json_deserialize(HTML::BrowsingContext const&, JsonValue const&);
|
||||
|
||||
}
|
107
Libraries/LibWeb/WebDriver/Properties.h
Normal file
107
Libraries/LibWeb/WebDriver/Properties.h
Normal file
|
@ -0,0 +1,107 @@
|
|||
/*
|
||||
* Copyright (c) 2024, Tim Flynn <trflynn89@ladybird.org>
|
||||
*
|
||||
* SPDX-License-Identifier: BSD-2-Clause
|
||||
*/
|
||||
|
||||
#pragma once
|
||||
|
||||
#include <AK/ByteString.h>
|
||||
#include <AK/JsonArray.h>
|
||||
#include <AK/JsonObject.h>
|
||||
#include <AK/JsonValue.h>
|
||||
#include <LibJS/Runtime/Value.h>
|
||||
#include <LibWeb/WebDriver/Error.h>
|
||||
|
||||
namespace Web::WebDriver {
|
||||
|
||||
template<typename PropertyType = ByteString>
|
||||
static ErrorOr<PropertyType, WebDriver::Error> get_property(JsonObject const& payload, StringView key)
|
||||
{
|
||||
auto property = payload.get(key);
|
||||
|
||||
if (!property.has_value())
|
||||
return WebDriver::Error::from_code(ErrorCode::InvalidArgument, ByteString::formatted("No property called '{}' present", key));
|
||||
|
||||
auto is_safe_number = []<typename T>(T value) {
|
||||
if constexpr (sizeof(T) >= 8) {
|
||||
if (value > static_cast<T>(JS::MAX_ARRAY_LIKE_INDEX))
|
||||
return false;
|
||||
|
||||
if constexpr (IsSigned<T>) {
|
||||
if (value < -static_cast<T>(JS::MAX_ARRAY_LIKE_INDEX))
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
return true;
|
||||
};
|
||||
|
||||
if constexpr (IsSame<PropertyType, ByteString>) {
|
||||
if (!property->is_string())
|
||||
return WebDriver::Error::from_code(ErrorCode::InvalidArgument, ByteString::formatted("Property '{}' is not a String", key));
|
||||
return property->as_string();
|
||||
} else if constexpr (IsSame<PropertyType, bool>) {
|
||||
if (!property->is_bool())
|
||||
return WebDriver::Error::from_code(ErrorCode::InvalidArgument, ByteString::formatted("Property '{}' is not a Boolean", key));
|
||||
return property->as_bool();
|
||||
} else if constexpr (IsIntegral<PropertyType>) {
|
||||
if (auto maybe_number = property->get_integer<PropertyType>(); maybe_number.has_value() && is_safe_number(*maybe_number))
|
||||
return *maybe_number;
|
||||
return WebDriver::Error::from_code(ErrorCode::InvalidArgument, ByteString::formatted("Property '{}' is not an Integer", key));
|
||||
} else if constexpr (IsSame<PropertyType, double>) {
|
||||
if (auto maybe_number = property->get_double_with_precision_loss(); maybe_number.has_value() && is_safe_number(*maybe_number))
|
||||
return *maybe_number;
|
||||
return WebDriver::Error::from_code(ErrorCode::InvalidArgument, ByteString::formatted("Property '{}' is not a Number", key));
|
||||
} else if constexpr (IsSame<PropertyType, JsonArray const*>) {
|
||||
if (!property->is_array())
|
||||
return WebDriver::Error::from_code(ErrorCode::InvalidArgument, ByteString::formatted("Property '{}' is not an Array", key));
|
||||
return &property->as_array();
|
||||
} else if constexpr (IsSame<PropertyType, JsonObject const*>) {
|
||||
if (!property->is_object())
|
||||
return WebDriver::Error::from_code(ErrorCode::InvalidArgument, ByteString::formatted("Property '{}' is not an Object", key));
|
||||
return &property->as_object();
|
||||
} else {
|
||||
static_assert(DependentFalse<PropertyType>, "get_property invoked with unknown property type");
|
||||
VERIFY_NOT_REACHED();
|
||||
}
|
||||
}
|
||||
|
||||
template<typename PropertyType = ByteString>
|
||||
static ErrorOr<PropertyType, WebDriver::Error> get_property(JsonValue const& payload, StringView key)
|
||||
{
|
||||
if (!payload.is_object())
|
||||
return WebDriver::Error::from_code(ErrorCode::InvalidArgument, "Payload is not a JSON object");
|
||||
return get_property<PropertyType>(payload.as_object(), key);
|
||||
}
|
||||
|
||||
template<typename PropertyType = ByteString>
|
||||
static ErrorOr<Optional<PropertyType>, WebDriver::Error> get_optional_property(JsonObject const& object, StringView key)
|
||||
{
|
||||
if (!object.has(key))
|
||||
return OptionalNone {};
|
||||
return get_property<PropertyType>(object, key);
|
||||
}
|
||||
|
||||
template<Arithmetic PropertyType>
|
||||
static ErrorOr<PropertyType, WebDriver::Error> get_property_with_limits(JsonObject const& object, StringView key, Optional<PropertyType> min, Optional<PropertyType> max)
|
||||
{
|
||||
auto value = TRY(get_property<PropertyType>(object, key));
|
||||
|
||||
if (min.has_value() && value < *min)
|
||||
return WebDriver::Error::from_code(WebDriver::ErrorCode::InvalidArgument, ByteString::formatted("Property '{}' must not be less than {}", key, *min));
|
||||
if (max.has_value() && value > *max)
|
||||
return WebDriver::Error::from_code(WebDriver::ErrorCode::InvalidArgument, ByteString::formatted("Property '{}' must not be greater than {}", key, *max));
|
||||
|
||||
return value;
|
||||
}
|
||||
|
||||
template<Arithmetic PropertyType>
|
||||
static ErrorOr<Optional<PropertyType>, WebDriver::Error> get_optional_property_with_limits(JsonObject const& object, StringView key, Optional<PropertyType> min, Optional<PropertyType> max)
|
||||
{
|
||||
if (!object.has(key))
|
||||
return OptionalNone {};
|
||||
return get_property_with_limits<PropertyType>(object, key, min, max);
|
||||
}
|
||||
|
||||
}
|
70
Libraries/LibWeb/WebDriver/Response.cpp
Normal file
70
Libraries/LibWeb/WebDriver/Response.cpp
Normal file
|
@ -0,0 +1,70 @@
|
|||
/*
|
||||
* Copyright (c) 2022, Tim Flynn <trflynn89@serenityos.org>
|
||||
*
|
||||
* SPDX-License-Identifier: BSD-2-Clause
|
||||
*/
|
||||
|
||||
#include <LibIPC/Decoder.h>
|
||||
#include <LibIPC/Encoder.h>
|
||||
#include <LibWeb/WebDriver/Response.h>
|
||||
|
||||
enum class ResponseType : u8 {
|
||||
Success,
|
||||
Error,
|
||||
};
|
||||
|
||||
namespace Web::WebDriver {
|
||||
|
||||
Response::Response(JsonValue&& value)
|
||||
: m_value_or_error(move(value))
|
||||
{
|
||||
}
|
||||
|
||||
Response::Response(Error&& error)
|
||||
: m_value_or_error(move(error))
|
||||
{
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
template<>
|
||||
ErrorOr<void> IPC::encode(Encoder& encoder, Web::WebDriver::Response const& response)
|
||||
{
|
||||
return response.visit(
|
||||
[](Empty) -> ErrorOr<void> { VERIFY_NOT_REACHED(); },
|
||||
[&](JsonValue const& value) -> ErrorOr<void> {
|
||||
TRY(encoder.encode(ResponseType::Success));
|
||||
TRY(encoder.encode(value));
|
||||
return {};
|
||||
},
|
||||
[&](Web::WebDriver::Error const& error) -> ErrorOr<void> {
|
||||
TRY(encoder.encode(ResponseType::Error));
|
||||
TRY(encoder.encode(error.http_status));
|
||||
TRY(encoder.encode(error.error));
|
||||
TRY(encoder.encode(error.message));
|
||||
TRY(encoder.encode(error.data));
|
||||
return {};
|
||||
});
|
||||
}
|
||||
|
||||
template<>
|
||||
ErrorOr<Web::WebDriver::Response> IPC::decode(Decoder& decoder)
|
||||
{
|
||||
auto type = TRY(decoder.decode<ResponseType>());
|
||||
|
||||
switch (type) {
|
||||
case ResponseType::Success:
|
||||
return TRY(decoder.decode<JsonValue>());
|
||||
|
||||
case ResponseType::Error: {
|
||||
auto http_status = TRY(decoder.decode<unsigned>());
|
||||
auto error = TRY(decoder.decode<ByteString>());
|
||||
auto message = TRY(decoder.decode<ByteString>());
|
||||
auto data = TRY(decoder.decode<Optional<JsonValue>>());
|
||||
|
||||
return Web::WebDriver::Error { http_status, move(error), move(message), move(data) };
|
||||
}
|
||||
}
|
||||
|
||||
VERIFY_NOT_REACHED();
|
||||
}
|
55
Libraries/LibWeb/WebDriver/Response.h
Normal file
55
Libraries/LibWeb/WebDriver/Response.h
Normal file
|
@ -0,0 +1,55 @@
|
|||
/*
|
||||
* Copyright (c) 2022, Tim Flynn <trflynn89@serenityos.org>
|
||||
*
|
||||
* SPDX-License-Identifier: BSD-2-Clause
|
||||
*/
|
||||
|
||||
#pragma once
|
||||
|
||||
#include <AK/JsonValue.h>
|
||||
#include <AK/Variant.h>
|
||||
#include <LibIPC/Forward.h>
|
||||
#include <LibWeb/WebDriver/Error.h>
|
||||
|
||||
namespace Web::WebDriver {
|
||||
|
||||
// FIXME: Ideally, this could be `using Response = ErrorOr<JsonValue, Error>`, but that won't be
|
||||
// default-constructible, which is a requirement for the generated IPC.
|
||||
struct [[nodiscard]] Response {
|
||||
Response() = default;
|
||||
Response(JsonValue&&);
|
||||
Response(Error&&);
|
||||
|
||||
JsonValue& value() { return m_value_or_error.template get<JsonValue>(); }
|
||||
JsonValue const& value() const { return m_value_or_error.template get<JsonValue>(); }
|
||||
|
||||
Error& error() { return m_value_or_error.template get<Error>(); }
|
||||
Error const& error() const { return m_value_or_error.template get<Error>(); }
|
||||
|
||||
bool is_error() const { return m_value_or_error.template has<Error>(); }
|
||||
|
||||
JsonValue release_value() { return move(value()); }
|
||||
Error release_error() { return move(error()); }
|
||||
|
||||
template<typename... Visitors>
|
||||
decltype(auto) visit(Visitors&&... visitors) const
|
||||
{
|
||||
return m_value_or_error.visit(forward<Visitors>(visitors)...);
|
||||
}
|
||||
|
||||
private:
|
||||
// Note: Empty is only a possible state until the Response has been decoded by IPC.
|
||||
Variant<Empty, JsonValue, Error> m_value_or_error;
|
||||
};
|
||||
|
||||
}
|
||||
|
||||
namespace IPC {
|
||||
|
||||
template<>
|
||||
ErrorOr<void> encode(Encoder&, Web::WebDriver::Response const&);
|
||||
|
||||
template<>
|
||||
ErrorOr<Web::WebDriver::Response> decode(Decoder&);
|
||||
|
||||
}
|
88
Libraries/LibWeb/WebDriver/Screenshot.cpp
Normal file
88
Libraries/LibWeb/WebDriver/Screenshot.cpp
Normal file
|
@ -0,0 +1,88 @@
|
|||
/*
|
||||
* Copyright (c) 2022-2024, Tim Flynn <trflynn89@ladybird.org>
|
||||
*
|
||||
* SPDX-License-Identifier: BSD-2-Clause
|
||||
*/
|
||||
|
||||
#include <LibGfx/Bitmap.h>
|
||||
#include <LibWeb/DOM/Document.h>
|
||||
#include <LibWeb/DOM/ElementFactory.h>
|
||||
#include <LibWeb/HTML/BrowsingContext.h>
|
||||
#include <LibWeb/HTML/HTMLCanvasElement.h>
|
||||
#include <LibWeb/HTML/TagNames.h>
|
||||
#include <LibWeb/HTML/TraversableNavigable.h>
|
||||
#include <LibWeb/Namespace.h>
|
||||
#include <LibWeb/Page/Page.h>
|
||||
#include <LibWeb/WebDriver/Screenshot.h>
|
||||
|
||||
namespace Web::WebDriver {
|
||||
|
||||
// https://w3c.github.io/webdriver/#dfn-draw-a-bounding-box-from-the-framebuffer
|
||||
ErrorOr<JS::NonnullGCPtr<HTML::HTMLCanvasElement>, WebDriver::Error> draw_bounding_box_from_the_framebuffer(HTML::BrowsingContext& browsing_context, DOM::Element& element, Gfx::IntRect rect)
|
||||
{
|
||||
// 1. If either the initial viewport's width or height is 0 CSS pixels, return error with error code unable to capture screen.
|
||||
auto viewport_rect = browsing_context.top_level_traversable()->viewport_rect();
|
||||
if (viewport_rect.is_empty())
|
||||
return Error::from_code(ErrorCode::UnableToCaptureScreen, "Viewport is empty"sv);
|
||||
|
||||
auto viewport_device_rect = browsing_context.page().enclosing_device_rect(viewport_rect).to_type<int>();
|
||||
|
||||
// 2. Let paint width be the initial viewport's width – min(rectangle x coordinate, rectangle x coordinate + rectangle width dimension).
|
||||
auto paint_width = viewport_device_rect.width() - min(rect.x(), rect.x() + rect.width());
|
||||
|
||||
// 3. Let paint height be the initial viewport's height – min(rectangle y coordinate, rectangle y coordinate + rectangle height dimension).
|
||||
auto paint_height = viewport_device_rect.height() - min(rect.y(), rect.y() + rect.height());
|
||||
|
||||
// 4. Let canvas be a new canvas element, and set its width and height to paint width and paint height, respectively.
|
||||
auto canvas_element = DOM::create_element(element.document(), HTML::TagNames::canvas, Namespace::HTML).release_value_but_fixme_should_propagate_errors();
|
||||
auto& canvas = verify_cast<HTML::HTMLCanvasElement>(*canvas_element);
|
||||
|
||||
// FIXME: Handle DevicePixelRatio in HiDPI mode.
|
||||
MUST(canvas.set_width(paint_width));
|
||||
MUST(canvas.set_height(paint_height));
|
||||
|
||||
// FIXME: 5. Let context, a canvas context mode, be the result of invoking the 2D context creation algorithm given canvas as the target.
|
||||
if (!canvas.allocate_painting_surface(paint_width, paint_height))
|
||||
return Error::from_code(ErrorCode::UnableToCaptureScreen, "Unable to create a screenshot bitmap"sv);
|
||||
|
||||
// 6. Complete implementation specific steps equivalent to drawing the region of the framebuffer specified by the following coordinates onto context:
|
||||
// - X coordinate: rectangle x coordinate
|
||||
// - Y coordinate: rectangle y coordinate
|
||||
// - Width: paint width
|
||||
// - Height: paint height
|
||||
Gfx::IntRect paint_rect { rect.x(), rect.y(), paint_width, paint_height };
|
||||
|
||||
auto bitmap = MUST(Gfx::Bitmap::create(Gfx::BitmapFormat::BGRA8888, Gfx::AlphaType::Premultiplied, canvas.surface()->size()));
|
||||
canvas.surface()->read_into_bitmap(*bitmap);
|
||||
auto backing_store = Web::Painting::BitmapBackingStore(bitmap);
|
||||
browsing_context.page().client().paint(paint_rect.to_type<Web::DevicePixels>(), backing_store);
|
||||
|
||||
// 7. Return success with canvas.
|
||||
return canvas;
|
||||
}
|
||||
|
||||
// https://w3c.github.io/webdriver/#dfn-encoding-a-canvas-as-base64
|
||||
Response encode_canvas_element(HTML::HTMLCanvasElement& canvas)
|
||||
{
|
||||
// FIXME: 1. If the canvas element’s bitmap’s origin-clean flag is set to false, return error with error code unable to capture screen.
|
||||
|
||||
// 2. If the canvas element’s bitmap has no pixels (i.e. either its horizontal dimension or vertical dimension is zero) then return error with error code unable to capture screen.
|
||||
if (canvas.surface()->size().is_empty())
|
||||
return Error::from_code(ErrorCode::UnableToCaptureScreen, "Captured screenshot is empty"sv);
|
||||
|
||||
// 3. Let file be a serialization of the canvas element’s bitmap as a file, using "image/png" as an argument.
|
||||
// 4. Let data url be a data: URL representing file. [RFC2397]
|
||||
auto data_url = canvas.to_data_url("image/png"sv, {});
|
||||
|
||||
// 5. Let index be the index of "," in data url.
|
||||
auto index = data_url.find_byte_offset(',');
|
||||
VERIFY(index.has_value());
|
||||
|
||||
// 6. Let encoded string be a substring of data url using (index + 1) as the start argument.
|
||||
auto encoded_string = MUST(data_url.substring_from_byte_offset(*index + 1));
|
||||
|
||||
// 7. Return success with data encoded string.
|
||||
return JsonValue { encoded_string.to_byte_string() };
|
||||
}
|
||||
|
||||
}
|
19
Libraries/LibWeb/WebDriver/Screenshot.h
Normal file
19
Libraries/LibWeb/WebDriver/Screenshot.h
Normal file
|
@ -0,0 +1,19 @@
|
|||
/*
|
||||
* Copyright (c) 2022-2024, Tim Flynn <trflynn89@ladybird.org>
|
||||
*
|
||||
* SPDX-License-Identifier: BSD-2-Clause
|
||||
*/
|
||||
|
||||
#pragma once
|
||||
|
||||
#include <LibGfx/Rect.h>
|
||||
#include <LibJS/Heap/GCPtr.h>
|
||||
#include <LibWeb/Forward.h>
|
||||
#include <LibWeb/WebDriver/Response.h>
|
||||
|
||||
namespace Web::WebDriver {
|
||||
|
||||
ErrorOr<JS::NonnullGCPtr<HTML::HTMLCanvasElement>, WebDriver::Error> draw_bounding_box_from_the_framebuffer(HTML::BrowsingContext&, DOM::Element&, Gfx::IntRect);
|
||||
Response encode_canvas_element(HTML::HTMLCanvasElement&);
|
||||
|
||||
}
|
93
Libraries/LibWeb/WebDriver/TimeoutsConfiguration.cpp
Normal file
93
Libraries/LibWeb/WebDriver/TimeoutsConfiguration.cpp
Normal file
|
@ -0,0 +1,93 @@
|
|||
/*
|
||||
* Copyright (c) 2022, Linus Groh <linusg@serenityos.org>
|
||||
*
|
||||
* SPDX-License-Identifier: BSD-2-Clause
|
||||
*/
|
||||
|
||||
#include <AK/JsonObject.h>
|
||||
#include <LibJS/Runtime/Value.h>
|
||||
#include <LibWeb/WebDriver/TimeoutsConfiguration.h>
|
||||
|
||||
namespace Web::WebDriver {
|
||||
|
||||
// https://w3c.github.io/webdriver/#dfn-timeouts-object
|
||||
JsonObject timeouts_object(TimeoutsConfiguration const& timeouts)
|
||||
{
|
||||
// 1. Let serialized be an empty map.
|
||||
JsonObject serialized;
|
||||
|
||||
// 2. Set serialized["script"] to timeouts' script timeout.
|
||||
serialized.set("script"sv, timeouts.script_timeout.has_value() ? *timeouts.script_timeout : JsonValue {});
|
||||
|
||||
// 3. Set serialized["pageLoad"] to timeouts' page load timeout.
|
||||
serialized.set("pageLoad"sv, timeouts.page_load_timeout.has_value() ? *timeouts.page_load_timeout : JsonValue {});
|
||||
|
||||
// 4. Set serialized["implicit"] to timeouts' implicit wait timeout.
|
||||
serialized.set("implicit"sv, timeouts.implicit_wait_timeout.has_value() ? *timeouts.implicit_wait_timeout : JsonValue {});
|
||||
|
||||
// 5. Return convert an Infra value to a JSON-compatible JavaScript value with serialized.
|
||||
return serialized;
|
||||
}
|
||||
|
||||
// https://w3c.github.io/webdriver/#dfn-deserialize-as-timeouts-configuration
|
||||
ErrorOr<TimeoutsConfiguration, Error> json_deserialize_as_a_timeouts_configuration(JsonValue const& timeouts)
|
||||
{
|
||||
// 2. Let configuration be a new timeouts configuration.
|
||||
TimeoutsConfiguration configuration {};
|
||||
|
||||
TRY(json_deserialize_as_a_timeouts_configuration_into(timeouts, configuration));
|
||||
|
||||
// 4. Return success with data configuration.
|
||||
return configuration;
|
||||
}
|
||||
|
||||
// https://w3c.github.io/webdriver/#dfn-deserialize-as-timeouts-configuration
|
||||
ErrorOr<void, Error> json_deserialize_as_a_timeouts_configuration_into(JsonValue const& timeouts, TimeoutsConfiguration& configuration)
|
||||
{
|
||||
// 1. Set timeouts to the result of converting a JSON-derived JavaScript value to an Infra value with timeouts.
|
||||
if (!timeouts.is_object())
|
||||
return Error::from_code(ErrorCode::InvalidArgument, "Payload is not a JSON object");
|
||||
|
||||
// 3. For each key → value in timeouts:
|
||||
TRY(timeouts.as_object().try_for_each_member([&](auto const& key, JsonValue const& value) -> ErrorOr<void, Error> {
|
||||
Optional<u64> parsed_value;
|
||||
|
||||
// 1. If «"script", "pageLoad", "implicit"» does not contain key, then continue.
|
||||
if (!key.is_one_of("script"sv, "pageLoad"sv, "implicit"sv))
|
||||
return {};
|
||||
|
||||
// 2. If value is neither null nor a number greater than or equal to 0 and less than or equal to the maximum
|
||||
// safe integer return error with error code invalid argument.
|
||||
if (!value.is_null()) {
|
||||
auto duration = value.get_integer<u64>();
|
||||
|
||||
if (!duration.has_value() || *duration > JS::MAX_ARRAY_LIKE_INDEX)
|
||||
return Error::from_code(ErrorCode::InvalidArgument, "Invalid timeout value");
|
||||
|
||||
parsed_value = static_cast<u64>(*duration);
|
||||
}
|
||||
|
||||
// 3. Run the substeps matching key:
|
||||
// -> "script"
|
||||
if (key == "script"sv) {
|
||||
// Set configuration's script timeout to value.
|
||||
configuration.script_timeout = parsed_value;
|
||||
}
|
||||
// -> "pageLoad"
|
||||
else if (key == "pageLoad"sv) {
|
||||
// Set configuration's page load timeout to value.
|
||||
configuration.page_load_timeout = parsed_value;
|
||||
}
|
||||
// -> "implicit"
|
||||
else if (key == "implicit"sv) {
|
||||
// Set configuration's implicit wait timeout to value.
|
||||
configuration.implicit_wait_timeout = parsed_value;
|
||||
}
|
||||
|
||||
return {};
|
||||
}));
|
||||
|
||||
return {};
|
||||
}
|
||||
|
||||
}
|
26
Libraries/LibWeb/WebDriver/TimeoutsConfiguration.h
Normal file
26
Libraries/LibWeb/WebDriver/TimeoutsConfiguration.h
Normal file
|
@ -0,0 +1,26 @@
|
|||
/*
|
||||
* Copyright (c) 2022, Linus Groh <linusg@serenityos.org>
|
||||
*
|
||||
* SPDX-License-Identifier: BSD-2-Clause
|
||||
*/
|
||||
|
||||
#pragma once
|
||||
|
||||
#include <AK/Forward.h>
|
||||
#include <AK/Optional.h>
|
||||
#include <LibWeb/WebDriver/Error.h>
|
||||
|
||||
namespace Web::WebDriver {
|
||||
|
||||
// https://w3c.github.io/webdriver/#dfn-timeouts-configuration
|
||||
struct TimeoutsConfiguration {
|
||||
Optional<u64> script_timeout { 30'000 };
|
||||
Optional<u64> page_load_timeout { 300'000 };
|
||||
Optional<u64> implicit_wait_timeout { 0 };
|
||||
};
|
||||
|
||||
JsonObject timeouts_object(TimeoutsConfiguration const&);
|
||||
ErrorOr<TimeoutsConfiguration, Error> json_deserialize_as_a_timeouts_configuration(JsonValue const&);
|
||||
ErrorOr<void, Error> json_deserialize_as_a_timeouts_configuration_into(JsonValue const&, TimeoutsConfiguration&);
|
||||
|
||||
}
|
Loading…
Add table
Add a link
Reference in a new issue