From c1caa82ba5f1437ce9930c01ea062cb074d9c8d3 Mon Sep 17 00:00:00 2001 From: Emmanouil Konstantinidis Date: Tue, 22 Dec 2020 21:19:33 +0000 Subject: [PATCH 1/9] feat: Build iOS - uninstall, install and launch on simulator --- src/cli/config.ts | 6 ++++-- src/cli/run.ts | 55 +++++++++++++++++++++++++++++++++++++++++++++-- src/cli/types.ts | 14 +++++++----- 3 files changed, 66 insertions(+), 9 deletions(-) diff --git a/src/cli/config.ts b/src/cli/config.ts index edeb4074..cd6fb189 100644 --- a/src/cli/config.ts +++ b/src/cli/config.ts @@ -13,12 +13,14 @@ export const validateSchema = (config: {}): Promise => { workspace: { type: 'string', nullable: true }, scheme: { type: 'string', nullable: true }, buildCommand: { type: 'string', nullable: true }, + binaryPath: { type: 'string', nullable: true }, quiet: { type: 'boolean', nullable: true }, + device: { type: 'string' }, }, - required: [], + required: ['device'], anyOf: [ { required: ['workspace', 'scheme'] }, - { required: ['buildCommand'] }, + { required: ['buildCommand', 'binaryPath'] }, ], nullable: true, additionalProperties: false, diff --git a/src/cli/run.ts b/src/cli/run.ts index 0e86c665..99041f1a 100644 --- a/src/cli/run.ts +++ b/src/cli/run.ts @@ -1,5 +1,56 @@ -import { BuildRunOptions } from './types'; +import path from 'path'; +import execa from 'execa'; + +import { BuildRunOptions, Config, Logger } from './types'; +import { createLogger } from '../logger'; +import { getConfig } from './config'; + +const getIOSBundleIdentifier = (appPath: string): string => { + const { stdout } = execa.commandSync( + `mdls -name kMDItemCFBundleIdentifier -r ${appPath}` + ); + return stdout; +}; + +export const runIOS = async (config: Config, logger: Logger) => { + const DEFAULT_BINARY_DIR = '/ios/build/Build/Products/Debug-iphonesimulator'; + const cwd = path.join( + process.cwd(), + config.ios?.binaryPath + ? path.dirname(config.ios?.binaryPath) + : DEFAULT_BINARY_DIR + ); + + const appFilename = config.ios?.binaryPath + ? path.basename(config.ios?.binaryPath) + : `${config.ios?.scheme}.app`; + const appPath = path.join(cwd, appFilename); + const bundleId = getIOSBundleIdentifier(appPath); + const simulator = config.ios?.device.replace(/([ /])/g, '\\$1'); + + // Uninstall + const uninstallCommand = `xcrun simctl uninstall ${simulator} ${bundleId}`; + await execa.command(uninstallCommand, { stdio: 'inherit', cwd }); + + // Install + const installCommand = `xcrun simctl install ${simulator} ${appFilename}`; + await execa.command(installCommand, { stdio: 'inherit', cwd }); + + // Launch + const launchCommand = `xcrun simctl launch ${simulator} ${bundleId}`; + await execa.command(launchCommand, { stdio: 'inherit', cwd }); +}; + +export const runAndroid = async (config: Config, logger: Logger) => { + // Coming Soon +}; export const runHandler = async (args: BuildRunOptions) => { - console.log(`OWL will build for the ${args.platform} platform.`); + const config = await getConfig(args.config); + const logger = createLogger(config.debug); + const runProject = args.platform === 'ios' ? runIOS : runAndroid; + + await runProject(config, logger); + + logger.info(`OWL will run the app on ${args.platform}.`); }; diff --git a/src/cli/types.ts b/src/cli/types.ts index f3ec5f8e..6bb5460a 100644 --- a/src/cli/types.ts +++ b/src/cli/types.ts @@ -6,27 +6,31 @@ export interface BuildRunOptions extends Arguments { } type ConfigIOS = { - /** The workspace to build */ + /** The workspace to build. */ workspace?: string; - /** The scheme to build */ + /** The scheme to build. */ scheme?: string; /** Overrides the `xcodebuild` command making the workspace & scheme options obselete. */ buildCommand?: string; - /** Passes the quiet flag to `xcodebuild` */ + /** Path to the .app that will get generated by a custom build command. Ignored when not using a custom build command. */ + binaryPath?: string; + /** Passes the quiet flag to `xcodebuild`. Does not print any output except for warnings and errors. */ quiet?: boolean; + /** The name of the simulator you would like to run tests on. Can be either the name or the id of the device. */ + device: string; }; type ConfigAndroid = { /** Overrides the `assembleDebug` gradle command. Should build the apk. */ buildCommand?: string; - /** Passes the quiet flag to `gradlew` */ + /** Passes the quiet flag to `gradlew`. */ quiet?: boolean; }; export type Config = { ios?: ConfigIOS; android?: ConfigAndroid; - /** Prevents the CLI/library from printing any logs/output*/ + /** Prevents the CLI/library from printing any logs/output. */ debug?: boolean; }; From cea875f3983c64ca6098fbd9a0e42a4f264546d1 Mon Sep 17 00:00:00 2001 From: Emmanouil Konstantinidis Date: Wed, 23 Dec 2020 10:36:25 +0000 Subject: [PATCH 2/9] chore: Fix existing tests - pass a device name --- src/cli/build.test.ts | 21 ++++++-------- src/cli/config.test.ts | 62 +++++++++++++++++++++++++++++++++++++++--- 2 files changed, 67 insertions(+), 16 deletions(-) diff --git a/src/cli/build.test.ts b/src/cli/build.test.ts index 352145b4..7a704ea0 100644 --- a/src/cli/build.test.ts +++ b/src/cli/build.test.ts @@ -8,19 +8,19 @@ import * as configHelpers from './config'; describe('build.ts', () => { const logger = createLogger(); + const execMock = jest.spyOn(execa, 'command').mockImplementation(); - describe('buildIOS', () => { - const execMock = jest.spyOn(execa, 'command').mockImplementation(); - - beforeEach(() => { - execMock.mockReset(); - }); + beforeEach(() => { + execMock.mockReset(); + }); + describe('buildIOS', () => { it('builds an iOS project with workspace/scheme', async () => { const config: Config = { ios: { workspace: 'ios/RNDemo.xcworkspace', scheme: 'RNDemo', + device: 'iPhone Simulator', }, }; @@ -41,6 +41,7 @@ describe('build.ts', () => { workspace: 'ios/RNDemo.xcworkspace', scheme: 'RNDemo', quiet: true, + device: 'iPhone Simulator', }, }; @@ -59,6 +60,7 @@ describe('build.ts', () => { const config: Config = { ios: { buildCommand: "echo 'Hello World'", + device: 'iPhone Simulator', }, }; @@ -72,12 +74,6 @@ describe('build.ts', () => { }); describe('buildAndroid', () => { - const execMock = jest.spyOn(execa, 'command').mockImplementation(); - - beforeEach(() => { - execMock.mockReset(); - }); - it('builds an Android project with the default build command', async () => { const config: Config = { android: {}, @@ -133,6 +129,7 @@ describe('build.ts', () => { const config: Config = { ios: { buildCommand: "echo 'Hello World'", + device: 'iPhone Simulator', }, android: { buildCommand: "echo 'Hello World'", diff --git a/src/cli/config.test.ts b/src/cli/config.test.ts index fe411648..e578b8da 100644 --- a/src/cli/config.test.ts +++ b/src/cli/config.test.ts @@ -1,13 +1,16 @@ import { promises as fs } from 'fs'; +import { Config } from './types'; import { getConfig, readConfigFile, validateSchema } from './config'; describe('config.ts', () => { describe('validateSchema', () => { it('validates a config', async () => { - const config = { + const config: Config = { ios: { buildCommand: 'echo "Hello iOS"', + binaryPath: '', + device: 'iPhone Simulator', }, android: { buildCommand: 'echo "Hello Android"', @@ -20,7 +23,14 @@ describe('config.ts', () => { }); it('accepts an ios config that has workspace/scheme but not a buildCommand', async () => { - const config = { ios: { workspace: 'Test', scheme: 'Test' } }; + const config = { + ios: { + workspace: 'ios/RNDemo.xcworkspace', + scheme: 'Test', + + device: 'iPhone Simulator', + }, + }; const validate = async () => await validateSchema(config); @@ -28,7 +38,14 @@ describe('config.ts', () => { }); it('accepts an ios config that has buildCommand but not workspace/scheme', async () => { - const config = { ios: { workspace: 'Test', scheme: 'Test' } }; + const config = { + ios: { + workspace: 'ios/RNDemo.xcworkspace', + scheme: 'Test', + + device: 'iPhone Simulator', + }, + }; const validate = async () => await validateSchema(config); @@ -44,7 +61,43 @@ describe('config.ts', () => { "should have required property 'workspace'" ); await expect(validate()).rejects.toContain( - "should have required property 'workspace'" + "should have required property 'buildCommand'" + ); + await expect(validate()).rejects.toContain( + 'should match some schema in anyOf' + ); + }); + + it('rejects an ios config that has a workspace but not a scheme', async () => { + const config = { + ios: { + workspace: 'ios/RNDemo.xcworkspace', + device: 'iPhone Simulator', + }, + }; + + const validate = async () => await validateSchema(config); + + await expect(validate()).rejects.toContain( + "should have required property 'scheme'" + ); + await expect(validate()).rejects.toContain( + 'should match some schema in anyOf' + ); + }); + + it('rejects an ios config that has a build command but not a binary path', async () => { + const config = { + ios: { + buildCommand: 'echo "Hello iOS"', + device: 'iPhone Simulator', + }, + }; + + const validate = async () => await validateSchema(config); + + await expect(validate()).rejects.toContain( + "should have required property 'binaryPath'" ); await expect(validate()).rejects.toContain( 'should match some schema in anyOf' @@ -93,6 +146,7 @@ describe('config.ts', () => { ios: { workspace: 'ios/RNDemo.xcworkspace', scheme: 'RNDemo', + device: 'iPhone Simulator', }, android: {}, }; From e219dcff432aefbf53a24be82cdd9ea342f4821e Mon Sep 17 00:00:00 2001 From: Emmanouil Konstantinidis Date: Wed, 23 Dec 2020 11:22:17 +0000 Subject: [PATCH 3/9] chore: Tests for build iOS --- src/cli/run.test.ts | 163 ++++++++++++++++++++++++++++++++++++++++++++ src/cli/run.ts | 2 +- 2 files changed, 164 insertions(+), 1 deletion(-) create mode 100644 src/cli/run.test.ts diff --git a/src/cli/run.test.ts b/src/cli/run.test.ts new file mode 100644 index 00000000..97b8b600 --- /dev/null +++ b/src/cli/run.test.ts @@ -0,0 +1,163 @@ +import path from 'path'; +import execa, { ExecaSyncReturnValue } from 'execa'; + +import { BuildRunOptions, Config } from './types'; +import { createLogger } from '../logger'; +import * as configHelpers from './config'; +import * as run from './run'; + +describe('run.ts', () => { + const logger = createLogger(); + const execMock = jest.spyOn(execa, 'command').mockImplementation(); + + const bundleIdIOS = 'org.reactjs.native.example.RNDemo'; + + beforeEach(() => { + execMock.mockReset(); + }); + + describe('getIOSBundleIdentifier', () => { + it('should return the bundle identifier', () => { + jest.spyOn(execa, 'commandSync').mockReturnValueOnce({ + stdout: bundleIdIOS, + } as ExecaSyncReturnValue); + + const result = run.getIOSBundleIdentifier('./path/to/RNDemo.app'); + + expect(result).toBe(bundleIdIOS); + }); + }); + + describe('runOS', () => { + const execMock = jest.spyOn(execa, 'command').mockImplementation(); + + beforeEach(() => { + execMock.mockReset(); + }); + + it('runs an iOS project - with the default build command', async () => { + const cwd = path.join( + process.cwd(), + '/ios/build/Build/Products/Debug-iphonesimulator' + ); + const appPath = path.join(cwd, 'RNDemo.app'); + + jest + .spyOn(run, 'getIOSBundleIdentifier') + .mockReturnValueOnce(bundleIdIOS); + + const config: Config = { + ios: { + workspace: 'ios/RNDemo.xcworkspace', + scheme: 'RNDemo', + device: 'iPhone Simulator', + }, + }; + + await run.runIOS(config, logger); + + expect(execMock).toHaveBeenNthCalledWith( + 1, + `xcrun simctl uninstall iPhone\\ Simulator ${bundleIdIOS}`, + { cwd, stdio: 'inherit' } + ); + + expect(execMock).toHaveBeenNthCalledWith( + 2, + 'xcrun simctl install iPhone\\ Simulator RNDemo.app', + { cwd, stdio: 'inherit' } + ); + + expect(execMock).toHaveBeenNthCalledWith( + 3, + `xcrun simctl launch iPhone\\ Simulator ${bundleIdIOS}`, + { cwd, stdio: 'inherit' } + ); + + expect(run.getIOSBundleIdentifier).toHaveBeenCalledWith(appPath); + }); + + it('runs an iOS project - with a custom build command and binaryPath', async () => { + const cwd = path.join(process.cwd(), '/ios/path/to'); + const appPath = path.join(cwd, 'RNDemo.app'); + + jest + .spyOn(run, 'getIOSBundleIdentifier') + .mockReturnValueOnce(bundleIdIOS); + + const config: Config = { + ios: { + buildCommand: "echo 'Hello World'", + binaryPath: './ios/path/to/RNDemo.app', + device: 'iPhone Simulator', + }, + }; + + await run.runIOS(config, logger); + + expect(execMock).toHaveBeenNthCalledWith( + 1, + `xcrun simctl uninstall iPhone\\ Simulator ${bundleIdIOS}`, + { cwd, stdio: 'inherit' } + ); + + expect(execMock).toHaveBeenNthCalledWith( + 2, + 'xcrun simctl install iPhone\\ Simulator RNDemo.app', + { cwd, stdio: 'inherit' } + ); + + expect(execMock).toHaveBeenNthCalledWith( + 3, + `xcrun simctl launch iPhone\\ Simulator ${bundleIdIOS}`, + { cwd, stdio: 'inherit' } + ); + + expect(run.getIOSBundleIdentifier).toHaveBeenCalledWith(appPath); + }); + }); + + describe('runAndroid', () => { + const execMock = jest.spyOn(execa, 'command').mockImplementation(); + + beforeEach(() => { + execMock.mockReset(); + }); + + pending('runs an Android project'); + }); + + describe('runHandler', () => { + const args = { + platform: 'ios', + config: './owl.config.json', + } as BuildRunOptions; + + const config: Config = { + ios: { + workspace: 'ios/RNDemo.xcworkspace', + scheme: 'RNDemo', + device: 'iPhone Simulator', + }, + android: { + buildCommand: "echo 'Hello World'", + }, + }; + + it('runs an iOS project', async () => { + jest.spyOn(configHelpers, 'getConfig').mockResolvedValueOnce(config); + jest.spyOn(run, 'runIOS').mockImplementation(); + const call = async () => run.runHandler(args); + await expect(call()).resolves.not.toThrow(); + await expect(run.runIOS).toHaveBeenCalled(); + }); + + it('runs an Android project', async () => { + jest.spyOn(configHelpers, 'getConfig').mockResolvedValueOnce(config); + jest.spyOn(run, 'runAndroid').mockImplementation(); + const call = async () => run.runHandler({ ...args, platform: 'android' }); + await expect(call()).resolves.not.toThrow(); + await expect(run.runAndroid).toHaveBeenCalled(); + }); + }); +}); diff --git a/src/cli/run.ts b/src/cli/run.ts index 99041f1a..ce361192 100644 --- a/src/cli/run.ts +++ b/src/cli/run.ts @@ -5,7 +5,7 @@ import { BuildRunOptions, Config, Logger } from './types'; import { createLogger } from '../logger'; import { getConfig } from './config'; -const getIOSBundleIdentifier = (appPath: string): string => { +export const getIOSBundleIdentifier = (appPath: string): string => { const { stdout } = execa.commandSync( `mdls -name kMDItemCFBundleIdentifier -r ${appPath}` ); From 98c9b7cef346580d2dda4fc9b2c6a2642cfe52d6 Mon Sep 17 00:00:00 2001 From: Emmanouil Konstantinidis Date: Wed, 23 Dec 2020 19:52:57 +0000 Subject: [PATCH 4/9] feat: Run Android apps on emulators --- src/cli/build.test.ts | 7 ++++++- src/cli/config.test.ts | 5 ++++- src/cli/config.ts | 5 +++-- src/cli/run.test.ts | 17 +++-------------- src/cli/run.ts | 28 +++++++++++++++++----------- src/cli/types.ts | 2 ++ 6 files changed, 35 insertions(+), 29 deletions(-) diff --git a/src/cli/build.test.ts b/src/cli/build.test.ts index 7a704ea0..abd398a3 100644 --- a/src/cli/build.test.ts +++ b/src/cli/build.test.ts @@ -76,7 +76,9 @@ describe('build.ts', () => { describe('buildAndroid', () => { it('builds an Android project with the default build command', async () => { const config: Config = { - android: {}, + android: { + packageName: 'com.rndemo', + }, }; await buildAndroid(config, logger); @@ -91,6 +93,7 @@ describe('build.ts', () => { it('builds an Android project with the default build command - with the quiet arg', async () => { const config: Config = { android: { + packageName: 'com.rndemo', quiet: true, }, }; @@ -107,6 +110,7 @@ describe('build.ts', () => { it('builds an Android project with a custom build command', async () => { const config: Config = { android: { + packageName: 'com.rndemo', buildCommand: "echo 'Hello World'", }, }; @@ -132,6 +136,7 @@ describe('build.ts', () => { device: 'iPhone Simulator', }, android: { + packageName: 'com.rndemo', buildCommand: "echo 'Hello World'", }, }; diff --git a/src/cli/config.test.ts b/src/cli/config.test.ts index e578b8da..4f400f2f 100644 --- a/src/cli/config.test.ts +++ b/src/cli/config.test.ts @@ -13,6 +13,7 @@ describe('config.ts', () => { device: 'iPhone Simulator', }, android: { + packageName: 'com.rndemo', buildCommand: 'echo "Hello Android"', }, }; @@ -148,7 +149,9 @@ describe('config.ts', () => { scheme: 'RNDemo', device: 'iPhone Simulator', }, - android: {}, + android: { + packageName: 'com.rndemo', + }, }; const filePath = './owl.config.json'; diff --git a/src/cli/config.ts b/src/cli/config.ts index cd6fb189..782ee1be 100644 --- a/src/cli/config.ts +++ b/src/cli/config.ts @@ -14,8 +14,8 @@ export const validateSchema = (config: {}): Promise => { scheme: { type: 'string', nullable: true }, buildCommand: { type: 'string', nullable: true }, binaryPath: { type: 'string', nullable: true }, - quiet: { type: 'boolean', nullable: true }, device: { type: 'string' }, + quiet: { type: 'boolean', nullable: true }, }, required: ['device'], anyOf: [ @@ -28,10 +28,11 @@ export const validateSchema = (config: {}): Promise => { android: { type: 'object', properties: { + packageName: { type: 'string' }, buildCommand: { type: 'string', nullable: true }, quiet: { type: 'boolean', nullable: true }, }, - required: [], + required: ['packageName'], nullable: true, additionalProperties: false, }, diff --git a/src/cli/run.test.ts b/src/cli/run.test.ts index 97b8b600..f7ab97cd 100644 --- a/src/cli/run.test.ts +++ b/src/cli/run.test.ts @@ -58,18 +58,12 @@ describe('run.ts', () => { expect(execMock).toHaveBeenNthCalledWith( 1, - `xcrun simctl uninstall iPhone\\ Simulator ${bundleIdIOS}`, - { cwd, stdio: 'inherit' } - ); - - expect(execMock).toHaveBeenNthCalledWith( - 2, 'xcrun simctl install iPhone\\ Simulator RNDemo.app', { cwd, stdio: 'inherit' } ); expect(execMock).toHaveBeenNthCalledWith( - 3, + 2, `xcrun simctl launch iPhone\\ Simulator ${bundleIdIOS}`, { cwd, stdio: 'inherit' } ); @@ -97,18 +91,12 @@ describe('run.ts', () => { expect(execMock).toHaveBeenNthCalledWith( 1, - `xcrun simctl uninstall iPhone\\ Simulator ${bundleIdIOS}`, - { cwd, stdio: 'inherit' } - ); - - expect(execMock).toHaveBeenNthCalledWith( - 2, 'xcrun simctl install iPhone\\ Simulator RNDemo.app', { cwd, stdio: 'inherit' } ); expect(execMock).toHaveBeenNthCalledWith( - 3, + 2, `xcrun simctl launch iPhone\\ Simulator ${bundleIdIOS}`, { cwd, stdio: 'inherit' } ); @@ -140,6 +128,7 @@ describe('run.ts', () => { device: 'iPhone Simulator', }, android: { + packageName: 'com.rndemo', buildCommand: "echo 'Hello World'", }, }; diff --git a/src/cli/run.ts b/src/cli/run.ts index ce361192..208bc5b9 100644 --- a/src/cli/run.ts +++ b/src/cli/run.ts @@ -21,28 +21,34 @@ export const runIOS = async (config: Config, logger: Logger) => { : DEFAULT_BINARY_DIR ); - const appFilename = config.ios?.binaryPath - ? path.basename(config.ios?.binaryPath) - : `${config.ios?.scheme}.app`; + const appFilename = config.ios!.binaryPath + ? path.basename(config.ios!.binaryPath) + : `${config.ios!.scheme}.app`; const appPath = path.join(cwd, appFilename); const bundleId = getIOSBundleIdentifier(appPath); - const simulator = config.ios?.device.replace(/([ /])/g, '\\$1'); + const simulator = config.ios!.device.replace(/([ /])/g, '\\$1'); - // Uninstall - const uninstallCommand = `xcrun simctl uninstall ${simulator} ${bundleId}`; - await execa.command(uninstallCommand, { stdio: 'inherit', cwd }); - - // Install const installCommand = `xcrun simctl install ${simulator} ${appFilename}`; await execa.command(installCommand, { stdio: 'inherit', cwd }); - // Launch const launchCommand = `xcrun simctl launch ${simulator} ${bundleId}`; await execa.command(launchCommand, { stdio: 'inherit', cwd }); }; export const runAndroid = async (config: Config, logger: Logger) => { - // Coming Soon + const stdio = config.debug ? 'inherit' : 'ignore'; + const DEFAULT_APK_DIR = '/android/app/build/outputs/apk/debug/'; + const cwd = path.join(process.cwd(), DEFAULT_APK_DIR); + + const appFilename = `app-debug.apk`; + const appPath = path.join(cwd, appFilename); + const { packageName } = config.android!; + + const installCommand = `adb install -r ${appPath}`; + await execa.command(installCommand, { stdio }); + + const launchCommand = `adb shell monkey -p "${packageName}" -c android.intent.category.LAUNCHER 1`; + await execa.command(launchCommand, { stdio }); }; export const runHandler = async (args: BuildRunOptions) => { diff --git a/src/cli/types.ts b/src/cli/types.ts index 6bb5460a..89e1036e 100644 --- a/src/cli/types.ts +++ b/src/cli/types.ts @@ -21,6 +21,8 @@ type ConfigIOS = { }; type ConfigAndroid = { + /** The package name of your Android app. See Manifest.xml. */ + packageName: string; /** Overrides the `assembleDebug` gradle command. Should build the apk. */ buildCommand?: string; /** Passes the quiet flag to `gradlew`. */ From 644684c8df9dffc341bd0867eba5b7ce5ade5dc6 Mon Sep 17 00:00:00 2001 From: Emmanouil Konstantinidis Date: Wed, 23 Dec 2020 20:04:11 +0000 Subject: [PATCH 5/9] chore: Tests for running the app on Android --- src/cli/run.test.ts | 51 ++++++++++++++++++++++++++++++++++++++++++--- src/cli/run.ts | 2 +- 2 files changed, 49 insertions(+), 4 deletions(-) diff --git a/src/cli/run.test.ts b/src/cli/run.test.ts index f7ab97cd..46b8de22 100644 --- a/src/cli/run.test.ts +++ b/src/cli/run.test.ts @@ -65,7 +65,7 @@ describe('run.ts', () => { expect(execMock).toHaveBeenNthCalledWith( 2, `xcrun simctl launch iPhone\\ Simulator ${bundleIdIOS}`, - { cwd, stdio: 'inherit' } + { stdio: 'inherit' } ); expect(run.getIOSBundleIdentifier).toHaveBeenCalledWith(appPath); @@ -98,7 +98,7 @@ describe('run.ts', () => { expect(execMock).toHaveBeenNthCalledWith( 2, `xcrun simctl launch iPhone\\ Simulator ${bundleIdIOS}`, - { cwd, stdio: 'inherit' } + { stdio: 'inherit' } ); expect(run.getIOSBundleIdentifier).toHaveBeenCalledWith(appPath); @@ -106,13 +106,58 @@ describe('run.ts', () => { }); describe('runAndroid', () => { + const cwd = path.join( + process.cwd(), + '/android/app/build/outputs/apk/debug' + ); + const appPath = path.join(cwd, 'app-debug.apk'); + const execMock = jest.spyOn(execa, 'command').mockImplementation(); beforeEach(() => { execMock.mockReset(); }); - pending('runs an Android project'); + it('runs an Android project - with the default build command', async () => { + const config: Config = { + android: { + packageName: 'com.rndemo', + }, + }; + + await run.runAndroid(config, logger); + + expect(execMock).toHaveBeenNthCalledWith(1, `adb install -r ${appPath}`, { + stdio: 'ignore', + }); + + expect(execMock).toHaveBeenNthCalledWith( + 2, + `adb shell monkey -p \"com.rndemo\" -c android.intent.category.LAUNCHER 1`, + { stdio: 'ignore' } + ); + }); + + it('runs an Android project - with the default build command', async () => { + const config: Config = { + android: { + packageName: 'com.rndemo', + buildCommand: "echo 'Hello World'", + }, + }; + + await run.runAndroid(config, logger); + + expect(execMock).toHaveBeenNthCalledWith(1, `adb install -r ${appPath}`, { + stdio: 'ignore', + }); + + expect(execMock).toHaveBeenNthCalledWith( + 2, + `adb shell monkey -p \"com.rndemo\" -c android.intent.category.LAUNCHER 1`, + { stdio: 'ignore' } + ); + }); }); describe('runHandler', () => { diff --git a/src/cli/run.ts b/src/cli/run.ts index 208bc5b9..a3d669f1 100644 --- a/src/cli/run.ts +++ b/src/cli/run.ts @@ -32,7 +32,7 @@ export const runIOS = async (config: Config, logger: Logger) => { await execa.command(installCommand, { stdio: 'inherit', cwd }); const launchCommand = `xcrun simctl launch ${simulator} ${bundleId}`; - await execa.command(launchCommand, { stdio: 'inherit', cwd }); + await execa.command(launchCommand, { stdio: 'inherit' }); }; export const runAndroid = async (config: Config, logger: Logger) => { From f03115f066616cf2f1e2d95c5242a2fcd9da5c73 Mon Sep 17 00:00:00 2001 From: Emmanouil Konstantinidis Date: Mon, 4 Jan 2021 16:14:23 +0000 Subject: [PATCH 6/9] feat: Add support for binaryPath on Android config --- package.json | 2 +- src/cli/config.ts | 2 ++ src/cli/run.ts | 3 ++- src/cli/types.ts | 2 ++ yarn.lock | 8 ++++---- 5 files changed, 11 insertions(+), 6 deletions(-) diff --git a/package.json b/package.json index be7f239e..c9fc2b83 100644 --- a/package.json +++ b/package.json @@ -21,7 +21,7 @@ "testEnvironment": "node" }, "dependencies": { - "ajv": "^7.0.1", + "ajv": "^7.0.3", "execa": "^5.0.0", "yargs": "^16.2.0" }, diff --git a/src/cli/config.ts b/src/cli/config.ts index 782ee1be..6cbad058 100644 --- a/src/cli/config.ts +++ b/src/cli/config.ts @@ -30,9 +30,11 @@ export const validateSchema = (config: {}): Promise => { properties: { packageName: { type: 'string' }, buildCommand: { type: 'string', nullable: true }, + binaryPath: { type: 'string', nullable: true }, quiet: { type: 'boolean', nullable: true }, }, required: ['packageName'], + anyOf: [{ required: [] }, { required: ['buildCommand', 'binaryPath'] }], nullable: true, additionalProperties: false, }, diff --git a/src/cli/run.ts b/src/cli/run.ts index a3d669f1..f30c53cf 100644 --- a/src/cli/run.ts +++ b/src/cli/run.ts @@ -38,7 +38,8 @@ export const runIOS = async (config: Config, logger: Logger) => { export const runAndroid = async (config: Config, logger: Logger) => { const stdio = config.debug ? 'inherit' : 'ignore'; const DEFAULT_APK_DIR = '/android/app/build/outputs/apk/debug/'; - const cwd = path.join(process.cwd(), DEFAULT_APK_DIR); + const cwd = + config.android?.binaryPath || path.join(process.cwd(), DEFAULT_APK_DIR); const appFilename = `app-debug.apk`; const appPath = path.join(cwd, appFilename); diff --git a/src/cli/types.ts b/src/cli/types.ts index 89e1036e..d5be46ea 100644 --- a/src/cli/types.ts +++ b/src/cli/types.ts @@ -25,6 +25,8 @@ type ConfigAndroid = { packageName: string; /** Overrides the `assembleDebug` gradle command. Should build the apk. */ buildCommand?: string; + /** Path to the .apk that will get generated by a custom build command. Ignored when not using a custom build command. */ + binaryPath?: string; /** Passes the quiet flag to `gradlew`. */ quiet?: boolean; }; diff --git a/yarn.lock b/yarn.lock index 85a04812..e0365d08 100644 --- a/yarn.lock +++ b/yarn.lock @@ -611,10 +611,10 @@ ajv@^6.12.3: json-schema-traverse "^0.4.1" uri-js "^4.2.2" -ajv@^7.0.1: - version "7.0.1" - resolved "https://registry.yarnpkg.com/ajv/-/ajv-7.0.1.tgz#d39ed49159672a91f796e2563e1e96984956e338" - integrity sha512-D2UYOXvDzGLVwld2/EyRu3xiJG885QUz2xS1phZzebYLPMPBFbPK4naXGDCtPltZoOG+I1+ZkNAJ65SJ3vqbsg== +ajv@^7.0.3: + version "7.0.3" + resolved "https://registry.yarnpkg.com/ajv/-/ajv-7.0.3.tgz#13ae747eff125cafb230ac504b2406cf371eece2" + integrity sha512-R50QRlXSxqXcQP5SvKUrw8VZeypvo12i2IX0EeR5PiZ7bEKeHWgzgo264LDadUsCU42lTJVhFikTqJwNeH34gQ== dependencies: fast-deep-equal "^3.1.1" json-schema-traverse "^1.0.0" From cc8ac1374fcd5619bbd726d4ba0d43908dec41b2 Mon Sep 17 00:00:00 2001 From: Emmanouil Konstantinidis Date: Mon, 4 Jan 2021 16:33:13 +0000 Subject: [PATCH 7/9] chore: Fix tests --- src/cli/run.test.ts | 28 ++++++++++++++++++---------- src/cli/run.ts | 18 +++++++++--------- 2 files changed, 27 insertions(+), 19 deletions(-) diff --git a/src/cli/run.test.ts b/src/cli/run.test.ts index 46b8de22..af8a17bb 100644 --- a/src/cli/run.test.ts +++ b/src/cli/run.test.ts @@ -72,8 +72,8 @@ describe('run.ts', () => { }); it('runs an iOS project - with a custom build command and binaryPath', async () => { - const cwd = path.join(process.cwd(), '/ios/path/to'); - const appPath = path.join(cwd, 'RNDemo.app'); + const binaryPath = '/Users/Demo/Desktop/RNDemo.app'; + const cwd = path.dirname(binaryPath); jest .spyOn(run, 'getIOSBundleIdentifier') @@ -82,7 +82,7 @@ describe('run.ts', () => { const config: Config = { ios: { buildCommand: "echo 'Hello World'", - binaryPath: './ios/path/to/RNDemo.app', + binaryPath, device: 'iPhone Simulator', }, }; @@ -101,7 +101,7 @@ describe('run.ts', () => { { stdio: 'inherit' } ); - expect(run.getIOSBundleIdentifier).toHaveBeenCalledWith(appPath); + expect(run.getIOSBundleIdentifier).toHaveBeenCalledWith(binaryPath); }); }); @@ -110,7 +110,6 @@ describe('run.ts', () => { process.cwd(), '/android/app/build/outputs/apk/debug' ); - const appPath = path.join(cwd, 'app-debug.apk'); const execMock = jest.spyOn(execa, 'command').mockImplementation(); @@ -119,6 +118,8 @@ describe('run.ts', () => { }); it('runs an Android project - with the default build command', async () => { + const appPath = path.join(cwd, 'app-debug.apk'); + const config: Config = { android: { packageName: 'com.rndemo', @@ -138,19 +139,26 @@ describe('run.ts', () => { ); }); - it('runs an Android project - with the default build command', async () => { + it('runs an Android project - with a custom build command', async () => { + const binaryPath = '/Users/Demo/Desktop/app-debug.apk'; + const config: Config = { android: { packageName: 'com.rndemo', - buildCommand: "echo 'Hello World'", + buildCommand: './gradlew example', + binaryPath, }, }; await run.runAndroid(config, logger); - expect(execMock).toHaveBeenNthCalledWith(1, `adb install -r ${appPath}`, { - stdio: 'ignore', - }); + expect(execMock).toHaveBeenNthCalledWith( + 1, + `adb install -r ${binaryPath}`, + { + stdio: 'ignore', + } + ); expect(execMock).toHaveBeenNthCalledWith( 2, diff --git a/src/cli/run.ts b/src/cli/run.ts index f30c53cf..dfe09620 100644 --- a/src/cli/run.ts +++ b/src/cli/run.ts @@ -14,12 +14,9 @@ export const getIOSBundleIdentifier = (appPath: string): string => { export const runIOS = async (config: Config, logger: Logger) => { const DEFAULT_BINARY_DIR = '/ios/build/Build/Products/Debug-iphonesimulator'; - const cwd = path.join( - process.cwd(), - config.ios?.binaryPath - ? path.dirname(config.ios?.binaryPath) - : DEFAULT_BINARY_DIR - ); + const cwd = config.ios?.binaryPath + ? path.dirname(config.ios?.binaryPath) + : path.join(process.cwd(), DEFAULT_BINARY_DIR); const appFilename = config.ios!.binaryPath ? path.basename(config.ios!.binaryPath) @@ -38,10 +35,13 @@ export const runIOS = async (config: Config, logger: Logger) => { export const runAndroid = async (config: Config, logger: Logger) => { const stdio = config.debug ? 'inherit' : 'ignore'; const DEFAULT_APK_DIR = '/android/app/build/outputs/apk/debug/'; - const cwd = - config.android?.binaryPath || path.join(process.cwd(), DEFAULT_APK_DIR); + const cwd = config.android?.binaryPath + ? path.dirname(config.android?.binaryPath) + : path.join(process.cwd(), DEFAULT_APK_DIR); - const appFilename = `app-debug.apk`; + const appFilename = config.android!.binaryPath + ? path.basename(config.android!.binaryPath) + : 'app-debug.apk'; const appPath = path.join(cwd, appFilename); const { packageName } = config.android!; From 744871cbf7d7f939fa121c9e7ae8b96337c37041 Mon Sep 17 00:00:00 2001 From: Emmanouil Konstantinidis Date: Mon, 4 Jan 2021 16:38:29 +0000 Subject: [PATCH 8/9] chore: Add example to README and binary path to config options --- README.md | 45 +++++++++++++++++++++++++++++++-------------- 1 file changed, 31 insertions(+), 14 deletions(-) diff --git a/README.md b/README.md index 593c9ec3..4fbbc7c7 100644 --- a/README.md +++ b/README.md @@ -14,20 +14,37 @@ npm install -D react-native-owl The config file - which unless specified in the cli should live in `./owl.config.json` - is used to descript how Owl should run your app and your tests. Below you can find all the options the can be specified. -| Name | Required | Description | -| ---------------------- | -------- | ------------------------------------------------------------------------------------------- | -| **general** | | | -| `debug` | false | Prevents the CLI/library from printing any logs/output | -| **ios config** | | | -| `ios.workspace` | true | Path to the `.xcworkspace` file of your react-native project | -| `ios.scheme` | true | The name of the scheme you would like to use for building the app | -| `ios.buildCommand` | false | Overrides the `xcodebuild` command making the above options obselete. Used only if required | -| `ios.quiet` | false | Passes the quiet flag to `xcode builds` | -| **android config** | | | -| `android.buildCommand` | false | Overrides the `assembleDebug` gradle command. Should build the apk | -| `android.quiet` | false | Passes the quiet flag to `gradlew` | - - | +### Options + +| Name | Required | Description | +| ---------------------- | -------- | -------------------------------------------------------------------- | +| **general** | | | +| `debug` | false | Prevents the CLI/library from printing any logs/output | +| **ios config** | | | +| `ios.workspace` | true | Path to the `.xcworkspace` file of your react-native project | +| `ios.scheme` | true | The name of the scheme you would like to use for building the app | +| `ios.buildCommand` | false | Overrides the `xcodebuild` command making the above options obselete | +| `ios.binaryPath` | false | The path to the binary, if you are using a custom build command | +| `ios.quiet` | false | Passes the quiet flag to `xcode builds` | +| **android config** | | | +| `android.buildCommand` | false | Overrides the `assembleDebug` gradle command. Should build the apk | +| `android.binaryPath` | false | The path to the binary, if you are using a custom build command | +| `android.quiet` | false | Passes the quiet flag to `gradlew` | + +### Example + +```json +{ + "ios": { + "workspace": "ios/OwlDemoApp.xcworkspace", + "scheme": "OwlDemoApp", + "device": "iPhone 12 Pro" + }, + "android": { + "packageName": "com.owldemoapp" + } +} +``` ## CLI From 5766ded4ab1fc8cfa08843b3f385ee6e069f316f Mon Sep 17 00:00:00 2001 From: Emmanouil Konstantinidis Date: Mon, 4 Jan 2021 17:02:17 +0000 Subject: [PATCH 9/9] chore: Better debug logs --- src/cli/build.ts | 8 +++++--- src/cli/run.ts | 4 +++- 2 files changed, 8 insertions(+), 4 deletions(-) diff --git a/src/cli/build.ts b/src/cli/build.ts index 90751e59..17e2a1a0 100644 --- a/src/cli/build.ts +++ b/src/cli/build.ts @@ -51,9 +51,11 @@ export const buildHandler = async (args: BuildRunOptions) => { const logger = createLogger(config.debug); const buildProject = args.platform === 'ios' ? buildIOS : buildAndroid; - await buildProject(config, logger); - logger.info( - `OWL will build for the ${args.platform} platform. Config file: ${args.config}` + `[OWL] Will build the app on ${args.platform} platform. Config file: ${args.config}` ); + + await buildProject(config, logger); + + logger.info(`[OWL] Successfully built for the ${args.platform} platform.`); }; diff --git a/src/cli/run.ts b/src/cli/run.ts index dfe09620..d9313b3e 100644 --- a/src/cli/run.ts +++ b/src/cli/run.ts @@ -57,7 +57,9 @@ export const runHandler = async (args: BuildRunOptions) => { const logger = createLogger(config.debug); const runProject = args.platform === 'ios' ? runIOS : runAndroid; + logger.info(`[OWL] Will run the app on ${args.platform}.`); + await runProject(config, logger); - logger.info(`OWL will run the app on ${args.platform}.`); + logger.info(`[OWL] Successfully run the app on ${args.platform}.`); };