Repository: electron/electron (language: cpp)

Issue:
<webview>.capturePage() test very unreliable
the `<webview>.capturePage() returns a Promise with a NativeImage` test was disabled in https://github.com/electron/electron/pull/32419 due to severe flakiness. Follow up & fix.


Discussion / hints:
This has been a flake for awhile but in my local build setup I'm finding it to be 100% repeatable in the roll and not in `main` so my suspicion is that something has changed in the roll, either a new issue or some change that made the existing problem more repeatable.

successful run (from `main`) with LOG() tracers inserted:

```txt
[1525095:0201/204955.687110:INFO:compositor_frame_sink_impl.cc(111)] CompositorFrameSinkImpl::CompositorFrameSinkImpl ctor list just called make_unique CompositorFrameSinkSupport
[1525050:0201/204956.034507:INFO:render_widget_host_impl.cc(3470)] RenderWidgetHostImpl::CreateFrameSink bound onceFrameSinkId(5, 3)
[1525050:0201/204956.034529:INFO:host_frame_sink_manager.cc(162)] HostFrameSinkManager::CreateCompositorFrameSinkFrameSinkId(5, 3)
[1525050:0201/204956.034539:INFO:host_frame_sink_manager.cc(203)] HostFrameSinkManager::CreateFrameSink calling CreateCompositorFrameSinkFrameSinkId(5, 3)
[1525095:0201/204956.034674:INFO:frame_sink_manager_impl.cc(221)] making a CompositorFrameSinkImpl for frame_sink_idFrameSinkId(5, 3)
[1525095:0201/204956.034827:INFO:compositor_frame_sink_support.cc(86)] CompositorFrameSinkSupport::CompositorFrameSinkSupport calling RegisterCompositorFrameSinkSupport
[1525095:0201/204956.034884:INFO:frame_sink_manager_impl.cc(411)] FrameSinkManagerImpl::RegisterCompositorFrameSinkSupport adding FrameSinkId(5, 3) to support_map_
[1525095:0201/204956.034990:INFO:compositor_frame_sink_impl.cc(111)] CompositorFrameSinkImpl::CompositorFrameSinkImpl ctor list just called make_unique CompositorFrameSinkSupport
[1525095:0201/204956.066729:INFO:frame_sink_manager_impl.cc(441)] FrameSinkManagerImpl::RegisterCompositorFrameSinkSupport erasing FrameSinkId(5, 3) from support_map_
[1525050:0201/204956.072057:INFO:render_widget_host_impl.cc(3470)] RenderWidgetHostImpl::CreateFrameSink bound onceFrameSinkId(6, 2)
[1525050:0201/204956.072080:INFO:host_frame_sink_manager.cc(162)] HostFrameSinkManager::CreateCompositorFrameSinkFrameSinkId(6, 2)
[1525050:0201/204956.072088:INFO:host_frame_sink_manager.cc(203)] HostFrameSinkManager::CreateFrameSink calling CreateCompositorFrameSinkFrameSinkId(6, 2)
[1525095:0201/204956.072240:INFO:frame_sink_manager_impl.cc(221)] making a CompositorFrameSinkImpl for frame_sink_idFrameSinkId(6, 2)
[1525095:0201/204956.072444:INFO:compositor_frame_sink_support.cc(86)] CompositorFrameSinkSupport::CompositorFrameSinkSupport calling RegisterCompositorFrameSinkSupport
[1525095:0201/204956.072507:INFO:frame_sink_manager_impl.cc(411)] FrameSinkManagerImpl::RegisterCompositorFrameSinkSupport adding FrameSinkId(6, 2) to support_map_
[1525095:0201/204956.072556:INFO:compositor_frame_sink_impl.cc(111)] CompositorFrameSinkImpl::CompositorFrameSinkImpl ctor list just called make_unique CompositorFrameSinkSupport
[1525050:0201/204956.081635:INFO:electron_api_web_contents.cc(3042)] in CapturePage
[1525050:0201/204956.081658:INFO:electron_api_web_contents.cc(3050)] ...getting view
[1525050:0201/204956.081672:INFO:electron_api_web_contents.cc(3083)] scale 1
[1525050:0201/204956.081680:INFO:electron_api_web_contents.cc(3088)]  rect x 0 y 0 w 784 h 150
[1525050:0201/204956.081685:INFO:electron_api_web_contents.cc(3089)]  bitmap size w 784 h 150
[1525050:0201/204956.081690:INFO:render_widget_host_view_child_frame.cc(824)]  RenderWidgetHostViewChildFrame::CopyFromSurface
[1525095:0201/204956.081846:INFO:frame_sink_manager_impl.cc(345)] looking in support_map_ for FrameSinkId(6, 2)
[1525095:0201/204956.081935:INFO:frame_sink_manager_impl.cc(351)] found the id
[1525050:0201/204956.102772:INFO:electron_api_web_contents.cc(390)] got a bitmap with bitmap.empty() 0
[1525050:0201/204956.102802:INFO:electron_api_web_contents.cc(392)] resolving with img 0
[1525050:0201/204956.102817:INFO:image_converter.cc(52)] ToV8 native image from gfx::Image whose IsEmpty()0
[1525050:0201/204956.102827:INFO:electron_api_native_image.cc(444)]  NativeImage::Create
[1525050:0201/204956.102832:INFO:electron_api_native_image.cc(113)]  in NativeImage ctor, IsEmpty() 0
[1525140:0201/204956.108094:INFO:electron_api_native_image.cc(113)]  in NativeImage ctor, IsEmpty() 0
[1525140:0201/204956.108391:INFO:electron_api_native_image.cc(205)]  in ToPNG, IsEmpty() is 0
[1525140:0201/204956.108417:INFO:electron_api_native_image.cc(207)]  scale_factor[1]
```
    
Unsuccessful run from roller branch with LOG() injections:
    
```txt
[1524426:0201/204538.336607:ERROR:power_monitor_device_source_stub.cc(11)] Not implemented reached in virtual bool base::PowerMonitorDeviceSource::IsOnBatteryPower()
[1524426:0201/204538.336989:INFO:content_main_runner_impl.cc(1146)] Chrome is running in full browser mode.
[1524426:0201/204538.706342:WARNING:bluez_dbus_manager.cc(248)] Floss manager not present, cannot set Floss enable/disable.
[1524489:0201/204538.733078:WARNING:simple_synchronous_entry.cc(1326)] Could not open platform files for entry.
[1524471:0201/204538.783226:ERROR:sandbox_linux.cc(377)] InitializeSandbox() called with multiple threads in process gpu-process.
[1524471:0201/204538.793911:INFO:frame_sink_manager_impl.cc(405)] FrameSinkManagerImpl::RegisterCompositorFrameSinkSupport addingFrameSinkId(0, 1)
[1524426:0201/204538.960912:INFO:render_widget_host_impl.cc(3479)] RenderWidgetHostImpl::CreateFrameSink bound onceFrameSinkId(4, 3)
[1524471:0201/204538.961090:INFO:frame_sink_manager_impl.cc(405)] FrameSinkManagerImpl::RegisterCompositorFrameSinkSupport addingFrameSinkId(4, 3)
[1524426:0201/204539.342794:INFO:render_widget_host_impl.cc(3479)] RenderWidgetHostImpl::CreateFrameSink bound onceFrameSinkId(5, 3)
[1524471:0201/204539.343030:INFO:frame_sink_manager_impl.cc(405)] FrameSinkManagerImpl::RegisterCompositorFrameSinkSupport addingFrameSinkId(5, 3)
[1524471:0201/204539.375221:INFO:frame_sink_manager_impl.cc(435)] FrameSinkManagerImpl::UnregisterCompositorFrameSinkSupport removingFrameSinkId(5, 3)
[1524426:0201/204539.388436:INFO:electron_api_web_contents.cc(3033)] in CapturePage
[1524426:0201/204539.388459:INFO:electron_api_web_contents.cc(3041)] ...getting view
[1524426:0201/204539.388485:INFO:electron_api_web_contents.cc(3074)] scale 1
[1524426:0201/204539.388492:INFO:electron_api_web_contents.cc(3079)]  rect x 0 y 0 w 784 h 150
[1524426:0201/204539.388497:INFO:electron_api_web_contents.cc(3080)]  bitmap size w 784 h 150
[1524426:0201/204539.388502:INFO:render_widget_host_view_child_frame.cc(824)] RenderWidgetHostViewChildFrame::CopyFromSurface
[1524426:0201/204539.388507:INFO:render_widget_host_view_child_frame.cc(847)] 
[1524426:0201/204539.388511:INFO:render_widget_host_view_child_frame.cc(853)] 
[1524426:0201/204539.388515:INFO:render_widget_host_view_child_frame.cc(855)] 
[1524426:0201/204539.388518:INFO:render_widget_host_view_child_frame.cc(867)] 
[1524426:0201/204539.388522:INFO:render_widget_host_view_child_frame.cc(870)]  GetCurrentSurfaceId() SurfaceId(FrameSinkId(6, 2), LocalSurfaceId(2, 1, 3D0B...))
[1524426:0201/204539.388537:INFO:host_frame_sink_manager.cc(283)] HostFrameSinkManager::RequestCopyOfOutput
[1524471:0201/204539.388737:INFO:frame_sink_manager_impl.cc(345)] FrameSinkManagerImpl::RequestCopyOfOutput
[1524471:0201/204539.388835:INFO:frame_sink_manager_impl.cc(348)] looking for surface idSurfaceId(FrameSinkId(6, 2), LocalSurfaceId(2, 1, 3D0B...))
[1524471:0201/204539.388876:INFO:frame_sink_manager_impl.cc(349)] which has frame_sink_idFrameSinkId(6, 2)
[1524471:0201/204539.388915:INFO:frame_sink_manager_impl.cc(351)] no such id
[1524426:0201/204539.389272:INFO:electron_api_web_contents.cc(389)] got a bitmap with bitmap.empty() 1
[1524426:0201/204539.389284:INFO:electron_api_web_contents.cc(391)] resolving with img 1
[1524426:0201/204539.389291:INFO:image_converter.cc(51)]  converting to a NativeImage a gfx::Image whose IsEmpty() is 1
[1524426:0201/204539.389295:INFO:electron_api_native_image.cc(443)]  NativeImage::Create with image IsEmpty() 1
[1524426:0201/204539.389299:INFO:electron_api_native_image.cc(113)]  in NativeImage ctor, IsEmpty() 1
[1524426:0201/204539.389518:INFO:render_widget_host_impl.cc(3479)] RenderWidgetHostImpl::CreateFrameSink bound onceFrameSinkId(6, 2)
[1524518:0201/204539.389581:INFO:electron_api_native_image.cc(113)]  in NativeImage ctor, IsEmpty() 1
[1524471:0201/204539.389700:INFO:frame_sink_manager_impl.cc(405)] FrameSinkManagerImpl::RegisterCompositorFrameSinkSupport addingFrameSinkId(6, 2)
[1524518:0201/204539.389908:INFO:electron_api_native_image.cc(204)]  in ToPNG, IsEmpty() is 1
[1524518:0201/204539.389919:INFO:electron_api_native_image.cc(63)]  GetScaleFactorFromOptions returning 1
```

Note the sequencing change of when the layer with  frame_sink_id `FrameSinkId(6, 2)` is created. In the successful run, it's created before we try to put it in an image. In the failed run it's created _after_. This sequencing issue appears to be why the image is empty and the test fails.

Fix patch:
diff --git a/spec-main/webview-spec.ts b/spec-main/webview-spec.ts
index ade63be8c8098..af53a543b9e88 100644
--- a/spec-main/webview-spec.ts
+++ b/spec-main/webview-spec.ts
@@ -3,12 +3,14 @@ import * as url from 'url';
 import { BrowserWindow, session, ipcMain, app, WebContents } from 'electron/main';
 import { closeAllWindows } from './window-helpers';
 import { emittedOnce, emittedUntil } from './events-helpers';
-import { ifit, delay, defer, itremote, useRemoteContext } from './spec-helpers';
+import { ifit, ifdescribe, delay, defer, itremote, useRemoteContext } from './spec-helpers';
 import { expect } from 'chai';
 import * as http from 'http';
 import { AddressInfo } from 'net';
+import * as auth from 'basic-auth';
 
 declare let WebView: any;
+const features = process._linkedBinding('electron_common_features');
 
 async function loadWebView (w: WebContents, attributes: Record<string, string>, opts?: {openDevTools?: boolean}): Promise<void> {
   const { openDevTools } = {
@@ -1746,5 +1748,422 @@ describe('<webview> tag', function () {
         expect(message).to.equal('undefined');
       });
     });
+
+    describe('media-started-playing and media-paused events', () => {
+      it('emits when audio starts and stops playing', async function () {
+        if (!await w.executeJavaScript('document.createElement(\'audio\').canPlayType(\'audio/wav\')')) { return this.skip(); }
+
+        await loadWebView(w, { src: blankPageUrl });
+
+        // With the new autoplay policy, audio elements must be unmuted
+        // see https://goo.gl/xX8pDD.
+        await w.executeJavaScript(`new Promise(resolve => {
+          webview.executeJavaScript(\`
+            const audio = document.createElement("audio")
+            audio.src = "../assets/tone.wav"
+            document.body.appendChild(audio);
+            audio.play()
+          \`, true)
+          webview.addEventListener('media-started-playing', () => resolve(), {once: true})
+        })`);
+
+        await w.executeJavaScript(`new Promise(resolve => {
+          webview.executeJavaScript(\`
+            document.querySelector("audio").pause()
+          \`, true)
+          webview.addEventListener('media-paused', () => resolve(), {once: true})
+        })`);
+      });
+    });
+  });
+
+  describe('methods', () => {
+    let w: WebContents;
+    before(async () => {
+      const window = new BrowserWindow({
+        show: false,
+        webPreferences: {
+          webviewTag: true,
+          nodeIntegration: true,
+          contextIsolation: false
+        }
+      });
+      await window.loadURL(`file://${fixtures}/pages/blank.html`);
+      w = window.webContents;
+    });
+    afterEach(async () => {
+      await w.executeJavaScript(`{
+        document.querySelectorAll('webview').forEach(el => el.remove())
+      }`);
+    });
+    after(closeAllWindows);
+
+    describe('<webview>.reload()', () => {
+      it('should emit beforeunload handler', async () => {
+        await loadWebView(w, {
+          nodeintegration: 'on',
+          webpreferences: 'contextIsolation=no',
+          src: `file://${fixtures}/pages/beforeunload-false.html`
+        });
+
+        // Event handler has to be added before reload.
+        const channel = await w.executeJavaScript(`new Promise(resolve => {
+          webview.addEventListener('ipc-message', e => resolve(e.channel))
+          webview.reload();
+        })`);
+
+        expect(channel).to.equal('onbeforeunload');
+      });
+    });
+
+    describe('<webview>.goForward()', () => {
+      useRemoteContext({ webPreferences: { webviewTag: true } });
+      itremote('should work after a replaced history entry', async (fixtures: string) => {
+        const webview = new WebView();
+
+        webview.setAttribute('nodeintegration', 'on');
+        webview.setAttribute('webpreferences', 'contextIsolation=no');
+        webview.src = `file://${fixtures}/pages/history-replace.html`;
+        document.body.appendChild(webview);
+
+        {
+          const [e] = await Promise.all([
+            new Promise<any>(resolve => webview.addEventListener('ipc-message', resolve, { once: true })),
+            new Promise<void>(resolve => webview.addEventListener('did-stop-loading', resolve, { once: true }))
+          ]);
+          expect(e.channel).to.equal('history');
+          expect(e.args[0]).to.equal(1);
+          expect(webview.canGoBack()).to.be.false();
+          expect(webview.canGoForward()).to.be.false();
+        }
+
+        webview.src = `file://${fixtures}/pages/base-page.html`;
+
+        await new Promise<void>(resolve => webview.addEventListener('did-stop-loading', resolve, { once: true }));
+
+        expect(webview.canGoBack()).to.be.true();
+        expect(webview.canGoForward()).to.be.false();
+
+        webview.goBack();
+
+        {
+          const [e] = await Promise.all([
+            new Promise<any>(resolve => webview.addEventListener('ipc-message', resolve, { once: true })),
+            new Promise<void>(resolve => webview.addEventListener('did-stop-loading', resolve, { once: true }))
+          ]);
+          expect(e.channel).to.equal('history');
+          expect(e.args[0]).to.equal(2);
+          expect(webview.canGoBack()).to.be.false();
+          expect(webview.canGoForward()).to.be.true();
+        }
+
+        webview.goForward();
+
+        await new Promise<void>(resolve => webview.addEventListener('did-stop-loading', resolve, { once: true }));
+
+        expect(webview.canGoBack()).to.be.true();
+        expect(webview.canGoForward()).to.be.false();
+      }, [fixtures]);
+    });
+
+    describe('<webview>.clearHistory()', () => {
+      it('should clear the navigation history', async () => {
+        await loadWebView(w, {
+          nodeintegration: 'on',
+          webpreferences: 'contextIsolation=no',
+          src: blankPageUrl
+        });
+
+        // Navigation must be triggered by a user gesture to make canGoBack() return true
+        await w.executeJavaScript('webview.executeJavaScript(`history.pushState(null, "", "foo.html")`, true)');
+
+        expect(await w.executeJavaScript('webview.canGoBack()')).to.be.true();
+
+        await w.executeJavaScript('webview.clearHistory()');
+        expect(await w.executeJavaScript('webview.canGoBack()')).to.be.false();
+      });
+    });
+
+    describe('executeJavaScript', () => {
+      it('can return the result of the executed script', async () => {
+        await loadWebView(w, {
+          src: 'about:blank'
+        });
+
+        const jsScript = "'4'+2";
+        const expectedResult = '42';
+
+        const result = await w.executeJavaScript(`webview.executeJavaScript(${JSON.stringify(jsScript)})`);
+        expect(result).to.equal(expectedResult);
+      });
+    });
+
+    it('supports inserting CSS', async () => {
+      await loadWebView(w, { src: `file://${fixtures}/pages/base-page.html` });
+      await w.executeJavaScript('webview.insertCSS(\'body { background-repeat: round; }\')');
+      const result = await w.executeJavaScript('webview.executeJavaScript(\'window.getComputedStyle(document.body).getPropertyValue("background-repeat")\')');
+      expect(result).to.equal('round');
+    });
+
+    it('supports removing inserted CSS', async () => {
+      await loadWebView(w, { src: `file://${fixtures}/pages/base-page.html` });
+      const key = await w.executeJavaScript('webview.insertCSS(\'body { background-repeat: round; }\')');
+      await w.executeJavaScript(`webview.removeInsertedCSS(${JSON.stringify(key)})`);
+      const result = await w.executeJavaScript('webview.executeJavaScript(\'window.getComputedStyle(document.body).getPropertyValue("background-repeat")\')');
+      expect(result).to.equal('repeat');
+    });
+
+    describe('sendInputEvent', () => {
+      it('can send keyboard event', async () => {
+        await loadWebViewAndWaitForEvent(w, {
+          nodeintegration: 'on',
+          webpreferences: 'contextIsolation=no',
+          src: `file://${fixtures}/pages/onkeyup.html`
+        }, 'dom-ready');
+
+        const waitForIpcMessage = w.executeJavaScript('new Promise(resolve => webview.addEventListener("ipc-message", e => resolve({...e})), {once: true})');
+        w.executeJavaScript(`webview.sendInputEvent({
+          type: 'keyup',
+          keyCode: 'c',
+          modifiers: ['shift']
+        })`);
+
+        const { channel, args } = await waitForIpcMessage;
+        expect(channel).to.equal('keyup');
+        expect(args).to.deep.equal(['C', 'KeyC', 67, true, false]);
+      });
+
+      it('can send mouse event', async () => {
+        await loadWebViewAndWaitForEvent(w, {
+          nodeintegration: 'on',
+          webpreferences: 'contextIsolation=no',
+          src: `file://${fixtures}/pages/onmouseup.html`
+        }, 'dom-ready');
+
+        const waitForIpcMessage = w.executeJavaScript('new Promise(resolve => webview.addEventListener("ipc-message", e => resolve({...e})), {once: true})');
+        w.executeJavaScript(`webview.sendInputEvent({
+          type: 'mouseup',
+          modifiers: ['ctrl'],
+          x: 10,
+          y: 20
+        })`);
+
+        const { channel, args } = await waitForIpcMessage;
+        expect(channel).to.equal('mouseup');
+        expect(args).to.deep.equal([10, 20, false, true]);
+      });
+    });
+
+    describe('<webview>.getWebContentsId', () => {
+      it('can return the WebContents ID', async () => {
+        await loadWebView(w, { src: 'about:blank' });
+
+        expect(await w.executeJavaScript('webview.getWebContentsId()')).to.be.a('number');
+      });
+    });
+
+    ifdescribe(features.isPrintingEnabled())('<webview>.printToPDF()', () => {
+      it('rejects on incorrectly typed parameters', async () => {
+        const badTypes = {
+          landscape: [],
+          displayHeaderFooter: '123',
+          printBackground: 2,
+          scale: 'not-a-number',
+          pageSize: 'IAmAPageSize',
+          margins: 'terrible',
+          pageRanges: { oops: 'im-not-the-right-key' },
+          headerTemplate: [1, 2, 3],
+          footerTemplate: [4, 5, 6],
+          preferCSSPageSize: 'no'
+        };
+
+        // These will hard crash in Chromium unless we type-check
+        for (const [key, value] of Object.entries(badTypes)) {
+          const param = { [key]: value };
+
+          const src = 'data:text/html,%3Ch1%3EHello%2C%20World!%3C%2Fh1%3E';
+          await loadWebView(w, { src });
+          await expect(w.executeJavaScript(`webview.printToPDF(${JSON.stringify(param)})`)).to.eventually.be.rejected();
+        }
+      });
+
+      it('can print to PDF', async () => {
+        const src = 'data:text/html,%3Ch1%3EHello%2C%20World!%3C%2Fh1%3E';
+        await loadWebView(w, { src });
+
+        const data = await w.executeJavaScript('webview.printToPDF({})');
+        expect(data).to.be.an.instanceof(Uint8Array).that.is.not.empty();
+      });
+    });
+
+    describe('DOM events', () => {
+      /*
+      let div;
+
+      beforeEach(() => {
+        div = document.createElement('div');
+        div.style.width = '100px';
+        div.style.height = '10px';
+        div.style.overflow = 'hidden';
+        webview.style.height = '100%';
+        webview.style.width = '100%';
+      });
+
+      afterEach(() => {
+        if (div != null) div.remove();
+      });
+      */
+
+      for (const [description, sandbox] of [
+        ['without sandbox', false] as const,
+        ['with sandbox', true] as const
+      ]) {
+        describe(description, () => {
+        // TODO(nornagon): disabled during chromium roll 2019-06-11 due to a
+        // 'ResizeObserver loop limit exceeded' error on Windows
+        /*
+          xit('emits resize events', async () => {
+            const firstResizeSignal = waitForEvent(webview, 'resize');
+            const domReadySignal = waitForEvent(webview, 'dom-ready');
+
+            webview.src = `file://${fixtures}/pages/a.html`;
+            webview.webpreferences = `sandbox=${sandbox ? 'yes' : 'no'}`;
+            div.appendChild(webview);
+            document.body.appendChild(div);
+
+            const firstResizeEvent = await firstResizeSignal;
+            expect(firstResizeEvent.target).to.equal(webview);
+            expect(firstResizeEvent.newWidth).to.equal(100);
+            expect(firstResizeEvent.newHeight).to.equal(10);
+
+            await domReadySignal;
+
+            const secondResizeSignal = waitForEvent(webview, 'resize');
+
+            const newWidth = 1234;
+            const newHeight = 789;
+            div.style.width = `${newWidth}px`;
+            div.style.height = `${newHeight}px`;
+
+            const secondResizeEvent = await secondResizeSignal;
+            expect(secondResizeEvent.target).to.equal(webview);
+            expect(secondResizeEvent.newWidth).to.equal(newWidth);
+            expect(secondResizeEvent.newHeight).to.equal(newHeight);
+          });
+          */
+
+          it('emits focus event', async () => {
+            await loadWebViewAndWaitForEvent(w, {
+              src: `file://${fixtures}/pages/a.html`,
+              webpreferences: `sandbox=${sandbox ? 'yes' : 'no'}`
+            }, 'dom-ready');
+
+            // If this test fails, check if webview.focus() still works.
+            await w.executeJavaScript(`new Promise(resolve => {
+              webview.addEventListener('focus', () => resolve(), {once: true});
+              webview.focus();
+            })`);
+          });
+        });
+      }
+    });
+
+    // TODO(miniak): figure out why this is failing on windows
+    ifdescribe(process.platform !== 'win32')('<webview>.capturePage()', () => {
+      it('returns a Promise with a NativeImage', async () => {
+        const src = 'data:text/html,%3Ch1%3EHello%2C%20World!%3C%2Fh1%3E';
+        await loadWebViewAndWaitForEvent(w, { src }, 'did-stop-loading');
+
+        // Retry a few times due to flake.
+        for (let i = 0; i < 5; i++) {
+          try {
+            const image = await w.executeJavaScript('webview.capturePage()');
+            const imgBuffer = image.toPNG();
+
+            // Check the 25th byte in the PNG.
+            // Values can be 0,2,3,4, or 6. We want 6, which is RGB + Alpha
+            expect(imgBuffer[25]).to.equal(6);
+            return;
+          } catch (e) {
+            /* drop the error */
+          }
+        }
+        expect(false).to.be.true('could not successfully capture the page');
+      });
+    });
+
+    // FIXME(zcbenz): Disabled because of moving to OOPIF webview.
+    xdescribe('setDevToolsWebContents() API', () => {
+      /*
+      it('sets webContents of webview as devtools', async () => {
+        const webview2 = new WebView();
+        loadWebView(webview2);
+
+        // Setup an event handler for further usage.
+        const waitForDomReady = waitForEvent(webview2, 'dom-ready');
+
+        loadWebView(webview, { src: 'about:blank' });
+        await waitForEvent(webview, 'dom-ready');
+        webview.getWebContents().setDevToolsWebContents(webview2.getWebContents());
+        webview.getWebContents().openDevTools();
+
+        await waitForDomReady;
+
+        // Its WebContents should be a DevTools.
+        const devtools = webview2.getWebContents();
+        expect(devtools.getURL().startsWith('devtools://devtools')).to.be.true();
+
+        const name = await devtools.executeJavaScript('InspectorFrontendHost.constructor.name');
+        document.body.removeChild(webview2);
+
+        expect(name).to.be.equal('InspectorFrontendHostImpl');
+      });
+      */
+    });
+  });
+
+  describe('basic auth', () => {
+    let w: WebContents;
+    before(async () => {
+      const window = new BrowserWindow({
+        show: false,
+        webPreferences: {
+          webviewTag: true,
+          nodeIntegration: true,
+          contextIsolation: false
+        }
+      });
+      await window.loadURL(`file://${fixtures}/pages/blank.html`);
+      w = window.webContents;
+    });
+    afterEach(async () => {
+      await w.executeJavaScript(`{
+        document.querySelectorAll('webview').forEach(el => el.remove())
+      }`);
+    });
+    after(closeAllWindows);
+
+    it('should authenticate with correct credentials', async () => {
+      const message = 'Authenticated';
+      const server = http.createServer((req, res) => {
+        const credentials = auth(req)!;
+        if (credentials.name === 'test' && credentials.pass === 'test') {
+          res.end(message);
+        } else {
+          res.end('failed');
+        }
+      });
+      defer(() => {
+        server.close();
+      });
+      await new Promise<void>(resolve => server.listen(0, '127.0.0.1', resolve));
+      const port = (server.address() as AddressInfo).port;
+      const e = await loadWebViewAndWaitForEvent(w, {
+        nodeintegration: 'on',
+        webpreferences: 'contextIsolation=no',
+        src: `file://${fixtures}/pages/basic-auth.html?port=${port}`
+      }, 'ipc-message');
+      expect(e.channel).to.equal(message);
+    });
   });
 });
diff --git a/spec/webview-spec.js b/spec/webview-spec.js
deleted file mode 100644
index 226b7c670db4d..0000000000000
--- a/spec/webview-spec.js
+++ /dev/null
@@ -1,431 +0,0 @@
-const { expect } = require('chai');
-const path = require('path');
-const http = require('http');
-const url = require('url');
-const { ipcRenderer } = require('electron');
-const { emittedOnce, waitForEvent } = require('./events-helpers');
-const { ifdescribe, ifit, delay } = require('./spec-helpers');
-
-const features = process._linkedBinding('electron_common_features');
-const nativeModulesEnabled = !process.env.ELECTRON_SKIP_NATIVE_MODULE_TESTS;
-
-/* Most of the APIs here don't use standard callbacks */
-/* eslint-disable standard/no-callback-literal */
-
-describe('<webview> tag', function () {
-  this.timeout(3 * 60 * 1000);
-
-  const fixtures = path.join(__dirname, 'fixtures');
-  let webview = null;
-
-  const loadWebView = async (webview, attributes = {}) => {
-    for (const [name, value] of Object.entries(attributes)) {
-      webview.setAttribute(name, value);
-    }
-    document.body.appendChild(webview);
-    await waitForEvent(webview, 'did-finish-load');
-    return webview;
-  };
-
-  const startLoadingWebViewAndWaitForMessage = async (webview, attributes = {}) => {
-    loadWebView(webview, attributes); // Don't wait for load to be finished.
-    const event = await waitForEvent(webview, 'console-message');
-    return event.message;
-  };
-
-  beforeEach(() => {
-    webview = new WebView();
-  });
-
-  afterEach(() => {
-    if (!document.body.contains(webview)) {
-      document.body.appendChild(webview);
-    }
-    webview.remove();
-  });
-
-  // FIXME(zcbenz): Disabled because of moving to OOPIF webview.
-  xdescribe('setDevToolsWebContents() API', () => {
-    it('sets webContents of webview as devtools', async () => {
-      const webview2 = new WebView();
-      loadWebView(webview2);
-
-      // Setup an event handler for further usage.
-      const waitForDomReady = waitForEvent(webview2, 'dom-ready');
-
-      loadWebView(webview, { src: 'about:blank' });
-      await waitForEvent(webview, 'dom-ready');
-      webview.getWebContents().setDevToolsWebContents(webview2.getWebContents());
-      webview.getWebContents().openDevTools();
-
-      await waitForDomReady;
-
-      // Its WebContents should be a DevTools.
-      const devtools = webview2.getWebContents();
-      expect(devtools.getURL().startsWith('devtools://devtools')).to.be.true();
-
-      const name = await devtools.executeJavaScript('InspectorFrontendHost.constructor.name');
-      document.body.removeChild(webview2);
-
-      expect(name).to.be.equal('InspectorFrontendHostImpl');
-    });
-  });
-
-  describe('<webview>.reload()', () => {
-    it('should emit beforeunload handler', async () => {
-      await loadWebView(webview, {
-        nodeintegration: 'on',
-        webpreferences: 'contextIsolation=no',
-        src: `file://${fixtures}/pages/beforeunload-false.html`
-      });
-
-      // Event handler has to be added before reload.
-      const waitForOnbeforeunload = waitForEvent(webview, 'ipc-message');
-
-      webview.reload();
-
-      const { channel } = await waitForOnbeforeunload;
-      expect(channel).to.equal('onbeforeunload');
-    });
-  });
-
-  describe('<webview>.goForward()', () => {
-    it('should work after a replaced history entry', (done) => {
-      let loadCount = 1;
-      const listener = (e) => {
-        if (loadCount === 1) {
-          expect(e.channel).to.equal('history');
-          expect(e.args[0]).to.equal(1);
-          expect(webview.canGoBack()).to.be.false();
-          expect(webview.canGoForward()).to.be.false();
-        } else if (loadCount === 2) {
-          expect(e.channel).to.equal('history');
-          expect(e.args[0]).to.equal(2);
-          expect(webview.canGoBack()).to.be.false();
-          expect(webview.canGoForward()).to.be.true();
-          webview.removeEventListener('ipc-message', listener);
-        }
-      };
-
-      const loadListener = () => {
-        try {
-          if (loadCount === 1) {
-            webview.src = `file://${fixtures}/pages/base-page.html`;
-          } else if (loadCount === 2) {
-            expect(webview.canGoBack()).to.be.true();
-            expect(webview.canGoForward()).to.be.false();
-
-            webview.goBack();
-          } else if (loadCount === 3) {
-            webview.goForward();
-          } else if (loadCount === 4) {
-            expect(webview.canGoBack()).to.be.true();
-            expect(webview.canGoForward()).to.be.false();
-
-            webview.removeEventListener('did-finish-load', loadListener);
-            done();
-          }
-
-          loadCount += 1;
-        } catch (e) {
-          done(e);
-        }
-      };
-
-      webview.addEventListener('ipc-message', listener);
-      webview.addEventListener('did-finish-load', loadListener);
-
-      loadWebView(webview, {
-        nodeintegration: 'on',
-        src: `file://${fixtures}/pages/history-replace.html`
-      });
-    });
-  });
-
-  // FIXME: https://github.com/electron/electron/issues/19397
-  xdescribe('<webview>.clearHistory()', () => {
-    it('should clear the navigation history', async () => {
-      const message = waitForEvent(webview, 'ipc-message');
-      await loadWebView(webview, {
-        nodeintegration: 'on',
-        src: `file://${fixtures}/pages/history.html`
-      });
-      const event = await message;
-
-      expect(event.channel).to.equal('history');
-      expect(event.args[0]).to.equal(2);
-      expect(webview.canGoBack()).to.be.true();
-
-      webview.clearHistory();
-      expect(webview.canGoBack()).to.be.false();
-    });
-  });
-
-  describe('basic auth', () => {
-    const auth = require('basic-auth');
-
-    it('should authenticate with correct credentials', (done) => {
-      const message = 'Authenticated';
-      const server = http.createServer((req, res) => {
-        const credentials = auth(req);
-        if (credentials.name === 'test' && credentials.pass === 'test') {
-          res.end(message);
-        } else {
-          res.end('failed');
-        }
-        server.close();
-      });
-      server.listen(0, '127.0.0.1', () => {
-        const port = server.address().port;
-        webview.addEventListener('ipc-message', (e) => {
-          try {
-            expect(e.channel).to.equal(message);
-            done();
-          } catch (e) {
-            done(e);
-          }
-        });
-        loadWebView(webview, {
-          nodeintegration: 'on',
-          webpreferences: 'contextIsolation=no',
-          src: `file://${fixtures}/pages/basic-auth.html?port=${port}`
-        });
-      });
-    });
-  });
-
-  describe('executeJavaScript', () => {
-    it('can return the result of the executed script', async () => {
-      await loadWebView(webview, {
-        src: 'about:blank'
-      });
-
-      const jsScript = "'4'+2";
-      const expectedResult = '42';
-
-      const result = await webview.executeJavaScript(jsScript);
-      expect(result).to.equal(expectedResult);
-    });
-  });
-
-  it('supports inserting CSS', async () => {
-    await loadWebView(webview, { src: `file://${fixtures}/pages/base-page.html` });
-    await webview.insertCSS('body { background-repeat: round; }');
-    const result = await webview.executeJavaScript('window.getComputedStyle(document.body).getPropertyValue("background-repeat")');
-    expect(result).to.equal('round');
-  });
-
-  it('supports removing inserted CSS', async () => {
-    await loadWebView(webview, { src: `file://${fixtures}/pages/base-page.html` });
-    const key = await webview.insertCSS('body { background-repeat: round; }');
-    await webview.removeInsertedCSS(key);
-    const result = await webview.executeJavaScript('window.getComputedStyle(document.body).getPropertyValue("background-repeat")');
-    expect(result).to.equal('repeat');
-  });
-
-  describe('sendInputEvent', () => {
-    it('can send keyboard event', async () => {
-      loadWebView(webview, {
-        nodeintegration: 'on',
-        webpreferences: 'contextIsolation=no',
-        src: `file://${fixtures}/pages/onkeyup.html`
-      });
-      await waitForEvent(webview, 'dom-ready');
-
-      const waitForIpcMessage = waitForEvent(webview, 'ipc-message');
-      webview.sendInputEvent({
-        type: 'keyup',
-        keyCode: 'c',
-        modifiers: ['shift']
-      });
-
-      const { channel, args } = await waitForIpcMessage;
-      expect(channel).to.equal('keyup');
-      expect(args).to.deep.equal(['C', 'KeyC', 67, true, false]);
-    });
-
-    it('can send mouse event', async () => {
-      loadWebView(webview, {
-        nodeintegration: 'on',
-        webpreferences: 'contextIsolation=no',
-        src: `file://${fixtures}/pages/onmouseup.html`
-      });
-      await waitForEvent(webview, 'dom-ready');
-
-      const waitForIpcMessage = waitForEvent(webview, 'ipc-message');
-      webview.sendInputEvent({
-        type: 'mouseup',
-        modifiers: ['ctrl'],
-        x: 10,
-        y: 20
-      });
-
-      const { channel, args } = await waitForIpcMessage;
-      expect(channel).to.equal('mouseup');
-      expect(args).to.deep.equal([10, 20, false, true]);
-    });
-  });
-
-  describe('media-started-playing media-paused events', () => {
-    beforeEach(function () {
-      if (!document.createElement('audio').canPlayType('audio/wav')) {
-        this.skip();
-      }
-    });
-
-    it('emits when audio starts and stops playing', async () => {
-      await loadWebView(webview, { src: `file://${fixtures}/pages/base-page.html` });
-
-      // With the new autoplay policy, audio elements must be unmuted
-      // see https://goo.gl/xX8pDD.
-      const source = `
-        const audio = document.createElement("audio")
-        audio.src = "../assets/tone.wav"
-        document.body.appendChild(audio);
-        audio.play()
-      `;
-      webview.executeJavaScript(source, true);
-      await waitForEvent(webview, 'media-started-playing');
-
-      webview.executeJavaScript('document.querySelector("audio").pause()', true);
-      await waitForEvent(webview, 'media-paused');
-    });
-  });
-
-  describe('<webview>.getWebContentsId', () => {
-    it('can return the WebContents ID', async () => {
-      const src = 'about:blank';
-      await loadWebView(webview, { src });
-
-      expect(webview.getWebContentsId()).to.be.a('number');
-    });
-  });
-
-  // TODO(nornagon): this seems to have become much less reliable as of
-  // https://github.com/electron/electron/pull/32419. Tracked at
-  // https://github.com/electron/electron/issues/32705.
-  describe.skip('<webview>.capturePage()', () => {
-    before(function () {
-      // TODO(miniak): figure out why this is failing on windows
-      if (process.platform === 'win32') {
-        this.skip();
-      }
-    });
-
-    it('returns a Promise with a NativeImage', async () => {
-      const src = 'data:text/html,%3Ch1%3EHello%2C%20World!%3C%2Fh1%3E';
-      await loadWebView(webview, { src });
-
-      const image = await webview.capturePage();
-      const imgBuffer = image.toPNG();
-
-      // Check the 25th byte in the PNG.
-      // Values can be 0,2,3,4, or 6. We want 6, which is RGB + Alpha
-      expect(imgBuffer[25]).to.equal(6);
-    });
-  });
-
-  ifdescribe(features.isPrintingEnabled())('<webview>.printToPDF()', () => {
-    it('rejects on incorrectly typed parameters', async () => {
-      const badTypes = {
-        landscape: [],
-        displayHeaderFooter: '123',
-        printBackground: 2,
-        scale: 'not-a-number',
-        pageSize: 'IAmAPageSize',
-        margins: 'terrible',
-        pageRanges: { oops: 'im-not-the-right-key' },
-        headerTemplate: [1, 2, 3],
-        footerTemplate: [4, 5, 6],
-        preferCSSPageSize: 'no'
-      };
-
-      // These will hard crash in Chromium unless we type-check
-      for (const [key, value] of Object.entries(badTypes)) {
-        const param = { [key]: value };
-
-        const src = 'data:text/html,%3Ch1%3EHello%2C%20World!%3C%2Fh1%3E';
-        await loadWebView(webview, { src });
-        await expect(webview.printToPDF(param)).to.eventually.be.rejected();
-      }
-    });
-
-    it('can print to PDF', async () => {
-      const src = 'data:text/html,%3Ch1%3EHello%2C%20World!%3C%2Fh1%3E';
-      await loadWebView(webview, { src });
-
-      const data = await webview.printToPDF({});
-      expect(data).to.be.an.instanceof(Uint8Array).that.is.not.empty();
-    });
-  });
-
-  describe('DOM events', () => {
-    let div;
-
-    beforeEach(() => {
-      div = document.createElement('div');
-      div.style.width = '100px';
-      div.style.height = '10px';
-      div.style.overflow = 'hidden';
-      webview.style.height = '100%';
-      webview.style.width = '100%';
-    });
-
-    afterEach(() => {
-      if (div != null) div.remove();
-    });
-
-    const generateSpecs = (description, sandbox) => {
-      describe(description, () => {
-        // TODO(nornagon): disabled during chromium roll 2019-06-11 due to a
-        // 'ResizeObserver loop limit exceeded' error on Windows
-        xit('emits resize events', async () => {
-          const firstResizeSignal = waitForEvent(webview, 'resize');
-          const domReadySignal = waitForEvent(webview, 'dom-ready');
-
-          webview.src = `file://${fixtures}/pages/a.html`;
-          webview.webpreferences = `sandbox=${sandbox ? 'yes' : 'no'}`;
-          div.appendChild(webview);
-          document.body.appendChild(div);
-
-          const firstResizeEvent = await firstResizeSignal;
-          expect(firstResizeEvent.target).to.equal(webview);
-          expect(firstResizeEvent.newWidth).to.equal(100);
-          expect(firstResizeEvent.newHeight).to.equal(10);
-
-          await domReadySignal;
-
-          const secondResizeSignal = waitForEvent(webview, 'resize');
-
-          const newWidth = 1234;
-          const newHeight = 789;
-          div.style.width = `${newWidth}px`;
-          div.style.height = `${newHeight}px`;
-
-          const secondResizeEvent = await secondResizeSignal;
-          expect(secondResizeEvent.target).to.equal(webview);
-          expect(secondResizeEvent.newWidth).to.equal(newWidth);
-          expect(secondResizeEvent.newHeight).to.equal(newHeight);
-        });
-
-        it('emits focus event', async () => {
-          const domReadySignal = waitForEvent(webview, 'dom-ready');
-          webview.src = `file://${fixtures}/pages/a.html`;
-          webview.webpreferences = `sandbox=${sandbox ? 'yes' : 'no'}`;
-          document.body.appendChild(webview);
-
-          await domReadySignal;
-
-          // If this test fails, check if webview.focus() still works.
-          const focusSignal = waitForEvent(webview, 'focus');
-          webview.focus();
-
-          await focusSignal;
-        });
-      });
-    };
-
-    generateSpecs('without sandbox', false);
-    generateSpecs('with sandbox', true);
-  });
-});


Test patch: