@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.
- package/Models/AnalyticsModels/ChangeEvent.ts +331 -0
- package/Models/AnalyticsModels/Index.ts +2 -0
- package/Server/API/EnterpriseLicenseAPI.ts +94 -19
- package/Server/EnvironmentConfig.ts +15 -5
- package/Server/Infrastructure/Postgres/SchemaMigrations/1789000000000-AddUserSlackAndMicrosoftTeams.ts +42 -0
- package/Server/Infrastructure/Postgres/SchemaMigrations/Index.ts +0 -2
- package/Server/Services/ChangeEventService.ts +11 -0
- package/Server/Services/EnterpriseLicenseService.ts +3 -3
- package/Server/Services/Index.ts +2 -0
- package/Server/Services/ProjectService.ts +104 -0
- package/Server/Utils/Attribution.ts +11 -11
- package/Server/Utils/Marketing/MarketingEventWebhook.ts +4 -5
- package/Server/Utils/Response.ts +2 -2
- package/Server/Utils/StartServer.ts +2 -2
- package/Server/Utils/Telemetry/ChangeEventRow.ts +228 -0
- package/Server/Views/Partials/AnalyticsConsent.ejs +441 -0
- package/Tests/App/Dashboard/AlertEpisodeViewFields.test.tsx +465 -0
- package/Tests/App/Dashboard/DashboardChartWidgetZoom.test.tsx +366 -0
- package/Tests/App/Dashboard/DiscoveryScanWizardValidation.test.tsx +408 -0
- package/Tests/App/Dashboard/EventOverlayHook.test.tsx +249 -0
- package/Tests/App/Dashboard/IncidentEpisodeViewFields.test.tsx +474 -0
- package/Tests/App/Dashboard/InvestigationArtifacts.test.tsx +315 -0
- package/Tests/App/Dashboard/InvestigationDrawer.test.tsx +258 -0
- package/Tests/App/Dashboard/MetricChartsCompare.test.tsx +171 -0
- package/Tests/App/Dashboard/MetricSeriesInvestigateMenu.test.tsx +368 -0
- package/Tests/App/Dashboard/MetricViewCompare.test.tsx +233 -0
- package/Tests/Server/API/EnterpriseLicenseReportUserCount.test.ts +763 -0
- package/Tests/Server/Infrastructure/Postgres/AddUserSlackAndMicrosoftTeamsMigration.test.ts +236 -0
- package/Tests/Server/Services/ProjectServiceChangePlan.test.ts +138 -0
- package/Tests/Server/Services/ProjectServiceCreateSubscriptionStarted.test.ts +516 -0
- package/Tests/Server/Utils/AI/SRE/Insights/Detectors/ExceptionIdentity.test.ts +224 -0
- package/Tests/Server/Utils/Attribution.test.ts +4 -5
- package/Tests/Server/Utils/Marketing/MarketingEventUtil.test.ts +2 -2
- package/Tests/Server/Utils/Monitor/MonitorCriteriaExpectationBuilder.test.ts +300 -0
- package/Tests/Server/Utils/ResponseRenderAnalytics.test.ts +53 -0
- package/Tests/Server/Views/AnalyticsConsentPartial.test.ts +241 -0
- package/Tests/UI/Components/Charts/ChartGhostSeries.test.tsx +103 -0
- package/Tests/UI/Components/Charts/ChartTooltipSorted.test.tsx +129 -0
- package/Tests/UI/Components/Charts/TooltipEntries.test.ts +191 -0
- package/Tests/UI/Components/Detail/DetailFieldErrors.test.tsx +347 -0
- package/Tests/UI/Components/Forms/BasicFormStepValidation.test.tsx +392 -0
- package/Tests/UI/Components/Forms/ValidationFormSteps.test.ts +549 -0
- package/Tests/Utils/EnterpriseLicenseSync.test.ts +562 -0
- package/Tests/Utils/Telemetry/CrossSignalScope.test.ts +13 -2
- package/Types/Analytics/RevenueEvent.ts +1 -0
- package/Types/AnalyticsDatabase/AnalyticsTableName.ts +7 -0
- package/Types/Marketing/Attribution.ts +9 -15
- package/Types/Marketing/MarketingEvent.ts +19 -12
- package/UI/Components/Charts/Area/AreaChart.tsx +23 -1
- package/UI/Components/Charts/Bar/BarChart.tsx +8 -1
- package/UI/Components/Charts/ChartGroup/ChartGroup.tsx +16 -1
- package/UI/Components/Charts/ChartLibrary/AreaChart/AreaChart.tsx +208 -97
- package/UI/Components/Charts/ChartLibrary/BarChart/BarChart.tsx +26 -3
- package/UI/Components/Charts/ChartLibrary/LineChart/LineChart.tsx +232 -118
- package/UI/Components/Charts/ChartLibrary/Utils/TooltipEntries.ts +120 -0
- package/UI/Components/Charts/Line/LineChart.tsx +23 -1
- package/UI/Components/Detail/Detail.tsx +18 -1
- package/Utils/EnterpriseLicense/EnterpriseLicenseSync.ts +244 -0
- package/Utils/Metrics/MetricExplorerUrl.ts +119 -5
- package/Utils/Telemetry/CrossSignalScope.ts +19 -10
- package/build/dist/Models/AnalyticsModels/ChangeEvent.js +298 -0
- package/build/dist/Models/AnalyticsModels/ChangeEvent.js.map +1 -0
- package/build/dist/Models/AnalyticsModels/Index.js +2 -0
- package/build/dist/Models/AnalyticsModels/Index.js.map +1 -1
- package/build/dist/Server/API/EnterpriseLicenseAPI.js +66 -12
- package/build/dist/Server/API/EnterpriseLicenseAPI.js.map +1 -1
- package/build/dist/Server/EnvironmentConfig.js +14 -5
- package/build/dist/Server/EnvironmentConfig.js.map +1 -1
- package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/1789000000000-AddUserSlackAndMicrosoftTeams.js +39 -0
- package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/1789000000000-AddUserSlackAndMicrosoftTeams.js.map +1 -1
- package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/Index.js +0 -2
- package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/Index.js.map +1 -1
- package/build/dist/Server/Services/ChangeEventService.js +9 -0
- package/build/dist/Server/Services/ChangeEventService.js.map +1 -0
- package/build/dist/Server/Services/EnterpriseLicenseService.js +3 -3
- package/build/dist/Server/Services/Index.js +2 -0
- package/build/dist/Server/Services/Index.js.map +1 -1
- package/build/dist/Server/Services/ProjectService.js +88 -0
- package/build/dist/Server/Services/ProjectService.js.map +1 -1
- package/build/dist/Server/Utils/Attribution.js +6 -6
- package/build/dist/Server/Utils/Marketing/MarketingEventWebhook.js +4 -5
- package/build/dist/Server/Utils/Marketing/MarketingEventWebhook.js.map +1 -1
- package/build/dist/Server/Utils/Response.js +2 -2
- package/build/dist/Server/Utils/Response.js.map +1 -1
- package/build/dist/Server/Utils/StartServer.js +2 -2
- package/build/dist/Server/Utils/StartServer.js.map +1 -1
- package/build/dist/Server/Utils/Telemetry/ChangeEventRow.js +150 -0
- package/build/dist/Server/Utils/Telemetry/ChangeEventRow.js.map +1 -0
- package/build/dist/Types/Analytics/RevenueEvent.js +1 -0
- package/build/dist/Types/Analytics/RevenueEvent.js.map +1 -1
- package/build/dist/Types/AnalyticsDatabase/AnalyticsTableName.js +7 -0
- package/build/dist/Types/AnalyticsDatabase/AnalyticsTableName.js.map +1 -1
- package/build/dist/Types/Marketing/Attribution.js +9 -15
- package/build/dist/Types/Marketing/Attribution.js.map +1 -1
- package/build/dist/Types/Marketing/MarketingEvent.js +12 -4
- package/build/dist/Types/Marketing/MarketingEvent.js.map +1 -1
- package/build/dist/UI/Components/Charts/Area/AreaChart.js +10 -2
- package/build/dist/UI/Components/Charts/Area/AreaChart.js.map +1 -1
- package/build/dist/UI/Components/Charts/Bar/BarChart.js +9 -1
- package/build/dist/UI/Components/Charts/Bar/BarChart.js.map +1 -1
- package/build/dist/UI/Components/Charts/ChartGroup/ChartGroup.js +9 -1
- package/build/dist/UI/Components/Charts/ChartGroup/ChartGroup.js.map +1 -1
- package/build/dist/UI/Components/Charts/ChartLibrary/AreaChart/AreaChart.js +114 -51
- package/build/dist/UI/Components/Charts/ChartLibrary/AreaChart/AreaChart.js.map +1 -1
- package/build/dist/UI/Components/Charts/ChartLibrary/BarChart/BarChart.js +35 -23
- package/build/dist/UI/Components/Charts/ChartLibrary/BarChart/BarChart.js.map +1 -1
- package/build/dist/UI/Components/Charts/ChartLibrary/LineChart/LineChart.js +122 -55
- package/build/dist/UI/Components/Charts/ChartLibrary/LineChart/LineChart.js.map +1 -1
- package/build/dist/UI/Components/Charts/ChartLibrary/Utils/TooltipEntries.js +86 -0
- package/build/dist/UI/Components/Charts/ChartLibrary/Utils/TooltipEntries.js.map +1 -0
- package/build/dist/UI/Components/Charts/Line/LineChart.js +10 -2
- package/build/dist/UI/Components/Charts/Line/LineChart.js.map +1 -1
- package/build/dist/UI/Components/Detail/Detail.js +17 -1
- package/build/dist/UI/Components/Detail/Detail.js.map +1 -1
- package/build/dist/Utils/EnterpriseLicense/EnterpriseLicenseSync.js +174 -0
- package/build/dist/Utils/EnterpriseLicense/EnterpriseLicenseSync.js.map +1 -0
- package/build/dist/Utils/Metrics/MetricExplorerUrl.js +81 -0
- package/build/dist/Utils/Metrics/MetricExplorerUrl.js.map +1 -1
- package/build/dist/Utils/Telemetry/CrossSignalScope.js +14 -8
- package/build/dist/Utils/Telemetry/CrossSignalScope.js.map +1 -1
- package/package.json +1 -1
- package/Server/Infrastructure/Postgres/SchemaMigrations/1788900000000-RedactStoredMonitorIngestSecrets.ts +0 -117
- package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/1788900000000-RedactStoredMonitorIngestSecrets.js +0 -106
- package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/1788900000000-RedactStoredMonitorIngestSecrets.js.map +0 -1
|
@@ -0,0 +1,549 @@
|
|
|
1
|
+
import Validation from "../../../../UI/Components/Forms/Validation";
|
|
2
|
+
import Field from "../../../../UI/Components/Forms/Types/Field";
|
|
3
|
+
import Fields from "../../../../UI/Components/Forms/Types/Fields";
|
|
4
|
+
import FormFieldSchemaType from "../../../../UI/Components/Forms/Types/FormFieldSchemaType";
|
|
5
|
+
import FormValues from "../../../../UI/Components/Forms/Types/FormValues";
|
|
6
|
+
import Dictionary from "../../../../Types/Dictionary";
|
|
7
|
+
import { JSONObject } from "../../../../Types/JSON";
|
|
8
|
+
import { describe, expect, test } from "@jest/globals";
|
|
9
|
+
|
|
10
|
+
/*
|
|
11
|
+
* Step scoping is the contract that makes a wizard's "Next" button a gate.
|
|
12
|
+
*
|
|
13
|
+
* BasicForm reuses one validator for every step and hands it the id of the
|
|
14
|
+
* step being submitted; Validation.validate then skips every field that lives
|
|
15
|
+
* somewhere else. That is what lets a field-level error render inline, under
|
|
16
|
+
* its own input, on the step the operator is actually looking at — instead of
|
|
17
|
+
* as one combined banner at the end (issue #3377, the Network Device Discovery
|
|
18
|
+
* Scan wizard).
|
|
19
|
+
*
|
|
20
|
+
* Nothing tested this branch before. ValidationJSON.test.ts is the only other
|
|
21
|
+
* direct test of Validation.validate and it never passes currentFormStepId at
|
|
22
|
+
* all, so the whole step filter — and every consequence of it, including the
|
|
23
|
+
* ones below that are deliberately surprising — was unpinned.
|
|
24
|
+
*
|
|
25
|
+
* Several tests here assert CURRENT behaviour that is not obviously desirable
|
|
26
|
+
* (customValidation skipped for absent keys; customValidation overwriting a
|
|
27
|
+
* built-in message; onValidate output not being step-scoped). They are here
|
|
28
|
+
* precisely because production code depends on those semantics today, so a
|
|
29
|
+
* change to them should be a deliberate, visible decision rather than a
|
|
30
|
+
* silent one.
|
|
31
|
+
*/
|
|
32
|
+
|
|
33
|
+
interface WizardEntity extends JSONObject {
|
|
34
|
+
target?: string | undefined;
|
|
35
|
+
probe?: string | undefined;
|
|
36
|
+
community?: string | undefined;
|
|
37
|
+
interval?: number | undefined;
|
|
38
|
+
repeat?: boolean | undefined;
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
const STEP_ONE: string = "scan-target";
|
|
42
|
+
const STEP_TWO: string = "snmp";
|
|
43
|
+
const STEP_THREE: string = "schedule";
|
|
44
|
+
|
|
45
|
+
type MakeFieldFunction = (
|
|
46
|
+
name: string,
|
|
47
|
+
overrides?: Partial<Field<WizardEntity>>,
|
|
48
|
+
) => Field<WizardEntity>;
|
|
49
|
+
|
|
50
|
+
const makeField: MakeFieldFunction = (
|
|
51
|
+
name: string,
|
|
52
|
+
overrides?: Partial<Field<WizardEntity>>,
|
|
53
|
+
): Field<WizardEntity> => {
|
|
54
|
+
return {
|
|
55
|
+
name: name,
|
|
56
|
+
title: name,
|
|
57
|
+
field: { [name]: true },
|
|
58
|
+
fieldType: FormFieldSchemaType.Text,
|
|
59
|
+
...overrides,
|
|
60
|
+
} as Field<WizardEntity>;
|
|
61
|
+
};
|
|
62
|
+
|
|
63
|
+
type RunValidateFunction = (args: {
|
|
64
|
+
formFields: Fields<WizardEntity>;
|
|
65
|
+
values: FormValues<WizardEntity>;
|
|
66
|
+
currentFormStepId?: string | null | undefined;
|
|
67
|
+
onValidate?: ((values: FormValues<WizardEntity>) => JSONObject) | undefined;
|
|
68
|
+
}) => Dictionary<string>;
|
|
69
|
+
|
|
70
|
+
const runValidate: RunValidateFunction = (args: {
|
|
71
|
+
formFields: Fields<WizardEntity>;
|
|
72
|
+
values: FormValues<WizardEntity>;
|
|
73
|
+
currentFormStepId?: string | null | undefined;
|
|
74
|
+
onValidate?: ((values: FormValues<WizardEntity>) => JSONObject) | undefined;
|
|
75
|
+
}): Dictionary<string> => {
|
|
76
|
+
return Validation.validate<WizardEntity>({
|
|
77
|
+
formFields: args.formFields,
|
|
78
|
+
values: args.values,
|
|
79
|
+
onValidate: args.onValidate,
|
|
80
|
+
currentFormStepId: args.currentFormStepId,
|
|
81
|
+
});
|
|
82
|
+
};
|
|
83
|
+
|
|
84
|
+
/*
|
|
85
|
+
* A three-step wizard shaped like the Discovery one: a validated text field on
|
|
86
|
+
* step 1, an optional field on step 2, and a numeric field on step 3.
|
|
87
|
+
*/
|
|
88
|
+
const WIZARD_FIELDS: Fields<WizardEntity> = [
|
|
89
|
+
makeField("target", {
|
|
90
|
+
stepId: STEP_ONE,
|
|
91
|
+
required: true,
|
|
92
|
+
customValidation: (values: FormValues<WizardEntity>): string | null => {
|
|
93
|
+
const value: string = (values.target || "").toString().trim();
|
|
94
|
+
|
|
95
|
+
if (!value) {
|
|
96
|
+
return null;
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
return value.includes("/") ? null : `"${value}" is not a subnet.`;
|
|
100
|
+
},
|
|
101
|
+
}),
|
|
102
|
+
makeField("probe", { stepId: STEP_ONE, required: true }),
|
|
103
|
+
makeField("community", { stepId: STEP_TWO, required: false }),
|
|
104
|
+
makeField("interval", {
|
|
105
|
+
stepId: STEP_THREE,
|
|
106
|
+
required: true,
|
|
107
|
+
fieldType: FormFieldSchemaType.Number,
|
|
108
|
+
validation: { minValue: 15 },
|
|
109
|
+
}),
|
|
110
|
+
];
|
|
111
|
+
|
|
112
|
+
describe("Validation.validate — a field is judged on its own step", () => {
|
|
113
|
+
test("an invalid value on the current step is reported", () => {
|
|
114
|
+
const errors: Dictionary<string> = runValidate({
|
|
115
|
+
formFields: WIZARD_FIELDS,
|
|
116
|
+
values: { target: "9876543210", probe: "probe-1" },
|
|
117
|
+
currentFormStepId: STEP_ONE,
|
|
118
|
+
});
|
|
119
|
+
|
|
120
|
+
expect(errors["target"]).toBe('"9876543210" is not a subnet.');
|
|
121
|
+
});
|
|
122
|
+
|
|
123
|
+
test("a valid value on the current step clears the step", () => {
|
|
124
|
+
const errors: Dictionary<string> = runValidate({
|
|
125
|
+
formFields: WIZARD_FIELDS,
|
|
126
|
+
values: { target: "192.168.1.0/24", probe: "probe-1" },
|
|
127
|
+
currentFormStepId: STEP_ONE,
|
|
128
|
+
});
|
|
129
|
+
|
|
130
|
+
expect(errors).toEqual({});
|
|
131
|
+
});
|
|
132
|
+
|
|
133
|
+
test("a required field left empty on the current step is reported", () => {
|
|
134
|
+
const errors: Dictionary<string> = runValidate({
|
|
135
|
+
formFields: WIZARD_FIELDS,
|
|
136
|
+
values: { target: "192.168.1.0/24" },
|
|
137
|
+
currentFormStepId: STEP_ONE,
|
|
138
|
+
});
|
|
139
|
+
|
|
140
|
+
expect(errors["probe"]).toBe("probe is required.");
|
|
141
|
+
});
|
|
142
|
+
|
|
143
|
+
test("validation.minValue is applied on its own step", () => {
|
|
144
|
+
const errors: Dictionary<string> = runValidate({
|
|
145
|
+
formFields: WIZARD_FIELDS,
|
|
146
|
+
values: { interval: 5, repeat: true },
|
|
147
|
+
currentFormStepId: STEP_THREE,
|
|
148
|
+
});
|
|
149
|
+
|
|
150
|
+
expect(errors["interval"]).toContain("should not be less than 15");
|
|
151
|
+
});
|
|
152
|
+
});
|
|
153
|
+
|
|
154
|
+
describe("Validation.validate — other steps are not judged", () => {
|
|
155
|
+
/*
|
|
156
|
+
* The half of the contract that keeps the button honest in the other
|
|
157
|
+
* direction: an error the operator cannot see must never block the step they
|
|
158
|
+
* are on. There is only one render path for a field error — the field
|
|
159
|
+
* itself — so an error keyed to a field on another step would make Next a
|
|
160
|
+
* dead button with nothing on screen to explain it.
|
|
161
|
+
*/
|
|
162
|
+
test("a broken value on a LATER step does not block the current one", () => {
|
|
163
|
+
const errors: Dictionary<string> = runValidate({
|
|
164
|
+
formFields: WIZARD_FIELDS,
|
|
165
|
+
values: { target: "192.168.1.0/24", probe: "probe-1", interval: 1 },
|
|
166
|
+
currentFormStepId: STEP_ONE,
|
|
167
|
+
});
|
|
168
|
+
|
|
169
|
+
expect(errors).toEqual({});
|
|
170
|
+
});
|
|
171
|
+
|
|
172
|
+
test("a broken value on an EARLIER step does not block the current one", () => {
|
|
173
|
+
const errors: Dictionary<string> = runValidate({
|
|
174
|
+
formFields: WIZARD_FIELDS,
|
|
175
|
+
values: { target: "not-a-subnet", interval: 60, repeat: true },
|
|
176
|
+
currentFormStepId: STEP_THREE,
|
|
177
|
+
});
|
|
178
|
+
|
|
179
|
+
expect(errors).toEqual({});
|
|
180
|
+
});
|
|
181
|
+
|
|
182
|
+
test("a required field on another step is not demanded early", () => {
|
|
183
|
+
const errors: Dictionary<string> = runValidate({
|
|
184
|
+
formFields: WIZARD_FIELDS,
|
|
185
|
+
values: {},
|
|
186
|
+
currentFormStepId: STEP_TWO,
|
|
187
|
+
});
|
|
188
|
+
|
|
189
|
+
// Both step-one fields are required and both are missing.
|
|
190
|
+
expect(errors).toEqual({});
|
|
191
|
+
});
|
|
192
|
+
|
|
193
|
+
test("each step reports only its own field", () => {
|
|
194
|
+
const values: FormValues<WizardEntity> = {
|
|
195
|
+
target: "not-a-subnet",
|
|
196
|
+
probe: "probe-1",
|
|
197
|
+
interval: 1,
|
|
198
|
+
repeat: true,
|
|
199
|
+
};
|
|
200
|
+
|
|
201
|
+
expect(
|
|
202
|
+
Object.keys(
|
|
203
|
+
runValidate({
|
|
204
|
+
formFields: WIZARD_FIELDS,
|
|
205
|
+
values: values,
|
|
206
|
+
currentFormStepId: STEP_ONE,
|
|
207
|
+
}),
|
|
208
|
+
),
|
|
209
|
+
).toEqual(["target"]);
|
|
210
|
+
|
|
211
|
+
expect(
|
|
212
|
+
Object.keys(
|
|
213
|
+
runValidate({
|
|
214
|
+
formFields: WIZARD_FIELDS,
|
|
215
|
+
values: values,
|
|
216
|
+
currentFormStepId: STEP_THREE,
|
|
217
|
+
}),
|
|
218
|
+
),
|
|
219
|
+
).toEqual(["interval"]);
|
|
220
|
+
});
|
|
221
|
+
});
|
|
222
|
+
|
|
223
|
+
describe("Validation.validate — a form with no steps judges everything", () => {
|
|
224
|
+
test.each([
|
|
225
|
+
["currentFormStepId omitted", undefined],
|
|
226
|
+
["currentFormStepId null", null],
|
|
227
|
+
["currentFormStepId empty string", ""],
|
|
228
|
+
])(
|
|
229
|
+
"%s validates every field regardless of stepId",
|
|
230
|
+
(_label: string, currentFormStepId: string | null | undefined) => {
|
|
231
|
+
const errors: Dictionary<string> = runValidate({
|
|
232
|
+
formFields: WIZARD_FIELDS,
|
|
233
|
+
values: { target: "not-a-subnet", interval: 1 },
|
|
234
|
+
currentFormStepId: currentFormStepId,
|
|
235
|
+
});
|
|
236
|
+
|
|
237
|
+
expect(errors["target"]).toBe('"not-a-subnet" is not a subnet.');
|
|
238
|
+
expect(errors["probe"]).toBe("probe is required.");
|
|
239
|
+
expect(errors["interval"]).toContain("should not be less than 15");
|
|
240
|
+
},
|
|
241
|
+
);
|
|
242
|
+
});
|
|
243
|
+
|
|
244
|
+
describe("Validation.validate — a field with no stepId at all", () => {
|
|
245
|
+
/*
|
|
246
|
+
* Current behaviour, pinned because it is a trap rather than a design: once
|
|
247
|
+
* currentFormStepId is set, `field.stepId !== currentFormStepId` is true for
|
|
248
|
+
* an undefined stepId on EVERY step, so the field is validated on none of
|
|
249
|
+
* them. BasicForm's render filter uses the same comparison, so the field is
|
|
250
|
+
* also drawn on no step — the two agree, and the field is simply inert.
|
|
251
|
+
* setAllTouched does NOT agree (it additionally requires a truthy stepId, so
|
|
252
|
+
* the field is marked touched everywhere), which is harmless only because
|
|
253
|
+
* nothing ever produces an error for it.
|
|
254
|
+
*
|
|
255
|
+
* The practical consequence: in a stepped form, a field that forgets its
|
|
256
|
+
* stepId silently stops being validated. That is why
|
|
257
|
+
* App/Tests/Dashboard/NetworkFormStepsInvariants.test.ts exists, and why the
|
|
258
|
+
* shared field helpers stamp every field they return.
|
|
259
|
+
*/
|
|
260
|
+
test("is skipped on every step once the form is stepped", () => {
|
|
261
|
+
const orphan: Fields<WizardEntity> = [
|
|
262
|
+
makeField("target", { required: true }),
|
|
263
|
+
];
|
|
264
|
+
|
|
265
|
+
for (const stepId of [STEP_ONE, STEP_TWO, STEP_THREE]) {
|
|
266
|
+
expect(
|
|
267
|
+
runValidate({
|
|
268
|
+
formFields: orphan,
|
|
269
|
+
values: {},
|
|
270
|
+
currentFormStepId: stepId,
|
|
271
|
+
}),
|
|
272
|
+
).toEqual({});
|
|
273
|
+
}
|
|
274
|
+
});
|
|
275
|
+
|
|
276
|
+
test("is validated normally when the form has no steps", () => {
|
|
277
|
+
const orphan: Fields<WizardEntity> = [
|
|
278
|
+
makeField("target", { required: true }),
|
|
279
|
+
];
|
|
280
|
+
|
|
281
|
+
expect(
|
|
282
|
+
runValidate({ formFields: orphan, values: {}, currentFormStepId: null }),
|
|
283
|
+
).toEqual({ target: "target is required." });
|
|
284
|
+
});
|
|
285
|
+
});
|
|
286
|
+
|
|
287
|
+
describe("Validation.validate — showIf short-circuits before every rule", () => {
|
|
288
|
+
test("a hidden field is not required, even on its own step", () => {
|
|
289
|
+
const fields: Fields<WizardEntity> = [
|
|
290
|
+
makeField("interval", {
|
|
291
|
+
stepId: STEP_THREE,
|
|
292
|
+
required: true,
|
|
293
|
+
showIf: (values: FormValues<WizardEntity>): boolean => {
|
|
294
|
+
return Boolean(values.repeat);
|
|
295
|
+
},
|
|
296
|
+
}),
|
|
297
|
+
];
|
|
298
|
+
|
|
299
|
+
expect(
|
|
300
|
+
runValidate({
|
|
301
|
+
formFields: fields,
|
|
302
|
+
values: { repeat: false },
|
|
303
|
+
currentFormStepId: STEP_THREE,
|
|
304
|
+
}),
|
|
305
|
+
).toEqual({});
|
|
306
|
+
});
|
|
307
|
+
|
|
308
|
+
test("a hidden field's customValidation does not run", () => {
|
|
309
|
+
const fields: Fields<WizardEntity> = [
|
|
310
|
+
makeField("interval", {
|
|
311
|
+
stepId: STEP_THREE,
|
|
312
|
+
showIf: (): boolean => {
|
|
313
|
+
return false;
|
|
314
|
+
},
|
|
315
|
+
customValidation: (): string | null => {
|
|
316
|
+
return "should never be seen";
|
|
317
|
+
},
|
|
318
|
+
}),
|
|
319
|
+
];
|
|
320
|
+
|
|
321
|
+
expect(
|
|
322
|
+
runValidate({
|
|
323
|
+
formFields: fields,
|
|
324
|
+
values: { interval: 1 },
|
|
325
|
+
currentFormStepId: STEP_THREE,
|
|
326
|
+
}),
|
|
327
|
+
).toEqual({});
|
|
328
|
+
});
|
|
329
|
+
|
|
330
|
+
test("the same field is validated once it is revealed", () => {
|
|
331
|
+
const fields: Fields<WizardEntity> = [
|
|
332
|
+
makeField("interval", {
|
|
333
|
+
stepId: STEP_THREE,
|
|
334
|
+
required: true,
|
|
335
|
+
showIf: (values: FormValues<WizardEntity>): boolean => {
|
|
336
|
+
return Boolean(values.repeat);
|
|
337
|
+
},
|
|
338
|
+
}),
|
|
339
|
+
];
|
|
340
|
+
|
|
341
|
+
expect(
|
|
342
|
+
runValidate({
|
|
343
|
+
formFields: fields,
|
|
344
|
+
values: { repeat: true },
|
|
345
|
+
currentFormStepId: STEP_THREE,
|
|
346
|
+
}),
|
|
347
|
+
).toEqual({ interval: "interval is required." });
|
|
348
|
+
});
|
|
349
|
+
});
|
|
350
|
+
|
|
351
|
+
describe("Validation.validate — customValidation semantics a wizard depends on", () => {
|
|
352
|
+
/*
|
|
353
|
+
* Pinned because App/FeatureSet/Dashboard/src/Components/Billing/PayAsYouGo.tsx
|
|
354
|
+
* relies on it (and works around it with getDefaultValue), and because it is
|
|
355
|
+
* the reason the Discovery validators short-circuit on empty instead of
|
|
356
|
+
* returning the parser's own "is required" text.
|
|
357
|
+
*/
|
|
358
|
+
test("does not run for a field whose key is absent from the values", () => {
|
|
359
|
+
const fields: Fields<WizardEntity> = [
|
|
360
|
+
makeField("target", {
|
|
361
|
+
stepId: STEP_ONE,
|
|
362
|
+
required: true,
|
|
363
|
+
customValidation: (): string | null => {
|
|
364
|
+
return "custom message";
|
|
365
|
+
},
|
|
366
|
+
}),
|
|
367
|
+
];
|
|
368
|
+
|
|
369
|
+
expect(
|
|
370
|
+
runValidate({
|
|
371
|
+
formFields: fields,
|
|
372
|
+
values: {},
|
|
373
|
+
currentFormStepId: STEP_ONE,
|
|
374
|
+
}),
|
|
375
|
+
).toEqual({ target: "target is required." });
|
|
376
|
+
});
|
|
377
|
+
|
|
378
|
+
test("does run once the key exists, even when the value is empty", () => {
|
|
379
|
+
const fields: Fields<WizardEntity> = [
|
|
380
|
+
makeField("target", {
|
|
381
|
+
stepId: STEP_ONE,
|
|
382
|
+
required: true,
|
|
383
|
+
customValidation: (): string | null => {
|
|
384
|
+
return "custom message";
|
|
385
|
+
},
|
|
386
|
+
}),
|
|
387
|
+
];
|
|
388
|
+
|
|
389
|
+
expect(
|
|
390
|
+
runValidate({
|
|
391
|
+
formFields: fields,
|
|
392
|
+
values: { target: "" },
|
|
393
|
+
currentFormStepId: STEP_ONE,
|
|
394
|
+
}),
|
|
395
|
+
).toEqual({ target: "custom message" });
|
|
396
|
+
});
|
|
397
|
+
|
|
398
|
+
test("runs last, so it overwrites a built-in message for the same field", () => {
|
|
399
|
+
/*
|
|
400
|
+
* Which is why a validator that speaks up for empty input replaces the
|
|
401
|
+
* short "X is required." with its own, longer sentence. Returning null for
|
|
402
|
+
* empty is not politeness — it is how the required message survives.
|
|
403
|
+
*/
|
|
404
|
+
const fields: Fields<WizardEntity> = [
|
|
405
|
+
makeField("target", {
|
|
406
|
+
stepId: STEP_ONE,
|
|
407
|
+
required: true,
|
|
408
|
+
validation: { minLength: 20 },
|
|
409
|
+
customValidation: (): string | null => {
|
|
410
|
+
return "custom wins";
|
|
411
|
+
},
|
|
412
|
+
}),
|
|
413
|
+
];
|
|
414
|
+
|
|
415
|
+
expect(
|
|
416
|
+
runValidate({
|
|
417
|
+
formFields: fields,
|
|
418
|
+
values: { target: "short" },
|
|
419
|
+
currentFormStepId: STEP_ONE,
|
|
420
|
+
}),
|
|
421
|
+
).toEqual({ target: "custom wins" });
|
|
422
|
+
});
|
|
423
|
+
|
|
424
|
+
test("returning null leaves the built-in message in place", () => {
|
|
425
|
+
const fields: Fields<WizardEntity> = [
|
|
426
|
+
makeField("target", {
|
|
427
|
+
stepId: STEP_ONE,
|
|
428
|
+
required: true,
|
|
429
|
+
validation: { minLength: 20 },
|
|
430
|
+
customValidation: (): string | null => {
|
|
431
|
+
return null;
|
|
432
|
+
},
|
|
433
|
+
}),
|
|
434
|
+
];
|
|
435
|
+
|
|
436
|
+
expect(
|
|
437
|
+
runValidate({
|
|
438
|
+
formFields: fields,
|
|
439
|
+
values: { target: "short" },
|
|
440
|
+
currentFormStepId: STEP_ONE,
|
|
441
|
+
})["target"],
|
|
442
|
+
).toContain("cannot be less than 20 characters");
|
|
443
|
+
});
|
|
444
|
+
|
|
445
|
+
test("is handed the whole form's values, not just its own field", () => {
|
|
446
|
+
let seen: FormValues<WizardEntity> | null = null;
|
|
447
|
+
|
|
448
|
+
const fields: Fields<WizardEntity> = [
|
|
449
|
+
makeField("interval", {
|
|
450
|
+
stepId: STEP_THREE,
|
|
451
|
+
customValidation: (values: FormValues<WizardEntity>): string | null => {
|
|
452
|
+
seen = values;
|
|
453
|
+
return null;
|
|
454
|
+
},
|
|
455
|
+
}),
|
|
456
|
+
];
|
|
457
|
+
|
|
458
|
+
runValidate({
|
|
459
|
+
formFields: fields,
|
|
460
|
+
values: { interval: 60, repeat: true, target: "192.168.1.0/24" },
|
|
461
|
+
currentFormStepId: STEP_THREE,
|
|
462
|
+
});
|
|
463
|
+
|
|
464
|
+
/*
|
|
465
|
+
* Cross-step values are readable, which is how a toggle on another step
|
|
466
|
+
* can switch a validator off.
|
|
467
|
+
*/
|
|
468
|
+
expect(seen).toEqual({
|
|
469
|
+
interval: 60,
|
|
470
|
+
repeat: true,
|
|
471
|
+
target: "192.168.1.0/24",
|
|
472
|
+
});
|
|
473
|
+
});
|
|
474
|
+
|
|
475
|
+
test("a customValidation on another step's field never runs", () => {
|
|
476
|
+
let calls: number = 0;
|
|
477
|
+
|
|
478
|
+
const fields: Fields<WizardEntity> = [
|
|
479
|
+
makeField("target", {
|
|
480
|
+
stepId: STEP_ONE,
|
|
481
|
+
customValidation: (): string | null => {
|
|
482
|
+
calls++;
|
|
483
|
+
return "nope";
|
|
484
|
+
},
|
|
485
|
+
}),
|
|
486
|
+
];
|
|
487
|
+
|
|
488
|
+
runValidate({
|
|
489
|
+
formFields: fields,
|
|
490
|
+
values: { target: "anything" },
|
|
491
|
+
currentFormStepId: STEP_TWO,
|
|
492
|
+
});
|
|
493
|
+
|
|
494
|
+
expect(calls).toBe(0);
|
|
495
|
+
});
|
|
496
|
+
});
|
|
497
|
+
|
|
498
|
+
describe("Validation.validate — onValidate output is NOT step-scoped", () => {
|
|
499
|
+
/*
|
|
500
|
+
* Current behaviour, pinned deliberately. The per-field loop is filtered by
|
|
501
|
+
* step; the form-level onValidate result is spread over the top of it
|
|
502
|
+
* unfiltered. A key naming a field on another step therefore blocks the
|
|
503
|
+
* button with nothing on screen to explain it.
|
|
504
|
+
*
|
|
505
|
+
* Unreachable today — ModelTable/CardModelDetail expose formSteps but no
|
|
506
|
+
* onValidate, and the single production onValidate in the repo is on a
|
|
507
|
+
* stepless form — so this is a latent trap rather than a live bug. Anyone
|
|
508
|
+
* adding an onValidate to a wizard should see this test fail their
|
|
509
|
+
* assumptions before their users do.
|
|
510
|
+
*/
|
|
511
|
+
test("its keys survive the step filter", () => {
|
|
512
|
+
const errors: Dictionary<string> = runValidate({
|
|
513
|
+
formFields: WIZARD_FIELDS,
|
|
514
|
+
values: { target: "192.168.1.0/24", probe: "probe-1" },
|
|
515
|
+
currentFormStepId: STEP_ONE,
|
|
516
|
+
onValidate: (): JSONObject => {
|
|
517
|
+
return { interval: "an error about a field three steps away" };
|
|
518
|
+
},
|
|
519
|
+
});
|
|
520
|
+
|
|
521
|
+
expect(errors["interval"]).toBe("an error about a field three steps away");
|
|
522
|
+
});
|
|
523
|
+
|
|
524
|
+
test("its keys win over a per-field message for the same field", () => {
|
|
525
|
+
const errors: Dictionary<string> = runValidate({
|
|
526
|
+
formFields: WIZARD_FIELDS,
|
|
527
|
+
values: { target: "9876543210", probe: "probe-1" },
|
|
528
|
+
currentFormStepId: STEP_ONE,
|
|
529
|
+
onValidate: (): JSONObject => {
|
|
530
|
+
return { target: "form-level message" };
|
|
531
|
+
},
|
|
532
|
+
});
|
|
533
|
+
|
|
534
|
+
expect(errors["target"]).toBe("form-level message");
|
|
535
|
+
});
|
|
536
|
+
|
|
537
|
+
test("an empty result adds nothing", () => {
|
|
538
|
+
const errors: Dictionary<string> = runValidate({
|
|
539
|
+
formFields: WIZARD_FIELDS,
|
|
540
|
+
values: { target: "192.168.1.0/24", probe: "probe-1" },
|
|
541
|
+
currentFormStepId: STEP_ONE,
|
|
542
|
+
onValidate: (): JSONObject => {
|
|
543
|
+
return {};
|
|
544
|
+
},
|
|
545
|
+
});
|
|
546
|
+
|
|
547
|
+
expect(errors).toEqual({});
|
|
548
|
+
});
|
|
549
|
+
});
|