objectmodel 4.3.0 → 4.4.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/.eslintignore +8 -8
- package/.eslintrc.json +25 -25
- package/.travis.yml +2 -2
- package/LICENSE +22 -22
- package/README.md +67 -67
- package/build/{add-banner.cjs → add-banner.js} +13 -13
- package/build/bundle-entry.dev.js +2 -2
- package/build/bundle-entry.js +11 -11
- package/build/{update-docs.cjs → update-docs.js} +7 -5
- package/dist/object-model.cjs +466 -472
- package/dist/object-model.js +466 -472
- package/dist/object-model.js.map +1 -1
- package/dist/object-model.min.js +2 -2
- package/dist/object-model.min.js.map +1 -1
- package/index.html +1603 -1603
- package/package.json +20 -10
- package/rollup.config.js +13 -13
- package/src/array-model.d.ts +16 -0
- package/src/array-model.js +68 -68
- package/src/devtool-formatter.js +198 -198
- package/src/function-model.d.ts +24 -0
- package/src/function-model.js +58 -65
- package/src/helpers.js +43 -43
- package/src/index.js +4 -4
- package/src/list-model.js +43 -43
- package/src/map-model.d.ts +18 -0
- package/src/map-model.js +48 -48
- package/src/object-model.d.ts +74 -0
- package/src/object-model.js +4 -3
- package/src/set-model.d.ts +16 -0
- package/src/set-model.js +41 -41
- package/test/array-model.spec.cjs +291 -291
- package/test/array-model.test-d.ts +24 -0
- package/test/basic-model.spec.cjs +263 -263
- package/test/basic-model.test-d.ts +30 -0
- package/test/bench/array.html +51 -51
- package/test/bench/bench-lib.js +49 -49
- package/test/bench/map-no-cast.html +53 -53
- package/test/bench/map-set.html +52 -52
- package/test/bench/map.html +51 -51
- package/test/bench/object-models.html +87 -87
- package/test/function-model.spec.cjs +161 -162
- package/test/function-model.test-d.ts +18 -0
- package/test/index.cjs +13 -13
- package/test/index.html +27 -27
- package/test/map-model.spec.cjs +224 -224
- package/test/map-model.test-d.ts +21 -0
- package/test/model.spec.cjs +30 -30
- package/test/object-model.spec.cjs +1345 -1327
- package/test/object-model.test-d.ts +53 -0
- package/test/set-model.spec.cjs +213 -213
- package/test/set-model.test-d.ts +17 -0
- package/test/umd.html +25 -25
- package/types/definitions.d.ts +43 -0
- package/types/helpers.d.ts +4 -0
- package/types/index.d.ts +6 -128
- package/test/lib/qunit.css +0 -436
- package/test/lib/qunit.js +0 -6582
- package/tsconfig.json +0 -10
|
@@ -1,1328 +1,1346 @@
|
|
|
1
|
-
/* global QUnit, Model, BasicModel, ArrayModel, ObjectModel */
|
|
2
|
-
QUnit.module("Object Models");
|
|
3
|
-
|
|
4
|
-
const consoleMock = (function (console) {
|
|
5
|
-
const methods = ["debug", "log", "warn", "error"];
|
|
6
|
-
const originals = {};
|
|
7
|
-
const mocks = {}
|
|
8
|
-
const lastArgs = {};
|
|
9
|
-
|
|
10
|
-
methods.forEach(method => {
|
|
11
|
-
originals[method] = console[method]
|
|
12
|
-
mocks[method] = function () { lastArgs[method] = arguments }
|
|
13
|
-
})
|
|
14
|
-
|
|
15
|
-
return {
|
|
16
|
-
apply: function () {
|
|
17
|
-
methods.forEach(method => {
|
|
18
|
-
lastArgs[method] = [];
|
|
19
|
-
console[method] = mocks[method]
|
|
20
|
-
})
|
|
21
|
-
},
|
|
22
|
-
revert: function () {
|
|
23
|
-
methods.forEach(method => {
|
|
24
|
-
lastArgs[method] = [];
|
|
25
|
-
console[method] = originals[method]
|
|
26
|
-
})
|
|
27
|
-
},
|
|
28
|
-
lastArgs
|
|
29
|
-
}
|
|
30
|
-
})(console);
|
|
31
|
-
|
|
32
|
-
QUnit.test("constructor && proto", async function (assert) {
|
|
33
|
-
assert.ok(ObjectModel instanceof Function, "ObjectModel instanceof Function");
|
|
34
|
-
|
|
35
|
-
const EmptyObjectModel = ObjectModel({});
|
|
36
|
-
|
|
37
|
-
assert.ok(typeof EmptyObjectModel.extend === "function", "test object model method extend");
|
|
38
|
-
assert.ok(typeof EmptyObjectModel.assert === "function", "test object model method assert");
|
|
39
|
-
assert.ok(typeof EmptyObjectModel.test === "function", "test object model method test");
|
|
40
|
-
assert.ok(typeof EmptyObjectModel.definition === "object", "test object model prop definition");
|
|
41
|
-
assert.ok(typeof EmptyObjectModel.assertions === "object", "test object model prop assertions");
|
|
42
|
-
|
|
43
|
-
const EmptyObjectModelThroughConstructor = new ObjectModel({});
|
|
44
|
-
|
|
45
|
-
assert.ok(typeof EmptyObjectModelThroughConstructor.extend === "function", "test new model method extend");
|
|
46
|
-
assert.ok(typeof EmptyObjectModelThroughConstructor.assert === "function", "test new model method assert");
|
|
47
|
-
assert.ok(typeof EmptyObjectModelThroughConstructor.test === "function", "test new model method test");
|
|
48
|
-
assert.ok(typeof EmptyObjectModelThroughConstructor.definition === "object", "test new model prop definition");
|
|
49
|
-
assert.ok(typeof EmptyObjectModelThroughConstructor.assertions === "object", "test new model prop assertions");
|
|
50
|
-
})
|
|
51
|
-
|
|
52
|
-
QUnit.test("behaviour for properties", function (assert) {
|
|
53
|
-
var Person = ObjectModel({
|
|
54
|
-
name: String,
|
|
55
|
-
age: Number,
|
|
56
|
-
birth: Date,
|
|
57
|
-
female: [Boolean],
|
|
58
|
-
address: {
|
|
59
|
-
work: {
|
|
60
|
-
city: [String]
|
|
61
|
-
}
|
|
62
|
-
}
|
|
63
|
-
});
|
|
64
|
-
|
|
65
|
-
var joe = Person({
|
|
66
|
-
name: "Joe",
|
|
67
|
-
age: 42,
|
|
68
|
-
birth: new Date(1990, 3, 25),
|
|
69
|
-
female: false,
|
|
70
|
-
address: {
|
|
71
|
-
work: {
|
|
72
|
-
city: "Lille"
|
|
73
|
-
}
|
|
74
|
-
}
|
|
75
|
-
});
|
|
76
|
-
|
|
77
|
-
assert.strictEqual(joe.name, "Joe", "String property retrieved");
|
|
78
|
-
assert.strictEqual(joe.age, 42, "Number property retrieved");
|
|
79
|
-
assert.strictEqual(joe.female, false, "Boolean property retrieved");
|
|
80
|
-
assert.equal(+joe.birth, +(new Date(1990, 3, 25)), "Date property retrieved");
|
|
81
|
-
assert.strictEqual(joe.address.work.city, "Lille", "nested property retrieved");
|
|
82
|
-
assert.ok(joe instanceof Person && joe instanceof Object, "instance is instanceof model and Object");
|
|
83
|
-
assert.ok(Person instanceof ObjectModel, "model is instanceof ObjectModel");
|
|
84
|
-
|
|
85
|
-
joe.name = "Big Joe";
|
|
86
|
-
joe.age++;
|
|
87
|
-
joe.birth = new Date(1990, 3, 26);
|
|
88
|
-
delete joe.female;
|
|
89
|
-
|
|
90
|
-
assert.throws(function () {
|
|
91
|
-
joe.name = 42;
|
|
92
|
-
}, /TypeError.*got Number 42/, "invalid Number set");
|
|
93
|
-
assert.throws(function () {
|
|
94
|
-
joe.age = true;
|
|
95
|
-
}, /TypeError.*got Boolean true/, "invalid Boolean set");
|
|
96
|
-
assert.throws(function () {
|
|
97
|
-
joe.birth = function () {
|
|
98
|
-
};
|
|
99
|
-
}, /TypeError.*got Function/, "invalid Function set");
|
|
100
|
-
assert.throws(function () {
|
|
101
|
-
joe.female = "nope";
|
|
102
|
-
}, /TypeError.*got String "nope"/, "invalid String set");
|
|
103
|
-
assert.throws(function () {
|
|
104
|
-
joe.address.work.city = [];
|
|
105
|
-
}, /TypeError.*got Array/, "invalid Array set");
|
|
106
|
-
assert.throws(function () {
|
|
107
|
-
joe.address.work = { city: 42 };
|
|
108
|
-
}, /TypeError.*got Number 42/, "invalid Object set");
|
|
109
|
-
assert.throws(function () {
|
|
110
|
-
Person({
|
|
111
|
-
name: "Joe",
|
|
112
|
-
age: 42,
|
|
113
|
-
birth: new Date(1990, 3, 25),
|
|
114
|
-
female: "false"
|
|
115
|
-
});
|
|
116
|
-
}, /TypeError.*expecting female to be Boolean.*got String "false"/,
|
|
117
|
-
"invalid prop at object model instanciation");
|
|
118
|
-
|
|
119
|
-
joe = Person({
|
|
120
|
-
name: "Joe",
|
|
121
|
-
age: 42,
|
|
122
|
-
birth: new Date(1990, 3, 25),
|
|
123
|
-
female: false,
|
|
124
|
-
address: {
|
|
125
|
-
work: {
|
|
126
|
-
city: "Lille"
|
|
127
|
-
}
|
|
128
|
-
},
|
|
129
|
-
job: "Taxi"
|
|
130
|
-
});
|
|
131
|
-
|
|
132
|
-
assert.strictEqual(joe.job, "Taxi", "Properties out of model definition are kept but are not validated");
|
|
133
|
-
|
|
134
|
-
assert.throws(function () {
|
|
135
|
-
Person({
|
|
136
|
-
name: false,
|
|
137
|
-
age: [42],
|
|
138
|
-
birth: "nope",
|
|
139
|
-
female: null,
|
|
140
|
-
address: {
|
|
141
|
-
work: {
|
|
142
|
-
city: true
|
|
143
|
-
}
|
|
144
|
-
}
|
|
145
|
-
});
|
|
146
|
-
}, function (err) {
|
|
147
|
-
return /TypeError/.test(err.toString())
|
|
148
|
-
&& /name/.test(err.toString())
|
|
149
|
-
&& /age/.test(err.toString())
|
|
150
|
-
&& /birth/.test(err.toString())
|
|
151
|
-
&& /city/.test(err.toString())
|
|
152
|
-
}, "check that errors are correctly stacked");
|
|
153
|
-
|
|
154
|
-
const A = ObjectModel({
|
|
155
|
-
id: [Number]
|
|
156
|
-
}).defaultTo({
|
|
157
|
-
setId(id) { this.id = id; }
|
|
158
|
-
})
|
|
159
|
-
|
|
160
|
-
let a = new A({})
|
|
161
|
-
assert.throws(() => { a.setId("32") }, /TypeError/, "methods should trigger type-checking");
|
|
162
|
-
|
|
163
|
-
});
|
|
164
|
-
|
|
165
|
-
QUnit.test("edge cases of constructors", function (assert) {
|
|
166
|
-
assert.ok(ObjectModel({}) instanceof ObjectModel, "ObjectModel can receive empty object as argument");
|
|
167
|
-
|
|
168
|
-
const M = ObjectModel({})
|
|
169
|
-
assert.strictEqual(M.test(undefined), true, "undefined is valid for empty objectmodels, due to null-safe object traversal")
|
|
170
|
-
assert.strictEqual(M.test(null), false, "null is invalid for empty objectmodels")
|
|
171
|
-
assert.strictEqual(M.test(1), false, "number is invalid for empty objectmodels")
|
|
172
|
-
assert.strictEqual(M.test(new Number(1)), true, "Numbers through constructor are valid for empty objectmodels")
|
|
173
|
-
assert.strictEqual(M.test("string"), false, "string is invalid for empty objectmodels")
|
|
174
|
-
assert.strictEqual(M.test(function () { }), true, "function is valid for empty objectmodels")
|
|
175
|
-
|
|
176
|
-
const O = ObjectModel({ x: [Number] })
|
|
177
|
-
assert.strictEqual(O.test(undefined), true, "undefined is valid for optional objectmodels, due to null-safe object traversal")
|
|
178
|
-
assert.strictEqual(O.test(null), false, "null is invalid for optional objectmodels")
|
|
179
|
-
assert.strictEqual(O.test(1), false, "number is invalid for optional objectmodels")
|
|
180
|
-
assert.strictEqual(O.test(new Number(1)), true, "Numbers through constructor are valid for optional objectmodels")
|
|
181
|
-
assert.strictEqual(O.test("string"), false, "string is invalid for optional objectmodels")
|
|
182
|
-
assert.strictEqual(O.test(function () { }), true, "function is valid for optional objectmodels")
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
/* //TODO: use FunctionModel for ObjectModel API ?
|
|
186
|
-
assert.throws(function () {
|
|
187
|
-
ObjectModel(undefined)
|
|
188
|
-
}, /expecting arguments\[0] to be Object, got undefined/,
|
|
189
|
-
"ObjectModel with definition undefined throws")
|
|
190
|
-
|
|
191
|
-
assert.throws(function () {
|
|
192
|
-
ObjectModel(42)
|
|
193
|
-
}, /expecting arguments\[0] to be Object, got Number 42/,
|
|
194
|
-
"ObjectModel with definition primitive throws")
|
|
195
|
-
*/
|
|
196
|
-
});
|
|
197
|
-
|
|
198
|
-
QUnit.test("optional and multiple parameters", function (assert) {
|
|
199
|
-
var Person = ObjectModel({
|
|
200
|
-
name: [String],
|
|
201
|
-
age: [Number, Date, String, Boolean, undefined],
|
|
202
|
-
female: [Boolean, Number, String, null],
|
|
203
|
-
haircolor: ["blond", "brown", "black", undefined],
|
|
204
|
-
address: {
|
|
205
|
-
work: {
|
|
206
|
-
city: [String]
|
|
207
|
-
}
|
|
208
|
-
}
|
|
209
|
-
});
|
|
210
|
-
|
|
211
|
-
var joe = Person({ female: false });
|
|
212
|
-
assert.ok(joe instanceof Person, "instanceof model test");
|
|
213
|
-
joe.name = "joe";
|
|
214
|
-
joe.name = undefined;
|
|
215
|
-
joe.name = null;
|
|
216
|
-
joe.age = new Date(1995, 1, 23);
|
|
217
|
-
joe.age = undefined;
|
|
218
|
-
assert.throws(function () {
|
|
219
|
-
joe.age = null;
|
|
220
|
-
}, /TypeError.*got null/, "invalid set null");
|
|
221
|
-
joe.female = "ann";
|
|
222
|
-
joe.female = 2;
|
|
223
|
-
joe.female = false;
|
|
224
|
-
assert.throws(function () {
|
|
225
|
-
joe.female = undefined;
|
|
226
|
-
}, /TypeError.*got undefined/, "invalid set undefined");
|
|
227
|
-
joe.address.work.city = "Lille";
|
|
228
|
-
joe.address.work.city = undefined;
|
|
229
|
-
joe.haircolor = "blond";
|
|
230
|
-
joe.haircolor = undefined;
|
|
231
|
-
assert.throws(function () {
|
|
232
|
-
joe.name = false;
|
|
233
|
-
}, /TypeError.*expecting name to be String.*got Boolean false/, "invalid type for optional prop");
|
|
234
|
-
assert.throws(function () {
|
|
235
|
-
joe.age = null;
|
|
236
|
-
}, /TypeError.*expecting age to be Number or Date or String or Boolean or undefined/, "invalid set null for optional union type prop");
|
|
237
|
-
assert.throws(function () {
|
|
238
|
-
joe.age = [];
|
|
239
|
-
}, /TypeError.*got Array/, "invalid set array for optional union type prop");
|
|
240
|
-
assert.throws(function () {
|
|
241
|
-
joe.address.work.city = 0;
|
|
242
|
-
}, /TypeError.*expecting address.work.city to be String.*got Number 0/, "invalid type for nested optional prop");
|
|
243
|
-
assert.throws(function () {
|
|
244
|
-
joe.haircolor = "";
|
|
245
|
-
}, /TypeError.*expecting haircolor to be "blond" or "brown" or "black" or undefined, got String ""/, "invalid type for value enum prop");
|
|
246
|
-
|
|
247
|
-
});
|
|
248
|
-
|
|
249
|
-
QUnit.test("fixed values", function (assert) {
|
|
250
|
-
var myModel = ObjectModel({
|
|
251
|
-
a: [1, 2, 3],
|
|
252
|
-
b: 42,
|
|
253
|
-
c: ["", false, null, 0],
|
|
254
|
-
haircolor: ["blond", "brown", "black"],
|
|
255
|
-
foo: "bar",
|
|
256
|
-
x: [Number, true]
|
|
257
|
-
});
|
|
258
|
-
|
|
259
|
-
var model = myModel({
|
|
260
|
-
a: 1,
|
|
261
|
-
b: 42,
|
|
262
|
-
c: 0,
|
|
263
|
-
haircolor: "blond",
|
|
264
|
-
foo: "bar",
|
|
265
|
-
x: true
|
|
266
|
-
});
|
|
267
|
-
|
|
268
|
-
model.x = 666;
|
|
269
|
-
model.haircolor = "brown";
|
|
270
|
-
|
|
271
|
-
assert.throws(function () {
|
|
272
|
-
model.a = 4;
|
|
273
|
-
}, /TypeError.*expecting a to be 1 or 2 or 3.*got Number 4/, 'invalid set on values enum 1/2');
|
|
274
|
-
assert.throws(function () {
|
|
275
|
-
model.b = 43;
|
|
276
|
-
}, /TypeError.*expecting b to be 42.*got Number 43/, "invalid set on fixed value 1/2");
|
|
277
|
-
assert.throws(function () {
|
|
278
|
-
model.c = undefined;
|
|
279
|
-
}, /TypeError.*expecting c to be "" or false or null or 0.*got undefined/, "invalid set undefined on mixed typed values enum");
|
|
280
|
-
assert.throws(function () {
|
|
281
|
-
model.haircolor = "roux";
|
|
282
|
-
}, /TypeError.*expecting haircolor to be "blond" or "brown" or "black".*got String "roux"/, 'invalid set on values enum 2/2');
|
|
283
|
-
assert.throws(function () {
|
|
284
|
-
model.foo = "baz";
|
|
285
|
-
}, /TypeError.*expecting foo to be "bar".*got String "baz"/, "invalid set on fixed value 2/2");
|
|
286
|
-
assert.throws(function () {
|
|
287
|
-
model.x = false;
|
|
288
|
-
}, /TypeError.*expecting x to be Number or true.*got Boolean false/, "invalid set on mixed type/values enum");
|
|
289
|
-
|
|
290
|
-
});
|
|
291
|
-
|
|
292
|
-
QUnit.test("default values", function (assert) {
|
|
293
|
-
|
|
294
|
-
let myModel = new ObjectModel({
|
|
295
|
-
name: String,
|
|
296
|
-
foo: {
|
|
297
|
-
bar: {
|
|
298
|
-
buz: Number
|
|
299
|
-
}
|
|
300
|
-
}
|
|
301
|
-
}).defaultTo({
|
|
302
|
-
name: "joe",
|
|
303
|
-
foo: {
|
|
304
|
-
bar: {
|
|
305
|
-
buz: 0
|
|
306
|
-
}
|
|
307
|
-
}
|
|
308
|
-
});
|
|
309
|
-
|
|
310
|
-
const model = myModel();
|
|
311
|
-
assert.strictEqual(model.name, "joe", "default values correctly applied");
|
|
312
|
-
assert.strictEqual(model.foo.bar.buz, 0, "default nested props values correctly applied");
|
|
313
|
-
assert.ok(myModel.test({}), "default should be applied when testing autocasted objects")
|
|
314
|
-
|
|
315
|
-
const model2 = myModel({ name: "jim", foo: { bar: { buz: 1 } } });
|
|
316
|
-
assert.strictEqual(model2.name, "jim", "default values not applied if provided");
|
|
317
|
-
assert.strictEqual(model2.foo.bar.buz, 1, "default nested props values not applied if provided");
|
|
318
|
-
|
|
319
|
-
const Person = Model({
|
|
320
|
-
name: String,
|
|
321
|
-
age: [Number]
|
|
322
|
-
}).defaultTo({
|
|
323
|
-
name: "new-name"
|
|
324
|
-
});
|
|
325
|
-
|
|
326
|
-
const Team = Model({
|
|
327
|
-
lead: Person,
|
|
328
|
-
members: ArrayModel(Person)
|
|
329
|
-
});
|
|
330
|
-
|
|
331
|
-
assert.strictEqual((new Team({ lead: new Person(), members: [] })).lead.name, "new-name", "default value through composition")
|
|
332
|
-
assert.throws(() => { new Team({ lead: 1, members: [] }) }, "invalid value through composition with default")
|
|
333
|
-
|
|
334
|
-
assert.throws(() => { myModel.defaultTo({ name: undefined }) }, /TypeError.*expecting name to be String, got undefined/, "check definition of provided defaults")
|
|
335
|
-
assert.throws(() => { myModel.defaultTo({ foo: { bar: { buz: "nope" } } }) }, /TypeError.*expecting foo.bar.buz to be Number, got String/, "check nested definition of provided defaults")
|
|
336
|
-
|
|
337
|
-
myModel = new ObjectModel({ x: Number, y: String })
|
|
338
|
-
.defaultTo({ x: 42, y: "hello" })
|
|
339
|
-
|
|
340
|
-
assert.strictEqual(myModel.default.x, 42, "object model defaults store the value as default property")
|
|
341
|
-
assert.strictEqual(myModel().x, 42, "object model default property is applied when undefined is passed");
|
|
342
|
-
assert.strictEqual(myModel().y, "hello", "defaulted object model still inherit from model proto");
|
|
343
|
-
|
|
344
|
-
myModel.default.x = "nope";
|
|
345
|
-
|
|
346
|
-
assert.throws(function () {
|
|
347
|
-
myModel()
|
|
348
|
-
}, /TypeError.*got String "nope"/, "invalid default property still throws TypeError for object models");
|
|
349
|
-
|
|
350
|
-
});
|
|
351
|
-
|
|
352
|
-
QUnit.test("RegExp values", function (assert) {
|
|
353
|
-
|
|
354
|
-
const myModel = ObjectModel({
|
|
355
|
-
phonenumber: /^[0-9]{10}$/,
|
|
356
|
-
voyels: [/^[aeiouy]+$/]
|
|
357
|
-
});
|
|
358
|
-
|
|
359
|
-
const m = myModel({
|
|
360
|
-
phonenumber: "0612345678"
|
|
361
|
-
});
|
|
362
|
-
|
|
363
|
-
m.voyels = "ouioui";
|
|
364
|
-
|
|
365
|
-
assert.throws(function () {
|
|
366
|
-
m.voyels = "nonnon"
|
|
367
|
-
}, /TypeError/, "regex matching");
|
|
368
|
-
assert.throws(function () {
|
|
369
|
-
m.phonenumber = "123456789"
|
|
370
|
-
}, /TypeError/, "regex matching 2");
|
|
371
|
-
|
|
372
|
-
});
|
|
373
|
-
|
|
374
|
-
QUnit.test("Private and constant properties", function (assert) {
|
|
375
|
-
|
|
376
|
-
let myModel = ObjectModel({
|
|
377
|
-
CONST: Number,
|
|
378
|
-
_private: Number,
|
|
379
|
-
normal: Number
|
|
380
|
-
});
|
|
381
|
-
|
|
382
|
-
let m = myModel({
|
|
383
|
-
CONST: 42,
|
|
384
|
-
_private: 43,
|
|
385
|
-
normal: 44
|
|
386
|
-
});
|
|
387
|
-
|
|
388
|
-
m.normal++;
|
|
389
|
-
|
|
390
|
-
assert.throws(function () {
|
|
391
|
-
m._private++;
|
|
392
|
-
}, /TypeError[\s\S]*private/, "try to modify private");
|
|
393
|
-
|
|
394
|
-
assert.throws(function () {
|
|
395
|
-
m.CONST++;
|
|
396
|
-
}, /TypeError[\s\S]*constant/, "try to modify constant");
|
|
397
|
-
assert.equal(Object.keys(m).length, 2, "non enumerable key not counted by Object.keys");
|
|
398
|
-
assert.equal(Object.keys(m).includes("_private"), false, "non enumerable key not found in Object.keys");
|
|
399
|
-
assert.equal(Object.getOwnPropertyNames(m).length, 2, "non enumerable key not counted by Object.getOwnPropertyNames");
|
|
400
|
-
assert.equal(Object.getOwnPropertyNames(m).includes("_private"), false, "non enumerable key not found in Object.getOwnPropertyNames");
|
|
401
|
-
assert.equal("normal" in m, true, "enumerable key found with operator in")
|
|
402
|
-
assert.equal("_private" in m, false, "non enumerable key not found with operator in")
|
|
403
|
-
assert.equal(Object.getOwnPropertyDescriptor(m, "normal").value, 45, "getOwnProperyDescriptor trap for normal prop")
|
|
404
|
-
assert.equal(Object.getOwnPropertyDescriptor(m, "_private"), undefined, "getOwnProperyDescriptor for private prop")
|
|
405
|
-
|
|
406
|
-
let M = ObjectModel({ _p: Number })
|
|
407
|
-
m = M({ _p: 42 })
|
|
408
|
-
|
|
409
|
-
assert.throws(function () {
|
|
410
|
-
Object.prototype.toString.call(m._p);
|
|
411
|
-
}, /TypeError[\s\S]*cannot access to private/, "try to access private from outside");
|
|
412
|
-
|
|
413
|
-
M.prototype.incrementPrivate = function () { this._p++ }
|
|
414
|
-
M.prototype.getPrivate = function () { return this._p }
|
|
415
|
-
m.incrementPrivate();
|
|
416
|
-
assert.equal(m.getPrivate(), 43, "can access and mutate private props through methods")
|
|
417
|
-
|
|
418
|
-
const A = ObjectModel({
|
|
419
|
-
_id: [Number]
|
|
420
|
-
}).defaultTo({
|
|
421
|
-
getId() { return this._id },
|
|
422
|
-
setId(id) { this._id = id; }
|
|
423
|
-
})
|
|
424
|
-
|
|
425
|
-
let a = new A({})
|
|
426
|
-
a.setId(32);
|
|
427
|
-
assert.equal(a.getId(), 32, "methods should be able to access and modify private vars");
|
|
428
|
-
|
|
429
|
-
const B = ObjectModel({
|
|
430
|
-
ID: [Number]
|
|
431
|
-
}).defaultTo({
|
|
432
|
-
setId(id) { this.ID = id; }
|
|
433
|
-
})
|
|
434
|
-
|
|
435
|
-
let b = new B({ ID: 0 })
|
|
436
|
-
assert.throws(() => { b.setId(32) }, /TypeError: cannot modify constant property ID/, "methods should not be able to modify constants");
|
|
437
|
-
|
|
438
|
-
const Circle = ObjectModel({
|
|
439
|
-
radius: Number, // public
|
|
440
|
-
_index: Number, // private
|
|
441
|
-
UNIT: ["px", "cm"], // constant
|
|
442
|
-
_ID: [Number], // private and constant
|
|
443
|
-
}).defaultTo({
|
|
444
|
-
_index: 0,
|
|
445
|
-
getIndex() { return this._index },
|
|
446
|
-
setIndex(value) { this._index = value }
|
|
447
|
-
});
|
|
448
|
-
|
|
449
|
-
let c = new Circle({ radius: 120, UNIT: "px", _ID: 1 });
|
|
450
|
-
c.radius = 100;
|
|
451
|
-
|
|
452
|
-
assert.throws(() => { c.UNIT = "cm" }, /TypeError: cannot modify constant property UNIT/, "cannot redefine constant");
|
|
453
|
-
assert.throws(() => { c._index = 1; }, /TypeError: cannot modify private property _index/, "cannot modify private property")
|
|
454
|
-
assert.throws(() => { c._index }, /TypeError: cannot access to private property _index/, "cannot access private property")
|
|
455
|
-
|
|
456
|
-
c.setIndex(2);
|
|
457
|
-
assert.strictEqual(c.getIndex(), 2, "can access and mutate private through method");
|
|
458
|
-
|
|
459
|
-
// change the private convention for all models
|
|
460
|
-
let initialConventionForPrivate = Model.prototype.conventionForPrivate;
|
|
461
|
-
Model.prototype.conventionForPrivate = key => key === "radius";
|
|
462
|
-
|
|
463
|
-
// remove the constant convention for Circle model
|
|
464
|
-
Circle.conventionForConstant = () => false;
|
|
465
|
-
|
|
466
|
-
c.UNIT = "cm";
|
|
467
|
-
assert.strictEqual(c.UNIT, "cm", "constant convention can be changed specifically for model")
|
|
468
|
-
|
|
469
|
-
assert.throws(() => { c.radius }, /TypeError[\s\S]*cannot access to private property/, "private convention can be changed")
|
|
470
|
-
c._index = 3;
|
|
471
|
-
assert.strictEqual(c._index, 3, "private convention can be changed and privates can be accessed and mutated")
|
|
472
|
-
|
|
473
|
-
Model.prototype.conventionForPrivate = initialConventionForPrivate;
|
|
474
|
-
|
|
475
|
-
class OM extends ObjectModel({
|
|
476
|
-
_privString: [String],
|
|
477
|
-
pubNum: [Number]
|
|
478
|
-
}) { }
|
|
479
|
-
|
|
480
|
-
class ParentOM extends ObjectModel({
|
|
481
|
-
_id: [String],
|
|
482
|
-
om: OM
|
|
483
|
-
}) { }
|
|
484
|
-
|
|
485
|
-
|
|
486
|
-
let nestedOM = new OM({
|
|
487
|
-
_privString: "only for me",
|
|
488
|
-
pubNum: 42
|
|
489
|
-
});
|
|
490
|
-
|
|
491
|
-
let parent = new ParentOM({ om: nestedOM });
|
|
492
|
-
assert.ok(parent instanceof ParentOM, "can nest private prop in a child OM");
|
|
493
|
-
assert.throws(() => parent.om._privString, /TypeError[\s\S]*cannot access to private property/, "cannot access nested private prop in a child OM");
|
|
494
|
-
|
|
495
|
-
const O = ObjectModel({ _priv: String }).defaultTo({
|
|
496
|
-
getPriv() {
|
|
497
|
-
this.randomMethod();
|
|
498
|
-
return this._priv
|
|
499
|
-
},
|
|
500
|
-
randomMethod() { }
|
|
501
|
-
})
|
|
502
|
-
|
|
503
|
-
const o = new O({ _priv: "test" })
|
|
504
|
-
assert.strictEqual(o.getPriv(), "test", "can grant private access even if several methods are called");
|
|
505
|
-
});
|
|
506
|
-
|
|
507
|
-
QUnit.test("Non-enumerable and non-writable properties with overridden convention", function (assert) {
|
|
508
|
-
|
|
509
|
-
const myModel = ObjectModel({
|
|
510
|
-
private_prop: Number,
|
|
511
|
-
constant_prop: Number,
|
|
512
|
-
normal_prop: Number
|
|
513
|
-
});
|
|
514
|
-
|
|
515
|
-
myModel.conventionForConstant = s => s.indexOf("constant_") === 0;
|
|
516
|
-
myModel.conventionForPrivate = s => s.indexOf("private_") === 0;
|
|
517
|
-
|
|
518
|
-
const m = myModel({
|
|
519
|
-
private_prop: 42,
|
|
520
|
-
constant_prop: 43,
|
|
521
|
-
normal_prop: 44
|
|
522
|
-
});
|
|
523
|
-
|
|
524
|
-
assert.throws(function () {
|
|
525
|
-
m.constant_prop++;
|
|
526
|
-
}, /TypeError[\s\S]*constant/, "try to redefine constant with overridden convention");
|
|
527
|
-
assert.equal(Object.keys(m).length, 2, "non enumerable key not counted by Object.keys with overridden convention");
|
|
528
|
-
assert.equal(Object.keys(m).includes("private_prop"), false, "non enumerable key not found in Object.keys with overridden convention");
|
|
529
|
-
assert.equal(Object.getOwnPropertyNames(m).length, 2, "non enumerable key not counted by Object.getOwnPropertyNames with overridden convention");
|
|
530
|
-
assert.equal(Object.getOwnPropertyNames(m).includes("private_prop"), false, "non enumerable key not found in Object.getOwnPropertyNames with overridden convention");
|
|
531
|
-
assert.equal("normal_prop" in m, true, "enumerable key found with operator in with overridden convention")
|
|
532
|
-
assert.equal("private_prop" in m, false, "non enumerable key not found with operator in with overridden convention")
|
|
533
|
-
|
|
534
|
-
});
|
|
535
|
-
|
|
536
|
-
QUnit.test("Extensions", function (assert) {
|
|
537
|
-
|
|
538
|
-
const Person = ObjectModel({
|
|
539
|
-
name: String,
|
|
540
|
-
age: Number,
|
|
541
|
-
birth: Date,
|
|
542
|
-
female: [Boolean],
|
|
543
|
-
address: {
|
|
544
|
-
work: {
|
|
545
|
-
city: [String]
|
|
546
|
-
}
|
|
547
|
-
}
|
|
548
|
-
});
|
|
549
|
-
|
|
550
|
-
const joe = Person({
|
|
551
|
-
name: "Joe",
|
|
552
|
-
age: 42,
|
|
553
|
-
birth: new Date(1990, 3, 25),
|
|
554
|
-
female: false,
|
|
555
|
-
address: {
|
|
556
|
-
work: {
|
|
557
|
-
city: "Lille"
|
|
558
|
-
}
|
|
559
|
-
}
|
|
560
|
-
});
|
|
561
|
-
|
|
562
|
-
const Woman = Person.extend({ female: true });
|
|
563
|
-
|
|
564
|
-
assert.ok(Person(joe), "Person valid model for joe");
|
|
565
|
-
|
|
566
|
-
assert.throws(function () {
|
|
567
|
-
Woman(joe);
|
|
568
|
-
}, /TypeError[\s\S]*female/, "Woman invalid model for joe");
|
|
569
|
-
|
|
570
|
-
assert.throws(function () {
|
|
571
|
-
Woman({
|
|
572
|
-
name: "Joe",
|
|
573
|
-
age: 42,
|
|
574
|
-
birth: new Date(1990, 3, 25),
|
|
575
|
-
female: false,
|
|
576
|
-
address: {
|
|
577
|
-
work: {
|
|
578
|
-
city: "Lille"
|
|
579
|
-
}
|
|
580
|
-
}
|
|
581
|
-
});
|
|
582
|
-
}, /TypeError[\s\S]*female/, "cant be woman from joe parameters");
|
|
583
|
-
|
|
584
|
-
assert.throws(function () {
|
|
585
|
-
Woman(joe);
|
|
586
|
-
}, /TypeError[\s\S]*female/, "cant be woman from Person joe");
|
|
587
|
-
|
|
588
|
-
const ann = Woman({
|
|
589
|
-
name: "Joe's wife",
|
|
590
|
-
age: 42,
|
|
591
|
-
birth: new Date(1990, 3, 25),
|
|
592
|
-
female: true,
|
|
593
|
-
address: {
|
|
594
|
-
work: {
|
|
595
|
-
city: "Lille"
|
|
596
|
-
}
|
|
597
|
-
}
|
|
598
|
-
});
|
|
599
|
-
|
|
600
|
-
const UnemployedWoman = Woman.extend({
|
|
601
|
-
address: {
|
|
602
|
-
work: {
|
|
603
|
-
city: undefined
|
|
604
|
-
}
|
|
605
|
-
}
|
|
606
|
-
});
|
|
607
|
-
|
|
608
|
-
assert.ok(Woman(ann), "Woman valid model for ann");
|
|
609
|
-
|
|
610
|
-
assert.ok(Woman.prototype.constructor === Woman, "extended model has a new constructor");
|
|
611
|
-
assert.ok(ann.constructor === Woman, "extended model instance has the right constructor");
|
|
612
|
-
|
|
613
|
-
assert.throws(function () {
|
|
614
|
-
UnemployedWoman(ann);
|
|
615
|
-
}, /TypeError[\s\S]*city/, "ann cant be UnemployedWoman; model extension nested undefined property");
|
|
616
|
-
|
|
617
|
-
|
|
618
|
-
const jane = UnemployedWoman({
|
|
619
|
-
name: "Jane",
|
|
620
|
-
age: 52,
|
|
621
|
-
birth: new Date(1990, 3, 25),
|
|
622
|
-
female: true
|
|
623
|
-
});
|
|
624
|
-
|
|
625
|
-
assert.ok(ann instanceof Person, "ann instanceof Person");
|
|
626
|
-
assert.ok(ann instanceof Woman, "ann instanceof Woman");
|
|
627
|
-
assert.ok(jane instanceof Person, "jane instanceof Person");
|
|
628
|
-
assert.ok(jane instanceof Woman, "jane instanceof Woman");
|
|
629
|
-
assert.ok(jane instanceof UnemployedWoman, "jane instanceof UnemployedWoman");
|
|
630
|
-
assert.equal(joe instanceof Woman, false, "joe not instanceof Woman");
|
|
631
|
-
assert.equal(joe instanceof UnemployedWoman, false, "joe not instanceof UnemployedWoman");
|
|
632
|
-
assert.equal(ann instanceof UnemployedWoman, false, "ann not instanceof UnemployedWoman");
|
|
633
|
-
|
|
634
|
-
let Vehicle = { speed: Number };
|
|
635
|
-
let Car = Object.create(Vehicle);
|
|
636
|
-
let Ferrari = ObjectModel({ expensive: true }).extend(Car);
|
|
637
|
-
assert.ok("speed" in Ferrari.definition, "should retrieve definitions from parent prototypes when extending with objects");
|
|
638
|
-
|
|
639
|
-
Vehicle = function () { };
|
|
640
|
-
Vehicle.prototype.speed = 99;
|
|
641
|
-
Car = function () { };
|
|
642
|
-
Car.prototype = new Vehicle();
|
|
643
|
-
Ferrari = ObjectModel({ price: [Number] }).extend(Car);
|
|
644
|
-
|
|
645
|
-
let ferrari = new Ferrari({ price: 999999 });
|
|
646
|
-
assert.equal(ferrari.speed, 99, "should retrieve properties from parent prototypes when extending with constructors");
|
|
647
|
-
assert.equal("price" in ferrari, true, "should trap in operator and return true for properties in definition");
|
|
648
|
-
assert.equal("speed" in ferrari, false, "should trap in operator and return false for properties out of definition");
|
|
649
|
-
|
|
650
|
-
});
|
|
651
|
-
|
|
652
|
-
QUnit.test("Multiple inheritance", function (assert) {
|
|
653
|
-
|
|
654
|
-
const A = new ObjectModel({
|
|
655
|
-
a: Boolean,
|
|
656
|
-
b: Boolean
|
|
657
|
-
});
|
|
658
|
-
|
|
659
|
-
const B = ObjectModel({
|
|
660
|
-
b: Number,
|
|
661
|
-
c: Number
|
|
662
|
-
});
|
|
663
|
-
|
|
664
|
-
const C = ObjectModel({
|
|
665
|
-
c: String,
|
|
666
|
-
d: {
|
|
667
|
-
d1: Boolean,
|
|
668
|
-
d2: Boolean
|
|
669
|
-
}
|
|
670
|
-
});
|
|
671
|
-
|
|
672
|
-
const D = ObjectModel({
|
|
673
|
-
a: String,
|
|
674
|
-
d: {
|
|
675
|
-
d2: Number,
|
|
676
|
-
d3: Number
|
|
677
|
-
}
|
|
678
|
-
});
|
|
679
|
-
|
|
680
|
-
let M1 = A.extend(B, C, D);
|
|
681
|
-
let M2 = D.extend(C, B, A);
|
|
682
|
-
|
|
683
|
-
assert.equal(Object.keys(M1.definition).sort().join(','), "a,b,c,d", "definition merge for multiple inheritance 1/4");
|
|
684
|
-
assert.equal(Object.keys(M2.definition).sort().join(','), "a,b,c,d", "definition merge for multiple inheritance 2/4");
|
|
685
|
-
assert.equal(Object.keys(M1.definition.d).sort().join(','), "d1,d2,d3", "definition merge for multiple inheritance 3/4");
|
|
686
|
-
assert.equal(Object.keys(M2.definition.d).sort().join(','), "d1,d2,d3", "definition merge for multiple inheritance 4/4");
|
|
687
|
-
|
|
688
|
-
let m1 = M1({
|
|
689
|
-
a: "",
|
|
690
|
-
b: 42,
|
|
691
|
-
c: "test",
|
|
692
|
-
d: {
|
|
693
|
-
d1: true,
|
|
694
|
-
d2: 2,
|
|
695
|
-
d3: 3
|
|
696
|
-
}
|
|
697
|
-
});
|
|
698
|
-
|
|
699
|
-
let m2 = M2({
|
|
700
|
-
a: false,
|
|
701
|
-
b: false,
|
|
702
|
-
c: 666,
|
|
703
|
-
d: {
|
|
704
|
-
d1: false,
|
|
705
|
-
d2: false,
|
|
706
|
-
d3: 0
|
|
707
|
-
}
|
|
708
|
-
});
|
|
709
|
-
|
|
710
|
-
assert.throws(function () {
|
|
711
|
-
m1.a = true;
|
|
712
|
-
}, /TypeError[\s\S]*a/, "type checking multiple inheritance 1/8");
|
|
713
|
-
assert.throws(function () {
|
|
714
|
-
m2.a = "nope";
|
|
715
|
-
}, /TypeError[\s\S]*a/, "type checking multiple inheritance 2/8");
|
|
716
|
-
assert.throws(function () {
|
|
717
|
-
m1.b = !m1.b;
|
|
718
|
-
}, /TypeError[\s\S]*b/, "type checking multiple inheritance 3/8");
|
|
719
|
-
assert.throws(function () {
|
|
720
|
-
m2.b += 7;
|
|
721
|
-
}, /TypeError[\s\S]*b/, "type checking multiple inheritance 4/8");
|
|
722
|
-
assert.throws(function () {
|
|
723
|
-
m1.c = undefined;
|
|
724
|
-
}, /TypeError[\s\S]*c/, "type checking multiple inheritance 5/8");
|
|
725
|
-
assert.throws(function () {
|
|
726
|
-
m2.c = null;
|
|
727
|
-
}, /TypeError[\s\S]*c/, "type checking multiple inheritance 6/8");
|
|
728
|
-
assert.throws(function () {
|
|
729
|
-
m1.d.d2 = true;
|
|
730
|
-
}, /TypeError[\s\S]*d2/, "type checking multiple inheritance 7/8");
|
|
731
|
-
assert.throws(function () {
|
|
732
|
-
m2.d.d2 = 1;
|
|
733
|
-
}, /TypeError[\s\S]*d2/, "type checking multiple inheritance 8/8");
|
|
734
|
-
|
|
735
|
-
A.defaultTo({
|
|
736
|
-
a: false,
|
|
737
|
-
b: false
|
|
738
|
-
});
|
|
739
|
-
|
|
740
|
-
B.defaultTo({
|
|
741
|
-
b: 0,
|
|
742
|
-
c: 0
|
|
743
|
-
});
|
|
744
|
-
|
|
745
|
-
C.defaultTo({
|
|
746
|
-
c: "",
|
|
747
|
-
d: {
|
|
748
|
-
d1: false,
|
|
749
|
-
d2: false
|
|
750
|
-
}
|
|
751
|
-
});
|
|
752
|
-
|
|
753
|
-
D.defaultTo({
|
|
754
|
-
a: "",
|
|
755
|
-
d: {
|
|
756
|
-
d2: 0,
|
|
757
|
-
d3: 0
|
|
758
|
-
}
|
|
759
|
-
});
|
|
760
|
-
|
|
761
|
-
M1 = A.extend(B, C, D);
|
|
762
|
-
M2 = D.extend(C, B, A);
|
|
763
|
-
m1 = M1();
|
|
764
|
-
m2 = M2();
|
|
765
|
-
|
|
766
|
-
assert.ok(m1.a === "" && m1.b === 0 && m1.c === "" && m1.d.d1 === false && m1.d.d2 === 0 && m1.d.d3 === 0, "defaults checking multiple inheritance 1/2");
|
|
767
|
-
assert.ok(m2.a === false && m2.b === false && m2.c === 0 && m2.d.d1 === false && m2.d.d2 === false && m2.d.d3 === 0, "defaults checking multiple inheritance 2/2");
|
|
768
|
-
|
|
769
|
-
function dummyAssert() {
|
|
770
|
-
return true;
|
|
771
|
-
}
|
|
772
|
-
|
|
773
|
-
A.assert(dummyAssert);
|
|
774
|
-
B.assert(dummyAssert);
|
|
775
|
-
C.assert(dummyAssert);
|
|
776
|
-
D.assert(dummyAssert);
|
|
777
|
-
|
|
778
|
-
M1 = A.extend(B, C, D);
|
|
779
|
-
M2 = D.extend(C, B, A);
|
|
780
|
-
m1 = M1();
|
|
781
|
-
m2 = M2();
|
|
782
|
-
|
|
783
|
-
assert.ok(M1.assertions.length === 4, "assertions checking multiple inheritance 1/2");
|
|
784
|
-
assert.ok(M2.assertions.length === 4, "assertions checking multiple inheritance 2/2");
|
|
785
|
-
|
|
786
|
-
});
|
|
787
|
-
|
|
788
|
-
QUnit.test("Composition", function (assert) {
|
|
789
|
-
|
|
790
|
-
const Person = ObjectModel({
|
|
791
|
-
name: String,
|
|
792
|
-
age: [Number, Date],
|
|
793
|
-
female: [Boolean],
|
|
794
|
-
address: {
|
|
795
|
-
work: {
|
|
796
|
-
city: [String]
|
|
797
|
-
}
|
|
798
|
-
}
|
|
799
|
-
});
|
|
800
|
-
|
|
801
|
-
const Family = ObjectModel({
|
|
802
|
-
father: Person,
|
|
803
|
-
mother: Person.extend({ female: true }),
|
|
804
|
-
children: ArrayModel(Person),
|
|
805
|
-
grandparents: [ArrayModel(Person).assert(function (persons) {
|
|
806
|
-
return persons && persons.length <= 4
|
|
807
|
-
})]
|
|
808
|
-
});
|
|
809
|
-
|
|
810
|
-
const joe = Person({
|
|
811
|
-
name: "Joe",
|
|
812
|
-
age: 42,
|
|
813
|
-
female: false
|
|
814
|
-
});
|
|
815
|
-
|
|
816
|
-
|
|
817
|
-
const ann = new Person({
|
|
818
|
-
female: true,
|
|
819
|
-
age: joe.age - 5,
|
|
820
|
-
name: joe.name + "'s wife"
|
|
821
|
-
});
|
|
822
|
-
|
|
823
|
-
let joefamily = new Family({
|
|
824
|
-
father: joe,
|
|
825
|
-
mother: ann,
|
|
826
|
-
children: [],
|
|
827
|
-
grandparents: []
|
|
828
|
-
});
|
|
829
|
-
|
|
830
|
-
assert.ok(joefamily instanceof Family, "joefamily instance of Family");
|
|
831
|
-
assert.ok(joefamily.father instanceof Person, "father instanceof Person");
|
|
832
|
-
assert.ok(joefamily.mother instanceof Person, "mother instanceof Person");
|
|
833
|
-
|
|
834
|
-
const duckmother = {
|
|
835
|
-
female: true,
|
|
836
|
-
age: joe.age - 5,
|
|
837
|
-
name: joe.name + "'s wife"
|
|
838
|
-
};
|
|
839
|
-
|
|
840
|
-
joefamily = new Family({
|
|
841
|
-
father: joe,
|
|
842
|
-
mother: duckmother,
|
|
843
|
-
children: []
|
|
844
|
-
});
|
|
845
|
-
|
|
846
|
-
assert.ok(Person.test(duckmother), "Autocasting for object properties 1/2");
|
|
847
|
-
assert.notOk(duckmother instanceof Person, "Autocasting for object properties 2/2");
|
|
848
|
-
|
|
849
|
-
joefamily.mother.name = "Daisy";
|
|
850
|
-
assert.equal(joefamily.mother.name, "Daisy", "Autocasted submodel property can be modified");
|
|
851
|
-
assert.throws(function () {
|
|
852
|
-
joefamily.mother.female = "Quack !";
|
|
853
|
-
}, /TypeError[\s\S]*female/, "validation of submodel autocasted at modification");
|
|
854
|
-
|
|
855
|
-
assert.throws(function () {
|
|
856
|
-
new Family({
|
|
857
|
-
father: joe,
|
|
858
|
-
mother: {
|
|
859
|
-
female: false,
|
|
860
|
-
age: joe.age - 5,
|
|
861
|
-
name: joe.name + "'s wife"
|
|
862
|
-
},
|
|
863
|
-
children: []
|
|
864
|
-
});
|
|
865
|
-
}, /TypeError[\s\S]*female/, "validation of submodel autocasted at instanciation");
|
|
866
|
-
|
|
867
|
-
});
|
|
868
|
-
|
|
869
|
-
QUnit.test("Assertions", function (assert) {
|
|
870
|
-
|
|
871
|
-
const NestedModel = ObjectModel({ foo: { bar: { baz: Boolean } } })
|
|
872
|
-
.assert(o => o.foo.bar.baz === true);
|
|
873
|
-
|
|
874
|
-
const nestedModel = NestedModel({ foo: { bar: { baz: true } } });
|
|
875
|
-
|
|
876
|
-
assert.throws(function () {
|
|
877
|
-
nestedModel.foo.bar.baz = false;
|
|
878
|
-
}, /TypeError/, "test assertion after nested property assignment");
|
|
879
|
-
|
|
880
|
-
function assertFail() {
|
|
881
|
-
return false;
|
|
882
|
-
}
|
|
883
|
-
|
|
884
|
-
function assertFailWithData() {
|
|
885
|
-
return -1;
|
|
886
|
-
}
|
|
887
|
-
|
|
888
|
-
Model.prototype.assert(assertFail, "expected message without data");
|
|
889
|
-
ObjectModel.prototype.assert(assertFailWithData, function (data) {
|
|
890
|
-
return "expected message with data " + data;
|
|
891
|
-
});
|
|
892
|
-
|
|
893
|
-
assert.equal(Model.prototype.assertions.length, 1, "check number of assertions on BasicModel.prototype")
|
|
894
|
-
assert.equal(ObjectModel.prototype.assertions.length, 2, "check number of assertions on ObjectModel.prototype");
|
|
895
|
-
|
|
896
|
-
const M = ObjectModel({ a: String });
|
|
897
|
-
|
|
898
|
-
assert.throws(function () {
|
|
899
|
-
M({ a: "test" })
|
|
900
|
-
}, /TypeError/, "expected message without data");
|
|
901
|
-
|
|
902
|
-
assert.throws(function () {
|
|
903
|
-
M({ a: "test" })
|
|
904
|
-
}, /TypeError/, "expected message with data -1");
|
|
905
|
-
|
|
906
|
-
// clean up global assertions
|
|
907
|
-
Model.prototype.assertions = [];
|
|
908
|
-
delete ObjectModel.prototype.assertions;
|
|
909
|
-
|
|
910
|
-
const AssertObject = ObjectModel({ name: [String] })
|
|
911
|
-
.assert((o => o.name.toLowerCase().length === o.name.length), "may throw exception");
|
|
912
|
-
|
|
913
|
-
new AssertObject({ name: "joe" });
|
|
914
|
-
|
|
915
|
-
assert.throws(function () {
|
|
916
|
-
new AssertObject({ name: undefined });
|
|
917
|
-
},
|
|
918
|
-
/assertion "may throw exception" returned TypeError.*for value {\s+name: undefined\s+}/,
|
|
919
|
-
"assertions catch exceptions on Object models");
|
|
920
|
-
|
|
921
|
-
});
|
|
922
|
-
|
|
923
|
-
QUnit.test("test method", function (assert) {
|
|
924
|
-
|
|
925
|
-
const assertFunction = (c => c === "GB");
|
|
926
|
-
|
|
927
|
-
assertFunction.toString = function () {
|
|
928
|
-
return "expected assertFunction toString";
|
|
929
|
-
}
|
|
930
|
-
|
|
931
|
-
const Address = new ObjectModel({
|
|
932
|
-
city: String,
|
|
933
|
-
country: BasicModel(String).assert(assertFunction, "Country must be GB")
|
|
934
|
-
});
|
|
935
|
-
|
|
936
|
-
function isPositive(n) { return n > 0 }
|
|
937
|
-
const PositiveNumber = BasicModel(Number)
|
|
938
|
-
.assert(isPositive, "expected assertion description")
|
|
939
|
-
.as('PositiveNumber');
|
|
940
|
-
|
|
941
|
-
const gbAddress = { city: "London", country: "GB" };
|
|
942
|
-
const frAddress = { city: "Paris", country: "FR" };
|
|
943
|
-
|
|
944
|
-
const Order = new ObjectModel({
|
|
945
|
-
sku: String,
|
|
946
|
-
address: Address,
|
|
947
|
-
quantity: [PositiveNumber]
|
|
948
|
-
});
|
|
949
|
-
|
|
950
|
-
const gbOrder = { sku: "ABC123", address: gbAddress };
|
|
951
|
-
const frOrder = { sku: "ABC123", address: frAddress };
|
|
952
|
-
|
|
953
|
-
new Order(gbOrder); // no errors
|
|
954
|
-
assert.throws(function () {
|
|
955
|
-
new Order(frOrder);
|
|
956
|
-
}, "should validate sub-objects assertions");
|
|
957
|
-
|
|
958
|
-
let errors = [];
|
|
959
|
-
Order.test(frOrder, function (err) {
|
|
960
|
-
errors.push(...err);
|
|
961
|
-
});
|
|
962
|
-
|
|
963
|
-
assert.equal(errors.length, 1, "should throw exactly one error here")
|
|
964
|
-
assert.equal(errors[0].expected, "expected assertFunction toString", "check assertion error expected parameter");
|
|
965
|
-
assert.equal(errors[0].received, "FR", "check assertion error received parameter");
|
|
966
|
-
assert.equal(errors[0].path, "address.country", "check assertion error path parameter");
|
|
967
|
-
assert.equal(errors[0].message, 'assertion "Country must be GB" returned false for address.country = "FR"', "check assertion error message parameter");
|
|
968
|
-
|
|
969
|
-
gbOrder.quantity = -1
|
|
970
|
-
errors = [];
|
|
971
|
-
Order.test(gbOrder, function (err) {
|
|
972
|
-
errors.push(...err);
|
|
973
|
-
});
|
|
974
|
-
|
|
975
|
-
assert.equal(errors.length, 1, "should throw exactly one error here")
|
|
976
|
-
assert.equal(errors[0].expected, isPositive, "check assertion error expected parameter");
|
|
977
|
-
assert.equal(errors[0].received, "-1", "check assertion error received parameter");
|
|
978
|
-
assert.equal(errors[0].path, "quantity", "check assertion error path parameter");
|
|
979
|
-
assert.equal(errors[0].message, `assertion "expected assertion description" returned false for quantity = -1`, "check assertion error message parameter");
|
|
980
|
-
|
|
981
|
-
});
|
|
982
|
-
|
|
983
|
-
QUnit.test("Cyclic detection", function (assert) {
|
|
984
|
-
|
|
985
|
-
let A, B, a, b;
|
|
986
|
-
|
|
987
|
-
A = ObjectModel({ b: [] });
|
|
988
|
-
B = ObjectModel({ a: A });
|
|
989
|
-
A.definition.b = [B];
|
|
990
|
-
|
|
991
|
-
a = A();
|
|
992
|
-
b = B({ a: a });
|
|
993
|
-
|
|
994
|
-
assert.ok(a.b = b, "valid cyclic value assignment");
|
|
995
|
-
assert.throws(function () { a.b = a; }, /TypeError/, "invalid cyclic value assignment");
|
|
996
|
-
|
|
997
|
-
A = ObjectModel({ b: [] });
|
|
998
|
-
B = ObjectModel({ a: A });
|
|
999
|
-
|
|
1000
|
-
A.definition.b = {
|
|
1001
|
-
c: {
|
|
1002
|
-
d: [B]
|
|
1003
|
-
}
|
|
1004
|
-
};
|
|
1005
|
-
|
|
1006
|
-
a = A();
|
|
1007
|
-
b = B({ a: a });
|
|
1008
|
-
|
|
1009
|
-
assert.ok((a.b = { c: { d: b } }), "valid deep cyclic value assignment");
|
|
1010
|
-
assert.throws(function () {
|
|
1011
|
-
a.b = { c: { d: a } };
|
|
1012
|
-
}, /TypeError/, "invalid deep cyclic value assignment");
|
|
1013
|
-
|
|
1014
|
-
const Honey = ObjectModel({
|
|
1015
|
-
sweetie: [] // Sweetie is not yet defined
|
|
1016
|
-
});
|
|
1017
|
-
|
|
1018
|
-
const Sweetie = ObjectModel({
|
|
1019
|
-
honey: Honey
|
|
1020
|
-
});
|
|
1021
|
-
|
|
1022
|
-
Honey.definition.sweetie = [Sweetie];
|
|
1023
|
-
|
|
1024
|
-
const joe = Honey({ sweetie: undefined }); // ann is not yet defined
|
|
1025
|
-
const ann = Sweetie({ honey: joe });
|
|
1026
|
-
assert.ok(joe.sweetie = ann, "website example valid assignment");
|
|
1027
|
-
assert.throws(function () { joe.sweetie = "dog" }, /TypeError/, "website example invalid assignment 1");
|
|
1028
|
-
assert.throws(function () { joe.sweetie = joe }, /TypeError/, "website example invalid assignment 2");
|
|
1029
|
-
|
|
1030
|
-
});
|
|
1031
|
-
|
|
1032
|
-
QUnit.test("Custom error collectors", function (assert) {
|
|
1033
|
-
|
|
1034
|
-
assert.expect(11);
|
|
1035
|
-
|
|
1036
|
-
let M = ObjectModel({
|
|
1037
|
-
a: {
|
|
1038
|
-
b: {
|
|
1039
|
-
c: true
|
|
1040
|
-
}
|
|
1041
|
-
}
|
|
1042
|
-
});
|
|
1043
|
-
|
|
1044
|
-
M.errorCollector = function (errors) {
|
|
1045
|
-
assert.ok(errors.length === 1, "check errors.length model collector");
|
|
1046
|
-
var err = errors[0];
|
|
1047
|
-
assert.equal(err.expected, true, "check error.expected model collector");
|
|
1048
|
-
assert.equal(err.received, false, "check error.received model collector");
|
|
1049
|
-
assert.equal(err.path, "a.b.c", "check error.path model collector");
|
|
1050
|
-
assert.equal(err.message, 'expecting a.b.c to be true, got Boolean false', "check error message model collector");
|
|
1051
|
-
}
|
|
1052
|
-
|
|
1053
|
-
M({
|
|
1054
|
-
a: {
|
|
1055
|
-
b: {
|
|
1056
|
-
c: false
|
|
1057
|
-
}
|
|
1058
|
-
}
|
|
1059
|
-
})
|
|
1060
|
-
|
|
1061
|
-
ObjectModel({
|
|
1062
|
-
d: {
|
|
1063
|
-
e: {
|
|
1064
|
-
f: null
|
|
1065
|
-
}
|
|
1066
|
-
}
|
|
1067
|
-
}).test({
|
|
1068
|
-
d: {
|
|
1069
|
-
e: {
|
|
1070
|
-
f: undefined
|
|
1071
|
-
}
|
|
1072
|
-
}
|
|
1073
|
-
}, function (errors) {
|
|
1074
|
-
assert.ok(errors.length === 1, "check nested errors.length custom collector");
|
|
1075
|
-
var err = errors[0];
|
|
1076
|
-
assert.deepEqual(err.expected, null, "check nested error.expected custom collector");
|
|
1077
|
-
assert.deepEqual(err.received, undefined, "check nested error.received custom collector");
|
|
1078
|
-
assert.equal(err.path, "d.e.f", "check nested error.path custom collector");
|
|
1079
|
-
assert.equal(err.message, 'expecting d.e.f to be null, got undefined', "check nested error.message custom collector");
|
|
1080
|
-
})
|
|
1081
|
-
|
|
1082
|
-
M = ObjectModel({ x: Number });
|
|
1083
|
-
M.errorCollector = function noop() { };
|
|
1084
|
-
|
|
1085
|
-
assert.equal(M.test({ x: "nope" }), false, "model.test should work even when errorCollector does not throw exceptions");
|
|
1086
|
-
|
|
1087
|
-
});
|
|
1088
|
-
|
|
1089
|
-
QUnit.test("Automatic model casting", function (assert) {
|
|
1090
|
-
|
|
1091
|
-
let User = new ObjectModel({ username: String, email: String })
|
|
1092
|
-
.defaultTo({ username: 'foo', email: 'foo@foo' });
|
|
1093
|
-
|
|
1094
|
-
let Article = new ObjectModel({ title: String, user: User })
|
|
1095
|
-
.defaultTo({ title: 'bar', user: new User() });
|
|
1096
|
-
|
|
1097
|
-
let a = new Article();
|
|
1098
|
-
a.user = { username: 'joe', email: 'foo' };
|
|
1099
|
-
|
|
1100
|
-
assert.ok(a.user instanceof User, "automatic model casting when assigning an autocasted object");
|
|
1101
|
-
assert.ok(a.user.username === "joe", "preserved props after automatic model casting of autocasted object");
|
|
1102
|
-
|
|
1103
|
-
User = new ObjectModel({ username: String, email: String })
|
|
1104
|
-
.defaultTo({ username: 'foo', email: 'foo@foo' });
|
|
1105
|
-
|
|
1106
|
-
Article = new ObjectModel({ title: String, user: [User] })
|
|
1107
|
-
.defaultTo({ title: 'bar', user: new User() });
|
|
1108
|
-
|
|
1109
|
-
a = new Article();
|
|
1110
|
-
a.user = { username: 'joe', email: 'foo' };
|
|
1111
|
-
|
|
1112
|
-
assert.ok(a.user instanceof User, "automatic optional model casting when assigning an autocasted object");
|
|
1113
|
-
assert.ok(a.user.username === "joe", "preserved props after automatic optional model casting of autocasted object");
|
|
1114
|
-
|
|
1115
|
-
|
|
1116
|
-
const Type1 = ObjectModel({ name: String, other1: [Boolean] });
|
|
1117
|
-
const Type2 = ObjectModel({ name: String, other2: [Number] });
|
|
1118
|
-
const Container = ObjectModel({ foo: { bar: [Type1, Type2] } });
|
|
1119
|
-
|
|
1120
|
-
consoleMock.apply();
|
|
1121
|
-
let c = new Container({ foo: { bar: { name: "dunno" } } });
|
|
1122
|
-
|
|
1123
|
-
assert.ok(/Ambiguous model for[\s\S]*?name: "dunno"[\s\S]*?other1: \[Boolean][\s\S]*?other2: \[Number]/
|
|
1124
|
-
.test(consoleMock.lastArgs.warn[0]),
|
|
1125
|
-
"should warn about ambiguous model for object sub prop"
|
|
1126
|
-
);
|
|
1127
|
-
assert.ok(c.foo.bar.name === "dunno", "should preserve values even when ambiguous model cast");
|
|
1128
|
-
assert.ok(!(c.foo.bar instanceof Type1 || c.foo.bar instanceof Type2), "should not cast when ambiguous model");
|
|
1129
|
-
consoleMock.revert();
|
|
1130
|
-
|
|
1131
|
-
consoleMock.apply();
|
|
1132
|
-
c = new Container({ foo: { bar: Type2({ name: "dunno" }) } });
|
|
1133
|
-
assert.ok(consoleMock.lastArgs.warn.length === 0, "should not warn when explicit model cast in ambiguous context");
|
|
1134
|
-
assert.ok(c.foo.bar.name === "dunno", "should preserve values when explicit model cast in ambiguous context");
|
|
1135
|
-
assert.ok(c.foo.bar instanceof Type2, "should preserve model when explicit cast in ambiguous context");
|
|
1136
|
-
consoleMock.revert();
|
|
1137
|
-
|
|
1138
|
-
let noProto = Object.create(null);
|
|
1139
|
-
noProto.x = true;
|
|
1140
|
-
let noProtoModel = ObjectModel({ x: Boolean })
|
|
1141
|
-
let noProtoInstance = noProtoModel(noProto)
|
|
1142
|
-
assert.equal(noProtoInstance.x, true, "should be able to init with no-proto objects");
|
|
1143
|
-
noProtoInstance.y = Object.create(null);
|
|
1144
|
-
assert.ok(typeof noProtoInstance.y === "object", "should be able to mutate and cast with no-proto objects");
|
|
1145
|
-
|
|
1146
|
-
})
|
|
1147
|
-
|
|
1148
|
-
QUnit.test("delete trap", function (assert) {
|
|
1149
|
-
|
|
1150
|
-
const M = ObjectModel({ _p: Boolean, C: Number, u: undefined, n: null, x: [Boolean] })
|
|
1151
|
-
const m = M({ _p: true, C: 42, u: undefined, n: null, x: false })
|
|
1152
|
-
|
|
1153
|
-
assert.throws(function () { delete m._p }, /TypeError.*private/, "cannot delete private prop");
|
|
1154
|
-
assert.throws(function () { delete m.C }, /TypeError.*constant/, "cannot delete constant prop");
|
|
1155
|
-
delete m.u; // can delete undefined properties
|
|
1156
|
-
assert.throws(function () { delete m.n }, /TypeError.*expecting n to be null, got undefined/, "delete should differenciate null and undefined");
|
|
1157
|
-
delete m.x // can delete optional properties
|
|
1158
|
-
|
|
1159
|
-
})
|
|
1160
|
-
|
|
1161
|
-
QUnit.test("defineProperty trap", function (assert) {
|
|
1162
|
-
|
|
1163
|
-
const M = ObjectModel({ _p: Boolean, C: Number, u: undefined, n: null, x: [Boolean] })
|
|
1164
|
-
const m = M({ _p: true, C: 42, u: undefined, n: null, x: false })
|
|
1165
|
-
|
|
1166
|
-
assert.throws(function () { Object.defineProperty(m, "_p", { value: true }) }, /TypeError.*private/, "cannot define private prop");
|
|
1167
|
-
assert.throws(function () { Object.defineProperty(m, "C", { value: 43 }) }, /TypeError.*constant/, "cannot define constant prop");
|
|
1168
|
-
assert.throws(function () { Object.defineProperty(m, "u", { value: "test" }) }, /TypeError.*expecting u to be undefined/, "check type after defineProperty");
|
|
1169
|
-
assert.throws(function () { Object.defineProperty(m, "n", { value: undefined }) }, /TypeError.*expecting n to be null, got undefined/, "defineProperty should differenciate null and undefined");
|
|
1170
|
-
Object.defineProperty(m, "x", { value: undefined }) // can define optional properties
|
|
1171
|
-
|
|
1172
|
-
})
|
|
1173
|
-
|
|
1174
|
-
QUnit.test("ownKeys/has trap", function (assert) {
|
|
1175
|
-
|
|
1176
|
-
const A = ObjectModel({ _pa: Boolean, a: Boolean, oa: [Boolean] })
|
|
1177
|
-
const B = A.extend({ _pb: Boolean, b: Boolean, ob: [Boolean] })
|
|
1178
|
-
const m = B({ _pa: true, _pb: true, a: true, b: true, oa: undefined, undefined: true })
|
|
1179
|
-
B.prototype.B = true;
|
|
1180
|
-
B.prototype._PB = true;
|
|
1181
|
-
A.prototype.A = true;
|
|
1182
|
-
A.prototype._PA = true;
|
|
1183
|
-
|
|
1184
|
-
assert.equal("a" in m, true, "inherited prop in")
|
|
1185
|
-
assert.equal("b" in m, true, "own prop in")
|
|
1186
|
-
assert.equal("toString" in m, true, "Object.prototype prop in")
|
|
1187
|
-
|
|
1188
|
-
assert.equal("A" in m, false, "custom prop inherited prototype in")
|
|
1189
|
-
assert.equal("B" in m, false, "custom prop own prototype in")
|
|
1190
|
-
assert.equal("_pa" in m, false, "private inherited prop in")
|
|
1191
|
-
assert.equal("_pb" in m, false, "private own prop in")
|
|
1192
|
-
assert.equal("_PA" in m, false, "inherited prototype custom private prop in")
|
|
1193
|
-
assert.equal("_PB" in m, false, "own prototype custom private prop in")
|
|
1194
|
-
assert.equal("oa" in m, true, "optional assigned prop in")
|
|
1195
|
-
assert.equal("ob" in m, false, "optional unassigned prop in")
|
|
1196
|
-
assert.equal("unknown" in m, false, "unassigned undefined prop in")
|
|
1197
|
-
assert.equal("undefined" in m, false, "assigned undefined prop in")
|
|
1198
|
-
|
|
1199
|
-
const oKeys = Object.keys(m);
|
|
1200
|
-
|
|
1201
|
-
const ownKeys = Object.getOwnPropertyNames(m);
|
|
1202
|
-
|
|
1203
|
-
assert.equal(oKeys.sort().join(","), "a,b,oa", "Object.keys")
|
|
1204
|
-
assert.equal(ownKeys.sort().join(","), "a,b,oa", "Object.getOwnPropertyNames")
|
|
1205
|
-
})
|
|
1206
|
-
|
|
1207
|
-
QUnit.test("class constructors", function (assert) {
|
|
1208
|
-
|
|
1209
|
-
const PersonModel = ObjectModel({ firstName: String, lastName: String, fullName: String })
|
|
1210
|
-
|
|
1211
|
-
class Person extends PersonModel {
|
|
1212
|
-
constructor({ firstName, lastName }) {
|
|
1213
|
-
const fullName = `${firstName} ${lastName}`
|
|
1214
|
-
super({ firstName, lastName, fullName })
|
|
1215
|
-
}
|
|
1216
|
-
}
|
|
1217
|
-
|
|
1218
|
-
const person = new Person({ firstName: "John", lastName: "Smith" })
|
|
1219
|
-
assert.ok(person instanceof Person, "person instanceof Person")
|
|
1220
|
-
assert.ok(person instanceof PersonModel, "person instanceof PersonModel")
|
|
1221
|
-
assert.equal(person.fullName, "John Smith", "check es6 class constructor")
|
|
1222
|
-
|
|
1223
|
-
const UserModel = Person.extend({ role: String });
|
|
1224
|
-
class User extends UserModel {
|
|
1225
|
-
constructor({ firstName, lastName, role }) {
|
|
1226
|
-
super({ firstName, lastName, role })
|
|
1227
|
-
if (role === "admin") {
|
|
1228
|
-
this.fullName += " [ADMIN]"
|
|
1229
|
-
}
|
|
1230
|
-
}
|
|
1231
|
-
}
|
|
1232
|
-
|
|
1233
|
-
const user = new User({ firstName: "John", lastName: "Smith", role: "admin" })
|
|
1234
|
-
|
|
1235
|
-
assert.ok(user instanceof User, "user instanceof User")
|
|
1236
|
-
assert.ok(user instanceof UserModel, "user instanceof UserModel")
|
|
1237
|
-
assert.ok(user instanceof Person, "user instanceof Person")
|
|
1238
|
-
assert.ok(user instanceof PersonModel, "user instanceof PersonModel")
|
|
1239
|
-
assert.equal(user.fullName, "John Smith [ADMIN]", "check es6 class constructor with extended class")
|
|
1240
|
-
assert.equal(Object.keys(User.definition).sort().join(","), "firstName,fullName,lastName,role", "check definition keys")
|
|
1241
|
-
assert.equal(Object.keys(user).sort().join(","), "firstName,fullName,lastName,role", "check instance keys")
|
|
1242
|
-
assert.throws(function () { user.role = null; }, /TypeError/, "extended class model check definition")
|
|
1243
|
-
|
|
1244
|
-
const Lovers = class Lovers extends ObjectModel({
|
|
1245
|
-
husband: Person,
|
|
1246
|
-
wife: Person,
|
|
1247
|
-
}) { };
|
|
1248
|
-
|
|
1249
|
-
const joe = { firstName: 'Joe', lastName: "Smith" };
|
|
1250
|
-
const ann = new Person({ firstName: "Ann", lastName: "Smith" });
|
|
1251
|
-
|
|
1252
|
-
const couple = new Lovers({
|
|
1253
|
-
husband: joe, // object autocasted
|
|
1254
|
-
wife: ann, // object model
|
|
1255
|
-
});
|
|
1256
|
-
|
|
1257
|
-
assert.ok(couple.husband instanceof Person, "autocasting works with class-based models");
|
|
1258
|
-
|
|
1259
|
-
assert.equal(Person.test({ firstName: 0, lastName: "" }), false, `test method with class-based models`);
|
|
1260
|
-
|
|
1261
|
-
class Request extends ObjectModel({ id: [Number] }) {
|
|
1262
|
-
setId(id) {
|
|
1263
|
-
this.id = id;
|
|
1264
|
-
}
|
|
1265
|
-
}
|
|
1266
|
-
|
|
1267
|
-
let x = new Request({});
|
|
1268
|
-
assert.throws(function () { x.setId("32") }, /TypeError/, "class setters methods should provide type checking");
|
|
1269
|
-
|
|
1270
|
-
const BaseOM = ObjectModel({})
|
|
1271
|
-
let getterRequiringNoValidationCallCount = 0;
|
|
1272
|
-
class BaseClass extends BaseOM {
|
|
1273
|
-
get getterRequiringNoValidation() {
|
|
1274
|
-
getterRequiringNoValidationCallCount++;
|
|
1275
|
-
return true;
|
|
1276
|
-
}
|
|
1277
|
-
}
|
|
1278
|
-
|
|
1279
|
-
const SubOM = BaseClass.extend({ test: [Boolean] }).assert(o => o.test)
|
|
1280
|
-
class SubClass extends SubOM { }
|
|
1281
|
-
|
|
1282
|
-
SubClass.test({ test: false }, () => { });
|
|
1283
|
-
|
|
1284
|
-
assert.equal(SubClass.errors.length, 0, "class-based models errors are cleaned up properly 1/4")
|
|
1285
|
-
assert.equal(SubOM.errors.length, 0, "class-based models errors are cleaned up properly 2/4")
|
|
1286
|
-
assert.equal(BaseClass.errors.length, 0, "class-based models errors are cleaned up properly 3/4")
|
|
1287
|
-
assert.equal(BaseOM.errors.length, 0, "class-based models errors are cleaned up properly 4/4")
|
|
1288
|
-
|
|
1289
|
-
let bm = new BaseClass({});
|
|
1290
|
-
bm.getterRequired = bm.getterRequiringNoValidation;
|
|
1291
|
-
assert.equal(getterRequiringNoValidationCallCount, 1, "class getter requiring no validation only get once per call")
|
|
1292
|
-
})
|
|
1293
|
-
|
|
1294
|
-
QUnit.test("Null-safe object traversal", function (assert) {
|
|
1295
|
-
const Config = new ObjectModel({
|
|
1296
|
-
local: {
|
|
1297
|
-
time: {
|
|
1298
|
-
format: ["12h", "24h", undefined]
|
|
1299
|
-
}
|
|
1300
|
-
}
|
|
1301
|
-
});
|
|
1302
|
-
|
|
1303
|
-
const config = Config({ local: undefined }); // object autocasted
|
|
1304
|
-
|
|
1305
|
-
assert.strictEqual(config.local.time.format, undefined, "null-safe object traversal getter")
|
|
1306
|
-
config.local.time.format = "12h";
|
|
1307
|
-
assert.strictEqual(config.local.time.format, "12h", "null-safe object traversal setter")
|
|
1308
|
-
})
|
|
1309
|
-
|
|
1310
|
-
QUnit.test("Check once mode", function(assert){
|
|
1311
|
-
const Address = Model({ city: String, street: { name: String, number: Number }})
|
|
1312
|
-
assert.throws(function () {
|
|
1313
|
-
Address({ city: "Paris", street: { name: null, number: 12 }}, Model.CHECK_ONCE)
|
|
1314
|
-
}, /TypeError.*expecting street.name to be String/, "check once mode still checks at instanciation")
|
|
1315
|
-
const a = new Address({ city: "Paris", street: { name: "Champs-Elysees", number: 12 }}, Model.CHECK_ONCE)
|
|
1316
|
-
a.street.name = null;
|
|
1317
|
-
assert.strictEqual(a.street.name, null, "check once mode does not check future mutations")
|
|
1318
|
-
|
|
1319
|
-
class Person extends Model({ name: String, age: Number }) {
|
|
1320
|
-
constructor({ name, age }) {
|
|
1321
|
-
super({ name, age }, Model.CHECK_ONCE)
|
|
1322
|
-
}
|
|
1323
|
-
}
|
|
1324
|
-
|
|
1325
|
-
const john = new Person({ name: "John", age: 32 })
|
|
1326
|
-
john.age = "twelve";
|
|
1327
|
-
assert.strictEqual(john.age, "twelve", "check once mode does not check future mutations for extended class-based models")
|
|
1
|
+
/* global QUnit, Model, BasicModel, ArrayModel, ObjectModel */
|
|
2
|
+
QUnit.module("Object Models");
|
|
3
|
+
|
|
4
|
+
const consoleMock = (function (console) {
|
|
5
|
+
const methods = ["debug", "log", "warn", "error"];
|
|
6
|
+
const originals = {};
|
|
7
|
+
const mocks = {}
|
|
8
|
+
const lastArgs = {};
|
|
9
|
+
|
|
10
|
+
methods.forEach(method => {
|
|
11
|
+
originals[method] = console[method]
|
|
12
|
+
mocks[method] = function () { lastArgs[method] = arguments }
|
|
13
|
+
})
|
|
14
|
+
|
|
15
|
+
return {
|
|
16
|
+
apply: function () {
|
|
17
|
+
methods.forEach(method => {
|
|
18
|
+
lastArgs[method] = [];
|
|
19
|
+
console[method] = mocks[method]
|
|
20
|
+
})
|
|
21
|
+
},
|
|
22
|
+
revert: function () {
|
|
23
|
+
methods.forEach(method => {
|
|
24
|
+
lastArgs[method] = [];
|
|
25
|
+
console[method] = originals[method]
|
|
26
|
+
})
|
|
27
|
+
},
|
|
28
|
+
lastArgs
|
|
29
|
+
}
|
|
30
|
+
})(console);
|
|
31
|
+
|
|
32
|
+
QUnit.test("constructor && proto", async function (assert) {
|
|
33
|
+
assert.ok(ObjectModel instanceof Function, "ObjectModel instanceof Function");
|
|
34
|
+
|
|
35
|
+
const EmptyObjectModel = ObjectModel({});
|
|
36
|
+
|
|
37
|
+
assert.ok(typeof EmptyObjectModel.extend === "function", "test object model method extend");
|
|
38
|
+
assert.ok(typeof EmptyObjectModel.assert === "function", "test object model method assert");
|
|
39
|
+
assert.ok(typeof EmptyObjectModel.test === "function", "test object model method test");
|
|
40
|
+
assert.ok(typeof EmptyObjectModel.definition === "object", "test object model prop definition");
|
|
41
|
+
assert.ok(typeof EmptyObjectModel.assertions === "object", "test object model prop assertions");
|
|
42
|
+
|
|
43
|
+
const EmptyObjectModelThroughConstructor = new ObjectModel({});
|
|
44
|
+
|
|
45
|
+
assert.ok(typeof EmptyObjectModelThroughConstructor.extend === "function", "test new model method extend");
|
|
46
|
+
assert.ok(typeof EmptyObjectModelThroughConstructor.assert === "function", "test new model method assert");
|
|
47
|
+
assert.ok(typeof EmptyObjectModelThroughConstructor.test === "function", "test new model method test");
|
|
48
|
+
assert.ok(typeof EmptyObjectModelThroughConstructor.definition === "object", "test new model prop definition");
|
|
49
|
+
assert.ok(typeof EmptyObjectModelThroughConstructor.assertions === "object", "test new model prop assertions");
|
|
50
|
+
})
|
|
51
|
+
|
|
52
|
+
QUnit.test("behaviour for properties", function (assert) {
|
|
53
|
+
var Person = ObjectModel({
|
|
54
|
+
name: String,
|
|
55
|
+
age: Number,
|
|
56
|
+
birth: Date,
|
|
57
|
+
female: [Boolean],
|
|
58
|
+
address: {
|
|
59
|
+
work: {
|
|
60
|
+
city: [String]
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
});
|
|
64
|
+
|
|
65
|
+
var joe = Person({
|
|
66
|
+
name: "Joe",
|
|
67
|
+
age: 42,
|
|
68
|
+
birth: new Date(1990, 3, 25),
|
|
69
|
+
female: false,
|
|
70
|
+
address: {
|
|
71
|
+
work: {
|
|
72
|
+
city: "Lille"
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
});
|
|
76
|
+
|
|
77
|
+
assert.strictEqual(joe.name, "Joe", "String property retrieved");
|
|
78
|
+
assert.strictEqual(joe.age, 42, "Number property retrieved");
|
|
79
|
+
assert.strictEqual(joe.female, false, "Boolean property retrieved");
|
|
80
|
+
assert.equal(+joe.birth, +(new Date(1990, 3, 25)), "Date property retrieved");
|
|
81
|
+
assert.strictEqual(joe.address.work.city, "Lille", "nested property retrieved");
|
|
82
|
+
assert.ok(joe instanceof Person && joe instanceof Object, "instance is instanceof model and Object");
|
|
83
|
+
assert.ok(Person instanceof ObjectModel, "model is instanceof ObjectModel");
|
|
84
|
+
|
|
85
|
+
joe.name = "Big Joe";
|
|
86
|
+
joe.age++;
|
|
87
|
+
joe.birth = new Date(1990, 3, 26);
|
|
88
|
+
delete joe.female;
|
|
89
|
+
|
|
90
|
+
assert.throws(function () {
|
|
91
|
+
joe.name = 42;
|
|
92
|
+
}, /TypeError.*got Number 42/, "invalid Number set");
|
|
93
|
+
assert.throws(function () {
|
|
94
|
+
joe.age = true;
|
|
95
|
+
}, /TypeError.*got Boolean true/, "invalid Boolean set");
|
|
96
|
+
assert.throws(function () {
|
|
97
|
+
joe.birth = function () {
|
|
98
|
+
};
|
|
99
|
+
}, /TypeError.*got Function/, "invalid Function set");
|
|
100
|
+
assert.throws(function () {
|
|
101
|
+
joe.female = "nope";
|
|
102
|
+
}, /TypeError.*got String "nope"/, "invalid String set");
|
|
103
|
+
assert.throws(function () {
|
|
104
|
+
joe.address.work.city = [];
|
|
105
|
+
}, /TypeError.*got Array/, "invalid Array set");
|
|
106
|
+
assert.throws(function () {
|
|
107
|
+
joe.address.work = { city: 42 };
|
|
108
|
+
}, /TypeError.*got Number 42/, "invalid Object set");
|
|
109
|
+
assert.throws(function () {
|
|
110
|
+
Person({
|
|
111
|
+
name: "Joe",
|
|
112
|
+
age: 42,
|
|
113
|
+
birth: new Date(1990, 3, 25),
|
|
114
|
+
female: "false"
|
|
115
|
+
});
|
|
116
|
+
}, /TypeError.*expecting female to be Boolean.*got String "false"/,
|
|
117
|
+
"invalid prop at object model instanciation");
|
|
118
|
+
|
|
119
|
+
joe = Person({
|
|
120
|
+
name: "Joe",
|
|
121
|
+
age: 42,
|
|
122
|
+
birth: new Date(1990, 3, 25),
|
|
123
|
+
female: false,
|
|
124
|
+
address: {
|
|
125
|
+
work: {
|
|
126
|
+
city: "Lille"
|
|
127
|
+
}
|
|
128
|
+
},
|
|
129
|
+
job: "Taxi"
|
|
130
|
+
});
|
|
131
|
+
|
|
132
|
+
assert.strictEqual(joe.job, "Taxi", "Properties out of model definition are kept but are not validated");
|
|
133
|
+
|
|
134
|
+
assert.throws(function () {
|
|
135
|
+
Person({
|
|
136
|
+
name: false,
|
|
137
|
+
age: [42],
|
|
138
|
+
birth: "nope",
|
|
139
|
+
female: null,
|
|
140
|
+
address: {
|
|
141
|
+
work: {
|
|
142
|
+
city: true
|
|
143
|
+
}
|
|
144
|
+
}
|
|
145
|
+
});
|
|
146
|
+
}, function (err) {
|
|
147
|
+
return /TypeError/.test(err.toString())
|
|
148
|
+
&& /name/.test(err.toString())
|
|
149
|
+
&& /age/.test(err.toString())
|
|
150
|
+
&& /birth/.test(err.toString())
|
|
151
|
+
&& /city/.test(err.toString())
|
|
152
|
+
}, "check that errors are correctly stacked");
|
|
153
|
+
|
|
154
|
+
const A = ObjectModel({
|
|
155
|
+
id: [Number]
|
|
156
|
+
}).defaultTo({
|
|
157
|
+
setId(id) { this.id = id; }
|
|
158
|
+
})
|
|
159
|
+
|
|
160
|
+
let a = new A({})
|
|
161
|
+
assert.throws(() => { a.setId("32") }, /TypeError/, "methods should trigger type-checking");
|
|
162
|
+
|
|
163
|
+
});
|
|
164
|
+
|
|
165
|
+
QUnit.test("edge cases of constructors", function (assert) {
|
|
166
|
+
assert.ok(ObjectModel({}) instanceof ObjectModel, "ObjectModel can receive empty object as argument");
|
|
167
|
+
|
|
168
|
+
const M = ObjectModel({})
|
|
169
|
+
assert.strictEqual(M.test(undefined), true, "undefined is valid for empty objectmodels, due to null-safe object traversal")
|
|
170
|
+
assert.strictEqual(M.test(null), false, "null is invalid for empty objectmodels")
|
|
171
|
+
assert.strictEqual(M.test(1), false, "number is invalid for empty objectmodels")
|
|
172
|
+
assert.strictEqual(M.test(new Number(1)), true, "Numbers through constructor are valid for empty objectmodels")
|
|
173
|
+
assert.strictEqual(M.test("string"), false, "string is invalid for empty objectmodels")
|
|
174
|
+
assert.strictEqual(M.test(function () { }), true, "function is valid for empty objectmodels")
|
|
175
|
+
|
|
176
|
+
const O = ObjectModel({ x: [Number] })
|
|
177
|
+
assert.strictEqual(O.test(undefined), true, "undefined is valid for optional objectmodels, due to null-safe object traversal")
|
|
178
|
+
assert.strictEqual(O.test(null), false, "null is invalid for optional objectmodels")
|
|
179
|
+
assert.strictEqual(O.test(1), false, "number is invalid for optional objectmodels")
|
|
180
|
+
assert.strictEqual(O.test(new Number(1)), true, "Numbers through constructor are valid for optional objectmodels")
|
|
181
|
+
assert.strictEqual(O.test("string"), false, "string is invalid for optional objectmodels")
|
|
182
|
+
assert.strictEqual(O.test(function () { }), true, "function is valid for optional objectmodels")
|
|
183
|
+
|
|
184
|
+
|
|
185
|
+
/* //TODO: use FunctionModel for ObjectModel API ?
|
|
186
|
+
assert.throws(function () {
|
|
187
|
+
ObjectModel(undefined)
|
|
188
|
+
}, /expecting arguments\[0] to be Object, got undefined/,
|
|
189
|
+
"ObjectModel with definition undefined throws")
|
|
190
|
+
|
|
191
|
+
assert.throws(function () {
|
|
192
|
+
ObjectModel(42)
|
|
193
|
+
}, /expecting arguments\[0] to be Object, got Number 42/,
|
|
194
|
+
"ObjectModel with definition primitive throws")
|
|
195
|
+
*/
|
|
196
|
+
});
|
|
197
|
+
|
|
198
|
+
QUnit.test("optional and multiple parameters", function (assert) {
|
|
199
|
+
var Person = ObjectModel({
|
|
200
|
+
name: [String],
|
|
201
|
+
age: [Number, Date, String, Boolean, undefined],
|
|
202
|
+
female: [Boolean, Number, String, null],
|
|
203
|
+
haircolor: ["blond", "brown", "black", undefined],
|
|
204
|
+
address: {
|
|
205
|
+
work: {
|
|
206
|
+
city: [String]
|
|
207
|
+
}
|
|
208
|
+
}
|
|
209
|
+
});
|
|
210
|
+
|
|
211
|
+
var joe = Person({ female: false });
|
|
212
|
+
assert.ok(joe instanceof Person, "instanceof model test");
|
|
213
|
+
joe.name = "joe";
|
|
214
|
+
joe.name = undefined;
|
|
215
|
+
joe.name = null;
|
|
216
|
+
joe.age = new Date(1995, 1, 23);
|
|
217
|
+
joe.age = undefined;
|
|
218
|
+
assert.throws(function () {
|
|
219
|
+
joe.age = null;
|
|
220
|
+
}, /TypeError.*got null/, "invalid set null");
|
|
221
|
+
joe.female = "ann";
|
|
222
|
+
joe.female = 2;
|
|
223
|
+
joe.female = false;
|
|
224
|
+
assert.throws(function () {
|
|
225
|
+
joe.female = undefined;
|
|
226
|
+
}, /TypeError.*got undefined/, "invalid set undefined");
|
|
227
|
+
joe.address.work.city = "Lille";
|
|
228
|
+
joe.address.work.city = undefined;
|
|
229
|
+
joe.haircolor = "blond";
|
|
230
|
+
joe.haircolor = undefined;
|
|
231
|
+
assert.throws(function () {
|
|
232
|
+
joe.name = false;
|
|
233
|
+
}, /TypeError.*expecting name to be String.*got Boolean false/, "invalid type for optional prop");
|
|
234
|
+
assert.throws(function () {
|
|
235
|
+
joe.age = null;
|
|
236
|
+
}, /TypeError.*expecting age to be Number or Date or String or Boolean or undefined/, "invalid set null for optional union type prop");
|
|
237
|
+
assert.throws(function () {
|
|
238
|
+
joe.age = [];
|
|
239
|
+
}, /TypeError.*got Array/, "invalid set array for optional union type prop");
|
|
240
|
+
assert.throws(function () {
|
|
241
|
+
joe.address.work.city = 0;
|
|
242
|
+
}, /TypeError.*expecting address.work.city to be String.*got Number 0/, "invalid type for nested optional prop");
|
|
243
|
+
assert.throws(function () {
|
|
244
|
+
joe.haircolor = "";
|
|
245
|
+
}, /TypeError.*expecting haircolor to be "blond" or "brown" or "black" or undefined, got String ""/, "invalid type for value enum prop");
|
|
246
|
+
|
|
247
|
+
});
|
|
248
|
+
|
|
249
|
+
QUnit.test("fixed values", function (assert) {
|
|
250
|
+
var myModel = ObjectModel({
|
|
251
|
+
a: [1, 2, 3],
|
|
252
|
+
b: 42,
|
|
253
|
+
c: ["", false, null, 0],
|
|
254
|
+
haircolor: ["blond", "brown", "black"],
|
|
255
|
+
foo: "bar",
|
|
256
|
+
x: [Number, true]
|
|
257
|
+
});
|
|
258
|
+
|
|
259
|
+
var model = myModel({
|
|
260
|
+
a: 1,
|
|
261
|
+
b: 42,
|
|
262
|
+
c: 0,
|
|
263
|
+
haircolor: "blond",
|
|
264
|
+
foo: "bar",
|
|
265
|
+
x: true
|
|
266
|
+
});
|
|
267
|
+
|
|
268
|
+
model.x = 666;
|
|
269
|
+
model.haircolor = "brown";
|
|
270
|
+
|
|
271
|
+
assert.throws(function () {
|
|
272
|
+
model.a = 4;
|
|
273
|
+
}, /TypeError.*expecting a to be 1 or 2 or 3.*got Number 4/, 'invalid set on values enum 1/2');
|
|
274
|
+
assert.throws(function () {
|
|
275
|
+
model.b = 43;
|
|
276
|
+
}, /TypeError.*expecting b to be 42.*got Number 43/, "invalid set on fixed value 1/2");
|
|
277
|
+
assert.throws(function () {
|
|
278
|
+
model.c = undefined;
|
|
279
|
+
}, /TypeError.*expecting c to be "" or false or null or 0.*got undefined/, "invalid set undefined on mixed typed values enum");
|
|
280
|
+
assert.throws(function () {
|
|
281
|
+
model.haircolor = "roux";
|
|
282
|
+
}, /TypeError.*expecting haircolor to be "blond" or "brown" or "black".*got String "roux"/, 'invalid set on values enum 2/2');
|
|
283
|
+
assert.throws(function () {
|
|
284
|
+
model.foo = "baz";
|
|
285
|
+
}, /TypeError.*expecting foo to be "bar".*got String "baz"/, "invalid set on fixed value 2/2");
|
|
286
|
+
assert.throws(function () {
|
|
287
|
+
model.x = false;
|
|
288
|
+
}, /TypeError.*expecting x to be Number or true.*got Boolean false/, "invalid set on mixed type/values enum");
|
|
289
|
+
|
|
290
|
+
});
|
|
291
|
+
|
|
292
|
+
QUnit.test("default values", function (assert) {
|
|
293
|
+
|
|
294
|
+
let myModel = new ObjectModel({
|
|
295
|
+
name: String,
|
|
296
|
+
foo: {
|
|
297
|
+
bar: {
|
|
298
|
+
buz: Number
|
|
299
|
+
}
|
|
300
|
+
}
|
|
301
|
+
}).defaultTo({
|
|
302
|
+
name: "joe",
|
|
303
|
+
foo: {
|
|
304
|
+
bar: {
|
|
305
|
+
buz: 0
|
|
306
|
+
}
|
|
307
|
+
}
|
|
308
|
+
});
|
|
309
|
+
|
|
310
|
+
const model = myModel();
|
|
311
|
+
assert.strictEqual(model.name, "joe", "default values correctly applied");
|
|
312
|
+
assert.strictEqual(model.foo.bar.buz, 0, "default nested props values correctly applied");
|
|
313
|
+
assert.ok(myModel.test({}), "default should be applied when testing autocasted objects")
|
|
314
|
+
|
|
315
|
+
const model2 = myModel({ name: "jim", foo: { bar: { buz: 1 } } });
|
|
316
|
+
assert.strictEqual(model2.name, "jim", "default values not applied if provided");
|
|
317
|
+
assert.strictEqual(model2.foo.bar.buz, 1, "default nested props values not applied if provided");
|
|
318
|
+
|
|
319
|
+
const Person = Model({
|
|
320
|
+
name: String,
|
|
321
|
+
age: [Number]
|
|
322
|
+
}).defaultTo({
|
|
323
|
+
name: "new-name"
|
|
324
|
+
});
|
|
325
|
+
|
|
326
|
+
const Team = Model({
|
|
327
|
+
lead: Person,
|
|
328
|
+
members: ArrayModel(Person)
|
|
329
|
+
});
|
|
330
|
+
|
|
331
|
+
assert.strictEqual((new Team({ lead: new Person(), members: [] })).lead.name, "new-name", "default value through composition")
|
|
332
|
+
assert.throws(() => { new Team({ lead: 1, members: [] }) }, "invalid value through composition with default")
|
|
333
|
+
|
|
334
|
+
assert.throws(() => { myModel.defaultTo({ name: undefined }) }, /TypeError.*expecting name to be String, got undefined/, "check definition of provided defaults")
|
|
335
|
+
assert.throws(() => { myModel.defaultTo({ foo: { bar: { buz: "nope" } } }) }, /TypeError.*expecting foo.bar.buz to be Number, got String/, "check nested definition of provided defaults")
|
|
336
|
+
|
|
337
|
+
myModel = new ObjectModel({ x: Number, y: String })
|
|
338
|
+
.defaultTo({ x: 42, y: "hello" })
|
|
339
|
+
|
|
340
|
+
assert.strictEqual(myModel.default.x, 42, "object model defaults store the value as default property")
|
|
341
|
+
assert.strictEqual(myModel().x, 42, "object model default property is applied when undefined is passed");
|
|
342
|
+
assert.strictEqual(myModel().y, "hello", "defaulted object model still inherit from model proto");
|
|
343
|
+
|
|
344
|
+
myModel.default.x = "nope";
|
|
345
|
+
|
|
346
|
+
assert.throws(function () {
|
|
347
|
+
myModel()
|
|
348
|
+
}, /TypeError.*got String "nope"/, "invalid default property still throws TypeError for object models");
|
|
349
|
+
|
|
350
|
+
});
|
|
351
|
+
|
|
352
|
+
QUnit.test("RegExp values", function (assert) {
|
|
353
|
+
|
|
354
|
+
const myModel = ObjectModel({
|
|
355
|
+
phonenumber: /^[0-9]{10}$/,
|
|
356
|
+
voyels: [/^[aeiouy]+$/]
|
|
357
|
+
});
|
|
358
|
+
|
|
359
|
+
const m = myModel({
|
|
360
|
+
phonenumber: "0612345678"
|
|
361
|
+
});
|
|
362
|
+
|
|
363
|
+
m.voyels = "ouioui";
|
|
364
|
+
|
|
365
|
+
assert.throws(function () {
|
|
366
|
+
m.voyels = "nonnon"
|
|
367
|
+
}, /TypeError/, "regex matching");
|
|
368
|
+
assert.throws(function () {
|
|
369
|
+
m.phonenumber = "123456789"
|
|
370
|
+
}, /TypeError/, "regex matching 2");
|
|
371
|
+
|
|
372
|
+
});
|
|
373
|
+
|
|
374
|
+
QUnit.test("Private and constant properties", function (assert) {
|
|
375
|
+
|
|
376
|
+
let myModel = ObjectModel({
|
|
377
|
+
CONST: Number,
|
|
378
|
+
_private: Number,
|
|
379
|
+
normal: Number
|
|
380
|
+
});
|
|
381
|
+
|
|
382
|
+
let m = myModel({
|
|
383
|
+
CONST: 42,
|
|
384
|
+
_private: 43,
|
|
385
|
+
normal: 44
|
|
386
|
+
});
|
|
387
|
+
|
|
388
|
+
m.normal++;
|
|
389
|
+
|
|
390
|
+
assert.throws(function () {
|
|
391
|
+
m._private++;
|
|
392
|
+
}, /TypeError[\s\S]*private/, "try to modify private");
|
|
393
|
+
|
|
394
|
+
assert.throws(function () {
|
|
395
|
+
m.CONST++;
|
|
396
|
+
}, /TypeError[\s\S]*constant/, "try to modify constant");
|
|
397
|
+
assert.equal(Object.keys(m).length, 2, "non enumerable key not counted by Object.keys");
|
|
398
|
+
assert.equal(Object.keys(m).includes("_private"), false, "non enumerable key not found in Object.keys");
|
|
399
|
+
assert.equal(Object.getOwnPropertyNames(m).length, 2, "non enumerable key not counted by Object.getOwnPropertyNames");
|
|
400
|
+
assert.equal(Object.getOwnPropertyNames(m).includes("_private"), false, "non enumerable key not found in Object.getOwnPropertyNames");
|
|
401
|
+
assert.equal("normal" in m, true, "enumerable key found with operator in")
|
|
402
|
+
assert.equal("_private" in m, false, "non enumerable key not found with operator in")
|
|
403
|
+
assert.equal(Object.getOwnPropertyDescriptor(m, "normal").value, 45, "getOwnProperyDescriptor trap for normal prop")
|
|
404
|
+
assert.equal(Object.getOwnPropertyDescriptor(m, "_private"), undefined, "getOwnProperyDescriptor for private prop")
|
|
405
|
+
|
|
406
|
+
let M = ObjectModel({ _p: Number })
|
|
407
|
+
m = M({ _p: 42 })
|
|
408
|
+
|
|
409
|
+
assert.throws(function () {
|
|
410
|
+
Object.prototype.toString.call(m._p);
|
|
411
|
+
}, /TypeError[\s\S]*cannot access to private/, "try to access private from outside");
|
|
412
|
+
|
|
413
|
+
M.prototype.incrementPrivate = function () { this._p++ }
|
|
414
|
+
M.prototype.getPrivate = function () { return this._p }
|
|
415
|
+
m.incrementPrivate();
|
|
416
|
+
assert.equal(m.getPrivate(), 43, "can access and mutate private props through methods")
|
|
417
|
+
|
|
418
|
+
const A = ObjectModel({
|
|
419
|
+
_id: [Number]
|
|
420
|
+
}).defaultTo({
|
|
421
|
+
getId() { return this._id },
|
|
422
|
+
setId(id) { this._id = id; }
|
|
423
|
+
})
|
|
424
|
+
|
|
425
|
+
let a = new A({})
|
|
426
|
+
a.setId(32);
|
|
427
|
+
assert.equal(a.getId(), 32, "methods should be able to access and modify private vars");
|
|
428
|
+
|
|
429
|
+
const B = ObjectModel({
|
|
430
|
+
ID: [Number]
|
|
431
|
+
}).defaultTo({
|
|
432
|
+
setId(id) { this.ID = id; }
|
|
433
|
+
})
|
|
434
|
+
|
|
435
|
+
let b = new B({ ID: 0 })
|
|
436
|
+
assert.throws(() => { b.setId(32) }, /TypeError: cannot modify constant property ID/, "methods should not be able to modify constants");
|
|
437
|
+
|
|
438
|
+
const Circle = ObjectModel({
|
|
439
|
+
radius: Number, // public
|
|
440
|
+
_index: Number, // private
|
|
441
|
+
UNIT: ["px", "cm"], // constant
|
|
442
|
+
_ID: [Number], // private and constant
|
|
443
|
+
}).defaultTo({
|
|
444
|
+
_index: 0,
|
|
445
|
+
getIndex() { return this._index },
|
|
446
|
+
setIndex(value) { this._index = value }
|
|
447
|
+
});
|
|
448
|
+
|
|
449
|
+
let c = new Circle({ radius: 120, UNIT: "px", _ID: 1 });
|
|
450
|
+
c.radius = 100;
|
|
451
|
+
|
|
452
|
+
assert.throws(() => { c.UNIT = "cm" }, /TypeError: cannot modify constant property UNIT/, "cannot redefine constant");
|
|
453
|
+
assert.throws(() => { c._index = 1; }, /TypeError: cannot modify private property _index/, "cannot modify private property")
|
|
454
|
+
assert.throws(() => { c._index }, /TypeError: cannot access to private property _index/, "cannot access private property")
|
|
455
|
+
|
|
456
|
+
c.setIndex(2);
|
|
457
|
+
assert.strictEqual(c.getIndex(), 2, "can access and mutate private through method");
|
|
458
|
+
|
|
459
|
+
// change the private convention for all models
|
|
460
|
+
let initialConventionForPrivate = Model.prototype.conventionForPrivate;
|
|
461
|
+
Model.prototype.conventionForPrivate = key => key === "radius";
|
|
462
|
+
|
|
463
|
+
// remove the constant convention for Circle model
|
|
464
|
+
Circle.conventionForConstant = () => false;
|
|
465
|
+
|
|
466
|
+
c.UNIT = "cm";
|
|
467
|
+
assert.strictEqual(c.UNIT, "cm", "constant convention can be changed specifically for model")
|
|
468
|
+
|
|
469
|
+
assert.throws(() => { c.radius }, /TypeError[\s\S]*cannot access to private property/, "private convention can be changed")
|
|
470
|
+
c._index = 3;
|
|
471
|
+
assert.strictEqual(c._index, 3, "private convention can be changed and privates can be accessed and mutated")
|
|
472
|
+
|
|
473
|
+
Model.prototype.conventionForPrivate = initialConventionForPrivate;
|
|
474
|
+
|
|
475
|
+
class OM extends ObjectModel({
|
|
476
|
+
_privString: [String],
|
|
477
|
+
pubNum: [Number]
|
|
478
|
+
}) { }
|
|
479
|
+
|
|
480
|
+
class ParentOM extends ObjectModel({
|
|
481
|
+
_id: [String],
|
|
482
|
+
om: OM
|
|
483
|
+
}) { }
|
|
484
|
+
|
|
485
|
+
|
|
486
|
+
let nestedOM = new OM({
|
|
487
|
+
_privString: "only for me",
|
|
488
|
+
pubNum: 42
|
|
489
|
+
});
|
|
490
|
+
|
|
491
|
+
let parent = new ParentOM({ om: nestedOM });
|
|
492
|
+
assert.ok(parent instanceof ParentOM, "can nest private prop in a child OM");
|
|
493
|
+
assert.throws(() => parent.om._privString, /TypeError[\s\S]*cannot access to private property/, "cannot access nested private prop in a child OM");
|
|
494
|
+
|
|
495
|
+
const O = ObjectModel({ _priv: String }).defaultTo({
|
|
496
|
+
getPriv() {
|
|
497
|
+
this.randomMethod();
|
|
498
|
+
return this._priv
|
|
499
|
+
},
|
|
500
|
+
randomMethod() { }
|
|
501
|
+
})
|
|
502
|
+
|
|
503
|
+
const o = new O({ _priv: "test" })
|
|
504
|
+
assert.strictEqual(o.getPriv(), "test", "can grant private access even if several methods are called");
|
|
505
|
+
});
|
|
506
|
+
|
|
507
|
+
QUnit.test("Non-enumerable and non-writable properties with overridden convention", function (assert) {
|
|
508
|
+
|
|
509
|
+
const myModel = ObjectModel({
|
|
510
|
+
private_prop: Number,
|
|
511
|
+
constant_prop: Number,
|
|
512
|
+
normal_prop: Number
|
|
513
|
+
});
|
|
514
|
+
|
|
515
|
+
myModel.conventionForConstant = s => s.indexOf("constant_") === 0;
|
|
516
|
+
myModel.conventionForPrivate = s => s.indexOf("private_") === 0;
|
|
517
|
+
|
|
518
|
+
const m = myModel({
|
|
519
|
+
private_prop: 42,
|
|
520
|
+
constant_prop: 43,
|
|
521
|
+
normal_prop: 44
|
|
522
|
+
});
|
|
523
|
+
|
|
524
|
+
assert.throws(function () {
|
|
525
|
+
m.constant_prop++;
|
|
526
|
+
}, /TypeError[\s\S]*constant/, "try to redefine constant with overridden convention");
|
|
527
|
+
assert.equal(Object.keys(m).length, 2, "non enumerable key not counted by Object.keys with overridden convention");
|
|
528
|
+
assert.equal(Object.keys(m).includes("private_prop"), false, "non enumerable key not found in Object.keys with overridden convention");
|
|
529
|
+
assert.equal(Object.getOwnPropertyNames(m).length, 2, "non enumerable key not counted by Object.getOwnPropertyNames with overridden convention");
|
|
530
|
+
assert.equal(Object.getOwnPropertyNames(m).includes("private_prop"), false, "non enumerable key not found in Object.getOwnPropertyNames with overridden convention");
|
|
531
|
+
assert.equal("normal_prop" in m, true, "enumerable key found with operator in with overridden convention")
|
|
532
|
+
assert.equal("private_prop" in m, false, "non enumerable key not found with operator in with overridden convention")
|
|
533
|
+
|
|
534
|
+
});
|
|
535
|
+
|
|
536
|
+
QUnit.test("Extensions", function (assert) {
|
|
537
|
+
|
|
538
|
+
const Person = ObjectModel({
|
|
539
|
+
name: String,
|
|
540
|
+
age: Number,
|
|
541
|
+
birth: Date,
|
|
542
|
+
female: [Boolean],
|
|
543
|
+
address: {
|
|
544
|
+
work: {
|
|
545
|
+
city: [String]
|
|
546
|
+
}
|
|
547
|
+
}
|
|
548
|
+
});
|
|
549
|
+
|
|
550
|
+
const joe = Person({
|
|
551
|
+
name: "Joe",
|
|
552
|
+
age: 42,
|
|
553
|
+
birth: new Date(1990, 3, 25),
|
|
554
|
+
female: false,
|
|
555
|
+
address: {
|
|
556
|
+
work: {
|
|
557
|
+
city: "Lille"
|
|
558
|
+
}
|
|
559
|
+
}
|
|
560
|
+
});
|
|
561
|
+
|
|
562
|
+
const Woman = Person.extend({ female: true });
|
|
563
|
+
|
|
564
|
+
assert.ok(Person(joe), "Person valid model for joe");
|
|
565
|
+
|
|
566
|
+
assert.throws(function () {
|
|
567
|
+
Woman(joe);
|
|
568
|
+
}, /TypeError[\s\S]*female/, "Woman invalid model for joe");
|
|
569
|
+
|
|
570
|
+
assert.throws(function () {
|
|
571
|
+
Woman({
|
|
572
|
+
name: "Joe",
|
|
573
|
+
age: 42,
|
|
574
|
+
birth: new Date(1990, 3, 25),
|
|
575
|
+
female: false,
|
|
576
|
+
address: {
|
|
577
|
+
work: {
|
|
578
|
+
city: "Lille"
|
|
579
|
+
}
|
|
580
|
+
}
|
|
581
|
+
});
|
|
582
|
+
}, /TypeError[\s\S]*female/, "cant be woman from joe parameters");
|
|
583
|
+
|
|
584
|
+
assert.throws(function () {
|
|
585
|
+
Woman(joe);
|
|
586
|
+
}, /TypeError[\s\S]*female/, "cant be woman from Person joe");
|
|
587
|
+
|
|
588
|
+
const ann = Woman({
|
|
589
|
+
name: "Joe's wife",
|
|
590
|
+
age: 42,
|
|
591
|
+
birth: new Date(1990, 3, 25),
|
|
592
|
+
female: true,
|
|
593
|
+
address: {
|
|
594
|
+
work: {
|
|
595
|
+
city: "Lille"
|
|
596
|
+
}
|
|
597
|
+
}
|
|
598
|
+
});
|
|
599
|
+
|
|
600
|
+
const UnemployedWoman = Woman.extend({
|
|
601
|
+
address: {
|
|
602
|
+
work: {
|
|
603
|
+
city: undefined
|
|
604
|
+
}
|
|
605
|
+
}
|
|
606
|
+
});
|
|
607
|
+
|
|
608
|
+
assert.ok(Woman(ann), "Woman valid model for ann");
|
|
609
|
+
|
|
610
|
+
assert.ok(Woman.prototype.constructor === Woman, "extended model has a new constructor");
|
|
611
|
+
assert.ok(ann.constructor === Woman, "extended model instance has the right constructor");
|
|
612
|
+
|
|
613
|
+
assert.throws(function () {
|
|
614
|
+
UnemployedWoman(ann);
|
|
615
|
+
}, /TypeError[\s\S]*city/, "ann cant be UnemployedWoman; model extension nested undefined property");
|
|
616
|
+
|
|
617
|
+
|
|
618
|
+
const jane = UnemployedWoman({
|
|
619
|
+
name: "Jane",
|
|
620
|
+
age: 52,
|
|
621
|
+
birth: new Date(1990, 3, 25),
|
|
622
|
+
female: true
|
|
623
|
+
});
|
|
624
|
+
|
|
625
|
+
assert.ok(ann instanceof Person, "ann instanceof Person");
|
|
626
|
+
assert.ok(ann instanceof Woman, "ann instanceof Woman");
|
|
627
|
+
assert.ok(jane instanceof Person, "jane instanceof Person");
|
|
628
|
+
assert.ok(jane instanceof Woman, "jane instanceof Woman");
|
|
629
|
+
assert.ok(jane instanceof UnemployedWoman, "jane instanceof UnemployedWoman");
|
|
630
|
+
assert.equal(joe instanceof Woman, false, "joe not instanceof Woman");
|
|
631
|
+
assert.equal(joe instanceof UnemployedWoman, false, "joe not instanceof UnemployedWoman");
|
|
632
|
+
assert.equal(ann instanceof UnemployedWoman, false, "ann not instanceof UnemployedWoman");
|
|
633
|
+
|
|
634
|
+
let Vehicle = { speed: Number };
|
|
635
|
+
let Car = Object.create(Vehicle);
|
|
636
|
+
let Ferrari = ObjectModel({ expensive: true }).extend(Car);
|
|
637
|
+
assert.ok("speed" in Ferrari.definition, "should retrieve definitions from parent prototypes when extending with objects");
|
|
638
|
+
|
|
639
|
+
Vehicle = function () { };
|
|
640
|
+
Vehicle.prototype.speed = 99;
|
|
641
|
+
Car = function () { };
|
|
642
|
+
Car.prototype = new Vehicle();
|
|
643
|
+
Ferrari = ObjectModel({ price: [Number] }).extend(Car);
|
|
644
|
+
|
|
645
|
+
let ferrari = new Ferrari({ price: 999999 });
|
|
646
|
+
assert.equal(ferrari.speed, 99, "should retrieve properties from parent prototypes when extending with constructors");
|
|
647
|
+
assert.equal("price" in ferrari, true, "should trap in operator and return true for properties in definition");
|
|
648
|
+
assert.equal("speed" in ferrari, false, "should trap in operator and return false for properties out of definition");
|
|
649
|
+
|
|
650
|
+
});
|
|
651
|
+
|
|
652
|
+
QUnit.test("Multiple inheritance", function (assert) {
|
|
653
|
+
|
|
654
|
+
const A = new ObjectModel({
|
|
655
|
+
a: Boolean,
|
|
656
|
+
b: Boolean
|
|
657
|
+
});
|
|
658
|
+
|
|
659
|
+
const B = ObjectModel({
|
|
660
|
+
b: Number,
|
|
661
|
+
c: Number
|
|
662
|
+
});
|
|
663
|
+
|
|
664
|
+
const C = ObjectModel({
|
|
665
|
+
c: String,
|
|
666
|
+
d: {
|
|
667
|
+
d1: Boolean,
|
|
668
|
+
d2: Boolean
|
|
669
|
+
}
|
|
670
|
+
});
|
|
671
|
+
|
|
672
|
+
const D = ObjectModel({
|
|
673
|
+
a: String,
|
|
674
|
+
d: {
|
|
675
|
+
d2: Number,
|
|
676
|
+
d3: Number
|
|
677
|
+
}
|
|
678
|
+
});
|
|
679
|
+
|
|
680
|
+
let M1 = A.extend(B, C, D);
|
|
681
|
+
let M2 = D.extend(C, B, A);
|
|
682
|
+
|
|
683
|
+
assert.equal(Object.keys(M1.definition).sort().join(','), "a,b,c,d", "definition merge for multiple inheritance 1/4");
|
|
684
|
+
assert.equal(Object.keys(M2.definition).sort().join(','), "a,b,c,d", "definition merge for multiple inheritance 2/4");
|
|
685
|
+
assert.equal(Object.keys(M1.definition.d).sort().join(','), "d1,d2,d3", "definition merge for multiple inheritance 3/4");
|
|
686
|
+
assert.equal(Object.keys(M2.definition.d).sort().join(','), "d1,d2,d3", "definition merge for multiple inheritance 4/4");
|
|
687
|
+
|
|
688
|
+
let m1 = M1({
|
|
689
|
+
a: "",
|
|
690
|
+
b: 42,
|
|
691
|
+
c: "test",
|
|
692
|
+
d: {
|
|
693
|
+
d1: true,
|
|
694
|
+
d2: 2,
|
|
695
|
+
d3: 3
|
|
696
|
+
}
|
|
697
|
+
});
|
|
698
|
+
|
|
699
|
+
let m2 = M2({
|
|
700
|
+
a: false,
|
|
701
|
+
b: false,
|
|
702
|
+
c: 666,
|
|
703
|
+
d: {
|
|
704
|
+
d1: false,
|
|
705
|
+
d2: false,
|
|
706
|
+
d3: 0
|
|
707
|
+
}
|
|
708
|
+
});
|
|
709
|
+
|
|
710
|
+
assert.throws(function () {
|
|
711
|
+
m1.a = true;
|
|
712
|
+
}, /TypeError[\s\S]*a/, "type checking multiple inheritance 1/8");
|
|
713
|
+
assert.throws(function () {
|
|
714
|
+
m2.a = "nope";
|
|
715
|
+
}, /TypeError[\s\S]*a/, "type checking multiple inheritance 2/8");
|
|
716
|
+
assert.throws(function () {
|
|
717
|
+
m1.b = !m1.b;
|
|
718
|
+
}, /TypeError[\s\S]*b/, "type checking multiple inheritance 3/8");
|
|
719
|
+
assert.throws(function () {
|
|
720
|
+
m2.b += 7;
|
|
721
|
+
}, /TypeError[\s\S]*b/, "type checking multiple inheritance 4/8");
|
|
722
|
+
assert.throws(function () {
|
|
723
|
+
m1.c = undefined;
|
|
724
|
+
}, /TypeError[\s\S]*c/, "type checking multiple inheritance 5/8");
|
|
725
|
+
assert.throws(function () {
|
|
726
|
+
m2.c = null;
|
|
727
|
+
}, /TypeError[\s\S]*c/, "type checking multiple inheritance 6/8");
|
|
728
|
+
assert.throws(function () {
|
|
729
|
+
m1.d.d2 = true;
|
|
730
|
+
}, /TypeError[\s\S]*d2/, "type checking multiple inheritance 7/8");
|
|
731
|
+
assert.throws(function () {
|
|
732
|
+
m2.d.d2 = 1;
|
|
733
|
+
}, /TypeError[\s\S]*d2/, "type checking multiple inheritance 8/8");
|
|
734
|
+
|
|
735
|
+
A.defaultTo({
|
|
736
|
+
a: false,
|
|
737
|
+
b: false
|
|
738
|
+
});
|
|
739
|
+
|
|
740
|
+
B.defaultTo({
|
|
741
|
+
b: 0,
|
|
742
|
+
c: 0
|
|
743
|
+
});
|
|
744
|
+
|
|
745
|
+
C.defaultTo({
|
|
746
|
+
c: "",
|
|
747
|
+
d: {
|
|
748
|
+
d1: false,
|
|
749
|
+
d2: false
|
|
750
|
+
}
|
|
751
|
+
});
|
|
752
|
+
|
|
753
|
+
D.defaultTo({
|
|
754
|
+
a: "",
|
|
755
|
+
d: {
|
|
756
|
+
d2: 0,
|
|
757
|
+
d3: 0
|
|
758
|
+
}
|
|
759
|
+
});
|
|
760
|
+
|
|
761
|
+
M1 = A.extend(B, C, D);
|
|
762
|
+
M2 = D.extend(C, B, A);
|
|
763
|
+
m1 = M1();
|
|
764
|
+
m2 = M2();
|
|
765
|
+
|
|
766
|
+
assert.ok(m1.a === "" && m1.b === 0 && m1.c === "" && m1.d.d1 === false && m1.d.d2 === 0 && m1.d.d3 === 0, "defaults checking multiple inheritance 1/2");
|
|
767
|
+
assert.ok(m2.a === false && m2.b === false && m2.c === 0 && m2.d.d1 === false && m2.d.d2 === false && m2.d.d3 === 0, "defaults checking multiple inheritance 2/2");
|
|
768
|
+
|
|
769
|
+
function dummyAssert() {
|
|
770
|
+
return true;
|
|
771
|
+
}
|
|
772
|
+
|
|
773
|
+
A.assert(dummyAssert);
|
|
774
|
+
B.assert(dummyAssert);
|
|
775
|
+
C.assert(dummyAssert);
|
|
776
|
+
D.assert(dummyAssert);
|
|
777
|
+
|
|
778
|
+
M1 = A.extend(B, C, D);
|
|
779
|
+
M2 = D.extend(C, B, A);
|
|
780
|
+
m1 = M1();
|
|
781
|
+
m2 = M2();
|
|
782
|
+
|
|
783
|
+
assert.ok(M1.assertions.length === 4, "assertions checking multiple inheritance 1/2");
|
|
784
|
+
assert.ok(M2.assertions.length === 4, "assertions checking multiple inheritance 2/2");
|
|
785
|
+
|
|
786
|
+
});
|
|
787
|
+
|
|
788
|
+
QUnit.test("Composition", function (assert) {
|
|
789
|
+
|
|
790
|
+
const Person = ObjectModel({
|
|
791
|
+
name: String,
|
|
792
|
+
age: [Number, Date],
|
|
793
|
+
female: [Boolean],
|
|
794
|
+
address: {
|
|
795
|
+
work: {
|
|
796
|
+
city: [String]
|
|
797
|
+
}
|
|
798
|
+
}
|
|
799
|
+
});
|
|
800
|
+
|
|
801
|
+
const Family = ObjectModel({
|
|
802
|
+
father: Person,
|
|
803
|
+
mother: Person.extend({ female: true }),
|
|
804
|
+
children: ArrayModel(Person),
|
|
805
|
+
grandparents: [ArrayModel(Person).assert(function (persons) {
|
|
806
|
+
return persons && persons.length <= 4
|
|
807
|
+
})]
|
|
808
|
+
});
|
|
809
|
+
|
|
810
|
+
const joe = Person({
|
|
811
|
+
name: "Joe",
|
|
812
|
+
age: 42,
|
|
813
|
+
female: false
|
|
814
|
+
});
|
|
815
|
+
|
|
816
|
+
|
|
817
|
+
const ann = new Person({
|
|
818
|
+
female: true,
|
|
819
|
+
age: joe.age - 5,
|
|
820
|
+
name: joe.name + "'s wife"
|
|
821
|
+
});
|
|
822
|
+
|
|
823
|
+
let joefamily = new Family({
|
|
824
|
+
father: joe,
|
|
825
|
+
mother: ann,
|
|
826
|
+
children: [],
|
|
827
|
+
grandparents: []
|
|
828
|
+
});
|
|
829
|
+
|
|
830
|
+
assert.ok(joefamily instanceof Family, "joefamily instance of Family");
|
|
831
|
+
assert.ok(joefamily.father instanceof Person, "father instanceof Person");
|
|
832
|
+
assert.ok(joefamily.mother instanceof Person, "mother instanceof Person");
|
|
833
|
+
|
|
834
|
+
const duckmother = {
|
|
835
|
+
female: true,
|
|
836
|
+
age: joe.age - 5,
|
|
837
|
+
name: joe.name + "'s wife"
|
|
838
|
+
};
|
|
839
|
+
|
|
840
|
+
joefamily = new Family({
|
|
841
|
+
father: joe,
|
|
842
|
+
mother: duckmother,
|
|
843
|
+
children: []
|
|
844
|
+
});
|
|
845
|
+
|
|
846
|
+
assert.ok(Person.test(duckmother), "Autocasting for object properties 1/2");
|
|
847
|
+
assert.notOk(duckmother instanceof Person, "Autocasting for object properties 2/2");
|
|
848
|
+
|
|
849
|
+
joefamily.mother.name = "Daisy";
|
|
850
|
+
assert.equal(joefamily.mother.name, "Daisy", "Autocasted submodel property can be modified");
|
|
851
|
+
assert.throws(function () {
|
|
852
|
+
joefamily.mother.female = "Quack !";
|
|
853
|
+
}, /TypeError[\s\S]*female/, "validation of submodel autocasted at modification");
|
|
854
|
+
|
|
855
|
+
assert.throws(function () {
|
|
856
|
+
new Family({
|
|
857
|
+
father: joe,
|
|
858
|
+
mother: {
|
|
859
|
+
female: false,
|
|
860
|
+
age: joe.age - 5,
|
|
861
|
+
name: joe.name + "'s wife"
|
|
862
|
+
},
|
|
863
|
+
children: []
|
|
864
|
+
});
|
|
865
|
+
}, /TypeError[\s\S]*female/, "validation of submodel autocasted at instanciation");
|
|
866
|
+
|
|
867
|
+
});
|
|
868
|
+
|
|
869
|
+
QUnit.test("Assertions", function (assert) {
|
|
870
|
+
|
|
871
|
+
const NestedModel = ObjectModel({ foo: { bar: { baz: Boolean } } })
|
|
872
|
+
.assert(o => o.foo.bar.baz === true);
|
|
873
|
+
|
|
874
|
+
const nestedModel = NestedModel({ foo: { bar: { baz: true } } });
|
|
875
|
+
|
|
876
|
+
assert.throws(function () {
|
|
877
|
+
nestedModel.foo.bar.baz = false;
|
|
878
|
+
}, /TypeError/, "test assertion after nested property assignment");
|
|
879
|
+
|
|
880
|
+
function assertFail() {
|
|
881
|
+
return false;
|
|
882
|
+
}
|
|
883
|
+
|
|
884
|
+
function assertFailWithData() {
|
|
885
|
+
return -1;
|
|
886
|
+
}
|
|
887
|
+
|
|
888
|
+
Model.prototype.assert(assertFail, "expected message without data");
|
|
889
|
+
ObjectModel.prototype.assert(assertFailWithData, function (data) {
|
|
890
|
+
return "expected message with data " + data;
|
|
891
|
+
});
|
|
892
|
+
|
|
893
|
+
assert.equal(Model.prototype.assertions.length, 1, "check number of assertions on BasicModel.prototype")
|
|
894
|
+
assert.equal(ObjectModel.prototype.assertions.length, 2, "check number of assertions on ObjectModel.prototype");
|
|
895
|
+
|
|
896
|
+
const M = ObjectModel({ a: String });
|
|
897
|
+
|
|
898
|
+
assert.throws(function () {
|
|
899
|
+
M({ a: "test" })
|
|
900
|
+
}, /TypeError/, "expected message without data");
|
|
901
|
+
|
|
902
|
+
assert.throws(function () {
|
|
903
|
+
M({ a: "test" })
|
|
904
|
+
}, /TypeError/, "expected message with data -1");
|
|
905
|
+
|
|
906
|
+
// clean up global assertions
|
|
907
|
+
Model.prototype.assertions = [];
|
|
908
|
+
delete ObjectModel.prototype.assertions;
|
|
909
|
+
|
|
910
|
+
const AssertObject = ObjectModel({ name: [String] })
|
|
911
|
+
.assert((o => o.name.toLowerCase().length === o.name.length), "may throw exception");
|
|
912
|
+
|
|
913
|
+
new AssertObject({ name: "joe" });
|
|
914
|
+
|
|
915
|
+
assert.throws(function () {
|
|
916
|
+
new AssertObject({ name: undefined });
|
|
917
|
+
},
|
|
918
|
+
/assertion "may throw exception" returned TypeError.*for value {\s+name: undefined\s+}/,
|
|
919
|
+
"assertions catch exceptions on Object models");
|
|
920
|
+
|
|
921
|
+
});
|
|
922
|
+
|
|
923
|
+
QUnit.test("test method", function (assert) {
|
|
924
|
+
|
|
925
|
+
const assertFunction = (c => c === "GB");
|
|
926
|
+
|
|
927
|
+
assertFunction.toString = function () {
|
|
928
|
+
return "expected assertFunction toString";
|
|
929
|
+
}
|
|
930
|
+
|
|
931
|
+
const Address = new ObjectModel({
|
|
932
|
+
city: String,
|
|
933
|
+
country: BasicModel(String).assert(assertFunction, "Country must be GB")
|
|
934
|
+
});
|
|
935
|
+
|
|
936
|
+
function isPositive(n) { return n > 0 }
|
|
937
|
+
const PositiveNumber = BasicModel(Number)
|
|
938
|
+
.assert(isPositive, "expected assertion description")
|
|
939
|
+
.as('PositiveNumber');
|
|
940
|
+
|
|
941
|
+
const gbAddress = { city: "London", country: "GB" };
|
|
942
|
+
const frAddress = { city: "Paris", country: "FR" };
|
|
943
|
+
|
|
944
|
+
const Order = new ObjectModel({
|
|
945
|
+
sku: String,
|
|
946
|
+
address: Address,
|
|
947
|
+
quantity: [PositiveNumber]
|
|
948
|
+
});
|
|
949
|
+
|
|
950
|
+
const gbOrder = { sku: "ABC123", address: gbAddress };
|
|
951
|
+
const frOrder = { sku: "ABC123", address: frAddress };
|
|
952
|
+
|
|
953
|
+
new Order(gbOrder); // no errors
|
|
954
|
+
assert.throws(function () {
|
|
955
|
+
new Order(frOrder);
|
|
956
|
+
}, "should validate sub-objects assertions");
|
|
957
|
+
|
|
958
|
+
let errors = [];
|
|
959
|
+
Order.test(frOrder, function (err) {
|
|
960
|
+
errors.push(...err);
|
|
961
|
+
});
|
|
962
|
+
|
|
963
|
+
assert.equal(errors.length, 1, "should throw exactly one error here")
|
|
964
|
+
assert.equal(errors[0].expected, "expected assertFunction toString", "check assertion error expected parameter");
|
|
965
|
+
assert.equal(errors[0].received, "FR", "check assertion error received parameter");
|
|
966
|
+
assert.equal(errors[0].path, "address.country", "check assertion error path parameter");
|
|
967
|
+
assert.equal(errors[0].message, 'assertion "Country must be GB" returned false for address.country = "FR"', "check assertion error message parameter");
|
|
968
|
+
|
|
969
|
+
gbOrder.quantity = -1
|
|
970
|
+
errors = [];
|
|
971
|
+
Order.test(gbOrder, function (err) {
|
|
972
|
+
errors.push(...err);
|
|
973
|
+
});
|
|
974
|
+
|
|
975
|
+
assert.equal(errors.length, 1, "should throw exactly one error here")
|
|
976
|
+
assert.equal(errors[0].expected, isPositive, "check assertion error expected parameter");
|
|
977
|
+
assert.equal(errors[0].received, "-1", "check assertion error received parameter");
|
|
978
|
+
assert.equal(errors[0].path, "quantity", "check assertion error path parameter");
|
|
979
|
+
assert.equal(errors[0].message, `assertion "expected assertion description" returned false for quantity = -1`, "check assertion error message parameter");
|
|
980
|
+
|
|
981
|
+
});
|
|
982
|
+
|
|
983
|
+
QUnit.test("Cyclic detection", function (assert) {
|
|
984
|
+
|
|
985
|
+
let A, B, a, b;
|
|
986
|
+
|
|
987
|
+
A = ObjectModel({ b: [] });
|
|
988
|
+
B = ObjectModel({ a: A });
|
|
989
|
+
A.definition.b = [B];
|
|
990
|
+
|
|
991
|
+
a = A();
|
|
992
|
+
b = B({ a: a });
|
|
993
|
+
|
|
994
|
+
assert.ok(a.b = b, "valid cyclic value assignment");
|
|
995
|
+
assert.throws(function () { a.b = a; }, /TypeError/, "invalid cyclic value assignment");
|
|
996
|
+
|
|
997
|
+
A = ObjectModel({ b: [] });
|
|
998
|
+
B = ObjectModel({ a: A });
|
|
999
|
+
|
|
1000
|
+
A.definition.b = {
|
|
1001
|
+
c: {
|
|
1002
|
+
d: [B]
|
|
1003
|
+
}
|
|
1004
|
+
};
|
|
1005
|
+
|
|
1006
|
+
a = A();
|
|
1007
|
+
b = B({ a: a });
|
|
1008
|
+
|
|
1009
|
+
assert.ok((a.b = { c: { d: b } }), "valid deep cyclic value assignment");
|
|
1010
|
+
assert.throws(function () {
|
|
1011
|
+
a.b = { c: { d: a } };
|
|
1012
|
+
}, /TypeError/, "invalid deep cyclic value assignment");
|
|
1013
|
+
|
|
1014
|
+
const Honey = ObjectModel({
|
|
1015
|
+
sweetie: [] // Sweetie is not yet defined
|
|
1016
|
+
});
|
|
1017
|
+
|
|
1018
|
+
const Sweetie = ObjectModel({
|
|
1019
|
+
honey: Honey
|
|
1020
|
+
});
|
|
1021
|
+
|
|
1022
|
+
Honey.definition.sweetie = [Sweetie];
|
|
1023
|
+
|
|
1024
|
+
const joe = Honey({ sweetie: undefined }); // ann is not yet defined
|
|
1025
|
+
const ann = Sweetie({ honey: joe });
|
|
1026
|
+
assert.ok(joe.sweetie = ann, "website example valid assignment");
|
|
1027
|
+
assert.throws(function () { joe.sweetie = "dog" }, /TypeError/, "website example invalid assignment 1");
|
|
1028
|
+
assert.throws(function () { joe.sweetie = joe }, /TypeError/, "website example invalid assignment 2");
|
|
1029
|
+
|
|
1030
|
+
});
|
|
1031
|
+
|
|
1032
|
+
QUnit.test("Custom error collectors", function (assert) {
|
|
1033
|
+
|
|
1034
|
+
assert.expect(11);
|
|
1035
|
+
|
|
1036
|
+
let M = ObjectModel({
|
|
1037
|
+
a: {
|
|
1038
|
+
b: {
|
|
1039
|
+
c: true
|
|
1040
|
+
}
|
|
1041
|
+
}
|
|
1042
|
+
});
|
|
1043
|
+
|
|
1044
|
+
M.errorCollector = function (errors) {
|
|
1045
|
+
assert.ok(errors.length === 1, "check errors.length model collector");
|
|
1046
|
+
var err = errors[0];
|
|
1047
|
+
assert.equal(err.expected, true, "check error.expected model collector");
|
|
1048
|
+
assert.equal(err.received, false, "check error.received model collector");
|
|
1049
|
+
assert.equal(err.path, "a.b.c", "check error.path model collector");
|
|
1050
|
+
assert.equal(err.message, 'expecting a.b.c to be true, got Boolean false', "check error message model collector");
|
|
1051
|
+
}
|
|
1052
|
+
|
|
1053
|
+
M({
|
|
1054
|
+
a: {
|
|
1055
|
+
b: {
|
|
1056
|
+
c: false
|
|
1057
|
+
}
|
|
1058
|
+
}
|
|
1059
|
+
})
|
|
1060
|
+
|
|
1061
|
+
ObjectModel({
|
|
1062
|
+
d: {
|
|
1063
|
+
e: {
|
|
1064
|
+
f: null
|
|
1065
|
+
}
|
|
1066
|
+
}
|
|
1067
|
+
}).test({
|
|
1068
|
+
d: {
|
|
1069
|
+
e: {
|
|
1070
|
+
f: undefined
|
|
1071
|
+
}
|
|
1072
|
+
}
|
|
1073
|
+
}, function (errors) {
|
|
1074
|
+
assert.ok(errors.length === 1, "check nested errors.length custom collector");
|
|
1075
|
+
var err = errors[0];
|
|
1076
|
+
assert.deepEqual(err.expected, null, "check nested error.expected custom collector");
|
|
1077
|
+
assert.deepEqual(err.received, undefined, "check nested error.received custom collector");
|
|
1078
|
+
assert.equal(err.path, "d.e.f", "check nested error.path custom collector");
|
|
1079
|
+
assert.equal(err.message, 'expecting d.e.f to be null, got undefined', "check nested error.message custom collector");
|
|
1080
|
+
})
|
|
1081
|
+
|
|
1082
|
+
M = ObjectModel({ x: Number });
|
|
1083
|
+
M.errorCollector = function noop() { };
|
|
1084
|
+
|
|
1085
|
+
assert.equal(M.test({ x: "nope" }), false, "model.test should work even when errorCollector does not throw exceptions");
|
|
1086
|
+
|
|
1087
|
+
});
|
|
1088
|
+
|
|
1089
|
+
QUnit.test("Automatic model casting", function (assert) {
|
|
1090
|
+
|
|
1091
|
+
let User = new ObjectModel({ username: String, email: String })
|
|
1092
|
+
.defaultTo({ username: 'foo', email: 'foo@foo' });
|
|
1093
|
+
|
|
1094
|
+
let Article = new ObjectModel({ title: String, user: User })
|
|
1095
|
+
.defaultTo({ title: 'bar', user: new User() });
|
|
1096
|
+
|
|
1097
|
+
let a = new Article();
|
|
1098
|
+
a.user = { username: 'joe', email: 'foo' };
|
|
1099
|
+
|
|
1100
|
+
assert.ok(a.user instanceof User, "automatic model casting when assigning an autocasted object");
|
|
1101
|
+
assert.ok(a.user.username === "joe", "preserved props after automatic model casting of autocasted object");
|
|
1102
|
+
|
|
1103
|
+
User = new ObjectModel({ username: String, email: String })
|
|
1104
|
+
.defaultTo({ username: 'foo', email: 'foo@foo' });
|
|
1105
|
+
|
|
1106
|
+
Article = new ObjectModel({ title: String, user: [User] })
|
|
1107
|
+
.defaultTo({ title: 'bar', user: new User() });
|
|
1108
|
+
|
|
1109
|
+
a = new Article();
|
|
1110
|
+
a.user = { username: 'joe', email: 'foo' };
|
|
1111
|
+
|
|
1112
|
+
assert.ok(a.user instanceof User, "automatic optional model casting when assigning an autocasted object");
|
|
1113
|
+
assert.ok(a.user.username === "joe", "preserved props after automatic optional model casting of autocasted object");
|
|
1114
|
+
|
|
1115
|
+
|
|
1116
|
+
const Type1 = ObjectModel({ name: String, other1: [Boolean] });
|
|
1117
|
+
const Type2 = ObjectModel({ name: String, other2: [Number] });
|
|
1118
|
+
const Container = ObjectModel({ foo: { bar: [Type1, Type2] } });
|
|
1119
|
+
|
|
1120
|
+
consoleMock.apply();
|
|
1121
|
+
let c = new Container({ foo: { bar: { name: "dunno" } } });
|
|
1122
|
+
|
|
1123
|
+
assert.ok(/Ambiguous model for[\s\S]*?name: "dunno"[\s\S]*?other1: \[Boolean][\s\S]*?other2: \[Number]/
|
|
1124
|
+
.test(consoleMock.lastArgs.warn[0]),
|
|
1125
|
+
"should warn about ambiguous model for object sub prop"
|
|
1126
|
+
);
|
|
1127
|
+
assert.ok(c.foo.bar.name === "dunno", "should preserve values even when ambiguous model cast");
|
|
1128
|
+
assert.ok(!(c.foo.bar instanceof Type1 || c.foo.bar instanceof Type2), "should not cast when ambiguous model");
|
|
1129
|
+
consoleMock.revert();
|
|
1130
|
+
|
|
1131
|
+
consoleMock.apply();
|
|
1132
|
+
c = new Container({ foo: { bar: Type2({ name: "dunno" }) } });
|
|
1133
|
+
assert.ok(consoleMock.lastArgs.warn.length === 0, "should not warn when explicit model cast in ambiguous context");
|
|
1134
|
+
assert.ok(c.foo.bar.name === "dunno", "should preserve values when explicit model cast in ambiguous context");
|
|
1135
|
+
assert.ok(c.foo.bar instanceof Type2, "should preserve model when explicit cast in ambiguous context");
|
|
1136
|
+
consoleMock.revert();
|
|
1137
|
+
|
|
1138
|
+
let noProto = Object.create(null);
|
|
1139
|
+
noProto.x = true;
|
|
1140
|
+
let noProtoModel = ObjectModel({ x: Boolean })
|
|
1141
|
+
let noProtoInstance = noProtoModel(noProto)
|
|
1142
|
+
assert.equal(noProtoInstance.x, true, "should be able to init with no-proto objects");
|
|
1143
|
+
noProtoInstance.y = Object.create(null);
|
|
1144
|
+
assert.ok(typeof noProtoInstance.y === "object", "should be able to mutate and cast with no-proto objects");
|
|
1145
|
+
|
|
1146
|
+
})
|
|
1147
|
+
|
|
1148
|
+
QUnit.test("delete trap", function (assert) {
|
|
1149
|
+
|
|
1150
|
+
const M = ObjectModel({ _p: Boolean, C: Number, u: undefined, n: null, x: [Boolean] })
|
|
1151
|
+
const m = M({ _p: true, C: 42, u: undefined, n: null, x: false })
|
|
1152
|
+
|
|
1153
|
+
assert.throws(function () { delete m._p }, /TypeError.*private/, "cannot delete private prop");
|
|
1154
|
+
assert.throws(function () { delete m.C }, /TypeError.*constant/, "cannot delete constant prop");
|
|
1155
|
+
delete m.u; // can delete undefined properties
|
|
1156
|
+
assert.throws(function () { delete m.n }, /TypeError.*expecting n to be null, got undefined/, "delete should differenciate null and undefined");
|
|
1157
|
+
delete m.x // can delete optional properties
|
|
1158
|
+
|
|
1159
|
+
})
|
|
1160
|
+
|
|
1161
|
+
QUnit.test("defineProperty trap", function (assert) {
|
|
1162
|
+
|
|
1163
|
+
const M = ObjectModel({ _p: Boolean, C: Number, u: undefined, n: null, x: [Boolean] })
|
|
1164
|
+
const m = M({ _p: true, C: 42, u: undefined, n: null, x: false })
|
|
1165
|
+
|
|
1166
|
+
assert.throws(function () { Object.defineProperty(m, "_p", { value: true }) }, /TypeError.*private/, "cannot define private prop");
|
|
1167
|
+
assert.throws(function () { Object.defineProperty(m, "C", { value: 43 }) }, /TypeError.*constant/, "cannot define constant prop");
|
|
1168
|
+
assert.throws(function () { Object.defineProperty(m, "u", { value: "test" }) }, /TypeError.*expecting u to be undefined/, "check type after defineProperty");
|
|
1169
|
+
assert.throws(function () { Object.defineProperty(m, "n", { value: undefined }) }, /TypeError.*expecting n to be null, got undefined/, "defineProperty should differenciate null and undefined");
|
|
1170
|
+
Object.defineProperty(m, "x", { value: undefined }) // can define optional properties
|
|
1171
|
+
|
|
1172
|
+
})
|
|
1173
|
+
|
|
1174
|
+
QUnit.test("ownKeys/has trap", function (assert) {
|
|
1175
|
+
|
|
1176
|
+
const A = ObjectModel({ _pa: Boolean, a: Boolean, oa: [Boolean] })
|
|
1177
|
+
const B = A.extend({ _pb: Boolean, b: Boolean, ob: [Boolean] })
|
|
1178
|
+
const m = B({ _pa: true, _pb: true, a: true, b: true, oa: undefined, undefined: true })
|
|
1179
|
+
B.prototype.B = true;
|
|
1180
|
+
B.prototype._PB = true;
|
|
1181
|
+
A.prototype.A = true;
|
|
1182
|
+
A.prototype._PA = true;
|
|
1183
|
+
|
|
1184
|
+
assert.equal("a" in m, true, "inherited prop in")
|
|
1185
|
+
assert.equal("b" in m, true, "own prop in")
|
|
1186
|
+
assert.equal("toString" in m, true, "Object.prototype prop in")
|
|
1187
|
+
|
|
1188
|
+
assert.equal("A" in m, false, "custom prop inherited prototype in")
|
|
1189
|
+
assert.equal("B" in m, false, "custom prop own prototype in")
|
|
1190
|
+
assert.equal("_pa" in m, false, "private inherited prop in")
|
|
1191
|
+
assert.equal("_pb" in m, false, "private own prop in")
|
|
1192
|
+
assert.equal("_PA" in m, false, "inherited prototype custom private prop in")
|
|
1193
|
+
assert.equal("_PB" in m, false, "own prototype custom private prop in")
|
|
1194
|
+
assert.equal("oa" in m, true, "optional assigned prop in")
|
|
1195
|
+
assert.equal("ob" in m, false, "optional unassigned prop in")
|
|
1196
|
+
assert.equal("unknown" in m, false, "unassigned undefined prop in")
|
|
1197
|
+
assert.equal("undefined" in m, false, "assigned undefined prop in")
|
|
1198
|
+
|
|
1199
|
+
const oKeys = Object.keys(m);
|
|
1200
|
+
|
|
1201
|
+
const ownKeys = Object.getOwnPropertyNames(m);
|
|
1202
|
+
|
|
1203
|
+
assert.equal(oKeys.sort().join(","), "a,b,oa", "Object.keys")
|
|
1204
|
+
assert.equal(ownKeys.sort().join(","), "a,b,oa", "Object.getOwnPropertyNames")
|
|
1205
|
+
})
|
|
1206
|
+
|
|
1207
|
+
QUnit.test("class constructors", function (assert) {
|
|
1208
|
+
|
|
1209
|
+
const PersonModel = ObjectModel({ firstName: String, lastName: String, fullName: String })
|
|
1210
|
+
|
|
1211
|
+
class Person extends PersonModel {
|
|
1212
|
+
constructor({ firstName, lastName }) {
|
|
1213
|
+
const fullName = `${firstName} ${lastName}`
|
|
1214
|
+
super({ firstName, lastName, fullName })
|
|
1215
|
+
}
|
|
1216
|
+
}
|
|
1217
|
+
|
|
1218
|
+
const person = new Person({ firstName: "John", lastName: "Smith" })
|
|
1219
|
+
assert.ok(person instanceof Person, "person instanceof Person")
|
|
1220
|
+
assert.ok(person instanceof PersonModel, "person instanceof PersonModel")
|
|
1221
|
+
assert.equal(person.fullName, "John Smith", "check es6 class constructor")
|
|
1222
|
+
|
|
1223
|
+
const UserModel = Person.extend({ role: String });
|
|
1224
|
+
class User extends UserModel {
|
|
1225
|
+
constructor({ firstName, lastName, role }) {
|
|
1226
|
+
super({ firstName, lastName, role })
|
|
1227
|
+
if (role === "admin") {
|
|
1228
|
+
this.fullName += " [ADMIN]"
|
|
1229
|
+
}
|
|
1230
|
+
}
|
|
1231
|
+
}
|
|
1232
|
+
|
|
1233
|
+
const user = new User({ firstName: "John", lastName: "Smith", role: "admin" })
|
|
1234
|
+
|
|
1235
|
+
assert.ok(user instanceof User, "user instanceof User")
|
|
1236
|
+
assert.ok(user instanceof UserModel, "user instanceof UserModel")
|
|
1237
|
+
assert.ok(user instanceof Person, "user instanceof Person")
|
|
1238
|
+
assert.ok(user instanceof PersonModel, "user instanceof PersonModel")
|
|
1239
|
+
assert.equal(user.fullName, "John Smith [ADMIN]", "check es6 class constructor with extended class")
|
|
1240
|
+
assert.equal(Object.keys(User.definition).sort().join(","), "firstName,fullName,lastName,role", "check definition keys")
|
|
1241
|
+
assert.equal(Object.keys(user).sort().join(","), "firstName,fullName,lastName,role", "check instance keys")
|
|
1242
|
+
assert.throws(function () { user.role = null; }, /TypeError/, "extended class model check definition")
|
|
1243
|
+
|
|
1244
|
+
const Lovers = class Lovers extends ObjectModel({
|
|
1245
|
+
husband: Person,
|
|
1246
|
+
wife: Person,
|
|
1247
|
+
}) { };
|
|
1248
|
+
|
|
1249
|
+
const joe = { firstName: 'Joe', lastName: "Smith" };
|
|
1250
|
+
const ann = new Person({ firstName: "Ann", lastName: "Smith" });
|
|
1251
|
+
|
|
1252
|
+
const couple = new Lovers({
|
|
1253
|
+
husband: joe, // object autocasted
|
|
1254
|
+
wife: ann, // object model
|
|
1255
|
+
});
|
|
1256
|
+
|
|
1257
|
+
assert.ok(couple.husband instanceof Person, "autocasting works with class-based models");
|
|
1258
|
+
|
|
1259
|
+
assert.equal(Person.test({ firstName: 0, lastName: "" }), false, `test method with class-based models`);
|
|
1260
|
+
|
|
1261
|
+
class Request extends ObjectModel({ id: [Number] }) {
|
|
1262
|
+
setId(id) {
|
|
1263
|
+
this.id = id;
|
|
1264
|
+
}
|
|
1265
|
+
}
|
|
1266
|
+
|
|
1267
|
+
let x = new Request({});
|
|
1268
|
+
assert.throws(function () { x.setId("32") }, /TypeError/, "class setters methods should provide type checking");
|
|
1269
|
+
|
|
1270
|
+
const BaseOM = ObjectModel({})
|
|
1271
|
+
let getterRequiringNoValidationCallCount = 0;
|
|
1272
|
+
class BaseClass extends BaseOM {
|
|
1273
|
+
get getterRequiringNoValidation() {
|
|
1274
|
+
getterRequiringNoValidationCallCount++;
|
|
1275
|
+
return true;
|
|
1276
|
+
}
|
|
1277
|
+
}
|
|
1278
|
+
|
|
1279
|
+
const SubOM = BaseClass.extend({ test: [Boolean] }).assert(o => o.test)
|
|
1280
|
+
class SubClass extends SubOM { }
|
|
1281
|
+
|
|
1282
|
+
SubClass.test({ test: false }, () => { });
|
|
1283
|
+
|
|
1284
|
+
assert.equal(SubClass.errors.length, 0, "class-based models errors are cleaned up properly 1/4")
|
|
1285
|
+
assert.equal(SubOM.errors.length, 0, "class-based models errors are cleaned up properly 2/4")
|
|
1286
|
+
assert.equal(BaseClass.errors.length, 0, "class-based models errors are cleaned up properly 3/4")
|
|
1287
|
+
assert.equal(BaseOM.errors.length, 0, "class-based models errors are cleaned up properly 4/4")
|
|
1288
|
+
|
|
1289
|
+
let bm = new BaseClass({});
|
|
1290
|
+
bm.getterRequired = bm.getterRequiringNoValidation;
|
|
1291
|
+
assert.equal(getterRequiringNoValidationCallCount, 1, "class getter requiring no validation only get once per call")
|
|
1292
|
+
})
|
|
1293
|
+
|
|
1294
|
+
QUnit.test("Null-safe object traversal", function (assert) {
|
|
1295
|
+
const Config = new ObjectModel({
|
|
1296
|
+
local: {
|
|
1297
|
+
time: {
|
|
1298
|
+
format: ["12h", "24h", undefined]
|
|
1299
|
+
}
|
|
1300
|
+
}
|
|
1301
|
+
});
|
|
1302
|
+
|
|
1303
|
+
const config = Config({ local: undefined }); // object autocasted
|
|
1304
|
+
|
|
1305
|
+
assert.strictEqual(config.local.time.format, undefined, "null-safe object traversal getter")
|
|
1306
|
+
config.local.time.format = "12h";
|
|
1307
|
+
assert.strictEqual(config.local.time.format, "12h", "null-safe object traversal setter")
|
|
1308
|
+
})
|
|
1309
|
+
|
|
1310
|
+
QUnit.test("Check once mode", function(assert){
|
|
1311
|
+
const Address = Model({ city: String, street: { name: String, number: Number }})
|
|
1312
|
+
assert.throws(function () {
|
|
1313
|
+
Address({ city: "Paris", street: { name: null, number: 12 }}, Model.CHECK_ONCE)
|
|
1314
|
+
}, /TypeError.*expecting street.name to be String/, "check once mode still checks at instanciation")
|
|
1315
|
+
const a = new Address({ city: "Paris", street: { name: "Champs-Elysees", number: 12 }}, Model.CHECK_ONCE)
|
|
1316
|
+
a.street.name = null;
|
|
1317
|
+
assert.strictEqual(a.street.name, null, "check once mode does not check future mutations")
|
|
1318
|
+
|
|
1319
|
+
class Person extends Model({ name: String, age: Number }) {
|
|
1320
|
+
constructor({ name, age }) {
|
|
1321
|
+
super({ name, age }, Model.CHECK_ONCE)
|
|
1322
|
+
}
|
|
1323
|
+
}
|
|
1324
|
+
|
|
1325
|
+
const john = new Person({ name: "John", age: 32 })
|
|
1326
|
+
john.age = "twelve";
|
|
1327
|
+
assert.strictEqual(john.age, "twelve", "check once mode does not check future mutations for extended class-based models")
|
|
1328
|
+
})
|
|
1329
|
+
|
|
1330
|
+
QUnit.test("Short-circuit validation when not receiving an object as expected", function(assert){
|
|
1331
|
+
const PersonModel = new ObjectModel({
|
|
1332
|
+
FirstName: String,
|
|
1333
|
+
LastName: String,
|
|
1334
|
+
})
|
|
1335
|
+
const errorsCollected = []
|
|
1336
|
+
PersonModel.errorCollector = function(errors){
|
|
1337
|
+
errorsCollected.push(...errors)
|
|
1338
|
+
}
|
|
1339
|
+
|
|
1340
|
+
PersonModel(42)
|
|
1341
|
+
assert.equal(errorsCollected.length, 1, "should only have 1 error")
|
|
1342
|
+
assert.equal(errorsCollected[0].message, `expecting {
|
|
1343
|
+
FirstName: String,
|
|
1344
|
+
LastName: String
|
|
1345
|
+
}, got Number 42`)
|
|
1328
1346
|
})
|