This is an automated email from the ASF dual-hosted git repository.

dpogue pushed a commit to branch master
in repository https://gitbox.apache.org/repos/asf/cordova-ios.git


The following commit(s) were added to refs/heads/master by this push:
     new f81013b6 feat!: Better Catalyst build support (#1313)
f81013b6 is described below

commit f81013b6d8377d8cc1c5ae03a4a081d1e258f2aa
Author: Darryl Pogue <dar...@dpogue.ca>
AuthorDate: Fri Aug 30 00:52:07 2024 -0700

    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/CordovaLib.xcodeproj/project.pbxproj    |  20 ++-
 lib/build.js                                       |  51 ++++---
 lib/run.js                                         |  82 ++++++++----
 .../__PROJECT_NAME__.xcodeproj/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(-)

diff --git a/CordovaLib/CordovaLib.xcodeproj/project.pbxproj 
b/CordovaLib/CordovaLib.xcodeproj/project.pbxproj
index 6b95e5da..14e66bea 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 73d57fa7..fbca2743 100644
--- a/lib/build.js
+++ b/lib/build.js
@@ -102,7 +102,7 @@ function getDefaultSimulatorTarget () {
 /** @returns {Promise<void>} */
 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 494439f0..a1899e8d 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 c0756253..7765440c 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 ad7282fa..c683d9eb 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 160c965f..00000000
--- 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 0bcb2a8d..c6e7e1f7 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...@2x.png')]: null,
-                            [path.join(iconsDir, 'icon...@3x.png')]: null,
-                            [path.join(iconsDir, 'icon...@2x.png')]: null,
-                            [path.join(iconsDir, 'icon...@3x.png')]: null,
-                            [path.join(iconsDir, 'icon...@2x.png')]: null,
-                            [path.join(iconsDir, 'icon...@3x.png')]: null,
-                            [path.join(iconsDir, 'icon...@2x.png')]: null,
-                            [path.join(iconsDir, 'icon...@3x.png')]: null,
-                            [path.join(iconsDir, 'icon...@2x.png')]: null,
-                            [path.join(iconsDir, 'icon...@3x.png')]: null,
-                            [path.join(iconsDir, 'icon...@2x.png')]: null,
-                            [path.join(iconsDir, 'icon...@3x.png')]: null,
-                            [path.join(iconsDir, 'icon...@2x.png')]: null,
-                            [path.join(iconsDir, 'icon...@2x.png')]: null,
-                            [path.join(iconsDir, 'icon-8...@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...@2x.png')]: null,
+                                [path.join(iconsDir, 'icon...@3x.png')]: null,
+                                [path.join(iconsDir, 'icon...@2x.png')]: null,
+                                [path.join(iconsDir, 'icon...@3x.png')]: null,
+                                [path.join(iconsDir, 'icon...@2x.png')]: null,
+                                [path.join(iconsDir, 'icon...@3x.png')]: null,
+                                [path.join(iconsDir, 'icon...@2x.png')]: null,
+                                [path.join(iconsDir, 'icon...@3x.png')]: null,
+                                [path.join(iconsDir, 'icon...@2x.png')]: null,
+                                [path.join(iconsDir, 'icon...@3x.png')]: null,
+                                [path.join(iconsDir, 'icon...@2x.png')]: null,
+                                [path.join(iconsDir, 'icon...@3x.png')]: null,
+                                [path.join(iconsDir, 'icon...@2x.png')]: null,
+                                [path.join(iconsDir, 'icon...@2x.png')]: null,
+                                [path.join(iconsDir, 'icon-8...@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 00000000..df8ad49d
--- /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'));
+            });
+        });
+    });
+});


---------------------------------------------------------------------
To unsubscribe, e-mail: commits-unsubscr...@cordova.apache.org
For additional commands, e-mail: commits-h...@cordova.apache.org


Reply via email to