LibWeb: Use reject_the_finished_promise() in abort_a_navigate_event()

This aligns our implementation with the specification. Doing this
fixes a number of WPT tests because this sets
`m_ongoing_api_method_tracker` to null, avoiding an assertion that
previously caused a crash.
This commit is contained in:
Tim Ledbetter
2026-02-10 20:04:10 +00:00
committed by Shannon Booth
parent 206a18acd4
commit dc649a7e46
Notes: github-actions[bot] 2026-02-14 19:23:21 +00:00
21 changed files with 413 additions and 2 deletions

View File

@@ -0,0 +1,23 @@
<!doctype html>
<script src="../../resources/testharness.js"></script>
<script src="../../resources/testharnessreport.js"></script>
<div id="d"></div>
<script>
promise_test(async () => {
let onnavigate_called = false;
navigation.onnavigate = () => onnavigate_called = true;
await navigation.navigate("#d").committed;
assert_equals(location.hash, "#d");
assert_true(onnavigate_called);
assert_equals(document.querySelector(":target"), d);
}, "navigate() navigates same-document and fires onnavigate (async)");
test(() => {
let onnavigate_called = false;
navigation.onnavigate = () => onnavigate_called = true;
navigation.navigate("#d");
assert_equals(location.hash, "#d");
assert_true(onnavigate_called);
assert_equals(document.querySelector(":target"), d);
}, "navigate() navigates same-document and fires onnavigate (sync)");
</script>

View File

@@ -0,0 +1,25 @@
<!doctype html>
<script src="../../../resources/testharness.js"></script>
<script src="../../../resources/testharnessreport.js"></script>
<script src="resources/helpers.js"></script>
<script>
promise_test(async t => {
let start_length = navigation.entries().length;
let start_index = navigation.currentEntry.index;
// Wait for after the load event so that the navigation doesn't get converted
// into a replace navigation.
await new Promise(resolve => window.onload = () => t.step_timeout(resolve, 0));
navigation.addEventListener("navigate", e => e.intercept());
const result1 = navigation.navigate("#1");
const result2 = navigation.navigate("#2");
assert_equals(navigation.entries().length, start_length + 2);
assert_array_equals(navigation.entries().slice(start_index).map(e => (new URL(e.url)).hash), ["", "#1", "#2"]);
await assertCommittedFulfillsFinishedRejectsDOM(t, result1, navigation.entries()[start_index + 1], "AbortError");
await assertBothFulfill(t, result2, navigation.currentEntry);
}, "interrupted navigate() promises with intercept()");
</script>

View File

@@ -0,0 +1,30 @@
<!doctype html>
<script src="../../../resources/testharness.js"></script>
<script src="../../../resources/testharnessreport.js"></script>
<script src="resources/helpers.js"></script>
<script>
promise_test(async t => {
let start_length = navigation.entries().length;
let start_index = navigation.currentEntry.index;
// Wait for after the load event so that the navigation doesn't get converted
// into a replace navigation.
await new Promise(resolve => window.onload = () => t.step_timeout(resolve, 0));
let result2;
navigation.onnavigate = t.step_func(e => {
if (e.info == 1) {
result2 = navigation.navigate("#2", { info: 2 });
assert_true(e.defaultPrevented);
}
});
const result1 = navigation.navigate("#1", { info: 1 });
assert_equals(navigation.entries().length, start_length + 1);
assert_array_equals(navigation.entries().slice(start_index).map(e => (new URL(e.url)).hash), ["", "#2"]);
await assertBothRejectDOM(t, result1, "AbortError");
await assertBothFulfill(t, result2, navigation.currentEntry);
}, "if navigate() is called inside onnavigate, the previous navigation and navigate event are cancelled");
</script>

View File

@@ -0,0 +1,23 @@
<!doctype html>
<script src="../../../resources/testharness.js"></script>
<script src="../../../resources/testharnessreport.js"></script>
<script src="resources/helpers.js"></script>
<script>
promise_test(async t => {
let start_length = navigation.entries().length;
let start_index = navigation.currentEntry.index;
// Wait for after the load event so that the navigation doesn't get converted
// into a replace navigation.
await new Promise(resolve => window.onload = () => t.step_timeout(resolve, 0));
const result1 = navigation.navigate("#1");
const result2 = navigation.navigate("#2");
assert_equals(navigation.entries().length, start_length + 2);
assert_array_equals(navigation.entries().slice(start_index).map(e => (new URL(e.url)).hash), ["", "#1", "#2"]);
await assertCommittedFulfillsFinishedRejectsDOM(t, result1, navigation.entries()[start_index + 1], "AbortError");
await assertBothFulfill(t, result2, navigation.currentEntry);
}, "interrupted navigate() promises");
</script>

View File

@@ -0,0 +1,154 @@
window.assertReturnValue = (result, w = window) => {
assert_equals(Object.getPrototypeOf(result), w.Object.prototype, "result object must be from the right realm");
assert_array_equals(Reflect.ownKeys(result), ["committed", "finished"]);
assert_true(result.committed instanceof w.Promise);
assert_true(result.finished instanceof w.Promise);
assert_not_equals(result.committed, result.finished);
};
window.assertNeverSettles = (t, result, w = window) => {
assertReturnValue(result, w);
result.committed.then(
t.unreached_func("committed must not fulfill"),
t.unreached_func("committed must not reject")
);
result.finished.then(
t.unreached_func("finished must not fulfill"),
t.unreached_func("finished must not reject")
);
};
window.assertBothFulfillEntryNotAvailable = async (t, result, w = window) => {
assertReturnValue(result, w);
// Don't use await here so that we can catch out-of-order settlements.
let committedValue;
result.committed.then(
t.step_func(v => { committedValue = v;}),
t.unreached_func("committed must not reject")
);
const finishedValue = await result.finished;
assert_not_equals(committedValue, undefined, "committed must fulfill before finished");
assert_equals(finishedValue, committedValue, "committed and finished must fulfill with the same value");
assert_true(finishedValue instanceof w.NavigationHistoryEntry, "fulfillment value must be a NavigationHistoryEntry");
};
window.assertBothFulfill = async (t, result, expected, w = window) => {
assertReturnValue(result, w);
// Don't use await here so that we can catch out-of-order settlements.
let committedValue;
result.committed.then(
t.step_func(v => { committedValue = v; }),
t.unreached_func("committed must not reject")
);
const finishedValue = await result.finished;
assert_not_equals(committedValue, undefined, "committed must fulfill before finished");
assert_equals(finishedValue, committedValue, "committed and finished must fulfill with the same value");
assert_true(finishedValue instanceof w.NavigationHistoryEntry, "fulfillment value must be a NavigationHistoryEntry");
assert_equals(finishedValue, expected);
};
window.assertCommittedFulfillsFinishedRejectsExactly = async (t, result, expectedEntry, expectedRejection, w = window) => {
assertReturnValue(result, w);
// Don't use await here so that we can catch out-of-order settlements.
let committedValue;
result.committed.then(
t.step_func(v => { committedValue = v; }),
t.unreached_func("committed must not reject")
);
await promise_rejects_exactly(t, expectedRejection, result.finished);
assert_not_equals(committedValue, undefined, "committed must fulfill before finished rejects");
assert_true(committedValue instanceof w.NavigationHistoryEntry, "fulfillment value must be a NavigationHistoryEntry");
assert_equals(committedValue, expectedEntry);
};
window.assertCommittedFulfillsFinishedRejectsDOM = async (t, result, expectedEntry, expectedDOMExceptionCode, w = window, domExceptionConstructor = w.DOMException, navigationHistoryEntryConstuctor = w.NavigationHistoryEntry) => {
assertReturnValue(result, w);
let committedValue;
result.committed.then(
t.step_func(v => { committedValue = v; }),
t.unreached_func("committed must not reject")
);
await promise_rejects_dom(t, expectedDOMExceptionCode, domExceptionConstructor, result.finished);
assert_not_equals(committedValue, undefined, "committed must fulfill before finished rejects");
assert_true(committedValue instanceof navigationHistoryEntryConstuctor, "fulfillment value must be an NavigationHistoryEntry");
assert_equals(committedValue, expectedEntry);
};
// We cannot use Promise.all() because the automatic coercion behavior when
// promises from multiple realms are involved causes it to hang if one of the
// promises is from a detached iframe's realm. See discussion at
// https://github.com/whatwg/html/issues/11252#issuecomment-2984143855.
window.waitForAllLenient = (iterable) => {
const { promise: all, resolve, reject } = Promise.withResolvers();
let remaining = 0;
let results = [];
for (const promise of iterable) {
let index = remaining++;
promise.then(v => {
results[index] = v;
--remaining;
if (!remaining) {
resolve(results);
}
return v;
}, v => reject(v));
}
if (!remaining) {
resolve(results);
}
return all;
}
window.assertBothRejectExactly = async (t, result, expectedRejection, w = window) => {
assertReturnValue(result, w);
let committedReason, finishedReason;
await waitForAllLenient([
result.committed.then(
t.unreached_func("committed must not fulfill"),
t.step_func(r => { committedReason = r; })
),
result.finished.then(
t.unreached_func("finished must not fulfill"),
t.step_func(r => { finishedReason = r; })
)
]);
assert_equals(committedReason, finishedReason, "committed and finished must reject with the same value");
assert_equals(expectedRejection, committedReason);
};
window.assertBothRejectDOM = async (t, result, expectedDOMExceptionCode, w = window, domExceptionConstructor = w.DOMException) => {
assertReturnValue(result, w);
// Don't use await here so that we can catch out-of-order settlements.
let committedReason, finishedReason;
await waitForAllLenient([
result.committed.then(
t.unreached_func("committed must not fulfill"),
t.step_func(r => { committedReason = r; })
),
result.finished.then(
t.unreached_func("finished must not fulfill"),
t.step_func(r => { finishedReason = r; })
)
]);
assert_equals(committedReason, finishedReason, "committed and finished must reject with the same value");
assert_throws_dom(expectedDOMExceptionCode, domExceptionConstructor, () => { throw committedReason; });
};