From 680399f47657bc9767ffe7b7c5146a7eb107cf4d Mon Sep 17 00:00:00 2001 From: Julien Isorce Date: Wed, 14 Aug 2019 23:51:15 -0700 Subject: [PATCH] feat: Implement BrowserWindow.getMediaSourceId() and BrowserWindow.moveAbove(mediaSourceId) (#18926) * feat: Implement BrowserWindow.moveAbove(mediaSourceId) BrowserWindow.{focus,blur,moveTop}() are not enough in some situations. For example when implementing an overlay that follows another window that can lose focus. In that case it is useful to move the overlay above the tracked window. sourceId is a string in the format of DesktopCapturerSource.id, for example "window:1869:0". Notes: Added BrowserWindow.moveAbove(mediaSourceId) https://github.com/electron/electron/issues/18922 * feat: Implement BrowserWindow.getMediaSourceId Return the Window id in the format of DesktopCapturerSource's id. For example "window:1234:0". https://github.com/electron/electron/issues/16460 Notes: Added BrowserWindow.getMediaSourceId --- docs/api/browser-window.md | 17 +++ .../browser/api/atom_api_top_level_window.cc | 12 ++ shell/browser/api/atom_api_top_level_window.h | 2 + shell/browser/native_window.h | 3 + shell/browser/native_window_mac.h | 2 + shell/browser/native_window_mac.mm | 24 +++- shell/browser/native_window_views.cc | 57 ++++++++ shell/browser/native_window_views.h | 2 + shell/browser/ui/x/x_window_utils.cc | 11 +- shell/browser/ui/x/x_window_utils.h | 8 +- spec-main/api-browser-window-spec.ts | 56 ++++++++ spec/api-desktop-capturer-spec.js | 130 +++++++++++++++++- 12 files changed, 320 insertions(+), 4 deletions(-) diff --git a/docs/api/browser-window.md b/docs/api/browser-window.md index 8a069da09dbd..12d8f3bd038e 100644 --- a/docs/api/browser-window.md +++ b/docs/api/browser-window.md @@ -1194,6 +1194,14 @@ can not be focused on. Returns `Boolean` - Whether the window is always on top of other windows. +#### `win.moveAbove(mediaSourceId)` + +* `mediaSourceId` String - Window id in the format of DesktopCapturerSource's id. For example "window:1869:0". + +Moves window above the source window in the sense of z-order. If the +`mediaSourceId` is not of type window or if the window does not exist then +this method throws an error. + #### `win.moveTop()` Moves window to top(z-order) regardless of focus @@ -1266,6 +1274,15 @@ Enters or leaves the kiosk mode. Returns `Boolean` - Whether the window is in kiosk mode. +#### `win.getMediaSourceId()` + +Returns `String` - Window id in the format of DesktopCapturerSource's id. For example "window:1234:0". + +More precisely the format is `window:id:other_id` where `id` is `HWND` on +Windows, `CGWindowID` (`uint64_t`) on macOS and `Window` (`unsigned long`) on +Linux. `other_id` is used to identify web contents (tabs) so within the same +top level window. + #### `win.getNativeWindowHandle()` Returns `Buffer` - The platform-specific handle of the window. diff --git a/shell/browser/api/atom_api_top_level_window.cc b/shell/browser/api/atom_api_top_level_window.cc index 372d9f2e0e2d..39004cadeecd 100644 --- a/shell/browser/api/atom_api_top_level_window.cc +++ b/shell/browser/api/atom_api_top_level_window.cc @@ -553,6 +553,11 @@ std::vector TopLevelWindow::GetPosition() { result[1] = pos.y(); return result; } +void TopLevelWindow::MoveAbove(const std::string& sourceId, + mate::Arguments* args) { + if (!window_->MoveAbove(sourceId)) + args->ThrowError("Invalid media source id"); +} void TopLevelWindow::MoveTop() { window_->MoveTop(); @@ -731,6 +736,11 @@ void TopLevelWindow::RemoveBrowserView(v8::Local value) { } } } + +std::string TopLevelWindow::GetMediaSourceId() const { + return window_->GetDesktopMediaID().ToString(); +} + v8::Local TopLevelWindow::GetNativeWindowHandle() { // TODO(MarshallOfSound): Replace once // https://chromium-review.googlesource.com/c/chromium/src/+/1253094/ has @@ -1077,6 +1087,7 @@ void TopLevelWindow::BuildPrototype(v8::Isolate* isolate, .SetMethod("setMaximumSize", &TopLevelWindow::SetMaximumSize) .SetMethod("getMaximumSize", &TopLevelWindow::GetMaximumSize) .SetMethod("setSheetOffset", &TopLevelWindow::SetSheetOffset) + .SetMethod("moveAbove", &TopLevelWindow::MoveAbove) .SetMethod("moveTop", &TopLevelWindow::MoveTop) .SetMethod("_setResizable", &TopLevelWindow::SetResizable) .SetMethod("_isResizable", &TopLevelWindow::IsResizable) @@ -1136,6 +1147,7 @@ void TopLevelWindow::BuildPrototype(v8::Isolate* isolate, .SetMethod("setBrowserView", &TopLevelWindow::SetBrowserView) .SetMethod("addBrowserView", &TopLevelWindow::AddBrowserView) .SetMethod("removeBrowserView", &TopLevelWindow::RemoveBrowserView) + .SetMethod("getMediaSourceId", &TopLevelWindow::GetMediaSourceId) .SetMethod("getNativeWindowHandle", &TopLevelWindow::GetNativeWindowHandle) .SetMethod("setProgressBar", &TopLevelWindow::SetProgressBar) diff --git a/shell/browser/api/atom_api_top_level_window.h b/shell/browser/api/atom_api_top_level_window.h index 02c79a95fd5f..418baa4060de 100644 --- a/shell/browser/api/atom_api_top_level_window.h +++ b/shell/browser/api/atom_api_top_level_window.h @@ -127,6 +127,7 @@ class TopLevelWindow : public mate::TrackableObject, void SetResizable(bool resizable); bool IsResizable(); void SetMovable(bool movable); + void MoveAbove(const std::string& sourceId, mate::Arguments* args); void MoveTop(); bool IsMovable(); void SetMinimizable(bool minimizable); @@ -173,6 +174,7 @@ class TopLevelWindow : public mate::TrackableObject, virtual void RemoveBrowserView(v8::Local value); virtual std::vector> GetBrowserViews() const; virtual void ResetBrowserViews(); + std::string GetMediaSourceId() const; v8::Local GetNativeWindowHandle(); void SetProgressBar(double progress, mate::Arguments* args); void SetOverlayIcon(const gfx::Image& overlay, diff --git a/shell/browser/native_window.h b/shell/browser/native_window.h index 3a3451d7d2e6..b49c7aa26bb3 100644 --- a/shell/browser/native_window.h +++ b/shell/browser/native_window.h @@ -15,6 +15,7 @@ #include "base/memory/weak_ptr.h" #include "base/observer_list.h" #include "base/supports_user_data.h" +#include "content/public/browser/desktop_media_id.h" #include "content/public/browser/web_contents_user_data.h" #include "extensions/browser/app_window/size_constraints.h" #include "shell/browser/native_window_observer.h" @@ -111,6 +112,7 @@ class NativeWindow : public base::SupportsUserData, virtual double GetSheetOffsetX(); virtual double GetSheetOffsetY(); virtual void SetResizable(bool resizable) = 0; + virtual bool MoveAbove(const std::string& sourceId) = 0; virtual void MoveTop() = 0; virtual bool IsResizable() = 0; virtual void SetMovable(bool movable) = 0; @@ -156,6 +158,7 @@ class NativeWindow : public base::SupportsUserData, virtual void SetParentWindow(NativeWindow* parent); virtual void AddBrowserView(NativeBrowserView* browser_view) = 0; virtual void RemoveBrowserView(NativeBrowserView* browser_view) = 0; + virtual content::DesktopMediaID GetDesktopMediaID() const = 0; virtual gfx::NativeView GetNativeView() const = 0; virtual gfx::NativeWindow GetNativeWindow() const = 0; virtual gfx::AcceleratedWidget GetAcceleratedWidget() const = 0; diff --git a/shell/browser/native_window_mac.h b/shell/browser/native_window_mac.h index 2b1ff8056f35..058237358c6b 100644 --- a/shell/browser/native_window_mac.h +++ b/shell/browser/native_window_mac.h @@ -59,6 +59,7 @@ class NativeWindowMac : public NativeWindow { void SetContentSizeConstraints( const extensions::SizeConstraints& size_constraints) override; void SetResizable(bool resizable) override; + bool MoveAbove(const std::string& sourceId) override; void MoveTop() override; bool IsResizable() override; void SetMovable(bool movable) override; @@ -108,6 +109,7 @@ class NativeWindowMac : public NativeWindow { void AddBrowserView(NativeBrowserView* browser_view) override; void RemoveBrowserView(NativeBrowserView* browser_view) override; void SetParentWindow(NativeWindow* parent) override; + content::DesktopMediaID GetDesktopMediaID() const override; gfx::NativeView GetNativeView() const override; gfx::NativeWindow GetNativeWindow() const override; gfx::AcceleratedWidget GetAcceleratedWidget() const override; diff --git a/shell/browser/native_window_mac.mm b/shell/browser/native_window_mac.mm index f70975f6c4fd..2f94a5347124 100644 --- a/shell/browser/native_window_mac.mm +++ b/shell/browser/native_window_mac.mm @@ -16,6 +16,7 @@ #include "base/strings/sys_string_conversions.h" #include "components/remote_cocoa/app_shim/native_widget_ns_window_bridge.h" #include "content/public/browser/browser_accessibility_state.h" +#include "content/public/browser/desktop_media_id.h" #include "native_mate/dictionary.h" #include "shell/browser/native_browser_view_mac.h" #include "shell/browser/ui/cocoa/atom_native_widget_mac.h" @@ -30,6 +31,7 @@ #include "shell/common/deprecate_util.h" #include "shell/common/options_switches.h" #include "skia/ext/skia_utils_mac.h" +#include "third_party/webrtc/modules/desktop_capture/mac/window_list_utils.h" #include "ui/gfx/skia_util.h" #include "ui/gl/gpu_switching_manager.h" #include "ui/views/background.h" @@ -724,6 +726,21 @@ void NativeWindowMac::SetContentSizeConstraints( NativeWindow::SetContentSizeConstraints(size_constraints); } +bool NativeWindowMac::MoveAbove(const std::string& sourceId) { + const content::DesktopMediaID id = content::DesktopMediaID::Parse(sourceId); + if (id.type != content::DesktopMediaID::TYPE_WINDOW) + return false; + + // Check if the window source is valid. + const CGWindowID window_id = id.id; + if (!webrtc::GetWindowOwnerPid(window_id)) + return false; + + [window_ orderWindow:NSWindowAbove relativeTo:id.id]; + + return true; +} + void NativeWindowMac::MoveTop() { [window_ orderWindow:NSWindowAbove relativeTo:0]; } @@ -1128,7 +1145,12 @@ gfx::NativeWindow NativeWindowMac::GetNativeWindow() const { } gfx::AcceleratedWidget NativeWindowMac::GetAcceleratedWidget() const { - return gfx::kNullAcceleratedWidget; + return [window_ windowNumber]; +} + +content::DesktopMediaID NativeWindowMac::GetDesktopMediaID() const { + return content::DesktopMediaID(content::DesktopMediaID::TYPE_WINDOW, + GetAcceleratedWidget()); } NativeWindowHandle NativeWindowMac::GetNativeWindowHandle() const { diff --git a/shell/browser/native_window_views.cc b/shell/browser/native_window_views.cc index cc030cf83c85..ab1bd3fe7eee 100644 --- a/shell/browser/native_window_views.cc +++ b/shell/browser/native_window_views.cc @@ -17,6 +17,7 @@ #include "base/stl_util.h" #include "base/strings/utf_string_conversions.h" #include "content/public/browser/browser_thread.h" +#include "content/public/browser/desktop_media_id.h" #include "native_mate/dictionary.h" #include "shell/browser/api/atom_api_web_contents.h" #include "shell/browser/native_browser_view_views.h" @@ -647,6 +648,31 @@ void NativeWindowViews::SetResizable(bool resizable) { resizable_ = resizable; } +bool NativeWindowViews::MoveAbove(const std::string& sourceId) { + const content::DesktopMediaID id = content::DesktopMediaID::Parse(sourceId); + if (id.type != content::DesktopMediaID::TYPE_WINDOW) + return false; + +#if defined(OS_WIN) + const HWND otherWindow = reinterpret_cast(id.id); + if (!::IsWindow(otherWindow)) + return false; + + gfx::Point pos = GetPosition(); + gfx::Size size = GetSize(); + ::SetWindowPos(GetAcceleratedWidget(), otherWindow, pos.x(), pos.y(), + size.width(), size.height(), + SWP_NOACTIVATE | SWP_NOMOVE | SWP_NOSIZE | SWP_SHOWWINDOW); +#elif defined(USE_X11) + if (!IsWindowValid(id.id)) + return false; + + electron::MoveWindowAbove(GetAcceleratedWidget(), id.id); +#endif + + return true; +} + void NativeWindowViews::MoveTop() { // TODO(julien.isorce): fix chromium in order to use existing // widget()->StackAtTop(). @@ -1117,6 +1143,37 @@ bool NativeWindowViews::IsVisibleOnAllWorkspaces() { return false; } +content::DesktopMediaID NativeWindowViews::GetDesktopMediaID() const { + const gfx::AcceleratedWidget accelerated_widget = GetAcceleratedWidget(); + content::DesktopMediaID::Id window_handle = content::DesktopMediaID::kNullId; + content::DesktopMediaID::Id aura_id = content::DesktopMediaID::kNullId; +#if defined(OS_WIN) + window_handle = + reinterpret_cast(accelerated_widget); +#elif defined(USE_X11) + window_handle = accelerated_widget; +#endif + aura::WindowTreeHost* const host = + aura::WindowTreeHost::GetForAcceleratedWidget(accelerated_widget); + aura::Window* const aura_window = host ? host->window() : nullptr; + if (aura_window) { + aura_id = content::DesktopMediaID::RegisterNativeWindow( + content::DesktopMediaID::TYPE_WINDOW, aura_window) + .window_id; + } + + // No constructor to pass the aura_id. Make sure to not use the other + // constructor that has a third parameter, it is for yet another purpose. + content::DesktopMediaID result = content::DesktopMediaID( + content::DesktopMediaID::TYPE_WINDOW, window_handle); + + // Confusing but this is how content::DesktopMediaID is designed. The id + // property is the window handle whereas the window_id property is an id + // given by a map containing all aura instances. + result.window_id = aura_id; + return result; +} + gfx::AcceleratedWidget NativeWindowViews::GetAcceleratedWidget() const { if (GetNativeWindow() && GetNativeWindow()->GetHost()) return GetNativeWindow()->GetHost()->GetAcceleratedWidget(); diff --git a/shell/browser/native_window_views.h b/shell/browser/native_window_views.h index e0939cba92de..5e46be51bf46 100644 --- a/shell/browser/native_window_views.h +++ b/shell/browser/native_window_views.h @@ -68,6 +68,7 @@ class NativeWindowViews : public NativeWindow, void SetContentSizeConstraints( const extensions::SizeConstraints& size_constraints) override; void SetResizable(bool resizable) override; + bool MoveAbove(const std::string& sourceId) override; void MoveTop() override; bool IsResizable() override; void SetMovable(bool movable) override; @@ -124,6 +125,7 @@ class NativeWindowViews : public NativeWindow, bool IsVisibleOnAllWorkspaces() override; + content::DesktopMediaID GetDesktopMediaID() const override; gfx::AcceleratedWidget GetAcceleratedWidget() const override; NativeWindowHandle GetNativeWindowHandle() const override; diff --git a/shell/browser/ui/x/x_window_utils.cc b/shell/browser/ui/x/x_window_utils.cc index 761ce6d9f73f..fae7c99470f4 100644 --- a/shell/browser/ui/x/x_window_utils.cc +++ b/shell/browser/ui/x/x_window_utils.cc @@ -89,6 +89,10 @@ bool ShouldUseGlobalMenuBar() { } void MoveWindowToForeground(::Window xwindow) { + MoveWindowAbove(xwindow, 0); +} + +void MoveWindowAbove(::Window xwindow, ::Window other_xwindow) { XDisplay* xdisplay = gfx::GetXDisplay(); XEvent xclient; memset(&xclient, 0, sizeof(xclient)); @@ -99,7 +103,7 @@ void MoveWindowToForeground(::Window xwindow) { xclient.xclient.message_type = GetAtom("_NET_RESTACK_WINDOW"); xclient.xclient.format = 32; xclient.xclient.data.l[0] = 2; - xclient.xclient.data.l[1] = 0; + xclient.xclient.data.l[1] = other_xwindow; xclient.xclient.data.l[2] = Above; xclient.xclient.data.l[3] = 0; xclient.xclient.data.l[4] = 0; @@ -109,4 +113,9 @@ void MoveWindowToForeground(::Window xwindow) { XFlush(xdisplay); } +bool IsWindowValid(::Window xwindow) { + XWindowAttributes attrs; + return XGetWindowAttributes(gfx::GetXDisplay(), xwindow, &attrs); +} + } // namespace electron diff --git a/shell/browser/ui/x/x_window_utils.h b/shell/browser/ui/x/x_window_utils.h index 9b313309a7b6..66a6ba9a57af 100644 --- a/shell/browser/ui/x/x_window_utils.h +++ b/shell/browser/ui/x/x_window_utils.h @@ -23,9 +23,15 @@ void SetWindowType(::Window xwindow, const std::string& type); // Returns true if the bus name "com.canonical.AppMenu.Registrar" is available. bool ShouldUseGlobalMenuBar(); -// Bring the given window to the front and give it the focus. +// Bring the given window to the front regardless of focus. void MoveWindowToForeground(::Window xwindow); +// Move a given window above the other one. +void MoveWindowAbove(::Window xwindow, ::Window other_xwindow); + +// Return true is the given window exists, false otherwise. +bool IsWindowValid(::Window xwindow); + } // namespace electron #endif // SHELL_BROWSER_UI_X_X_WINDOW_UTILS_H_ diff --git a/spec-main/api-browser-window-spec.ts b/spec-main/api-browser-window-spec.ts index 3658fd95e753..c42189537fe8 100644 --- a/spec-main/api-browser-window-spec.ts +++ b/spec-main/api-browser-window-spec.ts @@ -653,6 +653,48 @@ describe('BrowserWindow module', () => { }) }) + describe('BrowserWindow.moveAbove(mediaSourceId)', () => { + it('should throw an exception if wrong formatting', async () => { + const fakeSourceIds = [ + 'none', 'screen:0', 'window:fake', 'window:1234', 'foobar:1:2' + ] + fakeSourceIds.forEach((sourceId) => { + expect(() => { + w.moveAbove(sourceId) + }).to.throw(/Invalid media source id/) + }) + }) + it('should throw an exception if wrong type', async () => { + const fakeSourceIds = [null as any, 123 as any] + fakeSourceIds.forEach((sourceId) => { + expect(() => { + w.moveAbove(sourceId) + }).to.throw(/Error processing argument at index 0 */) + }) + }) + it('should throw an exception if invalid window', async () => { + // It is very unlikely that these window id exist. + const fakeSourceIds = ['window:99999999:0', 'window:123456:1', + 'window:123456:9'] + fakeSourceIds.forEach((sourceId) => { + expect(() => { + w.moveAbove(sourceId) + }).to.throw(/Invalid media source id/) + }) + }) + it('should not throw an exception', async () => { + const w2 = new BrowserWindow({ show: false, title: 'window2' }) + const w2Shown = emittedOnce(w2, 'show') + w2.show() + await w2Shown + + expect(() => { + w.moveAbove(w2.getMediaSourceId()) + }).to.not.throw() + + await closeWindow(w2, { assertNotWindows: false }) + }) + }) }) describe('sizing', () => { @@ -3363,6 +3405,20 @@ describe('BrowserWindow module', () => { }) }) + describe('window.getMediaSourceId()', () => { + afterEach(closeAllWindows) + it('returns valid source id', async () => { + const w = new BrowserWindow({show: false}) + const shown = emittedOnce(w, 'show') + w.show() + await shown + + // Check format 'window:1234:0'. + const sourceId = w.getMediaSourceId() + expect(sourceId).to.match(/^window:\d+:\d+$/) + }) + }) + ifdescribe(!process.env.ELECTRON_SKIP_NATIVE_MODULE_TESTS)('window.getNativeWindowHandle()', () => { afterEach(closeAllWindows) it('returns valid handle', () => { diff --git a/spec/api-desktop-capturer-spec.js b/spec/api-desktop-capturer-spec.js index fb9c1d0cef5c..f138feb9357a 100644 --- a/spec/api-desktop-capturer-spec.js +++ b/spec/api-desktop-capturer-spec.js @@ -91,7 +91,10 @@ describe('desktopCapturer', () => { w.show() await wShown - const sources = await desktopCapturer.getSources({ types: ['window', 'screen'], thumbnailSize: { width: 0, height: 0 } }) + const sources = await desktopCapturer.getSources({ + types: ['window', 'screen'], + thumbnailSize: { width: 0, height: 0 } + }) w.destroy() expect(sources).to.be.an('array').that.is.not.empty() for (const { thumbnail: thumbnailImage } of sources) { @@ -99,4 +102,129 @@ describe('desktopCapturer', () => { expect(thumbnailImage.isEmpty()).to.be.true() } }) + + it('getMediaSourceId should match DesktopCapturerSource.id', async () => { + const { BrowserWindow } = remote + const w = new BrowserWindow({ show: false, width: 100, height: 100 }) + const wShown = emittedOnce(w, 'show') + const wFocused = emittedOnce(w, 'focus') + w.show() + w.focus() + await wShown + await wFocused + + const mediaSourceId = w.getMediaSourceId() + const sources = await desktopCapturer.getSources({ + types: ['window'], + thumbnailSize: { width: 0, height: 0 } + }) + w.destroy() + + // TODO(julien.isorce): investigate why |sources| is empty on the linux + // bots while it is not on my workstation, as expected, with and without + // the --ci parameter. + if (process.platform === 'linux' && sources.length === 0) { + it.skip('desktopCapturer.getSources returned an empty source list') + return + } + + expect(sources).to.be.an('array').that.is.not.empty() + const foundSource = sources.find((source) => { + return source.id === mediaSourceId + }) + expect(mediaSourceId).to.equal(foundSource.id) + }) + + it('moveAbove should move the window at the requested place', async () => { + // DesktopCapturer.getSources() is guaranteed to return in the correct + // z-order from foreground to background. + const MAX_WIN = 4 + const { BrowserWindow } = remote + const mainWindow = remote.getCurrentWindow() + const wList = [mainWindow] + try { + // Add MAX_WIN-1 more window so we have MAX_WIN in total. + for (let i = 0; i < MAX_WIN - 1; i++) { + const w = new BrowserWindow({ show: true, width: 100, height: 100 }) + wList.push(w) + } + expect(wList.length).to.equal(MAX_WIN) + + // Show and focus all the windows. + wList.forEach(async (w) => { + const wFocused = emittedOnce(w, 'focus') + w.focus() + await wFocused + }) + // At this point our windows should be showing from bottom to top. + + // DesktopCapturer.getSources() returns sources sorted from foreground to + // background, i.e. top to bottom. + let sources = await desktopCapturer.getSources({ + types: ['window'], + thumbnailSize: { width: 0, height: 0 } + }) + + // TODO(julien.isorce): investigate why |sources| is empty on the linux + // bots while it is not on my workstation, as expected, with and without + // the --ci parameter. + if (process.platform === 'linux' && sources.length === 0) { + wList.forEach((w) => { + if (w !== mainWindow) { + w.destroy() + } + }) + it.skip('desktopCapturer.getSources returned an empty source list') + return + } + + expect(sources).to.be.an('array').that.is.not.empty() + expect(sources.length).to.gte(MAX_WIN) + + // Only keep our windows, they must be in the MAX_WIN first windows. + sources.splice(MAX_WIN, sources.length - MAX_WIN) + expect(sources.length).to.equal(MAX_WIN) + expect(sources.length).to.equal(wList.length) + + // Check that the sources and wList are sorted in the reverse order. + const wListReversed = wList.slice(0).reverse() + const canGoFurther = sources.every( + (source, index) => source.id === wListReversed[index].getMediaSourceId()) + if (!canGoFurther) { + // Skip remaining checks because either focus or window placement are + // not reliable in the running test environment. So there is no point + // to go further to test moveAbove as requirements are not met. + return + } + + // Do the real work, i.e. move each window above the next one so that + // wList is sorted from foreground to background. + wList.forEach(async (w, index) => { + if (index < (wList.length - 1)) { + const wNext = wList[index + 1] + w.moveAbove(wNext.getMediaSourceId()) + } + }) + + sources = await desktopCapturer.getSources({ + types: ['window'], + thumbnailSize: { width: 0, height: 0 } + }) + // Only keep our windows again. + sources.splice(MAX_WIN, sources.length - MAX_WIN) + expect(sources.length).to.equal(MAX_WIN) + expect(sources.length).to.equal(wList.length) + + // Check that the sources and wList are sorted in the same order. + sources.forEach((source, index) => { + expect(source.id).to.equal(wList[index].getMediaSourceId()) + }) + } finally { + wList.forEach((w) => { + if (w !== mainWindow) { + w.destroy() + } + }) + } + }) })