diff --git a/packages/polaris-viz/src/components/FunnelChartNext/Chart.tsx b/packages/polaris-viz/src/components/FunnelChartNext/Chart.tsx index 58b2493fd..712128643 100644 --- a/packages/polaris-viz/src/components/FunnelChartNext/Chart.tsx +++ b/packages/polaris-viz/src/components/FunnelChartNext/Chart.tsx @@ -25,7 +25,6 @@ import {SingleTextLine} from '../Labels'; import {ChartElements} from '../ChartElements'; import {FunnelChartXAxisLabels, Tooltip} from './components/'; -import {calculateDropOff} from './utilities/calculate-dropoff'; import type {FunnelChartNextProps} from './FunnelChartNext'; import {FunnelTooltip} from './components/FunnelTooltip/FunnelTooltip'; import { diff --git a/packages/polaris-viz/src/components/FunnelChartNext/tests/Chart.test.tsx b/packages/polaris-viz/src/components/FunnelChartNext/tests/Chart.test.tsx new file mode 100644 index 000000000..5d798920c --- /dev/null +++ b/packages/polaris-viz/src/components/FunnelChartNext/tests/Chart.test.tsx @@ -0,0 +1,113 @@ +import {mount} from '@shopify/react-testing'; +import {ChartContext} from '@shopify/polaris-viz-core'; +import type {DataSeries} from '@shopify/polaris-viz-core'; +import React from 'react'; + +import {Chart} from '../Chart'; +import {FunnelChartConnector, FunnelChartSegment} from '../../shared'; +import {FunnelTooltip} from '../components/FunnelTooltip/FunnelTooltip'; +import {SingleTextLine} from '../../Labels'; + +const mockData: DataSeries[] = [ + { + name: 'Group 1', + data: [ + {key: 'Step 1', value: 100}, + {key: 'Step 2', value: 75}, + {key: 'Step 3', value: 50}, + ], + }, +]; + +const mockContext = { + containerBounds: { + width: 500, + height: 300, + x: 0, + y: 0, + }, +}; + +const defaultProps = { + data: mockData, + showConnectionPercentage: false, + tooltipLabels: { + dropoff: 'Dropoff', + total: 'Total', + }, + xAxisOptions: { + hide: false, + labelFormatter: (value: string) => value, + }, + yAxisOptions: { + labelFormatter: (value: number) => value.toString(), + }, +}; + +describe('', () => { + it('renders funnel segments for each data point', () => { + const chart = mount( + + + , + ); + + expect(chart).toContainReactComponentTimes(FunnelChartSegment, 3); + }); + + it('renders n-1 connectors for n funnel segments, excluding the last segment', () => { + const chart = mount( + + + , + ); + + expect(chart).toContainReactComponentTimes(FunnelChartConnector, 2); + }); + + it('formats labels using the provided formatters', () => { + const customFormatter = (value: string) => `Custom ${value}`; + const chart = mount( + + + , + ); + + expect(chart).toContainReactComponent(SingleTextLine, { + text: 'Custom Step 1', + }); + }); + + it('shows tooltip when hovering over a segment', () => { + const chart = mount( + + + , + ); + + const firstSegment = chart.find(FunnelChartSegment); + firstSegment?.trigger('onMouseEnter', 0); + + expect(chart).toContainReactComponent(FunnelTooltip); + }); + + it('hides tooltip when mouse leaves a segment', () => { + const chart = mount( + + + , + ); + + const firstSegment = chart.find(FunnelChartSegment); + firstSegment?.trigger('onMouseEnter', 0); + firstSegment?.trigger('onMouseLeave'); + + expect(chart).not.toContainReactComponent(FunnelTooltip); // Tooltip container + }); +}); diff --git a/packages/polaris-viz/src/components/FunnelChartNext/tests/FunnelChartNext.test.tsx b/packages/polaris-viz/src/components/FunnelChartNext/tests/FunnelChartNext.test.tsx new file mode 100644 index 000000000..4f1f4c3d8 --- /dev/null +++ b/packages/polaris-viz/src/components/FunnelChartNext/tests/FunnelChartNext.test.tsx @@ -0,0 +1,127 @@ +import React from 'react'; +import {mount} from '@shopify/react-testing'; +import { + ChartState, + ChartContext, + DEFAULT_THEME_NAME, +} from '@shopify/polaris-viz-core'; +import {act} from 'react-dom/test-utils'; + +import {FunnelChartNext} from '../FunnelChartNext'; +import {Chart} from '../Chart'; +import {ChartSkeleton} from '../../ChartSkeleton'; +import {FunnelChartSegment} from '../../shared'; +import {FunnelConnector} from '../components'; + +const mockData = [ + { + name: 'Funnel', + data: [ + {key: 'Step 1', value: 1000}, + {key: 'Step 2', value: 750}, + {key: 'Step 3', value: 500}, + {key: 'Step 4', value: 250}, + ], + }, +]; + +const mockTooltipLabels = { + reached: 'Reached', + dropped: 'Dropped', +}; + +describe('', () => { + describe('rendering states', () => { + it('renders a Chart when state is Success', () => { + const funnel = mount( + , + ); + + expect(funnel).toContainReactComponent(Chart); + }); + + it('renders a ChartSkeleton when state is Loading', () => { + const funnel = mount( + , + ); + + expect(funnel).toContainReactComponent(ChartSkeleton, { + type: 'Funnel', + state: ChartState.Loading, + }); + }); + + it('renders a ChartSkeleton with error text when state is Error', () => { + const errorText = 'Something went wrong'; + const funnel = mount( + , + ); + + expect(funnel).toContainReactComponent(ChartSkeleton, { + type: 'Funnel', + errorText, + state: ChartState.Error, + }); + }); + }); + + describe('chart configuration', () => { + it('passes theme to Chart component', () => { + const funnel = mount( + , + ); + + expect(funnel).toContainReactComponent(Chart); + }); + + it('passes xAxisOptions to Chart', () => { + const xAxisOptions = {hide: true}; + const funnel = mount( + , + ); + + expect(funnel).toContainReactComponent(Chart, { + xAxisOptions: expect.objectContaining({hide: true}), + }); + }); + + it('passes yAxisOptions to Chart', () => { + const labelFormatter = (value: number) => `$${value}`; + const funnel = mount( + , + ); + + expect(funnel).toContainReactComponent(Chart, { + yAxisOptions: expect.objectContaining({labelFormatter}), + }); + }); + }); +}); diff --git a/packages/polaris-viz/src/hooks/tests/useFunnelBarScaling.test.tsx b/packages/polaris-viz/src/hooks/tests/useFunnelBarScaling.test.tsx new file mode 100644 index 000000000..51e325f74 --- /dev/null +++ b/packages/polaris-viz/src/hooks/tests/useFunnelBarScaling.test.tsx @@ -0,0 +1,118 @@ +import type {Root} from '@shopify/react-testing'; +import {mount} from '@shopify/react-testing'; +import {scaleLinear} from 'd3-scale'; +import React from 'react'; + +import { + useFunnelBarScaling, + SCALING_RATIO_THRESHOLD, + MINIMUM_SEGMENT_HEIGHT_RATIO, +} from '../useFunnelBarScaling'; + +const mockYScale = scaleLinear().domain([0, 100]).range([0, 400]); + +function parseData(result: Root) { + return JSON.parse(result.domNode?.dataset.data ?? ''); +} + +describe('useFunnelBarScaling', () => { + it('returns shouldApplyScaling=false when ratio above threshold', () => { + function TestComponent() { + const data = useFunnelBarScaling({ + yScale: mockYScale, + values: [90, 100], + }); + + return ; + } + + const result = mount(); + const data = parseData(result); + + expect(data.shouldApplyScaling).toBe(false); + }); + + it('returns shouldApplyScaling=true when ratio below threshold', () => { + function TestComponent() { + const data = useFunnelBarScaling({ + yScale: mockYScale, + values: [5, 100], + }); + + return ; + } + + const result = mount(); + const data = parseData(result); + + expect(data.shouldApplyScaling).toBe(true); + }); + + describe('getBarHeight', () => { + it('returns original height when scaling not needed', () => { + function TestComponent() { + const data = useFunnelBarScaling({ + yScale: mockYScale, + values: [90, 100], + }); + + const height = data.getBarHeight(90); + return ; + } + + const result = mount(); + const data = parseData(result); + + expect(data.height).toBe(mockYScale(90)); + }); + + it('returns scaled height when scaling needed', () => { + function TestComponent() { + const data = useFunnelBarScaling({ + yScale: mockYScale, + values: [5, 100], + }); + + const scaledHeight = data.getBarHeight(5); + const originalHeight = mockYScale(5); + const tallestHeight = mockYScale(100); + + return ( + + ); + } + + const result = mount(); + const data = parseData(result); + + expect(data.scaledHeight).toBeGreaterThan(data.originalHeight); + expect(data.scaledHeight).toBeLessThan(data.tallestHeight); + expect(data.scaledHeight / data.tallestHeight).toBeGreaterThanOrEqual( + MINIMUM_SEGMENT_HEIGHT_RATIO, + ); + }); + + it('returns original height for tallest bar even when scaling applied', () => { + function TestComponent() { + const data = useFunnelBarScaling({ + yScale: mockYScale, + values: [5, 100], + }); + + const height = data.getBarHeight(100); + return ; + } + + const result = mount(); + const data = parseData(result); + + expect(data.height).toBe(mockYScale(100)); + }); + }); +});