diff --git a/atom/browser/api/lib/app.coffee b/atom/browser/api/lib/app.coffee deleted file mode 100644 index fa9df9d9c70a..000000000000 --- a/atom/browser/api/lib/app.coffee +++ /dev/null @@ -1,73 +0,0 @@ -{deprecate, session, Menu} = require 'electron' -{EventEmitter} = require 'events' - -bindings = process.atomBinding 'app' -downloadItemBindings = process.atomBinding 'download_item' - -app = bindings.app -app.__proto__ = EventEmitter.prototype - -app.setApplicationMenu = (menu) -> - Menu.setApplicationMenu menu - -app.getApplicationMenu = -> - Menu.getApplicationMenu() - -app.commandLine = - appendSwitch: bindings.appendSwitch, - appendArgument: bindings.appendArgument - -if process.platform is 'darwin' - app.dock = - bounce: (type='informational') -> bindings.dockBounce type - cancelBounce: bindings.dockCancelBounce - setBadge: bindings.dockSetBadgeText - getBadge: bindings.dockGetBadgeText - hide: bindings.dockHide - show: bindings.dockShow - setMenu: bindings.dockSetMenu - -appPath = null -app.setAppPath = (path) -> - appPath = path - -app.getAppPath = -> - appPath - -### Routes the events to webContents. ### -for name in ['login', 'certificate-error', 'select-client-certificate'] - do (name) -> - app.on name, (event, webContents, args...) -> - webContents.emit name, event, args... - -### Deprecated. ### -app.getHomeDir = deprecate 'app.getHomeDir', 'app.getPath', -> - @getPath 'home' -app.getDataPath = deprecate 'app.getDataPath', 'app.getPath', -> - @getPath 'userData' -app.setDataPath = deprecate 'app.setDataPath', 'app.setPath', (path) -> - @setPath 'userData', path -app.resolveProxy = deprecate 'app.resolveProxy', 'session.defaultSession.resolveProxy', (url, callback) -> - session.defaultSession.resolveProxy url, callback -deprecate.rename app, 'terminate', 'quit' -deprecate.event app, 'finish-launching', 'ready', -> - ### give default app a chance to setup default menu. ### - setImmediate => - @emit 'finish-launching' -deprecate.event app, 'activate-with-no-open-windows', 'activate', (event, hasVisibleWindows) -> - @emit 'activate-with-no-open-windows', event if not hasVisibleWindows -deprecate.event app, 'select-certificate', 'select-client-certificate' - -### Wrappers for native classes. ### -wrapDownloadItem = (downloadItem) -> - ### downloadItem is an EventEmitter. ### - downloadItem.__proto__ = EventEmitter.prototype - ### Deprecated. ### - deprecate.property downloadItem, 'url', 'getURL' - deprecate.property downloadItem, 'filename', 'getFilename' - deprecate.property downloadItem, 'mimeType', 'getMimeType' - deprecate.rename downloadItem, 'getUrl', 'getURL' -downloadItemBindings._setWrapDownloadItem wrapDownloadItem - -### Only one App object pemitted. ### -module.exports = app diff --git a/atom/browser/api/lib/app.js b/atom/browser/api/lib/app.js new file mode 100644 index 000000000000..84b0c3c77e89 --- /dev/null +++ b/atom/browser/api/lib/app.js @@ -0,0 +1,131 @@ +var EventEmitter, Menu, app, appPath, bindings, deprecate, downloadItemBindings, fn, i, len, name, ref, ref1, session, wrapDownloadItem, + slice = [].slice; + +ref = require('electron'), deprecate = ref.deprecate, session = ref.session, Menu = ref.Menu; + +EventEmitter = require('events').EventEmitter; + +bindings = process.atomBinding('app'); + +downloadItemBindings = process.atomBinding('download_item'); + +app = bindings.app; + +app.__proto__ = EventEmitter.prototype; + +app.setApplicationMenu = function(menu) { + return Menu.setApplicationMenu(menu); +}; + +app.getApplicationMenu = function() { + return Menu.getApplicationMenu(); +}; + +app.commandLine = { + appendSwitch: bindings.appendSwitch, + appendArgument: bindings.appendArgument +}; + +if (process.platform === 'darwin') { + app.dock = { + bounce: function(type) { + if (type == null) { + type = 'informational'; + } + return bindings.dockBounce(type); + }, + cancelBounce: bindings.dockCancelBounce, + setBadge: bindings.dockSetBadgeText, + getBadge: bindings.dockGetBadgeText, + hide: bindings.dockHide, + show: bindings.dockShow, + setMenu: bindings.dockSetMenu + }; +} + +appPath = null; + +app.setAppPath = function(path) { + return appPath = path; +}; + +app.getAppPath = function() { + return appPath; +}; + + +/* Routes the events to webContents. */ + +ref1 = ['login', 'certificate-error', 'select-client-certificate']; +fn = function(name) { + return app.on(name, function() { + var args, event, webContents; + event = arguments[0], webContents = arguments[1], args = 3 <= arguments.length ? slice.call(arguments, 2) : []; + return webContents.emit.apply(webContents, [name, event].concat(slice.call(args))); + }); +}; +for (i = 0, len = ref1.length; i < len; i++) { + name = ref1[i]; + fn(name); +} + + +/* Deprecated. */ + +app.getHomeDir = deprecate('app.getHomeDir', 'app.getPath', function() { + return this.getPath('home'); +}); + +app.getDataPath = deprecate('app.getDataPath', 'app.getPath', function() { + return this.getPath('userData'); +}); + +app.setDataPath = deprecate('app.setDataPath', 'app.setPath', function(path) { + return this.setPath('userData', path); +}); + +app.resolveProxy = deprecate('app.resolveProxy', 'session.defaultSession.resolveProxy', function(url, callback) { + return session.defaultSession.resolveProxy(url, callback); +}); + +deprecate.rename(app, 'terminate', 'quit'); + +deprecate.event(app, 'finish-launching', 'ready', function() { + + /* give default app a chance to setup default menu. */ + return setImmediate((function(_this) { + return function() { + return _this.emit('finish-launching'); + }; + })(this)); +}); + +deprecate.event(app, 'activate-with-no-open-windows', 'activate', function(event, hasVisibleWindows) { + if (!hasVisibleWindows) { + return this.emit('activate-with-no-open-windows', event); + } +}); + +deprecate.event(app, 'select-certificate', 'select-client-certificate'); + + +/* Wrappers for native classes. */ + +wrapDownloadItem = function(downloadItem) { + + /* downloadItem is an EventEmitter. */ + downloadItem.__proto__ = EventEmitter.prototype; + + /* Deprecated. */ + deprecate.property(downloadItem, 'url', 'getURL'); + deprecate.property(downloadItem, 'filename', 'getFilename'); + deprecate.property(downloadItem, 'mimeType', 'getMimeType'); + return deprecate.rename(downloadItem, 'getUrl', 'getURL'); +}; + +downloadItemBindings._setWrapDownloadItem(wrapDownloadItem); + + +/* Only one App object pemitted. */ + +module.exports = app; diff --git a/atom/browser/api/lib/auto-updater.coffee b/atom/browser/api/lib/auto-updater.coffee deleted file mode 100644 index 5cb75832c755..000000000000 --- a/atom/browser/api/lib/auto-updater.coffee +++ /dev/null @@ -1,12 +0,0 @@ -{deprecate} = require 'electron' - -autoUpdater = - if process.platform is 'win32' - require './auto-updater/auto-updater-win' - else - require './auto-updater/auto-updater-native' - -### Deprecated. ### -deprecate.rename autoUpdater, 'setFeedUrl', 'setFeedURL' - -module.exports = autoUpdater diff --git a/atom/browser/api/lib/auto-updater.js b/atom/browser/api/lib/auto-updater.js new file mode 100644 index 000000000000..d666ba8eed6c --- /dev/null +++ b/atom/browser/api/lib/auto-updater.js @@ -0,0 +1,12 @@ +var autoUpdater, deprecate; + +deprecate = require('electron').deprecate; + +autoUpdater = process.platform === 'win32' ? require('./auto-updater/auto-updater-win') : require('./auto-updater/auto-updater-native'); + + +/* Deprecated. */ + +deprecate.rename(autoUpdater, 'setFeedUrl', 'setFeedURL'); + +module.exports = autoUpdater; diff --git a/atom/browser/api/lib/auto-updater/auto-updater-native.coffee b/atom/browser/api/lib/auto-updater/auto-updater-native.coffee deleted file mode 100644 index 187be64f5ade..000000000000 --- a/atom/browser/api/lib/auto-updater/auto-updater-native.coffee +++ /dev/null @@ -1,6 +0,0 @@ -{EventEmitter} = require 'events' -{autoUpdater} = process.atomBinding 'auto_updater' - -autoUpdater.__proto__ = EventEmitter.prototype - -module.exports = autoUpdater diff --git a/atom/browser/api/lib/auto-updater/auto-updater-native.js b/atom/browser/api/lib/auto-updater/auto-updater-native.js new file mode 100644 index 000000000000..9f22b33feb1d --- /dev/null +++ b/atom/browser/api/lib/auto-updater/auto-updater-native.js @@ -0,0 +1,9 @@ +var EventEmitter, autoUpdater; + +EventEmitter = require('events').EventEmitter; + +autoUpdater = process.atomBinding('auto_updater').autoUpdater; + +autoUpdater.__proto__ = EventEmitter.prototype; + +module.exports = autoUpdater; diff --git a/atom/browser/api/lib/auto-updater/auto-updater-win.coffee b/atom/browser/api/lib/auto-updater/auto-updater-win.coffee deleted file mode 100644 index 6d1caecb6bcb..000000000000 --- a/atom/browser/api/lib/auto-updater/auto-updater-win.coffee +++ /dev/null @@ -1,44 +0,0 @@ -{app} = require 'electron' -{EventEmitter} = require 'events' -url = require 'url' - -squirrelUpdate = require './squirrel-update-win' - -class AutoUpdater extends EventEmitter - quitAndInstall: -> - squirrelUpdate.processStart() - app.quit() - - setFeedURL: (updateURL) -> - @updateURL = updateURL - - checkForUpdates: -> - return @emitError 'Update URL is not set' unless @updateURL - return @emitError 'Can not find Squirrel' unless squirrelUpdate.supported() - - @emit 'checking-for-update' - - squirrelUpdate.download @updateURL, (error, update) => - return @emitError error if error? - return @emit 'update-not-available' unless update? - - @emit 'update-available' - - squirrelUpdate.update @updateURL, (error) => - return @emitError error if error? - - {releaseNotes, version} = update - ### Following information is not available on Windows, so fake them. ### - date = new Date - url = @updateURL - - @emit 'update-downloaded', {}, releaseNotes, version, date, url, => @quitAndInstall() - - ### - Private: Emit both error object and message, this is to keep compatibility - with Old APIs. - ### - emitError: (message) -> - @emit 'error', new Error(message), message - -module.exports = new AutoUpdater diff --git a/atom/browser/api/lib/auto-updater/auto-updater-win.js b/atom/browser/api/lib/auto-updater/auto-updater-win.js new file mode 100644 index 000000000000..b2ef7361d2d3 --- /dev/null +++ b/atom/browser/api/lib/auto-updater/auto-updater-win.js @@ -0,0 +1,78 @@ +var AutoUpdater, EventEmitter, app, squirrelUpdate, url, + extend = function(child, parent) { for (var key in parent) { if (hasProp.call(parent, key)) child[key] = parent[key]; } function ctor() { this.constructor = child; } ctor.prototype = parent.prototype; child.prototype = new ctor(); child.__super__ = parent.prototype; return child; }, + hasProp = {}.hasOwnProperty; + +app = require('electron').app; + +EventEmitter = require('events').EventEmitter; + +url = require('url'); + +squirrelUpdate = require('./squirrel-update-win'); + +AutoUpdater = (function(superClass) { + extend(AutoUpdater, superClass); + + function AutoUpdater() { + return AutoUpdater.__super__.constructor.apply(this, arguments); + } + + AutoUpdater.prototype.quitAndInstall = function() { + squirrelUpdate.processStart(); + return app.quit(); + }; + + AutoUpdater.prototype.setFeedURL = function(updateURL) { + return this.updateURL = updateURL; + }; + + AutoUpdater.prototype.checkForUpdates = function() { + if (!this.updateURL) { + return this.emitError('Update URL is not set'); + } + if (!squirrelUpdate.supported()) { + return this.emitError('Can not find Squirrel'); + } + this.emit('checking-for-update'); + return squirrelUpdate.download(this.updateURL, (function(_this) { + return function(error, update) { + if (error != null) { + return _this.emitError(error); + } + if (update == null) { + return _this.emit('update-not-available'); + } + _this.emit('update-available'); + return squirrelUpdate.update(_this.updateURL, function(error) { + var date, releaseNotes, version; + if (error != null) { + return _this.emitError(error); + } + releaseNotes = update.releaseNotes, version = update.version; + + /* Following information is not available on Windows, so fake them. */ + date = new Date; + url = _this.updateURL; + return _this.emit('update-downloaded', {}, releaseNotes, version, date, url, function() { + return _this.quitAndInstall(); + }); + }); + }; + })(this)); + }; + + + /* + Private: Emit both error object and message, this is to keep compatibility + with Old APIs. + */ + + AutoUpdater.prototype.emitError = function(message) { + return this.emit('error', new Error(message), message); + }; + + return AutoUpdater; + +})(EventEmitter); + +module.exports = new AutoUpdater; diff --git a/atom/browser/api/lib/auto-updater/squirrel-update-win.coffee b/atom/browser/api/lib/auto-updater/squirrel-update-win.coffee deleted file mode 100644 index 5c4342b0ff48..000000000000 --- a/atom/browser/api/lib/auto-updater/squirrel-update-win.coffee +++ /dev/null @@ -1,71 +0,0 @@ -fs = require 'fs' -path = require 'path' -{spawn} = require 'child_process' - -### i.e. my-app/app-0.1.13/ ### -appFolder = path.dirname process.execPath -### i.e. my-app/Update.exe ### -updateExe = path.resolve appFolder, '..', 'Update.exe' -exeName = path.basename process.execPath - -### - Spawn a command and invoke the callback when it completes with an error - and the output from standard out. -### -spawnUpdate = (args, detached, callback) -> - try - spawnedProcess = spawn updateExe, args, {detached} - catch error - ### Shouldn't happen, but still guard it. ### - process.nextTick -> callback error - return - - stdout = '' - stderr = '' - spawnedProcess.stdout.on 'data', (data) -> stdout += data - spawnedProcess.stderr.on 'data', (data) -> stderr += data - - errorEmitted = false - spawnedProcess.on 'error', (error) -> - errorEmitted = true - callback error - spawnedProcess.on 'exit', (code, signal) -> - ### We may have already emitted an error. ### - return if errorEmitted - - ### Process terminated with error. ### - if code isnt 0 - return callback "Command failed: #{signal ? code}\n#{stderr}" - - ### Success. ### - callback null, stdout - -### Start an instance of the installed app. ### -exports.processStart = (callback) -> - spawnUpdate ['--processStart', exeName], true, -> - -### Download the releases specified by the URL and write new results to stdout. ### -exports.download = (updateURL, callback) -> - spawnUpdate ['--download', updateURL], false, (error, stdout) -> - return callback(error) if error? - - try - ### Last line of output is the JSON details about the releases ### - json = stdout.trim().split('\n').pop() - update = JSON.parse(json)?.releasesToApply?.pop?() - catch - return callback "Invalid result:\n#{stdout}" - - callback null, update - -### Update the application to the latest remote version specified by URL. ### -exports.update = (updateURL, callback) -> - spawnUpdate ['--update', updateURL], false, callback - -### Is the Update.exe installed with the current application? ### -exports.supported = -> - try - fs.accessSync updateExe, fs.R_OK - return true - catch - return false diff --git a/atom/browser/api/lib/auto-updater/squirrel-update-win.js b/atom/browser/api/lib/auto-updater/squirrel-update-win.js new file mode 100644 index 000000000000..205f08ce34f9 --- /dev/null +++ b/atom/browser/api/lib/auto-updater/squirrel-update-win.js @@ -0,0 +1,118 @@ +var appFolder, exeName, fs, path, spawn, spawnUpdate, updateExe; + +fs = require('fs'); + +path = require('path'); + +spawn = require('child_process').spawn; + + +/* i.e. my-app/app-0.1.13/ */ + +appFolder = path.dirname(process.execPath); + + +/* i.e. my-app/Update.exe */ + +updateExe = path.resolve(appFolder, '..', 'Update.exe'); + +exeName = path.basename(process.execPath); + + +/* + Spawn a command and invoke the callback when it completes with an error + and the output from standard out. + */ + +spawnUpdate = function(args, detached, callback) { + var error, error1, errorEmitted, spawnedProcess, stderr, stdout; + try { + spawnedProcess = spawn(updateExe, args, { + detached: detached + }); + } catch (error1) { + error = error1; + + /* Shouldn't happen, but still guard it. */ + process.nextTick(function() { + return callback(error); + }); + return; + } + stdout = ''; + stderr = ''; + spawnedProcess.stdout.on('data', function(data) { + return stdout += data; + }); + spawnedProcess.stderr.on('data', function(data) { + return stderr += data; + }); + errorEmitted = false; + spawnedProcess.on('error', function(error) { + errorEmitted = true; + return callback(error); + }); + return spawnedProcess.on('exit', function(code, signal) { + + /* We may have already emitted an error. */ + if (errorEmitted) { + return; + } + + /* Process terminated with error. */ + if (code !== 0) { + return callback("Command failed: " + (signal != null ? signal : code) + "\n" + stderr); + } + + /* Success. */ + return callback(null, stdout); + }); +}; + + +/* Start an instance of the installed app. */ + +exports.processStart = function(callback) { + return spawnUpdate(['--processStart', exeName], true, function() {}); +}; + + +/* Download the releases specified by the URL and write new results to stdout. */ + +exports.download = function(updateURL, callback) { + return spawnUpdate(['--download', updateURL], false, function(error, stdout) { + var error1, json, ref, ref1, update; + if (error != null) { + return callback(error); + } + try { + + /* Last line of output is the JSON details about the releases */ + json = stdout.trim().split('\n').pop(); + update = (ref = JSON.parse(json)) != null ? (ref1 = ref.releasesToApply) != null ? typeof ref1.pop === "function" ? ref1.pop() : void 0 : void 0 : void 0; + } catch (error1) { + return callback("Invalid result:\n" + stdout); + } + return callback(null, update); + }); +}; + + +/* Update the application to the latest remote version specified by URL. */ + +exports.update = function(updateURL, callback) { + return spawnUpdate(['--update', updateURL], false, callback); +}; + + +/* Is the Update.exe installed with the current application? */ + +exports.supported = function() { + var error1; + try { + fs.accessSync(updateExe, fs.R_OK); + return true; + } catch (error1) { + return false; + } +}; diff --git a/atom/browser/api/lib/browser-window.coffee b/atom/browser/api/lib/browser-window.coffee deleted file mode 100644 index dd54f939df6f..000000000000 --- a/atom/browser/api/lib/browser-window.coffee +++ /dev/null @@ -1,119 +0,0 @@ -{ipcMain, deprecate} = require 'electron' -{EventEmitter} = require 'events' - -{BrowserWindow} = process.atomBinding 'window' -BrowserWindow::__proto__ = EventEmitter.prototype - -BrowserWindow::_init = -> - ### avoid recursive require. ### - {app} = require 'electron' - - ### Simulate the application menu on platforms other than OS X. ### - if process.platform isnt 'darwin' - menu = app.getApplicationMenu() - @setMenu menu if menu? - - ### Make new windows requested by links behave like "window.open" ### - @webContents.on '-new-window', (event, url, frameName) -> - options = show: true, width: 800, height: 600 - ipcMain.emit 'ATOM_SHELL_GUEST_WINDOW_MANAGER_WINDOW_OPEN', event, url, frameName, options - - ### - window.resizeTo(...) - window.moveTo(...) - ### - @webContents.on 'move', (event, size) => - @setBounds size - - ### Hide the auto-hide menu when webContents is focused. ### - @webContents.on 'activate', => - if process.platform isnt 'darwin' and @isMenuBarAutoHide() and @isMenuBarVisible() - @setMenuBarVisibility false - - ### Forward the crashed event. ### - @webContents.on 'crashed', => - @emit 'crashed' - - ### Change window title to page title. ### - @webContents.on 'page-title-updated', (event, title, explicitSet) => - @emit 'page-title-updated', event, title - @setTitle title unless event.defaultPrevented - - ### - Sometimes the webContents doesn't get focus when window is shown, so we have - to force focusing on webContents in this case. The safest way is to focus it - when we first start to load URL, if we do it earlier it won't have effect, - if we do it later we might move focus in the page. - Though this hack is only needed on OS X when the app is launched from - Finder, we still do it on all platforms in case of other bugs we don't know. - ### - @webContents.once 'load-url', -> - @focus() - - ### Redirect focus/blur event to app instance too. ### - @on 'blur', (event) => - app.emit 'browser-window-blur', event, this - @on 'focus', (event) => - app.emit 'browser-window-focus', event, this - - ### Notify the creation of the window. ### - app.emit 'browser-window-created', {}, this - - ### Be compatible with old APIs. ### - @webContents.on 'devtools-focused', => @emit 'devtools-focused' - @webContents.on 'devtools-opened', => @emit 'devtools-opened' - @webContents.on 'devtools-closed', => @emit 'devtools-closed' - Object.defineProperty this, 'devToolsWebContents', - enumerable: true, - configurable: false, - get: -> @webContents.devToolsWebContents - -BrowserWindow.getFocusedWindow = -> - windows = BrowserWindow.getAllWindows() - return window for window in windows when window.isFocused() - null - -BrowserWindow.fromWebContents = (webContents) -> - windows = BrowserWindow.getAllWindows() - return window for window in windows when window.webContents?.equal webContents - -BrowserWindow.fromDevToolsWebContents = (webContents) -> - windows = BrowserWindow.getAllWindows() - return window for window in windows when window.devToolsWebContents?.equal webContents - -### Helpers. ### -BrowserWindow::loadURL = -> @webContents.loadURL.apply @webContents, arguments -BrowserWindow::getURL = -> @webContents.getURL() -BrowserWindow::reload = -> @webContents.reload.apply @webContents, arguments -BrowserWindow::send = -> @webContents.send.apply @webContents, arguments -BrowserWindow::openDevTools = -> @webContents.openDevTools.apply @webContents, arguments -BrowserWindow::closeDevTools = -> @webContents.closeDevTools() -BrowserWindow::isDevToolsOpened = -> @webContents.isDevToolsOpened() -BrowserWindow::isDevToolsFocused = -> @webContents.isDevToolsFocused() -BrowserWindow::toggleDevTools = -> @webContents.toggleDevTools() -BrowserWindow::inspectElement = -> @webContents.inspectElement.apply @webContents, arguments -BrowserWindow::inspectServiceWorker = -> @webContents.inspectServiceWorker() - -### Deprecated. ### -deprecate.member BrowserWindow, 'undo', 'webContents' -deprecate.member BrowserWindow, 'redo', 'webContents' -deprecate.member BrowserWindow, 'cut', 'webContents' -deprecate.member BrowserWindow, 'copy', 'webContents' -deprecate.member BrowserWindow, 'paste', 'webContents' -deprecate.member BrowserWindow, 'selectAll', 'webContents' -deprecate.member BrowserWindow, 'reloadIgnoringCache', 'webContents' -deprecate.member BrowserWindow, 'isLoading', 'webContents' -deprecate.member BrowserWindow, 'isWaitingForResponse', 'webContents' -deprecate.member BrowserWindow, 'stop', 'webContents' -deprecate.member BrowserWindow, 'isCrashed', 'webContents' -deprecate.member BrowserWindow, 'print', 'webContents' -deprecate.member BrowserWindow, 'printToPDF', 'webContents' -deprecate.rename BrowserWindow, 'restart', 'reload' -deprecate.rename BrowserWindow, 'loadUrl', 'loadURL' -deprecate.rename BrowserWindow, 'getUrl', 'getURL' -BrowserWindow::executeJavaScriptInDevTools = deprecate 'executeJavaScriptInDevTools', 'devToolsWebContents.executeJavaScript', (code) -> - @devToolsWebContents?.executeJavaScript code -BrowserWindow::getPageTitle = deprecate 'getPageTitle', 'webContents.getTitle', -> - @webContents?.getTitle() - -module.exports = BrowserWindow diff --git a/atom/browser/api/lib/browser-window.js b/atom/browser/api/lib/browser-window.js new file mode 100644 index 000000000000..694c7c25e237 --- /dev/null +++ b/atom/browser/api/lib/browser-window.js @@ -0,0 +1,250 @@ +var BrowserWindow, EventEmitter, deprecate, ipcMain, ref; + +ref = require('electron'), ipcMain = ref.ipcMain, deprecate = ref.deprecate; + +EventEmitter = require('events').EventEmitter; + +BrowserWindow = process.atomBinding('window').BrowserWindow; + +BrowserWindow.prototype.__proto__ = EventEmitter.prototype; + +BrowserWindow.prototype._init = function() { + + /* avoid recursive require. */ + var app, menu; + app = require('electron').app; + + /* Simulate the application menu on platforms other than OS X. */ + if (process.platform !== 'darwin') { + menu = app.getApplicationMenu(); + if (menu != null) { + this.setMenu(menu); + } + } + + /* Make new windows requested by links behave like "window.open" */ + this.webContents.on('-new-window', function(event, url, frameName) { + var options; + options = { + show: true, + width: 800, + height: 600 + }; + return ipcMain.emit('ATOM_SHELL_GUEST_WINDOW_MANAGER_WINDOW_OPEN', event, url, frameName, options); + }); + + /* + window.resizeTo(...) + window.moveTo(...) + */ + this.webContents.on('move', (function(_this) { + return function(event, size) { + return _this.setBounds(size); + }; + })(this)); + + /* Hide the auto-hide menu when webContents is focused. */ + this.webContents.on('activate', (function(_this) { + return function() { + if (process.platform !== 'darwin' && _this.isMenuBarAutoHide() && _this.isMenuBarVisible()) { + return _this.setMenuBarVisibility(false); + } + }; + })(this)); + + /* Forward the crashed event. */ + this.webContents.on('crashed', (function(_this) { + return function() { + return _this.emit('crashed'); + }; + })(this)); + + /* Change window title to page title. */ + this.webContents.on('page-title-updated', (function(_this) { + return function(event, title, explicitSet) { + _this.emit('page-title-updated', event, title); + if (!event.defaultPrevented) { + return _this.setTitle(title); + } + }; + })(this)); + + /* + Sometimes the webContents doesn't get focus when window is shown, so we have + to force focusing on webContents in this case. The safest way is to focus it + when we first start to load URL, if we do it earlier it won't have effect, + if we do it later we might move focus in the page. + Though this hack is only needed on OS X when the app is launched from + Finder, we still do it on all platforms in case of other bugs we don't know. + */ + this.webContents.once('load-url', function() { + return this.focus(); + }); + + /* Redirect focus/blur event to app instance too. */ + this.on('blur', (function(_this) { + return function(event) { + return app.emit('browser-window-blur', event, _this); + }; + })(this)); + this.on('focus', (function(_this) { + return function(event) { + return app.emit('browser-window-focus', event, _this); + }; + })(this)); + + /* Notify the creation of the window. */ + app.emit('browser-window-created', {}, this); + + /* Be compatible with old APIs. */ + this.webContents.on('devtools-focused', (function(_this) { + return function() { + return _this.emit('devtools-focused'); + }; + })(this)); + this.webContents.on('devtools-opened', (function(_this) { + return function() { + return _this.emit('devtools-opened'); + }; + })(this)); + this.webContents.on('devtools-closed', (function(_this) { + return function() { + return _this.emit('devtools-closed'); + }; + })(this)); + return Object.defineProperty(this, 'devToolsWebContents', { + enumerable: true, + configurable: false, + get: function() { + return this.webContents.devToolsWebContents; + } + }); +}; + +BrowserWindow.getFocusedWindow = function() { + var i, len, window, windows; + windows = BrowserWindow.getAllWindows(); + for (i = 0, len = windows.length; i < len; i++) { + window = windows[i]; + if (window.isFocused()) { + return window; + } + } + return null; +}; + +BrowserWindow.fromWebContents = function(webContents) { + var i, len, ref1, window, windows; + windows = BrowserWindow.getAllWindows(); + for (i = 0, len = windows.length; i < len; i++) { + window = windows[i]; + if ((ref1 = window.webContents) != null ? ref1.equal(webContents) : void 0) { + return window; + } + } +}; + +BrowserWindow.fromDevToolsWebContents = function(webContents) { + var i, len, ref1, window, windows; + windows = BrowserWindow.getAllWindows(); + for (i = 0, len = windows.length; i < len; i++) { + window = windows[i]; + if ((ref1 = window.devToolsWebContents) != null ? ref1.equal(webContents) : void 0) { + return window; + } + } +}; + + +/* Helpers. */ + +BrowserWindow.prototype.loadURL = function() { + return this.webContents.loadURL.apply(this.webContents, arguments); +}; + +BrowserWindow.prototype.getURL = function() { + return this.webContents.getURL(); +}; + +BrowserWindow.prototype.reload = function() { + return this.webContents.reload.apply(this.webContents, arguments); +}; + +BrowserWindow.prototype.send = function() { + return this.webContents.send.apply(this.webContents, arguments); +}; + +BrowserWindow.prototype.openDevTools = function() { + return this.webContents.openDevTools.apply(this.webContents, arguments); +}; + +BrowserWindow.prototype.closeDevTools = function() { + return this.webContents.closeDevTools(); +}; + +BrowserWindow.prototype.isDevToolsOpened = function() { + return this.webContents.isDevToolsOpened(); +}; + +BrowserWindow.prototype.isDevToolsFocused = function() { + return this.webContents.isDevToolsFocused(); +}; + +BrowserWindow.prototype.toggleDevTools = function() { + return this.webContents.toggleDevTools(); +}; + +BrowserWindow.prototype.inspectElement = function() { + return this.webContents.inspectElement.apply(this.webContents, arguments); +}; + +BrowserWindow.prototype.inspectServiceWorker = function() { + return this.webContents.inspectServiceWorker(); +}; + + +/* Deprecated. */ + +deprecate.member(BrowserWindow, 'undo', 'webContents'); + +deprecate.member(BrowserWindow, 'redo', 'webContents'); + +deprecate.member(BrowserWindow, 'cut', 'webContents'); + +deprecate.member(BrowserWindow, 'copy', 'webContents'); + +deprecate.member(BrowserWindow, 'paste', 'webContents'); + +deprecate.member(BrowserWindow, 'selectAll', 'webContents'); + +deprecate.member(BrowserWindow, 'reloadIgnoringCache', 'webContents'); + +deprecate.member(BrowserWindow, 'isLoading', 'webContents'); + +deprecate.member(BrowserWindow, 'isWaitingForResponse', 'webContents'); + +deprecate.member(BrowserWindow, 'stop', 'webContents'); + +deprecate.member(BrowserWindow, 'isCrashed', 'webContents'); + +deprecate.member(BrowserWindow, 'print', 'webContents'); + +deprecate.member(BrowserWindow, 'printToPDF', 'webContents'); + +deprecate.rename(BrowserWindow, 'restart', 'reload'); + +deprecate.rename(BrowserWindow, 'loadUrl', 'loadURL'); + +deprecate.rename(BrowserWindow, 'getUrl', 'getURL'); + +BrowserWindow.prototype.executeJavaScriptInDevTools = deprecate('executeJavaScriptInDevTools', 'devToolsWebContents.executeJavaScript', function(code) { + var ref1; + return (ref1 = this.devToolsWebContents) != null ? ref1.executeJavaScript(code) : void 0; +}); + +BrowserWindow.prototype.getPageTitle = deprecate('getPageTitle', 'webContents.getTitle', function() { + var ref1; + return (ref1 = this.webContents) != null ? ref1.getTitle() : void 0; +}); + +module.exports = BrowserWindow; diff --git a/atom/browser/api/lib/content-tracing.coffee b/atom/browser/api/lib/content-tracing.coffee deleted file mode 100644 index 08cd36e4aa59..000000000000 --- a/atom/browser/api/lib/content-tracing.coffee +++ /dev/null @@ -1 +0,0 @@ -module.exports = process.atomBinding 'content_tracing' diff --git a/atom/browser/api/lib/content-tracing.js b/atom/browser/api/lib/content-tracing.js new file mode 100644 index 000000000000..b00c5666df59 --- /dev/null +++ b/atom/browser/api/lib/content-tracing.js @@ -0,0 +1 @@ +module.exports = process.atomBinding('content_tracing'); diff --git a/atom/browser/api/lib/dialog.coffee b/atom/browser/api/lib/dialog.coffee deleted file mode 100644 index 98e613bbaab6..000000000000 --- a/atom/browser/api/lib/dialog.coffee +++ /dev/null @@ -1,127 +0,0 @@ -{app, BrowserWindow} = require 'electron' - -binding = process.atomBinding 'dialog' -v8Util = process.atomBinding 'v8_util' - -fileDialogProperties = - openFile: 1 << 0 - openDirectory: 1 << 1 - multiSelections: 1 << 2 - createDirectory: 1 << 3 - -messageBoxTypes = ['none', 'info', 'warning', 'error', 'question'] - -messageBoxOptions = - noLink: 1 << 0 - -parseArgs = (window, options, callback) -> - unless window is null or window?.constructor is BrowserWindow - ### Shift. ### - callback = options - options = window - window = null - if not callback? and typeof options is 'function' - ### Shift. ### - callback = options - options = null - [window, options, callback] - -checkAppInitialized = -> - throw new Error('dialog module can only be used after app is ready') unless app.isReady() - -module.exports = - showOpenDialog: (args...) -> - checkAppInitialized() - [window, options, callback] = parseArgs args... - - options ?= title: 'Open', properties: ['openFile'] - options.properties ?= ['openFile'] - throw new TypeError('Properties need to be array') unless Array.isArray options.properties - - properties = 0 - for prop, value of fileDialogProperties - properties |= value if prop in options.properties - - options.title ?= '' - options.defaultPath ?= '' - options.filters ?= [] - - wrappedCallback = - if typeof callback is 'function' - (success, result) -> callback(if success then result) - else - null - - binding.showOpenDialog String(options.title), - String(options.defaultPath), - options.filters - properties, - window, - wrappedCallback - - showSaveDialog: (args...) -> - checkAppInitialized() - [window, options, callback] = parseArgs args... - - options ?= title: 'Save' - options.title ?= '' - options.defaultPath ?= '' - options.filters ?= [] - - wrappedCallback = - if typeof callback is 'function' - (success, result) -> callback(if success then result) - else - null - - binding.showSaveDialog String(options.title), - String(options.defaultPath), - options.filters - window, - wrappedCallback - - showMessageBox: (args...) -> - checkAppInitialized() - [window, options, callback] = parseArgs args... - - options ?= type: 'none' - options.type ?= 'none' - messageBoxType = messageBoxTypes.indexOf options.type - throw new TypeError('Invalid message box type') unless messageBoxType > -1 - - throw new TypeError('Buttons need to be array') unless Array.isArray options.buttons - - options.title ?= '' - options.message ?= '' - options.detail ?= '' - options.icon ?= null - options.defaultId ?= -1 - - ### Choose a default button to get selected when dialog is cancelled. ### - unless options.cancelId? - options.cancelId = 0 - for text, i in options.buttons - if text.toLowerCase() in ['cancel', 'no'] - options.cancelId = i - break - - flags = if options.noLink then messageBoxOptions.noLink else 0 - - binding.showMessageBox messageBoxType, - options.buttons, - options.defaultId, - options.cancelId, - flags, - options.title, - options.message, - options.detail, - options.icon, - window, - callback - - showErrorBox: (args...) -> - binding.showErrorBox args... - -### Mark standard asynchronous functions. ### -for api in ['showMessageBox', 'showOpenDialog', 'showSaveDialog'] - v8Util.setHiddenValue module.exports[api], 'asynchronous', true diff --git a/atom/browser/api/lib/dialog.js b/atom/browser/api/lib/dialog.js new file mode 100644 index 000000000000..5210e5d20a25 --- /dev/null +++ b/atom/browser/api/lib/dialog.js @@ -0,0 +1,175 @@ +var BrowserWindow, api, app, binding, checkAppInitialized, fileDialogProperties, j, len, messageBoxOptions, messageBoxTypes, parseArgs, ref, ref1, v8Util, + slice = [].slice, + indexOf = [].indexOf || function(item) { for (var i = 0, l = this.length; i < l; i++) { if (i in this && this[i] === item) return i; } return -1; }; + +ref = require('electron'), app = ref.app, BrowserWindow = ref.BrowserWindow; + +binding = process.atomBinding('dialog'); + +v8Util = process.atomBinding('v8_util'); + +fileDialogProperties = { + openFile: 1 << 0, + openDirectory: 1 << 1, + multiSelections: 1 << 2, + createDirectory: 1 << 3 +}; + +messageBoxTypes = ['none', 'info', 'warning', 'error', 'question']; + +messageBoxOptions = { + noLink: 1 << 0 +}; + +parseArgs = function(window, options, callback) { + if (!(window === null || (window != null ? window.constructor : void 0) === BrowserWindow)) { + + /* Shift. */ + callback = options; + options = window; + window = null; + } + if ((callback == null) && typeof options === 'function') { + + /* Shift. */ + callback = options; + options = null; + } + return [window, options, callback]; +}; + +checkAppInitialized = function() { + if (!app.isReady()) { + throw new Error('dialog module can only be used after app is ready'); + } +}; + +module.exports = { + showOpenDialog: function() { + var args, callback, options, prop, properties, ref1, value, window, wrappedCallback; + args = 1 <= arguments.length ? slice.call(arguments, 0) : []; + checkAppInitialized(); + ref1 = parseArgs.apply(null, args), window = ref1[0], options = ref1[1], callback = ref1[2]; + if (options == null) { + options = { + title: 'Open', + properties: ['openFile'] + }; + } + if (options.properties == null) { + options.properties = ['openFile']; + } + if (!Array.isArray(options.properties)) { + throw new TypeError('Properties need to be array'); + } + properties = 0; + for (prop in fileDialogProperties) { + value = fileDialogProperties[prop]; + if (indexOf.call(options.properties, prop) >= 0) { + properties |= value; + } + } + if (options.title == null) { + options.title = ''; + } + if (options.defaultPath == null) { + options.defaultPath = ''; + } + if (options.filters == null) { + options.filters = []; + } + wrappedCallback = typeof callback === 'function' ? function(success, result) { + return callback(success ? result : void 0); + } : null; + return binding.showOpenDialog(String(options.title), String(options.defaultPath), options.filters, properties, window, wrappedCallback); + }, + showSaveDialog: function() { + var args, callback, options, ref1, window, wrappedCallback; + args = 1 <= arguments.length ? slice.call(arguments, 0) : []; + checkAppInitialized(); + ref1 = parseArgs.apply(null, args), window = ref1[0], options = ref1[1], callback = ref1[2]; + if (options == null) { + options = { + title: 'Save' + }; + } + if (options.title == null) { + options.title = ''; + } + if (options.defaultPath == null) { + options.defaultPath = ''; + } + if (options.filters == null) { + options.filters = []; + } + wrappedCallback = typeof callback === 'function' ? function(success, result) { + return callback(success ? result : void 0); + } : null; + return binding.showSaveDialog(String(options.title), String(options.defaultPath), options.filters, window, wrappedCallback); + }, + showMessageBox: function() { + var args, callback, flags, i, j, len, messageBoxType, options, ref1, ref2, ref3, text, window; + args = 1 <= arguments.length ? slice.call(arguments, 0) : []; + checkAppInitialized(); + ref1 = parseArgs.apply(null, args), window = ref1[0], options = ref1[1], callback = ref1[2]; + if (options == null) { + options = { + type: 'none' + }; + } + if (options.type == null) { + options.type = 'none'; + } + messageBoxType = messageBoxTypes.indexOf(options.type); + if (!(messageBoxType > -1)) { + throw new TypeError('Invalid message box type'); + } + if (!Array.isArray(options.buttons)) { + throw new TypeError('Buttons need to be array'); + } + if (options.title == null) { + options.title = ''; + } + if (options.message == null) { + options.message = ''; + } + if (options.detail == null) { + options.detail = ''; + } + if (options.icon == null) { + options.icon = null; + } + if (options.defaultId == null) { + options.defaultId = -1; + } + + /* Choose a default button to get selected when dialog is cancelled. */ + if (options.cancelId == null) { + options.cancelId = 0; + ref2 = options.buttons; + for (i = j = 0, len = ref2.length; j < len; i = ++j) { + text = ref2[i]; + if ((ref3 = text.toLowerCase()) === 'cancel' || ref3 === 'no') { + options.cancelId = i; + break; + } + } + } + flags = options.noLink ? messageBoxOptions.noLink : 0; + return binding.showMessageBox(messageBoxType, options.buttons, options.defaultId, options.cancelId, flags, options.title, options.message, options.detail, options.icon, window, callback); + }, + showErrorBox: function() { + var args; + args = 1 <= arguments.length ? slice.call(arguments, 0) : []; + return binding.showErrorBox.apply(binding, args); + } +}; + + +/* Mark standard asynchronous functions. */ + +ref1 = ['showMessageBox', 'showOpenDialog', 'showSaveDialog']; +for (j = 0, len = ref1.length; j < len; j++) { + api = ref1[j]; + v8Util.setHiddenValue(module.exports[api], 'asynchronous', true); +} diff --git a/atom/browser/api/lib/exports/electron.coffee b/atom/browser/api/lib/exports/electron.coffee deleted file mode 100644 index 48ff349a29a3..000000000000 --- a/atom/browser/api/lib/exports/electron.coffee +++ /dev/null @@ -1,57 +0,0 @@ -common = require '../../../../common/api/lib/exports/electron' - -### Import common modules. ### -common.defineProperties exports - -Object.defineProperties exports, - ### Browser side modules, please sort with alphabet order. ### - app: - enumerable: true - get: -> require '../app' - autoUpdater: - enumerable: true - get: -> require '../auto-updater' - BrowserWindow: - enumerable: true - get: -> require '../browser-window' - contentTracing: - enumerable: true - get: -> require '../content-tracing' - dialog: - enumerable: true - get: -> require '../dialog' - ipcMain: - enumerable: true - get: -> require '../ipc-main' - globalShortcut: - enumerable: true - get: -> require '../global-shortcut' - Menu: - enumerable: true - get: -> require '../menu' - MenuItem: - enumerable: true - get: -> require '../menu-item' - powerMonitor: - enumerable: true - get: -> require '../power-monitor' - powerSaveBlocker: - enumerable: true - get: -> require '../power-save-blocker' - protocol: - enumerable: true - get: -> require '../protocol' - screen: - enumerable: true - get: -> require '../screen' - session: - enumerable: true - get: -> require '../session' - Tray: - enumerable: true - get: -> require '../tray' - ### The internal modules, invisible unless you know their names. ### - NavigationController: - get: -> require '../navigation-controller' - webContents: - get: -> require '../web-contents' diff --git a/atom/browser/api/lib/exports/electron.js b/atom/browser/api/lib/exports/electron.js new file mode 100644 index 000000000000..5ded35a401fb --- /dev/null +++ b/atom/browser/api/lib/exports/electron.js @@ -0,0 +1,115 @@ +var common; + +common = require('../../../../common/api/lib/exports/electron'); + + +/* Import common modules. */ + +common.defineProperties(exports); + +Object.defineProperties(exports, { + + /* Browser side modules, please sort with alphabet order. */ + app: { + enumerable: true, + get: function() { + return require('../app'); + } + }, + autoUpdater: { + enumerable: true, + get: function() { + return require('../auto-updater'); + } + }, + BrowserWindow: { + enumerable: true, + get: function() { + return require('../browser-window'); + } + }, + contentTracing: { + enumerable: true, + get: function() { + return require('../content-tracing'); + } + }, + dialog: { + enumerable: true, + get: function() { + return require('../dialog'); + } + }, + ipcMain: { + enumerable: true, + get: function() { + return require('../ipc-main'); + } + }, + globalShortcut: { + enumerable: true, + get: function() { + return require('../global-shortcut'); + } + }, + Menu: { + enumerable: true, + get: function() { + return require('../menu'); + } + }, + MenuItem: { + enumerable: true, + get: function() { + return require('../menu-item'); + } + }, + powerMonitor: { + enumerable: true, + get: function() { + return require('../power-monitor'); + } + }, + powerSaveBlocker: { + enumerable: true, + get: function() { + return require('../power-save-blocker'); + } + }, + protocol: { + enumerable: true, + get: function() { + return require('../protocol'); + } + }, + screen: { + enumerable: true, + get: function() { + return require('../screen'); + } + }, + session: { + enumerable: true, + get: function() { + return require('../session'); + } + }, + Tray: { + enumerable: true, + get: function() { + return require('../tray'); + } + }, + + /* The internal modules, invisible unless you know their names. */ + NavigationController: { + get: function() { + return require('../navigation-controller'); + } + }, + webContents: { + get: function() { + return require('../web-contents'); + } + } +}); diff --git a/atom/browser/api/lib/global-shortcut.coffee b/atom/browser/api/lib/global-shortcut.coffee deleted file mode 100644 index 56c3e128767e..000000000000 --- a/atom/browser/api/lib/global-shortcut.coffee +++ /dev/null @@ -1,3 +0,0 @@ -{globalShortcut} = process.atomBinding 'global_shortcut' - -module.exports = globalShortcut diff --git a/atom/browser/api/lib/global-shortcut.js b/atom/browser/api/lib/global-shortcut.js new file mode 100644 index 000000000000..daca6c232796 --- /dev/null +++ b/atom/browser/api/lib/global-shortcut.js @@ -0,0 +1,5 @@ +var globalShortcut; + +globalShortcut = process.atomBinding('global_shortcut').globalShortcut; + +module.exports = globalShortcut; diff --git a/atom/browser/api/lib/ipc-main.coffee b/atom/browser/api/lib/ipc-main.coffee deleted file mode 100644 index 8021544479d2..000000000000 --- a/atom/browser/api/lib/ipc-main.coffee +++ /dev/null @@ -1,3 +0,0 @@ -{EventEmitter} = require 'events' - -module.exports = new EventEmitter diff --git a/atom/browser/api/lib/ipc-main.js b/atom/browser/api/lib/ipc-main.js new file mode 100644 index 000000000000..bd4300d2c244 --- /dev/null +++ b/atom/browser/api/lib/ipc-main.js @@ -0,0 +1,5 @@ +var EventEmitter; + +EventEmitter = require('events').EventEmitter; + +module.exports = new EventEmitter; diff --git a/atom/browser/api/lib/ipc.coffee b/atom/browser/api/lib/ipc.coffee deleted file mode 100644 index 348c7d0cd459..000000000000 --- a/atom/browser/api/lib/ipc.coffee +++ /dev/null @@ -1,6 +0,0 @@ -{deprecate, ipcMain} = require 'electron' - -### This module is deprecated, we mirror everything from ipcMain. ### -deprecate.warn 'ipc module', 'require("electron").ipcMain' - -module.exports = ipcMain diff --git a/atom/browser/api/lib/ipc.js b/atom/browser/api/lib/ipc.js new file mode 100644 index 000000000000..162e56bea2c1 --- /dev/null +++ b/atom/browser/api/lib/ipc.js @@ -0,0 +1,10 @@ +var deprecate, ipcMain, ref; + +ref = require('electron'), deprecate = ref.deprecate, ipcMain = ref.ipcMain; + + +/* This module is deprecated, we mirror everything from ipcMain. */ + +deprecate.warn('ipc module', 'require("electron").ipcMain'); + +module.exports = ipcMain; diff --git a/atom/browser/api/lib/menu-item.coffee b/atom/browser/api/lib/menu-item.coffee deleted file mode 100644 index ab225d30095c..000000000000 --- a/atom/browser/api/lib/menu-item.coffee +++ /dev/null @@ -1,73 +0,0 @@ -v8Util = process.atomBinding 'v8_util' - -nextCommandId = 0 - -### Maps role to methods of webContents ### -rolesMap = - undo: 'undo' - redo: 'redo' - cut: 'cut' - copy: 'copy' - paste: 'paste' - selectall: 'selectAll' - minimize: 'minimize' - close: 'close' - -### Maps methods that should be called directly on the BrowserWindow instance ### -methodInBrowserWindow = - minimize: true - close: true - -class MenuItem - @types = ['normal', 'separator', 'submenu', 'checkbox', 'radio'] - - constructor: (options) -> - {Menu} = require 'electron' - - {click, @selector, @type, @role, @label, @sublabel, @accelerator, @icon, @enabled, @visible, @checked, @submenu} = options - - if @submenu? and @submenu.constructor isnt Menu - @submenu = Menu.buildFromTemplate @submenu - @type = 'submenu' if not @type? and @submenu? - throw new Error('Invalid submenu') if @type is 'submenu' and @submenu?.constructor isnt Menu - - @overrideReadOnlyProperty 'type', 'normal' - @overrideReadOnlyProperty 'role' - @overrideReadOnlyProperty 'accelerator' - @overrideReadOnlyProperty 'icon' - @overrideReadOnlyProperty 'submenu' - @overrideProperty 'label', '' - @overrideProperty 'sublabel', '' - @overrideProperty 'enabled', true - @overrideProperty 'visible', true - @overrideProperty 'checked', false - - throw new Error("Unknown menu type #{@type}") if MenuItem.types.indexOf(@type) is -1 - - @commandId = ++nextCommandId - @click = (focusedWindow) => - ### Manually flip the checked flags when clicked. ### - @checked = !@checked if @type in ['checkbox', 'radio'] - - if @role and rolesMap[@role] and process.platform isnt 'darwin' and focusedWindow? - methodName = rolesMap[@role] - if methodInBrowserWindow[methodName] - focusedWindow[methodName]() - else - focusedWindow.webContents?[methodName]() - else if typeof click is 'function' - click this, focusedWindow - else if typeof @selector is 'string' - Menu.sendActionToFirstResponder @selector - - overrideProperty: (name, defaultValue=null) -> - this[name] ?= defaultValue - - overrideReadOnlyProperty: (name, defaultValue=null) -> - this[name] ?= defaultValue - Object.defineProperty this, name, - enumerable: true - writable: false - value: this[name] - -module.exports = MenuItem diff --git a/atom/browser/api/lib/menu-item.js b/atom/browser/api/lib/menu-item.js new file mode 100644 index 000000000000..22d6c251f5e9 --- /dev/null +++ b/atom/browser/api/lib/menu-item.js @@ -0,0 +1,108 @@ +var MenuItem, methodInBrowserWindow, nextCommandId, rolesMap, v8Util; + +v8Util = process.atomBinding('v8_util'); + +nextCommandId = 0; + + +/* Maps role to methods of webContents */ + +rolesMap = { + undo: 'undo', + redo: 'redo', + cut: 'cut', + copy: 'copy', + paste: 'paste', + selectall: 'selectAll', + minimize: 'minimize', + close: 'close' +}; + + +/* Maps methods that should be called directly on the BrowserWindow instance */ + +methodInBrowserWindow = { + minimize: true, + close: true +}; + +MenuItem = (function() { + MenuItem.types = ['normal', 'separator', 'submenu', 'checkbox', 'radio']; + + function MenuItem(options) { + var Menu, click, ref; + Menu = require('electron').Menu; + click = options.click, this.selector = options.selector, this.type = options.type, this.role = options.role, this.label = options.label, this.sublabel = options.sublabel, this.accelerator = options.accelerator, this.icon = options.icon, this.enabled = options.enabled, this.visible = options.visible, this.checked = options.checked, this.submenu = options.submenu; + if ((this.submenu != null) && this.submenu.constructor !== Menu) { + this.submenu = Menu.buildFromTemplate(this.submenu); + } + if ((this.type == null) && (this.submenu != null)) { + this.type = 'submenu'; + } + if (this.type === 'submenu' && ((ref = this.submenu) != null ? ref.constructor : void 0) !== Menu) { + throw new Error('Invalid submenu'); + } + this.overrideReadOnlyProperty('type', 'normal'); + this.overrideReadOnlyProperty('role'); + this.overrideReadOnlyProperty('accelerator'); + this.overrideReadOnlyProperty('icon'); + this.overrideReadOnlyProperty('submenu'); + this.overrideProperty('label', ''); + this.overrideProperty('sublabel', ''); + this.overrideProperty('enabled', true); + this.overrideProperty('visible', true); + this.overrideProperty('checked', false); + if (MenuItem.types.indexOf(this.type) === -1) { + throw new Error("Unknown menu type " + this.type); + } + this.commandId = ++nextCommandId; + this.click = (function(_this) { + return function(focusedWindow) { + + /* Manually flip the checked flags when clicked. */ + var methodName, ref1, ref2; + if ((ref1 = _this.type) === 'checkbox' || ref1 === 'radio') { + _this.checked = !_this.checked; + } + if (_this.role && rolesMap[_this.role] && process.platform !== 'darwin' && (focusedWindow != null)) { + methodName = rolesMap[_this.role]; + if (methodInBrowserWindow[methodName]) { + return focusedWindow[methodName](); + } else { + return (ref2 = focusedWindow.webContents) != null ? ref2[methodName]() : void 0; + } + } else if (typeof click === 'function') { + return click(_this, focusedWindow); + } else if (typeof _this.selector === 'string') { + return Menu.sendActionToFirstResponder(_this.selector); + } + }; + })(this); + } + + MenuItem.prototype.overrideProperty = function(name, defaultValue) { + if (defaultValue == null) { + defaultValue = null; + } + return this[name] != null ? this[name] : this[name] = defaultValue; + }; + + MenuItem.prototype.overrideReadOnlyProperty = function(name, defaultValue) { + if (defaultValue == null) { + defaultValue = null; + } + if (this[name] == null) { + this[name] = defaultValue; + } + return Object.defineProperty(this, name, { + enumerable: true, + writable: false, + value: this[name] + }); + }; + + return MenuItem; + +})(); + +module.exports = MenuItem; diff --git a/atom/browser/api/lib/menu.coffee b/atom/browser/api/lib/menu.coffee deleted file mode 100644 index d987f304262c..000000000000 --- a/atom/browser/api/lib/menu.coffee +++ /dev/null @@ -1,179 +0,0 @@ -{BrowserWindow, MenuItem} = require 'electron' -{EventEmitter} = require 'events' - -v8Util = process.atomBinding 'v8_util' -bindings = process.atomBinding 'menu' - -### Automatically generated radio menu item's group id. ### -nextGroupId = 0 - -### Search between seperators to find a radio menu item and return its group id, ### -### otherwise generate a group id. ### -generateGroupId = (items, pos) -> - if pos > 0 - for i in [pos - 1..0] - item = items[i] - return item.groupId if item.type is 'radio' - break if item.type is 'separator' - else if pos < items.length - for i in [pos..items.length - 1] - item = items[i] - return item.groupId if item.type is 'radio' - break if item.type is 'separator' - ++nextGroupId - -### Returns the index of item according to |id|. ### -indexOfItemById = (items, id) -> - return i for item, i in items when item.id is id - -1 - -### Returns the index of where to insert the item according to |position|. ### -indexToInsertByPosition = (items, position) -> - return items.length unless position - - [query, id] = position.split '=' - insertIndex = indexOfItemById items, id - if insertIndex is -1 and query isnt 'endof' - console.warn "Item with id '#{id}' is not found" - return items.length - - switch query - when 'after' - insertIndex++ - when 'endof' - ### If the |id| doesn't exist, then create a new group with the |id|. ### - if insertIndex is -1 - items.push id: id, type: 'separator' - insertIndex = items.length - 1 - - ### Find the end of the group. ### - insertIndex++ - while insertIndex < items.length and items[insertIndex].type isnt 'separator' - insertIndex++ - - insertIndex - -Menu = bindings.Menu -Menu::__proto__ = EventEmitter.prototype - -Menu::_init = -> - @commandsMap = {} - @groupsMap = {} - @items = [] - @delegate = - isCommandIdChecked: (commandId) => @commandsMap[commandId]?.checked - isCommandIdEnabled: (commandId) => @commandsMap[commandId]?.enabled - isCommandIdVisible: (commandId) => @commandsMap[commandId]?.visible - getAcceleratorForCommandId: (commandId) => @commandsMap[commandId]?.accelerator - getIconForCommandId: (commandId) => @commandsMap[commandId]?.icon - executeCommand: (commandId) => - @commandsMap[commandId]?.click BrowserWindow.getFocusedWindow() - menuWillShow: => - ### Make sure radio groups have at least one menu item seleted. ### - for id, group of @groupsMap - checked = false - for radioItem in group when radioItem.checked - checked = true - break - v8Util.setHiddenValue group[0], 'checked', true unless checked - -Menu::popup = (window, x, y) -> - unless window?.constructor is BrowserWindow - ### Shift. ### - y = x - x = window - window = BrowserWindow.getFocusedWindow() - if x? and y? - @_popupAt(window, x, y) - else - @_popup window - -Menu::append = (item) -> - @insert @getItemCount(), item - -Menu::insert = (pos, item) -> - throw new TypeError('Invalid item') unless item?.constructor is MenuItem - - switch item.type - when 'normal' then @insertItem pos, item.commandId, item.label - when 'checkbox' then @insertCheckItem pos, item.commandId, item.label - when 'separator' then @insertSeparator pos - when 'submenu' then @insertSubMenu pos, item.commandId, item.label, item.submenu - when 'radio' - ### Grouping radio menu items. ### - item.overrideReadOnlyProperty 'groupId', generateGroupId(@items, pos) - @groupsMap[item.groupId] ?= [] - @groupsMap[item.groupId].push item - - ### Setting a radio menu item should flip other items in the group. ### - v8Util.setHiddenValue item, 'checked', item.checked - Object.defineProperty item, 'checked', - enumerable: true - get: -> v8Util.getHiddenValue item, 'checked' - set: (val) => - for otherItem in @groupsMap[item.groupId] when otherItem isnt item - v8Util.setHiddenValue otherItem, 'checked', false - v8Util.setHiddenValue item, 'checked', true - - @insertRadioItem pos, item.commandId, item.label, item.groupId - - @setSublabel pos, item.sublabel if item.sublabel? - @setIcon pos, item.icon if item.icon? - @setRole pos, item.role if item.role? - - ### Make menu accessable to items. ### - item.overrideReadOnlyProperty 'menu', this - - ### Remember the items. ### - @items.splice pos, 0, item - @commandsMap[item.commandId] = item - -### Force menuWillShow to be called ### -Menu::_callMenuWillShow = -> - @delegate?.menuWillShow() - item.submenu._callMenuWillShow() for item in @items when item.submenu? - -applicationMenu = null -Menu.setApplicationMenu = (menu) -> - throw new TypeError('Invalid menu') unless menu is null or menu.constructor is Menu - ### Keep a reference. ### - applicationMenu = menu - - if process.platform is 'darwin' - return if menu is null - menu._callMenuWillShow() - bindings.setApplicationMenu menu - else - windows = BrowserWindow.getAllWindows() - w.setMenu menu for w in windows - -Menu.getApplicationMenu = -> applicationMenu - -Menu.sendActionToFirstResponder = bindings.sendActionToFirstResponder - -Menu.buildFromTemplate = (template) -> - throw new TypeError('Invalid template for Menu') unless Array.isArray template - - positionedTemplate = [] - insertIndex = 0 - - for item in template - if item.position - insertIndex = indexToInsertByPosition positionedTemplate, item.position - else - ### If no |position| is specified, insert after last item. ### - insertIndex++ - positionedTemplate.splice insertIndex, 0, item - - menu = new Menu - - for item in positionedTemplate - throw new TypeError('Invalid template for MenuItem') unless typeof item is 'object' - - menuItem = new MenuItem(item) - menuItem[key] ?= value for key, value of item - menu.append menuItem - - menu - -module.exports = Menu diff --git a/atom/browser/api/lib/menu.js b/atom/browser/api/lib/menu.js new file mode 100644 index 000000000000..6ca5c9adde43 --- /dev/null +++ b/atom/browser/api/lib/menu.js @@ -0,0 +1,350 @@ +var BrowserWindow, EventEmitter, Menu, MenuItem, applicationMenu, bindings, generateGroupId, indexOfItemById, indexToInsertByPosition, nextGroupId, ref, v8Util; + +ref = require('electron'), BrowserWindow = ref.BrowserWindow, MenuItem = ref.MenuItem; + +EventEmitter = require('events').EventEmitter; + +v8Util = process.atomBinding('v8_util'); + +bindings = process.atomBinding('menu'); + + +/* Automatically generated radio menu item's group id. */ + +nextGroupId = 0; + + +/* Search between seperators to find a radio menu item and return its group id, */ + + +/* otherwise generate a group id. */ + +generateGroupId = function(items, pos) { + var i, item, j, k, ref1, ref2, ref3; + if (pos > 0) { + for (i = j = ref1 = pos - 1; ref1 <= 0 ? j <= 0 : j >= 0; i = ref1 <= 0 ? ++j : --j) { + item = items[i]; + if (item.type === 'radio') { + return item.groupId; + } + if (item.type === 'separator') { + break; + } + } + } else if (pos < items.length) { + for (i = k = ref2 = pos, ref3 = items.length - 1; ref2 <= ref3 ? k <= ref3 : k >= ref3; i = ref2 <= ref3 ? ++k : --k) { + item = items[i]; + if (item.type === 'radio') { + return item.groupId; + } + if (item.type === 'separator') { + break; + } + } + } + return ++nextGroupId; +}; + + +/* Returns the index of item according to |id|. */ + +indexOfItemById = function(items, id) { + var i, item, j, len; + for (i = j = 0, len = items.length; j < len; i = ++j) { + item = items[i]; + if (item.id === id) { + return i; + } + } + return -1; +}; + + +/* Returns the index of where to insert the item according to |position|. */ + +indexToInsertByPosition = function(items, position) { + var id, insertIndex, query, ref1; + if (!position) { + return items.length; + } + ref1 = position.split('='), query = ref1[0], id = ref1[1]; + insertIndex = indexOfItemById(items, id); + if (insertIndex === -1 && query !== 'endof') { + console.warn("Item with id '" + id + "' is not found"); + return items.length; + } + switch (query) { + case 'after': + insertIndex++; + break; + case 'endof': + + /* If the |id| doesn't exist, then create a new group with the |id|. */ + if (insertIndex === -1) { + items.push({ + id: id, + type: 'separator' + }); + insertIndex = items.length - 1; + } + + /* Find the end of the group. */ + insertIndex++; + while (insertIndex < items.length && items[insertIndex].type !== 'separator') { + insertIndex++; + } + } + return insertIndex; +}; + +Menu = bindings.Menu; + +Menu.prototype.__proto__ = EventEmitter.prototype; + +Menu.prototype._init = function() { + this.commandsMap = {}; + this.groupsMap = {}; + this.items = []; + return this.delegate = { + isCommandIdChecked: (function(_this) { + return function(commandId) { + var ref1; + return (ref1 = _this.commandsMap[commandId]) != null ? ref1.checked : void 0; + }; + })(this), + isCommandIdEnabled: (function(_this) { + return function(commandId) { + var ref1; + return (ref1 = _this.commandsMap[commandId]) != null ? ref1.enabled : void 0; + }; + })(this), + isCommandIdVisible: (function(_this) { + return function(commandId) { + var ref1; + return (ref1 = _this.commandsMap[commandId]) != null ? ref1.visible : void 0; + }; + })(this), + getAcceleratorForCommandId: (function(_this) { + return function(commandId) { + var ref1; + return (ref1 = _this.commandsMap[commandId]) != null ? ref1.accelerator : void 0; + }; + })(this), + getIconForCommandId: (function(_this) { + return function(commandId) { + var ref1; + return (ref1 = _this.commandsMap[commandId]) != null ? ref1.icon : void 0; + }; + })(this), + executeCommand: (function(_this) { + return function(commandId) { + var ref1; + return (ref1 = _this.commandsMap[commandId]) != null ? ref1.click(BrowserWindow.getFocusedWindow()) : void 0; + }; + })(this), + menuWillShow: (function(_this) { + return function() { + + /* Make sure radio groups have at least one menu item seleted. */ + var checked, group, id, j, len, radioItem, ref1, results; + ref1 = _this.groupsMap; + results = []; + for (id in ref1) { + group = ref1[id]; + checked = false; + for (j = 0, len = group.length; j < len; j++) { + radioItem = group[j]; + if (!radioItem.checked) { + continue; + } + checked = true; + break; + } + if (!checked) { + results.push(v8Util.setHiddenValue(group[0], 'checked', true)); + } else { + results.push(void 0); + } + } + return results; + }; + })(this) + }; +}; + +Menu.prototype.popup = function(window, x, y) { + if ((window != null ? window.constructor : void 0) !== BrowserWindow) { + + /* Shift. */ + y = x; + x = window; + window = BrowserWindow.getFocusedWindow(); + } + if ((x != null) && (y != null)) { + return this._popupAt(window, x, y); + } else { + return this._popup(window); + } +}; + +Menu.prototype.append = function(item) { + return this.insert(this.getItemCount(), item); +}; + +Menu.prototype.insert = function(pos, item) { + var base, name; + if ((item != null ? item.constructor : void 0) !== MenuItem) { + throw new TypeError('Invalid item'); + } + switch (item.type) { + case 'normal': + this.insertItem(pos, item.commandId, item.label); + break; + case 'checkbox': + this.insertCheckItem(pos, item.commandId, item.label); + break; + case 'separator': + this.insertSeparator(pos); + break; + case 'submenu': + this.insertSubMenu(pos, item.commandId, item.label, item.submenu); + break; + case 'radio': + + /* Grouping radio menu items. */ + item.overrideReadOnlyProperty('groupId', generateGroupId(this.items, pos)); + if ((base = this.groupsMap)[name = item.groupId] == null) { + base[name] = []; + } + this.groupsMap[item.groupId].push(item); + + /* Setting a radio menu item should flip other items in the group. */ + v8Util.setHiddenValue(item, 'checked', item.checked); + Object.defineProperty(item, 'checked', { + enumerable: true, + get: function() { + return v8Util.getHiddenValue(item, 'checked'); + }, + set: (function(_this) { + return function(val) { + var j, len, otherItem, ref1; + ref1 = _this.groupsMap[item.groupId]; + for (j = 0, len = ref1.length; j < len; j++) { + otherItem = ref1[j]; + if (otherItem !== item) { + v8Util.setHiddenValue(otherItem, 'checked', false); + } + } + return v8Util.setHiddenValue(item, 'checked', true); + }; + })(this) + }); + this.insertRadioItem(pos, item.commandId, item.label, item.groupId); + } + if (item.sublabel != null) { + this.setSublabel(pos, item.sublabel); + } + if (item.icon != null) { + this.setIcon(pos, item.icon); + } + if (item.role != null) { + this.setRole(pos, item.role); + } + + /* Make menu accessable to items. */ + item.overrideReadOnlyProperty('menu', this); + + /* Remember the items. */ + this.items.splice(pos, 0, item); + return this.commandsMap[item.commandId] = item; +}; + + +/* Force menuWillShow to be called */ + +Menu.prototype._callMenuWillShow = function() { + var item, j, len, ref1, ref2, results; + if ((ref1 = this.delegate) != null) { + ref1.menuWillShow(); + } + ref2 = this.items; + results = []; + for (j = 0, len = ref2.length; j < len; j++) { + item = ref2[j]; + if (item.submenu != null) { + results.push(item.submenu._callMenuWillShow()); + } + } + return results; +}; + +applicationMenu = null; + +Menu.setApplicationMenu = function(menu) { + var j, len, results, w, windows; + if (!(menu === null || menu.constructor === Menu)) { + throw new TypeError('Invalid menu'); + } + + /* Keep a reference. */ + applicationMenu = menu; + if (process.platform === 'darwin') { + if (menu === null) { + return; + } + menu._callMenuWillShow(); + return bindings.setApplicationMenu(menu); + } else { + windows = BrowserWindow.getAllWindows(); + results = []; + for (j = 0, len = windows.length; j < len; j++) { + w = windows[j]; + results.push(w.setMenu(menu)); + } + return results; + } +}; + +Menu.getApplicationMenu = function() { + return applicationMenu; +}; + +Menu.sendActionToFirstResponder = bindings.sendActionToFirstResponder; + +Menu.buildFromTemplate = function(template) { + var insertIndex, item, j, k, key, len, len1, menu, menuItem, positionedTemplate, value; + if (!Array.isArray(template)) { + throw new TypeError('Invalid template for Menu'); + } + positionedTemplate = []; + insertIndex = 0; + for (j = 0, len = template.length; j < len; j++) { + item = template[j]; + if (item.position) { + insertIndex = indexToInsertByPosition(positionedTemplate, item.position); + } else { + + /* If no |position| is specified, insert after last item. */ + insertIndex++; + } + positionedTemplate.splice(insertIndex, 0, item); + } + menu = new Menu; + for (k = 0, len1 = positionedTemplate.length; k < len1; k++) { + item = positionedTemplate[k]; + if (typeof item !== 'object') { + throw new TypeError('Invalid template for MenuItem'); + } + menuItem = new MenuItem(item); + for (key in item) { + value = item[key]; + if (menuItem[key] == null) { + menuItem[key] = value; + } + } + menu.append(menuItem); + } + return menu; +}; + +module.exports = Menu; diff --git a/atom/browser/api/lib/navigation-controller.coffee b/atom/browser/api/lib/navigation-controller.coffee deleted file mode 100644 index 3c7ef2492a8b..000000000000 --- a/atom/browser/api/lib/navigation-controller.coffee +++ /dev/null @@ -1,127 +0,0 @@ -{ipcMain} = require 'electron' - -### The history operation in renderer is redirected to browser. ### -ipcMain.on 'ATOM_SHELL_NAVIGATION_CONTROLLER', (event, method, args...) -> - event.sender[method] args... - -ipcMain.on 'ATOM_SHELL_SYNC_NAVIGATION_CONTROLLER', (event, method, args...) -> - event.returnValue = event.sender[method] args... - -### - JavaScript implementation of Chromium's NavigationController. - Instead of relying on Chromium for history control, we compeletely do history - control on user land, and only rely on WebContents.loadURL for navigation. - This helps us avoid Chromium's various optimizations so we can ensure renderer - process is restarted everytime. -### -class NavigationController - constructor: (@webContents) -> - @clearHistory() - - ### webContents may have already navigated to a page. ### - if @webContents._getURL() - @currentIndex++ - @history.push @webContents._getURL() - - @webContents.on 'navigation-entry-commited', (event, url, inPage, replaceEntry) => - if @inPageIndex > -1 and not inPage - ### Navigated to a new page, clear in-page mark. ### - @inPageIndex = -1 - else if @inPageIndex is -1 and inPage - ### Started in-page navigations. ### - @inPageIndex = @currentIndex - - if @pendingIndex >= 0 - ### Go to index. ### - @currentIndex = @pendingIndex - @pendingIndex = -1 - @history[@currentIndex] = url - else if replaceEntry - ### Non-user initialized navigation. ### - @history[@currentIndex] = url - else - ### Normal navigation. Clear history. ### - @history = @history.slice 0, @currentIndex + 1 - currentEntry = @history[@currentIndex] - if currentEntry?.url isnt url - @currentIndex++ - @history.push url - - loadURL: (url, options={}) -> - @pendingIndex = -1 - @webContents._loadURL url, options - @webContents.emit 'load-url', url, options - - getURL: -> - if @currentIndex is -1 - '' - else - @history[@currentIndex] - - stop: -> - @pendingIndex = -1 - @webContents._stop() - - reload: -> - @pendingIndex = @currentIndex - @webContents._loadURL @getURL(), {} - - reloadIgnoringCache: -> - @pendingIndex = @currentIndex - @webContents._loadURL @getURL(), {extraHeaders: "pragma: no-cache\n"} - - canGoBack: -> - @getActiveIndex() > 0 - - canGoForward: -> - @getActiveIndex() < @history.length - 1 - - canGoToIndex: (index) -> - index >=0 and index < @history.length - - canGoToOffset: (offset) -> - @canGoToIndex @currentIndex + offset - - clearHistory: -> - @history = [] - @currentIndex = -1 - @pendingIndex = -1 - @inPageIndex = -1 - - goBack: -> - return unless @canGoBack() - @pendingIndex = @getActiveIndex() - 1 - if @inPageIndex > -1 and @pendingIndex >= @inPageIndex - @webContents._goBack() - else - @webContents._loadURL @history[@pendingIndex], {} - - goForward: -> - return unless @canGoForward() - @pendingIndex = @getActiveIndex() + 1 - if @inPageIndex > -1 and @pendingIndex >= @inPageIndex - @webContents._goForward() - else - @webContents._loadURL @history[@pendingIndex], {} - - goToIndex: (index) -> - return unless @canGoToIndex index - @pendingIndex = index - @webContents._loadURL @history[@pendingIndex], {} - - goToOffset: (offset) -> - return unless @canGoToOffset offset - pendingIndex = @currentIndex + offset - if @inPageIndex > -1 and pendingIndex >= @inPageIndex - @pendingIndex = pendingIndex - @webContents._goToOffset offset - else - @goToIndex pendingIndex - - getActiveIndex: -> - if @pendingIndex is -1 then @currentIndex else @pendingIndex - - length: -> - @history.length - -module.exports = NavigationController diff --git a/atom/browser/api/lib/navigation-controller.js b/atom/browser/api/lib/navigation-controller.js new file mode 100644 index 000000000000..8e7753de5e3b --- /dev/null +++ b/atom/browser/api/lib/navigation-controller.js @@ -0,0 +1,195 @@ +var NavigationController, ipcMain, + slice = [].slice; + +ipcMain = require('electron').ipcMain; + + +/* The history operation in renderer is redirected to browser. */ + +ipcMain.on('ATOM_SHELL_NAVIGATION_CONTROLLER', function() { + var args, event, method, ref; + event = arguments[0], method = arguments[1], args = 3 <= arguments.length ? slice.call(arguments, 2) : []; + return (ref = event.sender)[method].apply(ref, args); +}); + +ipcMain.on('ATOM_SHELL_SYNC_NAVIGATION_CONTROLLER', function() { + var args, event, method, ref; + event = arguments[0], method = arguments[1], args = 3 <= arguments.length ? slice.call(arguments, 2) : []; + return event.returnValue = (ref = event.sender)[method].apply(ref, args); +}); + + +/* + JavaScript implementation of Chromium's NavigationController. + Instead of relying on Chromium for history control, we compeletely do history + control on user land, and only rely on WebContents.loadURL for navigation. + This helps us avoid Chromium's various optimizations so we can ensure renderer + process is restarted everytime. + */ + +NavigationController = (function() { + function NavigationController(webContents) { + this.webContents = webContents; + this.clearHistory(); + + /* webContents may have already navigated to a page. */ + if (this.webContents._getURL()) { + this.currentIndex++; + this.history.push(this.webContents._getURL()); + } + this.webContents.on('navigation-entry-commited', (function(_this) { + return function(event, url, inPage, replaceEntry) { + var currentEntry; + if (_this.inPageIndex > -1 && !inPage) { + + /* Navigated to a new page, clear in-page mark. */ + _this.inPageIndex = -1; + } else if (_this.inPageIndex === -1 && inPage) { + + /* Started in-page navigations. */ + _this.inPageIndex = _this.currentIndex; + } + if (_this.pendingIndex >= 0) { + + /* Go to index. */ + _this.currentIndex = _this.pendingIndex; + _this.pendingIndex = -1; + return _this.history[_this.currentIndex] = url; + } else if (replaceEntry) { + + /* Non-user initialized navigation. */ + return _this.history[_this.currentIndex] = url; + } else { + + /* Normal navigation. Clear history. */ + _this.history = _this.history.slice(0, _this.currentIndex + 1); + currentEntry = _this.history[_this.currentIndex]; + if ((currentEntry != null ? currentEntry.url : void 0) !== url) { + _this.currentIndex++; + return _this.history.push(url); + } + } + }; + })(this)); + } + + NavigationController.prototype.loadURL = function(url, options) { + if (options == null) { + options = {}; + } + this.pendingIndex = -1; + this.webContents._loadURL(url, options); + return this.webContents.emit('load-url', url, options); + }; + + NavigationController.prototype.getURL = function() { + if (this.currentIndex === -1) { + return ''; + } else { + return this.history[this.currentIndex]; + } + }; + + NavigationController.prototype.stop = function() { + this.pendingIndex = -1; + return this.webContents._stop(); + }; + + NavigationController.prototype.reload = function() { + this.pendingIndex = this.currentIndex; + return this.webContents._loadURL(this.getURL(), {}); + }; + + NavigationController.prototype.reloadIgnoringCache = function() { + this.pendingIndex = this.currentIndex; + return this.webContents._loadURL(this.getURL(), { + extraHeaders: "pragma: no-cache\n" + }); + }; + + NavigationController.prototype.canGoBack = function() { + return this.getActiveIndex() > 0; + }; + + NavigationController.prototype.canGoForward = function() { + return this.getActiveIndex() < this.history.length - 1; + }; + + NavigationController.prototype.canGoToIndex = function(index) { + return index >= 0 && index < this.history.length; + }; + + NavigationController.prototype.canGoToOffset = function(offset) { + return this.canGoToIndex(this.currentIndex + offset); + }; + + NavigationController.prototype.clearHistory = function() { + this.history = []; + this.currentIndex = -1; + this.pendingIndex = -1; + return this.inPageIndex = -1; + }; + + NavigationController.prototype.goBack = function() { + if (!this.canGoBack()) { + return; + } + this.pendingIndex = this.getActiveIndex() - 1; + if (this.inPageIndex > -1 && this.pendingIndex >= this.inPageIndex) { + return this.webContents._goBack(); + } else { + return this.webContents._loadURL(this.history[this.pendingIndex], {}); + } + }; + + NavigationController.prototype.goForward = function() { + if (!this.canGoForward()) { + return; + } + this.pendingIndex = this.getActiveIndex() + 1; + if (this.inPageIndex > -1 && this.pendingIndex >= this.inPageIndex) { + return this.webContents._goForward(); + } else { + return this.webContents._loadURL(this.history[this.pendingIndex], {}); + } + }; + + NavigationController.prototype.goToIndex = function(index) { + if (!this.canGoToIndex(index)) { + return; + } + this.pendingIndex = index; + return this.webContents._loadURL(this.history[this.pendingIndex], {}); + }; + + NavigationController.prototype.goToOffset = function(offset) { + var pendingIndex; + if (!this.canGoToOffset(offset)) { + return; + } + pendingIndex = this.currentIndex + offset; + if (this.inPageIndex > -1 && pendingIndex >= this.inPageIndex) { + this.pendingIndex = pendingIndex; + return this.webContents._goToOffset(offset); + } else { + return this.goToIndex(pendingIndex); + } + }; + + NavigationController.prototype.getActiveIndex = function() { + if (this.pendingIndex === -1) { + return this.currentIndex; + } else { + return this.pendingIndex; + } + }; + + NavigationController.prototype.length = function() { + return this.history.length; + }; + + return NavigationController; + +})(); + +module.exports = NavigationController; diff --git a/atom/browser/api/lib/power-monitor.coffee b/atom/browser/api/lib/power-monitor.coffee deleted file mode 100644 index 54bf9391827c..000000000000 --- a/atom/browser/api/lib/power-monitor.coffee +++ /dev/null @@ -1,7 +0,0 @@ -{EventEmitter} = require 'events' - -{powerMonitor} = process.atomBinding 'power_monitor' - -powerMonitor.__proto__ = EventEmitter.prototype - -module.exports = powerMonitor diff --git a/atom/browser/api/lib/power-monitor.js b/atom/browser/api/lib/power-monitor.js new file mode 100644 index 000000000000..df4b28c6eb66 --- /dev/null +++ b/atom/browser/api/lib/power-monitor.js @@ -0,0 +1,9 @@ +var EventEmitter, powerMonitor; + +EventEmitter = require('events').EventEmitter; + +powerMonitor = process.atomBinding('power_monitor').powerMonitor; + +powerMonitor.__proto__ = EventEmitter.prototype; + +module.exports = powerMonitor; diff --git a/atom/browser/api/lib/power-save-blocker.coffee b/atom/browser/api/lib/power-save-blocker.coffee deleted file mode 100644 index 58392bc9aa8b..000000000000 --- a/atom/browser/api/lib/power-save-blocker.coffee +++ /dev/null @@ -1,3 +0,0 @@ -{powerSaveBlocker} = process.atomBinding 'power_save_blocker' - -module.exports = powerSaveBlocker diff --git a/atom/browser/api/lib/power-save-blocker.js b/atom/browser/api/lib/power-save-blocker.js new file mode 100644 index 000000000000..c44e3e2b63a2 --- /dev/null +++ b/atom/browser/api/lib/power-save-blocker.js @@ -0,0 +1,5 @@ +var powerSaveBlocker; + +powerSaveBlocker = process.atomBinding('power_save_blocker').powerSaveBlocker; + +module.exports = powerSaveBlocker; diff --git a/atom/browser/api/lib/protocol.coffee b/atom/browser/api/lib/protocol.coffee deleted file mode 100644 index 1add325eaae0..000000000000 --- a/atom/browser/api/lib/protocol.coffee +++ /dev/null @@ -1,25 +0,0 @@ -{app} = require 'electron' - -throw new Error('Can not initialize protocol module before app is ready') unless app.isReady() - -{protocol} = process.atomBinding 'protocol' - -### Warn about removed APIs. ### -logAndThrow = (callback, message) -> - console.error message - if callback then callback(new Error(message)) else throw new Error(message) -protocol.registerProtocol = (scheme, handler, callback) -> - logAndThrow callback, - 'registerProtocol API has been replaced by the - register[File/Http/Buffer/String]Protocol API family, please - switch to the new APIs.' -protocol.isHandledProtocol = (scheme, callback) -> - logAndThrow callback, - 'isHandledProtocol API has been replaced by isProtocolHandled.' -protocol.interceptProtocol = (scheme, handler, callback) -> - logAndThrow callback, - 'interceptProtocol API has been replaced by the - intercept[File/Http/Buffer/String]Protocol API family, please - switch to the new APIs.' - -module.exports = protocol diff --git a/atom/browser/api/lib/protocol.js b/atom/browser/api/lib/protocol.js new file mode 100644 index 000000000000..d601e588cf32 --- /dev/null +++ b/atom/browser/api/lib/protocol.js @@ -0,0 +1,35 @@ +var app, logAndThrow, protocol; + +app = require('electron').app; + +if (!app.isReady()) { + throw new Error('Can not initialize protocol module before app is ready'); +} + +protocol = process.atomBinding('protocol').protocol; + + +/* Warn about removed APIs. */ + +logAndThrow = function(callback, message) { + console.error(message); + if (callback) { + return callback(new Error(message)); + } else { + throw new Error(message); + } +}; + +protocol.registerProtocol = function(scheme, handler, callback) { + return logAndThrow(callback, 'registerProtocol API has been replaced by the register[File/Http/Buffer/String]Protocol API family, please switch to the new APIs.'); +}; + +protocol.isHandledProtocol = function(scheme, callback) { + return logAndThrow(callback, 'isHandledProtocol API has been replaced by isProtocolHandled.'); +}; + +protocol.interceptProtocol = function(scheme, handler, callback) { + return logAndThrow(callback, 'interceptProtocol API has been replaced by the intercept[File/Http/Buffer/String]Protocol API family, please switch to the new APIs.'); +}; + +module.exports = protocol; diff --git a/atom/browser/api/lib/screen.coffee b/atom/browser/api/lib/screen.coffee deleted file mode 100644 index 87c42f091df2..000000000000 --- a/atom/browser/api/lib/screen.coffee +++ /dev/null @@ -1,6 +0,0 @@ -{EventEmitter} = require 'events' -{screen} = process.atomBinding 'screen' - -screen.__proto__ = EventEmitter.prototype - -module.exports = screen diff --git a/atom/browser/api/lib/screen.js b/atom/browser/api/lib/screen.js new file mode 100644 index 000000000000..3dca49e99d90 --- /dev/null +++ b/atom/browser/api/lib/screen.js @@ -0,0 +1,9 @@ +var EventEmitter, screen; + +EventEmitter = require('events').EventEmitter; + +screen = process.atomBinding('screen').screen; + +screen.__proto__ = EventEmitter.prototype; + +module.exports = screen; diff --git a/atom/browser/api/lib/session.coffee b/atom/browser/api/lib/session.coffee deleted file mode 100644 index 95f9e474e090..000000000000 --- a/atom/browser/api/lib/session.coffee +++ /dev/null @@ -1,24 +0,0 @@ -{EventEmitter} = require 'events' - -bindings = process.atomBinding 'session' - -PERSIST_PERFIX = 'persist:' - -### Returns the Session from |partition| string. ### -exports.fromPartition = (partition='') -> - return exports.defaultSession if partition is '' - if partition.startsWith PERSIST_PERFIX - bindings.fromPartition partition.substr(PERSIST_PERFIX.length), false - else - bindings.fromPartition partition, true - -### Returns the default session. ### -Object.defineProperty exports, 'defaultSession', - enumerable: true - get: -> bindings.fromPartition '', false - -wrapSession = (session) -> - ### session is an EventEmitter. ### - session.__proto__ = EventEmitter.prototype - -bindings._setWrapSession wrapSession diff --git a/atom/browser/api/lib/session.js b/atom/browser/api/lib/session.js new file mode 100644 index 000000000000..fa43a2ae219d --- /dev/null +++ b/atom/browser/api/lib/session.js @@ -0,0 +1,42 @@ +var EventEmitter, PERSIST_PERFIX, bindings, wrapSession; + +EventEmitter = require('events').EventEmitter; + +bindings = process.atomBinding('session'); + +PERSIST_PERFIX = 'persist:'; + + +/* Returns the Session from |partition| string. */ + +exports.fromPartition = function(partition) { + if (partition == null) { + partition = ''; + } + if (partition === '') { + return exports.defaultSession; + } + if (partition.startsWith(PERSIST_PERFIX)) { + return bindings.fromPartition(partition.substr(PERSIST_PERFIX.length), false); + } else { + return bindings.fromPartition(partition, true); + } +}; + + +/* Returns the default session. */ + +Object.defineProperty(exports, 'defaultSession', { + enumerable: true, + get: function() { + return bindings.fromPartition('', false); + } +}); + +wrapSession = function(session) { + + /* session is an EventEmitter. */ + return session.__proto__ = EventEmitter.prototype; +}; + +bindings._setWrapSession(wrapSession); diff --git a/atom/browser/api/lib/tray.coffee b/atom/browser/api/lib/tray.coffee deleted file mode 100644 index 70a423dd97d5..000000000000 --- a/atom/browser/api/lib/tray.coffee +++ /dev/null @@ -1,20 +0,0 @@ -{deprecate} = require 'electron' -{EventEmitter} = require 'events' - -{Tray} = process.atomBinding 'tray' -Tray::__proto__ = EventEmitter.prototype - -Tray::_init = -> - ### Deprecated. ### - deprecate.rename this, 'popContextMenu', 'popUpContextMenu' - deprecate.event this, 'clicked', 'click' - deprecate.event this, 'double-clicked', 'double-click' - deprecate.event this, 'right-clicked', 'right-click' - deprecate.event this, 'balloon-clicked', 'balloon-click' - -Tray::setContextMenu = (menu) -> - @_setContextMenu menu - ### Keep a strong reference of menu. ### - @menu = menu - -module.exports = Tray diff --git a/atom/browser/api/lib/tray.js b/atom/browser/api/lib/tray.js new file mode 100644 index 000000000000..822bf31eb3af --- /dev/null +++ b/atom/browser/api/lib/tray.js @@ -0,0 +1,28 @@ +var EventEmitter, Tray, deprecate; + +deprecate = require('electron').deprecate; + +EventEmitter = require('events').EventEmitter; + +Tray = process.atomBinding('tray').Tray; + +Tray.prototype.__proto__ = EventEmitter.prototype; + +Tray.prototype._init = function() { + + /* Deprecated. */ + deprecate.rename(this, 'popContextMenu', 'popUpContextMenu'); + deprecate.event(this, 'clicked', 'click'); + deprecate.event(this, 'double-clicked', 'double-click'); + deprecate.event(this, 'right-clicked', 'right-click'); + return deprecate.event(this, 'balloon-clicked', 'balloon-click'); +}; + +Tray.prototype.setContextMenu = function(menu) { + this._setContextMenu(menu); + + /* Keep a strong reference of menu. */ + return this.menu = menu; +}; + +module.exports = Tray; diff --git a/atom/browser/api/lib/web-contents.coffee b/atom/browser/api/lib/web-contents.coffee deleted file mode 100644 index 75e35c4fdc49..000000000000 --- a/atom/browser/api/lib/web-contents.coffee +++ /dev/null @@ -1,141 +0,0 @@ -{EventEmitter} = require 'events' -{deprecate, ipcMain, session, NavigationController, Menu} = require 'electron' - -binding = process.atomBinding 'web_contents' - -nextId = 0 -getNextId = -> ++nextId - -PDFPageSize = - A5: - custom_display_name: "A5" - height_microns: 210000 - name: "ISO_A5" - width_microns: 148000 - A4: - custom_display_name: "A4" - height_microns: 297000 - name: "ISO_A4" - is_default: "true" - width_microns: 210000 - A3: - custom_display_name: "A3" - height_microns: 420000 - name: "ISO_A3" - width_microns: 297000 - Legal: - custom_display_name: "Legal" - height_microns: 355600 - name: "NA_LEGAL" - width_microns: 215900 - Letter: - custom_display_name: "Letter" - height_microns: 279400 - name: "NA_LETTER" - width_microns: 215900 - Tabloid: - height_microns: 431800 - name: "NA_LEDGER" - width_microns: 279400 - custom_display_name: "Tabloid" - -wrapWebContents = (webContents) -> - ### webContents is an EventEmitter. ### - webContents.__proto__ = EventEmitter.prototype - - ### WebContents::send(channel, args..) ### - webContents.send = (channel, args...) -> - @_send channel, [args...] - - ### - Make sure webContents.executeJavaScript would run the code only when the - web contents has been loaded. - ### - webContents.executeJavaScript = (code, hasUserGesture=false) -> - if @getURL() and not @isLoading() - @_executeJavaScript code, hasUserGesture - else - webContents.once 'did-finish-load', @_executeJavaScript.bind(this, code, hasUserGesture) - - ### The navigation controller. ### - controller = new NavigationController(webContents) - for name, method of NavigationController.prototype when method instanceof Function - do (name, method) -> - webContents[name] = -> method.apply controller, arguments - - ### Dispatch IPC messages to the ipc module. ### - webContents.on 'ipc-message', (event, packed) -> - [channel, args...] = packed - ipcMain.emit channel, event, args... - webContents.on 'ipc-message-sync', (event, packed) -> - [channel, args...] = packed - Object.defineProperty event, 'returnValue', set: (value) -> event.sendReply JSON.stringify(value) - ipcMain.emit channel, event, args... - - ### Handle context menu action request from pepper plugin. ### - webContents.on 'pepper-context-menu', (event, params) -> - menu = Menu.buildFromTemplate params.menu - menu.popup params.x, params.y - - ### This error occurs when host could not be found. ### - webContents.on 'did-fail-provisional-load', (args...) -> - ### - Calling loadURL during this event might cause crash, so delay the event - until next tick. - ### - setImmediate => @emit 'did-fail-load', args... - - ### Delays the page-title-updated event to next tick. ### - webContents.on '-page-title-updated', (args...) -> - setImmediate => @emit 'page-title-updated', args... - - ### Deprecated. ### - deprecate.rename webContents, 'loadUrl', 'loadURL' - deprecate.rename webContents, 'getUrl', 'getURL' - deprecate.event webContents, 'page-title-set', 'page-title-updated', (args...) -> - @emit 'page-title-set', args... - - webContents.printToPDF = (options, callback) -> - printingSetting = - pageRage: [] - mediaSize: {} - landscape: false - color: 2 - headerFooterEnabled: false - marginsType: 0 - isFirstRequest: false - requestID: getNextId() - previewModifiable: true - printToPDF: true - printWithCloudPrint: false - printWithPrivet: false - printWithExtension: false - deviceName: "Save as PDF" - generateDraftData: true - fitToPageEnabled: false - duplex: 0 - copies: 1 - collate: true - shouldPrintBackgrounds: false - shouldPrintSelectionOnly: false - - if options.landscape - printingSetting.landscape = options.landscape - if options.marginsType - printingSetting.marginsType = options.marginsType - if options.printSelectionOnly - printingSetting.shouldPrintSelectionOnly = options.printSelectionOnly - if options.printBackground - printingSetting.shouldPrintBackgrounds = options.printBackground - - if options.pageSize and PDFPageSize[options.pageSize] - printingSetting.mediaSize = PDFPageSize[options.pageSize] - else - printingSetting.mediaSize = PDFPageSize['A4'] - - @_printToPDF printingSetting, callback - -binding._setWrapWebContents wrapWebContents - -module.exports.create = (options={}) -> - binding.create(options) diff --git a/atom/browser/api/lib/web-contents.js b/atom/browser/api/lib/web-contents.js new file mode 100644 index 000000000000..0d1b8b24bdd0 --- /dev/null +++ b/atom/browser/api/lib/web-contents.js @@ -0,0 +1,210 @@ +var EventEmitter, Menu, NavigationController, PDFPageSize, binding, deprecate, getNextId, ipcMain, nextId, ref, session, wrapWebContents, + slice = [].slice; + +EventEmitter = require('events').EventEmitter; + +ref = require('electron'), deprecate = ref.deprecate, ipcMain = ref.ipcMain, session = ref.session, NavigationController = ref.NavigationController, Menu = ref.Menu; + +binding = process.atomBinding('web_contents'); + +nextId = 0; + +getNextId = function() { + return ++nextId; +}; + +PDFPageSize = { + A5: { + custom_display_name: "A5", + height_microns: 210000, + name: "ISO_A5", + width_microns: 148000 + }, + A4: { + custom_display_name: "A4", + height_microns: 297000, + name: "ISO_A4", + is_default: "true", + width_microns: 210000 + }, + A3: { + custom_display_name: "A3", + height_microns: 420000, + name: "ISO_A3", + width_microns: 297000 + }, + Legal: { + custom_display_name: "Legal", + height_microns: 355600, + name: "NA_LEGAL", + width_microns: 215900 + }, + Letter: { + custom_display_name: "Letter", + height_microns: 279400, + name: "NA_LETTER", + width_microns: 215900 + }, + Tabloid: { + height_microns: 431800, + name: "NA_LEDGER", + width_microns: 279400, + custom_display_name: "Tabloid" + } +}; + +wrapWebContents = function(webContents) { + + /* webContents is an EventEmitter. */ + var controller, method, name, ref1; + webContents.__proto__ = EventEmitter.prototype; + + /* WebContents::send(channel, args..) */ + webContents.send = function() { + var args, channel; + channel = arguments[0], args = 2 <= arguments.length ? slice.call(arguments, 1) : []; + return this._send(channel, slice.call(args)); + }; + + /* + Make sure webContents.executeJavaScript would run the code only when the + web contents has been loaded. + */ + webContents.executeJavaScript = function(code, hasUserGesture) { + if (hasUserGesture == null) { + hasUserGesture = false; + } + if (this.getURL() && !this.isLoading()) { + return this._executeJavaScript(code, hasUserGesture); + } else { + return webContents.once('did-finish-load', this._executeJavaScript.bind(this, code, hasUserGesture)); + } + }; + + /* The navigation controller. */ + controller = new NavigationController(webContents); + ref1 = NavigationController.prototype; + for (name in ref1) { + method = ref1[name]; + if (method instanceof Function) { + (function(name, method) { + return webContents[name] = function() { + return method.apply(controller, arguments); + }; + })(name, method); + } + } + + /* Dispatch IPC messages to the ipc module. */ + webContents.on('ipc-message', function(event, packed) { + var args, channel; + channel = packed[0], args = 2 <= packed.length ? slice.call(packed, 1) : []; + return ipcMain.emit.apply(ipcMain, [channel, event].concat(slice.call(args))); + }); + webContents.on('ipc-message-sync', function(event, packed) { + var args, channel; + channel = packed[0], args = 2 <= packed.length ? slice.call(packed, 1) : []; + Object.defineProperty(event, 'returnValue', { + set: function(value) { + return event.sendReply(JSON.stringify(value)); + } + }); + return ipcMain.emit.apply(ipcMain, [channel, event].concat(slice.call(args))); + }); + + /* Handle context menu action request from pepper plugin. */ + webContents.on('pepper-context-menu', function(event, params) { + var menu; + menu = Menu.buildFromTemplate(params.menu); + return menu.popup(params.x, params.y); + }); + + /* This error occurs when host could not be found. */ + webContents.on('did-fail-provisional-load', function() { + var args; + args = 1 <= arguments.length ? slice.call(arguments, 0) : []; + + /* + Calling loadURL during this event might cause crash, so delay the event + until next tick. + */ + return setImmediate((function(_this) { + return function() { + return _this.emit.apply(_this, ['did-fail-load'].concat(slice.call(args))); + }; + })(this)); + }); + + /* Delays the page-title-updated event to next tick. */ + webContents.on('-page-title-updated', function() { + var args; + args = 1 <= arguments.length ? slice.call(arguments, 0) : []; + return setImmediate((function(_this) { + return function() { + return _this.emit.apply(_this, ['page-title-updated'].concat(slice.call(args))); + }; + })(this)); + }); + + /* Deprecated. */ + deprecate.rename(webContents, 'loadUrl', 'loadURL'); + deprecate.rename(webContents, 'getUrl', 'getURL'); + deprecate.event(webContents, 'page-title-set', 'page-title-updated', function() { + var args; + args = 1 <= arguments.length ? slice.call(arguments, 0) : []; + return this.emit.apply(this, ['page-title-set'].concat(slice.call(args))); + }); + return webContents.printToPDF = function(options, callback) { + var printingSetting; + printingSetting = { + pageRage: [], + mediaSize: {}, + landscape: false, + color: 2, + headerFooterEnabled: false, + marginsType: 0, + isFirstRequest: false, + requestID: getNextId(), + previewModifiable: true, + printToPDF: true, + printWithCloudPrint: false, + printWithPrivet: false, + printWithExtension: false, + deviceName: "Save as PDF", + generateDraftData: true, + fitToPageEnabled: false, + duplex: 0, + copies: 1, + collate: true, + shouldPrintBackgrounds: false, + shouldPrintSelectionOnly: false + }; + if (options.landscape) { + printingSetting.landscape = options.landscape; + } + if (options.marginsType) { + printingSetting.marginsType = options.marginsType; + } + if (options.printSelectionOnly) { + printingSetting.shouldPrintSelectionOnly = options.printSelectionOnly; + } + if (options.printBackground) { + printingSetting.shouldPrintBackgrounds = options.printBackground; + } + if (options.pageSize && PDFPageSize[options.pageSize]) { + printingSetting.mediaSize = PDFPageSize[options.pageSize]; + } else { + printingSetting.mediaSize = PDFPageSize['A4']; + } + return this._printToPDF(printingSetting, callback); + }; +}; + +binding._setWrapWebContents(wrapWebContents); + +module.exports.create = function(options) { + if (options == null) { + options = {}; + } + return binding.create(options); +}; diff --git a/atom/browser/lib/chrome-extension.coffee b/atom/browser/lib/chrome-extension.coffee deleted file mode 100644 index d20e4c3d5e85..000000000000 --- a/atom/browser/lib/chrome-extension.coffee +++ /dev/null @@ -1,98 +0,0 @@ -electron = require 'electron' -fs = require 'fs' -path = require 'path' -url = require 'url' - -### Mapping between hostname and file path. ### -hostPathMap = {} -hostPathMapNextKey = 0 - -getHostForPath = (path) -> - key = "extension-#{++hostPathMapNextKey}" - hostPathMap[key] = path - key - -getPathForHost = (host) -> - hostPathMap[host] - -### Cache extensionInfo. ### -extensionInfoMap = {} - -getExtensionInfoFromPath = (srcDirectory) -> - manifest = JSON.parse fs.readFileSync(path.join(srcDirectory, 'manifest.json')) - unless extensionInfoMap[manifest.name]? - ### - We can not use 'file://' directly because all resources in the extension - will be treated as relative to the root in Chrome. - ### - page = url.format - protocol: 'chrome-extension' - slashes: true - hostname: getHostForPath srcDirectory - pathname: manifest.devtools_page - extensionInfoMap[manifest.name] = - startPage: page - name: manifest.name - srcDirectory: srcDirectory - exposeExperimentalAPIs: true - extensionInfoMap[manifest.name] - -### The loaded extensions cache and its persistent path. ### -loadedExtensions = null -loadedExtensionsPath = null - -### Persistent loaded extensions. ### -{app} = electron -app.on 'will-quit', -> - try - loadedExtensions = Object.keys(extensionInfoMap).map (key) -> extensionInfoMap[key].srcDirectory - try - fs.mkdirSync path.dirname(loadedExtensionsPath) - catch e - fs.writeFileSync loadedExtensionsPath, JSON.stringify(loadedExtensions) - catch e - -### We can not use protocol or BrowserWindow until app is ready. ### -app.once 'ready', -> - {protocol, BrowserWindow} = electron - - ### Load persistented extensions. ### - loadedExtensionsPath = path.join app.getPath('userData'), 'DevTools Extensions' - - try - loadedExtensions = JSON.parse fs.readFileSync(loadedExtensionsPath) - loadedExtensions = [] unless Array.isArray loadedExtensions - ### Preheat the extensionInfo cache. ### - getExtensionInfoFromPath srcDirectory for srcDirectory in loadedExtensions - catch e - - ### The chrome-extension: can map a extension URL request to real file path. ### - chromeExtensionHandler = (request, callback) -> - parsed = url.parse request.url - return callback() unless parsed.hostname and parsed.path? - return callback() unless /extension-\d+/.test parsed.hostname - - directory = getPathForHost parsed.hostname - return callback() unless directory? - callback path.join(directory, parsed.path) - protocol.registerFileProtocol 'chrome-extension', chromeExtensionHandler, (error) -> - console.error 'Unable to register chrome-extension protocol' if error - - BrowserWindow::_loadDevToolsExtensions = (extensionInfoArray) -> - @devToolsWebContents?.executeJavaScript "DevToolsAPI.addExtensions(#{JSON.stringify(extensionInfoArray)});" - - BrowserWindow.addDevToolsExtension = (srcDirectory) -> - extensionInfo = getExtensionInfoFromPath srcDirectory - if extensionInfo - window._loadDevToolsExtensions [extensionInfo] for window in BrowserWindow.getAllWindows() - extensionInfo.name - - BrowserWindow.removeDevToolsExtension = (name) -> - delete extensionInfoMap[name] - - ### Load persistented extensions when devtools is opened. ### - init = BrowserWindow::_init - BrowserWindow::_init = -> - init.call this - @on 'devtools-opened', -> - @_loadDevToolsExtensions Object.keys(extensionInfoMap).map (key) -> extensionInfoMap[key] diff --git a/atom/browser/lib/chrome-extension.js b/atom/browser/lib/chrome-extension.js new file mode 100644 index 000000000000..e4de0b6779b3 --- /dev/null +++ b/atom/browser/lib/chrome-extension.js @@ -0,0 +1,163 @@ +var app, electron, extensionInfoMap, fs, getExtensionInfoFromPath, getHostForPath, getPathForHost, hostPathMap, hostPathMapNextKey, loadedExtensions, loadedExtensionsPath, path, url; + +electron = require('electron'); + +fs = require('fs'); + +path = require('path'); + +url = require('url'); + + +/* Mapping between hostname and file path. */ + +hostPathMap = {}; + +hostPathMapNextKey = 0; + +getHostForPath = function(path) { + var key; + key = "extension-" + (++hostPathMapNextKey); + hostPathMap[key] = path; + return key; +}; + +getPathForHost = function(host) { + return hostPathMap[host]; +}; + + +/* Cache extensionInfo. */ + +extensionInfoMap = {}; + +getExtensionInfoFromPath = function(srcDirectory) { + var manifest, page; + manifest = JSON.parse(fs.readFileSync(path.join(srcDirectory, 'manifest.json'))); + if (extensionInfoMap[manifest.name] == null) { + + /* + We can not use 'file://' directly because all resources in the extension + will be treated as relative to the root in Chrome. + */ + page = url.format({ + protocol: 'chrome-extension', + slashes: true, + hostname: getHostForPath(srcDirectory), + pathname: manifest.devtools_page + }); + extensionInfoMap[manifest.name] = { + startPage: page, + name: manifest.name, + srcDirectory: srcDirectory, + exposeExperimentalAPIs: true + }; + return extensionInfoMap[manifest.name]; + } +}; + + +/* The loaded extensions cache and its persistent path. */ + +loadedExtensions = null; + +loadedExtensionsPath = null; + + +/* Persistent loaded extensions. */ + +app = electron.app; + +app.on('will-quit', function() { + var e, error1, error2; + try { + loadedExtensions = Object.keys(extensionInfoMap).map(function(key) { + return extensionInfoMap[key].srcDirectory; + }); + try { + fs.mkdirSync(path.dirname(loadedExtensionsPath)); + } catch (error1) { + e = error1; + } + return fs.writeFileSync(loadedExtensionsPath, JSON.stringify(loadedExtensions)); + } catch (error2) { + e = error2; + } +}); + + +/* We can not use protocol or BrowserWindow until app is ready. */ + +app.once('ready', function() { + var BrowserWindow, chromeExtensionHandler, e, error1, i, init, len, protocol, srcDirectory; + protocol = electron.protocol, BrowserWindow = electron.BrowserWindow; + + /* Load persistented extensions. */ + loadedExtensionsPath = path.join(app.getPath('userData'), 'DevTools Extensions'); + try { + loadedExtensions = JSON.parse(fs.readFileSync(loadedExtensionsPath)); + if (!Array.isArray(loadedExtensions)) { + loadedExtensions = []; + } + + /* Preheat the extensionInfo cache. */ + for (i = 0, len = loadedExtensions.length; i < len; i++) { + srcDirectory = loadedExtensions[i]; + getExtensionInfoFromPath(srcDirectory); + } + } catch (error1) { + e = error1; + } + + /* The chrome-extension: can map a extension URL request to real file path. */ + chromeExtensionHandler = function(request, callback) { + var directory, parsed; + parsed = url.parse(request.url); + if (!(parsed.hostname && (parsed.path != null))) { + return callback(); + } + if (!/extension-\d+/.test(parsed.hostname)) { + return callback(); + } + directory = getPathForHost(parsed.hostname); + if (directory == null) { + return callback(); + } + return callback(path.join(directory, parsed.path)); + }; + protocol.registerFileProtocol('chrome-extension', chromeExtensionHandler, function(error) { + if (error) { + return console.error('Unable to register chrome-extension protocol'); + } + }); + BrowserWindow.prototype._loadDevToolsExtensions = function(extensionInfoArray) { + var ref; + return (ref = this.devToolsWebContents) != null ? ref.executeJavaScript("DevToolsAPI.addExtensions(" + (JSON.stringify(extensionInfoArray)) + ");") : void 0; + }; + BrowserWindow.addDevToolsExtension = function(srcDirectory) { + var extensionInfo, j, len1, ref, window; + extensionInfo = getExtensionInfoFromPath(srcDirectory); + if (extensionInfo) { + ref = BrowserWindow.getAllWindows(); + for (j = 0, len1 = ref.length; j < len1; j++) { + window = ref[j]; + window._loadDevToolsExtensions([extensionInfo]); + } + return extensionInfo.name; + } + }; + BrowserWindow.removeDevToolsExtension = function(name) { + return delete extensionInfoMap[name]; + }; + + /* Load persistented extensions when devtools is opened. */ + init = BrowserWindow.prototype._init; + return BrowserWindow.prototype._init = function() { + init.call(this); + return this.on('devtools-opened', function() { + return this._loadDevToolsExtensions(Object.keys(extensionInfoMap).map(function(key) { + return extensionInfoMap[key]; + })); + }); + }; +}); diff --git a/atom/browser/lib/desktop-capturer.coffee b/atom/browser/lib/desktop-capturer.coffee deleted file mode 100644 index f783ccc9f78b..000000000000 --- a/atom/browser/lib/desktop-capturer.coffee +++ /dev/null @@ -1,41 +0,0 @@ -{ipcMain} = require 'electron' -{desktopCapturer} = process.atomBinding 'desktop_capturer' - -deepEqual = (opt1, opt2) -> - return JSON.stringify(opt1) is JSON.stringify(opt2) - -### A queue for holding all requests from renderer process. ### -requestsQueue = [] - -ipcMain.on 'ATOM_BROWSER_DESKTOP_CAPTURER_GET_SOURCES', (event, captureWindow, captureScreen, thumbnailSize, id) -> - request = id: id, options: {captureWindow, captureScreen, thumbnailSize}, webContents: event.sender - requestsQueue.push request - desktopCapturer.startHandling captureWindow, captureScreen, thumbnailSize if requestsQueue.length is 1 - ### - If the WebContents is destroyed before receiving result, just remove the - reference from requestsQueue to make the module not send the result to it. - ### - event.sender.once 'destroyed', -> - request.webContents = null - -desktopCapturer.emit = (event, name, sources) -> - ### Receiving sources result from main process, now send them back to renderer. ### - handledRequest = requestsQueue.shift 0 - result = ({ id: source.id, name: source.name, thumbnail: source.thumbnail.toDataUrl() } for source in sources) - handledRequest.webContents?.send "ATOM_RENDERER_DESKTOP_CAPTURER_RESULT_#{handledRequest.id}", result - - ### - Check the queue to see whether there is other same request. If has, handle - it for reducing redunplicated `desktopCaptuer.startHandling` calls. - ### - unhandledRequestsQueue = [] - for request in requestsQueue - if deepEqual handledRequest.options, request.options - request.webContents?.send "ATOM_RENDERER_DESKTOP_CAPTURER_RESULT_#{request.id}", errorMessage, result - else - unhandledRequestsQueue.push request - requestsQueue = unhandledRequestsQueue - ### If the requestsQueue is not empty, start a new request handling. ### - if requestsQueue.length > 0 - {captureWindow, captureScreen, thumbnailSize} = requestsQueue[0].options - desktopCapturer.startHandling captureWindow, captureScreen, thumbnailSize diff --git a/atom/browser/lib/desktop-capturer.js b/atom/browser/lib/desktop-capturer.js new file mode 100644 index 000000000000..789aa37ebc8d --- /dev/null +++ b/atom/browser/lib/desktop-capturer.js @@ -0,0 +1,85 @@ +var deepEqual, desktopCapturer, ipcMain, requestsQueue; + +ipcMain = require('electron').ipcMain; + +desktopCapturer = process.atomBinding('desktop_capturer').desktopCapturer; + +deepEqual = function(opt1, opt2) { + return JSON.stringify(opt1) === JSON.stringify(opt2); +}; + + +/* A queue for holding all requests from renderer process. */ + +requestsQueue = []; + +ipcMain.on('ATOM_BROWSER_DESKTOP_CAPTURER_GET_SOURCES', function(event, captureWindow, captureScreen, thumbnailSize, id) { + var request; + request = { + id: id, + options: { + captureWindow: captureWindow, + captureScreen: captureScreen, + thumbnailSize: thumbnailSize + }, + webContents: event.sender + }; + requestsQueue.push(request); + if (requestsQueue.length === 1) { + desktopCapturer.startHandling(captureWindow, captureScreen, thumbnailSize); + } + + /* + If the WebContents is destroyed before receiving result, just remove the + reference from requestsQueue to make the module not send the result to it. + */ + return event.sender.once('destroyed', function() { + return request.webContents = null; + }); +}); + +desktopCapturer.emit = function(event, name, sources) { + + /* Receiving sources result from main process, now send them back to renderer. */ + var captureScreen, captureWindow, handledRequest, i, len, ref, ref1, ref2, request, result, source, thumbnailSize, unhandledRequestsQueue; + handledRequest = requestsQueue.shift(0); + result = (function() { + var i, len, results; + results = []; + for (i = 0, len = sources.length; i < len; i++) { + source = sources[i]; + results.push({ + id: source.id, + name: source.name, + thumbnail: source.thumbnail.toDataUrl() + }); + } + return results; + })(); + if ((ref = handledRequest.webContents) != null) { + ref.send("ATOM_RENDERER_DESKTOP_CAPTURER_RESULT_" + handledRequest.id, result); + } + + /* + Check the queue to see whether there is other same request. If has, handle + it for reducing redunplicated `desktopCaptuer.startHandling` calls. + */ + unhandledRequestsQueue = []; + for (i = 0, len = requestsQueue.length; i < len; i++) { + request = requestsQueue[i]; + if (deepEqual(handledRequest.options, request.options)) { + if ((ref1 = request.webContents) != null) { + ref1.send("ATOM_RENDERER_DESKTOP_CAPTURER_RESULT_" + request.id, errorMessage, result); + } + } else { + unhandledRequestsQueue.push(request); + } + } + requestsQueue = unhandledRequestsQueue; + + /* If the requestsQueue is not empty, start a new request handling. */ + if (requestsQueue.length > 0) { + ref2 = requestsQueue[0].options, captureWindow = ref2.captureWindow, captureScreen = ref2.captureScreen, thumbnailSize = ref2.thumbnailSize; + return desktopCapturer.startHandling(captureWindow, captureScreen, thumbnailSize); + } +}; diff --git a/atom/browser/lib/guest-view-manager.coffee b/atom/browser/lib/guest-view-manager.coffee deleted file mode 100644 index 037b9a4db2d6..000000000000 --- a/atom/browser/lib/guest-view-manager.coffee +++ /dev/null @@ -1,177 +0,0 @@ -{ipcMain, webContents} = require 'electron' - -### Doesn't exist in early initialization. ### -webViewManager = null - -supportedWebViewEvents = [ - 'load-commit' - 'did-finish-load' - 'did-fail-load' - 'did-frame-finish-load' - 'did-start-loading' - 'did-stop-loading' - 'did-get-response-details' - 'did-get-redirect-request' - 'dom-ready' - 'console-message' - 'devtools-opened' - 'devtools-closed' - 'devtools-focused' - 'new-window' - 'will-navigate' - 'did-navigate' - 'did-navigate-in-page' - 'close' - 'crashed' - 'gpu-crashed' - 'plugin-crashed' - 'destroyed' - 'page-title-updated' - 'page-favicon-updated' - 'enter-html-full-screen' - 'leave-html-full-screen' - 'media-started-playing' - 'media-paused' - 'found-in-page' - 'did-change-theme-color' -] - -nextInstanceId = 0 -guestInstances = {} -embedderElementsMap = {} -reverseEmbedderElementsMap = {} - -### Moves the last element of array to the first one. ### -moveLastToFirst = (list) -> - list.unshift list.pop() - -### Generate guestInstanceId. ### -getNextInstanceId = (webContents) -> - ++nextInstanceId - -### Create a new guest instance. ### -createGuest = (embedder, params) -> - webViewManager ?= process.atomBinding 'web_view_manager' - - id = getNextInstanceId embedder - guest = webContents.create {isGuest: true, partition: params.partition, embedder} - guestInstances[id] = {guest, embedder} - - ### Destroy guest when the embedder is gone or navigated. ### - destroyEvents = ['destroyed', 'crashed', 'did-navigate'] - destroy = -> - destroyGuest embedder, id if guestInstances[id]? - for event in destroyEvents - embedder.once event, destroy - ### - Users might also listen to the crashed event, so We must ensure the guest - is destroyed before users' listener gets called. It is done by moving our - listener to the first one in queue. - ### - listeners = embedder._events[event] - moveLastToFirst listeners if Array.isArray listeners - guest.once 'destroyed', -> - embedder.removeListener event, destroy for event in destroyEvents - - ### Init guest web view after attached. ### - guest.once 'did-attach', -> - params = @attachParams - delete @attachParams - - @viewInstanceId = params.instanceId - @setSize - normal: - width: params.elementWidth, height: params.elementHeight - enableAutoSize: params.autosize - min: - width: params.minwidth, height: params.minheight - max: - width: params.maxwidth, height: params.maxheight - - if params.src - opts = {} - opts.httpReferrer = params.httpreferrer if params.httpreferrer - opts.userAgent = params.useragent if params.useragent - @loadURL params.src, opts - - if params.allowtransparency? - @setAllowTransparency params.allowtransparency - - guest.allowPopups = params.allowpopups - - ### Dispatch events to embedder. ### - for event in supportedWebViewEvents - do (event) -> - guest.on event, (_, args...) -> - embedder.send "ATOM_SHELL_GUEST_VIEW_INTERNAL_DISPATCH_EVENT-#{guest.viewInstanceId}", event, args... - - ### Dispatch guest's IPC messages to embedder. ### - guest.on 'ipc-message-host', (_, packed) -> - [channel, args...] = packed - embedder.send "ATOM_SHELL_GUEST_VIEW_INTERNAL_IPC_MESSAGE-#{guest.viewInstanceId}", channel, args... - - ### Autosize. ### - guest.on 'size-changed', (_, args...) -> - embedder.send "ATOM_SHELL_GUEST_VIEW_INTERNAL_SIZE_CHANGED-#{guest.viewInstanceId}", args... - - id - -### Attach the guest to an element of embedder. ### -attachGuest = (embedder, elementInstanceId, guestInstanceId, params) -> - guest = guestInstances[guestInstanceId].guest - - ### Destroy the old guest when attaching. ### - key = "#{embedder.getId()}-#{elementInstanceId}" - oldGuestInstanceId = embedderElementsMap[key] - if oldGuestInstanceId? - ### Reattachment to the same guest is not currently supported. ### - return unless oldGuestInstanceId != guestInstanceId - - return unless guestInstances[oldGuestInstanceId]? - destroyGuest embedder, oldGuestInstanceId - - webPreferences = - guestInstanceId: guestInstanceId - nodeIntegration: params.nodeintegration ? false - plugins: params.plugins - webSecurity: !params.disablewebsecurity - webPreferences.preloadURL = params.preload if params.preload - webViewManager.addGuest guestInstanceId, elementInstanceId, embedder, guest, webPreferences - - guest.attachParams = params - embedderElementsMap[key] = guestInstanceId - reverseEmbedderElementsMap[guestInstanceId] = key - -### Destroy an existing guest instance. ### -destroyGuest = (embedder, id) -> - webViewManager.removeGuest embedder, id - guestInstances[id].guest.destroy() - delete guestInstances[id] - - key = reverseEmbedderElementsMap[id] - if key? - delete reverseEmbedderElementsMap[id] - delete embedderElementsMap[key] - -ipcMain.on 'ATOM_SHELL_GUEST_VIEW_MANAGER_CREATE_GUEST', (event, params, requestId) -> - event.sender.send "ATOM_SHELL_RESPONSE_#{requestId}", createGuest(event.sender, params) - -ipcMain.on 'ATOM_SHELL_GUEST_VIEW_MANAGER_ATTACH_GUEST', (event, elementInstanceId, guestInstanceId, params) -> - attachGuest event.sender, elementInstanceId, guestInstanceId, params - -ipcMain.on 'ATOM_SHELL_GUEST_VIEW_MANAGER_DESTROY_GUEST', (event, id) -> - destroyGuest event.sender, id - -ipcMain.on 'ATOM_SHELL_GUEST_VIEW_MANAGER_SET_SIZE', (event, id, params) -> - guestInstances[id]?.guest.setSize params - -ipcMain.on 'ATOM_SHELL_GUEST_VIEW_MANAGER_SET_ALLOW_TRANSPARENCY', (event, id, allowtransparency) -> - guestInstances[id]?.guest.setAllowTransparency allowtransparency - -### Returns WebContents from its guest id. ### -exports.getGuest = (id) -> - guestInstances[id]?.guest - -### Returns the embedder of the guest. ### -exports.getEmbedder = (id) -> - guestInstances[id]?.embedder diff --git a/atom/browser/lib/guest-view-manager.js b/atom/browser/lib/guest-view-manager.js new file mode 100644 index 000000000000..a1d82483a954 --- /dev/null +++ b/atom/browser/lib/guest-view-manager.js @@ -0,0 +1,238 @@ +var attachGuest, createGuest, destroyGuest, embedderElementsMap, getNextInstanceId, guestInstances, ipcMain, moveLastToFirst, nextInstanceId, ref, reverseEmbedderElementsMap, supportedWebViewEvents, webContents, webViewManager, + slice = [].slice; + +ref = require('electron'), ipcMain = ref.ipcMain, webContents = ref.webContents; + + +/* Doesn't exist in early initialization. */ + +webViewManager = null; + +supportedWebViewEvents = ['load-commit', 'did-finish-load', 'did-fail-load', 'did-frame-finish-load', 'did-start-loading', 'did-stop-loading', 'did-get-response-details', 'did-get-redirect-request', 'dom-ready', 'console-message', 'devtools-opened', 'devtools-closed', 'devtools-focused', 'new-window', 'will-navigate', 'did-navigate', 'did-navigate-in-page', 'close', 'crashed', 'gpu-crashed', 'plugin-crashed', 'destroyed', 'page-title-updated', 'page-favicon-updated', 'enter-html-full-screen', 'leave-html-full-screen', 'media-started-playing', 'media-paused', 'found-in-page', 'did-change-theme-color']; + +nextInstanceId = 0; + +guestInstances = {}; + +embedderElementsMap = {}; + +reverseEmbedderElementsMap = {}; + + +/* Moves the last element of array to the first one. */ + +moveLastToFirst = function(list) { + return list.unshift(list.pop()); +}; + + +/* Generate guestInstanceId. */ + +getNextInstanceId = function(webContents) { + return ++nextInstanceId; +}; + + +/* Create a new guest instance. */ + +createGuest = function(embedder, params) { + var destroy, destroyEvents, event, fn, guest, i, id, j, len, len1, listeners; + if (webViewManager == null) { + webViewManager = process.atomBinding('web_view_manager'); + } + id = getNextInstanceId(embedder); + guest = webContents.create({ + isGuest: true, + partition: params.partition, + embedder: embedder + }); + guestInstances[id] = { + guest: guest, + embedder: embedder + }; + + /* Destroy guest when the embedder is gone or navigated. */ + destroyEvents = ['destroyed', 'crashed', 'did-navigate']; + destroy = function() { + if (guestInstances[id] != null) { + return destroyGuest(embedder, id); + } + }; + for (i = 0, len = destroyEvents.length; i < len; i++) { + event = destroyEvents[i]; + embedder.once(event, destroy); + + /* + Users might also listen to the crashed event, so We must ensure the guest + is destroyed before users' listener gets called. It is done by moving our + listener to the first one in queue. + */ + listeners = embedder._events[event]; + if (Array.isArray(listeners)) { + moveLastToFirst(listeners); + } + } + guest.once('destroyed', function() { + var j, len1, results; + results = []; + for (j = 0, len1 = destroyEvents.length; j < len1; j++) { + event = destroyEvents[j]; + results.push(embedder.removeListener(event, destroy)); + } + return results; + }); + + /* Init guest web view after attached. */ + guest.once('did-attach', function() { + var opts; + params = this.attachParams; + delete this.attachParams; + this.viewInstanceId = params.instanceId; + this.setSize({ + normal: { + width: params.elementWidth, + height: params.elementHeight + }, + enableAutoSize: params.autosize, + min: { + width: params.minwidth, + height: params.minheight + }, + max: { + width: params.maxwidth, + height: params.maxheight + } + }); + if (params.src) { + opts = {}; + if (params.httpreferrer) { + opts.httpReferrer = params.httpreferrer; + } + if (params.useragent) { + opts.userAgent = params.useragent; + } + this.loadURL(params.src, opts); + } + if (params.allowtransparency != null) { + this.setAllowTransparency(params.allowtransparency); + } + return guest.allowPopups = params.allowpopups; + }); + + /* Dispatch events to embedder. */ + fn = function(event) { + return guest.on(event, function() { + var _, args; + _ = arguments[0], args = 2 <= arguments.length ? slice.call(arguments, 1) : []; + return embedder.send.apply(embedder, ["ATOM_SHELL_GUEST_VIEW_INTERNAL_DISPATCH_EVENT-" + guest.viewInstanceId, event].concat(slice.call(args))); + }); + }; + for (j = 0, len1 = supportedWebViewEvents.length; j < len1; j++) { + event = supportedWebViewEvents[j]; + fn(event); + } + + /* Dispatch guest's IPC messages to embedder. */ + guest.on('ipc-message-host', function(_, packed) { + var args, channel; + channel = packed[0], args = 2 <= packed.length ? slice.call(packed, 1) : []; + return embedder.send.apply(embedder, ["ATOM_SHELL_GUEST_VIEW_INTERNAL_IPC_MESSAGE-" + guest.viewInstanceId, channel].concat(slice.call(args))); + }); + + /* Autosize. */ + guest.on('size-changed', function() { + var _, args; + _ = arguments[0], args = 2 <= arguments.length ? slice.call(arguments, 1) : []; + return embedder.send.apply(embedder, ["ATOM_SHELL_GUEST_VIEW_INTERNAL_SIZE_CHANGED-" + guest.viewInstanceId].concat(slice.call(args))); + }); + return id; +}; + + +/* Attach the guest to an element of embedder. */ + +attachGuest = function(embedder, elementInstanceId, guestInstanceId, params) { + var guest, key, oldGuestInstanceId, ref1, webPreferences; + guest = guestInstances[guestInstanceId].guest; + + /* Destroy the old guest when attaching. */ + key = (embedder.getId()) + "-" + elementInstanceId; + oldGuestInstanceId = embedderElementsMap[key]; + if (oldGuestInstanceId != null) { + + /* Reattachment to the same guest is not currently supported. */ + if (oldGuestInstanceId === guestInstanceId) { + return; + } + if (guestInstances[oldGuestInstanceId] == null) { + return; + } + destroyGuest(embedder, oldGuestInstanceId); + } + webPreferences = { + guestInstanceId: guestInstanceId, + nodeIntegration: (ref1 = params.nodeintegration) != null ? ref1 : false, + plugins: params.plugins, + webSecurity: !params.disablewebsecurity + }; + if (params.preload) { + webPreferences.preloadURL = params.preload; + } + webViewManager.addGuest(guestInstanceId, elementInstanceId, embedder, guest, webPreferences); + guest.attachParams = params; + embedderElementsMap[key] = guestInstanceId; + return reverseEmbedderElementsMap[guestInstanceId] = key; +}; + + +/* Destroy an existing guest instance. */ + +destroyGuest = function(embedder, id) { + var key; + webViewManager.removeGuest(embedder, id); + guestInstances[id].guest.destroy(); + delete guestInstances[id]; + key = reverseEmbedderElementsMap[id]; + if (key != null) { + delete reverseEmbedderElementsMap[id]; + return delete embedderElementsMap[key]; + } +}; + +ipcMain.on('ATOM_SHELL_GUEST_VIEW_MANAGER_CREATE_GUEST', function(event, params, requestId) { + return event.sender.send("ATOM_SHELL_RESPONSE_" + requestId, createGuest(event.sender, params)); +}); + +ipcMain.on('ATOM_SHELL_GUEST_VIEW_MANAGER_ATTACH_GUEST', function(event, elementInstanceId, guestInstanceId, params) { + return attachGuest(event.sender, elementInstanceId, guestInstanceId, params); +}); + +ipcMain.on('ATOM_SHELL_GUEST_VIEW_MANAGER_DESTROY_GUEST', function(event, id) { + return destroyGuest(event.sender, id); +}); + +ipcMain.on('ATOM_SHELL_GUEST_VIEW_MANAGER_SET_SIZE', function(event, id, params) { + var ref1; + return (ref1 = guestInstances[id]) != null ? ref1.guest.setSize(params) : void 0; +}); + +ipcMain.on('ATOM_SHELL_GUEST_VIEW_MANAGER_SET_ALLOW_TRANSPARENCY', function(event, id, allowtransparency) { + var ref1; + return (ref1 = guestInstances[id]) != null ? ref1.guest.setAllowTransparency(allowtransparency) : void 0; +}); + + +/* Returns WebContents from its guest id. */ + +exports.getGuest = function(id) { + var ref1; + return (ref1 = guestInstances[id]) != null ? ref1.guest : void 0; +}; + + +/* Returns the embedder of the guest. */ + +exports.getEmbedder = function(id) { + var ref1; + return (ref1 = guestInstances[id]) != null ? ref1.embedder : void 0; +}; diff --git a/atom/browser/lib/guest-window-manager.coffee b/atom/browser/lib/guest-window-manager.coffee deleted file mode 100644 index 101bd79951b7..000000000000 --- a/atom/browser/lib/guest-window-manager.coffee +++ /dev/null @@ -1,88 +0,0 @@ -{ipcMain, BrowserWindow} = require 'electron' -v8Util = process.atomBinding 'v8_util' - -frameToGuest = {} - -### Copy attribute of |parent| to |child| if it is not defined in |child|. ### -mergeOptions = (child, parent) -> - for own key, value of parent when key not of child - if typeof value is 'object' - child[key] = mergeOptions {}, value - else - child[key] = value - child - -### Merge |options| with the |embedder|'s window's options. ### -mergeBrowserWindowOptions = (embedder, options) -> - if embedder.browserWindowOptions? - ### Inherit the original options if it is a BrowserWindow. ### - mergeOptions options, embedder.browserWindowOptions - else - ### Or only inherit web-preferences if it is a webview. ### - options.webPreferences ?= {} - mergeOptions options.webPreferences, embedder.getWebPreferences() - options - -### Create a new guest created by |embedder| with |options|. ### -createGuest = (embedder, url, frameName, options) -> - guest = frameToGuest[frameName] - if frameName and guest? - guest.loadURL url - return guest.id - - ### Remember the embedder window's id. ### - options.webPreferences ?= {} - options.webPreferences.openerId = BrowserWindow.fromWebContents(embedder)?.id - - guest = new BrowserWindow(options) - guest.loadURL url - - ### - When |embedder| is destroyed we should also destroy attached guest, and if - guest is closed by user then we should prevent |embedder| from double - closing guest. - ### - guestId = guest.id - closedByEmbedder = -> - guest.removeListener 'closed', closedByUser - guest.destroy() - closedByUser = -> - embedder.send "ATOM_SHELL_GUEST_WINDOW_MANAGER_WINDOW_CLOSED_#{guestId}" - embedder.removeListener 'render-view-deleted', closedByEmbedder - embedder.once 'render-view-deleted', closedByEmbedder - guest.once 'closed', closedByUser - - if frameName - frameToGuest[frameName] = guest - guest.frameName = frameName - guest.once 'closed', -> - delete frameToGuest[frameName] - - guest.id - -### Routed window.open messages. ### -ipcMain.on 'ATOM_SHELL_GUEST_WINDOW_MANAGER_WINDOW_OPEN', (event, args...) -> - [url, frameName, options] = args - options = mergeBrowserWindowOptions event.sender, options - event.sender.emit 'new-window', event, url, frameName, 'new-window', options - if (event.sender.isGuest() and not event.sender.allowPopups) or event.defaultPrevented - event.returnValue = null - else - event.returnValue = createGuest event.sender, url, frameName, options - -ipcMain.on 'ATOM_SHELL_GUEST_WINDOW_MANAGER_WINDOW_CLOSE', (event, guestId) -> - BrowserWindow.fromId(guestId)?.destroy() - -ipcMain.on 'ATOM_SHELL_GUEST_WINDOW_MANAGER_WINDOW_METHOD', (event, guestId, method, args...) -> - BrowserWindow.fromId(guestId)?[method] args... - -ipcMain.on 'ATOM_SHELL_GUEST_WINDOW_MANAGER_WINDOW_POSTMESSAGE', (event, guestId, message, targetOrigin, sourceOrigin) -> - sourceId = BrowserWindow.fromWebContents(event.sender)?.id - return unless sourceId? - - guestContents = BrowserWindow.fromId(guestId)?.webContents - if guestContents?.getURL().indexOf(targetOrigin) is 0 or targetOrigin is '*' - guestContents?.send 'ATOM_SHELL_GUEST_WINDOW_POSTMESSAGE', sourceId, message, sourceOrigin - -ipcMain.on 'ATOM_SHELL_GUEST_WINDOW_MANAGER_WEB_CONTENTS_METHOD', (event, guestId, method, args...) -> - BrowserWindow.fromId(guestId)?.webContents?[method] args... diff --git a/atom/browser/lib/guest-window-manager.js b/atom/browser/lib/guest-window-manager.js new file mode 100644 index 000000000000..5b1290c282ec --- /dev/null +++ b/atom/browser/lib/guest-window-manager.js @@ -0,0 +1,137 @@ +var BrowserWindow, createGuest, frameToGuest, ipcMain, mergeBrowserWindowOptions, mergeOptions, ref, v8Util, + hasProp = {}.hasOwnProperty, + slice = [].slice; + +ref = require('electron'), ipcMain = ref.ipcMain, BrowserWindow = ref.BrowserWindow; + +v8Util = process.atomBinding('v8_util'); + +frameToGuest = {}; + + +/* Copy attribute of |parent| to |child| if it is not defined in |child|. */ + +mergeOptions = function(child, parent) { + var key, value; + for (key in parent) { + if (!hasProp.call(parent, key)) continue; + value = parent[key]; + if (!(key in child)) { + if (typeof value === 'object') { + child[key] = mergeOptions({}, value); + } else { + child[key] = value; + } + } + } + return child; +}; + + +/* Merge |options| with the |embedder|'s window's options. */ + +mergeBrowserWindowOptions = function(embedder, options) { + if (embedder.browserWindowOptions != null) { + + /* Inherit the original options if it is a BrowserWindow. */ + mergeOptions(options, embedder.browserWindowOptions); + } else { + + /* Or only inherit web-preferences if it is a webview. */ + if (options.webPreferences == null) { + options.webPreferences = {}; + } + mergeOptions(options.webPreferences, embedder.getWebPreferences()); + } + return options; +}; + + +/* Create a new guest created by |embedder| with |options|. */ + +createGuest = function(embedder, url, frameName, options) { + var closedByEmbedder, closedByUser, guest, guestId, ref1; + guest = frameToGuest[frameName]; + if (frameName && (guest != null)) { + guest.loadURL(url); + return guest.id; + } + + /* Remember the embedder window's id. */ + if (options.webPreferences == null) { + options.webPreferences = {}; + } + options.webPreferences.openerId = (ref1 = BrowserWindow.fromWebContents(embedder)) != null ? ref1.id : void 0; + guest = new BrowserWindow(options); + guest.loadURL(url); + + /* + When |embedder| is destroyed we should also destroy attached guest, and if + guest is closed by user then we should prevent |embedder| from double + closing guest. + */ + guestId = guest.id; + closedByEmbedder = function() { + guest.removeListener('closed', closedByUser); + return guest.destroy(); + }; + closedByUser = function() { + embedder.send("ATOM_SHELL_GUEST_WINDOW_MANAGER_WINDOW_CLOSED_" + guestId); + return embedder.removeListener('render-view-deleted', closedByEmbedder); + }; + embedder.once('render-view-deleted', closedByEmbedder); + guest.once('closed', closedByUser); + if (frameName) { + frameToGuest[frameName] = guest; + guest.frameName = frameName; + guest.once('closed', function() { + return delete frameToGuest[frameName]; + }); + } + return guest.id; +}; + + +/* Routed window.open messages. */ + +ipcMain.on('ATOM_SHELL_GUEST_WINDOW_MANAGER_WINDOW_OPEN', function() { + var args, event, frameName, options, url; + event = arguments[0], args = 2 <= arguments.length ? slice.call(arguments, 1) : []; + url = args[0], frameName = args[1], options = args[2]; + options = mergeBrowserWindowOptions(event.sender, options); + event.sender.emit('new-window', event, url, frameName, 'new-window', options); + if ((event.sender.isGuest() && !event.sender.allowPopups) || event.defaultPrevented) { + return event.returnValue = null; + } else { + return event.returnValue = createGuest(event.sender, url, frameName, options); + } +}); + +ipcMain.on('ATOM_SHELL_GUEST_WINDOW_MANAGER_WINDOW_CLOSE', function(event, guestId) { + var ref1; + return (ref1 = BrowserWindow.fromId(guestId)) != null ? ref1.destroy() : void 0; +}); + +ipcMain.on('ATOM_SHELL_GUEST_WINDOW_MANAGER_WINDOW_METHOD', function() { + var args, event, guestId, method, ref1; + event = arguments[0], guestId = arguments[1], method = arguments[2], args = 4 <= arguments.length ? slice.call(arguments, 3) : []; + return (ref1 = BrowserWindow.fromId(guestId)) != null ? ref1[method].apply(ref1, args) : void 0; +}); + +ipcMain.on('ATOM_SHELL_GUEST_WINDOW_MANAGER_WINDOW_POSTMESSAGE', function(event, guestId, message, targetOrigin, sourceOrigin) { + var guestContents, ref1, ref2, sourceId; + sourceId = (ref1 = BrowserWindow.fromWebContents(event.sender)) != null ? ref1.id : void 0; + if (sourceId == null) { + return; + } + guestContents = (ref2 = BrowserWindow.fromId(guestId)) != null ? ref2.webContents : void 0; + if ((guestContents != null ? guestContents.getURL().indexOf(targetOrigin) : void 0) === 0 || targetOrigin === '*') { + return guestContents != null ? guestContents.send('ATOM_SHELL_GUEST_WINDOW_POSTMESSAGE', sourceId, message, sourceOrigin) : void 0; + } +}); + +ipcMain.on('ATOM_SHELL_GUEST_WINDOW_MANAGER_WEB_CONTENTS_METHOD', function() { + var args, event, guestId, method, ref1, ref2; + event = arguments[0], guestId = arguments[1], method = arguments[2], args = 4 <= arguments.length ? slice.call(arguments, 3) : []; + return (ref1 = BrowserWindow.fromId(guestId)) != null ? (ref2 = ref1.webContents) != null ? ref2[method].apply(ref2, args) : void 0 : void 0; +}); diff --git a/atom/browser/lib/init.coffee b/atom/browser/lib/init.coffee deleted file mode 100644 index ace81d50148d..000000000000 --- a/atom/browser/lib/init.coffee +++ /dev/null @@ -1,120 +0,0 @@ -fs = require 'fs' -path = require 'path' -util = require 'util' -Module = require 'module' - -### We modified the original process.argv to let node.js load the atom.js, ### -### we need to restore it here. ### -process.argv.splice 1, 1 - -### Clear search paths. ### -require path.resolve(__dirname, '..', '..', 'common', 'lib', 'reset-search-paths') - -### Import common settings. ### -require path.resolve(__dirname, '..', '..', 'common', 'lib', 'init') - -globalPaths = Module.globalPaths -unless process.env.ELECTRON_HIDE_INTERNAL_MODULES - globalPaths.push path.resolve(__dirname, '..', 'api', 'lib') - -### Expose public APIs. ### -globalPaths.push path.resolve(__dirname, '..', 'api', 'lib', 'exports') - -if process.platform is 'win32' - ### - Redirect node's console to use our own implementations, since node can not - handle console output when running as GUI program. - ### - consoleLog = (args...) -> - process.log util.format(args...) + "\n" - streamWrite = (chunk, encoding, callback) -> - chunk = chunk.toString(encoding) if Buffer.isBuffer chunk - process.log chunk - callback() if callback - true - console.log = console.error = console.warn = consoleLog - process.stdout.write = process.stderr.write = streamWrite - - ### Always returns EOF for stdin stream. ### - Readable = require('stream').Readable - stdin = new Readable - stdin.push null - process.__defineGetter__ 'stdin', -> stdin - -### Don't quit on fatal error. ### -process.on 'uncaughtException', (error) -> - ### Do nothing if the user has a custom uncaught exception handler. ### - if process.listeners('uncaughtException').length > 1 - return - - ### Show error in GUI. ### - {dialog} = require 'electron' - stack = error.stack ? "#{error.name}: #{error.message}" - message = "Uncaught Exception:\n#{stack}" - dialog.showErrorBox 'A JavaScript error occurred in the main process', message - -### Emit 'exit' event on quit. ### -{app} = require 'electron' -app.on 'quit', (event, exitCode) -> - process.emit 'exit', exitCode - -### Map process.exit to app.exit, which quits gracefully. ### -process.exit = app.exit - -### Load the RPC server. ### -require './rpc-server' - -### Load the guest view manager. ### -require './guest-view-manager' -require './guest-window-manager' - -### Now we try to load app's package.json. ### -packageJson = null - -searchPaths = [ 'app', 'app.asar', 'default_app' ] -for packagePath in searchPaths - try - packagePath = path.join process.resourcesPath, packagePath - packageJson = JSON.parse(fs.readFileSync(path.join(packagePath, 'package.json'))) - break - catch e - continue - -unless packageJson? - process.nextTick -> process.exit 1 - throw new Error("Unable to find a valid app") - -### Set application's version. ### -app.setVersion packageJson.version if packageJson.version? - -### Set application's name. ### -if packageJson.productName? - app.setName packageJson.productName -else if packageJson.name? - app.setName packageJson.name - -### Set application's desktop name. ### -if packageJson.desktopName? - app.setDesktopName packageJson.desktopName -else - app.setDesktopName "#{app.getName()}.desktop" - -### Chrome 42 disables NPAPI plugins by default, reenable them here ### -app.commandLine.appendSwitch 'enable-npapi' - -### Set the user path according to application's name. ### -app.setPath 'userData', path.join(app.getPath('appData'), app.getName()) -app.setPath 'userCache', path.join(app.getPath('cache'), app.getName()) -app.setAppPath packagePath - -### Load the chrome extension support. ### -require './chrome-extension' - -### Load internal desktop-capturer module. ### -require './desktop-capturer' - -### Set main startup script of the app. ### -mainStartupScript = packageJson.main or 'index.js' - -### Finally load app's main.js and transfer control to C++. ### -Module._load path.join(packagePath, mainStartupScript), Module, true diff --git a/atom/browser/lib/init.js b/atom/browser/lib/init.js new file mode 100644 index 000000000000..ea44e2c9b04e --- /dev/null +++ b/atom/browser/lib/init.js @@ -0,0 +1,201 @@ +var Module, Readable, app, consoleLog, e, error1, fs, globalPaths, i, len, mainStartupScript, packageJson, packagePath, path, searchPaths, stdin, streamWrite, util, + slice = [].slice; + +fs = require('fs'); + +path = require('path'); + +util = require('util'); + +Module = require('module'); + + +/* We modified the original process.argv to let node.js load the atom.js, */ + + +/* we need to restore it here. */ + +process.argv.splice(1, 1); + + +/* Clear search paths. */ + +require(path.resolve(__dirname, '..', '..', 'common', 'lib', 'reset-search-paths')); + + +/* Import common settings. */ + +require(path.resolve(__dirname, '..', '..', 'common', 'lib', 'init')); + +globalPaths = Module.globalPaths; + +if (!process.env.ELECTRON_HIDE_INTERNAL_MODULES) { + globalPaths.push(path.resolve(__dirname, '..', 'api', 'lib')); +} + + +/* Expose public APIs. */ + +globalPaths.push(path.resolve(__dirname, '..', 'api', 'lib', 'exports')); + +if (process.platform === 'win32') { + + /* + Redirect node's console to use our own implementations, since node can not + handle console output when running as GUI program. + */ + consoleLog = function() { + var args; + args = 1 <= arguments.length ? slice.call(arguments, 0) : []; + return process.log(util.format.apply(util, args) + "\n"); + }; + streamWrite = function(chunk, encoding, callback) { + if (Buffer.isBuffer(chunk)) { + chunk = chunk.toString(encoding); + } + process.log(chunk); + if (callback) { + callback(); + } + return true; + }; + console.log = console.error = console.warn = consoleLog; + process.stdout.write = process.stderr.write = streamWrite; + + /* Always returns EOF for stdin stream. */ + Readable = require('stream').Readable; + stdin = new Readable; + stdin.push(null); + process.__defineGetter__('stdin', function() { + return stdin; + }); +} + + +/* Don't quit on fatal error. */ + +process.on('uncaughtException', function(error) { + + /* Do nothing if the user has a custom uncaught exception handler. */ + var dialog, message, ref, stack; + if (process.listeners('uncaughtException').length > 1) { + return; + } + + /* Show error in GUI. */ + dialog = require('electron').dialog; + stack = (ref = error.stack) != null ? ref : error.name + ": " + error.message; + message = "Uncaught Exception:\n" + stack; + return dialog.showErrorBox('A JavaScript error occurred in the main process', message); +}); + + +/* Emit 'exit' event on quit. */ + +app = require('electron').app; + +app.on('quit', function(event, exitCode) { + return process.emit('exit', exitCode); +}); + + +/* Map process.exit to app.exit, which quits gracefully. */ + +process.exit = app.exit; + + +/* Load the RPC server. */ + +require('./rpc-server'); + + +/* Load the guest view manager. */ + +require('./guest-view-manager'); + +require('./guest-window-manager'); + + +/* Now we try to load app's package.json. */ + +packageJson = null; + +searchPaths = ['app', 'app.asar', 'default_app']; + +for (i = 0, len = searchPaths.length; i < len; i++) { + packagePath = searchPaths[i]; + try { + packagePath = path.join(process.resourcesPath, packagePath); + packageJson = JSON.parse(fs.readFileSync(path.join(packagePath, 'package.json'))); + break; + } catch (error1) { + e = error1; + continue; + } +} + +if (packageJson == null) { + process.nextTick(function() { + return process.exit(1); + }); + throw new Error("Unable to find a valid app"); +} + + +/* Set application's version. */ + +if (packageJson.version != null) { + app.setVersion(packageJson.version); +} + + +/* Set application's name. */ + +if (packageJson.productName != null) { + app.setName(packageJson.productName); +} else if (packageJson.name != null) { + app.setName(packageJson.name); +} + + +/* Set application's desktop name. */ + +if (packageJson.desktopName != null) { + app.setDesktopName(packageJson.desktopName); +} else { + app.setDesktopName((app.getName()) + ".desktop"); +} + + +/* Chrome 42 disables NPAPI plugins by default, reenable them here */ + +app.commandLine.appendSwitch('enable-npapi'); + + +/* Set the user path according to application's name. */ + +app.setPath('userData', path.join(app.getPath('appData'), app.getName())); + +app.setPath('userCache', path.join(app.getPath('cache'), app.getName())); + +app.setAppPath(packagePath); + + +/* Load the chrome extension support. */ + +require('./chrome-extension'); + + +/* Load internal desktop-capturer module. */ + +require('./desktop-capturer'); + + +/* Set main startup script of the app. */ + +mainStartupScript = packageJson.main || 'index.js'; + + +/* Finally load app's main.js and transfer control to C++. */ + +Module._load(path.join(packagePath, mainStartupScript), Module, true); diff --git a/atom/browser/lib/objects-registry.coffee b/atom/browser/lib/objects-registry.coffee deleted file mode 100644 index 661ee4958a1b..000000000000 --- a/atom/browser/lib/objects-registry.coffee +++ /dev/null @@ -1,73 +0,0 @@ -{EventEmitter} = require 'events' -v8Util = process.atomBinding 'v8_util' - -class ObjectsRegistry extends EventEmitter - constructor: -> - @setMaxListeners Number.MAX_VALUE - @nextId = 0 - - ### - Stores all objects by ref-counting. - (id) => {object, count} - ### - @storage = {} - - ### - Stores the IDs of objects referenced by WebContents. - (webContentsId) => {(id) => (count)} - ### - @owners = {} - - ### - Register a new object, the object would be kept referenced until you release - it explicitly. - ### - add: (webContentsId, obj) -> - id = @saveToStorage obj - ### Remember the owner. ### - @owners[webContentsId] ?= {} - @owners[webContentsId][id] ?= 0 - @owners[webContentsId][id]++ - ### Returns object's id ### - id - - ### Get an object according to its ID. ### - get: (id) -> - @storage[id]?.object - - ### Dereference an object according to its ID. ### - remove: (webContentsId, id) -> - @dereference id, 1 - ### Also reduce the count in owner. ### - pointer = @owners[webContentsId] - return unless pointer? - --pointer[id] - delete pointer[id] if pointer[id] is 0 - - ### Clear all references to objects refrenced by the WebContents. ### - clear: (webContentsId) -> - @emit "clear-#{webContentsId}" - return unless @owners[webContentsId]? - @dereference id, count for id, count of @owners[webContentsId] - delete @owners[webContentsId] - - ### Private: Saves the object into storage and assigns an ID for it. ### - saveToStorage: (object) -> - id = v8Util.getHiddenValue object, 'atomId' - unless id - id = ++@nextId - @storage[id] = {count: 0, object} - v8Util.setHiddenValue object, 'atomId', id - ++@storage[id].count - id - - ### Private: Dereference the object from store. ### - dereference: (id, count) -> - pointer = @storage[id] - return unless pointer? - pointer.count -= count - if pointer.count is 0 - v8Util.deleteHiddenValue pointer.object, 'atomId' - delete @storage[id] - -module.exports = new ObjectsRegistry diff --git a/atom/browser/lib/objects-registry.js b/atom/browser/lib/objects-registry.js new file mode 100644 index 000000000000..81fe1c2398f1 --- /dev/null +++ b/atom/browser/lib/objects-registry.js @@ -0,0 +1,133 @@ +var EventEmitter, ObjectsRegistry, v8Util, + extend = function(child, parent) { for (var key in parent) { if (hasProp.call(parent, key)) child[key] = parent[key]; } function ctor() { this.constructor = child; } ctor.prototype = parent.prototype; child.prototype = new ctor(); child.__super__ = parent.prototype; return child; }, + hasProp = {}.hasOwnProperty; + +EventEmitter = require('events').EventEmitter; + +v8Util = process.atomBinding('v8_util'); + +ObjectsRegistry = (function(superClass) { + extend(ObjectsRegistry, superClass); + + function ObjectsRegistry() { + this.setMaxListeners(Number.MAX_VALUE); + this.nextId = 0; + + /* + Stores all objects by ref-counting. + (id) => {object, count} + */ + this.storage = {}; + + /* + Stores the IDs of objects referenced by WebContents. + (webContentsId) => {(id) => (count)} + */ + this.owners = {}; + } + + + /* + Register a new object, the object would be kept referenced until you release + it explicitly. + */ + + ObjectsRegistry.prototype.add = function(webContentsId, obj) { + var base, base1, id; + id = this.saveToStorage(obj); + + /* Remember the owner. */ + if ((base = this.owners)[webContentsId] == null) { + base[webContentsId] = {}; + } + if ((base1 = this.owners[webContentsId])[id] == null) { + base1[id] = 0; + } + this.owners[webContentsId][id]++; + + /* Returns object's id */ + return id; + }; + + + /* Get an object according to its ID. */ + + ObjectsRegistry.prototype.get = function(id) { + var ref; + return (ref = this.storage[id]) != null ? ref.object : void 0; + }; + + + /* Dereference an object according to its ID. */ + + ObjectsRegistry.prototype.remove = function(webContentsId, id) { + var pointer; + this.dereference(id, 1); + + /* Also reduce the count in owner. */ + pointer = this.owners[webContentsId]; + if (pointer == null) { + return; + } + --pointer[id]; + if (pointer[id] === 0) { + return delete pointer[id]; + } + }; + + + /* Clear all references to objects refrenced by the WebContents. */ + + ObjectsRegistry.prototype.clear = function(webContentsId) { + var count, id, ref; + this.emit("clear-" + webContentsId); + if (this.owners[webContentsId] == null) { + return; + } + ref = this.owners[webContentsId]; + for (id in ref) { + count = ref[id]; + this.dereference(id, count); + } + return delete this.owners[webContentsId]; + }; + + + /* Private: Saves the object into storage and assigns an ID for it. */ + + ObjectsRegistry.prototype.saveToStorage = function(object) { + var id; + id = v8Util.getHiddenValue(object, 'atomId'); + if (!id) { + id = ++this.nextId; + this.storage[id] = { + count: 0, + object: object + }; + v8Util.setHiddenValue(object, 'atomId', id); + } + ++this.storage[id].count; + return id; + }; + + + /* Private: Dereference the object from store. */ + + ObjectsRegistry.prototype.dereference = function(id, count) { + var pointer; + pointer = this.storage[id]; + if (pointer == null) { + return; + } + pointer.count -= count; + if (pointer.count === 0) { + v8Util.deleteHiddenValue(pointer.object, 'atomId'); + return delete this.storage[id]; + } + }; + + return ObjectsRegistry; + +})(EventEmitter); + +module.exports = new ObjectsRegistry; diff --git a/atom/browser/lib/rpc-server.coffee b/atom/browser/lib/rpc-server.coffee deleted file mode 100644 index 7880c6ce4a33..000000000000 --- a/atom/browser/lib/rpc-server.coffee +++ /dev/null @@ -1,239 +0,0 @@ -path = require 'path' - -electron = require 'electron' -{ipcMain} = electron -objectsRegistry = require './objects-registry' - -v8Util = process.atomBinding 'v8_util' -{IDWeakMap} = process.atomBinding 'id_weak_map' - -### Convert a real value into meta data. ### -valueToMeta = (sender, value, optimizeSimpleObject=false) -> - meta = type: typeof value - - meta.type = 'buffer' if Buffer.isBuffer value - meta.type = 'value' if value is null - meta.type = 'array' if Array.isArray value - meta.type = 'error' if value instanceof Error - meta.type = 'date' if value instanceof Date - meta.type = 'promise' if value?.constructor.name is 'Promise' - - ### Treat simple objects as value. ### - if optimizeSimpleObject and meta.type is 'object' and v8Util.getHiddenValue value, 'simple' - meta.type = 'value' - - ### Treat the arguments object as array. ### - meta.type = 'array' if meta.type is 'object' and value.callee? and value.length? - - if meta.type is 'array' - meta.members = [] - meta.members.push valueToMeta(sender, el) for el in value - else if meta.type is 'object' or meta.type is 'function' - meta.name = value.constructor.name - - ### - Reference the original value if it's an object, because when it's - passed to renderer we would assume the renderer keeps a reference of - it. - ### - meta.id = objectsRegistry.add sender.getId(), value - - meta.members = ({name, type: typeof field} for name, field of value) - else if meta.type is 'buffer' - meta.value = Array::slice.call value, 0 - else if meta.type is 'promise' - meta.then = valueToMeta sender, value.then.bind(value) - else if meta.type is 'error' - meta.members = plainObjectToMeta value - ### Error.name is not part of own properties. ### - meta.members.push {name: 'name', value: value.name} - else if meta.type is 'date' - meta.value = value.getTime() - else - meta.type = 'value' - meta.value = value - - meta - -### Convert object to meta by value. ### -plainObjectToMeta = (obj) -> - Object.getOwnPropertyNames(obj).map (name) -> {name, value: obj[name]} - -### Convert Error into meta data. ### -exceptionToMeta = (error) -> - type: 'exception', message: error.message, stack: (error.stack || error) - -### Convert array of meta data from renderer into array of real values. ### -unwrapArgs = (sender, args) -> - metaToValue = (meta) -> - switch meta.type - when 'value' then meta.value - when 'remote-object' then objectsRegistry.get meta.id - when 'array' then unwrapArgs sender, meta.value - when 'buffer' then new Buffer(meta.value) - when 'date' then new Date(meta.value) - when 'promise' then Promise.resolve(then: metaToValue(meta.then)) - when 'object' - ret = v8Util.createObjectWithName meta.name - for member in meta.members - ret[member.name] = metaToValue(member.value) - ret - when 'function-with-return-value' - returnValue = metaToValue meta.value - -> returnValue - when 'function' - ### Cache the callbacks in renderer. ### - unless sender.callbacks - sender.callbacks = new IDWeakMap - sender.on 'render-view-deleted', -> - sender.callbacks.clear() - return sender.callbacks.get meta.id if sender.callbacks.has meta.id - - rendererReleased = false - objectsRegistry.once "clear-#{sender.getId()}", -> - rendererReleased = true - - ret = -> - if rendererReleased - throw new Error("Attempting to call a function in a renderer window - that has been closed or released. Function provided here: #{meta.location}.") - sender.send 'ATOM_RENDERER_CALLBACK', meta.id, valueToMeta(sender, arguments) - v8Util.setDestructor ret, -> - return if rendererReleased - sender.callbacks.remove meta.id - sender.send 'ATOM_RENDERER_RELEASE_CALLBACK', meta.id - sender.callbacks.set meta.id, ret - ret - else throw new TypeError("Unknown type: #{meta.type}") - - args.map metaToValue - -### - Call a function and send reply asynchronously if it's a an asynchronous - style function and the caller didn't pass a callback. -### -callFunction = (event, func, caller, args) -> - funcMarkedAsync = v8Util.getHiddenValue(func, 'asynchronous') - funcPassedCallback = typeof args[args.length - 1] is 'function' - - try - if funcMarkedAsync and not funcPassedCallback - args.push (ret) -> - event.returnValue = valueToMeta event.sender, ret, true - func.apply caller, args - else - ret = func.apply caller, args - event.returnValue = valueToMeta event.sender, ret, true - catch e - ### - Catch functions thrown further down in function invocation and wrap - them with the function name so it's easier to trace things like - `Error processing argument -1.` - ### - funcName = func.name ? "anonymous" - throw new Error("Could not call remote function `#{funcName}`. - Check that the function signature is correct. - Underlying error: #{e.message}") - -### Send by BrowserWindow when its render view is deleted. ### -process.on 'ATOM_BROWSER_RELEASE_RENDER_VIEW', (id) -> - objectsRegistry.clear id - -ipcMain.on 'ATOM_BROWSER_REQUIRE', (event, module) -> - try - event.returnValue = valueToMeta event.sender, process.mainModule.require(module) - catch e - event.returnValue = exceptionToMeta e - -ipcMain.on 'ATOM_BROWSER_GET_BUILTIN', (event, module) -> - try - event.returnValue = valueToMeta event.sender, electron[module] - catch e - event.returnValue = exceptionToMeta e - -ipcMain.on 'ATOM_BROWSER_GLOBAL', (event, name) -> - try - event.returnValue = valueToMeta event.sender, global[name] - catch e - event.returnValue = exceptionToMeta e - -ipcMain.on 'ATOM_BROWSER_CURRENT_WINDOW', (event) -> - try - event.returnValue = valueToMeta event.sender, event.sender.getOwnerBrowserWindow() - catch e - event.returnValue = exceptionToMeta e - -ipcMain.on 'ATOM_BROWSER_CURRENT_WEB_CONTENTS', (event) -> - event.returnValue = valueToMeta event.sender, event.sender - -ipcMain.on 'ATOM_BROWSER_CONSTRUCTOR', (event, id, args) -> - try - args = unwrapArgs event.sender, args - constructor = objectsRegistry.get id - ### - Call new with array of arguments. - http://stackoverflow.com/questions/1606797/use-of-apply-with-new-operator-is-this-possible - ### - obj = new (Function::bind.apply(constructor, [null].concat(args))) - event.returnValue = valueToMeta event.sender, obj - catch e - event.returnValue = exceptionToMeta e - -ipcMain.on 'ATOM_BROWSER_FUNCTION_CALL', (event, id, args) -> - try - args = unwrapArgs event.sender, args - func = objectsRegistry.get id - callFunction event, func, global, args - catch e - event.returnValue = exceptionToMeta e - -ipcMain.on 'ATOM_BROWSER_MEMBER_CONSTRUCTOR', (event, id, method, args) -> - try - args = unwrapArgs event.sender, args - constructor = objectsRegistry.get(id)[method] - ### Call new with array of arguments. ### - obj = new (Function::bind.apply(constructor, [null].concat(args))) - event.returnValue = valueToMeta event.sender, obj - catch e - event.returnValue = exceptionToMeta e - -ipcMain.on 'ATOM_BROWSER_MEMBER_CALL', (event, id, method, args) -> - try - args = unwrapArgs event.sender, args - obj = objectsRegistry.get id - callFunction event, obj[method], obj, args - catch e - event.returnValue = exceptionToMeta e - -ipcMain.on 'ATOM_BROWSER_MEMBER_SET', (event, id, name, value) -> - try - obj = objectsRegistry.get id - obj[name] = value - event.returnValue = null - catch e - event.returnValue = exceptionToMeta e - -ipcMain.on 'ATOM_BROWSER_MEMBER_GET', (event, id, name) -> - try - obj = objectsRegistry.get id - event.returnValue = valueToMeta event.sender, obj[name] - catch e - event.returnValue = exceptionToMeta e - -ipcMain.on 'ATOM_BROWSER_DEREFERENCE', (event, id) -> - objectsRegistry.remove event.sender.getId(), id - -ipcMain.on 'ATOM_BROWSER_GUEST_WEB_CONTENTS', (event, guestInstanceId) -> - try - guestViewManager = require './guest-view-manager' - event.returnValue = valueToMeta event.sender, guestViewManager.getGuest(guestInstanceId) - catch e - event.returnValue = exceptionToMeta e - -ipcMain.on 'ATOM_BROWSER_ASYNC_CALL_TO_GUEST_VIEW', (event, guestInstanceId, method, args...) -> - try - guestViewManager = require './guest-view-manager' - guest = guestViewManager.getGuest(guestInstanceId) - guest[method].apply(guest, args) - catch e - event.returnValue = exceptionToMeta e diff --git a/atom/browser/lib/rpc-server.js b/atom/browser/lib/rpc-server.js new file mode 100644 index 000000000000..426ebe4a3906 --- /dev/null +++ b/atom/browser/lib/rpc-server.js @@ -0,0 +1,389 @@ +var IDWeakMap, callFunction, electron, exceptionToMeta, ipcMain, objectsRegistry, path, plainObjectToMeta, unwrapArgs, v8Util, valueToMeta, + slice = [].slice; + +path = require('path'); + +electron = require('electron'); + +ipcMain = electron.ipcMain; + +objectsRegistry = require('./objects-registry'); + +v8Util = process.atomBinding('v8_util'); + +IDWeakMap = process.atomBinding('id_weak_map').IDWeakMap; + + +/* Convert a real value into meta data. */ + +valueToMeta = function(sender, value, optimizeSimpleObject) { + var el, field, i, len, meta, name; + if (optimizeSimpleObject == null) { + optimizeSimpleObject = false; + } + meta = { + type: typeof value + }; + if (Buffer.isBuffer(value)) { + meta.type = 'buffer'; + } + if (value === null) { + meta.type = 'value'; + } + if (Array.isArray(value)) { + meta.type = 'array'; + } + if (value instanceof Error) { + meta.type = 'error'; + } + if (value instanceof Date) { + meta.type = 'date'; + } + if ((value != null ? value.constructor.name : void 0) === 'Promise') { + meta.type = 'promise'; + } + + /* Treat simple objects as value. */ + if (optimizeSimpleObject && meta.type === 'object' && v8Util.getHiddenValue(value, 'simple')) { + meta.type = 'value'; + } + + /* Treat the arguments object as array. */ + if (meta.type === 'object' && (value.callee != null) && (value.length != null)) { + meta.type = 'array'; + } + if (meta.type === 'array') { + meta.members = []; + for (i = 0, len = value.length; i < len; i++) { + el = value[i]; + meta.members.push(valueToMeta(sender, el)); + } + } else if (meta.type === 'object' || meta.type === 'function') { + meta.name = value.constructor.name; + + /* + Reference the original value if it's an object, because when it's + passed to renderer we would assume the renderer keeps a reference of + it. + */ + meta.id = objectsRegistry.add(sender.getId(), value); + meta.members = (function() { + var results; + results = []; + for (name in value) { + field = value[name]; + results.push({ + name: name, + type: typeof field + }); + } + return results; + })(); + } else if (meta.type === 'buffer') { + meta.value = Array.prototype.slice.call(value, 0); + } else if (meta.type === 'promise') { + meta.then = valueToMeta(sender, value.then.bind(value)); + } else if (meta.type === 'error') { + meta.members = plainObjectToMeta(value); + + /* Error.name is not part of own properties. */ + meta.members.push({ + name: 'name', + value: value.name + }); + } else if (meta.type === 'date') { + meta.value = value.getTime(); + } else { + meta.type = 'value'; + meta.value = value; + } + return meta; +}; + + +/* Convert object to meta by value. */ + +plainObjectToMeta = function(obj) { + return Object.getOwnPropertyNames(obj).map(function(name) { + return { + name: name, + value: obj[name] + }; + }); +}; + + +/* Convert Error into meta data. */ + +exceptionToMeta = function(error) { + return { + type: 'exception', + message: error.message, + stack: error.stack || error + }; +}; + + +/* Convert array of meta data from renderer into array of real values. */ + +unwrapArgs = function(sender, args) { + var metaToValue; + metaToValue = function(meta) { + var i, len, member, ref, rendererReleased, ret, returnValue; + switch (meta.type) { + case 'value': + return meta.value; + case 'remote-object': + return objectsRegistry.get(meta.id); + case 'array': + return unwrapArgs(sender, meta.value); + case 'buffer': + return new Buffer(meta.value); + case 'date': + return new Date(meta.value); + case 'promise': + return Promise.resolve({ + then: metaToValue(meta.then) + }); + case 'object': + ret = v8Util.createObjectWithName(meta.name); + ref = meta.members; + for (i = 0, len = ref.length; i < len; i++) { + member = ref[i]; + ret[member.name] = metaToValue(member.value); + } + return ret; + case 'function-with-return-value': + returnValue = metaToValue(meta.value); + return function() { + return returnValue; + }; + case 'function': + + /* Cache the callbacks in renderer. */ + if (!sender.callbacks) { + sender.callbacks = new IDWeakMap; + sender.on('render-view-deleted', function() { + return sender.callbacks.clear(); + }); + } + if (sender.callbacks.has(meta.id)) { + return sender.callbacks.get(meta.id); + } + rendererReleased = false; + objectsRegistry.once("clear-" + (sender.getId()), function() { + return rendererReleased = true; + }); + ret = function() { + if (rendererReleased) { + throw new Error("Attempting to call a function in a renderer window that has been closed or released. Function provided here: " + meta.location + "."); + } + return sender.send('ATOM_RENDERER_CALLBACK', meta.id, valueToMeta(sender, arguments)); + }; + v8Util.setDestructor(ret, function() { + if (rendererReleased) { + return; + } + sender.callbacks.remove(meta.id); + return sender.send('ATOM_RENDERER_RELEASE_CALLBACK', meta.id); + }); + sender.callbacks.set(meta.id, ret); + return ret; + default: + throw new TypeError("Unknown type: " + meta.type); + } + }; + return args.map(metaToValue); +}; + + +/* + Call a function and send reply asynchronously if it's a an asynchronous + style function and the caller didn't pass a callback. + */ + +callFunction = function(event, func, caller, args) { + var e, error1, funcMarkedAsync, funcName, funcPassedCallback, ref, ret; + funcMarkedAsync = v8Util.getHiddenValue(func, 'asynchronous'); + funcPassedCallback = typeof args[args.length - 1] === 'function'; + try { + if (funcMarkedAsync && !funcPassedCallback) { + args.push(function(ret) { + return event.returnValue = valueToMeta(event.sender, ret, true); + }); + return func.apply(caller, args); + } else { + ret = func.apply(caller, args); + return event.returnValue = valueToMeta(event.sender, ret, true); + } + } catch (error1) { + e = error1; + + /* + Catch functions thrown further down in function invocation and wrap + them with the function name so it's easier to trace things like + `Error processing argument -1.` + */ + funcName = (ref = func.name) != null ? ref : "anonymous"; + throw new Error("Could not call remote function `" + funcName + "`. Check that the function signature is correct. Underlying error: " + e.message); + } +}; + + +/* Send by BrowserWindow when its render view is deleted. */ + +process.on('ATOM_BROWSER_RELEASE_RENDER_VIEW', function(id) { + return objectsRegistry.clear(id); +}); + +ipcMain.on('ATOM_BROWSER_REQUIRE', function(event, module) { + var e, error1; + try { + return event.returnValue = valueToMeta(event.sender, process.mainModule.require(module)); + } catch (error1) { + e = error1; + return event.returnValue = exceptionToMeta(e); + } +}); + +ipcMain.on('ATOM_BROWSER_GET_BUILTIN', function(event, module) { + var e, error1; + try { + return event.returnValue = valueToMeta(event.sender, electron[module]); + } catch (error1) { + e = error1; + return event.returnValue = exceptionToMeta(e); + } +}); + +ipcMain.on('ATOM_BROWSER_GLOBAL', function(event, name) { + var e, error1; + try { + return event.returnValue = valueToMeta(event.sender, global[name]); + } catch (error1) { + e = error1; + return event.returnValue = exceptionToMeta(e); + } +}); + +ipcMain.on('ATOM_BROWSER_CURRENT_WINDOW', function(event) { + var e, error1; + try { + return event.returnValue = valueToMeta(event.sender, event.sender.getOwnerBrowserWindow()); + } catch (error1) { + e = error1; + return event.returnValue = exceptionToMeta(e); + } +}); + +ipcMain.on('ATOM_BROWSER_CURRENT_WEB_CONTENTS', function(event) { + return event.returnValue = valueToMeta(event.sender, event.sender); +}); + +ipcMain.on('ATOM_BROWSER_CONSTRUCTOR', function(event, id, args) { + var constructor, e, error1, obj; + try { + args = unwrapArgs(event.sender, args); + constructor = objectsRegistry.get(id); + + /* + Call new with array of arguments. + http://stackoverflow.com/questions/1606797/use-of-apply-with-new-operator-is-this-possible + */ + obj = new (Function.prototype.bind.apply(constructor, [null].concat(args))); + return event.returnValue = valueToMeta(event.sender, obj); + } catch (error1) { + e = error1; + return event.returnValue = exceptionToMeta(e); + } +}); + +ipcMain.on('ATOM_BROWSER_FUNCTION_CALL', function(event, id, args) { + var e, error1, func; + try { + args = unwrapArgs(event.sender, args); + func = objectsRegistry.get(id); + return callFunction(event, func, global, args); + } catch (error1) { + e = error1; + return event.returnValue = exceptionToMeta(e); + } +}); + +ipcMain.on('ATOM_BROWSER_MEMBER_CONSTRUCTOR', function(event, id, method, args) { + var constructor, e, error1, obj; + try { + args = unwrapArgs(event.sender, args); + constructor = objectsRegistry.get(id)[method]; + + /* Call new with array of arguments. */ + obj = new (Function.prototype.bind.apply(constructor, [null].concat(args))); + return event.returnValue = valueToMeta(event.sender, obj); + } catch (error1) { + e = error1; + return event.returnValue = exceptionToMeta(e); + } +}); + +ipcMain.on('ATOM_BROWSER_MEMBER_CALL', function(event, id, method, args) { + var e, error1, obj; + try { + args = unwrapArgs(event.sender, args); + obj = objectsRegistry.get(id); + return callFunction(event, obj[method], obj, args); + } catch (error1) { + e = error1; + return event.returnValue = exceptionToMeta(e); + } +}); + +ipcMain.on('ATOM_BROWSER_MEMBER_SET', function(event, id, name, value) { + var e, error1, obj; + try { + obj = objectsRegistry.get(id); + obj[name] = value; + return event.returnValue = null; + } catch (error1) { + e = error1; + return event.returnValue = exceptionToMeta(e); + } +}); + +ipcMain.on('ATOM_BROWSER_MEMBER_GET', function(event, id, name) { + var e, error1, obj; + try { + obj = objectsRegistry.get(id); + return event.returnValue = valueToMeta(event.sender, obj[name]); + } catch (error1) { + e = error1; + return event.returnValue = exceptionToMeta(e); + } +}); + +ipcMain.on('ATOM_BROWSER_DEREFERENCE', function(event, id) { + return objectsRegistry.remove(event.sender.getId(), id); +}); + +ipcMain.on('ATOM_BROWSER_GUEST_WEB_CONTENTS', function(event, guestInstanceId) { + var e, error1, guestViewManager; + try { + guestViewManager = require('./guest-view-manager'); + return event.returnValue = valueToMeta(event.sender, guestViewManager.getGuest(guestInstanceId)); + } catch (error1) { + e = error1; + return event.returnValue = exceptionToMeta(e); + } +}); + +ipcMain.on('ATOM_BROWSER_ASYNC_CALL_TO_GUEST_VIEW', function() { + var args, e, error1, event, guest, guestInstanceId, guestViewManager, method; + event = arguments[0], guestInstanceId = arguments[1], method = arguments[2], args = 4 <= arguments.length ? slice.call(arguments, 3) : []; + try { + guestViewManager = require('./guest-view-manager'); + guest = guestViewManager.getGuest(guestInstanceId); + return guest[method].apply(guest, args); + } catch (error1) { + e = error1; + return event.returnValue = exceptionToMeta(e); + } +}); diff --git a/atom/common/api/lib/callbacks-registry.coffee b/atom/common/api/lib/callbacks-registry.coffee deleted file mode 100644 index 04eafc64bb8b..000000000000 --- a/atom/common/api/lib/callbacks-registry.coffee +++ /dev/null @@ -1,45 +0,0 @@ -v8Util = process.atomBinding 'v8_util' - -module.exports = -class CallbacksRegistry - constructor: -> - @nextId = 0 - @callbacks = {} - - add: (callback) -> - ### The callback is already added. ### - id = v8Util.getHiddenValue callback, 'callbackId' - return id if id? - - id = ++@nextId - - ### - Capture the location of the function and put it in the ID string, - so that release errors can be tracked down easily. - ### - regexp = /at (.*)/gi - stackString = (new Error).stack - - while (match = regexp.exec(stackString)) isnt null - [x, location] = match - continue if location.indexOf('(native)') isnt -1 - continue if location.indexOf('atom.asar') isnt -1 - [x, filenameAndLine] = /([^/^\)]*)\)?$/gi.exec(location) - break - - @callbacks[id] = callback - v8Util.setHiddenValue callback, 'callbackId', id - v8Util.setHiddenValue callback, 'location', filenameAndLine - id - - get: (id) -> - @callbacks[id] ? -> - - call: (id, args...) -> - @get(id).call global, args... - - apply: (id, args...) -> - @get(id).apply global, args... - - remove: (id) -> - delete @callbacks[id] diff --git a/atom/common/api/lib/callbacks-registry.js b/atom/common/api/lib/callbacks-registry.js new file mode 100644 index 000000000000..1b93bf128e1f --- /dev/null +++ b/atom/common/api/lib/callbacks-registry.js @@ -0,0 +1,68 @@ +var CallbacksRegistry, v8Util, + slice = [].slice; + +v8Util = process.atomBinding('v8_util'); + +module.exports = CallbacksRegistry = (function() { + function CallbacksRegistry() { + this.nextId = 0; + this.callbacks = {}; + } + + CallbacksRegistry.prototype.add = function(callback) { + + /* The callback is already added. */ + var filenameAndLine, id, location, match, ref, regexp, stackString, x; + id = v8Util.getHiddenValue(callback, 'callbackId'); + if (id != null) { + return id; + } + id = ++this.nextId; + + /* + Capture the location of the function and put it in the ID string, + so that release errors can be tracked down easily. + */ + regexp = /at (.*)/gi; + stackString = (new Error).stack; + while ((match = regexp.exec(stackString)) !== null) { + x = match[0], location = match[1]; + if (location.indexOf('(native)') !== -1) { + continue; + } + if (location.indexOf('atom.asar') !== -1) { + continue; + } + ref = /([^\/^\)]*)\)?$/gi.exec(location), x = ref[0], filenameAndLine = ref[1]; + break; + } + this.callbacks[id] = callback; + v8Util.setHiddenValue(callback, 'callbackId', id); + v8Util.setHiddenValue(callback, 'location', filenameAndLine); + return id; + }; + + CallbacksRegistry.prototype.get = function(id) { + var ref; + return (ref = this.callbacks[id]) != null ? ref : function() {}; + }; + + CallbacksRegistry.prototype.call = function() { + var args, id, ref; + id = arguments[0], args = 2 <= arguments.length ? slice.call(arguments, 1) : []; + return (ref = this.get(id)).call.apply(ref, [global].concat(slice.call(args))); + }; + + CallbacksRegistry.prototype.apply = function() { + var args, id, ref; + id = arguments[0], args = 2 <= arguments.length ? slice.call(arguments, 1) : []; + return (ref = this.get(id)).apply.apply(ref, [global].concat(slice.call(args))); + }; + + CallbacksRegistry.prototype.remove = function(id) { + return delete this.callbacks[id]; + }; + + return CallbacksRegistry; + +})(); diff --git a/atom/common/api/lib/clipboard.coffee b/atom/common/api/lib/clipboard.coffee deleted file mode 100644 index 47ee8abc7f7b..000000000000 --- a/atom/common/api/lib/clipboard.coffee +++ /dev/null @@ -1,5 +0,0 @@ -if process.platform is 'linux' and process.type is 'renderer' - ### On Linux we could not access clipboard in renderer process. ### - module.exports = require('electron').remote.clipboard -else - module.exports = process.atomBinding 'clipboard' diff --git a/atom/common/api/lib/clipboard.js b/atom/common/api/lib/clipboard.js new file mode 100644 index 000000000000..4e7a668167e8 --- /dev/null +++ b/atom/common/api/lib/clipboard.js @@ -0,0 +1,7 @@ +if (process.platform === 'linux' && process.type === 'renderer') { + + /* On Linux we could not access clipboard in renderer process. */ + module.exports = require('electron').remote.clipboard; +} else { + module.exports = process.atomBinding('clipboard'); +} diff --git a/atom/common/api/lib/crash-reporter.coffee b/atom/common/api/lib/crash-reporter.coffee deleted file mode 100644 index af62dfd2f9df..000000000000 --- a/atom/common/api/lib/crash-reporter.coffee +++ /dev/null @@ -1,69 +0,0 @@ -fs = require 'fs' -os = require 'os' -path = require 'path' -{spawn} = require 'child_process' - -electron = require 'electron' -binding = process.atomBinding 'crash_reporter' - -class CrashReporter - start: (options={}) -> - {@productName, companyName, submitURL, autoSubmit, ignoreSystemCrashHandler, extra} = options - - ### Deprecated. ### - {deprecate} = electron - if options.submitUrl - submitURL ?= options.submitUrl - deprecate.warn 'submitUrl', 'submitURL' - - {app} = if process.type is 'browser' then electron else electron.remote - - @productName ?= app.getName() - autoSubmit ?= true - ignoreSystemCrashHandler ?= false - extra ?= {} - - extra._productName ?= @productName - extra._companyName ?= companyName - extra._version ?= app.getVersion() - - unless companyName? - deprecate.log('companyName is now a required option to crashReporter.start') - return - - unless submitURL? - deprecate.log('submitURL is now a required option to crashReporter.start') - return - - start = => binding.start @productName, companyName, submitURL, autoSubmit, ignoreSystemCrashHandler, extra - - if process.platform is 'win32' - args = [ - "--reporter-url=#{submitURL}" - "--application-name=#{@productName}" - "--v=1" - ] - env = ATOM_SHELL_INTERNAL_CRASH_SERVICE: 1 - - spawn process.execPath, args, {env, detached: true} - start() - - getLastCrashReport: -> - reports = this.getUploadedReports() - if reports.length > 0 then reports[0] else null - - getUploadedReports: -> - tmpdir = - if process.platform is 'win32' - os.tmpdir() - else - '/tmp' - log = - if process.platform is 'darwin' - path.join tmpdir, "#{@productName} Crashes" - else - path.join tmpdir, "#{@productName} Crashes", 'uploads.log' - binding._getUploadedReports log - -crashRepoter = new CrashReporter -module.exports = crashRepoter diff --git a/atom/common/api/lib/crash-reporter.js b/atom/common/api/lib/crash-reporter.js new file mode 100644 index 000000000000..75a60a1a998a --- /dev/null +++ b/atom/common/api/lib/crash-reporter.js @@ -0,0 +1,104 @@ +var CrashReporter, binding, crashRepoter, electron, fs, os, path, spawn; + +fs = require('fs'); + +os = require('os'); + +path = require('path'); + +spawn = require('child_process').spawn; + +electron = require('electron'); + +binding = process.atomBinding('crash_reporter'); + +CrashReporter = (function() { + function CrashReporter() {} + + CrashReporter.prototype.start = function(options) { + var app, args, autoSubmit, companyName, deprecate, env, extra, ignoreSystemCrashHandler, start, submitURL; + if (options == null) { + options = {}; + } + this.productName = options.productName, companyName = options.companyName, submitURL = options.submitURL, autoSubmit = options.autoSubmit, ignoreSystemCrashHandler = options.ignoreSystemCrashHandler, extra = options.extra; + + /* Deprecated. */ + deprecate = electron.deprecate; + if (options.submitUrl) { + if (submitURL == null) { + submitURL = options.submitUrl; + } + deprecate.warn('submitUrl', 'submitURL'); + } + app = (process.type === 'browser' ? electron : electron.remote).app; + if (this.productName == null) { + this.productName = app.getName(); + } + if (autoSubmit == null) { + autoSubmit = true; + } + if (ignoreSystemCrashHandler == null) { + ignoreSystemCrashHandler = false; + } + if (extra == null) { + extra = {}; + } + if (extra._productName == null) { + extra._productName = this.productName; + } + if (extra._companyName == null) { + extra._companyName = companyName; + } + if (extra._version == null) { + extra._version = app.getVersion(); + } + if (companyName == null) { + deprecate.log('companyName is now a required option to crashReporter.start'); + return; + } + if (submitURL == null) { + deprecate.log('submitURL is now a required option to crashReporter.start'); + return; + } + start = (function(_this) { + return function() { + return binding.start(_this.productName, companyName, submitURL, autoSubmit, ignoreSystemCrashHandler, extra); + }; + })(this); + if (process.platform === 'win32') { + args = ["--reporter-url=" + submitURL, "--application-name=" + this.productName, "--v=1"]; + env = { + ATOM_SHELL_INTERNAL_CRASH_SERVICE: 1 + }; + spawn(process.execPath, args, { + env: env, + detached: true + }); + } + return start(); + }; + + CrashReporter.prototype.getLastCrashReport = function() { + var reports; + reports = this.getUploadedReports(); + if (reports.length > 0) { + return reports[0]; + } else { + return null; + } + }; + + CrashReporter.prototype.getUploadedReports = function() { + var log, tmpdir; + tmpdir = process.platform === 'win32' ? os.tmpdir() : '/tmp'; + log = process.platform === 'darwin' ? path.join(tmpdir, this.productName + " Crashes") : path.join(tmpdir, this.productName + " Crashes", 'uploads.log'); + return binding._getUploadedReports(log); + }; + + return CrashReporter; + +})(); + +crashRepoter = new CrashReporter; + +module.exports = crashRepoter; diff --git a/atom/common/api/lib/deprecate.coffee b/atom/common/api/lib/deprecate.coffee deleted file mode 100644 index 7c03fc6f595d..000000000000 --- a/atom/common/api/lib/deprecate.coffee +++ /dev/null @@ -1,69 +0,0 @@ -### Deprecate a method. ### -deprecate = (oldName, newName, fn) -> - warned = false - -> - unless warned or process.noDeprecation - warned = true - deprecate.warn oldName, newName - fn.apply this, arguments - -### The method is renamed. ### -deprecate.rename = (object, oldName, newName) -> - warned = false - newMethod = -> - unless warned or process.noDeprecation - warned = true - deprecate.warn oldName, newName - this[newName].apply this, arguments - if typeof object is 'function' - object.prototype[oldName] = newMethod - else - object[oldName] = newMethod - -### Forward the method to member. ### -deprecate.member = (object, method, member) -> - warned = false - object.prototype[method] = -> - unless warned or process.noDeprecation - warned = true - deprecate.warn method, "#{member}.#{method}" - this[member][method].apply this[member], arguments - -### Deprecate a property. ### -deprecate.property = (object, property, method) -> - Object.defineProperty object, property, - get: -> - warned = false - unless warned or process.noDeprecation - warned = true - deprecate.warn "#{property} property", "#{method} method" - this[method]() - -### Deprecate an event. ### -deprecate.event = (emitter, oldName, newName, fn) -> - warned = false - emitter.on newName, (args...) -> - ### there is listeners for old API. ### - if @listenerCount(oldName) > 0 - unless warned or process.noDeprecation - warned = true - deprecate.warn "'#{oldName}' event", "'#{newName}' event" - if fn? - fn.apply this, arguments - else - @emit oldName, args... - -### Print deprecation warning. ### -deprecate.warn = (oldName, newName) -> - deprecate.log "#{oldName} is deprecated. Use #{newName} instead." - -### Print deprecation message. ### -deprecate.log = (message) -> - if process.throwDeprecation - throw new Error(message) - else if process.traceDeprecation - console.trace message - else - console.warn "(electron) #{message}" - -module.exports = deprecate diff --git a/atom/common/api/lib/deprecate.js b/atom/common/api/lib/deprecate.js new file mode 100644 index 000000000000..be04b1fe18a0 --- /dev/null +++ b/atom/common/api/lib/deprecate.js @@ -0,0 +1,115 @@ + +/* Deprecate a method. */ +var deprecate, + slice = [].slice; + +deprecate = function(oldName, newName, fn) { + var warned; + warned = false; + return function() { + if (!(warned || process.noDeprecation)) { + warned = true; + deprecate.warn(oldName, newName); + } + return fn.apply(this, arguments); + }; +}; + + +/* The method is renamed. */ + +deprecate.rename = function(object, oldName, newName) { + var newMethod, warned; + warned = false; + newMethod = function() { + if (!(warned || process.noDeprecation)) { + warned = true; + deprecate.warn(oldName, newName); + } + return this[newName].apply(this, arguments); + }; + if (typeof object === 'function') { + return object.prototype[oldName] = newMethod; + } else { + return object[oldName] = newMethod; + } +}; + + +/* Forward the method to member. */ + +deprecate.member = function(object, method, member) { + var warned; + warned = false; + return object.prototype[method] = function() { + if (!(warned || process.noDeprecation)) { + warned = true; + deprecate.warn(method, member + "." + method); + } + return this[member][method].apply(this[member], arguments); + }; +}; + + +/* Deprecate a property. */ + +deprecate.property = function(object, property, method) { + return Object.defineProperty(object, property, { + get: function() { + var warned; + warned = false; + if (!(warned || process.noDeprecation)) { + warned = true; + deprecate.warn(property + " property", method + " method"); + } + return this[method](); + } + }); +}; + + +/* Deprecate an event. */ + +deprecate.event = function(emitter, oldName, newName, fn) { + var warned; + warned = false; + return emitter.on(newName, function() { + var args; + args = 1 <= arguments.length ? slice.call(arguments, 0) : []; + + /* there is listeners for old API. */ + if (this.listenerCount(oldName) > 0) { + if (!(warned || process.noDeprecation)) { + warned = true; + deprecate.warn("'" + oldName + "' event", "'" + newName + "' event"); + } + if (fn != null) { + return fn.apply(this, arguments); + } else { + return this.emit.apply(this, [oldName].concat(slice.call(args))); + } + } + }); +}; + + +/* Print deprecation warning. */ + +deprecate.warn = function(oldName, newName) { + return deprecate.log(oldName + " is deprecated. Use " + newName + " instead."); +}; + + +/* Print deprecation message. */ + +deprecate.log = function(message) { + if (process.throwDeprecation) { + throw new Error(message); + } else if (process.traceDeprecation) { + return console.trace(message); + } else { + return console.warn("(electron) " + message); + } +}; + +module.exports = deprecate; diff --git a/atom/common/api/lib/exports/electron.coffee b/atom/common/api/lib/exports/electron.coffee deleted file mode 100644 index c941ab919baa..000000000000 --- a/atom/common/api/lib/exports/electron.coffee +++ /dev/null @@ -1,29 +0,0 @@ -### Do not expose the internal modules to `require`. ### -exports.hideInternalModules = -> - {globalPaths} = require 'module' - if globalPaths.length is 3 - ### Remove the "common/api/lib" and "browser-or-renderer/api/lib". ### - globalPaths.splice 0, 2 - -### Attaches properties to |exports|. ### -exports.defineProperties = (exports) -> - Object.defineProperties exports, - ### Common modules, please sort with alphabet order. ### - clipboard: - ### Must be enumerable, otherwise it woulde be invisible to remote module. ### - enumerable: true - get: -> require '../clipboard' - crashReporter: - enumerable: true - get: -> require '../crash-reporter' - nativeImage: - enumerable: true - get: -> require '../native-image' - shell: - enumerable: true - get: -> require '../shell' - ### The internal modules, invisible unless you know their names. ### - CallbacksRegistry: - get: -> require '../callbacks-registry' - deprecate: - get: -> require '../deprecate' diff --git a/atom/common/api/lib/exports/electron.js b/atom/common/api/lib/exports/electron.js new file mode 100644 index 000000000000..56443d376062 --- /dev/null +++ b/atom/common/api/lib/exports/electron.js @@ -0,0 +1,59 @@ + +/* Do not expose the internal modules to `require`. */ +exports.hideInternalModules = function() { + var globalPaths; + globalPaths = require('module').globalPaths; + if (globalPaths.length === 3) { + + /* Remove the "common/api/lib" and "browser-or-renderer/api/lib". */ + return globalPaths.splice(0, 2); + } +}; + + +/* Attaches properties to |exports|. */ + +exports.defineProperties = function(exports) { + return Object.defineProperties(exports, { + + /* Common modules, please sort with alphabet order. */ + clipboard: { + + /* Must be enumerable, otherwise it woulde be invisible to remote module. */ + enumerable: true, + get: function() { + return require('../clipboard'); + } + }, + crashReporter: { + enumerable: true, + get: function() { + return require('../crash-reporter'); + } + }, + nativeImage: { + enumerable: true, + get: function() { + return require('../native-image'); + } + }, + shell: { + enumerable: true, + get: function() { + return require('../shell'); + } + }, + + /* The internal modules, invisible unless you know their names. */ + CallbacksRegistry: { + get: function() { + return require('../callbacks-registry'); + } + }, + deprecate: { + get: function() { + return require('../deprecate'); + } + } + }); +}; diff --git a/atom/common/api/lib/native-image.coffee b/atom/common/api/lib/native-image.coffee deleted file mode 100644 index 9884f6abf763..000000000000 --- a/atom/common/api/lib/native-image.coffee +++ /dev/null @@ -1,7 +0,0 @@ -{deprecate} = require 'electron' -nativeImage = process.atomBinding 'native_image' - -### Deprecated. ### -deprecate.rename nativeImage, 'createFromDataUrl', 'createFromDataURL' - -module.exports = nativeImage diff --git a/atom/common/api/lib/native-image.js b/atom/common/api/lib/native-image.js new file mode 100644 index 000000000000..b1e1b2610ff4 --- /dev/null +++ b/atom/common/api/lib/native-image.js @@ -0,0 +1,12 @@ +var deprecate, nativeImage; + +deprecate = require('electron').deprecate; + +nativeImage = process.atomBinding('native_image'); + + +/* Deprecated. */ + +deprecate.rename(nativeImage, 'createFromDataUrl', 'createFromDataURL'); + +module.exports = nativeImage; diff --git a/atom/common/api/lib/shell.coffee b/atom/common/api/lib/shell.coffee deleted file mode 100644 index 5fb935bacd21..000000000000 --- a/atom/common/api/lib/shell.coffee +++ /dev/null @@ -1 +0,0 @@ -module.exports = process.atomBinding 'shell' diff --git a/atom/common/api/lib/shell.js b/atom/common/api/lib/shell.js new file mode 100644 index 000000000000..08cc4e8eb41c --- /dev/null +++ b/atom/common/api/lib/shell.js @@ -0,0 +1 @@ +module.exports = process.atomBinding('shell'); diff --git a/atom/common/lib/asar.coffee b/atom/common/lib/asar.coffee deleted file mode 100644 index 3e785b4e6132..000000000000 --- a/atom/common/lib/asar.coffee +++ /dev/null @@ -1,394 +0,0 @@ -asar = process.binding 'atom_common_asar' -child_process = require 'child_process' -path = require 'path' -util = require 'util' - -### Cache asar archive objects. ### -cachedArchives = {} -getOrCreateArchive = (p) -> - archive = cachedArchives[p] - return archive if archive? - archive = asar.createArchive p - return false unless archive - cachedArchives[p] = archive - -### Clean cache on quit. ### -process.on 'exit', -> - archive.destroy() for own p, archive of cachedArchives - -### Separate asar package's path from full path. ### -splitPath = (p) -> - ### shortcut to disable asar. ### - return [false] if process.noAsar - - return [false] if typeof p isnt 'string' - return [true, p, ''] if p.substr(-5) is '.asar' - p = path.normalize p - index = p.lastIndexOf ".asar#{path.sep}" - return [false] if index is -1 - [true, p.substr(0, index + 5), p.substr(index + 6)] - -### Convert asar archive's Stats object to fs's Stats object. ### -nextInode = 0 -uid = if process.getuid? then process.getuid() else 0 -gid = if process.getgid? then process.getgid() else 0 -fakeTime = new Date() -asarStatsToFsStats = (stats) -> - { - dev: 1, - ino: ++nextInode, - mode: 33188, - nlink: 1, - uid: uid, - gid: gid, - rdev: 0, - atime: stats.atime || fakeTime, - birthtime: stats.birthtime || fakeTime, - mtime: stats.mtime || fakeTime, - ctime: stats.ctime || fakeTime, - size: stats.size, - isFile: -> stats.isFile - isDirectory: -> stats.isDirectory - isSymbolicLink: -> stats.isLink - isBlockDevice: -> false - isCharacterDevice: -> false - isFIFO: -> false - isSocket: -> false - } - -### Create a ENOENT error. ### -notFoundError = (asarPath, filePath, callback) -> - error = new Error("ENOENT, #{filePath} not found in #{asarPath}") - error.code = "ENOENT" - error.errno = -2 - unless typeof callback is 'function' - throw error - process.nextTick -> callback error - -### Create a ENOTDIR error. ### -notDirError = (callback) -> - error = new Error('ENOTDIR, not a directory') - error.code = 'ENOTDIR' - error.errno = -20 - unless typeof callback is 'function' - throw error - process.nextTick -> callback error - -### Create invalid archive error. ### -invalidArchiveError = (asarPath, callback) -> - error = new Error("Invalid package #{asarPath}") - unless typeof callback is 'function' - throw error - process.nextTick -> callback error - -### Override APIs that rely on passing file path instead of content to C++. ### -overrideAPISync = (module, name, arg = 0) -> - old = module[name] - module[name] = -> - p = arguments[arg] - [isAsar, asarPath, filePath] = splitPath p - return old.apply this, arguments unless isAsar - - archive = getOrCreateArchive asarPath - invalidArchiveError asarPath unless archive - - newPath = archive.copyFileOut filePath - notFoundError asarPath, filePath unless newPath - - arguments[arg] = newPath - old.apply this, arguments - -overrideAPI = (module, name, arg = 0) -> - old = module[name] - module[name] = -> - p = arguments[arg] - [isAsar, asarPath, filePath] = splitPath p - return old.apply this, arguments unless isAsar - - callback = arguments[arguments.length - 1] - return overrideAPISync module, name, arg unless typeof callback is 'function' - - archive = getOrCreateArchive asarPath - return invalidArchiveError asarPath, callback unless archive - - newPath = archive.copyFileOut filePath - return notFoundError asarPath, filePath, callback unless newPath - - arguments[arg] = newPath - old.apply this, arguments - -### Override fs APIs. ### -exports.wrapFsWithAsar = (fs) -> - lstatSync = fs.lstatSync - fs.lstatSync = (p) -> - [isAsar, asarPath, filePath] = splitPath p - return lstatSync p unless isAsar - - archive = getOrCreateArchive asarPath - invalidArchiveError asarPath unless archive - - stats = archive.stat filePath - notFoundError asarPath, filePath unless stats - - asarStatsToFsStats stats - - lstat = fs.lstat - fs.lstat = (p, callback) -> - [isAsar, asarPath, filePath] = splitPath p - return lstat p, callback unless isAsar - - archive = getOrCreateArchive asarPath - return invalidArchiveError asarPath, callback unless archive - - stats = getOrCreateArchive(asarPath).stat filePath - return notFoundError asarPath, filePath, callback unless stats - - process.nextTick -> callback null, asarStatsToFsStats stats - - statSync = fs.statSync - fs.statSync = (p) -> - [isAsar, asarPath, filePath] = splitPath p - return statSync p unless isAsar - - ### Do not distinguish links for now. ### - fs.lstatSync p - - stat = fs.stat - fs.stat = (p, callback) -> - [isAsar, asarPath, filePath] = splitPath p - return stat p, callback unless isAsar - - ### Do not distinguish links for now. ### - process.nextTick -> fs.lstat p, callback - - statSyncNoException = fs.statSyncNoException - fs.statSyncNoException = (p) -> - [isAsar, asarPath, filePath] = splitPath p - return statSyncNoException p unless isAsar - - archive = getOrCreateArchive asarPath - return false unless archive - stats = archive.stat filePath - return false unless stats - asarStatsToFsStats stats - - realpathSync = fs.realpathSync - fs.realpathSync = (p) -> - [isAsar, asarPath, filePath] = splitPath p - return realpathSync.apply this, arguments unless isAsar - - archive = getOrCreateArchive asarPath - invalidArchiveError asarPath unless archive - - real = archive.realpath filePath - notFoundError asarPath, filePath if real is false - - path.join realpathSync(asarPath), real - - realpath = fs.realpath - fs.realpath = (p, cache, callback) -> - [isAsar, asarPath, filePath] = splitPath p - return realpath.apply this, arguments unless isAsar - - if typeof cache is 'function' - callback = cache - cache = undefined - - archive = getOrCreateArchive asarPath - return invalidArchiveError asarPath, callback unless archive - - real = archive.realpath filePath - if real is false - return notFoundError asarPath, filePath, callback - - realpath asarPath, (err, p) -> - return callback err if err - callback null, path.join(p, real) - - exists = fs.exists - fs.exists = (p, callback) -> - [isAsar, asarPath, filePath] = splitPath p - return exists p, callback unless isAsar - - archive = getOrCreateArchive asarPath - return invalidArchiveError asarPath, callback unless archive - - process.nextTick -> callback archive.stat(filePath) isnt false - - existsSync = fs.existsSync - fs.existsSync = (p) -> - [isAsar, asarPath, filePath] = splitPath p - return existsSync p unless isAsar - - archive = getOrCreateArchive asarPath - return false unless archive - - archive.stat(filePath) isnt false - - open = fs.open - readFile = fs.readFile - fs.readFile = (p, options, callback) -> - [isAsar, asarPath, filePath] = splitPath p - return readFile.apply this, arguments unless isAsar - - if typeof options is 'function' - callback = options - options = undefined - - archive = getOrCreateArchive asarPath - return invalidArchiveError asarPath, callback unless archive - - info = archive.getFileInfo filePath - return notFoundError asarPath, filePath, callback unless info - - if info.size is 0 - return process.nextTick -> callback null, new Buffer(0) - - if info.unpacked - realPath = archive.copyFileOut filePath - return fs.readFile realPath, options, callback - - if not options - options = encoding: null - else if util.isString options - options = encoding: options - else if not util.isObject options - throw new TypeError('Bad arguments') - - encoding = options.encoding - - buffer = new Buffer(info.size) - fd = archive.getFd() - return notFoundError asarPath, filePath, callback unless fd >= 0 - - fs.read fd, buffer, 0, info.size, info.offset, (error) -> - callback error, if encoding then buffer.toString encoding else buffer - - openSync = fs.openSync - readFileSync = fs.readFileSync - fs.readFileSync = (p, opts) -> - ### this allows v8 to optimize this function ### - options = opts - - [isAsar, asarPath, filePath] = splitPath p - return readFileSync.apply this, arguments unless isAsar - - archive = getOrCreateArchive asarPath - invalidArchiveError asarPath unless archive - - info = archive.getFileInfo filePath - notFoundError asarPath, filePath unless info - - if info.size is 0 - return if options then '' else new Buffer(0) - - if info.unpacked - realPath = archive.copyFileOut filePath - return fs.readFileSync realPath, options - - if not options - options = encoding: null - else if util.isString options - options = encoding: options - else if not util.isObject options - throw new TypeError('Bad arguments') - - encoding = options.encoding - - buffer = new Buffer(info.size) - fd = archive.getFd() - notFoundError asarPath, filePath unless fd >= 0 - - fs.readSync fd, buffer, 0, info.size, info.offset - if encoding then buffer.toString encoding else buffer - - readdir = fs.readdir - fs.readdir = (p, callback) -> - [isAsar, asarPath, filePath] = splitPath p - return readdir.apply this, arguments unless isAsar - - archive = getOrCreateArchive asarPath - return invalidArchiveError asarPath, callback unless archive - - files = archive.readdir filePath - return notFoundError asarPath, filePath, callback unless files - - process.nextTick -> callback null, files - - readdirSync = fs.readdirSync - fs.readdirSync = (p) -> - [isAsar, asarPath, filePath] = splitPath p - return readdirSync.apply this, arguments unless isAsar - - archive = getOrCreateArchive asarPath - invalidArchiveError asarPath unless archive - - files = archive.readdir filePath - notFoundError asarPath, filePath unless files - - files - - internalModuleReadFile = process.binding('fs').internalModuleReadFile - process.binding('fs').internalModuleReadFile = (p) -> - [isAsar, asarPath, filePath] = splitPath p - return internalModuleReadFile p unless isAsar - - archive = getOrCreateArchive asarPath - return undefined unless archive - - info = archive.getFileInfo filePath - return undefined unless info - return '' if info.size is 0 - - if info.unpacked - realPath = archive.copyFileOut filePath - return fs.readFileSync realPath, encoding: 'utf8' - - buffer = new Buffer(info.size) - fd = archive.getFd() - return undefined unless fd >= 0 - - fs.readSync fd, buffer, 0, info.size, info.offset - buffer.toString 'utf8' - - internalModuleStat = process.binding('fs').internalModuleStat - process.binding('fs').internalModuleStat = (p) -> - [isAsar, asarPath, filePath] = splitPath p - return internalModuleStat p unless isAsar - - archive = getOrCreateArchive asarPath - ### -ENOENT ### - return -34 unless archive - - stats = archive.stat filePath - ### -ENOENT ### - return -34 unless stats - - if stats.isDirectory then return 1 else return 0 - - ### - Calling mkdir for directory inside asar archive should throw ENOTDIR - error, but on Windows it throws ENOENT. - This is to work around the recursive looping bug of mkdirp since it is - widely used. - ### - if process.platform is 'win32' - mkdir = fs.mkdir - fs.mkdir = (p, mode, callback) -> - callback = mode if typeof mode is 'function' - [isAsar, asarPath, filePath] = splitPath p - return notDirError callback if isAsar and filePath.length - mkdir p, mode, callback - - mkdirSync = fs.mkdirSync - fs.mkdirSync = (p, mode) -> - [isAsar, asarPath, filePath] = splitPath p - notDirError() if isAsar and filePath.length - mkdirSync p, mode - - overrideAPI fs, 'open' - overrideAPI child_process, 'execFile' - overrideAPISync process, 'dlopen', 1 - overrideAPISync require('module')._extensions, '.node', 1 - overrideAPISync fs, 'openSync' - overrideAPISync child_process, 'execFileSync' diff --git a/atom/common/lib/asar.js b/atom/common/lib/asar.js new file mode 100644 index 000000000000..60e4c5c69f8c --- /dev/null +++ b/atom/common/lib/asar.js @@ -0,0 +1,608 @@ +var asar, asarStatsToFsStats, cachedArchives, child_process, fakeTime, getOrCreateArchive, gid, invalidArchiveError, nextInode, notDirError, notFoundError, overrideAPI, overrideAPISync, path, splitPath, uid, util, + hasProp = {}.hasOwnProperty; + +asar = process.binding('atom_common_asar'); + +child_process = require('child_process'); + +path = require('path'); + +util = require('util'); + + +/* Cache asar archive objects. */ + +cachedArchives = {}; + +getOrCreateArchive = function(p) { + var archive; + archive = cachedArchives[p]; + if (archive != null) { + return archive; + } + archive = asar.createArchive(p); + if (!archive) { + return false; + } + return cachedArchives[p] = archive; +}; + + +/* Clean cache on quit. */ + +process.on('exit', function() { + var archive, p, results; + results = []; + for (p in cachedArchives) { + if (!hasProp.call(cachedArchives, p)) continue; + archive = cachedArchives[p]; + results.push(archive.destroy()); + } + return results; +}); + + +/* Separate asar package's path from full path. */ + +splitPath = function(p) { + + /* shortcut to disable asar. */ + var index; + if (process.noAsar) { + return [false]; + } + if (typeof p !== 'string') { + return [false]; + } + if (p.substr(-5) === '.asar') { + return [true, p, '']; + } + p = path.normalize(p); + index = p.lastIndexOf(".asar" + path.sep); + if (index === -1) { + return [false]; + } + return [true, p.substr(0, index + 5), p.substr(index + 6)]; +}; + + +/* Convert asar archive's Stats object to fs's Stats object. */ + +nextInode = 0; + +uid = process.getuid != null ? process.getuid() : 0; + +gid = process.getgid != null ? process.getgid() : 0; + +fakeTime = new Date(); + +asarStatsToFsStats = function(stats) { + return { + dev: 1, + ino: ++nextInode, + mode: 33188, + nlink: 1, + uid: uid, + gid: gid, + rdev: 0, + atime: stats.atime || fakeTime, + birthtime: stats.birthtime || fakeTime, + mtime: stats.mtime || fakeTime, + ctime: stats.ctime || fakeTime, + size: stats.size, + isFile: function() { + return stats.isFile; + }, + isDirectory: function() { + return stats.isDirectory; + }, + isSymbolicLink: function() { + return stats.isLink; + }, + isBlockDevice: function() { + return false; + }, + isCharacterDevice: function() { + return false; + }, + isFIFO: function() { + return false; + }, + isSocket: function() { + return false; + } + }; +}; + + +/* Create a ENOENT error. */ + +notFoundError = function(asarPath, filePath, callback) { + var error; + error = new Error("ENOENT, " + filePath + " not found in " + asarPath); + error.code = "ENOENT"; + error.errno = -2; + if (typeof callback !== 'function') { + throw error; + } + return process.nextTick(function() { + return callback(error); + }); +}; + + +/* Create a ENOTDIR error. */ + +notDirError = function(callback) { + var error; + error = new Error('ENOTDIR, not a directory'); + error.code = 'ENOTDIR'; + error.errno = -20; + if (typeof callback !== 'function') { + throw error; + } + return process.nextTick(function() { + return callback(error); + }); +}; + + +/* Create invalid archive error. */ + +invalidArchiveError = function(asarPath, callback) { + var error; + error = new Error("Invalid package " + asarPath); + if (typeof callback !== 'function') { + throw error; + } + return process.nextTick(function() { + return callback(error); + }); +}; + + +/* Override APIs that rely on passing file path instead of content to C++. */ + +overrideAPISync = function(module, name, arg) { + var old; + if (arg == null) { + arg = 0; + } + old = module[name]; + return module[name] = function() { + var archive, asarPath, filePath, isAsar, newPath, p, ref; + p = arguments[arg]; + ref = splitPath(p), isAsar = ref[0], asarPath = ref[1], filePath = ref[2]; + if (!isAsar) { + return old.apply(this, arguments); + } + archive = getOrCreateArchive(asarPath); + if (!archive) { + invalidArchiveError(asarPath); + } + newPath = archive.copyFileOut(filePath); + if (!newPath) { + notFoundError(asarPath, filePath); + } + arguments[arg] = newPath; + return old.apply(this, arguments); + }; +}; + +overrideAPI = function(module, name, arg) { + var old; + if (arg == null) { + arg = 0; + } + old = module[name]; + return module[name] = function() { + var archive, asarPath, callback, filePath, isAsar, newPath, p, ref; + p = arguments[arg]; + ref = splitPath(p), isAsar = ref[0], asarPath = ref[1], filePath = ref[2]; + if (!isAsar) { + return old.apply(this, arguments); + } + callback = arguments[arguments.length - 1]; + if (typeof callback !== 'function') { + return overrideAPISync(module, name, arg); + } + archive = getOrCreateArchive(asarPath); + if (!archive) { + return invalidArchiveError(asarPath, callback); + } + newPath = archive.copyFileOut(filePath); + if (!newPath) { + return notFoundError(asarPath, filePath, callback); + } + arguments[arg] = newPath; + return old.apply(this, arguments); + }; +}; + + +/* Override fs APIs. */ + +exports.wrapFsWithAsar = function(fs) { + var exists, existsSync, internalModuleReadFile, internalModuleStat, lstat, lstatSync, mkdir, mkdirSync, open, openSync, readFile, readFileSync, readdir, readdirSync, realpath, realpathSync, stat, statSync, statSyncNoException; + lstatSync = fs.lstatSync; + fs.lstatSync = function(p) { + var archive, asarPath, filePath, isAsar, ref, stats; + ref = splitPath(p), isAsar = ref[0], asarPath = ref[1], filePath = ref[2]; + if (!isAsar) { + return lstatSync(p); + } + archive = getOrCreateArchive(asarPath); + if (!archive) { + invalidArchiveError(asarPath); + } + stats = archive.stat(filePath); + if (!stats) { + notFoundError(asarPath, filePath); + } + return asarStatsToFsStats(stats); + }; + lstat = fs.lstat; + fs.lstat = function(p, callback) { + var archive, asarPath, filePath, isAsar, ref, stats; + ref = splitPath(p), isAsar = ref[0], asarPath = ref[1], filePath = ref[2]; + if (!isAsar) { + return lstat(p, callback); + } + archive = getOrCreateArchive(asarPath); + if (!archive) { + return invalidArchiveError(asarPath, callback); + } + stats = getOrCreateArchive(asarPath).stat(filePath); + if (!stats) { + return notFoundError(asarPath, filePath, callback); + } + return process.nextTick(function() { + return callback(null, asarStatsToFsStats(stats)); + }); + }; + statSync = fs.statSync; + fs.statSync = function(p) { + var asarPath, filePath, isAsar, ref; + ref = splitPath(p), isAsar = ref[0], asarPath = ref[1], filePath = ref[2]; + if (!isAsar) { + return statSync(p); + } + + /* Do not distinguish links for now. */ + return fs.lstatSync(p); + }; + stat = fs.stat; + fs.stat = function(p, callback) { + var asarPath, filePath, isAsar, ref; + ref = splitPath(p), isAsar = ref[0], asarPath = ref[1], filePath = ref[2]; + if (!isAsar) { + return stat(p, callback); + } + + /* Do not distinguish links for now. */ + return process.nextTick(function() { + return fs.lstat(p, callback); + }); + }; + statSyncNoException = fs.statSyncNoException; + fs.statSyncNoException = function(p) { + var archive, asarPath, filePath, isAsar, ref, stats; + ref = splitPath(p), isAsar = ref[0], asarPath = ref[1], filePath = ref[2]; + if (!isAsar) { + return statSyncNoException(p); + } + archive = getOrCreateArchive(asarPath); + if (!archive) { + return false; + } + stats = archive.stat(filePath); + if (!stats) { + return false; + } + return asarStatsToFsStats(stats); + }; + realpathSync = fs.realpathSync; + fs.realpathSync = function(p) { + var archive, asarPath, filePath, isAsar, real, ref; + ref = splitPath(p), isAsar = ref[0], asarPath = ref[1], filePath = ref[2]; + if (!isAsar) { + return realpathSync.apply(this, arguments); + } + archive = getOrCreateArchive(asarPath); + if (!archive) { + invalidArchiveError(asarPath); + } + real = archive.realpath(filePath); + if (real === false) { + notFoundError(asarPath, filePath); + } + return path.join(realpathSync(asarPath), real); + }; + realpath = fs.realpath; + fs.realpath = function(p, cache, callback) { + var archive, asarPath, filePath, isAsar, real, ref; + ref = splitPath(p), isAsar = ref[0], asarPath = ref[1], filePath = ref[2]; + if (!isAsar) { + return realpath.apply(this, arguments); + } + if (typeof cache === 'function') { + callback = cache; + cache = void 0; + } + archive = getOrCreateArchive(asarPath); + if (!archive) { + return invalidArchiveError(asarPath, callback); + } + real = archive.realpath(filePath); + if (real === false) { + return notFoundError(asarPath, filePath, callback); + } + return realpath(asarPath, function(err, p) { + if (err) { + return callback(err); + } + return callback(null, path.join(p, real)); + }); + }; + exists = fs.exists; + fs.exists = function(p, callback) { + var archive, asarPath, filePath, isAsar, ref; + ref = splitPath(p), isAsar = ref[0], asarPath = ref[1], filePath = ref[2]; + if (!isAsar) { + return exists(p, callback); + } + archive = getOrCreateArchive(asarPath); + if (!archive) { + return invalidArchiveError(asarPath, callback); + } + return process.nextTick(function() { + return callback(archive.stat(filePath) !== false); + }); + }; + existsSync = fs.existsSync; + fs.existsSync = function(p) { + var archive, asarPath, filePath, isAsar, ref; + ref = splitPath(p), isAsar = ref[0], asarPath = ref[1], filePath = ref[2]; + if (!isAsar) { + return existsSync(p); + } + archive = getOrCreateArchive(asarPath); + if (!archive) { + return false; + } + return archive.stat(filePath) !== false; + }; + open = fs.open; + readFile = fs.readFile; + fs.readFile = function(p, options, callback) { + var archive, asarPath, buffer, encoding, fd, filePath, info, isAsar, realPath, ref; + ref = splitPath(p), isAsar = ref[0], asarPath = ref[1], filePath = ref[2]; + if (!isAsar) { + return readFile.apply(this, arguments); + } + if (typeof options === 'function') { + callback = options; + options = void 0; + } + archive = getOrCreateArchive(asarPath); + if (!archive) { + return invalidArchiveError(asarPath, callback); + } + info = archive.getFileInfo(filePath); + if (!info) { + return notFoundError(asarPath, filePath, callback); + } + if (info.size === 0) { + return process.nextTick(function() { + return callback(null, new Buffer(0)); + }); + } + if (info.unpacked) { + realPath = archive.copyFileOut(filePath); + return fs.readFile(realPath, options, callback); + } + if (!options) { + options = { + encoding: null + }; + } else if (util.isString(options)) { + options = { + encoding: options + }; + } else if (!util.isObject(options)) { + throw new TypeError('Bad arguments'); + } + encoding = options.encoding; + buffer = new Buffer(info.size); + fd = archive.getFd(); + if (!(fd >= 0)) { + return notFoundError(asarPath, filePath, callback); + } + return fs.read(fd, buffer, 0, info.size, info.offset, function(error) { + return callback(error, encoding ? buffer.toString(encoding) : buffer); + }); + }; + openSync = fs.openSync; + readFileSync = fs.readFileSync; + fs.readFileSync = function(p, opts) { + + /* this allows v8 to optimize this function */ + var archive, asarPath, buffer, encoding, fd, filePath, info, isAsar, options, realPath, ref; + options = opts; + ref = splitPath(p), isAsar = ref[0], asarPath = ref[1], filePath = ref[2]; + if (!isAsar) { + return readFileSync.apply(this, arguments); + } + archive = getOrCreateArchive(asarPath); + if (!archive) { + invalidArchiveError(asarPath); + } + info = archive.getFileInfo(filePath); + if (!info) { + notFoundError(asarPath, filePath); + } + if (info.size === 0) { + if (options) { + return ''; + } else { + return new Buffer(0); + } + } + if (info.unpacked) { + realPath = archive.copyFileOut(filePath); + return fs.readFileSync(realPath, options); + } + if (!options) { + options = { + encoding: null + }; + } else if (util.isString(options)) { + options = { + encoding: options + }; + } else if (!util.isObject(options)) { + throw new TypeError('Bad arguments'); + } + encoding = options.encoding; + buffer = new Buffer(info.size); + fd = archive.getFd(); + if (!(fd >= 0)) { + notFoundError(asarPath, filePath); + } + fs.readSync(fd, buffer, 0, info.size, info.offset); + if (encoding) { + return buffer.toString(encoding); + } else { + return buffer; + } + }; + readdir = fs.readdir; + fs.readdir = function(p, callback) { + var archive, asarPath, filePath, files, isAsar, ref; + ref = splitPath(p), isAsar = ref[0], asarPath = ref[1], filePath = ref[2]; + if (!isAsar) { + return readdir.apply(this, arguments); + } + archive = getOrCreateArchive(asarPath); + if (!archive) { + return invalidArchiveError(asarPath, callback); + } + files = archive.readdir(filePath); + if (!files) { + return notFoundError(asarPath, filePath, callback); + } + return process.nextTick(function() { + return callback(null, files); + }); + }; + readdirSync = fs.readdirSync; + fs.readdirSync = function(p) { + var archive, asarPath, filePath, files, isAsar, ref; + ref = splitPath(p), isAsar = ref[0], asarPath = ref[1], filePath = ref[2]; + if (!isAsar) { + return readdirSync.apply(this, arguments); + } + archive = getOrCreateArchive(asarPath); + if (!archive) { + invalidArchiveError(asarPath); + } + files = archive.readdir(filePath); + if (!files) { + notFoundError(asarPath, filePath); + } + return files; + }; + internalModuleReadFile = process.binding('fs').internalModuleReadFile; + process.binding('fs').internalModuleReadFile = function(p) { + var archive, asarPath, buffer, fd, filePath, info, isAsar, realPath, ref; + ref = splitPath(p), isAsar = ref[0], asarPath = ref[1], filePath = ref[2]; + if (!isAsar) { + return internalModuleReadFile(p); + } + archive = getOrCreateArchive(asarPath); + if (!archive) { + return void 0; + } + info = archive.getFileInfo(filePath); + if (!info) { + return void 0; + } + if (info.size === 0) { + return ''; + } + if (info.unpacked) { + realPath = archive.copyFileOut(filePath); + return fs.readFileSync(realPath, { + encoding: 'utf8' + }); + } + buffer = new Buffer(info.size); + fd = archive.getFd(); + if (!(fd >= 0)) { + return void 0; + } + fs.readSync(fd, buffer, 0, info.size, info.offset); + return buffer.toString('utf8'); + }; + internalModuleStat = process.binding('fs').internalModuleStat; + process.binding('fs').internalModuleStat = function(p) { + var archive, asarPath, filePath, isAsar, ref, stats; + ref = splitPath(p), isAsar = ref[0], asarPath = ref[1], filePath = ref[2]; + if (!isAsar) { + return internalModuleStat(p); + } + archive = getOrCreateArchive(asarPath); + + /* -ENOENT */ + if (!archive) { + return -34; + } + stats = archive.stat(filePath); + + /* -ENOENT */ + if (!stats) { + return -34; + } + if (stats.isDirectory) { + return 1; + } else { + return 0; + } + }; + + /* + Calling mkdir for directory inside asar archive should throw ENOTDIR + error, but on Windows it throws ENOENT. + This is to work around the recursive looping bug of mkdirp since it is + widely used. + */ + if (process.platform === 'win32') { + mkdir = fs.mkdir; + fs.mkdir = function(p, mode, callback) { + var asarPath, filePath, isAsar, ref; + if (typeof mode === 'function') { + callback = mode; + } + ref = splitPath(p), isAsar = ref[0], asarPath = ref[1], filePath = ref[2]; + if (isAsar && filePath.length) { + return notDirError(callback); + } + return mkdir(p, mode, callback); + }; + mkdirSync = fs.mkdirSync; + fs.mkdirSync = function(p, mode) { + var asarPath, filePath, isAsar, ref; + ref = splitPath(p), isAsar = ref[0], asarPath = ref[1], filePath = ref[2]; + if (isAsar && filePath.length) { + notDirError(); + } + return mkdirSync(p, mode); + }; + } + overrideAPI(fs, 'open'); + overrideAPI(child_process, 'execFile'); + overrideAPISync(process, 'dlopen', 1); + overrideAPISync(require('module')._extensions, '.node', 1); + overrideAPISync(fs, 'openSync'); + return overrideAPISync(child_process, 'execFileSync'); +}; diff --git a/atom/common/lib/asar_init.coffee b/atom/common/lib/asar_init.coffee deleted file mode 100644 index 4bb03daa7607..000000000000 --- a/atom/common/lib/asar_init.coffee +++ /dev/null @@ -1,22 +0,0 @@ -return (process, require, asarSource) -> - {createArchive} = process.binding 'atom_common_asar' - - ### Make asar.coffee accessible via "require". ### - process.binding('natives').ATOM_SHELL_ASAR = asarSource - - ### Monkey-patch the fs module. ### - require('ATOM_SHELL_ASAR').wrapFsWithAsar require('fs') - - ### Make graceful-fs work with asar. ### - source = process.binding 'natives' - source['original-fs'] = source.fs - source['fs'] = """ - var src = '(function (exports, require, module, __filename, __dirname) { ' + - process.binding('natives')['original-fs'] + - ' });'; - var vm = require('vm'); - var fn = vm.runInThisContext(src, { filename: 'fs.js' }); - fn(exports, require, module); - var asar = require('ATOM_SHELL_ASAR'); - asar.wrapFsWithAsar(exports); - """ diff --git a/atom/common/lib/asar_init.js b/atom/common/lib/asar_init.js new file mode 100644 index 000000000000..51d438c2c001 --- /dev/null +++ b/atom/common/lib/asar_init.js @@ -0,0 +1,15 @@ +return function(process, require, asarSource) { + var createArchive, source; + createArchive = process.binding('atom_common_asar').createArchive; + + /* Make asar.coffee accessible via "require". */ + process.binding('natives').ATOM_SHELL_ASAR = asarSource; + + /* Monkey-patch the fs module. */ + require('ATOM_SHELL_ASAR').wrapFsWithAsar(require('fs')); + + /* Make graceful-fs work with asar. */ + source = process.binding('natives'); + source['original-fs'] = source.fs; + return source['fs'] = "var src = '(function (exports, require, module, __filename, __dirname) { ' +\n process.binding('natives')['original-fs'] +\n ' });';\nvar vm = require('vm');\nvar fn = vm.runInThisContext(src, { filename: 'fs.js' });\nfn(exports, require, module);\nvar asar = require('ATOM_SHELL_ASAR');\nasar.wrapFsWithAsar(exports);"; +}; diff --git a/atom/common/lib/init.coffee b/atom/common/lib/init.coffee deleted file mode 100644 index 604c6590bdd5..000000000000 --- a/atom/common/lib/init.coffee +++ /dev/null @@ -1,40 +0,0 @@ -fs = require 'fs' -path = require 'path' -timers = require 'timers' -Module = require 'module' - -process.atomBinding = (name) -> - try - process.binding "atom_#{process.type}_#{name}" - catch e - process.binding "atom_common_#{name}" if /No such module/.test e.message - -unless process.env.ELECTRON_HIDE_INTERNAL_MODULES - ### Add common/api/lib to module search paths. ### - Module.globalPaths.push path.resolve(__dirname, '..', 'api', 'lib') - -### - setImmediate and process.nextTick makes use of uv_check and uv_prepare to - run the callbacks, however since we only run uv loop on requests, the - callbacks wouldn't be called until something else activated the uv loop, - which would delay the callbacks for arbitrary long time. So we should - initiatively activate the uv loop once setImmediate and process.nextTick is - called. -### -wrapWithActivateUvLoop = (func) -> - -> - process.activateUvLoop() - func.apply this, arguments -process.nextTick = wrapWithActivateUvLoop process.nextTick -global.setImmediate = wrapWithActivateUvLoop timers.setImmediate -global.clearImmediate = timers.clearImmediate - -if process.type is 'browser' - ### - setTimeout needs to update the polling timeout of the event loop, when - called under Chromium's event loop the node's event loop won't get a chance - to update the timeout, so we have to force the node's event loop to - recalculate the timeout in browser process. - ### - global.setTimeout = wrapWithActivateUvLoop timers.setTimeout - global.setInterval = wrapWithActivateUvLoop timers.setInterval diff --git a/atom/common/lib/init.js b/atom/common/lib/init.js new file mode 100644 index 000000000000..c1c21d4e819b --- /dev/null +++ b/atom/common/lib/init.js @@ -0,0 +1,62 @@ +var Module, fs, path, timers, wrapWithActivateUvLoop; + +fs = require('fs'); + +path = require('path'); + +timers = require('timers'); + +Module = require('module'); + +process.atomBinding = function(name) { + var e, error; + try { + return process.binding("atom_" + process.type + "_" + name); + } catch (error) { + e = error; + if (/No such module/.test(e.message)) { + return process.binding("atom_common_" + name); + } + } +}; + +if (!process.env.ELECTRON_HIDE_INTERNAL_MODULES) { + + /* Add common/api/lib to module search paths. */ + Module.globalPaths.push(path.resolve(__dirname, '..', 'api', 'lib')); +} + + +/* + setImmediate and process.nextTick makes use of uv_check and uv_prepare to + run the callbacks, however since we only run uv loop on requests, the + callbacks wouldn't be called until something else activated the uv loop, + which would delay the callbacks for arbitrary long time. So we should + initiatively activate the uv loop once setImmediate and process.nextTick is + called. + */ + +wrapWithActivateUvLoop = function(func) { + return function() { + process.activateUvLoop(); + return func.apply(this, arguments); + }; +}; + +process.nextTick = wrapWithActivateUvLoop(process.nextTick); + +global.setImmediate = wrapWithActivateUvLoop(timers.setImmediate); + +global.clearImmediate = timers.clearImmediate; + +if (process.type === 'browser') { + + /* + setTimeout needs to update the polling timeout of the event loop, when + called under Chromium's event loop the node's event loop won't get a chance + to update the timeout, so we have to force the node's event loop to + recalculate the timeout in browser process. + */ + global.setTimeout = wrapWithActivateUvLoop(timers.setTimeout); + global.setInterval = wrapWithActivateUvLoop(timers.setInterval); +} diff --git a/atom/common/lib/reset-search-paths.coffee b/atom/common/lib/reset-search-paths.coffee deleted file mode 100644 index ae99abb4bf30..000000000000 --- a/atom/common/lib/reset-search-paths.coffee +++ /dev/null @@ -1,29 +0,0 @@ -path = require 'path' -Module = require 'module' - -### Clear Node's global search paths. ### -Module.globalPaths.length = 0 - -### Clear current and parent(init.coffee)'s search paths. ### -module.paths = [] -module.parent.paths = [] - -### Prevent Node from adding paths outside this app to search paths. ### -Module._nodeModulePaths = (from) -> - from = path.resolve from - - ### If "from" is outside the app then we do nothing. ### - skipOutsidePaths = from.startsWith process.resourcesPath - - ### Following logoic is copied from module.js. ### - splitRe = if process.platform is 'win32' then /[\/\\]/ else /\// - paths = [] - - parts = from.split splitRe - for part, tip in parts by -1 - continue if part is 'node_modules' - dir = parts.slice(0, tip + 1).join path.sep - break if skipOutsidePaths and not dir.startsWith process.resourcesPath - paths.push path.join(dir, 'node_modules') - - paths diff --git a/atom/common/lib/reset-search-paths.js b/atom/common/lib/reset-search-paths.js new file mode 100644 index 000000000000..61486b570e2c --- /dev/null +++ b/atom/common/lib/reset-search-paths.js @@ -0,0 +1,45 @@ +var Module, path; + +path = require('path'); + +Module = require('module'); + + +/* Clear Node's global search paths. */ + +Module.globalPaths.length = 0; + + +/* Clear current and parent(init.coffee)'s search paths. */ + +module.paths = []; + +module.parent.paths = []; + + +/* Prevent Node from adding paths outside this app to search paths. */ + +Module._nodeModulePaths = function(from) { + var dir, i, part, parts, paths, skipOutsidePaths, splitRe, tip; + from = path.resolve(from); + + /* If "from" is outside the app then we do nothing. */ + skipOutsidePaths = from.startsWith(process.resourcesPath); + + /* Following logoic is copied from module.js. */ + splitRe = process.platform === 'win32' ? /[\/\\]/ : /\//; + paths = []; + parts = from.split(splitRe); + for (tip = i = parts.length - 1; i >= 0; tip = i += -1) { + part = parts[tip]; + if (part === 'node_modules') { + continue; + } + dir = parts.slice(0, tip + 1).join(path.sep); + if (skipOutsidePaths && !dir.startsWith(process.resourcesPath)) { + break; + } + paths.push(path.join(dir, 'node_modules')); + } + return paths; +}; diff --git a/atom/renderer/api/lib/desktop-capturer.coffee b/atom/renderer/api/lib/desktop-capturer.coffee deleted file mode 100644 index 4fd0763d7e55..000000000000 --- a/atom/renderer/api/lib/desktop-capturer.coffee +++ /dev/null @@ -1,20 +0,0 @@ -{ipcRenderer, nativeImage} = require 'electron' - -nextId = 0 -getNextId = -> ++nextId - -### |options.type| can not be empty and has to include 'window' or 'screen'. ### -isValid = (options) -> - return options?.types? and Array.isArray options.types - -exports.getSources = (options, callback) -> - return callback new Error('Invalid options') unless isValid options - - captureWindow = 'window' in options.types - captureScreen = 'screen' in options.types - options.thumbnailSize ?= width: 150, height: 150 - - id = getNextId() - ipcRenderer.send 'ATOM_BROWSER_DESKTOP_CAPTURER_GET_SOURCES', captureWindow, captureScreen, options.thumbnailSize, id - ipcRenderer.once "ATOM_RENDERER_DESKTOP_CAPTURER_RESULT_#{id}", (event, sources) -> - callback null, ({id: source.id, name: source.name, thumbnail: nativeImage.createFromDataURL source.thumbnail} for source in sources) diff --git a/atom/renderer/api/lib/desktop-capturer.js b/atom/renderer/api/lib/desktop-capturer.js new file mode 100644 index 000000000000..4cc5362fbb73 --- /dev/null +++ b/atom/renderer/api/lib/desktop-capturer.js @@ -0,0 +1,50 @@ +var getNextId, ipcRenderer, isValid, nativeImage, nextId, ref, + indexOf = [].indexOf || function(item) { for (var i = 0, l = this.length; i < l; i++) { if (i in this && this[i] === item) return i; } return -1; }; + +ref = require('electron'), ipcRenderer = ref.ipcRenderer, nativeImage = ref.nativeImage; + +nextId = 0; + +getNextId = function() { + return ++nextId; +}; + + +/* |options.type| can not be empty and has to include 'window' or 'screen'. */ + +isValid = function(options) { + return ((options != null ? options.types : void 0) != null) && Array.isArray(options.types); +}; + +exports.getSources = function(options, callback) { + var captureScreen, captureWindow, id; + if (!isValid(options)) { + return callback(new Error('Invalid options')); + } + captureWindow = indexOf.call(options.types, 'window') >= 0; + captureScreen = indexOf.call(options.types, 'screen') >= 0; + if (options.thumbnailSize == null) { + options.thumbnailSize = { + width: 150, + height: 150 + }; + } + id = getNextId(); + ipcRenderer.send('ATOM_BROWSER_DESKTOP_CAPTURER_GET_SOURCES', captureWindow, captureScreen, options.thumbnailSize, id); + return ipcRenderer.once("ATOM_RENDERER_DESKTOP_CAPTURER_RESULT_" + id, function(event, sources) { + var source; + return callback(null, (function() { + var i, len, results; + results = []; + for (i = 0, len = sources.length; i < len; i++) { + source = sources[i]; + results.push({ + id: source.id, + name: source.name, + thumbnail: nativeImage.createFromDataURL(source.thumbnail) + }); + } + return results; + })()); + }); +}; diff --git a/atom/renderer/api/lib/exports/electron.coffee b/atom/renderer/api/lib/exports/electron.coffee deleted file mode 100644 index b53980905416..000000000000 --- a/atom/renderer/api/lib/exports/electron.coffee +++ /dev/null @@ -1,22 +0,0 @@ -common = require '../../../../common/api/lib/exports/electron' - -### Import common modules. ### -common.defineProperties exports - -Object.defineProperties exports, - ### Renderer side modules, please sort with alphabet order. ### - desktopCapturer: - enumerable: true - get: -> require '../desktop-capturer' - ipcRenderer: - enumerable: true - get: -> require '../ipc-renderer' - remote: - enumerable: true - get: -> require '../remote' - screen: - enumerable: true - get: -> require '../screen' - webFrame: - enumerable: true - get: -> require '../web-frame' diff --git a/atom/renderer/api/lib/exports/electron.js b/atom/renderer/api/lib/exports/electron.js new file mode 100644 index 000000000000..e1c960c4b365 --- /dev/null +++ b/atom/renderer/api/lib/exports/electron.js @@ -0,0 +1,43 @@ +var common; + +common = require('../../../../common/api/lib/exports/electron'); + + +/* Import common modules. */ + +common.defineProperties(exports); + +Object.defineProperties(exports, { + + /* Renderer side modules, please sort with alphabet order. */ + desktopCapturer: { + enumerable: true, + get: function() { + return require('../desktop-capturer'); + } + }, + ipcRenderer: { + enumerable: true, + get: function() { + return require('../ipc-renderer'); + } + }, + remote: { + enumerable: true, + get: function() { + return require('../remote'); + } + }, + screen: { + enumerable: true, + get: function() { + return require('../screen'); + } + }, + webFrame: { + enumerable: true, + get: function() { + return require('../web-frame'); + } + } +}); diff --git a/atom/renderer/api/lib/ipc-renderer.coffee b/atom/renderer/api/lib/ipc-renderer.coffee deleted file mode 100644 index d468bd353e94..000000000000 --- a/atom/renderer/api/lib/ipc-renderer.coffee +++ /dev/null @@ -1,18 +0,0 @@ -{EventEmitter} = require 'events' - -binding = process.atomBinding 'ipc' -v8Util = process.atomBinding 'v8_util' - -### Created by init.coffee. ### -ipcRenderer = v8Util.getHiddenValue global, 'ipc' - -ipcRenderer.send = (args...) -> - binding.send 'ipc-message', [args...] - -ipcRenderer.sendSync = (args...) -> - JSON.parse binding.sendSync('ipc-message-sync', [args...]) - -ipcRenderer.sendToHost = (args...) -> - binding.send 'ipc-message-host', [args...] - -module.exports = ipcRenderer diff --git a/atom/renderer/api/lib/ipc-renderer.js b/atom/renderer/api/lib/ipc-renderer.js new file mode 100644 index 000000000000..b5f23030117f --- /dev/null +++ b/atom/renderer/api/lib/ipc-renderer.js @@ -0,0 +1,33 @@ +var EventEmitter, binding, ipcRenderer, v8Util, + slice = [].slice; + +EventEmitter = require('events').EventEmitter; + +binding = process.atomBinding('ipc'); + +v8Util = process.atomBinding('v8_util'); + + +/* Created by init.coffee. */ + +ipcRenderer = v8Util.getHiddenValue(global, 'ipc'); + +ipcRenderer.send = function() { + var args; + args = 1 <= arguments.length ? slice.call(arguments, 0) : []; + return binding.send('ipc-message', slice.call(args)); +}; + +ipcRenderer.sendSync = function() { + var args; + args = 1 <= arguments.length ? slice.call(arguments, 0) : []; + return JSON.parse(binding.sendSync('ipc-message-sync', slice.call(args))); +}; + +ipcRenderer.sendToHost = function() { + var args; + args = 1 <= arguments.length ? slice.call(arguments, 0) : []; + return binding.send('ipc-message-host', slice.call(args)); +}; + +module.exports = ipcRenderer; diff --git a/atom/renderer/api/lib/ipc.coffee b/atom/renderer/api/lib/ipc.coffee deleted file mode 100644 index 1eb49fb8d0de..000000000000 --- a/atom/renderer/api/lib/ipc.coffee +++ /dev/null @@ -1,19 +0,0 @@ -{ipcRenderer, deprecate} = require 'electron' -{EventEmitter} = require 'events' - -### This module is deprecated, we mirror everything from ipcRenderer. ### -deprecate.warn 'ipc module', 'require("electron").ipcRenderer' - -### Routes events of ipcRenderer. ### -ipc = new EventEmitter -ipcRenderer.emit = (channel, event, args...) -> - ipc.emit channel, args... - EventEmitter::emit.apply ipcRenderer, arguments - -### Deprecated. ### -for method of ipcRenderer when method.startsWith 'send' - ipc[method] = ipcRenderer[method] -deprecate.rename ipc, 'sendChannel', 'send' -deprecate.rename ipc, 'sendChannelSync', 'sendSync' - -module.exports = ipc diff --git a/atom/renderer/api/lib/ipc.js b/atom/renderer/api/lib/ipc.js new file mode 100644 index 000000000000..7758ce5d5188 --- /dev/null +++ b/atom/renderer/api/lib/ipc.js @@ -0,0 +1,38 @@ +var EventEmitter, deprecate, ipc, ipcRenderer, method, ref, + slice = [].slice; + +ref = require('electron'), ipcRenderer = ref.ipcRenderer, deprecate = ref.deprecate; + +EventEmitter = require('events').EventEmitter; + + +/* This module is deprecated, we mirror everything from ipcRenderer. */ + +deprecate.warn('ipc module', 'require("electron").ipcRenderer'); + + +/* Routes events of ipcRenderer. */ + +ipc = new EventEmitter; + +ipcRenderer.emit = function() { + var args, channel, event; + channel = arguments[0], event = arguments[1], args = 3 <= arguments.length ? slice.call(arguments, 2) : []; + ipc.emit.apply(ipc, [channel].concat(slice.call(args))); + return EventEmitter.prototype.emit.apply(ipcRenderer, arguments); +}; + + +/* Deprecated. */ + +for (method in ipcRenderer) { + if (method.startsWith('send')) { + ipc[method] = ipcRenderer[method]; + } +} + +deprecate.rename(ipc, 'sendChannel', 'send'); + +deprecate.rename(ipc, 'sendChannelSync', 'sendSync'); + +module.exports = ipc; diff --git a/atom/renderer/api/lib/remote.coffee b/atom/renderer/api/lib/remote.coffee deleted file mode 100644 index b30e79e73ba5..000000000000 --- a/atom/renderer/api/lib/remote.coffee +++ /dev/null @@ -1,209 +0,0 @@ -{ipcRenderer, CallbacksRegistry} = require 'electron' -v8Util = process.atomBinding 'v8_util' - -callbacksRegistry = new CallbacksRegistry - -### Check for circular reference. ### -isCircular = (field, visited) -> - if typeof field is 'object' - if field in visited - return true - visited.push field - return false - -### Convert the arguments object into an array of meta data. ### -wrapArgs = (args, visited=[]) -> - valueToMeta = (value) -> - if Array.isArray value - type: 'array', value: wrapArgs(value, visited) - else if Buffer.isBuffer value - type: 'buffer', value: Array::slice.call(value, 0) - else if value instanceof Date - type: 'date', value: value.getTime() - else if value?.constructor.name is 'Promise' - type: 'promise', then: valueToMeta(value.then.bind(value)) - else if value? and typeof value is 'object' and v8Util.getHiddenValue value, 'atomId' - type: 'remote-object', id: v8Util.getHiddenValue value, 'atomId' - else if value? and typeof value is 'object' - ret = type: 'object', name: value.constructor.name, members: [] - for prop, field of value - ret.members.push - name: prop - value: valueToMeta(if isCircular(field, visited) then null else field) - ret - else if typeof value is 'function' and v8Util.getHiddenValue value, 'returnValue' - type: 'function-with-return-value', value: valueToMeta(value()) - else if typeof value is 'function' - type: 'function', id: callbacksRegistry.add(value), location: v8Util.getHiddenValue value, 'location' - else - type: 'value', value: value - - Array::slice.call(args).map valueToMeta - -### Convert meta data from browser into real value. ### -metaToValue = (meta) -> - switch meta.type - when 'value' then meta.value - when 'array' then (metaToValue(el) for el in meta.members) - when 'buffer' then new Buffer(meta.value) - when 'promise' then Promise.resolve(then: metaToValue(meta.then)) - when 'error' then metaToPlainObject meta - when 'date' then new Date(meta.value) - when 'exception' - throw new Error("#{meta.message}\n#{meta.stack}") - else - if meta.type is 'function' - ### A shadow class to represent the remote function object. ### - ret = - class RemoteFunction - constructor: -> - if @constructor == RemoteFunction - ### Constructor call. ### - obj = ipcRenderer.sendSync 'ATOM_BROWSER_CONSTRUCTOR', meta.id, wrapArgs(arguments) - - ### - Returning object in constructor will replace constructed object - with the returned object. - http://stackoverflow.com/questions/1978049/what-values-can-a-constructor-return-to-avoid-returning-this - ### - return metaToValue obj - else - ### Function call. ### - obj = ipcRenderer.sendSync 'ATOM_BROWSER_FUNCTION_CALL', meta.id, wrapArgs(arguments) - return metaToValue obj - else - ret = v8Util.createObjectWithName meta.name - - ### Polulate delegate members. ### - for member in meta.members - if member.type is 'function' - ret[member.name] = createRemoteMemberFunction meta.id, member.name - else - Object.defineProperty ret, member.name, createRemoteMemberProperty(meta.id, member.name) - - ### - Track delegate object's life time, and tell the browser to clean up - when the object is GCed. - ### - v8Util.setDestructor ret, -> - ipcRenderer.send 'ATOM_BROWSER_DEREFERENCE', meta.id - - ### Remember object's id. ### - v8Util.setHiddenValue ret, 'atomId', meta.id - - ret - -### Construct a plain object from the meta. ### -metaToPlainObject = (meta) -> - obj = switch meta.type - when 'error' then new Error - else {} - obj[name] = value for {name, value} in meta.members - obj - -### - Create a RemoteMemberFunction instance. - This function's content should not be inlined into metaToValue, otherwise V8 - may consider it circular reference. -### -createRemoteMemberFunction = (metaId, name) -> - class RemoteMemberFunction - constructor: -> - if @constructor is RemoteMemberFunction - ### Constructor call. ### - ret = ipcRenderer.sendSync 'ATOM_BROWSER_MEMBER_CONSTRUCTOR', metaId, name, wrapArgs(arguments) - return metaToValue ret - else - ### Call member function. ### - ret = ipcRenderer.sendSync 'ATOM_BROWSER_MEMBER_CALL', metaId, name, wrapArgs(arguments) - return metaToValue ret - -### - Create configuration for defineProperty. - This function's content should not be inlined into metaToValue, otherwise V8 - may consider it circular reference. -### -createRemoteMemberProperty = (metaId, name) -> - enumerable: true, - configurable: false, - set: (value) -> - ### Set member data. ### - ipcRenderer.sendSync 'ATOM_BROWSER_MEMBER_SET', metaId, name, value - value - get: -> - ### Get member data. ### - metaToValue ipcRenderer.sendSync('ATOM_BROWSER_MEMBER_GET', metaId, name) - -### Browser calls a callback in renderer. ### -ipcRenderer.on 'ATOM_RENDERER_CALLBACK', (event, id, args) -> - callbacksRegistry.apply id, metaToValue(args) - -### A callback in browser is released. ### -ipcRenderer.on 'ATOM_RENDERER_RELEASE_CALLBACK', (event, id) -> - callbacksRegistry.remove id - -### List all built-in modules in browser process. ### -browserModules = require '../../../browser/api/lib/exports/electron' -### And add a helper receiver for each one. ### -for name of browserModules - do (name) -> - Object.defineProperty exports, name, get: -> exports.getBuiltin name - -### - Get remote module. - (Just like node's require, the modules are cached permanently, note that this - is safe leak since the object is not expected to get freed in browser) -### -moduleCache = {} -exports.require = (module) -> - return moduleCache[module] if moduleCache[module]? - - meta = ipcRenderer.sendSync 'ATOM_BROWSER_REQUIRE', module - moduleCache[module] = metaToValue meta - -### Optimize require('electron'). ### -moduleCache.electron = exports - -### Alias to remote.require('electron').xxx. ### -builtinCache = {} -exports.getBuiltin = (module) -> - return builtinCache[module] if builtinCache[module]? - - meta = ipcRenderer.sendSync 'ATOM_BROWSER_GET_BUILTIN', module - builtinCache[module] = metaToValue meta - -### Get current BrowserWindow object. ### -windowCache = null -exports.getCurrentWindow = -> - return windowCache if windowCache? - meta = ipcRenderer.sendSync 'ATOM_BROWSER_CURRENT_WINDOW' - windowCache = metaToValue meta - -### Get current WebContents object. ### -webContentsCache = null -exports.getCurrentWebContents = -> - return webContentsCache if webContentsCache? - meta = ipcRenderer.sendSync 'ATOM_BROWSER_CURRENT_WEB_CONTENTS' - webContentsCache = metaToValue meta - -### Get a global object in browser. ### -exports.getGlobal = (name) -> - meta = ipcRenderer.sendSync 'ATOM_BROWSER_GLOBAL', name - metaToValue meta - -### Get the process object in browser. ### -processCache = null -exports.__defineGetter__ 'process', -> - processCache = exports.getGlobal('process') unless processCache? - processCache - -### Create a funtion that will return the specifed value when called in browser. ### -exports.createFunctionWithReturnValue = (returnValue) -> - func = -> returnValue - v8Util.setHiddenValue func, 'returnValue', true - func - -### Get the guest WebContents from guestInstanceId. ### -exports.getGuestWebContents = (guestInstanceId) -> - meta = ipcRenderer.sendSync 'ATOM_BROWSER_GUEST_WEB_CONTENTS', guestInstanceId - metaToValue meta diff --git a/atom/renderer/api/lib/remote.js b/atom/renderer/api/lib/remote.js new file mode 100644 index 000000000000..3c0324c0ba27 --- /dev/null +++ b/atom/renderer/api/lib/remote.js @@ -0,0 +1,394 @@ +var CallbacksRegistry, browserModules, builtinCache, callbacksRegistry, createRemoteMemberFunction, createRemoteMemberProperty, fn, ipcRenderer, isCircular, metaToPlainObject, metaToValue, moduleCache, name, processCache, ref, v8Util, webContentsCache, windowCache, wrapArgs, + indexOf = [].indexOf || function(item) { for (var i = 0, l = this.length; i < l; i++) { if (i in this && this[i] === item) return i; } return -1; }; + +ref = require('electron'), ipcRenderer = ref.ipcRenderer, CallbacksRegistry = ref.CallbacksRegistry; + +v8Util = process.atomBinding('v8_util'); + +callbacksRegistry = new CallbacksRegistry; + + +/* Check for circular reference. */ + +isCircular = function(field, visited) { + if (typeof field === 'object') { + if (indexOf.call(visited, field) >= 0) { + return true; + } + visited.push(field); + } + return false; +}; + + +/* Convert the arguments object into an array of meta data. */ + +wrapArgs = function(args, visited) { + var valueToMeta; + if (visited == null) { + visited = []; + } + valueToMeta = function(value) { + var field, prop, ret; + if (Array.isArray(value)) { + return { + type: 'array', + value: wrapArgs(value, visited) + }; + } else if (Buffer.isBuffer(value)) { + return { + type: 'buffer', + value: Array.prototype.slice.call(value, 0) + }; + } else if (value instanceof Date) { + return { + type: 'date', + value: value.getTime() + }; + } else if ((value != null ? value.constructor.name : void 0) === 'Promise') { + return { + type: 'promise', + then: valueToMeta(value.then.bind(value)) + }; + } else if ((value != null) && typeof value === 'object' && v8Util.getHiddenValue(value, 'atomId')) { + return { + type: 'remote-object', + id: v8Util.getHiddenValue(value, 'atomId') + }; + } else if ((value != null) && typeof value === 'object') { + ret = { + type: 'object', + name: value.constructor.name, + members: [] + }; + for (prop in value) { + field = value[prop]; + ret.members.push({ + name: prop, + value: valueToMeta(isCircular(field, visited) ? null : field) + }); + } + return ret; + } else if (typeof value === 'function' && v8Util.getHiddenValue(value, 'returnValue')) { + return { + type: 'function-with-return-value', + value: valueToMeta(value()) + }; + } else if (typeof value === 'function') { + return { + type: 'function', + id: callbacksRegistry.add(value), + location: v8Util.getHiddenValue(value, 'location') + }; + } else { + return { + type: 'value', + value: value + }; + } + }; + return Array.prototype.slice.call(args).map(valueToMeta); +}; + + +/* Convert meta data from browser into real value. */ + +metaToValue = function(meta) { + var RemoteFunction, el, i, j, len, len1, member, ref1, ref2, results, ret; + switch (meta.type) { + case 'value': + return meta.value; + case 'array': + ref1 = meta.members; + results = []; + for (i = 0, len = ref1.length; i < len; i++) { + el = ref1[i]; + results.push(metaToValue(el)); + } + return results; + case 'buffer': + return new Buffer(meta.value); + case 'promise': + return Promise.resolve({ + then: metaToValue(meta.then) + }); + case 'error': + return metaToPlainObject(meta); + case 'date': + return new Date(meta.value); + case 'exception': + throw new Error(meta.message + "\n" + meta.stack); + break; + default: + if (meta.type === 'function') { + + /* A shadow class to represent the remote function object. */ + ret = RemoteFunction = (function() { + function RemoteFunction() { + var obj; + if (this.constructor === RemoteFunction) { + + /* Constructor call. */ + obj = ipcRenderer.sendSync('ATOM_BROWSER_CONSTRUCTOR', meta.id, wrapArgs(arguments)); + + /* + Returning object in constructor will replace constructed object + with the returned object. + http://stackoverflow.com/questions/1978049/what-values-can-a-constructor-return-to-avoid-returning-this + */ + return metaToValue(obj); + } else { + + /* Function call. */ + obj = ipcRenderer.sendSync('ATOM_BROWSER_FUNCTION_CALL', meta.id, wrapArgs(arguments)); + return metaToValue(obj); + } + } + + return RemoteFunction; + + })(); + } else { + ret = v8Util.createObjectWithName(meta.name); + } + + /* Polulate delegate members. */ + ref2 = meta.members; + for (j = 0, len1 = ref2.length; j < len1; j++) { + member = ref2[j]; + if (member.type === 'function') { + ret[member.name] = createRemoteMemberFunction(meta.id, member.name); + } else { + Object.defineProperty(ret, member.name, createRemoteMemberProperty(meta.id, member.name)); + } + } + + /* + Track delegate object's life time, and tell the browser to clean up + when the object is GCed. + */ + v8Util.setDestructor(ret, function() { + return ipcRenderer.send('ATOM_BROWSER_DEREFERENCE', meta.id); + }); + + /* Remember object's id. */ + v8Util.setHiddenValue(ret, 'atomId', meta.id); + return ret; + } +}; + + +/* Construct a plain object from the meta. */ + +metaToPlainObject = function(meta) { + var i, len, name, obj, ref1, ref2, value; + obj = (function() { + switch (meta.type) { + case 'error': + return new Error; + default: + return {}; + } + })(); + ref1 = meta.members; + for (i = 0, len = ref1.length; i < len; i++) { + ref2 = ref1[i], name = ref2.name, value = ref2.value; + obj[name] = value; + } + return obj; +}; + + +/* + Create a RemoteMemberFunction instance. + This function's content should not be inlined into metaToValue, otherwise V8 + may consider it circular reference. + */ + +createRemoteMemberFunction = function(metaId, name) { + var RemoteMemberFunction; + return RemoteMemberFunction = (function() { + function RemoteMemberFunction() { + var ret; + if (this.constructor === RemoteMemberFunction) { + + /* Constructor call. */ + ret = ipcRenderer.sendSync('ATOM_BROWSER_MEMBER_CONSTRUCTOR', metaId, name, wrapArgs(arguments)); + return metaToValue(ret); + } else { + + /* Call member function. */ + ret = ipcRenderer.sendSync('ATOM_BROWSER_MEMBER_CALL', metaId, name, wrapArgs(arguments)); + return metaToValue(ret); + } + } + + return RemoteMemberFunction; + + })(); +}; + + +/* + Create configuration for defineProperty. + This function's content should not be inlined into metaToValue, otherwise V8 + may consider it circular reference. + */ + +createRemoteMemberProperty = function(metaId, name) { + return { + enumerable: true, + configurable: false, + set: function(value) { + + /* Set member data. */ + ipcRenderer.sendSync('ATOM_BROWSER_MEMBER_SET', metaId, name, value); + return value; + }, + get: function() { + + /* Get member data. */ + return metaToValue(ipcRenderer.sendSync('ATOM_BROWSER_MEMBER_GET', metaId, name)); + } + }; +}; + + +/* Browser calls a callback in renderer. */ + +ipcRenderer.on('ATOM_RENDERER_CALLBACK', function(event, id, args) { + return callbacksRegistry.apply(id, metaToValue(args)); +}); + + +/* A callback in browser is released. */ + +ipcRenderer.on('ATOM_RENDERER_RELEASE_CALLBACK', function(event, id) { + return callbacksRegistry.remove(id); +}); + + +/* List all built-in modules in browser process. */ + +browserModules = require('../../../browser/api/lib/exports/electron'); + + +/* And add a helper receiver for each one. */ + +fn = function(name) { + return Object.defineProperty(exports, name, { + get: function() { + return exports.getBuiltin(name); + } + }); +}; +for (name in browserModules) { + fn(name); +} + + +/* + Get remote module. + (Just like node's require, the modules are cached permanently, note that this + is safe leak since the object is not expected to get freed in browser) + */ + +moduleCache = {}; + +exports.require = function(module) { + var meta; + if (moduleCache[module] != null) { + return moduleCache[module]; + } + meta = ipcRenderer.sendSync('ATOM_BROWSER_REQUIRE', module); + return moduleCache[module] = metaToValue(meta); +}; + + +/* Optimize require('electron'). */ + +moduleCache.electron = exports; + + +/* Alias to remote.require('electron').xxx. */ + +builtinCache = {}; + +exports.getBuiltin = function(module) { + var meta; + if (builtinCache[module] != null) { + return builtinCache[module]; + } + meta = ipcRenderer.sendSync('ATOM_BROWSER_GET_BUILTIN', module); + return builtinCache[module] = metaToValue(meta); +}; + + +/* Get current BrowserWindow object. */ + +windowCache = null; + +exports.getCurrentWindow = function() { + var meta; + if (windowCache != null) { + return windowCache; + } + meta = ipcRenderer.sendSync('ATOM_BROWSER_CURRENT_WINDOW'); + return windowCache = metaToValue(meta); +}; + + +/* Get current WebContents object. */ + +webContentsCache = null; + +exports.getCurrentWebContents = function() { + var meta; + if (webContentsCache != null) { + return webContentsCache; + } + meta = ipcRenderer.sendSync('ATOM_BROWSER_CURRENT_WEB_CONTENTS'); + return webContentsCache = metaToValue(meta); +}; + + +/* Get a global object in browser. */ + +exports.getGlobal = function(name) { + var meta; + meta = ipcRenderer.sendSync('ATOM_BROWSER_GLOBAL', name); + return metaToValue(meta); +}; + + +/* Get the process object in browser. */ + +processCache = null; + +exports.__defineGetter__('process', function() { + if (processCache == null) { + processCache = exports.getGlobal('process'); + } + return processCache; +}); + + +/* Create a funtion that will return the specifed value when called in browser. */ + +exports.createFunctionWithReturnValue = function(returnValue) { + var func; + func = function() { + return returnValue; + }; + v8Util.setHiddenValue(func, 'returnValue', true); + return func; +}; + + +/* Get the guest WebContents from guestInstanceId. */ + +exports.getGuestWebContents = function(guestInstanceId) { + var meta; + meta = ipcRenderer.sendSync('ATOM_BROWSER_GUEST_WEB_CONTENTS', guestInstanceId); + return metaToValue(meta); +}; diff --git a/atom/renderer/api/lib/screen.coffee b/atom/renderer/api/lib/screen.coffee deleted file mode 100644 index 9eecd49dc5bf..000000000000 --- a/atom/renderer/api/lib/screen.coffee +++ /dev/null @@ -1 +0,0 @@ -module.exports = require('electron').remote.screen diff --git a/atom/renderer/api/lib/screen.js b/atom/renderer/api/lib/screen.js new file mode 100644 index 000000000000..fb74d78f79d2 --- /dev/null +++ b/atom/renderer/api/lib/screen.js @@ -0,0 +1 @@ +module.exports = require('electron').remote.screen; diff --git a/atom/renderer/api/lib/web-frame.coffee b/atom/renderer/api/lib/web-frame.coffee deleted file mode 100644 index e2ae3cae6192..000000000000 --- a/atom/renderer/api/lib/web-frame.coffee +++ /dev/null @@ -1,9 +0,0 @@ -{deprecate} = require 'electron' -{webFrame} = process.atomBinding 'web_frame' - -### Deprecated. ### -deprecate.rename webFrame, 'registerUrlSchemeAsSecure', 'registerURLSchemeAsSecure' -deprecate.rename webFrame, 'registerUrlSchemeAsBypassingCSP', 'registerURLSchemeAsBypassingCSP' -deprecate.rename webFrame, 'registerUrlSchemeAsPrivileged', 'registerURLSchemeAsPrivileged' - -module.exports = webFrame diff --git a/atom/renderer/api/lib/web-frame.js b/atom/renderer/api/lib/web-frame.js new file mode 100644 index 000000000000..90d1c5a87bd2 --- /dev/null +++ b/atom/renderer/api/lib/web-frame.js @@ -0,0 +1,16 @@ +var deprecate, webFrame; + +deprecate = require('electron').deprecate; + +webFrame = process.atomBinding('web_frame').webFrame; + + +/* Deprecated. */ + +deprecate.rename(webFrame, 'registerUrlSchemeAsSecure', 'registerURLSchemeAsSecure'); + +deprecate.rename(webFrame, 'registerUrlSchemeAsBypassingCSP', 'registerURLSchemeAsBypassingCSP'); + +deprecate.rename(webFrame, 'registerUrlSchemeAsPrivileged', 'registerURLSchemeAsPrivileged'); + +module.exports = webFrame; diff --git a/atom/renderer/lib/chrome-api.coffee b/atom/renderer/lib/chrome-api.coffee deleted file mode 100644 index 288afaf55457..000000000000 --- a/atom/renderer/lib/chrome-api.coffee +++ /dev/null @@ -1,10 +0,0 @@ -url = require 'url' - -chrome = window.chrome = window.chrome || {} -chrome.extension = - getURL: (path) -> - url.format - protocol: location.protocol - slashes: true - hostname: location.hostname - pathname: path diff --git a/atom/renderer/lib/chrome-api.js b/atom/renderer/lib/chrome-api.js new file mode 100644 index 000000000000..535f7f2103b5 --- /dev/null +++ b/atom/renderer/lib/chrome-api.js @@ -0,0 +1,16 @@ +var chrome, url; + +url = require('url'); + +chrome = window.chrome = window.chrome || {}; + +chrome.extension = { + getURL: function(path) { + return url.format({ + protocol: location.protocol, + slashes: true, + hostname: location.hostname, + pathname: path + }); + } +}; diff --git a/atom/renderer/lib/init.coffee b/atom/renderer/lib/init.coffee deleted file mode 100644 index 149c636ebeb4..000000000000 --- a/atom/renderer/lib/init.coffee +++ /dev/null @@ -1,111 +0,0 @@ -events = require 'events' -path = require 'path' -url = require 'url' -Module = require 'module' - -### - We modified the original process.argv to let node.js load the - atom-renderer.js, we need to restore it here. -### -process.argv.splice 1, 1 - -### Clear search paths. ### -require path.resolve(__dirname, '..', '..', 'common', 'lib', 'reset-search-paths') - -### Import common settings. ### -require path.resolve(__dirname, '..', '..', 'common', 'lib', 'init') - -globalPaths = Module.globalPaths -unless process.env.ELECTRON_HIDE_INTERNAL_MODULES - globalPaths.push path.resolve(__dirname, '..', 'api', 'lib') - -### Expose public APIs. ### -globalPaths.push path.resolve(__dirname, '..', 'api', 'lib', 'exports') - -### The global variable will be used by ipc for event dispatching ### -v8Util = process.atomBinding 'v8_util' -v8Util.setHiddenValue global, 'ipc', new events.EventEmitter - -### Process command line arguments. ### -nodeIntegration = 'false' -for arg in process.argv - if arg.indexOf('--guest-instance-id=') == 0 - ### This is a guest web view. ### - process.guestInstanceId = parseInt arg.substr(arg.indexOf('=') + 1) - else if arg.indexOf('--opener-id=') == 0 - ### This is a guest BrowserWindow. ### - process.openerId = parseInt arg.substr(arg.indexOf('=') + 1) - else if arg.indexOf('--node-integration=') == 0 - nodeIntegration = arg.substr arg.indexOf('=') + 1 - else if arg.indexOf('--preload=') == 0 - preloadScript = arg.substr arg.indexOf('=') + 1 - -if location.protocol is 'chrome-devtools:' - ### Override some inspector APIs. ### - require './inspector' - nodeIntegration = 'true' -else if location.protocol is 'chrome-extension:' - ### Add implementations of chrome API. ### - require './chrome-api' - nodeIntegration = 'true' -else - ### Override default web functions. ### - require './override' - ### Load webview tag implementation. ### - unless process.guestInstanceId? - require './web-view/web-view' - require './web-view/web-view-attributes' - -if nodeIntegration in ['true', 'all', 'except-iframe', 'manual-enable-iframe'] - ### Export node bindings to global. ### - global.require = require - global.module = module - - ### Set the __filename to the path of html file if it is file: protocol. ### - if window.location.protocol is 'file:' - pathname = - if process.platform is 'win32' and window.location.pathname[0] is '/' - window.location.pathname.substr 1 - else - window.location.pathname - global.__filename = path.normalize decodeURIComponent(pathname) - global.__dirname = path.dirname global.__filename - - ### Set module's filename so relative require can work as expected. ### - module.filename = global.__filename - - ### Also search for module under the html file. ### - module.paths = module.paths.concat Module._nodeModulePaths(global.__dirname) - else - global.__filename = __filename - global.__dirname = __dirname - - ### Redirect window.onerror to uncaughtException. ### - window.onerror = (message, filename, lineno, colno, error) -> - if global.process.listeners('uncaughtException').length > 0 - global.process.emit 'uncaughtException', error - true - else - false - - ### Emit the 'exit' event when page is unloading. ### - window.addEventListener 'unload', -> - process.emit 'exit' -else - ### Delete Node's symbols after the Environment has been loaded. ### - process.once 'loaded', -> - delete global.process - delete global.setImmediate - delete global.clearImmediate - delete global.global - -### Load the script specfied by the "preload" attribute. ### -if preloadScript - try - require preloadScript - catch error - if error.code is 'MODULE_NOT_FOUND' - console.error "Unable to load preload script #{preloadScript}" - else - console.error(error) - console.error(error.stack) diff --git a/atom/renderer/lib/init.js b/atom/renderer/lib/init.js new file mode 100644 index 000000000000..3d3026b595ef --- /dev/null +++ b/atom/renderer/lib/init.js @@ -0,0 +1,154 @@ +var Module, arg, error, error1, events, globalPaths, i, len, nodeIntegration, path, pathname, preloadScript, ref, url, v8Util; + +events = require('events'); + +path = require('path'); + +url = require('url'); + +Module = require('module'); + + +/* + We modified the original process.argv to let node.js load the + atom-renderer.js, we need to restore it here. + */ + +process.argv.splice(1, 1); + + +/* Clear search paths. */ + +require(path.resolve(__dirname, '..', '..', 'common', 'lib', 'reset-search-paths')); + + +/* Import common settings. */ + +require(path.resolve(__dirname, '..', '..', 'common', 'lib', 'init')); + +globalPaths = Module.globalPaths; + +if (!process.env.ELECTRON_HIDE_INTERNAL_MODULES) { + globalPaths.push(path.resolve(__dirname, '..', 'api', 'lib')); +} + + +/* Expose public APIs. */ + +globalPaths.push(path.resolve(__dirname, '..', 'api', 'lib', 'exports')); + + +/* The global variable will be used by ipc for event dispatching */ + +v8Util = process.atomBinding('v8_util'); + +v8Util.setHiddenValue(global, 'ipc', new events.EventEmitter); + + +/* Process command line arguments. */ + +nodeIntegration = 'false'; + +ref = process.argv; +for (i = 0, len = ref.length; i < len; i++) { + arg = ref[i]; + if (arg.indexOf('--guest-instance-id=') === 0) { + + /* This is a guest web view. */ + process.guestInstanceId = parseInt(arg.substr(arg.indexOf('=') + 1)); + } else if (arg.indexOf('--opener-id=') === 0) { + + /* This is a guest BrowserWindow. */ + process.openerId = parseInt(arg.substr(arg.indexOf('=') + 1)); + } else if (arg.indexOf('--node-integration=') === 0) { + nodeIntegration = arg.substr(arg.indexOf('=') + 1); + } else if (arg.indexOf('--preload=') === 0) { + preloadScript = arg.substr(arg.indexOf('=') + 1); + } +} + +if (location.protocol === 'chrome-devtools:') { + + /* Override some inspector APIs. */ + require('./inspector'); + nodeIntegration = 'true'; +} else if (location.protocol === 'chrome-extension:') { + + /* Add implementations of chrome API. */ + require('./chrome-api'); + nodeIntegration = 'true'; +} else { + + /* Override default web functions. */ + require('./override'); + + /* Load webview tag implementation. */ + if (process.guestInstanceId == null) { + require('./web-view/web-view'); + require('./web-view/web-view-attributes'); + } +} + +if (nodeIntegration === 'true' || nodeIntegration === 'all' || nodeIntegration === 'except-iframe' || nodeIntegration === 'manual-enable-iframe') { + + /* Export node bindings to global. */ + global.require = require; + global.module = module; + + /* Set the __filename to the path of html file if it is file: protocol. */ + if (window.location.protocol === 'file:') { + pathname = process.platform === 'win32' && window.location.pathname[0] === '/' ? window.location.pathname.substr(1) : window.location.pathname; + global.__filename = path.normalize(decodeURIComponent(pathname)); + global.__dirname = path.dirname(global.__filename); + + /* Set module's filename so relative require can work as expected. */ + module.filename = global.__filename; + + /* Also search for module under the html file. */ + module.paths = module.paths.concat(Module._nodeModulePaths(global.__dirname)); + } else { + global.__filename = __filename; + global.__dirname = __dirname; + } + + /* Redirect window.onerror to uncaughtException. */ + window.onerror = function(message, filename, lineno, colno, error) { + if (global.process.listeners('uncaughtException').length > 0) { + global.process.emit('uncaughtException', error); + return true; + } else { + return false; + } + }; + + /* Emit the 'exit' event when page is unloading. */ + window.addEventListener('unload', function() { + return process.emit('exit'); + }); +} else { + + /* Delete Node's symbols after the Environment has been loaded. */ + process.once('loaded', function() { + delete global.process; + delete global.setImmediate; + delete global.clearImmediate; + return delete global.global; + }); +} + + +/* Load the script specfied by the "preload" attribute. */ + +if (preloadScript) { + try { + require(preloadScript); + } catch (error1) { + error = error1; + if (error.code === 'MODULE_NOT_FOUND') { + console.error("Unable to load preload script " + preloadScript); + } else { + console.error(error); + console.error(error.stack); + } + } +} diff --git a/atom/renderer/lib/inspector.coffee b/atom/renderer/lib/inspector.coffee deleted file mode 100644 index e49f4b77c7b5..000000000000 --- a/atom/renderer/lib/inspector.coffee +++ /dev/null @@ -1,60 +0,0 @@ -window.onload = -> - ### Use menu API to show context menu. ### - InspectorFrontendHost.showContextMenuAtPoint = createMenu - - ### Use dialog API to override file chooser dialog. ### - WebInspector.createFileSelectorElement = createFileSelectorElement - -convertToMenuTemplate = (items) -> - template = [] - for item in items - do (item) -> - transformed = - if item.type is 'subMenu' - type: 'submenu' - label: item.label - enabled: item.enabled - submenu: convertToMenuTemplate item.subItems - else if item.type is 'separator' - type: 'separator' - else if item.type is 'checkbox' - type: 'checkbox' - label: item.label - enabled: item.enabled - checked: item.checked - else - type: 'normal' - label: item.label - enabled: item.enabled - if item.id? - transformed.click = -> - DevToolsAPI.contextMenuItemSelected item.id - DevToolsAPI.contextMenuCleared() - template.push transformed - template - -createMenu = (x, y, items, document) -> - {remote} = require 'electron' - {Menu} = remote - - menu = Menu.buildFromTemplate convertToMenuTemplate(items) - ### The menu is expected to show asynchronously. ### - setTimeout -> menu.popup remote.getCurrentWindow() - -showFileChooserDialog = (callback) -> - {remote} = require 'electron' - {dialog} = remote - files = dialog.showOpenDialog {} - callback pathToHtml5FileObject files[0] if files? - -pathToHtml5FileObject = (path) -> - fs = require 'fs' - blob = new Blob([fs.readFileSync(path)]) - blob.name = path - blob - -createFileSelectorElement = (callback) -> - fileSelectorElement = document.createElement 'span' - fileSelectorElement.style.display = 'none' - fileSelectorElement.click = showFileChooserDialog.bind this, callback - return fileSelectorElement diff --git a/atom/renderer/lib/inspector.js b/atom/renderer/lib/inspector.js new file mode 100644 index 000000000000..ee578aefcfa2 --- /dev/null +++ b/atom/renderer/lib/inspector.js @@ -0,0 +1,85 @@ +var convertToMenuTemplate, createFileSelectorElement, createMenu, pathToHtml5FileObject, showFileChooserDialog; + +window.onload = function() { + + /* Use menu API to show context menu. */ + InspectorFrontendHost.showContextMenuAtPoint = createMenu; + + /* Use dialog API to override file chooser dialog. */ + return WebInspector.createFileSelectorElement = createFileSelectorElement; +}; + +convertToMenuTemplate = function(items) { + var fn, i, item, len, template; + template = []; + fn = function(item) { + var transformed; + transformed = item.type === 'subMenu' ? { + type: 'submenu', + label: item.label, + enabled: item.enabled, + submenu: convertToMenuTemplate(item.subItems) + } : item.type === 'separator' ? { + type: 'separator' + } : item.type === 'checkbox' ? { + type: 'checkbox', + label: item.label, + enabled: item.enabled, + checked: item.checked + } : { + type: 'normal', + label: item.label, + enabled: item.enabled + }; + if (item.id != null) { + transformed.click = function() { + DevToolsAPI.contextMenuItemSelected(item.id); + return DevToolsAPI.contextMenuCleared(); + }; + } + return template.push(transformed); + }; + for (i = 0, len = items.length; i < len; i++) { + item = items[i]; + fn(item); + } + return template; +}; + +createMenu = function(x, y, items, document) { + var Menu, menu, remote; + remote = require('electron').remote; + Menu = remote.Menu; + menu = Menu.buildFromTemplate(convertToMenuTemplate(items)); + + /* The menu is expected to show asynchronously. */ + return setTimeout(function() { + return menu.popup(remote.getCurrentWindow()); + }); +}; + +showFileChooserDialog = function(callback) { + var dialog, files, remote; + remote = require('electron').remote; + dialog = remote.dialog; + files = dialog.showOpenDialog({}); + if (files != null) { + return callback(pathToHtml5FileObject(files[0])); + } +}; + +pathToHtml5FileObject = function(path) { + var blob, fs; + fs = require('fs'); + blob = new Blob([fs.readFileSync(path)]); + blob.name = path; + return blob; +}; + +createFileSelectorElement = function(callback) { + var fileSelectorElement; + fileSelectorElement = document.createElement('span'); + fileSelectorElement.style.display = 'none'; + fileSelectorElement.click = showFileChooserDialog.bind(this, callback); + return fileSelectorElement; +}; diff --git a/atom/renderer/lib/override.coffee b/atom/renderer/lib/override.coffee deleted file mode 100644 index 5b031c0ad44a..000000000000 --- a/atom/renderer/lib/override.coffee +++ /dev/null @@ -1,129 +0,0 @@ -{ipcRenderer, remote} = require 'electron' - -### Helper function to resolve relative url. ### -a = window.top.document.createElement 'a' -resolveURL = (url) -> - a.href = url - a.href - -### Window object returned by "window.open". ### -class BrowserWindowProxy - @proxies: {} - - @getOrCreate: (guestId) -> - @proxies[guestId] ?= new BrowserWindowProxy(guestId) - - @remove: (guestId) -> - delete @proxies[guestId] - - constructor: (@guestId) -> - @closed = false - ipcRenderer.once "ATOM_SHELL_GUEST_WINDOW_MANAGER_WINDOW_CLOSED_#{@guestId}", => - BrowserWindowProxy.remove(@guestId) - @closed = true - - close: -> - ipcRenderer.send 'ATOM_SHELL_GUEST_WINDOW_MANAGER_WINDOW_CLOSE', @guestId - - focus: -> - ipcRenderer.send 'ATOM_SHELL_GUEST_WINDOW_MANAGER_WINDOW_METHOD', @guestId, 'focus' - - blur: -> - ipcRenderer.send 'ATOM_SHELL_GUEST_WINDOW_MANAGER_WINDOW_METHOD', @guestId, 'blur' - - postMessage: (message, targetOrigin='*') -> - ipcRenderer.send 'ATOM_SHELL_GUEST_WINDOW_MANAGER_WINDOW_POSTMESSAGE', @guestId, message, targetOrigin, location.origin - - eval: (args...) -> - ipcRenderer.send 'ATOM_SHELL_GUEST_WINDOW_MANAGER_WEB_CONTENTS_METHOD', @guestId, 'executeJavaScript', args... - -unless process.guestInstanceId? - ### Override default window.close. ### - window.close = -> - remote.getCurrentWindow().close() - -### Make the browser window or guest view emit "new-window" event. ### -window.open = (url, frameName='', features='') -> - options = {} - ints = [ 'x', 'y', 'width', 'height', 'min-width', 'max-width', 'min-height', 'max-height', 'zoom-factor' ] - ### Make sure to get rid of excessive whitespace in the property name ### - for feature in features.split /,\s*/ - [name, value] = feature.split /\s*=/ - options[name] = - if value is 'yes' or value is '1' - true - else if value is 'no' or value is '0' - false - else - value - options.x ?= options.left if options.left - options.y ?= options.top if options.top - options.title ?= frameName - options.width ?= 800 - options.height ?= 600 - - ### Resolve relative urls. ### - url = resolveURL url - - (options[name] = parseInt(options[name], 10) if options[name]?) for name in ints - - guestId = ipcRenderer.sendSync 'ATOM_SHELL_GUEST_WINDOW_MANAGER_WINDOW_OPEN', url, frameName, options - if guestId - BrowserWindowProxy.getOrCreate(guestId) - else - null - -### Use the dialog API to implement alert(). ### -window.alert = (message, title='') -> - buttons = ['OK'] - message = message.toString() - remote.dialog.showMessageBox remote.getCurrentWindow(), {message, title, buttons} - ### Alert should always return undefined. ### - return - -### And the confirm(). ### -window.confirm = (message, title='') -> - buttons = ['OK', 'Cancel'] - cancelId = 1 - not remote.dialog.showMessageBox remote.getCurrentWindow(), {message, title, buttons, cancelId} - -### But we do not support prompt(). ### -window.prompt = -> - throw new Error('prompt() is and will not be supported.') - -if process.openerId? - window.opener = BrowserWindowProxy.getOrCreate process.openerId - -ipcRenderer.on 'ATOM_SHELL_GUEST_WINDOW_POSTMESSAGE', (event, sourceId, message, sourceOrigin) -> - ### Manually dispatch event instead of using postMessage because we also need to ### - ### set event.source. ### - event = document.createEvent 'Event' - event.initEvent 'message', false, false - event.data = message - event.origin = sourceOrigin - event.source = BrowserWindowProxy.getOrCreate(sourceId) - window.dispatchEvent event - -### Forward history operations to browser. ### -sendHistoryOperation = (args...) -> - ipcRenderer.send 'ATOM_SHELL_NAVIGATION_CONTROLLER', args... - -getHistoryOperation = (args...) -> - ipcRenderer.sendSync 'ATOM_SHELL_SYNC_NAVIGATION_CONTROLLER', args... - -window.history.back = -> sendHistoryOperation 'goBack' -window.history.forward = -> sendHistoryOperation 'goForward' -window.history.go = (offset) -> sendHistoryOperation 'goToOffset', offset -Object.defineProperty window.history, 'length', - get: -> - getHistoryOperation 'length' - -### Make document.hidden and document.visibilityState return the correct value. ### -Object.defineProperty document, 'hidden', - get: -> - currentWindow = remote.getCurrentWindow() - currentWindow.isMinimized() || !currentWindow.isVisible() - -Object.defineProperty document, 'visibilityState', - get: -> - if document.hidden then "hidden" else "visible" diff --git a/atom/renderer/lib/override.js b/atom/renderer/lib/override.js new file mode 100644 index 000000000000..1a5791feb3fc --- /dev/null +++ b/atom/renderer/lib/override.js @@ -0,0 +1,249 @@ +var BrowserWindowProxy, a, getHistoryOperation, ipcRenderer, ref, remote, resolveURL, sendHistoryOperation, + slice = [].slice; + +ref = require('electron'), ipcRenderer = ref.ipcRenderer, remote = ref.remote; + + +/* Helper function to resolve relative url. */ + +a = window.top.document.createElement('a'); + +resolveURL = function(url) { + a.href = url; + return a.href; +}; + + +/* Window object returned by "window.open". */ + +BrowserWindowProxy = (function() { + BrowserWindowProxy.proxies = {}; + + BrowserWindowProxy.getOrCreate = function(guestId) { + var base; + return (base = this.proxies)[guestId] != null ? base[guestId] : base[guestId] = new BrowserWindowProxy(guestId); + }; + + BrowserWindowProxy.remove = function(guestId) { + return delete this.proxies[guestId]; + }; + + function BrowserWindowProxy(guestId1) { + this.guestId = guestId1; + this.closed = false; + ipcRenderer.once("ATOM_SHELL_GUEST_WINDOW_MANAGER_WINDOW_CLOSED_" + this.guestId, (function(_this) { + return function() { + BrowserWindowProxy.remove(_this.guestId); + return _this.closed = true; + }; + })(this)); + } + + BrowserWindowProxy.prototype.close = function() { + return ipcRenderer.send('ATOM_SHELL_GUEST_WINDOW_MANAGER_WINDOW_CLOSE', this.guestId); + }; + + BrowserWindowProxy.prototype.focus = function() { + return ipcRenderer.send('ATOM_SHELL_GUEST_WINDOW_MANAGER_WINDOW_METHOD', this.guestId, 'focus'); + }; + + BrowserWindowProxy.prototype.blur = function() { + return ipcRenderer.send('ATOM_SHELL_GUEST_WINDOW_MANAGER_WINDOW_METHOD', this.guestId, 'blur'); + }; + + BrowserWindowProxy.prototype.postMessage = function(message, targetOrigin) { + if (targetOrigin == null) { + targetOrigin = '*'; + } + return ipcRenderer.send('ATOM_SHELL_GUEST_WINDOW_MANAGER_WINDOW_POSTMESSAGE', this.guestId, message, targetOrigin, location.origin); + }; + + BrowserWindowProxy.prototype["eval"] = function() { + var args; + args = 1 <= arguments.length ? slice.call(arguments, 0) : []; + return ipcRenderer.send.apply(ipcRenderer, ['ATOM_SHELL_GUEST_WINDOW_MANAGER_WEB_CONTENTS_METHOD', this.guestId, 'executeJavaScript'].concat(slice.call(args))); + }; + + return BrowserWindowProxy; + +})(); + +if (process.guestInstanceId == null) { + + /* Override default window.close. */ + window.close = function() { + return remote.getCurrentWindow().close(); + }; +} + + +/* Make the browser window or guest view emit "new-window" event. */ + +window.open = function(url, frameName, features) { + var feature, guestId, i, ints, j, len, len1, name, options, ref1, ref2, value; + if (frameName == null) { + frameName = ''; + } + if (features == null) { + features = ''; + } + options = {}; + ints = ['x', 'y', 'width', 'height', 'min-width', 'max-width', 'min-height', 'max-height', 'zoom-factor']; + + /* Make sure to get rid of excessive whitespace in the property name */ + ref1 = features.split(/,\s*/); + for (i = 0, len = ref1.length; i < len; i++) { + feature = ref1[i]; + ref2 = feature.split(/\s*=/), name = ref2[0], value = ref2[1]; + options[name] = value === 'yes' || value === '1' ? true : value === 'no' || value === '0' ? false : value; + } + if (options.left) { + if (options.x == null) { + options.x = options.left; + } + } + if (options.top) { + if (options.y == null) { + options.y = options.top; + } + } + if (options.title == null) { + options.title = frameName; + } + if (options.width == null) { + options.width = 800; + } + if (options.height == null) { + options.height = 600; + } + + /* Resolve relative urls. */ + url = resolveURL(url); + for (j = 0, len1 = ints.length; j < len1; j++) { + name = ints[j]; + if (options[name] != null) { + options[name] = parseInt(options[name], 10); + } + } + guestId = ipcRenderer.sendSync('ATOM_SHELL_GUEST_WINDOW_MANAGER_WINDOW_OPEN', url, frameName, options); + if (guestId) { + return BrowserWindowProxy.getOrCreate(guestId); + } else { + return null; + } +}; + + +/* Use the dialog API to implement alert(). */ + +window.alert = function(message, title) { + var buttons; + if (title == null) { + title = ''; + } + buttons = ['OK']; + message = message.toString(); + remote.dialog.showMessageBox(remote.getCurrentWindow(), { + message: message, + title: title, + buttons: buttons + }); + + /* Alert should always return undefined. */ +}; + + +/* And the confirm(). */ + +window.confirm = function(message, title) { + var buttons, cancelId; + if (title == null) { + title = ''; + } + buttons = ['OK', 'Cancel']; + cancelId = 1; + return !remote.dialog.showMessageBox(remote.getCurrentWindow(), { + message: message, + title: title, + buttons: buttons, + cancelId: cancelId + }); +}; + + +/* But we do not support prompt(). */ + +window.prompt = function() { + throw new Error('prompt() is and will not be supported.'); +}; + +if (process.openerId != null) { + window.opener = BrowserWindowProxy.getOrCreate(process.openerId); +} + +ipcRenderer.on('ATOM_SHELL_GUEST_WINDOW_POSTMESSAGE', function(event, sourceId, message, sourceOrigin) { + + /* Manually dispatch event instead of using postMessage because we also need to */ + + /* set event.source. */ + event = document.createEvent('Event'); + event.initEvent('message', false, false); + event.data = message; + event.origin = sourceOrigin; + event.source = BrowserWindowProxy.getOrCreate(sourceId); + return window.dispatchEvent(event); +}); + + +/* Forward history operations to browser. */ + +sendHistoryOperation = function() { + var args; + args = 1 <= arguments.length ? slice.call(arguments, 0) : []; + return ipcRenderer.send.apply(ipcRenderer, ['ATOM_SHELL_NAVIGATION_CONTROLLER'].concat(slice.call(args))); +}; + +getHistoryOperation = function() { + var args; + args = 1 <= arguments.length ? slice.call(arguments, 0) : []; + return ipcRenderer.sendSync.apply(ipcRenderer, ['ATOM_SHELL_SYNC_NAVIGATION_CONTROLLER'].concat(slice.call(args))); +}; + +window.history.back = function() { + return sendHistoryOperation('goBack'); +}; + +window.history.forward = function() { + return sendHistoryOperation('goForward'); +}; + +window.history.go = function(offset) { + return sendHistoryOperation('goToOffset', offset); +}; + +Object.defineProperty(window.history, 'length', { + get: function() { + return getHistoryOperation('length'); + } +}); + + +/* Make document.hidden and document.visibilityState return the correct value. */ + +Object.defineProperty(document, 'hidden', { + get: function() { + var currentWindow; + currentWindow = remote.getCurrentWindow(); + return currentWindow.isMinimized() || !currentWindow.isVisible(); + } +}); + +Object.defineProperty(document, 'visibilityState', { + get: function() { + if (document.hidden) { + return "hidden"; + } else { + return "visible"; + } + } +}); diff --git a/atom/renderer/lib/web-view/guest-view-internal.coffee b/atom/renderer/lib/web-view/guest-view-internal.coffee deleted file mode 100644 index 30fe3d2a6e3d..000000000000 --- a/atom/renderer/lib/web-view/guest-view-internal.coffee +++ /dev/null @@ -1,89 +0,0 @@ -{ipcRenderer, webFrame} = require 'electron' - -requestId = 0 - -WEB_VIEW_EVENTS = - 'load-commit': ['url', 'isMainFrame'] - 'did-finish-load': [] - 'did-fail-load': ['errorCode', 'errorDescription', 'validatedURL'] - 'did-frame-finish-load': ['isMainFrame'] - 'did-start-loading': [] - 'did-stop-loading': [] - 'did-get-response-details': ['status', 'newURL', 'originalURL', - 'httpResponseCode', 'requestMethod', 'referrer', - 'headers'] - 'did-get-redirect-request': ['oldURL', 'newURL', 'isMainFrame'] - 'dom-ready': [] - 'console-message': ['level', 'message', 'line', 'sourceId'] - 'devtools-opened': [] - 'devtools-closed': [] - 'devtools-focused': [] - 'new-window': ['url', 'frameName', 'disposition', 'options'] - 'will-navigate': ['url'] - 'did-navigate': ['url'] - 'did-navigate-in-page': ['url'] - 'close': [] - 'crashed': [] - 'gpu-crashed': [] - 'plugin-crashed': ['name', 'version'] - 'media-started-playing': [] - 'media-paused': [] - 'did-change-theme-color': ['themeColor'] - 'destroyed': [] - 'page-title-updated': ['title', 'explicitSet'] - 'page-favicon-updated': ['favicons'] - 'enter-html-full-screen': [] - 'leave-html-full-screen': [] - 'found-in-page': ['result'] - -DEPRECATED_EVENTS = - 'page-title-updated': 'page-title-set' - -dispatchEvent = (webView, eventName, eventKey, args...) -> - if DEPRECATED_EVENTS[eventName]? - dispatchEvent webView, DEPRECATED_EVENTS[eventName], eventKey, args... - domEvent = new Event(eventName) - for f, i in WEB_VIEW_EVENTS[eventKey] - domEvent[f] = args[i] - webView.dispatchEvent domEvent - webView.onLoadCommit domEvent if eventName is 'load-commit' - -module.exports = - registerEvents: (webView, viewInstanceId) -> - ipcRenderer.on "ATOM_SHELL_GUEST_VIEW_INTERNAL_DISPATCH_EVENT-#{viewInstanceId}", (event, eventName, args...) -> - dispatchEvent webView, eventName, eventName, args... - - ipcRenderer.on "ATOM_SHELL_GUEST_VIEW_INTERNAL_IPC_MESSAGE-#{viewInstanceId}", (event, channel, args...) -> - domEvent = new Event('ipc-message') - domEvent.channel = channel - domEvent.args = [args...] - webView.dispatchEvent domEvent - - ipcRenderer.on "ATOM_SHELL_GUEST_VIEW_INTERNAL_SIZE_CHANGED-#{viewInstanceId}", (event, args...) -> - domEvent = new Event('size-changed') - for f, i in ['oldWidth', 'oldHeight', 'newWidth', 'newHeight'] - domEvent[f] = args[i] - webView.onSizeChanged domEvent - - deregisterEvents: (viewInstanceId) -> - ipcRenderer.removeAllListeners "ATOM_SHELL_GUEST_VIEW_INTERNAL_DISPATCH_EVENT-#{viewInstanceId}" - ipcRenderer.removeAllListeners "ATOM_SHELL_GUEST_VIEW_INTERNAL_IPC_MESSAGE-#{viewInstanceId}" - ipcRenderer.removeAllListeners "ATOM_SHELL_GUEST_VIEW_INTERNAL_SIZE_CHANGED-#{viewInstanceId}" - - createGuest: (params, callback) -> - requestId++ - ipcRenderer.send 'ATOM_SHELL_GUEST_VIEW_MANAGER_CREATE_GUEST', params, requestId - ipcRenderer.once "ATOM_SHELL_RESPONSE_#{requestId}", callback - - attachGuest: (elementInstanceId, guestInstanceId, params) -> - ipcRenderer.send 'ATOM_SHELL_GUEST_VIEW_MANAGER_ATTACH_GUEST', elementInstanceId, guestInstanceId, params - webFrame.attachGuest elementInstanceId - - destroyGuest: (guestInstanceId) -> - ipcRenderer.send 'ATOM_SHELL_GUEST_VIEW_MANAGER_DESTROY_GUEST', guestInstanceId - - setSize: (guestInstanceId, params) -> - ipcRenderer.send 'ATOM_SHELL_GUEST_VIEW_MANAGER_SET_SIZE', guestInstanceId, params - - setAllowTransparency: (guestInstanceId, allowtransparency) -> - ipcRenderer.send 'ATOM_SHELL_GUEST_VIEW_MANAGER_SET_ALLOW_TRANSPARENCY', guestInstanceId, allowtransparency diff --git a/atom/renderer/lib/web-view/guest-view-internal.js b/atom/renderer/lib/web-view/guest-view-internal.js new file mode 100644 index 000000000000..5dd86aff6613 --- /dev/null +++ b/atom/renderer/lib/web-view/guest-view-internal.js @@ -0,0 +1,113 @@ +var DEPRECATED_EVENTS, WEB_VIEW_EVENTS, dispatchEvent, ipcRenderer, ref, requestId, webFrame, + slice = [].slice; + +ref = require('electron'), ipcRenderer = ref.ipcRenderer, webFrame = ref.webFrame; + +requestId = 0; + +WEB_VIEW_EVENTS = { + 'load-commit': ['url', 'isMainFrame'], + 'did-finish-load': [], + 'did-fail-load': ['errorCode', 'errorDescription', 'validatedURL'], + 'did-frame-finish-load': ['isMainFrame'], + 'did-start-loading': [], + 'did-stop-loading': [], + 'did-get-response-details': ['status', 'newURL', 'originalURL', 'httpResponseCode', 'requestMethod', 'referrer', 'headers'], + 'did-get-redirect-request': ['oldURL', 'newURL', 'isMainFrame'], + 'dom-ready': [], + 'console-message': ['level', 'message', 'line', 'sourceId'], + 'devtools-opened': [], + 'devtools-closed': [], + 'devtools-focused': [], + 'new-window': ['url', 'frameName', 'disposition', 'options'], + 'will-navigate': ['url'], + 'did-navigate': ['url'], + 'did-navigate-in-page': ['url'], + 'close': [], + 'crashed': [], + 'gpu-crashed': [], + 'plugin-crashed': ['name', 'version'], + 'media-started-playing': [], + 'media-paused': [], + 'did-change-theme-color': ['themeColor'], + 'destroyed': [], + 'page-title-updated': ['title', 'explicitSet'], + 'page-favicon-updated': ['favicons'], + 'enter-html-full-screen': [], + 'leave-html-full-screen': [], + 'found-in-page': ['result'] +}; + +DEPRECATED_EVENTS = { + 'page-title-updated': 'page-title-set' +}; + +dispatchEvent = function() { + var args, domEvent, eventKey, eventName, f, i, j, len, ref1, webView; + webView = arguments[0], eventName = arguments[1], eventKey = arguments[2], args = 4 <= arguments.length ? slice.call(arguments, 3) : []; + if (DEPRECATED_EVENTS[eventName] != null) { + dispatchEvent.apply(null, [webView, DEPRECATED_EVENTS[eventName], eventKey].concat(slice.call(args))); + } + domEvent = new Event(eventName); + ref1 = WEB_VIEW_EVENTS[eventKey]; + for (i = j = 0, len = ref1.length; j < len; i = ++j) { + f = ref1[i]; + domEvent[f] = args[i]; + } + webView.dispatchEvent(domEvent); + if (eventName === 'load-commit') { + return webView.onLoadCommit(domEvent); + } +}; + +module.exports = { + registerEvents: function(webView, viewInstanceId) { + ipcRenderer.on("ATOM_SHELL_GUEST_VIEW_INTERNAL_DISPATCH_EVENT-" + viewInstanceId, function() { + var args, event, eventName; + event = arguments[0], eventName = arguments[1], args = 3 <= arguments.length ? slice.call(arguments, 2) : []; + return dispatchEvent.apply(null, [webView, eventName, eventName].concat(slice.call(args))); + }); + ipcRenderer.on("ATOM_SHELL_GUEST_VIEW_INTERNAL_IPC_MESSAGE-" + viewInstanceId, function() { + var args, channel, domEvent, event; + event = arguments[0], channel = arguments[1], args = 3 <= arguments.length ? slice.call(arguments, 2) : []; + domEvent = new Event('ipc-message'); + domEvent.channel = channel; + domEvent.args = slice.call(args); + return webView.dispatchEvent(domEvent); + }); + return ipcRenderer.on("ATOM_SHELL_GUEST_VIEW_INTERNAL_SIZE_CHANGED-" + viewInstanceId, function() { + var args, domEvent, event, f, i, j, len, ref1; + event = arguments[0], args = 2 <= arguments.length ? slice.call(arguments, 1) : []; + domEvent = new Event('size-changed'); + ref1 = ['oldWidth', 'oldHeight', 'newWidth', 'newHeight']; + for (i = j = 0, len = ref1.length; j < len; i = ++j) { + f = ref1[i]; + domEvent[f] = args[i]; + } + return webView.onSizeChanged(domEvent); + }); + }, + deregisterEvents: function(viewInstanceId) { + ipcRenderer.removeAllListeners("ATOM_SHELL_GUEST_VIEW_INTERNAL_DISPATCH_EVENT-" + viewInstanceId); + ipcRenderer.removeAllListeners("ATOM_SHELL_GUEST_VIEW_INTERNAL_IPC_MESSAGE-" + viewInstanceId); + return ipcRenderer.removeAllListeners("ATOM_SHELL_GUEST_VIEW_INTERNAL_SIZE_CHANGED-" + viewInstanceId); + }, + createGuest: function(params, callback) { + requestId++; + ipcRenderer.send('ATOM_SHELL_GUEST_VIEW_MANAGER_CREATE_GUEST', params, requestId); + return ipcRenderer.once("ATOM_SHELL_RESPONSE_" + requestId, callback); + }, + attachGuest: function(elementInstanceId, guestInstanceId, params) { + ipcRenderer.send('ATOM_SHELL_GUEST_VIEW_MANAGER_ATTACH_GUEST', elementInstanceId, guestInstanceId, params); + return webFrame.attachGuest(elementInstanceId); + }, + destroyGuest: function(guestInstanceId) { + return ipcRenderer.send('ATOM_SHELL_GUEST_VIEW_MANAGER_DESTROY_GUEST', guestInstanceId); + }, + setSize: function(guestInstanceId, params) { + return ipcRenderer.send('ATOM_SHELL_GUEST_VIEW_MANAGER_SET_SIZE', guestInstanceId, params); + }, + setAllowTransparency: function(guestInstanceId, allowtransparency) { + return ipcRenderer.send('ATOM_SHELL_GUEST_VIEW_MANAGER_SET_ALLOW_TRANSPARENCY', guestInstanceId, allowtransparency); + } +}; diff --git a/atom/renderer/lib/web-view/web-view-attributes.coffee b/atom/renderer/lib/web-view/web-view-attributes.coffee deleted file mode 100644 index 051a0c8a86b3..000000000000 --- a/atom/renderer/lib/web-view/web-view-attributes.coffee +++ /dev/null @@ -1,240 +0,0 @@ -WebViewImpl = require './web-view' -guestViewInternal = require './guest-view-internal' -webViewConstants = require './web-view-constants' - -{remote} = require 'electron' - -### Helper function to resolve url set in attribute. ### -a = document.createElement 'a' -resolveURL = (url) -> - a.href = url - a.href - -### - Attribute objects. - Default implementation of a WebView attribute. -### -class WebViewAttribute - constructor: (name, webViewImpl) -> - @name = name - @webViewImpl = webViewImpl - @ignoreMutation = false - - @defineProperty() - - ### Retrieves and returns the attribute's value. ### - getValue: -> @webViewImpl.webviewNode.getAttribute(@name) || '' - - ### Sets the attribute's value. ### - setValue: (value) -> @webViewImpl.webviewNode.setAttribute(@name, value || '') - - ### Changes the attribute's value without triggering its mutation handler. ### - setValueIgnoreMutation: (value) -> - @ignoreMutation = true - @setValue value - @ignoreMutation = false - - ### Defines this attribute as a property on the webview node. ### - defineProperty: -> - Object.defineProperty @webViewImpl.webviewNode, @name, - get: => @getValue() - set: (value) => @setValue value - enumerable: true - - ### Called when the attribute's value changes. ### - handleMutation: -> - -### An attribute that is treated as a Boolean. ### -class BooleanAttribute extends WebViewAttribute - constructor: (name, webViewImpl) -> - super name, webViewImpl - - getValue: -> @webViewImpl.webviewNode.hasAttribute @name - - setValue: (value) -> - unless value - @webViewImpl.webviewNode.removeAttribute @name - else - @webViewImpl.webviewNode.setAttribute @name, '' - -### Attribute that specifies whether transparency is allowed in the webview. ### -class AllowTransparencyAttribute extends BooleanAttribute - constructor: (webViewImpl) -> - super webViewConstants.ATTRIBUTE_ALLOWTRANSPARENCY, webViewImpl - - handleMutation: (oldValue, newValue) -> - return unless @webViewImpl.guestInstanceId - guestViewInternal.setAllowTransparency @webViewImpl.guestInstanceId, @getValue() - -### Attribute used to define the demension limits of autosizing. ### -class AutosizeDimensionAttribute extends WebViewAttribute - constructor: (name, webViewImpl) -> - super name, webViewImpl - - getValue: -> parseInt(@webViewImpl.webviewNode.getAttribute(@name)) || 0 - - handleMutation: (oldValue, newValue) -> - return unless @webViewImpl.guestInstanceId - guestViewInternal.setSize @webViewImpl.guestInstanceId, - enableAutoSize: @webViewImpl.attributes[webViewConstants.ATTRIBUTE_AUTOSIZE].getValue() - min: - width: parseInt @webViewImpl.attributes[webViewConstants.ATTRIBUTE_MINWIDTH].getValue() || 0 - height: parseInt @webViewImpl.attributes[webViewConstants.ATTRIBUTE_MINHEIGHT].getValue() || 0 - max: - width: parseInt @webViewImpl.attributes[webViewConstants.ATTRIBUTE_MAXWIDTH].getValue() || 0 - height: parseInt @webViewImpl.attributes[webViewConstants.ATTRIBUTE_MAXHEIGHT].getValue() || 0 - -### Attribute that specifies whether the webview should be autosized. ### -class AutosizeAttribute extends BooleanAttribute - constructor: (webViewImpl) -> - super webViewConstants.ATTRIBUTE_AUTOSIZE, webViewImpl - - handleMutation: AutosizeDimensionAttribute::handleMutation - -### Attribute representing the state of the storage partition. ### -class PartitionAttribute extends WebViewAttribute - constructor: (webViewImpl) -> - super webViewConstants.ATTRIBUTE_PARTITION, webViewImpl - @validPartitionId = true - - handleMutation: (oldValue, newValue) -> - newValue = newValue || '' - - ### The partition cannot change if the webview has already navigated. ### - unless @webViewImpl.beforeFirstNavigation - window.console.error webViewConstants.ERROR_MSG_ALREADY_NAVIGATED - @setValueIgnoreMutation oldValue - return - - if newValue is 'persist:' - @validPartitionId = false - window.console.error webViewConstants.ERROR_MSG_INVALID_PARTITION_ATTRIBUTE - -### Attribute that handles the location and navigation of the webview. ### -class SrcAttribute extends WebViewAttribute - constructor: (webViewImpl) -> - super webViewConstants.ATTRIBUTE_SRC, webViewImpl - @setupMutationObserver() - - getValue: -> - if @webViewImpl.webviewNode.hasAttribute @name - resolveURL @webViewImpl.webviewNode.getAttribute(@name) - else - '' - - setValueIgnoreMutation: (value) -> - WebViewAttribute::setValueIgnoreMutation.call(this, value) - ### - takeRecords() is needed to clear queued up src mutations. Without it, it - is possible for this change to get picked up asyncronously by src's - mutation observer |observer|, and then get handled even though we do not - want to handle this mutation. - ### - @observer.takeRecords() - - handleMutation: (oldValue, newValue) -> - ### - Once we have navigated, we don't allow clearing the src attribute. - Once enters a navigated state, it cannot return to a - placeholder state. - ### - if not newValue and oldValue - ### - src attribute changes normally initiate a navigation. We suppress - the next src attribute handler call to avoid reloading the page - on every guest-initiated navigation. - ### - @setValueIgnoreMutation oldValue - return - @parse() - - ### - The purpose of this mutation observer is to catch assignment to the src - attribute without any changes to its value. This is useful in the case - where the webview guest has crashed and navigating to the same address - spawns off a new process. - ### - setupMutationObserver: -> - @observer = new MutationObserver (mutations) => - for mutation in mutations - oldValue = mutation.oldValue - newValue = @getValue() - return if oldValue isnt newValue - @handleMutation oldValue, newValue - params = - attributes: true, - attributeOldValue: true, - attributeFilter: [@name] - @observer.observe @webViewImpl.webviewNode, params - - parse: -> - if not @webViewImpl.elementAttached or - not @webViewImpl.attributes[webViewConstants.ATTRIBUTE_PARTITION].validPartitionId or - not @.getValue() - return - - unless @webViewImpl.guestInstanceId? - if @webViewImpl.beforeFirstNavigation - @webViewImpl.beforeFirstNavigation = false - @webViewImpl.createGuest() - return - - ### Navigate to |this.src|. ### - opts = {} - httpreferrer = @webViewImpl.attributes[webViewConstants.ATTRIBUTE_HTTPREFERRER].getValue() - if httpreferrer then opts.httpReferrer = httpreferrer - - useragent = @webViewImpl.attributes[webViewConstants.ATTRIBUTE_USERAGENT].getValue() - if useragent then opts.userAgent = useragent - - guestContents = remote.getGuestWebContents(@webViewImpl.guestInstanceId) - guestContents.loadURL @getValue(), opts - -### Attribute specifies HTTP referrer. ### -class HttpReferrerAttribute extends WebViewAttribute - constructor: (webViewImpl) -> - super webViewConstants.ATTRIBUTE_HTTPREFERRER, webViewImpl - -### Attribute specifies user agent ### -class UserAgentAttribute extends WebViewAttribute - constructor: (webViewImpl) -> - super webViewConstants.ATTRIBUTE_USERAGENT, webViewImpl - -### Attribute that set preload script. ### -class PreloadAttribute extends WebViewAttribute - constructor: (webViewImpl) -> - super webViewConstants.ATTRIBUTE_PRELOAD, webViewImpl - - getValue: -> - return '' unless @webViewImpl.webviewNode.hasAttribute @name - preload = resolveURL @webViewImpl.webviewNode.getAttribute(@name) - protocol = preload.substr 0, 5 - unless protocol is 'file:' - console.error webViewConstants.ERROR_MSG_INVALID_PRELOAD_ATTRIBUTE - preload = '' - preload - -### Sets up all of the webview attributes. ### -WebViewImpl::setupWebViewAttributes = -> - @attributes = {} - - @attributes[webViewConstants.ATTRIBUTE_ALLOWTRANSPARENCY] = new AllowTransparencyAttribute(this) - @attributes[webViewConstants.ATTRIBUTE_AUTOSIZE] = new AutosizeAttribute(this) - @attributes[webViewConstants.ATTRIBUTE_PARTITION] = new PartitionAttribute(this) - @attributes[webViewConstants.ATTRIBUTE_SRC] = new SrcAttribute(this) - @attributes[webViewConstants.ATTRIBUTE_HTTPREFERRER] = new HttpReferrerAttribute(this) - @attributes[webViewConstants.ATTRIBUTE_USERAGENT] = new UserAgentAttribute(this) - @attributes[webViewConstants.ATTRIBUTE_NODEINTEGRATION] = new BooleanAttribute(webViewConstants.ATTRIBUTE_NODEINTEGRATION, this) - @attributes[webViewConstants.ATTRIBUTE_PLUGINS] = new BooleanAttribute(webViewConstants.ATTRIBUTE_PLUGINS, this) - @attributes[webViewConstants.ATTRIBUTE_DISABLEWEBSECURITY] = new BooleanAttribute(webViewConstants.ATTRIBUTE_DISABLEWEBSECURITY, this) - @attributes[webViewConstants.ATTRIBUTE_ALLOWPOPUPS] = new BooleanAttribute(webViewConstants.ATTRIBUTE_ALLOWPOPUPS, this) - @attributes[webViewConstants.ATTRIBUTE_PRELOAD] = new PreloadAttribute(this) - - autosizeAttributes = [ - webViewConstants.ATTRIBUTE_MAXHEIGHT - webViewConstants.ATTRIBUTE_MAXWIDTH - webViewConstants.ATTRIBUTE_MINHEIGHT - webViewConstants.ATTRIBUTE_MINWIDTH - ] - for attribute in autosizeAttributes - @attributes[attribute] = new AutosizeDimensionAttribute(attribute, this) diff --git a/atom/renderer/lib/web-view/web-view-attributes.js b/atom/renderer/lib/web-view/web-view-attributes.js new file mode 100644 index 000000000000..fd5dad21c58e --- /dev/null +++ b/atom/renderer/lib/web-view/web-view-attributes.js @@ -0,0 +1,410 @@ +var AllowTransparencyAttribute, AutosizeAttribute, AutosizeDimensionAttribute, BooleanAttribute, HttpReferrerAttribute, PartitionAttribute, PreloadAttribute, SrcAttribute, UserAgentAttribute, WebViewAttribute, WebViewImpl, a, guestViewInternal, remote, resolveURL, webViewConstants, + extend = function(child, parent) { for (var key in parent) { if (hasProp.call(parent, key)) child[key] = parent[key]; } function ctor() { this.constructor = child; } ctor.prototype = parent.prototype; child.prototype = new ctor(); child.__super__ = parent.prototype; return child; }, + hasProp = {}.hasOwnProperty; + +WebViewImpl = require('./web-view'); + +guestViewInternal = require('./guest-view-internal'); + +webViewConstants = require('./web-view-constants'); + +remote = require('electron').remote; + + +/* Helper function to resolve url set in attribute. */ + +a = document.createElement('a'); + +resolveURL = function(url) { + a.href = url; + return a.href; +}; + + +/* + Attribute objects. + Default implementation of a WebView attribute. + */ + +WebViewAttribute = (function() { + function WebViewAttribute(name, webViewImpl) { + this.name = name; + this.webViewImpl = webViewImpl; + this.ignoreMutation = false; + this.defineProperty(); + } + + + /* Retrieves and returns the attribute's value. */ + + WebViewAttribute.prototype.getValue = function() { + return this.webViewImpl.webviewNode.getAttribute(this.name) || ''; + }; + + + /* Sets the attribute's value. */ + + WebViewAttribute.prototype.setValue = function(value) { + return this.webViewImpl.webviewNode.setAttribute(this.name, value || ''); + }; + + + /* Changes the attribute's value without triggering its mutation handler. */ + + WebViewAttribute.prototype.setValueIgnoreMutation = function(value) { + this.ignoreMutation = true; + this.setValue(value); + return this.ignoreMutation = false; + }; + + + /* Defines this attribute as a property on the webview node. */ + + WebViewAttribute.prototype.defineProperty = function() { + return Object.defineProperty(this.webViewImpl.webviewNode, this.name, { + get: (function(_this) { + return function() { + return _this.getValue(); + }; + })(this), + set: (function(_this) { + return function(value) { + return _this.setValue(value); + }; + })(this), + enumerable: true + }); + }; + + + /* Called when the attribute's value changes. */ + + WebViewAttribute.prototype.handleMutation = function() {}; + + return WebViewAttribute; + +})(); + + +/* An attribute that is treated as a Boolean. */ + +BooleanAttribute = (function(superClass) { + extend(BooleanAttribute, superClass); + + function BooleanAttribute(name, webViewImpl) { + BooleanAttribute.__super__.constructor.call(this, name, webViewImpl); + } + + BooleanAttribute.prototype.getValue = function() { + return this.webViewImpl.webviewNode.hasAttribute(this.name); + }; + + BooleanAttribute.prototype.setValue = function(value) { + if (!value) { + return this.webViewImpl.webviewNode.removeAttribute(this.name); + } else { + return this.webViewImpl.webviewNode.setAttribute(this.name, ''); + } + }; + + return BooleanAttribute; + +})(WebViewAttribute); + + +/* Attribute that specifies whether transparency is allowed in the webview. */ + +AllowTransparencyAttribute = (function(superClass) { + extend(AllowTransparencyAttribute, superClass); + + function AllowTransparencyAttribute(webViewImpl) { + AllowTransparencyAttribute.__super__.constructor.call(this, webViewConstants.ATTRIBUTE_ALLOWTRANSPARENCY, webViewImpl); + } + + AllowTransparencyAttribute.prototype.handleMutation = function(oldValue, newValue) { + if (!this.webViewImpl.guestInstanceId) { + return; + } + return guestViewInternal.setAllowTransparency(this.webViewImpl.guestInstanceId, this.getValue()); + }; + + return AllowTransparencyAttribute; + +})(BooleanAttribute); + + +/* Attribute used to define the demension limits of autosizing. */ + +AutosizeDimensionAttribute = (function(superClass) { + extend(AutosizeDimensionAttribute, superClass); + + function AutosizeDimensionAttribute(name, webViewImpl) { + AutosizeDimensionAttribute.__super__.constructor.call(this, name, webViewImpl); + } + + AutosizeDimensionAttribute.prototype.getValue = function() { + return parseInt(this.webViewImpl.webviewNode.getAttribute(this.name)) || 0; + }; + + AutosizeDimensionAttribute.prototype.handleMutation = function(oldValue, newValue) { + if (!this.webViewImpl.guestInstanceId) { + return; + } + return guestViewInternal.setSize(this.webViewImpl.guestInstanceId, { + enableAutoSize: this.webViewImpl.attributes[webViewConstants.ATTRIBUTE_AUTOSIZE].getValue(), + min: { + width: parseInt(this.webViewImpl.attributes[webViewConstants.ATTRIBUTE_MINWIDTH].getValue() || 0), + height: parseInt(this.webViewImpl.attributes[webViewConstants.ATTRIBUTE_MINHEIGHT].getValue() || 0) + }, + max: { + width: parseInt(this.webViewImpl.attributes[webViewConstants.ATTRIBUTE_MAXWIDTH].getValue() || 0), + height: parseInt(this.webViewImpl.attributes[webViewConstants.ATTRIBUTE_MAXHEIGHT].getValue() || 0) + } + }); + }; + + return AutosizeDimensionAttribute; + +})(WebViewAttribute); + + +/* Attribute that specifies whether the webview should be autosized. */ + +AutosizeAttribute = (function(superClass) { + extend(AutosizeAttribute, superClass); + + function AutosizeAttribute(webViewImpl) { + AutosizeAttribute.__super__.constructor.call(this, webViewConstants.ATTRIBUTE_AUTOSIZE, webViewImpl); + } + + AutosizeAttribute.prototype.handleMutation = AutosizeDimensionAttribute.prototype.handleMutation; + + return AutosizeAttribute; + +})(BooleanAttribute); + + +/* Attribute representing the state of the storage partition. */ + +PartitionAttribute = (function(superClass) { + extend(PartitionAttribute, superClass); + + function PartitionAttribute(webViewImpl) { + PartitionAttribute.__super__.constructor.call(this, webViewConstants.ATTRIBUTE_PARTITION, webViewImpl); + this.validPartitionId = true; + } + + PartitionAttribute.prototype.handleMutation = function(oldValue, newValue) { + newValue = newValue || ''; + + /* The partition cannot change if the webview has already navigated. */ + if (!this.webViewImpl.beforeFirstNavigation) { + window.console.error(webViewConstants.ERROR_MSG_ALREADY_NAVIGATED); + this.setValueIgnoreMutation(oldValue); + return; + } + if (newValue === 'persist:') { + this.validPartitionId = false; + return window.console.error(webViewConstants.ERROR_MSG_INVALID_PARTITION_ATTRIBUTE); + } + }; + + return PartitionAttribute; + +})(WebViewAttribute); + + +/* Attribute that handles the location and navigation of the webview. */ + +SrcAttribute = (function(superClass) { + extend(SrcAttribute, superClass); + + function SrcAttribute(webViewImpl) { + SrcAttribute.__super__.constructor.call(this, webViewConstants.ATTRIBUTE_SRC, webViewImpl); + this.setupMutationObserver(); + } + + SrcAttribute.prototype.getValue = function() { + if (this.webViewImpl.webviewNode.hasAttribute(this.name)) { + return resolveURL(this.webViewImpl.webviewNode.getAttribute(this.name)); + } else { + return ''; + } + }; + + SrcAttribute.prototype.setValueIgnoreMutation = function(value) { + WebViewAttribute.prototype.setValueIgnoreMutation.call(this, value); + + /* + takeRecords() is needed to clear queued up src mutations. Without it, it + is possible for this change to get picked up asyncronously by src's + mutation observer |observer|, and then get handled even though we do not + want to handle this mutation. + */ + return this.observer.takeRecords(); + }; + + SrcAttribute.prototype.handleMutation = function(oldValue, newValue) { + + /* + Once we have navigated, we don't allow clearing the src attribute. + Once enters a navigated state, it cannot return to a + placeholder state. + */ + if (!newValue && oldValue) { + + /* + src attribute changes normally initiate a navigation. We suppress + the next src attribute handler call to avoid reloading the page + on every guest-initiated navigation. + */ + this.setValueIgnoreMutation(oldValue); + return; + } + return this.parse(); + }; + + + /* + The purpose of this mutation observer is to catch assignment to the src + attribute without any changes to its value. This is useful in the case + where the webview guest has crashed and navigating to the same address + spawns off a new process. + */ + + SrcAttribute.prototype.setupMutationObserver = function() { + var params; + this.observer = new MutationObserver((function(_this) { + return function(mutations) { + var i, len, mutation, newValue, oldValue; + for (i = 0, len = mutations.length; i < len; i++) { + mutation = mutations[i]; + oldValue = mutation.oldValue; + newValue = _this.getValue(); + if (oldValue !== newValue) { + return; + } + _this.handleMutation(oldValue, newValue); + } + }; + })(this)); + params = { + attributes: true, + attributeOldValue: true, + attributeFilter: [this.name] + }; + return this.observer.observe(this.webViewImpl.webviewNode, params); + }; + + SrcAttribute.prototype.parse = function() { + var guestContents, httpreferrer, opts, useragent; + if (!this.webViewImpl.elementAttached || !this.webViewImpl.attributes[webViewConstants.ATTRIBUTE_PARTITION].validPartitionId || !this.getValue()) { + return; + } + if (this.webViewImpl.guestInstanceId == null) { + if (this.webViewImpl.beforeFirstNavigation) { + this.webViewImpl.beforeFirstNavigation = false; + this.webViewImpl.createGuest(); + } + return; + } + + /* Navigate to |this.src|. */ + opts = {}; + httpreferrer = this.webViewImpl.attributes[webViewConstants.ATTRIBUTE_HTTPREFERRER].getValue(); + if (httpreferrer) { + opts.httpReferrer = httpreferrer; + } + useragent = this.webViewImpl.attributes[webViewConstants.ATTRIBUTE_USERAGENT].getValue(); + if (useragent) { + opts.userAgent = useragent; + } + guestContents = remote.getGuestWebContents(this.webViewImpl.guestInstanceId); + return guestContents.loadURL(this.getValue(), opts); + }; + + return SrcAttribute; + +})(WebViewAttribute); + + +/* Attribute specifies HTTP referrer. */ + +HttpReferrerAttribute = (function(superClass) { + extend(HttpReferrerAttribute, superClass); + + function HttpReferrerAttribute(webViewImpl) { + HttpReferrerAttribute.__super__.constructor.call(this, webViewConstants.ATTRIBUTE_HTTPREFERRER, webViewImpl); + } + + return HttpReferrerAttribute; + +})(WebViewAttribute); + + +/* Attribute specifies user agent */ + +UserAgentAttribute = (function(superClass) { + extend(UserAgentAttribute, superClass); + + function UserAgentAttribute(webViewImpl) { + UserAgentAttribute.__super__.constructor.call(this, webViewConstants.ATTRIBUTE_USERAGENT, webViewImpl); + } + + return UserAgentAttribute; + +})(WebViewAttribute); + + +/* Attribute that set preload script. */ + +PreloadAttribute = (function(superClass) { + extend(PreloadAttribute, superClass); + + function PreloadAttribute(webViewImpl) { + PreloadAttribute.__super__.constructor.call(this, webViewConstants.ATTRIBUTE_PRELOAD, webViewImpl); + } + + PreloadAttribute.prototype.getValue = function() { + var preload, protocol; + if (!this.webViewImpl.webviewNode.hasAttribute(this.name)) { + return ''; + } + preload = resolveURL(this.webViewImpl.webviewNode.getAttribute(this.name)); + protocol = preload.substr(0, 5); + if (protocol !== 'file:') { + console.error(webViewConstants.ERROR_MSG_INVALID_PRELOAD_ATTRIBUTE); + preload = ''; + } + return preload; + }; + + return PreloadAttribute; + +})(WebViewAttribute); + + +/* Sets up all of the webview attributes. */ + +WebViewImpl.prototype.setupWebViewAttributes = function() { + var attribute, autosizeAttributes, i, len, results; + this.attributes = {}; + this.attributes[webViewConstants.ATTRIBUTE_ALLOWTRANSPARENCY] = new AllowTransparencyAttribute(this); + this.attributes[webViewConstants.ATTRIBUTE_AUTOSIZE] = new AutosizeAttribute(this); + this.attributes[webViewConstants.ATTRIBUTE_PARTITION] = new PartitionAttribute(this); + this.attributes[webViewConstants.ATTRIBUTE_SRC] = new SrcAttribute(this); + this.attributes[webViewConstants.ATTRIBUTE_HTTPREFERRER] = new HttpReferrerAttribute(this); + this.attributes[webViewConstants.ATTRIBUTE_USERAGENT] = new UserAgentAttribute(this); + this.attributes[webViewConstants.ATTRIBUTE_NODEINTEGRATION] = new BooleanAttribute(webViewConstants.ATTRIBUTE_NODEINTEGRATION, this); + this.attributes[webViewConstants.ATTRIBUTE_PLUGINS] = new BooleanAttribute(webViewConstants.ATTRIBUTE_PLUGINS, this); + this.attributes[webViewConstants.ATTRIBUTE_DISABLEWEBSECURITY] = new BooleanAttribute(webViewConstants.ATTRIBUTE_DISABLEWEBSECURITY, this); + this.attributes[webViewConstants.ATTRIBUTE_ALLOWPOPUPS] = new BooleanAttribute(webViewConstants.ATTRIBUTE_ALLOWPOPUPS, this); + this.attributes[webViewConstants.ATTRIBUTE_PRELOAD] = new PreloadAttribute(this); + autosizeAttributes = [webViewConstants.ATTRIBUTE_MAXHEIGHT, webViewConstants.ATTRIBUTE_MAXWIDTH, webViewConstants.ATTRIBUTE_MINHEIGHT, webViewConstants.ATTRIBUTE_MINWIDTH]; + results = []; + for (i = 0, len = autosizeAttributes.length; i < len; i++) { + attribute = autosizeAttributes[i]; + results.push(this.attributes[attribute] = new AutosizeDimensionAttribute(attribute, this)); + } + return results; +}; diff --git a/atom/renderer/lib/web-view/web-view-constants.coffee b/atom/renderer/lib/web-view/web-view-constants.coffee deleted file mode 100644 index 520517fbf58a..000000000000 --- a/atom/renderer/lib/web-view/web-view-constants.coffee +++ /dev/null @@ -1,28 +0,0 @@ -module.exports = - ### Attributes. ### - ATTRIBUTE_ALLOWTRANSPARENCY: 'allowtransparency' - ATTRIBUTE_AUTOSIZE: 'autosize' - ATTRIBUTE_MAXHEIGHT: 'maxheight' - ATTRIBUTE_MAXWIDTH: 'maxwidth' - ATTRIBUTE_MINHEIGHT: 'minheight' - ATTRIBUTE_MINWIDTH: 'minwidth' - ATTRIBUTE_NAME: 'name' - ATTRIBUTE_PARTITION: 'partition' - ATTRIBUTE_SRC: 'src' - ATTRIBUTE_HTTPREFERRER: 'httpreferrer' - ATTRIBUTE_NODEINTEGRATION: 'nodeintegration' - ATTRIBUTE_PLUGINS: 'plugins' - ATTRIBUTE_DISABLEWEBSECURITY: 'disablewebsecurity' - ATTRIBUTE_ALLOWPOPUPS: 'allowpopups' - ATTRIBUTE_PRELOAD: 'preload' - ATTRIBUTE_USERAGENT: 'useragent' - - ### Internal attribute. ### - ATTRIBUTE_INTERNALINSTANCEID: 'internalinstanceid' - - ### Error messages. ### - ERROR_MSG_ALREADY_NAVIGATED: 'The object has already navigated, so its partition cannot be changed.' - ERROR_MSG_CANNOT_INJECT_SCRIPT: ': ' + - 'Script cannot be injected into content until the page has loaded.' - ERROR_MSG_INVALID_PARTITION_ATTRIBUTE: 'Invalid partition attribute.' - ERROR_MSG_INVALID_PRELOAD_ATTRIBUTE: 'Only "file:" protocol is supported in "preload" attribute.' diff --git a/atom/renderer/lib/web-view/web-view-constants.js b/atom/renderer/lib/web-view/web-view-constants.js new file mode 100644 index 000000000000..098e02eb8875 --- /dev/null +++ b/atom/renderer/lib/web-view/web-view-constants.js @@ -0,0 +1,29 @@ +module.exports = { + + /* Attributes. */ + ATTRIBUTE_ALLOWTRANSPARENCY: 'allowtransparency', + ATTRIBUTE_AUTOSIZE: 'autosize', + ATTRIBUTE_MAXHEIGHT: 'maxheight', + ATTRIBUTE_MAXWIDTH: 'maxwidth', + ATTRIBUTE_MINHEIGHT: 'minheight', + ATTRIBUTE_MINWIDTH: 'minwidth', + ATTRIBUTE_NAME: 'name', + ATTRIBUTE_PARTITION: 'partition', + ATTRIBUTE_SRC: 'src', + ATTRIBUTE_HTTPREFERRER: 'httpreferrer', + ATTRIBUTE_NODEINTEGRATION: 'nodeintegration', + ATTRIBUTE_PLUGINS: 'plugins', + ATTRIBUTE_DISABLEWEBSECURITY: 'disablewebsecurity', + ATTRIBUTE_ALLOWPOPUPS: 'allowpopups', + ATTRIBUTE_PRELOAD: 'preload', + ATTRIBUTE_USERAGENT: 'useragent', + + /* Internal attribute. */ + ATTRIBUTE_INTERNALINSTANCEID: 'internalinstanceid', + + /* Error messages. */ + ERROR_MSG_ALREADY_NAVIGATED: 'The object has already navigated, so its partition cannot be changed.', + ERROR_MSG_CANNOT_INJECT_SCRIPT: ': ' + 'Script cannot be injected into content until the page has loaded.', + ERROR_MSG_INVALID_PARTITION_ATTRIBUTE: 'Invalid partition attribute.', + ERROR_MSG_INVALID_PRELOAD_ATTRIBUTE: 'Only "file:" protocol is supported in "preload" attribute.' +}; diff --git a/atom/renderer/lib/web-view/web-view.coffee b/atom/renderer/lib/web-view/web-view.coffee deleted file mode 100644 index 15610c85dd51..000000000000 --- a/atom/renderer/lib/web-view/web-view.coffee +++ /dev/null @@ -1,356 +0,0 @@ -{deprecate, webFrame, remote, ipcRenderer} = require 'electron' -v8Util = process.atomBinding 'v8_util' - -guestViewInternal = require './guest-view-internal' -webViewConstants = require './web-view-constants' - -### ID generator. ### -nextId = 0 -getNextId = -> ++nextId - -### Represents the internal state of the WebView node. ### -class WebViewImpl - constructor: (@webviewNode) -> - v8Util.setHiddenValue @webviewNode, 'internal', this - @attached = false - @elementAttached = false - - @beforeFirstNavigation = true - - ### on* Event handlers. ### - @on = {} - - @browserPluginNode = @createBrowserPluginNode() - shadowRoot = @webviewNode.createShadowRoot() - @setupWebViewAttributes() - @setupFocusPropagation() - - @viewInstanceId = getNextId() - - shadowRoot.appendChild @browserPluginNode - - createBrowserPluginNode: -> - ### - We create BrowserPlugin as a custom element in order to observe changes - to attributes synchronously. - ### - browserPluginNode = new WebViewImpl.BrowserPlugin() - v8Util.setHiddenValue browserPluginNode, 'internal', this - browserPluginNode - - ### Resets some state upon reattaching element to the DOM. ### - reset: -> - ### - If guestInstanceId is defined then the has navigated and has - already picked up a partition ID. Thus, we need to reset the initialization - state. However, it may be the case that beforeFirstNavigation is false BUT - guestInstanceId has yet to be initialized. This means that we have not - heard back from createGuest yet. We will not reset the flag in this case so - that we don't end up allocating a second guest. - ### - if @guestInstanceId - guestViewInternal.destroyGuest @guestInstanceId - @webContents = null - @guestInstanceId = undefined - @beforeFirstNavigation = true - @attributes[webViewConstants.ATTRIBUTE_PARTITION].validPartitionId = true - @internalInstanceId = 0 - - ### Sets the .request property. ### - setRequestPropertyOnWebViewNode: (request) -> - Object.defineProperty @webviewNode, 'request', value: request, enumerable: true - - setupFocusPropagation: -> - unless @webviewNode.hasAttribute 'tabIndex' - ### - needs a tabIndex in order to be focusable. - TODO(fsamuel): It would be nice to avoid exposing a tabIndex attribute - to allow to be focusable. - See http://crbug.com/231664. - ### - @webviewNode.setAttribute 'tabIndex', -1 - @webviewNode.addEventListener 'focus', (e) => - ### Focus the BrowserPlugin when the takes focus. ### - @browserPluginNode.focus() - @webviewNode.addEventListener 'blur', (e) => - ### Blur the BrowserPlugin when the loses focus. ### - @browserPluginNode.blur() - - ### - This observer monitors mutations to attributes of the and - updates the BrowserPlugin properties accordingly. In turn, updating - a BrowserPlugin property will update the corresponding BrowserPlugin - attribute, if necessary. See BrowserPlugin::UpdateDOMAttribute for more - details. - ### - handleWebviewAttributeMutation: (attributeName, oldValue, newValue) -> - if not @attributes[attributeName] or @attributes[attributeName].ignoreMutation - return - - ### Let the changed attribute handle its own mutation; ### - @attributes[attributeName].handleMutation oldValue, newValue - - handleBrowserPluginAttributeMutation: (attributeName, oldValue, newValue) -> - if attributeName is webViewConstants.ATTRIBUTE_INTERNALINSTANCEID and !oldValue and !!newValue - @browserPluginNode.removeAttribute webViewConstants.ATTRIBUTE_INTERNALINSTANCEID - @internalInstanceId = parseInt newValue - - ### Track when the element resizes using the element resize callback. ### - webFrame.registerElementResizeCallback @internalInstanceId, @onElementResize.bind(this) - - return unless @guestInstanceId - - guestViewInternal.attachGuest @internalInstanceId, @guestInstanceId, @buildParams() - - onSizeChanged: (webViewEvent) -> - newWidth = webViewEvent.newWidth - newHeight = webViewEvent.newHeight - - node = @webviewNode - - width = node.offsetWidth - height = node.offsetHeight - - ### Check the current bounds to make sure we do not resize ### - ### outside of current constraints. ### - maxWidth = @attributes[webViewConstants.ATTRIBUTE_MAXWIDTH].getValue() | width - maxHeight = @attributes[webViewConstants.ATTRIBUTE_MAXHEIGHT].getValue() | width - minWidth = @attributes[webViewConstants.ATTRIBUTE_MINWIDTH].getValue() | width - minHeight = @attributes[webViewConstants.ATTRIBUTE_MINHEIGHT].getValue() | width - - minWidth = Math.min minWidth, maxWidth - minHeight = Math.min minHeight, maxHeight - - if not @attributes[webViewConstants.ATTRIBUTE_AUTOSIZE].getValue() or - (newWidth >= minWidth and - newWidth <= maxWidth and - newHeight >= minHeight and - newHeight <= maxHeight) - node.style.width = newWidth + 'px' - node.style.height = newHeight + 'px' - ### - Only fire the DOM event if the size of the has actually - changed. - ### - @dispatchEvent webViewEvent - - onElementResize: (newSize) -> - ### Dispatch the 'resize' event. ### - resizeEvent = new Event('resize', bubbles: true) - resizeEvent.newWidth = newSize.width - resizeEvent.newHeight = newSize.height - @dispatchEvent resizeEvent - - if @guestInstanceId - guestViewInternal.setSize @guestInstanceId, normal: newSize - - createGuest: -> - guestViewInternal.createGuest @buildParams(), (event, guestInstanceId) => - @attachWindow guestInstanceId - - dispatchEvent: (webViewEvent) -> - @webviewNode.dispatchEvent webViewEvent - - ### Adds an 'on' property on the webview, which can be used to set/unset ### - ### an event handler. ### - setupEventProperty: (eventName) -> - propertyName = 'on' + eventName.toLowerCase() - Object.defineProperty @webviewNode, propertyName, - get: => @on[propertyName] - set: (value) => - if @on[propertyName] - @webviewNode.removeEventListener eventName, @on[propertyName] - @on[propertyName] = value - if value - @webviewNode.addEventListener eventName, value - enumerable: true - - ### Updates state upon loadcommit. ### - onLoadCommit: (webViewEvent) -> - oldValue = @webviewNode.getAttribute webViewConstants.ATTRIBUTE_SRC - newValue = webViewEvent.url - if webViewEvent.isMainFrame and (oldValue != newValue) - ### - Touching the src attribute triggers a navigation. To avoid - triggering a page reload on every guest-initiated navigation, - we do not handle this mutation. - ### - @attributes[webViewConstants.ATTRIBUTE_SRC].setValueIgnoreMutation newValue - - onAttach: (storagePartitionId) -> - @attributes[webViewConstants.ATTRIBUTE_PARTITION].setValue storagePartitionId - - buildParams: -> - params = - instanceId: @viewInstanceId - userAgentOverride: @userAgentOverride - for own attributeName, attribute of @attributes - params[attributeName] = attribute.getValue() - ### - When the WebView is not participating in layout (display:none) - then getBoundingClientRect() would report a width and height of 0. - However, in the case where the WebView has a fixed size we can - use that value to initially size the guest so as to avoid a relayout of - the on display:block. - ### - css = window.getComputedStyle @webviewNode, null - elementRect = @webviewNode.getBoundingClientRect() - params.elementWidth = parseInt(elementRect.width) || - parseInt(css.getPropertyValue('width')) - params.elementHeight = parseInt(elementRect.height) || - parseInt(css.getPropertyValue('height')) - params - - attachWindow: (guestInstanceId) -> - @guestInstanceId = guestInstanceId - @webContents = remote.getGuestWebContents @guestInstanceId - return true unless @internalInstanceId - - guestViewInternal.attachGuest @internalInstanceId, @guestInstanceId, @buildParams() - -### Registers browser plugin custom element. ### -registerBrowserPluginElement = -> - proto = Object.create HTMLObjectElement.prototype - - proto.createdCallback = -> - @setAttribute 'type', 'application/browser-plugin' - @setAttribute 'id', 'browser-plugin-' + getNextId() - ### The node fills in the container. ### - @style.display = 'block' - @style.width = '100%' - @style.height = '100%' - - proto.attributeChangedCallback = (name, oldValue, newValue) -> - internal = v8Util.getHiddenValue this, 'internal' - return unless internal - internal.handleBrowserPluginAttributeMutation name, oldValue, newValue - - proto.attachedCallback = -> - ### Load the plugin immediately. ### - unused = this.nonExistentAttribute - - WebViewImpl.BrowserPlugin = webFrame.registerEmbedderCustomElement 'browserplugin', - extends: 'object', prototype: proto - - delete proto.createdCallback - delete proto.attachedCallback - delete proto.detachedCallback - delete proto.attributeChangedCallback - -### Registers custom element. ### -registerWebViewElement = -> - proto = Object.create HTMLObjectElement.prototype - - proto.createdCallback = -> - new WebViewImpl(this) - - proto.attributeChangedCallback = (name, oldValue, newValue) -> - internal = v8Util.getHiddenValue this, 'internal' - return unless internal - internal.handleWebviewAttributeMutation name, oldValue, newValue - - proto.detachedCallback = -> - internal = v8Util.getHiddenValue this, 'internal' - return unless internal - guestViewInternal.deregisterEvents internal.viewInstanceId - internal.elementAttached = false - internal.reset() - - proto.attachedCallback = -> - internal = v8Util.getHiddenValue this, 'internal' - return unless internal - unless internal.elementAttached - guestViewInternal.registerEvents internal, internal.viewInstanceId - internal.elementAttached = true - internal.attributes[webViewConstants.ATTRIBUTE_SRC].parse() - - ### Public-facing API methods. ### - methods = [ - 'getURL' - 'getTitle' - 'isLoading' - 'isWaitingForResponse' - 'stop' - 'reload' - 'reloadIgnoringCache' - 'canGoBack' - 'canGoForward' - 'canGoToOffset' - 'clearHistory' - 'goBack' - 'goForward' - 'goToIndex' - 'goToOffset' - 'isCrashed' - 'setUserAgent' - 'getUserAgent' - 'openDevTools' - 'closeDevTools' - 'isDevToolsOpened' - 'isDevToolsFocused' - 'inspectElement' - 'setAudioMuted' - 'isAudioMuted' - 'undo' - 'redo' - 'cut' - 'copy' - 'paste' - 'pasteAndMatchStyle' - 'delete' - 'selectAll' - 'unselect' - 'replace' - 'replaceMisspelling' - 'findInPage' - 'stopFindInPage' - 'getId' - 'downloadURL' - 'inspectServiceWorker' - 'print' - 'printToPDF' - ] - - nonblockMethods = [ - 'send', - 'sendInputEvent', - 'executeJavaScript', - 'insertCSS' - ] - - ### Forward proto.foo* method calls to WebViewImpl.foo*. ### - createBlockHandler = (m) -> - (args...) -> - internal = v8Util.getHiddenValue this, 'internal' - internal.webContents[m] args... - proto[m] = createBlockHandler m for m in methods - - createNonBlockHandler = (m) -> - (args...) -> - internal = v8Util.getHiddenValue this, 'internal' - ipcRenderer.send('ATOM_BROWSER_ASYNC_CALL_TO_GUEST_VIEW', internal.guestInstanceId, m, args...) - - proto[m] = createNonBlockHandler m for m in nonblockMethods - - ### Deprecated. ### - deprecate.rename proto, 'getUrl', 'getURL' - - window.WebView = webFrame.registerEmbedderCustomElement 'webview', - prototype: proto - - ### Delete the callbacks so developers cannot call them and produce unexpected ### - ### behavior. ### - delete proto.createdCallback - delete proto.attachedCallback - delete proto.detachedCallback - delete proto.attributeChangedCallback - -useCapture = true -listener = (event) -> - return if document.readyState == 'loading' - registerBrowserPluginElement() - registerWebViewElement() - window.removeEventListener event.type, listener, useCapture -window.addEventListener 'readystatechange', listener, true - -module.exports = WebViewImpl diff --git a/atom/renderer/lib/web-view/web-view.js b/atom/renderer/lib/web-view/web-view.js new file mode 100644 index 000000000000..fcdfad1877ed --- /dev/null +++ b/atom/renderer/lib/web-view/web-view.js @@ -0,0 +1,435 @@ +var WebViewImpl, deprecate, getNextId, guestViewInternal, ipcRenderer, listener, nextId, ref, registerBrowserPluginElement, registerWebViewElement, remote, useCapture, v8Util, webFrame, webViewConstants, + hasProp = {}.hasOwnProperty, + slice = [].slice; + +ref = require('electron'), deprecate = ref.deprecate, webFrame = ref.webFrame, remote = ref.remote, ipcRenderer = ref.ipcRenderer; + +v8Util = process.atomBinding('v8_util'); + +guestViewInternal = require('./guest-view-internal'); + +webViewConstants = require('./web-view-constants'); + + +/* ID generator. */ + +nextId = 0; + +getNextId = function() { + return ++nextId; +}; + + +/* Represents the internal state of the WebView node. */ + +WebViewImpl = (function() { + function WebViewImpl(webviewNode) { + var shadowRoot; + this.webviewNode = webviewNode; + v8Util.setHiddenValue(this.webviewNode, 'internal', this); + this.attached = false; + this.elementAttached = false; + this.beforeFirstNavigation = true; + + /* on* Event handlers. */ + this.on = {}; + this.browserPluginNode = this.createBrowserPluginNode(); + shadowRoot = this.webviewNode.createShadowRoot(); + this.setupWebViewAttributes(); + this.setupFocusPropagation(); + this.viewInstanceId = getNextId(); + shadowRoot.appendChild(this.browserPluginNode); + } + + WebViewImpl.prototype.createBrowserPluginNode = function() { + + /* + We create BrowserPlugin as a custom element in order to observe changes + to attributes synchronously. + */ + var browserPluginNode; + browserPluginNode = new WebViewImpl.BrowserPlugin(); + v8Util.setHiddenValue(browserPluginNode, 'internal', this); + return browserPluginNode; + }; + + + /* Resets some state upon reattaching element to the DOM. */ + + WebViewImpl.prototype.reset = function() { + + /* + If guestInstanceId is defined then the has navigated and has + already picked up a partition ID. Thus, we need to reset the initialization + state. However, it may be the case that beforeFirstNavigation is false BUT + guestInstanceId has yet to be initialized. This means that we have not + heard back from createGuest yet. We will not reset the flag in this case so + that we don't end up allocating a second guest. + */ + if (this.guestInstanceId) { + guestViewInternal.destroyGuest(this.guestInstanceId); + this.webContents = null; + this.guestInstanceId = void 0; + this.beforeFirstNavigation = true; + this.attributes[webViewConstants.ATTRIBUTE_PARTITION].validPartitionId = true; + } + return this.internalInstanceId = 0; + }; + + + /* Sets the .request property. */ + + WebViewImpl.prototype.setRequestPropertyOnWebViewNode = function(request) { + return Object.defineProperty(this.webviewNode, 'request', { + value: request, + enumerable: true + }); + }; + + WebViewImpl.prototype.setupFocusPropagation = function() { + if (!this.webviewNode.hasAttribute('tabIndex')) { + + /* + needs a tabIndex in order to be focusable. + TODO(fsamuel): It would be nice to avoid exposing a tabIndex attribute + to allow to be focusable. + See http://crbug.com/231664. + */ + this.webviewNode.setAttribute('tabIndex', -1); + } + this.webviewNode.addEventListener('focus', (function(_this) { + return function(e) { + + /* Focus the BrowserPlugin when the takes focus. */ + return _this.browserPluginNode.focus(); + }; + })(this)); + return this.webviewNode.addEventListener('blur', (function(_this) { + return function(e) { + + /* Blur the BrowserPlugin when the loses focus. */ + return _this.browserPluginNode.blur(); + }; + })(this)); + }; + + + /* + This observer monitors mutations to attributes of the and + updates the BrowserPlugin properties accordingly. In turn, updating + a BrowserPlugin property will update the corresponding BrowserPlugin + attribute, if necessary. See BrowserPlugin::UpdateDOMAttribute for more + details. + */ + + WebViewImpl.prototype.handleWebviewAttributeMutation = function(attributeName, oldValue, newValue) { + if (!this.attributes[attributeName] || this.attributes[attributeName].ignoreMutation) { + return; + } + + /* Let the changed attribute handle its own mutation; */ + return this.attributes[attributeName].handleMutation(oldValue, newValue); + }; + + WebViewImpl.prototype.handleBrowserPluginAttributeMutation = function(attributeName, oldValue, newValue) { + if (attributeName === webViewConstants.ATTRIBUTE_INTERNALINSTANCEID && !oldValue && !!newValue) { + this.browserPluginNode.removeAttribute(webViewConstants.ATTRIBUTE_INTERNALINSTANCEID); + this.internalInstanceId = parseInt(newValue); + + /* Track when the element resizes using the element resize callback. */ + webFrame.registerElementResizeCallback(this.internalInstanceId, this.onElementResize.bind(this)); + if (!this.guestInstanceId) { + return; + } + return guestViewInternal.attachGuest(this.internalInstanceId, this.guestInstanceId, this.buildParams()); + } + }; + + WebViewImpl.prototype.onSizeChanged = function(webViewEvent) { + var height, maxHeight, maxWidth, minHeight, minWidth, newHeight, newWidth, node, width; + newWidth = webViewEvent.newWidth; + newHeight = webViewEvent.newHeight; + node = this.webviewNode; + width = node.offsetWidth; + height = node.offsetHeight; + + /* Check the current bounds to make sure we do not resize */ + + /* outside of current constraints. */ + maxWidth = this.attributes[webViewConstants.ATTRIBUTE_MAXWIDTH].getValue() | width; + maxHeight = this.attributes[webViewConstants.ATTRIBUTE_MAXHEIGHT].getValue() | width; + minWidth = this.attributes[webViewConstants.ATTRIBUTE_MINWIDTH].getValue() | width; + minHeight = this.attributes[webViewConstants.ATTRIBUTE_MINHEIGHT].getValue() | width; + minWidth = Math.min(minWidth, maxWidth); + minHeight = Math.min(minHeight, maxHeight); + if (!this.attributes[webViewConstants.ATTRIBUTE_AUTOSIZE].getValue() || (newWidth >= minWidth && newWidth <= maxWidth && newHeight >= minHeight && newHeight <= maxHeight)) { + node.style.width = newWidth + 'px'; + node.style.height = newHeight + 'px'; + + /* + Only fire the DOM event if the size of the has actually + changed. + */ + return this.dispatchEvent(webViewEvent); + } + }; + + WebViewImpl.prototype.onElementResize = function(newSize) { + + /* Dispatch the 'resize' event. */ + var resizeEvent; + resizeEvent = new Event('resize', { + bubbles: true + }); + resizeEvent.newWidth = newSize.width; + resizeEvent.newHeight = newSize.height; + this.dispatchEvent(resizeEvent); + if (this.guestInstanceId) { + return guestViewInternal.setSize(this.guestInstanceId, { + normal: newSize + }); + } + }; + + WebViewImpl.prototype.createGuest = function() { + return guestViewInternal.createGuest(this.buildParams(), (function(_this) { + return function(event, guestInstanceId) { + return _this.attachWindow(guestInstanceId); + }; + })(this)); + }; + + WebViewImpl.prototype.dispatchEvent = function(webViewEvent) { + return this.webviewNode.dispatchEvent(webViewEvent); + }; + + + /* Adds an 'on' property on the webview, which can be used to set/unset */ + + + /* an event handler. */ + + WebViewImpl.prototype.setupEventProperty = function(eventName) { + var propertyName; + propertyName = 'on' + eventName.toLowerCase(); + return Object.defineProperty(this.webviewNode, propertyName, { + get: (function(_this) { + return function() { + return _this.on[propertyName]; + }; + })(this), + set: (function(_this) { + return function(value) { + if (_this.on[propertyName]) { + _this.webviewNode.removeEventListener(eventName, _this.on[propertyName]); + } + _this.on[propertyName] = value; + if (value) { + return _this.webviewNode.addEventListener(eventName, value); + } + }; + })(this), + enumerable: true + }); + }; + + + /* Updates state upon loadcommit. */ + + WebViewImpl.prototype.onLoadCommit = function(webViewEvent) { + var newValue, oldValue; + oldValue = this.webviewNode.getAttribute(webViewConstants.ATTRIBUTE_SRC); + newValue = webViewEvent.url; + if (webViewEvent.isMainFrame && (oldValue !== newValue)) { + + /* + Touching the src attribute triggers a navigation. To avoid + triggering a page reload on every guest-initiated navigation, + we do not handle this mutation. + */ + return this.attributes[webViewConstants.ATTRIBUTE_SRC].setValueIgnoreMutation(newValue); + } + }; + + WebViewImpl.prototype.onAttach = function(storagePartitionId) { + return this.attributes[webViewConstants.ATTRIBUTE_PARTITION].setValue(storagePartitionId); + }; + + WebViewImpl.prototype.buildParams = function() { + var attribute, attributeName, css, elementRect, params, ref1; + params = { + instanceId: this.viewInstanceId, + userAgentOverride: this.userAgentOverride + }; + ref1 = this.attributes; + for (attributeName in ref1) { + if (!hasProp.call(ref1, attributeName)) continue; + attribute = ref1[attributeName]; + params[attributeName] = attribute.getValue(); + } + + /* + When the WebView is not participating in layout (display:none) + then getBoundingClientRect() would report a width and height of 0. + However, in the case where the WebView has a fixed size we can + use that value to initially size the guest so as to avoid a relayout of + the on display:block. + */ + css = window.getComputedStyle(this.webviewNode, null); + elementRect = this.webviewNode.getBoundingClientRect(); + params.elementWidth = parseInt(elementRect.width) || parseInt(css.getPropertyValue('width')); + params.elementHeight = parseInt(elementRect.height) || parseInt(css.getPropertyValue('height')); + return params; + }; + + WebViewImpl.prototype.attachWindow = function(guestInstanceId) { + this.guestInstanceId = guestInstanceId; + this.webContents = remote.getGuestWebContents(this.guestInstanceId); + if (!this.internalInstanceId) { + return true; + } + return guestViewInternal.attachGuest(this.internalInstanceId, this.guestInstanceId, this.buildParams()); + }; + + return WebViewImpl; + +})(); + + +/* Registers browser plugin custom element. */ + +registerBrowserPluginElement = function() { + var proto; + proto = Object.create(HTMLObjectElement.prototype); + proto.createdCallback = function() { + this.setAttribute('type', 'application/browser-plugin'); + this.setAttribute('id', 'browser-plugin-' + getNextId()); + + /* The node fills in the container. */ + this.style.display = 'block'; + this.style.width = '100%'; + return this.style.height = '100%'; + }; + proto.attributeChangedCallback = function(name, oldValue, newValue) { + var internal; + internal = v8Util.getHiddenValue(this, 'internal'); + if (!internal) { + return; + } + return internal.handleBrowserPluginAttributeMutation(name, oldValue, newValue); + }; + proto.attachedCallback = function() { + + /* Load the plugin immediately. */ + var unused; + return unused = this.nonExistentAttribute; + }; + WebViewImpl.BrowserPlugin = webFrame.registerEmbedderCustomElement('browserplugin', { + "extends": 'object', + prototype: proto + }); + delete proto.createdCallback; + delete proto.attachedCallback; + delete proto.detachedCallback; + return delete proto.attributeChangedCallback; +}; + + +/* Registers custom element. */ + +registerWebViewElement = function() { + var createBlockHandler, createNonBlockHandler, i, j, len, len1, m, methods, nonblockMethods, proto; + proto = Object.create(HTMLObjectElement.prototype); + proto.createdCallback = function() { + return new WebViewImpl(this); + }; + proto.attributeChangedCallback = function(name, oldValue, newValue) { + var internal; + internal = v8Util.getHiddenValue(this, 'internal'); + if (!internal) { + return; + } + return internal.handleWebviewAttributeMutation(name, oldValue, newValue); + }; + proto.detachedCallback = function() { + var internal; + internal = v8Util.getHiddenValue(this, 'internal'); + if (!internal) { + return; + } + guestViewInternal.deregisterEvents(internal.viewInstanceId); + internal.elementAttached = false; + return internal.reset(); + }; + proto.attachedCallback = function() { + var internal; + internal = v8Util.getHiddenValue(this, 'internal'); + if (!internal) { + return; + } + if (!internal.elementAttached) { + guestViewInternal.registerEvents(internal, internal.viewInstanceId); + internal.elementAttached = true; + return internal.attributes[webViewConstants.ATTRIBUTE_SRC].parse(); + } + }; + + /* Public-facing API methods. */ + methods = ['getURL', 'getTitle', 'isLoading', 'isWaitingForResponse', 'stop', 'reload', 'reloadIgnoringCache', 'canGoBack', 'canGoForward', 'canGoToOffset', 'clearHistory', 'goBack', 'goForward', 'goToIndex', 'goToOffset', 'isCrashed', 'setUserAgent', 'getUserAgent', 'openDevTools', 'closeDevTools', 'isDevToolsOpened', 'isDevToolsFocused', 'inspectElement', 'setAudioMuted', 'isAudioMuted', 'undo', 'redo', 'cut', 'copy', 'paste', 'pasteAndMatchStyle', 'delete', 'selectAll', 'unselect', 'replace', 'replaceMisspelling', 'findInPage', 'stopFindInPage', 'getId', 'downloadURL', 'inspectServiceWorker', 'print', 'printToPDF']; + nonblockMethods = ['send', 'sendInputEvent', 'executeJavaScript', 'insertCSS']; + + /* Forward proto.foo* method calls to WebViewImpl.foo*. */ + createBlockHandler = function(m) { + return function() { + var args, internal, ref1; + args = 1 <= arguments.length ? slice.call(arguments, 0) : []; + internal = v8Util.getHiddenValue(this, 'internal'); + return (ref1 = internal.webContents)[m].apply(ref1, args); + }; + }; + for (i = 0, len = methods.length; i < len; i++) { + m = methods[i]; + proto[m] = createBlockHandler(m); + } + createNonBlockHandler = function(m) { + return function() { + var args, internal; + args = 1 <= arguments.length ? slice.call(arguments, 0) : []; + internal = v8Util.getHiddenValue(this, 'internal'); + return ipcRenderer.send.apply(ipcRenderer, ['ATOM_BROWSER_ASYNC_CALL_TO_GUEST_VIEW', internal.guestInstanceId, m].concat(slice.call(args))); + }; + }; + for (j = 0, len1 = nonblockMethods.length; j < len1; j++) { + m = nonblockMethods[j]; + proto[m] = createNonBlockHandler(m); + } + + /* Deprecated. */ + deprecate.rename(proto, 'getUrl', 'getURL'); + window.WebView = webFrame.registerEmbedderCustomElement('webview', { + prototype: proto + }); + + /* Delete the callbacks so developers cannot call them and produce unexpected */ + + /* behavior. */ + delete proto.createdCallback; + delete proto.attachedCallback; + delete proto.detachedCallback; + return delete proto.attributeChangedCallback; +}; + +useCapture = true; + +listener = function(event) { + if (document.readyState === 'loading') { + return; + } + registerBrowserPluginElement(); + registerWebViewElement(); + return window.removeEventListener(event.type, listener, useCapture); +}; + +window.addEventListener('readystatechange', listener, true); + +module.exports = WebViewImpl; diff --git a/spec/api-app-spec.coffee b/spec/api-app-spec.coffee deleted file mode 100644 index 5e6bca82091f..000000000000 --- a/spec/api-app-spec.coffee +++ /dev/null @@ -1,80 +0,0 @@ -assert = require 'assert' -ChildProcess = require 'child_process' -path = require 'path' -{remote} = require 'electron' -{app, BrowserWindow} = remote.require 'electron' - -describe 'app module', -> - describe 'app.getVersion()', -> - it 'returns the version field of package.json', -> - assert.equal app.getVersion(), '0.1.0' - - describe 'app.setVersion(version)', -> - it 'overrides the version', -> - assert.equal app.getVersion(), '0.1.0' - app.setVersion 'test-version' - assert.equal app.getVersion(), 'test-version' - app.setVersion '0.1.0' - - describe 'app.getName()', -> - it 'returns the name field of package.json', -> - assert.equal app.getName(), 'Electron Test' - - describe 'app.setName(name)', -> - it 'overrides the name', -> - assert.equal app.getName(), 'Electron Test' - app.setName 'test-name' - assert.equal app.getName(), 'test-name' - app.setName 'Electron Test' - - describe 'app.getLocale()', -> - it 'should not be empty', -> - assert.notEqual app.getLocale(), '' - - describe 'app.exit(exitCode)', -> - appProcess = null - afterEach -> - appProcess?.kill() - - it 'emits a process exit event with the code', (done) -> - appPath = path.join(__dirname, 'fixtures', 'api', 'quit-app') - electronPath = remote.getGlobal('process').execPath - appProcess = ChildProcess.spawn(electronPath, [appPath]) - - output = '' - appProcess.stdout.on 'data', (data) -> output += data - appProcess.on 'close', (code) -> - # We skip the following assert on Windows, since we can't currently get - # stdout from a spawned electron process on Windows - if process.platform isnt 'win32' - assert.notEqual output.indexOf('Exit event with code: 123'), -1 - assert.equal code, 123 - done() - - describe 'BrowserWindow events', -> - w = null - afterEach -> - w.destroy() if w? - w = null - - it 'should emit browser-window-focus event when window is focused', (done) -> - app.once 'browser-window-focus', (e, window) -> - assert.equal w.id, window.id - done() - w = new BrowserWindow(show: false) - w.emit 'focus' - - it 'should emit browser-window-blur event when window is blured', (done) -> - app.once 'browser-window-blur', (e, window) -> - assert.equal w.id, window.id - done() - w = new BrowserWindow(show: false) - w.emit 'blur' - - it 'should emit browser-window-created event when window is created', (done) -> - app.once 'browser-window-created', (e, window) -> - setImmediate -> - assert.equal w.id, window.id - done() - w = new BrowserWindow(show: false) - w.emit 'blur' diff --git a/spec/api-app-spec.js b/spec/api-app-spec.js new file mode 100644 index 000000000000..dafaf44a2d15 --- /dev/null +++ b/spec/api-app-spec.js @@ -0,0 +1,111 @@ +var BrowserWindow, ChildProcess, app, assert, path, ref, remote; + +assert = require('assert'); + +ChildProcess = require('child_process'); + +path = require('path'); + +remote = require('electron').remote; + +ref = remote.require('electron'), app = ref.app, BrowserWindow = ref.BrowserWindow; + +describe('app module', function() { + describe('app.getVersion()', function() { + return it('returns the version field of package.json', function() { + return assert.equal(app.getVersion(), '0.1.0'); + }); + }); + describe('app.setVersion(version)', function() { + return it('overrides the version', function() { + assert.equal(app.getVersion(), '0.1.0'); + app.setVersion('test-version'); + assert.equal(app.getVersion(), 'test-version'); + return app.setVersion('0.1.0'); + }); + }); + describe('app.getName()', function() { + return it('returns the name field of package.json', function() { + return assert.equal(app.getName(), 'Electron Test'); + }); + }); + describe('app.setName(name)', function() { + return it('overrides the name', function() { + assert.equal(app.getName(), 'Electron Test'); + app.setName('test-name'); + assert.equal(app.getName(), 'test-name'); + return app.setName('Electron Test'); + }); + }); + describe('app.getLocale()', function() { + return it('should not be empty', function() { + return assert.notEqual(app.getLocale(), ''); + }); + }); + describe('app.exit(exitCode)', function() { + var appProcess; + appProcess = null; + afterEach(function() { + return appProcess != null ? appProcess.kill() : void 0; + }); + return it('emits a process exit event with the code', function(done) { + var appPath, electronPath, output; + appPath = path.join(__dirname, 'fixtures', 'api', 'quit-app'); + electronPath = remote.getGlobal('process').execPath; + appProcess = ChildProcess.spawn(electronPath, [appPath]); + output = ''; + appProcess.stdout.on('data', function(data) { + return output += data; + }); + return appProcess.on('close', function(code) { + if (process.platform !== 'win32') { + assert.notEqual(output.indexOf('Exit event with code: 123'), -1); + } + assert.equal(code, 123); + return done(); + }); + }); + }); + return describe('BrowserWindow events', function() { + var w; + w = null; + afterEach(function() { + if (w != null) { + w.destroy(); + } + return w = null; + }); + it('should emit browser-window-focus event when window is focused', function(done) { + app.once('browser-window-focus', function(e, window) { + assert.equal(w.id, window.id); + return done(); + }); + w = new BrowserWindow({ + show: false + }); + return w.emit('focus'); + }); + it('should emit browser-window-blur event when window is blured', function(done) { + app.once('browser-window-blur', function(e, window) { + assert.equal(w.id, window.id); + return done(); + }); + w = new BrowserWindow({ + show: false + }); + return w.emit('blur'); + }); + return it('should emit browser-window-created event when window is created', function(done) { + app.once('browser-window-created', function(e, window) { + return setImmediate(function() { + assert.equal(w.id, window.id); + return done(); + }); + }); + w = new BrowserWindow({ + show: false + }); + return w.emit('blur'); + }); + }); +}); diff --git a/spec/api-browser-window-spec.coffee b/spec/api-browser-window-spec.coffee deleted file mode 100644 index 724f149cd9e5..000000000000 --- a/spec/api-browser-window-spec.coffee +++ /dev/null @@ -1,339 +0,0 @@ -assert = require 'assert' -fs = require 'fs' -path = require 'path' -http = require 'http' -url = require 'url' -os = require 'os' - -{remote, screen} = require 'electron' -{ipcMain, BrowserWindow} = remote.require 'electron' - -isCI = remote.getGlobal('isCi') - -describe 'browser-window module', -> - fixtures = path.resolve __dirname, 'fixtures' - - w = null - beforeEach -> - w.destroy() if w? - w = new BrowserWindow(show: false, width: 400, height: 400) - afterEach -> - w.destroy() if w? - w = null - - describe 'BrowserWindow.close()', -> - it 'should emit unload handler', (done) -> - w.webContents.on 'did-finish-load', -> - w.close() - w.on 'closed', -> - test = path.join(fixtures, 'api', 'unload') - content = fs.readFileSync(test) - fs.unlinkSync(test) - assert.equal String(content), 'unload' - done() - w.loadURL 'file://' + path.join(fixtures, 'api', 'unload.html') - - it 'should emit beforeunload handler', (done) -> - w.on 'onbeforeunload', -> - done() - w.webContents.on 'did-finish-load', -> - w.close() - w.loadURL 'file://' + path.join(fixtures, 'api', 'beforeunload-false.html') - - describe 'window.close()', -> - it 'should emit unload handler', (done) -> - w.on 'closed', -> - test = path.join(fixtures, 'api', 'close') - content = fs.readFileSync(test) - fs.unlinkSync(test) - assert.equal String(content), 'close' - done() - w.loadURL 'file://' + path.join(fixtures, 'api', 'close.html') - - it 'should emit beforeunload handler', (done) -> - w.on 'onbeforeunload', -> - done() - w.loadURL 'file://' + path.join(fixtures, 'api', 'close-beforeunload-false.html') - - describe 'BrowserWindow.destroy()', -> - it 'prevents users to access methods of webContents', -> - webContents = w.webContents - w.destroy() - assert.throws (-> webContents.getId()), /Object has been destroyed/ - - describe 'BrowserWindow.loadURL(url)', -> - it 'should emit did-start-loading event', (done) -> - w.webContents.on 'did-start-loading', -> - done() - w.loadURL 'about:blank' - - it 'should emit did-fail-load event', (done) -> - w.webContents.on 'did-fail-load', -> - done() - w.loadURL 'file://a.txt' - - describe 'BrowserWindow.show()', -> - it 'should focus on window', -> - return if isCI - w.show() - assert w.isFocused() - - describe 'BrowserWindow.showInactive()', -> - it 'should not focus on window', -> - w.showInactive() - assert !w.isFocused() - - describe 'BrowserWindow.focus()', -> - it 'does not make the window become visible', -> - assert.equal w.isVisible(), false - w.focus() - assert.equal w.isVisible(), false - - describe 'BrowserWindow.capturePage(rect, callback)', -> - it 'calls the callback with a Buffer', (done) -> - w.capturePage {x: 0, y: 0, width: 100, height: 100}, (image) -> - assert.equal image.isEmpty(), true - done() - - describe 'BrowserWindow.setSize(width, height)', -> - it 'sets the window size', (done) -> - size = [300, 400] - w.once 'resize', -> - newSize = w.getSize() - assert.equal newSize[0], size[0] - assert.equal newSize[1], size[1] - done() - w.setSize size[0], size[1] - - describe 'BrowserWindow.setPosition(x, y)', -> - it 'sets the window position', (done) -> - pos = [10, 10] - w.once 'move', -> - newPos = w.getPosition() - assert.equal newPos[0], pos[0] - assert.equal newPos[1], pos[1] - done() - w.setPosition pos[0], pos[1] - - describe 'BrowserWindow.setContentSize(width, height)', -> - it 'sets the content size', -> - size = [400, 400] - w.setContentSize size[0], size[1] - after = w.getContentSize() - assert.equal after[0], size[0] - assert.equal after[1], size[1] - - it 'works for framless window', -> - w.destroy() - w = new BrowserWindow(show: false, frame: false, width: 400, height: 400) - size = [400, 400] - w.setContentSize size[0], size[1] - after = w.getContentSize() - assert.equal after[0], size[0] - assert.equal after[1], size[1] - - describe 'BrowserWindow.fromId(id)', -> - it 'returns the window with id', -> - assert.equal w.id, BrowserWindow.fromId(w.id).id - - describe 'BrowserWindow.setResizable(resizable)', -> - it 'does not change window size for frameless window', -> - w.destroy() - w = new BrowserWindow(show: true, frame: false) - s = w.getSize() - w.setResizable not w.isResizable() - assert.deepEqual s, w.getSize() - - describe '"useContentSize" option', -> - it 'make window created with content size when used', -> - w.destroy() - w = new BrowserWindow(show: false, width: 400, height: 400, useContentSize: true) - contentSize = w.getContentSize() - assert.equal contentSize[0], 400 - assert.equal contentSize[1], 400 - - it 'make window created with window size when not used', -> - size = w.getSize() - assert.equal size[0], 400 - assert.equal size[1], 400 - - it 'works for framless window', -> - w.destroy() - w = new BrowserWindow(show: false, frame: false, width: 400, height: 400, useContentSize: true) - contentSize = w.getContentSize() - assert.equal contentSize[0], 400 - assert.equal contentSize[1], 400 - size = w.getSize() - assert.equal size[0], 400 - assert.equal size[1], 400 - - describe '"title-bar-style" option', -> - return if process.platform isnt 'darwin' - return if parseInt(os.release().split('.')[0]) < 14 # only run these tests on Yosemite or newer - - it 'creates browser window with hidden title bar', -> - w.destroy() - w = new BrowserWindow(show: false, width: 400, height: 400, titleBarStyle: 'hidden') - contentSize = w.getContentSize() - assert.equal contentSize[1], 400 - - it 'creates browser window with hidden inset title bar', -> - w.destroy() - w = new BrowserWindow(show: false, width: 400, height: 400, titleBarStyle: 'hidden-inset') - contentSize = w.getContentSize() - assert.equal contentSize[1], 400 - - describe '"enableLargerThanScreen" option', -> - return if process.platform is 'linux' - - beforeEach -> - w.destroy() - w = new BrowserWindow(show: true, width: 400, height: 400, enableLargerThanScreen: true) - - it 'can move the window out of screen', -> - w.setPosition -10, -10 - after = w.getPosition() - assert.equal after[0], -10 - assert.equal after[1], -10 - - it 'can set the window larger than screen', -> - size = screen.getPrimaryDisplay().size - size.width += 100 - size.height += 100 - w.setSize size.width, size.height - after = w.getSize() - assert.equal after[0], size.width - assert.equal after[1], size.height - - describe '"web-preferences" option', -> - afterEach -> - ipcMain.removeAllListeners('answer') - - describe '"preload" option', -> - it 'loads the script before other scripts in window', (done) -> - preload = path.join fixtures, 'module', 'set-global.js' - ipcMain.once 'answer', (event, test) -> - assert.equal(test, 'preload') - done() - w.destroy() - w = new BrowserWindow - show: false - webPreferences: - preload: preload - w.loadURL 'file://' + path.join(fixtures, 'api', 'preload.html') - - describe '"node-integration" option', -> - it 'disables node integration when specified to false', (done) -> - preload = path.join fixtures, 'module', 'send-later.js' - ipcMain.once 'answer', (event, test) -> - assert.equal(test, 'undefined') - done() - w.destroy() - w = new BrowserWindow - show: false - webPreferences: - preload: preload - nodeIntegration: false - w.loadURL 'file://' + path.join(fixtures, 'api', 'blank.html') - - describe 'beforeunload handler', -> - it 'returning true would not prevent close', (done) -> - w.on 'closed', -> - done() - w.loadURL 'file://' + path.join(fixtures, 'api', 'close-beforeunload-true.html') - - it 'returning non-empty string would not prevent close', (done) -> - w.on 'closed', -> - done() - w.loadURL 'file://' + path.join(fixtures, 'api', 'close-beforeunload-string.html') - - it 'returning false would prevent close', (done) -> - w.on 'onbeforeunload', -> - done() - w.loadURL 'file://' + path.join(fixtures, 'api', 'close-beforeunload-false.html') - - it 'returning empty string would prevent close', (done) -> - w.on 'onbeforeunload', -> - done() - w.loadURL 'file://' + path.join(fixtures, 'api', 'close-beforeunload-empty-string.html') - - describe 'new-window event', -> - return if isCI and process.platform is 'darwin' - it 'emits when window.open is called', (done) -> - w.webContents.once 'new-window', (e, url, frameName) -> - e.preventDefault() - assert.equal url, 'http://host/' - assert.equal frameName, 'host' - done() - w.loadURL "file://#{fixtures}/pages/window-open.html" - - it 'emits when link with target is called', (done) -> - @timeout 10000 - w.webContents.once 'new-window', (e, url, frameName) -> - e.preventDefault() - assert.equal url, 'http://host/' - assert.equal frameName, 'target' - done() - w.loadURL "file://#{fixtures}/pages/target-name.html" - - describe 'maximize event', -> - return if isCI - it 'emits when window is maximized', (done) -> - @timeout 10000 - w.once 'maximize', -> done() - w.show() - w.maximize() - - describe 'unmaximize event', -> - return if isCI - it 'emits when window is unmaximized', (done) -> - @timeout 10000 - w.once 'unmaximize', -> done() - w.show() - w.maximize() - w.unmaximize() - - describe 'minimize event', -> - return if isCI - it 'emits when window is minimized', (done) -> - @timeout 10000 - w.once 'minimize', -> done() - w.show() - w.minimize() - - xdescribe 'beginFrameSubscription method', -> - it 'subscribes frame updates', (done) -> - w.loadURL "file://#{fixtures}/api/blank.html" - w.webContents.beginFrameSubscription (data) -> - assert.notEqual data.length, 0 - w.webContents.endFrameSubscription() - done() - - describe 'save page', -> - savePageDir = path.join fixtures, 'save_page' - savePageHtmlPath = path.join savePageDir, 'save_page.html' - savePageJsPath = path.join savePageDir, 'save_page_files', 'test.js' - savePageCssPath = path.join savePageDir, 'save_page_files', 'test.css' - it 'should save page', (done) -> - w.webContents.on 'did-finish-load', -> - w.webContents.savePage savePageHtmlPath, 'HTMLComplete', (error) -> - assert.equal error, null - assert fs.existsSync savePageHtmlPath - assert fs.existsSync savePageJsPath - assert fs.existsSync savePageCssPath - fs.unlinkSync savePageCssPath - fs.unlinkSync savePageJsPath - fs.unlinkSync savePageHtmlPath - fs.rmdirSync path.join savePageDir, 'save_page_files' - fs.rmdirSync savePageDir - done() - - w.loadURL "file://#{fixtures}/pages/save_page/index.html" - - describe 'BrowserWindow options argument is optional', -> - it 'should create a window with default size (800x600)', -> - w.destroy() - w = new BrowserWindow() - size = w.getSize() - assert.equal size[0], 800 - assert.equal size[1], 600 diff --git a/spec/api-browser-window-spec.js b/spec/api-browser-window-spec.js new file mode 100644 index 000000000000..432c9a739cde --- /dev/null +++ b/spec/api-browser-window-spec.js @@ -0,0 +1,492 @@ +var BrowserWindow, assert, fs, http, ipcMain, isCI, os, path, ref, ref1, remote, screen, url; + +assert = require('assert'); + +fs = require('fs'); + +path = require('path'); + +http = require('http'); + +url = require('url'); + +os = require('os'); + +ref = require('electron'), remote = ref.remote, screen = ref.screen; + +ref1 = remote.require('electron'), ipcMain = ref1.ipcMain, BrowserWindow = ref1.BrowserWindow; + +isCI = remote.getGlobal('isCi'); + +describe('browser-window module', function() { + var fixtures, w; + fixtures = path.resolve(__dirname, 'fixtures'); + w = null; + beforeEach(function() { + if (w != null) { + w.destroy(); + } + return w = new BrowserWindow({ + show: false, + width: 400, + height: 400 + }); + }); + afterEach(function() { + if (w != null) { + w.destroy(); + } + return w = null; + }); + describe('BrowserWindow.close()', function() { + it('should emit unload handler', function(done) { + w.webContents.on('did-finish-load', function() { + return w.close(); + }); + w.on('closed', function() { + var content, test; + test = path.join(fixtures, 'api', 'unload'); + content = fs.readFileSync(test); + fs.unlinkSync(test); + assert.equal(String(content), 'unload'); + return done(); + }); + return w.loadURL('file://' + path.join(fixtures, 'api', 'unload.html')); + }); + return it('should emit beforeunload handler', function(done) { + w.on('onbeforeunload', function() { + return done(); + }); + w.webContents.on('did-finish-load', function() { + return w.close(); + }); + return w.loadURL('file://' + path.join(fixtures, 'api', 'beforeunload-false.html')); + }); + }); + describe('window.close()', function() { + it('should emit unload handler', function(done) { + w.on('closed', function() { + var content, test; + test = path.join(fixtures, 'api', 'close'); + content = fs.readFileSync(test); + fs.unlinkSync(test); + assert.equal(String(content), 'close'); + return done(); + }); + return w.loadURL('file://' + path.join(fixtures, 'api', 'close.html')); + }); + return it('should emit beforeunload handler', function(done) { + w.on('onbeforeunload', function() { + return done(); + }); + return w.loadURL('file://' + path.join(fixtures, 'api', 'close-beforeunload-false.html')); + }); + }); + describe('BrowserWindow.destroy()', function() { + return it('prevents users to access methods of webContents', function() { + var webContents; + webContents = w.webContents; + w.destroy(); + return assert.throws((function() { + return webContents.getId(); + }), /Object has been destroyed/); + }); + }); + describe('BrowserWindow.loadURL(url)', function() { + it('should emit did-start-loading event', function(done) { + w.webContents.on('did-start-loading', function() { + return done(); + }); + return w.loadURL('about:blank'); + }); + return it('should emit did-fail-load event', function(done) { + w.webContents.on('did-fail-load', function() { + return done(); + }); + return w.loadURL('file://a.txt'); + }); + }); + describe('BrowserWindow.show()', function() { + return it('should focus on window', function() { + if (isCI) { + return; + } + w.show(); + return assert(w.isFocused()); + }); + }); + describe('BrowserWindow.showInactive()', function() { + return it('should not focus on window', function() { + w.showInactive(); + return assert(!w.isFocused()); + }); + }); + describe('BrowserWindow.focus()', function() { + return it('does not make the window become visible', function() { + assert.equal(w.isVisible(), false); + w.focus(); + return assert.equal(w.isVisible(), false); + }); + }); + describe('BrowserWindow.capturePage(rect, callback)', function() { + return it('calls the callback with a Buffer', function(done) { + return w.capturePage({ + x: 0, + y: 0, + width: 100, + height: 100 + }, function(image) { + assert.equal(image.isEmpty(), true); + return done(); + }); + }); + }); + describe('BrowserWindow.setSize(width, height)', function() { + return it('sets the window size', function(done) { + var size; + size = [300, 400]; + w.once('resize', function() { + var newSize; + newSize = w.getSize(); + assert.equal(newSize[0], size[0]); + assert.equal(newSize[1], size[1]); + return done(); + }); + return w.setSize(size[0], size[1]); + }); + }); + describe('BrowserWindow.setPosition(x, y)', function() { + return it('sets the window position', function(done) { + var pos; + pos = [10, 10]; + w.once('move', function() { + var newPos; + newPos = w.getPosition(); + assert.equal(newPos[0], pos[0]); + assert.equal(newPos[1], pos[1]); + return done(); + }); + return w.setPosition(pos[0], pos[1]); + }); + }); + describe('BrowserWindow.setContentSize(width, height)', function() { + it('sets the content size', function() { + var after, size; + size = [400, 400]; + w.setContentSize(size[0], size[1]); + after = w.getContentSize(); + assert.equal(after[0], size[0]); + return assert.equal(after[1], size[1]); + }); + return it('works for framless window', function() { + var after, size; + w.destroy(); + w = new BrowserWindow({ + show: false, + frame: false, + width: 400, + height: 400 + }); + size = [400, 400]; + w.setContentSize(size[0], size[1]); + after = w.getContentSize(); + assert.equal(after[0], size[0]); + return assert.equal(after[1], size[1]); + }); + }); + describe('BrowserWindow.fromId(id)', function() { + return it('returns the window with id', function() { + return assert.equal(w.id, BrowserWindow.fromId(w.id).id); + }); + }); + describe('BrowserWindow.setResizable(resizable)', function() { + return it('does not change window size for frameless window', function() { + var s; + w.destroy(); + w = new BrowserWindow({ + show: true, + frame: false + }); + s = w.getSize(); + w.setResizable(!w.isResizable()); + return assert.deepEqual(s, w.getSize()); + }); + }); + describe('"useContentSize" option', function() { + it('make window created with content size when used', function() { + var contentSize; + w.destroy(); + w = new BrowserWindow({ + show: false, + width: 400, + height: 400, + useContentSize: true + }); + contentSize = w.getContentSize(); + assert.equal(contentSize[0], 400); + return assert.equal(contentSize[1], 400); + }); + it('make window created with window size when not used', function() { + var size; + size = w.getSize(); + assert.equal(size[0], 400); + return assert.equal(size[1], 400); + }); + return it('works for framless window', function() { + var contentSize, size; + w.destroy(); + w = new BrowserWindow({ + show: false, + frame: false, + width: 400, + height: 400, + useContentSize: true + }); + contentSize = w.getContentSize(); + assert.equal(contentSize[0], 400); + assert.equal(contentSize[1], 400); + size = w.getSize(); + assert.equal(size[0], 400); + return assert.equal(size[1], 400); + }); + }); + describe('"title-bar-style" option', function() { + if (process.platform !== 'darwin') { + return; + } + if (parseInt(os.release().split('.')[0]) < 14) { + return; + } + it('creates browser window with hidden title bar', function() { + var contentSize; + w.destroy(); + w = new BrowserWindow({ + show: false, + width: 400, + height: 400, + titleBarStyle: 'hidden' + }); + contentSize = w.getContentSize(); + return assert.equal(contentSize[1], 400); + }); + return it('creates browser window with hidden inset title bar', function() { + var contentSize; + w.destroy(); + w = new BrowserWindow({ + show: false, + width: 400, + height: 400, + titleBarStyle: 'hidden-inset' + }); + contentSize = w.getContentSize(); + return assert.equal(contentSize[1], 400); + }); + }); + describe('"enableLargerThanScreen" option', function() { + if (process.platform === 'linux') { + return; + } + beforeEach(function() { + w.destroy(); + return w = new BrowserWindow({ + show: true, + width: 400, + height: 400, + enableLargerThanScreen: true + }); + }); + it('can move the window out of screen', function() { + var after; + w.setPosition(-10, -10); + after = w.getPosition(); + assert.equal(after[0], -10); + return assert.equal(after[1], -10); + }); + return it('can set the window larger than screen', function() { + var after, size; + size = screen.getPrimaryDisplay().size; + size.width += 100; + size.height += 100; + w.setSize(size.width, size.height); + after = w.getSize(); + assert.equal(after[0], size.width); + return assert.equal(after[1], size.height); + }); + }); + describe('"web-preferences" option', function() { + afterEach(function() { + return ipcMain.removeAllListeners('answer'); + }); + describe('"preload" option', function() { + return it('loads the script before other scripts in window', function(done) { + var preload; + preload = path.join(fixtures, 'module', 'set-global.js'); + ipcMain.once('answer', function(event, test) { + assert.equal(test, 'preload'); + return done(); + }); + w.destroy(); + w = new BrowserWindow({ + show: false, + webPreferences: { + preload: preload + } + }); + return w.loadURL('file://' + path.join(fixtures, 'api', 'preload.html')); + }); + }); + return describe('"node-integration" option', function() { + return it('disables node integration when specified to false', function(done) { + var preload; + preload = path.join(fixtures, 'module', 'send-later.js'); + ipcMain.once('answer', function(event, test) { + assert.equal(test, 'undefined'); + return done(); + }); + w.destroy(); + w = new BrowserWindow({ + show: false, + webPreferences: { + preload: preload, + nodeIntegration: false + } + }); + return w.loadURL('file://' + path.join(fixtures, 'api', 'blank.html')); + }); + }); + }); + describe('beforeunload handler', function() { + it('returning true would not prevent close', function(done) { + w.on('closed', function() { + return done(); + }); + return w.loadURL('file://' + path.join(fixtures, 'api', 'close-beforeunload-true.html')); + }); + it('returning non-empty string would not prevent close', function(done) { + w.on('closed', function() { + return done(); + }); + return w.loadURL('file://' + path.join(fixtures, 'api', 'close-beforeunload-string.html')); + }); + it('returning false would prevent close', function(done) { + w.on('onbeforeunload', function() { + return done(); + }); + return w.loadURL('file://' + path.join(fixtures, 'api', 'close-beforeunload-false.html')); + }); + return it('returning empty string would prevent close', function(done) { + w.on('onbeforeunload', function() { + return done(); + }); + return w.loadURL('file://' + path.join(fixtures, 'api', 'close-beforeunload-empty-string.html')); + }); + }); + describe('new-window event', function() { + if (isCI && process.platform === 'darwin') { + return; + } + it('emits when window.open is called', function(done) { + w.webContents.once('new-window', function(e, url, frameName) { + e.preventDefault(); + assert.equal(url, 'http://host/'); + assert.equal(frameName, 'host'); + return done(); + }); + return w.loadURL("file://" + fixtures + "/pages/window-open.html"); + }); + return it('emits when link with target is called', function(done) { + this.timeout(10000); + w.webContents.once('new-window', function(e, url, frameName) { + e.preventDefault(); + assert.equal(url, 'http://host/'); + assert.equal(frameName, 'target'); + return done(); + }); + return w.loadURL("file://" + fixtures + "/pages/target-name.html"); + }); + }); + describe('maximize event', function() { + if (isCI) { + return; + } + return it('emits when window is maximized', function(done) { + this.timeout(10000); + w.once('maximize', function() { + return done(); + }); + w.show(); + return w.maximize(); + }); + }); + describe('unmaximize event', function() { + if (isCI) { + return; + } + return it('emits when window is unmaximized', function(done) { + this.timeout(10000); + w.once('unmaximize', function() { + return done(); + }); + w.show(); + w.maximize(); + return w.unmaximize(); + }); + }); + describe('minimize event', function() { + if (isCI) { + return; + } + return it('emits when window is minimized', function(done) { + this.timeout(10000); + w.once('minimize', function() { + return done(); + }); + w.show(); + return w.minimize(); + }); + }); + xdescribe('beginFrameSubscription method', function() { + return it('subscribes frame updates', function(done) { + w.loadURL("file://" + fixtures + "/api/blank.html"); + return w.webContents.beginFrameSubscription(function(data) { + assert.notEqual(data.length, 0); + w.webContents.endFrameSubscription(); + return done(); + }); + }); + }); + describe('save page', function() { + var savePageCssPath, savePageDir, savePageHtmlPath, savePageJsPath; + savePageDir = path.join(fixtures, 'save_page'); + savePageHtmlPath = path.join(savePageDir, 'save_page.html'); + savePageJsPath = path.join(savePageDir, 'save_page_files', 'test.js'); + savePageCssPath = path.join(savePageDir, 'save_page_files', 'test.css'); + return it('should save page', function(done) { + w.webContents.on('did-finish-load', function() { + return w.webContents.savePage(savePageHtmlPath, 'HTMLComplete', function(error) { + assert.equal(error, null); + assert(fs.existsSync(savePageHtmlPath)); + assert(fs.existsSync(savePageJsPath)); + assert(fs.existsSync(savePageCssPath)); + fs.unlinkSync(savePageCssPath); + fs.unlinkSync(savePageJsPath); + fs.unlinkSync(savePageHtmlPath); + fs.rmdirSync(path.join(savePageDir, 'save_page_files')); + fs.rmdirSync(savePageDir); + return done(); + }); + }); + return w.loadURL("file://" + fixtures + "/pages/save_page/index.html"); + }); + }); + return describe('BrowserWindow options argument is optional', function() { + return it('should create a window with default size (800x600)', function() { + var size; + w.destroy(); + w = new BrowserWindow(); + size = w.getSize(); + assert.equal(size[0], 800); + return assert.equal(size[1], 600); + }); + }); +}); diff --git a/spec/api-clipboard-spec.coffee b/spec/api-clipboard-spec.coffee deleted file mode 100644 index 19da3fc75f1b..000000000000 --- a/spec/api-clipboard-spec.coffee +++ /dev/null @@ -1,52 +0,0 @@ -assert = require 'assert' -path = require 'path' - -{clipboard, nativeImage} = require 'electron' - -describe 'clipboard module', -> - fixtures = path.resolve __dirname, 'fixtures' - - describe 'clipboard.readImage()', -> - it 'returns NativeImage intance', -> - p = path.join fixtures, 'assets', 'logo.png' - i = nativeImage.createFromPath p - clipboard.writeImage p - assert.equal clipboard.readImage().toDataURL(), i.toDataURL() - - describe 'clipboard.readText()', -> - it 'returns unicode string correctly', -> - text = '千江有水千江月,万里无云万里天' - clipboard.writeText text - assert.equal clipboard.readText(), text - - describe 'clipboard.readHtml()', -> - it 'returns markup correctly', -> - text = 'Hi' - markup = - if process.platform is 'darwin' - 'Hi' - else if process.platform is 'linux' - 'Hi' - else - 'Hi' - clipboard.writeHtml text - assert.equal clipboard.readHtml(), markup - - describe 'clipboard.write()', -> - it 'returns data correctly', -> - text = 'test' - p = path.join fixtures, 'assets', 'logo.png' - i = nativeImage.createFromPath p - markup = - if process.platform is 'darwin' - 'Hi' - else if process.platform is 'linux' - 'Hi' - else - 'Hi' - clipboard.write {text: "test", html: 'Hi', image: p} - assert.equal clipboard.readText(), text - assert.equal clipboard.readHtml(), markup - assert.equal clipboard.readImage().toDataURL(), i.toDataURL() diff --git a/spec/api-clipboard-spec.js b/spec/api-clipboard-spec.js new file mode 100644 index 000000000000..6154181f0926 --- /dev/null +++ b/spec/api-clipboard-spec.js @@ -0,0 +1,55 @@ +var assert, clipboard, nativeImage, path, ref; + +assert = require('assert'); + +path = require('path'); + +ref = require('electron'), clipboard = ref.clipboard, nativeImage = ref.nativeImage; + +describe('clipboard module', function() { + var fixtures; + fixtures = path.resolve(__dirname, 'fixtures'); + describe('clipboard.readImage()', function() { + return it('returns NativeImage intance', function() { + var i, p; + p = path.join(fixtures, 'assets', 'logo.png'); + i = nativeImage.createFromPath(p); + clipboard.writeImage(p); + return assert.equal(clipboard.readImage().toDataURL(), i.toDataURL()); + }); + }); + describe('clipboard.readText()', function() { + return it('returns unicode string correctly', function() { + var text; + text = '千江有水千江月,万里无云万里天'; + clipboard.writeText(text); + return assert.equal(clipboard.readText(), text); + }); + }); + describe('clipboard.readHtml()', function() { + return it('returns markup correctly', function() { + var markup, text; + text = 'Hi'; + markup = process.platform === 'darwin' ? 'Hi' : process.platform === 'linux' ? 'Hi' : 'Hi'; + clipboard.writeHtml(text); + return assert.equal(clipboard.readHtml(), markup); + }); + }); + return describe('clipboard.write()', function() { + return it('returns data correctly', function() { + var i, markup, p, text; + text = 'test'; + p = path.join(fixtures, 'assets', 'logo.png'); + i = nativeImage.createFromPath(p); + markup = process.platform === 'darwin' ? 'Hi' : process.platform === 'linux' ? 'Hi' : 'Hi'; + clipboard.write({ + text: "test", + html: 'Hi', + image: p + }); + assert.equal(clipboard.readText(), text); + assert.equal(clipboard.readHtml(), markup); + return assert.equal(clipboard.readImage().toDataURL(), i.toDataURL()); + }); + }); +}); diff --git a/spec/api-crash-reporter-spec.coffee b/spec/api-crash-reporter-spec.coffee deleted file mode 100644 index 334956d3c06b..000000000000 --- a/spec/api-crash-reporter-spec.coffee +++ /dev/null @@ -1,66 +0,0 @@ -assert = require 'assert' -path = require 'path' -http = require 'http' -url = require 'url' -multiparty = require 'multiparty' - -{remote} = require 'electron' -{app, crashReporter, BrowserWindow} = remote.require 'electron' - -describe 'crash-reporter module', -> - fixtures = path.resolve __dirname, 'fixtures' - - w = null - beforeEach -> w = new BrowserWindow(show: false) - afterEach -> w.destroy() - - # It is not working for mas build. - return if process.mas - - # The crash-reporter test is not reliable on CI machine. - isCI = remote.getGlobal('isCi') - return if isCI - - it 'should send minidump when renderer crashes', (done) -> - @timeout 120000 - called = false - server = http.createServer (req, res) -> - server.close() - form = new multiparty.Form() - form.parse req, (error, fields, files) -> - # This callback can be called for twice sometimes. - return if called - called = true - - assert.equal fields['prod'], 'Electron' - assert.equal fields['ver'], process.versions['electron'] - assert.equal fields['process_type'], 'renderer' - assert.equal fields['platform'], process.platform - assert.equal fields['extra1'], 'extra1' - assert.equal fields['extra2'], 'extra2' - assert.equal fields['_productName'], 'Zombies' - assert.equal fields['_companyName'], 'Umbrella Corporation' - assert.equal fields['_version'], app.getVersion() - - res.end('abc-123-def') - done() - # Server port is generated randomly for the first run, it will be reused - # when page is refreshed. - port = remote.process.port - server.listen port, '127.0.0.1', -> - {port} = server.address() - remote.process.port = port - url = url.format - protocol: 'file' - pathname: path.join fixtures, 'api', 'crash.html' - search: "?port=#{port}" - if process.platform is 'darwin' - crashReporter.start - companyName: 'Umbrella Corporation' - submitURL: "http://127.0.0.1:#{port}" - w.loadURL url - - describe ".start(options)", -> - it 'requires that the companyName and submitURL options be specified', -> - assert.throws(-> crashReporter.start({companyName: 'Missing submitURL'})) - assert.throws(-> crashReporter.start({submitURL: 'Missing companyName'})) diff --git a/spec/api-crash-reporter-spec.js b/spec/api-crash-reporter-spec.js new file mode 100644 index 000000000000..88cbe0a666dc --- /dev/null +++ b/spec/api-crash-reporter-spec.js @@ -0,0 +1,94 @@ +var BrowserWindow, app, assert, crashReporter, http, multiparty, path, ref, remote, url; + +assert = require('assert'); + +path = require('path'); + +http = require('http'); + +url = require('url'); + +multiparty = require('multiparty'); + +remote = require('electron').remote; + +ref = remote.require('electron'), app = ref.app, crashReporter = ref.crashReporter, BrowserWindow = ref.BrowserWindow; + +describe('crash-reporter module', function() { + var fixtures, isCI, w; + fixtures = path.resolve(__dirname, 'fixtures'); + w = null; + beforeEach(function() { + return w = new BrowserWindow({ + show: false + }); + }); + afterEach(function() { + return w.destroy(); + }); + if (process.mas) { + return; + } + isCI = remote.getGlobal('isCi'); + if (isCI) { + return; + } + it('should send minidump when renderer crashes', function(done) { + var called, port, server; + this.timeout(120000); + called = false; + server = http.createServer(function(req, res) { + var form; + server.close(); + form = new multiparty.Form(); + return form.parse(req, function(error, fields, files) { + if (called) { + return; + } + called = true; + assert.equal(fields['prod'], 'Electron'); + assert.equal(fields['ver'], process.versions['electron']); + assert.equal(fields['process_type'], 'renderer'); + assert.equal(fields['platform'], process.platform); + assert.equal(fields['extra1'], 'extra1'); + assert.equal(fields['extra2'], 'extra2'); + assert.equal(fields['_productName'], 'Zombies'); + assert.equal(fields['_companyName'], 'Umbrella Corporation'); + assert.equal(fields['_version'], app.getVersion()); + res.end('abc-123-def'); + return done(); + }); + }); + port = remote.process.port; + return server.listen(port, '127.0.0.1', function() { + port = server.address().port; + remote.process.port = port; + url = url.format({ + protocol: 'file', + pathname: path.join(fixtures, 'api', 'crash.html'), + search: "?port=" + port + }); + if (process.platform === 'darwin') { + crashReporter.start({ + companyName: 'Umbrella Corporation', + submitURL: "http://127.0.0.1:" + port + }); + } + return w.loadURL(url); + }); + }); + return describe(".start(options)", function() { + return it('requires that the companyName and submitURL options be specified', function() { + assert.throws(function() { + return crashReporter.start({ + companyName: 'Missing submitURL' + }); + }); + return assert.throws(function() { + return crashReporter.start({ + submitURL: 'Missing companyName' + }); + }); + }); + }); +}); diff --git a/spec/api-desktop-capturer.coffee b/spec/api-desktop-capturer.coffee deleted file mode 100644 index 6f1842dd145c..000000000000 --- a/spec/api-desktop-capturer.coffee +++ /dev/null @@ -1,9 +0,0 @@ -assert = require 'assert' -{desktopCapturer} = require 'electron' - -describe 'desktopCapturer', -> - it 'should returns something', (done) -> - desktopCapturer.getSources {types: ['window', 'screen']}, (error, sources) -> - assert.equal error, null - assert.notEqual sources.length, 0 - done() diff --git a/spec/api-desktop-capturer.js b/spec/api-desktop-capturer.js new file mode 100644 index 000000000000..288e57ee4489 --- /dev/null +++ b/spec/api-desktop-capturer.js @@ -0,0 +1,17 @@ +var assert, desktopCapturer; + +assert = require('assert'); + +desktopCapturer = require('electron').desktopCapturer; + +describe('desktopCapturer', function() { + return it('should returns something', function(done) { + return desktopCapturer.getSources({ + types: ['window', 'screen'] + }, function(error, sources) { + assert.equal(error, null); + assert.notEqual(sources.length, 0); + return done(); + }); + }); +}); diff --git a/spec/api-ipc-spec.coffee b/spec/api-ipc-spec.coffee deleted file mode 100644 index 67ef717d5045..000000000000 --- a/spec/api-ipc-spec.coffee +++ /dev/null @@ -1,110 +0,0 @@ -assert = require 'assert' -path = require 'path' - -{ipcRenderer, remote} = require 'electron' -{ipcMain, BrowserWindow} = remote.require 'electron' - -comparePaths = (path1, path2) -> - if process.platform is 'win32' - # Paths in Windows are case insensitive. - path1 = path1.toLowerCase() - path2 = path2.toLowerCase() - assert.equal path1, path2 - -describe 'ipc module', -> - fixtures = path.join __dirname, 'fixtures' - - describe 'remote.require', -> - it 'should returns same object for the same module', -> - dialog1 = remote.require 'electron' - dialog2 = remote.require 'electron' - assert.equal dialog1, dialog2 - - it 'should work when object contains id property', -> - a = remote.require path.join(fixtures, 'module', 'id.js') - assert.equal a.id, 1127 - - it 'should search module from the user app', -> - comparePaths path.normalize(remote.process.mainModule.filename), path.resolve(__dirname, 'static', 'main.js') - comparePaths path.normalize(remote.process.mainModule.paths[0]), path.resolve(__dirname, 'static', 'node_modules') - - describe 'remote.createFunctionWithReturnValue', -> - it 'should be called in browser synchronously', -> - buf = new Buffer('test') - call = remote.require path.join(fixtures, 'module', 'call.js') - result = call.call remote.createFunctionWithReturnValue(buf) - assert.equal result.constructor.name, 'Buffer' - - describe 'remote object in renderer', -> - it 'can change its properties', -> - property = remote.require path.join(fixtures, 'module', 'property.js') - assert.equal property.property, 1127 - property.property = 1007 - assert.equal property.property, 1007 - property2 = remote.require path.join(fixtures, 'module', 'property.js') - assert.equal property2.property, 1007 - - # Restore. - property.property = 1127 - - it 'can construct an object from its member', -> - call = remote.require path.join(fixtures, 'module', 'call.js') - obj = new call.constructor - assert.equal obj.test, 'test' - - describe 'remote value in browser', -> - print = path.join(fixtures, 'module', 'print_name.js') - - it 'keeps its constructor name for objects', -> - buf = new Buffer('test') - print_name = remote.require print - assert.equal print_name.print(buf), 'Buffer' - - it 'supports instanceof Date', -> - now = new Date() - print_name = remote.require print - assert.equal print_name.print(now), 'Date' - assert.deepEqual print_name.echo(now), now - - describe 'remote promise', -> - it 'can be used as promise in each side', (done) -> - promise = remote.require path.join(fixtures, 'module', 'promise.js') - promise.twicePromise(Promise.resolve(1234)) - .then (value) => - assert.equal value, 2468 - done() - - describe 'ipc.sender.send', -> - it 'should work when sending an object containing id property', (done) -> - obj = id: 1, name: 'ly' - ipcRenderer.once 'message', (event, message) -> - assert.deepEqual message, obj - done() - ipcRenderer.send 'message', obj - - describe 'ipc.sendSync', -> - it 'can be replied by setting event.returnValue', -> - msg = ipcRenderer.sendSync 'echo', 'test' - assert.equal msg, 'test' - - it 'does not crash when reply is not sent and browser is destroyed', (done) -> - @timeout 10000 - w = new BrowserWindow(show: false) - ipcMain.once 'send-sync-message', (event) -> - event.returnValue = null - w.destroy() - done() - w.loadURL 'file://' + path.join(fixtures, 'api', 'send-sync-message.html') - - describe 'remote listeners', -> - w = null - afterEach -> - w.destroy() - - it 'can be added and removed correctly', -> - w = new BrowserWindow(show: false) - listener = -> - w.on 'test', listener - assert.equal w.listenerCount('test'), 1 - w.removeListener 'test', listener - assert.equal w.listenerCount('test'), 0 diff --git a/spec/api-ipc-spec.js b/spec/api-ipc-spec.js new file mode 100644 index 000000000000..fba5b6d68402 --- /dev/null +++ b/spec/api-ipc-spec.js @@ -0,0 +1,147 @@ +var BrowserWindow, assert, comparePaths, ipcMain, ipcRenderer, path, ref, ref1, remote; + +assert = require('assert'); + +path = require('path'); + +ref = require('electron'), ipcRenderer = ref.ipcRenderer, remote = ref.remote; + +ref1 = remote.require('electron'), ipcMain = ref1.ipcMain, BrowserWindow = ref1.BrowserWindow; + +comparePaths = function(path1, path2) { + if (process.platform === 'win32') { + path1 = path1.toLowerCase(); + path2 = path2.toLowerCase(); + } + return assert.equal(path1, path2); +}; + +describe('ipc module', function() { + var fixtures; + fixtures = path.join(__dirname, 'fixtures'); + describe('remote.require', function() { + it('should returns same object for the same module', function() { + var dialog1, dialog2; + dialog1 = remote.require('electron'); + dialog2 = remote.require('electron'); + return assert.equal(dialog1, dialog2); + }); + it('should work when object contains id property', function() { + var a; + a = remote.require(path.join(fixtures, 'module', 'id.js')); + return assert.equal(a.id, 1127); + }); + return it('should search module from the user app', function() { + comparePaths(path.normalize(remote.process.mainModule.filename), path.resolve(__dirname, 'static', 'main.js')); + return comparePaths(path.normalize(remote.process.mainModule.paths[0]), path.resolve(__dirname, 'static', 'node_modules')); + }); + }); + describe('remote.createFunctionWithReturnValue', function() { + return it('should be called in browser synchronously', function() { + var buf, call, result; + buf = new Buffer('test'); + call = remote.require(path.join(fixtures, 'module', 'call.js')); + result = call.call(remote.createFunctionWithReturnValue(buf)); + return assert.equal(result.constructor.name, 'Buffer'); + }); + }); + describe('remote object in renderer', function() { + it('can change its properties', function() { + var property, property2; + property = remote.require(path.join(fixtures, 'module', 'property.js')); + assert.equal(property.property, 1127); + property.property = 1007; + assert.equal(property.property, 1007); + property2 = remote.require(path.join(fixtures, 'module', 'property.js')); + assert.equal(property2.property, 1007); + return property.property = 1127; + }); + return it('can construct an object from its member', function() { + var call, obj; + call = remote.require(path.join(fixtures, 'module', 'call.js')); + obj = new call.constructor; + return assert.equal(obj.test, 'test'); + }); + }); + describe('remote value in browser', function() { + var print; + print = path.join(fixtures, 'module', 'print_name.js'); + it('keeps its constructor name for objects', function() { + var buf, print_name; + buf = new Buffer('test'); + print_name = remote.require(print); + return assert.equal(print_name.print(buf), 'Buffer'); + }); + return it('supports instanceof Date', function() { + var now, print_name; + now = new Date(); + print_name = remote.require(print); + assert.equal(print_name.print(now), 'Date'); + return assert.deepEqual(print_name.echo(now), now); + }); + }); + describe('remote promise', function() { + return it('can be used as promise in each side', function(done) { + var promise; + promise = remote.require(path.join(fixtures, 'module', 'promise.js')); + return promise.twicePromise(Promise.resolve(1234)).then((function(_this) { + return function(value) { + assert.equal(value, 2468); + return done(); + }; + })(this)); + }); + }); + describe('ipc.sender.send', function() { + return it('should work when sending an object containing id property', function(done) { + var obj; + obj = { + id: 1, + name: 'ly' + }; + ipcRenderer.once('message', function(event, message) { + assert.deepEqual(message, obj); + return done(); + }); + return ipcRenderer.send('message', obj); + }); + }); + describe('ipc.sendSync', function() { + it('can be replied by setting event.returnValue', function() { + var msg; + msg = ipcRenderer.sendSync('echo', 'test'); + return assert.equal(msg, 'test'); + }); + return it('does not crash when reply is not sent and browser is destroyed', function(done) { + var w; + this.timeout(10000); + w = new BrowserWindow({ + show: false + }); + ipcMain.once('send-sync-message', function(event) { + event.returnValue = null; + w.destroy(); + return done(); + }); + return w.loadURL('file://' + path.join(fixtures, 'api', 'send-sync-message.html')); + }); + }); + return describe('remote listeners', function() { + var w; + w = null; + afterEach(function() { + return w.destroy(); + }); + return it('can be added and removed correctly', function() { + var listener; + w = new BrowserWindow({ + show: false + }); + listener = function() {}; + w.on('test', listener); + assert.equal(w.listenerCount('test'), 1); + w.removeListener('test', listener); + return assert.equal(w.listenerCount('test'), 0); + }); + }); +}); diff --git a/spec/api-menu-spec.coffee b/spec/api-menu-spec.coffee deleted file mode 100644 index 2f1f6465898f..000000000000 --- a/spec/api-menu-spec.coffee +++ /dev/null @@ -1,172 +0,0 @@ -assert = require 'assert' - -{remote, ipcRenderer} = require 'electron' -{Menu, MenuItem} = remote.require 'electron' - -describe 'menu module', -> - describe 'Menu.buildFromTemplate', -> - it 'should be able to attach extra fields', -> - menu = Menu.buildFromTemplate [label: 'text', extra: 'field'] - assert.equal menu.items[0].extra, 'field' - - it 'does not modify the specified template', -> - template = ipcRenderer.sendSync 'eval', """ - var template = [{label: 'text', submenu: [{label: 'sub'}]}]; - require('electron').Menu.buildFromTemplate(template); - template; - """ - assert.deepStrictEqual template, [label: 'text', submenu: [label: 'sub']] - - describe 'Menu.buildFromTemplate should reorder based on item position specifiers', -> - it 'should position before existing item', -> - menu = Menu.buildFromTemplate [ - {label: '2', id: '2'} - {label: '3', id: '3'} - {label: '1', id: '1', position: 'before=2'} - ] - assert.equal menu.items[0].label, '1' - assert.equal menu.items[1].label, '2' - assert.equal menu.items[2].label, '3' - - it 'should position after existing item', -> - menu = Menu.buildFromTemplate [ - {label: '1', id: '1'} - {label: '3', id: '3'} - {label: '2', id: '2', position: 'after=1'} - ] - assert.equal menu.items[0].label, '1' - assert.equal menu.items[1].label, '2' - assert.equal menu.items[2].label, '3' - - it 'should position at endof existing separator groups', -> - menu = Menu.buildFromTemplate [ - {type: 'separator', id: 'numbers'} - {type: 'separator', id: 'letters'} - {label: 'a', id: 'a', position: 'endof=letters'} - {label: '1', id: '1', position: 'endof=numbers'} - {label: 'b', id: 'b', position: 'endof=letters'} - {label: '2', id: '2', position: 'endof=numbers'} - {label: 'c', id: 'c', position: 'endof=letters'} - {label: '3', id: '3', position: 'endof=numbers'} - ] - assert.equal menu.items[0].id, 'numbers' - assert.equal menu.items[1].label, '1' - assert.equal menu.items[2].label, '2' - assert.equal menu.items[3].label, '3' - assert.equal menu.items[4].id, 'letters' - assert.equal menu.items[5].label, 'a' - assert.equal menu.items[6].label, 'b' - assert.equal menu.items[7].label, 'c' - - it 'should create separator group if endof does not reference existing separator group', -> - menu = Menu.buildFromTemplate [ - {label: 'a', id: 'a', position: 'endof=letters'} - {label: '1', id: '1', position: 'endof=numbers'} - {label: 'b', id: 'b', position: 'endof=letters'} - {label: '2', id: '2', position: 'endof=numbers'} - {label: 'c', id: 'c', position: 'endof=letters'} - {label: '3', id: '3', position: 'endof=numbers'} - ] - - assert.equal menu.items[0].id, 'letters' - assert.equal menu.items[1].label, 'a' - assert.equal menu.items[2].label, 'b' - assert.equal menu.items[3].label, 'c' - assert.equal menu.items[4].id, 'numbers' - assert.equal menu.items[5].label, '1' - assert.equal menu.items[6].label, '2' - assert.equal menu.items[7].label, '3' - - it 'should continue inserting items at next index when no specifier is present', -> - menu = Menu.buildFromTemplate [ - {label: '4', id: '4'} - {label: '5', id: '5'} - {label: '1', id: '1', position: 'before=4'} - {label: '2', id: '2'} - {label: '3', id: '3'} - ] - assert.equal menu.items[0].label, '1' - assert.equal menu.items[1].label, '2' - assert.equal menu.items[2].label, '3' - assert.equal menu.items[3].label, '4' - assert.equal menu.items[4].label, '5' - - describe 'Menu.insert', -> - it 'should store item in @items by its index', -> - menu = Menu.buildFromTemplate [ - {label: '1'} - {label: '2'} - {label: '3'} - ] - item = new MenuItem(label: 'inserted') - menu.insert 1, item - - assert.equal menu.items[0].label, '1' - assert.equal menu.items[1].label, 'inserted' - assert.equal menu.items[2].label, '2' - assert.equal menu.items[3].label, '3' - - describe 'MenuItem.click', -> - it 'should be called with the item object passed', (done) -> - menu = Menu.buildFromTemplate [ - label: 'text' - click: (item) -> - assert.equal item.constructor.name, 'MenuItem' - assert.equal item.label, 'text' - done() - ] - menu.delegate.executeCommand menu.items[0].commandId - - describe 'MenuItem with checked property', -> - it 'clicking an checkbox item should flip the checked property', -> - menu = Menu.buildFromTemplate [ label: 'text', type: 'checkbox' ] - assert.equal menu.items[0].checked, false - menu.delegate.executeCommand menu.items[0].commandId - assert.equal menu.items[0].checked, true - - it 'clicking an radio item should always make checked property true', -> - menu = Menu.buildFromTemplate [ label: 'text', type: 'radio' ] - menu.delegate.executeCommand menu.items[0].commandId - assert.equal menu.items[0].checked, true - menu.delegate.executeCommand menu.items[0].commandId - assert.equal menu.items[0].checked, true - - it 'at least have one item checked in each group', -> - template = [] - template.push label: "#{i}", type: 'radio' for i in [0..10] - template.push type: 'separator' - template.push label: "#{i}", type: 'radio' for i in [12..20] - menu = Menu.buildFromTemplate template - menu.delegate.menuWillShow() - assert.equal menu.items[0].checked, true - assert.equal menu.items[12].checked, true - - it 'should assign groupId automatically', -> - template = [] - template.push label: "#{i}", type: 'radio' for i in [0..10] - template.push type: 'separator' - template.push label: "#{i}", type: 'radio' for i in [12..20] - menu = Menu.buildFromTemplate template - groupId = menu.items[0].groupId - assert.equal menu.items[i].groupId, groupId for i in [0..10] - assert.equal menu.items[i].groupId, groupId + 1 for i in [12..20] - - it "setting 'checked' should flip other items' 'checked' property", -> - template = [] - template.push label: "#{i}", type: 'radio' for i in [0..10] - template.push type: 'separator' - template.push label: "#{i}", type: 'radio' for i in [12..20] - menu = Menu.buildFromTemplate template - assert.equal menu.items[i].checked, false for i in [0..10] - menu.items[0].checked = true - assert.equal menu.items[0].checked, true - assert.equal menu.items[i].checked, false for i in [1..10] - menu.items[10].checked = true - assert.equal menu.items[10].checked, true - assert.equal menu.items[i].checked, false for i in [0..9] - assert.equal menu.items[i].checked, false for i in [12..20] - menu.items[12].checked = true - assert.equal menu.items[10].checked, true - assert.equal menu.items[i].checked, false for i in [0..9] - assert.equal menu.items[12].checked, true - assert.equal menu.items[i].checked, false for i in [13..20] diff --git a/spec/api-menu-spec.js b/spec/api-menu-spec.js new file mode 100644 index 000000000000..5771358337e4 --- /dev/null +++ b/spec/api-menu-spec.js @@ -0,0 +1,349 @@ +var Menu, MenuItem, assert, ipcRenderer, ref, ref1, remote; + +assert = require('assert'); + +ref = require('electron'), remote = ref.remote, ipcRenderer = ref.ipcRenderer; + +ref1 = remote.require('electron'), Menu = ref1.Menu, MenuItem = ref1.MenuItem; + +describe('menu module', function() { + describe('Menu.buildFromTemplate', function() { + it('should be able to attach extra fields', function() { + var menu; + menu = Menu.buildFromTemplate([ + { + label: 'text', + extra: 'field' + } + ]); + return assert.equal(menu.items[0].extra, 'field'); + }); + it('does not modify the specified template', function() { + var template; + template = ipcRenderer.sendSync('eval', "var template = [{label: 'text', submenu: [{label: 'sub'}]}];\nrequire('electron').Menu.buildFromTemplate(template);\ntemplate;"); + return assert.deepStrictEqual(template, [ + { + label: 'text', + submenu: [ + { + label: 'sub' + } + ] + } + ]); + }); + return describe('Menu.buildFromTemplate should reorder based on item position specifiers', function() { + it('should position before existing item', function() { + var menu; + menu = Menu.buildFromTemplate([ + { + label: '2', + id: '2' + }, { + label: '3', + id: '3' + }, { + label: '1', + id: '1', + position: 'before=2' + } + ]); + assert.equal(menu.items[0].label, '1'); + assert.equal(menu.items[1].label, '2'); + return assert.equal(menu.items[2].label, '3'); + }); + it('should position after existing item', function() { + var menu; + menu = Menu.buildFromTemplate([ + { + label: '1', + id: '1' + }, { + label: '3', + id: '3' + }, { + label: '2', + id: '2', + position: 'after=1' + } + ]); + assert.equal(menu.items[0].label, '1'); + assert.equal(menu.items[1].label, '2'); + return assert.equal(menu.items[2].label, '3'); + }); + it('should position at endof existing separator groups', function() { + var menu; + menu = Menu.buildFromTemplate([ + { + type: 'separator', + id: 'numbers' + }, { + type: 'separator', + id: 'letters' + }, { + label: 'a', + id: 'a', + position: 'endof=letters' + }, { + label: '1', + id: '1', + position: 'endof=numbers' + }, { + label: 'b', + id: 'b', + position: 'endof=letters' + }, { + label: '2', + id: '2', + position: 'endof=numbers' + }, { + label: 'c', + id: 'c', + position: 'endof=letters' + }, { + label: '3', + id: '3', + position: 'endof=numbers' + } + ]); + assert.equal(menu.items[0].id, 'numbers'); + assert.equal(menu.items[1].label, '1'); + assert.equal(menu.items[2].label, '2'); + assert.equal(menu.items[3].label, '3'); + assert.equal(menu.items[4].id, 'letters'); + assert.equal(menu.items[5].label, 'a'); + assert.equal(menu.items[6].label, 'b'); + return assert.equal(menu.items[7].label, 'c'); + }); + it('should create separator group if endof does not reference existing separator group', function() { + var menu; + menu = Menu.buildFromTemplate([ + { + label: 'a', + id: 'a', + position: 'endof=letters' + }, { + label: '1', + id: '1', + position: 'endof=numbers' + }, { + label: 'b', + id: 'b', + position: 'endof=letters' + }, { + label: '2', + id: '2', + position: 'endof=numbers' + }, { + label: 'c', + id: 'c', + position: 'endof=letters' + }, { + label: '3', + id: '3', + position: 'endof=numbers' + } + ]); + assert.equal(menu.items[0].id, 'letters'); + assert.equal(menu.items[1].label, 'a'); + assert.equal(menu.items[2].label, 'b'); + assert.equal(menu.items[3].label, 'c'); + assert.equal(menu.items[4].id, 'numbers'); + assert.equal(menu.items[5].label, '1'); + assert.equal(menu.items[6].label, '2'); + return assert.equal(menu.items[7].label, '3'); + }); + return it('should continue inserting items at next index when no specifier is present', function() { + var menu; + menu = Menu.buildFromTemplate([ + { + label: '4', + id: '4' + }, { + label: '5', + id: '5' + }, { + label: '1', + id: '1', + position: 'before=4' + }, { + label: '2', + id: '2' + }, { + label: '3', + id: '3' + } + ]); + assert.equal(menu.items[0].label, '1'); + assert.equal(menu.items[1].label, '2'); + assert.equal(menu.items[2].label, '3'); + assert.equal(menu.items[3].label, '4'); + return assert.equal(menu.items[4].label, '5'); + }); + }); + }); + describe('Menu.insert', function() { + return it('should store item in @items by its index', function() { + var item, menu; + menu = Menu.buildFromTemplate([ + { + label: '1' + }, { + label: '2' + }, { + label: '3' + } + ]); + item = new MenuItem({ + label: 'inserted' + }); + menu.insert(1, item); + assert.equal(menu.items[0].label, '1'); + assert.equal(menu.items[1].label, 'inserted'); + assert.equal(menu.items[2].label, '2'); + return assert.equal(menu.items[3].label, '3'); + }); + }); + describe('MenuItem.click', function() { + return it('should be called with the item object passed', function(done) { + var menu; + menu = Menu.buildFromTemplate([ + { + label: 'text', + click: function(item) { + assert.equal(item.constructor.name, 'MenuItem'); + assert.equal(item.label, 'text'); + return done(); + } + } + ]); + return menu.delegate.executeCommand(menu.items[0].commandId); + }); + }); + return describe('MenuItem with checked property', function() { + it('clicking an checkbox item should flip the checked property', function() { + var menu; + menu = Menu.buildFromTemplate([ + { + label: 'text', + type: 'checkbox' + } + ]); + assert.equal(menu.items[0].checked, false); + menu.delegate.executeCommand(menu.items[0].commandId); + return assert.equal(menu.items[0].checked, true); + }); + it('clicking an radio item should always make checked property true', function() { + var menu; + menu = Menu.buildFromTemplate([ + { + label: 'text', + type: 'radio' + } + ]); + menu.delegate.executeCommand(menu.items[0].commandId); + assert.equal(menu.items[0].checked, true); + menu.delegate.executeCommand(menu.items[0].commandId); + return assert.equal(menu.items[0].checked, true); + }); + it('at least have one item checked in each group', function() { + var i, j, k, menu, template; + template = []; + for (i = j = 0; j <= 10; i = ++j) { + template.push({ + label: "" + i, + type: 'radio' + }); + } + template.push({ + type: 'separator' + }); + for (i = k = 12; k <= 20; i = ++k) { + template.push({ + label: "" + i, + type: 'radio' + }); + } + menu = Menu.buildFromTemplate(template); + menu.delegate.menuWillShow(); + assert.equal(menu.items[0].checked, true); + return assert.equal(menu.items[12].checked, true); + }); + it('should assign groupId automatically', function() { + var groupId, i, j, k, l, m, menu, results, template; + template = []; + for (i = j = 0; j <= 10; i = ++j) { + template.push({ + label: "" + i, + type: 'radio' + }); + } + template.push({ + type: 'separator' + }); + for (i = k = 12; k <= 20; i = ++k) { + template.push({ + label: "" + i, + type: 'radio' + }); + } + menu = Menu.buildFromTemplate(template); + groupId = menu.items[0].groupId; + for (i = l = 0; l <= 10; i = ++l) { + assert.equal(menu.items[i].groupId, groupId); + } + results = []; + for (i = m = 12; m <= 20; i = ++m) { + results.push(assert.equal(menu.items[i].groupId, groupId + 1)); + } + return results; + }); + return it("setting 'checked' should flip other items' 'checked' property", function() { + var i, j, k, l, m, menu, n, o, p, q, results, template; + template = []; + for (i = j = 0; j <= 10; i = ++j) { + template.push({ + label: "" + i, + type: 'radio' + }); + } + template.push({ + type: 'separator' + }); + for (i = k = 12; k <= 20; i = ++k) { + template.push({ + label: "" + i, + type: 'radio' + }); + } + menu = Menu.buildFromTemplate(template); + for (i = l = 0; l <= 10; i = ++l) { + assert.equal(menu.items[i].checked, false); + } + menu.items[0].checked = true; + assert.equal(menu.items[0].checked, true); + for (i = m = 1; m <= 10; i = ++m) { + assert.equal(menu.items[i].checked, false); + } + menu.items[10].checked = true; + assert.equal(menu.items[10].checked, true); + for (i = n = 0; n <= 9; i = ++n) { + assert.equal(menu.items[i].checked, false); + } + for (i = o = 12; o <= 20; i = ++o) { + assert.equal(menu.items[i].checked, false); + } + menu.items[12].checked = true; + assert.equal(menu.items[10].checked, true); + for (i = p = 0; p <= 9; i = ++p) { + assert.equal(menu.items[i].checked, false); + } + assert.equal(menu.items[12].checked, true); + results = []; + for (i = q = 13; q <= 20; i = ++q) { + results.push(assert.equal(menu.items[i].checked, false)); + } + return results; + }); + }); +}); diff --git a/spec/api-protocol-spec.coffee b/spec/api-protocol-spec.coffee deleted file mode 100644 index 77eb90259bb2..000000000000 --- a/spec/api-protocol-spec.coffee +++ /dev/null @@ -1,499 +0,0 @@ -assert = require 'assert' -http = require 'http' -path = require 'path' -qs = require 'querystring' - -{remote} = require 'electron' -{protocol} = remote.require 'electron' - -describe 'protocol module', -> - protocolName = 'sp' - text = 'valar morghulis' - postData = - name: 'post test' - type: 'string' - - afterEach (done) -> - protocol.unregisterProtocol protocolName, -> - protocol.uninterceptProtocol 'http', -> done() - - describe 'protocol.register(Any)Protocol', -> - emptyHandler = (request, callback) -> callback() - it 'throws error when scheme is already registered', (done) -> - protocol.registerStringProtocol protocolName, emptyHandler, (error) -> - assert.equal error, null - protocol.registerBufferProtocol protocolName, emptyHandler, (error) -> - assert.notEqual error, null - done() - - it 'does not crash when handler is called twice', (done) -> - doubleHandler = (request, callback) -> - try - callback(text) - callback() - catch - protocol.registerStringProtocol protocolName, doubleHandler, (error) -> - return done(error) if error - $.ajax - url: "#{protocolName}://fake-host" - success: (data) -> - assert.equal data, text - done() - error: (xhr, errorType, error) -> - done(error) - - it 'sends error when callback is called with nothing', (done) -> - protocol.registerBufferProtocol protocolName, emptyHandler, (error) -> - return done(error) if error - $.ajax - url: "#{protocolName}://fake-host" - success: (data) -> - done('request succeeded but it should not') - error: (xhr, errorType, error) -> - assert.equal errorType, 'error' - done() - - it 'does not crash when callback is called in next tick', (done) -> - handler = (request, callback) -> - setImmediate -> callback(text) - protocol.registerStringProtocol protocolName, handler, (error) -> - return done(error) if error - $.ajax - url: "#{protocolName}://fake-host" - success: (data) -> - assert.equal data, text - done() - error: (xhr, errorType, error) -> - done(error) - - describe 'protocol.unregisterProtocol', -> - it 'returns error when scheme does not exist', (done) -> - protocol.unregisterProtocol 'not-exist', (error) -> - assert.notEqual error, null - done() - - describe 'protocol.registerStringProtocol', -> - it 'sends string as response', (done) -> - handler = (request, callback) -> callback(text) - protocol.registerStringProtocol protocolName, handler, (error) -> - return done(error) if error - $.ajax - url: "#{protocolName}://fake-host" - success: (data) -> - assert.equal data, text - done() - error: (xhr, errorType, error) -> - done(error) - - it 'sets Access-Control-Allow-Origin', (done) -> - handler = (request, callback) -> callback(text) - protocol.registerStringProtocol protocolName, handler, (error) -> - return done(error) if error - $.ajax - url: "#{protocolName}://fake-host" - success: (data, status, request) -> - assert.equal data, text - assert.equal( - request.getResponseHeader('Access-Control-Allow-Origin'), - '*') - done() - error: (xhr, errorType, error) -> - done(error) - - it 'sends object as response', (done) -> - handler = (request, callback) -> callback(data: text, mimeType: 'text/html') - protocol.registerStringProtocol protocolName, handler, (error) -> - return done(error) if error - $.ajax - url: "#{protocolName}://fake-host" - success: (data, statux, request) -> - assert.equal data, text - done() - error: (xhr, errorType, error) -> - done(error) - - it 'fails when sending object other than string', (done) -> - handler = (request, callback) -> callback(new Date) - protocol.registerBufferProtocol protocolName, handler, (error) -> - return done(error) if error - $.ajax - url: "#{protocolName}://fake-host" - success: (data) -> - done('request succeeded but it should not') - error: (xhr, errorType, error) -> - assert.equal errorType, 'error' - done() - - describe 'protocol.registerBufferProtocol', -> - buffer = new Buffer(text) - - it 'sends Buffer as response', (done) -> - handler = (request, callback) -> callback(buffer) - protocol.registerBufferProtocol protocolName, handler, (error) -> - return done(error) if error - $.ajax - url: "#{protocolName}://fake-host" - success: (data) -> - assert.equal data, text - done() - error: (xhr, errorType, error) -> - done(error) - - it 'sets Access-Control-Allow-Origin', (done) -> - handler = (request, callback) -> callback(buffer) - protocol.registerBufferProtocol protocolName, handler, (error) -> - return done(error) if error - $.ajax - url: "#{protocolName}://fake-host" - success: (data, status, request) -> - assert.equal data, text - assert.equal( - request.getResponseHeader('Access-Control-Allow-Origin'), - '*') - done() - error: (xhr, errorType, error) -> - done(error) - - it 'sends object as response', (done) -> - handler = (request, callback) -> callback(data: buffer, mimeType: 'text/html') - protocol.registerBufferProtocol protocolName, handler, (error) -> - return done(error) if error - $.ajax - url: "#{protocolName}://fake-host" - success: (data, statux, request) -> - assert.equal data, text - done() - error: (xhr, errorType, error) -> - done(error) - - it 'fails when sending string', (done) -> - handler = (request, callback) -> callback(text) - protocol.registerBufferProtocol protocolName, handler, (error) -> - return done(error) if error - $.ajax - url: "#{protocolName}://fake-host" - success: (data) -> - done('request succeeded but it should not') - error: (xhr, errorType, error) -> - assert.equal errorType, 'error' - done() - - describe 'protocol.registerFileProtocol', -> - filePath = path.join __dirname, 'fixtures', 'asar', 'a.asar', 'file1' - fileContent = require('fs').readFileSync(filePath) - - normalPath = path.join __dirname, 'fixtures', 'pages', 'a.html' - normalContent = require('fs').readFileSync(normalPath) - - it 'sends file path as response', (done) -> - handler = (request, callback) -> callback(filePath) - protocol.registerFileProtocol protocolName, handler, (error) -> - return done(error) if error - $.ajax - url: "#{protocolName}://fake-host" - success: (data) -> - assert.equal data, String(fileContent) - done() - error: (xhr, errorType, error) -> - done(error) - - it 'sets Access-Control-Allow-Origin', (done) -> - handler = (request, callback) -> callback(filePath) - protocol.registerFileProtocol protocolName, handler, (error) -> - return done(error) if error - $.ajax - url: "#{protocolName}://fake-host" - success: (data, status, request) -> - assert.equal data, String(fileContent) - assert.equal( - request.getResponseHeader('Access-Control-Allow-Origin'), - '*') - done() - error: (xhr, errorType, error) -> - done(error) - - it 'sends object as response', (done) -> - handler = (request, callback) -> callback(path: filePath) - protocol.registerFileProtocol protocolName, handler, (error) -> - return done(error) if error - $.ajax - url: "#{protocolName}://fake-host" - success: (data, statux, request) -> - assert.equal data, String(fileContent) - done() - error: (xhr, errorType, error) -> - done(error) - - it 'can send normal file', (done) -> - handler = (request, callback) -> callback(normalPath) - protocol.registerFileProtocol protocolName, handler, (error) -> - return done(error) if error - $.ajax - url: "#{protocolName}://fake-host" - success: (data) -> - assert.equal data, String(normalContent) - done() - error: (xhr, errorType, error) -> - done(error) - - it 'fails when sending unexist-file', (done) -> - fakeFilePath = path.join __dirname, 'fixtures', 'asar', 'a.asar', 'not-exist' - handler = (request, callback) -> callback(fakeFilePath) - protocol.registerBufferProtocol protocolName, handler, (error) -> - return done(error) if error - $.ajax - url: "#{protocolName}://fake-host" - success: (data) -> - done('request succeeded but it should not') - error: (xhr, errorType, error) -> - assert.equal errorType, 'error' - done() - - it 'fails when sending unsupported content', (done) -> - handler = (request, callback) -> callback(new Date) - protocol.registerBufferProtocol protocolName, handler, (error) -> - return done(error) if error - $.ajax - url: "#{protocolName}://fake-host" - success: (data) -> - done('request succeeded but it should not') - error: (xhr, errorType, error) -> - assert.equal errorType, 'error' - done() - - describe 'protocol.registerHttpProtocol', -> - it 'sends url as response', (done) -> - server = http.createServer (req, res) -> - assert.notEqual req.headers.accept, '' - res.end(text) - server.close() - server.listen 0, '127.0.0.1', -> - {port} = server.address() - url = "http://127.0.0.1:#{port}" - handler = (request, callback) -> callback({url}) - protocol.registerHttpProtocol protocolName, handler, (error) -> - return done(error) if error - $.ajax - url: "#{protocolName}://fake-host" - success: (data) -> - assert.equal data, text - done() - error: (xhr, errorType, error) -> - done(error) - - it 'fails when sending invalid url', (done) -> - handler = (request, callback) -> callback({url: 'url'}) - protocol.registerHttpProtocol protocolName, handler, (error) -> - return done(error) if error - $.ajax - url: "#{protocolName}://fake-host" - success: (data) -> - done('request succeeded but it should not') - error: (xhr, errorType, error) -> - assert.equal errorType, 'error' - done() - - it 'fails when sending unsupported content', (done) -> - handler = (request, callback) -> callback(new Date) - protocol.registerHttpProtocol protocolName, handler, (error) -> - return done(error) if error - $.ajax - url: "#{protocolName}://fake-host" - success: (data) -> - done('request succeeded but it should not') - error: (xhr, errorType, error) -> - assert.equal errorType, 'error' - done() - - describe 'protocol.isProtocolHandled', -> - it 'returns true for file:', (done) -> - protocol.isProtocolHandled 'file', (result) -> - assert.equal result, true - done() - - it 'returns true for http:', (done) -> - protocol.isProtocolHandled 'http', (result) -> - assert.equal result, true - done() - - it 'returns true for https:', (done) -> - protocol.isProtocolHandled 'https', (result) -> - assert.equal result, true - done() - - it 'returns false when scheme is not registred', (done) -> - protocol.isProtocolHandled 'no-exist', (result) -> - assert.equal result, false - done() - - it 'returns true for custom protocol', (done) -> - emptyHandler = (request, callback) -> callback() - protocol.registerStringProtocol protocolName, emptyHandler, (error) -> - assert.equal error, null - protocol.isProtocolHandled protocolName, (result) -> - assert.equal result, true - done() - - it 'returns true for intercepted protocol', (done) -> - emptyHandler = (request, callback) -> callback() - protocol.interceptStringProtocol 'http', emptyHandler, (error) -> - assert.equal error, null - protocol.isProtocolHandled 'http', (result) -> - assert.equal result, true - done() - - describe 'protocol.intercept(Any)Protocol', -> - emptyHandler = (request, callback) -> callback() - - it 'throws error when scheme is already intercepted', (done) -> - protocol.interceptStringProtocol 'http', emptyHandler, (error) -> - assert.equal error, null - protocol.interceptBufferProtocol 'http', emptyHandler, (error) -> - assert.notEqual error, null - done() - - it 'does not crash when handler is called twice', (done) -> - doubleHandler = (request, callback) -> - try - callback(text) - callback() - catch - protocol.interceptStringProtocol 'http', doubleHandler, (error) -> - return done(error) if error - $.ajax - url: 'http://fake-host' - success: (data) -> - assert.equal data, text - done() - error: (xhr, errorType, error) -> - done(error) - - it 'sends error when callback is called with nothing', (done) -> - # Flaky on Travis. - return done() if process.env.TRAVIS is 'true' - - protocol.interceptBufferProtocol 'http', emptyHandler, (error) -> - return done(error) if error - $.ajax - url: 'http://fake-host' - success: (data) -> - done('request succeeded but it should not') - error: (xhr, errorType, error) -> - assert.equal errorType, 'error' - done() - - describe 'protocol.interceptStringProtocol', -> - it 'can intercept http protocol', (done) -> - handler = (request, callback) -> callback(text) - protocol.interceptStringProtocol 'http', handler, (error) -> - return done(error) if error - $.ajax - url: 'http://fake-host' - success: (data) -> - assert.equal data, text - done() - error: (xhr, errorType, error) -> - done(error) - - it 'can set content-type', (done) -> - handler = (request, callback) -> - callback({mimeType: 'application/json', data: '{"value": 1}'}) - protocol.interceptStringProtocol 'http', handler, (error) -> - return done(error) if error - $.ajax - url: 'http://fake-host' - success: (data) -> - assert.equal typeof(data), 'object' - assert.equal data.value, 1 - done() - error: (xhr, errorType, error) -> - done(error) - - it 'can receive post data', (done) -> - handler = (request, callback) -> - uploadData = request.uploadData[0].bytes.toString() - callback({data: uploadData}) - protocol.interceptStringProtocol 'http', handler, (error) -> - return done(error) if error - $.ajax - url: "http://fake-host" - type: "POST" - data: postData - success: (data) -> - assert.deepEqual qs.parse(data), postData - done() - error: (xhr, errorType, error) -> - done(error) - - describe 'protocol.interceptBufferProtocol', -> - it 'can intercept http protocol', (done) -> - handler = (request, callback) -> callback(new Buffer(text)) - protocol.interceptBufferProtocol 'http', handler, (error) -> - return done(error) if error - $.ajax - url: 'http://fake-host' - success: (data) -> - assert.equal data, text - done() - error: (xhr, errorType, error) -> - done(error) - - it 'can receive post data', (done) -> - handler = (request, callback) -> - uploadData = request.uploadData[0].bytes - callback(uploadData) - protocol.interceptBufferProtocol 'http', handler, (error) -> - return done(error) if error - $.ajax - url: "http://fake-host" - type: "POST" - data: postData - success: (data) -> - assert.equal data, $.param postData - done() - error: (xhr, errorType, error) -> - done(error) - - describe 'protocol.interceptHttpProtocol', -> - it 'can send POST request', (done) -> - server = http.createServer (req, res) -> - body = '' - req.on 'data', (chunk) -> - body += chunk - req.on 'end', -> - res.end body - server.close() - server.listen 0, '127.0.0.1', -> - {port} = server.address() - url = "http://127.0.0.1:#{port}" - handler = (request, callback) -> - data = - url: url - method: 'POST' - uploadData: - contentType: 'application/x-www-form-urlencoded' - data: request.uploadData[0].bytes.toString() - session: null - callback(data) - protocol.interceptHttpProtocol 'http', handler, (error) -> - return done(error) if error - $.ajax - url: "http://fake-host" - type: "POST" - data: postData - success: (data) -> - assert.deepEqual qs.parse(data), postData - done() - error: (xhr, errorType, error) -> - done(error) - - describe 'protocol.uninterceptProtocol', -> - it 'returns error when scheme does not exist', (done) -> - protocol.uninterceptProtocol 'not-exist', (error) -> - assert.notEqual error, null - done() - - it 'returns error when scheme is not intercepted', (done) -> - protocol.uninterceptProtocol 'http', (error) -> - assert.notEqual error, null - done() diff --git a/spec/api-protocol-spec.js b/spec/api-protocol-spec.js new file mode 100644 index 000000000000..f0f19ecc5fb7 --- /dev/null +++ b/spec/api-protocol-spec.js @@ -0,0 +1,820 @@ +var assert, http, path, protocol, qs, remote; + +assert = require('assert'); + +http = require('http'); + +path = require('path'); + +qs = require('querystring'); + +remote = require('electron').remote; + +protocol = remote.require('electron').protocol; + +describe('protocol module', function() { + var postData, protocolName, text; + protocolName = 'sp'; + text = 'valar morghulis'; + postData = { + name: 'post test', + type: 'string' + }; + afterEach(function(done) { + return protocol.unregisterProtocol(protocolName, function() { + return protocol.uninterceptProtocol('http', function() { + return done(); + }); + }); + }); + describe('protocol.register(Any)Protocol', function() { + var emptyHandler; + emptyHandler = function(request, callback) { + return callback(); + }; + it('throws error when scheme is already registered', function(done) { + return protocol.registerStringProtocol(protocolName, emptyHandler, function(error) { + assert.equal(error, null); + return protocol.registerBufferProtocol(protocolName, emptyHandler, function(error) { + assert.notEqual(error, null); + return done(); + }); + }); + }); + it('does not crash when handler is called twice', function(done) { + var doubleHandler; + doubleHandler = function(request, callback) { + var error1; + try { + callback(text); + return callback(); + } catch (error1) { + + } + }; + return protocol.registerStringProtocol(protocolName, doubleHandler, function(error) { + if (error) { + return done(error); + } + return $.ajax({ + url: protocolName + "://fake-host", + success: function(data) { + assert.equal(data, text); + return done(); + }, + error: function(xhr, errorType, error) { + return done(error); + } + }); + }); + }); + it('sends error when callback is called with nothing', function(done) { + return protocol.registerBufferProtocol(protocolName, emptyHandler, function(error) { + if (error) { + return done(error); + } + return $.ajax({ + url: protocolName + "://fake-host", + success: function(data) { + return done('request succeeded but it should not'); + }, + error: function(xhr, errorType, error) { + assert.equal(errorType, 'error'); + return done(); + } + }); + }); + }); + return it('does not crash when callback is called in next tick', function(done) { + var handler; + handler = function(request, callback) { + return setImmediate(function() { + return callback(text); + }); + }; + return protocol.registerStringProtocol(protocolName, handler, function(error) { + if (error) { + return done(error); + } + return $.ajax({ + url: protocolName + "://fake-host", + success: function(data) { + assert.equal(data, text); + return done(); + }, + error: function(xhr, errorType, error) { + return done(error); + } + }); + }); + }); + }); + describe('protocol.unregisterProtocol', function() { + return it('returns error when scheme does not exist', function(done) { + return protocol.unregisterProtocol('not-exist', function(error) { + assert.notEqual(error, null); + return done(); + }); + }); + }); + describe('protocol.registerStringProtocol', function() { + it('sends string as response', function(done) { + var handler; + handler = function(request, callback) { + return callback(text); + }; + return protocol.registerStringProtocol(protocolName, handler, function(error) { + if (error) { + return done(error); + } + return $.ajax({ + url: protocolName + "://fake-host", + success: function(data) { + assert.equal(data, text); + return done(); + }, + error: function(xhr, errorType, error) { + return done(error); + } + }); + }); + }); + it('sets Access-Control-Allow-Origin', function(done) { + var handler; + handler = function(request, callback) { + return callback(text); + }; + return protocol.registerStringProtocol(protocolName, handler, function(error) { + if (error) { + return done(error); + } + return $.ajax({ + url: protocolName + "://fake-host", + success: function(data, status, request) { + assert.equal(data, text); + assert.equal(request.getResponseHeader('Access-Control-Allow-Origin'), '*'); + return done(); + }, + error: function(xhr, errorType, error) { + return done(error); + } + }); + }); + }); + it('sends object as response', function(done) { + var handler; + handler = function(request, callback) { + return callback({ + data: text, + mimeType: 'text/html' + }); + }; + return protocol.registerStringProtocol(protocolName, handler, function(error) { + if (error) { + return done(error); + } + return $.ajax({ + url: protocolName + "://fake-host", + success: function(data, statux, request) { + assert.equal(data, text); + return done(); + }, + error: function(xhr, errorType, error) { + return done(error); + } + }); + }); + }); + return it('fails when sending object other than string', function(done) { + var handler; + handler = function(request, callback) { + return callback(new Date); + }; + return protocol.registerBufferProtocol(protocolName, handler, function(error) { + if (error) { + return done(error); + } + return $.ajax({ + url: protocolName + "://fake-host", + success: function(data) { + return done('request succeeded but it should not'); + }, + error: function(xhr, errorType, error) { + assert.equal(errorType, 'error'); + return done(); + } + }); + }); + }); + }); + describe('protocol.registerBufferProtocol', function() { + var buffer; + buffer = new Buffer(text); + it('sends Buffer as response', function(done) { + var handler; + handler = function(request, callback) { + return callback(buffer); + }; + return protocol.registerBufferProtocol(protocolName, handler, function(error) { + if (error) { + return done(error); + } + return $.ajax({ + url: protocolName + "://fake-host", + success: function(data) { + assert.equal(data, text); + return done(); + }, + error: function(xhr, errorType, error) { + return done(error); + } + }); + }); + }); + it('sets Access-Control-Allow-Origin', function(done) { + var handler; + handler = function(request, callback) { + return callback(buffer); + }; + return protocol.registerBufferProtocol(protocolName, handler, function(error) { + if (error) { + return done(error); + } + return $.ajax({ + url: protocolName + "://fake-host", + success: function(data, status, request) { + assert.equal(data, text); + assert.equal(request.getResponseHeader('Access-Control-Allow-Origin'), '*'); + return done(); + }, + error: function(xhr, errorType, error) { + return done(error); + } + }); + }); + }); + it('sends object as response', function(done) { + var handler; + handler = function(request, callback) { + return callback({ + data: buffer, + mimeType: 'text/html' + }); + }; + return protocol.registerBufferProtocol(protocolName, handler, function(error) { + if (error) { + return done(error); + } + return $.ajax({ + url: protocolName + "://fake-host", + success: function(data, statux, request) { + assert.equal(data, text); + return done(); + }, + error: function(xhr, errorType, error) { + return done(error); + } + }); + }); + }); + return it('fails when sending string', function(done) { + var handler; + handler = function(request, callback) { + return callback(text); + }; + return protocol.registerBufferProtocol(protocolName, handler, function(error) { + if (error) { + return done(error); + } + return $.ajax({ + url: protocolName + "://fake-host", + success: function(data) { + return done('request succeeded but it should not'); + }, + error: function(xhr, errorType, error) { + assert.equal(errorType, 'error'); + return done(); + } + }); + }); + }); + }); + describe('protocol.registerFileProtocol', function() { + var fileContent, filePath, normalContent, normalPath; + filePath = path.join(__dirname, 'fixtures', 'asar', 'a.asar', 'file1'); + fileContent = require('fs').readFileSync(filePath); + normalPath = path.join(__dirname, 'fixtures', 'pages', 'a.html'); + normalContent = require('fs').readFileSync(normalPath); + it('sends file path as response', function(done) { + var handler; + handler = function(request, callback) { + return callback(filePath); + }; + return protocol.registerFileProtocol(protocolName, handler, function(error) { + if (error) { + return done(error); + } + return $.ajax({ + url: protocolName + "://fake-host", + success: function(data) { + assert.equal(data, String(fileContent)); + return done(); + }, + error: function(xhr, errorType, error) { + return done(error); + } + }); + }); + }); + it('sets Access-Control-Allow-Origin', function(done) { + var handler; + handler = function(request, callback) { + return callback(filePath); + }; + return protocol.registerFileProtocol(protocolName, handler, function(error) { + if (error) { + return done(error); + } + return $.ajax({ + url: protocolName + "://fake-host", + success: function(data, status, request) { + assert.equal(data, String(fileContent)); + assert.equal(request.getResponseHeader('Access-Control-Allow-Origin'), '*'); + return done(); + }, + error: function(xhr, errorType, error) { + return done(error); + } + }); + }); + }); + it('sends object as response', function(done) { + var handler; + handler = function(request, callback) { + return callback({ + path: filePath + }); + }; + return protocol.registerFileProtocol(protocolName, handler, function(error) { + if (error) { + return done(error); + } + return $.ajax({ + url: protocolName + "://fake-host", + success: function(data, statux, request) { + assert.equal(data, String(fileContent)); + return done(); + }, + error: function(xhr, errorType, error) { + return done(error); + } + }); + }); + }); + it('can send normal file', function(done) { + var handler; + handler = function(request, callback) { + return callback(normalPath); + }; + return protocol.registerFileProtocol(protocolName, handler, function(error) { + if (error) { + return done(error); + } + return $.ajax({ + url: protocolName + "://fake-host", + success: function(data) { + assert.equal(data, String(normalContent)); + return done(); + }, + error: function(xhr, errorType, error) { + return done(error); + } + }); + }); + }); + it('fails when sending unexist-file', function(done) { + var fakeFilePath, handler; + fakeFilePath = path.join(__dirname, 'fixtures', 'asar', 'a.asar', 'not-exist'); + handler = function(request, callback) { + return callback(fakeFilePath); + }; + return protocol.registerBufferProtocol(protocolName, handler, function(error) { + if (error) { + return done(error); + } + return $.ajax({ + url: protocolName + "://fake-host", + success: function(data) { + return done('request succeeded but it should not'); + }, + error: function(xhr, errorType, error) { + assert.equal(errorType, 'error'); + return done(); + } + }); + }); + }); + return it('fails when sending unsupported content', function(done) { + var handler; + handler = function(request, callback) { + return callback(new Date); + }; + return protocol.registerBufferProtocol(protocolName, handler, function(error) { + if (error) { + return done(error); + } + return $.ajax({ + url: protocolName + "://fake-host", + success: function(data) { + return done('request succeeded but it should not'); + }, + error: function(xhr, errorType, error) { + assert.equal(errorType, 'error'); + return done(); + } + }); + }); + }); + }); + describe('protocol.registerHttpProtocol', function() { + it('sends url as response', function(done) { + var server; + server = http.createServer(function(req, res) { + assert.notEqual(req.headers.accept, ''); + res.end(text); + return server.close(); + }); + return server.listen(0, '127.0.0.1', function() { + var handler, port, url; + port = server.address().port; + url = "http://127.0.0.1:" + port; + handler = function(request, callback) { + return callback({ + url: url + }); + }; + return protocol.registerHttpProtocol(protocolName, handler, function(error) { + if (error) { + return done(error); + } + return $.ajax({ + url: protocolName + "://fake-host", + success: function(data) { + assert.equal(data, text); + return done(); + }, + error: function(xhr, errorType, error) { + return done(error); + } + }); + }); + }); + }); + it('fails when sending invalid url', function(done) { + var handler; + handler = function(request, callback) { + return callback({ + url: 'url' + }); + }; + return protocol.registerHttpProtocol(protocolName, handler, function(error) { + if (error) { + return done(error); + } + return $.ajax({ + url: protocolName + "://fake-host", + success: function(data) { + return done('request succeeded but it should not'); + }, + error: function(xhr, errorType, error) { + assert.equal(errorType, 'error'); + return done(); + } + }); + }); + }); + return it('fails when sending unsupported content', function(done) { + var handler; + handler = function(request, callback) { + return callback(new Date); + }; + return protocol.registerHttpProtocol(protocolName, handler, function(error) { + if (error) { + return done(error); + } + return $.ajax({ + url: protocolName + "://fake-host", + success: function(data) { + return done('request succeeded but it should not'); + }, + error: function(xhr, errorType, error) { + assert.equal(errorType, 'error'); + return done(); + } + }); + }); + }); + }); + describe('protocol.isProtocolHandled', function() { + it('returns true for file:', function(done) { + return protocol.isProtocolHandled('file', function(result) { + assert.equal(result, true); + return done(); + }); + }); + it('returns true for http:', function(done) { + return protocol.isProtocolHandled('http', function(result) { + assert.equal(result, true); + return done(); + }); + }); + it('returns true for https:', function(done) { + return protocol.isProtocolHandled('https', function(result) { + assert.equal(result, true); + return done(); + }); + }); + it('returns false when scheme is not registred', function(done) { + return protocol.isProtocolHandled('no-exist', function(result) { + assert.equal(result, false); + return done(); + }); + }); + it('returns true for custom protocol', function(done) { + var emptyHandler; + emptyHandler = function(request, callback) { + return callback(); + }; + return protocol.registerStringProtocol(protocolName, emptyHandler, function(error) { + assert.equal(error, null); + return protocol.isProtocolHandled(protocolName, function(result) { + assert.equal(result, true); + return done(); + }); + }); + }); + return it('returns true for intercepted protocol', function(done) { + var emptyHandler; + emptyHandler = function(request, callback) { + return callback(); + }; + return protocol.interceptStringProtocol('http', emptyHandler, function(error) { + assert.equal(error, null); + return protocol.isProtocolHandled('http', function(result) { + assert.equal(result, true); + return done(); + }); + }); + }); + }); + describe('protocol.intercept(Any)Protocol', function() { + var emptyHandler; + emptyHandler = function(request, callback) { + return callback(); + }; + it('throws error when scheme is already intercepted', function(done) { + return protocol.interceptStringProtocol('http', emptyHandler, function(error) { + assert.equal(error, null); + return protocol.interceptBufferProtocol('http', emptyHandler, function(error) { + assert.notEqual(error, null); + return done(); + }); + }); + }); + it('does not crash when handler is called twice', function(done) { + var doubleHandler; + doubleHandler = function(request, callback) { + var error1; + try { + callback(text); + return callback(); + } catch (error1) { + + } + }; + return protocol.interceptStringProtocol('http', doubleHandler, function(error) { + if (error) { + return done(error); + } + return $.ajax({ + url: 'http://fake-host', + success: function(data) { + assert.equal(data, text); + return done(); + }, + error: function(xhr, errorType, error) { + return done(error); + } + }); + }); + }); + return it('sends error when callback is called with nothing', function(done) { + if (process.env.TRAVIS === 'true') { + return done(); + } + return protocol.interceptBufferProtocol('http', emptyHandler, function(error) { + if (error) { + return done(error); + } + return $.ajax({ + url: 'http://fake-host', + success: function(data) { + return done('request succeeded but it should not'); + }, + error: function(xhr, errorType, error) { + assert.equal(errorType, 'error'); + return done(); + } + }); + }); + }); + }); + describe('protocol.interceptStringProtocol', function() { + it('can intercept http protocol', function(done) { + var handler; + handler = function(request, callback) { + return callback(text); + }; + return protocol.interceptStringProtocol('http', handler, function(error) { + if (error) { + return done(error); + } + return $.ajax({ + url: 'http://fake-host', + success: function(data) { + assert.equal(data, text); + return done(); + }, + error: function(xhr, errorType, error) { + return done(error); + } + }); + }); + }); + it('can set content-type', function(done) { + var handler; + handler = function(request, callback) { + return callback({ + mimeType: 'application/json', + data: '{"value": 1}' + }); + }; + return protocol.interceptStringProtocol('http', handler, function(error) { + if (error) { + return done(error); + } + return $.ajax({ + url: 'http://fake-host', + success: function(data) { + assert.equal(typeof data, 'object'); + assert.equal(data.value, 1); + return done(); + }, + error: function(xhr, errorType, error) { + return done(error); + } + }); + }); + }); + return it('can receive post data', function(done) { + var handler; + handler = function(request, callback) { + var uploadData; + uploadData = request.uploadData[0].bytes.toString(); + return callback({ + data: uploadData + }); + }; + return protocol.interceptStringProtocol('http', handler, function(error) { + if (error) { + return done(error); + } + return $.ajax({ + url: "http://fake-host", + type: "POST", + data: postData, + success: function(data) { + assert.deepEqual(qs.parse(data), postData); + return done(); + }, + error: function(xhr, errorType, error) { + return done(error); + } + }); + }); + }); + }); + describe('protocol.interceptBufferProtocol', function() { + it('can intercept http protocol', function(done) { + var handler; + handler = function(request, callback) { + return callback(new Buffer(text)); + }; + return protocol.interceptBufferProtocol('http', handler, function(error) { + if (error) { + return done(error); + } + return $.ajax({ + url: 'http://fake-host', + success: function(data) { + assert.equal(data, text); + return done(); + }, + error: function(xhr, errorType, error) { + return done(error); + } + }); + }); + }); + return it('can receive post data', function(done) { + var handler; + handler = function(request, callback) { + var uploadData; + uploadData = request.uploadData[0].bytes; + return callback(uploadData); + }; + return protocol.interceptBufferProtocol('http', handler, function(error) { + if (error) { + return done(error); + } + return $.ajax({ + url: "http://fake-host", + type: "POST", + data: postData, + success: function(data) { + assert.equal(data, $.param(postData)); + return done(); + }, + error: function(xhr, errorType, error) { + return done(error); + } + }); + }); + }); + }); + describe('protocol.interceptHttpProtocol', function() { + return it('can send POST request', function(done) { + var server; + server = http.createServer(function(req, res) { + var body; + body = ''; + req.on('data', function(chunk) { + return body += chunk; + }); + req.on('end', function() { + return res.end(body); + }); + return server.close(); + }); + return server.listen(0, '127.0.0.1', function() { + var handler, port, url; + port = server.address().port; + url = "http://127.0.0.1:" + port; + handler = function(request, callback) { + var data; + data = { + url: url, + method: 'POST', + uploadData: { + contentType: 'application/x-www-form-urlencoded', + data: request.uploadData[0].bytes.toString() + }, + session: null + }; + return callback(data); + }; + return protocol.interceptHttpProtocol('http', handler, function(error) { + if (error) { + return done(error); + } + return $.ajax({ + url: "http://fake-host", + type: "POST", + data: postData, + success: function(data) { + assert.deepEqual(qs.parse(data), postData); + return done(); + }, + error: function(xhr, errorType, error) { + return done(error); + } + }); + }); + }); + }); + }); + return describe('protocol.uninterceptProtocol', function() { + it('returns error when scheme does not exist', function(done) { + return protocol.uninterceptProtocol('not-exist', function(error) { + assert.notEqual(error, null); + return done(); + }); + }); + return it('returns error when scheme is not intercepted', function(done) { + return protocol.uninterceptProtocol('http', function(error) { + assert.notEqual(error, null); + return done(); + }); + }); + }); +}); diff --git a/spec/api-screen-spec.coffee b/spec/api-screen-spec.coffee deleted file mode 100644 index 59ef49631cbc..000000000000 --- a/spec/api-screen-spec.coffee +++ /dev/null @@ -1,17 +0,0 @@ -assert = require 'assert' - -{screen} = require 'electron' - -describe 'screen module', -> - describe 'screen.getCursorScreenPoint()', -> - it 'returns a point object', -> - point = screen.getCursorScreenPoint() - assert.equal typeof(point.x), 'number' - assert.equal typeof(point.y), 'number' - - describe 'screen.getPrimaryDisplay()', -> - it 'returns a display object', -> - display = screen.getPrimaryDisplay() - assert.equal typeof(display.scaleFactor), 'number' - assert display.size.width > 0 - assert display.size.height > 0 diff --git a/spec/api-screen-spec.js b/spec/api-screen-spec.js new file mode 100644 index 000000000000..b393d4b99ee8 --- /dev/null +++ b/spec/api-screen-spec.js @@ -0,0 +1,25 @@ +var assert, screen; + +assert = require('assert'); + +screen = require('electron').screen; + +describe('screen module', function() { + describe('screen.getCursorScreenPoint()', function() { + return it('returns a point object', function() { + var point; + point = screen.getCursorScreenPoint(); + assert.equal(typeof point.x, 'number'); + return assert.equal(typeof point.y, 'number'); + }); + }); + return describe('screen.getPrimaryDisplay()', function() { + return it('returns a display object', function() { + var display; + display = screen.getPrimaryDisplay(); + assert.equal(typeof display.scaleFactor, 'number'); + assert(display.size.width > 0); + return assert(display.size.height > 0); + }); + }); +}); diff --git a/spec/api-session-spec.coffee b/spec/api-session-spec.coffee deleted file mode 100644 index 03c62c1f1db6..000000000000 --- a/spec/api-session-spec.coffee +++ /dev/null @@ -1,138 +0,0 @@ -assert = require 'assert' -http = require 'http' -path = require 'path' -fs = require 'fs' - -{ipcRenderer, remote} = require 'electron' -{app, ipcMain, session, BrowserWindow} = remote - -describe 'session module', -> - @timeout 10000 - fixtures = path.resolve __dirname, 'fixtures' - w = null - url = "http://127.0.0.1" - - beforeEach -> w = new BrowserWindow(show: false, width: 400, height: 400) - afterEach -> w.destroy() - - it 'should get cookies', (done) -> - server = http.createServer (req, res) -> - res.setHeader('Set-Cookie', ['0=0']) - res.end('finished') - server.close() - - server.listen 0, '127.0.0.1', -> - {port} = server.address() - w.loadURL "#{url}:#{port}" - w.webContents.on 'did-finish-load', -> - w.webContents.session.cookies.get {url: url}, (error, list) -> - return done(error) if error - for cookie in list when cookie.name is '0' - if cookie.value is '0' - return done() - else - return done("cookie value is #{cookie.value} while expecting 0") - done('Can not find cookie') - - it 'should over-write the existent cookie', (done) -> - session.defaultSession.cookies.set {url: url, name: '1', value: '1'}, (error) -> - return done(error) if error - session.defaultSession.cookies.get {url: url}, (error, list) -> - return done(error) if error - for cookie in list when cookie.name is '1' - if cookie.value is '1' - return done() - else - return done("cookie value is #{cookie.value} while expecting 1") - done('Can not find cookie') - - it 'should remove cookies', (done) -> - session.defaultSession.cookies.set {url: url, name: '2', value: '2'}, (error) -> - return done(error) if error - session.defaultSession.cookies.remove url, '2', -> - session.defaultSession.cookies.get {url: url}, (error, list) -> - return done(error) if error - for cookie in list when cookie.name is '2' - return done('Cookie not deleted') - done() - - describe 'session.clearStorageData(options)', -> - fixtures = path.resolve __dirname, 'fixtures' - it 'clears localstorage data', (done) -> - ipcMain.on 'count', (event, count) -> - ipcMain.removeAllListeners 'count' - assert not count - done() - w.loadURL 'file://' + path.join(fixtures, 'api', 'localstorage.html') - w.webContents.on 'did-finish-load', -> - options = - origin: "file://", - storages: ['localstorage'], - quotas: ['persistent'], - w.webContents.session.clearStorageData options, -> - w.webContents.send 'getcount' - - describe 'DownloadItem', -> - # A 5 MB mock pdf. - mockPDF = new Buffer 1024 * 1024 * 5 - contentDisposition = 'inline; filename="mock.pdf"' - downloadFilePath = path.join fixtures, 'mock.pdf' - downloadServer = http.createServer (req, res) -> - res.writeHead 200, { - 'Content-Length': mockPDF.length, - 'Content-Type': 'application/pdf', - 'Content-Disposition': contentDisposition - } - res.end mockPDF - downloadServer.close() - - assertDownload = (event, state, url, mimeType, receivedBytes, totalBytes, disposition, filename, port) -> - assert.equal state, 'completed' - assert.equal filename, 'mock.pdf' - assert.equal url, "http://127.0.0.1:#{port}/" - assert.equal mimeType, 'application/pdf' - assert.equal receivedBytes, mockPDF.length - assert.equal totalBytes, mockPDF.length - assert.equal disposition, contentDisposition - assert fs.existsSync downloadFilePath - fs.unlinkSync downloadFilePath - - it 'can download using BrowserWindow.loadURL', (done) -> - downloadServer.listen 0, '127.0.0.1', -> - {port} = downloadServer.address() - ipcRenderer.sendSync 'set-download-option', false - w.loadURL "#{url}:#{port}" - ipcRenderer.once 'download-done', (event, state, url, mimeType, receivedBytes, totalBytes, disposition, filename) -> - assertDownload event, state, url, mimeType, receivedBytes, totalBytes, disposition, filename, port - done() - - it 'can download using WebView.downloadURL', (done) -> - downloadServer.listen 0, '127.0.0.1', -> - {port} = downloadServer.address() - ipcRenderer.sendSync 'set-download-option', false - - webview = new WebView - webview.src = "file://#{fixtures}/api/blank.html" - webview.addEventListener 'did-finish-load', -> - webview.downloadURL "#{url}:#{port}/" - - ipcRenderer.once 'download-done', (event, state, url, mimeType, receivedBytes, totalBytes, disposition, filename) -> - assertDownload event, state, url, mimeType, receivedBytes, totalBytes, disposition, filename, port - document.body.removeChild(webview) - done() - - document.body.appendChild webview - - it 'can cancel download', (done) -> - downloadServer.listen 0, '127.0.0.1', -> - {port} = downloadServer.address() - ipcRenderer.sendSync 'set-download-option', true - w.loadURL "#{url}:#{port}/" - ipcRenderer.once 'download-done', (event, state, url, mimeType, receivedBytes, totalBytes, disposition, filename) -> - assert.equal state, 'cancelled' - assert.equal filename, 'mock.pdf' - assert.equal mimeType, 'application/pdf' - assert.equal receivedBytes, 0 - assert.equal totalBytes, mockPDF.length - assert.equal disposition, contentDisposition - done() diff --git a/spec/api-session-spec.js b/spec/api-session-spec.js new file mode 100644 index 000000000000..403e715cf5c6 --- /dev/null +++ b/spec/api-session-spec.js @@ -0,0 +1,218 @@ +var BrowserWindow, app, assert, fs, http, ipcMain, ipcRenderer, path, ref, remote, session; + +assert = require('assert'); + +http = require('http'); + +path = require('path'); + +fs = require('fs'); + +ref = require('electron'), ipcRenderer = ref.ipcRenderer, remote = ref.remote; + +app = remote.app, ipcMain = remote.ipcMain, session = remote.session, BrowserWindow = remote.BrowserWindow; + +describe('session module', function() { + var fixtures, url, w; + this.timeout(10000); + fixtures = path.resolve(__dirname, 'fixtures'); + w = null; + url = "http://127.0.0.1"; + beforeEach(function() { + return w = new BrowserWindow({ + show: false, + width: 400, + height: 400 + }); + }); + afterEach(function() { + return w.destroy(); + }); + it('should get cookies', function(done) { + var server; + server = http.createServer(function(req, res) { + res.setHeader('Set-Cookie', ['0=0']); + res.end('finished'); + return server.close(); + }); + return server.listen(0, '127.0.0.1', function() { + var port; + port = server.address().port; + w.loadURL(url + ":" + port); + return w.webContents.on('did-finish-load', function() { + return w.webContents.session.cookies.get({ + url: url + }, function(error, list) { + var cookie, i, len; + if (error) { + return done(error); + } + for (i = 0, len = list.length; i < len; i++) { + cookie = list[i]; + if (cookie.name === '0') { + if (cookie.value === '0') { + return done(); + } else { + return done("cookie value is " + cookie.value + " while expecting 0"); + } + } + } + return done('Can not find cookie'); + }); + }); + }); + }); + it('should over-write the existent cookie', function(done) { + return session.defaultSession.cookies.set({ + url: url, + name: '1', + value: '1' + }, function(error) { + if (error) { + return done(error); + } + return session.defaultSession.cookies.get({ + url: url + }, function(error, list) { + var cookie, i, len; + if (error) { + return done(error); + } + for (i = 0, len = list.length; i < len; i++) { + cookie = list[i]; + if (cookie.name === '1') { + if (cookie.value === '1') { + return done(); + } else { + return done("cookie value is " + cookie.value + " while expecting 1"); + } + } + } + return done('Can not find cookie'); + }); + }); + }); + it('should remove cookies', function(done) { + return session.defaultSession.cookies.set({ + url: url, + name: '2', + value: '2' + }, function(error) { + if (error) { + return done(error); + } + return session.defaultSession.cookies.remove(url, '2', function() { + return session.defaultSession.cookies.get({ + url: url + }, function(error, list) { + var cookie, i, len; + if (error) { + return done(error); + } + for (i = 0, len = list.length; i < len; i++) { + cookie = list[i]; + if (cookie.name === '2') { + return done('Cookie not deleted'); + } + } + return done(); + }); + }); + }); + }); + describe('session.clearStorageData(options)', function() { + fixtures = path.resolve(__dirname, 'fixtures'); + return it('clears localstorage data', function(done) { + ipcMain.on('count', function(event, count) { + ipcMain.removeAllListeners('count'); + assert(!count); + return done(); + }); + w.loadURL('file://' + path.join(fixtures, 'api', 'localstorage.html')); + return w.webContents.on('did-finish-load', function() { + var options; + options = { + origin: "file://", + storages: ['localstorage'], + quotas: ['persistent'] + }; + return w.webContents.session.clearStorageData(options, function() { + return w.webContents.send('getcount'); + }); + }); + }); + }); + return describe('DownloadItem', function() { + var assertDownload, contentDisposition, downloadFilePath, downloadServer, mockPDF; + mockPDF = new Buffer(1024 * 1024 * 5); + contentDisposition = 'inline; filename="mock.pdf"'; + downloadFilePath = path.join(fixtures, 'mock.pdf'); + downloadServer = http.createServer(function(req, res) { + res.writeHead(200, { + 'Content-Length': mockPDF.length, + 'Content-Type': 'application/pdf', + 'Content-Disposition': contentDisposition + }); + res.end(mockPDF); + return downloadServer.close(); + }); + assertDownload = function(event, state, url, mimeType, receivedBytes, totalBytes, disposition, filename, port) { + assert.equal(state, 'completed'); + assert.equal(filename, 'mock.pdf'); + assert.equal(url, "http://127.0.0.1:" + port + "/"); + assert.equal(mimeType, 'application/pdf'); + assert.equal(receivedBytes, mockPDF.length); + assert.equal(totalBytes, mockPDF.length); + assert.equal(disposition, contentDisposition); + assert(fs.existsSync(downloadFilePath)); + return fs.unlinkSync(downloadFilePath); + }; + it('can download using BrowserWindow.loadURL', function(done) { + return downloadServer.listen(0, '127.0.0.1', function() { + var port; + port = downloadServer.address().port; + ipcRenderer.sendSync('set-download-option', false); + w.loadURL(url + ":" + port); + return ipcRenderer.once('download-done', function(event, state, url, mimeType, receivedBytes, totalBytes, disposition, filename) { + assertDownload(event, state, url, mimeType, receivedBytes, totalBytes, disposition, filename, port); + return done(); + }); + }); + }); + it('can download using WebView.downloadURL', function(done) { + return downloadServer.listen(0, '127.0.0.1', function() { + var port, webview; + port = downloadServer.address().port; + ipcRenderer.sendSync('set-download-option', false); + webview = new WebView; + webview.src = "file://" + fixtures + "/api/blank.html"; + webview.addEventListener('did-finish-load', function() { + return webview.downloadURL(url + ":" + port + "/"); + }); + ipcRenderer.once('download-done', function(event, state, url, mimeType, receivedBytes, totalBytes, disposition, filename) { + assertDownload(event, state, url, mimeType, receivedBytes, totalBytes, disposition, filename, port); + document.body.removeChild(webview); + return done(); + }); + return document.body.appendChild(webview); + }); + }); + return it('can cancel download', function(done) { + return downloadServer.listen(0, '127.0.0.1', function() { + var port; + port = downloadServer.address().port; + ipcRenderer.sendSync('set-download-option', true); + w.loadURL(url + ":" + port + "/"); + return ipcRenderer.once('download-done', function(event, state, url, mimeType, receivedBytes, totalBytes, disposition, filename) { + assert.equal(state, 'cancelled'); + assert.equal(filename, 'mock.pdf'); + assert.equal(mimeType, 'application/pdf'); + assert.equal(receivedBytes, 0); + assert.equal(totalBytes, mockPDF.length); + assert.equal(disposition, contentDisposition); + return done(); + }); + }); + }); + }); +}); diff --git a/spec/api-web-frame-spec.coffee b/spec/api-web-frame-spec.coffee deleted file mode 100644 index cece329084e1..000000000000 --- a/spec/api-web-frame-spec.coffee +++ /dev/null @@ -1,18 +0,0 @@ -assert = require 'assert' -path = require 'path' - -{webFrame} = require 'electron' - -describe 'webFrame module', -> - fixtures = path.resolve __dirname, 'fixtures' - - describe 'webFrame.registerURLSchemeAsPrivileged', -> - it 'supports fetch api', (done) -> - webFrame.registerURLSchemeAsPrivileged 'file' - url = "file://#{fixtures}/assets/logo.png" - - fetch(url).then((response) -> - assert response.ok - done() - ).catch (err) -> - done('unexpected error : ' + err) diff --git a/spec/api-web-frame-spec.js b/spec/api-web-frame-spec.js new file mode 100644 index 000000000000..3d287a6acba9 --- /dev/null +++ b/spec/api-web-frame-spec.js @@ -0,0 +1,25 @@ +var assert, path, webFrame; + +assert = require('assert'); + +path = require('path'); + +webFrame = require('electron').webFrame; + +describe('webFrame module', function() { + var fixtures; + fixtures = path.resolve(__dirname, 'fixtures'); + return describe('webFrame.registerURLSchemeAsPrivileged', function() { + return it('supports fetch api', function(done) { + var url; + webFrame.registerURLSchemeAsPrivileged('file'); + url = "file://" + fixtures + "/assets/logo.png"; + return fetch(url).then(function(response) { + assert(response.ok); + return done(); + })["catch"](function(err) { + return done('unexpected error : ' + err); + }); + }); + }); +}); diff --git a/spec/api-web-request-spec.coffee b/spec/api-web-request-spec.coffee deleted file mode 100644 index 5c78ef1d30a6..000000000000 --- a/spec/api-web-request-spec.coffee +++ /dev/null @@ -1,243 +0,0 @@ -assert = require 'assert' -http = require 'http' - -{remote} = require 'electron' -{session} = remote - -describe 'webRequest module', -> - ses = session.defaultSession - server = http.createServer (req, res) -> - res.setHeader('Custom', ['Header']) - content = req.url - if req.headers.accept is '*/*;test/header' - content += 'header/received' - res.end content - defaultURL = null - - before (done) -> - server.listen 0, '127.0.0.1', -> - {port} = server.address() - defaultURL = "http://127.0.0.1:#{port}/" - done() - after -> - server.close() - - describe 'webRequest.onBeforeRequest', -> - afterEach -> - ses.webRequest.onBeforeRequest null - - it 'can cancel the request', (done) -> - ses.webRequest.onBeforeRequest (details, callback) -> - callback(cancel: true) - $.ajax - url: defaultURL - success: (data) -> done('unexpected success') - error: (xhr, errorType, error) -> done() - - it 'can filter URLs', (done) -> - filter = urls: ["#{defaultURL}filter/*"] - ses.webRequest.onBeforeRequest filter, (details, callback) -> - callback(cancel: true) - $.ajax - url: "#{defaultURL}nofilter/test" - success: (data) -> - assert.equal data, '/nofilter/test' - $.ajax - url: "#{defaultURL}filter/test" - success: (data) -> done('unexpected success') - error: (xhr, errorType, error) -> done() - error: (xhr, errorType, error) -> done(errorType) - - it 'receives details object', (done) -> - ses.webRequest.onBeforeRequest (details, callback) -> - assert.equal typeof details.id, 'number' - assert.equal typeof details.timestamp, 'number' - assert.equal details.url, defaultURL - assert.equal details.method, 'GET' - assert.equal details.resourceType, 'xhr' - callback({}) - $.ajax - url: defaultURL - success: (data) -> - assert.equal data, '/' - done() - error: (xhr, errorType, error) -> done(errorType) - - it 'can redirect the request', (done) -> - ses.webRequest.onBeforeRequest (details, callback) -> - if details.url is defaultURL - callback(redirectURL: "#{defaultURL}redirect") - else - callback({}) - $.ajax - url: defaultURL - success: (data) -> - assert.equal data, '/redirect' - done() - error: (xhr, errorType, error) -> done(errorType) - - describe 'webRequest.onBeforeSendHeaders', -> - afterEach -> - ses.webRequest.onBeforeSendHeaders null - - it 'receives details object', (done) -> - ses.webRequest.onBeforeSendHeaders (details, callback) -> - assert.equal typeof details.requestHeaders, 'object' - callback({}) - $.ajax - url: defaultURL - success: (data) -> - assert.equal data, '/' - done() - error: (xhr, errorType, error) -> done(errorType) - - it 'can change the request headers', (done) -> - ses.webRequest.onBeforeSendHeaders (details, callback) -> - {requestHeaders} = details - requestHeaders.Accept = '*/*;test/header' - callback({requestHeaders}) - $.ajax - url: defaultURL - success: (data, textStatus, request) -> - assert.equal data, '/header/received' - done() - error: (xhr, errorType, error) -> done(errorType) - - it 'resets the whole headers', (done) -> - requestHeaders = Test: 'header' - ses.webRequest.onBeforeSendHeaders (details, callback) -> - callback({requestHeaders}) - ses.webRequest.onSendHeaders (details) -> - assert.deepEqual details.requestHeaders, requestHeaders - done() - $.ajax - url: defaultURL - error: (xhr, errorType, error) -> done(errorType) - - describe 'webRequest.onSendHeaders', -> - afterEach -> - ses.webRequest.onSendHeaders null - - it 'receives details object', (done) -> - ses.webRequest.onSendHeaders (details) -> - assert.equal typeof details.requestHeaders, 'object' - $.ajax - url: defaultURL - success: (data) -> - assert.equal data, '/' - done() - error: (xhr, errorType, error) -> done(errorType) - - describe 'webRequest.onHeadersReceived', -> - afterEach -> - ses.webRequest.onHeadersReceived null - - it 'receives details object', (done) -> - ses.webRequest.onHeadersReceived (details, callback) -> - assert.equal details.statusLine, 'HTTP/1.1 200 OK' - assert.equal details.statusCode, 200 - assert.equal details.responseHeaders['Custom'], 'Header' - callback({}) - $.ajax - url: defaultURL - success: (data) -> - assert.equal data, '/' - done() - error: (xhr, errorType, error) -> done(errorType) - - it 'can change the response header', (done) -> - ses.webRequest.onHeadersReceived (details, callback) -> - {responseHeaders} = details - responseHeaders['Custom'] = ['Changed'] - callback({responseHeaders}) - $.ajax - url: defaultURL - success: (data, status, xhr) -> - assert.equal xhr.getResponseHeader('Custom'), 'Changed' - assert.equal data, '/' - done() - error: (xhr, errorType, error) -> done(errorType) - - it 'does not change header by default', (done) -> - ses.webRequest.onHeadersReceived (details, callback) -> - callback({}) - $.ajax - url: defaultURL - success: (data, status, xhr) -> - assert.equal xhr.getResponseHeader('Custom'), 'Header' - assert.equal data, '/' - done() - error: (xhr, errorType, error) -> done(errorType) - - describe 'webRequest.onResponseStarted', -> - afterEach -> - ses.webRequest.onResponseStarted null - - it 'receives details object', (done) -> - ses.webRequest.onResponseStarted (details) -> - assert.equal typeof details.fromCache, 'boolean' - assert.equal details.statusLine, 'HTTP/1.1 200 OK' - assert.equal details.statusCode, 200 - assert.equal details.responseHeaders['Custom'], 'Header' - $.ajax - url: defaultURL - success: (data, status, xhr) -> - assert.equal xhr.getResponseHeader('Custom'), 'Header' - assert.equal data, '/' - done() - error: (xhr, errorType, error) -> done(errorType) - - describe 'webRequest.onBeforeRedirect', -> - afterEach -> - ses.webRequest.onBeforeRedirect null - ses.webRequest.onBeforeRequest null - - it 'receives details object', (done) -> - redirectURL = "#{defaultURL}redirect" - ses.webRequest.onBeforeRequest (details, callback) -> - if details.url is defaultURL - callback({redirectURL}) - else - callback({}) - ses.webRequest.onBeforeRedirect (details) -> - assert.equal typeof details.fromCache, 'boolean' - assert.equal details.statusLine, 'HTTP/1.1 307 Internal Redirect' - assert.equal details.statusCode, 307 - assert.equal details.redirectURL, redirectURL - $.ajax - url: defaultURL - success: (data, status, xhr) -> - assert.equal data, '/redirect' - done() - error: (xhr, errorType, error) -> done(errorType) - - describe 'webRequest.onCompleted', -> - afterEach -> - ses.webRequest.onCompleted null - - it 'receives details object', (done) -> - ses.webRequest.onCompleted (details) -> - assert.equal typeof details.fromCache, 'boolean' - assert.equal details.statusLine, 'HTTP/1.1 200 OK' - assert.equal details.statusCode, 200 - $.ajax - url: defaultURL - success: (data, status, xhr) -> - assert.equal data, '/' - done() - error: (xhr, errorType, error) -> done(errorType) - - describe 'webRequest.onErrorOccurred', -> - afterEach -> - ses.webRequest.onErrorOccurred null - ses.webRequest.onBeforeRequest null - - it 'receives details object', (done) -> - ses.webRequest.onBeforeRequest (details, callback) -> - callback(cancel: true) - ses.webRequest.onErrorOccurred (details) -> - assert.equal details.error, 'net::ERR_BLOCKED_BY_CLIENT' - done() - $.ajax - url: defaultURL - success: (data) -> done('unexpected success') diff --git a/spec/api-web-request-spec.js b/spec/api-web-request-spec.js new file mode 100644 index 000000000000..854b28391213 --- /dev/null +++ b/spec/api-web-request-spec.js @@ -0,0 +1,372 @@ +var assert, http, remote, session; + +assert = require('assert'); + +http = require('http'); + +remote = require('electron').remote; + +session = remote.session; + +describe('webRequest module', function() { + var defaultURL, server, ses; + ses = session.defaultSession; + server = http.createServer(function(req, res) { + var content; + res.setHeader('Custom', ['Header']); + content = req.url; + if (req.headers.accept === '*/*;test/header') { + content += 'header/received'; + } + return res.end(content); + }); + defaultURL = null; + before(function(done) { + return server.listen(0, '127.0.0.1', function() { + var port; + port = server.address().port; + defaultURL = "http://127.0.0.1:" + port + "/"; + return done(); + }); + }); + after(function() { + return server.close(); + }); + describe('webRequest.onBeforeRequest', function() { + afterEach(function() { + return ses.webRequest.onBeforeRequest(null); + }); + it('can cancel the request', function(done) { + ses.webRequest.onBeforeRequest(function(details, callback) { + return callback({ + cancel: true + }); + }); + return $.ajax({ + url: defaultURL, + success: function(data) { + return done('unexpected success'); + }, + error: function(xhr, errorType, error) { + return done(); + } + }); + }); + it('can filter URLs', function(done) { + var filter; + filter = { + urls: [defaultURL + "filter/*"] + }; + ses.webRequest.onBeforeRequest(filter, function(details, callback) { + return callback({ + cancel: true + }); + }); + return $.ajax({ + url: defaultURL + "nofilter/test", + success: function(data) { + assert.equal(data, '/nofilter/test'); + return $.ajax({ + url: defaultURL + "filter/test", + success: function(data) { + return done('unexpected success'); + }, + error: function(xhr, errorType, error) { + return done(); + } + }); + }, + error: function(xhr, errorType, error) { + return done(errorType); + } + }); + }); + it('receives details object', function(done) { + ses.webRequest.onBeforeRequest(function(details, callback) { + assert.equal(typeof details.id, 'number'); + assert.equal(typeof details.timestamp, 'number'); + assert.equal(details.url, defaultURL); + assert.equal(details.method, 'GET'); + assert.equal(details.resourceType, 'xhr'); + return callback({}); + }); + return $.ajax({ + url: defaultURL, + success: function(data) { + assert.equal(data, '/'); + return done(); + }, + error: function(xhr, errorType, error) { + return done(errorType); + } + }); + }); + return it('can redirect the request', function(done) { + ses.webRequest.onBeforeRequest(function(details, callback) { + if (details.url === defaultURL) { + return callback({ + redirectURL: defaultURL + "redirect" + }); + } else { + return callback({}); + } + }); + return $.ajax({ + url: defaultURL, + success: function(data) { + assert.equal(data, '/redirect'); + return done(); + }, + error: function(xhr, errorType, error) { + return done(errorType); + } + }); + }); + }); + describe('webRequest.onBeforeSendHeaders', function() { + afterEach(function() { + return ses.webRequest.onBeforeSendHeaders(null); + }); + it('receives details object', function(done) { + ses.webRequest.onBeforeSendHeaders(function(details, callback) { + assert.equal(typeof details.requestHeaders, 'object'); + return callback({}); + }); + return $.ajax({ + url: defaultURL, + success: function(data) { + assert.equal(data, '/'); + return done(); + }, + error: function(xhr, errorType, error) { + return done(errorType); + } + }); + }); + it('can change the request headers', function(done) { + ses.webRequest.onBeforeSendHeaders(function(details, callback) { + var requestHeaders; + requestHeaders = details.requestHeaders; + requestHeaders.Accept = '*/*;test/header'; + return callback({ + requestHeaders: requestHeaders + }); + }); + return $.ajax({ + url: defaultURL, + success: function(data, textStatus, request) { + assert.equal(data, '/header/received'); + return done(); + }, + error: function(xhr, errorType, error) { + return done(errorType); + } + }); + }); + return it('resets the whole headers', function(done) { + var requestHeaders; + requestHeaders = { + Test: 'header' + }; + ses.webRequest.onBeforeSendHeaders(function(details, callback) { + return callback({ + requestHeaders: requestHeaders + }); + }); + ses.webRequest.onSendHeaders(function(details) { + assert.deepEqual(details.requestHeaders, requestHeaders); + return done(); + }); + return $.ajax({ + url: defaultURL, + error: function(xhr, errorType, error) { + return done(errorType); + } + }); + }); + }); + describe('webRequest.onSendHeaders', function() { + afterEach(function() { + return ses.webRequest.onSendHeaders(null); + }); + return it('receives details object', function(done) { + ses.webRequest.onSendHeaders(function(details) { + return assert.equal(typeof details.requestHeaders, 'object'); + }); + return $.ajax({ + url: defaultURL, + success: function(data) { + assert.equal(data, '/'); + return done(); + }, + error: function(xhr, errorType, error) { + return done(errorType); + } + }); + }); + }); + describe('webRequest.onHeadersReceived', function() { + afterEach(function() { + return ses.webRequest.onHeadersReceived(null); + }); + it('receives details object', function(done) { + ses.webRequest.onHeadersReceived(function(details, callback) { + assert.equal(details.statusLine, 'HTTP/1.1 200 OK'); + assert.equal(details.statusCode, 200); + assert.equal(details.responseHeaders['Custom'], 'Header'); + return callback({}); + }); + return $.ajax({ + url: defaultURL, + success: function(data) { + assert.equal(data, '/'); + return done(); + }, + error: function(xhr, errorType, error) { + return done(errorType); + } + }); + }); + it('can change the response header', function(done) { + ses.webRequest.onHeadersReceived(function(details, callback) { + var responseHeaders; + responseHeaders = details.responseHeaders; + responseHeaders['Custom'] = ['Changed']; + return callback({ + responseHeaders: responseHeaders + }); + }); + return $.ajax({ + url: defaultURL, + success: function(data, status, xhr) { + assert.equal(xhr.getResponseHeader('Custom'), 'Changed'); + assert.equal(data, '/'); + return done(); + }, + error: function(xhr, errorType, error) { + return done(errorType); + } + }); + }); + return it('does not change header by default', function(done) { + ses.webRequest.onHeadersReceived(function(details, callback) { + return callback({}); + }); + return $.ajax({ + url: defaultURL, + success: function(data, status, xhr) { + assert.equal(xhr.getResponseHeader('Custom'), 'Header'); + assert.equal(data, '/'); + return done(); + }, + error: function(xhr, errorType, error) { + return done(errorType); + } + }); + }); + }); + describe('webRequest.onResponseStarted', function() { + afterEach(function() { + return ses.webRequest.onResponseStarted(null); + }); + return it('receives details object', function(done) { + ses.webRequest.onResponseStarted(function(details) { + assert.equal(typeof details.fromCache, 'boolean'); + assert.equal(details.statusLine, 'HTTP/1.1 200 OK'); + assert.equal(details.statusCode, 200); + return assert.equal(details.responseHeaders['Custom'], 'Header'); + }); + return $.ajax({ + url: defaultURL, + success: function(data, status, xhr) { + assert.equal(xhr.getResponseHeader('Custom'), 'Header'); + assert.equal(data, '/'); + return done(); + }, + error: function(xhr, errorType, error) { + return done(errorType); + } + }); + }); + }); + describe('webRequest.onBeforeRedirect', function() { + afterEach(function() { + ses.webRequest.onBeforeRedirect(null); + return ses.webRequest.onBeforeRequest(null); + }); + return it('receives details object', function(done) { + var redirectURL; + redirectURL = defaultURL + "redirect"; + ses.webRequest.onBeforeRequest(function(details, callback) { + if (details.url === defaultURL) { + return callback({ + redirectURL: redirectURL + }); + } else { + return callback({}); + } + }); + ses.webRequest.onBeforeRedirect(function(details) { + assert.equal(typeof details.fromCache, 'boolean'); + assert.equal(details.statusLine, 'HTTP/1.1 307 Internal Redirect'); + assert.equal(details.statusCode, 307); + return assert.equal(details.redirectURL, redirectURL); + }); + return $.ajax({ + url: defaultURL, + success: function(data, status, xhr) { + assert.equal(data, '/redirect'); + return done(); + }, + error: function(xhr, errorType, error) { + return done(errorType); + } + }); + }); + }); + describe('webRequest.onCompleted', function() { + afterEach(function() { + return ses.webRequest.onCompleted(null); + }); + return it('receives details object', function(done) { + ses.webRequest.onCompleted(function(details) { + assert.equal(typeof details.fromCache, 'boolean'); + assert.equal(details.statusLine, 'HTTP/1.1 200 OK'); + return assert.equal(details.statusCode, 200); + }); + return $.ajax({ + url: defaultURL, + success: function(data, status, xhr) { + assert.equal(data, '/'); + return done(); + }, + error: function(xhr, errorType, error) { + return done(errorType); + } + }); + }); + }); + return describe('webRequest.onErrorOccurred', function() { + afterEach(function() { + ses.webRequest.onErrorOccurred(null); + return ses.webRequest.onBeforeRequest(null); + }); + return it('receives details object', function(done) { + ses.webRequest.onBeforeRequest(function(details, callback) { + return callback({ + cancel: true + }); + }); + ses.webRequest.onErrorOccurred(function(details) { + assert.equal(details.error, 'net::ERR_BLOCKED_BY_CLIENT'); + return done(); + }); + return $.ajax({ + url: defaultURL, + success: function(data) { + return done('unexpected success'); + } + }); + }); + }); +}); diff --git a/spec/asar-spec.coffee b/spec/asar-spec.coffee deleted file mode 100644 index 7642283cea4d..000000000000 --- a/spec/asar-spec.coffee +++ /dev/null @@ -1,578 +0,0 @@ -assert = require 'assert' -child_process = require 'child_process' -fs = require 'fs' -path = require 'path' - -{nativeImage, remote} = require 'electron' -{ipcMain, BrowserWindow} = remote.require 'electron' - -describe 'asar package', -> - fixtures = path.join __dirname, 'fixtures' - - describe 'node api', -> - describe 'fs.readFileSync', -> - it 'does not leak fd', -> - for i in [1..10000] - fs.readFileSync(path.join(process.resourcesPath, 'atom.asar', 'renderer', 'api', 'lib', 'ipc.js')) - - it 'reads a normal file', -> - file1 = path.join fixtures, 'asar', 'a.asar', 'file1' - assert.equal fs.readFileSync(file1).toString().trim(), 'file1' - file2 = path.join fixtures, 'asar', 'a.asar', 'file2' - assert.equal fs.readFileSync(file2).toString().trim(), 'file2' - file3 = path.join fixtures, 'asar', 'a.asar', 'file3' - assert.equal fs.readFileSync(file3).toString().trim(), 'file3' - - it 'reads from a empty file', -> - file = path.join fixtures, 'asar', 'empty.asar', 'file1' - buffer = fs.readFileSync(file) - assert.equal buffer.length, 0 - assert.equal buffer.toString(), '' - - it 'reads a linked file', -> - p = path.join fixtures, 'asar', 'a.asar', 'link1' - assert.equal fs.readFileSync(p).toString().trim(), 'file1' - - it 'reads a file from linked directory', -> - p = path.join fixtures, 'asar', 'a.asar', 'link2', 'file1' - assert.equal fs.readFileSync(p).toString().trim(), 'file1' - p = path.join fixtures, 'asar', 'a.asar', 'link2', 'link2', 'file1' - assert.equal fs.readFileSync(p).toString().trim(), 'file1' - - it 'throws ENOENT error when can not find file', -> - p = path.join fixtures, 'asar', 'a.asar', 'not-exist' - throws = -> fs.readFileSync p - assert.throws throws, /ENOENT/ - - it 'passes ENOENT error to callback when can not find file', -> - p = path.join fixtures, 'asar', 'a.asar', 'not-exist' - async = false - fs.readFile p, (e) -> - assert async - assert /ENOENT/.test e - async = true - - it 'reads a normal file with unpacked files', -> - p = path.join fixtures, 'asar', 'unpack.asar', 'a.txt' - assert.equal fs.readFileSync(p).toString().trim(), 'a' - - describe 'fs.readFile', -> - it 'reads a normal file', (done) -> - p = path.join fixtures, 'asar', 'a.asar', 'file1' - fs.readFile p, (err, content) -> - assert.equal err, null - assert.equal String(content).trim(), 'file1' - done() - - it 'reads from a empty file', (done) -> - p = path.join fixtures, 'asar', 'empty.asar', 'file1' - fs.readFile p, (err, content) -> - assert.equal err, null - assert.equal String(content), '' - done() - - it 'reads a linked file', (done) -> - p = path.join fixtures, 'asar', 'a.asar', 'link1' - fs.readFile p, (err, content) -> - assert.equal err, null - assert.equal String(content).trim(), 'file1' - done() - - it 'reads a file from linked directory', (done) -> - p = path.join fixtures, 'asar', 'a.asar', 'link2', 'link2', 'file1' - fs.readFile p, (err, content) -> - assert.equal err, null - assert.equal String(content).trim(), 'file1' - done() - - it 'throws ENOENT error when can not find file', (done) -> - p = path.join fixtures, 'asar', 'a.asar', 'not-exist' - fs.readFile p, (err, content) -> - assert.equal err.code, 'ENOENT' - done() - - describe 'fs.lstatSync', -> - it 'handles path with trailing slash correctly', -> - p = path.join fixtures, 'asar', 'a.asar', 'link2', 'link2', 'file1' - fs.lstatSync p - fs.lstatSync p + '/' - - it 'returns information of root', -> - p = path.join fixtures, 'asar', 'a.asar' - stats = fs.lstatSync p - assert.equal stats.isFile(), false - assert.equal stats.isDirectory(), true - assert.equal stats.isSymbolicLink(), false - assert.equal stats.size, 0 - - it 'returns information of a normal file', -> - for file in ['file1', 'file2', 'file3', path.join('dir1', 'file1'), path.join('link2', 'file1')] - p = path.join fixtures, 'asar', 'a.asar', file - stats = fs.lstatSync p - assert.equal stats.isFile(), true - assert.equal stats.isDirectory(), false - assert.equal stats.isSymbolicLink(), false - assert.equal stats.size, 6 - - it 'returns information of a normal directory', -> - for file in ['dir1', 'dir2', 'dir3'] - p = path.join fixtures, 'asar', 'a.asar', file - stats = fs.lstatSync p - assert.equal stats.isFile(), false - assert.equal stats.isDirectory(), true - assert.equal stats.isSymbolicLink(), false - assert.equal stats.size, 0 - - it 'returns information of a linked file', -> - for file in ['link1', path.join('dir1', 'link1'), path.join('link2', 'link2')] - p = path.join fixtures, 'asar', 'a.asar', file - stats = fs.lstatSync p - assert.equal stats.isFile(), false - assert.equal stats.isDirectory(), false - assert.equal stats.isSymbolicLink(), true - assert.equal stats.size, 0 - - it 'returns information of a linked directory', -> - for file in ['link2', path.join('dir1', 'link2'), path.join('link2', 'link2')] - p = path.join fixtures, 'asar', 'a.asar', file - stats = fs.lstatSync p - assert.equal stats.isFile(), false - assert.equal stats.isDirectory(), false - assert.equal stats.isSymbolicLink(), true - assert.equal stats.size, 0 - - it 'throws ENOENT error when can not find file', -> - for file in ['file4', 'file5', path.join('dir1', 'file4')] - p = path.join fixtures, 'asar', 'a.asar', file - throws = -> fs.lstatSync p - assert.throws throws, /ENOENT/ - - describe 'fs.lstat', -> - it 'handles path with trailing slash correctly', (done) -> - p = path.join fixtures, 'asar', 'a.asar', 'link2', 'link2', 'file1' - fs.lstat p + '/', done - - it 'returns information of root', (done) -> - p = path.join fixtures, 'asar', 'a.asar' - stats = fs.lstat p, (err, stats) -> - assert.equal err, null - assert.equal stats.isFile(), false - assert.equal stats.isDirectory(), true - assert.equal stats.isSymbolicLink(), false - assert.equal stats.size, 0 - done() - - it 'returns information of a normal file', (done) -> - p = path.join fixtures, 'asar', 'a.asar', 'link2', 'file1' - stats = fs.lstat p, (err, stats) -> - assert.equal err, null - assert.equal stats.isFile(), true - assert.equal stats.isDirectory(), false - assert.equal stats.isSymbolicLink(), false - assert.equal stats.size, 6 - done() - - it 'returns information of a normal directory', (done) -> - p = path.join fixtures, 'asar', 'a.asar', 'dir1' - stats = fs.lstat p, (err, stats) -> - assert.equal err, null - assert.equal stats.isFile(), false - assert.equal stats.isDirectory(), true - assert.equal stats.isSymbolicLink(), false - assert.equal stats.size, 0 - done() - - it 'returns information of a linked file', (done) -> - p = path.join fixtures, 'asar', 'a.asar', 'link2', 'link1' - stats = fs.lstat p, (err, stats) -> - assert.equal err, null - assert.equal stats.isFile(), false - assert.equal stats.isDirectory(), false - assert.equal stats.isSymbolicLink(), true - assert.equal stats.size, 0 - done() - - it 'returns information of a linked directory', (done) -> - p = path.join fixtures, 'asar', 'a.asar', 'link2', 'link2' - stats = fs.lstat p, (err, stats) -> - assert.equal err, null - assert.equal stats.isFile(), false - assert.equal stats.isDirectory(), false - assert.equal stats.isSymbolicLink(), true - assert.equal stats.size, 0 - done() - - it 'throws ENOENT error when can not find file', (done) -> - p = path.join fixtures, 'asar', 'a.asar', 'file4' - stats = fs.lstat p, (err, stats) -> - assert.equal err.code, 'ENOENT' - done() - - describe 'fs.realpathSync', -> - it 'returns real path root', -> - parent = fs.realpathSync path.join(fixtures, 'asar') - p = 'a.asar' - r = fs.realpathSync path.join(parent, p) - assert.equal r, path.join(parent, p) - - it 'returns real path of a normal file', -> - parent = fs.realpathSync path.join(fixtures, 'asar') - p = path.join 'a.asar', 'file1' - r = fs.realpathSync path.join(parent, p) - assert.equal r, path.join(parent, p) - - it 'returns real path of a normal directory', -> - parent = fs.realpathSync path.join(fixtures, 'asar') - p = path.join 'a.asar', 'dir1' - r = fs.realpathSync path.join(parent, p) - assert.equal r, path.join(parent, p) - - it 'returns real path of a linked file', -> - parent = fs.realpathSync path.join(fixtures, 'asar') - p = path.join 'a.asar', 'link2', 'link1' - r = fs.realpathSync path.join(parent, p) - assert.equal r, path.join(parent, 'a.asar', 'file1') - - it 'returns real path of a linked directory', -> - parent = fs.realpathSync path.join(fixtures, 'asar') - p = path.join 'a.asar', 'link2', 'link2' - r = fs.realpathSync path.join(parent, p) - assert.equal r, path.join(parent, 'a.asar', 'dir1') - - it 'throws ENOENT error when can not find file', -> - parent = fs.realpathSync path.join(fixtures, 'asar') - p = path.join 'a.asar', 'not-exist' - throws = -> fs.realpathSync path.join(parent, p) - assert.throws throws, /ENOENT/ - - describe 'fs.realpath', -> - it 'returns real path root', (done) -> - parent = fs.realpathSync path.join(fixtures, 'asar') - p = 'a.asar' - fs.realpath path.join(parent, p), (err, r) -> - assert.equal err, null - assert.equal r, path.join(parent, p) - done() - - it 'returns real path of a normal file', (done) -> - parent = fs.realpathSync path.join(fixtures, 'asar') - p = path.join 'a.asar', 'file1' - fs.realpath path.join(parent, p), (err, r) -> - assert.equal err, null - assert.equal r, path.join(parent, p) - done() - - it 'returns real path of a normal directory', (done) -> - parent = fs.realpathSync path.join(fixtures, 'asar') - p = path.join 'a.asar', 'dir1' - fs.realpath path.join(parent, p), (err, r) -> - assert.equal err, null - assert.equal r, path.join(parent, p) - done() - - it 'returns real path of a linked file', (done) -> - parent = fs.realpathSync path.join(fixtures, 'asar') - p = path.join 'a.asar', 'link2', 'link1' - fs.realpath path.join(parent, p), (err, r) -> - assert.equal err, null - assert.equal r, path.join(parent, 'a.asar', 'file1') - done() - - it 'returns real path of a linked directory', (done) -> - parent = fs.realpathSync path.join(fixtures, 'asar') - p = path.join 'a.asar', 'link2', 'link2' - fs.realpath path.join(parent, p), (err, r) -> - assert.equal err, null - assert.equal r, path.join(parent, 'a.asar', 'dir1') - done() - - it 'throws ENOENT error when can not find file', (done) -> - parent = fs.realpathSync path.join(fixtures, 'asar') - p = path.join 'a.asar', 'not-exist' - fs.realpath path.join(parent, p), (err, stats) -> - assert.equal err.code, 'ENOENT' - done() - - describe 'fs.readdirSync', -> - it 'reads dirs from root', -> - p = path.join fixtures, 'asar', 'a.asar' - dirs = fs.readdirSync p - assert.deepEqual dirs, ['dir1', 'dir2', 'dir3', 'file1', 'file2', 'file3', 'link1', 'link2', 'ping.js'] - - it 'reads dirs from a normal dir', -> - p = path.join fixtures, 'asar', 'a.asar', 'dir1' - dirs = fs.readdirSync p - assert.deepEqual dirs, ['file1', 'file2', 'file3', 'link1', 'link2'] - - it 'reads dirs from a linked dir', -> - p = path.join fixtures, 'asar', 'a.asar', 'link2', 'link2' - dirs = fs.readdirSync p - assert.deepEqual dirs, ['file1', 'file2', 'file3', 'link1', 'link2'] - - it 'throws ENOENT error when can not find file', -> - p = path.join fixtures, 'asar', 'a.asar', 'not-exist' - throws = -> fs.readdirSync p - assert.throws throws, /ENOENT/ - - describe 'fs.readdir', -> - it 'reads dirs from root', (done) -> - p = path.join fixtures, 'asar', 'a.asar' - dirs = fs.readdir p, (err, dirs) -> - assert.equal err, null - assert.deepEqual dirs, ['dir1', 'dir2', 'dir3', 'file1', 'file2', 'file3', 'link1', 'link2', 'ping.js'] - done() - - it 'reads dirs from a normal dir', (done) -> - p = path.join fixtures, 'asar', 'a.asar', 'dir1' - dirs = fs.readdir p, (err, dirs) -> - assert.equal err, null - assert.deepEqual dirs, ['file1', 'file2', 'file3', 'link1', 'link2'] - done() - - it 'reads dirs from a linked dir', (done) -> - p = path.join fixtures, 'asar', 'a.asar', 'link2', 'link2' - dirs = fs.readdir p, (err, dirs) -> - assert.equal err, null - assert.deepEqual dirs, ['file1', 'file2', 'file3', 'link1', 'link2'] - done() - - it 'throws ENOENT error when can not find file', (done) -> - p = path.join fixtures, 'asar', 'a.asar', 'not-exist' - fs.readdir p, (err, stats) -> - assert.equal err.code, 'ENOENT' - done() - - describe 'fs.openSync', -> - it 'opens a normal/linked/under-linked-directory file', -> - for file in ['file1', 'link1', path.join('link2', 'file1')] - p = path.join fixtures, 'asar', 'a.asar', file - fd = fs.openSync p, 'r' - buffer = new Buffer(6) - fs.readSync fd, buffer, 0, 6, 0 - assert.equal String(buffer).trim(), 'file1' - fs.closeSync fd - - it 'throws ENOENT error when can not find file', -> - p = path.join fixtures, 'asar', 'a.asar', 'not-exist' - throws = -> fs.openSync p - assert.throws throws, /ENOENT/ - - describe 'fs.open', -> - it 'opens a normal file', (done) -> - p = path.join fixtures, 'asar', 'a.asar', 'file1' - fs.open p, 'r', (err, fd) -> - assert.equal err, null - buffer = new Buffer(6) - fs.read fd, buffer, 0, 6, 0, (err) -> - assert.equal err, null - assert.equal String(buffer).trim(), 'file1' - fs.close fd, done - - it 'throws ENOENT error when can not find file', (done) -> - p = path.join fixtures, 'asar', 'a.asar', 'not-exist' - fs.open p, 'r', (err, stats) -> - assert.equal err.code, 'ENOENT' - done() - - describe 'fs.mkdir', -> - it 'throws error when calling inside asar archive', (done) -> - p = path.join fixtures, 'asar', 'a.asar', 'not-exist' - fs.mkdir p, (err) -> - assert.equal err.code, 'ENOTDIR' - done() - - describe 'fs.mkdirSync', -> - it 'throws error when calling inside asar archive', -> - p = path.join fixtures, 'asar', 'a.asar', 'not-exist' - assert.throws (-> fs.mkdirSync p), new RegExp('ENOTDIR') - - describe 'child_process.fork', -> - child_process = require 'child_process' - - it 'opens a normal js file', (done) -> - child = child_process.fork path.join(fixtures, 'asar', 'a.asar', 'ping.js') - child.on 'message', (msg) -> - assert.equal msg, 'message' - done() - child.send 'message' - - it 'supports asar in the forked js', (done) -> - file = path.join fixtures, 'asar', 'a.asar', 'file1' - child = child_process.fork path.join(fixtures, 'module', 'asar.js') - child.on 'message', (content) -> - assert.equal content, fs.readFileSync(file).toString() - done() - child.send file - - describe 'child_process.execFile', -> - return unless process.platform is 'darwin' - - {execFile, execFileSync} = require 'child_process' - echo = path.join fixtures, 'asar', 'echo.asar', 'echo' - - it 'executes binaries', (done) -> - child = execFile echo, ['test'], (error, stdout) -> - assert.equal error, null - assert.equal stdout, 'test\n' - done() - - # execFileSync makes the test flaky after a refresh. - xit 'execFileSync executes binaries', -> - output = execFileSync echo, ['test'] - assert.equal String(output), 'test\n' - - describe 'internalModuleReadFile', -> - internalModuleReadFile = process.binding('fs').internalModuleReadFile - - it 'read a normal file', -> - file1 = path.join fixtures, 'asar', 'a.asar', 'file1' - assert.equal internalModuleReadFile(file1).toString().trim(), 'file1' - file2 = path.join fixtures, 'asar', 'a.asar', 'file2' - assert.equal internalModuleReadFile(file2).toString().trim(), 'file2' - file3 = path.join fixtures, 'asar', 'a.asar', 'file3' - assert.equal internalModuleReadFile(file3).toString().trim(), 'file3' - - it 'reads a normal file with unpacked files', -> - p = path.join fixtures, 'asar', 'unpack.asar', 'a.txt' - assert.equal internalModuleReadFile(p).toString().trim(), 'a' - - describe 'process.noAsar', -> - errorName = if process.platform is 'win32' then 'ENOENT' else 'ENOTDIR' - - beforeEach -> - process.noAsar = true - afterEach -> - process.noAsar = false - - it 'disables asar support in sync API', -> - file = path.join fixtures, 'asar', 'a.asar', 'file1' - dir = path.join fixtures, 'asar', 'a.asar', 'dir1' - assert.throws (-> fs.readFileSync file), new RegExp(errorName) - assert.throws (-> fs.lstatSync file), new RegExp(errorName) - assert.throws (-> fs.realpathSync file), new RegExp(errorName) - assert.throws (-> fs.readdirSync dir), new RegExp(errorName) - - it 'disables asar support in async API', (done) -> - file = path.join fixtures, 'asar', 'a.asar', 'file1' - dir = path.join fixtures, 'asar', 'a.asar', 'dir1' - fs.readFile file, (error) -> - assert.equal error.code, errorName - fs.lstat file, (error) -> - assert.equal error.code, errorName - fs.realpath file, (error) -> - assert.equal error.code, errorName - fs.readdir dir, (error) -> - assert.equal error.code, errorName - done() - - it 'treats *.asar as normal file', -> - originalFs = require 'original-fs' - asar = path.join fixtures, 'asar', 'a.asar' - content1 = fs.readFileSync asar - content2 = originalFs.readFileSync asar - assert.equal content1.compare(content2), 0 - assert.throws (-> fs.readdirSync asar), /ENOTDIR/ - - describe 'asar protocol', -> - url = require 'url' - - it 'can request a file in package', (done) -> - p = path.resolve fixtures, 'asar', 'a.asar', 'file1' - $.get "file://#{p}", (data) -> - assert.equal data.trim(), 'file1' - done() - - it 'can request a file in package with unpacked files', (done) -> - p = path.resolve fixtures, 'asar', 'unpack.asar', 'a.txt' - $.get "file://#{p}", (data) -> - assert.equal data.trim(), 'a' - done() - - it 'can request a linked file in package', (done) -> - p = path.resolve fixtures, 'asar', 'a.asar', 'link2', 'link1' - $.get "file://#{p}", (data) -> - assert.equal data.trim(), 'file1' - done() - - it 'can request a file in filesystem', (done) -> - p = path.resolve fixtures, 'asar', 'file' - $.get "file://#{p}", (data) -> - assert.equal data.trim(), 'file' - done() - - it 'gets 404 when file is not found', (done) -> - p = path.resolve fixtures, 'asar', 'a.asar', 'no-exist' - $.ajax - url: "file://#{p}" - error: (err) -> - assert.equal err.status, 404 - done() - - it 'sets __dirname correctly', (done) -> - after -> - w.destroy() - ipcMain.removeAllListeners 'dirname' - - w = new BrowserWindow(show: false, width: 400, height: 400) - p = path.resolve fixtures, 'asar', 'web.asar', 'index.html' - u = url.format protocol: 'file', slashed: true, pathname: p - ipcMain.once 'dirname', (event, dirname) -> - assert.equal dirname, path.dirname(p) - done() - w.loadURL u - - it 'loads script tag in html', (done) -> - after -> - w.destroy() - ipcMain.removeAllListeners 'ping' - - w = new BrowserWindow(show: false, width: 400, height: 400) - p = path.resolve fixtures, 'asar', 'script.asar', 'index.html' - u = url.format protocol: 'file', slashed: true, pathname: p - w.loadURL u - ipcMain.once 'ping', (event, message) -> - assert.equal message, 'pong' - done() - - describe 'original-fs module', -> - originalFs = require 'original-fs' - - it 'treats .asar as file', -> - file = path.join fixtures, 'asar', 'a.asar' - stats = originalFs.statSync file - assert stats.isFile() - - it 'is available in forked scripts', (done) -> - child = child_process.fork path.join(fixtures, 'module', 'original-fs.js') - child.on 'message', (msg) -> - assert.equal msg, 'object' - done() - child.send 'message' - - describe 'graceful-fs module', -> - gfs = require 'graceful-fs' - - it 'recognize asar archvies', -> - p = path.join fixtures, 'asar', 'a.asar', 'link1' - assert.equal gfs.readFileSync(p).toString().trim(), 'file1' - - it 'does not touch global fs object', -> - assert.notEqual fs.readdir, gfs.readdir - - describe 'mkdirp module', -> - mkdirp = require 'mkdirp' - - it 'throws error when calling inside asar archive', -> - p = path.join fixtures, 'asar', 'a.asar', 'not-exist' - assert.throws (-> mkdirp.sync p), new RegExp('ENOTDIR') - - describe 'native-image', -> - it 'reads image from asar archive', -> - p = path.join fixtures, 'asar', 'logo.asar', 'logo.png' - logo = nativeImage.createFromPath p - assert.deepEqual logo.getSize(), {width: 55, height: 55} - - it 'reads image from asar archive with unpacked files', -> - p = path.join fixtures, 'asar', 'unpack.asar', 'atom.png' - logo = nativeImage.createFromPath p - assert.deepEqual logo.getSize(), {width: 1024, height: 1024} diff --git a/spec/asar-spec.js b/spec/asar-spec.js new file mode 100644 index 000000000000..6ccc4b46a011 --- /dev/null +++ b/spec/asar-spec.js @@ -0,0 +1,805 @@ +var BrowserWindow, assert, child_process, fs, ipcMain, nativeImage, path, ref, ref1, remote; + +assert = require('assert'); + +child_process = require('child_process'); + +fs = require('fs'); + +path = require('path'); + +ref = require('electron'), nativeImage = ref.nativeImage, remote = ref.remote; + +ref1 = remote.require('electron'), ipcMain = ref1.ipcMain, BrowserWindow = ref1.BrowserWindow; + +describe('asar package', function() { + var fixtures; + fixtures = path.join(__dirname, 'fixtures'); + describe('node api', function() { + describe('fs.readFileSync', function() { + it('does not leak fd', function() { + var i, j, results; + results = []; + for (i = j = 1; j <= 10000; i = ++j) { + results.push(fs.readFileSync(path.join(process.resourcesPath, 'atom.asar', 'renderer', 'api', 'lib', 'ipc.js'))); + } + return results; + }); + it('reads a normal file', function() { + var file1, file2, file3; + file1 = path.join(fixtures, 'asar', 'a.asar', 'file1'); + assert.equal(fs.readFileSync(file1).toString().trim(), 'file1'); + file2 = path.join(fixtures, 'asar', 'a.asar', 'file2'); + assert.equal(fs.readFileSync(file2).toString().trim(), 'file2'); + file3 = path.join(fixtures, 'asar', 'a.asar', 'file3'); + return assert.equal(fs.readFileSync(file3).toString().trim(), 'file3'); + }); + it('reads from a empty file', function() { + var buffer, file; + file = path.join(fixtures, 'asar', 'empty.asar', 'file1'); + buffer = fs.readFileSync(file); + assert.equal(buffer.length, 0); + return assert.equal(buffer.toString(), ''); + }); + it('reads a linked file', function() { + var p; + p = path.join(fixtures, 'asar', 'a.asar', 'link1'); + return assert.equal(fs.readFileSync(p).toString().trim(), 'file1'); + }); + it('reads a file from linked directory', function() { + var p; + p = path.join(fixtures, 'asar', 'a.asar', 'link2', 'file1'); + assert.equal(fs.readFileSync(p).toString().trim(), 'file1'); + p = path.join(fixtures, 'asar', 'a.asar', 'link2', 'link2', 'file1'); + return assert.equal(fs.readFileSync(p).toString().trim(), 'file1'); + }); + it('throws ENOENT error when can not find file', function() { + var p, throws; + p = path.join(fixtures, 'asar', 'a.asar', 'not-exist'); + throws = function() { + return fs.readFileSync(p); + }; + return assert.throws(throws, /ENOENT/); + }); + it('passes ENOENT error to callback when can not find file', function() { + var async, p; + p = path.join(fixtures, 'asar', 'a.asar', 'not-exist'); + async = false; + fs.readFile(p, function(e) { + assert(async); + return assert(/ENOENT/.test(e)); + }); + return async = true; + }); + return it('reads a normal file with unpacked files', function() { + var p; + p = path.join(fixtures, 'asar', 'unpack.asar', 'a.txt'); + return assert.equal(fs.readFileSync(p).toString().trim(), 'a'); + }); + }); + describe('fs.readFile', function() { + it('reads a normal file', function(done) { + var p; + p = path.join(fixtures, 'asar', 'a.asar', 'file1'); + return fs.readFile(p, function(err, content) { + assert.equal(err, null); + assert.equal(String(content).trim(), 'file1'); + return done(); + }); + }); + it('reads from a empty file', function(done) { + var p; + p = path.join(fixtures, 'asar', 'empty.asar', 'file1'); + return fs.readFile(p, function(err, content) { + assert.equal(err, null); + assert.equal(String(content), ''); + return done(); + }); + }); + it('reads a linked file', function(done) { + var p; + p = path.join(fixtures, 'asar', 'a.asar', 'link1'); + return fs.readFile(p, function(err, content) { + assert.equal(err, null); + assert.equal(String(content).trim(), 'file1'); + return done(); + }); + }); + it('reads a file from linked directory', function(done) { + var p; + p = path.join(fixtures, 'asar', 'a.asar', 'link2', 'link2', 'file1'); + return fs.readFile(p, function(err, content) { + assert.equal(err, null); + assert.equal(String(content).trim(), 'file1'); + return done(); + }); + }); + return it('throws ENOENT error when can not find file', function(done) { + var p; + p = path.join(fixtures, 'asar', 'a.asar', 'not-exist'); + return fs.readFile(p, function(err, content) { + assert.equal(err.code, 'ENOENT'); + return done(); + }); + }); + }); + describe('fs.lstatSync', function() { + it('handles path with trailing slash correctly', function() { + var p; + p = path.join(fixtures, 'asar', 'a.asar', 'link2', 'link2', 'file1'); + fs.lstatSync(p); + return fs.lstatSync(p + '/'); + }); + it('returns information of root', function() { + var p, stats; + p = path.join(fixtures, 'asar', 'a.asar'); + stats = fs.lstatSync(p); + assert.equal(stats.isFile(), false); + assert.equal(stats.isDirectory(), true); + assert.equal(stats.isSymbolicLink(), false); + return assert.equal(stats.size, 0); + }); + it('returns information of a normal file', function() { + var file, j, len, p, ref2, results, stats; + ref2 = ['file1', 'file2', 'file3', path.join('dir1', 'file1'), path.join('link2', 'file1')]; + results = []; + for (j = 0, len = ref2.length; j < len; j++) { + file = ref2[j]; + p = path.join(fixtures, 'asar', 'a.asar', file); + stats = fs.lstatSync(p); + assert.equal(stats.isFile(), true); + assert.equal(stats.isDirectory(), false); + assert.equal(stats.isSymbolicLink(), false); + results.push(assert.equal(stats.size, 6)); + } + return results; + }); + it('returns information of a normal directory', function() { + var file, j, len, p, ref2, results, stats; + ref2 = ['dir1', 'dir2', 'dir3']; + results = []; + for (j = 0, len = ref2.length; j < len; j++) { + file = ref2[j]; + p = path.join(fixtures, 'asar', 'a.asar', file); + stats = fs.lstatSync(p); + assert.equal(stats.isFile(), false); + assert.equal(stats.isDirectory(), true); + assert.equal(stats.isSymbolicLink(), false); + results.push(assert.equal(stats.size, 0)); + } + return results; + }); + it('returns information of a linked file', function() { + var file, j, len, p, ref2, results, stats; + ref2 = ['link1', path.join('dir1', 'link1'), path.join('link2', 'link2')]; + results = []; + for (j = 0, len = ref2.length; j < len; j++) { + file = ref2[j]; + p = path.join(fixtures, 'asar', 'a.asar', file); + stats = fs.lstatSync(p); + assert.equal(stats.isFile(), false); + assert.equal(stats.isDirectory(), false); + assert.equal(stats.isSymbolicLink(), true); + results.push(assert.equal(stats.size, 0)); + } + return results; + }); + it('returns information of a linked directory', function() { + var file, j, len, p, ref2, results, stats; + ref2 = ['link2', path.join('dir1', 'link2'), path.join('link2', 'link2')]; + results = []; + for (j = 0, len = ref2.length; j < len; j++) { + file = ref2[j]; + p = path.join(fixtures, 'asar', 'a.asar', file); + stats = fs.lstatSync(p); + assert.equal(stats.isFile(), false); + assert.equal(stats.isDirectory(), false); + assert.equal(stats.isSymbolicLink(), true); + results.push(assert.equal(stats.size, 0)); + } + return results; + }); + return it('throws ENOENT error when can not find file', function() { + var file, j, len, p, ref2, results, throws; + ref2 = ['file4', 'file5', path.join('dir1', 'file4')]; + results = []; + for (j = 0, len = ref2.length; j < len; j++) { + file = ref2[j]; + p = path.join(fixtures, 'asar', 'a.asar', file); + throws = function() { + return fs.lstatSync(p); + }; + results.push(assert.throws(throws, /ENOENT/)); + } + return results; + }); + }); + describe('fs.lstat', function() { + it('handles path with trailing slash correctly', function(done) { + var p; + p = path.join(fixtures, 'asar', 'a.asar', 'link2', 'link2', 'file1'); + return fs.lstat(p + '/', done); + }); + it('returns information of root', function(done) { + var p, stats; + p = path.join(fixtures, 'asar', 'a.asar'); + return stats = fs.lstat(p, function(err, stats) { + assert.equal(err, null); + assert.equal(stats.isFile(), false); + assert.equal(stats.isDirectory(), true); + assert.equal(stats.isSymbolicLink(), false); + assert.equal(stats.size, 0); + return done(); + }); + }); + it('returns information of a normal file', function(done) { + var p, stats; + p = path.join(fixtures, 'asar', 'a.asar', 'link2', 'file1'); + return stats = fs.lstat(p, function(err, stats) { + assert.equal(err, null); + assert.equal(stats.isFile(), true); + assert.equal(stats.isDirectory(), false); + assert.equal(stats.isSymbolicLink(), false); + assert.equal(stats.size, 6); + return done(); + }); + }); + it('returns information of a normal directory', function(done) { + var p, stats; + p = path.join(fixtures, 'asar', 'a.asar', 'dir1'); + return stats = fs.lstat(p, function(err, stats) { + assert.equal(err, null); + assert.equal(stats.isFile(), false); + assert.equal(stats.isDirectory(), true); + assert.equal(stats.isSymbolicLink(), false); + assert.equal(stats.size, 0); + return done(); + }); + }); + it('returns information of a linked file', function(done) { + var p, stats; + p = path.join(fixtures, 'asar', 'a.asar', 'link2', 'link1'); + return stats = fs.lstat(p, function(err, stats) { + assert.equal(err, null); + assert.equal(stats.isFile(), false); + assert.equal(stats.isDirectory(), false); + assert.equal(stats.isSymbolicLink(), true); + assert.equal(stats.size, 0); + return done(); + }); + }); + it('returns information of a linked directory', function(done) { + var p, stats; + p = path.join(fixtures, 'asar', 'a.asar', 'link2', 'link2'); + return stats = fs.lstat(p, function(err, stats) { + assert.equal(err, null); + assert.equal(stats.isFile(), false); + assert.equal(stats.isDirectory(), false); + assert.equal(stats.isSymbolicLink(), true); + assert.equal(stats.size, 0); + return done(); + }); + }); + return it('throws ENOENT error when can not find file', function(done) { + var p, stats; + p = path.join(fixtures, 'asar', 'a.asar', 'file4'); + return stats = fs.lstat(p, function(err, stats) { + assert.equal(err.code, 'ENOENT'); + return done(); + }); + }); + }); + describe('fs.realpathSync', function() { + it('returns real path root', function() { + var p, parent, r; + parent = fs.realpathSync(path.join(fixtures, 'asar')); + p = 'a.asar'; + r = fs.realpathSync(path.join(parent, p)); + return assert.equal(r, path.join(parent, p)); + }); + it('returns real path of a normal file', function() { + var p, parent, r; + parent = fs.realpathSync(path.join(fixtures, 'asar')); + p = path.join('a.asar', 'file1'); + r = fs.realpathSync(path.join(parent, p)); + return assert.equal(r, path.join(parent, p)); + }); + it('returns real path of a normal directory', function() { + var p, parent, r; + parent = fs.realpathSync(path.join(fixtures, 'asar')); + p = path.join('a.asar', 'dir1'); + r = fs.realpathSync(path.join(parent, p)); + return assert.equal(r, path.join(parent, p)); + }); + it('returns real path of a linked file', function() { + var p, parent, r; + parent = fs.realpathSync(path.join(fixtures, 'asar')); + p = path.join('a.asar', 'link2', 'link1'); + r = fs.realpathSync(path.join(parent, p)); + return assert.equal(r, path.join(parent, 'a.asar', 'file1')); + }); + it('returns real path of a linked directory', function() { + var p, parent, r; + parent = fs.realpathSync(path.join(fixtures, 'asar')); + p = path.join('a.asar', 'link2', 'link2'); + r = fs.realpathSync(path.join(parent, p)); + return assert.equal(r, path.join(parent, 'a.asar', 'dir1')); + }); + return it('throws ENOENT error when can not find file', function() { + var p, parent, throws; + parent = fs.realpathSync(path.join(fixtures, 'asar')); + p = path.join('a.asar', 'not-exist'); + throws = function() { + return fs.realpathSync(path.join(parent, p)); + }; + return assert.throws(throws, /ENOENT/); + }); + }); + describe('fs.realpath', function() { + it('returns real path root', function(done) { + var p, parent; + parent = fs.realpathSync(path.join(fixtures, 'asar')); + p = 'a.asar'; + return fs.realpath(path.join(parent, p), function(err, r) { + assert.equal(err, null); + assert.equal(r, path.join(parent, p)); + return done(); + }); + }); + it('returns real path of a normal file', function(done) { + var p, parent; + parent = fs.realpathSync(path.join(fixtures, 'asar')); + p = path.join('a.asar', 'file1'); + return fs.realpath(path.join(parent, p), function(err, r) { + assert.equal(err, null); + assert.equal(r, path.join(parent, p)); + return done(); + }); + }); + it('returns real path of a normal directory', function(done) { + var p, parent; + parent = fs.realpathSync(path.join(fixtures, 'asar')); + p = path.join('a.asar', 'dir1'); + return fs.realpath(path.join(parent, p), function(err, r) { + assert.equal(err, null); + assert.equal(r, path.join(parent, p)); + return done(); + }); + }); + it('returns real path of a linked file', function(done) { + var p, parent; + parent = fs.realpathSync(path.join(fixtures, 'asar')); + p = path.join('a.asar', 'link2', 'link1'); + return fs.realpath(path.join(parent, p), function(err, r) { + assert.equal(err, null); + assert.equal(r, path.join(parent, 'a.asar', 'file1')); + return done(); + }); + }); + it('returns real path of a linked directory', function(done) { + var p, parent; + parent = fs.realpathSync(path.join(fixtures, 'asar')); + p = path.join('a.asar', 'link2', 'link2'); + return fs.realpath(path.join(parent, p), function(err, r) { + assert.equal(err, null); + assert.equal(r, path.join(parent, 'a.asar', 'dir1')); + return done(); + }); + }); + return it('throws ENOENT error when can not find file', function(done) { + var p, parent; + parent = fs.realpathSync(path.join(fixtures, 'asar')); + p = path.join('a.asar', 'not-exist'); + return fs.realpath(path.join(parent, p), function(err, stats) { + assert.equal(err.code, 'ENOENT'); + return done(); + }); + }); + }); + describe('fs.readdirSync', function() { + it('reads dirs from root', function() { + var dirs, p; + p = path.join(fixtures, 'asar', 'a.asar'); + dirs = fs.readdirSync(p); + return assert.deepEqual(dirs, ['dir1', 'dir2', 'dir3', 'file1', 'file2', 'file3', 'link1', 'link2', 'ping.js']); + }); + it('reads dirs from a normal dir', function() { + var dirs, p; + p = path.join(fixtures, 'asar', 'a.asar', 'dir1'); + dirs = fs.readdirSync(p); + return assert.deepEqual(dirs, ['file1', 'file2', 'file3', 'link1', 'link2']); + }); + it('reads dirs from a linked dir', function() { + var dirs, p; + p = path.join(fixtures, 'asar', 'a.asar', 'link2', 'link2'); + dirs = fs.readdirSync(p); + return assert.deepEqual(dirs, ['file1', 'file2', 'file3', 'link1', 'link2']); + }); + return it('throws ENOENT error when can not find file', function() { + var p, throws; + p = path.join(fixtures, 'asar', 'a.asar', 'not-exist'); + throws = function() { + return fs.readdirSync(p); + }; + return assert.throws(throws, /ENOENT/); + }); + }); + describe('fs.readdir', function() { + it('reads dirs from root', function(done) { + var dirs, p; + p = path.join(fixtures, 'asar', 'a.asar'); + return dirs = fs.readdir(p, function(err, dirs) { + assert.equal(err, null); + assert.deepEqual(dirs, ['dir1', 'dir2', 'dir3', 'file1', 'file2', 'file3', 'link1', 'link2', 'ping.js']); + return done(); + }); + }); + it('reads dirs from a normal dir', function(done) { + var dirs, p; + p = path.join(fixtures, 'asar', 'a.asar', 'dir1'); + return dirs = fs.readdir(p, function(err, dirs) { + assert.equal(err, null); + assert.deepEqual(dirs, ['file1', 'file2', 'file3', 'link1', 'link2']); + return done(); + }); + }); + it('reads dirs from a linked dir', function(done) { + var dirs, p; + p = path.join(fixtures, 'asar', 'a.asar', 'link2', 'link2'); + return dirs = fs.readdir(p, function(err, dirs) { + assert.equal(err, null); + assert.deepEqual(dirs, ['file1', 'file2', 'file3', 'link1', 'link2']); + return done(); + }); + }); + return it('throws ENOENT error when can not find file', function(done) { + var p; + p = path.join(fixtures, 'asar', 'a.asar', 'not-exist'); + return fs.readdir(p, function(err, stats) { + assert.equal(err.code, 'ENOENT'); + return done(); + }); + }); + }); + describe('fs.openSync', function() { + it('opens a normal/linked/under-linked-directory file', function() { + var buffer, fd, file, j, len, p, ref2, results; + ref2 = ['file1', 'link1', path.join('link2', 'file1')]; + results = []; + for (j = 0, len = ref2.length; j < len; j++) { + file = ref2[j]; + p = path.join(fixtures, 'asar', 'a.asar', file); + fd = fs.openSync(p, 'r'); + buffer = new Buffer(6); + fs.readSync(fd, buffer, 0, 6, 0); + assert.equal(String(buffer).trim(), 'file1'); + results.push(fs.closeSync(fd)); + } + return results; + }); + return it('throws ENOENT error when can not find file', function() { + var p, throws; + p = path.join(fixtures, 'asar', 'a.asar', 'not-exist'); + throws = function() { + return fs.openSync(p); + }; + return assert.throws(throws, /ENOENT/); + }); + }); + describe('fs.open', function() { + it('opens a normal file', function(done) { + var p; + p = path.join(fixtures, 'asar', 'a.asar', 'file1'); + return fs.open(p, 'r', function(err, fd) { + var buffer; + assert.equal(err, null); + buffer = new Buffer(6); + return fs.read(fd, buffer, 0, 6, 0, function(err) { + assert.equal(err, null); + assert.equal(String(buffer).trim(), 'file1'); + return fs.close(fd, done); + }); + }); + }); + return it('throws ENOENT error when can not find file', function(done) { + var p; + p = path.join(fixtures, 'asar', 'a.asar', 'not-exist'); + return fs.open(p, 'r', function(err, stats) { + assert.equal(err.code, 'ENOENT'); + return done(); + }); + }); + }); + describe('fs.mkdir', function() { + return it('throws error when calling inside asar archive', function(done) { + var p; + p = path.join(fixtures, 'asar', 'a.asar', 'not-exist'); + return fs.mkdir(p, function(err) { + assert.equal(err.code, 'ENOTDIR'); + return done(); + }); + }); + }); + describe('fs.mkdirSync', function() { + return it('throws error when calling inside asar archive', function() { + var p; + p = path.join(fixtures, 'asar', 'a.asar', 'not-exist'); + return assert.throws((function() { + return fs.mkdirSync(p); + }), new RegExp('ENOTDIR')); + }); + }); + describe('child_process.fork', function() { + child_process = require('child_process'); + it('opens a normal js file', function(done) { + var child; + child = child_process.fork(path.join(fixtures, 'asar', 'a.asar', 'ping.js')); + child.on('message', function(msg) { + assert.equal(msg, 'message'); + return done(); + }); + return child.send('message'); + }); + return it('supports asar in the forked js', function(done) { + var child, file; + file = path.join(fixtures, 'asar', 'a.asar', 'file1'); + child = child_process.fork(path.join(fixtures, 'module', 'asar.js')); + child.on('message', function(content) { + assert.equal(content, fs.readFileSync(file).toString()); + return done(); + }); + return child.send(file); + }); + }); + describe('child_process.execFile', function() { + var echo, execFile, execFileSync, ref2; + if (process.platform !== 'darwin') { + return; + } + ref2 = require('child_process'), execFile = ref2.execFile, execFileSync = ref2.execFileSync; + echo = path.join(fixtures, 'asar', 'echo.asar', 'echo'); + it('executes binaries', function(done) { + var child; + return child = execFile(echo, ['test'], function(error, stdout) { + assert.equal(error, null); + assert.equal(stdout, 'test\n'); + return done(); + }); + }); + return xit('execFileSync executes binaries', function() { + var output; + output = execFileSync(echo, ['test']); + return assert.equal(String(output), 'test\n'); + }); + }); + describe('internalModuleReadFile', function() { + var internalModuleReadFile; + internalModuleReadFile = process.binding('fs').internalModuleReadFile; + it('read a normal file', function() { + var file1, file2, file3; + file1 = path.join(fixtures, 'asar', 'a.asar', 'file1'); + assert.equal(internalModuleReadFile(file1).toString().trim(), 'file1'); + file2 = path.join(fixtures, 'asar', 'a.asar', 'file2'); + assert.equal(internalModuleReadFile(file2).toString().trim(), 'file2'); + file3 = path.join(fixtures, 'asar', 'a.asar', 'file3'); + return assert.equal(internalModuleReadFile(file3).toString().trim(), 'file3'); + }); + return it('reads a normal file with unpacked files', function() { + var p; + p = path.join(fixtures, 'asar', 'unpack.asar', 'a.txt'); + return assert.equal(internalModuleReadFile(p).toString().trim(), 'a'); + }); + }); + return describe('process.noAsar', function() { + var errorName; + errorName = process.platform === 'win32' ? 'ENOENT' : 'ENOTDIR'; + beforeEach(function() { + return process.noAsar = true; + }); + afterEach(function() { + return process.noAsar = false; + }); + it('disables asar support in sync API', function() { + var dir, file; + file = path.join(fixtures, 'asar', 'a.asar', 'file1'); + dir = path.join(fixtures, 'asar', 'a.asar', 'dir1'); + assert.throws((function() { + return fs.readFileSync(file); + }), new RegExp(errorName)); + assert.throws((function() { + return fs.lstatSync(file); + }), new RegExp(errorName)); + assert.throws((function() { + return fs.realpathSync(file); + }), new RegExp(errorName)); + return assert.throws((function() { + return fs.readdirSync(dir); + }), new RegExp(errorName)); + }); + it('disables asar support in async API', function(done) { + var dir, file; + file = path.join(fixtures, 'asar', 'a.asar', 'file1'); + dir = path.join(fixtures, 'asar', 'a.asar', 'dir1'); + return fs.readFile(file, function(error) { + assert.equal(error.code, errorName); + return fs.lstat(file, function(error) { + assert.equal(error.code, errorName); + return fs.realpath(file, function(error) { + assert.equal(error.code, errorName); + return fs.readdir(dir, function(error) { + assert.equal(error.code, errorName); + return done(); + }); + }); + }); + }); + }); + return it('treats *.asar as normal file', function() { + var asar, content1, content2, originalFs; + originalFs = require('original-fs'); + asar = path.join(fixtures, 'asar', 'a.asar'); + content1 = fs.readFileSync(asar); + content2 = originalFs.readFileSync(asar); + assert.equal(content1.compare(content2), 0); + return assert.throws((function() { + return fs.readdirSync(asar); + }), /ENOTDIR/); + }); + }); + }); + describe('asar protocol', function() { + var url; + url = require('url'); + it('can request a file in package', function(done) { + var p; + p = path.resolve(fixtures, 'asar', 'a.asar', 'file1'); + return $.get("file://" + p, function(data) { + assert.equal(data.trim(), 'file1'); + return done(); + }); + }); + it('can request a file in package with unpacked files', function(done) { + var p; + p = path.resolve(fixtures, 'asar', 'unpack.asar', 'a.txt'); + return $.get("file://" + p, function(data) { + assert.equal(data.trim(), 'a'); + return done(); + }); + }); + it('can request a linked file in package', function(done) { + var p; + p = path.resolve(fixtures, 'asar', 'a.asar', 'link2', 'link1'); + return $.get("file://" + p, function(data) { + assert.equal(data.trim(), 'file1'); + return done(); + }); + }); + it('can request a file in filesystem', function(done) { + var p; + p = path.resolve(fixtures, 'asar', 'file'); + return $.get("file://" + p, function(data) { + assert.equal(data.trim(), 'file'); + return done(); + }); + }); + it('gets 404 when file is not found', function(done) { + var p; + p = path.resolve(fixtures, 'asar', 'a.asar', 'no-exist'); + return $.ajax({ + url: "file://" + p, + error: function(err) { + assert.equal(err.status, 404); + return done(); + } + }); + }); + it('sets __dirname correctly', function(done) { + var p, u, w; + after(function() { + w.destroy(); + return ipcMain.removeAllListeners('dirname'); + }); + w = new BrowserWindow({ + show: false, + width: 400, + height: 400 + }); + p = path.resolve(fixtures, 'asar', 'web.asar', 'index.html'); + u = url.format({ + protocol: 'file', + slashed: true, + pathname: p + }); + ipcMain.once('dirname', function(event, dirname) { + assert.equal(dirname, path.dirname(p)); + return done(); + }); + return w.loadURL(u); + }); + return it('loads script tag in html', function(done) { + var p, u, w; + after(function() { + w.destroy(); + return ipcMain.removeAllListeners('ping'); + }); + w = new BrowserWindow({ + show: false, + width: 400, + height: 400 + }); + p = path.resolve(fixtures, 'asar', 'script.asar', 'index.html'); + u = url.format({ + protocol: 'file', + slashed: true, + pathname: p + }); + w.loadURL(u); + return ipcMain.once('ping', function(event, message) { + assert.equal(message, 'pong'); + return done(); + }); + }); + }); + describe('original-fs module', function() { + var originalFs; + originalFs = require('original-fs'); + it('treats .asar as file', function() { + var file, stats; + file = path.join(fixtures, 'asar', 'a.asar'); + stats = originalFs.statSync(file); + return assert(stats.isFile()); + }); + return it('is available in forked scripts', function(done) { + var child; + child = child_process.fork(path.join(fixtures, 'module', 'original-fs.js')); + child.on('message', function(msg) { + assert.equal(msg, 'object'); + return done(); + }); + return child.send('message'); + }); + }); + describe('graceful-fs module', function() { + var gfs; + gfs = require('graceful-fs'); + it('recognize asar archvies', function() { + var p; + p = path.join(fixtures, 'asar', 'a.asar', 'link1'); + return assert.equal(gfs.readFileSync(p).toString().trim(), 'file1'); + }); + return it('does not touch global fs object', function() { + return assert.notEqual(fs.readdir, gfs.readdir); + }); + }); + describe('mkdirp module', function() { + var mkdirp; + mkdirp = require('mkdirp'); + return it('throws error when calling inside asar archive', function() { + var p; + p = path.join(fixtures, 'asar', 'a.asar', 'not-exist'); + return assert.throws((function() { + return mkdirp.sync(p); + }), new RegExp('ENOTDIR')); + }); + }); + return describe('native-image', function() { + it('reads image from asar archive', function() { + var logo, p; + p = path.join(fixtures, 'asar', 'logo.asar', 'logo.png'); + logo = nativeImage.createFromPath(p); + return assert.deepEqual(logo.getSize(), { + width: 55, + height: 55 + }); + }); + return it('reads image from asar archive with unpacked files', function() { + var logo, p; + p = path.join(fixtures, 'asar', 'unpack.asar', 'atom.png'); + logo = nativeImage.createFromPath(p); + return assert.deepEqual(logo.getSize(), { + width: 1024, + height: 1024 + }); + }); + }); +}); diff --git a/spec/chromium-spec.coffee b/spec/chromium-spec.coffee deleted file mode 100644 index cc28f2f7ad47..000000000000 --- a/spec/chromium-spec.coffee +++ /dev/null @@ -1,275 +0,0 @@ -assert = require 'assert' -http = require 'http' -https = require 'https' -path = require 'path' -ws = require 'ws' - -{remote} = require 'electron' -{BrowserWindow, session} = remote.require 'electron' - -describe 'chromium feature', -> - fixtures = path.resolve __dirname, 'fixtures' - - listener = null - afterEach -> - if listener? - window.removeEventListener 'message', listener - listener = null - - xdescribe 'heap snapshot', -> - it 'does not crash', -> - process.atomBinding('v8_util').takeHeapSnapshot() - - describe 'sending request of http protocol urls', -> - it 'does not crash', (done) -> - @timeout 5000 - server = http.createServer (req, res) -> - res.end() - server.close() - done() - server.listen 0, '127.0.0.1', -> - {port} = server.address() - $.get "http://127.0.0.1:#{port}" - - describe 'document.hidden', -> - url = "file://#{fixtures}/pages/document-hidden.html" - w = null - - afterEach -> - w?.destroy() - - it 'is set correctly when window is not shown', (done) -> - w = new BrowserWindow(show:false) - w.webContents.on 'ipc-message', (event, args) -> - assert.deepEqual args, ['hidden', true] - done() - w.loadURL url - - it 'is set correctly when window is inactive', (done) -> - w = new BrowserWindow(show:false) - w.webContents.on 'ipc-message', (event, args) -> - assert.deepEqual args, ['hidden', false] - done() - w.showInactive() - w.loadURL url - - xdescribe 'navigator.webkitGetUserMedia', -> - it 'calls its callbacks', (done) -> - @timeout 5000 - navigator.webkitGetUserMedia audio: true, video: false, - -> done() - -> done() - - describe 'navigator.language', -> - it 'should not be empty', -> - assert.notEqual navigator.language, '' - - describe 'navigator.serviceWorker', -> - url = "file://#{fixtures}/pages/service-worker/index.html" - w = null - - afterEach -> - w?.destroy() - - it 'should register for file scheme', (done) -> - w = new BrowserWindow(show:false) - w.webContents.on 'ipc-message', (event, args) -> - if args[0] == 'reload' - w.webContents.reload() - else if args[0] == 'error' - done('unexpected error : ' + args[1]) - else if args[0] == 'response' - assert.equal args[1], 'Hello from serviceWorker!' - session.defaultSession.clearStorageData {storages: ['serviceworkers']}, -> - done() - w.loadURL url - - describe 'window.open', -> - @timeout 20000 - - it 'returns a BrowserWindowProxy object', -> - b = window.open 'about:blank', '', 'show=no' - assert.equal b.closed, false - assert.equal b.constructor.name, 'BrowserWindowProxy' - b.close() - - it 'accepts "node-integration" as feature', (done) -> - listener = (event) -> - assert.equal event.data, 'undefined' - b.close() - done() - window.addEventListener 'message', listener - b = window.open "file://#{fixtures}/pages/window-opener-node.html", '', 'nodeIntegration=no,show=no' - - it 'inherit options of parent window', (done) -> - listener = (event) -> - [width, height] = remote.getCurrentWindow().getSize() - assert.equal event.data, "size: #{width} #{height}" - b.close() - done() - window.addEventListener 'message', listener - b = window.open "file://#{fixtures}/pages/window-open-size.html", '', 'show=no' - - it 'does not override child options', (done) -> - size = {width: 350, height: 450} - listener = (event) -> - assert.equal event.data, "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}" - - describe 'window.opener', -> - @timeout 10000 - - url = "file://#{fixtures}/pages/window-opener.html" - w = null - - afterEach -> - w?.destroy() - - it 'is null for main window', (done) -> - w = new BrowserWindow(show: false) - w.webContents.on 'ipc-message', (event, args) -> - assert.deepEqual args, ['opener', null] - done() - w.loadURL url - - it 'is not null for window opened by window.open', (done) -> - listener = (event) -> - assert.equal event.data, 'object' - b.close() - done() - window.addEventListener 'message', listener - b = window.open url, '', 'show=no' - - describe 'window.postMessage', -> - it 'sets the source and origin correctly', (done) -> - sourceId = remote.getCurrentWindow().id - listener = (event) -> - window.removeEventListener 'message', listener - b.close() - message = JSON.parse(event.data) - assert.equal message.data, 'testing' - assert.equal message.origin, 'file://' - assert.equal message.sourceEqualsOpener, true - assert.equal message.sourceId, sourceId - assert.equal event.origin, 'file://' - done() - window.addEventListener 'message', listener - b = window.open "file://#{fixtures}/pages/window-open-postMessage.html", '', 'show=no' - BrowserWindow.fromId(b.guestId).webContents.once 'did-finish-load', -> - b.postMessage('testing', '*') - - describe 'window.opener.postMessage', -> - it 'sets source and origin correctly', (done) -> - listener = (event) -> - window.removeEventListener 'message', listener - b.close() - assert.equal event.source, b - assert.equal event.origin, 'file://' - done() - window.addEventListener 'message', listener - b = window.open "file://#{fixtures}/pages/window-opener-postMessage.html", '', 'show=no' - - describe 'creating a Uint8Array under browser side', -> - it 'does not crash', -> - RUint8Array = remote.getGlobal 'Uint8Array' - new RUint8Array - - describe 'webgl', -> - it 'can be get as context in canvas', -> - return if process.platform is 'linux' - webgl = document.createElement('canvas').getContext 'webgl' - assert.notEqual webgl, null - - describe 'web workers', -> - it 'Worker can work', (done) -> - worker = new Worker('../fixtures/workers/worker.js') - message = 'ping' - worker.onmessage = (event) -> - assert.equal event.data, message - worker.terminate() - done() - worker.postMessage message - - it 'SharedWorker can work', (done) -> - worker = new SharedWorker('../fixtures/workers/shared_worker.js') - message = 'ping' - worker.port.onmessage = (event) -> - assert.equal event.data, message - done() - worker.port.postMessage message - - describe 'iframe', -> - iframe = null - - beforeEach -> - iframe = document.createElement 'iframe' - - afterEach -> - document.body.removeChild iframe - - it 'does not have node integration', (done) -> - iframe.src = "file://#{fixtures}/pages/set-global.html" - document.body.appendChild iframe - iframe.onload = -> - assert.equal iframe.contentWindow.test, 'undefined undefined undefined' - done() - - describe 'storage', -> - it 'requesting persitent quota works', (done) -> - navigator.webkitPersistentStorage.requestQuota 1024 * 1024, (grantedBytes) -> - assert.equal grantedBytes, 1048576 - done() - - describe 'websockets', -> - wss = null - server = null - WebSocketServer = ws.Server - - afterEach -> - wss.close() - server.close() - - it 'has user agent', (done) -> - server = http.createServer() - server.listen 0, '127.0.0.1', -> - port = server.address().port - wss = new WebSocketServer(server: server) - wss.on 'error', done - wss.on 'connection', (ws) -> - if ws.upgradeReq.headers['user-agent'] - done() - else - done('user agent is empty') - websocket = new WebSocket("ws://127.0.0.1:#{port}") - - describe 'Promise', -> - it 'resolves correctly in Node.js calls', (done) -> - document.registerElement('x-element', { - prototype: Object.create(HTMLElement.prototype, { - createdCallback: { value: -> } - }) - }) - - setImmediate -> - called = false - Promise.resolve().then -> - done(if called then undefined else new Error('wrong sequence')) - document.createElement 'x-element' - called = true - - it 'resolves correctly in Electron calls', (done) -> - document.registerElement('y-element', { - prototype: Object.create(HTMLElement.prototype, { - createdCallback: { value: -> } - }) - }) - - remote.getGlobal('setImmediate') -> - called = false - Promise.resolve().then -> - done(if called then undefined else new Error('wrong sequence')) - document.createElement 'y-element' - called = true diff --git a/spec/chromium-spec.js b/spec/chromium-spec.js new file mode 100644 index 000000000000..563eea742ffd --- /dev/null +++ b/spec/chromium-spec.js @@ -0,0 +1,368 @@ +var BrowserWindow, assert, http, https, path, ref, remote, session, ws; + +assert = require('assert'); + +http = require('http'); + +https = require('https'); + +path = require('path'); + +ws = require('ws'); + +remote = require('electron').remote; + +ref = remote.require('electron'), BrowserWindow = ref.BrowserWindow, session = ref.session; + +describe('chromium feature', function() { + var fixtures, listener; + fixtures = path.resolve(__dirname, 'fixtures'); + listener = null; + afterEach(function() { + if (listener != null) { + window.removeEventListener('message', listener); + } + return listener = null; + }); + xdescribe('heap snapshot', function() { + return it('does not crash', function() { + return process.atomBinding('v8_util').takeHeapSnapshot(); + }); + }); + describe('sending request of http protocol urls', function() { + return it('does not crash', function(done) { + var server; + this.timeout(5000); + server = http.createServer(function(req, res) { + res.end(); + server.close(); + return done(); + }); + return server.listen(0, '127.0.0.1', function() { + var port; + port = server.address().port; + return $.get("http://127.0.0.1:" + port); + }); + }); + }); + describe('document.hidden', function() { + var url, w; + url = "file://" + fixtures + "/pages/document-hidden.html"; + w = null; + afterEach(function() { + return w != null ? w.destroy() : void 0; + }); + it('is set correctly when window is not shown', function(done) { + w = new BrowserWindow({ + show: false + }); + w.webContents.on('ipc-message', function(event, args) { + assert.deepEqual(args, ['hidden', true]); + return done(); + }); + return w.loadURL(url); + }); + return it('is set correctly when window is inactive', function(done) { + w = new BrowserWindow({ + show: false + }); + w.webContents.on('ipc-message', function(event, args) { + assert.deepEqual(args, ['hidden', false]); + return done(); + }); + w.showInactive(); + return w.loadURL(url); + }); + }); + xdescribe('navigator.webkitGetUserMedia', function() { + return it('calls its callbacks', function(done) { + this.timeout(5000); + return navigator.webkitGetUserMedia({ + audio: true, + video: false + }, function() { + return done(); + }, function() { + return done(); + }); + }); + }); + describe('navigator.language', function() { + return it('should not be empty', function() { + return assert.notEqual(navigator.language, ''); + }); + }); + describe('navigator.serviceWorker', function() { + var url, w; + url = "file://" + fixtures + "/pages/service-worker/index.html"; + w = null; + afterEach(function() { + return w != null ? w.destroy() : void 0; + }); + return it('should register for file scheme', function(done) { + w = new BrowserWindow({ + show: false + }); + w.webContents.on('ipc-message', function(event, args) { + if (args[0] === 'reload') { + return w.webContents.reload(); + } else if (args[0] === 'error') { + return done('unexpected error : ' + args[1]); + } else if (args[0] === 'response') { + assert.equal(args[1], 'Hello from serviceWorker!'); + return session.defaultSession.clearStorageData({ + storages: ['serviceworkers'] + }, function() { + return done(); + }); + } + }); + return w.loadURL(url); + }); + }); + describe('window.open', function() { + this.timeout(20000); + it('returns a BrowserWindowProxy object', function() { + var b; + b = window.open('about:blank', '', 'show=no'); + assert.equal(b.closed, false); + assert.equal(b.constructor.name, 'BrowserWindowProxy'); + return b.close(); + }); + it('accepts "node-integration" as feature', function(done) { + var b; + listener = function(event) { + assert.equal(event.data, 'undefined'); + b.close(); + return done(); + }; + window.addEventListener('message', listener); + return b = window.open("file://" + fixtures + "/pages/window-opener-node.html", '', 'nodeIntegration=no,show=no'); + }); + it('inherit options of parent window', function(done) { + var b; + listener = function(event) { + var height, ref1, width; + ref1 = remote.getCurrentWindow().getSize(), width = ref1[0], height = ref1[1]; + assert.equal(event.data, "size: " + width + " " + height); + b.close(); + return done(); + }; + window.addEventListener('message', listener); + return b = window.open("file://" + fixtures + "/pages/window-open-size.html", '', 'show=no'); + }); + return it('does not override child options', function(done) { + var b, size; + size = { + width: 350, + height: 450 + }; + listener = function(event) { + assert.equal(event.data, "size: " + size.width + " " + size.height); + b.close(); + return done(); + }; + window.addEventListener('message', listener); + return b = window.open("file://" + fixtures + "/pages/window-open-size.html", '', "show=no,width=" + size.width + ",height=" + size.height); + }); + }); + describe('window.opener', function() { + var url, w; + this.timeout(10000); + url = "file://" + fixtures + "/pages/window-opener.html"; + w = null; + afterEach(function() { + return w != null ? w.destroy() : void 0; + }); + it('is null for main window', function(done) { + w = new BrowserWindow({ + show: false + }); + w.webContents.on('ipc-message', function(event, args) { + assert.deepEqual(args, ['opener', null]); + return done(); + }); + return w.loadURL(url); + }); + return it('is not null for window opened by window.open', function(done) { + var b; + listener = function(event) { + assert.equal(event.data, 'object'); + b.close(); + return done(); + }; + window.addEventListener('message', listener); + return b = window.open(url, '', 'show=no'); + }); + }); + describe('window.postMessage', function() { + return it('sets the source and origin correctly', function(done) { + var b, sourceId; + sourceId = remote.getCurrentWindow().id; + listener = function(event) { + var message; + window.removeEventListener('message', listener); + b.close(); + message = JSON.parse(event.data); + assert.equal(message.data, 'testing'); + assert.equal(message.origin, 'file://'); + assert.equal(message.sourceEqualsOpener, true); + assert.equal(message.sourceId, sourceId); + assert.equal(event.origin, 'file://'); + return done(); + }; + window.addEventListener('message', listener); + b = window.open("file://" + fixtures + "/pages/window-open-postMessage.html", '', 'show=no'); + return BrowserWindow.fromId(b.guestId).webContents.once('did-finish-load', function() { + return b.postMessage('testing', '*'); + }); + }); + }); + describe('window.opener.postMessage', function() { + return it('sets source and origin correctly', function(done) { + var b; + listener = function(event) { + window.removeEventListener('message', listener); + b.close(); + assert.equal(event.source, b); + assert.equal(event.origin, 'file://'); + return done(); + }; + window.addEventListener('message', listener); + return b = window.open("file://" + fixtures + "/pages/window-opener-postMessage.html", '', 'show=no'); + }); + }); + describe('creating a Uint8Array under browser side', function() { + return it('does not crash', function() { + var RUint8Array; + RUint8Array = remote.getGlobal('Uint8Array'); + return new RUint8Array; + }); + }); + describe('webgl', function() { + return it('can be get as context in canvas', function() { + var webgl; + if (process.platform === 'linux') { + return; + } + webgl = document.createElement('canvas').getContext('webgl'); + return assert.notEqual(webgl, null); + }); + }); + describe('web workers', function() { + it('Worker can work', function(done) { + var message, worker; + worker = new Worker('../fixtures/workers/worker.js'); + message = 'ping'; + worker.onmessage = function(event) { + assert.equal(event.data, message); + worker.terminate(); + return done(); + }; + return worker.postMessage(message); + }); + return it('SharedWorker can work', function(done) { + var message, worker; + worker = new SharedWorker('../fixtures/workers/shared_worker.js'); + message = 'ping'; + worker.port.onmessage = function(event) { + assert.equal(event.data, message); + return done(); + }; + return worker.port.postMessage(message); + }); + }); + describe('iframe', function() { + var iframe; + iframe = null; + beforeEach(function() { + return iframe = document.createElement('iframe'); + }); + afterEach(function() { + return document.body.removeChild(iframe); + }); + return it('does not have node integration', function(done) { + iframe.src = "file://" + fixtures + "/pages/set-global.html"; + document.body.appendChild(iframe); + return iframe.onload = function() { + assert.equal(iframe.contentWindow.test, 'undefined undefined undefined'); + return done(); + }; + }); + }); + describe('storage', function() { + return it('requesting persitent quota works', function(done) { + return navigator.webkitPersistentStorage.requestQuota(1024 * 1024, function(grantedBytes) { + assert.equal(grantedBytes, 1048576); + return done(); + }); + }); + }); + describe('websockets', function() { + var WebSocketServer, server, wss; + wss = null; + server = null; + WebSocketServer = ws.Server; + afterEach(function() { + wss.close(); + return server.close(); + }); + return it('has user agent', function(done) { + server = http.createServer(); + return server.listen(0, '127.0.0.1', function() { + var port, websocket; + port = server.address().port; + wss = new WebSocketServer({ + server: server + }); + wss.on('error', done); + wss.on('connection', function(ws) { + if (ws.upgradeReq.headers['user-agent']) { + return done(); + } else { + return done('user agent is empty'); + } + }); + return websocket = new WebSocket("ws://127.0.0.1:" + port); + }); + }); + }); + return describe('Promise', function() { + it('resolves correctly in Node.js calls', function(done) { + document.registerElement('x-element', { + prototype: Object.create(HTMLElement.prototype, { + createdCallback: { + value: function() {} + } + }) + }); + return setImmediate(function() { + var called; + called = false; + Promise.resolve().then(function() { + return done(called ? void 0 : new Error('wrong sequence')); + }); + document.createElement('x-element'); + return called = true; + }); + }); + return it('resolves correctly in Electron calls', function(done) { + document.registerElement('y-element', { + prototype: Object.create(HTMLElement.prototype, { + createdCallback: { + value: function() {} + } + }) + }); + return remote.getGlobal('setImmediate')(function() { + var called; + called = false; + Promise.resolve().then(function() { + return done(called ? void 0 : new Error('wrong sequence')); + }); + document.createElement('y-element'); + return called = true; + }); + }); + }); +}); diff --git a/spec/modules-spec.coffee b/spec/modules-spec.coffee deleted file mode 100644 index 1cdc6cf0ea5c..000000000000 --- a/spec/modules-spec.coffee +++ /dev/null @@ -1,38 +0,0 @@ -assert = require 'assert' -fs = require 'fs' -path = require 'path' -temp = require 'temp' - -describe 'third-party module', -> - fixtures = path.join __dirname, 'fixtures' - temp.track() - - # If the test is executed with the debug build on Windows, we will skip it - # because native modules don't work with the debug build (see issue #2558). - if process.platform isnt 'win32' or - process.execPath.toLowerCase().indexOf('\\out\\d\\') is -1 - describe 'runas', -> - it 'can be required in renderer', -> - require 'runas' - - it 'can be required in node binary', (done) -> - runas = path.join fixtures, 'module', 'runas.js' - child = require('child_process').fork runas - child.on 'message', (msg) -> - assert.equal msg, 'ok' - done() - - describe 'ffi', -> - it 'does not crash', -> - ffi = require 'ffi' - libm = ffi.Library('libm', ceil: [ 'double', [ 'double' ] ]) - assert.equal libm.ceil(1.5), 2 - - describe 'q', -> - Q = require 'q' - - describe 'Q.when', -> - it 'emits the fullfil callback', (done) -> - Q(true).then (val) -> - assert.equal val, true - done() diff --git a/spec/modules-spec.js b/spec/modules-spec.js new file mode 100644 index 000000000000..809aa547b1d9 --- /dev/null +++ b/spec/modules-spec.js @@ -0,0 +1,53 @@ +var assert, fs, path, temp; + +assert = require('assert'); + +fs = require('fs'); + +path = require('path'); + +temp = require('temp'); + +describe('third-party module', function() { + var fixtures; + fixtures = path.join(__dirname, 'fixtures'); + temp.track(); + if (process.platform !== 'win32' || process.execPath.toLowerCase().indexOf('\\out\\d\\') === -1) { + describe('runas', function() { + it('can be required in renderer', function() { + return require('runas'); + }); + return it('can be required in node binary', function(done) { + var child, runas; + runas = path.join(fixtures, 'module', 'runas.js'); + child = require('child_process').fork(runas); + return child.on('message', function(msg) { + assert.equal(msg, 'ok'); + return done(); + }); + }); + }); + describe('ffi', function() { + return it('does not crash', function() { + var ffi, libm; + ffi = require('ffi'); + libm = ffi.Library('libm', { + ceil: ['double', ['double']] + }); + return assert.equal(libm.ceil(1.5), 2); + }); + }); + } + return describe('q', function() { + var Q; + Q = require('q'); + return describe('Q.when', function() { + return it('emits the fullfil callback', function(done) { + return Q(true).then(function(val) { + assert.equal(val, true); + return done(); + }); + }); + }); + }); +}); diff --git a/spec/node-spec.coffee b/spec/node-spec.coffee deleted file mode 100644 index e6b2aa15821c..000000000000 --- a/spec/node-spec.coffee +++ /dev/null @@ -1,156 +0,0 @@ -assert = require 'assert' -child_process = require 'child_process' -fs = require 'fs' -path = require 'path' -os = require 'os' - -{remote} = require 'electron' - -describe 'node feature', -> - fixtures = path.join __dirname, 'fixtures' - - describe 'child_process', -> - describe 'child_process.fork', -> - it 'works in current process', (done) -> - child = child_process.fork path.join(fixtures, 'module', 'ping.js') - child.on 'message', (msg) -> - assert.equal msg, 'message' - done() - child.send 'message' - - it 'preserves args', (done) -> - args = ['--expose_gc', '-test', '1'] - child = child_process.fork path.join(fixtures, 'module', 'process_args.js'), args - child.on 'message', (msg) -> - assert.deepEqual args, msg.slice(2) - done() - child.send 'message' - - it 'works in forked process', (done) -> - child = child_process.fork path.join(fixtures, 'module', 'fork_ping.js') - child.on 'message', (msg) -> - assert.equal msg, 'message' - done() - child.send 'message' - - it 'works in forked process when options.env is specifed', (done) -> - child = child_process.fork path.join(fixtures, 'module', 'fork_ping.js'), - [], - path: process.env['PATH'] - child.on 'message', (msg) -> - assert.equal msg, 'message' - done() - child.send 'message' - - it 'works in browser process', (done) -> - fork = remote.require('child_process').fork - child = fork path.join(fixtures, 'module', 'ping.js') - child.on 'message', (msg) -> - assert.equal msg, 'message' - done() - child.send 'message' - - it 'has String::localeCompare working in script', (done) -> - child = child_process.fork path.join(fixtures, 'module', 'locale-compare.js') - child.on 'message', (msg) -> - assert.deepEqual msg, [0, -1, 1] - done() - child.send 'message' - - it 'has setImmediate working in script', (done) -> - child = child_process.fork path.join(fixtures, 'module', 'set-immediate.js') - child.on 'message', (msg) -> - assert.equal msg, 'ok' - done() - child.send 'message' - - describe 'contexts', -> - describe 'setTimeout in fs callback', -> - return if process.env.TRAVIS is 'true' - it 'does not crash', (done) -> - fs.readFile __filename, -> - setTimeout done, 0 - - describe 'throw error in node context', -> - it 'gets caught', (done) -> - error = new Error('boo!') - lsts = process.listeners 'uncaughtException' - process.removeAllListeners 'uncaughtException' - process.on 'uncaughtException', (err) -> - process.removeAllListeners 'uncaughtException' - for lst in lsts - process.on 'uncaughtException', lst - done() - fs.readFile __filename, -> - throw error - - describe 'setTimeout called under Chromium event loop in browser process', -> - it 'can be scheduled in time', (done) -> - remote.getGlobal('setTimeout')(done, 0) - - describe 'setInterval called under Chromium event loop in browser process', -> - it 'can be scheduled in time', (done) -> - clear = -> - remote.getGlobal('clearInterval')(interval) - done() - interval = remote.getGlobal('setInterval')(clear, 10) - - describe 'message loop', -> - describe 'process.nextTick', -> - it 'emits the callback', (done) -> - process.nextTick done - - it 'works in nested calls', (done) -> - process.nextTick -> - process.nextTick -> - process.nextTick done - - describe 'setImmediate', -> - it 'emits the callback', (done) -> - setImmediate done - - it 'works in nested calls', (done) -> - setImmediate -> - setImmediate -> - setImmediate done - - describe 'net.connect', -> - return unless process.platform is 'darwin' - - it 'emit error when connect to a socket path without listeners', (done) -> - socketPath = path.join os.tmpdir(), 'atom-shell-test.sock' - script = path.join(fixtures, 'module', 'create_socket.js') - child = child_process.fork script, [socketPath] - child.on 'exit', (code) -> - assert.equal code, 0 - client = require('net').connect socketPath - client.on 'error', (error) -> - assert.equal error.code, 'ECONNREFUSED' - done() - - describe 'Buffer', -> - it 'can be created from WebKit external string', -> - p = document.createElement 'p' - p.innerText = '闲云潭影日悠悠,物换星移几度秋' - b = new Buffer(p.innerText) - assert.equal b.toString(), '闲云潭影日悠悠,物换星移几度秋' - assert.equal Buffer.byteLength(p.innerText), 45 - - it 'correctly parses external one-byte UTF8 string', -> - p = document.createElement 'p' - p.innerText = 'Jøhänñéß' - b = new Buffer(p.innerText) - assert.equal b.toString(), 'Jøhänñéß' - assert.equal Buffer.byteLength(p.innerText), 13 - - describe 'process.stdout', -> - it 'should not throw exception', -> - process.stdout - - # Not reliable on some machines - xit 'should have isTTY defined', -> - assert.equal typeof(process.stdout.isTTY), 'boolean' - - describe 'vm.createContext', -> - it 'should not crash', -> - require('vm').runInNewContext('') diff --git a/spec/node-spec.js b/spec/node-spec.js new file mode 100644 index 000000000000..fb9a40ea8d42 --- /dev/null +++ b/spec/node-spec.js @@ -0,0 +1,213 @@ +var assert, child_process, fs, os, path, remote; + +assert = require('assert'); + +child_process = require('child_process'); + +fs = require('fs'); + +path = require('path'); + +os = require('os'); + +remote = require('electron').remote; + +describe('node feature', function() { + var fixtures; + fixtures = path.join(__dirname, 'fixtures'); + describe('child_process', function() { + return describe('child_process.fork', function() { + it('works in current process', function(done) { + var child; + child = child_process.fork(path.join(fixtures, 'module', 'ping.js')); + child.on('message', function(msg) { + assert.equal(msg, 'message'); + return done(); + }); + return child.send('message'); + }); + it('preserves args', function(done) { + var args, child; + args = ['--expose_gc', '-test', '1']; + child = child_process.fork(path.join(fixtures, 'module', 'process_args.js'), args); + child.on('message', function(msg) { + assert.deepEqual(args, msg.slice(2)); + return done(); + }); + return child.send('message'); + }); + it('works in forked process', function(done) { + var child; + child = child_process.fork(path.join(fixtures, 'module', 'fork_ping.js')); + child.on('message', function(msg) { + assert.equal(msg, 'message'); + return done(); + }); + return child.send('message'); + }); + it('works in forked process when options.env is specifed', function(done) { + var child; + child = child_process.fork(path.join(fixtures, 'module', 'fork_ping.js'), [], { + path: process.env['PATH'] + }); + child.on('message', function(msg) { + assert.equal(msg, 'message'); + return done(); + }); + return child.send('message'); + }); + it('works in browser process', function(done) { + var child, fork; + fork = remote.require('child_process').fork; + child = fork(path.join(fixtures, 'module', 'ping.js')); + child.on('message', function(msg) { + assert.equal(msg, 'message'); + return done(); + }); + return child.send('message'); + }); + it('has String::localeCompare working in script', function(done) { + var child; + child = child_process.fork(path.join(fixtures, 'module', 'locale-compare.js')); + child.on('message', function(msg) { + assert.deepEqual(msg, [0, -1, 1]); + return done(); + }); + return child.send('message'); + }); + return it('has setImmediate working in script', function(done) { + var child; + child = child_process.fork(path.join(fixtures, 'module', 'set-immediate.js')); + child.on('message', function(msg) { + assert.equal(msg, 'ok'); + return done(); + }); + return child.send('message'); + }); + }); + }); + describe('contexts', function() { + describe('setTimeout in fs callback', function() { + if (process.env.TRAVIS === 'true') { + return; + } + return it('does not crash', function(done) { + return fs.readFile(__filename, function() { + return setTimeout(done, 0); + }); + }); + }); + describe('throw error in node context', function() { + return it('gets caught', function(done) { + var error, lsts; + error = new Error('boo!'); + lsts = process.listeners('uncaughtException'); + process.removeAllListeners('uncaughtException'); + process.on('uncaughtException', function(err) { + var i, len, lst; + process.removeAllListeners('uncaughtException'); + for (i = 0, len = lsts.length; i < len; i++) { + lst = lsts[i]; + process.on('uncaughtException', lst); + } + return done(); + }); + return fs.readFile(__filename, function() { + throw error; + }); + }); + }); + describe('setTimeout called under Chromium event loop in browser process', function() { + return it('can be scheduled in time', function(done) { + return remote.getGlobal('setTimeout')(done, 0); + }); + }); + return describe('setInterval called under Chromium event loop in browser process', function() { + return it('can be scheduled in time', function(done) { + var clear, interval; + clear = function() { + remote.getGlobal('clearInterval')(interval); + return done(); + }; + return interval = remote.getGlobal('setInterval')(clear, 10); + }); + }); + }); + describe('message loop', function() { + describe('process.nextTick', function() { + it('emits the callback', function(done) { + return process.nextTick(done); + }); + return it('works in nested calls', function(done) { + return process.nextTick(function() { + return process.nextTick(function() { + return process.nextTick(done); + }); + }); + }); + }); + return describe('setImmediate', function() { + it('emits the callback', function(done) { + return setImmediate(done); + }); + return it('works in nested calls', function(done) { + return setImmediate(function() { + return setImmediate(function() { + return setImmediate(done); + }); + }); + }); + }); + }); + describe('net.connect', function() { + if (process.platform !== 'darwin') { + return; + } + return it('emit error when connect to a socket path without listeners', function(done) { + var child, script, socketPath; + socketPath = path.join(os.tmpdir(), 'atom-shell-test.sock'); + script = path.join(fixtures, 'module', 'create_socket.js'); + child = child_process.fork(script, [socketPath]); + return child.on('exit', function(code) { + var client; + assert.equal(code, 0); + client = require('net').connect(socketPath); + return client.on('error', function(error) { + assert.equal(error.code, 'ECONNREFUSED'); + return done(); + }); + }); + }); + }); + describe('Buffer', function() { + it('can be created from WebKit external string', function() { + var b, p; + p = document.createElement('p'); + p.innerText = '闲云潭影日悠悠,物换星移几度秋'; + b = new Buffer(p.innerText); + assert.equal(b.toString(), '闲云潭影日悠悠,物换星移几度秋'); + return assert.equal(Buffer.byteLength(p.innerText), 45); + }); + return it('correctly parses external one-byte UTF8 string', function() { + var b, p; + p = document.createElement('p'); + p.innerText = 'Jøhänñéß'; + b = new Buffer(p.innerText); + assert.equal(b.toString(), 'Jøhänñéß'); + return assert.equal(Buffer.byteLength(p.innerText), 13); + }); + }); + describe('process.stdout', function() { + it('should not throw exception', function() { + return process.stdout; + }); + return xit('should have isTTY defined', function() { + return assert.equal(typeof process.stdout.isTTY, 'boolean'); + }); + }); + return describe('vm.createContext', function() { + return it('should not crash', function() { + return require('vm').runInNewContext(''); + }); + }); +}); diff --git a/spec/webview-spec.coffee b/spec/webview-spec.coffee deleted file mode 100644 index 4754ea4348be..000000000000 --- a/spec/webview-spec.coffee +++ /dev/null @@ -1,517 +0,0 @@ -assert = require 'assert' -path = require 'path' -http = require 'http' -url = require 'url' - -describe ' tag', -> - @timeout 10000 - - fixtures = path.join __dirname, 'fixtures' - - webview = null - beforeEach -> - webview = new WebView - afterEach -> - document.body.removeChild(webview) if document.body.contains(webview) - - describe 'src attribute', -> - it 'specifies the page to load', (done) -> - webview.addEventListener 'console-message', (e) -> - assert.equal e.message, 'a' - done() - webview.src = "file://#{fixtures}/pages/a.html" - document.body.appendChild webview - - it 'navigates to new page when changed', (done) -> - listener = (e) -> - webview.src = "file://#{fixtures}/pages/b.html" - webview.addEventListener 'console-message', (e) -> - assert.equal e.message, 'b' - done() - webview.removeEventListener 'did-finish-load', listener - webview.addEventListener 'did-finish-load', listener - webview.src = "file://#{fixtures}/pages/a.html" - document.body.appendChild webview - - describe 'nodeintegration attribute', -> - it 'inserts no node symbols when not set', (done) -> - webview.addEventListener 'console-message', (e) -> - assert.equal e.message, 'undefined undefined undefined undefined' - done() - webview.src = "file://#{fixtures}/pages/c.html" - document.body.appendChild webview - - it 'inserts node symbols when set', (done) -> - webview.addEventListener 'console-message', (e) -> - assert.equal e.message, 'function object object' - done() - webview.setAttribute 'nodeintegration', 'on' - webview.src = "file://#{fixtures}/pages/d.html" - document.body.appendChild webview - - it 'loads node symbols after POST navigation when set', (done) -> - webview.addEventListener 'console-message', (e) -> - assert.equal e.message, 'function object object' - done() - webview.setAttribute 'nodeintegration', 'on' - webview.src = "file://#{fixtures}/pages/post.html" - document.body.appendChild webview - - # If the test is executed with the debug build on Windows, we will skip it - # because native modules don't work with the debug build (see issue #2558). - if process.platform isnt 'win32' or - process.execPath.toLowerCase().indexOf('\\out\\d\\') is -1 - it 'loads native modules when navigation happens', (done) -> - listener = (e) -> - webview.removeEventListener 'did-finish-load', listener - listener2 = (e) -> - assert.equal e.message, 'function' - done() - webview.addEventListener 'console-message', listener2 - webview.reload() - webview.addEventListener 'did-finish-load', listener - webview.setAttribute 'nodeintegration', 'on' - webview.src = "file://#{fixtures}/pages/native-module.html" - document.body.appendChild webview - - describe 'preload attribute', -> - it 'loads the script before other scripts in window', (done) -> - listener = (e) -> - assert.equal e.message, 'function object object' - webview.removeEventListener 'console-message', listener - done() - webview.addEventListener 'console-message', listener - webview.setAttribute 'preload', "#{fixtures}/module/preload.js" - webview.src = "file://#{fixtures}/pages/e.html" - document.body.appendChild webview - - it 'preload script can still use "process" in required modules when nodeintegration is off', (done) -> - webview.addEventListener 'console-message', (e) -> - assert.equal e.message, 'object undefined object' - done() - webview.setAttribute 'preload', "#{fixtures}/module/preload-node-off.js" - webview.src = "file://#{fixtures}/api/blank.html" - document.body.appendChild webview - - it 'receives ipc message in preload script', (done) -> - message = 'boom!' - listener = (e) -> - assert.equal e.channel, 'pong' - assert.deepEqual e.args, [message] - webview.removeEventListener 'ipc-message', listener - done() - listener2 = (e) -> - webview.send 'ping', message - webview.removeEventListener 'did-finish-load', listener2 - webview.addEventListener 'ipc-message', listener - webview.addEventListener 'did-finish-load', listener2 - webview.setAttribute 'preload', "#{fixtures}/module/preload-ipc.js" - webview.src = "file://#{fixtures}/pages/e.html" - document.body.appendChild webview - - describe 'httpreferrer attribute', -> - it 'sets the referrer url', (done) -> - referrer = 'http://github.com/' - listener = (e) -> - assert.equal e.message, referrer - webview.removeEventListener 'console-message', listener - done() - webview.addEventListener 'console-message', listener - webview.setAttribute 'httpreferrer', referrer - webview.src = "file://#{fixtures}/pages/referrer.html" - document.body.appendChild webview - - describe 'useragent attribute', -> - it 'sets the user agent', (done) -> - referrer = 'Mozilla/5.0 (Windows NT 6.1; WOW64; Trident/7.0; AS; rv:11.0) like Gecko' - listener = (e) -> - assert.equal e.message, referrer - webview.removeEventListener 'console-message', listener - done() - webview.addEventListener 'console-message', listener - webview.setAttribute 'useragent', referrer - webview.src = "file://#{fixtures}/pages/useragent.html" - document.body.appendChild webview - - describe 'disablewebsecurity attribute', -> - it 'does not disable web security when not set', (done) -> - src = " - - - " - encoded = btoa(unescape(encodeURIComponent(src))) - listener = (e) -> - assert /Not allowed to load local resource/.test(e.message) - webview.removeEventListener 'console-message', listener - done() - webview.addEventListener 'console-message', listener - webview.src = "data:text/html;base64,#{encoded}" - document.body.appendChild webview - - it 'disables web security when set', (done) -> - src = " - - - " - encoded = btoa(unescape(encodeURIComponent(src))) - listener = (e) -> - assert.equal e.message, 'ok' - webview.removeEventListener 'console-message', listener - done() - webview.addEventListener 'console-message', listener - webview.setAttribute 'disablewebsecurity', '' - webview.src = "data:text/html;base64,#{encoded}" - document.body.appendChild webview - - describe 'partition attribute', -> - it 'inserts no node symbols when not set', (done) -> - webview.addEventListener 'console-message', (e) -> - assert.equal e.message, 'undefined undefined undefined undefined' - done() - webview.src = "file://#{fixtures}/pages/c.html" - webview.partition = 'test1' - document.body.appendChild webview - - it 'inserts node symbols when set', (done) -> - webview.addEventListener 'console-message', (e) -> - assert.equal e.message, 'function object object' - done() - webview.setAttribute 'nodeintegration', 'on' - webview.src = "file://#{fixtures}/pages/d.html" - webview.partition = 'test2' - document.body.appendChild webview - - it 'isolates storage for different id', (done) -> - listener = (e) -> - assert.equal e.message, " 0" - webview.removeEventListener 'console-message', listener - done() - window.localStorage.setItem 'test', 'one' - webview.addEventListener 'console-message', listener - webview.src = "file://#{fixtures}/pages/partition/one.html" - webview.partition = 'test3' - document.body.appendChild webview - - it 'uses current session storage when no id is provided', (done) -> - listener = (e) -> - assert.equal e.message, "one 1" - webview.removeEventListener 'console-message', listener - done() - window.localStorage.setItem 'test', 'one' - webview.addEventListener 'console-message', listener - webview.src = "file://#{fixtures}/pages/partition/one.html" - document.body.appendChild webview - - describe 'allowpopups attribute', -> - it 'can not open new window when not set', (done) -> - listener = (e) -> - assert.equal e.message, 'null' - webview.removeEventListener 'console-message', listener - done() - webview.addEventListener 'console-message', listener - webview.src = "file://#{fixtures}/pages/window-open-hide.html" - document.body.appendChild webview - - it 'can open new window when set', (done) -> - listener = (e) -> - assert.equal e.message, 'window' - webview.removeEventListener 'console-message', listener - done() - webview.addEventListener 'console-message', listener - webview.setAttribute 'allowpopups', 'on' - webview.src = "file://#{fixtures}/pages/window-open-hide.html" - document.body.appendChild webview - - describe 'new-window event', -> - it 'emits when window.open is called', (done) -> - webview.addEventListener 'new-window', (e) -> - assert.equal e.url, 'http://host/' - assert.equal e.frameName, 'host' - done() - webview.src = "file://#{fixtures}/pages/window-open.html" - document.body.appendChild webview - - it 'emits when link with target is called', (done) -> - webview.addEventListener 'new-window', (e) -> - assert.equal e.url, 'http://host/' - assert.equal e.frameName, 'target' - done() - webview.src = "file://#{fixtures}/pages/target-name.html" - document.body.appendChild webview - - describe 'ipc-message event', -> - it 'emits when guest sends a ipc message to browser', (done) -> - webview.addEventListener 'ipc-message', (e) -> - assert.equal e.channel, 'channel' - assert.deepEqual e.args, ['arg1', 'arg2'] - done() - webview.src = "file://#{fixtures}/pages/ipc-message.html" - webview.setAttribute 'nodeintegration', 'on' - document.body.appendChild webview - - describe 'page-title-set event', -> - it 'emits when title is set', (done) -> - webview.addEventListener 'page-title-set', (e) -> - assert.equal e.title, 'test' - assert e.explicitSet - done() - webview.src = "file://#{fixtures}/pages/a.html" - document.body.appendChild webview - - describe 'page-favicon-updated event', -> - it 'emits when favicon urls are received', (done) -> - webview.addEventListener 'page-favicon-updated', (e) -> - assert.equal e.favicons.length, 2 - pageUrl = - if process.platform is 'win32' - 'file:///C:/favicon.png' - else - 'file:///favicon.png' - assert.equal e.favicons[0], pageUrl - done() - webview.src = "file://#{fixtures}/pages/a.html" - document.body.appendChild webview - - describe 'will-navigate event', -> - it 'emits when a url that leads to oustide of the page is clicked', (done) -> - webview.addEventListener 'will-navigate', (e) -> - assert.equal e.url, "http://host/" - done() - - webview.src = "file://#{fixtures}/pages/webview-will-navigate.html" - document.body.appendChild webview - - describe 'did-navigate event', -> - p = path.join fixtures, 'pages', 'webview-will-navigate.html' - p = p.replace /\\/g, '/' - pageUrl = url.format protocol: 'file', slashes: true, pathname: p - - it 'emits when a url that leads to outside of the page is clicked', (done) -> - webview.addEventListener 'did-navigate', (e) -> - assert.equal e.url, pageUrl - done() - - webview.src = pageUrl - document.body.appendChild webview - - describe 'did-navigate-in-page event', -> - it 'emits when an anchor link is clicked', (done) -> - p = path.join fixtures, 'pages', 'webview-did-navigate-in-page.html' - p = p.replace /\\/g, '/' - pageUrl = url.format protocol: 'file', slashes: true, pathname: p - - webview.addEventListener 'did-navigate-in-page', (e) -> - assert.equal e.url, "#{pageUrl}#test_content" - done() - - webview.src = pageUrl - document.body.appendChild webview - - it 'emits when window.history.replaceState is called', (done) -> - webview.addEventListener 'did-navigate-in-page', (e) -> - assert.equal e.url, "http://host/" - done() - - webview.src = "file://#{fixtures}/pages/webview-did-navigate-in-page-with-history.html" - document.body.appendChild webview - - it 'emits when window.location.hash is changed', (done) -> - p = path.join fixtures, 'pages', 'webview-did-navigate-in-page-with-hash.html' - p = p.replace /\\/g, '/' - pageUrl = url.format protocol: 'file', slashes: true, pathname: p - - webview.addEventListener 'did-navigate-in-page', (e) -> - assert.equal e.url, "#{pageUrl}#test" - done() - - webview.src = pageUrl - document.body.appendChild webview - - describe 'close event', -> - it 'should fire when interior page calls window.close', (done) -> - webview.addEventListener 'close', -> - done() - - webview.src = "file://#{fixtures}/pages/close.html" - document.body.appendChild webview - - describe 'devtools-opened event', -> - it 'should fire when webview.openDevTools() is called', (done) -> - listener = -> - webview.removeEventListener 'devtools-opened', listener - webview.closeDevTools() - done() - - webview.addEventListener 'devtools-opened', listener - webview.addEventListener 'dom-ready', -> - webview.openDevTools() - - webview.src = "file://#{fixtures}/pages/base-page.html" - document.body.appendChild webview - - describe 'devtools-closed event', -> - it 'should fire when webview.closeDevTools() is called', (done) -> - listener2 = -> - webview.removeEventListener 'devtools-closed', listener2 - done() - - listener = -> - webview.removeEventListener 'devtools-opened', listener - webview.closeDevTools() - - webview.addEventListener 'devtools-opened', listener - webview.addEventListener 'devtools-closed', listener2 - webview.addEventListener 'dom-ready', -> - webview.openDevTools() - - webview.src = "file://#{fixtures}/pages/base-page.html" - document.body.appendChild webview - - describe 'devtools-focused event', -> - it 'should fire when webview.openDevTools() is called', (done) -> - listener = -> - webview.removeEventListener 'devtools-focused', listener - webview.closeDevTools() - done() - - webview.addEventListener 'devtools-focused', listener - webview.addEventListener 'dom-ready', -> - webview.openDevTools() - - webview.src = "file://#{fixtures}/pages/base-page.html" - document.body.appendChild webview - - describe '.reload()', -> - it 'should emit beforeunload handler', (done) -> - listener = (e) -> - assert.equal e.channel, 'onbeforeunload' - webview.removeEventListener 'ipc-message', listener - done() - listener2 = (e) -> - webview.reload() - webview.removeEventListener 'did-finish-load', listener2 - webview.addEventListener 'ipc-message', listener - webview.addEventListener 'did-finish-load', listener2 - webview.setAttribute 'nodeintegration', 'on' - webview.src = "file://#{fixtures}/pages/beforeunload-false.html" - document.body.appendChild webview - - describe '.clearHistory()', -> - it 'should clear the navigation history', (done) -> - listener = (e) -> - assert.equal e.channel, 'history' - assert.equal e.args[0], 2 - assert webview.canGoBack() - webview.clearHistory() - assert not webview.canGoBack() - webview.removeEventListener 'ipc-message', listener - done() - webview.addEventListener 'ipc-message', listener - webview.setAttribute 'nodeintegration', 'on' - webview.src = "file://#{fixtures}/pages/history.html" - document.body.appendChild webview - - describe 'basic auth', -> - auth = require 'basic-auth' - - it 'should authenticate with correct credentials', (done) -> - message = 'Authenticated' - server = http.createServer (req, res) -> - credentials = auth(req) - if credentials.name == 'test' and credentials.pass == 'test' - res.end(message) - else - res.end('failed') - server.close() - server.listen 0, '127.0.0.1', -> - {port} = server.address() - webview.addEventListener 'ipc-message', (e) -> - assert.equal e.channel, message - done() - webview.src = "file://#{fixtures}/pages/basic-auth.html?port=#{port}" - webview.setAttribute 'nodeintegration', 'on' - document.body.appendChild webview - - describe 'dom-ready event', -> - it 'emits when document is loaded', (done) -> - server = http.createServer (req) -> - # Never respond, so the page never finished loading. - server.listen 0, '127.0.0.1', -> - {port} = server.address() - webview.addEventListener 'dom-ready', -> - done() - webview.src = "file://#{fixtures}/pages/dom-ready.html?port=#{port}" - document.body.appendChild webview - - describe 'executeJavaScript', -> - return unless process.env.TRAVIS is 'true' - - it 'should support user gesture', (done) -> - listener = (e) -> - webview.removeEventListener 'enter-html-full-screen', listener - done() - listener2 = (e) -> - jsScript = 'document.getElementsByTagName("video")[0].webkitRequestFullScreen()' - webview.executeJavaScript jsScript, true - webview.removeEventListener 'did-finish-load', listener2 - webview.addEventListener 'enter-html-full-screen', listener - webview.addEventListener 'did-finish-load', listener2 - webview.src = "file://#{fixtures}/pages/fullscreen.html" - document.body.appendChild webview - - describe 'sendInputEvent', -> - it 'can send keyboard event', (done) -> - webview.addEventListener 'ipc-message', (e) -> - assert.equal e.channel, 'keyup' - assert.deepEqual e.args, [67, true, false] - done() - webview.addEventListener 'dom-ready', -> - webview.sendInputEvent type: 'keyup', keyCode: 'c', modifiers: ['shift'] - webview.src = "file://#{fixtures}/pages/onkeyup.html" - webview.setAttribute 'nodeintegration', 'on' - document.body.appendChild webview - - it 'can send mouse event', (done) -> - webview.addEventListener 'ipc-message', (e) -> - assert.equal e.channel, 'mouseup' - assert.deepEqual e.args, [10, 20, false, true] - done() - webview.addEventListener 'dom-ready', -> - webview.sendInputEvent type: 'mouseup', modifiers: ['ctrl'], x: 10, y: 20 - webview.src = "file://#{fixtures}/pages/onmouseup.html" - webview.setAttribute 'nodeintegration', 'on' - document.body.appendChild webview - - describe 'media-started-playing media-paused events', -> - it 'emits when audio starts and stops playing', (done) -> - audioPlayed = false - webview.addEventListener 'media-started-playing', -> - audioPlayed = true - webview.addEventListener 'media-paused', -> - assert audioPlayed - done() - webview.src = "file://#{fixtures}/pages/audio.html" - document.body.appendChild webview - - describe 'found-in-page event', -> - it 'emits when a request is made', (done) -> - requestId = null - listener = (e) -> - assert.equal e.result.requestId, requestId - if e.result.finalUpdate - assert.equal e.result.matches, 3 - webview.stopFindInPage "clearSelection" - done() - listener2 = (e) -> - requestId = webview.findInPage "virtual" - webview.addEventListener 'found-in-page', listener - webview.addEventListener 'did-finish-load', listener2 - webview.src = "file://#{fixtures}/pages/content.html" - document.body.appendChild webview - - xdescribe 'did-change-theme-color event', -> - it 'emits when theme color changes', (done) -> - webview.addEventListener 'did-change-theme-color', (e) -> - done() - webview.src = "file://#{fixtures}/pages/theme-color.html" - document.body.appendChild webview diff --git a/spec/webview-spec.js b/spec/webview-spec.js new file mode 100644 index 000000000000..660a960cf946 --- /dev/null +++ b/spec/webview-spec.js @@ -0,0 +1,639 @@ +var assert, http, path, url; + +assert = require('assert'); + +path = require('path'); + +http = require('http'); + +url = require('url'); + +describe(' tag', function() { + var fixtures, webview; + this.timeout(10000); + fixtures = path.join(__dirname, 'fixtures'); + webview = null; + beforeEach(function() { + return webview = new WebView; + }); + afterEach(function() { + if (document.body.contains(webview)) { + return document.body.removeChild(webview); + } + }); + describe('src attribute', function() { + it('specifies the page to load', function(done) { + webview.addEventListener('console-message', function(e) { + assert.equal(e.message, 'a'); + return done(); + }); + webview.src = "file://" + fixtures + "/pages/a.html"; + return document.body.appendChild(webview); + }); + return it('navigates to new page when changed', function(done) { + var listener; + listener = function(e) { + webview.src = "file://" + fixtures + "/pages/b.html"; + webview.addEventListener('console-message', function(e) { + assert.equal(e.message, 'b'); + return done(); + }); + return webview.removeEventListener('did-finish-load', listener); + }; + webview.addEventListener('did-finish-load', listener); + webview.src = "file://" + fixtures + "/pages/a.html"; + return document.body.appendChild(webview); + }); + }); + describe('nodeintegration attribute', function() { + it('inserts no node symbols when not set', function(done) { + webview.addEventListener('console-message', function(e) { + assert.equal(e.message, 'undefined undefined undefined undefined'); + return done(); + }); + webview.src = "file://" + fixtures + "/pages/c.html"; + return document.body.appendChild(webview); + }); + it('inserts node symbols when set', function(done) { + webview.addEventListener('console-message', function(e) { + assert.equal(e.message, 'function object object'); + return done(); + }); + webview.setAttribute('nodeintegration', 'on'); + webview.src = "file://" + fixtures + "/pages/d.html"; + return document.body.appendChild(webview); + }); + it('loads node symbols after POST navigation when set', function(done) { + webview.addEventListener('console-message', function(e) { + assert.equal(e.message, 'function object object'); + return done(); + }); + webview.setAttribute('nodeintegration', 'on'); + webview.src = "file://" + fixtures + "/pages/post.html"; + return document.body.appendChild(webview); + }); + if (process.platform !== 'win32' || process.execPath.toLowerCase().indexOf('\\out\\d\\') === -1) { + return it('loads native modules when navigation happens', function(done) { + var listener; + listener = function(e) { + var listener2; + webview.removeEventListener('did-finish-load', listener); + listener2 = function(e) { + assert.equal(e.message, 'function'); + return done(); + }; + webview.addEventListener('console-message', listener2); + return webview.reload(); + }; + webview.addEventListener('did-finish-load', listener); + webview.setAttribute('nodeintegration', 'on'); + webview.src = "file://" + fixtures + "/pages/native-module.html"; + return document.body.appendChild(webview); + }); + } + }); + describe('preload attribute', function() { + it('loads the script before other scripts in window', function(done) { + var listener; + listener = function(e) { + assert.equal(e.message, 'function object object'); + webview.removeEventListener('console-message', listener); + return done(); + }; + webview.addEventListener('console-message', listener); + webview.setAttribute('preload', fixtures + "/module/preload.js"); + webview.src = "file://" + fixtures + "/pages/e.html"; + return document.body.appendChild(webview); + }); + it('preload script can still use "process" in required modules when nodeintegration is off', function(done) { + webview.addEventListener('console-message', function(e) { + assert.equal(e.message, 'object undefined object'); + return done(); + }); + webview.setAttribute('preload', fixtures + "/module/preload-node-off.js"); + webview.src = "file://" + fixtures + "/api/blank.html"; + return document.body.appendChild(webview); + }); + return it('receives ipc message in preload script', function(done) { + var listener, listener2, message; + message = 'boom!'; + listener = function(e) { + assert.equal(e.channel, 'pong'); + assert.deepEqual(e.args, [message]); + webview.removeEventListener('ipc-message', listener); + return done(); + }; + listener2 = function(e) { + webview.send('ping', message); + return webview.removeEventListener('did-finish-load', listener2); + }; + webview.addEventListener('ipc-message', listener); + webview.addEventListener('did-finish-load', listener2); + webview.setAttribute('preload', fixtures + "/module/preload-ipc.js"); + webview.src = "file://" + fixtures + "/pages/e.html"; + return document.body.appendChild(webview); + }); + }); + describe('httpreferrer attribute', function() { + return it('sets the referrer url', function(done) { + var listener, referrer; + referrer = 'http://github.com/'; + listener = function(e) { + assert.equal(e.message, referrer); + webview.removeEventListener('console-message', listener); + return done(); + }; + webview.addEventListener('console-message', listener); + webview.setAttribute('httpreferrer', referrer); + webview.src = "file://" + fixtures + "/pages/referrer.html"; + return document.body.appendChild(webview); + }); + }); + describe('useragent attribute', function() { + return it('sets the user agent', function(done) { + var listener, referrer; + referrer = 'Mozilla/5.0 (Windows NT 6.1; WOW64; Trident/7.0; AS; rv:11.0) like Gecko'; + listener = function(e) { + assert.equal(e.message, referrer); + webview.removeEventListener('console-message', listener); + return done(); + }; + webview.addEventListener('console-message', listener); + webview.setAttribute('useragent', referrer); + webview.src = "file://" + fixtures + "/pages/useragent.html"; + return document.body.appendChild(webview); + }); + }); + describe('disablewebsecurity attribute', function() { + it('does not disable web security when not set', function(done) { + var encoded, listener, src; + src = " "; + encoded = btoa(unescape(encodeURIComponent(src))); + listener = function(e) { + assert(/Not allowed to load local resource/.test(e.message)); + webview.removeEventListener('console-message', listener); + return done(); + }; + webview.addEventListener('console-message', listener); + webview.src = "data:text/html;base64," + encoded; + return document.body.appendChild(webview); + }); + return it('disables web security when set', function(done) { + var encoded, listener, src; + src = " "; + encoded = btoa(unescape(encodeURIComponent(src))); + listener = function(e) { + assert.equal(e.message, 'ok'); + webview.removeEventListener('console-message', listener); + return done(); + }; + webview.addEventListener('console-message', listener); + webview.setAttribute('disablewebsecurity', ''); + webview.src = "data:text/html;base64," + encoded; + return document.body.appendChild(webview); + }); + }); + describe('partition attribute', function() { + it('inserts no node symbols when not set', function(done) { + webview.addEventListener('console-message', function(e) { + assert.equal(e.message, 'undefined undefined undefined undefined'); + return done(); + }); + webview.src = "file://" + fixtures + "/pages/c.html"; + webview.partition = 'test1'; + return document.body.appendChild(webview); + }); + it('inserts node symbols when set', function(done) { + webview.addEventListener('console-message', function(e) { + assert.equal(e.message, 'function object object'); + return done(); + }); + webview.setAttribute('nodeintegration', 'on'); + webview.src = "file://" + fixtures + "/pages/d.html"; + webview.partition = 'test2'; + return document.body.appendChild(webview); + }); + it('isolates storage for different id', function(done) { + var listener; + listener = function(e) { + assert.equal(e.message, " 0"); + webview.removeEventListener('console-message', listener); + return done(); + }; + window.localStorage.setItem('test', 'one'); + webview.addEventListener('console-message', listener); + webview.src = "file://" + fixtures + "/pages/partition/one.html"; + webview.partition = 'test3'; + return document.body.appendChild(webview); + }); + return it('uses current session storage when no id is provided', function(done) { + var listener; + listener = function(e) { + assert.equal(e.message, "one 1"); + webview.removeEventListener('console-message', listener); + return done(); + }; + window.localStorage.setItem('test', 'one'); + webview.addEventListener('console-message', listener); + webview.src = "file://" + fixtures + "/pages/partition/one.html"; + return document.body.appendChild(webview); + }); + }); + describe('allowpopups attribute', function() { + it('can not open new window when not set', function(done) { + var listener; + listener = function(e) { + assert.equal(e.message, 'null'); + webview.removeEventListener('console-message', listener); + return done(); + }; + webview.addEventListener('console-message', listener); + webview.src = "file://" + fixtures + "/pages/window-open-hide.html"; + return document.body.appendChild(webview); + }); + return it('can open new window when set', function(done) { + var listener; + listener = function(e) { + assert.equal(e.message, 'window'); + webview.removeEventListener('console-message', listener); + return done(); + }; + webview.addEventListener('console-message', listener); + webview.setAttribute('allowpopups', 'on'); + webview.src = "file://" + fixtures + "/pages/window-open-hide.html"; + return document.body.appendChild(webview); + }); + }); + describe('new-window event', function() { + it('emits when window.open is called', function(done) { + webview.addEventListener('new-window', function(e) { + assert.equal(e.url, 'http://host/'); + assert.equal(e.frameName, 'host'); + return done(); + }); + webview.src = "file://" + fixtures + "/pages/window-open.html"; + return document.body.appendChild(webview); + }); + return it('emits when link with target is called', function(done) { + webview.addEventListener('new-window', function(e) { + assert.equal(e.url, 'http://host/'); + assert.equal(e.frameName, 'target'); + return done(); + }); + webview.src = "file://" + fixtures + "/pages/target-name.html"; + return document.body.appendChild(webview); + }); + }); + describe('ipc-message event', function() { + return it('emits when guest sends a ipc message to browser', function(done) { + webview.addEventListener('ipc-message', function(e) { + assert.equal(e.channel, 'channel'); + assert.deepEqual(e.args, ['arg1', 'arg2']); + return done(); + }); + webview.src = "file://" + fixtures + "/pages/ipc-message.html"; + webview.setAttribute('nodeintegration', 'on'); + return document.body.appendChild(webview); + }); + }); + describe('page-title-set event', function() { + return it('emits when title is set', function(done) { + webview.addEventListener('page-title-set', function(e) { + assert.equal(e.title, 'test'); + assert(e.explicitSet); + return done(); + }); + webview.src = "file://" + fixtures + "/pages/a.html"; + return document.body.appendChild(webview); + }); + }); + describe('page-favicon-updated event', function() { + return it('emits when favicon urls are received', function(done) { + webview.addEventListener('page-favicon-updated', function(e) { + var pageUrl; + assert.equal(e.favicons.length, 2); + pageUrl = process.platform === 'win32' ? 'file:///C:/favicon.png' : 'file:///favicon.png'; + assert.equal(e.favicons[0], pageUrl); + return done(); + }); + webview.src = "file://" + fixtures + "/pages/a.html"; + return document.body.appendChild(webview); + }); + }); + describe('will-navigate event', function() { + return it('emits when a url that leads to oustide of the page is clicked', function(done) { + webview.addEventListener('will-navigate', function(e) { + assert.equal(e.url, "http://host/"); + return done(); + }); + webview.src = "file://" + fixtures + "/pages/webview-will-navigate.html"; + return document.body.appendChild(webview); + }); + }); + describe('did-navigate event', function() { + var p, pageUrl; + p = path.join(fixtures, 'pages', 'webview-will-navigate.html'); + p = p.replace(/\\/g, '/'); + pageUrl = url.format({ + protocol: 'file', + slashes: true, + pathname: p + }); + return it('emits when a url that leads to outside of the page is clicked', function(done) { + webview.addEventListener('did-navigate', function(e) { + assert.equal(e.url, pageUrl); + return done(); + }); + webview.src = pageUrl; + return document.body.appendChild(webview); + }); + }); + describe('did-navigate-in-page event', function() { + it('emits when an anchor link is clicked', function(done) { + var p, pageUrl; + p = path.join(fixtures, 'pages', 'webview-did-navigate-in-page.html'); + p = p.replace(/\\/g, '/'); + pageUrl = url.format({ + protocol: 'file', + slashes: true, + pathname: p + }); + webview.addEventListener('did-navigate-in-page', function(e) { + assert.equal(e.url, pageUrl + "#test_content"); + return done(); + }); + webview.src = pageUrl; + return document.body.appendChild(webview); + }); + it('emits when window.history.replaceState is called', function(done) { + webview.addEventListener('did-navigate-in-page', function(e) { + assert.equal(e.url, "http://host/"); + return done(); + }); + webview.src = "file://" + fixtures + "/pages/webview-did-navigate-in-page-with-history.html"; + return document.body.appendChild(webview); + }); + return it('emits when window.location.hash is changed', function(done) { + var p, pageUrl; + p = path.join(fixtures, 'pages', 'webview-did-navigate-in-page-with-hash.html'); + p = p.replace(/\\/g, '/'); + pageUrl = url.format({ + protocol: 'file', + slashes: true, + pathname: p + }); + webview.addEventListener('did-navigate-in-page', function(e) { + assert.equal(e.url, pageUrl + "#test"); + return done(); + }); + webview.src = pageUrl; + return document.body.appendChild(webview); + }); + }); + describe('close event', function() { + return it('should fire when interior page calls window.close', function(done) { + webview.addEventListener('close', function() { + return done(); + }); + webview.src = "file://" + fixtures + "/pages/close.html"; + return document.body.appendChild(webview); + }); + }); + describe('devtools-opened event', function() { + return it('should fire when webview.openDevTools() is called', function(done) { + var listener; + listener = function() { + webview.removeEventListener('devtools-opened', listener); + webview.closeDevTools(); + return done(); + }; + webview.addEventListener('devtools-opened', listener); + webview.addEventListener('dom-ready', function() { + return webview.openDevTools(); + }); + webview.src = "file://" + fixtures + "/pages/base-page.html"; + return document.body.appendChild(webview); + }); + }); + describe('devtools-closed event', function() { + return it('should fire when webview.closeDevTools() is called', function(done) { + var listener, listener2; + listener2 = function() { + webview.removeEventListener('devtools-closed', listener2); + return done(); + }; + listener = function() { + webview.removeEventListener('devtools-opened', listener); + return webview.closeDevTools(); + }; + webview.addEventListener('devtools-opened', listener); + webview.addEventListener('devtools-closed', listener2); + webview.addEventListener('dom-ready', function() { + return webview.openDevTools(); + }); + webview.src = "file://" + fixtures + "/pages/base-page.html"; + return document.body.appendChild(webview); + }); + }); + describe('devtools-focused event', function() { + return it('should fire when webview.openDevTools() is called', function(done) { + var listener; + listener = function() { + webview.removeEventListener('devtools-focused', listener); + webview.closeDevTools(); + return done(); + }; + webview.addEventListener('devtools-focused', listener); + webview.addEventListener('dom-ready', function() { + return webview.openDevTools(); + }); + webview.src = "file://" + fixtures + "/pages/base-page.html"; + return document.body.appendChild(webview); + }); + }); + describe('.reload()', function() { + return it('should emit beforeunload handler', function(done) { + var listener, listener2; + listener = function(e) { + assert.equal(e.channel, 'onbeforeunload'); + webview.removeEventListener('ipc-message', listener); + return done(); + }; + listener2 = function(e) { + webview.reload(); + return webview.removeEventListener('did-finish-load', listener2); + }; + webview.addEventListener('ipc-message', listener); + webview.addEventListener('did-finish-load', listener2); + webview.setAttribute('nodeintegration', 'on'); + webview.src = "file://" + fixtures + "/pages/beforeunload-false.html"; + return document.body.appendChild(webview); + }); + }); + describe('.clearHistory()', function() { + return it('should clear the navigation history', function(done) { + var listener; + listener = function(e) { + assert.equal(e.channel, 'history'); + assert.equal(e.args[0], 2); + assert(webview.canGoBack()); + webview.clearHistory(); + assert(!webview.canGoBack()); + webview.removeEventListener('ipc-message', listener); + return done(); + }; + webview.addEventListener('ipc-message', listener); + webview.setAttribute('nodeintegration', 'on'); + webview.src = "file://" + fixtures + "/pages/history.html"; + return document.body.appendChild(webview); + }); + }); + describe('basic auth', function() { + var auth; + auth = require('basic-auth'); + return it('should authenticate with correct credentials', function(done) { + var message, server; + message = 'Authenticated'; + server = http.createServer(function(req, res) { + var credentials; + credentials = auth(req); + if (credentials.name === 'test' && credentials.pass === 'test') { + res.end(message); + } else { + res.end('failed'); + } + return server.close(); + }); + return server.listen(0, '127.0.0.1', function() { + var port; + port = server.address().port; + webview.addEventListener('ipc-message', function(e) { + assert.equal(e.channel, message); + return done(); + }); + webview.src = "file://" + fixtures + "/pages/basic-auth.html?port=" + port; + webview.setAttribute('nodeintegration', 'on'); + return document.body.appendChild(webview); + }); + }); + }); + describe('dom-ready event', function() { + return it('emits when document is loaded', function(done) { + var server; + server = http.createServer(function(req) {}); + return server.listen(0, '127.0.0.1', function() { + var port; + port = server.address().port; + webview.addEventListener('dom-ready', function() { + return done(); + }); + webview.src = "file://" + fixtures + "/pages/dom-ready.html?port=" + port; + return document.body.appendChild(webview); + }); + }); + }); + describe('executeJavaScript', function() { + if (process.env.TRAVIS !== 'true') { + return; + } + return it('should support user gesture', function(done) { + var listener, listener2; + listener = function(e) { + webview.removeEventListener('enter-html-full-screen', listener); + return done(); + }; + listener2 = function(e) { + var jsScript; + jsScript = 'document.getElementsByTagName("video")[0].webkitRequestFullScreen()'; + webview.executeJavaScript(jsScript, true); + return webview.removeEventListener('did-finish-load', listener2); + }; + webview.addEventListener('enter-html-full-screen', listener); + webview.addEventListener('did-finish-load', listener2); + webview.src = "file://" + fixtures + "/pages/fullscreen.html"; + return document.body.appendChild(webview); + }); + }); + describe('sendInputEvent', function() { + it('can send keyboard event', function(done) { + webview.addEventListener('ipc-message', function(e) { + assert.equal(e.channel, 'keyup'); + assert.deepEqual(e.args, [67, true, false]); + return done(); + }); + webview.addEventListener('dom-ready', function() { + return webview.sendInputEvent({ + type: 'keyup', + keyCode: 'c', + modifiers: ['shift'] + }); + }); + webview.src = "file://" + fixtures + "/pages/onkeyup.html"; + webview.setAttribute('nodeintegration', 'on'); + return document.body.appendChild(webview); + }); + return it('can send mouse event', function(done) { + webview.addEventListener('ipc-message', function(e) { + assert.equal(e.channel, 'mouseup'); + assert.deepEqual(e.args, [10, 20, false, true]); + return done(); + }); + webview.addEventListener('dom-ready', function() { + return webview.sendInputEvent({ + type: 'mouseup', + modifiers: ['ctrl'], + x: 10, + y: 20 + }); + }); + webview.src = "file://" + fixtures + "/pages/onmouseup.html"; + webview.setAttribute('nodeintegration', 'on'); + return document.body.appendChild(webview); + }); + }); + describe('media-started-playing media-paused events', function() { + return it('emits when audio starts and stops playing', function(done) { + var audioPlayed; + audioPlayed = false; + webview.addEventListener('media-started-playing', function() { + return audioPlayed = true; + }); + webview.addEventListener('media-paused', function() { + assert(audioPlayed); + return done(); + }); + webview.src = "file://" + fixtures + "/pages/audio.html"; + return document.body.appendChild(webview); + }); + }); + describe('found-in-page event', function() { + return it('emits when a request is made', function(done) { + var listener, listener2, requestId; + requestId = null; + listener = function(e) { + assert.equal(e.result.requestId, requestId); + if (e.result.finalUpdate) { + assert.equal(e.result.matches, 3); + webview.stopFindInPage("clearSelection"); + return done(); + } + }; + listener2 = function(e) { + return requestId = webview.findInPage("virtual"); + }; + webview.addEventListener('found-in-page', listener); + webview.addEventListener('did-finish-load', listener2); + webview.src = "file://" + fixtures + "/pages/content.html"; + return document.body.appendChild(webview); + }); + }); + return xdescribe('did-change-theme-color event', function() { + return it('emits when theme color changes', function(done) { + webview.addEventListener('did-change-theme-color', function(e) { + return done(); + }); + webview.src = "file://" + fixtures + "/pages/theme-color.html"; + return document.body.appendChild(webview); + }); + }); +});