handle device / client absence for deeplinks
Summary: This diff makes the new deeplink format feature complete, make sure VPN connection, plugin installation, client & device selection are now all handled. See the test plan for examples. Changelog: Flipper now supports a richer protocol for opening deeplinks: https://fbflipper.com/docs/extending/deeplinks#open-plugin Reviewed By: timur-valiev Differential Revision: D30423809 fbshipit-source-id: e6cf4bf852b2c64e9a79a33ef0842eb27f68f840
This commit is contained in:
committed by
Facebook GitHub Bot
parent
846246ffae
commit
a2644b4a2e
@@ -18,9 +18,11 @@ import {
|
|||||||
usePlugin,
|
usePlugin,
|
||||||
createState,
|
createState,
|
||||||
useValue,
|
useValue,
|
||||||
|
DevicePluginClient,
|
||||||
} from 'flipper-plugin';
|
} from 'flipper-plugin';
|
||||||
import {parseOpenPluginParams} from '../handleOpenPluginDeeplink';
|
import {parseOpenPluginParams} from '../handleOpenPluginDeeplink';
|
||||||
import {handleDeeplink} from '../../deeplink';
|
import {handleDeeplink} from '../../deeplink';
|
||||||
|
import {selectPlugin} from '../../reducers/connections';
|
||||||
|
|
||||||
test('open-plugin deeplink parsing', () => {
|
test('open-plugin deeplink parsing', () => {
|
||||||
const testpayload = 'http://www.google/?test=c o%20o+l';
|
const testpayload = 'http://www.google/?test=c o%20o+l';
|
||||||
@@ -118,3 +120,156 @@ test('Triggering a deeplink will work', async () => {
|
|||||||
</body>
|
</body>
|
||||||
`);
|
`);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
test('triggering a deeplink without applicable device can wait for a device', async () => {
|
||||||
|
let lastOS: string = '';
|
||||||
|
const definition = TestUtils.createTestDevicePlugin(
|
||||||
|
{
|
||||||
|
Component() {
|
||||||
|
return <p>Hello</p>;
|
||||||
|
},
|
||||||
|
devicePlugin(c: DevicePluginClient) {
|
||||||
|
lastOS = c.device.os;
|
||||||
|
return {};
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 'DevicePlugin',
|
||||||
|
supportedDevices: [{os: 'iOS'}],
|
||||||
|
},
|
||||||
|
);
|
||||||
|
const {renderer, store, createDevice} = await renderMockFlipperWithPlugin(
|
||||||
|
definition,
|
||||||
|
);
|
||||||
|
|
||||||
|
store.dispatch(
|
||||||
|
selectPlugin({selectedPlugin: 'nonexisting', deepLinkPayload: null}),
|
||||||
|
);
|
||||||
|
expect(renderer.baseElement).toMatchInlineSnapshot(`
|
||||||
|
<body>
|
||||||
|
<div />
|
||||||
|
</body>
|
||||||
|
`);
|
||||||
|
|
||||||
|
const handlePromise = handleDeeplink(
|
||||||
|
store,
|
||||||
|
`flipper://open-plugin?plugin-id=${definition.id}&devices=iOS`,
|
||||||
|
);
|
||||||
|
|
||||||
|
jest.runAllTimers();
|
||||||
|
|
||||||
|
// No device yet available (dialogs are not renderable atm)
|
||||||
|
expect(renderer.baseElement).toMatchInlineSnapshot(`
|
||||||
|
<body>
|
||||||
|
<div />
|
||||||
|
</body>
|
||||||
|
`);
|
||||||
|
|
||||||
|
// create a new device
|
||||||
|
createDevice({serial: 'device2', os: 'iOS'});
|
||||||
|
|
||||||
|
// wizard should continue automatically
|
||||||
|
await handlePromise;
|
||||||
|
expect(renderer.baseElement).toMatchInlineSnapshot(`
|
||||||
|
<body>
|
||||||
|
<div>
|
||||||
|
<div
|
||||||
|
class="css-1x2cmzz-SandySplitContainer e1hsqii10"
|
||||||
|
>
|
||||||
|
<div />
|
||||||
|
<div
|
||||||
|
class="css-1knrt0j-SandySplitContainer e1hsqii10"
|
||||||
|
>
|
||||||
|
<div
|
||||||
|
class="css-1woty6b-Container"
|
||||||
|
>
|
||||||
|
<p>
|
||||||
|
Hello
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
<div
|
||||||
|
class="css-724x97-View-FlexBox-FlexRow"
|
||||||
|
id="detailsSidebar"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</body>
|
||||||
|
`);
|
||||||
|
|
||||||
|
expect(lastOS).toBe('iOS');
|
||||||
|
});
|
||||||
|
|
||||||
|
test('triggering a deeplink without applicable client can wait for a device', async () => {
|
||||||
|
const definition = TestUtils.createTestPlugin(
|
||||||
|
{
|
||||||
|
Component() {
|
||||||
|
return <p>Hello</p>;
|
||||||
|
},
|
||||||
|
plugin() {
|
||||||
|
return {};
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 'pluggy',
|
||||||
|
},
|
||||||
|
);
|
||||||
|
const {renderer, store, createClient, device} =
|
||||||
|
await renderMockFlipperWithPlugin(definition);
|
||||||
|
|
||||||
|
store.dispatch(
|
||||||
|
selectPlugin({selectedPlugin: 'nonexisting', deepLinkPayload: null}),
|
||||||
|
);
|
||||||
|
expect(renderer.baseElement).toMatchInlineSnapshot(`
|
||||||
|
<body>
|
||||||
|
<div />
|
||||||
|
</body>
|
||||||
|
`);
|
||||||
|
|
||||||
|
const handlePromise = handleDeeplink(
|
||||||
|
store,
|
||||||
|
`flipper://open-plugin?plugin-id=${definition.id}&client=clienty`,
|
||||||
|
);
|
||||||
|
|
||||||
|
jest.runAllTimers();
|
||||||
|
|
||||||
|
// No device yet available (dialogs are not renderable atm)
|
||||||
|
expect(renderer.baseElement).toMatchInlineSnapshot(`
|
||||||
|
<body>
|
||||||
|
<div />
|
||||||
|
</body>
|
||||||
|
`);
|
||||||
|
|
||||||
|
// create a new client
|
||||||
|
createClient(device, 'clienty');
|
||||||
|
|
||||||
|
// wizard should continue automatically
|
||||||
|
await handlePromise;
|
||||||
|
expect(renderer.baseElement).toMatchInlineSnapshot(`
|
||||||
|
<body>
|
||||||
|
<div>
|
||||||
|
<div
|
||||||
|
class="css-1x2cmzz-SandySplitContainer e1hsqii10"
|
||||||
|
>
|
||||||
|
<div />
|
||||||
|
<div
|
||||||
|
class="css-1knrt0j-SandySplitContainer e1hsqii10"
|
||||||
|
>
|
||||||
|
<div
|
||||||
|
class="css-1woty6b-Container"
|
||||||
|
>
|
||||||
|
<p>
|
||||||
|
Hello
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
<div
|
||||||
|
class="css-724x97-View-FlexBox-FlexRow"
|
||||||
|
id="detailsSidebar"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div />
|
||||||
|
</body>
|
||||||
|
`);
|
||||||
|
});
|
||||||
|
|||||||
@@ -15,15 +15,20 @@ import {checkForUpdate} from '../fb-stubs/checkForUpdate';
|
|||||||
import {getAppVersion} from '../utils/info';
|
import {getAppVersion} from '../utils/info';
|
||||||
import {ACTIVE_SHEET_SIGN_IN, setActiveSheet} from '../reducers/application';
|
import {ACTIVE_SHEET_SIGN_IN, setActiveSheet} from '../reducers/application';
|
||||||
import {UserNotSignedInError} from '../utils/errors';
|
import {UserNotSignedInError} from '../utils/errors';
|
||||||
import {selectPlugin} from '../reducers/connections';
|
import {selectPlugin, setPluginEnabled} from '../reducers/connections';
|
||||||
import {getUpdateAvailableMessage} from '../chrome/UpdateIndicator';
|
import {getUpdateAvailableMessage} from '../chrome/UpdateIndicator';
|
||||||
import {Typography} from 'antd';
|
import {Typography} from 'antd';
|
||||||
import {getPluginStatus} from '../utils/pluginUtils';
|
import {getPluginStatus} from '../utils/pluginUtils';
|
||||||
import {loadPluginsFromMarketplace} from './fb-stubs/pluginMarketplace';
|
import {loadPluginsFromMarketplace} from './fb-stubs/pluginMarketplace';
|
||||||
import {loadPlugin} from '../reducers/pluginManager';
|
import {loadPlugin, switchPlugin} from '../reducers/pluginManager';
|
||||||
import {startPluginDownload} from '../reducers/pluginDownloads';
|
import {startPluginDownload} from '../reducers/pluginDownloads';
|
||||||
import isProduction, {isTest} from '../utils/isProduction';
|
import isProduction, {isTest} from '../utils/isProduction';
|
||||||
import restart from '../utils/restartFlipper';
|
import restart from '../utils/restartFlipper';
|
||||||
|
import BaseDevice from '../server/devices/BaseDevice';
|
||||||
|
import Client from '../Client';
|
||||||
|
import {Button} from 'antd';
|
||||||
|
import {RocketOutlined} from '@ant-design/icons';
|
||||||
|
import {showEmulatorLauncher} from '../sandy-chrome/appinspect/LaunchEmulator';
|
||||||
|
|
||||||
type OpenPluginParams = {
|
type OpenPluginParams = {
|
||||||
pluginId: string;
|
pluginId: string;
|
||||||
@@ -51,7 +56,7 @@ export function parseOpenPluginParams(query: string): OpenPluginParams {
|
|||||||
|
|
||||||
export async function handleOpenPluginDeeplink(store: Store, query: string) {
|
export async function handleOpenPluginDeeplink(store: Store, query: string) {
|
||||||
const params = parseOpenPluginParams(query);
|
const params = parseOpenPluginParams(query);
|
||||||
const title = `Starting plugin ${params.pluginId}…`;
|
const title = `Opening plugin ${params.pluginId}…`;
|
||||||
|
|
||||||
if (!(await verifyLighthouseAndUserLoggedIn(store, title))) {
|
if (!(await verifyLighthouseAndUserLoggedIn(store, title))) {
|
||||||
return;
|
return;
|
||||||
@@ -60,10 +65,78 @@ export async function handleOpenPluginDeeplink(store: Store, query: string) {
|
|||||||
if (!(await verifyPluginStatus(store, params.pluginId, title))) {
|
if (!(await verifyPluginStatus(store, params.pluginId, title))) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
// await verifyDevices();
|
|
||||||
// await verifyClient();
|
const isDevicePlugin = store
|
||||||
// await verifyPluginEnabled();
|
.getState()
|
||||||
await openPlugin(store, params);
|
.plugins.devicePlugins.has(params.pluginId);
|
||||||
|
const pluginDefinition = isDevicePlugin
|
||||||
|
? store.getState().plugins.devicePlugins.get(params.pluginId)!
|
||||||
|
: store.getState().plugins.clientPlugins.get(params.pluginId)!;
|
||||||
|
const deviceOrClient = await selectDevicesAndClient(
|
||||||
|
store,
|
||||||
|
params,
|
||||||
|
title,
|
||||||
|
isDevicePlugin,
|
||||||
|
);
|
||||||
|
if (deviceOrClient === false) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const client: Client | undefined = isDevicePlugin
|
||||||
|
? undefined
|
||||||
|
: (deviceOrClient as Client);
|
||||||
|
const device: BaseDevice = isDevicePlugin
|
||||||
|
? (deviceOrClient as BaseDevice)
|
||||||
|
: (deviceOrClient as Client).deviceSync;
|
||||||
|
|
||||||
|
// verify plugin supported by selected device / client
|
||||||
|
if (isDevicePlugin && !device.supportsPlugin(pluginDefinition)) {
|
||||||
|
await Dialog.alert({
|
||||||
|
title,
|
||||||
|
type: 'error',
|
||||||
|
message: `This plugin is not supported by device ${device.displayTitle()}`,
|
||||||
|
});
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (!isDevicePlugin && !client!.plugins.has(params.pluginId)) {
|
||||||
|
await Dialog.alert({
|
||||||
|
title,
|
||||||
|
type: 'error',
|
||||||
|
message: `This plugin is not supported by client ${client!.query.app}`,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// verify plugin enabled
|
||||||
|
if (isDevicePlugin) {
|
||||||
|
// for the device plugins enabling is a bit more complication and should go through the pluginManager
|
||||||
|
if (
|
||||||
|
!store.getState().connections.enabledDevicePlugins.has(params.pluginId)
|
||||||
|
) {
|
||||||
|
store.dispatch(switchPlugin({plugin: pluginDefinition}));
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
store.dispatch(setPluginEnabled(params.pluginId, client!.query.app));
|
||||||
|
}
|
||||||
|
|
||||||
|
// open the plugin
|
||||||
|
if (isDevicePlugin) {
|
||||||
|
store.dispatch(
|
||||||
|
selectPlugin({
|
||||||
|
selectedPlugin: params.pluginId,
|
||||||
|
selectedApp: null,
|
||||||
|
selectedDevice: device,
|
||||||
|
deepLinkPayload: params.payload,
|
||||||
|
}),
|
||||||
|
);
|
||||||
|
} else {
|
||||||
|
store.dispatch(
|
||||||
|
selectPlugin({
|
||||||
|
selectedPlugin: params.pluginId,
|
||||||
|
selectedApp: client!.query.app,
|
||||||
|
selectedDevice: device,
|
||||||
|
deepLinkPayload: params.payload,
|
||||||
|
}),
|
||||||
|
);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// check if user is connected to VPN and logged in. Returns true if OK, or false if aborted
|
// check if user is connected to VPN and logged in. Returns true if OK, or false if aborted
|
||||||
@@ -325,12 +398,169 @@ async function installMarketPlacePlugin(
|
|||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
function openPlugin(store: Store, params: OpenPluginParams) {
|
async function selectDevicesAndClient(
|
||||||
store.dispatch(
|
store: Store,
|
||||||
selectPlugin({
|
params: OpenPluginParams,
|
||||||
selectedApp: params.client,
|
title: string,
|
||||||
selectedPlugin: params.pluginId,
|
isDevicePlugin: boolean,
|
||||||
deepLinkPayload: params.payload,
|
): Promise<false | BaseDevice | Client> {
|
||||||
}),
|
function findValidDevices() {
|
||||||
);
|
// find connected devices with the right OS.
|
||||||
|
return store
|
||||||
|
.getState()
|
||||||
|
.connections.devices.filter((d) => d.connected.get())
|
||||||
|
.filter(
|
||||||
|
(d) => params.devices.length === 0 || params.devices.includes(d.os),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
// loop until we have devices (or abort)
|
||||||
|
while (!findValidDevices().length) {
|
||||||
|
if (!(await launchDeviceDialog(store, params, title))) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// at this point we have 1 or more valid devices
|
||||||
|
const availableDevices = findValidDevices();
|
||||||
|
// device plugin
|
||||||
|
if (isDevicePlugin) {
|
||||||
|
if (availableDevices.length === 1) {
|
||||||
|
return availableDevices[0];
|
||||||
|
}
|
||||||
|
return (await selectDeviceDialog(availableDevices, title)) ?? false;
|
||||||
|
}
|
||||||
|
|
||||||
|
// wait for valid client
|
||||||
|
while (true) {
|
||||||
|
const validClients = store
|
||||||
|
.getState()
|
||||||
|
.connections.clients.filter(
|
||||||
|
// correct app name, or, if not set, an app that at least supports this plugin
|
||||||
|
(c) =>
|
||||||
|
params.client
|
||||||
|
? c.query.app === params.client
|
||||||
|
: c.plugins.has(params.pluginId),
|
||||||
|
)
|
||||||
|
.filter((c) => c.connected.get())
|
||||||
|
.filter((c) => availableDevices.includes(c.deviceSync));
|
||||||
|
|
||||||
|
if (validClients.length === 1) {
|
||||||
|
return validClients[0];
|
||||||
|
}
|
||||||
|
if (validClients.length > 1) {
|
||||||
|
return (await selectClientDialog(validClients, title)) ?? false;
|
||||||
|
}
|
||||||
|
|
||||||
|
// no valid client yet
|
||||||
|
const result = await new Promise<boolean>((resolve) => {
|
||||||
|
const dialog = Dialog.alert({
|
||||||
|
title,
|
||||||
|
type: 'warning',
|
||||||
|
message: params.client
|
||||||
|
? `Application '${params.client}' doesn't seem to be connected yet. Please start a debug version of the app to continue.`
|
||||||
|
: `No application that supports plugin '${params.pluginId}' seems to be running. Please start a debug application that supports the plugin to continue.`,
|
||||||
|
okText: 'Cancel',
|
||||||
|
});
|
||||||
|
// eslint-disable-next-line promise/catch-or-return
|
||||||
|
dialog.then(() => resolve(false));
|
||||||
|
|
||||||
|
const origClients = store.getState().connections.clients;
|
||||||
|
// eslint-disable-next-line promise/catch-or-return
|
||||||
|
waitFor(store, (state) => state.connections.clients !== origClients).then(
|
||||||
|
() => {
|
||||||
|
dialog.close();
|
||||||
|
resolve(true);
|
||||||
|
},
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
if (!result) {
|
||||||
|
return false; // User cancelled
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Shows a warning that no device was found, with button to launch emulator.
|
||||||
|
* Resolves false if cancelled, or true if new devices were detected.
|
||||||
|
*/
|
||||||
|
async function launchDeviceDialog(
|
||||||
|
store: Store,
|
||||||
|
params: OpenPluginParams,
|
||||||
|
title: string,
|
||||||
|
) {
|
||||||
|
return new Promise<boolean>((resolve) => {
|
||||||
|
const dialog = Dialog.alert({
|
||||||
|
title,
|
||||||
|
type: 'warning',
|
||||||
|
message: (
|
||||||
|
<p>
|
||||||
|
To open the current deeplink for plugin {params.pluginId} a device{' '}
|
||||||
|
{params.devices.length ? ' of type ' + params.devices.join(', ') : ''}{' '}
|
||||||
|
should be up and running. No device was found. Please connect a device
|
||||||
|
or launch an emulator / simulator{' '}
|
||||||
|
<Button
|
||||||
|
icon={<RocketOutlined />}
|
||||||
|
title="Start Emulator / Simulator"
|
||||||
|
onClick={() => {
|
||||||
|
showEmulatorLauncher(store);
|
||||||
|
}}
|
||||||
|
size="small"
|
||||||
|
/>
|
||||||
|
.
|
||||||
|
</p>
|
||||||
|
),
|
||||||
|
okText: 'Cancel',
|
||||||
|
});
|
||||||
|
// eslint-disable-next-line promise/catch-or-return
|
||||||
|
dialog.then(() => {
|
||||||
|
// dialog was canceled
|
||||||
|
resolve(false);
|
||||||
|
});
|
||||||
|
|
||||||
|
const currentDevices = store.getState().connections.devices;
|
||||||
|
// new devices were found
|
||||||
|
// eslint-disable-next-line promise/catch-or-return
|
||||||
|
waitFor(
|
||||||
|
store,
|
||||||
|
(state) => state.connections.devices !== currentDevices,
|
||||||
|
).then(() => {
|
||||||
|
dialog.close();
|
||||||
|
resolve(true);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
async function selectDeviceDialog(
|
||||||
|
devices: BaseDevice[],
|
||||||
|
title: string,
|
||||||
|
): Promise<undefined | BaseDevice> {
|
||||||
|
const selectedId = await Dialog.options({
|
||||||
|
title,
|
||||||
|
message: 'Select the device to open:',
|
||||||
|
options: devices.map((d) => ({
|
||||||
|
value: d.serial,
|
||||||
|
label: d.displayTitle(),
|
||||||
|
})),
|
||||||
|
});
|
||||||
|
// might find nothing if id === false
|
||||||
|
return devices.find((d) => d.serial === selectedId);
|
||||||
|
}
|
||||||
|
|
||||||
|
async function selectClientDialog(
|
||||||
|
clients: Client[],
|
||||||
|
title: string,
|
||||||
|
): Promise<undefined | Client> {
|
||||||
|
const selectedId = await Dialog.options({
|
||||||
|
title,
|
||||||
|
message:
|
||||||
|
'Multiple applications running this plugin were found, please select one:',
|
||||||
|
options: clients.map((c) => ({
|
||||||
|
value: c.id,
|
||||||
|
label: `${c.query.app} on ${c.deviceSync.displayTitle()}`,
|
||||||
|
})),
|
||||||
|
});
|
||||||
|
// might find nothing if id === false
|
||||||
|
return clients.find((c) => c.id === selectedId);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -8,7 +8,7 @@
|
|||||||
*/
|
*/
|
||||||
|
|
||||||
import {createStore} from 'redux';
|
import {createStore} from 'redux';
|
||||||
import BaseDevice from '../server/devices/BaseDevice';
|
import BaseDevice, {OS} from '../server/devices/BaseDevice';
|
||||||
import {createRootReducer} from '../reducers';
|
import {createRootReducer} from '../reducers';
|
||||||
import {Store} from '../reducers/index';
|
import {Store} from '../reducers/index';
|
||||||
import Client, {ClientQuery} from '../Client';
|
import Client, {ClientQuery} from '../Client';
|
||||||
@@ -43,6 +43,7 @@ export interface DeviceOptions {
|
|||||||
serial?: string;
|
serial?: string;
|
||||||
isSupportedByPlugin?: (p: PluginDetails) => boolean;
|
isSupportedByPlugin?: (p: PluginDetails) => boolean;
|
||||||
archived?: boolean;
|
archived?: boolean;
|
||||||
|
os?: OS;
|
||||||
}
|
}
|
||||||
|
|
||||||
export default class MockFlipper {
|
export default class MockFlipper {
|
||||||
@@ -117,6 +118,7 @@ export default class MockFlipper {
|
|||||||
serial,
|
serial,
|
||||||
isSupportedByPlugin,
|
isSupportedByPlugin,
|
||||||
archived,
|
archived,
|
||||||
|
os,
|
||||||
}: DeviceOptions = {}): BaseDevice {
|
}: DeviceOptions = {}): BaseDevice {
|
||||||
const s = serial ?? `serial_${++this._deviceCounter}`;
|
const s = serial ?? `serial_${++this._deviceCounter}`;
|
||||||
const device = archived
|
const device = archived
|
||||||
@@ -126,7 +128,7 @@ export default class MockFlipper {
|
|||||||
title: 'archived device',
|
title: 'archived device',
|
||||||
os: 'Android',
|
os: 'Android',
|
||||||
})
|
})
|
||||||
: new BaseDevice(s, 'physical', 'MockAndroidDevice', 'Android');
|
: new BaseDevice(s, 'physical', 'MockAndroidDevice', os ?? 'Android');
|
||||||
device.supportsPlugin = !isSupportedByPlugin
|
device.supportsPlugin = !isSupportedByPlugin
|
||||||
? () => true
|
? () => true
|
||||||
: isSupportedByPlugin;
|
: isSupportedByPlugin;
|
||||||
|
|||||||
@@ -45,7 +45,7 @@ export type MockFlipperResult = {
|
|||||||
pluginKey: string;
|
pluginKey: string;
|
||||||
sendError(error: any, client?: Client): void;
|
sendError(error: any, client?: Client): void;
|
||||||
sendMessage(method: string, params: any, client?: Client): void;
|
sendMessage(method: string, params: any, client?: Client): void;
|
||||||
createDevice(serial: string): BaseDevice;
|
createDevice(options: Parameters<MockFlipper['createDevice']>[0]): BaseDevice;
|
||||||
createClient(
|
createClient(
|
||||||
device: BaseDevice,
|
device: BaseDevice,
|
||||||
name: string,
|
name: string,
|
||||||
@@ -127,8 +127,8 @@ export async function createMockFlipperWithPlugin(
|
|||||||
const logger = mockFlipper.logger;
|
const logger = mockFlipper.logger;
|
||||||
const store = mockFlipper.store;
|
const store = mockFlipper.store;
|
||||||
|
|
||||||
const createDevice = (serial: string, archived?: boolean) =>
|
const createDevice = (options: Parameters<MockFlipper['createDevice']>[0]) =>
|
||||||
mockFlipper.createDevice({serial, archived});
|
mockFlipper.createDevice(options);
|
||||||
const createClient = async (
|
const createClient = async (
|
||||||
device: BaseDevice,
|
device: BaseDevice,
|
||||||
name: string,
|
name: string,
|
||||||
@@ -171,7 +171,7 @@ export async function createMockFlipperWithPlugin(
|
|||||||
|
|
||||||
const device = options?.device
|
const device = options?.device
|
||||||
? mockFlipper.loadDevice(options?.device)
|
? mockFlipper.loadDevice(options?.device)
|
||||||
: createDevice('serial', options?.archivedDevice);
|
: createDevice({serial: 'serial', archived: options?.archivedDevice});
|
||||||
const client = await createClient(device, 'TestApp');
|
const client = await createClient(device, 'TestApp');
|
||||||
|
|
||||||
store.dispatch(selectDevice(device));
|
store.dispatch(selectDevice(device));
|
||||||
|
|||||||
@@ -295,7 +295,7 @@ test('queue - events are queued for plugins that are favorite when app is select
|
|||||||
selectDeviceLogs(store);
|
selectDeviceLogs(store);
|
||||||
expect(store.getState().connections.selectedPlugin).not.toBe('TestPlugin');
|
expect(store.getState().connections.selectedPlugin).not.toBe('TestPlugin');
|
||||||
|
|
||||||
const device2 = createDevice('serial2');
|
const device2 = createDevice({serial: 'serial2'});
|
||||||
const client2 = await createClient(device2, client.query.app); // same app id
|
const client2 = await createClient(device2, client.query.app); // same app id
|
||||||
store.dispatch(selectDevice(device2));
|
store.dispatch(selectDevice(device2));
|
||||||
store.dispatch(selectClient(client2.id));
|
store.dispatch(selectClient(client2.id));
|
||||||
|
|||||||
@@ -72,7 +72,7 @@ export const Dialog = {
|
|||||||
}}
|
}}
|
||||||
okButtonProps={{
|
okButtonProps={{
|
||||||
disabled: opts.onValidate
|
disabled: opts.onValidate
|
||||||
? opts.onValidate(currentValue) !== ''
|
? !!opts.onValidate(currentValue) // non-falsy value means validation error
|
||||||
: false,
|
: false,
|
||||||
}}
|
}}
|
||||||
onCancel={cancel}
|
onCancel={cancel}
|
||||||
@@ -175,11 +175,12 @@ export const Dialog = {
|
|||||||
message: React.ReactNode;
|
message: React.ReactNode;
|
||||||
options: {label: string; value: string}[];
|
options: {label: string; value: string}[];
|
||||||
onConfirm?: (value: string) => Promise<string>;
|
onConfirm?: (value: string) => Promise<string>;
|
||||||
}): DialogResult<string> {
|
}): DialogResult<string | false> {
|
||||||
return Dialog.show<string>({
|
return Dialog.show<string>({
|
||||||
...rest,
|
...rest,
|
||||||
defaultValue: '',
|
defaultValue: undefined as any,
|
||||||
onValidate: (value) => (value === '' ? 'Please select an option' : ''),
|
onValidate: (value) =>
|
||||||
|
value === undefined ? 'Please select an option' : '',
|
||||||
children: (value, onChange) => (
|
children: (value, onChange) => (
|
||||||
<Layout.Container gap style={{maxHeight: '50vh', overflow: 'auto'}}>
|
<Layout.Container gap style={{maxHeight: '50vh', overflow: 'auto'}}>
|
||||||
<Typography.Text>{message}</Typography.Text>
|
<Typography.Text>{message}</Typography.Text>
|
||||||
|
|||||||
@@ -17,6 +17,7 @@ import {render, unmountComponentAtNode} from 'react-dom';
|
|||||||
export function renderReactRoot(
|
export function renderReactRoot(
|
||||||
handler: (unmount: () => void) => React.ReactElement,
|
handler: (unmount: () => void) => React.ReactElement,
|
||||||
): () => void {
|
): () => void {
|
||||||
|
// TODO: find a way to make this visible in unit tests as well
|
||||||
const div = document.body.appendChild(document.createElement('div'));
|
const div = document.body.appendChild(document.createElement('div'));
|
||||||
const unmount = () => {
|
const unmount = () => {
|
||||||
unmountComponentAtNode(div);
|
unmountComponentAtNode(div);
|
||||||
|
|||||||
Reference in New Issue
Block a user