/** * Copyright (c) Facebook, Inc. and its affiliates. * * This source code is licensed under the MIT license found in the * LICENSE file in the root directory of this source tree. * * @format */ import path from 'path'; import fs from 'fs'; import {Store, State} from '../reducers'; import {getPluginKey} from './pluginUtils'; import {serialize} from './serialization'; import {isSandyPlugin} from '../plugin'; let pluginRecordingState: { recording: string; pluginName: string; startState: any; events: [string, any][]; endState: any; } = initialRecordingState(); function initialRecordingState(): typeof pluginRecordingState { return { recording: '', startState: undefined, events: [], endState: undefined, pluginName: '', }; } export function isRecordingEvents(pluginKey: string) { return pluginRecordingState.recording === pluginKey; } export function flipperRecorderAddEvent( pluginKey: string, method: string, params: any, ) { if (pluginRecordingState.recording === pluginKey) { pluginRecordingState.events.push([method, params]); } } async function flipperStartPluginRecording(state: State) { if (pluginRecordingState.recording) { throw new Error('A plugin recording is already running'); } const app = state.connections.selectedApp; const client = state.connections.clients.find((client) => client.id === app); if (!app || !client) { throw new Error('Can only record plugin states if a device is selected'); } const selectedPlugin = state.connections.selectedPlugin; const pluginKey = getPluginKey(client.id, null, selectedPlugin!); const plugin = state.plugins.clientPlugins.get(selectedPlugin!); if (!selectedPlugin || !plugin) { throw new Error('Can only record plugin states if a plugin is selected'); } pluginRecordingState = { recording: pluginKey, startState: undefined, events: [], endState: undefined, pluginName: selectedPlugin, }; // Note that we don't use the plugin's own serializeState, as that might interact with the // device state, and is used for creating Flipper Exports. pluginRecordingState.startState = await serialize( state.pluginStates[pluginKey] || (isSandyPlugin(plugin) ? {} : plugin.defaultPersistedState), ); console.log( `Started recordig the states of plugin ${selectedPlugin}..... Use window.flipperStopPluginRecording() to finish this process`, ); } async function flipperStopPluginRecording(state: State) { if (!pluginRecordingState.recording) { throw new Error('No plugin recording is running. '); } if (!pluginRecordingState.events.length) { console.warn('No events were captured, cancelling recording'); pluginRecordingState = initialRecordingState(); return; } pluginRecordingState.endState = await serialize( state.pluginStates[pluginRecordingState.recording], ); const pluginName = pluginRecordingState.pluginName; const snapShotFileContents = JSON.stringify(pluginRecordingState); const snapShotFileName = `${pluginName}.pluginSnapshot.json`; const testFileName = `${pluginName}EventsRunner.tsx`; const outDir = getOutputDir(pluginName); const testFileContents = generateTestSuite(pluginName, snapShotFileName); await fs.promises.writeFile( path.join(outDir, snapShotFileName), snapShotFileContents, 'utf8', ); await fs.promises.writeFile( path.join(outDir, testFileName), testFileContents, 'utf8', ); console.log( `Finished recording ${pluginRecordingState.events.length} for plugin ${ pluginRecordingState.recording }. Generated files ${path.join(outDir, testFileName)} and ${path.join( outDir, snapShotFileName, )}. Move them to the '__tests__ folder of your plugin to incorporate them`, ); pluginRecordingState = initialRecordingState(); } export function registerRecordingHooks(store: Store) { Object.assign(window, { flipperStartPluginRecording() { flipperStartPluginRecording(store.getState()); }, flipperStopPluginRecording() { flipperStopPluginRecording(store.getState()); }, }); } function getOutputDir(pluginName: string) { const outDir = path.join(process.cwd(), '..'); const fbPluginDir = path.join( outDir, 'plugins', 'fb', pluginName.toLowerCase(), '__tests__', ); const defaultPluginDir = path.join( outDir, 'plugins', pluginName.toLowerCase(), '__tests__', ); if (fs.existsSync(fbPluginDir)) { return fbPluginDir; } else if (fs.existsSync(defaultPluginDir)) { return defaultPluginDir; } return outDir; } function generateTestSuite(pluginName: string, snapShotFileName: string) { return `\ /** * Copyright (c) Facebook, Inc. and its affiliates. * * This source code is licensed under the MIT license found in the * LICENSE file in the root directory of this source tree. * * @format */ // This file was initially generated by using \`flipperStartPluginRecording()\` in Flipper console import fs from 'fs'; import path from 'path'; import {deserialize} from '../ui'; import Plugin from '../'; test('Verify events produce a consistent end state for plugin ${pluginName}', async () => { const snapshotData: { startState: string; endState: string; events: [string, any][]; } = JSON.parse( await fs.promises.readFile( path.join(__dirname, '${snapShotFileName}'), 'utf8', ), ); const startState: typeof Plugin.defaultPersistedState = deserialize( snapshotData.startState, ); const endState: typeof Plugin.defaultPersistedState = deserialize( snapshotData.endState, ); const startTime = Date.now(); const generatedEndState = snapshotData.events.reduce( (store, [method, params]) => Plugin.persistedStateReducer(store, method, params), startState, ); const totalTime = Date.now() - startTime; expect(generatedEndState).toEqual(endState); console.log( \`Reducer took $\{totalTime\}ms. to process $\{snapshotData.events.length\} events\`, ); }); `; }