1
0
Fork 0
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:
Timothy Flynn 2024-11-09 12:25:08 -05:00 committed by Andreas Kling
parent 950e819ee7
commit 93712b24bf
Notes: github-actions[bot] 2024-11-10 11:51:52 +00:00
4547 changed files with 104 additions and 113 deletions

File diff suppressed because it is too large Load diff

View 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);
}

View 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 Apples 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 capabilitys 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 nodes 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();
}
}

View 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);
}

View 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());
}
}

View 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;
};
}

View 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 windows 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();
}
}

View 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&);
}

View 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();
}
}

View 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);
}

View 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 };
}
}

View 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);
}

View 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_))
{
}
}

View 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));
}
};

View 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 contexts 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 realms 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);
}
}

View 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);
}

View 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();
}
}

View 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 };
};
}

View 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;
}
}

View 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&);
}

View 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);
}
}

View 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&);
}

View 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);
}
}

View 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&);
}

View 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);
}
}

View 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();
}

View 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&);
}

View 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 elements bitmaps origin-clean flag is set to false, return error with error code unable to capture screen.
// 2. If the canvas elements 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 elements 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() };
}
}

View 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&);
}

View 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 {};
}
}

View 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&);
}