fashion-model
Version:
JavaScript library for defining types and their properties with support for wrapping/unwrapping and serialization/deserialization.
1,211 lines (975 loc) • 26.1 kB
JavaScript
const test = require('ava');
const Model = require('../Model');
const Enum = require('../Enum');
const DateType = require('../Date');
const ObservableModel = require('../ObservableModel');
const ArrayType = require('../Array');
const IntegerType = require('../Integer');
const BooleanType = require('../Boolean');
const ObjectType = require('../Object');
const StringType = require('../String');
const NumberType = require('../Number');
const FunctionType = require('../Function');
test('should provide metadata', function (t) {
const Person = Model.extend({
properties: {
dateOfBirth: Date
}
});
const DerivedPerson = Person.extend({
});
const Stub = Model.extend({
});
const DerivedStub = Stub.extend({
properties: {
name: String
}
});
const Simple = Stub.extend({
properties: {}
});
const ToString = Model.extend({
wrap: false,
coerce: function (value) {
return (value == null) ? value : value.toString();
}
});
t.true(Person.hasProperties());
t.true(Person.isWrapped());
t.true(DerivedPerson.hasProperties());
t.true(DerivedPerson.isWrapped());
t.false(Stub.hasProperties());
t.true(Stub.isWrapped());
t.true(DerivedStub.hasProperties());
t.true(DerivedStub.isWrapped());
t.false(Simple.hasProperties());
t.true(Simple.isWrapped());
t.false(ToString.hasProperties());
t.false(ToString.isWrapped());
});
test('should handle Date type', function (t) {
const date = DateType.coerce('1980-02-01T00:00:00.000Z');
t.deepEqual(date.getTime(), new Date(Date.UTC(1980, 1, 1, 0, 0, 0)).getTime());
t.throws(function () {
DateType.coerce(true);
}, Error);
const Person = Model.extend({
properties: {
dateOfBirth: Date
}
});
const person = new Person();
t.is(person.Model.properties.dateOfBirth.getName(), 'dateOfBirth');
person.setDateOfBirth(new Date(1980, 1, 1));
t.deepEqual(person.getDateOfBirth(), new Date(1980, 1, 1));
});
test('should coerce date with sub-millisecond precision when using static date coercion', function (t) {
const date = DateType.coerce('2022-01-10T21:15:41.573740635Z');
t.deepEqual(date.getTime(), 1641849341573);
});
test('should coerce date with sub-millisecond precision when using model getter/setter', function (t) {
const Document = Model.extend({
properties: {
dateCreated: Date
}
});
const document = new Document();
document.setDateCreated('2022-01-10T21:15:41.573740635Z');
t.deepEqual(document.getDateCreated().getTime(), 1641849341573);
});
test('should return null if invalid date supplied to date coercion', function (t) {
const date = DateType.coerce('invalid date');
t.deepEqual(date, null);
});
test('should serialize and deserialize date properly', function (t) {
const date = new Date();
const Ping = Model.extend({
properties: {
timestamp: Date
}
});
const ping = new Ping({
timestamp: date
});
const pong = Ping.wrap(JSON.parse(JSON.stringify(ping.clean())));
t.is(pong.getTimestamp().getTime(), ping.getTimestamp().getTime());
});
test('should serialize and deserialize date properly without Z suffix', function (t) {
const date = new Date('2016-04-13T18:00:00');
const Ping = Model.extend({
properties: {
timestamp: Date
}
});
const ping = new Ping({
timestamp: date
});
t.is(ping.getTimestamp(), date);
const pong = Ping.wrap(JSON.parse(JSON.stringify(ping.clean())));
t.is(pong.getTimestamp().getTime(), ping.getTimestamp().getTime());
});
test('should parse dates without Z suffix', function (t) {
const dateStr = '2016-04-13T18:00:00';
const date = new Date(dateStr);
const Ping = Model.extend({
properties: {
timestamp: Date
}
});
const ping = new Ping();
ping.setTimestamp(dateStr);
t.is(ping.getTimestamp().getTime(), date.getTime());
});
test('should provide setters', function (t) {
const Person = Model.extend({
properties: {
name: String,
dateOfBirth: Date
}
});
const person = new Person();
person.setName('John Doe');
person.setDateOfBirth(new Date(1980, 1, 1));
t.is(person.getName(), 'John Doe');
t.deepEqual(person.getDateOfBirth(), new Date(1980, 1, 1));
const rawPerson = person.unwrap();
t.is(rawPerson.name, 'John Doe');
t.deepEqual(rawPerson.dateOfBirth, new Date(1980, 1, 1));
});
test('should allow wrapping existing', function (t) {
const Person = Model.extend({
properties: {
name: String,
dateOfBirth: Date
}
});
const rawPerson = {
name: 'John Doe',
dateOfBirth: new Date(1980, 1, 1)
};
const person = new Person(rawPerson);
person.setName('Jane Doe');
// raw person should also reflect any changes
t.is(person.getName(), 'Jane Doe');
t.is(person.getName(), person.data.name);
t.deepEqual(person.getDateOfBirth(), new Date(1980, 1, 1));
t.is(person.getDateOfBirth(), person.data.dateOfBirth);
});
test('should allow custom property names', function (t) {
const Entity = Model.extend({
properties: {
id: {
type: String,
key: '_id'
}
}
});
const Person = Entity.extend({
properties: {
name: String,
dateOfBirth: Date
}
});
const person = new Person({
_id: 'test',
name: 'John Doe',
dateOfBirth: new Date(1980, 1, 1)
});
t.is(person.getId(), 'test');
});
test('should allow opaque wrapper type', function (t) {
const Token = Model.extend({});
const token = new Token({
a: 1,
b: 2
});
t.deepEqual(token.data, {
a: 1,
b: 2
});
const raw = token.unwrap();
t.is(raw.a, 1);
t.is(raw.b, 2);
t.deepEqual(token.clean(), {
a: 1,
b: 2
});
});
test('should support simplified enum array type', function (t) {
const Color = Enum.create({
values: ['red', 'green', 'blue']
});
t.is(Color.RED.clean(), 'red');
t.is(Color.RED.value(), 'red');
t.is(Color.RED.toString(), 'red');
const ColorPalette = Model.extend({
properties: {
colors: [Color]
}
});
const colorPalette = new ColorPalette({
colors: ['red', 'green', 'blue']
});
const colors = [];
colorPalette.getColors().forEach(function (color, index) {
t.is(color.constructor, Color);
colors[index] = color;
});
t.is(colors.length, 3);
t.is(colors[0], Color.RED);
t.is(colors[1], Color.GREEN);
t.is(colors[2], Color.BLUE);
t.is(Color.wrap(colorPalette.getColors()[0]), Color.RED);
t.is(Color.wrap(colorPalette.getColors()[1]), Color.GREEN);
t.is(Color.wrap(colorPalette.getColors()[2]), Color.BLUE);
});
test('should handle enum conversion errors', function (t) {
const Color = Enum.create({
values: ['red', 'green', 'blue']
});
t.throws(function () {
try {
Color.coerce('yellow');
} catch (e) {
t.is(e.source, Model);
throw e;
}
}, Error);
let errors;
errors = [];
// "yellow" is not a valid color
Color.coerce('yellow', errors);
t.is(errors.length, 1);
const Shirt = Model.extend({
properties: {
color: Color
}
});
const Person = Model.extend({
properties: {
shirt: Shirt
}
});
errors = [];
const person = new Person({
shirt: {
// "pink" is not a valid color
color: 'pink'
}
}, errors);
t.is(errors.length, 1);
errors = [];
// Manually add unrecognized property
person.data.blah = true;
const rawPerson = person.clean(errors);
t.is(errors.length, 1);
// color will be undefined since it is invalid
t.deepEqual(rawPerson, {
shirt: {}
});
});
test('should coerce Number primitive type', function (t) {
const Person = Model.extend({
properties: {
age: Number
}
});
const person = new Person();
person.setAge('10');
t.is(person.getAge(), 10);
t.throws(function () {
person.setAge('asdfsadf');
}, Error);
});
test('should coerce Boolean primitive type', function (t) {
const Person = Model.extend({
properties: {
happy: Boolean
}
});
const person = new Person();
person.setHappy(1);
t.true(person.getHappy());
person.setHappy(0);
t.false(person.getHappy());
person.setHappy();
t.is(person.getHappy(), undefined);
person.setHappy(null);
t.is(person.getHappy(), null);
});
test('should coerce String primitive type', function (t) {
const Person = Model.extend({
properties: {
message: String
}
});
const person = new Person();
person.setMessage(true);
t.is(person.getMessage(), 'true');
person.setMessage('Hello');
t.is(person.getMessage(), 'Hello');
person.setMessage(42);
t.is(person.getMessage(), '42');
person.setMessage(0);
t.is(person.getMessage(), '0');
person.setMessage();
t.is(person.getMessage(), undefined);
person.setMessage(null);
t.is(person.getMessage(), null);
});
test('should coerce array of primitives', function (t) {
const Something = Model.extend({
properties: {
arrayOfBooleans: [Boolean],
arrayOfAnything: [],
alsoArrayOfAnything: [{}],
anotherArrayOfAnything: [{type: {}}],
arrayOfIntegers: [{type: 'integer'}]
}
});
const something = Something.wrap({
arrayOfBooleans: [0, 1, 'abc', -1, 'true']
});
const arrayOfBooleans = something.getArrayOfBooleans();
t.false(arrayOfBooleans[0]);
t.true(arrayOfBooleans[1]);
t.false(arrayOfBooleans[2]);
t.true(arrayOfBooleans[3]);
t.true(arrayOfBooleans[4]);
something.setArrayOfAnything([123, 'abc', true]);
something.setAlsoArrayOfAnything([123, 'abc', true]);
something.setAnotherArrayOfAnything([123, 'abc', true]);
something.setArrayOfIntegers([123, 456]);
t.is(something.getArrayOfAnything()[0], 123);
t.is(something.getArrayOfAnything()[1], 'abc');
t.true(something.getArrayOfAnything()[2]);
t.is(something.getAlsoArrayOfAnything()[0], 123);
t.is(something.getAlsoArrayOfAnything()[1], 'abc');
t.true(something.getAlsoArrayOfAnything()[2]);
t.is(something.getAnotherArrayOfAnything()[0], 123);
t.is(something.getAnotherArrayOfAnything()[1], 'abc');
t.true(something.getAnotherArrayOfAnything()[2]);
t.is(something.getArrayOfIntegers()[0], 123);
t.is(something.getArrayOfIntegers()[1], 456);
});
test('should allow array as argument to wrap', function (t) {
const Something = Model.extend({
properties: {
anything: {},
alsoAnything: {
type: {}
}
}
});
const somethingList = Something.wrap([
{
anything: 123,
alsoAnything: 123
},
{
anything: 'abc',
alsoAnything: 'abc'
},
{
anything: true,
alsoAnything: true
}
]);
t.is(somethingList[0].getAnything(), 123);
t.is(somethingList[1].getAnything(), 'abc');
t.true(somethingList[2].getAnything());
t.is(somethingList[0].getAlsoAnything(), 123);
t.is(somethingList[1].getAlsoAnything(), 'abc');
t.true(somethingList[2].getAlsoAnything());
});
test('should coerce array of enums', function (t) {
const Color = Enum.create({
values: ['red', 'green', 'blue']
});
const Person = Model.extend({
properties: {
favoriteColors: [Color]
}
});
let person = Person.wrap({
favoriteColors: ['red', 'green', 'blue']
});
const favoriteColors = person.getFavoriteColors();
t.is(favoriteColors[0], Color.RED);
t.is(favoriteColors[1], Color.GREEN);
t.is(favoriteColors[2], Color.BLUE);
t.throws(function () {
person = Person.wrap({
favoriteColors: ['zero']
});
}, Error);
const errors = [];
person = Person.wrap({
favoriteColors: ['fake']
}, errors);
// should capture one error
t.is(errors.length, 1);
});
test('should coerce array of models', function (t) {
const Person = Model.extend({
properties: {
happy: Boolean
}
});
const Something = Model.extend({
properties: {
people: [Person]
}
});
const something = Something.wrap({
people: [
{
happy: 0
},
{
happy: false
},
{
happy: 1
},
{
happy: true
}
]
});
const people = something.getPeople();
t.false(people[0].getHappy());
t.false(people[1].getHappy());
t.true(people[2].getHappy());
t.true(people[3].getHappy());
t.false(Person.wrap(something.getPeople()[0]).getHappy());
t.false(Person.wrap(something.getPeople()[1]).getHappy());
t.true(Person.wrap(something.getPeople()[2]).getHappy());
t.true(Person.wrap(something.getPeople()[3]).getHappy());
const cleanSomething = Model.clean(something);
t.deepEqual(cleanSomething, {
people: [
{
happy: false
},
{
happy: false
},
{
happy: true
},
{
happy: true
}
]
});
});
test('should support integer type', function (t) {
const IntegerType = require('../Integer');
const ArrayType = require('../Array');
const Something = Model.extend({
properties: {
first: 'integer',
second: IntegerType,
firstArray: ['integer'],
secondArray: [IntegerType]
}
});
t.is(Something.getProperty('first').getType(), IntegerType);
t.is(Something.getProperty('second').getType(), IntegerType);
t.is(Something.getProperty('firstArray').getType(), ArrayType);
t.is(Something.getProperty('secondArray').getType(), ArrayType);
t.is(Something.getProperty('firstArray').getItems().type, IntegerType);
t.is(Something.getProperty('secondArray').getItems().type, IntegerType);
});
test('should support complex object validation', function (t) {
const IntegerType = require('../Integer');
const Something = Model.extend({
properties: {
name: String,
age: IntegerType
}
});
let errors;
errors = [];
Something.wrap({
name: 'John',
age: 30
}, errors);
t.is(errors.length, 0);
Something.wrap({
name: 'John',
age: 'blah'
}, errors);
t.is(errors.length, 1);
errors = [];
Something.wrap({
blah: 'Blah'
}, errors);
t.is(errors.length, 1);
});
test('should support strict validation', function (t) {
const IntegerType = require('../Integer');
const Something = Model.extend({
properties: {
someString: String,
someBoolean: Boolean,
someDate: Date,
someNumber: Number,
someInteger: IntegerType
},
additionalProperties: true
});
const errors = [];
Something.wrap({
someString: 123,
someBoolean: 1,
someDate: 0,
someNumber: '123',
someInteger: '123'
}, {
strict: true,
errors: errors
});
t.is(errors.length, 5);
});
test('should support array of array type', function (t) {
const IntegerType = require('../Integer');
const Item = Model.extend({
typeName: 'Item',
properties: {
id: IntegerType
}
});
const Something = Model.extend({
typeName: 'Something',
properties: {
// short-hand for defining an Array of Array
stuff: [[Item]],
// long-hand for defining an Array of Array
moreStuff: {
type: Array,
items: {
type: Array,
items: Item
}
}
},
additionalProperties: true
});
let errors;
let something;
errors = [];
something = Something.wrap({
stuff: [
[{id: '1'}, {id: '2'}],
[{id: '3'}, {id: '4'}]
]
}, errors);
t.is(errors.length, 0);
t.deepEqual(something.clean(), {
stuff: [
[{id: 1}, {id: 2}],
[{id: 3}, {id: 4}]
]
});
});
test('should not wrap and unwrap Model instances if property type is declared as object', function (t) {
const Item = Model.extend({
properties: {
id: String
}
});
const Something = Model.extend({
properties: {
// Use Object as type
item: Object
}
});
const something = new Something();
something.setItem(new Item({
id: 'abc'
}));
t.is(something.getItem().getId(), 'abc');
});
test('should allow Function type', function (t) {
const Item = Model.extend({
properties: {
handler: Function
}
});
let item;
t.throws(function () {
item = new Item({
handler: 'abc'
});
}, Error);
item = new Item({
handler: function () {}
});
t.is(item.getHandler().constructor, Function);
});
test('should implement isPrimitive', function (t) {
const Item = Model.extend({
properties: {
handler: Function
}
});
t.false(Item.isPrimitive());
const primitives = require('../primitives');
Object.keys(primitives).forEach(function (name) {
const Type = primitives[name];
t.true(Type.isPrimitive());
});
});
test('should provide an ObservableModel that emits change event', function (t) {
const Test = ObservableModel.extend({
typeName: 'Test',
properties: {
value: Number
}
});
const DerivedTest = Test.extend({
typeName: 'DerivedTest',
properties: {
anotherValue: Number
}
});
let emitCount = 0;
const test = new Test();
test.on('change', function () {
emitCount++;
});
test.setValue(1);
t.is(test.getValue(), 1);
t.is(emitCount, 1);
// reset emit count
emitCount = 0;
const derivedTest = new DerivedTest();
derivedTest.on('change', function () {
emitCount++;
});
derivedTest.setAnotherValue(2);
t.is(derivedTest.getAnotherValue(), 2);
t.is(emitCount, 1);
});
test('should support getters and setters', function (t) {
let getCallCount = 0;
let setCallCount = 0;
const Test = Model.extend({
properties: {
name: {
type: String,
get: function (property) {
getCallCount++;
t.is(property.getKey(), 'name');
t.is(property.getName(), 'name');
return this.data[property] + '!!!';
},
set: function (value, property) {
setCallCount++;
t.is(property.getKey(), 'name');
t.is(value, 'TEST');
t.is(property.getName(), 'name');
// our setter will convert to lower case
this.data[property] = value.toLowerCase();
}
}
}
});
const test = new Test();
test.setName('TEST');
// make sure setter converted to lower case...
t.is(test.data.name, 'test');
t.is(setCallCount, 1);
t.is(getCallCount, 0);
//
t.is(test.getName(), 'test!!!');
t.is(setCallCount, 1);
t.is(getCallCount, 1);
});
test('should allow self type references in property type', function (t) {
const NodeValue = Enum.create({
values: ['a', 'b', 'c']
});
const Node = Model.extend({
properties: {
next: 'self',
value: NodeValue
}
});
let errors = [];
const node = new Node();
node.setNext({
// invalid value
value: 'd'
}, errors);
t.is(errors.length, 1);
t.is(node.getNext().getValue(), undefined);
node.setNext({
value: 'a'
}, errors);
errors = [];
t.is(errors.length, 0);
t.is(node.getNext().getValue().toString(), 'a');
});
test('should allow self array type references in property type (version 1)', function (t) {
const TreeNodeValue = Enum.create({
values: ['a', 'b', 'c']
});
const TreeNode = Model.extend({
properties: {
children: 'self[]',
value: TreeNodeValue
}
});
const errors = [];
const node = new TreeNode();
node.setChildren([
{
value: 'a'
},
{
value: 'b'
},
{
value: 'c'
},
{
// invalid value
value: 'd'
}
], errors);
t.is(errors.length, 1);
t.is(TreeNode.wrap(node.getChildren()[0]).getValue().toString(), 'a');
t.is(TreeNode.wrap(node.getChildren()[1]).getValue().toString(), 'b');
t.is(TreeNode.wrap(node.getChildren()[2]).getValue().toString(), 'c');
t.is(TreeNode.wrap(node.getChildren()[3]).getValue(), undefined);
});
test('should allow self array type references in property type (version 2)', function (t) {
const TreeNodeValue = Enum.create({
values: ['a', 'b', 'c']
});
const TreeNode = Model.extend({
properties: {
children: ['self'],
value: TreeNodeValue
}
});
const errors = [];
const node = new TreeNode();
node.setChildren([
{
value: 'a'
},
{
value: 'b'
},
{
value: 'c'
},
{
// invalid value
value: 'd'
}
], errors);
t.is(errors.length, 1);
t.is(TreeNode.wrap(node.getChildren()[0]).getValue().toString(), 'a');
t.is(TreeNode.wrap(node.getChildren()[1]).getValue().toString(), 'b');
t.is(TreeNode.wrap(node.getChildren()[2]).getValue().toString(), 'c');
t.is(TreeNode.wrap(node.getChildren()[3]).getValue(), undefined);
});
test('should handle wrapping array values', function (t) {
const Color = Enum.create({
values: ['red', 'green', 'blue', 'yellow']
});
// original array
const colors = ['red', 'GREEN', 'blue'];
const newColors = Color.convertArray(colors);
// values in original array are untouched
t.is(colors[0], 'red');
t.is(colors[1], 'GREEN');
t.is(colors[2], 'blue');
// values in new array are model instances
t.is(newColors[0], Color.RED);
t.is(newColors[1], Color.GREEN);
t.is(newColors[2], Color.BLUE);
});
test('should handle converting simple type array values', function (t) {
const values = [0, 1];
const newValues = BooleanType.convertArray(values);
t.true(values !== newValues);
// old array is not modified
t.is(values[0], 0);
t.is(values[1], 1);
// values in original array are coerced
t.false(newValues[0]);
t.true(newValues[1]);
});
test('should handle converting Object type array values', function (t) {
const v0 = {a: 1};
const v1 = {a: 2};
const values = [v0, v1];
const newValues = ObjectType.convertArray(values);
t.true(values !== newValues);
t.deepEqual(values, newValues);
});
test('should handle null/undefined when wrapping primitives', function (t) {
t.is(BooleanType.wrap(null), null);
t.is(BooleanType.wrap(undefined), undefined);
t.is(IntegerType.wrap(null), null);
t.is(IntegerType.wrap(undefined), undefined);
t.is(StringType.wrap(null), null);
t.is(StringType.wrap(undefined), undefined);
t.is(NumberType.wrap(null), null);
t.is(NumberType.wrap(undefined), undefined);
t.is(ArrayType.wrap(null), null);
t.is(ArrayType.wrap(undefined), undefined);
t.is(DateType.wrap(null), null);
t.is(DateType.wrap(undefined), undefined);
t.is(FunctionType.wrap(null), null);
t.is(FunctionType.wrap(undefined), undefined);
});
test('should handle wrapping an object that was created with a null prototype', function (t) {
const Person = Model.extend({
properties: {
name: String
}
});
const data = Object.create(null);
data.name = 'Austin';
const errors = [];
const person = Person.wrap(data, errors);
t.is(errors.length, 0);
t.is(person.getName(), 'Austin');
});
test('should handle wrapping property value whose type is Array', function (t) {
const Color = Enum.create({
values: ['red', 'green', 'blue', 'yellow']
});
const Palette = Model.extend({
properties: {
colors: [Color]
}
});
// original array
const colors = ['red', 'GREEN', 'blue'];
const palette = new Palette({
colors: colors
});
// values in original array are not modified
t.is(colors[0], 'red');
t.is(colors[1], 'GREEN');
t.is(colors[2], 'blue');
t.is(palette.data.colors[0], Color.RED);
t.is(palette.data.colors[1], Color.GREEN);
t.is(palette.data.colors[2], Color.BLUE);
t.is(palette.getColors(), palette.getColors());
t.is(palette.getColors()[0], Color.RED);
t.is(palette.getColors()[1], Color.GREEN);
t.is(palette.getColors()[2], Color.BLUE);
palette.addToColors('yellow');
t.is(palette.getColors()[3], Color.YELLOW);
});
test('should handle cleaning model with arrays', function (t) {
const Color = Enum.create({
values: ['red', 'green', 'blue', 'yellow']
});
const Palette = Model.extend({
properties: {
colors: [Color]
}
});
// original array
const colors = ['red', 'green', 'blue'];
const palette = new Palette({
colors: colors
});
palette.getColors().push(Color.YELLOW);
t.is(palette.stringify(), '{"colors":["red","green","blue","yellow"]}');
t.is(palette.getColors().length, 4);
t.is(palette.getColors()[0], Color.RED);
t.is(palette.getColors()[1], Color.GREEN);
t.is(palette.getColors()[2], Color.BLUE);
t.is(palette.getColors()[3], Color.YELLOW);
t.deepEqual(palette.clean(), {
colors: ['red', 'green', 'blue', 'yellow']
});
});
test('should addToProperty method for modifying arrays of primitive types', function (t) {
const Collection = Model.extend({
properties: {
items: []
}
});
// original array
const items = [
'abc',
'def'
];
const collection = new Collection({
items: items
});
t.is(collection.getItems().length, 2);
t.is(collection.getItems()[0], 'abc');
t.is(collection.getItems()[1], 'def');
t.is(collection.stringify(), '{"items":["abc","def"]}');
collection.addToItems('123');
t.is(collection.getItems().length, 3);
t.is(collection.stringify(), '{"items":["abc","def","123"]}');
t.deepEqual(collection.clean(), {
items: [
'abc',
'def',
'123'
]
});
});
test('should addToProperty method for modifying arrays of models', function (t) {
const Person = Model.extend({
properties: {
name: String,
age: IntegerType
}
});
const Group = Model.extend({
properties: {
people: [Person]
}
});
// original array
const people = [
{
name: 'John',
age: 10
},
{
name: 'Alice',
age: 12
}
];
const group = new Group({
people: people
});
group.addToPeople({
name: 'Bob',
age: 14
});
t.is(group.stringify(), '{"people":[{"name":"John","age":10},{"name":"Alice","age":12},{"name":"Bob","age":14}]}');
t.deepEqual(group.clean(), {
people: [
{
name: 'John',
age: 10
},
{
name: 'Alice',
age: 12
},
{
name: 'Bob',
age: 14
}
]
});
});