Add a guestinstance attribute to webviews reflecting their current guest
authorDave Townsend <dtownsend@oxymoronical.com>
Thu, 8 Sep 2016 17:01:01 +0000 (10:01 -0700)
committerDave Townsend <dtownsend@oxymoronical.com>
Fri, 9 Sep 2016 17:32:05 +0000 (10:32 -0700)
instance ID and allowing moving a guest instance to a new webview.

12 files changed:
atom/browser/api/atom_api_web_contents.cc
atom/browser/api/atom_api_web_contents.h
atom/renderer/api/atom_api_web_frame.cc
atom/renderer/api/atom_api_web_frame.h
docs/api/web-view-tag.md
lib/browser/guest-view-manager.js
lib/renderer/web-view/guest-view-internal.js
lib/renderer/web-view/web-view-attributes.js
lib/renderer/web-view/web-view-constants.js
lib/renderer/web-view/web-view.js
spec/fixtures/pages/webview-move-to-window.html [new file with mode: 0644]
spec/webview-spec.js

index cfb1e7f..5390661 100644 (file)
@@ -1433,6 +1433,25 @@ content::WebContents* WebContents::HostWebContents() {
   return embedder_->web_contents();
 }
 
+void WebContents::SetEmbedder(const WebContents* embedder) {
+  if (embedder) {
+    NativeWindow* owner_window = nullptr;
+    auto relay = NativeWindowRelay::FromWebContents(embedder->web_contents());
+    if (relay) {
+      owner_window = relay->window.get();
+    }
+    if (owner_window)
+      SetOwnerWindow(owner_window);
+
+    content::RenderWidgetHostView* rwhv =
+        web_contents()->GetRenderWidgetHostView();
+    if (rwhv) {
+      rwhv->Hide();
+      rwhv->Show();
+    }
+  }
+}
+
 v8::Local<v8::Value> WebContents::DevToolsWebContents(v8::Isolate* isolate) {
   if (devtools_web_contents_.IsEmpty())
     return v8::Null(isolate);
@@ -1529,6 +1548,7 @@ void WebContents::BuildPrototype(v8::Isolate* isolate,
                  &WebContents::ShowDefinitionForSelection)
       .SetMethod("copyImageAt", &WebContents::CopyImageAt)
       .SetMethod("capturePage", &WebContents::CapturePage)
+      .SetMethod("setEmbedder", &WebContents::SetEmbedder)
       .SetProperty("id", &WebContents::ID)
       .SetProperty("session", &WebContents::Session)
       .SetProperty("hostWebContents", &WebContents::HostWebContents)
index a4d50ef..1e55830 100644 (file)
@@ -102,6 +102,7 @@ class WebContents : public mate::TrackableObject<WebContents>,
   void SetAudioMuted(bool muted);
   bool IsAudioMuted();
   void Print(mate::Arguments* args);
+  void SetEmbedder(const WebContents* embedder);
 
   // Print current page as PDF.
   void PrintToPDF(const base::DictionaryValue& setting,
index dd28ad7..ddcea9e 100644 (file)
@@ -110,6 +110,10 @@ void WebFrame::AttachGuest(int id) {
   content::RenderFrame::FromWebFrame(web_frame_)->AttachGuest(id);
 }
 
+void WebFrame::DetachGuest(int id) {
+  content::RenderFrame::FromWebFrame(web_frame_)->DetachGuest(id);
+}
+
 void WebFrame::SetSpellCheckProvider(mate::Arguments* args,
                                      const std::string& language,
                                      bool auto_spell_correct_turned_on,
@@ -202,6 +206,7 @@ void WebFrame::BuildPrototype(
       .SetMethod("registerElementResizeCallback",
                  &WebFrame::RegisterElementResizeCallback)
       .SetMethod("attachGuest", &WebFrame::AttachGuest)
+      .SetMethod("detachGuest", &WebFrame::DetachGuest)
       .SetMethod("setSpellCheckProvider", &WebFrame::SetSpellCheckProvider)
       .SetMethod("registerURLSchemeAsSecure",
                  &WebFrame::RegisterURLSchemeAsSecure)
index 7b2401d..570a9df 100644 (file)
@@ -53,6 +53,7 @@ class WebFrame : public mate::Wrappable<WebFrame> {
       int element_instance_id,
       const GuestViewContainer::ResizeCallback& callback);
   void AttachGuest(int element_instance_id);
+  void DetachGuest(int element_instance_id);
 
   // Set the provider that will be used by SpellCheckClient for spell check.
   void SetSpellCheckProvider(mate::Arguments* args,
index 90153d8..20f9d27 100644 (file)
@@ -214,6 +214,21 @@ A list of strings which specifies the blink features to be disabled separated by
 The full list of supported feature strings can be found in the
 [RuntimeEnabledFeatures.in][blink-feature-string] file.
 
+### `guestinstance`
+
+```html
+<webview src="https://www.github.com/" guestinstance="3"></webview>
+```
+
+A value that links the webview to a specific webContents. When a webview
+first loads a new webContents is created and this attribute is set to its
+instance identifier. Setting this attribute on a new or existing webview
+connects it to the existing webContents that currently renders in a different
+webview.
+
+The existing webview will see the `destroy` event and will then create a new
+webContents when a new url is loaded.
+
 ## Methods
 
 The `webview` tag has the following methods:
index fdd426c..df2589a 100644 (file)
@@ -8,6 +8,7 @@ let webViewManager = null
 
 const supportedWebViewEvents = [
   'load-commit',
+  'did-attach',
   'did-finish-load',
   'did-fail-load',
   'did-frame-finish-load',
@@ -40,10 +41,9 @@ const supportedWebViewEvents = [
   'update-target-url'
 ]
 
-let nextInstanceId = 0
+let nextGuestInstanceId = 0
 const guestInstances = {}
 const embedderElementsMap = {}
-const reverseEmbedderElementsMap = {}
 
 // Moves the last element of array to the first one.
 const moveLastToFirst = function (list) {
@@ -51,8 +51,8 @@ const moveLastToFirst = function (list) {
 }
 
 // Generate guestInstanceId.
-const getNextInstanceId = function () {
-  return ++nextInstanceId
+const getNextGuestInstanceId = function () {
+  return ++nextGuestInstanceId
 }
 
 // Create a new guest instance.
@@ -61,43 +61,21 @@ const createGuest = function (embedder, params) {
     webViewManager = process.atomBinding('web_view_manager')
   }
 
-  const id = getNextInstanceId(embedder)
+  const guestInstanceId = getNextGuestInstanceId(embedder)
   const guest = webContents.create({
     isGuest: true,
     partition: params.partition,
     embedder: embedder
   })
-  guestInstances[id] = {
+  guestInstances[guestInstanceId] = {
     guest: guest,
     embedder: embedder
   }
 
-  // Destroy guest when the embedder is gone or navigated.
-  const destroyEvents = ['will-destroy', 'crashed', 'did-navigate']
-  const destroy = function () {
-    if (guestInstances[id] != null) {
-      destroyGuest(embedder, id)
-    }
-  }
-  for (const event of destroyEvents) {
-    embedder.once(event, destroy)
-
-    // Users might also listen to the crashed event, so we must ensure the guest
-    // is destroyed before users' listener gets called. It is done by moving our
-    // listener to the first one in queue.
-    const listeners = embedder._events[event]
-    if (Array.isArray(listeners)) {
-      moveLastToFirst(listeners)
-    }
-  }
-  guest.once('destroyed', function () {
-    for (const event of destroyEvents) {
-      embedder.removeListener(event, destroy)
-    }
-  })
+  watchEmbedder(embedder)
 
   // Init guest web view after attached.
-  guest.once('did-attach', function () {
+  guest.on('did-attach', function () {
     let opts
     params = this.attachParams
     delete this.attachParams
@@ -133,6 +111,10 @@ const createGuest = function (embedder, params) {
   // Dispatch events to embedder.
   const fn = function (event) {
     guest.on(event, function (_, ...args) {
+      const embedder = getEmbedder(guestInstanceId)
+      if (!embedder) {
+        return
+      }
       embedder.send.apply(embedder, ['ELECTRON_GUEST_VIEW_INTERNAL_DISPATCH_EVENT-' + guest.viewInstanceId, event].concat(args))
     })
   }
@@ -142,35 +124,56 @@ const createGuest = function (embedder, params) {
 
   // Dispatch guest's IPC messages to embedder.
   guest.on('ipc-message-host', function (_, [channel, ...args]) {
+    const embedder = getEmbedder(guestInstanceId)
+    if (!embedder) {
+      return
+    }
     embedder.send.apply(embedder, ['ELECTRON_GUEST_VIEW_INTERNAL_IPC_MESSAGE-' + guest.viewInstanceId, channel].concat(args))
   })
 
   // Autosize.
   guest.on('size-changed', function (_, ...args) {
+    const embedder = getEmbedder(guestInstanceId)
+    if (!embedder) {
+      return
+    }
     embedder.send.apply(embedder, ['ELECTRON_GUEST_VIEW_INTERNAL_SIZE_CHANGED-' + guest.viewInstanceId].concat(args))
   })
 
-  return id
+  return guestInstanceId
 }
 
 // Attach the guest to an element of embedder.
 const attachGuest = function (embedder, elementInstanceId, guestInstanceId, params) {
-  let guest, key, oldGuestInstanceId, ref1, webPreferences
-  guest = guestInstances[guestInstanceId].guest
+  let guest, guestInstance, key, oldKey, oldGuestInstanceId, ref1, webPreferences
 
   // Destroy the old guest when attaching.
   key = (embedder.getId()) + '-' + elementInstanceId
   oldGuestInstanceId = embedderElementsMap[key]
   if (oldGuestInstanceId != null) {
-    // Reattachment to the same guest is not currently supported.
+    // Reattachment to the same guest is just a no-op.
     if (oldGuestInstanceId === guestInstanceId) {
       return
     }
-    if (guestInstances[oldGuestInstanceId] == null) {
-      return
-    }
+
     destroyGuest(embedder, oldGuestInstanceId)
   }
+
+  guestInstance = guestInstances[guestInstanceId]
+  // If this isn't a valid guest instance then do nothing.
+  if (!guestInstance) {
+    return
+  }
+  guest = guestInstance.guest
+
+  // If this guest is already attached to an element then remove it
+  if (guestInstance.elementInstanceId) {
+    oldKey = (guestInstance.embedder.getId()) + '-' + guestInstance.elementInstanceId
+    delete embedderElementsMap[oldKey]
+    webViewManager.removeGuest(guestInstance.embedder, guestInstanceId)
+    guestInstance.embedder.send('ELECTRON_GUEST_VIEW_INTERNAL_DESTROY_GUEST-' + guest.viewInstanceId)
+  }
+
   webPreferences = {
     guestInstanceId: guestInstanceId,
     nodeIntegration: (ref1 = params.nodeintegration) != null ? ref1 : false,
@@ -187,19 +190,67 @@ const attachGuest = function (embedder, elementInstanceId, guestInstanceId, para
   webViewManager.addGuest(guestInstanceId, elementInstanceId, embedder, guest, webPreferences)
   guest.attachParams = params
   embedderElementsMap[key] = guestInstanceId
-  reverseEmbedderElementsMap[guestInstanceId] = key
+
+  guest.setEmbedder(embedder)
+  guestInstance.embedder = embedder
+  guestInstance.elementInstanceId = elementInstanceId
+
+  watchEmbedder(embedder)
 }
 
 // Destroy an existing guest instance.
-const destroyGuest = function (embedder, id) {
-  webViewManager.removeGuest(embedder, id)
-  guestInstances[id].guest.destroy()
-  delete guestInstances[id]
-
-  const key = reverseEmbedderElementsMap[id]
-  if (key != null) {
-    delete reverseEmbedderElementsMap[id]
-    return delete embedderElementsMap[key]
+const destroyGuest = function (embedder, guestInstanceId) {
+  if (!(guestInstanceId in guestInstances)) {
+    return
+  }
+
+  let guestInstance = guestInstances[guestInstanceId]
+  if (embedder !== guestInstance.embedder) {
+    return
+  }
+
+  webViewManager.removeGuest(embedder, guestInstanceId)
+  guestInstance.guest.destroy()
+  delete guestInstances[guestInstanceId]
+
+  const key = embedder.getId() + '-' + guestInstance.elementInstanceId
+  return delete embedderElementsMap[key]
+}
+
+// Once an embedder has had a guest attached we watch it for destruction to
+// destroy any remaining guests.
+const watchedEmbedders = new Set()
+const watchEmbedder = function (embedder) {
+  if (watchedEmbedders.has(embedder)) {
+    return
+  }
+  watchedEmbedders.add(embedder)
+
+  const destroyEvents = ['will-destroy', 'crashed', 'did-navigate']
+  const destroy = function () {
+    for (const guestInstanceId of Object.keys(guestInstances)) {
+      if (guestInstances[guestInstanceId].embedder === embedder) {
+        destroyGuest(embedder, parseInt(guestInstanceId))
+      }
+    }
+
+    for (const event of destroyEvents) {
+      embedder.removeListener(event, destroy)
+    }
+
+    watchedEmbedders.delete(embedder)
+  }
+
+  for (const event of destroyEvents) {
+    embedder.once(event, destroy)
+
+    // Users might also listen to the crashed event, so we must ensure the guest
+    // is destroyed before users' listener gets called. It is done by moving our
+    // listener to the first one in queue.
+    const listeners = embedder._events[event]
+    if (Array.isArray(listeners)) {
+      moveLastToFirst(listeners)
+    }
   }
 }
 
@@ -211,23 +262,24 @@ ipcMain.on('ELECTRON_GUEST_VIEW_MANAGER_ATTACH_GUEST', function (event, elementI
   attachGuest(event.sender, elementInstanceId, guestInstanceId, params)
 })
 
-ipcMain.on('ELECTRON_GUEST_VIEW_MANAGER_DESTROY_GUEST', function (event, id) {
-  destroyGuest(event.sender, id)
+ipcMain.on('ELECTRON_GUEST_VIEW_MANAGER_DESTROY_GUEST', function (event, guestInstanceId) {
+  destroyGuest(event.sender, guestInstanceId)
 })
 
-ipcMain.on('ELECTRON_GUEST_VIEW_MANAGER_SET_SIZE', function (event, id, params) {
-  const guestInstance = guestInstances[id]
+ipcMain.on('ELECTRON_GUEST_VIEW_MANAGER_SET_SIZE', function (event, guestInstanceId, params) {
+  const guestInstance = guestInstances[guestInstanceId]
   return guestInstance != null ? guestInstance.guest.setSize(params) : void 0
 })
 
 // Returns WebContents from its guest id.
-exports.getGuest = function (id) {
-  const guestInstance = guestInstances[id]
+exports.getGuest = function (guestInstanceId) {
+  const guestInstance = guestInstances[guestInstanceId]
   return guestInstance != null ? guestInstance.guest : void 0
 }
 
 // Returns the embedder of the guest.
-exports.getEmbedder = function (id) {
-  const guestInstance = guestInstances[id]
+const getEmbedder = function (guestInstanceId) {
+  const guestInstance = guestInstances[guestInstanceId]
   return guestInstance != null ? guestInstance.embedder : void 0
 }
+exports.getEmbedder = getEmbedder
index fed0275..9947c27 100644 (file)
@@ -7,6 +7,7 @@ var requestId = 0
 
 var WEB_VIEW_EVENTS = {
   'load-commit': ['url', 'isMainFrame'],
+  'did-attach': [],
   'did-finish-load': [],
   'did-fail-load': ['errorCode', 'errorDescription', 'validatedURL', 'isMainFrame'],
   'did-frame-finish-load': ['isMainFrame'],
@@ -62,6 +63,15 @@ var dispatchEvent = function (webView, eventName, eventKey, ...args) {
 
 module.exports = {
   registerEvents: function (webView, viewInstanceId) {
+    ipcRenderer.on('ELECTRON_GUEST_VIEW_INTERNAL_DESTROY_GUEST-' + viewInstanceId, function () {
+      var domEvent
+      webFrame.detachGuest(webView.internalInstanceId)
+      webView.guestInstanceId = undefined
+      webView.reset()
+      domEvent = new Event('destroyed')
+      webView.dispatchEvent(domEvent)
+    })
+
     ipcRenderer.on('ELECTRON_GUEST_VIEW_INTERNAL_DISPATCH_EVENT-' + viewInstanceId, function (event, eventName, ...args) {
       dispatchEvent.apply(null, [webView, eventName, eventName].concat(args))
     })
@@ -85,6 +95,7 @@ module.exports = {
     })
   },
   deregisterEvents: function (viewInstanceId) {
+    ipcRenderer.removeAllListeners('ELECTRON_GUEST_VIEW_INTERNAL_DESTROY_GUEST-' + viewInstanceId)
     ipcRenderer.removeAllListeners('ELECTRON_GUEST_VIEW_INTERNAL_DISPATCH_EVENT-' + viewInstanceId)
     ipcRenderer.removeAllListeners('ELECTRON_GUEST_VIEW_INTERNAL_IPC_MESSAGE-' + viewInstanceId)
     return ipcRenderer.removeAllListeners('ELECTRON_GUEST_VIEW_INTERNAL_SIZE_CHANGED-' + viewInstanceId)
index 01eb777..3bbc833 100644 (file)
@@ -130,6 +130,46 @@ class PartitionAttribute extends WebViewAttribute {
   }
 }
 
+// An attribute that controls the guest instance this webview is connected to
+class GuestInstanceAttribute extends WebViewAttribute {
+  constructor (webViewImpl) {
+    super(webViewConstants.ATTRIBUTE_GUESTINSTANCE, webViewImpl)
+  }
+
+  // Retrieves and returns the attribute's value.
+  getValue () {
+    if (this.webViewImpl.webviewNode.hasAttribute(this.name)) {
+      return parseInt(this.webViewImpl.webviewNode.getAttribute(this.name))
+    }
+    return undefined
+  }
+
+  // Sets the attribute's value.
+  setValue (value) {
+    if (!value) {
+      return this.webViewImpl.webviewNode.removeAttribute(this.name)
+    }
+    if (isNaN(value)) {
+      return
+    }
+    return this.webViewImpl.webviewNode.setAttribute(this.name, value)
+  }
+
+  handleMutation (oldValue, newValue) {
+    if (!newValue) {
+      this.webViewImpl.reset()
+      return
+    }
+
+    const intVal = parseInt(newValue)
+    if (!isNaN(newValue) && remote.getGuestWebContents(intVal)) {
+      this.webViewImpl.attachGuestInstance(intVal)
+    } else {
+      this.setValueIgnoreMutation(oldValue)
+    }
+  }
+}
+
 // Attribute that handles the location and navigation of the webview.
 class SrcAttribute extends WebViewAttribute {
   constructor (webViewImpl) {
@@ -287,6 +327,7 @@ WebViewImpl.prototype.setupWebViewAttributes = function () {
   this.attributes[webViewConstants.ATTRIBUTE_PRELOAD] = new PreloadAttribute(this)
   this.attributes[webViewConstants.ATTRIBUTE_BLINKFEATURES] = new BlinkFeaturesAttribute(this)
   this.attributes[webViewConstants.ATTRIBUTE_DISABLEBLINKFEATURES] = new DisableBlinkFeaturesAttribute(this)
+  this.attributes[webViewConstants.ATTRIBUTE_GUESTINSTANCE] = new GuestInstanceAttribute(this)
 
   const autosizeAttributes = [webViewConstants.ATTRIBUTE_MAXHEIGHT, webViewConstants.ATTRIBUTE_MAXWIDTH, webViewConstants.ATTRIBUTE_MINHEIGHT, webViewConstants.ATTRIBUTE_MINWIDTH]
   autosizeAttributes.forEach((attribute) => {
index 96ee289..5aed6d5 100644 (file)
@@ -17,6 +17,7 @@ module.exports = {
   ATTRIBUTE_USERAGENT: 'useragent',
   ATTRIBUTE_BLINKFEATURES: 'blinkfeatures',
   ATTRIBUTE_DISABLEBLINKFEATURES: 'disableblinkfeatures',
+  ATTRIBUTE_GUESTINSTANCE: 'guestinstance',
 
   // Internal attribute.
   ATTRIBUTE_INTERNALINSTANCEID: 'internalinstanceid',
index 6e5e817..35c5e97 100644 (file)
@@ -71,12 +71,13 @@ var WebViewImpl = (function () {
     // that we don't end up allocating a second guest.
     if (this.guestInstanceId) {
       guestViewInternal.destroyGuest(this.guestInstanceId)
-      this.webContents = null
       this.guestInstanceId = void 0
-      this.beforeFirstNavigation = true
-      this.attributes[webViewConstants.ATTRIBUTE_PARTITION].validPartitionId = true
     }
-    this.internalInstanceId = 0
+
+    this.webContents = null
+    this.attributes[webViewConstants.ATTRIBUTE_GUESTINSTANCE].setValueIgnoreMutation(undefined)
+    this.beforeFirstNavigation = true
+    this.attributes[webViewConstants.ATTRIBUTE_PARTITION].validPartitionId = true
   }
 
   // Sets the <webview>.request property.
@@ -184,7 +185,7 @@ var WebViewImpl = (function () {
 
   WebViewImpl.prototype.createGuest = function () {
     return guestViewInternal.createGuest(this.buildParams(), (event, guestInstanceId) => {
-      this.attachWindow(guestInstanceId)
+      this.attachGuestInstance(guestInstanceId)
     })
   }
 
@@ -257,8 +258,9 @@ var WebViewImpl = (function () {
     return params
   }
 
-  WebViewImpl.prototype.attachWindow = function (guestInstanceId) {
+  WebViewImpl.prototype.attachGuestInstance = function (guestInstanceId) {
     this.guestInstanceId = guestInstanceId
+    this.attributes[webViewConstants.ATTRIBUTE_GUESTINSTANCE].setValueIgnoreMutation(guestInstanceId)
     this.webContents = remote.getGuestWebContents(this.guestInstanceId)
     if (!this.internalInstanceId) {
       return true
@@ -324,10 +326,11 @@ var registerWebViewElement = function () {
     }
     guestViewInternal.deregisterEvents(internal.viewInstanceId)
     internal.elementAttached = false
-    return internal.reset()
+    internal.reset()
+    this.internalInstanceId = 0
   }
   proto.attachedCallback = function () {
-    var internal
+    var internal, instance
     internal = v8Util.getHiddenValue(this, 'internal')
     if (!internal) {
       return
@@ -335,6 +338,10 @@ var registerWebViewElement = function () {
     if (!internal.elementAttached) {
       guestViewInternal.registerEvents(internal, internal.viewInstanceId)
       internal.elementAttached = true
+      instance = internal.attributes[webViewConstants.ATTRIBUTE_GUESTINSTANCE].getValue()
+      if (instance) {
+        return internal.attachGuestInstance(instance)
+      }
       return internal.attributes[webViewConstants.ATTRIBUTE_SRC].parse()
     }
   }
diff --git a/spec/fixtures/pages/webview-move-to-window.html b/spec/fixtures/pages/webview-move-to-window.html
new file mode 100644 (file)
index 0000000..fc14222
--- /dev/null
@@ -0,0 +1,17 @@
+<html>
+<body>
+<script type="text/javascript" charset="utf-8">
+  var ipcRenderer = require('electron').ipcRenderer
+
+  ipcRenderer.on('guestinstance', function(event, instance) {
+    var webview = new WebView()
+    webview.setAttribute('guestinstance', instance)
+    document.body.appendChild(webview)
+
+    webview.addEventListener('did-attach', function (){
+      ipcRenderer.send('pong')
+    })
+  })
+</script>
+</body>
+</html>
index 6b36c84..02323b9 100644 (file)
@@ -2,7 +2,7 @@ const assert = require('assert')
 const path = require('path')
 const http = require('http')
 const url = require('url')
-const {app, session, ipcMain, BrowserWindow} = require('electron').remote
+const {app, session, getGuestWebContents, ipcMain, BrowserWindow} = require('electron').remote
 
 describe('<webview> tag', function () {
   this.timeout(20000)
@@ -975,4 +975,194 @@ describe('<webview> tag', function () {
       done()
     })
   })
+
+  describe('guestinstance attribute', function () {
+    it('before loading there is no attribute', function () {
+      document.body.appendChild(webview)
+      assert(!webview.hasAttribute('guestinstance'))
+    })
+
+    it('loading a page sets the guest view', function (done) {
+      var loadListener = function () {
+        webview.removeEventListener('did-finish-load', loadListener, false)
+        var instance = webview.getAttribute('guestinstance')
+        assert.equal(instance, parseInt(instance))
+
+        var guest = getGuestWebContents(parseInt(instance))
+        assert.equal(guest, webview.getWebContents())
+        done()
+      }
+      webview.addEventListener('did-finish-load', loadListener, false)
+      webview.src = 'file://' + fixtures + '/api/blank.html'
+      document.body.appendChild(webview)
+    })
+
+    it('deleting the attribute destroys the webview', function (done) {
+      var loadListener = function () {
+        webview.removeEventListener('did-finish-load', loadListener, false)
+        var destroyListener = function () {
+          webview.removeEventListener('destroyed', destroyListener, false)
+          assert.equal(getGuestWebContents(instance), null)
+          done()
+        }
+        webview.addEventListener('destroyed', destroyListener, false)
+
+        var instance = parseInt(webview.getAttribute('guestinstance'))
+        webview.removeAttribute('guestinstance')
+      }
+      webview.addEventListener('did-finish-load', loadListener, false)
+      webview.src = 'file://' + fixtures + '/api/blank.html'
+      document.body.appendChild(webview)
+    })
+
+    it('setting the attribute on a new webview moves the contents', function (done) {
+      var loadListener = function () {
+        webview.removeEventListener('did-finish-load', loadListener, false)
+        var webContents = webview.getWebContents()
+        var instance = webview.getAttribute('guestinstance')
+
+        var destroyListener = function () {
+          webview.removeEventListener('destroyed', destroyListener, false)
+          assert.equal(webContents, webview2.getWebContents())
+          // Make sure that events are hooked up to the right webview now
+          webview2.addEventListener('console-message', function (e) {
+            assert.equal(e.message, 'a')
+            document.body.removeChild(webview2)
+            done()
+          })
+
+          webview2.src = 'file://' + fixtures + '/pages/a.html'
+        }
+        webview.addEventListener('destroyed', destroyListener, false)
+
+        var webview2 = new WebView()
+        webview2.setAttribute('guestinstance', instance)
+        document.body.appendChild(webview2)
+      }
+      webview.addEventListener('did-finish-load', loadListener, false)
+      webview.src = 'file://' + fixtures + '/api/blank.html'
+      document.body.appendChild(webview)
+    })
+
+    it('setting the attribute to an invalid guestinstance does nothing', function (done) {
+      var loadListener = function () {
+        webview.removeEventListener('did-finish-load', loadListener, false)
+        webview.setAttribute('guestinstance', 55)
+
+        // Make sure that events are still hooked up to the webview
+        webview.addEventListener('console-message', function (e) {
+          assert.equal(e.message, 'a')
+          done()
+        })
+
+        webview.src = 'file://' + fixtures + '/pages/a.html'
+      }
+      webview.addEventListener('did-finish-load', loadListener, false)
+
+      webview.src = 'file://' + fixtures + '/api/blank.html'
+      document.body.appendChild(webview)
+    })
+
+    it('setting the attribute on an existing webview moves the contents', function (done) {
+      var load1Listener = function () {
+        webview.removeEventListener('did-finish-load', load1Listener, false)
+        var webContents = webview.getWebContents()
+        var instance = webview.getAttribute('guestinstance')
+        var destroyedInstance
+
+        var destroyListener = function () {
+          webview.removeEventListener('destroyed', destroyListener, false)
+          assert.equal(webContents, webview2.getWebContents())
+          assert.equal(null, getGuestWebContents(parseInt(destroyedInstance)))
+
+          // Make sure that events are hooked up to the right webview now
+          webview2.addEventListener('console-message', function (e) {
+            assert.equal(e.message, 'a')
+            document.body.removeChild(webview2)
+            done()
+          })
+
+          webview2.src = 'file://' + fixtures + '/pages/a.html'
+        }
+        webview.addEventListener('destroyed', destroyListener, false)
+
+        var webview2 = new WebView()
+        var load2Listener = function () {
+          webview2.removeEventListener('did-finish-load', load2Listener, false)
+          destroyedInstance = webview2.getAttribute('guestinstance')
+          assert.notEqual(instance, destroyedInstance)
+
+          webview2.setAttribute('guestinstance', instance)
+        }
+        webview2.addEventListener('did-finish-load', load2Listener, false)
+        webview2.src = 'file://' + fixtures + '/api/blank.html'
+        document.body.appendChild(webview2)
+      }
+      webview.addEventListener('did-finish-load', load1Listener, false)
+      webview.src = 'file://' + fixtures + '/api/blank.html'
+      document.body.appendChild(webview)
+    })
+
+    it('moving a guest back to its original webview should work', function (done) {
+      var loadListener = function () {
+        webview.removeEventListener('did-finish-load', loadListener, false)
+        var webContents = webview.getWebContents()
+        var instance = webview.getAttribute('guestinstance')
+
+        var destroy1Listener = function () {
+          webview.removeEventListener('destroyed', destroy1Listener, false)
+          assert.equal(webContents, webview2.getWebContents())
+          assert.equal(null, webview.getWebContents())
+
+          var destroy2Listener = function () {
+            webview2.removeEventListener('destroyed', destroy2Listener, false)
+            assert.equal(webContents, webview.getWebContents())
+            assert.equal(null, webview2.getWebContents())
+
+            // Make sure that events are hooked up to the right webview now
+            webview.addEventListener('console-message', function (e) {
+              assert.equal(e.message, 'a')
+              document.body.removeChild(webview2)
+              done()
+            })
+
+            webview.src = 'file://' + fixtures + '/pages/a.html'
+          }
+          webview2.addEventListener('destroyed', destroy2Listener, false)
+
+          webview.setAttribute('guestinstance', instance)
+        }
+        webview.addEventListener('destroyed', destroy1Listener, false)
+
+        var webview2 = new WebView()
+        webview2.setAttribute('guestinstance', instance)
+        document.body.appendChild(webview2)
+      }
+      webview.addEventListener('did-finish-load', loadListener, false)
+      webview.src = 'file://' + fixtures + '/api/blank.html'
+      document.body.appendChild(webview)
+    })
+
+    it('setting the attribute on a webview in a different window moves the contents', function (done) {
+      var loadListener = function () {
+        webview.removeEventListener('did-finish-load', loadListener, false)
+        var instance = webview.getAttribute('guestinstance')
+
+        w = new BrowserWindow({ show: false })
+        w.webContents.once('did-finish-load', function () {
+          ipcMain.once('pong', function () {
+            assert(!webview.hasAttribute('guestinstance'))
+
+            done()
+          })
+
+          w.webContents.send('guestinstance', instance)
+        })
+        w.loadURL('file://' + fixtures + '/pages/webview-move-to-window.html')
+      }
+      webview.addEventListener('did-finish-load', loadListener, false)
+      webview.src = 'file://' + fixtures + '/api/blank.html'
+      document.body.appendChild(webview)
+    })
+  })
 })