@featurevisor/core 2.13.0 → 2.14.0
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/CHANGELOG.md +11 -0
- package/coverage/clover.xml +906 -557
- package/coverage/coverage-final.json +11 -8
- package/coverage/lcov-report/builder/allocator.ts.html +1 -1
- package/coverage/lcov-report/builder/buildScopedConditions.ts.html +1 -1
- package/coverage/lcov-report/builder/buildScopedDatafile.ts.html +1 -1
- package/coverage/lcov-report/builder/buildScopedSegments.ts.html +1 -1
- package/coverage/lcov-report/builder/index.html +38 -8
- package/coverage/lcov-report/builder/mutateVariables.ts.html +400 -0
- package/coverage/lcov-report/builder/mutator.ts.html +796 -0
- package/coverage/lcov-report/builder/revision.ts.html +1 -1
- package/coverage/lcov-report/builder/traffic.ts.html +1 -1
- package/coverage/lcov-report/config/index.html +1 -1
- package/coverage/lcov-report/config/projectConfig.ts.html +1 -1
- package/coverage/lcov-report/datasource/adapter.ts.html +1 -1
- package/coverage/lcov-report/datasource/datasource.ts.html +4 -4
- package/coverage/lcov-report/datasource/filesystemAdapter.ts.html +23 -23
- package/coverage/lcov-report/datasource/index.html +1 -1
- package/coverage/lcov-report/datasource/index.ts.html +1 -1
- package/coverage/lcov-report/index.html +28 -28
- package/coverage/lcov-report/linter/attributeSchema.ts.html +1 -1
- package/coverage/lcov-report/linter/checkCircularDependency.ts.html +1 -1
- package/coverage/lcov-report/linter/checkPercentageExceedingSlot.ts.html +1 -1
- package/coverage/lcov-report/linter/conditionSchema.ts.html +7 -7
- package/coverage/lcov-report/linter/featureSchema.ts.html +789 -318
- package/coverage/lcov-report/linter/groupSchema.ts.html +1 -1
- package/coverage/lcov-report/linter/index.html +34 -19
- package/coverage/lcov-report/linter/lintProject.ts.html +25 -25
- package/coverage/lcov-report/linter/mutationNotation.ts.html +1309 -0
- package/coverage/lcov-report/linter/printError.ts.html +1 -1
- package/coverage/lcov-report/linter/schema.ts.html +69 -69
- package/coverage/lcov-report/linter/segmentSchema.ts.html +1 -1
- package/coverage/lcov-report/linter/testSchema.ts.html +5 -5
- package/coverage/lcov-report/list/index.html +1 -1
- package/coverage/lcov-report/list/matrix.ts.html +1 -1
- package/coverage/lcov-report/parsers/index.html +1 -1
- package/coverage/lcov-report/parsers/index.ts.html +1 -1
- package/coverage/lcov-report/parsers/json.ts.html +1 -1
- package/coverage/lcov-report/parsers/yml.ts.html +2 -2
- package/coverage/lcov-report/tester/cliFormat.ts.html +1 -1
- package/coverage/lcov-report/tester/helpers.ts.html +1 -1
- package/coverage/lcov-report/tester/index.html +1 -1
- package/coverage/lcov-report/utils/git.ts.html +1 -1
- package/coverage/lcov-report/utils/index.html +1 -1
- package/coverage/lcov.info +1839 -1120
- package/lib/builder/buildDatafile.js +8 -3
- package/lib/builder/buildDatafile.js.map +1 -1
- package/lib/builder/mutateVariables.d.ts +14 -0
- package/lib/builder/mutateVariables.js +90 -0
- package/lib/builder/mutateVariables.js.map +1 -0
- package/lib/builder/mutateVariables.spec.d.ts +1 -0
- package/lib/builder/mutateVariables.spec.js +1045 -0
- package/lib/builder/mutateVariables.spec.js.map +1 -0
- package/lib/builder/mutator.d.ts +20 -0
- package/lib/builder/mutator.js +223 -0
- package/lib/builder/mutator.js.map +1 -0
- package/lib/builder/mutator.spec.d.ts +1 -0
- package/lib/builder/mutator.spec.js +368 -0
- package/lib/builder/mutator.spec.js.map +1 -0
- package/lib/linter/featureSchema.d.ts +7 -7
- package/lib/linter/featureSchema.js +138 -62
- package/lib/linter/featureSchema.js.map +1 -1
- package/lib/linter/featureSchema.spec.js +150 -0
- package/lib/linter/featureSchema.spec.js.map +1 -1
- package/lib/linter/mutationNotation.d.ts +47 -0
- package/lib/linter/mutationNotation.js +381 -0
- package/lib/linter/mutationNotation.js.map +1 -0
- package/lib/linter/mutationNotation.spec.d.ts +1 -0
- package/lib/linter/mutationNotation.spec.js +549 -0
- package/lib/linter/mutationNotation.spec.js.map +1 -0
- package/package.json +2 -2
- package/src/builder/buildDatafile.ts +29 -3
- package/src/builder/mutateVariables.spec.ts +1134 -0
- package/src/builder/mutateVariables.ts +105 -0
- package/src/builder/mutator.spec.ts +413 -0
- package/src/builder/mutator.ts +237 -0
- package/src/linter/featureSchema.spec.ts +182 -0
- package/src/linter/featureSchema.ts +254 -97
- package/src/linter/mutationNotation.spec.ts +642 -0
- package/src/linter/mutationNotation.ts +408 -0
|
@@ -0,0 +1,237 @@
|
|
|
1
|
+
import type { VariableSchema, VariableValue } from "@featurevisor/types";
|
|
2
|
+
|
|
3
|
+
export type PathPart =
|
|
4
|
+
| { key: string }
|
|
5
|
+
| { key: string; index: number }
|
|
6
|
+
| { key: string; selector: { prop: string; value: string } };
|
|
7
|
+
|
|
8
|
+
export type MutationOperation = "set" | "append" | "prepend" | "after" | "before" | "remove";
|
|
9
|
+
|
|
10
|
+
export interface ParsedNotation {
|
|
11
|
+
segments: PathPart[];
|
|
12
|
+
operation: MutationOperation;
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
export function parseNotation(notation: string): ParsedNotation {
|
|
16
|
+
let rest = notation.trim();
|
|
17
|
+
const operationMatch = rest.match(/:((?:append|prepend|after|before|remove))$/);
|
|
18
|
+
const operation: MutationOperation = operationMatch
|
|
19
|
+
? (operationMatch[1] as MutationOperation)
|
|
20
|
+
: "set";
|
|
21
|
+
if (operationMatch) {
|
|
22
|
+
rest = rest.slice(0, -operationMatch[0].length);
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
const segments: PathPart[] = [];
|
|
26
|
+
let i = 0;
|
|
27
|
+
while (i < rest.length) {
|
|
28
|
+
let key = "";
|
|
29
|
+
while (i < rest.length && rest[i] !== "." && rest[i] !== "[") {
|
|
30
|
+
key += rest[i];
|
|
31
|
+
i++;
|
|
32
|
+
}
|
|
33
|
+
key = key.trim();
|
|
34
|
+
if (key) {
|
|
35
|
+
if (rest[i] === "[") {
|
|
36
|
+
i++;
|
|
37
|
+
const bracketStart = i;
|
|
38
|
+
while (i < rest.length && rest[i] !== "]") i++;
|
|
39
|
+
const bracketContent = rest.slice(bracketStart, i);
|
|
40
|
+
i++;
|
|
41
|
+
const eq = bracketContent.indexOf("=");
|
|
42
|
+
if (eq >= 0) {
|
|
43
|
+
const prop = bracketContent.slice(0, eq).trim();
|
|
44
|
+
let val = bracketContent.slice(eq + 1).trim();
|
|
45
|
+
if (val.startsWith('"') && val.endsWith('"')) val = val.slice(1, -1);
|
|
46
|
+
else if (val.startsWith("'") && val.endsWith("'")) val = val.slice(1, -1);
|
|
47
|
+
segments.push({ key, selector: { prop, value: val } });
|
|
48
|
+
} else {
|
|
49
|
+
const index = parseInt(bracketContent.trim(), 10);
|
|
50
|
+
segments.push({ key, index });
|
|
51
|
+
}
|
|
52
|
+
} else {
|
|
53
|
+
segments.push({ key });
|
|
54
|
+
}
|
|
55
|
+
} else if (rest[i] === "[") {
|
|
56
|
+
i++;
|
|
57
|
+
const bracketStart = i;
|
|
58
|
+
while (i < rest.length && rest[i] !== "]") i++;
|
|
59
|
+
const bracketContent = rest.slice(bracketStart, i);
|
|
60
|
+
i++;
|
|
61
|
+
const eq = bracketContent.indexOf("=");
|
|
62
|
+
if (eq >= 0) {
|
|
63
|
+
const prop = bracketContent.slice(0, eq).trim();
|
|
64
|
+
let val = bracketContent.slice(eq + 1).trim();
|
|
65
|
+
if (val.startsWith('"') && val.endsWith('"')) val = val.slice(1, -1);
|
|
66
|
+
else if (val.startsWith("'") && val.endsWith("'")) val = val.slice(1, -1);
|
|
67
|
+
segments.push({ key: "", selector: { prop, value: val } });
|
|
68
|
+
} else {
|
|
69
|
+
const index = parseInt(bracketContent.trim(), 10);
|
|
70
|
+
segments.push({ key: "", index });
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
if (rest[i] === ".") i++;
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
return { segments, operation };
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
function getAtSegment(obj: VariableValue, seg: PathPart): VariableValue {
|
|
80
|
+
if (obj === null || obj === undefined) return undefined;
|
|
81
|
+
const o = obj as Record<string, unknown>;
|
|
82
|
+
if ("index" in seg) {
|
|
83
|
+
const arr =
|
|
84
|
+
seg.key === ""
|
|
85
|
+
? Array.isArray(obj)
|
|
86
|
+
? obj
|
|
87
|
+
: undefined
|
|
88
|
+
: Array.isArray(obj)
|
|
89
|
+
? obj
|
|
90
|
+
: (o[seg.key] as unknown[]);
|
|
91
|
+
if (!Array.isArray(arr)) return undefined;
|
|
92
|
+
return arr[seg.index] as VariableValue;
|
|
93
|
+
}
|
|
94
|
+
if ("selector" in seg) {
|
|
95
|
+
const arr =
|
|
96
|
+
seg.key === ""
|
|
97
|
+
? Array.isArray(obj)
|
|
98
|
+
? obj
|
|
99
|
+
: undefined
|
|
100
|
+
: Array.isArray(obj)
|
|
101
|
+
? obj
|
|
102
|
+
: (o[seg.key] as unknown[]);
|
|
103
|
+
if (!Array.isArray(arr)) return undefined;
|
|
104
|
+
const { prop, value } = seg.selector;
|
|
105
|
+
const found = arr.find((item) => {
|
|
106
|
+
if (item === null || typeof item !== "object") return false;
|
|
107
|
+
const v = (item as Record<string, unknown>)[prop];
|
|
108
|
+
return String(v) === value;
|
|
109
|
+
});
|
|
110
|
+
return found as VariableValue;
|
|
111
|
+
}
|
|
112
|
+
return o[seg.key] as VariableValue;
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
function setAtSegment(
|
|
116
|
+
obj: Record<string, unknown> | unknown[],
|
|
117
|
+
seg: PathPart,
|
|
118
|
+
_value: VariableValue,
|
|
119
|
+
op: MutationOperation,
|
|
120
|
+
setValue: VariableValue | undefined,
|
|
121
|
+
): void {
|
|
122
|
+
if ("index" in seg) {
|
|
123
|
+
const i = seg.index;
|
|
124
|
+
const arr =
|
|
125
|
+
seg.key === ""
|
|
126
|
+
? Array.isArray(obj)
|
|
127
|
+
? obj
|
|
128
|
+
: undefined
|
|
129
|
+
: Array.isArray(obj)
|
|
130
|
+
? obj
|
|
131
|
+
: ((obj as Record<string, unknown>)[seg.key] as unknown[]);
|
|
132
|
+
if (!Array.isArray(arr)) return;
|
|
133
|
+
if (op === "remove") {
|
|
134
|
+
arr.splice(i, 1);
|
|
135
|
+
return;
|
|
136
|
+
}
|
|
137
|
+
if (op === "set") {
|
|
138
|
+
arr[i] = setValue;
|
|
139
|
+
return;
|
|
140
|
+
}
|
|
141
|
+
return;
|
|
142
|
+
}
|
|
143
|
+
if ("selector" in seg) {
|
|
144
|
+
const arr =
|
|
145
|
+
seg.key === ""
|
|
146
|
+
? Array.isArray(obj)
|
|
147
|
+
? obj
|
|
148
|
+
: undefined
|
|
149
|
+
: Array.isArray(obj)
|
|
150
|
+
? obj
|
|
151
|
+
: ((obj as Record<string, unknown>)[seg.key] as unknown[]);
|
|
152
|
+
if (!Array.isArray(arr)) return;
|
|
153
|
+
const { prop, value: selVal } = seg.selector;
|
|
154
|
+
const numVal = /^\d+$/.test(selVal) ? parseInt(selVal, 10) : null;
|
|
155
|
+
const idx = arr.findIndex((item) => {
|
|
156
|
+
if (item === null || typeof item !== "object") return false;
|
|
157
|
+
const v = (item as Record<string, unknown>)[prop];
|
|
158
|
+
return String(v) === selVal || (numVal !== null && v === numVal);
|
|
159
|
+
});
|
|
160
|
+
if (idx < 0) return;
|
|
161
|
+
if (op === "remove") {
|
|
162
|
+
arr.splice(idx, 1);
|
|
163
|
+
return;
|
|
164
|
+
}
|
|
165
|
+
if (op === "after" && setValue !== undefined) {
|
|
166
|
+
arr.splice(idx + 1, 0, setValue);
|
|
167
|
+
return;
|
|
168
|
+
}
|
|
169
|
+
if (op === "before" && setValue !== undefined) {
|
|
170
|
+
arr.splice(idx, 0, setValue);
|
|
171
|
+
return;
|
|
172
|
+
}
|
|
173
|
+
return;
|
|
174
|
+
}
|
|
175
|
+
const key = seg.key;
|
|
176
|
+
const o = obj as Record<string, unknown>;
|
|
177
|
+
if (op === "remove") {
|
|
178
|
+
delete o[key];
|
|
179
|
+
return;
|
|
180
|
+
}
|
|
181
|
+
if (op === "append" && setValue !== undefined) {
|
|
182
|
+
const arr = (o[key] ?? []) as unknown[];
|
|
183
|
+
arr.push(setValue);
|
|
184
|
+
o[key] = arr;
|
|
185
|
+
return;
|
|
186
|
+
}
|
|
187
|
+
if (op === "prepend" && setValue !== undefined) {
|
|
188
|
+
const arr = (o[key] ?? []) as unknown[];
|
|
189
|
+
arr.unshift(setValue);
|
|
190
|
+
o[key] = arr;
|
|
191
|
+
return;
|
|
192
|
+
}
|
|
193
|
+
if (op === "set") {
|
|
194
|
+
o[key] = setValue as unknown;
|
|
195
|
+
}
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
export function mutate(
|
|
199
|
+
_schema: VariableSchema,
|
|
200
|
+
value: VariableValue,
|
|
201
|
+
notation: string,
|
|
202
|
+
setValue: VariableValue | undefined,
|
|
203
|
+
): VariableValue {
|
|
204
|
+
if (value === null || value === undefined) return value;
|
|
205
|
+
const result = JSON.parse(JSON.stringify(value)) as VariableValue;
|
|
206
|
+
|
|
207
|
+
const { segments, operation } = parseNotation(notation);
|
|
208
|
+
if (segments.length === 0) {
|
|
209
|
+
if (Array.isArray(result) && setValue !== undefined) {
|
|
210
|
+
const arr = result as VariableValue[];
|
|
211
|
+
if (operation === "append") {
|
|
212
|
+
arr.push(setValue);
|
|
213
|
+
return result;
|
|
214
|
+
}
|
|
215
|
+
if (operation === "prepend") {
|
|
216
|
+
arr.unshift(setValue);
|
|
217
|
+
return result;
|
|
218
|
+
}
|
|
219
|
+
}
|
|
220
|
+
return result;
|
|
221
|
+
}
|
|
222
|
+
|
|
223
|
+
const last = segments[segments.length - 1];
|
|
224
|
+
const parentSegments = segments.slice(0, -1);
|
|
225
|
+
|
|
226
|
+
let container: Record<string, unknown> | unknown[] = result as
|
|
227
|
+
| Record<string, unknown>
|
|
228
|
+
| unknown[];
|
|
229
|
+
for (const seg of parentSegments) {
|
|
230
|
+
const next = getAtSegment(container as VariableValue, seg);
|
|
231
|
+
if (next === undefined) return result;
|
|
232
|
+
container = next as Record<string, unknown> | unknown[];
|
|
233
|
+
}
|
|
234
|
+
|
|
235
|
+
setAtSegment(container, last, undefined, operation, setValue);
|
|
236
|
+
return result;
|
|
237
|
+
}
|
|
@@ -166,6 +166,38 @@ describe("featureSchema.ts :: getFeatureZodSchema (variablesSchema and variable
|
|
|
166
166
|
);
|
|
167
167
|
});
|
|
168
168
|
|
|
169
|
+
it("rejects variable with schema reference when schema could not be loaded (missing from schemasByKey)", () => {
|
|
170
|
+
const projectConfig = minimalProjectConfig();
|
|
171
|
+
const conditionsZodSchema = getConditionsZodSchema(projectConfig, TEST_ATTRIBUTES);
|
|
172
|
+
const schemaWithEmptySchemasByKey = getFeatureZodSchema(
|
|
173
|
+
projectConfig,
|
|
174
|
+
conditionsZodSchema,
|
|
175
|
+
TEST_ATTRIBUTES,
|
|
176
|
+
TEST_SEGMENTS,
|
|
177
|
+
TEST_FEATURES,
|
|
178
|
+
["link"],
|
|
179
|
+
{},
|
|
180
|
+
);
|
|
181
|
+
const result = schemaWithEmptySchemasByKey.safeParse(
|
|
182
|
+
baseFeature({
|
|
183
|
+
variablesSchema: {
|
|
184
|
+
myLink: {
|
|
185
|
+
schema: "link",
|
|
186
|
+
defaultValue: { title: "Home", url: "/" },
|
|
187
|
+
},
|
|
188
|
+
},
|
|
189
|
+
}),
|
|
190
|
+
);
|
|
191
|
+
expect(result.success).toBe(false);
|
|
192
|
+
if (result.success) return;
|
|
193
|
+
const messages = (result as z.SafeParseError<unknown>).error.issues
|
|
194
|
+
.map((i) => (typeof i.message === "string" ? i.message : ""))
|
|
195
|
+
.join(" ");
|
|
196
|
+
expect(messages).toContain("could not be loaded");
|
|
197
|
+
expect(messages).toContain("link");
|
|
198
|
+
expect(messages).toContain("myLink");
|
|
199
|
+
});
|
|
200
|
+
|
|
169
201
|
it("rejects variable with schema reference when defaultValue does not match schema", () => {
|
|
170
202
|
expectParseFailure(
|
|
171
203
|
baseFeature({
|
|
@@ -1215,4 +1247,154 @@ describe("featureSchema.ts :: getFeatureZodSchema (variablesSchema and variable
|
|
|
1215
1247
|
);
|
|
1216
1248
|
});
|
|
1217
1249
|
});
|
|
1250
|
+
|
|
1251
|
+
describe("mutation notation in variables", () => {
|
|
1252
|
+
it("accepts valid dot-notation mutation keys in rules.variables", () => {
|
|
1253
|
+
expectParseSuccess(
|
|
1254
|
+
baseFeature({
|
|
1255
|
+
variablesSchema: {
|
|
1256
|
+
config: {
|
|
1257
|
+
type: "object",
|
|
1258
|
+
properties: {
|
|
1259
|
+
theme: { type: "string" },
|
|
1260
|
+
width: { type: "integer" },
|
|
1261
|
+
},
|
|
1262
|
+
defaultValue: { theme: "light", width: 100 },
|
|
1263
|
+
},
|
|
1264
|
+
},
|
|
1265
|
+
rules: {
|
|
1266
|
+
staging: [
|
|
1267
|
+
{
|
|
1268
|
+
key: "r1",
|
|
1269
|
+
segments: "*",
|
|
1270
|
+
percentage: 100,
|
|
1271
|
+
variables: { "config.theme": "dark", "config.width": 1200 },
|
|
1272
|
+
},
|
|
1273
|
+
],
|
|
1274
|
+
production: [{ key: "r1", segments: "*", percentage: 100 }],
|
|
1275
|
+
},
|
|
1276
|
+
}),
|
|
1277
|
+
);
|
|
1278
|
+
});
|
|
1279
|
+
|
|
1280
|
+
it("accepts valid array-index mutation keys in rules.variables", () => {
|
|
1281
|
+
expectParseSuccess(
|
|
1282
|
+
baseFeature({
|
|
1283
|
+
variablesSchema: {
|
|
1284
|
+
tags: {
|
|
1285
|
+
type: "array",
|
|
1286
|
+
items: { type: "string" },
|
|
1287
|
+
defaultValue: ["a", "b"],
|
|
1288
|
+
},
|
|
1289
|
+
},
|
|
1290
|
+
rules: {
|
|
1291
|
+
staging: [
|
|
1292
|
+
{ key: "r1", segments: "*", percentage: 100, variables: { "tags[0]": "first" } },
|
|
1293
|
+
],
|
|
1294
|
+
production: [{ key: "r1", segments: "*", percentage: 100 }],
|
|
1295
|
+
},
|
|
1296
|
+
}),
|
|
1297
|
+
);
|
|
1298
|
+
});
|
|
1299
|
+
|
|
1300
|
+
it("rejects mutation key when root variable is not in variablesSchema", () => {
|
|
1301
|
+
expectErrorSurfaces(
|
|
1302
|
+
baseFeature({
|
|
1303
|
+
variablesSchema: {
|
|
1304
|
+
title: { type: "string", defaultValue: "Default" },
|
|
1305
|
+
},
|
|
1306
|
+
rules: {
|
|
1307
|
+
staging: [
|
|
1308
|
+
{ key: "r1", segments: "*", percentage: 100, variables: { "unknownVar.foo": "x" } },
|
|
1309
|
+
],
|
|
1310
|
+
production: [{ key: "r1", segments: "*", percentage: 100 }],
|
|
1311
|
+
},
|
|
1312
|
+
}),
|
|
1313
|
+
{
|
|
1314
|
+
pathContains: ["rules", "variables", "unknownVar.foo"],
|
|
1315
|
+
messageContains: "not defined in",
|
|
1316
|
+
},
|
|
1317
|
+
);
|
|
1318
|
+
});
|
|
1319
|
+
|
|
1320
|
+
it("rejects invalid path (property not in schema)", () => {
|
|
1321
|
+
expectErrorSurfaces(
|
|
1322
|
+
baseFeature({
|
|
1323
|
+
variablesSchema: {
|
|
1324
|
+
config: {
|
|
1325
|
+
type: "object",
|
|
1326
|
+
properties: { theme: { type: "string" } },
|
|
1327
|
+
defaultValue: { theme: "light" },
|
|
1328
|
+
},
|
|
1329
|
+
},
|
|
1330
|
+
rules: {
|
|
1331
|
+
staging: [
|
|
1332
|
+
{
|
|
1333
|
+
key: "r1",
|
|
1334
|
+
segments: "*",
|
|
1335
|
+
percentage: 100,
|
|
1336
|
+
variables: { "config.unknownProp": "x" },
|
|
1337
|
+
},
|
|
1338
|
+
],
|
|
1339
|
+
production: [{ key: "r1", segments: "*", percentage: 100 }],
|
|
1340
|
+
},
|
|
1341
|
+
}),
|
|
1342
|
+
{ pathContains: ["rules", "variables"], messageContains: "path does not exist" },
|
|
1343
|
+
);
|
|
1344
|
+
});
|
|
1345
|
+
|
|
1346
|
+
it("rejects :append on non-array (object property)", () => {
|
|
1347
|
+
expectErrorSurfaces(
|
|
1348
|
+
baseFeature({
|
|
1349
|
+
variablesSchema: {
|
|
1350
|
+
config: {
|
|
1351
|
+
type: "object",
|
|
1352
|
+
properties: { theme: { type: "string" } },
|
|
1353
|
+
defaultValue: { theme: "light" },
|
|
1354
|
+
},
|
|
1355
|
+
},
|
|
1356
|
+
rules: {
|
|
1357
|
+
staging: [
|
|
1358
|
+
{ key: "r1", segments: "*", percentage: 100, variables: { "config:append": "x" } },
|
|
1359
|
+
],
|
|
1360
|
+
production: [{ key: "r1", segments: "*", percentage: 100 }],
|
|
1361
|
+
},
|
|
1362
|
+
}),
|
|
1363
|
+
{ pathContains: ["rules", "variables"], messageContains: "only allowed on array" },
|
|
1364
|
+
);
|
|
1365
|
+
});
|
|
1366
|
+
|
|
1367
|
+
it("rejects :remove on required object property", () => {
|
|
1368
|
+
expectErrorSurfaces(
|
|
1369
|
+
baseFeature({
|
|
1370
|
+
variablesSchema: {
|
|
1371
|
+
config: {
|
|
1372
|
+
type: "object",
|
|
1373
|
+
properties: {
|
|
1374
|
+
theme: { type: "string" },
|
|
1375
|
+
compact: { type: "boolean" },
|
|
1376
|
+
},
|
|
1377
|
+
required: ["compact"],
|
|
1378
|
+
defaultValue: { theme: "light", compact: true },
|
|
1379
|
+
},
|
|
1380
|
+
},
|
|
1381
|
+
rules: {
|
|
1382
|
+
staging: [
|
|
1383
|
+
{
|
|
1384
|
+
key: "r1",
|
|
1385
|
+
segments: "*",
|
|
1386
|
+
percentage: 100,
|
|
1387
|
+
variables: { "config.compact:remove": null },
|
|
1388
|
+
},
|
|
1389
|
+
],
|
|
1390
|
+
production: [{ key: "r1", segments: "*", percentage: 100 }],
|
|
1391
|
+
},
|
|
1392
|
+
}),
|
|
1393
|
+
{
|
|
1394
|
+
pathContains: ["rules", "variables"],
|
|
1395
|
+
messageContains: "Cannot remove required property",
|
|
1396
|
+
},
|
|
1397
|
+
);
|
|
1398
|
+
});
|
|
1399
|
+
});
|
|
1218
1400
|
});
|