Move screenshot to iOSBridge

Summary:
In order to support IOS cloud devices, we need to abstract
over the direct uses of idb/xcrun so we can switch them
out based on more than the device type.

Note that there's a bit of a type weirdness in there. I'll
clean this up with the next diff.

Reviewed By: mweststrate

Differential Revision: D30248036

fbshipit-source-id: ec8571429e04abe059850ef334a6645ae4a5e034
This commit is contained in:
Pascal Hartig
2021-08-11 11:02:10 -07:00
committed by Facebook GitHub Bot
parent f515df1c01
commit 52b3edc5ad
3 changed files with 66 additions and 19 deletions

View File

@@ -12,14 +12,10 @@ import child_process, {ChildProcess} from 'child_process';
import BaseDevice from './BaseDevice'; import BaseDevice from './BaseDevice';
import JSONStream from 'JSONStream'; import JSONStream from 'JSONStream';
import {Transform} from 'stream'; 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 {exec} from 'promisify-child-process';
import {default as promiseTimeout} from '../utils/promiseTimeout'; import {default as promiseTimeout} from '../utils/promiseTimeout';
import {IOSBridge} from '../utils/IOSBridge'; import {IOSBridge} from '../utils/IOSBridge';
import split2 from 'split2'; import split2 from 'split2';
import {getAppTempPath} from '../utils/pathUtils';
type IOSLogLevel = 'Default' | 'Info' | 'Debug' | 'Error' | 'Fault'; type IOSLogLevel = 'Default' | 'Info' | 'Debug' | 'Error' | 'Fault';
@@ -67,19 +63,8 @@ export default class IOSDevice extends BaseDevice {
if (!this.connected.get()) { if (!this.connected.get()) {
return Buffer.from([]); return Buffer.from([]);
} }
const tmpImageName = uuid() + '.png'; // HACK: Will restructure the types to allow for the ! to be removed.
const tmpDirectory = getAppTempPath(); return await this.iOSBridge.screenshot!(this.serial);
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;
});
} }
navigateToLocation(location: string) { navigateToLocation(location: string) {

View File

@@ -7,9 +7,13 @@
* @format * @format
*/ */
import fs from 'fs'; import fs from 'fs-extra';
import child_process from 'child_process'; import child_process from 'child_process';
import {DeviceType} from 'flipper-plugin-lib'; 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 { export interface IOSBridge {
idbAvailable: boolean; idbAvailable: boolean;
@@ -17,6 +21,7 @@ export interface IOSBridge {
udid: string, udid: string,
deviceType: DeviceType, deviceType: DeviceType,
) => child_process.ChildProcessWithoutNullStreams; ) => child_process.ChildProcessWithoutNullStreams;
screenshot?: (serial: string) => Promise<Buffer>;
} }
async function isAvailable(idbPath: string): Promise<boolean> { async function isAvailable(idbPath: string): Promise<boolean> {
@@ -32,7 +37,8 @@ async function isAvailable(idbPath: string): Promise<boolean> {
function getLogExtraArgs(deviceType: DeviceType) { function getLogExtraArgs(deviceType: DeviceType) {
if (deviceType === 'physical') { if (deviceType === 'physical') {
return [ 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 { } else {
return [ 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<Buffer> {
return exec(command)
.then(() => fs.readFile(imagePath))
.then(async (buffer) => {
await fs.unlink(imagePath);
return buffer;
});
}
export async function xcrunScreenshot(serial: string): Promise<Buffer> {
const imagePath = makeTempScreenshotFilePath();
const command = `xcrun simctl io ${serial} screenshot ${imagePath}`;
return runScreenshotCommand(command, imagePath);
}
export async function idbScreenshot(serial: string): Promise<Buffer> {
const imagePath = makeTempScreenshotFilePath();
const command = `idb screenshot --udid ${serial} ${imagePath}`;
return runScreenshotCommand(command, imagePath);
}
export async function makeIOSBridge( export async function makeIOSBridge(
idbPath: string, idbPath: string,
isXcodeDetected: boolean, isXcodeDetected: boolean,
@@ -87,6 +123,7 @@ export async function makeIOSBridge(
return { return {
idbAvailable: true, idbAvailable: true,
startLogListener: idbStartLogListener.bind(null, idbPath), startLogListener: idbStartLogListener.bind(null, idbPath),
screenshot: idbScreenshot,
}; };
} }
@@ -95,6 +132,7 @@ export async function makeIOSBridge(
return { return {
idbAvailable: false, idbAvailable: false,
startLogListener: xcrunStartLogListener, startLogListener: xcrunStartLogListener,
screenshot: xcrunScreenshot,
}; };
} }
// no idb, and not a simulator, we can't log this device // no idb, and not a simulator, we can't log this device

View File

@@ -9,11 +9,15 @@
import {makeIOSBridge} from '../IOSBridge'; import {makeIOSBridge} from '../IOSBridge';
import childProcess from 'child_process'; import childProcess from 'child_process';
import * as promisifyChildProcess from 'promisify-child-process';
import {mocked} from 'ts-jest/utils'; import {mocked} from 'ts-jest/utils';
jest.mock('child_process'); jest.mock('child_process');
const spawn = mocked(childProcess.spawn); 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 () => { test('uses xcrun with no idb when xcode is detected', async () => {
const ib = await makeIOSBridge('', true); const ib = await makeIOSBridge('', true);
expect(ib.startLogListener).toBeDefined(); expect(ib.startLogListener).toBeDefined();
@@ -92,3 +96,23 @@ test('uses no log listener when xcode is not detected', async () => {
const ib = await makeIOSBridge('', false); const ib = await makeIOSBridge('', false);
expect(ib.startLogListener).toBeUndefined(); 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',
);
});