diff options
author | Timothy Flynn <trflynn89@pm.me> | 2022-11-17 12:57:14 -0500 |
---|---|---|
committer | Linus Groh <mail@linusgroh.de> | 2022-11-18 12:21:57 +0000 |
commit | e0c7b5747d5e7f082c253caf2ebe9f2ac653f2fd (patch) | |
tree | 9f6d26590bd7bb2dc83fab1fc0a30fc58a002219 /Userland | |
parent | 5b5b563968f45a7dfde0cb3ea9873a98900fa580 (diff) | |
download | serenity-e0c7b5747d5e7f082c253caf2ebe9f2ac653f2fd.zip |
LibWeb+WebDriver: Begin processing and matching WebDriver capabilities
Still some TODOs here:
* We don't handle all capabilities (e.g. proxy)
* We don't match the capabilities against the running browser
But this will parse the capabilities JSON object received from the
WebDriver client.
Diffstat (limited to 'Userland')
-rw-r--r-- | Userland/Libraries/LibWeb/CMakeLists.txt | 1 | ||||
-rw-r--r-- | Userland/Libraries/LibWeb/WebDriver/Capabilities.cpp | 254 | ||||
-rw-r--r-- | Userland/Libraries/LibWeb/WebDriver/Capabilities.h | 59 | ||||
-rw-r--r-- | Userland/Services/WebDriver/Client.cpp | 11 |
4 files changed, 321 insertions, 4 deletions
diff --git a/Userland/Libraries/LibWeb/CMakeLists.txt b/Userland/Libraries/LibWeb/CMakeLists.txt index 03e44cf3a8..b0464ba9fa 100644 --- a/Userland/Libraries/LibWeb/CMakeLists.txt +++ b/Userland/Libraries/LibWeb/CMakeLists.txt @@ -439,6 +439,7 @@ set(SOURCES WebAssembly/WebAssemblyTableConstructor.cpp WebAssembly/WebAssemblyTableObject.cpp WebAssembly/WebAssemblyTablePrototype.cpp + WebDriver/Capabilities.cpp WebDriver/Client.cpp WebDriver/ElementLocationStrategies.cpp WebDriver/Error.cpp diff --git a/Userland/Libraries/LibWeb/WebDriver/Capabilities.cpp b/Userland/Libraries/LibWeb/WebDriver/Capabilities.cpp new file mode 100644 index 0000000000..38e0df66ed --- /dev/null +++ b/Userland/Libraries/LibWeb/WebDriver/Capabilities.cpp @@ -0,0 +1,254 @@ +/* + * Copyright (c) 2022, Tim Flynn <trflynn89@serenityos.org> + * + * SPDX-License-Identifier: BSD-2-Clause + */ + +#include <AK/JsonArray.h> +#include <AK/JsonObject.h> +#include <AK/JsonValue.h> +#include <AK/Optional.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-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, 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 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, "browser_version"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, String::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)); + } + + // FIXME: -> name equals "proxy" + // FIXME: Let deserialized be the result of trying to deserialize as a proxy with argument 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. + // FIXME: -> name is the key of an extension capability + // FIXME: 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. + + // -> The remote end is an endpoint node + else { + // Return an error with error code invalid argument. + return Error::from_code(ErrorCode::InvalidArgument, String::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 const* primary_value = primary.get_ptr(name); + + // d. If primary value is not undefined, return an error with error code invalid argument. + if (primary_value != nullptr) + return Error::from_code(ErrorCode::InvalidArgument, String::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; +} + +// 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 const* capabilities_request_ptr = parameters.as_object().get_ptr("capabilities"sv); + if (!capabilities_request_ptr || !capabilities_request_ptr->is_object()) + return Error::from_code(ErrorCode::InvalidArgument, "Capabilities is not an object"sv); + + auto const& capabilities_request = capabilities_request_ptr->as_object(); + + // 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 const* capability = capabilities_request.get_ptr("alwaysMatch"sv)) { + // 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 const* capabilities = capabilities_request.get_ptr("firstMatch"sv)) { + // 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.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.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.append(move(merged)); + return {}; + })); + + // FIXME: 8. For each capabilities corresponding to an indexed property in merged capabilities: + // FIXME: a. Let matched capabilities be the result of trying to match capabilities with capabilities as an argument. + // FIXME: b. If matched capabilities is not null, return success with data matched capabilities. + + // For now, we just assume the first validated capabilties object is a match. + return merged_capabilities.take(0); + + // 9. Return success with data null. +} + +} diff --git a/Userland/Libraries/LibWeb/WebDriver/Capabilities.h b/Userland/Libraries/LibWeb/WebDriver/Capabilities.h new file mode 100644 index 0000000000..72153f688b --- /dev/null +++ b/Userland/Libraries/LibWeb/WebDriver/Capabilities.h @@ -0,0 +1,59 @@ +/* + * 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(); +} + +Response process_capabilities(JsonValue const& parameters); + +} diff --git a/Userland/Services/WebDriver/Client.cpp b/Userland/Services/WebDriver/Client.cpp index fa099e5048..24a777e3fb 100644 --- a/Userland/Services/WebDriver/Client.cpp +++ b/Userland/Services/WebDriver/Client.cpp @@ -11,6 +11,7 @@ #include <AK/Debug.h> #include <AK/JsonObject.h> #include <AK/JsonValue.h> +#include <LibWeb/WebDriver/Capabilities.h> #include <WebDriver/Client.h> namespace WebDriver { @@ -73,7 +74,7 @@ void Client::close_session(unsigned session_id) // 8.1 New Session, https://w3c.github.io/webdriver/#dfn-new-sessions // POST /session -Web::WebDriver::Response Client::new_session(Web::WebDriver::Parameters, JsonValue) +Web::WebDriver::Response Client::new_session(Web::WebDriver::Parameters, JsonValue payload) { dbgln_if(WEBDRIVER_DEBUG, "Handling POST /session"); @@ -90,10 +91,12 @@ Web::WebDriver::Response Client::new_session(Web::WebDriver::Parameters, JsonVal // FIXME: 3. If the maximum active sessions is equal to the length of the list of active sessions, // return error with error code session not created. - // FIXME: 4. Let capabilities be the result of trying to process capabilities with parameters as an argument. - auto capabilities = JsonObject {}; + // 4. Let capabilities be the result of trying to process capabilities with parameters as an argument. + auto capabilities = TRY(Web::WebDriver::process_capabilities(payload)); - // FIXME: 5. If capabilitiesās is null, return error with error code session not created. + // 5. If capabilitiesās is null, return error with error code session not created. + if (capabilities.is_null()) + return Web::WebDriver::Error::from_code(Web::WebDriver::ErrorCode::SessionNotCreated, "Could not match capabilities"sv); // 6. Let session id be the result of generating a UUID. // FIXME: Actually create a UUID. |