@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,392 @@
1
+ import BasicForm from "../../../../UI/Components/Forms/BasicForm";
2
+ import Fields from "../../../../UI/Components/Forms/Types/Fields";
3
+ import FormFieldSchemaType from "../../../../UI/Components/Forms/Types/FormFieldSchemaType";
4
+ import FormValues from "../../../../UI/Components/Forms/Types/FormValues";
5
+ import { FormStep } from "../../../../UI/Components/Forms/Types/FormStep";
6
+ import ScanTargetUtil from "../../../../Utils/NetworkDiscovery/ScanTargetUtil";
7
+ import getJestMockFunction, { MockFunction } from "../../../MockType";
8
+ import "@testing-library/jest-dom";
9
+ import { cleanup, render, screen, waitFor } from "@testing-library/react";
10
+ import userEvent from "@testing-library/user-event";
11
+ import { UserEvent } from "@testing-library/user-event/dist/types/setup/setup";
12
+ import * as React from "react";
13
+ import { afterEach, describe, expect, test } from "@jest/globals";
14
+
15
+ /*
16
+ * "Next" has to be a gate, not a page-turn.
17
+ *
18
+ * Issue #3377, against the Create Network Device Discovery Scan wizard: every
19
+ * step accepted whatever was typed into it. An invalid scan target cleared
20
+ * step 1, cleared step 2, cleared step 3, and then failed the final submit
21
+ * with one combined banner rendered above the SCHEDULE fields — describing a
22
+ * value entered two steps earlier, with nothing marking the field at fault.
23
+ *
24
+ * The fix is a field-level validator, and the reason a field-level validator
25
+ * is sufficient is the behaviour pinned here: BasicForm validates only the
26
+ * current step, refuses to advance while that step has an error, and renders
27
+ * the message inline under the offending input. Nothing in the repo tested
28
+ * that — no test clicked a Next button — so the whole mechanism the fix leans
29
+ * on was unpinned.
30
+ *
31
+ * The wizard below is shaped like the real one and uses the real scan-target
32
+ * parser, so a regression in either the step machinery or the parser shows up
33
+ * here as a step that turns when it should not.
34
+ */
35
+
36
+ /*
37
+ * Type without userEvent's default inter-keystroke await, and give the typing
38
+ * tests explicit timeouts. Both are load-bearing and both are borrowed from
39
+ * BasicForm.test.tsx: Input syncs a display value from props onto the DOM
40
+ * node, so a keystroke arriving before React has committed the previous one is
41
+ * destroyed rather than delayed; and re-validating once per keystroke has no
42
+ * headroom against jest's 5s default on a loaded runner.
43
+ */
44
+ function setupUser(): UserEvent {
45
+ return userEvent.setup({ delay: null });
46
+ }
47
+
48
+ const TEST_TIMEOUT_MS: number = 30000;
49
+
50
+ const SUBMIT_BUTTON_TEXT: string = "Create Scan";
51
+
52
+ const STEPS: Array<FormStep<FormValues<any>>> = [
53
+ { title: "Scan Target", id: "scan-target" },
54
+ { title: "SNMP Credentials", id: "snmp" },
55
+ ];
56
+
57
+ const FIELDS: Fields<FormValues<any>> = [
58
+ {
59
+ field: { cidr: true },
60
+ title: "Scan Target",
61
+ stepId: "scan-target",
62
+ fieldType: FormFieldSchemaType.Text,
63
+ required: true,
64
+ dataTestId: "cidr",
65
+ /*
66
+ * The real validator's shape: delegate to the parser, and stay silent on
67
+ * empty so `required` keeps its own shorter message.
68
+ */
69
+ customValidation: (values: FormValues<any>): string | null => {
70
+ const raw: string =
71
+ values["cidr"] === undefined || values["cidr"] === null
72
+ ? ""
73
+ : String(values["cidr"]);
74
+
75
+ /*
76
+ * Empty is `required`'s; blank is the parser's. validateRequired
77
+ * measures the UNTRIMMED string, so a lone space satisfies it — see the
78
+ * whitespace test below.
79
+ */
80
+ if (raw === "") {
81
+ return null;
82
+ }
83
+
84
+ return ScanTargetUtil.getValidationError(raw.trim());
85
+ },
86
+ },
87
+ {
88
+ field: { snmpCommunityString: true },
89
+ title: "SNMP Community String",
90
+ stepId: "snmp",
91
+ fieldType: FormFieldSchemaType.Text,
92
+ required: true,
93
+ dataTestId: "community",
94
+ },
95
+ ];
96
+
97
+ interface RenderWizardResult {
98
+ handleSubmit: MockFunction;
99
+ user: UserEvent;
100
+ }
101
+
102
+ function renderWizard(): RenderWizardResult {
103
+ const handleSubmit: MockFunction = getJestMockFunction();
104
+
105
+ render(
106
+ <BasicForm
107
+ id="discovery-wizard"
108
+ fields={FIELDS}
109
+ steps={STEPS}
110
+ initialValues={{}}
111
+ onSubmit={handleSubmit}
112
+ submitButtonText={SUBMIT_BUTTON_TEXT}
113
+ />,
114
+ );
115
+
116
+ return { handleSubmit, user: setupUser() };
117
+ }
118
+
119
+ /*
120
+ * The button's LABEL is "Next" until the last step, but its test id is always
121
+ * the submitButtonText it was given — see BasicForm's Button props. Reading it
122
+ * by test id therefore works on every step.
123
+ */
124
+ function stepButton(): HTMLElement {
125
+ return screen.getByTestId(SUBMIT_BUTTON_TEXT);
126
+ }
127
+
128
+ function errorMessages(): Array<string> {
129
+ return screen
130
+ .queryAllByTestId("error-message")
131
+ .map((element: HTMLElement): string => {
132
+ return element.textContent || "";
133
+ });
134
+ }
135
+
136
+ function onStepOne(): boolean {
137
+ return screen.queryByTestId("cidr") !== null;
138
+ }
139
+
140
+ function onStepTwo(): boolean {
141
+ return screen.queryByTestId("community") !== null;
142
+ }
143
+
144
+ describe("BasicForm wizard — an invalid field holds its own step", () => {
145
+ afterEach(() => {
146
+ cleanup();
147
+ });
148
+
149
+ test(
150
+ "the reported case: a phone-number-shaped scan target does not advance",
151
+ async () => {
152
+ const { handleSubmit, user }: RenderWizardResult = renderWizard();
153
+
154
+ await user.type(screen.getByTestId("cidr"), "9876543210");
155
+ await user.click(stepButton());
156
+
157
+ await waitFor(() => {
158
+ expect(errorMessages().join(" ")).toContain(
159
+ "is not a valid scan target",
160
+ );
161
+ });
162
+
163
+ // Still on step one, and nothing was submitted.
164
+ expect(onStepOne()).toBe(true);
165
+ expect(onStepTwo()).toBe(false);
166
+ expect(handleSubmit).not.toHaveBeenCalled();
167
+ },
168
+ TEST_TIMEOUT_MS,
169
+ );
170
+
171
+ test(
172
+ "the message is rendered inline, on the step that owns the field",
173
+ async () => {
174
+ const { user }: RenderWizardResult = renderWizard();
175
+
176
+ await user.type(screen.getByTestId("cidr"), "10.0.0.256");
177
+ await user.click(stepButton());
178
+
179
+ await waitFor(() => {
180
+ expect(errorMessages().join(" ")).toContain("between 0 and 255");
181
+ });
182
+
183
+ /*
184
+ * Exactly one message, attached to the one field on this step — not a
185
+ * combined banner, and not a message about a field the operator cannot
186
+ * see.
187
+ */
188
+ expect(errorMessages()).toHaveLength(1);
189
+ expect(screen.getByTestId("cidr")).toBeInTheDocument();
190
+ },
191
+ TEST_TIMEOUT_MS,
192
+ );
193
+
194
+ test(
195
+ "an over-sized but well-formed target is stopped at step one too",
196
+ async () => {
197
+ const { user }: RenderWizardResult = renderWizard();
198
+
199
+ // Syntactically perfect; 16.7M addresses.
200
+ await user.type(screen.getByTestId("cidr"), "10.0.0.0/8");
201
+ await user.click(stepButton());
202
+
203
+ await waitFor(() => {
204
+ expect(errorMessages().join(" ")).toContain("scan limit");
205
+ });
206
+
207
+ expect(onStepTwo()).toBe(false);
208
+ },
209
+ TEST_TIMEOUT_MS,
210
+ );
211
+
212
+ test(
213
+ "a blank field does not advance either, even though `required` passes it",
214
+ async () => {
215
+ const { handleSubmit, user }: RenderWizardResult = renderWizard();
216
+
217
+ /*
218
+ * The trap this whole distinction exists for: Validation.validateRequired
219
+ * tests `!content || content.length === 0` on the RAW string, so a single
220
+ * space has length 1 and satisfies it. No length rule fires either (the
221
+ * inferred maxLength is an upper bound, and trims to 0 anyway). If the
222
+ * field's own validator also read " " as empty, nothing on the form would
223
+ * speak — and the step would turn on a target the server rejects.
224
+ */
225
+ await user.type(screen.getByTestId("cidr"), " ");
226
+ await user.click(stepButton());
227
+
228
+ await waitFor(() => {
229
+ expect(errorMessages().join(" ")).toContain(
230
+ "A scan target is required",
231
+ );
232
+ });
233
+
234
+ expect(onStepTwo()).toBe(false);
235
+ expect(handleSubmit).not.toHaveBeenCalled();
236
+ },
237
+ TEST_TIMEOUT_MS,
238
+ );
239
+
240
+ test(
241
+ "an empty required field is reported as required, not as malformed",
242
+ async () => {
243
+ const { handleSubmit, user }: RenderWizardResult = renderWizard();
244
+
245
+ await user.click(stepButton());
246
+
247
+ await waitFor(() => {
248
+ expect(errorMessages().join(" ")).toContain("Scan Target is required.");
249
+ });
250
+
251
+ /*
252
+ * The customValidation returns null for empty precisely so this message
253
+ * survives — it runs after validateRequired and would otherwise replace
254
+ * it with the parser's longer sentence.
255
+ */
256
+ expect(errorMessages().join(" ")).not.toContain("is not a valid");
257
+ expect(onStepTwo()).toBe(false);
258
+ expect(handleSubmit).not.toHaveBeenCalled();
259
+ },
260
+ TEST_TIMEOUT_MS,
261
+ );
262
+ });
263
+
264
+ describe("BasicForm wizard — a corrected field releases the step", () => {
265
+ afterEach(() => {
266
+ cleanup();
267
+ });
268
+
269
+ test(
270
+ "correcting the value advances to the next step",
271
+ async () => {
272
+ const { user }: RenderWizardResult = renderWizard();
273
+
274
+ await user.type(screen.getByTestId("cidr"), "9876543210");
275
+ await user.click(stepButton());
276
+
277
+ await waitFor(() => {
278
+ expect(errorMessages().length).toBeGreaterThan(0);
279
+ });
280
+
281
+ await user.clear(screen.getByTestId("cidr"));
282
+ await user.type(screen.getByTestId("cidr"), "192.168.1.0/24");
283
+ await user.click(stepButton());
284
+
285
+ await waitFor(() => {
286
+ expect(onStepTwo()).toBe(true);
287
+ });
288
+
289
+ expect(onStepOne()).toBe(false);
290
+ expect(errorMessages()).toHaveLength(0);
291
+ },
292
+ TEST_TIMEOUT_MS,
293
+ );
294
+
295
+ test(
296
+ "a valid target advances first time, with no error shown on the way",
297
+ async () => {
298
+ const { user }: RenderWizardResult = renderWizard();
299
+
300
+ await user.type(screen.getByTestId("cidr"), "10.16-22.0-255.51-66");
301
+ await user.click(stepButton());
302
+
303
+ await waitFor(() => {
304
+ expect(onStepTwo()).toBe(true);
305
+ });
306
+
307
+ expect(errorMessages()).toHaveLength(0);
308
+ },
309
+ TEST_TIMEOUT_MS,
310
+ );
311
+
312
+ test(
313
+ "the whole wizard submits once every step is satisfied",
314
+ async () => {
315
+ const { handleSubmit, user }: RenderWizardResult = renderWizard();
316
+
317
+ await user.type(screen.getByTestId("cidr"), "192.168.1.0/24");
318
+ await user.click(stepButton());
319
+
320
+ await waitFor(() => {
321
+ expect(onStepTwo()).toBe(true);
322
+ });
323
+
324
+ await user.type(screen.getByTestId("community"), "public");
325
+ await user.click(stepButton());
326
+
327
+ await waitFor(() => {
328
+ expect(handleSubmit).toHaveBeenCalled();
329
+ });
330
+
331
+ expect(handleSubmit.mock.calls[0]?.[0]).toMatchObject({
332
+ cidr: "192.168.1.0/24",
333
+ snmpCommunityString: "public",
334
+ });
335
+ },
336
+ TEST_TIMEOUT_MS,
337
+ );
338
+ });
339
+
340
+ describe("BasicForm wizard — a later step's field never blocks an earlier one", () => {
341
+ afterEach(() => {
342
+ cleanup();
343
+ });
344
+
345
+ test(
346
+ "step two's required field is not demanded while on step one",
347
+ async () => {
348
+ const { user }: RenderWizardResult = renderWizard();
349
+
350
+ await user.type(screen.getByTestId("cidr"), "192.168.1.0/24");
351
+ await user.click(stepButton());
352
+
353
+ /*
354
+ * The community string is required and empty. If the step filter ever
355
+ * stopped applying, "Next" would become a dead button: the error would
356
+ * block the advance while its field is on a step that has not been
357
+ * rendered yet, so nothing on screen could explain the refusal.
358
+ */
359
+ await waitFor(() => {
360
+ expect(onStepTwo()).toBe(true);
361
+ });
362
+
363
+ expect(errorMessages()).toHaveLength(0);
364
+ },
365
+ TEST_TIMEOUT_MS,
366
+ );
367
+
368
+ test(
369
+ "step two blocks on its own field once the operator gets there",
370
+ async () => {
371
+ const { handleSubmit, user }: RenderWizardResult = renderWizard();
372
+
373
+ await user.type(screen.getByTestId("cidr"), "192.168.1.0/24");
374
+ await user.click(stepButton());
375
+
376
+ await waitFor(() => {
377
+ expect(onStepTwo()).toBe(true);
378
+ });
379
+
380
+ await user.click(stepButton());
381
+
382
+ await waitFor(() => {
383
+ expect(errorMessages().join(" ")).toContain(
384
+ "SNMP Community String is required.",
385
+ );
386
+ });
387
+
388
+ expect(handleSubmit).not.toHaveBeenCalled();
389
+ },
390
+ TEST_TIMEOUT_MS,
391
+ );
392
+ });