Summary: This diff changes the sidebar navigation, fixing a bunch of issues: It will be possible to quickly switch again between the same plugins in multiple apps No need to expand-and-check the app dropdown until the app is connected No need for ugly fallback selections if some app connects faster than another one Reviewed By: nikoant Differential Revision: D19272701 fbshipit-source-id: 10f5fab42391014ef4a4a4c91c529d93f8bfb125
406 lines
11 KiB
TypeScript
406 lines
11 KiB
TypeScript
/**
|
|
* 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 {FlipperPlugin} from '../../plugin';
|
|
import {createMockFlipperWithPlugin} from '../../test-utils/createMockFlipperWithPlugin';
|
|
import {GK, Store, Client} from 'flipper';
|
|
import {selectPlugin, starPlugin} from '../../reducers/connections';
|
|
import {processMessageQueue} from '../messageQueue';
|
|
import {getPluginKey} from '../pluginUtils';
|
|
import {TestIdler} from '../Idler';
|
|
import pluginMessageQueue, {
|
|
State,
|
|
queueMessage,
|
|
} from '../../reducers/pluginMessageQueue';
|
|
|
|
interface PersistedState {
|
|
count: 1;
|
|
}
|
|
|
|
class TestPlugin extends FlipperPlugin<any, any, any> {
|
|
static id = 'TestPlugin';
|
|
|
|
static defaultPersistedState = {
|
|
count: 0,
|
|
};
|
|
|
|
static persistedStateReducer(
|
|
persistedState: PersistedState,
|
|
method: string,
|
|
payload: {delta?: number},
|
|
) {
|
|
if (method === 'inc') {
|
|
return Object.assign({}, persistedState, {
|
|
count: persistedState.count + ((payload && payload?.delta) || 1),
|
|
});
|
|
}
|
|
return persistedState;
|
|
}
|
|
|
|
render() {
|
|
return null;
|
|
}
|
|
}
|
|
|
|
function starTestPlugin(store: Store, client: Client) {
|
|
store.dispatch(
|
|
starPlugin({
|
|
selectedPlugin: TestPlugin.id,
|
|
selectedApp: client.query.app,
|
|
}),
|
|
);
|
|
}
|
|
|
|
function selectDeviceLogs(store: Store) {
|
|
store.dispatch(
|
|
selectPlugin({
|
|
selectedPlugin: 'DeviceLogs',
|
|
selectedApp: null,
|
|
deepLinkPayload: null,
|
|
selectedDevice: store.getState().connections.selectedDevice!,
|
|
}),
|
|
);
|
|
}
|
|
|
|
function selectTestPlugin(store: Store, client: Client) {
|
|
store.dispatch(
|
|
selectPlugin({
|
|
selectedPlugin: TestPlugin.id,
|
|
selectedApp: client.query.app,
|
|
deepLinkPayload: null,
|
|
selectedDevice: store.getState().connections.selectedDevice!,
|
|
}),
|
|
);
|
|
}
|
|
|
|
test('will process event with GK disabled', async () => {
|
|
await createMockFlipperWithPlugin(
|
|
TestPlugin,
|
|
async ({store, sendMessage}) => {
|
|
expect(store.getState().connections.selectedPlugin).toBe('TestPlugin');
|
|
sendMessage('inc', {});
|
|
expect(store.getState().pluginStates).toMatchInlineSnapshot(`
|
|
Object {
|
|
"TestApp#Android#unit_test#serial#TestPlugin": Object {
|
|
"count": 1,
|
|
},
|
|
}
|
|
`);
|
|
},
|
|
);
|
|
});
|
|
|
|
test('queue - events are processed immediately if plugin is selected', async () => {
|
|
await createMockFlipperWithPlugin(
|
|
TestPlugin,
|
|
async ({store, sendMessage}) => {
|
|
await GK.withWhitelistedGK('flipper_event_queue', () => {
|
|
expect(store.getState().connections.selectedPlugin).toBe('TestPlugin');
|
|
sendMessage('inc', {});
|
|
expect(store.getState().pluginStates).toMatchInlineSnapshot(`
|
|
Object {
|
|
"TestApp#Android#unit_test#serial#TestPlugin": Object {
|
|
"count": 1,
|
|
},
|
|
}
|
|
`);
|
|
expect(store.getState().pluginMessageQueue).toMatchInlineSnapshot(
|
|
`Object {}`,
|
|
);
|
|
});
|
|
},
|
|
);
|
|
});
|
|
|
|
test('queue - events are NOT processed immediately if plugin is NOT selected (but starred)', async () => {
|
|
await createMockFlipperWithPlugin(
|
|
TestPlugin,
|
|
async ({client, device, store, sendMessage}) => {
|
|
await GK.withWhitelistedGK('flipper_event_queue', async () => {
|
|
selectDeviceLogs(store);
|
|
expect(store.getState().connections.selectedPlugin).not.toBe(
|
|
'TestPlugin',
|
|
);
|
|
|
|
sendMessage('inc', {});
|
|
sendMessage('inc', {delta: 2});
|
|
expect(store.getState().pluginStates).toMatchInlineSnapshot(
|
|
`Object {}`,
|
|
);
|
|
expect(store.getState().pluginMessageQueue).toMatchInlineSnapshot(`
|
|
Object {
|
|
"TestApp#Android#unit_test#serial#TestPlugin": Array [
|
|
Object {
|
|
"method": "inc",
|
|
"params": Object {},
|
|
},
|
|
Object {
|
|
"method": "inc",
|
|
"params": Object {
|
|
"delta": 2,
|
|
},
|
|
},
|
|
],
|
|
}
|
|
`);
|
|
|
|
// process the message
|
|
const pluginKey = getPluginKey(client.id, device, TestPlugin.id);
|
|
await processMessageQueue(TestPlugin, pluginKey, store);
|
|
expect(store.getState().pluginStates).toEqual({
|
|
[pluginKey]: {
|
|
count: 3,
|
|
},
|
|
});
|
|
|
|
expect(store.getState().pluginMessageQueue).toEqual({
|
|
[pluginKey]: [],
|
|
});
|
|
|
|
// unstar, but, messages still arrives because selected
|
|
starTestPlugin(store, client);
|
|
selectTestPlugin(store, client);
|
|
sendMessage('inc', {delta: 3});
|
|
// active, immediately processed
|
|
expect(store.getState().pluginStates).toEqual({
|
|
[pluginKey]: {
|
|
count: 6,
|
|
},
|
|
});
|
|
|
|
// different plugin, and not starred, message will never arrive
|
|
selectDeviceLogs(store);
|
|
sendMessage('inc', {delta: 4});
|
|
expect(store.getState().pluginMessageQueue).toEqual({
|
|
[pluginKey]: [],
|
|
});
|
|
|
|
// star again, plugin still not selected, message is queued
|
|
starTestPlugin(store, client);
|
|
sendMessage('inc', {delta: 5});
|
|
|
|
expect(store.getState().pluginMessageQueue).toEqual({
|
|
[pluginKey]: [{method: 'inc', params: {delta: 5}}],
|
|
});
|
|
});
|
|
},
|
|
);
|
|
});
|
|
|
|
test('queue - events processing will be paused', async () => {
|
|
await createMockFlipperWithPlugin(
|
|
TestPlugin,
|
|
async ({client, device, store, sendMessage}) => {
|
|
await GK.withWhitelistedGK('flipper_event_queue', async () => {
|
|
selectDeviceLogs(store);
|
|
|
|
sendMessage('inc', {});
|
|
sendMessage('inc', {delta: 3});
|
|
sendMessage('inc', {delta: 5});
|
|
|
|
// process the message
|
|
const pluginKey = getPluginKey(client.id, device, TestPlugin.id);
|
|
|
|
// controlled idler will signal and and off that idling is needed
|
|
const idler = new TestIdler();
|
|
|
|
const p = processMessageQueue(
|
|
TestPlugin,
|
|
pluginKey,
|
|
store,
|
|
undefined,
|
|
idler,
|
|
);
|
|
|
|
expect(store.getState().pluginStates).toEqual({
|
|
[pluginKey]: {
|
|
count: 4,
|
|
},
|
|
});
|
|
|
|
expect(store.getState().pluginMessageQueue).toEqual({
|
|
[pluginKey]: [{method: 'inc', params: {delta: 5}}],
|
|
});
|
|
|
|
await idler.next();
|
|
expect(store.getState().pluginStates).toEqual({
|
|
[pluginKey]: {
|
|
count: 9,
|
|
},
|
|
});
|
|
|
|
expect(store.getState().pluginMessageQueue).toEqual({
|
|
[pluginKey]: [],
|
|
});
|
|
|
|
// don't idle anymore
|
|
idler.run();
|
|
await p;
|
|
});
|
|
},
|
|
);
|
|
});
|
|
|
|
test('queue - messages that arrive during processing will be queued', async () => {
|
|
await createMockFlipperWithPlugin(
|
|
TestPlugin,
|
|
async ({client, device, store, sendMessage}) => {
|
|
await GK.withWhitelistedGK('flipper_event_queue', async () => {
|
|
selectDeviceLogs(store);
|
|
|
|
sendMessage('inc', {});
|
|
sendMessage('inc', {delta: 2});
|
|
sendMessage('inc', {delta: 3});
|
|
|
|
// process the message
|
|
const pluginKey = getPluginKey(client.id, device, TestPlugin.id);
|
|
|
|
const idler = new TestIdler();
|
|
|
|
const p = processMessageQueue(
|
|
TestPlugin,
|
|
pluginKey,
|
|
store,
|
|
undefined,
|
|
idler,
|
|
);
|
|
|
|
// first message is consumed
|
|
expect(store.getState().pluginMessageQueue[pluginKey].length).toBe(1);
|
|
expect(store.getState().pluginStates[pluginKey].count).toBe(3);
|
|
|
|
// Select the current plugin as active, still, messages should end up in the queue
|
|
store.dispatch(
|
|
selectPlugin({
|
|
selectedPlugin: TestPlugin.id,
|
|
selectedApp: client.id,
|
|
deepLinkPayload: null,
|
|
selectedDevice: device,
|
|
}),
|
|
);
|
|
expect(store.getState().connections.selectedPlugin).toBe('TestPlugin');
|
|
|
|
sendMessage('inc', {delta: 4});
|
|
// should not be processed yet
|
|
expect(store.getState().pluginMessageQueue[pluginKey].length).toBe(2);
|
|
expect(store.getState().pluginStates[pluginKey].count).toBe(3);
|
|
|
|
await idler.next();
|
|
expect(store.getState().pluginMessageQueue[pluginKey].length).toBe(0);
|
|
expect(store.getState().pluginStates[pluginKey].count).toBe(10);
|
|
|
|
idler.run();
|
|
await p;
|
|
});
|
|
},
|
|
);
|
|
});
|
|
|
|
test('queue - processing can be cancelled', async () => {
|
|
await createMockFlipperWithPlugin(
|
|
TestPlugin,
|
|
async ({client, device, store, sendMessage}) => {
|
|
await GK.withWhitelistedGK('flipper_event_queue', async () => {
|
|
selectDeviceLogs(store);
|
|
|
|
sendMessage('inc', {});
|
|
sendMessage('inc', {delta: 2});
|
|
sendMessage('inc', {delta: 3});
|
|
sendMessage('inc', {delta: 4});
|
|
sendMessage('inc', {delta: 5});
|
|
|
|
// process the message
|
|
const pluginKey = getPluginKey(client.id, device, TestPlugin.id);
|
|
|
|
const idler = new TestIdler();
|
|
|
|
const p = processMessageQueue(
|
|
TestPlugin,
|
|
pluginKey,
|
|
store,
|
|
undefined,
|
|
idler,
|
|
);
|
|
|
|
// first message is consumed
|
|
await idler.next();
|
|
expect(store.getState().pluginMessageQueue[pluginKey].length).toBe(1);
|
|
expect(store.getState().pluginStates[pluginKey].count).toBe(10);
|
|
|
|
idler.cancel();
|
|
|
|
expect(store.getState().pluginMessageQueue[pluginKey].length).toBe(1);
|
|
expect(store.getState().pluginStates[pluginKey].count).toBe(10);
|
|
await p;
|
|
});
|
|
},
|
|
);
|
|
});
|
|
|
|
test('queue - make sure resetting plugin state clears the message queue', async () => {
|
|
await createMockFlipperWithPlugin(
|
|
TestPlugin,
|
|
async ({client, device, store, sendMessage}) => {
|
|
await GK.withWhitelistedGK('flipper_event_queue', async () => {
|
|
selectDeviceLogs(store);
|
|
|
|
sendMessage('inc', {});
|
|
sendMessage('inc', {delta: 2});
|
|
|
|
const pluginKey = getPluginKey(client.id, device, TestPlugin.id);
|
|
|
|
expect(store.getState().pluginMessageQueue[pluginKey].length).toBe(2);
|
|
|
|
store.dispatch({
|
|
type: 'CLEAR_PLUGIN_STATE',
|
|
payload: {clientId: client.id, devicePlugins: new Set()},
|
|
});
|
|
|
|
expect(store.getState().pluginMessageQueue[pluginKey]).toBe(undefined);
|
|
});
|
|
},
|
|
);
|
|
});
|
|
|
|
test('queue will be cleaned up when it exceeds maximum size', () => {
|
|
let state: State = {};
|
|
const pluginKey = 'test';
|
|
const queueSize = 5000;
|
|
let i = 0;
|
|
for (i = 0; i < queueSize; i++) {
|
|
state = pluginMessageQueue(
|
|
state,
|
|
queueMessage(pluginKey, 'test', {i}, queueSize),
|
|
);
|
|
}
|
|
// almost full
|
|
expect(state[pluginKey][0]).toEqual({method: 'test', params: {i: 0}});
|
|
expect(state[pluginKey].length).toBe(queueSize); // ~5000
|
|
expect(state[pluginKey][queueSize - 1]).toEqual({
|
|
method: 'test',
|
|
params: {i: queueSize - 1}, // ~4999
|
|
});
|
|
|
|
state = pluginMessageQueue(
|
|
state,
|
|
queueMessage(pluginKey, 'test', {i: ++i}, queueSize),
|
|
);
|
|
|
|
const newLength = Math.ceil(0.9 * queueSize) + 1; // ~4500
|
|
expect(state[pluginKey].length).toBe(newLength);
|
|
expect(state[pluginKey][0]).toEqual({
|
|
method: 'test',
|
|
params: {i: queueSize - newLength + 1}, // ~500
|
|
});
|
|
expect(state[pluginKey][newLength - 1]).toEqual({
|
|
method: 'test',
|
|
params: {i: i}, // ~50001
|
|
});
|
|
});
|