test: refactor with minor fixes & improvements (#131)

* test (handler): refactor and minor fix
* test (parser): refactor and minor fix
* test (update): refactor
* test (PackageJsonParser): refactor and improvement
* test (ManifestJsonParser): refactor and improvement
* test (Api): refactor and improvements
* test (create): refactor and improvements
* test (nyc configure): require 100% coverage
* refactor: apply suggestions from code review

Co-Authored-By: Raphael von der Grün <raphinesse@gmail.com>
diff --git a/bin/lib/create.js b/bin/lib/create.js
index 7c3f1b2..3b95999 100644
--- a/bin/lib/create.js
+++ b/bin/lib/create.js
@@ -36,9 +36,6 @@
     events.emit('log', '\tPath: ' + platform_dir);
     events.emit('log', '\tName: ' + project_name);
 
-    // Set default values for path, package and name
-    platform_dir = platform_dir || 'CordovaExample';
-
     // Check if project already exists
     if (fs.existsSync(platform_dir)) {
         events.emit('error', 'Oops, destination already exists! Delete it and try again');
diff --git a/bin/templates/cordova/Api.js b/bin/templates/cordova/Api.js
index 9198966..b705f08 100644
--- a/bin/templates/cordova/Api.js
+++ b/bin/templates/cordova/Api.js
@@ -23,16 +23,14 @@
 */
 const path = require('path');
 const fs = require('fs-extra');
-const CordovaCommon = require('cordova-common');
-const CordovaLogger = CordovaCommon.CordovaLogger;
-// const ConfigParser = CordovaCommon.ConfigParser;
-const ActionStack = CordovaCommon.ActionStack;
-const selfEvents = CordovaCommon.events;
-// const xmlHelpers = CordovaCommon.xmlHelpers;
-const PlatformJson = CordovaCommon.PlatformJson;
-const PlatformMunger = CordovaCommon.ConfigChanges.PlatformMunger;
-const PluginInfoProvider = CordovaCommon.PluginInfoProvider;
-
+const {
+    ActionStack,
+    ConfigChanges: { PlatformMunger },
+    CordovaLogger,
+    events: selfEvents,
+    PlatformJson,
+    PluginInfoProvider
+} = require('cordova-common');
 const Parser = require('./parser');
 
 function setupEvents (externalEventEmitter) {
@@ -95,7 +93,7 @@
 
     addPlugin (pluginInfo, installOptions) {
         if (!pluginInfo) {
-            return Promise.reject(new Error('The parameter is incorrect. The first parameter should be valid PluginInfo instance'));
+            return Promise.reject(new Error('Missing plugin info parameter. The first parameter should contain a valid PluginInfo instance.'));
         }
 
         installOptions = installOptions || {};
@@ -143,7 +141,11 @@
             });
     }
 
-    removePlugin (plugin, uninstallOptions) {
+    removePlugin (pluginInfo, uninstallOptions) {
+        if (!pluginInfo) {
+            return Promise.reject(new Error('Missing plugin info parameter. The first parameter should contain a valid PluginInfo instance.'));
+        }
+
         uninstallOptions = uninstallOptions || {};
         // CB-10108 platformVersion option is required for proper plugin installation
         uninstallOptions.platformVersion = uninstallOptions.platformVersion || this.getPlatformInfo().version;
@@ -151,18 +153,18 @@
         const actions = new ActionStack();
 
         let platform = this.platform;
-        if (!plugin.getPlatformsArray().includes(platform)) { // if `cordova-electron` is not defined in plugin.xml, `browser` is used instead.
+        if (!pluginInfo.getPlatformsArray().includes(platform)) { // if `cordova-electron` is not defined in plugin.xml, `browser` is used instead.
             platform = 'browser';
         }
 
         // queue up plugin files
-        plugin.getFilesAndFrameworks(platform)
-            .concat(plugin.getAssets(platform))
-            .concat(plugin.getJsModules(platform))
+        pluginInfo.getFilesAndFrameworks(platform)
+            .concat(pluginInfo.getAssets(platform))
+            .concat(pluginInfo.getJsModules(platform))
             .forEach((item) => {
                 actions.push(actions.createAction(
-                    this._getUninstaller(item.itemType), [item, plugin.dir, plugin.id, uninstallOptions],
-                    this._getInstaller(item.itemType), [item, plugin.dir, plugin.id, uninstallOptions]));
+                    this._getUninstaller(item.itemType), [item, pluginInfo.dir, pluginInfo.id, uninstallOptions],
+                    this._getInstaller(item.itemType), [item, pluginInfo.dir, pluginInfo.id, uninstallOptions]));
             });
 
         // run through the action stack
@@ -171,17 +173,17 @@
                 this._munger
                     // Ignore passed `is_top_level` option since platform itself doesn't know
                     // anything about managing dependencies - it's responsibility of caller.
-                    .remove_plugin_changes(plugin, /* is_top_level= */true)
+                    .remove_plugin_changes(pluginInfo, /* is_top_level= */true)
                     .save_all();
 
                 const targetDir = uninstallOptions.usePlatformWww
                     ? this.getPlatformInfo().locations.platformWww
                     : this.getPlatformInfo().locations.www;
 
-                this._removeModulesInfo(plugin, targetDir);
+                this._removeModulesInfo(pluginInfo, targetDir);
                 // Remove stale plugin directory
                 // @todo this should be done by plugin files uninstaller
-                fs.removeSync(path.resolve(this.root, 'Plugins', plugin.id));
+                fs.removeSync(path.resolve(this.root, 'Plugins', pluginInfo.id));
             });
     }
 
diff --git a/bin/templates/cordova/handler.js b/bin/templates/cordova/handler.js
index 8d1e810..a1275c6 100644
--- a/bin/templates/cordova/handler.js
+++ b/bin/templates/cordova/handler.js
@@ -19,7 +19,7 @@
 
 const path = require('path');
 const fs = require('fs-extra');
-const events = require('cordova-common').events;
+const { events } = require('cordova-common');
 
 module.exports = {
     www_dir: (project_dir) => path.join(project_dir, 'www'),
@@ -77,12 +77,12 @@
         install: (obj, plugin_dir, project_dir, plugin_id, options) => {
             // var dest = path.join(obj.targetDir, path.basename(obj.src));
             // common.copyFile(plugin_dir, obj.src, project_dir, dest);
-            events.emit('verbose', 'source-file.install is currently not supported for electron');
+            events.emit('verbose', 'source-file.install is not supported for electron');
         },
         uninstall: (obj, project_dir, plugin_id, options) => {
             // var dest = path.join(obj.targetDir, path.basename(obj.src));
             // common.removeFile(project_dir, dest);
-            events.emit('verbose', 'source-file.uninstall is currently not supported for electron');
+            events.emit('verbose', 'source-file.uninstall is not supported for electron');
         }
     },
     'header-file': {
@@ -123,7 +123,7 @@
             const dest = path.join(wwwDest, asset.target);
             const destDir = path.parse(dest).dir;
 
-            if (destDir) fs.ensureDirSync(destDir);
+            fs.ensureDirSync(destDir);
             fs.copySync(src, dest);
         },
         uninstall: (asset, wwwDest, plugin_id) => {
diff --git a/bin/templates/cordova/lib/ManifestJsonParser.js b/bin/templates/cordova/lib/ManifestJsonParser.js
index 1b4989d..6b17088 100644
--- a/bin/templates/cordova/lib/ManifestJsonParser.js
+++ b/bin/templates/cordova/lib/ManifestJsonParser.js
@@ -61,7 +61,7 @@
                 sizes: `${icon.width}x${icon.height}`
             }));
 
-        if (icons) this.manifest.icons = icons;
+        if (icons.length) this.manifest.icons = icons;
 
         return this;
     }
diff --git a/bin/templates/cordova/parser.js b/bin/templates/cordova/parser.js
index 87bd8cd..4ee2ba2 100644
--- a/bin/templates/cordova/parser.js
+++ b/bin/templates/cordova/parser.js
@@ -44,15 +44,6 @@
         return path.join(this.path, 'www');
     }
 
-    // Used for creating platform_www in projects created by older versions.
-    // cordovajs_path (libDir) {
-    //     return path.resolve(path.join(libDir, 'cordova-lib', 'cordova.js'));
-    // }
-
-    // cordovajs_src_path (libDir) {
-    //     return path.resolve(path.join(libDir, 'cordova-js-src'));
-    // }
-
     // Replace the www dir with contents of platform_www and app www.
     update_www (cordovaProject, opts) {
         const platform_www = path.join(this.path, 'platform_www');
@@ -77,8 +68,6 @@
         FileUpdater.mergeAndUpdateDir(sourceDirs, targetDir, { rootDir: cordovaProject.root }, logFileOp);
     }
 
-    // update_overrides () {}
-
     config_xml () {
         return path.join(this.path, 'config.xml');
     }
diff --git a/package.json b/package.json
index e4622e7..c10c15f 100644
--- a/package.json
+++ b/package.json
@@ -51,6 +51,12 @@
   },
   "nyc": {
     "all": true,
+    "check-coverage": true,
+    "per-file": true,
+    "lines": 100,
+    "statements": 100,
+    "functions": 100,
+    "branches": 100,
     "exclude": [
       ".npm-scripts/",
       "bin/templates/build-res/",
diff --git a/tests/spec/fixtures/test-browser-plugin/plugin.xml b/tests/spec/fixtures/test-browser-plugin/plugin.xml
new file mode 100644
index 0000000..2760357
--- /dev/null
+++ b/tests/spec/fixtures/test-browser-plugin/plugin.xml
@@ -0,0 +1,35 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!--
+    Licensed to the Apache Software Foundation (ASF) under one
+    or more contributor license agreements.  See the NOTICE file
+    distributed with this work for additional information
+    regarding copyright ownership.  The ASF licenses this file
+    to you under the Apache License, Version 2.0 (the
+    "License"); you may not use this file except in compliance
+    with the License.  You may obtain a copy of the License at
+
+    http://www.apache.org/licenses/LICENSE-2.0
+
+    Unless required by applicable law or agreed to in writing,
+    software distributed under the License is distributed on an
+    "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+    KIND, either express or implied.  See the License for the
+    specific language governing permissions and limitations
+    under the License.
+-->
+
+<plugin xmlns="http://cordova.apache.org/ns/plugins/1.0"
+    xmlns:android="http://schemas.android.com/apk/res/android"
+    id="org.apache.testbrowserplugin"
+    version="1.0.0">
+
+    <name>Test Browser Plugin</name>
+
+    <platform name="browser">
+        <js-module src="www/plugin.js" name="TestBrowserPlugin">
+            <clobbers target="TestBrowserPlugin.clobbers" />
+            <merges target="TestBrowserPlugin.merges" />
+            <runs />
+        </js-module>
+    </platform>
+</plugin>
diff --git a/tests/spec/fixtures/test-browser-plugin/www/plugin.js b/tests/spec/fixtures/test-browser-plugin/www/plugin.js
new file mode 100644
index 0000000..2befe6a
--- /dev/null
+++ b/tests/spec/fixtures/test-browser-plugin/www/plugin.js
@@ -0,0 +1,26 @@
+/*
+ *
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License.  You may obtain a copy of the License at
+ *
+ *   http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied.  See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ *
+*/
+
+class TestBrowserPlugin {
+
+}
+
+module.exports = new TestBrowserPlugin();
diff --git a/tests/spec/fixtures/test-config-no-author-custom-email.xml b/tests/spec/fixtures/test-config-no-author-custom-email.xml
new file mode 100644
index 0000000..b1d3f0a
--- /dev/null
+++ b/tests/spec/fixtures/test-config-no-author-custom-email.xml
@@ -0,0 +1,57 @@
+<?xml version='1.0' encoding='utf-8'?>
+<!--
+  Licensed to the Apache Software Foundation (ASF) under one
+  or more contributor license agreements.  See the NOTICE file
+  distributed with this work for additional information
+  regarding copyright ownership.  The ASF licenses this file
+  to you under the Apache License, Version 2.0 (the
+  "License"); you may not use this file except in compliance
+  with the License.  You may obtain a copy of the License at
+
+    http://www.apache.org/licenses/LICENSE-2.0
+
+  Unless required by applicable law or agreed to in writing,
+  software distributed under the License is distributed on an
+  "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+  KIND, either express or implied.  See the License for the
+  specific language governing permissions and limitations
+  under the License.
+-->
+
+<widget id="whatever" version="1.1.1" xmlns="http://www.w3.org/ns/widgets" xmlns:cdv="http://cordova.apache.org/ns/1.0">
+    <name>HelloWorld</name>
+    <description>
+        A sample Apache Cordova application.
+    </description>
+    <author email="dev@cordova.com" href="http://cordova.io"></author>
+    <license>Apache 2.0 License</license>
+    <preference name="Orientation" value="portrait" />
+    <content src="index.html" main="index.js" />
+    <plugin name="cordova-plugin-whitelist" spec="1" />
+    <access origin="*" />
+    <allow-intent href="http://*/*" />
+    <allow-intent href="https://*/*" />
+    <allow-intent href="tel:*" />
+    <allow-intent href="sms:*" />
+    <allow-intent href="mailto:*" />
+    <allow-intent href="geo:*" />
+    <platform name="android">
+        <allow-intent href="market:*" />
+    </platform>
+    <platform name="ios">
+        <allow-intent href="itms:*" />
+        <allow-intent href="itms-apps:*" />
+    </platform>
+    <icon src="res/electron/cordova.png"  width="16" height="16"/>
+    <platform name="electron">
+        <icon src="res/electron/cordova.png"/>
+        <icon src="res/electron/cordova@1.5.png" />
+        <icon src="res/electron/cordova@2.png" />
+        <icon src="res/electron/cordova@3.png" />
+        <icon src="res/electron/cordova@4.png" />
+        <icon src="res/electron/cordova@8.png" />
+        <icon src="res/electron/cordova@16.png" />
+        <icon src="res/electron/cordova_512.png" target="installer" />
+    </platform>
+    <engine name="electron" spec="github:Apache/cordova-electron" />
+</widget>
diff --git a/tests/spec/fixtures/test-non-electron-plugin/plugin.xml b/tests/spec/fixtures/test-non-electron-plugin/plugin.xml
new file mode 100644
index 0000000..4f123cc
--- /dev/null
+++ b/tests/spec/fixtures/test-non-electron-plugin/plugin.xml
@@ -0,0 +1,29 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!--
+    Licensed to the Apache Software Foundation (ASF) under one
+    or more contributor license agreements.  See the NOTICE file
+    distributed with this work for additional information
+    regarding copyright ownership.  The ASF licenses this file
+    to you under the Apache License, Version 2.0 (the
+    "License"); you may not use this file except in compliance
+    with the License.  You may obtain a copy of the License at
+
+    http://www.apache.org/licenses/LICENSE-2.0
+
+    Unless required by applicable law or agreed to in writing,
+    software distributed under the License is distributed on an
+    "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+    KIND, either express or implied.  See the License for the
+    specific language governing permissions and limitations
+    under the License.
+-->
+
+<plugin xmlns="http://cordova.apache.org/ns/plugins/1.0"
+    xmlns:android="http://schemas.android.com/apk/res/android"
+    id="org.apache.testnonelectronplugin"
+    version="1.0.0">
+
+    <name>Test Non Electron Plugin</name>
+
+    <platform name="android"></platform>
+</plugin>
diff --git a/tests/spec/fixtures/testplugin-empty-jsmodule/plugin.xml b/tests/spec/fixtures/testplugin-empty-jsmodule/plugin.xml
new file mode 100644
index 0000000..4d5f697
--- /dev/null
+++ b/tests/spec/fixtures/testplugin-empty-jsmodule/plugin.xml
@@ -0,0 +1,32 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!--
+    Licensed to the Apache Software Foundation (ASF) under one
+    or more contributor license agreements.  See the NOTICE file
+    distributed with this work for additional information
+    regarding copyright ownership.  The ASF licenses this file
+    to you under the Apache License, Version 2.0 (the
+    "License"); you may not use this file except in compliance
+    with the License.  You may obtain a copy of the License at
+
+    http://www.apache.org/licenses/LICENSE-2.0
+
+    Unless required by applicable law or agreed to in writing,
+    software distributed under the License is distributed on an
+    "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+    KIND, either express or implied.  See the License for the
+    specific language governing permissions and limitations
+    under the License.
+-->
+
+<plugin xmlns="http://cordova.apache.org/ns/plugins/1.0"
+    xmlns:android="http://schemas.android.com/apk/res/android"
+    id="org.apache.testplugin2"
+    version="1.0.0">
+
+    <name>Test Electron Plugin</name>
+
+    <platform name="electron">
+      <js-module src="www/MyTestPlugin2.js">
+      </js-module>
+    </platform>
+</plugin>
diff --git a/tests/spec/fixtures/testplugin-empty-jsmodule/www/MyTestPlugin2.js b/tests/spec/fixtures/testplugin-empty-jsmodule/www/MyTestPlugin2.js
new file mode 100644
index 0000000..f9c22c7
--- /dev/null
+++ b/tests/spec/fixtures/testplugin-empty-jsmodule/www/MyTestPlugin2.js
@@ -0,0 +1,26 @@
+/*
+ *
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License.  You may obtain a copy of the License at
+ *
+ *   http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied.  See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ *
+*/
+
+class TestPlugin2 {
+
+}
+
+module.exports = new TestPlugin2();
diff --git a/tests/spec/fixtures/testplugin/plugin.xml b/tests/spec/fixtures/testplugin/plugin.xml
new file mode 100644
index 0000000..f6878aa
--- /dev/null
+++ b/tests/spec/fixtures/testplugin/plugin.xml
@@ -0,0 +1,39 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!--
+    Licensed to the Apache Software Foundation (ASF) under one
+    or more contributor license agreements.  See the NOTICE file
+    distributed with this work for additional information
+    regarding copyright ownership.  The ASF licenses this file
+    to you under the Apache License, Version 2.0 (the
+    "License"); you may not use this file except in compliance
+    with the License.  You may obtain a copy of the License at
+
+    http://www.apache.org/licenses/LICENSE-2.0
+
+    Unless required by applicable law or agreed to in writing,
+    software distributed under the License is distributed on an
+    "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+    KIND, either express or implied.  See the License for the
+    specific language governing permissions and limitations
+    under the License.
+-->
+
+<plugin xmlns="http://cordova.apache.org/ns/plugins/1.0"
+    xmlns:android="http://schemas.android.com/apk/res/android"
+    id="org.apache.testplugin"
+    version="1.0.0">
+
+    <name>Test Electron Plugin</name>
+
+    <platform name="electron">
+      <js-module src="www/plugin.js" name="TestPlugin">
+          <clobbers target="TestPlugin.clobbers" />
+          <merges target="TestPlugin.merges" />
+          <runs />
+      </js-module>
+
+      <asset src="src/electron/sample.json" target="js/electron.json" />
+
+      <source-file src="src/electron/sample.json" target-dir="js" />
+    </platform>
+</plugin>
diff --git a/tests/spec/unit/Api.spec.js b/tests/spec/unit/Api.spec.js
index 48a2574..7a148d0 100644
--- a/tests/spec/unit/Api.spec.js
+++ b/tests/spec/unit/Api.spec.js
@@ -20,12 +20,20 @@
 const fs = require('fs-extra');
 const path = require('path');
 const rewire = require('rewire');
+const { events, PluginInfo } = require('cordova-common');
+
 const templateDir = path.resolve(__dirname, '..', '..', '..', 'bin', 'templates');
+
+const create = require(path.join(templateDir, '../lib/create'));
 const Api = rewire(path.join(templateDir, 'cordova', 'Api'));
+
 const tmpDir = path.join(__dirname, '../../../temp');
 const apiRequire = Api.__get__('require');
 const FIXTURES = path.join(__dirname, '..', 'fixtures');
 const pluginFixture = path.join(FIXTURES, 'testplugin');
+const pluginFixtureEmptyJSModule = path.join(FIXTURES, 'testplugin-empty-jsmodule');
+const pluginNotElectronFixture = path.join(FIXTURES, 'test-non-electron-plugin');
+const pluginBrowserFixture = path.join(FIXTURES, 'test-browser-plugin');
 const testProjectDir = path.join(tmpDir, 'testapp');
 
 function dirExists (dir) {
@@ -36,60 +44,45 @@
     return fs.existsSync(file) && fs.statSync(file).isFile();
 }
 
-function readJson (file) {
-    return JSON.parse(
-        fs.readFileSync(
-            file
-        )
-    );
-}
-
-function writeJson (file, json) {
-    fs.writeFileSync(
-        file,
-        JSON.stringify(json, null, '  '),
-        'utf-8'
-    );
-}
+const mockExpectedLocations = {
+    platformRootDir: testProjectDir,
+    root: testProjectDir,
+    www: path.join(testProjectDir, 'www'),
+    res: path.join(testProjectDir, 'res'),
+    platformWww: path.join(testProjectDir, 'platform_www'),
+    configXml: path.join(testProjectDir, 'config.xml'),
+    defaultConfigXml: path.join(testProjectDir, 'cordova/defaults.xml'),
+    build: path.join(testProjectDir, 'build'),
+    buildRes: path.join(testProjectDir, 'build-res'),
+    cache: path.join(testProjectDir, 'cache'),
+    cordovaJs: 'bin/templates/project/assets/www/cordova.js',
+    cordovaJsSrc: 'cordova-js-src'
+};
 
 describe('Api class', () => {
-    fs.ensureDirSync(tmpDir);
-    fs.copySync(path.resolve(FIXTURES, 'testapp'), path.resolve(tmpDir, 'testapp'));
+    let api;
+    let apiEvents;
 
-    const api = new Api(null, testProjectDir);
-    const apiEvents = Api.__get__('selfEvents');
-    apiEvents.removeAllListeners();
+    beforeAll(() => {
+        fs.ensureDirSync(tmpDir);
+        fs.copySync(path.resolve(FIXTURES, 'testapp'), path.resolve(tmpDir, 'testapp'));
 
-    const rootDir = testProjectDir;
-    const mockExpectedLocations = {
-        platformRootDir: rootDir,
-        root: rootDir,
-        www: path.join(rootDir, 'www'),
-        res: path.join(rootDir, 'res'),
-        platformWww: path.join(rootDir, 'platform_www'),
-        configXml: path.join(rootDir, 'config.xml'),
-        defaultConfigXml: path.join(rootDir, 'cordova/defaults.xml'),
-        build: path.join(rootDir, 'build'),
-        buildRes: path.join(rootDir, 'build-res'),
-        cache: path.join(rootDir, 'cache'),
-        cordovaJs: 'bin/templates/project/assets/www/cordova.js',
-        cordovaJsSrc: 'cordova-js-src'
-    };
-
-    it('should exist', () => {
-        expect(Api).toBeDefined();
+        apiEvents = Api.__get__('selfEvents');
+        apiEvents.addListener('verbose', (data) => { });
     });
 
-    describe('Api constructor', () => {
-        it('should be ale to construct.', () => {
+    afterAll(() => {
+        fs.removeSync(tmpDir);
+        apiEvents.removeAllListeners();
+    });
+
+    beforeEach(() => {
+        api = new Api(null, testProjectDir);
+    });
+
+    describe('constructor', () => {
+        it('should have been constructed with initial values.', () => {
             expect(api).toBeDefined();
-        });
-
-        it('should have set platform as electron.', () => {
-            expect(api.platform).toBe('electron');
-        });
-
-        it('should have set the root path.', () => {
             /**
              * In Unit Testing:
              *   The API file path is located in "cordova-electron/bin/templates/cordova".
@@ -99,397 +92,333 @@
              *   The API file path is actually located in "<project_dir>/platforms/electron/cordova".
              *   The expected path is "<project_dir>/platforms/electron" which is the electron's platform root dir
              */
-            expect(api.root).toBe(rootDir);
-        });
-
-        it('should configure proper locations.', () => {
-            expect(api.locations).toEqual(mockExpectedLocations);
+            expect(api.root).toEqual(testProjectDir);
+            expect(api.locations).toEqual(jasmine.objectContaining(mockExpectedLocations));
         });
     });
 
     describe('getPlatformInfo method', () => {
-        beforeEach(() => {
+        it('should return object containing platform information', () => {
             // Mocking require that is called to get version.
             Api.__set__('require', () => '1.0.0');
-        });
 
-        afterEach(() => {
-            Api.__set__('require', apiRequire);
-        });
-
-        it('should return object containing platform information', () => {
-            const actual = api.getPlatformInfo();
-            const expected = {
+            expect(api.getPlatformInfo()).toEqual({
                 locations: mockExpectedLocations,
-                root: rootDir,
+                root: testProjectDir,
                 name: 'electron',
                 version: '1.0.0',
                 projectConfig: undefined
-            };
-            expect(actual).toEqual(expected);
+            });
+
+            Api.__set__('require', apiRequire);
         });
     });
 
     describe('prepare method', () => {
-        const prepareSpy = jasmine.createSpy('prepare');
-        beforeEach(() => {
-            // Mocking require that is called to get version.
-            Api.__set__('require', () => ({
-                prepare: prepareSpy
-            }));
-        });
-
-        afterEach(() => {
-            Api.__set__('require', apiRequire);
-        });
-
         it('should return object containing platform information', () => {
+            const prepare = jasmine.createSpy('prepare');
+
+            Api.__set__('require', () => ({ prepare }));
             api.prepare('', {});
-            expect(prepareSpy).toHaveBeenCalledWith(jasmine.any(String), jasmine.any(Object));
+            expect(prepare).toHaveBeenCalledWith(jasmine.any(String), jasmine.any(Object));
+            Api.__set__('require', apiRequire);
         });
     });
 
     describe('addPlugin method', () => {
-        let logs = {};
         beforeEach(() => {
+            spyOn(events, 'emit');
+
             fs.removeSync(path.resolve(testProjectDir, 'electron.json'));
             fs.removeSync(path.resolve(testProjectDir, 'www'));
-            apiEvents.addListener('verbose', (data) => {
-                logs.verbose.push(data);
-            });
-            logs = {
-                verbose: []
-            };
         });
 
         afterEach(() => {
+            fs.removeSync(path.resolve(testProjectDir, 'electron.json'));
+            fs.removeSync(path.resolve(testProjectDir, 'www'));
+
             apiEvents.removeAllListeners();
         });
 
-        it('should reject when missing plugin information', () => {
-            api.addPlugin().then(
+        it('should Promise reject when missing PluginInfo parameter.', () => {
+            return api.addPlugin().then(
+                () => {},
+                error => {
+                    expect(error).toEqual(new Error('Missing plugin info parameter. The first parameter should contain a valid PluginInfo instance.'));
+                }
+            );
+        });
+
+        it('should error out when a PluginInfo parameter is not valid', () => {
+            class FakePluginInfo {}
+            return expect(() => {
+                api.addPlugin(new FakePluginInfo());
+            }).toThrowError();
+        });
+
+        describe('Use Electron Plugin with Default Install Options', () => {
+            beforeEach(() => {
+                const pluginInfo = new PluginInfo(pluginFixture);
+                return api.addPlugin(pluginInfo);
+            });
+
+            it('should add the plugins assets and meta data.', () => {
+                expect(dirExists(path.resolve(testProjectDir, 'www'))).toBeTruthy();
+                expect(fileExists(path.resolve(testProjectDir, 'electron.json'))).toBeTruthy();
+                expect(fileExists(path.resolve(testProjectDir, 'www/cordova_plugins.js'))).toBeTruthy();
+                expect(fileExists(path.resolve(testProjectDir, 'www/js/electron.json'))).toBeTruthy();
+                expect(fileExists(path.resolve(testProjectDir, 'www/plugins/org.apache.testplugin/www/plugin.js'))).toBeTruthy();
+            });
+
+            it('should emit that source-file is not supported.', () => {
+                expect(events.emit).toHaveBeenCalledWith(
+                    'verbose',
+                    jasmine.stringMatching(/not supported/)
+                );
+            });
+
+            it('should add a second plugins assets', () => {
+                const pluginInfo2 = new PluginInfo(pluginBrowserFixture);
+                return api.addPlugin(pluginInfo2).then(() => {
+                    expect(fileExists(path.resolve(testProjectDir, 'www/plugins/org.apache.testbrowserplugin/www/plugin.js'))).toBeTruthy();
+                });
+            });
+
+            it('should have "clobber", "merge", and "run" set when defined in "js-module".', () => {
+                const { modules } = fs.readJsonSync(path.resolve(testProjectDir, 'electron.json'));
+                expect(modules[0].clobbers).toBeDefined();
+                expect(modules[0].merges).toBeDefined();
+                expect(modules[0].runs).toBeDefined();
+            });
+
+            it('should have module id containing the name attribute value.', () => {
+                const { modules } = fs.readJsonSync(path.resolve(testProjectDir, 'electron.json'));
+                expect(modules[0].id).toBe('org.apache.testplugin.TestPlugin');
+            });
+        });
+
+        // usePlatformWww
+        describe('Use Empty JS-Module Plugin', () => {
+            beforeEach(() => {
+                const pluginInfo = new PluginInfo(pluginFixtureEmptyJSModule);
+                return api.addPlugin(pluginInfo);
+            });
+
+            it('should not have "clobber", "merge", and "run" set when not defined in "js-module".', () => {
+                const { modules } = fs.readJsonSync(path.resolve(testProjectDir, 'electron.json'));
+                expect(modules[0].clobbers).not.toBeDefined();
+                expect(modules[0].merges).not.toBeDefined();
+                expect(modules[0].runs).not.toBeDefined();
+            });
+
+            it('should use js filename for plugin id if name is missing.', () => {
+                const { modules } = fs.readJsonSync(path.resolve(testProjectDir, 'electron.json'));
+                expect(modules[0].id).toBe('org.apache.testplugin2.MyTestPlugin2');
+            });
+        });
+
+        describe('Use Electron Plugin with Custom Install Options', () => {
+            beforeEach(() => {
+                const pluginInfo = new PluginInfo(pluginFixture);
+                return api.addPlugin(pluginInfo, {
+                    variables: { PACKAGE_NAME: 'com.foobar.newpackagename' },
+                    usePlatformWww: true
+                });
+            });
+
+            it('should use custom package name.', () => {
+                const { installed_plugins } = fs.readJsonSync(path.resolve(testProjectDir, 'electron.json'));
+                expect(installed_plugins['org.apache.testplugin'].PACKAGE_NAME).toEqual('com.foobar.newpackagename');
+            });
+
+            it('should use platform www instead of www.', () => {
+                expect(fileExists(path.resolve(testProjectDir, 'www/cordova_plugins.js'))).toBeFalsy();
+                expect(fileExists(path.resolve(testProjectDir, 'platform_www/cordova_plugins.js'))).toBeTruthy();
+            });
+        });
+
+        /**
+         * @todo verfiy validity of an "unknown" itemType acgtually being able to be set.
+         */
+        it('should warn when unknown itemType is added.', () => {
+            const pluginInfo = new PluginInfo(pluginFixture);
+            pluginInfo.getAssets = () => [{ itemType: 'unknown' }];
+
+            return api.addPlugin(pluginInfo).then(
                 () => {
-                    fail('Unwanted code branch');
-                },
-                (error) => {
-                    expect(error).toEqual(new Error('The parameter is incorrect. The first parameter should be valid PluginInfo instance'));
+                    expect(events.emit).toHaveBeenCalledWith(
+                        'warn',
+                        jasmine.stringMatching(/Unrecognized type/)
+                    );
                 }
             );
         });
 
-        it('empty plugin', () => {
-            return api.addPlugin({
-                id: 'empty_plugin',
-                getPlatformsArray: () => ['electron'],
-                getFilesAndFrameworks: platform => [],
-                getAssets: platform => [],
-                getJsModules: platform => [],
-                getConfigFiles: platform => []
-            }, { }).then(
-                (result) => {
-                    expect(result).not.toBeDefined();
+        it('should add browser plugins as well.', () => {
+            const pluginInfo = new PluginInfo(pluginBrowserFixture);
+            return api.addPlugin(pluginInfo).then(
+                () => {
                     expect(dirExists(path.resolve(testProjectDir, 'www'))).toBeTruthy();
                     expect(fileExists(path.resolve(testProjectDir, 'electron.json'))).toBeTruthy();
                     expect(fileExists(path.resolve(testProjectDir, 'www', 'cordova_plugins.js'))).toBeTruthy();
-                },
-                (error) => {
-                    fail(`Unwanted code branch: ${error}`);
-                }
-            );
-        });
-
-        it('asset plugin', () => {
-            return api.addPlugin({
-                id: 'asset-plugin',
-                dir: pluginFixture,
-                getPlatformsArray: () => ['electron'],
-                getFilesAndFrameworks: platform => [],
-                getAssets: platform => [{
-                    itemType: 'asset',
-                    src: 'src/electron/sample.json',
-                    target: 'js/sample.json'
-                }],
-                getJsModules: platform => [],
-                getConfigFiles: platform => []
-            }, { }).then(
-                (result) => {
-                    expect(result).not.toBeDefined();
-                    expect(fileExists(path.resolve(testProjectDir, 'www', 'js', 'sample.json'))).toBeTruthy();
-                    expect(readJson(path.resolve(testProjectDir, 'www', 'js', 'sample.json')).title).toEqual('sample');
-                },
-                (error) => {
-                    fail(`Unwanted code branch: ${error}`);
-                }
-            );
-        });
-
-        it('js-module plugin', () => {
-            return api.addPlugin({
-                id: 'module-plugin',
-                dir: pluginFixture,
-                getPlatformsArray: () => ['electron'],
-                getFilesAndFrameworks: platform => [],
-                getAssets: platform => [],
-                getJsModules: platform => [{
-                    itemType: 'js-module',
-                    name: 'testmodule',
-                    src: 'www/plugin.js',
-                    clobbers: ['ModulePlugin.clobbers'],
-                    merges: ['ModulePlugin.merges'],
-                    runs: true
-                }],
-                getConfigFiles: platform => []
-            }, { }).then(
-                (result) => {
-                    expect(result).not.toBeDefined();
-                    expect(fileExists(path.resolve(testProjectDir, 'www', 'plugins', 'module-plugin', 'www', 'plugin.js'))).toBeTruthy();
-                },
-                (error) => {
-                    fail(`Unwanted code branch: ${error}`);
-                }
-            );
-        });
-
-        it('unrecognized type plugin', () => {
-            const _events = api.events;
-            const emitSpy = jasmine.createSpy('emit');
-            api.events = {
-                emit: emitSpy
-            };
-
-            return api.addPlugin({
-                id: 'unrecognized-plugin',
-                dir: pluginFixture,
-                getPlatformsArray: () => ['electron'],
-                getFilesAndFrameworks: platform => [],
-                getAssets: platform => [{
-                    itemType: 'unrecognized'
-                }],
-                getJsModules: platform => [],
-                getConfigFiles: platform => []
-            }, { }).then(
-                (result) => {
-                    expect(emitSpy.calls.argsFor(0)[1]).toContain('unrecognized');
-                    expect(result).not.toBeDefined();
-                    api.events = _events;
-                },
-                (error) => {
-                    fail(`Unwanted code branch: ${error}`);
-                    api.events = _events;
-                }
-            );
-        });
-
-        it('source-file type plugin', () => {
-            return api.addPlugin({
-                id: 'source-file-plugin',
-                dir: pluginFixture,
-                getPlatformsArray: () => ['electron'],
-                getFilesAndFrameworks: platform => [],
-                getAssets: platform => [{
-                    itemType: 'source-file'
-                }],
-                getJsModules: platform => [],
-                getConfigFiles: platform => []
-            }, { }).then(
-                (result) => {
-                    expect(result).not.toBeDefined();
-                    expect(logs.verbose.some(message => message === 'source-file.install is currently not supported for electron')).toBeTruthy();
-                },
-                (error) => {
-                    fail(`Unwanted code branch: ${error}`);
-                }
-            );
-        });
-
-        it('empty plugin with browser platform', () => {
-            return api.addPlugin({
-                id: 'empty_plugin',
-                getPlatformsArray: () => ['browser'],
-                getFilesAndFrameworks: platform => [],
-                getAssets: platform => [],
-                getJsModules: platform => [],
-                getConfigFiles: platform => []
-            }, { }).then(
-                (result) => {
-                    expect(result).not.toBeDefined();
-                    expect(dirExists(path.resolve(testProjectDir, 'www'))).toBeTruthy();
-                    expect(fileExists(path.resolve(testProjectDir, 'electron.json'))).toBeTruthy();
-                    expect(fileExists(path.resolve(testProjectDir, 'www', 'cordova_plugins.js'))).toBeTruthy();
-                },
-                (error) => {
-                    fail(`Unwanted code branch: ${error}`);
                 }
             );
         });
     });
 
-    /**
-     * @todo Add useful tests.
-     */
     describe('removePlugin method', () => {
-        let logs = {};
         beforeEach(() => {
+            spyOn(events, 'emit');
+
             fs.removeSync(path.resolve(testProjectDir, 'electron.json'));
             fs.removeSync(path.resolve(testProjectDir, 'www'));
-            apiEvents.addListener('verbose', (data) => {
-                logs.verbose.push(data);
-            });
-            logs = {
-                verbose: []
-            };
         });
 
         afterEach(() => {
+            fs.removeSync(path.resolve(testProjectDir, 'electron.json'));
+            fs.removeSync(path.resolve(testProjectDir, 'www'));
+
             apiEvents.removeAllListeners();
         });
 
-        it('should exist', () => {
-            expect(api.removePlugin).toBeDefined();
-            expect(typeof api.removePlugin).toBe('function');
-        });
-
-        it('remove empty plugin', () => {
-            return api.removePlugin({
-                id: 'empty_plugin',
-                getPlatformsArray: () => ['electron'],
-                getFilesAndFrameworks: platform => [],
-                getAssets: platform => [],
-                getJsModules: platform => [],
-                getConfigFiles: platform => []
-            }, { }).then(
-                (result) => {
-                    expect(result).not.toBeDefined();
-                },
-                (error) => {
-                    fail(`Unwanted code branch: ${error}`);
+        it('should Promise reject when missing PluginInfo parameter.', () => {
+            return api.removePlugin().then(
+                () => {},
+                error => {
+                    expect(error).toEqual(new Error('Missing plugin info parameter. The first parameter should contain a valid PluginInfo instance.'));
                 }
             );
         });
 
-        it('asset plugin', () => {
-            fs.ensureDirSync(path.resolve(testProjectDir, 'www', 'js'));
-            writeJson(path.resolve(testProjectDir, 'www', 'js', 'sample.json'), { title: 'sample' });
-            return api.removePlugin({
-                id: 'empty_plugin',
-                dir: pluginFixture,
-                getPlatformsArray: () => ['electron'],
-                getFilesAndFrameworks: platform => [],
-                getAssets: platform => [{
-                    itemType: 'asset',
-                    src: 'src/electron/sample.json',
-                    target: 'js/sample.json'
-                }],
-                getJsModules: platform => [],
-                getConfigFiles: platform => []
-            }, { }).then(
-                (result) => {
-                    expect(result).not.toBeDefined();
-                    expect(fileExists(path.resolve(testProjectDir, 'www', 'js', 'sample.json'))).toBeFalsy();
-                },
-                (error) => {
-                    fail(`Unwanted code branch: ${error}`);
-                }
-            );
+        it('should error out when a PluginInfo parameter is not valid.', () => {
+            class FakePluginInfo {}
+            return expect(() => {
+                api.removePlugin(new FakePluginInfo());
+            }).toThrowError();
         });
 
-        it('js-module plugin', () => {
-            fs.ensureDirSync(path.resolve(testProjectDir, 'www', 'plugins', 'module-plugin', 'www'));
-            fs.copySync(path.resolve(pluginFixture, 'www', 'plugin.js'), path.resolve(testProjectDir, 'www', 'plugins', 'module-plugin', 'www', 'plugin.js'));
-            expect(fileExists(path.resolve(testProjectDir, 'www', 'plugins', 'module-plugin', 'www', 'plugin.js'))).toBeTruthy();
-            return api.removePlugin({
-                id: 'module-plugin',
-                dir: pluginFixture,
-                getPlatformsArray: () => ['electron'],
-                getFilesAndFrameworks: platform => [],
-                getAssets: platform => [],
-                getJsModules: platform => [{
-                    itemType: 'js-module',
-                    name: 'testmodule',
-                    src: 'www/plugin.js',
-                    clobbers: ['ModulePlugin.clobbers'],
-                    merges: ['ModulePlugin.merges'],
-                    runs: true
-                }],
-                getConfigFiles: platform => []
-            }, { }).then(
-                (result) => {
-                    expect(result).not.toBeDefined();
-                    expect(fileExists(path.resolve(testProjectDir, 'www', 'plugins', 'module-plugin', 'www', 'plugin.js'))).toBeFalsy();
-                },
-                (error) => {
-                    fail(`Unwanted code branch: ${error}`);
-                }
-            );
+        describe('Use Electron Plugin with Default Install Options', () => {
+            let pluginInfo;
+
+            beforeEach(() => {
+                pluginInfo = new PluginInfo(pluginFixture);
+                return api.addPlugin(pluginInfo);
+            });
+
+            it('should remove the empty plugin data from electron.json.', () => {
+                return api.removePlugin(pluginInfo).then(
+                    () => {
+                        const { plugin_metadata, modules, installed_plugins } = fs.readJsonSync(path.resolve(testProjectDir, 'electron.json'));
+                        expect(plugin_metadata).toEqual({});
+                        expect(modules).toEqual([]);
+                        expect(installed_plugins).toEqual({});
+                    }
+                );
+            });
+
+            it('should remove the added plugin assets, source files, and meta data.', () => {
+                return api.removePlugin(pluginInfo).then(() => {
+                    expect(dirExists(path.resolve(testProjectDir, 'www'))).toBeTruthy();
+                    expect(fileExists(path.resolve(testProjectDir, 'electron.json'))).toBeTruthy();
+                    expect(fileExists(path.resolve(testProjectDir, 'www/cordova_plugins.js'))).toBeTruthy();
+                    expect(fileExists(path.resolve(testProjectDir, 'www/js/electron.json'))).toBeFalsy();
+                    expect(fileExists(path.resolve(testProjectDir, 'www/plugins/org.apache.testplugin/www/plugin.js'))).toBeFalsy();
+
+                    const cordovaPluginContent = fs.readFileSync(path.resolve(testProjectDir, 'www/cordova_plugins.js'), 'utf8');
+                    expect(cordovaPluginContent).not.toContain(/org.apache.testplugin/);
+                });
+            });
         });
 
-        it('unrecognized type plugin', () => {
-            return api.removePlugin({
-                id: 'unrecognized-plugin',
-                dir: pluginFixture,
-                getPlatformsArray: () => ['electron'],
-                getFilesAndFrameworks: platform => [],
-                getAssets: platform => [{
-                    itemType: 'unrecognized'
-                }],
-                getJsModules: platform => [],
-                getConfigFiles: platform => []
-            }, { }).then(
-                (result) => {
-                    expect(result).not.toBeDefined();
-                },
-                (error) => {
-                    fail(`Unwanted code branch: ${error}`);
-                }
-            );
+        it('should remove the added plugin assets, from the platform www.', () => {
+            const pluginInfo = new PluginInfo(pluginFixture);
+            return api.addPlugin(pluginInfo, { usePlatformWww: true })
+                .then(() => api.removePlugin(pluginInfo, { usePlatformWww: true }))
+                .then(() => {
+                    expect(fileExists(path.resolve(testProjectDir, 'www/cordova_plugins.js'))).toBeFalsy();
+                    expect(fileExists(path.resolve(testProjectDir, 'platform_www/cordova_plugins.js'))).toBeTruthy();
+                });
         });
 
-        it('source-file type plugin', () => {
-            return api.removePlugin({
-                id: 'source-file-plugin',
-                dir: pluginFixture,
-                getPlatformsArray: () => ['electron'],
-                getFilesAndFrameworks: platform => [],
-                getAssets: platform => [{
-                    itemType: 'source-file'
-                }],
-                getJsModules: platform => [],
-                getConfigFiles: platform => []
-            }, { }).then(
-                (result) => {
-                    expect(result).not.toBeDefined();
-                    expect(logs.verbose.some(message => message === 'source-file.uninstall is currently not supported for electron')).toBeTruthy();
-                },
-                (error) => {
-                    fail(`Unwanted code branch: ${error}`);
-                }
-            );
+        /**
+         * @todo verfiy validity of an "unknown" itemType acgtually being able to be set.
+         */
+        it('should remove the plugin assets and source files.', () => {
+            const pluginInfo = new PluginInfo(pluginFixture);
+            pluginInfo.getAssets = () => [{ itemType: 'unknown' }];
+
+            return api.removePlugin(pluginInfo).then(() => {
+                expect(events.emit).toHaveBeenCalledWith(
+                    'warn',
+                    jasmine.stringMatching(/unrecognized type/)
+                );
+            });
         });
 
-        it('remove empty plugin with browser platform', () => {
-            return api.removePlugin({
-                id: 'empty_plugin',
-                getPlatformsArray: () => ['browser'],
-                getFilesAndFrameworks: platform => [],
-                getAssets: platform => [],
-                getJsModules: platform => [],
-                getConfigFiles: platform => []
-            }, { }).then(
-                (result) => {
-                    expect(result).not.toBeDefined();
-                },
-                (error) => {
-                    fail(`Unwanted code branch: ${error}`);
-                }
-            );
+        it('should remove the empty non-electron plugin using platform www as target.', () => {
+            const pluginInfo = new PluginInfo(pluginNotElectronFixture);
+            return api.addPlugin(pluginInfo, { usePlatformWww: true })
+                .then(() => api.removePlugin(pluginInfo, { usePlatformWww: true }))
+                .then(() => {
+                    const cordovaPluginContent = fs.readFileSync(path.resolve(testProjectDir, 'platform_www/cordova_plugins.js'), 'utf8');
+                    expect(cordovaPluginContent).not.toContain(/org.apache.testnonelectronplugin/);
+                });
         });
     });
 
+    describe('build method', () => {
+        it('should execute build', () => {
+            const call = jasmine.createSpy('run');
+            const mockBuildOptions = { foo: 'bar' };
+
+            Api.__set__('require', () => ({ run: { call: call } }));
+            api.build(mockBuildOptions);
+            expect(call).toHaveBeenCalledWith(api, mockBuildOptions, api);
+            Api.__set__('require', apiRequire);
+        });
+    });
+
+    describe('run method', () => {
+        it('should execute run', () => {
+            const run = jasmine.createSpy('run');
+            const mockRunOptions = { foo: 'bar' };
+
+            Api.__set__('require', () => ({ run }));
+            api.run(mockRunOptions);
+            expect(run).toHaveBeenCalledWith(mockRunOptions);
+            Api.__set__('require', apiRequire);
+        });
+    });
+
+    describe('clean method', () => {
+        it('should execute clean', () => {
+            const run = jasmine.createSpy('clean');
+            const mockCleanOptions = { foo: 'bar' };
+
+            Api.__set__('require', () => ({ run }));
+            api.clean(mockCleanOptions);
+            expect(run).toHaveBeenCalledWith(mockCleanOptions);
+            Api.__set__('require', apiRequire);
+        });
+    });
+
+    describe('requirements method', () => {
+        it('should execute requirements', () => {
+            const run = jasmine.createSpy('requirements');
+
+            Api.__set__('require', () => ({ run }));
+            api.requirements();
+            expect(run).toHaveBeenCalled();
+            Api.__set__('require', apiRequire);
+        });
+    });
+});
+
+describe('Api prototype methods', () => {
     describe('updatePlatform method', () => {
         it('should return a resolved promise.', () => {
             Api.updatePlatform().then(
-                (result) => {
-                    // Currently updatePlatform only resolves with nothing.
+                result => {
                     expect(result).toBeUndefined();
                 }
             );
@@ -509,99 +438,22 @@
          * @todo improve createPlatform to test actual created platforms.
          */
         it('should export static createPlatform function', () => {
+            spyOn(events, 'emit');
+
             return Api.createPlatform(tmpDir)
                 .then((results) => {
-                    expect(results.constructor.name).toBe(api.constructor.name);
+                    expect(events.emit).toHaveBeenCalledWith(
+                        'log',
+                        jasmine.stringMatching(/Creating Cordova project/)
+                    );
+
+                    expect(results.constructor.name).toBe('Api');
                 });
         });
 
         it('should emit createPlatform not callable when error occurs.', () => {
-            Api.__set__('require', () => ({
-                createProject: () => { throw new Error('Some Random Error'); }
-            }));
-
-            expect(() => Api.createPlatform(tmpDir)).toThrowError(/createPlatform is not callable from the electron project API/);
-
-            Api.__set__('require', apiRequire);
-        });
-    });
-
-    describe('build method', () => {
-        const runSpy = jasmine.createSpy('run');
-        beforeEach(() => {
-            // Mocking require that is called to get version.
-            Api.__set__('require', () => ({
-                run: { call: runSpy }
-            }));
-        });
-
-        afterEach(() => {
-            Api.__set__('require', apiRequire);
-        });
-
-        it('should execute build', () => {
-            const mockBuildOptions = { foo: 'bar' };
-            api.build(mockBuildOptions);
-            expect(runSpy).toHaveBeenCalledWith(api, mockBuildOptions, api);
-        });
-    });
-
-    describe('run method', () => {
-        const runSpy = jasmine.createSpy('run');
-        beforeEach(() => {
-            // Mocking require that is called to get version.
-            Api.__set__('require', () => ({
-                run: runSpy
-            }));
-        });
-
-        afterEach(() => {
-            Api.__set__('require', apiRequire);
-        });
-
-        it('should execute run', () => {
-            const mockRunOptions = { foo: 'bar' };
-            api.run(mockRunOptions);
-            expect(runSpy).toHaveBeenCalledWith(mockRunOptions);
-        });
-    });
-
-    describe('clean method', () => {
-        const cleanSpy = jasmine.createSpy('clean');
-        beforeEach(() => {
-            // Mocking require that is called to get version.
-            Api.__set__('require', () => ({
-                run: cleanSpy
-            }));
-        });
-
-        afterEach(() => {
-            Api.__set__('require', apiRequire);
-        });
-
-        it('should execute clean', () => {
-            const mockCleanOptions = { foo: 'bar' };
-            api.clean(mockCleanOptions);
-            expect(cleanSpy).toHaveBeenCalledWith(mockCleanOptions);
-        });
-    });
-
-    describe('requirements method', () => {
-        const requirementsSpy = jasmine.createSpy('requirements');
-        beforeEach(() => {
-            // Mocking require that is called to get version.
-            Api.__set__('require', () => ({
-                run: requirementsSpy
-            }));
-        });
-
-        afterEach(() => {
-            Api.__set__('require', apiRequire);
-        });
-
-        it('should execute requirements', () => {
-            api.requirements();
-            expect(requirementsSpy).toHaveBeenCalled();
+            spyOn(create, 'createProject').and.returnValue(new Error('Some Random Error'));
+            expect(() => Api.createPlatform(tmpDir)).toThrowError();
         });
     });
 });
diff --git a/tests/spec/unit/create.spec.js b/tests/spec/unit/create.spec.js
index 36d58a9..ac40ecc 100644
--- a/tests/spec/unit/create.spec.js
+++ b/tests/spec/unit/create.spec.js
@@ -49,7 +49,7 @@
     fs.removeSync(tmpDir);
 }
 
-function createAndValidateProjectDirName (projectname, projectid) {
+function createAndValidateProjectDirName (projectname, projectid, { copyNodeModules = false } = {}) {
     // remove existing folder
     fs.removeSync(tmpDir);
     fs.ensureDirSync(tmpDir);
@@ -59,7 +59,8 @@
     const _fs = create.__get__('fs');
     create.__set__('fs', {
         ensureDirSync: _fs.ensureDirSync,
-        existsSync: path => !(path === projectPath),
+        existsSync: path => path !== projectPath &&
+            !(path.includes('node_modules') && copyNodeModules),
         copySync: () => true
     });
 
@@ -127,6 +128,13 @@
         return createAndValidateProjectDirName(projectname, projectid);
     });
 
+    it('copy node_modules folder if it exists', () => {
+        const projectname = 'withnodemodules';
+        const projectid = 'com.test.withnodemodules';
+
+        return createAndValidateProjectDirName(projectname, projectid, { copyNodeModules: true });
+    });
+
     it('should stop creating project when project destination already exists', () => {
         const _fs = create.__get__('fs');
         create.__set__('fs', {
diff --git a/tests/spec/unit/handler.spec.js b/tests/spec/unit/handler.spec.js
index 7686bc3..60bc511 100644
--- a/tests/spec/unit/handler.spec.js
+++ b/tests/spec/unit/handler.spec.js
@@ -17,320 +17,122 @@
     under the License.
 */
 
+const path = require('path');
+const fs = require('fs-extra');
+const { events } = require('cordova-common');
 const rewire = require('rewire');
 const handler = rewire('../../../bin/templates/cordova/handler');
-const path = require('path');
 
 describe('Handler export', () => {
     describe('www_dir method', () => {
-        it('should return the project\'s www dir.', () => {
-            const projectDir = 'random-project-dir';
-            const actual = handler.www_dir(projectDir);
-            const expected = path.join(projectDir, 'www');
-            expect(actual).toBe(expected);
+        it(`should return the project's www dir.`, () => {
+            const projectDir = 'mocked-project-dir-path';
+            expect(
+                handler.www_dir(projectDir)
+            ).toBe(path.join(projectDir, 'www'));
         });
     });
 
     describe('package_name method', () => {
-        it('should return default package name when config.xml is missing.', () => {
-            const projectDir = 'random-project-dir';
-
-            const existsSyncSpy = jasmine.createSpy('existsSync').and.returnValue(false);
-            handler.__set__('fs', {
-                existsSync: existsSyncSpy
-            });
-
-            const actual = handler.package_name(projectDir);
-            const expected = 'io.cordova.hellocordova';
-            expect(actual).toBe(expected);
+        it('should return default package name when config.xml file is missing', () => {
+            spyOn(fs, 'existsSync').and.returnValue(false);
+            expect(
+                handler.package_name('mocked-project-dir-path')
+            ).toBe('io.cordova.hellocordova');
         });
 
-        it('should return default package name when config.xml is missing.', () => {
-            const projectDir = 'random-project-dir';
+        it('should return default package name when config.xml file does not contain the widget id', () => {
+            spyOn(fs, 'existsSync').and.returnValue(true);
+            spyOn(fs, 'readFileSync').and.returnValue(''); // blank config.file
+            expect(
+                handler.package_name('mocked-project-dir-path')
+            ).toBe('io.cordova.hellocordova');
+        });
 
-            const existsSyncSpy = jasmine.createSpy('existsSync').and.returnValue(true);
-            const readFileSyncSpy = jasmine.createSpy('readFileSync').and.returnValue(`
-<?xml version='1.0' encoding='utf-8'?>
-<widget id="com.foobar.random" version="1.0.0" xmlns="http://www.w3.org/ns/widgets" xmlns:cdv="http://cordova.apache.org/ns/1.0">
-</widget>
+        it('should return package name defined on the widget element id attribute in config.xml file', () => {
+            spyOn(fs, 'existsSync').and.returnValue(true);
+            spyOn(fs, 'readFileSync').and.returnValue(`
+                <?xml version='1.0' encoding='utf-8'?>
+                    <widget id="com.foobar.random" version="1.0.0" xmlns="http://www.w3.org/ns/widgets" xmlns:cdv="http://cordova.apache.org/ns/1.0">
+                </widget>
             `);
-            handler.__set__('fs', {
-                existsSync: existsSyncSpy,
-                readFileSync: readFileSyncSpy
-            });
-
-            const actual = handler.package_name(projectDir);
-            const expected = 'com.foobar.random';
-            expect(actual).toBe(expected);
+            expect(
+                handler.package_name('mocked-project-dir-path')
+            ).toBe('com.foobar.random');
         });
     });
 
-    describe('JSModule', () => {
-        describe('Install', () => {
-            it('should write module but does not contain json export.', () => {
-                const jsModule = {
-                    src: 'src',
-                    name: 'dummy-module'
-                };
-                const plugin_id = 'com.foo.dummy-plugin';
+    describe('js-module method', () => {
+        describe('js-module.install', () => {
+            const moduleExportTestCase = jsModule => {
                 const plugin_dir = 'mock';
+                const plugin_id = 'com.foo.dummy-plugin';
                 const www_dir = 'www';
+                const moduleName = `${plugin_id}.${jsModule.name || path.basename(jsModule.src, path.extname(jsModule.src))}`;
 
                 // spies
-                const ensureDirSyncSpy = jasmine.createSpy('ensureDirSync').and.returnValue(true);
-                const readFileSyncSpy = jasmine.createSpy('readFileSync').and.returnValue('module-content');
-                const writeFileSyncSpy = jasmine.createSpy('writeFileSync');
-                handler.__set__('fs', {
-                    ensureDirSync: ensureDirSyncSpy,
-                    readFileSync: readFileSyncSpy,
-                    writeFileSync: writeFileSyncSpy
-                });
+                spyOn(fs, 'readFileSync').and.returnValue(''); // fake scriptContent
+                spyOn(fs, 'ensureDirSync').and.returnValue(true);
+                spyOn(fs, 'writeFileSync');
 
                 handler['js-module'].install(jsModule, plugin_dir, plugin_id, www_dir);
 
-                expect(readFileSyncSpy).toHaveBeenCalled();
-                expect(ensureDirSyncSpy).toHaveBeenCalled();
-                expect(writeFileSyncSpy).toHaveBeenCalled();
+                const moduleDetination = path.dirname(path.resolve(www_dir, 'plugins', plugin_id, jsModule.src));
+                const writeFileSyncContent = fs.writeFileSync.calls.argsFor(0)[1];
 
-                const actual = writeFileSyncSpy.calls.argsFor(0)[1];
+                expect(fs.readFileSync).toHaveBeenCalledWith(path.resolve(plugin_dir, jsModule.src), 'utf-8');
+                expect(fs.ensureDirSync).toHaveBeenCalledWith(moduleDetination);
+                expect(writeFileSyncContent).toContain(`cordova.define('${moduleName}'`);
 
-                expect(actual).toContain('com.foo.dummy-plugin.dummy-module');
-                expect(actual).toContain('module-content');
+                return writeFileSyncContent;
+            };
+
+            it('should write cordova.define for module "com.foo.dummy-plugin.dummy-module" (from source name) and not module.exports as module source is not JSON.', () => {
+                const jsModule = { src: 'src/dummy-module', name: 'dummy-module' };
+                const writeFileSyncContent = moduleExportTestCase(jsModule);
+                expect(writeFileSyncContent).not.toContain(`module.exports`);
             });
 
-            it('should write module but does not contain json export.', () => {
-                const jsModule = {
-                    src: 'src.json',
-                    name: 'dummy-module'
-                };
-                const plugin_id = 'com.foo.dummy-plugin';
-                const plugin_dir = 'mock';
-                const www_dir = 'www';
+            it('should write cordova.define for module "com.foo.dummy-plugin.dummy-module" (from source filepath) and not module.exports as module source is not JSON.', () => {
+                const jsModule = { src: 'src/dummy-module' };
+                const writeFileSyncContent = moduleExportTestCase(jsModule);
+                expect(writeFileSyncContent).not.toContain(`module.exports`);
+            });
 
-                // spies
-                const ensureDirSyncSpy = jasmine.createSpy('ensureDirSync').and.returnValue(true);
-                const readFileSyncSpy = jasmine.createSpy('readFileSync').and.returnValue('module-content');
-                const writeFileSyncSpy = jasmine.createSpy('writeFileSync');
-                handler.__set__('fs', {
-                    ensureDirSync: ensureDirSyncSpy,
-                    readFileSync: readFileSyncSpy,
-                    writeFileSync: writeFileSyncSpy
-                });
-
-                handler['js-module'].install(jsModule, plugin_dir, plugin_id, www_dir);
-
-                expect(readFileSyncSpy).toHaveBeenCalled();
-                expect(ensureDirSyncSpy).toHaveBeenCalled();
-                expect(writeFileSyncSpy).toHaveBeenCalled();
-
-                const actual = writeFileSyncSpy.calls.argsFor(0)[1];
-
-                expect(actual).toContain('com.foo.dummy-plugin.dummy-module');
-                expect(actual).toContain(`module.exports =  + module-content`);
+            it('should write cordova.define for module "com.foo.dummy-plugin.dummy-module" (from source filepath) and not module.exports as module source is not JSON.', () => {
+                const jsModule = { src: 'src/dummy-module/something.json' };
+                const writeFileSyncContent = moduleExportTestCase(jsModule);
+                expect(writeFileSyncContent).toContain(`module.exports`);
             });
         });
 
-        describe('Uninstall', () => {
-            let emitSpy;
-            beforeEach(() => {
-                emitSpy = jasmine.createSpy('emit');
-                handler.__set__('events', {
-                    emit: emitSpy
-                });
-            });
-
+        describe('js-module.uninstall', () => {
             it('should emit that js-module uninstall was called.', () => {
-                const jsModule = {
-                    src: 'src.json',
-                    name: 'dummy-module'
-                };
-                const plugin_id = 'com.foo.dummy-plugin';
+                const jsModule = { src: 'src/dummy-module', name: 'dummy-module' };
                 const www_dir = 'www';
-                const pluginRelativePath = path.join('plugins', plugin_id, jsModule.src);
+                const plugin_id = 'com.foo.dummy-plugin';
 
-                const removeSyncSpy = jasmine.createSpy('writeFileSync');
-                handler.__set__('fs', {
-                    removeSync: removeSyncSpy
-                });
+                // spies
+                spyOn(fs, 'removeSync').and.returnValue(true);
+                spyOn(events, 'emit');
 
                 handler['js-module'].uninstall(jsModule, www_dir, plugin_id);
 
-                expect(removeSyncSpy).toHaveBeenCalled();
-
-                const actual = emitSpy.calls.argsFor(0)[1];
-                const expected = `js-module uninstall called : ${pluginRelativePath}`;
-                expect(emitSpy).toHaveBeenCalled();
-                expect(actual).toBe(expected);
+                expect(fs.removeSync).toHaveBeenCalledWith(path.join(www_dir, 'plugins', plugin_id));
+                expect(events.emit).toHaveBeenCalledWith(
+                    'verbose',
+                    `js-module uninstall called : ${path.join('plugins', plugin_id, jsModule.src)}`
+                );
             });
         });
     });
 
-    describe('SourceFile', () => {
-        let emitSpy;
-        beforeEach(() => {
-            emitSpy = jasmine.createSpy('emit');
-            handler.__set__('events', {
-                emit: emitSpy
-            });
-        });
-
-        describe('Install', () => {
-            it('should emit that source-file install is not supported.', () => {
-                handler['source-file'].install();
-
-                const actual = emitSpy.calls.argsFor(0)[1];
-                const expected = 'source-file.install is currently not supported for electron';
-                expect(emitSpy).toHaveBeenCalled();
-                expect(actual).toBe(expected);
-            });
-        });
-
-        describe('Uninstall', () => {
-            it('should emit that source-file uninstall is not supported.', () => {
-                handler['source-file'].uninstall();
-
-                const actual = emitSpy.calls.argsFor(0)[1];
-                const expected = 'source-file.uninstall is currently not supported for electron';
-                expect(emitSpy).toHaveBeenCalled();
-                expect(actual).toBe(expected);
-            });
-        });
-    });
-
-    describe('HeaderFile', () => {
-        let emitSpy;
-        beforeEach(() => {
-            emitSpy = jasmine.createSpy('emit');
-            handler.__set__('events', {
-                emit: emitSpy
-            });
-        });
-
-        describe('Install', () => {
-            it('should emit that header-file install is not supported.', () => {
-                handler['header-file'].install();
-
-                const actual = emitSpy.calls.argsFor(0)[1];
-                const expected = 'header-file.install is not supported for electron';
-                expect(emitSpy).toHaveBeenCalled();
-                expect(actual).toBe(expected);
-            });
-        });
-
-        describe('Uninstall', () => {
-            it('should emit that header-file uninstall is not supported.', () => {
-                handler['header-file'].uninstall();
-
-                const actual = emitSpy.calls.argsFor(0)[1];
-                const expected = 'header-file.uninstall is not supported for electron';
-                expect(emitSpy).toHaveBeenCalled();
-                expect(actual).toBe(expected);
-            });
-        });
-    });
-
-    describe('ResourceFile', () => {
-        let emitSpy;
-        beforeEach(() => {
-            emitSpy = jasmine.createSpy('emit');
-            handler.__set__('events', {
-                emit: emitSpy
-            });
-        });
-
-        describe('Install', () => {
-            it('should emit that resource-file install is not supported.', () => {
-                handler['resource-file'].install();
-
-                const actual = emitSpy.calls.argsFor(0)[1];
-                const expected = 'resource-file.install is not supported for electron';
-                expect(emitSpy).toHaveBeenCalled();
-                expect(actual).toBe(expected);
-            });
-        });
-
-        describe('Uninstall', () => {
-            it('should emit that resource-file uninstall is not supported.', () => {
-                handler['resource-file'].uninstall();
-
-                const actual = emitSpy.calls.argsFor(0)[1];
-                const expected = 'resource-file.uninstall is not supported for electron';
-                expect(emitSpy).toHaveBeenCalled();
-                expect(actual).toBe(expected);
-            });
-        });
-    });
-
-    describe('Framework', () => {
-        let emitSpy;
-        beforeEach(() => {
-            emitSpy = jasmine.createSpy('emit');
-            handler.__set__('events', {
-                emit: emitSpy
-            });
-        });
-
-        describe('Install', () => {
-            it('should emit that framework install is not supported.', () => {
-                handler['framework'].install();
-
-                const actual = emitSpy.calls.argsFor(0)[1];
-                const expected = 'framework.install is not supported for electron';
-                expect(emitSpy).toHaveBeenCalled();
-                expect(actual).toBe(expected);
-            });
-        });
-
-        describe('Uninstall', () => {
-            it('should emit that framework uninstall is not supported.', () => {
-                handler['framework'].uninstall();
-
-                const actual = emitSpy.calls.argsFor(0)[1];
-                const expected = 'framework.uninstall is not supported for electron';
-                expect(emitSpy).toHaveBeenCalled();
-                expect(actual).toBe(expected);
-            });
-        });
-    });
-
-    describe('LibFile', () => {
-        let emitSpy;
-        beforeEach(() => {
-            emitSpy = jasmine.createSpy('emit');
-            handler.__set__('events', {
-                emit: emitSpy
-            });
-        });
-
-        describe('Install', () => {
-            it('should emit that lib-file install is not supported.', () => {
-                handler['lib-file'].install();
-
-                const actual = emitSpy.calls.argsFor(0)[1];
-                const expected = 'lib-file.install is not supported for electron';
-                expect(emitSpy).toHaveBeenCalled();
-                expect(actual).toBe(expected);
-            });
-        });
-
-        describe('Uninstall', () => {
-            it('should emit that lib-file uninstall is not supported.', () => {
-                handler['lib-file'].uninstall();
-
-                const actual = emitSpy.calls.argsFor(0)[1];
-                const expected = 'lib-file.uninstall is not supported for electron';
-                expect(emitSpy).toHaveBeenCalled();
-                expect(actual).toBe(expected);
-            });
-        });
-    });
-
-    describe('Assets', () => {
+    describe('asset method', () => {
         const plugin_dir = 'pluginDir';
         const wwwDest = 'dest';
 
-        describe('Install', () => {
-            it('should copySync with a directory path.', () => {
+        describe('asset.install', () => {
+            it('should copySync assets to destination.', () => {
                 const asset = {
                     itemType: 'asset',
                     src: 'someSrc/ServiceWorker.js',
@@ -338,57 +140,53 @@
                 };
 
                 // Spies
-                const copySyncSpy = jasmine.createSpy('copySync');
-                const ensureDirSyncSpy = jasmine.createSpy('ensureDirSync').and.returnValue(true);
-
-                handler.__set__('fs', {
-                    copySync: copySyncSpy,
-                    ensureDirSync: ensureDirSyncSpy
-                });
+                spyOn(fs, 'copySync');
+                spyOn(fs, 'ensureDirSync').and.returnValue(true);
 
                 handler.asset.install(asset, plugin_dir, wwwDest);
-                expect(ensureDirSyncSpy).toHaveBeenCalled();
-                expect(copySyncSpy).toHaveBeenCalledWith(jasmine.any(String), path.join('dest', asset.target));
-            });
 
-            it('should call copySync with a file path.', () => {
-                const asset = {
-                    itemType: 'asset',
-                    src: 'someSrc/ServiceWorker.js',
-                    target: 'ServiceWorker.js'
-                };
-
-                // Spies
-                const copySyncSpy = jasmine.createSpy('copySync');
-                const ensureDirSyncSpy = jasmine.createSpy('ensureDirSync');
-
-                handler.__set__('fs', {
-                    copySync: copySyncSpy,
-                    ensureDirSync: ensureDirSyncSpy
-                });
-
-                handler.asset.install(asset, plugin_dir, wwwDest);
-                expect(ensureDirSyncSpy).toHaveBeenCalled();
-                expect(copySyncSpy).toHaveBeenCalledWith(jasmine.any(String), path.join('dest', asset.target));
+                expect(fs.ensureDirSync).toHaveBeenCalled();
+                expect(fs.copySync).toHaveBeenCalledWith(jasmine.any(String), path.join('dest', asset.target));
             });
         });
 
-        describe('Uninstall', () => {
+        describe('asset.uninstall', () => {
             it('should remove assets', () => {
                 const asset = { itemType: 'asset', src: 'someSrc', target: 'ServiceWorker.js' };
                 const mockPluginId = 'com.foobar.random-plugin-id';
 
                 // Spies
-                const fsRemoveSyncSpy = jasmine.createSpy('removeSync');
-                handler.__set__('fs', {
-                    removeSync: fsRemoveSyncSpy
-                });
+                spyOn(fs, 'removeSync').and.returnValue(true);
 
                 handler.asset.uninstall(asset, wwwDest, mockPluginId);
 
-                expect(fsRemoveSyncSpy).toHaveBeenCalled();
-                expect(fsRemoveSyncSpy.calls.argsFor(0)[0]).toBe(path.join(wwwDest, asset.target));
-                expect(fsRemoveSyncSpy.calls.argsFor(1)[0]).toBe(path.join(wwwDest, 'plugins', mockPluginId));
+                expect(fs.removeSync).toHaveBeenCalled();
+                expect(fs.removeSync.calls.argsFor(0)[0]).toBe(path.join(wwwDest, asset.target));
+                expect(fs.removeSync.calls.argsFor(1)[0]).toBe(path.join(wwwDest, 'plugins', mockPluginId));
+            });
+        });
+    });
+
+    describe('Unsupported Handlers', () => {
+        it('should emit that the install and uninstall methods are not supported for X types.', () => {
+            spyOn(events, 'emit');
+
+            // loop unsupported types
+            [
+                'source-file',
+                'header-file',
+                'resource-file',
+                'framework',
+                'lib-file'
+            ].forEach(type => {
+                for (const method of ['install', 'uninstall']) {
+                    handler[type][method]();
+                    expect(events.emit).toHaveBeenCalledWith(
+                        'verbose',
+                        `${type}.${method} is not supported for electron`
+                    );
+                    events.emit.calls.reset();
+                }
             });
         });
     });
diff --git a/tests/spec/unit/lib/update.spec.js b/tests/spec/unit/lib/update.spec.js
index 2849bcb..41be855 100644
--- a/tests/spec/unit/lib/update.spec.js
+++ b/tests/spec/unit/lib/update.spec.js
@@ -25,7 +25,7 @@
         it('should reject with an errror that update is not supported.', () => {
             update.run().then(
                 () => {},
-                (error) => {
+                error => {
                     expect(error).toEqual(new Error('Update not supported'));
                 }
             );
@@ -34,14 +34,9 @@
 
     describe('help export method', () => {
         it('should warn that updating is not support for Electron.', () => {
-            const logSpy = jasmine.createSpy('log');
-            update.__set__('console', {
-                log: logSpy
-            });
-
+            spyOn(console, 'log');
             update.help();
-
-            expect(logSpy.calls.argsFor(0)[0]).toContain('WARNING : Electron does not support updating. Remove and then re-Add the platform to get the latest.');
+            expect(console.log).toHaveBeenCalledWith('WARNING : Electron does not support updating. Remove and then re-Add the platform to get the latest.');
         });
     });
 });
diff --git a/tests/spec/unit/templates/cordova/lib/ManifestJsonParser.spec.js b/tests/spec/unit/templates/cordova/lib/ManifestJsonParser.spec.js
index 4856a81..4ee70ee 100644
--- a/tests/spec/unit/templates/cordova/lib/ManifestJsonParser.spec.js
+++ b/tests/spec/unit/templates/cordova/lib/ManifestJsonParser.spec.js
@@ -19,7 +19,9 @@
 
 const rewire = require('rewire');
 const path = require('path');
-const ConfigParser = require('cordova-common').ConfigParser;
+const fs = require('fs-extra');
+const { ConfigParser } = require('cordova-common');
+const ManifestJsonParser = rewire('../../../../../../bin/templates/cordova/lib/ManifestJsonParser');
 
 const FIXTURES = path.join(__dirname, '..', '..', '..', '..', 'fixtures');
 
@@ -28,211 +30,148 @@
 const cfg2 = new ConfigParser(path.join(FIXTURES, 'test-config-2.xml'));
 const cfgEmpty = new ConfigParser(path.join(FIXTURES, 'test-config-empty.xml'));
 
-describe('Testing ManifestJsonParser.js:', () => {
-    let ManifestJsonParser;
-    let locations;
+const locations = {
+    buildRes: path.join('mock', 'build-res'),
+    www: path.join('mock', 'www'),
+    configXml: path.join('mock', 'config.xml')
+};
+
+const defaultInitManifest = {
+    background_color: '#FFF',
+    display: 'standalone',
+    orientation: 'any',
+    start_url: 'index.html'
+};
+
+describe('ManifestJson class', () => {
+    let manifestJsonParser;
 
     beforeEach(() => {
-        ManifestJsonParser = rewire('../../../../../../bin/templates/cordova/lib/ManifestJsonParser');
-
-        locations = {
-            buildRes: path.join('mock', 'build-res'),
-            www: path.join('mock', 'www'),
-            configXml: path.join('mock', 'config.xml')
-        };
+        manifestJsonParser = new ManifestJsonParser(locations.www);
     });
 
-    describe('ManifestJson class', () => {
-        let manifestJsonParser;
+    it('should have been constructed with initial values.', () => {
+        expect(manifestJsonParser).toBeDefined();
+        expect(manifestJsonParser.path).toEqual(path.join(locations.www, 'manifest.json'));
+        expect(manifestJsonParser.www).toEqual(locations.www);
+        expect(manifestJsonParser.manifest).toEqual(defaultInitManifest);
+    });
 
-        beforeEach(() => {
-            manifestJsonParser = ManifestJsonParser.__get__('ManifestJsonParser');
-        });
+    it('should return when config xml is not defined.', () => {
+        manifestJsonParser.configure();
+        expect(manifestJsonParser.manifest).toEqual(defaultInitManifest);
+    });
 
-        it('should should be defined.', () => {
-            expect(manifestJsonParser).toBeDefined();
-        });
+    it('should set manifest json object values to default, when config xml is empty.', () => {
+        manifestJsonParser.configure(cfgEmpty);
+        expect(manifestJsonParser.manifest).toEqual(jasmine.objectContaining({
+            start_url: undefined
+        }));
+    });
 
-        it('should set initial value correctly.', () => {
-            manifestJsonParser = new ManifestJsonParser(locations.www);
+    it('should read and set manifest json object values from the first config xml.', () => {
+        manifestJsonParser.configure(cfg1);
+        expect(manifestJsonParser.manifest).toEqual(jasmine.objectContaining({
+            orientation: 'portrait',
+            name: 'HelloWorld',
+            short_name: 'HelloWorld',
+            version: 'whatever',
+            description: 'A sample Apache Cordova application.',
+            author: 'Cordova Team',
+            icons: [{ src: 'res/electron/cordova.png', type: 'image/png', sizes: '16x16' }]
+        }));
+    });
 
-            // mock manifest JSON Object
-            const manifestJsonObj = {
-                background_color: '#FFF',
-                display: 'standalone',
-                orientation: 'any',
-                start_url: 'index.html'
-            };
+    it('should read and set manifest json object values from second config xml.', () => {
+        manifestJsonParser.configure(cfg2);
+        expect(manifestJsonParser.manifest).toEqual(jasmine.objectContaining({
+            orientation: 'landscape',
+            name: 'HelloWorld',
+            short_name: 'Hello',
+            theme_color: '0xff0000ff',
+            version: 'whatever',
+            description: 'A sample Apache Cordova application.',
+            author: 'Cordova Team'
+        }));
+    });
 
-            expect(manifestJsonParser.path).toEqual(path.join('mock', 'www', 'manifest.json'));
-            expect(manifestJsonParser.www).toEqual(locations.www);
-            expect(manifestJsonParser.manifest).toEqual(manifestJsonObj);
-        });
+    it('should update theme color if start_url exists in path and contains meta theme-color.', () => {
+        spyOn(fs, 'existsSync').and.returnValue(true);
+        spyOn(fs, 'readFileSync').and.returnValue('<meta name="theme-color" content="#33363b">');
 
-        it('should return when config xml is not defined.', () => {
-            manifestJsonParser = new ManifestJsonParser(locations.www).configure(undefined);
+        manifestJsonParser.configureThemeColor(cfg1);
+        expect(manifestJsonParser.manifest.theme_color).toEqual('#33363b');
+    });
 
-            // mock manifest JSON Object
-            const manifestJsonObj = {
-                background_color: '#FFF',
-                display: 'standalone',
-                orientation: 'any',
-                start_url: 'index.html'
-            };
+    it('should update theme color with StatusBarBackgroundColor if start_url file does not contain meta theme-color.', () => {
+        spyOn(fs, 'existsSync').and.returnValue(true);
+        spyOn(fs, 'readFileSync').and.returnValue('');
 
-            expect(manifestJsonParser.manifest).toEqual(manifestJsonObj);
-        });
+        manifestJsonParser.configureThemeColor(cfg2);
+        expect(manifestJsonParser.manifest.theme_color).toEqual('0xff0000ff');
+    });
 
-        it('should set manifest json object values to default, when config xml is empty.', () => {
-            manifestJsonParser = new ManifestJsonParser(locations.www).configure(cfgEmpty);
+    it('should update theme color with no value when StatusBarBackgroundColor is missing and start_url file does not contain meta theme-color.', () => {
+        spyOn(fs, 'existsSync').and.returnValue(true);
+        spyOn(fs, 'readFileSync').and.returnValue('');
 
-            // mock manifest JSON Object
-            const manifestJsonObj = {
-                background_color: '#FFF',
-                display: 'standalone',
-                orientation: 'any',
-                start_url: undefined,
-                icons: []
-            };
+        manifestJsonParser.configureThemeColor(cfgEmpty);
+        expect(manifestJsonParser.manifest.theme_color).toEqual(undefined);
+    });
 
-            expect(manifestJsonParser.manifest).toEqual(manifestJsonObj);
-        });
+    it('should write something.', () => {
+        spyOn(fs, 'writeFileSync').and.returnValue(true);
 
-        it('should read and set manifest json object values from the first config xml.', () => {
-            manifestJsonParser = new ManifestJsonParser(locations.www).configure(cfg1);
+        manifestJsonParser.write();
+        expect(fs.writeFileSync).toHaveBeenCalledWith(
+            jasmine.any(String),
+            JSON.stringify(defaultInitManifest, null, 2),
+            'utf8'
+        );
+    });
 
-            // mock manifest JSON Object
-            const manifestJsonObj = {
-                background_color: '#FFF',
-                display: 'standalone',
-                orientation: 'portrait',
-                start_url: 'index.html',
-                name: 'HelloWorld',
-                short_name: 'HelloWorld',
-                version: 'whatever',
-                description: 'A sample Apache Cordova application.',
-                author: 'Cordova Team',
-                icons: [{ src: 'res/electron/cordova.png', type: 'image/png', sizes: '16x16' }]
-            };
-            expect(manifestJsonParser.manifest).toEqual(manifestJsonObj);
-        });
+    it('should write defaults with empty config.xml.', () => {
+        spyOn(fs, 'writeFileSync').and.returnValue(true);
 
-        it('should read and set manifest json object values from second config xml.', () => {
-            manifestJsonParser = new ManifestJsonParser(locations.www).configure(cfg2);
+        manifestJsonParser.configure(cfgEmpty)
+            .write();
 
-            // mock manifest JSON Object
-            const manifestJsonObj = {
-                background_color: '#FFF',
-                display: 'standalone',
-                orientation: 'landscape',
-                start_url: 'index.html',
-                name: 'HelloWorld',
-                short_name: 'Hello',
-                theme_color: '0xff0000ff',
-                version: 'whatever',
-                description: 'A sample Apache Cordova application.',
-                author: 'Cordova Team',
-                icons: []
-            };
-            expect(manifestJsonParser.manifest).toEqual(manifestJsonObj);
-        });
+        const expectedManifest = {
+            background_color: '#FFF',
+            display: 'standalone',
+            orientation: 'any'
+        };
 
-        it('should be called if start_url is defined in config xml.', () => {
-            const existsSyncSpy = jasmine.createSpy('existsSync').and.returnValue(true);
-            const readFileSyncSpy = jasmine.createSpy('readFileSync').and.returnValue('<meta name="theme-color" content="#33363b">');
-            manifestJsonParser.__set__('fs', { readFileSync: readFileSyncSpy, existsSync: existsSyncSpy });
+        expect(fs.writeFileSync).toHaveBeenCalledWith(
+            jasmine.any(String),
+            JSON.stringify(expectedManifest, null, 2),
+            'utf8'
+        );
+    });
 
-            manifestJsonParser = new ManifestJsonParser(locations.www).configureThemeColor(cfg1);
+    it('should write with user defined values from config.xml.', () => {
+        spyOn(fs, 'writeFileSync').and.returnValue(true);
 
-            expect(existsSyncSpy).toHaveBeenCalled();
-            expect(readFileSyncSpy).toHaveBeenCalled();
-        });
+        manifestJsonParser.configure(cfg1)
+            .write();
 
-        it('should not be called if start_url is not defined in config xml.', () => {
-            const existsSyncSpy = jasmine.createSpy('existsSync').and.returnValue(true);
-            const readFileSyncSpy = jasmine.createSpy('readFileSync');
-            manifestJsonParser.__set__('fs', { readFileSync: readFileSyncSpy, existsSync: existsSyncSpy });
+        const expectedManifest = {
+            background_color: '#FFF',
+            display: 'standalone',
+            orientation: 'portrait',
+            start_url: 'index.html',
+            name: 'HelloWorld',
+            short_name: 'HelloWorld',
+            version: 'whatever',
+            description: 'A sample Apache Cordova application.',
+            author: 'Cordova Team',
+            icons: [{ src: 'res/electron/cordova.png', type: 'image/png', sizes: '16x16' }]
+        };
 
-            manifestJsonParser = new ManifestJsonParser(locations.www).configureThemeColor(cfgEmpty);
-
-            expect(existsSyncSpy).toHaveBeenCalled();
-            expect(readFileSyncSpy).toHaveBeenCalled();
-        });
-
-        it('should write provided data when config xml is empty.', () => {
-            const existsSyncSpy = jasmine.createSpy('existsSync').and.returnValue(true);
-            const readFileSyncSpy = jasmine.createSpy('readFileSync');
-            const writeFileSyncSpy = jasmine.createSpy('writeFileSync');
-            manifestJsonParser.__set__('fs', { readFileSync: readFileSyncSpy, existsSync: existsSyncSpy, writeFileSync: writeFileSyncSpy });
-
-            manifestJsonParser = new ManifestJsonParser(locations.www).configure(cfgEmpty).write();
-
-            expect(existsSyncSpy).not.toHaveBeenCalled();
-            expect(readFileSyncSpy).not.toHaveBeenCalled();
-            expect(writeFileSyncSpy).toHaveBeenCalled();
-
-            // mock manifest JSON Object
-            let manifestJsonObj = {
-                background_color: '#FFF',
-                display: 'standalone',
-                orientation: 'any',
-                icons: []
-            };
-
-            const manifestPath = writeFileSyncSpy.calls.argsFor(0)[0];
-            expect(manifestPath).toEqual(path.join('mock', 'www', 'manifest.json'));
-
-            // get manifest json file content and remove white spaces
-            let manifestFile = writeFileSyncSpy.calls.argsFor(0)[1];
-
-            // convert to remove white space
-            manifestFile = manifestFile.replace(/\s+/g, '');
-            manifestJsonObj = JSON.stringify(manifestJsonObj).replace(/\s/g, '');
-            expect(manifestFile).toEqual(manifestJsonObj);
-
-            const manifestFormat = writeFileSyncSpy.calls.argsFor(0)[2];
-            expect(manifestFormat).toEqual('utf8');
-        });
-
-        it('should write manifest json object values from config xml.', () => {
-            const existsSyncSpy = jasmine.createSpy('existsSync').and.returnValue(true);
-            const readFileSyncSpy = jasmine.createSpy('readFileSync');
-            const writeFileSyncSpy = jasmine.createSpy('writeFileSync');
-            manifestJsonParser.__set__('fs', { readFileSync: readFileSyncSpy, existsSync: existsSyncSpy, writeFileSync: writeFileSyncSpy });
-
-            manifestJsonParser = new ManifestJsonParser(locations.www).configure(cfg1).write();
-
-            expect(existsSyncSpy).toHaveBeenCalled();
-            expect(readFileSyncSpy).toHaveBeenCalled();
-            expect(writeFileSyncSpy).toHaveBeenCalled();
-
-            // mock manifest JSON Object
-            let manifestJsonObj = {
-                background_color: '#FFF',
-                display: 'standalone',
-                orientation: 'portrait',
-                start_url: 'index.html',
-                name: 'HelloWorld',
-                short_name: 'HelloWorld',
-                version: 'whatever',
-                description: 'A sample Apache Cordova application.',
-                author: 'Cordova Team',
-                icons: [{ src: 'res/electron/cordova.png', type: 'image/png', sizes: '16x16' }]
-            };
-
-            const manifestPath = writeFileSyncSpy.calls.argsFor(0)[0];
-            expect(manifestPath).toEqual(path.join('mock', 'www', 'manifest.json'));
-
-            // get manifest json file content and remove white spaces
-            let manifestFile = writeFileSyncSpy.calls.argsFor(0)[1];
-
-            // convert to remove white space
-            manifestFile = manifestFile.replace(/\s+/g, '');
-            manifestJsonObj = JSON.stringify(manifestJsonObj).replace(/\s/g, '');
-            expect(manifestFile).toEqual(manifestJsonObj);
-
-            const manifestFormat = writeFileSyncSpy.calls.argsFor(0)[2];
-            expect(manifestFormat).toEqual('utf8');
-        });
+        expect(fs.writeFileSync).toHaveBeenCalledWith(
+            jasmine.any(String),
+            JSON.stringify(expectedManifest, null, 2),
+            'utf8'
+        );
     });
 });
diff --git a/tests/spec/unit/templates/cordova/lib/PackageJsonParser.spec.js b/tests/spec/unit/templates/cordova/lib/PackageJsonParser.spec.js
index f85360f..c3975c1 100644
--- a/tests/spec/unit/templates/cordova/lib/PackageJsonParser.spec.js
+++ b/tests/spec/unit/templates/cordova/lib/PackageJsonParser.spec.js
@@ -22,13 +22,14 @@
 const rewire = require('rewire');
 
 const PackageJsonParser = rewire('../../../../../../bin/templates/cordova/lib/PackageJsonParser');
-const ConfigParser = require('cordova-common').ConfigParser;
+const { ConfigParser, events } = require('cordova-common');
 
 const FIXTURES = path.join(__dirname, '..', '..', '..', '..', 'fixtures');
 
 // Create a real config object before mocking out everything.
 const cfg = new ConfigParser(path.join(FIXTURES, 'test-config-1.xml'));
 const cfgEmpty = new ConfigParser(path.join(FIXTURES, 'test-config-empty.xml'));
+const cfgNoAuthorCustomEmail = new ConfigParser(path.join(FIXTURES, 'test-config-no-author-custom-email.xml'));
 
 const defaultMockProjectPackageJson = {
     name: 'io.cordova.electronTest',
@@ -37,7 +38,10 @@
     description: 'A Sample Apache Cordova Electron Application.',
     author: 'Apache Cordova Team',
     license: 'Apache-2.0',
-    dependencies: { 'cordova-electron': '^1.0.2' },
+    dependencies: {
+        'cordova-electron': '^1.0.0',
+        'cordova-plugin-camera': '^1.0.0'
+    },
     devDependencies: {},
     cordova: {
         plugins: {},
@@ -51,133 +55,130 @@
     configXml: path.join('mock', 'config.xml')
 };
 
+const defaultInitPackageObj = { main: 'cdv-electron-main.js' };
+
 describe('PackageJsonParser class', () => {
     let packageJsonParser;
-    let emitSpy;
 
     beforeEach(() => {
-        packageJsonParser = PackageJsonParser.__get__('PackageJsonParser');
-
-        emitSpy = jasmine.createSpy('emit');
-        packageJsonParser.__set__('events', { emit: emitSpy });
-    });
-
-    it('should should be defined.', () => {
-        expect(packageJsonParser).toBeDefined();
-    });
-
-    it('should set initial value correctly.', () => {
         packageJsonParser = new PackageJsonParser(locations.www, '/root/project');
+        spyOn(events, 'emit');
+    });
 
-        // mock package JSON Object
-        const packageJsonObj = { main: 'cdv-electron-main.js' };
-
-        expect(packageJsonParser.path).toEqual(path.join('mock', 'www', 'package.json'));
+    it('should have been constructed with initial values.', () => {
+        expect(packageJsonParser).toBeDefined();
+        expect(packageJsonParser.path).toEqual(path.join(locations.www, 'package.json'));
         expect(packageJsonParser.www).toEqual(locations.www);
-        expect(packageJsonParser.package).toEqual(packageJsonObj);
+        expect(packageJsonParser.package).toEqual(defaultInitPackageObj);
     });
 
-    it('should return when config xml is not defined.', () => {
-        packageJsonParser = new PackageJsonParser(locations.www, '/root/project')
-            .configure(undefined);
+    it('should not modify the package object when config is not provided.', () => {
+        packageJsonParser.configure();
+        // the package object should be the same as it was initialized
+        expect(packageJsonParser.package).toEqual(defaultInitPackageObj);
+    });
 
-        // mock package JSON Object
-        const packageJsonObj = { main: 'cdv-electron-main.js' };
+    it('should update the package object with default values, when config.xml is empty.', () => {
+        packageJsonParser.configure(cfgEmpty, defaultMockProjectPackageJson);
+
+        // Expected Mock Package Object
+        const packageJsonObj = Object.assign({}, defaultInitPackageObj, {
+            name: 'io.cordova.hellocordova',
+            displayName: 'HelloCordova',
+            version: '1.0.0',
+            description: 'A sample Apache Cordova application that responds to the deviceready event.',
+            dependencies: {
+                'cordova-electron': '^1.0.0',
+                'cordova-plugin-camera': '^1.0.0'
+            },
+            homepage: 'https://cordova.io',
+            license: 'Apache-2.0',
+            author: 'Apache Cordova Team'
+        });
 
         expect(packageJsonParser.package).toEqual(packageJsonObj);
     });
 
-    it('should set package json object values to default, when config xml is empty.', () => {
-        packageJsonParser = new PackageJsonParser(locations.www, '/root/project')
-            .configure(cfgEmpty, defaultMockProjectPackageJson);
+    it('should update package object with values from config.xml.', () => {
+        packageJsonParser.configure(cfg, defaultMockProjectPackageJson);
 
-        // mock package JSON Object
-        const packageJsonObj = {
-            package:
-                {
-                    main: 'cdv-electron-main.js',
-                    name: 'io.cordova.hellocordova',
-                    displayName: 'HelloCordova',
-                    version: '1.0.0',
-                    description: 'A sample Apache Cordova application that responds to the deviceready event.',
-                    dependencies: { 'cordova-electron': '^1.0.2' },
-                    homepage: 'https://cordova.io',
-                    license: 'Apache-2.0',
-                    author: 'Apache Cordova Team'
-                }
-        };
+        // Expected Mock Package Object
+        const packageJsonObj = Object.assign({}, defaultInitPackageObj, {
+            name: 'whatever',
+            displayName: 'HelloWorld',
+            version: '1.1.1',
+            description: 'A sample Apache Cordova application.',
+            dependencies: {
+                'cordova-electron': '^1.0.0',
+                'cordova-plugin-camera': '^1.0.0'
+            },
+            homepage: 'http://cordova.io',
+            license: 'Apache 2.0 License',
+            author: { name: 'Cordova Team', email: 'dev@cordova.com' }
+        });
 
-        expect(packageJsonParser.package).toEqual(packageJsonObj.package);
+        expect(packageJsonParser.package).toEqual(packageJsonObj);
     });
 
-    it('should read and set package json object values from config xml.', () => {
-        packageJsonParser = new PackageJsonParser(locations.www, '/root/project')
-            .configure(cfg, defaultMockProjectPackageJson);
+    it('should warn when any of the dependencies contain cordova-* is defined but not for cordova-plugin-*.', () => {
+        packageJsonParser.configure(cfg, defaultMockProjectPackageJson);
 
-        // mock package JSON Object
-        const packageJsonObj = {
-            package:
-                {
-                    main: 'cdv-electron-main.js',
-                    name: 'whatever',
-                    displayName: 'HelloWorld',
-                    version: '1.1.1',
-                    description: 'A sample Apache Cordova application.',
-                    dependencies: { 'cordova-electron': '^1.0.2' },
-                    homepage: 'http://cordova.io',
-                    license: 'Apache 2.0 License',
-                    author: { name: 'Cordova Team', email: 'dev@cordova.com' }
-                }
-        };
+        expect(events.emit).toHaveBeenCalledWith(
+            'warn',
+            jasmine.stringMatching(/^\[Cordova Electron\] The built package size/)
+        );
 
-        expect(packageJsonParser.package).toEqual(packageJsonObj.package);
+        expect(events.emit).toHaveBeenCalledWith(
+            'verbose',
+            jasmine.stringMatching(/The following Cordova package\(s\) were detected/)
+        );
+
+        expect(events.emit).toHaveBeenCalledWith(
+            'verbose',
+            jasmine.stringMatching(/cordova-electron/)
+        );
+
+        expect(events.emit).not.toHaveBeenCalledWith(
+            'verbose',
+            jasmine.stringMatching(/cordova-plugin-camera/)
+        );
     });
 
-    it('should warn when "cordova-*" prefixed dependency is defined in package.json dependencies.', () => {
-        packageJsonParser = new PackageJsonParser(locations.www, '/root/project')
-            .configure(cfg, defaultMockProjectPackageJson);
-
-        expect(emitSpy).toHaveBeenCalled();
-
-        let actual = emitSpy.calls.argsFor(0)[1];
-        let expectedPartial = '[Cordova Electron] The built package size may be larger than necessary.';
-        expect(actual).toContain(expectedPartial);
-
-        actual = emitSpy.calls.argsFor(1)[1];
-        expectedPartial = '[Cordova Electron] The following Cordova package(s) were detected as "dependencies" in the projects "package.json" file.';
-        expect(actual).toContain(expectedPartial);
-        expect(actual).toContain('cordova-electron');
+    it('should set default author when missing but author email is defined.', () => {
+        packageJsonParser.configure(cfgNoAuthorCustomEmail, defaultMockProjectPackageJson);
+        expect(packageJsonParser.package.author).toEqual({
+            name: 'Apache Cordova Team',
+            email: 'dev@cordova.com'
+        });
     });
 
-    it('should not warn when "cordova-*" prefixed dependency is not defined in package.json dependencies.', () => {
+    it('should not warn when any cordova-* packages are defined as devDependency.', () => {
+        // Fix defaultMockProjectPackageJson where cordova-* is devDependency
         const mockProjectPackageJson = Object.assign({}, defaultMockProjectPackageJson);
         mockProjectPackageJson.devDependencies = Object.assign({}, defaultMockProjectPackageJson.dependencies);
         mockProjectPackageJson.dependencies = { foobar: '1.0.0' }; // setting a non "cordova-" dependency
 
-        packageJsonParser = new PackageJsonParser(locations.www, '/root/project')
-            .configure(cfg, mockProjectPackageJson);
+        packageJsonParser.configure(cfg, mockProjectPackageJson);
 
-        expect(emitSpy).not.toHaveBeenCalled();
+        expect(events.emit).not.toHaveBeenCalled();
     });
 
     it('should skip configuring the Electron app\'s dependencies when the Cordova project\'s package.json dependencies are not set.', () => {
         const mockProjectPackageJson = Object.assign({}, defaultMockProjectPackageJson);
         mockProjectPackageJson.dependencies = {};
 
-        packageJsonParser = new PackageJsonParser(locations.www, '/root/project')
-            .configure(cfg, mockProjectPackageJson);
+        packageJsonParser.configure(cfg, mockProjectPackageJson);
 
-        expect(emitSpy).not.toHaveBeenCalled();
+        expect(events.emit).not.toHaveBeenCalled();
     });
 
     it('should skip preparing npm packages that already contain absolute paths.', () => {
         const mockProjectPackageJson = Object.assign({}, defaultMockProjectPackageJson);
         mockProjectPackageJson.dependencies = { foobar: 'file:/tmp/foobar.tar.gz' };
 
-        packageJsonParser = new PackageJsonParser(locations.www, '/root/project')
-            .configure(cfg, mockProjectPackageJson);
+        packageJsonParser.configure(cfg, mockProjectPackageJson);
 
-        expect(emitSpy).not.toHaveBeenCalled();
+        expect(events.emit).not.toHaveBeenCalled();
     });
 
     it('should convert npm packages that contain relative path to be absolute paths.', () => {
@@ -186,10 +187,9 @@
 
         spyOn(fs, 'pathExistsSync').and.returnValue(true);
 
-        packageJsonParser = new PackageJsonParser(locations.www, '/root/project', '/root/project')
-            .configure(cfg, mockProjectPackageJson);
+        packageJsonParser.configure(cfg, mockProjectPackageJson);
 
-        expect(emitSpy).not.toHaveBeenCalled();
+        expect(events.emit).not.toHaveBeenCalled();
     });
 
     it('should warn that an npm packages will be dropped when the absolute path could not be found.', () => {
@@ -198,13 +198,17 @@
 
         spyOn(fs, 'pathExistsSync').and.returnValue(false);
 
-        packageJsonParser = new PackageJsonParser(locations.www, '/root/project', '/root/project')
-            .configure(cfg, mockProjectPackageJson);
+        packageJsonParser.configure(cfg, mockProjectPackageJson);
 
-        const actual = emitSpy.calls.argsFor(0)[1];
-        const expectedPartial = '[Cordova Electron] The following local npm dependencies could not be located and will not be deployed to the Electron app:';
-        expect(actual).toContain(expectedPartial);
-        expect(actual).toContain('foobar');
+        expect(events.emit).toHaveBeenCalledWith(
+            'warn',
+            jasmine.stringMatching(/^\[Cordova Electron\] The following local npm dependencies could not be located and will not be deployed/)
+        );
+
+        expect(events.emit).toHaveBeenCalledWith(
+            'warn',
+            jasmine.stringMatching(/foobar/)
+        );
     });
 
     it('should not set package dependencies when project dependencies is missing.', () => {
@@ -212,92 +216,32 @@
         mockProjectPackageJson.devDependencies = Object.assign({}, defaultMockProjectPackageJson.dependencies);
         delete mockProjectPackageJson.dependencies;
 
-        packageJsonParser = new PackageJsonParser(locations.www, '/root/project')
-            .configure(cfg, mockProjectPackageJson);
+        packageJsonParser.configure(cfg, mockProjectPackageJson);
 
-        expect(emitSpy).not.toHaveBeenCalled();
+        expect(events.emit).not.toHaveBeenCalled();
         expect(packageJsonParser.package.dependencies).not.toBeDefined();
     });
 
-    it('should write provided data when config xml is empty.', () => {
-        const writeFileSyncSpy = jasmine.createSpy('writeFileSync');
-        packageJsonParser.__set__('fs', { writeFileSync: writeFileSyncSpy });
+    it('should write something.', () => {
+        spyOn(fs, 'writeFileSync').and.returnValue(true);
+        packageJsonParser.write();
 
-        packageJsonParser = new PackageJsonParser(locations.www, '/root/project')
-            .configure(cfgEmpty, defaultMockProjectPackageJson)
-            .write();
-
-        expect(writeFileSyncSpy).toHaveBeenCalled();
-
-        // mock package JSON Object
-        let packageJsonObj = {
-            package:
-                {
-                    main: 'cdv-electron-main.js',
-                    name: 'io.cordova.hellocordova',
-                    displayName: 'HelloCordova',
-                    version: '1.0.0',
-                    description: 'A sample Apache Cordova application that responds to the deviceready event.',
-                    dependencies: { 'cordova-electron': '^1.0.2' },
-                    homepage: 'https://cordova.io',
-                    license: 'Apache-2.0',
-                    author: 'Apache Cordova Team'
-                }
-        };
-
-        const packagePath = writeFileSyncSpy.calls.argsFor(0)[0];
-        expect(packagePath).toEqual(path.join('mock', 'www', 'package.json'));
-
-        // get package json file content and remove white spaces
-        let packageFile = writeFileSyncSpy.calls.argsFor(0)[1];
-
-        // convert to remove white space
-        packageFile = packageFile.replace(/\s+/g, '');
-        packageJsonObj = JSON.stringify(packageJsonObj.package).replace(/\s/g, '');
-        expect(packageFile).toEqual(packageJsonObj);
-
-        const packageFormat = writeFileSyncSpy.calls.argsFor(0)[2];
-        expect(packageFormat).toEqual('utf8');
+        expect(fs.writeFileSync).toHaveBeenCalledWith(
+            jasmine.any(String),
+            JSON.stringify(defaultInitPackageObj, null, 2),
+            'utf8'
+        );
     });
 
-    it('should write package json object values from config xml.', () => {
-        const writeFileSyncSpy = jasmine.createSpy('writeFileSync');
-        packageJsonParser.__set__('fs', { writeFileSync: writeFileSyncSpy });
-
-        packageJsonParser = new PackageJsonParser(locations.www, '/root/project')
-            .configure(cfg, defaultMockProjectPackageJson)
+    it('should write package object out with user custom defined values.', () => {
+        spyOn(fs, 'writeFileSync').and.returnValue(true);
+        packageJsonParser.configure(cfg, defaultMockProjectPackageJson)
             .write();
 
-        expect(writeFileSyncSpy).toHaveBeenCalled();
-
-        // mock package JSON Object
-        let packageJsonObj = {
-            package:
-                {
-                    main: 'cdv-electron-main.js',
-                    name: 'whatever',
-                    displayName: 'HelloWorld',
-                    version: '1.1.1',
-                    description: 'A sample Apache Cordova application.',
-                    dependencies: { 'cordova-electron': '^1.0.2' },
-                    homepage: 'http://cordova.io',
-                    license: 'Apache 2.0 License',
-                    author: { name: 'Cordova Team', email: 'dev@cordova.com' }
-                }
-        };
-
-        const packagePath = writeFileSyncSpy.calls.argsFor(0)[0];
-        expect(packagePath).toEqual(path.join('mock', 'www', 'package.json'));
-
-        // get package json file content and remove white spaces
-        let packageFile = writeFileSyncSpy.calls.argsFor(0)[1];
-
-        // convert to remove white space
-        packageFile = packageFile.replace(/\s+/g, '');
-        packageJsonObj = JSON.stringify(packageJsonObj.package).replace(/\s/g, '');
-        expect(packageFile).toEqual(packageJsonObj);
-
-        const packageFormat = writeFileSyncSpy.calls.argsFor(0)[2];
-        expect(packageFormat).toEqual('utf8');
+        expect(fs.writeFileSync).toHaveBeenCalledWith(
+            jasmine.any(String),
+            jasmine.stringMatching(/whatever/),
+            'utf8'
+        );
     });
 });
diff --git a/tests/spec/unit/templates/parser.spec.js b/tests/spec/unit/templates/parser.spec.js
index 0062cc0..4c4fadc 100644
--- a/tests/spec/unit/templates/parser.spec.js
+++ b/tests/spec/unit/templates/parser.spec.js
@@ -17,15 +17,16 @@
     under the License.
 */
 
-const rewire = require('rewire');
 const path = require('path');
-const CordovaError = require('cordova-common').CordovaError;
+const fs = require('fs-extra');
+const rewire = require('rewire');
+const { CordovaError, events } = require('cordova-common');
 const Parser = rewire('../../../../bin/templates/cordova/parser');
 
+const mockProjectPath = 'mock_project_path';
+
 describe('Parser class', () => {
     describe('Constructing parser instanse', () => {
-        const mockProjectPath = 'mock_project_path';
-
         it('should not have valid electron project', () => {
             Parser.__set__('dirExists', jasmine.createSpy('dirExists').and.returnValue(false));
 
@@ -36,63 +37,35 @@
 
         it('should have valid electron project and set path', () => {
             Parser.__set__('dirExists', jasmine.createSpy('dirExists').and.returnValue(true));
-
             const parser = new Parser(mockProjectPath);
-
             expect(parser.path).toBe(mockProjectPath);
         });
     });
 
-    describe('update_from_config method', () => {
-        const mockProjectPath = 'mock_project_path';
+    describe('Class Methods', () => {
+        let parser;
 
-        it('should return a resolved promise.', () => {
+        beforeEach(() => {
             Parser.__set__('dirExists', jasmine.createSpy('dirExists').and.returnValue(true));
-
-            const parser = new Parser(mockProjectPath);
-            const actual = parser.update_from_config();
-
-            expect(typeof actual).toBe(typeof Promise.resolve());
+            parser = new Parser(mockProjectPath);
         });
-    });
 
-    describe('www_dir method', () => {
-        const mockProjectPath = 'mock_project_path';
-
-        it('should return the projects www dir path.', () => {
-            Parser.__set__('dirExists', jasmine.createSpy('dirExists').and.returnValue(true));
-
-            const parser = new Parser(mockProjectPath);
-            const actual = parser.www_dir();
-
-            expect(actual).toBe(path.join(mockProjectPath, 'www'));
+        describe('update_from_config method', () => {
+            it('should return a resolved promise.', () => {
+                const actual = parser.update_from_config();
+                expect(typeof actual).toBe(typeof Promise.resolve());
+            });
         });
-    });
 
-    describe('update_www method', () => {
-        const mockProjectPath = 'mock_project_path';
-
-        it('should detect merges/electron file path and merge/update user source files into the platform staging dir', () => {
-            Parser.__set__('dirExists', jasmine.createSpy('dirExists').and.returnValue(true));
-
-            const fsExistsSyncSpy = jasmine.createSpy('existsSync').and.returnValue(true);
-            Parser.__set__('fs', {
-                existsSync: fsExistsSyncSpy
+        describe('www_dir method', () => {
+            it('should return the projects www dir path.', () => {
+                const actual = parser.www_dir();
+                expect(actual).toBe(path.join(mockProjectPath, 'www'));
             });
+        });
 
-            const emitSpy = jasmine.createSpy('emit');
-            Parser.__set__('events', {
-                emit: emitSpy
-            });
-
-            const mergeAndUpdateDirSpy = jasmine.createSpy('mergeAndUpdateDir');
-            Parser.__set__('FileUpdater', {
-                mergeAndUpdateDir: mergeAndUpdateDirSpy
-            });
-
-            const parser = new Parser(mockProjectPath);
-
-            const cordovaProject = {
+        describe('update_www method', () => {
+            const mockConfigParser = {
                 root: 'mock',
                 projectConfig: {
                     path: path.join('mock', 'config.xml'),
@@ -103,134 +76,102 @@
                 }
             };
 
-            parser.update_www(cordovaProject);
+            it('should detect merges/electron file path and merge/update user source files into the platform staging dir', () => {
+                spyOn(fs, 'existsSync').and.returnValue(true);
+                spyOn(events, 'emit');
 
-            expect(fsExistsSyncSpy).toHaveBeenCalled();
-            expect(emitSpy).toHaveBeenCalled();
+                const mergeAndUpdateDirSpy = jasmine.createSpy('mergeAndUpdateDir');
+                Parser.__set__('FileUpdater', {
+                    mergeAndUpdateDir: mergeAndUpdateDirSpy
+                });
 
-            // The emit message was.
-            let actualEmit = emitSpy.calls.argsFor(0)[1];
-            let expectedEmit = 'Found "merges/electron" folder. Copying its contents into the electron project.';
-            expect(actualEmit).toBe(expectedEmit);
+                parser.update_www(mockConfigParser);
 
-            const expectedSourceDirs = [
-                'www',
-                path.join('..', mockProjectPath, 'platform_www'),
-                path.join('merges', 'electron')
-            ];
-            const expectedTargetDir = path.join('..', mockProjectPath, 'www');
-            actualEmit = emitSpy.calls.argsFor(1)[1];
-            expectedEmit = `Merging and updating files from [${expectedSourceDirs.join(', ')}] to ${expectedTargetDir}`;
-            expect(actualEmit).toBe(expectedEmit);
+                expect(events.emit).toHaveBeenCalledWith(
+                    'verbose',
+                    'Found "merges/electron" folder. Copying its contents into the electron project.'
+                );
 
-            expect(mergeAndUpdateDirSpy).toHaveBeenCalled();
+                const expectedSourceDirs = [
+                    'www',
+                    path.join('..', mockProjectPath, 'platform_www'),
+                    path.join('merges', 'electron')
+                ];
+                const expectedTargetDir = path.join('..', mockProjectPath, 'www');
+
+                expect(events.emit).toHaveBeenCalledWith(
+                    'verbose',
+                    `Merging and updating files from [${expectedSourceDirs.join(', ')}] to ${expectedTargetDir}`
+                );
+
+                expect(mergeAndUpdateDirSpy).toHaveBeenCalled();
+            });
+
+            it('should detect merges/electron file path and merge/update user source files into the platform staging dir', () => {
+                spyOn(fs, 'existsSync').and.returnValue(false);
+                spyOn(events, 'emit');
+
+                const mergeAndUpdateDirSpy = jasmine.createSpy('mergeAndUpdateDir');
+                Parser.__set__('FileUpdater', {
+                    mergeAndUpdateDir: mergeAndUpdateDirSpy
+                });
+
+                parser.update_www(mockConfigParser);
+
+                expect(fs.existsSync).toHaveBeenCalled();
+                expect(events.emit).toHaveBeenCalled();
+
+                // The emit message was.
+                let actualEmit = events.emit.calls.argsFor(0)[1];
+                let expectedEmit = 'Found "merges/electron" folder. Copying its contents into the electron project.';
+                expect(actualEmit).not.toBe(expectedEmit);
+
+                const expectedSourceDirs = [
+                    'www',
+                    path.join('..', mockProjectPath, 'platform_www')
+                ];
+                const expectedTargetDir = path.join('..', mockProjectPath, 'www');
+                actualEmit = events.emit.calls.argsFor(0)[1];
+                expectedEmit = `Merging and updating files from [${expectedSourceDirs.join(', ')}] to ${expectedTargetDir}`;
+                expect(actualEmit).toBe(expectedEmit);
+                expect(mergeAndUpdateDirSpy).toHaveBeenCalled();
+            });
         });
 
-        it('should detect merges/electron file path and merge/update user source files into the platform staging dir', () => {
-            Parser.__set__('dirExists', jasmine.createSpy('dirExists').and.returnValue(true));
-
-            const fsExistsSyncSpy = jasmine.createSpy('existsSync').and.returnValue(false);
-            Parser.__set__('fs', {
-                existsSync: fsExistsSyncSpy
+        describe('config_xml method', () => {
+            it('should return the projects config.xml file path.', () => {
+                expect(parser.config_xml()).toBe(path.join(mockProjectPath, 'config.xml'));
             });
-
-            const emitSpy = jasmine.createSpy('emit');
-            Parser.__set__('events', {
-                emit: emitSpy
-            });
-
-            const mergeAndUpdateDirSpy = jasmine.createSpy('mergeAndUpdateDir');
-            Parser.__set__('FileUpdater', {
-                mergeAndUpdateDir: mergeAndUpdateDirSpy
-            });
-
-            const parser = new Parser(mockProjectPath);
-
-            const cordovaProject = {
-                root: 'mock',
-                projectConfig: {
-                    path: path.join('mock', 'config.xml'),
-                    cdvNamespacePrefix: 'cdv'
-                },
-                locations: {
-                    www: path.join('mock', 'www')
-                }
-            };
-
-            parser.update_www(cordovaProject);
-
-            expect(fsExistsSyncSpy).toHaveBeenCalled();
-            expect(emitSpy).toHaveBeenCalled();
-
-            // The emit message was.
-            let actualEmit = emitSpy.calls.argsFor(0)[1];
-            let expectedEmit = 'Found "merges/electron" folder. Copying its contents into the electron project.';
-            expect(actualEmit).not.toBe(expectedEmit);
-
-            const expectedSourceDirs = [
-                'www',
-                path.join('..', mockProjectPath, 'platform_www')
-            ];
-            const expectedTargetDir = path.join('..', mockProjectPath, 'www');
-            actualEmit = emitSpy.calls.argsFor(0)[1];
-            expectedEmit = `Merging and updating files from [${expectedSourceDirs.join(', ')}] to ${expectedTargetDir}`;
-            expect(actualEmit).toBe(expectedEmit);
-
-            expect(mergeAndUpdateDirSpy).toHaveBeenCalled();
         });
-    });
 
-    describe('config_xml method', () => {
-        const mockProjectPath = 'mock_project_path';
+        describe('update_project method', () => {
+            it('should copy munged config.xml to platform www dir.', () => {
+                const fsCopySyncSpy = jasmine.createSpy('copySync');
+                Parser.__set__('fs', { copySync: fsCopySyncSpy });
 
-        it('should return the projects config.xml file path.', () => {
-            Parser.__set__('dirExists', jasmine.createSpy('dirExists').and.returnValue(true));
+                parser.update_project().then(() => {
+                    const actualSrc = fsCopySyncSpy.calls.argsFor(0)[0];
+                    const actualDest = fsCopySyncSpy.calls.argsFor(0)[1];
 
-            const parser = new Parser(mockProjectPath);
-            const actual = parser.config_xml();
-
-            expect(actual).toBe(path.join(mockProjectPath, 'config.xml'));
-        });
-    });
-
-    describe('update_project method', () => {
-        const mockProjectPath = 'mock_project_path';
-
-        it('should copy munged config.xml to platform www dir.', () => {
-            const fsCopySyncSpy = jasmine.createSpy('copySync');
-            Parser.__set__('fs', {
-                copySync: fsCopySyncSpy
-            });
-
-            Parser.__set__('dirExists', jasmine.createSpy('dirExists').and.returnValue(true));
-
-            const parser = new Parser(mockProjectPath);
-
-            parser.update_project().then(() => {
-                expect(fsCopySyncSpy).toHaveBeenCalled();
-
-                const actualSrc = fsCopySyncSpy.calls.argsFor(0)[0];
-                const actualDest = fsCopySyncSpy.calls.argsFor(0)[1];
-                expect(actualSrc).toBe(path.join(mockProjectPath, 'config.xml'));
-                expect(actualDest).toBe(path.join(mockProjectPath, 'www', 'config.xml'));
+                    expect(fsCopySyncSpy).toHaveBeenCalled();
+                    expect(actualSrc).toBe(path.join(mockProjectPath, 'config.xml'));
+                    expect(actualDest).toBe(path.join(mockProjectPath, 'www', 'config.xml'));
+                });
             });
         });
     });
 
     describe('logFileOp method', () => {
         it('should emit passed in message.', () => {
-            const emitSpy = jasmine.createSpy('emit');
-            Parser.__set__('events', {
-                emit: emitSpy
-            });
+            spyOn(events, 'emit');
 
             const msg = 'random message';
             const logFileOp = Parser.__get__('logFileOp');
             logFileOp(msg);
 
             // The emit message was.
-            const actualEmit = emitSpy.calls.argsFor(0)[1];
-            expect(emitSpy).toHaveBeenCalled();
+            const actualEmit = events.emit.calls.argsFor(0)[1];
+            expect(events.emit).toHaveBeenCalled();
             expect(actualEmit).toBe(`  ${msg}`);
         });
     });