LibWeb: Resume intercepted history traversals
When a traverse navigate event is intercepted, queue the resume step specified by the Navigation API. This keeps intercepted traversals from stopping at URL update time. Do not resume that queued step if the intercepted NavigateEvent was aborted or replaced by a newer ongoing event before it runs. Add tests for the basic intercepted traverse case and the superseded intercepted traverse case.
This commit is contained in:
parent
e356fd8188
commit
3ec9733efe
8 changed files with 241 additions and 9 deletions
|
|
@ -1142,14 +1142,48 @@ bool Navigation::inner_navigate_event_firing_algorithm(
|
|||
// promise as handled at the equivalent place.
|
||||
WebIDL::mark_promise_as_handled(*m_transition->committed());
|
||||
|
||||
// 6. If navigationType is "traverse", then set navigation's suppress normal scroll restoration during ongoing navigation to true.
|
||||
// NOTE: If event's scroll behavior was set to "after-transition", then scroll restoration will happen as part of finishing
|
||||
// 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
|
||||
// is either done manually, by the web developer, or is done after the transition.
|
||||
if (navigation_type == Bindings::NavigationType::Traverse)
|
||||
// Switch on event's navigationType:
|
||||
// - "traverse":
|
||||
if (navigation_type == Bindings::NavigationType::Traverse) {
|
||||
// 1. Set navigation's suppress normal scroll restoration during ongoing navigation to true.
|
||||
// NOTE: If event's scroll behavior was set to "after-transition", then scroll restoration will happen as part of finishing
|
||||
// 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
|
||||
// is either done manually, by the web developer, or is done after the transition.
|
||||
m_suppress_scroll_restoration_during_ongoing_navigation = true;
|
||||
|
||||
// 2. Let userInvolvement be "none".
|
||||
auto user_involvement_for_resume = UserNavigationInvolvement::None;
|
||||
|
||||
// 3. If event's userInitiated is true, then set userInvolvement to "activation".
|
||||
if (event->user_initiated())
|
||||
user_involvement_for_resume = UserNavigationInvolvement::Activation;
|
||||
|
||||
// NOTE: At this point after interception, it is not consequential whether the
|
||||
// activation was a result of browser UI.
|
||||
|
||||
// 4. Append the following session history traversal steps to navigable's traversable navigable:
|
||||
auto destination_entry = event->destination()->navigation_history_entry();
|
||||
VERIFY(destination_entry);
|
||||
auto target_step = destination_entry->session_history_entry().step().get<int>();
|
||||
auto traversable = navigable->traversable_navigable();
|
||||
traversable->append_session_history_traversal_steps(GC::create_function(heap(), [this, event, traversable, target_step, user_involvement_for_resume](NonnullRefPtr<Core::Promise<Empty>> signal) {
|
||||
// NB: This appended step can run after a later navigation has aborted the intercepted
|
||||
// traverse. In that case, the aborted traverse must not be resumed.
|
||||
if (event->abort_controller()->signal()->aborted() || event != m_ongoing_navigate_event) {
|
||||
signal->resolve({});
|
||||
return;
|
||||
}
|
||||
|
||||
// 1. Resume applying the traverse history step given event's destination's entry's session history entry's step,
|
||||
// navigable's traversable navigable, and userInvolvement.
|
||||
traversable->resume_applying_the_traverse_history_step(target_step, user_involvement_for_resume,
|
||||
GC::create_function(traversable->heap(), [signal](HistoryStepResult) {
|
||||
signal->resolve({});
|
||||
}));
|
||||
}));
|
||||
}
|
||||
|
||||
// 7. If navigationType is "push" or "replace", then run the URL and history update steps given document and
|
||||
// event's destination's URL, with serializedData set to event's classic history API state and historyHandling
|
||||
// set to navigationType.
|
||||
|
|
|
|||
|
|
@ -1235,9 +1235,9 @@ public:
|
|||
// 2. Let targetEntry be the result of getting the target history entry given traversable and targetStep.
|
||||
m_target_entry = m_traversable->get_the_target_history_entry(target_step.value());
|
||||
|
||||
// 3. If targetEntry is not traversable's current session history entry, and targetEntry's document state's origin is not the same as
|
||||
// traversable's current session history entry's document state's origin, then:
|
||||
if (m_target_entry != m_traversable->current_session_history_entry() && m_target_entry->document_state()->origin() != m_traversable->current_session_history_entry()->document_state()->origin()) {
|
||||
// 3. If targetEntry is not traversable's current session history entry, and targetEntry's document state's origin is the same as
|
||||
// traversable's current session history entry's document state's origin:
|
||||
if (m_target_entry != m_traversable->current_session_history_entry() && m_target_entry->document_state()->origin() == m_traversable->current_session_history_entry()->document_state()->origin()) {
|
||||
|
||||
// 1. Let eventsFired be false.
|
||||
|
||||
|
|
@ -1620,6 +1620,18 @@ void TraversableNavigable::apply_the_traverse_history_step(int step, GC::Ptr<Sou
|
|||
apply_the_history_step(step, true, source_snapshot_params, initiator_to_check, user_involvement, Bindings::NavigationType::Traverse, SynchronousNavigation::No, nullptr, on_complete);
|
||||
}
|
||||
|
||||
// https://html.spec.whatwg.org/multipage/browsing-the-web.html#resume-applying-the-traverse-history-step
|
||||
void TraversableNavigable::resume_applying_the_traverse_history_step(int step, UserNavigationInvolvement user_involvement, GC::Ref<GC::Function<void(HistoryStepResult)>> on_complete)
|
||||
{
|
||||
// To resume applying the traverse history step given a non-negative integer step, a traversable
|
||||
// navigable traversable, and user navigation involvement userInvolvement, apply step to
|
||||
// traversable given false, null, null, userInvolvement, and "traverse".
|
||||
// NOTE: When resuming a traverse, we are already past the cancelation, initiator, and
|
||||
// source snapshot checks, and this traversal has already been determined to be a
|
||||
// same-document traversal. Hence, we can pass false and null for those arguments.
|
||||
apply_the_history_step(step, false, {}, {}, user_involvement, Bindings::NavigationType::Traverse, SynchronousNavigation::No, nullptr, on_complete);
|
||||
}
|
||||
|
||||
// https://html.spec.whatwg.org/multipage/document-sequences.html#close-a-top-level-traversable
|
||||
void TraversableNavigable::close_top_level_traversable()
|
||||
{
|
||||
|
|
|
|||
|
|
@ -60,6 +60,7 @@ public:
|
|||
HistoryObjectLengthAndIndex get_the_history_object_length_and_index(int) const;
|
||||
|
||||
void apply_the_traverse_history_step(int, GC::Ptr<SourceSnapshotParams>, GC::Ptr<Navigable>, UserNavigationInvolvement, GC::Ref<GC::Function<void(HistoryStepResult)>> on_complete);
|
||||
void resume_applying_the_traverse_history_step(int, UserNavigationInvolvement, GC::Ref<GC::Function<void(HistoryStepResult)>> on_complete);
|
||||
void apply_the_reload_history_step(UserNavigationInvolvement, GC::Ref<GC::Function<void(HistoryStepResult)>> on_complete);
|
||||
enum class SynchronousNavigation : bool {
|
||||
Yes,
|
||||
|
|
|
|||
|
|
@ -18,6 +18,8 @@ Text/input/css/font-face-load-dedups-same-url.html
|
|||
|
||||
; pushState with path URL requires HTTP(s) scheme.
|
||||
Text/input/navigation/history-replace-push-then-back.html
|
||||
Text/input/navigation/intercepted-traverse-updates-history-entry.html
|
||||
Text/input/navigation/aborted-intercepted-traverse-does-not-resume.html
|
||||
|
||||
; CSS @font-face url() requires HTTP(s) scheme.
|
||||
Text/input/selection-rect-consistency-with-kerning.html
|
||||
|
|
|
|||
|
|
@ -0,0 +1,9 @@
|
|||
navigate type=traverse destination=/r/programming current=/r/programming/comments/post location=/r/programming/comments/post
|
||||
navigateerror current=/r/programming/comments/post location=/r/programming/comments/post
|
||||
navigate type=push destination=/r/programming/comments/post#other current=/r/programming/comments/post location=/r/programming/comments/post
|
||||
currententrychange type=push current=/r/programming/comments/post#other location=/r/programming/comments/post#other
|
||||
navigatesuccess current=/r/programming/comments/post#other location=/r/programming/comments/post#other
|
||||
navigate type=traverse destination=/r/programming/comments/post current=/r/programming/comments/post#other location=/r/programming/comments/post#other
|
||||
currententrychange type=traverse current=/r/programming/comments/post location=/r/programming/comments/post
|
||||
navigatesuccess current=/r/programming/comments/post location=/r/programming/comments/post
|
||||
final current=/r/programming/comments/post location=/r/programming/comments/post
|
||||
|
|
@ -0,0 +1,4 @@
|
|||
navigate destination=/r/programming current=/r/programming/comments/post location=/r/programming/comments/post
|
||||
handler current=/r/programming/comments/post location=/r/programming/comments/post
|
||||
navigatesuccess current=/r/programming/comments/post location=/r/programming/comments/post
|
||||
currententrychange current=/r/programming location=/r/programming
|
||||
|
|
@ -0,0 +1,105 @@
|
|||
<!DOCTYPE html>
|
||||
<script src="../include.js"></script>
|
||||
<script>
|
||||
asyncTest(done => {
|
||||
let sawTraverseNavigate = false;
|
||||
let sawVerificationCurrentEntryChange = false;
|
||||
let sawVerificationNavigateSuccess = false;
|
||||
let traverseNavigateCount = 0;
|
||||
let startedVerificationBack = false;
|
||||
let finished = false;
|
||||
|
||||
function pathAndHashFromURL(url) {
|
||||
const parsed = new URL(url);
|
||||
return `${parsed.pathname}${parsed.hash}`;
|
||||
}
|
||||
|
||||
function locationPathAndHash() {
|
||||
return `${location.pathname}${location.hash}`;
|
||||
}
|
||||
|
||||
function maybeDone() {
|
||||
if (finished)
|
||||
return;
|
||||
if (!sawTraverseNavigate || !sawVerificationCurrentEntryChange || !sawVerificationNavigateSuccess)
|
||||
return;
|
||||
if (locationPathAndHash() !== "/r/programming/comments/post" || pathAndHashFromURL(navigation.currentEntry.url) !== "/r/programming/comments/post")
|
||||
return;
|
||||
|
||||
finished = true;
|
||||
println(`final current=${pathAndHashFromURL(navigation.currentEntry.url)} location=${locationPathAndHash()}`);
|
||||
done();
|
||||
}
|
||||
|
||||
navigation.addEventListener("navigate", event => {
|
||||
if (!sawTraverseNavigate && event.navigationType !== "traverse")
|
||||
return;
|
||||
|
||||
println(`navigate type=${event.navigationType} destination=${pathAndHashFromURL(event.destination.url)} current=${pathAndHashFromURL(navigation.currentEntry.url)} location=${locationPathAndHash()}`);
|
||||
|
||||
if (event.navigationType !== "traverse")
|
||||
return;
|
||||
|
||||
++traverseNavigateCount;
|
||||
if (traverseNavigateCount !== 1)
|
||||
return;
|
||||
|
||||
sawTraverseNavigate = true;
|
||||
event.intercept({
|
||||
handler() {
|
||||
println(`unexpected traverse handler current=${pathAndHashFromURL(navigation.currentEntry.url)} location=${locationPathAndHash()}`);
|
||||
},
|
||||
});
|
||||
|
||||
Promise.resolve().then(() => {
|
||||
navigation.navigate("#other", { history: "push" });
|
||||
});
|
||||
});
|
||||
|
||||
navigation.addEventListener("currententrychange", event => {
|
||||
if (!sawTraverseNavigate)
|
||||
return;
|
||||
|
||||
println(`currententrychange type=${event.navigationType} current=${pathAndHashFromURL(navigation.currentEntry.url)} location=${locationPathAndHash()}`);
|
||||
|
||||
if (event.navigationType === "traverse" && traverseNavigateCount < 2) {
|
||||
done();
|
||||
return;
|
||||
}
|
||||
|
||||
if (event.navigationType === "traverse")
|
||||
sawVerificationCurrentEntryChange = true;
|
||||
|
||||
maybeDone();
|
||||
});
|
||||
|
||||
navigation.addEventListener("navigatesuccess", () => {
|
||||
if (!sawTraverseNavigate)
|
||||
return;
|
||||
|
||||
println(`navigatesuccess current=${pathAndHashFromURL(navigation.currentEntry.url)} location=${locationPathAndHash()}`);
|
||||
|
||||
if (!startedVerificationBack && locationPathAndHash() === "/r/programming/comments/post#other") {
|
||||
startedVerificationBack = true;
|
||||
history.back();
|
||||
return;
|
||||
}
|
||||
|
||||
if (startedVerificationBack && locationPathAndHash() === "/r/programming/comments/post")
|
||||
sawVerificationNavigateSuccess = true;
|
||||
|
||||
maybeDone();
|
||||
});
|
||||
|
||||
navigation.addEventListener("navigateerror", () => {
|
||||
if (!sawTraverseNavigate)
|
||||
return;
|
||||
|
||||
println(`navigateerror current=${pathAndHashFromURL(navigation.currentEntry.url)} location=${locationPathAndHash()}`);
|
||||
});
|
||||
|
||||
history.replaceState({}, "", "/r/programming");
|
||||
history.pushState({}, "", "/r/programming/comments/post");
|
||||
history.back();
|
||||
});
|
||||
</script>
|
||||
|
|
@ -0,0 +1,65 @@
|
|||
<!DOCTYPE html>
|
||||
<script src="../include.js"></script>
|
||||
<script>
|
||||
asyncTest(done => {
|
||||
let sawTraverseNavigate = false;
|
||||
let sawTraverseHandler = false;
|
||||
let sawTraverseCurrentEntryChange = false;
|
||||
let sawNavigateSuccess = false;
|
||||
let finished = false;
|
||||
|
||||
function pathFromURL(url) {
|
||||
return new URL(url).pathname;
|
||||
}
|
||||
|
||||
function maybeDone() {
|
||||
if (finished)
|
||||
return;
|
||||
if (!sawTraverseNavigate || !sawTraverseHandler || !sawTraverseCurrentEntryChange || !sawNavigateSuccess)
|
||||
return;
|
||||
if (location.pathname !== "/r/programming" || pathFromURL(navigation.currentEntry.url) !== "/r/programming")
|
||||
return;
|
||||
|
||||
finished = true;
|
||||
done();
|
||||
}
|
||||
|
||||
navigation.addEventListener("navigate", event => {
|
||||
if (event.navigationType !== "traverse")
|
||||
return;
|
||||
|
||||
sawTraverseNavigate = true;
|
||||
println(`navigate destination=${pathFromURL(event.destination.url)} current=${pathFromURL(navigation.currentEntry.url)} location=${location.pathname}`);
|
||||
|
||||
event.intercept({
|
||||
handler() {
|
||||
sawTraverseHandler = true;
|
||||
println(`handler current=${pathFromURL(navigation.currentEntry.url)} location=${location.pathname}`);
|
||||
maybeDone();
|
||||
},
|
||||
});
|
||||
});
|
||||
|
||||
navigation.addEventListener("currententrychange", event => {
|
||||
if (event.navigationType !== "traverse")
|
||||
return;
|
||||
|
||||
sawTraverseCurrentEntryChange = true;
|
||||
println(`currententrychange current=${pathFromURL(navigation.currentEntry.url)} location=${location.pathname}`);
|
||||
maybeDone();
|
||||
});
|
||||
|
||||
navigation.addEventListener("navigatesuccess", () => {
|
||||
if (!sawTraverseNavigate || !sawTraverseHandler)
|
||||
return;
|
||||
|
||||
sawNavigateSuccess = true;
|
||||
println(`navigatesuccess current=${pathFromURL(navigation.currentEntry.url)} location=${location.pathname}`);
|
||||
maybeDone();
|
||||
});
|
||||
|
||||
history.replaceState({}, "", "/r/programming");
|
||||
history.pushState({}, "", "/r/programming/comments/post");
|
||||
history.back();
|
||||
});
|
||||
</script>
|
||||
Loading…
Reference in a new issue