diff --git a/spec/unit/Adb.spec.js b/spec/unit/Adb.spec.js
new file mode 100644
index 00000000..c6379785
--- /dev/null
+++ b/spec/unit/Adb.spec.js
@@ -0,0 +1,229 @@
+/**
+ 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.
+*/
+
+const CordovaError = require('cordova-common').CordovaError;
+const rewire = require('rewire');
+
+describe('Adb', () => {
+ const adbOutput = `List of devices attached
+emulator-5554\tdevice
+123a76565509e124\tdevice`;
+ const [, emulatorLine, deviceLine] = adbOutput.split('\n');
+ const emulatorId = emulatorLine.split('\t')[0];
+ const deviceId = deviceLine.split('\t')[0];
+
+ const alreadyExistsError = 'adb: failed to install app.apk: Failure[INSTALL_FAILED_ALREADY_EXISTS]';
+ const certificateError = 'adb: failed to install app.apk: Failure[INSTALL_PARSE_FAILED_NO_CERTIFICATES]';
+ const downgradeError = 'adb: failed to install app.apk: Failure[INSTALL_FAILED_VERSION_DOWNGRADE]';
+
+ let Adb;
+ let spawnSpy;
+
+ beforeEach(() => {
+ Adb = rewire('../../bin/templates/cordova/lib/Adb');
+ spawnSpy = jasmine.createSpy('spawn');
+ Adb.__set__('spawn', spawnSpy);
+ });
+
+ describe('isDevice', () => {
+ it('should return true for a real device', () => {
+ const isDevice = Adb.__get__('isDevice');
+
+ expect(isDevice(deviceLine)).toBeTruthy();
+ expect(isDevice(emulatorLine)).toBeFalsy();
+ });
+ });
+
+ describe('isEmulator', () => {
+ it('should return true for an emulator', () => {
+ const isEmulator = Adb.__get__('isEmulator');
+
+ expect(isEmulator(emulatorLine)).toBeTruthy();
+ expect(isEmulator(deviceLine)).toBeFalsy();
+ });
+ });
+
+ describe('devices', () => {
+ beforeEach(() => {
+ spawnSpy.and.returnValue(Promise.resolve(adbOutput));
+ });
+
+ it('should return only devices if no options are specified', () => {
+ return Adb.devices().then(devices => {
+ expect(devices.length).toBe(1);
+ expect(devices[0]).toBe(deviceId);
+ });
+ });
+
+ it('should return only emulators if opts.emulators is true', () => {
+ return Adb.devices({emulators: true}).then(devices => {
+ expect(devices.length).toBe(1);
+ expect(devices[0]).toBe(emulatorId);
+ });
+ });
+ });
+
+ describe('install', () => {
+ beforeEach(() => {
+ spawnSpy.and.returnValue(Promise.resolve(''));
+ });
+
+ it('should target the passed device id to adb', () => {
+ return Adb.install(deviceId).then(() => {
+ const args = spawnSpy.calls.argsFor(0);
+ expect(args[0]).toBe('adb');
+
+ const adbArgs = args[1].join(' ');
+ expect(adbArgs).toMatch(`-s ${deviceId}`);
+ });
+ });
+
+ it('should add the -r flag if opts.replace is set', () => {
+ return Adb.install(deviceId, '', { replace: true }).then(() => {
+ const adbArgs = spawnSpy.calls.argsFor(0)[1];
+ expect(adbArgs).toContain('-r');
+ });
+ });
+
+ it('should pass the correct package path to adb', () => {
+ const packagePath = 'build/test/app.apk';
+
+ return Adb.install(deviceId, packagePath).then(() => {
+ const adbArgs = spawnSpy.calls.argsFor(0)[1];
+ expect(adbArgs).toContain(packagePath);
+ });
+ });
+
+ it('should reject with a CordovaError if the adb output suggests a failure', () => {
+ spawnSpy.and.returnValue(Promise.resolve(alreadyExistsError));
+
+ return Adb.install(deviceId, '').then(
+ () => fail('Unexpectedly resolved'),
+ err => {
+ expect(err).toEqual(jasmine.any(CordovaError));
+ }
+ );
+ });
+
+ // The following two tests are somewhat brittle as they are dependent on the
+ // exact message returned. But it is better to have them tested than not at all.
+ it('should give a more specific error message if there is a certificate failure', () => {
+ spawnSpy.and.returnValue(Promise.resolve(certificateError));
+
+ return Adb.install(deviceId, '').then(
+ () => fail('Unexpectedly resolved'),
+ err => {
+ expect(err).toEqual(jasmine.any(CordovaError));
+ expect(err.message).toMatch('Sign the build');
+ }
+ );
+ });
+
+ it('should give a more specific error message if there is a downgrade error', () => {
+ spawnSpy.and.returnValue(Promise.resolve(downgradeError));
+
+ return Adb.install(deviceId, '').then(
+ () => fail('Unexpectedly resolved'),
+ err => {
+ expect(err).toEqual(jasmine.any(CordovaError));
+ expect(err.message).toMatch('lower versionCode');
+ }
+ );
+ });
+ });
+
+ describe('uninstall', () => {
+ it('should call adb uninstall with the correct arguments', () => {
+ const packageId = 'io.cordova.test';
+ spawnSpy.and.returnValue(Promise.resolve(''));
+
+ return Adb.uninstall(deviceId, packageId).then(() => {
+ const args = spawnSpy.calls.argsFor(0);
+ expect(args[0]).toBe('adb');
+
+ const adbArgs = args[1];
+ expect(adbArgs).toContain('uninstall');
+ expect(adbArgs.join(' ')).toContain(`-s ${deviceId}`);
+ expect(adbArgs[adbArgs.length - 1]).toBe(packageId);
+ });
+ });
+ });
+
+ describe('shell', () => {
+ const shellCommand = 'ls -l /sdcard';
+
+ it('should run the passed command on the target device', () => {
+ spawnSpy.and.returnValue(Promise.resolve(''));
+
+ return Adb.shell(deviceId, shellCommand).then(() => {
+ const args = spawnSpy.calls.argsFor(0);
+ expect(args[0]).toBe('adb');
+
+ const adbArgs = args[1].join(' ');
+ expect(adbArgs).toContain('shell');
+ expect(adbArgs).toContain(`-s ${deviceId}`);
+ expect(adbArgs).toMatch(new RegExp(`${shellCommand}$`));
+ });
+ });
+
+ it('should reject with a CordovaError on failure', () => {
+ const errorMessage = 'shell error';
+ spawnSpy.and.returnValue(Promise.reject(errorMessage));
+
+ return Adb.shell(deviceId, shellCommand).then(
+ () => fail('Unexpectedly resolved'),
+ err => {
+ expect(err).toEqual(jasmine.any(CordovaError));
+ expect(err.message).toMatch(errorMessage);
+ }
+ );
+ });
+ });
+
+ describe('start', () => {
+ const activityName = 'io.cordova.test/.MainActivity';
+
+ it('should start an activity using the shell activity manager', () => {
+ const shellSpy = spyOn(Adb, 'shell').and.returnValue(Promise.resolve(''));
+
+ return Adb.start(deviceId, activityName).then(() => {
+ expect(shellSpy).toHaveBeenCalled();
+
+ const [target, command] = shellSpy.calls.argsFor(0);
+ expect(target).toBe(deviceId);
+ expect(command).toContain('am start');
+ expect(command).toContain(`-n${activityName}`);
+ });
+ });
+
+ it('should reject with a CordovaError on a shell error', () => {
+ const errorMessage = 'Test Start error';
+ spyOn(Adb, 'shell').and.returnValue(Promise.reject(errorMessage));
+
+ return Adb.start(deviceId, activityName).then(
+ () => fail('Unexpectedly resolved'),
+ err => {
+ expect(err).toEqual(jasmine.any(CordovaError));
+ expect(err.message).toMatch(errorMessage);
+ }
+ );
+ });
+ });
+
+});
diff --git a/spec/unit/AndroidManifest.spec.js b/spec/unit/AndroidManifest.spec.js
new file mode 100644
index 00000000..84c577b1
--- /dev/null
+++ b/spec/unit/AndroidManifest.spec.js
@@ -0,0 +1,318 @@
+/**
+ 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.
+*/
+
+const fs = require('fs');
+const os = require('os');
+const path = require('path');
+const rewire = require('rewire');
+
+describe('AndroidManifest', () => {
+ const VERSION_CODE = '50407';
+ const VERSION_NAME = '5.4.7';
+ const PACKAGE_ID = 'io.cordova.test';
+ const ACTIVITY_LAUNCH_MODE = 'singleTop';
+ const ACTIVITY_NAME = 'MainActivity';
+ const ACTIVITY_ORIENTATION = 'portrait';
+ const MIN_SDK_VERSION = '12';
+ const MAX_SDK_VERSION = '88';
+ const TARGET_SDK_VERSION = '27';
+
+ const DEFAULT_MANIFEST = `
+
+
+
+
+
+
+
+
+
+
+
+
+`;
+
+ const manifestPath = path.join(os.tmpdir(), `AndroidManifest${Date.now()}.xml`);
+
+ function createTempManifestFile (xml) {
+ fs.writeFileSync(manifestPath, xml);
+ }
+
+ function removeTempManifestFile () {
+ fs.unlinkSync(manifestPath);
+ }
+
+ let AndroidManifest;
+ let manifest;
+
+ beforeEach(() => {
+ createTempManifestFile(DEFAULT_MANIFEST);
+
+ AndroidManifest = rewire('../../bin/templates/cordova/lib/AndroidManifest');
+ manifest = new AndroidManifest(manifestPath);
+ });
+
+ afterEach(() => {
+ removeTempManifestFile();
+ });
+
+ describe('constructor', () => {
+ it('should parse the manifest', () => {
+ expect(manifest.doc.getroot().tag).toBe('manifest');
+ });
+
+ it('should throw an error if not a valid manifest', () => {
+ createTempManifestFile(``);
+
+ expect(() => new AndroidManifest(manifestPath)).toThrowError();
+ });
+ });
+
+ describe('versionName', () => {
+ it('should get the version name', () => {
+ expect(manifest.getVersionName()).toBe(VERSION_NAME);
+ });
+
+ it('should set the version name', () => {
+ const newVersionName = `${VERSION_NAME}55555`;
+ manifest.setVersionName(newVersionName);
+ expect(manifest.getVersionName()).toBe(newVersionName);
+ });
+ });
+
+ describe('versionCode', () => {
+ it('should get the version code', () => {
+ expect(manifest.getVersionCode()).toBe(VERSION_CODE);
+ });
+
+ it('should set the version code', () => {
+ const newVersionName = `${VERSION_CODE}12345`;
+ manifest.setVersionCode(newVersionName);
+ expect(manifest.getVersionCode()).toBe(newVersionName);
+ });
+ });
+
+ describe('packageId', () => {
+ it('should get the package ID', () => {
+ expect(manifest.getPackageId()).toBe(PACKAGE_ID);
+ });
+
+ it('should set the package ID', () => {
+ const newPackageId = `${PACKAGE_ID}new`;
+ manifest.setPackageId(newPackageId);
+ expect(manifest.getPackageId()).toBe(newPackageId);
+ });
+ });
+
+ describe('activity', () => {
+ let activity;
+
+ beforeEach(() => {
+ activity = manifest.getActivity();
+ });
+
+ describe('name', () => {
+ it('should get the activity name', () => {
+ expect(activity.getName()).toBe(ACTIVITY_NAME);
+ });
+
+ it('should set the activity name', () => {
+ const newActivityName = `${ACTIVITY_NAME}New`;
+ activity.setName(newActivityName);
+ expect(activity.getName()).toBe(newActivityName);
+ });
+
+ it('should remove the activity name if set to empty', () => {
+ activity.setName();
+ expect(activity.getName()).toBe(undefined);
+ });
+ });
+
+ describe('orientation', () => {
+ it('should get the activity orientation', () => {
+ expect(activity.getOrientation()).toBe(ACTIVITY_ORIENTATION);
+ });
+
+ it('should set the activity orienation', () => {
+ const newOrientation = 'landscape';
+ activity.setOrientation(newOrientation);
+ expect(activity.getOrientation()).toBe(newOrientation);
+ });
+
+ it('should remove the orientation if set to default', () => {
+ activity.setOrientation(AndroidManifest.__get__('DEFAULT_ORIENTATION'));
+ expect(activity.getOrientation()).toBe(undefined);
+ });
+
+ it('should remove the orientation if set to empty', () => {
+ activity.setOrientation();
+ expect(activity.getOrientation()).toBe(undefined);
+ });
+ });
+
+ describe('launch mode', () => {
+ it('should get the activity launch mode', () => {
+ expect(activity.getLaunchMode()).toBe(ACTIVITY_LAUNCH_MODE);
+ });
+
+ it('should set the activity launch mode', () => {
+ const newLaunchMode = 'standard';
+ activity.setLaunchMode(newLaunchMode);
+ expect(activity.getLaunchMode()).toBe(newLaunchMode);
+ });
+
+ it('should remove the launch mode if set to empty', () => {
+ activity.setLaunchMode();
+ expect(activity.getLaunchMode()).toBe(undefined);
+ });
+ });
+ });
+
+ describe('minSdkVersion', () => {
+ it('should get minSdkVersion', () => {
+ expect(manifest.getMinSdkVersion()).toBe(MIN_SDK_VERSION);
+ });
+
+ it('should set minSdkVersion', () => {
+ const newMinSdkVersion = `${MIN_SDK_VERSION}111`;
+ manifest.setMinSdkVersion(newMinSdkVersion);
+ expect(manifest.getMinSdkVersion()).toBe(newMinSdkVersion);
+ });
+
+ it('should create the uses-sdk node if it does not exist when setting minSdkVersion', () => {
+ const root = manifest.doc.getroot();
+ root.remove(root.find('./uses-sdk'));
+
+ expect(root.find('./uses-sdk')).toBe(null);
+
+ manifest.setMinSdkVersion(1);
+
+ expect(root.find('./uses-sdk')).not.toBe(null);
+ expect(manifest.getMinSdkVersion()).toBe(1);
+ });
+ });
+
+ describe('maxSdkVersion', () => {
+ it('should get maxSdkVersion', () => {
+ expect(manifest.getMaxSdkVersion()).toBe(MAX_SDK_VERSION);
+ });
+
+ it('should set maxSdkVersion', () => {
+ const newMaxSdkVersion = `${MAX_SDK_VERSION}999`;
+ manifest.setMaxSdkVersion(newMaxSdkVersion);
+ expect(manifest.getMaxSdkVersion()).toBe(newMaxSdkVersion);
+ });
+
+ it('should create the uses-sdk node if it does not exist when setting maxSdkVersion', () => {
+ const root = manifest.doc.getroot();
+ root.remove(root.find('./uses-sdk'));
+
+ expect(root.find('./uses-sdk')).toBe(null);
+
+ manifest.setMaxSdkVersion(1);
+
+ expect(root.find('./uses-sdk')).not.toBe(null);
+ expect(manifest.getMaxSdkVersion()).toBe(1);
+ });
+ });
+
+ describe('targetSdkVersion', () => {
+ it('should get targetSdkVersion', () => {
+ expect(manifest.getTargetSdkVersion()).toBe(TARGET_SDK_VERSION);
+ });
+
+ it('should set targetSdkVersion', () => {
+ const newTargetSdkVersion = `${TARGET_SDK_VERSION}555`;
+ manifest.setTargetSdkVersion(newTargetSdkVersion);
+ expect(manifest.getTargetSdkVersion()).toBe(newTargetSdkVersion);
+ });
+
+ it('should create the uses-sdk node if it does not exist when setting targetSdkVersion', () => {
+ const root = manifest.doc.getroot();
+ root.remove(root.find('./uses-sdk'));
+
+ expect(root.find('./uses-sdk')).toBe(null);
+
+ manifest.setTargetSdkVersion(1);
+
+ expect(root.find('./uses-sdk')).not.toBe(null);
+ expect(manifest.getTargetSdkVersion()).toBe(1);
+ });
+ });
+
+ describe('debuggable', () => {
+ it('should get debuggable', () => {
+ expect(manifest.getDebuggable()).toBe(true);
+ });
+
+ it('should remove debuggable if set to a falsy value', () => {
+ manifest.setDebuggable(false);
+ expect(manifest.doc.getroot().find('./application').attrib['android:debuggable']).toBe(undefined);
+ });
+
+ it('should set debuggable to true', () => {
+ const NO_DEBUGGABLE_MANIFEST = DEFAULT_MANIFEST.replace('android:debuggable="true"', '');
+ createTempManifestFile(NO_DEBUGGABLE_MANIFEST);
+ manifest = new AndroidManifest(manifestPath);
+
+ expect(manifest.getDebuggable()).toBe(false);
+
+ manifest.setDebuggable(true);
+ expect(manifest.getDebuggable()).toBe(true);
+ });
+ });
+
+ describe('write', () => {
+ let fsSpy;
+
+ beforeEach(() => {
+ fsSpy = jasmine.createSpyObj('fs', ['writeFileSync']);
+ AndroidManifest.__set__('fs', fsSpy);
+ });
+
+ it('should overwrite existing manifest if path not specified', () => {
+ manifest.write();
+
+ expect(fsSpy.writeFileSync).toHaveBeenCalledWith(manifestPath, jasmine.any(String), jasmine.any(String));
+ });
+
+ it('should save to the specified path', () => {
+ const testPath = 'NewAndroidManifest.xml';
+ manifest.write(testPath);
+
+ expect(fsSpy.writeFileSync).toHaveBeenCalledWith(testPath, jasmine.any(String), jasmine.any(String));
+ });
+
+ it('should write the manifest from the parsed XML as utf-8', () => {
+ const newXml = '';
+ spyOn(manifest.doc, 'write').and.returnValue(newXml);
+
+ manifest.write();
+
+ expect(fsSpy.writeFileSync).toHaveBeenCalledWith(jasmine.any(String), newXml, 'utf-8');
+ });
+ });
+
+});
diff --git a/spec/unit/Api.spec.js b/spec/unit/Api.spec.js
index ec36eee9..fa24e6a7 100644
--- a/spec/unit/Api.spec.js
+++ b/spec/unit/Api.spec.js
@@ -32,10 +32,11 @@ var FIXTURES = path.join(__dirname, '../e2e/fixtures');
var FAKE_PROJECT_DIR = path.join(os.tmpdir(), 'plugin-test-project');
describe('addPlugin method', function () {
- var api, fail, gradleBuilder, oldClean;
- var Api = rewire('../../bin/templates/cordova/Api');
+ var api, Api, fail, gradleBuilder;
beforeEach(function () {
+ Api = rewire('../../bin/templates/cordova/Api');
+
var pluginManager = jasmine.createSpyObj('pluginManager', ['addPlugin']);
pluginManager.addPlugin.and.returnValue(Q());
spyOn(common.PluginManager, 'get').and.returnValue(pluginManager);
@@ -43,8 +44,11 @@ describe('addPlugin method', function () {
var projectSpy = jasmine.createSpyObj('AndroidProject', ['getPackageName', 'write', 'isClean']);
spyOn(AndroidProject, 'getProjectFile').and.returnValue(projectSpy);
- oldClean = Api.__get__('Api.prototype.clean');
Api.__set__('Api.prototype.clean', Q);
+
+ // Prevent logging to avoid polluting the test reports
+ Api.__set__('selfEvents.emit', jasmine.createSpy());
+
api = new Api('android', FAKE_PROJECT_DIR);
fail = jasmine.createSpy('fail');
@@ -52,10 +56,6 @@ describe('addPlugin method', function () {
spyOn(builders, 'getBuilder').and.returnValue(gradleBuilder);
});
- afterEach(function () {
- Api.__set__('Api.prototype.clean', oldClean);
- });
-
it('Test#001 : should call gradleBuilder.prepBuildFiles for every plugin with frameworks', function (done) {
api.addPlugin(new PluginInfo(path.join(FIXTURES, 'cordova-plugin-fake'))).catch(fail).fin(function () {
expect(fail).not.toHaveBeenCalled();