diff --git a/desktop/app/src/devices/IOSDevice.tsx b/desktop/app/src/devices/IOSDevice.tsx index 1157358c0..ce69b0145 100644 --- a/desktop/app/src/devices/IOSDevice.tsx +++ b/desktop/app/src/devices/IOSDevice.tsx @@ -12,14 +12,10 @@ import child_process, {ChildProcess} from 'child_process'; import BaseDevice from './BaseDevice'; import JSONStream from 'JSONStream'; import {Transform} from 'stream'; -import fs from 'fs-extra'; -import {v1 as uuid} from 'uuid'; -import path from 'path'; import {exec} from 'promisify-child-process'; import {default as promiseTimeout} from '../utils/promiseTimeout'; import {IOSBridge} from '../utils/IOSBridge'; import split2 from 'split2'; -import {getAppTempPath} from '../utils/pathUtils'; type IOSLogLevel = 'Default' | 'Info' | 'Debug' | 'Error' | 'Fault'; @@ -67,19 +63,8 @@ export default class IOSDevice extends BaseDevice { if (!this.connected.get()) { return Buffer.from([]); } - const tmpImageName = uuid() + '.png'; - const tmpDirectory = getAppTempPath(); - const tmpFilePath = path.join(tmpDirectory, tmpImageName); - const command = - this.deviceType === 'emulator' - ? `xcrun simctl io ${this.serial} screenshot ${tmpFilePath}` - : `idb screenshot --udid ${this.serial} ${tmpFilePath}`; - return exec(command) - .then(() => fs.readFile(tmpFilePath)) - .then(async (buffer) => { - await fs.unlink(tmpFilePath); - return buffer; - }); + // HACK: Will restructure the types to allow for the ! to be removed. + return await this.iOSBridge.screenshot!(this.serial); } navigateToLocation(location: string) { diff --git a/desktop/app/src/utils/IOSBridge.tsx b/desktop/app/src/utils/IOSBridge.tsx index 6d16fd94b..cd69cc125 100644 --- a/desktop/app/src/utils/IOSBridge.tsx +++ b/desktop/app/src/utils/IOSBridge.tsx @@ -7,9 +7,13 @@ * @format */ -import fs from 'fs'; +import fs from 'fs-extra'; import child_process from 'child_process'; import {DeviceType} from 'flipper-plugin-lib'; +import {v1 as uuid} from 'uuid'; +import path from 'path'; +import {exec} from 'promisify-child-process'; +import {getAppTempPath} from '../utils/pathUtils'; export interface IOSBridge { idbAvailable: boolean; @@ -17,6 +21,7 @@ export interface IOSBridge { udid: string, deviceType: DeviceType, ) => child_process.ChildProcessWithoutNullStreams; + screenshot?: (serial: string) => Promise; } async function isAvailable(idbPath: string): Promise { @@ -32,7 +37,8 @@ async function isAvailable(idbPath: string): Promise { function getLogExtraArgs(deviceType: DeviceType) { if (deviceType === 'physical') { return [ - // idb has a --json option, but that doesn't actually work! + // idb has a --json option, but that doesn't actually work for physical + // devices! ]; } else { return [ @@ -77,6 +83,36 @@ export function xcrunStartLogListener(udid: string, deviceType: DeviceType) { ); } +function makeTempScreenshotFilePath() { + const imageName = uuid() + '.png'; + const directory = getAppTempPath(); + return path.join(directory, imageName); +} + +function runScreenshotCommand( + command: string, + imagePath: string, +): Promise { + return exec(command) + .then(() => fs.readFile(imagePath)) + .then(async (buffer) => { + await fs.unlink(imagePath); + return buffer; + }); +} + +export async function xcrunScreenshot(serial: string): Promise { + const imagePath = makeTempScreenshotFilePath(); + const command = `xcrun simctl io ${serial} screenshot ${imagePath}`; + return runScreenshotCommand(command, imagePath); +} + +export async function idbScreenshot(serial: string): Promise { + const imagePath = makeTempScreenshotFilePath(); + const command = `idb screenshot --udid ${serial} ${imagePath}`; + return runScreenshotCommand(command, imagePath); +} + export async function makeIOSBridge( idbPath: string, isXcodeDetected: boolean, @@ -87,6 +123,7 @@ export async function makeIOSBridge( return { idbAvailable: true, startLogListener: idbStartLogListener.bind(null, idbPath), + screenshot: idbScreenshot, }; } @@ -95,6 +132,7 @@ export async function makeIOSBridge( return { idbAvailable: false, startLogListener: xcrunStartLogListener, + screenshot: xcrunScreenshot, }; } // no idb, and not a simulator, we can't log this device diff --git a/desktop/app/src/utils/__tests__/IOSBridge.node.tsx b/desktop/app/src/utils/__tests__/IOSBridge.node.tsx index 12c2957ee..9d2d66f2a 100644 --- a/desktop/app/src/utils/__tests__/IOSBridge.node.tsx +++ b/desktop/app/src/utils/__tests__/IOSBridge.node.tsx @@ -9,11 +9,15 @@ import {makeIOSBridge} from '../IOSBridge'; import childProcess from 'child_process'; +import * as promisifyChildProcess from 'promisify-child-process'; import {mocked} from 'ts-jest/utils'; jest.mock('child_process'); const spawn = mocked(childProcess.spawn); +jest.mock('promisify-child-process'); +const exec = mocked(promisifyChildProcess.exec); + test('uses xcrun with no idb when xcode is detected', async () => { const ib = await makeIOSBridge('', true); expect(ib.startLogListener).toBeDefined(); @@ -92,3 +96,23 @@ test('uses no log listener when xcode is not detected', async () => { const ib = await makeIOSBridge('', false); expect(ib.startLogListener).toBeUndefined(); }); + +test('uses xcrun to take screenshots with no idb when xcode is detected', async () => { + const ib = await makeIOSBridge('', true); + + ib.screenshot!('deadbeef'); + + expect(exec).toHaveBeenCalledWith( + 'xcrun simctl io deadbeef screenshot /temp/00000000-0000-0000-0000-000000000000.png', + ); +}); + +test('uses idb to take screenshots when available', async () => { + const ib = await makeIOSBridge('/usr/local/bin/idb', true, async (_) => true); + + ib.screenshot!('deadbeef'); + + expect(exec).toHaveBeenCalledWith( + 'idb screenshot --udid deadbeef /temp/00000000-0000-0000-0000-000000000000.png', + ); +});