From: Dave Townsend Date: Thu, 8 Sep 2016 17:01:01 +0000 (-0700) Subject: Add a guestinstance attribute to webviews reflecting their current guest X-Git-Url: http://review.tizen.org/git/?a=commitdiff_plain;h=313b2faa3c96c508816947c4942b3b53827a31c8;p=platform%2Fframework%2Fweb%2Fcrosswalk-tizen.git Add a guestinstance attribute to webviews reflecting their current guest instance ID and allowing moving a guest instance to a new webview. --- diff --git a/atom/browser/api/atom_api_web_contents.cc b/atom/browser/api/atom_api_web_contents.cc index cfb1e7f..5390661 100644 --- a/atom/browser/api/atom_api_web_contents.cc +++ b/atom/browser/api/atom_api_web_contents.cc @@ -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 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) diff --git a/atom/browser/api/atom_api_web_contents.h b/atom/browser/api/atom_api_web_contents.h index a4d50ef..1e55830 100644 --- a/atom/browser/api/atom_api_web_contents.h +++ b/atom/browser/api/atom_api_web_contents.h @@ -102,6 +102,7 @@ class WebContents : public mate::TrackableObject, 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, diff --git a/atom/renderer/api/atom_api_web_frame.cc b/atom/renderer/api/atom_api_web_frame.cc index dd28ad7..ddcea9e 100644 --- a/atom/renderer/api/atom_api_web_frame.cc +++ b/atom/renderer/api/atom_api_web_frame.cc @@ -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) diff --git a/atom/renderer/api/atom_api_web_frame.h b/atom/renderer/api/atom_api_web_frame.h index 7b2401d..570a9df 100644 --- a/atom/renderer/api/atom_api_web_frame.h +++ b/atom/renderer/api/atom_api_web_frame.h @@ -53,6 +53,7 @@ class WebFrame : public mate::Wrappable { 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, diff --git a/docs/api/web-view-tag.md b/docs/api/web-view-tag.md index 90153d8..20f9d27 100644 --- a/docs/api/web-view-tag.md +++ b/docs/api/web-view-tag.md @@ -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 + +``` + +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: diff --git a/lib/browser/guest-view-manager.js b/lib/browser/guest-view-manager.js index fdd426c..df2589a 100644 --- a/lib/browser/guest-view-manager.js +++ b/lib/browser/guest-view-manager.js @@ -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 diff --git a/lib/renderer/web-view/guest-view-internal.js b/lib/renderer/web-view/guest-view-internal.js index fed0275..9947c27 100644 --- a/lib/renderer/web-view/guest-view-internal.js +++ b/lib/renderer/web-view/guest-view-internal.js @@ -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) diff --git a/lib/renderer/web-view/web-view-attributes.js b/lib/renderer/web-view/web-view-attributes.js index 01eb777..3bbc833 100644 --- a/lib/renderer/web-view/web-view-attributes.js +++ b/lib/renderer/web-view/web-view-attributes.js @@ -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) => { diff --git a/lib/renderer/web-view/web-view-constants.js b/lib/renderer/web-view/web-view-constants.js index 96ee289..5aed6d5 100644 --- a/lib/renderer/web-view/web-view-constants.js +++ b/lib/renderer/web-view/web-view-constants.js @@ -17,6 +17,7 @@ module.exports = { ATTRIBUTE_USERAGENT: 'useragent', ATTRIBUTE_BLINKFEATURES: 'blinkfeatures', ATTRIBUTE_DISABLEBLINKFEATURES: 'disableblinkfeatures', + ATTRIBUTE_GUESTINSTANCE: 'guestinstance', // Internal attribute. ATTRIBUTE_INTERNALINSTANCEID: 'internalinstanceid', diff --git a/lib/renderer/web-view/web-view.js b/lib/renderer/web-view/web-view.js index 6e5e817..35c5e97 100644 --- a/lib/renderer/web-view/web-view.js +++ b/lib/renderer/web-view/web-view.js @@ -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 .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 index 0000000..fc14222 --- /dev/null +++ b/spec/fixtures/pages/webview-move-to-window.html @@ -0,0 +1,17 @@ + + + + + diff --git a/spec/webview-spec.js b/spec/webview-spec.js index 6b36c84..02323b9 100644 --- a/spec/webview-spec.js +++ b/spec/webview-spec.js @@ -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(' tag', function () { this.timeout(20000) @@ -975,4 +975,194 @@ describe(' 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) + }) + }) })