LibWeb: Do not run microtasks when the event loop is paused

For example, running `alert(1)` will pause the event loop, during which
time no JavaScript should execute. This patch extends this disruption to
microtasks. This avoids a crash inside the microtask executor, which
asserts the JS execution context stack is empty.

This makes us behave the same as Firefox in the following page:

    <script>
        queueMicrotask(() => {
            console.log("inside microtask");
        });

        alert("hi");
    </script>

Before the aforementioned assertion was added, we would execute that
microtask before showing the alert. Firefox does not do this, and now
we don't either.
This commit is contained in:
Timothy Flynn 2025-01-19 13:13:59 -05:00 committed by Tim Ledbetter
commit 43dc0f52a6
Notes: github-actions[bot] 2025-01-19 20:48:47 +00:00
4 changed files with 42 additions and 21 deletions

View file

@ -503,6 +503,9 @@ void perform_a_microtask_checkpoint()
// https://html.spec.whatwg.org/#perform-a-microtask-checkpoint // https://html.spec.whatwg.org/#perform-a-microtask-checkpoint
void EventLoop::perform_a_microtask_checkpoint() void EventLoop::perform_a_microtask_checkpoint()
{ {
if (execution_paused())
return;
// NOTE: This assertion is per requirement 9.5 of the ECMA-262 spec, see: https://tc39.es/ecma262/#sec-jobs // NOTE: This assertion is per requirement 9.5 of the ECMA-262 spec, see: https://tc39.es/ecma262/#sec-jobs
// > At some future point in time, when there is no running context in the agent for which the job is scheduled and that agent's execution context stack is empty... // > At some future point in time, when there is no running context in the agent for which the job is scheduled and that agent's execution context stack is empty...
VERIFY(vm().execution_context_stack().is_empty()); VERIFY(vm().execution_context_stack().is_empty());
@ -653,6 +656,8 @@ EventLoop::PauseHandle::~PauseHandle()
// https://html.spec.whatwg.org/multipage/webappapis.html#pause // https://html.spec.whatwg.org/multipage/webappapis.html#pause
EventLoop::PauseHandle EventLoop::pause() EventLoop::PauseHandle EventLoop::pause()
{ {
m_execution_paused = true;
// 1. Let global be the current global object. // 1. Let global be the current global object.
auto& global = current_principal_global_object(); auto& global = current_principal_global_object();
@ -667,7 +672,6 @@ EventLoop::PauseHandle EventLoop::pause()
// not run further tasks, and any script in the currently running task must block. User agents should remain // not run further tasks, and any script in the currently running task must block. User agents should remain
// responsive to user input while paused, however, albeit in a reduced capacity since the event loop will not be // responsive to user input while paused, however, albeit in a reduced capacity since the event loop will not be
// doing anything. // doing anything.
m_execution_paused = true;
return PauseHandle { *this, global, time_before_pause }; return PauseHandle { *this, global, time_before_pause };
} }

View file

@ -0,0 +1 @@
PASS (didn't crash)

View file

@ -0,0 +1,7 @@
<script src="include.js"></script>
<script>
test(() => {
alert("Well hello friends!");
println("PASS (didn't crash)");
});
</script>

View file

@ -379,6 +379,30 @@ static void run_test(HeadlessWebView& view, Test& test, Application& app)
view.on_text_test_finish = {}; view.on_text_test_finish = {};
promise->when_resolved([&view, &test, &app](auto) {
auto url = URL::create_with_file_scheme(MUST(FileSystem::real_path(test.input_path)));
switch (test.mode) {
case TestMode::Text:
case TestMode::Layout:
run_dump_test(view, test, url, app.per_test_timeout_in_seconds * 1000);
return;
case TestMode::Ref:
run_ref_test(view, test, url, app.per_test_timeout_in_seconds * 1000);
return;
case TestMode::Crash:
run_dump_test(view, test, url, app.per_test_timeout_in_seconds * 1000);
return;
}
VERIFY_NOT_REACHED();
});
view.load("about:blank"sv);
}
static void set_ui_callbacks_for_tests(HeadlessWebView& view)
{
view.on_request_file_picker = [&](auto const& accepted_file_types, auto allow_multiple_files) { view.on_request_file_picker = [&](auto const& accepted_file_types, auto allow_multiple_files) {
// Create some dummy files for tests. // Create some dummy files for tests.
Vector<Web::HTML::SelectedFile> selected_files; Vector<Web::HTML::SelectedFile> selected_files;
@ -420,26 +444,10 @@ static void run_test(HeadlessWebView& view, Test& test, Application& app)
view.file_picker_closed(move(selected_files)); view.file_picker_closed(move(selected_files));
}; };
promise->when_resolved([&view, &test, &app](auto) { view.on_request_alert = [&](auto const&) {
auto url = URL::create_with_file_scheme(MUST(FileSystem::real_path(test.input_path))); // For tests, just close the alert right away to unblock JS execution.
view.alert_closed();
switch (test.mode) { };
case TestMode::Text:
case TestMode::Layout:
run_dump_test(view, test, url, app.per_test_timeout_in_seconds * 1000);
return;
case TestMode::Ref:
run_ref_test(view, test, url, app.per_test_timeout_in_seconds * 1000);
return;
case TestMode::Crash:
run_dump_test(view, test, url, app.per_test_timeout_in_seconds * 1000);
return;
}
VERIFY_NOT_REACHED();
});
view.load("about:blank"sv);
} }
ErrorOr<void> run_tests(Core::AnonymousBuffer const& theme, Web::DevicePixelSize window_size) ErrorOr<void> run_tests(Core::AnonymousBuffer const& theme, Web::DevicePixelSize window_size)
@ -516,6 +524,7 @@ ErrorOr<void> run_tests(Core::AnonymousBuffer const& theme, Web::DevicePixelSize
Vector<TestCompletion> non_passing_tests; Vector<TestCompletion> non_passing_tests;
app.for_each_web_view([&](auto& view) { app.for_each_web_view([&](auto& view) {
set_ui_callbacks_for_tests(view);
view.clear_content_filters(); view.clear_content_filters();
auto run_next_test = [&]() { auto run_next_test = [&]() {