LibWebView: Keep browser history in the UI process

Use the LibWebView history mirror to preserve traversable session
history across WebContent process swaps. WebContent reports snapshots to
the UI process, and new renderers can be seeded from the mirror.

Browser back and forward now resolve through the UI-owned used history
steps. WebContent still runs the spec traversal path when the current
renderer has enough matching state to do so.

Handle canceled and no-op UI navigations without leaving speculative
history entries or pending WebDriver waits behind. Preserve traversal
precheck state across synchronous IPC shutdown, and avoid overwriting a
restored target entry's persisted scroll state before the document has
adopted that entry.
This commit is contained in:
Andreas Kling 2026-06-13 16:06:49 +02:00 committed by Andreas Kling
parent 44ae6d3438
commit 24f37c6732
50 changed files with 3252 additions and 355 deletions

View file

@ -222,6 +222,7 @@
#include <LibWeb/UIEvents/PointerTypes.h> #include <LibWeb/UIEvents/PointerTypes.h>
#include <LibWeb/UIEvents/TextEvent.h> #include <LibWeb/UIEvents/TextEvent.h>
#include <LibWeb/ViewTransition/ViewTransition.h> #include <LibWeb/ViewTransition/ViewTransition.h>
#include <LibWeb/WebDriver/UserPrompt.h>
#include <LibWeb/WebIDL/AbstractOperations.h> #include <LibWeb/WebIDL/AbstractOperations.h>
#include <LibWeb/WebIDL/DOMException.h> #include <LibWeb/WebIDL/DOMException.h>
#include <LibWeb/WebIDL/ExceptionOr.h> #include <LibWeb/WebIDL/ExceptionOr.h>
@ -6652,7 +6653,9 @@ void Document::update_for_history_step_application(NonnullRefPtr<HTML::SessionHi
auto pop_state_event = HTML::PopStateEvent::create(realm(), "popstate"_fly_string, popstate_event_init); auto pop_state_event = HTML::PopStateEvent::create(realm(), "popstate"_fly_string, popstate_event_init);
relevant_global_object.dispatch_event(pop_state_event); relevant_global_object.dispatch_event(pop_state_event);
// FIXME: 4. Restore persisted state given entry. // 4. Restore persisted state given entry.
if (auto navigable = this->navigable())
navigable->restore_persisted_state_from_session_history_entry(*entry);
// 5. If oldURL's fragment is not equal to entry's URL's fragment, then queue a global task on the DOM manipulation task source // 5. If oldURL's fragment is not equal to entry's URL's fragment, then queue a global task on the DOM manipulation task source
// given document's relevant global object to fire an event named hashchange at document's relevant global object, // given document's relevant global object to fire an event named hashchange at document's relevant global object,
@ -6674,7 +6677,9 @@ void Document::update_for_history_step_application(NonnullRefPtr<HTML::SessionHi
// 1. Assert: entriesForNavigationAPI is given. // 1. Assert: entriesForNavigationAPI is given.
VERIFY(entries_for_navigation_api.has_value()); VERIFY(entries_for_navigation_api.has_value());
// FIXME: 2. Restore persisted state given entry. // 2. Restore persisted state given entry.
if (auto navigable = this->navigable())
navigable->restore_persisted_state_from_session_history_entry(*entry);
// 3. Initialize the navigation API entries for a new document given navigation, entriesForNavigationAPI, and entry. // 3. Initialize the navigation API entries for a new document given navigation, entriesForNavigationAPI, and entry.
navigation->initialize_the_navigation_api_entries_for_a_new_document(*entries_for_navigation_api, entry); navigation->initialize_the_navigation_api_entries_for_a_new_document(*entries_for_navigation_api, entry);
@ -8805,8 +8810,8 @@ Document::StepsToFireBeforeunloadResult Document::steps_to_fire_beforeunload(boo
// 5. Decrease document's relevant agent's event loop's termination nesting level by 1. // 5. Decrease document's relevant agent's event loop's termination nesting level by 1.
event_loop.decrement_termination_nesting_level(); event_loop.decrement_termination_nesting_level();
// FIXME: 6. If all of the following are true: // 6. If all of the following are true:
if (false && if (
// - unloadPromptShown is false; // - unloadPromptShown is false;
!unload_prompt_shown !unload_prompt_shown
// - document's active sandboxing flag set does not have its sandboxed modals flag set; // - document's active sandboxing flag set does not have its sandboxed modals flag set;
@ -8817,10 +8822,18 @@ Document::StepsToFireBeforeunloadResult Document::steps_to_fire_beforeunload(boo
&& (!event_firing_result || !beforeunload_event->return_value().is_empty()) && (!event_firing_result || !beforeunload_event->return_value().is_empty())
// - FIXME: showing an unload prompt is unlikely to be annoying, deceptive, or pointless // - FIXME: showing an unload prompt is unlikely to be annoying, deceptive, or pointless
) { ) {
// FIXME: 1. Set unloadPromptShown to true. // 1. Set unloadPromptShown to true.
unload_prompt_shown = true;
// FIXME: 2. Invoke WebDriver BiDi user prompt opened with document's relevant global object, "beforeunload", and "". // FIXME: 2. Invoke WebDriver BiDi user prompt opened with document's relevant global object, "beforeunload", and "".
// FIXME: 3. Ask the user to confirm that they wish to unload the document, and pause while waiting for the user's response. // FIXME: 3. Ask the user to confirm that they wish to unload the document, and pause while waiting for the user's response.
// FIXME: 4. If the user did not confirm the page navigation, set unloadPromptCanceled to true.
auto user_prompt_handler = WebDriver::get_the_prompt_handler(WebDriver::PromptType::BeforeUnload);
// 4. If the user did not confirm the page navigation, set unloadPromptCanceled to true.
if (user_prompt_handler.handler == WebDriver::PromptHandler::Dismiss)
unload_prompt_canceled = true;
// FIXME: 5. Invoke WebDriver BiDi user prompt closed with document's relevant global object and true if unloadPromptCanceled is false or false otherwise. // FIXME: 5. Invoke WebDriver BiDi user prompt closed with document's relevant global object and true if unloadPromptCanceled is false or false otherwise.
} }

View file

@ -864,6 +864,7 @@ public:
void set_deferred_parser_start(GC::Ref<GC::Function<void()>>); void set_deferred_parser_start(GC::Ref<GC::Function<void()>>);
bool has_deferred_parser_start() const { return m_deferred_parser_start; } bool has_deferred_parser_start() const { return m_deferred_parser_start; }
RefPtr<HTML::SessionHistoryEntry> latest_entry() const { return m_latest_entry; }
void set_latest_entry(RefPtr<HTML::SessionHistoryEntry>); void set_latest_entry(RefPtr<HTML::SessionHistoryEntry>);
void element_id_changed(Badge<DOM::Element>, GC::Ref<DOM::Element> element, Optional<FlyString> old_id); void element_id_changed(Badge<DOM::Element>, GC::Ref<DOM::Element> element, Optional<FlyString> old_id);

View file

@ -59,7 +59,12 @@ namespace Web {
static void dump_session_history_entry(StringBuilder& builder, HTML::SessionHistoryEntry const& session_history_entry, int indent_levels) static void dump_session_history_entry(StringBuilder& builder, HTML::SessionHistoryEntry const& session_history_entry, int indent_levels)
{ {
dump_indent(builder, indent_levels); dump_indent(builder, indent_levels);
builder.appendff("step=({}) url=({})\n", session_history_entry.step().get<int>(), session_history_entry.url()); builder.appendff("step=({}) url=({})", session_history_entry.step().get<int>(), session_history_entry.url());
if (session_history_entry.scroll_position_data().viewport_scroll_position.has_value()) {
auto const& viewport_scroll_position = *session_history_entry.scroll_position_data().viewport_scroll_position;
builder.appendff(" viewport-scroll=({}, {})", viewport_scroll_position.x(), viewport_scroll_position.y());
}
builder.append('\n');
for (auto const& nested_history : session_history_entry.document_state()->nested_histories()) { for (auto const& nested_history : session_history_entry.document_state()->nested_histories()) {
for (auto const& nested_she : nested_history.entries) { for (auto const& nested_she : nested_history.entries) {
dump_session_history_entry(builder, *nested_she, indent_levels + 1); dump_session_history_entry(builder, *nested_she, indent_levels + 1);

View file

@ -851,7 +851,7 @@ ErrorOr<void> HTMLFormElement::submit_as_entity_body(URL::URL parsed_action, GC:
// 2. Let mimeType be the isomorphic encoding of the concatenation of "multipart/form-data; boundary=" and the multipart/form-data // 2. Let mimeType be the isomorphic encoding of the concatenation of "multipart/form-data; boundary=" and the multipart/form-data
// boundary string generated by the multipart/form-data encoding algorithm. // boundary string generated by the multipart/form-data encoding algorithm.
mime_type = POSTResource::RequestContentType::MultipartFormData; mime_type = POSTResource::RequestContentType::MultipartFormData;
mime_type_directives.empend("boundary"sv, move(body_and_mime_type.boundary)); mime_type_directives.empend(TRY(String::from_utf8("boundary"sv)), move(body_and_mime_type.boundary));
break; break;
} }
case EncodingTypeAttributeState::PlainText: { case EncodingTypeAttributeState::PlainText: {

View file

@ -473,7 +473,8 @@ RefPtr<SessionHistoryEntry> Navigable::get_the_target_history_entry(int target_s
// https://html.spec.whatwg.org/multipage/browsing-the-web.html#activate-history-entry // https://html.spec.whatwg.org/multipage/browsing-the-web.html#activate-history-entry
void Navigable::activate_history_entry(RefPtr<SessionHistoryEntry> entry, GC::Ref<DOM::Document> document) void Navigable::activate_history_entry(RefPtr<SessionHistoryEntry> entry, GC::Ref<DOM::Document> document)
{ {
// FIXME: 1. Save persisted state to the navigable's active session history entry. // 1. Save persisted state to the navigable's active session history entry.
save_persisted_state_to_active_session_history_entry();
// 2. Let newDocument be entry's document. // 2. Let newDocument be entry's document.
auto new_document = document; auto new_document = document;
@ -523,6 +524,51 @@ void Navigable::activate_history_entry(RefPtr<SessionHistoryEntry> entry, GC::Re
} }
} }
// https://html.spec.whatwg.org/multipage/browsing-the-web.html#save-persisted-state
void Navigable::save_persisted_state_to_active_session_history_entry()
{
auto entry = active_session_history_entry();
if (!entry)
return;
// 1. Set the scroll position data of entry to contain the scroll positions for all of entry's document's
// restorable scrollable regions.
auto scroll_position_data = entry->scroll_position_data();
scroll_position_data.viewport_scroll_position = viewport_scroll_offset();
entry->set_scroll_position_data(move(scroll_position_data));
// FIXME: 2. Optionally, update entry's persisted user state.
}
// https://html.spec.whatwg.org/multipage/browsing-the-web.html#restore-persisted-user-state
void Navigable::restore_persisted_state_from_session_history_entry(SessionHistoryEntry const& entry)
{
// 1. If entry's scroll restoration mode is "auto", and entry's document's relevant global object's navigation
// API's suppress normal scroll restoration during ongoing navigation is false, then restore scroll position
// data given entry.
if (entry.scroll_restoration_mode() == ScrollRestorationMode::Auto) {
if (auto window = active_window()) {
if (!window->navigation()->suppress_normal_scroll_restoration_during_ongoing_navigation())
restore_scroll_position_data(entry);
}
}
// FIXME: 2. Optionally, update other aspects of entry's document and its rendering, for instance values of form
// fields, that the user agent had previously recorded in entry's persisted user state.
}
// https://html.spec.whatwg.org/multipage/browsing-the-web.html#restore-scroll-position-data
void Navigable::restore_scroll_position_data(SessionHistoryEntry const& entry)
{
auto const& scroll_position_data = entry.scroll_position_data();
if (!scroll_position_data.viewport_scroll_position.has_value())
return;
// FIXME: If the document has been scrolled by the user, return.
perform_scroll_of_viewport_scrolling_box(*scroll_position_data.viewport_scroll_position);
clamp_viewport_scroll_offset();
}
// https://html.spec.whatwg.org/multipage/document-sequences.html#nav-document // https://html.spec.whatwg.org/multipage/document-sequences.html#nav-document
GC::Ptr<DOM::Document> Navigable::active_document() const GC::Ptr<DOM::Document> Navigable::active_document() const
{ {
@ -1838,6 +1884,30 @@ void Navigable::populate_session_history_entry_document(
} }
} }
static Bindings::NavigationHistoryBehavior determine_history_handling_for_navigation(Bindings::NavigationHistoryBehavior history_handling, URL::URL const& url, DOM::Document const& active_document, URL::Origin const& initiator_origin_snapshot)
{
// 12. If historyHandling is "auto", then:
if (history_handling == Bindings::NavigationHistoryBehavior::Auto) {
// NB: The spec says "targetNavigable" here, but this algorithm has a "navigable".
// 1. If url equals navigable's active document's URL,
// and initiatorOriginSnapshot is same origin with targetNavigable's active document's origin,
// then set historyHandling to "replace".
if (url == active_document.url() && initiator_origin_snapshot.is_same_origin(active_document.origin()))
history_handling = Bindings::NavigationHistoryBehavior::Replace;
// 2. Otherwise, set historyHandling to "push".
else
history_handling = Bindings::NavigationHistoryBehavior::Push;
}
// 13. If the navigation must be a replace given url and navigable's active document, then set historyHandling to
// "replace".
if (navigation_must_be_a_replace(url, active_document))
history_handling = Bindings::NavigationHistoryBehavior::Replace;
return history_handling;
}
WebIDL::ExceptionOr<void> Navigable::navigate(NavigateParams params) WebIDL::ExceptionOr<void> Navigable::navigate(NavigateParams params)
{ {
// AD-HOC: Not in the spec but subsequent steps will fail if the navigable doesn't have an active window. // AD-HOC: Not in the spec but subsequent steps will fail if the navigable doesn't have an active window.
@ -1849,13 +1919,6 @@ WebIDL::ExceptionOr<void> Navigable::navigate(NavigateParams params)
auto& active_document = *this->active_document(); auto& active_document = *this->active_document();
auto& realm = active_document.realm(); auto& realm = active_document.realm();
auto& page_client = active_document.page().client();
// AD-HOC: If we are not able to continue in this process, request a new process from the UI.
if (is_top_level_traversable() && !page_client.is_url_suitable_for_same_process_navigation(active_document.url(), params.url)) {
page_client.request_new_process_for_navigation(params.url);
return {};
}
// 2. Let sourceSnapshotParams be the result of snapshotting source snapshot params given sourceDocument. // 2. Let sourceSnapshotParams be the result of snapshotting source snapshot params given sourceDocument.
auto source_snapshot_params = source_document->snapshot_source_snapshot_params(); auto source_snapshot_params = source_document->snapshot_source_snapshot_params();
@ -1994,23 +2057,8 @@ void Navigable::begin_navigation(NavigateParams params)
} }
} }
// 12. If historyHandling is "auto", then: // 12-13. Determine historyHandling for this navigation.
if (history_handling == Bindings::NavigationHistoryBehavior::Auto) { history_handling = determine_history_handling_for_navigation(history_handling, url, active_document, initiator_origin_snapshot);
// FIXME: Fix spec typo targetNavigable --> navigable
// 1. If url equals navigable's active document's URL,
// and initiatorOriginSnapshot is same origin with targetNavigable's active document's origin,
// then set historyHandling to "replace".
if (url == active_document.url() && initiator_origin_snapshot.is_same_origin(active_document.origin()))
history_handling = Bindings::NavigationHistoryBehavior::Replace;
// 2. Otherwise, set historyHandling to "push".
else
history_handling = Bindings::NavigationHistoryBehavior::Push;
}
// 13. If the navigation must be a replace given url and navigable's active document, then set historyHandling to "replace".
if (navigation_must_be_a_replace(url, active_document))
history_handling = Bindings::NavigationHistoryBehavior::Replace;
// 14. If all of the following are true: // 14. If all of the following are true:
// - documentResource is null; // - documentResource is null;
@ -2113,11 +2161,6 @@ void Navigable::begin_navigation(NavigateParams params)
return; return;
} }
// AD-HOC: Tell the UI that we started loading.
if (is_top_level_traversable()) {
active_browsing_context()->page().client().page_did_start_loading(url, false);
}
// FIXME: 22. If sourceDocument is navigable's container document, then reserve deferred fetch quota for navigable's container given url's origin. // FIXME: 22. If sourceDocument is navigable's container document, then reserve deferred fetch quota for navigable's container given url's origin.
// 23. In parallel, run these steps: // 23. In parallel, run these steps:
@ -2132,15 +2175,34 @@ void Navigable::begin_navigation(NavigateParams params)
traversable_navigable()->check_if_unloading_is_canceled(this->active_document()->inclusive_descendant_navigables(), traversable_navigable()->check_if_unloading_is_canceled(this->active_document()->inclusive_descendant_navigables(),
GC::create_function(heap(), [this, source_snapshot_params, target_snapshot_params, csp_navigation_type, document_resource, url, navigation_id, referrer_policy, initiator_origin_snapshot, response, history_handling, initiator_base_url_snapshot, user_involvement](TraversableNavigable::CheckIfUnloadingIsCanceledResult unload_prompt_canceled) { GC::create_function(heap(), [this, source_snapshot_params, target_snapshot_params, csp_navigation_type, document_resource, url, navigation_id, referrer_policy, initiator_origin_snapshot, response, history_handling, initiator_base_url_snapshot, user_involvement](TraversableNavigable::CheckIfUnloadingIsCanceledResult unload_prompt_canceled) {
// 2. If unloadPromptCanceled is not "continue", or navigable's ongoing navigation is no longer navigationId: // 2. If unloadPromptCanceled is not "continue", or navigable's ongoing navigation is no longer navigationId:
if (unload_prompt_canceled != TraversableNavigable::CheckIfUnloadingIsCanceledResult::Continue || ongoing_navigation() != navigation_id) { if (unload_prompt_canceled != TraversableNavigable::CheckIfUnloadingIsCanceledResult::Continue) {
// FIXME: 1. Invoke WebDriver BiDi navigation failed with navigable and a new WebDriver BiDi navigation status whose id is navigationId, status is "canceled", and url is url. // FIXME: 1. Invoke WebDriver BiDi navigation failed with navigable and a new WebDriver BiDi navigation status whose id is navigationId, status is "canceled", and url is url.
if (is_top_level_traversable())
active_browsing_context()->page().client().page_did_cancel_loading(url);
// 2. Abort these steps. // 2. Abort these steps.
set_delaying_load_events(false); set_delaying_load_events(false);
return; return;
} }
// AD-HOC: Not in the spec but subsequent steps will fail if the navigable doesn't have an active window. if (ongoing_navigation() != navigation_id) {
set_delaying_load_events(false);
return;
}
// AD-HOC: If we are not able to continue in this process, request a new process from the UI.
if (is_top_level_traversable() && !active_browsing_context()->page().client().is_url_suitable_for_same_process_navigation(this->active_document()->url(), url)) {
active_browsing_context()->page().client().request_new_process_for_navigation(url, document_resource, history_handling);
set_delaying_load_events(false);
return;
}
// AD-HOC: Tell the UI that we started loading.
if (is_top_level_traversable()) {
active_browsing_context()->page().client().page_did_start_loading(url, document_resource, false, history_handling);
}
// AD-HOC: Subsequent steps will fail if the navigable doesn't have an active window.
if (!active_window()) { if (!active_window()) {
set_delaying_load_events(false); set_delaying_load_events(false);
return; return;
@ -2327,6 +2389,9 @@ void Navigable::navigate_to_a_fragment(URL::URL const& url, HistoryHandlingBehav
if (!continue_) if (!continue_)
return; return;
save_persisted_state_to_active_session_history_entry();
auto active_entry = active_session_history_entry();
// 6. Let historyEntry be a new session history entry, with // 6. Let historyEntry be a new session history entry, with
// URL: url // URL: url
// document state: navigable's active session history entry's document state // document state: navigable's active session history entry's document state
@ -2334,12 +2399,13 @@ void Navigable::navigate_to_a_fragment(URL::URL const& url, HistoryHandlingBehav
// scroll restoration mode: navigable's active session history entry's scroll restoration mode // scroll restoration mode: navigable's active session history entry's scroll restoration mode
auto history_entry = SessionHistoryEntry::create(); auto history_entry = SessionHistoryEntry::create();
history_entry->set_url(url); history_entry->set_url(url);
history_entry->set_document_state(active_session_history_entry()->document_state()); history_entry->set_document_state(active_entry->document_state());
history_entry->set_navigation_api_state(destination_navigation_api_state); history_entry->set_navigation_api_state(destination_navigation_api_state);
history_entry->set_scroll_restoration_mode(active_session_history_entry()->scroll_restoration_mode()); history_entry->set_scroll_restoration_mode(active_entry->scroll_restoration_mode());
history_entry->set_scroll_position_data(active_entry->scroll_position_data());
// 7. Let entryToReplace be navigable's active session history entry if historyHandling is "replace", otherwise null. // 7. Let entryToReplace be navigable's active session history entry if historyHandling is "replace", otherwise null.
auto entry_to_replace = history_handling == HistoryHandlingBehavior::Replace ? active_session_history_entry() : nullptr; auto entry_to_replace = history_handling == HistoryHandlingBehavior::Replace ? active_entry : nullptr;
// 8. Let history be navigable's active document's history object. // 8. Let history be navigable's active document's history object.
auto history = active_document()->history(); auto history = active_document()->history();
@ -2641,6 +2707,13 @@ void Navigable::reload(Optional<SerializationRecord> navigation_api_state, UserN
// 3. Let traversable be navigable's traversable navigable. // 3. Let traversable be navigable's traversable navigable.
auto traversable = traversable_navigable(); auto traversable = traversable_navigable();
// AD-HOC: Report the reload-pending document state to the UI process before the reload history step finishes,
// so the UI-owned session history mirror remains synchronized during an in-flight reload.
if (traversable->page().client().should_report_session_history_updates()) {
auto session_history_snapshot = traversable->create_session_history_snapshot();
traversable->page().client().page_did_update_session_history(session_history_snapshot.top_level_session_history_entries, session_history_snapshot.used_session_history_steps, session_history_snapshot.current_used_step_index);
}
// 4. Append the following session history traversal steps to traversable: // 4. Append the following session history traversal steps to traversable:
traversable->append_session_history_traversal_steps(GC::create_function(heap(), [traversable, user_involvement](NonnullRefPtr<Core::Promise<Empty>> signal) { traversable->append_session_history_traversal_steps(GC::create_function(heap(), [traversable, user_involvement](NonnullRefPtr<Core::Promise<Empty>> signal) {
// 1. Apply the reload history step to traversable given userInvolvement. // 1. Apply the reload history step to traversable given userInvolvement.
@ -2832,6 +2905,7 @@ void perform_url_and_history_update_steps(DOM::Document& document, URL::URL new_
// 2. Let activeEntry be navigable's active session history entry. // 2. Let activeEntry be navigable's active session history entry.
auto active_entry = navigable->active_session_history_entry(); auto active_entry = navigable->active_session_history_entry();
navigable->save_persisted_state_to_active_session_history_entry();
// 3. Let newEntry be a new session history entry, with // 3. Let newEntry be a new session history entry, with
// URL: newURL // URL: newURL
@ -2844,6 +2918,7 @@ void perform_url_and_history_update_steps(DOM::Document& document, URL::URL new_
new_entry->set_classic_history_api_state(serialized_data.value_or(active_entry->classic_history_api_state())); new_entry->set_classic_history_api_state(serialized_data.value_or(active_entry->classic_history_api_state()));
new_entry->set_document_state(active_entry->document_state()); new_entry->set_document_state(active_entry->document_state());
new_entry->set_scroll_restoration_mode(active_entry->scroll_restoration_mode()); new_entry->set_scroll_restoration_mode(active_entry->scroll_restoration_mode());
new_entry->set_scroll_position_data(active_entry->scroll_position_data());
// 4. If document's is initial about:blank is true, then set historyHandling to "replace". // 4. If document's is initial about:blank is true, then set historyHandling to "replace".
if (document.is_initial_about_blank()) { if (document.is_initial_about_blank()) {

View file

@ -115,6 +115,10 @@ public:
RefPtr<SessionHistoryEntry> get_the_target_history_entry(int target_step) const; RefPtr<SessionHistoryEntry> get_the_target_history_entry(int target_step) const;
void save_persisted_state_to_active_session_history_entry();
void restore_persisted_state_from_session_history_entry(SessionHistoryEntry const&);
void restore_scroll_position_data(SessionHistoryEntry const&);
String target_name() const; String target_name() const;
GC::Ptr<NavigableContainer> container() const; GC::Ptr<NavigableContainer> container() const;

View file

@ -15,6 +15,7 @@
#include <LibWeb/DOM/AbortSignal.h> #include <LibWeb/DOM/AbortSignal.h>
#include <LibWeb/DOM/Document.h> #include <LibWeb/DOM/Document.h>
#include <LibWeb/HTML/Focus.h> #include <LibWeb/HTML/Focus.h>
#include <LibWeb/HTML/Navigable.h>
#include <LibWeb/HTML/NavigateEvent.h> #include <LibWeb/HTML/NavigateEvent.h>
#include <LibWeb/HTML/Navigation.h> #include <LibWeb/HTML/Navigation.h>
#include <LibWeb/HTML/NavigationDestination.h> #include <LibWeb/HTML/NavigationDestination.h>
@ -184,10 +185,13 @@ void NavigateEvent::process_scroll_behavior()
// 2. Set event's interception state to "scrolled". // 2. Set event's interception state to "scrolled".
m_interception_state = InterceptionState::Scrolled; m_interception_state = InterceptionState::Scrolled;
// FIXME: 3. If event's navigationType was initialized to "traverse" or "reload", then restore scroll position data // 3. If event's navigationType was initialized to "traverse" or "reload", then restore scroll position data
// given event's relevant global object's navigable's active session history entry. // given event's relevant global object's navigable's active session history entry.
if (m_navigation_type == Bindings::NavigationType::Traverse || m_navigation_type == Bindings::NavigationType::Reload) { if (m_navigation_type == Bindings::NavigationType::Traverse || m_navigation_type == Bindings::NavigationType::Reload) {
dbgln("FIXME: restore scroll position data after traversal or reload navigation"); auto navigable = as<Window>(HTML::relevant_global_object(*this)).navigable();
VERIFY(navigable);
if (auto active_entry = navigable->active_session_history_entry())
navigable->restore_scroll_position_data(*active_entry);
} }
// 4. Otherwise: // 4. Otherwise:

View file

@ -758,7 +758,7 @@ void Navigation::abort_the_ongoing_navigation(GC::Ptr<WebIDL::DOMException> erro
m_focus_changed_during_ongoing_navigation = false; m_focus_changed_during_ongoing_navigation = false;
// 4. Set navigation's suppress normal scroll restoration during ongoing navigation to false. // 4. Set navigation's suppress normal scroll restoration during ongoing navigation to false.
m_suppress_scroll_restoration_during_ongoing_navigation = false; m_suppress_normal_scroll_restoration_during_ongoing_navigation = false;
// 5. If error was not given, then let error be a new "AbortError" DOMException created in navigation's relevant realm. // 5. If error was not given, then let error be a new "AbortError" DOMException created in navigation's relevant realm.
if (!error) if (!error)
@ -1193,7 +1193,7 @@ bool Navigation::inner_navigate_event_firing_algorithm(
m_focus_changed_during_ongoing_navigation = false; m_focus_changed_during_ongoing_navigation = false;
// 28. Set navigation's suppress normal scroll restoration during ongoing navigation to false. // 28. Set navigation's suppress normal scroll restoration during ongoing navigation to false.
m_suppress_scroll_restoration_during_ongoing_navigation = false; m_suppress_normal_scroll_restoration_during_ongoing_navigation = false;
// 29. Let dispatchResult be the result of dispatching event at navigation. // 29. Let dispatchResult be the result of dispatching event at navigation.
auto dispatch_result = dispatch_event(*event); auto dispatch_result = dispatch_event(*event);
@ -1254,7 +1254,7 @@ bool Navigation::inner_navigate_event_firing_algorithm(
// the relevant NavigateEvent. Otherwise, there will be no scroll restoration. That is, no navigation which is intercepted // the relevant NavigateEvent. Otherwise, there will be no scroll restoration. That is, no navigation which is intercepted
// by intercept() goes through the normal scroll restoration process; scroll restoration for such navigations // by intercept() goes through the normal scroll restoration process; scroll restoration for such navigations
// is either done manually, by the web developer, or is done after the transition. // is either done manually, by the web developer, or is done after the transition.
m_suppress_scroll_restoration_during_ongoing_navigation = true; m_suppress_normal_scroll_restoration_during_ongoing_navigation = true;
// 2. Let userInvolvement be "none". // 2. Let userInvolvement be "none".
auto user_involvement_for_resume = UserNavigationInvolvement::None; auto user_involvement_for_resume = UserNavigationInvolvement::None;
@ -1495,6 +1495,13 @@ void Navigation::initialize_the_navigation_api_entries_for_a_new_document(Vector
m_current_entry_index = get_the_navigation_api_entry_index(*initial_she); m_current_entry_index = get_the_navigation_api_entry_index(*initial_she);
} }
void Navigation::initialize_the_navigation_api_entries_for_reconstructed_session_history(Vector<NonnullRefPtr<SessionHistoryEntry>> const& new_shes, NonnullRefPtr<SessionHistoryEntry> initial_she)
{
m_entry_list.clear();
m_current_entry_index = -1;
initialize_the_navigation_api_entries_for_a_new_document(new_shes, move(initial_she));
}
// https://html.spec.whatwg.org/multipage/nav-history-apis.html#update-the-navigation-api-entries-for-a-same-document-navigation // https://html.spec.whatwg.org/multipage/nav-history-apis.html#update-the-navigation-api-entries-for-a-same-document-navigation
void Navigation::update_the_navigation_api_entries_for_a_same_document_navigation(NonnullRefPtr<SessionHistoryEntry> destination_she, Bindings::NavigationType navigation_type) void Navigation::update_the_navigation_api_entries_for_a_same_document_navigation(NonnullRefPtr<SessionHistoryEntry> destination_she, Bindings::NavigationType navigation_type)
{ {

View file

@ -97,6 +97,7 @@ public:
bool fire_a_download_request_navigate_event(URL::URL destination_url, UserNavigationInvolvement user_involvement, GC::Ptr<DOM::Element> source_element, String filename); bool fire_a_download_request_navigate_event(URL::URL destination_url, UserNavigationInvolvement user_involvement, GC::Ptr<DOM::Element> source_element, String filename);
void initialize_the_navigation_api_entries_for_a_new_document(Vector<NonnullRefPtr<SessionHistoryEntry>> const& new_shes, NonnullRefPtr<SessionHistoryEntry> initial_she); void initialize_the_navigation_api_entries_for_a_new_document(Vector<NonnullRefPtr<SessionHistoryEntry>> const& new_shes, NonnullRefPtr<SessionHistoryEntry> initial_she);
void initialize_the_navigation_api_entries_for_reconstructed_session_history(Vector<NonnullRefPtr<SessionHistoryEntry>> const& new_shes, NonnullRefPtr<SessionHistoryEntry> initial_she);
void update_the_navigation_api_entries_for_a_same_document_navigation(NonnullRefPtr<SessionHistoryEntry> destination_she, Bindings::NavigationType); void update_the_navigation_api_entries_for_a_same_document_navigation(NonnullRefPtr<SessionHistoryEntry> destination_she, Bindings::NavigationType);
virtual ~Navigation() override; virtual ~Navigation() override;
@ -107,6 +108,8 @@ public:
bool focus_changed_during_ongoing_navigation() const { return m_focus_changed_during_ongoing_navigation; } bool focus_changed_during_ongoing_navigation() const { return m_focus_changed_during_ongoing_navigation; }
void set_focus_changed_during_ongoing_navigation(bool b) { m_focus_changed_during_ongoing_navigation = b; } void set_focus_changed_during_ongoing_navigation(bool b) { m_focus_changed_during_ongoing_navigation = b; }
bool suppress_normal_scroll_restoration_during_ongoing_navigation() const { return m_suppress_normal_scroll_restoration_during_ongoing_navigation; }
void set_was_initial_about_blank_opened(bool b) { m_was_initial_about_blank_opened = b; } void set_was_initial_about_blank_opened(bool b) { m_was_initial_about_blank_opened = b; }
private: private:
@ -156,7 +159,7 @@ private:
bool m_focus_changed_during_ongoing_navigation { false }; bool m_focus_changed_during_ongoing_navigation { false };
// https://html.spec.whatwg.org/multipage/nav-history-apis.html#suppress-normal-scroll-restoration-during-ongoing-navigation // https://html.spec.whatwg.org/multipage/nav-history-apis.html#suppress-normal-scroll-restoration-during-ongoing-navigation
bool m_suppress_scroll_restoration_during_ongoing_navigation { false }; bool m_suppress_normal_scroll_restoration_during_ongoing_navigation { false };
// https://html.spec.whatwg.org/multipage/nav-history-apis.html#ongoing-api-method-tracker // https://html.spec.whatwg.org/multipage/nav-history-apis.html#ongoing-api-method-tracker
GC::Ptr<NavigationAPIMethodTracker> m_ongoing_api_method_tracker = nullptr; GC::Ptr<NavigationAPIMethodTracker> m_ongoing_api_method_tracker = nullptr;

View file

@ -6,7 +6,9 @@
* SPDX-License-Identifier: BSD-2-Clause * SPDX-License-Identifier: BSD-2-Clause
*/ */
#include <AK/HashMap.h>
#include <AK/NeverDestroyed.h> #include <AK/NeverDestroyed.h>
#include <AK/NumericLimits.h>
#include <AK/QuickSort.h> #include <AK/QuickSort.h>
#include <LibGfx/Bitmap.h> #include <LibGfx/Bitmap.h>
#include <LibGfx/SkiaBackendContext.h> #include <LibGfx/SkiaBackendContext.h>
@ -197,6 +199,232 @@ bool TraversableNavigable::is_top_level_traversable() const
return parent() == nullptr; return parent() == nullptr;
} }
static bool session_history_entry_descriptors_are_valid(Vector<SessionHistoryEntryDescriptor> const& entries)
{
Optional<i32> previous_step;
for (auto const& entry : entries) {
if (entry.step < 0)
return false;
if (previous_step.has_value() && entry.step <= *previous_step)
return false;
for (auto const& nested_history : entry.document_state.nested_histories) {
if (!session_history_entry_descriptors_are_valid(nested_history.entries))
return false;
}
previous_step = entry.step;
}
return true;
}
struct SessionHistoryEntryReconstructionState {
HashMap<u64, RefPtr<DocumentState>> document_states;
};
static NonnullRefPtr<SessionHistoryEntry> create_session_history_entry_from_ui_process(SessionHistoryEntryDescriptor, SessionHistoryEntryReconstructionState&);
static DocumentState::NestedHistory create_nested_history_from_ui_process(SessionHistoryNestedHistoryDescriptor nested_history_descriptor, SessionHistoryEntryReconstructionState& reconstruction_state)
{
Vector<NonnullRefPtr<SessionHistoryEntry>> entries;
entries.ensure_capacity(nested_history_descriptor.entries.size());
for (auto& entry_descriptor : nested_history_descriptor.entries)
entries.unchecked_append(create_session_history_entry_from_ui_process(move(entry_descriptor), reconstruction_state));
return {
.id = move(nested_history_descriptor.id),
.entries = move(entries),
};
}
static void populate_nested_histories_from_ui_process(DocumentState& document_state, Vector<SessionHistoryNestedHistoryDescriptor> nested_history_descriptors, SessionHistoryEntryReconstructionState& reconstruction_state)
{
auto& nested_histories = document_state.nested_histories();
if (nested_histories.size() == nested_history_descriptors.size()) {
// NB: The UI process keeps session history across WebContent process
// swaps, but nested history ids are process-local navigable ids. When
// rebuilding history for an already-loaded document, preserve the live
// ids created by the new WebContent process.
for (size_t i = 0; i < nested_history_descriptors.size(); ++i)
nested_history_descriptors[i].id = nested_histories[i].id;
}
nested_histories.clear();
nested_histories.ensure_capacity(nested_history_descriptors.size());
for (auto& nested_history_descriptor : nested_history_descriptors)
nested_histories.unchecked_append(create_nested_history_from_ui_process(move(nested_history_descriptor), reconstruction_state));
}
static void apply_session_history_entry_descriptor_from_ui_process(SessionHistoryEntry& entry, SessionHistoryEntryDescriptor& entry_descriptor)
{
entry.set_url(move(entry_descriptor.url));
entry.set_step(static_cast<int>(entry_descriptor.step));
// NB: Older UI-process mirrors can carry an empty serialization record for
// provisional entries. Do not preserve stale state from a reused entry, but
// also do not install an invalid record that would crash when restored.
auto& vm = Bindings::main_thread_vm();
if (entry_descriptor.classic_history_api_state.is_empty())
entry.set_classic_history_api_state(MUST(structured_serialize_for_storage(vm, JS::js_null())));
else
entry.set_classic_history_api_state(move(entry_descriptor.classic_history_api_state));
if (entry_descriptor.navigation_api_state.is_empty())
entry.set_navigation_api_state(MUST(structured_serialize_for_storage(vm, JS::js_undefined())));
else
entry.set_navigation_api_state(move(entry_descriptor.navigation_api_state));
entry.set_navigation_api_key(move(entry_descriptor.navigation_api_key));
entry.set_navigation_api_id(move(entry_descriptor.navigation_api_id));
entry.set_scroll_restoration_mode(entry_descriptor.scroll_restoration_mode);
entry.set_scroll_position_data(move(entry_descriptor.scroll_position_data));
}
static void apply_session_history_document_state_descriptor_from_ui_process(DocumentState& document_state, SessionHistoryDocumentStateDescriptor const& document_state_descriptor)
{
document_state.set_history_policy_container(document_state_descriptor.history_policy_container);
document_state.set_request_referrer(document_state_descriptor.request_referrer);
document_state.set_request_referrer_policy(document_state_descriptor.request_referrer_policy);
document_state.set_initiator_origin(document_state_descriptor.initiator_origin);
document_state.set_origin(document_state_descriptor.origin);
document_state.set_about_base_url(document_state_descriptor.about_base_url);
document_state.set_resource(document_state_descriptor.resource);
document_state.set_reload_pending(document_state_descriptor.reload_pending);
// AD-HOC: Descriptor ID 0 marks a provisional UI-created document state whose entry has not yet been populated
// by WebContent. Treat it as already populated when reseeding the active entry, since a replacement
// WebContent process already has the active document for that entry.
document_state.set_ever_populated(document_state_descriptor.id == 0 ? true : document_state_descriptor.ever_populated);
document_state.set_navigable_target_name(document_state_descriptor.navigable_target_name);
}
static RefPtr<DocumentState> get_or_create_document_state_from_ui_process(SessionHistoryDocumentStateDescriptor const& document_state_descriptor, SessionHistoryEntryReconstructionState& reconstruction_state)
{
RefPtr<DocumentState> document_state;
if (document_state_descriptor.id != 0) {
if (auto existing_document_state = reconstruction_state.document_states.get(document_state_descriptor.id); existing_document_state.has_value())
document_state = *existing_document_state;
}
if (!document_state) {
document_state = DocumentState::create();
if (document_state_descriptor.id != 0)
reconstruction_state.document_states.set(document_state_descriptor.id, document_state);
}
apply_session_history_document_state_descriptor_from_ui_process(*document_state, document_state_descriptor);
return document_state;
}
static NonnullRefPtr<SessionHistoryEntry> create_session_history_entry_from_ui_process(SessionHistoryEntryDescriptor entry_descriptor, SessionHistoryEntryReconstructionState& reconstruction_state)
{
auto entry = SessionHistoryEntry::create();
apply_session_history_entry_descriptor_from_ui_process(*entry, entry_descriptor);
auto document_state = get_or_create_document_state_from_ui_process(entry_descriptor.document_state, reconstruction_state);
VERIFY(document_state);
populate_nested_histories_from_ui_process(*document_state, move(entry_descriptor.document_state.nested_histories), reconstruction_state);
entry->set_document_state(move(document_state));
return entry;
}
bool TraversableNavigable::replace_top_level_session_history_entries_from_ui_process(Vector<SessionHistoryEntryDescriptor> entries_from_ui_process, size_t current_top_level_entry_index)
{
if (entries_from_ui_process.is_empty() || current_top_level_entry_index >= entries_from_ui_process.size())
return false;
VERIFY(is_top_level_traversable());
if (!session_history_entry_descriptors_are_valid(entries_from_ui_process))
return false;
// NB: The UI process stores a traversable's top-level session history entries
// across WebContent process swaps. When seeding a fresh WebContent process,
// current_top_level_entry_index is an index into the traversable's session
// history entries list, not an index into the result of getting all used
// history steps.
// https://html.spec.whatwg.org/multipage/document-sequences.html#tn-session-history-entries
// https://html.spec.whatwg.org/multipage/browsing-the-web.html#getting-all-used-history-steps
auto active_entry = active_session_history_entry();
VERIFY(active_entry);
SessionHistoryEntryReconstructionState reconstruction_state;
if (entries_from_ui_process[current_top_level_entry_index].document_state.id != 0) {
auto active_document_state = active_entry->document_state();
VERIFY(active_document_state);
reconstruction_state.document_states.set(entries_from_ui_process[current_top_level_entry_index].document_state.id, active_document_state);
}
Vector<NonnullRefPtr<SessionHistoryEntry>> entries;
entries.ensure_capacity(entries_from_ui_process.size());
for (size_t i = 0; i < entries_from_ui_process.size(); ++i) {
auto entry_descriptor = move(entries_from_ui_process[i]);
NonnullRefPtr<SessionHistoryEntry> entry = *active_entry;
if (i == current_top_level_entry_index) {
VERIFY(entry->document_state());
apply_session_history_entry_descriptor_from_ui_process(*entry, entry_descriptor);
apply_session_history_document_state_descriptor_from_ui_process(*entry->document_state(), entry_descriptor.document_state);
populate_nested_histories_from_ui_process(*entry->document_state(), move(entry_descriptor.document_state.nested_histories), reconstruction_state);
} else {
entry = create_session_history_entry_from_ui_process(move(entry_descriptor), reconstruction_state);
}
entries.unchecked_append(move(entry));
}
m_session_history_entries = move(entries);
auto current_entry = m_session_history_entries[current_top_level_entry_index];
set_active_session_history_entry(current_entry);
set_current_session_history_entry(current_entry);
m_current_session_history_step = current_entry->step().get<int>();
auto document = active_document();
VERIFY(document);
auto history_object_length_and_index = get_the_history_object_length_and_index(m_current_session_history_step);
document->history()->m_index = history_object_length_and_index.script_history_index;
document->history()->m_length = history_object_length_and_index.script_history_length;
// NB: The UI process can seed a replacement WebContent process before the new document has loaded. Do not
// restore the UI-owned entry's classic history API state or persisted state onto the initial about:blank
// document; the navigation algorithm will restore them onto the document that is actually created for the
// entry.
if (!document->is_initial_about_blank()) {
document->restore_the_history_object_state(current_entry);
restore_persisted_state_from_session_history_entry(*current_entry);
}
auto entries_for_navigation_api = get_session_history_entries_for_the_navigation_api(*this, m_current_session_history_step);
active_window()->navigation()->initialize_the_navigation_api_entries_for_reconstructed_session_history(entries_for_navigation_api, current_entry);
return true;
}
void TraversableNavigable::reset_session_history_for_testing(GC::Ref<GC::Function<void()>> on_complete)
{
append_session_history_traversal_steps(GC::create_function(heap(), [this, on_complete](NonnullRefPtr<Core::Promise<Empty>> signal) {
auto maybe_active_entry = active_session_history_entry();
VERIFY(maybe_active_entry);
auto active_entry = maybe_active_entry.release_nonnull();
active_entry->set_step(0);
m_session_history_entries.clear();
m_session_history_entries.append(active_entry);
set_active_session_history_entry(active_entry);
set_current_session_history_entry(active_entry);
m_current_session_history_step = 0;
auto document = active_document();
VERIFY(document);
auto history_object_length_and_index = get_the_history_object_length_and_index(m_current_session_history_step);
document->history()->m_index = history_object_length_and_index.script_history_index;
document->history()->m_length = history_object_length_and_index.script_history_length;
auto entries_for_navigation_api = get_session_history_entries_for_the_navigation_api(*this, m_current_session_history_step);
active_window()->navigation()->initialize_the_navigation_api_entries_for_reconstructed_session_history(entries_for_navigation_api, active_entry);
if (page().client().should_report_session_history_updates()) {
auto session_history_snapshot = create_session_history_snapshot();
page().client().page_did_update_session_history(session_history_snapshot.top_level_session_history_entries, session_history_snapshot.used_session_history_steps, session_history_snapshot.current_used_step_index);
}
page().client().page_did_update_navigation_buttons_state(false, false);
signal->resolve({});
on_complete->function()();
}));
}
// https://html.spec.whatwg.org/multipage/browsing-the-web.html#getting-all-used-history-steps // https://html.spec.whatwg.org/multipage/browsing-the-web.html#getting-all-used-history-steps
Vector<int> TraversableNavigable::get_all_used_history_steps() const Vector<int> TraversableNavigable::get_all_used_history_steps() const
{ {
@ -787,7 +1015,7 @@ void ApplyHistoryStepState::start()
|| target_entry->document_state()->reload_pending()); || target_entry->document_state()->reload_pending());
if (needs_population) { if (needs_population) {
if (target_entry->document_state()->reload_pending() && navigable->is_top_level_traversable()) if (target_entry->document_state()->reload_pending() && navigable->is_top_level_traversable())
navigable->page().client().page_did_start_loading(target_entry->url(), false); navigable->page().client().page_did_start_loading(target_entry->url(), Empty {}, false);
// FIXME: 1. Let navTimingType be "back_forward" if targetEntry's document is null; otherwise "reload". // FIXME: 1. Let navTimingType be "back_forward" if targetEntry's document is null; otherwise "reload".
@ -1022,6 +1250,14 @@ void ApplyHistoryStepState::process_continuations()
// 2. Queue a global task on the navigation and traversal task source given navigable's active window to perform afterPotentialUnloads. // 2. Queue a global task on the navigation and traversal task source given navigable's active window to perform afterPotentialUnloads.
queue_global_task(Task::Source::NavigationAndTraversal, *navigable->active_window(), after_potential_unload); queue_global_task(Task::Source::NavigationAndTraversal, *navigable->active_window(), after_potential_unload);
} }
// AD-HOC: During navigable creation, the initial about:blank document can be
// replaced by the container's initial navigation while applying the
// creation/destruction history step. That hook passes a null
// navigationType per spec, and there is no outgoing document to unload.
else if (!m_navigation_type.has_value() && displayed_document->is_initial_about_blank()) {
navigable->set_ongoing_navigation({}, m_navigation_api_abort_behavior);
after_potential_unload->function()();
}
// 11. Otherwise: // 11. Otherwise:
else { else {
// 1. Assert: navigationType is not null. // 1. Assert: navigationType is not null.
@ -1078,6 +1314,106 @@ void ApplyHistoryStepState::enter_waiting_for_non_changing_jobs()
try_advance(); try_advance();
} }
struct SessionHistoryEntryDescriptorCreationState {
HashMap<DocumentState const*, u64> document_state_ids;
u64 next_document_state_id { 1 };
};
static u64 document_state_id_for_descriptor(DocumentState const& document_state, SessionHistoryEntryDescriptorCreationState& creation_state)
{
if (auto id = creation_state.document_state_ids.get(&document_state); id.has_value())
return *id;
auto id = creation_state.next_document_state_id++;
VERIFY(id != 0);
creation_state.document_state_ids.set(&document_state, id);
return id;
}
static SessionHistoryEntryDescriptor create_session_history_entry_descriptor(SessionHistoryEntry const&, SessionHistoryEntryDescriptorCreationState&);
static SessionHistoryDocumentStateDescriptor create_session_history_document_state_descriptor(DocumentState const& document_state, SessionHistoryEntryDescriptorCreationState& creation_state)
{
Vector<SessionHistoryNestedHistoryDescriptor> nested_history_descriptors;
nested_history_descriptors.ensure_capacity(document_state.nested_histories().size());
for (auto const& nested_history : document_state.nested_histories()) {
Vector<SessionHistoryEntryDescriptor> nested_entry_descriptors;
nested_entry_descriptors.ensure_capacity(nested_history.entries.size());
for (auto const& nested_entry : nested_history.entries)
nested_entry_descriptors.unchecked_append(create_session_history_entry_descriptor(nested_entry, creation_state));
nested_history_descriptors.unchecked_append({
.id = nested_history.id,
.entries = move(nested_entry_descriptors),
});
}
return {
.id = document_state_id_for_descriptor(document_state, creation_state),
.history_policy_container = document_state.history_policy_container(),
.request_referrer = document_state.request_referrer(),
.request_referrer_policy = document_state.request_referrer_policy(),
.initiator_origin = document_state.initiator_origin(),
.origin = document_state.origin(),
.about_base_url = document_state.about_base_url(),
.resource = document_state.resource(),
.reload_pending = document_state.reload_pending(),
.ever_populated = document_state.ever_populated(),
.navigable_target_name = document_state.navigable_target_name(),
.nested_histories = move(nested_history_descriptors),
};
}
static SessionHistoryEntryDescriptor create_session_history_entry_descriptor(SessionHistoryEntry const& entry, SessionHistoryEntryDescriptorCreationState& creation_state)
{
SessionHistoryDocumentStateDescriptor document_state_descriptor;
if (auto document_state = entry.document_state()) {
document_state_descriptor = create_session_history_document_state_descriptor(*document_state, creation_state);
}
return {
.step = static_cast<i32>(entry.step().get<int>()),
.url = entry.url(),
.document_state = move(document_state_descriptor),
.classic_history_api_state = entry.classic_history_api_state(),
.navigation_api_state = entry.navigation_api_state(),
.navigation_api_key = entry.navigation_api_key(),
.navigation_api_id = entry.navigation_api_id(),
.scroll_restoration_mode = entry.scroll_restoration_mode(),
.scroll_position_data = entry.scroll_position_data(),
};
}
TraversableNavigable::SessionHistorySnapshot TraversableNavigable::create_session_history_snapshot(SaveActiveEntryPersistedState save_active_entry_persisted_state)
{
if (save_active_entry_persisted_state == SaveActiveEntryPersistedState::Yes)
save_persisted_state_to_active_session_history_entry();
Vector<SessionHistoryEntryDescriptor> top_level_session_history_entries;
top_level_session_history_entries.ensure_capacity(session_history_entries().size());
SessionHistoryEntryDescriptorCreationState creation_state;
for (auto const& entry : session_history_entries())
top_level_session_history_entries.unchecked_append(create_session_history_entry_descriptor(entry, creation_state));
auto used_history_steps = get_all_used_history_steps();
Vector<i32> used_session_history_steps;
used_session_history_steps.ensure_capacity(used_history_steps.size());
Optional<size_t> current_used_step_index;
for (size_t i = 0; i < used_history_steps.size(); ++i) {
auto step = used_history_steps[i];
used_session_history_steps.unchecked_append(static_cast<i32>(step));
if (step == current_session_history_step())
current_used_step_index = i;
}
VERIFY(current_used_step_index.has_value());
return {
.top_level_session_history_entries = move(top_level_session_history_entries),
.used_session_history_steps = move(used_session_history_steps),
.current_used_step_index = *current_used_step_index,
};
}
void ApplyHistoryStepState::complete() void ApplyHistoryStepState::complete()
{ {
if (m_phase == Phase::Completed) if (m_phase == Phase::Completed)
@ -1088,9 +1424,25 @@ void ApplyHistoryStepState::complete()
// 20. Set traversable's current session history step to targetStep. // 20. Set traversable's current session history step to targetStep.
m_traversable->m_current_session_history_step = m_target_step; m_traversable->m_current_session_history_step = m_target_step;
// Not in the spec: // AD-HOC: Report the updated session history descriptors to the UI-process mirror.
auto back_enabled = m_traversable->m_current_session_history_step > 0; if (m_traversable->page().client().should_report_session_history_updates()) {
auto save_active_entry_persisted_state = TraversableNavigable::SaveActiveEntryPersistedState::Yes;
// NB: During history traversal, the active entry can point at the target
// entry before the active document's queued history-step update has
// restored the target entry's persisted state. Do not overwrite that
// target entry with the document's pre-restoration viewport offset.
if (m_navigation_type == Bindings::NavigationType::Traverse) {
auto document = m_traversable->active_document();
auto active_entry = m_traversable->active_session_history_entry();
if (document && active_entry && document->latest_entry() != active_entry)
save_active_entry_persisted_state = TraversableNavigable::SaveActiveEntryPersistedState::No;
}
auto session_history_snapshot = m_traversable->create_session_history_snapshot(save_active_entry_persisted_state);
m_traversable->page().client().page_did_update_session_history(session_history_snapshot.top_level_session_history_entries, session_history_snapshot.used_session_history_steps, session_history_snapshot.current_used_step_index);
}
VERIFY(m_traversable->m_session_history_entries.size() > 0); VERIFY(m_traversable->m_session_history_entries.size() > 0);
auto back_enabled = m_traversable->can_go_back();
auto forward_enabled = m_traversable->can_go_forward(); auto forward_enabled = m_traversable->can_go_forward();
m_traversable->page().client().page_did_update_navigation_buttons_state(back_enabled, forward_enabled); m_traversable->page().client().page_did_update_navigation_buttons_state(back_enabled, forward_enabled);
m_traversable->page().client().page_did_change_url(m_traversable->current_session_history_entry()->url()); m_traversable->page().client().page_did_change_url(m_traversable->current_session_history_entry()->url());
@ -1123,6 +1475,29 @@ void TraversableNavigable::apply_the_history_step(
VERIFY(!m_apply_history_step_state || m_paused_apply_history_step_state); VERIFY(!m_apply_history_step_state || m_paused_apply_history_step_state);
run_the_history_step_prechecks(step, check_for_cancelation, source_snapshot_params, initiator_to_check, user_involvement, navigation_type, navigation_api_abort_behavior,
GC::create_function(heap(), [this, step, source_snapshot_params, user_involvement, navigation_type, synchronous_navigation, pending_document, on_complete](HistoryStepResult result, int target_step, Navigable::NavigationAPIAbortBehavior navigation_api_abort_behavior) {
if (result != HistoryStepResult::Applied) {
on_complete->function()(result);
return;
}
// 6. Let changingNavigables be the result of get all navigables whose current session history entry will
// change or reload given traversable and targetStep.
apply_the_history_step_after_unload_check(step, target_step, source_snapshot_params, user_involvement, navigation_type, synchronous_navigation, navigation_api_abort_behavior, pending_document, on_complete);
}));
}
void TraversableNavigable::run_the_history_step_prechecks(
int step,
bool check_for_cancelation,
GC::Ptr<SourceSnapshotParams> source_snapshot_params,
GC::Ptr<Navigable> initiator_to_check,
UserNavigationInvolvement user_involvement,
Optional<Bindings::NavigationType> navigation_type,
Navigable::NavigationAPIAbortBehavior navigation_api_abort_behavior,
GC::Ref<OnHistoryStepPrechecksComplete> on_complete)
{
// 2. Let targetStep be the result of getting the used step given traversable and step. // 2. Let targetStep be the result of getting the used step given traversable and step.
auto target_step = get_the_used_step(step); auto target_step = get_the_used_step(step);
@ -1131,11 +1506,18 @@ void TraversableNavigable::apply_the_history_step(
// 1. Assert: sourceSnapshotParams is not null. // 1. Assert: sourceSnapshotParams is not null.
VERIFY(source_snapshot_params); VERIFY(source_snapshot_params);
auto target_top_level_entry = get_the_target_history_entry(target_step);
if (target_top_level_entry != current_session_history_entry()
&& !initiator_to_check->allowed_by_sandboxing_to_navigate(*this, *source_snapshot_params)) {
on_complete->function()(HistoryStepResult::InitiatorDisallowed, target_step, navigation_api_abort_behavior);
return;
}
// 2. For each navigable of get all navigables whose current session history entry will change or reload: // 2. For each navigable of get all navigables whose current session history entry will change or reload:
// if initiatorToCheck is not allowed by sandboxing to navigate navigable given sourceSnapshotParams, then return "initiator-disallowed". // if initiatorToCheck is not allowed by sandboxing to navigate navigable given sourceSnapshotParams, then return "initiator-disallowed".
for (auto const& navigable : get_all_navigables_whose_current_session_history_entry_will_change_or_reload(target_step)) { for (auto const& navigable : get_all_navigables_whose_current_session_history_entry_will_change_or_reload(target_step)) {
if (!initiator_to_check->allowed_by_sandboxing_to_navigate(*navigable, *source_snapshot_params)) { if (!initiator_to_check->allowed_by_sandboxing_to_navigate(*navigable, *source_snapshot_params)) {
on_complete->function()(HistoryStepResult::InitiatorDisallowed); on_complete->function()(HistoryStepResult::InitiatorDisallowed, target_step, navigation_api_abort_behavior);
return; return;
} }
} }
@ -1153,22 +1535,21 @@ void TraversableNavigable::apply_the_history_step(
// and userInvolvement is not "continue", then return that result. // and userInvolvement is not "continue", then return that result.
if (check_for_cancelation) { if (check_for_cancelation) {
check_if_unloading_is_canceled(navigables_crossing_documents, *this, target_step, user_involvement, check_if_unloading_is_canceled(navigables_crossing_documents, *this, target_step, user_involvement,
GC::create_function(heap(), [this, step, target_step, source_snapshot_params, user_involvement, navigation_type, synchronous_navigation, navigation_api_abort_behavior, pending_document, on_complete](CheckIfUnloadingIsCanceledResult result) mutable { GC::create_function(heap(), [target_step, navigation_api_abort_behavior, on_complete](CheckIfUnloadingIsCanceledResult result) mutable {
if (result == CheckIfUnloadingIsCanceledResult::CanceledByBeforeUnload) { if (result == CheckIfUnloadingIsCanceledResult::CanceledByBeforeUnload) {
on_complete->function()(HistoryStepResult::CanceledByBeforeUnload); on_complete->function()(HistoryStepResult::CanceledByBeforeUnload, target_step, navigation_api_abort_behavior);
return; return;
} }
if (result == CheckIfUnloadingIsCanceledResult::CanceledByNavigate) { if (result == CheckIfUnloadingIsCanceledResult::CanceledByNavigate) {
on_complete->function()(HistoryStepResult::CanceledByNavigate); on_complete->function()(HistoryStepResult::CanceledByNavigate, target_step, navigation_api_abort_behavior);
return; return;
} }
apply_the_history_step_after_unload_check(step, target_step, source_snapshot_params, user_involvement, navigation_type, synchronous_navigation, navigation_api_abort_behavior, pending_document, on_complete); on_complete->function()(HistoryStepResult::Applied, target_step, navigation_api_abort_behavior);
})); }));
return; return;
} }
// 6. Let changingNavigables be the result of get all navigables whose current session history entry will change or reload given traversable and targetStep. on_complete->function()(HistoryStepResult::Applied, target_step, navigation_api_abort_behavior);
apply_the_history_step_after_unload_check(step, target_step, source_snapshot_params, user_involvement, navigation_type, synchronous_navigation, navigation_api_abort_behavior, pending_document, on_complete);
} }
void TraversableNavigable::apply_the_history_step_after_unload_check( void TraversableNavigable::apply_the_history_step_after_unload_check(
@ -1526,26 +1907,20 @@ void TraversableNavigable::clear_the_forward_session_history()
} }
} }
bool TraversableNavigable::can_go_back() const
{
auto all_steps = get_all_used_history_steps();
auto current_step_index = all_steps.find_first_index(current_session_history_step());
VERIFY(current_step_index.has_value());
return *current_step_index > 0;
}
bool TraversableNavigable::can_go_forward() const bool TraversableNavigable::can_go_forward() const
{ {
auto step = current_session_history_step(); auto all_steps = get_all_used_history_steps();
auto current_step_index = all_steps.find_first_index(current_session_history_step());
Vector<Vector<NonnullRefPtr<SessionHistoryEntry>> const&> entry_lists; VERIFY(current_step_index.has_value());
entry_lists.append(session_history_entries()); return *current_step_index + 1 < all_steps.size();
while (!entry_lists.is_empty()) {
auto const& entry_list = entry_lists.take_first();
for (auto const& entry : entry_list) {
if (entry->step().template get<int>() > step)
return true;
for (auto& nested_history : entry->document_state()->nested_histories())
entry_lists.append(nested_history.entries);
}
}
return false;
} }
// https://html.spec.whatwg.org/multipage/browsing-the-web.html#traverse-the-history-by-a-delta // https://html.spec.whatwg.org/multipage/browsing-the-web.html#traverse-the-history-by-a-delta
@ -1558,7 +1933,7 @@ void TraversableNavigable::traverse_the_history_by_delta(int delta, GC::Ptr<DOM:
// 2. Let userInvolvement be "browser UI". // 2. Let userInvolvement be "browser UI".
UserNavigationInvolvement user_involvement = UserNavigationInvolvement::BrowserUI; UserNavigationInvolvement user_involvement = UserNavigationInvolvement::BrowserUI;
// 1. If sourceDocument is given, then: // 3. If sourceDocument is given, then:
if (source_document) { if (source_document) {
// 1. Set sourceSnapshotParams to the result of snapshotting source snapshot params given sourceDocument. // 1. Set sourceSnapshotParams to the result of snapshotting source snapshot params given sourceDocument.
source_snapshot_params = source_document->snapshot_source_snapshot_params(); source_snapshot_params = source_document->snapshot_source_snapshot_params();
@ -1579,23 +1954,122 @@ void TraversableNavigable::traverse_the_history_by_delta(int delta, GC::Ptr<DOM:
auto current_step_index = *all_steps.find_first_index(current_session_history_step()); auto current_step_index = *all_steps.find_first_index(current_session_history_step());
// 3. Let targetStepIndex be currentStepIndex plus delta // 3. Let targetStepIndex be currentStepIndex plus delta
auto target_step_index = current_step_index + delta; size_t target_step_index = 0;
if (delta < 0) {
auto magnitude = static_cast<size_t>(-static_cast<i64>(delta));
if (magnitude > current_step_index) {
if (source_snapshot_params && page().client().page_did_request_traverse_the_history_by_delta(delta, HistoryTraversalPrecheck::Needed)) {
signal->resolve({});
return;
}
signal->resolve({});
return;
}
target_step_index = current_step_index - magnitude;
} else {
auto magnitude = static_cast<size_t>(delta);
if (magnitude >= all_steps.size() - current_step_index) {
if (source_snapshot_params && page().client().page_did_request_traverse_the_history_by_delta(delta, HistoryTraversalPrecheck::Needed)) {
signal->resolve({});
return;
}
signal->resolve({});
return;
}
target_step_index = current_step_index + magnitude;
}
// 4. If allSteps[targetStepIndex] does not exist, then abort these steps. // 4. If allSteps[targetStepIndex] does not exist, then abort these steps.
if (target_step_index >= all_steps.size()) { if (target_step_index >= all_steps.size()) {
if (source_snapshot_params && page().client().page_did_request_traverse_the_history_by_delta(delta, HistoryTraversalPrecheck::Needed)) {
signal->resolve({});
return;
}
signal->resolve({}); signal->resolve({});
return; return;
} }
auto target_step = all_steps[target_step_index];
if (source_snapshot_params) {
RefPtr<SessionHistoryEntry> target_top_level_entry;
for (auto const& entry : session_history_entries()) {
if (entry->step().template get<int>() > target_step)
break;
target_top_level_entry = entry;
}
if (target_top_level_entry && current_session_history_entry() && !page().client().is_url_suitable_for_same_process_navigation(current_session_history_entry()->url(), target_top_level_entry->url())) {
run_the_history_step_prechecks(target_step, true, source_snapshot_params, initiator_to_check, user_involvement, Bindings::NavigationType::Traverse, Navigable::NavigationAPIAbortBehavior::Abort,
GC::create_function(heap(), [this, delta, signal](HistoryStepResult result, int, Navigable::NavigationAPIAbortBehavior) {
if (result == HistoryStepResult::Applied)
(void)page().client().page_did_request_traverse_the_history_by_delta(delta, HistoryTraversalPrecheck::AlreadyDone);
signal->resolve({});
}));
return;
}
}
// 5. Apply the traverse history step allSteps[targetStepIndex] to traversable, given sourceSnapshotParams, // 5. Apply the traverse history step allSteps[targetStepIndex] to traversable, given sourceSnapshotParams,
// initiatorToCheck, and userInvolvement. // initiatorToCheck, and userInvolvement.
apply_the_traverse_history_step(all_steps[target_step_index], source_snapshot_params, initiator_to_check, user_involvement, apply_the_traverse_history_step(target_step, source_snapshot_params, initiator_to_check, user_involvement,
GC::create_function(heap(), [signal](HistoryStepResult) { GC::create_function(heap(), [signal](HistoryStepResult) {
signal->resolve({}); signal->resolve({});
})); }));
})); }));
} }
void TraversableNavigable::traverse_the_history_to_step(int step, GC::Ref<GC::Function<void(bool step_was_available, HistoryStepResult)>> on_complete)
{
// NB: This is used when the UI process owns the top-level session
// history and has already resolved the browser UI delta to a stable step.
append_session_history_traversal_steps(GC::create_function(heap(), [this, step, on_complete](NonnullRefPtr<Core::Promise<Empty>> signal) {
auto all_steps = get_all_used_history_steps();
if (!all_steps.contains_slow(step)) {
on_complete->function()(false, HistoryStepResult::Applied);
signal->resolve({});
return;
}
apply_the_traverse_history_step(step, nullptr, nullptr, UserNavigationInvolvement::BrowserUI,
GC::create_function(heap(), [signal, on_complete](HistoryStepResult result) {
on_complete->function()(true, result);
signal->resolve({});
}));
}));
}
void TraversableNavigable::check_if_traverse_history_step_is_canceled(int step, GC::Ref<OnApplyHistoryStepComplete> on_complete)
{
// NB: This is used when the UI process owns the top-level session history
// and needs WebContent to run the cancelable part of the traverse algorithm
// before the UI process applies its own history mirror update.
append_session_history_traversal_steps(GC::create_function(heap(), [this, step, on_complete](NonnullRefPtr<Core::Promise<Empty>> signal) {
auto all_steps = get_all_used_history_steps();
if (!all_steps.contains_slow(step)) {
// The UI process can ask about a step in its authoritative
// session history mirror that this WebContent process cannot
// address locally, for example after a process swap with a partial
// restored history. We cannot run the full traverse prechecks
// without the target entry, but the active document tree still must
// get a chance to cancel unloading before the UI process replaces
// this WebContent process or fallback-loads the target entry.
check_if_unloading_is_canceled(active_document()->inclusive_descendant_navigables(),
GC::create_function(heap(), [signal, on_complete](CheckIfUnloadingIsCanceledResult result) {
on_complete->function()(result == CheckIfUnloadingIsCanceledResult::Continue ? HistoryStepResult::Applied : HistoryStepResult::CanceledByBeforeUnload);
signal->resolve({});
}));
return;
}
run_the_history_step_prechecks(step, true, nullptr, nullptr, UserNavigationInvolvement::BrowserUI, Bindings::NavigationType::Traverse, Navigable::NavigationAPIAbortBehavior::Abort,
GC::create_function(heap(), [signal, on_complete](HistoryStepResult result, int, Navigable::NavigationAPIAbortBehavior) {
on_complete->function()(result);
signal->resolve({});
}));
}));
}
// https://html.spec.whatwg.org/multipage/browsing-the-web.html#update-for-navigable-creation/destruction // https://html.spec.whatwg.org/multipage/browsing-the-web.html#update-for-navigable-creation/destruction
void TraversableNavigable::update_for_navigable_creation_or_destruction(GC::Ref<OnApplyHistoryStepComplete> on_complete) void TraversableNavigable::update_for_navigable_creation_or_destruction(GC::Ref<OnApplyHistoryStepComplete> on_complete)
{ {
@ -1613,7 +2087,22 @@ void TraversableNavigable::apply_the_reload_history_step(UserNavigationInvolveme
auto step = current_session_history_step(); auto step = current_session_history_step();
// 2. Return the result of applying the history step step to traversable given true, null, null, null, and "reload". // 2. Return the result of applying the history step step to traversable given true, null, null, null, and "reload".
apply_the_history_step(step, true, {}, {}, user_involvement, Bindings::NavigationType::Reload, SynchronousNavigation::No, Navigable::NavigationAPIAbortBehavior::Abort, nullptr, on_complete); apply_the_history_step(step, true, {}, {}, user_involvement, Bindings::NavigationType::Reload, SynchronousNavigation::No, Navigable::NavigationAPIAbortBehavior::Abort, nullptr,
GC::create_function(heap(), [this, on_complete](HistoryStepResult result) {
if (result != HistoryStepResult::Applied) {
// NB: A canceled reload must not keep treating the active
// session history entry as an in-flight reload.
if (auto current_entry = current_session_history_entry(); current_entry && current_entry->document_state()->reload_pending()) {
current_entry->document_state()->set_reload_pending(false);
if (page().client().should_report_session_history_updates()) {
auto session_history_snapshot = create_session_history_snapshot();
page().client().page_did_update_session_history(session_history_snapshot.top_level_session_history_entries, session_history_snapshot.used_session_history_steps, session_history_snapshot.current_used_step_index);
}
}
}
on_complete->function()(result);
}));
} }
// https://html.spec.whatwg.org/multipage/browsing-the-web.html#apply-the-push/replace-history-step // https://html.spec.whatwg.org/multipage/browsing-the-web.html#apply-the-push/replace-history-step

View file

@ -46,6 +46,16 @@ public:
int current_session_history_step() const { return m_current_session_history_step; } int current_session_history_step() const { return m_current_session_history_step; }
Vector<NonnullRefPtr<SessionHistoryEntry>>& session_history_entries() { return m_session_history_entries; } Vector<NonnullRefPtr<SessionHistoryEntry>>& session_history_entries() { return m_session_history_entries; }
Vector<NonnullRefPtr<SessionHistoryEntry>> const& session_history_entries() const { return m_session_history_entries; } Vector<NonnullRefPtr<SessionHistoryEntry>> const& session_history_entries() const { return m_session_history_entries; }
struct SessionHistorySnapshot {
Vector<SessionHistoryEntryDescriptor> top_level_session_history_entries;
Vector<i32> used_session_history_steps;
size_t current_used_step_index { 0 };
};
enum class SaveActiveEntryPersistedState : bool {
No,
Yes,
};
SessionHistorySnapshot create_session_history_snapshot(SaveActiveEntryPersistedState = SaveActiveEntryPersistedState::Yes);
VisibilityState system_visibility_state() const { return m_system_visibility_state; } VisibilityState system_visibility_state() const { return m_system_visibility_state; }
void set_system_visibility_state(VisibilityState); void set_system_visibility_state(VisibilityState);
@ -77,6 +87,10 @@ public:
Vector<int> get_all_used_history_steps() const; Vector<int> get_all_used_history_steps() const;
void clear_the_forward_session_history(); void clear_the_forward_session_history();
void traverse_the_history_by_delta(int delta, GC::Ptr<DOM::Document> source_document = {}); void traverse_the_history_by_delta(int delta, GC::Ptr<DOM::Document> source_document = {});
void traverse_the_history_to_step(int step, GC::Ref<GC::Function<void(bool step_was_available, HistoryStepResult)>> on_complete);
void check_if_traverse_history_step_is_canceled(int step, GC::Ref<OnApplyHistoryStepComplete> on_complete);
bool replace_top_level_session_history_entries_from_ui_process(Vector<SessionHistoryEntryDescriptor>, size_t current_top_level_entry_index);
void reset_session_history_for_testing(GC::Ref<GC::Function<void()>> on_complete);
void close_top_level_traversable(); void close_top_level_traversable();
void definitely_close_top_level_traversable(); void definitely_close_top_level_traversable();
@ -128,7 +142,7 @@ private:
virtual void visit_edges(Cell::Visitor&) override; virtual void visit_edges(Cell::Visitor&) override;
// FIXME: Fix spec typo cancelation --> cancellation // NB: The HTML Standard spells this algorithm argument "checkForCancelation".
void apply_the_history_step( void apply_the_history_step(
int step, int step,
bool check_for_cancelation, bool check_for_cancelation,
@ -152,10 +166,22 @@ private:
GC::Ptr<DOM::Document> pending_document, GC::Ptr<DOM::Document> pending_document,
GC::Ref<OnApplyHistoryStepComplete> on_complete); GC::Ref<OnApplyHistoryStepComplete> on_complete);
using OnHistoryStepPrechecksComplete = GC::Function<void(HistoryStepResult, int target_step, Navigable::NavigationAPIAbortBehavior)>;
void run_the_history_step_prechecks(
int step,
bool check_for_cancelation,
GC::Ptr<SourceSnapshotParams>,
GC::Ptr<Navigable> initiator_to_check,
UserNavigationInvolvement user_involvement,
Optional<Bindings::NavigationType> navigation_type,
Navigable::NavigationAPIAbortBehavior,
GC::Ref<OnHistoryStepPrechecksComplete>);
void check_if_unloading_is_canceled(Vector<GC::Root<Navigable>> navigables_that_need_before_unload, GC::Ptr<TraversableNavigable> traversable, Optional<int> target_step, Optional<UserNavigationInvolvement> user_involvement_for_navigate_events, GC::Ref<GC::Function<void(CheckIfUnloadingIsCanceledResult)>> callback); void check_if_unloading_is_canceled(Vector<GC::Root<Navigable>> navigables_that_need_before_unload, GC::Ptr<TraversableNavigable> traversable, Optional<int> target_step, Optional<UserNavigationInvolvement> user_involvement_for_navigate_events, GC::Ref<GC::Function<void(CheckIfUnloadingIsCanceledResult)>> callback);
Vector<NonnullRefPtr<SessionHistoryEntry>> get_session_history_entries_for_the_navigation_api(GC::Ref<Navigable>, int); Vector<NonnullRefPtr<SessionHistoryEntry>> get_session_history_entries_for_the_navigation_api(GC::Ref<Navigable>, int);
[[nodiscard]] bool can_go_back() const;
[[nodiscard]] bool can_go_forward() const; [[nodiscard]] bool can_go_forward() const;
// https://html.spec.whatwg.org/multipage/document-sequences.html#tn-current-session-history-step // https://html.spec.whatwg.org/multipage/document-sequences.html#tn-current-session-history-step

View file

@ -671,7 +671,13 @@ String Internals::dump_session_history()
auto step = entry->step(); auto step = entry->step();
auto const& url = entry->url(); auto const& url = entry->url();
auto filename = url.basename(); auto filename = url.basename();
auto display = url.fragment().has_value() ? MUST(String::formatted("{}#{}", filename, *url.fragment())) : MUST(String::from_byte_string(filename)); StringBuilder display_builder;
display_builder.append(filename);
if (url.query().has_value())
display_builder.appendff("?{}", *url.query());
if (url.fragment().has_value())
display_builder.appendff("#{}", *url.fragment());
auto display = display_builder.to_string_without_validation();
auto is_current = step.has<int>() && step.get<int>() == current_step; auto is_current = step.has<int>() && step.get<int>() == current_step;
auto relative_step = step.has<int>() && min_step.has_value() ? String::number(step.get<int>() - *min_step) : "pending"_string; auto relative_step = step.has<int>() && min_step.has_value() ? String::number(step.get<int>() - *min_step) : "pending"_string;
builder.appendff(" step {} {}{}\n", relative_step, display, is_current ? " (current)"sv : ""sv); builder.appendff(" step {} {}{}\n", relative_step, display, is_current ? " (current)"sv : ""sv);
@ -679,6 +685,11 @@ String Internals::dump_session_history()
return builder.to_string_without_validation(); return builder.to_string_without_validation();
} }
String Internals::dump_ui_process_session_history()
{
return window().associated_document().page().client().page_did_request_ui_process_session_history_for_testing();
}
GC::Ptr<DOM::ShadowRoot> Internals::get_shadow_root(GC::Ref<DOM::Element> element) GC::Ptr<DOM::ShadowRoot> Internals::get_shadow_root(GC::Ref<DOM::Element> element)
{ {
return element->shadow_root(); return element->shadow_root();

View file

@ -111,6 +111,7 @@ public:
String dump_stacking_context_tree(); String dump_stacking_context_tree();
String dump_gc_graph(); String dump_gc_graph();
String dump_session_history(); String dump_session_history();
String dump_ui_process_session_history();
GC::Ptr<DOM::ShadowRoot> get_shadow_root(GC::Ref<DOM::Element>); GC::Ptr<DOM::ShadowRoot> get_shadow_root(GC::Ref<DOM::Element>);

View file

@ -97,6 +97,7 @@ interface Internals {
DOMString dumpStackingContextTree(); DOMString dumpStackingContextTree();
DOMString dumpGCGraph(); DOMString dumpGCGraph();
DOMString dumpSessionHistory(); DOMString dumpSessionHistory();
DOMString dumpUIProcessSessionHistory();
// Returns the shadow root of the element, if it has one, even if it's not normally accessible to JS. // Returns the shadow root of the element, if it has one, even if it's not normally accessible to JS.
ShadowRoot? getShadowRoot(Element element); ShadowRoot? getShadowRoot(Element element);

View file

@ -116,9 +116,21 @@ void Page::navigable_document_destroyed(Badge<DOM::Document>, HTML::Navigable& n
m_focused_navigable = nullptr; m_focused_navigable = nullptr;
} }
void Page::load(URL::URL const& url) void Page::load(URL::URL const& url, Bindings::NavigationHistoryBehavior history_handling)
{ {
(void)top_level_traversable()->navigate({ .url = url, .source_document = *top_level_traversable()->active_document(), .user_involvement = HTML::UserNavigationInvolvement::BrowserUI }); (void)top_level_traversable()->navigate({ .url = url, .source_document = *top_level_traversable()->active_document(), .history_handling = history_handling, .user_involvement = HTML::UserNavigationInvolvement::BrowserUI });
}
void Page::load(URL::URL const& url, Variant<Empty, String, HTML::POSTResource> document_resource,
Bindings::NavigationHistoryBehavior history_handling)
{
(void)top_level_traversable()->navigate({
.url = url,
.source_document = *top_level_traversable()->active_document(),
.document_resource = move(document_resource),
.history_handling = history_handling,
.user_involvement = HTML::UserNavigationInvolvement::BrowserUI,
});
} }
void Page::load_html(StringView html) void Page::load_html(StringView html)
@ -163,6 +175,14 @@ void Page::reload()
} }
void Page::traverse_the_history_by_delta(int delta) void Page::traverse_the_history_by_delta(int delta)
{
if (m_client->page_did_request_traverse_the_history_by_delta(delta, HistoryTraversalPrecheck::Needed))
return;
traverse_the_history_by_delta_from_ui_process(delta);
}
void Page::traverse_the_history_by_delta_from_ui_process(int delta)
{ {
top_level_traversable()->traverse_the_history_by_delta(delta); top_level_traversable()->traverse_the_history_by_delta(delta);
} }

View file

@ -31,6 +31,7 @@
#include <LibRequests/RequestTimingInfo.h> #include <LibRequests/RequestTimingInfo.h>
#include <LibURL/URL.h> #include <LibURL/URL.h>
#include <LibWeb/Bindings/AgentType.h> #include <LibWeb/Bindings/AgentType.h>
#include <LibWeb/Bindings/Navigation.h>
#include <LibWeb/CSS/PreferredColorScheme.h> #include <LibWeb/CSS/PreferredColorScheme.h>
#include <LibWeb/CSS/PreferredContrast.h> #include <LibWeb/CSS/PreferredContrast.h>
#include <LibWeb/CSS/PreferredMotion.h> #include <LibWeb/CSS/PreferredMotion.h>
@ -42,7 +43,9 @@
#include <LibWeb/HTML/AudioPlayState.h> #include <LibWeb/HTML/AudioPlayState.h>
#include <LibWeb/HTML/ColorPickerUpdateState.h> #include <LibWeb/HTML/ColorPickerUpdateState.h>
#include <LibWeb/HTML/FileFilter.h> #include <LibWeb/HTML/FileFilter.h>
#include <LibWeb/HTML/POSTResource.h>
#include <LibWeb/HTML/SelectItem.h> #include <LibWeb/HTML/SelectItem.h>
#include <LibWeb/HTML/SessionHistoryEntry.h>
#include <LibWeb/HTML/TokenizedFeatures.h> #include <LibWeb/HTML/TokenizedFeatures.h>
#include <LibWeb/HTML/WebViewHints.h> #include <LibWeb/HTML/WebViewHints.h>
#include <LibWeb/HTML/WorkerAgentForward.h> #include <LibWeb/HTML/WorkerAgentForward.h>
@ -97,7 +100,9 @@ public:
void set_focused_navigable(Badge<EventHandler>, HTML::Navigable&); void set_focused_navigable(Badge<EventHandler>, HTML::Navigable&);
void navigable_document_destroyed(Badge<DOM::Document>, HTML::Navigable&); void navigable_document_destroyed(Badge<DOM::Document>, HTML::Navigable&);
void load(URL::URL const&); void load(URL::URL const&, Bindings::NavigationHistoryBehavior = Bindings::NavigationHistoryBehavior::Auto);
void load(URL::URL const&, Variant<Empty, String, HTML::POSTResource>,
Bindings::NavigationHistoryBehavior = Bindings::NavigationHistoryBehavior::Auto);
void load_html(StringView); void load_html(StringView);
void load_html(StringView, URL::URL const&); void load_html(StringView, URL::URL const&);
@ -105,6 +110,7 @@ public:
void reload(); void reload();
void traverse_the_history_by_delta(int delta); void traverse_the_history_by_delta(int delta);
void traverse_the_history_by_delta_from_ui_process(int delta);
CSSPixelPoint device_to_css_point(DevicePixelPoint) const; CSSPixelPoint device_to_css_point(DevicePixelPoint) const;
DevicePixelPoint css_to_device_point(CSSPixelPoint) const; DevicePixelPoint css_to_device_point(CSSPixelPoint) const;
@ -406,6 +412,12 @@ enum class ContextMenuForInputEventsTarget : u8 {
Yes, Yes,
}; };
enum class HistoryTraversalPrecheck : u8 {
Needed,
AlreadyDone,
SourceDocumentSandboxingAlreadyDone,
};
class PageClient : public JS::Cell { class PageClient : public JS::Cell {
GC_CELL(PageClient, JS::Cell); GC_CELL(PageClient, JS::Cell);
@ -417,7 +429,7 @@ public:
virtual bool has_focus() const { return true; } virtual bool has_focus() const { return true; }
virtual bool has_active_devtools_client() const { return false; } virtual bool has_active_devtools_client() const { return false; }
virtual bool is_url_suitable_for_same_process_navigation([[maybe_unused]] URL::URL const& current_url, [[maybe_unused]] URL::URL const& target_url) const { return true; } virtual bool is_url_suitable_for_same_process_navigation([[maybe_unused]] URL::URL const& current_url, [[maybe_unused]] URL::URL const& target_url) const { return true; }
virtual void request_new_process_for_navigation(URL::URL const&) { } virtual void request_new_process_for_navigation(URL::URL const&, Variant<Empty, String, HTML::POSTResource>, Bindings::NavigationHistoryBehavior) { }
virtual Gfx::Palette palette() const = 0; virtual Gfx::Palette palette() const = 0;
virtual DevicePixelRect screen_rect() const = 0; virtual DevicePixelRect screen_rect() const = 0;
virtual double zoom_level() const = 0; virtual double zoom_level() const = 0;
@ -446,7 +458,13 @@ public:
virtual void page_did_request_minimize_window() { } virtual void page_did_request_minimize_window() { }
virtual void page_did_request_fullscreen_window() { } virtual void page_did_request_fullscreen_window() { }
virtual void page_did_request_exit_fullscreen() { } virtual void page_did_request_exit_fullscreen() { }
virtual void page_did_start_loading(URL::URL const&, bool is_redirect) { (void)is_redirect; } virtual void page_did_start_loading(URL::URL const&, Variant<Empty, String, HTML::POSTResource> document_resource, bool is_redirect, Bindings::NavigationHistoryBehavior history_handling = Bindings::NavigationHistoryBehavior::Auto)
{
(void)document_resource;
(void)is_redirect;
(void)history_handling;
}
virtual void page_did_cancel_loading(URL::URL const&) { }
virtual void page_did_create_new_document(Web::DOM::Document&) { } virtual void page_did_create_new_document(Web::DOM::Document&) { }
virtual void page_did_change_active_document_in_top_level_browsing_context(Web::DOM::Document&) { } virtual void page_did_change_active_document_in_top_level_browsing_context(Web::DOM::Document&) { }
virtual void page_did_finish_loading(URL::URL const&) { } virtual void page_did_finish_loading(URL::URL const&) { }
@ -499,6 +517,10 @@ public:
virtual void page_did_request_activate_tab() { } virtual void page_did_request_activate_tab() { }
virtual void page_did_close_top_level_traversable() { } virtual void page_did_close_top_level_traversable() { }
virtual void page_did_update_navigation_buttons_state([[maybe_unused]] bool back_enabled, [[maybe_unused]] bool forward_enabled) { } virtual void page_did_update_navigation_buttons_state([[maybe_unused]] bool back_enabled, [[maybe_unused]] bool forward_enabled) { }
virtual bool should_report_session_history_updates() const { return true; }
virtual void page_did_update_session_history([[maybe_unused]] Vector<HTML::SessionHistoryEntryDescriptor> const& entries, [[maybe_unused]] Vector<i32> const& used_steps, [[maybe_unused]] size_t current_used_step_index) { }
virtual String page_did_request_ui_process_session_history_for_testing() { return "{}"_string; }
virtual bool page_did_request_traverse_the_history_by_delta([[maybe_unused]] int delta, [[maybe_unused]] HistoryTraversalPrecheck history_traversal_precheck) { return false; }
virtual void page_did_change_needs_beforeunload_check([[maybe_unused]] bool needs_beforeunload_check) { } virtual void page_did_change_needs_beforeunload_check([[maybe_unused]] bool needs_beforeunload_check) { }
virtual void request_file(FileRequest) = 0; virtual void request_file(FileRequest) = 0;

View file

@ -41,9 +41,10 @@ void set_default_interface_mode(InterfaceMode interface_mode)
static Response deserialize_as_ladybird_capability(StringView name, JsonValue value) static Response deserialize_as_ladybird_capability(StringView name, JsonValue value)
{ {
if (name == "ladybird:headless"sv) { if (name.is_one_of("ladybird:headless"sv, "ladybird:enableTestHooks"sv)) {
if (!value.is_bool()) if (!value.is_bool())
return Error::from_code(ErrorCode::InvalidArgument, "Extension capability ladybird:headless must be a boolean"sv); return Error::from_code(ErrorCode::InvalidArgument,
MUST(String::formatted("Extension capability {} must be a boolean", name)));
} }
return value; return value;
@ -52,6 +53,7 @@ static Response deserialize_as_ladybird_capability(StringView name, JsonValue va
static void set_default_ladybird_capabilities(JsonObject& options) static void set_default_ladybird_capabilities(JsonObject& options)
{ {
options.set("ladybird:headless"sv, default_interface_mode == InterfaceMode::Headless); options.set("ladybird:headless"sv, default_interface_mode == InterfaceMode::Headless);
options.set("ladybird:enableTestHooks"sv, false);
} }
// https://w3c.github.io/webdriver/#dfn-validate-capabilities // https://w3c.github.io/webdriver/#dfn-validate-capabilities
@ -440,6 +442,8 @@ LadybirdOptions::LadybirdOptions(JsonObject const& capabilities)
{ {
if (auto headless = capabilities.get_bool("ladybird:headless"sv); headless.has_value()) if (auto headless = capabilities.get_bool("ladybird:headless"sv); headless.has_value())
this->headless = *headless; this->headless = *headless;
if (auto enable_test_hooks = capabilities.get_bool("ladybird:enableTestHooks"sv); enable_test_hooks.has_value())
this->enable_test_hooks = *enable_test_hooks;
} }
} }

View file

@ -48,6 +48,7 @@ struct WEB_API LadybirdOptions {
explicit LadybirdOptions(JsonObject const& capabilities); explicit LadybirdOptions(JsonObject const& capabilities);
bool headless { false }; bool headless { false };
bool enable_test_hooks { false };
}; };
WEB_API Response process_capabilities(JsonValue const& parameters, SessionFlags flags); WEB_API Response process_capabilities(JsonValue const& parameters, SessionFlags flags);

View file

@ -71,6 +71,11 @@ static constexpr auto s_webdriver_endpoints = Array {
ROUTE(POST, "/session/:session_id/window/minimize"sv, minimize_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/fullscreen"sv, fullscreen_window),
ROUTE(POST, "/session/:session_id/window/consume-user-activation"sv, consume_user_activation), ROUTE(POST, "/session/:session_id/window/consume-user-activation"sv, consume_user_activation),
ROUTE(POST, "/session/:session_id/ladybird/crash-current-page"sv, crash_current_page),
ROUTE(POST, "/session/:session_id/ladybird/load-url-from-ui"sv, load_url_from_ui),
ROUTE(POST, "/session/:session_id/ladybird/traverse-history-from-ui"sv, traverse_history_from_ui),
ROUTE(POST, "/session/:session_id/ladybird/mark-web-content-session-history-stale"sv, mark_web_content_session_history_stale),
ROUTE(GET, "/session/:session_id/ladybird/session-history"sv, get_session_history),
ROUTE(POST, "/session/:session_id/element"sv, find_element), ROUTE(POST, "/session/:session_id/element"sv, find_element),
ROUTE(POST, "/session/:session_id/elements"sv, find_elements), 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/element"sv, find_element_from_element),

View file

@ -65,6 +65,13 @@ public:
// Extension: https://html.spec.whatwg.org/multipage/interaction.html#user-activation-user-agent-automation // Extension: https://html.spec.whatwg.org/multipage/interaction.html#user-activation-user-agent-automation
virtual Response consume_user_activation(Parameters parameters, JsonValue payload) = 0; virtual Response consume_user_activation(Parameters parameters, JsonValue payload) = 0;
// Ladybird extension for browser integration tests.
virtual Response crash_current_page(Parameters parameters, JsonValue payload) = 0;
virtual Response load_url_from_ui(Parameters parameters, JsonValue payload) = 0;
virtual Response traverse_history_from_ui(Parameters parameters, JsonValue payload) = 0;
virtual Response mark_web_content_session_history_stale(Parameters parameters, JsonValue payload) = 0;
virtual Response get_session_history(Parameters parameters, JsonValue payload) = 0;
// 12. Elements, https://w3c.github.io/webdriver/#elements // 12. Elements, https://w3c.github.io/webdriver/#elements
virtual Response find_element(Parameters parameters, JsonValue payload) = 0; virtual Response find_element(Parameters parameters, JsonValue payload) = 0;
virtual Response find_elements(Parameters parameters, JsonValue payload) = 0; virtual Response find_elements(Parameters parameters, JsonValue payload) = 0;

View file

@ -6,6 +6,7 @@
#include <AK/Array.h> #include <AK/Array.h>
#include <AK/JsonObject.h> #include <AK/JsonObject.h>
#include <AK/NeverDestroyed.h>
#include <LibIPC/Decoder.h> #include <LibIPC/Decoder.h>
#include <LibIPC/Encoder.h> #include <LibIPC/Encoder.h>
#include <LibWeb/WebDriver/Error.h> #include <LibWeb/WebDriver/Error.h>
@ -116,6 +117,35 @@ void set_user_prompt_handler(UserPromptHandler user_prompt_handler)
user_prompt_handler_storage() = move(user_prompt_handler); user_prompt_handler_storage() = move(user_prompt_handler);
} }
// https://w3c.github.io/webdriver/#dfn-get-the-prompt-handler
PromptHandlerConfiguration get_the_prompt_handler(PromptType type)
{
static NeverDestroyed<UserPromptHandler::ValueType> empty_user_prompt_handler;
// 1. If the user prompt handler is null, let handlers be an empty map. Otherwise let handlers be user prompt
// handler.
auto const& handlers = user_prompt_handler_storage().has_value() ? *user_prompt_handler_storage() : *empty_user_prompt_handler;
// 2. If handlers contains type return handlers[type].
if (auto handler = handlers.get(type); handler.has_value())
return *handler;
// 3. If handlers contains "default" return handlers["default"].
if (auto handler = handlers.get(PromptType::Default); handler.has_value())
return *handler;
// 4. If type is "beforeUnload", return a prompt handler configuration with handler "accept" and notify false.
if (type == PromptType::BeforeUnload)
return { .handler = PromptHandler::Accept, .notify = PromptHandlerConfiguration::Notify::No };
// 5. If handlers contains "fallbackDefault" return handlers["fallbackDefault"].
if (auto handler = handlers.get(PromptType::FallbackDefault); handler.has_value())
return *handler;
// 6. Return a prompt handler configuration with handler "dismiss" and notify true.
return { .handler = PromptHandler::Dismiss, .notify = PromptHandlerConfiguration::Notify::Yes };
}
// https://w3c.github.io/webdriver/#dfn-deserialize-as-an-unhandled-prompt-behavior // https://w3c.github.io/webdriver/#dfn-deserialize-as-an-unhandled-prompt-behavior
Response deserialize_as_an_unhandled_prompt_behavior(JsonValue value) Response deserialize_as_an_unhandled_prompt_behavior(JsonValue value)
{ {

View file

@ -56,6 +56,7 @@ using UserPromptHandler = Optional<HashMap<PromptType, PromptHandlerConfiguratio
WEB_API UserPromptHandler const& user_prompt_handler(); WEB_API UserPromptHandler const& user_prompt_handler();
WEB_API void set_user_prompt_handler(UserPromptHandler); WEB_API void set_user_prompt_handler(UserPromptHandler);
WEB_API PromptHandlerConfiguration get_the_prompt_handler(PromptType);
Response deserialize_as_an_unhandled_prompt_behavior(JsonValue); Response deserialize_as_an_unhandled_prompt_behavior(JsonValue);
bool check_user_prompt_handler_matches(JsonObject const&); bool check_user_prompt_handler_matches(JsonObject const&);
WEB_API void update_the_user_prompt_handler(JsonObject const&); WEB_API void update_the_user_prompt_handler(JsonObject const&);

View file

@ -1818,7 +1818,7 @@ void Application::navigate_tab(DevTools::TabDescription const& description, Stri
void Application::traverse_the_history_by_delta(DevTools::TabDescription const& description, int delta) const void Application::traverse_the_history_by_delta(DevTools::TabDescription const& description, int delta) const
{ {
if (auto view = ViewImplementation::find_view_by_id(description.id); view.has_value()) if (auto view = ViewImplementation::find_view_by_id(description.id); view.has_value())
view->traverse_the_history_by_delta(delta); (void)view->traverse_the_history_by_delta(delta);
} }
Vector<HTTP::Cookie::Cookie> Application::cookies(DevTools::TabDescription const& description) const Vector<HTTP::Cookie::Cookie> Application::cookies(DevTools::TabDescription const& description) const

View file

@ -122,6 +122,8 @@ ErrorOr<NonnullRefPtr<WebView::WebContentClient>> launch_web_content_process(u64
arguments.append("--disable-async-scrolling"sv); arguments.append("--disable-async-scrolling"sv);
if (web_content_options.file_scheme_urls_have_tuple_origins == FileSchemeUrlsHaveTupleOrigins::Yes) if (web_content_options.file_scheme_urls_have_tuple_origins == FileSchemeUrlsHaveTupleOrigins::Yes)
arguments.append("--tuple-file-origins"sv); arguments.append("--tuple-file-origins"sv);
if (web_content_options.report_session_history_updates_in_test_mode == ReportSessionHistoryUpdatesInTestMode::Yes)
arguments.append("--report-session-history-updates-in-test-mode"sv);
if (browser_options.enable_sandbox == EnableSandbox::Yes) if (browser_options.enable_sandbox == EnableSandbox::Yes)
arguments.append("--enable-sandbox"sv); arguments.append("--enable-sandbox"sv);

View file

@ -180,6 +180,11 @@ enum class FileSchemeUrlsHaveTupleOrigins {
Yes, Yes,
}; };
enum class ReportSessionHistoryUpdatesInTestMode {
No,
Yes,
};
struct WebContentOptions { struct WebContentOptions {
Optional<ByteString> config_path {}; Optional<ByteString> config_path {};
Optional<StringView> user_agent_preset {}; Optional<StringView> user_agent_preset {};
@ -198,6 +203,7 @@ struct WebContentOptions {
PaintViewportScrollbars paint_viewport_scrollbars { PaintViewportScrollbars::Yes }; PaintViewportScrollbars paint_viewport_scrollbars { PaintViewportScrollbars::Yes };
EnableAsyncScrolling enable_async_scrolling { EnableAsyncScrolling::Yes }; EnableAsyncScrolling enable_async_scrolling { EnableAsyncScrolling::Yes };
FileSchemeUrlsHaveTupleOrigins file_scheme_urls_have_tuple_origins { FileSchemeUrlsHaveTupleOrigins::No }; FileSchemeUrlsHaveTupleOrigins file_scheme_urls_have_tuple_origins { FileSchemeUrlsHaveTupleOrigins::No };
ReportSessionHistoryUpdatesInTestMode report_session_history_updates_in_test_mode { ReportSessionHistoryUpdatesInTestMode::No };
Optional<StringView> default_time_zone {}; Optional<StringView> default_time_zone {};
Optional<u64> style_invalidation_counter_dump_interval {}; Optional<u64> style_invalidation_counter_dump_interval {};
}; };

File diff suppressed because it is too large Load diff

View file

@ -16,6 +16,7 @@
#include <AK/OwnPtr.h> #include <AK/OwnPtr.h>
#include <AK/Queue.h> #include <AK/Queue.h>
#include <AK/String.h> #include <AK/String.h>
#include <AK/Types.h>
#include <AK/Utf16String.h> #include <AK/Utf16String.h>
#include <LibCore/AnonymousBuffer.h> #include <LibCore/AnonymousBuffer.h>
#include <LibCore/Forward.h> #include <LibCore/Forward.h>
@ -30,6 +31,7 @@
#include <LibHTTP/Header.h> #include <LibHTTP/Header.h>
#include <LibRequests/Forward.h> #include <LibRequests/Forward.h>
#include <LibRequests/NetworkError.h> #include <LibRequests/NetworkError.h>
#include <LibWeb/Bindings/Navigation.h>
#include <LibWeb/Forward.h> #include <LibWeb/Forward.h>
#include <LibWeb/HTML/ActivateTab.h> #include <LibWeb/HTML/ActivateTab.h>
#include <LibWeb/HTML/AudioPlayState.h> #include <LibWeb/HTML/AudioPlayState.h>
@ -39,10 +41,12 @@
#include <LibWeb/Page/EventResult.h> #include <LibWeb/Page/EventResult.h>
#include <LibWeb/Page/InputEvent.h> #include <LibWeb/Page/InputEvent.h>
#include <LibWeb/Page/ViewportIsFullscreen.h> #include <LibWeb/Page/ViewportIsFullscreen.h>
#include <LibWeb/WebDriver/Response.h>
#include <LibWebView/BookmarkStore.h> #include <LibWebView/BookmarkStore.h>
#include <LibWebView/DOMNodeProperties.h> #include <LibWebView/DOMNodeProperties.h>
#include <LibWebView/Forward.h> #include <LibWebView/Forward.h>
#include <LibWebView/PageInfo.h> #include <LibWebView/PageInfo.h>
#include <LibWebView/SessionHistory.h>
#include <LibWebView/Settings.h> #include <LibWebView/Settings.h>
#include <LibWebView/StorageSetResult.h> #include <LibWebView/StorageSetResult.h>
#include <LibWebView/WebContentClient.h> #include <LibWebView/WebContentClient.h>
@ -73,7 +77,7 @@ public:
String const& handle() const { return m_client_state.client_handle; } String const& handle() const { return m_client_state.client_handle; }
void create_new_process_for_cross_site_navigation(URL::URL const&); void create_new_process_for_cross_site_navigation(URL::URL const&, Variant<Empty, String, Web::HTML::POSTResource>, Web::Bindings::NavigationHistoryBehavior);
void server_did_paint(Badge<WebContentClient>, i32 bitmap_id, Gfx::IntSize size); void server_did_paint(Badge<WebContentClient>, i32 bitmap_id, Gfx::IntSize size);
@ -83,12 +87,32 @@ public:
void set_system_visibility_state(Web::HTML::VisibilityState); void set_system_visibility_state(Web::HTML::VisibilityState);
void load(URL::URL const&); void load(URL::URL const&, Web::Bindings::NavigationHistoryBehavior = Web::Bindings::NavigationHistoryBehavior::Auto);
void load_html(StringView); void load_html(StringView);
void load_navigation_error_page(StringView); void load_navigation_error_page(StringView);
void reload(); void reload();
void traverse_the_history_by_delta(int delta); enum class HistoryTraversalStatus : u8 {
Started,
NoEntry,
Canceled,
};
// NB: The HTML Standard spells this algorithm argument "checkForCancelation".
enum class CheckForCancelation : u8 {
Yes,
No,
IfWebContentCannotTraverseTarget,
};
struct HistoryTraversalOutcome {
HistoryTraversalStatus status { HistoryTraversalStatus::NoEntry };
bool will_replace_web_content_process { false };
bool will_change_top_level_entry { false };
bool waiting_for_cancelation_check { false };
};
[[nodiscard]] HistoryTraversalOutcome traverse_the_history_by_delta(
int delta,
CheckForCancelation = CheckForCancelation::Yes,
Function<void(HistoryTraversalOutcome)> = nullptr);
void zoom_in(); void zoom_in();
void zoom_out(); void zoom_out();
@ -208,7 +232,18 @@ public:
void did_change_audio_play_state(Badge<WebContentClient>, Web::HTML::AudioPlayState); void did_change_audio_play_state(Badge<WebContentClient>, Web::HTML::AudioPlayState);
Web::HTML::AudioPlayState audio_play_state() const { return m_audio_play_state; } Web::HTML::AudioPlayState audio_play_state() const { return m_audio_play_state; }
void did_update_navigation_buttons_state(Badge<WebContentClient>, bool back_enabled, bool forward_enabled) const; void did_update_navigation_buttons_state(Badge<WebContentClient>, bool back_enabled, bool forward_enabled);
void did_update_session_history(Badge<WebContentClient>, Vector<Web::HTML::SessionHistoryEntryDescriptor>, Vector<i32>, size_t current_used_step_index);
void did_set_top_level_session_history(Badge<WebContentClient>, bool accepted, Vector<Web::HTML::SessionHistoryEntryDescriptor>, Vector<i32> used_steps, size_t current_used_step_index);
void did_traverse_the_history_to_step(Badge<WebContentClient>, i32 step, bool step_was_available, Web::HTML::HistoryStepResult);
void did_check_if_traverse_history_step_is_canceled(
Badge<WebContentClient>, u64 request_id, i32 step, bool canceled);
void did_reset_session_history_for_testing(Badge<WebContentClient>);
void mark_web_content_session_history_stale_for_testing(Badge<WebContentClient>);
void did_start_webdriver_navigation(Badge<WebContentClient>, URL::URL const&);
String ui_process_session_history_for_testing(Badge<WebContentClient>) const;
JsonValue webdriver_session_history() const;
void wait_for_webdriver_navigation_completion(Badge<WebContentClient>, Optional<u64> page_load_timeout, Function<void(Web::WebDriver::Response)>);
void did_change_needs_beforeunload_check(Badge<WebContentClient>, bool needs_beforeunload_check); void did_change_needs_beforeunload_check(Badge<WebContentClient>, bool needs_beforeunload_check);
void did_change_background_color(Badge<WebContentClient>, Gfx::Color); void did_change_background_color(Badge<WebContentClient>, Gfx::Color);
Gfx::Color page_background_color() const { return m_page_background_color; } Gfx::Color page_background_color() const { return m_page_background_color; }
@ -339,8 +374,28 @@ protected:
u64 page_id() const; u64 page_id() const;
void set_url(URL::URL); void set_url(URL::URL);
void did_start_navigation(URL::URL const&, Variant<Empty, String, Web::HTML::POSTResource>, bool is_redirect, Web::Bindings::NavigationHistoryBehavior);
void did_cancel_navigation(URL::URL const&);
void did_finish_navigation(URL::URL const&);
void complete_webdriver_navigation_completion(u64 request_id, Web::WebDriver::Response);
void complete_webdriver_pending_navigation_if_url_matches(URL::URL const&);
void update_navigation_action_state();
enum class SessionHistoryDumpMode {
IfDebuggingEnabled,
Always,
};
void dump_session_history(StringView reason, SessionHistoryDumpMode = SessionHistoryDumpMode::IfDebuggingEnabled) const;
bool restore_pending_session_history_navigation(StringView reason);
void abandon_pending_web_content_session_history_seed();
void seed_web_content_session_history_from_ui_process();
void prepare_to_seed_web_content_session_history_from_ui_process();
void restore_current_session_history_entry_from_ui_process();
void load_current_session_history_entry_from_ui_process();
void load_session_history_traversal_target_from_ui_process(TraversableSessionHistory::TraversalTarget const&, StringView dump_reason);
NonnullRefPtr<Core::Promise<Empty>> reset_session_history_for_testing();
virtual void update_zoom(); virtual void update_zoom();
virtual bool should_manage_session_history_in_ui_process() const { return true; }
String current_host() const; String current_host() const;
void apply_zoom_for_current_host(); void apply_zoom_for_current_host();
@ -392,6 +447,7 @@ protected:
URL::URL m_url; URL::URL m_url;
Utf16String m_title; Utf16String m_title;
Optional<String> m_favicon_base64_png; Optional<String> m_favicon_base64_png;
bool m_is_showing_crash_page { false };
double m_zoom_level { 1.0 }; double m_zoom_level { 1.0 };
double m_device_pixel_ratio { 1.0 }; double m_device_pixel_ratio { 1.0 };
@ -463,6 +519,66 @@ protected:
Web::HTML::MuteState m_mute_state { Web::HTML::MuteState::Unmuted }; Web::HTML::MuteState m_mute_state { Web::HTML::MuteState::Unmuted };
struct PendingSessionHistoryNavigation {
enum class WebContentRestoreMode : u8 {
PreserveCurrentProcessState,
RestoreFromUIProcess,
};
URL::URL url;
TraversableSessionHistory previous_session_history;
WebContentRestoreMode web_content_restore_mode { WebContentRestoreMode::PreserveCurrentProcessState };
};
static StringView pending_session_history_navigation_web_content_restore_mode_to_string(PendingSessionHistoryNavigation::WebContentRestoreMode);
struct PendingWebContentSessionHistorySeed {
bool should_send_entries { false };
bool ignore_updates_until_seed { false };
bool waiting_for_ack { false };
bool should_reseed_after_current_history_load { false };
Optional<i32> step_after_loading_top_level_entry;
void clear() { *this = {}; }
};
struct PendingSessionHistoryTraversal {
enum class Stage : u8 {
ApplyingInWebContent,
CheckingCancelation,
LoadingEntryFromUIProcess,
ReplacingWebContentProcess,
RestoringNestedStepAfterSeed,
};
i32 target_step { 0 };
size_t target_step_index { 0 };
u64 cancelation_check_request_id { 0 };
bool will_change_top_level_entry { false };
bool will_replace_web_content_process { false };
Stage stage { Stage::ApplyingInWebContent };
Function<void(HistoryTraversalOutcome)> on_cancelation_check_complete;
};
static StringView pending_session_history_traversal_stage_to_string(PendingSessionHistoryTraversal::Stage);
TraversableSessionHistory m_session_history;
bool m_current_web_content_session_history_matches_mirror { false };
Optional<PendingSessionHistoryNavigation> m_pending_session_history_navigation;
Optional<PendingSessionHistoryTraversal> m_pending_session_history_traversal;
u64 m_next_traverse_history_step_cancelation_check_request_id { 0 };
bool m_loading_session_history_entry_from_ui_process { false };
PendingWebContentSessionHistorySeed m_pending_web_content_session_history_seed;
Optional<URL::URL> m_webdriver_pending_navigation_url;
bool m_webdriver_pending_navigation_completes_with_session_history_update { false };
RefPtr<Core::Promise<Empty>> m_pending_session_history_reset_for_testing;
struct WebDriverNavigationCompletionRequest {
Function<void(Web::WebDriver::Response)> on_complete;
RefPtr<Core::Timer> timer;
u64 navigation_listener_id { 0 };
};
u64 m_next_webdriver_navigation_completion_request_id { 0 };
HashMap<u64, OwnPtr<WebDriverNavigationCompletionRequest>> m_pending_webdriver_navigation_completion_requests;
// Most recent caret position pushed by WebContent, Used for placing platform IME overlays without a sync IPC. // Most recent caret position pushed by WebContent, Used for placing platform IME overlays without a sync IPC.
Optional<Web::DevicePixelRect> m_input_caret_rect; Optional<Web::DevicePixelRect> m_input_caret_rect;

View file

@ -10,12 +10,14 @@
#include <AK/NeverDestroyed.h> #include <AK/NeverDestroyed.h>
#include <AK/WeakPtr.h> #include <AK/WeakPtr.h>
#include <LibCore/ElapsedTimer.h> #include <LibCore/ElapsedTimer.h>
#include <LibCore/EventLoop.h>
#include <LibCore/Timer.h> #include <LibCore/Timer.h>
#include <LibDevTools/StorageHelpers.h> #include <LibDevTools/StorageHelpers.h>
#include <LibHTTP/Cookie/ParsedCookie.h> #include <LibHTTP/Cookie/ParsedCookie.h>
#include <LibIPC/Transport.h> #include <LibIPC/Transport.h>
#include <LibIPC/TransportHandle.h> #include <LibIPC/TransportHandle.h>
#include <LibWeb/Page/InputEvent.h> #include <LibWeb/Page/InputEvent.h>
#include <LibWeb/WebDriver/Error.h>
#include <LibWebView/Application.h> #include <LibWebView/Application.h>
#include <LibWebView/CookieJar.h> #include <LibWebView/CookieJar.h>
#include <LibWebView/HSTSStore.h> #include <LibWebView/HSTSStore.h>
@ -341,10 +343,16 @@ void WebContentClient::did_present_bitmap(u64 page_id, Gfx::IntRect rect, i32 bi
} }
} }
void WebContentClient::did_request_new_process_for_navigation(u64 page_id, URL::URL url) void WebContentClient::did_request_new_process_for_navigation(u64 page_id, URL::URL url, Variant<Empty, String, Web::HTML::POSTResource> document_resource, Web::Bindings::NavigationHistoryBehavior history_handling)
{ {
if (auto view = view_for_page_id(page_id); view.has_value()) if (auto view = view_for_page_id(page_id); view.has_value())
view->create_new_process_for_cross_site_navigation(url); view->create_new_process_for_cross_site_navigation(url, move(document_resource), history_handling);
}
void WebContentClient::did_start_webdriver_navigation(u64 page_id, URL::URL url)
{
if (auto view = view_for_page_id(page_id); view.has_value())
view->did_start_webdriver_navigation({}, url);
} }
void WebContentClient::maybe_record_history_visit_for_current_load(u64 page_id, URL::URL const& url, Optional<String> title, StringView reason) void WebContentClient::maybe_record_history_visit_for_current_load(u64 page_id, URL::URL const& url, Optional<String> title, StringView reason)
@ -366,7 +374,7 @@ void WebContentClient::maybe_record_history_visit_for_current_load(u64 page_id,
m_history_recorded_urls_for_current_load.set(page_id, normalized_url.release_value()); m_history_recorded_urls_for_current_load.set(page_id, normalized_url.release_value());
} }
void WebContentClient::did_start_loading(u64 page_id, URL::URL url, bool is_redirect) void WebContentClient::did_start_loading(u64 page_id, URL::URL url, Variant<Empty, String, Web::HTML::POSTResource> document_resource, bool is_redirect, Web::Bindings::NavigationHistoryBehavior history_handling)
{ {
if (auto process = WebView::Application::the().find_process(m_process_handle.pid); process.has_value()) if (auto process = WebView::Application::the().find_process(m_process_handle.pid); process.has_value())
process->set_title(OptionalNone {}); process->set_title(OptionalNone {});
@ -376,6 +384,7 @@ void WebContentClient::did_start_loading(u64 page_id, URL::URL url, bool is_redi
if (auto view = view_for_page_id(page_id); view.has_value()) { if (auto view = view_for_page_id(page_id); view.has_value()) {
view->m_should_suppress_history_for_current_load = view->m_should_suppress_history_for_next_load; view->m_should_suppress_history_for_current_load = view->m_should_suppress_history_for_next_load;
view->m_should_suppress_history_for_next_load = false; view->m_should_suppress_history_for_next_load = false;
view->did_start_navigation(url, move(document_resource), is_redirect, history_handling);
view->set_url({}, url); view->set_url({}, url);
@ -389,6 +398,14 @@ void WebContentClient::did_start_loading(u64 page_id, URL::URL url, bool is_redi
} }
} }
void WebContentClient::did_cancel_loading(u64 page_id, URL::URL url)
{
m_history_recorded_urls_for_current_load.remove(page_id);
if (auto view = view_for_page_id(page_id); view.has_value())
view->did_cancel_navigation(url);
}
void WebContentClient::did_finish_loading(u64 page_id, URL::URL url) void WebContentClient::did_finish_loading(u64 page_id, URL::URL url)
{ {
if (url.scheme() == "about"sv && url.paths().size() == 1) { if (url.scheme() == "about"sv && url.paths().size() == 1) {
@ -421,6 +438,9 @@ void WebContentClient::did_finish_loading(u64 page_id, URL::URL url)
if (view->favicon_base64_png().has_value()) if (view->favicon_base64_png().has_value())
Application::history_store().update_favicon(url, *view->favicon_base64_png()); Application::history_store().update_favicon(url, *view->favicon_base64_png());
} }
view->did_finish_navigation(client_url);
if (view->on_load_finish) if (view->on_load_finish)
view->on_load_finish(client_url); view->on_load_finish(client_url);
@ -1098,6 +1118,149 @@ void WebContentClient::did_change_needs_beforeunload_check(u64 page_id, bool nee
view->did_change_needs_beforeunload_check({}, needs_beforeunload_check); view->did_change_needs_beforeunload_check({}, needs_beforeunload_check);
} }
void WebContentClient::did_check_if_traverse_history_step_is_canceled(
u64 page_id, u64 request_id, i32 step, bool canceled)
{
if (auto view = view_for_page_id(page_id); view.has_value())
view->did_check_if_traverse_history_step_is_canceled({}, request_id, step, canceled);
}
Messages::WebContentClient::DidRequestTraverseTheHistoryByDeltaResponse WebContentClient::did_request_traverse_the_history_by_delta(u64 page_id, i32 delta, Web::HistoryTraversalPrecheck history_traversal_precheck)
{
if (auto view = view_for_page_id(page_id); view.has_value()) {
auto view_id = view->view_id();
// This request is already a synchronous IPC from WebContent, so defer
// the UI traversal before it possibly calls back into WebContent for
// cancelation checks.
Core::deferred_invoke([view_id, delta, history_traversal_precheck] {
auto view = ViewImplementation::find_view_by_id(view_id);
if (!view.has_value())
return;
auto check_for_cancelation = ViewImplementation::CheckForCancelation::IfWebContentCannotTraverseTarget;
if (history_traversal_precheck == Web::HistoryTraversalPrecheck::Needed)
check_for_cancelation = ViewImplementation::CheckForCancelation::Yes;
// NB: SourceDocumentSandboxingAlreadyDone only covers the source-document sandboxing
// check. If the UI process has to apply the traversal itself, WebContent still needs
// to run the cancelable part of the traverse history step prechecks.
else if (history_traversal_precheck == Web::HistoryTraversalPrecheck::SourceDocumentSandboxingAlreadyDone)
check_for_cancelation = ViewImplementation::CheckForCancelation::Yes;
(void)view->traverse_the_history_by_delta(delta, check_for_cancelation);
});
return true;
}
return false;
}
void WebContentClient::did_request_webdriver_history_traversal(u64 page_id, u64 request_id, i32 delta)
{
if (auto view = view_for_page_id(page_id); view.has_value()) {
auto view_id = view->view_id();
// This request originates from WebDriver in WebContent. Defer the UI
// traversal so it can safely call back into WebContent for the
// cancelation checks from the traverse history step algorithm.
Core::deferred_invoke([this, page_id, request_id, view_id, delta] {
auto view = ViewImplementation::find_view_by_id(view_id);
if (!view.has_value()) {
async_complete_webdriver_history_traversal(page_id, request_id, false, false, false);
return;
}
auto complete = [this, page_id, request_id](ViewImplementation::HistoryTraversalOutcome outcome) {
auto traversal_started = outcome.status == ViewImplementation::HistoryTraversalStatus::Started;
async_complete_webdriver_history_traversal(
page_id,
request_id,
true,
traversal_started && outcome.will_replace_web_content_process,
traversal_started && outcome.will_change_top_level_entry);
};
auto outcome = view->traverse_the_history_by_delta(delta, ViewImplementation::CheckForCancelation::Yes,
[this, page_id, request_id](ViewImplementation::HistoryTraversalOutcome outcome) {
auto traversal_started = outcome.status == ViewImplementation::HistoryTraversalStatus::Started;
async_complete_webdriver_history_traversal(
page_id,
request_id,
true,
traversal_started && outcome.will_replace_web_content_process,
traversal_started && outcome.will_change_top_level_entry);
});
if (!outcome.waiting_for_cancelation_check)
complete(outcome);
});
return;
}
async_complete_webdriver_history_traversal(page_id, request_id, false, false, false);
}
Messages::WebContentClient::DidRequestWebdriverLoadUrlFromUiResponse WebContentClient::did_request_webdriver_load_url_from_ui(u64 page_id, URL::URL url)
{
if (auto view = view_for_page_id(page_id); view.has_value()) {
auto view_id = view->view_id();
if (url.scheme() != "javascript"sv)
view->did_start_webdriver_navigation({}, url);
Core::deferred_invoke([view_id, url = move(url)] {
auto view = ViewImplementation::find_view_by_id(view_id);
if (!view.has_value())
return;
view->load(url);
});
return { JsonValue {} };
}
return { Web::WebDriver::Error::from_code(Web::WebDriver::ErrorCode::NoSuchWindow, "Window not found"sv) };
}
Messages::WebContentClient::DidRequestWebdriverTraverseHistoryFromUiResponse WebContentClient::did_request_webdriver_traverse_history_from_ui(u64 page_id, i32 delta)
{
if (auto view = view_for_page_id(page_id); view.has_value()) {
auto view_id = view->view_id();
// This request is already a synchronous IPC from WebContent, so defer the
// UI traversal before running cancelation checks against WebContent.
Core::deferred_invoke([view_id, delta] {
auto view = ViewImplementation::find_view_by_id(view_id);
if (!view.has_value())
return;
(void)view->traverse_the_history_by_delta(delta, ViewImplementation::CheckForCancelation::Yes);
});
return { JsonValue {} };
}
return { Web::WebDriver::Error::from_code(Web::WebDriver::ErrorCode::NoSuchWindow, "Window not found"sv) };
}
Messages::WebContentClient::DidRequestWebdriverMarkWebContentSessionHistoryStaleResponse WebContentClient::did_request_webdriver_mark_web_content_session_history_stale(u64 page_id)
{
if (auto view = view_for_page_id(page_id); view.has_value()) {
view->mark_web_content_session_history_stale_for_testing({});
return { JsonValue {} };
}
return { Web::WebDriver::Error::from_code(Web::WebDriver::ErrorCode::NoSuchWindow, "Window not found"sv) };
}
Messages::WebContentClient::DidRequestWebdriverSessionHistoryResponse WebContentClient::did_request_webdriver_session_history(u64 page_id)
{
if (auto view = view_for_page_id(page_id); view.has_value())
return { view->webdriver_session_history() };
return { Web::WebDriver::Error::from_code(Web::WebDriver::ErrorCode::NoSuchWindow, "Window not found"sv) };
}
void WebContentClient::did_request_webdriver_navigation_completion(u64 page_id, u64 request_id, Optional<u64> page_load_timeout)
{
if (auto view = view_for_page_id(page_id); view.has_value()) {
view->wait_for_webdriver_navigation_completion({}, page_load_timeout, [this, page_id, request_id](Web::WebDriver::Response response) {
async_complete_webdriver_navigation_completion(page_id, request_id, move(response));
});
return;
}
async_complete_webdriver_navigation_completion(page_id, request_id, Web::WebDriver::Error::from_code(Web::WebDriver::ErrorCode::NoSuchWindow, "Window not found"sv));
}
void WebContentClient::did_update_resource_count(u64 page_id, i32 count_waiting) void WebContentClient::did_update_resource_count(u64 page_id, i32 count_waiting)
{ {
if (auto view = view_for_page_id(page_id); view.has_value()) { if (auto view = view_for_page_id(page_id); view.has_value()) {
@ -1258,8 +1421,43 @@ void WebContentClient::did_change_audio_play_state(u64 page_id, Web::HTML::Audio
void WebContentClient::did_update_navigation_buttons_state(u64 page_id, bool back_enabled, bool forward_enabled) void WebContentClient::did_update_navigation_buttons_state(u64 page_id, bool back_enabled, bool forward_enabled)
{ {
if (auto view = view_for_page_id(page_id); view.has_value()) if (auto view = view_for_page_id(page_id); view.has_value()) {
if (view->should_manage_session_history_in_ui_process())
return;
view->did_update_navigation_buttons_state({}, back_enabled, forward_enabled); view->did_update_navigation_buttons_state({}, back_enabled, forward_enabled);
}
}
void WebContentClient::did_update_session_history(u64 page_id, Vector<Web::HTML::SessionHistoryEntryDescriptor> entries, Vector<i32> used_steps, size_t current_used_step_index)
{
if (auto view = view_for_page_id(page_id); view.has_value())
view->did_update_session_history({}, move(entries), move(used_steps), current_used_step_index);
}
Messages::WebContentClient::DidRequestUiProcessSessionHistoryForTestingResponse WebContentClient::did_request_ui_process_session_history_for_testing(u64 page_id)
{
if (auto view = view_for_page_id(page_id); view.has_value())
return { view->ui_process_session_history_for_testing({}) };
return { "{}"_string };
}
void WebContentClient::did_set_top_level_session_history(u64 page_id, bool accepted, Vector<Web::HTML::SessionHistoryEntryDescriptor> entries, Vector<i32> used_steps, size_t current_used_step_index)
{
if (auto view = view_for_page_id(page_id); view.has_value())
view->did_set_top_level_session_history({}, accepted, move(entries), move(used_steps), current_used_step_index);
}
void WebContentClient::did_traverse_the_history_to_step(u64 page_id, i32 step, bool step_was_available, Web::HTML::HistoryStepResult result)
{
if (auto view = view_for_page_id(page_id); view.has_value())
view->did_traverse_the_history_to_step({}, step, step_was_available, result);
}
void WebContentClient::did_reset_session_history_for_testing(u64 page_id)
{
if (auto view = view_for_page_id(page_id); view.has_value())
view->did_reset_session_history_for_testing({});
} }
void WebContentClient::did_present_backing_stores(u64 page_id, i32 front_bitmap_id, Gfx::SharedImage front_backing_store, i32 back_bitmap_id, Gfx::SharedImage back_backing_store) void WebContentClient::did_present_backing_stores(u64 page_id, i32 front_bitmap_id, Gfx::SharedImage front_backing_store, i32 back_bitmap_id, Gfx::SharedImage back_backing_store)

View file

@ -22,12 +22,14 @@
#include <LibRequests/NetworkError.h> #include <LibRequests/NetworkError.h>
#include <LibRequests/RequestTimingInfo.h> #include <LibRequests/RequestTimingInfo.h>
#include <LibWeb/Bindings/MainThreadVM.h> #include <LibWeb/Bindings/MainThreadVM.h>
#include <LibWeb/Bindings/Navigation.h>
#include <LibWeb/CSS/StyleSheetIdentifier.h> #include <LibWeb/CSS/StyleSheetIdentifier.h>
#include <LibWeb/Compositor/Types.h> #include <LibWeb/Compositor/Types.h>
#include <LibWeb/Forward.h> #include <LibWeb/Forward.h>
#include <LibWeb/HTML/ActivateTab.h> #include <LibWeb/HTML/ActivateTab.h>
#include <LibWeb/HTML/FileFilter.h> #include <LibWeb/HTML/FileFilter.h>
#include <LibWeb/HTML/SelectItem.h> #include <LibWeb/HTML/SelectItem.h>
#include <LibWeb/HTML/SessionHistoryEntry.h>
#include <LibWeb/HTML/WebViewHints.h> #include <LibWeb/HTML/WebViewHints.h>
#include <LibWeb/HTML/WorkerAgentTypes.h> #include <LibWeb/HTML/WorkerAgentTypes.h>
#include <LibWeb/Page/EventResult.h> #include <LibWeb/Page/EventResult.h>
@ -96,7 +98,8 @@ private:
virtual Messages::WebContentClient::AllocateCompositorContextIdResponse allocate_compositor_context_id(u64 page_id, Web::Compositor::PagePresentationRegistration) override; virtual Messages::WebContentClient::AllocateCompositorContextIdResponse allocate_compositor_context_id(u64 page_id, Web::Compositor::PagePresentationRegistration) override;
virtual void did_destroy_compositor_context(Web::Compositor::CompositorContextId) override; virtual void did_destroy_compositor_context(Web::Compositor::CompositorContextId) override;
virtual void did_request_new_process_for_navigation(u64 page_id, URL::URL url) override; virtual void did_request_new_process_for_navigation(u64 page_id, URL::URL url, Variant<Empty, String, Web::HTML::POSTResource> document_resource, Web::Bindings::NavigationHistoryBehavior history_handling) override;
virtual void did_start_webdriver_navigation(u64 page_id, URL::URL url) override;
virtual void did_finish_loading(u64 page_id, URL::URL) override; virtual void did_finish_loading(u64 page_id, URL::URL) override;
virtual void did_request_refresh(u64 page_id) override; virtual void did_request_refresh(u64 page_id) override;
virtual void did_request_cursor_change(u64 page_id, Gfx::Cursor) override; virtual void did_request_cursor_change(u64 page_id, Gfx::Cursor) override;
@ -110,7 +113,8 @@ private:
virtual void did_unhover_link(u64 page_id) override; virtual void did_unhover_link(u64 page_id) override;
virtual void did_click_link(u64 page_id, URL::URL, ByteString, unsigned) override; virtual void did_click_link(u64 page_id, URL::URL, ByteString, unsigned) override;
virtual void did_middle_click_link(u64 page_id, URL::URL, ByteString, unsigned) override; virtual void did_middle_click_link(u64 page_id, URL::URL, ByteString, unsigned) override;
virtual void did_start_loading(u64 page_id, URL::URL, bool) override; virtual void did_start_loading(u64 page_id, URL::URL, Variant<Empty, String, Web::HTML::POSTResource>, bool, Web::Bindings::NavigationHistoryBehavior) override;
virtual void did_cancel_loading(u64 page_id, URL::URL) override;
virtual void did_request_context_menu(u64 page_id, Gfx::IntPoint, Web::ContextMenuForInputEventsTarget) override; virtual void did_request_context_menu(u64 page_id, Gfx::IntPoint, Web::ContextMenuForInputEventsTarget) override;
virtual void did_request_link_context_menu(u64 page_id, Gfx::IntPoint, URL::URL, ByteString, unsigned) override; virtual void did_request_link_context_menu(u64 page_id, Gfx::IntPoint, URL::URL, ByteString, unsigned) override;
virtual void did_request_image_context_menu(u64 page_id, Gfx::IntPoint, URL::URL, ByteString, unsigned, Optional<Gfx::ShareableBitmap>) override; virtual void did_request_image_context_menu(u64 page_id, Gfx::IntPoint, URL::URL, ByteString, unsigned, Optional<Gfx::ShareableBitmap>) override;
@ -167,6 +171,13 @@ private:
virtual void did_request_activate_tab(u64 page_id) override; virtual void did_request_activate_tab(u64 page_id) override;
virtual void did_close_browsing_context(u64 page_id) override; virtual void did_close_browsing_context(u64 page_id) override;
virtual void did_change_needs_beforeunload_check(u64 page_id, bool needs_beforeunload_check) override; virtual void did_change_needs_beforeunload_check(u64 page_id, bool needs_beforeunload_check) override;
virtual Messages::WebContentClient::DidRequestTraverseTheHistoryByDeltaResponse did_request_traverse_the_history_by_delta(u64 page_id, i32 delta, Web::HistoryTraversalPrecheck) override;
virtual void did_request_webdriver_history_traversal(u64 page_id, u64 request_id, i32 delta) override;
virtual Messages::WebContentClient::DidRequestWebdriverLoadUrlFromUiResponse did_request_webdriver_load_url_from_ui(u64 page_id, URL::URL url) override;
virtual Messages::WebContentClient::DidRequestWebdriverTraverseHistoryFromUiResponse did_request_webdriver_traverse_history_from_ui(u64 page_id, i32 delta) override;
virtual Messages::WebContentClient::DidRequestWebdriverMarkWebContentSessionHistoryStaleResponse did_request_webdriver_mark_web_content_session_history_stale(u64 page_id) override;
virtual Messages::WebContentClient::DidRequestWebdriverSessionHistoryResponse did_request_webdriver_session_history(u64 page_id) override;
virtual void did_request_webdriver_navigation_completion(u64 page_id, u64 request_id, Optional<u64> page_load_timeout) override;
virtual void did_update_resource_count(u64 page_id, i32 count_waiting) override; virtual void did_update_resource_count(u64 page_id, i32 count_waiting) override;
virtual void did_request_restore_window(u64 page_id) override; virtual void did_request_restore_window(u64 page_id) override;
virtual void did_request_reposition_window(u64 page_id, Gfx::IntPoint) override; virtual void did_request_reposition_window(u64 page_id, Gfx::IntPoint) override;
@ -194,6 +205,13 @@ private:
virtual void did_update_primary_selection(u64 page_id, String) override; virtual void did_update_primary_selection(u64 page_id, String) override;
virtual void did_change_audio_play_state(u64 page_id, Web::HTML::AudioPlayState) override; virtual void did_change_audio_play_state(u64 page_id, Web::HTML::AudioPlayState) override;
virtual void did_update_navigation_buttons_state(u64 page_id, bool back_enabled, bool forward_enabled) override; virtual void did_update_navigation_buttons_state(u64 page_id, bool back_enabled, bool forward_enabled) override;
virtual void did_update_session_history(u64 page_id, Vector<Web::HTML::SessionHistoryEntryDescriptor>, Vector<i32>, size_t current_used_step_index) override;
virtual Messages::WebContentClient::DidRequestUiProcessSessionHistoryForTestingResponse did_request_ui_process_session_history_for_testing(u64 page_id) override;
virtual void did_set_top_level_session_history(u64 page_id, bool accepted, Vector<Web::HTML::SessionHistoryEntryDescriptor>, Vector<i32> used_steps, size_t current_used_step_index) override;
virtual void did_traverse_the_history_to_step(u64 page_id, i32 step, bool step_was_available, Web::HTML::HistoryStepResult) override;
virtual void did_check_if_traverse_history_step_is_canceled(
u64 page_id, u64 request_id, i32 step, bool canceled) override;
virtual void did_reset_session_history_for_testing(u64 page_id) override;
virtual Messages::WebContentClient::StartWorkerAgentResponse start_worker_agent(u64 page_id, Web::HTML::WorkerAgentStartRequest request) override; virtual Messages::WebContentClient::StartWorkerAgentResponse start_worker_agent(u64 page_id, Web::HTML::WorkerAgentStartRequest request) override;
virtual void close_worker_agent(u64 page_id, Web::HTML::WorkerAgentId agent_id, Web::HTML::WorkerAgentOwnerToken owner_token) override; virtual void close_worker_agent(u64 page_id, Web::HTML::WorkerAgentId agent_id, Web::HTML::WorkerAgentOwnerToken owner_token) override;

View file

@ -46,6 +46,7 @@
#include <LibWeb/Fetch/Fetching/Fetching.h> #include <LibWeb/Fetch/Fetching/Fetching.h>
#include <LibWeb/HTML/BroadcastChannel.h> #include <LibWeb/HTML/BroadcastChannel.h>
#include <LibWeb/HTML/BrowsingContext.h> #include <LibWeb/HTML/BrowsingContext.h>
#include <LibWeb/HTML/EventLoop/EventLoop.h>
#include <LibWeb/HTML/HTMLInputElement.h> #include <LibWeb/HTML/HTMLInputElement.h>
#include <LibWeb/HTML/Navigable.h> #include <LibWeb/HTML/Navigable.h>
#include <LibWeb/HTML/NavigableContainer.h> #include <LibWeb/HTML/NavigableContainer.h>
@ -152,7 +153,7 @@ Messages::WebContentServer::GetWindowHandleResponse ConnectionFromClient::get_wi
void ConnectionFromClient::set_window_handle(u64 page_id, String handle) void ConnectionFromClient::set_window_handle(u64 page_id, String handle)
{ {
if (auto page = this->page(page_id); page.has_value()) { if (auto page = this->page(page_id); page.has_value()) {
page->page().top_level_traversable()->set_window_handle(move(handle)); page->set_window_handle(move(handle));
page->send_current_needs_beforeunload_check(); page->send_current_needs_beforeunload_check();
} }
} }
@ -166,6 +167,18 @@ void ConnectionFromClient::connect_to_webdriver(u64 page_id, ByteString webdrive
} }
} }
void ConnectionFromClient::complete_webdriver_navigation_completion(u64 page_id, u64 request_id, Web::WebDriver::Response response)
{
if (auto page = this->page(page_id); page.has_value())
page->did_complete_webdriver_navigation_completion(request_id, move(response));
}
void ConnectionFromClient::complete_webdriver_history_traversal(u64 page_id, u64 request_id, bool accepted, bool will_replace_web_content_process, bool will_change_top_level_entry)
{
if (auto page = this->page(page_id); page.has_value())
page->did_complete_webdriver_history_traversal(request_id, accepted, will_replace_web_content_process, will_change_top_level_entry);
}
void ConnectionFromClient::connect_to_web_ui(u64 page_id, IPC::TransportHandle handle) void ConnectionFromClient::connect_to_web_ui(u64 page_id, IPC::TransportHandle handle)
{ {
if (auto page = this->page(page_id); page.has_value()) { if (auto page = this->page(page_id); page.has_value()) {
@ -218,13 +231,24 @@ void ConnectionFromClient::update_screen_rects(u64 page_id, Vector<Web::DevicePi
page->set_screen_rects(rects, main_screen); page->set_screen_rects(rects, main_screen);
} }
void ConnectionFromClient::load_url(u64 page_id, URL::URL url) void ConnectionFromClient::load_url(u64 page_id, URL::URL url, Web::Bindings::NavigationHistoryBehavior history_handling)
{ {
auto page = this->page(page_id); auto page = this->page(page_id);
if (!page.has_value()) if (!page.has_value())
return; return;
page->page().load(url); page->page().load(url, history_handling);
}
void ConnectionFromClient::load_url_with_document_resource(u64 page_id, URL::URL url,
Variant<Empty, String, Web::HTML::POSTResource> document_resource,
Web::Bindings::NavigationHistoryBehavior history_handling)
{
auto page = this->page(page_id);
if (!page.has_value())
return;
page->page().load(url, move(document_resource), history_handling);
} }
void ConnectionFromClient::load_html(u64 page_id, ByteString html) void ConnectionFromClient::load_html(u64 page_id, ByteString html)
@ -248,7 +272,65 @@ void ConnectionFromClient::reload(u64 page_id)
void ConnectionFromClient::traverse_the_history_by_delta(u64 page_id, i32 delta) void ConnectionFromClient::traverse_the_history_by_delta(u64 page_id, i32 delta)
{ {
if (auto page = this->page(page_id); page.has_value()) if (auto page = this->page(page_id); page.has_value())
page->page().traverse_the_history_by_delta(delta); page->page().traverse_the_history_by_delta_from_ui_process(delta);
}
void ConnectionFromClient::traverse_the_history_to_step(u64 page_id, i32 step)
{
auto page = this->page(page_id);
if (!page.has_value()) {
async_did_traverse_the_history_to_step(page_id, step, false, Web::HTML::HistoryStepResult::Applied);
return;
}
page->page().top_level_traversable()->traverse_the_history_to_step(step,
GC::create_function(Web::HTML::main_thread_event_loop().heap(), [this, page_id, step](bool step_was_available, Web::HTML::HistoryStepResult result) {
async_did_traverse_the_history_to_step(page_id, step, step_was_available, result);
}));
}
void ConnectionFromClient::check_if_traverse_history_step_is_canceled(u64 page_id, u64 request_id, i32 step)
{
auto page = this->page(page_id);
if (!page.has_value()) {
async_did_check_if_traverse_history_step_is_canceled(page_id, request_id, step, true);
return;
}
auto& heap = Web::HTML::main_thread_event_loop().heap();
page->page().top_level_traversable()->check_if_traverse_history_step_is_canceled(step,
GC::create_function(heap, [this, page_id, request_id, step](Web::HTML::HistoryStepResult result) {
async_did_check_if_traverse_history_step_is_canceled(
page_id, request_id, step, result != Web::HTML::HistoryStepResult::Applied);
}));
}
void ConnectionFromClient::set_top_level_session_history(u64 page_id, Vector<Web::HTML::SessionHistoryEntryDescriptor> entries, size_t current_top_level_entry_index)
{
if (auto page = this->page(page_id); page.has_value()) {
auto accepted = page->page().top_level_traversable()->replace_top_level_session_history_entries_from_ui_process(move(entries), current_top_level_entry_index);
if (accepted) {
auto session_history_snapshot = page->page().top_level_traversable()->create_session_history_snapshot();
async_did_set_top_level_session_history(page_id, accepted, move(session_history_snapshot.top_level_session_history_entries), move(session_history_snapshot.used_session_history_steps), session_history_snapshot.current_used_step_index);
} else {
async_did_set_top_level_session_history(page_id, accepted, {}, {}, 0);
}
} else {
async_did_set_top_level_session_history(page_id, false, {}, {}, 0);
}
}
void ConnectionFromClient::reset_session_history_for_testing(u64 page_id)
{
if (auto page = this->page(page_id); page.has_value()) {
auto& event_loop = Web::HTML::main_thread_event_loop();
page->page().top_level_traversable()->reset_session_history_for_testing(
GC::create_function(event_loop.heap(), [this, page_id] {
async_did_reset_session_history_for_testing(page_id);
}));
} else {
async_did_reset_session_history_for_testing(page_id);
}
} }
void ConnectionFromClient::set_viewport(u64 page_id, Web::DevicePixelSize size, double device_pixel_ratio, Web::ViewportIsFullscreen is_fullscreen) void ConnectionFromClient::set_viewport(u64 page_id, Web::DevicePixelSize size, double device_pixel_ratio, Web::ViewportIsFullscreen is_fullscreen)

View file

@ -18,11 +18,13 @@
#include <LibGC/Root.h> #include <LibGC/Root.h>
#include <LibIPC/ConnectionFromClient.h> #include <LibIPC/ConnectionFromClient.h>
#include <LibJS/Forward.h> #include <LibJS/Forward.h>
#include <LibWeb/Bindings/Navigation.h>
#include <LibWeb/CSS/PreferredColorScheme.h> #include <LibWeb/CSS/PreferredColorScheme.h>
#include <LibWeb/CSS/PreferredContrast.h> #include <LibWeb/CSS/PreferredContrast.h>
#include <LibWeb/CSS/PreferredMotion.h> #include <LibWeb/CSS/PreferredMotion.h>
#include <LibWeb/Compositor/Types.h> #include <LibWeb/Compositor/Types.h>
#include <LibWeb/Forward.h> #include <LibWeb/Forward.h>
#include <LibWeb/HTML/SessionHistoryEntry.h>
#include <LibWeb/HTML/WorkerAgentTypes.h> #include <LibWeb/HTML/WorkerAgentTypes.h>
#include <LibWeb/Loader/FileRequest.h> #include <LibWeb/Loader/FileRequest.h>
#include <LibWeb/Page/EventResult.h> #include <LibWeb/Page/EventResult.h>
@ -72,6 +74,8 @@ private:
virtual Messages::WebContentServer::GetWindowHandleResponse get_window_handle(u64 page_id) override; virtual Messages::WebContentServer::GetWindowHandleResponse get_window_handle(u64 page_id) override;
virtual void set_window_handle(u64 page_id, String handle) override; virtual void set_window_handle(u64 page_id, String handle) override;
virtual void connect_to_webdriver(u64 page_id, ByteString webdriver_endpoint) override; virtual void connect_to_webdriver(u64 page_id, ByteString webdriver_endpoint) override;
virtual void complete_webdriver_history_traversal(u64 page_id, u64 request_id, bool accepted, bool will_replace_web_content_process, bool will_change_top_level_entry) override;
virtual void complete_webdriver_navigation_completion(u64 page_id, u64 request_id, Web::WebDriver::Response response) override;
virtual void connect_to_web_ui(u64 page_id, IPC::TransportHandle handle) override; virtual void connect_to_web_ui(u64 page_id, IPC::TransportHandle handle) override;
virtual void connect_to_request_server(IPC::TransportHandle handle) override; virtual void connect_to_request_server(IPC::TransportHandle handle) override;
virtual void connect_to_image_decoder(IPC::TransportHandle handle) override; virtual void connect_to_image_decoder(IPC::TransportHandle handle) override;
@ -79,11 +83,17 @@ private:
virtual void compositor_process_reconnected() override; virtual void compositor_process_reconnected() override;
virtual void update_system_theme(u64 page_id, Core::AnonymousBuffer) override; virtual void update_system_theme(u64 page_id, Core::AnonymousBuffer) override;
virtual void update_screen_rects(u64 page_id, Vector<Web::DevicePixelRect>, u32) override; virtual void update_screen_rects(u64 page_id, Vector<Web::DevicePixelRect>, u32) override;
virtual void load_url(u64 page_id, URL::URL) override; virtual void load_url(u64 page_id, URL::URL, Web::Bindings::NavigationHistoryBehavior) override;
virtual void load_url_with_document_resource(u64 page_id, URL::URL,
Variant<Empty, String, Web::HTML::POSTResource>, Web::Bindings::NavigationHistoryBehavior) override;
virtual void load_html(u64 page_id, ByteString) override; virtual void load_html(u64 page_id, ByteString) override;
virtual void load_html_with_url(u64 page_id, ByteString, URL::URL) override; virtual void load_html_with_url(u64 page_id, ByteString, URL::URL) override;
virtual void reload(u64 page_id) override; virtual void reload(u64 page_id) override;
virtual void traverse_the_history_by_delta(u64 page_id, i32 delta) override; virtual void traverse_the_history_by_delta(u64 page_id, i32 delta) override;
virtual void traverse_the_history_to_step(u64 page_id, i32 step) override;
virtual void check_if_traverse_history_step_is_canceled(u64 page_id, u64 request_id, i32 step) override;
virtual void set_top_level_session_history(u64 page_id, Vector<Web::HTML::SessionHistoryEntryDescriptor>, size_t current_top_level_entry_index) override;
virtual void reset_session_history_for_testing(u64 page_id) override;
virtual void set_viewport(u64 page_id, Web::DevicePixelSize, double device_pixel_ratio, Web::ViewportIsFullscreen is_fullscreen) override; virtual void set_viewport(u64 page_id, Web::DevicePixelSize, double device_pixel_ratio, Web::ViewportIsFullscreen is_fullscreen) override;
virtual void key_event(u64 page_id, Web::KeyEvent) override; virtual void key_event(u64 page_id, Web::KeyEvent) override;
virtual void mouse_event(u64 page_id, Web::MouseEvent) override; virtual void mouse_event(u64 page_id, Web::MouseEvent) override;

View file

@ -34,6 +34,7 @@
#include <LibWeb/HTML/Scripting/ClassicScript.h> #include <LibWeb/HTML/Scripting/ClassicScript.h>
#include <LibWeb/HTML/Scripting/TemporaryExecutionContext.h> #include <LibWeb/HTML/Scripting/TemporaryExecutionContext.h>
#include <LibWeb/HTML/TraversableNavigable.h> #include <LibWeb/HTML/TraversableNavigable.h>
#include <LibWeb/HTML/Window.h>
#include <LibWeb/HighResolutionTime/TimeOrigin.h> #include <LibWeb/HighResolutionTime/TimeOrigin.h>
#include <LibWeb/InvalidateDisplayList.h> #include <LibWeb/InvalidateDisplayList.h>
#include <LibWeb/Layout/Viewport.h> #include <LibWeb/Layout/Viewport.h>
@ -54,6 +55,7 @@ namespace WebContent {
static PageClient::UseSkiaPainter s_use_skia_painter = PageClient::UseSkiaPainter::GPUBackendIfAvailable; static PageClient::UseSkiaPainter s_use_skia_painter = PageClient::UseSkiaPainter::GPUBackendIfAvailable;
static bool s_is_headless { false }; static bool s_is_headless { false };
static bool s_async_scrolling_enabled { false }; static bool s_async_scrolling_enabled { false };
static bool s_should_report_session_history_updates_in_test_mode { false };
GC_DEFINE_ALLOCATOR(PageClient); GC_DEFINE_ALLOCATOR(PageClient);
@ -86,6 +88,11 @@ void PageClient::set_async_scrolling_enabled(bool enabled)
s_async_scrolling_enabled = enabled; s_async_scrolling_enabled = enabled;
} }
void PageClient::set_should_report_session_history_updates_in_test_mode(bool should_report)
{
s_should_report_session_history_updates_in_test_mode = should_report;
}
GC::Ref<PageClient> PageClient::create(JS::VM& vm, PageHost& page_host, u64 id) GC::Ref<PageClient> PageClient::create(JS::VM& vm, PageHost& page_host, u64 id)
{ {
return vm.heap().allocate<PageClient>(page_host, id); return vm.heap().allocate<PageClient>(page_host, id);
@ -143,6 +150,19 @@ void PageClient::set_has_focus(bool has_focus)
} }
} }
void PageClient::set_window_handle(String window_handle)
{
page().top_level_traversable()->set_window_handle(move(window_handle));
if (m_webdriver)
m_webdriver->page_did_set_window_handle({}, page().top_level_traversable()->window_handle());
}
void PageClient::did_start_webdriver_navigation(URL::URL const& url)
{
client().async_did_start_webdriver_navigation(m_id, url);
}
void PageClient::setup_palette() void PageClient::setup_palette()
{ {
// FIXME: Get the proper palette from our peer somehow // FIXME: Get the proper palette from our peer somehow
@ -165,9 +185,12 @@ bool PageClient::is_url_suitable_for_same_process_navigation(URL::URL const& cur
return WebView::is_url_suitable_for_same_process_navigation(current_url, target_url); return WebView::is_url_suitable_for_same_process_navigation(current_url, target_url);
} }
void PageClient::request_new_process_for_navigation(URL::URL const& url) void PageClient::request_new_process_for_navigation(URL::URL const& url, Variant<Empty, String, Web::HTML::POSTResource> document_resource, Web::Bindings::NavigationHistoryBehavior history_handling)
{ {
client().async_did_request_new_process_for_navigation(m_id, url); if (m_webdriver)
m_webdriver->page_did_start_window_replacement({}, page().top_level_traversable()->window_handle());
client().async_did_request_new_process_for_navigation(m_id, url, move(document_resource), history_handling);
} }
Gfx::Palette PageClient::palette() const Gfx::Palette PageClient::palette() const
@ -384,9 +407,20 @@ void PageClient::page_did_middle_click_link(URL::URL const& url, ByteString cons
client().async_did_middle_click_link(m_id, url, target, modifiers); client().async_did_middle_click_link(m_id, url, target, modifiers);
} }
void PageClient::page_did_start_loading(URL::URL const& url, bool is_redirect) void PageClient::page_did_start_loading(URL::URL const& url, Variant<Empty, String, Web::HTML::POSTResource> document_resource, bool is_redirect, Web::Bindings::NavigationHistoryBehavior history_handling)
{ {
client().async_did_start_loading(m_id, url, is_redirect); if (m_webdriver)
m_webdriver->page_did_start_loading({}, url);
client().async_did_start_loading(m_id, url, move(document_resource), is_redirect, history_handling);
}
void PageClient::page_did_cancel_loading(URL::URL const& url)
{
if (m_webdriver)
m_webdriver->page_did_cancel_loading({}, url);
client().async_did_cancel_loading(m_id, url);
} }
void PageClient::page_did_create_new_document(Web::DOM::Document& document) void PageClient::page_did_create_new_document(Web::DOM::Document& document)
@ -415,6 +449,22 @@ void PageClient::page_did_finish_loading(URL::URL const& url)
client().async_did_finish_loading(m_id, url); client().async_did_finish_loading(m_id, url);
} }
void PageClient::wait_for_webdriver_navigation_completion(Optional<u64> page_load_timeout, Function<void(Web::WebDriver::Response)> on_complete)
{
auto request_id = m_next_webdriver_navigation_completion_request_id++;
m_pending_webdriver_navigation_completion_requests.set(request_id, move(on_complete));
client().async_did_request_webdriver_navigation_completion(m_id, request_id, page_load_timeout);
}
void PageClient::did_complete_webdriver_navigation_completion(u64 request_id, Web::WebDriver::Response response)
{
auto maybe_callback = m_pending_webdriver_navigation_completion_requests.take(request_id);
if (!maybe_callback.has_value())
return;
maybe_callback.value()(move(response));
}
void PageClient::page_did_finish_test(String const& text) void PageClient::page_did_finish_test(String const& text)
{ {
client().async_did_finish_test(m_id, text); client().async_did_finish_test(m_id, text);
@ -764,6 +814,9 @@ void PageClient::page_did_close_top_level_traversable()
{ {
page().top_level_traversable()->compositor_context().set_presentation_mode(Empty {}); page().top_level_traversable()->compositor_context().set_presentation_mode(Empty {});
if (m_webdriver)
m_webdriver->page_did_close_window({}, page().top_level_traversable()->window_handle());
// FIXME: Rename this IPC call // FIXME: Rename this IPC call
client().async_did_close_browsing_context(m_id); client().async_did_close_browsing_context(m_id);
@ -787,6 +840,66 @@ void PageClient::page_did_update_navigation_buttons_state(bool back_enabled, boo
client().async_did_update_navigation_buttons_state(m_id, back_enabled, forward_enabled); client().async_did_update_navigation_buttons_state(m_id, back_enabled, forward_enabled);
} }
bool PageClient::should_report_session_history_updates() const
{
return !Web::HTML::Window::in_test_mode() || s_should_report_session_history_updates_in_test_mode;
}
void PageClient::page_did_update_session_history(Vector<Web::HTML::SessionHistoryEntryDescriptor> const& entries, Vector<i32> const& used_steps, size_t current_used_step_index)
{
client().async_did_update_session_history(m_id, entries, used_steps, current_used_step_index);
}
String PageClient::page_did_request_ui_process_session_history_for_testing()
{
return client().did_request_ui_process_session_history_for_testing(m_id);
}
bool PageClient::page_did_request_traverse_the_history_by_delta(int delta, Web::HistoryTraversalPrecheck history_traversal_precheck)
{
return client().did_request_traverse_the_history_by_delta(m_id, delta, history_traversal_precheck);
}
void PageClient::request_webdriver_history_traversal(int delta, Function<void(WebDriverHistoryTraversalResult)> on_complete)
{
auto request_id = m_next_webdriver_history_traversal_request_id++;
m_pending_webdriver_history_traversal_requests.set(request_id, move(on_complete));
client().async_did_request_webdriver_history_traversal(m_id, request_id, delta);
}
void PageClient::did_complete_webdriver_history_traversal(u64 request_id, bool accepted, bool will_replace_web_content_process, bool will_change_top_level_entry)
{
auto maybe_callback = m_pending_webdriver_history_traversal_requests.take(request_id);
if (!maybe_callback.has_value())
return;
maybe_callback.value()(WebDriverHistoryTraversalResult {
.accepted = accepted,
.will_replace_web_content_process = will_replace_web_content_process,
.will_change_top_level_entry = will_change_top_level_entry,
});
}
Web::WebDriver::Response PageClient::request_webdriver_load_url_from_ui(URL::URL const& url)
{
return client().did_request_webdriver_load_url_from_ui(m_id, url);
}
Web::WebDriver::Response PageClient::request_webdriver_traverse_history_from_ui(int delta)
{
return client().did_request_webdriver_traverse_history_from_ui(m_id, delta);
}
Web::WebDriver::Response PageClient::request_webdriver_mark_web_content_session_history_stale()
{
return client().did_request_webdriver_mark_web_content_session_history_stale(m_id);
}
Web::WebDriver::Response PageClient::request_webdriver_session_history()
{
return client().did_request_webdriver_session_history(m_id);
}
void PageClient::request_file(Web::FileRequest file_request) void PageClient::request_file(Web::FileRequest file_request)
{ {
client().request_file(m_id, move(file_request)); client().request_file(m_id, move(file_request));

View file

@ -8,13 +8,17 @@
#pragma once #pragma once
#include <AK/Function.h>
#include <AK/HashMap.h>
#include <LibGfx/Rect.h> #include <LibGfx/Rect.h>
#include <LibWeb/CSS/StyleSheetIdentifier.h> #include <LibWeb/CSS/StyleSheetIdentifier.h>
#include <LibWeb/HTML/AudioPlayState.h> #include <LibWeb/HTML/AudioPlayState.h>
#include <LibWeb/HTML/FileFilter.h> #include <LibWeb/HTML/FileFilter.h>
#include <LibWeb/HTML/SessionHistoryEntry.h>
#include <LibWeb/Page/Page.h> #include <LibWeb/Page/Page.h>
#include <LibWeb/PixelUnits.h> #include <LibWeb/PixelUnits.h>
#include <LibWeb/StorageAPI/StorageEndpoint.h> #include <LibWeb/StorageAPI/StorageEndpoint.h>
#include <LibWeb/WebDriver/Response.h>
#include <LibWebView/Forward.h> #include <LibWebView/Forward.h>
#include <LibWebView/Mutation.h> #include <LibWebView/Mutation.h>
#include <LibWebView/StorageSetResult.h> #include <LibWebView/StorageSetResult.h>
@ -43,6 +47,7 @@ public:
static void set_is_headless(bool); static void set_is_headless(bool);
static void set_async_scrolling_enabled(bool); static void set_async_scrolling_enabled(bool);
static void set_should_report_session_history_updates_in_test_mode(bool);
virtual Web::Page& page() override { return *m_page; } virtual Web::Page& page() override { return *m_page; }
virtual Web::Page const& page() const override { return *m_page; } virtual Web::Page const& page() const override { return *m_page; }
@ -68,6 +73,19 @@ public:
void set_preferred_contrast(Web::CSS::PreferredContrast); void set_preferred_contrast(Web::CSS::PreferredContrast);
void set_preferred_motion(Web::CSS::PreferredMotion); void set_preferred_motion(Web::CSS::PreferredMotion);
void set_has_focus(bool); void set_has_focus(bool);
void set_window_handle(String);
void did_start_webdriver_navigation(URL::URL const&);
struct WebDriverHistoryTraversalResult {
bool accepted { false };
bool will_replace_web_content_process { false };
bool will_change_top_level_entry { false };
};
void request_webdriver_history_traversal(int delta, Function<void(WebDriverHistoryTraversalResult)>);
void did_complete_webdriver_history_traversal(u64 request_id, bool accepted, bool will_replace_web_content_process, bool will_change_top_level_entry);
Web::WebDriver::Response request_webdriver_load_url_from_ui(URL::URL const&);
Web::WebDriver::Response request_webdriver_traverse_history_from_ui(int delta);
Web::WebDriver::Response request_webdriver_mark_web_content_session_history_stale();
Web::WebDriver::Response request_webdriver_session_history();
void set_is_scripting_enabled(bool); void set_is_scripting_enabled(bool);
void set_window_position(Web::DevicePixelPoint); void set_window_position(Web::DevicePixelPoint);
void set_window_size(Web::DevicePixelSize); void set_window_size(Web::DevicePixelSize);
@ -111,6 +129,8 @@ public:
void queue_screenshot_task(Optional<Web::UniqueNodeID> node_id); void queue_screenshot_task(Optional<Web::UniqueNodeID> node_id);
void send_current_needs_beforeunload_check(); void send_current_needs_beforeunload_check();
void wait_for_webdriver_navigation_completion(Optional<u64> page_load_timeout, Function<void(Web::WebDriver::Response)>);
void did_complete_webdriver_navigation_completion(u64 request_id, Web::WebDriver::Response);
void clear_pending_dom_mutations(); void clear_pending_dom_mutations();
void did_delete_all_cookies(u64 request_id); void did_delete_all_cookies(u64 request_id);
@ -127,7 +147,7 @@ private:
// ^PageClient // ^PageClient
virtual bool is_connection_open() const override; virtual bool is_connection_open() const override;
virtual bool is_url_suitable_for_same_process_navigation(URL::URL const& current_url, URL::URL const& target_url) const override; virtual bool is_url_suitable_for_same_process_navigation(URL::URL const& current_url, URL::URL const& target_url) const override;
virtual void request_new_process_for_navigation(URL::URL const&) override; virtual void request_new_process_for_navigation(URL::URL const&, Variant<Empty, String, Web::HTML::POSTResource>, Web::Bindings::NavigationHistoryBehavior) override;
virtual Gfx::Palette palette() const override; virtual Gfx::Palette palette() const override;
virtual Web::DevicePixelRect screen_rect() const override { return m_all_screen_rects[m_main_screen_index]; } virtual Web::DevicePixelRect screen_rect() const override { return m_all_screen_rects[m_main_screen_index]; }
virtual size_t screen_count() const override { return m_all_screen_rects.size(); } virtual size_t screen_count() const override { return m_all_screen_rects.size(); }
@ -158,7 +178,8 @@ private:
virtual void page_did_request_link_context_menu(Web::CSSPixelPoint, URL::URL const&, ByteString const& target, unsigned modifiers) override; virtual void page_did_request_link_context_menu(Web::CSSPixelPoint, URL::URL const&, ByteString const& target, unsigned modifiers) override;
virtual void page_did_request_image_context_menu(Web::CSSPixelPoint, URL::URL const&, ByteString const& target, unsigned modifiers, Optional<Gfx::Bitmap const*>) override; virtual void page_did_request_image_context_menu(Web::CSSPixelPoint, URL::URL const&, ByteString const& target, unsigned modifiers, Optional<Gfx::Bitmap const*>) override;
virtual void page_did_request_media_context_menu(Web::CSSPixelPoint, ByteString const& target, unsigned modifiers, Web::Page::MediaContextMenu const&) override; virtual void page_did_request_media_context_menu(Web::CSSPixelPoint, ByteString const& target, unsigned modifiers, Web::Page::MediaContextMenu const&) override;
virtual void page_did_start_loading(URL::URL const&, bool) override; virtual void page_did_start_loading(URL::URL const&, Variant<Empty, String, Web::HTML::POSTResource>, bool, Web::Bindings::NavigationHistoryBehavior) override;
virtual void page_did_cancel_loading(URL::URL const&) override;
virtual void page_did_create_new_document(Web::DOM::Document&) override; virtual void page_did_create_new_document(Web::DOM::Document&) override;
virtual void page_did_change_active_document_in_top_level_browsing_context(Web::DOM::Document&) override; virtual void page_did_change_active_document_in_top_level_browsing_context(Web::DOM::Document&) override;
virtual void page_did_finish_loading(URL::URL const&) override; virtual void page_did_finish_loading(URL::URL const&) override;
@ -195,6 +216,10 @@ private:
virtual void page_did_close_top_level_traversable() override; virtual void page_did_close_top_level_traversable() override;
virtual void page_did_change_needs_beforeunload_check(bool needs_beforeunload_check) override; virtual void page_did_change_needs_beforeunload_check(bool needs_beforeunload_check) override;
virtual void page_did_update_navigation_buttons_state(bool back_enabled, bool forward_enabled) override; virtual void page_did_update_navigation_buttons_state(bool back_enabled, bool forward_enabled) override;
virtual bool should_report_session_history_updates() const override;
virtual void page_did_update_session_history(Vector<Web::HTML::SessionHistoryEntryDescriptor> const&, Vector<i32> const& used_steps, size_t current_used_step_index) override;
virtual String page_did_request_ui_process_session_history_for_testing() override;
virtual bool page_did_request_traverse_the_history_by_delta(int delta, Web::HistoryTraversalPrecheck) override;
virtual void request_file(Web::FileRequest) override; virtual void request_file(Web::FileRequest) override;
virtual void page_did_request_color_picker(Color current_color) override; virtual void page_did_request_color_picker(Color current_color) override;
virtual void page_did_request_file_picker(Web::HTML::FileFilter const& accepted_file_types, Web::HTML::AllowMultipleFiles) override; virtual void page_did_request_file_picker(Web::HTML::FileFilter const& accepted_file_types, Web::HTML::AllowMultipleFiles) override;
@ -247,6 +272,11 @@ private:
Core::AnonymousBuffer m_document_cookie_version_buffer; Core::AnonymousBuffer m_document_cookie_version_buffer;
u64 m_next_webdriver_navigation_completion_request_id { 0 };
HashMap<u64, Function<void(Web::WebDriver::Response)>> m_pending_webdriver_navigation_completion_requests;
u64 m_next_webdriver_history_traversal_request_id { 0 };
HashMap<u64, Function<void(WebDriverHistoryTraversalResult)>> m_pending_webdriver_history_traversal_requests;
RefPtr<WebDriverConnection> m_webdriver; RefPtr<WebDriverConnection> m_webdriver;
RefPtr<WebUIConnection> m_web_ui; RefPtr<WebUIConnection> m_web_ui;

View file

@ -10,6 +10,7 @@
#include <LibRequests/NetworkError.h> #include <LibRequests/NetworkError.h>
#include <LibRequests/RequestTimingInfo.h> #include <LibRequests/RequestTimingInfo.h>
#include <LibURL/URL.h> #include <LibURL/URL.h>
#include <LibWeb/Bindings/Navigation.h>
#include <LibWeb/Bindings/MainThreadVM.h> #include <LibWeb/Bindings/MainThreadVM.h>
#include <LibWeb/Clipboard/SystemClipboard.h> #include <LibWeb/Clipboard/SystemClipboard.h>
#include <LibWeb/Compositor/Types.h> #include <LibWeb/Compositor/Types.h>
@ -19,12 +20,15 @@
#include <LibWeb/HTML/AudioPlayState.h> #include <LibWeb/HTML/AudioPlayState.h>
#include <LibWeb/HTML/BroadcastChannelMessage.h> #include <LibWeb/HTML/BroadcastChannelMessage.h>
#include <LibWeb/HTML/FileFilter.h> #include <LibWeb/HTML/FileFilter.h>
#include <LibWeb/HTML/Navigable.h>
#include <LibWeb/HTML/SelectedFile.h> #include <LibWeb/HTML/SelectedFile.h>
#include <LibWeb/HTML/SelectItem.h> #include <LibWeb/HTML/SelectItem.h>
#include <LibWeb/HTML/SessionHistoryEntry.h>
#include <LibWeb/HTML/WebViewHints.h> #include <LibWeb/HTML/WebViewHints.h>
#include <LibWeb/HTML/WorkerAgentTypes.h> #include <LibWeb/HTML/WorkerAgentTypes.h>
#include <LibWeb/Page/EventResult.h> #include <LibWeb/Page/EventResult.h>
#include <LibWeb/Page/Page.h> #include <LibWeb/Page/Page.h>
#include <LibWeb/WebDriver/Response.h>
#include <LibWebView/Attribute.h> #include <LibWebView/Attribute.h>
#include <LibWebView/ConsoleOutput.h> #include <LibWebView/ConsoleOutput.h>
#include <LibWebView/DOMNodeProperties.h> #include <LibWebView/DOMNodeProperties.h>
@ -39,8 +43,10 @@ endpoint WebContentClient
allocate_compositor_context_id(u64 page_id, Web::Compositor::PagePresentationRegistration page_presentation_registration) => (Web::Compositor::CompositorContextId context_id) allocate_compositor_context_id(u64 page_id, Web::Compositor::PagePresentationRegistration page_presentation_registration) => (Web::Compositor::CompositorContextId context_id)
did_destroy_compositor_context(Web::Compositor::CompositorContextId context_id) =| did_destroy_compositor_context(Web::Compositor::CompositorContextId context_id) =|
did_request_new_process_for_navigation(u64 page_id, URL::URL url) =| did_request_new_process_for_navigation(u64 page_id, URL::URL url, Variant<Empty, String, Web::HTML::POSTResource> document_resource, Web::Bindings::NavigationHistoryBehavior history_handling) =|
did_start_loading(u64 page_id, URL::URL url, bool is_redirect) =| did_start_webdriver_navigation(u64 page_id, URL::URL url) =|
did_start_loading(u64 page_id, URL::URL url, Variant<Empty, String, Web::HTML::POSTResource> document_resource, bool is_redirect, Web::Bindings::NavigationHistoryBehavior history_handling) =|
did_cancel_loading(u64 page_id, URL::URL url) =|
did_finish_loading(u64 page_id, URL::URL url) =| did_finish_loading(u64 page_id, URL::URL url) =|
did_request_refresh(u64 page_id) =| did_request_refresh(u64 page_id) =|
did_request_cursor_change(u64 page_id, Gfx::Cursor cursor) =| did_request_cursor_change(u64 page_id, Gfx::Cursor cursor) =|
@ -114,6 +120,13 @@ endpoint WebContentClient
did_request_activate_tab(u64 page_id) =| did_request_activate_tab(u64 page_id) =|
did_close_browsing_context(u64 page_id) =| did_close_browsing_context(u64 page_id) =|
did_change_needs_beforeunload_check(u64 page_id, bool needs_beforeunload_check) =| did_change_needs_beforeunload_check(u64 page_id, bool needs_beforeunload_check) =|
did_request_traverse_the_history_by_delta(u64 page_id, i32 delta, Web::HistoryTraversalPrecheck history_traversal_precheck) => (bool accepted)
did_request_webdriver_history_traversal(u64 page_id, u64 request_id, i32 delta) =|
did_request_webdriver_load_url_from_ui(u64 page_id, URL::URL url) => (Web::WebDriver::Response response)
did_request_webdriver_traverse_history_from_ui(u64 page_id, i32 delta) => (Web::WebDriver::Response response)
did_request_webdriver_mark_web_content_session_history_stale(u64 page_id) => (Web::WebDriver::Response response)
did_request_webdriver_session_history(u64 page_id) => (Web::WebDriver::Response response)
did_request_webdriver_navigation_completion(u64 page_id, u64 request_id, Optional<u64> page_load_timeout) =|
did_request_restore_window(u64 page_id) =| did_request_restore_window(u64 page_id) =|
did_request_reposition_window(u64 page_id, Gfx::IntPoint position) =| did_request_reposition_window(u64 page_id, Gfx::IntPoint position) =|
did_request_resize_window(u64 page_id, Gfx::IntSize size) =| did_request_resize_window(u64 page_id, Gfx::IntSize size) =|
@ -136,6 +149,12 @@ endpoint WebContentClient
did_update_primary_selection(u64 page_id, String text) =| did_update_primary_selection(u64 page_id, String text) =|
did_update_navigation_buttons_state(u64 page_id, bool back_enabled, bool forward_enabled) =| did_update_navigation_buttons_state(u64 page_id, bool back_enabled, bool forward_enabled) =|
did_update_session_history(u64 page_id, Vector<Web::HTML::SessionHistoryEntryDescriptor> entries, Vector<i32> used_steps, size_t current_used_step_index) =|
did_request_ui_process_session_history_for_testing(u64 page_id) => (String session_history)
did_set_top_level_session_history(u64 page_id, bool accepted, Vector<Web::HTML::SessionHistoryEntryDescriptor> entries, Vector<i32> used_steps, size_t current_used_step_index) =|
did_traverse_the_history_to_step(u64 page_id, i32 step, bool step_was_available, Web::HTML::HistoryStepResult result) =|
did_check_if_traverse_history_step_is_canceled(u64 page_id, u64 request_id, i32 step, bool canceled) =|
did_reset_session_history_for_testing(u64 page_id) =|
did_change_audio_play_state(u64 page_id, Web::HTML::AudioPlayState play_state) =| did_change_audio_play_state(u64 page_id, Web::HTML::AudioPlayState play_state) =|

View file

@ -4,6 +4,7 @@
#include <LibIPC/File.h> #include <LibIPC/File.h>
#include <LibIPC/TransportHandle.h> #include <LibIPC/TransportHandle.h>
#include <LibURL/URL.h> #include <LibURL/URL.h>
#include <LibWeb/Bindings/Navigation.h>
#include <LibWeb/Clipboard/SystemClipboard.h> #include <LibWeb/Clipboard/SystemClipboard.h>
#include <LibWeb/CSS/PreferredColorScheme.h> #include <LibWeb/CSS/PreferredColorScheme.h>
#include <LibWeb/CSS/PreferredContrast.h> #include <LibWeb/CSS/PreferredContrast.h>
@ -13,12 +14,14 @@
#include <LibWeb/HTML/ColorPickerUpdateState.h> #include <LibWeb/HTML/ColorPickerUpdateState.h>
#include <LibWeb/HTML/BroadcastChannelMessage.h> #include <LibWeb/HTML/BroadcastChannelMessage.h>
#include <LibWeb/HTML/SelectedFile.h> #include <LibWeb/HTML/SelectedFile.h>
#include <LibWeb/HTML/SessionHistoryEntry.h>
#include <LibWeb/HTML/VisibilityState.h> #include <LibWeb/HTML/VisibilityState.h>
#include <LibWeb/HTML/WorkerAgentTypes.h> #include <LibWeb/HTML/WorkerAgentTypes.h>
#include <LibWeb/Page/InputEvent.h> #include <LibWeb/Page/InputEvent.h>
#include <LibWeb/StorageAPI/StorageEndpoint.h> #include <LibWeb/StorageAPI/StorageEndpoint.h>
#include <LibWeb/Page/ViewportIsFullscreen.h> #include <LibWeb/Page/ViewportIsFullscreen.h>
#include <LibWeb/WebDriver/ExecuteScript.h> #include <LibWeb/WebDriver/ExecuteScript.h>
#include <LibWeb/WebDriver/Response.h>
#include <LibWebView/Attribute.h> #include <LibWebView/Attribute.h>
#include <LibWebView/DOMNodeProperties.h> #include <LibWebView/DOMNodeProperties.h>
#include <LibWebView/PageInfo.h> #include <LibWebView/PageInfo.h>
@ -35,6 +38,8 @@ endpoint WebContentServer
set_window_handle(u64 page_id, String handle) =| set_window_handle(u64 page_id, String handle) =|
connect_to_webdriver(u64 page_id, ByteString webdriver_endpoint) =| connect_to_webdriver(u64 page_id, ByteString webdriver_endpoint) =|
complete_webdriver_history_traversal(u64 page_id, u64 request_id, bool accepted, bool will_replace_web_content_process, bool will_change_top_level_entry) =|
complete_webdriver_navigation_completion(u64 page_id, u64 request_id, Web::WebDriver::Response response) =|
connect_to_web_ui(u64 page_id, IPC::TransportHandle handle) =| connect_to_web_ui(u64 page_id, IPC::TransportHandle handle) =|
connect_to_request_server(IPC::TransportHandle handle) =| connect_to_request_server(IPC::TransportHandle handle) =|
connect_to_image_decoder(IPC::TransportHandle handle) =| connect_to_image_decoder(IPC::TransportHandle handle) =|
@ -44,11 +49,18 @@ endpoint WebContentServer
update_system_theme(u64 page_id, Core::AnonymousBuffer theme_buffer) =| update_system_theme(u64 page_id, Core::AnonymousBuffer theme_buffer) =|
update_screen_rects(u64 page_id, Vector<Web::DevicePixelRect> rects, u32 main_screen_index) =| update_screen_rects(u64 page_id, Vector<Web::DevicePixelRect> rects, u32 main_screen_index) =|
load_url(u64 page_id, URL::URL url) =| load_url(u64 page_id, URL::URL url, Web::Bindings::NavigationHistoryBehavior history_handling) =|
load_url_with_document_resource(u64 page_id, URL::URL url,
Variant<Empty, String, Web::HTML::POSTResource> document_resource,
Web::Bindings::NavigationHistoryBehavior history_handling) =|
load_html(u64 page_id, ByteString html) =| load_html(u64 page_id, ByteString html) =|
load_html_with_url(u64 page_id, ByteString html, URL::URL url) =| load_html_with_url(u64 page_id, ByteString html, URL::URL url) =|
reload(u64 page_id) =| reload(u64 page_id) =|
traverse_the_history_by_delta(u64 page_id, i32 delta) =| traverse_the_history_by_delta(u64 page_id, i32 delta) =|
traverse_the_history_to_step(u64 page_id, i32 step) =|
check_if_traverse_history_step_is_canceled(u64 page_id, u64 request_id, i32 step) =|
set_top_level_session_history(u64 page_id, Vector<Web::HTML::SessionHistoryEntryDescriptor> entries, size_t current_top_level_entry_index) =|
reset_session_history_for_testing(u64 page_id) =|
set_viewport(u64 page_id, Web::DevicePixelSize size, double device_pixel_ratio, Web::ViewportIsFullscreen is_fullscreen) =| set_viewport(u64 page_id, Web::DevicePixelSize size, double device_pixel_ratio, Web::ViewportIsFullscreen is_fullscreen) =|

View file

@ -10,11 +10,12 @@ endpoint WebDriverClient {
set_is_webdriver_active(bool active) =| set_is_webdriver_active(bool active) =|
get_timeouts() => (Web::WebDriver::Response response) get_timeouts() => (Web::WebDriver::Response response)
set_timeouts(JsonValue payload) => (Web::WebDriver::Response response) set_timeouts(JsonValue payload) => (Web::WebDriver::Response response)
navigate_to(JsonValue payload) => (Web::WebDriver::Response response) navigate_to(JsonValue payload) => (Web::WebDriver::Response response, bool will_replace_web_content_process)
get_current_url() => (Web::WebDriver::Response response) get_current_url() => (Web::WebDriver::Response response)
back() => (Web::WebDriver::Response response) back() => (Web::WebDriver::Response response, bool will_replace_web_content_process, bool wait_for_driver_execution_complete, bool wait_for_navigation_completion)
forward() => (Web::WebDriver::Response response) forward() => (Web::WebDriver::Response response, bool will_replace_web_content_process, bool wait_for_driver_execution_complete, bool wait_for_navigation_completion)
refresh() => (Web::WebDriver::Response response) refresh() => (Web::WebDriver::Response response)
wait_for_navigation_completion() => (Web::WebDriver::Response response)
get_title() => (Web::WebDriver::Response response) get_title() => (Web::WebDriver::Response response)
close_window() => (Web::WebDriver::Response response) close_window() => (Web::WebDriver::Response response)
switch_to_window(String handle) => (Web::WebDriver::Response response) switch_to_window(String handle) => (Web::WebDriver::Response response)
@ -27,6 +28,11 @@ endpoint WebDriverClient {
minimize_window() => (Web::WebDriver::Response response) minimize_window() => (Web::WebDriver::Response response)
fullscreen_window() => (Web::WebDriver::Response response) fullscreen_window() => (Web::WebDriver::Response response)
consume_user_activation() => (Web::WebDriver::Response response) consume_user_activation() => (Web::WebDriver::Response response)
crash_current_page() =|
load_url_from_ui(JsonValue payload) => (Web::WebDriver::Response response)
traverse_history_from_ui(JsonValue payload) => (Web::WebDriver::Response response)
mark_web_content_session_history_stale() => (Web::WebDriver::Response response)
get_session_history() => (Web::WebDriver::Response response)
find_element(JsonValue payload) => (Web::WebDriver::Response response) find_element(JsonValue payload) => (Web::WebDriver::Response response)
find_elements(JsonValue payload) => (Web::WebDriver::Response response) find_elements(JsonValue payload) => (Web::WebDriver::Response response)
find_element_from_element(JsonValue payload, String element_id) => (Web::WebDriver::Response response) find_element_from_element(JsonValue payload, String element_id) => (Web::WebDriver::Response response)

View file

@ -8,13 +8,15 @@
* SPDX-License-Identifier: BSD-2-Clause * SPDX-License-Identifier: BSD-2-Clause
*/ */
#include <AK/JsonArray.h>
#include <AK/JsonObject.h> #include <AK/JsonObject.h>
#include <AK/JsonValue.h> #include <AK/JsonValue.h>
#include <AK/LexicalPath.h> #include <AK/LexicalPath.h>
#include <AK/NeverDestroyed.h> #include <AK/RefCounted.h>
#include <AK/Time.h> #include <AK/Time.h>
#include <AK/Vector.h> #include <AK/Vector.h>
#include <LibCore/File.h> #include <LibCore/File.h>
#include <LibCore/Process.h>
#if !defined(AK_OS_MACOS) #if !defined(AK_OS_MACOS)
# include <LibCore/Socket.h> # include <LibCore/Socket.h>
#else #else
@ -70,10 +72,21 @@
#include <LibWeb/WebDriver/Properties.h> #include <LibWeb/WebDriver/Properties.h>
#include <LibWeb/WebDriver/Screenshot.h> #include <LibWeb/WebDriver/Screenshot.h>
#include <LibWeb/WebDriver/UserPrompt.h> #include <LibWeb/WebDriver/UserPrompt.h>
#include <LibWebView/HistoryDebug.h>
#include <WebContent/PageClient.h>
#include <WebContent/WebDriverConnection.h> #include <WebContent/WebDriverConnection.h>
namespace WebContent { namespace WebContent {
struct WebDriverHistoryTraversalMetadata
: public RefCounted<WebDriverHistoryTraversalMetadata> {
Web::WebDriver::Response response { JsonValue {} };
bool will_replace_web_content_process { false };
bool wait_for_driver_execution_complete { true };
bool wait_for_navigation_completion { true };
bool sync_response_returned { false };
};
#define WEBDRIVER_TRY(expression) \ #define WEBDRIVER_TRY(expression) \
({ \ ({ \
/* Ignore -Wshadow to allow nesting the macro. */ \ /* Ignore -Wshadow to allow nesting the macro. */ \
@ -126,6 +139,30 @@ static Gfx::IntRect compute_window_rect(Web::Page const& page)
}; };
} }
static Optional<size_t> current_top_level_entry_index(Web::HTML::TraversableNavigable::SessionHistorySnapshot const& session_history_snapshot)
{
VERIFY(session_history_snapshot.current_used_step_index < session_history_snapshot.used_session_history_steps.size());
auto current_step = session_history_snapshot.used_session_history_steps[session_history_snapshot.current_used_step_index];
Optional<size_t> result;
for (size_t i = 0; i < session_history_snapshot.top_level_session_history_entries.size(); ++i) {
if (session_history_snapshot.top_level_session_history_entries[i].step > current_step)
break;
result = i;
}
return result;
}
static JsonObject serialize_session_history_snapshot_for_webdriver(Web::HTML::TraversableNavigable::SessionHistorySnapshot const& session_history_snapshot)
{
JsonObject serialized;
serialized.set("currentUsedStepIndex"sv, session_history_snapshot.current_used_step_index);
serialized.set("currentStep"sv, session_history_snapshot.used_session_history_steps[session_history_snapshot.current_used_step_index]);
serialized.set("entries"sv, WebView::history_json_entries(session_history_snapshot.top_level_session_history_entries, current_top_level_entry_index(session_history_snapshot)));
serialized.set("usedSteps"sv, WebView::history_json_steps(session_history_snapshot.used_session_history_steps, session_history_snapshot.current_used_step_index));
return serialized;
}
// https://w3c.github.io/webdriver/#dfn-scrolls-into-view // https://w3c.github.io/webdriver/#dfn-scrolls-into-view
static void scroll_element_into_view(Web::DOM::Element& element) static void scroll_element_into_view(Web::DOM::Element& element)
{ {
@ -228,6 +265,41 @@ WebDriverConnection::WebDriverConnection(NonnullOwnPtr<IPC::Transport> transport
set_current_top_level_browsing_context(page_client.page().top_level_browsing_context()); set_current_top_level_browsing_context(page_client.page().top_level_browsing_context());
} }
void WebDriverConnection::page_did_set_window_handle(Badge<PageClient>, String const& window_handle)
{
async_did_set_window_handle(window_handle);
}
void WebDriverConnection::page_did_start_window_replacement(Badge<PageClient>, String const& window_handle)
{
async_did_start_window_replacement(window_handle);
if (m_should_complete_driver_execution_when_navigation_starts_or_is_canceled) {
m_should_complete_driver_execution_when_navigation_starts_or_is_canceled = false;
async_driver_execution_complete(JsonValue {});
}
}
void WebDriverConnection::page_did_start_loading(Badge<PageClient>, URL::URL const&)
{
if (m_should_complete_driver_execution_when_navigation_starts_or_is_canceled) {
m_should_complete_driver_execution_when_navigation_starts_or_is_canceled = false;
async_driver_execution_complete(JsonValue {});
}
}
void WebDriverConnection::page_did_cancel_loading(Badge<PageClient>, URL::URL const&)
{
if (m_should_complete_driver_execution_when_navigation_starts_or_is_canceled) {
m_should_complete_driver_execution_when_navigation_starts_or_is_canceled = false;
async_driver_execution_complete(JsonValue {});
}
}
void WebDriverConnection::page_did_close_window(Badge<PageClient>, String const& window_handle)
{
async_did_close_window(window_handle);
}
void WebDriverConnection::visit_edges(JS::Cell::Visitor& visitor) void WebDriverConnection::visit_edges(JS::Cell::Visitor& visitor)
{ {
visitor.visit(m_current_browsing_context); visitor.visit(m_current_browsing_context);
@ -305,17 +377,21 @@ Messages::WebDriverClient::SetTimeoutsResponse WebDriverConnection::set_timeouts
Messages::WebDriverClient::NavigateToResponse WebDriverConnection::navigate_to(JsonValue payload) Messages::WebDriverClient::NavigateToResponse WebDriverConnection::navigate_to(JsonValue payload)
{ {
// 1. If the current top-level browsing context is no longer open, return error with error code no such window. // 1. If the current top-level browsing context is no longer open, return error with error code no such window.
TRY(ensure_current_top_level_browsing_context_is_open()); if (auto result = ensure_current_top_level_browsing_context_is_open(); result.is_error())
return { Web::WebDriver::Response { result.release_error() }, false };
// 2. Let url be the result of getting the property url from the parameters argument. // 2. Let url be the result of getting the property url from the parameters argument.
if (!payload.is_object() || !payload.as_object().has_string("url"sv)) if (!payload.is_object() || !payload.as_object().has_string("url"sv))
return Web::WebDriver::Error::from_code(Web::WebDriver::ErrorCode::InvalidArgument, "Payload doesn't have a string `url`"sv); return { Web::WebDriver::Error::from_code(Web::WebDriver::ErrorCode::InvalidArgument, "Payload doesn't have a string `url`"sv), false };
auto url = URL::Parser::basic_parse(payload.as_object().get_string("url"sv).value()); auto url = URL::Parser::basic_parse(payload.as_object().get_string("url"sv).value());
// FIXME: 3. If url is not an absolute URL or is not an absolute URL with fragment or not a local scheme, return error with error code invalid argument. // FIXME: 3. If url is not an absolute URL or is not an absolute URL with fragment or not a local scheme, return error with error code invalid argument.
auto const& current_url = current_top_level_browsing_context()->active_document()->url();
auto will_replace_web_content_process = !current_top_level_browsing_context()->page().client().is_url_suitable_for_same_process_navigation(current_url, url.value());
// 4. Handle any user prompts and return its value if it is an error. // 4. Handle any user prompts and return its value if it is an error.
handle_any_user_prompts([this, url = move(url)]() { handle_any_user_prompts([this, url = move(url), will_replace_web_content_process]() {
// 5. Let current URL be the current top-level browsing contexts active documents URL. // 5. Let current URL be the current top-level browsing contexts active documents URL.
auto const& current_url = current_top_level_browsing_context()->active_document()->url(); auto const& current_url = current_top_level_browsing_context()->active_document()->url();
@ -323,33 +399,32 @@ Messages::WebDriverClient::NavigateToResponse WebDriverConnection::navigate_to(J
// FIXME: a. If timer has not been started, start a timer. If this algorithm has not completed before timer reaches the sessions session page load timeout in milliseconds, return an error with error code timeout. // FIXME: a. If timer has not been started, start a timer. If this algorithm has not completed before timer reaches the sessions session page load timeout in milliseconds, return an error with error code timeout.
// 7. Navigate the current top-level browsing context to url. // 7. Navigate the current top-level browsing context to url.
// NB: "Navigate to a javascript: URL" can evaluate without producing a new Document,
// in which case "we will not perform a navigation".
// https://html.spec.whatwg.org/multipage/browsing-the-web.html#navigate-to-a-javascript:-url
auto is_same_document_fragment_navigation = url->fragment().has_value()
&& url->equals(current_url, URL::ExcludeFragment::Yes);
if (url->scheme() != "javascript"sv && !is_same_document_fragment_navigation)
static_cast<WebContent::PageClient&>(current_top_level_browsing_context()->page().client()).did_start_webdriver_navigation(url.value());
current_top_level_browsing_context()->page().load(url.value()); current_top_level_browsing_context()->page().load(url.value());
auto navigation_complete = GC::create_function(current_top_level_browsing_context()->heap(), [this](Web::WebDriver::Response result) { auto navigation_complete = GC::create_function(current_top_level_browsing_context()->heap(), [this, will_replace_web_content_process](Web::WebDriver::Response result) {
// 9. Set the current browsing context with the current top-level browsing context. // 9. Set the current browsing context with the current top-level browsing context.
set_current_browsing_context(*current_top_level_browsing_context()); set_current_browsing_context(*current_top_level_browsing_context());
// FIXME: 10. If the current top-level browsing context contains a refresh state pragma directive of time 1 second or less, wait until the refresh timeout has elapsed, a new navigate has begun, and return to the first step of this algorithm. // FIXME: 10. If the current top-level browsing context contains a refresh state pragma directive of time 1 second or less, wait until the refresh timeout has elapsed, a new navigate has begun, and return to the first step of this algorithm.
async_driver_execution_complete(move(result)); if (will_replace_web_content_process)
m_should_complete_driver_execution_when_navigation_starts_or_is_canceled = true;
else
async_driver_execution_complete(move(result));
}); });
// 8. If url is special except for file and current URL and URL do not have the same absolute URL: navigation_complete->function()(JsonValue {});
// AD-HOC: We wait for the navigation to complete regardless of whether the current URL differs from the provided
// URL. Even if they're the same, the navigation queues a tasks that we must await, otherwise subsequent
// endpoint invocations will attempt to operate on the wrong page.
if (url->is_special() && url->scheme() != "file"sv) {
// a. Try to wait for navigation to complete.
wait_for_navigation_to_complete(navigation_complete);
// FIXME: b. Try to run the post-navigation checks.
} else {
navigation_complete->function()(JsonValue {});
}
}); });
// 11. Return success with data null. // 11. Return success with data null.
return JsonValue {}; return { JsonValue {}, will_replace_web_content_process };
} }
// 10.2 Get Current URL, https://w3c.github.io/webdriver/#get-current-url // 10.2 Get Current URL, https://w3c.github.io/webdriver/#get-current-url
@ -374,148 +449,64 @@ Messages::WebDriverClient::GetCurrentUrlResponse WebDriverConnection::get_curren
Messages::WebDriverClient::BackResponse WebDriverConnection::back() Messages::WebDriverClient::BackResponse WebDriverConnection::back()
{ {
// 1. If session's current top-level browsing context is no longer open, return error with error code no such window. // 1. If session's current top-level browsing context is no longer open, return error with error code no such window.
TRY(ensure_current_top_level_browsing_context_is_open()); if (auto result = ensure_current_top_level_browsing_context_is_open(); result.is_error())
return { Web::WebDriver::Response { result.release_error() }, false, false, false };
// 2. Try to handle any user prompts with session. // 2. Try to handle any user prompts with session.
handle_any_user_prompts([this]() { auto metadata = adopt_ref(*new WebDriverHistoryTraversalMetadata);
auto& realm = current_top_level_browsing_context()->active_document()->realm(); handle_any_user_prompts([this, metadata]() {
auto& page_client = static_cast<WebContent::PageClient&>(current_top_level_browsing_context()->page().client());
// 3. Let timeout be session' session timeouts page load timeout. page_client.request_webdriver_history_traversal(-1, [this, metadata](auto traversal_result) {
auto timeout = m_timeouts_configuration.page_load_timeout; if (!traversal_result.accepted) {
async_driver_execution_complete(Web::WebDriver::Error::from_code(Web::WebDriver::ErrorCode::NoSuchWindow, "Window not found"sv));
// 4. Let timer be a new timer.
auto timer = realm.heap().allocate<GC::Timer>();
auto on_complete = GC::create_function(realm.heap(), [this, timer]() {
timer->stop();
if (m_document_observer) {
m_document_observer->set_document_page_showing_observer({});
m_document_observer = nullptr;
}
// 8. If timer' timeout fired flag is set:
if (timer->is_timed_out()) {
// 1. Handle any user prompts.
handle_any_user_prompts([this]() {
// 2. Return error with error code timeout.
async_driver_execution_complete(Web::WebDriver::Error::from_code(Web::WebDriver::ErrorCode::Timeout, "Navigation timed out"sv));
});
return; return;
} }
// 9. Return success with data null. metadata->will_replace_web_content_process = traversal_result.will_replace_web_content_process;
async_driver_execution_complete(JsonValue {}); metadata->wait_for_navigation_completion = traversal_result.will_change_top_level_entry;
if (metadata->will_replace_web_content_process)
async_did_start_window_replacement(current_top_level_browsing_context()->page().top_level_traversable()->window_handle());
if (metadata->sync_response_returned)
async_driver_execution_complete(JsonValue {});
else
metadata->wait_for_driver_execution_complete = false;
}); });
// 5. If timeout is not null:
if (timeout.has_value()) {
// 1. Start the timer with timer and timeout.
timer->start(*timeout, on_complete);
}
// 6. Traverse the history by a delta 1 for session's current browsing context.
current_top_level_browsing_context()->top_level_traversable()->traverse_the_history_by_delta(-1);
// 7. If the previous step completed results in a pageHide event firing, wait until pageShow event fires or
// timer' timeout fired flag to be set, whichever occurs first.
current_top_level_browsing_context()->top_level_traversable()->append_session_history_traversal_steps(GC::create_function(realm.heap(), [this, timer, on_complete](NonnullRefPtr<Core::Promise<Empty>> signal) {
if (timer->is_timed_out()) {
signal->resolve({});
return;
}
if (auto* document = current_top_level_browsing_context()->active_document(); document->page_showing()) {
on_complete->function()();
} else {
auto& realm = document->realm();
m_document_observer = realm.create<Web::DOM::DocumentObserver>(realm, *document);
m_document_observer->set_document_page_showing_observer([on_complete](auto) {
on_complete->function()();
});
}
signal->resolve({});
}));
}); });
return JsonValue {}; metadata->sync_response_returned = true;
return { move(metadata->response), metadata->will_replace_web_content_process, metadata->wait_for_driver_execution_complete, metadata->wait_for_navigation_completion };
} }
// 10.4 Forward, https://w3c.github.io/webdriver/#dfn-forward // 10.4 Forward, https://w3c.github.io/webdriver/#dfn-forward
Messages::WebDriverClient::ForwardResponse WebDriverConnection::forward() Messages::WebDriverClient::ForwardResponse WebDriverConnection::forward()
{ {
// 1. If session's current top-level browsing context is no longer open, return error with error code no such window. // 1. If session's current top-level browsing context is no longer open, return error with error code no such window.
TRY(ensure_current_top_level_browsing_context_is_open()); if (auto result = ensure_current_top_level_browsing_context_is_open(); result.is_error())
return { Web::WebDriver::Response { result.release_error() }, false, false, false };
// 2. Try to handle any user prompts with session. // 2. Try to handle any user prompts with session.
handle_any_user_prompts([this]() { auto metadata = adopt_ref(*new WebDriverHistoryTraversalMetadata);
auto& realm = current_top_level_browsing_context()->active_document()->realm(); handle_any_user_prompts([this, metadata]() {
auto& page_client = static_cast<WebContent::PageClient&>(current_top_level_browsing_context()->page().client());
// 3. Let timeout be session' session timeouts page load timeout. page_client.request_webdriver_history_traversal(1, [this, metadata](auto traversal_result) {
auto timeout = m_timeouts_configuration.page_load_timeout; if (!traversal_result.accepted) {
async_driver_execution_complete(Web::WebDriver::Error::from_code(Web::WebDriver::ErrorCode::NoSuchWindow, "Window not found"sv));
// 4. Let timer be a new timer.
auto timer = realm.heap().allocate<GC::Timer>();
auto on_complete = GC::create_function(realm.heap(), [this, timer]() {
timer->stop();
if (m_document_observer) {
m_document_observer->set_document_page_showing_observer({});
m_document_observer = nullptr;
}
// 8. If timer' timeout fired flag is set:
if (timer->is_timed_out()) {
// 1. Handle any user prompts.
handle_any_user_prompts([this]() {
// 2. Return error with error code timeout.
async_driver_execution_complete(Web::WebDriver::Error::from_code(Web::WebDriver::ErrorCode::Timeout, "Navigation timed out"sv));
});
return; return;
} }
// 9. Return success with data null. metadata->will_replace_web_content_process = traversal_result.will_replace_web_content_process;
async_driver_execution_complete(JsonValue {}); metadata->wait_for_navigation_completion = traversal_result.will_change_top_level_entry;
if (metadata->will_replace_web_content_process)
async_did_start_window_replacement(current_top_level_browsing_context()->page().top_level_traversable()->window_handle());
if (metadata->sync_response_returned)
async_driver_execution_complete(JsonValue {});
else
metadata->wait_for_driver_execution_complete = false;
}); });
// 5. If timeout is not null:
if (timeout.has_value()) {
// 1. Start the timer with timer and timeout.
timer->start(*timeout, on_complete);
}
// 6. Traverse the history by a delta 1 for session's current browsing context.
current_top_level_browsing_context()->top_level_traversable()->traverse_the_history_by_delta(1);
// 7. If the previous step completed results in a pageHide event firing, wait until pageShow event fires or
// timer' timeout fired flag to be set, whichever occurs first.
current_top_level_browsing_context()->top_level_traversable()->append_session_history_traversal_steps(GC::create_function(realm.heap(), [this, timer, on_complete](NonnullRefPtr<Core::Promise<Empty>> signal) {
if (timer->is_timed_out()) {
signal->resolve({});
return;
}
if (auto* document = current_top_level_browsing_context()->active_document(); document->page_showing()) {
on_complete->function()();
} else {
auto& realm = document->realm();
m_document_observer = realm.create<Web::DOM::DocumentObserver>(realm, *document);
m_document_observer->set_document_page_showing_observer([on_complete](auto) {
on_complete->function()();
});
}
signal->resolve({});
}));
}); });
return JsonValue {}; metadata->sync_response_returned = true;
return { move(metadata->response), metadata->will_replace_web_content_process, metadata->wait_for_driver_execution_complete, metadata->wait_for_navigation_completion };
} }
// 10.5 Refresh, https://w3c.github.io/webdriver/#dfn-refresh // 10.5 Refresh, https://w3c.github.io/webdriver/#dfn-refresh
@ -543,6 +534,107 @@ Messages::WebDriverClient::RefreshResponse WebDriverConnection::refresh()
return JsonValue {}; return JsonValue {};
} }
Messages::WebDriverClient::WaitForNavigationCompletionResponse WebDriverConnection::wait_for_navigation_completion()
{
if (m_page_load_strategy == Web::WebDriver::PageLoadStrategy::None) {
async_driver_execution_complete(JsonValue {});
return JsonValue {};
}
auto& page_client = static_cast<WebContent::PageClient&>(current_top_level_browsing_context()->page().client());
page_client.wait_for_webdriver_navigation_completion(m_timeouts_configuration.page_load_timeout, [this](Web::WebDriver::Response response) {
async_driver_execution_complete(move(response));
});
return JsonValue {};
}
void WebDriverConnection::crash_current_page()
{
Core::deferred_invoke([] {
Core::Process::terminate_immediately(1);
});
}
Messages::WebDriverClient::LoadUrlFromUiResponse WebDriverConnection::load_url_from_ui(JsonValue payload)
{
TRY(ensure_current_top_level_browsing_context_is_open());
if (!payload.is_object() || !payload.as_object().has_string("url"sv))
return Web::WebDriver::Error::from_code(Web::WebDriver::ErrorCode::InvalidArgument, "Payload doesn't have a string `url`"sv);
auto url = URL::Parser::basic_parse(payload.as_object().get_string("url"sv).value());
if (!url.has_value())
return Web::WebDriver::Error::from_code(Web::WebDriver::ErrorCode::InvalidArgument, "Payload has an invalid `url`"sv);
auto const& current_url = current_top_level_browsing_context()->active_document()->url();
auto will_replace_web_content_process = !current_top_level_browsing_context()->page().client().is_url_suitable_for_same_process_navigation(current_url, *url);
auto& page_client = static_cast<WebContent::PageClient&>(current_top_level_browsing_context()->page().client());
auto response = page_client.request_webdriver_load_url_from_ui(*url);
if (response.is_error())
return response.release_error();
JsonObject result;
result.set("willReplaceWebContentProcess"sv, will_replace_web_content_process);
async_driver_execution_complete(JsonValue { move(result) });
return JsonValue {};
}
Messages::WebDriverClient::TraverseHistoryFromUiResponse WebDriverConnection::traverse_history_from_ui(JsonValue payload)
{
TRY(ensure_current_top_level_browsing_context_is_open());
if (!payload.is_object() || !payload.as_object().has_i32("delta"sv))
return Web::WebDriver::Error::from_code(Web::WebDriver::ErrorCode::InvalidArgument, "Payload doesn't have an integer `delta`"sv);
auto& page_client = static_cast<WebContent::PageClient&>(current_top_level_browsing_context()->page().client());
page_client.request_webdriver_history_traversal(payload.as_object().get_i32("delta"sv).value(), [this](auto traversal_result) {
if (!traversal_result.accepted) {
async_driver_execution_complete(Web::WebDriver::Error::from_code(Web::WebDriver::ErrorCode::NoSuchWindow, "Window not found"sv));
return;
}
if (traversal_result.will_replace_web_content_process)
async_did_start_window_replacement(current_top_level_browsing_context()->page().top_level_traversable()->window_handle());
JsonObject result;
result.set("willReplaceWebContentProcess"sv, traversal_result.will_replace_web_content_process);
result.set("willChangeTopLevelEntry"sv, traversal_result.will_change_top_level_entry);
async_driver_execution_complete(JsonValue { move(result) });
});
return JsonValue {};
}
Messages::WebDriverClient::MarkWebContentSessionHistoryStaleResponse WebDriverConnection::mark_web_content_session_history_stale()
{
TRY(ensure_current_top_level_browsing_context_is_open());
auto& page_client = static_cast<WebContent::PageClient&>(current_top_level_browsing_context()->page().client());
auto response = page_client.request_webdriver_mark_web_content_session_history_stale();
if (response.is_error())
return response.release_error();
async_driver_execution_complete(JsonValue {});
return JsonValue {};
}
Messages::WebDriverClient::GetSessionHistoryResponse WebDriverConnection::get_session_history()
{
TRY(ensure_current_top_level_browsing_context_is_open());
auto& page_client = static_cast<WebContent::PageClient&>(current_top_level_browsing_context()->page().client());
auto ui_session_history = page_client.request_webdriver_session_history();
if (ui_session_history.is_error())
return ui_session_history.release_error();
auto web_content_session_history_snapshot = current_top_level_browsing_context()->top_level_traversable()->create_session_history_snapshot();
JsonObject result;
result.set("ui"sv, ui_session_history.release_value());
result.set("webContent"sv, serialize_session_history_snapshot_for_webdriver(web_content_session_history_snapshot));
async_driver_execution_complete(JsonValue { move(result) });
return JsonValue {};
}
// 10.6 Get Title, https://w3c.github.io/webdriver/#dfn-get-title // 10.6 Get Title, https://w3c.github.io/webdriver/#dfn-get-title
Messages::WebDriverClient::GetTitleResponse WebDriverConnection::get_title() Messages::WebDriverClient::GetTitleResponse WebDriverConnection::get_title()
{ {
@ -2668,30 +2760,7 @@ ErrorOr<void, Web::WebDriver::Error> WebDriverConnection::ensure_current_top_lev
// https://w3c.github.io/webdriver/#dfn-get-the-prompt-handler // https://w3c.github.io/webdriver/#dfn-get-the-prompt-handler
Web::WebDriver::PromptHandlerConfiguration WebDriverConnection::get_the_prompt_handler(Web::WebDriver::PromptType type) const Web::WebDriver::PromptHandlerConfiguration WebDriverConnection::get_the_prompt_handler(Web::WebDriver::PromptType type) const
{ {
static NeverDestroyed<Web::WebDriver::UserPromptHandler::ValueType> empty_user_prompt_handler; return Web::WebDriver::get_the_prompt_handler(type);
auto const& user_prompt_handler = Web::WebDriver::user_prompt_handler();
// 1. If the user prompt handler is null, let handlers be an empty map. Otherwise let handlers be user prompt handler.
auto const& handlers = user_prompt_handler.has_value() ? *user_prompt_handler : *empty_user_prompt_handler;
// 2. If handlers contains type return handlers[type].
if (auto handler = handlers.get(type); handler.has_value())
return *handler;
// 3. If handlers contains "default" return handlers["default"].
if (auto handler = handlers.get(Web::WebDriver::PromptType::Default); handler.has_value())
return *handler;
// 4. If type is "beforeUnload", return a prompt handler configuration with handler "accept" and notify false.
if (type == Web::WebDriver::PromptType::BeforeUnload)
return { .handler = Web::WebDriver::PromptHandler::Accept, .notify = Web::WebDriver::PromptHandlerConfiguration::Notify::No };
// 5. If handlers contains "fallbackDefault" return handlers["fallbackDefault"].
if (auto handler = handlers.get(Web::WebDriver::PromptType::FallbackDefault); handler.has_value())
return *handler;
// 6. Return a prompt handler configuration with handler "dismiss" and notify true.
return { .handler = Web::WebDriver::PromptHandler::Dismiss, .notify = Web::WebDriver::PromptHandlerConfiguration::Notify::Yes };
} }
// https://w3c.github.io/webdriver/#dfn-annotated-unexpected-alert-open-error // https://w3c.github.io/webdriver/#dfn-annotated-unexpected-alert-open-error

View file

@ -40,6 +40,11 @@ public:
void visit_edges(JS::Cell::Visitor&); void visit_edges(JS::Cell::Visitor&);
void page_did_open_dialog(Badge<PageClient>); void page_did_open_dialog(Badge<PageClient>);
void page_did_set_window_handle(Badge<PageClient>, String const& window_handle);
void page_did_start_window_replacement(Badge<PageClient>, String const& window_handle);
void page_did_start_loading(Badge<PageClient>, URL::URL const& url);
void page_did_cancel_loading(Badge<PageClient>, URL::URL const& url);
void page_did_close_window(Badge<PageClient>, String const& window_handle);
private: private:
WebDriverConnection(NonnullOwnPtr<IPC::Transport> transport, Web::PageClient& page_client); WebDriverConnection(NonnullOwnPtr<IPC::Transport> transport, Web::PageClient& page_client);
@ -58,6 +63,7 @@ private:
virtual Messages::WebDriverClient::BackResponse back() override; virtual Messages::WebDriverClient::BackResponse back() override;
virtual Messages::WebDriverClient::ForwardResponse forward() override; virtual Messages::WebDriverClient::ForwardResponse forward() override;
virtual Messages::WebDriverClient::RefreshResponse refresh() override; virtual Messages::WebDriverClient::RefreshResponse refresh() override;
virtual Messages::WebDriverClient::WaitForNavigationCompletionResponse wait_for_navigation_completion() override;
virtual Messages::WebDriverClient::GetTitleResponse get_title() override; virtual Messages::WebDriverClient::GetTitleResponse get_title() override;
virtual Messages::WebDriverClient::CloseWindowResponse close_window() override; virtual Messages::WebDriverClient::CloseWindowResponse close_window() override;
virtual Messages::WebDriverClient::SwitchToWindowResponse switch_to_window(String handle) override; virtual Messages::WebDriverClient::SwitchToWindowResponse switch_to_window(String handle) override;
@ -70,6 +76,11 @@ private:
virtual Messages::WebDriverClient::MinimizeWindowResponse minimize_window() override; virtual Messages::WebDriverClient::MinimizeWindowResponse minimize_window() override;
virtual Messages::WebDriverClient::FullscreenWindowResponse fullscreen_window() override; virtual Messages::WebDriverClient::FullscreenWindowResponse fullscreen_window() override;
virtual Messages::WebDriverClient::ConsumeUserActivationResponse consume_user_activation() override; virtual Messages::WebDriverClient::ConsumeUserActivationResponse consume_user_activation() override;
virtual void crash_current_page() override;
virtual Messages::WebDriverClient::LoadUrlFromUiResponse load_url_from_ui(JsonValue payload) override;
virtual Messages::WebDriverClient::TraverseHistoryFromUiResponse traverse_history_from_ui(JsonValue payload) override;
virtual Messages::WebDriverClient::MarkWebContentSessionHistoryStaleResponse mark_web_content_session_history_stale() override;
virtual Messages::WebDriverClient::GetSessionHistoryResponse get_session_history() override;
virtual Messages::WebDriverClient::FindElementResponse find_element(JsonValue payload) override; virtual Messages::WebDriverClient::FindElementResponse find_element(JsonValue payload) override;
virtual Messages::WebDriverClient::FindElementsResponse find_elements(JsonValue payload) override; virtual Messages::WebDriverClient::FindElementsResponse find_elements(JsonValue payload) override;
virtual Messages::WebDriverClient::FindElementFromElementResponse find_element_from_element(JsonValue payload, String element_id) override; virtual Messages::WebDriverClient::FindElementFromElementResponse find_element_from_element(JsonValue payload, String element_id) override;
@ -183,6 +194,8 @@ private:
GC::Ptr<Web::DOM::DocumentObserver> m_document_observer; GC::Ptr<Web::DOM::DocumentObserver> m_document_observer;
GC::Ptr<Web::HTML::NavigationObserver> m_navigation_observer; GC::Ptr<Web::HTML::NavigationObserver> m_navigation_observer;
GC::Ptr<GC::Timer> m_navigation_timer; GC::Ptr<GC::Timer> m_navigation_timer;
bool m_should_complete_driver_execution_when_navigation_starts_or_is_canceled { false };
}; };
} }

View file

@ -3,4 +3,6 @@
endpoint WebDriverServer { endpoint WebDriverServer {
driver_execution_complete(Web::WebDriver::Response response) =| driver_execution_complete(Web::WebDriver::Response response) =|
did_set_window_handle(String handle) =| did_set_window_handle(String handle) =|
did_start_window_replacement(String handle) =|
did_close_window(String handle) =|
} }

View file

@ -152,6 +152,7 @@ ErrorOr<int> ladybird_main(Main::Arguments arguments)
bool disable_scrollbar_painting = false; bool disable_scrollbar_painting = false;
bool disable_async_scrolling = false; bool disable_async_scrolling = false;
bool enable_sandbox = false; bool enable_sandbox = false;
bool report_session_history_updates_in_test_mode = false;
StringView echo_server_port_string_view {}; StringView echo_server_port_string_view {};
StringView default_time_zone {}; StringView default_time_zone {};
StringView style_invalidation_counter_dump_interval {}; StringView style_invalidation_counter_dump_interval {};
@ -175,6 +176,7 @@ ErrorOr<int> ladybird_main(Main::Arguments arguments)
args_parser.add_option(disable_scrollbar_painting, "Don't paint horizontal or vertical viewport scrollbars", "disable-scrollbar-painting"); args_parser.add_option(disable_scrollbar_painting, "Don't paint horizontal or vertical viewport scrollbars", "disable-scrollbar-painting");
args_parser.add_option(disable_async_scrolling, "Disable async scrolling", "disable-async-scrolling"); args_parser.add_option(disable_async_scrolling, "Disable async scrolling", "disable-async-scrolling");
args_parser.add_option(enable_sandbox, "Enable process sandboxing", "enable-sandbox"); args_parser.add_option(enable_sandbox, "Enable process sandboxing", "enable-sandbox");
args_parser.add_option(report_session_history_updates_in_test_mode, "Report session history updates in test mode", "report-session-history-updates-in-test-mode");
args_parser.add_option(echo_server_port_string_view, "Echo server port used in test internals", "echo-server-port", 0, "echo_server_port"); args_parser.add_option(echo_server_port_string_view, "Echo server port used in test internals", "echo-server-port", 0, "echo_server_port");
args_parser.add_option(is_headless, "Report that the browser is running in headless mode", "headless"); args_parser.add_option(is_headless, "Report that the browser is running in headless mode", "headless");
args_parser.add_option(default_time_zone, "Default time zone", "default-time-zone", 0, "time-zone-id"); args_parser.add_option(default_time_zone, "Default time zone", "default-time-zone", 0, "time-zone-id");
@ -226,6 +228,7 @@ ErrorOr<int> ladybird_main(Main::Arguments arguments)
Web::Painting::set_paint_viewport_scrollbars(!disable_scrollbar_painting); Web::Painting::set_paint_viewport_scrollbars(!disable_scrollbar_painting);
WebContent::PageClient::set_async_scrolling_enabled(!disable_async_scrolling); WebContent::PageClient::set_async_scrolling_enabled(!disable_async_scrolling);
WebContent::PageClient::set_should_report_session_history_updates_in_test_mode(report_session_history_updates_in_test_mode);
if (!echo_server_port_string_view.is_empty()) { if (!echo_server_port_string_view.is_empty()) {
if (auto maybe_echo_server_port = echo_server_port_string_view.to_number<u16>(); maybe_echo_server_port.has_value()) if (auto maybe_echo_server_port = echo_server_port_string_view.to_number<u16>(); maybe_echo_server_port.has_value())

View file

@ -11,6 +11,7 @@
#include <AK/Debug.h> #include <AK/Debug.h>
#include <AK/JsonObject.h> #include <AK/JsonObject.h>
#include <AK/JsonValue.h> #include <AK/JsonValue.h>
#include <AK/ScopeGuard.h>
#include <LibCore/EventLoop.h> #include <LibCore/EventLoop.h>
#include <LibCore/Timer.h> #include <LibCore/Timer.h>
#include <LibWeb/WebDriver/Capabilities.h> #include <LibWeb/WebDriver/Capabilities.h>
@ -21,6 +22,41 @@
namespace WebDriver { namespace WebDriver {
template<typename StartTraversal>
static Web::WebDriver::Response perform_history_traversal(Session& session, StartTraversal start_traversal, bool& wait_for_navigation_completion)
{
Optional<Web::WebDriver::Response> response;
RefPtr<WebContentConnection> connection { &session.web_content_connection() };
ScopeGuard guard { [&]() { connection->on_driver_execution_complete = nullptr; } };
connection->on_driver_execution_complete = [&](auto result) { response = move(result); };
auto traversal_response = start_traversal(*connection);
auto immediate_response = TRY(traversal_response.take_response());
wait_for_navigation_completion = traversal_response.wait_for_navigation_completion();
if (traversal_response.will_replace_web_content_process())
session.mark_current_window_as_awaiting_replacement(*connection);
if (!traversal_response.wait_for_driver_execution_complete())
return immediate_response;
Core::EventLoop::current().spin_until([&]() {
return response.has_value();
});
return TRY(response.release_value());
}
static ErrorOr<NonnullRefPtr<Session>, Web::WebDriver::Error>
find_session_with_ladybird_test_hooks(Web::WebDriver::Parameters const& parameters)
{
auto session = TRY(Session::find_session(parameters[0]));
if (!session->test_hooks_enabled())
return Web::WebDriver::Error::from_code(Web::WebDriver::ErrorCode::UnknownCommand,
"Ladybird test hooks are not enabled for this session"sv);
return session;
}
ErrorOr<NonnullRefPtr<Client>> Client::try_create(NonnullOwnPtr<Core::BufferedTCPSocket> socket, LaunchBrowserCallback launch_browser_callback) ErrorOr<NonnullRefPtr<Client>> Client::try_create(NonnullOwnPtr<Core::BufferedTCPSocket> socket, LaunchBrowserCallback launch_browser_callback)
{ {
if (!launch_browser_callback) if (!launch_browser_callback)
@ -162,9 +198,17 @@ Web::WebDriver::Response Client::navigate_to(Web::WebDriver::Parameters paramete
dbgln_if(WEBDRIVER_DEBUG, "Handling POST /session/<session_id>/url"); dbgln_if(WEBDRIVER_DEBUG, "Handling POST /session/<session_id>/url");
auto session = TRY(Session::find_session(parameters[0])); auto session = TRY(Session::find_session(parameters[0]));
return session->perform_async_action([&](auto& connection) { auto response = TRY(session->perform_async_action([&](auto& connection) {
return connection.navigate_to(move(payload)); auto navigate_response = connection.navigate_to(move(payload));
}); return navigate_response.response();
},
Session::WebContentReplacement::Allow));
TRY(session->wait_for_current_window_to_have_web_content_connection());
response = TRY(session->perform_async_action([&](auto& connection) {
return connection.wait_for_navigation_completion();
}));
TRY(session->wait_for_current_window_to_have_web_content_connection());
return response;
} }
// 10.2 Get Current URL, https://w3c.github.io/webdriver/#dfn-get-current-url // 10.2 Get Current URL, https://w3c.github.io/webdriver/#dfn-get-current-url
@ -186,9 +230,16 @@ Web::WebDriver::Response Client::back(Web::WebDriver::Parameters parameters, Jso
dbgln_if(WEBDRIVER_DEBUG, "Handling POST /session/<session_id>/back"); dbgln_if(WEBDRIVER_DEBUG, "Handling POST /session/<session_id>/back");
auto session = TRY(Session::find_session(parameters[0])); auto session = TRY(Session::find_session(parameters[0]));
return session->perform_async_action([&](auto& connection) { bool wait_for_navigation_completion = true;
return connection.back(); auto response = TRY(perform_history_traversal(*session, [&](auto& connection) { return connection.back(); }, wait_for_navigation_completion));
}); TRY(session->wait_for_current_window_to_have_web_content_connection());
if (wait_for_navigation_completion) {
response = TRY(session->perform_async_action([&](auto& connection) {
return connection.wait_for_navigation_completion();
}));
TRY(session->wait_for_current_window_to_have_web_content_connection());
}
return response;
} }
// 10.4 Forward, https://w3c.github.io/webdriver/#dfn-forward // 10.4 Forward, https://w3c.github.io/webdriver/#dfn-forward
@ -198,9 +249,16 @@ Web::WebDriver::Response Client::forward(Web::WebDriver::Parameters parameters,
dbgln_if(WEBDRIVER_DEBUG, "Handling POST /session/<session_id>/forward"); dbgln_if(WEBDRIVER_DEBUG, "Handling POST /session/<session_id>/forward");
auto session = TRY(Session::find_session(parameters[0])); auto session = TRY(Session::find_session(parameters[0]));
return session->perform_async_action([&](auto& connection) { bool wait_for_navigation_completion = true;
return connection.forward(); auto response = TRY(perform_history_traversal(*session, [&](auto& connection) { return connection.forward(); }, wait_for_navigation_completion));
}); TRY(session->wait_for_current_window_to_have_web_content_connection());
if (wait_for_navigation_completion) {
response = TRY(session->perform_async_action([&](auto& connection) {
return connection.wait_for_navigation_completion();
}));
TRY(session->wait_for_current_window_to_have_web_content_connection());
}
return response;
} }
// 10.5 Refresh, https://w3c.github.io/webdriver/#dfn-refresh // 10.5 Refresh, https://w3c.github.io/webdriver/#dfn-refresh
@ -210,9 +268,14 @@ Web::WebDriver::Response Client::refresh(Web::WebDriver::Parameters parameters,
dbgln_if(WEBDRIVER_DEBUG, "Handling POST /session/<session_id>/refresh"); dbgln_if(WEBDRIVER_DEBUG, "Handling POST /session/<session_id>/refresh");
auto session = TRY(Session::find_session(parameters[0])); auto session = TRY(Session::find_session(parameters[0]));
return session->perform_async_action([&](auto& connection) { auto response = TRY(session->perform_async_action([&](auto& connection) {
return connection.refresh(); return connection.refresh();
}); }));
if (TRY(session->wait_for_current_window_to_have_web_content_connection()))
response = TRY(session->perform_async_action([&](auto& connection) {
return connection.wait_for_navigation_completion();
}));
return response;
} }
// 10.6 Get Title, https://w3c.github.io/webdriver/#dfn-get-title // 10.6 Get Title, https://w3c.github.io/webdriver/#dfn-get-title
@ -227,6 +290,92 @@ Web::WebDriver::Response Client::get_title(Web::WebDriver::Parameters parameters
}); });
} }
// Extension: POST /session/{session id}/ladybird/crash-current-page
Web::WebDriver::Response Client::crash_current_page(Web::WebDriver::Parameters parameters, JsonValue)
{
dbgln_if(WEBDRIVER_DEBUG, "Handling POST /session/<session_id>/ladybird/crash-current-page");
auto session = TRY(find_session_with_ladybird_test_hooks(parameters));
RefPtr connection { &session->web_content_connection() };
session->mark_current_window_as_awaiting_replacement(*connection);
connection->async_crash_current_page();
TRY(session->wait_for_current_window_to_have_web_content_connection());
return session->perform_async_action([&](auto& connection) {
return connection.wait_for_navigation_completion();
});
}
// Extension: POST /session/{session id}/ladybird/load-url-from-ui
Web::WebDriver::Response Client::load_url_from_ui(Web::WebDriver::Parameters parameters, JsonValue payload)
{
dbgln_if(WEBDRIVER_DEBUG, "Handling POST /session/<session_id>/ladybird/load-url-from-ui");
auto session = TRY(find_session_with_ladybird_test_hooks(parameters));
RefPtr previous_connection { &session->web_content_connection() };
auto response = TRY(session->perform_async_action([&](auto& connection) {
return connection.load_url_from_ui(move(payload));
}));
if (response.is_object() && response.as_object().get_bool("willReplaceWebContentProcess"sv).value_or(false))
session->mark_current_window_as_awaiting_replacement(*previous_connection);
TRY(session->wait_for_current_window_to_have_web_content_connection());
response = TRY(session->perform_async_action([&](auto& connection) {
return connection.wait_for_navigation_completion();
}));
TRY(session->wait_for_current_window_to_have_web_content_connection());
return response;
}
// Extension: POST /session/{session id}/ladybird/traverse-history-from-ui
Web::WebDriver::Response Client::traverse_history_from_ui(Web::WebDriver::Parameters parameters, JsonValue payload)
{
dbgln_if(WEBDRIVER_DEBUG, "Handling POST /session/<session_id>/ladybird/traverse-history-from-ui");
auto session = TRY(find_session_with_ladybird_test_hooks(parameters));
auto wait_for_navigation_completion = true;
if (payload.is_object())
wait_for_navigation_completion = payload.as_object().get_bool("waitForNavigationCompletion"sv).value_or(true);
RefPtr previous_connection { &session->web_content_connection() };
auto response = TRY(session->perform_async_action([&](auto& connection) {
return connection.traverse_history_from_ui(move(payload));
}));
if (response.is_object() && response.as_object().get_bool("willReplaceWebContentProcess"sv).value_or(false))
session->mark_current_window_as_awaiting_replacement(*previous_connection);
TRY(session->wait_for_current_window_to_have_web_content_connection());
if (!wait_for_navigation_completion)
return JsonValue {};
response = TRY(session->perform_async_action([&](auto& connection) {
return connection.wait_for_navigation_completion();
}));
TRY(session->wait_for_current_window_to_have_web_content_connection());
return response;
}
// Extension: POST /session/{session id}/ladybird/mark-web-content-session-history-stale
Web::WebDriver::Response Client::mark_web_content_session_history_stale(Web::WebDriver::Parameters parameters, JsonValue)
{
dbgln_if(WEBDRIVER_DEBUG, "Handling POST /session/<session_id>/ladybird/mark-web-content-session-history-stale");
auto session = TRY(find_session_with_ladybird_test_hooks(parameters));
return session->perform_async_action([&](auto& connection) {
return connection.mark_web_content_session_history_stale();
});
}
// Extension: GET /session/{session id}/ladybird/session-history
Web::WebDriver::Response Client::get_session_history(Web::WebDriver::Parameters parameters, JsonValue)
{
dbgln_if(WEBDRIVER_DEBUG, "Handling GET /session/<session_id>/ladybird/session-history");
auto session = TRY(find_session_with_ladybird_test_hooks(parameters));
return session->perform_async_action([&](auto& connection) {
return connection.get_session_history();
});
}
// 11.1 Get Window Handle, https://w3c.github.io/webdriver/#get-window-handle // 11.1 Get Window Handle, https://w3c.github.io/webdriver/#get-window-handle
// GET /session/{session id}/window // GET /session/{session id}/window
Web::WebDriver::Response Client::get_window_handle(Web::WebDriver::Parameters parameters, JsonValue) Web::WebDriver::Response Client::get_window_handle(Web::WebDriver::Parameters parameters, JsonValue)
@ -755,7 +904,8 @@ Web::WebDriver::Response Client::perform_actions(Web::WebDriver::Parameters para
return session->perform_async_action([&](auto& connection) { return session->perform_async_action([&](auto& connection) {
return connection.perform_actions(move(payload)); return connection.perform_actions(move(payload));
}); },
Session::WebContentReplacement::Allow);
} }
// 15.8 Release Actions, https://w3c.github.io/webdriver/#release-actions // 15.8 Release Actions, https://w3c.github.io/webdriver/#release-actions

View file

@ -55,6 +55,11 @@ private:
virtual Web::WebDriver::Response minimize_window(Web::WebDriver::Parameters parameters, JsonValue payload) override; virtual Web::WebDriver::Response minimize_window(Web::WebDriver::Parameters parameters, JsonValue payload) override;
virtual Web::WebDriver::Response fullscreen_window(Web::WebDriver::Parameters parameters, JsonValue payload) override; virtual Web::WebDriver::Response fullscreen_window(Web::WebDriver::Parameters parameters, JsonValue payload) override;
virtual Web::WebDriver::Response consume_user_activation(Web::WebDriver::Parameters parameters, JsonValue payload) override; virtual Web::WebDriver::Response consume_user_activation(Web::WebDriver::Parameters parameters, JsonValue payload) override;
virtual Web::WebDriver::Response crash_current_page(Web::WebDriver::Parameters parameters, JsonValue payload) override;
virtual Web::WebDriver::Response load_url_from_ui(Web::WebDriver::Parameters parameters, JsonValue payload) override;
virtual Web::WebDriver::Response traverse_history_from_ui(Web::WebDriver::Parameters parameters, JsonValue payload) override;
virtual Web::WebDriver::Response mark_web_content_session_history_stale(Web::WebDriver::Parameters parameters, JsonValue payload) override;
virtual Web::WebDriver::Response get_session_history(Web::WebDriver::Parameters parameters, JsonValue payload) override;
virtual Web::WebDriver::Response find_element(Web::WebDriver::Parameters parameters, JsonValue payload) override; virtual Web::WebDriver::Response find_element(Web::WebDriver::Parameters parameters, JsonValue payload) override;
virtual Web::WebDriver::Response find_elements(Web::WebDriver::Parameters parameters, JsonValue payload) override; virtual Web::WebDriver::Response find_elements(Web::WebDriver::Parameters parameters, JsonValue payload) override;
virtual Web::WebDriver::Response find_element_from_element(Web::WebDriver::Parameters parameters, JsonValue payload) override; virtual Web::WebDriver::Response find_element_from_element(Web::WebDriver::Parameters parameters, JsonValue payload) override;

View file

@ -10,6 +10,7 @@
#include <AK/HashMap.h> #include <AK/HashMap.h>
#include <AK/JsonObject.h> #include <AK/JsonObject.h>
#include <AK/NumericLimits.h>
#if !defined(AK_OS_MACOS) #if !defined(AK_OS_MACOS)
# include <LibCore/LocalServer.h> # include <LibCore/LocalServer.h>
# include <LibCore/Socket.h> # include <LibCore/Socket.h>
@ -19,6 +20,7 @@
# include <LibWebView/Utilities.h> # include <LibWebView/Utilities.h>
#endif #endif
#include <LibCore/System.h> #include <LibCore/System.h>
#include <LibCore/Timer.h>
#include <LibIPC/Transport.h> #include <LibIPC/Transport.h>
#include <LibWeb/Crypto/Crypto.h> #include <LibWeb/Crypto/Crypto.h>
#include <LibWeb/WebDriver/Proxy.h> #include <LibWeb/WebDriver/Proxy.h>
@ -140,7 +142,7 @@ ErrorOr<NonnullRefPtr<Session>, Web::WebDriver::Error> Session::find_session(Str
if (auto session = sessions.get(session_id); session.has_value()) { if (auto session = sessions.get(session_id); session.has_value()) {
if (allow_invalid_window_handle == AllowInvalidWindowHandle::No) if (allow_invalid_window_handle == AllowInvalidWindowHandle::No)
TRY(session.value()->ensure_current_window_handle_is_valid()); TRY(session.value()->wait_for_current_window_to_have_web_content_connection());
return *session.release_value(); return *session.release_value();
} }
@ -181,8 +183,17 @@ void Session::close()
Web::WebDriver::reset_has_proxy_configuration(); Web::WebDriver::reset_has_proxy_configuration();
// 5. Optionally, close all top-level browsing contexts, without prompting to unload. // 5. Optionally, close all top-level browsing contexts, without prompting to unload.
for (auto& it : m_windows) for (auto& it : m_windows) {
if (!it.value.web_content_connection)
continue;
it.value.web_content_connection->on_close = nullptr;
it.value.web_content_connection->on_driver_execution_complete = nullptr;
it.value.web_content_connection->on_did_set_window_handle = nullptr;
it.value.web_content_connection->on_did_start_window_replacement = nullptr;
it.value.web_content_connection->on_did_close_window = nullptr;
it.value.web_content_connection->close_session(); it.value.web_content_connection->close_session();
}
} }
// -> Remote end is an intermediary node // -> Remote end is an intermediary node
// 1. Close the associated session. If this causes an error to occur, complete the remainder of this algorithm // 1. Close the associated session. If this causes an error to occur, complete the remainder of this algorithm
@ -191,7 +202,10 @@ void Session::close()
// 4. Perform any implementation-specific cleanup steps. // 4. Perform any implementation-specific cleanup steps.
for (auto& [_, connection] : m_pending_connections) { for (auto& [_, connection] : m_pending_connections) {
connection->on_close = nullptr; connection->on_close = nullptr;
connection->on_driver_execution_complete = nullptr;
connection->on_did_set_window_handle = nullptr; connection->on_did_set_window_handle = nullptr;
connection->on_did_start_window_replacement = nullptr;
connection->on_did_close_window = nullptr;
} }
m_pending_connections.clear(); m_pending_connections.clear();
@ -225,21 +239,26 @@ ErrorOr<void> Session::accept_web_content_transport(NonnullOwnPtr<IPC::Transport
} }
}; };
web_content_connection->on_did_set_window_handle = [this, promise, connection_id](String window_handle) { web_content_connection->on_did_set_window_handle = [this, promise, connection_id, connection = web_content_connection.ptr()](String window_handle) {
auto maybe_pending_connection = m_pending_connections.take(connection_id); auto maybe_pending_connection = m_pending_connections.take(connection_id);
if (!maybe_pending_connection.has_value()) if (!maybe_pending_connection.has_value()) {
did_update_window_handle(move(window_handle), *connection);
return; return;
}
auto pending_connection = maybe_pending_connection.value(); auto pending_connection = maybe_pending_connection.value();
pending_connection->on_did_set_window_handle = nullptr;
dbgln_if(WEBDRIVER_DEBUG, "Window {} registered with WebDriver.", window_handle); dbgln_if(WEBDRIVER_DEBUG, "Window {} registered with WebDriver.", window_handle);
pending_connection->on_close = [this, window_handle]() { pending_connection->on_close = [this, connection]() {
dbgln_if(WEBDRIVER_DEBUG, "Window {} was closed remotely.", window_handle); dbgln_if(WEBDRIVER_DEBUG, "WebContent connection closed remotely.");
m_windows.remove(window_handle); web_content_connection_closed(*connection);
if (m_windows.is_empty()) };
close(); pending_connection->on_did_start_window_replacement = [this, connection](String replaced_window_handle) {
did_start_window_replacement(replaced_window_handle, *connection);
};
pending_connection->on_did_close_window = [this, connection](String closed_window_handle) {
did_close_window(closed_window_handle, *connection);
}; };
pending_connection->async_set_page_load_strategy(m_page_load_strategy); pending_connection->async_set_page_load_strategy(m_page_load_strategy);
@ -248,7 +267,12 @@ ErrorOr<void> Session::accept_web_content_transport(NonnullOwnPtr<IPC::Transport
if (m_timeouts_configuration.has_value()) if (m_timeouts_configuration.has_value())
pending_connection->async_set_timeouts(*m_timeouts_configuration); pending_connection->async_set_timeouts(*m_timeouts_configuration);
m_windows.set(window_handle, Session::Window { window_handle, move(pending_connection) }); if (auto window = m_windows.find(window_handle); window != m_windows.end()) {
window->value.web_content_connection = move(pending_connection);
window->value.is_awaiting_replacement = false;
} else {
m_windows.set(window_handle, Session::Window { window_handle, move(pending_connection) });
}
if (m_current_window_handle.is_empty()) if (m_current_window_handle.is_empty())
m_current_window_handle = window_handle; m_current_window_handle = window_handle;
@ -258,6 +282,98 @@ ErrorOr<void> Session::accept_web_content_transport(NonnullOwnPtr<IPC::Transport
return {}; return {};
} }
void Session::web_content_connection_closed(WebContentConnection const& connection)
{
Optional<String> closed_window_handle;
for (auto& window : m_windows) {
if (window.value.web_content_connection.ptr() != &connection)
continue;
if (window.value.is_awaiting_replacement) {
window.value.web_content_connection = nullptr;
return;
}
closed_window_handle = window.key;
break;
}
if (closed_window_handle.has_value())
remove_window(*closed_window_handle);
}
void Session::did_update_window_handle(String window_handle, WebContentConnection const& connection)
{
Optional<String> previous_window_handle;
for (auto const& window : m_windows) {
if (window.value.web_content_connection.ptr() == &connection) {
previous_window_handle = window.key;
break;
}
}
if (!previous_window_handle.has_value() || *previous_window_handle == window_handle)
return;
auto maybe_window = m_windows.take(*previous_window_handle);
if (!maybe_window.has_value())
return;
auto window = maybe_window.release_value();
window.handle = window_handle;
window.is_awaiting_replacement = false;
if (auto existing_window = m_windows.find(window_handle); existing_window != m_windows.end()) {
existing_window->value.web_content_connection = move(window.web_content_connection);
existing_window->value.is_awaiting_replacement = false;
} else {
m_windows.set(window_handle, move(window));
}
if (m_current_window_handle == *previous_window_handle)
m_current_window_handle = move(window_handle);
}
void Session::did_start_window_replacement(String const& window_handle, WebContentConnection const& connection)
{
auto window = m_windows.find(window_handle);
if (window == m_windows.end() || window->value.web_content_connection.ptr() != &connection)
return;
window->value.is_awaiting_replacement = true;
window->value.web_content_connection = nullptr;
}
void Session::mark_current_window_as_awaiting_replacement(WebContentConnection const& connection)
{
auto window = m_windows.find(m_current_window_handle);
if (window == m_windows.end() || window->value.web_content_connection.ptr() != &connection)
return;
window->value.is_awaiting_replacement = true;
window->value.web_content_connection = nullptr;
}
void Session::did_close_window(String const& window_handle, WebContentConnection const& connection)
{
auto window = m_windows.find(window_handle);
if (window == m_windows.end() || window->value.web_content_connection.ptr() != &connection)
return;
remove_window(window_handle);
}
void Session::remove_window(StringView window_handle)
{
m_windows.remove(window_handle);
if (m_current_window_handle == window_handle)
m_current_window_handle = "NoSuchWindowPleaseSelectANewOne"_string;
if (m_windows.is_empty())
close();
}
ErrorOr<void> Session::create_server(NonnullRefPtr<ServerPromise> promise) ErrorOr<void> Session::create_server(NonnullRefPtr<ServerPromise> promise)
{ {
#if defined(AK_OS_WINDOWS) #if defined(AK_OS_WINDOWS)
@ -356,14 +472,9 @@ Web::WebDriver::Response Session::close_window()
return connection.close_window(); return connection.close_window();
})); }));
{ // 4. If there are no more open top-level browsing contexts, then close the session.
// Defer removing the window handle from this session until after we know we are done with its connection. auto closed_window_handle = m_current_window_handle;
ScopeGuard guard { [this] { m_windows.remove(m_current_window_handle); m_current_window_handle = "NoSuchWindowPleaseSelectANewOne"_string; } }; remove_window(closed_window_handle);
// 4. If there are no more open top-level browsing contexts, then close the session.
if (m_windows.size() == 1)
close();
}
// 5. Return the result of running the remote end steps for the Get Window Handles command. // 5. Return the result of running the remote end steps for the Get Window Handles command.
return get_window_handles(); return get_window_handles();
@ -375,10 +486,14 @@ Web::WebDriver::Response Session::switch_to_window(StringView handle)
// 4. If handle is equal to the associated window handle for some top-level browsing context, let context be the that // 4. If handle is equal to the associated window handle for some top-level browsing context, let context be the that
// browsing context, and set the current top-level browsing context with session and context. // browsing context, and set the current top-level browsing context with session and context.
// Otherwise, return error with error code no such window. // Otherwise, return error with error code no such window.
if (auto it = m_windows.find(handle); it != m_windows.end()) if (auto it = m_windows.find(handle); it != m_windows.end()) {
if (!it->value.web_content_connection)
return Web::WebDriver::Error::from_code(Web::WebDriver::ErrorCode::UnknownError, "Window is waiting for a replacement WebContent process"sv);
m_current_window_handle = it->key; m_current_window_handle = it->key;
else } else {
return Web::WebDriver::Error::from_code(Web::WebDriver::ErrorCode::NoSuchWindow, "Window not found"sv); return Web::WebDriver::Error::from_code(Web::WebDriver::ErrorCode::NoSuchWindow, "Window not found"sv);
}
// 5. Update any implementation-specific state that would result from the user selecting the current // 5. Update any implementation-specific state that would result from the user selecting the current
// browsing context for interaction, without altering OS-level focus. // browsing context for interaction, without altering OS-level focus.
@ -405,9 +520,60 @@ Web::WebDriver::Response Session::get_window_handles() const
ErrorOr<void, Web::WebDriver::Error> Session::ensure_current_window_handle_is_valid() const ErrorOr<void, Web::WebDriver::Error> Session::ensure_current_window_handle_is_valid() const
{ {
if (auto current_window = m_windows.get(m_current_window_handle); current_window.has_value()) auto current_window = m_windows.get(m_current_window_handle);
return {}; if (!current_window.has_value())
return Web::WebDriver::Error::from_code(Web::WebDriver::ErrorCode::NoSuchWindow, "Window not found"sv); return Web::WebDriver::Error::from_code(Web::WebDriver::ErrorCode::NoSuchWindow, "Window not found"sv);
if (!current_window->web_content_connection)
return Web::WebDriver::Error::from_code(Web::WebDriver::ErrorCode::UnknownError, "Window is waiting for a replacement WebContent process"sv);
return {};
}
ErrorOr<bool, Web::WebDriver::Error> Session::wait_for_current_window_to_have_web_content_connection()
{
m_event_loop.pump(Core::EventLoop::WaitMode::PollForEvents);
auto current_window = m_windows.get(m_current_window_handle);
if (!current_window.has_value())
return Web::WebDriver::Error::from_code(Web::WebDriver::ErrorCode::NoSuchWindow, "Window not found"sv);
if (current_window->web_content_connection)
return false;
Optional<u64> page_load_timeout = Web::WebDriver::TimeoutsConfiguration {}.page_load_timeout;
if (m_timeouts_configuration.has_value() && m_timeouts_configuration->is_object()) {
if (auto value = m_timeouts_configuration->as_object().get("pageLoad"sv); value.has_value()) {
if (value->is_null())
page_load_timeout = {};
else
page_load_timeout = value->get_integer<u64>().value_or(*page_load_timeout);
}
}
bool timed_out = false;
RefPtr<Core::Timer> timer;
if (page_load_timeout.has_value()) {
auto timer_interval = *page_load_timeout > NumericLimits<int>::max() ? NumericLimits<int>::max() : static_cast<int>(*page_load_timeout);
timer = Core::Timer::create_single_shot(timer_interval, [&timed_out] {
timed_out = true;
});
timer->start();
}
Core::EventLoop::current().spin_until([this, &timed_out] {
auto current_window = m_windows.get(m_current_window_handle);
return !current_window.has_value() || current_window->web_content_connection || timed_out;
});
if (timer)
timer->stop();
if (timed_out)
return Web::WebDriver::Error::from_code(Web::WebDriver::ErrorCode::Timeout, "Timed out waiting for replacement WebContent process"sv);
TRY(ensure_current_window_handle_is_valid());
return true;
} }
} }

View file

@ -46,15 +46,17 @@ public:
struct Window { struct Window {
String handle; String handle;
NonnullRefPtr<WebContentConnection> web_content_connection; RefPtr<WebContentConnection> web_content_connection;
bool is_awaiting_replacement { false };
}; };
WebContentConnection& web_content_connection() const WebContentConnection& web_content_connection() const
{ {
auto current_window = m_windows.get(m_current_window_handle); auto current_window = m_windows.get(m_current_window_handle);
VERIFY(current_window.has_value()); VERIFY(current_window.has_value());
VERIFY(current_window->web_content_connection);
return current_window->web_content_connection; return *current_window->web_content_connection;
} }
void close(); void close();
@ -62,6 +64,7 @@ public:
String session_id() const { return m_session_id; } String session_id() const { return m_session_id; }
Web::WebDriver::SessionFlags session_flags() const { return m_session_flags; } Web::WebDriver::SessionFlags session_flags() const { return m_session_flags; }
String const& current_window_handle() const { return m_current_window_handle; } String const& current_window_handle() const { return m_current_window_handle; }
bool test_hooks_enabled() const { return m_options.enable_test_hooks; }
bool has_window_handle(StringView handle) const { return m_windows.contains(handle); } bool has_window_handle(StringView handle) const { return m_windows.contains(handle); }
@ -70,23 +73,44 @@ public:
Web::WebDriver::Response switch_to_window(StringView); Web::WebDriver::Response switch_to_window(StringView);
Web::WebDriver::Response get_window_handles() const; Web::WebDriver::Response get_window_handles() const;
ErrorOr<void, Web::WebDriver::Error> ensure_current_window_handle_is_valid() const; ErrorOr<void, Web::WebDriver::Error> ensure_current_window_handle_is_valid() const;
ErrorOr<bool, Web::WebDriver::Error> wait_for_current_window_to_have_web_content_connection();
void mark_current_window_as_awaiting_replacement(WebContentConnection const&);
enum class WebContentReplacement {
Disallow,
Allow,
};
template<typename Action> template<typename Action>
Web::WebDriver::Response perform_async_action(Action&& action) Web::WebDriver::Response perform_async_action(Action&& action, WebContentReplacement web_content_replacement = WebContentReplacement::Disallow)
{ {
Optional<Web::WebDriver::Response> response; Optional<Web::WebDriver::Response> response;
auto& connection = web_content_connection(); RefPtr connection { &web_content_connection() };
ScopeGuard guard { [&]() { connection.on_driver_execution_complete = nullptr; } }; ScopeGuard guard { [&]() { connection->on_driver_execution_complete = nullptr; } };
connection.on_driver_execution_complete = [&](auto result) { response = move(result); }; connection->on_driver_execution_complete = [&](auto result) { response = move(result); };
TRY(action(connection)); TRY(action(*connection));
Core::EventLoop::current().spin_until([&]() { Core::EventLoop::current().spin_until([&]() {
return response.has_value(); if (response.has_value())
return true;
if (web_content_replacement == WebContentReplacement::Disallow)
return false;
auto current_window = m_windows.get(m_current_window_handle);
return !current_window.has_value() || (current_window->is_awaiting_replacement && !current_window->web_content_connection);
}); });
return response.release_value(); if (response.has_value())
return response.release_value();
TRY(wait_for_current_window_to_have_web_content_connection());
if (response.has_value())
return response.release_value();
return JsonValue {};
} }
private: private:
@ -97,6 +121,11 @@ private:
ErrorOr<void> start(LaunchBrowserCallback const&); ErrorOr<void> start(LaunchBrowserCallback const&);
ErrorOr<void> accept_web_content_transport(NonnullOwnPtr<IPC::Transport>, NonnullRefPtr<ServerPromise> promise); ErrorOr<void> accept_web_content_transport(NonnullOwnPtr<IPC::Transport>, NonnullRefPtr<ServerPromise> promise);
ErrorOr<void> create_server(NonnullRefPtr<ServerPromise> promise); ErrorOr<void> create_server(NonnullRefPtr<ServerPromise> promise);
void web_content_connection_closed(WebContentConnection const&);
void did_update_window_handle(String window_handle, WebContentConnection const&);
void did_start_window_replacement(String const& window_handle, WebContentConnection const&);
void did_close_window(String const& window_handle, WebContentConnection const&);
void remove_window(StringView window_handle);
NonnullRefPtr<Client> m_client; NonnullRefPtr<Client> m_client;
Web::WebDriver::LadybirdOptions m_options; Web::WebDriver::LadybirdOptions m_options;

View file

@ -32,4 +32,16 @@ void WebContentConnection::did_set_window_handle(String handle)
on_did_set_window_handle(move(handle)); on_did_set_window_handle(move(handle));
} }
void WebContentConnection::did_start_window_replacement(String handle)
{
if (on_did_start_window_replacement)
on_did_start_window_replacement(move(handle));
}
void WebContentConnection::did_close_window(String handle)
{
if (on_did_close_window)
on_did_close_window(move(handle));
}
} }

View file

@ -21,15 +21,26 @@ class WebContentConnection
public: public:
explicit WebContentConnection(NonnullOwnPtr<IPC::Transport> transport); explicit WebContentConnection(NonnullOwnPtr<IPC::Transport> transport);
Web::WebDriver::Response wait_for_navigation_completion()
{
auto response = send_sync_but_allow_failure<Messages::WebDriverClient::WaitForNavigationCompletion>();
VERIFY(response);
return response->response();
}
Function<void()> on_close; Function<void()> on_close;
Function<void(Web::WebDriver::Response)> on_driver_execution_complete; Function<void(Web::WebDriver::Response)> on_driver_execution_complete;
Function<void(String)> on_did_set_window_handle; Function<void(String)> on_did_set_window_handle;
Function<void(String)> on_did_start_window_replacement;
Function<void(String)> on_did_close_window;
private: private:
virtual void die() override; virtual void die() override;
virtual void driver_execution_complete(Web::WebDriver::Response) override; virtual void driver_execution_complete(Web::WebDriver::Response) override;
virtual void did_set_window_handle(String) override; virtual void did_set_window_handle(String) override;
virtual void did_start_window_replacement(String) override;
virtual void did_close_window(String) override;
}; };
} }

View file

@ -7,6 +7,7 @@
#include <AK/Platform.h> #include <AK/Platform.h>
#include <LibCore/ArgsParser.h> #include <LibCore/ArgsParser.h>
#include <LibCore/Directory.h> #include <LibCore/Directory.h>
#include <LibCore/Environment.h>
#include <LibCore/EventLoop.h> #include <LibCore/EventLoop.h>
#include <LibCore/Process.h> #include <LibCore/Process.h>
#include <LibCore/StandardPaths.h> #include <LibCore/StandardPaths.h>
@ -68,7 +69,8 @@ static Vector<ByteString> create_arguments(ByteString const& webdriver_endpoint,
arguments.append(ByteString::formatted("--default-time-zone={}", default_time_zone.value())); arguments.append(ByteString::formatted("--default-time-zone={}", default_time_zone.value()));
// FIXME: WebDriver does not yet handle the WebContent process switch brought by site isolation. // FIXME: WebDriver does not yet handle the WebContent process switch brought by site isolation.
arguments.append("--disable-site-isolation"sv); if (!Core::Environment::has("LADYBIRD_WEBDRIVER_ENABLE_SITE_ISOLATION"sv))
arguments.append("--disable-site-isolation"sv);
arguments.append("about:blank"sv); arguments.append("about:blank"sv);
return arguments; return arguments;

View file

@ -89,11 +89,11 @@ void BrowserWindow::register_actions()
add_action("go-back", [](BrowserWindow& self) { add_action("go-back", [](BrowserWindow& self) {
if (auto* tab = self.current_tab()) if (auto* tab = self.current_tab())
tab->view().traverse_the_history_by_delta(-1); }, false); (void)tab->view().traverse_the_history_by_delta(-1); }, false);
add_action("go-forward", [](BrowserWindow& self) { add_action("go-forward", [](BrowserWindow& self) {
if (auto* tab = self.current_tab()) if (auto* tab = self.current_tab())
tab->view().traverse_the_history_by_delta(1); }, false); (void)tab->view().traverse_the_history_by_delta(1); }, false);
add_action("zoom-in", [](BrowserWindow& self) { add_action("zoom-in", [](BrowserWindow& self) {
if (auto* tab = self.current_tab()) if (auto* tab = self.current_tab())
@ -400,17 +400,6 @@ int BrowserWindow::tab_count() const
return adw_tab_view_get_n_pages(m_tab_view); return adw_tab_view_get_n_pages(m_tab_view);
} }
void BrowserWindow::update_navigation_buttons(bool back_enabled, bool forward_enabled)
{
auto* back_action = G_SIMPLE_ACTION(g_action_map_lookup_action(G_ACTION_MAP(m_window), "go-back"));
if (back_action)
g_simple_action_set_enabled(back_action, back_enabled);
auto* forward_action = G_SIMPLE_ACTION(g_action_map_lookup_action(G_ACTION_MAP(m_window), "go-forward"));
if (forward_action)
g_simple_action_set_enabled(forward_action, forward_enabled);
}
void BrowserWindow::bind_navigation_actions(WebContentView& view) void BrowserWindow::bind_navigation_actions(WebContentView& view)
{ {
m_back_binding.detach(); m_back_binding.detach();

View file

@ -37,7 +37,6 @@ public:
void present(); void present();
int tab_count() const; int tab_count() const;
void update_navigation_buttons(bool back_enabled, bool forward_enabled);
void update_location_entry(StringView url); void update_location_entry(StringView url);
void update_location_favicon(GdkPaintable* favicon); void update_location_favicon(GdkPaintable* favicon);
void update_location_loading(bool is_loading); void update_location_loading(bool is_loading);

View file

@ -575,9 +575,9 @@ void WebContentView::mouseReleaseEvent(QMouseEvent* event)
enqueue_native_event(Web::MouseEvent::Type::MouseUp, *event); enqueue_native_event(Web::MouseEvent::Type::MouseUp, *event);
if (event->button() == Qt::MouseButton::BackButton) if (event->button() == Qt::MouseButton::BackButton)
traverse_the_history_by_delta(-1); (void)traverse_the_history_by_delta(-1);
else if (event->button() == Qt::MouseButton::ForwardButton) else if (event->button() == Qt::MouseButton::ForwardButton)
traverse_the_history_by_delta(1); (void)traverse_the_history_by_delta(1);
} }
void WebContentView::wheelEvent(QWheelEvent* event) void WebContentView::wheelEvent(QWheelEvent* event)