@oneuptime/common 12.0.19 → 12.0.21

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 (124) hide show
  1. package/Models/AnalyticsModels/ChangeEvent.ts +331 -0
  2. package/Models/AnalyticsModels/Index.ts +2 -0
  3. package/Server/API/EnterpriseLicenseAPI.ts +94 -19
  4. package/Server/EnvironmentConfig.ts +15 -5
  5. package/Server/Infrastructure/Postgres/SchemaMigrations/1789000000000-AddUserSlackAndMicrosoftTeams.ts +42 -0
  6. package/Server/Infrastructure/Postgres/SchemaMigrations/Index.ts +0 -2
  7. package/Server/Services/ChangeEventService.ts +11 -0
  8. package/Server/Services/EnterpriseLicenseService.ts +3 -3
  9. package/Server/Services/Index.ts +2 -0
  10. package/Server/Services/ProjectService.ts +104 -0
  11. package/Server/Utils/Attribution.ts +11 -11
  12. package/Server/Utils/Marketing/MarketingEventWebhook.ts +4 -5
  13. package/Server/Utils/Response.ts +2 -2
  14. package/Server/Utils/StartServer.ts +2 -2
  15. package/Server/Utils/Telemetry/ChangeEventRow.ts +228 -0
  16. package/Server/Views/Partials/AnalyticsConsent.ejs +441 -0
  17. package/Tests/App/Dashboard/AlertEpisodeViewFields.test.tsx +465 -0
  18. package/Tests/App/Dashboard/DashboardChartWidgetZoom.test.tsx +366 -0
  19. package/Tests/App/Dashboard/DiscoveryScanWizardValidation.test.tsx +408 -0
  20. package/Tests/App/Dashboard/EventOverlayHook.test.tsx +249 -0
  21. package/Tests/App/Dashboard/IncidentEpisodeViewFields.test.tsx +474 -0
  22. package/Tests/App/Dashboard/InvestigationArtifacts.test.tsx +315 -0
  23. package/Tests/App/Dashboard/InvestigationDrawer.test.tsx +258 -0
  24. package/Tests/App/Dashboard/MetricChartsCompare.test.tsx +171 -0
  25. package/Tests/App/Dashboard/MetricSeriesInvestigateMenu.test.tsx +368 -0
  26. package/Tests/App/Dashboard/MetricViewCompare.test.tsx +233 -0
  27. package/Tests/Server/API/EnterpriseLicenseReportUserCount.test.ts +763 -0
  28. package/Tests/Server/Infrastructure/Postgres/AddUserSlackAndMicrosoftTeamsMigration.test.ts +236 -0
  29. package/Tests/Server/Services/ProjectServiceChangePlan.test.ts +138 -0
  30. package/Tests/Server/Services/ProjectServiceCreateSubscriptionStarted.test.ts +516 -0
  31. package/Tests/Server/Utils/AI/SRE/Insights/Detectors/ExceptionIdentity.test.ts +224 -0
  32. package/Tests/Server/Utils/Attribution.test.ts +4 -5
  33. package/Tests/Server/Utils/Marketing/MarketingEventUtil.test.ts +2 -2
  34. package/Tests/Server/Utils/Monitor/MonitorCriteriaExpectationBuilder.test.ts +300 -0
  35. package/Tests/Server/Utils/ResponseRenderAnalytics.test.ts +53 -0
  36. package/Tests/Server/Views/AnalyticsConsentPartial.test.ts +241 -0
  37. package/Tests/UI/Components/Charts/ChartGhostSeries.test.tsx +103 -0
  38. package/Tests/UI/Components/Charts/ChartTooltipSorted.test.tsx +129 -0
  39. package/Tests/UI/Components/Charts/TooltipEntries.test.ts +191 -0
  40. package/Tests/UI/Components/Detail/DetailFieldErrors.test.tsx +347 -0
  41. package/Tests/UI/Components/Forms/BasicFormStepValidation.test.tsx +392 -0
  42. package/Tests/UI/Components/Forms/ValidationFormSteps.test.ts +549 -0
  43. package/Tests/Utils/EnterpriseLicenseSync.test.ts +562 -0
  44. package/Tests/Utils/Telemetry/CrossSignalScope.test.ts +13 -2
  45. package/Types/Analytics/RevenueEvent.ts +1 -0
  46. package/Types/AnalyticsDatabase/AnalyticsTableName.ts +7 -0
  47. package/Types/Marketing/Attribution.ts +9 -15
  48. package/Types/Marketing/MarketingEvent.ts +19 -12
  49. package/UI/Components/Charts/Area/AreaChart.tsx +23 -1
  50. package/UI/Components/Charts/Bar/BarChart.tsx +8 -1
  51. package/UI/Components/Charts/ChartGroup/ChartGroup.tsx +16 -1
  52. package/UI/Components/Charts/ChartLibrary/AreaChart/AreaChart.tsx +208 -97
  53. package/UI/Components/Charts/ChartLibrary/BarChart/BarChart.tsx +26 -3
  54. package/UI/Components/Charts/ChartLibrary/LineChart/LineChart.tsx +232 -118
  55. package/UI/Components/Charts/ChartLibrary/Utils/TooltipEntries.ts +120 -0
  56. package/UI/Components/Charts/Line/LineChart.tsx +23 -1
  57. package/UI/Components/Detail/Detail.tsx +18 -1
  58. package/Utils/EnterpriseLicense/EnterpriseLicenseSync.ts +244 -0
  59. package/Utils/Metrics/MetricExplorerUrl.ts +119 -5
  60. package/Utils/Telemetry/CrossSignalScope.ts +19 -10
  61. package/build/dist/Models/AnalyticsModels/ChangeEvent.js +298 -0
  62. package/build/dist/Models/AnalyticsModels/ChangeEvent.js.map +1 -0
  63. package/build/dist/Models/AnalyticsModels/Index.js +2 -0
  64. package/build/dist/Models/AnalyticsModels/Index.js.map +1 -1
  65. package/build/dist/Server/API/EnterpriseLicenseAPI.js +66 -12
  66. package/build/dist/Server/API/EnterpriseLicenseAPI.js.map +1 -1
  67. package/build/dist/Server/EnvironmentConfig.js +14 -5
  68. package/build/dist/Server/EnvironmentConfig.js.map +1 -1
  69. package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/1789000000000-AddUserSlackAndMicrosoftTeams.js +39 -0
  70. package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/1789000000000-AddUserSlackAndMicrosoftTeams.js.map +1 -1
  71. package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/Index.js +0 -2
  72. package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/Index.js.map +1 -1
  73. package/build/dist/Server/Services/ChangeEventService.js +9 -0
  74. package/build/dist/Server/Services/ChangeEventService.js.map +1 -0
  75. package/build/dist/Server/Services/EnterpriseLicenseService.js +3 -3
  76. package/build/dist/Server/Services/Index.js +2 -0
  77. package/build/dist/Server/Services/Index.js.map +1 -1
  78. package/build/dist/Server/Services/ProjectService.js +88 -0
  79. package/build/dist/Server/Services/ProjectService.js.map +1 -1
  80. package/build/dist/Server/Utils/Attribution.js +6 -6
  81. package/build/dist/Server/Utils/Marketing/MarketingEventWebhook.js +4 -5
  82. package/build/dist/Server/Utils/Marketing/MarketingEventWebhook.js.map +1 -1
  83. package/build/dist/Server/Utils/Response.js +2 -2
  84. package/build/dist/Server/Utils/Response.js.map +1 -1
  85. package/build/dist/Server/Utils/StartServer.js +2 -2
  86. package/build/dist/Server/Utils/StartServer.js.map +1 -1
  87. package/build/dist/Server/Utils/Telemetry/ChangeEventRow.js +150 -0
  88. package/build/dist/Server/Utils/Telemetry/ChangeEventRow.js.map +1 -0
  89. package/build/dist/Types/Analytics/RevenueEvent.js +1 -0
  90. package/build/dist/Types/Analytics/RevenueEvent.js.map +1 -1
  91. package/build/dist/Types/AnalyticsDatabase/AnalyticsTableName.js +7 -0
  92. package/build/dist/Types/AnalyticsDatabase/AnalyticsTableName.js.map +1 -1
  93. package/build/dist/Types/Marketing/Attribution.js +9 -15
  94. package/build/dist/Types/Marketing/Attribution.js.map +1 -1
  95. package/build/dist/Types/Marketing/MarketingEvent.js +12 -4
  96. package/build/dist/Types/Marketing/MarketingEvent.js.map +1 -1
  97. package/build/dist/UI/Components/Charts/Area/AreaChart.js +10 -2
  98. package/build/dist/UI/Components/Charts/Area/AreaChart.js.map +1 -1
  99. package/build/dist/UI/Components/Charts/Bar/BarChart.js +9 -1
  100. package/build/dist/UI/Components/Charts/Bar/BarChart.js.map +1 -1
  101. package/build/dist/UI/Components/Charts/ChartGroup/ChartGroup.js +9 -1
  102. package/build/dist/UI/Components/Charts/ChartGroup/ChartGroup.js.map +1 -1
  103. package/build/dist/UI/Components/Charts/ChartLibrary/AreaChart/AreaChart.js +114 -51
  104. package/build/dist/UI/Components/Charts/ChartLibrary/AreaChart/AreaChart.js.map +1 -1
  105. package/build/dist/UI/Components/Charts/ChartLibrary/BarChart/BarChart.js +35 -23
  106. package/build/dist/UI/Components/Charts/ChartLibrary/BarChart/BarChart.js.map +1 -1
  107. package/build/dist/UI/Components/Charts/ChartLibrary/LineChart/LineChart.js +122 -55
  108. package/build/dist/UI/Components/Charts/ChartLibrary/LineChart/LineChart.js.map +1 -1
  109. package/build/dist/UI/Components/Charts/ChartLibrary/Utils/TooltipEntries.js +86 -0
  110. package/build/dist/UI/Components/Charts/ChartLibrary/Utils/TooltipEntries.js.map +1 -0
  111. package/build/dist/UI/Components/Charts/Line/LineChart.js +10 -2
  112. package/build/dist/UI/Components/Charts/Line/LineChart.js.map +1 -1
  113. package/build/dist/UI/Components/Detail/Detail.js +17 -1
  114. package/build/dist/UI/Components/Detail/Detail.js.map +1 -1
  115. package/build/dist/Utils/EnterpriseLicense/EnterpriseLicenseSync.js +174 -0
  116. package/build/dist/Utils/EnterpriseLicense/EnterpriseLicenseSync.js.map +1 -0
  117. package/build/dist/Utils/Metrics/MetricExplorerUrl.js +81 -0
  118. package/build/dist/Utils/Metrics/MetricExplorerUrl.js.map +1 -1
  119. package/build/dist/Utils/Telemetry/CrossSignalScope.js +14 -8
  120. package/build/dist/Utils/Telemetry/CrossSignalScope.js.map +1 -1
  121. package/package.json +1 -1
  122. package/Server/Infrastructure/Postgres/SchemaMigrations/1788900000000-RedactStoredMonitorIngestSecrets.ts +0 -117
  123. package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/1788900000000-RedactStoredMonitorIngestSecrets.js +0 -106
  124. package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/1788900000000-RedactStoredMonitorIngestSecrets.js.map +0 -1
@@ -0,0 +1,366 @@
1
+ import "@testing-library/jest-dom";
2
+ import {
3
+ afterEach,
4
+ beforeEach,
5
+ describe,
6
+ expect,
7
+ jest,
8
+ test,
9
+ } from "@jest/globals";
10
+ import {
11
+ RenderResult,
12
+ act,
13
+ cleanup,
14
+ fireEvent,
15
+ render,
16
+ screen,
17
+ waitFor,
18
+ } from "@testing-library/react";
19
+ import * as React from "react";
20
+ import getJestMockFunction, { MockFunction } from "../../MockType";
21
+
22
+ /*
23
+ * Drag-to-zoom on a dashboard chart widget is the "investigate this
24
+ * spike" entry point: selecting a window must narrow THIS widget only
25
+ * (never the dashboard-wide range), refetch for the narrowed window, and
26
+ * offer a way back (Reset) and a way deeper (Open in Explorer, carrying
27
+ * the zoomed window). Edit mode must not zoom — the drag gesture belongs
28
+ * to widget move/resize there, and series pivots would navigate away from
29
+ * unsaved dashboard changes.
30
+ */
31
+
32
+ const fetchResultsMock: MockFunction = getJestMockFunction();
33
+ const metricChartsRenderMock: MockFunction = getJestMockFunction();
34
+
35
+ /*
36
+ * The arrow wrappers are load bearing: jest.mock is hoisted above the
37
+ * compiled requires, so the mock variables above are still unassigned when
38
+ * the factory runs. Dereferencing them lazily, at call time, is what makes
39
+ * this work.
40
+ */
41
+ jest.mock(
42
+ "../../../../App/FeatureSet/Dashboard/src/Components/Metrics/Utils/Metrics",
43
+ () => {
44
+ return {
45
+ __esModule: true,
46
+ default: {
47
+ fetchResults: (...args: Array<any>) => {
48
+ return fetchResultsMock(...args);
49
+ },
50
+ clearQueryTopNOverridesForScope: () => {
51
+ return undefined;
52
+ },
53
+ },
54
+ };
55
+ },
56
+ );
57
+
58
+ /*
59
+ * The widget's job under test is which window it ASKS for and which
60
+ * affordances it hands down; a real recharts surface in jsdom would test
61
+ * neither.
62
+ */
63
+ jest.mock(
64
+ "../../../../App/FeatureSet/Dashboard/src/Components/Metrics/MetricCharts",
65
+ () => {
66
+ return {
67
+ __esModule: true,
68
+ default: (props: Record<string, unknown>): React.ReactElement => {
69
+ metricChartsRenderMock(props);
70
+ return React.createElement(
71
+ "div",
72
+ { "data-testid": "metric-charts" },
73
+ "chart",
74
+ );
75
+ },
76
+ };
77
+ },
78
+ );
79
+
80
+ import DashboardChartComponentElement from "../../../../App/FeatureSet/Dashboard/src/Components/Dashboard/Components/DashboardChartComponent";
81
+ import { DashboardBaseComponentProps } from "../../../../App/FeatureSet/Dashboard/src/Components/Dashboard/Components/DashboardBaseComponent";
82
+ import InBetween from "../../../Types/BaseDatabase/InBetween";
83
+ import DashboardChartType from "../../../Types/Dashboard/Chart/ChartType";
84
+ import DashboardComponentType from "../../../Types/Dashboard/DashboardComponentType";
85
+ import DashboardChartComponent from "../../../Types/Dashboard/DashboardComponents/DashboardChartComponent";
86
+ import DashboardViewConfig from "../../../Types/Dashboard/DashboardViewConfig";
87
+ import { ObjectType } from "../../../Types/JSON";
88
+ import MetricsAggregationType from "../../../Types/Metrics/MetricsAggregationType";
89
+ import MetricViewData from "../../../Types/Metrics/MetricViewData";
90
+ import ObjectID from "../../../Types/ObjectID";
91
+ import RangeStartAndEndDateTime from "../../../Types/Time/RangeStartAndEndDateTime";
92
+ import TimeRange from "../../../Types/Time/TimeRange";
93
+
94
+ const COMPONENT_ID: ObjectID = new ObjectID(
95
+ "22222222-1111-4111-8111-222222222222",
96
+ );
97
+
98
+ const DASHBOARD_START: Date = new Date("2026-08-20T09:00:00.000Z");
99
+ const DASHBOARD_END: Date = new Date("2026-08-20T10:00:00.000Z");
100
+ const ZOOM_START: Date = new Date("2026-08-20T09:20:00.000Z");
101
+ const ZOOM_END: Date = new Date("2026-08-20T09:35:00.000Z");
102
+
103
+ /*
104
+ * A CUSTOM range pins the window to fixed instants — a relative range
105
+ * would resolve against the wall clock and make the window assertions
106
+ * untestable.
107
+ */
108
+ const DASHBOARD_RANGE: RangeStartAndEndDateTime = {
109
+ range: TimeRange.CUSTOM,
110
+ startAndEndDate: new InBetween<Date>(DASHBOARD_START, DASHBOARD_END),
111
+ };
112
+
113
+ const DASHBOARD_VIEW_CONFIG: DashboardViewConfig = {
114
+ _type: ObjectType.DashboardViewConfig,
115
+ components: [],
116
+ heightInDashboardUnits: 60,
117
+ };
118
+
119
+ function buildBaseProps(
120
+ overrides: Partial<DashboardBaseComponentProps> = {},
121
+ ): DashboardBaseComponentProps {
122
+ return {
123
+ componentId: COMPONENT_ID,
124
+ isEditMode: false,
125
+ isSelected: false,
126
+ key: "chart-widget",
127
+ onComponentUpdate: (): void => {
128
+ // The widget under test never writes back through this.
129
+ },
130
+ totalCurrentDashboardWidthInPx: 1200,
131
+ dashboardCanvasTopInPx: 0,
132
+ dashboardCanvasLeftInPx: 0,
133
+ dashboardCanvasWidthInPx: 1200,
134
+ dashboardCanvasHeightInPx: 800,
135
+ dashboardComponentHeightInPx: 320,
136
+ dashboardComponentWidthInPx: 480,
137
+ dashboardViewConfig: DASHBOARD_VIEW_CONFIG,
138
+ dashboardStartAndEndDate: DASHBOARD_RANGE,
139
+ metricTypes: [],
140
+ refreshTick: 0,
141
+ variables: undefined,
142
+ ...overrides,
143
+ } as DashboardBaseComponentProps;
144
+ }
145
+
146
+ function buildChartComponent(): DashboardChartComponent {
147
+ return {
148
+ _type: ObjectType.DashboardComponent,
149
+ componentId: COMPONENT_ID,
150
+ componentType: DashboardComponentType.Chart,
151
+ topInDashboardUnits: 0,
152
+ leftInDashboardUnits: 0,
153
+ widthInDashboardUnits: 6,
154
+ heightInDashboardUnits: 3,
155
+ minWidthInDashboardUnits: 6,
156
+ minHeightInDashboardUnits: 3,
157
+ arguments: {
158
+ chartTitle: "CPU by host",
159
+ chartType: DashboardChartType.Line,
160
+ metricQueryConfig: {
161
+ metricAliasData: { metricVariable: "a" },
162
+ metricQueryData: {
163
+ filterData: {
164
+ metricName: "cpu.usage",
165
+ aggegationType: MetricsAggregationType.Avg,
166
+ },
167
+ groupByAttributeKeys: ["host.name"],
168
+ },
169
+ },
170
+ },
171
+ } as unknown as DashboardChartComponent;
172
+ }
173
+
174
+ function renderWidget(
175
+ overrides: Partial<DashboardBaseComponentProps> = {},
176
+ ): RenderResult {
177
+ return render(
178
+ <DashboardChartComponentElement
179
+ {...buildBaseProps(overrides)}
180
+ component={buildChartComponent()}
181
+ />,
182
+ );
183
+ }
184
+
185
+ interface CapturedChartProps {
186
+ metricViewData: MetricViewData;
187
+ metricResults?: Array<Record<string, unknown>> | undefined;
188
+ onTimeRangeSelect?: ((startTime: Date, endTime: Date) => void) | undefined;
189
+ enableSeriesActions?: boolean | undefined;
190
+ }
191
+
192
+ function lastChartProps(): CapturedChartProps {
193
+ const calls: Array<Array<unknown>> = metricChartsRenderMock.mock.calls;
194
+ return calls[calls.length - 1]?.[0] as CapturedChartProps;
195
+ }
196
+
197
+ interface CapturedFetchArgs {
198
+ metricViewData: MetricViewData;
199
+ }
200
+
201
+ function lastFetchArgs(): CapturedFetchArgs {
202
+ const calls: Array<Array<unknown>> = fetchResultsMock.mock.calls;
203
+ return calls[calls.length - 1]?.[0] as CapturedFetchArgs;
204
+ }
205
+
206
+ beforeEach(() => {
207
+ fetchResultsMock.mockReset();
208
+ metricChartsRenderMock.mockReset();
209
+ fetchResultsMock.mockReturnValue(
210
+ Promise.resolve([{ data: [], truncated: false }]),
211
+ );
212
+ });
213
+
214
+ afterEach(() => {
215
+ cleanup();
216
+ });
217
+
218
+ describe("dashboard chart widget drag-to-zoom", () => {
219
+ test("drag-selecting a window refetches THIS widget for that window and shows the zoom bar", async () => {
220
+ renderWidget();
221
+
222
+ await waitFor(() => {
223
+ expect(screen.getByTestId("metric-charts")).toBeInTheDocument();
224
+ });
225
+
226
+ // Initial fetch asks for the dashboard's window.
227
+ expect(lastFetchArgs().metricViewData.startAndEndDate?.startValue).toEqual(
228
+ DASHBOARD_START,
229
+ );
230
+
231
+ const initialProps: CapturedChartProps = lastChartProps();
232
+ expect(initialProps.onTimeRangeSelect).toBeDefined();
233
+ expect(initialProps.enableSeriesActions).toBe(true);
234
+ // A pinned Custom dashboard window is not a rolling token.
235
+ expect(initialProps.metricViewData.rangeToken).toBe(TimeRange.CUSTOM);
236
+
237
+ const fetchCountBeforeZoom: number = fetchResultsMock.mock.calls.length;
238
+
239
+ act(() => {
240
+ initialProps.onTimeRangeSelect?.(ZOOM_START, ZOOM_END);
241
+ });
242
+
243
+ // The zoom bar names the window and offers the escape hatches.
244
+ expect(screen.getByText(/Zoomed:/)).toBeInTheDocument();
245
+ expect(screen.getByText("Reset")).toBeInTheDocument();
246
+ expect(screen.getByText("Open in Explorer")).toBeInTheDocument();
247
+
248
+ await waitFor(() => {
249
+ expect(fetchResultsMock.mock.calls.length).toBeGreaterThan(
250
+ fetchCountBeforeZoom,
251
+ );
252
+ });
253
+
254
+ // The refetch and the chart both use the ZOOMED window…
255
+ expect(lastFetchArgs().metricViewData.startAndEndDate?.startValue).toEqual(
256
+ ZOOM_START,
257
+ );
258
+ expect(lastFetchArgs().metricViewData.startAndEndDate?.endValue).toEqual(
259
+ ZOOM_END,
260
+ );
261
+ // …and a zoomed window is a deliberate pin: no rolling range token.
262
+ await waitFor(() => {
263
+ expect(lastChartProps().metricViewData.rangeToken).toBeUndefined();
264
+ });
265
+ });
266
+
267
+ test("Reset returns to the dashboard's window", async () => {
268
+ renderWidget();
269
+
270
+ await waitFor(() => {
271
+ expect(screen.getByTestId("metric-charts")).toBeInTheDocument();
272
+ });
273
+
274
+ act(() => {
275
+ lastChartProps().onTimeRangeSelect?.(ZOOM_START, ZOOM_END);
276
+ });
277
+
278
+ expect(screen.getByText(/Zoomed:/)).toBeInTheDocument();
279
+
280
+ fireEvent.click(screen.getByText("Reset"));
281
+
282
+ expect(screen.queryByText(/Zoomed:/)).toBeNull();
283
+
284
+ await waitFor(() => {
285
+ expect(
286
+ lastFetchArgs().metricViewData.startAndEndDate?.startValue,
287
+ ).toEqual(DASHBOARD_START);
288
+ });
289
+ expect(lastChartProps().metricViewData.rangeToken).toBe(TimeRange.CUSTOM);
290
+ });
291
+
292
+ test("a stale fetch response can never overwrite a newer window's data", async () => {
293
+ /*
294
+ * Zoom then Reset puts two aggregate calls in flight; if the SLOWER
295
+ * (zoom) response lands last, its data must be discarded — otherwise
296
+ * the chart paints the zoomed window's series under the dashboard
297
+ * window's axis.
298
+ */
299
+ const pendingFetches: Array<(value: unknown) => void> = [];
300
+ fetchResultsMock.mockImplementation(() => {
301
+ return new Promise((resolve: (value: unknown) => void) => {
302
+ pendingFetches.push(resolve);
303
+ });
304
+ });
305
+
306
+ renderWidget();
307
+
308
+ await waitFor(() => {
309
+ expect(pendingFetches.length).toBe(1);
310
+ });
311
+ act(() => {
312
+ pendingFetches[0]?.([{ data: [], truncated: false }]);
313
+ });
314
+ await waitFor(() => {
315
+ expect(screen.getByTestId("metric-charts")).toBeInTheDocument();
316
+ });
317
+
318
+ act(() => {
319
+ lastChartProps().onTimeRangeSelect?.(ZOOM_START, ZOOM_END);
320
+ });
321
+ await waitFor(() => {
322
+ expect(pendingFetches.length).toBe(2);
323
+ });
324
+
325
+ fireEvent.click(screen.getByText("Reset"));
326
+ await waitFor(() => {
327
+ expect(pendingFetches.length).toBe(3);
328
+ });
329
+
330
+ const staleZoomResults: Array<Record<string, unknown>> = [
331
+ { data: [{ marker: "stale-zoom" }], truncated: false },
332
+ ];
333
+ const freshResetResults: Array<Record<string, unknown>> = [
334
+ { data: [], truncated: false },
335
+ ];
336
+
337
+ // The newer (reset) fetch answers first…
338
+ act(() => {
339
+ pendingFetches[2]?.(freshResetResults);
340
+ });
341
+ await waitFor(() => {
342
+ expect(lastChartProps().metricResults).toBe(freshResetResults);
343
+ });
344
+
345
+ // …then the stale zoom fetch answers last and must be ignored.
346
+ act(() => {
347
+ pendingFetches[1]?.(staleZoomResults);
348
+ });
349
+ await act(async () => {
350
+ await Promise.resolve();
351
+ });
352
+ expect(lastChartProps().metricResults).toBe(freshResetResults);
353
+ });
354
+
355
+ test("edit mode disables zoom and series navigation", async () => {
356
+ renderWidget({ isEditMode: true });
357
+
358
+ await waitFor(() => {
359
+ expect(screen.getByTestId("metric-charts")).toBeInTheDocument();
360
+ });
361
+
362
+ const captured: CapturedChartProps = lastChartProps();
363
+ expect(captured.onTimeRangeSelect).toBeUndefined();
364
+ expect(captured.enableSeriesActions).toBe(false);
365
+ });
366
+ });