@oneuptime/common 12.0.10 → 12.0.11

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (81) hide show
  1. package/Models/DatabaseModels/NetworkDevice.ts +44 -0
  2. package/Models/DatabaseModels/NetworkDeviceLink.ts +116 -0
  3. package/Server/Infrastructure/Postgres/SchemaMigrations/1787400000000-AddDeviceRoleAndDeclaredLinkParent.ts +52 -0
  4. package/Server/Infrastructure/Postgres/SchemaMigrations/Index.ts +2 -0
  5. package/Server/Services/NetworkDeviceLinkService.ts +338 -2
  6. package/Server/Types/Billing/MeteredPlan/AllMeteredPlans.ts +19 -5
  7. package/Server/Types/Database/JSONColumnQuery.ts +42 -4
  8. package/Server/Utils/Telemetry/InventoryEntityRegistry.ts +186 -38
  9. package/Tests/App/Dashboard/AdminNotificationRulesPage.test.tsx +420 -112
  10. package/Tests/App/Dashboard/PayAsYouGoConsentGate.test.tsx +543 -0
  11. package/Tests/App/Dashboard/PayAsYouGoFreePlan.test.ts +125 -0
  12. package/Tests/App/Dashboard/PayAsYouGoNotices.test.tsx +612 -0
  13. package/Tests/App/Dashboard/SloWidgetChartRendering.test.tsx +536 -0
  14. package/Tests/Models/AnalyticsModels/CommonModel.test.ts +312 -0
  15. package/Tests/Server/Infrastructure/Postgres/DeviceRoleAndDeclaredLinkParentMigration.test.ts +250 -0
  16. package/Tests/Server/Infrastructure/Postgres/EpisodeMemberNotifyIndexesMigration.test.ts +46 -18
  17. package/Tests/Server/Services/NetworkDeviceLinkParentValidation.test.ts +1043 -0
  18. package/Tests/Server/Types/Billing/PayAsYouGoMeteredPricing.test.ts +125 -0
  19. package/Tests/Server/Types/Database/JSONColumnQuery.test.ts +508 -0
  20. package/Tests/Server/Utils/Telemetry/InventoryEntityRegistry.test.ts +668 -0
  21. package/Tests/Types/Billing/PayAsYouGoPricing.test.ts +64 -0
  22. package/Tests/Types/CustomField/CustomFieldTypeCorrespondence.test.ts +107 -0
  23. package/Tests/Types/Monitor/MonitorType.test.ts +62 -0
  24. package/Tests/UI/Components/Charts/ChartXAxisDataKey.test.tsx +256 -0
  25. package/Tests/UI/Components/Charts/DataPointUtil.test.ts +323 -0
  26. package/Tests/UI/Components/ModelTable/CustomFieldColumns.test.tsx +349 -1
  27. package/Tests/Utils/Monitor/NetworkDeviceRoleUtil.test.ts +502 -0
  28. package/Tests/Utils/Monitor/NetworkTopologyDeclaredHierarchy.test.ts +759 -0
  29. package/Tests/Utils/Schema/BaseSchema.test.ts +651 -0
  30. package/Types/Billing/PayAsYouGoPricing.ts +47 -0
  31. package/Types/CustomField/CustomFieldType.ts +28 -0
  32. package/Types/Monitor/MonitorType.ts +11 -0
  33. package/Types/Monitor/SnmpMonitor/NetworkTopology.ts +13 -0
  34. package/UI/Components/Charts/Area/AreaChart.tsx +4 -2
  35. package/UI/Components/Charts/Bar/BarChart.tsx +4 -2
  36. package/UI/Components/Charts/ChartLibrary/Types/ChartDataPoint.ts +13 -0
  37. package/UI/Components/Charts/Line/LineChart.tsx +4 -2
  38. package/UI/Components/Charts/Types/XAxis/XAxis.ts +6 -0
  39. package/UI/Components/Charts/Utils/DataPoint.ts +15 -12
  40. package/UI/Components/ModelTable/CustomFieldColumns.tsx +55 -0
  41. package/Utils/Monitor/NetworkDeviceRoleUtil.ts +45 -0
  42. package/Utils/Monitor/NetworkTopologyUtil.ts +42 -2
  43. package/build/dist/Models/DatabaseModels/NetworkDevice.js +45 -0
  44. package/build/dist/Models/DatabaseModels/NetworkDevice.js.map +1 -1
  45. package/build/dist/Models/DatabaseModels/NetworkDeviceLink.js +115 -0
  46. package/build/dist/Models/DatabaseModels/NetworkDeviceLink.js.map +1 -1
  47. package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/1787400000000-AddDeviceRoleAndDeclaredLinkParent.js +33 -0
  48. package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/1787400000000-AddDeviceRoleAndDeclaredLinkParent.js.map +1 -0
  49. package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/Index.js +2 -0
  50. package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/Index.js.map +1 -1
  51. package/build/dist/Server/Services/NetworkDeviceLinkService.js +253 -1
  52. package/build/dist/Server/Services/NetworkDeviceLinkService.js.map +1 -1
  53. package/build/dist/Server/Types/Billing/MeteredPlan/AllMeteredPlans.js +12 -5
  54. package/build/dist/Server/Types/Billing/MeteredPlan/AllMeteredPlans.js.map +1 -1
  55. package/build/dist/Server/Types/Database/JSONColumnQuery.js +40 -4
  56. package/build/dist/Server/Types/Database/JSONColumnQuery.js.map +1 -1
  57. package/build/dist/Server/Utils/Telemetry/InventoryEntityRegistry.js +128 -35
  58. package/build/dist/Server/Utils/Telemetry/InventoryEntityRegistry.js.map +1 -1
  59. package/build/dist/Types/Billing/PayAsYouGoPricing.js +41 -0
  60. package/build/dist/Types/Billing/PayAsYouGoPricing.js.map +1 -0
  61. package/build/dist/Types/CustomField/CustomFieldType.js +26 -0
  62. package/build/dist/Types/CustomField/CustomFieldType.js.map +1 -1
  63. package/build/dist/Types/Monitor/MonitorType.js +10 -0
  64. package/build/dist/Types/Monitor/MonitorType.js.map +1 -1
  65. package/build/dist/UI/Components/Charts/Area/AreaChart.js +2 -1
  66. package/build/dist/UI/Components/Charts/Area/AreaChart.js.map +1 -1
  67. package/build/dist/UI/Components/Charts/Bar/BarChart.js +2 -1
  68. package/build/dist/UI/Components/Charts/Bar/BarChart.js.map +1 -1
  69. package/build/dist/UI/Components/Charts/ChartLibrary/Types/ChartDataPoint.js +12 -0
  70. package/build/dist/UI/Components/Charts/ChartLibrary/Types/ChartDataPoint.js.map +1 -1
  71. package/build/dist/UI/Components/Charts/Line/LineChart.js +2 -1
  72. package/build/dist/UI/Components/Charts/Line/LineChart.js.map +1 -1
  73. package/build/dist/UI/Components/Charts/Utils/DataPoint.js +17 -10
  74. package/build/dist/UI/Components/Charts/Utils/DataPoint.js.map +1 -1
  75. package/build/dist/UI/Components/ModelTable/CustomFieldColumns.js +42 -0
  76. package/build/dist/UI/Components/ModelTable/CustomFieldColumns.js.map +1 -1
  77. package/build/dist/Utils/Monitor/NetworkDeviceRoleUtil.js +37 -0
  78. package/build/dist/Utils/Monitor/NetworkDeviceRoleUtil.js.map +1 -1
  79. package/build/dist/Utils/Monitor/NetworkTopologyUtil.js +25 -3
  80. package/build/dist/Utils/Monitor/NetworkTopologyUtil.js.map +1 -1
  81. package/package.json +1 -1
@@ -0,0 +1,64 @@
1
+ import { describe, expect, it } from "@jest/globals";
2
+ import {
3
+ ACTIVE_MONITOR_PRICE_IN_USD_PER_MONTH,
4
+ PRICING_PAGE_URL,
5
+ SESSION_REPLAY_PRICE_IN_USD_PER_GB,
6
+ TELEMETRY_PRICE_IN_USD_PER_GB,
7
+ TELEMETRY_PRICE_RETENTION_IN_DAYS,
8
+ formatPriceInUSD,
9
+ } from "../../../Types/Billing/PayAsYouGoPricing";
10
+
11
+ /*
12
+ * These constants are quoted to users, verbatim, on the telemetry ingestion
13
+ * key and create monitor pages. They are pinned here because changing one
14
+ * silently changes what the product promises a customer - a change to any of
15
+ * them has to be deliberate, and has to be made on the pricing page too.
16
+ */
17
+
18
+ describe("PayAsYouGoPricing", () => {
19
+ describe("rates", () => {
20
+ it("charges $0.10 per GB of telemetry, quoted for 15 day retention", () => {
21
+ expect(TELEMETRY_PRICE_IN_USD_PER_GB).toBe(0.1);
22
+ expect(TELEMETRY_PRICE_RETENTION_IN_DAYS).toBe(15);
23
+ });
24
+
25
+ it("charges $2 per GB for session replay, on the same 15 day window", () => {
26
+ expect(SESSION_REPLAY_PRICE_IN_USD_PER_GB).toBe(2);
27
+ });
28
+
29
+ it("charges $1 per active monitor per month", () => {
30
+ expect(ACTIVE_MONITOR_PRICE_IN_USD_PER_MONTH).toBe(1);
31
+ });
32
+
33
+ it("points at the public pricing page", () => {
34
+ expect(PRICING_PAGE_URL).toBe("https://oneuptime.com/pricing");
35
+ });
36
+ });
37
+
38
+ describe("formatPriceInUSD", () => {
39
+ it("writes a whole number of dollars without cents, like the pricing page", () => {
40
+ expect(formatPriceInUSD(1)).toBe("$1");
41
+ expect(formatPriceInUSD(22)).toBe("$22");
42
+ expect(formatPriceInUSD(0)).toBe("$0");
43
+ });
44
+
45
+ it("writes a fractional amount with two decimal places", () => {
46
+ expect(formatPriceInUSD(0.1)).toBe("$0.10");
47
+ expect(formatPriceInUSD(1.5)).toBe("$1.50");
48
+ expect(formatPriceInUSD(2.05)).toBe("$2.05");
49
+ });
50
+
51
+ it("rounds to cents rather than spilling extra digits into the copy", () => {
52
+ expect(formatPriceInUSD(0.125)).toBe("$0.13");
53
+ expect(formatPriceInUSD(0.006)).toBe("$0.01");
54
+ });
55
+
56
+ it("renders the advertised rates exactly as the pricing page writes them", () => {
57
+ expect(formatPriceInUSD(TELEMETRY_PRICE_IN_USD_PER_GB)).toBe("$0.10");
58
+ expect(formatPriceInUSD(SESSION_REPLAY_PRICE_IN_USD_PER_GB)).toBe("$2");
59
+ expect(formatPriceInUSD(ACTIVE_MONITOR_PRICE_IN_USD_PER_MONTH)).toBe(
60
+ "$1",
61
+ );
62
+ });
63
+ });
64
+ });
@@ -0,0 +1,107 @@
1
+ import CustomFieldType from "../../../Types/CustomField/CustomFieldType";
2
+ import FormFieldSchemaType from "../../../UI/Components/Forms/Types/FormFieldSchemaType";
3
+ import FieldType from "../../../UI/Components/Types/FieldType";
4
+ import { describe, expect, test } from "@jest/globals";
5
+
6
+ /*
7
+ * CustomFieldsDetail does not translate a definition's `customFieldType` into a
8
+ * render type - it hands the raw string to a Detail field and to a form field
9
+ * as their `fieldType`, through an `as any` that erases the enum on the way.
10
+ * Nothing in the compiler is watching that seam, so a member added here without
11
+ * a same-VALUE member in FieldType and FormFieldSchemaType type-checks, builds,
12
+ * ships, and only then shows itself: the Detail column falls off the end of the
13
+ * render switch and paints an empty cell, and the form falls off the end of the
14
+ * input switch and paints a control the user cannot type into. The field looks
15
+ * present and is silently unusable, which is the worst shape a regression can
16
+ * take because nothing throws and nothing logs.
17
+ *
18
+ * These are value-set comparisons rather than key comparisons on purpose. The
19
+ * three enums are free to spell a member however reads best on their own side -
20
+ * `Boolean` reaches the form as `FormFieldSchemaType.Toggle`, which is written
21
+ * `Toggle = "Boolean"` for exactly this reason - and only the value crosses the
22
+ * seam. A key-based check would both miss real breakage and reject that legal
23
+ * spelling.
24
+ */
25
+
26
+ const customFieldTypeEntries: Array<[string, string]> = Object.entries(
27
+ CustomFieldType,
28
+ ) as Array<[string, string]>;
29
+
30
+ const fieldTypeValues: Set<string> = new Set<string>(
31
+ Object.values(FieldType) as Array<string>,
32
+ );
33
+
34
+ const formFieldSchemaTypeValues: Set<string> = new Set<string>(
35
+ Object.values(FormFieldSchemaType) as Array<string>,
36
+ );
37
+
38
+ describe("CustomFieldType corresponds to FieldType", () => {
39
+ test.each(customFieldTypeEntries)(
40
+ "CustomFieldType.%s is a FieldType value",
41
+ (_key: string, value: string) => {
42
+ expect(fieldTypeValues.has(value)).toBe(true);
43
+ },
44
+ );
45
+ });
46
+
47
+ describe("CustomFieldType corresponds to FormFieldSchemaType", () => {
48
+ test.each(customFieldTypeEntries)(
49
+ "CustomFieldType.%s is a FormFieldSchemaType value",
50
+ (_key: string, value: string) => {
51
+ expect(formFieldSchemaTypeValues.has(value)).toBe(true);
52
+ },
53
+ );
54
+ });
55
+
56
+ describe("CustomFieldType member set", () => {
57
+ test("has at least one member so the correspondence loops cannot pass vacuously", () => {
58
+ /*
59
+ * The two blocks above are generated from this enum. If it were ever
60
+ * emptied - a bad merge, a barrel file re-export that resolves to {} - the
61
+ * per-member assertions would simply stop existing and the suite would go
62
+ * green while guarding nothing.
63
+ */
64
+ expect(customFieldTypeEntries.length).toBeGreaterThan(0);
65
+ });
66
+
67
+ test("has no duplicate values", () => {
68
+ /*
69
+ * Two keys sharing a value collapse at runtime: the second one wins every
70
+ * lookup, and a definition saved under the first key renders as the second
71
+ * one's type. It also makes the loops above lie, because a duplicate is
72
+ * checked twice and the shadowed member is never exercised on its own.
73
+ */
74
+ const values: Array<string> = customFieldTypeEntries.map(
75
+ ([, value]: [string, string]) => {
76
+ return value;
77
+ },
78
+ );
79
+
80
+ expect(new Set<string>(values).size).toBe(values.length);
81
+ });
82
+
83
+ test("includes Date and DateTime", () => {
84
+ /*
85
+ * The date members are what the jsonb range filters and the ISO-8601
86
+ * storage format exist for. Losing them is a silent feature removal - old
87
+ * definitions keep their stored `customFieldType` string in the database
88
+ * and would then fall through every switch that reads this enum.
89
+ */
90
+ expect(CustomFieldType.Date).toEqual("Date");
91
+ expect(CustomFieldType.DateTime).toEqual("DateTime");
92
+ });
93
+ });
94
+
95
+ describe("CustomFieldType correspondence is by value, not by key", () => {
96
+ test("Boolean reaches the form as Toggle, which is only visible by value", () => {
97
+ /*
98
+ * This is the case that proves the checks above have to compare values.
99
+ * FormFieldSchemaType has no member named `Boolean`; it names the same
100
+ * value `Toggle`. A correspondence test written against key names would
101
+ * fail here on code that works perfectly, and would then be "fixed" by
102
+ * relaxing it into something that no longer catches a missing member.
103
+ */
104
+ expect(Object.keys(FormFieldSchemaType)).not.toContain("Boolean");
105
+ expect(FormFieldSchemaType.Toggle).toEqual(CustomFieldType.Boolean);
106
+ });
107
+ });
@@ -253,6 +253,68 @@ describe("MonitorTypeHelper", () => {
253
253
  });
254
254
  });
255
255
 
256
+ /*
257
+ * The dashboard tells Free plan users which monitors cost money before they
258
+ * create one, and gates the create button on acknowledging it. That warning
259
+ * is only honest while this predicate agrees with what the server actually
260
+ * meters - ActiveMonitoringMeteredPlan counts every monitor whose type is
261
+ * not Manual.
262
+ */
263
+ describe("isBilledAsActiveMonitor", () => {
264
+ test("Manual monitors are free", () => {
265
+ expect(
266
+ MonitorTypeHelper.isBilledAsActiveMonitor(MonitorType.Manual),
267
+ ).toBe(false);
268
+ });
269
+
270
+ test("every other monitor type is billed", () => {
271
+ const billed: Array<MonitorType> = Object.values(MonitorType).filter(
272
+ (monitorType: MonitorType) => {
273
+ return monitorType !== MonitorType.Manual;
274
+ },
275
+ );
276
+
277
+ expect(billed.length).toBeGreaterThan(0);
278
+
279
+ for (const monitorType of billed) {
280
+ expect(MonitorTypeHelper.isBilledAsActiveMonitor(monitorType)).toBe(
281
+ true,
282
+ );
283
+ }
284
+ });
285
+
286
+ test("telemetry monitors are billed as active monitors too, on top of their ingest", () => {
287
+ expect(MonitorTypeHelper.isBilledAsActiveMonitor(MonitorType.Logs)).toBe(
288
+ true,
289
+ );
290
+ expect(
291
+ MonitorTypeHelper.isBilledAsActiveMonitor(MonitorType.Metrics),
292
+ ).toBe(true);
293
+ expect(
294
+ MonitorTypeHelper.isBilledAsActiveMonitor(MonitorType.Traces),
295
+ ).toBe(true);
296
+ });
297
+
298
+ test("agrees exactly with getActiveMonitorTypes", () => {
299
+ const active: Array<MonitorType> =
300
+ MonitorTypeHelper.getActiveMonitorTypes();
301
+
302
+ for (const monitorType of Object.values(MonitorType)) {
303
+ expect(MonitorTypeHelper.isBilledAsActiveMonitor(monitorType)).toBe(
304
+ active.includes(monitorType),
305
+ );
306
+ }
307
+ });
308
+
309
+ test("is the exact inverse of isManualMonitor", () => {
310
+ for (const monitorType of Object.values(MonitorType)) {
311
+ expect(MonitorTypeHelper.isBilledAsActiveMonitor(monitorType)).toBe(
312
+ !MonitorTypeHelper.isManualMonitor(monitorType),
313
+ );
314
+ }
315
+ });
316
+ });
317
+
256
318
  describe("doesMonitorTypeHaveGraphs", () => {
257
319
  test("returns true for graphable types and false otherwise", () => {
258
320
  expect(
@@ -0,0 +1,256 @@
1
+ import "@testing-library/jest-dom";
2
+ import { cleanup, render } from "@testing-library/react";
3
+ import React from "react";
4
+ import { afterEach, describe, expect, test } from "@jest/globals";
5
+
6
+ /*
7
+ * ResponsiveContainer measures its parent, which is always 0x0 in jsdom, so
8
+ * the chart renders nothing without a fixed size. Same shim as
9
+ * AxisTickColor.test.tsx — anything that asserts on rendered GEOMETRY needs
10
+ * it, because a 0x0 chart draws no marks either and would make every one of
11
+ * these assertions pass for the wrong reason.
12
+ */
13
+ jest.mock("recharts", () => {
14
+ const actual: Record<string, any> = jest.requireActual("recharts");
15
+ return {
16
+ ...actual,
17
+ ResponsiveContainer: ({ children }: { children: React.ReactElement }) => {
18
+ return React.cloneElement(children, { width: 600, height: 300 });
19
+ },
20
+ };
21
+ });
22
+
23
+ import AreaChartElement from "../../../../UI/Components/Charts/Area/AreaChart";
24
+ import BarChartElement from "../../../../UI/Components/Charts/Bar/BarChart";
25
+ import LineChartElement from "../../../../UI/Components/Charts/Line/LineChart";
26
+ import { CHART_DATA_POINT_X_AXIS_KEY } from "../../../../UI/Components/Charts/ChartLibrary/Types/ChartDataPoint";
27
+ import ChartCurve from "../../../../UI/Components/Charts/Types/ChartCurve";
28
+ import DataPoint from "../../../../UI/Components/Charts/Types/DataPoint";
29
+ import SeriesPoint from "../../../../UI/Components/Charts/Types/SeriesPoints";
30
+ import {
31
+ XAxis as ChartXAxis,
32
+ XAxisAggregateType,
33
+ } from "../../../../UI/Components/Charts/Types/XAxis/XAxis";
34
+ import XAxisType from "../../../../UI/Components/Charts/Types/XAxis/XAxisType";
35
+ import YAxis, {
36
+ YAxisPrecision,
37
+ } from "../../../../UI/Components/Charts/Types/YAxis/YAxis";
38
+ import YAxisType from "../../../../UI/Components/Charts/Types/YAxis/YAxisType";
39
+
40
+ /*
41
+ * The three chart wrappers hand recharts a FIXED x-axis dataKey, while the
42
+ * rows they hand it are built by DataPointUtil. Those two used to agree only
43
+ * by convention: the util keyed each row by the caller's `xAxis.legend`, and
44
+ * every caller but one happened to pass the literal "Time".
45
+ *
46
+ * The one that did not — the SLO dashboard widget, which passed "" — got a
47
+ * chart with axes, grid and tooltip but no line: recharts looked up "Time" on
48
+ * every row, found nothing, and drew no path. Nothing failed, nothing logged;
49
+ * the widget just rendered an empty frame on public and private dashboards
50
+ * alike.
51
+ *
52
+ * These tests hold the axis key on both sides of that seam: whatever legend a
53
+ * caller passes, the marks get drawn.
54
+ */
55
+
56
+ const START: Date = new Date("2026-08-10T00:00:00.000Z");
57
+ const END: Date = new Date("2026-08-10T01:00:00.000Z");
58
+
59
+ /* An hour window buckets at one minute, so these land on distinct buckets. */
60
+ type BuildPointsFunction = () => Array<DataPoint>;
61
+
62
+ const buildPoints: BuildPointsFunction = (): Array<DataPoint> => {
63
+ const points: Array<DataPoint> = [];
64
+
65
+ for (let index: number = 0; index < 10; index++) {
66
+ points.push({
67
+ x: new Date(START.getTime() + index * 60 * 1000),
68
+ y: 90 + index,
69
+ });
70
+ }
71
+
72
+ return points;
73
+ };
74
+
75
+ type BuildXAxisFunction = (legend: string) => ChartXAxis;
76
+
77
+ const buildXAxis: BuildXAxisFunction = (legend: string): ChartXAxis => {
78
+ return {
79
+ legend: legend,
80
+ options: {
81
+ type: XAxisType.Time,
82
+ min: START,
83
+ max: END,
84
+ aggregateType: XAxisAggregateType.Average,
85
+ },
86
+ };
87
+ };
88
+
89
+ const Y_AXIS: YAxis = {
90
+ legend: "%",
91
+ options: {
92
+ type: YAxisType.Number,
93
+ min: "auto",
94
+ max: 100,
95
+ precision: YAxisPrecision.TwoDecimals,
96
+ formatter: (value: number): string => {
97
+ return `${value}%`;
98
+ },
99
+ },
100
+ };
101
+
102
+ const SERIES_NAME: string = "SLI";
103
+
104
+ type BuildSeriesFunction = () => Array<SeriesPoint>;
105
+
106
+ const buildSeries: BuildSeriesFunction = (): Array<SeriesPoint> => {
107
+ return [{ seriesName: SERIES_NAME, data: buildPoints() }];
108
+ };
109
+
110
+ /*
111
+ * Legends worth covering: the canonical one every other caller passes, the
112
+ * empty string the SLO widget passed (the reported bug), and an arbitrary
113
+ * label, which is what a future caller is most likely to reach for.
114
+ */
115
+ const LEGENDS: Array<[string, string]> = [
116
+ ['the canonical "Time"', "Time"],
117
+ ["an empty legend", ""],
118
+ ["an arbitrary legend", "Timestamp"],
119
+ ["a legend with markup-ish characters", "Time (UTC) <>"],
120
+ ];
121
+
122
+ interface ChartUnderTest {
123
+ name: string;
124
+ /** CSS selector for the SVG geometry this chart type draws per series. */
125
+ markSelector: string;
126
+ render: (xAxis: ChartXAxis) => React.ReactElement;
127
+ }
128
+
129
+ const CHARTS: Array<ChartUnderTest> = [
130
+ {
131
+ name: "LineChart",
132
+ markSelector: "path.recharts-line-curve",
133
+ render: (xAxis: ChartXAxis): React.ReactElement => {
134
+ return (
135
+ <LineChartElement
136
+ data={buildSeries()}
137
+ xAxis={xAxis}
138
+ yAxis={Y_AXIS}
139
+ curve={ChartCurve.MONOTONE}
140
+ heightInPx={300}
141
+ showLegend={false}
142
+ sync={false}
143
+ syncid="x-axis-key-test"
144
+ />
145
+ );
146
+ },
147
+ },
148
+ {
149
+ name: "AreaChart",
150
+ markSelector: "path.recharts-area-area",
151
+ render: (xAxis: ChartXAxis): React.ReactElement => {
152
+ return (
153
+ <AreaChartElement
154
+ data={buildSeries()}
155
+ xAxis={xAxis}
156
+ yAxis={Y_AXIS}
157
+ curve={ChartCurve.MONOTONE}
158
+ heightInPx={300}
159
+ showLegend={false}
160
+ sync={false}
161
+ syncid="x-axis-key-test"
162
+ />
163
+ );
164
+ },
165
+ },
166
+ {
167
+ /*
168
+ * Bars are drawn through a custom `shape` renderer, so the mark is a bare
169
+ * <path> under recharts' own rectangle group rather than a classed one.
170
+ */
171
+ name: "BarChart",
172
+ markSelector: "g.recharts-bar-rectangle path",
173
+ render: (xAxis: ChartXAxis): React.ReactElement => {
174
+ return (
175
+ <BarChartElement
176
+ data={buildSeries()}
177
+ xAxis={xAxis}
178
+ yAxis={Y_AXIS}
179
+ heightInPx={300}
180
+ showLegend={false}
181
+ sync={false}
182
+ syncid="x-axis-key-test"
183
+ />
184
+ );
185
+ },
186
+ },
187
+ ];
188
+
189
+ afterEach((): void => {
190
+ cleanup();
191
+ });
192
+
193
+ describe("Chart x-axis data key", () => {
194
+ test("the canonical key is the literal recharts reads", (): void => {
195
+ /*
196
+ * Pinned, not derived. The wrappers and DataPointUtil both import this
197
+ * constant, so a rename would keep them agreeing with each other while
198
+ * silently changing the shape handed to recharts.
199
+ */
200
+ expect(CHART_DATA_POINT_X_AXIS_KEY).toBe("Time");
201
+ });
202
+
203
+ for (const chart of CHARTS) {
204
+ describe(chart.name, () => {
205
+ test.each(LEGENDS)(
206
+ `draws its series with %s`,
207
+ (_label: string, legend: string): void => {
208
+ const { container } = render(chart.render(buildXAxis(legend)));
209
+
210
+ const marks: Array<Element> = Array.from(
211
+ container.querySelectorAll(chart.markSelector),
212
+ );
213
+
214
+ expect(marks.length).toBeGreaterThan(0);
215
+
216
+ /*
217
+ * Present-but-empty geometry is the exact failure mode here: a
218
+ * <path> with no `d` renders nothing at all, so the element count
219
+ * alone would not have caught the bug.
220
+ */
221
+ for (const mark of marks) {
222
+ expect(mark.getAttribute("d")).toBeTruthy();
223
+ }
224
+ },
225
+ );
226
+
227
+ test("draws the same geometry whatever the legend says", (): void => {
228
+ const canonical: RenderResultContainer = render(
229
+ chart.render(buildXAxis("Time")),
230
+ );
231
+ const canonicalPaths: Array<string | null> = Array.from(
232
+ canonical.container.querySelectorAll(chart.markSelector),
233
+ ).map((element: Element) => {
234
+ return element.getAttribute("d");
235
+ });
236
+
237
+ cleanup();
238
+
239
+ const other: RenderResultContainer = render(
240
+ chart.render(buildXAxis("")),
241
+ );
242
+ const otherPaths: Array<string | null> = Array.from(
243
+ other.container.querySelectorAll(chart.markSelector),
244
+ ).map((element: Element) => {
245
+ return element.getAttribute("d");
246
+ });
247
+
248
+ expect(otherPaths).toEqual(canonicalPaths);
249
+ });
250
+ });
251
+ }
252
+ });
253
+
254
+ interface RenderResultContainer {
255
+ container: HTMLElement;
256
+ }