visibilitychange:hidden doesn't fire during page navigations
https://bugs.webkit.org/show_bug.cgi?id=151234
<rdar://problem/23688763>

Reviewed by Ryosuke Niwa.

LayoutTests/imported/w3c:

Import page-visibility WPT tests from upstream.

* resources/import-expectations.json:
* web-platform-tests/html/browsers/the-window-object/apis-for-creating-and-navigating-browsing-contexts-by-name/no_window_open_when_term_nesting_level_nonzero.window-expected.txt:
* web-platform-tests/page-visibility/*: Added.

Source/WebCore:

Fire a visibilitychange during document unload, as per the specification:
- https://www.w3.org/TR/page-visibility/#reacting-to-visibilitychange-changes
- https://html.spec.whatwg.org/multipage/browsing-the-web.html#unloading-document-visibility-change-steps

Note that the specification currently says to fire the visibilitychange event before the pagehide event.
However, Both Chrome and Firefox fire the pagehide event then the visibilitychange event. This change
aligns our behavior with both Chrome and Firefox. The following bug has been filed against the
specification:
- https://github.com/w3c/page-visibility/issues/67

We also fire a visibilitychange event when coming out of the back/forward cache. This makes sense given
that we fire one when the document enters the back/forward cache. This is also Firefox's behavior.
I have verified that the new fast/history/back-forward-cache-visibility-state.html layout test is passing
in Firefox.

Tests: fast/history/back-forward-cache-visibility-state.html
       imported/w3c/web-platform-tests/page-visibility/idlharness.window.html
       imported/w3c/web-platform-tests/page-visibility/iframe-unload.html
       imported/w3c/web-platform-tests/page-visibility/onvisibilitychange.html
       imported/w3c/web-platform-tests/page-visibility/test_attributes_exist.html
       imported/w3c/web-platform-tests/page-visibility/test_child_document.html
       imported/w3c/web-platform-tests/page-visibility/test_default_view.html
       imported/w3c/web-platform-tests/page-visibility/test_read_only.html
       imported/w3c/web-platform-tests/page-visibility/unload-bubbles.html
       imported/w3c/web-platform-tests/page-visibility/unload.html

* dom/Document.cpp:
(WebCore::Document::visibilityState const):
(WebCore::Document::setHiddenDueToDismissal):
* dom/Document.h:
* history/CachedPage.cpp:
(WebCore::firePageShowAndPopStateEvents):
* loader/FrameLoader.cpp:
(WebCore::FrameLoader::dispatchUnloadEvents):

LayoutTests:

Add test coverage for the visibilitychange event and document.visibilitystate when entering
and coming out of the back/forward cache.

* fast/history/back-forward-cache-visibility-state-expected.txt: Added.
* fast/history/back-forward-cache-visibility-state.html: Added.


git-svn-id: http://svn.webkit.org/repository/webkit/trunk@267614 268f45cc-cd09-0410-ab3c-d52691b4dbfc
diff --git a/LayoutTests/ChangeLog b/LayoutTests/ChangeLog
index a5799bc..755b3ef 100644
--- a/LayoutTests/ChangeLog
+++ b/LayoutTests/ChangeLog
@@ -1,3 +1,17 @@
+2020-09-25  Chris Dumez  <cdumez@apple.com>
+
+        visibilitychange:hidden doesn't fire during page navigations
+        https://bugs.webkit.org/show_bug.cgi?id=151234
+        <rdar://problem/23688763>
+
+        Reviewed by Ryosuke Niwa.
+
+        Add test coverage for the visibilitychange event and document.visibilitystate when entering
+        and coming out of the back/forward cache.
+
+        * fast/history/back-forward-cache-visibility-state-expected.txt: Added.
+        * fast/history/back-forward-cache-visibility-state.html: Added.
+
 2020-09-25  James Darpinian  <jdarpinian@chromium.org>
 
         Support OES_fbo_render_mipmap
diff --git a/LayoutTests/fast/history/back-forward-cache-visibility-state-expected.txt b/LayoutTests/fast/history/back-forward-cache-visibility-state-expected.txt
new file mode 100644
index 0000000..9997b96
--- /dev/null
+++ b/LayoutTests/fast/history/back-forward-cache-visibility-state-expected.txt
@@ -0,0 +1,22 @@
+Tests document.visibilityState and the visibilitychange event when entering the backforward cache.
+
+On success, you will see a series of "PASS" messages, followed by "TEST COMPLETE".
+
+
+pageshow - not from cache
+PASS document.visibilityState is "visible"
+pagehide - entering cache
+PASS document.visibilityState is "visible"
+PASS visibilityChangeEventCount is 0
+visibilitychange - visibilityState: hidden
+PASS document.visibilityState is "hidden"
+visibilitychange - visibilityState: visible
+PASS document.visibilityState is "visible"
+pageshow - from cache
+PASS document.visibilityState is "visible"
+PASS visibilityChangeEventCount is 2
+PASS Page did enter and was restored from the page cache
+PASS successfullyParsed is true
+
+TEST COMPLETE
+
diff --git a/LayoutTests/fast/history/back-forward-cache-visibility-state.html b/LayoutTests/fast/history/back-forward-cache-visibility-state.html
new file mode 100644
index 0000000..6e0a664
--- /dev/null
+++ b/LayoutTests/fast/history/back-forward-cache-visibility-state.html
@@ -0,0 +1,58 @@
+<!-- webkit-test-runner [ enableBackForwardCache=true ] -->
+<!DOCTYPE html>
+<html>
+<body>
+<script src="../../resources/js-test.js"></script>
+<script>
+description('Tests document.visibilityState and the visibilitychange event when entering the backforward cache.');
+window.jsTestIsAsync = true;
+
+if (window.testRunner)
+    testRunner.clearAllDatabases();
+
+let visibilityChangeEventCount = 0;
+
+window.addEventListener("pageshow", function(event) {
+    debug("pageshow - " + (event.persisted ? "" : "not ") + "from cache");
+    shouldBeEqualToString("document.visibilityState", "visible");
+
+    if (event.persisted)
+        shouldBe("visibilityChangeEventCount", "2");
+    
+    if (event.persisted) {
+        testPassed("Page did enter and was restored from the page cache");
+        finishJSTest();
+    }
+}, false);
+
+window.addEventListener("pagehide", function(event) {
+    debug("pagehide - " + (event.persisted ? "" : "not ") + "entering cache");
+    shouldBeEqualToString("document.visibilityState", "visible");
+    shouldBe("visibilityChangeEventCount", "0");
+    if (!event.persisted) {
+        testFailed("Page did not enter the page cache.");
+        finishJSTest();
+    }
+}, false);
+
+window.addEventListener('load', function() {
+    setTimeout(function() {
+        document.addEventListener("visibilitychange", () => {
+            if (visibilityChangeEventCount > 2)
+                return;
+
+            ++visibilityChangeEventCount;
+            debug("visibilitychange - visibilityState: " + document.visibilityState);
+            if (visibilityChangeEventCount == 1)
+                shouldBeEqualToString("document.visibilityState", "hidden");
+            else
+                shouldBeEqualToString("document.visibilityState", "visible");
+        });
+
+        window.location.href = "resources/page-cache-helper.html";
+    }, 0);
+}, false);
+
+</script>
+</body>
+</html>
diff --git a/LayoutTests/imported/w3c/ChangeLog b/LayoutTests/imported/w3c/ChangeLog
index 53eff39..74a06b4 100644
--- a/LayoutTests/imported/w3c/ChangeLog
+++ b/LayoutTests/imported/w3c/ChangeLog
@@ -1,5 +1,19 @@
 2020-09-25  Chris Dumez  <cdumez@apple.com>
 
+        visibilitychange:hidden doesn't fire during page navigations
+        https://bugs.webkit.org/show_bug.cgi?id=151234
+        <rdar://problem/23688763>
+
+        Reviewed by Ryosuke Niwa.
+
+        Import page-visibility WPT tests from upstream.
+
+        * resources/import-expectations.json:
+        * web-platform-tests/html/browsers/the-window-object/apis-for-creating-and-navigating-browsing-contexts-by-name/no_window_open_when_term_nesting_level_nonzero.window-expected.txt:
+        * web-platform-tests/page-visibility/*: Added.
+
+2020-09-25  Chris Dumez  <cdumez@apple.com>
+
         Unreviewed, reverting r267589.
 
         Broke document.visibilityState when coming out of back/forward
diff --git a/LayoutTests/imported/w3c/resources/import-expectations.json b/LayoutTests/imported/w3c/resources/import-expectations.json
index 4e1722c..6eda6b7 100644
--- a/LayoutTests/imported/w3c/resources/import-expectations.json
+++ b/LayoutTests/imported/w3c/resources/import-expectations.json
@@ -336,7 +336,7 @@
     "web-platform-tests/old-tests": "skip", 
     "web-platform-tests/orientation-event": "skip", 
     "web-platform-tests/orientation-sensor": "skip", 
-    "web-platform-tests/page-visibility": "skip", 
+    "web-platform-tests/page-visibility": "import", 
     "web-platform-tests/paint-timing": "skip", 
     "web-platform-tests/payment-handler": "skip", 
     "web-platform-tests/payment-method-basic-card": "skip", 
diff --git a/LayoutTests/imported/w3c/web-platform-tests/html/browsers/the-window-object/apis-for-creating-and-navigating-browsing-contexts-by-name/no_window_open_when_term_nesting_level_nonzero.window-expected.txt b/LayoutTests/imported/w3c/web-platform-tests/html/browsers/the-window-object/apis-for-creating-and-navigating-browsing-contexts-by-name/no_window_open_when_term_nesting_level_nonzero.window-expected.txt
index 1a56ec0..e8e243b 100644
--- a/LayoutTests/imported/w3c/web-platform-tests/html/browsers/the-window-object/apis-for-creating-and-navigating-browsing-contexts-by-name/no_window_open_when_term_nesting_level_nonzero.window-expected.txt
+++ b/LayoutTests/imported/w3c/web-platform-tests/html/browsers/the-window-object/apis-for-creating-and-navigating-browsing-contexts-by-name/no_window_open_when_term_nesting_level_nonzero.window-expected.txt
@@ -1,4 +1,5 @@
 CONSOLE MESSAGE: Error: assert_equals: expected no popup during pagehide expected null but got object "[object Window]"
+CONSOLE MESSAGE: Error: assert_equals: expected no popup during visibilitychange expected null but got object "[object Window]"
 CONSOLE MESSAGE: Error: assert_equals: expected no popup during unload expected null but got object "[object Window]"
 
 PASS no popups with frame removal 
diff --git a/LayoutTests/imported/w3c/web-platform-tests/page-visibility/META.yml b/LayoutTests/imported/w3c/web-platform-tests/page-visibility/META.yml
new file mode 100644
index 0000000..9b9aea8
--- /dev/null
+++ b/LayoutTests/imported/w3c/web-platform-tests/page-visibility/META.yml
@@ -0,0 +1,4 @@
+spec: https://w3c.github.io/page-visibility/
+suggested_reviewers:
+  - plehegar
+  - igrigorik
diff --git a/LayoutTests/imported/w3c/web-platform-tests/page-visibility/idlharness.window-expected.txt b/LayoutTests/imported/w3c/web-platform-tests/page-visibility/idlharness.window-expected.txt
new file mode 100644
index 0000000..7b8a61f
--- /dev/null
+++ b/LayoutTests/imported/w3c/web-platform-tests/page-visibility/idlharness.window-expected.txt
@@ -0,0 +1,19 @@
+
+PASS idl_test setup 
+PASS idl_test validation 
+PASS Partial interface Document: original interface defined 
+PASS Partial interface Document: member names are unique 
+PASS Partial interface Document[2]: member names are unique 
+PASS Partial interface Document[3]: member names are unique 
+PASS Document includes NonElementParentNode: member names are unique 
+PASS Document includes ParentNode: member names are unique 
+PASS Document includes XPathEvaluatorBase: member names are unique 
+PASS Document includes GlobalEventHandlers: member names are unique 
+PASS Document includes DocumentAndElementEventHandlers: member names are unique 
+PASS Document interface: attribute hidden 
+PASS Document interface: attribute visibilityState 
+PASS Document interface: attribute onvisibilitychange 
+PASS Document interface: document must inherit property "hidden" with the proper type 
+PASS Document interface: document must inherit property "visibilityState" with the proper type 
+PASS Document interface: document must inherit property "onvisibilitychange" with the proper type 
+
diff --git a/LayoutTests/imported/w3c/web-platform-tests/page-visibility/idlharness.window.html b/LayoutTests/imported/w3c/web-platform-tests/page-visibility/idlharness.window.html
new file mode 100644
index 0000000..2382913
--- /dev/null
+++ b/LayoutTests/imported/w3c/web-platform-tests/page-visibility/idlharness.window.html
@@ -0,0 +1 @@
+<!-- This file is required for WebKit test infrastructure to run the templated test -->
\ No newline at end of file
diff --git a/LayoutTests/imported/w3c/web-platform-tests/page-visibility/idlharness.window.js b/LayoutTests/imported/w3c/web-platform-tests/page-visibility/idlharness.window.js
new file mode 100644
index 0000000..7af89b6
--- /dev/null
+++ b/LayoutTests/imported/w3c/web-platform-tests/page-visibility/idlharness.window.js
@@ -0,0 +1,17 @@
+// META: script=/resources/WebIDLParser.js
+// META: script=/resources/idlharness.js
+// META: timeout=long
+
+'use strict';
+
+// https://w3c.github.io/page-visibility/
+
+idl_test(
+  ['page-visibility'],
+  ['dom', 'html'],
+  idl_array => {
+    idl_array.add_objects({
+      Document: ['document'],
+    });
+  }
+);
diff --git a/LayoutTests/imported/w3c/web-platform-tests/page-visibility/iframe-unload-expected.txt b/LayoutTests/imported/w3c/web-platform-tests/page-visibility/iframe-unload-expected.txt
new file mode 100644
index 0000000..9b10f2b
--- /dev/null
+++ b/LayoutTests/imported/w3c/web-platform-tests/page-visibility/iframe-unload-expected.txt
@@ -0,0 +1,3 @@
+
+PASS visibilitychange fires on unload with iframes 
+
diff --git a/LayoutTests/imported/w3c/web-platform-tests/page-visibility/iframe-unload.html b/LayoutTests/imported/w3c/web-platform-tests/page-visibility/iframe-unload.html
new file mode 100644
index 0000000..6d049a8
--- /dev/null
+++ b/LayoutTests/imported/w3c/web-platform-tests/page-visibility/iframe-unload.html
@@ -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>
diff --git a/LayoutTests/imported/w3c/web-platform-tests/page-visibility/onvisibilitychange-expected.txt b/LayoutTests/imported/w3c/web-platform-tests/page-visibility/onvisibilitychange-expected.txt
new file mode 100644
index 0000000..526a38c
--- /dev/null
+++ b/LayoutTests/imported/w3c/web-platform-tests/page-visibility/onvisibilitychange-expected.txt
@@ -0,0 +1,3 @@
+
+PASS onvisibilitychange attribute is a proper event handler 
+
diff --git a/LayoutTests/imported/w3c/web-platform-tests/page-visibility/onvisibilitychange.html b/LayoutTests/imported/w3c/web-platform-tests/page-visibility/onvisibilitychange.html
new file mode 100644
index 0000000..f854084
--- /dev/null
+++ b/LayoutTests/imported/w3c/web-platform-tests/page-visibility/onvisibilitychange.html
@@ -0,0 +1,16 @@
+<!doctype html>
+<title>onvisibilitychange attribute is a proper event handler</title>
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+<div id="log"></div>
+<script>
+var ast = new async_test("onvisibilitychange attribute is a proper event handler");
+function startTest() {
+  var iframe1 = document.getElementById("frame1");
+  iframe1.contentWindow.document.onvisibilitychange = ast.step_func(function() {
+    ast.done();
+  });
+  frame1.parentNode.removeChild(frame1);
+}
+</script>
+<iframe id="frame1" src='resources/iframe.html'></iframe>
diff --git a/LayoutTests/imported/w3c/web-platform-tests/page-visibility/resources/blank_page_green.html b/LayoutTests/imported/w3c/web-platform-tests/page-visibility/resources/blank_page_green.html
new file mode 100644
index 0000000..b8a1947
--- /dev/null
+++ b/LayoutTests/imported/w3c/web-platform-tests/page-visibility/resources/blank_page_green.html
@@ -0,0 +1,10 @@
+<!DOCTYPE HTML>
+<html>
+    <head>
+        <meta content="text/html; charset=utf-8" http-equiv="Content-Type" />
+        <title>Green Test Page</title>
+    </head>
+    <body style="background-color:#00FF00;">
+        <h1>Placeholder</h1>
+    </body>
+</html>
diff --git a/LayoutTests/imported/w3c/web-platform-tests/page-visibility/resources/iframe-with-subframes.html b/LayoutTests/imported/w3c/web-platform-tests/page-visibility/resources/iframe-with-subframes.html
new file mode 100644
index 0000000..febb954
--- /dev/null
+++ b/LayoutTests/imported/w3c/web-platform-tests/page-visibility/resources/iframe-with-subframes.html
@@ -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>
diff --git a/LayoutTests/imported/w3c/web-platform-tests/page-visibility/resources/iframe.html b/LayoutTests/imported/w3c/web-platform-tests/page-visibility/resources/iframe.html
new file mode 100644
index 0000000..e08acb8
--- /dev/null
+++ b/LayoutTests/imported/w3c/web-platform-tests/page-visibility/resources/iframe.html
@@ -0,0 +1,12 @@
+<!doctype html>
+<html>
+<head><title>Document</title></head>
+<body>
+<h1>Document</h1>
+<script>
+onload = function() {
+  parent.startTest();
+}
+</script>
+</body>
+</html>
diff --git a/LayoutTests/imported/w3c/web-platform-tests/page-visibility/resources/pagevistestharness.js b/LayoutTests/imported/w3c/web-platform-tests/page-visibility/resources/pagevistestharness.js
new file mode 100644
index 0000000..bfc4deb
--- /dev/null
+++ b/LayoutTests/imported/w3c/web-platform-tests/page-visibility/resources/pagevistestharness.js
@@ -0,0 +1,121 @@
+/*
+Distributed under both the W3C Test Suite License [1] and the W3C
+3-clause BSD License [2]. To contribute to a W3C Test Suite, see the
+policies and contribution forms [3].
+
+[1] http://www.w3.org/Consortium/Legal/2008/04-testsuite-license
+[2] http://www.w3.org/Consortium/Legal/2008/03-bsd-license
+[3] http://www.w3.org/2004/10/27-testcases
+*/
+
+//
+// Helper Functions for PageVisibility W3C tests
+//
+var VISIBILITY_STATES =
+{
+    HIDDEN: "hidden",
+    VISIBLE: "visible"
+};
+
+var feature_check = false;
+
+//
+// All test() functions in the WebPerf PageVis test suite should use pv_test() instead.
+//
+// pv_test() validates the document.hidden and document.visibilityState attributes
+// exist prior to running tests and immediately shows a failure if they do not.
+//
+
+function pv_test(func, msg, doc)
+{
+    if (!doc)
+    {
+        doc = document;
+    }
+
+    // only run the feature check once, unless func == null, in which case,
+    // this call is intended as a feature check
+    if (!feature_check)
+    {
+        feature_check = true;
+
+        var hiddenVal = doc.hidden;
+        var visStateVal = doc.visibilityState;
+
+        // show a single error that the Page Visibility feature is undefined
+        test(function()
+        {
+            assert_true(hiddenVal !== undefined && hiddenVal != null,
+                        "document.hidden is defined and not null.");},
+                        "document.hidden is defined and not null.");
+
+        test(function()
+        {
+            assert_true(visStateVal !== undefined && hiddenVal != null,
+                        "document.visibilityState is defined and not null.");},
+                        "document.visibilityState is defined and not null.");
+
+    }
+
+    if (func)
+    {
+        test(func, msg);
+    }
+}
+
+
+function test_feature_exists(doc, msg)
+{
+    if (!msg)
+    {
+        msg = "";
+    }
+    var hiddenMsg = "document.hidden is defined" + msg + ".";
+    var stateMsg = "document.visibilityState is defined" + msg + ".";
+    pv_test(function(){assert_true(document.hidden !== undefined, hiddenMsg);}, hiddenMsg, doc);
+    pv_test(function(){assert_true(document.visibilityState !== undefined, stateMsg);}, stateMsg, doc);
+}
+
+//
+// Common helper functions
+//
+
+function test_true(value, msg)
+{
+    pv_test(function() { assert_true(value, msg); }, msg);
+}
+
+function test_equals(value, equals, msg)
+{
+    pv_test(function() { assert_equals(value, equals, msg); }, msg);
+}
+
+//
+// asynchronous test helper functions
+//
+
+function add_async_result(test_obj, pass_state)
+{
+    // add assertion to manual test for the pass state
+    test_obj.step(function() { assert_true(pass_state) });
+
+    // end manual test
+    test_obj.done();
+}
+
+function add_async_result_assert(test_obj, func)
+{
+    // add assertion to manual test for the pass state
+    test_obj.step(func);
+
+    // end manual test
+    test_obj.done();
+}
+
+var open_link;
+function TabSwitch()
+{
+    //var open_link = window.open("http://www.bing.com");
+    open_link = window.open('', '_blank');
+    step_timeout(function() { open_link.close(); }, 2000);
+}
diff --git a/LayoutTests/imported/w3c/web-platform-tests/page-visibility/resources/unload-bubbles.html b/LayoutTests/imported/w3c/web-platform-tests/page-visibility/resources/unload-bubbles.html
new file mode 100644
index 0000000..44f0c0c
--- /dev/null
+++ b/LayoutTests/imported/w3c/web-platform-tests/page-visibility/resources/unload-bubbles.html
@@ -0,0 +1,15 @@
+<!doctype html>
+<html>
+<head><title>Document</title></head>
+<body>
+<h1>Document</h1>
+<script>
+window.addEventListener("load", function() {
+  window.addEventListener("visibilitychange", function() {
+    opener.postMessage(document.visibilityState, "*");
+  });
+  opener.postMessage("close", "*");
+});
+</script>
+</body>
+</html>
diff --git a/LayoutTests/imported/w3c/web-platform-tests/page-visibility/resources/unload.html b/LayoutTests/imported/w3c/web-platform-tests/page-visibility/resources/unload.html
new file mode 100644
index 0000000..b548518
--- /dev/null
+++ b/LayoutTests/imported/w3c/web-platform-tests/page-visibility/resources/unload.html
@@ -0,0 +1,17 @@
+<!doctype html>
+<html>
+<head><title>Document</title></head>
+<body>
+<h1>Document</h1>
+<script>
+onload = function() {
+  document.addEventListener("visibilitychange", onVisibilityChange, false);
+  opener.postMessage("close", "*");
+}
+
+function onVisibilityChange() {
+  opener.postMessage(document.visibilityState, "*");
+}
+</script>
+</body>
+</html>
diff --git a/LayoutTests/imported/w3c/web-platform-tests/page-visibility/resources/w3c-import.log b/LayoutTests/imported/w3c/web-platform-tests/page-visibility/resources/w3c-import.log
new file mode 100644
index 0000000..0f48ed8
--- /dev/null
+++ b/LayoutTests/imported/w3c/web-platform-tests/page-visibility/resources/w3c-import.log
@@ -0,0 +1,22 @@
+The tests in this directory were imported from the W3C repository.
+Do NOT modify these tests directly in WebKit.
+Instead, create a pull request on the WPT github:
+	https://github.com/web-platform-tests/wpt
+
+Then run the Tools/Scripts/import-w3c-tests in WebKit to reimport
+
+Do NOT modify or remove this file.
+
+------------------------------------------------------------------------
+Properties requiring vendor prefixes:
+None
+Property values requiring vendor prefixes:
+None
+------------------------------------------------------------------------
+List of files:
+/LayoutTests/imported/w3c/web-platform-tests/page-visibility/resources/blank_page_green.html
+/LayoutTests/imported/w3c/web-platform-tests/page-visibility/resources/iframe-with-subframes.html
+/LayoutTests/imported/w3c/web-platform-tests/page-visibility/resources/iframe.html
+/LayoutTests/imported/w3c/web-platform-tests/page-visibility/resources/pagevistestharness.js
+/LayoutTests/imported/w3c/web-platform-tests/page-visibility/resources/unload-bubbles.html
+/LayoutTests/imported/w3c/web-platform-tests/page-visibility/resources/unload.html
diff --git a/LayoutTests/imported/w3c/web-platform-tests/page-visibility/test_attributes_exist-expected.txt b/LayoutTests/imported/w3c/web-platform-tests/page-visibility/test_attributes_exist-expected.txt
new file mode 100644
index 0000000..c76d841
--- /dev/null
+++ b/LayoutTests/imported/w3c/web-platform-tests/page-visibility/test_attributes_exist-expected.txt
@@ -0,0 +1,10 @@
+Description
+
+This test validates that all of the attributes associated with the Page Visibility feature exist (but does not validate that their values are correct).
+
+
+PASS document.hidden is defined and not null. 
+PASS document.visibilityState is defined and not null. 
+PASS document.hidden is defined. 
+PASS document.visibilityState is defined. 
+
diff --git a/LayoutTests/imported/w3c/web-platform-tests/page-visibility/test_attributes_exist.html b/LayoutTests/imported/w3c/web-platform-tests/page-visibility/test_attributes_exist.html
new file mode 100644
index 0000000..748161f
--- /dev/null
+++ b/LayoutTests/imported/w3c/web-platform-tests/page-visibility/test_attributes_exist.html
@@ -0,0 +1,22 @@
+<!DOCTYPE html>
+<html>
+    <head>
+        <meta charset="utf-8" />
+        <title>Page Visibility API Definition</title>
+
+        <script type="text/javascript" src="/resources/testharness.js"></script>
+        <script src="/resources/testharnessreport.js"></script>
+        <script type="text/javascript" src="resources/pagevistestharness.js"></script>
+    </head>
+    <body>
+        <h1>Description</h1>
+        <p>This test validates that all of the attributes associated with the Page Visibility feature exist
+           (but does not validate that their values are correct).</p>
+
+        <div id="log"></div>
+
+        <script type="text/javascript" >
+            test_feature_exists();
+        </script>
+    </body>
+</html>
diff --git a/LayoutTests/imported/w3c/web-platform-tests/page-visibility/test_child_document-expected.txt b/LayoutTests/imported/w3c/web-platform-tests/page-visibility/test_child_document-expected.txt
new file mode 100644
index 0000000..90ab0dd
--- /dev/null
+++ b/LayoutTests/imported/w3c/web-platform-tests/page-visibility/test_child_document-expected.txt
@@ -0,0 +1,24 @@
+Description
+
+This test validates that, within child documents, all of the Page Visibility API attributes exist, are read-only, and match the value of the attributes within the parent document.
+
+
+PASS document.hidden is defined and not null. 
+PASS document.visibilityState is defined and not null. 
+PASS Page Visibility API Child Document Test 
+PASS document.hidden is defined for frame with no style attribute. 
+PASS document.visibilityState is defined for frame with no style attribute. 
+PASS document.visibilityState for frame with no style attribute == visible 
+PASS Page Visibility API Child Document Test 1 
+PASS document.hidden is defined for frame with 'display:none' style. 
+PASS document.visibilityState is defined for frame with 'display:none' style. 
+PASS document.visibilityState for frame with 'display:none' style == visible 
+PASS Page Visibility API Child Document Test 2 
+PASS document.hidden is defined for frame with 'visibility:hidden' style. 
+PASS document.visibilityState is defined for frame with 'visibility:hidden' style. 
+PASS document.visibilityState for frame with 'visibility:hidden' style == visible 
+
+IFrame with no style attribute 
+IFrame with "display:none" style
+IFrame with "visibility:hidden" style 
+
diff --git a/LayoutTests/imported/w3c/web-platform-tests/page-visibility/test_child_document.html b/LayoutTests/imported/w3c/web-platform-tests/page-visibility/test_child_document.html
new file mode 100644
index 0000000..77ec8f8
--- /dev/null
+++ b/LayoutTests/imported/w3c/web-platform-tests/page-visibility/test_child_document.html
@@ -0,0 +1,94 @@
+<!DOCTYPE html>
+<html>
+    <head>
+        <meta charset="utf-8" />
+        <title>Page Visibility API Child Document Test</title>
+
+        <script src="/resources/testharness.js"></script>
+        <script src="/resources/testharnessreport.js"></script>
+        <script type="text/javascript" src="resources/pagevistestharness.js"></script>
+
+        <style type="text/css">
+            iframe
+            {
+                width:250px;
+                height:250px;
+                margin-left:5px;
+            }
+
+            div.docs
+            {
+                position:relative;
+                float:left;
+                text-align:center;
+                margin:10px;
+                border:solid 1px black;
+                padding:3px;
+            }
+        </style>
+
+        <script type="text/javascript" >
+            setup({explicit_done: true});
+
+            function onload_test()
+            {
+                pv_test();
+
+                var frames = document.getElementsByTagName("iframe");
+                var doc, doc_name;
+
+                for (var i = 0; i < frames.length; i++)
+                {
+                    doc = frames[i].contentDocument;
+                    doc_name = "IFrame with " + frames[i].id;
+
+                    pv_test(function()
+                    {
+                        test_feature_exists(doc, " for frame with " + frames[i].id);
+                    });
+
+                    test_equals(doc.visibilityState, VISIBILITY_STATES.VISIBLE,
+                                "document.visibilityState for frame with " +
+                                frames[i].id + " == " +
+                                VISIBILITY_STATES.VISIBLE);
+                }
+
+                done();
+            }
+        </script>
+    </head>
+    <body onload="onload_test()">
+        <h1>Description</h1>
+        <p>This test validates that, within child documents, all of the Page Visibility API attributes exist,
+           are read-only, and match the value of the attributes within the parent document.</p>
+
+        <div id="log"></div>
+
+        <br/>
+
+        <div class="docs">
+            IFrame with no style attribute
+            <br/>
+            <iframe id="no style attribute" src="resources/blank_page_green.html">
+                iframes unsupported
+            </iframe>
+        </div>
+
+        <div class="docs">
+            IFrame with "display:none" style<br/>
+            <iframe id="'display:none' style" style="display:none"
+                    src="resources/blank_page_green.html">
+                iframes unsupported
+            </iframe>
+        </div>
+
+        <div class="docs">
+            IFrame with "visibility:hidden" style
+            <br/>
+            <iframe id="'visibility:hidden' style" style="visibility:hidden"
+                    src="resources/blank_page_green.html">
+                iframes unsupported
+            </iframe>
+        </div>
+    </body>
+</html>
diff --git a/LayoutTests/imported/w3c/web-platform-tests/page-visibility/test_default_view-expected.txt b/LayoutTests/imported/w3c/web-platform-tests/page-visibility/test_default_view-expected.txt
new file mode 100644
index 0000000..459e12a
--- /dev/null
+++ b/LayoutTests/imported/w3c/web-platform-tests/page-visibility/test_default_view-expected.txt
@@ -0,0 +1,13 @@
+Description
+
+This test validates that document.hidden == false and document.visibilityState == "visible" for windowless subdocuments.
+
+
+PASS document.hidden is defined and not null. 
+PASS document.visibilityState is defined and not null. 
+PASS windowless subdocument generated for test has a null default view 
+PASS document.hidden is definedwindowless subdocument. 
+PASS document.visibilityState is definedwindowless subdocument. 
+PASS hidden == true for windowless subdocuments with a null default view 
+PASS visibilityState == hidden for windowless subdocuments with a null default view 
+
diff --git a/LayoutTests/imported/w3c/web-platform-tests/page-visibility/test_default_view.html b/LayoutTests/imported/w3c/web-platform-tests/page-visibility/test_default_view.html
new file mode 100644
index 0000000..6e2f970
--- /dev/null
+++ b/LayoutTests/imported/w3c/web-platform-tests/page-visibility/test_default_view.html
@@ -0,0 +1,43 @@
+<!DOCTYPE html>
+<html>
+    <head>
+        <meta charset="utf-8" />
+        <title>Page Visibility Null Default View Test</title>
+        <link rel="author" title="Microsoft" href="http://www.microsoft.com/" />
+
+        <script src="/resources/testharness.js"></script>
+        <script src="/resources/testharnessreport.js"></script>
+        <script type="text/javascript" src="resources/pagevistestharness.js"></script>
+
+        <script type="text/javascript" >
+            setup({explicit_done: true});
+
+            function onload_test()
+            {
+                // inject a windowless subdocument as a child of the root document <html> element
+                var subDoc = document.implementation.createDocument('resources/blank_page_green.html', 'html', null);
+
+                // Test precondition: ensure subdocument has a null default view
+                test_true(subDoc.defaultView == null, "windowless subdocument generated for test has a null default view");
+
+                // check that feature exists within subdocument
+                test_feature_exists(subDoc, 'windowless subdocument');
+
+                // check that the subdocument has a hidden visibility state
+                test_true(subDoc.hidden,
+                          "hidden == true for windowless subdocuments with a null default view");
+                test_equals(subDoc.visibilityState, VISIBILITY_STATES.HIDDEN,
+                            "visibilityState == " + VISIBILITY_STATES.HIDDEN +
+                            " for windowless subdocuments with a null default view");
+
+                done();
+            }
+        </script>
+    </head>
+    <body onload="onload_test()">
+        <h1>Description</h1>
+        <p>This test validates that document.hidden == false and
+           document.visibilityState == "visible" for windowless subdocuments.</p>
+        <div id="log"></div>
+    </body>
+</html>
diff --git a/LayoutTests/imported/w3c/web-platform-tests/page-visibility/test_read_only-expected.txt b/LayoutTests/imported/w3c/web-platform-tests/page-visibility/test_read_only-expected.txt
new file mode 100644
index 0000000..8a086e2
--- /dev/null
+++ b/LayoutTests/imported/w3c/web-platform-tests/page-visibility/test_read_only-expected.txt
@@ -0,0 +1,10 @@
+Description
+
+This test validates that the Page Visibility attributes are read only.
+
+
+PASS document.hidden is defined and not null. 
+PASS document.visibilityState is defined and not null. 
+PASS document.hidden is read only. 
+PASS document.visibilityState is read only. 
+
diff --git a/LayoutTests/imported/w3c/web-platform-tests/page-visibility/test_read_only.html b/LayoutTests/imported/w3c/web-platform-tests/page-visibility/test_read_only.html
new file mode 100644
index 0000000..6d37022
--- /dev/null
+++ b/LayoutTests/imported/w3c/web-platform-tests/page-visibility/test_read_only.html
@@ -0,0 +1,37 @@
+<!DOCTYPE html>
+<html>
+    <head>
+        <meta charset="utf-8" />
+        <title>Page Visibility API is Read Only</title>
+
+        <script src="/resources/testharness.js"></script>
+        <script src="/resources/testharnessreport.js"></script>
+        <script type="text/javascript" src="resources/pagevistestharness.js"></script>
+
+        <script type="text/javascript">
+            setup({ explicit_done: true });
+            function onload_test()
+            {
+                //check for feature definition first before attempting to overwrite
+                pv_test();
+
+                //Check document.hidden
+                document.hidden = "new value";
+                test_true(document.hidden !== "new value", 'document.hidden is read only.');
+
+                //Check document.visibilityState
+                document.visibilityState = "new value";
+                test_true(document.visibilityState !== "new value",
+                                "document.visibilityState is read only.");
+
+                done();
+            }
+        </script>
+    </head>
+    <body onload="onload_test();">
+        <h1>Description</h1>
+        <p>This test validates that the Page Visibility attributes are read only.</p>
+
+        <div id="log"></div>
+    </body>
+</html>
diff --git a/LayoutTests/imported/w3c/web-platform-tests/page-visibility/unload-bubbles-expected.txt b/LayoutTests/imported/w3c/web-platform-tests/page-visibility/unload-bubbles-expected.txt
new file mode 100644
index 0000000..d3b292a
--- /dev/null
+++ b/LayoutTests/imported/w3c/web-platform-tests/page-visibility/unload-bubbles-expected.txt
@@ -0,0 +1,3 @@
+
+PASS visibilitychange event bubbles when fired on unload 
+
diff --git a/LayoutTests/imported/w3c/web-platform-tests/page-visibility/unload-bubbles.html b/LayoutTests/imported/w3c/web-platform-tests/page-visibility/unload-bubbles.html
new file mode 100644
index 0000000..41f40c5
--- /dev/null
+++ b/LayoutTests/imported/w3c/web-platform-tests/page-visibility/unload-bubbles.html
@@ -0,0 +1,18 @@
+<!doctype html>
+<title>visibilitychange event bubbles when fired on unload</title>
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+<div id="log"></div>
+<script>
+async_test(function(t) {
+  var w = window.open("resources/unload-bubbles.html");
+  window.onmessage = t.step_func(function(event) {
+    if (event.data === "close") {
+      w.close();
+      return;
+    }
+    assert_equals(event.data, "hidden");
+    t.done();
+  });
+});
+</script>
diff --git a/LayoutTests/imported/w3c/web-platform-tests/page-visibility/unload-expected.txt b/LayoutTests/imported/w3c/web-platform-tests/page-visibility/unload-expected.txt
new file mode 100644
index 0000000..18a937e
--- /dev/null
+++ b/LayoutTests/imported/w3c/web-platform-tests/page-visibility/unload-expected.txt
@@ -0,0 +1,3 @@
+
+PASS visibilitychange fires on unload 
+
diff --git a/LayoutTests/imported/w3c/web-platform-tests/page-visibility/unload.html b/LayoutTests/imported/w3c/web-platform-tests/page-visibility/unload.html
new file mode 100644
index 0000000..9b7dcb8
--- /dev/null
+++ b/LayoutTests/imported/w3c/web-platform-tests/page-visibility/unload.html
@@ -0,0 +1,18 @@
+<!doctype html>
+<title>visibilitychange fires on unload</title>
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+<div id="log"></div>
+<script>
+async_test(function(t) {
+  var w = window.open("resources/unload.html");
+  window.onmessage = t.step_func(function(event) {
+    if (event.data === "close") {
+      w.close();
+      return;
+    }
+    assert_equals(event.data, "hidden");
+    t.done();
+  });
+});
+</script>
diff --git a/LayoutTests/imported/w3c/web-platform-tests/page-visibility/w3c-import.log b/LayoutTests/imported/w3c/web-platform-tests/page-visibility/w3c-import.log
new file mode 100644
index 0000000..d1344b8
--- /dev/null
+++ b/LayoutTests/imported/w3c/web-platform-tests/page-visibility/w3c-import.log
@@ -0,0 +1,26 @@
+The tests in this directory were imported from the W3C repository.
+Do NOT modify these tests directly in WebKit.
+Instead, create a pull request on the WPT github:
+	https://github.com/web-platform-tests/wpt
+
+Then run the Tools/Scripts/import-w3c-tests in WebKit to reimport
+
+Do NOT modify or remove this file.
+
+------------------------------------------------------------------------
+Properties requiring vendor prefixes:
+None
+Property values requiring vendor prefixes:
+None
+------------------------------------------------------------------------
+List of files:
+/LayoutTests/imported/w3c/web-platform-tests/page-visibility/META.yml
+/LayoutTests/imported/w3c/web-platform-tests/page-visibility/idlharness.window.js
+/LayoutTests/imported/w3c/web-platform-tests/page-visibility/iframe-unload.html
+/LayoutTests/imported/w3c/web-platform-tests/page-visibility/onvisibilitychange.html
+/LayoutTests/imported/w3c/web-platform-tests/page-visibility/test_attributes_exist.html
+/LayoutTests/imported/w3c/web-platform-tests/page-visibility/test_child_document.html
+/LayoutTests/imported/w3c/web-platform-tests/page-visibility/test_default_view.html
+/LayoutTests/imported/w3c/web-platform-tests/page-visibility/test_read_only.html
+/LayoutTests/imported/w3c/web-platform-tests/page-visibility/unload-bubbles.html
+/LayoutTests/imported/w3c/web-platform-tests/page-visibility/unload.html
diff --git a/Source/WebCore/ChangeLog b/Source/WebCore/ChangeLog
index ebeaab3..86ca518 100644
--- a/Source/WebCore/ChangeLog
+++ b/Source/WebCore/ChangeLog
@@ -1,3 +1,46 @@
+2020-09-25  Chris Dumez  <cdumez@apple.com>
+
+        visibilitychange:hidden doesn't fire during page navigations
+        https://bugs.webkit.org/show_bug.cgi?id=151234
+        <rdar://problem/23688763>
+
+        Reviewed by Ryosuke Niwa.
+
+        Fire a visibilitychange during document unload, as per the specification:
+        - https://www.w3.org/TR/page-visibility/#reacting-to-visibilitychange-changes
+        - https://html.spec.whatwg.org/multipage/browsing-the-web.html#unloading-document-visibility-change-steps
+
+        Note that the specification currently says to fire the visibilitychange event before the pagehide event.
+        However, Both Chrome and Firefox fire the pagehide event then the visibilitychange event. This change
+        aligns our behavior with both Chrome and Firefox. The following bug has been filed against the
+        specification:
+        - https://github.com/w3c/page-visibility/issues/67
+
+        We also fire a visibilitychange event when coming out of the back/forward cache. This makes sense given
+        that we fire one when the document enters the back/forward cache. This is also Firefox's behavior.
+        I have verified that the new fast/history/back-forward-cache-visibility-state.html layout test is passing
+        in Firefox.
+
+        Tests: fast/history/back-forward-cache-visibility-state.html
+               imported/w3c/web-platform-tests/page-visibility/idlharness.window.html
+               imported/w3c/web-platform-tests/page-visibility/iframe-unload.html
+               imported/w3c/web-platform-tests/page-visibility/onvisibilitychange.html
+               imported/w3c/web-platform-tests/page-visibility/test_attributes_exist.html
+               imported/w3c/web-platform-tests/page-visibility/test_child_document.html
+               imported/w3c/web-platform-tests/page-visibility/test_default_view.html
+               imported/w3c/web-platform-tests/page-visibility/test_read_only.html
+               imported/w3c/web-platform-tests/page-visibility/unload-bubbles.html
+               imported/w3c/web-platform-tests/page-visibility/unload.html
+
+        * dom/Document.cpp:
+        (WebCore::Document::visibilityState const):
+        (WebCore::Document::setHiddenDueToDismissal):
+        * dom/Document.h:
+        * history/CachedPage.cpp:
+        (WebCore::firePageShowAndPopStateEvents):
+        * loader/FrameLoader.cpp:
+        (WebCore::FrameLoader::dispatchUnloadEvents):
+
 2020-09-25  James Darpinian  <jdarpinian@chromium.org>
 
         Support OES_fbo_render_mipmap
diff --git a/Source/WebCore/dom/Document.cpp b/Source/WebCore/dom/Document.cpp
index a2454c9..ab7f119 100644
--- a/Source/WebCore/dom/Document.cpp
+++ b/Source/WebCore/dom/Document.cpp
@@ -1772,7 +1772,7 @@
     // page. If there is no page associated with the document, we will assume
     // that the page is hidden, as specified by the spec:
     // https://w3c.github.io/page-visibility/#visibilitystate-attribute
-    if (!m_frame || !m_frame->page())
+    if (!m_frame || !m_frame->page() || m_visibilityHiddenDueToDismissal)
         return VisibilityState::Hidden;
     return m_frame->page()->visibilityState();
 }
@@ -3257,6 +3257,15 @@
     didChangeTimerAlignmentInterval();
 }
 
+void Document::setVisibilityHiddenDueToDismissal(bool hiddenDueToDismissal)
+{
+    if (m_visibilityHiddenDueToDismissal == hiddenDueToDismissal)
+        return;
+
+    m_visibilityHiddenDueToDismissal = hiddenDueToDismissal;
+    dispatchEvent(Event::create(eventNames().visibilitychangeEvent, Event::CanBubble::Yes, Event::IsCancelable::No));
+}
+
 Seconds Document::domTimerAlignmentInterval(bool hasReachedMaxNestingLevel) const
 {
     auto alignmentInterval = ScriptExecutionContext::domTimerAlignmentInterval(hasReachedMaxNestingLevel);
diff --git a/Source/WebCore/dom/Document.h b/Source/WebCore/dom/Document.h
index 84771ec..864ad73 100644
--- a/Source/WebCore/dom/Document.h
+++ b/Source/WebCore/dom/Document.h
@@ -491,6 +491,8 @@
     void setTimerThrottlingEnabled(bool);
     bool isTimerThrottlingEnabled() const { return m_isTimerThrottlingEnabled; }
 
+    void setVisibilityHiddenDueToDismissal(bool);
+
     WEBCORE_EXPORT ExceptionOr<Ref<Node>> adoptNode(Node& source);
 
     WEBCORE_EXPORT Ref<HTMLCollection> images();
@@ -2128,6 +2130,8 @@
 #endif
     bool m_hasVisuallyNonEmptyCustomContent { false };
 
+    bool m_visibilityHiddenDueToDismissal { false };
+
     Ref<UndoManager> m_undoManager;
 #if PLATFORM(IOS_FAMILY)
     std::unique_ptr<ContentChangeObserver> m_contentChangeObserver;
diff --git a/Source/WebCore/history/CachedPage.cpp b/Source/WebCore/history/CachedPage.cpp
index b7dd48b..e3c5e67 100644
--- a/Source/WebCore/history/CachedPage.cpp
+++ b/Source/WebCore/history/CachedPage.cpp
@@ -92,8 +92,9 @@
         if (!document)
             continue;
 
-        // FIXME: Update Page Visibility state here.
-        // https://bugs.webkit.org/show_bug.cgi?id=116770
+        // This takes care of firing the visibilitychange event and making sure the document is reported as visible.
+        document->setVisibilityHiddenDueToDismissal(false);
+
         document->dispatchPageshowEvent(PageshowEventPersisted);
 
         auto* historyItem = child->loader().history().currentItem();
diff --git a/Source/WebCore/loader/FrameLoader.cpp b/Source/WebCore/loader/FrameLoader.cpp
index 5d847e9..58e83fb 100644
--- a/Source/WebCore/loader/FrameLoader.cpp
+++ b/Source/WebCore/loader/FrameLoader.cpp
@@ -3280,8 +3280,8 @@
                 m_frame.document()->domWindow()->dispatchEvent(PageTransitionEvent::create(eventNames().pagehideEvent, m_frame.document()->backForwardCacheState() == Document::AboutToEnterBackForwardCache), m_frame.document());
             }
 
-            // FIXME: update Page Visibility state here.
-            // https://bugs.webkit.org/show_bug.cgi?id=116770
+            // This takes care of firing the visibilitychange event and making sure the document is reported as hidden.
+            m_frame.document()->setVisibilityHiddenDueToDismissal(true);
 
             if (m_frame.document()->backForwardCacheState() == Document::NotInBackForwardCache) {
                 Ref<Event> unloadEvent(Event::create(eventNames().unloadEvent, Event::CanBubble::No, Event::IsCancelable::No));