WebDriver: Handle script execution results without spinning event loops
We currently spin the platform event loop while awaiting scripts to complete. This causes WebContent to hang if another component is also spinning the event loop. The particular example that instigated this patch was the navigable's navigation loop (which spins until the fetch process is complete), triggered by a form submission to an iframe. So instead of spinning, we now return immediately from the script executors, after setting up listeners for either the script's promise to be resolved or for a timeout. The HTTP request to WebDriver must finish synchronously though, so now the WebDriver process spins its event loop until WebContent signals that the script completed. This should be ok - the WebDriver process isn't expected to be doing anything else in the meantime. Also, as a consequence of these changes, we now actually handle time outs. We were previously creating the timeout timer, but not starting it.
This commit is contained in:
parent
6f31a19c5f
commit
c2cf65adac
Notes:
github-actions[bot]
2024-09-13 14:12:19 +00:00
Author: https://github.com/trflynn89 Commit: https://github.com/LadybirdBrowser/ladybird/commit/c2cf65adac7 Pull-request: https://github.com/LadybirdBrowser/ladybird/pull/1389 Reviewed-by: https://github.com/ADKaster Reviewed-by: https://github.com/awesomekling ✅
10 changed files with 242 additions and 124 deletions
|
@ -283,7 +283,63 @@ static JS::ThrowCompletionOr<JS::Value> execute_a_function_body(Web::Page& page,
|
|||
return completion;
|
||||
}
|
||||
|
||||
ExecuteScriptResultSerialized execute_script(Web::Page& page, ByteString const& body, JS::MarkedVector<JS::Value> arguments, Optional<u64> const& timeout_ms)
|
||||
class HeapTimer : public JS::Cell {
|
||||
JS_CELL(HeapTimer, JS::Cell);
|
||||
JS_DECLARE_ALLOCATOR(HeapTimer);
|
||||
|
||||
public:
|
||||
explicit HeapTimer()
|
||||
: m_timer(Core::Timer::create())
|
||||
{
|
||||
}
|
||||
|
||||
virtual ~HeapTimer() override = default;
|
||||
|
||||
void start(u64 timeout_ms, JS::NonnullGCPtr<OnScriptComplete> on_timeout)
|
||||
{
|
||||
m_on_timeout = on_timeout;
|
||||
|
||||
m_timer->on_timeout = [this]() {
|
||||
m_timed_out = true;
|
||||
|
||||
if (m_on_timeout) {
|
||||
auto error_object = JsonObject {};
|
||||
error_object.set("name", "Error");
|
||||
error_object.set("message", "Script Timeout");
|
||||
|
||||
m_on_timeout->function()({ ExecuteScriptResultType::Timeout, move(error_object) });
|
||||
m_on_timeout = nullptr;
|
||||
}
|
||||
};
|
||||
|
||||
m_timer->set_interval(static_cast<int>(timeout_ms));
|
||||
m_timer->set_single_shot(true);
|
||||
m_timer->start();
|
||||
}
|
||||
|
||||
void stop()
|
||||
{
|
||||
m_on_timeout = nullptr;
|
||||
m_timer->stop();
|
||||
}
|
||||
|
||||
bool is_timed_out() const { return m_timed_out; }
|
||||
|
||||
private:
|
||||
virtual void visit_edges(JS::Cell::Visitor& visitor) override
|
||||
{
|
||||
Base::visit_edges(visitor);
|
||||
visitor.visit(m_on_timeout);
|
||||
}
|
||||
|
||||
NonnullRefPtr<Core::Timer> m_timer;
|
||||
JS::GCPtr<OnScriptComplete> m_on_timeout;
|
||||
bool m_timed_out { false };
|
||||
};
|
||||
|
||||
JS_DEFINE_ALLOCATOR(HeapTimer);
|
||||
|
||||
void execute_script(Web::Page& page, ByteString body, JS::MarkedVector<JS::Value> arguments, Optional<u64> const& timeout_ms, JS::NonnullGCPtr<OnScriptComplete> on_complete)
|
||||
{
|
||||
auto* document = page.top_level_browsing_context().active_document();
|
||||
auto* window = page.top_level_browsing_context().active_window();
|
||||
|
@ -291,28 +347,25 @@ ExecuteScriptResultSerialized execute_script(Web::Page& page, ByteString const&
|
|||
auto& vm = window->vm();
|
||||
|
||||
// 5. Let timer be a new timer.
|
||||
auto timeout_flag = false;
|
||||
auto timer = Core::Timer::create();
|
||||
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->on_timeout = [&]() {
|
||||
timeout_flag = true;
|
||||
};
|
||||
timer->set_interval(timeout_ms.value());
|
||||
timer->set_single_shot(true);
|
||||
timer->start(timeout_ms.value(), on_complete);
|
||||
}
|
||||
|
||||
// AD-HOC: An execution context is required for Promise creation hooks.
|
||||
HTML::TemporaryExecutionContext execution_context { document->relevant_settings_object() };
|
||||
HTML::TemporaryExecutionContext execution_context { document->relevant_settings_object(), 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([&realm, &page, promise_capability, promise, body = move(body), arguments = move(arguments)]() mutable {
|
||||
Platform::EventLoopPlugin::the().deferred_invoke([&realm, &page, promise_capability, document, promise, body = move(body), arguments = move(arguments)]() mutable {
|
||||
HTML::TemporaryExecutionContext execution_context { document->relevant_settings_object() };
|
||||
|
||||
// 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(page, body, move(arguments));
|
||||
|
||||
|
@ -328,40 +381,40 @@ ExecuteScriptResultSerialized execute_script(Web::Page& page, ByteString const&
|
|||
});
|
||||
|
||||
// 9. Wait until promise is resolved, or timer's timeout fired flag is set, whichever occurs first.
|
||||
vm.custom_data()->spin_event_loop_until([&] {
|
||||
return timeout_flag || promise->state() != JS::Promise::State::Pending;
|
||||
auto reaction_steps = JS::create_heap_function(vm.heap(), [&realm, promise, timer, on_complete](JS::Value) -> WebIDL::ExceptionOr<JS::Value> {
|
||||
if (timer->is_timed_out())
|
||||
return JS::js_undefined();
|
||||
timer->stop();
|
||||
|
||||
auto json_value_or_error = json_clone(realm, promise->result());
|
||||
if (json_value_or_error.is_error()) {
|
||||
auto error_object = JsonObject {};
|
||||
error_object.set("name", "Error");
|
||||
error_object.set("message", "Could not clone result value");
|
||||
|
||||
on_complete->function()({ ExecuteScriptResultType::JavaScriptError, move(error_object) });
|
||||
}
|
||||
|
||||
// 10. If promise is still pending and timer's timeout fired flag is set, return error with error code script timeout.
|
||||
// NOTE: This is handled by the HeapTimer.
|
||||
|
||||
// 11. If promise is fulfilled with value v, let result be JSON clone with session and v, and return success with data result.
|
||||
else if (promise->state() == JS::Promise::State::Fulfilled) {
|
||||
on_complete->function()({ ExecuteScriptResultType::PromiseResolved, json_value_or_error.release_value() });
|
||||
}
|
||||
|
||||
// 12. If promise is rejected with reason r, let result be JSON clone with session and r, and return error with error code javascript error and data result.
|
||||
else if (promise->state() == JS::Promise::State::Rejected) {
|
||||
on_complete->function()({ ExecuteScriptResultType::PromiseRejected, json_value_or_error.release_value() });
|
||||
}
|
||||
|
||||
return JS::js_undefined();
|
||||
});
|
||||
|
||||
// 10. If promise is still pending and timer's timeout fired flag is set, return error with error code script timeout.
|
||||
if (timeout_flag && promise->state() == JS::Promise::State::Pending) {
|
||||
auto error_object = JsonObject {};
|
||||
error_object.set("name", "Error");
|
||||
error_object.set("message", "Script Timeout");
|
||||
return { ExecuteScriptResultType::Timeout, move(error_object) };
|
||||
}
|
||||
|
||||
auto json_value_or_error = json_clone(realm, promise->result());
|
||||
if (json_value_or_error.is_error()) {
|
||||
auto error_object = JsonObject {};
|
||||
error_object.set("name", "Error");
|
||||
error_object.set("message", "Could not clone result value");
|
||||
return { ExecuteScriptResultType::JavaScriptError, move(error_object) };
|
||||
}
|
||||
|
||||
// 11. If promise is fulfilled with value v, let result be JSON clone with session and v, and return success with data result.
|
||||
if (promise->state() == JS::Promise::State::Fulfilled) {
|
||||
return { ExecuteScriptResultType::PromiseResolved, json_value_or_error.release_value() };
|
||||
}
|
||||
|
||||
// 12. If promise is rejected with reason r, let result be JSON clone with session and r, and return error with error code javascript error and data result.
|
||||
if (promise->state() == JS::Promise::State::Rejected) {
|
||||
return { ExecuteScriptResultType::PromiseRejected, json_value_or_error.release_value() };
|
||||
}
|
||||
|
||||
VERIFY_NOT_REACHED();
|
||||
WebIDL::react_to_promise(promise_capability, reaction_steps, reaction_steps);
|
||||
}
|
||||
|
||||
ExecuteScriptResultSerialized execute_async_script(Web::Page& page, ByteString const& body, JS::MarkedVector<JS::Value> arguments, Optional<u64> const& timeout_ms)
|
||||
void execute_async_script(Web::Page& page, ByteString body, JS::MarkedVector<JS::Value> arguments, Optional<u64> const& timeout_ms, JS::NonnullGCPtr<OnScriptComplete> on_complete)
|
||||
{
|
||||
auto* document = page.top_level_browsing_context().active_document();
|
||||
auto* window = page.top_level_browsing_context().active_window();
|
||||
|
@ -369,28 +422,25 @@ ExecuteScriptResultSerialized execute_async_script(Web::Page& page, ByteString c
|
|||
auto& vm = window->vm();
|
||||
|
||||
// 5. Let timer be a new timer.
|
||||
IGNORE_USE_IN_ESCAPING_LAMBDA auto timeout_flag = false;
|
||||
auto timer = Core::Timer::create();
|
||||
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->on_timeout = [&]() {
|
||||
timeout_flag = true;
|
||||
};
|
||||
timer->set_interval(timeout_ms.value());
|
||||
timer->set_single_shot(true);
|
||||
timer->start(timeout_ms.value(), on_complete);
|
||||
}
|
||||
|
||||
// AD-HOC: An execution context is required for Promise creation hooks.
|
||||
HTML::TemporaryExecutionContext execution_context { document->relevant_settings_object() };
|
||||
HTML::TemporaryExecutionContext execution_context { document->relevant_settings_object(), 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([&vm, &realm, &page, &timeout_flag, promise_capability, promise, body = move(body), arguments = move(arguments)]() mutable {
|
||||
Platform::EventLoopPlugin::the().deferred_invoke([&vm, &realm, &page, timer, document, promise_capability, promise, body = move(body), arguments = move(arguments)]() mutable {
|
||||
HTML::TemporaryExecutionContext execution_context { document->relevant_settings_object() };
|
||||
|
||||
// 1. Let resolvingFunctions be CreateResolvingFunctions(promise).
|
||||
auto resolving_functions = promise->create_resolving_functions();
|
||||
|
||||
|
@ -434,7 +484,7 @@ ExecuteScriptResultSerialized execute_async_script(Web::Page& page, ByteString c
|
|||
auto& script_promise = static_cast<JS::Promise&>(*script_promise_or_error.value());
|
||||
|
||||
vm.custom_data()->spin_event_loop_until([&] {
|
||||
return timeout_flag || script_promise.state() != JS::Promise::State::Pending;
|
||||
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.
|
||||
|
@ -447,37 +497,37 @@ ExecuteScriptResultSerialized execute_async_script(Web::Page& page, ByteString c
|
|||
});
|
||||
|
||||
// 9. Wait until promise is resolved, or timer's timeout fired flag is set, whichever occurs first.
|
||||
vm.custom_data()->spin_event_loop_until([&] {
|
||||
return timeout_flag || promise->state() != JS::Promise::State::Pending;
|
||||
auto reaction_steps = JS::create_heap_function(vm.heap(), [&realm, promise, timer, on_complete](JS::Value) -> WebIDL::ExceptionOr<JS::Value> {
|
||||
if (timer->is_timed_out())
|
||||
return JS::js_undefined();
|
||||
timer->stop();
|
||||
|
||||
auto json_value_or_error = json_clone(realm, promise->result());
|
||||
if (json_value_or_error.is_error()) {
|
||||
auto error_object = JsonObject {};
|
||||
error_object.set("name", "Error");
|
||||
error_object.set("message", "Could not clone result value");
|
||||
|
||||
on_complete->function()({ ExecuteScriptResultType::JavaScriptError, move(error_object) });
|
||||
}
|
||||
|
||||
// 10. If promise is still pending and timer's timeout fired flag is set, return error with error code script timeout.
|
||||
// NOTE: This is handled by the HeapTimer.
|
||||
|
||||
// 11. If promise is fulfilled with value v, let result be JSON clone with session and v, and return success with data result.
|
||||
else if (promise->state() == JS::Promise::State::Fulfilled) {
|
||||
on_complete->function()({ ExecuteScriptResultType::PromiseResolved, json_value_or_error.release_value() });
|
||||
}
|
||||
|
||||
// 12. If promise is rejected with reason r, let result be JSON clone with session and r, and return error with error code javascript error and data result.
|
||||
else if (promise->state() == JS::Promise::State::Rejected) {
|
||||
on_complete->function()({ ExecuteScriptResultType::PromiseRejected, json_value_or_error.release_value() });
|
||||
}
|
||||
|
||||
return JS::js_undefined();
|
||||
});
|
||||
|
||||
// 10. If promise is still pending and timer's timeout fired flag is set, return error with error code script
|
||||
if (timeout_flag && promise->state() == JS::Promise::State::Pending) {
|
||||
auto error_object = JsonObject {};
|
||||
error_object.set("name", "Error");
|
||||
error_object.set("message", "Script Timeout");
|
||||
return { ExecuteScriptResultType::Timeout, move(error_object) };
|
||||
}
|
||||
|
||||
auto json_value_or_error = json_clone(realm, promise->result());
|
||||
if (json_value_or_error.is_error()) {
|
||||
auto error_object = JsonObject {};
|
||||
error_object.set("name", "Error");
|
||||
error_object.set("message", "Could not clone result value");
|
||||
return { ExecuteScriptResultType::JavaScriptError, move(error_object) };
|
||||
}
|
||||
|
||||
// 11. If promise is fulfilled with value v, let result be JSON clone with session and v, and return success with data result.
|
||||
if (promise->state() == JS::Promise::State::Fulfilled) {
|
||||
return { ExecuteScriptResultType::PromiseResolved, json_value_or_error.release_value() };
|
||||
}
|
||||
|
||||
// 12. If promise is rejected with reason r, let result be JSON clone with session and r, and return error with error code javascript error and data result.
|
||||
if (promise->state() == JS::Promise::State::Rejected) {
|
||||
return { ExecuteScriptResultType::PromiseRejected, json_value_or_error.release_value() };
|
||||
}
|
||||
|
||||
VERIFY_NOT_REACHED();
|
||||
WebIDL::react_to_promise(promise_capability, reaction_steps, reaction_steps);
|
||||
}
|
||||
|
||||
}
|
||||
|
|
|
@ -9,6 +9,7 @@
|
|||
#include <AK/Forward.h>
|
||||
#include <AK/JsonValue.h>
|
||||
#include <LibJS/Forward.h>
|
||||
#include <LibJS/Heap/HeapFunction.h>
|
||||
#include <LibJS/Runtime/Value.h>
|
||||
#include <LibWeb/Forward.h>
|
||||
|
||||
|
@ -32,7 +33,9 @@ struct ExecuteScriptResultSerialized {
|
|||
JsonValue value;
|
||||
};
|
||||
|
||||
ExecuteScriptResultSerialized execute_script(Page& page, ByteString const& body, JS::MarkedVector<JS::Value> arguments, Optional<u64> const& timeout_ms);
|
||||
ExecuteScriptResultSerialized execute_async_script(Page& page, ByteString const& body, JS::MarkedVector<JS::Value> arguments, Optional<u64> const& timeout_ms);
|
||||
using OnScriptComplete = JS::HeapFunction<void(ExecuteScriptResultSerialized)>;
|
||||
|
||||
void execute_script(Page& page, ByteString body, JS::MarkedVector<JS::Value> arguments, Optional<u64> const& timeout_ms, JS::NonnullGCPtr<OnScriptComplete> on_complete);
|
||||
void execute_async_script(Page& page, ByteString body, JS::MarkedVector<JS::Value> arguments, Optional<u64> const& timeout_ms, JS::NonnullGCPtr<OnScriptComplete> on_complete);
|
||||
|
||||
}
|
||||
|
|
|
@ -1845,8 +1845,11 @@ Messages::WebDriverClient::GetSourceResponse WebDriverConnection::get_source()
|
|||
// 13.2.1 Execute Script, https://w3c.github.io/webdriver/#dfn-execute-script
|
||||
Messages::WebDriverClient::ExecuteScriptResponse WebDriverConnection::execute_script(JsonValue const& payload)
|
||||
{
|
||||
auto* window = m_page_client->page().top_level_browsing_context().active_window();
|
||||
auto& vm = window->vm();
|
||||
|
||||
// 1. Let body and arguments be the result of trying to extract the script arguments from a request with argument parameters.
|
||||
auto const& [body, arguments] = TRY(extract_the_script_arguments_from_a_request(payload));
|
||||
auto [body, arguments] = TRY(extract_the_script_arguments_from_a_request(vm, payload));
|
||||
|
||||
// 2. If the current browsing context is no longer open, return error with error code no such window.
|
||||
TRY(ensure_open_top_level_browsing_context());
|
||||
|
@ -1858,32 +1861,43 @@ Messages::WebDriverClient::ExecuteScriptResponse WebDriverConnection::execute_sc
|
|||
auto timeout_ms = m_timeouts_configuration.script_timeout;
|
||||
|
||||
// This handles steps 5 to 9 and produces the appropriate result type for the following steps.
|
||||
auto result = Web::WebDriver::execute_script(m_page_client->page(), body, move(arguments), timeout_ms);
|
||||
dbgln_if(WEBDRIVER_DEBUG, "Executing script returned: {}", result.value);
|
||||
Web::WebDriver::execute_script(m_page_client->page(), move(body), move(arguments), timeout_ms, JS::create_heap_function(vm.heap(), [&](Web::WebDriver::ExecuteScriptResultSerialized result) {
|
||||
dbgln_if(WEBDRIVER_DEBUG, "Executing script returned: {}", result.value);
|
||||
Web::WebDriver::Response response;
|
||||
|
||||
switch (result.type) {
|
||||
// 10. If promise is still pending and the session script timeout is reached, return error with error code script timeout.
|
||||
case Web::WebDriver::ExecuteScriptResultType::Timeout:
|
||||
return Web::WebDriver::Error::from_code(Web::WebDriver::ErrorCode::ScriptTimeoutError, "Script timed out");
|
||||
// 11. Upon fulfillment of promise with value v, let result be a JSON clone of v, and return success with data result.
|
||||
case Web::WebDriver::ExecuteScriptResultType::PromiseResolved:
|
||||
return move(result.value);
|
||||
// 12. Upon rejection of promise with reason r, let result be a JSON clone of r, and return error with error code javascript error and data result.
|
||||
case Web::WebDriver::ExecuteScriptResultType::PromiseRejected:
|
||||
case Web::WebDriver::ExecuteScriptResultType::JavaScriptError:
|
||||
return Web::WebDriver::Error::from_code(Web::WebDriver::ErrorCode::JavascriptError, "Script returned an error", move(result.value));
|
||||
case Web::WebDriver::ExecuteScriptResultType::BrowsingContextDiscarded:
|
||||
return Web::WebDriver::Error::from_code(Web::WebDriver::ErrorCode::StaleElementReference, "Browsing context has been discarded", move(result.value));
|
||||
}
|
||||
switch (result.type) {
|
||||
// 10. If promise is still pending and the session script timeout is reached, return error with error code script timeout.
|
||||
case Web::WebDriver::ExecuteScriptResultType::Timeout:
|
||||
response = Web::WebDriver::Error::from_code(Web::WebDriver::ErrorCode::ScriptTimeoutError, "Script timed out");
|
||||
break;
|
||||
// 11. Upon fulfillment of promise with value v, let result be a JSON clone of v, and return success with data result.
|
||||
case Web::WebDriver::ExecuteScriptResultType::PromiseResolved:
|
||||
response = move(result.value);
|
||||
break;
|
||||
// 12. Upon rejection of promise with reason r, let result be a JSON clone of r, and return error with error code javascript error and data result.
|
||||
case Web::WebDriver::ExecuteScriptResultType::PromiseRejected:
|
||||
case Web::WebDriver::ExecuteScriptResultType::JavaScriptError:
|
||||
response = Web::WebDriver::Error::from_code(Web::WebDriver::ErrorCode::JavascriptError, "Script returned an error", move(result.value));
|
||||
break;
|
||||
case Web::WebDriver::ExecuteScriptResultType::BrowsingContextDiscarded:
|
||||
response = Web::WebDriver::Error::from_code(Web::WebDriver::ErrorCode::StaleElementReference, "Browsing context has been discarded", move(result.value));
|
||||
break;
|
||||
}
|
||||
|
||||
VERIFY_NOT_REACHED();
|
||||
async_script_executed(move(response));
|
||||
}));
|
||||
|
||||
return JsonValue {};
|
||||
}
|
||||
|
||||
// 13.2.2 Execute Async Script, https://w3c.github.io/webdriver/#dfn-execute-async-script
|
||||
Messages::WebDriverClient::ExecuteAsyncScriptResponse WebDriverConnection::execute_async_script(JsonValue const& payload)
|
||||
{
|
||||
auto* window = m_page_client->page().top_level_browsing_context().active_window();
|
||||
auto& vm = window->vm();
|
||||
|
||||
// 1. Let body and arguments by the result of trying to extract the script arguments from a request with argument parameters.
|
||||
auto const& [body, arguments] = TRY(extract_the_script_arguments_from_a_request(payload));
|
||||
auto [body, arguments] = TRY(extract_the_script_arguments_from_a_request(vm, payload));
|
||||
|
||||
// 2. If the current browsing context is no longer open, return error with error code no such window.
|
||||
TRY(ensure_open_top_level_browsing_context());
|
||||
|
@ -1895,25 +1909,33 @@ Messages::WebDriverClient::ExecuteAsyncScriptResponse WebDriverConnection::execu
|
|||
auto timeout_ms = m_timeouts_configuration.script_timeout;
|
||||
|
||||
// This handles steps 5 to 9 and produces the appropriate result type for the following steps.
|
||||
auto result = Web::WebDriver::execute_async_script(m_page_client->page(), body, move(arguments), timeout_ms);
|
||||
dbgln_if(WEBDRIVER_DEBUG, "Executing async script returned: {}", result.value);
|
||||
Web::WebDriver::execute_async_script(m_page_client->page(), move(body), move(arguments), timeout_ms, JS::create_heap_function(vm.heap(), [&](Web::WebDriver::ExecuteScriptResultSerialized result) {
|
||||
dbgln_if(WEBDRIVER_DEBUG, "Executing async script returned: {}", result.value);
|
||||
Web::WebDriver::Response response;
|
||||
|
||||
switch (result.type) {
|
||||
// 10. If promise is still pending and the session script timeout is reached, return error with error code script timeout.
|
||||
case Web::WebDriver::ExecuteScriptResultType::Timeout:
|
||||
return Web::WebDriver::Error::from_code(Web::WebDriver::ErrorCode::ScriptTimeoutError, "Script timed out");
|
||||
// 11. Upon fulfillment of promise with value v, let result be a JSON clone of v, and return success with data result.
|
||||
case Web::WebDriver::ExecuteScriptResultType::PromiseResolved:
|
||||
return move(result.value);
|
||||
// 12. Upon rejection of promise with reason r, let result be a JSON clone of r, and return error with error code javascript error and data result.
|
||||
case Web::WebDriver::ExecuteScriptResultType::PromiseRejected:
|
||||
case Web::WebDriver::ExecuteScriptResultType::JavaScriptError:
|
||||
return Web::WebDriver::Error::from_code(Web::WebDriver::ErrorCode::JavascriptError, "Script returned an error", move(result.value));
|
||||
case Web::WebDriver::ExecuteScriptResultType::BrowsingContextDiscarded:
|
||||
return Web::WebDriver::Error::from_code(Web::WebDriver::ErrorCode::StaleElementReference, "Browsing context has been discarded", move(result.value));
|
||||
}
|
||||
switch (result.type) {
|
||||
// 10. If promise is still pending and the session script timeout is reached, return error with error code script timeout.
|
||||
case Web::WebDriver::ExecuteScriptResultType::Timeout:
|
||||
response = Web::WebDriver::Error::from_code(Web::WebDriver::ErrorCode::ScriptTimeoutError, "Script timed out");
|
||||
break;
|
||||
// 11. Upon fulfillment of promise with value v, let result be a JSON clone of v, and return success with data result.
|
||||
case Web::WebDriver::ExecuteScriptResultType::PromiseResolved:
|
||||
response = move(result.value);
|
||||
break;
|
||||
// 12. Upon rejection of promise with reason r, let result be a JSON clone of r, and return error with error code javascript error and data result.
|
||||
case Web::WebDriver::ExecuteScriptResultType::PromiseRejected:
|
||||
case Web::WebDriver::ExecuteScriptResultType::JavaScriptError:
|
||||
response = Web::WebDriver::Error::from_code(Web::WebDriver::ErrorCode::JavascriptError, "Script returned an error", move(result.value));
|
||||
break;
|
||||
case Web::WebDriver::ExecuteScriptResultType::BrowsingContextDiscarded:
|
||||
response = Web::WebDriver::Error::from_code(Web::WebDriver::ErrorCode::StaleElementReference, "Browsing context has been discarded", move(result.value));
|
||||
break;
|
||||
}
|
||||
|
||||
VERIFY_NOT_REACHED();
|
||||
async_script_executed(move(response));
|
||||
}));
|
||||
|
||||
return JsonValue {};
|
||||
}
|
||||
|
||||
// 14.1 Get All Cookies, https://w3c.github.io/webdriver/#dfn-get-all-cookies
|
||||
|
@ -2487,11 +2509,8 @@ ErrorOr<JsonArray, Web::WebDriver::Error> WebDriverConnection::find(StartNodeGet
|
|||
}
|
||||
|
||||
// https://w3c.github.io/webdriver/#dfn-extract-the-script-arguments-from-a-request
|
||||
ErrorOr<WebDriverConnection::ScriptArguments, Web::WebDriver::Error> WebDriverConnection::extract_the_script_arguments_from_a_request(JsonValue const& payload)
|
||||
ErrorOr<WebDriverConnection::ScriptArguments, Web::WebDriver::Error> WebDriverConnection::extract_the_script_arguments_from_a_request(JS::VM& vm, JsonValue const& payload)
|
||||
{
|
||||
auto* window = m_page_client->page().top_level_browsing_context().active_window();
|
||||
auto& vm = window->vm();
|
||||
|
||||
// 1. Let script be the result of getting a property named script from the parameters.
|
||||
// 2. If script is not a String, return error with error code invalid argument.
|
||||
auto script = TRY(get_property(payload, "script"sv));
|
||||
|
|
|
@ -120,7 +120,7 @@ private:
|
|||
ByteString script;
|
||||
JS::MarkedVector<JS::Value> arguments;
|
||||
};
|
||||
ErrorOr<ScriptArguments, Web::WebDriver::Error> extract_the_script_arguments_from_a_request(JsonValue const& payload);
|
||||
static ErrorOr<ScriptArguments, Web::WebDriver::Error> extract_the_script_arguments_from_a_request(JS::VM&, JsonValue const& payload);
|
||||
void delete_cookies(Optional<StringView> const& name = {});
|
||||
|
||||
JS::NonnullGCPtr<Web::PageClient> m_page_client;
|
||||
|
|
|
@ -1,2 +1,5 @@
|
|||
#include <LibWeb/WebDriver/Response.h>
|
||||
|
||||
endpoint WebDriverServer {
|
||||
script_executed(Web::WebDriver::Response response) =|
|
||||
}
|
||||
|
|
|
@ -620,7 +620,7 @@ Web::WebDriver::Response Client::execute_script(Web::WebDriver::Parameters param
|
|||
{
|
||||
dbgln_if(WEBDRIVER_DEBUG, "Handling POST /session/<session_id>/execute/sync");
|
||||
auto session = TRY(find_session_with_id(parameters[0]));
|
||||
return session->web_content_connection().execute_script(payload);
|
||||
return session->execute_script(move(payload), Session::ScriptMode::Sync);
|
||||
}
|
||||
|
||||
// 13.2.2 Execute Async Script, https://w3c.github.io/webdriver/#dfn-execute-async-script
|
||||
|
@ -629,7 +629,7 @@ Web::WebDriver::Response Client::execute_async_script(Web::WebDriver::Parameters
|
|||
{
|
||||
dbgln_if(WEBDRIVER_DEBUG, "Handling POST /session/<session_id>/execute/async");
|
||||
auto session = TRY(find_session_with_id(parameters[0]));
|
||||
return session->web_content_connection().execute_async_script(payload);
|
||||
return session->execute_script(move(payload), Session::ScriptMode::Async);
|
||||
}
|
||||
|
||||
// 14.1 Get All Cookies, https://w3c.github.io/webdriver/#dfn-get-all-cookies
|
||||
|
|
|
@ -12,6 +12,7 @@
|
|||
#include "Client.h"
|
||||
#include <AK/JsonObject.h>
|
||||
#include <AK/ScopeGuard.h>
|
||||
#include <LibCore/EventLoop.h>
|
||||
#include <LibCore/LocalServer.h>
|
||||
#include <LibCore/StandardPaths.h>
|
||||
#include <LibCore/System.h>
|
||||
|
@ -168,4 +169,29 @@ Web::WebDriver::Response Session::get_window_handles() const
|
|||
return JsonValue { move(handles) };
|
||||
}
|
||||
|
||||
Web::WebDriver::Response Session::execute_script(JsonValue payload, ScriptMode mode) const
|
||||
{
|
||||
ScopeGuard guard { [&]() { web_content_connection().on_script_executed = nullptr; } };
|
||||
|
||||
Optional<Web::WebDriver::Response> response;
|
||||
web_content_connection().on_script_executed = [&](auto result) {
|
||||
response = move(result);
|
||||
};
|
||||
|
||||
switch (mode) {
|
||||
case ScriptMode::Sync:
|
||||
TRY(web_content_connection().execute_script(move(payload)));
|
||||
break;
|
||||
case ScriptMode::Async:
|
||||
TRY(web_content_connection().execute_async_script(move(payload)));
|
||||
break;
|
||||
}
|
||||
|
||||
Core::EventLoop::current().spin_until([&]() {
|
||||
return response.has_value();
|
||||
});
|
||||
|
||||
return response.release_value();
|
||||
}
|
||||
|
||||
}
|
||||
|
|
|
@ -9,6 +9,7 @@
|
|||
#pragma once
|
||||
|
||||
#include <AK/Error.h>
|
||||
#include <AK/JsonValue.h>
|
||||
#include <AK/RefCounted.h>
|
||||
#include <AK/RefPtr.h>
|
||||
#include <AK/String.h>
|
||||
|
@ -53,6 +54,12 @@ public:
|
|||
Web::WebDriver::Response switch_to_window(StringView);
|
||||
Web::WebDriver::Response get_window_handles() const;
|
||||
|
||||
enum class ScriptMode {
|
||||
Sync,
|
||||
Async,
|
||||
};
|
||||
Web::WebDriver::Response execute_script(JsonValue, ScriptMode) const;
|
||||
|
||||
private:
|
||||
using ServerPromise = Core::Promise<ErrorOr<void>>;
|
||||
ErrorOr<NonnullRefPtr<Core::LocalServer>> create_server(NonnullRefPtr<ServerPromise> promise);
|
||||
|
|
|
@ -20,4 +20,10 @@ void WebContentConnection::die()
|
|||
on_close();
|
||||
}
|
||||
|
||||
void WebContentConnection::script_executed(Web::WebDriver::Response const& response)
|
||||
{
|
||||
if (on_script_executed)
|
||||
on_script_executed(response);
|
||||
}
|
||||
|
||||
}
|
||||
|
|
|
@ -21,8 +21,12 @@ public:
|
|||
WebContentConnection(NonnullOwnPtr<Core::LocalSocket> socket);
|
||||
|
||||
Function<void()> on_close;
|
||||
Function<void(Web::WebDriver::Response)> on_script_executed;
|
||||
|
||||
private:
|
||||
virtual void die() override;
|
||||
|
||||
virtual void script_executed(Web::WebDriver::Response const&) override;
|
||||
};
|
||||
|
||||
}
|
||||
|
|
Loading…
Add table
Reference in a new issue