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();