flipper-plugin
Version:
Flipper Desktop plugin SDK and components
504 lines • 17.9 kB
JavaScript
/**
* Copyright (c) Meta Platforms, Inc. and affiliates.
*
* This source code is licensed under the MIT license found in the
* LICENSE file in the root directory of this source tree.
*
* @format
*/
var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
if (k2 === undefined) k2 = k;
var desc = Object.getOwnPropertyDescriptor(m, k);
if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
desc = { enumerable: true, get: function() { return m[k]; } };
}
Object.defineProperty(o, k2, desc);
}) : (function(o, m, k, k2) {
if (k2 === undefined) k2 = k;
o[k2] = m[k];
}));
var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
Object.defineProperty(o, "default", { enumerable: true, value: v });
}) : function(o, v) {
o["default"] = v;
});
var __importStar = (this && this.__importStar) || function (mod) {
if (mod && mod.__esModule) return mod;
var result = {};
if (mod != null) for (var k in mod) if (k !== "default" && Object.prototype.hasOwnProperty.call(mod, k)) __createBinding(result, mod, k);
__setModuleDefault(result, mod);
return result;
};
Object.defineProperty(exports, "__esModule", { value: true });
exports.createStubFlipperServerConfig = exports.renderDevicePlugin = exports.startDevicePlugin = exports.renderPlugin = exports.startPlugin = exports.createFlipperServerMock = exports.createTestDevicePlugin = exports.createTestPlugin = exports.createMockPluginDetails = exports.createMockFlipperLib = exports.createStubFunction = void 0;
const React = __importStar(require("react"));
const flipper_common_1 = require("flipper-common");
const PluginRenderer_1 = require("../plugin/PluginRenderer");
const flipper_common_2 = require("flipper-common");
const DevicePlugin_1 = require("../plugin/DevicePlugin");
const FlipperLib_1 = require("../plugin/FlipperLib");
const Plugin_1 = require("../plugin/Plugin");
const SandyPluginDefinition_1 = require("../plugin/SandyPluginDefinition");
const atom_1 = require("../state/atom");
const Logger_1 = require("../utils/Logger");
function createStubFunction() {
// we shouldn't be usign jest.fn() outside a unit test, as it would not resolve / cause jest to be bundled up!
if (typeof jest !== 'undefined') {
return jest.fn();
}
return (() => {
console.warn('Using a stub function outside a test environment!');
});
}
exports.createStubFunction = createStubFunction;
function createMockFlipperLib(options) {
return {
isFB: false,
logger: Logger_1.stubLogger,
enableMenuEntries: createStubFunction(),
createPaste: createStubFunction(),
GK(gk) {
return options?.GKs?.includes(gk) || false;
},
selectPlugin: createStubFunction(),
writeTextToClipboard: createStubFunction(),
openLink: createStubFunction(),
showNotification: createStubFunction(),
exportFile: createStubFunction(),
exportFileBinary: createStubFunction(),
importFile: createStubFunction(),
paths: {
appPath: process.cwd(),
homePath: `/dev/null`,
staticPath: process.cwd(),
tempPath: `/dev/null`,
},
environmentInfo: {
os: {
arch: 'Test',
unixname: 'test',
platform: 'linux',
},
env: {},
isHeadlessBuild: true,
},
intern: {
graphGet: createStubFunction(),
graphPost: createStubFunction(),
isLoggedIn: createStubFunction(),
currentUser: () => (0, atom_1.createState)(null),
isConnected: () => (0, atom_1.createState)(true),
},
runDeviceAction: () => {
return undefined;
},
remoteServerContext: {
childProcess: {
exec: createStubFunction(),
},
fs: {
access: createStubFunction(),
pathExists: createStubFunction(),
unlink: createStubFunction(),
mkdir: createStubFunction(),
rm: createStubFunction(),
copyFile: createStubFunction(),
constants: flipper_common_2.fsConstants,
stat: createStubFunction(),
readlink: createStubFunction(),
readFile: createStubFunction(),
readFileBinary: createStubFunction(),
writeFile: createStubFunction(),
writeFileBinary: createStubFunction(),
},
downloadFile: createStubFunction(),
},
settings: createStubFunction(),
};
}
exports.createMockFlipperLib = createMockFlipperLib;
function createMockPluginDetails(details) {
return {
id: 'TestPlugin',
dir: '',
name: 'TestPlugin',
specVersion: 0,
entry: '',
isActivatable: true,
main: '',
source: '',
title: 'Testing Plugin',
version: '',
...details,
};
}
exports.createMockPluginDetails = createMockPluginDetails;
function createTestPlugin(implementation, details) {
return new SandyPluginDefinition_1.SandyPluginDefinition(createMockPluginDetails({
pluginType: 'client',
...details,
}), {
Component() {
return null;
},
...implementation,
});
}
exports.createTestPlugin = createTestPlugin;
function createTestDevicePlugin(implementation, details) {
return new SandyPluginDefinition_1.SandyPluginDefinition(createMockPluginDetails({
pluginType: 'device',
...details,
}), {
supportsDevice() {
return true;
},
Component() {
return null;
},
...implementation,
});
}
exports.createTestDevicePlugin = createTestDevicePlugin;
function createFlipperServerMock(overrides) {
return {
async connect() { },
on: createStubFunction(),
off: createStubFunction(),
exec: jest
.fn()
.mockImplementation(async (cmd, ...args) => {
if (overrides?.[cmd]) {
return overrides[cmd](...args);
}
console.warn(`Empty server response stubbed for command '${cmd}', set 'getRenderHostInstance().flipperServer.exec' in your test to override the behavior.`);
return undefined;
}),
close: createStubFunction(),
};
}
exports.createFlipperServerMock = createFlipperServerMock;
function startPlugin(module, options) {
// eslint-disable-next-line no-eval
const { act } = eval('require("@testing-library/react")');
const definition = new SandyPluginDefinition_1.SandyPluginDefinition(createMockPluginDetails(), module);
if (definition.isDevicePlugin) {
throw new Error('Use `startDevicePlugin` or `renderDevicePlugin` to test device plugins');
}
const sendStub = createStubFunction();
const flipperUtils = createMockFlipperLib(options);
const testDevice = createMockDevice(options);
const appName = 'TestApplication';
const deviceName = 'TestDevice';
const fakeFlipperClient = {
id: `${appName}#${testDevice.os}#${deviceName}#${testDevice.serial}`,
plugins: new Set([definition.id]),
query: {
app: appName,
app_id: `com.facebook.flipper.${appName}`,
device: deviceName,
device_id: testDevice.serial,
os: testDevice.serial,
},
device: testDevice,
isBackgroundPlugin(_pluginId) {
return !!options?.isBackgroundPlugin;
},
connected: (0, atom_1.createState)(true),
initPlugin() {
if (options?.isArchived) {
return;
}
this.connected.set(true);
pluginInstance.connect();
},
deinitPlugin() {
if (options?.isArchived) {
return;
}
this.connected.set(false);
pluginInstance.disconnect();
},
call(_api, method, _fromPlugin, params) {
return sendStub(method, params);
},
async supportsMethod(_api, method) {
return !options?.unsupportedMethods?.includes(method);
},
};
const serverAddOnControls = createServerAddOnControlsMock();
(0, FlipperLib_1.setFlipperLibImplementation)(flipperUtils);
const pluginInstance = new Plugin_1.SandyPluginInstance(serverAddOnControls, flipperUtils, definition, fakeFlipperClient, `${fakeFlipperClient.id}#${definition.id}`, options?.initialState);
const res = {
...createBasePluginResult(pluginInstance, serverAddOnControls),
instance: pluginInstance.instanceApi,
module,
connect: () => pluginInstance.connect(),
disconnect: () => pluginInstance.disconnect(),
onSend: sendStub,
sendEvent: (event, params) => {
res.sendEvents([
{
method: event,
params,
},
]);
},
sendEvents: (messages) => {
act(() => {
pluginInstance.receiveMessages(messages);
});
},
serverAddOnControls,
};
res._backingInstance = pluginInstance;
// we start activated
if (options?.isBackgroundPlugin) {
pluginInstance.connect(); // otherwise part of activate
}
if (!options?.startUnactivated) {
pluginInstance.activate();
}
return res;
}
exports.startPlugin = startPlugin;
function renderPlugin(module, options) {
// prevent bundling in UI bundle
// eslint-disable-next-line no-eval
const { render, act } = eval('require("@testing-library/react")');
const res = startPlugin(module, options);
const pluginInstance = res._backingInstance;
const renderer = render(React.createElement(PluginRenderer_1.SandyPluginRenderer, { plugin: pluginInstance }));
return {
...res,
renderer,
act,
destroy: () => {
renderer.unmount();
pluginInstance.destroy();
},
};
}
exports.renderPlugin = renderPlugin;
function startDevicePlugin(module, options) {
// eslint-disable-next-line no-eval
const { act } = eval('require("@testing-library/react")');
const definition = new SandyPluginDefinition_1.SandyPluginDefinition(createMockPluginDetails({ pluginType: 'device' }), module);
if (!definition.isDevicePlugin) {
throw new Error('Use `startPlugin` or `renderPlugin` to test non-device plugins');
}
const flipperLib = createMockFlipperLib(options);
const testDevice = createMockDevice(options);
const serverAddOnControls = createServerAddOnControlsMock();
(0, FlipperLib_1.setFlipperLibImplementation)(flipperLib);
const pluginInstance = new DevicePlugin_1.SandyDevicePluginInstance(serverAddOnControls, flipperLib, definition, testDevice, `${testDevice.serial}#${definition.id}`, options?.initialState);
const res = {
...createBasePluginResult(pluginInstance, serverAddOnControls),
module,
instance: pluginInstance.instanceApi,
sendLogEntry: (entry) => {
act(() => {
testDevice.addLogEntry(entry);
});
},
};
res._backingInstance = pluginInstance;
if (!options?.startUnactivated) {
// we start connected
pluginInstance.activate();
}
return res;
}
exports.startDevicePlugin = startDevicePlugin;
function renderDevicePlugin(module, options) {
// eslint-disable-next-line no-eval
const { render, act } = eval('require("@testing-library/react")');
const res = startDevicePlugin(module, options);
// @ts-ignore hidden api
const pluginInstance = res
._backingInstance;
const renderer = render(React.createElement(PluginRenderer_1.SandyPluginRenderer, { plugin: pluginInstance }));
return {
...res,
renderer,
act,
destroy: () => {
renderer.unmount();
pluginInstance.destroy();
},
};
}
exports.renderDevicePlugin = renderDevicePlugin;
function createBasePluginResult(pluginInstance, serverAddOnControls) {
return {
flipperLib: pluginInstance.flipperLib,
activate: () => pluginInstance.activate(),
deactivate: () => pluginInstance.deactivate(),
exportStateAsync: () => pluginInstance.exportState(createStubIdler(), () => { }),
// eslint-disable-next-line node/no-sync
exportState: () => pluginInstance.exportStateSync(),
triggerDeepLink: async (deepLink) => {
pluginInstance.triggerDeepLink(deepLink);
return new Promise((resolve) => {
// this ensures the test won't continue until the setImmediate used by
// the deeplink handling event is handled
setTimeout(resolve, 0);
});
},
destroy: () => pluginInstance.destroy(),
triggerMenuEntry: (action) => {
const entry = pluginInstance.menuEntries.find((e) => e.action === action);
if (!entry) {
throw new Error(`No menu entry found with action: ${action}`);
}
entry.handler();
},
serverAddOnControls,
};
}
function createMockDevice(options) {
const logListeners = [];
const crashListeners = [];
return {
os: 'Android',
description: {
os: 'Android',
deviceType: 'emulator',
features: {
screenCaptureAvailable: false,
screenshotAvailable: false,
},
serial: '123',
title: 'Test device',
},
deviceType: 'emulator',
serial: 'serial-000',
...options?.testDevice,
isArchived: !!options?.isArchived,
connected: (0, atom_1.createState)(true),
addLogListener(cb) {
logListeners.push(cb);
return (logListeners.length - 1);
},
removeLogListener(idx) {
logListeners[idx] = undefined;
},
addCrashListener(cb) {
crashListeners.push(cb);
return (crashListeners.length - 1);
},
removeCrashListener(idx) {
crashListeners[idx] = undefined;
},
addLogEntry(entry) {
logListeners.forEach((f) => f?.(entry));
},
executeShell: createStubFunction(),
clearLogs: createStubFunction(),
forwardPort: createStubFunction(),
get isConnected() {
return this.connected.get();
},
installApp(_) {
return Promise.resolve();
},
navigateToLocation: createStubFunction(),
screenshot: createStubFunction(),
sendMetroCommand: createStubFunction(),
};
}
function createStubIdler() {
return {
shouldIdle() {
return false;
},
idle() {
return Promise.resolve();
},
cancel() { },
isCancelled() {
return false;
},
};
}
function createServerAddOnControlsMock() {
return {
start: createStubFunction(),
stop: createStubFunction(),
sendMessage: createStubFunction(),
receiveMessage: createStubFunction(),
receiveAnyMessage: createStubFunction(),
unsubscribePlugin: createStubFunction(),
unsubscribe: createStubFunction(),
};
}
function createStubFlipperServerConfig() {
const rootPath = '/root';
const stubConfig = {
sessionId: (0, flipper_common_1.uuid)(),
environmentInfo: {
processId: 4242,
appVersion: '0.0.0',
isProduction: true,
releaseChannel: flipper_common_1.ReleaseChannel.DEFAULT,
flipperReleaseRevision: '000',
os: {
arch: 'arm64',
platform: 'darwin',
unixname: 'iamyourfather',
},
versions: {
node: '16.14.2',
platform: '22.6.0',
},
},
env: {
NODE_ENV: 'test',
},
gatekeepers: {
TEST_PASSING_GK: true,
TEST_FAILING_GK: false,
},
launcherSettings: {
ignoreLocalPin: false,
releaseChannel: flipper_common_1.ReleaseChannel.DEFAULT,
},
paths: {
appPath: rootPath,
desktopPath: `/dev/null`,
execPath: '/exec',
homePath: `/dev/null`,
staticPath: `${rootPath}/static`,
tempPath: '/temp',
},
processConfig: {
disabledPlugins: [],
lastWindowPosition: null,
launcherEnabled: false,
launcherMsg: null,
screenCapturePath: `/dev/null`,
updaterEnabled: true,
suppressPluginUpdateNotifications: false,
},
settings: {
androidHome: `/dev/null`,
darkMode: 'light',
enableAndroid: false,
enableIOS: false,
enablePhysicalIOS: false,
enablePrefetching: flipper_common_1.Tristate.False,
idbPath: `/dev/null`,
showWelcomeAtStartup: false,
suppressPluginErrors: false,
persistDeviceData: false,
enablePluginMarketplace: false,
marketplaceURL: '',
enablePluginMarketplaceAutoUpdate: true,
},
validWebSocketOrigins: [],
};
return stubConfig;
}
exports.createStubFlipperServerConfig = createStubFlipperServerConfig;
//# sourceMappingURL=test-utils.js.map
;