diff --git a/docs/api/methods.md b/docs/api/methods.md index 878835f3..5caaa792 100644 --- a/docs/api/methods.md +++ b/docs/api/methods.md @@ -235,3 +235,36 @@ describe('App.tsx', () => { }); }); ``` + +### reload() + +Terminates the app in the emulator/simulator, and restarts it. + +This is useful when you want to complete a set of tests and nove onto a new set of tests. + +Depending on your use-case, you could call `reload` in a `beforeAll`, or `beforeEach` callback, or within a test case, to get the app into a clean state before each set of tests for example. + + +#### Example + + +```js title="__tests__/App.owl.tsx" +import { reload } from 'react-native-owl'; + +describe('App.tsx', () => { + describe('the checkout flow', () => { + beforeAll(async () => { + // highlight-next-line + await reload(); + }), + + it('adds product to cart', async () => { + ... + }); + + it('starts checkout flow', async () => { + ... + }); + }); +}); +``` diff --git a/example/.owl/baseline/android/after-reload.png b/example/.owl/baseline/android/after-reload.png new file mode 100644 index 00000000..f27c9f09 Binary files /dev/null and b/example/.owl/baseline/android/after-reload.png differ diff --git a/example/.owl/baseline/ios/after-reload.png b/example/.owl/baseline/ios/after-reload.png new file mode 100644 index 00000000..a2e2ef3a Binary files /dev/null and b/example/.owl/baseline/ios/after-reload.png differ diff --git a/example/__tests__/App.owl.tsx b/example/__tests__/App.owl.tsx index 7ebecc88..b9548f63 100644 --- a/example/__tests__/App.owl.tsx +++ b/example/__tests__/App.owl.tsx @@ -6,56 +6,71 @@ import { scrollTo, scrollToEnd, longPress, + reload, } from 'react-native-owl'; jest.setTimeout(30000); describe('App.tsx', () => { - it('takes a screenshot of the initial screen', async () => { - const screen = await takeScreenshot('initial'); + describe('Basic navigation', () => { + it('takes a screenshot of the initial screen', async () => { + const screen = await takeScreenshot('initial'); - expect(screen).toMatchBaseline(); - }); + expect(screen).toMatchBaseline(); + }); - it('longPress a Pressable, then takes a screenshot', async () => { - await longPress('Pressable'); + it('longPress a Pressable, then takes a screenshot', async () => { + await longPress('Pressable'); - const screen = await takeScreenshot('long-press'); + const screen = await takeScreenshot('long-press'); - expect(screen).toMatchBaseline(); - }); + expect(screen).toMatchBaseline(); + }); - it('press a Pressable, waits for an element then takes a screenshot', async () => { - await press('Pressable'); + it('press a Pressable, waits for an element then takes a screenshot', async () => { + await press('Pressable'); - await toExist('TextInput'); + await toExist('TextInput'); - const screen = await takeScreenshot('test-input'); + const screen = await takeScreenshot('test-input'); - expect(screen).toMatchBaseline(); - }); + expect(screen).toMatchBaseline(); + }); - it('enters some text and takes a screenshot', async () => { - await changeText('TextInput', 'Entered text'); + it('enters some text and takes a screenshot', async () => { + await changeText('TextInput', 'Entered text'); - const screen = await takeScreenshot('entered-text'); + const screen = await takeScreenshot('entered-text'); - expect(screen).toMatchBaseline(); - }); + expect(screen).toMatchBaseline(); + }); + + it('scrolls a bit and takes a screenshot', async () => { + await scrollTo('ScrollView', { y: 50 }); + + const screen = await takeScreenshot('scroll-to'); + + expect(screen).toMatchBaseline(); + }); - it('scrolls a bit and takes a screenshot', async () => { - await scrollTo('ScrollView', { y: 50 }); + it('scrolls to end and takes a screenshot', async () => { + await scrollToEnd('ScrollView'); - const screen = await takeScreenshot('scroll-to'); + const screen = await takeScreenshot('scroll-to-end'); - expect(screen).toMatchBaseline(); + expect(screen).toMatchBaseline(); + }); }); - it('scrolls to end and takes a screenshot', async () => { - await scrollToEnd('ScrollView'); + describe('Reload example', () => { + beforeAll(async () => { + await reload(); + }); - const screen = await takeScreenshot('scroll-to-end'); + it('takes a screenshot of the welcome screen', async () => { + const screen = await takeScreenshot('after-reload'); - expect(screen).toMatchBaseline(); + expect(screen).toMatchBaseline(); + }); }); }); diff --git a/example/ios/Podfile.lock b/example/ios/Podfile.lock index c12e36d0..3680f150 100644 --- a/example/ios/Podfile.lock +++ b/example/ios/Podfile.lock @@ -523,4 +523,4 @@ SPEC CHECKSUMS: PODFILE CHECKSUM: 3190b6d7739c1a0f98e11d88f08dd49936f770e0 -COCOAPODS: 1.11.2 +COCOAPODS: 1.11.3 diff --git a/example/package.json b/example/package.json index ed5ecb1d..59deb67b 100644 --- a/example/package.json +++ b/example/package.json @@ -17,7 +17,7 @@ "dependencies": { "react": "17.0.2", "react-native": "0.66.0", - "react-native-owl": "link:../" + "react-native-owl": "link:.." }, "devDependencies": { "@babel/core": "^7.12.9", diff --git a/lib/actions.ts b/lib/actions.ts index b79282ab..117ed0c8 100644 --- a/lib/actions.ts +++ b/lib/actions.ts @@ -1,4 +1,10 @@ +import { getConfig } from './cli/config'; + import { Logger } from './logger'; +import { CliRunOptions } from './types'; +import { adbLaunch, adbTerminate } from './utils/adb'; +import { waitFor } from './utils/wait-for'; +import { xcrunLaunch, xcrunTerminate, xcrunUi } from './utils/xcrun'; import { createWebSocketClient } from './websocket'; import { SOCKET_TEST_REQUEST, @@ -57,3 +63,62 @@ export const scrollToEnd = (testID: string) => export const toExist = (testID: string) => sendEvent({ type: 'LAYOUT', action: 'EXISTS', testID }); + +export const reload = async () => { + const args = (global as any).OWL_CLI_ARGS as CliRunOptions; + + if (!args) { + return; + } + + const config = await getConfig(args.config); + + if (args.platform === 'ios') { + if (!config.ios?.device) { + return Promise.reject('Missing device name'); + } + + await xcrunTerminate({ + debug: config.debug, + binaryPath: config.ios?.binaryPath, + device: config.ios.device, + scheme: config.ios?.scheme, + configuration: config.ios?.configuration, + }); + + await xcrunLaunch({ + debug: config.debug, + binaryPath: config.ios?.binaryPath, + device: config.ios.device, + scheme: config.ios?.scheme, + configuration: config.ios?.configuration, + }); + + await waitFor(1000); + + await xcrunUi({ + debug: config.debug, + device: config.ios.device, + configuration: config.ios.configuration, + binaryPath: config.ios.binaryPath, + }); + } + + if (args.platform === 'android') { + if (!config.android?.packageName) { + return Promise.reject('Missing package name'); + } + + await adbTerminate({ + debug: config.debug, + packageName: config.android.packageName, + }); + + await adbLaunch({ + debug: config.debug, + packageName: config.android.packageName, + }); + + await waitFor(500); + } +}; diff --git a/lib/cli/run.test.ts b/lib/cli/run.test.ts index 00646e9e..dbbeadd5 100644 --- a/lib/cli/run.test.ts +++ b/lib/cli/run.test.ts @@ -1,44 +1,38 @@ -import path from 'path'; import { promises as fs } from 'fs'; -import execa, { ExecaReturnValue } from 'execa'; +import path from 'path'; +import * as reportHelpers from '../report'; +import * as configHelpers from './config'; import { CliRunOptions, Config } from '../types'; -import { Logger } from '../logger'; -import * as configHelpers from './config'; import * as run from './run'; -import * as reportHelpers from '../report'; - -describe('run.ts', () => { - const logger = new Logger(); - const bundleIdIOS = 'org.reactjs.native.example.RNDemo'; - const mockBundleIdResponse = { stdout: bundleIdIOS } as ExecaReturnValue; +import * as xcrun from '../utils/xcrun'; +import * as adb from '../utils/adb'; +import execa from 'execa'; +import { Logger } from '../logger'; - const mkdirMock = jest.spyOn(fs, 'mkdir'); +jest.mock('../utils/xcrun'); +jest.mock('../utils/adb'); - jest - .spyOn(process, 'cwd') - .mockReturnValue('/Users/johndoe/Projects/my-project'); +jest + .spyOn(process, 'cwd') + .mockReturnValue('/Users/johndoe/Projects/my-project'); +describe('run.ts', () => { + const mkdirMock = jest.spyOn(fs, 'mkdir'); const execKillMock = { kill: jest.fn(), } as unknown as execa.ExecaChildProcess; + const execMock = jest.spyOn(execa, 'command').mockImplementation(); beforeEach(() => { mkdirMock.mockReset(); execMock.mockReset().mockReturnValue(execKillMock); + jest.clearAllMocks(); }); describe('runIOS', () => { - it('runs an iOS project - with the default build command', async () => { - const cwd = path.join( - process.cwd(), - '/ios/build/Build/Products/Debug-iphonesimulator' - ); - const plistPath = path.join(cwd, 'RNDemo.app', 'Info.plist'); - - execMock.mockResolvedValueOnce(mockBundleIdResponse); - + it('runs an iOS project', async () => { const config: Config = { ios: { workspace: 'ios/RNDemo.xcworkspace', @@ -48,84 +42,47 @@ describe('run.ts', () => { }, }; - await run.runIOS(config, logger); - - expect(execMock).toHaveBeenNthCalledWith( - 1, - `./PlistBuddy -c 'Print CFBundleIdentifier' ${plistPath}`, - { cwd: '/usr/libexec', shell: true } - ); - - expect(execMock).toHaveBeenNthCalledWith( - 2, - 'xcrun simctl status_bar iPhone\\ Simulator override --time 9:41', - { cwd, stdio: 'ignore' } - ); - - expect(execMock).toHaveBeenNthCalledWith( - 3, - 'xcrun simctl install iPhone\\ Simulator RNDemo.app', - { cwd, stdio: 'ignore' } - ); - - expect(execMock).toHaveBeenNthCalledWith( - 4, - `xcrun simctl launch iPhone\\ Simulator ${bundleIdIOS}`, - { stdio: 'ignore' } - ); - }); - - it('runs an iOS project - with a custom build command and binaryPath', async () => { - const binaryPath = 'custom/path/RNDemo.app'; - const cwd = path.dirname(binaryPath); - - execMock.mockResolvedValueOnce(mockBundleIdResponse); - - const config: Config = { - ios: { - buildCommand: "echo 'Hello World'", - binaryPath, - device: 'iPhone Simulator', - }, - }; - - await run.runIOS(config, logger); + await run.runIOS(config); - expect(execMock).toHaveBeenNthCalledWith( - 1, - `./PlistBuddy -c 'Print CFBundleIdentifier' ${binaryPath}/Info.plist`, - { cwd: '/usr/libexec', shell: true } - ); + expect(xcrun.xcrunStatusBar).toHaveBeenCalledTimes(1); + expect(xcrun.xcrunStatusBar).toHaveBeenCalledWith({ + debug: config.debug, + device: config.ios?.device, + configuration: config.ios?.configuration, + binaryPath: config.ios?.binaryPath, + }); - expect(execMock).toHaveBeenNthCalledWith( - 2, - 'xcrun simctl status_bar iPhone\\ Simulator override --time 9:41', - { cwd, stdio: 'ignore' } - ); + expect(xcrun.xcrunInstall).toHaveBeenCalledTimes(1); + expect(xcrun.xcrunInstall).toHaveBeenCalledWith({ + debug: config.debug, + device: config.ios?.device, + configuration: config.ios?.configuration, + binaryPath: config.ios?.binaryPath, + scheme: config.ios?.scheme, + }); - expect(execMock).toHaveBeenNthCalledWith( - 3, - 'xcrun simctl install iPhone\\ Simulator RNDemo.app', - { cwd, stdio: 'ignore' } - ); + expect(xcrun.xcrunLaunch).toHaveBeenCalledTimes(1); + expect(xcrun.xcrunLaunch).toHaveBeenCalledWith({ + debug: config.debug, + device: config.ios?.device, + configuration: config.ios?.configuration, + binaryPath: config.ios?.binaryPath, + scheme: config.ios?.scheme, + }); - expect(execMock).toHaveBeenNthCalledWith( - 4, - `xcrun simctl launch iPhone\\ Simulator ${bundleIdIOS}`, - { stdio: 'ignore' } - ); + expect(xcrun.xcrunUi).toHaveBeenCalledTimes(1); + expect(xcrun.xcrunUi).toHaveBeenCalledWith({ + debug: config.debug, + device: config.ios?.device, + configuration: config.ios?.configuration, + binaryPath: config.ios?.binaryPath, + }); }); }); describe('restoreIOSUI', () => { it('cleans up an iOS project', async () => { - const cwd = path.join( - process.cwd(), - '/ios/build/Build/Products/Debug-iphonesimulator' - ); - - execMock.mockResolvedValueOnce(mockBundleIdResponse); - + const logger = new Logger(); const config: Config = { ios: { workspace: 'ios/RNDemo.xcworkspace', @@ -137,23 +94,18 @@ describe('run.ts', () => { await run.restoreIOSUI(config, logger); - expect(execMock).toHaveBeenNthCalledWith( - 1, - 'xcrun simctl status_bar iPhone\\ Simulator clear', - { cwd, stdio: 'ignore' } - ); + expect(xcrun.xcrunRestore).toHaveBeenCalledTimes(1); + expect(xcrun.xcrunRestore).toHaveBeenCalledWith({ + debug: config.debug, + device: config.ios?.device, + configuration: config.ios?.configuration, + binaryPath: config.ios?.binaryPath, + }); }); }); describe('runAndroid', () => { - const cwd = path.join( - process.cwd(), - '/android/app/build/outputs/apk/release' - ); - - it('runs an Android project - with the default build command', async () => { - const appPath = path.join(cwd, 'app-release.apk'); - + it('runs an Android project', async () => { const config: Config = { android: { packageName: 'com.rndemo', @@ -161,39 +113,20 @@ describe('run.ts', () => { }, }; - await run.runAndroid(config, logger); + await run.runAndroid(config); - expect(execMock).toHaveBeenNthCalledWith(1, `adb install -r ${appPath}`, { - stdio: 'ignore', + expect(adb.adbInstall).toHaveBeenCalledTimes(1); + expect(adb.adbInstall).toHaveBeenCalledWith({ + debug: config.debug, + buildType: config.android?.buildType, + binaryPath: config.android?.binaryPath, }); - expect(execMock).toHaveBeenNthCalledWith( - 2, - `adb shell monkey -p \"com.rndemo\" -c android.intent.category.LAUNCHER 1`, - { stdio: 'ignore' } - ); - }); - - it('runs an Android project - with a custom build command', async () => { - const binaryPath = '/Users/Demo/Desktop/app-release.apk'; - - const config: Config = { - android: { - packageName: 'com.rndemo', - buildCommand: './gradlew example', - binaryPath, - }, - }; - - await run.runAndroid(config, logger); - - expect(execMock).toHaveBeenNthCalledWith( - 1, - `adb install -r ${binaryPath}`, - { - stdio: 'ignore', - } - ); + expect(adb.adbLaunch).toHaveBeenCalledTimes(1); + expect(adb.adbLaunch).toHaveBeenCalledWith({ + debug: config.debug, + packageName: config.android?.packageName, + }); }); }); @@ -222,8 +155,6 @@ describe('run.ts', () => { process.cwd() )} --runInBand`; - const expectedJestCommandWithReport = `${expectedJestCommand} --json --outputFile=/Users/johndoe/Projects/my-project/.owl/report/jest-report.json`; - const commandSyncMock = jest.spyOn(execa, 'commandSync'); const mockGenerateReport = jest.spyOn(reportHelpers, 'generateReport'); @@ -251,7 +182,7 @@ describe('run.ts', () => { await expect(mockRunIOS).toHaveBeenCalled(); await expect(commandSyncMock).toHaveBeenCalledTimes(1); await expect(commandSyncMock).toHaveBeenCalledWith( - expectedJestCommandWithReport, + `${expectedJestCommand} --globals='{\"OWL_CLI_ARGS\":{\"platform\":\"ios\",\"config\":\"./owl.config.json\",\"update\":false}}' --json --outputFile=/Users/johndoe/Projects/my-project/.owl/report/jest-report.json`, { env: { OWL_DEBUG: 'false', @@ -275,15 +206,18 @@ describe('run.ts', () => { await expect(mockRunAndroid).toHaveBeenCalled(); await expect(commandSyncMock).toHaveBeenCalledTimes(1); - await expect(commandSyncMock).toHaveBeenCalledWith(expectedJestCommand, { - env: { - OWL_DEBUG: 'false', - OWL_IOS_SIMULATOR: 'iPhone Simulator', - OWL_PLATFORM: 'android', - OWL_UPDATE_BASELINE: 'false', - }, - stdio: 'inherit', - }); + await expect(commandSyncMock).toHaveBeenCalledWith( + `${expectedJestCommand} --globals='{\"OWL_CLI_ARGS\":{\"platform\":\"android\",\"config\":\"./owl.config.json\",\"update\":false}}'`, + { + env: { + OWL_DEBUG: 'false', + OWL_IOS_SIMULATOR: 'iPhone Simulator', + OWL_PLATFORM: 'android', + OWL_UPDATE_BASELINE: 'false', + }, + stdio: 'inherit', + } + ); }); it('runs with the update baseline flag on', async () => { @@ -294,15 +228,18 @@ describe('run.ts', () => { await expect(mockRunIOS).toHaveBeenCalled(); await expect(commandSyncMock).toHaveBeenCalledTimes(1); - await expect(commandSyncMock).toHaveBeenCalledWith(expectedJestCommand, { - env: { - OWL_DEBUG: 'false', - OWL_IOS_SIMULATOR: 'iPhone Simulator', - OWL_PLATFORM: 'ios', - OWL_UPDATE_BASELINE: 'true', - }, - stdio: 'inherit', - }); + await expect(commandSyncMock).toHaveBeenCalledWith( + `${expectedJestCommand} --globals='{\"OWL_CLI_ARGS\":{\"platform\":\"ios\",\"config\":\"./owl.config.json\",\"update\":true}}'`, + { + env: { + OWL_DEBUG: 'false', + OWL_IOS_SIMULATOR: 'iPhone Simulator', + OWL_PLATFORM: 'ios', + OWL_UPDATE_BASELINE: 'true', + }, + stdio: 'inherit', + } + ); }); it('runs the scripts/websocket-server.js script', async () => { @@ -350,7 +287,7 @@ describe('run.ts', () => { await run.runHandler({ ...args }); } catch { await expect(commandSyncMock).toHaveBeenCalledWith( - expectedJestCommand, + `${expectedJestCommand} --globals='{\"OWL_CLI_ARGS\":{\"platform\":\"ios\",\"config\":\"./owl.config.json\",\"update\":false}}'`, { env: { OWL_DEBUG: 'false', diff --git a/lib/cli/run.ts b/lib/cli/run.ts index 72df6054..f8e610a0 100644 --- a/lib/cli/run.ts +++ b/lib/cli/run.ts @@ -8,80 +8,84 @@ import { generateReport, cleanupReport } from '../report'; import { getConfig } from './config'; import { Logger } from '../logger'; import { waitFor } from '../utils/wait-for'; +import { adbInstall, adbLaunch } from '../utils/adb'; +import { + xcrunInstall, + xcrunLaunch, + xcrunRestore, + xcrunStatusBar, + xcrunUi, +} from '../utils/xcrun'; + +export const runIOS = async (config: Config) => { + if (!config.ios) { + return; + } -export const runIOS = async (config: Config, logger: Logger) => { - const stdio = config.debug ? 'inherit' : 'ignore'; - const DEFAULT_BINARY_DIR = `/ios/build/Build/Products/${config.ios?.configuration}-iphonesimulator`; - 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) - : `${config.ios!.scheme}.app`; - const plistPath = path.join(cwd, appFilename, 'Info.plist'); - const simulator = config.ios!.device.replace(/([ /])/g, '\\$1'); - - const { stdout: bundleId } = await execa.command( - `./PlistBuddy -c 'Print CFBundleIdentifier' ${plistPath}`, - { shell: true, cwd: '/usr/libexec' } - ); - - logger.print(`[OWL - CLI] Found bundle id: ${bundleId}`); - - const SIMULATOR_TIME = '9:41'; - const setTimeCommand = `xcrun simctl status_bar ${simulator} override --time ${SIMULATOR_TIME}`; - await execa.command(setTimeCommand, { stdio, cwd }); + await xcrunStatusBar({ + debug: config.debug, + device: config.ios.device, + configuration: config.ios.configuration, + binaryPath: config.ios.binaryPath, + }); - const installCommand = `xcrun simctl install ${simulator} ${appFilename}`; - await execa.command(installCommand, { stdio, cwd }); + await xcrunInstall({ + debug: config.debug, + device: config.ios.device, + configuration: config.ios.configuration, + binaryPath: config.ios.binaryPath, + scheme: config.ios.scheme, + }); - const launchCommand = `xcrun simctl launch ${simulator} ${bundleId}`; - await execa.command(launchCommand, { stdio }); + await xcrunLaunch({ + debug: config.debug, + device: config.ios.device, + configuration: config.ios.configuration, + binaryPath: config.ios.binaryPath, + scheme: config.ios.scheme, + }); await waitFor(1000); // Workaround to force the virtual home button's color to become consistent - const appearanceCommand = `xcrun simctl ui ${simulator} appearance`; - await execa.command(`${appearanceCommand} dark`, { stdio, cwd }); - await waitFor(500); - await execa.command(`${appearanceCommand} light`, { stdio, cwd }); - await waitFor(500); + await xcrunUi({ + debug: config.debug, + device: config.ios.device, + configuration: config.ios.configuration, + binaryPath: config.ios.binaryPath, + }); }; export const restoreIOSUI = async (config: Config, logger: Logger) => { - const stdio = config.debug ? 'inherit' : 'ignore'; - const DEFAULT_BINARY_DIR = `/ios/build/Build/Products/${config.ios?.configuration}-iphonesimulator`; - const cwd = config.ios?.binaryPath - ? path.dirname(config.ios?.binaryPath) - : path.join(process.cwd(), DEFAULT_BINARY_DIR); - const simulator = config.ios!.device.replace(/([ /])/g, '\\$1'); + if (!config.ios) { + return; + } - const restoreTimeCommand = `xcrun simctl status_bar ${simulator} clear`; - await execa.command(restoreTimeCommand, { stdio, cwd }); + await xcrunRestore({ + debug: config.debug, + device: config.ios.device, + configuration: config.ios.configuration, + binaryPath: config.ios.binaryPath, + }); logger.print(`[OWL - CLI] Restored status bar time`); }; -export const runAndroid = async (config: Config, logger: Logger) => { - const stdio = config.debug ? 'inherit' : 'ignore'; - const buildType = config.android?.buildType?.toLowerCase(); - const DEFAULT_APK_DIR = `/android/app/build/outputs/apk/${buildType}/`; - const cwd = config.android?.binaryPath - ? path.dirname(config.android?.binaryPath) - : path.join(process.cwd(), DEFAULT_APK_DIR); - - const appFilename = config.android!.binaryPath - ? path.basename(config.android!.binaryPath) - : `app-${buildType}.apk`; - const appPath = path.join(cwd, appFilename); - const { packageName } = config.android!; +export const runAndroid = async (config: Config) => { + if (!config.android) { + return; + } - const installCommand = `adb install -r ${appPath}`; - await execa.command(installCommand, { stdio }); + await adbInstall({ + debug: config.debug, + buildType: config.android.buildType, + binaryPath: config.android.binaryPath, + }); - const launchCommand = `adb shell monkey -p "${packageName}" -c android.intent.category.LAUNCHER 1`; - await execa.command(launchCommand, { stdio }); + await adbLaunch({ + debug: config.debug, + packageName: config.android.packageName, + }); await waitFor(500); }; @@ -107,7 +111,8 @@ export const runHandler = async (args: CliRunOptions) => { }); logger.print(`[OWL - CLI] Running tests on ${args.platform}.`); - await runProject(config, logger); + + await runProject(config); const jestConfigPath = path.join(__dirname, '..', 'jest-config.json'); const jestCommandArgs = [ @@ -115,6 +120,7 @@ export const runHandler = async (args: CliRunOptions) => { `--config=${jestConfigPath}`, `--roots=${cwd}`, '--runInBand', + `--globals='${JSON.stringify({ OWL_CLI_ARGS: args })}'`, ]; if (config.report) { diff --git a/lib/index.ts b/lib/index.ts index b73d001b..5600fd01 100644 --- a/lib/index.ts +++ b/lib/index.ts @@ -8,4 +8,5 @@ export { scrollTo, scrollToEnd, toExist, + reload, } from './actions'; diff --git a/lib/types.ts b/lib/types.ts index 6fa887f4..a0a621fd 100644 --- a/lib/types.ts +++ b/lib/types.ts @@ -17,7 +17,7 @@ export type ConfigEnv = { ENTRY_FILE?: string; }; -type ConfigIOS = { +export type ConfigIOS = { /** The workspace to build. */ workspace?: string; /** The scheme to build. */ @@ -34,7 +34,7 @@ type ConfigIOS = { device: string; }; -type ConfigAndroid = { +export type ConfigAndroid = { /** The package name of your Android app. See Manifest.xml. */ packageName: string; /** Overrides the `assembleDebug` gradle command. Should build the apk. */ diff --git a/lib/utils/adb.test.ts b/lib/utils/adb.test.ts new file mode 100644 index 00000000..b4426bc1 --- /dev/null +++ b/lib/utils/adb.test.ts @@ -0,0 +1,108 @@ +import execa from 'execa'; + +import * as adb from './adb'; + +describe('adb.ts', () => { + jest + .spyOn(process, 'cwd') + .mockReturnValue('/Users/johndoe/Projects/my-project'); + + const execKillMock = { + kill: jest.fn(), + } as unknown as execa.ExecaChildProcess; + const execMock = jest.spyOn(execa, 'command').mockReturnValue(execKillMock); + + beforeEach(() => { + execMock.mockReset(); + }); + + describe('adbInstall', () => { + it('installs an app with default config', async () => { + await adb.adbInstall({}); + + expect(execMock).toHaveBeenCalledTimes(1); + expect(execMock).toHaveBeenCalledWith( + 'adb install -r /Users/johndoe/Projects/my-project/android/app/build/outputs/apk/release/app-release.apk', + { stdio: 'ignore' } + ); + }); + + it('installs an app with debugging', async () => { + await adb.adbInstall({ debug: true }); + + expect(execMock).toHaveBeenCalledTimes(1); + expect(execMock).toHaveBeenCalledWith( + 'adb install -r /Users/johndoe/Projects/my-project/android/app/build/outputs/apk/release/app-release.apk', + { stdio: 'inherit' } + ); + }); + + it('installs an app with custom buildType', async () => { + await adb.adbInstall({ + buildType: 'Debug', + }); + + expect(execMock).toHaveBeenCalledTimes(1); + expect(execMock).toHaveBeenCalledWith( + 'adb install -r /Users/johndoe/Projects/my-project/android/app/build/outputs/apk/debug/app-debug.apk', + { stdio: 'ignore' } + ); + }); + + it('installs an app with custom binaryPath', async () => { + await adb.adbInstall({ + binaryPath: '/custom/path/app.apk', + }); + + expect(execMock).toHaveBeenCalledTimes(1); + expect(execMock).toHaveBeenCalledWith( + 'adb install -r /custom/path/app.apk', + { stdio: 'ignore' } + ); + }); + }); + + describe('adbTerminate', () => { + it('terminates an app', async () => { + await adb.adbTerminate({ packageName: 'com.name.app' }); + + expect(execMock).toHaveBeenCalledTimes(1); + expect(execMock).toHaveBeenCalledWith( + 'adb shell am force-stop com.name.app', + { stdio: 'ignore' } + ); + }); + + it('terminates an app with debugging', async () => { + await adb.adbTerminate({ debug: true, packageName: 'com.name.app' }); + + expect(execMock).toHaveBeenCalledTimes(1); + expect(execMock).toHaveBeenCalledWith( + 'adb shell am force-stop com.name.app', + { stdio: 'inherit' } + ); + }); + }); + + describe('adbLaunch', () => { + it('launches an app', async () => { + await adb.adbLaunch({ packageName: 'com.name.app' }); + + expect(execMock).toHaveBeenCalledTimes(1); + expect(execMock).toHaveBeenCalledWith( + 'adb shell monkey -p "com.name.app" -c android.intent.category.LAUNCHER 1', + { stdio: 'ignore' } + ); + }); + + it('launches an app with debugging', async () => { + await adb.adbLaunch({ debug: true, packageName: 'com.name.app' }); + + expect(execMock).toHaveBeenCalledTimes(1); + expect(execMock).toHaveBeenCalledWith( + 'adb shell monkey -p "com.name.app" -c android.intent.category.LAUNCHER 1', + { stdio: 'inherit' } + ); + }); + }); +}); diff --git a/lib/utils/adb.ts b/lib/utils/adb.ts new file mode 100644 index 00000000..811db7d5 --- /dev/null +++ b/lib/utils/adb.ts @@ -0,0 +1,53 @@ +import path from 'path'; +import execa from 'execa'; +import { ConfigAndroid } from '../types'; + +export const adbInstall = async ({ + debug, + binaryPath, + buildType = 'Release', +}: { + debug?: boolean; + binaryPath?: ConfigAndroid['binaryPath']; + buildType?: ConfigAndroid['buildType']; +}) => { + const stdio = debug ? 'inherit' : 'ignore'; + const DEFAULT_APK_DIR = `/android/app/build/outputs/apk/${buildType.toLowerCase()}/`; + const cwd = binaryPath + ? path.dirname(binaryPath) + : path.join(process.cwd(), DEFAULT_APK_DIR); + + const appFilename = binaryPath + ? path.basename(binaryPath) + : `app-${buildType.toLowerCase()}.apk`; + const appPath = path.join(cwd, appFilename); + + const command = `adb install -r ${appPath}`; + await execa.command(command, { stdio }); +}; + +export const adbTerminate = async ({ + debug, + packageName, +}: { + debug?: boolean; + packageName: string; +}) => { + const stdio = debug ? 'inherit' : 'ignore'; + + const command = `adb shell am force-stop ${packageName}`; + await execa.command(command, { stdio }); +}; + +export const adbLaunch = async ({ + debug, + packageName, +}: { + debug?: boolean; + packageName: string; +}) => { + const stdio = debug ? 'inherit' : 'ignore'; + + const command = `adb shell monkey -p "${packageName}" -c android.intent.category.LAUNCHER 1`; + await execa.command(command, { stdio }); +}; diff --git a/lib/utils/xcrun.test.ts b/lib/utils/xcrun.test.ts new file mode 100644 index 00000000..491011cd --- /dev/null +++ b/lib/utils/xcrun.test.ts @@ -0,0 +1,433 @@ +import execa from 'execa'; + +import * as xcrun from './xcrun'; + +describe('xcrun.ts', () => { + jest + .spyOn(process, 'cwd') + .mockReturnValue('/Users/johndoe/Projects/my-project'); + + const execKillMock = { + kill: jest.fn(), + stdout: 'bundleId', + } as unknown as execa.ExecaChildProcess; + const execMock = jest.spyOn(execa, 'command').mockReturnValue(execKillMock); + + beforeEach(() => { + execMock.mockClear(); + }); + + describe('xcrunStatusBar', () => { + it('updates the status bar with default config', async () => { + await xcrun.xcrunStatusBar({ device: 'iPhone 13 Pro' }); + + expect(execMock).toHaveBeenCalledTimes(1); + expect(execMock).toHaveBeenCalledWith( + 'xcrun simctl status_bar iPhone\\ 13\\ Pro override --time 9:41', + { + cwd: '/Users/johndoe/Projects/my-project/ios/build/Build/Products/Debug-iphonesimulator', + stdio: 'ignore', + } + ); + }); + + it('updates the status bar with debug', async () => { + await xcrun.xcrunStatusBar({ device: 'iPhone 13 Pro', debug: true }); + + expect(execMock).toHaveBeenCalledTimes(1); + expect(execMock).toHaveBeenCalledWith( + 'xcrun simctl status_bar iPhone\\ 13\\ Pro override --time 9:41', + { + cwd: '/Users/johndoe/Projects/my-project/ios/build/Build/Products/Debug-iphonesimulator', + stdio: 'inherit', + } + ); + }); + + it('updates the status bar with custom configuration', async () => { + await xcrun.xcrunStatusBar({ + device: 'iPhone 13 Pro', + configuration: 'Release', + }); + + expect(execMock).toHaveBeenCalledTimes(1); + expect(execMock).toHaveBeenCalledWith( + 'xcrun simctl status_bar iPhone\\ 13\\ Pro override --time 9:41', + { + cwd: '/Users/johndoe/Projects/my-project/ios/build/Build/Products/Release-iphonesimulator', + stdio: 'ignore', + } + ); + }); + + it('updates the status bar with custom binaryPath', async () => { + await xcrun.xcrunStatusBar({ + device: 'iPhone 13 Pro', + binaryPath: '/some/path/to/my/app.app', + }); + + expect(execMock).toHaveBeenCalledTimes(1); + expect(execMock).toHaveBeenCalledWith( + 'xcrun simctl status_bar iPhone\\ 13\\ Pro override --time 9:41', + { + cwd: '/some/path/to/my', + stdio: 'ignore', + } + ); + }); + }); + + describe('xcrunInstall', () => { + it('installs the app with default config', async () => { + await xcrun.xcrunInstall({ device: 'iPhone 13 Pro', scheme: 'MyApp' }); + + expect(execMock).toHaveBeenCalledTimes(1); + expect(execMock).toHaveBeenCalledWith( + 'xcrun simctl install iPhone\\ 13\\ Pro MyApp.app', + { + cwd: '/Users/johndoe/Projects/my-project/ios/build/Build/Products/Debug-iphonesimulator', + stdio: 'ignore', + } + ); + }); + + it('installs the app with debug', async () => { + await xcrun.xcrunInstall({ + device: 'iPhone 13 Pro', + scheme: 'MyApp', + debug: true, + }); + + expect(execMock).toHaveBeenCalledTimes(1); + expect(execMock).toHaveBeenCalledWith( + 'xcrun simctl install iPhone\\ 13\\ Pro MyApp.app', + { + cwd: '/Users/johndoe/Projects/my-project/ios/build/Build/Products/Debug-iphonesimulator', + stdio: 'inherit', + } + ); + }); + + it('installs the app with custom configuration', async () => { + await xcrun.xcrunInstall({ + device: 'iPhone 13 Pro', + scheme: 'MyApp', + configuration: 'Release', + }); + + expect(execMock).toHaveBeenCalledTimes(1); + expect(execMock).toHaveBeenCalledWith( + 'xcrun simctl install iPhone\\ 13\\ Pro MyApp.app', + { + cwd: '/Users/johndoe/Projects/my-project/ios/build/Build/Products/Release-iphonesimulator', + stdio: 'ignore', + } + ); + }); + + it('installs the app with custom binaryPath', async () => { + await xcrun.xcrunInstall({ + device: 'iPhone 13 Pro', + configuration: 'Release', + binaryPath: '/some/path/to/my/app.app', + }); + + expect(execMock).toHaveBeenCalledTimes(1); + expect(execMock).toHaveBeenCalledWith( + 'xcrun simctl install iPhone\\ 13\\ Pro app.app', + { + cwd: '/some/path/to/my', + stdio: 'ignore', + } + ); + }); + }); + + describe('xcrunTerminate', () => { + it('terminates the app with default config', async () => { + await xcrun.xcrunTerminate({ device: 'iPhone 13 Pro', scheme: 'MyApp' }); + + expect(execMock).toHaveBeenCalledTimes(2); + expect(execMock).toHaveBeenNthCalledWith( + 1, + "./PlistBuddy -c 'Print CFBundleIdentifier' /Users/johndoe/Projects/my-project/ios/build/Build/Products/Debug-iphonesimulator/MyApp.app/Info.plist", + { + cwd: '/usr/libexec', + shell: true, + } + ); + expect(execMock).toHaveBeenNthCalledWith( + 2, + 'xcrun simctl terminate iPhone\\ 13\\ Pro bundleId', + { + cwd: '/Users/johndoe/Projects/my-project/ios/build/Build/Products/Debug-iphonesimulator', + stdio: 'ignore', + } + ); + }); + + it('terminates the app with debug', async () => { + await xcrun.xcrunTerminate({ + device: 'iPhone 13 Pro', + scheme: 'MyApp', + debug: true, + }); + + expect(execMock).toHaveBeenCalledTimes(2); + expect(execMock).toHaveBeenNthCalledWith( + 2, + 'xcrun simctl terminate iPhone\\ 13\\ Pro bundleId', + { + cwd: '/Users/johndoe/Projects/my-project/ios/build/Build/Products/Debug-iphonesimulator', + stdio: 'inherit', + } + ); + }); + + it('terminates the app with custom configuration', async () => { + await xcrun.xcrunTerminate({ + device: 'iPhone 13 Pro', + scheme: 'MyApp', + configuration: 'Release', + }); + + expect(execMock).toHaveBeenCalledTimes(2); + expect(execMock).toHaveBeenNthCalledWith( + 1, + "./PlistBuddy -c 'Print CFBundleIdentifier' /Users/johndoe/Projects/my-project/ios/build/Build/Products/Release-iphonesimulator/MyApp.app/Info.plist", + { + cwd: '/usr/libexec', + shell: true, + } + ); + expect(execMock).toHaveBeenNthCalledWith( + 2, + 'xcrun simctl terminate iPhone\\ 13\\ Pro bundleId', + { + cwd: '/Users/johndoe/Projects/my-project/ios/build/Build/Products/Release-iphonesimulator', + stdio: 'ignore', + } + ); + }); + + it('terminates the app with custom binaryPath', async () => { + await xcrun.xcrunTerminate({ + device: 'iPhone 13 Pro', + configuration: 'Release', + binaryPath: '/some/path/to/my/app.app', + }); + + expect(execMock).toHaveBeenCalledTimes(2); + expect(execMock).toHaveBeenNthCalledWith( + 1, + "./PlistBuddy -c 'Print CFBundleIdentifier' /some/path/to/my/app.app/Info.plist", + { + cwd: '/usr/libexec', + shell: true, + } + ); + expect(execMock).toHaveBeenNthCalledWith( + 2, + 'xcrun simctl terminate iPhone\\ 13\\ Pro bundleId', + { + cwd: '/some/path/to/my', + stdio: 'ignore', + } + ); + }); + }); + + describe('xcrunLaunch', () => { + it('launches the app with default config', async () => { + await xcrun.xcrunLaunch({ device: 'iPhone 13 Pro', scheme: 'MyApp' }); + + expect(execMock).toHaveBeenCalledTimes(2); + expect(execMock).toHaveBeenNthCalledWith( + 1, + "./PlistBuddy -c 'Print CFBundleIdentifier' /Users/johndoe/Projects/my-project/ios/build/Build/Products/Debug-iphonesimulator/MyApp.app/Info.plist", + { + cwd: '/usr/libexec', + shell: true, + } + ); + expect(execMock).toHaveBeenNthCalledWith( + 2, + 'xcrun simctl launch iPhone\\ 13\\ Pro bundleId', + { + cwd: '/Users/johndoe/Projects/my-project/ios/build/Build/Products/Debug-iphonesimulator', + stdio: 'ignore', + } + ); + }); + + it('launches the app with debug', async () => { + await xcrun.xcrunLaunch({ + device: 'iPhone 13 Pro', + scheme: 'MyApp', + debug: true, + }); + + expect(execMock).toHaveBeenCalledTimes(2); + expect(execMock).toHaveBeenNthCalledWith( + 2, + 'xcrun simctl launch iPhone\\ 13\\ Pro bundleId', + { + cwd: '/Users/johndoe/Projects/my-project/ios/build/Build/Products/Debug-iphonesimulator', + stdio: 'inherit', + } + ); + }); + + it('launches the app with custom configuration', async () => { + await xcrun.xcrunLaunch({ + device: 'iPhone 13 Pro', + scheme: 'MyApp', + configuration: 'Release', + }); + + expect(execMock).toHaveBeenCalledTimes(2); + expect(execMock).toHaveBeenNthCalledWith( + 1, + "./PlistBuddy -c 'Print CFBundleIdentifier' /Users/johndoe/Projects/my-project/ios/build/Build/Products/Release-iphonesimulator/MyApp.app/Info.plist", + { + cwd: '/usr/libexec', + shell: true, + } + ); + expect(execMock).toHaveBeenNthCalledWith( + 2, + 'xcrun simctl launch iPhone\\ 13\\ Pro bundleId', + { + cwd: '/Users/johndoe/Projects/my-project/ios/build/Build/Products/Release-iphonesimulator', + stdio: 'ignore', + } + ); + }); + + it('launches the app with custom binaryPath', async () => { + await xcrun.xcrunLaunch({ + device: 'iPhone 13 Pro', + configuration: 'Release', + binaryPath: '/some/path/to/my/app.app', + }); + + expect(execMock).toHaveBeenCalledTimes(2); + expect(execMock).toHaveBeenNthCalledWith( + 1, + "./PlistBuddy -c 'Print CFBundleIdentifier' /some/path/to/my/app.app/Info.plist", + { + cwd: '/usr/libexec', + shell: true, + } + ); + expect(execMock).toHaveBeenNthCalledWith( + 2, + 'xcrun simctl launch iPhone\\ 13\\ Pro bundleId', + { + cwd: '/some/path/to/my', + stdio: 'ignore', + } + ); + }); + }); + + describe('xcrunUi', () => { + it('sets the simulator UI with default config', async () => { + await xcrun.xcrunUi({ device: 'iPhone 13 Pro' }); + + expect(execMock).toHaveBeenCalledTimes(2); + expect(execMock).toHaveBeenNthCalledWith( + 1, + 'xcrun simctl ui iPhone\\ 13\\ Pro appearance dark', + { + cwd: '/Users/johndoe/Projects/my-project/ios/build/Build/Products/Debug-iphonesimulator', + stdio: 'ignore', + } + ); + expect(execMock).toHaveBeenNthCalledWith( + 2, + 'xcrun simctl ui iPhone\\ 13\\ Pro appearance light', + { + cwd: '/Users/johndoe/Projects/my-project/ios/build/Build/Products/Debug-iphonesimulator', + stdio: 'ignore', + } + ); + }); + + it('sets the simulator UI with debug', async () => { + await xcrun.xcrunUi({ + device: 'iPhone 13 Pro', + debug: true, + }); + + expect(execMock).toHaveBeenCalledTimes(2); + expect(execMock).toHaveBeenNthCalledWith( + 1, + 'xcrun simctl ui iPhone\\ 13\\ Pro appearance dark', + { + cwd: '/Users/johndoe/Projects/my-project/ios/build/Build/Products/Debug-iphonesimulator', + stdio: 'inherit', + } + ); + expect(execMock).toHaveBeenNthCalledWith( + 2, + 'xcrun simctl ui iPhone\\ 13\\ Pro appearance light', + { + cwd: '/Users/johndoe/Projects/my-project/ios/build/Build/Products/Debug-iphonesimulator', + stdio: 'inherit', + } + ); + }); + + it('sets the simulator UI with custom configuration', async () => { + await xcrun.xcrunUi({ + device: 'iPhone 13 Pro', + configuration: 'Release', + }); + + expect(execMock).toHaveBeenCalledTimes(2); + expect(execMock).toHaveBeenNthCalledWith( + 1, + 'xcrun simctl ui iPhone\\ 13\\ Pro appearance dark', + { + cwd: '/Users/johndoe/Projects/my-project/ios/build/Build/Products/Release-iphonesimulator', + stdio: 'ignore', + } + ); + expect(execMock).toHaveBeenNthCalledWith( + 2, + 'xcrun simctl ui iPhone\\ 13\\ Pro appearance light', + { + cwd: '/Users/johndoe/Projects/my-project/ios/build/Build/Products/Release-iphonesimulator', + stdio: 'ignore', + } + ); + }); + + it('sets the simulator UI with custom binaryPath', async () => { + await xcrun.xcrunUi({ + device: 'iPhone 13 Pro', + configuration: 'Release', + binaryPath: '/some/path/to/my/app.app', + }); + + expect(execMock).toHaveBeenCalledTimes(2); + expect(execMock).toHaveBeenNthCalledWith( + 1, + 'xcrun simctl ui iPhone\\ 13\\ Pro appearance dark', + { + cwd: '/some/path/to/my', + stdio: 'ignore', + } + ); + expect(execMock).toHaveBeenNthCalledWith( + 2, + 'xcrun simctl ui iPhone\\ 13\\ Pro appearance light', + { + cwd: '/some/path/to/my', + stdio: 'ignore', + } + ); + }); + }); +}); diff --git a/lib/utils/xcrun.ts b/lib/utils/xcrun.ts new file mode 100644 index 00000000..eb6c3f86 --- /dev/null +++ b/lib/utils/xcrun.ts @@ -0,0 +1,170 @@ +import path from 'path'; +import execa from 'execa'; +import { ConfigIOS } from '../types'; +import { waitFor } from './wait-for'; + +export const xcrunStatusBar = async ({ + debug, + binaryPath, + device, + configuration = 'Debug', +}: { + debug?: boolean; + binaryPath?: ConfigIOS['binaryPath']; + device: ConfigIOS['device']; + configuration?: ConfigIOS['configuration']; +}) => { + const stdio = debug ? 'inherit' : 'ignore'; + const DEFAULT_BINARY_DIR = `/ios/build/Build/Products/${configuration}-iphonesimulator`; + const cwd = binaryPath + ? path.dirname(binaryPath) + : path.join(process.cwd(), DEFAULT_BINARY_DIR); + + const simulator = device.replace(/([ /])/g, '\\$1'); + const SIMULATOR_TIME = '9:41'; + + const command = `xcrun simctl status_bar ${simulator} override --time ${SIMULATOR_TIME}`; + await execa.command(command, { stdio, cwd }); +}; + +export const xcrunInstall = async ({ + debug, + binaryPath, + device, + scheme, + configuration = 'Debug', +}: { + debug?: boolean; + binaryPath?: ConfigIOS['binaryPath']; + device: ConfigIOS['device']; + scheme?: ConfigIOS['scheme']; + configuration?: ConfigIOS['configuration']; +}) => { + const stdio = debug ? 'inherit' : 'ignore'; + const DEFAULT_BINARY_DIR = `/ios/build/Build/Products/${configuration}-iphonesimulator`; + const cwd = binaryPath + ? path.dirname(binaryPath) + : path.join(process.cwd(), DEFAULT_BINARY_DIR); + + const appFilename = binaryPath ? path.basename(binaryPath) : `${scheme}.app`; + + const simulator = device.replace(/([ /])/g, '\\$1'); + + const command = `xcrun simctl install ${simulator} ${appFilename}`; + await execa.command(command, { stdio, cwd }); +}; + +export const xcrunTerminate = async ({ + debug, + binaryPath, + device, + scheme, + configuration = 'Debug', +}: { + debug?: boolean; + binaryPath?: ConfigIOS['binaryPath']; + device: ConfigIOS['device']; + scheme?: ConfigIOS['scheme']; + configuration?: ConfigIOS['configuration']; +}) => { + const stdio = debug ? 'inherit' : 'ignore'; + const DEFAULT_BINARY_DIR = `/ios/build/Build/Products/${configuration}-iphonesimulator`; + const cwd = binaryPath + ? path.dirname(binaryPath) + : path.join(process.cwd(), DEFAULT_BINARY_DIR); + + const appFilename = binaryPath ? path.basename(binaryPath) : `${scheme}.app`; + const plistPath = path.join(cwd, appFilename, 'Info.plist'); + + const { stdout: bundleId } = await execa.command( + `./PlistBuddy -c 'Print CFBundleIdentifier' ${plistPath}`, + { shell: true, cwd: '/usr/libexec' } + ); + + const simulator = device.replace(/([ /])/g, '\\$1'); + + const command = `xcrun simctl terminate ${simulator} ${bundleId}`; + await execa.command(command, { stdio, cwd }); +}; + +export const xcrunLaunch = async ({ + debug, + binaryPath, + device, + scheme, + configuration = 'Debug', +}: { + debug?: boolean; + binaryPath?: ConfigIOS['binaryPath']; + device: ConfigIOS['device']; + scheme?: ConfigIOS['scheme']; + configuration?: ConfigIOS['configuration']; +}) => { + const stdio = debug ? 'inherit' : 'ignore'; + const DEFAULT_BINARY_DIR = `/ios/build/Build/Products/${configuration}-iphonesimulator`; + const cwd = binaryPath + ? path.dirname(binaryPath) + : path.join(process.cwd(), DEFAULT_BINARY_DIR); + + const appFilename = binaryPath ? path.basename(binaryPath) : `${scheme}.app`; + const plistPath = path.join(cwd, appFilename, 'Info.plist'); + + const { stdout: bundleId } = await execa.command( + `./PlistBuddy -c 'Print CFBundleIdentifier' ${plistPath}`, + { shell: true, cwd: '/usr/libexec' } + ); + + const simulator = device.replace(/([ /])/g, '\\$1'); + + const command = `xcrun simctl launch ${simulator} ${bundleId}`; + await execa.command(command, { stdio, cwd }); +}; + +export const xcrunUi = async ({ + debug, + binaryPath, + device, + configuration = 'Debug', +}: { + debug?: boolean; + binaryPath?: ConfigIOS['binaryPath']; + device: ConfigIOS['device']; + configuration?: ConfigIOS['configuration']; +}) => { + const stdio = debug ? 'inherit' : 'ignore'; + const DEFAULT_BINARY_DIR = `/ios/build/Build/Products/${configuration}-iphonesimulator`; + const cwd = binaryPath + ? path.dirname(binaryPath) + : path.join(process.cwd(), DEFAULT_BINARY_DIR); + + const simulator = device.replace(/([ /])/g, '\\$1'); + + const command = `xcrun simctl ui ${simulator} appearance`; + await execa.command(`${command} dark`, { stdio, cwd }); + await waitFor(500); + await execa.command(`${command} light`, { stdio, cwd }); + await waitFor(500); +}; + +export const xcrunRestore = async ({ + debug, + binaryPath, + device, + configuration = 'Debug', +}: { + debug?: boolean; + binaryPath?: ConfigIOS['binaryPath']; + device: ConfigIOS['device']; + configuration?: ConfigIOS['configuration']; +}) => { + const stdio = debug ? 'inherit' : 'ignore'; + const DEFAULT_BINARY_DIR = `/ios/build/Build/Products/${configuration}-iphonesimulator`; + const cwd = binaryPath + ? path.dirname(binaryPath) + : path.join(process.cwd(), DEFAULT_BINARY_DIR); + + const simulator = device.replace(/([ /])/g, '\\$1'); + + const command = `xcrun simctl status_bar ${simulator} clear`; + await execa.command(command, { stdio, cwd }); +};