@featurevisor/core 0.0.3
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 +30 -0
- package/LICENSE +21 -0
- package/README.md +15 -0
- package/lib/builder.d.ts +12 -0
- package/lib/builder.js +245 -0
- package/lib/builder.js.map +1 -0
- package/lib/config.d.ts +25 -0
- package/lib/config.js +41 -0
- package/lib/config.js.map +1 -0
- package/lib/index.d.ts +5 -0
- package/lib/index.js +22 -0
- package/lib/index.js.map +1 -0
- package/lib/init.d.ts +6 -0
- package/lib/init.js +38 -0
- package/lib/init.js.map +1 -0
- package/lib/linter.d.ts +9 -0
- package/lib/linter.js +334 -0
- package/lib/linter.js.map +1 -0
- package/lib/tester.d.ts +26 -0
- package/lib/tester.js +98 -0
- package/lib/tester.js.map +1 -0
- package/lib/traffic.d.ts +2 -0
- package/lib/traffic.js +105 -0
- package/lib/traffic.js.map +1 -0
- package/lib/traffic.spec.d.ts +1 -0
- package/lib/traffic.spec.js +636 -0
- package/lib/traffic.spec.js.map +1 -0
- package/lib/utils.d.ts +5 -0
- package/lib/utils.js +70 -0
- package/lib/utils.js.map +1 -0
- package/package.json +57 -0
- package/src/builder.ts +359 -0
- package/src/config.ts +62 -0
- package/src/index.ts +5 -0
- package/src/init.ts +44 -0
- package/src/linter.ts +328 -0
- package/src/tester.ts +163 -0
- package/src/traffic.spec.ts +681 -0
- package/src/traffic.ts +136 -0
- package/src/utils.ts +83 -0
- package/tsconfig.cjs.json +7 -0
package/src/linter.ts
ADDED
|
@@ -0,0 +1,328 @@
|
|
|
1
|
+
// for use in node only
|
|
2
|
+
import * as fs from "fs";
|
|
3
|
+
import * as path from "path";
|
|
4
|
+
|
|
5
|
+
import * as Joi from "joi";
|
|
6
|
+
|
|
7
|
+
import { ProjectConfig } from "./config";
|
|
8
|
+
import { getYAMLFiles, parseYaml } from "./utils";
|
|
9
|
+
|
|
10
|
+
export function getAttributeJoiSchema(projectConfig: ProjectConfig) {
|
|
11
|
+
const attributeJoiSchema = Joi.object({
|
|
12
|
+
archived: Joi.boolean(),
|
|
13
|
+
type: Joi.string().allow("boolean", "string", "integer", "double"),
|
|
14
|
+
description: Joi.string(),
|
|
15
|
+
capture: Joi.boolean().optional(),
|
|
16
|
+
});
|
|
17
|
+
|
|
18
|
+
return attributeJoiSchema;
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
export function getConditionsJoiSchema(projectConfig: ProjectConfig) {
|
|
22
|
+
const plainConditionJoiSchema = Joi.object({
|
|
23
|
+
attribute: Joi.string(),
|
|
24
|
+
operator: Joi.string().valid(
|
|
25
|
+
"equals",
|
|
26
|
+
"notEquals",
|
|
27
|
+
|
|
28
|
+
// numeric
|
|
29
|
+
"greaterThan",
|
|
30
|
+
"greaterThanOrEquals",
|
|
31
|
+
"lessThan",
|
|
32
|
+
"lessThanOrEquals",
|
|
33
|
+
|
|
34
|
+
// string
|
|
35
|
+
"contains",
|
|
36
|
+
"notContains",
|
|
37
|
+
"startsWith",
|
|
38
|
+
"endsWith",
|
|
39
|
+
|
|
40
|
+
// array of strings
|
|
41
|
+
"in",
|
|
42
|
+
"notIn",
|
|
43
|
+
),
|
|
44
|
+
value: Joi.alternatives().try(
|
|
45
|
+
// @TODO: make them more specific
|
|
46
|
+
Joi.string(),
|
|
47
|
+
Joi.number(),
|
|
48
|
+
Joi.boolean(),
|
|
49
|
+
Joi.array().items(Joi.string()),
|
|
50
|
+
),
|
|
51
|
+
});
|
|
52
|
+
|
|
53
|
+
const andOrConditionJoiSchema = Joi.alternatives()
|
|
54
|
+
.try(
|
|
55
|
+
Joi.object({
|
|
56
|
+
and: Joi.array().items(Joi.link("#andOrCondition"), plainConditionJoiSchema),
|
|
57
|
+
}),
|
|
58
|
+
Joi.object({
|
|
59
|
+
or: Joi.array().items(Joi.link("#andOrCondition"), plainConditionJoiSchema),
|
|
60
|
+
}),
|
|
61
|
+
)
|
|
62
|
+
.id("andOrCondition");
|
|
63
|
+
|
|
64
|
+
const conditionJoiSchema = Joi.alternatives().try(
|
|
65
|
+
andOrConditionJoiSchema,
|
|
66
|
+
plainConditionJoiSchema,
|
|
67
|
+
);
|
|
68
|
+
|
|
69
|
+
const conditionsJoiSchema = Joi.alternatives().try(
|
|
70
|
+
conditionJoiSchema,
|
|
71
|
+
Joi.array().items(conditionJoiSchema),
|
|
72
|
+
);
|
|
73
|
+
|
|
74
|
+
return conditionsJoiSchema;
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
export function getSegmentJoiSchema(projectConfig: ProjectConfig, conditionsJoiSchema) {
|
|
78
|
+
const segmentJoiSchema = Joi.object({
|
|
79
|
+
archived: Joi.boolean().optional(),
|
|
80
|
+
description: Joi.string(),
|
|
81
|
+
conditions: conditionsJoiSchema,
|
|
82
|
+
});
|
|
83
|
+
|
|
84
|
+
return segmentJoiSchema;
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
export function getFeatureJoiSchema(projectConfig: ProjectConfig, conditionsJoiSchema) {
|
|
88
|
+
const variationValueJoiSchema = Joi.alternatives().try(Joi.string(), Joi.number(), Joi.boolean());
|
|
89
|
+
const variableValueJoiSchema = Joi.alternatives()
|
|
90
|
+
.try(Joi.string(), Joi.number(), Joi.boolean(), Joi.array().items(Joi.string()))
|
|
91
|
+
.allow("");
|
|
92
|
+
|
|
93
|
+
const plainGroupSegment = Joi.string();
|
|
94
|
+
|
|
95
|
+
const andOrGroupSegment = Joi.alternatives()
|
|
96
|
+
.try(
|
|
97
|
+
Joi.object({
|
|
98
|
+
and: Joi.array().items(Joi.link("#andOrGroupSegment"), plainGroupSegment),
|
|
99
|
+
}),
|
|
100
|
+
Joi.object({
|
|
101
|
+
or: Joi.array().items(Joi.link("#andOrGroupSegment"), plainGroupSegment),
|
|
102
|
+
}),
|
|
103
|
+
)
|
|
104
|
+
.id("andOrGroupSegment");
|
|
105
|
+
|
|
106
|
+
const groupSegment = Joi.alternatives().try(andOrGroupSegment, plainGroupSegment);
|
|
107
|
+
|
|
108
|
+
const groupSegmentsJoiSchema = Joi.alternatives().try(
|
|
109
|
+
Joi.array().items(groupSegment),
|
|
110
|
+
groupSegment,
|
|
111
|
+
);
|
|
112
|
+
|
|
113
|
+
const environmentJoiSchema = Joi.object({
|
|
114
|
+
expose: Joi.boolean(),
|
|
115
|
+
rules: Joi.array().items(
|
|
116
|
+
Joi.object({
|
|
117
|
+
key: Joi.string(), // @TODO: make it unique among siblings
|
|
118
|
+
segments: groupSegmentsJoiSchema,
|
|
119
|
+
percentage: Joi.number().min(0).max(100), // @TODO: allow maximum 3 decimal places
|
|
120
|
+
}),
|
|
121
|
+
),
|
|
122
|
+
force: Joi.array().items(
|
|
123
|
+
Joi.object({
|
|
124
|
+
// @TODO: either of the two below
|
|
125
|
+
segments: groupSegmentsJoiSchema,
|
|
126
|
+
conditions: conditionsJoiSchema,
|
|
127
|
+
|
|
128
|
+
variation: variationValueJoiSchema,
|
|
129
|
+
variables: Joi.object(), // @TODO: make it stricter
|
|
130
|
+
}),
|
|
131
|
+
),
|
|
132
|
+
});
|
|
133
|
+
|
|
134
|
+
const allEnvironomentsSchema = {};
|
|
135
|
+
projectConfig.environments.forEach((environmentKey) => {
|
|
136
|
+
allEnvironomentsSchema[environmentKey] = environmentJoiSchema;
|
|
137
|
+
});
|
|
138
|
+
const allEnvironomentsJoiSchema = Joi.object(allEnvironomentsSchema);
|
|
139
|
+
|
|
140
|
+
const featureJoiSchema = Joi.object({
|
|
141
|
+
archived: Joi.boolean().optional(),
|
|
142
|
+
description: Joi.string(),
|
|
143
|
+
tags: Joi.array().items(Joi.string()),
|
|
144
|
+
|
|
145
|
+
defaultVariation: variationValueJoiSchema,
|
|
146
|
+
|
|
147
|
+
bucketBy: Joi.alternatives().try(Joi.string(), Joi.array().items(Joi.string())),
|
|
148
|
+
|
|
149
|
+
variablesSchema: Joi.array().items(
|
|
150
|
+
Joi.object({
|
|
151
|
+
key: Joi.string(), // @TODO: make it unique among siblings
|
|
152
|
+
type: Joi.string().valid("string", "integer", "boolean", "double", "array"),
|
|
153
|
+
defaultValue: variableValueJoiSchema, // @TODO: make it stricter based on `type`
|
|
154
|
+
}),
|
|
155
|
+
),
|
|
156
|
+
|
|
157
|
+
variations: Joi.array().items(
|
|
158
|
+
Joi.object({
|
|
159
|
+
type: Joi.string().valid("string", "integer", "boolean", "double"),
|
|
160
|
+
value: variationValueJoiSchema, // @TODO: make it unique among siblings
|
|
161
|
+
weight: Joi.number().integer().min(0).max(100), // @TODO: total sum among siblings should be exactly 100, allow maximum 3 decimal places
|
|
162
|
+
variables: Joi.array().items(
|
|
163
|
+
Joi.object({
|
|
164
|
+
key: Joi.string(), // @TODO: make it unique among siblings
|
|
165
|
+
value: variableValueJoiSchema,
|
|
166
|
+
overrides: Joi.array().items(
|
|
167
|
+
Joi.object({
|
|
168
|
+
// @TODO: either segments or conditions prsent at a time
|
|
169
|
+
segments: groupSegmentsJoiSchema,
|
|
170
|
+
conditions: conditionsJoiSchema,
|
|
171
|
+
|
|
172
|
+
// @TODO: make it stricter based on `type`
|
|
173
|
+
value: variableValueJoiSchema,
|
|
174
|
+
}),
|
|
175
|
+
),
|
|
176
|
+
}),
|
|
177
|
+
),
|
|
178
|
+
}),
|
|
179
|
+
),
|
|
180
|
+
|
|
181
|
+
environments: allEnvironomentsJoiSchema,
|
|
182
|
+
});
|
|
183
|
+
|
|
184
|
+
return featureJoiSchema;
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
export function getTestsJoiSchema(projectConfig: ProjectConfig) {
|
|
188
|
+
const testsJoiSchema = Joi.object({
|
|
189
|
+
tests: Joi.array().items(
|
|
190
|
+
Joi.object({
|
|
191
|
+
description: Joi.string().optional(),
|
|
192
|
+
tag: Joi.string(), // @TODO: make it specific
|
|
193
|
+
environment: Joi.string().valid("production", "staging", "testing", "development"), // TODO: make it specific
|
|
194
|
+
features: Joi.array().items(
|
|
195
|
+
Joi.object({
|
|
196
|
+
key: Joi.string(), // @TODO: make it specific
|
|
197
|
+
assertions: Joi.array().items(
|
|
198
|
+
Joi.object({
|
|
199
|
+
description: Joi.string().optional(),
|
|
200
|
+
at: Joi.number().integer().min(0).max(100),
|
|
201
|
+
attributes: Joi.object(), // @TODO: make it specific
|
|
202
|
+
|
|
203
|
+
// @TODO: one or both below
|
|
204
|
+
expectedVariation: Joi.alternatives().try(
|
|
205
|
+
Joi.string(),
|
|
206
|
+
Joi.number(),
|
|
207
|
+
Joi.boolean(),
|
|
208
|
+
), // @TODO: make it specific
|
|
209
|
+
expectedVariables: Joi.object(), // @TODO: make it specific
|
|
210
|
+
}),
|
|
211
|
+
),
|
|
212
|
+
}),
|
|
213
|
+
),
|
|
214
|
+
}),
|
|
215
|
+
),
|
|
216
|
+
});
|
|
217
|
+
|
|
218
|
+
return testsJoiSchema;
|
|
219
|
+
}
|
|
220
|
+
|
|
221
|
+
export function printJoiError(e: Joi.ValidationError) {
|
|
222
|
+
const { details } = e;
|
|
223
|
+
|
|
224
|
+
details.forEach((detail) => {
|
|
225
|
+
console.error(" => Error:", detail.message);
|
|
226
|
+
console.error(" => Path:", detail.path.join("."));
|
|
227
|
+
console.error(" => Value:", detail.context?.value);
|
|
228
|
+
});
|
|
229
|
+
}
|
|
230
|
+
|
|
231
|
+
export async function lintProject(projectConfig: ProjectConfig): Promise<boolean> {
|
|
232
|
+
let hasError = false;
|
|
233
|
+
|
|
234
|
+
// lint attributes
|
|
235
|
+
console.log("Linting attributes...\n");
|
|
236
|
+
const attributeFilePaths = getYAMLFiles(path.join(projectConfig.attributesDirectoryPath));
|
|
237
|
+
const attributeJoiSchema = getAttributeJoiSchema(projectConfig);
|
|
238
|
+
|
|
239
|
+
for (const filePath of attributeFilePaths) {
|
|
240
|
+
const key = path.basename(filePath, ".yml");
|
|
241
|
+
const parsed = parseYaml(fs.readFileSync(filePath, "utf8")) as any;
|
|
242
|
+
console.log(" =>", key);
|
|
243
|
+
|
|
244
|
+
try {
|
|
245
|
+
await attributeJoiSchema.validateAsync(parsed);
|
|
246
|
+
} catch (e) {
|
|
247
|
+
if (e instanceof Joi.ValidationError) {
|
|
248
|
+
printJoiError(e);
|
|
249
|
+
} else {
|
|
250
|
+
console.log(e);
|
|
251
|
+
}
|
|
252
|
+
|
|
253
|
+
hasError = true;
|
|
254
|
+
}
|
|
255
|
+
}
|
|
256
|
+
|
|
257
|
+
// lint segments
|
|
258
|
+
console.log("\nLinting segments...\n");
|
|
259
|
+
const segmentFilePaths = getYAMLFiles(path.join(projectConfig.segmentsDirectoryPath));
|
|
260
|
+
const conditionsJoiSchema = getConditionsJoiSchema(projectConfig);
|
|
261
|
+
const segmentJoiSchema = getSegmentJoiSchema(projectConfig, conditionsJoiSchema);
|
|
262
|
+
|
|
263
|
+
for (const filePath of segmentFilePaths) {
|
|
264
|
+
const key = path.basename(filePath, ".yml");
|
|
265
|
+
const parsed = parseYaml(fs.readFileSync(filePath, "utf8")) as any;
|
|
266
|
+
console.log(" =>", key);
|
|
267
|
+
|
|
268
|
+
try {
|
|
269
|
+
await segmentJoiSchema.validateAsync(parsed);
|
|
270
|
+
} catch (e) {
|
|
271
|
+
if (e instanceof Joi.ValidationError) {
|
|
272
|
+
printJoiError(e);
|
|
273
|
+
} else {
|
|
274
|
+
console.log(e);
|
|
275
|
+
}
|
|
276
|
+
|
|
277
|
+
hasError = true;
|
|
278
|
+
}
|
|
279
|
+
}
|
|
280
|
+
|
|
281
|
+
// lint features
|
|
282
|
+
console.log("\nLinting features...\n");
|
|
283
|
+
const featureFilePaths = getYAMLFiles(path.join(projectConfig.featuresDirectoryPath));
|
|
284
|
+
const featureJoiSchema = getFeatureJoiSchema(projectConfig, conditionsJoiSchema);
|
|
285
|
+
|
|
286
|
+
for (const filePath of featureFilePaths) {
|
|
287
|
+
const key = path.basename(filePath, ".yml");
|
|
288
|
+
const parsed = parseYaml(fs.readFileSync(filePath, "utf8")) as any;
|
|
289
|
+
console.log(" =>", key);
|
|
290
|
+
|
|
291
|
+
try {
|
|
292
|
+
await featureJoiSchema.validateAsync(parsed);
|
|
293
|
+
} catch (e) {
|
|
294
|
+
if (e instanceof Joi.ValidationError) {
|
|
295
|
+
printJoiError(e);
|
|
296
|
+
} else {
|
|
297
|
+
console.log(e);
|
|
298
|
+
}
|
|
299
|
+
|
|
300
|
+
hasError = true;
|
|
301
|
+
}
|
|
302
|
+
}
|
|
303
|
+
|
|
304
|
+
// lint tests
|
|
305
|
+
console.log("\nLinting tests...\n");
|
|
306
|
+
const testFilePaths = getYAMLFiles(path.join(projectConfig.testsDirectoryPath));
|
|
307
|
+
const testsJoiSchema = getTestsJoiSchema(projectConfig);
|
|
308
|
+
|
|
309
|
+
for (const filePath of testFilePaths) {
|
|
310
|
+
const key = path.basename(filePath, ".yml");
|
|
311
|
+
const parsed = parseYaml(fs.readFileSync(filePath, "utf8")) as any;
|
|
312
|
+
console.log(" =>", key);
|
|
313
|
+
|
|
314
|
+
try {
|
|
315
|
+
await testsJoiSchema.validateAsync(parsed);
|
|
316
|
+
} catch (e) {
|
|
317
|
+
if (e instanceof Joi.ValidationError) {
|
|
318
|
+
printJoiError(e);
|
|
319
|
+
} else {
|
|
320
|
+
console.log(e);
|
|
321
|
+
}
|
|
322
|
+
|
|
323
|
+
hasError = true;
|
|
324
|
+
}
|
|
325
|
+
}
|
|
326
|
+
|
|
327
|
+
return hasError;
|
|
328
|
+
}
|
package/src/tester.ts
ADDED
|
@@ -0,0 +1,163 @@
|
|
|
1
|
+
import * as fs from "fs";
|
|
2
|
+
import * as path from "path";
|
|
3
|
+
|
|
4
|
+
import {
|
|
5
|
+
Attributes,
|
|
6
|
+
DatafileContent,
|
|
7
|
+
VariableKey,
|
|
8
|
+
VariableValue,
|
|
9
|
+
VariationValue,
|
|
10
|
+
} from "@featurevisor/types";
|
|
11
|
+
import { FeaturevisorSDK, MAX_BUCKETED_NUMBER } from "@featurevisor/sdk";
|
|
12
|
+
|
|
13
|
+
import { ProjectConfig } from "./config";
|
|
14
|
+
import { parseYaml } from "./utils";
|
|
15
|
+
import { getDatafilePath } from "./builder";
|
|
16
|
+
|
|
17
|
+
export interface Assertion {
|
|
18
|
+
description?: string;
|
|
19
|
+
at: number; // bucket weight: 0 to 100
|
|
20
|
+
attributes: Attributes;
|
|
21
|
+
expectedVariation?: VariationValue;
|
|
22
|
+
expectedVariables?: {
|
|
23
|
+
[key: VariableKey]: VariableValue;
|
|
24
|
+
};
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
export interface TestFeature {
|
|
28
|
+
key: string;
|
|
29
|
+
assertions: Assertion[];
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
export interface Test {
|
|
33
|
+
description?: string;
|
|
34
|
+
tag: string;
|
|
35
|
+
environment: string;
|
|
36
|
+
features: TestFeature[];
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
export interface Spec {
|
|
40
|
+
tests: Test[];
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
const errorColor = "\x1b[31m";
|
|
44
|
+
|
|
45
|
+
// @TODO: make it better
|
|
46
|
+
export function checkIfArraysAreEqual(a, b) {
|
|
47
|
+
if (!Array.isArray(a) || !Array.isArray(b)) {
|
|
48
|
+
return false;
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
if (a.length !== b.length) return false;
|
|
52
|
+
|
|
53
|
+
for (let i = 0; i < a.length; ++i) {
|
|
54
|
+
if (a[i] !== b[i]) {
|
|
55
|
+
return false;
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
return true;
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
export function testProject(rootDirectoryPath: string, projectConfig: ProjectConfig): boolean {
|
|
63
|
+
let hasError = false;
|
|
64
|
+
|
|
65
|
+
if (!fs.existsSync(projectConfig.testsDirectoryPath)) {
|
|
66
|
+
console.error(`Tests directory does not exist: ${projectConfig.testsDirectoryPath}`);
|
|
67
|
+
hasError = true;
|
|
68
|
+
|
|
69
|
+
return hasError;
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
const testFiles = fs
|
|
73
|
+
.readdirSync(projectConfig.testsDirectoryPath)
|
|
74
|
+
.filter((f) => f.endsWith(".yml"));
|
|
75
|
+
|
|
76
|
+
if (testFiles.length === 0) {
|
|
77
|
+
console.error(`No tests found in: ${projectConfig.testsDirectoryPath}`);
|
|
78
|
+
hasError = true;
|
|
79
|
+
|
|
80
|
+
return hasError;
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
for (const testFile of testFiles) {
|
|
84
|
+
const testFilePath = path.join(projectConfig.testsDirectoryPath, testFile);
|
|
85
|
+
|
|
86
|
+
console.log(` => Testing: ${testFilePath.replace(rootDirectoryPath, "")}`);
|
|
87
|
+
|
|
88
|
+
const parsed = parseYaml(fs.readFileSync(testFilePath, "utf8")) as Spec;
|
|
89
|
+
|
|
90
|
+
parsed.tests.forEach(function (test, tIndex) {
|
|
91
|
+
const datafilePath = getDatafilePath(projectConfig, test.environment, test.tag);
|
|
92
|
+
|
|
93
|
+
if (!fs.existsSync(datafilePath)) {
|
|
94
|
+
console.error(` => Datafile does not exist: ${datafilePath}`);
|
|
95
|
+
hasError = true;
|
|
96
|
+
|
|
97
|
+
return;
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
const datafileContent = JSON.parse(fs.readFileSync(datafilePath, "utf8")) as DatafileContent;
|
|
101
|
+
|
|
102
|
+
let currentAt = 0;
|
|
103
|
+
|
|
104
|
+
let sdk = new FeaturevisorSDK({
|
|
105
|
+
datafile: datafileContent,
|
|
106
|
+
configureBucketValue: (feature, attributes, bucketValue) => {
|
|
107
|
+
return currentAt;
|
|
108
|
+
},
|
|
109
|
+
});
|
|
110
|
+
|
|
111
|
+
test.features.forEach(function (feature, fIndex) {
|
|
112
|
+
const featureKey = feature.key;
|
|
113
|
+
|
|
114
|
+
console.log(` => Feature "${featureKey}" in environment "${test.environment}":`);
|
|
115
|
+
|
|
116
|
+
feature.assertions.forEach(function (assertion, aIndex) {
|
|
117
|
+
console.log(` => Assertion #${aIndex + 1}: ${assertion.description || ""}`);
|
|
118
|
+
|
|
119
|
+
let assertionHasError = false;
|
|
120
|
+
currentAt = assertion.at * (MAX_BUCKETED_NUMBER / 100);
|
|
121
|
+
|
|
122
|
+
// variation
|
|
123
|
+
if ("expectedVariation" in assertion) {
|
|
124
|
+
const variation = sdk.getVariation(featureKey, assertion.attributes);
|
|
125
|
+
|
|
126
|
+
if (variation !== assertion.expectedVariation) {
|
|
127
|
+
hasError = true;
|
|
128
|
+
assertionHasError = true;
|
|
129
|
+
|
|
130
|
+
console.error(
|
|
131
|
+
` Variation failed: expected "${assertion.expectedVariation}", got "${variation}"`,
|
|
132
|
+
);
|
|
133
|
+
}
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
// variables
|
|
137
|
+
if (typeof assertion.expectedVariables === "object") {
|
|
138
|
+
Object.keys(assertion.expectedVariables).forEach(function (variableKey) {
|
|
139
|
+
const expectedValue =
|
|
140
|
+
assertion.expectedVariables && assertion.expectedVariables[variableKey];
|
|
141
|
+
const actualValue = sdk.getVariable(featureKey, variableKey, assertion.attributes);
|
|
142
|
+
|
|
143
|
+
const passed = Array.isArray(expectedValue)
|
|
144
|
+
? checkIfArraysAreEqual(expectedValue, actualValue)
|
|
145
|
+
: expectedValue === actualValue;
|
|
146
|
+
|
|
147
|
+
if (!passed) {
|
|
148
|
+
hasError = true;
|
|
149
|
+
assertionHasError = true;
|
|
150
|
+
|
|
151
|
+
console.error(
|
|
152
|
+
` Variable "${variableKey}" failed: expected "${expectedValue}", got "${actualValue}"`,
|
|
153
|
+
);
|
|
154
|
+
}
|
|
155
|
+
});
|
|
156
|
+
}
|
|
157
|
+
});
|
|
158
|
+
});
|
|
159
|
+
});
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
return hasError;
|
|
163
|
+
}
|