Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
10 changes: 8 additions & 2 deletions Libraries/LibWeb/DOM/Document.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -5781,10 +5781,16 @@ void Document::unload(GC::Ptr<Document>)
// 2. Fire a page transition event named pagehide at oldDocument's relevant global object with oldDocument's
// salvageable state.
HTML::relevant_window(*this).fire_a_page_transition_event(HTML::EventNames::pagehide, m_salvageable);
}

// 3. Update the visibility state of oldDocument to "hidden".
// 3. Update the visibility state of oldDocument to "hidden".
// AD-HOC: Violate the spec requirement for the page to be showing (in the spec, this step is a substep of step 10
// above — not a sibling step) and instead do this for any page ready for post-load tasks; e.g., the initial
// about:blank of a never-navigated child. Gecko/WebKit/Blink consider such pages to be completely loaded —
// and update their visibility state when unloading them.
// See https://github.com/whatwg/html/issues/12288
if (m_ready_for_post_load_tasks)
update_the_visibility_state(HTML::VisibilityState::Hidden);
}

// FIXME: 11. If unloadTimingInfo is not null, then set unloadTimingInfo's unload event start time to the current high
// resolution time given newDocument's relevant global object, coarsened given oldDocument's relevant settings
Expand Down
13 changes: 10 additions & 3 deletions Libraries/LibWeb/HTML/NavigableContainer.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -311,7 +311,7 @@ void NavigableContainer::destroy_the_child_navigable()

// Not in the spec:
// Setting container's content navigable makes document *not* be "fully active".
// Therefore, it is moved to run in afterAllDestruction callback of "destroy a document and its descendants"
// Therefore, it is moved to run in the after-all-unloads callback of "unload a document and its descendants"
// when all queued tasks are done.
// "Has been destroyed" flag is used instead to check whether navigable is already destroyed.
auto& local_navigable = as<LocalNavigable>(*navigable);
Expand Down Expand Up @@ -365,14 +365,21 @@ void NavigableContainer::destroy_the_child_navigable()
});

// 5. Destroy a document and its descendants given navigable's active document.
// AD-HOC: We unload the document and its descendants, instead of just destroying. Unloading fires pagehide at the
// document's relevant global object, updates its visibility state to "hidden" (firing visibilitychange),
// and fires unload — before destroying the document. The spec as written would leave a removed container's
// content documents reporting a "visible" visibility state — with no events fired; Gecko/WebKit/Blink all
// fire those events, and report such documents as hidden. This also means starting a view transition in a
// removed document skips the transition — since startViewTransition() skips transitions for hidden docs.
// See https://github.com/whatwg/html/issues/12288
// AD-HOC: The spec assumes the active document is non-null here, but during an ancestor
// unload the child documents are unloaded (and destroyed) before the ancestor's
// pagehide fires. If that pagehide handler then removes a subtree containing this
// container, we reach step 5 with navigable's active document already null. We
// treat the destroy step as a no-op in that case and proceed with the remaining
// treat the unload step as a no-op in that case and proceed with the remaining
// post-destruction cleanup.
if (auto active_document = local_navigable.active_document())
active_document->destroy_a_document_and_its_descendants(after_document_destruction);
active_document->unload_a_document_and_its_descendants({}, after_document_destruction);
else
after_document_destruction->function()();
}
Expand Down
1 change: 1 addition & 0 deletions Tests/LibWeb/TestConfig.ini
Original file line number Diff line number Diff line change
Expand Up @@ -272,6 +272,7 @@ Text/input/wpt-import/html/webappapis/dynamic-markup-insertion/opening-the-input
Text/input/wpt-import/html/webappapis/scripting/processing-model-2/integration-with-the-javascript-job-queue/promise-job-entry-different-function-realm.html
Text/input/wpt-import/navigation-api/navigation-methods/reload-state-and-info.html
Text/input/wpt-import/navigation-api/navigation-methods/reload-state-undefined.html
Text/input/wpt-import/page-visibility/iframe-unload.html
Text/input/wpt-import/page-visibility/test_child_document.html
Text/input/wpt-import/html/canvas/offscreen/text/2d.text.font.default.worker.html
Text/input/wpt-import/html/canvas/offscreen/text/2d.text.font.parse.basic.worker.html
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
before removal: visible
pagehide fired: persisted=false
visibilitychange fired: hidden
unload fired
after removal: hidden
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
after removal: hidden
ready rejected: InvalidStateError
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
Harness status: OK

Found 1 tests

1 Pass
Pass visibilitychange fires on unload with iframes
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
<!DOCTYPE html>
<script src="../include.js"></script>
<script>
// Removing an iframe must unload its content document: pagehide fires at the content window, the content document's
// visibility state becomes "hidden" (firing visibilitychange), and "unload" fires. See #9893.
asyncTest(async (done) => {
const iframe = document.createElement("iframe");
iframe.srcdoc = "<p>child</p>";
await new Promise(resolve => {
iframe.onload = resolve;
document.body.appendChild(iframe);
});

const childDocument = iframe.contentDocument;
const childWindow = iframe.contentWindow;
println(`before removal: ${childDocument.visibilityState}`);

childWindow.addEventListener("pagehide", event => {
println(`pagehide fired: persisted=${event.persisted}`);
});
childDocument.addEventListener("visibilitychange", () => {
println(`visibilitychange fired: ${childDocument.visibilityState}`);
});
childWindow.addEventListener("unload", () => {
println("unload fired");
setTimeout(() => {
println(`after removal: ${childDocument.visibilityState}`);
done();
}, 0);
});

iframe.remove();
});
</script>
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
<!DOCTYPE html>
<script src="../include.js"></script>
<script>
// A view transition started in the document of a removed iframe must be skipped with an InvalidStateError — because
// the document's visibility state is "hidden" after removal. See #9893.
asyncTest(async (done) => {
const iframe = document.createElement("iframe");
iframe.srcdoc = "<p>child</p>";
await new Promise(r => { iframe.onload = r; document.body.appendChild(iframe); });
const childDocument = iframe.contentDocument;
const hidden = new Promise(resolve => childDocument.addEventListener("visibilitychange", resolve, { once: true }));
iframe.remove();
await hidden;
println(`after removal: ${childDocument.visibilityState}`);
const transition = childDocument.startViewTransition(() => {});
Comment thread
coderabbitai[bot] marked this conversation as resolved.
const result = await Promise.race([
transition.ready.then(() => "ready resolved", (e) => `ready rejected: ${e.name}`),
new Promise(r => setTimeout(() => r("ready never settled (hang)"), 1500)),
]);
println(result);
done();
});
</script>
Original file line number Diff line number Diff line change
@@ -0,0 +1,49 @@
<html>
<title>visibilitychange fires on unload with iframes</title>
<script src="../resources/testharness.js"></script>
<script src="../resources/testharnessreport.js"></script>
<div id="log"></div>
<script>

var frameDocs = [];
var docsLoaded = 0;
var numFrames = 3;

var ast = new async_test("visibilitychange fires on unload with iframes");

function startTest() {
if (++docsLoaded < numFrames)
return;

ast.step(function () {
frameDocs.push(window[0].document);
frameDocs.push(window[0][0].document);
frameDocs.push(window[0][1].document);

for (var i = 0; i < frameDocs.length; ++i) {
frameDocs[i].addEventListener(
"visibilitychange",
onVisibilityChange.bind(null, i), false);
}

document.body.removeChild(document.getElementById("frame1"));
});
}

var checkedFrames = 0;

function onVisibilityChange(i) {
ast.step(function () {
assert_equals(frameDocs[i].visibilityState, "hidden");
});
if (++checkedFrames >= numFrames) {
ast.done();
}
}



</script>
<iframe id="frame1" src="resources/iframe-with-subframes.html"></iframe>
</body>
</html>
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
<html>
<body onload="parent.startTest()">
<iframe id="subIframe1" onload="parent.parent.startTest()"></iframe>
<iframe id="subIframe2" onload="parent.parent.startTest()"></iframe>
</body>
</html>
Loading