Skip to content

Commit c57b2d0

Browse files
committed
core unit tests
1 parent 10428e8 commit c57b2d0

File tree

6 files changed

+386
-5
lines changed

6 files changed

+386
-5
lines changed
Lines changed: 94 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,94 @@
1+
// Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved.
2+
// SPDX-License-Identifier: Apache-2.0
3+
4+
import { render, waitFor } from "@testing-library/react";
5+
import Highcharts from "highcharts";
6+
7+
import "highcharts/modules/no-data-to-display";
8+
import "@cloudscape-design/components/test-utils/dom";
9+
import { CloudscapeHighcharts, CloudscapeHighchartsProps } from "../../../lib/components/core/chart-core";
10+
import testClasses from "../../../lib/components/core/test-classes/styles.selectors";
11+
import createWrapper, { ElementWrapper } from "../../../lib/components/test-utils/dom";
12+
13+
class TestWrapper extends ElementWrapper {
14+
findNoData = () => this.findByClassName(testClasses["no-data"]);
15+
}
16+
17+
function renderChart(props: Partial<CloudscapeHighchartsProps>, Component = CloudscapeHighcharts) {
18+
render(<Component highcharts={Highcharts} className="test-chart" options={{}} {...props} />);
19+
const wrapper = new TestWrapper(createWrapper().findByClassName("test-chart")!.getElement());
20+
return wrapper;
21+
}
22+
23+
const series: Highcharts.SeriesOptionsType[] = [{ type: "line", name: "Line series", data: [1, 2, 3] }];
24+
25+
const noDataContent = {
26+
loading: "no-data: loading",
27+
empty: "no-data: empty",
28+
noMatch: "no-data: no-match",
29+
error: "no-data: error",
30+
};
31+
32+
describe("CloudscapeHighcharts: no-data", () => {
33+
test('does not render no-data when statusType="finished"', () => {
34+
const wrapper = renderChart({
35+
options: { series },
36+
noData: { statusType: "finished", ...noDataContent },
37+
});
38+
39+
expect(wrapper.findNoData()).toBe(null);
40+
});
41+
42+
test('renders no-data loading when statusType="loading"', async () => {
43+
const wrapper = renderChart({
44+
options: { series },
45+
noData: { statusType: "loading", ...noDataContent },
46+
});
47+
48+
await waitFor(() => {
49+
expect(wrapper.findNoData()).not.toBe(null);
50+
expect(wrapper.findNoData()!.getElement().textContent).toBe("no-data: loading");
51+
expect(wrapper.findLiveRegion()!.getElement()).toHaveTextContent("no-data: loading");
52+
});
53+
});
54+
55+
test('renders no-data loading when statusType="error"', async () => {
56+
const wrapper = renderChart({
57+
options: { series },
58+
noData: { statusType: "error", ...noDataContent },
59+
});
60+
61+
await waitFor(() => {
62+
expect(wrapper.findNoData()).not.toBe(null);
63+
expect(wrapper.findNoData()!.getElement().textContent).toBe("no-data: error");
64+
expect(wrapper.findLiveRegion()!.getElement()).toHaveTextContent("no-data: error");
65+
});
66+
});
67+
68+
test('renders no-data empty when statusType="finished" and no series provided', async () => {
69+
const wrapper = renderChart({
70+
options: { series: [] },
71+
noData: { statusType: "error", ...noDataContent },
72+
});
73+
74+
await waitFor(() => {
75+
expect(wrapper.findNoData()).not.toBe(null);
76+
expect(wrapper.findNoData()!.getElement().textContent).toBe("no-data: empty");
77+
expect(wrapper.findLiveRegion()).toBe(null);
78+
});
79+
});
80+
81+
test('renders no-data no-match when statusType="finished" and no series visible', async () => {
82+
const wrapper = renderChart({
83+
options: { series },
84+
noData: { statusType: "error", ...noDataContent },
85+
visibleSeries: [],
86+
});
87+
88+
await waitFor(() => {
89+
expect(wrapper.findNoData()).not.toBe(null);
90+
expect(wrapper.findNoData()!.getElement().textContent).toBe("no-data: no-match");
91+
expect(wrapper.findLiveRegion()).toBe(null);
92+
});
93+
});
94+
});
Lines changed: 37 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,37 @@
1+
// Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved.
2+
// SPDX-License-Identifier: Apache-2.0
3+
4+
import { render } from "@testing-library/react";
5+
import Highcharts from "highcharts";
6+
7+
import "@cloudscape-design/components/test-utils/dom";
8+
import { CloudscapeHighcharts, CloudscapeHighchartsProps } from "../../../lib/components/core/chart-core";
9+
import createWrapper from "../../../lib/components/test-utils/dom";
10+
11+
function renderChart(props: Partial<CloudscapeHighchartsProps>) {
12+
render(<CloudscapeHighcharts highcharts={Highcharts} className="test-chart" options={{}} {...props} />);
13+
return createWrapper().findByClassName("test-chart");
14+
}
15+
16+
describe("CloudscapeHighcharts: rendering", () => {
17+
test("renders default fallback with highcharts=null", () => {
18+
const wrapper = renderChart({ highcharts: null });
19+
expect(wrapper).not.toBe(null);
20+
expect(wrapper!.findSpinner()).not.toBe(null);
21+
});
22+
23+
test("renders custom fallback with highcharts=null", () => {
24+
const wrapper = renderChart({ highcharts: null, fallback: "Custom fallback" });
25+
expect(wrapper).not.toBe(null);
26+
expect(wrapper!.findSpinner()).toBe(null);
27+
expect(wrapper!.getElement()).toHaveTextContent("Custom fallback");
28+
});
29+
30+
test("renders chart with highcharts=Highcharts", () => {
31+
const wrapper = renderChart({
32+
options: { title: { text: "Chart title" } },
33+
});
34+
expect(wrapper).not.toBe(null);
35+
expect(wrapper!.getElement()).toHaveTextContent("Chart title");
36+
});
37+
});
Lines changed: 236 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,236 @@
1+
// Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved.
2+
// SPDX-License-Identifier: Apache-2.0
3+
4+
import { useState } from "react";
5+
import { render } from "@testing-library/react";
6+
import Highcharts from "highcharts";
7+
import { vi } from "vitest";
8+
9+
import "@cloudscape-design/components/test-utils/dom";
10+
import { CloudscapeHighcharts, CloudscapeHighchartsProps } from "../../../lib/components/core/chart-core";
11+
import createWrapper, { ElementWrapper } from "../../../lib/components/test-utils/dom";
12+
13+
class TestWrapper extends ElementWrapper {
14+
findLegendItems = () => this.findAllByClassName("highcharts-legend-item");
15+
findHiddenLegendItems = () => this.findAllByClassName("highcharts-legend-item-hidden");
16+
}
17+
18+
function StatefulChart(props: CloudscapeHighchartsProps) {
19+
const [visibleSeries, setVisibleSeries] = useState<null | string[]>(props.visibleSeries ?? null);
20+
const [visibleItems, setVisibleItems] = useState<null | string[]>(props.visibleItems ?? null);
21+
return (
22+
<CloudscapeHighcharts
23+
{...props}
24+
visibleSeries={visibleSeries}
25+
onToggleVisibleSeries={(state) => {
26+
setVisibleSeries(state);
27+
props.onToggleVisibleSeries?.(state);
28+
}}
29+
visibleItems={visibleItems}
30+
onToggleVisibleItems={(state) => {
31+
setVisibleItems(state);
32+
props.onToggleVisibleItems?.(state);
33+
}}
34+
/>
35+
);
36+
}
37+
38+
function renderChart(props: Partial<CloudscapeHighchartsProps>, Component = CloudscapeHighcharts) {
39+
const { rerender } = render(<Component highcharts={Highcharts} className="test-chart" options={{}} {...props} />);
40+
const wrapper = new TestWrapper(createWrapper().findByClassName("test-chart")!.getElement());
41+
return {
42+
wrapper,
43+
rerender: (props: Partial<CloudscapeHighchartsProps>) =>
44+
rerender(<Component highcharts={Highcharts} className="test-chart" options={{}} {...props} />),
45+
};
46+
}
47+
48+
function renderStatefulChart(props: Partial<CloudscapeHighchartsProps>) {
49+
return renderChart(props, StatefulChart);
50+
}
51+
52+
describe("CloudscapeHighcharts: visibility", () => {
53+
test.each(["uncontrolled", "controlled"])("toggles series visibility by clicking on legend, %s", (mode) => {
54+
const onToggleVisibleSeries = vi.fn();
55+
const series: Highcharts.SeriesOptionsType[] = [{ type: "line", name: "Line series", data: [1, 2, 3] }];
56+
const { wrapper } =
57+
mode === "uncontrolled"
58+
? renderChart({ options: { series } })
59+
: renderStatefulChart({ options: { series }, visibleSeries: ["Line series"], onToggleVisibleSeries });
60+
61+
expect(wrapper.findLegendItems()).toHaveLength(1);
62+
expect(wrapper.findLegendItems()[0].getElement()).toHaveTextContent("Line series");
63+
expect(wrapper.findHiddenLegendItems()).toHaveLength(0);
64+
65+
wrapper.findLegendItems()[0].click();
66+
67+
expect(wrapper.findLegendItems()).toHaveLength(1);
68+
expect(wrapper.findHiddenLegendItems()).toHaveLength(1);
69+
expect(wrapper.findHiddenLegendItems()[0].getElement()).toHaveTextContent("Line series");
70+
71+
if (mode === "controlled") {
72+
expect(onToggleVisibleSeries).toHaveBeenCalledWith(expect.arrayContaining([]));
73+
}
74+
});
75+
76+
test("changes series visibility from the outside", () => {
77+
const onToggleVisibleSeries = vi.fn();
78+
const series: Highcharts.SeriesOptionsType[] = [
79+
{ type: "line", name: "Line series 1", data: [1, 2, 3] },
80+
{ type: "line", name: "Line series 2", data: [1, 2, 3] },
81+
];
82+
const { wrapper, rerender } = renderChart({ options: { series }, visibleSeries: null, onToggleVisibleSeries });
83+
const getLegendItemContent = () => wrapper.findLegendItems().map((w) => w.getElement().textContent);
84+
const getHiddenLegendItemContent = () => wrapper.findHiddenLegendItems().map((w) => w.getElement().textContent);
85+
86+
expect(getLegendItemContent()).toEqual(["Line series 1", "Line series 2"]);
87+
expect(getHiddenLegendItemContent()).toEqual([]);
88+
89+
rerender({ options: { series }, visibleSeries: ["Line series 1", "Line series 2"], onToggleVisibleSeries });
90+
91+
expect(getLegendItemContent()).toEqual(["Line series 1", "Line series 2"]);
92+
expect(getHiddenLegendItemContent()).toEqual([]);
93+
94+
rerender({ options: { series }, visibleSeries: ["Line series 1"], onToggleVisibleSeries });
95+
96+
expect(getLegendItemContent()).toEqual(["Line series 1", "Line series 2"]);
97+
expect(getHiddenLegendItemContent()).toEqual(["Line series 2"]);
98+
99+
rerender({ options: { series }, visibleSeries: ["Line series 2"], onToggleVisibleSeries });
100+
101+
expect(getLegendItemContent()).toEqual(["Line series 1", "Line series 2"]);
102+
expect(getHiddenLegendItemContent()).toEqual(["Line series 1"]);
103+
104+
rerender({ options: { series }, visibleSeries: [], onToggleVisibleSeries });
105+
106+
expect(getLegendItemContent()).toEqual(["Line series 1", "Line series 2"]);
107+
expect(getHiddenLegendItemContent()).toEqual(["Line series 1", "Line series 2"]);
108+
});
109+
110+
test("prefers series id over series name", () => {
111+
const onToggleVisibleSeries = vi.fn();
112+
const series: Highcharts.SeriesOptionsType[] = [
113+
{ type: "line", id: "1", name: "Line", data: [1, 2, 3] },
114+
{ type: "line", id: "2", name: "Line", data: [1, 2, 3] },
115+
];
116+
const { wrapper, rerender } = renderChart({ options: { series }, visibleSeries: ["Line"], onToggleVisibleSeries });
117+
const getLegendItemContent = () => wrapper.findLegendItems().map((w) => w.getElement().textContent);
118+
const getHiddenLegendItemContent = () => wrapper.findHiddenLegendItems().map((w) => w.getElement().textContent);
119+
120+
expect(getLegendItemContent()).toEqual(["Line", "Line"]);
121+
expect(getHiddenLegendItemContent()).toEqual(["Line", "Line"]);
122+
123+
rerender({ options: { series }, visibleSeries: ["1", "2"], onToggleVisibleSeries });
124+
125+
expect(getLegendItemContent()).toEqual(["Line", "Line"]);
126+
expect(getHiddenLegendItemContent()).toEqual([]);
127+
128+
wrapper.findLegendItems()[0].click();
129+
130+
expect(onToggleVisibleSeries).toHaveBeenCalledWith(expect.arrayContaining(["2"]));
131+
});
132+
133+
test.each(["uncontrolled", "controlled"])("toggles items visibility by clicking on legend, %s", (mode) => {
134+
const onToggleVisibleItems = vi.fn();
135+
const series: Highcharts.SeriesOptionsType[] = [
136+
{
137+
type: "pie",
138+
name: "Pie series",
139+
data: [
140+
{ name: "A", y: 20 },
141+
{ name: "B", y: 80 },
142+
],
143+
showInLegend: true,
144+
},
145+
];
146+
const { wrapper } =
147+
mode === "uncontrolled"
148+
? renderChart({ options: { series } })
149+
: renderStatefulChart({ options: { series }, visibleItems: ["A", "B"], onToggleVisibleItems });
150+
151+
expect(wrapper.findLegendItems()).toHaveLength(2);
152+
expect(wrapper.findLegendItems()[1].getElement()).toHaveTextContent("B");
153+
expect(wrapper.findHiddenLegendItems()).toHaveLength(0);
154+
155+
wrapper.findLegendItems()[1].click();
156+
157+
expect(wrapper.findLegendItems()).toHaveLength(2);
158+
expect(wrapper.findHiddenLegendItems()).toHaveLength(1);
159+
expect(wrapper.findHiddenLegendItems()[0].getElement()).toHaveTextContent("B");
160+
161+
if (mode === "controlled") {
162+
expect(onToggleVisibleItems).toHaveBeenCalledWith(expect.arrayContaining(["A"]));
163+
}
164+
});
165+
166+
test("changes items visibility from the outside", () => {
167+
const onToggleVisibleItems = vi.fn();
168+
const series: Highcharts.SeriesOptionsType[] = [
169+
{
170+
type: "pie",
171+
name: "Pie series",
172+
data: [
173+
{ name: "A", y: 20 },
174+
{ name: "B", y: 80 },
175+
],
176+
showInLegend: true,
177+
},
178+
];
179+
const { wrapper, rerender } = renderChart({ options: { series }, visibleItems: null, onToggleVisibleItems });
180+
const getLegendItemContent = () => wrapper.findLegendItems().map((w) => w.getElement().textContent);
181+
const getHiddenLegendItemContent = () => wrapper.findHiddenLegendItems().map((w) => w.getElement().textContent);
182+
183+
expect(getLegendItemContent()).toEqual(["A", "B"]);
184+
expect(getHiddenLegendItemContent()).toEqual([]);
185+
186+
rerender({ options: { series }, visibleItems: ["A", "B"], onToggleVisibleItems });
187+
188+
expect(getLegendItemContent()).toEqual(["A", "B"]);
189+
expect(getHiddenLegendItemContent()).toEqual([]);
190+
191+
rerender({ options: { series }, visibleItems: ["A"], onToggleVisibleItems });
192+
193+
expect(getLegendItemContent()).toEqual(["A", "B"]);
194+
expect(getHiddenLegendItemContent()).toEqual(["B"]);
195+
196+
rerender({ options: { series }, visibleItems: ["B"], onToggleVisibleItems });
197+
198+
expect(getLegendItemContent()).toEqual(["A", "B"]);
199+
expect(getHiddenLegendItemContent()).toEqual(["A"]);
200+
201+
rerender({ options: { series }, visibleItems: [], onToggleVisibleItems });
202+
203+
expect(getLegendItemContent()).toEqual(["A", "B"]);
204+
expect(getHiddenLegendItemContent()).toEqual(["A", "B"]);
205+
});
206+
207+
test("prefers item id over item name", () => {
208+
const onToggleVisibleItems = vi.fn();
209+
const series: Highcharts.SeriesOptionsType[] = [
210+
{
211+
type: "pie",
212+
name: "Pie series",
213+
data: [
214+
{ id: "1", name: "Segment", y: 20 },
215+
{ id: "2", name: "Segment", y: 80 },
216+
],
217+
showInLegend: true,
218+
},
219+
];
220+
const { wrapper, rerender } = renderChart({ options: { series }, visibleItems: ["Segment"], onToggleVisibleItems });
221+
const getLegendItemContent = () => wrapper.findLegendItems().map((w) => w.getElement().textContent);
222+
const getHiddenLegendItemContent = () => wrapper.findHiddenLegendItems().map((w) => w.getElement().textContent);
223+
224+
expect(getLegendItemContent()).toEqual(["Segment", "Segment"]);
225+
expect(getHiddenLegendItemContent()).toEqual(["Segment", "Segment"]);
226+
227+
rerender({ options: { series }, visibleItems: ["1", "2"], onToggleVisibleItems });
228+
229+
expect(getLegendItemContent()).toEqual(["Segment", "Segment"]);
230+
expect(getHiddenLegendItemContent()).toEqual([]);
231+
232+
wrapper.findLegendItems()[0].click();
233+
234+
expect(onToggleVisibleItems).toHaveBeenCalledWith(expect.arrayContaining(["2"]));
235+
});
236+
});

src/core/chart-core.tsx

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -19,7 +19,7 @@ import { ChartNoDataProps, LegendMarkersProps, TooltipProps } from "./interfaces
1919
import * as Styles from "./styles";
2020
import { getSeriesToIdMap } from "./utils";
2121

22-
interface CloudscapeHighchartsCoreProps {
22+
export interface CloudscapeHighchartsProps {
2323
highcharts: null | typeof Highcharts;
2424
options: Highcharts.Options;
2525
tooltip?: TooltipProps;
@@ -38,7 +38,7 @@ export interface CloudscapeHighchartsRef {
3838
}
3939

4040
interface CloudscapeHighchartsForwardRefType {
41-
(props: CloudscapeHighchartsCoreProps & { ref?: React.Ref<CloudscapeHighchartsRef> }): JSX.Element;
41+
(props: CloudscapeHighchartsProps & { ref?: React.Ref<CloudscapeHighchartsRef> }): JSX.Element;
4242
}
4343

4444
/**
@@ -171,8 +171,8 @@ export const CloudscapeHighcharts = forwardRef(
171171
}
172172

173173
// Handle items visibility (e.g. pie segments or treemap values).
174-
if (visibleItemsIndex !== undefined && highcharts && event.legendItem instanceof highcharts.Point) {
175-
const itemId = event.legendItem.options?.id;
174+
if (visibleItemsExternal !== undefined && highcharts && event.legendItem instanceof highcharts.Point) {
175+
const itemId = event.legendItem.options?.id ?? event.legendItem.options.name;
176176
const visible = event.legendItem.visible;
177177
if (itemId) {
178178
const nextVisible = !visible;

0 commit comments

Comments
 (0)