You are viewing a plain text version of this content. The canonical link for it is here.
Posted to commits@cordova.apache.org by an...@apache.org on 2015/09/02 13:31:43 UTC

[5/5] cordova-lib git commit: CB-9597 Initial Implementation of PlatformApiPoly

CB-9597 Initial Implementation of PlatformApiPoly

This implements PlatformApiPoly class according to PlatformApi spec, which allows to:
  * create/update platform
  * execute platform's actions (build/run/add/update)
  * do a prepare (needed for CLI workflow only)
  * install/uninstall plugins

Other noticeable changes:
  * removes `getPlatformProject` and PlatformProject method/class in favor of PlatformApiPoly/getPlatformApi
  * make assets and js-modules installing/uninstalling through ActionStack
  * refactor configChanges to not require plugins_dir in constructor
  * moves mergeXml helper to xml-helpers

This should be used along with cordova-cli/platformApi branch


Project: http://git-wip-us.apache.org/repos/asf/cordova-lib/repo
Commit: http://git-wip-us.apache.org/repos/asf/cordova-lib/commit/07271a5c
Tree: http://git-wip-us.apache.org/repos/asf/cordova-lib/tree/07271a5c
Diff: http://git-wip-us.apache.org/repos/asf/cordova-lib/diff/07271a5c

Branch: refs/heads/master
Commit: 07271a5c6162c0b2bee55e1ea23f91ebdfbbb34c
Parents: b0c1965
Author: Vladimir Kotikov <v-...@microsoft.com>
Authored: Thu Jul 9 16:55:31 2015 +0300
Committer: Vladimir Kotikov <v-...@microsoft.com>
Committed: Wed Sep 2 14:24:03 2015 +0300

----------------------------------------------------------------------
 cordova-lib/spec-cordova/cofdova-lib.spec.js    |  23 -
 cordova-lib/spec-cordova/compile.spec.js        |  36 +-
 cordova-lib/spec-cordova/cordova-lib.spec.js    |  23 +
 cordova-lib/spec-cordova/emulate.spec.js        |  48 +-
 .../platforms/android/AndroidManifest.xml       |  14 +
 .../platformApi/platforms/android/android.json  |  11 +
 .../platforms/android/res/xml/config.xml        |  17 +
 .../platforms/windows/cordova/Api.js            |   3 +
 .../metadata/android_parser.spec.js             |  12 +-
 .../metadata/blackberry_parser.spec.js          |  12 +-
 .../metadata/browser_parser.spec.js             |  10 +-
 .../metadata/firefoxos_parser.spec.js           |  10 +-
 .../spec-cordova/metadata/ios_parser.spec.js    |  12 +-
 .../spec-cordova/metadata/webos_parser.spec.js  |   6 +-
 .../metadata/windows8_parser.spec.js            |  12 +-
 .../spec-cordova/metadata/wp8_parser.spec.js    |  12 +-
 .../platforms/PlatformApiPoly.spec.js           | 315 +++++++++
 .../spec-cordova/platforms/platforms.spec.js    |  72 ++
 cordova-lib/spec-cordova/prepare.spec.js        | 159 +----
 cordova-lib/spec-cordova/run.spec.js            |  26 +-
 cordova-lib/spec-cordova/save.spec.js           |  31 +-
 cordova-lib/spec-cordova/xml-helpers.spec.js    | 132 ++++
 .../spec-plugman/install-browserify.spec.js     | 519 --------------
 cordova-lib/spec-plugman/install.spec.js        | 224 +++---
 cordova-lib/spec-plugman/prepare.spec.js        |  73 --
 .../spec-plugman/projects/wp8/config.xml        |  12 +
 .../spec-plugman/uninstall-browserify.spec.js   | 315 ---------
 cordova-lib/spec-plugman/uninstall.spec.js      | 117 ++-
 .../spec-plugman/util/action-stack.spec.js      |  14 +-
 .../spec-plugman/util/config-changes.spec.js    |  96 ++-
 cordova-lib/src/PluginInfo.js                   |   7 +-
 cordova-lib/src/cordova/clean.js                |  20 +-
 cordova-lib/src/cordova/compile.js              |  27 +-
 cordova-lib/src/cordova/emulate.js              |  18 +-
 cordova-lib/src/cordova/platform.js             | 161 ++---
 cordova-lib/src/cordova/plugin.js               | 107 ++-
 cordova-lib/src/cordova/prepare.js              | 205 +-----
 cordova-lib/src/cordova/requirements.js         |  42 +-
 cordova-lib/src/cordova/run.js                  |  17 +-
 cordova-lib/src/cordova/serve.js                |  19 +-
 cordova-lib/src/cordova/targets.js              |   7 +-
 cordova-lib/src/cordova/util.js                 |   9 +-
 cordova-lib/src/platforms/PlatformApiPoly.js    | 706 +++++++++++++++++++
 cordova-lib/src/platforms/platforms.js          | 107 +--
 cordova-lib/src/plugman/browserify.js           | 181 +++++
 cordova-lib/src/plugman/install.js              |  45 +-
 cordova-lib/src/plugman/platforms/common.js     |  30 +-
 cordova-lib/src/plugman/plugman.js              |   3 +-
 cordova-lib/src/plugman/prepare-browserify.js   | 214 ------
 cordova-lib/src/plugman/prepare.js              | 159 -----
 cordova-lib/src/plugman/uninstall.js            |  57 +-
 cordova-lib/src/plugman/util/ConfigFile.js      |   2 +-
 cordova-lib/src/plugman/util/PlatformJson.js    |  20 +
 cordova-lib/src/plugman/util/action-stack.js    |  22 +-
 cordova-lib/src/plugman/util/config-changes.js  |  71 +-
 cordova-lib/src/util/xml-helpers.js             |  72 ++
 56 files changed, 2268 insertions(+), 2426 deletions(-)
----------------------------------------------------------------------


http://git-wip-us.apache.org/repos/asf/cordova-lib/blob/07271a5c/cordova-lib/spec-cordova/cofdova-lib.spec.js
----------------------------------------------------------------------
diff --git a/cordova-lib/spec-cordova/cofdova-lib.spec.js b/cordova-lib/spec-cordova/cofdova-lib.spec.js
deleted file mode 100644
index 7f734b4..0000000
--- a/cordova-lib/spec-cordova/cofdova-lib.spec.js
+++ /dev/null
@@ -1,23 +0,0 @@
-/**
-    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.
-*/
-
-/* jshint unused:false */
-
-// Verify that cordova-lib.js can be loaded
-var cordovaLib = require('../cordova-lib');

http://git-wip-us.apache.org/repos/asf/cordova-lib/blob/07271a5c/cordova-lib/spec-cordova/compile.spec.js
----------------------------------------------------------------------
diff --git a/cordova-lib/spec-cordova/compile.spec.js b/cordova-lib/spec-cordova/compile.spec.js
index ea69743..67a8968 100644
--- a/cordova-lib/spec-cordova/compile.spec.js
+++ b/cordova-lib/spec-cordova/compile.spec.js
@@ -18,9 +18,7 @@
 */
 var cordova = require('../src/cordova/cordova'),
     platforms = require('../src/platforms/platforms'),
-    path = require('path'),
     HooksRunner = require('../src/hooks/HooksRunner'),
-    superspawn = require('../src/cordova/superspawn'),
     util = require('../src/cordova/util'),
     Q = require('q');
 
@@ -28,7 +26,7 @@ var supported_platforms = Object.keys(platforms).filter(function(p) { return p !
 
 
 describe('compile command', function() {
-    var is_cordova, list_platforms, fire, result, cd_project_root;
+    var is_cordova, list_platforms, fire, result, cd_project_root, fail, platformApi, getPlatformApi;
     var project_dir = '/some/path';
 
     function wrapper(f, post) {
@@ -43,7 +41,9 @@ describe('compile command', function() {
         cd_project_root = spyOn(util, 'cdProjectRoot').andReturn(project_dir);
         list_platforms = spyOn(util, 'listPlatforms').andReturn(supported_platforms);
         fire = spyOn(HooksRunner.prototype, 'fire').andReturn(Q());
-        spyOn(superspawn, 'spawn').andCallFake(function() { return Q(); });
+        platformApi = { build: jasmine.createSpy('build').andReturn(Q()) };
+        getPlatformApi = spyOn(platforms, 'getPlatformApi').andReturn(platformApi);
+        fail = function (err) { expect(err.stack).not.toBeDefined(); };
     });
     describe('failure', function() {
         it('should not run inside a Cordova-based project with no added platforms by calling util.listPlatforms', function() {
@@ -63,17 +63,21 @@ describe('compile command', function() {
     describe('success', function() {
         it('should run inside a Cordova-based project with at least one added platform and shell out to build', function(done) {
             cordova.raw.compile(['android','ios']).then(function() {
-                expect(superspawn.spawn).toHaveBeenCalledWith(path.join(project_dir, 'platforms', 'android', 'cordova', 'build'), [], jasmine.any(Object));
-                expect(superspawn.spawn).toHaveBeenCalledWith(path.join(project_dir, 'platforms', 'ios', 'cordova', 'build'), [], jasmine.any(Object));
-                done();
-            });
+                expect(getPlatformApi).toHaveBeenCalledWith('android');
+                expect(getPlatformApi).toHaveBeenCalledWith('ios');
+                expect(platformApi.build).toHaveBeenCalled();
+            })
+            .fail(fail)
+            .fin(done);
         });
 
         it('should pass down optional parameters', function (done) {
-            cordova.raw.compile({platforms:['blackberry10'], options:['--release']}).then(function () {
-                expect(superspawn.spawn).toHaveBeenCalledWith(path.join(project_dir, 'platforms', 'blackberry10', 'cordova', 'build'), ['--release'], jasmine.any(Object));
-                done();
-            });
+            cordova.raw.compile({platforms:['blackberry10'], options:{release: true}}).then(function () {
+                expect(getPlatformApi).toHaveBeenCalledWith('blackberry10');
+                expect(platformApi.build).toHaveBeenCalledWith({release: true});
+            })
+            .fail(fail)
+            .fin(done);
         });
     });
 
@@ -83,13 +87,17 @@ describe('compile command', function() {
                 cordova.raw.compile(['android', 'ios']).then(function() {
                     expect(fire).toHaveBeenCalledWith('before_compile', {verbose: false, platforms:['android', 'ios'], options: []});
                     done();
-                });
+                })
+                .fail(fail)
+                .fin(done);
             });
             it('should fire after hooks through the hooker module', function(done) {
                 cordova.raw.compile('android').then(function() {
                      expect(fire).toHaveBeenCalledWith('after_compile', {verbose: false, platforms:['android'], options: []});
                      done();
-                });
+                })
+                .fail(fail)
+                .fin(done);
             });
         });
 

http://git-wip-us.apache.org/repos/asf/cordova-lib/blob/07271a5c/cordova-lib/spec-cordova/cordova-lib.spec.js
----------------------------------------------------------------------
diff --git a/cordova-lib/spec-cordova/cordova-lib.spec.js b/cordova-lib/spec-cordova/cordova-lib.spec.js
new file mode 100644
index 0000000..7f734b4
--- /dev/null
+++ b/cordova-lib/spec-cordova/cordova-lib.spec.js
@@ -0,0 +1,23 @@
+/**
+    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.
+*/
+
+/* jshint unused:false */
+
+// Verify that cordova-lib.js can be loaded
+var cordovaLib = require('../cordova-lib');

http://git-wip-us.apache.org/repos/asf/cordova-lib/blob/07271a5c/cordova-lib/spec-cordova/emulate.spec.js
----------------------------------------------------------------------
diff --git a/cordova-lib/spec-cordova/emulate.spec.js b/cordova-lib/spec-cordova/emulate.spec.js
index 9561447..e38715f 100644
--- a/cordova-lib/spec-cordova/emulate.spec.js
+++ b/cordova-lib/spec-cordova/emulate.spec.js
@@ -18,8 +18,6 @@
 */
 var cordova = require('../src/cordova/cordova'),
     platforms = require('../src/platforms/platforms'),
-    superspawn = require('../src/cordova/superspawn'),
-    path = require('path'),
     HooksRunner = require('../src/hooks/HooksRunner'),
     Q = require('q'),
     util = require('../src/cordova/util');
@@ -27,9 +25,9 @@ var cordova = require('../src/cordova/cordova'),
 var supported_platforms = Object.keys(platforms).filter(function(p) { return p != 'www'; });
 
 describe('emulate command', function() {
-    var is_cordova, cd_project_root, list_platforms, fire, result;
+    var is_cordova, cd_project_root, list_platforms, fire, result, fail;
     var project_dir = '/some/path';
-    var prepare_spy;
+    var prepare_spy, platformApi, getPlatformApi;
 
     function wrapper(f, post) {
         runs(function() {
@@ -45,7 +43,9 @@ describe('emulate command', function() {
         list_platforms = spyOn(util, 'listPlatforms').andReturn(supported_platforms);
         fire = spyOn(HooksRunner.prototype, 'fire').andReturn(Q());
         prepare_spy = spyOn(cordova.raw, 'prepare').andReturn(Q());
-        spyOn(superspawn, 'spawn').andCallFake(Q);
+        fail = function (err) { expect(err.stack).not.toBeDefined(); };
+        platformApi = { run: jasmine.createSpy('run').andReturn(Q()) };
+        getPlatformApi = spyOn(platforms, 'getPlatformApi').andReturn(platformApi);
     });
     describe('failure', function() {
         it('should not run inside a Cordova-based project with no added platforms by calling util.listPlatforms', function() {
@@ -66,19 +66,21 @@ describe('emulate command', function() {
         it('should run inside a Cordova-based project with at least one added platform and call prepare and shell out to the emulate script', function(done) {
             cordova.raw.emulate(['android','ios']).then(function(err) {
                 expect(prepare_spy).toHaveBeenCalledWith(['android', 'ios']);
-                expect(superspawn.spawn).toHaveBeenCalledWith(path.join(project_dir, 'platforms', 'android', 'cordova', 'run'), ['--emulator'], jasmine.any(Object));
-                expect(superspawn.spawn).toHaveBeenCalledWith(path.join(project_dir, 'platforms', 'ios', 'cordova', 'run'), ['--emulator'], jasmine.any(Object));
-
-                done();
-            });
+                expect(getPlatformApi).toHaveBeenCalledWith('android');
+                expect(getPlatformApi).toHaveBeenCalledWith('ios');
+                expect(platformApi.run).toHaveBeenCalled();
+            })
+            .fail(fail)
+            .fin(done);
         });
         it('should pass down options', function(done) {
-            cordova.raw.emulate({platforms: ['ios'], options:['--optionTastic']}).then(function(err) {
+            cordova.raw.emulate({platforms: ['ios'], options: {optionTastic: true }}).then(function(err) {
                 expect(prepare_spy).toHaveBeenCalledWith(['ios']);
-                expect(superspawn.spawn).toHaveBeenCalledWith(path.join(project_dir, 'platforms', 'ios', 'cordova', 'run'), ['--emulator', '--optionTastic'], jasmine.any(Object));
-
-                done();
-            });
+                expect(getPlatformApi).toHaveBeenCalledWith('ios');
+                expect(platformApi.run).toHaveBeenCalledWith({ device: false, emulator: true, optionTastic: true });
+            })
+            .fail(fail)
+            .fin(done);
         });
     });
 
@@ -86,15 +88,19 @@ describe('emulate command', function() {
         describe('when platforms are added', function() {
             it('should fire before hooks through the hooker module', function(done) {
                 cordova.raw.emulate(['android', 'ios']).then(function() {
-                    expect(fire).toHaveBeenCalledWith('before_emulate', {verbose: false, platforms:['android', 'ios'], options: []});
-                    done();
-                });
+                    expect(fire).toHaveBeenCalledWith('before_emulate',
+                        jasmine.objectContaining({verbose: false, platforms:['android', 'ios'], options: jasmine.any(Object)}));
+                })
+                .fail(fail)
+                .fin(done);
             });
             it('should fire after hooks through the hooker module', function(done) {
                 cordova.raw.emulate('android').then(function() {
-                     expect(fire).toHaveBeenCalledWith('after_emulate', {verbose: false, platforms:['android'], options: []});
-                     done();
-                });
+                     expect(fire).toHaveBeenCalledWith('after_emulate',
+                        jasmine.objectContaining({verbose: false, platforms:['android'], options: jasmine.any(Object)}));
+                })
+                .fail(fail)
+                .fin(done);
             });
         });
 

http://git-wip-us.apache.org/repos/asf/cordova-lib/blob/07271a5c/cordova-lib/spec-cordova/fixtures/projects/platformApi/platforms/android/AndroidManifest.xml
----------------------------------------------------------------------
diff --git a/cordova-lib/spec-cordova/fixtures/projects/platformApi/platforms/android/AndroidManifest.xml b/cordova-lib/spec-cordova/fixtures/projects/platformApi/platforms/android/AndroidManifest.xml
new file mode 100644
index 0000000..be3f245
--- /dev/null
+++ b/cordova-lib/spec-cordova/fixtures/projects/platformApi/platforms/android/AndroidManifest.xml
@@ -0,0 +1,14 @@
+<?xml version='1.0' encoding='utf-8'?>
+<manifest android:hardwareAccelerated="true" android:versionCode="1" android:versionName="0.0.1" android:windowSoftInputMode="adjustPan" package="org.testing" xmlns:android="http://schemas.android.com/apk/res/android">
+    <supports-screens android:anyDensity="true" android:largeScreens="true" android:normalScreens="true" android:resizeable="true" android:smallScreens="true" android:xlargeScreens="true" />
+    <uses-permission android:name="android.permission.INTERNET" />
+    <application android:debuggable="true" android:hardwareAccelerated="true" android:icon="@drawable/icon" android:label="@string/app_name">
+        <activity android:configChanges="orientation|keyboardHidden|keyboard|screenSize|locale" android:label="@string/app_name" android:name="TestBase" android:theme="@android:style/Theme.Black.NoTitleBar">
+            <intent-filter>
+                <action android:name="android.intent.action.MAIN" />
+                <category android:name="android.intent.category.LAUNCHER" />
+            </intent-filter>
+        </activity>
+    </application>
+    <uses-sdk android:minSdkVersion="10" android:targetSdkVersion="17" />
+</manifest>

http://git-wip-us.apache.org/repos/asf/cordova-lib/blob/07271a5c/cordova-lib/spec-cordova/fixtures/projects/platformApi/platforms/android/android.json
----------------------------------------------------------------------
diff --git a/cordova-lib/spec-cordova/fixtures/projects/platformApi/platforms/android/android.json b/cordova-lib/spec-cordova/fixtures/projects/platformApi/platforms/android/android.json
new file mode 100644
index 0000000..07c3697
--- /dev/null
+++ b/cordova-lib/spec-cordova/fixtures/projects/platformApi/platforms/android/android.json
@@ -0,0 +1,11 @@
+{
+    "prepare_queue": {
+        "installed": [],
+        "uninstalled": []
+    },
+    "config_munge": {
+        "files": {}
+    },
+    "installed_plugins": {},
+    "dependent_plugins": {}
+}
\ No newline at end of file

http://git-wip-us.apache.org/repos/asf/cordova-lib/blob/07271a5c/cordova-lib/spec-cordova/fixtures/projects/platformApi/platforms/android/res/xml/config.xml
----------------------------------------------------------------------
diff --git a/cordova-lib/spec-cordova/fixtures/projects/platformApi/platforms/android/res/xml/config.xml b/cordova-lib/spec-cordova/fixtures/projects/platformApi/platforms/android/res/xml/config.xml
new file mode 100644
index 0000000..645eeb5
--- /dev/null
+++ b/cordova-lib/spec-cordova/fixtures/projects/platformApi/platforms/android/res/xml/config.xml
@@ -0,0 +1,17 @@
+<?xml version='1.0' encoding='utf-8'?>
+<widget id="io.cordova.hellocordova" version="0.0.1" xmlns="http://www.w3.org/ns/widgets" xmlns:cdv="http://cordova.apache.org/ns/1.0">
+    <description>
+        A sample Apache Cordova application that responds to the deviceready event.
+    </description>
+    <name>Hello Cordova</name>
+    <description>
+        A sample Apache Cordova application that responds to the deviceready event.
+    </description>
+    <author email="dev@cordova.apache.org" href="http://cordova.io">
+        Apache Cordova Team
+    </author>
+    <content src="index.html" />
+    <access origin="*" />
+    <preference name="fullscreen" value="true" />
+    <preference name="webviewbounce" value="true" />
+</widget>

http://git-wip-us.apache.org/repos/asf/cordova-lib/blob/07271a5c/cordova-lib/spec-cordova/fixtures/projects/platformApi/platforms/windows/cordova/Api.js
----------------------------------------------------------------------
diff --git a/cordova-lib/spec-cordova/fixtures/projects/platformApi/platforms/windows/cordova/Api.js b/cordova-lib/spec-cordova/fixtures/projects/platformApi/platforms/windows/cordova/Api.js
new file mode 100644
index 0000000..bb56cda
--- /dev/null
+++ b/cordova-lib/spec-cordova/fixtures/projects/platformApi/platforms/windows/cordova/Api.js
@@ -0,0 +1,3 @@
+module.exports = function PlatformApi (argument) {
+    this.platform = 'windows';
+};

http://git-wip-us.apache.org/repos/asf/cordova-lib/blob/07271a5c/cordova-lib/spec-cordova/metadata/android_parser.spec.js
----------------------------------------------------------------------
diff --git a/cordova-lib/spec-cordova/metadata/android_parser.spec.js b/cordova-lib/spec-cordova/metadata/android_parser.spec.js
index fd29806..1f56628 100644
--- a/cordova-lib/spec-cordova/metadata/android_parser.spec.js
+++ b/cordova-lib/spec-cordova/metadata/android_parser.spec.js
@@ -19,7 +19,7 @@
 
 /* jshint boss:true */
 
-var platforms = require('../../src/platforms/platforms'),
+var androidParser = require('../../src/cordova/metadata/android_parser'),
     util = require('../../src/cordova/util'),
     path = require('path'),
     shell = require('shelljs'),
@@ -65,12 +65,12 @@ describe('android project parser', function() {
         it('should throw if provided directory does not contain an AndroidManifest.xml', function() {
             exists.andReturn(false);
             expect(function() {
-                new platforms.android.parser(android_proj);
+                new androidParser(android_proj);
             }).toThrow();
         });
         it('should create an instance with path, strings, manifest and android_config properties', function() {
             expect(function() {
-                var p = new platforms.android.parser(android_proj);
+                var p = new androidParser(android_proj);
                 expect(p.path).toEqual(android_proj);
                 expect(p.strings).toEqual(path.join(android_proj, 'res', 'values', 'strings.xml'));
                 expect(p.manifest).toEqual(path.join(android_proj, 'AndroidManifest.xml'));
@@ -78,11 +78,11 @@ describe('android project parser', function() {
             }).not.toThrow();
         });
         it('should be an instance of Parser', function() {
-            expect(new platforms.android.parser(android_proj) instanceof Parser).toBe(true);
+            expect(new androidParser(android_proj) instanceof Parser).toBe(true);
         });
         it('should call super with the correct arguments', function() {
             var call = spyOn(Parser, 'call');
-            var p = new platforms.android.parser(android_proj);
+            var p = new androidParser(android_proj);
             expect(call).toHaveBeenCalledWith(p, 'android', android_proj);
         });
     });
@@ -94,7 +94,7 @@ describe('android project parser', function() {
         beforeEach(function() {
             stringsRoot = null;
             manifestRoot = null;
-            p = new platforms.android.parser(android_proj);
+            p = new androidParser(android_proj);
             cp = spyOn(shell, 'cp');
             rm = spyOn(shell, 'rm');
             is_cordova = spyOn(util, 'isCordova').andReturn(android_proj);

http://git-wip-us.apache.org/repos/asf/cordova-lib/blob/07271a5c/cordova-lib/spec-cordova/metadata/blackberry_parser.spec.js
----------------------------------------------------------------------
diff --git a/cordova-lib/spec-cordova/metadata/blackberry_parser.spec.js b/cordova-lib/spec-cordova/metadata/blackberry_parser.spec.js
index aea0a49..7df3e24 100644
--- a/cordova-lib/spec-cordova/metadata/blackberry_parser.spec.js
+++ b/cordova-lib/spec-cordova/metadata/blackberry_parser.spec.js
@@ -17,7 +17,7 @@
     under the License.
 */
 
-var platforms = require('../../src/platforms/platforms'),
+var blackberryParser = require('../../src/cordova/metadata/blackberry10_parser'),
     util = require('../../src/cordova/util'),
     path = require('path'),
     shell = require('shelljs'),
@@ -76,22 +76,22 @@ describe('blackberry10 project parser', function() {
         it('should throw an exception with a path that is not a native blackberry project', function() {
             exists.andReturn(false);
             expect(function() {
-                new platforms.blackberry10.parser(proj);
+                new blackberryParser(proj);
             }).toThrow();
         });
         it('should accept a proper native blackberry project path as construction parameter', function() {
             var project;
             expect(function() {
-                project = new platforms.blackberry10.parser(proj);
+                project = new blackberryParser(proj);
             }).not.toThrow();
             expect(project).toBeDefined();
         });
         it('should be an instance of Parser', function() {
-            expect(new platforms.blackberry10.parser(proj) instanceof Parser).toBe(true);
+            expect(new blackberryParser(proj) instanceof Parser).toBe(true);
         });
         it('should call super with the correct arguments', function() {
             var call = spyOn(Parser, 'call');
-            var p = new platforms.blackberry10.parser(proj);
+            var p = new blackberryParser(proj);
             expect(call).toHaveBeenCalledWith(p, 'blackberry10', proj);
         });
     });
@@ -100,7 +100,7 @@ describe('blackberry10 project parser', function() {
         var p, cp, rm, mkdir, is_cordova, write, read;
         var bb_proj = path.join(proj, 'platforms', 'blackberry10');
         beforeEach(function() {
-            p = new platforms.blackberry10.parser(bb_proj);
+            p = new blackberryParser(bb_proj);
             cp = spyOn(shell, 'cp');
             rm = spyOn(shell, 'rm');
             mkdir = spyOn(shell, 'mkdir');

http://git-wip-us.apache.org/repos/asf/cordova-lib/blob/07271a5c/cordova-lib/spec-cordova/metadata/browser_parser.spec.js
----------------------------------------------------------------------
diff --git a/cordova-lib/spec-cordova/metadata/browser_parser.spec.js b/cordova-lib/spec-cordova/metadata/browser_parser.spec.js
index 2677103..a44d7d3 100644
--- a/cordova-lib/spec-cordova/metadata/browser_parser.spec.js
+++ b/cordova-lib/spec-cordova/metadata/browser_parser.spec.js
@@ -17,7 +17,7 @@
     under the License.
 */
 
-var platforms = require('../../src/platforms/platforms'),
+var browserParser = require('../../src/cordova/metadata/browser_parser'),
     util = require('../../src/cordova/util'),
     path = require('path'),
     shell = require('shelljs'),
@@ -35,16 +35,16 @@ describe('browser project parser', function() {
     describe('constructions', function() {
         it('should create an instance with a path', function() {
             expect(function() {
-                var p = new platforms.browser.parser(proj);
+                var p = new browserParser(proj);
                 expect(p.path).toEqual(proj);
             }).not.toThrow();
         });
         it('should be an instance of Parser', function() {
-            expect(new platforms.browser.parser(proj) instanceof Parser).toBe(true);
+            expect(new browserParser(proj) instanceof Parser).toBe(true);
         });
         it('should call super with the correct arguments', function() {
             var call = spyOn(Parser, 'call');
-            var p = new platforms.browser.parser(proj);
+            var p = new browserParser(proj);
             expect(call).toHaveBeenCalledWith(p, 'browser', proj);
         });
     });
@@ -54,7 +54,7 @@ describe('browser project parser', function() {
         var browser_proj = path.join(proj, 'platforms', 'browser');
 
         beforeEach(function() {
-            p = new platforms.browser.parser(browser_proj);
+            p = new browserParser(browser_proj);
             cp = spyOn(shell, 'cp');
             rm = spyOn(shell, 'rm');
             mkdir = spyOn(shell, 'mkdir');

http://git-wip-us.apache.org/repos/asf/cordova-lib/blob/07271a5c/cordova-lib/spec-cordova/metadata/firefoxos_parser.spec.js
----------------------------------------------------------------------
diff --git a/cordova-lib/spec-cordova/metadata/firefoxos_parser.spec.js b/cordova-lib/spec-cordova/metadata/firefoxos_parser.spec.js
index c918d16..e4a7eaa 100644
--- a/cordova-lib/spec-cordova/metadata/firefoxos_parser.spec.js
+++ b/cordova-lib/spec-cordova/metadata/firefoxos_parser.spec.js
@@ -19,7 +19,7 @@
 
 /* jshint boss:true */
 
-var platforms = require('../../src/platforms/platforms'),
+var firefoxosParser = require('../../src/cordova/metadata/firefoxos_parser'),
     util = require('../../src/cordova/util'),
     path = require('path'),
     shell = require('shelljs'),
@@ -53,18 +53,18 @@ describe('firefoxos project parser', function() {
     describe('constructions', function() {
         it('should create an instance with a path', function() {
             expect(function() {
-                var p = new platforms.firefoxos.parser(proj);
+                var p = new firefoxosParser(proj);
                 expect(p.path).toEqual(proj);
                 expect(p.config_path).toEqual(path.join(proj, 'config.xml'));
                 expect(p.manifest_path).toEqual(path.join(p.www_dir(), 'manifest.webapp'));
             }).not.toThrow();
         });
         it('should be an instance of Parser', function() {
-            expect(new platforms.firefoxos.parser(proj) instanceof Parser).toBe(true);
+            expect(new firefoxosParser(proj) instanceof Parser).toBe(true);
         });
         it('should call super with the correct arguments', function() {
             var call = spyOn(Parser, 'call');
-            var p = new platforms.firefoxos.parser(proj);
+            var p = new firefoxosParser(proj);
             expect(call).toHaveBeenCalledWith(p, 'firefoxos', proj);
         });
     });
@@ -74,7 +74,7 @@ describe('firefoxos project parser', function() {
         var ff_proj = path.join(proj, 'platforms', 'firefoxos');
         var manifestJson = null;
         beforeEach(function() {
-            p = new platforms.firefoxos.parser(ff_proj);
+            p = new firefoxosParser(ff_proj);
             cp = spyOn(shell, 'cp');
             rm = spyOn(shell, 'rm');
             is_cordova = spyOn(util, 'isCordova').andReturn(proj);

http://git-wip-us.apache.org/repos/asf/cordova-lib/blob/07271a5c/cordova-lib/spec-cordova/metadata/ios_parser.spec.js
----------------------------------------------------------------------
diff --git a/cordova-lib/spec-cordova/metadata/ios_parser.spec.js b/cordova-lib/spec-cordova/metadata/ios_parser.spec.js
index 6ab42da..d7ae6f0 100644
--- a/cordova-lib/spec-cordova/metadata/ios_parser.spec.js
+++ b/cordova-lib/spec-cordova/metadata/ios_parser.spec.js
@@ -16,7 +16,7 @@
  specific language governing permissions and limitations
  under the License.
  */
-var platforms = require('../../src/platforms/platforms'),
+var iosParser = require('../../src/cordova/metadata/ios_parser'),
     util = require('../../src/cordova/util'),
     path = require('path'),
     shell = require('shelljs'),
@@ -55,23 +55,23 @@ describe('ios project parser', function () {
         it('should throw if provided directory does not contain an xcodeproj file', function() {
             readdir.andReturn(['noxcodehere']);
             expect(function() {
-                new platforms.ios.parser(proj);
+                new iosParser(proj);
             }).toThrow();
         });
         it('should create an instance with path, pbxproj, xcodeproj, originalName and cordovaproj properties', function() {
             expect(function() {
-                var p = new platforms.ios.parser(proj);
+                var p = new iosParser(proj);
                 expect(p.path).toEqual(proj);
                 expect(p.pbxproj).toEqual(path.join(proj, 'test.xcodeproj', 'project.pbxproj'));
                 expect(p.xcodeproj).toEqual(path.join(proj, 'test.xcodeproj'));
             }).not.toThrow();
         });
         it('should be an instance of Parser', function() {
-            expect(new platforms.ios.parser(proj) instanceof Parser).toBe(true);
+            expect(new iosParser(proj) instanceof Parser).toBe(true);
         });
         it('should call super with the correct arguments', function() {
             var call = spyOn(Parser, 'call');
-            var p = new platforms.ios.parser(proj);
+            var p = new iosParser(proj);
             expect(call).toHaveBeenCalledWith(p, 'ios', proj);
         });
     });
@@ -80,7 +80,7 @@ describe('ios project parser', function () {
         var p, cp, rm, mkdir, is_cordova, write, read, getOrientation;
         var ios_proj = path.join(proj, 'platforms', 'ios');
         beforeEach(function() {
-            p = new platforms.ios.parser(ios_proj);
+            p = new iosParser(ios_proj);
             cp = spyOn(shell, 'cp');
             rm = spyOn(shell, 'rm');
             mkdir = spyOn(shell, 'mkdir');

http://git-wip-us.apache.org/repos/asf/cordova-lib/blob/07271a5c/cordova-lib/spec-cordova/metadata/webos_parser.spec.js
----------------------------------------------------------------------
diff --git a/cordova-lib/spec-cordova/metadata/webos_parser.spec.js b/cordova-lib/spec-cordova/metadata/webos_parser.spec.js
index 59010c4..c8b1b9b 100755
--- a/cordova-lib/spec-cordova/metadata/webos_parser.spec.js
+++ b/cordova-lib/spec-cordova/metadata/webos_parser.spec.js
@@ -16,7 +16,7 @@
     specific language governing permissions and limitations
     under the License.
 */
-var platforms = require('../../src/platforms/platforms'),
+var webosParser = require('../../src/cordova/metadata/webos_parser'),
     util = require('../../src/cordova/util'),
     path = require('path'),
     shell = require('shelljs'),
@@ -39,7 +39,7 @@ describe('webos project parser', function() {
     describe('constructions', function() {
         it('should create an instance with a path', function() {
             expect(function() {
-                var p = new platforms.android.parser(proj);
+                var p = new webosParser(proj);
                 expect(p.path).toEqual(proj);
             }).not.toThrow();
         });
@@ -49,7 +49,7 @@ describe('webos project parser', function() {
         var p, cp, rm, is_cordova, write, read;
         var wos_proj = path.join(proj, 'platforms', 'webos');
         beforeEach(function() {
-            p = new platforms.webos.parser(wos_proj);
+            p = new webosParser(wos_proj);
             cp = spyOn(shell, 'cp');
             rm = spyOn(shell, 'rm');
             is_cordova = spyOn(util, 'isCordova').andReturn(proj);

http://git-wip-us.apache.org/repos/asf/cordova-lib/blob/07271a5c/cordova-lib/spec-cordova/metadata/windows8_parser.spec.js
----------------------------------------------------------------------
diff --git a/cordova-lib/spec-cordova/metadata/windows8_parser.spec.js b/cordova-lib/spec-cordova/metadata/windows8_parser.spec.js
index f8371d5..d26b5b3 100644
--- a/cordova-lib/spec-cordova/metadata/windows8_parser.spec.js
+++ b/cordova-lib/spec-cordova/metadata/windows8_parser.spec.js
@@ -19,7 +19,7 @@
 
 /* jshint boss:true */
 
-var platforms = require('../../src/platforms/platforms'),
+var windowsParser = require('../../src/cordova/metadata/windows_parser'),
     util = require('../../src/cordova/util'),
     path = require('path'),
     shell = require('shelljs'),
@@ -81,22 +81,22 @@ describe('windows8 project parser', function() {
         it('should throw if provided directory does not contain a jsproj file', function() {
             readdir.andReturn([]);
             expect(function() {
-                new platforms.windows8.parser(proj);
+                new windowsParser(proj);
             }).toThrow();
         });
         it('should create an instance with path, manifest properties', function() {
             expect(function() {
-                var parser = new platforms.windows8.parser(proj);
+                var parser = new windowsParser(proj);
                 expect(parser.projDir).toEqual(proj);
                 expect(parser.manifestPath).toEqual(path.join(proj, 'package.appxmanifest'));
             }).not.toThrow();
         });
         it('should be an instance of Parser', function() {
-            expect(new platforms.windows8.parser(proj) instanceof Parser).toBe(true);
+            expect(new windowsParser(proj) instanceof Parser).toBe(true);
         });
         it('should call super with the correct arguments', function() {
             var call = spyOn(Parser, 'call');
-            var p = new platforms.windows8.parser(proj);
+            var p = new windowsParser(proj);
             expect(call).toHaveBeenCalledWith(p, 'windows8', proj);
         });
     });
@@ -105,7 +105,7 @@ describe('windows8 project parser', function() {
         var parser, cp, rm, is_cordova, write, read, mv, mkdir;
         var windows8_proj = path.join(proj, 'platforms', 'windows8');
         beforeEach(function() {
-            parser = new platforms.windows8.parser(windows8_proj);
+            parser = new windowsParser(windows8_proj);
             cp = spyOn(shell, 'cp');
             rm = spyOn(shell, 'rm');
             mv = spyOn(shell, 'mv');

http://git-wip-us.apache.org/repos/asf/cordova-lib/blob/07271a5c/cordova-lib/spec-cordova/metadata/wp8_parser.spec.js
----------------------------------------------------------------------
diff --git a/cordova-lib/spec-cordova/metadata/wp8_parser.spec.js b/cordova-lib/spec-cordova/metadata/wp8_parser.spec.js
index 5228fa7..aae2643 100644
--- a/cordova-lib/spec-cordova/metadata/wp8_parser.spec.js
+++ b/cordova-lib/spec-cordova/metadata/wp8_parser.spec.js
@@ -19,7 +19,7 @@
 
 /* jshint boss:true, sub:true */
 
-var platforms = require('../../src/platforms/platforms'),
+var wp8Parser = require('../../src/cordova/metadata/wp8_parser'),
     util = require('../../src/cordova/util'),
     path = require('path'),
     shell = require('shelljs'),
@@ -105,22 +105,22 @@ describe('wp8 project parser', function() {
         it('should throw if provided directory does not contain a csproj file', function() {
             readdir.andReturn([]);
             expect(function() {
-                new platforms.wp8.parser(proj);
+                new wp8Parser(proj);
             }).toThrow();
         });
         it('should create an instance with path, manifest properties', function() {
             expect(function() {
-                var p = new platforms.wp8.parser(proj);
+                var p = new wp8Parser(proj);
                 expect(p.wp8_proj_dir).toEqual(proj);
                 expect(p.manifest_path).toEqual(path.join(proj, 'Properties', 'WMAppManifest.xml'));
             }).not.toThrow();
         });
         it('should be an instance of Parser', function() {
-            expect(new platforms.wp8.parser(proj) instanceof Parser).toBe(true);
+            expect(new wp8Parser(proj) instanceof Parser).toBe(true);
         });
         it('should call super with the correct arguments', function() {
             var call = spyOn(Parser, 'call');
-            var p = new platforms.wp8.parser(proj);
+            var p = new wp8Parser(proj);
             expect(call).toHaveBeenCalledWith(p, 'wp8', proj);
         });
     });
@@ -129,7 +129,7 @@ describe('wp8 project parser', function() {
         var p, cp, rm, is_cordova, write, read, mv, mkdir, getOrientation;
         var wp8_proj = path.join(proj, 'platforms', 'wp8');
         beforeEach(function() {
-            p = new platforms.wp8.parser(wp8_proj);
+            p = new wp8Parser(wp8_proj);
             cp = spyOn(shell, 'cp');
             rm = spyOn(shell, 'rm');
             mv = spyOn(shell, 'mv');

http://git-wip-us.apache.org/repos/asf/cordova-lib/blob/07271a5c/cordova-lib/spec-cordova/platforms/PlatformApiPoly.spec.js
----------------------------------------------------------------------
diff --git a/cordova-lib/spec-cordova/platforms/PlatformApiPoly.spec.js b/cordova-lib/spec-cordova/platforms/PlatformApiPoly.spec.js
new file mode 100644
index 0000000..0f60dd8
--- /dev/null
+++ b/cordova-lib/spec-cordova/platforms/PlatformApiPoly.spec.js
@@ -0,0 +1,315 @@
+/**
+    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');
+var et = require('elementtree');
+var path = require('path');
+var shell = require('shelljs');
+var xmlHelpers = require('../../src/util/xml-helpers');
+var ActionStack = require('../../src/plugman/util/action-stack');
+var superspawn = require('../../src/cordova/superspawn');
+var PluginInfo = require('../../src/PluginInfo');
+var ConfigParser = require('../../src/configparser/ConfigParser');
+var knownPlatforms = require('../../src/platforms/platforms');
+var PlatformApiPoly = require('../../src/platforms/PlatformApiPoly');
+
+var PLATFORM = 'android';
+var PLATFORM_VERSION = '3.7.0';
+var PLATFORM_LIB = '/some/platform/lib';
+var CORDOVA_ROOT = path.join(__dirname, '../fixtures/projects/platformApi');
+var PLATFORM_ROOT = path.join(CORDOVA_ROOT, 'platforms/android');
+var DUMMY_PLUGIN = path.join(__dirname, '../fixtures/plugins/test');
+var TEST_XML = '<?xml version="1.0" encoding="UTF-8"?>\n' +
+    '<widget xmlns     = "http://www.w3.org/ns/widgets"\n' +
+    '        xmlns:cdv = "http://cordova.apache.org/ns/1.0"\n' +
+    '        id        = "io.cordova.hellocordova"\n' +
+    '        version   = "0.0.1">\n' +
+    '    <name>Hello Cordova</name>\n' +
+    '    <description>\n' +
+    '        A sample Apache Cordova application that responds to the deviceready event.\n' +
+    '    </description>\n' +
+    '    <author href="http://cordova.io" email="dev@cordova.apache.org">\n' +
+    '        Apache Cordova Team\n' +
+    '    </author>\n' +
+    '    <content src="index.html" />\n' +
+    '    <access origin="*" />\n' +
+    '    <preference name="fullscreen" value="true" />\n' +
+    '    <preference name="webviewbounce" value="true" />\n' +
+    '</widget>\n';
+
+var platformApiPolyPublicMethods = [
+    'getPlatformInfo',
+    'prepare',
+    'addPlugin',
+    'removePlugin',
+    'updatePlugin',
+    'build',
+    'run',
+    'clean',
+    'requirements'
+];
+
+describe('PlatformApi polyfill', function () {
+    var platformApi;
+
+    beforeEach(function () {
+        var originalParseElementtreeSync = xmlHelpers.parseElementtreeSync;
+        spyOn(xmlHelpers, 'parseElementtreeSync').andCallFake(function (configPath) {
+            return /config\.xml$/.test(configPath) ? new et.ElementTree(et.XML(TEST_XML)) :
+                originalParseElementtreeSync(configPath);
+        });
+
+        platformApi = new PlatformApiPoly(PLATFORM, PLATFORM_ROOT);
+    });
+
+    it('should be constructable', function () {
+        var api;
+        expect(function(){api = new PlatformApiPoly(PLATFORM, PLATFORM_ROOT);}).not.toThrow();
+        expect(api).toEqual(jasmine.any(PlatformApiPoly));
+    });
+
+    it('should fail when unknown platform is specified', function () {
+        var api;
+        expect(function(){api = new PlatformApiPoly('fakePlatform', PLATFORM_ROOT);}).toThrow();
+    });
+
+    it('should fail when mandatory argument is not specified', function () {
+        var api;
+        expect(function(){api = new PlatformApiPoly(PLATFORM);}).toThrow();
+        expect(function(){api = new PlatformApiPoly(null, PLATFORM_ROOT);}).toThrow();
+    });
+
+    it('should have fields defined', function () {
+        expect(platformApi.platform).toBe(PLATFORM);
+        expect(platformApi.root).toBe(PLATFORM_ROOT);
+    });
+
+    it('should have \'static\' methods defined', function () {
+        expect(platformApi.constructor.createPlatform).toEqual(jasmine.any(Function));
+        expect(platformApi.constructor.updatePlatform).toEqual(jasmine.any(Function));
+    });
+
+    it('should have methods defined', function () {
+        platformApiPolyPublicMethods.forEach(function (methodName) {
+            expect(platformApi[methodName]).toEqual(jasmine.any(Function));
+        });
+    });
+
+    describe('methods:', function () {
+
+        var FAKE_PROJECT, OPTIONS, getPlatformApi, fail, success;
+
+        beforeEach(function () {
+            getPlatformApi = spyOn(knownPlatforms, 'getPlatformApi').andReturn(platformApi);
+
+            spyOn(shell, 'cp');
+            spyOn(shell, 'rm');
+            spyOn(shell, 'mkdir');
+            spyOn(fs, 'writeFileSync');
+
+            fail = jasmine.createSpy('fail');
+            success = jasmine.createSpy('success');
+
+            FAKE_PROJECT = {locations: {platforms: path.dirname(PLATFORM_ROOT), www: path.join(CORDOVA_ROOT, 'www')}, projectConfig: new ConfigParser('/fake/config.xml')};
+            OPTIONS = {platformDetails: {libDir: PLATFORM_LIB, platform: PLATFORM, version: PLATFORM_VERSION}};
+        });
+
+        describe('static create/updatePlatform methods', function () {
+            var spawn;
+
+            beforeEach(function () {
+                spawn = spyOn(superspawn, 'spawn').andReturn(Q());
+            });
+
+            it('should create/update platform through running platforms\' scripts', function (done) {
+                Q.all([PlatformApiPoly.createPlatform(FAKE_PROJECT, OPTIONS),
+                       PlatformApiPoly.updatePlatform(FAKE_PROJECT, OPTIONS)])
+                .then(function () {
+                    expect(spawn).toHaveBeenCalled();
+                    expect(spawn.calls.length).toBe(2);
+                }).fail(function (err) {
+                    expect(err).not.toBeDefined();
+                }).fin(done);
+            });
+
+            it('should pass down arguments to platforms\' scripts', function (done) {
+                Q.all([PlatformApiPoly.createPlatform(FAKE_PROJECT, OPTIONS),
+                       PlatformApiPoly.updatePlatform(FAKE_PROJECT, OPTIONS)])
+                .then(function () {
+                    expect(spawn).toHaveBeenCalled();
+                    expect(spawn.calls.length).toBe(2);
+                    expect(spawn.calls[0].args[0]).toBe(path.join(PLATFORM_LIB, 'bin/create'));
+                    expect(spawn.calls[0].args[1]).toContain(PLATFORM_ROOT);
+                    expect(spawn.calls[1].args[0]).toBe(path.join(PLATFORM_LIB, 'bin/update'));
+                    expect(spawn.calls[1].args[1]).toContain(PLATFORM_ROOT);
+                }).fail(function (err) {
+                    expect(err).not.toBeDefined();
+                }).fin(done);
+            });
+
+            it('should copy cordova JS sources into created platform', function (done) {
+                Q.all([PlatformApiPoly.createPlatform(FAKE_PROJECT, OPTIONS),
+                       PlatformApiPoly.updatePlatform(FAKE_PROJECT, OPTIONS)])
+                .then(function () {
+                    expect(shell.cp).toHaveBeenCalled();
+                    expect(shell.cp.calls.length).toBe(2);
+                }).fail(fail)
+                .fin(function () {
+                    expect(fail).not.toHaveBeenCalled();
+                    done();
+                });
+            });
+
+            it('should fail immediately if options.platformInfo is not specified', function (done) {
+                Q.all([PlatformApiPoly.createPlatform(FAKE_PROJECT),
+                       PlatformApiPoly.updatePlatform(FAKE_PROJECT)])
+                .then(success)
+                .fail(fail)
+                .fin(function function_name (argument) {
+                    expect(success).not.toHaveBeenCalled();
+                    expect(fail).toHaveBeenCalled();
+                    expect(spawn).not.toHaveBeenCalled();
+                    done();
+                });
+            });
+        });
+
+        describe('prepare method', function () {
+            beforeEach(function () {
+                spyOn(platformApi._parser, 'update_www');
+                spyOn(platformApi._parser, 'update_project').andReturn(Q());
+            });
+
+            it('should return promise', function (done) {
+                var promise = platformApi.prepare(FAKE_PROJECT, OPTIONS);
+                expect(Q.isPromise(promise)).toBeTruthy();
+                promise.fin(done);
+            });
+
+            it('should call parser\'s corresponding methods', function (done) {
+                platformApi.prepare(FAKE_PROJECT, OPTIONS)
+                .then(function () {
+                    [platformApi._parser.update_www, platformApi._parser.update_project]
+                    .forEach(function (method) {
+                        expect(method).toHaveBeenCalled();
+                    });
+                })
+                .fail(fail)
+                .fin(function () {
+                    expect(fail).not.toHaveBeenCalled();
+                    done();
+                });
+            });
+        });
+
+        describe('pluginAdd method', function () {
+            var plugin, actionsProcess;
+
+            beforeEach(function () {
+                plugin = new PluginInfo(DUMMY_PLUGIN);
+                actionsProcess = spyOn(ActionStack.prototype, 'process').andCallThrough();
+            });
+
+            it('should return promise', function (done) {
+                var promise = platformApi.addPlugin(plugin);
+                expect(Q.isPromise(promise)).toBeTruthy();
+                promise.fin(done);
+            });
+
+            it('should fail if plugin parameter is not specified', function (done) {
+                platformApi.addPlugin()
+                .then(success)
+                .fail(fail)
+                .fin(function () {
+                    expect(success).not.toHaveBeenCalled();
+                    expect(fail).toHaveBeenCalled();
+                    done();
+                });
+            });
+
+            it('should process all plugin files through action stack', function (done) {
+                platformApi.addPlugin(plugin)
+                .then(success)
+                .fail(fail)
+                .fin(function () {
+                    expect(actionsProcess).toHaveBeenCalled();
+                    expect(success).toHaveBeenCalled();
+                    expect(fail).not.toHaveBeenCalled();
+                    done();
+                });
+            });
+        });
+
+        describe('platform actions', function () {
+            var spawnSpy;
+
+            beforeEach(function () {
+                spawnSpy = spyOn(superspawn, 'spawn');
+            });
+
+            it('should return promise', function (done) {
+                var ops = [
+                    platformApi.build(/*opts*/),
+                    platformApi.run(/*opts*/),
+                    platformApi.clean(/*opts*/),
+                    platformApi.requirements()
+                ];
+
+                ops.forEach(function (op) {
+                    expect(Q.isPromise(op));
+                });
+                Q.all(ops).fin(done);
+            });
+
+            it('should do their job through running platforms\' scripts', function (done) {
+                var ops = [
+                    platformApi.build(/*opts*/),
+                    platformApi.run(/*opts*/),
+                    platformApi.clean(/*opts*/)
+                ];
+
+                Q.all(ops)
+                .then(function () {
+                    expect(spawnSpy).toHaveBeenCalled();
+                    expect(spawnSpy.calls.length).toEqual(3);
+                }).fin(done);
+            });
+
+            it('should convert and pass down options to platforms\' scripts', function (done) {
+                var options = {
+                    release: true,
+                    nobuild: true,
+                    device: true,
+                    target: 'FakeDevice',
+                    archs: ['arm', 'x86'],
+                    buildConfig: '/some/path'
+                };
+                spawnSpy.andReturn(Q());
+                platformApi.build(options)
+                .then(function () {
+                    ['--release', '--nobuild', '--device', '--target=' + options.target, '--archs=arm,x86', '--buildConfig='  +options.buildConfig]
+                    .forEach(function (arg) {
+                        expect(spawnSpy.calls[0].args[1]).toContain(arg);
+                    });
+                }).fin(done);
+            });
+        });
+    });
+});

http://git-wip-us.apache.org/repos/asf/cordova-lib/blob/07271a5c/cordova-lib/spec-cordova/platforms/platforms.spec.js
----------------------------------------------------------------------
diff --git a/cordova-lib/spec-cordova/platforms/platforms.spec.js b/cordova-lib/spec-cordova/platforms/platforms.spec.js
new file mode 100644
index 0000000..90b690c
--- /dev/null
+++ b/cordova-lib/spec-cordova/platforms/platforms.spec.js
@@ -0,0 +1,72 @@
+/**
+    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 path = require('path');
+var rewire = require('rewire');
+
+var util = require('../../src/cordova/util');
+var platforms = rewire('../../src/platforms/platforms');
+
+var CORDOVA_ROOT = path.join(__dirname, '../fixtures/projects/platformApi');
+var PLATFORM_WITH_API = path.join(CORDOVA_ROOT, 'platforms/windows');
+var PLATFORM_WOUT_API = path.join(CORDOVA_ROOT, 'platforms/android');
+
+var MockPlatformApi = require(path.join(PLATFORM_WITH_API, 'cordova', 'Api'));
+var PlatformApiPoly = require('../../src/platforms/PlatformApiPoly');
+
+describe('getPlatformApi method', function () {
+    var isCordova;
+
+    beforeEach(function () {
+        // reset api cache after each spec
+        platforms.__set__('cachedApis', {});
+        isCordova = spyOn(util, 'isCordova').andReturn(CORDOVA_ROOT);
+    });
+
+    it('should return PlatformApi class defined by platform', function () {
+        var platformApi = platforms.getPlatformApi('windows', PLATFORM_WITH_API);
+        expect(platformApi).toEqual(jasmine.any(MockPlatformApi));
+    });
+
+    it('should return PlatformApi polyfill if PlatformApi is not defined by platform', function () {
+        var platformApi = platforms.getPlatformApi('android', PLATFORM_WOUT_API);
+        expect(platformApi).toEqual(jasmine.any(PlatformApiPoly));
+    });
+
+    it('should cache PlatformApi instance for further calls', function () {
+        var platformApi = platforms.getPlatformApi('windows', PLATFORM_WITH_API);
+        expect(platformApi.fakeProperty).not.toBeDefined();
+        platformApi.fakeProperty = 'fakeValue';
+        expect(platforms.getPlatformApi('windows', PLATFORM_WITH_API).fakeProperty).toBe('fakeValue');
+    });
+
+    it('should succeed if called inside of cordova project w/out platformRoot param', function () {
+        var platformApi = platforms.getPlatformApi('windows');
+        expect(platformApi instanceof MockPlatformApi).toBeTruthy();
+    });
+
+    it('should throw if called outside of cordova project w/out platformRoot param', function () {
+        isCordova.andReturn(false);
+        expect(function () { platforms.getPlatformApi('windows'); }).toThrow();
+    });
+
+    it('should throw for unknown platform', function () {
+        expect(function () { platforms.getPlatformApi('invalid_platform'); }).toThrow();
+    });
+});

http://git-wip-us.apache.org/repos/asf/cordova-lib/blob/07271a5c/cordova-lib/spec-cordova/prepare.spec.js
----------------------------------------------------------------------
diff --git a/cordova-lib/spec-cordova/prepare.spec.js b/cordova-lib/spec-cordova/prepare.spec.js
index 666394c..bf9d31a 100644
--- a/cordova-lib/spec-cordova/prepare.spec.js
+++ b/cordova-lib/spec-cordova/prepare.spec.js
@@ -18,7 +18,6 @@
 */
 
 var shell = require('shelljs'),
-    plugman = require('../src/plugman/plugman'),
     PlatformJson = require('../src/plugman/util/PlatformJson'),
     path = require('path'),
     util = require('../src/cordova/util'),
@@ -59,35 +58,33 @@ describe('prepare command', function() {
         list_platforms,
         fire,
         parsers = {},
-        plugman_prepare,
         find_plugins,
         cp,
         mkdir,
-        load;
-    beforeEach(function() {
-        is_cordova = spyOn(util, 'isCordova').andReturn(project_dir);
-        cd_project_root = spyOn(util, 'cdProjectRoot').andReturn(project_dir);
-        list_platforms = spyOn(util, 'listPlatforms').andReturn(supported_platforms);
-        fire = spyOn(HooksRunner.prototype, 'fire').andReturn(Q());
+        load, platformApi, getPlatformApi;
 
-        spyOn(platforms, 'getPlatformProject').andCallFake(function(platform, rootDir) {
+    beforeEach(function () {
+        getPlatformApi = spyOn(platforms, 'getPlatformApi').andCallFake(function (platform, rootDir) {
             return {
-                update_www: jasmine.createSpy(platform + ' update_www'),
-                cordovajs_path: function(libDir) { return 'path/to/cordova.js/in/.cordova/lib';},
-                www_dir:function() { return path.join(project_dir, 'platforms', platform, 'www'); },
-                config_xml: function () { return path.join(project_dir, 'platforms', platform, 'www', 'config.xml');},
-                update_project: function () { return Q();},
+                prepare: jasmine.createSpy('prepare').andReturn(Q()),
+                getPlatformInfo: jasmine.createSpy('getPlatformInfo').andReturn({
+                    locations: {
+                        www: path.join(project_dir, 'platforms', platform, 'www')
+                    }
+                }),
             };
         });
+        is_cordova = spyOn(util, 'isCordova').andReturn(project_dir);
+        cd_project_root = spyOn(util, 'cdProjectRoot').andReturn(project_dir);
+        list_platforms = spyOn(util, 'listPlatforms').andReturn(supported_platforms);
+        fire = spyOn(HooksRunner.prototype, 'fire').andReturn(Q());
 
-        plugman_prepare = spyOn(plugman, 'prepare').andReturn(Q());
         find_plugins = spyOn(util, 'findPlugins').andReturn([]);
         spyOn(PlatformJson, 'load').andReturn(new PlatformJson(null, null, {}));
         spyOn(PlatformJson.prototype, 'save');
         load = spyOn(lazy_load, 'based_on_config').andReturn(Q());
         cp = spyOn(shell, 'cp').andReturn(true);
         mkdir = spyOn(shell, 'mkdir');
-        spyOn(prepare, '_mergeXml');
         spyOn(ConfigParser.prototype, 'write');
         spyOn(xmlHelpers, 'parseElementtreeSync').andCallFake(function() {
             return new et.ElementTree(et.XML(TEST_XML));
@@ -122,26 +119,17 @@ describe('prepare command', function() {
                 expect(err).toBeUndefined();
             }).fin(done);
         });
-        it('should invoke each platform\'s parser\'s update_project method', function(done) {
+        it('should get PlatformApi instance for each platform and invoke its\' run method', function(done) {
             prepare().then(function() {
                 supported_platforms.forEach(function(p) {
                     expect(parsers[p]).toHaveBeenCalled();
+                    expect(getPlatformApi).toHaveBeenCalledWith(p);
                 });
+                expect(platformApi.run).toHaveBeenCalled();
             }, function(err) {
                 expect(err).toBeUndefined();
             }).fin(done);
         });
-        describe('plugman integration', function() {
-            it('should invoke plugman.prepare after update_project', function(done) {
-                prepare().then(function() {
-                    supported_platforms.forEach(function(p) {
-                        expect(plugman_prepare).toHaveBeenCalled();
-                    });
-                }, function(err) {
-                    expect(err).toBeUndefined();
-                }).fin(done);
-            });
-        });
     });
 
     describe('hooks', function() {
@@ -178,118 +166,3 @@ describe('prepare command', function() {
         });
     });
 });
-
-describe('prepare._mergeXml', function () {
-    var dstXml;
-    beforeEach(function() {
-        dstXml = et.XML(TEST_XML);
-    });
-    it('should merge attributes and text of the root element without clobbering', function () {
-        var testXml = et.XML('<widget foo="bar" id="NOTANID">TEXT</widget>');
-        prepare._mergeXml(testXml, dstXml);
-        expect(dstXml.attrib.foo).toEqual('bar');
-        expect(dstXml.attrib.id).not.toEqual('NOTANID');
-        expect(dstXml.text).not.toEqual('TEXT');
-    });
-
-    it('should merge attributes and text of the root element with clobbering', function () {
-        var testXml = et.XML('<widget foo="bar" id="NOTANID">TEXT</widget>');
-        prepare._mergeXml(testXml, dstXml, 'foo', true);
-        expect(dstXml.attrib.foo).toEqual('bar');
-        expect(dstXml.attrib.id).toEqual('NOTANID');
-        expect(dstXml.text).toEqual('TEXT');
-    });
-
-    it('should not merge platform tags with the wrong platform', function () {
-        var testXml = et.XML('<widget><platform name="bar"><testElement testAttrib="value">testTEXT</testElement></platform></widget>'),
-            origCfg = et.tostring(dstXml);
-
-        prepare._mergeXml(testXml, dstXml, 'foo', true);
-        expect(et.tostring(dstXml)).toEqual(origCfg);
-    });
-
-    it('should merge platform tags with the correct platform', function () {
-        var testXml = et.XML('<widget><platform name="bar"><testElement testAttrib="value">testTEXT</testElement></platform></widget>'),
-            origCfg = et.tostring(dstXml);
-
-        prepare._mergeXml(testXml, dstXml, 'bar', true);
-        expect(et.tostring(dstXml)).not.toEqual(origCfg);
-        var testElement = dstXml.find('testElement');
-        expect(testElement).toBeDefined();
-        expect(testElement.attrib.testAttrib).toEqual('value');
-        expect(testElement.text).toEqual('testTEXT');
-    });
-
-    it('should merge singelton children without clobber', function () {
-        var testXml = et.XML('<widget><author testAttrib="value" href="http://www.nowhere.com">SUPER_AUTHOR</author></widget>');
-
-        prepare._mergeXml(testXml, dstXml);
-        var testElements = dstXml.findall('author');
-        expect(testElements).toBeDefined();
-        expect(testElements.length).toEqual(1);
-        expect(testElements[0].attrib.testAttrib).toEqual('value');
-        expect(testElements[0].attrib.href).toEqual('http://cordova.io');
-        expect(testElements[0].attrib.email).toEqual('dev@cordova.apache.org');
-        expect(testElements[0].text).toContain('Apache Cordova Team');
-    });
-
-    it('should clobber singelton children with clobber', function () {
-        var testXml = et.XML('<widget><author testAttrib="value" href="http://www.nowhere.com">SUPER_AUTHOR</author></widget>');
-
-        prepare._mergeXml(testXml, dstXml, '', true);
-        var testElements = dstXml.findall('author');
-        expect(testElements).toBeDefined();
-        expect(testElements.length).toEqual(1);
-        expect(testElements[0].attrib.testAttrib).toEqual('value');
-        expect(testElements[0].attrib.href).toEqual('http://www.nowhere.com');
-        expect(testElements[0].attrib.email).toEqual('dev@cordova.apache.org');
-        expect(testElements[0].text).toEqual('SUPER_AUTHOR');
-    });
-
-    it('should append non singelton children', function () {
-        var testXml = et.XML('<widget><preference num="1"/> <preference num="2"/></widget>');
-
-        prepare._mergeXml(testXml, dstXml, '', true);
-        var testElements = dstXml.findall('preference');
-        expect(testElements.length).toEqual(4);
-    });
-
-    it('should handle namespaced elements', function () {
-        var testXml = et.XML('<widget><foo:bar testAttrib="value">testText</foo:bar></widget>');
-
-        prepare._mergeXml(testXml, dstXml, 'foo', true);
-        var testElement = dstXml.find('foo:bar');
-        expect(testElement).toBeDefined();
-        expect(testElement.attrib.testAttrib).toEqual('value');
-        expect(testElement.text).toEqual('testText');
-    });
-
-    it('should not append duplicate non singelton children', function () {
-        var testXml = et.XML('<widget><preference name="fullscreen" value="true"/></widget>');
-
-        prepare._mergeXml(testXml, dstXml, '', true);
-        var testElements = dstXml.findall('preference');
-        expect(testElements.length).toEqual(2);
-    });
-
-    it('should not skip partial duplicate non singelton children', function () {
-        //remove access tags from dstXML
-        var testElements = dstXml.findall('access');
-        for(var i = 0; i < testElements.length; i++) {
-            dstXml.remove(testElements[i]);
-        }
-        testElements = dstXml.findall('access');
-        expect(testElements.length).toEqual(0);
-        //add an external whitelist access tag
-        var testXml = et.XML('<widget><access origin="*" launch-external="yes"/></widget>');
-        prepare._mergeXml(testXml, dstXml, '', true);
-        testElements = dstXml.findall('access');
-        expect(testElements.length).toEqual(1);
-        //add an internal whitelist access tag
-        testXml = et.XML('<widget><access origin="*"/></widget>');
-        prepare._mergeXml(testXml, dstXml, '', true);
-        testElements = dstXml.findall('access');
-        expect(testElements.length).toEqual(2);
-
-    });    
-});

http://git-wip-us.apache.org/repos/asf/cordova-lib/blob/07271a5c/cordova-lib/spec-cordova/run.spec.js
----------------------------------------------------------------------
diff --git a/cordova-lib/spec-cordova/run.spec.js b/cordova-lib/spec-cordova/run.spec.js
index 6271cfe..627451b 100644
--- a/cordova-lib/spec-cordova/run.spec.js
+++ b/cordova-lib/spec-cordova/run.spec.js
@@ -18,8 +18,6 @@
 */
 var cordova = require('../src/cordova/cordova'),
     platforms = require('../src/platforms/platforms'),
-    superspawn = require('../src/cordova/superspawn'),
-    path = require('path'),
     HooksRunner = require('../src/hooks/HooksRunner'),
     Q = require('q'),
     util = require('../src/cordova/util');
@@ -27,7 +25,7 @@ var cordova = require('../src/cordova/cordova'),
 var supported_platforms = Object.keys(platforms).filter(function(p) { return p != 'www'; });
 
 describe('run command', function() {
-    var is_cordova, cd_project_root, list_platforms, fire;
+    var is_cordova, cd_project_root, list_platforms, fire, platformApi, getPlatformApi;
     var project_dir = '/some/path';
     var prepare_spy;
 
@@ -37,7 +35,8 @@ describe('run command', function() {
         list_platforms = spyOn(util, 'listPlatforms').andReturn(supported_platforms);
         fire = spyOn(HooksRunner.prototype, 'fire').andReturn(Q());
         prepare_spy = spyOn(cordova.raw, 'prepare').andReturn(Q());
-        spyOn(superspawn, 'spawn').andReturn(Q);
+        platformApi = { run: jasmine.createSpy('run').andReturn(Q()) };
+        getPlatformApi = spyOn(platforms, 'getPlatformApi').andReturn(platformApi);
     });
     describe('failure', function() {
         it('should not run inside a Cordova-based project with no added platforms by calling util.listPlatforms', function(done) {
@@ -61,19 +60,26 @@ describe('run command', function() {
     });
 
     describe('success', function() {
-        it('should run inside a Cordova-based project with at least one added platform and call prepare and shell out to the run script', function(done) {
+        it('should call prepare before actually run platform ', function(done) {
             cordova.raw.run(['android','ios']).then(function() {
                 expect(prepare_spy).toHaveBeenCalledWith({ platforms: [ 'android', 'ios' ], verbose: false, options: [] });
-                expect(superspawn.spawn).toHaveBeenCalledWith(path.join(project_dir, 'platforms', 'android', 'cordova', 'run'), [], jasmine.any(Object));
-                expect(superspawn.spawn).toHaveBeenCalledWith(path.join(project_dir, 'platforms', 'ios', 'cordova', 'run'), [], jasmine.any(Object));
+            }, function(err) {
+                expect(err).toBeUndefined();
+            }).fin(done);
+        });
+        it('should get PlatformApi instance for each platform and call its\' run method', function(done) {
+            cordova.raw.run(['android','ios']).then(function() {
+                expect(getPlatformApi).toHaveBeenCalledWith('android');
+                expect(getPlatformApi).toHaveBeenCalledWith('ios');
+                expect(platformApi.run).toHaveBeenCalled();
             }, function(err) {
                 expect(err).toBeUndefined();
             }).fin(done);
         });
         it('should pass down parameters', function(done) {
-            cordova.raw.run({platforms: ['blackberry10'], options:['--password', '1q1q']}).then(function() {
-                expect(prepare_spy).toHaveBeenCalledWith({ platforms: [ 'blackberry10' ], options: [ '--password', '1q1q' ], verbose: false });
-                expect(superspawn.spawn).toHaveBeenCalledWith(path.join(project_dir, 'platforms', 'blackberry10', 'cordova', 'run'), ['--password', '1q1q'], jasmine.any(Object));
+            cordova.raw.run({platforms: ['blackberry10'], options:{password: '1q1q'}}).then(function() {
+                expect(prepare_spy).toHaveBeenCalledWith({ platforms: [ 'blackberry10' ], options: { password: '1q1q' }, verbose: false });
+                expect(platformApi.run).toHaveBeenCalledWith({password: '1q1q'});
             }, function(err) {
                 expect(err).toBeUndefined();
             }).fin(done);

http://git-wip-us.apache.org/repos/asf/cordova-lib/blob/07271a5c/cordova-lib/spec-cordova/save.spec.js
----------------------------------------------------------------------
diff --git a/cordova-lib/spec-cordova/save.spec.js b/cordova-lib/spec-cordova/save.spec.js
index a04dc4b..24a6128 100644
--- a/cordova-lib/spec-cordova/save.spec.js
+++ b/cordova-lib/spec-cordova/save.spec.js
@@ -23,9 +23,11 @@ describe('(save flag)', function () {
         helpers     = require('./helpers'),
         path        = require('path'),
         Q           = require('q'),
+        fs          = require('fs'),
         shell       = require('shelljs'),
         util        = require('../src/cordova/util'),
         prepare     = require('../src/cordova/prepare'),
+        PlatformApi = require('../src/platforms/PlatformApiPoly'),
         platform    = rewire('../src/cordova/platform');
 
     var appName             = 'testApp',
@@ -54,9 +56,8 @@ describe('(save flag)', function () {
         timeout             = 60 * 1000;
 
     //mock variables
-    var revert_copy_cordova_js,
-        revert_copy_cordovajs_src,
-        revert_install_plugins_for_new_platform;
+    var revert_install_plugins_for_new_platform,
+        createPlatformOrig = PlatformApi.createPlatform;
 
     beforeEach(function (done) {
         // initial cleanup
@@ -68,9 +69,10 @@ describe('(save flag)', function () {
         spyOn(util, 'cdProjectRoot').andReturn(appPath);
         spyOn(cordova.raw, 'prepare').andReturn(Q());
 
+        spyOn(PlatformApi, 'createPlatform').andReturn(Q());
+        spyOn(PlatformApi, 'updatePlatform').andReturn(Q());
+
         //rewire mocks
-        revert_copy_cordova_js = platform.__set__('copy_cordova_js', function () {});
-        revert_copy_cordovajs_src = platform.__set__('copy_cordovajs_src', function () {});
         revert_install_plugins_for_new_platform = platform.__set__('installPluginsForNewPlatform', function () { return Q(); });
 
        //creating test app
@@ -86,8 +88,6 @@ describe('(save flag)', function () {
     }, timeout);
 
     afterEach(function () {
-        revert_copy_cordova_js();
-        revert_copy_cordovajs_src();
         revert_install_plugins_for_new_platform();
     });
 
@@ -232,7 +232,11 @@ describe('(save flag)', function () {
             helpers.setEngineSpec(appPath, platformName, platformVersionNew);
             platform('add', platformName + '@' + platformVersionNew)
             .then(function () {
-                return cordova.raw.platform('update', platformName + '@' + platformVersionOld, { 'save': true });
+                var fsExistsSync = fs.existsSync.bind(fs);
+                spyOn(fs, 'existsSync').andCallFake(function (somePath) {
+                    return (somePath === path.join(appPath, 'platforms', platformName)) || fsExistsSync(somePath);
+                });
+                return platform('update', platformName + '@' + platformVersionOld, { 'save': true });
             }).then(function () {
                 expect(helpers.getEngineSpec(appPath, platformName)).toBe('~' + platformVersionOld);
                 done();
@@ -247,7 +251,11 @@ describe('(save flag)', function () {
             helpers.setEngineSpec(appPath, platformName, platformVersionNew);
             platform('add', platformName + '@' + platformVersionNew)
             .then(function () {
-                return cordova.raw.platform('update', platformGitUrl, { 'save': true });
+                var fsExistsSync = fs.existsSync.bind(fs);
+                spyOn(fs, 'existsSync').andCallFake(function (somePath) {
+                    return (somePath === path.join(appPath, 'platforms', platformName)) || fsExistsSync(somePath);
+                });
+                return platform('update', platformGitUrl, { 'save': true });
             }).then(function () {
                 var spec = helpers.getEngineSpec(appPath, platformName);
                 expect(spec).not.toBe(null);
@@ -437,6 +445,11 @@ describe('(save flag)', function () {
     });
 
     describe('prepare', function () {
+        beforeEach(function () {
+            // Restore back mocked createPlatform functionality
+            PlatformApi.createPlatform = createPlatformOrig;
+        });
+
         it('spec.23 should restore all platforms and plugins', function (done) {
             helpers.setEngineSpec(appPath, platformName, platformLocalPath);
             helpers.setPluginSpec(appPath, localPluginName, localPluginPath);

http://git-wip-us.apache.org/repos/asf/cordova-lib/blob/07271a5c/cordova-lib/spec-cordova/xml-helpers.spec.js
----------------------------------------------------------------------
diff --git a/cordova-lib/spec-cordova/xml-helpers.spec.js b/cordova-lib/spec-cordova/xml-helpers.spec.js
index 25e6c8f..ade3c47 100644
--- a/cordova-lib/spec-cordova/xml-helpers.spec.js
+++ b/cordova-lib/spec-cordova/xml-helpers.spec.js
@@ -34,6 +34,23 @@ var path = require('path')
   , goodbyeTag = et.XML('<h1>GOODBYE</h1>')
   , helloTagTwo = et.XML('<h1>  HELLO  </h1>');
 
+var TEST_XML = '<?xml version="1.0" encoding="UTF-8"?>\n' +
+    '<widget xmlns     = "http://www.w3.org/ns/widgets"\n' +
+    '        xmlns:cdv = "http://cordova.apache.org/ns/1.0"\n' +
+    '        id        = "io.cordova.hellocordova"\n' +
+    '        version   = "0.0.1">\n' +
+    '    <name>Hello Cordova</name>\n' +
+    '    <description>\n' +
+    '        A sample Apache Cordova application that responds to the deviceready event.\n' +
+    '    </description>\n' +
+    '    <author href="http://cordova.io" email="dev@cordova.apache.org">\n' +
+    '        Apache Cordova Team\n' +
+    '    </author>\n' +
+    '    <content src="index.html" />\n' +
+    '    <access origin="*" />\n' +
+    '    <preference name="fullscreen" value="true" />\n' +
+    '    <preference name="webviewbounce" value="true" />\n' +
+    '</widget>\n';
 
 describe('xml-helpers', function(){
     describe('parseElementtreeSync', function() {
@@ -136,4 +153,119 @@ describe('xml-helpers', function(){
             expect(config_xml.findall('access').length).toEqual(3);
         });
     });
+
+    describe('mergeXml', function () {
+        var dstXml;
+        beforeEach(function() {
+            dstXml = et.XML(TEST_XML);
+        });
+        it('should merge attributes and text of the root element without clobbering', function () {
+            var testXml = et.XML('<widget foo="bar" id="NOTANID">TEXT</widget>');
+            xml_helpers.mergeXml(testXml, dstXml);
+            expect(dstXml.attrib.foo).toEqual('bar');
+            expect(dstXml.attrib.id).not.toEqual('NOTANID');
+            expect(dstXml.text).not.toEqual('TEXT');
+        });
+
+        it('should merge attributes and text of the root element with clobbering', function () {
+            var testXml = et.XML('<widget foo="bar" id="NOTANID">TEXT</widget>');
+            xml_helpers.mergeXml(testXml, dstXml, 'foo', true);
+            expect(dstXml.attrib.foo).toEqual('bar');
+            expect(dstXml.attrib.id).toEqual('NOTANID');
+            expect(dstXml.text).toEqual('TEXT');
+        });
+
+        it('should not merge platform tags with the wrong platform', function () {
+            var testXml = et.XML('<widget><platform name="bar"><testElement testAttrib="value">testTEXT</testElement></platform></widget>'),
+                origCfg = et.tostring(dstXml);
+
+            xml_helpers.mergeXml(testXml, dstXml, 'foo', true);
+            expect(et.tostring(dstXml)).toEqual(origCfg);
+        });
+
+        it('should merge platform tags with the correct platform', function () {
+            var testXml = et.XML('<widget><platform name="bar"><testElement testAttrib="value">testTEXT</testElement></platform></widget>'),
+                origCfg = et.tostring(dstXml);
+
+            xml_helpers.mergeXml(testXml, dstXml, 'bar', true);
+            expect(et.tostring(dstXml)).not.toEqual(origCfg);
+            var testElement = dstXml.find('testElement');
+            expect(testElement).toBeDefined();
+            expect(testElement.attrib.testAttrib).toEqual('value');
+            expect(testElement.text).toEqual('testTEXT');
+        });
+
+        it('should merge singelton children without clobber', function () {
+            var testXml = et.XML('<widget><author testAttrib="value" href="http://www.nowhere.com">SUPER_AUTHOR</author></widget>');
+
+            xml_helpers.mergeXml(testXml, dstXml);
+            var testElements = dstXml.findall('author');
+            expect(testElements).toBeDefined();
+            expect(testElements.length).toEqual(1);
+            expect(testElements[0].attrib.testAttrib).toEqual('value');
+            expect(testElements[0].attrib.href).toEqual('http://cordova.io');
+            expect(testElements[0].attrib.email).toEqual('dev@cordova.apache.org');
+            expect(testElements[0].text).toContain('Apache Cordova Team');
+        });
+
+        it('should clobber singelton children with clobber', function () {
+            var testXml = et.XML('<widget><author testAttrib="value" href="http://www.nowhere.com">SUPER_AUTHOR</author></widget>');
+
+            xml_helpers.mergeXml(testXml, dstXml, '', true);
+            var testElements = dstXml.findall('author');
+            expect(testElements).toBeDefined();
+            expect(testElements.length).toEqual(1);
+            expect(testElements[0].attrib.testAttrib).toEqual('value');
+            expect(testElements[0].attrib.href).toEqual('http://www.nowhere.com');
+            expect(testElements[0].attrib.email).toEqual('dev@cordova.apache.org');
+            expect(testElements[0].text).toEqual('SUPER_AUTHOR');
+        });
+
+        it('should append non singelton children', function () {
+            var testXml = et.XML('<widget><preference num="1"/> <preference num="2"/></widget>');
+
+            xml_helpers.mergeXml(testXml, dstXml, '', true);
+            var testElements = dstXml.findall('preference');
+            expect(testElements.length).toEqual(4);
+        });
+
+        it('should handle namespaced elements', function () {
+            var testXml = et.XML('<widget><foo:bar testAttrib="value">testText</foo:bar></widget>');
+
+            xml_helpers.mergeXml(testXml, dstXml, 'foo', true);
+            var testElement = dstXml.find('foo:bar');
+            expect(testElement).toBeDefined();
+            expect(testElement.attrib.testAttrib).toEqual('value');
+            expect(testElement.text).toEqual('testText');
+        });
+
+        it('should not append duplicate non singelton children', function () {
+            var testXml = et.XML('<widget><preference name="fullscreen" value="true"/></widget>');
+
+            xml_helpers.mergeXml(testXml, dstXml, '', true);
+            var testElements = dstXml.findall('preference');
+            expect(testElements.length).toEqual(2);
+        });
+
+        it('should not skip partial duplicate non singelton children', function () {
+            //remove access tags from dstXML
+            var testElements = dstXml.findall('access');
+            for(var i = 0; i < testElements.length; i++) {
+                dstXml.remove(testElements[i]);
+            }
+            testElements = dstXml.findall('access');
+            expect(testElements.length).toEqual(0);
+            //add an external whitelist access tag
+            var testXml = et.XML('<widget><access origin="*" launch-external="yes"/></widget>');
+            xml_helpers.mergeXml(testXml, dstXml, '', true);
+            testElements = dstXml.findall('access');
+            expect(testElements.length).toEqual(1);
+            //add an internal whitelist access tag
+            testXml = et.XML('<widget><access origin="*"/></widget>');
+            xml_helpers.mergeXml(testXml, dstXml, '', true);
+            testElements = dstXml.findall('access');
+            expect(testElements.length).toEqual(2);
+
+        });
+    });
 });


---------------------------------------------------------------------
To unsubscribe, e-mail: commits-unsubscribe@cordova.apache.org
For additional commands, e-mail: commits-help@cordova.apache.org