From f81013b6d8377d8cc1c5ae03a4a081d1e258f2aa Mon Sep 17 00:00:00 2001 From: Darryl Pogue Date: Fri, 30 Aug 2024 00:52:07 -0700 Subject: [PATCH] feat!: Better Catalyst build support (#1313) * feat!: Support for Catalyst builds `cordova build ios --device --target=mac ...` Output is to `build/Debug-maccatalyst` * fix: Disable iPad-on-Mac builds in favour of Catalyst Xcode warns that only one can be enabled at a time, so we need to pick one, and Catalyst seems like the better option for a proper macOS app experience. * feat!: Enable compiling for Apple Vision platform * chore(ci): Add unit tests for run and Catalyst stuff * fix(build): Don't check for ios-deploy at build time If neither `--device` nor `--emulator` are specified for the build command, it will check for a connected device and assume `--device` if one is found. However, it was also checking for the availability of the ios-deploy tool which is used to deploy to a connected device. If we're just building, we don't need to check for a deploy tool. The run command already has this check to ensure that ios-deploy is available before actually trying to deploy. Closes GH-420. Closes GH-677. --- .../CordovaLib.xcodeproj/project.pbxproj | 20 ++- lib/build.js | 51 ++++-- lib/run.js | 82 +++++++--- .../project.pbxproj | 8 +- tests/spec/unit/build.spec.js | 46 +++--- tests/spec/unit/lib/run.spec.js | 74 --------- tests/spec/unit/prepare.spec.js | 72 ++++----- tests/spec/unit/run.spec.js | 149 ++++++++++++++++++ 8 files changed, 328 insertions(+), 174 deletions(-) delete mode 100644 tests/spec/unit/lib/run.spec.js create mode 100644 tests/spec/unit/run.spec.js diff --git a/CordovaLib/CordovaLib.xcodeproj/project.pbxproj b/CordovaLib/CordovaLib.xcodeproj/project.pbxproj index 6b95e5dad..14e66bea4 100644 --- a/CordovaLib/CordovaLib.xcodeproj/project.pbxproj +++ b/CordovaLib/CordovaLib.xcodeproj/project.pbxproj @@ -638,7 +638,10 @@ MODULE_VERIFIER_SUPPORTED_LANGUAGES = "objective-c objective-c++"; MODULE_VERIFIER_SUPPORTED_LANGUAGE_STANDARDS = "gnu11 gnu++14"; PUBLIC_HEADERS_FOLDER_PATH = include/Cordova; - TARGETED_DEVICE_FAMILY = "1,2"; + SUPPORTED_PLATFORMS = "iphoneos iphonesimulator xros xrsimulator"; + SUPPORTS_MACCATALYST = YES; + TARGETED_DEVICE_FAMILY = "1,2,7"; + XROS_DEPLOYMENT_TARGET = 1.0; }; name = Debug; }; @@ -649,7 +652,10 @@ MODULE_VERIFIER_SUPPORTED_LANGUAGES = "objective-c objective-c++"; MODULE_VERIFIER_SUPPORTED_LANGUAGE_STANDARDS = "gnu11 gnu++14"; PUBLIC_HEADERS_FOLDER_PATH = include/Cordova; - TARGETED_DEVICE_FAMILY = "1,2"; + SUPPORTED_PLATFORMS = "iphoneos iphonesimulator xros xrsimulator"; + SUPPORTS_MACCATALYST = YES; + TARGETED_DEVICE_FAMILY = "1,2,7"; + XROS_DEPLOYMENT_TARGET = 1.0; }; name = Release; }; @@ -809,9 +815,12 @@ MODULE_VERIFIER_SUPPORTED_LANGUAGE_STANDARDS = "gnu11 gnu++14"; PRODUCT_BUNDLE_IDENTIFIER = org.apache.cordova.Cordova; SKIP_INSTALL = NO; - TARGETED_DEVICE_FAMILY = "1,2"; + SUPPORTED_PLATFORMS = "iphoneos iphonesimulator xros xrsimulator"; + SUPPORTS_MACCATALYST = YES; + TARGETED_DEVICE_FAMILY = "1,2,7"; VERSIONING_SYSTEM = "apple-generic"; VERSION_INFO_PREFIX = ""; + XROS_DEPLOYMENT_TARGET = 1.0; }; name = Debug; }; @@ -832,9 +841,12 @@ MODULE_VERIFIER_SUPPORTED_LANGUAGE_STANDARDS = "gnu11 gnu++14"; PRODUCT_BUNDLE_IDENTIFIER = org.apache.cordova.Cordova; SKIP_INSTALL = NO; - TARGETED_DEVICE_FAMILY = "1,2"; + SUPPORTED_PLATFORMS = "iphoneos iphonesimulator xros xrsimulator"; + SUPPORTS_MACCATALYST = YES; + TARGETED_DEVICE_FAMILY = "1,2,7"; VERSIONING_SYSTEM = "apple-generic"; VERSION_INFO_PREFIX = ""; + XROS_DEPLOYMENT_TARGET = 1.0; }; name = Release; }; diff --git a/lib/build.js b/lib/build.js index 73d57fa7f..fbca27434 100644 --- a/lib/build.js +++ b/lib/build.js @@ -102,7 +102,7 @@ function getDefaultSimulatorTarget () { /** @returns {Promise} */ module.exports.run = function (buildOpts) { const projectPath = this.root; - let emulatorTarget = ''; + let emulatorTarget = 'iOS Device'; let projectName = ''; buildOpts = buildOpts || {}; @@ -115,6 +115,14 @@ module.exports.run = function (buildOpts) { return Promise.reject(new CordovaError('Cannot specify "device" and "emulator" options together.')); } + if (buildOpts.target && buildOpts.target.match(/mac/i)) { + buildOpts.catalyst = true; + buildOpts.device = true; + buildOpts.emulator = false; + + emulatorTarget = 'macOS Catalyst'; + } + if (buildOpts.buildConfig) { if (!fs.existsSync(buildOpts.buildConfig)) { return Promise.reject(new CordovaError(`Build config file does not exist: ${buildOpts.buildConfig}`)); @@ -133,15 +141,18 @@ module.exports.run = function (buildOpts) { } } - return require('./listDevices').run() - .then(devices => { - if (devices.length > 0 && !(buildOpts.emulator)) { - // we also explicitly set device flag in options as we pass - // those parameters to other api (build as an example) - buildOpts.device = true; - return check_reqs.check_ios_deploy(); + return Promise.resolve() + .then(() => { + if (!buildOpts.emulator && !buildOpts.catalyst) { + return require('./listDevices').run().then(devices => { + if (devices.length > 0) { + // we explicitly set device flag in options + buildOpts.device = true; + } + }); } - }).then(() => { + }) + .then(() => { // CB-12287: Determine the device we should target when building for a simulator if (!buildOpts.device) { let newTarget = buildOpts.target || ''; @@ -175,8 +186,8 @@ module.exports.run = function (buildOpts) { let extraConfig = ''; if (buildOpts.codeSignIdentity) { extraConfig += `CODE_SIGN_IDENTITY = ${buildOpts.codeSignIdentity}\n`; - extraConfig += `CODE_SIGN_IDENTITY[sdk=iphoneos*] = ${buildOpts.codeSignIdentity}\n`; } + if (buildOpts.provisioningProfile) { if (typeof buildOpts.provisioningProfile === 'string') { extraConfig += `PROVISIONING_PROFILE_SPECIFIER = ${buildOpts.provisioningProfile}\n`; @@ -225,7 +236,7 @@ module.exports.run = function (buildOpts) { const xcodebuildArgs = getXcodeBuildArgs(projectName, projectPath, configuration, emulatorTarget, buildOpts); return execa('xcodebuild', xcodebuildArgs, { cwd: projectPath, stdio: 'inherit' }); }).then(() => { - if (!buildOpts.device || buildOpts.noSign) { + if (!buildOpts.device || buildOpts.catalyst || buildOpts.noSign) { return; } @@ -336,7 +347,7 @@ function getXcodeBuildArgs (projectName, projectPath, configuration, emulatorTar } } - if (buildConfig.device) { + if (buildConfig.device && !buildConfig.catalyst) { options = [ '-workspace', customArgs.workspace || `${projectName}.xcworkspace`, '-scheme', customArgs.scheme || projectName, @@ -377,10 +388,20 @@ function getXcodeBuildArgs (projectName, projectPath, configuration, emulatorTar options = [ '-workspace', customArgs.workspace || `${projectName}.xcworkspace`, '-scheme', customArgs.scheme || projectName, - '-configuration', customArgs.configuration || configuration, - '-sdk', customArgs.sdk || 'iphonesimulator', - '-destination', customArgs.destination || `platform=iOS Simulator,name=${emulatorTarget}` + '-configuration', customArgs.configuration || configuration ]; + + if (buildConfig.catalyst) { + options = options.concat([ + '-destination', customArgs.destination || 'generic/platform=macOS,variant=Mac Catalyst' + ]); + } else { + options = options.concat([ + '-sdk', customArgs.sdk || 'iphonesimulator', + '-destination', customArgs.destination || `platform=iOS Simulator,name=${emulatorTarget}` + ]); + } + buildActions = ['build']; settings = [`SYMROOT=${path.join(projectPath, 'build')}`]; diff --git a/lib/run.js b/lib/run.js index 494439f00..a1899e8d7 100644 --- a/lib/run.js +++ b/lib/run.js @@ -41,32 +41,38 @@ module.exports.run = function (runOptions) { return module.exports.listDevices().then(() => module.exports.listEmulators()); } - let useDevice = !!runOptions.device; + const useCatalyst = runOptions.target && runOptions.target.match(/mac/i); + let useDevice = !!runOptions.device && !useCatalyst; const configuration = runOptions.release ? 'Release' : 'Debug'; - return require('./listDevices').run() - .then(devices => { - if (devices.length > 0 && !(runOptions.emulator)) { - useDevice = true; - // we also explicitly set device flag in options as we pass - // those parameters to other api (build as an example) - runOptions.device = true; - return check_reqs.check_ios_deploy(); + return Promise.resolve() + .then(() => { + if (!runOptions.emulator && !useCatalyst) { + return module.exports.execListDevices().then(devices => { + if (devices.length > 0) { + useDevice = true; + + // we also explicitly set device flag in options as we pass + // those parameters to other api (build as an example) + runOptions.device = true; + return check_reqs.check_ios_deploy(); + } + }); } - }).then(() => { + }) + .then(() => { if (!runOptions.nobuild) { return build.run(runOptions); - } else { - return Promise.resolve(); } - }).then(() => build.findXCodeProjectIn(projectPath)) + }) + .then(() => build.findXCodeProjectIn(projectPath)) .then(projectName => { - let appPath = path.join(projectPath, 'build', `${configuration}-iphonesimulator`, `${projectName}.app`); - const buildOutputDir = path.join(projectPath, 'build', `${configuration}-iphoneos`); - // select command to run and arguments depending whether - // we're running on device/emulator + // we're running on device/catalyst/emulator if (useDevice) { + const buildOutputDir = path.join(projectPath, 'build', `${configuration}-iphoneos`); + const appPath = path.join(buildOutputDir, `${projectName}.app`); + return module.exports.checkDeviceConnected() .then(() => { // Unpack IPA @@ -78,13 +84,12 @@ module.exports.run = function (runOptions) { .then(() => { // Uncompress IPA (zip file) const appFileInflated = path.join(buildOutputDir, 'Payload', `${projectName}.app`); - const appFile = path.join(buildOutputDir, `${projectName}.app`); const payloadFolder = path.join(buildOutputDir, 'Payload'); // delete the existing platform/ios/build/device/appname.app - fs.rmSync(appFile, { recursive: true, force: true }); + fs.rmSync(appPath, { recursive: true, force: true }); // move the platform/ios/build/device/Payload/appname.app to parent - fs.renameSync(appFileInflated, appFile); + fs.renameSync(appFileInflated, appPath); // delete the platform/ios/build/device/Payload folder fs.rmSync(payloadFolder, { recursive: true, force: true }); @@ -92,7 +97,6 @@ module.exports.run = function (runOptions) { }) .then( () => { - appPath = path.join(projectPath, 'build', `${configuration}-iphoneos`, `${projectName}.app`); let extraArgs = []; if (runOptions.argv) { // argv.slice(2) removes node and run.js, filterSupportedArgs removes the run.js args @@ -101,9 +105,14 @@ module.exports.run = function (runOptions) { return module.exports.deployToDevice(appPath, runOptions.target, extraArgs); }, // if device connection check failed use emulator then + // This might fail due to being the wrong type of app bundle () => module.exports.deployToSim(appPath, runOptions.target) ); + } else if (useCatalyst) { + const appPath = path.join(projectPath, 'build', `${configuration}-maccatalyst`, `${projectName}.app`); + return module.exports.deployToMac(appPath); } else { + const appPath = path.join(projectPath, 'build', `${configuration}-iphonesimulator`, `${projectName}.app`); return module.exports.deployToSim(appPath, runOptions.target); } }) @@ -113,10 +122,13 @@ module.exports.run = function (runOptions) { module.exports.filterSupportedArgs = filterSupportedArgs; module.exports.checkDeviceConnected = checkDeviceConnected; module.exports.deployToDevice = deployToDevice; +module.exports.deployToMac = deployToMac; module.exports.deployToSim = deployToSim; module.exports.startSim = startSim; module.exports.listDevices = listDevices; module.exports.listEmulators = listEmulators; +module.exports.execListDevices = execListDevices; +module.exports.execListEmulatorTargets = execListEmulatorTargets; /** * Filters the args array and removes supported args for the 'run' command. @@ -164,6 +176,16 @@ function deployToDevice (appPath, target, extraArgs) { return execa('ios-deploy', args.concat(extraArgs), { stdio: 'inherit' }); } +/** + * Runs specified app package on the local macOS system. + * @param {String} appPath Path to application package + * @return {Promise} Resolves when deploy succeeds otherwise rejects + */ +function deployToMac (appPath) { + events.emit('log', 'Deploying to local macOS system'); + return execa('open', [appPath], { stdio: 'inherit' }); +} + /** * Deploy specified app package to ios-sim simulator * @param {String} appPath Path to application package @@ -175,13 +197,13 @@ async function deployToSim (appPath, target) { if (!target) { // Select target device for emulator (preferring iPhone Emulators) - const emulators = await require('./listEmulatorImages').run(); + const emulators = await module.exports.execListEmulatorTargets(); const iPhoneEmus = emulators.filter(emulator => emulator.startsWith('iPhone')); target = iPhoneEmus.concat(emulators)[0]; events.emit('log', `No target specified for emulator. Deploying to "${target}" simulator.`); } - return startSim(appPath, target); + return module.exports.startSim(appPath, target); } function startSim (appPath, target) { @@ -210,8 +232,18 @@ function startSim (appPath, target) { return subprocess; } +/* istanbul ignore next */ +function execListDevices () { + return require('./listDevices').run(); +} + +/* istanbul ignore next */ +function execListEmulatorTargets () { + return require('./listEmulatorTargets').run(); +} + function listDevices () { - return require('./listDevices').run() + return module.exports.execListDevices() .then(devices => { events.emit('log', 'Available iOS Devices:'); devices.forEach(device => { @@ -221,7 +253,7 @@ function listDevices () { } function listEmulators () { - return require('./listEmulatorImages').run() + return module.exports.execListEmulatorTargets() .then(emulators => { events.emit('log', 'Available iOS Simulators:'); emulators.forEach(emulator => { diff --git a/templates/project/__PROJECT_NAME__.xcodeproj/project.pbxproj b/templates/project/__PROJECT_NAME__.xcodeproj/project.pbxproj index c07562530..7765440c0 100755 --- a/templates/project/__PROJECT_NAME__.xcodeproj/project.pbxproj +++ b/templates/project/__PROJECT_NAME__.xcodeproj/project.pbxproj @@ -25,7 +25,7 @@ objects = { /* Begin PBXBuildFile section */ - 902AE2142C6C059A0041150F /* Cordova.framework in Embed Frameworks */ = {isa = PBXBuildFile; fileRef = 907F985F2C06B8DE00D2D242 /* Cordova.framework */; platformFilters = (ios, maccatalyst, ); settings = {ATTRIBUTES = (CodeSignOnCopy, RemoveHeadersOnCopy, ); }; }; + 902AE2142C6C059A0041150F /* Cordova.framework in Embed Frameworks */ = {isa = PBXBuildFile; fileRef = 907F985F2C06B8DE00D2D242 /* Cordova.framework */; settings = {ATTRIBUTES = (CodeSignOnCopy, RemoveHeadersOnCopy, ); }; }; 907F98562C06B87200D2D242 /* PrivacyInfo.xcprivacy in Resources */ = {isa = PBXBuildFile; fileRef = 907F98552C06B87200D2D242 /* PrivacyInfo.xcprivacy */; }; 907F98662C06BC1B00D2D242 /* config.xml in Resources */ = {isa = PBXBuildFile; fileRef = 907F98652C06BC1B00D2D242 /* config.xml */; }; 907F986A2C06BCD300D2D242 /* www in Resources */ = {isa = PBXBuildFile; fileRef = 907F98692C06BCD300D2D242 /* www */; }; @@ -388,8 +388,11 @@ ONLY_ACTIVE_ARCH = YES; SDKROOT = iphoneos; SUPPORTS_MACCATALYST = YES; + SUPPORTS_MAC_DESIGNED_FOR_IPHONE_IPAD = NO; SWIFT_ACTIVE_COMPILATION_CONDITIONS = "DEBUG $(inherited)"; SWIFT_OPTIMIZATION_LEVEL = "-Onone"; + TARGETED_DEVICE_FAMILY = "1,2"; + VALIDATE_WORKSPACE = NO; }; name = Debug; }; @@ -445,8 +448,11 @@ MTL_FAST_MATH = YES; SDKROOT = iphoneos; SUPPORTS_MACCATALYST = YES; + SUPPORTS_MAC_DESIGNED_FOR_IPHONE_IPAD = NO; SWIFT_COMPILATION_MODE = wholemodule; + TARGETED_DEVICE_FAMILY = "1,2"; VALIDATE_PRODUCT = YES; + VALIDATE_WORKSPACE = NO; }; name = Release; }; diff --git a/tests/spec/unit/build.spec.js b/tests/spec/unit/build.spec.js index ad7282fad..c683d9eb0 100644 --- a/tests/spec/unit/build.spec.js +++ b/tests/spec/unit/build.spec.js @@ -225,6 +225,27 @@ describe('build', () => { ]); expect(args.length).toEqual(18); }); + + it('should generate appropriate args for Catalyst macOS builds', () => { + const buildOpts = { + catalyst: true + }; + + const args = getXcodeBuildArgs('TestProjectName', testProjectPath, 'TestConfiguration', '', buildOpts); + expect(args).toEqual([ + '-workspace', + 'TestProjectName.xcworkspace', + '-scheme', + 'TestProjectName', + '-configuration', + 'TestConfiguration', + '-destination', + 'generic/platform=macOS,variant=Mac Catalyst', + 'build', + `SYMROOT=${path.join(testProjectPath, 'build')}` + ]); + expect(args.length).toEqual(10); + }); }); describe('getXcodeArchiveArgs method', () => { @@ -353,35 +374,22 @@ describe('build', () => { }); describe('run method', () => { - beforeEach(() => { - spyOn(Promise, 'reject'); - }); - it('should not accept debug and release options together', () => { - build.run({ - debug: true, - release: true - }); - - expect(Promise.reject).toHaveBeenCalledWith(new CordovaError('Cannot specify "debug" and "release" options together.')); + return expectAsync(build.run({ debug: true, release: true })) + .toBeRejectedWithError(CordovaError, 'Cannot specify "debug" and "release" options together.'); }); it('should not accept device and emulator options together', () => { - build.run({ - device: true, - emulator: true - }); - - expect(Promise.reject).toHaveBeenCalledWith(new CordovaError('Cannot specify "device" and "emulator" options together.')); + return expectAsync(build.run({ device: true, emulator: true })) + .toBeRejectedWithError(CordovaError, 'Cannot specify "device" and "emulator" options together.'); }); it('should reject when build config file missing', () => { spyOn(fs, 'existsSync').and.returnValue(false); - const buildConfig = './some/config/path'; - build.run({ buildConfig: './some/config/path' }); - expect(Promise.reject).toHaveBeenCalledWith(new CordovaError(`Build config file does not exist: ${buildConfig}`)); + return expectAsync(build.run({ buildConfig: './some/config/path' })) + .toBeRejectedWithError(CordovaError, `Build config file does not exist: ${buildConfig}`); }); }); diff --git a/tests/spec/unit/lib/run.spec.js b/tests/spec/unit/lib/run.spec.js deleted file mode 100644 index 160c965f1..000000000 --- a/tests/spec/unit/lib/run.spec.js +++ /dev/null @@ -1,74 +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. -*/ - -// Requiring lib/run below has some side effects, mainly, -// it ends up pulling in the ios-sim module and requiring the specific macOS -// environment bits that allow for interacting with iOS Simulators. On -// Windows+Linux we are bound to not-have-that. -if (process.platform === 'darwin') { - const run = require('../../../../lib/run'); - - describe('cordova/lib/run', () => { - describe('--list option', () => { - beforeEach(() => { - spyOn(run, 'listDevices').and.returnValue(Promise.resolve()); - spyOn(run, 'listEmulators').and.returnValue(Promise.resolve()); - }); - it('should delegate to listDevices method if `options.device` specified', () => { - return run.run({ list: true, device: true }).then(() => { - expect(run.listDevices).toHaveBeenCalled(); - expect(run.listEmulators).not.toHaveBeenCalled(); - }); - }); - it('should delegate to listEmulators method if `options.device` specified', () => { - return run.run({ list: true, emulator: true }).then(() => { - expect(run.listDevices).not.toHaveBeenCalled(); - expect(run.listEmulators).toHaveBeenCalled(); - }); - }); - it('should delegate to both listEmulators and listDevices methods if neither `options.device` nor `options.emulator` are specified', () => { - return run.run({ list: true }).then(() => { - expect(run.listDevices).toHaveBeenCalled(); - expect(run.listEmulators).toHaveBeenCalled(); - }); - }); - - it('should delegate to "listDevices" when the "runListDevices" method options param contains "options.device".', () => { - return run.runListDevices({ options: { device: true } }).then(() => { - expect(run.listDevices).toHaveBeenCalled(); - expect(run.listEmulators).not.toHaveBeenCalled(); - }); - }); - - it('should delegate to "listDevices" when the "runListDevices" method options param contains "options.emulator".', () => { - return run.runListDevices({ options: { emulator: true } }).then(() => { - expect(run.listDevices).not.toHaveBeenCalled(); - expect(run.listEmulators).toHaveBeenCalled(); - }); - }); - - it('should delegate to both "listEmulators" and "listDevices" when the "runListDevices" method does not contain "options.device" or "options.emulator".', () => { - return run.runListDevices({ options: {} }).then(() => { - expect(run.listDevices).toHaveBeenCalled(); - expect(run.listEmulators).toHaveBeenCalled(); - }); - }); - }); - }); -} diff --git a/tests/spec/unit/prepare.spec.js b/tests/spec/unit/prepare.spec.js index 0bcb2a8d5..c6e7e1f78 100644 --- a/tests/spec/unit/prepare.spec.js +++ b/tests/spec/unit/prepare.spec.js @@ -710,33 +710,33 @@ describe('prepare', () => { fs.cpSync(path.join(FIXTURES, 'icon-support', 'res'), path.join(iosProject, 'res'), { recursive: true }); // copy icons and update Contents.json - updateIcons(project, p.locations); - - // now, clean the images - const updatePaths = spyOn(FileUpdater, 'updatePaths'); - - return cleanIcons(iosProject, project.projectConfig, p.locations) - .then(() => { - expect(updatePaths).toHaveBeenCalledWith({ - [path.join(iconsDir, 'icon.png')]: null, - [path.join(iconsDir, 'watchos.png')]: null, - [path.join(iconsDir, 'icon-20@2x.png')]: null, - [path.join(iconsDir, 'icon-20@3x.png')]: null, - [path.join(iconsDir, 'icon-29@2x.png')]: null, - [path.join(iconsDir, 'icon-29@3x.png')]: null, - [path.join(iconsDir, 'icon-38@2x.png')]: null, - [path.join(iconsDir, 'icon-38@3x.png')]: null, - [path.join(iconsDir, 'icon-40@2x.png')]: null, - [path.join(iconsDir, 'icon-40@3x.png')]: null, - [path.join(iconsDir, 'icon-60@2x.png')]: null, - [path.join(iconsDir, 'icon-60@3x.png')]: null, - [path.join(iconsDir, 'icon-64@2x.png')]: null, - [path.join(iconsDir, 'icon-64@3x.png')]: null, - [path.join(iconsDir, 'icon-68@2x.png')]: null, - [path.join(iconsDir, 'icon-76@2x.png')]: null, - [path.join(iconsDir, 'icon-83.5@2x.png')]: null - }, { rootDir: iosProject, all: true }, logFileOp); - }); + return updateIcons(project, p.locations).then(() => { + // now, clean the images + const updatePaths = spyOn(FileUpdater, 'updatePaths'); + + return cleanIcons(iosProject, project.projectConfig, p.locations) + .then(() => { + expect(updatePaths).toHaveBeenCalledWith({ + [path.join(iconsDir, 'icon.png')]: null, + [path.join(iconsDir, 'watchos.png')]: null, + [path.join(iconsDir, 'icon-20@2x.png')]: null, + [path.join(iconsDir, 'icon-20@3x.png')]: null, + [path.join(iconsDir, 'icon-29@2x.png')]: null, + [path.join(iconsDir, 'icon-29@3x.png')]: null, + [path.join(iconsDir, 'icon-38@2x.png')]: null, + [path.join(iconsDir, 'icon-38@3x.png')]: null, + [path.join(iconsDir, 'icon-40@2x.png')]: null, + [path.join(iconsDir, 'icon-40@3x.png')]: null, + [path.join(iconsDir, 'icon-60@2x.png')]: null, + [path.join(iconsDir, 'icon-60@3x.png')]: null, + [path.join(iconsDir, 'icon-64@2x.png')]: null, + [path.join(iconsDir, 'icon-64@3x.png')]: null, + [path.join(iconsDir, 'icon-68@2x.png')]: null, + [path.join(iconsDir, 'icon-76@2x.png')]: null, + [path.join(iconsDir, 'icon-83.5@2x.png')]: null + }, { rootDir: iosProject, all: true }, logFileOp); + }); + }); }); it('should have no effect if no icons are specified', () => { @@ -751,15 +751,15 @@ describe('prepare', () => { fs.cpSync(path.join(FIXTURES, 'icon-support', 'res'), path.join(iosProject, 'res'), { recursive: true }); // copy icons and update Contents.json - updateIcons(project, p.locations); - - // now, clean the images - const updatePaths = spyOn(FileUpdater, 'updatePaths'); - - return cleanIcons(iosProject, project.projectConfig, p.locations) - .then(() => { - expect(updatePaths).not.toHaveBeenCalled(); - }); + return updateIcons(project, p.locations).then(() => { + // now, clean the images + const updatePaths = spyOn(FileUpdater, 'updatePaths'); + + return cleanIcons(iosProject, project.projectConfig, p.locations) + .then(() => { + expect(updatePaths).not.toHaveBeenCalled(); + }); + }); }); }); }); diff --git a/tests/spec/unit/run.spec.js b/tests/spec/unit/run.spec.js new file mode 100644 index 000000000..df8ad49d1 --- /dev/null +++ b/tests/spec/unit/run.spec.js @@ -0,0 +1,149 @@ +/* + 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 path = require('node:path'); +const { CordovaError, events } = require('cordova-common'); +const build = require('../../../lib/build'); +const check_reqs = require('../../../lib/check_reqs'); +const run = require('../../../lib/run'); + +describe('cordova/lib/run', () => { + const testProjectPath = path.join('/test', 'project', 'path'); + + beforeEach(() => { + run.root = testProjectPath; + }); + + describe('runListDevices method', () => { + beforeEach(() => { + spyOn(events, 'emit'); + spyOn(run, 'execListDevices').and.returnValue(Promise.resolve(['iPhone Xs'])); + spyOn(run, 'execListEmulatorTargets').and.returnValue(Promise.resolve(['iPhone 15 Simulator'])); + }); + + it('should delegate to "listDevices" when the "runListDevices" method options param contains "options.device".', () => { + return run.runListDevices({ options: { device: true } }).then(() => { + expect(run.execListDevices).toHaveBeenCalled(); + expect(run.execListEmulatorTargets).not.toHaveBeenCalled(); + + expect(events.emit).toHaveBeenCalledWith('log', '\tiPhone Xs'); + }); + }); + + it('should delegate to "listDevices" when the "runListDevices" method options param contains "options.emulator".', () => { + return run.runListDevices({ options: { emulator: true } }).then(() => { + expect(run.execListDevices).not.toHaveBeenCalled(); + expect(run.execListEmulatorTargets).toHaveBeenCalled(); + + expect(events.emit).toHaveBeenCalledWith('log', '\tiPhone 15 Simulator'); + }); + }); + + it('should delegate to both "listEmulators" and "listDevices" when the "runListDevices" method does not contain "options.device" or "options.emulator".', () => { + return run.runListDevices().then(() => { + expect(run.execListDevices).toHaveBeenCalled(); + expect(run.execListEmulatorTargets).toHaveBeenCalled(); + + expect(events.emit).toHaveBeenCalledWith('log', '\tiPhone Xs'); + expect(events.emit).toHaveBeenCalledWith('log', '\tiPhone 15 Simulator'); + }); + }); + }); + + describe('run method', () => { + beforeEach(() => { + spyOn(build, 'run').and.returnValue(Promise.resolve()); + spyOn(build, 'findXCodeProjectIn').and.returnValue('ProjectName'); + spyOn(run, 'execListDevices').and.resolveTo([]); + spyOn(run, 'execListEmulatorTargets').and.resolveTo([]); + spyOn(run, 'listDevices').and.resolveTo(); + spyOn(run, 'deployToMac').and.resolveTo(); + spyOn(run, 'deployToSim').and.resolveTo(); + spyOn(run, 'checkDeviceConnected').and.rejectWith(new Error('No Device Connected')); + }); + + describe('--list option', () => { + beforeEach(() => { + spyOn(run, 'listEmulators').and.returnValue(Promise.resolve()); + }); + + it('should delegate to listDevices method if `options.device` specified', () => { + return run.run({ list: true, device: true }).then(() => { + expect(run.listDevices).toHaveBeenCalled(); + expect(run.listEmulators).not.toHaveBeenCalled(); + }); + }); + + it('should delegate to listEmulators method if `options.device` specified', () => { + return run.run({ list: true, emulator: true }).then(() => { + expect(run.listDevices).not.toHaveBeenCalled(); + expect(run.listEmulators).toHaveBeenCalled(); + }); + }); + + it('should delegate to both listEmulators and listDevices methods if neither `options.device` nor `options.emulator` are specified', () => { + return run.run({ list: true }).then(() => { + expect(run.listDevices).toHaveBeenCalled(); + expect(run.listEmulators).toHaveBeenCalled(); + }); + }); + }); + + it('should not accept device and emulator options together', () => { + return expectAsync(run.run({ device: true, emulator: true })) + .toBeRejectedWithError(CordovaError, 'Only one of "device"/"emulator" options should be specified'); + }); + + it('should run on a simulator if --device is not specified and no device is connected', () => { + return run.run({ }).then(() => { + expect(run.deployToSim).toHaveBeenCalled(); + expect(build.run).toHaveBeenCalled(); + }); + }); + + it('should try to run on a device if --device is not specified and a device is connected', () => { + spyOn(check_reqs, 'check_ios_deploy'); + run.execListDevices.and.resolveTo(['iPhone 12 Plus']); + + return run.run({ }).then(() => { + expect(run.checkDeviceConnected).toHaveBeenCalled(); + expect(build.run).toHaveBeenCalledWith(jasmine.objectContaining({ device: true })); + }); + }); + + it('should try to run on a device if --device is specified', () => { + return run.run({ device: true }).then(() => { + expect(run.checkDeviceConnected).toHaveBeenCalled(); + expect(build.run).toHaveBeenCalledWith(jasmine.objectContaining({ device: true })); + }); + }); + + it('should not run a build if --noBuild is passed', () => { + return run.run({ emulator: true, nobuild: true }).then(() => { + expect(build.run).not.toHaveBeenCalled(); + }); + }); + + it('should try to launch the macOS Catalyst app bundle', () => { + return run.run({ device: true, target: 'mac', release: true }).then(() => { + expect(run.deployToMac).toHaveBeenCalledWith(path.join(testProjectPath, 'build', 'Release-maccatalyst', 'ProjectName.app')); + }); + }); + }); +});