You are viewing a plain text version of this content. The canonical link for it is here.
Posted to commits@cordova.apache.org by ka...@apache.org on 2014/05/02 20:33:30 UTC

[11/24] Split out cordova-lib: move cordova-cli files

http://git-wip-us.apache.org/repos/asf/cordova-cli/blob/b51e1c12/cordova-lib/spec-cordova/hooker.spec.js
----------------------------------------------------------------------
diff --git a/cordova-lib/spec-cordova/hooker.spec.js b/cordova-lib/spec-cordova/hooker.spec.js
new file mode 100644
index 0000000..c85cc59
--- /dev/null
+++ b/cordova-lib/spec-cordova/hooker.spec.js
@@ -0,0 +1,261 @@
+ /**
+    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 cordova = require('../cordova'),
+    hooker = require('../src/hooker'),
+    shell  = require('shelljs'),
+    path   = require('path'),
+    fs     = require('fs'),
+    os     = require('os'),
+    Q      = require('q'),
+    child_process = require('child_process'),
+    helpers = require('./helpers');
+
+var platform = os.platform();
+var tmpDir = helpers.tmpDir('hooks_test');
+var project = path.join(tmpDir, 'project');
+var dotCordova = path.join(project, '.cordova');
+var hooksDir = path.join(project, '.cordova', 'hooks');
+var ext = platform.match(/(win32|win64)/)?'bat':'sh';
+
+
+// copy fixture
+shell.rm('-rf', project);
+shell.mkdir('-p', project);
+shell.cp('-R', path.join(__dirname, 'fixtures', 'base', '*'), project);
+shell.mkdir('-p', dotCordova);
+shell.cp('-R', path.join(__dirname, 'fixtures', 'hooks_' + ext), dotCordova);
+shell.mv(path.join(dotCordova, 'hooks_' + ext), hooksDir);
+shell.chmod('-R', 'ug+x', hooksDir);
+
+
+describe('hooker', function() {
+    it('should throw if provided directory is not a cordova project', function() {
+        expect(function() {
+            new hooker(tmpDir);
+        }).toThrow();
+    });
+});
+
+describe('global (static) fire method', function() {
+    it('should execute listeners serially', function(done) {
+        var test_event = 'foo';
+        var h1_fired = false;
+        var h1 = function() {
+            expect(h2_fired).toBe(false);
+            // Delay 100 ms here to check that h2 is not executed until after
+            // the promise returned by h1 is resolved.
+            var q = Q.delay(100).then(function() {
+                h1_fired = true;
+            });
+            return q;
+        };
+        var h2_fired = false;
+        var h2 = function() {
+            h2_fired = true;
+            expect(h1_fired).toBe(true);
+            return Q();
+        };
+
+        cordova.on(test_event, h1);
+        cordova.on(test_event, h2);
+        hooker.fire(test_event).then(function() {
+            expect(h1_fired).toBe(true);
+            expect(h2_fired).toBe(true);
+            done();
+        });
+    });
+});
+
+describe('module-level hooks', function() {
+    var handler = jasmine.createSpy().andReturn(Q());
+    var test_event = 'before_build';
+    var h;
+
+    beforeEach(function() {
+        h = new hooker(project);
+    });
+
+    afterEach(function() {
+        cordova.removeAllListeners(test_event);
+        handler.reset();
+    });
+
+    it('should fire handlers using cordova.on', function(done) {
+        cordova.on(test_event, handler);
+        h.fire(test_event)
+        .then(function() {
+            expect(handler).toHaveBeenCalled();
+        })
+        .fail(function(err) {
+            expect(err).not.toBeDefined();
+        })
+        .fin(done);
+    });
+
+    it('should pass the project root folder as parameter into the module-level handlers', function(done) {
+        cordova.on(test_event, handler);
+        h.fire(test_event)
+        .then(function() {
+            expect(handler).toHaveBeenCalledWith({root:project});
+        })
+        .fail(function(err) {
+            console.log(err);
+            expect(err).not.toBeDefined();
+        })
+        .fin(done);
+    });
+
+    it('should be able to stop listening to events using cordova.off', function(done) {
+        cordova.on(test_event, handler);
+        cordova.off(test_event, handler);
+        h.fire(test_event)
+        .then(function() {
+            expect(handler).not.toHaveBeenCalled();
+        })
+        .fail(function(err) {
+            console.log(err);
+            expect(err).toBeUndefined();
+        })
+        .fin(done);
+    });
+
+    it('should allow for hook to opt into asynchronous execution and block further hooks from firing using the done callback', function(done) {
+        var h1_fired = false;
+        var h1 = function() {
+            h1_fired = true;
+            expect(h2_fired).toBe(false);
+            return Q();
+        };
+        var h2_fired = false;
+        var h2 = function() {
+            h2_fired = true;
+            expect(h1_fired).toBe(true);
+            return Q();
+        };
+
+        cordova.on(test_event, h1);
+        cordova.on(test_event, h2);
+        h.fire(test_event).then(function() {
+            expect(h1_fired).toBe(true);
+            expect(h2_fired).toBe(true);
+            done();
+        });
+    });
+
+    it('should pass data object that fire calls into async handlers', function(done) {
+        var data = {
+            "hi":"ho",
+            "offtowork":"wego"
+        };
+        var async = function(opts) {
+            data.root = tmpDir;
+            expect(opts).toEqual(data);
+            return Q();
+        };
+        cordova.on(test_event, async);
+        h.fire(test_event, data).then(done);
+    });
+
+    it('should pass data object that fire calls into sync handlers', function(done) {
+        var data = {
+            "hi":"ho",
+            "offtowork":"wego"
+        };
+        var async = function(opts) {
+            data.root = tmpDir;
+            expect(opts).toEqual(data);
+        };
+        cordova.on(test_event, async);
+        h.fire(test_event, data).then(done);
+    });
+});
+
+
+describe('hooks', function() {
+    var h;
+    beforeEach(function() {
+        h = new hooker(project);
+    });
+
+
+    it('should not error if the hook is unrecognized', function(done) {
+        h.fire('CLEAN YOUR SHORTS GODDAMNIT LIKE A BIG BOY!')
+        .fail(function (err) {
+            expect('Call with unrecogized hook ').toBe('successful.\n' + err);
+        })
+        .fin(done);
+    });
+
+    it('should error if any script exits with non-zero code', function(done) {
+        h.fire('fail').then(function() {
+            expect('the call').toBe('a failure');
+        }, function(err) {
+            expect(err).toBeDefined();
+        })
+        .fin(done);
+    });
+
+    it('should execute all scripts in order', function(done) {
+        h.fire('test')
+        .then(function() {
+            var hooksOrderFile = path.join(project, 'hooks_order.txt');
+            var hooksEnvFile = path.join(project, 'hooks_env.json');
+            var hooksParamsFile = path.join(project, 'hooks_params.txt');
+            expect(hooksOrderFile).toExist();
+            expect(hooksEnvFile).toExist();
+            expect(hooksParamsFile).toExist();
+            expect(path.join(project, 'dotted_hook_should_not_fire.txt')).not.toExist();
+
+            var order = fs.readFileSync(hooksOrderFile, 'ascii').replace(/\W/gm, '');
+            expect(order).toBe('ab');
+
+            var params = fs.readFileSync(hooksParamsFile, 'ascii').trim().trim('"');
+            expect(params).toMatch(project.replace(/\\/g, '\\\\'));
+
+            var env = JSON.parse(fs.readFileSync(hooksEnvFile, 'ascii'));
+            expect(env.CORDOVA_VERSION).toEqual(require('../package').version);
+        })
+        .fail(function(err) {
+            console.log(err);
+            expect('Test hook call').toBe('successful');
+        })
+        .fin(done);
+
+    });
+
+    // Cleanup. Must be the last spec. Is there a better place for final cleanup in Jasmine?
+    it('should not fail during cleanup', function() {
+        process.chdir(path.join(__dirname, '..'));  // Non e2e tests assume CWD is repo root.
+        if(ext == 'sh') {
+            //shell.rm('-rf', tmpDir);
+        } else { // Windows:
+            // For some mysterious reason, both shell.rm and RMDIR /S /Q won't
+            // delete the dir on Windows, but they do remove the files leaving
+            // only folders. But the dir is removed just fine by
+            // shell.rm('-rf', tmpDir) at the top of this file with the next
+            // invocation of this test. The benefit of RMDIR /S /Q is that it
+            // doesn't print warnings like shell.rmdir() that look like this:
+            // rm: could not remove directory (code ENOTEMPTY): C:\Users\...
+            var cmd =  'RMDIR /S /Q ' + tmpDir;
+            child_process.exec(cmd);
+        }
+    });
+});

http://git-wip-us.apache.org/repos/asf/cordova-cli/blob/b51e1c12/cordova-lib/spec-cordova/lazy_load.spec.js
----------------------------------------------------------------------
diff --git a/cordova-lib/spec-cordova/lazy_load.spec.js b/cordova-lib/spec-cordova/lazy_load.spec.js
new file mode 100644
index 0000000..1a92ee9
--- /dev/null
+++ b/cordova-lib/spec-cordova/lazy_load.spec.js
@@ -0,0 +1,200 @@
+/**
+    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 lazy_load = require('../src/lazy_load'),
+    config = require('../src/config'),
+    util = require('../src/util'),
+    shell = require('shelljs'),
+    npmconf = require('npmconf');
+    path = require('path'),
+    hooker = require('../src/hooker'),
+    request = require('request'),
+    fs = require('fs'),
+    Q = require('q'),
+    platforms = require('../platforms');
+
+describe('lazy_load module', function() {
+    var custom_path;
+    beforeEach(function() {
+        custom_path = spyOn(config, 'has_custom_path').andReturn(false);
+    });
+    describe('cordova method (loads stock cordova libs)', function() {
+        var custom;
+        beforeEach(function() {
+            custom = spyOn(lazy_load, 'custom').andReturn(Q(path.join('lib','dir')));
+        });
+        it('should throw if platform is not a stock cordova platform', function(done) {
+            lazy_load.cordova('atari').then(function() {
+                expect('this call').toEqual('to fail');
+            }, function(err) {
+                expect('' + err).toContain('Cordova library "atari" not recognized.');
+            }).fin(done);
+        });
+        it('should invoke lazy_load.custom with appropriate url, platform, and version as specified in platforms manifest', function(done) {
+            lazy_load.cordova('android').then(function(dir) {
+                expect(custom).toHaveBeenCalledWith(platforms.android.url + ';a=snapshot;h=' + platforms.android.version + ';sf=tgz', 'cordova', 'android', platforms.android.version);
+                expect(dir).toBeDefined();
+                done();
+            });
+        });
+    });
+
+    describe('custom method (loads custom cordova libs)', function() {
+        var exists, fire, rm;
+        beforeEach(function() {
+            spyOn(shell, 'mkdir');
+            rm = spyOn(shell, 'rm');
+            mv = spyOn(shell, 'mv');
+            exists = spyOn(fs, 'existsSync').andReturn(false);
+            readdir = spyOn(fs, 'readdirSync').andReturn(['somefile.txt']);
+            fire = spyOn(hooker, 'fire').andReturn(Q());
+        });
+
+        it('should callback with no errors and not fire event hooks if library already exists', function(done) {
+            exists.andReturn(true);
+            lazy_load.custom('http://some remote url', 'some id', 'platform X', 'three point five').then(function() {
+                expect(fire).not.toHaveBeenCalled()
+            }, function(err) {
+                expect(err).not.toBeDefined();
+            }).fin(done);
+        });
+        it('should callback with no errors and fire event hooks even if library already exists if the lib url is a local dir', function(done) {
+            exists.andReturn(true);
+            lazy_load.custom('some local dir', 'some id', 'platform X', 'three point six').then(function() {
+                expect(fire).not.toHaveBeenCalled()
+            }, function(err) {
+                expect(err).not.toBeDefined();
+            }).fin(done);
+        });
+
+        describe('remote URLs for libraries', function() {
+            var npmConfProxy;
+            var req,
+                load_spy,
+                events = {},
+                fakeRequest = {
+                    on: jasmine.createSpy().andCallFake(function(event, cb) {
+                        events[event] = cb;
+                        return fakeRequest;
+                    }),
+                    pipe: jasmine.createSpy().andCallFake(function() { return fakeRequest; })
+                };
+            beforeEach(function() {
+                npmConfProxy = null;
+                events = {};
+                fakeRequest.on.reset();
+                fakeRequest.pipe.reset();
+                req = spyOn(request, 'get').andCallFake(function() {
+                    // Fire the 'end' event shortly.
+                    setTimeout(function() {
+                        events['end']();
+                    }, 10);
+                    return fakeRequest;
+                });
+                load_spy = spyOn(npmconf, 'load').andCallFake(function(cb) { cb(null, { get: function() { return npmConfProxy }}); });
+            });
+
+            it('should call request with appopriate url params', function(done) {
+                var url = 'https://github.com/apache/someplugin';
+                lazy_load.custom(url, 'random', 'android', '1.0').then(function() {
+                    expect(req).toHaveBeenCalledWith({
+                        uri:url
+                    }, jasmine.any(Function));
+                }, function(err) {
+                    expect(err).not.toBeDefined();
+                }).fin(done);
+            });
+            it('should take into account https-proxy npm configuration var if exists for https:// calls', function(done) {
+                var proxy = 'https://somelocalproxy.com';
+                npmConfProxy = proxy;
+                var url = 'https://github.com/apache/someplugin';
+                lazy_load.custom(url, 'random', 'android', '1.0').then(function() {
+                    expect(req).toHaveBeenCalledWith({
+                        uri:url,
+                        proxy:proxy
+                    }, jasmine.any(Function));
+                }, function(err) {
+                    expect(err).not.toBeDefined();
+                }).fin(done);
+            });
+            it('should take into account proxy npm config var if exists for http:// calls', function(done) {
+                var proxy = 'http://somelocalproxy.com';
+                npmConfProxy = proxy;
+                var url = 'http://github.com/apache/someplugin';
+                lazy_load.custom(url, 'random', 'android', '1.0').then(function() {
+                    expect(req).toHaveBeenCalledWith({
+                        uri:url,
+                        proxy:proxy
+                    }, jasmine.any(Function));
+                }, function(err) {
+                    expect(err).not.toBeDefined();
+                }).fin(done);
+            });
+        });
+
+        describe('local paths for libraries', function() {
+            it('should return the local path, no symlink', function(done) {
+                lazy_load.custom('/some/random/lib', 'id', 'X', 'three point eight').then(function(dir) {
+                    expect(dir).toEqual('/some/random/lib');
+                }, function(err) {
+                    expect(err).toBeUndefined();
+                }).fin(done);
+            });
+            it('should not file download hook', function(done) {
+                lazy_load.custom('/some/random/lib', 'id', 'X', 'three point nine').then(function() {
+                    expect(fire).not.toHaveBeenCalledWith('after_library_download', {platform:'X',url:'/some/random/lib',id:'id',version:'three point nine',path:'/some/random/lib', symlink:false});
+                }, function(err) {
+                    expect(err).toBeUndefined();
+                }).fin(done);
+            });
+        });
+    });
+
+    describe('based_on_config method', function() {
+        var cordova, custom;
+        beforeEach(function() {
+            cordova = spyOn(lazy_load, 'cordova').andReturn(Q());
+            custom = spyOn(lazy_load, 'custom').andReturn(Q());
+        });
+        it('should invoke custom if a custom lib is specified', function(done) {
+            var read = spyOn(config, 'read').andReturn({
+                lib:{
+                    maybe:{
+                        uri:'you or eye?',
+                        id:'eye dee',
+                        version:'four point twenty'
+                    }
+                }
+            });
+            var p = '/some/random/custom/path';
+            custom_path.andReturn(p);
+            lazy_load.based_on_config('yup', 'maybe').then(function() {
+                expect(custom).toHaveBeenCalledWith('you or eye?', 'eye dee', 'maybe', 'four point twenty');
+            }, function(err) {
+                expect(err).toBeUndefined();
+            }).fin(done);
+        });
+        it('should invoke cordova if no custom lib is specified', function(done) {
+            lazy_load.based_on_config('yup', 'ios').then(function() {
+                expect(cordova).toHaveBeenCalledWith('ios');
+            }, function(err) {
+                expect(err).toBeUndefined();
+            }).fin(done);
+        });
+    });
+});

http://git-wip-us.apache.org/repos/asf/cordova-cli/blob/b51e1c12/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
new file mode 100644
index 0000000..6c6ea41
--- /dev/null
+++ b/cordova-lib/spec-cordova/metadata/android_parser.spec.js
@@ -0,0 +1,211 @@
+/**
+    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 platforms = require('../../platforms'),
+    util = require('../../src/util'),
+    path = require('path'),
+    shell = require('shelljs'),
+    fs = require('fs'),
+    et = require('elementtree'),
+    xmlHelpers = require('../../src/xml-helpers'),
+    Q = require('q'),
+    config = require('../../src/config'),
+    ConfigParser = require('../../src/ConfigParser'),
+    cordova = require('../../cordova');
+
+// Create a real config object before mocking out everything.
+var cfg = new ConfigParser(path.join(__dirname, '..', 'test-config.xml'));
+
+var STRINGS_XML = '<resources> <string name="app_name">mobilespec</string> </resources>';
+var MANIFEST_XML = '<manifest android:versionCode="1" android:versionName="0.0.1" package="org.apache.mobilespec">\n' +
+    '<application android:hardwareAccelerated="true" android:icon="@drawable/icon" android:label="@string/app_name">\n' +
+    '    <activity android:configChanges="orientation|keyboardHidden|keyboard|screenSize|locale" android:label="@string/app_name" android:name="mobilespec" android:screenOrientation="VAL">\n' +
+    '        <intent-filter>\n' +
+    '            <action android:name="android.intent.action.MAIN" />\n' +
+    '            <category android:name="android.intent.category.LAUNCHER" />\n' +
+    '        </intent-filter>\n' +
+    '    </activity>\n' +
+    '</application>\n' +
+    '</manifest>\n';
+
+describe('android project parser', function() {
+    var proj = path.join('some', 'path');
+    var exists;
+    beforeEach(function() {
+        exists = spyOn(fs, 'existsSync').andReturn(true);
+        spyOn(config, 'has_custom_path').andReturn(false);
+    });
+
+    function wrapper(p, done, post) {
+        p.then(post, function(err) {
+            expect(err).toBeUndefined();
+        }).fin(done);
+    }
+
+    function errorWrapper(p, done, post) {
+        p.then(function() {
+            expect('this call').toBe('fail');
+        }, post).fin(done);
+    }
+
+    describe('constructions', function() {
+        it('should throw if provided directory does not contain an AndroidManifest.xml', function() {
+            exists.andReturn(false);
+            expect(function() {
+                new platforms.android.parser(proj);
+            }).toThrow();
+        });
+        it('should create an instance with path, strings, manifest and android_config properties', function() {
+            expect(function() {
+                var p = new platforms.android.parser(proj);
+                expect(p.path).toEqual(proj);
+                expect(p.strings).toEqual(path.join(proj, 'res', 'values', 'strings.xml'));
+                expect(p.manifest).toEqual(path.join(proj, 'AndroidManifest.xml'));
+                expect(p.android_config).toEqual(path.join(proj, 'res', 'xml', 'config.xml'));
+            }).not.toThrow();
+        });
+    });
+
+    describe('instance', function() {
+        var p, cp, rm, mkdir, is_cordova, write, read;
+        var android_proj = path.join(proj, 'platforms', 'android');
+        var stringsRoot;
+        var manifestRoot;
+        beforeEach(function() {
+            stringsRoot = null;
+            manifestRoot = null;
+            p = new platforms.android.parser(android_proj);
+            cp = spyOn(shell, 'cp');
+            rm = spyOn(shell, 'rm');
+            is_cordova = spyOn(util, 'isCordova').andReturn(proj);
+            write = spyOn(fs, 'writeFileSync');
+            read = spyOn(fs, 'readFileSync');
+            mkdir = spyOn(shell, 'mkdir');
+            spyOn(xmlHelpers, 'parseElementtreeSync').andCallFake(function(path) {
+                if (/strings/.exec(path)) {
+                    return stringsRoot = new et.ElementTree(et.XML(STRINGS_XML));
+                } else if (/AndroidManifest/.exec(path)) {
+                    return manifestRoot = new et.ElementTree(et.XML(MANIFEST_XML));
+                }
+            });
+        });
+
+        describe('update_from_config method', function() {
+            beforeEach(function() {
+                spyOn(fs, 'readdirSync').andReturn([path.join(proj, 'src', 'android_pkg', 'MyApp.java')]);
+                cfg.name = function() { return 'testname' };
+                cfg.packageName = function() { return 'testpkg' };
+                cfg.version = function() { return 'one point oh' };
+                read.andReturn('package org.cordova.somepackage; public class MyApp extends CordovaActivity { }');
+            });
+
+            it('should handle no orientation', function() {
+                cfg.getPreference = function() { return null; };
+                p.update_from_config(cfg);
+                expect(manifestRoot.getroot().find('./application/activity').attrib['android:screenOrientation']).toEqual('VAL');
+            });
+            it('should handle default orientation', function() {
+                cfg.getPreference = function() { return 'default'; };
+                p.update_from_config(cfg);
+                expect(manifestRoot.getroot().find('./application/activity').attrib['android:screenOrientation']).toBeUndefined();
+            });
+            it('should handle portrait orientation', function() {
+                cfg.getPreference = function() { return 'portrait'; };
+                p.update_from_config(cfg);
+                expect(manifestRoot.getroot().find('./application/activity').attrib['android:screenOrientation']).toEqual('portrait');
+            });
+            it('should handle invalid orientation', function() {
+                cfg.getPreference = function() { return 'prtrait'; };
+                p.update_from_config(cfg);
+                expect(manifestRoot.getroot().find('./application/activity').attrib['android:screenOrientation']).toEqual('VAL');
+            });
+            it('should write out the app name to strings.xml', function() {
+                p.update_from_config(cfg);
+                expect(stringsRoot.getroot().find('string').text).toEqual('testname');
+            });
+            it('should write out the app id to androidmanifest.xml and update the cordova-android entry Java class', function() {
+                p.update_from_config(cfg);
+                expect(manifestRoot.getroot().attrib.package).toEqual('testpkg');
+            });
+            it('should write out the app version to androidmanifest.xml', function() {
+                p.update_from_config(cfg);
+                expect(manifestRoot.getroot().attrib['android:versionName']).toEqual('one point oh');
+            });
+        });
+        describe('www_dir method', function() {
+            it('should return assets/www', function() {
+                expect(p.www_dir()).toEqual(path.join(android_proj, 'assets', 'www'));
+            });
+        });
+        describe('config_xml method', function() {
+            it('should return the location of the config.xml', function() {
+                expect(p.config_xml()).toEqual(p.android_config);
+            });
+        });
+        describe('update_www method', function() {
+            it('should rm project-level www and cp in platform agnostic www', function() {
+                p.update_www();
+                expect(rm).toHaveBeenCalled();
+                expect(cp).toHaveBeenCalled();
+            });
+        });
+        describe('update_overrides method', function() {
+            it('should do nothing if merges directory does not exist', function() {
+                exists.andReturn(false);
+                p.update_overrides();
+                expect(cp).not.toHaveBeenCalled();
+            });
+            it('should copy merges path into www', function() {
+                p.update_overrides();
+                expect(cp).toHaveBeenCalled();
+            });
+        });
+        describe('update_project method', function() {
+            var config, www, overrides, svn;
+            beforeEach(function() {
+                config = spyOn(p, 'update_from_config');
+                www = spyOn(p, 'update_www');
+                overrides = spyOn(p, 'update_overrides');
+                svn = spyOn(util, 'deleteSvnFolders');
+            });
+            it('should call update_from_config', function() {
+                p.update_project();
+                expect(config).toHaveBeenCalled();
+            });
+            it('should throw if update_from_config throws', function(done) {
+                var err = new Error('uh oh!');
+                config.andCallFake(function() { throw err; });
+                errorWrapper(p.update_project({}), done, function(err) {
+                    expect(err).toEqual(err);
+                });
+            });
+            it('should not call update_www', function() {
+                p.update_project();
+                expect(www).not.toHaveBeenCalled();
+            });
+            it('should call update_overrides', function() {
+                p.update_project();
+                expect(overrides).toHaveBeenCalled();
+            });
+            it('should call deleteSvnFolders', function() {
+                p.update_project();
+                expect(svn).toHaveBeenCalled();
+            });
+        });
+    });
+});

http://git-wip-us.apache.org/repos/asf/cordova-cli/blob/b51e1c12/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
new file mode 100644
index 0000000..b1d0b47
--- /dev/null
+++ b/cordova-lib/spec-cordova/metadata/blackberry_parser.spec.js
@@ -0,0 +1,225 @@
+/**
+    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 platforms = require('../../platforms'),
+    util = require('../../src/util'),
+    path = require('path'),
+    shell = require('shelljs'),
+    fs = require('fs'),
+    et = require('elementtree'),
+    xmlHelpers = require('../../src/xml-helpers'),
+    Q = require('q'),
+    child_process = require('child_process'),
+    config = require('../../src/config'),
+    ConfigParser = require('../../src/ConfigParser'),
+    cordova = require('../../cordova');
+
+var cfg = new ConfigParser(path.join(__dirname, '..', 'test-config.xml'));
+
+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('blackberry10 project parser', function() {
+    var proj = '/some/path';
+    var exists, custom, sh;
+    beforeEach(function() {
+        exists = spyOn(fs, 'existsSync').andReturn(true);
+        custom = spyOn(config, 'has_custom_path').andReturn(false);
+        sh = spyOn(child_process, 'exec').andCallFake(function(cmd, opts, cb) {
+            (cb || opts)(0, '', '');
+        });
+        spyOn(ConfigParser.prototype, 'write');
+        spyOn(xmlHelpers, 'parseElementtreeSync').andCallFake(function() {
+            return new et.ElementTree(et.XML(TEST_XML));
+        });
+    });
+
+    function wrapper(p, done, post) {
+        p.then(post, function(err) {
+            expect(err).toBeUndefined();
+        }).fin(done);
+    }
+
+    function errorWrapper(p, done, post) {
+        p.then(function() {
+            expect('this call').toBe('fail');
+        }, post).fin(done);
+    }
+
+    describe('constructions', 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);
+            }).toThrow();
+        });
+        it('should accept a proper native blackberry project path as construction parameter', function() {
+            var project;
+            expect(function() {
+                project = new platforms.blackberry10.parser(proj);
+            }).not.toThrow();
+            expect(project).toBeDefined();
+        });
+    });
+
+    describe('check_requirements', function() {
+        it('should fire a callback if the blackberry-deploy shell-out fails', function(done) {
+            sh.andCallFake(function(cmd, opts, cb) {
+                (cb || opts)(1, 'no bb-deploy dewd!');
+            });
+            errorWrapper(platforms.blackberry10.parser.check_requirements(proj), done, function(err) {
+                expect(err).toContain('no bb-deploy dewd');
+            });
+        });
+        it('should fire a callback with no error if shell out is successful', function(done) {
+            wrapper(platforms.blackberry10.parser.check_requirements(proj), done, function() {
+                expect(1).toBe(1);
+            });
+        });
+    });
+    describe('instance', 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);
+            cp = spyOn(shell, 'cp');
+            rm = spyOn(shell, 'rm');
+            mkdir = spyOn(shell, 'mkdir');
+            is_cordova = spyOn(util, 'isCordova').andReturn(proj);
+            write = spyOn(fs, 'writeFileSync');
+            read = spyOn(fs, 'readFileSync');
+        });
+
+        describe('update_from_config method', function() {
+            var xml_name, xml_pkg, xml_version, xml_access_rm, xml_update, xml_append, xml_content;
+            beforeEach(function() {
+                xml_content = jasmine.createSpy('xml content');
+                xml_name = jasmine.createSpy('xml name');
+                xml_pkg = jasmine.createSpy('xml pkg');
+                xml_version = jasmine.createSpy('xml version');
+                xml_access_rm = jasmine.createSpy('xml access rm');
+                xml_access_add = jasmine.createSpy('xml access add');
+                xml_update = jasmine.createSpy('xml update');
+                xml_append = jasmine.createSpy('xml append');
+                xml_preference_remove = jasmine.createSpy('xml preference rm');
+                xml_preference_add = jasmine.createSpy('xml preference add');
+                p.xml.name = xml_name;
+                p.xml.packageName = xml_pkg;
+                p.xml.version = xml_version;
+                p.xml.content = xml_content;
+                p.xml.access = {
+                    remove:xml_access_rm,
+                    add: xml_access_add
+                };
+                p.xml.update = xml_update;
+                p.xml.doc = {
+                    getroot:function() { return { append:xml_append}; }
+                };
+                p.xml.preference = {
+                    add: xml_preference_add,
+                    remove: xml_preference_remove
+                };
+                cfg.name = function() { return 'testname'; };
+                cfg.packageName = function() { return 'testpkg'; };
+                cfg.version = function() { return 'one point oh'; };
+            });
+        });
+        describe('www_dir method', function() {
+            it('should return /www', function() {
+                expect(p.www_dir()).toEqual(path.join(bb_proj, 'www'));
+            });
+        });
+        describe('config_xml method', function() {
+            it('should return the location of the config.xml', function() {
+                expect(p.config_xml()).toEqual(path.join(proj, 'platforms', 'blackberry10', 'www', 'config.xml'));
+            });
+        });
+        describe('update_www method', function() {
+
+            it('should rm project-level www and cp in platform agnostic www', function() {
+                p.update_www();
+                expect(rm).toHaveBeenCalled();
+                expect(cp).toHaveBeenCalled();
+            });
+        });
+        describe('update_overrides method', function() {
+            it('should do nothing if merges directory does not exist', function() {
+                exists.andReturn(false);
+                p.update_overrides();
+                expect(cp).not.toHaveBeenCalled();
+            });
+            it('should copy merges path into www', function() {
+                p.update_overrides();
+                expect(cp).toHaveBeenCalledWith('-rf', path.join(proj, 'merges', 'blackberry10', '*'), path.join(proj, 'platforms', 'blackberry10', 'www'));
+            });
+        });
+        describe('update_project method', function() {
+            var config, www, overrides, svn, parse, get_env, write_env;
+            beforeEach(function() {
+                config = spyOn(p, 'update_from_config');
+                www = spyOn(p, 'update_www');
+                overrides = spyOn(p, 'update_overrides');
+                svn = spyOn(util, 'deleteSvnFolders');
+                parse = spyOn(JSON, 'parse').andReturn({blackberry:{qnx:{}}});
+            });
+            it('should call update_from_config', function(done) {
+                wrapper(p.update_project(), done, function() {
+                    expect(config).toHaveBeenCalled();
+                });
+            });
+            it('should throw if update_from_config throws', function(done) {
+                var err = new Error('uh oh!');
+                config.andCallFake(function() { throw err; });
+                errorWrapper(p.update_project({}), done, function(e) {
+                    expect(e).toEqual(err);
+                });
+            });
+            it('should not call update_www', function(done) {
+                wrapper(p.update_project(), done, function() {
+                    expect(www).not.toHaveBeenCalled();
+                });
+            });
+            it('should call update_overrides', function(done) {
+                wrapper(p.update_project(), done, function() {
+                    expect(overrides).toHaveBeenCalled();
+                });
+            });
+            it('should call deleteSvnFolders', function(done) {
+                wrapper(p.update_project(), done, function() {
+                    expect(svn).toHaveBeenCalled();
+                });
+            });
+        });
+    });
+});

http://git-wip-us.apache.org/repos/asf/cordova-cli/blob/b51e1c12/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
new file mode 100644
index 0000000..49694eb
--- /dev/null
+++ b/cordova-lib/spec-cordova/metadata/firefoxos_parser.spec.js
@@ -0,0 +1,74 @@
+/**
+    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 platforms = require('../../platforms'),
+    util = require('../../src/util'),
+    path = require('path'),
+    shell = require('shelljs'),
+    fs = require('fs'),
+    config = require('../../src/config'),
+    ConfigParser = require('../../src/ConfigParser'),
+    cordova = require('../../cordova');
+
+var cfg = new ConfigParser(path.join(__dirname, '..', 'test-config.xml'));
+describe('firefoxos project parser', function() {
+    var proj = path.join('some', 'path');
+    var exists, exec, custom;
+    beforeEach(function() {
+        exists = spyOn(fs, 'existsSync').andReturn(true);
+        exec = spyOn(shell, 'exec').andCallFake(function(cmd, opts, cb) {
+            cb(0, '');
+        });
+        custom = spyOn(config, 'has_custom_path').andReturn(false);
+    });
+
+    describe('constructions', function() {
+        it('should create an instance with a path', function() {
+            expect(function() {
+                var p = new platforms.android.parser(proj);
+                expect(p.path).toEqual(proj);
+            }).not.toThrow();
+        });
+    });
+
+    describe('instance', function() {
+        var p, cp, rm, is_cordova, write, read;
+        var ff_proj = path.join(proj, 'platforms', 'firefoxos');
+        beforeEach(function() {
+            p = new platforms.firefoxos.parser(ff_proj);
+            cp = spyOn(shell, 'cp');
+            rm = spyOn(shell, 'rm');
+            is_cordova = spyOn(util, 'isCordova').andReturn(proj);
+            write = spyOn(fs, 'writeFileSync');
+            read = spyOn(fs, 'readFileSync').andReturn('');
+        });
+
+        describe('update_from_config method', function() {
+            beforeEach(function() {
+                cfg.name = function() { return 'testname'; };
+                cfg.packageName = function() { return 'testpkg'; };
+                cfg.version = function() { return '1.0'; };
+            });
+
+          /*  it('should write manifest.webapp', function() {
+                //p.update_from_config(cfg);
+                //expect(write.mostRecentCall.args[0]).toEqual('manifest.webapp');
+            });*/
+        });
+    });
+});

http://git-wip-us.apache.org/repos/asf/cordova-cli/blob/b51e1c12/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
new file mode 100644
index 0000000..5b2977e
--- /dev/null
+++ b/cordova-lib/spec-cordova/metadata/ios_parser.spec.js
@@ -0,0 +1,199 @@
+/**
+ 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 platforms = require('../../platforms'),
+    util = require('../../src/util'),
+    path = require('path'),
+    shell = require('shelljs'),
+    plist = require('plist-with-patches'),
+    xcode = require('xcode'),
+    et = require('elementtree'),
+    fs = require('fs'),
+    Q = require('q'),
+    config = require('../../src/config'),
+    ConfigParser = require('../../src/ConfigParser'),
+    cordova = require('../../cordova');
+
+// Create a real config object before mocking out everything.
+var cfg = new ConfigParser(path.join(__dirname, '..', 'test-config.xml'));
+
+describe('ios project parser', function () {
+    var proj = path.join('some', 'path');
+    var custom, readdir;
+    beforeEach(function() {
+        custom = spyOn(config, 'has_custom_path').andReturn(false);
+        readdir = spyOn(fs, 'readdirSync').andReturn(['test.xcodeproj']);
+    });
+
+    function wrapper(p, done, post) {
+        p.then(post, function(err) {
+            expect(err).toBeUndefined();
+        }).fin(done);
+    }
+
+    function errorWrapper(p, done, post) {
+        p.then(function() {
+            expect('this call').toBe('fail');
+        }, post).fin(done);
+    }
+
+    describe('constructions', function() {
+        it('should throw if provided directory does not contain an xcodeproj file', function() {
+            readdir.andReturn(['noxcodehere']);
+            expect(function() {
+                new platforms.ios.parser(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);
+                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();
+        });
+    });
+
+    describe('instance', function() {
+        var p, cp, rm, mkdir, is_cordova, write, read;
+        var ios_proj = path.join(proj, 'platforms', 'ios');
+        beforeEach(function() {
+            p = new platforms.ios.parser(ios_proj);
+            cp = spyOn(shell, 'cp');
+            rm = spyOn(shell, 'rm');
+            mkdir = spyOn(shell, 'mkdir');
+            is_cordova = spyOn(util, 'isCordova').andReturn(proj);
+            write = spyOn(fs, 'writeFileSync');
+            read = spyOn(fs, 'readFileSync').andReturn('');
+        });
+
+        describe('update_from_config method', function() {
+            var mv;
+            var cfg_access_add, cfg_access_rm, cfg_pref_add, cfg_pref_rm, cfg_content;
+            var plist_parse, plist_build, xc;
+            var update_name, xc_write;
+            beforeEach(function() {
+                mv = spyOn(shell, 'mv');
+                plist_parse = spyOn(plist, 'parseFileSync').andReturn({
+                });
+                plist_build = spyOn(plist, 'build').andReturn('');
+                update_name = jasmine.createSpy('update_name');
+                xc_write = jasmine.createSpy('xcode writeSync');
+                xc = spyOn(xcode, 'project').andReturn({
+                    parse:function(cb) {cb();},
+                    updateProductName:update_name,
+                    writeSync:xc_write
+                });
+                cfg.name = function() { return 'testname' };
+                cfg.packageName = function() { return 'testpkg' };
+                cfg.version = function() { return 'one point oh' };
+                p = new platforms.ios.parser(ios_proj);
+            });
+
+            it('should update the app name in pbxproj by calling xcode.updateProductName, and move the ios native files to match the new name', function(done) {
+                var test_path = path.join(proj, 'platforms', 'ios', 'test');
+                var testname_path = path.join(proj, 'platforms', 'ios', 'testname');
+                wrapper(p.update_from_config(cfg), done, function() {
+                    expect(update_name).toHaveBeenCalledWith('testname');
+                    expect(mv).toHaveBeenCalledWith(path.join(test_path, 'test-Info.plist'), path.join(test_path, 'testname-Info.plist'));
+                    expect(mv).toHaveBeenCalledWith(path.join(test_path, 'test-Prefix.pch'), path.join(test_path, 'testname-Prefix.pch'));
+                    expect(mv).toHaveBeenCalledWith(test_path + '.xcodeproj', testname_path + '.xcodeproj');
+                    expect(mv).toHaveBeenCalledWith(test_path, testname_path);
+                });
+            });
+            it('should write out the app id to info plist as CFBundleIdentifier', function(done) {
+                wrapper(p.update_from_config(cfg), done, function() {
+                    expect(plist_build.mostRecentCall.args[0].CFBundleIdentifier).toEqual('testpkg');
+                });
+            });
+            it('should write out the app version to info plist as CFBundleVersion', function(done) {
+                wrapper(p.update_from_config(cfg), done, function() {
+                    expect(plist_build.mostRecentCall.args[0].CFBundleShortVersionString).toEqual('one point oh');
+                });
+            });
+        });
+        describe('www_dir method', function() {
+            it('should return /www', function() {
+                expect(p.www_dir()).toEqual(path.join(ios_proj, 'www'));
+            });
+        });
+        describe('config_xml method', function() {
+            it('should return the location of the config.xml', function() {
+                expect(p.config_xml()).toEqual(path.join(ios_proj, 'test', 'config.xml'));
+            });
+        });
+        describe('update_www method', function() {
+            it('should rm project-level www and cp in platform agnostic www', function() {
+                p.update_www(path.join('lib','dir'));
+                expect(rm).toHaveBeenCalled();
+                expect(cp).toHaveBeenCalled();
+            });
+        });
+        describe('update_overrides method', function() {
+            var exists;
+            beforeEach(function() {
+                exists = spyOn(fs, 'existsSync').andReturn(true);
+            });
+            it('should do nothing if merges directory does not exist', function() {
+                exists.andReturn(false);
+                p.update_overrides();
+                expect(cp).not.toHaveBeenCalled();
+            });
+            it('should copy merges path into www', function() {
+                p.update_overrides();
+                expect(cp).toHaveBeenCalled();
+            });
+        });
+        describe('update_project method', function() {
+            var config, www, overrides, svn;
+            beforeEach(function() {
+                config = spyOn(p, 'update_from_config').andReturn(Q());
+                www = spyOn(p, 'update_www');
+                overrides = spyOn(p, 'update_overrides');
+                svn = spyOn(util, 'deleteSvnFolders');
+            });
+            it('should call update_from_config', function(done) {
+                wrapper(p.update_project(), done, function() {
+                    expect(config).toHaveBeenCalled();
+                });
+            });
+            it('should throw if update_from_config errors', function(done) {
+                var e = new Error('uh oh!');
+                config.andReturn(Q.reject(e));
+                errorWrapper(p.update_project({}), done, function(err) {
+                    expect(err).toEqual(e);
+                });
+            });
+            it('should not call update_www', function(done) {
+                wrapper(p.update_project({}), done, function() {
+                    expect(www).not().toHaveBeenCalled();
+                });
+            });
+            it('should call update_overrides', function(done) {
+                wrapper(p.update_project(), done, function() {
+                    expect(overrides).toHaveBeenCalled();
+                });
+            });
+            it('should call deleteSvnFolders', function(done) {
+                wrapper(p.update_project(), done, function() {
+                    expect(svn).toHaveBeenCalled();
+                });
+            });
+        });
+    });
+});

http://git-wip-us.apache.org/repos/asf/cordova-cli/blob/b51e1c12/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
new file mode 100644
index 0000000..726391f
--- /dev/null
+++ b/cordova-lib/spec-cordova/metadata/windows8_parser.spec.js
@@ -0,0 +1,189 @@
+/**
+    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 platforms = require('../../platforms'),
+    util = require('../../src/util'),
+    path = require('path'),
+    shell = require('shelljs'),
+    child_process = require('child_process'),
+    xmlHelpers = require('../../src/xml-helpers'),
+    et = require('elementtree'),
+    Q = require('q'),
+    fs = require('fs'),
+    config = require('../../src/config'),
+    ConfigParser = require('../../src/ConfigParser'),
+    cordova = require('../../cordova');
+
+// Create a real config object before mocking out everything.
+var cfg = new ConfigParser(path.join(__dirname, '..', 'test-config.xml'));
+
+describe('windows8 project parser', function() {
+
+    var proj = '/some/path';
+    var exists, exec, custom, readdir, cfg_parser;
+    var winXml;
+    beforeEach(function() {
+        exists = spyOn(fs, 'existsSync').andReturn(true);
+        exec = spyOn(child_process, 'exec').andCallFake(function(cmd, opts, cb) {
+            if (!cb) cb = opts;
+            cb(null, '', '');
+        });
+        custom = spyOn(config, 'has_custom_path').andReturn(false);
+        readdir = spyOn(fs, 'readdirSync').andReturn(['test.jsproj']);
+        winXml = null;
+        spyOn(xmlHelpers, 'parseElementtreeSync').andCallFake(function(path) {
+            return winXml = new et.ElementTree(et.XML('<foo><Application/><Identity/><VisualElements><a/></VisualElements><Capabilities><a/></Capabilities></foo>'));
+        });
+    });
+
+    function wrapper(promise, done, post) {
+        promise.then(post, function(err) {
+            expect(err).toBeUndefined();
+        }).fin(done);
+    }
+
+    function errorWrapper(promise, done, post) {
+        promise.then(function() {
+            expect('this call').toBe('fail');
+        }, post).fin(done);
+    }
+
+    describe('constructions', function() {
+        it('should throw if provided directory does not contain a jsproj file', function() {
+            readdir.andReturn([]);
+            expect(function() {
+                new platforms.windows8.parser(proj);
+            }).toThrow();
+        });
+        it('should create an instance with path, manifest properties', function() {
+            expect(function() {
+                var parser = new platforms.windows8.parser(proj);
+                expect(parser.windows8_proj_dir).toEqual(proj);
+                expect(parser.manifest_path).toEqual(path.join(proj, 'package.appxmanifest'));
+            }).not.toThrow();
+        });
+    });
+
+    describe('check_requirements', function() {
+        it('should fire a callback if there is an error during shelling out', function(done) {
+            exec.andCallFake(function(cmd, opts, cb) {
+                if (!cb) cb = opts;
+                cb(50, 'there was an errorz!', '');
+            });
+            errorWrapper(platforms.windows8.parser.check_requirements(proj), done, function(err) {
+                expect(err).toContain('there was an errorz!');
+            });
+        });
+        it('should check by calling check_reqs on the stock lib path if no custom path is defined', function(done) {
+            wrapper(platforms.windows8.parser.check_requirements(proj), done, function() {
+                expect(exec.mostRecentCall.args[0]).toContain(util.libDirectory);
+                expect(exec.mostRecentCall.args[0]).toMatch(/check_reqs"$/);
+            });
+        });
+        it('should check by calling check_reqs on a custom path if it is so defined', function(done) {
+            var custom_path = path.join('some','custom','path','to','windows8','lib');
+            custom.andReturn(custom_path);
+            wrapper(platforms.windows8.parser.check_requirements(proj),done, function() {
+                expect(exec.mostRecentCall.args[0]).toContain(custom_path);
+                expect(exec.mostRecentCall.args[0]).toMatch(/check_reqs"$/);
+            });
+            done();
+        });
+    });
+
+    describe('instance', 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);
+            cp = spyOn(shell, 'cp');
+            rm = spyOn(shell, 'rm');
+            mv = spyOn(shell, 'mv');
+            mkdir = spyOn(shell, 'mkdir');
+            is_cordova = spyOn(util, 'isCordova').andReturn(proj);
+            write = spyOn(fs, 'writeFileSync');
+            read = spyOn(fs, 'readFileSync').andReturn('');
+        });
+
+        describe('update_from_config method', function() {
+            beforeEach(function() {
+                cfg.name = function() { return 'testname' };
+                cfg.content = function() { return 'index.html' };
+                cfg.packageName = function() { return 'testpkg' };
+                cfg.version = function() { return 'one point oh' };
+                readdir.andReturn(['test.sln']);
+            });
+
+            it('should write out the app name to package.appxmanifest', function() {
+                parser.update_from_config(cfg);
+                var identityNode = winXml.getroot().find('.//Identity');
+                expect(identityNode.attrib.Name).toEqual(cfg.packageName());
+            });
+
+            it('should write out the app version to package.appxmanifest', function() {
+                parser.update_from_config(cfg);
+                var identityNode = winXml.getroot().find('.//Identity');
+                expect(identityNode.attrib.Version).toEqual('one point oh');
+            });
+        });
+
+        describe('www_dir method', function() {
+            it('should return www', function() {
+                expect(parser.www_dir()).toEqual(path.join(windows8_proj, 'www'));
+            });
+        });
+        describe('update_www method', function() {
+            var update_jsproj;
+            beforeEach(function() {
+                update_jsproj = spyOn(parser, 'update_jsproj');
+            });
+            it('should rm project-level www and cp in platform agnostic www', function() {
+                parser.update_www(path.join('lib','dir'));
+                expect(rm).toHaveBeenCalled();
+                expect(cp).toHaveBeenCalled();
+            });
+        });
+        describe('update_project method', function() {
+            var config, www, overrides, svn;
+            beforeEach(function() {
+                config = spyOn(parser, 'update_from_config');
+                www = spyOn(parser, 'update_www');
+                www = spyOn(parser, 'update_jsproj');
+                svn = spyOn(util, 'deleteSvnFolders');
+                exists.andReturn(false);
+            });
+            it('should call update_from_config', function() {
+                parser.update_project();
+                expect(config).toHaveBeenCalled();
+            });
+            it('should throw if update_from_config throws', function(done) {
+                var err = new Error('uh oh!');
+                config.andCallFake(function() { throw err; });
+                errorWrapper(parser.update_project({}), done, function(err) {
+                    expect(err).toEqual(err);
+                });
+            });
+            it('should call deleteSvnFolders', function(done) {
+                wrapper(parser.update_project(), done, function() {
+                    expect(svn).toHaveBeenCalled();
+                });
+            });
+        });
+    });
+});

http://git-wip-us.apache.org/repos/asf/cordova-cli/blob/b51e1c12/cordova-lib/spec-cordova/metadata/wp7_parser.spec.js
----------------------------------------------------------------------
diff --git a/cordova-lib/spec-cordova/metadata/wp7_parser.spec.js b/cordova-lib/spec-cordova/metadata/wp7_parser.spec.js
new file mode 100644
index 0000000..0856391
--- /dev/null
+++ b/cordova-lib/spec-cordova/metadata/wp7_parser.spec.js
@@ -0,0 +1,208 @@
+/**
+    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 platforms = require('../../platforms'),
+    util = require('../../src/util'),
+    path = require('path'),
+    shell = require('shelljs'),
+    fs = require('fs'),
+    et = require('elementtree'),
+    xmlHelpers = require('../../src/xml-helpers'),
+    Q = require('q'),
+    child_process = require('child_process'),
+    config = require('../../src/config'),
+    ConfigParser = require('../../src/ConfigParser'),
+    CordovaError = require('../../src/CordovaError'),
+    cordova = require('../../cordova');
+
+// Create a real config object before mocking out everything.
+var cfg = new ConfigParser(path.join(__dirname, '..', 'test-config.xml'));
+
+describe('wp7 project parser', function() {
+    var proj = '/some/path';
+    var exists, exec, custom, readdir, cfg_parser;
+    var projXml, manifestXml;
+    beforeEach(function() {
+        exists = spyOn(fs, 'existsSync').andReturn(true);
+        exec = spyOn(child_process, 'exec').andCallFake(function(cmd, opts, cb) {
+            (cb || opts)(0, '', '');
+        });
+        custom = spyOn(config, 'has_custom_path').andReturn(false);
+        readdir = spyOn(fs, 'readdirSync').andReturn(['test.csproj']);
+        projXml = manifestXml = null;
+        spyOn(xmlHelpers, 'parseElementtreeSync').andCallFake(function(path) {
+            if (/WMAppManifest.xml$/.exec(path)) {
+                return manifestXml = new et.ElementTree(et.XML('<foo><App Title="s"><PrimaryToken /><RootNamespace/><SilverlightAppEntry/><XapFilename/><AssemblyName/></App></foo>'));
+            } else if (/csproj$/.exec(path)) {
+                return projXml = new et.ElementTree(et.XML('<foo><App Title="s"><PrimaryToken /><RootNamespace/><SilverlightAppEntry/><XapFilename/><AssemblyName/></App></foo>'));
+            } else if (/xaml$/.exec(path)) {
+                return new et.ElementTree(et.XML('<foo><App Title="s"><PrimaryToken /><RootNamespace/><SilverlightAppEntry/><XapFilename/><AssemblyName/></App></foo>'));
+            } else {
+                throw new CordovaError('Unexpected parseElementtreeSync: ' + path);
+            }
+        });
+    });
+
+    function wrapper(p, done, post) {
+        p.then(post, function(err) {
+            expect(err).toBeUndefined();
+        }).fin(done);
+    }
+
+    function errorWrapper(p, done, post) {
+        p.then(function() {
+            expect('this call').toBe('fail');
+        }, post).fin(done);
+    }
+
+    describe('constructions', function() {
+        it('should throw if provided directory does not contain a csproj file', function() {
+            readdir.andReturn([]);
+            expect(function() {
+                new platforms.wp7.parser(proj);
+            }).toThrow();
+        });
+        it('should create an instance with path, manifest properties', function() {
+            expect(function() {
+                var p = new platforms.wp7.parser(proj);
+                expect(p.wp7_proj_dir).toEqual(proj);
+                expect(p.manifest_path).toEqual(path.join(proj, 'Properties', 'WMAppManifest.xml'));
+            }).not.toThrow();
+        });
+    });
+
+    describe('check_requirements', function() {
+        it('should fire a callback if there is an error during shelling out', function(done) {
+            exec.andCallFake(function(cmd, opts, cb) {
+                (cb || opts)(50, 'there was an errorz!');
+            });
+            errorWrapper(platforms.wp7.parser.check_requirements(proj), done, function(err) {
+                expect(err).toContain('there was an errorz!');
+            });
+        });
+        it('should check by calling check_reqs on the stock lib path if no custom path is defined', function(done) {
+            wrapper(platforms.wp7.parser.check_requirements(proj), done, function(err) {
+                expect(exec.mostRecentCall.args[0]).toContain(util.libDirectory);
+                expect(exec.mostRecentCall.args[0]).toMatch(/check_reqs"$/);
+            });
+        });
+        it('should check by calling check_reqs on a custom path if it is so defined', function(done) {
+            var custom_path = path.join('some','custom','path','to','wp7','lib');
+            custom.andReturn(custom_path);
+            wrapper(platforms.wp7.parser.check_requirements(proj), done, function() {
+                expect(exec.mostRecentCall.args[0]).toContain(custom_path);
+                expect(exec.mostRecentCall.args[0]).toMatch(/check_reqs"$/);
+            });
+        });
+    });
+
+    describe('instance', function() {
+        var p, cp, rm, is_cordova, write, read, mv, mkdir;
+        var wp7_proj = path.join(proj, 'platforms', 'wp7');
+        beforeEach(function() {
+            p = new platforms.wp7.parser(wp7_proj);
+            cp = spyOn(shell, 'cp');
+            rm = spyOn(shell, 'rm');
+            mv = spyOn(shell, 'mv');
+            mkdir = spyOn(shell, 'mkdir');
+            is_cordova = spyOn(util, 'isCordova').andReturn(proj);
+            write = spyOn(fs, 'writeFileSync');
+            read = spyOn(fs, 'readFileSync').andReturn('');
+        });
+
+        describe('update_from_config method', function() {
+            beforeEach(function() {
+                cfg.name = function() { return 'testname' };
+                cfg.content = function() { return 'index.html' };
+                cfg.packageName = function() { return 'testpkg' };
+                cfg.version = function() { return 'one point oh' };
+                readdir.andReturn(['test.sln']);
+            });
+
+            it('should write out the app name to wmappmanifest.xml', function() {
+                p.update_from_config(cfg);
+                var appEl = manifestXml.getroot().find('.//App');
+                expect(appEl.attrib.Title).toEqual('testname');
+            });
+            it('should write out the app id to csproj file', function() {
+                p.update_from_config(cfg);
+                var appEl = projXml.getroot().find('.//RootNamespace');
+                expect(appEl.text).toContain('testpkg');
+            });
+            it('should write out the app version to wmappmanifest.xml', function() {
+                p.update_from_config(cfg);
+                var appEl = manifestXml.getroot().find('.//App');
+                expect(appEl.attrib.Version).toEqual('one point oh');
+            });
+        });
+        describe('www_dir method', function() {
+            it('should return www', function() {
+                expect(p.www_dir()).toEqual(path.join(wp7_proj, 'www'));
+            });
+        });
+        describe('config_xml method', function() {
+            it('should return the location of the config.xml', function() {
+                expect(p.config_xml()).toEqual(path.join(wp7_proj, 'config.xml'));
+            });
+        });
+        describe('update_www method', function() {
+            var update_csproj;
+            beforeEach(function() {
+                update_csproj = spyOn(p, 'update_csproj');
+            });
+            it('should rm project-level www and cp in platform agnostic www', function() {
+                p.update_www();
+                expect(rm).toHaveBeenCalled();
+                expect(cp).toHaveBeenCalled();
+            });
+        });
+        describe('update_project method', function() {
+            var config, www, overrides, svn, cfg, csproj;
+            beforeEach(function() {
+                config = spyOn(p, 'update_from_config');
+                www = spyOn(p, 'update_www');
+                svn = spyOn(util, 'deleteSvnFolders');
+                csproj = spyOn(p, 'update_csproj');
+                exists.andReturn(false);
+            });
+            it('should call update_from_config', function(done) {
+                wrapper(p.update_project(), done, function(){
+                    expect(config).toHaveBeenCalled();
+                });
+            });
+            it('should throw if update_from_config throws', function(done) {
+                var err = new Error('uh oh!');
+                config.andCallFake(function() { throw err; });
+                errorWrapper(p.update_project({}), done, function(e) {
+                    expect(e).toEqual(err);
+                });
+            });
+            it('should call update_www', function(done) {
+                wrapper(p.update_project(), done, function() {
+                    expect(www).not.toHaveBeenCalled();
+                });
+            });
+            it('should call deleteSvnFolders', function(done) {
+                wrapper(p.update_project(), done, function() {
+                    expect(svn).toHaveBeenCalled();
+                });
+            });
+        });
+    });
+});

http://git-wip-us.apache.org/repos/asf/cordova-cli/blob/b51e1c12/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
new file mode 100644
index 0000000..5ea461e
--- /dev/null
+++ b/cordova-lib/spec-cordova/metadata/wp8_parser.spec.js
@@ -0,0 +1,208 @@
+/**
+    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 platforms = require('../../platforms'),
+    util = require('../../src/util'),
+    path = require('path'),
+    shell = require('shelljs'),
+    fs = require('fs'),
+    et = require('elementtree'),
+    xmlHelpers = require('../../src/xml-helpers'),
+    Q = require('q'),
+    child_process = require('child_process'),
+    config = require('../../src/config'),
+    ConfigParser = require('../../src/ConfigParser'),
+    CordovaError = require('../../src/CordovaError'),
+    cordova = require('../../cordova');
+
+// Create a real config object before mocking out everything.
+var cfg = new ConfigParser(path.join(__dirname, '..', 'test-config.xml'));
+
+describe('wp8 project parser', function() {
+    var proj = '/some/path';
+    var exists, exec, custom, readdir, cfg_parser;
+    var manifestXml, projXml;
+    beforeEach(function() {
+        exists = spyOn(fs, 'existsSync').andReturn(true);
+        exec = spyOn(child_process, 'exec').andCallFake(function(cmd, opts, cb) {
+            (cb || opts)(0, '', '');
+        });
+        custom = spyOn(config, 'has_custom_path').andReturn(false);
+        readdir = spyOn(fs, 'readdirSync').andReturn(['test.csproj']);
+        projXml = manifestXml = null;
+        spyOn(xmlHelpers, 'parseElementtreeSync').andCallFake(function(path) {
+            if (/WMAppManifest.xml$/.exec(path)) {
+                return manifestXml = new et.ElementTree(et.XML('<foo><App Title="s"><PrimaryToken /><RootNamespace/><SilverlightAppEntry/><XapFilename/><AssemblyName/></App></foo>'));
+            } else if (/csproj$/.exec(path)) {
+                return projXml = new et.ElementTree(et.XML('<foo><App Title="s"><PrimaryToken /><RootNamespace/><SilverlightAppEntry/><XapFilename/><AssemblyName/></App></foo>'));
+            } else if (/xaml$/.exec(path)) {
+                return new et.ElementTree(et.XML('<foo><App Title="s"><PrimaryToken /><RootNamespace/><SilverlightAppEntry/><XapFilename/><AssemblyName/></App></foo>'));
+            } else {
+                throw new CordovaError('Unexpected parseElementtreeSync: ' + path);
+            }
+        });
+    });
+
+    function wrapper(p, done, post) {
+        p.then(post, function(err) {
+            expect(err).toBeUndefined();
+        }).fin(done);
+    }
+
+    function errorWrapper(p, done, post) {
+        p.then(function() {
+            expect('this call').toBe('fail');
+        }, post).fin(done);
+    }
+
+    describe('constructions', function() {
+        it('should throw if provided directory does not contain a csproj file', function() {
+            readdir.andReturn([]);
+            expect(function() {
+                new platforms.wp8.parser(proj);
+            }).toThrow();
+        });
+        it('should create an instance with path, manifest properties', function() {
+            expect(function() {
+                var p = new platforms.wp8.parser(proj);
+                expect(p.wp8_proj_dir).toEqual(proj);
+                expect(p.manifest_path).toEqual(path.join(proj, 'Properties', 'WMAppManifest.xml'));
+            }).not.toThrow();
+        });
+    });
+
+    describe('check_requirements', function() {
+        it('should fire a callback if there is an error during shelling out', function(done) {
+            exec.andCallFake(function(cmd, opts, cb) {
+                (cb || opts)(50, 'there was an errorz!');
+            });
+            errorWrapper(platforms.wp8.parser.check_requirements(proj), done, function(err) {
+                expect(err).toContain('there was an errorz!');
+            });
+        });
+        it('should check by calling check_reqs on the stock lib path if no custom path is defined', function(done) {
+            wrapper(platforms.wp8.parser.check_requirements(proj), done, function() {
+                expect(exec.mostRecentCall.args[0]).toContain(util.libDirectory);
+                expect(exec.mostRecentCall.args[0]).toMatch(/check_reqs"$/);
+            });
+        });
+        it('should check by calling check_reqs on a custom path if it is so defined', function(done) {
+            var custom_path = path.join('some','custom','path','to','wp8','lib');
+            custom.andReturn(custom_path);
+            wrapper(platforms.wp8.parser.check_requirements(proj), done, function(err) {
+                expect(exec.mostRecentCall.args[0]).toContain(custom_path);
+                expect(exec.mostRecentCall.args[0]).toMatch(/check_reqs"$/);
+            });
+        });
+    });
+
+    describe('instance', function() {
+        var p, cp, rm, is_cordova, write, read, mv, mkdir;
+        var wp8_proj = path.join(proj, 'platforms', 'wp8');
+        beforeEach(function() {
+            p = new platforms.wp8.parser(wp8_proj);
+            cp = spyOn(shell, 'cp');
+            rm = spyOn(shell, 'rm');
+            mv = spyOn(shell, 'mv');
+            mkdir = spyOn(shell, 'mkdir');
+            is_cordova = spyOn(util, 'isCordova').andReturn(proj);
+            write = spyOn(fs, 'writeFileSync');
+            read = spyOn(fs, 'readFileSync').andReturn('');
+        });
+
+        describe('update_from_config method', function() {
+            beforeEach(function() {
+                cfg.name = function() { return 'testname' };
+                cfg.content = function() { return 'index.html' };
+                cfg.packageName = function() { return 'testpkg' };
+                cfg.version = function() { return 'one point oh' };
+                readdir.andReturn(['test.sln']);
+            });
+
+            it('should write out the app name to wmappmanifest.xml', function() {
+                p.update_from_config(cfg);
+                var appEl = manifestXml.getroot().find('.//App');
+                expect(appEl.attrib.Title).toEqual('testname');
+            });
+            it('should write out the app id to csproj file', function() {
+                p.update_from_config(cfg);
+                var appEl = projXml.getroot().find('.//RootNamespace');
+                expect(appEl.text).toContain('testpkg');
+            });
+            it('should write out the app version to wmappmanifest.xml', function() {
+                p.update_from_config(cfg);
+                var appEl = manifestXml.getroot().find('.//App');
+                expect(appEl.attrib.Version).toEqual('one point oh');
+            });
+        });
+        describe('www_dir method', function() {
+            it('should return www', function() {
+                expect(p.www_dir()).toEqual(path.join(wp8_proj, 'www'));
+            });
+        });
+        describe('config_xml method', function() {
+            it('should return the location of the config.xml', function() {
+                expect(p.config_xml()).toEqual(path.join(wp8_proj, 'config.xml'));
+            });
+        });
+        describe('update_www method', function() {
+            var update_csproj;
+            beforeEach(function() {
+                update_csproj = spyOn(p, 'update_csproj');
+            });
+            it('should rm project-level www and cp in platform agnostic www', function() {
+                p.update_www();
+                expect(rm).toHaveBeenCalled();
+                expect(cp).toHaveBeenCalled();
+            });
+        });
+        describe('update_project method', function() {
+            var config, www, overrides, svn, csproj;
+            beforeEach(function() {
+                config = spyOn(p, 'update_from_config');
+                www = spyOn(p, 'update_www');
+                svn = spyOn(util, 'deleteSvnFolders');
+                csproj = spyOn(p, 'update_csproj');
+                exists.andReturn(false);
+            });
+            it('should call update_from_config', function(done) {
+                wrapper(p.update_project(), done, function() {
+                    expect(config).toHaveBeenCalled();
+                });
+            });
+            it('should throw if update_from_config throws', function(done) {
+                var err = new Error('uh oh!');
+                config.andCallFake(function() { throw err; });
+                errorWrapper(p.update_project({}), done, function(e) {
+                    expect(e).toEqual(err);
+                });
+            });
+            it('should not call update_www', function(done) {
+                wrapper(p.update_project(), done, function() {
+                    expect(www).not.toHaveBeenCalled();
+                });
+            });
+            it('should call deleteSvnFolders', function(done) {
+                wrapper(p.update_project(), done, function() {
+                    expect(svn).toHaveBeenCalled();
+                });
+            });
+        });
+    });
+});

http://git-wip-us.apache.org/repos/asf/cordova-cli/blob/b51e1c12/cordova-lib/spec-cordova/platform.spec.js
----------------------------------------------------------------------
diff --git a/cordova-lib/spec-cordova/platform.spec.js b/cordova-lib/spec-cordova/platform.spec.js
new file mode 100644
index 0000000..e16d51d
--- /dev/null
+++ b/cordova-lib/spec-cordova/platform.spec.js
@@ -0,0 +1,108 @@
+
+var helpers = require('./helpers'),
+    path = require('path'),
+    fs = require('fs'),
+    shell = require('shelljs'),
+    platforms = require('../platforms'),
+    superspawn = require('../src/superspawn'),
+    config = require('../src/config'),
+    Q = require('q'),
+    events = require('../src/events'),
+    cordova = require('../cordova');
+
+var tmpDir = helpers.tmpDir('platform_test');
+var project = path.join(tmpDir, 'project');
+
+var platformParser = platforms[helpers.testPlatform].parser;
+
+describe('platform end-to-end', function() {
+    var results;
+
+    beforeEach(function() {
+        shell.rm('-rf', tmpDir);
+    });
+    afterEach(function() {
+        process.chdir(path.join(__dirname, '..'));  // Needed to rm the dir on Windows.
+        shell.rm('-rf', tmpDir);
+    });
+
+    // Factoring out some repeated checks.
+    function emptyPlatformList() {
+        return cordova.raw.platform('list').then(function() {
+            var installed = results.match(/Installed platforms: (.*)/);
+            expect(installed).toBeDefined();
+            expect(installed[1].indexOf(helpers.testPlatform)).toBe(-1);
+        });
+    }
+
+    function fullPlatformList() {
+        return cordova.raw.platform('list').then(function() {
+            var installed = results.match(/Installed platforms: (.*)/);
+            expect(installed).toBeDefined();
+            expect(installed[1].indexOf(helpers.testPlatform)).toBeGreaterThan(-1);
+        });
+    }
+
+    // The flows we want to test are add, rm, list, and upgrade.
+    // They should run the appropriate hooks.
+    // They should fail when not inside a Cordova project.
+    // These tests deliberately have no beforeEach and afterEach that are cleaning things up.
+    it('should successfully run', function(done) {
+        // cp then mv because we need to copy everything, but that means it'll copy the whole directory.
+        // Using /* doesn't work because of hidden files.
+        shell.cp('-R', path.join(__dirname, 'fixtures', 'base'), tmpDir);
+        shell.mv(path.join(tmpDir, 'base'), project);
+        process.chdir(project);
+
+        // Now we load the config.json in the newly created project and edit the target platform's lib entry
+        // to point at the fixture version. This is necessary so that cordova.prepare can find cordova.js there.
+        var c = config.read(project);
+        c.lib[helpers.testPlatform].uri = path.join(__dirname, 'fixtures', 'platforms', helpers.testPlatform + '-lib');
+        config.write(project, c);
+
+        // The config.json in the fixture project points at fake "local" paths.
+        // Since it's not a URL, the lazy-loader will just return the junk path.
+        spyOn(superspawn, 'spawn').andCallFake(function(cmd, args) {
+            if (cmd.match(/create\b/)) {
+                // This is a call to the bin/create script, so do the copy ourselves.
+                shell.cp('-R', path.join(__dirname, 'fixtures', 'platforms', 'android'), path.join(project, 'platforms'));
+            } else if(cmd.match(/version\b/)) {
+                return Q('3.3.0');
+            } else if(cmd.match(/update\b/)) {
+                fs.writeFileSync(path.join(project, 'platforms', helpers.testPlatform, 'updated'), 'I was updated!', 'utf-8');
+            }
+            return Q();
+        });
+
+        events.on('results', function(res) { results = res; });
+
+        // Check there are no platforms yet.
+        emptyPlatformList().then(function() {
+            // Add the testing platform.
+            return cordova.raw.platform('add', [helpers.testPlatform]);
+        }).then(function() {
+            // Check the platform add was successful.
+            expect(path.join(project, 'platforms', helpers.testPlatform)).toExist();
+            expect(path.join(project, 'merges', helpers.testPlatform)).toExist();
+            expect(path.join(project, 'platforms', helpers.testPlatform, 'cordova')).toExist();
+        }).then(fullPlatformList) // Check for it in platform ls.
+        .then(function() {
+            // Try to update the platform.
+            return cordova.raw.platform('update', [helpers.testPlatform]);
+        }).then(function() {
+            // Our fake update script in the exec mock above creates this dummy file.
+            expect(path.join(project, 'platforms', helpers.testPlatform, 'updated')).toExist();
+        }).then(fullPlatformList) // Platform should still be in platform ls.
+        .then(function() {
+            // And now remove it.
+            return cordova.raw.platform('rm', [helpers.testPlatform]);
+        }).then(function() {
+            // It should be gone.
+            expect(path.join(project, 'platforms', helpers.testPlatform)).not.toExist();
+        }).then(emptyPlatformList) // platform ls should be empty too.
+        .fail(function(err) {
+            expect(err).toBeUndefined();
+        }).fin(done);
+    });
+});
+