Make DataTable / DataInspector unit tests predictable
Summary: Having time / async / non-blocking behavior in components in unit tests is really annoying, as it makes unit tests async without an easy way to determine 'done'. This diff makes sure that DataTable & DataInspector don't break down their work in smaller tasks, but do everything block if they are running in a unit test. Reviewed By: nikoant Differential Revision: D28054487 fbshipit-source-id: 72c3b519e092ad69ed71eb1731e1fed80022f91f
This commit is contained in:
committed by
Facebook GitHub Bot
parent
39be769bab
commit
bbcb16d8fb
@@ -26,6 +26,7 @@ import {useHighlighter, HighlightManager} from '../Highlight';
|
|||||||
import {Dropdown, Menu, Tooltip} from 'antd';
|
import {Dropdown, Menu, Tooltip} from 'antd';
|
||||||
import {tryGetFlipperLibImplementation} from '../../plugin/FlipperLib';
|
import {tryGetFlipperLibImplementation} from '../../plugin/FlipperLib';
|
||||||
import {safeStringify} from '../../utils/safeStringify';
|
import {safeStringify} from '../../utils/safeStringify';
|
||||||
|
import {useInUnitTest} from '../../utils/useInUnitTest()';
|
||||||
|
|
||||||
export {DataValueExtractor} from './DataPreview';
|
export {DataValueExtractor} from './DataPreview';
|
||||||
|
|
||||||
@@ -300,6 +301,7 @@ export const DataInspectorNode: React.FC<DataInspectorProps> = memo(
|
|||||||
}) {
|
}) {
|
||||||
const highlighter = useHighlighter();
|
const highlighter = useHighlighter();
|
||||||
const getRoot = useContext(RootDataContext);
|
const getRoot = useContext(RootDataContext);
|
||||||
|
const isUnitTest = useInUnitTest();
|
||||||
|
|
||||||
const shouldExpand = useRef(false);
|
const shouldExpand = useRef(false);
|
||||||
const expandHandle = useRef(undefined as any);
|
const expandHandle = useRef(undefined as any);
|
||||||
@@ -366,14 +368,20 @@ export const DataInspectorNode: React.FC<DataInspectorProps> = memo(
|
|||||||
if (!shouldExpand.current) {
|
if (!shouldExpand.current) {
|
||||||
setRenderExpanded(false);
|
setRenderExpanded(false);
|
||||||
} else {
|
} else {
|
||||||
expandHandle.current = requestIdleCallback(() => {
|
if (isUnitTest) {
|
||||||
setRenderExpanded(true);
|
setRenderExpanded(true);
|
||||||
});
|
} else {
|
||||||
|
expandHandle.current = requestIdleCallback(() => {
|
||||||
|
setRenderExpanded(true);
|
||||||
|
});
|
||||||
|
}
|
||||||
}
|
}
|
||||||
return () => {
|
return () => {
|
||||||
cancelIdleCallback(expandHandle.current);
|
if (!isUnitTest) {
|
||||||
|
cancelIdleCallback(expandHandle.current);
|
||||||
|
}
|
||||||
};
|
};
|
||||||
}, [shouldExpand.current]);
|
}, [shouldExpand.current, isUnitTest]);
|
||||||
|
|
||||||
const setExpanded = useCallback(
|
const setExpanded = useCallback(
|
||||||
(pathParts: Array<string>, isExpanded: boolean) => {
|
(pathParts: Array<string>, isExpanded: boolean) => {
|
||||||
@@ -387,10 +395,12 @@ export const DataInspectorNode: React.FC<DataInspectorProps> = memo(
|
|||||||
);
|
);
|
||||||
|
|
||||||
const handleClick = useCallback(() => {
|
const handleClick = useCallback(() => {
|
||||||
cancelIdleCallback(expandHandle.current);
|
if (!isUnitTest) {
|
||||||
|
cancelIdleCallback(expandHandle.current);
|
||||||
|
}
|
||||||
const isExpanded = shouldBeExpanded(expandedPaths, path, collapsed);
|
const isExpanded = shouldBeExpanded(expandedPaths, path, collapsed);
|
||||||
setExpanded(path, !isExpanded);
|
setExpanded(path, !isExpanded);
|
||||||
}, [expandedPaths, path, collapsed]);
|
}, [expandedPaths, path, collapsed, isUnitTest]);
|
||||||
|
|
||||||
const handleDelete = useCallback(
|
const handleDelete = useCallback(
|
||||||
(path: Array<string>) => {
|
(path: Array<string>) => {
|
||||||
|
|||||||
@@ -13,35 +13,6 @@ import {render, fireEvent, waitFor, act} from '@testing-library/react';
|
|||||||
import {DataInspector} from '../DataInspector';
|
import {DataInspector} from '../DataInspector';
|
||||||
import {sleep} from '../../../utils/sleep';
|
import {sleep} from '../../../utils/sleep';
|
||||||
|
|
||||||
const mocks = {
|
|
||||||
requestIdleCallback(fn: Function) {
|
|
||||||
return setTimeout(fn, 1);
|
|
||||||
},
|
|
||||||
cancelIdleCallback(handle: any) {
|
|
||||||
clearTimeout(handle);
|
|
||||||
},
|
|
||||||
};
|
|
||||||
|
|
||||||
beforeAll(() => {
|
|
||||||
Object.keys(mocks).forEach((key) => {
|
|
||||||
// @ts-ignore
|
|
||||||
if (!global[key]) {
|
|
||||||
// @ts-ignore
|
|
||||||
global[key] = mocks[key];
|
|
||||||
}
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
afterAll(() => {
|
|
||||||
Object.keys(mocks).forEach((key) => {
|
|
||||||
// @ts-ignore
|
|
||||||
if (global[key] === mocks[key]) {
|
|
||||||
// @ts-ignore
|
|
||||||
delete global[key];
|
|
||||||
}
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
const json = {
|
const json = {
|
||||||
data: {
|
data: {
|
||||||
is: {
|
is: {
|
||||||
|
|||||||
@@ -22,6 +22,7 @@ import {DataSource} from '../../state/DataSource';
|
|||||||
import {useVirtual} from 'react-virtual';
|
import {useVirtual} from 'react-virtual';
|
||||||
import styled from '@emotion/styled';
|
import styled from '@emotion/styled';
|
||||||
import observeRect from '@reach/observe-rect';
|
import observeRect from '@reach/observe-rect';
|
||||||
|
import {useInUnitTest} from '../../utils/useInUnitTest()';
|
||||||
|
|
||||||
// how fast we update if updates are low-prio (e.g. out of window and not super significant)
|
// how fast we update if updates are low-prio (e.g. out of window and not super significant)
|
||||||
const LOW_PRIO_UPDATE = 1000; //ms
|
const LOW_PRIO_UPDATE = 1000; //ms
|
||||||
@@ -89,7 +90,6 @@ export const DataSourceRenderer: <T extends object, C>(
|
|||||||
onRangeChange,
|
onRangeChange,
|
||||||
onUpdateAutoScroll,
|
onUpdateAutoScroll,
|
||||||
emptyRenderer,
|
emptyRenderer,
|
||||||
_testHeight,
|
|
||||||
}: DataSourceProps<any, any>) {
|
}: DataSourceProps<any, any>) {
|
||||||
/**
|
/**
|
||||||
* Virtualization
|
* Virtualization
|
||||||
@@ -100,13 +100,12 @@ export const DataSourceRenderer: <T extends object, C>(
|
|||||||
const [, setForceUpdate] = useState(0);
|
const [, setForceUpdate] = useState(0);
|
||||||
const forceHeightRecalculation = useRef(0);
|
const forceHeightRecalculation = useRef(0);
|
||||||
const parentRef = React.useRef<null | HTMLDivElement>(null);
|
const parentRef = React.useRef<null | HTMLDivElement>(null);
|
||||||
|
const isUnitTest = useInUnitTest();
|
||||||
|
|
||||||
const virtualizer = useVirtual({
|
const virtualizer = useVirtual({
|
||||||
size: dataSource.view.size,
|
size: dataSource.view.size,
|
||||||
parentRef,
|
parentRef,
|
||||||
useObserver: _testHeight
|
useObserver: isUnitTest ? () => ({height: 500, width: 1000}) : undefined,
|
||||||
? () => ({height: _testHeight, width: 1000})
|
|
||||||
: undefined,
|
|
||||||
// eslint-disable-next-line
|
// eslint-disable-next-line
|
||||||
estimateSize: useCallback(() => defaultRowHeight, [forceHeightRecalculation.current, defaultRowHeight]),
|
estimateSize: useCallback(() => defaultRowHeight, [forceHeightRecalculation.current, defaultRowHeight]),
|
||||||
overscan: 0,
|
overscan: 0,
|
||||||
@@ -138,7 +137,7 @@ export const DataSourceRenderer: <T extends object, C>(
|
|||||||
// the height of some existing rows might have changed
|
// the height of some existing rows might have changed
|
||||||
forceHeightRecalculation.current++;
|
forceHeightRecalculation.current++;
|
||||||
}
|
}
|
||||||
if (_testHeight) {
|
if (isUnitTest) {
|
||||||
// test environment, update immediately
|
// test environment, update immediately
|
||||||
forceUpdate();
|
forceUpdate();
|
||||||
return;
|
return;
|
||||||
@@ -202,7 +201,7 @@ export const DataSourceRenderer: <T extends object, C>(
|
|||||||
dataSource.view.setListener(undefined);
|
dataSource.view.setListener(undefined);
|
||||||
};
|
};
|
||||||
},
|
},
|
||||||
[dataSource, setForceUpdate, useFixedRowHeight, _testHeight],
|
[dataSource, setForceUpdate, useFixedRowHeight, isUnitTest],
|
||||||
);
|
);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
|
|||||||
@@ -47,6 +47,7 @@ import {Formatter} from '../DataFormatter';
|
|||||||
import {usePluginInstance} from '../../plugin/PluginContext';
|
import {usePluginInstance} from '../../plugin/PluginContext';
|
||||||
import {debounce} from 'lodash';
|
import {debounce} from 'lodash';
|
||||||
import {StaticDataSourceRenderer} from './StaticDataSourceRenderer';
|
import {StaticDataSourceRenderer} from './StaticDataSourceRenderer';
|
||||||
|
import {useInUnitTest} from '../../utils/useInUnitTest()';
|
||||||
|
|
||||||
interface DataTableProps<T = any> {
|
interface DataTableProps<T = any> {
|
||||||
columns: DataTableColumn<T>[];
|
columns: DataTableColumn<T>[];
|
||||||
@@ -57,7 +58,6 @@ interface DataTableProps<T = any> {
|
|||||||
onRowStyle?(record: T): CSSProperties | undefined;
|
onRowStyle?(record: T): CSSProperties | undefined;
|
||||||
// multiselect?: true
|
// multiselect?: true
|
||||||
tableManagerRef?: RefObject<DataTableManager<T> | undefined>; // Actually we want a MutableRefObject, but that is not what React.createRef() returns, and we don't want to put the burden on the plugin dev to cast it...
|
tableManagerRef?: RefObject<DataTableManager<T> | undefined>; // Actually we want a MutableRefObject, but that is not what React.createRef() returns, and we don't want to put the burden on the plugin dev to cast it...
|
||||||
_testHeight?: number; // exposed for unit testing only
|
|
||||||
onCopyRows?(records: T[]): string;
|
onCopyRows?(records: T[]): string;
|
||||||
onContextMenu?: (selection: undefined | T) => React.ReactElement;
|
onContextMenu?: (selection: undefined | T) => React.ReactElement;
|
||||||
searchbar?: boolean;
|
searchbar?: boolean;
|
||||||
@@ -113,11 +113,10 @@ export function DataTable<T extends object>(
|
|||||||
useAssertStableRef(props.columns, 'columns');
|
useAssertStableRef(props.columns, 'columns');
|
||||||
useAssertStableRef(onCopyRows, 'onCopyRows');
|
useAssertStableRef(onCopyRows, 'onCopyRows');
|
||||||
useAssertStableRef(onContextMenu, 'onContextMenu');
|
useAssertStableRef(onContextMenu, 'onContextMenu');
|
||||||
useAssertStableRef(props._testHeight, '_testHeight');
|
const isUnitTest = useInUnitTest();
|
||||||
|
|
||||||
// lint disabled for conditional inclusion of a hook (_testHeight is asserted to be stable)
|
|
||||||
// eslint-disable-next-line
|
// eslint-disable-next-line
|
||||||
const scope = props._testHeight ? "" : usePluginInstance().pluginKey;
|
const scope = isUnitTest ? "" : usePluginInstance().pluginKey;
|
||||||
const virtualizerRef = useRef<DataSourceVirtualizer | undefined>();
|
const virtualizerRef = useRef<DataSourceVirtualizer | undefined>();
|
||||||
const [tableState, dispatch] = useReducer(
|
const [tableState, dispatch] = useReducer(
|
||||||
dataTableManagerReducer as DataTableReducer<T>,
|
dataTableManagerReducer as DataTableReducer<T>,
|
||||||
@@ -273,7 +272,7 @@ export function DataTable<T extends object>(
|
|||||||
computeDataTableFilter(search, useRegex, columns),
|
computeDataTableFilter(search, useRegex, columns),
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
return props._testHeight ? setFilter : debounce(setFilter, 250);
|
return isUnitTest ? setFilter : debounce(setFilter, 250);
|
||||||
});
|
});
|
||||||
useEffect(
|
useEffect(
|
||||||
function updateFilter() {
|
function updateFilter() {
|
||||||
@@ -360,7 +359,7 @@ export function DataTable<T extends object>(
|
|||||||
);
|
);
|
||||||
|
|
||||||
/** Context menu */
|
/** Context menu */
|
||||||
const contexMenu = props._testHeight
|
const contexMenu = isUnitTest
|
||||||
? undefined
|
? undefined
|
||||||
: // eslint-disable-next-line
|
: // eslint-disable-next-line
|
||||||
useCallback(
|
useCallback(
|
||||||
@@ -446,7 +445,6 @@ export function DataTable<T extends object>(
|
|||||||
onRangeChange={onRangeChange}
|
onRangeChange={onRangeChange}
|
||||||
onUpdateAutoScroll={onUpdateAutoScroll}
|
onUpdateAutoScroll={onUpdateAutoScroll}
|
||||||
emptyRenderer={emptyRenderer}
|
emptyRenderer={emptyRenderer}
|
||||||
_testHeight={props._testHeight}
|
|
||||||
/>
|
/>
|
||||||
</Layout.Top>
|
</Layout.Top>
|
||||||
) : (
|
) : (
|
||||||
@@ -479,7 +477,7 @@ export function DataTable<T extends object>(
|
|||||||
/>
|
/>
|
||||||
</AutoScroller>
|
</AutoScroller>
|
||||||
)}
|
)}
|
||||||
{range && <RangeFinder>{range}</RangeFinder>}
|
{range && !isUnitTest && <RangeFinder>{range}</RangeFinder>}
|
||||||
</Layout.Container>
|
</Layout.Container>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -10,7 +10,6 @@
|
|||||||
import React, {memo, useCallback, useEffect, useState} from 'react';
|
import React, {memo, useCallback, useEffect, useState} from 'react';
|
||||||
import {DataSource} from '../../state/DataSource';
|
import {DataSource} from '../../state/DataSource';
|
||||||
import {useVirtual} from 'react-virtual';
|
import {useVirtual} from 'react-virtual';
|
||||||
import styled from '@emotion/styled';
|
|
||||||
import {RedrawContext} from './DataSourceRenderer';
|
import {RedrawContext} from './DataSourceRenderer';
|
||||||
|
|
||||||
export type DataSourceVirtualizer = ReturnType<typeof useVirtual>;
|
export type DataSourceVirtualizer = ReturnType<typeof useVirtual>;
|
||||||
|
|||||||
@@ -43,12 +43,7 @@ test('update and append', async () => {
|
|||||||
const ds = createTestDataSource();
|
const ds = createTestDataSource();
|
||||||
const ref = createRef<DataTableManager<Todo>>();
|
const ref = createRef<DataTableManager<Todo>>();
|
||||||
const rendering = render(
|
const rendering = render(
|
||||||
<DataTable
|
<DataTable dataSource={ds} columns={columns} tableManagerRef={ref} />,
|
||||||
dataSource={ds}
|
|
||||||
columns={columns}
|
|
||||||
tableManagerRef={ref}
|
|
||||||
_testHeight={400}
|
|
||||||
/>,
|
|
||||||
);
|
);
|
||||||
{
|
{
|
||||||
const elem = await rendering.findAllByText('test DataTable');
|
const elem = await rendering.findAllByText('test DataTable');
|
||||||
@@ -100,12 +95,7 @@ test('column visibility', async () => {
|
|||||||
const ds = createTestDataSource();
|
const ds = createTestDataSource();
|
||||||
const ref = createRef<DataTableManager<Todo>>();
|
const ref = createRef<DataTableManager<Todo>>();
|
||||||
const rendering = render(
|
const rendering = render(
|
||||||
<DataTable
|
<DataTable dataSource={ds} columns={columns} tableManagerRef={ref} />,
|
||||||
dataSource={ds}
|
|
||||||
columns={columns}
|
|
||||||
tableManagerRef={ref}
|
|
||||||
_testHeight={400}
|
|
||||||
/>,
|
|
||||||
);
|
);
|
||||||
{
|
{
|
||||||
const elem = await rendering.findAllByText('test DataTable');
|
const elem = await rendering.findAllByText('test DataTable');
|
||||||
@@ -176,12 +166,7 @@ test('sorting', async () => {
|
|||||||
});
|
});
|
||||||
const ref = createRef<DataTableManager<Todo>>();
|
const ref = createRef<DataTableManager<Todo>>();
|
||||||
const rendering = render(
|
const rendering = render(
|
||||||
<DataTable
|
<DataTable dataSource={ds} columns={columns} tableManagerRef={ref} />,
|
||||||
dataSource={ds}
|
|
||||||
columns={columns}
|
|
||||||
tableManagerRef={ref}
|
|
||||||
_testHeight={400}
|
|
||||||
/>,
|
|
||||||
);
|
);
|
||||||
// insertion order
|
// insertion order
|
||||||
{
|
{
|
||||||
@@ -256,7 +241,6 @@ test('search', async () => {
|
|||||||
columns={columns}
|
columns={columns}
|
||||||
tableManagerRef={ref}
|
tableManagerRef={ref}
|
||||||
extraActions={<Button>Test Button</Button>}
|
extraActions={<Button>Test Button</Button>}
|
||||||
_testHeight={400}
|
|
||||||
/>,
|
/>,
|
||||||
);
|
);
|
||||||
{
|
{
|
||||||
@@ -540,7 +524,6 @@ test('onSelect callback fires, and in order', () => {
|
|||||||
dataSource={ds}
|
dataSource={ds}
|
||||||
columns={columns}
|
columns={columns}
|
||||||
tableManagerRef={ref}
|
tableManagerRef={ref}
|
||||||
_testHeight={400}
|
|
||||||
onSelect={(item, items) => {
|
onSelect={(item, items) => {
|
||||||
events.push([item, items]);
|
events.push([item, items]);
|
||||||
}}
|
}}
|
||||||
@@ -592,7 +575,6 @@ test('selection always has the latest state', () => {
|
|||||||
dataSource={ds}
|
dataSource={ds}
|
||||||
columns={columns}
|
columns={columns}
|
||||||
tableManagerRef={ref}
|
tableManagerRef={ref}
|
||||||
_testHeight={400}
|
|
||||||
onSelect={(item, items) => {
|
onSelect={(item, items) => {
|
||||||
events.push([item, items]);
|
events.push([item, items]);
|
||||||
}}
|
}}
|
||||||
|
|||||||
@@ -38,9 +38,7 @@ const columns: DataTableColumn[] = [
|
|||||||
|
|
||||||
test('update and append', async () => {
|
test('update and append', async () => {
|
||||||
let records = createTestRecords();
|
let records = createTestRecords();
|
||||||
const rendering = render(
|
const rendering = render(<DataTable records={records} columns={columns} />);
|
||||||
<DataTable records={records} columns={columns} _testHeight={400} />,
|
|
||||||
);
|
|
||||||
{
|
{
|
||||||
const elem = await rendering.findAllByText('test DataTable');
|
const elem = await rendering.findAllByText('test DataTable');
|
||||||
expect(elem.length).toBe(1);
|
expect(elem.length).toBe(1);
|
||||||
@@ -63,9 +61,7 @@ test('update and append', async () => {
|
|||||||
}
|
}
|
||||||
|
|
||||||
function rerender() {
|
function rerender() {
|
||||||
rendering.rerender(
|
rendering.rerender(<DataTable records={records} columns={columns} />);
|
||||||
<DataTable records={records} columns={columns} _testHeight={400} />,
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// append
|
// append
|
||||||
|
|||||||
16
desktop/flipper-plugin/src/utils/useInUnitTest().tsx
Normal file
16
desktop/flipper-plugin/src/utils/useInUnitTest().tsx
Normal file
@@ -0,0 +1,16 @@
|
|||||||
|
/**
|
||||||
|
* 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
|
||||||
|
*/
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Check if we are currently running a unit test.
|
||||||
|
* Use this hook to disable certain functionality that is probably not going to work as expected in the JSDom implementaiton
|
||||||
|
*/
|
||||||
|
export function useInUnitTest(): boolean {
|
||||||
|
return process.env.NODE_ENV === 'test';
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user