Normalize whitespace (#46)
authorRaphael von der Grün <raphinesse@gmail.com>
Sun, 9 Sep 2018 13:36:17 +0000 (15:36 +0200)
committerGitHub <noreply@github.com>
Sun, 9 Sep 2018 13:36:17 +0000 (15:36 +0200)
23 files changed:
.eslintignore
RELEASENOTES.md
spec/.eslintrc.yml
spec/PlatformJson.spec.js
spec/PluginManager.spec.js
spec/fixtures/plugins/ChildBrowser/plugin.xml
spec/fixtures/plugins/ChildBrowser/src/android/ChildBrowser.java
spec/fixtures/plugins/com.adobe.vars/plugin.xml
spec/fixtures/plugins/org.test.multiple-children/plugin.xml
spec/fixtures/plugins/org.test.src/plugin.xml
spec/fixtures/projects/android/AndroidManifest.xml
spec/fixtures/projects/android_two/AndroidManifest.xml
spec/fixtures/projects/android_two_no_perms/AndroidManifest.xml
spec/fixtures/projects/ios-config-xml/SampleApp/SampleApp-Info.plist
spec/fixtures/projects/ios-config-xml/SampleApp/config.xml
spec/fixtures/projects/windows/TestApp.jsproj
spec/fixtures/projects/windows/TestApp.projitems
spec/fixtures/projects/windows/bom_test.xml
spec/fixtures/projects/windows/www/cordova-2.6.0.js
spec/fixtures/projects/windows/www/css/index.css
spec/superspawn.spec.js
src/CordovaLogger.js
src/PluginManager.js

index 161d0c6..d606f61 100644 (file)
@@ -1 +1 @@
-spec/fixtures/*
\ No newline at end of file
+spec/fixtures/*
index 409fc6f..456f48b 100644 (file)
@@ -35,7 +35,7 @@
 * [CB-13674](https://issues.apache.org/jira/browse/CB-13674): updated dependencies
 
 ### 2.2.0 (Nov 22, 2017)
-* [CB-13471](https://issues.apache.org/jira/browse/CB-13471) File Provider fix belongs in cordova-common 
+* [CB-13471](https://issues.apache.org/jira/browse/CB-13471) File Provider fix belongs in cordova-common
 * [CB-11244](https://issues.apache.org/jira/browse/CB-11244) Spot fix for upcoming `cordova-android@7` changes. https://github.com/apache/cordova-android/pull/389
 
 ### 2.1.1 (Oct 04, 2017)
index 6afba65..043fd14 100644 (file)
@@ -1,2 +1,2 @@
 env:
-    jasmine: true
\ No newline at end of file
+    jasmine: true
index d2c3c7e..f6845bd 100644 (file)
-/**\r
-    Licensed to the Apache Software Foundation (ASF) under one\r
-    or more contributor license agreements.  See the NOTICE file\r
-    distributed with this work for additional information\r
-    regarding copyright ownership.  The ASF licenses this file\r
-    to you under the Apache License, Version 2.0 (the\r
-    "License"); you may not use this file except in compliance\r
-    with the License.  You may obtain a copy of the License at\r
-\r
-    http://www.apache.org/licenses/LICENSE-2.0\r
-\r
-    Unless required by applicable law or agreed to in writing,\r
-    software distributed under the License is distributed on an\r
-    "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY\r
-    KIND, either express or implied.  See the License for the\r
-    specific language governing permissions and limitations\r
-    under the License.\r
-*/\r
-\r
-var rewire = require('rewire');\r
-var PlatformJson = rewire('../src/PlatformJson');\r
-var ModuleMetadata = PlatformJson.__get__('ModuleMetadata');\r
-\r
-var FAKE_MODULE = {\r
-    name: 'fakeModule',\r
-    src: 'www/fakeModule.js',\r
-    clobbers: [{target: 'window.fakeClobber'}],\r
-    merges: [{target: 'window.fakeMerge'}],\r
-    runs: true\r
-};\r
-\r
-describe('PlatformJson class', function () {\r
-    it('Test 001 : should be constructable', function () {\r
-        expect(new PlatformJson()).toEqual(jasmine.any(PlatformJson));\r
-    });\r
-\r
-    describe('instance', function () {\r
-        var platformJson;\r
-        var fakePlugin;\r
-\r
-        beforeEach(function () {\r
-            platformJson = new PlatformJson('/fake/path', 'android');\r
-            fakePlugin = jasmine.createSpyObj('fakePlugin', ['getJsModules']);\r
-            fakePlugin.id = 'fakeId';\r
-            fakePlugin.version = '1.0.0';\r
-            fakePlugin.getJsModules.and.returnValue([FAKE_MODULE]);\r
-        });\r
-\r
-        describe('addPluginMetadata method', function () {\r
-            it('Test 002 : should not throw if root "modules" property is missing', function () {\r
-                expect(function () {\r
-                    platformJson.addPluginMetadata(fakePlugin);\r
-                }).not.toThrow();\r
-            });\r
-\r
-            it('Test 003 : should add each module to "root.modules" array', function () {\r
-                platformJson.addPluginMetadata(fakePlugin);\r
-                expect(platformJson.root.modules.length).toBe(1);\r
-                expect(platformJson.root.modules[0]).toEqual(jasmine.any(ModuleMetadata));\r
-            });\r
-\r
-            it('Test 004 : shouldn\'t add module if there is already module with the same file added', function () {\r
-                platformJson.root.modules = [{\r
-                    name: 'fakePlugin2',\r
-                    file: 'plugins/fakeId/www/fakeModule.js'\r
-                }];\r
-\r
-                platformJson.addPluginMetadata(fakePlugin);\r
-                expect(platformJson.root.modules.length).toBe(1);\r
-                expect(platformJson.root.modules[0].name).toBe('fakePlugin2');\r
-            });\r
-\r
-            it('Test 005 : should add entry to plugin_metadata with corresponding version', function () {\r
-                platformJson.addPluginMetadata(fakePlugin);\r
-                expect(platformJson.root.plugin_metadata[fakePlugin.id]).toBe(fakePlugin.version);\r
-            });\r
-        });\r
-\r
-        describe('removePluginMetadata method', function () {\r
-            it('Test 006 : should not throw if root "modules" property is missing', function () {\r
-                expect(function () {\r
-                    platformJson.removePluginMetadata(fakePlugin);\r
-                }).not.toThrow();\r
-            });\r
-\r
-            it('Test 007 : should remove plugin modules from "root.modules" array based on file path', function () {\r
-\r
-                var pluginPaths = [\r
-                    'plugins/fakeId/www/fakeModule.js',\r
-                    'plugins/otherPlugin/www/module1.js',\r
-                    'plugins/otherPlugin/www/module1.js'\r
-                ];\r
-\r
-                platformJson.root.modules = pluginPaths.map(function (p) { return {file: p}; });\r
-                platformJson.removePluginMetadata(fakePlugin);\r
-                var resultantPaths = platformJson.root.modules\r
-                    .map(function (p) { return p.file; })\r
-                    .filter(function (f) { return /fakeModule\.js$/.test(f); });\r
-\r
-                expect(resultantPaths.length).toBe(0);\r
-            });\r
-\r
-            it('Test 008 : should remove entry from plugin_metadata with corresponding version', function () {\r
-                platformJson.root.plugin_metadata = {};\r
-                platformJson.root.plugin_metadata[fakePlugin.id] = fakePlugin.version;\r
-                platformJson.removePluginMetadata(fakePlugin);\r
-                expect(platformJson.root.plugin_metadata[fakePlugin.id]).not.toBeDefined();\r
-            });\r
-        });\r
-\r
-        function evaluateCordovaDefineStatement (str) {\r
-            expect(typeof str).toBe('string');\r
-            const fnString = str.replace(/^\s*cordova\.define\('cordova\/plugin_list',\s*([\s\S]+)\);\s*$/, '($1)');\r
-            const mod = {exports: {}};\r
-            global.eval(fnString)(null, mod.exports, mod); // eslint-disable-line no-eval\r
-            return mod;\r
-        }\r
-\r
-        function expectedMetadata () {\r
-            // Create plain objects from ModuleMetadata instances\r
-            const modules = platformJson.root.modules.map(o => Object.assign({}, o));\r
-            modules.metadata = platformJson.root.plugin_metadata;\r
-            return modules;\r
-        }\r
-\r
-        describe('generateMetadata method', function () {\r
-            it('Test 009 : should generate text metadata containing list of installed modules', function () {\r
-                const meta = platformJson.addPluginMetadata(fakePlugin).generateMetadata();\r
-                const mod = evaluateCordovaDefineStatement(meta);\r
-\r
-                expect(mod.exports).toEqual(expectedMetadata());\r
-            });\r
-        });\r
-\r
-        describe('generateAndSaveMetadata method', function () {\r
-            it('should save generated metadata', function () {\r
-                // Needs to use graceful-fs, since that is used by fs-extra\r
-                const spy = spyOn(require('graceful-fs'), 'writeFileSync');\r
-\r
-                const dest = require('path').join(__dirname, 'test-destination');\r
-                platformJson.addPluginMetadata(fakePlugin).generateAndSaveMetadata(dest);\r
-\r
-                expect(spy).toHaveBeenCalledTimes(1);\r
-                const [file, data] = spy.calls.argsFor(0);\r
-                expect(file).toBe(dest);\r
-                const mod = evaluateCordovaDefineStatement(data);\r
-                expect(mod.exports).toEqual(expectedMetadata());\r
-            });\r
-        });\r
-    });\r
-});\r
-\r
-describe('ModuleMetadata class', function () {\r
-    it('Test 010 : should be constructable', function () {\r
-        var meta;\r
-        expect(function () {\r
-            meta = new ModuleMetadata('fakePlugin', {src: 'www/fakeModule.js'});\r
-        }).not.toThrow();\r
-        expect(meta instanceof ModuleMetadata).toBeTruthy();\r
-    });\r
-\r
-    it('Test 011 : should throw if either pluginId or jsModule argument isn\'t specified', function () {\r
-        expect(ModuleMetadata).toThrow();\r
-        expect(function () { new ModuleMetadata('fakePlugin', {}); }).toThrow(); /* eslint no-new : 0 */\r
-    });\r
-\r
-    it('Test 012 : should guess module id either from name property of from module src', function () {\r
-        expect(new ModuleMetadata('fakePlugin', {name: 'fakeModule'}).id).toMatch(/fakeModule$/);\r
-        expect(new ModuleMetadata('fakePlugin', {src: 'www/fakeModule.js'}).id).toMatch(/fakeModule$/);\r
-    });\r
-\r
-    it('Test 013 : should read "clobbers" property from module', function () {\r
-        expect(new ModuleMetadata('fakePlugin', {name: 'fakeModule'}).clobbers).not.toBeDefined();\r
-        var metadata = new ModuleMetadata('fakePlugin', FAKE_MODULE);\r
-        expect(metadata.clobbers).toEqual(jasmine.any(Array));\r
-        expect(metadata.clobbers[0]).toBe(FAKE_MODULE.clobbers[0].target);\r
-    });\r
-\r
-    it('Test 014 : should read "merges" property from module', function () {\r
-        expect(new ModuleMetadata('fakePlugin', {name: 'fakeModule'}).merges).not.toBeDefined();\r
-        var metadata = new ModuleMetadata('fakePlugin', FAKE_MODULE);\r
-        expect(metadata.merges).toEqual(jasmine.any(Array));\r
-        expect(metadata.merges[0]).toBe(FAKE_MODULE.merges[0].target);\r
-    });\r
-\r
-    it('Test 015 : should read "runs" property from module', function () {\r
-        expect(new ModuleMetadata('fakePlugin', {name: 'fakeModule'}).runs).not.toBeDefined();\r
-        expect(new ModuleMetadata('fakePlugin', FAKE_MODULE).runs).toBe(true);\r
-    });\r
-});\r
+/**
+    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.
+*/
+
+var rewire = require('rewire');
+var PlatformJson = rewire('../src/PlatformJson');
+var ModuleMetadata = PlatformJson.__get__('ModuleMetadata');
+
+var FAKE_MODULE = {
+    name: 'fakeModule',
+    src: 'www/fakeModule.js',
+    clobbers: [{target: 'window.fakeClobber'}],
+    merges: [{target: 'window.fakeMerge'}],
+    runs: true
+};
+
+describe('PlatformJson class', function () {
+    it('Test 001 : should be constructable', function () {
+        expect(new PlatformJson()).toEqual(jasmine.any(PlatformJson));
+    });
+
+    describe('instance', function () {
+        var platformJson;
+        var fakePlugin;
+
+        beforeEach(function () {
+            platformJson = new PlatformJson('/fake/path', 'android');
+            fakePlugin = jasmine.createSpyObj('fakePlugin', ['getJsModules']);
+            fakePlugin.id = 'fakeId';
+            fakePlugin.version = '1.0.0';
+            fakePlugin.getJsModules.and.returnValue([FAKE_MODULE]);
+        });
+
+        describe('addPluginMetadata method', function () {
+            it('Test 002 : should not throw if root "modules" property is missing', function () {
+                expect(function () {
+                    platformJson.addPluginMetadata(fakePlugin);
+                }).not.toThrow();
+            });
+
+            it('Test 003 : should add each module to "root.modules" array', function () {
+                platformJson.addPluginMetadata(fakePlugin);
+                expect(platformJson.root.modules.length).toBe(1);
+                expect(platformJson.root.modules[0]).toEqual(jasmine.any(ModuleMetadata));
+            });
+
+            it('Test 004 : shouldn\'t add module if there is already module with the same file added', function () {
+                platformJson.root.modules = [{
+                    name: 'fakePlugin2',
+                    file: 'plugins/fakeId/www/fakeModule.js'
+                }];
+
+                platformJson.addPluginMetadata(fakePlugin);
+                expect(platformJson.root.modules.length).toBe(1);
+                expect(platformJson.root.modules[0].name).toBe('fakePlugin2');
+            });
+
+            it('Test 005 : should add entry to plugin_metadata with corresponding version', function () {
+                platformJson.addPluginMetadata(fakePlugin);
+                expect(platformJson.root.plugin_metadata[fakePlugin.id]).toBe(fakePlugin.version);
+            });
+        });
+
+        describe('removePluginMetadata method', function () {
+            it('Test 006 : should not throw if root "modules" property is missing', function () {
+                expect(function () {
+                    platformJson.removePluginMetadata(fakePlugin);
+                }).not.toThrow();
+            });
+
+            it('Test 007 : should remove plugin modules from "root.modules" array based on file path', function () {
+
+                var pluginPaths = [
+                    'plugins/fakeId/www/fakeModule.js',
+                    'plugins/otherPlugin/www/module1.js',
+                    'plugins/otherPlugin/www/module1.js'
+                ];
+
+                platformJson.root.modules = pluginPaths.map(function (p) { return {file: p}; });
+                platformJson.removePluginMetadata(fakePlugin);
+                var resultantPaths = platformJson.root.modules
+                    .map(function (p) { return p.file; })
+                    .filter(function (f) { return /fakeModule\.js$/.test(f); });
+
+                expect(resultantPaths.length).toBe(0);
+            });
+
+            it('Test 008 : should remove entry from plugin_metadata with corresponding version', function () {
+                platformJson.root.plugin_metadata = {};
+                platformJson.root.plugin_metadata[fakePlugin.id] = fakePlugin.version;
+                platformJson.removePluginMetadata(fakePlugin);
+                expect(platformJson.root.plugin_metadata[fakePlugin.id]).not.toBeDefined();
+            });
+        });
+
+        function evaluateCordovaDefineStatement (str) {
+            expect(typeof str).toBe('string');
+            const fnString = str.replace(/^\s*cordova\.define\('cordova\/plugin_list',\s*([\s\S]+)\);\s*$/, '($1)');
+            const mod = {exports: {}};
+            global.eval(fnString)(null, mod.exports, mod); // eslint-disable-line no-eval
+            return mod;
+        }
+
+        function expectedMetadata () {
+            // Create plain objects from ModuleMetadata instances
+            const modules = platformJson.root.modules.map(o => Object.assign({}, o));
+            modules.metadata = platformJson.root.plugin_metadata;
+            return modules;
+        }
+
+        describe('generateMetadata method', function () {
+            it('Test 009 : should generate text metadata containing list of installed modules', function () {
+                const meta = platformJson.addPluginMetadata(fakePlugin).generateMetadata();
+                const mod = evaluateCordovaDefineStatement(meta);
+
+                expect(mod.exports).toEqual(expectedMetadata());
+            });
+        });
+
+        describe('generateAndSaveMetadata method', function () {
+            it('should save generated metadata', function () {
+                // Needs to use graceful-fs, since that is used by fs-extra
+                const spy = spyOn(require('graceful-fs'), 'writeFileSync');
+
+                const dest = require('path').join(__dirname, 'test-destination');
+                platformJson.addPluginMetadata(fakePlugin).generateAndSaveMetadata(dest);
+
+                expect(spy).toHaveBeenCalledTimes(1);
+                const [file, data] = spy.calls.argsFor(0);
+                expect(file).toBe(dest);
+                const mod = evaluateCordovaDefineStatement(data);
+                expect(mod.exports).toEqual(expectedMetadata());
+            });
+        });
+    });
+});
+
+describe('ModuleMetadata class', function () {
+    it('Test 010 : should be constructable', function () {
+        var meta;
+        expect(function () {
+            meta = new ModuleMetadata('fakePlugin', {src: 'www/fakeModule.js'});
+        }).not.toThrow();
+        expect(meta instanceof ModuleMetadata).toBeTruthy();
+    });
+
+    it('Test 011 : should throw if either pluginId or jsModule argument isn\'t specified', function () {
+        expect(ModuleMetadata).toThrow();
+        expect(function () { new ModuleMetadata('fakePlugin', {}); }).toThrow(); /* eslint no-new : 0 */
+    });
+
+    it('Test 012 : should guess module id either from name property of from module src', function () {
+        expect(new ModuleMetadata('fakePlugin', {name: 'fakeModule'}).id).toMatch(/fakeModule$/);
+        expect(new ModuleMetadata('fakePlugin', {src: 'www/fakeModule.js'}).id).toMatch(/fakeModule$/);
+    });
+
+    it('Test 013 : should read "clobbers" property from module', function () {
+        expect(new ModuleMetadata('fakePlugin', {name: 'fakeModule'}).clobbers).not.toBeDefined();
+        var metadata = new ModuleMetadata('fakePlugin', FAKE_MODULE);
+        expect(metadata.clobbers).toEqual(jasmine.any(Array));
+        expect(metadata.clobbers[0]).toBe(FAKE_MODULE.clobbers[0].target);
+    });
+
+    it('Test 014 : should read "merges" property from module', function () {
+        expect(new ModuleMetadata('fakePlugin', {name: 'fakeModule'}).merges).not.toBeDefined();
+        var metadata = new ModuleMetadata('fakePlugin', FAKE_MODULE);
+        expect(metadata.merges).toEqual(jasmine.any(Array));
+        expect(metadata.merges[0]).toBe(FAKE_MODULE.merges[0].target);
+    });
+
+    it('Test 015 : should read "runs" property from module', function () {
+        expect(new ModuleMetadata('fakePlugin', {name: 'fakeModule'}).runs).not.toBeDefined();
+        expect(new ModuleMetadata('fakePlugin', FAKE_MODULE).runs).toBe(true);
+    });
+});
index adefa8c..b606286 100644 (file)
-/**\r
-    Licensed to the Apache Software Foundation (ASF) under one\r
-    or more contributor license agreements.  See the NOTICE file\r
-    distributed with this work for additional information\r
-    regarding copyright ownership.  The ASF licenses this file\r
-    to you under the Apache License, Version 2.0 (the\r
-    "License"); you may not use this file except in compliance\r
-    with the License.  You may obtain a copy of the License at\r
-\r
-    http://www.apache.org/licenses/LICENSE-2.0\r
-\r
-    Unless required by applicable law or agreed to in writing,\r
-    software distributed under the License is distributed on an\r
-    "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY\r
-    KIND, either express or implied.  See the License for the\r
-    specific language governing permissions and limitations\r
-    under the License.\r
-*/\r
-\r
-// Promise-matchers do not work with jasmine 2.0.\r
-// require('promise-matchers');\r
-\r
-var Q = require('q');\r
-var fs = require('fs-extra');\r
-var path = require('path');\r
-var rewire = require('rewire');\r
-var PluginManager = rewire('../src/PluginManager');\r
-var PluginInfo = require('../src/PluginInfo/PluginInfo');\r
-var ConfigChanges = require('../src/ConfigChanges/ConfigChanges');\r
-\r
-var DUMMY_PLUGIN = path.join(__dirname, 'fixtures/plugins/org.test.plugins.dummyplugin');\r
-var FAKE_PLATFORM = 'cordova-atari';\r
-var FAKE_LOCATIONS = {\r
-    root: '/some/fake/path',\r
-    platformWww: '/some/fake/path/platform_www',\r
-    www: '/some/www/dir'\r
-};\r
-\r
-describe('PluginManager class', function () {\r
-\r
-    beforeEach(function () {\r
-        spyOn(ConfigChanges, 'PlatformMunger');\r
-        spyOn(fs, 'outputJsonSync');\r
-        spyOn(fs, 'writeFileSync');\r
-        spyOn(fs, 'ensureDirSync');\r
-    });\r
-\r
-    it('Test 001 : should be constructable', function () {\r
-        expect(new PluginManager(FAKE_PLATFORM, FAKE_LOCATIONS)).toEqual(jasmine.any(PluginManager));\r
-    });\r
-\r
-    it('Test 002 : should return new instance for every PluginManager.get call', function () {\r
-        expect(PluginManager.get(FAKE_PLATFORM, FAKE_LOCATIONS)).toEqual(jasmine.any(PluginManager));\r
-        expect(PluginManager.get(FAKE_PLATFORM, FAKE_LOCATIONS))\r
-            .not.toBe(PluginManager.get(FAKE_PLATFORM, FAKE_LOCATIONS));\r
-    });\r
-\r
-    describe('instance', function () {\r
-        var actions, manager;\r
-        var FAKE_PROJECT;\r
-        var fail = jasmine.createSpy('fail');\r
-        var ActionStackOrig = PluginManager.__get__('ActionStack');\r
-\r
-        beforeEach(function () {\r
-            FAKE_PROJECT = jasmine.createSpyObj('project', ['getInstaller', 'getUninstaller', 'write']);\r
-            manager = new PluginManager('windows', FAKE_LOCATIONS, FAKE_PROJECT);\r
-            actions = jasmine.createSpyObj('actions', ['createAction', 'push', 'process']);\r
-            actions.process.and.returnValue(Q.resolve());\r
-            PluginManager.__set__('ActionStack', function () { return actions; });\r
-        });\r
-\r
-        afterEach(function () {\r
-            PluginManager.__set__('ActionStack', ActionStackOrig);\r
-        });\r
-\r
-        describe('addPlugin method', function () {\r
-            it('should return a promise', function () {\r
-                expect(Q.isPromise(manager.addPlugin(null, {}))).toBe(true);\r
-            });\r
-            // Promise-matchers do not work with jasmine 2.0.\r
-            xit('Test 003 : should reject if "plugin" parameter is not specified or not a PluginInfo instance', function (done) {\r
-                expect(manager.addPlugin(null, {})).toHaveBeenRejected(done);\r
-                expect(manager.addPlugin({}, {})).toHaveBeenRejected(done);\r
-                expect(manager.addPlugin(new PluginInfo(DUMMY_PLUGIN), {})).not.toHaveBeenRejected(done);\r
-            });\r
-\r
-            it('Test 004 : should iterate through all plugin\'s files and frameworks', function (done) {\r
-                manager.addPlugin(new PluginInfo(DUMMY_PLUGIN), {})\r
-                    .then(function () {\r
-                        expect(FAKE_PROJECT.getInstaller.calls.count()).toBe(16);\r
-                        expect(FAKE_PROJECT.getUninstaller.calls.count()).toBe(16);\r
-\r
-                        expect(actions.push.calls.count()).toBe(16);\r
-                        expect(actions.process).toHaveBeenCalled();\r
-                        expect(FAKE_PROJECT.write).toHaveBeenCalled();\r
-                    })\r
-                    .fail(fail)\r
-                    .done(function () {\r
-                        expect(fail).not.toHaveBeenCalled();\r
-                        done();\r
-                    });\r
-            });\r
-\r
-            it('Test 005 : should save plugin metadata to www directory', function (done) {\r
-                var metadataPath = path.join(manager.locations.www, 'cordova_plugins.js');\r
-                var platformWwwMetadataPath = path.join(manager.locations.platformWww, 'cordova_plugins.js');\r
-\r
-                manager.addPlugin(new PluginInfo(DUMMY_PLUGIN), {})\r
-                    .then(function () {\r
-                        expect(fs.writeFileSync).toHaveBeenCalledWith(metadataPath, jasmine.any(String), 'utf-8');\r
-                        expect(fs.writeFileSync).not.toHaveBeenCalledWith(platformWwwMetadataPath, jasmine.any(String), 'utf-8');\r
-                    })\r
-                    .fail(fail)\r
-                    .done(function () {\r
-                        expect(fail).not.toHaveBeenCalled();\r
-                        done();\r
-                    });\r
-            });\r
-\r
-            it('Test 006 : should save plugin metadata to both www ans platform_www directories when options.usePlatformWww is specified', function (done) {\r
-                var metadataPath = path.join(manager.locations.www, 'cordova_plugins.js');\r
-                var platformWwwMetadataPath = path.join(manager.locations.platformWww, 'cordova_plugins.js');\r
-\r
-                manager.addPlugin(new PluginInfo(DUMMY_PLUGIN), {usePlatformWww: true})\r
-                    .then(function () {\r
-                        expect(fs.writeFileSync).toHaveBeenCalledWith(metadataPath, jasmine.any(String), 'utf-8');\r
-                        expect(fs.writeFileSync).toHaveBeenCalledWith(platformWwwMetadataPath, jasmine.any(String), 'utf-8');\r
-                    })\r
-                    .fail(fail)\r
-                    .done(function () {\r
-                        expect(fail).not.toHaveBeenCalled();\r
-                        done();\r
-                    });\r
-            });\r
-        });\r
-    });\r
-});\r
+/**
+    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.
+*/
+
+// Promise-matchers do not work with jasmine 2.0.
+// require('promise-matchers');
+
+var Q = require('q');
+var fs = require('fs-extra');
+var path = require('path');
+var rewire = require('rewire');
+var PluginManager = rewire('../src/PluginManager');
+var PluginInfo = require('../src/PluginInfo/PluginInfo');
+var ConfigChanges = require('../src/ConfigChanges/ConfigChanges');
+
+var DUMMY_PLUGIN = path.join(__dirname, 'fixtures/plugins/org.test.plugins.dummyplugin');
+var FAKE_PLATFORM = 'cordova-atari';
+var FAKE_LOCATIONS = {
+    root: '/some/fake/path',
+    platformWww: '/some/fake/path/platform_www',
+    www: '/some/www/dir'
+};
+
+describe('PluginManager class', function () {
+
+    beforeEach(function () {
+        spyOn(ConfigChanges, 'PlatformMunger');
+        spyOn(fs, 'outputJsonSync');
+        spyOn(fs, 'writeFileSync');
+        spyOn(fs, 'ensureDirSync');
+    });
+
+    it('Test 001 : should be constructable', function () {
+        expect(new PluginManager(FAKE_PLATFORM, FAKE_LOCATIONS)).toEqual(jasmine.any(PluginManager));
+    });
+
+    it('Test 002 : should return new instance for every PluginManager.get call', function () {
+        expect(PluginManager.get(FAKE_PLATFORM, FAKE_LOCATIONS)).toEqual(jasmine.any(PluginManager));
+        expect(PluginManager.get(FAKE_PLATFORM, FAKE_LOCATIONS))
+            .not.toBe(PluginManager.get(FAKE_PLATFORM, FAKE_LOCATIONS));
+    });
+
+    describe('instance', function () {
+        var actions, manager;
+        var FAKE_PROJECT;
+        var fail = jasmine.createSpy('fail');
+        var ActionStackOrig = PluginManager.__get__('ActionStack');
+
+        beforeEach(function () {
+            FAKE_PROJECT = jasmine.createSpyObj('project', ['getInstaller', 'getUninstaller', 'write']);
+            manager = new PluginManager('windows', FAKE_LOCATIONS, FAKE_PROJECT);
+            actions = jasmine.createSpyObj('actions', ['createAction', 'push', 'process']);
+            actions.process.and.returnValue(Q.resolve());
+            PluginManager.__set__('ActionStack', function () { return actions; });
+        });
+
+        afterEach(function () {
+            PluginManager.__set__('ActionStack', ActionStackOrig);
+        });
+
+        describe('addPlugin method', function () {
+            it('should return a promise', function () {
+                expect(Q.isPromise(manager.addPlugin(null, {}))).toBe(true);
+            });
+            // Promise-matchers do not work with jasmine 2.0.
+            xit('Test 003 : should reject if "plugin" parameter is not specified or not a PluginInfo instance', function (done) {
+                expect(manager.addPlugin(null, {})).toHaveBeenRejected(done);
+                expect(manager.addPlugin({}, {})).toHaveBeenRejected(done);
+                expect(manager.addPlugin(new PluginInfo(DUMMY_PLUGIN), {})).not.toHaveBeenRejected(done);
+            });
+
+            it('Test 004 : should iterate through all plugin\'s files and frameworks', function (done) {
+                manager.addPlugin(new PluginInfo(DUMMY_PLUGIN), {})
+                    .then(function () {
+                        expect(FAKE_PROJECT.getInstaller.calls.count()).toBe(16);
+                        expect(FAKE_PROJECT.getUninstaller.calls.count()).toBe(16);
+
+                        expect(actions.push.calls.count()).toBe(16);
+                        expect(actions.process).toHaveBeenCalled();
+                        expect(FAKE_PROJECT.write).toHaveBeenCalled();
+                    })
+                    .fail(fail)
+                    .done(function () {
+                        expect(fail).not.toHaveBeenCalled();
+                        done();
+                    });
+            });
+
+            it('Test 005 : should save plugin metadata to www directory', function (done) {
+                var metadataPath = path.join(manager.locations.www, 'cordova_plugins.js');
+                var platformWwwMetadataPath = path.join(manager.locations.platformWww, 'cordova_plugins.js');
+
+                manager.addPlugin(new PluginInfo(DUMMY_PLUGIN), {})
+                    .then(function () {
+                        expect(fs.writeFileSync).toHaveBeenCalledWith(metadataPath, jasmine.any(String), 'utf-8');
+                        expect(fs.writeFileSync).not.toHaveBeenCalledWith(platformWwwMetadataPath, jasmine.any(String), 'utf-8');
+                    })
+                    .fail(fail)
+                    .done(function () {
+                        expect(fail).not.toHaveBeenCalled();
+                        done();
+                    });
+            });
+
+            it('Test 006 : should save plugin metadata to both www ans platform_www directories when options.usePlatformWww is specified', function (done) {
+                var metadataPath = path.join(manager.locations.www, 'cordova_plugins.js');
+                var platformWwwMetadataPath = path.join(manager.locations.platformWww, 'cordova_plugins.js');
+
+                manager.addPlugin(new PluginInfo(DUMMY_PLUGIN), {usePlatformWww: true})
+                    .then(function () {
+                        expect(fs.writeFileSync).toHaveBeenCalledWith(metadataPath, jasmine.any(String), 'utf-8');
+                        expect(fs.writeFileSync).toHaveBeenCalledWith(platformWwwMetadataPath, jasmine.any(String), 'utf-8');
+                    })
+                    .fail(fail)
+                    .done(function () {
+                        expect(fail).not.toHaveBeenCalled();
+                        done();
+                    });
+            });
+        });
+    });
+});
index 700ef7c..205d5e7 100644 (file)
@@ -36,7 +36,7 @@
         <access origin="build.phonegap.com" />
         <access origin="s3.amazonaws.com" />
     </config-file>
-    
+
     <info>No matter what platform you are installing to, this notice is very important.</info>
 
     <!-- android -->
@@ -82,7 +82,7 @@
         <config-file target="*-Info.plist" parent="AppId">
             <string>$APP_ID</string>
         </config-file>
-        
+
         <config-file target="*-Info.plist" parent="CFBundleURLTypes">
             <array>
               <dict>
                      target-dir="Plugins\" />
 
         <!-- modify the project file to include the added files -->
-        <config-file target=".csproj" parent=".">  
-        </config-file> 
+        <config-file target=".csproj" parent=".">
+        </config-file>
 
     </platform>
 </plugin>
index 36113b6..bbc11dd 100644 (file)
@@ -14,4 +14,4 @@
     KIND, either express or implied.  See the License for the
     specific language governing permissions and limitations
     under the License.
-*/
\ No newline at end of file
+*/
index d1e1bff..9238747 100644 (file)
             <poop name="GoogleMapsApiKey" value="$API_KEY" />
             <package>$PACKAGE_NAME</package>
                </config-file>
-               
+
     </platform>
-    
+
     <!-- amazon fireos -->
     <platform name="amazon-fireos">
                <config-file target="AndroidManifest.xml" parent="/manifest">
             <poop name="GoogleMapsApiKey" value="$API_KEY" />
             <package>$PACKAGE_NAME</package>
                </config-file>
-               
+
     </platform>
 
     <!-- ios -->
index 1e30814..53db8f6 100644 (file)
     <platform name="android">
                <config-file target="AndroidManifest.xml" parent="/manifest">
                        <uses-permission android:name="android.permission.ACCESS_NETWORK_STATE"/>
-                       
+
                        <!--library-->
                        <uses-permission android:name="android.permission.READ_PHONE_STATE"/>
-                       
+
                        <!-- GCM connects to Google Services. -->
                        <uses-permission android:name="android.permission.INTERNET"/>
-                       
+
                        <!-- GCM requires a Google account. -->
                        <uses-permission android:name="android.permission.GET_ACCOUNTS"/>
-                       
+
                        <!-- Keeps the processor from sleeping when a message is received. -->
                        <uses-permission android:name="android.permission.WAKE_LOCK"/>
-                       
+
                        <!--
                         Creates a custom permission so only this app can receive its messages.
-                        
+
                         NOTE: the permission *must* be called PACKAGE.permission.C2D_MESSAGE,
                         where PACKAGE is the application's package name.
                         -->
                        android:protectionLevel="signature"/>
                        <uses-permission
                        android:name="$PACKAGE_NAME.permission.C2D_MESSAGE"/>
-                       
+
                        <!-- This app has permission to register and receive data message. -->
                        <uses-permission
                        android:name="com.google.android.c2dm.permission.RECEIVE"/>
                </config-file>
-               
+
                <config-file target="AndroidManifest.xml" parent="/manifest/application/activity">
                        <intent-filter>
                                <action android:name="$PACKAGE_NAME.MESSAGE"/>
 
                <config-file target="AndroidManifest.xml" parent="/manifest/application">
                        <activity android:name="com.arellomobile.android.push.PushWebview"/>
-                       
+
                        <activity android:name="com.arellomobile.android.push.MessageActivity"/>
-                       
+
                        <activity android:name="com.arellomobile.android.push.PushHandlerActivity"/>
-                       
+
                        <!--
                         BroadcastReceiver that will receive intents from GCM
                         services and handle them to the custom IntentService.
-                        
+
                         The com.google.android.c2dm.permission.SEND permission is necessary
                         so only GCM services can send data messages for the app.
                         -->
                                        <category android:name="$PACKAGE_NAME"/>
                                </intent-filter>
                        </receiver>
-                       
+
                        <!--
                         Application-specific subclass of PushGCMIntentService that will
                         handle received messages.
                         -->
-                       <service android:name="com.arellomobile.android.push.PushGCMIntentService"/>                                            
-                       
+                       <service android:name="com.arellomobile.android.push.PushGCMIntentService"/>
+
                </config-file>
-               
+
                <config-file target="res/xml/plugins.xml" parent="/plugins">
             <plugin name="PushNotification"
                        value="com.pushwoosh.test.plugin.pushnotifications.PushNotifications" onload="true"/>
index 6d1aa91..78f60f1 100644 (file)
@@ -99,4 +99,4 @@
       <preference name="WindowsToastCapable" value="true"/>
     </config-file>
   </platform>
-</plugin>
\ No newline at end of file
+</plugin>
index a97674e..f10faa3 100644 (file)
@@ -40,8 +40,8 @@
     <uses-permission android:name="android.permission.RECORD_VIDEO"/>
     <uses-permission android:name="android.permission.MODIFY_AUDIO_SETTINGS" />
     <uses-permission android:name="android.permission.READ_CONTACTS" />
-    <uses-permission android:name="android.permission.WRITE_CONTACTS" />   
-    <uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE" />   
+    <uses-permission android:name="android.permission.WRITE_CONTACTS" />
+    <uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE" />
     <uses-permission android:name="android.permission.ACCESS_NETWORK_STATE" />
     <uses-permission android:name="android.permission.GET_ACCOUNTS" />
     <uses-permission android:name="android.permission.BROADCAST_STICKY" />
 
     <application android:icon="@drawable/icon" android:label="@string/app_name"
        android:debuggable="true">
-               <activity android:name="ChildApp" android:label="@string/app_name" 
+               <activity android:name="ChildApp" android:label="@string/app_name"
                                  android:configChanges="orientation|keyboardHidden">
                        <intent-filter>
                                <action android:name="android.intent.action.MAIN" />
                                <category android:name="android.intent.category.LAUNCHER" />
                        </intent-filter>
         </activity>
-        <activity android:name="com.phonegap.DroidGap" android:label="@string/app_name" 
+        <activity android:name="com.phonegap.DroidGap" android:label="@string/app_name"
                  android:configChanges="orientation|keyboardHidden">
                <intent-filter>
                </intent-filter>
@@ -66,4 +66,4 @@
     </application>
 
        <uses-sdk android:minSdkVersion="5" />
-</manifest> 
+</manifest>
index 019caae..2761208 100644 (file)
@@ -40,8 +40,8 @@
     <uses-permission android:name="android.permission.RECORD_VIDEO"/>
     <uses-permission android:name="android.permission.MODIFY_AUDIO_SETTINGS" />
     <uses-permission android:name="android.permission.READ_CONTACTS" />
-    <uses-permission android:name="android.permission.WRITE_CONTACTS" />   
-    <uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE" />   
+    <uses-permission android:name="android.permission.WRITE_CONTACTS" />
+    <uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE" />
     <uses-permission android:name="android.permission.ACCESS_NETWORK_STATE" />
     <uses-permission android:name="android.permission.GET_ACCOUNTS" />
     <uses-permission android:name="android.permission.BROADCAST_STICKY" />
 
     <application android:icon="@drawable/icon" android:label="@string/app_name"
        android:debuggable="true">
-               <activity android:name="ChildApp" android:label="@string/app_name" 
+               <activity android:name="ChildApp" android:label="@string/app_name"
                                  android:configChanges="orientation|keyboardHidden">
                        <intent-filter>
                                <action android:name="android.intent.action.MAIN" />
                                <category android:name="android.intent.category.LAUNCHER" />
                        </intent-filter>
         </activity>
-        <activity android:name="org.test.DroidGap" android:label="@string/app_name" 
+        <activity android:name="org.test.DroidGap" android:label="@string/app_name"
                  android:configChanges="orientation|keyboardHidden">
                <intent-filter>
                </intent-filter>
@@ -66,4 +66,4 @@
     </application>
 
        <uses-sdk android:minSdkVersion="5" />
-</manifest> 
+</manifest>
index 74aeecc..85155f9 100644 (file)
 
     <application android:icon="@drawable/icon" android:label="@string/app_name"
        android:debuggable="true">
-               <activity android:name="ChildApp" android:label="@string/app_name" 
+               <activity android:name="ChildApp" android:label="@string/app_name"
                                  android:configChanges="orientation|keyboardHidden">
                        <intent-filter>
                                <action android:name="android.intent.action.MAIN" />
                                <category android:name="android.intent.category.LAUNCHER" />
                        </intent-filter>
         </activity>
-        <activity android:name="org.test.DroidGap" android:label="@string/app_name" 
+        <activity android:name="org.test.DroidGap" android:label="@string/app_name"
                  android:configChanges="orientation|keyboardHidden">
                <intent-filter>
                </intent-filter>
@@ -46,4 +46,4 @@
     </application>
 
        <uses-sdk android:minSdkVersion="5" />
-</manifest> 
+</manifest>
index 1edf010..22edad6 100644 (file)
@@ -9,9 +9,9 @@
 # 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
index f0a823b..883c160 100644 (file)
@@ -8,9 +8,9 @@
 # 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
index ff6cf68..1e1964f 100644 (file)
@@ -68,7 +68,7 @@
   </ItemGroup>
   <Import Project="$(MSBuildExtensionsPath32)\Microsoft\VisualStudio\v$(VisualStudioVersion)\$(WMSJSProjectDirectory)\Microsoft.VisualStudio.$(WMSJSProject).targets" />
   <!-- To modify your build process, add your task inside one of the targets below then uncomment
-       that target and the DisableFastUpToDateCheck PropertyGroup. 
+       that target and the DisableFastUpToDateCheck PropertyGroup.
        Other similar extension points exist, see Microsoft.Common.targets.
   <Target Name="BeforeBuild">
   </Target>
index c408373..b97e851 100644 (file)
@@ -31,4 +31,4 @@
   </ItemGroup>
   <Import Project="CordovaAppDebug.projitems" Condition="Exists('$(MSBuildThisFileDirectory)CordovaAppDebug.projitems') And '$(Configuration)'=='Debug'" />
   <Import Project="CordovaAppRelease.projitems" Condition="Exists('$(MSBuildThisFileDirectory)CordovaAppRelease.projitems') And '$(Configuration)'!='Debug'" />
-</Project>
\ No newline at end of file
+</Project>
index 57cadf6..446869d 100644 (file)
@@ -8,9 +8,9 @@
 # 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
index f8c32b2..84d49c3 100644 (file)
@@ -12,9 +12,9 @@
  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
@@ -8042,7 +8042,7 @@ require('cordova/channel').onNativeReady.fire();
     }
 
     // Try to XHR the cordova_plugins.json file asynchronously.
-    try { // we commented we were going to try, so let us actually try and catch 
+    try { // we commented we were going to try, so let us actually try and catch
         var xhr = new context.XMLHttpRequest();
         xhr.onreadystatechange = function() {
             if (this.readyState != 4) { // not DONE
index 51daa79..583c001 100644 (file)
@@ -102,13 +102,13 @@ h1 {
     50% { opacity: 0.4; }
     to { opacity: 1.0; }
 }
+
 @-webkit-keyframes fade {
     from { opacity: 1.0; }
     50% { opacity: 0.4; }
     to { opacity: 1.0; }
 }
+
 .blink {
     animation:fade 3000ms infinite;
     -webkit-animation:fade 3000ms infinite;
index 2a9729c..5a63564 100644 (file)
@@ -1,91 +1,91 @@
-/**\r
-    Licensed to the Apache Software Foundation (ASF) under one\r
-    or more contributor license agreements.  See the NOTICE file\r
-    distributed with this work for additional information\r
-    regarding copyright ownership.  The ASF licenses this file\r
-    to you under the Apache License, Version 2.0 (the\r
-    "License"); you may not use this file except in compliance\r
-    with the License.  You may obtain a copy of the License at\r
-\r
-    http://www.apache.org/licenses/LICENSE-2.0\r
-\r
-    Unless required by applicable law or agreed to in writing,\r
-    software distributed under the License is distributed on an\r
-    "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY\r
-    KIND, either express or implied.  See the License for the\r
-    specific language governing permissions and limitations\r
-    under the License.\r
-*/\r
-\r
-var Q = require('q');\r
-var superspawn = require('../src/superspawn');\r
-\r
-var LS = process.platform === 'win32' ? 'dir' : 'ls';\r
-\r
-describe('spawn method', function () {\r
-    var progressSpy, failSpy;\r
-\r
-    beforeEach(function () {\r
-        progressSpy = jasmine.createSpy('progress');\r
-        failSpy = jasmine.createSpy('fail'); /* eslint no-unused-vars : 0 */\r
-    });\r
-\r
-    it('Test 001 : should return a promise', function () {\r
-        expect(Q.isPromise(superspawn.spawn(LS))).toBe(true);\r
-        expect(Q.isPromise(superspawn.spawn('invalid_command'))).toBe(true);\r
-    });\r
-\r
-    it('Test 002 : should notify about stdout "data" events', function (done) {\r
-        superspawn.spawn(LS, [], {stdio: 'pipe'})\r
-            .progress(progressSpy)\r
-            .fin(function () {\r
-                expect(progressSpy).toHaveBeenCalledWith({'stdout': jasmine.any(String)});\r
-                done();\r
-            });\r
-    });\r
-\r
-    it('Test 003 : should notify about stderr "data" events', function (done) {\r
-        superspawn.spawn(LS, ['doesnt-exist'], {stdio: 'pipe'})\r
-            .progress(progressSpy)\r
-            .fin(function () {\r
-                expect(progressSpy).toHaveBeenCalledWith({'stderr': jasmine.any(String)});\r
-                done();\r
-            });\r
-    });\r
-\r
-    it('Test 004 : reject handler should pass in Error object with stdout and stderr properties', function (done) {\r
-        var cp = require('child_process');\r
-        spyOn(cp, 'spawn').and.callFake(function (cmd, args, opts) {\r
-            return {\r
-                stdout: {\r
-                    setEncoding: function () {},\r
-                    on: function (evt, handler) {\r
-                        // some sample stdout output\r
-                        handler('business as usual');\r
-                    }\r
-                },\r
-                stderr: {\r
-                    setEncoding: function () {},\r
-                    on: function (evt, handler) {\r
-                        // some sample stderr output\r
-                        handler('mayday mayday');\r
-                    }\r
-                },\r
-                on: function (evt, handler) {\r
-                    // What's passed to handler here is the exit code, so we can control\r
-                    // resolve/reject flow via this argument.\r
-                    handler(1); // this will trigger error flow\r
-                },\r
-                removeListener: function () {}\r
-            };\r
-        });\r
-        superspawn.spawn('this aggression', ['will', 'not', 'stand', 'man'], {})\r
-            .catch(function (err) {\r
-                expect(err).toBeDefined();\r
-                expect(err.stdout).toContain('usual');\r
-                expect(err.stderr).toContain('mayday');\r
-                done();\r
-            });\r
-    });\r
-\r
-});\r
+/**
+    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.
+*/
+
+var Q = require('q');
+var superspawn = require('../src/superspawn');
+
+var LS = process.platform === 'win32' ? 'dir' : 'ls';
+
+describe('spawn method', function () {
+    var progressSpy, failSpy;
+
+    beforeEach(function () {
+        progressSpy = jasmine.createSpy('progress');
+        failSpy = jasmine.createSpy('fail'); /* eslint no-unused-vars : 0 */
+    });
+
+    it('Test 001 : should return a promise', function () {
+        expect(Q.isPromise(superspawn.spawn(LS))).toBe(true);
+        expect(Q.isPromise(superspawn.spawn('invalid_command'))).toBe(true);
+    });
+
+    it('Test 002 : should notify about stdout "data" events', function (done) {
+        superspawn.spawn(LS, [], {stdio: 'pipe'})
+            .progress(progressSpy)
+            .fin(function () {
+                expect(progressSpy).toHaveBeenCalledWith({'stdout': jasmine.any(String)});
+                done();
+            });
+    });
+
+    it('Test 003 : should notify about stderr "data" events', function (done) {
+        superspawn.spawn(LS, ['doesnt-exist'], {stdio: 'pipe'})
+            .progress(progressSpy)
+            .fin(function () {
+                expect(progressSpy).toHaveBeenCalledWith({'stderr': jasmine.any(String)});
+                done();
+            });
+    });
+
+    it('Test 004 : reject handler should pass in Error object with stdout and stderr properties', function (done) {
+        var cp = require('child_process');
+        spyOn(cp, 'spawn').and.callFake(function (cmd, args, opts) {
+            return {
+                stdout: {
+                    setEncoding: function () {},
+                    on: function (evt, handler) {
+                        // some sample stdout output
+                        handler('business as usual');
+                    }
+                },
+                stderr: {
+                    setEncoding: function () {},
+                    on: function (evt, handler) {
+                        // some sample stderr output
+                        handler('mayday mayday');
+                    }
+                },
+                on: function (evt, handler) {
+                    // What's passed to handler here is the exit code, so we can control
+                    // resolve/reject flow via this argument.
+                    handler(1); // this will trigger error flow
+                },
+                removeListener: function () {}
+            };
+        });
+        superspawn.spawn('this aggression', ['will', 'not', 'stand', 'man'], {})
+            .catch(function (err) {
+                expect(err).toBeDefined();
+                expect(err.stdout).toContain('usual');
+                expect(err.stderr).toContain('mayday');
+                done();
+            });
+    });
+
+});
index b5c3564..b6564d2 100644 (file)
-/*\r
- Licensed to the Apache Software Foundation (ASF) under one\r
- or more contributor license agreements.  See the NOTICE file\r
- distributed with this work for additional information\r
- regarding copyright ownership.  The ASF licenses this file\r
- to you under the Apache License, Version 2.0 (the\r
- "License"); you may not use this file except in compliance\r
- with the License.  You may obtain a copy of the License at\r
-\r
- http://www.apache.org/licenses/LICENSE-2.0\r
-\r
- Unless required by applicable law or agreed to in writing,\r
- software distributed under the License is distributed on an\r
- "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY\r
- KIND, either express or implied.  See the License for the\r
- specific language governing permissions and limitations\r
- under the License.\r
- */\r
-\r
-var ansi = require('ansi');\r
-var EventEmitter = require('events').EventEmitter;\r
-var CordovaError = require('./CordovaError/CordovaError');\r
-var EOL = require('os').EOL;\r
-\r
-var INSTANCE;\r
-\r
-/**\r
- * @class CordovaLogger\r
- *\r
- * Implements logging facility that anybody could use. Should not be\r
- *   instantiated directly, `CordovaLogger.get()` method should be used instead\r
- *   to acquire logger instance\r
- */\r
-function CordovaLogger () {\r
-    this.levels = {};\r
-    this.colors = {};\r
-    this.stdout = process.stdout;\r
-    this.stderr = process.stderr;\r
-\r
-    this.stdoutCursor = ansi(this.stdout);\r
-    this.stderrCursor = ansi(this.stderr);\r
-\r
-    this.addLevel('verbose', 1000, 'grey');\r
-    this.addLevel('normal', 2000);\r
-    this.addLevel('warn', 2000, 'yellow');\r
-    this.addLevel('info', 3000, 'blue');\r
-    this.addLevel('error', 5000, 'red');\r
-    this.addLevel('results', 10000);\r
-\r
-    this.setLevel('normal');\r
-}\r
-\r
-/**\r
- * Static method to create new or acquire existing instance.\r
- *\r
- * @return  {CordovaLogger}  Logger instance\r
- */\r
-CordovaLogger.get = function () {\r
-    return INSTANCE || (INSTANCE = new CordovaLogger());\r
-};\r
-\r
-CordovaLogger.VERBOSE = 'verbose';\r
-CordovaLogger.NORMAL = 'normal';\r
-CordovaLogger.WARN = 'warn';\r
-CordovaLogger.INFO = 'info';\r
-CordovaLogger.ERROR = 'error';\r
-CordovaLogger.RESULTS = 'results';\r
-\r
-/**\r
- * Emits log message to process' stdout/stderr depending on message's severity\r
- *   and current log level. If severity is less than current logger's level,\r
- *   then the message is ignored.\r
- *\r
- * @param   {String}  logLevel  The message's log level. The logger should have\r
- *   corresponding level added (via logger.addLevel), otherwise\r
- *   `CordovaLogger.NORMAL` level will be used.\r
- * @param   {String}  message   The message, that should be logged to process'\r
- *   stdio\r
- *\r
- * @return  {CordovaLogger}     Current instance, to allow calls chaining.\r
- */\r
-CordovaLogger.prototype.log = function (logLevel, message) {\r
-    // if there is no such logLevel defined, or provided level has\r
-    // less severity than active level, then just ignore this call and return\r
-    if (!this.levels[logLevel] || this.levels[logLevel] < this.levels[this.logLevel]) {\r
-        // return instance to allow to chain calls\r
-        return this;\r
-    }\r
-\r
-    var isVerbose = this.logLevel === 'verbose';\r
-    var cursor = this.stdoutCursor;\r
-\r
-    if (message instanceof Error || logLevel === CordovaLogger.ERROR) {\r
-        message = formatError(message, isVerbose);\r
-        cursor = this.stderrCursor;\r
-    }\r
-\r
-    var color = this.colors[logLevel];\r
-    if (color) {\r
-        cursor.bold().fg[color]();\r
-    }\r
-\r
-    cursor.write(message).reset().write(EOL);\r
-\r
-    return this;\r
-};\r
-\r
-/**\r
- * Adds a new level to logger instance. This method also creates a shortcut\r
- *   method to log events with the level provided (i.e. after adding new level\r
- *   'debug', the method `debug(message)`, equal to logger.log('debug', message),\r
- *   will be added to logger instance)\r
- *\r
- * @param  {String}  level     A log level name. The levels with the following\r
- *   names added by default to every instance: 'verbose', 'normal', 'warn',\r
- *   'info', 'error', 'results'\r
- * @param  {Number}  severity  A number that represents level's severity.\r
- * @param  {String}  color     A valid color name, that will be used to log\r
- *   messages with this level. Any CSS color code or RGB value is allowed\r
- *   (according to ansi documentation:\r
- *   https://github.com/TooTallNate/ansi.js#features)\r
- *\r
- * @return  {CordovaLogger}     Current instance, to allow calls chaining.\r
- */\r
-CordovaLogger.prototype.addLevel = function (level, severity, color) {\r
-\r
-    this.levels[level] = severity;\r
-\r
-    if (color) {\r
-        this.colors[level] = color;\r
-    }\r
-\r
-    // Define own method with corresponding name\r
-    if (!this[level]) {\r
-        this[level] = this.log.bind(this, level);\r
-    }\r
-\r
-    return this;\r
-};\r
-\r
-/**\r
- * Sets the current logger level to provided value. If logger doesn't have level\r
- *   with this name, `CordovaLogger.NORMAL` will be used.\r
- *\r
- * @param  {String}  logLevel  Level name. The level with this name should be\r
- *   added to logger before.\r
- *\r
- * @return  {CordovaLogger}     Current instance, to allow calls chaining.\r
- */\r
-CordovaLogger.prototype.setLevel = function (logLevel) {\r
-    this.logLevel = this.levels[logLevel] ? logLevel : CordovaLogger.NORMAL;\r
-\r
-    return this;\r
-};\r
-\r
-/**\r
- * Adjusts the current logger level according to the passed options.\r
- *\r
- * @param   {Object|Array}  opts  An object or args array with options\r
- *\r
- * @return  {CordovaLogger}     Current instance, to allow calls chaining.\r
- */\r
-CordovaLogger.prototype.adjustLevel = function (opts) {\r
-    if (opts.verbose || (Array.isArray(opts) && opts.includes('--verbose'))) {\r
-        this.setLevel('verbose');\r
-    } else if (opts.silent || (Array.isArray(opts) && opts.includes('--silent'))) {\r
-        this.setLevel('error');\r
-    }\r
-\r
-    return this;\r
-};\r
-\r
-/**\r
- * Attaches logger to EventEmitter instance provided.\r
- *\r
- * @param   {EventEmitter}  eventEmitter  An EventEmitter instance to attach\r
- *   logger to.\r
- *\r
- * @return  {CordovaLogger}     Current instance, to allow calls chaining.\r
- */\r
-CordovaLogger.prototype.subscribe = function (eventEmitter) {\r
-\r
-    if (!(eventEmitter instanceof EventEmitter)) { throw new Error('Subscribe method only accepts an EventEmitter instance as argument'); }\r
-\r
-    eventEmitter.on('verbose', this.verbose)\r
-        .on('log', this.normal)\r
-        .on('info', this.info)\r
-        .on('warn', this.warn)\r
-        .on('warning', this.warn)\r
-        // Set up event handlers for logging and results emitted as events.\r
-        .on('results', this.results);\r
-\r
-    return this;\r
-};\r
-\r
-function formatError (error, isVerbose) {\r
-    var message = '';\r
-\r
-    if (error instanceof CordovaError) {\r
-        message = error.toString(isVerbose);\r
-    } else if (error instanceof Error) {\r
-        if (isVerbose) {\r
-            message = error.stack;\r
-        } else {\r
-            message = error.message;\r
-        }\r
-    } else {\r
-        // Plain text error message\r
-        message = error;\r
-    }\r
-\r
-    if (typeof message === 'string' && !message.toUpperCase().startsWith('ERROR:')) {\r
-        // Needed for backward compatibility with external tools\r
-        message = 'Error: ' + message;\r
-    }\r
-\r
-    return message;\r
-}\r
-\r
-module.exports = CordovaLogger;\r
+/*
+ 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.
+ */
+
+var ansi = require('ansi');
+var EventEmitter = require('events').EventEmitter;
+var CordovaError = require('./CordovaError/CordovaError');
+var EOL = require('os').EOL;
+
+var INSTANCE;
+
+/**
+ * @class CordovaLogger
+ *
+ * Implements logging facility that anybody could use. Should not be
+ *   instantiated directly, `CordovaLogger.get()` method should be used instead
+ *   to acquire logger instance
+ */
+function CordovaLogger () {
+    this.levels = {};
+    this.colors = {};
+    this.stdout = process.stdout;
+    this.stderr = process.stderr;
+
+    this.stdoutCursor = ansi(this.stdout);
+    this.stderrCursor = ansi(this.stderr);
+
+    this.addLevel('verbose', 1000, 'grey');
+    this.addLevel('normal', 2000);
+    this.addLevel('warn', 2000, 'yellow');
+    this.addLevel('info', 3000, 'blue');
+    this.addLevel('error', 5000, 'red');
+    this.addLevel('results', 10000);
+
+    this.setLevel('normal');
+}
+
+/**
+ * Static method to create new or acquire existing instance.
+ *
+ * @return  {CordovaLogger}  Logger instance
+ */
+CordovaLogger.get = function () {
+    return INSTANCE || (INSTANCE = new CordovaLogger());
+};
+
+CordovaLogger.VERBOSE = 'verbose';
+CordovaLogger.NORMAL = 'normal';
+CordovaLogger.WARN = 'warn';
+CordovaLogger.INFO = 'info';
+CordovaLogger.ERROR = 'error';
+CordovaLogger.RESULTS = 'results';
+
+/**
+ * Emits log message to process' stdout/stderr depending on message's severity
+ *   and current log level. If severity is less than current logger's level,
+ *   then the message is ignored.
+ *
+ * @param   {String}  logLevel  The message's log level. The logger should have
+ *   corresponding level added (via logger.addLevel), otherwise
+ *   `CordovaLogger.NORMAL` level will be used.
+ * @param   {String}  message   The message, that should be logged to process'
+ *   stdio
+ *
+ * @return  {CordovaLogger}     Current instance, to allow calls chaining.
+ */
+CordovaLogger.prototype.log = function (logLevel, message) {
+    // if there is no such logLevel defined, or provided level has
+    // less severity than active level, then just ignore this call and return
+    if (!this.levels[logLevel] || this.levels[logLevel] < this.levels[this.logLevel]) {
+        // return instance to allow to chain calls
+        return this;
+    }
+
+    var isVerbose = this.logLevel === 'verbose';
+    var cursor = this.stdoutCursor;
+
+    if (message instanceof Error || logLevel === CordovaLogger.ERROR) {
+        message = formatError(message, isVerbose);
+        cursor = this.stderrCursor;
+    }
+
+    var color = this.colors[logLevel];
+    if (color) {
+        cursor.bold().fg[color]();
+    }
+
+    cursor.write(message).reset().write(EOL);
+
+    return this;
+};
+
+/**
+ * Adds a new level to logger instance. This method also creates a shortcut
+ *   method to log events with the level provided (i.e. after adding new level
+ *   'debug', the method `debug(message)`, equal to logger.log('debug', message),
+ *   will be added to logger instance)
+ *
+ * @param  {String}  level     A log level name. The levels with the following
+ *   names added by default to every instance: 'verbose', 'normal', 'warn',
+ *   'info', 'error', 'results'
+ * @param  {Number}  severity  A number that represents level's severity.
+ * @param  {String}  color     A valid color name, that will be used to log
+ *   messages with this level. Any CSS color code or RGB value is allowed
+ *   (according to ansi documentation:
+ *   https://github.com/TooTallNate/ansi.js#features)
+ *
+ * @return  {CordovaLogger}     Current instance, to allow calls chaining.
+ */
+CordovaLogger.prototype.addLevel = function (level, severity, color) {
+
+    this.levels[level] = severity;
+
+    if (color) {
+        this.colors[level] = color;
+    }
+
+    // Define own method with corresponding name
+    if (!this[level]) {
+        this[level] = this.log.bind(this, level);
+    }
+
+    return this;
+};
+
+/**
+ * Sets the current logger level to provided value. If logger doesn't have level
+ *   with this name, `CordovaLogger.NORMAL` will be used.
+ *
+ * @param  {String}  logLevel  Level name. The level with this name should be
+ *   added to logger before.
+ *
+ * @return  {CordovaLogger}     Current instance, to allow calls chaining.
+ */
+CordovaLogger.prototype.setLevel = function (logLevel) {
+    this.logLevel = this.levels[logLevel] ? logLevel : CordovaLogger.NORMAL;
+
+    return this;
+};
+
+/**
+ * Adjusts the current logger level according to the passed options.
+ *
+ * @param   {Object|Array}  opts  An object or args array with options
+ *
+ * @return  {CordovaLogger}     Current instance, to allow calls chaining.
+ */
+CordovaLogger.prototype.adjustLevel = function (opts) {
+    if (opts.verbose || (Array.isArray(opts) && opts.includes('--verbose'))) {
+        this.setLevel('verbose');
+    } else if (opts.silent || (Array.isArray(opts) && opts.includes('--silent'))) {
+        this.setLevel('error');
+    }
+
+    return this;
+};
+
+/**
+ * Attaches logger to EventEmitter instance provided.
+ *
+ * @param   {EventEmitter}  eventEmitter  An EventEmitter instance to attach
+ *   logger to.
+ *
+ * @return  {CordovaLogger}     Current instance, to allow calls chaining.
+ */
+CordovaLogger.prototype.subscribe = function (eventEmitter) {
+
+    if (!(eventEmitter instanceof EventEmitter)) { throw new Error('Subscribe method only accepts an EventEmitter instance as argument'); }
+
+    eventEmitter.on('verbose', this.verbose)
+        .on('log', this.normal)
+        .on('info', this.info)
+        .on('warn', this.warn)
+        .on('warning', this.warn)
+        // Set up event handlers for logging and results emitted as events.
+        .on('results', this.results);
+
+    return this;
+};
+
+function formatError (error, isVerbose) {
+    var message = '';
+
+    if (error instanceof CordovaError) {
+        message = error.toString(isVerbose);
+    } else if (error instanceof Error) {
+        if (isVerbose) {
+            message = error.stack;
+        } else {
+            message = error.message;
+        }
+    } else {
+        // Plain text error message
+        message = error;
+    }
+
+    if (typeof message === 'string' && !message.toUpperCase().startsWith('ERROR:')) {
+        // Needed for backward compatibility with external tools
+        message = 'Error: ' + message;
+    }
+
+    return message;
+}
+
+module.exports = CordovaLogger;
index 5a018de..c8f971a 100644 (file)
-/*\r
-       Licensed to the Apache Software Foundation (ASF) under one\r
-       or more contributor license agreements.  See the NOTICE file\r
-       distributed with this work for additional information\r
-       regarding copyright ownership.  The ASF licenses this file\r
-       to you under the Apache License, Version 2.0 (the\r
-       "License"); you may not use this file except in compliance\r
-       with the License.  You may obtain a copy of the License at\r
-\r
-         http://www.apache.org/licenses/LICENSE-2.0\r
-\r
-       Unless required by applicable law or agreed to in writing,\r
-       software distributed under the License is distributed on an\r
-       "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY\r
-       KIND, either express or implied.  See the License for the\r
-       specific language governing permissions and limitations\r
-       under the License.\r
-*/\r
-\r
-var Q = require('q');\r
-var fs = require('fs-extra');\r
-var path = require('path');\r
-\r
-var ActionStack = require('./ActionStack');\r
-var PlatformJson = require('./PlatformJson');\r
-var CordovaError = require('./CordovaError/CordovaError');\r
-var PlatformMunger = require('./ConfigChanges/ConfigChanges').PlatformMunger;\r
-var PluginInfoProvider = require('./PluginInfo/PluginInfoProvider');\r
-\r
-/**\r
- * @constructor\r
- * @class PluginManager\r
- * Represents an entity for adding/removing plugins for platforms\r
- *\r
- * @param {String} platform Platform name\r
- * @param {Object} locations - Platform files and directories\r
- * @param {IDEProject} ideProject The IDE project to add/remove plugin changes to/from\r
- */\r
-function PluginManager (platform, locations, ideProject) {\r
-    this.platform = platform;\r
-    this.locations = locations;\r
-    this.project = ideProject;\r
-\r
-    var platformJson = PlatformJson.load(locations.root, platform);\r
-    this.munger = new PlatformMunger(platform, locations.root, platformJson, new PluginInfoProvider());\r
-}\r
-\r
-/**\r
- * @constructs PluginManager\r
- * A convenience shortcut to new PluginManager(...)\r
- *\r
- * @param {String} platform Platform name\r
- * @param {Object} locations - Platform files and directories\r
- * @param {IDEProject} ideProject The IDE project to add/remove plugin changes to/from\r
- * @returns new PluginManager instance\r
- */\r
-PluginManager.get = function (platform, locations, ideProject) {\r
-    return new PluginManager(platform, locations, ideProject);\r
-};\r
-\r
-PluginManager.INSTALL = 'install';\r
-PluginManager.UNINSTALL = 'uninstall';\r
-\r
-module.exports = PluginManager;\r
-\r
-/**\r
- * Describes and implements common plugin installation/uninstallation routine. The flow is the following:\r
- *  * Validate and set defaults for options. Note that options are empty by default. Everything\r
- *    needed for platform IDE project must be passed from outside. Plugin variables (which\r
- *    are the part of the options) also must be already populated with 'PACKAGE_NAME' variable.\r
- *  * Collect all plugin's native and web files, get installers/uninstallers and process\r
- *    all these via ActionStack.\r
- *  * Save the IDE project, so the changes made by installers are persisted.\r
- *  * Generate config changes munge for plugin and apply it to all required files\r
- *  * Generate metadata for plugin and plugin modules and save it to 'cordova_plugins.js'\r
- *\r
- * @param {PluginInfo} plugin A PluginInfo structure representing plugin to install\r
- * @param {Object} [options={}] An installation options. It is expected but is not necessary\r
- *   that options would contain 'variables' inner object with 'PACKAGE_NAME' field set by caller.\r
- *\r
- * @returns {Promise} Returns a Q promise, either resolved in case of success, rejected otherwise.\r
- */\r
-PluginManager.prototype.doOperation = function (operation, plugin, options) {\r
-    if (operation !== PluginManager.INSTALL && operation !== PluginManager.UNINSTALL) { return Q.reject(new CordovaError('The parameter is incorrect. The opeation must be either "add" or "remove"')); }\r
-\r
-    if (!plugin || plugin.constructor.name !== 'PluginInfo') { return Q.reject(new CordovaError('The parameter is incorrect. The first parameter should be a PluginInfo instance')); }\r
-\r
-    // Set default to empty object to play safe when accesing properties\r
-    options = options || {};\r
-\r
-    var self = this;\r
-    var actions = new ActionStack();\r
-\r
-    // gather all files need to be handled during operation ...\r
-    plugin.getFilesAndFrameworks(this.platform, options)\r
-        .concat(plugin.getAssets(this.platform))\r
-        .concat(plugin.getJsModules(this.platform))\r
-        // ... put them into stack ...\r
-        .forEach(function (item) {\r
-            var installer = self.project.getInstaller(item.itemType);\r
-            var uninstaller = self.project.getUninstaller(item.itemType);\r
-            var actionArgs = [item, plugin, self.project, options];\r
-\r
-            var action;\r
-            if (operation === PluginManager.INSTALL) {\r
-                action = actions.createAction.apply(actions, [installer, actionArgs, uninstaller, actionArgs]); /* eslint no-useless-call: 0 */\r
-            } else /* op === PluginManager.UNINSTALL */{\r
-                action = actions.createAction.apply(actions, [uninstaller, actionArgs, installer, actionArgs]); /* eslint no-useless-call: 0 */\r
-            }\r
-            actions.push(action);\r
-        });\r
-\r
-    // ... and run through the action stack\r
-    return actions.process(this.platform)\r
-        .then(function () {\r
-            if (self.project.write) {\r
-                self.project.write();\r
-            }\r
-\r
-            if (operation === PluginManager.INSTALL) {\r
-                // Ignore passed `is_top_level` option since platform itself doesn't know\r
-                // anything about managing dependencies - it's responsibility of caller.\r
-                self.munger.add_plugin_changes(plugin, options.variables, /* is_top_level= */true, /* should_increment= */true, options.force);\r
-                self.munger.platformJson.addPluginMetadata(plugin);\r
-            } else {\r
-                self.munger.remove_plugin_changes(plugin, /* is_top_level= */true);\r
-                self.munger.platformJson.removePluginMetadata(plugin);\r
-            }\r
-\r
-            // Save everything (munge and plugin/modules metadata)\r
-            self.munger.save_all();\r
-\r
-            var metadata = self.munger.platformJson.generateMetadata();\r
-            fs.writeFileSync(path.join(self.locations.www, 'cordova_plugins.js'), metadata, 'utf-8');\r
-\r
-            // CB-11022 save plugin metadata to both www and platform_www if options.usePlatformWww is specified\r
-            if (options.usePlatformWww) {\r
-                fs.writeFileSync(path.join(self.locations.platformWww, 'cordova_plugins.js'), metadata, 'utf-8');\r
-            }\r
-        });\r
-};\r
-\r
-PluginManager.prototype.addPlugin = function (plugin, installOptions) {\r
-    return this.doOperation(PluginManager.INSTALL, plugin, installOptions);\r
-};\r
-\r
-PluginManager.prototype.removePlugin = function (plugin, uninstallOptions) {\r
-    return this.doOperation(PluginManager.UNINSTALL, plugin, uninstallOptions);\r
-};\r
+/*
+       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.
+*/
+
+var Q = require('q');
+var fs = require('fs-extra');
+var path = require('path');
+
+var ActionStack = require('./ActionStack');
+var PlatformJson = require('./PlatformJson');
+var CordovaError = require('./CordovaError/CordovaError');
+var PlatformMunger = require('./ConfigChanges/ConfigChanges').PlatformMunger;
+var PluginInfoProvider = require('./PluginInfo/PluginInfoProvider');
+
+/**
+ * @constructor
+ * @class PluginManager
+ * Represents an entity for adding/removing plugins for platforms
+ *
+ * @param {String} platform Platform name
+ * @param {Object} locations - Platform files and directories
+ * @param {IDEProject} ideProject The IDE project to add/remove plugin changes to/from
+ */
+function PluginManager (platform, locations, ideProject) {
+    this.platform = platform;
+    this.locations = locations;
+    this.project = ideProject;
+
+    var platformJson = PlatformJson.load(locations.root, platform);
+    this.munger = new PlatformMunger(platform, locations.root, platformJson, new PluginInfoProvider());
+}
+
+/**
+ * @constructs PluginManager
+ * A convenience shortcut to new PluginManager(...)
+ *
+ * @param {String} platform Platform name
+ * @param {Object} locations - Platform files and directories
+ * @param {IDEProject} ideProject The IDE project to add/remove plugin changes to/from
+ * @returns new PluginManager instance
+ */
+PluginManager.get = function (platform, locations, ideProject) {
+    return new PluginManager(platform, locations, ideProject);
+};
+
+PluginManager.INSTALL = 'install';
+PluginManager.UNINSTALL = 'uninstall';
+
+module.exports = PluginManager;
+
+/**
+ * Describes and implements common plugin installation/uninstallation routine. The flow is the following:
+ *  * Validate and set defaults for options. Note that options are empty by default. Everything
+ *    needed for platform IDE project must be passed from outside. Plugin variables (which
+ *    are the part of the options) also must be already populated with 'PACKAGE_NAME' variable.
+ *  * Collect all plugin's native and web files, get installers/uninstallers and process
+ *    all these via ActionStack.
+ *  * Save the IDE project, so the changes made by installers are persisted.
+ *  * Generate config changes munge for plugin and apply it to all required files
+ *  * Generate metadata for plugin and plugin modules and save it to 'cordova_plugins.js'
+ *
+ * @param {PluginInfo} plugin A PluginInfo structure representing plugin to install
+ * @param {Object} [options={}] An installation options. It is expected but is not necessary
+ *   that options would contain 'variables' inner object with 'PACKAGE_NAME' field set by caller.
+ *
+ * @returns {Promise} Returns a Q promise, either resolved in case of success, rejected otherwise.
+ */
+PluginManager.prototype.doOperation = function (operation, plugin, options) {
+    if (operation !== PluginManager.INSTALL && operation !== PluginManager.UNINSTALL) { return Q.reject(new CordovaError('The parameter is incorrect. The opeation must be either "add" or "remove"')); }
+
+    if (!plugin || plugin.constructor.name !== 'PluginInfo') { return Q.reject(new CordovaError('The parameter is incorrect. The first parameter should be a PluginInfo instance')); }
+
+    // Set default to empty object to play safe when accesing properties
+    options = options || {};
+
+    var self = this;
+    var actions = new ActionStack();
+
+    // gather all files need to be handled during operation ...
+    plugin.getFilesAndFrameworks(this.platform, options)
+        .concat(plugin.getAssets(this.platform))
+        .concat(plugin.getJsModules(this.platform))
+        // ... put them into stack ...
+        .forEach(function (item) {
+            var installer = self.project.getInstaller(item.itemType);
+            var uninstaller = self.project.getUninstaller(item.itemType);
+            var actionArgs = [item, plugin, self.project, options];
+
+            var action;
+            if (operation === PluginManager.INSTALL) {
+                action = actions.createAction.apply(actions, [installer, actionArgs, uninstaller, actionArgs]); /* eslint no-useless-call: 0 */
+            } else /* op === PluginManager.UNINSTALL */{
+                action = actions.createAction.apply(actions, [uninstaller, actionArgs, installer, actionArgs]); /* eslint no-useless-call: 0 */
+            }
+            actions.push(action);
+        });
+
+    // ... and run through the action stack
+    return actions.process(this.platform)
+        .then(function () {
+            if (self.project.write) {
+                self.project.write();
+            }
+
+            if (operation === PluginManager.INSTALL) {
+                // Ignore passed `is_top_level` option since platform itself doesn't know
+                // anything about managing dependencies - it's responsibility of caller.
+                self.munger.add_plugin_changes(plugin, options.variables, /* is_top_level= */true, /* should_increment= */true, options.force);
+                self.munger.platformJson.addPluginMetadata(plugin);
+            } else {
+                self.munger.remove_plugin_changes(plugin, /* is_top_level= */true);
+                self.munger.platformJson.removePluginMetadata(plugin);
+            }
+
+            // Save everything (munge and plugin/modules metadata)
+            self.munger.save_all();
+
+            var metadata = self.munger.platformJson.generateMetadata();
+            fs.writeFileSync(path.join(self.locations.www, 'cordova_plugins.js'), metadata, 'utf-8');
+
+            // CB-11022 save plugin metadata to both www and platform_www if options.usePlatformWww is specified
+            if (options.usePlatformWww) {
+                fs.writeFileSync(path.join(self.locations.platformWww, 'cordova_plugins.js'), metadata, 'utf-8');
+            }
+        });
+};
+
+PluginManager.prototype.addPlugin = function (plugin, installOptions) {
+    return this.doOperation(PluginManager.INSTALL, plugin, installOptions);
+};
+
+PluginManager.prototype.removePlugin = function (plugin, uninstallOptions) {
+    return this.doOperation(PluginManager.UNINSTALL, plugin, uninstallOptions);
+};