UNPKG

flipper-plugin

Version:

Flipper Desktop plugin SDK and components

504 lines 17.9 kB
"use strict"; /** * 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