Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

test(xychart): add refined Tooltip, TooltipProvider tests #852

Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
119 changes: 104 additions & 15 deletions packages/visx-xychart/test/components/Tooltip.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -2,29 +2,42 @@ import React from 'react';
import ResizeObserver from 'resize-observer-polyfill';
import { mount } from 'enzyme';
import { Tooltip as BaseTooltip } from '@visx/tooltip';
import { Tooltip, TooltipContext, TooltipContextType } from '../../src';
import { DataContext, Tooltip, TooltipContext, TooltipContextType } from '../../src';
import { TooltipProps } from '../../src/components/Tooltip';
import getDataContext from '../mocks/getDataContext';

describe('<Tooltip />', () => {
type SetupProps =
| {
props?: Partial<TooltipProps<object>>;
context?: Partial<TooltipContextType<object>>;
Parent?: ({ children }: { children: React.ReactElement }) => React.ReactElement;
}
| undefined;
function setup({ props, context }: SetupProps = {}) {

function setup({
props,
context,
Parent = ({ children }: { children: React.ReactElement }) => children,
}: SetupProps = {}) {
const wrapper = mount(
<TooltipContext.Provider
value={{
tooltipOpen: false,
showTooltip: jest.fn(),
updateTooltip: jest.fn(),
hideTooltip: jest.fn(),
...context,
}}
>
<Tooltip resizeObserverPolyfill={ResizeObserver} renderTooltip={() => null} {...props} />
</TooltipContext.Provider>,
<Parent>
<TooltipContext.Provider
value={{
tooltipOpen: false,
showTooltip: jest.fn(),
updateTooltip: jest.fn(),
hideTooltip: jest.fn(),
...context,
}}
>
<Tooltip
resizeObserverPolyfill={ResizeObserver}
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

What happens if the polyfill is not passed?

Copy link
Collaborator Author

@williaster williaster Oct 7, 2020

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

if it's not passed and it's not available globally on window, it will throw (this logic lives in react-use-measure). Throwing may seem extreme but I think it's the cleanest way to polyfill, we import it globally in @visx/responsive's ParentSize and have gotten negative feedback about it.

I think we just need to be clear about it in the docs like in @visx/tooltip + react-use-measure.

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

ic. a bit more gear towards power users i guess.
If doc emphasizes it enough probably ok.
Perhaps can make it a bit more convenient later to do this once instead of passing to every Tooltip instance.

renderTooltip={() => <div />}
{...props}
/>
</TooltipContext.Provider>
</Parent>,
);
return wrapper;
}
Expand All @@ -38,13 +51,13 @@ describe('<Tooltip />', () => {
});

it('should not render a BaseTooltip when TooltipContext.tooltipOpen=true and renderTooltip returns false', () => {
const wrapper = setup({ context: { tooltipOpen: true } });
const wrapper = setup({ context: { tooltipOpen: true }, props: { renderTooltip: () => null } });
expect(wrapper.find(BaseTooltip)).toHaveLength(0);
});

it('should render a BaseTooltip when TooltipContext.tooltipOpen=true and renderTooltip returns non-null', () => {
const wrapper = setup({
props: { renderTooltip: () => <div /> },
props: { renderTooltip: () => <div />, snapTooltipToDatumX: true },
context: { tooltipOpen: true },
});
expect(wrapper.find(BaseTooltip)).toHaveLength(1);
Expand All @@ -66,4 +79,80 @@ describe('<Tooltip />', () => {
});
expect(renderTooltip).toHaveBeenCalledTimes(1);
});

it('should render a vertical crosshair if showVerticalCrossHair=true', () => {
const wrapper = setup({
props: { showVerticalCrosshair: true },
context: { tooltipOpen: true },
});
expect(wrapper.find('div.visx-crosshair-vertical')).toHaveLength(1);
});

it('should render a horizontal crosshair if showVerticalCrossHair=true', () => {
const wrapper = setup({
props: { showHorizontalCrosshair: true },
context: { tooltipOpen: true },
});
expect(wrapper.find('div.visx-crosshair-horizontal')).toHaveLength(1);
});

it('should not render a glyph if showDatumGlyph=true and there is no nearestDatum', () => {
const wrapper = setup({
props: { showDatumGlyph: true },
context: { tooltipOpen: true },
});
expect(wrapper.find('div.visx-tooltip-glyph')).toHaveLength(0);
});
it('should render a glyph if showDatumGlyph=true if there is a nearestDatum', () => {
const wrapper = setup({
props: { showDatumGlyph: true },
context: {
tooltipOpen: true,
tooltipData: {
nearestDatum: { distance: 1, key: '', index: 0, datum: {} },
datumByKey: {},
},
},
});
expect(wrapper.find('div.visx-tooltip-glyph')).toHaveLength(1);
});
it('should render a glyph for each series if showSeriesGlyphs=true', () => {
const wrapper = setup({
props: { showSeriesGlyphs: true },
context: {
tooltipOpen: true,
tooltipData: {
datumByKey: {
d1: { key: 'd1', index: 0, datum: {} },
d2: { key: 'd2', index: 1, datum: {} },
},
},
},
Parent: (
{ children }, // glyphs snap to data points, so scales/accessors must exist
) => (
<DataContext.Provider
value={{
...getDataContext([
{
key: 'd1',
xAccessor: () => 3,
yAccessor: () => 7,
data: [{}],
},
{
key: 'd2',
xAccessor: () => 3,
yAccessor: () => 7,
data: [{}],
},
]),
}}
>
{children}
</DataContext.Provider>
),
});
expect(wrapper.find('div.visx-tooltip-glyph')).toHaveLength(2);
});
});
60 changes: 56 additions & 4 deletions packages/visx-xychart/test/providers/TooltipProvider.test.tsx
Original file line number Diff line number Diff line change
@@ -1,18 +1,70 @@
import React, { useContext } from 'react';
import React, { useContext, useEffect } from 'react';
import { mount } from 'enzyme';
import { TooltipProvider, TooltipContext } from '../../src';
import { TooltipProvider, TooltipContext, TooltipData } from '../../src';

describe('<TooltipProvider />', () => {
it('should be defined', () => {
expect(TooltipProvider).toBeDefined();
});

it('should provide an emitter for subscribing and emitting events', () => {
it('should provide tooltip state', () => {
expect.assertions(1);

const TooltipConsumer = () => {
const tooltipContext = useContext(TooltipContext);
expect(tooltipContext).toBeDefined();
expect(tooltipContext).toMatchObject({
tooltipOpen: expect.any(Boolean),
showTooltip: expect.any(Function),
updateTooltip: expect.any(Function),
hideTooltip: expect.any(Function),
});

return null;
};

mount(
<TooltipProvider>
<TooltipConsumer />
</TooltipProvider>,
);
});

it('showTooltip should update tooltipData.nearestDatum/datumByKey', () => {
expect.assertions(1);

const TooltipConsumer = () => {
const tooltipContext = useContext(TooltipContext);
const tooltipOpen = tooltipContext?.tooltipOpen;
const showTooltip = tooltipContext?.showTooltip;

useEffect(() => {
// this triggers a re-render of the component which triggers the assertion block
if (!tooltipOpen && showTooltip) {
showTooltip({
key: 'near',
index: 0,
distanceX: 0,
distanceY: 0,
datum: { hi: 'hello' },
});
showTooltip({
key: 'far',
index: 1,
datum: { good: 'bye' },
// no distance = Infinity
});
}
}, [tooltipOpen, showTooltip]);

if (tooltipOpen) {
expect(tooltipContext?.tooltipData).toMatchObject({
nearestDatum: { key: 'near', index: 0, distance: 0, datum: { hi: 'hello' } },
datumByKey: {
near: { key: 'near', index: 0, datum: { hi: 'hello' } },
far: { key: 'far', index: 1, datum: { good: 'bye' } },
},
} as TooltipData<object>);
}

return null;
};
Expand Down