-
Notifications
You must be signed in to change notification settings - Fork 5
Expand file tree
/
Copy pathAdvancedChartImpl.funnelOrder.test.tsx
More file actions
141 lines (127 loc) · 4.77 KB
/
Copy pathAdvancedChartImpl.funnelOrder.test.tsx
File metadata and controls
141 lines (127 loc) · 4.77 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
/**
* ObjectUI
* Copyright (c) 2024-present ObjectStack Inc.
*
* This source code is licensed under the MIT license found in the
* LICENSE file in the root directory of this source tree.
*/
/**
* framework#3588 — a funnel's shape asserts a sequence.
*
* The renderer used to sort funnel segments by value descending,
* unconditionally — so a sales pipeline rendered as a tidy narrowing shape
* regardless of the stages' real order, and any server-side ordering was
* silently overridden. With a declared `categoryOrder` the pipeline order wins;
* without one the value-descending default is preserved.
*/
import React from 'react';
import { describe, it, expect, vi, afterEach } from 'vitest';
import { render, cleanup } from '@testing-library/react';
// Recharts' ResponsiveContainer measures its parent via ResizeObserver, which
// reports 0×0 under the headless DOM — so the funnel never paints. Replace it
// with a fixed-size passthrough so segments actually render.
vi.mock('recharts', async () => {
const actual = await vi.importActual<any>('recharts');
return {
...actual,
ResponsiveContainer: ({ children }: any) =>
React.cloneElement(children, { width: 320, height: 320 }),
};
});
import AdvancedChartImpl from './AdvancedChartImpl';
afterEach(cleanup);
/**
* Rows as analytics returns them: alphabetical, and deliberately NOT
* monotonically narrowing — Proposal (200) is the largest, so a value sort and
* the declared pipeline order produce visibly different funnels.
*/
const PIPELINE = [
{ stage: 'Needs Analysis', total_amount: 120 },
{ stage: 'Negotiation', total_amount: 90 },
{ stage: 'Proposal', total_amount: 200 },
{ stage: 'Qualification', total_amount: 150 },
];
/** As `buildCategoryOrder` emits it: value and label adjacent, in declared order. */
const DECLARED = [
'qualification', 'Qualification',
'needs_analysis', 'Needs Analysis',
'proposal', 'Proposal',
'negotiation', 'Negotiation',
];
/**
* The segment labels in the order Recharts drew them. `<Funnel>` renders one
* `<text>` label per trapezoid in source order, which is exactly the ordering
* under test.
*/
function drawnOrder(container: HTMLElement): string[] {
return Array.from(container.querySelectorAll('text'))
.map((t) => t.textContent?.trim() ?? '')
.filter(Boolean);
}
function renderFunnel(extra: Record<string, unknown>) {
return render(
<AdvancedChartImpl
chartType="funnel"
data={PIPELINE}
xAxisKey="stage"
series={[{ dataKey: 'total_amount' }]}
config={{ total_amount: { label: 'Amount' } }}
isAnimationActive={false}
{...extra}
/>,
);
}
describe('funnel stage order (#3588)', () => {
it('draws every stage (guards the assertions below against an empty render)', () => {
const { container } = renderFunnel({ categoryOrder: DECLARED });
expect(container.querySelectorAll('.recharts-funnel-trapezoid')).toHaveLength(4);
expect(drawnOrder(container)).toHaveLength(4);
});
it('draws in the DECLARED pipeline order, not by value', () => {
const { container } = renderFunnel({ categoryOrder: DECLARED });
expect(drawnOrder(container)).toEqual([
'Qualification', 'Needs Analysis', 'Proposal', 'Negotiation',
]);
});
it('keeps the value-descending default when no order is declared', () => {
const { container } = renderFunnel({});
expect(drawnOrder(container)).toEqual([
'Proposal', 'Qualification', 'Needs Analysis', 'Negotiation',
]);
});
it('keeps a category missing from the declared order, after the declared ones', () => {
const { container } = render(
<AdvancedChartImpl
chartType="funnel"
data={[...PIPELINE, { stage: 'Closed Won', total_amount: 10 }]}
xAxisKey="stage"
series={[{ dataKey: 'total_amount' }]}
config={{ total_amount: { label: 'Amount' } }}
isAnimationActive={false}
categoryOrder={DECLARED}
/>,
);
// Never dropped — an unlisted stage still renders, and lands last.
expect(drawnOrder(container)).toEqual([
'Qualification', 'Needs Analysis', 'Proposal', 'Negotiation', 'Closed Won',
]);
});
it('orders by the stored VALUE too, for rows the server left unlabeled', () => {
const { container } = render(
<AdvancedChartImpl
chartType="funnel"
data={[
{ stage: 'negotiation', total_amount: 90 },
{ stage: 'qualification', total_amount: 150 },
{ stage: 'proposal', total_amount: 200 },
]}
xAxisKey="stage"
series={[{ dataKey: 'total_amount' }]}
config={{ total_amount: { label: 'Amount' } }}
isAnimationActive={false}
categoryOrder={DECLARED}
/>,
);
expect(drawnOrder(container)).toEqual(['qualification', 'proposal', 'negotiation']);
});
});