diff --git a/bin/templates/cordova/lib/device.js b/bin/templates/cordova/lib/device.js deleted file mode 100644 index 1ea4cdc0d2..0000000000 --- a/bin/templates/cordova/lib/device.js +++ /dev/null @@ -1,48 +0,0 @@ -/* - Licensed to the Apache Software Foundation (ASF) under one - or more contributor license agreements. See the NOTICE file - distributed with this work for additional information - regarding copyright ownership. The ASF licenses this file - to you under the Apache License, Version 2.0 (the - "License"); you may not use this file except in compliance - with the License. You may obtain a copy of the License at - - http://www.apache.org/licenses/LICENSE-2.0 - - Unless required by applicable law or agreed to in writing, - software distributed under the License is distributed on an - "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY - KIND, either express or implied. See the License for the - specific language governing permissions and limitations - under the License. -*/ - -var build = require('./build'); -var Adb = require('./Adb'); -var CordovaError = require('cordova-common').CordovaError; - -/** - * Returns a promise for the list of the device ID's found - */ -module.exports.list = async () => { - return (await Adb.devices()) - .filter(id => !id.startsWith('emulator-')); -}; - -module.exports.resolveTarget = function (target) { - return this.list().then(function (device_list) { - if (!device_list || !device_list.length) { - return Promise.reject(new CordovaError('Failed to deploy to device, no devices found.')); - } - // default device - target = target || device_list[0]; - - if (device_list.indexOf(target) < 0) { - return Promise.reject(new CordovaError('ERROR: Unable to find target \'' + target + '\'.')); - } - - return build.detectArchitecture(target).then(function (arch) { - return { target: target, arch: arch, isEmulator: false }; - }); - }); -}; diff --git a/bin/templates/cordova/lib/emulator.js b/bin/templates/cordova/lib/emulator.js index 7c9ee410d4..7a5fbd1702 100644 --- a/bin/templates/cordova/lib/emulator.js +++ b/bin/templates/cordova/lib/emulator.js @@ -20,7 +20,6 @@ const execa = require('execa'); const fs = require('fs-extra'); var android_versions = require('android-versions'); -var build = require('./build'); var path = require('path'); var Adb = require('./Adb'); var events = require('cordova-common').events; @@ -349,21 +348,3 @@ module.exports.wait_for_boot = function (emulator_id, time_remaining) { } }); }; - -module.exports.resolveTarget = function (target) { - return this.list_started().then(function (emulator_list) { - if (emulator_list.length < 1) { - return Promise.reject(new CordovaError('No running Android emulators found, please start an emulator before deploying your project.')); - } - - // default emulator - target = target || emulator_list[0]; - if (emulator_list.indexOf(target) < 0) { - return Promise.reject(new CordovaError('Unable to find target \'' + target + '\'. Failed to deploy to emulator.')); - } - - return build.detectArchitecture(target).then(function (arch) { - return { target: target, arch: arch, isEmulator: true }; - }); - }); -}; diff --git a/bin/templates/cordova/lib/install-device b/bin/templates/cordova/lib/install-device index 2d5a8b1787..1340a4d521 100755 --- a/bin/templates/cordova/lib/install-device +++ b/bin/templates/cordova/lib/install-device @@ -19,21 +19,21 @@ under the License. */ -const { install } = require('./target'); -var device = require('./device'); +const { resolve, install } = require('./target'); + var args = process.argv; +const targetSpec = { type: 'device' }; if (args.length > 2) { - var install_target; if (args[2].substring(0, 9) === '--target=') { - install_target = args[2].substring(9, args[2].length); + targetSpec.id = args[2].substring(9, args[2].length); } else { console.error('ERROR : argument \'' + args[2] + '\' not recognized.'); process.exit(2); } } -device.resolveTarget(install_target).then(install).catch(err => { +resolve(targetSpec).then(install).catch(err => { console.error('ERROR: ' + err); process.exit(2); }); diff --git a/bin/templates/cordova/lib/install-emulator b/bin/templates/cordova/lib/install-emulator index d67997ed6e..f73cd050b4 100755 --- a/bin/templates/cordova/lib/install-emulator +++ b/bin/templates/cordova/lib/install-emulator @@ -19,21 +19,21 @@ under the License. */ -const { install } = require('./target'); -var emulator = require('./emulator'); +const { resolve, install } = require('./target'); + var args = process.argv; +const targetSpec = { type: 'emulator' }; -var install_target; if (args.length > 2) { if (args[2].substring(0, 9) === '--target=') { - install_target = args[2].substring(9, args[2].length); + targetSpec.id = args[2].substring(9, args[2].length); } else { console.error('ERROR : argument \'' + args[2] + '\' not recognized.'); process.exit(2); } } -emulator.resolveTarget(install_target).then(install).catch(err => { +resolve(targetSpec).then(install).catch(err => { console.error('ERROR: ' + err); process.exit(2); }); diff --git a/bin/templates/cordova/lib/list-devices b/bin/templates/cordova/lib/list-devices index 339c6658d5..ed8e111f3f 100755 --- a/bin/templates/cordova/lib/list-devices +++ b/bin/templates/cordova/lib/list-devices @@ -19,14 +19,16 @@ under the License. */ -var devices = require('./device'); +const { list } = require('./target'); // Usage support for when args are given require('./check_reqs').check_android().then(function () { - devices.list().then(function (device_list) { - device_list && device_list.forEach(function (dev) { - console.log(dev); - }); + list().then(targets => { + const deviceIds = targets + .filter(({ type }) => type === 'device') + .map(({ id }) => id); + + console.log(deviceIds.join('\n')); }, function (err) { console.error('ERROR: ' + err); process.exit(2); diff --git a/bin/templates/cordova/lib/run.js b/bin/templates/cordova/lib/run.js index 05ff775f2a..1741659261 100644 --- a/bin/templates/cordova/lib/run.js +++ b/bin/templates/cordova/lib/run.js @@ -19,22 +19,30 @@ var path = require('path'); var emulator = require('./emulator'); -var device = require('./device'); const target = require('./target'); var PackageType = require('./PackageType'); -const { CordovaError, events } = require('cordova-common'); +const { events } = require('cordova-common'); -function getInstallTarget (runOptions) { - var install_target; +/** + * Builds a target spec from a runOptions object + * + * @param {{target?: string, device?: boolean, emulator?: boolean}} runOptions + * @return {target.TargetSpec} + */ +function buildTargetSpec (runOptions) { + const spec = {}; if (runOptions.target) { - install_target = runOptions.target; + spec.id = runOptions.target; } else if (runOptions.device) { - install_target = '--device'; + spec.type = 'device'; } else if (runOptions.emulator) { - install_target = '--emulator'; + spec.type = 'emulator'; } + return spec; +} - return install_target; +function formatResolvedTarget ({ id, type }) { + return `${type} ${id}`; } /** @@ -51,55 +59,11 @@ module.exports.run = function (runOptions) { runOptions = runOptions || {}; var self = this; - var install_target = getInstallTarget(runOptions); + const spec = buildTargetSpec(runOptions); + + return target.resolve(spec).then(function (resolvedTarget) { + events.emit('log', `Deploying to ${formatResolvedTarget(resolvedTarget)}`); - return Promise.resolve().then(function () { - if (!install_target) { - // no target given, deploy to device if available, otherwise use the emulator. - return device.list().then(function (device_list) { - if (device_list.length > 0) { - events.emit('warn', 'No target specified, deploying to device \'' + device_list[0] + '\'.'); - install_target = device_list[0]; - } else { - events.emit('warn', 'No target specified and no devices found, deploying to emulator'); - install_target = '--emulator'; - } - }); - } - }).then(function () { - if (install_target === '--device') { - return device.resolveTarget(null); - } else if (install_target === '--emulator') { - // Give preference to any already started emulators. Else, start one. - return emulator.list_started().then(function (started) { - return started && started.length > 0 ? started[0] : emulator.start(); - }).then(function (emulatorId) { - return emulator.resolveTarget(emulatorId); - }); - } - // They specified a specific device/emulator ID. - return device.list().then(function (devices) { - if (devices.indexOf(install_target) > -1) { - return device.resolveTarget(install_target); - } - return emulator.list_started().then(function (started_emulators) { - if (started_emulators.indexOf(install_target) > -1) { - return emulator.resolveTarget(install_target); - } - return emulator.list_images().then(function (avds) { - // if target emulator isn't started, then start it. - for (var avd in avds) { - if (avds[avd].name === install_target) { - return emulator.start(install_target).then(function (emulatorId) { - return emulator.resolveTarget(emulatorId); - }); - } - } - return Promise.reject(new CordovaError(`Target '${install_target}' not found, unable to run project`)); - }); - }); - }); - }).then(function (resolvedTarget) { return new Promise((resolve) => { const buildOptions = require('./build').parseBuildOptions(runOptions, null, self.root); @@ -112,7 +76,7 @@ module.exports.run = function (runOptions) { resolve(self._builder.fetchBuildResults(buildOptions.buildType, buildOptions.arch)); }).then(async function (buildResults) { - if (resolvedTarget && resolvedTarget.isEmulator) { + if (resolvedTarget.type === 'emulator') { await emulator.wait_for_boot(resolvedTarget.id); } diff --git a/bin/templates/cordova/lib/target.js b/bin/templates/cordova/lib/target.js index 1494144af1..ea03640109 100644 --- a/bin/templates/cordova/lib/target.js +++ b/bin/templates/cordova/lib/target.js @@ -18,17 +18,97 @@ */ const path = require('path'); +const { inspect } = require('util'); const Adb = require('./Adb'); const build = require('./build'); +const emulator = require('./emulator'); const AndroidManifest = require('./AndroidManifest'); +const { compareBy } = require('./utils'); const { retryPromise } = require('./retry'); -const { events } = require('cordova-common'); +const { events, CordovaError } = require('cordova-common'); const INSTALL_COMMAND_TIMEOUT = 5 * 60 * 1000; const NUM_INSTALL_RETRIES = 3; const EXEC_KILL_SIGNAL = 'SIGKILL'; -exports.install = async function ({ target, arch, isEmulator }, buildResults) { +/** + * @typedef { 'device' | 'emulator' } TargetType + * @typedef { { id: string, type: TargetType } } Target + * @typedef { { id?: string, type?: TargetType } } TargetSpec + */ + +/** + * Returns a list of available targets (connected devices & started emulators) + * + * @return {Promise} + */ +exports.list = async () => { + return (await Adb.devices()) + .map(id => ({ + id, + type: id.startsWith('emulator-') ? 'emulator' : 'device' + })); +}; + +/** + * @param {TargetSpec?} spec + * @return {Promise} + */ +async function resolveToOnlineTarget (spec = {}) { + const targetList = await exports.list(); + if (targetList.length === 0) return null; + + // Sort by type: devices first, then emulators. + targetList.sort(compareBy(t => t.type)); + + // Find first matching target for spec. {} matches any target. + return targetList.find(target => + Object.keys(spec).every(k => spec[k] === target[k]) + ) || null; +} + +async function isEmulatorName (name) { + const emus = await emulator.list_images(); + return emus.some(avd => avd.name === name); +} + +/** + * @param {TargetSpec?} spec + * @return {Promise} + */ +async function resolveToOfflineEmulator (spec = {}) { + if (spec.type === 'device') return null; + if (spec.id && !(await isEmulatorName(spec.id))) return null; + + // try to start an emulator with name spec.id + // if spec.id is undefined, picks best match regarding target API + const emulatorId = await emulator.start(spec.id); + + return { id: emulatorId, type: 'emulator' }; +} + +/** + * @param {TargetSpec?} spec + * @return {Promise} + */ +exports.resolve = async (spec = {}) => { + events.emit('verbose', `Trying to find target matching ${inspect(spec)}`); + + const resolvedTarget = + (await resolveToOnlineTarget(spec)) || + (await resolveToOfflineEmulator(spec)); + + if (!resolvedTarget) { + throw new CordovaError(`Could not find target matching ${inspect(spec)}`); + } + + return { + ...resolvedTarget, + arch: await build.detectArchitecture(resolvedTarget.id) + }; +}; + +exports.install = async function ({ id: target, arch, type }, buildResults) { const apk_path = build.findBestApkForArchitecture(buildResults, arch); const manifest = new AndroidManifest(path.join(__dirname, '../../app/src/main/AndroidManifest.xml')); const pkgName = manifest.getPackageId(); @@ -56,7 +136,7 @@ exports.install = async function ({ target, arch, isEmulator }, buildResults) { } } - if (isEmulator) { + if (type === 'emulator') { // Work around sporadic emulator hangs: http://issues.apache.org/jira/browse/CB-9119 await retryPromise(NUM_INSTALL_RETRIES, () => doInstall({ timeout: INSTALL_COMMAND_TIMEOUT, diff --git a/spec/unit/device.spec.js b/spec/unit/device.spec.js deleted file mode 100644 index 499bb636bc..0000000000 --- a/spec/unit/device.spec.js +++ /dev/null @@ -1,106 +0,0 @@ -/* - Licensed to the Apache Software Foundation (ASF) under one - or more contributor license agreements. See the NOTICE file - distributed with this work for additional information - regarding copyright ownership. The ASF licenses this file - to you under the Apache License, Version 2.0 (the - "License"); you may not use this file except in compliance - with the License. You may obtain a copy of the License at - - http://www.apache.org/licenses/LICENSE-2.0 - - Unless required by applicable law or agreed to in writing, - software distributed under the License is distributed on an - "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY - KIND, either express or implied. See the License for the - specific language governing permissions and limitations - under the License. -*/ - -const rewire = require('rewire'); - -const CordovaError = require('cordova-common').CordovaError; - -describe('device', () => { - const DEVICE_LIST = ['device1', 'device2', 'device3']; - let AdbSpy; - let device; - - beforeEach(() => { - device = rewire('../../bin/templates/cordova/lib/device'); - AdbSpy = jasmine.createSpyObj('Adb', ['devices', 'install', 'shell', 'start', 'uninstall']); - device.__set__('Adb', AdbSpy); - }); - - describe('list', () => { - it('should return a list of all connected devices', () => { - AdbSpy.devices.and.resolveTo(['emulator-5556', '123a76565509e124']); - - return device.list().then(list => { - expect(list).toEqual(['123a76565509e124']); - }); - }); - }); - - describe('resolveTarget', () => { - let buildSpy; - - beforeEach(() => { - buildSpy = jasmine.createSpyObj('build', ['detectArchitecture']); - buildSpy.detectArchitecture.and.returnValue(Promise.resolve()); - device.__set__('build', buildSpy); - - spyOn(device, 'list').and.returnValue(Promise.resolve(DEVICE_LIST)); - }); - - it('should select the first device to be the target if none is specified', () => { - return device.resolveTarget().then(deviceInfo => { - expect(deviceInfo.target).toBe(DEVICE_LIST[0]); - }); - }); - - it('should use the given target instead of the default', () => { - return device.resolveTarget(DEVICE_LIST[2]).then(deviceInfo => { - expect(deviceInfo.target).toBe(DEVICE_LIST[2]); - }); - }); - - it('should set emulator to false', () => { - return device.resolveTarget().then(deviceInfo => { - expect(deviceInfo.isEmulator).toBe(false); - }); - }); - - it('should throw an error if there are no devices', () => { - device.list.and.returnValue(Promise.resolve([])); - - return device.resolveTarget().then( - () => fail('Unexpectedly resolved'), - err => { - expect(err).toEqual(jasmine.any(CordovaError)); - } - ); - }); - - it('should throw an error if the specified target does not exist', () => { - return device.resolveTarget('nonexistent-target').then( - () => fail('Unexpectedly resolved'), - err => { - expect(err).toEqual(jasmine.any(CordovaError)); - } - ); - }); - - it('should detect the architecture and return it with the device info', () => { - const target = DEVICE_LIST[1]; - const arch = 'unittestarch'; - - buildSpy.detectArchitecture.and.returnValue(Promise.resolve(arch)); - - return device.resolveTarget(target).then(deviceInfo => { - expect(buildSpy.detectArchitecture).toHaveBeenCalledWith(target); - expect(deviceInfo.arch).toBe(arch); - }); - }); - }); -}); diff --git a/spec/unit/emulator.spec.js b/spec/unit/emulator.spec.js index c15eaee843..35079268c9 100644 --- a/spec/unit/emulator.spec.js +++ b/spec/unit/emulator.spec.js @@ -26,7 +26,6 @@ const CordovaError = require('cordova-common').CordovaError; const check_reqs = require('../../bin/templates/cordova/lib/check_reqs'); describe('emulator', () => { - const EMULATOR_LIST = ['emulator-5555', 'emulator-5556', 'emulator-5557']; let emu; beforeEach(() => { @@ -529,50 +528,4 @@ describe('emulator', () => { }); }); }); - - describe('resolveTarget', () => { - const arch = 'arm7-test'; - - beforeEach(() => { - const buildSpy = jasmine.createSpyObj('build', ['detectArchitecture']); - buildSpy.detectArchitecture.and.returnValue(Promise.resolve(arch)); - emu.__set__('build', buildSpy); - - spyOn(emu, 'list_started').and.returnValue(Promise.resolve(EMULATOR_LIST)); - }); - - it('should throw an error if there are no running emulators', () => { - emu.list_started.and.returnValue(Promise.resolve([])); - - return emu.resolveTarget().then( - () => fail('Unexpectedly resolved'), - err => { - expect(err).toEqual(jasmine.any(CordovaError)); - } - ); - }); - - it('should throw an error if the requested emulator is not running', () => { - const targetEmulator = 'unstarted-emu'; - - return emu.resolveTarget(targetEmulator).then( - () => fail('Unexpectedly resolved'), - err => { - expect(err.message).toContain(targetEmulator); - } - ); - }); - - it('should return info on the first running emulator if none is specified', () => { - return emu.resolveTarget().then(emulatorInfo => { - expect(emulatorInfo.target).toBe(EMULATOR_LIST[0]); - }); - }); - - it('should return the emulator info', () => { - return emu.resolveTarget(EMULATOR_LIST[1]).then(emulatorInfo => { - expect(emulatorInfo).toEqual({ target: EMULATOR_LIST[1], arch, isEmulator: true }); - }); - }); - }); }); diff --git a/spec/unit/run.spec.js b/spec/unit/run.spec.js index 287905abfb..e780e68466 100644 --- a/spec/unit/run.spec.js +++ b/spec/unit/run.spec.js @@ -25,45 +25,38 @@ describe('run', () => { beforeEach(() => { run = rewire('../../bin/templates/cordova/lib/run'); + run.__set__({ + events: jasmine.createSpyObj('eventsSpy', ['emit']) + }); }); - describe('getInstallTarget', () => { - const targetOpts = { target: 'emu' }; - const deviceOpts = { device: true }; - const emulatorOpts = { emulator: true }; - const emptyOpts = {}; - + describe('buildTargetSpec', () => { it('Test#001 : should select correct target based on the run opts', () => { - const getInstallTarget = run.__get__('getInstallTarget'); - expect(getInstallTarget(targetOpts)).toBe('emu'); - expect(getInstallTarget(deviceOpts)).toBe('--device'); - expect(getInstallTarget(emulatorOpts)).toBe('--emulator'); - expect(getInstallTarget(emptyOpts)).toBeUndefined(); + const buildTargetSpec = run.__get__('buildTargetSpec'); + + expect(buildTargetSpec({ target: 'emu' })).toEqual({ id: 'emu' }); + expect(buildTargetSpec({ device: true })).toEqual({ type: 'device' }); + expect(buildTargetSpec({ emulator: true })).toEqual({ type: 'emulator' }); + expect(buildTargetSpec({})).toEqual({}); }); }); describe('run method', () => { - let deviceSpyObj; - let emulatorSpyObj; - let targetSpyObj; - let eventsSpyObj; - let getInstallTargetSpy; + let targetSpyObj, emulatorSpyObj, resolvedTarget; beforeEach(() => { - deviceSpyObj = jasmine.createSpyObj('deviceSpy', ['list', 'resolveTarget']); - emulatorSpyObj = jasmine.createSpyObj('emulatorSpy', ['list_images', 'list_started', 'resolveTarget', 'start', 'wait_for_boot']); - eventsSpyObj = jasmine.createSpyObj('eventsSpy', ['emit']); - getInstallTargetSpy = jasmine.createSpy('getInstallTargetSpy'); + resolvedTarget = { id: 'dev1', type: 'device', arch: 'atari' }; - targetSpyObj = jasmine.createSpyObj('target', ['install']); + targetSpyObj = jasmine.createSpyObj('deviceSpy', ['resolve', 'install']); + targetSpyObj.resolve.and.resolveTo(resolvedTarget); targetSpyObj.install.and.resolveTo(); + emulatorSpyObj = jasmine.createSpyObj('emulatorSpy', ['wait_for_boot']); + emulatorSpyObj.wait_for_boot.and.resolveTo(); + run.__set__({ - device: deviceSpyObj, - emulator: emulatorSpyObj, target: targetSpyObj, - events: eventsSpyObj, - getInstallTarget: getInstallTargetSpy + emulator: emulatorSpyObj }); // run needs `this` to behave like an Api instance @@ -72,152 +65,19 @@ describe('run', () => { }); }); - it('should run on default device when no target arguments are specified', () => { - const deviceList = ['testDevice1', 'testDevice2']; - - getInstallTargetSpy.and.returnValue(null); - deviceSpyObj.list.and.returnValue(Promise.resolve(deviceList)); - - return run.run().then(() => { - expect(deviceSpyObj.resolveTarget).toHaveBeenCalledWith(deviceList[0]); - }); - }); - - it('should run on emulator when no target arguments are specified, and no devices are found', () => { - const deviceList = []; - - getInstallTargetSpy.and.returnValue(null); - deviceSpyObj.list.and.returnValue(Promise.resolve(deviceList)); - emulatorSpyObj.list_started.and.returnValue(Promise.resolve([])); - - return run.run().then(() => { - expect(emulatorSpyObj.list_started).toHaveBeenCalled(); - }); - }); - - it('should run on default device when device is requested, but none specified', () => { - getInstallTargetSpy.and.returnValue('--device'); - - return run.run().then(() => { - // Default device is selected by calling device.resolveTarget(null) - expect(deviceSpyObj.resolveTarget).toHaveBeenCalledWith(null); - }); - }); - - it('should run on a running emulator if one exists', () => { - const emulatorList = ['emulator1', 'emulator2']; - - getInstallTargetSpy.and.returnValue('--emulator'); - emulatorSpyObj.list_started.and.returnValue(Promise.resolve(emulatorList)); - - return run.run().then(() => { - expect(emulatorSpyObj.resolveTarget).toHaveBeenCalledWith(emulatorList[0]); - }); - }); - - it('should start an emulator and run on that if none is running', () => { - const emulatorList = []; - const defaultEmulator = 'default-emu'; - - getInstallTargetSpy.and.returnValue('--emulator'); - emulatorSpyObj.list_started.and.returnValue(Promise.resolve(emulatorList)); - emulatorSpyObj.start.and.returnValue(Promise.resolve(defaultEmulator)); - + it('should install on target after build', () => { return run.run().then(() => { - expect(emulatorSpyObj.resolveTarget).toHaveBeenCalledWith(defaultEmulator); - }); - }); - - it('should run on a named device if it is specified', () => { - const deviceList = ['device1', 'device2', 'device3']; - getInstallTargetSpy.and.returnValue(deviceList[1]); - - deviceSpyObj.list.and.returnValue(Promise.resolve(deviceList)); - - return run.run().then(() => { - expect(deviceSpyObj.resolveTarget).toHaveBeenCalledWith(deviceList[1]); - }); - }); - - it('should run on a named emulator if it is specified', () => { - const startedEmulatorList = ['emu1', 'emu2', 'emu3']; - getInstallTargetSpy.and.returnValue(startedEmulatorList[2]); - - deviceSpyObj.list.and.returnValue(Promise.resolve([])); - emulatorSpyObj.list_started.and.returnValue(Promise.resolve(startedEmulatorList)); - - return run.run().then(() => { - expect(emulatorSpyObj.resolveTarget).toHaveBeenCalledWith(startedEmulatorList[2]); - }); - }); - - it('should start named emulator and then run on it if it is specified', () => { - const emulatorList = [ - { name: 'emu1', id: 1 }, - { name: 'emu2', id: 2 }, - { name: 'emu3', id: 3 } - ]; - getInstallTargetSpy.and.returnValue(emulatorList[2].name); - - deviceSpyObj.list.and.returnValue(Promise.resolve([])); - emulatorSpyObj.list_started.and.returnValue(Promise.resolve([])); - emulatorSpyObj.list_images.and.returnValue(Promise.resolve(emulatorList)); - emulatorSpyObj.start.and.returnValue(Promise.resolve(emulatorList[2].id)); - - return run.run().then(() => { - expect(emulatorSpyObj.start).toHaveBeenCalledWith(emulatorList[2].name); - expect(emulatorSpyObj.resolveTarget).toHaveBeenCalledWith(emulatorList[2].id); - }); - }); - - it('should throw an error if target is specified but does not exist', () => { - const emulatorList = [{ name: 'emu1', id: 1 }]; - const deviceList = ['device1']; - const target = 'nonexistentdevice'; - getInstallTargetSpy.and.returnValue(target); - - deviceSpyObj.list.and.returnValue(Promise.resolve(deviceList)); - emulatorSpyObj.list_started.and.returnValue(Promise.resolve([])); - emulatorSpyObj.list_images.and.returnValue(Promise.resolve(emulatorList)); - - return run.run().then( - () => fail('Expected error to be thrown'), - err => expect(err.message).toContain(target) - ); - }); - - it('should install on device after build', () => { - const deviceTarget = { target: 'device1', isEmulator: false }; - getInstallTargetSpy.and.returnValue('--device'); - deviceSpyObj.resolveTarget.and.returnValue(deviceTarget); - - return run.run().then(() => { - expect(targetSpyObj.install).toHaveBeenCalledWith(deviceTarget, { apkPaths: [], buildType: 'debug' }); - }); - }); - - it('should install on emulator after build', () => { - const emulatorTarget = { target: 'emu1', isEmulator: true }; - getInstallTargetSpy.and.returnValue('--emulator'); - emulatorSpyObj.list_started.and.returnValue(Promise.resolve([emulatorTarget.target])); - emulatorSpyObj.resolveTarget.and.returnValue(emulatorTarget); - emulatorSpyObj.wait_for_boot.and.returnValue(Promise.resolve()); - - return run.run().then(() => { - expect(targetSpyObj.install).toHaveBeenCalledWith(emulatorTarget, { apkPaths: [], buildType: 'debug' }); + expect(targetSpyObj.install).toHaveBeenCalledWith( + resolvedTarget, + { apkPaths: [], buildType: 'debug' } + ); }); }); it('should fail with the error message if --packageType=bundle setting is used', () => { - const deviceList = ['testDevice1', 'testDevice2']; - getInstallTargetSpy.and.returnValue(null); - - deviceSpyObj.list.and.returnValue(Promise.resolve(deviceList)); - - return run.run({ argv: ['--packageType=bundle'] }).then( - () => fail('Expected error to be thrown'), - err => expect(err).toContain('Package type "bundle" is not supported during cordova run.') - ); + targetSpyObj.resolve.and.resolveTo(resolvedTarget); + return expectAsync(run.run({ argv: ['--packageType=bundle'] })) + .toBeRejectedWith(jasmine.stringMatching(/Package type "bundle" is not supported/)); }); }); diff --git a/spec/unit/target.spec.js b/spec/unit/target.spec.js index 345cda1ad7..0e0fcc6ae1 100644 --- a/spec/unit/target.spec.js +++ b/spec/unit/target.spec.js @@ -27,6 +27,193 @@ describe('target', () => { target = rewire('../../bin/templates/cordova/lib/target'); }); + describe('list', () => { + it('should return available targets from Adb.devices', () => { + const AdbSpy = jasmine.createSpyObj('Adb', ['devices']); + AdbSpy.devices.and.resolveTo(['emulator-5556', '123a76565509e124']); + target.__set__('Adb', AdbSpy); + + return target.list().then(emus => { + expect(emus).toEqual([ + { id: 'emulator-5556', type: 'emulator' }, + { id: '123a76565509e124', type: 'device' } + ]); + }); + }); + }); + + describe('resolveToOnlineTarget', () => { + let resolveToOnlineTarget, emus, devs; + + beforeEach(() => { + resolveToOnlineTarget = target.__get__('resolveToOnlineTarget'); + + emus = [ + { id: 'emu1', type: 'emulator' }, + { id: 'emu2', type: 'emulator' } + ]; + devs = [ + { id: 'dev1', type: 'device' }, + { id: 'dev2', type: 'device' } + ]; + + spyOn(target, 'list').and.returnValue([...emus, ...devs]); + }); + + it('should return first device when no target arguments are specified', async () => { + return resolveToOnlineTarget().then(result => { + expect(result.id).toBe('dev1'); + }); + }); + + it('should return first emulator when no target arguments are specified and no devices are found', async () => { + target.list.and.resolveTo(emus); + return resolveToOnlineTarget().then(result => { + expect(result.id).toBe('emu1'); + }); + }); + + it('should return first device when type device is specified', async () => { + return resolveToOnlineTarget({ type: 'device' }).then(result => { + expect(result.id).toBe('dev1'); + }); + }); + + it('should return first running emulator when type emulator is specified', async () => { + return resolveToOnlineTarget({ type: 'emulator' }).then(result => { + expect(result.id).toBe('emu1'); + }); + }); + + it('should return a device that matches given ID', async () => { + return resolveToOnlineTarget({ id: 'dev2' }).then(result => { + expect(result.id).toBe('dev2'); + }); + }); + + it('should return a running emulator that matches given ID', async () => { + return resolveToOnlineTarget({ id: 'emu2' }).then(result => { + expect(result.id).toBe('emu2'); + }); + }); + + it('should return null if there are no online targets', async () => { + target.list.and.resolveTo([]); + return expectAsync(resolveToOnlineTarget()) + .toBeResolvedTo(null); + }); + + it('should return null if no target matches given ID', async () => { + return expectAsync(resolveToOnlineTarget({ id: 'foo' })) + .toBeResolvedTo(null); + }); + + it('should return null if no target matches given type', async () => { + target.list.and.resolveTo(devs); + return expectAsync(resolveToOnlineTarget({ type: 'emulator' })) + .toBeResolvedTo(null); + }); + }); + + describe('resolveToOfflineEmulator', () => { + const emuId = 'emulator-5554'; + let resolveToOfflineEmulator, emulatorSpyObj; + + beforeEach(() => { + resolveToOfflineEmulator = target.__get__('resolveToOfflineEmulator'); + + emulatorSpyObj = jasmine.createSpyObj('emulatorSpy', ['start']); + emulatorSpyObj.start.and.resolveTo(emuId); + + target.__set__({ + emulator: emulatorSpyObj, + isEmulatorName: name => name.startsWith('emu') + }); + }); + + it('should start an emulator and run on that if none is running', () => { + return resolveToOfflineEmulator().then(result => { + expect(result).toEqual({ id: emuId, type: 'emulator' }); + expect(emulatorSpyObj.start).toHaveBeenCalled(); + }); + }); + + it('should start named emulator and then run on it if it is specified', () => { + return resolveToOfflineEmulator({ id: 'emu3' }).then(result => { + expect(result).toEqual({ id: emuId, type: 'emulator' }); + expect(emulatorSpyObj.start).toHaveBeenCalledWith('emu3'); + }); + }); + + it('should return null if given ID is not an avd name', () => { + return resolveToOfflineEmulator({ id: 'dev1' }).then(result => { + expect(result).toBe(null); + expect(emulatorSpyObj.start).not.toHaveBeenCalled(); + }); + }); + + it('should return null if given type is not emulator', () => { + return resolveToOfflineEmulator({ type: 'device' }).then(result => { + expect(result).toBe(null); + expect(emulatorSpyObj.start).not.toHaveBeenCalled(); + }); + }); + }); + + describe('resolve', () => { + let resolveToOnlineTarget, resolveToOfflineEmulator; + + beforeEach(() => { + resolveToOnlineTarget = jasmine.createSpy('resolveToOnlineTarget') + .and.resolveTo(null); + + resolveToOfflineEmulator = jasmine.createSpy('resolveToOfflineEmulator') + .and.resolveTo(null); + + target.__set__({ + resolveToOnlineTarget, + resolveToOfflineEmulator, + build: { detectArchitecture: id => id + '-arch' } + }); + }); + + it('should delegate to resolveToOnlineTarget', () => { + const spec = { type: 'device' }; + resolveToOnlineTarget.and.resolveTo({ id: 'dev1', type: 'device' }); + + return target.resolve(spec).then(result => { + expect(result.id).toBe('dev1'); + expect(resolveToOnlineTarget).toHaveBeenCalledWith(spec); + expect(resolveToOfflineEmulator).not.toHaveBeenCalled(); + }); + }); + + it('should delegate to resolveToOfflineEmulator if resolveToOnlineTarget fails', () => { + const spec = { type: 'emulator' }; + resolveToOfflineEmulator.and.resolveTo({ id: 'emu1', type: 'emulator' }); + + return target.resolve(spec).then(result => { + expect(result.id).toBe('emu1'); + expect(resolveToOnlineTarget).toHaveBeenCalledWith(spec); + expect(resolveToOfflineEmulator).toHaveBeenCalledWith(spec); + }); + }); + + it('should add the target arch', () => { + const spec = { type: 'device' }; + resolveToOnlineTarget.and.resolveTo({ id: 'dev1', type: 'device' }); + + return target.resolve(spec).then(result => { + expect(result.arch).toBe('dev1-arch'); + }); + }); + + it('should throw an error if target cannot be resolved', () => { + return expectAsync(target.resolve()) + .toBeRejectedWithError(/Could not find target matching/); + }); + }); + describe('install', () => { let AndroidManifestSpy; let AndroidManifestFns; @@ -36,7 +223,7 @@ describe('target', () => { let installTarget; beforeEach(() => { - installTarget = { target: 'emulator-5556', isEmulator: true, arch: 'atari' }; + installTarget = { id: 'emulator-5556', type: 'emulator', arch: 'atari' }; buildSpy = jasmine.createSpyObj('build', ['findBestApkForArchitecture']); target.__set__('build', buildSpy); @@ -60,7 +247,7 @@ describe('target', () => { it('should install to the passed target', () => { return target.install(installTarget, {}).then(() => { - expect(AdbSpy.install.calls.argsFor(0)[0]).toBe(installTarget.target); + expect(AdbSpy.install.calls.argsFor(0)[0]).toBe(installTarget.id); }); }); @@ -108,7 +295,7 @@ describe('target', () => { it('should unlock the screen on device', () => { return target.install(installTarget, {}).then(() => { - expect(AdbSpy.shell).toHaveBeenCalledWith(installTarget.target, 'input keyevent 82'); + expect(AdbSpy.shell).toHaveBeenCalledWith(installTarget.id, 'input keyevent 82'); }); }); @@ -119,7 +306,7 @@ describe('target', () => { AndroidManifestGetActivitySpy.getName.and.returnValue(activityName); return target.install(installTarget, {}).then(() => { - expect(AdbSpy.start).toHaveBeenCalledWith(installTarget.target, `${packageId}/.${activityName}`); + expect(AdbSpy.start).toHaveBeenCalledWith(installTarget.id, `${packageId}/.${activityName}`); }); }); });