Bug 1430019 - Add tests for gesture activation propagation upon navigation. draft
authorChris Pearce <cpearce@mozilla.com>
Tue, 20 Feb 2018 16:47:00 +1300
changeset 773288 cab260c4d11c8a1a0bee19ee691ef6bbe6c7173b
parent 773287 bc78594d2f6c83d011a34b3caf06d2a3936d5cfa
push id104207
push userbmo:cpearce@mozilla.com
push dateTue, 27 Mar 2018 20:19:54 +0000
bugs1430019
milestone61.0a1
Bug 1430019 - Add tests for gesture activation propagation upon navigation. MozReview-Commit-ID: JpIsidxYYN1
docshell/base/nsDocShell.cpp
docshell/shistory/nsISHEntry.idl
docshell/shistory/nsSHEntry.cpp
docshell/shistory/nsSHEntryShared.cpp
docshell/shistory/nsSHEntryShared.h
dom/media/test/AutoplayTestUtils.js
dom/media/test/file_autoplay_policy_navigation_window.html
dom/media/test/mochitest.ini
dom/media/test/test_autoplay_policy_navigation.html
--- a/docshell/base/nsDocShell.cpp
+++ b/docshell/base/nsDocShell.cpp
@@ -7966,16 +7966,18 @@ nsDocShell::CaptureState()
 
   if (!mScriptGlobal) {
     return NS_ERROR_FAILURE;
   }
 
   nsCOMPtr<nsISupports> windowState = mScriptGlobal->SaveWindowState();
   NS_ENSURE_TRUE(windowState, NS_ERROR_FAILURE);
 
+  mOSHE->SaveUserGestureActivationState();
+
 #ifdef DEBUG_PAGE_CACHE
   nsCOMPtr<nsIURI> uri;
   mOSHE->GetURI(getter_AddRefs(uri));
   nsAutoCString spec;
   if (uri) {
     uri->GetSpec(spec);
   }
   printf("Saving presentation into session history\n");
@@ -8765,25 +8767,25 @@ nsDocShell::CreateContentViewer(const ns
 
   // Notify the current document that it is about to be unloaded!!
   //
   // It is important to fire the unload() notification *before* any state
   // is changed within the DocShell - otherwise, javascript will get the
   // wrong information :-(
   //
 
-  nsCOMPtr<nsIPrincipal> oldPrincipal;
+  // nsCOMPtr<nsIPrincipal> oldPrincipal = viewer->GetDocument()->NodePrincipal();
 
   if (mSavingOldViewer) {
     // We determined that it was safe to cache the document presentation
     // at the time we initiated the new load.  We need to check whether
     // it's still safe to do so, since there may have been DOM mutations
     // or new requests initiated.
     nsCOMPtr<nsIDocument> doc = viewer->GetDocument();
-    oldPrincipal = doc->NodePrincipal();
+    // oldPrincipal = doc->NodePrincipal();
     mSavingOldViewer = CanSavePresentation(mLoadType, aRequest, doc);
   }
 
   NS_ASSERTION(!mLoadingURI, "Re-entering unload?");
 
   nsCOMPtr<nsIChannel> aOpenedChannel = do_QueryInterface(aRequest);
   if (aOpenedChannel) {
     aOpenedChannel->GetURI(getter_AddRefs(mLoadingURI));
@@ -8978,19 +8980,49 @@ nsDocShell::NewContentViewerObj(const ns
                                                  aContentHandler,
                                                  aViewer);
   NS_ENSURE_SUCCESS(rv, rv);
 
   (*aViewer)->SetContainer(this);
   return NS_OK;
 }
 
+
+// static nsCString
+// OriginOf(nsIPrincipal* principal)
+// {
+//   if (!principal) {
+//     return NS_LITERAL_CSTRING("null");
+//   }
+//   nsCString origin;
+//   if (NS_FAILED(principal->GetOrigin(origin))) {
+//     return NS_LITERAL_CSTRING("null");
+//   }
+//   return origin;
+// }
+
+// static nsCString
+// OriginOf(nsIDocument* doc)
+// {
+//   nsCOMPtr<nsIPrincipal> principal = doc->NodePrincipal();
+//   if (!principal) {
+//     return NS_LITERAL_CSTRING("null");
+//   }
+//   nsCString origin;
+//   if (NS_FAILED(principal->GetOrigin(origin))) {
+//     return NS_LITERAL_CSTRING("null");
+//   }
+//   return origin;
+// }
+
 nsresult
 nsDocShell::SetupNewViewer(nsIContentViewer* aNewViewer)
 {
+  // printf("%p %s mSavingOldViewer=%d\n", this, __func__, mSavingOldViewer);
+
   MOZ_ASSERT(!mIsBeingDestroyed);
 
   //
   // Copy content viewer state from previous or parent content viewer.
   //
   // The following logic is mirrored in nsHTMLDocument::StartDocumentLoad!
   //
   // Do NOT to maintain a reference to the old content viewer outside
@@ -9052,16 +9084,21 @@ nsDocShell::SetupNewViewer(nsIContentVie
     }
 
     if (oldCv) {
       // Record the user-gesture activation state from the old document, and
       // its principal. This ensures we can forward the unblock-autoplay-media
       // state into the new document.
       nsIDocument* doc = oldCv->GetDocument();
       activatedByUserGesture = doc && doc->HasBeenUserActivated();
+      if (mSavingOldViewer && mOSHE) {
+        mOSHE->SetUserGestureActivated(activatedByUserGesture);
+        // printf("Saved gesture activation, doc=%p (%s) activated=%d\n",
+        //   doc, OriginOf(doc).get(), activatedByUserGesture);
+      }
       oldPrincipal = doc ? doc->NodePrincipal() : nullptr;
       newCv = aNewViewer;
       if (newCv) {
         forceCharset = oldCv->GetForceCharset();
         hintCharset = oldCv->GetHintCharset();
         NS_ENSURE_SUCCESS(oldCv->GetHintCharacterSetSource(&hintCharsetSource),
                           NS_ERROR_FAILURE);
         NS_ENSURE_SUCCESS(oldCv->GetMinFontSize(&minFontSize),
@@ -9073,18 +9110,18 @@ nsDocShell::SetupNewViewer(nsIContentVie
         NS_ENSURE_SUCCESS(oldCv->GetOverrideDPPX(&overrideDPPX),
                           NS_ERROR_FAILURE);
         NS_ENSURE_SUCCESS(oldCv->GetAuthorStyleDisabled(&styleDisabled),
                           NS_ERROR_FAILURE);
       }
     }
   }
 
-  // printf("%p %s activatedByUserGesture=%d oldPrincipal=%p\n",
-  //   this, __func__, activatedByUserGesture, oldPrincipal);
+  // printf("%p %s activatedByUserGesture=%d oldPrincipal=%p (%s)\n",
+  //   this, __func__, activatedByUserGesture, oldPrincipal, OriginOf(oldPrincipal).get());
 
   nscolor bgcolor = NS_RGBA(0, 0, 0, 0);
   bool isActive = false;
   // Ensure that the content viewer is destroyed *after* the GC - bug 71515
   nsCOMPtr<nsIContentViewer> contentViewer = mContentViewer;
   if (contentViewer) {
     // Stop any activity that may be happening in the old document before
     // releasing it...
@@ -9146,24 +9183,38 @@ nsDocShell::SetupNewViewer(nsIContentVie
     NS_ENSURE_SUCCESS(newCv->SetOverrideDPPX(overrideDPPX),
                       NS_ERROR_FAILURE);
     NS_ENSURE_SUCCESS(newCv->SetAuthorStyleDisabled(styleDisabled),
                       NS_ERROR_FAILURE);
 
     // Restore activated-by-user-gesture state, to propogate autoplay media
     // state into the new if it's same origin.
     nsIDocument* doc = newCv->GetDocument();
-    if (activatedByUserGesture && doc) {
-      nsCOMPtr<nsIPrincipal> newPrincipal = doc->NodePrincipal();
-      if (oldPrincipal->Equals(newPrincipal)) {
-        // TODO: Should use Equals()? Subsumes?
-        doc->NotifyUserActivation();
-      }
+    if (mLSHE) {
+      bool activated = false;
+      if (NS_SUCCEEDED(mLSHE->GetUserGestureActivated(&activated)) && activated) {
+        // printf("mLSHE was gesture activated.\n");
+        activatedByUserGesture = activated;
+      }
+    // } else {
+    //   printf("Null mLSHE\n");
+    }
+    if (activatedByUserGesture) {
+      if (doc) {
+        nsCOMPtr<nsIPrincipal> newPrincipal = doc->NodePrincipal();
+        if (oldPrincipal->Equals(newPrincipal)) {
+          // TODO: Should use Equals()? Subsumes?
+          doc->NotifyUserActivation();
+        // } else {
+        //   printf("Unequal prinicipals new=%s old=%s.\n",
+        //     OriginOf(newPrincipal).get(), OriginOf(oldPrincipal).get());
+        }
       // } else {
-      //   printf("!doc\n");
+      //   printf("!doc, but was activated\n");
+      }
     }
   }
 
   // Stuff the bgcolor from the old pres shell into the new
   // pres shell. This improves page load continuity.
   nsCOMPtr<nsIPresShell> shell;
   mContentViewer->GetPresShell(getter_AddRefs(shell));
 
@@ -11928,17 +11979,17 @@ nsDocShell::AddState(JS::Handle<JS::Valu
   // one.  This operation may modify mOSHE, which we need later, so we
   // keep a reference here.
   NS_ENSURE_TRUE(mOSHE, NS_ERROR_FAILURE);
   nsCOMPtr<nsISHEntry> oldOSHE = mOSHE;
 
   mLoadType = LOAD_PUSHSTATE;
 
   nsCOMPtr<nsISHEntry> newSHEntry;
-  if (!aReplace) {
+  if (!aReplace) { // TODO: Save gesture-activation here too? Where to restore?
     // Save the current scroll position (bug 590573).
     nscoord cx = 0, cy = 0;
     GetCurScrollPos(ScrollOrientation_X, &cx);
     GetCurScrollPos(ScrollOrientation_Y, &cy);
     mOSHE->SetScrollPosition(cx, cy);
 
     bool scrollRestorationIsManual = false;
     mOSHE->GetScrollRestorationIsManual(&scrollRestorationIsManual);
--- a/docshell/shistory/nsISHEntry.idl
+++ b/docshell/shistory/nsISHEntry.idl
@@ -27,17 +27,17 @@ interface nsISHistory;
 #include "nsRect.h"
 class nsDocShellEditorData;
 class nsSHEntryShared;
 %}
 [ref] native nsIntRect(nsIntRect);
 [ptr] native nsDocShellEditorDataPtr(nsDocShellEditorData);
 [ptr] native nsSHEntryShared(nsSHEntryShared);
 
-[scriptable, uuid(0dad26b8-a259-42c7-93f1-2fa7fc076e45)]
+[scriptable, uuid(1e46717b-b94d-4d54-abcc-80acb68dc4dc)]
 interface nsISHEntry : nsISupports
 {
     /**
      * A readonly property that returns the URI
      * of the current entry. The object returned is
      * of type nsIURI
      */
     readonly attribute nsIURI URI;
@@ -341,16 +341,24 @@ interface nsISHEntry : nsISupports
     readonly attribute boolean loadedInThisProcess;
 
     /**
      * The session history it belongs to. It's usually only set on root entries.
      * SHEntry is strictly bound to the SHistory it belongs to; it should not be
      * changed once set to a non-null value.
      */
     [noscript] attribute nsISHistory SHistory;
+
+    /**
+     * Whether this document was activated by a user gesture (mouse or key
+     * event).
+     */
+    attribute boolean userGestureActivated;
+
+    [noscript] void SaveUserGestureActivationState();
 };
 
 [scriptable, uuid(bb66ac35-253b-471f-a317-3ece940f04c5)]
 interface nsISHEntryInternal : nsISupports
 {
     [notxpcom] void RemoveFromBFCacheAsync();
     [notxpcom] void RemoveFromBFCacheSync();
 
--- a/docshell/shistory/nsSHEntry.cpp
+++ b/docshell/shistory/nsSHEntry.cpp
@@ -418,16 +418,37 @@ nsSHEntry::GetExpirationStatus(bool* aFl
 NS_IMETHODIMP
 nsSHEntry::SetExpirationStatus(bool aFlag)
 {
   mShared->mExpired = aFlag;
   return NS_OK;
 }
 
 NS_IMETHODIMP
+nsSHEntry::GetUserGestureActivated(bool* aOutUserGestureActivated)
+{
+  *aOutUserGestureActivated = mShared->mUserGestureActivated;
+  return NS_OK;
+}
+
+NS_IMETHODIMP
+nsSHEntry::SetUserGestureActivated(bool aUserGestureActivated)
+{
+  mShared->mUserGestureActivated = aUserGestureActivated;
+  return NS_OK;
+}
+
+NS_IMETHODIMP
+nsSHEntry::SaveUserGestureActivationState()
+{
+  mShared->SaveUserGestureActivationState();
+  return NS_OK;
+}
+
+NS_IMETHODIMP
 nsSHEntry::GetContentType(nsACString& aContentType)
 {
   aContentType = mShared->mContentType;
   return NS_OK;
 }
 
 NS_IMETHODIMP
 nsSHEntry::SetContentType(const nsACString& aContentType)
--- a/docshell/shistory/nsSHEntryShared.cpp
+++ b/docshell/shistory/nsSHEntryShared.cpp
@@ -39,16 +39,17 @@ nsSHEntryShared::nsSHEntryShared()
   : mDocShellID({0})
   , mLastTouched(0)
   , mID(gSHEntrySharedID++)
   , mViewerBounds(0, 0, 0, 0)
   , mIsFrameNavigation(false)
   , mSaveLayoutState(true)
   , mSticky(true)
   , mDynamicallyCreated(false)
+  , mUserGestureActivated(false)
   , mExpired(false)
 {
 }
 
 nsSHEntryShared::~nsSHEntryShared()
 {
   // The destruction can be caused by either the entry is removed from session
   // history and no one holds the reference, or the whole session history is on
@@ -130,16 +131,27 @@ nsSHEntryShared::DropPresentationState()
   mSticky = true;
   mWindowState = nullptr;
   mViewerBounds.SetRect(0, 0, 0, 0);
   mChildShells.Clear();
   mRefreshURIList = nullptr;
   mEditorData = nullptr;
 }
 
+void
+nsSHEntryShared::SaveUserGestureActivationState()
+{
+  if (!mDocument) {
+    return;
+  }
+  mUserGestureActivated = mDocument->HasBeenUserActivated();
+  printf("Saved gesture activation state %d for doc=%p\n",
+    mUserGestureActivated, mDocument.get());
+}
+
 nsresult
 nsSHEntryShared::SetContentViewer(nsIContentViewer* aViewer)
 {
   NS_PRECONDITION(!aViewer || !mContentViewer,
                   "SHEntryShared already contains viewer");
 
   if (mContentViewer || !aViewer) {
     DropPresentationState();
--- a/docshell/shistory/nsSHEntryShared.h
+++ b/docshell/shistory/nsSHEntryShared.h
@@ -55,16 +55,17 @@ private:
 
   friend class nsSHEntry;
 
   static already_AddRefed<nsSHEntryShared> Duplicate(nsSHEntryShared* aEntry);
 
   void RemoveFromExpirationTracker();
   nsresult SyncPresentationState();
   void DropPresentationState();
+  void SaveUserGestureActivationState();
 
   nsresult SetContentViewer(nsIContentViewer* aViewer);
 
   // See nsISHEntry.idl for an explanation of these members.
 
   // These members are copied by nsSHEntryShared::Duplicate().  If you add a
   // member here, be sure to update the Duplicate() implementation.
   nsID mDocShellID;
@@ -88,14 +89,15 @@ private:
   nsExpirationState mExpirationState;
   nsAutoPtr<nsDocShellEditorData> mEditorData;
   nsWeakPtr mSHistory;
 
   bool mIsFrameNavigation;
   bool mSaveLayoutState;
   bool mSticky;
   bool mDynamicallyCreated;
+  bool mUserGestureActivated;
 
   // This flag is about necko cache, not bfcache.
   bool mExpired;
 };
 
 #endif
--- a/dom/media/test/AutoplayTestUtils.js
+++ b/dom/media/test/AutoplayTestUtils.js
@@ -1,21 +1,34 @@
-function playAndPostResult(muted, parent_window) {
+// Enumerated actions for navigation tests.
+const Activate = 0;
+const NavigateSameOrigin = 1;
+const NavigateCrossOrigin = 2;
+const NavigateBack = 3;
+const NavigateForward = 4;
+const Reload = 5;
+
+async function playAndPostResult(muted, parent_window) {
   let element = document.createElement("video");
   element.preload = "auto";
   element.muted = muted;
   element.src = "short.mp4";
   element.id = "video";
+  element.load();
   document.body.appendChild(element);
+  
+  // await nextEvent(element, "loadedmetadata");
 
   element.play().then(
       () => {
+        // parent_window.postMessage("played", "*");
         parent_window.postMessage({played: true}, "*");
       },
       () => {
+        // parent_window.postMessage("blocked", "*");
         parent_window.postMessage({played: false}, "*");
       }
     );
 }
 
 function nextEvent(eventTarget, eventName) {
   return new Promise(function(resolve, reject) {
     let f = function(event) {
@@ -26,12 +39,13 @@ function nextEvent(eventTarget, eventNam
   });
 }
 
 function nextWindowMessage() {
   return nextEvent(window, "message");
 }
 
 function log(msg) {
-  var log_pane = document.body;
-  log_pane.appendChild(document.createTextNode(msg));
-  log_pane.appendChild(document.createElement("br"));
+  // var log_pane = document.body;
+  // log_pane.appendChild(document.createTextNode(msg));
+  // log_pane.appendChild(document.createElement("br"));
+  dump(msg + "\n");
 }
new file mode 100644
--- /dev/null
+++ b/dom/media/test/file_autoplay_policy_navigation_window.html
@@ -0,0 +1,153 @@
+<!DOCTYPE HTML>
+<html>
+  <head>
+    <title>Autoplay policy window</title>
+    <style>
+      video {
+        width: 50%;
+        height: 50%;
+      }
+    </style>
+    <script type="text/javascript" src="AutoplayTestUtils.js"></script>
+    <script type="text/javascript" src="/tests/SimpleTest/EventUtils.js"></script>
+  </head>
+  <body>
+    <pre id="test">
+      <script>
+
+        // SimpleTest.requestFlakyTimeout("Because there's not a good way to detect iframe history navigation complete");
+
+
+        // function awhile() {
+        //   return new Promise(function(resolve, reject) {
+        //     setTimeout(()=>{resolve();}, 1000);
+        //   });
+        // }
+
+        // async function testAutoplayNavigation(test_case, parent_window) {
+        //   log("testAutoplayNavigation: " + test_case.name);
+
+        //   let navigationHistory = new NavigationHistory();
+        //   let counter = 1;
+
+        //   // Create a child iframe...
+        //   var frame = document.createElement("iframe");
+        //   const resource = "/tests/dom/media/test/file_autoplay_policy_activation_frame.html";
+        //   const sameOriginResource = "http://mochi.test:8888" + resource;
+        //   const crossOriginResource = "http://example.org" + resource;
+        //   frame.src = resource + "?" + counter++;
+        //   navigationHistory.push(frame.src);
+          
+        //   // Wait for it to load...
+        //   document.body.appendChild(frame);
+        //   await once(frame, "load");
+
+        //   // Perform test case's actions.
+        //   for (let action of test_case.actions) {
+        //     switch (action) {
+        //       case Activate: {
+        //         synthesizeMouseAtCenter(frame, {});
+        //         break;
+        //       }
+        //       case NavigateSameOrigin: {
+        //         let uri = sameOriginResource + "?" + counter++;
+        //         frame.contentWindow.location = uri;
+        //         navigationHistory.push(uri);
+        //         await once(frame, "load");
+        //         break;
+        //       }
+        //       case NavigateCrossOrigin: {
+        //         let uri = crossOriginResource + "?" + counter++;
+        //         frame.contentWindow.location = uri;
+        //         navigationHistory.push(uri);
+        //         await once(frame, "load");
+        //         break;
+        //       }
+        //       case NavigateBack: {
+        //         // log("going back");
+        //         // document.body.addEventListener("load", (e)=>{
+        //         //   log("frame load");
+        //         // }, true);
+        //         log("frame.contentWindow.location=" + SpecialPowers.wrap(frame.contentWindow).location);
+        //         SpecialPowers.wrap(frame.contentWindow).history.back();
+        //         let uri = navigationHistory.back();
+        //         log("Awaiting location change");
+        //         await locationChangedTo(frame, uri);
+        //         log("frame.contentWindow.location=" + SpecialPowers.wrap(frame.contentWindow).location);
+        //         // log("awaiting go back complete");
+        //         // await once(frame, "load");
+        //         // log("finished going back");
+        //         // await awhile();
+        //         break;
+        //       }
+        //       case NavigateForward: {
+        //         SpecialPowers.wrap(frame.contentWindow).history.forward();
+        //         let uri = navigationHistory.forward();
+        //         log("Awaiting location change to " + uri);
+        //         await locationChangedTo(frame, uri);
+        //         break;
+        //       }
+        //       case Reload: {
+        //         frame.contentWindow.location.reload(true);
+        //         await once(frame, "load");
+        //         break;
+        //       }
+        //     }
+        //   }
+
+        //   // Ask the newly loaded frame to play a video.
+        //   frame.contentWindow.postMessage(test_case, "*");
+
+        //   // Wait for the frame to tell us whether it could play video.
+        //   let result = await nextWindowMessage();
+
+        //   // Report whether the iframe could play to the parent.
+        //   parent_window.postMessage(result.data, "*");
+        // }
+
+        // nextWindowMessage().then(
+        //   (event) => {
+        //     let test_case = event.data;
+        //     let parent_window = event.source;
+        //     testAutoplayNavigation(test_case, parent_window);
+        //   });
+
+        window.addEventListener("mousedown", ()=>{
+          dump("mousedown\n");
+        }, false);
+
+        window.addEventListener("mouseup", ()=>{
+          dump("mouseup\n");
+        }, false);
+
+        window.addEventListener("message",
+          (event) => {
+            dump("message: " + event.data + "\n");
+            if (event.data == "play-audible") {
+              playAndPostResult(false, event.source);
+            } else if (event.data == "click") {
+              synthesizeMouseAtCenter(document.body, {});
+              window.opener.postMessage("clicked", "*");
+            } else if (event.data == "navigate-back") {
+              history.back();
+            }
+          });
+
+        // Onload we fire a "ready" message back to the opener.
+        dump("Firing 'ready'\n");
+        window.opener.postMessage("ready", "*");
+
+
+        window.addEventListener("pagehide", (e)=>{
+          dump("pagehide\n");
+        });
+
+        window.addEventListener("pageshow", (e)=>{
+          dump("pageshow\n");
+          window.opener.postMessage("pageshow", "*");
+        });
+
+      </script>
+    </pre>
+  </body>
+</html>
--- a/dom/media/test/mochitest.ini
+++ b/dom/media/test/mochitest.ini
@@ -436,16 +436,17 @@ support-files =
   dirac.ogg^headers^
   dynamic_redirect.sjs
   dynamic_resource.sjs
   eme.js
   file_access_controls.html
   file_autoplay_policy_unmute_pauses.html
   file_autoplay_policy_activation_window.html
   file_autoplay_policy_activation_frame.html
+  file_autoplay_policy_navigation_window.html
   flac-s24.flac
   flac-s24.flac^headers^
   flac-noheader-s16.flac
   flac-noheader-s16.flac^headers^
   fragment_noplay.js
   fragment_play.js
   gizmo.mp4
   gizmo.mp4^headers^
@@ -687,16 +688,18 @@ skip-if = true # bug 475110 - disabled s
 [test_autoplay_contentEditable.html]
 skip-if = android_version == '15' || android_version == '17' || android_version == '22' # android(bug 1232305, bug 1232318, bug 1372457)
 [test_autoplay_policy.html]
 skip-if = android_version == '23' # bug 1424903
 [test_autoplay_policy_activation.html]
 skip-if = android_version == '23' # bug 1424903
 [test_autoplay_policy_unmute_pauses.html]
 skip-if = android_version == '23' # bug 1424903
+[test_autoplay_policy_navigation.html]
+skip-if = android_version == '23' # bug 1424903
 [test_buffered.html]
 skip-if = android_version == '15' || android_version == '22' # bug 1308388, android(bug 1232305)
 [test_bug448534.html]
 [test_bug463162.xhtml]
 [test_bug465498.html]
 skip-if = toolkit == 'android' # android(bug 1232305)
 [test_bug495145.html]
 skip-if = (os == 'mac' && os_version == '10.6') || (toolkit == 'android')  # bug 1311229, android(bug 1232305)
new file mode 100644
--- /dev/null
+++ b/dom/media/test/test_autoplay_policy_navigation.html
@@ -0,0 +1,271 @@
+<!DOCTYPE HTML>
+<html>
+  <head>
+    <title>Autoplay policy test</title>
+    <script type="text/javascript" src="/tests/SimpleTest/SimpleTest.js"></script>
+    <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css" />
+    <script type="text/javascript" src="/tests/SimpleTest/EventUtils.js"></script>
+    <script type="text/javascript" src="manifest.js"></script>
+    <script type="text/javascript" src="AutoplayTestUtils.js"></script>
+  </head>
+  <body>
+    <pre id="test">
+      <script>
+
+        // Tests that videos can only play audibly in windows/frames
+        // which have been activated by same-origin user gesture.
+
+        gTestPrefs.push(["media.autoplay.enabled", false],
+                        ["media.autoplay.enabled.user-gestures-needed", true]);
+
+        SpecialPowers.pushPrefEnv({'set': gTestPrefs}, () => {
+          runTest();
+        });
+
+        let test_cases = [
+          {
+            name: "Activated document navigating same origin should be able to play after navigation.",
+            actions: [Activate, NavigateSameOrigin],
+            should_play: true,
+          },
+          {
+            name: "Unactivated document navigating same origin should not be able to play after navigation.",
+            actions: [NavigateSameOrigin],
+            should_play: false,
+          },
+          {
+            name: "Activated document navigating cross origin should not be able to play after navigation.",
+            actions: [Activate, NavigateCrossOrigin],
+            should_play: false,
+          },
+          {
+            name: "Unactivated document navigating cross origin should not be able to play after navigation.",
+            actions: [NavigateCrossOrigin],
+            should_play: false,
+          },
+          {
+            name: "Unactivated document after reloading should still be blocked.",
+            actions: [Reload],
+            should_play: false,
+          },
+          {
+            name: "Activated document reloading should still be able to play after reload.",
+            actions: [Activate, Reload],
+            should_play: true,
+          },
+
+          // busted 
+          {
+            name: "Navigating cross origin back to an activated document should still be able to play.",
+            actions: [Activate, NavigateCrossOrigin, NavigateBack],
+            should_play: true,
+          },
+
+          {
+            name: "Navigating same origin back to an activated document should still be able to play.",
+            actions: [Activate, NavigateSameOrigin, NavigateBack],
+            should_play: true,
+          },
+          
+          {
+            name: "Navigating cross origin and activate should play.",
+            actions: [NavigateCrossOrigin, Activate],
+            should_play: true,
+          },
+
+          // busted
+          {
+            name: "Navigating cross origin forward to a previously activated document should still be able to play.",
+            actions: [NavigateCrossOrigin, Activate, NavigateBack, NavigateForward],
+            should_play: true,
+          },
+
+        ];
+
+        function NavigationHistory() {
+
+          this.stack = [];
+          this.index = -1;
+
+          this.push = function(url) {
+            this.stack.push(url);
+            this.index += 1;
+          }
+
+          this.forward = function() {
+            if (this.index + 1 < this.stack.length) {
+              this.index++;
+            }
+            return this.stack[this.index];
+          }
+
+          this.back = function() {
+            if (this.index - 1 >= 0) {
+              this.index--;
+            }
+            return this.stack[this.index];
+          }
+        }
+
+        const same_origin = "http://mochi.test:8888";
+        const cross_origin = "http://example.org";
+        const path = "/tests/dom/media/test/file_autoplay_policy_navigation_window.html";
+        const same_origin_url = same_origin + path;
+        const cross_origin_url = cross_origin + path;
+
+
+        function nextNamedMessage(name) {
+          return new Promise(function(resolve, reject) {
+            let f = (event) => {
+              if (event.data == name) {
+                window.removeEventListener("message", f, false);
+                resolve(event);
+              }
+            };
+            window.addEventListener("message", f, false);
+          });
+        }
+
+        function nextReadyMessage() {
+          return nextNamedMessage("ready");
+        }
+
+        function nextPageShowMessage() {
+          return nextNamedMessage("pageshow");
+        }
+
+        function nextClickedMessage() {
+          return nextNamedMessage("clicked"); 
+        }
+
+        function nextPlayedOrBlockedMessage() {
+          return new Promise(function(resolve, reject) {
+            let f = (event) => {
+              dump("nextPlayedMessage event.data.played=" + event.data.played + "\n");
+              if (event.data.played != undefined) {
+                window.removeEventListener("message", f, false);
+                resolve(event);
+              }
+            };
+            window.addEventListener("message", f, false);
+          });
+        }
+
+        async function runTest() {
+          try {           
+            for (test_case of test_cases) {
+              log("case '" + test_case.name + "'");
+              // Run each test in a new window, to ensure its user gesture
+              // activation state isn't tainted by preceeding tests.
+              let navigationHistory = new NavigationHistory();
+              let counter = 1;
+              
+              let child = window.open(same_origin_url, "", "width=500,height=500");
+              child.addEventListener('load', ()=>{log("child load");}, false);
+              navigationHistory.push(same_origin_url);
+              await nextReadyMessage();
+              log("Got window message");
+              // await once(child, "load");
+
+              // Perform test case's actions.
+              for (let action of test_case.actions) {
+                switch (action) {
+                  case Activate: {
+                    log("Activate")
+                    child.postMessage("click", "*");
+                    await nextClickedMessage();
+                    log("Activate complete");
+                    break;
+                  }
+                  case NavigateSameOrigin: {
+                    log("NavigateSameOrigin")
+                    let uri = same_origin_url + "?" + counter++;
+                    SpecialPowers.wrap(child).location = uri;
+                    navigationHistory.push(uri);
+                    log("Awaiting loaded")
+                    // await nextPageShowMessage();
+                    await nextReadyMessage();
+                    log("NavigateSameOrigin complete");
+                    break;
+                  }
+                  case NavigateCrossOrigin: {
+                    log("NavigateCrossOrigin")
+                    let uri = cross_origin_url + "?" + counter++;
+                    // let p = once(child, "load");
+                    child.location = uri;
+                    navigationHistory.push(uri);
+                    // await once(child, "load");
+                    log("Awaiting loaded")
+                    // await p;
+                    // await nextPageShowMessage();
+                    await nextReadyMessage();
+                    log("NavigateCrossOrigin complete");
+                    break;
+                  }
+                  case NavigateBack: {
+                    log("NavigateBack")
+                    // log("going back");
+                    // document.body.addEventListener("load", (e)=>{
+                    //   log("frame load");
+                    // }, true);
+
+                    child.postMessage("navigate-back", "*");
+
+                    log("child.location=" + SpecialPowers.wrap(child).location);
+                    // SpecialPowers.wrap(child).history.back();
+                    let uri = navigationHistory.back();
+                    log("Awaiting location change");
+                    // await locationChangedTo(child, uri);
+                    await nextPageShowMessage();
+                    log("child.location=" + SpecialPowers.wrap(child).location);
+                    // log("awaiting go back complete");
+                    // await once(frame, "load");
+                    // log("finished going back");
+                    // await awhile();
+                    log("NavigateBack complete");
+                    break;
+                  }
+                  case NavigateForward: {
+                    log("NavigateForward")                
+                    SpecialPowers.wrap(child).history.forward();
+                    let uri = navigationHistory.forward();
+                    log("Awaiting location change to " + uri);
+                    // await nextWindowMessage();
+                    await nextPageShowMessage();
+                    // await locationChangedTo(child, uri);
+                    log("NavigateForward complete");
+                    break;
+                  }
+                  case Reload: {
+                    log("Reload")                
+                    child.location.reload(true);
+                    await nextReadyMessage();
+                    // await once(child, "load");
+                    // await nextPageShowMessage();
+                    log("Reload complete");
+                    break;
+                  }
+                }
+              }
+
+              let played = nextPlayedOrBlockedMessage();
+              child.postMessage("play-audible", "*");
+              dump("Awaiting p\n");
+              played = await played;
+              dump("Awaited p complete\n");
+
+              SimpleTest.is(played.data.played, test_case.should_play, test_case.name);
+              child.close();
+            }
+          } catch (ex) {
+            log("exception " + ex.message);
+          }
+          SimpleTest.finish();
+        }
+        
+        SimpleTest.waitForExplicitFinish();
+
+      </script>
+    </pre>
+  </body>
+</html>