Browse Source

test: make sure tests fail properly instead of timing out (#24316)

Milan Burda 4 years ago
parent
commit
c6db47182a

+ 16 - 26
spec-main/api-app-spec.ts

@@ -209,21 +209,17 @@ describe('app module', () => {
   });
 
   describe('app.requestSingleInstanceLock', () => {
-    it('prevents the second launch of app', function (done) {
+    it('prevents the second launch of app', async function () {
       this.timeout(120000);
       const appPath = path.join(fixturesPath, 'api', 'singleton');
       const first = cp.spawn(process.execPath, [appPath]);
-      first.once('exit', code => {
-        expect(code).to.equal(0);
-      });
+      await emittedOnce(first.stdout, 'data');
       // Start second app when received output.
-      first.stdout.once('data', () => {
-        const second = cp.spawn(process.execPath, [appPath]);
-        second.once('exit', code => {
-          expect(code).to.equal(1);
-          done();
-        });
-      });
+      const second = cp.spawn(process.execPath, [appPath]);
+      const [code2] = await emittedOnce(second, 'exit');
+      expect(code2).to.equal(1);
+      const [code1] = await emittedOnce(first, 'exit');
+      expect(code1).to.equal(0);
     });
 
     it('passes arguments to the second-instance event', async () => {
@@ -1003,34 +999,28 @@ describe('app module', () => {
       }
     });
 
-    it('does not launch for argument following a URL', done => {
+    it('does not launch for argument following a URL', async () => {
       const appPath = path.join(fixturesPath, 'api', 'quit-app');
       // App should exit with non 123 code.
       const first = cp.spawn(process.execPath, [appPath, 'electron-test:?', 'abc']);
-      first.once('exit', code => {
-        expect(code).to.not.equal(123);
-        done();
-      });
+      const [code] = await emittedOnce(first, 'exit');
+      expect(code).to.not.equal(123);
     });
 
-    it('launches successfully for argument following a file path', done => {
+    it('launches successfully for argument following a file path', async () => {
       const appPath = path.join(fixturesPath, 'api', 'quit-app');
       // App should exit with code 123.
       const first = cp.spawn(process.execPath, [appPath, 'e:\\abc', 'abc']);
-      first.once('exit', code => {
-        expect(code).to.equal(123);
-        done();
-      });
+      const [code] = await emittedOnce(first, 'exit');
+      expect(code).to.equal(123);
     });
 
-    it('launches successfully for multiple URIs following --', done => {
+    it('launches successfully for multiple URIs following --', async () => {
       const appPath = path.join(fixturesPath, 'api', 'quit-app');
       // App should exit with code 123.
       const first = cp.spawn(process.execPath, [appPath, '--', 'http://electronjs.org', 'electron-test://testdata']);
-      first.once('exit', code => {
-        expect(code).to.equal(123);
-        done();
-      });
+      const [code] = await emittedOnce(first, 'exit');
+      expect(code).to.equal(123);
     });
   });
 

+ 14 - 17
spec-main/api-auto-updater-spec.ts

@@ -1,16 +1,16 @@
 import { autoUpdater } from 'electron/main';
 import { expect } from 'chai';
 import { ifit, ifdescribe } from './spec-helpers';
+import { emittedOnce } from './events-helpers';
 
 ifdescribe(!process.mas)('autoUpdater module', function () {
   describe('checkForUpdates', function () {
-    ifit(process.platform === 'win32')('emits an error on Windows if the feed URL is not set', function (done) {
-      autoUpdater.once('error', function (error) {
-        expect(error.message).to.equal('Update URL is not set');
-        done();
-      });
+    ifit(process.platform === 'win32')('emits an error on Windows if the feed URL is not set', async function () {
+      const errorEvent = emittedOnce(autoUpdater, 'error');
       autoUpdater.setFeedURL({ url: '' });
       autoUpdater.checkForUpdates();
+      const [error] = await errorEvent;
+      expect(error.message).to.equal('Update URL is not set');
     });
   });
 
@@ -19,11 +19,10 @@ ifdescribe(!process.mas)('autoUpdater module', function () {
       expect(autoUpdater.getFeedURL()).to.equal('');
     });
 
-    ifit(process.platform === 'win32')('correctly fetches the previously set FeedURL', function (done) {
+    ifit(process.platform === 'win32')('correctly fetches the previously set FeedURL', function () {
       const updateURL = 'https://fake-update.electron.io';
       autoUpdater.setFeedURL({ url: updateURL });
       expect(autoUpdater.getFeedURL()).to.equal(updateURL);
-      done();
     });
   });
 
@@ -56,12 +55,11 @@ ifdescribe(!process.mas)('autoUpdater module', function () {
     });
 
     ifdescribe(process.platform === 'darwin')('on Mac', function () {
-      it('emits an error when the application is unsigned', done => {
-        autoUpdater.once('error', function (error) {
-          expect(error.message).equal('Could not get code signature for running application');
-          done();
-        });
+      it('emits an error when the application is unsigned', async () => {
+        const errorEvent = emittedOnce(autoUpdater, 'error');
         autoUpdater.setFeedURL({ url: '' });
+        const [error] = await errorEvent;
+        expect(error.message).equal('Could not get code signature for running application');
       });
 
       it('does not throw if default is the serverType', () => {
@@ -81,12 +79,11 @@ ifdescribe(!process.mas)('autoUpdater module', function () {
   });
 
   describe('quitAndInstall', () => {
-    ifit(process.platform === 'win32')('emits an error on Windows when no update is available', function (done) {
-      autoUpdater.once('error', function (error) {
-        expect(error.message).to.equal('No update available, can\'t quit and install');
-        done();
-      });
+    ifit(process.platform === 'win32')('emits an error on Windows when no update is available', async function () {
+      const errorEvent = emittedOnce(autoUpdater, 'error');
       autoUpdater.quitAndInstall();
+      const [error] = await errorEvent;
+      expect(error.message).to.equal('No update available, can\'t quit and install');
     });
   });
 });

+ 7 - 8
spec-main/api-browser-view-spec.ts

@@ -233,17 +233,16 @@ describe('BrowserView module', () => {
   });
 
   describe('window.open()', () => {
-    it('works in BrowserView', (done) => {
+    it('works in BrowserView', async () => {
       view = new BrowserView();
       w.setBrowserView(view);
-      view.webContents.once('new-window', (e, url, frameName, disposition, options, additionalFeatures) => {
-        e.preventDefault();
-        expect(url).to.equal('http://host/');
-        expect(frameName).to.equal('host');
-        expect(additionalFeatures[0]).to.equal('this-is-not-a-standard-feature');
-        done();
-      });
+      const newWindow = emittedOnce(view.webContents, 'new-window');
+      view.webContents.once('new-window', event => event.preventDefault());
       view.webContents.loadFile(path.join(fixtures, 'pages', 'window-open.html'));
+      const [, url, frameName,,, additionalFeatures] = await newWindow;
+      expect(url).to.equal('http://host/');
+      expect(frameName).to.equal('host');
+      expect(additionalFeatures[0]).to.equal('this-is-not-a-standard-feature');
     });
   });
 });

File diff suppressed because it is too large
+ 319 - 309
spec-main/api-browser-window-spec.ts


+ 29 - 65
spec-main/api-debugger-spec.ts

@@ -4,7 +4,7 @@ import * as path from 'path';
 import { AddressInfo } from 'net';
 import { BrowserWindow } from 'electron/main';
 import { closeAllWindows } from './window-helpers';
-import { emittedOnce } from './events-helpers';
+import { emittedOnce, emittedUntil } from './events-helpers';
 
 describe('debugger module', () => {
   const fixtures = path.resolve(__dirname, '..', 'spec', 'fixtures');
@@ -21,18 +21,11 @@ describe('debugger module', () => {
   afterEach(closeAllWindows);
 
   describe('debugger.attach', () => {
-    it('succeeds when devtools is already open', done => {
-      w.webContents.on('did-finish-load', () => {
-        w.webContents.openDevTools();
-        try {
-          w.webContents.debugger.attach();
-        } catch (err) {
-          done(`unexpected error : ${err}`);
-        }
-        expect(w.webContents.debugger.isAttached()).to.be.true();
-        done();
-      });
-      w.webContents.loadURL('about:blank');
+    it('succeeds when devtools is already open', async () => {
+      await w.webContents.loadURL('about:blank');
+      w.webContents.openDevTools();
+      w.webContents.debugger.attach();
+      expect(w.webContents.debugger.isAttached()).to.be.true();
     });
 
     it('fails when protocol version is not supported', done => {
@@ -44,49 +37,33 @@ describe('debugger module', () => {
       }
     });
 
-    it('attaches when no protocol version is specified', done => {
-      try {
-        w.webContents.debugger.attach();
-      } catch (err) {
-        done(`unexpected error : ${err}`);
-      }
+    it('attaches when no protocol version is specified', async () => {
+      w.webContents.debugger.attach();
       expect(w.webContents.debugger.isAttached()).to.be.true();
-      done();
     });
   });
 
   describe('debugger.detach', () => {
-    it('fires detach event', (done) => {
-      w.webContents.debugger.on('detach', (e, reason) => {
-        expect(reason).to.equal('target closed');
-        expect(w.webContents.debugger.isAttached()).to.be.false();
-        done();
-      });
-
-      try {
-        w.webContents.debugger.attach();
-      } catch (err) {
-        done(`unexpected error : ${err}`);
-      }
+    it('fires detach event', async () => {
+      const detach = emittedOnce(w.webContents.debugger, 'detach');
+      w.webContents.debugger.attach();
       w.webContents.debugger.detach();
+      const [, reason] = await detach;
+      expect(reason).to.equal('target closed');
+      expect(w.webContents.debugger.isAttached()).to.be.false();
     });
 
-    it('doesn\'t disconnect an active devtools session', done => {
+    it('doesn\'t disconnect an active devtools session', async () => {
       w.webContents.loadURL('about:blank');
-      try {
-        w.webContents.debugger.attach();
-      } catch (err) {
-        return done(`unexpected error : ${err}`);
-      }
+      const detach = emittedOnce(w.webContents.debugger, 'detach');
+      w.webContents.debugger.attach();
       w.webContents.openDevTools();
       w.webContents.once('devtools-opened', () => {
         w.webContents.debugger.detach();
       });
-      w.webContents.debugger.on('detach', () => {
-        expect(w.webContents.debugger.isAttached()).to.be.false();
-        expect((w as any).devToolsWebContents.isDestroyed()).to.be.false();
-        done();
-      });
+      await detach;
+      expect(w.webContents.debugger.isAttached()).to.be.false();
+      expect((w as any).devToolsWebContents.isDestroyed()).to.be.false();
     });
   });
 
@@ -130,33 +107,20 @@ describe('debugger module', () => {
       w.webContents.debugger.detach();
     });
 
-    it('fires message event', done => {
+    it('fires message event', async () => {
       const url = process.platform !== 'win32'
         ? `file://${path.join(fixtures, 'pages', 'a.html')}`
         : `file:///${path.join(fixtures, 'pages', 'a.html').replace(/\\/g, '/')}`;
       w.webContents.loadURL(url);
-
-      try {
-        w.webContents.debugger.attach();
-      } catch (err) {
-        done(`unexpected error : ${err}`);
-      }
-
-      w.webContents.debugger.on('message', (e, method, params) => {
-        if (method === 'Console.messageAdded') {
-          try {
-            expect(params.message.level).to.equal('log');
-            expect(params.message.url).to.equal(url);
-            expect(params.message.text).to.equal('a');
-            done();
-          } catch (e) {
-            done(e);
-          } finally {
-            w.webContents.debugger.detach();
-          }
-        }
-      });
+      w.webContents.debugger.attach();
+      const message = emittedUntil(w.webContents.debugger, 'message',
+        (event: Electron.Event, method: string) => method === 'Console.messageAdded');
       w.webContents.debugger.sendCommand('Console.enable');
+      const [,, params] = await message;
+      w.webContents.debugger.detach();
+      expect((params as any).message.level).to.equal('log');
+      expect((params as any).message.url).to.equal(url);
+      expect((params as any).message.text).to.equal('a');
     });
 
     it('returns error message when command fails', async () => {

+ 7 - 3
spec-main/api-menu-item-spec.ts

@@ -43,9 +43,13 @@ describe('MenuItems', () => {
       const menu = Menu.buildFromTemplate([{
         label: 'text',
         click: (item) => {
-          expect(item.constructor.name).to.equal('MenuItem');
-          expect(item.label).to.equal('text');
-          done();
+          try {
+            expect(item.constructor.name).to.equal('MenuItem');
+            expect(item.label).to.equal('text');
+            done();
+          } catch (e) {
+            done(e);
+          }
         }
       }]);
       menu._executeCommand({}, menu.items[0].commandId);

+ 12 - 4
spec-main/api-protocol-spec.ts

@@ -305,8 +305,12 @@ describe('protocol module', () => {
 
     it('can access request headers', (done) => {
       protocol.registerHttpProtocol(protocolName, (request) => {
-        expect(request).to.have.property('headers');
-        done();
+        try {
+          expect(request).to.have.property('headers');
+          done();
+        } catch (e) {
+          done(e);
+        }
       });
       ajax(protocolName + '://fake-host');
     });
@@ -597,8 +601,12 @@ describe('protocol module', () => {
 
     it('can access request headers', (done) => {
       protocol.interceptHttpProtocol('http', (request) => {
-        expect(request).to.have.property('headers');
-        done();
+        try {
+          expect(request).to.have.property('headers');
+          done();
+        } catch (e) {
+          done(e);
+        }
       });
       ajax('http://fake-host');
     });

+ 4 - 6
spec-main/api-remote-spec.ts

@@ -344,7 +344,7 @@ ifdescribe(features.isRemoteModuleEnabled())('remote module', () => {
   });
 
   describe('remote objects registry', () => {
-    it('does not dereference until the render view is deleted (regression)', (done) => {
+    it('does not dereference until the render view is deleted (regression)', async () => {
       const w = new BrowserWindow({
         show: false,
         webPreferences: {
@@ -353,12 +353,10 @@ ifdescribe(features.isRemoteModuleEnabled())('remote module', () => {
         }
       });
 
-      ipcMain.once('error-message', (event, message) => {
-        expect(message).to.match(/^Cannot call method 'getURL' on missing remote object/);
-        done();
-      });
-
+      const message = emittedOnce(ipcMain, 'error-message');
       w.loadFile(path.join(fixtures, 'api', 'render-view-deleted.html'));
+      const [, msg] = await message;
+      expect(msg).to.match(/^Cannot call method 'getURL' on missing remote object/);
     });
   });
 

+ 64 - 28
spec-main/api-session-spec.ts

@@ -423,15 +423,23 @@ describe('session module', () => {
                        </html>`;
 
       protocol.registerStringProtocol(scheme, (request, callback) => {
-        if (request.method === 'GET') {
-          callback({ data: content, mimeType: 'text/html' });
-        } else if (request.method === 'POST') {
-          const uuid = request.uploadData![1].blobUUID;
-          expect(uuid).to.be.a('string');
-          session.defaultSession.getBlobData(uuid!).then(result => {
-            expect(result.toString()).to.equal(postData);
-            done();
-          });
+        try {
+          if (request.method === 'GET') {
+            callback({ data: content, mimeType: 'text/html' });
+          } else if (request.method === 'POST') {
+            const uuid = request.uploadData![1].blobUUID;
+            expect(uuid).to.be.a('string');
+            session.defaultSession.getBlobData(uuid!).then(result => {
+              try {
+                expect(result.toString()).to.equal(postData);
+                done();
+              } catch (e) {
+                done(e);
+              }
+            });
+          }
+        } catch (e) {
+          done(e);
         }
       });
       const w = new BrowserWindow({ show: false });
@@ -618,8 +626,12 @@ describe('session module', () => {
       session.defaultSession.once('will-download', function (e, item) {
         item.savePath = downloadFilePath;
         item.on('done', function (e, state) {
-          assertDownload(state, item);
-          done();
+          try {
+            assertDownload(state, item);
+            done();
+          } catch (e) {
+            done(e);
+          }
         });
       });
       session.defaultSession.downloadURL(`${url}:${port}`);
@@ -631,8 +643,12 @@ describe('session module', () => {
       w.webContents.session.once('will-download', function (e, item) {
         item.savePath = downloadFilePath;
         item.on('done', function (e, state) {
-          assertDownload(state, item);
-          done();
+          try {
+            assertDownload(state, item);
+            done();
+          } catch (e) {
+            done(e);
+          }
         });
       });
       w.webContents.downloadURL(`${url}:${port}`);
@@ -649,8 +665,12 @@ describe('session module', () => {
       w.webContents.session.once('will-download', function (e, item) {
         item.savePath = downloadFilePath;
         item.on('done', function (e, state) {
-          assertDownload(state, item, true);
-          done();
+          try {
+            assertDownload(state, item, true);
+            done();
+          } catch (e) {
+            done(e);
+          }
         });
       });
       w.webContents.downloadURL(`${protocolName}://item`);
@@ -687,13 +707,17 @@ describe('session module', () => {
       w.webContents.session.once('will-download', function (e, item) {
         item.savePath = downloadFilePath;
         item.on('done', function (e, state) {
-          expect(state).to.equal('cancelled');
-          expect(item.getFilename()).to.equal('mock.pdf');
-          expect(item.getMimeType()).to.equal('application/pdf');
-          expect(item.getReceivedBytes()).to.equal(0);
-          expect(item.getTotalBytes()).to.equal(mockPDF.length);
-          expect(item.getContentDisposition()).to.equal(contentDisposition);
-          done();
+          try {
+            expect(state).to.equal('cancelled');
+            expect(item.getFilename()).to.equal('mock.pdf');
+            expect(item.getMimeType()).to.equal('application/pdf');
+            expect(item.getReceivedBytes()).to.equal(0);
+            expect(item.getTotalBytes()).to.equal(mockPDF.length);
+            expect(item.getContentDisposition()).to.equal(contentDisposition);
+            done();
+          } catch (e) {
+            done(e);
+          }
         });
         item.cancel();
       });
@@ -712,8 +736,12 @@ describe('session module', () => {
       w.webContents.session.once('will-download', function (e, item) {
         item.savePath = downloadFilePath;
         item.on('done', function () {
-          expect(item.getFilename()).to.equal('download.pdf');
-          done();
+          try {
+            expect(item.getFilename()).to.equal('download.pdf');
+            done();
+          } catch (e) {
+            done(e);
+          }
         });
         item.cancel();
       });
@@ -744,8 +772,12 @@ describe('session module', () => {
         item.setSavePath(filePath);
         item.setSaveDialogOptions(options);
         item.on('done', function () {
-          expect(item.getSaveDialogOptions()).to.deep.equal(options);
-          done();
+          try {
+            expect(item.getSaveDialogOptions()).to.deep.equal(options);
+            done();
+          } catch (e) {
+            done(e);
+          }
         });
         item.cancel();
       });
@@ -761,8 +793,12 @@ describe('session module', () => {
             item.resume();
           }
           item.on('done', function (e, state) {
-            expect(state).to.equal('interrupted');
-            done();
+            try {
+              expect(state).to.equal('interrupted');
+              done();
+            } catch (e) {
+              done(e);
+            }
           });
         });
         w.webContents.downloadURL(`file://${path.join(__dirname, 'does-not-exist.txt')}`);

+ 182 - 181
spec-main/api-web-contents-spec.ts

@@ -8,7 +8,7 @@ import { BrowserWindow, ipcMain, webContents, session, WebContents, app } from '
 import { clipboard } from 'electron/common';
 import { emittedOnce } from './events-helpers';
 import { closeAllWindows } from './window-helpers';
-import { ifdescribe, ifit, delay } from './spec-helpers';
+import { ifdescribe, ifit, delay, defer } from './spec-helpers';
 
 const pdfjs = require('pdfjs-dist');
 const fixturesPath = path.resolve(__dirname, '..', 'spec', 'fixtures');
@@ -223,29 +223,23 @@ describe('webContents module', () => {
         server.close();
       });
 
-      it('works after page load and during subframe load', (done) => {
-        w.webContents.once('did-finish-load', () => {
-          // initiate a sub-frame load, then try and execute script during it
-          w.webContents.executeJavaScript(`
-            var iframe = document.createElement('iframe')
-            iframe.src = '${serverUrl}/slow'
-            document.body.appendChild(iframe)
-            null // don't return the iframe
-          `).then(() => {
-            w.webContents.executeJavaScript('console.log(\'hello\')').then(() => {
-              done();
-            });
-          });
-        });
-        w.loadURL(serverUrl);
-      });
-
-      it('executes after page load', (done) => {
-        w.webContents.executeJavaScript('(() => "test")()').then(result => {
-          expect(result).to.equal('test');
-          done();
-        });
+      it('works after page load and during subframe load', async () => {
+        await w.loadURL(serverUrl);
+        // initiate a sub-frame load, then try and execute script during it
+        await w.webContents.executeJavaScript(`
+          var iframe = document.createElement('iframe')
+          iframe.src = '${serverUrl}/slow'
+          document.body.appendChild(iframe)
+          null // don't return the iframe
+        `);
+        await w.webContents.executeJavaScript('console.log(\'hello\')');
+      });
+
+      it('executes after page load', async () => {
+        const executeJavaScript = w.webContents.executeJavaScript('(() => "test")()');
         w.loadURL(serverUrl);
+        const result = await executeJavaScript;
+        expect(result).to.equal('test');
       });
     });
   });
@@ -467,13 +461,11 @@ describe('webContents module', () => {
 
   describe('getWebPreferences() API', () => {
     afterEach(closeAllWindows);
-    it('should not crash when called for devTools webContents', (done) => {
+    it('should not crash when called for devTools webContents', async () => {
       const w = new BrowserWindow({ show: false });
       w.webContents.openDevTools();
-      w.webContents.once('devtools-opened', () => {
-        expect(w.webContents.devToolsWebContents!.getWebPreferences()).to.be.null();
-        done();
-      });
+      await emittedOnce(w.webContents, 'devtools-opened');
+      expect(w.webContents.devToolsWebContents!.getWebPreferences()).to.be.null();
     });
   });
 
@@ -652,71 +644,66 @@ describe('webContents module', () => {
     });
     afterEach(closeAllWindows);
 
-    it('can send keydown events', (done) => {
-      ipcMain.once('keydown', (event, key, code, keyCode, shiftKey, ctrlKey, altKey) => {
-        expect(key).to.equal('a');
-        expect(code).to.equal('KeyA');
-        expect(keyCode).to.equal(65);
-        expect(shiftKey).to.be.false();
-        expect(ctrlKey).to.be.false();
-        expect(altKey).to.be.false();
-        done();
-      });
+    it('can send keydown events', async () => {
+      const keydown = emittedOnce(ipcMain, 'keydown');
       w.webContents.sendInputEvent({ type: 'keyDown', keyCode: 'A' });
+      const [, key, code, keyCode, shiftKey, ctrlKey, altKey] = await keydown;
+      expect(key).to.equal('a');
+      expect(code).to.equal('KeyA');
+      expect(keyCode).to.equal(65);
+      expect(shiftKey).to.be.false();
+      expect(ctrlKey).to.be.false();
+      expect(altKey).to.be.false();
     });
 
-    it('can send keydown events with modifiers', (done) => {
-      ipcMain.once('keydown', (event, key, code, keyCode, shiftKey, ctrlKey, altKey) => {
-        expect(key).to.equal('Z');
-        expect(code).to.equal('KeyZ');
-        expect(keyCode).to.equal(90);
-        expect(shiftKey).to.be.true();
-        expect(ctrlKey).to.be.true();
-        expect(altKey).to.be.false();
-        done();
-      });
+    it('can send keydown events with modifiers', async () => {
+      const keydown = emittedOnce(ipcMain, 'keydown');
       w.webContents.sendInputEvent({ type: 'keyDown', keyCode: 'Z', modifiers: ['shift', 'ctrl'] });
+      const [, key, code, keyCode, shiftKey, ctrlKey, altKey] = await keydown;
+      expect(key).to.equal('Z');
+      expect(code).to.equal('KeyZ');
+      expect(keyCode).to.equal(90);
+      expect(shiftKey).to.be.true();
+      expect(ctrlKey).to.be.true();
+      expect(altKey).to.be.false();
     });
 
-    it('can send keydown events with special keys', (done) => {
-      ipcMain.once('keydown', (event, key, code, keyCode, shiftKey, ctrlKey, altKey) => {
-        expect(key).to.equal('Tab');
-        expect(code).to.equal('Tab');
-        expect(keyCode).to.equal(9);
-        expect(shiftKey).to.be.false();
-        expect(ctrlKey).to.be.false();
-        expect(altKey).to.be.true();
-        done();
-      });
+    it('can send keydown events with special keys', async () => {
+      const keydown = emittedOnce(ipcMain, 'keydown');
       w.webContents.sendInputEvent({ type: 'keyDown', keyCode: 'Tab', modifiers: ['alt'] });
+      const [, key, code, keyCode, shiftKey, ctrlKey, altKey] = await keydown;
+      expect(key).to.equal('Tab');
+      expect(code).to.equal('Tab');
+      expect(keyCode).to.equal(9);
+      expect(shiftKey).to.be.false();
+      expect(ctrlKey).to.be.false();
+      expect(altKey).to.be.true();
     });
 
-    it('can send char events', (done) => {
-      ipcMain.once('keypress', (event, key, code, keyCode, shiftKey, ctrlKey, altKey) => {
-        expect(key).to.equal('a');
-        expect(code).to.equal('KeyA');
-        expect(keyCode).to.equal(65);
-        expect(shiftKey).to.be.false();
-        expect(ctrlKey).to.be.false();
-        expect(altKey).to.be.false();
-        done();
-      });
+    it('can send char events', async () => {
+      const keypress = emittedOnce(ipcMain, 'keypress');
       w.webContents.sendInputEvent({ type: 'keyDown', keyCode: 'A' });
       w.webContents.sendInputEvent({ type: 'char', keyCode: 'A' });
+      const [, key, code, keyCode, shiftKey, ctrlKey, altKey] = await keypress;
+      expect(key).to.equal('a');
+      expect(code).to.equal('KeyA');
+      expect(keyCode).to.equal(65);
+      expect(shiftKey).to.be.false();
+      expect(ctrlKey).to.be.false();
+      expect(altKey).to.be.false();
     });
 
-    it('can send char events with modifiers', (done) => {
-      ipcMain.once('keypress', (event, key, code, keyCode, shiftKey, ctrlKey, altKey) => {
-        expect(key).to.equal('Z');
-        expect(code).to.equal('KeyZ');
-        expect(keyCode).to.equal(90);
-        expect(shiftKey).to.be.true();
-        expect(ctrlKey).to.be.true();
-        expect(altKey).to.be.false();
-        done();
-      });
+    it('can send char events with modifiers', async () => {
+      const keypress = emittedOnce(ipcMain, 'keypress');
       w.webContents.sendInputEvent({ type: 'keyDown', keyCode: 'Z' });
       w.webContents.sendInputEvent({ type: 'char', keyCode: 'Z', modifiers: ['shift', 'ctrl'] });
+      const [, key, code, keyCode, shiftKey, ctrlKey, altKey] = await keypress;
+      expect(key).to.equal('Z');
+      expect(code).to.equal('KeyZ');
+      expect(keyCode).to.equal(90);
+      expect(shiftKey).to.be.true();
+      expect(ctrlKey).to.be.true();
+      expect(altKey).to.be.false();
     });
   });
 
@@ -953,46 +940,54 @@ describe('webContents module', () => {
         e.sender.send(`${host}-zoom-set`);
       });
       ipcMain.on('host1-zoom-level', (e) => {
-        const zoomLevel = e.sender.getZoomLevel();
-        const expectedZoomLevel = hostZoomMap.host1;
-        expect(zoomLevel).to.equal(expectedZoomLevel);
-        if (finalNavigation) {
-          done();
-        } else {
-          w.loadURL(`${scheme}://host2`);
+        try {
+          const zoomLevel = e.sender.getZoomLevel();
+          const expectedZoomLevel = hostZoomMap.host1;
+          expect(zoomLevel).to.equal(expectedZoomLevel);
+          if (finalNavigation) {
+            done();
+          } else {
+            w.loadURL(`${scheme}://host2`);
+          }
+        } catch (e) {
+          done(e);
         }
       });
       ipcMain.once('host2-zoom-level', (e) => {
-        const zoomLevel = e.sender.getZoomLevel();
-        const expectedZoomLevel = hostZoomMap.host2;
-        expect(zoomLevel).to.equal(expectedZoomLevel);
-        finalNavigation = true;
-        w.webContents.goBack();
+        try {
+          const zoomLevel = e.sender.getZoomLevel();
+          const expectedZoomLevel = hostZoomMap.host2;
+          expect(zoomLevel).to.equal(expectedZoomLevel);
+          finalNavigation = true;
+          w.webContents.goBack();
+        } catch (e) {
+          done(e);
+        }
       });
       w.loadURL(`${scheme}://host1`);
     });
 
-    it('can propagate zoom level across same session', (done) => {
+    it('can propagate zoom level across same session', async () => {
       const w = new BrowserWindow({ show: false, webPreferences: { nodeIntegration: true } });
       const w2 = new BrowserWindow({ show: false });
-      w2.webContents.on('did-finish-load', () => {
-        const zoomLevel1 = w.webContents.zoomLevel;
-        expect(zoomLevel1).to.equal(hostZoomMap.host3);
 
-        const zoomLevel2 = w2.webContents.zoomLevel;
-        expect(zoomLevel1).to.equal(zoomLevel2);
+      defer(() => {
         w2.setClosable(true);
         w2.close();
-        done();
-      });
-      w.webContents.on('did-finish-load', () => {
-        w.webContents.zoomLevel = hostZoomMap.host3;
-        w2.loadURL(`${scheme}://host3`);
       });
-      w.loadURL(`${scheme}://host3`);
+
+      await w.loadURL(`${scheme}://host3`);
+      w.webContents.zoomLevel = hostZoomMap.host3;
+
+      await w2.loadURL(`${scheme}://host3`);
+      const zoomLevel1 = w.webContents.zoomLevel;
+      expect(zoomLevel1).to.equal(hostZoomMap.host3);
+
+      const zoomLevel2 = w2.webContents.zoomLevel;
+      expect(zoomLevel1).to.equal(zoomLevel2);
     });
 
-    it('cannot propagate zoom level across different session', (done) => {
+    it('cannot propagate zoom level across different session', async () => {
       const w = new BrowserWindow({ show: false, webPreferences: { nodeIntegration: true } });
       const w2 = new BrowserWindow({
         show: false,
@@ -1004,24 +999,24 @@ describe('webContents module', () => {
       protocol.registerStringProtocol(scheme, (request, callback) => {
         callback('hello');
       });
-      w2.webContents.on('did-finish-load', () => {
-        const zoomLevel1 = w.webContents.zoomLevel;
-        expect(zoomLevel1).to.equal(hostZoomMap.host3);
 
-        const zoomLevel2 = w2.webContents.zoomLevel;
-        expect(zoomLevel2).to.equal(0);
-        expect(zoomLevel1).to.not.equal(zoomLevel2);
-
-        protocol.unregisterProtocol(scheme);
+      defer(() => {
         w2.setClosable(true);
         w2.close();
-        done();
       });
-      w.webContents.on('did-finish-load', () => {
-        w.webContents.zoomLevel = hostZoomMap.host3;
-        w2.loadURL(`${scheme}://host3`);
-      });
-      w.loadURL(`${scheme}://host3`);
+
+      await w.loadURL(`${scheme}://host3`);
+      w.webContents.zoomLevel = hostZoomMap.host3;
+
+      await w2.loadURL(`${scheme}://host3`);
+      const zoomLevel1 = w.webContents.zoomLevel;
+      expect(zoomLevel1).to.equal(hostZoomMap.host3);
+
+      const zoomLevel2 = w2.webContents.zoomLevel;
+      expect(zoomLevel2).to.equal(0);
+      expect(zoomLevel1).to.not.equal(zoomLevel2);
+
+      protocol.unregisterProtocol(scheme);
     });
 
     it('can persist when it contains iframe', (done) => {
@@ -1036,12 +1031,17 @@ describe('webContents module', () => {
         const content = `<iframe src=${url}></iframe>`;
         w.webContents.on('did-frame-finish-load', (e, isMainFrame) => {
           if (!isMainFrame) {
-            const zoomLevel = w.webContents.zoomLevel;
-            expect(zoomLevel).to.equal(2.0);
+            try {
+              const zoomLevel = w.webContents.zoomLevel;
+              expect(zoomLevel).to.equal(2.0);
 
-            w.webContents.zoomLevel = 0;
-            server.close();
-            done();
+              w.webContents.zoomLevel = 0;
+              done();
+            } catch (e) {
+              done(e);
+            } finally {
+              server.close();
+            }
           }
         });
         w.webContents.on('dom-ready', () => {
@@ -1051,30 +1051,25 @@ describe('webContents module', () => {
       });
     });
 
-    it('cannot propagate when used with webframe', (done) => {
+    it('cannot propagate when used with webframe', async () => {
       const w = new BrowserWindow({ show: false, webPreferences: { nodeIntegration: true } });
-      let finalZoomLevel = 0;
-      const w2 = new BrowserWindow({
-        show: false
-      });
-      w2.webContents.on('did-finish-load', () => {
-        const zoomLevel1 = w.webContents.zoomLevel;
-        expect(zoomLevel1).to.equal(finalZoomLevel);
-
-        const zoomLevel2 = w2.webContents.zoomLevel;
-        expect(zoomLevel2).to.equal(0);
-        expect(zoomLevel1).to.not.equal(zoomLevel2);
+      const w2 = new BrowserWindow({ show: false });
 
-        w2.setClosable(true);
-        w2.close();
-        done();
-      });
-      ipcMain.once('temporary-zoom-set', (e) => {
-        const zoomLevel = e.sender.getZoomLevel();
-        w2.loadFile(path.join(fixturesPath, 'pages', 'c.html'));
-        finalZoomLevel = zoomLevel;
-      });
+      const temporaryZoomSet = emittedOnce(ipcMain, 'temporary-zoom-set');
       w.loadFile(path.join(fixturesPath, 'pages', 'webframe-zoom.html'));
+      await temporaryZoomSet;
+
+      const finalZoomLevel = w.webContents.getZoomLevel();
+      await w2.loadFile(path.join(fixturesPath, 'pages', 'c.html'));
+      const zoomLevel1 = w.webContents.zoomLevel;
+      const zoomLevel2 = w2.webContents.zoomLevel;
+
+      w2.setClosable(true);
+      w2.close();
+
+      expect(zoomLevel1).to.equal(finalZoomLevel);
+      expect(zoomLevel2).to.equal(0);
+      expect(zoomLevel1).to.not.equal(zoomLevel2);
     });
 
     describe('with unique domains', () => {
@@ -1168,13 +1163,9 @@ describe('webContents module', () => {
 
     afterEach(closeAllWindows);
 
-    it('does not emit current-render-view-deleted when speculative RVHs are deleted', (done) => {
+    it('does not emit current-render-view-deleted when speculative RVHs are deleted', async () => {
       const w = new BrowserWindow({ show: false });
       let currentRenderViewDeletedEmitted = false;
-      w.webContents.once('destroyed', () => {
-        expect(currentRenderViewDeletedEmitted).to.be.false('current-render-view-deleted was emitted');
-        done();
-      });
       const renderViewDeletedHandler = () => {
         currentRenderViewDeletedEmitted = true;
       };
@@ -1183,40 +1174,41 @@ describe('webContents module', () => {
         w.webContents.removeListener('current-render-view-deleted' as any, renderViewDeletedHandler);
         w.close();
       });
+      const destroyed = emittedOnce(w.webContents, 'destroyed');
       w.loadURL(`${serverUrl}/redirect-cross-site`);
+      await destroyed;
+      expect(currentRenderViewDeletedEmitted).to.be.false('current-render-view-deleted was emitted');
     });
 
-    it('emits current-render-view-deleted if the current RVHs are deleted', (done) => {
+    it('emits current-render-view-deleted if the current RVHs are deleted', async () => {
       const w = new BrowserWindow({ show: false });
       let currentRenderViewDeletedEmitted = false;
-      w.webContents.once('destroyed', () => {
-        expect(currentRenderViewDeletedEmitted).to.be.true('current-render-view-deleted wasn\'t emitted');
-        done();
-      });
       w.webContents.on('current-render-view-deleted' as any, () => {
         currentRenderViewDeletedEmitted = true;
       });
       w.webContents.on('did-finish-load', () => {
         w.close();
       });
+      const destroyed = emittedOnce(w.webContents, 'destroyed');
       w.loadURL(`${serverUrl}/redirect-cross-site`);
+      await destroyed;
+      expect(currentRenderViewDeletedEmitted).to.be.true('current-render-view-deleted wasn\'t emitted');
     });
 
-    it('emits render-view-deleted if any RVHs are deleted', (done) => {
+    it('emits render-view-deleted if any RVHs are deleted', async () => {
       const w = new BrowserWindow({ show: false });
       let rvhDeletedCount = 0;
-      w.webContents.once('destroyed', () => {
-        const expectedRenderViewDeletedEventCount = 1;
-        expect(rvhDeletedCount).to.equal(expectedRenderViewDeletedEventCount, 'render-view-deleted wasn\'t emitted the expected nr. of times');
-        done();
-      });
       w.webContents.on('render-view-deleted' as any, () => {
         rvhDeletedCount++;
       });
       w.webContents.on('did-finish-load', () => {
         w.close();
       });
+      const destroyed = emittedOnce(w.webContents, 'destroyed');
       w.loadURL(`${serverUrl}/redirect-cross-site`);
+      await destroyed;
+      const expectedRenderViewDeletedEventCount = 1;
+      expect(rvhDeletedCount).to.equal(expectedRenderViewDeletedEventCount, 'render-view-deleted wasn\'t emitted the expected nr. of times');
     });
   });
 
@@ -1304,13 +1296,17 @@ describe('webContents module', () => {
       const w = new BrowserWindow({ show: true });
       let count = 0;
       w.webContents.on('did-change-theme-color', (e, color) => {
-        if (count === 0) {
-          count += 1;
-          expect(color).to.equal('#FFEEDD');
-          w.loadFile(path.join(fixturesPath, 'pages', 'base-page.html'));
-        } else if (count === 1) {
-          expect(color).to.be.null();
-          done();
+        try {
+          if (count === 0) {
+            count += 1;
+            expect(color).to.equal('#FFEEDD');
+            w.loadFile(path.join(fixturesPath, 'pages', 'base-page.html'));
+          } else if (count === 1) {
+            expect(color).to.be.null();
+            done();
+          }
+        } catch (e) {
+          done(e);
         }
       });
       w.loadFile(path.join(fixturesPath, 'pages', 'theme-color.html'));
@@ -1374,9 +1370,14 @@ describe('webContents module', () => {
       const w = new BrowserWindow({ show: false });
       const server = http.createServer((req, res) => {
         if (req.url === '/should_have_referrer') {
-          expect(req.headers.referer).to.equal(`http://127.0.0.1:${(server.address() as AddressInfo).port}/`);
-          server.close();
-          return done();
+          try {
+            expect(req.headers.referer).to.equal(`http://127.0.0.1:${(server.address() as AddressInfo).port}/`);
+            return done();
+          } catch (e) {
+            return done(e);
+          } finally {
+            server.close();
+          }
         }
         res.end('<a id="a" href="/should_have_referrer" target="_blank">link</a>');
       });
@@ -1399,8 +1400,12 @@ describe('webContents module', () => {
       const w = new BrowserWindow({ show: false });
       const server = http.createServer((req, res) => {
         if (req.url === '/should_have_referrer') {
-          expect(req.headers.referer).to.equal(`http://127.0.0.1:${(server.address() as AddressInfo).port}/`);
-          return done();
+          try {
+            expect(req.headers.referer).to.equal(`http://127.0.0.1:${(server.address() as AddressInfo).port}/`);
+            return done();
+          } catch (e) {
+            return done(e);
+          }
         }
         res.end('');
       });
@@ -1640,8 +1645,7 @@ describe('webContents module', () => {
     });
 
     it('respects custom settings', async () => {
-      w.loadFile(path.join(__dirname, 'fixtures', 'api', 'print-to-pdf.html'));
-      await emittedOnce(w.webContents, 'did-finish-load');
+      await w.loadFile(path.join(__dirname, 'fixtures', 'api', 'print-to-pdf.html'));
 
       const data = await w.webContents.printToPDF({
         pageRanges: {
@@ -1676,15 +1680,12 @@ describe('webContents module', () => {
 
   describe('PictureInPicture video', () => {
     afterEach(closeAllWindows);
-    it('works as expected', (done) => {
+    it('works as expected', async () => {
       const w = new BrowserWindow({ show: false, webPreferences: { sandbox: true } });
-      w.webContents.once('did-finish-load', async () => {
-        const result = await w.webContents.executeJavaScript(
-          `runTest(${features.isPictureInPictureEnabled()})`, true);
-        expect(result).to.be.true();
-        done();
-      });
-      w.loadFile(path.join(fixturesPath, 'api', 'picture-in-picture.html'));
+      await w.loadFile(path.join(fixturesPath, 'api', 'picture-in-picture.html'));
+      const result = await w.webContents.executeJavaScript(
+        `runTest(${features.isPictureInPictureEnabled()})`, true);
+      expect(result).to.be.true();
     });
   });
 

+ 16 - 19
spec-main/asar-spec.ts

@@ -2,6 +2,7 @@ import { expect } from 'chai';
 import * as path from 'path';
 import { BrowserWindow, ipcMain } from 'electron/main';
 import { closeAllWindows } from './window-helpers';
+import { emittedOnce } from './events-helpers';
 
 describe('asar package', () => {
   const fixtures = path.join(__dirname, '..', 'spec', 'fixtures');
@@ -10,7 +11,7 @@ describe('asar package', () => {
   afterEach(closeAllWindows);
 
   describe('asar protocol', () => {
-    it('sets __dirname correctly', function (done) {
+    it('sets __dirname correctly', async function () {
       after(function () {
         ipcMain.removeAllListeners('dirname');
       });
@@ -24,14 +25,13 @@ describe('asar package', () => {
         }
       });
       const p = path.resolve(asarDir, 'web.asar', 'index.html');
-      ipcMain.once('dirname', function (event, dirname) {
-        expect(dirname).to.equal(path.dirname(p));
-        done();
-      });
+      const dirnameEvent = emittedOnce(ipcMain, 'dirname');
       w.loadFile(p);
+      const [, dirname] = await dirnameEvent;
+      expect(dirname).to.equal(path.dirname(p));
     });
 
-    it('loads script tag in html', function (done) {
+    it('loads script tag in html', async function () {
       after(function () {
         ipcMain.removeAllListeners('ping');
       });
@@ -45,14 +45,13 @@ describe('asar package', () => {
         }
       });
       const p = path.resolve(asarDir, 'script.asar', 'index.html');
+      const ping = emittedOnce(ipcMain, 'ping');
       w.loadFile(p);
-      ipcMain.once('ping', function (event, message) {
-        expect(message).to.equal('pong');
-        done();
-      });
+      const [, message] = await ping;
+      expect(message).to.equal('pong');
     });
 
-    it('loads video tag in html', function (done) {
+    it('loads video tag in html', async function () {
       this.timeout(60000);
 
       after(function () {
@@ -69,14 +68,12 @@ describe('asar package', () => {
       });
       const p = path.resolve(asarDir, 'video.asar', 'index.html');
       w.loadFile(p);
-      ipcMain.on('asar-video', function (event, message, error) {
-        if (message === 'ended') {
-          expect(error).to.be.null();
-          done();
-        } else if (message === 'error') {
-          done(error);
-        }
-      });
+      const [, message, error] = await emittedOnce(ipcMain, 'asar-video');
+      if (message === 'ended') {
+        expect(error).to.be.null();
+      } else if (message === 'error') {
+        throw new Error(error);
+      }
     });
   });
 });

+ 92 - 101
spec-main/chromium-spec.ts

@@ -10,7 +10,7 @@ import * as url from 'url';
 import * as ChildProcess from 'child_process';
 import { EventEmitter } from 'events';
 import { promisify } from 'util';
-import { ifit, ifdescribe, delay } from './spec-helpers';
+import { ifit, ifdescribe, delay, defer } from './spec-helpers';
 import { AddressInfo } from 'net';
 import { PipeTransport } from './pipe-transport';
 
@@ -257,22 +257,21 @@ describe('command line switches', () => {
   });
   describe('--lang switch', () => {
     const currentLocale = app.getLocale();
-    const testLocale = (locale: string, result: string, done: () => void) => {
+    const testLocale = async (locale: string, result: string) => {
       const appPath = path.join(fixturesPath, 'api', 'locale-check');
       const electronPath = process.execPath;
-      let output = '';
       appProcess = ChildProcess.spawn(electronPath, [appPath, `--lang=${locale}`]);
 
+      let output = '';
       appProcess.stdout.on('data', (data) => { output += data; });
-      appProcess.stdout.on('end', () => {
-        output = output.replace(/(\r\n|\n|\r)/gm, '');
-        expect(output).to.equal(result);
-        done();
-      });
+
+      await emittedOnce(appProcess.stdout, 'end');
+      output = output.replace(/(\r\n|\n|\r)/gm, '');
+      expect(output).to.equal(result);
     };
 
-    it('should set the locale', (done) => testLocale('fr', 'fr', done));
-    it('should not set an invalid locale', (done) => testLocale('asdfkl', currentLocale, done));
+    it('should set the locale', async () => testLocale('fr', 'fr'));
+    it('should not set an invalid locale', async () => testLocale('asdfkl', currentLocale));
   });
 
   describe('--remote-debugging-pipe switch', () => {
@@ -330,10 +329,15 @@ describe('command line switches', () => {
           appProcess!.stderr.removeAllListeners('data');
           const port = m[1];
           http.get(`http://127.0.0.1:${port}`, (res) => {
-            res.destroy();
-            expect(res.statusCode).to.eql(200);
-            expect(parseInt(res.headers['content-length']!)).to.be.greaterThan(0);
-            done();
+            try {
+              expect(res.statusCode).to.eql(200);
+              expect(parseInt(res.headers['content-length']!)).to.be.greaterThan(0);
+              done();
+            } catch (e) {
+              done(e);
+            } finally {
+              res.destroy();
+            }
           });
         }
       });
@@ -551,7 +555,7 @@ describe('chromium features', () => {
 
   describe('window.open', () => {
     for (const show of [true, false]) {
-      it(`inherits parent visibility over parent {show=${show}} option`, (done) => {
+      it(`inherits parent visibility over parent {show=${show}} option`, async () => {
         const w = new BrowserWindow({ show });
 
         // toggle visibility
@@ -561,12 +565,12 @@ describe('chromium features', () => {
           w.show();
         }
 
-        w.webContents.once('new-window', (e, url, frameName, disposition, options) => {
-          expect(options.show).to.equal(w.isVisible());
-          w.close();
-          done();
-        });
+        defer(() => { w.close(); });
+
+        const newWindow = emittedOnce(w.webContents, 'new-window');
         w.loadFile(path.join(fixturesPath, 'pages', 'window-open.html'));
+        const [,,,, options] = await newWindow;
+        expect(options.show).to.equal(w.isVisible());
       });
     }
 
@@ -602,7 +606,7 @@ describe('chromium features', () => {
       expect(preferences.javascript).to.be.false();
     });
 
-    it('handles cycles when merging the parent options into the child options', (done) => {
+    it('handles cycles when merging the parent options into the child options', async () => {
       const foo = {} as any;
       foo.bar = foo;
       foo.baz = {
@@ -614,22 +618,20 @@ describe('chromium features', () => {
       const w = new BrowserWindow({ show: false, foo: foo } as any);
 
       w.loadFile(path.join(fixturesPath, 'pages', 'window-open.html'));
-      w.webContents.once('new-window', (event, url, frameName, disposition, options) => {
-        expect(options.show).to.be.false();
-        expect((options as any).foo).to.deep.equal({
-          bar: undefined,
-          baz: {
-            hello: {
-              world: true
-            }
-          },
-          baz2: {
-            hello: {
-              world: true
-            }
+      const [,,,, options] = await emittedOnce(w.webContents, 'new-window');
+      expect(options.show).to.be.false();
+      expect((options as any).foo).to.deep.equal({
+        bar: undefined,
+        baz: {
+          hello: {
+            world: true
           }
-        });
-        done();
+        },
+        baz2: {
+          hello: {
+            world: true
+          }
+        }
       });
     });
 
@@ -959,44 +961,39 @@ describe('chromium features', () => {
         contents = null as any;
       });
 
-      it('cannot access localStorage', (done) => {
-        ipcMain.once('local-storage-response', (event, error) => {
-          expect(error).to.equal('Failed to read the \'localStorage\' property from \'Window\': Access is denied for this document.');
-          done();
-        });
+      it('cannot access localStorage', async () => {
+        const response = emittedOnce(ipcMain, 'local-storage-response');
         contents.loadURL(protocolName + '://host/localStorage');
+        const [, error] = await response;
+        expect(error).to.equal('Failed to read the \'localStorage\' property from \'Window\': Access is denied for this document.');
       });
 
-      it('cannot access sessionStorage', (done) => {
-        ipcMain.once('session-storage-response', (event, error) => {
-          expect(error).to.equal('Failed to read the \'sessionStorage\' property from \'Window\': Access is denied for this document.');
-          done();
-        });
+      it('cannot access sessionStorage', async () => {
+        const response = emittedOnce(ipcMain, 'session-storage-response');
         contents.loadURL(`${protocolName}://host/sessionStorage`);
+        const [, error] = await response;
+        expect(error).to.equal('Failed to read the \'sessionStorage\' property from \'Window\': Access is denied for this document.');
       });
 
-      it('cannot access WebSQL database', (done) => {
-        ipcMain.once('web-sql-response', (event, error) => {
-          expect(error).to.equal('Failed to execute \'openDatabase\' on \'Window\': Access to the WebDatabase API is denied in this context.');
-          done();
-        });
+      it('cannot access WebSQL database', async () => {
+        const response = emittedOnce(ipcMain, 'web-sql-response');
         contents.loadURL(`${protocolName}://host/WebSQL`);
+        const [, error] = await response;
+        expect(error).to.equal('Failed to execute \'openDatabase\' on \'Window\': Access to the WebDatabase API is denied in this context.');
       });
 
-      it('cannot access indexedDB', (done) => {
-        ipcMain.once('indexed-db-response', (event, error) => {
-          expect(error).to.equal('Failed to execute \'open\' on \'IDBFactory\': access to the Indexed Database API is denied in this context.');
-          done();
-        });
+      it('cannot access indexedDB', async () => {
+        const response = emittedOnce(ipcMain, 'indexed-db-response');
         contents.loadURL(`${protocolName}://host/indexedDB`);
+        const [, error] = await response;
+        expect(error).to.equal('Failed to execute \'open\' on \'IDBFactory\': access to the Indexed Database API is denied in this context.');
       });
 
-      it('cannot access cookie', (done) => {
-        ipcMain.once('cookie-response', (event, error) => {
-          expect(error).to.equal('Failed to set the \'cookie\' property on \'Document\': Access is denied for this document.');
-          done();
-        });
+      it('cannot access cookie', async () => {
+        const response = emittedOnce(ipcMain, 'cookie-response');
         contents.loadURL(`${protocolName}://host/cookie`);
+        const [, error] = await response;
+        expect(error).to.equal('Failed to set the \'cookie\' property on \'Document\': Access is denied for this document.');
       });
     });
 
@@ -1034,7 +1031,7 @@ describe('chromium features', () => {
       afterEach(closeAllWindows);
 
       const testLocalStorageAfterXSiteRedirect = (testTitle: string, extraPreferences = {}) => {
-        it(testTitle, (done) => {
+        it(testTitle, async () => {
           const w = new BrowserWindow({
             show: false,
             ...extraPreferences
@@ -1047,11 +1044,8 @@ describe('chromium features', () => {
             expect(url).to.equal(`${serverCrossSiteUrl}/redirected`);
             redirected = true;
           });
-          w.webContents.on('did-finish-load', () => {
-            expect(redirected).to.be.true('didnt redirect');
-            done();
-          });
-          w.loadURL(`${serverUrl}/redirect-cross-site`);
+          await w.loadURL(`${serverUrl}/redirect-cross-site`);
+          expect(redirected).to.be.true('didnt redirect');
         });
       };
 
@@ -1363,47 +1357,44 @@ describe('iframe using HTML fullscreen API while window is OS-fullscreened', ()
     server.close();
   });
 
-  it('can fullscreen from out-of-process iframes (OOPIFs)', done => {
-    ipcMain.once('fullscreenChange', async () => {
-      const fullscreenWidth = await w.webContents.executeJavaScript(
-        "document.querySelector('iframe').offsetWidth"
-      );
-      expect(fullscreenWidth > 0).to.be.true();
-
-      await w.webContents.executeJavaScript(
-        "document.querySelector('iframe').contentWindow.postMessage('exitFullscreen', '*')"
-      );
+  it('can fullscreen from out-of-process iframes (OOPIFs)', async () => {
+    const fullscreenChange = emittedOnce(ipcMain, 'fullscreenChange');
+    const html =
+      '<iframe style="width: 0" frameborder=0 src="http://localhost:8989" allowfullscreen></iframe>';
+    w.loadURL(`data:text/html,${html}`);
+    await fullscreenChange;
 
-      await delay(500);
+    const fullscreenWidth = await w.webContents.executeJavaScript(
+      "document.querySelector('iframe').offsetWidth"
+    );
+    expect(fullscreenWidth > 0).to.be.true();
 
-      const width = await w.webContents.executeJavaScript(
-        "document.querySelector('iframe').offsetWidth"
-      );
-      expect(width).to.equal(0);
+    await w.webContents.executeJavaScript(
+      "document.querySelector('iframe').contentWindow.postMessage('exitFullscreen', '*')"
+    );
 
-      done();
-    });
+    await delay(500);
 
-    const html =
-      '<iframe style="width: 0" frameborder=0 src="http://localhost:8989" allowfullscreen></iframe>';
-    w.loadURL(`data:text/html,${html}`);
+    const width = await w.webContents.executeJavaScript(
+      "document.querySelector('iframe').offsetWidth"
+    );
+    expect(width).to.equal(0);
   });
 
-  it('can fullscreen from in-process iframes', done => {
-    ipcMain.once('fullscreenChange', async () => {
-      const fullscreenWidth = await w.webContents.executeJavaScript(
-        "document.querySelector('iframe').offsetWidth"
-      );
-      expect(fullscreenWidth > 0).to.true();
+  it('can fullscreen from in-process iframes', async () => {
+    const fullscreenChange = emittedOnce(ipcMain, 'fullscreenChange');
+    w.loadFile(path.join(fixturesPath, 'pages', 'fullscreen-ipif.html'));
+    await fullscreenChange;
 
-      await w.webContents.executeJavaScript('document.exitFullscreen()');
-      const width = await w.webContents.executeJavaScript(
-        "document.querySelector('iframe').offsetWidth"
-      );
-      expect(width).to.equal(0);
-      done();
-    });
+    const fullscreenWidth = await w.webContents.executeJavaScript(
+      "document.querySelector('iframe').offsetWidth"
+    );
+    expect(fullscreenWidth > 0).to.true();
 
-    w.loadFile(path.join(fixturesPath, 'pages', 'fullscreen-ipif.html'));
+    await w.webContents.executeJavaScript('document.exitFullscreen()');
+    const width = await w.webContents.executeJavaScript(
+      "document.querySelector('iframe').offsetWidth"
+    );
+    expect(width).to.equal(0);
   });
 });

+ 4 - 5
spec-main/modules-spec.ts

@@ -4,6 +4,7 @@ import * as fs from 'fs';
 import { BrowserWindow } from 'electron/main';
 import { ifdescribe, ifit } from './spec-helpers';
 import { closeAllWindows } from './window-helpers';
+import { emittedOnce } from './events-helpers';
 import * as childProcess from 'child_process';
 
 const Module = require('module');
@@ -23,12 +24,10 @@ describe('modules support', () => {
         await expect(w.webContents.executeJavaScript('{ require(\'echo\'); null }')).to.be.fulfilled();
       });
 
-      ifit(features.isRunAsNodeEnabled())('can be required in node binary', function (done) {
+      ifit(features.isRunAsNodeEnabled())('can be required in node binary', async function () {
         const child = childProcess.fork(path.join(fixtures, 'module', 'echo.js'));
-        child.on('message', (msg) => {
-          expect(msg).to.equal('ok');
-          done();
-        });
+        const [msg] = await emittedOnce(child, 'message');
+        expect(msg).to.equal('ok');
       });
 
       ifit(process.platform === 'win32')('can be required if electron.exe is renamed', () => {

+ 27 - 38
spec-main/node-spec.ts

@@ -12,13 +12,12 @@ describe('node feature', () => {
   const fixtures = path.join(__dirname, '..', 'spec', 'fixtures');
   describe('child_process', () => {
     describe('child_process.fork', () => {
-      it('Works in browser process', (done) => {
+      it('Works in browser process', async () => {
         const child = childProcess.fork(path.join(fixtures, 'module', 'ping.js'));
-        child.on('message', (msg) => {
-          expect(msg).to.equal('message');
-          done();
-        });
+        const message = emittedOnce(child, 'message');
         child.send('message');
+        const [msg] = await message;
+        expect(msg).to.equal('message');
       });
     });
   });
@@ -199,7 +198,7 @@ describe('node feature', () => {
       child.stdout.on('data', listener);
     });
 
-    it('Supports starting the v8 inspector with --inspect and a provided port', (done) => {
+    it('Supports starting the v8 inspector with --inspect and a provided port', async () => {
       child = childProcess.spawn(process.execPath, ['--inspect=17364', path.join(fixtures, 'module', 'run-as-node.js')], {
         env: { ELECTRON_RUN_AS_NODE: 'true' }
       });
@@ -214,18 +213,16 @@ describe('node feature', () => {
 
       child.stderr.on('data', listener);
       child.stdout.on('data', listener);
-      child.on('exit', () => {
-        cleanup();
-        if (/^Debugger listening on ws:/m.test(output)) {
-          expect(output.trim()).to.contain(':17364', 'should be listening on port 17364');
-          done();
-        } else {
-          done(new Error(`Unexpected output: ${output.toString()}`));
-        }
-      });
+      await emittedOnce(child, 'exit');
+      cleanup();
+      if (/^Debugger listening on ws:/m.test(output)) {
+        expect(output.trim()).to.contain(':17364', 'should be listening on port 17364');
+      } else {
+        throw new Error(`Unexpected output: ${output.toString()}`);
+      }
     });
 
-    it('Does not start the v8 inspector when --inspect is after a -- argument', (done) => {
+    it('Does not start the v8 inspector when --inspect is after a -- argument', async () => {
       child = childProcess.spawn(process.execPath, [path.join(fixtures, 'module', 'noop.js'), '--', '--inspect']);
       exitPromise = emittedOnce(child, 'exit');
 
@@ -233,13 +230,10 @@ describe('node feature', () => {
       const listener = (data: Buffer) => { output += data; };
       child.stderr.on('data', listener);
       child.stdout.on('data', listener);
-      child.on('exit', () => {
-        if (output.trim().startsWith('Debugger listening on ws://')) {
-          done(new Error('Inspector was started when it should not have been'));
-        } else {
-          done();
-        }
-      });
+      await emittedOnce(child, 'exit');
+      if (output.trim().startsWith('Debugger listening on ws://')) {
+        throw new Error('Inspector was started when it should not have been');
+      }
     });
 
     // IPC Electron child process not supported on Windows
@@ -289,20 +283,17 @@ describe('node feature', () => {
       });
     });
 
-    it('Supports js binding', (done) => {
+    it('Supports js binding', async () => {
       child = childProcess.spawn(process.execPath, ['--inspect', path.join(fixtures, 'module', 'inspector-binding.js')], {
         env: { ELECTRON_RUN_AS_NODE: 'true' },
         stdio: ['ipc']
       }) as childProcess.ChildProcessWithoutNullStreams;
       exitPromise = emittedOnce(child, 'exit');
 
-      child.on('message', ({ cmd, debuggerEnabled, success }) => {
-        if (cmd === 'assert') {
-          expect(debuggerEnabled).to.be.true();
-          expect(success).to.be.true();
-          done();
-        }
-      });
+      const [{ cmd, debuggerEnabled, success }] = await emittedOnce(child, 'message');
+      expect(cmd).to.equal('assert');
+      expect(debuggerEnabled).to.be.true();
+      expect(success).to.be.true();
     });
   });
 
@@ -311,17 +302,15 @@ describe('node feature', () => {
     expect(result.status).to.equal(0);
   });
 
-  ifit(features.isRunAsNodeEnabled())('handles Promise timeouts correctly', (done) => {
+  ifit(features.isRunAsNodeEnabled())('handles Promise timeouts correctly', async () => {
     const scriptPath = path.join(fixtures, 'module', 'node-promise-timer.js');
     const child = childProcess.spawn(process.execPath, [scriptPath], {
       env: { ELECTRON_RUN_AS_NODE: 'true' }
     });
-    emittedOnce(child, 'exit').then(([code, signal]) => {
-      expect(code).to.equal(0);
-      expect(signal).to.equal(null);
-      child.kill();
-      done();
-    });
+    const [code, signal] = await emittedOnce(child, 'exit');
+    expect(code).to.equal(0);
+    expect(signal).to.equal(null);
+    child.kill();
   });
 
   it('performs microtask checkpoint correctly', (done) => {

+ 21 - 47
spec/api-web-frame-spec.js

@@ -39,70 +39,51 @@ describe('webFrame module', function () {
       childFrameElement.remove();
     });
 
-    it('executeJavaScript() yields results via a promise and a sync callback', done => {
+    it('executeJavaScript() yields results via a promise and a sync callback', async () => {
       let callbackResult, callbackError;
 
-      childFrame
+      const executeJavaScript = childFrame
         .executeJavaScript('1 + 1', (result, error) => {
           callbackResult = result;
           callbackError = error;
-        })
-        .then(
-          promiseResult => {
-            expect(promiseResult).to.equal(2);
-            done();
-          },
-          promiseError => {
-            done(promiseError);
-          }
-        );
+        });
 
       expect(callbackResult).to.equal(2);
       expect(callbackError).to.be.undefined();
+
+      const promiseResult = await executeJavaScript;
+      expect(promiseResult).to.equal(2);
     });
 
-    it('executeJavaScriptInIsolatedWorld() yields results via a promise and a sync callback', done => {
+    it('executeJavaScriptInIsolatedWorld() yields results via a promise and a sync callback', async () => {
       let callbackResult, callbackError;
 
-      childFrame
+      const executeJavaScriptInIsolatedWorld = childFrame
         .executeJavaScriptInIsolatedWorld(999, [{ code: '1 + 1' }], (result, error) => {
           callbackResult = result;
           callbackError = error;
-        })
-        .then(
-          promiseResult => {
-            expect(promiseResult).to.equal(2);
-            done();
-          },
-          promiseError => {
-            done(promiseError);
-          }
-        );
+        });
 
       expect(callbackResult).to.equal(2);
       expect(callbackError).to.be.undefined();
+
+      const promiseResult = await executeJavaScriptInIsolatedWorld;
+      expect(promiseResult).to.equal(2);
     });
 
-    it('executeJavaScript() yields errors via a promise and a sync callback', done => {
+    it('executeJavaScript() yields errors via a promise and a sync callback', async () => {
       let callbackResult, callbackError;
 
-      childFrame
+      const executeJavaScript = childFrame
         .executeJavaScript('thisShouldProduceAnError()', (result, error) => {
           callbackResult = result;
           callbackError = error;
-        })
-        .then(
-          promiseResult => {
-            done(new Error('error is expected'));
-          },
-          promiseError => {
-            expect(promiseError).to.be.an('error');
-            done();
-          }
-        );
+        });
 
       expect(callbackResult).to.be.undefined();
       expect(callbackError).to.be.an('error');
+
+      await expect(executeJavaScript).to.eventually.be.rejected('error is expected');
     });
 
     // executeJavaScriptInIsolatedWorld is failing to detect exec errors and is neither
@@ -113,23 +94,16 @@ describe('webFrame module', function () {
     // it('executeJavaScriptInIsolatedWorld() yields errors via a promise and a sync callback', done => {
     //   let callbackResult, callbackError
     //
-    //   childFrame
+    //   const executeJavaScriptInIsolatedWorld = childFrame
     //     .executeJavaScriptInIsolatedWorld(999, [{ code: 'thisShouldProduceAnError()' }], (result, error) => {
     //       callbackResult = result
     //       callbackError = error
-    //     })
-    //     .then(
-    //       promiseResult => {
-    //         done(new Error('error is expected'))
-    //       },
-    //       promiseError => {
-    //         expect(promiseError).to.be.an('error')
-    //         done()
-    //       }
-    //     )
+    //     });
     //
     //   expect(callbackResult).to.be.undefined()
     //   expect(callbackError).to.be.an('error')
+    //
+    //   expect(executeJavaScriptInIsolatedWorld).to.eventually.be.rejected('error is expected');
     // })
 
     it('executeJavaScript(InIsolatedWorld) can be used without a callback', async () => {

+ 406 - 183
spec/asar-spec.js

@@ -4,6 +4,8 @@ const fs = require('fs');
 const path = require('path');
 const temp = require('temp').track();
 const util = require('util');
+const { emittedOnce } = require('./events-helpers');
+const { ifit } = require('./spec-helpers');
 const nativeImage = require('electron').nativeImage;
 
 const features = process._linkedBinding('electron_common_features');
@@ -99,53 +101,77 @@ describe('asar package', function () {
       it('reads a normal file', function (done) {
         const p = path.join(asarDir, 'a.asar', 'file1');
         fs.readFile(p, function (err, content) {
-          expect(err).to.be.null();
-          expect(String(content).trim()).to.equal('file1');
-          done();
+          try {
+            expect(err).to.be.null();
+            expect(String(content).trim()).to.equal('file1');
+            done();
+          } catch (e) {
+            done(e);
+          }
         });
       });
 
       it('reads from a empty file', function (done) {
         const p = path.join(asarDir, 'empty.asar', 'file1');
         fs.readFile(p, function (err, content) {
-          expect(err).to.be.null();
-          expect(String(content)).to.equal('');
-          done();
+          try {
+            expect(err).to.be.null();
+            expect(String(content)).to.equal('');
+            done();
+          } catch (e) {
+            done(e);
+          }
         });
       });
 
       it('reads from a empty file with encoding', function (done) {
         const p = path.join(asarDir, 'empty.asar', 'file1');
         fs.readFile(p, 'utf8', function (err, content) {
-          expect(err).to.be.null();
-          expect(content).to.equal('');
-          done();
+          try {
+            expect(err).to.be.null();
+            expect(content).to.equal('');
+            done();
+          } catch (e) {
+            done(e);
+          }
         });
       });
 
       it('reads a linked file', function (done) {
         const p = path.join(asarDir, 'a.asar', 'link1');
         fs.readFile(p, function (err, content) {
-          expect(err).to.be.null();
-          expect(String(content).trim()).to.equal('file1');
-          done();
+          try {
+            expect(err).to.be.null();
+            expect(String(content).trim()).to.equal('file1');
+            done();
+          } catch (e) {
+            done(e);
+          }
         });
       });
 
       it('reads a file from linked directory', function (done) {
         const p = path.join(asarDir, 'a.asar', 'link2', 'link2', 'file1');
         fs.readFile(p, function (err, content) {
-          expect(err).to.be.null();
-          expect(String(content).trim()).to.equal('file1');
-          done();
+          try {
+            expect(err).to.be.null();
+            expect(String(content).trim()).to.equal('file1');
+            done();
+          } catch (e) {
+            done(e);
+          }
         });
       });
 
       it('throws ENOENT error when can not find file', function (done) {
         const p = path.join(asarDir, 'a.asar', 'not-exist');
         fs.readFile(p, function (err) {
-          expect(err.code).to.equal('ENOENT');
-          done();
+          try {
+            expect(err.code).to.equal('ENOENT');
+            done();
+          } catch (e) {
+            done(e);
+          }
         });
       });
     });
@@ -192,9 +218,13 @@ describe('asar package', function () {
         const p = path.join(asarDir, 'a.asar', 'file1');
         const dest = temp.path();
         fs.copyFile(p, dest, function (err) {
-          expect(err).to.be.null();
-          expect(fs.readFileSync(p).equals(fs.readFileSync(dest))).to.be.true();
-          done();
+          try {
+            expect(err).to.be.null();
+            expect(fs.readFileSync(p).equals(fs.readFileSync(dest))).to.be.true();
+            done();
+          } catch (e) {
+            done(e);
+          }
         });
       });
 
@@ -202,9 +232,13 @@ describe('asar package', function () {
         const p = path.join(asarDir, 'unpack.asar', 'a.txt');
         const dest = temp.path();
         fs.copyFile(p, dest, function (err) {
-          expect(err).to.be.null();
-          expect(fs.readFileSync(p).equals(fs.readFileSync(dest))).to.be.true();
-          done();
+          try {
+            expect(err).to.be.null();
+            expect(fs.readFileSync(p).equals(fs.readFileSync(dest))).to.be.true();
+            done();
+          } catch (e) {
+            done(e);
+          }
         });
       });
     });
@@ -339,80 +373,108 @@ describe('asar package', function () {
       it('returns information of root', function (done) {
         const p = path.join(asarDir, 'a.asar');
         fs.lstat(p, function (err, stats) {
-          expect(err).to.be.null();
-          expect(stats.isFile()).to.be.false();
-          expect(stats.isDirectory()).to.be.true();
-          expect(stats.isSymbolicLink()).to.be.false();
-          expect(stats.size).to.equal(0);
-          done();
+          try {
+            expect(err).to.be.null();
+            expect(stats.isFile()).to.be.false();
+            expect(stats.isDirectory()).to.be.true();
+            expect(stats.isSymbolicLink()).to.be.false();
+            expect(stats.size).to.equal(0);
+            done();
+          } catch (e) {
+            done(e);
+          }
         });
       });
 
       it('returns information of root with stats as bigint', function (done) {
         const p = path.join(asarDir, 'a.asar');
         fs.lstat(p, { bigint: false }, function (err, stats) {
-          expect(err).to.be.null();
-          expect(stats.isFile()).to.be.false();
-          expect(stats.isDirectory()).to.be.true();
-          expect(stats.isSymbolicLink()).to.be.false();
-          expect(stats.size).to.equal(0);
-          done();
+          try {
+            expect(err).to.be.null();
+            expect(stats.isFile()).to.be.false();
+            expect(stats.isDirectory()).to.be.true();
+            expect(stats.isSymbolicLink()).to.be.false();
+            expect(stats.size).to.equal(0);
+            done();
+          } catch (e) {
+            done(e);
+          }
         });
       });
 
       it('returns information of a normal file', function (done) {
         const p = path.join(asarDir, 'a.asar', 'link2', 'file1');
         fs.lstat(p, function (err, stats) {
-          expect(err).to.be.null();
-          expect(stats.isFile()).to.be.true();
-          expect(stats.isDirectory()).to.be.false();
-          expect(stats.isSymbolicLink()).to.be.false();
-          expect(stats.size).to.equal(6);
-          done();
+          try {
+            expect(err).to.be.null();
+            expect(stats.isFile()).to.be.true();
+            expect(stats.isDirectory()).to.be.false();
+            expect(stats.isSymbolicLink()).to.be.false();
+            expect(stats.size).to.equal(6);
+            done();
+          } catch (e) {
+            done(e);
+          }
         });
       });
 
       it('returns information of a normal directory', function (done) {
         const p = path.join(asarDir, 'a.asar', 'dir1');
         fs.lstat(p, function (err, stats) {
-          expect(err).to.be.null();
-          expect(stats.isFile()).to.be.false();
-          expect(stats.isDirectory()).to.be.true();
-          expect(stats.isSymbolicLink()).to.be.false();
-          expect(stats.size).to.equal(0);
-          done();
+          try {
+            expect(err).to.be.null();
+            expect(stats.isFile()).to.be.false();
+            expect(stats.isDirectory()).to.be.true();
+            expect(stats.isSymbolicLink()).to.be.false();
+            expect(stats.size).to.equal(0);
+            done();
+          } catch (e) {
+            done(e);
+          }
         });
       });
 
       it('returns information of a linked file', function (done) {
         const p = path.join(asarDir, 'a.asar', 'link2', 'link1');
         fs.lstat(p, function (err, stats) {
-          expect(err).to.be.null();
-          expect(stats.isFile()).to.be.false();
-          expect(stats.isDirectory()).to.be.false();
-          expect(stats.isSymbolicLink()).to.be.true();
-          expect(stats.size).to.equal(0);
-          done();
+          try {
+            expect(err).to.be.null();
+            expect(stats.isFile()).to.be.false();
+            expect(stats.isDirectory()).to.be.false();
+            expect(stats.isSymbolicLink()).to.be.true();
+            expect(stats.size).to.equal(0);
+            done();
+          } catch (e) {
+            done(e);
+          }
         });
       });
 
       it('returns information of a linked directory', function (done) {
         const p = path.join(asarDir, 'a.asar', 'link2', 'link2');
         fs.lstat(p, function (err, stats) {
-          expect(err).to.be.null();
-          expect(stats.isFile()).to.be.false();
-          expect(stats.isDirectory()).to.be.false();
-          expect(stats.isSymbolicLink()).to.be.true();
-          expect(stats.size).to.equal(0);
-          done();
+          try {
+            expect(err).to.be.null();
+            expect(stats.isFile()).to.be.false();
+            expect(stats.isDirectory()).to.be.false();
+            expect(stats.isSymbolicLink()).to.be.true();
+            expect(stats.size).to.equal(0);
+            done();
+          } catch (e) {
+            done(e);
+          }
         });
       });
 
       it('throws ENOENT error when can not find file', function (done) {
         const p = path.join(asarDir, 'a.asar', 'file4');
         fs.lstat(p, function (err) {
-          expect(err.code).to.equal('ENOENT');
-          done();
+          try {
+            expect(err.code).to.equal('ENOENT');
+            done();
+          } catch (e) {
+            done(e);
+          }
         });
       });
     });
@@ -592,9 +654,13 @@ describe('asar package', function () {
         const parent = fs.realpathSync(asarDir);
         const p = 'a.asar';
         fs.realpath(path.join(parent, p), (err, r) => {
-          expect(err).to.be.null();
-          expect(r).to.equal(path.join(parent, p));
-          done();
+          try {
+            expect(err).to.be.null();
+            expect(r).to.equal(path.join(parent, p));
+            done();
+          } catch (e) {
+            done(e);
+          }
         });
       });
 
@@ -602,9 +668,13 @@ describe('asar package', function () {
         const parent = fs.realpathSync(asarDir);
         const p = path.join('a.asar', 'file1');
         fs.realpath(path.join(parent, p), (err, r) => {
-          expect(err).to.be.null();
-          expect(r).to.equal(path.join(parent, p));
-          done();
+          try {
+            expect(err).to.be.null();
+            expect(r).to.equal(path.join(parent, p));
+            done();
+          } catch (e) {
+            done(e);
+          }
         });
       });
 
@@ -612,9 +682,13 @@ describe('asar package', function () {
         const parent = fs.realpathSync(asarDir);
         const p = path.join('a.asar', 'dir1');
         fs.realpath(path.join(parent, p), (err, r) => {
-          expect(err).to.be.null();
-          expect(r).to.equal(path.join(parent, p));
-          done();
+          try {
+            expect(err).to.be.null();
+            expect(r).to.equal(path.join(parent, p));
+            done();
+          } catch (e) {
+            done(e);
+          }
         });
       });
 
@@ -622,9 +696,13 @@ describe('asar package', function () {
         const parent = fs.realpathSync(asarDir);
         const p = path.join('a.asar', 'link2', 'link1');
         fs.realpath(path.join(parent, p), (err, r) => {
-          expect(err).to.be.null();
-          expect(r).to.equal(path.join(parent, 'a.asar', 'file1'));
-          done();
+          try {
+            expect(err).to.be.null();
+            expect(r).to.equal(path.join(parent, 'a.asar', 'file1'));
+            done();
+          } catch (e) {
+            done(e);
+          }
         });
       });
 
@@ -632,9 +710,13 @@ describe('asar package', function () {
         const parent = fs.realpathSync(asarDir);
         const p = path.join('a.asar', 'link2', 'link2');
         fs.realpath(path.join(parent, p), (err, r) => {
-          expect(err).to.be.null();
-          expect(r).to.equal(path.join(parent, 'a.asar', 'dir1'));
-          done();
+          try {
+            expect(err).to.be.null();
+            expect(r).to.equal(path.join(parent, 'a.asar', 'dir1'));
+            done();
+          } catch (e) {
+            done(e);
+          }
         });
       });
 
@@ -642,9 +724,13 @@ describe('asar package', function () {
         const parent = fs.realpathSync(asarDir);
         const p = path.join('unpack.asar', 'a.txt');
         fs.realpath(path.join(parent, p), (err, r) => {
-          expect(err).to.be.null();
-          expect(r).to.equal(path.join(parent, p));
-          done();
+          try {
+            expect(err).to.be.null();
+            expect(r).to.equal(path.join(parent, p));
+            done();
+          } catch (e) {
+            done(e);
+          }
         });
       });
 
@@ -652,8 +738,12 @@ describe('asar package', function () {
         const parent = fs.realpathSync(asarDir);
         const p = path.join('a.asar', 'not-exist');
         fs.realpath(path.join(parent, p), err => {
-          expect(err.code).to.equal('ENOENT');
-          done();
+          try {
+            expect(err.code).to.equal('ENOENT');
+            done();
+          } catch (e) {
+            done(e);
+          }
         });
       });
     });
@@ -713,9 +803,13 @@ describe('asar package', function () {
         const parent = fs.realpathSync.native(asarDir);
         const p = 'a.asar';
         fs.realpath.native(path.join(parent, p), (err, r) => {
-          expect(err).to.be.null();
-          expect(r).to.equal(path.join(parent, p));
-          done();
+          try {
+            expect(err).to.be.null();
+            expect(r).to.equal(path.join(parent, p));
+            done();
+          } catch (e) {
+            done(e);
+          }
         });
       });
 
@@ -723,9 +817,13 @@ describe('asar package', function () {
         const parent = fs.realpathSync.native(asarDir);
         const p = path.join('a.asar', 'file1');
         fs.realpath.native(path.join(parent, p), (err, r) => {
-          expect(err).to.be.null();
-          expect(r).to.equal(path.join(parent, p));
-          done();
+          try {
+            expect(err).to.be.null();
+            expect(r).to.equal(path.join(parent, p));
+            done();
+          } catch (e) {
+            done(e);
+          }
         });
       });
 
@@ -733,9 +831,13 @@ describe('asar package', function () {
         const parent = fs.realpathSync.native(asarDir);
         const p = path.join('a.asar', 'dir1');
         fs.realpath.native(path.join(parent, p), (err, r) => {
-          expect(err).to.be.null();
-          expect(r).to.equal(path.join(parent, p));
-          done();
+          try {
+            expect(err).to.be.null();
+            expect(r).to.equal(path.join(parent, p));
+            done();
+          } catch (e) {
+            done(e);
+          }
         });
       });
 
@@ -743,9 +845,13 @@ describe('asar package', function () {
         const parent = fs.realpathSync.native(asarDir);
         const p = path.join('a.asar', 'link2', 'link1');
         fs.realpath.native(path.join(parent, p), (err, r) => {
-          expect(err).to.be.null();
-          expect(r).to.equal(path.join(parent, 'a.asar', 'file1'));
-          done();
+          try {
+            expect(err).to.be.null();
+            expect(r).to.equal(path.join(parent, 'a.asar', 'file1'));
+            done();
+          } catch (e) {
+            done(e);
+          }
         });
       });
 
@@ -753,9 +859,13 @@ describe('asar package', function () {
         const parent = fs.realpathSync.native(asarDir);
         const p = path.join('a.asar', 'link2', 'link2');
         fs.realpath.native(path.join(parent, p), (err, r) => {
-          expect(err).to.be.null();
-          expect(r).to.equal(path.join(parent, 'a.asar', 'dir1'));
-          done();
+          try {
+            expect(err).to.be.null();
+            expect(r).to.equal(path.join(parent, 'a.asar', 'dir1'));
+            done();
+          } catch (e) {
+            done(e);
+          }
         });
       });
 
@@ -763,9 +873,13 @@ describe('asar package', function () {
         const parent = fs.realpathSync.native(asarDir);
         const p = path.join('unpack.asar', 'a.txt');
         fs.realpath.native(path.join(parent, p), (err, r) => {
-          expect(err).to.be.null();
-          expect(r).to.equal(path.join(parent, p));
-          done();
+          try {
+            expect(err).to.be.null();
+            expect(r).to.equal(path.join(parent, p));
+            done();
+          } catch (e) {
+            done(e);
+          }
         });
       });
 
@@ -773,8 +887,12 @@ describe('asar package', function () {
         const parent = fs.realpathSync.native(asarDir);
         const p = path.join('a.asar', 'not-exist');
         fs.realpath.native(path.join(parent, p), err => {
-          expect(err.code).to.equal('ENOENT');
-          done();
+          try {
+            expect(err.code).to.equal('ENOENT');
+            done();
+          } catch (e) {
+            done(e);
+          }
         });
       });
     });
@@ -820,9 +938,13 @@ describe('asar package', function () {
       it('reads dirs from root', function (done) {
         const p = path.join(asarDir, 'a.asar');
         fs.readdir(p, function (err, dirs) {
-          expect(err).to.be.null();
-          expect(dirs).to.deep.equal(['dir1', 'dir2', 'dir3', 'file1', 'file2', 'file3', 'link1', 'link2', 'ping.js']);
-          done();
+          try {
+            expect(err).to.be.null();
+            expect(dirs).to.deep.equal(['dir1', 'dir2', 'dir3', 'file1', 'file2', 'file3', 'link1', 'link2', 'ping.js']);
+            done();
+          } catch (e) {
+            done(e);
+          }
         });
       });
 
@@ -830,40 +952,56 @@ describe('asar package', function () {
         const p = path.join(asarDir, 'a.asar');
 
         fs.readdir(p, { withFileTypes: true }, (err, dirs) => {
-          expect(err).to.be.null();
-          for (const dir of dirs) {
-            expect(dir instanceof fs.Dirent).to.be.true();
+          try {
+            expect(err).to.be.null();
+            for (const dir of dirs) {
+              expect(dir instanceof fs.Dirent).to.be.true();
+            }
+
+            const names = dirs.map(a => a.name);
+            expect(names).to.deep.equal(['dir1', 'dir2', 'dir3', 'file1', 'file2', 'file3', 'link1', 'link2', 'ping.js']);
+            done();
+          } catch (e) {
+            done(e);
           }
-
-          const names = dirs.map(a => a.name);
-          expect(names).to.deep.equal(['dir1', 'dir2', 'dir3', 'file1', 'file2', 'file3', 'link1', 'link2', 'ping.js']);
-          done();
         });
       });
 
       it('reads dirs from a normal dir', function (done) {
         const p = path.join(asarDir, 'a.asar', 'dir1');
         fs.readdir(p, function (err, dirs) {
-          expect(err).to.be.null();
-          expect(dirs).to.deep.equal(['file1', 'file2', 'file3', 'link1', 'link2']);
-          done();
+          try {
+            expect(err).to.be.null();
+            expect(dirs).to.deep.equal(['file1', 'file2', 'file3', 'link1', 'link2']);
+            done();
+          } catch (e) {
+            done(e);
+          }
         });
       });
 
       it('reads dirs from a linked dir', function (done) {
         const p = path.join(asarDir, 'a.asar', 'link2', 'link2');
         fs.readdir(p, function (err, dirs) {
-          expect(err).to.be.null();
-          expect(dirs).to.deep.equal(['file1', 'file2', 'file3', 'link1', 'link2']);
-          done();
+          try {
+            expect(err).to.be.null();
+            expect(dirs).to.deep.equal(['file1', 'file2', 'file3', 'link1', 'link2']);
+            done();
+          } catch (e) {
+            done(e);
+          }
         });
       });
 
       it('throws ENOENT error when can not find file', function (done) {
         const p = path.join(asarDir, 'a.asar', 'not-exist');
         fs.readdir(p, function (err) {
-          expect(err.code).to.equal('ENOENT');
-          done();
+          try {
+            expect(err.code).to.equal('ENOENT');
+            done();
+          } catch (e) {
+            done(e);
+          }
         });
       });
     });
@@ -942,8 +1080,12 @@ describe('asar package', function () {
       it('throws ENOENT error when can not find file', function (done) {
         const p = path.join(asarDir, 'a.asar', 'not-exist');
         fs.open(p, 'r', function (err) {
-          expect(err.code).to.equal('ENOENT');
-          done();
+          try {
+            expect(err.code).to.equal('ENOENT');
+            done();
+          } catch (e) {
+            done(e);
+          }
         });
       });
     });
@@ -968,8 +1110,12 @@ describe('asar package', function () {
       it('throws error when calling inside asar archive', function (done) {
         const p = path.join(asarDir, 'a.asar', 'not-exist');
         fs.mkdir(p, function (err) {
-          expect(err.code).to.equal('ENOTDIR');
-          done();
+          try {
+            expect(err.code).to.equal('ENOTDIR');
+            done();
+          } catch (e) {
+            done(e);
+          }
         });
       });
     });
@@ -995,8 +1141,12 @@ describe('asar package', function () {
         const p = path.join(asarDir, 'a.asar', 'file1');
         // eslint-disable-next-line
         fs.exists(p, function (exists) {
-          expect(exists).to.be.true();
-          done();
+          try {
+            expect(exists).to.be.true();
+            done();
+          } catch (e) {
+            done(e);
+          }
         });
       });
 
@@ -1004,8 +1154,12 @@ describe('asar package', function () {
         const p = path.join(asarDir, 'a.asar', 'not-exist');
         // eslint-disable-next-line
         fs.exists(p, function (exists) {
-          expect(exists).to.be.false();
-          done();
+          try {
+            expect(exists).to.be.false();
+            done();
+          } catch (e) {
+            done(e);
+          }
         });
       });
 
@@ -1013,8 +1167,12 @@ describe('asar package', function () {
         const p = path.join(asarDir, 'a.asar', 'file1');
         // eslint-disable-next-line
         util.promisify(fs.exists)(p).then(exists => {
-          expect(exists).to.be.true();
-          done();
+          try {
+            expect(exists).to.be.true();
+            done();
+          } catch (e) {
+            done(e);
+          }
         });
       });
 
@@ -1022,8 +1180,12 @@ describe('asar package', function () {
         const p = path.join(asarDir, 'a.asar', 'not-exist');
         // eslint-disable-next-line
         util.promisify(fs.exists)(p).then(exists => {
-          expect(exists).to.be.false();
-          done();
+          try {
+            expect(exists).to.be.false();
+            done();
+          } catch (e) {
+            done(e);
+          }
         });
       });
     });
@@ -1044,32 +1206,48 @@ describe('asar package', function () {
       it('accesses a normal file', function (done) {
         const p = path.join(asarDir, 'a.asar', 'file1');
         fs.access(p, function (err) {
-          expect(err).to.be.undefined();
-          done();
+          try {
+            expect(err).to.be.undefined();
+            done();
+          } catch (e) {
+            done(e);
+          }
         });
       });
 
       it('throws an error when called with write mode', function (done) {
         const p = path.join(asarDir, 'a.asar', 'file1');
         fs.access(p, fs.constants.R_OK | fs.constants.W_OK, function (err) {
-          expect(err.code).to.equal('EACCES');
-          done();
+          try {
+            expect(err.code).to.equal('EACCES');
+            done();
+          } catch (e) {
+            done(e);
+          }
         });
       });
 
       it('throws an error when called on non-existent file', function (done) {
         const p = path.join(asarDir, 'a.asar', 'not-exist');
         fs.access(p, function (err) {
-          expect(err.code).to.equal('ENOENT');
-          done();
+          try {
+            expect(err.code).to.equal('ENOENT');
+            done();
+          } catch (e) {
+            done(e);
+          }
         });
       });
 
       it('allows write mode for unpacked files', function (done) {
         const p = path.join(asarDir, 'unpack.asar', 'a.txt');
         fs.access(p, fs.constants.R_OK | fs.constants.W_OK, function (err) {
-          expect(err).to.be.null();
-          done();
+          try {
+            expect(err).to.be.null();
+            done();
+          } catch (e) {
+            done(e);
+          }
         });
       });
     });
@@ -1136,8 +1314,12 @@ describe('asar package', function () {
       it('opens a normal js file', function (done) {
         const child = ChildProcess.fork(path.join(asarDir, 'a.asar', 'ping.js'));
         child.on('message', function (msg) {
-          expect(msg).to.equal('message');
-          done();
+          try {
+            expect(msg).to.equal('message');
+            done();
+          } catch (e) {
+            done(e);
+          }
         });
         child.send('message');
       });
@@ -1146,8 +1328,12 @@ describe('asar package', function () {
         const file = path.join(asarDir, 'a.asar', 'file1');
         const child = ChildProcess.fork(path.join(fixtures, 'module', 'asar.js'));
         child.on('message', function (content) {
-          expect(content).to.equal(fs.readFileSync(file).toString());
-          done();
+          try {
+            expect(content).to.equal(fs.readFileSync(file).toString());
+            done();
+          } catch (e) {
+            done(e);
+          }
         });
         child.send(file);
       });
@@ -1158,9 +1344,13 @@ describe('asar package', function () {
 
       it('should not try to extract the command if there is a reference to a file inside an .asar', function (done) {
         ChildProcess.exec('echo ' + echo + ' foo bar', function (error, stdout) {
-          expect(error).to.be.null();
-          expect(stdout.toString().replace(/\r/g, '')).to.equal(echo + ' foo bar\n');
-          done();
+          try {
+            expect(error).to.be.null();
+            expect(stdout.toString().replace(/\r/g, '')).to.equal(echo + ' foo bar\n');
+            done();
+          } catch (e) {
+            done(e);
+          }
         });
       });
 
@@ -1175,9 +1365,13 @@ describe('asar package', function () {
       const echo = path.join(asarDir, 'echo.asar', 'echo');
 
       it('should not try to extract the command if there is a reference to a file inside an .asar', function (done) {
-        const stdout = ChildProcess.execSync('echo ' + echo + ' foo bar');
-        expect(stdout.toString().replace(/\r/g, '')).to.equal(echo + ' foo bar\n');
-        done();
+        try {
+          const stdout = ChildProcess.execSync('echo ' + echo + ' foo bar');
+          expect(stdout.toString().replace(/\r/g, '')).to.equal(echo + ' foo bar\n');
+          done();
+        } catch (e) {
+          done(e);
+        }
       });
     });
 
@@ -1194,21 +1388,28 @@ describe('asar package', function () {
 
       it('executes binaries', function (done) {
         execFile(echo, ['test'], function (error, stdout) {
-          expect(error).to.be.null();
-          expect(stdout).to.equal('test\n');
-          done();
+          try {
+            expect(error).to.be.null();
+            expect(stdout).to.equal('test\n');
+            done();
+          } catch (e) {
+            done(e);
+          }
         });
       });
 
       it('executes binaries without callback', function (done) {
         const process = execFile(echo, ['test']);
         process.on('close', function (code) {
-          expect(code).to.equal(0);
-          done();
+          try {
+            expect(code).to.equal(0);
+            done();
+          } catch (e) {
+            done(e);
+          }
         });
         process.on('error', function () {
-          expect.fail();
-          done();
+          done('error');
         });
       });
 
@@ -1351,9 +1552,13 @@ describe('asar package', function () {
           }
         });
         forked.on('message', function (stats) {
-          expect(stats.isFile).to.be.true();
-          expect(stats.size).to.equal(778);
-          done();
+          try {
+            expect(stats.isFile).to.be.true();
+            expect(stats.size).to.equal(778);
+            done();
+          } catch (e) {
+            done(e);
+          }
         });
       });
 
@@ -1370,10 +1575,14 @@ describe('asar package', function () {
           output += data;
         });
         spawned.stdout.on('close', function () {
-          const stats = JSON.parse(output);
-          expect(stats.isFile).to.be.true();
-          expect(stats.size).to.equal(778);
-          done();
+          try {
+            const stats = JSON.parse(output);
+            expect(stats.isFile).to.be.true();
+            expect(stats.size).to.equal(778);
+            done();
+          } catch (e) {
+            done(e);
+          }
         });
       });
     });
@@ -1383,32 +1592,48 @@ describe('asar package', function () {
     it('can request a file in package', function (done) {
       const p = path.resolve(asarDir, 'a.asar', 'file1');
       $.get('file://' + p, function (data) {
-        expect(data.trim()).to.equal('file1');
-        done();
+        try {
+          expect(data.trim()).to.equal('file1');
+          done();
+        } catch (e) {
+          done(e);
+        }
       });
     });
 
     it('can request a file in package with unpacked files', function (done) {
       const p = path.resolve(asarDir, 'unpack.asar', 'a.txt');
       $.get('file://' + p, function (data) {
-        expect(data.trim()).to.equal('a');
-        done();
+        try {
+          expect(data.trim()).to.equal('a');
+          done();
+        } catch (e) {
+          done(e);
+        }
       });
     });
 
     it('can request a linked file in package', function (done) {
       const p = path.resolve(asarDir, 'a.asar', 'link2', 'link1');
       $.get('file://' + p, function (data) {
-        expect(data.trim()).to.equal('file1');
-        done();
+        try {
+          expect(data.trim()).to.equal('file1');
+          done();
+        } catch (e) {
+          done(e);
+        }
       });
     });
 
     it('can request a file in filesystem', function (done) {
       const p = path.resolve(asarDir, 'file');
       $.get('file://' + p, function (data) {
-        expect(data.trim()).to.equal('file');
-        done();
+        try {
+          expect(data.trim()).to.equal('file');
+          done();
+        } catch (e) {
+          done(e);
+        }
       });
     });
 
@@ -1417,8 +1642,12 @@ describe('asar package', function () {
       $.ajax({
         url: 'file://' + p,
         error: function (err) {
-          expect(err.status).to.equal(404);
-          done();
+          try {
+            expect(err.status).to.equal(404);
+            done();
+          } catch (e) {
+            done(e);
+          }
         }
       });
     });
@@ -1433,18 +1662,12 @@ describe('asar package', function () {
       expect(stats.isFile()).to.be.true();
     });
 
-    it('is available in forked scripts', function (done) {
-      if (!features.isRunAsNodeEnabled()) {
-        this.skip();
-        done();
-      }
-
+    ifit(features.isRunAsNodeEnabled())('is available in forked scripts', async function () {
       const child = ChildProcess.fork(path.join(fixtures, 'module', 'original-fs.js'));
-      child.on('message', function (msg) {
-        expect(msg).to.equal('object');
-        done();
-      });
+      const message = emittedOnce(child, 'message');
       child.send('message');
+      const [msg] = await message;
+      expect(msg).to.equal('object');
     });
 
     it('can be used with streams', () => {

+ 104 - 156
spec/chromium-spec.js

@@ -6,8 +6,9 @@ const ws = require('ws');
 const url = require('url');
 const ChildProcess = require('child_process');
 const { ipcRenderer } = require('electron');
-const { emittedOnce } = require('./events-helpers');
+const { emittedOnce, waitForEvent } = require('./events-helpers');
 const { resolveGetters } = require('./expect-helpers');
+const { ifdescribe, delay } = require('./spec-helpers');
 const features = process._linkedBinding('electron_common_features');
 
 /* Most of the APIs here don't use standard callbacks */
@@ -15,14 +16,6 @@ const features = process._linkedBinding('electron_common_features');
 
 describe('chromium feature', () => {
   const fixtures = path.resolve(__dirname, 'fixtures');
-  let listener = null;
-
-  afterEach(() => {
-    if (listener != null) {
-      window.removeEventListener('message', listener);
-    }
-    listener = null;
-  });
 
   describe('heap snapshot', () => {
     it('does not crash', function () {
@@ -46,58 +39,34 @@ describe('chromium feature', () => {
     });
   });
 
-  describe('navigator.geolocation', () => {
-    before(function () {
-      if (!features.isFakeLocationProviderEnabled()) {
-        return this.skip();
-      }
-    });
-
-    it('returns position when permission is granted', (done) => {
-      navigator.geolocation.getCurrentPosition((position) => {
-        expect(position).to.have.a.property('coords');
-        expect(position).to.have.a.property('timestamp');
-        done();
-      }, (error) => {
-        done(error);
-      });
+  ifdescribe(features.isFakeLocationProviderEnabled())('navigator.geolocation', () => {
+    it('returns position when permission is granted', async () => {
+      const position = await new Promise((resolve, reject) => navigator.geolocation.getCurrentPosition(resolve, reject));
+      expect(position).to.have.a.property('coords');
+      expect(position).to.have.a.property('timestamp');
     });
   });
 
   describe('window.open', () => {
-    it('accepts "nodeIntegration" as feature', (done) => {
-      let b = null;
-      listener = (event) => {
-        expect(event.data.isProcessGlobalUndefined).to.be.true();
-        b.close();
-        done();
-      };
-      window.addEventListener('message', listener);
-      b = window.open(`file://${fixtures}/pages/window-opener-node.html`, '', 'nodeIntegration=no,show=no');
+    it('accepts "nodeIntegration" as feature', async () => {
+      const message = waitForEvent(window, 'message');
+      const b = window.open(`file://${fixtures}/pages/window-opener-node.html`, '', 'nodeIntegration=no,show=no');
+      const event = await message;
+      b.close();
+      expect(event.data.isProcessGlobalUndefined).to.be.true();
     });
 
-    it('inherit options of parent window', (done) => {
-      let b = null;
-      listener = (event) => {
-        const width = outerWidth;
-        const height = outerHeight;
-        expect(event.data).to.equal(`size: ${width} ${height}`);
-        b.close();
-        done();
-      };
-      window.addEventListener('message', listener);
-      b = window.open(`file://${fixtures}/pages/window-open-size.html`, '', 'show=no');
+    it('inherit options of parent window', async () => {
+      const message = waitForEvent(window, 'message');
+      const b = window.open(`file://${fixtures}/pages/window-open-size.html`, '', 'show=no');
+      const event = await message;
+      b.close();
+      const width = outerWidth;
+      const height = outerHeight;
+      expect(event.data).to.equal(`size: ${width} ${height}`);
     });
 
-    it('disables node integration when it is disabled on the parent window', (done) => {
-      let b = null;
-      listener = (event) => {
-        expect(event.data.isProcessGlobalUndefined).to.be.true();
-        b.close();
-        done();
-      };
-      window.addEventListener('message', listener);
-
+    it('disables node integration when it is disabled on the parent window', async () => {
       const windowUrl = require('url').format({
         pathname: `${fixtures}/pages/window-opener-no-node-integration.html`,
         protocol: 'file',
@@ -106,18 +75,14 @@ describe('chromium feature', () => {
         },
         slashes: true
       });
-      b = window.open(windowUrl, '', 'nodeIntegration=no,show=no');
+      const message = waitForEvent(window, 'message');
+      const b = window.open(windowUrl, '', 'nodeIntegration=no,show=no');
+      const event = await message;
+      b.close();
+      expect(event.data.isProcessGlobalUndefined).to.be.true();
     });
 
-    it('disables the <webview> tag when it is disabled on the parent window', (done) => {
-      let b = null;
-      listener = (event) => {
-        expect(event.data.isWebViewGlobalUndefined).to.be.true();
-        b.close();
-        done();
-      };
-      window.addEventListener('message', listener);
-
+    it('disables the <webview> tag when it is disabled on the parent window', async () => {
       const windowUrl = require('url').format({
         pathname: `${fixtures}/pages/window-opener-no-webview-tag.html`,
         protocol: 'file',
@@ -126,22 +91,23 @@ describe('chromium feature', () => {
         },
         slashes: true
       });
-      b = window.open(windowUrl, '', 'webviewTag=no,nodeIntegration=yes,show=no');
+      const message = waitForEvent(window, 'message');
+      const b = window.open(windowUrl, '', 'webviewTag=no,nodeIntegration=yes,show=no');
+      const event = await message;
+      b.close();
+      expect(event.data.isWebViewGlobalUndefined).to.be.true();
     });
 
-    it('does not override child options', (done) => {
-      let b = null;
+    it('does not override child options', async () => {
       const size = {
         width: 350,
         height: 450
       };
-      listener = (event) => {
-        expect(event.data).to.equal(`size: ${size.width} ${size.height}`);
-        b.close();
-        done();
-      };
-      window.addEventListener('message', listener);
-      b = window.open(`file://${fixtures}/pages/window-open-size.html`, '', 'show=no,width=' + size.width + ',height=' + size.height);
+      const message = waitForEvent(window, 'message');
+      const b = window.open(`file://${fixtures}/pages/window-open-size.html`, '', 'show=no,width=' + size.width + ',height=' + size.height);
+      const event = await message;
+      b.close();
+      expect(event.data).to.equal(`size: ${size.width} ${size.height}`);
     });
 
     it('throws an exception when the arguments cannot be converted to strings', () => {
@@ -164,15 +130,12 @@ describe('chromium feature', () => {
   });
 
   describe('window.opener', () => {
-    it('is not null for window opened by window.open', (done) => {
-      let b = null;
-      listener = (event) => {
-        expect(event.data).to.equal('object');
-        b.close();
-        done();
-      };
-      window.addEventListener('message', listener);
-      b = window.open(`file://${fixtures}/pages/window-opener.html`, '', 'show=no');
+    it('is not null for window opened by window.open', async () => {
+      const message = waitForEvent(window, 'message');
+      const b = window.open(`file://${fixtures}/pages/window-opener.html`, '', 'show=no');
+      const event = await message;
+      b.close();
+      expect(event.data).to.equal('object');
     });
   });
 
@@ -187,26 +150,21 @@ describe('chromium feature', () => {
   });
 
   describe('window.opener.postMessage', () => {
-    it('sets source and origin correctly', (done) => {
-      let b = null;
-      listener = (event) => {
-        window.removeEventListener('message', listener);
+    it('sets source and origin correctly', async () => {
+      const message = waitForEvent(window, 'message');
+      const b = window.open(`file://${fixtures}/pages/window-opener-postMessage.html`, '', 'show=no');
+      const event = await message;
+      try {
         expect(event.source).to.deep.equal(b);
-        b.close();
         expect(event.origin).to.equal('file://');
-        done();
-      };
-      window.addEventListener('message', listener);
-      b = window.open(`file://${fixtures}/pages/window-opener-postMessage.html`, '', 'show=no');
+      } finally {
+        b.close();
+      }
     });
 
-    it('supports windows opened from a <webview>', (done) => {
+    it('supports windows opened from a <webview>', async () => {
       const webview = new WebView();
-      webview.addEventListener('console-message', (e) => {
-        webview.remove();
-        expect(e.message).to.equal('message');
-        done();
-      });
+      const consoleMessage = waitForEvent(webview, 'console-message');
       webview.allowpopups = true;
       webview.src = url.format({
         pathname: `${fixtures}/pages/webview-opener-postMessage.html`,
@@ -217,6 +175,9 @@ describe('chromium feature', () => {
         slashes: true
       });
       document.body.appendChild(webview);
+      const event = await consoleMessage;
+      webview.remove();
+      expect(event.message).to.equal('message');
     });
 
     describe('targetOrigin argument', () => {
@@ -239,16 +200,12 @@ describe('chromium feature', () => {
         server.close();
       });
 
-      it('delivers messages that match the origin', (done) => {
-        let b = null;
-        listener = (event) => {
-          window.removeEventListener('message', listener);
-          b.close();
-          expect(event.data).to.equal('deliver');
-          done();
-        };
-        window.addEventListener('message', listener);
-        b = window.open(serverURL, '', 'show=no');
+      it('delivers messages that match the origin', async () => {
+        const message = waitForEvent(window, 'message');
+        const b = window.open(serverURL, '', 'show=no');
+        const event = await message;
+        b.close();
+        expect(event.data).to.equal('deliver');
       });
     });
   });
@@ -273,71 +230,63 @@ describe('chromium feature', () => {
   });
 
   describe('web workers', () => {
-    it('Worker can work', (done) => {
+    it('Worker can work', async () => {
       const worker = new Worker('../fixtures/workers/worker.js');
       const message = 'ping';
-      worker.onmessage = (event) => {
-        expect(event.data).to.equal(message);
-        worker.terminate();
-        done();
-      };
+      const eventPromise = new Promise((resolve) => { worker.onmessage = resolve; });
       worker.postMessage(message);
+      const event = await eventPromise;
+      worker.terminate();
+      expect(event.data).to.equal(message);
     });
 
-    it('Worker has no node integration by default', (done) => {
+    it('Worker has no node integration by default', async () => {
       const worker = new Worker('../fixtures/workers/worker_node.js');
-      worker.onmessage = (event) => {
-        expect(event.data).to.equal('undefined undefined undefined undefined');
-        worker.terminate();
-        done();
-      };
+      const event = await new Promise((resolve) => { worker.onmessage = resolve; });
+      worker.terminate();
+      expect(event.data).to.equal('undefined undefined undefined undefined');
     });
 
-    it('Worker has node integration with nodeIntegrationInWorker', (done) => {
+    it('Worker has node integration with nodeIntegrationInWorker', async () => {
       const webview = new WebView();
-      webview.addEventListener('ipc-message', (e) => {
-        expect(e.channel).to.equal('object function object function');
-        webview.remove();
-        done();
-      });
+      const eventPromise = waitForEvent(webview, 'ipc-message');
       webview.src = `file://${fixtures}/pages/worker.html`;
       webview.setAttribute('webpreferences', 'nodeIntegration, nodeIntegrationInWorker');
       document.body.appendChild(webview);
+      const event = await eventPromise;
+      webview.remove();
+      expect(event.channel).to.equal('object function object function');
     });
 
-    // FIXME: disabled during chromium update due to crash in content::WorkerScriptFetchInitiator::CreateScriptLoaderOnIO
-    xdescribe('SharedWorker', () => {
-      it('can work', (done) => {
+    describe('SharedWorker', () => {
+      it('can work', async () => {
         const worker = new SharedWorker('../fixtures/workers/shared_worker.js');
         const message = 'ping';
-        worker.port.onmessage = (event) => {
-          expect(event.data).to.equal(message);
-          done();
-        };
+        const eventPromise = new Promise((resolve) => { worker.port.onmessage = resolve; });
         worker.port.postMessage(message);
+        const event = await eventPromise;
+        expect(event.data).to.equal(message);
       });
 
-      it('has no node integration by default', (done) => {
+      it('has no node integration by default', async () => {
         const worker = new SharedWorker('../fixtures/workers/shared_worker_node.js');
-        worker.port.onmessage = (event) => {
-          expect(event.data).to.equal('undefined undefined undefined undefined');
-          done();
-        };
+        const event = await new Promise((resolve) => { worker.port.onmessage = resolve; });
+        expect(event.data).to.equal('undefined undefined undefined undefined');
       });
 
-      it('has node integration with nodeIntegrationInWorker', (done) => {
+      // FIXME: disabled during chromium update due to crash in content::WorkerScriptFetchInitiator::CreateScriptLoaderOnIO
+      xit('has node integration with nodeIntegrationInWorker', async () => {
         const webview = new WebView();
         webview.addEventListener('console-message', (e) => {
           console.log(e);
         });
-        webview.addEventListener('ipc-message', (e) => {
-          expect(e.channel).to.equal('object function object function');
-          webview.remove();
-          done();
-        });
+        const eventPromise = waitForEvent(webview, 'ipc-message');
         webview.src = `file://${fixtures}/pages/shared_worker.html`;
         webview.setAttribute('webpreferences', 'nodeIntegration, nodeIntegrationInWorker');
         document.body.appendChild(webview);
+        const event = await eventPromise;
+        webview.remove();
+        expect(event.channel).to.equal('object function object function');
       });
     });
   });
@@ -353,13 +302,11 @@ describe('chromium feature', () => {
       document.body.removeChild(iframe);
     });
 
-    it('does not have node integration', (done) => {
+    it('does not have node integration', async () => {
       iframe.src = `file://${fixtures}/pages/set-global.html`;
       document.body.appendChild(iframe);
-      iframe.onload = () => {
-        expect(iframe.contentWindow.test).to.equal('undefined undefined undefined');
-        done();
-      };
+      await waitForEvent(iframe, 'load');
+      expect(iframe.contentWindow.test).to.equal('undefined undefined undefined');
     });
   });
 
@@ -367,7 +314,7 @@ describe('chromium feature', () => {
     describe('DOM storage quota increase', () => {
       ['localStorage', 'sessionStorage'].forEach((storageName) => {
         const storage = window[storageName];
-        it(`allows saving at least 40MiB in ${storageName}`, (done) => {
+        it(`allows saving at least 40MiB in ${storageName}`, async () => {
           // Although JavaScript strings use UTF-16, the underlying
           // storage provider may encode strings differently, muddling the
           // translation between character and byte counts. However,
@@ -384,11 +331,12 @@ describe('chromium feature', () => {
           // failed to detect a real problem (perhaps related to DOM storage data caching)
           // wherein calling `getItem` immediately after `setItem` would appear to work
           // but then later (e.g. next tick) it would not.
-          setTimeout(() => {
+          await delay(1);
+          try {
             expect(storage.getItem(testKeyName)).to.have.lengthOf(length);
+          } finally {
             storage.removeItem(testKeyName);
-            done();
-          }, 1);
+          }
         });
         it(`throws when attempting to use more than 128MiB in ${storageName}`, () => {
           expect(() => {
@@ -404,11 +352,11 @@ describe('chromium feature', () => {
       });
     });
 
-    it('requesting persitent quota works', (done) => {
-      navigator.webkitPersistentStorage.requestQuota(1024 * 1024, (grantedBytes) => {
-        expect(grantedBytes).to.equal(1048576);
-        done();
+    it('requesting persitent quota works', async () => {
+      const grantedBytes = await new Promise(resolve => {
+        navigator.webkitPersistentStorage.requestQuota(1024 * 1024, resolve);
       });
+      expect(grantedBytes).to.equal(1048576);
     });
   });
 

+ 50 - 66
spec/node-spec.js

@@ -20,96 +20,85 @@ describe('node feature', () => {
     });
 
     describe('child_process.fork', () => {
-      it('works in current process', (done) => {
+      it('works in current process', async () => {
         const child = ChildProcess.fork(path.join(fixtures, 'module', 'ping.js'));
-        child.on('message', msg => {
-          expect(msg).to.equal('message');
-          done();
-        });
+        const message = emittedOnce(child, 'message');
         child.send('message');
+        const [msg] = await message;
+        expect(msg).to.equal('message');
       });
 
-      it('preserves args', (done) => {
+      it('preserves args', async () => {
         const args = ['--expose_gc', '-test', '1'];
         const child = ChildProcess.fork(path.join(fixtures, 'module', 'process_args.js'), args);
-        child.on('message', (msg) => {
-          expect(args).to.deep.equal(msg.slice(2));
-          done();
-        });
+        const message = emittedOnce(child, 'message');
         child.send('message');
+        const [msg] = await message;
+        expect(args).to.deep.equal(msg.slice(2));
       });
 
-      it('works in forked process', (done) => {
+      it('works in forked process', async () => {
         const child = ChildProcess.fork(path.join(fixtures, 'module', 'fork_ping.js'));
-        child.on('message', (msg) => {
-          expect(msg).to.equal('message');
-          done();
-        });
+        const message = emittedOnce(child, 'message');
         child.send('message');
+        const [msg] = await message;
+        expect(msg).to.equal('message');
       });
 
-      it('works in forked process when options.env is specifed', (done) => {
+      it('works in forked process when options.env is specifed', async () => {
         const child = ChildProcess.fork(path.join(fixtures, 'module', 'fork_ping.js'), [], {
           path: process.env.PATH
         });
-        child.on('message', (msg) => {
-          expect(msg).to.equal('message');
-          done();
-        });
+        const message = emittedOnce(child, 'message');
         child.send('message');
+        const [msg] = await message;
+        expect(msg).to.equal('message');
       });
 
-      it('has String::localeCompare working in script', (done) => {
+      it('has String::localeCompare working in script', async () => {
         const child = ChildProcess.fork(path.join(fixtures, 'module', 'locale-compare.js'));
-        child.on('message', (msg) => {
-          expect(msg).to.deep.equal([0, -1, 1]);
-          done();
-        });
+        const message = emittedOnce(child, 'message');
         child.send('message');
+        const [msg] = await message;
+        expect(msg).to.deep.equal([0, -1, 1]);
       });
 
-      it('has setImmediate working in script', (done) => {
+      it('has setImmediate working in script', async () => {
         const child = ChildProcess.fork(path.join(fixtures, 'module', 'set-immediate.js'));
-        child.on('message', (msg) => {
-          expect(msg).to.equal('ok');
-          done();
-        });
+        const message = emittedOnce(child, 'message');
         child.send('message');
+        const [msg] = await message;
+        expect(msg).to.equal('ok');
       });
 
-      it('pipes stdio', (done) => {
+      it('pipes stdio', async () => {
         const child = ChildProcess.fork(path.join(fixtures, 'module', 'process-stdout.js'), { silent: true });
         let data = '';
         child.stdout.on('data', (chunk) => {
           data += String(chunk);
         });
-        child.on('close', (code) => {
-          expect(code).to.equal(0);
-          expect(data).to.equal('pipes stdio');
-          done();
-        });
+        const [code] = await emittedOnce(child, 'close');
+        expect(code).to.equal(0);
+        expect(data).to.equal('pipes stdio');
       });
 
-      it('works when sending a message to a process forked with the --eval argument', (done) => {
+      it('works when sending a message to a process forked with the --eval argument', async () => {
         const source = "process.on('message', (message) => { process.send(message) })";
         const forked = ChildProcess.fork('--eval', [source]);
-        forked.once('message', (message) => {
-          expect(message).to.equal('hello');
-          done();
-        });
+        const message = emittedOnce(forked, 'message');
         forked.send('hello');
+        const [msg] = await message;
+        expect(msg).to.equal('hello');
       });
 
-      it('has the electron version in process.versions', (done) => {
+      it('has the electron version in process.versions', async () => {
         const source = 'process.send(process.versions)';
         const forked = ChildProcess.fork('--eval', [source]);
-        forked.on('message', (message) => {
-          expect(message)
-            .to.have.own.property('electron')
-            .that.is.a('string')
-            .and.matches(/^\d+\.\d+\.\d+(\S*)?$/);
-          done();
-        });
+        const [message] = await emittedOnce(forked, 'message');
+        expect(message)
+          .to.have.own.property('electron')
+          .that.is.a('string')
+          .and.matches(/^\d+\.\d+\.\d+(\S*)?$/);
       });
     });
 
@@ -120,7 +109,7 @@ describe('node feature', () => {
         if (child != null) child.kill();
       });
 
-      it('supports spawning Electron as a node process via the ELECTRON_RUN_AS_NODE env var', (done) => {
+      it('supports spawning Electron as a node process via the ELECTRON_RUN_AS_NODE env var', async () => {
         child = ChildProcess.spawn(process.execPath, [path.join(__dirname, 'fixtures', 'module', 'run-as-node.js')], {
           env: {
             ELECTRON_RUN_AS_NODE: true
@@ -131,13 +120,11 @@ describe('node feature', () => {
         child.stdout.on('data', data => {
           output += data;
         });
-        child.stdout.on('close', () => {
-          expect(JSON.parse(output)).to.deep.equal({
-            processLog: process.platform === 'win32' ? 'function' : 'undefined',
-            processType: 'undefined',
-            window: 'undefined'
-          });
-          done();
+        await emittedOnce(child.stdout, 'close');
+        expect(JSON.parse(output)).to.deep.equal({
+          processLog: process.platform === 'win32' ? 'function' : 'undefined',
+          processType: 'undefined',
+          window: 'undefined'
         });
       });
     });
@@ -258,18 +245,15 @@ describe('node feature', () => {
       }
     });
 
-    it('emit error when connect to a socket path without listeners', (done) => {
+    it('emit error when connect to a socket path without listeners', async () => {
       const socketPath = path.join(os.tmpdir(), 'atom-shell-test.sock');
       const script = path.join(fixtures, 'module', 'create_socket.js');
       const child = ChildProcess.fork(script, [socketPath]);
-      child.on('exit', (code) => {
-        expect(code).to.equal(0);
-        const client = require('net').connect(socketPath);
-        client.on('error', (error) => {
-          expect(error.code).to.equal('ECONNREFUSED');
-          done();
-        });
-      });
+      const [code] = await emittedOnce(child, 'exit');
+      expect(code).to.equal(0);
+      const client = require('net').connect(socketPath);
+      const [error] = await emittedOnce(client, 'error');
+      expect(error.code).to.equal('ECONNREFUSED');
     });
   });
 

+ 2 - 0
spec/spec-helpers.js

@@ -1,2 +1,4 @@
 exports.ifit = (condition) => (condition ? it : it.skip);
 exports.ifdescribe = (condition) => (condition ? describe : describe.skip);
+
+exports.delay = (time = 0) => new Promise(resolve => setTimeout(resolve, time));

+ 44 - 32
spec/webview-spec.js

@@ -4,7 +4,7 @@ const http = require('http');
 const url = require('url');
 const { ipcRenderer } = require('electron');
 const { emittedOnce, waitForEvent } = require('./events-helpers');
-const { ifdescribe, ifit } = require('./spec-helpers');
+const { ifdescribe, ifit, delay } = require('./spec-helpers');
 
 const features = process._linkedBinding('electron_common_features');
 const nativeModulesEnabled = process.env.ELECTRON_SKIP_NATIVE_MODULE_TESTS;
@@ -284,10 +284,15 @@ describe('<webview> tag', function () {
     it('sets the referrer url', (done) => {
       const referrer = 'http://github.com/';
       const server = http.createServer((req, res) => {
-        res.end();
-        server.close();
-        expect(req.headers.referer).to.equal(referrer);
-        done();
+        try {
+          expect(req.headers.referer).to.equal(referrer);
+          done();
+        } catch (e) {
+          done(e);
+        } finally {
+          res.end();
+          server.close();
+        }
       }).listen(0, '127.0.0.1', () => {
         const port = server.address().port;
         loadWebView(webview, {
@@ -737,24 +742,28 @@ describe('<webview> tag', function () {
       };
 
       const loadListener = () => {
-        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();
+        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);
         }
-
-        loadCount += 1;
       };
 
       webview.addEventListener('ipc-message', listener);
@@ -803,8 +812,12 @@ describe('<webview> tag', function () {
       server.listen(0, '127.0.0.1', () => {
         const port = server.address().port;
         webview.addEventListener('ipc-message', (e) => {
-          expect(e.channel).to.equal(message);
-          done();
+          try {
+            expect(e.channel).to.equal(message);
+            done();
+          } catch (e) {
+            done(e);
+          }
         });
         loadWebView(webview, {
           nodeintegration: 'on',
@@ -1042,15 +1055,14 @@ describe('<webview> tag', function () {
   });
 
   describe('will-attach-webview event', () => {
-    it('does not emit when src is not changed', (done) => {
+    it('does not emit when src is not changed', async () => {
+      console.log('loadWebView(webview)');
       loadWebView(webview);
-      setTimeout(() => {
-        const expectedErrorMessage =
-            'The WebView must be attached to the DOM ' +
-            'and the dom-ready event emitted before this method can be called.';
-        expect(() => { webview.stop(); }).to.throw(expectedErrorMessage);
-        done();
-      });
+      await delay();
+      const expectedErrorMessage =
+          'The WebView must be attached to the DOM ' +
+          'and the dom-ready event emitted before this method can be called.';
+      expect(() => { webview.stop(); }).to.throw(expectedErrorMessage);
     });
 
     it('supports changing the web preferences', async () => {

Some files were not shown because too many files changed in this diff