Portals: Expose portalHost in predecessor

Exposing it in the activate callback ensures that the portalHost is
exposed when the promise callback is executed.

Bug: 948118, 921776
Change-Id: Ie654fdefd3e75187a06e8c06eba3de52f8276ff1
Reviewed-on: https://chromium-review.googlesource.com/c/chromium/src/+/1548316
Reviewed-by: Nasko Oskov <[email protected]>
Reviewed-by: Daniel Cheng <[email protected]>
Reviewed-by: Jeremy Roman <[email protected]>
Reviewed-by: Lucas Gadani <[email protected]>
Commit-Queue: Adithya Srinivasan <[email protected]>
Cr-Commit-Position: refs/heads/master@{#652383}
diff --git a/content/browser/frame_host/render_frame_host_impl.cc b/content/browser/frame_host/render_frame_host_impl.cc
index 7f26611..e0feb2d 100644
--- a/content/browser/frame_host/render_frame_host_impl.cc
+++ b/content/browser/frame_host/render_frame_host_impl.cc
@@ -1048,9 +1048,10 @@
 void RenderFrameHostImpl::OnPortalActivated(
     const base::UnguessableToken& portal_token,
     blink::mojom::PortalAssociatedPtrInfo portal,
-    blink::TransferableMessage data) {
-  GetNavigationControl()->OnPortalActivated(portal_token, std::move(portal),
-                                            std::move(data));
+    blink::TransferableMessage data,
+    base::OnceCallback<void(bool)> callback) {
+  GetNavigationControl()->OnPortalActivated(
+      portal_token, std::move(portal), std::move(data), std::move(callback));
 }
 
 void RenderFrameHostImpl::ForwardMessageToPortalHost(
diff --git a/content/browser/frame_host/render_frame_host_impl.h b/content/browser/frame_host/render_frame_host_impl.h
index 97eb8f9..3186e56 100644
--- a/content/browser/frame_host/render_frame_host_impl.h
+++ b/content/browser/frame_host/render_frame_host_impl.h
@@ -869,7 +869,8 @@
   // PortalActivateEvent.
   void OnPortalActivated(const base::UnguessableToken& portal_token,
                          blink::mojom::PortalAssociatedPtrInfo portal,
-                         blink::TransferableMessage data);
+                         blink::TransferableMessage data,
+                         base::OnceCallback<void(bool)> callback);
 
   // Called on the main frame of a page embedded in a Portal to forward a
   // message to the PortalHost object in the frame.
diff --git a/content/browser/portal/portal.cc b/content/browser/portal/portal.cc
index 55dcb5cf..d7faeb9 100644
--- a/content/browser/portal/portal.cc
+++ b/content/browser/portal/portal.cc
@@ -131,7 +131,7 @@
 }
 
 void Portal::Activate(blink::TransferableMessage data,
-                      base::OnceCallback<void()> callback) {
+                      ActivateCallback callback) {
   WebContentsImpl* outer_contents = static_cast<WebContentsImpl*>(
       WebContents::FromRenderFrameHost(owner_render_frame_host_));
 
@@ -163,8 +163,8 @@
   portal->SetPortalContents(std::move(predecessor_web_contents));
 
   portal_contents_impl_->GetMainFrame()->OnPortalActivated(
-      portal->portal_token_, portal_ptr.PassInterface(), std::move(data));
-  std::move(callback).Run();
+      portal->portal_token_, portal_ptr.PassInterface(), std::move(data),
+      std::move(callback));
 }
 
 void Portal::PostMessage(blink::TransferableMessage message,
diff --git a/content/browser/portal/portal.h b/content/browser/portal/portal.h
index 5829af8..98b94182 100644
--- a/content/browser/portal/portal.h
+++ b/content/browser/portal/portal.h
@@ -56,7 +56,7 @@
   // blink::mojom::Portal implementation.
   void Navigate(const GURL& url) override;
   void Activate(blink::TransferableMessage data,
-                base::OnceCallback<void()> callback) override;
+                ActivateCallback callback) override;
   void PostMessage(const blink::TransferableMessage message,
                    const base::Optional<url::Origin>& target_origin) override;
 
diff --git a/content/browser/portal/portal_browsertest.cc b/content/browser/portal/portal_browsertest.cc
index 07cdd163a..52f05325 100644
--- a/content/browser/portal/portal_browsertest.cc
+++ b/content/browser/portal/portal_browsertest.cc
@@ -46,7 +46,7 @@
   static PortalInterceptorForTesting* From(content::Portal* portal);
 
   void Activate(blink::TransferableMessage data,
-                base::OnceCallback<void()> callback) override {
+                ActivateCallback callback) override {
     portal_activated_ = true;
 
     if (run_loop_) {
diff --git a/content/common/frame.mojom b/content/common/frame.mojom
index b2082e2..7deab3e 100644
--- a/content/common/frame.mojom
+++ b/content/common/frame.mojom
@@ -225,10 +225,12 @@
   // activated. The frame has the option to adopt the previous page as a portal
   // identified by |portal_token| with the interface |portal|. The activation
   // can optionally include a message |data| dispatched with the
-  // PortalActivateEvent.
+  // PortalActivateEvent. The return value |was_adopted| indicates if the portal
+  // for the predecessor (identified by |portal_token|) was adopted by the
+  // current frame.
   OnPortalActivated(mojo_base.mojom.UnguessableToken portal_token,
                     associated blink.mojom.Portal portal,
-                    blink.mojom.TransferableMessage data);
+                    blink.mojom.TransferableMessage data) => (bool was_adopted);
 };
 
 // Implemented by the frame (e.g. renderer processes).
diff --git a/content/renderer/render_frame_impl.cc b/content/renderer/render_frame_impl.cc
index 8813916..c4321ec 100644
--- a/content/renderer/render_frame_impl.cc
+++ b/content/renderer/render_frame_impl.cc
@@ -2667,8 +2667,10 @@
 void RenderFrameImpl::OnPortalActivated(
     const base::UnguessableToken& portal_token,
     blink::mojom::PortalAssociatedPtrInfo portal,
-    blink::TransferableMessage data) {
-  frame_->OnPortalActivated(portal_token, portal.PassHandle(), std::move(data));
+    blink::TransferableMessage data,
+    OnPortalActivatedCallback callback) {
+  frame_->OnPortalActivated(portal_token, portal.PassHandle(), std::move(data),
+                            std::move(callback));
 }
 
 void RenderFrameImpl::ForwardMessageToPortalHost(
diff --git a/content/renderer/render_frame_impl.h b/content/renderer/render_frame_impl.h
index 15ef0dce..4613ce47 100644
--- a/content/renderer/render_frame_impl.h
+++ b/content/renderer/render_frame_impl.h
@@ -648,7 +648,8 @@
       JavaScriptExecuteRequestInIsolatedWorldCallback callback) override;
   void OnPortalActivated(const base::UnguessableToken& portal_token,
                          blink::mojom::PortalAssociatedPtrInfo portal,
-                         blink::TransferableMessage data) override;
+                         blink::TransferableMessage data,
+                         OnPortalActivatedCallback callback) override;
 
   // mojom::FullscreenVideoElementHandler implementation:
   void RequestFullscreenVideoElement() override;
diff --git a/third_party/blink/public/mojom/portal/portal.mojom b/third_party/blink/public/mojom/portal/portal.mojom
index 6d414c8..4819ef2d 100644
--- a/third_party/blink/public/mojom/portal/portal.mojom
+++ b/third_party/blink/public/mojom/portal/portal.mojom
@@ -15,7 +15,7 @@
   Navigate(url.mojom.Url url);
 
   // When a portal is activated, it'll replace the current tab with the portal.
-  Activate(TransferableMessage data) => ();
+  Activate(TransferableMessage data) => (bool was_adopted);
 
   // Sends message to the browser process, where it can be forwarded to the
   // portal's main frame.
diff --git a/third_party/blink/public/web/web_local_frame.h b/third_party/blink/public/web/web_local_frame.h
index fbfaf4b..6b17777 100644
--- a/third_party/blink/public/web/web_local_frame.h
+++ b/third_party/blink/public/web/web_local_frame.h
@@ -631,10 +631,12 @@
   // portal's unique identifier, and the message pipe |portal_pipe| is the
   // portal's mojo interface. |data| is an optional message sent together with
   // the portal's activation.
+  using OnPortalActivatedCallback = base::OnceCallback<void(bool)>;
   virtual void OnPortalActivated(
       const base::UnguessableToken& portal_token,
       mojo::ScopedInterfaceEndpointHandle portal_pipe,
-      TransferableMessage data) = 0;
+      TransferableMessage data,
+      OnPortalActivatedCallback callback) = 0;
 
   // Forwards message to the PortalHost associated with frame.
   virtual void ForwardMessageToPortalHost(
diff --git a/third_party/blink/renderer/core/events/portal_activate_event.cc b/third_party/blink/renderer/core/events/portal_activate_event.cc
index 27bdeed..ad1d0b3 100644
--- a/third_party/blink/renderer/core/events/portal_activate_event.cc
+++ b/third_party/blink/renderer/core/events/portal_activate_event.cc
@@ -23,11 +23,13 @@
     const base::UnguessableToken& predecessor_portal_token,
     mojom::blink::PortalAssociatedPtr predecessor_portal_ptr,
     scoped_refptr<SerializedScriptValue> data,
-    MessagePortArray* ports) {
+    MessagePortArray* ports,
+    OnPortalActivatedCallback callback) {
   return MakeGarbageCollected<PortalActivateEvent>(
       frame->GetDocument(), predecessor_portal_token,
       std::move(predecessor_portal_ptr),
-      SerializedScriptValue::Unpack(std::move(data)), ports);
+      SerializedScriptValue::Unpack(std::move(data)), ports,
+      std::move(callback));
 }
 
 PortalActivateEvent* PortalActivateEvent::Create(
@@ -41,7 +43,8 @@
     const base::UnguessableToken& predecessor_portal_token,
     mojom::blink::PortalAssociatedPtr predecessor_portal_ptr,
     UnpackedSerializedScriptValue* data,
-    MessagePortArray* ports)
+    MessagePortArray* ports,
+    OnPortalActivatedCallback callback)
     : Event(event_type_names::kPortalactivate,
             Bubbles::kNo,
             Cancelable::kNo,
@@ -50,7 +53,8 @@
       predecessor_portal_token_(predecessor_portal_token),
       predecessor_portal_ptr_(std::move(predecessor_portal_ptr)),
       data_(data),
-      ports_(ports) {}
+      ports_(ports),
+      on_portal_activated_callback_(std::move(callback)) {}
 
 PortalActivateEvent::PortalActivateEvent(const AtomicString& type,
                                          const PortalActivateEventInit* init)
@@ -120,7 +124,15 @@
   HTMLPortalElement* portal = MakeGarbageCollected<HTMLPortalElement>(
       *document_, predecessor_portal_token_,
       std::move(predecessor_portal_ptr_));
+  std::move(on_portal_activated_callback_).Run(true);
   return portal;
 }
 
+void PortalActivateEvent::DetachPortalIfNotAdopted() {
+  if (predecessor_portal_ptr_) {
+    std::move(on_portal_activated_callback_).Run(false);
+    predecessor_portal_ptr_.reset();
+  }
+}
+
 }  // namespace blink
diff --git a/third_party/blink/renderer/core/events/portal_activate_event.h b/third_party/blink/renderer/core/events/portal_activate_event.h
index 117a8c6..5a53b69 100644
--- a/third_party/blink/renderer/core/events/portal_activate_event.h
+++ b/third_party/blink/renderer/core/events/portal_activate_event.h
@@ -26,6 +26,7 @@
 class PortalActivateEventInit;
 class ScriptState;
 class ScriptValue;
+using OnPortalActivatedCallback = base::OnceCallback<void(bool)>;
 
 class CORE_EXPORT PortalActivateEvent : public Event {
   DEFINE_WRAPPERTYPEINFO();
@@ -37,7 +38,8 @@
       const base::UnguessableToken& predecessor_portal_token,
       mojom::blink::PortalAssociatedPtr predecessor_portal_ptr,
       scoped_refptr<SerializedScriptValue> data,
-      MessagePortArray* ports);
+      MessagePortArray* ports,
+      OnPortalActivatedCallback callback);
 
   // Web-exposed and called directly by authors.
   static PortalActivateEvent* Create(const AtomicString& type,
@@ -47,7 +49,8 @@
                       const base::UnguessableToken& predecessor_portal_token,
                       mojom::blink::PortalAssociatedPtr predecessor_portal_ptr,
                       UnpackedSerializedScriptValue* data,
-                      MessagePortArray*);
+                      MessagePortArray*,
+                      OnPortalActivatedCallback callback);
   PortalActivateEvent(const AtomicString& type, const PortalActivateEventInit*);
 
   ~PortalActivateEvent() override;
@@ -61,6 +64,8 @@
   ScriptValue data(ScriptState*);
   HTMLPortalElement* adoptPredecessor(ExceptionState& exception_state);
 
+  void DetachPortalIfNotAdopted();
+
  private:
   Member<Document> document_;
   base::UnguessableToken predecessor_portal_token_;
@@ -77,6 +82,7 @@
   // |data_from_init_|.
   HeapHashMap<WeakMember<ScriptState>, TraceWrapperV8Reference<v8::Value>>
       v8_data_;
+  OnPortalActivatedCallback on_portal_activated_callback_;
 };
 
 }  // namespace blink
diff --git a/third_party/blink/renderer/core/frame/web_local_frame_impl.cc b/third_party/blink/renderer/core/frame/web_local_frame_impl.cc
index c572b6f76..5161840 100644
--- a/third_party/blink/renderer/core/frame/web_local_frame_impl.cc
+++ b/third_party/blink/renderer/core/frame/web_local_frame_impl.cc
@@ -2571,7 +2571,8 @@
 void WebLocalFrameImpl::OnPortalActivated(
     const base::UnguessableToken& portal_token,
     mojo::ScopedInterfaceEndpointHandle portal_pipe,
-    TransferableMessage data) {
+    TransferableMessage data,
+    OnPortalActivatedCallback callback) {
   GetFrame()->GetPage()->SetInsidePortal(false);
 
   LocalDOMWindow* window = GetFrame()->DomWindow();
@@ -2587,7 +2588,7 @@
       frame_.Get(), portal_token,
       mojom::blink::PortalAssociatedPtr(mojom::blink::PortalAssociatedPtrInfo(
           std::move(portal_pipe), mojom::blink::Portal::Version_)),
-      std::move(blink_data.message), ports);
+      std::move(blink_data.message), ports, std::move(callback));
 
   ThreadDebugger* debugger = MainThreadDebugger::Instance();
   if (debugger)
@@ -2595,6 +2596,7 @@
   GetFrame()->DomWindow()->DispatchEvent(*event);
   if (debugger)
     debugger->ExternalAsyncTaskFinished(blink_data.sender_stack_trace_id);
+  event->DetachPortalIfNotAdopted();
 }
 
 void WebLocalFrameImpl::ForwardMessageToPortalHost(
diff --git a/third_party/blink/renderer/core/frame/web_local_frame_impl.h b/third_party/blink/renderer/core/frame/web_local_frame_impl.h
index 06f3aee..04874b6 100644
--- a/third_party/blink/renderer/core/frame/web_local_frame_impl.h
+++ b/third_party/blink/renderer/core/frame/web_local_frame_impl.h
@@ -306,7 +306,8 @@
                                 const WebMediaPlayerAction&) override;
   void OnPortalActivated(const base::UnguessableToken& portal_token,
                          mojo::ScopedInterfaceEndpointHandle portal_pipe,
-                         TransferableMessage data) override;
+                         TransferableMessage data,
+                         OnPortalActivatedCallback callback) override;
   void ForwardMessageToPortalHost(
       TransferableMessage message,
       const WebSecurityOrigin& source_origin,
diff --git a/third_party/blink/renderer/core/html/portal/html_portal_element.cc b/third_party/blink/renderer/core/html/portal/html_portal_element.cc
index 21566f1..ab3210d 100644
--- a/third_party/blink/renderer/core/html/portal/html_portal_element.cc
+++ b/third_party/blink/renderer/core/html/portal/html_portal_element.cc
@@ -214,17 +214,20 @@
   // PortalPtr stays alive until the callback is called.
   is_activating_ = true;
   auto* raw_portal_ptr = portal_ptr_.get();
-  raw_portal_ptr->Activate(std::move(data),
-                           WTF::Bind(
-                               [](HTMLPortalElement* portal,
-                                  mojom::blink::PortalAssociatedPtr portal_ptr,
-                                  ScriptPromiseResolver* resolver) {
-                                 resolver->Resolve();
-                                 portal->is_activating_ = false;
-                                 portal->ConsumePortal();
-                               },
-                               WrapPersistent(this), std::move(portal_ptr_),
-                               WrapPersistent(resolver)));
+  raw_portal_ptr->Activate(
+      std::move(data),
+      WTF::Bind(
+          [](HTMLPortalElement* portal,
+             mojom::blink::PortalAssociatedPtr portal_ptr,
+             ScriptPromiseResolver* resolver, bool was_adopted) {
+            if (was_adopted)
+              portal->GetDocument().GetPage()->SetInsidePortal(true);
+            resolver->Resolve();
+            portal->is_activating_ = false;
+            portal->ConsumePortal();
+          },
+          WrapPersistent(this), std::move(portal_ptr_),
+          WrapPersistent(resolver)));
   return promise;
 }
 
diff --git a/third_party/blink/web_tests/external/wpt/portals/portal-activate-data.html b/third_party/blink/web_tests/external/wpt/portals/portal-activate-data.html
index 0d8ec33..d352826 100644
--- a/third_party/blink/web_tests/external/wpt/portals/portal-activate-data.html
+++ b/third_party/blink/web_tests/external/wpt/portals/portal-activate-data.html
@@ -19,8 +19,9 @@
     portal.src = new URL('resources/portal-activate-data-portal.html?logic=' + encodeURIComponent(logic), location.href);
     w.document.body.appendChild(portal);
     assert_equals((await nextMessage(bc)).data, 'ready');
+    let replyPromise = nextMessage(bc);
     await portal.activate(activateOptions);
-    return (await nextMessage(bc)).data;
+    return (await replyPromise).data;
   } finally {
     w.close();
     bc.close();
diff --git a/third_party/blink/web_tests/external/wpt/portals/portals-activate-resolution.html b/third_party/blink/web_tests/external/wpt/portals/portals-activate-resolution.html
new file mode 100644
index 0000000..9fb99e42
--- /dev/null
+++ b/third_party/blink/web_tests/external/wpt/portals/portals-activate-resolution.html
@@ -0,0 +1,21 @@
+<!DOCTYPE html>
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+<script>
+  promise_test(async () => {
+    var win = window.open();
+    var portal = win.document.createElement("portal");
+    portal.src = new URL("resources/simple-portal.html", location.href)
+
+    await new Promise((resolve, reject) => {
+      var bc = new BroadcastChannel("simple-portal");
+      bc.onmessage = () => {
+        bc.close();
+        resolve();
+      }
+      win.document.body.appendChild(portal);
+    });
+
+    return portal.activate();
+  });
+</script>
diff --git a/third_party/blink/web_tests/external/wpt/portals/portals-adopt-predecessor.html b/third_party/blink/web_tests/external/wpt/portals/portals-adopt-predecessor.html
index 99c44f0..27e4052 100644
--- a/third_party/blink/web_tests/external/wpt/portals/portals-adopt-predecessor.html
+++ b/third_party/blink/web_tests/external/wpt/portals/portals-adopt-predecessor.html
@@ -10,7 +10,6 @@
       assert_equals(e.data, "passed");
       bc.close();
     });
-    var portalUrl = encodeURIComponent(`portal-activate-event-portal.html?test=${test}`);
     window.open(`resources/portals-adopt-predecessor.html?test=${test}`);
   }, "Tests that a portal can adopt its predecessor.");
 
@@ -21,7 +20,6 @@
       assert_equals(e.data, "passed");
       bc.close();
     });
-    var portalUrl = encodeURIComponent(`portal-activate-event-portal.html?test=${test}`);
     window.open(`resources/portals-adopt-predecessor.html?test=${test}`);
   }, "Tests that trying to adopt the predecessor twice will throw an exception.");
 </script>
diff --git a/third_party/blink/web_tests/external/wpt/portals/portals-create-orphaned.html b/third_party/blink/web_tests/external/wpt/portals/portals-create-orphaned.html
deleted file mode 100644
index 903186f..0000000
--- a/third_party/blink/web_tests/external/wpt/portals/portals-create-orphaned.html
+++ /dev/null
@@ -1,19 +0,0 @@
-<!DOCTYPE html>
-<script src="/resources/testharness.js"></script>
-<script src="/resources/testharnessreport.js"></script>
-<body>
-  <script>
-    promise_test(async () => {
-      let waitForMessage = new Promise((resolve, reject) => {
-        var bc = new BroadcastChannel("portals-create-orphaned");
-        bc.onmessage = e => {
-          bc.close();
-          resolve(e.data);
-        }
-      });
-      window.open("resources/portal-create-orphaned.html");
-      let message = await waitForMessage;
-      assert_equals(message, "portal loaded");
-    }, "creating a portal from an orphaned portal should succeed");
-  </script>
-</body>
diff --git a/third_party/blink/web_tests/external/wpt/portals/resources/portal-create-orphaned.html b/third_party/blink/web_tests/external/wpt/portals/resources/portal-create-orphaned.html
deleted file mode 100644
index 89b927f..0000000
--- a/third_party/blink/web_tests/external/wpt/portals/resources/portal-create-orphaned.html
+++ /dev/null
@@ -1,28 +0,0 @@
-<!DOCTYPE html>
-<body>
-  <script>
-    var portal = document.createElement("portal");
-    portal.src = "simple-portal.html";
-    let waitForMessage = new Promise((resolve, reject) => {
-      var bc_portal = new BroadcastChannel("simple-portal");
-      bc_portal.onmessage = e => {
-        bc_portal.close();
-        portal.activate();
-        var portal2 = document.createElement("portal");
-        portal2.src = "simple-portal.html";
-        document.body.appendChild(portal2);
-        var bc2 = new BroadcastChannel("simple-portal");
-        bc2.onmessage = e => {
-          bc2.close();
-          resolve("portal loaded");
-        }
-      }
-    });
-    document.body.appendChild(portal);
-    waitForMessage.then(message => {
-      var bc = new BroadcastChannel("portals-create-orphaned");
-      bc.postMessage(message);
-      bc.close();
-    });
-  </script>
-</body>
diff --git a/third_party/blink/web_tests/external/wpt/portals/resources/portals-adopt-predecessor-portal.html b/third_party/blink/web_tests/external/wpt/portals/resources/portals-adopt-predecessor-portal.html
index 96de3b7..14d1018 100644
--- a/third_party/blink/web_tests/external/wpt/portals/resources/portals-adopt-predecessor-portal.html
+++ b/third_party/blink/web_tests/external/wpt/portals/resources/portals-adopt-predecessor-portal.html
@@ -2,7 +2,6 @@
 <script>
   var searchParams = new URL(location).searchParams;
   var test = searchParams.get("test");
-  var bc = new BroadcastChannel(`portal-${test}`);
 
   window.onportalactivate = function(e) {
     var portal = e.adoptPredecessor();
@@ -10,19 +9,19 @@
 
     if (test == "adopt-once") {
       if (portal instanceof HTMLPortalElement) {
-        bc.postMessage("passed");
-        bc.close();
+        portal.postMessage("adopted", "*");
       }
     }
     if (test == "adopt-twice") {
       try {
-        portal = e.adoptPredecessor();
+        e.adoptPredecessor();
       } catch(e) {
-        bc.postMessage("passed");
-        bc.close();
+        portal.postMessage("passed", "*");
       }
     }
   }
 
+  var bc = new BroadcastChannel(`portal-${test}`);
   bc.postMessage("loaded");
+  bc.close();
 </script>
diff --git a/third_party/blink/web_tests/external/wpt/portals/resources/portals-adopt-predecessor.html b/third_party/blink/web_tests/external/wpt/portals/resources/portals-adopt-predecessor.html
index b92ad8a..287ba2c3 100644
--- a/third_party/blink/web_tests/external/wpt/portals/resources/portals-adopt-predecessor.html
+++ b/third_party/blink/web_tests/external/wpt/portals/resources/portals-adopt-predecessor.html
@@ -5,18 +5,15 @@
   var searchParams = new URL(location).searchParams;
   var test = searchParams.get("test");
   var bc = new BroadcastChannel(`portal-${test}`);
-  bc.onmessage = function(e) {
-    switch (e.data) {
-      case "loaded":
-        document.querySelector("portal").activate();
-        break;
-
-      case "passed":
-        bc.close();
+  bc.onmessage = e => {
+    bc.close();
+    document.querySelector("portal").activate().then(() => {
+      window.portalHost.addEventListener("message", () => {
         var bc_test = new BroadcastChannel(`test-${test}`);
         bc_test.postMessage("passed");
         bc_test.close();
-    }
+      });
+    });
   }
 
   var portal = document.createElement("portal");
diff --git a/third_party/blink/web_tests/wpt_internal/portals/portals-create-orphaned.html b/third_party/blink/web_tests/wpt_internal/portals/portals-create-orphaned.html
new file mode 100644
index 0000000..0a3a66f
--- /dev/null
+++ b/third_party/blink/web_tests/wpt_internal/portals/portals-create-orphaned.html
@@ -0,0 +1,29 @@
+<!DOCTYPE html>
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+<body>
+  <script>
+    function createPortal(doc, src, channel) {
+      var portal = doc.createElement("portal");
+      portal.src = new URL(src, location.href);
+      return new Promise((resolve, reject) => {
+        var bc = new BroadcastChannel(channel);
+        bc.onmessage = () => {
+          bc.close();
+          resolve(portal);
+        }
+        doc.body.appendChild(portal);
+      });
+    }
+
+    promise_test(async () => {
+      var w = window.open();
+      var doc = w.document;
+      var portal = await createPortal(doc,
+          "resources/portals-create-orphaned-portal.html",
+          "create-orphaned-portal");
+      portal.activate();
+      return createPortal(doc, "resources/simple-portal.html", "simple-portal");
+    }, "creating a portal from an orphaned portal should succeed");
+  </script>
+</body>
diff --git a/third_party/blink/web_tests/wpt_internal/portals/resources/portals-create-orphaned-portal.html b/third_party/blink/web_tests/wpt_internal/portals/resources/portals-create-orphaned-portal.html
new file mode 100644
index 0000000..ff18d5e
--- /dev/null
+++ b/third_party/blink/web_tests/wpt_internal/portals/resources/portals-create-orphaned-portal.html
@@ -0,0 +1,11 @@
+<!DOCTYPE html>
+<script>
+  window.onportalactivate = e => {
+    // Busy-loop to keep the predecessor in the "orphaned" state. This only
+    // works if we create portals in a separate process.
+    while (true) {}
+  }
+  var bc = new BroadcastChannel("create-orphaned-portal");
+  bc.postMessage("loaded");
+  bc.close();
+</script>
diff --git a/third_party/blink/web_tests/wpt_internal/portals/resources/simple-portal.html b/third_party/blink/web_tests/wpt_internal/portals/resources/simple-portal.html
new file mode 100644
index 0000000..957a8f2
--- /dev/null
+++ b/third_party/blink/web_tests/wpt_internal/portals/resources/simple-portal.html
@@ -0,0 +1,6 @@
+<!DOCTYPE html>
+<script>
+  var bc = new BroadcastChannel("simple-portal");
+  bc.postMessage("loaded");
+  bc.close();
+</script>