diff --git a/src/Client.js b/src/Client.js index 40a29eb8e..95b00b797 100644 --- a/src/Client.js +++ b/src/Client.js @@ -27,6 +27,11 @@ export type ClientQuery = {| device_id: string, |}; +export type ClientExport = {| + id: string, + query: ClientQuery, +|}; + type ErrorType = {message: string, stacktrace: string, name: string}; type RequestMetadata = {method: string, id: number, params: ?Object}; @@ -271,7 +276,7 @@ export default class Client extends EventEmitter { } } - toJSON() { + toJSON(): ClientExport { return {id: this.id, query: this.query}; } diff --git a/src/devices/BaseDevice.js b/src/devices/BaseDevice.js index 45c91ffba..e77f29d07 100644 --- a/src/devices/BaseDevice.js +++ b/src/devices/BaseDevice.js @@ -36,6 +36,13 @@ export type DeviceLogListener = (entry: DeviceLogEntry) => void; export type DeviceType = 'emulator' | 'physical'; +export type DeviceExport = {| + os: string, + title: string, + deviceType: DeviceType, + serial: string, +|}; + export type OS = 'iOS' | 'Android' | 'Windows'; export default class BaseDevice { @@ -67,7 +74,7 @@ export default class BaseDevice { return os.toLowerCase() === this.os.toLowerCase(); } - toJSON() { + toJSON(): DeviceExport { return { os: this.os, title: this.title, diff --git a/src/utils/__tests__/exportData.node.js b/src/utils/__tests__/exportData.node.js new file mode 100644 index 000000000..94383a9bd --- /dev/null +++ b/src/utils/__tests__/exportData.node.js @@ -0,0 +1,345 @@ +/** + * Copyright 2018-present Facebook. + * This source code is licensed under the MIT license found in the + * LICENSE file in the root directory of this source tree. + * @format + */ + +import {default as BaseDevice} from '../../devices/BaseDevice'; +import {processStore} from '../exportData'; +import {IOSDevice} from '../../..'; +import {FlipperDevicePlugin} from '../../plugin.js'; +import type {Notification} from '../../plugin.js'; +import type {ClientExport} from '../../Client.js'; + +class TestDevicePlugin extends FlipperDevicePlugin { + static id = 'TestDevicePlugin'; +} + +function generateNotifications( + id: string, + title: string, + message: string, + severity: 'warning' | 'error', +): Notification { + return {id, title, message, severity}; +} + +function generateClientIdentifier(device: BaseDevice, app: string): string { + const {os, deviceType, serial} = device; + const identifier = `${app}#${os}#${deviceType}#${serial}`; + return identifier; +} + +function generateClientFromDevice( + device: BaseDevice, + app: string, +): ClientExport { + const {os, deviceType, serial} = device; + const identifier = generateClientIdentifier(device, app); + return { + id: identifier, + query: {app, os, device: deviceType, device_id: serial}, + }; +} + +test('test generateClientFromDevice helper function', () => { + const device = new IOSDevice('serial', 'emulator', 'TestiPhone'); + const client = generateClientFromDevice(device, 'app'); + expect(client).toEqual({ + id: 'app#iOS#emulator#serial', + query: {app: 'app', os: 'iOS', device: 'emulator', device_id: 'serial'}, + }); +}); + +test('test generateClientIdentifier helper function', () => { + const device = new IOSDevice('serial', 'emulator', 'TestiPhone'); + const identifier = generateClientIdentifier(device, 'app'); + expect(identifier).toEqual('app#iOS#emulator#serial'); +}); + +test('test generateNotifications helper function', () => { + const notification = generateNotifications('id', 'title', 'msg', 'error'); + expect(notification).toEqual({ + id: 'id', + title: 'title', + message: 'msg', + severity: 'error', + }); +}); + +test('test processStore function for empty state', () => { + const json = processStore([], null, {}, [], new Map()); + expect(json).toBeNull(); +}); + +test('test processStore function for an iOS device connected', () => { + const json = processStore( + [], + new IOSDevice('serial', 'emulator', 'TestiPhone'), + {}, + [], + new Map(), + ); + expect(json).toBeDefined(); + // $FlowFixMe Flow doesn't that its a test and the assertion for null is already done + const {device, clients} = json; + expect(device).toBeDefined(); + expect(clients).toEqual([]); + //$FlowFixMe Flow doesn't that its a test and the assertion for null is already done + const {serial, deviceType, title, os} = device; + expect(serial).toEqual('serial'); + expect(deviceType).toEqual('emulator'); + expect(title).toEqual('TestiPhone'); + expect(os).toEqual('iOS'); + //$FlowFixMe Flow doesn't that its a test and the assertion for null is already done + const {pluginStates, activeNotifications} = json.store; + expect(pluginStates).toEqual({}); + expect(activeNotifications).toEqual([]); +}); + +test('test processStore function for an iOS device connected with client plugin data', () => { + const device = new IOSDevice('serial', 'emulator', 'TestiPhone'); + const clientIdentifier = generateClientIdentifier(device, 'testapp'); + const json = processStore( + [], + device, + {[clientIdentifier]: {msg: 'Test plugin'}}, + [generateClientFromDevice(device, 'testapp')], + new Map(), + ); + expect(json).toBeDefined(); + //$FlowFixMe Flow doesn't that its a test and the assertion for null is already done + const {pluginStates} = json.store; + let expectedPluginState = { + [clientIdentifier]: {msg: 'Test plugin'}, + }; + expect(pluginStates).toEqual(expectedPluginState); +}); + +test('test processStore function to have only the client for the selected device', () => { + const selectedDevice = new IOSDevice('serial', 'emulator', 'TestiPhone'); + const unselectedDevice = new IOSDevice( + 'identifier', + 'emulator', + 'TestiPhone', + ); + + const unselectedDeviceClientIdentifier = generateClientIdentifier( + unselectedDevice, + 'testapp', + ); + const selectedDeviceClientIdentifier = generateClientIdentifier( + selectedDevice, + 'testapp', + ); + const selectedDeviceClient = generateClientFromDevice( + selectedDevice, + 'testapp', + ); + const json = processStore( + [], + selectedDevice, + { + [unselectedDeviceClientIdentifier + '#testapp']: { + msg: 'Test plugin unselected device', + }, + [selectedDeviceClientIdentifier + '#testapp']: { + msg: 'Test plugin selected device', + }, + }, + [ + selectedDeviceClient, + generateClientFromDevice(unselectedDevice, 'testapp'), + ], + new Map(), + ); + expect(json).toBeDefined(); + //$FlowFixMe Flow doesn't that its a test and the assertion for null is already added + const {clients} = json; + //$FlowFixMe Flow doesn't that its a test and the assertion for null is already added + const {pluginStates} = json.store; + let expectedPluginState = { + [selectedDeviceClientIdentifier + '#testapp']: { + msg: 'Test plugin selected device', + }, + }; + expect(clients).toEqual([selectedDeviceClient]); + expect(pluginStates).toEqual(expectedPluginState); +}); + +test('test processStore function to have multiple clients for the selected device', () => { + const selectedDevice = new IOSDevice('serial', 'emulator', 'TestiPhone'); + + const clientIdentifierApp1 = generateClientIdentifier( + selectedDevice, + 'testapp1', + ); + const clientIdentifierApp2 = generateClientIdentifier( + selectedDevice, + 'testapp2', + ); + + const client1 = generateClientFromDevice(selectedDevice, 'testapp1'); + const client2 = generateClientFromDevice(selectedDevice, 'testapp2'); + + const json = processStore( + [], + selectedDevice, + { + [clientIdentifierApp1 + '#testapp1']: { + msg: 'Test plugin App1', + }, + [clientIdentifierApp2 + '#testapp2']: { + msg: 'Test plugin App2', + }, + }, + [ + generateClientFromDevice(selectedDevice, 'testapp1'), + generateClientFromDevice(selectedDevice, 'testapp2'), + ], + new Map(), + ); + expect(json).toBeDefined(); + //$FlowFixMe Flow doesn't that its a test and the assertion for null is already added + const {clients} = json; + //$FlowFixMe Flow doesn't that its a test and the assertion for null is already added + const {pluginStates} = json.store; + let expectedPluginState = { + [clientIdentifierApp1 + '#testapp1']: { + msg: 'Test plugin App1', + }, + [clientIdentifierApp2 + '#testapp2']: { + msg: 'Test plugin App2', + }, + }; + expect(clients).toEqual([client1, client2]); + expect(pluginStates).toEqual(expectedPluginState); +}); + +test('test processStore function for device plugin state and no clients', () => { + // Test case to verify that device plugin data is exported even if there are no clients + const selectedDevice = new IOSDevice('serial', 'emulator', 'TestiPhone'); + const json = processStore( + [], + selectedDevice, + { + 'serial#TestDevicePlugin': { + msg: 'Test Device plugin', + }, + }, + [], + new Map([['TestDevicePlugin', TestDevicePlugin]]), + ); + expect(json).toBeDefined(); + //$FlowFixMe Flow doesn't that its a test and the assertion for null is already done + const {pluginStates} = json.store; + //$FlowFixMe Flow doesn't that its a test and the assertion for null is already done + const {clients} = json; + let expectedPluginState = { + 'serial#TestDevicePlugin': {msg: 'Test Device plugin'}, + }; + expect(pluginStates).toEqual(expectedPluginState); + expect(clients).toEqual([]); +}); + +test('test processStore function for unselected device plugin state and no clients', () => { + // Test case to verify that device plugin data is exported even if there are no clients + const selectedDevice = new IOSDevice('serial', 'emulator', 'TestiPhone'); + const json = processStore( + [], + selectedDevice, + { + 'unselectedDeviceIdentifier#TestDevicePlugin': { + msg: 'Test Device plugin', + }, + }, + [], + new Map([['TestDevicePlugin', TestDevicePlugin]]), + ); + expect(json).toBeDefined(); + //$FlowFixMe Flow doesn't that its a test and the assertion for null is already done + const {pluginStates} = json.store; + //$FlowFixMe Flow doesn't that its a test and the assertion for null is already done + const {clients} = json; + expect(pluginStates).toEqual({}); + expect(clients).toEqual([]); +}); + +test('test processStore function for notifications for selected device', () => { + // Test case to verify that device plugin data is exported even if there are no clients + const selectedDevice = new IOSDevice('serial', 'emulator', 'TestiPhone'); + const client = generateClientFromDevice(selectedDevice, 'testapp1'); + const notification = generateNotifications( + 'notificationID', + 'title', + 'Notification Message', + 'warning', + ); + const activeNotification = { + pluginId: 'TestNotification', + notification, + client: client.id, + }; + const json = processStore( + [activeNotification], + selectedDevice, + {}, + [client], + new Map([['TestDevicePlugin', TestDevicePlugin]]), + ); + expect(json).toBeDefined(); + //$FlowFixMe Flow doesn't that its a test and the assertion for null is already done + const {pluginStates} = json.store; + //$FlowFixMe Flow doesn't that its a test and the assertion for null is already done + const {clients} = json; + expect(pluginStates).toEqual({}); + expect(clients).toEqual([client]); + //$FlowFixMe Flow doesn't that its a test and the assertion for null is already done + const {activeNotifications} = json.store; + expect(activeNotifications).toEqual([activeNotification]); +}); + +test('test processStore function for notifications for unselected device', () => { + // Test case to verify that device plugin data is exported even if there are no clients + const selectedDevice = new IOSDevice('serial', 'emulator', 'TestiPhone'); + const unselectedDevice = new IOSDevice( + 'identifier', + 'emulator', + 'TestiPhone', + ); + + const client = generateClientFromDevice(selectedDevice, 'testapp1'); + const unselectedclient = generateClientFromDevice( + unselectedDevice, + 'testapp1', + ); + const notification = generateNotifications( + 'notificationID', + 'title', + 'Notification Message', + 'warning', + ); + const activeNotification = { + pluginId: 'TestNotification', + notification, + client: unselectedclient.id, + }; + const json = processStore( + [activeNotification], + selectedDevice, + {}, + [client, unselectedclient], + new Map(), + ); + expect(json).toBeDefined(); + //$FlowFixMe Flow doesn't that its a test and the assertion for null is already done + const {pluginStates} = json.store; + //$FlowFixMe Flow doesn't that its a test and the assertion for null is already done + const {clients} = json; + expect(pluginStates).toEqual({}); + expect(clients).toEqual([client]); + //$FlowFixMe Flow doesn't that its a test and the assertion for null is already done + const {activeNotifications} = json.store; + expect(activeNotifications).toEqual([]); +}); diff --git a/src/utils/exportData.js b/src/utils/exportData.js index 9a970b949..5ab6129b1 100644 --- a/src/utils/exportData.js +++ b/src/utils/exportData.js @@ -4,6 +4,15 @@ * LICENSE file in the root directory of this source tree. * @format */ +import type {Store} from '../reducers'; +import type {DeviceExport} from '../devices/BaseDevice'; +import type {State as PluginStates} from '../reducers/pluginStates'; +import type {PluginNotification} from '../reducers/notifications.js'; +import type {ClientExport} from '../Client.js'; +import type {State as PluginStatesState} from '../reducers/pluginStates'; +import {FlipperDevicePlugin} from '../plugin.js'; +import {default as BaseDevice} from '../devices/BaseDevice'; + import fs from 'fs'; import os from 'os'; import path from 'path'; @@ -14,54 +23,117 @@ const exportFilePath = path.join( 'FlipperExport.json', ); -export const exportStoreToFile = (data: Store): Promise => { - const state = data.getState(); - const json = { - fileVersion: '1.0.0', - device: {}, - clients: [], - store: { - pluginStates: {}, - activeNotifications: [], - }, - }; +export type ExportType = {| + fileVersion: '1.0.0', + clients: Array, + device: ?DeviceExport, + store: { + pluginStates: PluginStates, + activeNotifications: Array, + }, +|}; - const device = state.connections.selectedDevice; +export function processClients( + clients: Array, + serial: string, +): Array { + return clients.filter(client => client.query.device_id === serial); +} + +export function processPluginStates( + clients: Array, + serial: string, + allPluginStates: PluginStatesState, + devicePlugins: Map>>, +): PluginStatesState { + let pluginStates = {}; + for (let key in allPluginStates) { + let keyArray = key.split('#'); + const pluginName = keyArray.pop(); + const filteredClients = clients.filter(client => { + // Remove the last entry related to plugin + return client.id.includes(keyArray.join('#')); + }); + if ( + filteredClients.length > 0 || + (devicePlugins.has(pluginName) && serial === keyArray[0]) + ) { + // There need not be any client for device Plugins + pluginStates = {...pluginStates, [key]: allPluginStates[key]}; + } + } + return pluginStates; +} + +export function processNotificationStates( + clients: Array, + serial: string, + allActiveNotifications: Array, + devicePlugins: Map>>, +): Array { + let activeNotifications = allActiveNotifications.filter(notif => { + const filteredClients = clients.filter( + client => (notif.client ? client.id.includes(notif.client) : false), + ); + return ( + filteredClients.length > 0 || + (devicePlugins.has(notif.pluginId) && serial === notif.client) + ); // There need not be any client for device Plugins + }); + return activeNotifications; +} + +export const processStore = ( + activeNotifications: Array, + device: ?BaseDevice, + pluginStates: PluginStatesState, + clients: Array, + devicePlugins: Map>>, +): ?ExportType => { if (device) { const {serial} = device; - json.device = device.toJSON(); - const clients = state.connections.clients - .filter(client => { - return client.query.device_id === serial; - }) - .map(client => { - return client.toJSON(); - }); + const processedClients = processClients(clients, serial); + let processedPluginStates = processPluginStates( + processedClients, + serial, + pluginStates, + devicePlugins, + ); + const processedActiveNotifications = processNotificationStates( + processedClients, + serial, + activeNotifications, + devicePlugins, + ); + return { + fileVersion: '1.0.0', + clients: processedClients, + device: device.toJSON(), + store: { + pluginStates: processedPluginStates, + activeNotifications: processedActiveNotifications, + }, + }; + } + return null; +}; - json.clients = clients; - - const allPluginStates = state.pluginStates; - let pluginStates = {}; - for (let key in allPluginStates) { - const filteredClients = clients.filter(client => { - let keyArray = key.split('#'); - keyArray.pop(); // Remove the last entry related to plugin - return client.id.includes(keyArray.join('#')); - }); - if (filteredClients.length > 0) { - pluginStates = {...pluginStates, [key]: allPluginStates[key]}; - json.store.pluginStates = pluginStates; - } - } - - const allActiveNotifications = state.notifications.activeNotifications; - let activeNotifications = allActiveNotifications.filter(notif => { - const filteredClients = clients.filter(client => - client.id.includes(notif.client), - ); - return filteredClients.length > 0; - }); - json.store.activeNotifications = activeNotifications; +export const exportStoreToFile = (data: Store): Promise => { + const state = data.getState(); + const {activeNotifications} = state.notifications; + const {selectedDevice, clients} = state.connections; + const {pluginStates} = state; + const {devicePlugins} = state.plugins; + // TODO: T39612653 Make Client mockable. Currently rsocket logic is tightly coupled. + // Not passing the entire state as currently Client is not mockable. + const json = processStore( + activeNotifications, + selectedDevice, + pluginStates, + clients.map(client => client.toJSON()), + devicePlugins, + ); + if (json) { return new Promise((resolve, reject) => { fs.writeFile(exportFilePath, JSON.stringify(json), err => { if (err) {