diff --git a/config/webpack.renderer.js b/config/webpack.renderer.js index 68a8be4f..8b8d00d5 100644 --- a/config/webpack.renderer.js +++ b/config/webpack.renderer.js @@ -5,92 +5,125 @@ const { spawn } = require('child_process'); const env = process.env.NAME || 'development'; -module.exports = { - devtool: 'source-map', - target: 'electron-renderer', - mode: env, +const outputPath = path.join(__dirname, '../app'); - entry: [ - path.join(__dirname, '../src/renderer/index.js'), - ], +module.exports = [ + { + devtool: 'source-map', + target: 'electron-renderer', + mode: env, + entry: [ + path.join(__dirname, '../src/renderer/index.js'), + ], - output: { - path: path.join(__dirname, '../app'), - filename: 'renderer.js', - }, + output: { + path: outputPath, + filename: 'renderer.js', + }, - module: { - rules: [ - { - test: /\.js$/, - exclude: /node_modules/, - use: { - loader: 'babel-loader', - }, - }, - { - test: /\.scss$/, - use: [ - { - loader: 'style-loader', + module: { + rules: [ + { + test: /\.js$/, + exclude: /node_modules/, + use: { + loader: 'babel-loader', }, - { - loader: 'css-loader', - options: { - modules: true, + }, + { + test: /\.scss$/, + use: [ + { + loader: 'style-loader', }, + { + loader: 'css-loader', + options: { + modules: true, + }, + }, + { + loader: 'sass-loader', + }, + ], + }, + { + test: /\.svg$/, + exclude: /node_modules/, + use: { + loader: 'file-loader', }, - { - loader: 'sass-loader', - }, - ], - }, - { - test: /\.svg$/, - exclude: /node_modules/, - use: { - loader: 'file-loader', }, - }, - { - test: /\.png$/, - use: { - loader: 'url-loader', + { + test: /\.png$/, + use: { + loader: 'url-loader', + }, }, - }, - ], - }, + ], + }, - plugins: [ - new webpack.NoEmitOnErrorsPlugin(), - new HtmlWebpackPlugin({ - template: path.join(__dirname, '../src/renderer/index.html'), - }), - ], + plugins: [ + new webpack.DefinePlugin({ + OUTPUT_DIR: JSON.stringify(outputPath), + }), + new webpack.NoEmitOnErrorsPlugin(), + new HtmlWebpackPlugin({ + template: path.join(__dirname, '../src/renderer/index.html'), + }), + ], - devServer: { - headers: { 'Access-Control-Allow-Origin': '*' }, - lazy: false, - compress: true, - noInfo: true, - stats: 'errors-only', - inline: true, - hot: true, - historyApiFallback: { - index: './app/index.html' - }, + devServer: { + headers: { 'Access-Control-Allow-Origin': '*' }, + lazy: false, + compress: true, + noInfo: true, + stats: 'errors-only', + inline: true, + hot: true, + historyApiFallback: { + index: './app/index.html' + }, - before() { - // eslint-disable-next-line no-console - console.log('Starting Main Process...'); - spawn('npm', ['run', 'start-main-dev'], { - shell: true, - env: process.env, - stdio: 'inherit' - }) - .on('close', code => process.exit(code)) + before() { // eslint-disable-next-line no-console - .on('error', spawnError => console.error(spawnError)); - } + console.log('Starting Main Process...'); + spawn('npm', ['run', 'start-main-dev'], { + shell: true, + env: process.env, + stdio: 'inherit' + }) + .on('close', code => process.exit(code)) + // eslint-disable-next-line no-console + .on('error', spawnError => console.error(spawnError)); + + console.log('Starting Webview Preload Process...'); + spawn('yarn', ['start', '--config-name webview-preload'], { + shell: true, + env: process.env, + stdio: 'inherit' + }) + .on('close', code => process.exit(code)) + // eslint-disable-next-line no-console + .on('error', spawnError => console.error(spawnError)); + } + }, + }, + { + entry: path.join(__dirname, '../src/renderer/lib/webview-preload.js'), + target: 'electron-renderer', + name: 'webview-preload', + devtool: 'cheap-module-source-map', + mode: env, + output: { + path: outputPath, + publicPath: '/', + filename: 'webview-preload.js', + }, + devServer: { + hot: false, + inline: false, + writeToDisk: true, + }, }, -}; +]; diff --git a/package.json b/package.json index 99d36fc9..b60a8980 100644 --- a/package.json +++ b/package.json @@ -91,6 +91,9 @@ "lcov", "text" ], + "globals": { + "OUTPUT_DIR": "/some/path" + }, "setupFilesAfterEnv": [ "/src/renderer/tests/setup.js" ], diff --git a/src/main/index.js b/src/main/index.js index dd9a67c8..19918108 100644 --- a/src/main/index.js +++ b/src/main/index.js @@ -1,5 +1,5 @@ import path from 'path'; -import { app, Menu } from 'electron'; +import { app, ipcMain, Menu } from 'electron'; import { autoUpdater } from 'electron-updater'; import electronDebug from 'electron-debug'; import createWindow from './helpers/window'; @@ -33,6 +33,10 @@ require('electron-context-menu')(); let mainWindow; let isQuitting = false; +ipcMain.on('notificationClick', () => { + mainWindow.show(); +}); + function createMainWindow() { if (process.argv.indexOf('test') !== -1) { try { diff --git a/src/main/index.spec.js b/src/main/index.spec.js index c57ddb12..5b738ffb 100644 --- a/src/main/index.spec.js +++ b/src/main/index.spec.js @@ -1,4 +1,4 @@ -const { app, BrowserWindow } = require('electron'); +const { app, ipcMain, BrowserWindow } = require('electron'); const chai = require('chai'); const sinon = require('sinon'); const sinonChai = require('sinon-chai'); @@ -10,9 +10,11 @@ chai.use(sinonChai); describe('App', () => { const sandbox = sinon.createSandbox(); const originalPlatform = process.platform; + const getOnReadyCb = () => app.on.getCalls().find(call => call.args[0] === 'ready').args[1]; beforeEach(() => { sandbox.stub(app, 'requestSingleInstanceLock').returns(true); + sandbox.stub(ipcMain, 'on'); }); afterEach(() => { @@ -24,13 +26,29 @@ describe('App', () => { }); }); + it('should listen for ipcMain notificationClick', async () => { + sandbox.spy(BrowserWindow.prototype, 'show'); + sandbox.stub(app, 'on'); + require('./'); + const onReadyCb = getOnReadyCb(); + + await onReadyCb(); + + expect(ipcMain.on).to.have.been.calledWith('notificationClick', sinon.match.func); + ipcMain.on.getCalls() + .find(call => call.args[0] === 'notificationClick') + .args[1](); + + expect(BrowserWindow.prototype.show).to.have.been.called; + }); + it('should not throw an exception upon ready', async () => { sandbox.spy(app, 'on'); require('./'); expect(app.on).to.have.been.called; - const onReadyCb = app.on.getCalls().find(call => call.args[0] === 'ready').args[1]; + const onReadyCb = getOnReadyCb(); expect(async () => await onReadyCb()).to.not.throw(); }); @@ -39,7 +57,7 @@ describe('App', () => { sandbox.spy(app, 'on'); require('./'); - const onReadyCb = app.on.getCalls().find(call => call.args[0] === 'ready').args[1]; + const onReadyCb = getOnReadyCb(); Object.defineProperty(process, 'platform', { value: 'win32', }); diff --git a/src/renderer/.eslintrc.js b/src/renderer/.eslintrc.js index 12e1564b..ffc48050 100644 --- a/src/renderer/.eslintrc.js +++ b/src/renderer/.eslintrc.js @@ -5,6 +5,7 @@ module.exports = { }, globals: { module: true, + OUTPUT_DIR: true, }, overrides: [ { diff --git a/src/renderer/lib/webview-handler.js b/src/renderer/lib/webview-handler.js index ff908c2b..c0d325f2 100644 --- a/src/renderer/lib/webview-handler.js +++ b/src/renderer/lib/webview-handler.js @@ -97,6 +97,10 @@ export class WebviewHandler { } const webview = document.createElement('webview'); + + const preloadPath = `file://${OUTPUT_DIR}/webview-preload.js`; + + webview.setAttribute('preload', preloadPath); webview.setAttribute('src', url); webview.setAttribute('style', webviewStyle); webview.setAttribute('data-name', name); diff --git a/src/renderer/lib/webview-handler.test.js b/src/renderer/lib/webview-handler.test.js index f2576943..fd292b15 100644 --- a/src/renderer/lib/webview-handler.test.js +++ b/src/renderer/lib/webview-handler.test.js @@ -269,6 +269,7 @@ describe('lib/WebviewHandler', () => { expect(mockElem.setAttribute).to.have.been.calledWith('src', url); expect(mockElem.setAttribute).to.have.been.calledWith('style', sinon.match(/absolute/)); expect(mockElem.setAttribute).to.have.been.calledWith('data-name', name); + expect(mockElem.setAttribute).to.have.been.calledWith('preload', sinon.match(/^file:\/\//)); expect(webviewHandler.container.appendChild).to.have.been.calledWith(mockElem); expect(webviewHandler.addedWebviews).to.eql([name]); diff --git a/src/renderer/lib/webview-preload.js b/src/renderer/lib/webview-preload.js new file mode 100644 index 00000000..a265425b --- /dev/null +++ b/src/renderer/lib/webview-preload.js @@ -0,0 +1,13 @@ +import { ipcRenderer } from 'electron'; + +const originalNotification = window.Notification; + +window.Notification = function(title, opts) { + const _notice = new originalNotification(title, opts); + + _notice.addEventListener('click', () => { + ipcRenderer.sendToHost('notificationClick', opts); + }); + + return _notice; +}; diff --git a/src/renderer/lib/webview-preload.test.js b/src/renderer/lib/webview-preload.test.js new file mode 100644 index 00000000..f708c75e --- /dev/null +++ b/src/renderer/lib/webview-preload.test.js @@ -0,0 +1,31 @@ +import { expect } from 'chai'; +import sinon from 'sinon'; +import { ipcRenderer } from 'electron'; + +describe('/renderer/li/webview-preload', () => { + const sandbox = sinon.createSandbox(); + let originalNotification; + + beforeEach(() => { + originalNotification = sinon.stub(); + window.Notification = originalNotification; + require('./webview-preload'); + }); + + afterEach(() => { + sandbox.restore(); + }); + + it('should ipcRenderer.sendToHost upon click event', () => { + const mockNoti = document.createElement('div'); + originalNotification.returns(mockNoti); + ipcRenderer.sendToHost = sinon.spy(); + + const noti = new Notification('read this'); + + const event = new MouseEvent('click', { bubbles: true, cancelable: false }); + noti.dispatchEvent(event); + + expect(ipcRenderer.sendToHost).to.have.been.calledWith('notificationClick'); + }); +}); diff --git a/src/renderer/middlewares/Webviews/index.js b/src/renderer/middlewares/Webviews/index.js index 4e57145b..96ee2843 100644 --- a/src/renderer/middlewares/Webviews/index.js +++ b/src/renderer/middlewares/Webviews/index.js @@ -1,3 +1,4 @@ +import { ipcRenderer } from 'electron'; import WebviewHandler from '../../lib/webview-handler'; import { DISPLAY_WEBVIEW, @@ -17,6 +18,14 @@ const WebviewsMiddleware = ({ dispatch }) => next => action => { const webview = WebviewHandler.addWebview(action.name, url); dispatch(monitorWebview(webview, action.name)); createdWebviews[action.name] = true; + + webview.addEventListener('ipc-message', (event) => { + if (event.channel === 'notificationClick') { + ipcRenderer.send('notificationClick', event); + + window.location.assign(`#/mailbox/${action.name}`); + } + }); } WebviewHandler.show(); diff --git a/src/renderer/middlewares/Webviews/index.test.js b/src/renderer/middlewares/Webviews/index.test.js index de9a17f1..0efa326c 100644 --- a/src/renderer/middlewares/Webviews/index.test.js +++ b/src/renderer/middlewares/Webviews/index.test.js @@ -1,5 +1,6 @@ import { expect } from 'chai'; import sinon from 'sinon'; +import { ipcRenderer } from 'electron'; import WebviewHandler from '../../lib/webview-handler'; import { @@ -15,6 +16,10 @@ describe('middlewares/Webviews', () => { const next = sandbox.spy(); const dispatch = sandbox.spy(); + beforeEach(() => { + sandbox.stub(WebviewHandler, 'addWebview').callsFake(() => document.createElement('webview')); + }); + afterEach(() => { sandbox.restore(); }); @@ -30,7 +35,7 @@ describe('middlewares/Webviews', () => { it('should call monitorWebview upon first DISPLAY_WEBVIEW', () => { const name = 'jens'; const mockAction = { mon: 'itor' }; - const mockWebview = { web: 'view' }; + const mockWebview = document.createElement('webview'); const darkTheme = true; const action = { type: DISPLAY_WEBVIEW, @@ -38,7 +43,7 @@ describe('middlewares/Webviews', () => { name, }; sandbox.stub(actions, 'monitorWebview').returns(mockAction); - sandbox.stub(WebviewHandler, 'addWebview').callsFake((n) => name === n && mockWebview); + WebviewHandler.addWebview.callsFake((n) => name === n && mockWebview); sandbox.stub(WebviewHandler, 'show'); sandbox.stub(WebviewHandler, 'displayView'); @@ -56,6 +61,32 @@ describe('middlewares/Webviews', () => { expect(dispatch).to.not.have.been.called; }); + it('should listen for ipc-message event upon first DISPLAY_WEBVIEW', () => { + const mockWebview = document.createElement('webview'); + sandbox.stub(mockWebview, 'addEventListener'); + const action = { + type: DISPLAY_WEBVIEW, + darkTheme: true, + name: 'ingegerd', + }; + sandbox.stub(actions, 'monitorWebview'); + WebviewHandler.addWebview.returns(mockWebview); + sandbox.stub(WebviewHandler, 'show'); + sandbox.stub(WebviewHandler, 'displayView'); + sandbox.stub(ipcRenderer, 'send'); + + Webviews({ dispatch })(next)(action); + + expect(mockWebview.addEventListener).to.have.been.calledWith('ipc-message', sinon.match.func); + + const mockEvent = { channel: 'notificationClick' }; + mockWebview.addEventListener.getCalls() + .find(call => call.args[0] === 'ipc-message') + .args[1](mockEvent); + + expect(ipcRenderer.send).to.have.been.calledWith('notificationClick', mockEvent); + }); + it('should call WebviewHandler upon DISPLAY_WEBVIEW', () => { const name = 'matthew'; sandbox.stub(WebviewHandler, 'show'); @@ -76,7 +107,7 @@ describe('middlewares/Webviews', () => { }); }); - it('should add an window event listener upon DISPLAY_WEBVIEW', () => { + it('should add a window event listener upon DISPLAY_WEBVIEW', () => { sandbox.stub(window, 'addEventListener'); Webviews({ dispatch })(next)({ type: DISPLAY_WEBVIEW,